web_view.dart 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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. return Obx(() {
  57. // 使用响应式状态栏样式
  58. final statusBarStyle = controller.currentStatusBarStyle.value;
  59. return AnnotatedRegion<SystemUiOverlayStyle>(
  60. value: statusBarStyle,
  61. child: Stack(
  62. children: [
  63. InAppWebView(
  64. initialUrlRequest: URLRequest(url: WebUri(controller.url)),
  65. initialSettings: InAppWebViewSettings(
  66. userAgent: controller.userAgent,
  67. javaScriptEnabled: true,
  68. useShouldOverrideUrlLoading: true,
  69. useOnLoadResource: true,
  70. mediaPlaybackRequiresUserGesture: false,
  71. allowsInlineMediaPlayback: true,
  72. iframeAllow: "camera; microphone",
  73. iframeAllowFullscreen: true,
  74. allowFileAccessFromFileURLs: true,
  75. allowUniversalAccessFromFileURLs: true,
  76. ),
  77. onWebViewCreated:
  78. (InAppWebViewController webViewController) async {
  79. controller.setWebViewController(webViewController);
  80. controller.setupJavaScriptHandlers(webViewController);
  81. // 如果 URL 是 assets 路径,则使用 loadData 加载
  82. if (controller.url.startsWith('assets/')) {
  83. await controller.loadAssetHtml(controller.url);
  84. }
  85. },
  86. onProgressChanged:
  87. (InAppWebViewController webViewController, int progress) {
  88. controller.loadingProgress.value = progress / 100;
  89. controller.isLoading.value = progress < 100;
  90. },
  91. onLoadStart:
  92. (InAppWebViewController webViewController, Uri? url) {
  93. controller.checkCanGoBack();
  94. },
  95. onLoadStop:
  96. (InAppWebViewController webViewController, Uri? url) async {
  97. controller.isLoading.value = false;
  98. controller.loadingProgress.value = 0.0;
  99. controller.checkCanGoBack();
  100. // 页面加载完成后再次注入,确保万无一失
  101. await controller.injectNativeAttrs();
  102. },
  103. onReceivedError:
  104. (
  105. InAppWebViewController webViewController,
  106. WebResourceRequest request,
  107. WebResourceError error,
  108. ) {
  109. controller.isLoading.value = false;
  110. controller.loadingProgress.value = 0.0;
  111. },
  112. shouldOverrideUrlLoading:
  113. (
  114. InAppWebViewController webViewController,
  115. NavigationAction navigationAction,
  116. ) async {
  117. final url = navigationAction.request.url?.toString() ?? '';
  118. try {
  119. final uri = Uri.parse(url);
  120. // 处理 intent:// 链接
  121. if (url.startsWith("intent://")) {
  122. final parsedUrl = controller.parseIntentUrl(url);
  123. final fallbackUrl = controller.parseFallbackUrl(url);
  124. if (parsedUrl != null &&
  125. await canLaunchUrl(Uri.parse(parsedUrl))) {
  126. await launchUrl(
  127. Uri.parse(parsedUrl),
  128. mode: LaunchMode.externalApplication,
  129. );
  130. } else if (fallbackUrl != null &&
  131. await canLaunchUrl(Uri.parse(fallbackUrl))) {
  132. await launchUrl(
  133. Uri.parse(fallbackUrl),
  134. mode: LaunchMode.externalApplication,
  135. );
  136. }
  137. return NavigationActionPolicy.CANCEL;
  138. }
  139. // 处理下载链接
  140. else if (uri.path.endsWith(".apk") ||
  141. uri.path.endsWith(".pdf") ||
  142. uri.path.contains("download")) {
  143. if (await canLaunchUrl(uri)) {
  144. await launchUrl(
  145. uri,
  146. mode: LaunchMode.externalApplication,
  147. );
  148. }
  149. return NavigationActionPolicy.CANCEL;
  150. }
  151. // 处理 http/https 链接
  152. if (uri.scheme == 'http' || uri.scheme == 'https') {
  153. return NavigationActionPolicy.ALLOW;
  154. }
  155. // 处理其他 scheme
  156. if (await canLaunchUrl(uri)) {
  157. await launchUrl(
  158. uri,
  159. mode: LaunchMode.externalApplication,
  160. );
  161. }
  162. } catch (e) {
  163. log("Error handling shouldOverrideUrlLoading: $e");
  164. }
  165. return NavigationActionPolicy.CANCEL;
  166. },
  167. ),
  168. Obx(
  169. () => controller.isLoading.value
  170. ? LinearProgressIndicator(
  171. value: controller.loadingProgress.value,
  172. backgroundColor:
  173. Get.reactiveTheme.scaffoldBackgroundColor,
  174. minHeight: 2.w,
  175. valueColor: AlwaysStoppedAnimation<Color>(
  176. Get.reactiveTheme.primaryColor,
  177. ),
  178. )
  179. : const SizedBox.shrink(),
  180. ),
  181. ],
  182. ),
  183. );
  184. });
  185. }
  186. }