import 'package:flutter/material.dart'; import '../../core/api/api_config.dart'; import '../../core/api/services/user_api.dart'; import '../../core/auth/auth_service.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_spacing.dart'; import '../../core/theme/app_typography.dart'; import '../../core/user/user_state.dart'; import '../../shared/widgets/top_nav_bar.dart'; import 'models/credits_record_item.dart'; class PointsHistoryScreen extends StatefulWidget { const PointsHistoryScreen({super.key}); @override State createState() => _PointsHistoryScreenState(); } class _PointsHistoryScreenState extends State { static const int _pageSize = 20; final ScrollController _scrollController = ScrollController(); final List _records = []; bool _loading = true; bool _loadingMore = false; bool _hasNext = true; String? _error; int _currentPage = 1; @override void initState() { super.initState(); _scrollController.addListener(_onScroll); _loadRecords(refresh: true); } @override void dispose() { _scrollController.dispose(); super.dispose(); } void _onScroll() { if (!_scrollController.hasClients || _loadingMore || !_hasNext || _loading) { return; } final pos = _scrollController.position; if (pos.pixels >= pos.maxScrollExtent - 200) { _loadRecords(refresh: false); } } Future _loadRecords({required bool refresh}) async { if (refresh) { setState(() { _loading = true; _error = null; _currentPage = 1; _hasNext = true; }); } else { if (_loadingMore || !_hasNext) return; setState(() => _loadingMore = true); } try { await AuthService.loginComplete; final page = refresh ? 1 : _currentPage; final res = await UserApi.getCreditsPage( sentinel: ApiConfig.appId, trophy: page.toString(), heatmap: _pageSize.toString(), ); if (!mounted) return; if (res.isSuccess && res.data is Map) { final data = res.data as Map; final intensify = data['intensify'] as List? ?? []; final list = intensify .whereType>() .map(CreditsRecordItem.fromJson) .toList(); final pages = (data['coordinate'] as num?)?.toInt() ?? page; final current = (data['empower'] as num?)?.toInt() ?? page; setState(() { if (refresh) { _records ..clear() ..addAll(list); } else { _records.addAll(list); } _currentPage = current + 1; _hasNext = current < pages; _loading = false; _loadingMore = false; }); } else { setState(() { if (refresh) _records.clear(); _loading = false; _loadingMore = false; _error = 'Unable to load points history right now. Please try again.'; }); } } catch (_) { if (!mounted) return; setState(() { if (refresh) _records.clear(); _loading = false; _loadingMore = false; _error = 'Something went wrong while loading points history. Please try again.'; }); } } @override Widget build(BuildContext context) { final points = UserCreditsData.of(context)?.creditsDisplay ?? '--'; return Scaffold( backgroundColor: AppColors.background, appBar: const PreferredSize( preferredSize: Size.fromHeight(56), child: TopNavBar( title: 'Points History', showBackButton: true, ), ), body: _loading ? const Center(child: CircularProgressIndicator()) : _error != null ? Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xxl), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( _error!, textAlign: TextAlign.center, style: AppTypography.bodyRegular.copyWith( color: AppColors.textSecondary, ), ), const SizedBox(height: AppSpacing.lg), TextButton( onPressed: () => _loadRecords(refresh: true), child: const Text('Retry'), ), ], ), ), ) : RefreshIndicator( onRefresh: () => _loadRecords(refresh: true), child: ListView( controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB( AppSpacing.screenPadding, AppSpacing.xxl, AppSpacing.screenPadding, AppSpacing.screenPaddingLarge, ), children: [ _CurrentPointsCard(points: points), const SizedBox(height: AppSpacing.xl), if (_records.isEmpty) Padding( padding: const EdgeInsets.symmetric( vertical: AppSpacing.screenPaddingLarge, ), child: Center( child: Text( 'No history yet', style: AppTypography.bodyRegular.copyWith( color: AppColors.textSecondary, ), ), ), ) else ..._records.map( (item) => Padding( padding: const EdgeInsets.only(bottom: AppSpacing.md), child: _HistoryRow(item: item), ), ), if (_loadingMore) const Padding( padding: EdgeInsets.symmetric(vertical: AppSpacing.md), child: Center( child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ), ), ), ], ), ), ); } } class _CurrentPointsCard extends StatelessWidget { const _CurrentPointsCard({required this.points}); final String points; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.xl, vertical: AppSpacing.xl, ), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Current Points', style: AppTypography.caption.copyWith(color: AppColors.textSecondary), ), const SizedBox(height: AppSpacing.sm), Text( points, style: AppTypography.bodyLarge.copyWith( color: AppColors.textPrimary, fontSize: 34, fontWeight: FontWeight.w700, ), ), ], ), ); } } class _HistoryRow extends StatelessWidget { const _HistoryRow({required this.item}); final CreditsRecordItem item; @override Widget build(BuildContext context) { final isPositive = item.isIncrease; return Container( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.xl, vertical: AppSpacing.lg, ), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.border), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _formatTime(item.createTime), style: AppTypography.caption.copyWith(color: AppColors.textSecondary), ), Text( item.deltaText, style: AppTypography.bodyMedium.copyWith( color: isPositive ? const Color(0xFF16A34A) : const Color(0xFFDC2626), fontWeight: FontWeight.w700, ), ), ], ), ); } } String _formatTime(int raw) { if (raw <= 0) return '--'; final dt = raw > 1000000000000 ? DateTime.fromMillisecondsSinceEpoch(raw) : DateTime.fromMillisecondsSinceEpoch(raw * 1000); String two(int n) => n.toString().padLeft(2, '0'); return '${dt.year}-${two(dt.month)}-${two(dt.day)} ${two(dt.hour)}:${two(dt.minute)}'; }