diff --git a/android/app/src/main/kotlin/io/openim/flutter/openim/MainActivity.kt b/android/app/src/main/kotlin/io/openim/flutter/openim/MainActivity.kt index 089a7f3..6b5230d 100644 --- a/android/app/src/main/kotlin/io/openim/flutter/openim/MainActivity.kt +++ b/android/app/src/main/kotlin/io/openim/flutter/openim/MainActivity.kt @@ -31,10 +31,10 @@ class MainActivity : FlutterActivity() { private val WEBVIEW_CACHE_CHANNEL = "io.openim.flutter.im_webview_app/webview_cache" private val TAG = "MainActivity" private val MAX_BRANDING_ICON_SIZE = 192 - private val WEBVIEW_DATA_DIRECTORY_SUFFIX = "h5_shell_fresh_profile_v4" + private val WEBVIEW_DATA_DIRECTORY_SUFFIX = "h5_shell_fresh_profile_v5" private val WEBVIEW_STORAGE_RESET_PREFS = "h5_shell_webview_storage" private val WEBVIEW_STORAGE_RESET_KEY = "reset_version" - private val WEBVIEW_STORAGE_RESET_VERSION = 4 + private val WEBVIEW_STORAGE_RESET_VERSION = 5 override fun onCreate(savedInstanceState: android.os.Bundle?) { configureWebViewDataDirectory() diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 8d831a4..0502675 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -21,7 +21,11 @@ class AppConfig { final host = environmentHosts.isNotEmpty ? environmentHosts.first : 'h5-im.imharry.work'; - return _normalizeHomeUrl(host); + final uri = Uri.parse(_normalizeHomeUrl(host)); + final queryParameters = Map.from(uri.queryParameters) + ..['_h5_t'] = DateTime.now().millisecondsSinceEpoch.toString(); + + return uri.replace(queryParameters: queryParameters).toString(); } static String withFreshShellParams(String url) { diff --git a/lib/main.dart b/lib/main.dart index 070b24b..1fb3ea5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,8 +11,48 @@ import 'config/app_config.dart'; const _shellBackground = Color(0xFFF8FBFF); const _shellAccent = Color(0xFF0089FF); const _shellSubText = Color(0xFF8E9AB0); +const _showH5DebugOverlay = bool.fromEnvironment( + 'H5_SHELL_DEBUG', + defaultValue: true, +); const _shellBrandingChannel = MethodChannel('io.openim.flutter.im_webview_app/shell_branding'); +const _h5MountedCheckScript = r''' +(() => document.body?.classList.contains('app-mounted') === true)(); +'''; +const _h5SnapshotScript = r''' +(() => { + const shrinkAssetUrl = (value) => { + try { + const absolute = new URL(value, window.location.href).href; + const match = absolute.match(/\/assets\/[^?#]+/); + return match ? match[0] : absolute; + } catch (_) { + return value || ''; + } + }; + + const scripts = Array.from(document.scripts) + .map((script) => script.src) + .filter(Boolean) + .map(shrinkAssetUrl) + .filter((src) => src.includes('/assets/')); + const bodyText = (document.body?.innerText || '') + .replace(/\s+/g, ' ') + .slice(0, 180); + let shellBrand = ''; + try { + shellBrand = window.sessionStorage.getItem('OPENIM_FLUTTER_SHELL_BRAND') || ''; + } catch (_) {} + + return JSON.stringify({ + href: window.location.href, + scripts, + bodyText, + shellBrand, + }); +})(); +'''; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -97,6 +137,7 @@ class _H5ShellPageState extends State { int _progress = 0; String? _loadError; + String _h5DebugText = 'H5 loading...'; bool _showShellCover = true; bool _shellBrandingLoaded = false; late ShellBranding _shellBranding; @@ -161,7 +202,9 @@ class _H5ShellPageState extends State { Future _handlePageFinished() async { await _loadShellBrandingIfNeeded(); + await _waitForH5Mounted(); await _syncShellBranding(); + await _updateH5DebugSnapshot(); if (mounted) { setState(() { _progress = 100; @@ -204,6 +247,58 @@ class _H5ShellPageState extends State { return _runJavaScriptSafely(script); } + Future _waitForH5Mounted() async { + for (var index = 0; index < 20; index += 1) { + try { + final result = await _controller.runJavaScriptReturningResult( + _h5MountedCheckScript, + ); + if (result == true || result.toString() == 'true') { + return; + } + } catch (_) {} + await Future.delayed(const Duration(milliseconds: 100)); + } + } + + Future _updateH5DebugSnapshot() async { + if (!_showH5DebugOverlay) { + return; + } + + try { + final result = await _controller.runJavaScriptReturningResult( + _h5SnapshotScript, + ); + final snapshot = _decodeJavaScriptStringResult(result); + debugPrint('[H5Shell] snapshot: $snapshot'); + if (mounted) { + setState(() => _h5DebugText = snapshot); + } + } catch (error) { + debugPrint('[H5Shell] snapshot failed: $error'); + if (mounted) { + setState(() => _h5DebugText = 'H5 snapshot failed: $error'); + } + } + } + + String _decodeJavaScriptStringResult(Object? result) { + if (result == null) { + return ''; + } + if (result is String) { + try { + final decoded = jsonDecode(result); + if (decoded is String) { + return decoded; + } + } catch (_) {} + return result; + } + return result.toString(); + } + Future _loadHome() async { await _loadUrl(_freshHomeUrl()); } @@ -290,6 +385,15 @@ class _H5ShellPageState extends State { message: _loadError!, onRetry: () => unawaited(_loadHome()), ), + if (_showH5DebugOverlay) + Positioned( + left: 8, + right: 8, + bottom: 10, + child: IgnorePointer( + child: _H5DebugOverlay(text: _h5DebugText), + ), + ), ], ), ), @@ -339,6 +443,36 @@ class _ErrorPanel extends StatelessWidget { } } +class _H5DebugOverlay extends StatelessWidget { + const _H5DebugOverlay({required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.72), + borderRadius: BorderRadius.circular(6), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: Text( + text, + maxLines: 5, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 10, + height: 1.25, + letterSpacing: 0, + ), + ), + ), + ); + } +} + class _ShellFallback extends StatelessWidget { const _ShellFallback();