Vaani/lib/pages/library_item_page.dart

353 lines
11 KiB
Dart
Raw Normal View History

2024-05-09 23:23:50 -04:00
import 'dart:math';
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
2024-05-09 00:41:19 -04:00
import 'package:flutter/material.dart';
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-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-11 04:06:25 -04:00
final item = ref.watch(libraryItemProvider(itemId));
var itemBookMetadata =
item.valueOrNull?.media.metadata as shelfsdk.BookMetadata?;
final useMaterialThemeOnItemPage =
ref.watch(appSettingsProvider).useMaterialThemeOnItemPage;
AsyncValue<ColorScheme?> coverColorScheme = const AsyncValue.loading();
if (useMaterialThemeOnItemPage) {
coverColorScheme = ref.watch(
themeOfLibraryItemProvider(
item.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,
2024-05-11 04:06:25 -04:00
// data: coverColorScheme.valueOrNull != null && useMaterialThemeOnItemPage
// ? ThemeData.from(
// colorScheme: coverColorScheme.valueOrNull!,
// textTheme: Theme.of(context).textTheme,
// )
// : Theme.of(context),
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,
item: item,
itemBookMetadata: itemBookMetadata,
bookDetailsCached: bookDetailsCached,
coverColorScheme: coverColorScheme,
useMaterialThemeOnItemPage: useMaterialThemeOnItemPage,
2024-05-11 04:06:25 -04:00
),
),
],
2024-05-09 23:23:50 -04:00
),
);
},
2024-05-11 04:06:25 -04:00
),
),
);
}
}
2024-05-09 23:23:50 -04:00
class LibraryItemHeroSection extends StatelessWidget {
const LibraryItemHeroSection({
2024-05-11 04:06:25 -04:00
super.key,
required this.itemId,
required this.extraMap,
required this.providedCacheImage,
required this.item,
required this.itemBookMetadata,
required this.bookDetailsCached,
required this.coverColorScheme,
required this.useMaterialThemeOnItemPage,
2024-05-11 04:06:25 -04:00
});
2024-05-09 23:23:50 -04:00
final bool useMaterialThemeOnItemPage;
2024-05-11 04:06:25 -04:00
final String itemId;
final LibraryItemExtras? extraMap;
final Image? providedCacheImage;
final AsyncValue<shelfsdk.LibraryItem> item;
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
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
LayoutBuilder(
builder: (context, constraints) {
return SizedBox(
height: calculateWidth(
context,
constraints,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: _BookCover(
itemId: itemId,
extraMap: extraMap,
providedCacheImage: providedCacheImage,
coverColorScheme: coverColorScheme.valueOrNull,
item: item,
2024-05-11 04:06:25 -04:00
),
),
2024-05-11 04:06:25 -04:00
);
},
),
const SizedBox.square(
dimension: 8,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_BookTitle(
extraMap: extraMap,
itemBookMetadata: itemBookMetadata,
bookDetailsCached: bookDetailsCached,
),
_BookAuthors(
itemBookMetadata: itemBookMetadata,
bookDetailsCached: bookDetailsCached,
),
// series info if available
// narrators info if available
],
),
),
],
),
);
}
}
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) {
return ThemeSwitcher(
builder: (context) {
// change theme after 2 seconds
Future.delayed(150.ms, () {
ThemeSwitcher.of(context).changeTheme(
theme: coverColorScheme != null
? ThemeData.from(
colorScheme: coverColorScheme!,
textTheme: Theme.of(context).textTheme,
)
: Theme.of(context),
);
});
return Hero(
tag: HeroTagPrefixes.bookCover +
itemId +
(extraMap?.heroTagSuffix ?? ''),
child: 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()),
),
);
},
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) {
return 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: Theme.of(context).textTheme.headlineSmall,
itemBookMetadata?.title ?? bookDetailsCached?.metadata.title ?? '',
2024-05-09 00:41:19 -04:00
),
);
}
}
2024-05-09 23:23:50 -04:00
class _BookAuthors extends HookConsumerWidget {
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
Widget build(BuildContext context, WidgetRef ref) {
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(', ');
}
final useMaterialThemeOnItemPage =
ref.watch(appSettingsProvider).useMaterialThemeOnItemPage;
2024-05-11 04:06:25 -04:00
return Row(
children: [
Container(
margin: const EdgeInsets.only(right: 8),
child: FaIcon(
FontAwesomeIcons.penNib,
size: 16,
color: useMaterialThemeOnItemPage
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onBackground.withOpacity(0.75),
2024-05-11 04:06:25 -04:00
),
),
Expanded(
child: Text(
style: Theme.of(context).textTheme.titleSmall,
generateAuthorsString(),
),
),
],
);
}
}
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
double maxHeightToUse = 0.2,
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;
}