web_controller.dart 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. import 'dart:io';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:get/get.dart';
  5. import 'package:device_info_plus/device_info_plus.dart';
  6. import 'package:package_info_plus/package_info_plus.dart';
  7. import 'package:flutter_inappwebview/flutter_inappwebview.dart';
  8. import 'package:url_launcher/url_launcher.dart';
  9. import '../../../controllers/core_controller.dart';
  10. import '../../../data/sp/ix_sp.dart';
  11. import '../../../constants/enums.dart' as enums;
  12. import '../../../../utils/log/logger.dart';
  13. import '../utils/js_bridge.dart';
  14. export 'package:flutter/services.dart' show rootBundle;
  15. class WebController extends GetxController {
  16. final TAG = 'WebController';
  17. var title = '';
  18. var url = '';
  19. InAppWebViewController? webViewController;
  20. final isLoading = true.obs;
  21. final loadingProgress = 0.0.obs;
  22. final canGoBack = false.obs; // 添加是否可以回退的观察变量
  23. final isFullScreen = false.obs; // 全面屏模式
  24. // 当前的状态栏样式(响应式)
  25. final currentStatusBarStyle = const SystemUiOverlayStyle(
  26. statusBarColor: Colors.transparent,
  27. statusBarIconBrightness: Brightness.light,
  28. statusBarBrightness: Brightness.light,
  29. ).obs;
  30. // final _externalSchemes = [
  31. // 'mailto:',
  32. // 'tel:',
  33. // 'sms:',
  34. // 'geo:',
  35. // 'intent:',
  36. // 'market:'
  37. // ];
  38. @override
  39. void onInit() {
  40. if (Get.arguments != null) {
  41. title = Get.arguments['title'] ?? '';
  42. url = Get.arguments['url'] ?? '';
  43. isFullScreen.value = Get.arguments['fullScreen'] ?? false;
  44. }
  45. loadUserAgent();
  46. _listenToVPNStatusChanges();
  47. super.onInit();
  48. }
  49. /// 加载本地 Assets HTML 文件
  50. Future<void> loadAssetHtml(String assetPath) async {
  51. try {
  52. final htmlContent = await rootBundle.loadString(assetPath);
  53. if (webViewController != null) {
  54. await webViewController!.loadData(
  55. data: htmlContent,
  56. baseUrl: WebUri('about:blank'),
  57. mimeType: 'text/html',
  58. encoding: 'utf-8',
  59. );
  60. log(TAG, '成功加载本地 HTML: $assetPath');
  61. }
  62. } catch (e) {
  63. log(TAG, '加载本地 HTML 失败: $e');
  64. }
  65. }
  66. Future<void> loadUserAgent() async {
  67. initWebView(await generateUserAgent());
  68. }
  69. Future<String> generateUserAgent() async {
  70. try {
  71. final deviceInfo = DeviceInfoPlugin();
  72. final packageInfo = await PackageInfo.fromPlatform();
  73. if (Platform.isIOS) {
  74. final iosInfo = await deviceInfo.iosInfo;
  75. // 使用类似 Safari 的 UA 格式
  76. return 'Mozilla/5.0 (iPhone; CPU iPhone OS ${iosInfo.systemVersion.replaceAll('.', '_')} like Mac OS X) '
  77. 'AppleWebKit/605.1.15 (KHTML, like Gecko) '
  78. 'Version/${packageInfo.version} Mobile/15E148 Safari/604.1';
  79. } else {
  80. final androidInfo = await deviceInfo.androidInfo;
  81. // 使用简化的 Android UA 格式,避免 WebView 标识
  82. return 'Mozilla/5.0 (Linux; Android ${androidInfo.version.release}; ${androidInfo.model}) '
  83. 'AppleWebKit/537.36 (KHTML, like Gecko) '
  84. 'Version/${packageInfo.version} Mobile Safari/537.36';
  85. }
  86. } catch (e) {
  87. // 如果获取设备信息失败,返回一个通用的安全 UA
  88. return 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) '
  89. 'AppleWebKit/605.1.15 (KHTML, like Gecko) '
  90. 'Version/16.6 Mobile/15E148 Safari/604.1';
  91. }
  92. }
  93. String? userAgent;
  94. void initWebView(String ua) {
  95. userAgent = ua;
  96. // InAppWebView 的初始化在 WebView 组件中完成
  97. // 这里只需要设置 URL
  98. if (url.isNotEmpty) {
  99. // URL 将在 WebView 组件中加载
  100. }
  101. }
  102. // 设置 WebViewController 的引用
  103. void setWebViewController(InAppWebViewController controller) {
  104. webViewController = controller;
  105. // 设置 User Agent
  106. if (userAgent != null) {
  107. webViewController?.setSettings(
  108. settings: InAppWebViewSettings(
  109. userAgent: userAgent!,
  110. javaScriptEnabled: true,
  111. useShouldOverrideUrlLoading: true,
  112. useOnLoadResource: true,
  113. mediaPlaybackRequiresUserGesture: false,
  114. allowsInlineMediaPlayback: true,
  115. iframeAllow: "camera; microphone",
  116. iframeAllowFullscreen: true,
  117. ),
  118. );
  119. }
  120. }
  121. // 检查是否可以回退的方法
  122. Future<bool> checkCanGoBack() async {
  123. try {
  124. if (webViewController != null) {
  125. final canBack = await webViewController!.canGoBack();
  126. canGoBack.value = canBack;
  127. }
  128. } catch (e) {
  129. // 如果检查失败,默认设置为false
  130. canGoBack.value = false;
  131. }
  132. return canGoBack.value;
  133. }
  134. // 回退方法
  135. Future<void> goBack() async {
  136. if (canGoBack.value && webViewController != null) {
  137. await webViewController!.goBack();
  138. // 回退后重新检查状态
  139. checkCanGoBack();
  140. }
  141. }
  142. // 刷新方法
  143. Future<void> reload() async {
  144. if (webViewController != null) {
  145. await webViewController!.reload();
  146. }
  147. }
  148. String? parseIntentUrl(String url) {
  149. if (!url.startsWith("intent://")) return url;
  150. // 提取 scheme
  151. final schemeMatch = RegExp(r"scheme=([a-zA-Z0-9.+-]+);").firstMatch(url);
  152. final scheme = schemeMatch?.group(1) ?? "https";
  153. // 替换 intent:// -> scheme://
  154. var cleanUrl = url
  155. .replaceFirst("intent://", "$scheme://")
  156. .replaceAll(RegExp(r"#Intent;.*;end$"), "");
  157. return cleanUrl;
  158. }
  159. String? parseFallbackUrl(String url) {
  160. final match = RegExp(r"S\.browser_fallback_url=([^;]+);").firstMatch(url);
  161. return match != null ? Uri.decodeComponent(match.group(1)!) : null;
  162. }
  163. // ==================== JS Bridge 功能 ====================
  164. /// 设置 JavaScript Handlers
  165. void setupJavaScriptHandlers(InAppWebViewController controller) {
  166. // 设置状态栏颜色
  167. controller.addJavaScriptHandler(
  168. handlerName: JSBridgeConstants.setStatusBarColor,
  169. callback: (args) async {
  170. return await _handleSetStatusBarColor(args);
  171. },
  172. );
  173. // 退出 WebView
  174. controller.addJavaScriptHandler(
  175. handlerName: JSBridgeConstants.exitWebView,
  176. callback: (args) async {
  177. return await _handleExitWebView(args);
  178. },
  179. );
  180. // 连接 VPN
  181. controller.addJavaScriptHandler(
  182. handlerName: JSBridgeConstants.connectVPN,
  183. callback: (args) async {
  184. return await _handleConnectVPN(args);
  185. },
  186. );
  187. // 断开 VPN
  188. controller.addJavaScriptHandler(
  189. handlerName: JSBridgeConstants.disconnectVPN,
  190. callback: (args) async {
  191. return await _handleDisconnectVPN(args);
  192. },
  193. );
  194. // 打开指定 App
  195. controller.addJavaScriptHandler(
  196. handlerName: JSBridgeConstants.openApp,
  197. callback: (args) async {
  198. return await _handleOpenApp(args);
  199. },
  200. );
  201. // 获取 VPN 状态
  202. controller.addJavaScriptHandler(
  203. handlerName: JSBridgeConstants.getVPNStatus,
  204. callback: (args) async {
  205. return await _handleGetVPNStatus(args);
  206. },
  207. );
  208. log(TAG, 'JavaScript Handlers 已设置');
  209. }
  210. /// 注入原生属性到 JS
  211. Future<void> injectNativeAttrs() async {
  212. if (webViewController == null) return;
  213. try {
  214. // 获取状态栏和底部安全区域高度
  215. double statusBarHeight = 0;
  216. double bottomBarHeight = 0;
  217. try {
  218. statusBarHeight = Get.mediaQuery.padding.top;
  219. bottomBarHeight = Get.mediaQuery.padding.bottom;
  220. } catch (e) {
  221. log(TAG, '获取 MediaQuery 失败,使用默认值: $e');
  222. // 使用默认值
  223. statusBarHeight = Platform.isIOS ? 47 : 24;
  224. bottomBarHeight = Platform.isIOS ? 34 : 0;
  225. }
  226. // 获取 token
  227. final user = IXSP.getUser();
  228. final token = user?.accessToken ?? '';
  229. // 注入 nativeAttrs 对象
  230. final jsCode =
  231. '''
  232. (function() {
  233. // 创建 nativeAttrs 对象
  234. window.${JSBridgeConstants.nativeAttrs} = {
  235. statusBarHeight: $statusBarHeight,
  236. bottomBarHeight: $bottomBarHeight,
  237. token: '$token',
  238. platform: '${Platform.isIOS ? 'ios' : 'android'}',
  239. version: '${await _getAppVersion()}',
  240. };
  241. // 创建原生方法调用接口
  242. window.native = {
  243. // 设置状态栏模式(只支持 'dark' 或 'light')
  244. setStatusBarColor: function(mode) {
  245. return window.flutter_inappwebview.callHandler('${JSBridgeConstants.setStatusBarColor}', {color: mode});
  246. },
  247. // 退出 WebView
  248. exit: function() {
  249. return window.flutter_inappwebview.callHandler('${JSBridgeConstants.exitWebView}');
  250. },
  251. // 连接 VPN
  252. connectVPN: function(id, code) {
  253. return window.flutter_inappwebview.callHandler('${JSBridgeConstants.connectVPN}', {id: id, code: code});
  254. },
  255. // 断开 VPN
  256. disconnectVPN: function() {
  257. return window.flutter_inappwebview.callHandler('${JSBridgeConstants.disconnectVPN}');
  258. },
  259. // 打开指定 App
  260. openApp: function(packageName, scheme, extras) {
  261. return window.flutter_inappwebview.callHandler('${JSBridgeConstants.openApp}', {
  262. packageName: packageName,
  263. scheme: scheme,
  264. extras: extras
  265. });
  266. },
  267. // 获取 VPN 状态
  268. getVPNStatus: function() {
  269. return window.flutter_inappwebview.callHandler('${JSBridgeConstants.getVPNStatus}');
  270. }
  271. };
  272. // 创建事件回调接口
  273. window.nativeCallbacks = {
  274. onVPNStatusChanged: function(status) {
  275. console.log('VPN状态变化:', status);
  276. },
  277. onVPNConnected: function() {
  278. console.log('VPN已连接');
  279. },
  280. onVPNDisconnected: function() {
  281. console.log('VPN已断开');
  282. },
  283. onVPNConnecting: function() {
  284. console.log('VPN连接中');
  285. }
  286. };
  287. console.log('Native bridge initialized:', window.${JSBridgeConstants.nativeAttrs});
  288. })();
  289. ''';
  290. await webViewController!.evaluateJavascript(source: jsCode);
  291. log(TAG, 'Native Attrs 注入成功');
  292. } catch (e) {
  293. log(TAG, '注入 Native Attrs 失败: $e');
  294. }
  295. }
  296. /// 获取应用版本
  297. Future<String> _getAppVersion() async {
  298. try {
  299. final packageInfo = await PackageInfo.fromPlatform();
  300. return packageInfo.version;
  301. } catch (e) {
  302. return '1.0.0';
  303. }
  304. }
  305. // ==================== Handler 实现 ====================
  306. /// 处理设置状态栏模式(只支持 dark/light)
  307. Future<Map<String, dynamic>> _handleSetStatusBarColor(
  308. List<dynamic> args,
  309. ) async {
  310. try {
  311. if (args.isEmpty) {
  312. return JSBridgeResponse(success: false, error: '缺少模式参数').toJson();
  313. }
  314. final params = args[0] as Map<String, dynamic>;
  315. final mode = params['color'] as String?;
  316. if (mode == null || mode.isEmpty) {
  317. return JSBridgeResponse(success: false, error: '模式值不能为空').toJson();
  318. }
  319. // 只支持 'dark' 和 'light' 两种模式
  320. final Brightness iconBrightness;
  321. if (mode.toLowerCase() == 'dark') {
  322. // dark 模式 = 白色图标(用于深色背景)
  323. iconBrightness = Brightness.light;
  324. } else if (mode.toLowerCase() == 'light') {
  325. // light 模式 = 黑色图标(用于浅色背景)
  326. iconBrightness = Brightness.dark;
  327. } else {
  328. return JSBridgeResponse(
  329. success: false,
  330. error: '只支持 "dark" 或 "light" 模式',
  331. ).toJson();
  332. }
  333. // 创建新的状态栏样式(背景色保持透明)
  334. final newStyle = SystemUiOverlayStyle(
  335. statusBarColor: Colors.transparent, // 保持透明
  336. statusBarIconBrightness: iconBrightness, // 只改变图标颜色
  337. statusBarBrightness: iconBrightness,
  338. );
  339. // 更新响应式状态栏样式,触发 AnnotatedRegion 重新渲染
  340. currentStatusBarStyle.value = newStyle;
  341. log(TAG, '设置状态栏模式: $mode (图标亮度: $iconBrightness)');
  342. return JSBridgeResponse(success: true, data: {'mode': mode}).toJson();
  343. } catch (e) {
  344. log(TAG, '设置状态栏模式失败: $e');
  345. return JSBridgeResponse(success: false, error: e.toString()).toJson();
  346. }
  347. }
  348. /// 处理退出 WebView
  349. Future<Map<String, dynamic>> _handleExitWebView(List<dynamic> args) async {
  350. try {
  351. log(TAG, '退出 WebView');
  352. Get.back();
  353. return JSBridgeResponse(success: true).toJson();
  354. } catch (e) {
  355. log(TAG, '退出 WebView 失败: $e');
  356. return JSBridgeResponse(success: false, error: e.toString()).toJson();
  357. }
  358. }
  359. /// 处理连接 VPN
  360. Future<Map<String, dynamic>> _handleConnectVPN(List<dynamic> args) async {
  361. try {
  362. final params = args.isNotEmpty
  363. ? args[0] as Map<String, dynamic>
  364. : <String, dynamic>{};
  365. final vpnParams = VPNConnectParams.fromJson(params);
  366. log(TAG, '连接 VPN: id=${vpnParams.id}, code=${vpnParams.code}');
  367. // 如果提供了 id 和 code,保存为选中的位置
  368. if (vpnParams.id != null && vpnParams.code != null) {
  369. await IXSP.saveSelectedLocation({
  370. 'id': vpnParams.id,
  371. 'code': vpnParams.code,
  372. });
  373. }
  374. // 调用 CoreController 连接 VPN
  375. final coreController = Get.find<CoreController>();
  376. coreController.handleConnection();
  377. return JSBridgeResponse(
  378. success: true,
  379. data: {'id': vpnParams.id, 'code': vpnParams.code},
  380. ).toJson();
  381. } catch (e) {
  382. log(TAG, '连接 VPN 失败: $e');
  383. return JSBridgeResponse(success: false, error: e.toString()).toJson();
  384. }
  385. }
  386. /// 处理断开 VPN
  387. Future<Map<String, dynamic>> _handleDisconnectVPN(List<dynamic> args) async {
  388. try {
  389. log(TAG, '断开 VPN');
  390. // 调用 CoreController 断开 VPN
  391. final coreController = Get.find<CoreController>();
  392. if (coreController.state != enums.ConnectionState.disconnected) {
  393. coreController.handleConnection();
  394. }
  395. return JSBridgeResponse(success: true).toJson();
  396. } catch (e) {
  397. log(TAG, '断开 VPN 失败: $e');
  398. return JSBridgeResponse(success: false, error: e.toString()).toJson();
  399. }
  400. }
  401. /// 处理打开指定 App
  402. Future<Map<String, dynamic>> _handleOpenApp(List<dynamic> args) async {
  403. try {
  404. if (args.isEmpty) {
  405. return JSBridgeResponse(success: false, error: '缺少参数').toJson();
  406. }
  407. final params = args[0] as Map<String, dynamic>;
  408. final appParams = OpenAppParams.fromJson(params);
  409. log(TAG, '打开 App: ${appParams.packageName}');
  410. Uri? uri;
  411. // 优先使用 scheme
  412. if (appParams.scheme != null && appParams.scheme!.isNotEmpty) {
  413. uri = Uri.parse(appParams.scheme!);
  414. } else if (Platform.isAndroid) {
  415. // Android 使用 package name
  416. uri = Uri.parse('package:${appParams.packageName}');
  417. } else if (Platform.isIOS) {
  418. // iOS 需要使用 app 的 URL Scheme
  419. return JSBridgeResponse(
  420. success: false,
  421. error: 'iOS 需要提供 URL Scheme',
  422. ).toJson();
  423. }
  424. if (uri != null && await canLaunchUrl(uri)) {
  425. await launchUrl(uri, mode: LaunchMode.externalApplication);
  426. return JSBridgeResponse(
  427. success: true,
  428. data: {'packageName': appParams.packageName},
  429. ).toJson();
  430. } else {
  431. return JSBridgeResponse(success: false, error: '无法打开应用').toJson();
  432. }
  433. } catch (e) {
  434. log(TAG, '打开 App 失败: $e');
  435. return JSBridgeResponse(success: false, error: e.toString()).toJson();
  436. }
  437. }
  438. /// 处理获取 VPN 状态
  439. Future<Map<String, dynamic>> _handleGetVPNStatus(List<dynamic> args) async {
  440. try {
  441. final coreController = Get.find<CoreController>();
  442. final state = coreController.state;
  443. final timer = coreController.timer;
  444. String statusText;
  445. switch (state) {
  446. case enums.ConnectionState.connected:
  447. statusText = 'connected';
  448. break;
  449. case enums.ConnectionState.connecting:
  450. statusText = 'connecting';
  451. break;
  452. case enums.ConnectionState.disconnected:
  453. statusText = 'disconnected';
  454. break;
  455. default:
  456. statusText = 'unknown';
  457. }
  458. log(TAG, '获取 VPN 状态: $statusText');
  459. return JSBridgeResponse(
  460. success: true,
  461. data: {'status': statusText, 'timer': timer},
  462. ).toJson();
  463. } catch (e) {
  464. log(TAG, '获取 VPN 状态失败: $e');
  465. return JSBridgeResponse(success: false, error: e.toString()).toJson();
  466. }
  467. }
  468. // ==================== 监听 VPN 状态变化并通知 JS ====================
  469. // 保存 Worker 引用
  470. Worker? _vpnStatusWorker;
  471. @override
  472. void onClose() {
  473. // 取消 VPN 状态监听
  474. _vpnStatusWorker?.dispose();
  475. _vpnStatusWorker = null;
  476. super.onClose();
  477. }
  478. /// 监听 VPN 状态变化
  479. void _listenToVPNStatusChanges() {
  480. try {
  481. final coreController = Get.find<CoreController>();
  482. // 使用 ever 监听 CoreController 提供的状态流
  483. _vpnStatusWorker = ever(coreController.stateStream, (
  484. enums.ConnectionState state,
  485. ) {
  486. _notifyVPNStatusToJS(state);
  487. log(TAG, 'VPN 状态已变化并通知 JS: $state');
  488. });
  489. log(TAG, 'VPN 状态监听已启动');
  490. } catch (e) {
  491. log(TAG, '监听 VPN 状态变化失败: $e');
  492. }
  493. }
  494. /// 通知 JS VPN 状态变化
  495. void _notifyVPNStatusToJS(enums.ConnectionState state) async {
  496. if (webViewController == null) return;
  497. try {
  498. String statusText;
  499. String callbackName;
  500. switch (state) {
  501. case enums.ConnectionState.connected:
  502. statusText = 'connected';
  503. callbackName = 'onVPNConnected';
  504. break;
  505. case enums.ConnectionState.connecting:
  506. statusText = 'connecting';
  507. callbackName = 'onVPNConnecting';
  508. break;
  509. case enums.ConnectionState.disconnected:
  510. statusText = 'disconnected';
  511. callbackName = 'onVPNDisconnected';
  512. break;
  513. default:
  514. statusText = 'unknown';
  515. callbackName = 'onVPNStatusChanged';
  516. }
  517. // 调用 JS 回调
  518. final jsCode =
  519. '''
  520. (function() {
  521. try {
  522. // 调用通用状态变化回调
  523. if (window.nativeCallbacks && typeof window.nativeCallbacks.onVPNStatusChanged === 'function') {
  524. window.nativeCallbacks.onVPNStatusChanged('$statusText');
  525. }
  526. // 调用特定状态回调
  527. if (window.nativeCallbacks && typeof window.nativeCallbacks.$callbackName === 'function') {
  528. window.nativeCallbacks.$callbackName();
  529. }
  530. // 触发自定义事件
  531. window.dispatchEvent(new CustomEvent('vpnStatusChanged', {
  532. detail: { status: '$statusText' }
  533. }));
  534. } catch (e) {
  535. console.error('VPN status callback error:', e);
  536. }
  537. })();
  538. ''';
  539. await webViewController!.evaluateJavascript(source: jsCode);
  540. log(TAG, '通知 JS VPN 状态变化: $statusText');
  541. } catch (e) {
  542. log(TAG, '通知 JS VPN 状态变化失败: $e');
  543. }
  544. }
  545. }