mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-06 02:59:28 +00:00
progress on home screen
This commit is contained in:
parent
865a662b56
commit
479242427a
9 changed files with 333 additions and 124 deletions
|
|
@ -1,11 +1,9 @@
|
|||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||
|
||||
extension LibraryItemConversion on LibraryItem {
|
||||
LibraryItemExpanded get asExpanded =>
|
||||
LibraryItemExpanded.fromJson(toJson());
|
||||
LibraryItemExpanded get asExpanded => LibraryItemExpanded.fromJson(toJson());
|
||||
|
||||
LibraryItemMinified get asMinified =>
|
||||
LibraryItemMinified.fromJson(toJson());
|
||||
LibraryItemMinified get asMinified => LibraryItemMinified.fromJson(toJson());
|
||||
}
|
||||
|
||||
extension MediaConversion on Media {
|
||||
|
|
@ -46,3 +44,10 @@ extension ShelfConversion on Shelf {
|
|||
SeriesShelf get asSeriesShelf => SeriesShelf.fromJson(toJson());
|
||||
AuthorShelf get asAuthorShelf => AuthorShelf.fromJson(toJson());
|
||||
}
|
||||
|
||||
extension UserConversion on User {
|
||||
UserWithSessionAndMostRecentProgress
|
||||
get asUserWithSessionAndMostRecentProgress =>
|
||||
UserWithSessionAndMostRecentProgress.fromJson(toJson());
|
||||
User get asUser => User.fromJson(toJson());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,24 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||
import 'package:shimmer/shimmer.dart' show Shimmer;
|
||||
import 'package:whispering_pages/api/api_provider.dart';
|
||||
import 'package:whispering_pages/api/image_provider.dart';
|
||||
import 'package:whispering_pages/api/library_item_provider.dart'
|
||||
show libraryItemProvider;
|
||||
import 'package:whispering_pages/constants/hero_tag_conventions.dart';
|
||||
import 'package:whispering_pages/features/item_viewer/view/library_item_actions.dart';
|
||||
import 'package:whispering_pages/features/player/providers/audiobook_player.dart';
|
||||
import 'package:whispering_pages/router/models/library_item_extras.dart';
|
||||
import 'package:whispering_pages/router/router.dart';
|
||||
import 'package:whispering_pages/settings/app_settings_provider.dart';
|
||||
import 'package:whispering_pages/shared/extensions/model_conversions.dart';
|
||||
import 'package:whispering_pages/shared/widgets/shelves/home_shelf.dart';
|
||||
import 'package:whispering_pages/theme/theme_from_cover_provider.dart';
|
||||
|
||||
/// A shelf that displays books on the home page
|
||||
class BookHomeShelf extends HookConsumerWidget {
|
||||
|
|
@ -49,10 +57,14 @@ class BookOnShelf extends HookConsumerWidget {
|
|||
super.key,
|
||||
required this.item,
|
||||
this.heroTagSuffix = '',
|
||||
this.showPlayButton = true,
|
||||
});
|
||||
|
||||
final LibraryItem item;
|
||||
|
||||
/// whether to show the play button on the book cover
|
||||
final bool showPlayButton;
|
||||
|
||||
/// makes the hero tag unique
|
||||
final String heroTagSuffix;
|
||||
|
||||
|
|
@ -65,97 +77,119 @@ class BookOnShelf extends HookConsumerWidget {
|
|||
builder: (context, constraints) {
|
||||
final height = min(constraints.maxHeight, 500);
|
||||
final width = height * 0.75;
|
||||
handleTapOnBook() {
|
||||
// open the book
|
||||
context.pushNamed(
|
||||
Routes.libraryItem.name,
|
||||
pathParameters: {
|
||||
Routes.libraryItem.pathParamName!: item.id,
|
||||
},
|
||||
extra: LibraryItemExtras(
|
||||
book: book,
|
||||
heroTagSuffix: heroTagSuffix,
|
||||
coverImage: coverImage.valueOrNull,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// the cover image of the book
|
||||
// take up remaining space hence the expanded
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Hero(
|
||||
tag: HeroTagPrefixes.bookCover + item.id + heroTagSuffix,
|
||||
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(
|
||||
image,
|
||||
fit: BoxFit.fill,
|
||||
cacheWidth: (height *
|
||||
1.2 *
|
||||
MediaQuery.of(context).devicePixelRatio)
|
||||
.round(),
|
||||
);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
child: InkWell(
|
||||
onTap: handleTapOnBook,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: 8.0, right: 4.0, left: 4.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// the cover image of the book
|
||||
// take up remaining space hence the expanded
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
Hero(
|
||||
tag: HeroTagPrefixes.bookCover +
|
||||
item.id +
|
||||
heroTagSuffix,
|
||||
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(
|
||||
image,
|
||||
fit: BoxFit.fill,
|
||||
cacheWidth: (height *
|
||||
1.2 *
|
||||
MediaQuery.of(context)
|
||||
.devicePixelRatio)
|
||||
.round(),
|
||||
);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
child: imageWidget,
|
||||
);
|
||||
},
|
||||
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);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
// a play button on the book cover
|
||||
if (showPlayButton)
|
||||
_BookOnShelfPlayButton(
|
||||
libraryItemId: item.id,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// the title and author of the book
|
||||
// AutoScrollText(
|
||||
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),
|
||||
Hero(
|
||||
tag: HeroTagPrefixes.authorName + item.id + heroTagSuffix,
|
||||
child: Text(
|
||||
metadata.authorName ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// the title and author of the book
|
||||
// AutoScrollText(
|
||||
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),
|
||||
Hero(
|
||||
tag: HeroTagPrefixes.authorName + item.id + heroTagSuffix,
|
||||
child: Text(
|
||||
metadata.authorName ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
@ -163,6 +197,113 @@ class BookOnShelf extends HookConsumerWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _BookOnShelfPlayButton extends HookConsumerWidget {
|
||||
const _BookOnShelfPlayButton({
|
||||
super.key,
|
||||
required this.libraryItemId,
|
||||
});
|
||||
|
||||
/// the id of the library item of the book
|
||||
final String libraryItemId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final me = ref.watch(meProvider);
|
||||
final player = ref.watch(audiobookPlayerProvider);
|
||||
final isCurrentBookSetInPlayer =
|
||||
player.book?.libraryItemId == libraryItemId;
|
||||
final isPlayingThisBook = player.playing && isCurrentBookSetInPlayer;
|
||||
|
||||
final userProgress = me.valueOrNull?.mediaProgress
|
||||
?.firstWhereOrNull((element) => element.libraryItemId == libraryItemId);
|
||||
final isBookCompleted = userProgress?.isFinished ?? false;
|
||||
|
||||
const size = 40.0;
|
||||
|
||||
// if there is user progress for this book show a circular progress indicator around the play button
|
||||
var strokeWidth = size / 8;
|
||||
|
||||
final useMaterialThemeOnItemPage =
|
||||
ref.watch(appSettingsProvider).useMaterialThemeOnItemPage;
|
||||
|
||||
AsyncValue<ColorScheme?> coverColorScheme = const AsyncValue.loading();
|
||||
if (useMaterialThemeOnItemPage && isCurrentBookSetInPlayer) {
|
||||
final itemFromApi = ref.watch(libraryItemProvider(libraryItemId));
|
||||
coverColorScheme = ref.watch(
|
||||
themeOfLibraryItemProvider(
|
||||
itemFromApi.valueOrNull,
|
||||
brightness: Theme.of(context).brightness,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Theme(
|
||||
// if current book is set in player, get theme from the cover image
|
||||
data: ThemeData(
|
||||
colorScheme:
|
||||
coverColorScheme.valueOrNull ?? Theme.of(context).colorScheme,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(strokeWidth / 2 + 2),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// the circular progress indicator
|
||||
if (userProgress != null)
|
||||
SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: CircularProgressIndicator(
|
||||
value: userProgress.progress,
|
||||
strokeWidth: strokeWidth,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary.withOpacity(0.8),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// the play button
|
||||
IconButton(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(
|
||||
EdgeInsets.zero,
|
||||
),
|
||||
minimumSize: WidgetStateProperty.all(
|
||||
const Size(size, size),
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Theme.of(context).colorScheme.onPrimary.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
final book =
|
||||
await ref.watch(libraryItemProvider(libraryItemId).future);
|
||||
|
||||
libraryItemPlayButtonOnPressed(
|
||||
ref: ref,
|
||||
book: book.media.asBookExpanded,
|
||||
userMediaProgress: userProgress,
|
||||
);
|
||||
},
|
||||
icon: Hero(
|
||||
tag: HeroTagPrefixes.libraryItemPlayButton + libraryItemId,
|
||||
child: DynamicItemPlayIcon(
|
||||
isBookCompleted: isBookCompleted,
|
||||
isPlayingThisBook: isPlayingThisBook,
|
||||
isCurrentBookSetInPlayer: isCurrentBookSetInPlayer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// a skeleton for the book cover
|
||||
class BookCoverSkeleton extends StatelessWidget {
|
||||
const BookCoverSkeleton({
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class SimpleHomeShelf extends HookConsumerWidget {
|
|||
return const SizedBox();
|
||||
}
|
||||
|
||||
return const SizedBox(width: 16);
|
||||
return const SizedBox(width: 4);
|
||||
},
|
||||
itemCount: children.length +
|
||||
2, // add some extra space at the start and end so that the first and last items are not at the edge
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue