import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/modules/index/main_route.dart'; import 'package:yumi/shared/business_logic/models/res/sc_task_list_res.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; class TaskPage extends StatefulWidget { const TaskPage({super.key}); @override State createState() => _TaskPageState(); } class _TaskPageState extends State { static const String _taskMicAsset = 'sc_images/index/sc_icon_task_mic.png'; static const String _taskGameAsset = 'sc_images/index/sc_icon_task_game.png'; static const String _taskGiftAsset = 'sc_images/index/sc_icon_task_gift.png'; final SCAccountRepository _repository = SCAccountRepository(); final Set _claimedTaskIds = {}; final Set _claimingTaskIds = {}; List _tasks = []; bool _loading = true; String? _toastMessage; Timer? _toastTimer; @override void initState() { super.initState(); _loadTasks(); } @override void dispose() { _toastTimer?.cancel(); super.dispose(); } Future _loadTasks() async { try { final tasks = await _repository.tasks(); if (!mounted) return; setState(() { _tasks = tasks ..sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0)); _loading = false; }); } catch (_) { if (!mounted) return; setState(() { _tasks = []; _loading = false; }); } } @override Widget build(BuildContext context) { final dailyTasks = _sectionTasks(isNewcomer: false); final newcomerTasks = _sectionTasks(isNewcomer: true); return Scaffold( backgroundColor: const Color(0xFF00221D), body: SafeArea( bottom: false, child: Stack( children: [ Column( children: [ _buildTopBar(), Expanded( child: RefreshIndicator( color: const Color(0xFF16DBB2), backgroundColor: const Color(0xFF06342A), onRefresh: _loadTasks, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics( parent: BouncingScrollPhysics(), ), padding: EdgeInsets.fromLTRB(10.w, 18.w, 10.w, 28.w), child: Column( children: [ if (_loading) Padding( padding: EdgeInsets.only(top: 120.w), child: SizedBox( width: 24.w, height: 24.w, child: const CircularProgressIndicator( strokeWidth: 2, color: Color(0xFF16DBB2), ), ), ) else ..._buildTaskSections(dailyTasks, newcomerTasks), ], ), ), ), ), ], ), if (_toastMessage != null) _buildToast(), ], ), ), ); } Widget _buildTopBar() { return SizedBox( height: 44.w, child: Stack( alignment: Alignment.center, children: [ Align( alignment: AlignmentDirectional.centerStart, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => SCNavigatorUtils.goBack(context), child: SizedBox( width: 44.w, height: 44.w, child: Icon( SCGlobalConfig.lang == 'ar' ? Icons.keyboard_arrow_right : Icons.keyboard_arrow_left, color: Colors.white, size: 28.w, ), ), ), ), Text( SCAppLocalizations.of(context)!.task, style: TextStyle( color: Colors.white, fontSize: 20.sp, fontWeight: FontWeight.w500, ), ), ], ), ); } Widget _buildToast() { return Positioned( left: 50.w, right: 50.w, top: 316.w, child: IgnorePointer( child: Container( height: 56.w, alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xFF063F36), borderRadius: BorderRadius.circular(12.w), border: Border.all(color: const Color(0xFF18DDB5), width: 1.w), ), child: Text( _toastMessage!, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.white, fontSize: 13.sp, fontWeight: FontWeight.w400, ), ), ), ), ); } List _buildTaskSections( List<_TaskUiItem> dailyTasks, List<_TaskUiItem> newcomerTasks, ) { final sections = []; final l10n = SCAppLocalizations.of(context)!; if (dailyTasks.isNotEmpty) { sections.add( _TaskSection( title: l10n.dailyTasks, subtitle: l10n.resetsDailyAtMidnight, tasks: dailyTasks, onAction: _handleTaskAction, isClaiming: _claimingTaskIds.contains, ), ); } if (newcomerTasks.isNotEmpty) { if (sections.isNotEmpty) sections.add(SizedBox(height: 14.w)); sections.add( _TaskSection( title: l10n.exclusiveForNewcomers, subtitle: l10n.limitedToOneTime, tasks: newcomerTasks, onAction: _handleTaskAction, isClaiming: _claimingTaskIds.contains, ), ); } return sections; } List<_TaskUiItem> _sectionTasks({required bool isNewcomer}) { final source = _tasks.where((task) { final taskType = (task.taskType ?? 0).toInt(); return isNewcomer ? taskType != 0 : taskType == 0; }).toList(); final mapped = source.map(_mapTask).toList(); if (_tasks.isNotEmpty) return mapped; final demoTasks = _demoTasks(isNewcomer: isNewcomer); return demoTasks .map( (task) => _claimedTaskIds.contains(task.id) ? task.copyWith(status: _TaskActionStatus.claimed) : task, ) .toList(); } _TaskUiItem _mapTask(SCTaskListRes task) { final id = task.taskId ?? -1; final currentValue = _parseNumber(task.completedValue) ?? 0; final targetValue = _parseNumber(task.targetValue) ?? _parseNumber(task.conditionValue) ?? 1000000; final claimed = _claimedTaskIds.contains(id) || (task.isRewardCollected ?? 0) == 1; final completedByValue = targetValue > 0 && currentValue >= targetValue; final completedByStatus = (task.taskStatus ?? 0) == 1; final status = claimed ? _TaskActionStatus.claimed : (completedByStatus || completedByValue) ? _TaskActionStatus.claim : _TaskActionStatus.go; return _TaskUiItem( id: id, title: _nonEmpty(task.taskName) ?? SCAppLocalizations.of(context)!.useMicrophoneForOneMin, progressText: '${_formatPlainNumber(currentValue)}/${_formatPlainNumber(targetValue)}', rewardText: _formatCompactNumber(task.quantity ?? 1100), status: status, iconAsset: _iconForTask(task), jumpPage: _nonEmpty(task.jumpPage), fromApi: task.taskId != null, ); } Future _handleTaskAction(_TaskUiItem task) async { if (task.status == _TaskActionStatus.claimed) return; if (task.status == _TaskActionStatus.go) { _openJumpPage(task.jumpPage); return; } if (_claimingTaskIds.contains(task.id)) return; setState(() => _claimingTaskIds.add(task.id)); var success = true; if (task.fromApi && task.id >= 0) { try { success = await _repository.taskReward(task.id); } catch (_) { success = false; } } if (!mounted) return; setState(() { _claimingTaskIds.remove(task.id); if (success) _claimedTaskIds.add(task.id); }); if (success) { _showToast(SCAppLocalizations.of(context)!.rewardClaimedSuccessfully); unawaited(_loadTasks()); } else { _showToast(SCAppLocalizations.of(context)!.rewardClaimFailed); } } void _openJumpPage(String? jumpPage) { final target = jumpPage?.trim() ?? ''; if (target.isEmpty) return; if (target.startsWith('http://') || target.startsWith('https://')) { SCNavigatorUtils.push( context, '${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(target)}&showTitle=false', ); return; } if (target.startsWith('/')) { SCNavigatorUtils.push(context, target); } } void _showToast(String message) { _toastTimer?.cancel(); setState(() => _toastMessage = message); _toastTimer = Timer(const Duration(milliseconds: 1600), () { if (mounted) setState(() => _toastMessage = null); }); } String _iconForTask(SCTaskListRes task) { final text = '${task.conditionType ?? ''} ${task.taskName ?? ''} ${task.taskDesc ?? ''}' .toLowerCase(); if (text.contains('gift')) return _taskGiftAsset; if (text.contains('game')) return _taskGameAsset; return _taskMicAsset; } static num? _parseNumber(Object? value) { if (value is num) return value; if (value is String) return num.tryParse(value); return null; } static String? _nonEmpty(String? value) { final text = value?.trim(); return text == null || text.isEmpty ? null : text; } static String _formatPlainNumber(num value) { if (value % 1 == 0) return value.toInt().toString(); return value.toStringAsFixed(1); } static String _formatCompactNumber(num value) { final absValue = value.abs(); if (absValue >= 1000000) { return '${_trimTrailingZero(value / 1000000)}m'; } if (absValue >= 1000) { return '${_trimTrailingZero(value / 1000)}k'; } return _formatPlainNumber(value); } static String _trimTrailingZero(num value) { final text = value.toStringAsFixed(1); return text.endsWith('.0') ? text.substring(0, text.length - 2) : text; } List<_TaskUiItem> _demoTasks({required bool isNewcomer}) { final title = SCAppLocalizations.of(context)!.useMicrophoneForOneMin; final baseId = isNewcomer ? -200 : -100; return [ _TaskUiItem( id: baseId - 1, title: title, progressText: '0/1000000', rewardText: '1.1k', status: _TaskActionStatus.go, iconAsset: _taskMicAsset, ), _TaskUiItem( id: baseId - 2, title: title, progressText: '0/1000000', rewardText: '1.1k', status: _TaskActionStatus.claim, iconAsset: _taskGameAsset, ), _TaskUiItem( id: baseId - 3, title: title, progressText: '0/1000000', rewardText: '1.1k', status: _TaskActionStatus.claimed, iconAsset: _taskGiftAsset, ), ]; } } class _TaskSection extends StatelessWidget { const _TaskSection({ required this.title, required this.subtitle, required this.tasks, required this.onAction, required this.isClaiming, }); final String title; final String subtitle; final List<_TaskUiItem> tasks; final ValueChanged<_TaskUiItem> onAction; final bool Function(num id) isClaiming; @override Widget build(BuildContext context) { return Container( width: double.infinity, decoration: BoxDecoration( color: const Color(0xFF052E25), borderRadius: BorderRadius.circular(7.w), border: Border.all(color: const Color(0xFFB4FCCE), width: 1.w), ), child: Column( children: [ Padding( padding: EdgeInsets.fromLTRB(12.w, 24.w, 12.w, 12.w), child: Row( children: [ Expanded(child: _GradientTaskTitle(title)), SizedBox(width: 8.w), Text( subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.white, fontSize: 12.sp, fontWeight: FontWeight.w400, height: 1, ), ), ], ), ), for (int i = 0; i < tasks.length; i++) ...[ _TaskRow( task: tasks[i], onAction: onAction, claiming: isClaiming(tasks[i].id), ), if (i != tasks.length - 1) Padding( padding: EdgeInsetsDirectional.only(start: 62.w), child: Divider( height: 1.w, thickness: 0.5.w, color: const Color(0xFF295346).withValues(alpha: 0.8), ), ), ], ], ), ); } } class _GradientTaskTitle extends StatelessWidget { const _GradientTaskTitle(this.text); final String text; @override Widget build(BuildContext context) { final style = TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w900, height: 1, ); return LayoutBuilder( builder: (context, constraints) { return SizedBox( width: constraints.maxWidth, height: 23.w, child: Stack( clipBehavior: Clip.none, children: [ PositionedDirectional( top: 3.w, start: 0, end: 0, child: Text( text, maxLines: 1, overflow: TextOverflow.ellipsis, style: style.copyWith(color: const Color(0xFF157D10)), ), ), PositionedDirectional( start: 0, end: 0, child: Text( text, maxLines: 1, overflow: TextOverflow.ellipsis, style: style.copyWith( foreground: Paint() ..style = PaintingStyle.stroke ..strokeWidth = 1.w ..color = const Color(0xFF157D10), ), ), ), PositionedDirectional( start: 0, end: 0, child: ShaderMask( blendMode: BlendMode.srcIn, shaderCallback: (bounds) => const LinearGradient( begin: Alignment.centerRight, end: Alignment.centerLeft, colors: [ Color(0xFFFFFFFF), Color(0xFFBEFAE8), Color(0xFFFFFFFF), ], stops: [0.224, 0.4907, 0.7224], ).createShader(bounds), child: Text( text, maxLines: 1, overflow: TextOverflow.ellipsis, style: style.copyWith(color: Colors.white), ), ), ), ], ), ); }, ); } } class _TaskRow extends StatelessWidget { const _TaskRow({ required this.task, required this.onAction, required this.claiming, }); final _TaskUiItem task; final ValueChanged<_TaskUiItem> onAction; final bool claiming; @override Widget build(BuildContext context) { return SizedBox( height: 65.w, child: Row( children: [ SizedBox(width: 12.w), Container( width: 42.w, height: 42.w, decoration: const BoxDecoration( color: Color(0x30BEF9E8), shape: BoxShape.circle, ), alignment: Alignment.center, child: Image.asset( task.iconAsset, width: 29.4.w, height: 29.4.w, fit: BoxFit.contain, ), ), SizedBox(width: 20.w), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( task.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.white, fontSize: 12.sp, fontWeight: FontWeight.w400, height: 1.15, ), ), SizedBox(height: 3.w), Text( task.progressText, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.white.withValues(alpha: 0.62), fontSize: 10.sp, fontWeight: FontWeight.w400, height: 1, ), ), SizedBox(height: 6.w), Row( children: [ Image.asset( 'sc_images/general/sc_icon_jb.png', width: 12.w, height: 12.w, ), SizedBox(width: 5.w), Text( task.rewardText, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: const Color(0xFFFFD155), fontSize: 12.sp, fontWeight: FontWeight.w400, height: 1, ), ), ], ), ], ), ), SizedBox(width: 10.w), _TaskActionButton( status: task.status, claiming: claiming, onTap: () => onAction(task), ), SizedBox(width: 12.w), ], ), ); } } class _TaskActionButton extends StatelessWidget { const _TaskActionButton({ required this.status, required this.claiming, required this.onTap, }); final _TaskActionStatus status; final bool claiming; final VoidCallback onTap; @override Widget build(BuildContext context) { final config = _TaskButtonConfig.fromStatus( status, SCAppLocalizations.of(context)!, ); return GestureDetector( behavior: HitTestBehavior.opaque, onTap: claiming ? null : onTap, child: Container( width: config.width.w, height: 20.w, alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10.w), border: config.borderColor == null ? null : Border.all(color: config.borderColor!, width: 1.w), gradient: config.gradient, color: config.gradient == null ? config.backgroundColor : null, ), child: claiming ? SizedBox( width: 10.w, height: 10.w, child: CircularProgressIndicator( strokeWidth: 1.5.w, color: config.textColor, ), ) : Text( config.label, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: config.textColor, fontSize: 13.sp, fontWeight: FontWeight.w400, height: 1, ), ), ), ); } } class _TaskButtonConfig { const _TaskButtonConfig({ required this.label, required this.textColor, required this.width, this.backgroundColor, this.borderColor, this.gradient, }); final String label; final Color textColor; final double width; final Color? backgroundColor; final Color? borderColor; final Gradient? gradient; factory _TaskButtonConfig.fromStatus( _TaskActionStatus status, SCAppLocalizations l10n, ) { switch (status) { case _TaskActionStatus.claim: return _TaskButtonConfig( label: l10n.claim, textColor: Color(0xFF2B1500), width: 53, gradient: const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFFFFD981), Color(0xFFFF9E2D)], ), ); case _TaskActionStatus.claimed: return _TaskButtonConfig( label: l10n.claimed, textColor: Colors.white, width: 58, backgroundColor: const Color(0xFF062E28), borderColor: Colors.white.withValues(alpha: 0.56), ); case _TaskActionStatus.go: return _TaskButtonConfig( label: l10n.go, textColor: Colors.white, width: 53, backgroundColor: const Color(0xFF062E28), borderColor: const Color(0xFF98E4C2), ); } } } class _TaskUiItem { const _TaskUiItem({ required this.id, required this.title, required this.progressText, required this.rewardText, required this.status, required this.iconAsset, this.jumpPage, this.fromApi = false, }); final num id; final String title; final String progressText; final String rewardText; final _TaskActionStatus status; final String iconAsset; final String? jumpPage; final bool fromApi; _TaskUiItem copyWith({_TaskActionStatus? status}) { return _TaskUiItem( id: id, title: title, progressText: progressText, rewardText: rewardText, status: status ?? this.status, iconAsset: iconAsset, jumpPage: jumpPage, fromApi: fromApi, ); } } enum _TaskActionStatus { go, claim, claimed }