mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-02-17 14:59:35 +00:00
123
This commit is contained in:
parent
66439018fb
commit
861572db87
11 changed files with 326 additions and 209 deletions
|
|
@ -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;
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
);
|
||||||
),
|
},
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
44
lib/shared/widgets/skeletons.dart
Normal file
44
lib/shared/widgets/skeletons.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue