This commit is contained in:
rang 2025-12-20 11:54:14 +08:00
parent 66439018fb
commit 861572db87
11 changed files with 326 additions and 209 deletions

View file

@ -1,12 +1,12 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:hooks_riverpod/hooks_riverpod.dart' show Ref; import 'package:hooks_riverpod/hooks_riverpod.dart' show Ref;
import 'package:logging/logging.dart' show Logger; import 'package:logging/logging.dart' show Logger;
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart' import 'package:shelfsdk/audiobookshelf_api.dart'
show GetLibrarysItemsReqParams, Library, LibraryItemMinified, LibraryItem; show GetLibrarysItemsReqParams, Library, LibraryItem;
import 'package:vaani/api/api_provider.dart' show authenticatedApiProvider; import 'package:vaani/api/api_provider.dart' show authenticatedApiProvider;
import 'package:vaani/features/settings/api_settings_provider.dart' import 'package:vaani/features/settings/api_settings_provider.dart'
show apiSettingsProvider; show apiSettingsProvider;
import 'package:vaani/shared/extensions/model_conversions.dart';
part 'library_provider.g.dart'; part 'library_provider.g.dart';
@ -59,38 +59,177 @@ class Libraries extends _$Libraries {
} }
} }
@riverpod class LibraryItemsState {
class LibraryItemsParams extends _$LibraryItemsParams { final List<LibraryItem> items;
@override final int limit;
GetLibrarysItemsReqParams build() { final int page;
return const GetLibrarysItemsReqParams( final String? sort;
limit: 18, final bool? desc;
page: 0, final bool isLoading;
minified: true, final bool isRefreshing;
final bool hasMore;
final bool hasError;
final String? errorMessage;
const LibraryItemsState({
this.items = const [],
this.limit = 18,
this.page = 0,
this.sort,
this.desc,
this.isLoading = false,
this.isRefreshing = false,
this.hasMore = false,
this.hasError = false,
this.errorMessage,
});
LibraryItemsState copyWith({
List<LibraryItem>? items,
int? limit,
int? page,
String? sort,
bool? desc,
bool? isLoading,
bool? isRefreshing,
bool? hasMore,
bool? hasError,
String? errorMessage,
}) {
return LibraryItemsState(
items: items ?? this.items,
limit: limit ?? this.limit,
page: page ?? this.page,
sort: sort ?? this.sort,
desc: desc ?? this.desc,
isLoading: isLoading ?? this.isLoading,
isRefreshing: isRefreshing ?? this.isRefreshing,
hasMore: hasMore ?? this.hasMore,
hasError: hasError ?? this.hasError,
errorMessage: errorMessage ?? this.errorMessage,
); );
} }
factory LibraryItemsState.initial() => const LibraryItemsState();
}
@riverpod
class LibraryItems extends _$LibraryItems {
@override
LibraryItemsState build() {
//
Future.microtask(_loadInitialData);
return LibraryItemsState.initial();
}
//
Future<void> refresh() async {
if (state.isRefreshing) return;
//
state = state.copyWith(
page: 0,
isRefreshing: true,
hasError: false,
errorMessage: null,
);
try {
final items = await _load();
state = state.copyWith(
items: [...items],
page: state.page + 1,
isRefreshing: false,
hasMore: items.length == state.limit,
);
} catch (e) {
state = state.copyWith(
isRefreshing: false,
hasError: true,
errorMessage: e.toString(),
);
}
}
//
Future<void> _loadInitialData() async {
// await _loadMore(skip: true);
await _loadMore();
}
//
Future<void> loadMore() async {
if (state.isLoading || !state.hasMore) return;
await _loadMore();
}
//
Future<void> _loadMore({bool skip = false}) async {
if (!skip) {
state = state.copyWith(
isLoading: true,
hasError: false,
errorMessage: null,
);
}
try {
final items = await _load();
state = state.copyWith(
items: [...state.items, ...items],
page: state.page + 1,
isLoading: false,
hasMore: items.length == state.limit,
);
} catch (e) {
state = state.copyWith(
isLoading: false,
hasError: true,
errorMessage: e.toString(),
);
}
}
//
Future<List<LibraryItem>> _load() async {
final api = ref.read(authenticatedApiProvider);
final libraryId =
ref.watch(apiSettingsProvider.select((s) => s.activeLibraryId));
if (libraryId != null) {
final newItems = await api.libraries.getItems(
libraryId: libraryId,
parameters: GetLibrarysItemsReqParams(
limit: state.limit,
page: state.page,
sort: state.sort,
desc: state.desc,
minified: true,
),
);
return newItems?.results ?? [];
}
return [];
}
} }
// //
@riverpod // @riverpod
Future<List<LibraryItem>> currentLibraryItems(Ref ref) async { // Future<List<LibraryItem>> currentLibraryItems(Ref ref) async {
final api = ref.watch(authenticatedApiProvider); // final api = ref.watch(authenticatedApiProvider);
final libraryId = // final libraryId =
ref.watch(apiSettingsProvider.select((s) => s.activeLibraryId)); // ref.watch(apiSettingsProvider.select((s) => s.activeLibraryId));
if (libraryId == null) { // if (libraryId == null) {
_logger.warning('No active library id found'); // _logger.warning('No active library id found');
return []; // return [];
} // }
final items = await api.libraries.getItems( // final items = await api.libraries.getItems(
libraryId: libraryId, // libraryId: libraryId,
parameters: const GetLibrarysItemsReqParams( // parameters: const GetLibrarysItemsReqParams(
limit: 18, // limit: 18,
page: 0, // page: 0,
minified: true, // minified: true,
), // ),
); // );
if (items == null) { // if (items == null) {
return []; // return [];
} // }
return items.results; // return items.results;
} // }

View file

@ -173,26 +173,6 @@ final currentLibraryProvider = AutoDisposeFutureProvider<Library?>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef CurrentLibraryRef = AutoDisposeFutureProviderRef<Library?>; typedef CurrentLibraryRef = AutoDisposeFutureProviderRef<Library?>;
String _$currentLibraryItemsHash() =>
r'b0d0dcca86e760ee08f327c06b5ad5deaf7852e1';
/// See also [currentLibraryItems].
@ProviderFor(currentLibraryItems)
final currentLibraryItemsProvider =
AutoDisposeFutureProvider<List<LibraryItem>>.internal(
currentLibraryItems,
name: r'currentLibraryItemsProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$currentLibraryItemsHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef CurrentLibraryItemsRef
= AutoDisposeFutureProviderRef<List<LibraryItem>>;
String _$librariesHash() => r'95ebd4d1ac0cc2acf7617dc22895eff0ca30600f'; String _$librariesHash() => r'95ebd4d1ac0cc2acf7617dc22895eff0ca30600f';
/// See also [Libraries]. /// See also [Libraries].
@ -208,22 +188,20 @@ final librariesProvider =
); );
typedef _$Libraries = AutoDisposeAsyncNotifier<List<Library>>; typedef _$Libraries = AutoDisposeAsyncNotifier<List<Library>>;
String _$libraryItemsParamsHash() => String _$libraryItemsHash() => r'847ff8f5c325a786f257c2b98986098a9664cbb5';
r'9e7f11ab185eb99e926ae87e06466fc12aee7f72';
/// See also [LibraryItemsParams]. /// See also [LibraryItems].
@ProviderFor(LibraryItemsParams) @ProviderFor(LibraryItems)
final libraryItemsParamsProvider = AutoDisposeNotifierProvider< final libraryItemsProvider =
LibraryItemsParams, GetLibrarysItemsReqParams>.internal( AutoDisposeNotifierProvider<LibraryItems, LibraryItemsState>.internal(
LibraryItemsParams.new, LibraryItems.new,
name: r'libraryItemsParamsProvider', name: r'libraryItemsProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') debugGetCreateSourceHash:
? null const bool.fromEnvironment('dart.vm.product') ? null : _$libraryItemsHash,
: _$libraryItemsParamsHash,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
typedef _$LibraryItemsParams = AutoDisposeNotifier<GetLibrarysItemsReqParams>; typedef _$LibraryItems = AutoDisposeNotifier<LibraryItemsState>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -11,12 +11,12 @@ import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/constants/hero_tag_conventions.dart'; import 'package:vaani/constants/hero_tag_conventions.dart';
import 'package:vaani/features/explore/providers/search_controller.dart'; import 'package:vaani/features/explore/providers/search_controller.dart';
import 'package:vaani/features/explore/view/search_result_page.dart'; import 'package:vaani/features/explore/view/search_result_page.dart';
import 'package:vaani/generated/l10n.dart';
import 'package:vaani/router/router.dart';
import 'package:vaani/features/settings/api_settings_provider.dart'; import 'package:vaani/features/settings/api_settings_provider.dart';
import 'package:vaani/features/settings/app_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/generated/l10n.dart';
import 'package:vaani/router/router.dart';
import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; import 'package:vaani/shared/widgets/skeletons.dart';
const Duration debounceDuration = Duration(milliseconds: 500); const Duration debounceDuration = Duration(milliseconds: 500);

View file

@ -12,7 +12,7 @@ import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/router/models/library_item_extras.dart'; import 'package:vaani/router/models/library_item_extras.dart';
import 'package:vaani/shared/extensions/duration_format.dart'; import 'package:vaani/shared/extensions/duration_format.dart';
import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; import 'package:vaani/shared/widgets/skeletons.dart';
class LibraryItemHeroSection extends HookConsumerWidget { class LibraryItemHeroSection extends HookConsumerWidget {
const LibraryItemHeroSection({ const LibraryItemHeroSection({

View file

@ -2,7 +2,6 @@ import 'package:just_audio/just_audio.dart';
import 'package:just_audio_media_kit/just_audio_media_kit.dart'; import 'package:just_audio_media_kit/just_audio_media_kit.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:vaani/features/player/core/abs_audio_player.dart'; import 'package:vaani/features/player/core/abs_audio_player.dart';
import 'package:vaani/shared/extensions/chapter.dart';
final _logger = Logger('AbsPlatformAudioPlayer'); final _logger = Logger('AbsPlatformAudioPlayer');
@ -39,9 +38,9 @@ class AbsPlatformAudioPlayer extends AbsAudioPlayer {
positionInBook >= (chapter?.end ?? Duration.zero)) { positionInBook >= (chapter?.end ?? Duration.zero)) {
final chapter = book?.findChapterAtTime(positionInBook); final chapter = book?.findChapterAtTime(positionInBook);
if (chapter != currentChapter) { if (chapter != currentChapter) {
print('当前章节时长: ${currentChapter?.duration}'); // print('当前章节时长: ${currentChapter?.duration}');
print('切换章节时长: ${chapter?.duration}'); // print('切换章节时长: ${chapter?.duration}');
print('当前播放音轨时长: ${_player.duration}'); // print('当前播放音轨时长: ${_player.duration}');
chapterStreamController.add(chapter); chapterStreamController.add(chapter);
} }
} }

View file

@ -9,6 +9,7 @@ import 'package:vaani/router/router.dart';
import 'package:vaani/features/settings/api_settings_provider.dart'; import 'package:vaani/features/settings/api_settings_provider.dart';
import 'package:vaani/features/settings/app_settings_provider.dart' import 'package:vaani/features/settings/app_settings_provider.dart'
show appSettingsProvider; show appSettingsProvider;
import 'package:vaani/shared/widgets/skeletons.dart';
import '../shared/widgets/shelves/home_shelf.dart'; import '../shared/widgets/shelves/home_shelf.dart';
@ -112,7 +113,7 @@ class HomePage extends HookConsumerWidget {
), ),
); );
}, },
loading: () => const HomePageSkeleton(), loading: () => const PageSkeleton(),
error: (error, stack) { error: (error, stack) {
if (apiSettings.activeUser == null || if (apiSettings.activeUser == null ||
apiSettings.activeServer == null) { apiSettings.activeServer == null) {
@ -138,16 +139,3 @@ class HomePage extends HookConsumerWidget {
); );
} }
} }
class HomePageSkeleton extends StatelessWidget {
const HomePageSkeleton({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}

View file

@ -11,14 +11,15 @@ import 'package:vaani/api/image_provider.dart';
import 'package:vaani/api/library_provider.dart'; import 'package:vaani/api/library_provider.dart';
import 'package:vaani/features/you/view/widgets/library_switch_chip.dart'; import 'package:vaani/features/you/view/widgets/library_switch_chip.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/router/models/library_item_extras.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/icons/abs_icons.dart'; import 'package:vaani/shared/icons/abs_icons.dart';
import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; import 'package:vaani/shared/widgets/skeletons.dart';
// TODO: implement the library page // TODO: implement the library page
class LibraryPage extends HookConsumerWidget { class LibraryPage extends HookConsumerWidget {
const LibraryPage({this.libraryId, super.key}); LibraryPage({this.libraryId, super.key});
final String? libraryId; final String? libraryId;
@override @override
@ -38,7 +39,10 @@ class LibraryPage extends HookConsumerWidget {
// ); // );
// } // }
final views = ref.watch(currentLibraryItemsProvider); // final views = ref.watch(currentLibraryItemsProvider);
final pageData = ref.watch(libraryItemsProvider);
final items = pageData.items;
final scrollController = useScrollController(); final scrollController = useScrollController();
return Scaffold( return Scaffold(
@ -68,9 +72,7 @@ class LibraryPage extends HookConsumerWidget {
IconButton( IconButton(
icon: Icon(Icons.refresh), icon: Icon(Icons.refresh),
tooltip: '刷新', // Helpful tooltip for users tooltip: '刷新', // Helpful tooltip for users
onPressed: () { onPressed: () => ref.read(libraryItemsProvider.notifier).refresh(),
return ref.refresh(currentLibraryItemsProvider);
},
), ),
IconButton( IconButton(
icon: Icon(Icons.download), icon: Icon(Icons.download),
@ -83,36 +85,26 @@ class LibraryPage extends HookConsumerWidget {
), ),
// drawer: const MyDrawer(), // drawer: const MyDrawer(),
body: RefreshIndicator( body: RefreshIndicator(
onRefresh: () async { onRefresh: () => ref.read(libraryItemsProvider.notifier).refresh(),
return ref.refresh(currentLibraryItemsProvider); child: LayoutBuilder(
}, builder: (context, constraints) {
child: views.when( final height = getDefaultHeight(context);
data: (data) { // final height = min(constraints.maxHeight, 500.0);
return LayoutBuilder( final width = height * 0.75;
builder: (context, constraints) { return AlignedGridView.count(
final height = getDefaultHeight(context); crossAxisCount: constraints.maxWidth ~/ width,
// final height = min(constraints.maxHeight, 500.0); mainAxisSpacing: 12,
final width = height * 0.75; crossAxisSpacing: 12,
return AlignedGridView.count( padding: EdgeInsets.only(top: 0, left: 8, right: 8),
crossAxisCount: constraints.maxWidth ~/ width, itemCount: items.length,
mainAxisSpacing: 12, controller: scrollController,
crossAxisSpacing: 12, itemBuilder: (context, index) {
padding: EdgeInsets.only(top: 0, left: 8, right: 8), return LibraryPageItem(
itemCount: data.length, item: items[index],
controller: scrollController,
itemBuilder: (context, index) {
return LibraryPageItem(
item: data[index],
);
},
); );
}, },
); );
}, },
loading: () => const LibraryPageSkeleton(),
error: (error, stack) {
return Text('Error: $error');
},
), ),
), ),
); );
@ -137,20 +129,6 @@ class LibraryPage extends HookConsumerWidget {
} }
} }
//
class LibraryPageSkeleton extends StatelessWidget {
const LibraryPageSkeleton({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
class LibraryPageItem extends HookConsumerWidget { class LibraryPageItem extends HookConsumerWidget {
const LibraryPageItem({ const LibraryPageItem({
super.key, super.key,
@ -161,25 +139,37 @@ class LibraryPageItem extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final book = item.media.asBookMinified; final book = item.media.asBookMinified;
final metadata = book.metadata.asBookMetadataMinified; final metadata = book.metadata.asBookMetadataMinified;
return Column( return InkWell(
crossAxisAlignment: CrossAxisAlignment.start, onTap: () => context.pushNamed(
children: [ Routes.libraryItem.name,
BookCoverWidget(itemId: item.id), pathParameters: {
const SizedBox(height: 3), Routes.libraryItem.pathParamName!: item.id,
Text( },
metadata.title ?? '', extra: LibraryItemExtras(
maxLines: 1, book: book,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyLarge,
), ),
const SizedBox(height: 3), ),
Text( borderRadius: BorderRadius.circular(10),
metadata.authorName ?? '', child: Column(
maxLines: 1, crossAxisAlignment: CrossAxisAlignment.start,
overflow: TextOverflow.ellipsis, children: [
style: Theme.of(context).textTheme.bodySmall, BookCoverWidget(itemId: item.id),
), const SizedBox(height: 3),
], Text(
metadata.title ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 2),
Text(
metadata.authorName ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
); );
} }
} }
@ -196,26 +186,28 @@ class BookCoverWidget extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final coverImage = ref.watch(coverImageProvider(itemId)); final coverImage = ref.watch(coverImageProvider(itemId));
return coverImage.when( return ClipRRect(
data: (image) { borderRadius: BorderRadius.circular(10),
// return const BookCoverSkeleton(); child: coverImage.when(
if (image.isEmpty) { data: (image) {
return const Icon(Icons.error); if (image.isEmpty) {
} return const Icon(Icons.error);
}
return Image.memory( return Image.memory(
image, image,
fit: BoxFit.cover, fit: BoxFit.cover,
); );
}, },
loading: () { loading: () {
return const Center( return const Center(
child: BookCoverSkeleton(), child: BookCoverSkeleton(),
); );
}, },
error: (error, stack) { error: (error, stack) {
return const Center(child: Icon(Icons.error)); return const Center(child: Icon(Icons.error));
}, },
),
); );
} }
} }

View file

@ -126,7 +126,7 @@ class MyAppRouter {
GoRoute( GoRoute(
path: Routes.library.localPath, path: Routes.library.localPath,
name: Routes.library.name, name: Routes.library.name,
pageBuilder: defaultPageBuilder(const LibraryPage()), pageBuilder: defaultPageBuilder(LibraryPage()),
), ),
], ],
), ),

View file

@ -31,21 +31,24 @@ CustomTransitionPage buildPageWithDefaultTransition<T>({
// transitionDuration: 1250.ms, // transitionDuration: 1250.ms,
// reverseTransitionDuration: 1250.ms, // reverseTransitionDuration: 1250.ms,
child: child, child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) => transitionsBuilder: (context, animation, secondaryAnimation, child) {
FadeTransition( // CurvedAnimation
opacity: animation, final curvedAnimation = animation.drive(
child: SlideTransition( CurveTween(curve: Curves.easeOut),
position: animation.drive( );
Tween( return FadeTransition(
begin: const Offset(0, 1.50), opacity: animation,
child: SlideTransition(
position: Tween(
begin: const Offset(0, 0.3),
end: Offset.zero, end: Offset.zero,
).chain( ).animate(
CurveTween(curve: Curves.easeOut), curvedAnimation,
), ),
child: child,
), ),
child: child, );
), },
),
); );
} }

View file

@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:shimmer/shimmer.dart' show Shimmer;
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
import 'package:vaani/api/image_provider.dart'; import 'package:vaani/api/image_provider.dart';
import 'package:vaani/api/library_item_provider.dart' show libraryItemProvider; import 'package:vaani/api/library_item_provider.dart' show libraryItemProvider;
@ -17,6 +16,7 @@ import 'package:vaani/router/models/library_item_extras.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/widgets/shelves/home_shelf.dart'; import 'package:vaani/shared/widgets/shelves/home_shelf.dart';
import 'package:vaani/shared/widgets/skeletons.dart';
import 'package:vaani/theme/providers/theme_from_cover_provider.dart'; import 'package:vaani/theme/providers/theme_from_cover_provider.dart';
/// A shelf that displays books on the home page /// A shelf that displays books on the home page
@ -316,32 +316,6 @@ class _BookOnShelfPlayButton extends HookConsumerWidget {
} }
} }
// 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.withValues(alpha: 0.3),
highlightColor:
Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.1),
child: Container(
color: Theme.of(context).colorScheme.surface,
),
),
),
);
}
}
class BookCoverWidget extends HookConsumerWidget { class BookCoverWidget extends HookConsumerWidget {
const BookCoverWidget({ const BookCoverWidget({
super.key, super.key,

View file

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
///
//
class PageSkeleton extends StatelessWidget {
const PageSkeleton({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
// 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.withValues(alpha: 0.3),
highlightColor:
Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.1),
child: Container(
color: Theme.of(context).colorScheme.surface,
),
),
),
);
}
}