153 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			153 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:easy_localization/easy_localization.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| 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/widgets/app_scaffold.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';
 | |
| 
 | |
| @riverpod
 | |
| Future<double> socialCredits(Ref ref) async {
 | |
|   final client = ref.watch(apiClientProvider);
 | |
|   final response = await client.get('/id/accounts/me/credits');
 | |
|   if (response.statusCode != 200) {
 | |
|     throw Exception('Failed to load social credits');
 | |
|   }
 | |
|   return response.data?.toDouble() ?? 0.0;
 | |
| }
 | |
| 
 | |
| @riverpod
 | |
| class SocialCreditHistoryNotifier extends _$SocialCreditHistoryNotifier
 | |
|     with CursorPagingNotifierMixin<SnSocialCreditRecord> {
 | |
|   static const int _pageSize = 20;
 | |
| 
 | |
|   @override
 | |
|   Future<CursorPagingData<SnSocialCreditRecord>> build() => fetch(cursor: null);
 | |
| 
 | |
|   @override
 | |
|   Future<CursorPagingData<SnSocialCreditRecord>> fetch({
 | |
|     required String? cursor,
 | |
|   }) async {
 | |
|     final client = ref.read(apiClientProvider);
 | |
|     final offset = cursor == null ? 0 : int.parse(cursor);
 | |
| 
 | |
|     final queryParams = {'offset': offset, 'take': _pageSize};
 | |
| 
 | |
|     final response = await client.get(
 | |
|       '/id/accounts/me/credits/history',
 | |
|       queryParameters: queryParams,
 | |
|     );
 | |
|     final total = int.parse(response.headers.value('X-Total') ?? '0');
 | |
|     final List<dynamic> data = response.data;
 | |
|     final records =
 | |
|         data.map((json) => SnSocialCreditRecord.fromJson(json)).toList();
 | |
| 
 | |
|     final hasMore = offset + records.length < total;
 | |
|     final nextCursor = hasMore ? (offset + records.length).toString() : null;
 | |
| 
 | |
|     return CursorPagingData(
 | |
|       items: records,
 | |
|       hasMore: hasMore,
 | |
|       nextCursor: nextCursor,
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class SocialCreditsScreen extends HookConsumerWidget {
 | |
|   const SocialCreditsScreen({super.key});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     final socialCredits = ref.watch(socialCreditsProvider);
 | |
| 
 | |
|     return AppScaffold(
 | |
|       appBar: AppBar(title: Text('socialCredits').tr()),
 | |
|       body: Column(
 | |
|         children: [
 | |
|           Card(
 | |
|             margin: EdgeInsets.only(left: 16, right: 16, top: 8),
 | |
|             child: socialCredits
 | |
|                 .when(
 | |
|                   data:
 | |
|                       (credits) => Stack(
 | |
|                         children: [
 | |
|                           Column(
 | |
|                             crossAxisAlignment: CrossAxisAlignment.start,
 | |
|                             children: [
 | |
|                               Text(
 | |
|                                 credits < 100
 | |
|                                     ? 'socialCreditsLevelPoor'.tr()
 | |
|                                     : credits < 150
 | |
|                                     ? 'socialCreditsLevelNormal'.tr()
 | |
|                                     : credits < 200
 | |
|                                     ? 'socialCreditsLevelGood'.tr()
 | |
|                                     : 'socialCreditsLevelExcellent'.tr(),
 | |
|                               ).tr().bold().fontSize(20),
 | |
|                               Text(
 | |
|                                 '${credits.toStringAsFixed(2)} pts',
 | |
|                               ).fontSize(14),
 | |
|                               const Gap(8),
 | |
|                               LinearProgressIndicator(value: credits / 200),
 | |
|                             ],
 | |
|                           ),
 | |
|                           Positioned(
 | |
|                             right: 0,
 | |
|                             top: 0,
 | |
|                             child: IconButton(
 | |
|                               onPressed: () {},
 | |
|                               icon: const Icon(Symbols.info),
 | |
|                               tooltip: 'socialCreditsDescription'.tr(),
 | |
|                             ),
 | |
|                           ),
 | |
|                         ],
 | |
|                       ),
 | |
|                   error: (_, _) => Text('Error loading credits'),
 | |
|                   loading: () => const LinearProgressIndicator(),
 | |
|                 )
 | |
|                 .padding(horizontal: 20, vertical: 16),
 | |
|           ),
 | |
|           Expanded(
 | |
|             child: PagingHelperView(
 | |
|               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: 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,
 | |
|                           ),
 | |
|                         ),
 | |
|                       );
 | |
|                     },
 | |
|                   ),
 | |
|             ),
 | |
|           ),
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 |