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/main.dart' show routeObserver; import 'package:yumi/shared/business_logic/models/res/sc_task_list_res.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; import 'package:yumi/shared/tools/sc_entry_popup_coordinator.dart'; class TaskPage extends StatefulWidget { const TaskPage({super.key}); @override State createState() => _TaskPageState(); } class _TaskPageState extends State with WidgetsBindingObserver, RouteAware { 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'; static const String _taskDefaultAsset = 'sc_images/index/sc_icon_task.png'; static const Duration _taskStatusCacheRefreshDelay = Duration(seconds: 65); final SCAccountRepository _repository = SCAccountRepository(); final Set _claimedTaskIds = {}; final Set _claimingTaskIds = {}; List _tasks = []; bool _loading = true; bool _loadingTasks = false; String? _toastMessage; Timer? _toastTimer; Timer? _cacheRefreshTimer; PageRoute? _routeObserverRoute; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _loadTasks(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); routeObserver.unsubscribe(this); _toastTimer?.cancel(); _cacheRefreshTimer?.cancel(); super.dispose(); } @override void didChangeDependencies() { super.didChangeDependencies(); final route = ModalRoute.of(context); if (route is PageRoute && _routeObserverRoute != route) { if (_routeObserverRoute != null) { routeObserver.unsubscribe(this); } _routeObserverRoute = route; routeObserver.subscribe(this, route); } } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { debugPrint('[TaskCenter][Page] refresh on app resumed'); unawaited(_loadTasks()); _scheduleTaskStatusCacheRefresh('app resumed'); } } @override void didPopNext() { debugPrint('[TaskCenter][Page] refresh on route return'); unawaited(_loadTasks()); _scheduleTaskStatusCacheRefresh('route return'); } Future _loadTasks() async { if (_loadingTasks) { debugPrint('[TaskCenter][Page] load skipped: request in flight'); return; } _loadingTasks = true; debugPrint('[TaskCenter][Page] load start'); try { final tasks = await _repository.tasks(); if (!mounted) return; final sortedTasks = tasks..sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0)); final dailyCount = sortedTasks.where((task) => (task.taskType ?? 0).toInt() == 0).length; final exclusiveCount = sortedTasks.length - dailyCount; debugPrint( '[TaskCenter][Page] load success total=${sortedTasks.length} ' 'daily=$dailyCount exclusive=$exclusiveCount ' 'sample=${_taskDebugSample(sortedTasks)}', ); setState(() { _tasks = sortedTasks; _loading = false; }); } catch (error) { debugPrint('[TaskCenter][Page] load failed error=$error'); if (!mounted) return; setState(() { _tasks = []; _loading = false; }); } finally { _loadingTasks = false; } } Future _handleManualRefresh() async { await _loadTasks(); _scheduleTaskStatusCacheRefresh('manual refresh'); } void _scheduleTaskStatusCacheRefresh(String reason) { _cacheRefreshTimer?.cancel(); debugPrint( '[TaskCenter][Page] schedule cache refresh reason=$reason ' 'delayMs=${_taskStatusCacheRefreshDelay.inMilliseconds}', ); _cacheRefreshTimer = Timer(_taskStatusCacheRefreshDelay, () { _cacheRefreshTimer = null; if (!mounted) return; debugPrint( '[TaskCenter][Page] refresh after cache window reason=$reason', ); unawaited(_loadTasks()); }); } @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: _handleManualRefresh, 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, ) { debugPrint( '[TaskCenter][Page] build sections raw=${_tasks.length} ' 'daily=${dailyTasks.length} exclusive=${newcomerTasks.length}', ); 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, ), ); } if (sections.isEmpty) { sections.add( Padding( padding: EdgeInsets.only(top: 120.w), child: Text( l10n.noPromptsToday, style: TextStyle( color: Colors.white.withValues(alpha: 0.72), fontSize: 14.sp, fontWeight: FontWeight.w500, ), ), ), ); } 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(); return source.map(_mapTask).toList(); } _TaskUiItem _mapTask(SCTaskListRes task) { final id = task.taskId ?? ''; final currentValue = _parseNumber(task.completedValue) ?? 0; final parsedTargetValue = _parseNumber(task.targetValue) ?? _parseNumber(task.conditionValue); final targetValue = parsedTargetValue ?? 0; final hasProgress = parsedTargetValue != null && parsedTargetValue > 0; final claimed = _claimedTaskIds.contains(id) || (task.isRewardCollected ?? 0) == 1; final completedByValue = hasProgress && 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: _taskProgressText( task, currentValue: currentValue, targetValue: targetValue, hasProgress: hasProgress, ), requirementText: _taskRequirementText( task, targetValue: targetValue, hasProgress: hasProgress, ), rewardText: _formatPlainNumber(task.quantity ?? 1100), status: status, iconAsset: _iconForTask(task), iconUrl: _nonEmpty(task.taskIcon) ?? _nonEmpty(task.cover), jumpPage: _nonEmpty(task.jumpPage), jumpType: _nonEmpty(task.jumpType), conditionType: _nonEmpty(task.conditionType), roomId: _nonEmpty(task.roomId), roomTask: _isRoomCompletionTask(task), gameTask: _isGameCompletionTask(task), fromApi: id.isNotEmpty, ); } Future _handleTaskAction(_TaskUiItem task) async { debugPrint( '[TaskCenter][Page] action id=${task.id} status=${task.status.name} ' 'fromApi=${task.fromApi} conditionType=${task.conditionType} ' 'jumpType=${task.jumpType} jumpPage=${task.jumpPage} ' 'roomId=${task.roomId} roomTask=${task.roomTask} ' 'gameTask=${task.gameTask}', ); if (task.status == _TaskActionStatus.claimed) return; if (task.status == _TaskActionStatus.go) { final opened = await _openJumpPage(task); if (opened && mounted) { debugPrint('[TaskCenter][Page] refresh after jump return'); unawaited(_loadTasks()); _scheduleTaskStatusCacheRefresh('jump return'); } return; } if (_claimingTaskIds.contains(task.id)) return; setState(() => _claimingTaskIds.add(task.id)); var success = true; if (task.fromApi && task.id.isNotEmpty) { 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); } } Future _openJumpPage(_TaskUiItem task) async { var target = _taskJumpUrl(task); if (target.isEmpty) return false; var uri = Uri.tryParse(target); var path = uri?.path ?? ''; var roomId = uri?.queryParameters['roomId']?.trim() ?? ''; if ((path == '/room' || path == '/game-center') && roomId.isEmpty) { debugPrint( '[TaskCenter][Page] room jump missing roomId, fallback first room ' 'target=$target taskId=${task.id}', ); final fallbackTarget = await _firstRoomJumpUrl(target); if (!mounted) return false; if (fallbackTarget.isEmpty) { _showToast(SCAppLocalizations.of(context)!.enterRoomFailedRetry); return false; } target = fallbackTarget; uri = Uri.tryParse(target); path = uri?.path ?? ''; roomId = uri?.queryParameters['roomId']?.trim() ?? ''; } final jumpType = _taskJumpType(task, target); debugPrint( '[TaskCenter][Page] open jump type=$jumpType url=$target ' 'taskId=${task.id}', ); await SCEntryPopupCoordinator.openConfiguredJump( context, jumpType: jumpType, jumpUrl: target, ); return true; } Future _firstRoomJumpUrl(String target) async { try { final rooms = await SCChatRoomRepository().discovery(allRegion: true); for (final room in rooms) { final roomId = room.id?.trim() ?? ''; if (roomId.isEmpty) continue; final resolvedTarget = _withRoomId(target, roomId); debugPrint( '[TaskCenter][Page] fallback first room roomId=$roomId ' 'target=$resolvedTarget', ); return resolvedTarget; } debugPrint('[TaskCenter][Page] fallback first room empty list'); } catch (error) { debugPrint('[TaskCenter][Page] fallback first room failed error=$error'); } return ''; } String _withRoomId(String target, String roomId) { final uri = Uri.tryParse(target); if (uri == null) { return '/room?roomId=${Uri.encodeComponent(roomId)}'; } final params = Map.from(uri.queryParameters); params['roomId'] = roomId; return uri.replace(queryParameters: params).toString(); } String _taskJumpUrl(_TaskUiItem task) { final target = task.jumpPage?.trim() ?? ''; final roomId = task.roomId?.trim() ?? ''; if (target.isEmpty) { return _defaultTaskJumpUrl(task, roomId); } if (roomId.isEmpty) return target; final uri = Uri.tryParse(target); if (uri == null) return target; if (uri.path != '/room' && uri.path != '/game-center') return target; return _withRoomId(target, roomId); } String _defaultTaskJumpUrl(_TaskUiItem task, String roomId) { if (_isGameJumpType(task.jumpType) || task.gameTask) { return _routeWithOptionalRoomId('/game-center', roomId); } if (_isRoomJumpType(task.jumpType) || task.roomTask) { return _routeWithOptionalRoomId('/room', roomId); } if (_isRechargeTask(task)) { return '/recharge'; } return roomId.isEmpty ? '' : _routeWithOptionalRoomId('/room', roomId); } String _routeWithOptionalRoomId(String route, String roomId) { if (roomId.isEmpty) return route; return '$route?roomId=${Uri.encodeComponent(roomId)}'; } String _taskJumpType(_TaskUiItem task, String target) { final uri = Uri.tryParse(target); final path = uri?.path ?? ''; final jumpType = task.jumpType?.trim(); if (jumpType != null && jumpType.isNotEmpty) { if (_isGameJumpType(jumpType)) return 'GAME'; if (_isRoomJumpType(jumpType)) return 'APP_ROUTE'; } if (uri?.hasScheme == true) { return 'H5'; } if (path == '/game-center') { return 'GAME'; } if (path.startsWith('/')) { return 'APP_ROUTE'; } if (jumpType != null && jumpType.isNotEmpty) return jumpType; return 'APP_ROUTE'; } bool _isRoomJumpType(String? jumpType) { final normalized = jumpType?.trim().toUpperCase() ?? ''; return normalized == 'ROOM' || normalized == 'VOICE_ROOM' || normalized == 'VOICECHAT_ROOM' || normalized == 'ENTER_ROOM' || normalized == 'JOIN_ROOM'; } bool _isGameJumpType(String? jumpType) { final normalized = jumpType?.trim().toUpperCase() ?? ''; return normalized == 'GAME' || normalized == 'GAME_CENTER' || normalized == 'GAME_CONSUME_GOLD'; } 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; if (text.contains('mic')) return _taskMicAsset; return _taskDefaultAsset; } String _taskProgressText( SCTaskListRes task, { required num currentValue, required num targetValue, required bool hasProgress, }) { if (!hasProgress) return ''; final l10n = SCAppLocalizations.of(context)!; return '${l10n.taskProgress}: ' '${_formatTaskValue(task, currentValue)}/${_formatTaskValue(task, targetValue)}'; } String _taskRequirementText( SCTaskListRes task, { required num targetValue, required bool hasProgress, }) { final l10n = SCAppLocalizations.of(context)!; if (hasProgress) { return '${l10n.taskRequirement}: ${_formatTaskValue(task, targetValue)}'; } return _nonEmpty(task.taskDesc) ?? ''; } bool _isGameCompletionTask(SCTaskListRes task) { final conditionType = task.conditionType?.trim().toUpperCase() ?? ''; if (conditionType.contains('GAME')) return true; final text = '${task.taskName ?? ''} ${task.taskDesc ?? ''}'.toLowerCase(); return text.contains('game') || text.contains('游戏'); } bool _isRoomCompletionTask(SCTaskListRes task) { final conditionType = task.conditionType?.trim().toUpperCase() ?? ''; if (conditionType.contains('RECHARGE')) return false; if (_isGameCompletionTask(task)) return false; if (conditionType.contains('MIC') || conditionType.contains('VOICE') || conditionType.contains('ROOM') || conditionType.contains('GIFT') || conditionType.contains('CONSUME')) { return true; } final text = '${task.taskName ?? ''} ${task.taskDesc ?? ''}'.toLowerCase(); if (text.contains('recharge') || text.contains('充值')) return false; if (text.contains('game') || text.contains('游戏')) return false; return text.contains('mic') || text.contains('voice') || text.contains('room') || text.contains('gift') || text.contains('consume') || text.contains('spend') || text.contains('上麦') || text.contains('房间') || text.contains('语音') || text.contains('礼物') || text.contains('消费'); } bool _isRechargeTask(_TaskUiItem task) { final text = '${task.conditionType ?? ''} ${task.title} ${task.jumpType ?? ''}' .toLowerCase(); return text.contains('recharge') || text.contains('充值'); } static num? _parseNumber(Object? value) { if (value is num) return value; if (value is String) { final text = value.trim(); if (text.isEmpty) return null; return num.tryParse(text) ?? num.tryParse(text.replaceAll(',', '')); } 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); } String _formatTaskValue(SCTaskListRes task, num value) { if (_isTimeValueTask(task)) return _formatDurationSeconds(value); return _formatPlainNumber(value); } bool _isTimeValueTask(SCTaskListRes task) { final conditionType = task.conditionType?.trim().toUpperCase() ?? ''; if (conditionType.contains('GOLD') || conditionType.contains('CONSUME')) { return false; } if (conditionType.contains('SECOND') || conditionType.contains('DURATION') || RegExp(r'(^|_)TIME($|_)').hasMatch(conditionType)) { return true; } final text = '${task.taskName ?? ''} ${task.taskDesc ?? ''}'.toLowerCase(); return text.contains('second') || text.contains('seconds') || text.contains('minute') || text.contains('minutes') || text.contains('hour') || text.contains('hours') || text.contains(' min') || text.contains('秒') || text.contains('分钟') || text.contains('小时'); } static String _formatDurationSeconds(num seconds) { final absSeconds = seconds.abs(); if (absSeconds >= 3600) { return '${_formatShortDecimal(seconds / 3600)}h'; } if (absSeconds >= 60) { return '${_formatShortDecimal(seconds / 60)}min'; } return '${_formatShortDecimal(seconds)}s'; } static String _formatShortDecimal(num value) { final fixed = value.toStringAsFixed(1); return fixed.endsWith('.0') ? fixed.substring(0, fixed.length - 2) : fixed; } String _taskDebugSample(List tasks) { if (tasks.isEmpty) return 'empty'; return tasks .take(3) .map((task) { return '{id=${task.taskId},type=${task.taskType},' 'status=${task.taskStatus},collected=${task.isRewardCollected},' 'current=${task.completedValue},target=${task.targetValue},' 'quantity=${task.quantity},name=${task.taskName},' 'condition=${task.conditionType},' 'jumpType=${task.jumpType},jumpPage=${task.jumpPage},' 'roomId=${task.roomId}}'; }) .join(' '); } } 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(String 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: 76.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: _TaskIcon(task: task), ), 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), if (task.requirementText.isNotEmpty) ...[ Text( task.requirementText, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.white.withValues(alpha: 0.72), fontSize: 10.sp, fontWeight: FontWeight.w400, height: 1, ), ), SizedBox(height: 3.w), ], if (task.progressText.isNotEmpty) ...[ 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: 5.w), ] else 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 _TaskIcon extends StatelessWidget { const _TaskIcon({required this.task}); final _TaskUiItem task; @override Widget build(BuildContext context) { final iconUrl = task.iconUrl?.trim(); if (iconUrl == null || iconUrl.isEmpty) { return _buildAssetIcon(); } return Image.network( iconUrl, width: 29.4.w, height: 29.4.w, fit: BoxFit.contain, errorBuilder: (_, __, ___) => _buildAssetIcon(), loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return _buildAssetIcon(); }, ); } Widget _buildAssetIcon() { return Image.asset( task.iconAsset, width: 29.4.w, height: 29.4.w, fit: BoxFit.contain, ); } } 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.requirementText, required this.rewardText, required this.status, required this.iconAsset, this.iconUrl, this.jumpPage, this.jumpType, this.conditionType, this.roomId, this.roomTask = false, this.gameTask = false, this.fromApi = false, }); final String id; final String title; final String progressText; final String requirementText; final String rewardText; final _TaskActionStatus status; final String iconAsset; final String? iconUrl; final String? jumpPage; final String? jumpType; final String? conditionType; final String? roomId; final bool roomTask; final bool gameTask; final bool fromApi; _TaskUiItem copyWith({_TaskActionStatus? status}) { return _TaskUiItem( id: id, title: title, progressText: progressText, requirementText: requirementText, rewardText: rewardText, status: status ?? this.status, iconAsset: iconAsset, iconUrl: iconUrl, jumpPage: jumpPage, jumpType: jumpType, conditionType: conditionType, roomId: roomId, roomTask: roomTask, gameTask: gameTask, fromApi: fromApi, ); } } enum _TaskActionStatus { go, claim, claimed }