Files
ReelShort/lib/pages/reward/reward_tasks_view.dart
T

422 lines
12 KiB
Dart

// 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<void> Function() onRefresh;
final Future<void> Function() onCheckIn;
final Future<void> 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<void> 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<void> 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<void> 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),
),
),
);
}
}