⚗️ Testing out new own pagination utils
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
82
lib/pods/paging.dart
Normal 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
65
lib/pods/timeline.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -116,7 +116,7 @@ class FileListScreen extends HookConsumerWidget {
|
||||
completer.future
|
||||
.then((uploadedFile) {
|
||||
if (uploadedFile != null) {
|
||||
ref.invalidate(cloudFileListNotifierProvider);
|
||||
ref.invalidate(indexedCloudFileListNotifierProvider);
|
||||
}
|
||||
})
|
||||
.catchError((error) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
87
lib/widgets/paging/pagination_list.dart
Normal file
87
lib/widgets/paging/pagination_list.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user