first commit

This commit is contained in:
Your Name
2026-06-10 18:23:39 +08:00
commit 790110de8e
225 changed files with 10068 additions and 0 deletions

View File

@@ -0,0 +1,182 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
class FlipCard extends StatefulWidget {
final bool isFlipped;
final bool isMatched;
final int shakeToken;
final bool shouldShake;
final bool disabled;
final VoidCallback? onPress;
final Widget front;
final Widget back;
final double borderRadius;
final Duration flipDuration;
const FlipCard({
super.key,
required this.isFlipped,
required this.isMatched,
required this.shakeToken,
required this.shouldShake,
required this.disabled,
required this.onPress,
required this.front,
required this.back,
this.borderRadius = 16,
this.flipDuration = const Duration(milliseconds: 220),
});
@override
State<FlipCard> createState() => _FlipCardState();
}
class _FlipCardState extends State<FlipCard> with TickerProviderStateMixin {
late final AnimationController _flipController;
late final AnimationController _popController;
late final AnimationController _glowController;
late final AnimationController _shakeController;
late Animation<double> _flip;
late Animation<double> _pop;
late Animation<double> _glow;
late Animation<double> _shake;
int _prevShakeToken = 0;
bool _prevMatched = false;
@override
void initState() {
super.initState();
_prevMatched = widget.isMatched;
_prevShakeToken = widget.shakeToken;
_flipController = AnimationController(
vsync: this,
duration: widget.flipDuration,
value: widget.isFlipped ? 1 : 0,
);
_popController = AnimationController(vsync: this, duration: const Duration(milliseconds: 210));
_glowController = AnimationController(vsync: this, duration: const Duration(milliseconds: 340));
_shakeController = AnimationController(vsync: this, duration: const Duration(milliseconds: 180));
_flip = CurvedAnimation(parent: _flipController, curve: Curves.easeInOut);
_pop = TweenSequence<double>([
TweenSequenceItem(tween: Tween(begin: 1.0, end: 1.08), weight: 90),
TweenSequenceItem(tween: Tween(begin: 1.08, end: 1.0), weight: 120),
]).animate(_popController);
_glow = TweenSequence<double>([
TweenSequenceItem(tween: Tween(begin: 0, end: 1), weight: 120),
TweenSequenceItem(tween: Tween(begin: 1, end: 0), weight: 220),
]).animate(_glowController);
_shake = TweenSequence<double>([
TweenSequenceItem(tween: Tween(begin: 0, end: 8), weight: 45),
TweenSequenceItem(tween: Tween(begin: 8, end: -8), weight: 45),
TweenSequenceItem(tween: Tween(begin: -8, end: 8), weight: 45),
TweenSequenceItem(tween: Tween(begin: 8, end: 0), weight: 45),
]).animate(_shakeController);
}
@override
void didUpdateWidget(covariant FlipCard oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isFlipped != oldWidget.isFlipped) {
if (widget.isFlipped) {
_flipController.forward();
} else {
_flipController.reverse();
}
}
if (!_prevMatched && widget.isMatched) {
_popController
..reset()
..forward();
_glowController
..reset()
..forward();
}
_prevMatched = widget.isMatched;
if (widget.shakeToken != _prevShakeToken) {
_prevShakeToken = widget.shakeToken;
if (widget.shouldShake) {
_shakeController
..reset()
..forward();
}
}
}
@override
void dispose() {
_flipController.dispose();
_popController.dispose();
_glowController.dispose();
_shakeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.disabled ? null : widget.onPress,
child: AnimatedBuilder(
animation: Listenable.merge([
_flipController,
_popController,
_glowController,
_shakeController,
]),
builder: (context, child) {
final angle = _flip.value * math.pi;
final showBack = angle > math.pi / 2;
final scale = _popController.isAnimating ? _pop.value : 1.0;
final dx = _shakeController.isAnimating ? _shake.value : 0.0;
return Transform.translate(
offset: Offset(dx, 0),
child: Transform.scale(
scale: scale,
child: Stack(
fit: StackFit.expand,
children: [
Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.0012)
..rotateY(angle),
child: ClipRRect(
borderRadius: BorderRadius.circular(widget.borderRadius),
child: showBack
? Transform(
alignment: Alignment.center,
transform: Matrix4.identity()..rotateY(math.pi),
child: widget.back,
)
: widget.front,
),
),
IgnorePointer(
child: Opacity(
opacity: _glow.value,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(widget.borderRadius),
border: Border.all(
color: const Color.fromRGBO(120, 255, 255, 0.9),
width: 3,
),
),
),
),
),
],
),
),
);
},
),
);
}
}

View File

@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
class ProgressBar extends StatelessWidget {
final double percent;
final double width;
final double height;
final double insetX;
final double insetY;
const ProgressBar({
super.key,
required this.percent,
required this.width,
required this.height,
this.insetX = 8,
this.insetY = 6,
});
@override
Widget build(BuildContext context) {
final p = percent.clamp(0.0, 1.0);
final innerW = (width - insetX * 2).clamp(0.0, double.infinity);
final innerH = (height - insetY * 2).clamp(0.0, double.infinity);
final fillW = innerW * p;
final fillAsset = p >= 1
? 'assets/images/04ac514f79905c0fd3fd97534829cc34.png'
: 'assets/images/4ea0244a9d7ad2e9357424465415f8b4.png';
return SizedBox(
width: width,
height: height,
child: Stack(
children: [
Positioned.fill(
child: Image.asset(
'assets/images/18a74842e653d3118c500a2248a2fda7.png',
fit: BoxFit.fill,
),
),
Positioned(
left: insetX,
top: insetY,
width: innerW,
height: innerH,
child: Align(
alignment: Alignment.centerLeft,
child: ClipRect(
child: SizedBox(
width: fillW,
height: innerH,
child: OverflowBox(
alignment: Alignment.centerLeft,
minWidth: innerW,
maxWidth: innerW,
child: Image.asset(fillAsset, fit: BoxFit.fill),
),
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,216 @@
import 'dart:ui';
import 'package:flutter/material.dart';
typedef RewardAddTimeHandler =
Future<void> Function(void Function(int sec) grant);
class ResultModal extends StatefulWidget {
final bool visible;
final String status;
final int score;
final int bestScore;
final bool hasNext;
final VoidCallback onRetry;
final VoidCallback onNext;
final bool showAddTime;
final int addTimeSeconds;
final RewardAddTimeHandler? onRequestRewardAddTime;
const ResultModal({
super.key,
required this.visible,
required this.status,
required this.score,
required this.bestScore,
required this.hasNext,
required this.onRetry,
required this.onNext,
this.showAddTime = false,
this.addTimeSeconds = 30,
this.onRequestRewardAddTime,
});
@override
State<ResultModal> createState() => _ResultModalState();
}
class _ResultModalState extends State<ResultModal> {
String? toast;
bool rewardLoading = false;
void showToast(String msg) {
setState(() => toast = msg);
Future.delayed(const Duration(milliseconds: 1800), () {
if (mounted && toast == msg) {
setState(() => toast = null);
}
});
}
Future<void> _handleRewardAddTime() async {
if (rewardLoading) return;
setState(() => rewardLoading = true);
try {
if (widget.onRequestRewardAddTime != null) {
await widget.onRequestRewardAddTime!((_) {});
}
} catch (_) {
showToast('Rewarded ad failed to load. Please try again.');
} finally {
if (mounted) {
setState(() => rewardLoading = false);
}
}
}
@override
Widget build(BuildContext context) {
if (!widget.visible) return const SizedBox.shrink();
final isWin = widget.status == 'won';
final showNext = isWin && widget.hasNext;
final showAddTime = !isWin && widget.showAddTime;
return Positioned.fill(
child: Material(
color: Colors.transparent,
child: Stack(
children: [
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8),
child: Container(color: const Color.fromRGBO(0, 0, 0, 0.35)),
),
),
Center(
child: Container(
width: MediaQuery.of(context).size.width.clamp(0, 520).toDouble(),
margin: const EdgeInsets.symmetric(horizontal: 16),
child: AspectRatio(
aspectRatio: 0.67,
child: Stack(
fit: StackFit.expand,
children: [
Positioned.fill(
child: Image.asset(
isWin
? 'assets/images/settlement/win.png'
: 'assets/images/settlement/lose.png',
fit: BoxFit.fill,
),
),
Positioned.fill(
child: Padding(
padding: const EdgeInsets.fromLTRB(34, 168, 34, 32),
child: Column(
children: [
Text(
'Best Score',
style: TextStyle(
fontSize: isWin ? 22 : 20,
fontWeight: FontWeight.w900,
color: isWin
? const Color(0xFFB944D7)
: const Color(0xFF4F6BDE),
),
),
const SizedBox(height: 16),
Image.asset(
'assets/images/settlement/gold.png',
width: 118,
height: 118,
),
const SizedBox(height: 12),
Text(
'${widget.bestScore}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: isWin ? 38 : 34,
fontWeight: FontWeight.w900,
color: isWin
? const Color(0xFFFF6CA8)
: const Color(0xFF4F6BDE),
letterSpacing: 1,
),
),
const Spacer(),
Row(
children: [
if (showNext) ...[
Expanded(
child: _ActionImageButton(
asset: 'assets/images/settlement/next.png',
onTap: widget.onNext,
),
),
const SizedBox(width: 14),
],
if (showAddTime) ...[
Expanded(
child: _ActionImageButton(
asset: 'assets/images/settlement/addTime.png',
onTap: widget.onRequestRewardAddTime == null
? null
: _handleRewardAddTime,
),
),
const SizedBox(width: 14),
],
Expanded(
child: _ActionImageButton(
asset: 'assets/images/settlement/retry.png',
onTap: widget.onRetry,
),
),
],
),
if (toast != null) ...[
const SizedBox(height: 12),
Text(
toast!,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
),
textAlign: TextAlign.center,
),
],
],
),
),
),
],
),
),
),
),
],
),
),
);
}
}
class _ActionImageButton extends StatelessWidget {
const _ActionImageButton({
required this.asset,
required this.onTap,
});
final String asset;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Opacity(
opacity: onTap == null ? 0.55 : 1,
child: SizedBox(
height: 56,
child: Image.asset(asset, fit: BoxFit.fill),
),
),
);
}
}

View File

@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
typedef RewardRequest = Future<void> Function(void Function(int sec) grant);
class RewardAddTimeButton extends StatefulWidget {
final int extraSeconds;
final bool disabled;
final RewardRequest onRequestReward;
const RewardAddTimeButton({
super.key,
required this.extraSeconds,
required this.disabled,
required this.onRequestReward,
});
@override
State<RewardAddTimeButton> createState() => _RewardAddTimeButtonState();
}
class _RewardAddTimeButtonState extends State<RewardAddTimeButton> {
bool loading = false;
void safeGrant(int sec) {
if (mounted) {
setState(() => loading = false);
}
}
@override
Widget build(BuildContext context) {
final disabled = widget.disabled || loading;
return GestureDetector(
onTap: disabled
? null
: () async {
setState(() => loading = true);
await widget.onRequestReward(safeGrant);
},
child: AnimatedOpacity(
duration: const Duration(milliseconds: 120),
opacity: disabled ? 0.55 : 1,
child: Container(
height: 54,
decoration: BoxDecoration(
color: const Color.fromRGBO(255, 190, 40, 0.92),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color.fromRGBO(255, 255, 255, 0.22), width: 2),
),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
loading ? 'LOADING...' : 'ADD TIME +${widget.extraSeconds}s',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w900,
letterSpacing: 1,
),
),
const SizedBox(height: 2),
const Text(
'Watch Rewarded Ad',
style: TextStyle(
color: Color.fromRGBO(255, 255, 255, 0.92),
fontSize: 12,
fontWeight: FontWeight.w800,
),
)
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
class ScoreText extends StatefulWidget {
final int value;
final TextStyle? style;
const ScoreText({super.key, required this.value, this.style});
@override
State<ScoreText> createState() => _ScoreTextState();
}
class _ScoreTextState extends State<ScoreText>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late Animation<double> _scale;
int _prev = 0;
@override
void initState() {
super.initState();
_prev = widget.value;
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 210),
);
_scale = const AlwaysStoppedAnimation(1.0);
}
@override
void didUpdateWidget(covariant ScoreText oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.value == _prev) return;
final begin = widget.value > _prev ? 1.12 : 0.88;
_prev = widget.value;
_scale = TweenSequence<double>([
TweenSequenceItem(tween: Tween(begin: 1.0, end: begin), weight: 90),
TweenSequenceItem(tween: Tween(begin: begin, end: 1.0), weight: 120),
]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
_controller
..reset()
..forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(scale: _scale.value, child: child);
},
child: Text('${widget.value}', style: widget.style),
);
}
}