274 lines
8.1 KiB
Dart
274 lines
8.1 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_visuals.dart';
|
|
|
|
const double _cardRadius = 14;
|
|
const double _cardBorderWidth = 1;
|
|
const double _titleFont = 20;
|
|
const double _boostFont = 12;
|
|
const double _subtitleFont = 13;
|
|
const double _dayHeight = 80;
|
|
const double _dayRadius = 9;
|
|
const double _dayGap = 5;
|
|
const double _dayRewardFont = 13;
|
|
const double _dayLabelFont = 11;
|
|
const double _dayCoinSize = 24;
|
|
const double _buttonHeight = 48;
|
|
const double _buttonFont = 16;
|
|
const double _loadingSize = 20;
|
|
const double _loadingStroke = 2;
|
|
const Color _cardStart = Color(0xFF1A1A1C);
|
|
const Color _cardEnd = Color(0xFF101010);
|
|
const Color _cardBorder = Color(0xFF3A3A3C);
|
|
const Color _daySurface = Color(0xFF202022);
|
|
const Color _dayBorder = Color(0xFF363638);
|
|
const Color _currentSurfaceStart = Color(0xFF3E0813);
|
|
const Color _currentSurfaceEnd = Color(0xFF24070D);
|
|
const Color _currentBorder = Color(0xFFFF4860);
|
|
const Color _buttonEnd = Color(0xFFFF565B);
|
|
|
|
/// 奖励中心签到卡,展示七日规则与签到操作。
|
|
class RewardCheckinCard extends StatelessWidget {
|
|
const RewardCheckinCard({
|
|
super.key,
|
|
required this.l10n,
|
|
required this.rewards,
|
|
required this.checkin,
|
|
required this.onCheckIn,
|
|
required this.onBoost,
|
|
});
|
|
|
|
final AppLocalizations l10n;
|
|
final RewardController rewards;
|
|
final RewardCheckinState checkin;
|
|
final Future<void> Function() onCheckIn;
|
|
final VoidCallback onBoost;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
key: const Key('reward-checkin-card'),
|
|
padding: const EdgeInsets.all(AppDimens.cardPadding),
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(colors: [_cardStart, _cardEnd]),
|
|
borderRadius: BorderRadius.circular(_cardRadius),
|
|
border: Border.all(color: _cardBorder, width: _cardBorderWidth),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_header(),
|
|
const SizedBox(height: AppDimens.spaceXs),
|
|
Text(
|
|
l10n.rewardCheckInSubtitle,
|
|
style: const TextStyle(
|
|
color: AppColors.textSecondary,
|
|
fontSize: _subtitleFont,
|
|
),
|
|
),
|
|
const SizedBox(height: AppDimens.spaceLg),
|
|
_days(),
|
|
const SizedBox(height: AppDimens.spaceLg),
|
|
_action(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _header() {
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
l10n.rewardCheckInTitle,
|
|
style: const TextStyle(
|
|
color: AppColors.textPrimary,
|
|
fontSize: _titleFont,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
),
|
|
InkWell(
|
|
onTap: onBoost,
|
|
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: AppDimens.spaceSm),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
l10n.rewardBoostCheckIn,
|
|
style: const TextStyle(
|
|
color: AppColors.brandRed,
|
|
fontSize: _boostFont,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const Icon(
|
|
Icons.chevron_right_rounded,
|
|
color: AppColors.brandRed,
|
|
size: 18,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _days() {
|
|
if (checkin.days.isEmpty) return const SizedBox.shrink();
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
for (var index = 0; index < checkin.days.length; index++) ...[
|
|
Expanded(
|
|
child: _RewardDay(l10n: l10n, day: checkin.days[index]),
|
|
),
|
|
if (index < checkin.days.length - 1) const SizedBox(width: _dayGap),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _action() {
|
|
final disabled = checkin.checkedInToday || rewards.checkingIn;
|
|
return Container(
|
|
key: const Key('reward-checkin-action'),
|
|
height: _buttonHeight,
|
|
decoration: BoxDecoration(
|
|
gradient: disabled
|
|
? const LinearGradient(
|
|
colors: [AppColors.contentChip, AppColors.contentChip],
|
|
)
|
|
: const LinearGradient(colors: [AppColors.brandRed, _buttonEnd]),
|
|
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
|
|
),
|
|
child: Material(
|
|
color: AppColors.transparent,
|
|
child: InkWell(
|
|
onTap: disabled ? null : () => unawaited(onCheckIn()),
|
|
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
|
|
child: Center(child: _actionContent()),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _actionContent() {
|
|
if (rewards.checkingIn) {
|
|
return const SizedBox.square(
|
|
dimension: _loadingSize,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: _loadingStroke,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
);
|
|
}
|
|
final label = checkin.checkedInToday
|
|
? l10n.rewardCheckedIn
|
|
: l10n.rewardCheckInActionWithCoins(_currentReward());
|
|
return Row(
|
|
key: const Key('reward-checkin-action-content'),
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (!checkin.checkedInToday) ...[
|
|
const RewardAdIcon(),
|
|
const SizedBox(width: AppDimens.spaceMd),
|
|
],
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 220),
|
|
child: Text(
|
|
label,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
color: AppColors.textPrimary,
|
|
fontSize: _buttonFont,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
int _currentReward() {
|
|
for (final day in checkin.days) {
|
|
if (day.current) return day.rewardCoin;
|
|
}
|
|
return checkin.days.isEmpty ? 0 : checkin.days.first.rewardCoin;
|
|
}
|
|
}
|
|
|
|
class _RewardDay extends StatelessWidget {
|
|
const _RewardDay({required this.l10n, required this.day});
|
|
|
|
final AppLocalizations l10n;
|
|
final RewardCheckinDay day;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
height: _dayHeight,
|
|
decoration: BoxDecoration(
|
|
gradient: day.current
|
|
? const LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [_currentSurfaceStart, _currentSurfaceEnd],
|
|
)
|
|
: null,
|
|
color: day.current ? null : _daySurface,
|
|
borderRadius: BorderRadius.circular(_dayRadius),
|
|
border: Border.all(color: day.current ? _currentBorder : _dayBorder),
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
child: Text(
|
|
'+${day.rewardCoin}',
|
|
style: TextStyle(
|
|
color: day.current
|
|
? const Color(0xFFFFE18C)
|
|
: AppColors.textPrimary,
|
|
fontSize: _dayRewardFont,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: AppDimens.spaceXs),
|
|
const RewardCoinIcon(size: _dayCoinSize),
|
|
const SizedBox(height: AppDimens.spaceXs),
|
|
FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
child: Text(
|
|
day.current
|
|
? l10n.rewardToday
|
|
: l10n.rewardDayLabel(day.dayIndex),
|
|
style: TextStyle(
|
|
color: day.checked
|
|
? AppColors.success
|
|
: AppColors.textSecondary,
|
|
fontSize: _dayLabelFont,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|