import 'dart:async'; // import 'dart:collection'; // import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; // import '../adjust/adjust_service.dart'; // import '../utils/aes_decrypt.dart'; import '../utils/next_setp.dart'; import '../utils/report.dart'; const String _webLoginTokenKey = 'assammzeeeass'; const int _maxWebViewProgressShowCount = 1; // 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; bool _webViewFirstLoadDone = false; bool _webViewProgressVisible = false; double _webViewProgress = 0; int _webViewProgressShowCount = 0; String? _pendingRedirectUrl; String? _currentWebViewUrl; String _redirect = ''; Timer? _smoothTimer; Timer? _webViewHideTimer; URLRequest? _initialUrlRequest; // late final UnmodifiableListView _initialUserScripts; int _webViewReloadNonce = 0; @override void initState() { super.initState(); // AudioManager.instance.playBgm(); // _initialUserScripts = UnmodifiableListView([ // UserScript( // source: _decryptScript(), // injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START, // forMainFrameOnly: true, // ), // ]); ReportService.I.reportInstall(); _startSmoothProgress(); _bootstrap(); } @override void dispose() { _smoothTimer?.cancel(); _webViewHideTimer?.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 { await NextStep.I.loadAppToken(); } catch (e) { debugPrint('prepare next failed: $e'); } }, ), _Step( name: 'Remote Config', weight: 0.18, run: () async { //await _delay(450); try { await NextStep.I.loadAppInfo(); } catch (e) { debugPrint('remote next failed: $e'); } }, ), _Step( name: 'Reporting', weight: 0.18, run: () async { //await _delay(350); 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 { final prefs = await SharedPreferences.getInstance(); final token = prefs.getString(_webLoginTokenKey)?.trim() ?? ''; final loadUrl = _urlWithToken(url, token); _initialUrlRequest = URLRequest(url: WebUri(loadUrl)); _currentWebViewUrl = loadUrl; _webViewFirstLoadDone = false; _webViewProgressVisible = false; _webViewProgress = 0; _webViewProgressShowCount = 0; _pendingRedirectUrl = null; _webViewHideTimer?.cancel(); } Future _reloadCurrentWebView() async { final reloadUrl = _currentWebViewUrl?.trim().isNotEmpty == true ? _currentWebViewUrl!.trim() : _redirect.trim(); if (reloadUrl.isEmpty) { Navigator.of(context).pushReplacementNamed('/start'); return; } _webViewHideTimer?.cancel(); setState(() { _webviewShow = false; _initialUrlRequest = null; _webViewFirstLoadDone = false; _webViewProgressVisible = false; _webViewProgress = 0; _pendingRedirectUrl = null; }); await Future.delayed(const Duration(milliseconds: 80)); if (!mounted) return; await _initWebView(reloadUrl); if (!mounted) return; setState(() { _webViewReloadNonce++; _webviewShow = true; }); } String _urlWithToken(String url, String token) { if (!NextStep.I.j) { return url; } if (token.isEmpty) return url; try { final uri = Uri.parse(url); final params = Map.from(uri.queryParameters); params['token'] = token; return uri.replace(queryParameters: params).toString(); } catch (e) { debugPrint('append web login token failed: $e'); return url; } } void _hideWebViewProgressSoon() { _webViewHideTimer?.cancel(); _webViewHideTimer = Timer(const Duration(milliseconds: 450), () { if (!mounted) return; setState(() { _webViewProgress = 1; _webViewFirstLoadDone = true; _webViewProgressVisible = false; _pendingRedirectUrl = null; }); }); } void _showWebViewProgress({double? progress}) { if (_webViewProgressVisible) { _webViewProgress = progress ?? _webViewProgress; _webViewFirstLoadDone = false; return; } if (_webViewProgressShowCount >= _maxWebViewProgressShowCount) { _webViewProgress = progress ?? _webViewProgress; return; } _webViewProgressShowCount++; _webViewProgressVisible = true; _webViewFirstLoadDone = false; _webViewProgress = progress ?? _webViewProgress; } void _reportWebViewEvent(String eventName, Map params) { ReportService.I.report(eventName, { ...params, 'currentUrl': _safeReportUrl(_currentWebViewUrl), 'redirectUrl': _safeReportUrl(_redirect), 'reloadNonce': _webViewReloadNonce, }); } String _safeReportUrl(String? url) { if (url == null || url.isEmpty) return ''; try { final uri = Uri.parse(url); final params = Map.from(uri.queryParameters); for (final key in params.keys.toList()) { final lowerKey = key.toLowerCase(); if (lowerKey.contains('token') || lowerKey == 'pass' || lowerKey == 'password') { params[key] = '***'; } } return uri.replace(queryParameters: params).toString(); } catch (_) { return 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 (_) {} // } // debugPrint('WebView event: $event'); // if (event == "openWindow") { // final url = parseParams['url'] ?? ''; // if (url.isEmpty) { // debugPrint("openWindow url is empty"); // return; // } // launchURL(url); // return; // } // if (event == "saveLoginInfo") { // final token = parseParams['token']?.toString().trim() ?? ''; // if (token.isNotEmpty) { // final prefs = await SharedPreferences.getInstance(); // await prefs.setString(_webLoginTokenKey, token); // } // return; // } // AdjustService.instance.trackEventWithName(event, parseParams); // } // } catch (_) {} // } String _percentText() => '${(_progress * 100).round()}%'; // String _decryptScript() { // return AesDecrypt.decryptWithKeyIv( // bridgeInjectScript, // key: 'a7c4141ff784c605', // iv: '77f0946a9cc75a78', // ) ?? // ''; // } @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/bg/freecell_bg3.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: Stack( children: [ Positioned.fill( child: InAppWebView( key: ValueKey(_webViewReloadNonce), initialUrlRequest: _initialUrlRequest, initialSettings: InAppWebViewSettings( javaScriptEnabled: true, transparentBackground: false, useShouldOverrideUrlLoading: true, mediaPlaybackRequiresUserGesture: false, allowsInlineMediaPlayback: true, ), // 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) { final startedUrl = url?.toString(); if (startedUrl != null && startedUrl.isNotEmpty) { _currentWebViewUrl = startedUrl; } if (!_webViewFirstLoadDone && mounted) { _webViewHideTimer?.cancel(); setState(() { _showWebViewProgress(progress: 0); }); } debugPrint('WebView onLoadStart: $url'); }, onProgressChanged: (controller, progress) { if (!mounted) return; _webViewHideTimer?.cancel(); setState(() { final webViewProgress = (progress / 100) .clamp(0, 1) .toDouble(); if (progress >= 100) { _webViewProgress = webViewProgress; } else { _showWebViewProgress(progress: webViewProgress); } }); if (progress >= 100) { _hideWebViewProgressSoon(); } debugPrint('WebView onProgressChanged: $progress'); }, onLoadStop: (controller, url) async { final loadedUrl = url?.toString(); if (loadedUrl != null && loadedUrl.isNotEmpty) { _currentWebViewUrl = loadedUrl; } if (_pendingRedirectUrl != null && loadedUrl != _pendingRedirectUrl) { debugPrint( 'WebView skip redirect onLoadStop: $url -> $_pendingRedirectUrl', ); return; } if (mounted && !_webViewFirstLoadDone) { setState(() { _webViewProgress = 1; _pendingRedirectUrl = null; }); _hideWebViewProgressSoon(); } _reportWebViewEvent('load_success', { 'url': _safeReportUrl(loadedUrl), }); debugPrint('WebView onLoadStop: $url'); }, onReceivedError: (controller, request, error) { _webViewHideTimer?.cancel(); if (mounted && !_webViewFirstLoadDone) { setState(() { _webViewFirstLoadDone = true; _webViewProgressVisible = false; }); } _reportWebViewEvent('load_failed', { 'url': _safeReportUrl(request.url.toString()), 'isForMainFrame': request.isForMainFrame, 'errorType': error.type.toString(), 'errorDescription': error.description, }); debugPrint( 'WebView onReceivedError error: ${error.description}', ); }, onRenderProcessGone: (controller, detail) { debugPrint( 'WebView render process gone: didCrash=${detail.didCrash}, priority=${detail.rendererPriorityAtExit}', ); _webViewHideTimer?.cancel(); if (!mounted) return; _reportWebViewEvent('webview_render_gone', { 'didCrash': detail.didCrash, 'rendererPriorityAtExit': detail .rendererPriorityAtExit ?.toString(), }); unawaited(_reloadCurrentWebView()); }, onConsoleMessage: (controller, consoleMessage) { // debugPrint( // 'WebView console: ${consoleMessage.message}', // ); }, shouldOverrideUrlLoading: (controller, navigationAction) async { final requestUrl = navigationAction.request.url ?.toString(); if (requestUrl != null && requestUrl.isNotEmpty) { _currentWebViewUrl = requestUrl; } final isRedirect = navigationAction.isRedirect == true; if (isRedirect && mounted) { _webViewHideTimer?.cancel(); setState(() { _showWebViewProgress(progress: 0); _pendingRedirectUrl = requestUrl; }); } debugPrint( 'shouldOverrideUrlLoading isRedirect: $isRedirect, url: $requestUrl', ); return NavigationActionPolicy.ALLOW; }, ), ), if (_webViewProgressVisible) Center( child: SizedBox( width: MediaQuery.of(context).size.width > 360 ? 320 : MediaQuery.of(context).size.width - 40, child: LinearProgressIndicator( value: _webViewProgress > 0 ? _webViewProgress : null, minHeight: 4, backgroundColor: Colors.white24, valueColor: const AlwaysStoppedAnimation( Color(0xFF56F0FF), ), ), ), ), ], ), ), ), ], ), ), ); } } 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/bg/jd1.png' : 'assets/images/bg/jd2.png'; return SizedBox( width: width, height: height, child: Stack( children: [ Positioned.fill( child: Image.asset('assets/images/bg/jd.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, ), ), ), ), ), ), ), ], ), ); } }