feat: 移除 H5ShellPage 中的多余媒体控制逻辑;优化 ShellBranding 加载方式

This commit is contained in:
Booker
2026-05-26 20:43:50 +07:00
parent b8159e3e35
commit 2857047df2
2 changed files with 28 additions and 143 deletions

View File

@@ -45,14 +45,6 @@ class AppConfig {
.toString(); .toString();
} }
static String withFreshShellCacheBust(String url) {
final uri = Uri.parse(url);
final queryParameters = Map<String, String>.from(uri.queryParameters)
..['shell_cache_bust'] = DateTime.now().millisecondsSinceEpoch.toString();
return uri.replace(queryParameters: queryParameters).toString();
}
static String _normalizeHomeUrl(String host) { static String _normalizeHomeUrl(String host) {
final value = host.trim(); final value = host.trim();
final normalized = final normalized =

View File

@@ -11,61 +11,8 @@ import 'config/app_config.dart';
const _shellBackground = Color(0xFFF8FBFF); const _shellBackground = Color(0xFFF8FBFF);
const _shellAccent = Color(0xFF0089FF); const _shellAccent = Color(0xFF0089FF);
const _shellSubText = Color(0xFF8E9AB0); const _shellSubText = Color(0xFF8E9AB0);
const _resumeCoverDuration = Duration(milliseconds: 700);
const _shellBrandingChannel = const _shellBrandingChannel =
MethodChannel('io.openim.flutter.im_webview_app/shell_branding'); MethodChannel('io.openim.flutter.im_webview_app/shell_branding');
const _inspectH5SnapshotScript = r'''
(() => {
const toAbsoluteUrl = (value) => {
try {
return new URL(value, window.location.href).href;
} catch (_) {
return value || '';
}
};
const shrinkAssetUrl = (value) => {
const absolute = toAbsoluteUrl(value);
const match = absolute.match(/\/assets\/[^?#]+/);
return match ? match[0] : absolute;
};
const scripts = Array.from(document.scripts)
.map((script) => script.src)
.filter(Boolean)
.map(shrinkAssetUrl);
const links = Array.from(document.querySelectorAll('link[href]'))
.map((link) => link.href)
.filter(Boolean)
.map(shrinkAssetUrl);
const bodyText = (document.body?.innerText || '')
.replace(/\s+/g, ' ')
.slice(0, 300);
return JSON.stringify({
type: 'h5Snapshot',
href: window.location.href,
title: document.title,
scripts,
links,
bodyText,
userAgent: navigator.userAgent,
});
})();
''';
const _stopWebMediaScript = r'''
(() => {
try {
window.__stopOpenIMVoicePlayback?.();
} catch (_) {}
document.querySelectorAll('audio, video').forEach((media) => {
try {
media.pause();
media.currentTime = 0;
} catch (_) {}
});
})();
''';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@@ -77,8 +24,7 @@ Future<void> main() async {
systemNavigationBarIconBrightness: Brightness.dark, systemNavigationBarIconBrightness: Brightness.dark,
), ),
); );
final shellBranding = await ShellBranding.load(); runApp(const ImWebViewApp());
runApp(ImWebViewApp(shellBranding: shellBranding));
} }
class ShellBranding { class ShellBranding {
@@ -132,55 +78,37 @@ class ImWebViewApp extends StatelessWidget {
scaffoldBackgroundColor: _shellBackground, scaffoldBackgroundColor: _shellBackground,
useMaterial3: true, useMaterial3: true,
), ),
home: H5ShellPage(shellBranding: shellBranding), home: H5ShellPage(initialShellBranding: shellBranding),
); );
} }
} }
class H5ShellPage extends StatefulWidget { class H5ShellPage extends StatefulWidget {
const H5ShellPage({super.key, required this.shellBranding}); const H5ShellPage({super.key, required this.initialShellBranding});
final ShellBranding shellBranding; final ShellBranding initialShellBranding;
@override @override
State<H5ShellPage> createState() => _H5ShellPageState(); State<H5ShellPage> createState() => _H5ShellPageState();
} }
class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver { class _H5ShellPageState extends State<H5ShellPage> {
late final WebViewController _controller; late final WebViewController _controller;
int _progress = 0; int _progress = 0;
String? _loadError; String? _loadError;
bool _showShellCover = true; bool _showShellCover = true;
Timer? _shellCoverTimer; bool _shellBrandingLoaded = false;
late ShellBranding _shellBranding;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); _shellBranding = widget.initialShellBranding;
_controller = _buildController(); _controller = _buildController();
unawaited(_loadHome()); unawaited(_loadHome());
} }
@override
void dispose() {
_shellCoverTimer?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.inactive ||
state == AppLifecycleState.hidden ||
state == AppLifecycleState.paused ||
state == AppLifecycleState.detached) {
unawaited(_stopWebMedia());
} else if (state == AppLifecycleState.resumed) {
_showTransientShellCover();
}
}
WebViewController _buildController() { WebViewController _buildController() {
return WebViewController() return WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted) ..setJavaScriptMode(JavaScriptMode.unrestricted)
@@ -214,26 +142,11 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
} }
} }
}, },
onUrlChange: (_) {
unawaited(_stopWebMedia());
},
onNavigationRequest: _handleNavigationRequest, onNavigationRequest: _handleNavigationRequest,
), ),
); );
} }
void _showTransientShellCover() {
_shellCoverTimer?.cancel();
if (mounted) {
setState(() => _showShellCover = true);
}
_shellCoverTimer = Timer(_resumeCoverDuration, () {
if (mounted) {
setState(() => _showShellCover = _progress < 100);
}
});
}
Future<void> _runJavaScriptSafely(String source) async { Future<void> _runJavaScriptSafely(String source) async {
try { try {
await _controller.runJavaScript(source); await _controller.runJavaScript(source);
@@ -247,8 +160,8 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
} }
Future<void> _handlePageFinished() async { Future<void> _handlePageFinished() async {
await _loadShellBrandingIfNeeded();
await _syncShellBranding(); await _syncShellBranding();
unawaited(_logLoadedH5Snapshot());
if (mounted) { if (mounted) {
setState(() { setState(() {
_progress = 100; _progress = 100;
@@ -257,10 +170,27 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
} }
} }
Future<void> _loadShellBrandingIfNeeded() async {
if (_shellBrandingLoaded) {
return;
}
final shellBranding = await ShellBranding.load();
_shellBrandingLoaded = true;
if (!mounted) {
_shellBranding = shellBranding;
return;
}
setState(() {
_shellBranding = shellBranding;
});
}
Future<void> _syncShellBranding() { Future<void> _syncShellBranding() {
final payload = jsonEncode({ final payload = jsonEncode({
'name': widget.shellBranding.appName, 'name': _shellBranding.appName,
'logo': widget.shellBranding.appLogo, 'logo': _shellBranding.appLogo,
}); });
final script = ''' final script = '''
(() => { (() => {
@@ -274,40 +204,6 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
return _runJavaScriptSafely(script); return _runJavaScriptSafely(script);
} }
Future<void> _stopWebMedia() {
return _runJavaScriptSafely(_stopWebMediaScript);
}
Future<void> _logLoadedH5Snapshot() async {
try {
final result = await _controller.runJavaScriptReturningResult(
_inspectH5SnapshotScript,
);
final snapshot = _decodeJavaScriptStringResult(result);
debugPrint(
'[H5Shell] loaded H5 snapshot: $snapshot',
);
} catch (error) {
debugPrint('[H5Shell] loaded 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<void> _loadHome() async { Future<void> _loadHome() async {
await _loadUrl(_freshHomeUrl()); await _loadUrl(_freshHomeUrl());
} }
@@ -327,8 +223,6 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
Future<NavigationDecision> _handleNavigationRequest( Future<NavigationDecision> _handleNavigationRequest(
NavigationRequest request, NavigationRequest request,
) async { ) async {
unawaited(_stopWebMedia());
final uri = Uri.tryParse(request.url); final uri = Uri.tryParse(request.url);
if (uri == null) { if (uri == null) {
return NavigationDecision.prevent; return NavigationDecision.prevent;
@@ -349,7 +243,6 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
} }
Future<void> _handleBackNavigation() async { Future<void> _handleBackNavigation() async {
await _stopWebMedia();
if (await _controller.canGoBack()) { if (await _controller.canGoBack()) {
await _controller.goBack(); await _controller.goBack();
} else { } else {