This commit is contained in:
admin
2026-06-02 19:01:08 +08:00
commit 7222796781
205 changed files with 10850 additions and 0 deletions

View File

@@ -0,0 +1,523 @@
import 'dart:async';
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 privacyAccepted = false;
bool _isAcceptingPrivacy = false;
bool _isNavigatingToGame = 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 value = prefs.getString(privacyKey);
if (!mounted) return;
setState(() {
privacyAccepted = value == '1';
});
} catch (_) {}
}
Future<void> _persistPrivacyAccepted() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(privacyKey, '1');
} catch (_) {}
}
Future<void> _playBgm() async {
try {
AudioManager.instance.playBgm();
debugPrint('BGM started');
} catch (e) {
debugPrint('Failed to play BGM: $e');
}
}
void _goGame() {
if (_isNavigatingToGame || !mounted) return;
_isNavigatingToGame = true;
Navigator.of(context).pushReplacementNamed('/game');
}
void onStart() {
unawaited(_handleStart());
}
Future<void> _handleStart() async {
if (_isNavigatingToGame || _isAcceptingPrivacy) return;
if (privacyAccepted) {
_goGame();
return;
}
final agreed = await _showPrivacyConfirmDialog();
if (!mounted || agreed != true) return;
await _acceptPrivacyAndStart();
}
Future<void> _acceptPrivacyAndStart() async {
if (_isAcceptingPrivacy || _isNavigatingToGame) return;
setState(() {
_isAcceptingPrivacy = true;
privacyAccepted = true;
});
unawaited(_persistPrivacyAccepted());
await Future<void>.delayed(const Duration(milliseconds: 220));
if (!mounted) return;
_goGame();
}
Future<bool?> _showPrivacyConfirmDialog() {
return showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return Dialog(
insetPadding: const EdgeInsets.symmetric(horizontal: 20),
backgroundColor: Colors.transparent,
child: Container(
width: double.infinity,
constraints: const BoxConstraints(maxWidth: 420),
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: () async {
final agreed = await _showPrivacyDialog();
if (!dialogContext.mounted || agreed != true) return;
Navigator.of(dialogContext).pop(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: () =>
Navigator.of(dialogContext).pop(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: () =>
Navigator.of(dialogContext).pop(true),
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,
),
),
),
),
),
],
),
],
),
),
);
},
);
}
Future<bool?> _showPrivacyDialog() {
return showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
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(dialogContext).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: () => Navigator.of(dialogContext).pop(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()),
Container(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
decoration: const BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(color: Color(0xFFDDDDDD), width: 0.5),
),
),
child: Row(
children: [
Expanded(
child: SizedBox(
height: 46,
child: OutlinedButton(
onPressed: () =>
Navigator.of(dialogContext).pop(false),
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: Color(0xFFBFC6D4),
width: 1.2,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'Close',
style: TextStyle(
color: Color(0xFF3B4657),
fontSize: 15,
fontWeight: FontWeight.w700,
),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: SizedBox(
height: 46,
child: FilledButton(
onPressed: () =>
Navigator.of(dialogContext).pop(true),
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFF2E8BFF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'Agree',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w800,
),
),
),
),
),
],
),
),
],
),
),
),
);
},
);
}
@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/bg/freecell_bg3.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/bg/start.png',
width: 260,
height: 90,
fit: BoxFit.contain,
),
),
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: SizedBox.expand(
child: Stack(
fit: StackFit.expand,
children: [_buildBackground(), _buildMainContent()],
),
),
);
}
}
class _PrivacyInAppWebView extends StatefulWidget {
const _PrivacyInAppWebView();
@override
State<_PrivacyInAppWebView> createState() => _PrivacyInAppWebViewState();
}
class _PrivacyInAppWebViewState extends State<_PrivacyInAppWebView> {
bool _isLoaded = false;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const BoxDecoration(color: Colors.white),
child: Stack(
fit: StackFit.expand,
children: [
AnimatedOpacity(
opacity: _isLoaded ? 1 : 0,
duration: const Duration(milliseconds: 180),
child: 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 {
if (!mounted) return;
setState(() {
_isLoaded = true;
});
debugPrint('Privacy page loaded: $url');
},
onReceivedError: (controller, request, error) {
if (!mounted) return;
setState(() {
_isLoaded = true;
});
debugPrint('Privacy page load error: ${error.description}');
},
onConsoleMessage: (controller, consoleMessage) {
debugPrint('WebView console: ${consoleMessage.message}');
},
),
),
if (!_isLoaded)
const ColoredBox(
color: Colors.white,
child: Center(
child: SizedBox(
width: 26,
height: 26,
child: CircularProgressIndicator(strokeWidth: 2.6),
),
),
),
],
),
);
}
}