web_view.dart 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:flutter_inappwebview/flutter_inappwebview.dart';
  4. import 'package:flutter_screenutil/flutter_screenutil.dart';
  5. import 'package:get/get.dart';
  6. import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
  7. import 'package:url_launcher/url_launcher.dart';
  8. import '../../../../utils/log/logger.dart';
  9. import '../../../base/base_view.dart';
  10. import '../../../widgets/ix_app_bar.dart';
  11. import '../controllers/web_controller.dart';
  12. class WebView extends BaseView<WebController> {
  13. const WebView({super.key});
  14. @override
  15. PreferredSizeWidget? get appBar {
  16. // 如果是全面屏模式,不显示AppBar
  17. return controller.isFullScreen.value
  18. ? null
  19. : IXAppBar(
  20. title: controller.title,
  21. // 不需要传递颜色参数,会自动使用响应式主题
  22. onBackPressed: () async {
  23. if (await controller.checkCanGoBack()) {
  24. controller.goBack();
  25. } else {
  26. Get.back();
  27. }
  28. },
  29. actions: [
  30. IconButton(
  31. onPressed: () {
  32. controller.reload();
  33. },
  34. icon: const Icon(Icons.refresh_rounded),
  35. ),
  36. Obx(
  37. () => controller.canGoBack.value
  38. ? IconButton(
  39. onPressed: () {
  40. Get.back();
  41. },
  42. icon: const Icon(Icons.close_rounded),
  43. )
  44. : const SizedBox.shrink(),
  45. ),
  46. ],
  47. );
  48. }
  49. @override
  50. bool get extendBodyBehindAppBar => controller.isFullScreen.value;
  51. @override
  52. Widget buildContent(BuildContext context) {
  53. return _buildWebContent();
  54. }
  55. Widget _buildWebContent() {
  56. // 将 WebView 移出 Obx,避免重复创建
  57. return Obx(
  58. () => AnnotatedRegion<SystemUiOverlayStyle>(
  59. value: controller.currentStatusBarStyle.value,
  60. child: Stack(
  61. children: [
  62. InAppWebView(
  63. key: const ValueKey('nomo_webview'), // 添加唯一 key
  64. initialUrlRequest: URLRequest(url: WebUri(controller.url)),
  65. initialSettings: InAppWebViewSettings(
  66. userAgent: controller.userAgent,
  67. javaScriptEnabled: true,
  68. useShouldOverrideUrlLoading: true,
  69. mediaPlaybackRequiresUserGesture: false,
  70. allowsInlineMediaPlayback: true,
  71. allowFileAccessFromFileURLs: true,
  72. allowUniversalAccessFromFileURLs: true,
  73. // iOS 特定设置
  74. allowsBackForwardNavigationGestures: true,
  75. suppressesIncrementalRendering: false,
  76. transparentBackground: false,
  77. isInspectable: true,
  78. ),
  79. onWebViewCreated:
  80. (InAppWebViewController webViewController) async {
  81. controller.setWebViewController(webViewController);
  82. controller.setupJavaScriptHandlers(webViewController);
  83. // 如果 URL 是 assets 路径,则使用 loadData 加载
  84. if (controller.url.startsWith('assets/')) {
  85. await controller.loadAssetHtml(controller.url);
  86. }
  87. },
  88. onProgressChanged:
  89. (InAppWebViewController webViewController, int progress) {
  90. controller.loadingProgress.value = progress / 100;
  91. controller.isLoading.value = progress < 100;
  92. },
  93. onLoadStart:
  94. (InAppWebViewController webViewController, Uri? url) {
  95. controller.checkCanGoBack();
  96. },
  97. onLoadStop:
  98. (InAppWebViewController webViewController, Uri? url) async {
  99. controller.isLoading.value = false;
  100. controller.loadingProgress.value = 0.0;
  101. controller.checkCanGoBack();
  102. // 页面加载完成后再次注入,确保万无一失
  103. await controller.injectNativeAttrs();
  104. },
  105. onReceivedError:
  106. (
  107. InAppWebViewController webViewController,
  108. WebResourceRequest request,
  109. WebResourceError error,
  110. ) {
  111. controller.isLoading.value = false;
  112. controller.loadingProgress.value = 0.0;
  113. },
  114. shouldOverrideUrlLoading:
  115. (
  116. InAppWebViewController webViewController,
  117. NavigationAction navigationAction,
  118. ) async {
  119. final url = navigationAction.request.url?.toString() ?? '';
  120. // 如果是空 URL 或本地资源,允许加载
  121. if (url.isEmpty ||
  122. url.startsWith('assets/') ||
  123. url.startsWith('file://') ||
  124. url.startsWith('about:')) {
  125. return NavigationActionPolicy.ALLOW;
  126. }
  127. try {
  128. final uri = Uri.parse(url);
  129. // 处理 http/https 链接 - 允许在 WebView 中加载
  130. if (uri.scheme == 'http' || uri.scheme == 'https') {
  131. return NavigationActionPolicy.ALLOW;
  132. }
  133. // 处理 intent:// 链接
  134. if (url.startsWith("intent://")) {
  135. final parsedUrl = controller.parseIntentUrl(url);
  136. final fallbackUrl = controller.parseFallbackUrl(url);
  137. if (parsedUrl != null &&
  138. await canLaunchUrl(Uri.parse(parsedUrl))) {
  139. await launchUrl(
  140. Uri.parse(parsedUrl),
  141. mode: LaunchMode.externalApplication,
  142. );
  143. } else if (fallbackUrl != null &&
  144. await canLaunchUrl(Uri.parse(fallbackUrl))) {
  145. await launchUrl(
  146. Uri.parse(fallbackUrl),
  147. mode: LaunchMode.externalApplication,
  148. );
  149. }
  150. return NavigationActionPolicy.CANCEL;
  151. }
  152. // 处理下载链接
  153. if (uri.path.endsWith(".apk") ||
  154. uri.path.endsWith(".pdf") ||
  155. uri.path.contains("download")) {
  156. if (await canLaunchUrl(uri)) {
  157. await launchUrl(
  158. uri,
  159. mode: LaunchMode.externalApplication,
  160. );
  161. }
  162. return NavigationActionPolicy.CANCEL;
  163. }
  164. // 处理其他自定义 scheme(如 tel:, mailto:, weixin: 等)
  165. if (uri.scheme != 'http' &&
  166. uri.scheme != 'https' &&
  167. uri.scheme != 'file' &&
  168. uri.scheme != 'about') {
  169. if (await canLaunchUrl(uri)) {
  170. await launchUrl(
  171. uri,
  172. mode: LaunchMode.externalApplication,
  173. );
  174. return NavigationActionPolicy.CANCEL;
  175. }
  176. }
  177. } catch (e) {
  178. log("Error handling shouldOverrideUrlLoading: $e");
  179. }
  180. // 默认允许加载
  181. return NavigationActionPolicy.ALLOW;
  182. },
  183. ),
  184. // 加载进度条
  185. Obx(
  186. () => controller.isLoading.value
  187. ? LinearProgressIndicator(
  188. value: controller.loadingProgress.value,
  189. backgroundColor:
  190. Get.reactiveTheme.scaffoldBackgroundColor,
  191. minHeight: 2.w,
  192. valueColor: AlwaysStoppedAnimation<Color>(
  193. Get.reactiveTheme.primaryColor,
  194. ),
  195. )
  196. : const SizedBox.shrink(),
  197. ),
  198. ],
  199. ),
  200. ),
  201. );
  202. }
  203. }