From 6aba84e506fc6f32c04bb91d89a5d8999d5c1b76 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 4 Dec 2025 23:43:35 +0800 Subject: [PATCH] :alembic: Testing out new own pagination utils --- lib/pods/file_list.dart | 65 ++++----- lib/pods/file_list.g.dart | 42 ------ lib/pods/paging.dart | 82 +++++++++++ lib/pods/timeline.dart | 65 +++++++++ lib/screens/explore.dart | 176 +++++------------------ lib/screens/explore.g.dart | 181 ------------------------ lib/screens/files/file_list.dart | 2 +- lib/widgets/file_list_view.dart | 78 ++++------ lib/widgets/paging/pagination_list.dart | 87 ++++++++++++ 9 files changed, 329 insertions(+), 449 deletions(-) create mode 100644 lib/pods/paging.dart create mode 100644 lib/pods/timeline.dart delete mode 100644 lib/screens/explore.g.dart create mode 100644 lib/widgets/paging/pagination_list.dart diff --git a/lib/pods/file_list.dart b/lib/pods/file_list.dart index 28a3279e..b2d4fce6 100644 --- a/lib/pods/file_list.dart +++ b/lib/pods/file_list.dart @@ -2,14 +2,24 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/file.dart'; import 'package:island/models/file_list_item.dart'; import 'package:island/pods/network.dart'; +import 'package:island/pods/paging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; part 'file_list.g.dart'; @riverpod -class CloudFileListNotifier extends _$CloudFileListNotifier - with CursorPagingNotifierMixin { +Future?> billingUsage(Ref ref) async { + final client = ref.read(apiClientProvider); + final response = await client.get('/drive/billing/usage'); + return response.data; +} + +final indexedCloudFileListNotifierProvider = AsyncNotifierProvider( + IndexedCloudFileListNotifier.new, +); + +class IndexedCloudFileListNotifier extends AsyncNotifier> + with AsyncPaginationController { String _currentPath = '/'; String? _poolId; String? _query; @@ -42,12 +52,7 @@ class CloudFileListNotifier extends _$CloudFileListNotifier } @override - Future> build() => fetch(cursor: null); - - @override - Future> fetch({ - required String? cursor, - }) async { + Future> fetch() async { final client = ref.read(apiClientProvider); final queryParameters = {'path': _currentPath}; @@ -83,21 +88,16 @@ class CloudFileListNotifier extends _$CloudFileListNotifier ...files.map((file) => FileListItem.file(file)), ]; - // The new API returns all files in the path, no pagination - return CursorPagingData(items: items, hasMore: false, nextCursor: null); + return items; } } -@riverpod -Future?> billingUsage(Ref ref) async { - final client = ref.read(apiClientProvider); - final response = await client.get('/drive/billing/usage'); - return response.data; -} +final unindexedFileListNotifierProvider = AsyncNotifierProvider( + UnindexedFileListNotifier.new, +); -@riverpod -class UnindexedFileListNotifier extends _$UnindexedFileListNotifier - with CursorPagingNotifierMixin { +class UnindexedFileListNotifier extends AsyncNotifier> + with AsyncPaginationController { String? _poolId; bool _recycled = false; String? _query; @@ -129,21 +129,15 @@ class UnindexedFileListNotifier extends _$UnindexedFileListNotifier ref.invalidateSelf(); } - @override - Future> build() => fetch(cursor: null); + static const int pageSize = 20; @override - Future> fetch({ - required String? cursor, - }) async { + Future> fetch() async { final client = ref.read(apiClientProvider); - final offset = cursor != null ? int.tryParse(cursor) ?? 0 : 0; - const take = 50; // Default page size - final queryParameters = { - 'take': take.toString(), - 'offset': offset.toString(), + 'take': pageSize.toString(), + 'offset': fetchedCount.toString(), }; if (_poolId != null) { @@ -169,7 +163,7 @@ class UnindexedFileListNotifier extends _$UnindexedFileListNotifier queryParameters: queryParameters, ); - final total = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0; + totalCount = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0; final List files = (response.data as List) @@ -179,14 +173,7 @@ class UnindexedFileListNotifier extends _$UnindexedFileListNotifier final List items = files.map((file) => FileListItem.unindexedFile(file)).toList(); - final hasMore = offset + take < total; - final nextCursor = hasMore ? (offset + take).toString() : null; - - return CursorPagingData( - items: items, - hasMore: hasMore, - nextCursor: nextCursor, - ); + return items; } } diff --git a/lib/pods/file_list.g.dart b/lib/pods/file_list.g.dart index e9263160..5285d3c8 100644 --- a/lib/pods/file_list.g.dart +++ b/lib/pods/file_list.g.dart @@ -44,47 +44,5 @@ final billingQuotaProvider = @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef BillingQuotaRef = AutoDisposeFutureProviderRef?>; -String _$cloudFileListNotifierHash() => - r'533dfa86f920b60cf7491fb4aeb95ece19e428af'; - -/// See also [CloudFileListNotifier]. -@ProviderFor(CloudFileListNotifier) -final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider< - CloudFileListNotifier, - CursorPagingData ->.internal( - CloudFileListNotifier.new, - name: r'cloudFileListNotifierProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$cloudFileListNotifierHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$CloudFileListNotifier = - AutoDisposeAsyncNotifier>; -String _$unindexedFileListNotifierHash() => - r'afa487d7b956b71b21ca1b073a01364a34ede1d5'; - -/// See also [UnindexedFileListNotifier]. -@ProviderFor(UnindexedFileListNotifier) -final unindexedFileListNotifierProvider = AutoDisposeAsyncNotifierProvider< - UnindexedFileListNotifier, - CursorPagingData ->.internal( - UnindexedFileListNotifier.new, - name: r'unindexedFileListNotifierProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$unindexedFileListNotifierHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$UnindexedFileListNotifier = - AutoDisposeAsyncNotifier>; // 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 diff --git a/lib/pods/paging.dart b/lib/pods/paging.dart new file mode 100644 index 00000000..0a5becee --- /dev/null +++ b/lib/pods/paging.dart @@ -0,0 +1,82 @@ +import 'dart:async'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +abstract class PaginationController { + int? get totalCount; + int get fetchedCount; + + bool get fetchedAll; + + FutureOr> fetch(); + + Future refresh(); + + Future fetchFurther(); +} + +abstract class PaginationFiltered { + late F currentFilter; + + Future applyFilter(F filter); +} + +mixin AsyncPaginationController on AsyncNotifier> + implements PaginationController { + @override + int? totalCount; + + @override + int fetchedCount = 0; + + @override + bool get fetchedAll => totalCount != null && fetchedCount >= totalCount!; + + @override + FutureOr> build() async => fetch(); + + @override + Future refresh() async { + totalCount = null; + fetchedCount = 0; + state = AsyncLoading>(); + + final newState = await AsyncValue.guard>(() async { + return await fetch(); + }); + state = newState; + } + + @override + Future fetchFurther() async { + if (!fetchedAll) return; + + state = AsyncLoading>(); + + final newState = await AsyncValue.guard>(() async { + final elements = await fetch(); + return [...?state.valueOrNull, ...elements]; + }); + + state = newState; + fetchedCount = newState.value?.length ?? 0; + } +} + +mixin AsyncPaginationFilter on AsyncPaginationController + implements PaginationFiltered { + @override + Future applyFilter(F filter) async { + // Reset the data + totalCount = null; + fetchedCount = 0; + currentFilter = filter; + + state = AsyncLoading>(); + + final newState = await AsyncValue.guard>(() async { + return await fetch(); + }); + state = newState; + } +} diff --git a/lib/pods/timeline.dart b/lib/pods/timeline.dart new file mode 100644 index 00000000..d1abd4eb --- /dev/null +++ b/lib/pods/timeline.dart @@ -0,0 +1,65 @@ +import 'package:flutter/foundation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/activity.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/pods/paging.dart'; + +final activityListNotifierProvider = + AsyncNotifierProvider>( + ActivityListNotifier.new, + ); + +class ActivityListNotifier extends AsyncNotifier> + with + AsyncPaginationController, + AsyncPaginationFilter { + static const int pageSize = 20; + + @override + String? currentFilter; + + @override + Future> fetch() async { + final client = ref.read(apiClientProvider); + + final cursor = + state.valueOrNull?.lastOrNull?.createdAt.toUtc().toIso8601String(); + + final queryParameters = { + if (cursor != null) 'cursor': cursor, + 'take': pageSize, + if (currentFilter != null) 'filter': currentFilter, + if (kDebugMode) + 'debugInclude': 'realms,publishers,articles,shuffledPosts', + }; + + final response = await client.get( + '/sphere/timeline', + queryParameters: queryParameters, + ); + + final List items = + (response.data as List) + .map((e) => SnTimelineEvent.fromJson(e as Map)) + .toList(); + + final hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty'; + + totalCount = + (state.valueOrNull?.length ?? 0) + + items.length + + (hasMore ? pageSize : 0); + + return items; + } + + void updateOne(int index, SnTimelineEvent activity) { + final currentState = state.valueOrNull; + if (currentState == null) return; + + final updatedItems = [...currentState]; + updatedItems[index] = activity; + + state = AsyncData(updatedItems); + } +} diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index e24c6fb4..50a51af7 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -1,6 +1,5 @@ import 'package:desktop_drop/desktop_drop.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -12,6 +11,7 @@ import 'package:island/models/publisher.dart'; import 'package:island/models/realm.dart'; import 'package:island/models/webfeed.dart'; import 'package:island/pods/event_calendar.dart'; +import 'package:island/pods/timeline.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/screens/auth/login_modal.dart'; import 'package:island/screens/notification.dart'; @@ -21,24 +21,20 @@ import 'package:island/widgets/account/friends_overview.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/models/post.dart'; import 'package:island/widgets/check_in.dart'; +import 'package:island/widgets/extended_refresh_indicator.dart'; import 'package:island/widgets/navigation/fab_menu.dart'; +import 'package:island/widgets/paging/pagination_list.dart'; import 'package:island/widgets/post/post_featured.dart'; import 'package:island/widgets/post/post_item.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; -import 'package:island/pods/network.dart'; import 'package:island/widgets/realm/realm_card.dart'; import 'package:island/widgets/publisher/publisher_card.dart'; import 'package:island/widgets/web_article_card.dart'; -import 'package:island/widgets/extended_refresh_indicator.dart'; import 'package:island/services/event_bus.dart'; import 'package:island/widgets/share/share_sheet.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:super_sliver_list/super_sliver_list.dart'; -part 'explore.g.dart'; - Widget notificationIndicatorWidget( BuildContext context, { required int count, @@ -114,13 +110,19 @@ class ExploreScreen extends HookConsumerWidget { return () => tabController.removeListener(listener); }, [tabController]); + final notifier = ref.watch(activityListNotifierProvider.notifier); + + useEffect(() { + Future(() { + notifier.applyFilter(currentFilter.value); + }); + return null; + }, [currentFilter.value]); + // Listen for post creation events to refresh activities useEffect(() { final subscription = eventBus.on().listen((event) { - // Refresh all activity lists when a new post is created - ref.invalidate(activityListNotifierProvider(null)); - ref.invalidate(activityListNotifierProvider('subscriptions')); - ref.invalidate(activityListNotifierProvider('friends')); + ref.invalidate(activityListNotifierProvider); }); return subscription.cancel; }, []); @@ -276,7 +278,6 @@ class ExploreScreen extends HookConsumerWidget { query, events, selectedDay, - currentFilter.value, ) : _buildNarrowBody(context, ref, currentFilter.value), ), @@ -315,29 +316,15 @@ class ExploreScreen extends HookConsumerWidget { ); } - Widget _buildActivityList( - BuildContext context, - WidgetRef ref, - String? filter, - ) { - final activitiesNotifier = ref.watch( - activityListNotifierProvider(filter).notifier, - ); - + Widget _buildActivityList(BuildContext context, WidgetRef ref) { final isWide = isWideScreen(context); - return PagingHelperSliverView( - provider: activityListNotifierProvider(filter), - futureRefreshable: activityListNotifierProvider(filter).future, - notifierRefreshable: activityListNotifierProvider(filter).notifier, - contentBuilder: - (data, widgetCount, endItemView) => _ActivityListView( - data: data, - widgetCount: widgetCount, - endItemView: endItemView, - activitiesNotifier: activitiesNotifier, - isWide: isWide, - ), + return PaginationWidget( + provider: activityListNotifierProvider, + notifier: activityListNotifierProvider.notifier, + // Sliver list cannot provide refresh handled by the pagination list + isRefreshable: false, + contentBuilder: (data) => _ActivityListView(data: data, isWide: isWide), ); } @@ -350,13 +337,10 @@ class ExploreScreen extends HookConsumerWidget { ValueNotifier query, AsyncValue> events, ValueNotifier selectedDay, - String? currentFilter, ) { - final bodyView = _buildActivityList(context, ref, currentFilter); + final bodyView = _buildActivityList(context, ref); - final activitiesNotifier = ref.watch( - activityListNotifierProvider(currentFilter).notifier, - ); + final notifier = ref.watch(activityListNotifierProvider.notifier); return Row( spacing: 12, @@ -364,7 +348,7 @@ class ExploreScreen extends HookConsumerWidget { Flexible( flex: 3, child: ExtendedRefreshIndicator( - onRefresh: () => Future.sync(activitiesNotifier.forceRefresh), + onRefresh: notifier.refresh, child: CustomScrollView( slivers: [ const SliverGap(12), @@ -575,17 +559,15 @@ class ExploreScreen extends HookConsumerWidget { notificationUnreadCountNotifierProvider, ); - final activitiesNotifier = ref.watch( - activityListNotifierProvider(currentFilter).notifier, - ); + final bodyView = _buildActivityList(context, ref); - final bodyView = _buildActivityList(context, ref, currentFilter); + final notifier = ref.watch(activityListNotifierProvider.notifier); return Expanded( - child: ExtendedRefreshIndicator( - onRefresh: () => Future.sync(activitiesNotifier.forceRefresh), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(8)), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: ExtendedRefreshIndicator( + onRefresh: notifier.refresh, child: CustomScrollView( slivers: [ const SliverGap(8), @@ -623,8 +605,8 @@ class ExploreScreen extends HookConsumerWidget { bodyView, ], ), - ).padding(horizontal: 8), - ), + ), + ).padding(horizontal: 8), ); } } @@ -741,31 +723,20 @@ class _DiscoveryActivityItem extends StatelessWidget { } class _ActivityListView extends HookConsumerWidget { - final CursorPagingData data; - final int widgetCount; - final Widget endItemView; - final ActivityListNotifier activitiesNotifier; + final List data; final bool isWide; - const _ActivityListView({ - required this.data, - required this.widgetCount, - required this.endItemView, - required this.activitiesNotifier, - required this.isWide, - }); + const _ActivityListView({required this.data, required this.isWide}); @override Widget build(BuildContext context, WidgetRef ref) { + final notifier = ref.watch(activityListNotifierProvider.notifier); + return SliverList.separated( - itemCount: widgetCount, + itemCount: data.length, separatorBuilder: (_, _) => const Gap(8), itemBuilder: (context, index) { - if (index == widgetCount - 1) { - return endItemView; - } - - final item = data.items[index]; + final item = data[index]; if (item.data == null) { return const SizedBox.shrink(); } @@ -778,13 +749,10 @@ class _ActivityListView extends HookConsumerWidget { borderRadius: 8, item: SnPost.fromJson(item.data!), onRefresh: () { - activitiesNotifier.forceRefresh(); + notifier.refresh(); }, onUpdate: (post) { - activitiesNotifier.updateOne( - index, - item.copyWith(data: post.toJson()), - ); + notifier.updateOne(index, item.copyWith(data: post.toJson())); }, ); itemWidget = Card(margin: EdgeInsets.zero, child: itemWidget); @@ -801,69 +769,3 @@ class _ActivityListView extends HookConsumerWidget { ); } } - -@riverpod -class ActivityListNotifier extends _$ActivityListNotifier - with CursorPagingNotifierMixin { - @override - Future> build(String? filter) => - fetch(cursor: null); - - @override - Future> fetch({ - required String? cursor, - }) async { - final client = ref.read(apiClientProvider); - final take = 20; - - final queryParameters = { - if (cursor != null) 'cursor': cursor, - 'take': take, - if (filter != null) 'filter': filter, - if (kDebugMode) - 'debugInclude': 'realms,publishers,articles,shuffledPosts', - }; - - final response = await client.get( - '/sphere/timeline', - queryParameters: queryParameters, - ); - - final List items = - (response.data as List) - .map((e) => SnTimelineEvent.fromJson(e as Map)) - .toList(); - - final hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty'; - final nextCursor = - items.isNotEmpty - ? items - .map((x) => x.createdAt) - .reduce((a, b) => a.isBefore(b) ? a : b) - .toUtc() - .toIso8601String() - : null; - - return CursorPagingData( - items: items, - hasMore: hasMore, - nextCursor: nextCursor, - ); - } - - void updateOne(int index, SnTimelineEvent activity) { - final currentState = state.valueOrNull; - if (currentState == null) return; - - final updatedItems = [...currentState.items]; - updatedItems[index] = activity; - - state = AsyncData( - CursorPagingData( - items: updatedItems, - hasMore: currentState.hasMore, - nextCursor: currentState.nextCursor, - ), - ); - } -} diff --git a/lib/screens/explore.g.dart b/lib/screens/explore.g.dart deleted file mode 100644 index 5dfc59a3..00000000 --- a/lib/screens/explore.g.dart +++ /dev/null @@ -1,181 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'explore.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$activityListNotifierHash() => - r'77ffc7852feffa5438b56fa26123d453b7c310cf'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -abstract class _$ActivityListNotifier - extends - BuildlessAutoDisposeAsyncNotifier> { - late final String? filter; - - FutureOr> build(String? filter); -} - -/// See also [ActivityListNotifier]. -@ProviderFor(ActivityListNotifier) -const activityListNotifierProvider = ActivityListNotifierFamily(); - -/// See also [ActivityListNotifier]. -class ActivityListNotifierFamily - extends Family>> { - /// See also [ActivityListNotifier]. - const ActivityListNotifierFamily(); - - /// See also [ActivityListNotifier]. - ActivityListNotifierProvider call(String? filter) { - return ActivityListNotifierProvider(filter); - } - - @override - ActivityListNotifierProvider getProviderOverride( - covariant ActivityListNotifierProvider provider, - ) { - return call(provider.filter); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'activityListNotifierProvider'; -} - -/// See also [ActivityListNotifier]. -class ActivityListNotifierProvider - extends - AutoDisposeAsyncNotifierProviderImpl< - ActivityListNotifier, - CursorPagingData - > { - /// See also [ActivityListNotifier]. - ActivityListNotifierProvider(String? filter) - : this._internal( - () => ActivityListNotifier()..filter = filter, - from: activityListNotifierProvider, - name: r'activityListNotifierProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$activityListNotifierHash, - dependencies: ActivityListNotifierFamily._dependencies, - allTransitiveDependencies: - ActivityListNotifierFamily._allTransitiveDependencies, - filter: filter, - ); - - ActivityListNotifierProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.filter, - }) : super.internal(); - - final String? filter; - - @override - FutureOr> runNotifierBuild( - covariant ActivityListNotifier notifier, - ) { - return notifier.build(filter); - } - - @override - Override overrideWith(ActivityListNotifier Function() create) { - return ProviderOverride( - origin: this, - override: ActivityListNotifierProvider._internal( - () => create()..filter = filter, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - filter: filter, - ), - ); - } - - @override - AutoDisposeAsyncNotifierProviderElement< - ActivityListNotifier, - CursorPagingData - > - createElement() { - return _ActivityListNotifierProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is ActivityListNotifierProvider && other.filter == filter; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, filter.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin ActivityListNotifierRef - on AutoDisposeAsyncNotifierProviderRef> { - /// The parameter `filter` of this provider. - String? get filter; -} - -class _ActivityListNotifierProviderElement - extends - AutoDisposeAsyncNotifierProviderElement< - ActivityListNotifier, - CursorPagingData - > - with ActivityListNotifierRef { - _ActivityListNotifierProviderElement(super.provider); - - @override - String? get filter => (origin as ActivityListNotifierProvider).filter; -} - -// 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 diff --git a/lib/screens/files/file_list.dart b/lib/screens/files/file_list.dart index c4600bd6..431d6795 100644 --- a/lib/screens/files/file_list.dart +++ b/lib/screens/files/file_list.dart @@ -116,7 +116,7 @@ class FileListScreen extends HookConsumerWidget { completer.future .then((uploadedFile) { if (uploadedFile != null) { - ref.invalidate(cloudFileListNotifierProvider); + ref.invalidate(indexedCloudFileListNotifierProvider); } }) .catchError((error) { diff --git a/lib/widgets/file_list_view.dart b/lib/widgets/file_list_view.dart index 4afd816a..5c626b22 100644 --- a/lib/widgets/file_list_view.dart +++ b/lib/widgets/file_list_view.dart @@ -22,9 +22,9 @@ import 'package:island/utils/format.dart'; import 'package:island/utils/text.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/cloud_files.dart'; +import 'package:island/widgets/paging/pagination_list.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:styled_widget/styled_widget.dart'; enum FileListMode { normal, unindexed } @@ -59,7 +59,9 @@ class FileListView extends HookConsumerWidget { useEffect(() { if (mode.value == FileListMode.normal) { - final notifier = ref.read(cloudFileListNotifierProvider.notifier); + final notifier = ref.read( + indexedCloudFileListNotifierProvider.notifier, + ); notifier.setPath(currentPath.value); } return null; @@ -70,7 +72,9 @@ class FileListView extends HookConsumerWidget { final unindexedNotifier = ref.read( unindexedFileListNotifierProvider.notifier, ); - final cloudNotifier = ref.read(cloudFileListNotifierProvider.notifier); + final cloudNotifier = ref.read( + indexedCloudFileListNotifierProvider.notifier, + ); final recycled = useState(false); final poolsAsync = ref.watch(poolsProvider); final isSelectionMode = useState(false); @@ -115,27 +119,26 @@ class FileListView extends HookConsumerWidget { final isRefreshing = ref.watch( mode.value == FileListMode.normal - ? cloudFileListNotifierProvider.select((value) => value.isLoading) + ? indexedCloudFileListNotifierProvider.select( + (value) => value.isLoading, + ) : unindexedFileListNotifierProvider.select( (value) => value.isLoading, ), ); final bodyWidget = switch (mode.value) { - FileListMode.unindexed => PagingHelperSliverView( + FileListMode.unindexed => PaginationWidget( provider: unindexedFileListNotifierProvider, - futureRefreshable: unindexedFileListNotifierProvider.future, - notifierRefreshable: unindexedFileListNotifierProvider.notifier, + notifier: unindexedFileListNotifierProvider.notifier, contentBuilder: - (data, widgetCount, endItemView) => - data.items.isEmpty + (data) => + data.isEmpty ? SliverToBoxAdapter( child: _buildEmptyUnindexedFilesHint(ref), ) : _buildUnindexedFileListContent( - data.items, - widgetCount, - endItemView, + data, ref, context, viewMode, @@ -144,20 +147,17 @@ class FileListView extends HookConsumerWidget { currentVisibleItems, ), ), - _ => PagingHelperSliverView( - provider: cloudFileListNotifierProvider, - futureRefreshable: cloudFileListNotifierProvider.future, - notifierRefreshable: cloudFileListNotifierProvider.notifier, + _ => PaginationWidget( + provider: indexedCloudFileListNotifierProvider, + notifier: indexedCloudFileListNotifierProvider.notifier, contentBuilder: - (data, widgetCount, endItemView) => - data.items.isEmpty + (data) => + data.isEmpty ? SliverToBoxAdapter( child: _buildEmptyDirectoryHint(ref, currentPath), ) : _buildFileListContent( - data.items, - widgetCount, - endItemView, + data, ref, context, currentPath, @@ -255,7 +255,7 @@ class FileListView extends HookConsumerWidget { completer.future .then((uploadedFile) { if (uploadedFile != null) { - ref.invalidate(cloudFileListNotifierProvider); + ref.invalidate(indexedCloudFileListNotifierProvider); } }) .catchError((error) { @@ -532,7 +532,7 @@ class FileListView extends HookConsumerWidget { isSelectionMode.value = false; ref.invalidate( mode.value == FileListMode.normal - ? cloudFileListNotifierProvider + ? indexedCloudFileListNotifierProvider : unindexedFileListNotifierProvider, ); showSnackBar('Deleted $count files.'); @@ -560,8 +560,6 @@ class FileListView extends HookConsumerWidget { Widget _buildFileListContent( List items, - int widgetCount, - Widget endItemView, WidgetRef ref, BuildContext context, ValueNotifier currentPath, @@ -580,10 +578,6 @@ class FileListView extends HookConsumerWidget { crossAxisSpacing: 8, mainAxisSpacing: 8, delegate: SliverChildBuilderDelegate((context, index) { - if (index == widgetCount - 1) { - return endItemView; - } - if (index >= items.length) { return const SizedBox.shrink(); } @@ -615,16 +609,12 @@ class FileListView extends HookConsumerWidget { return const SizedBox.shrink(); }, ); - }, childCount: widgetCount), + }, childCount: items.length), ), // ListView mode _ => SliverList.builder( - itemCount: widgetCount, + itemCount: items.length, itemBuilder: (context, index) { - if (index == widgetCount - 1) { - return endItemView; - } - final item = items[index]; return item.map( file: @@ -801,7 +791,7 @@ class FileListView extends HookConsumerWidget { await client.delete( '/drive/index/remove/${fileItem.fileIndex.id}', ); - ref.invalidate(cloudFileListNotifierProvider); + ref.invalidate(indexedCloudFileListNotifierProvider); } catch (e) { showSnackBar('failedToDeleteFile'.tr()); } finally { @@ -1010,8 +1000,6 @@ class FileListView extends HookConsumerWidget { Widget _buildUnindexedFileListContent( List items, - int widgetCount, - Widget endItemView, WidgetRef ref, BuildContext context, ValueNotifier currentViewMode, @@ -1029,10 +1017,6 @@ class FileListView extends HookConsumerWidget { crossAxisSpacing: 12, mainAxisSpacing: 12, delegate: SliverChildBuilderDelegate((context, index) { - if (index == widgetCount - 1) { - return endItemView; - } - if (index >= items.length) { return const SizedBox.shrink(); } @@ -1067,16 +1051,12 @@ class FileListView extends HookConsumerWidget { }, ), ); - }, childCount: widgetCount), + }, childCount: items.length), ), // ListView mode _ => SliverList.builder( - itemCount: widgetCount, + itemCount: items.length, itemBuilder: (context, index) { - if (index == widgetCount - 1) { - return endItemView; - } - final item = items[index]; return item.map( file: (fileItem) { @@ -1168,7 +1148,7 @@ class FileListView extends HookConsumerWidget { try { final client = ref.read(apiClientProvider); await client.delete('/drive/index/remove/${fileItem.fileIndex.id}'); - ref.invalidate(cloudFileListNotifierProvider); + ref.invalidate(indexedCloudFileListNotifierProvider); } catch (e) { showSnackBar('failedToDeleteFile'.tr()); } finally { diff --git a/lib/widgets/paging/pagination_list.dart b/lib/widgets/paging/pagination_list.dart new file mode 100644 index 00000000..4ddd0ea3 --- /dev/null +++ b/lib/widgets/paging/pagination_list.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/pods/paging.dart'; +import 'package:island/widgets/extended_refresh_indicator.dart'; +import 'package:super_sliver_list/super_sliver_list.dart'; + +class PaginationList extends HookConsumerWidget { + final ProviderListenable>> provider; + final Refreshable> notifier; + final Widget? Function(BuildContext, int, T) itemBuilder; + final bool isRefreshable; + const PaginationList({ + super.key, + required this.provider, + required this.notifier, + required this.itemBuilder, + this.isRefreshable = true, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final data = ref.watch(provider); + final noti = ref.watch(notifier); + final listView = SuperListView.builder( + itemBuilder: (context, idx) { + final entry = data.valueOrNull?[idx]; + if (entry != null) return itemBuilder(context, idx, entry); + return null; + }, + ); + + final child = NotificationListener( + onNotification: (ScrollNotification scrollInfo) { + if (scrollInfo is ScrollEndNotification && + scrollInfo.metrics.axisDirection == AxisDirection.down && + scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent) { + if (!noti.fetchedAll) { + noti.fetchFurther(); + } + } + return true; + }, + child: listView, + ); + + return isRefreshable + ? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: child) + : child; + } +} + +class PaginationWidget extends HookConsumerWidget { + final ProviderListenable>> provider; + final Refreshable> notifier; + final Widget Function(List) contentBuilder; + final bool isRefreshable; + const PaginationWidget({ + super.key, + required this.provider, + required this.notifier, + required this.contentBuilder, + this.isRefreshable = true, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final data = ref.watch(provider); + final noti = ref.watch(notifier); + final content = NotificationListener( + onNotification: (ScrollNotification scrollInfo) { + if (scrollInfo is ScrollEndNotification && + scrollInfo.metrics.axisDirection == AxisDirection.down && + scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent) { + if (!noti.fetchedAll) { + noti.fetchFurther(); + } + } + return true; + }, + child: contentBuilder(data.valueOrNull ?? []), + ); + + return isRefreshable + ? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: content) + : content; + } +}