mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-02-15 22:09:35 +00:00
226 lines
6.9 KiB
Dart
226 lines
6.9 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
|
import 'package:vaani/api/api_provider.dart';
|
|
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/models/library_item_extras.dart';
|
|
import 'package:vaani/router/router.dart';
|
|
import 'package:vaani/shared/extensions/model_conversions.dart';
|
|
import 'package:vaani/shared/extensions/style.dart';
|
|
import 'package:vaani/shared/icons/abs_icons.dart';
|
|
import 'package:vaani/shared/widgets/skeletons.dart';
|
|
|
|
// TODO: implement the library page
|
|
class LibraryPage extends HookConsumerWidget {
|
|
LibraryPage({this.libraryId, super.key});
|
|
|
|
final String? libraryId;
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
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;
|
|
// set the library id as the active library
|
|
// if (libraryId != null) {
|
|
// ref.read(apiSettingsProvider.notifier).updateState(
|
|
// ref.watch(apiSettingsProvider).copyWith(activeLibraryId: libraryId),
|
|
// );
|
|
// }
|
|
|
|
// final views = ref.watch(currentLibraryItemsProvider);
|
|
final pageData = ref.watch(libraryItemsProvider);
|
|
final items = pageData.items;
|
|
|
|
final scrollController = useScrollController();
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: GestureDetector(
|
|
child: Text(appBarTitle),
|
|
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);
|
|
},
|
|
),
|
|
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: () => ref.read(libraryItemsProvider.notifier).refresh(),
|
|
),
|
|
IconButton(
|
|
icon: Icon(Icons.download),
|
|
tooltip: S.of(context).bookDownloads, // Helpful tooltip for users
|
|
onPressed: () {
|
|
GoRouter.of(context).pushNamed(Routes.downloads.name);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
// drawer: const MyDrawer(),
|
|
body: RefreshIndicator(
|
|
onRefresh: () => ref.read(libraryItemsProvider.notifier).refresh(),
|
|
child: 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: items.length,
|
|
controller: scrollController,
|
|
itemBuilder: (context, index) {
|
|
return LibraryPageItem(
|
|
item: items[index],
|
|
width: width,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
class LibraryPageItem extends HookConsumerWidget {
|
|
const LibraryPageItem({
|
|
super.key,
|
|
required this.item,
|
|
required this.width,
|
|
});
|
|
final LibraryItem item;
|
|
final double width;
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final book = item.media.asBookMinified;
|
|
final metadata = book.metadata.asBookMetadataMinified;
|
|
final bodyLarge = Theme.of(context).textTheme.bodyLarge;
|
|
final bodySmall = Theme.of(context).textTheme.bodySmall;
|
|
final height = width +
|
|
15 +
|
|
(bodyLarge?.calculateHeight ?? 0) +
|
|
(bodySmall?.calculateHeight ?? 0);
|
|
return InkWell(
|
|
onTap: () => context.pushNamed(
|
|
Routes.libraryItem.name,
|
|
pathParameters: {
|
|
Routes.libraryItem.pathParamName!: item.id,
|
|
},
|
|
extra: LibraryItemExtras(
|
|
book: book,
|
|
),
|
|
),
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: SizedBox(
|
|
height: height,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(child: BookCoverWidget(itemId: item.id)),
|
|
const SizedBox(height: 3),
|
|
Text(
|
|
metadata.title ?? '',
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: bodyLarge,
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
metadata.authorName ?? '',
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: 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 ClipRRect(
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: coverImage.when(
|
|
data: (image) {
|
|
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));
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|