422 lines
12 KiB
Dart
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),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|