⚗️ Testing out new own pagination utils

This commit is contained in:
2025-12-04 23:43:35 +08:00
parent c6f104afc7
commit 6aba84e506
9 changed files with 329 additions and 449 deletions

View File

@@ -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<FileListItem> {
Future<Map<String, dynamic>?> 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<List<FileListItem>>
with AsyncPaginationController<FileListItem> {
String _currentPath = '/';
String? _poolId;
String? _query;
@@ -42,12 +52,7 @@ class CloudFileListNotifier extends _$CloudFileListNotifier
}
@override
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
@override
Future<CursorPagingData<FileListItem>> fetch({
required String? cursor,
}) async {
Future<List<FileListItem>> fetch() async {
final client = ref.read(apiClientProvider);
final queryParameters = <String, String>{'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<Map<String, dynamic>?> 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<FileListItem> {
class UnindexedFileListNotifier extends AsyncNotifier<List<FileListItem>>
with AsyncPaginationController<FileListItem> {
String? _poolId;
bool _recycled = false;
String? _query;
@@ -129,21 +129,15 @@ class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
ref.invalidateSelf();
}
@override
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
static const int pageSize = 20;
@override
Future<CursorPagingData<FileListItem>> fetch({
required String? cursor,
}) async {
Future<List<FileListItem>> fetch() async {
final client = ref.read(apiClientProvider);
final offset = cursor != null ? int.tryParse(cursor) ?? 0 : 0;
const take = 50; // Default page size
final queryParameters = <String, String>{
'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<SnCloudFile> files =
(response.data as List)
@@ -179,14 +173,7 @@ class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
final List<FileListItem> 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;
}
}

View File

@@ -44,47 +44,5 @@ final billingQuotaProvider =
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
String _$cloudFileListNotifierHash() =>
r'533dfa86f920b60cf7491fb4aeb95ece19e428af';
/// See also [CloudFileListNotifier].
@ProviderFor(CloudFileListNotifier)
final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
CloudFileListNotifier,
CursorPagingData<FileListItem>
>.internal(
CloudFileListNotifier.new,
name: r'cloudFileListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$cloudFileListNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$CloudFileListNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
String _$unindexedFileListNotifierHash() =>
r'afa487d7b956b71b21ca1b073a01364a34ede1d5';
/// See also [UnindexedFileListNotifier].
@ProviderFor(UnindexedFileListNotifier)
final unindexedFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
UnindexedFileListNotifier,
CursorPagingData<FileListItem>
>.internal(
UnindexedFileListNotifier.new,
name: r'unindexedFileListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$unindexedFileListNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$UnindexedFileListNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
// 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

82
lib/pods/paging.dart Normal file
View File

@@ -0,0 +1,82 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
abstract class PaginationController<T> {
int? get totalCount;
int get fetchedCount;
bool get fetchedAll;
FutureOr<List<T>> fetch();
Future<void> refresh();
Future<void> fetchFurther();
}
abstract class PaginationFiltered<F> {
late F currentFilter;
Future<void> applyFilter(F filter);
}
mixin AsyncPaginationController<T> on AsyncNotifier<List<T>>
implements PaginationController<T> {
@override
int? totalCount;
@override
int fetchedCount = 0;
@override
bool get fetchedAll => totalCount != null && fetchedCount >= totalCount!;
@override
FutureOr<List<T>> build() async => fetch();
@override
Future<void> refresh() async {
totalCount = null;
fetchedCount = 0;
state = AsyncLoading<List<T>>();
final newState = await AsyncValue.guard<List<T>>(() async {
return await fetch();
});
state = newState;
}
@override
Future<void> fetchFurther() async {
if (!fetchedAll) return;
state = AsyncLoading<List<T>>();
final newState = await AsyncValue.guard<List<T>>(() async {
final elements = await fetch();
return [...?state.valueOrNull, ...elements];
});
state = newState;
fetchedCount = newState.value?.length ?? 0;
}
}
mixin AsyncPaginationFilter<F, T> on AsyncPaginationController<T>
implements PaginationFiltered<F> {
@override
Future<void> applyFilter(F filter) async {
// Reset the data
totalCount = null;
fetchedCount = 0;
currentFilter = filter;
state = AsyncLoading<List<T>>();
final newState = await AsyncValue.guard<List<T>>(() async {
return await fetch();
});
state = newState;
}
}

65
lib/pods/timeline.dart Normal file
View File

@@ -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, List<SnTimelineEvent>>(
ActivityListNotifier.new,
);
class ActivityListNotifier extends AsyncNotifier<List<SnTimelineEvent>>
with
AsyncPaginationController<SnTimelineEvent>,
AsyncPaginationFilter<String?, SnTimelineEvent> {
static const int pageSize = 20;
@override
String? currentFilter;
@override
Future<List<SnTimelineEvent>> 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<SnTimelineEvent> items =
(response.data as List)
.map((e) => SnTimelineEvent.fromJson(e as Map<String, dynamic>))
.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);
}
}

View File

@@ -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<PostCreatedEvent>().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<EventCalendarQuery> query,
AsyncValue<List<dynamic>> events,
ValueNotifier<DateTime> 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<SnTimelineEvent> data;
final int widgetCount;
final Widget endItemView;
final ActivityListNotifier activitiesNotifier;
final List<SnTimelineEvent> 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<SnTimelineEvent> {
@override
Future<CursorPagingData<SnTimelineEvent>> build(String? filter) =>
fetch(cursor: null);
@override
Future<CursorPagingData<SnTimelineEvent>> 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<SnTimelineEvent> items =
(response.data as List)
.map((e) => SnTimelineEvent.fromJson(e as Map<String, dynamic>))
.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,
),
);
}
}

View File

@@ -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<CursorPagingData<SnTimelineEvent>> {
late final String? filter;
FutureOr<CursorPagingData<SnTimelineEvent>> build(String? filter);
}
/// See also [ActivityListNotifier].
@ProviderFor(ActivityListNotifier)
const activityListNotifierProvider = ActivityListNotifierFamily();
/// See also [ActivityListNotifier].
class ActivityListNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnTimelineEvent>>> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'activityListNotifierProvider';
}
/// See also [ActivityListNotifier].
class ActivityListNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
ActivityListNotifier,
CursorPagingData<SnTimelineEvent>
> {
/// 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<CursorPagingData<SnTimelineEvent>> 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<SnTimelineEvent>
>
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<CursorPagingData<SnTimelineEvent>> {
/// The parameter `filter` of this provider.
String? get filter;
}
class _ActivityListNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
ActivityListNotifier,
CursorPagingData<SnTimelineEvent>
>
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

View File

@@ -116,7 +116,7 @@ class FileListScreen extends HookConsumerWidget {
completer.future
.then((uploadedFile) {
if (uploadedFile != null) {
ref.invalidate(cloudFileListNotifierProvider);
ref.invalidate(indexedCloudFileListNotifierProvider);
}
})
.catchError((error) {

View File

@@ -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<bool>(false);
final poolsAsync = ref.watch(poolsProvider);
final isSelectionMode = useState<bool>(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<FileListItem> items,
int widgetCount,
Widget endItemView,
WidgetRef ref,
BuildContext context,
ValueNotifier<String> 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<FileListItem> items,
int widgetCount,
Widget endItemView,
WidgetRef ref,
BuildContext context,
ValueNotifier<FileListViewMode> 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 {

View File

@@ -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<T> extends HookConsumerWidget {
final ProviderListenable<AsyncValue<List<T>>> provider;
final Refreshable<PaginationController<T>> 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<T> extends HookConsumerWidget {
final ProviderListenable<AsyncValue<List<T>>> provider;
final Refreshable<PaginationController<T>> notifier;
final Widget Function(List<T>) 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;
}
}