⚗️ 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.dart';
|
||||||
import 'package:island/models/file_list_item.dart';
|
import 'package:island/models/file_list_item.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/pods/paging.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
|
||||||
|
|
||||||
part 'file_list.g.dart';
|
part 'file_list.g.dart';
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
class CloudFileListNotifier extends _$CloudFileListNotifier
|
Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
|
||||||
with CursorPagingNotifierMixin<FileListItem> {
|
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 _currentPath = '/';
|
||||||
String? _poolId;
|
String? _poolId;
|
||||||
String? _query;
|
String? _query;
|
||||||
@@ -42,12 +52,7 @@ class CloudFileListNotifier extends _$CloudFileListNotifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
Future<List<FileListItem>> fetch() async {
|
||||||
|
|
||||||
@override
|
|
||||||
Future<CursorPagingData<FileListItem>> fetch({
|
|
||||||
required String? cursor,
|
|
||||||
}) async {
|
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
|
|
||||||
final queryParameters = <String, String>{'path': _currentPath};
|
final queryParameters = <String, String>{'path': _currentPath};
|
||||||
@@ -83,21 +88,16 @@ class CloudFileListNotifier extends _$CloudFileListNotifier
|
|||||||
...files.map((file) => FileListItem.file(file)),
|
...files.map((file) => FileListItem.file(file)),
|
||||||
];
|
];
|
||||||
|
|
||||||
// The new API returns all files in the path, no pagination
|
return items;
|
||||||
return CursorPagingData(items: items, hasMore: false, nextCursor: null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@riverpod
|
final unindexedFileListNotifierProvider = AsyncNotifierProvider(
|
||||||
Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
|
UnindexedFileListNotifier.new,
|
||||||
final client = ref.read(apiClientProvider);
|
);
|
||||||
final response = await client.get('/drive/billing/usage');
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
@riverpod
|
class UnindexedFileListNotifier extends AsyncNotifier<List<FileListItem>>
|
||||||
class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
with AsyncPaginationController<FileListItem> {
|
||||||
with CursorPagingNotifierMixin<FileListItem> {
|
|
||||||
String? _poolId;
|
String? _poolId;
|
||||||
bool _recycled = false;
|
bool _recycled = false;
|
||||||
String? _query;
|
String? _query;
|
||||||
@@ -129,21 +129,15 @@ class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
|||||||
ref.invalidateSelf();
|
ref.invalidateSelf();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
static const int pageSize = 20;
|
||||||
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CursorPagingData<FileListItem>> fetch({
|
Future<List<FileListItem>> fetch() async {
|
||||||
required String? cursor,
|
|
||||||
}) async {
|
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
|
|
||||||
final offset = cursor != null ? int.tryParse(cursor) ?? 0 : 0;
|
|
||||||
const take = 50; // Default page size
|
|
||||||
|
|
||||||
final queryParameters = <String, String>{
|
final queryParameters = <String, String>{
|
||||||
'take': take.toString(),
|
'take': pageSize.toString(),
|
||||||
'offset': offset.toString(),
|
'offset': fetchedCount.toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (_poolId != null) {
|
if (_poolId != null) {
|
||||||
@@ -169,7 +163,7 @@ class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
|||||||
queryParameters: queryParameters,
|
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 =
|
final List<SnCloudFile> files =
|
||||||
(response.data as List)
|
(response.data as List)
|
||||||
@@ -179,14 +173,7 @@ class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
|||||||
final List<FileListItem> items =
|
final List<FileListItem> items =
|
||||||
files.map((file) => FileListItem.unindexedFile(file)).toList();
|
files.map((file) => FileListItem.unindexedFile(file)).toList();
|
||||||
|
|
||||||
final hasMore = offset + take < total;
|
return items;
|
||||||
final nextCursor = hasMore ? (offset + take).toString() : null;
|
|
||||||
|
|
||||||
return CursorPagingData(
|
|
||||||
items: items,
|
|
||||||
hasMore: hasMore,
|
|
||||||
nextCursor: nextCursor,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,47 +44,5 @@ final billingQuotaProvider =
|
|||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
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: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|||||||
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:desktop_drop/desktop_drop.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.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/realm.dart';
|
||||||
import 'package:island/models/webfeed.dart';
|
import 'package:island/models/webfeed.dart';
|
||||||
import 'package:island/pods/event_calendar.dart';
|
import 'package:island/pods/event_calendar.dart';
|
||||||
|
import 'package:island/pods/timeline.dart';
|
||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/screens/auth/login_modal.dart';
|
import 'package:island/screens/auth/login_modal.dart';
|
||||||
import 'package:island/screens/notification.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/widgets/app_scaffold.dart';
|
||||||
import 'package:island/models/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
import 'package:island/widgets/check_in.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/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_featured.dart';
|
||||||
import 'package:island/widgets/post/post_item.dart';
|
import 'package:island/widgets/post/post_item.dart';
|
||||||
import 'package:material_symbols_icons/symbols.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/realm/realm_card.dart';
|
||||||
import 'package:island/widgets/publisher/publisher_card.dart';
|
import 'package:island/widgets/publisher/publisher_card.dart';
|
||||||
import 'package:island/widgets/web_article_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/services/event_bus.dart';
|
||||||
import 'package:island/widgets/share/share_sheet.dart';
|
import 'package:island/widgets/share/share_sheet.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||||
|
|
||||||
part 'explore.g.dart';
|
|
||||||
|
|
||||||
Widget notificationIndicatorWidget(
|
Widget notificationIndicatorWidget(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required int count,
|
required int count,
|
||||||
@@ -114,13 +110,19 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
return () => tabController.removeListener(listener);
|
return () => tabController.removeListener(listener);
|
||||||
}, [tabController]);
|
}, [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
|
// Listen for post creation events to refresh activities
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
final subscription = eventBus.on<PostCreatedEvent>().listen((event) {
|
final subscription = eventBus.on<PostCreatedEvent>().listen((event) {
|
||||||
// Refresh all activity lists when a new post is created
|
ref.invalidate(activityListNotifierProvider);
|
||||||
ref.invalidate(activityListNotifierProvider(null));
|
|
||||||
ref.invalidate(activityListNotifierProvider('subscriptions'));
|
|
||||||
ref.invalidate(activityListNotifierProvider('friends'));
|
|
||||||
});
|
});
|
||||||
return subscription.cancel;
|
return subscription.cancel;
|
||||||
}, []);
|
}, []);
|
||||||
@@ -276,7 +278,6 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
query,
|
query,
|
||||||
events,
|
events,
|
||||||
selectedDay,
|
selectedDay,
|
||||||
currentFilter.value,
|
|
||||||
)
|
)
|
||||||
: _buildNarrowBody(context, ref, currentFilter.value),
|
: _buildNarrowBody(context, ref, currentFilter.value),
|
||||||
),
|
),
|
||||||
@@ -315,29 +316,15 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildActivityList(
|
Widget _buildActivityList(BuildContext context, WidgetRef ref) {
|
||||||
BuildContext context,
|
|
||||||
WidgetRef ref,
|
|
||||||
String? filter,
|
|
||||||
) {
|
|
||||||
final activitiesNotifier = ref.watch(
|
|
||||||
activityListNotifierProvider(filter).notifier,
|
|
||||||
);
|
|
||||||
|
|
||||||
final isWide = isWideScreen(context);
|
final isWide = isWideScreen(context);
|
||||||
|
|
||||||
return PagingHelperSliverView(
|
return PaginationWidget(
|
||||||
provider: activityListNotifierProvider(filter),
|
provider: activityListNotifierProvider,
|
||||||
futureRefreshable: activityListNotifierProvider(filter).future,
|
notifier: activityListNotifierProvider.notifier,
|
||||||
notifierRefreshable: activityListNotifierProvider(filter).notifier,
|
// Sliver list cannot provide refresh handled by the pagination list
|
||||||
contentBuilder:
|
isRefreshable: false,
|
||||||
(data, widgetCount, endItemView) => _ActivityListView(
|
contentBuilder: (data) => _ActivityListView(data: data, isWide: isWide),
|
||||||
data: data,
|
|
||||||
widgetCount: widgetCount,
|
|
||||||
endItemView: endItemView,
|
|
||||||
activitiesNotifier: activitiesNotifier,
|
|
||||||
isWide: isWide,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,13 +337,10 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
ValueNotifier<EventCalendarQuery> query,
|
ValueNotifier<EventCalendarQuery> query,
|
||||||
AsyncValue<List<dynamic>> events,
|
AsyncValue<List<dynamic>> events,
|
||||||
ValueNotifier<DateTime> selectedDay,
|
ValueNotifier<DateTime> selectedDay,
|
||||||
String? currentFilter,
|
|
||||||
) {
|
) {
|
||||||
final bodyView = _buildActivityList(context, ref, currentFilter);
|
final bodyView = _buildActivityList(context, ref);
|
||||||
|
|
||||||
final activitiesNotifier = ref.watch(
|
final notifier = ref.watch(activityListNotifierProvider.notifier);
|
||||||
activityListNotifierProvider(currentFilter).notifier,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
@@ -364,7 +348,7 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
Flexible(
|
Flexible(
|
||||||
flex: 3,
|
flex: 3,
|
||||||
child: ExtendedRefreshIndicator(
|
child: ExtendedRefreshIndicator(
|
||||||
onRefresh: () => Future.sync(activitiesNotifier.forceRefresh),
|
onRefresh: notifier.refresh,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
const SliverGap(12),
|
const SliverGap(12),
|
||||||
@@ -575,17 +559,15 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
notificationUnreadCountNotifierProvider,
|
notificationUnreadCountNotifierProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
final activitiesNotifier = ref.watch(
|
final bodyView = _buildActivityList(context, ref);
|
||||||
activityListNotifierProvider(currentFilter).notifier,
|
|
||||||
);
|
|
||||||
|
|
||||||
final bodyView = _buildActivityList(context, ref, currentFilter);
|
final notifier = ref.watch(activityListNotifierProvider.notifier);
|
||||||
|
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: ExtendedRefreshIndicator(
|
|
||||||
onRefresh: () => Future.sync(activitiesNotifier.forceRefresh),
|
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: ExtendedRefreshIndicator(
|
||||||
|
onRefresh: notifier.refresh,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
const SliverGap(8),
|
const SliverGap(8),
|
||||||
@@ -623,8 +605,8 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
bodyView,
|
bodyView,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
).padding(horizontal: 8),
|
|
||||||
),
|
),
|
||||||
|
).padding(horizontal: 8),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -741,31 +723,20 @@ class _DiscoveryActivityItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ActivityListView extends HookConsumerWidget {
|
class _ActivityListView extends HookConsumerWidget {
|
||||||
final CursorPagingData<SnTimelineEvent> data;
|
final List<SnTimelineEvent> data;
|
||||||
final int widgetCount;
|
|
||||||
final Widget endItemView;
|
|
||||||
final ActivityListNotifier activitiesNotifier;
|
|
||||||
final bool isWide;
|
final bool isWide;
|
||||||
|
|
||||||
const _ActivityListView({
|
const _ActivityListView({required this.data, required this.isWide});
|
||||||
required this.data,
|
|
||||||
required this.widgetCount,
|
|
||||||
required this.endItemView,
|
|
||||||
required this.activitiesNotifier,
|
|
||||||
required this.isWide,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final notifier = ref.watch(activityListNotifierProvider.notifier);
|
||||||
|
|
||||||
return SliverList.separated(
|
return SliverList.separated(
|
||||||
itemCount: widgetCount,
|
itemCount: data.length,
|
||||||
separatorBuilder: (_, _) => const Gap(8),
|
separatorBuilder: (_, _) => const Gap(8),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == widgetCount - 1) {
|
final item = data[index];
|
||||||
return endItemView;
|
|
||||||
}
|
|
||||||
|
|
||||||
final item = data.items[index];
|
|
||||||
if (item.data == null) {
|
if (item.data == null) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
@@ -778,13 +749,10 @@ class _ActivityListView extends HookConsumerWidget {
|
|||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
item: SnPost.fromJson(item.data!),
|
item: SnPost.fromJson(item.data!),
|
||||||
onRefresh: () {
|
onRefresh: () {
|
||||||
activitiesNotifier.forceRefresh();
|
notifier.refresh();
|
||||||
},
|
},
|
||||||
onUpdate: (post) {
|
onUpdate: (post) {
|
||||||
activitiesNotifier.updateOne(
|
notifier.updateOne(index, item.copyWith(data: post.toJson()));
|
||||||
index,
|
|
||||||
item.copyWith(data: post.toJson()),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
itemWidget = Card(margin: EdgeInsets.zero, child: itemWidget);
|
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
|
completer.future
|
||||||
.then((uploadedFile) {
|
.then((uploadedFile) {
|
||||||
if (uploadedFile != null) {
|
if (uploadedFile != null) {
|
||||||
ref.invalidate(cloudFileListNotifierProvider);
|
ref.invalidate(indexedCloudFileListNotifierProvider);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catchError((error) {
|
.catchError((error) {
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ import 'package:island/utils/format.dart';
|
|||||||
import 'package:island/utils/text.dart';
|
import 'package:island/utils/text.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.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:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
enum FileListMode { normal, unindexed }
|
enum FileListMode { normal, unindexed }
|
||||||
@@ -59,7 +59,9 @@ class FileListView extends HookConsumerWidget {
|
|||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (mode.value == FileListMode.normal) {
|
if (mode.value == FileListMode.normal) {
|
||||||
final notifier = ref.read(cloudFileListNotifierProvider.notifier);
|
final notifier = ref.read(
|
||||||
|
indexedCloudFileListNotifierProvider.notifier,
|
||||||
|
);
|
||||||
notifier.setPath(currentPath.value);
|
notifier.setPath(currentPath.value);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -70,7 +72,9 @@ class FileListView extends HookConsumerWidget {
|
|||||||
final unindexedNotifier = ref.read(
|
final unindexedNotifier = ref.read(
|
||||||
unindexedFileListNotifierProvider.notifier,
|
unindexedFileListNotifierProvider.notifier,
|
||||||
);
|
);
|
||||||
final cloudNotifier = ref.read(cloudFileListNotifierProvider.notifier);
|
final cloudNotifier = ref.read(
|
||||||
|
indexedCloudFileListNotifierProvider.notifier,
|
||||||
|
);
|
||||||
final recycled = useState<bool>(false);
|
final recycled = useState<bool>(false);
|
||||||
final poolsAsync = ref.watch(poolsProvider);
|
final poolsAsync = ref.watch(poolsProvider);
|
||||||
final isSelectionMode = useState<bool>(false);
|
final isSelectionMode = useState<bool>(false);
|
||||||
@@ -115,27 +119,26 @@ class FileListView extends HookConsumerWidget {
|
|||||||
|
|
||||||
final isRefreshing = ref.watch(
|
final isRefreshing = ref.watch(
|
||||||
mode.value == FileListMode.normal
|
mode.value == FileListMode.normal
|
||||||
? cloudFileListNotifierProvider.select((value) => value.isLoading)
|
? indexedCloudFileListNotifierProvider.select(
|
||||||
|
(value) => value.isLoading,
|
||||||
|
)
|
||||||
: unindexedFileListNotifierProvider.select(
|
: unindexedFileListNotifierProvider.select(
|
||||||
(value) => value.isLoading,
|
(value) => value.isLoading,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final bodyWidget = switch (mode.value) {
|
final bodyWidget = switch (mode.value) {
|
||||||
FileListMode.unindexed => PagingHelperSliverView(
|
FileListMode.unindexed => PaginationWidget(
|
||||||
provider: unindexedFileListNotifierProvider,
|
provider: unindexedFileListNotifierProvider,
|
||||||
futureRefreshable: unindexedFileListNotifierProvider.future,
|
notifier: unindexedFileListNotifierProvider.notifier,
|
||||||
notifierRefreshable: unindexedFileListNotifierProvider.notifier,
|
|
||||||
contentBuilder:
|
contentBuilder:
|
||||||
(data, widgetCount, endItemView) =>
|
(data) =>
|
||||||
data.items.isEmpty
|
data.isEmpty
|
||||||
? SliverToBoxAdapter(
|
? SliverToBoxAdapter(
|
||||||
child: _buildEmptyUnindexedFilesHint(ref),
|
child: _buildEmptyUnindexedFilesHint(ref),
|
||||||
)
|
)
|
||||||
: _buildUnindexedFileListContent(
|
: _buildUnindexedFileListContent(
|
||||||
data.items,
|
data,
|
||||||
widgetCount,
|
|
||||||
endItemView,
|
|
||||||
ref,
|
ref,
|
||||||
context,
|
context,
|
||||||
viewMode,
|
viewMode,
|
||||||
@@ -144,20 +147,17 @@ class FileListView extends HookConsumerWidget {
|
|||||||
currentVisibleItems,
|
currentVisibleItems,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_ => PagingHelperSliverView(
|
_ => PaginationWidget(
|
||||||
provider: cloudFileListNotifierProvider,
|
provider: indexedCloudFileListNotifierProvider,
|
||||||
futureRefreshable: cloudFileListNotifierProvider.future,
|
notifier: indexedCloudFileListNotifierProvider.notifier,
|
||||||
notifierRefreshable: cloudFileListNotifierProvider.notifier,
|
|
||||||
contentBuilder:
|
contentBuilder:
|
||||||
(data, widgetCount, endItemView) =>
|
(data) =>
|
||||||
data.items.isEmpty
|
data.isEmpty
|
||||||
? SliverToBoxAdapter(
|
? SliverToBoxAdapter(
|
||||||
child: _buildEmptyDirectoryHint(ref, currentPath),
|
child: _buildEmptyDirectoryHint(ref, currentPath),
|
||||||
)
|
)
|
||||||
: _buildFileListContent(
|
: _buildFileListContent(
|
||||||
data.items,
|
data,
|
||||||
widgetCount,
|
|
||||||
endItemView,
|
|
||||||
ref,
|
ref,
|
||||||
context,
|
context,
|
||||||
currentPath,
|
currentPath,
|
||||||
@@ -255,7 +255,7 @@ class FileListView extends HookConsumerWidget {
|
|||||||
completer.future
|
completer.future
|
||||||
.then((uploadedFile) {
|
.then((uploadedFile) {
|
||||||
if (uploadedFile != null) {
|
if (uploadedFile != null) {
|
||||||
ref.invalidate(cloudFileListNotifierProvider);
|
ref.invalidate(indexedCloudFileListNotifierProvider);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catchError((error) {
|
.catchError((error) {
|
||||||
@@ -532,7 +532,7 @@ class FileListView extends HookConsumerWidget {
|
|||||||
isSelectionMode.value = false;
|
isSelectionMode.value = false;
|
||||||
ref.invalidate(
|
ref.invalidate(
|
||||||
mode.value == FileListMode.normal
|
mode.value == FileListMode.normal
|
||||||
? cloudFileListNotifierProvider
|
? indexedCloudFileListNotifierProvider
|
||||||
: unindexedFileListNotifierProvider,
|
: unindexedFileListNotifierProvider,
|
||||||
);
|
);
|
||||||
showSnackBar('Deleted $count files.');
|
showSnackBar('Deleted $count files.');
|
||||||
@@ -560,8 +560,6 @@ class FileListView extends HookConsumerWidget {
|
|||||||
|
|
||||||
Widget _buildFileListContent(
|
Widget _buildFileListContent(
|
||||||
List<FileListItem> items,
|
List<FileListItem> items,
|
||||||
int widgetCount,
|
|
||||||
Widget endItemView,
|
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
ValueNotifier<String> currentPath,
|
ValueNotifier<String> currentPath,
|
||||||
@@ -580,10 +578,6 @@ class FileListView extends HookConsumerWidget {
|
|||||||
crossAxisSpacing: 8,
|
crossAxisSpacing: 8,
|
||||||
mainAxisSpacing: 8,
|
mainAxisSpacing: 8,
|
||||||
delegate: SliverChildBuilderDelegate((context, index) {
|
delegate: SliverChildBuilderDelegate((context, index) {
|
||||||
if (index == widgetCount - 1) {
|
|
||||||
return endItemView;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index >= items.length) {
|
if (index >= items.length) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
@@ -615,16 +609,12 @@ class FileListView extends HookConsumerWidget {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}, childCount: widgetCount),
|
}, childCount: items.length),
|
||||||
),
|
),
|
||||||
// ListView mode
|
// ListView mode
|
||||||
_ => SliverList.builder(
|
_ => SliverList.builder(
|
||||||
itemCount: widgetCount,
|
itemCount: items.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == widgetCount - 1) {
|
|
||||||
return endItemView;
|
|
||||||
}
|
|
||||||
|
|
||||||
final item = items[index];
|
final item = items[index];
|
||||||
return item.map(
|
return item.map(
|
||||||
file:
|
file:
|
||||||
@@ -801,7 +791,7 @@ class FileListView extends HookConsumerWidget {
|
|||||||
await client.delete(
|
await client.delete(
|
||||||
'/drive/index/remove/${fileItem.fileIndex.id}',
|
'/drive/index/remove/${fileItem.fileIndex.id}',
|
||||||
);
|
);
|
||||||
ref.invalidate(cloudFileListNotifierProvider);
|
ref.invalidate(indexedCloudFileListNotifierProvider);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showSnackBar('failedToDeleteFile'.tr());
|
showSnackBar('failedToDeleteFile'.tr());
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1010,8 +1000,6 @@ class FileListView extends HookConsumerWidget {
|
|||||||
|
|
||||||
Widget _buildUnindexedFileListContent(
|
Widget _buildUnindexedFileListContent(
|
||||||
List<FileListItem> items,
|
List<FileListItem> items,
|
||||||
int widgetCount,
|
|
||||||
Widget endItemView,
|
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
ValueNotifier<FileListViewMode> currentViewMode,
|
ValueNotifier<FileListViewMode> currentViewMode,
|
||||||
@@ -1029,10 +1017,6 @@ class FileListView extends HookConsumerWidget {
|
|||||||
crossAxisSpacing: 12,
|
crossAxisSpacing: 12,
|
||||||
mainAxisSpacing: 12,
|
mainAxisSpacing: 12,
|
||||||
delegate: SliverChildBuilderDelegate((context, index) {
|
delegate: SliverChildBuilderDelegate((context, index) {
|
||||||
if (index == widgetCount - 1) {
|
|
||||||
return endItemView;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index >= items.length) {
|
if (index >= items.length) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
@@ -1067,16 +1051,12 @@ class FileListView extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}, childCount: widgetCount),
|
}, childCount: items.length),
|
||||||
),
|
),
|
||||||
// ListView mode
|
// ListView mode
|
||||||
_ => SliverList.builder(
|
_ => SliverList.builder(
|
||||||
itemCount: widgetCount,
|
itemCount: items.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == widgetCount - 1) {
|
|
||||||
return endItemView;
|
|
||||||
}
|
|
||||||
|
|
||||||
final item = items[index];
|
final item = items[index];
|
||||||
return item.map(
|
return item.map(
|
||||||
file: (fileItem) {
|
file: (fileItem) {
|
||||||
@@ -1168,7 +1148,7 @@ class FileListView extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
await client.delete('/drive/index/remove/${fileItem.fileIndex.id}');
|
await client.delete('/drive/index/remove/${fileItem.fileIndex.id}');
|
||||||
ref.invalidate(cloudFileListNotifierProvider);
|
ref.invalidate(indexedCloudFileListNotifierProvider);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showSnackBar('failedToDeleteFile'.tr());
|
showSnackBar('failedToDeleteFile'.tr());
|
||||||
} finally {
|
} 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