import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter_android/webview_flutter_android.dart'; import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; const _homeUrl = 'https://h5-im.imharry.work/'; const _stopWebMediaScript = r''' (() => { try { window.__stopOpenIMVoicePlayback?.(); } catch (_) {} document.querySelectorAll('audio, video').forEach((media) => { try { media.pause(); media.currentTime = 0; } catch (_) {} }); })(); '''; void main() { WidgetsFlutterBinding.ensureInitialized(); SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.dark, systemNavigationBarColor: Colors.white, systemNavigationBarIconBrightness: Brightness.dark, ), ); runApp(const ImWebViewApp()); } class ImWebViewApp extends StatelessWidget { const ImWebViewApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: '集中营', debugShowCheckedModeBanner: false, theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1F6FEB)), scaffoldBackgroundColor: Colors.white, useMaterial3: true, ), home: const H5ShellPage(), ); } } class H5ShellPage extends StatefulWidget { const H5ShellPage({super.key}); @override State createState() => _H5ShellPageState(); } class _H5ShellPageState extends State with WidgetsBindingObserver { late final WebViewController _controller; int _progress = 0; String? _loadError; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _controller = _buildController()..loadRequest(Uri.parse(_homeUrl)); } @override void dispose() { 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()); } } WebViewController _buildController() { PlatformWebViewControllerCreationParams params = const PlatformWebViewControllerCreationParams(); if (WebViewPlatform.instance is WebKitWebViewPlatform) { params = WebKitWebViewControllerCreationParams( allowsInlineMediaPlayback: true, mediaTypesRequiringUserAction: const {}, ); } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { params = AndroidWebViewControllerCreationParams .fromPlatformWebViewControllerCreationParams(params); } final controller = WebViewController.fromPlatformCreationParams( params, onPermissionRequest: (request) { unawaited(_handleWebViewPermissionRequest(request)); }, ) ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(Colors.white) ..setNavigationDelegate( NavigationDelegate( onProgress: (progress) { if (mounted) { setState(() => _progress = progress); } }, onPageStarted: (_) { if (mounted) { setState(() { _loadError = null; _progress = 0; }); } }, onPageFinished: (_) { if (mounted) { setState(() => _progress = 100); } }, onWebResourceError: (error) { if (error.isForMainFrame ?? true) { if (mounted) { setState(() => _loadError = error.description); } } }, onUrlChange: (_) { unawaited(_stopWebMedia()); }, onNavigationRequest: _handleNavigationRequest, ), ); final platformController = controller.platform; if (platformController is AndroidWebViewController) { AndroidWebViewController.enableDebugging(false); unawaited(platformController.setMediaPlaybackRequiresUserGesture(false)); unawaited(platformController.setGeolocationEnabled(true)); unawaited( platformController.setGeolocationPermissionsPromptCallbacks( onShowPrompt: (_) async { final allowed = await _requestPermission(Permission.locationWhenInUse); return GeolocationPermissionsResponse( allow: allowed, retain: allowed, ); }, ), ); } return controller; } Future _runJavaScriptSafely(String source) async { try { await _controller.runJavaScript(source); } catch (_) { // WebView can reject JavaScript while a page is still navigating. } } Future _stopWebMedia() { return _runJavaScriptSafely(_stopWebMediaScript); } Future _handleNavigationRequest( NavigationRequest request, ) async { unawaited(_stopWebMedia()); final uri = Uri.tryParse(request.url); if (uri == null) { return NavigationDecision.prevent; } const webSchemes = {'http', 'https', 'about', 'data'}; if (webSchemes.contains(uri.scheme)) { return NavigationDecision.navigate; } try { await launchUrl(uri, mode: LaunchMode.externalApplication); } catch (_) { // Ignore unsupported custom schemes so the WebView does not navigate to // an error page. } return NavigationDecision.prevent; } Future _handleWebViewPermissionRequest( WebViewPermissionRequest request, ) async { final permissions = []; if (request.types.contains(WebViewPermissionResourceType.camera)) { permissions.add(Permission.camera); } if (request.types.contains(WebViewPermissionResourceType.microphone)) { permissions.add(Permission.microphone); } final allowed = permissions.isEmpty || await Future.wait(permissions.map(_requestPermission)) .then((results) => results.every((allowed) => allowed)); if (allowed) { await request.grant(); } else { await request.deny(); } } Future _requestPermission(Permission permission) async { final status = await permission.request(); return status.isGranted || status.isLimited; } Future _handleBackNavigation() async { await _stopWebMedia(); if (await _controller.canGoBack()) { await _controller.goBack(); } else { await SystemNavigator.pop(); } } @override Widget build(BuildContext context) { return PopScope( canPop: false, onPopInvokedWithResult: (didPop, _) { if (!didPop) { unawaited(_handleBackNavigation()); } }, child: Scaffold( body: SafeArea( child: Stack( children: [ WebViewWidget(controller: _controller), if (_progress < 100) LinearProgressIndicator( value: _progress == 0 ? null : _progress / 100, minHeight: 2, ), if (_loadError != null) _ErrorPanel( message: _loadError!, onRetry: () => _controller.loadRequest(Uri.parse(_homeUrl)), ), ], ), ), ), ); } } class _ErrorPanel extends StatelessWidget { const _ErrorPanel({required this.message, required this.onRetry}); final String message; final VoidCallback onRetry; @override Widget build(BuildContext context) { return ColoredBox( color: Colors.white, child: Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.wifi_off_rounded, size: 44), const SizedBox(height: 16), const Text( '页面加载失败', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), const SizedBox(height: 8), Text( message, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 20), FilledButton( onPressed: onRetry, child: const Text('重新加载'), ), ], ), ), ), ); } }