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,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}');
},
);
}
}