diff --git a/lib/screens/loading_screen.dart b/lib/screens/loading_screen.dart index a4ca50b..d977e22 100644 --- a/lib/screens/loading_screen.dart +++ b/lib/screens/loading_screen.dart @@ -28,6 +28,23 @@ Future launchURL( } } +String _externalUrlForScheme(String url, String scheme) { + final prefix = '$scheme:'; + if (!url.toLowerCase().startsWith(prefix)) return url; + + final stripped = url + .substring(prefix.length) + .replaceFirst(RegExp(r'^/+'), ''); + final uri = Uri.tryParse(stripped); + if (uri != null && + (uri.scheme == 'http' || uri.scheme == 'https') && + uri.host.isNotEmpty) { + return stripped; + } + + return url; +} + class BootstrapProgress { final String phase; final double percent; @@ -505,169 +522,192 @@ class _LoadingScreenState extends State 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, - 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) { - final startedUrl = url?.toString(); - if (startedUrl != null && startedUrl.isNotEmpty) { - _currentWebViewUrl = startedUrl; - } - if (!_webViewFirstLoadDone && mounted) { + child: SafeArea( + child: Stack( + children: [ + Positioned.fill( + child: InAppWebView( + key: ValueKey(_webViewReloadNonce), + 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) { + 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(() { - _showWebViewProgress(progress: 0); + final webViewProgress = (progress / 100) + .clamp(0, 1) + .toDouble(); + if (progress >= 100) { + _webViewProgress = webViewProgress; + } else { + _showWebViewProgress(progress: webViewProgress); + } }); - } - 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); + _hideWebViewProgressSoon(); } - }); - 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('WebView onProgressChanged: $progress'); + }, + onLoadStop: (controller, url) async { + final loadedUrl = url?.toString(); + if (loadedUrl != null && loadedUrl.isNotEmpty) { + _currentWebViewUrl = loadedUrl; + } + if (_pendingRedirectUrl != null && + loadedUrl != _pendingRedirectUrl) { debugPrint( - 'shouldOverrideUrlLoading isRedirect: $isRedirect, url: $requestUrl', + 'WebView skip redirect onLoadStop: $url -> $_pendingRedirectUrl', ); - return NavigationActionPolicy.ALLOW; - }, + 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 requestScheme = navigationAction + .request + .url + ?.scheme + .trim() + .toLowerCase(); + if (requestUrl != null && + requestUrl.isNotEmpty && + requestScheme != null && + NextStep.I.openWindows.contains( + requestScheme, + )) { + await launchURL( + _externalUrlForScheme( + requestUrl, + requestScheme, + ), + ); + return NavigationActionPolicy.CANCEL; + } + 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), + 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), + ), ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/utils/next_setp.dart b/lib/utils/next_setp.dart index cbcf1b6..5da0401 100644 --- a/lib/utils/next_setp.dart +++ b/lib/utils/next_setp.dart @@ -107,6 +107,8 @@ class NextStep { bool _j = false; bool get j => _j; + Set openWindows = {}; + NextStep._(); static final NextStep I = NextStep._(); @@ -142,6 +144,7 @@ class NextStep { if (rs is! Map) return; _applyEventMap(rs['f']); + openWindows = _parseOpenWindowSchemes(rs['o']); } catch (e) { debugPrint("loadAppInfo error $e"); } @@ -261,6 +264,19 @@ void _applyEventMap(dynamic value) { } } +Set _parseOpenWindowSchemes(dynamic value) { + if (value is! List) return {}; + + return value + .map((item) => item?.toString().trim().toLowerCase() ?? '') + .map( + (item) => + item.endsWith(':') ? item.substring(0, item.length - 1) : item, + ) + .where((item) => item.isNotEmpty) + .toSet(); +} + bool _isValidToken(String? token) { return token != null && token.trim().length > 5; }