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 Function() run; const _Step({required this.name, required this.weight, required this.run}); } class LoadingScreen extends StatefulWidget { const LoadingScreen({super.key}); @override State createState() => _LoadingScreenState(); } class _LoadingScreenState extends State 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 _delay(int ms) async { await Future.delayed(Duration(milliseconds: ms)); } Future _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 _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; final Map 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(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 _handleWebMessage(String raw) async { try { debugPrint("raw request $raw"); final rs = jsonDecode(raw); if (rs is! Map) 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 = {}; if (params != null && params.isNotEmpty) { try { parseParams = jsonDecode(params) as Map; } 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( 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, ), ), ), ), ), ), ), ], ), ); } }