basic audiobook player

This commit is contained in:
Dr-Blank 2024-05-14 06:13:16 -04:00
parent 097caf8ec2
commit 610d9a2aa0
No known key found for this signature in database
GPG key ID: 7452CC63F210A266
26 changed files with 458 additions and 110 deletions

View file

@ -9,6 +9,7 @@
"**/*.g.dart": true
},
"cSpell.words": [
"audioplayers",
"Autovalidate",
"mocktail",
"riverpod",

View file

@ -37,7 +37,7 @@ AudiobookshelfApi audiobookshelfApi(AudiobookshelfApiRef ref, Uri? baseUrl) {
/// get the api instance for the authenticated user
///
/// if the user is not authenticated throw an error
@riverpod
@Riverpod(keepAlive: true)
AudiobookshelfApi authenticatedApi(AuthenticatedApiRef ref) {
final apiSettings = ref.watch(apiSettingsProvider);
final user = apiSettings.activeUser;

View file

@ -168,7 +168,7 @@ class _AudiobookshelfApiProviderElement
Uri? get baseUrl => (origin as AudiobookshelfApiProvider).baseUrl;
}
String _$authenticatedApiHash() => r'62213d5d0268eeaa2a16211cd60b1b6f0d19dd40';
String _$authenticatedApiHash() => r'd99ea87b21dfb63b5f6fed8f79e835af42f2296f';
/// get the api instance for the authenticated user
///
@ -176,8 +176,7 @@ String _$authenticatedApiHash() => r'62213d5d0268eeaa2a16211cd60b1b6f0d19dd40';
///
/// Copied from [authenticatedApi].
@ProviderFor(authenticatedApi)
final authenticatedApiProvider =
AutoDisposeProvider<AudiobookshelfApi>.internal(
final authenticatedApiProvider = Provider<AudiobookshelfApi>.internal(
authenticatedApi,
name: r'authenticatedApiProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@ -187,7 +186,7 @@ final authenticatedApiProvider =
allTransitiveDependencies: null,
);
typedef AuthenticatedApiRef = AutoDisposeProviderRef<AudiobookshelfApi>;
typedef AuthenticatedApiRef = ProviderRef<AudiobookshelfApi>;
String _$isServerAliveHash() => r'f839350795fbdeb0ca1d5f0c84a9065cac4dd40a';
/// ping the server to check if it is reachable

View file

@ -14,7 +14,7 @@ import 'package:whispering_pages/db/cache_manager.dart';
part 'image_provider.g.dart';
@riverpod
@Riverpod(keepAlive: true)
class CoverImage extends _$CoverImage {
@override
Stream<Uint8List> build(LibraryItem libraryItem) async* {

View file

@ -6,7 +6,7 @@ part of 'image_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$coverImageHash() => r'3f4ef56a2539dd2082e7de55098bed8876098e9f';
String _$coverImageHash() => r'fa97592576b5450053066fcd644f2b5c30d3a5bc';
/// Copied from Dart SDK
class _SystemHash {
@ -29,8 +29,7 @@ class _SystemHash {
}
}
abstract class _$CoverImage
extends BuildlessAutoDisposeStreamNotifier<Uint8List> {
abstract class _$CoverImage extends BuildlessStreamNotifier<Uint8List> {
late final LibraryItem libraryItem;
Stream<Uint8List> build(
@ -82,7 +81,7 @@ class CoverImageFamily extends Family<AsyncValue<Uint8List>> {
/// See also [CoverImage].
class CoverImageProvider
extends AutoDisposeStreamNotifierProviderImpl<CoverImage, Uint8List> {
extends StreamNotifierProviderImpl<CoverImage, Uint8List> {
/// See also [CoverImage].
CoverImageProvider(
LibraryItem libraryItem,
@ -138,8 +137,7 @@ class CoverImageProvider
}
@override
AutoDisposeStreamNotifierProviderElement<CoverImage, Uint8List>
createElement() {
StreamNotifierProviderElement<CoverImage, Uint8List> createElement() {
return _CoverImageProviderElement(this);
}
@ -157,13 +155,13 @@ class CoverImageProvider
}
}
mixin CoverImageRef on AutoDisposeStreamNotifierProviderRef<Uint8List> {
mixin CoverImageRef on StreamNotifierProviderRef<Uint8List> {
/// The parameter `libraryItem` of this provider.
LibraryItem get libraryItem;
}
class _CoverImageProviderElement
extends AutoDisposeStreamNotifierProviderElement<CoverImage, Uint8List>
extends StreamNotifierProviderElement<CoverImage, Uint8List>
with CoverImageRef {
_CoverImageProviderElement(super.provider);

View file

@ -33,7 +33,10 @@ class LibraryItem extends _$LibraryItem {
);
yield cachedItem;
}
final item = await api.items.get(libraryItemId: id);
final item = await api.items.get(
libraryItemId: id,
parameters: const shelfsdk.GetItemReqParams(expanded: true),
);
if (item != null) {
// save to cache
final newFile = await apiResponseCacheManager.putFile(

View file

@ -6,7 +6,7 @@ part of 'library_item_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$libraryItemHash() => r'c7919065062e066a0d086508ca6c44187b0bc257';
String _$libraryItemHash() => r'ce6222e417b43dceed9ea7e5a8b43782755fc117';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -9,13 +9,14 @@ import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk;
import 'package:whispering_pages/api/image_provider.dart';
import 'package:whispering_pages/api/library_item_provider.dart';
import 'package:whispering_pages/extensions/hero_tag_conventions.dart';
import 'package:whispering_pages/features/player/providers/audiobook_player_provider.dart';
import 'package:whispering_pages/router/models/library_item_extras.dart';
import 'package:whispering_pages/settings/app_settings_provider.dart';
import 'package:whispering_pages/theme/theme_from_cover_provider.dart';
import 'package:whispering_pages/widgets/shelves/book_shelf.dart';
import '../widgets/expandable_description.dart';
import '../widgets/library_item_sliver_app_bar.dart';
import '../../../widgets/expandable_description.dart';
import '../../../widgets/library_item_sliver_app_bar.dart';
class LibraryItemPage extends HookConsumerWidget {
const LibraryItemPage({
@ -36,8 +37,13 @@ class LibraryItemPage extends HookConsumerWidget {
: null;
final itemFromApi = ref.watch(libraryItemProvider(itemId));
var itemBookMetadata =
itemFromApi.valueOrNull?.media.metadata as shelfsdk.BookMetadata?;
var itemBookMetadata = itemFromApi.valueOrNull == null
? null
: shelfsdk.BookMetadataExpanded.fromJson(
itemFromApi.valueOrNull!.media.metadata.toJson(),
);
// itemFromApi.valueOrNull?.media.metadata as shelfsdk.BookMetadata?;
final useMaterialThemeOnItemPage =
ref.watch(appSettingsProvider).useMaterialThemeOnItemPage;
@ -87,9 +93,10 @@ class LibraryItemPage extends HookConsumerWidget {
: const SizedBox.shrink(),
),
// a row of actions like play, download, share, etc
const SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
sliver: LibraryItemActions(),
SliverToBoxAdapter(
child: itemFromApi.valueOrNull != null
? LibraryItemActions(item: itemFromApi.valueOrNull!)
: const SizedBox.shrink(),
),
// a expandable section for book description
SliverToBoxAdapter(
@ -122,7 +129,7 @@ class LibraryItemMetadata extends StatelessWidget {
});
final shelfsdk.LibraryItem item;
final shelfsdk.BookMetadata? itemBookMetadata;
final shelfsdk.BookMetadataExpanded? itemBookMetadata;
final shelfsdk.BookMinified? bookDetailsCached;
@override
@ -178,7 +185,7 @@ class LibraryItemMetadata extends StatelessWidget {
/// will add up all the durations of the audio files first
/// then convert them to the given format
String? getDurationFormatted() {
final book = (item.media as shelfsdk.Book?);
final book = (item.media as shelfsdk.BookExpanded?);
if (book == null) {
return null;
}
@ -195,7 +202,7 @@ class LibraryItemMetadata extends StatelessWidget {
/// will add up all the sizes of the audio files first
/// then convert them to MB
String? getSizeFormatted() {
final book = (item.media as shelfsdk.Book?);
final book = (item.media as shelfsdk.BookExpanded?);
if (book == null) {
return null;
}
@ -207,7 +214,7 @@ class LibraryItemMetadata extends StatelessWidget {
/// will return the codec and bitrate of the book
String? getCodecAndBitrate() {
final book = (item.media as shelfsdk.Book?);
final book = (item.media as shelfsdk.BookExpanded?);
if (book == null) {
return null;
}
@ -256,26 +263,46 @@ class _MetadataItem extends StatelessWidget {
}
}
class LibraryItemActions extends StatelessWidget {
const LibraryItemActions({
class LibraryItemActions extends HookConsumerWidget {
LibraryItemActions({
super.key,
});
required this.item,
}) {
book = shelfsdk.BookExpanded.fromJson(item.media.toJson());
}
final shelfsdk.LibraryItem item;
late final shelfsdk.BookExpanded book;
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.read(audiobookPlayerProvider);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
// play/resume button the same widht as image
// play/resume button the same width as image
LayoutBuilder(
builder: (context, constraints) {
return SizedBox(
width: calculateWidth(context, constraints),
// a boxy button with icon and text but little rounded corner
child: ElevatedButton.icon(
onPressed: () {},
onPressed: () async {
// play the book
debugPrint('Pressed play/resume button');
// set the book to the player if not already set
if (player.book != book) {
debugPrint('Setting the book ${book.libraryItemId}');
await player.setSourceAudioBook(book);
ref
.read(audiobookPlayerProvider.notifier)
.notifyListeners();
}
// toggle play/pause
player.togglePlayPause();
},
icon: const Icon(Icons.play_arrow_rounded),
label: const Text('Play/Resume'),
style: ElevatedButton.styleFrom(
@ -350,7 +377,7 @@ class LibraryItemHeroSection extends HookConsumerWidget {
final LibraryItemExtras? extraMap;
final Image? providedCacheImage;
final AsyncValue<shelfsdk.LibraryItem> item;
final shelfsdk.BookMetadata? itemBookMetadata;
final shelfsdk.BookMetadataExpanded? itemBookMetadata;
final shelfsdk.BookMinified? bookDetailsCached;
final AsyncValue<ColorScheme?> coverColorScheme;
@ -490,7 +517,7 @@ class _BookSeries extends StatelessWidget {
required this.bookDetailsCached,
});
final shelfsdk.BookMetadata? itemBookMetadata;
final shelfsdk.BookMetadataExpanded? itemBookMetadata;
final shelfsdk.BookMinified? bookDetailsCached;
@override
@ -536,12 +563,11 @@ class _BookNarrators extends StatelessWidget {
required this.bookDetailsCached,
});
final shelfsdk.BookMetadata? itemBookMetadata;
final shelfsdk.BookMetadataExpanded? itemBookMetadata;
final shelfsdk.BookMinified? bookDetailsCached;
@override
Widget build(BuildContext context) {
String generateNarratorsString() {
final narrators = (itemBookMetadata)?.narrators ?? [];
if (narrators.isEmpty) {
@ -554,7 +580,6 @@ class _BookNarrators extends StatelessWidget {
final themeData = Theme.of(context);
return generateNarratorsString() == ''
? const SizedBox.shrink()
: _HeroSectionSubLabelWithIcon(
@ -654,7 +679,7 @@ class _BookTitle extends StatelessWidget {
});
final LibraryItemExtras? extraMap;
final shelfsdk.BookMetadata? itemBookMetadata;
final shelfsdk.BookMetadataExpanded? itemBookMetadata;
final shelfsdk.BookMinified? bookDetailsCached;
@override
@ -683,10 +708,7 @@ class _BookTitle extends StatelessWidget {
? const SizedBox.shrink()
: Text(
style: themeData.textTheme.titleSmall?.copyWith(
color: themeData
.colorScheme
.onBackground
.withOpacity(0.8),
color: themeData.colorScheme.onBackground.withOpacity(0.8),
),
itemBookMetadata?.subtitle ?? '',
),
@ -702,7 +724,7 @@ class _BookAuthors extends StatelessWidget {
required this.bookDetailsCached,
});
final shelfsdk.BookMetadata? itemBookMetadata;
final shelfsdk.BookMetadataExpanded? itemBookMetadata;
final shelfsdk.BookMinified? bookDetailsCached;
@override

View file

@ -0,0 +1,72 @@
/// a wrapper around the audioplayers package to manage the audio player instance
///
/// this is needed as audiobook can be a list of audio files instead of a single file
library;
import 'package:audioplayers/audioplayers.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
/// will manage the audio player instance
class AudiobookPlayer extends AudioPlayer {
// constructor which takes in the BookExpanded object
AudiobookPlayer(this.token, this.baseUrl) : super() {
// set the source of the player to the first track in the book
}
/// the [BookExpanded] being played
BookExpanded? _book;
/// the [BookExpanded] being played
///
/// to set the book, use [setSourceAudioBook]
BookExpanded? get book => _book;
/// the authentication token to access the [AudioTrack.contentUrl]
final String token;
/// the base url for the audio files
final Uri baseUrl;
// the current index of the audio file in the [book]
final int _currentIndex = 0;
/// sets the current [AudioTrack] as the source of the player
Future<void> setSourceAudioBook(BookExpanded book) async {
// see if the book is the same as the current book
if (_book == book) {
// if the book is the same, do nothing
return;
}
var track = book.tracks[_currentIndex];
var url = '$baseUrl${track.contentUrl}?token=$token';
await setSourceUrl(
url,
// '${track.contentUrl}?token=$token',
mimeType: track.mimeType,
);
_book = book;
}
/// toggles the player between play and pause
Future<void> togglePlayPause() {
// check if book is set
if (_book == null) {
throw StateError('No book is set');
}
return switch (state) {
PlayerState.playing => pause(),
PlayerState.paused ||
PlayerState.stopped ||
PlayerState.completed =>
resume(),
// do nothing if the player is disposed
PlayerState.disposed => throw StateError('Player is disposed'),
};
}
}
void main(List<String> args) {
final AudiobookPlayer abPlayer = AudiobookPlayer('', Uri.parse(''));
print(abPlayer.resume());
}

View file

@ -0,0 +1,85 @@
import 'package:shelfsdk/audiobookshelf_api.dart';
/// will manage the playlist of items
///
/// you are responsible for updating the current index and sub index
class AudiobookPlaylist {
/// list of items in the playlist
final List<BookExpanded> books;
/// current index of the item in the playlist
int _currentIndex;
/// current index of the audio file in the current item
int _subCurrentIndex;
// wrappers for adding and removing items
void add(BookExpanded item) => books.add(item);
void remove(BookExpanded item) => books.remove(item);
void clear() {
books.clear();
_currentIndex = 0;
_subCurrentIndex = 0;
}
// move an item from one index to another
void move(int from, int to) {
final item = books.removeAt(from);
books.insert(to, item);
}
/// the book being played
BookExpanded? get currentBook {
if (_currentIndex >= books.length || _currentIndex < 0 || books.isEmpty) {
return null;
}
return books[_currentIndex];
}
/// of the book in the playlist
int get currentIndex => _currentIndex;
// every time current index changes, we need to update the sub index
set currentIndex(int index) {
// if the index is the same, do nothing
if (_currentIndex == index) {
return;
}
_currentIndex = index;
subCurrentIndex = 0;
}
/// of the audio file in the current book
int get subCurrentIndex => _subCurrentIndex;
set subCurrentIndex(int index) {
if (index < 0) {
index = 0;
}
_subCurrentIndex = index;
}
AudiobookPlaylist({
this.books = const [],
currentIndex = 0,
subCurrentIndex = 0,
}) : _currentIndex = currentIndex,
_subCurrentIndex = subCurrentIndex;
// most important method, gets the audio file to play
// this is needed as a library item is a list of audio files
AudioTrack? getAudioTrack() {
final book = currentBook;
if (book == null) {
return null;
}
if (subCurrentIndex > book.tracks.length || book.tracks.isEmpty) {
return null;
}
return book.tracks[subCurrentIndex];
}
bool get isBookFinished => subCurrentIndex >= currentBook!.tracks.length;
// a method to get the next audio file and advance the sub index
}

View file

@ -0,0 +1,18 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:whispering_pages/features/player/playlist.dart';
part 'playlist_provider.g.dart';
@riverpod
class Playlist extends _$Playlist {
@override
AudiobookPlaylist build() {
return AudiobookPlaylist();
}
void add(BookExpanded item) {
state.add(item);
ref.notifyListeners();
}
}

View file

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'playlist_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$playlistHash() => r'bed4642e4c2de829e4d0630cb5bf92bffeeb1f60';
/// See also [Playlist].
@ProviderFor(Playlist)
final playlistProvider =
AutoDisposeNotifierProvider<Playlist, AudiobookPlaylist>.internal(
Playlist.new,
name: r'playlistProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$playlistHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$Playlist = AutoDisposeNotifier<AudiobookPlaylist>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -0,0 +1,34 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:whispering_pages/api/api_provider.dart';
import 'package:whispering_pages/features/player/audiobook_payer.dart' as abp;
part 'audiobook_player_provider.g.dart';
// @Riverpod(keepAlive: true)
// abp.AudiobookPlayer audiobookPlayer(
// AudiobookPlayerRef ref,
// ) {
// final api = ref.watch(authenticatedApiProvider);
// final player = abp.AudiobookPlayer(api.token!, api.baseUrl);
// ref.onDispose(player.dispose);
// return player;
// }
@Riverpod(keepAlive: true)
class AudiobookPlayer extends _$AudiobookPlayer {
@override
abp.AudiobookPlayer build() {
final api = ref.watch(authenticatedApiProvider);
final player = abp.AudiobookPlayer(api.token!, api.baseUrl);
ref.onDispose(player.dispose);
return player;
}
void notifyListeners() {
ref.notifyListeners();
}
}

View file

@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'audiobook_player_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$audiobookPlayerHash() => r'8cbadcb264382300e63b3dbaf167a3bea1638a6e';
/// See also [AudiobookPlayer].
@ProviderFor(AudiobookPlayer)
final audiobookPlayerProvider =
NotifierProvider<AudiobookPlayer, abp.AudiobookPlayer>.internal(
AudiobookPlayer.new,
name: r'audiobookPlayerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$audiobookPlayerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AudiobookPlayer = Notifier<abp.AudiobookPlayer>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:whispering_pages/pages/app_settings.dart';
import 'package:whispering_pages/pages/home_page.dart';
import 'package:whispering_pages/pages/library_item_page.dart';
import 'package:whispering_pages/features/item_viewer/view/library_item_page.dart';
import 'package:whispering_pages/pages/library_page.dart';
import 'package:whispering_pages/pages/onboarding/onboarding_single_page.dart';

View file

@ -9,7 +9,7 @@ part 'api_settings_provider.g.dart';
final _box = AvailableHiveBoxes.apiSettingsBox;
@riverpod
@Riverpod(keepAlive: true)
class ApiSettings extends _$ApiSettings {
@override
model.ApiSettings build() {

View file

@ -6,12 +6,12 @@ part of 'api_settings_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$apiSettingsHash() => r'5f826922e898bfe13e2536cee62862e83f15b603';
String _$apiSettingsHash() => r'b009ae0d14203a15abaa497287fc68f57eb86bde';
/// See also [ApiSettings].
@ProviderFor(ApiSettings)
final apiSettingsProvider =
AutoDisposeNotifierProvider<ApiSettings, model.ApiSettings>.internal(
NotifierProvider<ApiSettings, model.ApiSettings>.internal(
ApiSettings.new,
name: r'apiSettingsProvider',
debugGetCreateSourceHash:
@ -20,6 +20,6 @@ final apiSettingsProvider =
allTransitiveDependencies: null,
);
typedef _$ApiSettings = AutoDisposeNotifier<model.ApiSettings>;
typedef _$ApiSettings = Notifier<model.ApiSettings>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -6,14 +6,16 @@ import 'package:whispering_pages/api/image_provider.dart';
part 'theme_from_cover_provider.g.dart';
@riverpod
@Riverpod(keepAlive: true)
Future<FutureOr<ColorScheme?>> themeFromCover(
ThemeFromCoverRef ref,
ImageProvider<Object> img, {
Brightness brightness = Brightness.dark,
}) async {
// add deliberate delay to simulate a long running task
// ! add deliberate delay to simulate a long running task
await Future.delayed(500.ms);
debugPrint('Generating color scheme from cover image');
return ColorScheme.fromImageProvider(
provider: img,
brightness: brightness,
@ -41,7 +43,7 @@ Future<FutureOr<ColorScheme?>> themeFromCover(
// return scheme;
}
@riverpod
@Riverpod(keepAlive: true)
FutureOr<ColorScheme?> themeOfLibraryItem(
ThemeOfLibraryItemRef ref,
LibraryItem? item, {

View file

@ -6,7 +6,7 @@ part of 'theme_from_cover_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$themeFromCoverHash() => r'7a364393ffff46152db31f0ed0f8f8b9d58c3b5e';
String _$themeFromCoverHash() => r'bb4c5f32dfe7b6da6f43b8d002267d554cdf98ec';
/// Copied from Dart SDK
class _SystemHash {
@ -75,8 +75,7 @@ class ThemeFromCoverFamily extends Family<AsyncValue<FutureOr<ColorScheme?>>> {
}
/// See also [themeFromCover].
class ThemeFromCoverProvider
extends AutoDisposeFutureProvider<FutureOr<ColorScheme?>> {
class ThemeFromCoverProvider extends FutureProvider<FutureOr<ColorScheme?>> {
/// See also [themeFromCover].
ThemeFromCoverProvider(
ImageProvider<Object> img, {
@ -135,7 +134,7 @@ class ThemeFromCoverProvider
}
@override
AutoDisposeFutureProviderElement<FutureOr<ColorScheme?>> createElement() {
FutureProviderElement<FutureOr<ColorScheme?>> createElement() {
return _ThemeFromCoverProviderElement(this);
}
@ -156,8 +155,7 @@ class ThemeFromCoverProvider
}
}
mixin ThemeFromCoverRef
on AutoDisposeFutureProviderRef<FutureOr<ColorScheme?>> {
mixin ThemeFromCoverRef on FutureProviderRef<FutureOr<ColorScheme?>> {
/// The parameter `img` of this provider.
ImageProvider<Object> get img;
@ -166,7 +164,7 @@ mixin ThemeFromCoverRef
}
class _ThemeFromCoverProviderElement
extends AutoDisposeFutureProviderElement<FutureOr<ColorScheme?>>
extends FutureProviderElement<FutureOr<ColorScheme?>>
with ThemeFromCoverRef {
_ThemeFromCoverProviderElement(super.provider);
@ -177,7 +175,7 @@ class _ThemeFromCoverProviderElement
}
String _$themeOfLibraryItemHash() =>
r'53be78f35075ced924e7b2f3cb7310a09d4cd232';
r'575a390a0ab0e66cf54cb090a358c08847270798';
/// See also [themeOfLibraryItem].
@ProviderFor(themeOfLibraryItem)
@ -225,8 +223,7 @@ class ThemeOfLibraryItemFamily extends Family<AsyncValue<ColorScheme?>> {
}
/// See also [themeOfLibraryItem].
class ThemeOfLibraryItemProvider
extends AutoDisposeFutureProvider<ColorScheme?> {
class ThemeOfLibraryItemProvider extends FutureProvider<ColorScheme?> {
/// See also [themeOfLibraryItem].
ThemeOfLibraryItemProvider(
LibraryItem? item, {
@ -284,7 +281,7 @@ class ThemeOfLibraryItemProvider
}
@override
AutoDisposeFutureProviderElement<ColorScheme?> createElement() {
FutureProviderElement<ColorScheme?> createElement() {
return _ThemeOfLibraryItemProviderElement(this);
}
@ -305,7 +302,7 @@ class ThemeOfLibraryItemProvider
}
}
mixin ThemeOfLibraryItemRef on AutoDisposeFutureProviderRef<ColorScheme?> {
mixin ThemeOfLibraryItemRef on FutureProviderRef<ColorScheme?> {
/// The parameter `item` of this provider.
LibraryItem? get item;
@ -314,8 +311,7 @@ mixin ThemeOfLibraryItemRef on AutoDisposeFutureProviderRef<ColorScheme?> {
}
class _ThemeOfLibraryItemProviderElement
extends AutoDisposeFutureProviderElement<ColorScheme?>
with ThemeOfLibraryItemRef {
extends FutureProviderElement<ColorScheme?> with ThemeOfLibraryItemRef {
_ThemeOfLibraryItemProviderElement(super.provider);
@override

View file

@ -76,15 +76,7 @@ class BookOnShelf extends HookConsumerWidget {
child: Hero(
tag: HeroTagPrefixes.bookCover + item.id + heroTagSuffix,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: coverImage.when(
data: (image) {
// return const BookCoverSkeleton();
if (image.isEmpty) {
return const Icon(Icons.error);
}
var imageWidget = InkWell(
child: InkWell(
onTap: () {
// open the book
context.pushNamed(
@ -99,14 +91,22 @@ class BookOnShelf extends HookConsumerWidget {
),
);
},
child: Image.memory(
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: coverImage.when(
data: (image) {
// return const BookCoverSkeleton();
if (image.isEmpty) {
return const Icon(Icons.error);
}
var imageWidget = Image.memory(
image,
fit: BoxFit.fill,
cacheWidth: (height *
1.2 *
MediaQuery.of(context).devicePixelRatio)
.round(),
),
);
return Container(
decoration: BoxDecoration(
@ -128,6 +128,7 @@ class BookOnShelf extends HookConsumerWidget {
),
),
),
),
// the title and author of the book
// AutoScrollText(
Hero(

View file

@ -6,10 +6,14 @@
#include "generated_plugin_registrant.h"
#include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin");
isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar);

View file

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
isar_flutter_libs
url_launcher_linux
)

View file

@ -65,6 +65,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
audioplayers:
dependency: "direct main"
description:
name: audioplayers
sha256: "752039d6aa752597c98ec212e9759519061759e402e7da59a511f39d43aa07d2"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
audioplayers_android:
dependency: transitive
description:
name: audioplayers_android
sha256: de576b890befe27175c2f511ba8b742bec83765fa97c3ce4282bba46212f58e4
url: "https://pub.dev"
source: hosted
version: "5.0.0"
audioplayers_darwin:
dependency: transitive
description:
name: audioplayers_darwin
sha256: e507887f3ff18d8e5a10a668d7bedc28206b12e10b98347797257c6ae1019c3b
url: "https://pub.dev"
source: hosted
version: "6.0.0"
audioplayers_linux:
dependency: transitive
description:
name: audioplayers_linux
sha256: "3d3d244c90436115417f170426ce768856d8fe4dfc5ed66a049d2890acfa82f9"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
audioplayers_platform_interface:
dependency: transitive
description:
name: audioplayers_platform_interface
sha256: "6834dd48dfb7bc6c2404998ebdd161f79cd3774a7e6779e1348d54a3bfdcfaa5"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
audioplayers_web:
dependency: transitive
description:
name: audioplayers_web
sha256: db8fc420dadf80da18e2286c18e746fb4c3b2c5adbf0c963299dde046828886d
url: "https://pub.dev"
source: hosted
version: "5.0.0"
audioplayers_windows:
dependency: transitive
description:
name: audioplayers_windows
sha256: "8605762dddba992138d476f6a0c3afd9df30ac5b96039929063eceed416795c2"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
auto_scroll_text:
dependency: "direct main"
description:

View file

@ -32,6 +32,7 @@ isar_version: &isar_version ^4.0.0-dev.13 # define the version to be used
dependencies:
animated_list_plus: ^0.5.2
animated_theme_switcher: ^2.0.10
audioplayers: ^6.0.0
auto_scroll_text: ^0.0.7
cached_network_image: ^3.3.1
coast: ^2.0.2

View file

@ -6,10 +6,13 @@
#include "generated_plugin_registrant.h"
#include <audioplayers_windows/audioplayers_windows_plugin.h>
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
AudioplayersWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
IsarFlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(

View file

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_windows
isar_flutter_libs
url_launcher_windows
)