diff --git a/lib/pods/paging.dart b/lib/pods/paging.dart index ab337600..4c779332 100644 --- a/lib/pods/paging.dart +++ b/lib/pods/paging.dart @@ -61,6 +61,47 @@ mixin AsyncPaginationController on AsyncNotifier> } } +mixin AutoDisposeAsyncPaginationController + on AutoDisposeAsyncNotifier> + implements PaginationController { + @override + int? totalCount; + + @override + int get fetchedCount => state.value?.length ?? 0; + + @override + bool get fetchedAll => totalCount != null && fetchedCount >= totalCount!; + + @override + FutureOr> build() async => fetch(); + + @override + Future refresh() async { + totalCount = null; + state = AsyncData>([]); + + final newState = await AsyncValue.guard>(() async { + return await fetch(); + }); + state = newState; + } + + @override + Future fetchFurther() async { + if (fetchedAll) return; + + state = AsyncLoading>(); + + final newState = await AsyncValue.guard>(() async { + final elements = await fetch(); + return [...?state.valueOrNull, ...elements]; + }); + + state = newState; + } +} + mixin FamilyAsyncPaginationController on AutoDisposeFamilyAsyncNotifier, Arg> implements PaginationController { diff --git a/lib/screens/posts/post_categories_list.dart b/lib/screens/posts/post_categories_list.dart index 5640efc1..d38244e5 100644 --- a/lib/screens/posts/post_categories_list.dart +++ b/lib/screens/posts/post_categories_list.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -6,134 +5,55 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/post_category.dart'; import 'package:island/models/post_tag.dart'; import 'package:island/pods/network.dart'; +import 'package:island/pods/paging.dart'; import 'package:island/widgets/app_scaffold.dart'; -import 'package:island/widgets/response.dart'; +import 'package:island/widgets/paging/pagination_list.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; // Post Categories Notifier -final postCategoriesNotifierProvider = StateNotifierProvider.autoDispose< +final postCategoriesNotifierProvider = AsyncNotifierProvider.autoDispose< PostCategoriesNotifier, - AsyncValue> ->((ref) { - return PostCategoriesNotifier(ref); -}); + List +>(PostCategoriesNotifier.new); class PostCategoriesNotifier - extends StateNotifier>> { - final AutoDisposeRef ref; - static const int _pageSize = 20; - bool _isLoading = false; + extends AutoDisposeAsyncNotifier> + with AutoDisposeAsyncPaginationController { + @override + Future> fetch() async { + final client = ref.read(apiClientProvider); - PostCategoriesNotifier(this.ref) : super(const AsyncValue.loading()) { - state = const AsyncValue.data( - CursorPagingData(items: [], hasMore: false, nextCursor: null), + final response = await client.get( + '/sphere/posts/categories', + queryParameters: {'offset': fetchedCount, 'take': 20, 'order': 'usage'}, ); - fetch(cursor: null); - } - Future fetch({String? cursor}) async { - if (_isLoading) return; - - _isLoading = true; - if (cursor == null) { - state = const AsyncValue.loading(); - } - - try { - final client = ref.read(apiClientProvider); - final offset = cursor == null ? 0 : int.parse(cursor); - - final response = await client.get( - '/sphere/posts/categories', - queryParameters: { - 'offset': offset, - 'take': _pageSize, - 'order': 'usage', - }, - ); - - final data = response.data as List; - final categories = - data.map((json) => SnPostCategory.fromJson(json)).toList(); - final hasMore = categories.length == _pageSize; - final nextCursor = - hasMore ? (offset + categories.length).toString() : null; - - state = AsyncValue.data( - CursorPagingData( - items: [...(state.value?.items ?? []), ...categories], - hasMore: hasMore, - nextCursor: nextCursor, - ), - ); - } catch (e, stack) { - state = AsyncValue.error(e, stack); - } finally { - _isLoading = false; - } + totalCount = int.parse(response.headers.value('X-Total') ?? '0'); + final data = response.data as List; + return data.map((json) => SnPostCategory.fromJson(json)).toList(); } } // Post Tags Notifier -final postTagsNotifierProvider = StateNotifierProvider.autoDispose< - PostTagsNotifier, - AsyncValue> ->((ref) { - return PostTagsNotifier(ref); -}); - -class PostTagsNotifier - extends StateNotifier>> { - final AutoDisposeRef ref; - static const int _pageSize = 20; - bool _isLoading = false; - - PostTagsNotifier(this.ref) : super(const AsyncValue.loading()) { - state = const AsyncValue.data( - CursorPagingData(items: [], hasMore: false, nextCursor: null), +final postTagsNotifierProvider = + AsyncNotifierProvider.autoDispose>( + PostTagsNotifier.new, ); - fetch(cursor: null); - } - Future fetch({String? cursor}) async { - if (_isLoading) return; +class PostTagsNotifier extends AutoDisposeAsyncNotifier> + with AutoDisposeAsyncPaginationController { + @override + Future> fetch() async { + final client = ref.read(apiClientProvider); - _isLoading = true; - if (cursor == null) { - state = const AsyncValue.loading(); - } + final response = await client.get( + '/sphere/posts/tags', + queryParameters: {'offset': fetchedCount, 'take': 20, 'order': 'usage'}, + ); - try { - final client = ref.read(apiClientProvider); - final offset = cursor == null ? 0 : int.parse(cursor); - - final response = await client.get( - '/sphere/posts/tags', - queryParameters: { - 'offset': offset, - 'take': _pageSize, - 'order': 'usage', - }, - ); - - final data = response.data as List; - final tags = data.map((json) => SnPostTag.fromJson(json)).toList(); - final hasMore = tags.length == _pageSize; - final nextCursor = hasMore ? (offset + tags.length).toString() : null; - - state = AsyncValue.data( - CursorPagingData( - items: [...(state.value?.items ?? []), ...tags], - hasMore: hasMore, - nextCursor: nextCursor, - ), - ); - } catch (e, stack) { - state = AsyncValue.error(e, stack); - } finally { - _isLoading = false; - } + totalCount = int.parse(response.headers.value('X-Total') ?? '0'); + final data = response.data as List; + return data.map((json) => SnPostTag.fromJson(json)).toList(); } } @@ -142,48 +62,27 @@ class PostCategoriesListScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final categoriesState = ref.watch(postCategoriesNotifierProvider); - return AppScaffold( appBar: AppBar(title: const Text('categories').tr()), - body: categoriesState.when( - data: (data) { - if (data.items.isEmpty) { - return const Center(child: Text('No categories found')); - } - return ListView.builder( - padding: EdgeInsets.zero, - itemCount: data.items.length + (data.hasMore ? 1 : 0), - itemBuilder: (context, index) { - if (index >= data.items.length) { - ref - .read(postCategoriesNotifierProvider.notifier) - .fetch(cursor: data.nextCursor); - return const Center(child: CircularProgressIndicator()); - } - final category = data.items[index]; - return ListTile( - leading: const Icon(Symbols.category), - contentPadding: EdgeInsets.symmetric(horizontal: 24), - trailing: const Icon(Symbols.chevron_right), - title: Text(category.categoryDisplayTitle), - subtitle: Text('postCount'.plural(category.usage)), - onTap: () { - context.pushNamed( - 'postCategoryDetail', - pathParameters: {'slug': category.slug}, - ); - }, + body: PaginationList( + provider: postCategoriesNotifierProvider, + notifier: postCategoriesNotifierProvider.notifier, + padding: EdgeInsets.zero, + itemBuilder: (context, index, category) { + return ListTile( + leading: const Icon(Symbols.category), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + trailing: const Icon(Symbols.chevron_right), + title: Text(category.categoryDisplayTitle), + subtitle: Text('postCount'.plural(category.usage)), + onTap: () { + context.pushNamed( + 'postCategoryDetail', + pathParameters: {'slug': category.slug}, ); }, ); }, - loading: () => const Center(child: CircularProgressIndicator()), - error: - (error, stack) => ResponseErrorWidget( - error: error, - onRetry: () => ref.invalidate(postCategoriesNotifierProvider), - ), ), ); } @@ -194,48 +93,27 @@ class PostTagsListScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final tagsState = ref.watch(postTagsNotifierProvider); - return AppScaffold( appBar: AppBar(title: const Text('tags').tr()), - body: tagsState.when( - data: (data) { - if (data.items.isEmpty) { - return const Center(child: Text('No tags found')); - } - return ListView.builder( - padding: EdgeInsets.zero, - itemCount: data.items.length + (data.hasMore ? 1 : 0), - itemBuilder: (context, index) { - if (index >= data.items.length) { - ref - .read(postTagsNotifierProvider.notifier) - .fetch(cursor: data.nextCursor); - return const Center(child: CircularProgressIndicator()); - } - final tag = data.items[index]; - return ListTile( - title: Text(tag.name ?? '#${tag.slug}'), - contentPadding: EdgeInsets.symmetric(horizontal: 24), - leading: const Icon(Symbols.label), - trailing: const Icon(Symbols.chevron_right), - subtitle: Text('postCount'.plural(tag.usage)), - onTap: () { - context.pushNamed( - 'postTagDetail', - pathParameters: {'slug': tag.slug}, - ); - }, + body: PaginationList( + provider: postTagsNotifierProvider, + notifier: postTagsNotifierProvider.notifier, + padding: EdgeInsets.zero, + itemBuilder: (context, index, tag) { + return ListTile( + title: Text(tag.name ?? '#${tag.slug}'), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Symbols.label), + trailing: const Icon(Symbols.chevron_right), + subtitle: Text('postCount'.plural(tag.usage)), + onTap: () { + context.pushNamed( + 'postTagDetail', + pathParameters: {'slug': tag.slug}, ); }, ); }, - loading: () => const Center(child: CircularProgressIndicator()), - error: - (error, stack) => ResponseErrorWidget( - error: error, - onRetry: () => ref.invalidate(postTagsNotifierProvider), - ), ), ); } diff --git a/lib/screens/posts/post_search.dart b/lib/screens/posts/post_search.dart index a6a248ee..34df0c59 100644 --- a/lib/screens/posts/post_search.dart +++ b/lib/screens/posts/post_search.dart @@ -7,18 +7,18 @@ import 'package:island/models/post.dart'; import 'package:island/pods/network.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/post/post_item.dart'; -import 'package:island/widgets/response.dart'; -import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; + +import 'package:island/pods/paging.dart'; +import 'package:island/widgets/paging/pagination_list.dart'; import 'package:styled_widget/styled_widget.dart'; -final postSearchNotifierProvider = StateNotifierProvider.autoDispose< - PostSearchNotifier, - AsyncValue> ->((ref) => PostSearchNotifier(ref)); +final postSearchNotifierProvider = + AsyncNotifierProvider.autoDispose>( + PostSearchNotifier.new, + ); -class PostSearchNotifier - extends StateNotifier>> { - final AutoDisposeRef ref; +class PostSearchNotifier extends AutoDisposeAsyncNotifier> + with AutoDisposeAsyncPaginationController { static const int _pageSize = 20; String _currentQuery = ''; String? _pubName; @@ -28,12 +28,13 @@ class PostSearchNotifier List? _tags; bool _shuffle = false; bool? _pinned; - bool _isLoading = false; - PostSearchNotifier(this.ref) : super(const AsyncValue.loading()) { - state = const AsyncValue.data( - CursorPagingData(items: [], hasMore: false, nextCursor: null), - ); + @override + FutureOr> build() async { + // Initial state is empty if no query/filters, or fetch if needed + // But original logic allowed initial empty state. + // Let's replicate original logic: return empty list initially if no query. + return []; } Future search( @@ -46,8 +47,6 @@ class PostSearchNotifier bool shuffle = false, bool? pinned, }) async { - if (_isLoading) return; - _currentQuery = query.trim(); _pubName = pubName; _realm = realm; @@ -57,7 +56,6 @@ class PostSearchNotifier _shuffle = shuffle; _pinned = pinned; - // Allow search even with empty query if any filters are applied final hasFilters = pubName != null || realm != null || @@ -68,59 +66,38 @@ class PostSearchNotifier pinned != null; if (_currentQuery.isEmpty && !hasFilters) { - state = AsyncValue.data( - CursorPagingData(items: [], hasMore: false, nextCursor: null), - ); + state = const AsyncData([]); + totalCount = null; return; } - await fetch(cursor: null); + await refresh(); } - Future fetch({String? cursor}) async { - if (_isLoading) return; + @override + Future> fetch() async { + final client = ref.read(apiClientProvider); - _isLoading = true; - state = const AsyncValue.loading(); + final response = await client.get( + '/sphere/posts', + queryParameters: { + 'query': _currentQuery, + 'offset': fetchedCount, + 'take': _pageSize, + 'vector': false, + if (_pubName != null) 'pub': _pubName, + if (_realm != null) 'realm': _realm, + if (_type != null) 'type': _type, + if (_tags != null) 'tags': _tags, + if (_categories != null) 'categories': _categories, + if (_shuffle) 'shuffle': true, + if (_pinned != null) 'pinned': _pinned, + }, + ); - try { - final client = ref.read(apiClientProvider); - final offset = cursor == null ? 0 : int.parse(cursor); - - final response = await client.get( - '/sphere/posts', - queryParameters: { - 'query': _currentQuery, - 'offset': offset, - 'take': _pageSize, - 'vector': false, - if (_pubName != null) 'pub': _pubName, - if (_realm != null) 'realm': _realm, - if (_type != null) 'type': _type, - if (_tags != null) 'tags': _tags, - if (_categories != null) 'categories': _categories, - if (_shuffle) 'shuffle': true, - if (_pinned != null) 'pinned': _pinned, - }, - ); - - final data = response.data as List; - final posts = data.map((json) => SnPost.fromJson(json)).toList(); - final hasMore = posts.length == _pageSize; - final nextCursor = hasMore ? (offset + posts.length).toString() : null; - - state = AsyncValue.data( - CursorPagingData( - items: posts, - hasMore: hasMore, - nextCursor: nextCursor, - ), - ); - } catch (e, stack) { - state = AsyncValue.error(e, stack); - } finally { - _isLoading = false; - } + totalCount = int.parse(response.headers.value('X-Total') ?? '0'); + final data = response.data as List; + return data.map((json) => SnPost.fromJson(json)).toList(); } } @@ -339,55 +316,34 @@ class PostSearchScreen extends HookConsumerWidget { ), ), ), - searchState.when( - data: (data) { - if (data.items.isEmpty && searchController.text.isNotEmpty) { - return SliverFillRemaining( - child: Center(child: Text('noResultsFound'.tr())), - ); - } - - return SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - if (index >= data.items.length) { - ref - .read(postSearchNotifierProvider.notifier) - .fetch(cursor: data.nextCursor); - return Center(child: CircularProgressIndicator()); - } - - final post = data.items[index]; - return Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 600), - child: Card( - margin: EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - child: PostActionableItem( - item: post, - borderRadius: 8, - ), - ), + // Use PaginationList with isSliver=true + PaginationList( + provider: postSearchNotifierProvider, + notifier: postSearchNotifierProvider.notifier, + isSliver: true, + isRefreshable: + false, // CustomScrollView handles refreshing usually, but here we don't have PullToRefresh + itemBuilder: (context, index, post) { + return Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 600), + child: Card( + margin: EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, ), - ); - }, childCount: data.items.length + (data.hasMore ? 1 : 0)), - ); - }, - loading: - () => SliverFillRemaining( - child: Center(child: CircularProgressIndicator()), - ), - error: - (error, stack) => SliverFillRemaining( - child: ResponseErrorWidget( - error: error, - onRetry: - () => ref.invalidate(postSearchNotifierProvider), + child: PostActionableItem(item: post, borderRadius: 8), ), ), + ); + }, ), + if (searchState.valueOrNull?.isEmpty == true && + searchController.text.isNotEmpty && + !searchState.isLoading) + SliverFillRemaining( + child: Center(child: Text('noResultsFound'.tr())), + ), ], ); }, diff --git a/lib/screens/realm/realm_detail.dart b/lib/screens/realm/realm_detail.dart index e3edb2f1..354077c5 100644 --- a/lib/screens/realm/realm_detail.dart +++ b/lib/screens/realm/realm_detail.dart @@ -22,47 +22,51 @@ import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.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/paging.dart'; +import 'package:island/widgets/paging/pagination_list.dart'; import 'package:styled_widget/styled_widget.dart'; -part 'realm_detail.g.dart'; +final realmAppbarForegroundColorProvider = FutureProvider.autoDispose + .family((ref, realmSlug) async { + final realm = await ref.watch(realmProvider(realmSlug).future); + if (realm?.background == null) return null; + final colors = await ColorExtractionService.getColorsFromImage( + CloudImageWidget.provider( + fileId: realm!.background!.id, + serverUrl: ref.watch(serverUrlProvider), + ), + ); + if (colors.isEmpty) return null; + final dominantColor = colors.first; + return dominantColor.computeLuminance() > 0.5 + ? Colors.black + : Colors.white; + }); -@riverpod -Future realmAppbarForegroundColor(Ref ref, String realmSlug) async { - final realm = await ref.watch(realmProvider(realmSlug).future); - if (realm?.background == null) return null; - final colors = await ColorExtractionService.getColorsFromImage( - CloudImageWidget.provider( - fileId: realm!.background!.id, - serverUrl: ref.watch(serverUrlProvider), - ), - ); - if (colors.isEmpty) return null; - final dominantColor = colors.first; - return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; -} +final realmIdentityProvider = FutureProvider.autoDispose + .family((ref, realmSlug) async { + try { + final apiClient = ref.watch(apiClientProvider); + final response = await apiClient.get( + '/pass/realms/$realmSlug/members/me', + ); + return SnRealmMember.fromJson(response.data); + } catch (err) { + if (err is DioException && err.response?.statusCode == 404) { + return null; // No identity found, user is not a member + } + rethrow; + } + }); -@riverpod -Future realmIdentity(Ref ref, String realmSlug) async { - try { - final apiClient = ref.watch(apiClientProvider); - final response = await apiClient.get('/pass/realms/$realmSlug/members/me'); - return SnRealmMember.fromJson(response.data); - } catch (err) { - if (err is DioException && err.response?.statusCode == 404) { - return null; // No identity found, user is not a member - } - rethrow; - } -} - -@riverpod -Future> realmChatRooms(Ref ref, String realmSlug) async { - final apiClient = ref.watch(apiClientProvider); - final response = await apiClient.get('/sphere/realms/$realmSlug/chat'); - return (response.data as List).map((e) => SnChatRoom.fromJson(e)).toList(); -} +final realmChatRoomsProvider = FutureProvider.autoDispose + .family, String>((ref, realmSlug) async { + final apiClient = ref.watch(apiClientProvider); + final response = await apiClient.get('/sphere/realms/$realmSlug/chat'); + return (response.data as List) + .map((e) => SnChatRoom.fromJson(e)) + .toList(); + }); class RealmDetailScreen extends HookConsumerWidget { final String slug; @@ -520,49 +524,32 @@ class _RealmActionMenu extends HookConsumerWidget { } } -@riverpod -class RealmMemberListNotifier extends _$RealmMemberListNotifier - with CursorPagingNotifierMixin { +final realmMemberListNotifierProvider = AsyncNotifierProvider.autoDispose + .family, String>( + RealmMemberListNotifier.new, + ); + +class RealmMemberListNotifier + extends AutoDisposeFamilyAsyncNotifier, String> + with FamilyAsyncPaginationController { static const int _pageSize = 20; - ValueNotifier totalCount = ValueNotifier(0); @override - Future> build(String realmSlug) async { - totalCount.value = 0; - return fetch(); - } - - @override - Future> fetch({String? cursor}) async { + Future> fetch() async { final apiClient = ref.read(apiClientProvider); - final offset = cursor != null ? int.parse(cursor) : 0; final response = await apiClient.get( - '/pass/realms/$realmSlug/members', + '/pass/realms/$arg/members', queryParameters: { - 'offset': offset, + 'offset': fetchedCount, 'take': _pageSize, 'withStatus': true, }, ); - final total = int.parse(response.headers.value('X-Total') ?? '0'); - totalCount.value = total; + totalCount = int.parse(response.headers.value('X-Total') ?? '0'); final List data = response.data; - final members = data.map((e) => SnRealmMember.fromJson(e)).toList(); - - final hasMore = offset + members.length < total; - final nextCursor = hasMore ? (offset + members.length).toString() : null; - - return CursorPagingData( - items: members, - hasMore: hasMore, - nextCursor: nextCursor, - ); - } - - void dispose() { - totalCount.dispose(); + return data.map((e) => SnRealmMember.fromJson(e)).toList(); } } @@ -574,11 +561,10 @@ class _RealmMemberListSheet extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final realmIdentity = ref.watch(realmIdentityProvider(realmSlug)); final memberListProvider = realmMemberListNotifierProvider(realmSlug); - final memberListNotifier = ref.watch(memberListProvider.notifier); - - useEffect(() { - return memberListNotifier.dispose; - }, []); + // memberListNotifier is not watched here to prevent unnecessary rebuilds of this widget + // when we only need it for passing to PaginationList as a Refreshable + // However, we used useEffect to dispose it, but AutoDispose handles it. + // So we remove the useEffect and the watch. Future invitePerson() async { final result = await showModalBottomSheet( @@ -606,17 +592,19 @@ class _RealmMemberListSheet extends HookConsumerWidget { padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12), child: Row( children: [ - ListenableBuilder( - listenable: memberListNotifier.totalCount, - builder: - (context, _) => Text( - 'members'.plural(memberListNotifier.totalCount.value), - key: ValueKey(memberListNotifier), - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w600, - letterSpacing: -0.5, - ), + Consumer( + builder: (context, ref, _) { + // effective watch to rebuild when data changes (and totalCount updates) + ref.watch(memberListProvider); + final notifier = ref.read(memberListProvider.notifier); + return Text( + 'members'.plural(notifier.totalCount ?? 0), + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + letterSpacing: -0.5, ), + ); + }, ), const Spacer(), IconButton( @@ -643,105 +631,94 @@ class _RealmMemberListSheet extends HookConsumerWidget { Widget buildMemberListContent() { return Expanded( - child: PagingHelperView( + child: PaginationList( provider: memberListProvider, - futureRefreshable: memberListProvider.future, - notifierRefreshable: memberListProvider.notifier, - contentBuilder: (data, widgetCount, endItemView) { - return ListView.builder( - itemCount: widgetCount, - itemBuilder: (context, index) { - if (index == data.items.length) { - return endItemView; - } - - final member = data.items[index]; - return ListTile( - contentPadding: EdgeInsets.only(left: 16, right: 12), - leading: AccountPfcGestureDetector( - uname: member.account!.name, - child: ProfilePictureWidget( - fileId: member.account!.profile.picture?.id, + notifier: memberListProvider.notifier, + itemBuilder: (context, index, member) { + return ListTile( + contentPadding: EdgeInsets.only(left: 16, right: 12), + leading: AccountPfcGestureDetector( + uname: member.account!.name, + child: ProfilePictureWidget( + fileId: member.account!.profile.picture?.id, + ), + ), + title: Row( + spacing: 6, + children: [ + Flexible( + child: Text( + member.account!.nick, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), - title: Row( - spacing: 6, - children: [ - Flexible( - child: Text( - member.account!.nick, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - if (member.status != null) - AccountStatusLabel(status: member.status!), - if (member.joinedAt == null) - const Icon(Symbols.pending_actions, size: 20), - ], - ), - subtitle: Row( - children: [ - Text( - member.role >= 100 - ? 'permissionOwner' - : member.role >= 50 - ? 'permissionModerator' - : 'permissionMember', - ).tr(), - Text('·').bold().padding(horizontal: 6), - Expanded(child: Text("@${member.account!.name}")), - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if ((realmIdentity.value?.role ?? 0) >= 50) - IconButton( - icon: const Icon(Symbols.edit), - onPressed: () { - showModalBottomSheet( - isScrollControlled: true, - context: context, - builder: - (context) => _RealmMemberRoleSheet( - realmSlug: realmSlug, - member: member, - ), - ).then((value) { - if (value != null) { - // Refresh the provider - ref.invalidate(memberListProvider); - } - }); - }, - ), - if ((realmIdentity.value?.role ?? 0) >= 50) - IconButton( - icon: const Icon(Symbols.delete), - onPressed: () { - showConfirmAlert( - 'removeRealmMemberHint'.tr(), - 'removeRealmMember'.tr(), - ).then((confirm) async { - if (confirm != true) return; - try { - final apiClient = ref.watch(apiClientProvider); - await apiClient.delete( - '/pass/realms/$realmSlug/members/${member.accountId}', - ); - // Refresh the provider - ref.invalidate(memberListProvider); - } catch (err) { - showErrorAlert(err); - } - }); - }, - ), - ], - ), - ); - }, + if (member.status != null) + AccountStatusLabel(status: member.status!), + if (member.joinedAt == null) + const Icon(Symbols.pending_actions, size: 20), + ], + ), + subtitle: Row( + children: [ + Text( + member.role >= 100 + ? 'permissionOwner' + : member.role >= 50 + ? 'permissionModerator' + : 'permissionMember', + ).tr(), + Text('·').bold().padding(horizontal: 6), + Expanded(child: Text("@${member.account!.name}")), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if ((realmIdentity.value?.role ?? 0) >= 50) + IconButton( + icon: const Icon(Symbols.edit), + onPressed: () { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: + (context) => _RealmMemberRoleSheet( + realmSlug: realmSlug, + member: member, + ), + ).then((value) { + if (value != null) { + // Refresh the provider + ref.invalidate(memberListProvider); + } + }); + }, + ), + if ((realmIdentity.value?.role ?? 0) >= 50) + IconButton( + icon: const Icon(Symbols.delete), + onPressed: () { + showConfirmAlert( + 'removeRealmMemberHint'.tr(), + 'removeRealmMember'.tr(), + ).then((confirm) async { + if (confirm != true) return; + try { + final apiClient = ref.watch(apiClientProvider); + await apiClient.delete( + '/pass/realms/$realmSlug/members/${member.accountId}', + ); + // Refresh the provider + ref.invalidate(memberListProvider); + } catch (err) { + showErrorAlert(err); + } + }); + }, + ), + ], + ), ); }, ), diff --git a/lib/screens/realm/realm_detail.g.dart b/lib/screens/realm/realm_detail.g.dart deleted file mode 100644 index 0167453b..00000000 --- a/lib/screens/realm/realm_detail.g.dart +++ /dev/null @@ -1,552 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'realm_detail.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$realmAppbarForegroundColorHash() => - r'8131c047a984318a4cc3fbb5daa5ef0ce44dfae5'; - -/// 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)); - } -} - -/// See also [realmAppbarForegroundColor]. -@ProviderFor(realmAppbarForegroundColor) -const realmAppbarForegroundColorProvider = RealmAppbarForegroundColorFamily(); - -/// See also [realmAppbarForegroundColor]. -class RealmAppbarForegroundColorFamily extends Family> { - /// See also [realmAppbarForegroundColor]. - const RealmAppbarForegroundColorFamily(); - - /// See also [realmAppbarForegroundColor]. - RealmAppbarForegroundColorProvider call(String realmSlug) { - return RealmAppbarForegroundColorProvider(realmSlug); - } - - @override - RealmAppbarForegroundColorProvider getProviderOverride( - covariant RealmAppbarForegroundColorProvider provider, - ) { - return call(provider.realmSlug); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'realmAppbarForegroundColorProvider'; -} - -/// See also [realmAppbarForegroundColor]. -class RealmAppbarForegroundColorProvider - extends AutoDisposeFutureProvider { - /// See also [realmAppbarForegroundColor]. - RealmAppbarForegroundColorProvider(String realmSlug) - : this._internal( - (ref) => realmAppbarForegroundColor( - ref as RealmAppbarForegroundColorRef, - realmSlug, - ), - from: realmAppbarForegroundColorProvider, - name: r'realmAppbarForegroundColorProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$realmAppbarForegroundColorHash, - dependencies: RealmAppbarForegroundColorFamily._dependencies, - allTransitiveDependencies: - RealmAppbarForegroundColorFamily._allTransitiveDependencies, - realmSlug: realmSlug, - ); - - RealmAppbarForegroundColorProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.realmSlug, - }) : super.internal(); - - final String realmSlug; - - @override - Override overrideWith( - FutureOr Function(RealmAppbarForegroundColorRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: RealmAppbarForegroundColorProvider._internal( - (ref) => create(ref as RealmAppbarForegroundColorRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - realmSlug: realmSlug, - ), - ); - } - - @override - AutoDisposeFutureProviderElement createElement() { - return _RealmAppbarForegroundColorProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is RealmAppbarForegroundColorProvider && - other.realmSlug == realmSlug; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, realmSlug.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin RealmAppbarForegroundColorRef on AutoDisposeFutureProviderRef { - /// The parameter `realmSlug` of this provider. - String get realmSlug; -} - -class _RealmAppbarForegroundColorProviderElement - extends AutoDisposeFutureProviderElement - with RealmAppbarForegroundColorRef { - _RealmAppbarForegroundColorProviderElement(super.provider); - - @override - String get realmSlug => - (origin as RealmAppbarForegroundColorProvider).realmSlug; -} - -String _$realmIdentityHash() => r'd5a3ecc6eeec291cebbfc9a45d8aac7195366381'; - -/// See also [realmIdentity]. -@ProviderFor(realmIdentity) -const realmIdentityProvider = RealmIdentityFamily(); - -/// See also [realmIdentity]. -class RealmIdentityFamily extends Family> { - /// See also [realmIdentity]. - const RealmIdentityFamily(); - - /// See also [realmIdentity]. - RealmIdentityProvider call(String realmSlug) { - return RealmIdentityProvider(realmSlug); - } - - @override - RealmIdentityProvider getProviderOverride( - covariant RealmIdentityProvider provider, - ) { - return call(provider.realmSlug); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'realmIdentityProvider'; -} - -/// See also [realmIdentity]. -class RealmIdentityProvider extends AutoDisposeFutureProvider { - /// See also [realmIdentity]. - RealmIdentityProvider(String realmSlug) - : this._internal( - (ref) => realmIdentity(ref as RealmIdentityRef, realmSlug), - from: realmIdentityProvider, - name: r'realmIdentityProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$realmIdentityHash, - dependencies: RealmIdentityFamily._dependencies, - allTransitiveDependencies: - RealmIdentityFamily._allTransitiveDependencies, - realmSlug: realmSlug, - ); - - RealmIdentityProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.realmSlug, - }) : super.internal(); - - final String realmSlug; - - @override - Override overrideWith( - FutureOr Function(RealmIdentityRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: RealmIdentityProvider._internal( - (ref) => create(ref as RealmIdentityRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - realmSlug: realmSlug, - ), - ); - } - - @override - AutoDisposeFutureProviderElement createElement() { - return _RealmIdentityProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is RealmIdentityProvider && other.realmSlug == realmSlug; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, realmSlug.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin RealmIdentityRef on AutoDisposeFutureProviderRef { - /// The parameter `realmSlug` of this provider. - String get realmSlug; -} - -class _RealmIdentityProviderElement - extends AutoDisposeFutureProviderElement - with RealmIdentityRef { - _RealmIdentityProviderElement(super.provider); - - @override - String get realmSlug => (origin as RealmIdentityProvider).realmSlug; -} - -String _$realmChatRoomsHash() => r'5f199906fb287b109e2a2d2a81dcb6675bdcb816'; - -/// See also [realmChatRooms]. -@ProviderFor(realmChatRooms) -const realmChatRoomsProvider = RealmChatRoomsFamily(); - -/// See also [realmChatRooms]. -class RealmChatRoomsFamily extends Family>> { - /// See also [realmChatRooms]. - const RealmChatRoomsFamily(); - - /// See also [realmChatRooms]. - RealmChatRoomsProvider call(String realmSlug) { - return RealmChatRoomsProvider(realmSlug); - } - - @override - RealmChatRoomsProvider getProviderOverride( - covariant RealmChatRoomsProvider provider, - ) { - return call(provider.realmSlug); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'realmChatRoomsProvider'; -} - -/// See also [realmChatRooms]. -class RealmChatRoomsProvider - extends AutoDisposeFutureProvider> { - /// See also [realmChatRooms]. - RealmChatRoomsProvider(String realmSlug) - : this._internal( - (ref) => realmChatRooms(ref as RealmChatRoomsRef, realmSlug), - from: realmChatRoomsProvider, - name: r'realmChatRoomsProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$realmChatRoomsHash, - dependencies: RealmChatRoomsFamily._dependencies, - allTransitiveDependencies: - RealmChatRoomsFamily._allTransitiveDependencies, - realmSlug: realmSlug, - ); - - RealmChatRoomsProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.realmSlug, - }) : super.internal(); - - final String realmSlug; - - @override - Override overrideWith( - FutureOr> Function(RealmChatRoomsRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: RealmChatRoomsProvider._internal( - (ref) => create(ref as RealmChatRoomsRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - realmSlug: realmSlug, - ), - ); - } - - @override - AutoDisposeFutureProviderElement> createElement() { - return _RealmChatRoomsProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is RealmChatRoomsProvider && other.realmSlug == realmSlug; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, realmSlug.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin RealmChatRoomsRef on AutoDisposeFutureProviderRef> { - /// The parameter `realmSlug` of this provider. - String get realmSlug; -} - -class _RealmChatRoomsProviderElement - extends AutoDisposeFutureProviderElement> - with RealmChatRoomsRef { - _RealmChatRoomsProviderElement(super.provider); - - @override - String get realmSlug => (origin as RealmChatRoomsProvider).realmSlug; -} - -String _$realmMemberListNotifierHash() => - r'ab38c550c43cbf93d4c3e92e6658d76f40252c1f'; - -abstract class _$RealmMemberListNotifier - extends BuildlessAutoDisposeAsyncNotifier> { - late final String realmSlug; - - FutureOr> build(String realmSlug); -} - -/// See also [RealmMemberListNotifier]. -@ProviderFor(RealmMemberListNotifier) -const realmMemberListNotifierProvider = RealmMemberListNotifierFamily(); - -/// See also [RealmMemberListNotifier]. -class RealmMemberListNotifierFamily - extends Family>> { - /// See also [RealmMemberListNotifier]. - const RealmMemberListNotifierFamily(); - - /// See also [RealmMemberListNotifier]. - RealmMemberListNotifierProvider call(String realmSlug) { - return RealmMemberListNotifierProvider(realmSlug); - } - - @override - RealmMemberListNotifierProvider getProviderOverride( - covariant RealmMemberListNotifierProvider provider, - ) { - return call(provider.realmSlug); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'realmMemberListNotifierProvider'; -} - -/// See also [RealmMemberListNotifier]. -class RealmMemberListNotifierProvider - extends - AutoDisposeAsyncNotifierProviderImpl< - RealmMemberListNotifier, - CursorPagingData - > { - /// See also [RealmMemberListNotifier]. - RealmMemberListNotifierProvider(String realmSlug) - : this._internal( - () => RealmMemberListNotifier()..realmSlug = realmSlug, - from: realmMemberListNotifierProvider, - name: r'realmMemberListNotifierProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$realmMemberListNotifierHash, - dependencies: RealmMemberListNotifierFamily._dependencies, - allTransitiveDependencies: - RealmMemberListNotifierFamily._allTransitiveDependencies, - realmSlug: realmSlug, - ); - - RealmMemberListNotifierProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.realmSlug, - }) : super.internal(); - - final String realmSlug; - - @override - FutureOr> runNotifierBuild( - covariant RealmMemberListNotifier notifier, - ) { - return notifier.build(realmSlug); - } - - @override - Override overrideWith(RealmMemberListNotifier Function() create) { - return ProviderOverride( - origin: this, - override: RealmMemberListNotifierProvider._internal( - () => create()..realmSlug = realmSlug, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - realmSlug: realmSlug, - ), - ); - } - - @override - AutoDisposeAsyncNotifierProviderElement< - RealmMemberListNotifier, - CursorPagingData - > - createElement() { - return _RealmMemberListNotifierProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is RealmMemberListNotifierProvider && - other.realmSlug == realmSlug; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, realmSlug.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin RealmMemberListNotifierRef - on AutoDisposeAsyncNotifierProviderRef> { - /// The parameter `realmSlug` of this provider. - String get realmSlug; -} - -class _RealmMemberListNotifierProviderElement - extends - AutoDisposeAsyncNotifierProviderElement< - RealmMemberListNotifier, - CursorPagingData - > - with RealmMemberListNotifierRef { - _RealmMemberListNotifierProviderElement(super.provider); - - @override - String get realmSlug => (origin as RealmMemberListNotifierProvider).realmSlug; -} - -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/widgets/chat/chat_link_attachments.dart b/lib/widgets/chat/chat_link_attachments.dart index dfe5500b..74780966 100644 --- a/lib/widgets/chat/chat_link_attachments.dart +++ b/lib/widgets/chat/chat_link_attachments.dart @@ -5,29 +5,28 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/file.dart'; import 'package:island/pods/network.dart'; +import 'package:island/pods/paging.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/sheet.dart'; +import 'package:island/widgets/paging/pagination_list.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:styled_widget/styled_widget.dart'; import 'package:url_launcher/url_launcher_string.dart'; -part 'chat_link_attachments.g.dart'; +final chatCloudFileListNotifierProvider = AsyncNotifierProvider.autoDispose< + ChatCloudFileListNotifier, + List +>(ChatCloudFileListNotifier.new); -@riverpod -class ChatCloudFileListNotifier extends _$ChatCloudFileListNotifier - with CursorPagingNotifierMixin { +class ChatCloudFileListNotifier + extends AutoDisposeAsyncNotifier> + with AutoDisposeAsyncPaginationController { @override - Future> build() => fetch(cursor: null); - - @override - Future> fetch({required String? cursor}) async { + Future> fetch() async { final client = ref.read(apiClientProvider); - final offset = cursor == null ? 0 : int.parse(cursor); final take = 20; - final queryParameters = {'offset': offset, 'take': take}; + final queryParameters = {'offset': fetchedCount, 'take': take}; final response = await client.get( '/drive/files/me', @@ -38,16 +37,9 @@ class ChatCloudFileListNotifier extends _$ChatCloudFileListNotifier (response.data as List) .map((e) => SnCloudFile.fromJson(e as Map)) .toList(); - final total = int.parse(response.headers.value('X-Total') ?? '0'); + totalCount = int.parse(response.headers.value('X-Total') ?? '0'); - final hasMore = offset + items.length < total; - final nextCursor = hasMore ? (offset + items.length).toString() : null; - - return CursorPagingData( - items: items, - hasMore: hasMore, - nextCursor: nextCursor, - ); + return items; } } @@ -77,61 +69,49 @@ class ChatLinkAttachment extends HookConsumerWidget { Expanded( child: TabBarView( children: [ - PagingHelperView( + PaginationList( provider: chatCloudFileListNotifierProvider, - futureRefreshable: chatCloudFileListNotifierProvider.future, - notifierRefreshable: - chatCloudFileListNotifierProvider.notifier, - contentBuilder: - (data, widgetCount, endItemView) => ListView.builder( - padding: EdgeInsets.only(top: 8), - itemCount: widgetCount, - itemBuilder: (context, index) { - if (index == widgetCount - 1) { - return endItemView; - } - - final item = data.items[index]; - final itemType = - item.mimeType?.split('/').firstOrNull; - return ListTile( - leading: ClipRRect( - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), - child: SizedBox( - height: 48, - width: 48, - child: switch (itemType) { - 'image' => CloudImageWidget(file: item), - 'audio' => - const Icon( - Symbols.audio_file, - fill: 1, - ).center(), - 'video' => - const Icon( - Symbols.video_file, - fill: 1, - ).center(), - _ => - const Icon( - Symbols.body_system, - fill: 1, - ).center(), - }, - ), - ), - title: - item.name.isEmpty - ? Text('untitled').tr().italic() - : Text(item.name), - onTap: () { - Navigator.pop(context, item); - }, - ); - }, + notifier: chatCloudFileListNotifierProvider.notifier, + padding: EdgeInsets.only(top: 8), + itemBuilder: (context, index, item) { + final itemType = item.mimeType?.split('/').firstOrNull; + return ListTile( + leading: ClipRRect( + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + child: SizedBox( + height: 48, + width: 48, + child: switch (itemType) { + 'image' => CloudImageWidget(file: item), + 'audio' => + const Icon( + Symbols.audio_file, + fill: 1, + ).center(), + 'video' => + const Icon( + Symbols.video_file, + fill: 1, + ).center(), + _ => + const Icon( + Symbols.body_system, + fill: 1, + ).center(), + }, + ), ), + title: + item.name.isEmpty + ? Text('untitled').tr().italic() + : Text(item.name), + onTap: () { + Navigator.pop(context, item); + }, + ); + }, ), SingleChildScrollView( child: Column( diff --git a/lib/widgets/chat/chat_link_attachments.g.dart b/lib/widgets/chat/chat_link_attachments.g.dart deleted file mode 100644 index 5b9ad693..00000000 --- a/lib/widgets/chat/chat_link_attachments.g.dart +++ /dev/null @@ -1,31 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'chat_link_attachments.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$chatCloudFileListNotifierHash() => - r'5da3929229fe00212530f63bd19ae4cd829176f5'; - -/// See also [ChatCloudFileListNotifier]. -@ProviderFor(ChatCloudFileListNotifier) -final chatCloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider< - ChatCloudFileListNotifier, - CursorPagingData ->.internal( - ChatCloudFileListNotifier.new, - name: r'chatCloudFileListNotifierProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$chatCloudFileListNotifierHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$ChatCloudFileListNotifier = - AutoDisposeAsyncNotifier>; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/widgets/poll/poll_feedback.dart b/lib/widgets/poll/poll_feedback.dart index 5940c438..d2945d1c 100644 --- a/lib/widgets/poll/poll_feedback.dart +++ b/lib/widgets/poll/poll_feedback.dart @@ -4,55 +4,40 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/poll.dart'; import 'package:island/pods/network.dart'; +import 'package:island/pods/paging.dart'; import 'package:island/screens/creators/poll/poll_list.dart'; import 'package:island/services/time.dart'; import 'package:island/widgets/account/account_pfc.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/sheet.dart'; +import 'package:island/widgets/paging/pagination_list.dart'; import 'package:island/widgets/poll/poll_stats_widget.dart'; import 'package:island/widgets/response.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:styled_widget/styled_widget.dart'; -part 'poll_feedback.g.dart'; +final pollFeedbackNotifierProvider = AsyncNotifierProvider.autoDispose + .family, String>( + PollFeedbackNotifier.new, + ); -@riverpod -class PollFeedbackNotifier extends _$PollFeedbackNotifier - with CursorPagingNotifierMixin { +class PollFeedbackNotifier + extends AutoDisposeFamilyAsyncNotifier, String> + with FamilyAsyncPaginationController { static const int _pageSize = 20; @override - Future> build(String id) { - // immediately load first page - return fetch(cursor: null); - } - - @override - Future> fetch({ - required String? cursor, - }) async { + Future> fetch() async { final client = ref.read(apiClientProvider); - final offset = cursor == null ? 0 : int.parse(cursor); - final queryParams = {'offset': offset, 'take': _pageSize}; + final queryParams = {'offset': fetchedCount, 'take': _pageSize}; final response = await client.get( - '/sphere/polls/$id/feedback', + '/sphere/polls/$arg/feedback', queryParameters: queryParams, ); - final total = int.parse(response.headers.value('X-Total') ?? '0'); + totalCount = int.parse(response.headers.value('X-Total') ?? '0'); final List data = response.data; - final items = data.map((json) => SnPollAnswer.fromJson(json)).toList(); - - final hasMore = offset + items.length < total; - final nextCursor = hasMore ? (offset + items.length).toString() : null; - - return CursorPagingData( - items: items, - hasMore: hasMore, - nextCursor: nextCursor, - ); + return data.map((json) => SnPollAnswer.fromJson(json)).toList(); } } @@ -64,6 +49,7 @@ class PollFeedbackSheet extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final poll = ref.watch(pollWithStatsProvider(pollId)); + final provider = pollFeedbackNotifierProvider(pollId); return SheetScaffold( titleText: title ?? 'Poll feedback', @@ -74,27 +60,20 @@ class PollFeedbackSheet extends HookConsumerWidget { SliverToBoxAdapter(child: _PollHeader(poll: data)), SliverToBoxAdapter(child: const Divider(height: 1)), SliverGap(4), - PagingHelperSliverView( - provider: pollFeedbackNotifierProvider(pollId), - futureRefreshable: - pollFeedbackNotifierProvider(pollId).future, - notifierRefreshable: - pollFeedbackNotifierProvider(pollId).notifier, - contentBuilder: - (val, widgetCount, endItemView) => SliverList.separated( - itemCount: widgetCount, - itemBuilder: (context, index) { - if (index == widgetCount - 1) { - // Provided by PagingHelperView to indicate end/loading - return endItemView; - } - final answer = val.items[index]; - return _PollAnswerTile(answer: answer, poll: data); - }, - separatorBuilder: - (context, index) => - const Divider(height: 1).padding(vertical: 4), - ), + PaginationList( + provider: provider, + notifier: provider.notifier, + isSliver: true, + itemBuilder: (context, index, answer) { + return Column( + children: [ + _PollAnswerTile(answer: answer, poll: data), + if (index < + (ref.read(provider).valueOrNull?.length ?? 0) - 1) + const Divider(height: 1).padding(vertical: 4), + ], + ); + }, ), SliverGap(4 + MediaQuery.of(context).padding.bottom), ], diff --git a/lib/widgets/poll/poll_feedback.g.dart b/lib/widgets/poll/poll_feedback.g.dart deleted file mode 100644 index 855ea5a3..00000000 --- a/lib/widgets/poll/poll_feedback.g.dart +++ /dev/null @@ -1,180 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'poll_feedback.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$pollFeedbackNotifierHash() => - r'1bf3925b5b751cfd1a9abafb75274f1e95e7f27e'; - -/// 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 _$PollFeedbackNotifier - extends BuildlessAutoDisposeAsyncNotifier> { - late final String id; - - FutureOr> build(String id); -} - -/// See also [PollFeedbackNotifier]. -@ProviderFor(PollFeedbackNotifier) -const pollFeedbackNotifierProvider = PollFeedbackNotifierFamily(); - -/// See also [PollFeedbackNotifier]. -class PollFeedbackNotifierFamily - extends Family>> { - /// See also [PollFeedbackNotifier]. - const PollFeedbackNotifierFamily(); - - /// See also [PollFeedbackNotifier]. - PollFeedbackNotifierProvider call(String id) { - return PollFeedbackNotifierProvider(id); - } - - @override - PollFeedbackNotifierProvider getProviderOverride( - covariant PollFeedbackNotifierProvider provider, - ) { - return call(provider.id); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'pollFeedbackNotifierProvider'; -} - -/// See also [PollFeedbackNotifier]. -class PollFeedbackNotifierProvider - extends - AutoDisposeAsyncNotifierProviderImpl< - PollFeedbackNotifier, - CursorPagingData - > { - /// See also [PollFeedbackNotifier]. - PollFeedbackNotifierProvider(String id) - : this._internal( - () => PollFeedbackNotifier()..id = id, - from: pollFeedbackNotifierProvider, - name: r'pollFeedbackNotifierProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$pollFeedbackNotifierHash, - dependencies: PollFeedbackNotifierFamily._dependencies, - allTransitiveDependencies: - PollFeedbackNotifierFamily._allTransitiveDependencies, - id: id, - ); - - PollFeedbackNotifierProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.id, - }) : super.internal(); - - final String id; - - @override - FutureOr> runNotifierBuild( - covariant PollFeedbackNotifier notifier, - ) { - return notifier.build(id); - } - - @override - Override overrideWith(PollFeedbackNotifier Function() create) { - return ProviderOverride( - origin: this, - override: PollFeedbackNotifierProvider._internal( - () => create()..id = id, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - id: id, - ), - ); - } - - @override - AutoDisposeAsyncNotifierProviderElement< - PollFeedbackNotifier, - CursorPagingData - > - createElement() { - return _PollFeedbackNotifierProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is PollFeedbackNotifierProvider && other.id == id; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, id.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin PollFeedbackNotifierRef - on AutoDisposeAsyncNotifierProviderRef> { - /// The parameter `id` of this provider. - String get id; -} - -class _PollFeedbackNotifierProviderElement - extends - AutoDisposeAsyncNotifierProviderElement< - PollFeedbackNotifier, - CursorPagingData - > - with PollFeedbackNotifierRef { - _PollFeedbackNotifierProviderElement(super.provider); - - @override - String get id => (origin as PollFeedbackNotifierProvider).id; -} - -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/widgets/post/compose_link_attachments.dart b/lib/widgets/post/compose_link_attachments.dart index 1cd70556..4f60ca30 100644 --- a/lib/widgets/post/compose_link_attachments.dart +++ b/lib/widgets/post/compose_link_attachments.dart @@ -5,49 +5,38 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/file.dart'; import 'package:island/pods/network.dart'; +import 'package:island/pods/paging.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/sheet.dart'; +import 'package:island/widgets/paging/pagination_list.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:styled_widget/styled_widget.dart'; import 'package:url_launcher/url_launcher_string.dart'; -part 'compose_link_attachments.g.dart'; +final cloudFileListNotifierProvider = + AsyncNotifierProvider.autoDispose>( + CloudFileListNotifier.new, + ); -@riverpod -class CloudFileListNotifier extends _$CloudFileListNotifier - with CursorPagingNotifierMixin { +class CloudFileListNotifier extends AutoDisposeAsyncNotifier> + with AutoDisposeAsyncPaginationController { @override - Future> build() => fetch(cursor: null); - - @override - Future> fetch({required String? cursor}) async { + Future> fetch() async { final client = ref.read(apiClientProvider); - final offset = cursor == null ? 0 : int.parse(cursor); final take = 20; - final queryParameters = {'offset': offset, 'take': take}; + final queryParameters = {'offset': fetchedCount, 'take': take}; final response = await client.get( '/drive/files/me', queryParameters: queryParameters, ); - final List items = - (response.data as List) - .map((e) => SnCloudFile.fromJson(e as Map)) - .toList(); - final total = int.parse(response.headers.value('X-Total') ?? '0'); - - final hasMore = offset + items.length < total; - final nextCursor = hasMore ? (offset + items.length).toString() : null; - - return CursorPagingData( - items: items, - hasMore: hasMore, - nextCursor: nextCursor, - ); + totalCount = int.parse(response.headers.value('X-Total') ?? '0'); + final List data = response.data; + return data + .map((e) => SnCloudFile.fromJson(e as Map)) + .toList(); } } @@ -58,6 +47,7 @@ class ComposeLinkAttachment extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final idController = useTextEditingController(); final errorMessage = useState(null); + final provider = cloudFileListNotifierProvider; return SheetScaffold( heightFactor: 0.6, @@ -77,60 +67,49 @@ class ComposeLinkAttachment extends HookConsumerWidget { Expanded( child: TabBarView( children: [ - PagingHelperView( - provider: cloudFileListNotifierProvider, - futureRefreshable: cloudFileListNotifierProvider.future, - notifierRefreshable: cloudFileListNotifierProvider.notifier, - contentBuilder: - (data, widgetCount, endItemView) => ListView.builder( - padding: EdgeInsets.only(top: 8), - itemCount: widgetCount, - itemBuilder: (context, index) { - if (index == widgetCount - 1) { - return endItemView; - } - - final item = data.items[index]; - final itemType = - item.mimeType?.split('/').firstOrNull; - return ListTile( - leading: ClipRRect( - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), - child: SizedBox( - height: 48, - width: 48, - child: switch (itemType) { - 'image' => CloudImageWidget(file: item), - 'audio' => - const Icon( - Symbols.audio_file, - fill: 1, - ).center(), - 'video' => - const Icon( - Symbols.video_file, - fill: 1, - ).center(), - _ => - const Icon( - Symbols.body_system, - fill: 1, - ).center(), - }, - ), - ), - title: - item.name.isEmpty - ? Text('untitled').tr().italic() - : Text(item.name), - onTap: () { - Navigator.pop(context, item); - }, - ); - }, + PaginationList( + padding: EdgeInsets.only(top: 8), + provider: provider, + notifier: provider.notifier, + itemBuilder: (context, index, item) { + final itemType = item.mimeType?.split('/').firstOrNull; + return ListTile( + leading: ClipRRect( + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + child: SizedBox( + height: 48, + width: 48, + child: switch (itemType) { + 'image' => CloudImageWidget(file: item), + 'audio' => + const Icon( + Symbols.audio_file, + fill: 1, + ).center(), + 'video' => + const Icon( + Symbols.video_file, + fill: 1, + ).center(), + _ => + const Icon( + Symbols.body_system, + fill: 1, + ).center(), + }, + ), ), + title: + item.name.isEmpty + ? Text('untitled').tr().italic() + : Text(item.name), + onTap: () { + Navigator.pop(context, item); + }, + ); + }, ), SingleChildScrollView( child: Column( diff --git a/lib/widgets/post/compose_link_attachments.g.dart b/lib/widgets/post/compose_link_attachments.g.dart deleted file mode 100644 index b58e6fc4..00000000 --- a/lib/widgets/post/compose_link_attachments.g.dart +++ /dev/null @@ -1,31 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'compose_link_attachments.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$cloudFileListNotifierHash() => - r'e2c8a076a9e635c7b43a87d00f78775427ba6334'; - -/// See also [CloudFileListNotifier]. -@ProviderFor(CloudFileListNotifier) -final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider< - CloudFileListNotifier, - CursorPagingData ->.internal( - CloudFileListNotifier.new, - name: r'cloudFileListNotifierProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$cloudFileListNotifierHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$CloudFileListNotifier = - AutoDisposeAsyncNotifier>; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/widgets/post/post_award_history_sheet.dart b/lib/widgets/post/post_award_history_sheet.dart index b6cceda2..f7b0bd26 100644 --- a/lib/widgets/post/post_award_history_sheet.dart +++ b/lib/widgets/post/post_award_history_sheet.dart @@ -2,45 +2,33 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/post.dart'; import 'package:island/pods/network.dart'; +import 'package:island/pods/paging.dart'; import 'package:island/widgets/content/sheet.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; +import 'package:island/widgets/paging/pagination_list.dart'; -part 'post_award_history_sheet.g.dart'; +final postAwardListNotifierProvider = AsyncNotifierProvider.autoDispose + .family, String>( + PostAwardListNotifier.new, + ); -@riverpod -class PostAwardListNotifier extends _$PostAwardListNotifier - with CursorPagingNotifierMixin { +class PostAwardListNotifier + extends AutoDisposeFamilyAsyncNotifier, String> + with FamilyAsyncPaginationController { static const int _pageSize = 20; @override - Future> build({required String postId}) { - return fetch(cursor: null); - } - - @override - Future> fetch({required String? cursor}) async { + Future> fetch() async { final client = ref.read(apiClientProvider); - final offset = cursor == null ? 0 : int.parse(cursor); - final queryParams = {'offset': offset, 'take': _pageSize}; + final queryParams = {'offset': fetchedCount, 'take': _pageSize}; final response = await client.get( - '/sphere/posts/$postId/awards', + '/sphere/posts/$arg/awards', queryParameters: queryParams, ); - final total = int.parse(response.headers.value('X-Total') ?? '0'); + totalCount = int.parse(response.headers.value('X-Total') ?? '0'); final List data = response.data; - final awards = data.map((json) => SnPostAward.fromJson(json)).toList(); - - final hasMore = offset + awards.length < total; - final nextCursor = hasMore ? (offset + awards.length).toString() : null; - - return CursorPagingData( - items: awards, - hasMore: hasMore, - nextCursor: nextCursor, - ); + return data.map((json) => SnPostAward.fromJson(json)).toList(); } } @@ -51,31 +39,22 @@ class PostAwardHistorySheet extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final provider = postAwardListNotifierProvider(postId: postId); + final provider = postAwardListNotifierProvider(postId); return SheetScaffold( titleText: 'Award History', - child: PagingHelperView( + child: PaginationList( provider: provider, - futureRefreshable: provider.future, - notifierRefreshable: provider.notifier, - contentBuilder: - (data, widgetCount, endItemView) => ListView.builder( - itemCount: widgetCount, - itemBuilder: (context, index) { - if (index == widgetCount - 1) { - return endItemView; - } - - final award = data.items[index]; - return Column( - children: [ - PostAwardItem(award: award), - const Divider(height: 1), - ], - ); - }, - ), + notifier: provider.notifier, + itemBuilder: (context, index, award) { + return Column( + children: [ + PostAwardItem(award: award), + if (index < (ref.read(provider).valueOrNull?.length ?? 0) - 1) + const Divider(height: 1), + ], + ); + }, ), ); } diff --git a/lib/widgets/post/post_award_history_sheet.g.dart b/lib/widgets/post/post_award_history_sheet.g.dart deleted file mode 100644 index 36f371f4..00000000 --- a/lib/widgets/post/post_award_history_sheet.g.dart +++ /dev/null @@ -1,180 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'post_award_history_sheet.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$postAwardListNotifierHash() => - r'834d08f90ef352a2dfb0192455c75b1620e859c2'; - -/// 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 _$PostAwardListNotifier - extends BuildlessAutoDisposeAsyncNotifier> { - late final String postId; - - FutureOr> build({required String postId}); -} - -/// See also [PostAwardListNotifier]. -@ProviderFor(PostAwardListNotifier) -const postAwardListNotifierProvider = PostAwardListNotifierFamily(); - -/// See also [PostAwardListNotifier]. -class PostAwardListNotifierFamily - extends Family>> { - /// See also [PostAwardListNotifier]. - const PostAwardListNotifierFamily(); - - /// See also [PostAwardListNotifier]. - PostAwardListNotifierProvider call({required String postId}) { - return PostAwardListNotifierProvider(postId: postId); - } - - @override - PostAwardListNotifierProvider getProviderOverride( - covariant PostAwardListNotifierProvider provider, - ) { - return call(postId: provider.postId); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'postAwardListNotifierProvider'; -} - -/// See also [PostAwardListNotifier]. -class PostAwardListNotifierProvider - extends - AutoDisposeAsyncNotifierProviderImpl< - PostAwardListNotifier, - CursorPagingData - > { - /// See also [PostAwardListNotifier]. - PostAwardListNotifierProvider({required String postId}) - : this._internal( - () => PostAwardListNotifier()..postId = postId, - from: postAwardListNotifierProvider, - name: r'postAwardListNotifierProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$postAwardListNotifierHash, - dependencies: PostAwardListNotifierFamily._dependencies, - allTransitiveDependencies: - PostAwardListNotifierFamily._allTransitiveDependencies, - postId: postId, - ); - - PostAwardListNotifierProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.postId, - }) : super.internal(); - - final String postId; - - @override - FutureOr> runNotifierBuild( - covariant PostAwardListNotifier notifier, - ) { - return notifier.build(postId: postId); - } - - @override - Override overrideWith(PostAwardListNotifier Function() create) { - return ProviderOverride( - origin: this, - override: PostAwardListNotifierProvider._internal( - () => create()..postId = postId, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - postId: postId, - ), - ); - } - - @override - AutoDisposeAsyncNotifierProviderElement< - PostAwardListNotifier, - CursorPagingData - > - createElement() { - return _PostAwardListNotifierProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is PostAwardListNotifierProvider && other.postId == postId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, postId.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin PostAwardListNotifierRef - on AutoDisposeAsyncNotifierProviderRef> { - /// The parameter `postId` of this provider. - String get postId; -} - -class _PostAwardListNotifierProviderElement - extends - AutoDisposeAsyncNotifierProviderElement< - PostAwardListNotifier, - CursorPagingData - > - with PostAwardListNotifierRef { - _PostAwardListNotifierProviderElement(super.provider); - - @override - String get postId => (origin as PostAwardListNotifierProvider).postId; -} - -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/widgets/post/post_list.dart b/lib/widgets/post/post_list.dart index 6f24af87..a6080b15 100644 --- a/lib/widgets/post/post_list.dart +++ b/lib/widgets/post/post_list.dart @@ -1,79 +1,75 @@ import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/post.dart'; import 'package:island/pods/network.dart'; +import 'package:island/pods/paging.dart'; +import 'package:island/widgets/paging/pagination_list.dart'; import 'package:island/widgets/post/post_item.dart'; import 'package:island/widgets/post/post_item_creator.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; -part 'post_list.g.dart'; +part 'post_list.freezed.dart'; -@riverpod -class PostListNotifier extends _$PostListNotifier - with CursorPagingNotifierMixin { - static const int _pageSize = 20; - - @override - Future> build({ +@freezed +sealed class PostListQuery with _$PostListQuery { + const factory PostListQuery({ String? pubName, String? realm, int? type, List? categories, List? tags, bool? pinned, - bool shuffle = false, + @Default(false) bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, - bool orderDesc = true, - }) { - return fetch(cursor: null); - } + @Default(true) bool orderDesc, + }) = _PostListQuery; +} + +final postListNotifierProvider = AsyncNotifierProvider.autoDispose + .family, PostListQuery>( + PostListNotifier.new, + ); + +class PostListNotifier + extends AutoDisposeFamilyAsyncNotifier, PostListQuery> + with FamilyAsyncPaginationController { + static const int _pageSize = 20; @override - Future> fetch({required String? cursor}) async { + Future> fetch() async { final client = ref.read(apiClientProvider); - final offset = cursor == null ? 0 : int.parse(cursor); final queryParams = { - 'offset': offset, + 'offset': fetchedCount, 'take': _pageSize, - 'replies': includeReplies, - 'orderDesc': orderDesc, - if (shuffle) 'shuffle': shuffle, - if (pubName != null) 'pub': pubName, - if (realm != null) 'realm': realm, - if (type != null) 'type': type, - if (tags != null) 'tags': tags, - if (categories != null) 'categories': categories, - if (pinned != null) 'pinned': pinned, - if (order != null) 'order': order, - if (periodStart != null) 'periodStart': periodStart, - if (periodEnd != null) 'periodEnd': periodEnd, - if (queryTerm != null) 'query': queryTerm, - if (mediaOnly != null) 'media': mediaOnly, + 'replies': arg.includeReplies, + 'orderDesc': arg.orderDesc, + if (arg.shuffle) 'shuffle': arg.shuffle, + if (arg.pubName != null) 'pub': arg.pubName, + if (arg.realm != null) 'realm': arg.realm, + if (arg.type != null) 'type': arg.type, + if (arg.tags != null) 'tags': arg.tags, + if (arg.categories != null) 'categories': arg.categories, + if (arg.pinned != null) 'pinned': arg.pinned, + if (arg.order != null) 'order': arg.order, + if (arg.periodStart != null) 'periodStart': arg.periodStart, + if (arg.periodEnd != null) 'periodEnd': arg.periodEnd, + if (arg.queryTerm != null) 'query': arg.queryTerm, + if (arg.mediaOnly != null) 'media': arg.mediaOnly, }; final response = await client.get( '/sphere/posts', queryParameters: queryParams, ); - final total = int.parse(response.headers.value('X-Total') ?? '0'); + totalCount = int.parse(response.headers.value('X-Total') ?? '0'); final List data = response.data; - final posts = data.map((json) => SnPost.fromJson(json)).toList(); - - final hasMore = offset + posts.length < total; - final nextCursor = hasMore ? (offset + posts.length).toString() : null; - - return CursorPagingData( - items: posts, - hasMore: hasMore, - nextCursor: nextCursor, - ); + return data.map((json) => SnPost.fromJson(json)).toList(); } } @@ -137,7 +133,7 @@ class SliverPostList extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final provider = postListNotifierProvider( + final params = PostListQuery( pubName: pubName, realm: realm, type: type, @@ -153,32 +149,26 @@ class SliverPostList extends HookConsumerWidget { periodEnd: periodEnd, orderDesc: orderDesc ?? true, ); - return PagingHelperSliverView( + final provider = postListNotifierProvider(params); + final notifier = provider.notifier; + + return PaginationList( provider: provider, - futureRefreshable: provider.future, - notifierRefreshable: provider.notifier, - contentBuilder: - (data, widgetCount, endItemView) => SliverList.builder( - itemCount: widgetCount, - itemBuilder: (context, index) { - if (index == widgetCount - 1) { - return endItemView; - } + notifier: notifier, + isRefreshable: false, + isSliver: true, + itemBuilder: (context, index, post) { + if (maxWidth != null) { + return Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxWidth!), + child: _buildPostItem(post), + ), + ); + } - final post = data.items[index]; - - if (maxWidth != null) { - return Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: maxWidth!), - child: _buildPostItem(post), - ), - ); - } - - return _buildPostItem(post); - }, - ), + return _buildPostItem(post); + }, ); } diff --git a/lib/widgets/post/post_list.freezed.dart b/lib/widgets/post/post_list.freezed.dart new file mode 100644 index 00000000..ab37dde2 --- /dev/null +++ b/lib/widgets/post/post_list.freezed.dart @@ -0,0 +1,320 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'post_list.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$PostListQuery { + + String? get pubName; String? get realm; int? get type; List? get categories; List? get tags; bool? get pinned; bool get shuffle; bool? get includeReplies; bool? get mediaOnly; String? get queryTerm; String? get order; int? get periodStart; int? get periodEnd; bool get orderDesc; +/// Create a copy of PostListQuery +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$PostListQueryCopyWith get copyWith => _$PostListQueryCopyWithImpl(this as PostListQuery, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is PostListQuery&&(identical(other.pubName, pubName) || other.pubName == pubName)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.pinned, pinned) || other.pinned == pinned)&&(identical(other.shuffle, shuffle) || other.shuffle == shuffle)&&(identical(other.includeReplies, includeReplies) || other.includeReplies == includeReplies)&&(identical(other.mediaOnly, mediaOnly) || other.mediaOnly == mediaOnly)&&(identical(other.queryTerm, queryTerm) || other.queryTerm == queryTerm)&&(identical(other.order, order) || other.order == order)&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&(identical(other.orderDesc, orderDesc) || other.orderDesc == orderDesc)); +} + + +@override +int get hashCode => Object.hash(runtimeType,pubName,realm,type,const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(tags),pinned,shuffle,includeReplies,mediaOnly,queryTerm,order,periodStart,periodEnd,orderDesc); + +@override +String toString() { + return 'PostListQuery(pubName: $pubName, realm: $realm, type: $type, categories: $categories, tags: $tags, pinned: $pinned, shuffle: $shuffle, includeReplies: $includeReplies, mediaOnly: $mediaOnly, queryTerm: $queryTerm, order: $order, periodStart: $periodStart, periodEnd: $periodEnd, orderDesc: $orderDesc)'; +} + + +} + +/// @nodoc +abstract mixin class $PostListQueryCopyWith<$Res> { + factory $PostListQueryCopyWith(PostListQuery value, $Res Function(PostListQuery) _then) = _$PostListQueryCopyWithImpl; +@useResult +$Res call({ + String? pubName, String? realm, int? type, List? categories, List? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc +}); + + + + +} +/// @nodoc +class _$PostListQueryCopyWithImpl<$Res> + implements $PostListQueryCopyWith<$Res> { + _$PostListQueryCopyWithImpl(this._self, this._then); + + final PostListQuery _self; + final $Res Function(PostListQuery) _then; + +/// Create a copy of PostListQuery +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? pubName = freezed,Object? realm = freezed,Object? type = freezed,Object? categories = freezed,Object? tags = freezed,Object? pinned = freezed,Object? shuffle = null,Object? includeReplies = freezed,Object? mediaOnly = freezed,Object? queryTerm = freezed,Object? order = freezed,Object? periodStart = freezed,Object? periodEnd = freezed,Object? orderDesc = null,}) { + return _then(_self.copyWith( +pubName: freezed == pubName ? _self.pubName : pubName // ignore: cast_nullable_to_non_nullable +as String?,realm: freezed == realm ? _self.realm : realm // ignore: cast_nullable_to_non_nullable +as String?,type: freezed == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as int?,categories: freezed == categories ? _self.categories : categories // ignore: cast_nullable_to_non_nullable +as List?,tags: freezed == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable +as List?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable +as bool?,shuffle: null == shuffle ? _self.shuffle : shuffle // ignore: cast_nullable_to_non_nullable +as bool,includeReplies: freezed == includeReplies ? _self.includeReplies : includeReplies // ignore: cast_nullable_to_non_nullable +as bool?,mediaOnly: freezed == mediaOnly ? _self.mediaOnly : mediaOnly // ignore: cast_nullable_to_non_nullable +as bool?,queryTerm: freezed == queryTerm ? _self.queryTerm : queryTerm // ignore: cast_nullable_to_non_nullable +as String?,order: freezed == order ? _self.order : order // ignore: cast_nullable_to_non_nullable +as String?,periodStart: freezed == periodStart ? _self.periodStart : periodStart // ignore: cast_nullable_to_non_nullable +as int?,periodEnd: freezed == periodEnd ? _self.periodEnd : periodEnd // ignore: cast_nullable_to_non_nullable +as int?,orderDesc: null == orderDesc ? _self.orderDesc : orderDesc // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// Adds pattern-matching-related methods to [PostListQuery]. +extension PostListQueryPatterns on PostListQuery { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _PostListQuery value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _PostListQuery() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _PostListQuery value) $default,){ +final _that = this; +switch (_that) { +case _PostListQuery(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _PostListQuery value)? $default,){ +final _that = this; +switch (_that) { +case _PostListQuery() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String? pubName, String? realm, int? type, List? categories, List? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _PostListQuery() when $default != null: +return $default(_that.pubName,_that.realm,_that.type,_that.categories,_that.tags,_that.pinned,_that.shuffle,_that.includeReplies,_that.mediaOnly,_that.queryTerm,_that.order,_that.periodStart,_that.periodEnd,_that.orderDesc);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String? pubName, String? realm, int? type, List? categories, List? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc) $default,) {final _that = this; +switch (_that) { +case _PostListQuery(): +return $default(_that.pubName,_that.realm,_that.type,_that.categories,_that.tags,_that.pinned,_that.shuffle,_that.includeReplies,_that.mediaOnly,_that.queryTerm,_that.order,_that.periodStart,_that.periodEnd,_that.orderDesc);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? pubName, String? realm, int? type, List? categories, List? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc)? $default,) {final _that = this; +switch (_that) { +case _PostListQuery() when $default != null: +return $default(_that.pubName,_that.realm,_that.type,_that.categories,_that.tags,_that.pinned,_that.shuffle,_that.includeReplies,_that.mediaOnly,_that.queryTerm,_that.order,_that.periodStart,_that.periodEnd,_that.orderDesc);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _PostListQuery implements PostListQuery { + const _PostListQuery({this.pubName, this.realm, this.type, final List? categories, final List? tags, this.pinned, this.shuffle = false, this.includeReplies, this.mediaOnly, this.queryTerm, this.order, this.periodStart, this.periodEnd, this.orderDesc = true}): _categories = categories,_tags = tags; + + +@override final String? pubName; +@override final String? realm; +@override final int? type; + final List? _categories; +@override List? get categories { + final value = _categories; + if (value == null) return null; + if (_categories is EqualUnmodifiableListView) return _categories; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + + final List? _tags; +@override List? get tags { + final value = _tags; + if (value == null) return null; + if (_tags is EqualUnmodifiableListView) return _tags; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + +@override final bool? pinned; +@override@JsonKey() final bool shuffle; +@override final bool? includeReplies; +@override final bool? mediaOnly; +@override final String? queryTerm; +@override final String? order; +@override final int? periodStart; +@override final int? periodEnd; +@override@JsonKey() final bool orderDesc; + +/// Create a copy of PostListQuery +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$PostListQueryCopyWith<_PostListQuery> get copyWith => __$PostListQueryCopyWithImpl<_PostListQuery>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostListQuery&&(identical(other.pubName, pubName) || other.pubName == pubName)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.pinned, pinned) || other.pinned == pinned)&&(identical(other.shuffle, shuffle) || other.shuffle == shuffle)&&(identical(other.includeReplies, includeReplies) || other.includeReplies == includeReplies)&&(identical(other.mediaOnly, mediaOnly) || other.mediaOnly == mediaOnly)&&(identical(other.queryTerm, queryTerm) || other.queryTerm == queryTerm)&&(identical(other.order, order) || other.order == order)&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&(identical(other.orderDesc, orderDesc) || other.orderDesc == orderDesc)); +} + + +@override +int get hashCode => Object.hash(runtimeType,pubName,realm,type,const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_tags),pinned,shuffle,includeReplies,mediaOnly,queryTerm,order,periodStart,periodEnd,orderDesc); + +@override +String toString() { + return 'PostListQuery(pubName: $pubName, realm: $realm, type: $type, categories: $categories, tags: $tags, pinned: $pinned, shuffle: $shuffle, includeReplies: $includeReplies, mediaOnly: $mediaOnly, queryTerm: $queryTerm, order: $order, periodStart: $periodStart, periodEnd: $periodEnd, orderDesc: $orderDesc)'; +} + + +} + +/// @nodoc +abstract mixin class _$PostListQueryCopyWith<$Res> implements $PostListQueryCopyWith<$Res> { + factory _$PostListQueryCopyWith(_PostListQuery value, $Res Function(_PostListQuery) _then) = __$PostListQueryCopyWithImpl; +@override @useResult +$Res call({ + String? pubName, String? realm, int? type, List? categories, List? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc +}); + + + + +} +/// @nodoc +class __$PostListQueryCopyWithImpl<$Res> + implements _$PostListQueryCopyWith<$Res> { + __$PostListQueryCopyWithImpl(this._self, this._then); + + final _PostListQuery _self; + final $Res Function(_PostListQuery) _then; + +/// Create a copy of PostListQuery +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? pubName = freezed,Object? realm = freezed,Object? type = freezed,Object? categories = freezed,Object? tags = freezed,Object? pinned = freezed,Object? shuffle = null,Object? includeReplies = freezed,Object? mediaOnly = freezed,Object? queryTerm = freezed,Object? order = freezed,Object? periodStart = freezed,Object? periodEnd = freezed,Object? orderDesc = null,}) { + return _then(_PostListQuery( +pubName: freezed == pubName ? _self.pubName : pubName // ignore: cast_nullable_to_non_nullable +as String?,realm: freezed == realm ? _self.realm : realm // ignore: cast_nullable_to_non_nullable +as String?,type: freezed == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as int?,categories: freezed == categories ? _self._categories : categories // ignore: cast_nullable_to_non_nullable +as List?,tags: freezed == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable +as List?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable +as bool?,shuffle: null == shuffle ? _self.shuffle : shuffle // ignore: cast_nullable_to_non_nullable +as bool,includeReplies: freezed == includeReplies ? _self.includeReplies : includeReplies // ignore: cast_nullable_to_non_nullable +as bool?,mediaOnly: freezed == mediaOnly ? _self.mediaOnly : mediaOnly // ignore: cast_nullable_to_non_nullable +as bool?,queryTerm: freezed == queryTerm ? _self.queryTerm : queryTerm // ignore: cast_nullable_to_non_nullable +as String?,order: freezed == order ? _self.order : order // ignore: cast_nullable_to_non_nullable +as String?,periodStart: freezed == periodStart ? _self.periodStart : periodStart // ignore: cast_nullable_to_non_nullable +as int?,periodEnd: freezed == periodEnd ? _self.periodEnd : periodEnd // ignore: cast_nullable_to_non_nullable +as int?,orderDesc: null == orderDesc ? _self.orderDesc : orderDesc // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + +// dart format on diff --git a/lib/widgets/post/post_list.g.dart b/lib/widgets/post/post_list.g.dart deleted file mode 100644 index 40357597..00000000 --- a/lib/widgets/post/post_list.g.dart +++ /dev/null @@ -1,457 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'post_list.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$postListNotifierHash() => r'bfc3d652dffc5ff3a94a6c3d04aac65354fe63b5'; - -/// 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 _$PostListNotifier - extends BuildlessAutoDisposeAsyncNotifier> { - late final String? pubName; - late final String? realm; - late final int? type; - late final List? categories; - late final List? tags; - late final bool? pinned; - late final bool shuffle; - late final bool? includeReplies; - late final bool? mediaOnly; - late final String? queryTerm; - late final String? order; - late final int? periodStart; - late final int? periodEnd; - late final bool orderDesc; - - FutureOr> build({ - String? pubName, - String? realm, - int? type, - List? categories, - List? tags, - bool? pinned, - bool shuffle = false, - bool? includeReplies, - bool? mediaOnly, - String? queryTerm, - String? order, - int? periodStart, - int? periodEnd, - bool orderDesc = true, - }); -} - -/// See also [PostListNotifier]. -@ProviderFor(PostListNotifier) -const postListNotifierProvider = PostListNotifierFamily(); - -/// See also [PostListNotifier]. -class PostListNotifierFamily - extends Family>> { - /// See also [PostListNotifier]. - const PostListNotifierFamily(); - - /// See also [PostListNotifier]. - PostListNotifierProvider call({ - String? pubName, - String? realm, - int? type, - List? categories, - List? tags, - bool? pinned, - bool shuffle = false, - bool? includeReplies, - bool? mediaOnly, - String? queryTerm, - String? order, - int? periodStart, - int? periodEnd, - bool orderDesc = true, - }) { - return PostListNotifierProvider( - pubName: pubName, - realm: realm, - type: type, - categories: categories, - tags: tags, - pinned: pinned, - shuffle: shuffle, - includeReplies: includeReplies, - mediaOnly: mediaOnly, - queryTerm: queryTerm, - order: order, - periodStart: periodStart, - periodEnd: periodEnd, - orderDesc: orderDesc, - ); - } - - @override - PostListNotifierProvider getProviderOverride( - covariant PostListNotifierProvider provider, - ) { - return call( - pubName: provider.pubName, - realm: provider.realm, - type: provider.type, - categories: provider.categories, - tags: provider.tags, - pinned: provider.pinned, - shuffle: provider.shuffle, - includeReplies: provider.includeReplies, - mediaOnly: provider.mediaOnly, - queryTerm: provider.queryTerm, - order: provider.order, - periodStart: provider.periodStart, - periodEnd: provider.periodEnd, - orderDesc: provider.orderDesc, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'postListNotifierProvider'; -} - -/// See also [PostListNotifier]. -class PostListNotifierProvider - extends - AutoDisposeAsyncNotifierProviderImpl< - PostListNotifier, - CursorPagingData - > { - /// See also [PostListNotifier]. - PostListNotifierProvider({ - String? pubName, - String? realm, - int? type, - List? categories, - List? tags, - bool? pinned, - bool shuffle = false, - bool? includeReplies, - bool? mediaOnly, - String? queryTerm, - String? order, - int? periodStart, - int? periodEnd, - bool orderDesc = true, - }) : this._internal( - () => - PostListNotifier() - ..pubName = pubName - ..realm = realm - ..type = type - ..categories = categories - ..tags = tags - ..pinned = pinned - ..shuffle = shuffle - ..includeReplies = includeReplies - ..mediaOnly = mediaOnly - ..queryTerm = queryTerm - ..order = order - ..periodStart = periodStart - ..periodEnd = periodEnd - ..orderDesc = orderDesc, - from: postListNotifierProvider, - name: r'postListNotifierProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$postListNotifierHash, - dependencies: PostListNotifierFamily._dependencies, - allTransitiveDependencies: - PostListNotifierFamily._allTransitiveDependencies, - pubName: pubName, - realm: realm, - type: type, - categories: categories, - tags: tags, - pinned: pinned, - shuffle: shuffle, - includeReplies: includeReplies, - mediaOnly: mediaOnly, - queryTerm: queryTerm, - order: order, - periodStart: periodStart, - periodEnd: periodEnd, - orderDesc: orderDesc, - ); - - PostListNotifierProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.pubName, - required this.realm, - required this.type, - required this.categories, - required this.tags, - required this.pinned, - required this.shuffle, - required this.includeReplies, - required this.mediaOnly, - required this.queryTerm, - required this.order, - required this.periodStart, - required this.periodEnd, - required this.orderDesc, - }) : super.internal(); - - final String? pubName; - final String? realm; - final int? type; - final List? categories; - final List? tags; - final bool? pinned; - final bool shuffle; - final bool? includeReplies; - final bool? mediaOnly; - final String? queryTerm; - final String? order; - final int? periodStart; - final int? periodEnd; - final bool orderDesc; - - @override - FutureOr> runNotifierBuild( - covariant PostListNotifier notifier, - ) { - return notifier.build( - pubName: pubName, - realm: realm, - type: type, - categories: categories, - tags: tags, - pinned: pinned, - shuffle: shuffle, - includeReplies: includeReplies, - mediaOnly: mediaOnly, - queryTerm: queryTerm, - order: order, - periodStart: periodStart, - periodEnd: periodEnd, - orderDesc: orderDesc, - ); - } - - @override - Override overrideWith(PostListNotifier Function() create) { - return ProviderOverride( - origin: this, - override: PostListNotifierProvider._internal( - () => - create() - ..pubName = pubName - ..realm = realm - ..type = type - ..categories = categories - ..tags = tags - ..pinned = pinned - ..shuffle = shuffle - ..includeReplies = includeReplies - ..mediaOnly = mediaOnly - ..queryTerm = queryTerm - ..order = order - ..periodStart = periodStart - ..periodEnd = periodEnd - ..orderDesc = orderDesc, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - pubName: pubName, - realm: realm, - type: type, - categories: categories, - tags: tags, - pinned: pinned, - shuffle: shuffle, - includeReplies: includeReplies, - mediaOnly: mediaOnly, - queryTerm: queryTerm, - order: order, - periodStart: periodStart, - periodEnd: periodEnd, - orderDesc: orderDesc, - ), - ); - } - - @override - AutoDisposeAsyncNotifierProviderElement< - PostListNotifier, - CursorPagingData - > - createElement() { - return _PostListNotifierProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is PostListNotifierProvider && - other.pubName == pubName && - other.realm == realm && - other.type == type && - other.categories == categories && - other.tags == tags && - other.pinned == pinned && - other.shuffle == shuffle && - other.includeReplies == includeReplies && - other.mediaOnly == mediaOnly && - other.queryTerm == queryTerm && - other.order == order && - other.periodStart == periodStart && - other.periodEnd == periodEnd && - other.orderDesc == orderDesc; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, pubName.hashCode); - hash = _SystemHash.combine(hash, realm.hashCode); - hash = _SystemHash.combine(hash, type.hashCode); - hash = _SystemHash.combine(hash, categories.hashCode); - hash = _SystemHash.combine(hash, tags.hashCode); - hash = _SystemHash.combine(hash, pinned.hashCode); - hash = _SystemHash.combine(hash, shuffle.hashCode); - hash = _SystemHash.combine(hash, includeReplies.hashCode); - hash = _SystemHash.combine(hash, mediaOnly.hashCode); - hash = _SystemHash.combine(hash, queryTerm.hashCode); - hash = _SystemHash.combine(hash, order.hashCode); - hash = _SystemHash.combine(hash, periodStart.hashCode); - hash = _SystemHash.combine(hash, periodEnd.hashCode); - hash = _SystemHash.combine(hash, orderDesc.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin PostListNotifierRef - on AutoDisposeAsyncNotifierProviderRef> { - /// The parameter `pubName` of this provider. - String? get pubName; - - /// The parameter `realm` of this provider. - String? get realm; - - /// The parameter `type` of this provider. - int? get type; - - /// The parameter `categories` of this provider. - List? get categories; - - /// The parameter `tags` of this provider. - List? get tags; - - /// The parameter `pinned` of this provider. - bool? get pinned; - - /// The parameter `shuffle` of this provider. - bool get shuffle; - - /// The parameter `includeReplies` of this provider. - bool? get includeReplies; - - /// The parameter `mediaOnly` of this provider. - bool? get mediaOnly; - - /// The parameter `queryTerm` of this provider. - String? get queryTerm; - - /// The parameter `order` of this provider. - String? get order; - - /// The parameter `periodStart` of this provider. - int? get periodStart; - - /// The parameter `periodEnd` of this provider. - int? get periodEnd; - - /// The parameter `orderDesc` of this provider. - bool get orderDesc; -} - -class _PostListNotifierProviderElement - extends - AutoDisposeAsyncNotifierProviderElement< - PostListNotifier, - CursorPagingData - > - with PostListNotifierRef { - _PostListNotifierProviderElement(super.provider); - - @override - String? get pubName => (origin as PostListNotifierProvider).pubName; - @override - String? get realm => (origin as PostListNotifierProvider).realm; - @override - int? get type => (origin as PostListNotifierProvider).type; - @override - List? get categories => - (origin as PostListNotifierProvider).categories; - @override - List? get tags => (origin as PostListNotifierProvider).tags; - @override - bool? get pinned => (origin as PostListNotifierProvider).pinned; - @override - bool get shuffle => (origin as PostListNotifierProvider).shuffle; - @override - bool? get includeReplies => - (origin as PostListNotifierProvider).includeReplies; - @override - bool? get mediaOnly => (origin as PostListNotifierProvider).mediaOnly; - @override - String? get queryTerm => (origin as PostListNotifierProvider).queryTerm; - @override - String? get order => (origin as PostListNotifierProvider).order; - @override - int? get periodStart => (origin as PostListNotifierProvider).periodStart; - @override - int? get periodEnd => (origin as PostListNotifierProvider).periodEnd; - @override - bool get orderDesc => (origin as PostListNotifierProvider).orderDesc; -} - -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/widgets/post/post_reaction_sheet.dart b/lib/widgets/post/post_reaction_sheet.dart index 02df1ea4..b0eabebb 100644 --- a/lib/widgets/post/post_reaction_sheet.dart +++ b/lib/widgets/post/post_reaction_sheet.dart @@ -8,60 +8,63 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/post.dart'; import 'package:island/pods/network.dart'; +import 'package:island/pods/paging.dart'; import 'package:island/services/time.dart'; import 'package:island/widgets/account/account_pfc.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/sheet.dart'; +import 'package:island/widgets/paging/pagination_list.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:styled_widget/styled_widget.dart'; import 'package:island/widgets/stickers/sticker_picker.dart'; import 'package:island/pods/config.dart'; -part 'post_reaction_sheet.g.dart'; +class ReactionListParams { + final String symbol; + final String postId; -@riverpod -class ReactionListNotifier extends _$ReactionListNotifier - with CursorPagingNotifierMixin { + const ReactionListParams({required this.symbol, required this.postId}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ReactionListParams && + runtimeType == other.runtimeType && + symbol == other.symbol && + postId == other.postId; + + @override + int get hashCode => symbol.hashCode ^ postId.hashCode; +} + +final reactionListNotifierProvider = AsyncNotifierProvider.autoDispose + .family, ReactionListParams>( + ReactionListNotifier.new, + ); + +class ReactionListNotifier + extends + AutoDisposeFamilyAsyncNotifier, ReactionListParams> + with FamilyAsyncPaginationController { static const int _pageSize = 20; - int? totalCount; - @override - Future> build({ - required String symbol, - required String postId, - }) { - return fetch(cursor: null); - } - - @override - Future> fetch({ - required String? cursor, - }) async { + Future> fetch() async { final client = ref.read(apiClientProvider); - final offset = cursor == null ? 0 : int.parse(cursor); final response = await client.get( - '/sphere/posts/$postId/reactions', - queryParameters: {'symbol': symbol, 'offset': offset, 'take': _pageSize}, + '/sphere/posts/${arg.postId}/reactions', + queryParameters: { + 'symbol': arg.symbol, + 'offset': fetchedCount, + 'take': _pageSize, + }, ); totalCount = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0; final List data = response.data; - final reactions = - data.map((json) => SnPostReaction.fromJson(json)).toList(); - - final hasMore = reactions.length == _pageSize; - final nextCursor = hasMore ? (offset + reactions.length).toString() : null; - - return CursorPagingData( - items: reactions, - hasMore: hasMore, - nextCursor: nextCursor, - ); + return data.map((json) => SnPostReaction.fromJson(json)).toList(); } } @@ -485,10 +488,8 @@ class ReactionDetailsPopup extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final provider = reactionListNotifierProvider( - symbol: symbol, - postId: postId, - ); + final params = ReactionListParams(symbol: symbol, postId: postId); + final provider = reactionListNotifierProvider(params); final width = math.min(MediaQuery.of(context).size.width * 0.8, 480.0); return PopupCard( @@ -516,34 +517,24 @@ class ReactionDetailsPopup extends HookConsumerWidget { ), const Divider(height: 1), Expanded( - child: PagingHelperView( + child: PaginationList( provider: provider, - futureRefreshable: provider.future, - notifierRefreshable: provider.notifier, - contentBuilder: - (data, widgetCount, endItemView) => ListView.builder( - padding: EdgeInsets.zero, - itemCount: widgetCount, - itemBuilder: (context, index) { - if (index == widgetCount - 1) { - return endItemView; - } - - final reaction = data.items[index]; - return ListTile( - leading: AccountPfcGestureDetector( - uname: reaction.account?.name ?? 'unknown', - child: ProfilePictureWidget( - file: reaction.account?.profile.picture, - ), - ), - title: Text(reaction.account?.nick ?? 'unknown'.tr()), - subtitle: Text( - '${reaction.createdAt.formatRelative(context)} · ${reaction.createdAt.formatSystem()}', - ), - ); - }, + notifier: provider.notifier, + padding: EdgeInsets.zero, + itemBuilder: (context, index, reaction) { + return ListTile( + leading: AccountPfcGestureDetector( + uname: reaction.account?.name ?? 'unknown', + child: ProfilePictureWidget( + file: reaction.account?.profile.picture, + ), ), + title: Text(reaction.account?.nick ?? 'unknown'.tr()), + subtitle: Text( + '${reaction.createdAt.formatRelative(context)} · ${reaction.createdAt.formatSystem()}', + ), + ); + }, ), ), ], diff --git a/lib/widgets/post/post_reaction_sheet.g.dart b/lib/widgets/post/post_reaction_sheet.g.dart deleted file mode 100644 index 06750266..00000000 --- a/lib/widgets/post/post_reaction_sheet.g.dart +++ /dev/null @@ -1,206 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'post_reaction_sheet.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$reactionListNotifierHash() => - r'92cf80d2461e46ca62cf6e6a37f8b16c239e7449'; - -/// 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 _$ReactionListNotifier - extends - BuildlessAutoDisposeAsyncNotifier> { - late final String symbol; - late final String postId; - - FutureOr> build({ - required String symbol, - required String postId, - }); -} - -/// See also [ReactionListNotifier]. -@ProviderFor(ReactionListNotifier) -const reactionListNotifierProvider = ReactionListNotifierFamily(); - -/// See also [ReactionListNotifier]. -class ReactionListNotifierFamily - extends Family>> { - /// See also [ReactionListNotifier]. - const ReactionListNotifierFamily(); - - /// See also [ReactionListNotifier]. - ReactionListNotifierProvider call({ - required String symbol, - required String postId, - }) { - return ReactionListNotifierProvider(symbol: symbol, postId: postId); - } - - @override - ReactionListNotifierProvider getProviderOverride( - covariant ReactionListNotifierProvider provider, - ) { - return call(symbol: provider.symbol, postId: provider.postId); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'reactionListNotifierProvider'; -} - -/// See also [ReactionListNotifier]. -class ReactionListNotifierProvider - extends - AutoDisposeAsyncNotifierProviderImpl< - ReactionListNotifier, - CursorPagingData - > { - /// See also [ReactionListNotifier]. - ReactionListNotifierProvider({required String symbol, required String postId}) - : this._internal( - () => - ReactionListNotifier() - ..symbol = symbol - ..postId = postId, - from: reactionListNotifierProvider, - name: r'reactionListNotifierProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$reactionListNotifierHash, - dependencies: ReactionListNotifierFamily._dependencies, - allTransitiveDependencies: - ReactionListNotifierFamily._allTransitiveDependencies, - symbol: symbol, - postId: postId, - ); - - ReactionListNotifierProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.symbol, - required this.postId, - }) : super.internal(); - - final String symbol; - final String postId; - - @override - FutureOr> runNotifierBuild( - covariant ReactionListNotifier notifier, - ) { - return notifier.build(symbol: symbol, postId: postId); - } - - @override - Override overrideWith(ReactionListNotifier Function() create) { - return ProviderOverride( - origin: this, - override: ReactionListNotifierProvider._internal( - () => - create() - ..symbol = symbol - ..postId = postId, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - symbol: symbol, - postId: postId, - ), - ); - } - - @override - AutoDisposeAsyncNotifierProviderElement< - ReactionListNotifier, - CursorPagingData - > - createElement() { - return _ReactionListNotifierProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is ReactionListNotifierProvider && - other.symbol == symbol && - other.postId == postId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, symbol.hashCode); - hash = _SystemHash.combine(hash, postId.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin ReactionListNotifierRef - on AutoDisposeAsyncNotifierProviderRef> { - /// The parameter `symbol` of this provider. - String get symbol; - - /// The parameter `postId` of this provider. - String get postId; -} - -class _ReactionListNotifierProviderElement - extends - AutoDisposeAsyncNotifierProviderElement< - ReactionListNotifier, - CursorPagingData - > - with ReactionListNotifierRef { - _ReactionListNotifierProviderElement(super.provider); - - @override - String get symbol => (origin as ReactionListNotifierProvider).symbol; - @override - String get postId => (origin as ReactionListNotifierProvider).postId; -} - -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/widgets/post/post_replies.dart b/lib/widgets/post/post_replies.dart index 98008e86..318001fa 100644 --- a/lib/widgets/post/post_replies.dart +++ b/lib/widgets/post/post_replies.dart @@ -2,54 +2,30 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/post.dart'; import 'package:island/pods/network.dart'; +import 'package:island/pods/paging.dart'; +import 'package:island/widgets/paging/pagination_list.dart'; import 'package:island/widgets/post/post_item.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; -import 'package:styled_widget/styled_widget.dart'; -part 'post_replies.g.dart'; +final postRepliesNotifierProvider = AsyncNotifierProvider.autoDispose + .family, String>(PostRepliesNotifier.new); -@riverpod -class PostRepliesNotifier extends _$PostRepliesNotifier - with CursorPagingNotifierMixin { +class PostRepliesNotifier + extends AutoDisposeFamilyAsyncNotifier, String> + with FamilyAsyncPaginationController { static const int _pageSize = 20; - PostRepliesNotifier(); - - String? _postId; - @override - Future> build(String postId) { - _postId = postId; - return fetch(cursor: null); - } - - @override - Future> fetch({required String? cursor}) async { - if (_postId == null) { - throw StateError('PostRepliesNotifier must be initialized with postId'); - } - + Future> fetch() async { final client = ref.read(apiClientProvider); - final offset = cursor == null ? 0 : int.parse(cursor); final response = await client.get( - '/sphere/posts/$_postId/replies', - queryParameters: {'offset': offset, 'take': _pageSize}, + '/sphere/posts/$arg/replies', + queryParameters: {'offset': fetchedCount, 'take': _pageSize}, ); - final total = int.parse(response.headers.value('X-Total') ?? '0'); + totalCount = int.parse(response.headers.value('X-Total') ?? '0'); final List data = response.data; - final posts = data.map((json) => SnPost.fromJson(json)).toList(); - - final hasMore = offset + posts.length < total; - final nextCursor = hasMore ? (offset + posts.length).toString() : null; - - return CursorPagingData( - items: posts, - hasMore: hasMore, - nextCursor: nextCursor, - ); + return data.map((json) => SnPost.fromJson(json)).toList(); } } @@ -66,52 +42,32 @@ class PostRepliesList extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return PagingHelperSliverView( - provider: postRepliesNotifierProvider(postId), - futureRefreshable: postRepliesNotifierProvider(postId).future, - notifierRefreshable: postRepliesNotifierProvider(postId).notifier, - contentBuilder: (data, widgetCount, endItemView) { - if (data.items.isEmpty) { - return SliverToBoxAdapter( - child: Column( - children: [ - Text( - 'No replies', - textAlign: TextAlign.center, - ).fontSize(18).bold(), - const Text('Why not start a discussion?'), - ], - ).padding(vertical: 16), - ); - } + final provider = postRepliesNotifierProvider(postId); - return SliverList.builder( - itemCount: widgetCount, - itemBuilder: (context, index) { - if (index == widgetCount - 1) { - return endItemView; - } + return PaginationList( + provider: provider, + notifier: provider.notifier, + isRefreshable: false, + isSliver: true, + itemBuilder: (context, index, item) { + final contentWidget = Card( + margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: PostActionableItem( + borderRadius: 8, + item: item, + isShowReference: false, + isEmbedOpenable: true, + onOpen: onOpen, + ), + ); - final contentWidget = Card( - margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: PostActionableItem( - borderRadius: 8, - item: data.items[index], - isShowReference: false, - isEmbedOpenable: true, - onOpen: onOpen, - ), - ); + if (maxWidth == null) return contentWidget; - if (maxWidth == null) return contentWidget; - - return Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: maxWidth!), - child: contentWidget, - ), - ); - }, + return Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxWidth!), + child: contentWidget, + ), ); }, ); diff --git a/lib/widgets/post/post_replies.g.dart b/lib/widgets/post/post_replies.g.dart deleted file mode 100644 index d315bb7a..00000000 --- a/lib/widgets/post/post_replies.g.dart +++ /dev/null @@ -1,180 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'post_replies.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$postRepliesNotifierHash() => - r'1cdda919249e3bf34459369e033ad5de8dbcf3f8'; - -/// 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 _$PostRepliesNotifier - extends BuildlessAutoDisposeAsyncNotifier> { - late final String postId; - - FutureOr> build(String postId); -} - -/// See also [PostRepliesNotifier]. -@ProviderFor(PostRepliesNotifier) -const postRepliesNotifierProvider = PostRepliesNotifierFamily(); - -/// See also [PostRepliesNotifier]. -class PostRepliesNotifierFamily - extends Family>> { - /// See also [PostRepliesNotifier]. - const PostRepliesNotifierFamily(); - - /// See also [PostRepliesNotifier]. - PostRepliesNotifierProvider call(String postId) { - return PostRepliesNotifierProvider(postId); - } - - @override - PostRepliesNotifierProvider getProviderOverride( - covariant PostRepliesNotifierProvider provider, - ) { - return call(provider.postId); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'postRepliesNotifierProvider'; -} - -/// See also [PostRepliesNotifier]. -class PostRepliesNotifierProvider - extends - AutoDisposeAsyncNotifierProviderImpl< - PostRepliesNotifier, - CursorPagingData - > { - /// See also [PostRepliesNotifier]. - PostRepliesNotifierProvider(String postId) - : this._internal( - () => PostRepliesNotifier()..postId = postId, - from: postRepliesNotifierProvider, - name: r'postRepliesNotifierProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$postRepliesNotifierHash, - dependencies: PostRepliesNotifierFamily._dependencies, - allTransitiveDependencies: - PostRepliesNotifierFamily._allTransitiveDependencies, - postId: postId, - ); - - PostRepliesNotifierProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.postId, - }) : super.internal(); - - final String postId; - - @override - FutureOr> runNotifierBuild( - covariant PostRepliesNotifier notifier, - ) { - return notifier.build(postId); - } - - @override - Override overrideWith(PostRepliesNotifier Function() create) { - return ProviderOverride( - origin: this, - override: PostRepliesNotifierProvider._internal( - () => create()..postId = postId, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - postId: postId, - ), - ); - } - - @override - AutoDisposeAsyncNotifierProviderElement< - PostRepliesNotifier, - CursorPagingData - > - createElement() { - return _PostRepliesNotifierProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is PostRepliesNotifierProvider && other.postId == postId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, postId.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin PostRepliesNotifierRef - on AutoDisposeAsyncNotifierProviderRef> { - /// The parameter `postId` of this provider. - String get postId; -} - -class _PostRepliesNotifierProviderElement - extends - AutoDisposeAsyncNotifierProviderElement< - PostRepliesNotifier, - CursorPagingData - > - with PostRepliesNotifierRef { - _PostRepliesNotifierProviderElement(super.provider); - - @override - String get postId => (origin as PostRepliesNotifierProvider).postId; -} - -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/widgets/post/post_shuffle.dart b/lib/widgets/post/post_shuffle.dart index 4335d251..fcf279e2 100644 --- a/lib/widgets/post/post_shuffle.dart +++ b/lib/widgets/post/post_shuffle.dart @@ -14,9 +14,10 @@ class PostShuffleScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final postListState = ref.watch(postListNotifierProvider(shuffle: true)); + const params = PostListQuery(shuffle: true); + final postListState = ref.watch(postListNotifierProvider(params)); final postListNotifier = ref.watch( - postListNotifierProvider(shuffle: true).notifier, + postListNotifierProvider(params).notifier, ); final cardSwiperController = useMemoized(() => CardSwiperController(), []); @@ -37,12 +38,13 @@ class PostShuffleScreen extends HookConsumerWidget { kBottomControlHeight + MediaQuery.of(context).padding.bottom, ), child: Builder( - key: ValueKey(postListState.value?.items.length ?? 0), + key: ValueKey(postListState.valueOrNull?.length ?? 0), builder: (context) { - if ((postListState.value?.items.length ?? 0) > 0) { + final items = postListState.valueOrNull ?? []; + if (items.isNotEmpty) { return CardSwiper( controller: cardSwiperController, - cardsCount: postListState.value!.items.length, + cardsCount: items.length, isLoop: false, cardBuilder: ( context, @@ -60,9 +62,7 @@ class PostShuffleScreen extends HookConsumerWidget { borderRadius: const BorderRadius.all( Radius.circular(8), ), - child: PostActionableItem( - item: postListState.value!.items[index], - ), + child: PostActionableItem(item: items[index]), ), ), ), @@ -70,8 +70,8 @@ class PostShuffleScreen extends HookConsumerWidget { ); }, onEnd: () async { - if (postListState.value?.hasMore ?? true) { - postListNotifier.forceRefresh(); + if (!postListNotifier.fetchedAll) { + postListNotifier.fetchFurther(); } }, ); diff --git a/lib/widgets/realm/realm_list.dart b/lib/widgets/realm/realm_list.dart index 54bb4d0d..28ebff72 100644 --- a/lib/widgets/realm/realm_list.dart +++ b/lib/widgets/realm/realm_list.dart @@ -3,53 +3,36 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/realm.dart'; import 'package:island/pods/network.dart'; +import 'package:island/pods/paging.dart'; +import 'package:island/widgets/paging/pagination_list.dart'; import 'package:island/widgets/realm/realm_card.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:styled_widget/styled_widget.dart'; -part 'realm_list.g.dart'; +final realmListNotifierProvider = AsyncNotifierProvider.autoDispose + .family, String?>(RealmListNotifier.new); -@riverpod -class RealmListNotifier extends _$RealmListNotifier - with CursorPagingNotifierMixin { +class RealmListNotifier + extends AutoDisposeFamilyAsyncNotifier, String?> + with FamilyAsyncPaginationController { static const int _pageSize = 20; @override - Future> build(String? query) { - return fetch(cursor: null, query: query); - } - - @override - Future> fetch({ - required String? cursor, - String? query, - }) async { + Future> fetch() async { final client = ref.read(apiClientProvider); - final offset = cursor == null ? 0 : int.parse(cursor); final queryParams = { - 'offset': offset, + 'offset': fetchedCount, 'take': _pageSize, - if (query != null && query.isNotEmpty) 'query': query, + if (arg != null && arg!.isNotEmpty) 'query': arg, }; final response = await client.get( '/sphere/discovery/realms', queryParameters: queryParams, ); - final total = int.parse(response.headers.value('X-Total') ?? '0'); + totalCount = int.parse(response.headers.value('X-Total') ?? '0'); final List data = response.data; - final realms = data.map((json) => SnRealm.fromJson(json)).toList(); - - final hasMore = offset + realms.length < total; - final nextCursor = hasMore ? (offset + realms.length).toString() : null; - - return CursorPagingData( - items: realms, - hasMore: hasMore, - nextCursor: nextCursor, - ); + return data.map((json) => SnRealm.fromJson(json)).toList(); } } @@ -60,34 +43,32 @@ class SliverRealmList extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return PagingHelperSliverView( - provider: realmListNotifierProvider(query), - futureRefreshable: realmListNotifierProvider(query).future, - notifierRefreshable: realmListNotifierProvider(query).notifier, - contentBuilder: - (data, widgetCount, endItemView) => SliverList.separated( - itemCount: widgetCount, - itemBuilder: (context, index) { - if (index == widgetCount - 1) { - return endItemView; - } - - final realm = data.items[index]; - - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - child: - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 540), - child: RealmCard(realm: realm), - ).center(), - ); - }, - separatorBuilder: (_, _) => const Gap(8), - ), + final provider = realmListNotifierProvider(query); + return PaginationList( + provider: provider, + notifier: provider.notifier, + isSliver: true, + isRefreshable: false, + itemBuilder: (context, index, realm) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 540), + child: RealmCard(realm: realm), + ).center(), + ), + if (index < + (ref.read(provider).valueOrNull?.length ?? 0) - + 1) // Add gap except for last item? Actually PaginationList handles loading indicator which might look like last item. + // Wait, ref.read(provider).valueOrNull?.length might change. + // Simpler to just add bottom padding to all, or Gap. + const Gap(8), + ], + ); + }, ); } } diff --git a/lib/widgets/realm/realm_list.g.dart b/lib/widgets/realm/realm_list.g.dart deleted file mode 100644 index 742497ee..00000000 --- a/lib/widgets/realm/realm_list.g.dart +++ /dev/null @@ -1,179 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'realm_list.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$realmListNotifierHash() => r'8ae5c3ae2837acae4c7bf5e44578518afc9ea1a6'; - -/// 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 _$RealmListNotifier - extends BuildlessAutoDisposeAsyncNotifier> { - late final String? query; - - FutureOr> build(String? query); -} - -/// See also [RealmListNotifier]. -@ProviderFor(RealmListNotifier) -const realmListNotifierProvider = RealmListNotifierFamily(); - -/// See also [RealmListNotifier]. -class RealmListNotifierFamily - extends Family>> { - /// See also [RealmListNotifier]. - const RealmListNotifierFamily(); - - /// See also [RealmListNotifier]. - RealmListNotifierProvider call(String? query) { - return RealmListNotifierProvider(query); - } - - @override - RealmListNotifierProvider getProviderOverride( - covariant RealmListNotifierProvider provider, - ) { - return call(provider.query); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'realmListNotifierProvider'; -} - -/// See also [RealmListNotifier]. -class RealmListNotifierProvider - extends - AutoDisposeAsyncNotifierProviderImpl< - RealmListNotifier, - CursorPagingData - > { - /// See also [RealmListNotifier]. - RealmListNotifierProvider(String? query) - : this._internal( - () => RealmListNotifier()..query = query, - from: realmListNotifierProvider, - name: r'realmListNotifierProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$realmListNotifierHash, - dependencies: RealmListNotifierFamily._dependencies, - allTransitiveDependencies: - RealmListNotifierFamily._allTransitiveDependencies, - query: query, - ); - - RealmListNotifierProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.query, - }) : super.internal(); - - final String? query; - - @override - FutureOr> runNotifierBuild( - covariant RealmListNotifier notifier, - ) { - return notifier.build(query); - } - - @override - Override overrideWith(RealmListNotifier Function() create) { - return ProviderOverride( - origin: this, - override: RealmListNotifierProvider._internal( - () => create()..query = query, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - query: query, - ), - ); - } - - @override - AutoDisposeAsyncNotifierProviderElement< - RealmListNotifier, - CursorPagingData - > - createElement() { - return _RealmListNotifierProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is RealmListNotifierProvider && other.query == query; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, query.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin RealmListNotifierRef - on AutoDisposeAsyncNotifierProviderRef> { - /// The parameter `query` of this provider. - String? get query; -} - -class _RealmListNotifierProviderElement - extends - AutoDisposeAsyncNotifierProviderElement< - RealmListNotifier, - CursorPagingData - > - with RealmListNotifierRef { - _RealmListNotifierProviderElement(super.provider); - - @override - String? get query => (origin as RealmListNotifierProvider).query; -} - -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/widgets/thought/thought_sequence_list.dart b/lib/widgets/thought/thought_sequence_list.dart index f70074e3..d6ff796b 100644 --- a/lib/widgets/thought/thought_sequence_list.dart +++ b/lib/widgets/thought/thought_sequence_list.dart @@ -2,49 +2,34 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/thought.dart'; import 'package:island/pods/network.dart'; +import 'package:island/pods/paging.dart'; import 'package:island/services/time.dart'; import 'package:island/widgets/content/sheet.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; +import 'package:island/widgets/paging/pagination_list.dart'; -part 'thought_sequence_list.g.dart'; +final thoughtSequenceListNotifierProvider = AsyncNotifierProvider.autoDispose< + ThoughtSequenceListNotifier, + List +>(ThoughtSequenceListNotifier.new); -@riverpod -class ThoughtSequenceListNotifier extends _$ThoughtSequenceListNotifier - with CursorPagingNotifierMixin { +class ThoughtSequenceListNotifier + extends AutoDisposeAsyncNotifier> + with AutoDisposeAsyncPaginationController { static const int _pageSize = 20; @override - Future> build() { - return fetch(cursor: null); - } - - @override - Future> fetch({ - required String? cursor, - }) async { + Future> fetch() async { final client = ref.read(apiClientProvider); - final offset = cursor == null ? 0 : int.parse(cursor); - final queryParams = {'offset': offset, 'take': _pageSize}; + final queryParams = {'offset': fetchedCount, 'take': _pageSize}; final response = await client.get( '/insight/thought/sequences', queryParameters: queryParams, ); - final total = int.parse(response.headers.value('X-Total') ?? '0'); + totalCount = int.parse(response.headers.value('X-Total') ?? '0'); final List data = response.data; - final sequences = - data.map((json) => SnThinkingSequence.fromJson(json)).toList(); - - final hasMore = offset + sequences.length < total; - final nextCursor = hasMore ? (offset + sequences.length).toString() : null; - - return CursorPagingData( - items: sequences, - hasMore: hasMore, - nextCursor: nextCursor, - ); + return data.map((json) => SnThinkingSequence.fromJson(json)).toList(); } } @@ -58,29 +43,19 @@ class ThoughtSequenceSelector extends HookConsumerWidget { final provider = thoughtSequenceListNotifierProvider; return SheetScaffold( titleText: 'Select Conversation', - child: PagingHelperView( + child: PaginationList( provider: provider, - futureRefreshable: provider.future, - notifierRefreshable: provider.notifier, - contentBuilder: - (data, widgetCount, endItemView) => ListView.builder( - itemCount: widgetCount, - itemBuilder: (context, index) { - if (index == widgetCount - 1) { - return endItemView; - } - - final sequence = data.items[index]; - return ListTile( - title: Text(sequence.topic ?? 'Untitled Conversation'), - subtitle: Text(sequence.createdAt.formatSystem()), - onTap: () { - onSequenceSelected(sequence.id); - Navigator.of(context).pop(); - }, - ); - }, - ), + notifier: provider.notifier, + itemBuilder: (context, index, sequence) { + return ListTile( + title: Text(sequence.topic ?? 'Untitled Conversation'), + subtitle: Text(sequence.createdAt.formatSystem()), + onTap: () { + onSequenceSelected(sequence.id); + Navigator.of(context).pop(); + }, + ); + }, ), ); } diff --git a/lib/widgets/thought/thought_sequence_list.g.dart b/lib/widgets/thought/thought_sequence_list.g.dart deleted file mode 100644 index ccc78e4b..00000000 --- a/lib/widgets/thought/thought_sequence_list.g.dart +++ /dev/null @@ -1,31 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'thought_sequence_list.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$thoughtSequenceListNotifierHash() => - r'fc619402d8691d523d19d3c7633adfada4956db4'; - -/// See also [ThoughtSequenceListNotifier]. -@ProviderFor(ThoughtSequenceListNotifier) -final thoughtSequenceListNotifierProvider = AutoDisposeAsyncNotifierProvider< - ThoughtSequenceListNotifier, - CursorPagingData ->.internal( - ThoughtSequenceListNotifier.new, - name: r'thoughtSequenceListNotifierProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$thoughtSequenceListNotifierHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$ThoughtSequenceListNotifier = - AutoDisposeAsyncNotifier>; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package