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 createState() => _StartScreenState(); } class _StartScreenState extends State with SingleTickerProviderStateMixin { bool privacyAccepted = false; bool _isAcceptingPrivacy = false; bool _isNavigatingToGame = false; late final AnimationController _scaleController; late final Animation _scaleAnimation; @override void initState() { super.initState(); _initPrivacyState(); _playBgm(); _scaleController = AnimationController( vsync: this, duration: const Duration(milliseconds: 900), ); _scaleAnimation = Tween(begin: 1.0, end: 1.08).animate( CurvedAnimation(parent: _scaleController, curve: Curves.easeInOut), ); _scaleController.repeat(reverse: true); } Future _initPrivacyState() async { try { final prefs = await SharedPreferences.getInstance(); final value = prefs.getString(privacyKey); if (!mounted) return; setState(() { privacyAccepted = value == '1'; }); } catch (_) {} } Future _persistPrivacyAccepted() async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString(privacyKey, '1'); } catch (_) {} } Future _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 _handleStart() async { if (_isNavigatingToGame || _isAcceptingPrivacy) return; if (privacyAccepted) { _goGame(); return; } final agreed = await _showPrivacyConfirmDialog(); if (!mounted || agreed != true) return; await _acceptPrivacyAndStart(); } Future _acceptPrivacyAndStart() async { if (_isAcceptingPrivacy || _isNavigatingToGame) return; setState(() { _isAcceptingPrivacy = true; privacyAccepted = true; }); unawaited(_persistPrivacyAccepted()); await Future.delayed(const Duration(milliseconds: 220)); if (!mounted) return; _goGame(); } Future _showPrivacyConfirmDialog() { return showDialog( 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 _showPrivacyDialog() { return showDialog( 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), ), ), ), ], ), ); } }