Vaani/lib/pages/library_page.dart

222 lines
6.5 KiB
Dart
Raw Normal View History

2025-12-05 17:59:13 +08:00
import 'dart:math';
2024-05-09 00:41:19 -04:00
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
2025-12-05 17:59:13 +08:00
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:go_router/go_router.dart';
2024-05-09 00:41:19 -04:00
import 'package:hooks_riverpod/hooks_riverpod.dart';
2025-12-05 17:59:13 +08:00
import 'package:shelfsdk/audiobookshelf_api.dart';
2024-08-23 04:21:46 -04:00
import 'package:vaani/api/api_provider.dart';
2025-12-05 17:59:13 +08:00
import 'package:vaani/api/image_provider.dart';
import 'package:vaani/api/library_provider.dart';
import 'package:vaani/features/you/view/widgets/library_switch_chip.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/icons/abs_icons.dart';
import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
2024-05-09 00:41:19 -04:00
// TODO: implement the library page
class LibraryPage extends HookConsumerWidget {
const LibraryPage({this.libraryId, super.key});
final String? libraryId;
@override
Widget build(BuildContext context, WidgetRef ref) {
2025-12-05 17:59:13 +08:00
final currentLibrary = ref.watch(currentLibraryProvider).valueOrNull;
// Determine the icon to use, with a fallback
final IconData libraryIconData =
AbsIcons.getIconByName(currentLibrary?.icon) ?? Icons.library_books;
// Determine the title text
final String appBarTitle = currentLibrary?.name ?? S.of(context).library;
2024-05-09 00:41:19 -04:00
// set the library id as the active library
2025-12-05 17:59:13 +08:00
// if (libraryId != null) {
// ref.read(apiSettingsProvider.notifier).updateState(
// ref.watch(apiSettingsProvider).copyWith(activeLibraryId: libraryId),
// );
// }
2024-05-09 00:41:19 -04:00
2025-12-05 17:59:13 +08:00
final views = ref.watch(currentLibraryItemsProvider);
2024-05-09 00:41:19 -04:00
final scrollController = useScrollController();
return Scaffold(
appBar: AppBar(
title: GestureDetector(
2025-12-05 17:59:13 +08:00
child: Text(appBarTitle),
2024-05-09 00:41:19 -04:00
onTap: () {
// scroll to the top of the page
scrollController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
// refresh the view
ref.invalidate(personalizedViewProvider);
},
),
2025-12-05 17:59:13 +08:00
leading: IconButton(
icon: Icon(libraryIconData),
tooltip:
S.of(context).librarySwitchTooltip, // Helpful tooltip for users
onPressed: () {
showLibrarySwitcher(context, ref);
},
),
actions: [
IconButton(
icon: Icon(Icons.refresh),
tooltip: '刷新', // Helpful tooltip for users
onPressed: () {
return ref.refresh(currentLibraryItemsProvider);
},
),
IconButton(
icon: Icon(Icons.download),
tooltip: S.of(context).bookDownloads, // Helpful tooltip for users
onPressed: () {
GoRouter.of(context).pushNamed(Routes.downloads.name);
},
),
],
2024-05-09 00:41:19 -04:00
),
2025-12-05 17:59:13 +08:00
// drawer: const MyDrawer(),
body: RefreshIndicator(
onRefresh: () async {
return ref.refresh(currentLibraryItemsProvider);
},
2024-05-09 00:41:19 -04:00
child: views.when(
data: (data) {
2025-12-05 17:59:13 +08:00
return LayoutBuilder(
builder: (context, constraints) {
final height = getDefaultHeight(context);
// final height = min(constraints.maxHeight, 500.0);
final width = height * 0.75;
return AlignedGridView.count(
crossAxisCount: constraints.maxWidth ~/ width,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
padding: EdgeInsets.only(top: 0, left: 8, right: 8),
itemCount: data.length,
controller: scrollController,
itemBuilder: (context, index) {
return LibraryPageItem(
item: data[index],
);
},
);
2024-05-09 00:41:19 -04:00
},
);
},
loading: () => const LibraryPageSkeleton(),
error: (error, stack) {
return Text('Error: $error');
},
),
),
);
}
2025-12-05 17:59:13 +08:00
double getDefaultHeight(
BuildContext context, {
bool ignoreWidth = false,
atMin = 150.0,
perCent = 0.3,
}) {
double referenceSide;
if (ignoreWidth) {
referenceSide = MediaQuery.of(context).size.height;
} else {
referenceSide = min(
MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height,
);
}
return max(atMin, referenceSide * perCent);
}
2024-05-09 00:41:19 -04:00
}
2025-12-05 17:59:13 +08:00
// 加载
2024-05-09 00:41:19 -04:00
class LibraryPageSkeleton extends StatelessWidget {
const LibraryPageSkeleton({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
2025-12-05 17:59:13 +08:00
class LibraryPageItem extends HookConsumerWidget {
const LibraryPageItem({
super.key,
required this.item,
});
final LibraryItem item;
@override
Widget build(BuildContext context, WidgetRef ref) {
final book = item.media.asBookMinified;
final metadata = book.metadata.asBookMetadataMinified;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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: 3),
Text(
metadata.authorName ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
],
);
}
}
class BookCoverWidget extends HookConsumerWidget {
const BookCoverWidget({
super.key,
required this.itemId,
});
final String itemId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final coverImage = ref.watch(coverImageProvider(itemId));
return coverImage.when(
data: (image) {
// return const BookCoverSkeleton();
if (image.isEmpty) {
return const Icon(Icons.error);
}
return Image.memory(
image,
fit: BoxFit.cover,
);
},
loading: () {
return const Center(
child: BookCoverSkeleton(),
);
},
error: (error, stack) {
return const Center(child: Icon(Icons.error));
},
);
}
}