diff --git a/.vscode/launch.json b/.vscode/launch.json index 44a2752..cc37f37 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,7 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { "name": "whispering_pages", "request": "launch", diff --git a/lib/api/image_provider.dart b/lib/api/image_provider.dart index 37245d6..07b02ba 100644 --- a/lib/api/image_provider.dart +++ b/lib/api/image_provider.dart @@ -20,11 +20,17 @@ class CoverImage extends _$CoverImage { Stream build(LibraryItem libraryItem) 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.getFileFromCache(libraryItem.id); if (file != null) { // if the image is in the cache, yield it + debugPrint( + 'cover image found in cache for ${libraryItem.id} at ${file.file.path}', + ); yield await file.file.readAsBytes(); // return if no need to fetch from the server if (libraryItem.updatedAt.isBefore(await file.file.lastModified())) { @@ -39,7 +45,7 @@ class CoverImage extends _$CoverImage { // check if the image is in the cache final coverImage = await api.items.getCover( libraryItemId: libraryItem.id, - parameters: const GetImageReqParams(width: 500), + parameters: const GetImageReqParams(width: 1000), ); // save the image to the cache final newFile = await imageCacheManager.putFile( diff --git a/lib/api/image_provider.g.dart b/lib/api/image_provider.g.dart index 25cf5af..ac06dbf 100644 --- a/lib/api/image_provider.g.dart +++ b/lib/api/image_provider.g.dart @@ -6,7 +6,7 @@ part of 'image_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$coverImageHash() => r'34c6aaf6831fea198984d22ecdf2c5b74e110891'; +String _$coverImageHash() => r'57a164772b0350cd451535ed9d6347ff74671d2e'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 2a83101..c0fc365 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -38,19 +38,16 @@ class HomePage extends HookConsumerWidget { child: views.when( data: (data) { final shelvesToDisplay = data - .where((element) => !element.id.contains('discover')) - .map( - (shelf) => HomeShelf( - title: Text(shelf.label), - shelf: shelf, - ), - ) - .toList(); + // .where((element) => !element.id.contains('discover')) + .map((shelf) { + debugPrint('building shelf ${shelf.label}'); + return HomeShelf( + title: shelf.label, + shelf: shelf, + ); + }).toList(); return RefreshIndicator( onRefresh: () async { - // await ref - // .read(personalizedViewProvider.notifier) - // .forceRefresh(); return ref.refresh(personalizedViewProvider); }, child: ListView.separated( @@ -65,7 +62,7 @@ class HomePage extends HookConsumerWidget { ), ); }, - loading: () => const CircularProgressIndicator(), + loading: () => const HomePageSkeleton(), error: (error, stack) { return Text('Error: $error'); }, @@ -74,3 +71,17 @@ class HomePage extends HookConsumerWidget { ); } } + + +class HomePageSkeleton extends StatelessWidget { + const HomePageSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } +} diff --git a/lib/widgets/shelves/author_shelf.dart b/lib/widgets/shelves/author_shelf.dart index 0c48ebc..3a5e066 100644 --- a/lib/widgets/shelves/author_shelf.dart +++ b/lib/widgets/shelves/author_shelf.dart @@ -12,7 +12,7 @@ class AuthorHomeShelf extends HookConsumerWidget { required this.title, }); - final Widget title; + final String title; final AuthorShelf shelf; @override diff --git a/lib/widgets/shelves/book_shelf.dart b/lib/widgets/shelves/book_shelf.dart index 3a65184..35b6231 100644 --- a/lib/widgets/shelves/book_shelf.dart +++ b/lib/widgets/shelves/book_shelf.dart @@ -1,7 +1,9 @@ -import 'package:auto_scroll_text/auto_scroll_text.dart'; +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; +import 'package:shimmer/shimmer.dart' show Shimmer, ShimmerDirection; import 'package:whispering_pages/api/image_provider.dart'; import 'package:whispering_pages/widgets/shelves/home_shelf.dart'; @@ -13,7 +15,7 @@ class BookHomeShelf extends HookConsumerWidget { required this.title, }); - final Widget title; + final String title; final LibraryItemShelf shelf; @override @@ -49,69 +51,94 @@ class BookOnShelf extends HookConsumerWidget { final book = BookMinified.fromJson(item.media.toJson()); final metadata = BookMetadataMinified.fromJson(book.metadata.toJson()); final coverImage = ref.watch(coverImageProvider(item)); - const coverSize = 150.0; - return Container( - margin: const EdgeInsets.only(right: 10, bottom: 10), - constraints: const BoxConstraints(maxWidth: coverSize), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(10), - child: AspectRatio( - aspectRatio: 1, - child: Container( - constraints: const BoxConstraints(maxWidth: coverSize), - color: Colors.grey[800], - child: coverImage.when( - data: (image) { - if (image.isEmpty) { + return LayoutBuilder( + builder: (context, constraints) { + final height = min(constraints.maxHeight, 500); + final width = height * 0.75; + return SizedBox( + width: width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // the cover image of the book + // take up remaining space + Expanded( + // border radius + child: 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); - } - return Image.memory( - image, - fit: BoxFit.cover, - cacheWidth: - (coverSize * MediaQuery.of(context).devicePixelRatio) - .round(), - ); - }, - loading: () { - return const Center(child: CircularProgressIndicator()); - }, - 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, + ), + const SizedBox(height: 3), + Text( + metadata.authorName ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), + ], ), - Container( - margin: const EdgeInsets.all(5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AutoScrollText( - 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, - ), - const SizedBox(height: 3), - Text( - metadata.authorName ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), + ); + }, + ); + } +} + +// a skeleton for the book cover +class BookCoverSkeleton extends StatelessWidget { + const BookCoverSkeleton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1, + child: SizedBox( + width: 150, + child: Shimmer.fromColors( + baseColor: Theme.of(context).colorScheme.surface.withOpacity(0.3), + highlightColor: + Theme.of(context).colorScheme.onSurface.withOpacity(0.1), + child: Container( + color: Theme.of(context).colorScheme.surface, ), - ], + ), ), ); } diff --git a/lib/widgets/shelves/home_shelf.dart b/lib/widgets/shelves/home_shelf.dart index c881f33..af559af 100644 --- a/lib/widgets/shelves/home_shelf.dart +++ b/lib/widgets/shelves/home_shelf.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; @@ -14,7 +16,7 @@ class HomeShelf extends HookConsumerWidget { required this.title, }); - final Widget title; + final String title; final Shelf shelf; @override @@ -33,30 +35,46 @@ class HomeShelf extends HookConsumerWidget { } } -/// A shelf that displays books on the home page +/// A shelf that displays children on the home page class SimpleHomeShelf extends HookConsumerWidget { const SimpleHomeShelf({ super.key, required this.children, required this.title, + this.height, }); - final Widget title; + /// the title of the shelf + final String title; + + /// the children to display on the shelf final List children; + final double? height; @override Widget build(BuildContext context, WidgetRef ref) { + // if height is null take up 30% of the smallest screen dimension + return Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - title, + Text(title, style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 16), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: children, + SizedBox( + height: max( + min( + height ?? 0.3 * MediaQuery.of(context).size.shortestSide, + 200.0, + ), + 150.0, + ), + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) => children[index], + separatorBuilder: (context, index) => const SizedBox(width: 16), + itemCount: children.length, ), ), ], diff --git a/pubspec.lock b/pubspec.lock index 15c1a8f..0bd3643 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -379,6 +379,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" freezed: dependency: "direct dev" description: @@ -411,6 +416,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "9e0f7d1a3e7dc5010903e330fbc5497872c4c3cf6626381d69083cc1d5113c1e" + url: "https://pub.dev" + source: hosted + version: "14.0.2" graphs: dependency: transitive description: @@ -786,6 +799,14 @@ packages: relative: true source: path version: "1.0.0" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index e41bb60..32a86b8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,10 +44,11 @@ dependencies: flutter_hooks: ^0.20.5 flutter_settings_ui: ^3.0.1 freezed_annotation: ^2.4.1 + go_router: ^14.0.2 hive: ^4.0.0-dev.2 hooks_riverpod: ^2.5.1 - isar: *isar_version - isar_flutter_libs: *isar_version # contains Isar Core + isar: ^4.0.0-dev.13 + isar_flutter_libs: ^4.0.0-dev.13 json_annotation: ^4.9.0 lottie: ^3.1.0 path: ^1.9.0 @@ -56,6 +57,7 @@ dependencies: scroll_loop_auto_scroll: ^0.0.5 shelfsdk: path: ../../_dart/shelfsdk + shimmer: ^3.0.0 dev_dependencies: build_runner: ^2.4.9 custom_lint: ^0.6.4