import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../controllers/core_controller.dart'; import '../../../data/sp/ix_sp.dart'; import '../../../constants/enums.dart' as enums; import '../../../../utils/log/logger.dart'; import '../utils/js_bridge.dart'; export 'package:flutter/services.dart' show rootBundle; class WebController extends GetxController { final TAG = 'WebController'; var title = ''; var url = ''; InAppWebViewController? webViewController; final isLoading = true.obs; final loadingProgress = 0.0.obs; final canGoBack = false.obs; // 添加是否可以回退的观察变量 final isFullScreen = false.obs; // 全面屏模式 // 当前的状态栏样式(响应式) final currentStatusBarStyle = const SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.light, statusBarBrightness: Brightness.light, ).obs; // final _externalSchemes = [ // 'mailto:', // 'tel:', // 'sms:', // 'geo:', // 'intent:', // 'market:' // ]; @override void onInit() { if (Get.arguments != null) { title = Get.arguments['title'] ?? ''; url = Get.arguments['url'] ?? ''; isFullScreen.value = Get.arguments['fullScreen'] ?? false; } loadUserAgent(); _listenToVPNStatusChanges(); super.onInit(); } /// 加载本地 Assets HTML 文件 Future loadAssetHtml(String assetPath) async { try { final htmlContent = await rootBundle.loadString(assetPath); if (webViewController != null) { await webViewController!.loadData( data: htmlContent, baseUrl: WebUri('about:blank'), mimeType: 'text/html', encoding: 'utf-8', ); log(TAG, '成功加载本地 HTML: $assetPath'); } } catch (e) { log(TAG, '加载本地 HTML 失败: $e'); } } Future loadUserAgent() async { initWebView(await generateUserAgent()); } Future generateUserAgent() async { try { final deviceInfo = DeviceInfoPlugin(); final packageInfo = await PackageInfo.fromPlatform(); if (Platform.isIOS) { final iosInfo = await deviceInfo.iosInfo; // 使用类似 Safari 的 UA 格式 return 'Mozilla/5.0 (iPhone; CPU iPhone OS ${iosInfo.systemVersion.replaceAll('.', '_')} like Mac OS X) ' 'AppleWebKit/605.1.15 (KHTML, like Gecko) ' 'Version/${packageInfo.version} Mobile/15E148 Safari/604.1'; } else { final androidInfo = await deviceInfo.androidInfo; // 使用简化的 Android UA 格式,避免 WebView 标识 return 'Mozilla/5.0 (Linux; Android ${androidInfo.version.release}; ${androidInfo.model}) ' 'AppleWebKit/537.36 (KHTML, like Gecko) ' 'Version/${packageInfo.version} Mobile Safari/537.36'; } } catch (e) { // 如果获取设备信息失败,返回一个通用的安全 UA return 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) ' 'AppleWebKit/605.1.15 (KHTML, like Gecko) ' 'Version/16.6 Mobile/15E148 Safari/604.1'; } } String? userAgent; void initWebView(String ua) { userAgent = ua; // InAppWebView 的初始化在 WebView 组件中完成 // 这里只需要设置 URL if (url.isNotEmpty) { // URL 将在 WebView 组件中加载 } } // 设置 WebViewController 的引用 void setWebViewController(InAppWebViewController controller) { webViewController = controller; // 设置 User Agent if (userAgent != null) { webViewController?.setSettings( settings: InAppWebViewSettings( userAgent: userAgent!, javaScriptEnabled: true, useShouldOverrideUrlLoading: true, useOnLoadResource: true, mediaPlaybackRequiresUserGesture: false, allowsInlineMediaPlayback: true, iframeAllow: "camera; microphone", iframeAllowFullscreen: true, ), ); } } // 检查是否可以回退的方法 Future checkCanGoBack() async { try { if (webViewController != null) { final canBack = await webViewController!.canGoBack(); canGoBack.value = canBack; } } catch (e) { // 如果检查失败,默认设置为false canGoBack.value = false; } return canGoBack.value; } // 回退方法 Future goBack() async { if (canGoBack.value && webViewController != null) { await webViewController!.goBack(); // 回退后重新检查状态 checkCanGoBack(); } } // 刷新方法 Future reload() async { if (webViewController != null) { await webViewController!.reload(); } } String? parseIntentUrl(String url) { if (!url.startsWith("intent://")) return url; // 提取 scheme final schemeMatch = RegExp(r"scheme=([a-zA-Z0-9.+-]+);").firstMatch(url); final scheme = schemeMatch?.group(1) ?? "https"; // 替换 intent:// -> scheme:// var cleanUrl = url .replaceFirst("intent://", "$scheme://") .replaceAll(RegExp(r"#Intent;.*;end$"), ""); return cleanUrl; } String? parseFallbackUrl(String url) { final match = RegExp(r"S\.browser_fallback_url=([^;]+);").firstMatch(url); return match != null ? Uri.decodeComponent(match.group(1)!) : null; } // ==================== JS Bridge 功能 ==================== /// 设置 JavaScript Handlers void setupJavaScriptHandlers(InAppWebViewController controller) { // 设置状态栏颜色 controller.addJavaScriptHandler( handlerName: JSBridgeConstants.setStatusBarColor, callback: (args) async { return await _handleSetStatusBarColor(args); }, ); // 退出 WebView controller.addJavaScriptHandler( handlerName: JSBridgeConstants.exitWebView, callback: (args) async { return await _handleExitWebView(args); }, ); // 连接 VPN controller.addJavaScriptHandler( handlerName: JSBridgeConstants.connectVPN, callback: (args) async { return await _handleConnectVPN(args); }, ); // 断开 VPN controller.addJavaScriptHandler( handlerName: JSBridgeConstants.disconnectVPN, callback: (args) async { return await _handleDisconnectVPN(args); }, ); // 打开指定 App controller.addJavaScriptHandler( handlerName: JSBridgeConstants.openApp, callback: (args) async { return await _handleOpenApp(args); }, ); // 获取 VPN 状态 controller.addJavaScriptHandler( handlerName: JSBridgeConstants.getVPNStatus, callback: (args) async { return await _handleGetVPNStatus(args); }, ); log(TAG, 'JavaScript Handlers 已设置'); } /// 注入原生属性到 JS Future injectNativeAttrs() async { if (webViewController == null) return; try { // 获取状态栏和底部安全区域高度 double statusBarHeight = 0; double bottomBarHeight = 0; try { statusBarHeight = Get.mediaQuery.padding.top; bottomBarHeight = Get.mediaQuery.padding.bottom; } catch (e) { log(TAG, '获取 MediaQuery 失败,使用默认值: $e'); // 使用默认值 statusBarHeight = Platform.isIOS ? 47 : 24; bottomBarHeight = Platform.isIOS ? 34 : 0; } // 获取 token final user = IXSP.getUser(); final token = user?.accessToken ?? ''; // 注入 nativeAttrs 对象 final jsCode = ''' (function() { // 创建 nativeAttrs 对象 window.${JSBridgeConstants.nativeAttrs} = { statusBarHeight: $statusBarHeight, bottomBarHeight: $bottomBarHeight, token: '$token', platform: '${Platform.isIOS ? 'ios' : 'android'}', version: '${await _getAppVersion()}', }; // 创建原生方法调用接口 window.native = { // 设置状态栏模式(只支持 'dark' 或 'light') setStatusBarColor: function(mode) { return window.flutter_inappwebview.callHandler('${JSBridgeConstants.setStatusBarColor}', {color: mode}); }, // 退出 WebView exit: function() { return window.flutter_inappwebview.callHandler('${JSBridgeConstants.exitWebView}'); }, // 连接 VPN connectVPN: function(id, code) { return window.flutter_inappwebview.callHandler('${JSBridgeConstants.connectVPN}', {id: id, code: code}); }, // 断开 VPN disconnectVPN: function() { return window.flutter_inappwebview.callHandler('${JSBridgeConstants.disconnectVPN}'); }, // 打开指定 App openApp: function(packageName, scheme, extras) { return window.flutter_inappwebview.callHandler('${JSBridgeConstants.openApp}', { packageName: packageName, scheme: scheme, extras: extras }); }, // 获取 VPN 状态 getVPNStatus: function() { return window.flutter_inappwebview.callHandler('${JSBridgeConstants.getVPNStatus}'); } }; // 创建事件回调接口 window.nativeCallbacks = { onVPNStatusChanged: function(status) { console.log('VPN状态变化:', status); }, onVPNConnected: function() { console.log('VPN已连接'); }, onVPNDisconnected: function() { console.log('VPN已断开'); }, onVPNConnecting: function() { console.log('VPN连接中'); } }; console.log('Native bridge initialized:', window.${JSBridgeConstants.nativeAttrs}); })(); '''; await webViewController!.evaluateJavascript(source: jsCode); log(TAG, 'Native Attrs 注入成功'); } catch (e) { log(TAG, '注入 Native Attrs 失败: $e'); } } /// 获取应用版本 Future _getAppVersion() async { try { final packageInfo = await PackageInfo.fromPlatform(); return packageInfo.version; } catch (e) { return '1.0.0'; } } // ==================== Handler 实现 ==================== /// 处理设置状态栏模式(只支持 dark/light) Future> _handleSetStatusBarColor( List args, ) async { try { if (args.isEmpty) { return JSBridgeResponse(success: false, error: '缺少模式参数').toJson(); } final params = args[0] as Map; final mode = params['color'] as String?; if (mode == null || mode.isEmpty) { return JSBridgeResponse(success: false, error: '模式值不能为空').toJson(); } // 只支持 'dark' 和 'light' 两种模式 final Brightness iconBrightness; if (mode.toLowerCase() == 'dark') { // dark 模式 = 白色图标(用于深色背景) iconBrightness = Brightness.light; } else if (mode.toLowerCase() == 'light') { // light 模式 = 黑色图标(用于浅色背景) iconBrightness = Brightness.dark; } else { return JSBridgeResponse( success: false, error: '只支持 "dark" 或 "light" 模式', ).toJson(); } // 创建新的状态栏样式(背景色保持透明) final newStyle = SystemUiOverlayStyle( statusBarColor: Colors.transparent, // 保持透明 statusBarIconBrightness: iconBrightness, // 只改变图标颜色 statusBarBrightness: iconBrightness, ); // 更新响应式状态栏样式,触发 AnnotatedRegion 重新渲染 currentStatusBarStyle.value = newStyle; log(TAG, '设置状态栏模式: $mode (图标亮度: $iconBrightness)'); return JSBridgeResponse(success: true, data: {'mode': mode}).toJson(); } catch (e) { log(TAG, '设置状态栏模式失败: $e'); return JSBridgeResponse(success: false, error: e.toString()).toJson(); } } /// 处理退出 WebView Future> _handleExitWebView(List args) async { try { log(TAG, '退出 WebView'); Get.back(); return JSBridgeResponse(success: true).toJson(); } catch (e) { log(TAG, '退出 WebView 失败: $e'); return JSBridgeResponse(success: false, error: e.toString()).toJson(); } } /// 处理连接 VPN Future> _handleConnectVPN(List args) async { try { final params = args.isNotEmpty ? args[0] as Map : {}; final vpnParams = VPNConnectParams.fromJson(params); log(TAG, '连接 VPN: id=${vpnParams.id}, code=${vpnParams.code}'); // 如果提供了 id 和 code,保存为选中的位置 if (vpnParams.id != null && vpnParams.code != null) { await IXSP.saveSelectedLocation({ 'id': vpnParams.id, 'code': vpnParams.code, }); } // 调用 CoreController 连接 VPN final coreController = Get.find(); coreController.handleConnection(); return JSBridgeResponse( success: true, data: {'id': vpnParams.id, 'code': vpnParams.code}, ).toJson(); } catch (e) { log(TAG, '连接 VPN 失败: $e'); return JSBridgeResponse(success: false, error: e.toString()).toJson(); } } /// 处理断开 VPN Future> _handleDisconnectVPN(List args) async { try { log(TAG, '断开 VPN'); // 调用 CoreController 断开 VPN final coreController = Get.find(); if (coreController.state != enums.ConnectionState.disconnected) { coreController.handleConnection(); } return JSBridgeResponse(success: true).toJson(); } catch (e) { log(TAG, '断开 VPN 失败: $e'); return JSBridgeResponse(success: false, error: e.toString()).toJson(); } } /// 处理打开指定 App Future> _handleOpenApp(List args) async { try { if (args.isEmpty) { return JSBridgeResponse(success: false, error: '缺少参数').toJson(); } final params = args[0] as Map; final appParams = OpenAppParams.fromJson(params); log(TAG, '打开 App: ${appParams.packageName}'); Uri? uri; // 优先使用 scheme if (appParams.scheme != null && appParams.scheme!.isNotEmpty) { uri = Uri.parse(appParams.scheme!); } else if (Platform.isAndroid) { // Android 使用 package name uri = Uri.parse('package:${appParams.packageName}'); } else if (Platform.isIOS) { // iOS 需要使用 app 的 URL Scheme return JSBridgeResponse( success: false, error: 'iOS 需要提供 URL Scheme', ).toJson(); } if (uri != null && await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); return JSBridgeResponse( success: true, data: {'packageName': appParams.packageName}, ).toJson(); } else { return JSBridgeResponse(success: false, error: '无法打开应用').toJson(); } } catch (e) { log(TAG, '打开 App 失败: $e'); return JSBridgeResponse(success: false, error: e.toString()).toJson(); } } /// 处理获取 VPN 状态 Future> _handleGetVPNStatus(List args) async { try { final coreController = Get.find(); final state = coreController.state; final timer = coreController.timer; String statusText; switch (state) { case enums.ConnectionState.connected: statusText = 'connected'; break; case enums.ConnectionState.connecting: statusText = 'connecting'; break; case enums.ConnectionState.disconnected: statusText = 'disconnected'; break; default: statusText = 'unknown'; } log(TAG, '获取 VPN 状态: $statusText'); return JSBridgeResponse( success: true, data: {'status': statusText, 'timer': timer}, ).toJson(); } catch (e) { log(TAG, '获取 VPN 状态失败: $e'); return JSBridgeResponse(success: false, error: e.toString()).toJson(); } } // ==================== 监听 VPN 状态变化并通知 JS ==================== // 保存 Worker 引用 Worker? _vpnStatusWorker; @override void onClose() { // 取消 VPN 状态监听 _vpnStatusWorker?.dispose(); _vpnStatusWorker = null; super.onClose(); } /// 监听 VPN 状态变化 void _listenToVPNStatusChanges() { try { final coreController = Get.find(); // 使用 ever 监听 CoreController 提供的状态流 _vpnStatusWorker = ever(coreController.stateStream, ( enums.ConnectionState state, ) { _notifyVPNStatusToJS(state); log(TAG, 'VPN 状态已变化并通知 JS: $state'); }); log(TAG, 'VPN 状态监听已启动'); } catch (e) { log(TAG, '监听 VPN 状态变化失败: $e'); } } /// 通知 JS VPN 状态变化 void _notifyVPNStatusToJS(enums.ConnectionState state) async { if (webViewController == null) return; try { String statusText; String callbackName; switch (state) { case enums.ConnectionState.connected: statusText = 'connected'; callbackName = 'onVPNConnected'; break; case enums.ConnectionState.connecting: statusText = 'connecting'; callbackName = 'onVPNConnecting'; break; case enums.ConnectionState.disconnected: statusText = 'disconnected'; callbackName = 'onVPNDisconnected'; break; default: statusText = 'unknown'; callbackName = 'onVPNStatusChanged'; } // 调用 JS 回调 final jsCode = ''' (function() { try { // 调用通用状态变化回调 if (window.nativeCallbacks && typeof window.nativeCallbacks.onVPNStatusChanged === 'function') { window.nativeCallbacks.onVPNStatusChanged('$statusText'); } // 调用特定状态回调 if (window.nativeCallbacks && typeof window.nativeCallbacks.$callbackName === 'function') { window.nativeCallbacks.$callbackName(); } // 触发自定义事件 window.dispatchEvent(new CustomEvent('vpnStatusChanged', { detail: { status: '$statusText' } })); } catch (e) { console.error('VPN status callback error:', e); } })(); '''; await webViewController!.evaluateJavascript(source: jsCode); log(TAG, '通知 JS VPN 状态变化: $statusText'); } catch (e) { log(TAG, '通知 JS VPN 状态变化失败: $e'); } } }