diff --git a/.vscode/settings.json b/.vscode/settings.json index 43111e1..24c9cdb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,7 @@ "**/*.g.dart": true }, "cSpell.words": [ + "audioplayers", "Autovalidate", "mocktail", "riverpod", diff --git a/lib/api/api_provider.dart b/lib/api/api_provider.dart index cfb9bb9..219d353 100644 --- a/lib/api/api_provider.dart +++ b/lib/api/api_provider.dart @@ -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; diff --git a/lib/api/api_provider.g.dart b/lib/api/api_provider.g.dart index 64b6122..d9d27d9 100644 --- a/lib/api/api_provider.g.dart +++ b/lib/api/api_provider.g.dart @@ -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.internal( +final authenticatedApiProvider = Provider.internal( authenticatedApi, name: r'authenticatedApiProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -187,7 +186,7 @@ final authenticatedApiProvider = allTransitiveDependencies: null, ); -typedef AuthenticatedApiRef = AutoDisposeProviderRef; +typedef AuthenticatedApiRef = ProviderRef; String _$isServerAliveHash() => r'f839350795fbdeb0ca1d5f0c84a9065cac4dd40a'; /// ping the server to check if it is reachable diff --git a/lib/api/image_provider.dart b/lib/api/image_provider.dart index 9f1b82f..fdcebbb 100644 --- a/lib/api/image_provider.dart +++ b/lib/api/image_provider.dart @@ -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 build(LibraryItem libraryItem) async* { diff --git a/lib/api/image_provider.g.dart b/lib/api/image_provider.g.dart index dee6a41..87240f0 100644 --- a/lib/api/image_provider.g.dart +++ b/lib/api/image_provider.g.dart @@ -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 { +abstract class _$CoverImage extends BuildlessStreamNotifier { late final LibraryItem libraryItem; Stream build( @@ -82,7 +81,7 @@ class CoverImageFamily extends Family> { /// See also [CoverImage]. class CoverImageProvider - extends AutoDisposeStreamNotifierProviderImpl { + extends StreamNotifierProviderImpl { /// See also [CoverImage]. CoverImageProvider( LibraryItem libraryItem, @@ -138,8 +137,7 @@ class CoverImageProvider } @override - AutoDisposeStreamNotifierProviderElement - createElement() { + StreamNotifierProviderElement createElement() { return _CoverImageProviderElement(this); } @@ -157,13 +155,13 @@ class CoverImageProvider } } -mixin CoverImageRef on AutoDisposeStreamNotifierProviderRef { +mixin CoverImageRef on StreamNotifierProviderRef { /// The parameter `libraryItem` of this provider. LibraryItem get libraryItem; } class _CoverImageProviderElement - extends AutoDisposeStreamNotifierProviderElement + extends StreamNotifierProviderElement with CoverImageRef { _CoverImageProviderElement(super.provider); diff --git a/lib/api/library_item_provider.dart b/lib/api/library_item_provider.dart index 73c9505..ded3fe7 100644 --- a/lib/api/library_item_provider.dart +++ b/lib/api/library_item_provider.dart @@ -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( diff --git a/lib/api/library_item_provider.g.dart b/lib/api/library_item_provider.g.dart index 5057c0e..4c8a2b4 100644 --- a/lib/api/library_item_provider.g.dart +++ b/lib/api/library_item_provider.g.dart @@ -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 { diff --git a/lib/pages/library_item_page.dart b/lib/features/item_viewer/view/library_item_page.dart similarity index 90% rename from lib/pages/library_item_page.dart rename to lib/features/item_viewer/view/library_item_page.dart index 23718fa..274a8f7 100644 --- a/lib/pages/library_item_page.dart +++ b/lib/features/item_viewer/view/library_item_page.dart @@ -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; } @@ -246,7 +253,7 @@ class _MetadataItem extends StatelessWidget { Text( style: themeData.textTheme.bodySmall?.copyWith( color: themeData.colorScheme.onBackground.withOpacity(0.7), - ), + ), title, maxLines: 1, overflow: TextOverflow.ellipsis, @@ -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 item; - final shelfsdk.BookMetadata? itemBookMetadata; + final shelfsdk.BookMetadataExpanded? itemBookMetadata; final shelfsdk.BookMinified? bookDetailsCached; final AsyncValue 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) { @@ -553,7 +579,6 @@ class _BookNarrators extends StatelessWidget { } final themeData = Theme.of(context); - return generateNarratorsString() == '' ? const SizedBox.shrink() @@ -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,11 +708,8 @@ 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 diff --git a/lib/features/player/audiobook_payer.dart b/lib/features/player/audiobook_payer.dart new file mode 100644 index 0000000..22d5701 --- /dev/null +++ b/lib/features/player/audiobook_payer.dart @@ -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 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 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 args) { + final AudiobookPlayer abPlayer = AudiobookPlayer('', Uri.parse('')); + print(abPlayer.resume()); +} diff --git a/lib/features/player/playlist.dart b/lib/features/player/playlist.dart new file mode 100644 index 0000000..61c2326 --- /dev/null +++ b/lib/features/player/playlist.dart @@ -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 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 +} diff --git a/lib/features/player/playlist_provider.dart b/lib/features/player/playlist_provider.dart new file mode 100644 index 0000000..71a49ae --- /dev/null +++ b/lib/features/player/playlist_provider.dart @@ -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(); + } +} diff --git a/lib/features/player/playlist_provider.g.dart b/lib/features/player/playlist_provider.g.dart new file mode 100644 index 0000000..061289c --- /dev/null +++ b/lib/features/player/playlist_provider.g.dart @@ -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.internal( + Playlist.new, + name: r'playlistProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$playlistHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$Playlist = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/features/player/providers/audiobook_player_provider.dart b/lib/features/player/providers/audiobook_player_provider.dart new file mode 100644 index 0000000..7759744 --- /dev/null +++ b/lib/features/player/providers/audiobook_player_provider.dart @@ -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(); + } +} diff --git a/lib/features/player/providers/audiobook_player_provider.g.dart b/lib/features/player/providers/audiobook_player_provider.g.dart new file mode 100644 index 0000000..e4eb1bf --- /dev/null +++ b/lib/features/player/providers/audiobook_player_provider.g.dart @@ -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.internal( + AudiobookPlayer.new, + name: r'audiobookPlayerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$audiobookPlayerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$AudiobookPlayer = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/router/router.dart b/lib/router/router.dart index d485680..4545727 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -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'; diff --git a/lib/settings/api_settings_provider.dart b/lib/settings/api_settings_provider.dart index 4b2e0f1..0d47f1a 100644 --- a/lib/settings/api_settings_provider.dart +++ b/lib/settings/api_settings_provider.dart @@ -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() { diff --git a/lib/settings/api_settings_provider.g.dart b/lib/settings/api_settings_provider.g.dart index f8b7f93..e2e673c 100644 --- a/lib/settings/api_settings_provider.g.dart +++ b/lib/settings/api_settings_provider.g.dart @@ -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.internal( + NotifierProvider.internal( ApiSettings.new, name: r'apiSettingsProvider', debugGetCreateSourceHash: @@ -20,6 +20,6 @@ final apiSettingsProvider = allTransitiveDependencies: null, ); -typedef _$ApiSettings = AutoDisposeNotifier; +typedef _$ApiSettings = Notifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/theme/theme_from_cover_provider.dart b/lib/theme/theme_from_cover_provider.dart index 9024377..ea50661 100644 --- a/lib/theme/theme_from_cover_provider.dart +++ b/lib/theme/theme_from_cover_provider.dart @@ -6,14 +6,16 @@ import 'package:whispering_pages/api/image_provider.dart'; part 'theme_from_cover_provider.g.dart'; -@riverpod +@Riverpod(keepAlive: true) Future> themeFromCover( ThemeFromCoverRef ref, ImageProvider 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> themeFromCover( // return scheme; } -@riverpod +@Riverpod(keepAlive: true) FutureOr themeOfLibraryItem( ThemeOfLibraryItemRef ref, LibraryItem? item, { diff --git a/lib/theme/theme_from_cover_provider.g.dart b/lib/theme/theme_from_cover_provider.g.dart index 2e2572e..a2c3594 100644 --- a/lib/theme/theme_from_cover_provider.g.dart +++ b/lib/theme/theme_from_cover_provider.g.dart @@ -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>> { } /// See also [themeFromCover]. -class ThemeFromCoverProvider - extends AutoDisposeFutureProvider> { +class ThemeFromCoverProvider extends FutureProvider> { /// See also [themeFromCover]. ThemeFromCoverProvider( ImageProvider img, { @@ -135,7 +134,7 @@ class ThemeFromCoverProvider } @override - AutoDisposeFutureProviderElement> createElement() { + FutureProviderElement> createElement() { return _ThemeFromCoverProviderElement(this); } @@ -156,8 +155,7 @@ class ThemeFromCoverProvider } } -mixin ThemeFromCoverRef - on AutoDisposeFutureProviderRef> { +mixin ThemeFromCoverRef on FutureProviderRef> { /// The parameter `img` of this provider. ImageProvider get img; @@ -166,7 +164,7 @@ mixin ThemeFromCoverRef } class _ThemeFromCoverProviderElement - extends AutoDisposeFutureProviderElement> + extends FutureProviderElement> 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> { } /// See also [themeOfLibraryItem]. -class ThemeOfLibraryItemProvider - extends AutoDisposeFutureProvider { +class ThemeOfLibraryItemProvider extends FutureProvider { /// See also [themeOfLibraryItem]. ThemeOfLibraryItemProvider( LibraryItem? item, { @@ -284,7 +281,7 @@ class ThemeOfLibraryItemProvider } @override - AutoDisposeFutureProviderElement createElement() { + FutureProviderElement createElement() { return _ThemeOfLibraryItemProviderElement(this); } @@ -305,7 +302,7 @@ class ThemeOfLibraryItemProvider } } -mixin ThemeOfLibraryItemRef on AutoDisposeFutureProviderRef { +mixin ThemeOfLibraryItemRef on FutureProviderRef { /// The parameter `item` of this provider. LibraryItem? get item; @@ -314,8 +311,7 @@ mixin ThemeOfLibraryItemRef on AutoDisposeFutureProviderRef { } class _ThemeOfLibraryItemProviderElement - extends AutoDisposeFutureProviderElement - with ThemeOfLibraryItemRef { + extends FutureProviderElement with ThemeOfLibraryItemRef { _ThemeOfLibraryItemProviderElement(super.provider); @override diff --git a/lib/widgets/shelves/book_shelf.dart b/lib/widgets/shelves/book_shelf.dart index e960d6a..69a22a1 100644 --- a/lib/widgets/shelves/book_shelf.dart +++ b/lib/widgets/shelves/book_shelf.dart @@ -76,53 +76,54 @@ 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( - onTap: () { - // open the book - context.pushNamed( - Routes.libraryItem.name, - pathParameters: { - Routes.libraryItem.pathParamName!: item.id, - }, - extra: LibraryItemExtras( - book: book, - heroTagSuffix: heroTagSuffix, - coverImage: coverImage.valueOrNull, - ), - ); - }, - child: Image.memory( + child: InkWell( + onTap: () { + // open the book + context.pushNamed( + Routes.libraryItem.name, + pathParameters: { + Routes.libraryItem.pathParamName!: item.id, + }, + extra: LibraryItemExtras( + book: book, + heroTagSuffix: heroTagSuffix, + coverImage: coverImage.valueOrNull, + ), + ); + }, + + 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( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - child: imageWidget, - ); - }, - loading: () { - return const Center(child: BookCoverSkeleton()); - }, - error: (error, stack) { - return const Icon(Icons.error); - }, + ); + return Container( + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + child: imageWidget, + ); + }, + loading: () { + return const Center(child: BookCoverSkeleton()); + }, + error: (error, stack) { + return const Icon(Icons.error); + }, + ), ), ), ), diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index bfc0d08..35ac2ac 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include 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); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 6237f02..6d6f2a8 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_linux isar_flutter_libs url_launcher_linux ) diff --git a/pubspec.lock b/pubspec.lock index 1440e05..c353acd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index cec9aea..c3dc892 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 65cb334..ab3246f 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,10 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AudioplayersWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); IsarFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 439b324..eaeb6e5 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_windows isar_flutter_libs url_launcher_windows )