feat(ui): 7-day daily-reward popup on home
Presentational DailyRewardSheet (7 cells, today highlighted, reward icons from the calendar table, claim + watch-ad-2x buttons). HomeScreen becomes a ConsumerStatefulWidget that shows it once per mount via a post-frame guard; the 2x path grants the doubled reward only if the rewarded ad was earned, else the base reward. Guards the throwing saveRepositoryProvider default so a repo-less mount is a no-op. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
// lib/ui/widgets/daily_reward_sheet.dart
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../game/daily/daily_reward.dart';
|
||||
import '../../game/models/booster.dart';
|
||||
import '../../l10n/gen/app_localizations.dart';
|
||||
|
||||
/// Presentational 7-day login calendar. [day] is today's calendar position
|
||||
/// (1..7); past days read as claimed, the future stays locked. [onClaim] fires
|
||||
/// with `false` for a plain claim and `true` for the watch-an-ad "2×" claim.
|
||||
class DailyRewardSheet extends StatelessWidget {
|
||||
const DailyRewardSheet({super.key, required this.day, required this.onClaim});
|
||||
|
||||
final int day;
|
||||
final void Function(bool doubled) onClaim;
|
||||
|
||||
static const _cal = DailyRewardCalendar();
|
||||
static const _icons = {
|
||||
BoosterType.hammer: Icons.gavel,
|
||||
BoosterType.shuffle: Icons.shuffle,
|
||||
BoosterType.lineBomb: Icons.clear_all,
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
l10n.dailyRewardTitle,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
for (var d = 1; d <= DailyRewardCalendar.cycle; d++)
|
||||
_DayCell(
|
||||
key: ValueKey('daily_day_$d'),
|
||||
day: d,
|
||||
reward: _cal.rewardFor(d),
|
||||
past: d < day,
|
||||
today: d == day,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton(
|
||||
key: const ValueKey('daily_claim'),
|
||||
onPressed: () => onClaim(false),
|
||||
child: Text(l10n.dailyClaim),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
TextButton(
|
||||
key: const ValueKey('daily_double'),
|
||||
onPressed: () => onClaim(true),
|
||||
child: Text(l10n.dailyDoubleWithAd),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DayCell extends StatelessWidget {
|
||||
const _DayCell({
|
||||
super.key,
|
||||
required this.day,
|
||||
required this.reward,
|
||||
required this.past,
|
||||
required this.today,
|
||||
});
|
||||
|
||||
final int day;
|
||||
final Map<BoosterType, int> reward;
|
||||
final bool past;
|
||||
final bool today;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Opacity(
|
||||
opacity: past ? 0.45 : 1,
|
||||
child: Container(
|
||||
width: 66,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: today
|
||||
? scheme.primary.withValues(alpha: 0.25)
|
||||
: Colors.white.withValues(alpha: 0.06),
|
||||
border: today
|
||||
? Border.all(color: scheme.primary, width: 2)
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('$day', style: Theme.of(context).textTheme.labelMedium),
|
||||
const SizedBox(height: 4),
|
||||
Wrap(
|
||||
spacing: 2,
|
||||
runSpacing: 2,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
for (final entry in reward.entries)
|
||||
_RewardIcon(
|
||||
icon: DailyRewardSheet._icons[entry.key]!,
|
||||
count: entry.value,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (past)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 2),
|
||||
child: Icon(Icons.check_circle,
|
||||
size: 14, color: Colors.greenAccent),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RewardIcon extends StatelessWidget {
|
||||
const _RewardIcon({required this.icon, required this.count});
|
||||
|
||||
final IconData icon;
|
||||
final int count;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 14),
|
||||
Text('$count', style: const TextStyle(fontSize: 11)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user