diff --git a/lib/api/image_provider.dart b/lib/api/image_provider.dart index 42b9e4b..9707542 100644 --- a/lib/api/image_provider.dart +++ b/lib/api/image_provider.dart @@ -4,6 +4,7 @@ import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/api/api_provider.dart'; +import 'package:vaani/api/library_item_provider.dart'; import 'package:vaani/db/cache_manager.dart'; /// provides cover images for the audiobooks @@ -19,52 +20,53 @@ final _logger = Logger('cover_image_provider'); @Riverpod(keepAlive: true) class CoverImage extends _$CoverImage { @override - Stream build(LibraryItem libraryItem) async* { + Stream build(String itemId) async* { final api = ref.watch(authenticatedApiProvider); // ! artifical delay for testing // await Future.delayed(const Duration(seconds: 2)); // try to get the image from the cache - final file = await imageCacheManager.getFileFromMemory(libraryItem.id) ?? - await imageCacheManager.getFileFromCache(libraryItem.id); + final file = await imageCacheManager.getFileFromMemory(itemId) ?? + await imageCacheManager.getFileFromCache(itemId); if (file != null) { // if the image is in the cache, yield it _logger.fine( - 'cover image found in cache for ${libraryItem.id} at ${file.file.path}', + 'cover image found in cache for $itemId at ${file.file.path}', ); yield await file.file.readAsBytes(); + final libraryItem = await ref.watch(libraryItemProvider(itemId).future); // return if no need to fetch from the server if (libraryItem.updatedAt.isBefore(await file.file.lastModified())) { _logger.fine( - 'cover image is up to date for ${libraryItem.id}, no need to fetch from the server', + 'cover image is up to date for $itemId, no need to fetch from the server', ); return; } else { _logger.fine( - 'cover image stale for ${libraryItem.id}, fetching from the server', + 'cover image stale for $itemId, fetching from the server', ); } } else { - _logger.fine('cover image not found in cache for ${libraryItem.id}'); + _logger.fine('cover image not found in cache for $itemId'); } // check if the image is in the cache final coverImage = await api.items.getCover( - libraryItemId: libraryItem.id, + libraryItemId: itemId, parameters: const GetImageReqParams(width: 1200), ); // save the image to the cache if (coverImage != null) { final newFile = await imageCacheManager.putFile( - libraryItem.id, + itemId, coverImage, - key: libraryItem.id, + key: itemId, fileExtension: 'jpg', ); _logger.fine( - 'cover image fetched for for ${libraryItem.id}, file time: ${await newFile.lastModified()}', + 'cover image fetched for for $itemId, file time: ${await newFile.lastModified()}', ); } diff --git a/lib/api/image_provider.g.dart b/lib/api/image_provider.g.dart index 74f9d8e..5fa646e 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'702afafa217dfcbb290837caf21cc1ef54defd55'; +String _$coverImageHash() => r'89cc4783cbc76bb41beae34384d92fb277135c75'; /// Copied from Dart SDK class _SystemHash { @@ -30,10 +30,10 @@ class _SystemHash { } abstract class _$CoverImage extends BuildlessStreamNotifier { - late final LibraryItem libraryItem; + late final String itemId; Stream build( - LibraryItem libraryItem, + String itemId, ); } @@ -48,10 +48,10 @@ class CoverImageFamily extends Family> { /// See also [CoverImage]. CoverImageProvider call( - LibraryItem libraryItem, + String itemId, ) { return CoverImageProvider( - libraryItem, + itemId, ); } @@ -60,7 +60,7 @@ class CoverImageFamily extends Family> { covariant CoverImageProvider provider, ) { return call( - provider.libraryItem, + provider.itemId, ); } @@ -84,9 +84,9 @@ class CoverImageProvider extends StreamNotifierProviderImpl { /// See also [CoverImage]. CoverImageProvider( - LibraryItem libraryItem, + String itemId, ) : this._internal( - () => CoverImage()..libraryItem = libraryItem, + () => CoverImage()..itemId = itemId, from: coverImageProvider, name: r'coverImageProvider', debugGetCreateSourceHash: @@ -96,7 +96,7 @@ class CoverImageProvider dependencies: CoverImageFamily._dependencies, allTransitiveDependencies: CoverImageFamily._allTransitiveDependencies, - libraryItem: libraryItem, + itemId: itemId, ); CoverImageProvider._internal( @@ -106,17 +106,17 @@ class CoverImageProvider required super.allTransitiveDependencies, required super.debugGetCreateSourceHash, required super.from, - required this.libraryItem, + required this.itemId, }) : super.internal(); - final LibraryItem libraryItem; + final String itemId; @override Stream runNotifierBuild( covariant CoverImage notifier, ) { return notifier.build( - libraryItem, + itemId, ); } @@ -125,13 +125,13 @@ class CoverImageProvider return ProviderOverride( origin: this, override: CoverImageProvider._internal( - () => create()..libraryItem = libraryItem, + () => create()..itemId = itemId, from: from, name: null, dependencies: null, allTransitiveDependencies: null, debugGetCreateSourceHash: null, - libraryItem: libraryItem, + itemId: itemId, ), ); } @@ -143,21 +143,21 @@ class CoverImageProvider @override bool operator ==(Object other) { - return other is CoverImageProvider && other.libraryItem == libraryItem; + return other is CoverImageProvider && other.itemId == itemId; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, libraryItem.hashCode); + hash = _SystemHash.combine(hash, itemId.hashCode); return _SystemHash.finish(hash); } } mixin CoverImageRef on StreamNotifierProviderRef { - /// The parameter `libraryItem` of this provider. - LibraryItem get libraryItem; + /// The parameter `itemId` of this provider. + String get itemId; } class _CoverImageProviderElement @@ -166,7 +166,7 @@ class _CoverImageProviderElement _CoverImageProviderElement(super.provider); @override - LibraryItem get libraryItem => (origin as CoverImageProvider).libraryItem; + String get itemId => (origin as CoverImageProvider).itemId; } // 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/api/library_item_provider.dart b/lib/api/library_item_provider.dart index cf81ebd..013f62e 100644 --- a/lib/api/library_item_provider.dart +++ b/lib/api/library_item_provider.dart @@ -13,7 +13,7 @@ part 'library_item_provider.g.dart'; final _logger = Logger('LibraryItemProvider'); /// provides the library item for the given id -@riverpod +@Riverpod(keepAlive: true) class LibraryItem extends _$LibraryItem { @override Stream build(String id) async* { @@ -22,7 +22,7 @@ class LibraryItem extends _$LibraryItem { _logger.fine('LibraryItemProvider fetching library item: $id'); // ! this is a mock delay - // await Future.delayed(const Duration(seconds: 10)); + // await Future.delayed(const Duration(seconds: 150)); // look for the item in the cache final key = CacheKey.libraryItem(id); diff --git a/lib/api/library_item_provider.g.dart b/lib/api/library_item_provider.g.dart index 80370c6..af297d0 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'fa3f8309349c5b1b777f1bc919616e51c3f5b520'; +String _$libraryItemHash() => r'a3cfa7f912e9498a70b5782899018b6964d6445c'; /// Copied from Dart SDK class _SystemHash { @@ -30,7 +30,7 @@ class _SystemHash { } abstract class _$LibraryItem - extends BuildlessAutoDisposeStreamNotifier { + extends BuildlessStreamNotifier { late final String id; Stream build( @@ -92,8 +92,8 @@ class LibraryItemFamily /// provides the library item for the given id /// /// Copied from [LibraryItem]. -class LibraryItemProvider extends AutoDisposeStreamNotifierProviderImpl< - LibraryItem, shelfsdk.LibraryItemExpanded> { +class LibraryItemProvider extends StreamNotifierProviderImpl { /// provides the library item for the given id /// /// Copied from [LibraryItem]. @@ -151,8 +151,8 @@ class LibraryItemProvider extends AutoDisposeStreamNotifierProviderImpl< } @override - AutoDisposeStreamNotifierProviderElement createElement() { + StreamNotifierProviderElement + createElement() { return _LibraryItemProviderElement(this); } @@ -171,14 +171,13 @@ class LibraryItemProvider extends AutoDisposeStreamNotifierProviderImpl< } mixin LibraryItemRef - on AutoDisposeStreamNotifierProviderRef { + on StreamNotifierProviderRef { /// The parameter `id` of this provider. String get id; } -class _LibraryItemProviderElement - extends AutoDisposeStreamNotifierProviderElement with LibraryItemRef { +class _LibraryItemProviderElement extends StreamNotifierProviderElement< + LibraryItem, shelfsdk.LibraryItemExpanded> with LibraryItemRef { _LibraryItemProviderElement(super.provider); @override diff --git a/lib/features/explore/view/explore_page.dart b/lib/features/explore/view/explore_page.dart index e25db0e..2a46983 100644 --- a/lib/features/explore/view/explore_page.dart +++ b/lib/features/explore/view/explore_page.dart @@ -234,7 +234,7 @@ class BookSearchResultMini extends HookConsumerWidget { final item = ref.watch(libraryItemProvider(book.libraryItemId)).valueOrNull; final image = item == null ? const AsyncValue.loading() - : ref.watch(coverImageProvider(item)); + : ref.watch(coverImageProvider(item.id)); return ListTile( leading: SizedBox( width: 50, diff --git a/lib/features/item_viewer/view/library_item_actions.dart b/lib/features/item_viewer/view/library_item_actions.dart index 1e96e14..7bc4c9c 100644 --- a/lib/features/item_viewer/view/library_item_actions.dart +++ b/lib/features/item_viewer/view/library_item_actions.dart @@ -26,17 +26,19 @@ import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/utils.dart'; class LibraryItemActions extends HookConsumerWidget { - LibraryItemActions({ + const LibraryItemActions({ super.key, - required this.item, - }) { - book = item.media.asBookExpanded; - } + required this.id, + }); + + final String id; - final shelfsdk.LibraryItemExpanded item; - late final shelfsdk.BookExpanded book; @override Widget build(BuildContext context, WidgetRef ref) { + final item = ref.watch(libraryItemProvider(id)).valueOrNull; + if (item == null) { + return const SizedBox.shrink(); + } final downloadHistory = ref.watch(downloadHistoryProvider(group: item.id)); final apiSettings = ref.watch(apiSettingsProvider); diff --git a/lib/features/item_viewer/view/library_item_hero_section.dart b/lib/features/item_viewer/view/library_item_hero_section.dart index 88ef7b0..ea21da6 100644 --- a/lib/features/item_viewer/view/library_item_hero_section.dart +++ b/lib/features/item_viewer/view/library_item_hero_section.dart @@ -5,6 +5,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk; import 'package:vaani/api/image_provider.dart'; +import 'package:vaani/api/library_item_provider.dart'; import 'package:vaani/constants/hero_tag_conventions.dart'; import 'package:vaani/features/item_viewer/view/library_item_page.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; @@ -14,125 +15,120 @@ import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/duration_format.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; +import 'package:vaani/theme/theme_from_cover_provider.dart'; class LibraryItemHeroSection extends HookConsumerWidget { const LibraryItemHeroSection({ super.key, required this.itemId, required this.extraMap, - required this.providedCacheImage, - required this.item, - required this.itemBookMetadata, - required this.bookDetailsCached, - required this.coverColorScheme, }); final String itemId; final LibraryItemExtras? extraMap; - final Image? providedCacheImage; - final AsyncValue item; - final shelfsdk.BookMetadataExpanded? itemBookMetadata; - final shelfsdk.BookMinified? bookDetailsCached; - final AsyncValue coverColorScheme; @override Widget build(BuildContext context, WidgetRef ref) { return SliverToBoxAdapter( - child: LayoutBuilder( - builder: (context, constraints) { - return Container( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - // book cover - LayoutBuilder( - builder: (context, constraints) { - return SizedBox( - width: calculateWidth(context, constraints), - child: Column( - children: [ - Hero( - tag: HeroTagPrefixes.bookCover + - itemId + - (extraMap?.heroTagSuffix ?? ''), - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: _BookCover( - itemId: itemId, - extraMap: extraMap, - providedCacheImage: providedCacheImage, - coverColorScheme: coverColorScheme.valueOrNull, - item: item, - ), - ), - ), - // a progress bar if available - item.when( - data: (libraryItem) { - return Padding( - padding: const EdgeInsets.only( - top: 8.0, - right: 8.0, - left: 8.0, - ), - child: _LibraryItemProgressIndicator( - libraryItem: libraryItem, - ), - ); - }, - loading: () => const SizedBox.shrink(), - error: (error, stack) => const SizedBox.shrink(), - ), - ], - ), - ); - }, - ), - // book details - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _BookTitle( - extraMap: extraMap, - itemBookMetadata: itemBookMetadata, - bookDetailsCached: bookDetailsCached, - ), - Container( - margin: const EdgeInsets.symmetric(vertical: 16), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - // authors info if available - _BookAuthors( - itemBookMetadata: itemBookMetadata, - bookDetailsCached: bookDetailsCached, - ), - // narrators info if available - _BookNarrators( - itemBookMetadata: itemBookMetadata, - bookDetailsCached: bookDetailsCached, - ), - // series info if available - _BookSeries( - itemBookMetadata: itemBookMetadata, - bookDetailsCached: bookDetailsCached, - ), - ], - ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // book cover + LayoutBuilder( + builder: (context, constraints) { + return SizedBox( + width: calculateWidth(context, constraints), + child: Column( + children: [ + Hero( + tag: HeroTagPrefixes.bookCover + + itemId + + (extraMap?.heroTagSuffix ?? ''), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: _BookCover( + itemId: itemId, ), - ], + ), ), - ), + // a progress bar + Padding( + padding: const EdgeInsets.only( + top: 8.0, + right: 8.0, + left: 8.0, + ), + child: _LibraryItemProgressIndicator( + id: itemId, + ), + ), + ], ), - ], + ); + }, + ), + // book details + _BookDetails(id: itemId, extraMap: extraMap), + ], + ), + ); + } +} + +class _BookDetails extends HookConsumerWidget { + const _BookDetails({ + super.key, + required this.id, + this.extraMap, + }); + + final String id; + final LibraryItemExtras? extraMap; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final itemFromApi = ref.watch(libraryItemProvider(id)); + + final itemBookMetadata = + itemFromApi.valueOrNull?.media.metadata.asBookMetadataExpanded; + + return Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _BookTitle( + extraMap: extraMap, + itemBookMetadata: itemBookMetadata, ), - ); - }, + Container( + margin: const EdgeInsets.symmetric(vertical: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // authors info if available + _BookAuthors( + itemBookMetadata: itemBookMetadata, + bookDetailsCached: extraMap?.book, + ), + // narrators info if available + _BookNarrators( + itemBookMetadata: itemBookMetadata, + bookDetailsCached: extraMap?.book, + ), + // series info if available + _BookSeries( + itemBookMetadata: itemBookMetadata, + bookDetailsCached: extraMap?.book, + ), + ], + ), + ), + ], + ), ), ); } @@ -141,14 +137,19 @@ class LibraryItemHeroSection extends HookConsumerWidget { class _LibraryItemProgressIndicator extends HookConsumerWidget { const _LibraryItemProgressIndicator({ super.key, - required this.libraryItem, + required this.id, }); - final shelfsdk.LibraryItemExpanded libraryItem; + final String id; @override Widget build(BuildContext context, WidgetRef ref) { final player = ref.watch(audiobookPlayerProvider); + final libraryItem = ref.watch(libraryItemProvider(id)).valueOrNull; + if (libraryItem == null) { + return const SizedBox.shrink(); + } + final mediaProgress = libraryItem.userMediaProgress; if (mediaProgress == null && player.book?.libraryItemId != libraryItem.id) { return const SizedBox.shrink(); @@ -188,6 +189,8 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget { LinearProgressIndicator( value: progress.clamp(0.03, 1), borderRadius: BorderRadius.circular(8), + semanticsLabel: 'Book progress', + semanticsValue: '${progressInPercent.toStringAsFixed(2)}%', ), const SizedBox.square( dimension: 4.0, @@ -341,24 +344,30 @@ class _BookCover extends HookConsumerWidget { const _BookCover({ super.key, required this.itemId, - required this.extraMap, - required this.providedCacheImage, - required this.item, - this.coverColorScheme, }); final String itemId; - final LibraryItemExtras? extraMap; - final Image? providedCacheImage; - final AsyncValue item; - final ColorScheme? coverColorScheme; @override Widget build(BuildContext context, WidgetRef ref) { + final coverImage = ref.watch(coverImageProvider(itemId)); final themeData = Theme.of(context); + // final item = ref.watch(libraryItemProvider(itemId)); final useMaterialThemeOnItemPage = ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage; + ColorScheme? coverColorScheme; + if (useMaterialThemeOnItemPage) { + coverColorScheme = ref + .watch( + themeOfLibraryItemProvider( + itemId, + brightness: Theme.of(context).brightness, + ), + ) + .valueOrNull; + } + return ThemeSwitcher( builder: (context) { // change theme after 2 seconds @@ -368,7 +377,7 @@ class _BookCover extends HookConsumerWidget { ThemeSwitcher.of(context).changeTheme( theme: coverColorScheme != null ? ThemeData.from( - colorScheme: coverColorScheme!, + colorScheme: coverColorScheme, textTheme: themeData.textTheme, ) : themeData, @@ -378,42 +387,27 @@ class _BookCover extends HookConsumerWidget { } }); } - return providedCacheImage ?? - item.when( - data: (libraryItem) { - final coverImage = ref.watch(coverImageProvider(libraryItem)); - return Stack( - children: [ - coverImage.when( - data: (image) { - // return const BookCoverSkeleton(); - if (image.isEmpty) { - return const Icon(Icons.error); - } - // cover 80% of parent height - return Image.memory( - image, - fit: BoxFit.cover, - // cacheWidth: (height * - // MediaQuery.of(context).devicePixelRatio) - // .round(), - ); - }, - loading: () { - return const Center( - child: BookCoverSkeleton(), - ); - }, - error: (error, stack) { - return const Icon(Icons.error); - }, - ), - ], - ); - }, - error: (error, stack) => const Icon(Icons.error), - loading: () => const Center(child: BookCoverSkeleton()), + return coverImage.when( + data: (image) { + // return const BookCoverSkeleton(); + if (image.isEmpty) { + return const Icon(Icons.error); + } + + return Image.memory( + image, + fit: BoxFit.cover, + ); + }, + loading: () { + return const Center( + child: BookCoverSkeleton(), ); + }, + error: (error, stack) { + return const Center(child: Icon(Icons.error)); + }, + ); }, ); } @@ -424,12 +418,10 @@ class _BookTitle extends StatelessWidget { super.key, required this.extraMap, required this.itemBookMetadata, - required this.bookDetailsCached, }); final LibraryItemExtras? extraMap; final shelfsdk.BookMetadataExpanded? itemBookMetadata; - final shelfsdk.BookMinified? bookDetailsCached; @override Widget build(BuildContext context) { @@ -449,7 +441,7 @@ class _BookTitle extends StatelessWidget { // pauseBetween: 150.ms, // numberOfReps: 3, style: themeData.textTheme.headlineLarge, - itemBookMetadata?.title ?? bookDetailsCached?.metadata.title ?? '', + itemBookMetadata?.title ?? extraMap?.book?.metadata.title ?? '', ), ), // subtitle if available @@ -482,7 +474,7 @@ class _BookAuthors extends StatelessWidget { String generateAuthorsString() { final authors = (itemBookMetadata)?.authors ?? []; if (authors.isEmpty) { - return (bookDetailsCached?.metadata as shelfsdk.BookMetadataMinified?) + return (bookDetailsCached?.metadata.asBookMetadataMinified) ?.authorName ?? ''; } diff --git a/lib/features/item_viewer/view/library_item_metadata.dart b/lib/features/item_viewer/view/library_item_metadata.dart index 1eef261..5874a92 100644 --- a/lib/features/item_viewer/view/library_item_metadata.dart +++ b/lib/features/item_viewer/view/library_item_metadata.dart @@ -1,20 +1,65 @@ import 'package:flutter/material.dart'; -import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:vaani/api/library_item_provider.dart'; +import 'package:vaani/shared/extensions/model_conversions.dart'; -class LibraryItemMetadata extends StatelessWidget { +class LibraryItemMetadata extends HookConsumerWidget { const LibraryItemMetadata({ super.key, - required this.item, - this.itemBookMetadata, - this.bookDetailsCached, + required this.id, }); - final shelfsdk.LibraryItemExpanded item; - final shelfsdk.BookMetadataExpanded? itemBookMetadata; - final shelfsdk.BookMinified? bookDetailsCached; + final String id; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final item = ref.watch(libraryItemProvider(id)).valueOrNull; + + /// formats the duration of the book as `10h 30m` + /// + /// will add up all the durations of the audio files first + /// then convert them to the given format + String? getDurationFormatted() { + final book = (item?.media.asBookExpanded); + if (book == null) { + return null; + } + final duration = book.audioFiles + .map((e) => e.duration) + .reduce((value, element) => value + element); + final hours = duration.inHours; + final minutes = duration.inMinutes.remainder(60); + return '${hours}h ${minutes}m'; + } + + /// will return the size of the book in MB + /// + /// will add up all the sizes of the audio files first + /// then convert them to MB + String? getSizeFormatted() { + final book = (item?.media.asBookExpanded); + if (book == null) { + return null; + } + final size = book.audioFiles + .map((e) => e.metadata.size) + .reduce((value, element) => value + element); + return '${size / 1024 ~/ 1024} MB'; + } + + /// will return the codec and bitrate of the book + String? getCodecAndBitrate() { + final book = (item?.media.asBookExpanded); + if (book == null) { + return null; + } + final codec = book.audioFiles.first.codec.toUpperCase(); + // final bitrate = book.audioFiles.first.bitRate; + return codec; + } + + final itemBookMetadata = item?.media.metadata.asBookMetadataExpanded; + final children = [ // duration of the book _MetadataItem( @@ -59,49 +104,6 @@ class LibraryItemMetadata extends StatelessWidget { ), ); } - - /// formats the duration of the book as `10h 30m` - /// - /// 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.BookExpanded?); - if (book == null) { - return null; - } - final duration = book.audioFiles - .map((e) => e.duration) - .reduce((value, element) => value + element); - final hours = duration.inHours; - final minutes = duration.inMinutes.remainder(60); - return '${hours}h ${minutes}m'; - } - - /// will return the size of the book in MB - /// - /// will add up all the sizes of the audio files first - /// then convert them to MB - String? getSizeFormatted() { - final book = (item.media as shelfsdk.BookExpanded?); - if (book == null) { - return null; - } - final size = book.audioFiles - .map((e) => e.metadata.size) - .reduce((value, element) => value + element); - return '${size / 1024 ~/ 1024} MB'; - } - - /// will return the codec and bitrate of the book - String? getCodecAndBitrate() { - final book = (item.media as shelfsdk.BookExpanded?); - if (book == null) { - return null; - } - final codec = book.audioFiles.first.codec.toUpperCase(); - // final bitrate = book.audioFiles.first.bitRate; - return codec; - } } /// key-value pair to display as column diff --git a/lib/features/item_viewer/view/library_item_page.dart b/lib/features/item_viewer/view/library_item_page.dart index d682f4a..9e98003 100644 --- a/lib/features/item_viewer/view/library_item_page.dart +++ b/lib/features/item_viewer/view/library_item_page.dart @@ -8,10 +8,7 @@ import 'package:vaani/api/library_item_provider.dart'; import 'package:vaani/features/item_viewer/view/library_item_sliver_app_bar.dart'; import 'package:vaani/features/player/providers/player_form.dart'; import 'package:vaani/router/models/library_item_extras.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; -import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/widgets/expandable_description.dart'; -import 'package:vaani/theme/theme_from_cover_provider.dart'; import 'library_item_actions.dart'; import 'library_item_hero_section.dart'; @@ -28,33 +25,9 @@ class LibraryItemPage extends HookConsumerWidget { final Object? extra; @override Widget build(BuildContext context, WidgetRef ref) { - final extraMap = + final additionalItemData = extra is LibraryItemExtras ? extra as LibraryItemExtras : null; - final bookDetailsCached = extraMap?.book; - final providedCacheImage = extraMap?.coverImage != null - ? Image.memory(extraMap!.coverImage!) - : null; - final itemFromApi = ref.watch(libraryItemProvider(itemId)); - - var itemBookMetadata = - itemFromApi.valueOrNull?.media.metadata.asBookMetadataExpanded; - - final useMaterialThemeOnItemPage = - ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage; - AsyncValue coverColorScheme = const AsyncValue.loading(); - if (useMaterialThemeOnItemPage) { - coverColorScheme = ref.watch( - themeOfLibraryItemProvider( - itemFromApi.valueOrNull, - brightness: Theme.of(context).brightness, - ), - ); - debugPrint('ColorScheme: ${coverColorScheme.valueOrNull}'); - } else { - debugPrint('useMaterialThemeOnItemPage is false'); - // AsyncValue coverColorScheme = const AsyncValue.loading(); - } return ThemeProvider( initTheme: Theme.of(context), duration: 200.ms, @@ -67,40 +40,20 @@ class LibraryItemPage extends HookConsumerWidget { padding: const EdgeInsets.all(8), sliver: LibraryItemHeroSection( itemId: itemId, - extraMap: extraMap, - providedCacheImage: providedCacheImage, - item: itemFromApi, - itemBookMetadata: itemBookMetadata, - bookDetailsCached: bookDetailsCached, - coverColorScheme: coverColorScheme, + extraMap: additionalItemData, ), ), // a horizontal display with dividers of metadata SliverToBoxAdapter( - child: itemFromApi.valueOrNull != null - ? LibraryItemMetadata( - item: itemFromApi.valueOrNull!, - itemBookMetadata: itemBookMetadata, - bookDetailsCached: bookDetailsCached, - ) - : const SizedBox.shrink(), + child: LibraryItemMetadata(id: itemId), ), // a row of actions like play, download, share, etc SliverToBoxAdapter( - child: itemFromApi.valueOrNull != null - ? LibraryItemActions(item: itemFromApi.valueOrNull!) - : const SizedBox.shrink(), + child: LibraryItemActions(id: itemId), ), // a expandable section for book description SliverToBoxAdapter( - child: - itemFromApi.valueOrNull?.media.metadata.description != null - ? ExpandableDescription( - title: 'About the Book', - content: itemFromApi - .valueOrNull!.media.metadata.description!, - ) - : const SizedBox.shrink(), + child: LibraryItemDescription(id: itemId), ), // a padding at the bottom to make sure the last item is not hidden by mini player const SliverToBoxAdapter( @@ -114,6 +67,26 @@ class LibraryItemPage extends HookConsumerWidget { } } +class LibraryItemDescription extends HookConsumerWidget { + const LibraryItemDescription({ + super.key, + required this.id, + }); + + final String id; + @override + Widget build(BuildContext context, WidgetRef ref) { + final item = ref.watch(libraryItemProvider(id)).valueOrNull; + if (item == null) { + return const SizedBox(); + } + return ExpandableDescription( + title: 'About the Book', + content: item.media.metadata.description ?? 'Sorry, no description found', + ); + } +} + /// Calculate the width of the book cover based on the screen size double calculateWidth( BuildContext context, diff --git a/lib/features/player/view/audiobook_player.dart b/lib/features/player/view/audiobook_player.dart index f227342..46ed7aa 100644 --- a/lib/features/player/view/audiobook_player.dart +++ b/lib/features/player/view/audiobook_player.dart @@ -36,7 +36,7 @@ class AudiobookPlayer extends HookConsumerWidget { final player = ref.watch(audiobookPlayerProvider); final imageOfItemBeingPlayed = itemBeingPlayed.valueOrNull != null ? ref.watch( - coverImageProvider(itemBeingPlayed.valueOrNull!), + coverImageProvider(itemBeingPlayed.valueOrNull!.id), ) : null; final imgWidget = imageOfItemBeingPlayed?.valueOrNull != null @@ -63,7 +63,7 @@ class AudiobookPlayer extends HookConsumerWidget { // theme from image final imageTheme = ref.watch( themeOfLibraryItemProvider( - itemBeingPlayed.valueOrNull, + itemBeingPlayed.valueOrNull?.id, brightness: Theme.of(context).brightness, ), ); diff --git a/lib/router/models/library_item_extras.dart b/lib/router/models/library_item_extras.dart index 089d649..0d50040 100644 --- a/lib/router/models/library_item_extras.dart +++ b/lib/router/models/library_item_extras.dart @@ -1,7 +1,5 @@ // a freezed class to store the settings of the app -import 'dart:typed_data'; - import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; @@ -17,7 +15,6 @@ class LibraryItemExtras with _$LibraryItemExtras { const factory LibraryItemExtras({ BookMinified? book, @Default('') String heroTagSuffix, - Uint8List? coverImage, }) = _LibraryItemExtras; } diff --git a/lib/router/models/library_item_extras.freezed.dart b/lib/router/models/library_item_extras.freezed.dart index 00c6385..900fdec 100644 --- a/lib/router/models/library_item_extras.freezed.dart +++ b/lib/router/models/library_item_extras.freezed.dart @@ -18,7 +18,6 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$LibraryItemExtras { BookMinified? get book => throw _privateConstructorUsedError; String get heroTagSuffix => throw _privateConstructorUsedError; - Uint8List? get coverImage => throw _privateConstructorUsedError; /// Create a copy of LibraryItemExtras /// with the given fields replaced by the non-null parameter values. @@ -33,7 +32,7 @@ abstract class $LibraryItemExtrasCopyWith<$Res> { LibraryItemExtras value, $Res Function(LibraryItemExtras) then) = _$LibraryItemExtrasCopyWithImpl<$Res, LibraryItemExtras>; @useResult - $Res call({BookMinified? book, String heroTagSuffix, Uint8List? coverImage}); + $Res call({BookMinified? book, String heroTagSuffix}); } /// @nodoc @@ -53,7 +52,6 @@ class _$LibraryItemExtrasCopyWithImpl<$Res, $Val extends LibraryItemExtras> $Res call({ Object? book = freezed, Object? heroTagSuffix = null, - Object? coverImage = freezed, }) { return _then(_value.copyWith( book: freezed == book @@ -64,10 +62,6 @@ class _$LibraryItemExtrasCopyWithImpl<$Res, $Val extends LibraryItemExtras> ? _value.heroTagSuffix : heroTagSuffix // ignore: cast_nullable_to_non_nullable as String, - coverImage: freezed == coverImage - ? _value.coverImage - : coverImage // ignore: cast_nullable_to_non_nullable - as Uint8List?, ) as $Val); } } @@ -80,7 +74,7 @@ abstract class _$$LibraryItemExtrasImplCopyWith<$Res> __$$LibraryItemExtrasImplCopyWithImpl<$Res>; @override @useResult - $Res call({BookMinified? book, String heroTagSuffix, Uint8List? coverImage}); + $Res call({BookMinified? book, String heroTagSuffix}); } /// @nodoc @@ -98,7 +92,6 @@ class __$$LibraryItemExtrasImplCopyWithImpl<$Res> $Res call({ Object? book = freezed, Object? heroTagSuffix = null, - Object? coverImage = freezed, }) { return _then(_$LibraryItemExtrasImpl( book: freezed == book @@ -109,10 +102,6 @@ class __$$LibraryItemExtrasImplCopyWithImpl<$Res> ? _value.heroTagSuffix : heroTagSuffix // ignore: cast_nullable_to_non_nullable as String, - coverImage: freezed == coverImage - ? _value.coverImage - : coverImage // ignore: cast_nullable_to_non_nullable - as Uint8List?, )); } } @@ -120,20 +109,17 @@ class __$$LibraryItemExtrasImplCopyWithImpl<$Res> /// @nodoc class _$LibraryItemExtrasImpl implements _LibraryItemExtras { - const _$LibraryItemExtrasImpl( - {this.book, this.heroTagSuffix = '', this.coverImage}); + const _$LibraryItemExtrasImpl({this.book, this.heroTagSuffix = ''}); @override final BookMinified? book; @override @JsonKey() final String heroTagSuffix; - @override - final Uint8List? coverImage; @override String toString() { - return 'LibraryItemExtras(book: $book, heroTagSuffix: $heroTagSuffix, coverImage: $coverImage)'; + return 'LibraryItemExtras(book: $book, heroTagSuffix: $heroTagSuffix)'; } @override @@ -143,14 +129,11 @@ class _$LibraryItemExtrasImpl implements _LibraryItemExtras { other is _$LibraryItemExtrasImpl && (identical(other.book, book) || other.book == book) && (identical(other.heroTagSuffix, heroTagSuffix) || - other.heroTagSuffix == heroTagSuffix) && - const DeepCollectionEquality() - .equals(other.coverImage, coverImage)); + other.heroTagSuffix == heroTagSuffix)); } @override - int get hashCode => Object.hash(runtimeType, book, heroTagSuffix, - const DeepCollectionEquality().hash(coverImage)); + int get hashCode => Object.hash(runtimeType, book, heroTagSuffix); /// Create a copy of LibraryItemExtras /// with the given fields replaced by the non-null parameter values. @@ -165,15 +148,12 @@ class _$LibraryItemExtrasImpl implements _LibraryItemExtras { abstract class _LibraryItemExtras implements LibraryItemExtras { const factory _LibraryItemExtras( {final BookMinified? book, - final String heroTagSuffix, - final Uint8List? coverImage}) = _$LibraryItemExtrasImpl; + final String heroTagSuffix}) = _$LibraryItemExtrasImpl; @override BookMinified? get book; @override String get heroTagSuffix; - @override - Uint8List? get coverImage; /// Create a copy of LibraryItemExtras /// with the given fields replaced by the non-null parameter values. diff --git a/lib/shared/widgets/shelves/book_shelf.dart b/lib/shared/widgets/shelves/book_shelf.dart index e4d264a..692d3b3 100644 --- a/lib/shared/widgets/shelves/book_shelf.dart +++ b/lib/shared/widgets/shelves/book_shelf.dart @@ -8,8 +8,7 @@ import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shimmer/shimmer.dart' show Shimmer; import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/image_provider.dart'; -import 'package:vaani/api/library_item_provider.dart' - show libraryItemProvider; +import 'package:vaani/api/library_item_provider.dart' show libraryItemProvider; import 'package:vaani/constants/hero_tag_conventions.dart'; import 'package:vaani/features/item_viewer/view/library_item_actions.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; @@ -72,7 +71,7 @@ class BookOnShelf extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final book = item.media.asBookMinified; final metadata = book.metadata.asBookMetadataMinified; - final coverImage = ref.watch(coverImageProvider(item)); + final coverImage = ref.watch(coverImageProvider(item.id)); return LayoutBuilder( builder: (context, constraints) { final height = min(constraints.maxHeight, 500); @@ -87,7 +86,6 @@ class BookOnShelf extends HookConsumerWidget { extra: LibraryItemExtras( book: book, heroTagSuffix: heroTagSuffix, - coverImage: coverImage.valueOrNull, ), ); } @@ -228,10 +226,9 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { AsyncValue coverColorScheme = const AsyncValue.loading(); if (useMaterialThemeOnItemPage && isCurrentBookSetInPlayer) { - final itemFromApi = ref.watch(libraryItemProvider(libraryItemId)); coverColorScheme = ref.watch( themeOfLibraryItemProvider( - itemFromApi.valueOrNull, + libraryItemId, brightness: Theme.of(context).brightness, ), ); diff --git a/lib/theme/theme_from_cover_provider.dart b/lib/theme/theme_from_cover_provider.dart index e6bb8b6..a535f2f 100644 --- a/lib/theme/theme_from_cover_provider.dart +++ b/lib/theme/theme_from_cover_provider.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/api/image_provider.dart'; part 'theme_from_cover_provider.g.dart'; @@ -49,13 +48,13 @@ Future> themeFromCover( @Riverpod(keepAlive: true) FutureOr themeOfLibraryItem( ThemeOfLibraryItemRef ref, - LibraryItem? item, { + String? itemId, { Brightness brightness = Brightness.dark, }) async { - if (item == null) { + if (itemId == null) { return null; } - final coverImage = await ref.watch(coverImageProvider(item).future); + final coverImage = await ref.watch(coverImageProvider(itemId).future); final val = await ref.watch( themeFromCoverProvider(MemoryImage(coverImage), brightness: brightness) .future, diff --git a/lib/theme/theme_from_cover_provider.g.dart b/lib/theme/theme_from_cover_provider.g.dart index 8201917..61a86a9 100644 --- a/lib/theme/theme_from_cover_provider.g.dart +++ b/lib/theme/theme_from_cover_provider.g.dart @@ -175,7 +175,7 @@ class _ThemeFromCoverProviderElement } String _$themeOfLibraryItemHash() => - r'575a390a0ab0e66cf54cb090a358c08847270798'; + r'a1d0e5d81f4debe88d5a6ce46c3af28623ad4273'; /// See also [themeOfLibraryItem]. @ProviderFor(themeOfLibraryItem) @@ -188,11 +188,11 @@ class ThemeOfLibraryItemFamily extends Family> { /// See also [themeOfLibraryItem]. ThemeOfLibraryItemProvider call( - LibraryItem? item, { + String? itemId, { Brightness brightness = Brightness.dark, }) { return ThemeOfLibraryItemProvider( - item, + itemId, brightness: brightness, ); } @@ -202,7 +202,7 @@ class ThemeOfLibraryItemFamily extends Family> { covariant ThemeOfLibraryItemProvider provider, ) { return call( - provider.item, + provider.itemId, brightness: provider.brightness, ); } @@ -226,12 +226,12 @@ class ThemeOfLibraryItemFamily extends Family> { class ThemeOfLibraryItemProvider extends FutureProvider { /// See also [themeOfLibraryItem]. ThemeOfLibraryItemProvider( - LibraryItem? item, { + String? itemId, { Brightness brightness = Brightness.dark, }) : this._internal( (ref) => themeOfLibraryItem( ref as ThemeOfLibraryItemRef, - item, + itemId, brightness: brightness, ), from: themeOfLibraryItemProvider, @@ -243,7 +243,7 @@ class ThemeOfLibraryItemProvider extends FutureProvider { dependencies: ThemeOfLibraryItemFamily._dependencies, allTransitiveDependencies: ThemeOfLibraryItemFamily._allTransitiveDependencies, - item: item, + itemId: itemId, brightness: brightness, ); @@ -254,11 +254,11 @@ class ThemeOfLibraryItemProvider extends FutureProvider { required super.allTransitiveDependencies, required super.debugGetCreateSourceHash, required super.from, - required this.item, + required this.itemId, required this.brightness, }) : super.internal(); - final LibraryItem? item; + final String? itemId; final Brightness brightness; @override @@ -274,7 +274,7 @@ class ThemeOfLibraryItemProvider extends FutureProvider { dependencies: null, allTransitiveDependencies: null, debugGetCreateSourceHash: null, - item: item, + itemId: itemId, brightness: brightness, ), ); @@ -288,14 +288,14 @@ class ThemeOfLibraryItemProvider extends FutureProvider { @override bool operator ==(Object other) { return other is ThemeOfLibraryItemProvider && - other.item == item && + other.itemId == itemId && other.brightness == brightness; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, item.hashCode); + hash = _SystemHash.combine(hash, itemId.hashCode); hash = _SystemHash.combine(hash, brightness.hashCode); return _SystemHash.finish(hash); @@ -303,8 +303,8 @@ class ThemeOfLibraryItemProvider extends FutureProvider { } mixin ThemeOfLibraryItemRef on FutureProviderRef { - /// The parameter `item` of this provider. - LibraryItem? get item; + /// The parameter `itemId` of this provider. + String? get itemId; /// The parameter `brightness` of this provider. Brightness get brightness; @@ -315,7 +315,7 @@ class _ThemeOfLibraryItemProviderElement _ThemeOfLibraryItemProviderElement(super.provider); @override - LibraryItem? get item => (origin as ThemeOfLibraryItemProvider).item; + String? get itemId => (origin as ThemeOfLibraryItemProvider).itemId; @override Brightness get brightness => (origin as ThemeOfLibraryItemProvider).brightness;