// Author: fruit.com.cn import 'dart:async'; import 'package:flutter/material.dart'; import '../../core/data/models/reward.dart'; import '../../core/data/services/reward_controller.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.dart'; import '../../l10n/app_localizations.dart'; import 'reward_checkin_card.dart'; import 'reward_visuals.dart'; const double _balanceLabelFont = 18; const double _balanceFont = 36; const double _sectionTitleFont = 20; const double _sectionSubtitleFont = 13; const double _taskRowMinHeight = 68; const double _taskTitleFont = 15; const double _taskSubtitleFont = 12; const double _taskRewardFont = 14; const double _taskButtonHeight = 36; const double _taskButtonMinWidth = 80; const double _taskButtonFont = 13; const double _taskCardRadius = 14; const double _taskCardBorderWidth = 1; const Color _taskCardStart = Color(0xFF1A1A1C); const Color _taskCardEnd = Color(0xFF101010); const Color _taskCardBorder = Color(0xFF29292B); const Color _mutedActionStart = Color(0xFF3C171C); const Color _divider = Color(0xFF333336); const String _checkInSectionCode = 'CHECK_IN'; /// 奖励任务内容,负责余额、签到卡与可配置任务列表。 class RewardTasksView extends StatelessWidget { const RewardTasksView({ super.key, required this.l10n, required this.rewards, required this.overview, required this.onRefresh, required this.onCheckIn, required this.onTaskAction, this.onOpenWallet, this.onBoostCheckIn, }); final AppLocalizations l10n; final RewardController rewards; final RewardOverview overview; final Future Function() onRefresh; final Future Function() onCheckIn; final Future Function(RewardTask task) onTaskAction; final VoidCallback? onOpenWallet; final VoidCallback? onBoostCheckIn; @override Widget build(BuildContext context) { return RefreshIndicator( color: AppColors.brandRed, backgroundColor: AppColors.contentCard, onRefresh: onRefresh, child: ListView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB( AppDimens.pageHorizontal, AppDimens.spaceLg, AppDimens.pageHorizontal, AppDimens.spaceXl, ), children: [ _Balance( key: const Key('reward-balance'), balance: overview.coinBalance, onOpenWallet: onOpenWallet, ), const SizedBox(height: AppDimens.spaceLg), RewardCheckinCard( l10n: l10n, rewards: rewards, checkin: overview.checkin, onCheckIn: onCheckIn, onBoost: onBoostCheckIn ?? () {}, ), for (final section in overview.sections) if (_isVisibleSection(section)) ...[ const SizedBox(height: AppDimens.spaceXl), _SectionHeader( key: Key('reward-section-${section.code}'), l10n: l10n, section: section, ), const SizedBox(height: AppDimens.spaceMd), _TaskGroupCard( key: Key('reward-task-card-${section.code}'), l10n: l10n, rewards: rewards, section: section, onTaskAction: onTaskAction, ), ], if (!overview.sections.any(_isVisibleSection)) _Empty(label: l10n.rewardEmptyTasks), ], ), ); } } bool _isVisibleSection(RewardSection section) { return section.code.toUpperCase() != _checkInSectionCode && section.tasks.isNotEmpty; } class _Balance extends StatelessWidget { const _Balance({super.key, required this.balance, this.onOpenWallet}); final int balance; final VoidCallback? onOpenWallet; @override Widget build(BuildContext context) { final content = Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Flexible( child: FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text( AppLocalizations.of(context)!.rewardMyCoins, style: const TextStyle( color: AppColors.textSecondary, fontSize: _balanceLabelFont, fontWeight: FontWeight.w500, ), ), ), ), const SizedBox(width: AppDimens.spaceMd), Text( '$balance', style: const TextStyle( color: AppColors.textPrimary, fontSize: _balanceFont, fontWeight: FontWeight.w800, height: 1, ), ), const SizedBox(width: AppDimens.spaceSm), const RewardCoinIcon(key: Key('reward-balance-coin-icon'), size: 32), ], ); if (onOpenWallet == null) return content; return InkWell( onTap: onOpenWallet, borderRadius: BorderRadius.circular(AppDimens.cardRadius), child: Padding( padding: const EdgeInsets.symmetric(vertical: AppDimens.spaceXs), child: content, ), ); } } class _SectionHeader extends StatelessWidget { const _SectionHeader({super.key, required this.l10n, required this.section}); final AppLocalizations l10n; final RewardSection section; @override Widget build(BuildContext context) { final subtitle = _sectionSubtitle(l10n, section.code); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( section.title, style: const TextStyle( color: AppColors.textPrimary, fontSize: _sectionTitleFont, fontWeight: FontWeight.w800, ), ), if (subtitle.isNotEmpty) ...[ const SizedBox(height: AppDimens.spaceXs), Text( subtitle, style: const TextStyle( color: AppColors.textSecondary, fontSize: _sectionSubtitleFont, ), ), ], ], ); } } String _sectionSubtitle(AppLocalizations l10n, String code) { return switch (code.toUpperCase()) { 'DAILY' => l10n.rewardDailySectionSubtitle, 'NEWBIE' => l10n.rewardNewbieSectionSubtitle, 'INVITE' || 'INVITATION' => l10n.rewardInviteSectionSubtitle, _ => '', }; } class _TaskGroupCard extends StatelessWidget { const _TaskGroupCard({ super.key, required this.l10n, required this.rewards, required this.section, required this.onTaskAction, }); final AppLocalizations l10n; final RewardController rewards; final RewardSection section; final Future Function(RewardTask task) onTaskAction; @override Widget build(BuildContext context) { return Container( key: Key('reward-task-surface-${section.code}'), decoration: BoxDecoration( gradient: const LinearGradient(colors: [_taskCardStart, _taskCardEnd]), borderRadius: BorderRadius.circular(_taskCardRadius), border: Border.all(color: _taskCardBorder, width: _taskCardBorderWidth), ), child: Column( children: [ for (var index = 0; index < section.tasks.length; index++) ...[ _TaskRow( l10n: l10n, rewards: rewards, sectionCode: section.code, task: section.tasks[index], onTaskAction: onTaskAction, ), if (index < section.tasks.length - 1) const Divider( color: _divider, height: 1, indent: 16, endIndent: 16, ), ], ], ), ); } } class _TaskRow extends StatelessWidget { const _TaskRow({ required this.l10n, required this.rewards, required this.sectionCode, required this.task, required this.onTaskAction, }); final AppLocalizations l10n; final RewardController rewards; final String sectionCode; final RewardTask task; final Future Function(RewardTask task) onTaskAction; @override Widget build(BuildContext context) { return ConstrainedBox( constraints: const BoxConstraints(minHeight: _taskRowMinHeight), child: Padding( padding: const EdgeInsets.all(AppDimens.cardPadding), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ RewardTaskLeading(task: task), const SizedBox(width: AppDimens.spaceMd), Expanded(child: _copy()), const SizedBox(width: AppDimens.spaceMd), _TaskButton( l10n: l10n, rewards: rewards, sectionCode: sectionCode, task: task, onTaskAction: onTaskAction, ), ], ), ), ); } Widget _copy() { return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( task.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.textPrimary, fontSize: _taskTitleFont, fontWeight: FontWeight.w600, ), ), if (task.subtitle.isNotEmpty) ...[ const SizedBox(height: AppDimens.spaceXs), Text( task.subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.textSecondary, fontSize: _taskSubtitleFont, ), ), ], if (task.rewardCoin > 0) ...[ const SizedBox(height: AppDimens.spaceXs), Row( mainAxisSize: MainAxisSize.min, children: [ Text( '+${task.rewardCoin}', style: const TextStyle( color: AppColors.brandRed, fontSize: _taskRewardFont, fontWeight: FontWeight.w700, ), ), const SizedBox(width: AppDimens.spaceXs), const RewardCoinIcon(size: 20), ], ), ], ], ); } } class _TaskButton extends StatelessWidget { const _TaskButton({ required this.l10n, required this.rewards, required this.sectionCode, required this.task, required this.onTaskAction, }); final AppLocalizations l10n; final RewardController rewards; final String sectionCode; final RewardTask task; final Future Function(RewardTask task) onTaskAction; @override Widget build(BuildContext context) { final loading = rewards.claimingTaskId == task.id; final dark = sectionCode.toUpperCase() == 'NEWBIE' && !task.claimable; final label = task.claimed ? l10n.rewardClaimed : task.claimable ? l10n.rewardClaimAction : (task.ctaText.isEmpty ? l10n.rewardGoAction : task.ctaText); return ConstrainedBox( key: Key('reward-task-action-${task.id}'), constraints: const BoxConstraints( minWidth: _taskButtonMinWidth, minHeight: _taskButtonHeight, ), child: FilledButton( onPressed: task.claimed || loading ? null : () => unawaited(onTaskAction(task)), style: FilledButton.styleFrom( backgroundColor: dark ? _mutedActionStart : AppColors.brandRed, disabledBackgroundColor: AppColors.contentChip, minimumSize: const Size(_taskButtonMinWidth, _taskButtonHeight), tapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.symmetric(horizontal: AppDimens.spaceSm), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppDimens.pillRadius), ), ), child: loading ? const SizedBox.square( dimension: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : Text(label, style: const TextStyle(fontSize: _taskButtonFont)), ), ); } } class _Empty extends StatelessWidget { const _Empty({required this.label}); final String label; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: AppDimens.spaceXl), child: Center( child: Text( label, style: const TextStyle(color: AppColors.textTertiary), ), ), ); } }