diff --git a/lib/main.dart b/lib/main.dart index 17a9a4a..791774f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -37,6 +37,7 @@ class MyApp extends ConsumerWidget { } return MaterialApp.router( + debugShowCheckedModeBanner: false, theme: lightTheme, darkTheme: darkTheme, themeMode: ref.watch(appSettingsProvider).isDarkMode diff --git a/lib/pages/app_settings.dart b/lib/pages/app_settings.dart index 759df6b..23b3904 100644 --- a/lib/pages/app_settings.dart +++ b/lib/pages/app_settings.dart @@ -39,6 +39,18 @@ class AppSettingsPage extends HookConsumerWidget { ref.read(appSettingsProvider.notifier).toggleDarkMode(); }, ), + SettingsTile.switchTile( + initialValue: appSettings.useMaterialThemeOnItemPage, + title: const Text('Use Material Theming on Item Page'), + leading: const Icon(Icons.dynamic_form_outlined), + onToggle: (value) { + ref.read(appSettingsProvider.notifier).updateState( + appSettings.copyWith( + useMaterialThemeOnItemPage: value, + ), + ); + }, + ), ], ), ], diff --git a/lib/pages/library_item_page.dart b/lib/pages/library_item_page.dart index d90f025..1acdc9c 100644 --- a/lib/pages/library_item_page.dart +++ b/lib/pages/library_item_page.dart @@ -1,13 +1,19 @@ import 'dart:math'; import 'package:flutter/material.dart'; +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:whispering_pages/api/image_provider.dart'; import 'package:whispering_pages/api/library_item_provider.dart'; import 'package:whispering_pages/extensions/hero_tag_conventions.dart'; import 'package:whispering_pages/router/models/library_item_extras.dart'; +import 'package:whispering_pages/settings/app_settings_provider.dart'; +import 'package:whispering_pages/theme/theme_from_cover_provider.dart'; import 'package:whispering_pages/widgets/shelves/book_shelf.dart'; +import '../widgets/library_item_sliver_app_bar.dart'; + class LibraryItemPage extends HookConsumerWidget { const LibraryItemPage({ super.key, @@ -21,82 +27,93 @@ class LibraryItemPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final extraMap = extra is LibraryItemExtras ? extra as LibraryItemExtras : null; - final item = ref.watch(libraryItemProvider(itemId)); + final bookDetailsCached = extraMap?.book; final providedCacheImage = extraMap?.coverImage != null ? Image.memory(extraMap!.coverImage!) : null; - return Scaffold( - appBar: AppBar(), - body: Center( - child: Column( - children: [ - // cover image - Hero( - tag: HeroTagPrefixes.bookCover + - itemId + - (extraMap?.heroTagSuffix ?? ''), - child: LayoutBuilder( - builder: (context, constraints) { - final width = - calculateWidth(context, constraints, heightRatio: 0.35); - return SizedBox( - height: width, - width: width, - child: providedCacheImage ?? - item.when( - data: (libraryItem) { - final coverImage = - ref.watch(coverImageProvider(libraryItem)); - return Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(10), - child: 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 CircularProgressIndicator(), - loading: () => - const Center(child: BookCoverSkeleton()), - ), - ); - }, + final item = ref.watch(libraryItemProvider(itemId)); + var itemBookMetadata = + item.valueOrNull?.media.metadata as shelfsdk.BookMetadata?; + + final useMaterialThemeOnItemPage = + ref.watch(appSettingsProvider).useMaterialThemeOnItemPage; + AsyncValue 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 coverColorScheme = const AsyncValue.loading(); + } + return Theme( + data: coverColorScheme.valueOrNull != null && useMaterialThemeOnItemPage + ? ThemeData.from( + colorScheme: coverColorScheme.valueOrNull!, + textTheme: Theme.of(context).textTheme, + ) + : Theme.of(context), + child: Scaffold( + body: CustomScrollView( + slivers: [ + const LibraryItemSliverAppBar(), + SliverPadding( + padding: const EdgeInsets.all(8), + sliver: 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, + item: item, + ), + ), + ); + }, + ), + const SizedBox.square( + dimension: 8, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _BookTitle( + extraMap: extraMap, + itemBookMetadata: itemBookMetadata, + bookDetailsCached: bookDetailsCached, + ), + _BookAuthors( + itemBookMetadata: itemBookMetadata, + bookDetailsCached: bookDetailsCached, + coverColorScheme: coverColorScheme.valueOrNull, + ), + // series info if available + + // narrators info if available + ], + ), + ), + ], + ), ), ), - - // author - - // title - - - // description - const Text('Description'), ], ), ), @@ -104,33 +121,162 @@ class LibraryItemPage extends HookConsumerWidget { } } -// child: Hero( -// tag: HeroTagPrefixes.bookCover + -// itemId + -// (extraMap?.heroTagSuffix ?? ''), -// child: Container( -// color: Colors.amber, -// height: 200, -// width: 200, -// ), -// ), +class _BookCover extends HookConsumerWidget { + const _BookCover({ + super.key, + required this.itemId, + required this.extraMap, + required this.providedCacheImage, + required this.item, + }); + final String itemId; + final LibraryItemExtras? extraMap; + final Image? providedCacheImage; + final AsyncValue item; + + @override + Widget build(BuildContext context, WidgetRef ref) { + 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()), + ), + ); + } +} + +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 ?? '', + ), + ); + } +} + +class _BookAuthors extends StatelessWidget { + const _BookAuthors({ + super.key, + required this.itemBookMetadata, + required this.bookDetailsCached, + this.coverColorScheme, + }); + + final shelfsdk.BookMetadata? itemBookMetadata; + final shelfsdk.BookMinified? bookDetailsCached; + final ColorScheme? coverColorScheme; + + @override + Widget build(BuildContext context) { + String generateAuthorsString() { + final authors = (itemBookMetadata)?.authors ?? []; + if (authors.isEmpty) { + return (bookDetailsCached?.metadata as shelfsdk.BookMetadataMinified?) + ?.authorName ?? + ''; + } + return authors.map((e) => e.name).join(', '); + } + + return Row( + children: [ + Container( + margin: const EdgeInsets.only(right: 8), + child: FaIcon( + FontAwesomeIcons.penNib, + size: 16, + color: coverColorScheme?.primary ?? + Theme.of(context).colorScheme.onBackground, + ), + ), + Expanded( + child: Text( + style: Theme.of(context).textTheme.titleSmall, + generateAuthorsString(), + ), + ), + ], + ); + } +} + +/// Calculate the width of the book cover based on the screen size double calculateWidth( BuildContext context, BoxConstraints constraints, { - double heightRatio = 0.25, - double widthRatio = 0.9, + /// 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, }) { final availHeight = min(constraints.maxHeight, MediaQuery.of(context).size.height); final availWidth = min(constraints.maxWidth, MediaQuery.of(context).size.width); - // make the width 90% of the available width + // make the width widthRatio of the available width var width = availWidth * widthRatio; - // but never exceed more than 25% of height - if (width > availHeight * heightRatio) { - width = availHeight * heightRatio; + // but never exceed more than heightRatio of height + if (width > availHeight * maxHeightToUse) { + width = availHeight * maxHeightToUse; } return width; diff --git a/lib/pages/onboarding/onboarding_single_page.dart b/lib/pages/onboarding/onboarding_single_page.dart index 775df13..de7887c 100644 --- a/lib/pages/onboarding/onboarding_single_page.dart +++ b/lib/pages/onboarding/onboarding_single_page.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:whispering_pages/api/api_provider.dart'; import 'package:whispering_pages/api/authenticated_user_provider.dart'; import 'package:whispering_pages/api/server_provider.dart'; +import 'package:whispering_pages/router/router.dart'; import 'package:whispering_pages/settings/api_settings_provider.dart'; import 'package:whispering_pages/settings/models/models.dart' as model; import 'package:whispering_pages/widgets/add_new_server.dart'; @@ -86,10 +88,13 @@ class OnboardingSinglePage extends HookConsumerWidget { activeUser: authenticatedUser, ), ); + + // redirect to the library page + GoRouter.of(context).goNamed(Routes.home); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Login failed'), + content: Text('Login failed. Please check your credentials.'), ), ); // give focus back to the username field @@ -205,7 +210,7 @@ class RedirectToABS extends StatelessWidget { Future _launchUrl(Uri url) async { if (!await launchUrl( url, - mode: LaunchMode.inAppWebView, + mode: LaunchMode.platformDefault, webOnlyWindowName: '_blank', )) { // throw Exception('Could not launch $url'); diff --git a/lib/router/transitions/slide.dart b/lib/router/transitions/slide.dart index adeddc4..969edea 100644 --- a/lib/router/transitions/slide.dart +++ b/lib/router/transitions/slide.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; import 'package:go_router/go_router.dart'; // class CustomSlideTransition extends CustomTransitionPage { @@ -29,7 +28,8 @@ CustomTransitionPage buildPageWithDefaultTransition({ }) { return CustomTransitionPage( key: state.pageKey, - transitionDuration: 250.ms, + // transitionDuration: 1250.ms, + // reverseTransitionDuration: 1250.ms, child: child, transitionsBuilder: (context, animation, secondaryAnimation, child) => FadeTransition( diff --git a/lib/settings/api_settings_provider.g.dart b/lib/settings/api_settings_provider.g.dart index 4b7a856..f8b7f93 100644 --- a/lib/settings/api_settings_provider.g.dart +++ b/lib/settings/api_settings_provider.g.dart @@ -6,7 +6,7 @@ part of 'api_settings_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$apiSettingsHash() => r'f08d87b716b31bfb4040fc6440840ac97b7ee686'; +String _$apiSettingsHash() => r'5f826922e898bfe13e2536cee62862e83f15b603'; /// See also [ApiSettings]. @ProviderFor(ApiSettings) diff --git a/lib/settings/models/app_settings.dart b/lib/settings/models/app_settings.dart index 9d7172e..41f032c 100644 --- a/lib/settings/models/app_settings.dart +++ b/lib/settings/models/app_settings.dart @@ -12,6 +12,7 @@ part 'app_settings.g.dart'; class AppSettings with _$AppSettings { const factory AppSettings({ @Default(true) bool isDarkMode, + @Default(false) bool useMaterialThemeOnItemPage, }) = _AppSettings; factory AppSettings.fromJson(Map json) => diff --git a/lib/settings/models/app_settings.freezed.dart b/lib/settings/models/app_settings.freezed.dart index 395d7f2..e5eb67c 100644 --- a/lib/settings/models/app_settings.freezed.dart +++ b/lib/settings/models/app_settings.freezed.dart @@ -21,6 +21,7 @@ AppSettings _$AppSettingsFromJson(Map json) { /// @nodoc mixin _$AppSettings { bool get isDarkMode => throw _privateConstructorUsedError; + bool get useMaterialThemeOnItemPage => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -34,7 +35,7 @@ abstract class $AppSettingsCopyWith<$Res> { AppSettings value, $Res Function(AppSettings) then) = _$AppSettingsCopyWithImpl<$Res, AppSettings>; @useResult - $Res call({bool isDarkMode}); + $Res call({bool isDarkMode, bool useMaterialThemeOnItemPage}); } /// @nodoc @@ -51,12 +52,17 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings> @override $Res call({ Object? isDarkMode = null, + Object? useMaterialThemeOnItemPage = null, }) { return _then(_value.copyWith( isDarkMode: null == isDarkMode ? _value.isDarkMode : isDarkMode // ignore: cast_nullable_to_non_nullable as bool, + useMaterialThemeOnItemPage: null == useMaterialThemeOnItemPage + ? _value.useMaterialThemeOnItemPage + : useMaterialThemeOnItemPage // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } } @@ -69,7 +75,7 @@ abstract class _$$AppSettingsImplCopyWith<$Res> __$$AppSettingsImplCopyWithImpl<$Res>; @override @useResult - $Res call({bool isDarkMode}); + $Res call({bool isDarkMode, bool useMaterialThemeOnItemPage}); } /// @nodoc @@ -84,12 +90,17 @@ class __$$AppSettingsImplCopyWithImpl<$Res> @override $Res call({ Object? isDarkMode = null, + Object? useMaterialThemeOnItemPage = null, }) { return _then(_$AppSettingsImpl( isDarkMode: null == isDarkMode ? _value.isDarkMode : isDarkMode // ignore: cast_nullable_to_non_nullable as bool, + useMaterialThemeOnItemPage: null == useMaterialThemeOnItemPage + ? _value.useMaterialThemeOnItemPage + : useMaterialThemeOnItemPage // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -97,7 +108,8 @@ class __$$AppSettingsImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$AppSettingsImpl implements _AppSettings { - const _$AppSettingsImpl({this.isDarkMode = true}); + const _$AppSettingsImpl( + {this.isDarkMode = true, this.useMaterialThemeOnItemPage = false}); factory _$AppSettingsImpl.fromJson(Map json) => _$$AppSettingsImplFromJson(json); @@ -105,10 +117,13 @@ class _$AppSettingsImpl implements _AppSettings { @override @JsonKey() final bool isDarkMode; + @override + @JsonKey() + final bool useMaterialThemeOnItemPage; @override String toString() { - return 'AppSettings(isDarkMode: $isDarkMode)'; + return 'AppSettings(isDarkMode: $isDarkMode, useMaterialThemeOnItemPage: $useMaterialThemeOnItemPage)'; } @override @@ -117,12 +132,17 @@ class _$AppSettingsImpl implements _AppSettings { (other.runtimeType == runtimeType && other is _$AppSettingsImpl && (identical(other.isDarkMode, isDarkMode) || - other.isDarkMode == isDarkMode)); + other.isDarkMode == isDarkMode) && + (identical(other.useMaterialThemeOnItemPage, + useMaterialThemeOnItemPage) || + other.useMaterialThemeOnItemPage == + useMaterialThemeOnItemPage)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, isDarkMode); + int get hashCode => + Object.hash(runtimeType, isDarkMode, useMaterialThemeOnItemPage); @JsonKey(ignore: true) @override @@ -139,7 +159,9 @@ class _$AppSettingsImpl implements _AppSettings { } abstract class _AppSettings implements AppSettings { - const factory _AppSettings({final bool isDarkMode}) = _$AppSettingsImpl; + const factory _AppSettings( + {final bool isDarkMode, + final bool useMaterialThemeOnItemPage}) = _$AppSettingsImpl; factory _AppSettings.fromJson(Map json) = _$AppSettingsImpl.fromJson; @@ -147,6 +169,8 @@ abstract class _AppSettings implements AppSettings { @override bool get isDarkMode; @override + bool get useMaterialThemeOnItemPage; + @override @JsonKey(ignore: true) _$$AppSettingsImplCopyWith<_$AppSettingsImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/settings/models/app_settings.g.dart b/lib/settings/models/app_settings.g.dart index 94b8c3a..533f9c7 100644 --- a/lib/settings/models/app_settings.g.dart +++ b/lib/settings/models/app_settings.g.dart @@ -9,9 +9,12 @@ part of 'app_settings.dart'; _$AppSettingsImpl _$$AppSettingsImplFromJson(Map json) => _$AppSettingsImpl( isDarkMode: json['isDarkMode'] as bool? ?? true, + useMaterialThemeOnItemPage: + json['useMaterialThemeOnItemPage'] as bool? ?? false, ); Map _$$AppSettingsImplToJson(_$AppSettingsImpl instance) => { 'isDarkMode': instance.isDarkMode, + 'useMaterialThemeOnItemPage': instance.useMaterialThemeOnItemPage, }; diff --git a/lib/theme/theme_from_cover_provider.dart b/lib/theme/theme_from_cover_provider.dart new file mode 100644 index 0000000..d704aa4 --- /dev/null +++ b/lib/theme/theme_from_cover_provider.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shelfsdk/audiobookshelf_api.dart'; +import 'package:whispering_pages/api/image_provider.dart'; + +part 'theme_from_cover_provider.g.dart'; + +@riverpod +FutureOr themeFromCover( + ThemeFromCoverRef ref, + ImageProvider img, { + Brightness brightness = Brightness.dark, +}) { + return ColorScheme.fromImageProvider( + provider: img, + brightness: brightness, + ); +} + +@riverpod +FutureOr themeOfLibraryItem( + ThemeOfLibraryItemRef ref, + LibraryItem? item, { + Brightness brightness = Brightness.dark, +}) async { + if (item == null) { + return null; + } + final coverImage = await ref.watch(coverImageProvider(item).future); + final val = await ref.watch( + themeFromCoverProvider(MemoryImage(coverImage), brightness: brightness) + .future, + ); + return val; + // coverImage.when( + // data: (value) async { + // debugPrint('CoverImage: $value'); + // final val = ref.watch(themeFromCoverProvider(MemoryImage(value))); + // debugPrint('ColorScheme generated: $val'); + // ref.invalidateSelf(); + // return val; + // }, + // loading: () => null, + // error: (error, stackTrace) => null, + // ); + // return null; +} diff --git a/lib/theme/theme_from_cover_provider.g.dart b/lib/theme/theme_from_cover_provider.g.dart new file mode 100644 index 0000000..94f581a --- /dev/null +++ b/lib/theme/theme_from_cover_provider.g.dart @@ -0,0 +1,325 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'theme_from_cover_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$themeFromCoverHash() => r'e52e7b9c644f3fcc266cfc480b7003ec7492431c'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [themeFromCover]. +@ProviderFor(themeFromCover) +const themeFromCoverProvider = ThemeFromCoverFamily(); + +/// See also [themeFromCover]. +class ThemeFromCoverFamily extends Family> { + /// See also [themeFromCover]. + const ThemeFromCoverFamily(); + + /// See also [themeFromCover]. + ThemeFromCoverProvider call( + ImageProvider img, { + Brightness brightness = Brightness.dark, + }) { + return ThemeFromCoverProvider( + img, + brightness: brightness, + ); + } + + @override + ThemeFromCoverProvider getProviderOverride( + covariant ThemeFromCoverProvider provider, + ) { + return call( + provider.img, + brightness: provider.brightness, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'themeFromCoverProvider'; +} + +/// See also [themeFromCover]. +class ThemeFromCoverProvider extends AutoDisposeFutureProvider { + /// See also [themeFromCover]. + ThemeFromCoverProvider( + ImageProvider img, { + Brightness brightness = Brightness.dark, + }) : this._internal( + (ref) => themeFromCover( + ref as ThemeFromCoverRef, + img, + brightness: brightness, + ), + from: themeFromCoverProvider, + name: r'themeFromCoverProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$themeFromCoverHash, + dependencies: ThemeFromCoverFamily._dependencies, + allTransitiveDependencies: + ThemeFromCoverFamily._allTransitiveDependencies, + img: img, + brightness: brightness, + ); + + ThemeFromCoverProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.img, + required this.brightness, + }) : super.internal(); + + final ImageProvider img; + final Brightness brightness; + + @override + Override overrideWith( + FutureOr Function(ThemeFromCoverRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: ThemeFromCoverProvider._internal( + (ref) => create(ref as ThemeFromCoverRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + img: img, + brightness: brightness, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _ThemeFromCoverProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is ThemeFromCoverProvider && + other.img == img && + other.brightness == brightness; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, img.hashCode); + hash = _SystemHash.combine(hash, brightness.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin ThemeFromCoverRef on AutoDisposeFutureProviderRef { + /// The parameter `img` of this provider. + ImageProvider get img; + + /// The parameter `brightness` of this provider. + Brightness get brightness; +} + +class _ThemeFromCoverProviderElement + extends AutoDisposeFutureProviderElement + with ThemeFromCoverRef { + _ThemeFromCoverProviderElement(super.provider); + + @override + ImageProvider get img => (origin as ThemeFromCoverProvider).img; + @override + Brightness get brightness => (origin as ThemeFromCoverProvider).brightness; +} + +String _$themeOfLibraryItemHash() => + r'53be78f35075ced924e7b2f3cb7310a09d4cd232'; + +/// See also [themeOfLibraryItem]. +@ProviderFor(themeOfLibraryItem) +const themeOfLibraryItemProvider = ThemeOfLibraryItemFamily(); + +/// See also [themeOfLibraryItem]. +class ThemeOfLibraryItemFamily extends Family> { + /// See also [themeOfLibraryItem]. + const ThemeOfLibraryItemFamily(); + + /// See also [themeOfLibraryItem]. + ThemeOfLibraryItemProvider call( + LibraryItem? item, { + Brightness brightness = Brightness.dark, + }) { + return ThemeOfLibraryItemProvider( + item, + brightness: brightness, + ); + } + + @override + ThemeOfLibraryItemProvider getProviderOverride( + covariant ThemeOfLibraryItemProvider provider, + ) { + return call( + provider.item, + brightness: provider.brightness, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'themeOfLibraryItemProvider'; +} + +/// See also [themeOfLibraryItem]. +class ThemeOfLibraryItemProvider + extends AutoDisposeFutureProvider { + /// See also [themeOfLibraryItem]. + ThemeOfLibraryItemProvider( + LibraryItem? item, { + Brightness brightness = Brightness.dark, + }) : this._internal( + (ref) => themeOfLibraryItem( + ref as ThemeOfLibraryItemRef, + item, + brightness: brightness, + ), + from: themeOfLibraryItemProvider, + name: r'themeOfLibraryItemProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$themeOfLibraryItemHash, + dependencies: ThemeOfLibraryItemFamily._dependencies, + allTransitiveDependencies: + ThemeOfLibraryItemFamily._allTransitiveDependencies, + item: item, + brightness: brightness, + ); + + ThemeOfLibraryItemProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.item, + required this.brightness, + }) : super.internal(); + + final LibraryItem? item; + final Brightness brightness; + + @override + Override overrideWith( + FutureOr Function(ThemeOfLibraryItemRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: ThemeOfLibraryItemProvider._internal( + (ref) => create(ref as ThemeOfLibraryItemRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + item: item, + brightness: brightness, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _ThemeOfLibraryItemProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is ThemeOfLibraryItemProvider && + other.item == item && + other.brightness == brightness; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, item.hashCode); + hash = _SystemHash.combine(hash, brightness.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin ThemeOfLibraryItemRef on AutoDisposeFutureProviderRef { + /// The parameter `item` of this provider. + LibraryItem? get item; + + /// The parameter `brightness` of this provider. + Brightness get brightness; +} + +class _ThemeOfLibraryItemProviderElement + extends AutoDisposeFutureProviderElement + with ThemeOfLibraryItemRef { + _ThemeOfLibraryItemProviderElement(super.provider); + + @override + LibraryItem? get item => (origin as ThemeOfLibraryItemProvider).item; + @override + Brightness get brightness => + (origin as ThemeOfLibraryItemProvider).brightness; +} +// 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/widgets/library_item_sliver_app_bar.dart b/lib/widgets/library_item_sliver_app_bar.dart new file mode 100644 index 0000000..01bd109 --- /dev/null +++ b/lib/widgets/library_item_sliver_app_bar.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class LibraryItemSliverAppBar extends StatelessWidget { + const LibraryItemSliverAppBar({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return SliverAppBar( + // backgroundColor: Colors.transparent, + elevation: 0, + floating: true, + primary: true, + snap: true, + actions: [ + // cast button + IconButton(onPressed: () {}, icon: const Icon(Icons.cast)), + IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)), + ], + ); + } +} diff --git a/lib/widgets/shelves/book_shelf.dart b/lib/widgets/shelves/book_shelf.dart index 2e6beea..0e6f165 100644 --- a/lib/widgets/shelves/book_shelf.dart +++ b/lib/widgets/shelves/book_shelf.dart @@ -73,82 +73,88 @@ class BookOnShelf extends HookConsumerWidget { // take up remaining space Expanded( child: Center( - child: InkWell( - onTap: () { - // open the book - context.pushNamed( - Routes.libraryItem.name, - pathParameters: { - Routes.libraryItem.pathParamName!: item.id, - }, - extra: LibraryItemExtras( - book: book, - heroTagSuffix: heroTagSuffix, - coverImage: coverImage.valueOrNull, - ), - ); - }, - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: coverImage.when( - data: (image) { - // return const BookCoverSkeleton(); - if (image.isEmpty) { - return const Icon(Icons.error); - } - var imageWidget = Image.memory( + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: coverImage.when( + data: (image) { + // return const BookCoverSkeleton(); + if (image.isEmpty) { + return const Icon(Icons.error); + } + var imageWidget = InkWell( + onTap: () { + // open the book + context.pushNamed( + Routes.libraryItem.name, + pathParameters: { + Routes.libraryItem.pathParamName!: item.id, + }, + extra: LibraryItemExtras( + book: book, + heroTagSuffix: heroTagSuffix, + coverImage: coverImage.valueOrNull, + ), + ); + }, + child: Image.memory( image, fit: BoxFit.fill, cacheWidth: (height * 1.2 * MediaQuery.of(context).devicePixelRatio) .round(), - ); - return Hero( - tag: HeroTagPrefixes.bookCover + - item.id + - heroTagSuffix, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - child: imageWidget, + ), + ); + return Hero( + tag: HeroTagPrefixes.bookCover + + item.id + + heroTagSuffix, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, ), - ); - }, - loading: () { - return const Center(child: BookCoverSkeleton()); - }, - error: (error, stack) { - return const Icon(Icons.error); - }, - ), + child: imageWidget, + ), + ); + }, + loading: () { + return const Center(child: BookCoverSkeleton()); + }, + error: (error, stack) { + return const Icon(Icons.error); + }, ), ), ), ), // the title and author of the book // AutoScrollText( - Text( - metadata.title ?? '', - // mode: AutoScrollTextMode.bouncing, - // curve: Curves.easeInOut, - // velocity: const Velocity(pixelsPerSecond: Offset(15, 0)), - // delayBefore: const Duration(seconds: 2), - // pauseBetween: const Duration(seconds: 2), - // numberOfReps: 15, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyLarge, + Hero( + tag: HeroTagPrefixes.bookTitle + item.id + heroTagSuffix, + child: Text( + metadata.title ?? '', + // mode: AutoScrollTextMode.bouncing, + // curve: Curves.easeInOut, + // velocity: const Velocity(pixelsPerSecond: Offset(15, 0)), + // delayBefore: const Duration(seconds: 2), + // pauseBetween: const Duration(seconds: 2), + // numberOfReps: 15, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyLarge, + ), ), const SizedBox(height: 3), - Text( - metadata.authorName ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall, + Hero( + tag: HeroTagPrefixes.authorName + item.id + heroTagSuffix, + child: Text( + metadata.authorName ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), ), ], ), diff --git a/pubspec.lock b/pubspec.lock index 71cc39a..1440e05 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -400,6 +400,14 @@ packages: description: flutter source: sdk version: "0.0.0" + font_awesome_flutter: + dependency: "direct main" + description: + name: font_awesome_flutter + sha256: "275ff26905134bcb59417cf60ad979136f1f8257f2f449914b2c3e05bbb4cd6f" + url: "https://pub.dev" + source: hosted + version: "10.7.0" freezed: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 525d84d..cec9aea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,7 @@ dependencies: flutter_cache_manager: ^3.3.2 flutter_hooks: ^0.20.5 flutter_settings_ui: ^3.0.1 + font_awesome_flutter: ^10.7.0 freezed_annotation: ^2.4.1 go_router: ^14.0.2 hive: ^4.0.0-dev.2