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,473 @@
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import '../components/result_modal.dart';
import '../flame/memory_match_flame_game.dart';
import '../game/levels.dart';
class GameScreenFlame extends StatefulWidget {
const GameScreenFlame({super.key, this.levelId = 1});
final int levelId;
@override
State<GameScreenFlame> createState() => _GameScreenFlameState();
}
class _GameScreenFlameState extends State<GameScreenFlame> {
late int currentLevelId;
late MemoryMatchFlameGame flameGame;
@override
void initState() {
super.initState();
currentLevelId = widget.levelId;
flameGame = _buildGame(currentLevelId);
}
MemoryMatchFlameGame _buildGame(int levelId) {
final level = levels[(levelId - 1).clamp(0, levels.length - 1)];
return MemoryMatchFlameGame(
level: level,
onStateChanged: () {
if (mounted) {
setState(() {});
}
},
onResult: (status, score) {
if (mounted) {
setState(() {});
}
},
);
}
Future<void> _restartSameLevel() async {
await flameGame.restartSameLevel();
if (mounted) {
setState(() {});
}
}
Future<void> _goNext() async {
if (currentLevelId >= levels.length) {
await _restartSameLevel();
return;
}
currentLevelId += 1;
await flameGame.nextLevel(levels[currentLevelId - 1]);
if (mounted) {
setState(() {});
}
}
@override
Widget build(BuildContext context) {
final isReady = flameGame.isReady;
final scoreText = isReady ? '${flameGame.controller.score}' : '0';
return Scaffold(
body: Stack(
children: [
GameWidget(game: flameGame),
SafeArea(
child: Column(
children: [
Padding(
padding: EdgeInsets.symmetric(
horizontal: MediaQuery.of(context).size.width >= 768
? 22
: 12,
vertical: 8,
),
child: _TopHud(
flameGame: flameGame,
onPause: () {
flameGame.togglePause();
},
onMusic: () {
flameGame.toggleMusic();
},
onSfx: () {
flameGame.toggleSfx();
},
),
),
Text(
'Level ${flameGame.level.id}',
style: const TextStyle(
fontSize: 34,
fontWeight: FontWeight.w900,
color: Color(0xFF56F0FF),
letterSpacing: 1,
shadows: [
Shadow(
color: Color.fromRGBO(80, 255, 255, 0.55),
blurRadius: 12,
offset: Offset(0, 2),
),
],
),
),
const Spacer(),
Padding(
padding: EdgeInsets.symmetric(
horizontal: MediaQuery.of(context).size.width >= 768
? 22
: 18,
),
child: _BottomHud(
flameGame: flameGame,
scoreText: scoreText,
onRetry: () {
_restartSameLevel();
},
),
),
// Padding(
// padding: const EdgeInsets.symmetric(vertical: 6),
// child: Text(
// 'Moves: $moves Mistakes: $mistakes${flameGame.level.maxMistakes != null ? ' / ${flameGame.level.maxMistakes}' : ''} Grid: ${flameGame.level.rowPattern.join('-')}',
// style: const TextStyle(
// color: Color.fromRGBO(255, 255, 255, 0.85),
// ),
// ),
// ),
],
),
),
if (flameGame.pausedByUser)
Positioned.fill(
child: _PauseOverlay(
onResume: () {
flameGame.togglePause();
},
onRetry: () {
_restartSameLevel();
},
onHome: () {
Navigator.of(context).pushReplacementNamed('/start');
},
),
),
ResultModal(
visible: flameGame.showResult,
status: flameGame.resultStatus,
score: flameGame.finalScore,
bestScore: flameGame.bestScore,
hasNext: currentLevelId + 1 <= levels.length,
onRetry: _restartSameLevel,
onNext: _goNext,
showAddTime: flameGame.resultStatus == 'lost',
addTimeSeconds: 30,
onRequestRewardAddTime: (grant) async {
await Future.delayed(const Duration(milliseconds: 600));
final sec = await flameGame.reviveWithReward();
grant(sec);
},
),
],
),
);
}
}
class _TopHud extends StatelessWidget {
const _TopHud({
required this.flameGame,
required this.onPause,
required this.onMusic,
required this.onSfx,
});
final MemoryMatchFlameGame flameGame;
final VoidCallback onPause;
final VoidCallback onMusic;
final VoidCallback onSfx;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final compact = constraints.maxWidth < 380;
final iconSize = compact ? 42.0 : 46.0;
final itemGap = compact ? 6.0 : 10.0;
final groupGap = compact ? 8.0 : 12.0;
final topBadgeHeight = compact ? 52.0 : 58.0;
final rightGroupWidth = iconSize * 2 + itemGap;
final timerWidth =
(constraints.maxWidth - iconSize - rightGroupWidth - groupGap * 2)
.clamp(118.0, 176.0);
return Row(
children: [
_NeonIconButton(
iconAsset: flameGame.pausedByUser
? 'assets/images/0f3f8a4a21cdb16db2f080e4d7695cdc.png'
: 'assets/images/e86bdd5d8908079d1ed4c06015f37eb0.png',
onTap: onPause,
size: iconSize,
),
SizedBox(width: groupGap),
_HudValueBadge(
backgroundAsset: 'assets/images/b5bf27b2555de44e3df2230080db5a1d.png',
text: flameGame.timeText,
width: timerWidth,
height: topBadgeHeight,
textLeftInset: compact ? 64 : 76,
textRightInset: compact ? 12 : 16,
textStyle: TextStyle(
color: const Color(0xFF69F6FF),
fontSize: compact ? 16 : 18,
fontWeight: FontWeight.w900,
letterSpacing: compact ? 0.5 : 1,
),
),
const Spacer(),
_NeonIconButton(
iconAsset: flameGame.mutedMusic
? 'assets/images/5efe427a4e20b0ab12afe98c3cb50a60.png'
: 'assets/images/5efe427a4e20b0ab12afe98c3cb50a60.png',
active: !flameGame.mutedMusic,
onTap: onMusic,
size: iconSize,
),
SizedBox(width: itemGap),
_NeonIconButton(
iconAsset: flameGame.mutedSfx
? 'assets/images/849ade26e1b6da2ed96d00db639a0985.png'
: 'assets/images/1548af1c94ad45584324df8f08baf227.png',
active: !flameGame.mutedSfx,
onTap: onSfx,
size: iconSize,
),
],
);
},
);
}
}
class _BottomHud extends StatelessWidget {
const _BottomHud({
required this.flameGame,
required this.scoreText,
required this.onRetry,
});
final MemoryMatchFlameGame flameGame;
final String scoreText;
final VoidCallback onRetry;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_HudValueBadge(
backgroundAsset: 'assets/images/5e36941b3d856737e81516acd45edc50.png',
text: scoreText,
width: 118,
height: 44,
textLeftInset: 54,
textRightInset: 10,
textStyle: const TextStyle(
color: Color(0xFF56F0FF),
fontSize: 18,
fontWeight: FontWeight.w900,
letterSpacing: 1,
),
),
_NeonIconButton(
iconAsset: 'assets/images/161747ec4dc9f55f1760195593742232.png',
onTap: onRetry,
),
],
);
}
}
class _HudValueBadge extends StatelessWidget {
const _HudValueBadge({
required this.backgroundAsset,
required this.text,
required this.width,
required this.height,
required this.textLeftInset,
required this.textRightInset,
required this.textStyle,
});
final String backgroundAsset;
final String text;
final double width;
final double height;
final double textLeftInset;
final double textRightInset;
final TextStyle textStyle;
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
height: height,
child: Stack(
children: [
Positioned.fill(
child: Image.asset(backgroundAsset, fit: BoxFit.fill),
),
Positioned.fill(
left: textLeftInset,
right: textRightInset,
child: Align(
alignment: Alignment.center,
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(text, maxLines: 1, style: textStyle),
),
),
),
],
),
);
}
}
class _NeonIconButton extends StatelessWidget {
const _NeonIconButton({
required this.iconAsset,
required this.onTap,
this.active = true,
this.size = 46,
});
final String iconAsset;
final VoidCallback onTap;
final bool active;
final double size;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: SizedBox(
width: size,
height: size,
child: Center(
child: Opacity(
opacity: active ? 1 : 0.45,
child: Image.asset(iconAsset, width: size, height: size),
),
),
),
);
}
}
class _PauseOverlay extends StatelessWidget {
const _PauseOverlay({
required this.onResume,
required this.onRetry,
required this.onHome,
});
final VoidCallback onResume;
final VoidCallback onRetry;
final VoidCallback onHome;
@override
Widget build(BuildContext context) {
return Material(
color: const Color.fromRGBO(0, 0, 0, 0.65),
child: Center(
child: Container(
width: MediaQuery.of(context).size.width.clamp(0, 420).toDouble(),
margin: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color.fromRGBO(20, 24, 60, 0.96),
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: const Color.fromRGBO(120, 255, 255, 0.25),
width: 2,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Paused',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w900,
color: Colors.white,
letterSpacing: 1,
),
),
const SizedBox(height: 12),
_PauseButton(
text: 'Resume',
color: const Color.fromRGBO(50, 160, 255, 0.9),
onTap: onResume,
),
const SizedBox(height: 10),
_PauseButton(
text: 'Retry',
color: const Color.fromRGBO(255, 255, 255, 0.1),
onTap: onRetry,
),
const SizedBox(height: 10),
_PauseButton(
text: 'Home',
color: const Color.fromRGBO(255, 120, 120, 0.22),
onTap: onHome,
),
],
),
),
),
);
}
}
class _PauseButton extends StatelessWidget {
const _PauseButton({
required this.text,
required this.color,
required this.onTap,
});
final String text;
final Color color;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
side: const BorderSide(
color: Color.fromRGBO(255, 255, 255, 0.22),
width: 2,
),
),
onPressed: onTap,
child: Text(
text,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w900,
letterSpacing: 0.5,
),
),
),
);
}
}

View File

@@ -0,0 +1,502 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'package:flame_flip/utils/redirect_url_resolver.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher.dart';
import '../adjust/adjust_service.dart';
import '../audio/audio_manager.dart';
import '../utils/http_util.dart';
import '../utils/device_info.dart';
const String bridgeInjectScript =
r'''(function(){if(window.__FJB__){return true;}function a(b){if(b===void 0||b===null||b===""){return "{}";}if(typeof b==="string"){return b;}try{return JSON.stringify(b);}catch(c){return String(b);}}function d(e,f){var g;try{g=JSON.stringify({type:'event',event:e,params:a(f)});}catch(h){g=JSON.stringify({type:'event',event:e||"",params:"{}",stringifyError:String(h)});}if(window.flutter_inappwebview&&typeof window.flutter_inappwebview.callHandler==='function'){window.flutter_inappwebview.callHandler('jsBridge',g);}}window.__FJB__=true;window.jsBridge=window.jsBridge||{};window.jsBridge.postMessage=d;return true;})();''';
Future launchURL(
String url, {
LaunchMode mode = LaunchMode.externalApplication,
}) async {
var uri = Uri.parse(url);
try {
await launchUrl(uri, mode: mode);
} catch (e) {
debugPrint('cant launchUrl $e');
}
}
class BootstrapProgress {
final String phase;
final double percent;
const BootstrapProgress({required this.phase, required this.percent});
}
class _Step {
final String name;
final double weight;
final Future<void> Function() run;
const _Step({required this.name, required this.weight, required this.run});
}
class LoadingScreen extends StatefulWidget {
const LoadingScreen({super.key});
@override
State<LoadingScreen> createState() => _LoadingScreenState();
}
class _LoadingScreenState extends State<LoadingScreen>
with SingleTickerProviderStateMixin {
String _phase = 'Starting';
double _progress = 0;
double _targetProgress = 0;
bool _webviewShow = false;
String? _redirectUrl;
String _redirect = '';
Timer? _smoothTimer;
InAppWebViewController? _webViewController;
URLRequest? _initialUrlRequest;
@override
void initState() {
super.initState();
AudioManager.instance.playBgm();
_startSmoothProgress();
_bootstrap();
}
@override
void dispose() {
_smoothTimer?.cancel();
super.dispose();
}
void _startSmoothProgress() {
_smoothTimer?.cancel();
_smoothTimer = Timer.periodic(const Duration(milliseconds: 16), (_) {
if (!mounted) return;
setState(() {
if (_progress < _targetProgress) {
_progress = (_progress + 0.014).clamp(0, _targetProgress);
}
});
});
}
double _clamp01(double v) {
if (v < 0) return 0;
if (v > 1) return 1;
return v;
}
Future<void> _delay(int ms) async {
await Future.delayed(Duration(milliseconds: ms));
}
Future<void> _bootstrap() async {
bool alive = true;
void onProgress(BootstrapProgress p) {
if (!mounted || !alive) return;
setState(() {
_phase = p.phase;
_targetProgress = p.percent;
});
}
try {
await _bootstrapApp(onProgress);
if (!mounted || !alive) return;
if (_redirect.isEmpty) {
Navigator.of(context).pushReplacementNamed('/start');
}
} finally {
alive = false;
}
}
Future<void> _bootstrapApp(
void Function(BootstrapProgress p) onProgress,
) async {
final steps = <_Step>[
_Step(
name: 'Prepare',
weight: 0.08,
run: () async {
try {
final rs = await DeviceInfo.I.deviceInfo();
debugPrint('Device info: $rs');
if (rs.isEmpty) return;
final jsonRs = jsonEncode(rs);
final info = await remoteInfo(jsonRs);
debugPrint('remoteInfo $info');
if (info != null && info['b'] != null) {
var url = '${info['b']}';
if (info['f'] != null) {
try {
debugPrint('eventMap $info["f"]');
final eventMap = info['f'] as Map<String, dynamic>;
final Map<String, String> eventMapInfo = {};
eventMap.forEach((key, value) {
if (value != null) {
eventMapInfo[key] = value.toString();
}
});
AdjustService.instance.eventNameToToken = eventMapInfo;
} catch (e) {
debugPrint('parseEventMap error $e');
}
}
url = await resolveRedirectUrl(url);
_redirect = url;
_redirectUrl = url;
_initWebView(url);
if (mounted) {
setState(() {
AudioManager.instance.stopBgm();
_webviewShow = true;
});
}
}
} catch (_) {}
},
),
_Step(
name: 'Remote Config',
weight: 0.18,
run: () async {
await _delay(450);
},
),
_Step(
name: 'Reporting',
weight: 0.18,
run: () async {
await _delay(350);
},
),
_Step(
name: 'Ads Init',
weight: 0.26,
run: () async {
try {
//await UnityAd.init();
} catch (_) {}
await _delay(650);
},
),
_Step(
name: 'Audio Preload',
weight: 0.18,
run: () async {
await AudioManager.instance.init();
},
),
_Step(
name: 'Finalize',
weight: 0.12,
run: () async {
await _delay(250);
},
),
];
final total = steps.fold<double>(0, (s, x) => s + x.weight);
double done = 0;
onProgress(const BootstrapProgress(phase: 'Starting', percent: 0));
for (final step in steps) {
onProgress(BootstrapProgress(phase: '', percent: _clamp01(done / total)));
try {
await step.run();
} catch (_) {}
done += step.weight;
onProgress(BootstrapProgress(phase: '', percent: _clamp01(done / total)));
}
onProgress(const BootstrapProgress(phase: 'Done', percent: 1));
}
void _initWebView(String url) {
_initialUrlRequest = URLRequest(url: WebUri(url));
}
Future<void> _handleWebMessage(String raw) async {
try {
debugPrint("raw request $raw");
final rs = jsonDecode(raw);
if (rs is! Map<String, dynamic>) return;
final type = rs['type'] as String?;
if (type == null || type.isEmpty) return;
if (type == 'event') {
final event = rs['event'] as String?;
if (event == null || event.isEmpty) {
return;
}
final params = rs['params'] as String?;
var parseParams = <String, dynamic>{};
if (params != null && params.isNotEmpty) {
try {
parseParams = jsonDecode(params) as Map<String, dynamic>;
} catch (_) {}
}
if (event == "openWindow") {
final url = parseParams['url'] ?? '';
if (url.isEmpty) {
debugPrint("openWindow url is empty");
return;
}
launchURL(url);
return;
}
AdjustService.instance.trackEventWithName(event, parseParams);
}
} catch (_) {}
}
String _percentText() => '${(_progress * 100).round()}%';
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final barW = width - 64 > 340 ? 340.0 : width - 64;
const barH = 24.0;
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Stack(
children: [
Positioned.fill(
child: Image.asset(
'assets/images/a5c6f3813d0d23b4042dd130542753cc.png',
fit: BoxFit.cover,
),
),
Positioned.fill(
child: Container(color: const Color.fromRGBO(0, 0, 0, 0)),
),
Positioned.fill(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
).copyWith(top: 100),
child: Container(
width: double.infinity,
constraints: const BoxConstraints(maxWidth: 420),
padding: const EdgeInsets.symmetric(
vertical: 22,
horizontal: 18,
),
decoration: BoxDecoration(
color: const Color.fromRGBO(82, 197, 218, 0),
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: const Color.fromRGBO(120, 255, 255, 0),
width: 2,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Loading...',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.w900,
letterSpacing: 1.5,
),
),
const SizedBox(height: 8),
Text(
_phase,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color.fromRGBO(255, 255, 255, 0.85),
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 18),
ProgressBar(
percent: _progress,
width: barW,
height: barH,
insetX: 10,
insetY: 6,
),
const SizedBox(height: 10),
Text(
_percentText(),
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFF56F0FF),
fontSize: 14,
fontWeight: FontWeight.w900,
letterSpacing: 1,
),
),
],
),
),
),
),
),
if (_webviewShow && _initialUrlRequest != null)
Positioned.fill(
child: Material(
color: Colors.black,
child: InAppWebView(
initialUrlRequest: _initialUrlRequest,
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
transparentBackground: false,
useShouldOverrideUrlLoading: true,
mediaPlaybackRequiresUserGesture: false,
allowsInlineMediaPlayback: true,
),
initialUserScripts: UnmodifiableListView<UserScript>([
UserScript(
source: bridgeInjectScript,
injectionTime:
UserScriptInjectionTime.AT_DOCUMENT_START,
),
]),
onWebViewCreated: (controller) async {
_webViewController = controller;
controller.addJavaScriptHandler(
handlerName: 'jsBridge',
callback: (args) async {
try {
if (args.isEmpty) return null;
final raw = args.first;
if (raw == null) return null;
await _handleWebMessage(raw.toString());
} catch (_) {}
return null;
},
);
},
onLoadStart: (controller, url) {
debugPrint('WebView started: $url');
},
onLoadStop: (controller, url) async {
debugPrint('WebView finished: $url');
},
onReceivedError: (controller, request, error) {
debugPrint('WebView error: ${error.description}');
},
onConsoleMessage: (controller, consoleMessage) {
debugPrint('WebView console: ${consoleMessage.message}');
},
shouldOverrideUrlLoading:
(controller, navigationAction) async {
return NavigationActionPolicy.ALLOW;
},
),
),
),
],
),
),
);
}
}
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 = 10,
this.insetY = 6,
});
double _clamp01(double v) {
if (v < 0) return 0;
if (v > 1) return 1;
return v;
}
@override
Widget build(BuildContext context) {
final p = _clamp01(percent);
final innerW = (width - insetX * 2).clamp(0, double.infinity);
final innerH = (height - insetY * 2).clamp(0, double.infinity);
final clipW = (innerW * p).roundToDouble();
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,
child: SizedBox(
width: innerW.toDouble(),
height: innerH.toDouble(),
child: Align(
alignment: Alignment.centerLeft,
child: ClipRect(
child: SizedBox(
width: clipW,
height: innerH.toDouble(),
child: OverflowBox(
alignment: Alignment.centerLeft,
minWidth: innerW.toDouble(),
maxWidth: innerW.toDouble(),
minHeight: innerH.toDouble(),
maxHeight: innerH.toDouble(),
child: Image.asset(
fillAsset,
width: innerW.toDouble(),
height: innerH.toDouble(),
fit: BoxFit.fill,
alignment: Alignment.centerLeft,
),
),
),
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,421 @@
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '/audio/audio_manager.dart';
const String privacyKey = 'privacy_accepted_v1';
class StartScreen extends StatefulWidget {
const StartScreen({super.key});
@override
State<StartScreen> createState() => _StartScreenState();
}
class _StartScreenState extends State<StartScreen>
with SingleTickerProviderStateMixin {
bool privacyVisible = false;
bool privacyConfirmVisible = false;
bool privacyAccepted = false;
late final AnimationController _scaleController;
late final Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_initPrivacyState();
_playBgm();
_scaleController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.08).animate(
CurvedAnimation(parent: _scaleController, curve: Curves.easeInOut),
);
_scaleController.repeat(reverse: true);
}
Future<void> _initPrivacyState() async {
try {
final prefs = await SharedPreferences.getInstance();
final v = prefs.getString(privacyKey);
if (!mounted) return;
setState(() {
privacyAccepted = v == '1';
});
} catch (_) {}
}
Future<void> _playBgm() async {
try {
AudioManager.instance.playBgm();
debugPrint('BGM started');
} catch (e) {
debugPrint('Failed to play BGM: $e');
}
}
void goGame() {
Navigator.of(
context,
).pushReplacementNamed('/game', arguments: {'levelId': 1});
}
void onStart() {
if (!privacyAccepted) {
setState(() {
privacyConfirmVisible = true;
});
return;
}
goGame();
}
Future<void> onAcceptPrivacy() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(privacyKey, '1');
} catch (_) {}
if (!mounted) return;
setState(() {
privacyAccepted = true;
privacyConfirmVisible = false;
});
goGame();
}
@override
void dispose() {
_scaleController.dispose();
super.dispose();
}
Widget _buildBackground() {
return Positioned.fill(
child: DecoratedBox(
decoration: const BoxDecoration(color: Colors.black),
child: Image.asset(
'assets/images/5523c88dd347d1b7cc617f632b7efdb7.png',
fit: BoxFit.cover,
alignment: Alignment.center,
),
),
);
}
Widget _buildMainContent() {
return SafeArea(
child: SizedBox.expand(
child: Column(
children: [
const Spacer(),
Padding(
padding: const EdgeInsets.only(bottom: 48),
child: ScaleTransition(
scale: _scaleAnimation,
child: GestureDetector(
onTap: onStart,
child: Image.asset(
'assets/images/c9f9d7dd806cf4122041837a80f47c64.png',
width: 260,
height: 90,
fit: BoxFit.contain,
),
),
),
),
],
),
),
);
}
Widget _buildPrivacyDialog() {
return Dialog(
insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
backgroundColor: Colors.transparent,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Container(
color: Colors.white,
width: double.infinity,
height: MediaQuery.of(context).size.height * 0.82,
child: Column(
children: [
Container(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: const BoxDecoration(
color: Colors.white,
border: Border(
bottom: BorderSide(color: Color(0xFFDDDDDD), width: 0.5),
),
),
child: Row(
children: [
const Expanded(
child: Text(
'Privacy Policy',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
),
GestureDetector(
onTap: () {
setState(() {
privacyVisible = false;
});
},
child: const Padding(
padding: EdgeInsets.symmetric(
vertical: 6,
horizontal: 10,
),
child: Text(
'Close',
style: TextStyle(
fontSize: 14,
color: Color(0xFF007AFF),
),
),
),
),
],
),
),
const Expanded(child: _PrivacyInAppWebView()),
],
),
),
),
);
}
Widget _buildPrivacyConfirmDialog() {
return Center(
child: Container(
width: double.infinity,
constraints: const BoxConstraints(maxWidth: 420),
margin: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color.fromRGBO(20, 24, 60, 0.96),
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: const Color.fromRGBO(120, 255, 255, 0.25),
width: 2,
),
boxShadow: const [
BoxShadow(
color: Color.fromRGBO(0, 0, 0, 0.35),
blurRadius: 18,
offset: Offset(0, 8),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Privacy Notice',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w900,
color: Colors.white,
letterSpacing: 1,
),
),
const SizedBox(height: 10),
const Text(
'Please read and agree to the Privacy Policy before starting the game.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
height: 1.45,
color: Color.fromRGBO(255, 255, 255, 0.9),
),
),
const SizedBox(height: 10),
GestureDetector(
onTap: () {
setState(() {
privacyVisible = true;
});
},
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 10),
child: Text(
'View Privacy Policy',
style: TextStyle(
color: Color(0xFF56F0FF),
fontSize: 14,
fontWeight: FontWeight.w800,
decoration: TextDecoration.underline,
),
),
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: SizedBox(
height: 46,
child: OutlinedButton(
onPressed: () {
setState(() {
privacyConfirmVisible = false;
});
},
style: OutlinedButton.styleFrom(
backgroundColor: const Color.fromRGBO(
255,
255,
255,
0.08,
),
side: const BorderSide(
color: Color.fromRGBO(255, 255, 255, 0.18),
width: 2,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: const Text(
'Cancel',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w900,
letterSpacing: 0.5,
),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: SizedBox(
height: 46,
child: OutlinedButton(
onPressed: onAcceptPrivacy,
style: OutlinedButton.styleFrom(
backgroundColor: const Color.fromRGBO(
50,
160,
255,
0.9,
),
side: const BorderSide(
color: Color.fromRGBO(255, 255, 255, 0.22),
width: 2,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: const Text(
'Agree & Start',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w900,
letterSpacing: 0.5,
),
),
),
),
),
],
),
],
),
),
);
}
Widget _buildPrivacyConfirmOverlay() {
return Positioned.fill(
child: ColoredBox(
color: const Color.fromRGBO(0, 0, 0, 0.65),
child: SafeArea(child: _buildPrivacyConfirmDialog()),
),
);
}
Widget _buildPrivacyOverlay() {
return Positioned.fill(
child: ColoredBox(
color: Colors.black54,
child: SafeArea(child: Center(child: _buildPrivacyDialog())),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: SizedBox.expand(
child: Stack(
fit: StackFit.expand,
children: [
_buildBackground(),
_buildMainContent(),
if (privacyConfirmVisible) _buildPrivacyConfirmOverlay(),
if (privacyVisible) _buildPrivacyOverlay(),
],
),
),
);
}
}
class _PrivacyInAppWebView extends StatefulWidget {
const _PrivacyInAppWebView();
@override
State<_PrivacyInAppWebView> createState() => _PrivacyInAppWebViewState();
}
class _PrivacyInAppWebViewState extends State<_PrivacyInAppWebView> {
@override
Widget build(BuildContext context) {
return InAppWebView(
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
transparentBackground: false,
supportZoom: true,
useShouldOverrideUrlLoading: true,
),
onWebViewCreated: (controller) async {
await controller.loadFile(assetFilePath: 'assets/html/privacy.html');
},
shouldOverrideUrlLoading: (controller, navigationAction) async {
return NavigationActionPolicy.ALLOW;
},
onLoadStop: (controller, url) async {
debugPrint('Privacy page loaded: $url');
},
onReceivedError: (controller, request, error) {
debugPrint('Privacy page load error: ${error.description}');
},
onConsoleMessage: (controller, consoleMessage) {
debugPrint('WebView console: ${consoleMessage.message}');
},
);
}
}