2024-05-09 23:23:50 -04:00
|
|
|
import 'dart:math';
|
|
|
|
|
|
2024-05-11 05:13:56 -04:00
|
|
|
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
|
2024-05-09 00:41:19 -04:00
|
|
|
import 'package:flutter/material.dart';
|
2024-05-11 05:13:56 -04:00
|
|
|
import 'package:flutter_animate/flutter_animate.dart';
|
2024-05-11 04:06:25 -04:00
|
|
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
2024-05-09 00:41:19 -04:00
|
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
2024-05-11 04:06:25 -04:00
|
|
|
import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk;
|
2024-05-09 23:23:50 -04:00
|
|
|
import 'package:whispering_pages/api/image_provider.dart';
|
|
|
|
|
import 'package:whispering_pages/api/library_item_provider.dart';
|
2024-05-09 00:41:19 -04:00
|
|
|
import 'package:whispering_pages/extensions/hero_tag_conventions.dart';
|
|
|
|
|
import 'package:whispering_pages/router/models/library_item_extras.dart';
|
2024-05-11 04:06:25 -04:00
|
|
|
import 'package:whispering_pages/settings/app_settings_provider.dart';
|
|
|
|
|
import 'package:whispering_pages/theme/theme_from_cover_provider.dart';
|
2024-05-09 23:23:50 -04:00
|
|
|
import 'package:whispering_pages/widgets/shelves/book_shelf.dart';
|
2024-05-09 00:41:19 -04:00
|
|
|
|
2024-05-12 05:38:30 -04:00
|
|
|
import '../widgets/expandable_description.dart';
|
2024-05-11 04:06:25 -04:00
|
|
|
import '../widgets/library_item_sliver_app_bar.dart';
|
|
|
|
|
|
2024-05-09 00:41:19 -04:00
|
|
|
class LibraryItemPage extends HookConsumerWidget {
|
|
|
|
|
const LibraryItemPage({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.itemId,
|
|
|
|
|
this.extra,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
final String itemId;
|
|
|
|
|
final Object? extra;
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
|
|
|
final extraMap =
|
|
|
|
|
extra is LibraryItemExtras ? extra as LibraryItemExtras : null;
|
2024-05-11 04:06:25 -04:00
|
|
|
final bookDetailsCached = extraMap?.book;
|
2024-05-09 23:23:50 -04:00
|
|
|
final providedCacheImage = extraMap?.coverImage != null
|
|
|
|
|
? Image.memory(extraMap!.coverImage!)
|
|
|
|
|
: null;
|
2024-05-09 00:41:19 -04:00
|
|
|
|
2024-05-12 05:38:30 -04:00
|
|
|
final itemFromApi = ref.watch(libraryItemProvider(itemId));
|
2024-05-11 04:06:25 -04:00
|
|
|
var itemBookMetadata =
|
2024-05-12 05:38:30 -04:00
|
|
|
itemFromApi.valueOrNull?.media.metadata as shelfsdk.BookMetadata?;
|
2024-05-11 04:06:25 -04:00
|
|
|
|
|
|
|
|
final useMaterialThemeOnItemPage =
|
|
|
|
|
ref.watch(appSettingsProvider).useMaterialThemeOnItemPage;
|
|
|
|
|
AsyncValue<ColorScheme?> coverColorScheme = const AsyncValue.loading();
|
|
|
|
|
if (useMaterialThemeOnItemPage) {
|
|
|
|
|
coverColorScheme = ref.watch(
|
|
|
|
|
themeOfLibraryItemProvider(
|
2024-05-12 05:38:30 -04:00
|
|
|
itemFromApi.valueOrNull,
|
2024-05-11 04:06:25 -04:00
|
|
|
brightness: Theme.of(context).brightness,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
debugPrint('ColorScheme: ${coverColorScheme.valueOrNull}');
|
|
|
|
|
} else {
|
|
|
|
|
debugPrint('useMaterialThemeOnItemPage is false');
|
|
|
|
|
// AsyncValue<ColorScheme?> coverColorScheme = const AsyncValue.loading();
|
|
|
|
|
}
|
2024-05-11 05:13:56 -04:00
|
|
|
return ThemeProvider(
|
|
|
|
|
initTheme: Theme.of(context),
|
|
|
|
|
duration: 200.ms,
|
|
|
|
|
child: ThemeSwitchingArea(
|
|
|
|
|
child: Builder(
|
|
|
|
|
builder: (context) {
|
|
|
|
|
return Scaffold(
|
|
|
|
|
body: CustomScrollView(
|
|
|
|
|
slivers: [
|
|
|
|
|
const LibraryItemSliverAppBar(),
|
|
|
|
|
SliverPadding(
|
|
|
|
|
padding: const EdgeInsets.all(8),
|
|
|
|
|
sliver: LibraryItemHeroSection(
|
|
|
|
|
itemId: itemId,
|
|
|
|
|
extraMap: extraMap,
|
|
|
|
|
providedCacheImage: providedCacheImage,
|
2024-05-12 05:38:30 -04:00
|
|
|
item: itemFromApi,
|
2024-05-11 05:13:56 -04:00
|
|
|
itemBookMetadata: itemBookMetadata,
|
|
|
|
|
bookDetailsCached: bookDetailsCached,
|
|
|
|
|
coverColorScheme: coverColorScheme,
|
2024-05-11 04:06:25 -04:00
|
|
|
),
|
2024-05-11 05:13:56 -04:00
|
|
|
),
|
2024-05-12 05:38:30 -04:00
|
|
|
// a horizontal display with dividers of metadata
|
|
|
|
|
SliverToBoxAdapter(
|
|
|
|
|
child: itemFromApi.valueOrNull != null
|
|
|
|
|
? LibraryItemMetadata(
|
|
|
|
|
item: itemFromApi.valueOrNull!,
|
|
|
|
|
itemBookMetadata: itemBookMetadata,
|
|
|
|
|
bookDetailsCached: bookDetailsCached,
|
|
|
|
|
)
|
|
|
|
|
: const SizedBox.shrink(),
|
|
|
|
|
),
|
|
|
|
|
// a row of actions like play, download, share, etc
|
|
|
|
|
const SliverPadding(
|
|
|
|
|
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
|
|
|
|
sliver: LibraryItemActions(),
|
|
|
|
|
),
|
|
|
|
|
// 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(),
|
|
|
|
|
),
|
2024-05-11 05:13:56 -04:00
|
|
|
],
|
2024-05-09 23:23:50 -04:00
|
|
|
),
|
2024-05-11 05:13:56 -04:00
|
|
|
);
|
|
|
|
|
},
|
2024-05-11 04:06:25 -04:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-05-09 23:23:50 -04:00
|
|
|
|
2024-05-12 05:38:30 -04:00
|
|
|
class LibraryItemMetadata extends StatelessWidget {
|
|
|
|
|
const LibraryItemMetadata({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.item,
|
|
|
|
|
this.itemBookMetadata,
|
|
|
|
|
this.bookDetailsCached,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
final shelfsdk.LibraryItem item;
|
|
|
|
|
final shelfsdk.BookMetadata? itemBookMetadata;
|
|
|
|
|
final shelfsdk.BookMinified? bookDetailsCached;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final children = [
|
|
|
|
|
// duration of the book
|
|
|
|
|
_MetadataItem(
|
|
|
|
|
title: switch (itemBookMetadata?.abridged) {
|
|
|
|
|
true => 'Abridged',
|
|
|
|
|
false => 'Unabridged',
|
|
|
|
|
_ => 'Length',
|
|
|
|
|
},
|
|
|
|
|
value: getDurationFormatted() ?? 'time is just a concept',
|
|
|
|
|
),
|
|
|
|
|
_MetadataItem(
|
|
|
|
|
title: 'Published',
|
|
|
|
|
value: itemBookMetadata?.publishedDate ??
|
|
|
|
|
itemBookMetadata?.publishedYear ??
|
|
|
|
|
'Unknown',
|
|
|
|
|
),
|
|
|
|
|
_MetadataItem(
|
|
|
|
|
title: getCodecAndBitrate() ?? 'Codec & Bitrate',
|
|
|
|
|
value: getSizeFormatted() ?? 'Unknown',
|
|
|
|
|
),
|
|
|
|
|
];
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
|
|
|
child: IntrinsicHeight(
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
|
|
|
// alternate between metadata and vertical divider
|
|
|
|
|
children: List.generate(
|
|
|
|
|
children.length * 2 - 1,
|
|
|
|
|
(index) {
|
|
|
|
|
if (index.isEven) {
|
|
|
|
|
return children[index ~/ 2];
|
|
|
|
|
}
|
|
|
|
|
return VerticalDivider(
|
|
|
|
|
indent: 6,
|
|
|
|
|
endIndent: 6,
|
|
|
|
|
color:
|
|
|
|
|
Theme.of(context).colorScheme.onBackground.withOpacity(0.6),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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.Book?);
|
|
|
|
|
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.Book?);
|
|
|
|
|
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.Book?);
|
|
|
|
|
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
|
|
|
|
|
class _MetadataItem extends StatelessWidget {
|
|
|
|
|
const _MetadataItem({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.title,
|
|
|
|
|
required this.value,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
final String title;
|
|
|
|
|
final String value;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final themeData = Theme.of(context);
|
|
|
|
|
return Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
style: themeData.textTheme.titleMedium?.copyWith(
|
|
|
|
|
color: themeData.colorScheme.onBackground.withOpacity(0.90),
|
|
|
|
|
),
|
|
|
|
|
value,
|
|
|
|
|
maxLines: 1,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
),
|
|
|
|
|
Text(
|
|
|
|
|
style: themeData.textTheme.bodySmall?.copyWith(
|
|
|
|
|
color: themeData.colorScheme.onBackground.withOpacity(0.7),
|
|
|
|
|
),
|
|
|
|
|
title,
|
|
|
|
|
maxLines: 1,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class LibraryItemActions extends StatelessWidget {
|
|
|
|
|
const LibraryItemActions({
|
|
|
|
|
super.key,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return SliverToBoxAdapter(
|
|
|
|
|
child: Container(
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
|
|
|
children: [
|
|
|
|
|
// play/resume button the same widht 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: () {},
|
|
|
|
|
icon: const Icon(Icons.play_arrow_rounded),
|
|
|
|
|
label: const Text('Play/Resume'),
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(4),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: LayoutBuilder(
|
|
|
|
|
builder: (context, constraints) {
|
|
|
|
|
return SizedBox(
|
|
|
|
|
width: constraints.maxWidth * 0.6,
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
|
|
|
children: [
|
|
|
|
|
// read list button
|
|
|
|
|
IconButton(
|
|
|
|
|
onPressed: () {},
|
|
|
|
|
icon: const Icon(
|
|
|
|
|
Icons.playlist_add_rounded,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// share button
|
|
|
|
|
IconButton(
|
|
|
|
|
onPressed: () {},
|
|
|
|
|
icon: const Icon(Icons.share_rounded),
|
|
|
|
|
),
|
|
|
|
|
// download button
|
|
|
|
|
IconButton(
|
|
|
|
|
onPressed: () {},
|
|
|
|
|
icon: const Icon(
|
|
|
|
|
Icons.download_rounded,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// more button
|
|
|
|
|
IconButton(
|
|
|
|
|
onPressed: () {},
|
|
|
|
|
icon: const Icon(
|
|
|
|
|
Icons.more_vert_rounded,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class LibraryItemHeroSection extends HookConsumerWidget {
|
2024-05-11 05:13:56 -04:00
|
|
|
const LibraryItemHeroSection({
|
2024-05-11 04:06:25 -04:00
|
|
|
super.key,
|
|
|
|
|
required this.itemId,
|
|
|
|
|
required this.extraMap,
|
|
|
|
|
required this.providedCacheImage,
|
|
|
|
|
required this.item,
|
2024-05-11 05:13:56 -04:00
|
|
|
required this.itemBookMetadata,
|
|
|
|
|
required this.bookDetailsCached,
|
|
|
|
|
required this.coverColorScheme,
|
2024-05-11 04:06:25 -04:00
|
|
|
});
|
2024-05-09 23:23:50 -04:00
|
|
|
|
2024-05-11 04:06:25 -04:00
|
|
|
final String itemId;
|
|
|
|
|
final LibraryItemExtras? extraMap;
|
|
|
|
|
final Image? providedCacheImage;
|
|
|
|
|
final AsyncValue<shelfsdk.LibraryItem> item;
|
2024-05-11 05:13:56 -04:00
|
|
|
final shelfsdk.BookMetadata? itemBookMetadata;
|
|
|
|
|
final shelfsdk.BookMinified? bookDetailsCached;
|
|
|
|
|
final AsyncValue<ColorScheme?> coverColorScheme;
|
2024-05-09 23:23:50 -04:00
|
|
|
|
2024-05-11 04:06:25 -04:00
|
|
|
@override
|
2024-05-12 05:38:30 -04:00
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
2024-05-11 05:13:56 -04:00
|
|
|
return SliverToBoxAdapter(
|
2024-05-12 05:38:30 -04:00
|
|
|
child: LayoutBuilder(
|
|
|
|
|
builder: (context, constraints) {
|
|
|
|
|
return Container(
|
|
|
|
|
child: Row(
|
2024-05-11 05:13:56 -04:00
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
2024-05-12 05:38:30 -04:00
|
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
2024-05-11 05:13:56 -04:00
|
|
|
children: [
|
2024-05-12 05:38:30 -04:00
|
|
|
// book cover
|
|
|
|
|
LayoutBuilder(
|
|
|
|
|
builder: (context, constraints) {
|
|
|
|
|
return SizedBox(
|
|
|
|
|
width: calculateWidth(context, constraints),
|
|
|
|
|
child: 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,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
2024-05-11 05:13:56 -04:00
|
|
|
),
|
2024-05-12 05:38:30 -04:00
|
|
|
// 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,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
2024-05-11 05:13:56 -04:00
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2024-05-12 05:38:30 -04:00
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
|
|
|
|
|
const _HeroSectionSubLabelWithIcon({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.icon,
|
|
|
|
|
required this.text,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
final IconData icon;
|
|
|
|
|
final Widget text;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
|
|
|
final themeData = Theme.of(context);
|
|
|
|
|
final useFontAwesome =
|
|
|
|
|
icon.runtimeType == FontAwesomeIcons.book.runtimeType;
|
|
|
|
|
final useMaterialThemeOnItemPage =
|
|
|
|
|
ref.watch(appSettingsProvider).useMaterialThemeOnItemPage;
|
|
|
|
|
final color = useMaterialThemeOnItemPage
|
|
|
|
|
? themeData.colorScheme.primary
|
|
|
|
|
: themeData.colorScheme.onBackground.withOpacity(0.75);
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.only(bottom: 8.0),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
margin: const EdgeInsets.only(right: 8, top: 2),
|
|
|
|
|
child: useFontAwesome
|
|
|
|
|
? FaIcon(
|
|
|
|
|
icon,
|
|
|
|
|
size: 16,
|
|
|
|
|
color: color,
|
|
|
|
|
)
|
|
|
|
|
: Icon(
|
|
|
|
|
icon,
|
|
|
|
|
size: 16,
|
|
|
|
|
color: color,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: text,
|
2024-05-11 05:13:56 -04:00
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-12 05:38:30 -04:00
|
|
|
class _BookSeries extends StatelessWidget {
|
|
|
|
|
const _BookSeries({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.itemBookMetadata,
|
|
|
|
|
required this.bookDetailsCached,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
final shelfsdk.BookMetadata? itemBookMetadata;
|
|
|
|
|
final shelfsdk.BookMinified? bookDetailsCached;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final themeData = Theme.of(context);
|
|
|
|
|
String generateSeriesString() {
|
|
|
|
|
final series = (itemBookMetadata)?.series ?? <shelfsdk.SeriesSequence>[];
|
|
|
|
|
if (series.isEmpty) {
|
|
|
|
|
return (bookDetailsCached?.metadata as shelfsdk.BookMetadataMinified?)
|
|
|
|
|
?.seriesName ??
|
|
|
|
|
'';
|
|
|
|
|
}
|
|
|
|
|
return series
|
|
|
|
|
.map((e) {
|
|
|
|
|
try {
|
|
|
|
|
e as shelfsdk.SeriesSequence;
|
|
|
|
|
final seq = e.sequence != null ? '#${e.sequence} of ' : '';
|
|
|
|
|
return '$seq${e.name}';
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.toList()
|
|
|
|
|
.join(', ');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return generateSeriesString() == ''
|
|
|
|
|
? const SizedBox.shrink()
|
|
|
|
|
: _HeroSectionSubLabelWithIcon(
|
|
|
|
|
icon: Icons.library_books_rounded,
|
|
|
|
|
text: Text(
|
|
|
|
|
style: themeData.textTheme.titleSmall,
|
|
|
|
|
generateSeriesString(),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _BookNarrators extends StatelessWidget {
|
|
|
|
|
const _BookNarrators({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.itemBookMetadata,
|
|
|
|
|
required this.bookDetailsCached,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
final shelfsdk.BookMetadata? itemBookMetadata;
|
|
|
|
|
final shelfsdk.BookMinified? bookDetailsCached;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
|
|
|
|
String generateNarratorsString() {
|
|
|
|
|
final narrators = (itemBookMetadata)?.narrators ?? [];
|
|
|
|
|
if (narrators.isEmpty) {
|
|
|
|
|
return (bookDetailsCached?.metadata as shelfsdk.BookMetadataMinified?)
|
|
|
|
|
?.narratorName ??
|
|
|
|
|
'';
|
|
|
|
|
}
|
|
|
|
|
return narrators.join(', ');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final themeData = Theme.of(context);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return generateNarratorsString() == ''
|
|
|
|
|
? const SizedBox.shrink()
|
|
|
|
|
: _HeroSectionSubLabelWithIcon(
|
|
|
|
|
icon: Icons.record_voice_over,
|
|
|
|
|
text: Text(
|
|
|
|
|
style: themeData.textTheme.titleSmall,
|
|
|
|
|
generateNarratorsString(),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-11 05:13:56 -04:00
|
|
|
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.LibraryItem> item;
|
|
|
|
|
final ColorScheme? coverColorScheme;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
2024-05-12 05:38:30 -04:00
|
|
|
final themeData = Theme.of(context);
|
|
|
|
|
final useMaterialThemeOnItemPage =
|
|
|
|
|
ref.watch(appSettingsProvider).useMaterialThemeOnItemPage;
|
|
|
|
|
|
2024-05-11 05:13:56 -04:00
|
|
|
return ThemeSwitcher(
|
|
|
|
|
builder: (context) {
|
|
|
|
|
// change theme after 2 seconds
|
2024-05-12 05:38:30 -04:00
|
|
|
if (useMaterialThemeOnItemPage) {
|
|
|
|
|
Future.delayed(150.ms, () {
|
|
|
|
|
ThemeSwitcher.of(context).changeTheme(
|
|
|
|
|
theme: coverColorScheme != null
|
|
|
|
|
? ThemeData.from(
|
|
|
|
|
colorScheme: coverColorScheme!,
|
|
|
|
|
textTheme: themeData.textTheme,
|
|
|
|
|
)
|
|
|
|
|
: themeData,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
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) {
|
2024-05-11 05:13:56 -04:00
|
|
|
return const Icon(Icons.error);
|
2024-05-12 05:38:30 -04:00
|
|
|
}
|
|
|
|
|
// 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()),
|
|
|
|
|
);
|
2024-05-11 05:13:56 -04:00
|
|
|
},
|
2024-05-11 04:06:25 -04:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _BookTitle extends StatelessWidget {
|
|
|
|
|
const _BookTitle({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.extraMap,
|
|
|
|
|
required this.itemBookMetadata,
|
|
|
|
|
required this.bookDetailsCached,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
final LibraryItemExtras? extraMap;
|
|
|
|
|
final shelfsdk.BookMetadata? itemBookMetadata;
|
|
|
|
|
final shelfsdk.BookMinified? bookDetailsCached;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2024-05-12 05:38:30 -04:00
|
|
|
final themeData = Theme.of(context);
|
|
|
|
|
return Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Hero(
|
|
|
|
|
tag: HeroTagPrefixes.bookTitle +
|
|
|
|
|
// itemId +
|
|
|
|
|
(extraMap?.heroTagSuffix ?? ''),
|
|
|
|
|
child: Text(
|
|
|
|
|
// mode: AutoScrollTextMode.bouncing,
|
|
|
|
|
// curve: Curves.fastEaseInToSlowEaseOut,
|
|
|
|
|
// velocity: const Velocity(pixelsPerSecond: Offset(30, 0)),
|
|
|
|
|
// delayBefore: 500.ms,
|
|
|
|
|
// pauseBetween: 150.ms,
|
|
|
|
|
// numberOfReps: 3,
|
|
|
|
|
style: themeData.textTheme.headlineLarge,
|
|
|
|
|
itemBookMetadata?.title ?? bookDetailsCached?.metadata.title ?? '',
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// subtitle if available
|
|
|
|
|
itemBookMetadata?.subtitle == null
|
|
|
|
|
? const SizedBox.shrink()
|
|
|
|
|
: Text(
|
|
|
|
|
style: themeData.textTheme.titleSmall?.copyWith(
|
|
|
|
|
color: themeData
|
|
|
|
|
.colorScheme
|
|
|
|
|
.onBackground
|
|
|
|
|
.withOpacity(0.8),
|
|
|
|
|
),
|
|
|
|
|
itemBookMetadata?.subtitle ?? '',
|
|
|
|
|
),
|
|
|
|
|
],
|
2024-05-09 00:41:19 -04:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-05-09 23:23:50 -04:00
|
|
|
|
2024-05-12 05:38:30 -04:00
|
|
|
class _BookAuthors extends StatelessWidget {
|
2024-05-11 04:06:25 -04:00
|
|
|
const _BookAuthors({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.itemBookMetadata,
|
|
|
|
|
required this.bookDetailsCached,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
final shelfsdk.BookMetadata? itemBookMetadata;
|
|
|
|
|
final shelfsdk.BookMinified? bookDetailsCached;
|
|
|
|
|
|
|
|
|
|
@override
|
2024-05-12 05:38:30 -04:00
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final themeData = Theme.of(context);
|
2024-05-11 04:06:25 -04:00
|
|
|
String generateAuthorsString() {
|
|
|
|
|
final authors = (itemBookMetadata)?.authors ?? [];
|
|
|
|
|
if (authors.isEmpty) {
|
|
|
|
|
return (bookDetailsCached?.metadata as shelfsdk.BookMetadataMinified?)
|
|
|
|
|
?.authorName ??
|
|
|
|
|
'';
|
|
|
|
|
}
|
|
|
|
|
return authors.map((e) => e.name).join(', ');
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-12 05:38:30 -04:00
|
|
|
return generateAuthorsString() == ''
|
|
|
|
|
? const SizedBox.shrink()
|
|
|
|
|
: _HeroSectionSubLabelWithIcon(
|
|
|
|
|
icon: FontAwesomeIcons.penNib,
|
|
|
|
|
text: Text(
|
|
|
|
|
style: themeData.textTheme.titleSmall,
|
|
|
|
|
generateAuthorsString(),
|
|
|
|
|
),
|
|
|
|
|
);
|
2024-05-11 04:06:25 -04:00
|
|
|
}
|
|
|
|
|
}
|
2024-05-09 23:23:50 -04:00
|
|
|
|
2024-05-11 04:06:25 -04:00
|
|
|
/// Calculate the width of the book cover based on the screen size
|
2024-05-09 23:23:50 -04:00
|
|
|
double calculateWidth(
|
|
|
|
|
BuildContext context,
|
|
|
|
|
BoxConstraints constraints, {
|
2024-05-11 04:06:25 -04:00
|
|
|
/// width ratio of the cover image to the available width
|
|
|
|
|
double widthRatio = 0.4,
|
|
|
|
|
|
|
|
|
|
/// height ratio of the cover image to the available height
|
2024-05-12 05:38:30 -04:00
|
|
|
double maxHeightToUse = 0.25,
|
2024-05-09 23:23:50 -04:00
|
|
|
}) {
|
|
|
|
|
final availHeight =
|
|
|
|
|
min(constraints.maxHeight, MediaQuery.of(context).size.height);
|
|
|
|
|
final availWidth =
|
|
|
|
|
min(constraints.maxWidth, MediaQuery.of(context).size.width);
|
|
|
|
|
|
2024-05-11 04:06:25 -04:00
|
|
|
// make the width widthRatio of the available width
|
2024-05-09 23:23:50 -04:00
|
|
|
var width = availWidth * widthRatio;
|
2024-05-11 04:06:25 -04:00
|
|
|
// but never exceed more than heightRatio of height
|
|
|
|
|
if (width > availHeight * maxHeightToUse) {
|
|
|
|
|
width = availHeight * maxHeightToUse;
|
2024-05-09 23:23:50 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return width;
|
|
|
|
|
}
|