⚗️ 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);
}
}