yumi-flutter/lib/modules/user/task/task_page.dart
2026-05-09 18:31:48 +08:00

1134 lines
34 KiB
Dart

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,
this.showBackButton = true,
this.onTaskStatusChanged,
});
final bool showBackButton;
final VoidCallback? onTaskStatusChanged;
@override
State<TaskPage> createState() => _TaskPageState();
}
class _TaskPageState extends State<TaskPage>
with WidgetsBindingObserver, RouteAware {
static const String _taskGiftAsset = 'sc_images/index/sc_icon_task_gift.png';
static const Duration _taskStatusCacheRefreshDelay = Duration(seconds: 65);
final SCAccountRepository _repository = SCAccountRepository();
final Set<String> _claimedTaskIds = {};
final Set<String> _claimingTaskIds = {};
List<SCTaskListRes> _tasks = [];
bool _loading = true;
bool _loadingTasks = false;
String? _toastMessage;
Timer? _toastTimer;
Timer? _cacheRefreshTimer;
PageRoute<dynamic>? _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<dynamic> && _routeObserverRoute != route) {
if (_routeObserverRoute != null) {
routeObserver.unsubscribe(this);
}
_routeObserverRoute = route;
routeObserver.subscribe(this, route);
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
unawaited(_loadTasks());
_scheduleTaskStatusCacheRefresh('app resumed');
}
}
@override
void didPopNext() {
unawaited(_loadTasks());
_scheduleTaskStatusCacheRefresh('route return');
}
Future<void> _loadTasks() async {
if (_loadingTasks) {
return;
}
_loadingTasks = true;
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;
setState(() {
_tasks = sortedTasks;
_loading = false;
});
widget.onTaskStatusChanged?.call();
} catch (error) {
if (!mounted) return;
setState(() {
_tasks = [];
_loading = false;
});
} finally {
_loadingTasks = false;
}
}
Future<void> _handleManualRefresh() async {
await _loadTasks();
_scheduleTaskStatusCacheRefresh('manual refresh');
}
void _scheduleTaskStatusCacheRefresh(String reason) {
_cacheRefreshTimer?.cancel();
_cacheRefreshTimer = Timer(_taskStatusCacheRefreshDelay, () {
_cacheRefreshTimer = null;
if (!mounted) return;
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:
widget.showBackButton
? 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,
),
),
)
: SizedBox(width: 44.w, height: 44.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<Widget> _buildTaskSections(
List<_TaskUiItem> dailyTasks,
List<_TaskUiItem> newcomerTasks,
) {
final sections = <Widget>[];
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<void> _handleTaskAction(_TaskUiItem task) async {
if (task.status == _TaskActionStatus.claimed) return;
if (task.status == _TaskActionStatus.go) {
final opened = await _openJumpPage(task);
if (opened && mounted) {
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<bool> _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) {
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);
await SCEntryPopupCoordinator.openConfiguredJump(
context,
jumpType: jumpType,
jumpUrl: target,
);
return true;
}
Future<String> _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);
return resolvedTarget;
}
} catch (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<String, String>.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) {
return _taskGiftAsset;
}
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<SCTaskListRes> 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 }