From 7f1b8afa70f161c629f95119be35ccf35ebe2bff Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 1 Apr 2026 16:48:55 +0800 Subject: [PATCH] refactor: clean code --- packages/web_android_shell/.gitignore | 45 - packages/web_android_shell/.metadata | 45 - packages/web_android_shell/README.md | 7 - .../web_android_shell/analysis_options.yaml | 9 - packages/web_android_shell/android/.gitignore | 14 - .../android/app/build.gradle.kts | 99 - .../android/app/src/debug/AndroidManifest.xml | 7 - .../android/app/src/main/AndroidManifest.xml | 129 - .../web_android_shell/MainActivity.java | 241 -- .../res/drawable-v21/launch_background.xml | 12 - .../main/res/drawable/launch_background.xml | 12 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 0 bytes .../app/src/main/res/values-night/styles.xml | 18 - .../app/src/main/res/values/styles.xml | 18 - .../app/src/profile/AndroidManifest.xml | 7 - .../android/build.gradle.kts | 24 - .../android/gradle.properties | 6 - .../gradle/wrapper/gradle-wrapper.properties | 5 - .../android/settings.gradle.kts | 26 - packages/web_android_shell/lib/main.dart | 2078 ----------------- packages/web_android_shell/pubspec.lock | 578 ----- packages/web_android_shell/pubspec.yaml | 35 - .../web_android_shell/test/widget_test.dart | 10 - .../tool/flutter_run_fresh.ps1 | 187 -- 28 files changed, 3612 deletions(-) delete mode 100644 packages/web_android_shell/.gitignore delete mode 100644 packages/web_android_shell/.metadata delete mode 100644 packages/web_android_shell/README.md delete mode 100644 packages/web_android_shell/analysis_options.yaml delete mode 100644 packages/web_android_shell/android/.gitignore delete mode 100644 packages/web_android_shell/android/app/build.gradle.kts delete mode 100644 packages/web_android_shell/android/app/src/debug/AndroidManifest.xml delete mode 100644 packages/web_android_shell/android/app/src/main/AndroidManifest.xml delete mode 100644 packages/web_android_shell/android/app/src/main/java/com/yuanxuan/webshell/web_android_shell/MainActivity.java delete mode 100644 packages/web_android_shell/android/app/src/main/res/drawable-v21/launch_background.xml delete mode 100644 packages/web_android_shell/android/app/src/main/res/drawable/launch_background.xml delete mode 100644 packages/web_android_shell/android/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 packages/web_android_shell/android/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 packages/web_android_shell/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 packages/web_android_shell/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 packages/web_android_shell/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 packages/web_android_shell/android/app/src/main/res/values-night/styles.xml delete mode 100644 packages/web_android_shell/android/app/src/main/res/values/styles.xml delete mode 100644 packages/web_android_shell/android/app/src/profile/AndroidManifest.xml delete mode 100644 packages/web_android_shell/android/build.gradle.kts delete mode 100644 packages/web_android_shell/android/gradle.properties delete mode 100644 packages/web_android_shell/android/gradle/wrapper/gradle-wrapper.properties delete mode 100644 packages/web_android_shell/android/settings.gradle.kts delete mode 100644 packages/web_android_shell/lib/main.dart delete mode 100644 packages/web_android_shell/pubspec.lock delete mode 100644 packages/web_android_shell/pubspec.yaml delete mode 100644 packages/web_android_shell/test/widget_test.dart delete mode 100644 packages/web_android_shell/tool/flutter_run_fresh.ps1 diff --git a/packages/web_android_shell/.gitignore b/packages/web_android_shell/.gitignore deleted file mode 100644 index 6f0d006..0000000 --- a/packages/web_android_shell/.gitignore +++ /dev/null @@ -1,45 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.build/ -.buildlog/ -.history -.svn/ -.swiftpm/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins-dependencies -.pub-cache/ -.pub/ -/build/ -/coverage/ - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release diff --git a/packages/web_android_shell/.metadata b/packages/web_android_shell/.metadata deleted file mode 100644 index c0fe018..0000000 --- a/packages/web_android_shell/.metadata +++ /dev/null @@ -1,45 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "90673a4eef275d1a6692c26ac80d6d746d41a73a" - channel: "stable" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - - platform: android - create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - - platform: ios - create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - - platform: linux - create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - - platform: macos - create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - - platform: web - create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - - platform: windows - create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/web_android_shell/README.md b/packages/web_android_shell/README.md deleted file mode 100644 index f5015e2..0000000 --- a/packages/web_android_shell/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# web_android_shell(已迁移) - -> ⚠️ 本包为旧版入口,已迁移至 Monorepo 架构。 - -当前用途:为旧版 `MainActivity` 提供壳层入口,调用 `web_shell_core` 启动应用。 - -新品牌应用请使用 `apps/` 目录下的独立应用,参考 [`apps/quanxue/`](../../apps/quanxue/)。 diff --git a/packages/web_android_shell/analysis_options.yaml b/packages/web_android_shell/analysis_options.yaml deleted file mode 100644 index f13d6ae..0000000 --- a/packages/web_android_shell/analysis_options.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# Dart 分析器配置,用于在开发阶段发现错误、警告和代码规范问题。 -# 可通过 `flutter analyze` 执行静态检查。 -include: package:flutter_lints/flutter.yaml - -linter: - # 如需自定义规则,可在此处开启或关闭指定 lint。 - rules: - # avoid_print: false # 取消注释后可关闭 `avoid_print` 规则。 - # prefer_single_quotes: true # 取消注释后可开启 `prefer_single_quotes` 规则。 diff --git a/packages/web_android_shell/android/.gitignore b/packages/web_android_shell/android/.gitignore deleted file mode 100644 index c908258..0000000 --- a/packages/web_android_shell/android/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java -.cxx/ - -# Remember to never publicly share your keystore. -# See https://flutter.dev/to/reference-keystore -key.properties -**/*.keystore -**/*.jks diff --git a/packages/web_android_shell/android/app/build.gradle.kts b/packages/web_android_shell/android/app/build.gradle.kts deleted file mode 100644 index 5fcd3e1..0000000 --- a/packages/web_android_shell/android/app/build.gradle.kts +++ /dev/null @@ -1,99 +0,0 @@ -plugins { - id("com.android.application") - id("kotlin-android") - // Flutter Gradle 插件必须放在 Android 与 Kotlin Gradle 插件之后应用。 - id("dev.flutter.flutter-gradle-plugin") -} - -import java.io.File -import java.io.FileInputStream -import java.util.Properties - -val keystoreProperties = Properties() -val keystorePropertiesFile = rootProject.file("key.properties") -if (keystorePropertiesFile.exists()) { - FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) } -} - -val defaultReleaseStoreFile = rootProject.file("../../../tool/key.jks") -fun resolveKeystoreFile(rawPath: String?): File { - val candidate = rawPath?.trim().orEmpty() - if (candidate.isEmpty()) { - return defaultReleaseStoreFile - } - - val expandedHome = if (candidate.startsWith("~/") || candidate == "~") { - candidate.replaceFirst("~", System.getProperty("user.home")) - } else { - candidate - } - - val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar) - val storeFile = File(normalized) - if (storeFile.isAbsolute) { - return storeFile - } - - val rootRelativeFile = rootProject.file(normalized) - if (rootRelativeFile.exists()) { - return rootRelativeFile - } - - val moduleRelativeFile = project.file(normalized) - if (moduleRelativeFile.exists()) { - return moduleRelativeFile - } - - return rootRelativeFile -} - -val releaseStoreFile = resolveKeystoreFile(keystoreProperties["storeFile"] as String?) -val releaseKeyAlias = keystoreProperties["keyAlias"] as String? ?: "my-key-alias" -val releaseKeyPassword = keystoreProperties["keyPassword"] as String? ?: "123456" -val releaseStorePassword = keystoreProperties["storePassword"] as String? ?: "123456" - -android { - namespace = "com.yuanxuan.webshell.web_android_shell" - compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - - defaultConfig { - // 待补充:请替换成你自己的唯一应用标识。 - applicationId = "com.yuanxuan.webshell.web_android_shell" - // 下面这些值可以按应用实际需求调整。 - // 更多说明可参考:https://flutter.dev/to/review-gradle-config。 - minSdk = flutter.minSdkVersion - // 为兼容旧版系统 WebView,这里保持稳定的 targetSdk。 - targetSdk = 34 - versionCode = flutter.versionCode - versionName = flutter.versionName - } - - signingConfigs { - create("release") { - keyAlias = releaseKeyAlias - keyPassword = releaseKeyPassword - storeFile = releaseStoreFile - storePassword = releaseStorePassword - } - } - - buildTypes { - release { - signingConfig = signingConfigs.getByName("release") - } - } -} - -flutter { - source = "../.." -} diff --git a/packages/web_android_shell/android/app/src/debug/AndroidManifest.xml b/packages/web_android_shell/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 8ffe024..0000000 --- a/packages/web_android_shell/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/packages/web_android_shell/android/app/src/main/AndroidManifest.xml b/packages/web_android_shell/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index fe3afb2..0000000 --- a/packages/web_android_shell/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/web_android_shell/android/app/src/main/java/com/yuanxuan/webshell/web_android_shell/MainActivity.java b/packages/web_android_shell/android/app/src/main/java/com/yuanxuan/webshell/web_android_shell/MainActivity.java deleted file mode 100644 index ee38a3e..0000000 --- a/packages/web_android_shell/android/app/src/main/java/com/yuanxuan/webshell/web_android_shell/MainActivity.java +++ /dev/null @@ -1,241 +0,0 @@ -package com.yuanxuan.webshell.web_android_shell; - -import android.app.ActivityManager; -import android.content.SharedPreferences; -import android.content.pm.PackageInfo; -import android.os.Build; -import android.os.Bundle; -import android.os.Process; -import android.os.SystemClock; -import android.webkit.CookieManager; -import android.webkit.WebStorage; -import android.webkit.WebView; - -import androidx.annotation.NonNull; - -import java.io.File; -import java.util.HashMap; -import java.util.Map; - -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugin.common.MethodChannel; - -public class MainActivity extends FlutterActivity { - private static final String DEVICE_CHANNEL = "app_shell/device"; - private static final String DEBUG_PROCESS_PREFS = "app_shell_debug_process"; - private static final String DEBUG_PROCESS_PID_KEY = "pid"; - private String webViewDataDirectorySuffix; - - @Override - protected void onCreate(Bundle savedInstanceState) { - terminatePreviousDebugProcessIfNeeded(); - prepareWebViewDataDirectory(); - super.onCreate(savedInstanceState); - registerCurrentDebugProcess(); - } - - @Override - protected void onDestroy() { - clearCurrentDebugProcessRegistration(); - super.onDestroy(); - } - - @Override - public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { - super.configureFlutterEngine(flutterEngine); - new MethodChannel( - flutterEngine.getDartExecutor().getBinaryMessenger(), - DEVICE_CHANNEL - ).setMethodCallHandler((call, result) -> { - switch (call.method) { - case "getAndroidWebViewInfo": - result.success(buildAndroidWebViewInfo()); - break; - case "resetAndroidWebViewState": - resetAndroidWebViewState(result); - break; - default: - result.notImplemented(); - break; - } - }); - } - - private Map buildAndroidWebViewInfo() { - final Map info = new HashMap<>(); - info.put("sdkInt", Build.VERSION.SDK_INT); - info.put("manufacturer", Build.MANUFACTURER); - info.put("brand", Build.BRAND); - info.put("model", Build.MODEL); - if (webViewDataDirectorySuffix != null) { - info.put("webViewDataDirectorySuffix", webViewDataDirectorySuffix); - } - - final PackageInfo currentWebViewPackage = getCurrentWebViewPackageCompat(); - if (currentWebViewPackage != null) { - info.put("webViewPackageName", currentWebViewPackage.packageName); - if (currentWebViewPackage.versionName != null) { - info.put("webViewVersionName", currentWebViewPackage.versionName); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - info.put( - "webViewLongVersionCode", - currentWebViewPackage.getLongVersionCode() - ); - } else { - info.put( - "webViewLongVersionCode", - (long) currentWebViewPackage.versionCode - ); - } - } - return info; - } - - private void prepareWebViewDataDirectory() { - webViewDataDirectorySuffix = null; - if (!isDebuggableBuild() || Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - return; - } - - clearLegacyDebugWebViewData(); - } - - private void terminatePreviousDebugProcessIfNeeded() { - if (!isDebuggableBuild()) { - return; - } - - final SharedPreferences preferences = getSharedPreferences( - DEBUG_PROCESS_PREFS, - MODE_PRIVATE - ); - final int currentPid = Process.myPid(); - final int previousPid = preferences.getInt(DEBUG_PROCESS_PID_KEY, -1); - if (previousPid <= 0 || previousPid == currentPid) { - return; - } - - if (!isSamePackageProcessRunning(previousPid)) { - preferences.edit().remove(DEBUG_PROCESS_PID_KEY).apply(); - return; - } - - try { - Process.killProcess(previousPid); - SystemClock.sleep(180); - } catch (RuntimeException ignored) { - // 忽略终止旧进程失败的情况,并继续使用当前进程启动。 - } - } - - private void registerCurrentDebugProcess() { - if (!isDebuggableBuild()) { - return; - } - - getSharedPreferences(DEBUG_PROCESS_PREFS, MODE_PRIVATE) - .edit() - .putInt(DEBUG_PROCESS_PID_KEY, Process.myPid()) - .apply(); - } - - private void clearCurrentDebugProcessRegistration() { - if (!isDebuggableBuild()) { - return; - } - - final SharedPreferences preferences = getSharedPreferences( - DEBUG_PROCESS_PREFS, - MODE_PRIVATE - ); - if (preferences.getInt(DEBUG_PROCESS_PID_KEY, -1) != Process.myPid()) { - return; - } - preferences.edit().remove(DEBUG_PROCESS_PID_KEY).apply(); - } - - private boolean isSamePackageProcessRunning(int pid) { - final ActivityManager activityManager = - (ActivityManager) getSystemService(ACTIVITY_SERVICE); - if (activityManager == null) { - return false; - } - - for (final ActivityManager.RunningAppProcessInfo processInfo - : activityManager.getRunningAppProcesses()) { - if (processInfo.pid == pid) { - return getPackageName().equals(processInfo.processName); - } - } - return false; - } - - private void clearLegacyDebugWebViewData() { - try { - final File appWebViewDir = new File(getApplicationInfo().dataDir, "app_webview"); - deleteRecursively(appWebViewDir); - deleteDatabase("webview.db"); - deleteDatabase("webviewCache.db"); - } catch (RuntimeException ignored) { - // 忽略旧设备上的清理失败,继续执行启动流程。 - } - } - - private boolean isDebuggableBuild() { - return (getApplicationInfo().flags & android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0; - } - - private void deleteRecursively(File target) { - if (target == null || !target.exists()) { - return; - } - - if (target.isDirectory()) { - final File[] children = target.listFiles(); - if (children != null) { - for (final File child : children) { - deleteRecursively(child); - } - } - } - - - target.delete(); - } - - private PackageInfo getCurrentWebViewPackageCompat() { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - return WebView.getCurrentWebViewPackage(); - } - } catch (RuntimeException ignored) { - return null; - } - return null; - } - - private void resetAndroidWebViewState(@NonNull MethodChannel.Result result) { - try { - WebStorage.getInstance().deleteAllData(); - - final CookieManager cookieManager = CookieManager.getInstance(); - cookieManager.removeSessionCookies(null); - cookieManager.removeAllCookies(value -> { - try { - cookieManager.flush(); - WebView.clearClientCertPreferences(() -> result.success(true)); - } catch (RuntimeException error) { - result.error( - "webview_reset_failed", - error.toString(), - null - ); - } - }); - } catch (RuntimeException error) { - result.error("webview_reset_failed", error.toString(), null); - } - } -} diff --git a/packages/web_android_shell/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/web_android_shell/android/app/src/main/res/drawable-v21/launch_background.xml deleted file mode 100644 index 1cb7aa2..0000000 --- a/packages/web_android_shell/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/packages/web_android_shell/android/app/src/main/res/drawable/launch_background.xml b/packages/web_android_shell/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 8403758..0000000 --- a/packages/web_android_shell/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/packages/web_android_shell/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/web_android_shell/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/packages/web_android_shell/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/web_android_shell/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/packages/web_android_shell/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/web_android_shell/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/packages/web_android_shell/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/web_android_shell/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb28e45604e46eeda8dd24651419bc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/packages/web_android_shell/android/app/src/main/res/values-night/styles.xml b/packages/web_android_shell/android/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 360a160..0000000 --- a/packages/web_android_shell/android/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/packages/web_android_shell/android/app/src/main/res/values/styles.xml b/packages/web_android_shell/android/app/src/main/res/values/styles.xml deleted file mode 100644 index 5fac679..0000000 --- a/packages/web_android_shell/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/packages/web_android_shell/android/app/src/profile/AndroidManifest.xml b/packages/web_android_shell/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 8ffe024..0000000 --- a/packages/web_android_shell/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/packages/web_android_shell/android/build.gradle.kts b/packages/web_android_shell/android/build.gradle.kts deleted file mode 100644 index 1f88145..0000000 --- a/packages/web_android_shell/android/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -allprojects { - repositories { - google() - mavenCentral() - } -} - -val newBuildDir: Directory = - rootProject.layout.buildDirectory - .dir("../../build") - .get() -rootProject.layout.buildDirectory.value(newBuildDir) - -subprojects { - val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) - project.layout.buildDirectory.value(newSubprojectBuildDir) -} -subprojects { - project.evaluationDependsOn(":app") -} - -tasks.register("clean") { - delete(rootProject.layout.buildDirectory) -} diff --git a/packages/web_android_shell/android/gradle.properties b/packages/web_android_shell/android/gradle.properties deleted file mode 100644 index 4623a70..0000000 --- a/packages/web_android_shell/android/gradle.properties +++ /dev/null @@ -1,6 +0,0 @@ -org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -org.gradle.parallel=false -org.gradle.caching=false -org.gradle.vfs.watch=false -org.gradle.workers.max=1 -android.useAndroidX=true diff --git a/packages/web_android_shell/android/gradle/wrapper/gradle-wrapper.properties b/packages/web_android_shell/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 568dedc..0000000 --- a/packages/web_android_shell/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.14-bin.zip diff --git a/packages/web_android_shell/android/settings.gradle.kts b/packages/web_android_shell/android/settings.gradle.kts deleted file mode 100644 index 4dcef4b..0000000 --- a/packages/web_android_shell/android/settings.gradle.kts +++ /dev/null @@ -1,26 +0,0 @@ -pluginManagement { - val flutterSdkPath = - run { - val properties = java.util.Properties() - file("local.properties").inputStream().use { properties.load(it) } - val flutterSdkPath = properties.getProperty("flutter.sdk") - require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } - flutterSdkPath - } - - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.11.1" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false -} - -include(":app") diff --git a/packages/web_android_shell/lib/main.dart b/packages/web_android_shell/lib/main.dart deleted file mode 100644 index f10f27b..0000000 --- a/packages/web_android_shell/lib/main.dart +++ /dev/null @@ -1,2078 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:webview_flutter/webview_flutter.dart'; -import 'package:webview_flutter_android/webview_flutter_android.dart'; - -const String _defaultInitialUrl = 'http://xszy.lzzneng.com/login.html'; -const String _configuredInitialUrl = String.fromEnvironment( - 'APP_INITIAL_URL', - defaultValue: _defaultInitialUrl, -); -const String _appShellBridgeChannel = 'AppShellBridge'; -const String _appShellBridgeVersion = '1.0.0'; -const Duration _releaseStartupWatchdogDuration = Duration(seconds: 15); -const Duration _debugStartupWatchdogDuration = Duration(seconds: 15); -const double _pickedImageMaxWidth = 1600; -const double _pickedImageMaxHeight = 1600; -const int _pickedImageQuality = 85; -const MethodChannel _appShellDeviceChannel = MethodChannel('app_shell/device'); -const int _legacyWebViewMajorVersionThreshold = 110; -const Color _shellAccentColor = Color(0xFF3ED37B); -const Color _shellBackgroundColor = Color(0xFFFFFFFF); -const Color _shellTextColor = Color(0xFF1F2937); -const Color _shellMutedTextColor = Color(0xFF6B7280); - -final Uri _initialUri = _resolveInitialUri(); -final String _initialUrl = _initialUri.toString(); - -Uri _resolveInitialUri() { - final String candidate = _configuredInitialUrl.trim(); - if (candidate.isEmpty) { - return Uri.parse(_defaultInitialUrl); - } - - final Uri? directUri = Uri.tryParse(candidate); - if (directUri != null && directUri.hasScheme) { - return directUri; - } - - if (candidate.startsWith('//')) { - final Uri? protocolRelativeUri = Uri.tryParse('https:$candidate'); - if (protocolRelativeUri != null && protocolRelativeUri.hasScheme) { - return protocolRelativeUri; - } - } - - final Uri? httpsUri = Uri.tryParse('https://$candidate'); - if (httpsUri != null && httpsUri.hasScheme && httpsUri.host.isNotEmpty) { - return httpsUri; - } - return Uri.parse(_defaultInitialUrl); -} - -bool _supportsEmbeddedWebView({bool isWeb = kIsWeb, TargetPlatform? platform}) { - if (isWeb) { - return false; - } - - final TargetPlatform target = platform ?? defaultTargetPlatform; - return target == TargetPlatform.android; -} - -Future main() async { - WidgetsFlutterBinding.ensureInitialized(); - await SystemChrome.setPreferredOrientations(const [ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); - await _enterImmersiveMode(); - runApp(const WebShellApp()); -} - -Future _enterImmersiveMode() async { - await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); - SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: Brightness.light, - statusBarBrightness: Brightness.dark, - systemNavigationBarColor: Colors.black, - systemNavigationBarDividerColor: Colors.black, - systemNavigationBarIconBrightness: Brightness.light, - ), - ); -} - -enum _AndroidRenderMode { - texture, - hybrid; - - bool get usesHybridComposition => this == _AndroidRenderMode.hybrid; - - String get logName => switch (this) { - _AndroidRenderMode.texture => 'texture-layer', - _AndroidRenderMode.hybrid => 'hybrid-composition', - }; - - String get displayName => switch (this) { - _AndroidRenderMode.texture => '标准模式', - _AndroidRenderMode.hybrid => '兼容模式', - }; -} - -class _AndroidWebViewInfo { - _AndroidWebViewInfo({ - required this.sdkInt, - required this.manufacturer, - required this.brand, - required this.model, - this.webViewDataDirectorySuffix, - this.webViewPackageName, - this.webViewVersionName, - this.webViewLongVersionCode, - }) : webViewMajorVersion = _parseWebViewMajorVersion(webViewVersionName); - - factory _AndroidWebViewInfo.fromMap(Map raw) { - return _AndroidWebViewInfo( - sdkInt: _readInt(raw['sdkInt']), - manufacturer: _readString(raw['manufacturer']), - brand: _readString(raw['brand']), - model: _readString(raw['model']), - webViewDataDirectorySuffix: _readNullableString( - raw['webViewDataDirectorySuffix'], - ), - webViewPackageName: _readNullableString(raw['webViewPackageName']), - webViewVersionName: _readNullableString(raw['webViewVersionName']), - webViewLongVersionCode: _readNullableInt(raw['webViewLongVersionCode']), - ); - } - - final int sdkInt; - final String manufacturer; - final String brand; - final String model; - final String? webViewDataDirectorySuffix; - final String? webViewPackageName; - final String? webViewVersionName; - final int? webViewLongVersionCode; - final int? webViewMajorVersion; - - bool get isLegacyWebView => - webViewMajorVersion != null && - webViewMajorVersion! <= _legacyWebViewMajorVersionThreshold; - - bool get isF136A => model.toUpperCase() == 'F136A'; - - String get summary { - final List parts = [ - 'sdk=$sdkInt', - if (manufacturer.isNotEmpty || model.isNotEmpty) - 'device=${[manufacturer, model].where((part) => part.isNotEmpty).join(' ')}', - if (webViewPackageName case final String packageName - when packageName.isNotEmpty) - 'webViewPackage=$packageName', - if (webViewVersionName case final String versionName - when versionName.isNotEmpty) - 'webViewVersion=$versionName', - if (webViewDataDirectorySuffix case final String suffix - when suffix.isNotEmpty) - 'webViewSuffix=$suffix', - ]; - return parts.join(', '); - } - - static int _readInt(Object? value, {int fallback = 0}) { - if (value is int) { - return value; - } - return int.tryParse((value ?? '').toString()) ?? fallback; - } - - static int? _readNullableInt(Object? value) { - if (value == null) { - return null; - } - if (value is int) { - return value; - } - return int.tryParse(value.toString()); - } - - static String _readString(Object? value) { - return (value ?? '').toString().trim(); - } - - static String? _readNullableString(Object? value) { - final String normalized = _readString(value); - return normalized.isEmpty ? null : normalized; - } -} - -class _AndroidCompatibilityPlan { - const _AndroidCompatibilityPlan({ - required this.renderModes, - required this.useWideViewPort, - required this.suggestWebViewUpdate, - required this.prefersAggressiveRecovery, - }); - - factory _AndroidCompatibilityPlan.fallback() { - return _AndroidCompatibilityPlan( - renderModes: kDebugMode - ? const <_AndroidRenderMode>[ - _AndroidRenderMode.texture, - _AndroidRenderMode.hybrid, - ] - : const <_AndroidRenderMode>[ - _AndroidRenderMode.hybrid, - _AndroidRenderMode.texture, - ], - useWideViewPort: true, - suggestWebViewUpdate: false, - prefersAggressiveRecovery: true, - ); - } - - factory _AndroidCompatibilityPlan.fromInfo(_AndroidWebViewInfo? info) { - if (info == null) { - return _AndroidCompatibilityPlan.fallback(); - } - - final bool legacyAndroid = info.sdkInt <= 28; - final bool legacyWebView = info.isLegacyWebView; - final bool preferHybridFirst = - info.isF136A || legacyAndroid || legacyWebView; - - return _AndroidCompatibilityPlan( - renderModes: preferHybridFirst - ? const <_AndroidRenderMode>[ - _AndroidRenderMode.hybrid, - _AndroidRenderMode.texture, - ] - : const <_AndroidRenderMode>[ - _AndroidRenderMode.texture, - _AndroidRenderMode.hybrid, - ], - useWideViewPort: true, - suggestWebViewUpdate: info.isF136A || legacyWebView || info.sdkInt <= 26, - prefersAggressiveRecovery: info.isF136A || legacyAndroid || legacyWebView, - ); - } - - final List<_AndroidRenderMode> renderModes; - final bool useWideViewPort; - final bool suggestWebViewUpdate; - final bool prefersAggressiveRecovery; - - String describe() { - return [ - 'modes=${renderModes.map((mode) => mode.logName).join(' -> ')}', - 'wideViewport=$useWideViewPort', - 'aggressiveRecovery=$prefersAggressiveRecovery', - ].join(', '); - } -} - -int? _parseWebViewMajorVersion(String? versionName) { - if (versionName == null || versionName.isEmpty) { - return null; - } - final String candidate = versionName.split('.').first.trim(); - return int.tryParse(candidate); -} - -class WebShellApp extends StatelessWidget { - const WebShellApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - debugShowCheckedModeBanner: false, - home: _supportsEmbeddedWebView() - ? const WebShellPage() - : const UnsupportedPlatformPage(), - ); - } -} - -class WebShellPage extends StatefulWidget { - const WebShellPage({super.key}); - - @override - State createState() => _WebShellPageState(); -} - -class _WebShellPageState extends State - with WidgetsBindingObserver { - final ImagePicker _imagePicker = ImagePicker(); - final WebViewCookieManager _cookieManager = WebViewCookieManager(); - - late WebViewController _controller; - late WebViewWidget _webViewWidget; - late Future _controllerSetupFuture; - late final Future _androidCompatibilityFuture; - _AndroidWebViewInfo? _androidWebViewInfo; - _AndroidCompatibilityPlan _androidCompatibilityPlan = - _AndroidCompatibilityPlan.fallback(); - _AndroidRenderMode? _configuredRenderMode; - int _webViewGeneration = 0; - int _renderModeIndex = 0; - - bool _hasTriggeredInitialLoad = false; - bool _hasAppliedCompatibilityPlan = false; - bool _isLoadingPage = false; - bool _hasBootstrapped = false; - bool _hasStartedRemoteMainFrame = false; - bool _hasMainFrameError = false; - bool _hasMeasuredProgress = false; - Timer? _startupWatchdogTimer; - int _progress = 0; - int _startupRetryCount = 0; - String _currentUrl = _initialUrl; - String _errorTitle = '页面加载失败'; - String _errorMessage = '请检查网络后重试。'; - String _lastWebViewEvent = '应用启动'; - - @override - void initState() { - super.initState(); - debugPrint('WebShell 初始化,初始地址=$_initialUrl'); - WidgetsBinding.instance.addObserver(this); - _androidCompatibilityFuture = _prepareAndroidCompatibility(); - _recreateWebView(); - - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted || _hasTriggeredInitialLoad) { - return; - } - _hasTriggeredInitialLoad = true; - unawaited(_loadInitialPage()); - }); - } - - int get _safeRenderModeIndex { - if (_renderModeIndex < 0) { - return 0; - } - final int maxIndex = _androidCompatibilityPlan.renderModes.length - 1; - if (_renderModeIndex > maxIndex) { - return maxIndex; - } - return _renderModeIndex; - } - - _AndroidRenderMode get _activeRenderMode => - _androidCompatibilityPlan.renderModes[_safeRenderModeIndex]; - - Duration get _effectiveStartupWatchdogDuration { - if (defaultTargetPlatform != TargetPlatform.android) { - return _releaseStartupWatchdogDuration; - } - if (kDebugMode) { - return _debugStartupWatchdogDuration; - } - return _releaseStartupWatchdogDuration; - } - - Future _prepareAndroidCompatibility() async { - if (defaultTargetPlatform != TargetPlatform.android) { - return; - } - - try { - final Map? rawInfo = await _appShellDeviceChannel - .invokeMapMethod('getAndroidWebViewInfo'); - if (rawInfo != null) { - _androidWebViewInfo = _AndroidWebViewInfo.fromMap(rawInfo); - } - } catch (error, stackTrace) { - debugPrint('读取 Android WebView 信息失败:$error\n$stackTrace'); - } - - _androidCompatibilityPlan = _AndroidCompatibilityPlan.fromInfo( - _androidWebViewInfo, - ); - _renderModeIndex = 0; - debugPrint( - 'WebShell Android WebView 信息:' - '${_androidWebViewInfo?.summary ?? '不可用'}', - ); - debugPrint( - 'WebShell 兼容策略:${_androidCompatibilityPlan.describe()}', - ); - } - - Future _ensureCompatibilityPlanApplied() async { - await _androidCompatibilityFuture; - if (!mounted || - _hasAppliedCompatibilityPlan || - defaultTargetPlatform != TargetPlatform.android) { - return; - } - - _hasAppliedCompatibilityPlan = true; - if (_configuredRenderMode == _activeRenderMode) { - debugPrint( - 'WebShell 兼容策略保持当前 WebView ' - '(${_activeRenderMode.logName})', - ); - return; - } - _recreateWebView(); - } - - bool _switchToNextRenderMode() { - final int nextIndex = _safeRenderModeIndex + 1; - if (nextIndex >= _androidCompatibilityPlan.renderModes.length) { - return false; - } - - _renderModeIndex = nextIndex; - debugPrint('WebShell 已切换渲染模式为 ${_activeRenderMode.logName}'); - return true; - } - - String _buildCompatibilityGuidance() { - if (defaultTargetPlatform != TargetPlatform.android) { - return ''; - } - - final List lines = [ - '当前渲染策略:${_activeRenderMode.displayName}', - if (_androidWebViewInfo?.model case final String model - when model.isNotEmpty) - '当前设备:$model', - if (_androidWebViewInfo?.webViewVersionName case final String version - when version.isNotEmpty) - '系统 WebView:$version', - if (_androidCompatibilityPlan.suggestWebViewUpdate) - '如果内嵌页面仍异常,建议更新 Android System WebView 或 Chrome。', - ]; - return lines.join('\n'); - } - - void _recreateWebView() { - final PlatformWebViewControllerCreationParams controllerParams = - const PlatformWebViewControllerCreationParams(); - final WebViewController controller = - WebViewController.fromPlatformCreationParams(controllerParams); - final int generation = ++_webViewGeneration; - final _AndroidRenderMode renderMode = _activeRenderMode; - - _controller = controller; - _controllerSetupFuture = _configureController(controller, generation); - _configuredRenderMode = renderMode; - debugPrint( - 'WebShell 正在以 ${renderMode.logName} 重建 WebView #$generation', - ); - PlatformWebViewWidgetCreationParams widgetParams = - PlatformWebViewWidgetCreationParams( - key: ValueKey('webview-$generation-${renderMode.logName}'), - controller: controller.platform, - layoutDirection: TextDirection.ltr, - ); - - if (defaultTargetPlatform == TargetPlatform.android) { - widgetParams = - AndroidWebViewWidgetCreationParams.fromPlatformWebViewWidgetCreationParams( - widgetParams, - displayWithHybridComposition: renderMode.usesHybridComposition, - ); - } - - _webViewWidget = WebViewWidget.fromPlatformCreationParams( - params: widgetParams, - ); - } - - bool _isActiveWebViewGeneration(int generation) { - return generation == _webViewGeneration; - } - - Future _configureController( - WebViewController controller, - int generation, - ) async { - await controller.setJavaScriptMode(JavaScriptMode.unrestricted); - await controller.enableZoom(false); - await controller.setBackgroundColor(Colors.white); - await controller.addJavaScriptChannel( - _appShellBridgeChannel, - onMessageReceived: (JavaScriptMessage message) { - if (!_isActiveWebViewGeneration(generation)) { - return; - } - unawaited(_handleBridgeMessage(message.message)); - }, - ); - await controller.setNavigationDelegate( - _buildNavigationDelegate(generation), - ); - - if (controller.platform is AndroidWebViewController) { - final AndroidWebViewController androidController = - controller.platform as AndroidWebViewController; - - await AndroidWebViewController.enableDebugging( - kDebugMode && !_androidCompatibilityPlan.prefersAggressiveRecovery, - ); - await androidController.setMediaPlaybackRequiresUserGesture(false); - await androidController.setMixedContentMode(MixedContentMode.alwaysAllow); - await androidController.setOverScrollMode(WebViewOverScrollMode.never); - await androidController.setUseWideViewPort( - _androidCompatibilityPlan.useWideViewPort, - ); - await androidController.setTextZoom(100); - await androidController.setVerticalScrollBarEnabled(false); - await androidController.setHorizontalScrollBarEnabled(false); - await androidController.setOnShowFileSelector(_handleFileSelector); - await androidController.setOnPlatformPermissionRequest( - _handlePlatformPermissionRequest, - ); - await androidController.setGeolocationPermissionsPromptCallbacks( - onShowPrompt: _handleGeolocationPermissionRequest, - ); - await androidController.setOnConsoleMessage((message) { - debugPrint( - 'WebView 控制台 [${message.level.name}] ${message.message}', - ); - }); - } - } - - NavigationDelegate _buildNavigationDelegate(int generation) { - return NavigationDelegate( - onProgress: (int progress) { - if (!_isActiveWebViewGeneration(generation)) { - return; - } - if (progress == 10 || - progress == 30 || - progress == 60 || - progress == 90) { - _recordWebViewEvent('加载进度 $progress%'); - } - if (!mounted) { - return; - } - setState(() { - _progress = progress; - if (_hasStartedRemoteMainFrame || progress >= 100) { - _hasMeasuredProgress = true; - } - }); - }, - onNavigationRequest: (NavigationRequest request) async { - if (!_isActiveWebViewGeneration(generation)) { - return NavigationDecision.navigate; - } - return _handleNavigationRequest(request); - }, - onUrlChange: (UrlChange change) { - if (!_isActiveWebViewGeneration(generation)) { - return; - } - final String? url = change.url; - if (url == null || url.isEmpty) { - return; - } - _recordWebViewEvent('地址变化:$url'); - if (!mounted) { - return; - } - setState(() { - _currentUrl = url; - }); - }, - onPageStarted: (url) { - if (!_isActiveWebViewGeneration(generation)) { - return; - } - if (!_isNetworkUrl(url)) { - debugPrint('WebView 忽略非网络页面开始事件:$url'); - return; - } - _hasStartedRemoteMainFrame = true; - _cancelStartupWatchdog(); - _recordWebViewEvent('页面开始加载:$url'); - if (!mounted) { - return; - } - setState(() { - _currentUrl = url; - if (_hasMeasuredProgress) { - _progress = _progress < 30 ? 30 : _progress; - } - _isLoadingPage = true; - _hasBootstrapped = true; - _hasMainFrameError = false; - _errorTitle = '页面加载失败'; - _errorMessage = '请检查网络后重试。'; - }); - }, - onPageFinished: (url) { - if (!_isActiveWebViewGeneration(generation)) { - return; - } - if (!_isNetworkUrl(url)) { - debugPrint('WebView 忽略非网络页面完成事件:$url'); - return; - } - _recordWebViewEvent('页面加载完成:$url'); - _cancelStartupWatchdog(); - unawaited(_injectAppShellBridge(url)); - if (!mounted) { - return; - } - setState(() { - _currentUrl = url; - _hasMeasuredProgress = true; - _progress = 100; - _isLoadingPage = false; - _hasBootstrapped = true; - _hasMainFrameError = false; - _startupRetryCount = 0; - }); - }, - onHttpError: (HttpResponseError error) { - if (!_isActiveWebViewGeneration(generation)) { - return; - } - final int? statusCode = error.response?.statusCode; - final Uri? requestUri = error.request?.uri; - if (!_shouldTreatHttpErrorAsMainFrame(error)) { - debugPrint( - '忽略子资源 HTTP 错误:' - '${statusCode ?? '未知'} ${requestUri ?? '未知'}', - ); - return; - } - _recordWebViewEvent( - 'HTTP 错误:${statusCode ?? '未知'} ${requestUri ?? _currentUrl}', - ); - _setMainFrameError( - title: statusCode == null ? '服务器响应异常' : '服务器异常 $statusCode', - message: statusCode == null - ? '页面返回异常,请稍后重试。' - : '页面返回了 $statusCode 状态码,请稍后重试。', - ); - }, - onWebResourceError: (error) { - if (!_isActiveWebViewGeneration(generation)) { - return; - } - _recordWebViewEvent( - '资源错误:code=${error.errorCode}, ' - 'type=${error.errorType}, ' - 'mainFrame=${error.isForMainFrame}, ' - 'url=${error.url}, ' - 'description=${error.description}', - ); - if (!mounted || error.isForMainFrame == false) { - return; - } - _setMainFrameError( - title: _friendlyErrorTitle(error), - message: _friendlyErrorMessage(error), - ); - }, - ); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.resumed) { - _enterImmersiveMode(); - } - } - - @override - void dispose() { - _cancelStartupWatchdog(); - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } - - Future _loadInitialPage() async { - if (!mounted) { - return; - } - await _ensureCompatibilityPlanApplied(); - if (!mounted) { - return; - } - debugPrint('WebShell 首帧已就绪,开始初始加载'); - await _startLoadSequence(rebuildWebView: false, resetRetryCount: true); - } - - Future _handleBackPressed() async { - if (await _controller.canGoBack()) { - await _controller.goBack(); - return; - } - await SystemNavigator.pop(); - } - - Future _handleNavigationRequest( - NavigationRequest request, - ) async { - final Uri? uri = Uri.tryParse(request.url); - if (uri == null || _shouldStayInWebView(uri)) { - return NavigationDecision.navigate; - } - - final bool opened = await _openExternalUri(uri); - if (!opened) { - await _showWebAlert('无法打开外部应用:${uri.scheme}'); - } - return NavigationDecision.prevent; - } - - bool _isNetworkUrl(String? url) { - final Uri? uri = Uri.tryParse(url ?? ''); - if (uri == null) { - return false; - } - return uri.scheme == 'http' || uri.scheme == 'https'; - } - - bool _shouldStayInWebView(Uri uri) { - return { - 'http', - 'https', - 'about', - 'data', - 'javascript', - 'file', - 'blob', - }.contains(uri.scheme.toLowerCase()); - } - - void _recordWebViewEvent(String event) { - _lastWebViewEvent = event; - debugPrint('WebView 事件:$event'); - } - - void _armStartupWatchdog() { - _cancelStartupWatchdog(); - _startupWatchdogTimer = Timer(_effectiveStartupWatchdogDuration, () { - unawaited(_handleStartupTimeout()); - }); - } - - void _cancelStartupWatchdog() { - _startupWatchdogTimer?.cancel(); - _startupWatchdogTimer = null; - } - - Future _handleStartupTimeout() async { - if (!mounted || - !_isLoadingPage || - _hasMainFrameError || - _hasStartedRemoteMainFrame) { - return; - } - - final bool switchedRenderMode = _switchToNextRenderMode(); - final int maxRetryCount = - _androidCompatibilityPlan.prefersAggressiveRecovery ? 2 : 1; - - if (switchedRenderMode || _startupRetryCount < maxRetryCount) { - final int nextRetryCount = _startupRetryCount + 1; - final String recoveryAction = switchedRenderMode - ? '切换到${_activeRenderMode.displayName}' - : '深度清理 WebView 状态'; - _recordWebViewEvent('启动超时,$recoveryAction 并自动重试第 $nextRetryCount 次'); - setState(() { - _startupRetryCount = nextRetryCount; - _hasMeasuredProgress = false; - _progress = 0; - _isLoadingPage = true; - }); - await _recoverFromBrokenStartupState(deepReset: true); - await _startLoadSequence(rebuildWebView: true, resetRetryCount: false); - return; - } - - _setMainFrameError( - title: '页面启动超时', - message: [ - '${_effectiveStartupWatchdogDuration.inSeconds} 秒内没有完成页面加载。', - '最近事件:$_lastWebViewEvent', - _buildCompatibilityGuidance(), - ].where((line) => line.isNotEmpty).join('\n'), - ); - } - - void _setMainFrameError({required String title, required String message}) { - _cancelStartupWatchdog(); - if (!mounted) { - return; - } - setState(() { - _isLoadingPage = false; - _hasMainFrameError = true; - _hasMeasuredProgress = false; - _errorTitle = title; - _errorMessage = message; - _progress = 0; - }); - } - - Future _recoverFromBrokenStartupState({bool deepReset = false}) async { - final Future controllerSetupFuture = _controllerSetupFuture; - try { - await controllerSetupFuture; - } catch (error, stackTrace) { - debugPrint('等待 WebView 控制器初始化失败:$error\n$stackTrace'); - } - - try { - await _controller.clearCache(); - } catch (error, stackTrace) { - debugPrint('清理 WebView 缓存失败:$error\n$stackTrace'); - } - - try { - await _controller.clearLocalStorage(); - } catch (error, stackTrace) { - debugPrint('清理 WebView 本地存储失败:$error\n$stackTrace'); - } - - try { - await _cookieManager.clearCookies(); - } catch (error, stackTrace) { - debugPrint('清理 WebView Cookie 失败:$error\n$stackTrace'); - } - - if (!deepReset || defaultTargetPlatform != TargetPlatform.android) { - return; - } - - try { - await _appShellDeviceChannel.invokeMethod( - 'resetAndroidWebViewState', - ); - } catch (error, stackTrace) { - debugPrint('重置 Android WebView 状态失败:$error\n$stackTrace'); - } - } - - String _friendlyErrorTitle(WebResourceError error) { - switch (error.errorType) { - case WebResourceErrorType.timeout: - return '请求超时'; - case WebResourceErrorType.hostLookup: - case WebResourceErrorType.connect: - case WebResourceErrorType.io: - return '网络连接失败'; - case WebResourceErrorType.failedSslHandshake: - return '安全连接失败'; - default: - return '页面加载失败'; - } - } - - String _friendlyErrorMessage(WebResourceError error) { - switch (error.errorType) { - case WebResourceErrorType.timeout: - return '当前网络较慢,请稍后重新加载。'; - case WebResourceErrorType.hostLookup: - case WebResourceErrorType.connect: - case WebResourceErrorType.io: - return '没有成功连接到服务器,请检查网络后重试。'; - case WebResourceErrorType.failedSslHandshake: - return '当前站点证书校验失败,请稍后再试。'; - default: - final String description = error.description.trim(); - return description.isEmpty ? '请稍后重新加载页面。' : description; - } - } - - bool _shouldTreatHttpErrorAsMainFrame(HttpResponseError error) { - final Uri? requestUri = error.request?.uri; - if (requestUri == null) { - return true; - } - - if (!_isNetworkUrl(requestUri.toString())) { - return false; - } - - final Uri? currentUri = Uri.tryParse(_currentUrl); - if (_isSameDocumentRequest(requestUri, currentUri)) { - return true; - } - - if (!_hasBootstrapped && _isSameDocumentRequest(requestUri, _initialUri)) { - return true; - } - - return false; - } - - bool _isSameDocumentRequest(Uri left, Uri? right) { - if (right == null) { - return false; - } - - return _normalizeComparableUri(left) == _normalizeComparableUri(right); - } - - String _normalizeComparableUri(Uri uri) { - final String scheme = uri.scheme.toLowerCase(); - final String host = uri.host.toLowerCase(); - final int port = uri.hasPort ? uri.port : _defaultPortForScheme(scheme); - final String path = _normalizeComparablePath(uri.path); - final String query = uri.query; - return '$scheme://$host:$port$path?$query'; - } - - int _defaultPortForScheme(String scheme) { - return switch (scheme) { - 'https' => 443, - 'http' => 80, - _ => -1, - }; - } - - String _normalizeComparablePath(String path) { - if (path.isEmpty) { - return '/'; - } - if (path.length > 1 && path.endsWith('/')) { - return path.substring(0, path.length - 1); - } - return path; - } - - Future _injectAppShellBridge(String url) async { - try { - await _controller.runJavaScript(_buildAppShellBridgeScript()); - debugPrint('已为 $url 注入 AppShell 桥接'); - } catch (error, stackTrace) { - debugPrint('注入 AppShell 桥接失败:$error\n$stackTrace'); - } - } - - Future _handleBridgeMessage(String rawMessage) async { - String requestId = ''; - - try { - final dynamic decoded = jsonDecode(rawMessage); - if (decoded is! Map) { - return; - } - - final Map message = Map.from(decoded); - requestId = (message['requestId'] ?? '').toString(); - final String action = (message['action'] ?? '').toString(); - final Map payload = message['payload'] is Map - ? Map.from(message['payload'] as Map) - : {}; - - if (requestId.isEmpty || action.isEmpty) { - return; - } - - debugPrint('AppShell 桥接请求:action=$action payload=$payload'); - - late final Object? data; - switch (action) { - case 'pickImage': - data = await _pickImagesFromBridge( - source: ImageSource.gallery, - payload: payload, - ); - case 'captureImage': - data = await _pickImagesFromBridge( - source: ImageSource.camera, - payload: payload, - ); - case 'pickFile': - data = await _pickFilesFromBridge(payload); - case 'openExternal': - data = await _openExternalFromBridge(payload); - case 'requestPermissions': - data = await _requestPermissionsFromBridge(payload); - case 'reloadPage': - await _reloadPage(); - data = true; - case 'goBack': - data = await _goBackFromBridge(); - case 'closeApp': - await _sendBridgeResponse(requestId: requestId, success: true); - await SystemNavigator.pop(); - return; - default: - throw UnsupportedError('Unsupported AppShell action: $action'); - } - - await _sendBridgeResponse( - requestId: requestId, - success: true, - data: data, - ); - } catch (error, stackTrace) { - debugPrint('处理 AppShell 桥接请求失败:$error\n$stackTrace'); - if (requestId.isNotEmpty) { - await _sendBridgeResponse( - requestId: requestId, - success: false, - error: error.toString(), - ); - } - } - } - - Future _pickImagesFromBridge({ - required ImageSource source, - required Map payload, - }) async { - final bool multiple = - source == ImageSource.gallery && _boolValue(payload['multiple']); - final String responseType = (payload['responseType'] ?? 'dataUrl') - .toString(); - - List files = []; - if (source == ImageSource.camera) { - final XFile? file = await _pickCameraImage(showPermissionAlert: true); - if (file != null) { - files = [file]; - } - } else if (multiple) { - files = await _imagePicker.pickMultiImage( - imageQuality: _pickedImageQuality, - maxWidth: _pickedImageMaxWidth, - maxHeight: _pickedImageMaxHeight, - ); - } else { - final XFile? file = await _imagePicker.pickImage( - source: ImageSource.gallery, - imageQuality: _pickedImageQuality, - maxWidth: _pickedImageMaxWidth, - maxHeight: _pickedImageMaxHeight, - ); - if (file != null) { - files = [file]; - } - } - - final List> serialized = await _serializeXFiles( - files, - responseType: responseType, - ); - return multiple ? serialized : serialized.firstOrNull; - } - - Future _pickFilesFromBridge(Map payload) async { - final String responseType = (payload['responseType'] ?? 'uri').toString(); - final bool includeBinary = - responseType == 'dataUrl' || responseType == 'base64'; - - final FilePickerResult? result = await FilePicker.platform.pickFiles( - allowMultiple: _boolValue(payload['multiple']), - withData: includeBinary, - type: FileType.any, - ); - - if (result == null) { - return _boolValue(payload['multiple']) ? >[] : null; - } - - final List> serialized = await _serializePlatformFiles( - result.files, - responseType: responseType, - ); - return _boolValue(payload['multiple']) - ? serialized - : serialized.firstOrNull; - } - - Future _openExternalFromBridge(Map payload) async { - final String url = (payload['url'] ?? '').toString(); - final Uri? uri = Uri.tryParse(url); - if (uri == null) { - return false; - } - if (_isNetworkUrl(uri.toString())) { - return false; - } - return _openExternalUri(uri); - } - - Future> _requestPermissionsFromBridge( - Map payload, - ) async { - final List types = - (payload['types'] as List? ?? const []) - .map((type) => type.toString()) - .toList(); - - final Map permissions = { - for (final String type in types) - if (_permissionForType(type) case final Permission permission) - type: permission, - }; - - if (permissions.isEmpty) { - return {}; - } - - final Map statuses = await permissions.values - .toSet() - .toList() - .request(); - - return { - for (final MapEntry entry in permissions.entries) - entry.key: (statuses[entry.value] ?? PermissionStatus.denied).name, - }; - } - - Future _goBackFromBridge() async { - if (await _controller.canGoBack()) { - await _controller.goBack(); - return true; - } - return false; - } - - Future _sendBridgeResponse({ - required String requestId, - required bool success, - Object? data, - String? error, - }) async { - final Map response = { - 'requestId': requestId, - 'success': success, - 'data': data, - 'error': error, - }; - - try { - await _controller.runJavaScript( - 'window.__appShellReceiveResponse(${jsonEncode(response)});', - ); - } catch (bridgeError, stackTrace) { - debugPrint( - '发送 AppShell 桥接响应失败:$bridgeError\n$stackTrace', - ); - } - } - - Future> _handleFileSelector(FileSelectorParams params) async { - debugPrint( - 'WebView 文件选择: ' - 'accept=${params.acceptTypes}, ' - 'capture=${params.isCaptureEnabled}, ' - 'mode=${params.mode.name}', - ); - - if (params.mode == FileSelectorMode.save) { - return []; - } - - try { - final bool acceptsImages = _acceptsImages(params.acceptTypes); - final bool imagesOnly = _acceptsOnlyImages(params.acceptTypes); - - if (params.isCaptureEnabled && acceptsImages) { - final XFile? capturedImage = await _pickCameraImage(); - return _xFilesToUriStrings( - capturedImage == null ? const [] : [capturedImage], - ); - } - - if (imagesOnly) { - if (params.mode == FileSelectorMode.openMultiple) { - final List images = await _imagePicker.pickMultiImage( - imageQuality: _pickedImageQuality, - maxWidth: _pickedImageMaxWidth, - maxHeight: _pickedImageMaxHeight, - ); - return _xFilesToUriStrings(images); - } - - final XFile? image = await _imagePicker.pickImage( - source: ImageSource.gallery, - imageQuality: _pickedImageQuality, - maxWidth: _pickedImageMaxWidth, - maxHeight: _pickedImageMaxHeight, - ); - return _xFilesToUriStrings( - image == null ? const [] : [image], - ); - } - - final FilePickerResult? result = await FilePicker.platform.pickFiles( - allowMultiple: params.mode == FileSelectorMode.openMultiple, - type: FileType.any, - ); - - if (result == null) { - return []; - } - - return result.files - .map((file) => file.path) - .whereType() - .map((path) => Uri.file(path).toString()) - .toList(); - } catch (error, stackTrace) { - debugPrint('处理文件选择失败:$error\n$stackTrace'); - return []; - } - } - - Future _handlePlatformPermissionRequest( - PlatformWebViewPermissionRequest request, - ) async { - debugPrint( - 'WebView 权限请求:' - '${request.types.map((type) => type.name).join(', ')}', - ); - - final List permissions = [ - if (request.types.contains(WebViewPermissionResourceType.camera)) - Permission.camera, - if (request.types.contains(WebViewPermissionResourceType.microphone)) - Permission.microphone, - ]; - - if (permissions.isEmpty) { - await request.deny(); - return; - } - - final Map statuses = await permissions - .request(); - final bool allGranted = statuses.values.every((status) => status.isGranted); - - if (allGranted) { - await request.grant(); - return; - } - - await request.deny(); - } - - Future _handleGeolocationPermissionRequest( - GeolocationPermissionsRequestParams request, - ) async { - debugPrint('WebView 地理位置权限请求:${request.origin}'); - final PermissionStatus status = await Permission.location.request(); - return GeolocationPermissionsResponse( - allow: status.isGranted, - retain: status.isGranted, - ); - } - - Future _pickCameraImage({bool showPermissionAlert = false}) async { - final PermissionStatus cameraStatus = await Permission.camera.request(); - if (!cameraStatus.isGranted) { - if (showPermissionAlert) { - await _showWebAlert('请先在系统设置中允许相机权限'); - } - return null; - } - - try { - return await _imagePicker.pickImage( - source: ImageSource.camera, - preferredCameraDevice: CameraDevice.rear, - imageQuality: _pickedImageQuality, - maxWidth: _pickedImageMaxWidth, - maxHeight: _pickedImageMaxHeight, - ); - } catch (error, stackTrace) { - debugPrint('调用相机拍照失败:$error\n$stackTrace'); - if (showPermissionAlert) { - await _showWebAlert('无法打开系统相机,请稍后重试'); - } - return null; - } - } - - Future>> _serializeXFiles( - List files, { - required String responseType, - }) async { - final bool includeBase64 = - responseType == 'base64' || responseType == 'dataUrl'; - final bool includeDataUrl = responseType == 'dataUrl'; - - final List> serialized = >[]; - for (final XFile file in files) { - String? base64Value; - final String mimeType = _guessMimeType(file.name); - - if (includeBase64 || includeDataUrl) { - base64Value = base64Encode(await file.readAsBytes()); - } - - serialized.add({ - 'name': file.name, - 'uri': Uri.file(file.path).toString(), - 'mimeType': mimeType, - 'size': await file.length(), - if (responseType == 'base64') 'base64': base64Value, - if (includeDataUrl && base64Value != null) - 'dataUrl': 'data:$mimeType;base64,$base64Value', - }); - } - return serialized; - } - - Future>> _serializePlatformFiles( - List files, { - required String responseType, - }) async { - final bool includeBase64 = - responseType == 'base64' || responseType == 'dataUrl'; - final bool includeDataUrl = responseType == 'dataUrl'; - - final List> serialized = >[]; - for (final PlatformFile file in files) { - final String mimeType = _guessMimeType(file.name); - String? base64Value; - - if (includeBase64 || includeDataUrl) { - final List? bytes = - file.bytes ?? - (file.path == null ? null : await XFile(file.path!).readAsBytes()); - if (bytes != null) { - base64Value = base64Encode(bytes); - } - } - - serialized.add({ - 'name': file.name, - 'uri': file.path == null ? null : Uri.file(file.path!).toString(), - 'mimeType': mimeType, - 'size': file.size, - if (responseType == 'base64') 'base64': base64Value, - if (includeDataUrl && base64Value != null) - 'dataUrl': 'data:$mimeType;base64,$base64Value', - }); - } - return serialized; - } - - Future _openExternalUri(Uri uri) async { - try { - return await launchUrl(uri, mode: LaunchMode.externalApplication); - } catch (error, stackTrace) { - debugPrint('外部打开 URI 失败:$error\n$stackTrace'); - return false; - } - } - - Permission? _permissionForType(String type) { - return switch (type.toLowerCase()) { - 'camera' => Permission.camera, - 'microphone' || 'audio' => Permission.microphone, - 'location' => Permission.location, - 'photos' || 'images' => Permission.photos, - 'videos' => Permission.videos, - 'storage' => Permission.storage, - _ => null, - }; - } - - bool _boolValue(Object? value, {bool defaultValue = false}) { - return switch (value) { - bool boolValue => boolValue, - String stringValue => stringValue.toLowerCase() == 'true', - int intValue => intValue != 0, - _ => defaultValue, - }; - } - - Future _showWebAlert(String message) async { - try { - await _controller.runJavaScript('window.alert(${jsonEncode(message)});'); - } catch (error, stackTrace) { - debugPrint('展示网页弹窗失败:$error\n$stackTrace'); - } - } - - List _xFilesToUriStrings(List files) { - return files.map((file) => Uri.file(file.path).toString()).toList(); - } - - bool _acceptsImages(List acceptTypes) { - return acceptTypes - .map((type) => type.trim()) - .where((type) => type.isNotEmpty) - .any(_isImageAcceptType); - } - - bool _acceptsOnlyImages(List acceptTypes) { - final List normalizedTypes = acceptTypes - .map((type) => type.trim()) - .where((type) => type.isNotEmpty) - .toList(); - if (normalizedTypes.isEmpty) { - return false; - } - return normalizedTypes.every(_isImageAcceptType); - } - - bool _isImageAcceptType(String acceptType) { - final String value = acceptType.toLowerCase(); - return value.startsWith('image/') || - const { - '.png', - '.jpg', - '.jpeg', - '.webp', - '.gif', - '.bmp', - '.heic', - '.heif', - }.contains(value); - } - - String _guessMimeType(String fileName) { - final String lower = fileName.toLowerCase(); - if (lower.endsWith('.png')) { - return 'image/png'; - } - if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) { - return 'image/jpeg'; - } - if (lower.endsWith('.webp')) { - return 'image/webp'; - } - if (lower.endsWith('.gif')) { - return 'image/gif'; - } - if (lower.endsWith('.bmp')) { - return 'image/bmp'; - } - if (lower.endsWith('.heic')) { - return 'image/heic'; - } - if (lower.endsWith('.heif')) { - return 'image/heif'; - } - if (lower.endsWith('.pdf')) { - return 'application/pdf'; - } - if (lower.endsWith('.txt')) { - return 'text/plain'; - } - if (lower.endsWith('.apk')) { - return 'application/vnd.android.package-archive'; - } - return 'application/octet-stream'; - } - - String _buildAppShellBridgeScript() { - return ''' -(() => { - const channel = window.$_appShellBridgeChannel; - if (!channel || typeof channel.postMessage !== 'function') { - return; - } - - if (window.AppShell && window.AppShell.__nativeShellVersion === '$_appShellBridgeVersion') { - return; - } - - const pending = new Map(); - - window.__appShellReceiveResponse = function(response) { - if (!response || !response.requestId) { - return; - } - - const task = pending.get(response.requestId); - if (!task) { - return; - } - - pending.delete(response.requestId); - - if (response.success) { - task.resolve(response.data ?? null); - return; - } - - task.reject(new Error(response.error || 'Native request failed')); - }; - - const send = (action, payload = {}) => new Promise((resolve, reject) => { - const requestId = - Date.now().toString(36) + Math.random().toString(36).slice(2); - pending.set(requestId, { resolve, reject }); - channel.postMessage(JSON.stringify({ requestId, action, payload })); - }); - - window.AppShell = { - __nativeShellVersion: '$_appShellBridgeVersion', - isNativeShell: true, - version: '$_appShellBridgeVersion', - pickImage: (options = {}) => send('pickImage', options), - captureImage: (options = {}) => send('captureImage', options), - pickFile: (options = {}) => send('pickFile', options), - openExternal: (url) => send('openExternal', { url }), - requestPermissions: (types = []) => send('requestPermissions', { types }), - reloadPage: () => send('reloadPage'), - goBack: () => send('goBack'), - closeApp: () => send('closeApp'), - }; - - window.dispatchEvent(new CustomEvent('app-shell-ready', { - detail: { - version: '$_appShellBridgeVersion', - isNativeShell: true, - }, - })); - - if (!window.__appShellLegacyCameraCompatInstalled) { - window.__appShellLegacyCameraCompatInstalled = true; - - const installLegacyCameraCompat = () => { - if (typeof window.openCamera !== 'function') { - return false; - } - if (window.openCamera.__appShellCompatWrapped) { - return true; - } - - const originalOpenCamera = window.openCamera.bind(window); - const originalCapturePhoto = - typeof window.capturePhoto === 'function' - ? window.capturePhoto.bind(window) - : null; - - const deliverCaptureResult = (args, result) => { - const detail = { - args: Array.from(args), - result, - }; - - if (typeof window.handleNativeCapture === 'function') { - window.handleNativeCapture(detail); - return true; - } - - const targetId = - detail.args[0] ?? - window.currentUploadId ?? - null; - - if ( - targetId != null && - result && - result.dataUrl && - typeof window.addPreview === 'function' - ) { - window.currentUploadId = targetId; - window.addPreview(targetId, result.dataUrl); - return true; - } - - window.dispatchEvent( - new CustomEvent('app-shell-image-captured', { - detail, - }), - ); - return false; - }; - - const closeLegacyCameraModal = () => { - if (typeof window.closeCamera === 'function') { - try { - window.closeCamera(); - } catch (_) {} - } - }; - - const wrappedOpenCamera = async function(...args) { - try { - const result = await window.AppShell.captureImage({ - responseType: 'dataUrl', - }); - const delivered = deliverCaptureResult(args, result); - if (!delivered) { - return result; - } - closeLegacyCameraModal(); - return result; - } catch (error) { - console.warn( - '[AppShell] legacy openCamera fallback to page implementation', - error, - ); - return originalOpenCamera(...args); - } - }; - - wrappedOpenCamera.__appShellCompatWrapped = true; - window.openCamera = wrappedOpenCamera; - - if (originalCapturePhoto) { - const wrappedCapturePhoto = function(...args) { - if ( - window.currentUploadId != null && - window.__lastAppShellCaptureResult && - window.__lastAppShellCaptureResult.dataUrl && - typeof window.addPreview === 'function' - ) { - window.addPreview( - window.currentUploadId, - window.__lastAppShellCaptureResult.dataUrl, - ); - closeLegacyCameraModal(); - return; - } - return originalCapturePhoto(...args); - }; - wrappedCapturePhoto.__appShellCompatWrapped = true; - window.capturePhoto = wrappedCapturePhoto; - } - - return true; - }; - - const originalCaptureImage = window.AppShell.captureImage; - window.AppShell.captureImage = async function(options = {}) { - const result = await originalCaptureImage(options); - window.__lastAppShellCaptureResult = result; - return result; - }; - - installLegacyCameraCompat(); - - let attempts = 0; - const compatTimer = setInterval(() => { - attempts += 1; - const installed = installLegacyCameraCompat(); - if (installed || attempts >= 20) { - clearInterval(compatTimer); - } - }, 500); - } -})(); -'''; - } - - Future _startLoadSequence({ - required bool rebuildWebView, - required bool resetRetryCount, - }) async { - _cancelStartupWatchdog(); - if (!mounted) { - return; - } - if (rebuildWebView) { - _recreateWebView(); - } - - final int generation = _webViewGeneration; - final Future controllerSetupFuture = _controllerSetupFuture; - - _hasStartedRemoteMainFrame = false; - setState(() { - _isLoadingPage = true; - _hasMainFrameError = false; - _hasMeasuredProgress = false; - _progress = 0; - if (resetRetryCount) { - _startupRetryCount = 0; - } - _errorTitle = '页面加载失败'; - _errorMessage = '请检查网络后重试。'; - _currentUrl = _initialUrl; - }); - - await controllerSetupFuture; - if (!mounted || !_isActiveWebViewGeneration(generation)) { - return; - } - - await _waitForWebViewMount(generation); - - debugPrint( - 'WebShell 开始在 WebView #$generation 加载真实地址 ' - '(${_activeRenderMode.logName}): $_initialUrl', - ); - try { - _armStartupWatchdog(); - await _controller.loadRequest(_initialUri); - } catch (error, stackTrace) { - debugPrint('初始地址加载失败:$error\n$stackTrace'); - _setMainFrameError( - title: '地址无法加载', - message: '无法打开 $_initialUrl,请检查地址格式或网络后重试。', - ); - } - } - - Future _waitForWebViewMount(int generation) async { - final bool isAndroidDebug = - kDebugMode && defaultTargetPlatform == TargetPlatform.android; - final bool usesHybridComposition = - defaultTargetPlatform == TargetPlatform.android && - _activeRenderMode.usesHybridComposition; - final int framesToWait = isAndroidDebug - ? (usesHybridComposition ? 6 : 4) - : (usesHybridComposition ? 2 : 1); - final Duration settleDelay = isAndroidDebug - ? (usesHybridComposition - ? const Duration(milliseconds: 250) - : const Duration(milliseconds: 140)) - : (usesHybridComposition - ? const Duration(milliseconds: 80) - : const Duration(milliseconds: 40)); - - for (int index = 0; index < framesToWait; index += 1) { - await SchedulerBinding.instance.endOfFrame; - if (!mounted || !_isActiveWebViewGeneration(generation)) { - return; - } - await Future.delayed(settleDelay); - if (!mounted || !_isActiveWebViewGeneration(generation)) { - return; - } - } - } - - Future _reloadPage() async { - if (!mounted) { - return; - } - await _ensureCompatibilityPlanApplied(); - if (!mounted) { - return; - } - if (!_hasBootstrapped) { - await _recoverFromBrokenStartupState(deepReset: true); - await _startLoadSequence(rebuildWebView: true, resetRetryCount: true); - return; - } - setState(() { - _hasStartedRemoteMainFrame = false; - _isLoadingPage = true; - _hasMainFrameError = false; - _hasMeasuredProgress = false; - _progress = 0; - _startupRetryCount = 0; - _errorTitle = '页面加载失败'; - _errorMessage = '请检查网络后重试。'; - }); - try { - _armStartupWatchdog(); - await _controller.reload(); - } catch (error, stackTrace) { - debugPrint('重新加载当前页面失败:$error\n$stackTrace'); - await _startLoadSequence(rebuildWebView: true, resetRetryCount: true); - } - } - - @override - Widget build(BuildContext context) { - final bool showProgressBar = - _isLoadingPage && (!_hasMeasuredProgress || _progress < 100); - final bool showLaunchOverlay = !_hasBootstrapped && !_hasMainFrameError; - - return PopScope( - canPop: false, - onPopInvokedWithResult: (didPop, result) { - if (!didPop) { - unawaited(_handleBackPressed()); - } - }, - child: Scaffold( - backgroundColor: _shellBackgroundColor, - body: DecoratedBox( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [_shellBackgroundColor, Color(0xFFF4FBF7)], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: SafeArea( - top: false, - bottom: false, - child: Stack( - children: [ - Positioned.fill( - child: ColoredBox( - color: _shellBackgroundColor, - child: _webViewWidget, - ), - ), - if (showProgressBar) - Positioned( - top: 0, - left: 0, - right: 0, - child: _TopProgressBar( - progress: _progress, - hasMeasuredProgress: _hasMeasuredProgress, - ), - ), - if (showLaunchOverlay) - Positioned.fill( - child: _LaunchOverlay( - progress: _progress, - hasMeasuredProgress: _hasMeasuredProgress, - ), - ), - if (_hasMainFrameError) - Positioned.fill( - child: _ErrorOverlay( - title: _errorTitle, - message: _errorMessage, - currentUrl: _currentUrl, - onRetry: _reloadPage, - ), - ), - ], - ), - ), - ), - ), - ); - } -} - -class UnsupportedPlatformPage extends StatelessWidget { - const UnsupportedPlatformPage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: _shellBackgroundColor, - body: Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 28), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 84, - height: 84, - decoration: BoxDecoration( - color: _shellAccentColor.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(24), - ), - alignment: Alignment.center, - child: const Icon( - Icons.language_rounded, - size: 42, - color: _shellAccentColor, - ), - ), - const SizedBox(height: 20), - const Text( - '当前平台不支持内嵌 WebView', - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.w700, - color: _shellTextColor, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 12), - Text( - '当前项目仅支持 Android 平板运行。\n$_initialUrl', - textAlign: TextAlign.center, - style: const TextStyle( - height: 1.6, - color: _shellMutedTextColor, - ), - ), - ], - ), - ), - ), - ); - } -} - -class _TopProgressBar extends StatelessWidget { - const _TopProgressBar({ - required this.progress, - required this.hasMeasuredProgress, - }); - - final int progress; - final bool hasMeasuredProgress; - - @override - Widget build(BuildContext context) { - return LinearProgressIndicator( - minHeight: 3, - value: hasMeasuredProgress ? progress / 100 : null, - backgroundColor: Colors.white.withValues(alpha: 0.8), - valueColor: const AlwaysStoppedAnimation(_shellAccentColor), - ); - } -} - -class _LaunchOverlay extends StatelessWidget { - const _LaunchOverlay({ - required this.progress, - required this.hasMeasuredProgress, - }); - - final int progress; - final bool hasMeasuredProgress; - - @override - Widget build(BuildContext context) { - return ColoredBox( - color: _shellBackgroundColor, - child: Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 88, - height: 88, - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [Color(0xFF66E59A), _shellAccentColor], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(28), - boxShadow: const [ - BoxShadow( - color: Color(0x263ED37B), - blurRadius: 24, - offset: Offset(0, 14), - ), - ], - ), - alignment: Alignment.center, - child: const Icon( - Icons.public_rounded, - size: 40, - color: Colors.white, - ), - ), - const SizedBox(height: 24), - const Text( - '页面加载中', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w800, - color: _shellTextColor, - ), - ), - const SizedBox(height: 10), - const Text( - '正在为你启动 H5 页面', - style: TextStyle(color: _shellMutedTextColor, fontSize: 14), - ), - const SizedBox(height: 24), - ClipRRect( - borderRadius: BorderRadius.circular(999), - child: SizedBox( - width: 220, - child: LinearProgressIndicator( - minHeight: 6, - value: hasMeasuredProgress ? progress / 100 : null, - backgroundColor: const Color(0xFFE7F3EB), - valueColor: const AlwaysStoppedAnimation( - _shellAccentColor, - ), - ), - ), - ), - const SizedBox(height: 10), - Text( - hasMeasuredProgress ? '$progress%' : '连接中...', - style: const TextStyle( - color: Color(0xFF94A3B8), - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - ); - } -} - -class _ErrorOverlay extends StatelessWidget { - const _ErrorOverlay({ - required this.title, - required this.message, - required this.currentUrl, - required this.onRetry, - }); - - final String title; - final String message; - final String currentUrl; - final Future Function() onRetry; - - @override - Widget build(BuildContext context) { - return ColoredBox( - color: _shellBackgroundColor, - child: Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 28), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 92, - height: 92, - decoration: BoxDecoration( - color: const Color(0xFFFEF2F2), - borderRadius: BorderRadius.circular(28), - ), - alignment: Alignment.center, - child: const Icon( - Icons.wifi_off_rounded, - size: 44, - color: Color(0xFFEF4444), - ), - ), - const SizedBox(height: 22), - Text( - title, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.w800, - color: _shellTextColor, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 10), - Text( - message, - style: const TextStyle( - fontSize: 14, - height: 1.6, - color: _shellMutedTextColor, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 12), - Text( - currentUrl, - style: const TextStyle(fontSize: 12, color: Color(0xFF94A3B8)), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - FilledButton.icon( - onPressed: onRetry, - style: FilledButton.styleFrom( - backgroundColor: _shellAccentColor, - foregroundColor: Colors.black, - ), - icon: const Icon(Icons.refresh_rounded), - label: const Text('重新加载'), - ), - ], - ), - ), - ), - ); - } -} diff --git a/packages/web_android_shell/pubspec.lock b/packages/web_android_shell/pubspec.lock deleted file mode 100644 index 1997430..0000000 --- a/packages/web_android_shell/pubspec.lock +++ /dev/null @@ -1,578 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.13.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.4.1" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.2" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.19.1" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.3.5+2" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.8" - dbus: - dependency: transitive - description: - name: dbus - sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.7.12" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.0" - file_picker: - dependency: "direct main" - description: - name: file_picker - sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343" - url: "https://pub.flutter-io.cn" - source: hosted - version: "10.3.10" - file_selector_linux: - dependency: transitive - description: - name: file_selector_linux - sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.9.4" - file_selector_macos: - dependency: transitive - description: - name: file_selector_macos - sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.9.5" - file_selector_platform_interface: - dependency: transitive - description: - name: file_selector_platform_interface - sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.7.0" - file_selector_windows: - dependency: transitive - description: - name: file_selector_windows - sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.9.3+5" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.0.0" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.33" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - http: - dependency: transitive - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.6.0" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.1.2" - image_picker: - dependency: "direct main" - description: - name: image_picker - sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.2.1" - image_picker_android: - dependency: transitive - description: - name: image_picker_android - sha256: eda9b91b7e266d9041084a42d605a74937d996b87083395c5e47835916a86156 - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.8.13+14" - image_picker_for_web: - dependency: transitive - description: - name: image_picker_for_web - sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.1.1" - image_picker_ios: - dependency: transitive - description: - name: image_picker_ios - sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.8.13+6" - image_picker_linux: - dependency: transitive - description: - name: image_picker_linux - sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.2.2" - image_picker_macos: - dependency: transitive - description: - name: image_picker_macos - sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.2.2+1" - image_picker_platform_interface: - dependency: transitive - description: - name: image_picker_platform_interface - sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.11.1" - image_picker_windows: - dependency: transitive - description: - name: image_picker_windows - sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.2.2" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.flutter-io.cn" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.1.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.12.18" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.13.0" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.17.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.9.1" - permission_handler: - dependency: "direct main" - description: - name: permission_handler - sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 - url: "https://pub.flutter-io.cn" - source: hosted - version: "12.0.1" - permission_handler_android: - dependency: transitive - description: - name: permission_handler_android - sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" - url: "https://pub.flutter-io.cn" - source: hosted - version: "13.0.1" - permission_handler_apple: - dependency: transitive - description: - name: permission_handler_apple - sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 - url: "https://pub.flutter-io.cn" - source: hosted - version: "9.4.7" - permission_handler_html: - dependency: transitive - description: - name: permission_handler_html - sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.1.3+5" - permission_handler_platform_interface: - dependency: transitive - description: - name: permission_handler_platform_interface - sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.3.0" - permission_handler_windows: - dependency: transitive - description: - name: permission_handler_windows - sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.2.1" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" - url: "https://pub.flutter-io.cn" - source: hosted - version: "7.0.2" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.8" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.10.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.7.9" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.4.0" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.3.2" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.3.28" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.4.1" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.2.2" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.2.5" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.3.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.4.2" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.1.5" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.flutter-io.cn" - source: hosted - version: "15.0.2" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.1" - webview_flutter: - dependency: "direct main" - description: - name: webview_flutter - sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9 - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.13.1" - webview_flutter_android: - dependency: "direct main" - description: - name: webview_flutter_android - sha256: "2a03df01df2fd30b075d1e7f24c28aee593f2e5d5ac4c3c4283c5eda63717b24" - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.10.13" - webview_flutter_platform_interface: - dependency: transitive - description: - name: webview_flutter_platform_interface - sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.14.0" - webview_flutter_wkwebview: - dependency: transitive - description: - name: webview_flutter_wkwebview - sha256: "2df8fd9ada04d699b9db8e79aa783a16e5d89b69e5b74009b87e16b59912cf98" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.24.0" - win32: - dependency: transitive - description: - name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.flutter-io.cn" - source: hosted - version: "5.15.0" - xml: - dependency: transitive - description: - name: xml - sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.6.1" -sdks: - dart: ">=3.11.0 <4.0.0" - flutter: ">=3.38.0" diff --git a/packages/web_android_shell/pubspec.yaml b/packages/web_android_shell/pubspec.yaml deleted file mode 100644 index 2780208..0000000 --- a/packages/web_android_shell/pubspec.yaml +++ /dev/null @@ -1,35 +0,0 @@ -name: web_android_shell -description: "H5壳子项目." -# 阻止误发布到 pub.dev。 -publish_to: 'none' - -# Android 应用版本号:`build-name` 对应 `versionName`,`build-number` 对应 `versionCode`。 -version: 1.0.0+1 - -environment: - sdk: ^3.11.0 - -# 应用运行依赖。 -dependencies: - flutter: - sdk: flutter - - # Material 图标已覆盖主要场景;此依赖保留以兼容历史代码。 - cupertino_icons: ^1.0.8 - webview_flutter: ^4.13.1 - webview_flutter_android: ^4.10.13 - image_picker: ^1.2.1 - file_picker: ^10.3.10 - permission_handler: ^12.0.1 - url_launcher: ^6.3.2 - -dev_dependencies: - flutter_test: - sdk: flutter - - # 推荐的 Flutter lint 规则集合。 - flutter_lints: ^6.0.0 - -flutter: - # 启用 Material 图标字体。 - uses-material-design: true diff --git a/packages/web_android_shell/test/widget_test.dart b/packages/web_android_shell/test/widget_test.dart deleted file mode 100644 index 8e61bf6..0000000 --- a/packages/web_android_shell/test/widget_test.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:web_android_shell/main.dart'; - -void main() { - test('WebShellApp can be created', () { - expect(const WebShellApp(), isA()); - }); -} diff --git a/packages/web_android_shell/tool/flutter_run_fresh.ps1 b/packages/web_android_shell/tool/flutter_run_fresh.ps1 deleted file mode 100644 index e4591e7..0000000 --- a/packages/web_android_shell/tool/flutter_run_fresh.ps1 +++ /dev/null @@ -1,187 +0,0 @@ -[CmdletBinding()] -param( - [ValidateSet('Auto', 'Disable', 'Enable', 'Keep')] - [string]$WebViewMultiprocess = 'Auto', - - [Parameter(ValueFromRemainingArguments = $true)] - [string[]]$FlutterArgs -) - -$ErrorActionPreference = 'Stop' - -$projectRoot = Split-Path -Parent $PSScriptRoot -$localPropertiesPath = Join-Path $projectRoot 'android\local.properties' -$packageName = 'com.yuanxuan.webshell.web_android_shell' - -function Get-LocalPropertyValue { - param( - [string]$Path, - [string]$Name - ) - - if (-not (Test-Path $Path)) { - return $null - } - - $escapedName = [regex]::Escape($Name) - foreach ($line in Get-Content $Path) { - if ($line -match "^$escapedName=(.+)$") { - return $matches[1].Trim() -replace '\\\\', '\' - } - } - - return $null -} - -function Get-DeviceIdFromArgs { - param([string[]]$ArgsToScan) - - for ($index = 0; $index -lt $ArgsToScan.Length; $index += 1) { - $arg = $ArgsToScan[$index] - if ($arg -eq '-d' -or $arg -eq '--device-id') { - if ($index + 1 -lt $ArgsToScan.Length) { - return $ArgsToScan[$index + 1] - } - } - - if ($arg.StartsWith('--device-id=')) { - return $arg.Substring('--device-id='.Length) - } - } - - return $null -} - -function Get-DeviceModel { - param( - [string]$AdbPath, - [string[]]$AdbArgsPrefix - ) - - return ((& $AdbPath @AdbArgsPrefix shell getprop ro.product.model) | Out-String).Trim() -} - -function Get-WebViewUpdateState { - param( - [string]$AdbPath, - [string[]]$AdbArgsPrefix - ) - - return (& $AdbPath @AdbArgsPrefix shell dumpsys webviewupdate | Out-String) -} - -function Get-WebViewInfo { - param( - [string]$AdbPath, - [string[]]$AdbArgsPrefix - ) - - $state = Get-WebViewUpdateState -AdbPath $AdbPath -AdbArgsPrefix $AdbArgsPrefix - $multiprocessEnabled = $state -match 'Multiprocess enabled:\s+true' - $versionName = $null - - if ($state -match 'Current WebView package \(name, version\): \([^\),]+,\s*([^\)]+)\)') { - $versionName = $matches[1].Trim() - } - - $majorVersion = $null - if ($versionName) { - $majorToken = $versionName.Split('.')[0] - $parsedMajor = 0 - if ([int]::TryParse($majorToken, [ref]$parsedMajor)) { - $majorVersion = $parsedMajor - } - } - - return @{ - MultiprocessEnabled = $multiprocessEnabled - VersionName = $versionName - MajorVersion = $majorVersion - RawState = $state - } -} - -function Set-WebViewMultiprocessState { - param( - [string]$AdbPath, - [string[]]$AdbArgsPrefix, - [bool]$Enabled - ) - - $command = if ($Enabled) { 'enable-multiprocess' } else { 'disable-multiprocess' } - & $AdbPath @AdbArgsPrefix shell cmd webviewupdate $command | Out-Host - Start-Sleep -Seconds 1 -} - -$sdkDir = Get-LocalPropertyValue -Path $localPropertiesPath -Name 'sdk.dir' -$adbPath = $null - -if ($sdkDir) { - $candidate = Join-Path $sdkDir 'platform-tools\adb.exe' - if (Test-Path $candidate) { - $adbPath = $candidate - } -} - -if (-not $adbPath) { - $adbCommand = Get-Command adb.exe -ErrorAction SilentlyContinue - if ($adbCommand) { - $adbPath = $adbCommand.Source - } -} - -if (-not $adbPath) { - throw "未找到 adb.exe,请检查 android/local.properties 中的 sdk.dir。" -} - -$deviceId = Get-DeviceIdFromArgs -ArgsToScan $FlutterArgs -$adbArgsPrefix = @() - -if ($deviceId) { - $adbArgsPrefix = @('-s', $deviceId) -} - -Write-Host "Using adb: $adbPath" -if ($deviceId) { - Write-Host "Target device: $deviceId" -} - -& $adbPath @adbArgsPrefix start-server | Out-Null - -$deviceModel = Get-DeviceModel -AdbPath $adbPath -AdbArgsPrefix $adbArgsPrefix -$webViewInfo = Get-WebViewInfo -AdbPath $adbPath -AdbArgsPrefix $adbArgsPrefix - -Write-Host "Device model: $deviceModel" -if ($webViewInfo.VersionName) { - Write-Host "System WebView: $($webViewInfo.VersionName)" -} -Write-Host "WebView multiprocess enabled: $($webViewInfo.MultiprocessEnabled)" - -$shouldDisableMultiprocess = $false -switch ($WebViewMultiprocess) { - 'Disable' { - $shouldDisableMultiprocess = $true - } - 'Auto' { - $shouldDisableMultiprocess = - $deviceModel -eq 'F136A' -or - ($webViewInfo.MajorVersion -ne $null -and $webViewInfo.MajorVersion -le 101) - } -} - -if ($shouldDisableMultiprocess -and $webViewInfo.MultiprocessEnabled) { - Write-Host "Disabling Android WebView multiprocess for compatibility..." - Set-WebViewMultiprocessState -AdbPath $adbPath -AdbArgsPrefix $adbArgsPrefix -Enabled:$false -} elseif ($WebViewMultiprocess -eq 'Enable' -and -not $webViewInfo.MultiprocessEnabled) { - Write-Host "Re-enabling Android WebView multiprocess..." - Set-WebViewMultiprocessState -AdbPath $adbPath -AdbArgsPrefix $adbArgsPrefix -Enabled:$true -} - -Write-Host "Force-stopping previous app process: $packageName" -& $adbPath @adbArgsPrefix shell am force-stop $packageName | Out-Null - -Start-Sleep -Milliseconds 800 - -Write-Host "Starting flutter run..." -& flutter run @FlutterArgs -exit $LASTEXITCODE