| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634 |
- 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<void> 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<void> loadUserAgent() async {
- initWebView(await generateUserAgent());
- }
- Future<String> 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<bool> checkCanGoBack() async {
- try {
- if (webViewController != null) {
- final canBack = await webViewController!.canGoBack();
- canGoBack.value = canBack;
- }
- } catch (e) {
- // 如果检查失败,默认设置为false
- canGoBack.value = false;
- }
- return canGoBack.value;
- }
- // 回退方法
- Future<void> goBack() async {
- if (canGoBack.value && webViewController != null) {
- await webViewController!.goBack();
- // 回退后重新检查状态
- checkCanGoBack();
- }
- }
- // 刷新方法
- Future<void> 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<void> 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<String> _getAppVersion() async {
- try {
- final packageInfo = await PackageInfo.fromPlatform();
- return packageInfo.version;
- } catch (e) {
- return '1.0.0';
- }
- }
- // ==================== Handler 实现 ====================
- /// 处理设置状态栏模式(只支持 dark/light)
- Future<Map<String, dynamic>> _handleSetStatusBarColor(
- List<dynamic> args,
- ) async {
- try {
- if (args.isEmpty) {
- return JSBridgeResponse(success: false, error: '缺少模式参数').toJson();
- }
- final params = args[0] as Map<String, dynamic>;
- 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<Map<String, dynamic>> _handleExitWebView(List<dynamic> 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<Map<String, dynamic>> _handleConnectVPN(List<dynamic> args) async {
- try {
- final params = args.isNotEmpty
- ? args[0] as Map<String, dynamic>
- : <String, dynamic>{};
- 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>();
- 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<Map<String, dynamic>> _handleDisconnectVPN(List<dynamic> args) async {
- try {
- log(TAG, '断开 VPN');
- // 调用 CoreController 断开 VPN
- final coreController = Get.find<CoreController>();
- 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<Map<String, dynamic>> _handleOpenApp(List<dynamic> args) async {
- try {
- if (args.isEmpty) {
- return JSBridgeResponse(success: false, error: '缺少参数').toJson();
- }
- final params = args[0] as Map<String, dynamic>;
- 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<Map<String, dynamic>> _handleGetVPNStatus(List<dynamic> args) async {
- try {
- final coreController = Get.find<CoreController>();
- 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<CoreController>();
- // 使用 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');
- }
- }
- }
|