diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 0d485d85..8a69fb38 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -1492,5 +1492,6 @@ "accountActivationAlertHint": "Unactivated account may leads to various of permission issues, activate your account by clicking the link we sent to your email inbox.", "accountActivationResendHint": "Didn't see it? Try click the button below to resend one. If you need to update your email while your account was unactivated, feel free to contact our customer service.", "accountActivationResend": "Resend", - "ipAddress": "IP Address" + "ipAddress": "IP Address", + "noFurtherData": "No further data" } diff --git a/lib/screens/account/credits.dart b/lib/screens/account/credits.dart index 384b764b..62f14d50 100644 --- a/lib/screens/account/credits.dart +++ b/lib/screens/account/credits.dart @@ -4,9 +4,11 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/account.dart'; import 'package:island/pods/network.dart'; +import 'package:island/pods/paging.dart'; +import 'package:island/services/time.dart'; +import 'package:island/widgets/paging/pagination_list.dart'; import 'package:material_symbols_icons/material_symbols_icons.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 'credits.g.dart'; @@ -21,40 +23,35 @@ Future socialCredits(Ref ref) async { return response.data?.toDouble() ?? 0.0; } -@riverpod -class SocialCreditHistoryNotifier extends _$SocialCreditHistoryNotifier - with CursorPagingNotifierMixin { - static const int _pageSize = 20; +final socialCreditHistoryNotifierProvider = AsyncNotifierProvider( + SocialCreditHistoryNotifier.new, +); + +class SocialCreditHistoryNotifier + extends AsyncNotifier> + with AsyncPaginationController { + static const int pageSize = 20; @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 queryParams = {'offset': offset, 'take': _pageSize}; + final queryParams = {'offset': fetchedCount.toString(), 'take': pageSize}; final response = await client.get( '/pass/accounts/me/credits/history', queryParameters: queryParams, ); - final total = int.parse(response.headers.value('X-Total') ?? '0'); - final List data = response.data; + + totalCount = int.parse(response.headers.value('X-Total') ?? '0'); + final records = - data.map((json) => SnSocialCreditRecord.fromJson(json)).toList(); + response.data + .map((json) => SnSocialCreditRecord.fromJson(json)) + .cast() + .toList(); - final hasMore = offset + records.length < total; - final nextCursor = hasMore ? (offset + records.length).toString() : null; - - return CursorPagingData( - items: records, - hasMore: hasMore, - nextCursor: nextCursor, - ); + return records; } } @@ -110,38 +107,45 @@ class SocialCreditsTab extends HookConsumerWidget { .padding(horizontal: 20, vertical: 16), ), Expanded( - child: PagingHelperView( + child: PaginationList( + padding: EdgeInsets.zero, provider: socialCreditHistoryNotifierProvider, - futureRefreshable: socialCreditHistoryNotifierProvider.future, - notifierRefreshable: socialCreditHistoryNotifierProvider.notifier, - contentBuilder: - (data, widgetCount, endItemView) => ListView.builder( - padding: EdgeInsets.zero, - itemCount: widgetCount, - itemBuilder: (context, index) { - if (index == widgetCount - 1) { - return endItemView; - } - final record = data.items[index]; - return ListTile( - contentPadding: const EdgeInsets.symmetric( - horizontal: 24, - ), - title: Text(record.reason), - subtitle: Text( - DateFormat.yMMMd().format(record.createdAt), - ), - trailing: Text( - record.delta > 0 - ? '+${record.delta}' - : '${record.delta}', - style: TextStyle( - color: record.delta > 0 ? Colors.green : Colors.red, - ), - ), - ); - }, + notifier: socialCreditHistoryNotifierProvider.notifier, + itemBuilder: (context, idx, record) { + final isExpired = + record.expiredAt != null && + record.expiredAt!.isBefore(DateTime.now()); + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + title: Text( + record.reason, + style: + isExpired + ? TextStyle( + decoration: TextDecoration.lineThrough, + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.8), + ) + : null, ), + subtitle: Row( + spacing: 4, + children: [ + Text(record.createdAt.formatSystem()), + Text('to'), + if (record.expiredAt != null) + Text(record.expiredAt!.formatSystem()), + ], + ), + trailing: Text( + record.delta > 0 ? '+${record.delta}' : '${record.delta}', + style: TextStyle( + color: record.delta > 0 ? Colors.green : Colors.red, + ), + ), + ); + }, ), ), ], diff --git a/lib/screens/account/credits.g.dart b/lib/screens/account/credits.g.dart index 9bedeb36..0fd1c911 100644 --- a/lib/screens/account/credits.g.dart +++ b/lib/screens/account/credits.g.dart @@ -24,26 +24,5 @@ final socialCreditsProvider = AutoDisposeFutureProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef SocialCreditsRef = AutoDisposeFutureProviderRef; -String _$socialCreditHistoryNotifierHash() => - r'3e87af246cc5dc72a1f3a87b81d1c87169bdfb5b'; - -/// See also [SocialCreditHistoryNotifier]. -@ProviderFor(SocialCreditHistoryNotifier) -final socialCreditHistoryNotifierProvider = AutoDisposeAsyncNotifierProvider< - SocialCreditHistoryNotifier, - CursorPagingData ->.internal( - SocialCreditHistoryNotifier.new, - name: r'socialCreditHistoryNotifierProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$socialCreditHistoryNotifierHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$SocialCreditHistoryNotifier = - 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/paging/pagination_list.dart b/lib/widgets/paging/pagination_list.dart index df7e7094..a055ab73 100644 --- a/lib/widgets/paging/pagination_list.dart +++ b/lib/widgets/paging/pagination_list.dart @@ -1,9 +1,11 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/pods/paging.dart'; import 'package:island/widgets/extended_refresh_indicator.dart'; import 'package:island/widgets/response.dart'; +import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:super_sliver_list/super_sliver_list.dart'; import 'package:visibility_detector/visibility_detector.dart'; @@ -15,6 +17,7 @@ class PaginationList extends HookConsumerWidget { final bool isRefreshable; final bool isSliver; final bool showDefaultWidgets; + final EdgeInsets? padding; const PaginationList({ super.key, required this.provider, @@ -23,6 +26,7 @@ class PaginationList extends HookConsumerWidget { this.isRefreshable = true, this.isSliver = false, this.showDefaultWidgets = true, + this.padding, }); @override @@ -57,6 +61,7 @@ class PaginationList extends HookConsumerWidget { }, ) : SuperListView.builder( + padding: padding, itemCount: (data.valueOrNull?.length ?? 0) + 1, itemBuilder: (context, idx) { if (idx == data.valueOrNull?.length) { @@ -134,7 +139,19 @@ class PaginationListFooter extends StatelessWidget { Widget build(BuildContext context) { final child = SizedBox( height: 64, - child: Center(child: CircularProgressIndicator()).padding(all: 8), + child: Center( + child: + data.isLoading + ? CircularProgressIndicator() + : Row( + spacing: 8, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Symbols.close, size: 16), + Text('noFurtherData').tr().fontSize(13), + ], + ).opacity(0.9), + ).padding(all: 8), ); return VisibilityDetector(