refactor: update cover image handling to use item IDs and simplify library item actions

This commit is contained in:
Dr-Blank 2024-09-23 03:55:32 -04:00
parent d25d23a0b7
commit 405d625cdc
No known key found for this signature in database
GPG key ID: 7452CC63F210A266
15 changed files with 305 additions and 362 deletions

View file

@ -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<Uint8List> build(LibraryItem libraryItem) async* {
Stream<Uint8List> 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()}',
);
}

View file

@ -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<Uint8List> {
late final LibraryItem libraryItem;
late final String itemId;
Stream<Uint8List> build(
LibraryItem libraryItem,
String itemId,
);
}
@ -48,10 +48,10 @@ class CoverImageFamily extends Family<AsyncValue<Uint8List>> {
/// See also [CoverImage].
CoverImageProvider call(
LibraryItem libraryItem,
String itemId,
) {
return CoverImageProvider(
libraryItem,
itemId,
);
}
@ -60,7 +60,7 @@ class CoverImageFamily extends Family<AsyncValue<Uint8List>> {
covariant CoverImageProvider provider,
) {
return call(
provider.libraryItem,
provider.itemId,
);
}
@ -84,9 +84,9 @@ class CoverImageProvider
extends StreamNotifierProviderImpl<CoverImage, Uint8List> {
/// 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<Uint8List> 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<Uint8List> {
/// 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

View file

@ -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<shelfsdk.LibraryItemExpanded> 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);

View file

@ -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<shelfsdk.LibraryItemExpanded> {
extends BuildlessStreamNotifier<shelfsdk.LibraryItemExpanded> {
late final String id;
Stream<shelfsdk.LibraryItemExpanded> 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<LibraryItem,
shelfsdk.LibraryItemExpanded> {
/// provides the library item for the given id
///
/// Copied from [LibraryItem].
@ -151,8 +151,8 @@ class LibraryItemProvider extends AutoDisposeStreamNotifierProviderImpl<
}
@override
AutoDisposeStreamNotifierProviderElement<LibraryItem,
shelfsdk.LibraryItemExpanded> createElement() {
StreamNotifierProviderElement<LibraryItem, shelfsdk.LibraryItemExpanded>
createElement() {
return _LibraryItemProviderElement(this);
}
@ -171,14 +171,13 @@ class LibraryItemProvider extends AutoDisposeStreamNotifierProviderImpl<
}
mixin LibraryItemRef
on AutoDisposeStreamNotifierProviderRef<shelfsdk.LibraryItemExpanded> {
on StreamNotifierProviderRef<shelfsdk.LibraryItemExpanded> {
/// The parameter `id` of this provider.
String get id;
}
class _LibraryItemProviderElement
extends AutoDisposeStreamNotifierProviderElement<LibraryItem,
shelfsdk.LibraryItemExpanded> with LibraryItemRef {
class _LibraryItemProviderElement extends StreamNotifierProviderElement<
LibraryItem, shelfsdk.LibraryItemExpanded> with LibraryItemRef {
_LibraryItemProviderElement(super.provider);
@override

View file

@ -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,

View file

@ -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);

View file

@ -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,33 +15,21 @@ 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<shelfsdk.LibraryItemExpanded> item;
final shelfsdk.BookMetadataExpanded? itemBookMetadata;
final shelfsdk.BookMinified? bookDetailsCached;
final AsyncValue<ColorScheme?> 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,
@ -60,29 +49,19 @@ class LibraryItemHeroSection extends HookConsumerWidget {
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(
// a progress bar
Padding(
padding: const EdgeInsets.only(
top: 8.0,
right: 8.0,
left: 8.0,
),
child: _LibraryItemProgressIndicator(
libraryItem: libraryItem,
id: itemId,
),
);
},
loading: () => const SizedBox.shrink(),
error: (error, stack) => const SizedBox.shrink(),
),
],
),
@ -90,7 +69,31 @@ class LibraryItemHeroSection extends HookConsumerWidget {
},
),
// book details
Expanded(
_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(
@ -100,7 +103,6 @@ class LibraryItemHeroSection extends HookConsumerWidget {
_BookTitle(
extraMap: extraMap,
itemBookMetadata: itemBookMetadata,
bookDetailsCached: bookDetailsCached,
),
Container(
margin: const EdgeInsets.symmetric(vertical: 16),
@ -110,17 +112,17 @@ class LibraryItemHeroSection extends HookConsumerWidget {
// authors info if available
_BookAuthors(
itemBookMetadata: itemBookMetadata,
bookDetailsCached: bookDetailsCached,
bookDetailsCached: extraMap?.book,
),
// narrators info if available
_BookNarrators(
itemBookMetadata: itemBookMetadata,
bookDetailsCached: bookDetailsCached,
bookDetailsCached: extraMap?.book,
),
// series info if available
_BookSeries(
itemBookMetadata: itemBookMetadata,
bookDetailsCached: bookDetailsCached,
bookDetailsCached: extraMap?.book,
),
],
),
@ -128,12 +130,6 @@ class LibraryItemHeroSection extends HookConsumerWidget {
],
),
),
),
],
),
);
},
),
);
}
}
@ -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<shelfsdk.LibraryItemExpanded> 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,25 +387,16 @@ class _BookCover extends HookConsumerWidget {
}
});
}
return providedCacheImage ??
item.when(
data: (libraryItem) {
final coverImage = ref.watch(coverImageProvider(libraryItem));
return Stack(
children: [
coverImage.when(
return 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: () {
@ -405,14 +405,8 @@ class _BookCover extends HookConsumerWidget {
);
},
error: (error, stack) {
return const Icon(Icons.error);
return const Center(child: Icon(Icons.error));
},
),
],
);
},
error: (error, stack) => const Icon(Icons.error),
loading: () => const Center(child: BookCoverSkeleton()),
);
},
);
@ -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 ??
'';
}

View file

@ -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

View file

@ -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<ColorScheme?> 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<ColorScheme?> 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,

View file

@ -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,
),
);

View file

@ -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;
}

View file

@ -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.

View file

@ -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<ColorScheme?> 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,
),
);

View file

@ -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<FutureOr<ColorScheme?>> themeFromCover(
@Riverpod(keepAlive: true)
FutureOr<ColorScheme?> 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,

View file

@ -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<AsyncValue<ColorScheme?>> {
/// 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<AsyncValue<ColorScheme?>> {
covariant ThemeOfLibraryItemProvider provider,
) {
return call(
provider.item,
provider.itemId,
brightness: provider.brightness,
);
}
@ -226,12 +226,12 @@ class ThemeOfLibraryItemFamily extends Family<AsyncValue<ColorScheme?>> {
class ThemeOfLibraryItemProvider extends FutureProvider<ColorScheme?> {
/// 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<ColorScheme?> {
dependencies: ThemeOfLibraryItemFamily._dependencies,
allTransitiveDependencies:
ThemeOfLibraryItemFamily._allTransitiveDependencies,
item: item,
itemId: itemId,
brightness: brightness,
);
@ -254,11 +254,11 @@ class ThemeOfLibraryItemProvider extends FutureProvider<ColorScheme?> {
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<ColorScheme?> {
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
item: item,
itemId: itemId,
brightness: brightness,
),
);
@ -288,14 +288,14 @@ class ThemeOfLibraryItemProvider extends FutureProvider<ColorScheme?> {
@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<ColorScheme?> {
}
mixin ThemeOfLibraryItemRef on FutureProviderRef<ColorScheme?> {
/// 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;