295 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			295 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			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/pods/userinfo.dart';
 | |
| import 'package:island/screens/account/credits.dart';
 | |
| import 'package:island/services/time.dart';
 | |
| import 'package:island/widgets/account/leveling_progress.dart';
 | |
| import 'package:island/widgets/account/stellar_program_tab.dart';
 | |
| import 'package:island/widgets/app_scaffold.dart';
 | |
| import 'package:easy_localization/easy_localization.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 'leveling.g.dart';
 | |
| 
 | |
| @riverpod
 | |
| class LevelingHistoryNotifier extends _$LevelingHistoryNotifier
 | |
|     with CursorPagingNotifierMixin<SnExperienceRecord> {
 | |
|   static const int _pageSize = 20;
 | |
| 
 | |
|   @override
 | |
|   Future<CursorPagingData<SnExperienceRecord>> build() => fetch(cursor: null);
 | |
| 
 | |
|   @override
 | |
|   Future<CursorPagingData<SnExperienceRecord>> 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(
 | |
|       '/pass/accounts/me/leveling',
 | |
|       queryParameters: queryParams,
 | |
|     );
 | |
|     final total = int.parse(response.headers.value('X-Total') ?? '0');
 | |
|     final List<dynamic> data = response.data;
 | |
|     final records =
 | |
|         data.map((json) => SnExperienceRecord.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 LevelingScreen extends HookConsumerWidget {
 | |
|   const LevelingScreen({super.key});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     final user = ref.watch(userInfoProvider);
 | |
| 
 | |
|     if (user.value == null) {
 | |
|       return AppScaffold(
 | |
|         appBar: AppBar(title: Text('levelingProgress'.tr())),
 | |
|         body: const Center(child: CircularProgressIndicator()),
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return DefaultTabController(
 | |
|       length: 3,
 | |
|       child: AppScaffold(
 | |
|         appBar: AppBar(
 | |
|           title: Text('levelingProgress'.tr()),
 | |
|           bottom: TabBar(
 | |
|             tabs: [
 | |
|               Tab(
 | |
|                 child: Text(
 | |
|                   'leveling'.tr(),
 | |
|                   textAlign: TextAlign.center,
 | |
|                   style: TextStyle(
 | |
|                     color: Theme.of(context).appBarTheme.foregroundColor!,
 | |
|                   ),
 | |
|                 ),
 | |
|               ),
 | |
|               Tab(
 | |
|                 child: Text(
 | |
|                   'socialCredits'.tr(),
 | |
|                   textAlign: TextAlign.center,
 | |
|                   style: TextStyle(
 | |
|                     color: Theme.of(context).appBarTheme.foregroundColor!,
 | |
|                   ),
 | |
|                 ),
 | |
|               ),
 | |
|               Tab(
 | |
|                 child: Text(
 | |
|                   'stellarProgram'.tr(),
 | |
|                   textAlign: TextAlign.center,
 | |
|                   style: TextStyle(
 | |
|                     color: Theme.of(context).appBarTheme.foregroundColor!,
 | |
|                   ),
 | |
|                 ),
 | |
|               ),
 | |
|             ],
 | |
|           ),
 | |
|         ),
 | |
|         body: TabBarView(
 | |
|           children: [
 | |
|             _buildLevelingTab(context, ref, user.value!),
 | |
|             const SocialCreditsTab(),
 | |
|             const StellarProgramTab(),
 | |
|           ],
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _buildLevelingTab(
 | |
|     BuildContext context,
 | |
|     WidgetRef ref,
 | |
|     SnAccount user,
 | |
|   ) {
 | |
|     final currentLevel = user.profile.level;
 | |
|     final currentExp = user.profile.experience;
 | |
|     final progress = user.profile.levelingProgress;
 | |
| 
 | |
|     return Center(
 | |
|       child: Container(
 | |
|         padding: const EdgeInsets.symmetric(horizontal: 20),
 | |
|         child: CustomScrollView(
 | |
|           slivers: [
 | |
|             const SliverGap(20),
 | |
| 
 | |
|             // Current Progress Card
 | |
|             SliverToBoxAdapter(
 | |
|               child: LevelingProgressCard(
 | |
|                 level: currentLevel,
 | |
|                 experience: currentExp,
 | |
|                 progress: progress,
 | |
|               ),
 | |
|             ),
 | |
|             const SliverGap(24),
 | |
| 
 | |
|             // Level Stairs Graph
 | |
|             SliverToBoxAdapter(
 | |
|               child: Text(
 | |
|                 'levelProgress'.tr(),
 | |
|                 style: Theme.of(context).textTheme.headlineSmall?.copyWith(
 | |
|                   fontWeight: FontWeight.bold,
 | |
|                 ),
 | |
|               ),
 | |
|             ),
 | |
|             const SliverGap(16),
 | |
| 
 | |
|             SliverToBoxAdapter(
 | |
|               child: Card(
 | |
|                 margin: EdgeInsets.zero,
 | |
|                 child: Column(
 | |
|                   crossAxisAlignment: CrossAxisAlignment.stretch,
 | |
|                   children: [
 | |
|                     Text(
 | |
|                       '${'levelingProgressLevel'.tr(args: [currentLevel.toString()])} / 120',
 | |
|                       textAlign: TextAlign.start,
 | |
|                       style: Theme.of(context).textTheme.bodySmall,
 | |
|                     ),
 | |
|                     const Gap(8),
 | |
|                     LinearProgressIndicator(
 | |
|                       value: currentLevel / 120,
 | |
|                       minHeight: 10,
 | |
|                       stopIndicatorRadius: 0,
 | |
|                       trackGap: 0,
 | |
|                       color: Theme.of(context).colorScheme.primary,
 | |
|                       backgroundColor:
 | |
|                           Theme.of(context).colorScheme.surfaceContainerHigh,
 | |
|                       borderRadius: BorderRadius.circular(32),
 | |
|                     ),
 | |
|                   ],
 | |
|                 ).padding(horizontal: 16, top: 16, bottom: 12),
 | |
|               ),
 | |
|             ),
 | |
|             const SliverGap(16),
 | |
|             // Leveling History
 | |
|             SliverToBoxAdapter(
 | |
|               child: Text(
 | |
|                 'levelingHistory'.tr(),
 | |
|                 style: Theme.of(context).textTheme.headlineSmall?.copyWith(
 | |
|                   fontWeight: FontWeight.bold,
 | |
|                 ),
 | |
|               ),
 | |
|             ),
 | |
|             const SliverGap(8),
 | |
|             PagingHelperSliverView(
 | |
|               provider: levelingHistoryNotifierProvider,
 | |
|               futureRefreshable: levelingHistoryNotifierProvider.future,
 | |
|               notifierRefreshable: levelingHistoryNotifierProvider.notifier,
 | |
|               contentBuilder:
 | |
|                   (data, widgetCount, endItemView) => SliverList.builder(
 | |
|                     itemCount: widgetCount,
 | |
|                     itemBuilder: (context, index) {
 | |
|                       if (index == widgetCount - 1) {
 | |
|                         return endItemView;
 | |
|                       }
 | |
|                       final record = data.items[index];
 | |
|                       return ListTile(
 | |
|                         title: Column(
 | |
|                           mainAxisSize: MainAxisSize.min,
 | |
|                           crossAxisAlignment: CrossAxisAlignment.stretch,
 | |
|                           children: [
 | |
|                             Text(record.reason),
 | |
|                             Row(
 | |
|                               spacing: 4,
 | |
|                               children: [
 | |
|                                 Text(
 | |
|                                   record.createdAt.formatRelative(context),
 | |
|                                 ).fontSize(13),
 | |
|                                 Text('·').fontSize(13).bold(),
 | |
|                                 Text(
 | |
|                                   record.createdAt.formatSystem(),
 | |
|                                 ).fontSize(13),
 | |
|                               ],
 | |
|                             ).opacity(0.8),
 | |
|                           ],
 | |
|                         ),
 | |
|                         subtitle: Row(
 | |
|                           spacing: 8,
 | |
|                           children: [
 | |
|                             Text(
 | |
|                               '${record.delta > 0 ? '+' : ''}${record.delta} EXP',
 | |
|                             ),
 | |
|                             if (record.bonusMultiplier != 1.0)
 | |
|                               Text('x${record.bonusMultiplier}'),
 | |
|                           ],
 | |
|                         ),
 | |
|                         minTileHeight: 56,
 | |
|                         contentPadding: EdgeInsets.symmetric(horizontal: 4),
 | |
|                       );
 | |
|                     },
 | |
|                   ),
 | |
|             ),
 | |
| 
 | |
|             SliverGap(20),
 | |
|           ],
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class LevelStairsPainter extends CustomPainter {
 | |
|   final int currentLevel;
 | |
|   final int totalLevels;
 | |
|   final Color primaryColor;
 | |
|   final Color surfaceColor;
 | |
|   final Color onSurfaceColor;
 | |
|   final double stairHeight;
 | |
|   final double stairWidth;
 | |
| 
 | |
|   LevelStairsPainter({
 | |
|     required this.currentLevel,
 | |
|     required this.totalLevels,
 | |
|     required this.primaryColor,
 | |
|     required this.surfaceColor,
 | |
|     required this.onSurfaceColor,
 | |
|     required this.stairHeight,
 | |
|     required this.stairWidth,
 | |
|   });
 | |
| 
 | |
|   @override
 | |
|   void paint(Canvas canvas, Size size) {
 | |
|     final paint =
 | |
|         Paint()
 | |
|           ..color = surfaceColor.withOpacity(0.2)
 | |
|           ..strokeWidth = 1.5
 | |
|           ..style = PaintingStyle.stroke;
 | |
| 
 | |
|     // Draw connecting lines between stairs
 | |
|     for (int i = 0; i < totalLevels - 1; i++) {
 | |
|       final startX = 20.0 + (i * (stairWidth + 8)) + stairWidth;
 | |
|       final startHeight =
 | |
|           40.0 + (i * 15.0); // Progressive height for current stair
 | |
|       final startY = size.height - (20.0 + startHeight);
 | |
| 
 | |
|       final endX = 20.0 + ((i + 1) * (stairWidth + 8));
 | |
|       final endHeight =
 | |
|           40.0 + ((i + 1) * 15.0); // Progressive height for next stair
 | |
|       final endY = size.height - (20.0 + endHeight);
 | |
| 
 | |
|       canvas.drawLine(Offset(startX, startY), Offset(endX, endY), paint);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
 | |
| }
 |