diff --git a/.gitignore b/.gitignore index 25e6ce601..1b4868006 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Gradle / Android 构建产物 .gradle/ +.kotlin/ .gradle-user-home/ .pnpm-store/ build/ diff --git a/AGENTS.md b/AGENTS.md index f4487334d..21f41a66b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,9 @@ # AGENTS.md +> Miuix 独立体验版:`standardPersonal` 使用包名 `com.dsh.client.miuix`、名称 DSHA Miuix、本机签名和 `debuggable=false`,可与原应用并存且数据独立。该构建类型不替换正式版 E7E3 签名规则,也不进入正式 release 目录。构建方式与验证边界见 `docs/ui-miuix.md`。 + +> 2026-10-17 UI 更新:按用户指定接入 `compose-miuix-ui/miuix`,覆盖历史“无 Kotlin / 仅 Material3”界面限制。Kotlin 限于 `ui/miuix` 的 Compose 展示层,业务保持 Java 17、单模块 `:app`。固定 Miuix 0.8.8 / Compose Android 1.10.5 / Kotlin Compose 编译器 2.3.20;保留 Standard API 30、Low API 23。当前迁移范围、定向检查与未验边界见 `docs/ui-miuix.md`;不将源码验证写成正式 APK 或真机交付。 + > 2026-10-15 本轮新增授权:仅同步当前应用源码、构建脚本、必要文档与 Star History 到 GitHub;不上传 APK、离线归档、设备取证、密钥或本机交付资料,不发布 Release 或网站。下方历史“未上传”描述对应各自当时的本地交付状态,本轮源码同步范围见 docs/source-updates/build173.md。 > build173 PiP 补充交付(2026-10-15):C17 标准包修复“小窗显示时停止 → 重启 → 再进入”残留 Task 0.275 缩放;失效空宿主退休,新代次使用新 Task,同代网页实例继续保留。62 项定向 Activity 宿主回归、76 项开机边界恢复关联测试及最终 37 项软件门禁通过;两版 JUnit 各1700项、40 skip,Release Lint/完整资产/历史签名通过。正式同签名 Android13 原应用覆盖后,真实重启恢复、受管更新、PiP 停止及导出停止后的新窗口5/30秒、同代恢复/返回/旋转均有实际记录。1269条数据无增删,个人配置、会话、附件字节保留;历史4条未知记录与1条追加消息缺口继续保留。有限日志有2次 renderer 报告和2次 SAF 系统捕获 NPE,实际导出成功,不作零崩溃结论。标准 APK SHA c11abc4197f747b690897ea2198c990514eedd5d6ca10b9111ce7da90ed02f57 / 288099627 bytes,已替换 release 同名 rc2.1 标准包;C14 原件及封存材料保留,Low172 原字节不动。回执 artifacts/deliveries/build173-standard-pip-fix.json,源码 artifacts/source/build173-pip-fix/,补充报告 docs/releases/v0.2.24-build173-pip-20261015.md;不发布 GitHub、网站或 Release。 @@ -13,7 +17,7 @@ - 本轮新增修复只看仍开放的 issue / PR,已关闭条目不追做,插件收录跳过。QuickChat 已按用户要求撤回,不再恢复其生产入口。标准版优先,Low 不安装、不做设备验收,既有 Low172 交付文件和校验保持原字节;不能把软件双版通过写成设备双版通过。 - 引擎只保留 bxroot / proot。首次升级优先 bxroot;升级后的显式 proot 选择保留。启动阶段明确引擎失败、无插件故障和用户业务提交,且本轮受管后代树与 launcher 都已确认退出,才允许一次自动 proot 重试。慢启动、超时、未知执行结果和已开始的写任务不能借此重放。proroot 只保留历史记录读取,不保留生产执行。 - 彻底重装/完整格式化功能移除,包括生产入口、Intent 触发和擦除后端。外置工作区维护只能断开私有引用,不遍历清理、覆盖或改名其内容;旧恢复日志不能自动重放外置写删。冷测速等合法私有临时清理必须使用受控目标及 NOFOLLOW,不能继承完整格式化能力。 -- 原生外壳继续 Java17 / Material3 和现有导航,只迁移透明状态栏、背景跟色、图标明暗与 Insets。状态栏保持可见,键盘、挖孔和 PiP 的安全区规则保留;双网页内核使用同一个网页实例切换全屏 / PiP / 隐藏。恢复全屏时按系统当前 PiP 状态同步并幂等清除自有缩放与位移,不能依赖旧几何缓存宣称变换已清除。 +- 原生外壳的业务继续 Java 17,导航流程保持;界面允许按本轮 Miuix 迁移说明使用 Kotlin / Compose。透明状态栏、背景跟色、图标明暗与 Insets 继续共用原生处理。状态栏保持可见,键盘、挖孔和 PiP 的安全区规则保留;双网页内核使用同一个网页实例切换全屏 / PiP / 隐藏。恢复全屏时按系统当前 PiP 状态同步并幂等清除自有缩放与位移,不能依赖旧几何缓存宣称变换已清除。 - 内置移动插件固定 3.1.0 / ab3a85bead1c53b723e8627f5b44e3c306d85604;DSH 仍 0.2.0-rc.2,Ubuntu base10 不变。上游已吸收的本地差异退休,安全服务端替换、快捷键搜索、可见区域 CSS 和视口合并仍须实际回归。 - 屏幕确认策略为全部手动 / 按应用自定义 / 全部允许。全部允许覆盖敏感页面,仅免 DSHA 确认;系统能力、实际目标、运行代次、最新帧与节点身份仍校验。持久规则只在本机,停止使本轮许可失效,规则修改或撤销使排队请求失效,不从备份导入授权。 - 不实现整合包/市场及插件收录。任务通知采用标准进度通知,不请求系统 promoted 胶囊;原生终端、插件安装及生命周期脚本继续开放,工具审批不能宣称强沙箱。 @@ -28,8 +32,7 @@ DSHA 重构骨架。本文让你不扫全库就能上手 —— 读它之前先 ## 一句话 APK 用 bxroot/proot 把完整 Ubuntu rootfs 搬进 app 私有目录,在里面跑 Node 24 + -pnpm + `@deepseek-ai/dsh`(**0.2.0-rc.2**)的 Web UI(默认动态端口,手动模式可指定 `3080`)。原生层是纯 Java 17、 -Material3、单 Gradle 模块 `:app`。 +pnpm + `@deepseek-ai/dsh`(**0.2.0-rc.2**)的 Web UI(默认动态端口,手动模式可指定 `3080`)。原生业务层是 Java 17,界面使用 Miuix / Compose 与既有 Material3 控件,单 Gradle 模块 `:app`。 ## 技术约束(围绕这些设计) @@ -55,7 +58,7 @@ Material3、单 Gradle 模块 `:app`。 - **签名规则(最新验收要求)**:`release` 和手机验收统一沿用 E7E3 历史发布证书(完整指纹见 build 131 记录);A3F4 仅保留为历史手机验收规则,不得用于本轮正式验收或放入 `release`。覆盖安装前核对包名、版本码、证书和 `debuggable=false`,不得卸载后用错误证书替代。 - **发布交付目录为 `F:\DSHA_RESTART\release\\`**:只放两版 APK 和对应 `.apk.sha256`,同组历史 APK 放该版本的 `history/`。源码放 `artifacts/source/`,回执放 `artifacts/deliveries/`,其他历史原件移至 `artifacts/release-history/`;迁移记录保留原始 SHA,旧封档正文不改。 -- **Java 17,无 Kotlin**,单模块 `:app`。 +- **业务 Java 17,Kotlin 仅用于 Miuix 界面**,单模块 `:app`。 - `applicationId com.dsh.client`;Java 包 `com.deepseekharness.app`;标准版 `minSdk 30`、兼容版 `minSdk 23`, `compileSdk/targetSdk 37`(SDK 平台包 `android-37.0`)、AGP 9.1.1、Gradle 9.3.1、NDK 26、**arm64-v8a only**。 - 离线 rootfs(`assets/offline-rootfs.bin`)等大型生成归档**不提交**;正式本地/CI 包必须含经过核对的完整 Ubuntu 与 DSH 分包,不能把源码检出缺少归档当作正式精简交付。 diff --git a/BUILD.md b/BUILD.md index f134a8142..ef29d0f39 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,14 +1,16 @@ # DSHA 现行构建说明 +> Miuix 独立体验版:使用 `:app:assembleStandardPersonal` 构建 `com.dsh.client.miuix`(DSHA Miuix),本机签名、非调试、数据独立,可保留原应用。此包不进入正式 release;正式版继续使用 E7E3 签名。完整离线资产仍是打包前提,见 [UI 迁移说明](docs/ui-miuix.md)。 + 先读 [README](README.md)、[AGENTS](AGENTS.md)、[贡献流程](CONTRIBUTING.md) 与[接手指南](docs/接手指南.md)。应用版本以 `app/build.gradle` 为准,DSH 依赖以 `tools/dsh-runtime/package.json`/锁文件为准,完整交付和未验证范围以本轮报告为准。 -本轮只做本地正式产物与软件验证,不操作手机、不生成额外调试/审计 APK、不上传 GitHub 或部署网站。JVM 测试名称中的 Debug 表示测试变体,不授权 assembleDebug 或安装额外 APK。 +本次 UI PR 提交源码、构建配置与必要文档,不上传 APK、离线归档、设备取证或密钥,不发布 Release 或网站。JVM 测试名称中的 Debug 表示测试变体;独立体验版通过 Personal 构建类型生成。 ## 环境与 flavor | 输入 | 现行要求 | |---|---| -| JDK / 原生语言 | Java 17,无 Kotlin | +| JDK / 原生语言 | Java 17;Miuix 展示层使用 Kotlin 2.3.20 / Compose 编译器,JVM 目标仍为 17 | | Gradle / AGP | 仓库 Wrapper 9.3.1 / AGP 9.1.1 | | Android SDK | `platforms;android-37.0`、build-tools 36.0.0;compile/target API 37 | | NDK | 26.3.11579264;终端与 Low proot 按各自受管入口使用 API 23 与真实 ELF 对齐检查 | @@ -20,6 +22,8 @@ 配置本机 `local.properties` 的 `sdk.dir`,并将 JAVA_HOME、ANDROID_SDK_ROOT/ANDROID_HOME、DSHA_PYTHON 指向已有工具。不要复制开发者的绝对工具链目录。版本与固定 APK 文件名由 `app/build.gradle` 的 `versionCode`、`versionName` 和 `dshaApkNameVersion` 定义,不在本页再维护另一组当前数字。 +Miuix UI 的依赖、混合界面边界与不依赖离线归档的定向检查见 [UI 迁移说明](docs/ui-miuix.md)。AGP 9.1.1 使用内置 Kotlin 支持,不重复应用旧 `org.jetbrains.kotlin.android` 插件。 + ## 输入准备与证明 离线 rootfs 和大型生成物不提交;固定输入、生成順序与 CI 传递方式见 CONTRIBUTING。Ubuntu 基础环境身份独立于 DSH 与 UI 版本。`offline-rootfs.bin` / `dsh-runtime.bin` 为 split-runtime-v1,独立应急 pinned 输入由 `tools/recovery-runtime/lock.json` 约束,不能用正式环境作为救援依赖。 diff --git a/README.md b/README.md index 0cb18208f..d3d533627 100644 --- a/README.md +++ b/README.md @@ -269,9 +269,11 @@ agent 通过本机 `127.0.0.1:3090` 桥调用以下能力(token 门控,`/app ## 架构 +原生首页、设置、底部导航与共用管理卡片接入 Miuix;复杂表单与网页界面继续使用原有实现。迁移结构及验证范围见 [Miuix UI 说明](docs/ui-miuix.md)。 + ``` ┌──────────────────────── APK ────────────────────────┐ -│ 原生 Android(Java 17 · Material3 · 单模块 :app) │ +│ 原生 Android(Java 17 · Miuix / Compose · 单模块 :app) │ │ ├ 启动 / 安装修复 / 配置 / 工作区 / 插件 / 终端 │ │ ├ 网页内核:系统 WebView(Standard)/ Gecko(Low) │ │ ├ 前台服务 + 看门狗 + 通知 + 画中画 │ diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 18ab6cb44..015f82422 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -56,7 +56,10 @@ 当前 Gradle 直接运行依赖坐标(不包含测试依赖;完整传递依赖许可审查仍需实际解析结果): -- `androidx.appcompat:appcompat:1.6.1` +- `androidx.appcompat:appcompat:1.7.1` +- `androidx.compose.foundation:foundation:1.10.5` +- `androidx.compose.ui:ui:1.10.5` +- `androidx.graphics:graphics-path:1.0.1` - `androidx.webkit:webkit:1.15.0` - `com.android.tools:desugar_jdk_libs:2.0.4` - `com.google.android.material:material:1.11.0` @@ -69,6 +72,15 @@ - `org.bouncycastle:bcprov-jdk15to18:1.85.2` - `org.mozilla.geckoview:geckoview-arm64-v8a:143.0.20251003115653` - `org.yaml:snakeyaml:2.4` +- `top.yukonga.miuix.kmp:miuix:0.8.8` + +## Miuix 与 Compose 原生界面 + +- Miuix `top.yukonga.miuix.kmp:miuix:0.8.8`:Apache-2.0,源码与许可见 https://github.com/compose-miuix-ui/miuix/tree/v0.8.8 。 +- AndroidX Compose UI / Foundation 1.10.5 与 AppCompat 1.7.1:Apache-2.0,源码见 https://android.googlesource.com/platform/frameworks/support/ 。 +- Compose 的 Android 图形库 `androidx.graphics:graphics-path:1.0.1`:Apache-2.0;两版 APK 均包含 ARM64 `libandroidx.graphics.path.so`,AAR 及成员摘要由 `tools/native-dependencies.lock.json` 与实际 APK 门禁核对。 +- Kotlin Compose 编译器 2.3.20:Apache-2.0,仅在构建时使用,源码见 https://github.com/JetBrains/kotlin 。 +- Android 依赖由 Maven 获取固定发布版本,其 SHA-256 由 `gradle/verification-metadata.xml` 核验。 ## 实际入库 JNI 成员与来源边界 diff --git a/app/build.gradle b/app/build.gradle index 519f87ea9..e1705d110 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,6 @@ plugins { id 'com.android.application' + id 'org.jetbrains.kotlin.plugin.compose' } // 两版共用功能和离线环境;只有 low 携带兼容旧 Android 的 Gecko 内核。 @@ -85,6 +86,15 @@ android { if (!unsignedPackage) signingConfig signingConfigs.publish proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } + // 用户授权的独立安装版;与正式应用分开保存数据,沿用本机密钥便于以后覆盖更新。 + personal { + initWith release + applicationIdSuffix '.miuix' + versionNameSuffix '-miuix' + debuggable false + signingConfig signingConfigs.debug + matchingFallbacks = ['release'] + } } packagingOptions { @@ -112,7 +122,12 @@ android { targetCompatibility JavaVersion.VERSION_17 } + testOptions { + unitTests.includeAndroidResources = true + } + buildFeatures { + compose = true resValues = true aidl = true buildConfig = true @@ -128,6 +143,18 @@ android { } } +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +androidComponents { + beforeVariants(selector().withBuildType('personal')) { variant -> + variant.enableUnitTest = true + } +} + def verifyPublishCertificate = tasks.register("verifyPublishCertificate") { doLast { def signing = android.signingConfigs.publish @@ -332,7 +359,8 @@ def verifyBackupDependencies = tasks.register("verifyBackupDependencies") { } hash.digest().encodeHex().toString() } - ["standardReleaseRuntimeClasspath", "lowReleaseRuntimeClasspath"].each { name -> + ["standardReleaseRuntimeClasspath", "lowReleaseRuntimeClasspath", + "standardPersonalRuntimeClasspath", "lowPersonalRuntimeClasspath"].each { name -> def resolved = configurations.getByName(name).resolvedConfiguration.resolvedArtifacts lock.dependencies.each { expected -> def selected = resolved.findAll { it.moduleVersion.id.group == expected.group && it.name == expected.name } @@ -360,14 +388,26 @@ def prepareUiLanguages = tasks.register("prepareUiLanguages", Exec) { "--output", layout.buildDirectory.dir("generated/uiLanguage").get().asFile.absolutePath) } tasks.named("preBuild").configure { dependsOn(prepareUiLanguages) } +// Kotlin 同样读取 Java 文案生成目录;显式声明生成顺序,支持独立 UI 编译。 +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + dependsOn(prepareUiLanguages) +} +tasks.withType(JavaCompile).configureEach { dependsOn(prepareUiLanguages) } dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' lowImplementation 'org.mozilla.geckoview:geckoview-arm64-v8a:143.0.20251003115653' - implementation 'androidx.appcompat:appcompat:1.6.1' + // 1.7 的 Dialog 提供 Compose 所需的视图生命周期。 + implementation 'androidx.appcompat:appcompat:1.7.1' // 1.15 保留 API 23;1.16 起要求 API 24,不能用于兼容版。 implementation 'androidx.webkit:webkit:1.15.0' implementation 'com.google.android.material:material:1.11.0' + // Miuix 0.8.8 保留 API 23;界面使用 Kotlin,业务与运行时继续使用 Java 17。 + implementation 'top.yukonga.miuix.kmp:miuix:0.8.8' + implementation 'androidx.compose.ui:ui:1.10.5' + implementation 'androidx.compose.foundation:foundation:1.10.5' + // Compose 的图形原生依赖也纳入实际 APK 成员校验。 + implementation 'androidx.graphics:graphics-path:1.0.1' standardImplementation 'dev.rikka.shizuku:api:13.1.5' standardImplementation 'dev.rikka.shizuku:provider:13.1.5' // 13.1.5 从 API 24 起可用;兼容版以 12.2.0 的相同 Binder 协议支持 Android 6。 @@ -382,6 +422,7 @@ dependencies { implementation 'org.bouncycastle:bcprov-jdk15to18:1.85.2' // 宿主凭据 YAML 仅使用 data-only SafeConstructor;来源、JAR 和许可由备份锁核验。 implementation 'org.yaml:snakeyaml:2.4' + testImplementation 'androidx.compose.ui:ui-test-junit4:1.10.5' testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.15.1' } diff --git a/app/src/androidTest/java/com/deepseekharness/app/core/PluginUiInstrumentation.java b/app/src/androidTest/java/com/deepseekharness/app/core/PluginUiInstrumentation.java index 098949177..24da3e652 100644 --- a/app/src/androidTest/java/com/deepseekharness/app/core/PluginUiInstrumentation.java +++ b/app/src/androidTest/java/com/deepseekharness/app/core/PluginUiInstrumentation.java @@ -103,7 +103,7 @@ public void onStart() { until(() -> page != null, "主界面未就绪"); runOnMainSync( () -> - ((com.google.android.material.bottomnavigation.BottomNavigationView) + ((com.deepseekharness.app.ui.miuix.MiuixNavigation) page.findViewById(R.id.bottom_nav)) .setSelectedItemId(R.id.nav_plugins)); until(() -> page.findViewById(R.id.btnPluginInstall) != null, "插件页没有加载"); diff --git a/app/src/androidTest/java/com/deepseekharness/app/ui/ThemeInstrumentation.java b/app/src/androidTest/java/com/deepseekharness/app/ui/ThemeInstrumentation.java index 13d719a13..eb05e9359 100644 --- a/app/src/androidTest/java/com/deepseekharness/app/ui/ThemeInstrumentation.java +++ b/app/src/androidTest/java/com/deepseekharness/app/ui/ThemeInstrumentation.java @@ -261,10 +261,10 @@ public void onStart() { MainActivity current = MainActivity.active(); runOnMainSync( () -> - ((com.google.android.material.bottomnavigation.BottomNavigationView) + ((com.deepseekharness.app.ui.miuix.MiuixNavigation) current.findViewById(R.id.bottom_nav)) .setSelectedItemId(R.id.nav_settings)); - until(() -> MainActivity.active().findViewById(R.id.settings_tabs) != null, "设置页未加载"); + until(() -> MainActivity.active().findViewById(R.id.settings_page) != null, "设置页未加载"); runOnMainSync( () -> audit(MainActivity.active().findViewById(android.R.id.content), "actual-settings")); @@ -275,7 +275,7 @@ public void onStart() { require(PtyTerminalFragment.preferred(getTargetContext()), "当前选择简易终端,需使用独立 PTY 验收"); runOnMainSync( () -> - ((com.google.android.material.bottomnavigation.BottomNavigationView) + ((com.deepseekharness.app.ui.miuix.MiuixNavigation) MainActivity.active().findViewById(R.id.bottom_nav)) .setSelectedItemId(R.id.nav_terminal)); until( @@ -321,10 +321,10 @@ public void onStart() { MainActivity current = MainActivity.active(); runOnMainSync( () -> - ((com.google.android.material.bottomnavigation.BottomNavigationView) + ((com.deepseekharness.app.ui.miuix.MiuixNavigation) current.findViewById(R.id.bottom_nav)) .setSelectedItemId(R.id.nav_settings)); - until(() -> MainActivity.active().findViewById(R.id.settings_tabs) != null, "外观设置未加载"); + until(() -> MainActivity.active().findViewById(R.id.settings_page) != null, "外观设置未加载"); runOnMainSync( () -> ThemeController.select(MainActivity.active(), dark ? "dark" : "light")); until( @@ -367,7 +367,7 @@ public void onStart() { until( () -> MainActivity.active() != before && !ThemeController.isDark(MainActivity.active()), "顶部切换无效"); - require(MainActivity.active().findViewById(R.id.settings_tabs) != null, "切换丢失当前页面"); + require(MainActivity.active().findViewById(R.id.settings_page) != null, "切换丢失当前页面"); runOnMainSync(() -> ThemeController.select(MainActivity.active(), "dark")); until(() -> ThemeController.isDark(MainActivity.active()), "持久性检查准备失败"); output.putString("result", "PASS"); diff --git a/app/src/debug/java/com/deepseekharness/app/ui/FragmentEntrySelfTest.java b/app/src/debug/java/com/deepseekharness/app/ui/FragmentEntrySelfTest.java index ca47909d7..4ba3bbbb9 100644 --- a/app/src/debug/java/com/deepseekharness/app/ui/FragmentEntrySelfTest.java +++ b/app/src/debug/java/com/deepseekharness/app/ui/FragmentEntrySelfTest.java @@ -51,14 +51,19 @@ static String run( driver, () -> { for (int i = 0; i < 30; i++) - entry.requireView().findViewById(R.id.launch_start).performClick(); - require(!entry.requireView().findViewById(R.id.launch_start).isEnabled(), "鉴权中按钮未禁用"); + ((com.deepseekharness.app.ui.miuix.MiuixLaunch) entry.requireView()).start.click(); require( - entry.requireView().findViewById(R.id.launch_status).getVisibility() + !((com.deepseekharness.app.ui.miuix.MiuixLaunch) entry.requireView()) + .start.getEnabled(), + "鉴权中按钮未禁用"); + require( + ((com.deepseekharness.app.ui.miuix.MiuixLaunch) entry.requireView()) + .status.getVisibility() == View.VISIBLE, "鉴权状态被隐藏"); require( - ((TextView) entry.requireView().findViewById(R.id.launch_status)) + ((com.deepseekharness.app.ui.miuix.MiuixLaunch) entry.requireView()) + .status .getText() .toString() .contains("验证"), @@ -71,7 +76,7 @@ static String run( driver, () -> { for (int i = 0; i < 30; i++) - entry.requireView().findViewById(R.id.launch_start).performClick(); + ((com.deepseekharness.app.ui.miuix.MiuixLaunch) entry.requireView()).start.click(); }); require(entry.auth.get() == 1 && entry.opened.get() == 1, "已打开后重复进入"); @@ -94,23 +99,31 @@ static String run( driver, () -> require( - !entry.requireView().findViewById(R.id.launch_start).isEnabled(), "旧回调解除了新请求的锁")); + !((com.deepseekharness.app.ui.miuix.MiuixLaunch) entry.requireView()) + .start.getEnabled(), + "旧回调解除了新请求的锁")); entry.release.countDown(); until(() -> entry.opened.get() == 2); rebuild(driver, host, entry); entry.throwAuth = true; click(driver, entry); - untilUi(driver, () -> entry.requireView().findViewById(R.id.launch_start).isEnabled()); + untilUi( + driver, + () -> + ((com.deepseekharness.app.ui.miuix.MiuixLaunch) entry.requireView()) + .start.getEnabled()); // 超过一次正常状态刷新,失败原因仍必须实际显示,不能只在隐藏的 TextView 中保留文本。 Thread.sleep(1200); main( driver, () -> require( - entry.requireView().findViewById(R.id.launch_status).getVisibility() + ((com.deepseekharness.app.ui.miuix.MiuixLaunch) entry.requireView()) + .status.getVisibility() == View.VISIBLE - && ((TextView) entry.requireView().findViewById(R.id.launch_status)) + && ((com.deepseekharness.app.ui.miuix.MiuixLaunch) entry.requireView()) + .status .getText() .toString() .contains("鉴权失败"), @@ -120,26 +133,37 @@ static String run( entry.emptyCookie = true; click(driver, entry); - untilUi(driver, () -> entry.requireView().findViewById(R.id.launch_start).isEnabled()); + untilUi( + driver, + () -> + ((com.deepseekharness.app.ui.miuix.MiuixLaunch) entry.requireView()) + .start.getEnabled()); main( driver, () -> require( - entry.requireView().findViewById(R.id.launch_status).getVisibility() + ((com.deepseekharness.app.ui.miuix.MiuixLaunch) entry.requireView()) + .status.getVisibility() == View.VISIBLE, "空 cookie 失败原因被隐藏")); require(entry.opened.get() == 2, "空 cookie 仍打开页面"); entry.emptyCookie = false; entry.throwOpen = true; click(driver, entry); - untilUi(driver, () -> entry.requireView().findViewById(R.id.launch_start).isEnabled()); + untilUi( + driver, + () -> + ((com.deepseekharness.app.ui.miuix.MiuixLaunch) entry.requireView()) + .start.getEnabled()); main( driver, () -> require( - entry.requireView().findViewById(R.id.launch_status).getVisibility() + ((com.deepseekharness.app.ui.miuix.MiuixLaunch) entry.requireView()) + .status.getVisibility() == View.VISIBLE - && ((TextView) entry.requireView().findViewById(R.id.launch_status)) + && ((com.deepseekharness.app.ui.miuix.MiuixLaunch) entry.requireView()) + .status .getText() .toString() .contains("无法打开"), @@ -162,7 +186,11 @@ static String run( until(() -> entry.auth.get() == authBefore + 1); main(driver, () -> entry.generation++); entry.release.countDown(); - untilUi(driver, () -> entry.requireView().findViewById(R.id.launch_start).isEnabled()); + untilUi( + driver, + () -> + ((com.deepseekharness.app.ui.miuix.MiuixLaunch) entry.requireView()) + .start.getEnabled()); require(entry.opened.get() == 2, "Web 代次变化后仍打开旧页面"); result.accept("launch=PASS:连点只进入一次,进入失败实际可见,旧视图/代次/URL 失效后不残留错误"); @@ -241,11 +269,14 @@ private static void rebuild(Instrumentation driver, FragmentActivity host, Fragm } private static void click(Instrumentation driver, EntryProbe entry) { - main(driver, () -> entry.requireView().findViewById(R.id.launch_start).performClick()); + main( + driver, + () -> ((com.deepseekharness.app.ui.miuix.MiuixLaunch) entry.requireView()).start.click()); } private static boolean visibleStatusContains(EntryProbe entry, String text) { - TextView status = entry.requireView().findViewById(R.id.launch_status); + com.deepseekharness.app.ui.miuix.MiuixLaunch.Label status = + ((com.deepseekharness.app.ui.miuix.MiuixLaunch) entry.requireView()).status; return status.getVisibility() == View.VISIBLE && status.getText().toString().contains(text); } diff --git a/app/src/debug/java/com/deepseekharness/app/ui/LanguageSettingsAudit.java b/app/src/debug/java/com/deepseekharness/app/ui/LanguageSettingsAudit.java index 787e0301e..276b7845b 100644 --- a/app/src/debug/java/com/deepseekharness/app/ui/LanguageSettingsAudit.java +++ b/app/src/debug/java/com/deepseekharness/app/ui/LanguageSettingsAudit.java @@ -79,7 +79,11 @@ private AccessibilityNodeInfo find(AccessibilityNodeInfo node, String label) { } private void choose(String label) throws Exception { - ui(() -> page.findViewById(R.id.settings_language).performClick()); + ui( + () -> + ((SettingsFragment) + page.getSupportFragmentManager().findFragmentById(R.id.fragment_container)) + .open(R.id.settings_appearance)); Thread.sleep(250); AccessibilityNodeInfo option = find(getUiAutomation().getRootInActiveWindow(), label); check(option != null, "语言选项不可见:" + label); @@ -171,9 +175,8 @@ public void onStart() { check(page != null, "测试宿主未打开"); mount(new SettingsFragment()); check( - ((TextView) page.findViewById(R.id.settings_language)) - .getText() - .toString() + ((com.deepseekharness.app.ui.miuix.MiuixSettings) page.findViewById(R.id.settings_page)) + .getLanguage() .contains("简体中文"), "默认中文未显示"); choose("English"); diff --git a/app/src/debug/java/com/deepseekharness/app/ui/LaunchLogAudit.java b/app/src/debug/java/com/deepseekharness/app/ui/LaunchLogAudit.java index 46e276c1e..80603f35e 100644 --- a/app/src/debug/java/com/deepseekharness/app/ui/LaunchLogAudit.java +++ b/app/src/debug/java/com/deepseekharness/app/ui/LaunchLogAudit.java @@ -6,9 +6,10 @@ import android.view.*; import android.widget.*; import com.deepseekharness.app.R; +import com.deepseekharness.app.ui.miuix.MiuixLaunch; import java.lang.reflect.Method; -/** #61:真实启动 Fragment 的长日志、独立滚动、短窗口和横屏布局回归。 */ +/** #61:真实启动 Fragment 的日志边界、独立滚动和旋转回归;Compose 控件可达性由 UI 包测试验证。 */ public final class LaunchLogAudit extends Instrumentation { private FragmentSessionTestActivity page; private LaunchFragment fragment; @@ -69,15 +70,12 @@ private void actualBounds() throws Exception { log(1000); ui( () -> { - View root = fragment.requireView(), - controls = root.findViewById(R.id.launch_controls), - scroll = root.findViewById(R.id.launch_log_scroll); - check( - rect(controls, root).top >= 0 && rect(controls, root).bottom <= root.getHeight(), - "实际窗口操作栏越界"); - check( - scroll.getHeight() > 0 && rect(scroll, root).bottom <= root.getHeight(), "实际窗口日志越界"); - check(root.getScrollY() == 0, "实际窗口发生整页滚动"); + MiuixLaunch root = (MiuixLaunch) fragment.requireView(); + View scroll = root.logScroll; + int limit = Math.round(220 * root.getResources().getDisplayMetrics().density); + check(root.getWidth() > 0 && root.getHeight() > 0, "Compose 页面未取得实际窗口尺寸"); + check(scroll.getHeight() > 0 && scroll.getHeight() <= limit, "日志未限制自身高度"); + check(scroll.getWidth() <= root.getWidth(), "日志超出页面宽度"); }); } @@ -164,7 +162,7 @@ private void profile(int width, int height) throws Exception { Thread.sleep(250); waitForIdleSync(); log(1); - View controls = root.findViewById(R.id.launch_controls); + View controls = ((MiuixLaunch) root).logScroll; ScrollView scroll = root.findViewById(R.id.launch_log_scroll); android.graphics.Rect[] anchor = {null}; ui(() -> anchor[0] = rect(controls, root)); @@ -174,14 +172,8 @@ private void profile(int width, int height) throws Exception { () -> { check( rect(controls, root).equals(anchor[0]), - "日志 " + lines + " 行移动了操作栏 " + width + "x" + height); - check(anchor[0].top >= 0 && anchor[0].bottom <= root.getHeight(), "操作栏超出窗口"); - check( - scroll.getHeight() > 0 && rect(scroll, root).bottom <= root.getHeight(), - "日志没有被剩余高度约束"); - check( - root.getScrollY() == 0 && root.findViewById(R.id.launch_sections).getScrollY() == 0, - "整页产生了滚动"); + "日志 " + lines + " 行改变了日志容器边界 " + width + "x" + height); + check(scroll.getHeight() > 0 && scroll.getWidth() <= root.getWidth(), "日志未获得独立有界的滚动区域"); }); } int[] historical = {0}; @@ -204,7 +196,7 @@ private void profile(int width, int height) throws Exception { == Math.max(0, scroll.getChildAt(0).getHeight() - scroll.getHeight()), "回到底部后未继续跟随")); Bundle progress = new Bundle(); - progress.putString("stream", "PASS " + width + "x" + height + "dp:1/50/1000 行、顶部按钮、独立滚动与跟随\n"); + progress.putString("stream", "PASS " + width + "x" + height + "dp:1/50/1000 行、容器边界、独立滚动与跟随\n"); sendStatus(0, progress); } @@ -249,11 +241,11 @@ public void onStart() { boolean[] clicked = {false}; ui( () -> { - View button = fragment.requireView().findViewById(R.id.launch_start); + MiuixLaunch.Action button = ((MiuixLaunch) fragment.requireView()).start; button.setOnClickListener(v -> clicked[0] = true); button.setEnabled(true); - button.performClick(); - check(clicked[0], "长日志后启动按钮不可点击"); + button.click(); + check(clicked[0], "长日志后启动动作未派发"); }); result.putString("result", "PASS"); result.putInt("checks", checks); diff --git a/app/src/debug/java/com/deepseekharness/app/ui/LayoutAuditInstrumentation.java b/app/src/debug/java/com/deepseekharness/app/ui/LayoutAuditInstrumentation.java index 4653da1ee..e6da8a050 100644 --- a/app/src/debug/java/com/deepseekharness/app/ui/LayoutAuditInstrumentation.java +++ b/app/src/debug/java/com/deepseekharness/app/ui/LayoutAuditInstrumentation.java @@ -324,15 +324,17 @@ private void navigation() throws Exception { String port = store.getPort(); ui( () -> - ((com.google.android.material.bottomnavigation.BottomNavigationView) + ((com.deepseekharness.app.ui.miuix.MiuixNavigation) MainActivity.active().findViewById(R.id.bottom_nav)) .setSelectedItemId(R.id.nav_settings)); - until(() -> MainActivity.active().findViewById(R.id.settings_tabs) != null, "设置页未打开"); + until(() -> MainActivity.active().findViewById(R.id.settings_page) != null, "设置页未打开"); ui( () -> - ((ViewGroup) MainActivity.active().findViewById(R.id.settings_tabs)) - .getChildAt(1) - .performClick()); + ((SettingsFragment) + MainActivity.active() + .getSupportFragmentManager() + .findFragmentById(R.id.fragment_container)) + .open(R.id.settings_config)); until(() -> MainActivity.active().findViewById(R.id.config_save) != null, "配置页未打开"); require( ((TextView) MainActivity.active().findViewById(R.id.app_title)) @@ -359,7 +361,7 @@ private void navigation() throws Exception { .equals("39081"), "主题切换丢失未保存输入"); ui(() -> MainActivity.active().findViewById(R.id.sub_back).performClick()); - until(() -> MainActivity.active().findViewById(R.id.settings_tabs) != null, "返回设置失败"); + until(() -> MainActivity.active().findViewById(R.id.settings_page) != null, "返回设置失败"); require( MainActivity.active().findViewById(R.id.sub_back).getVisibility() == View.GONE, "根页面仍显示子页返回"); diff --git a/app/src/debug/java/com/deepseekharness/app/ui/LayoutPreviewActivity.java b/app/src/debug/java/com/deepseekharness/app/ui/LayoutPreviewActivity.java index cbf8e7f2b..24eb9a53e 100644 --- a/app/src/debug/java/com/deepseekharness/app/ui/LayoutPreviewActivity.java +++ b/app/src/debug/java/com/deepseekharness/app/ui/LayoutPreviewActivity.java @@ -127,7 +127,10 @@ void showScene(String scene) { : scene.startsWith("launch") ? "fragment_launch" : scene, "layout", getPackageName()); - View body = getLayoutInflater().inflate(layout, null, false); + View body = + scene.startsWith("launch") + ? new com.deepseekharness.app.ui.miuix.MiuixLaunch(this) + : getLayoutInflater().inflate(layout, null, false); boolean fragment = scene.startsWith("fragment_") || scene.startsWith("plugins") || scene.startsWith("launch"); canvas = fragment ? getLayoutInflater().inflate(R.layout.activity_main, null, false) : body; @@ -160,8 +163,7 @@ void showScene(String scene) { ((TextView) canvas.findViewById(R.id.btn_theme)) .setText( com.deepseekharness.app.util.UiText.text(ThemeController.isDark(this) ? "黑夜" : "白天")); - com.google.android.material.bottomnavigation.BottomNavigationView nav = - canvas.findViewById(R.id.bottom_nav); + com.deepseekharness.app.ui.miuix.MiuixNavigation nav = canvas.findViewById(R.id.bottom_nav); nav.setSelectedItemId( title.equals("插件") ? R.id.nav_plugins @@ -252,15 +254,14 @@ private void bind(String scene) { throw new IllegalStateException(error); } } else if (scene.startsWith("launch")) { - text( - R.id.launch_status, - scene.equals("launch_error") ? "连续启动失败,自动重启已暂停。可检查插件后再试。" : "准备好后点击启动,进入本机对话。"); - text(R.id.launch_log, "[布局样例] 检查运行环境\n[布局样例] 等待启动\n\n本页面不启动 dsh,不执行模型请求。"); - if (scene.equals("launch_error")) { - visible(R.id.launch_recovery, true); - text(R.id.launch_recovery, "恢复选项"); - text(R.id.launch_run_state, "自动重启已暂停"); - } + com.deepseekharness.app.ui.miuix.MiuixLaunch launch = + (com.deepseekharness.app.ui.miuix.MiuixLaunch) canvas.findViewById(R.id.launch_panel); + launch.status.setText( + com.deepseekharness.app.util.UiText.text( + scene.equals("launch_error") ? "连续启动失败,自动重启已暂停。可检查插件后再试。" : "准备好后点击启动,进入本机对话。")); + launch.logText.setText("[布局样例] 检查运行环境\n[布局样例] 等待启动\n\n本页面不启动 dsh,不执行模型请求。"); + if (scene.equals("launch_error")) + launch.runState.setText(com.deepseekharness.app.util.UiText.text("自动重启已暂停")); } else if (scene.startsWith("plugins")) { boolean management = scene.equals("plugins_installed"); visible(R.id.pluginMarketCard, !management); diff --git a/app/src/debug/java/com/deepseekharness/app/ui/UiMotionAudit.java b/app/src/debug/java/com/deepseekharness/app/ui/UiMotionAudit.java index 71e7a4b0f..00e74c5f4 100644 --- a/app/src/debug/java/com/deepseekharness/app/ui/UiMotionAudit.java +++ b/app/src/debug/java/com/deepseekharness/app/ui/UiMotionAudit.java @@ -46,7 +46,7 @@ public void onStart() { if (activity == null) throw new AssertionError("主页面未就绪"); ui( () -> - ((com.google.android.material.bottomnavigation.BottomNavigationView) + ((com.deepseekharness.app.ui.miuix.MiuixNavigation) activity.findViewById(R.id.bottom_nav)) .setSelectedItemId(R.id.nav_settings)); settle(); @@ -59,9 +59,18 @@ public void onStart() { final int row = index; ui( () -> - ((ViewGroup) activity.findViewById(R.id.settings_tabs)) - .getChildAt(row) - .performClick()); + ((SettingsFragment) + activity + .getSupportFragmentManager() + .findFragmentById(R.id.fragment_container)) + .open( + new int[] { + R.id.settings_install, + R.id.settings_config, + R.id.settings_data, + R.id.settings_device + } + [row])); settle(); ui(() -> activity.findViewById(R.id.sub_back).performClick()); settle(); diff --git a/app/src/main/assets/runtime-descriptor.json b/app/src/main/assets/runtime-descriptor.json index aeb58d527..f84b41baf 100644 --- a/app/src/main/assets/runtime-descriptor.json +++ b/app/src/main/assets/runtime-descriptor.json @@ -1,6 +1,6 @@ { "version": 1, - "runtimeId": "fec1731f6ba5952e80fd96c01eb354142dfc1b541d7170a20091f826d8655794", + "runtimeId": "aba76b1865dd386f98bae1c13261506d2c07eaf11c79fc4b6680efacbcaf5bfa", "baseVersion": "10", "dshVersion": "0.2.0-rc.2", "launcherContract": "DSHA_ARM64_V2", @@ -158,7 +158,7 @@ "generator:tools/bxroot/test-linux.py": "88662fb1239813eb0b47445bf8cf943586c848de109dd874f8270e44432801cb", "generator:tools/generate-builtin-plugins.py": "2cd39bd2b7ff8f791c18f7a7988d91e15d269438f27005fb18e674358bb7e9cd", "generator:tools/generate-credential-paths.py": "d9012f99294f788357fe202318fb534c092b0cb51514db13511fa0b04018de01", - "generator:tools/generate-third-party-notices.py": "32f1965d928a1a32a651b249d272844f3bffe679a3e6177f9585e4f3e32296e4", + "generator:tools/generate-third-party-notices.py": "35f97f288e8d5bf23baefe4dc641af10971f4c1f1864dd4ff3ba46640ece0038", "generator:tools/mobile-plugin-upstream.lock.json": "29d6c25e10dfd7191aa548f23417a4873712d36ea947ee485ecfe0bacc306786", "generator:tools/mobile-plugin-upstream/lib/client.js": "4e4be68a033be4d0b24d45232665af9704325b13568dd3c5bbf68a615ab2fe33", "generator:tools/mobile-plugin-upstream/lib/compress.js": "66c7cc33cb01c5fcc03830422ca587a378daa8c8e4bb10c300c86dccf7f0f5f3", @@ -168,11 +168,11 @@ "generator:tools/mobile-server/delete-session.js": "cb119ff9703716fe1111ee5a2f211d14886ab1d2310601c5787efc8d91219d51", "generator:tools/mobile-server/index.js": "607ea57134e8772fa5e9bb1778c556703ba21191f8ef198ec278b764f945797a", "generator:tools/mobile-viewport-sync.js": "f0d015d9187ebf0977032c98338b075a6f6d1f89895f16fabc55febbebc3df68", - "generator:tools/native-dependencies.lock.json": "56cbab695a09ab2704e70a2f586b06de77b62e09620630c796551e023cce1a6e", + "generator:tools/native-dependencies.lock.json": "d27d0c433de5ea158ac7d055976a9a353bcde017efed25b8603f82671be6358b", "generator:tools/native-packaging.lock.json": "7af483737ac51c5c55491d288de23f1d9492854e082b6d02ef3e7ee7d0f9589c", "generator:tools/native-session/build.py": "779c3d2bcfd4ba548f951aaca060f920f6749ee8ff32c950e6519aec016e7ede", "generator:tools/native-session/session-launcher.c": "5a37d8a66b10e190738dc30ffaa7a01ced56e6ae852b66a496d4e4bb513abbc1", - "generator:tools/native_runtime_proof.py": "43c48c148209226f9ce22a5819c744c8f41eee49e4d4708a353397d43c879c7c", + "generator:tools/native_runtime_proof.py": "646042962122f65396856b8f29a32dc676fc07090983011884d8bc867574104a", "generator:tools/pc-migration/README.md": "54de253613e338b47ee7861eda0244e700ecaea04ce5c75f3507cbb7381bd6b7", "generator:tools/pc-migration/dsha-portable-restore.mjs": "f829e9695b3375bcb5f58f7c47b36ce2622e9f925e9c71bebca1d6e477ed95ac", "generator:tools/pc-migration/dsha-portable.py": "8eaa783727df449b922df01b84d8447b7bdb0de35a0c7f571806341799d88dfb", @@ -267,7 +267,7 @@ "runtime/WebProcessManager.java": "321a412cf982539d7135c279e5d94959c75c7649582347d044f1d8511c66d4c8", "runtime/WebRecordedStop.java": "90b02684a3eb0177c4f080669d21379167173daf0c6147261c87f4346dfdaa2c", "ui/BrowserOriginRestore.java": "0882b41b186038dc3c846df2eea487197f4bccd173dc2085c4b49bfbc8e237e5", - "ui/BrowserOriginRestoreActivity.java": "d1f9a6e14166ac65df2adf192d5178c3cbeff4eff6471dbbeff57d58f5e9e24c", + "ui/BrowserOriginRestoreActivity.java": "2b2c0255a0e6e3fc4a0294825c1915957d8445af2fd9e6522f459ee496dc0e62", "ui/BrowserOriginRestoreServer.java": "d17546920d66ccdc8121bbcce2376e784ca010af78eeb97b74d9ea63687114ce", "ui/BrowserOriginRestoreWebView.java": "c349045a7ec1f32022fa459d92ef67883ab90daa71e5cdf51be6642aeba7476a", "util/AccessibilityDumpBounds.java": "fb12e29cfa1916e0c2289107292083156d08ff53de7d2d86415672addbb176e1", diff --git a/app/src/main/java/com/deepseekharness/app/ui/AboutFragment.java b/app/src/main/java/com/deepseekharness/app/ui/AboutFragment.java index 3a3bfb765..3c5135cf4 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/AboutFragment.java +++ b/app/src/main/java/com/deepseekharness/app/ui/AboutFragment.java @@ -34,8 +34,10 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa 12, R.color.text_secondary); version.setGravity(Gravity.CENTER); + version.setTextIsSelectable(true); version.setPadding(0, 0, 0, ui.dp(24)); ui.content.addView(version); + ui.label(UiText.choose("应用信息", "Application information")); LinearLayout metadata = ui.card(); ui.kv(metadata, UiText.choose("包名", "Package"), BuildConfig.APPLICATION_ID); ui.kv( diff --git a/app/src/main/java/com/deepseekharness/app/ui/ActionRow.java b/app/src/main/java/com/deepseekharness/app/ui/ActionRow.java new file mode 100644 index 000000000..5c28d22b2 --- /dev/null +++ b/app/src/main/java/com/deepseekharness/app/ui/ActionRow.java @@ -0,0 +1,43 @@ +package com.deepseekharness.app.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; + +/** 等宽操作在文字放不下时改为纵排,避免固定两列截掉按钮文案。 */ +public final class ActionRow extends LinearLayout { + public ActionRow(Context context) { + this(context, null); + } + + public ActionRow(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + int needed = getPaddingLeft() + getPaddingRight(); + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child.getVisibility() == GONE) continue; + child.measure( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + LayoutParams p = (LayoutParams) child.getLayoutParams(); + needed += child.getMeasuredWidth() + p.leftMargin + p.rightMargin; + } + boolean stack = + MeasureSpec.getMode(widthSpec) != MeasureSpec.UNSPECIFIED + && (needed > MeasureSpec.getSize(widthSpec) + || getResources().getConfiguration().fontScale > 1.3f); + setOrientation(stack ? VERTICAL : HORIZONTAL); + for (int i = 0; i < getChildCount(); i++) { + LayoutParams p = (LayoutParams) getChildAt(i).getLayoutParams(); + p.width = stack ? LayoutParams.MATCH_PARENT : 0; + p.height = LayoutParams.WRAP_CONTENT; + p.weight = stack ? 0 : 1; + } + super.onMeasure(widthSpec, heightSpec); + } +} diff --git a/app/src/main/java/com/deepseekharness/app/ui/AdbPairActivity.java b/app/src/main/java/com/deepseekharness/app/ui/AdbPairActivity.java index fcc0fe7bd..757b62080 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/AdbPairActivity.java +++ b/app/src/main/java/com/deepseekharness/app/ui/AdbPairActivity.java @@ -300,6 +300,8 @@ private EditText portField( label.setPadding(0, page.dp(10), 0, page.dp(8)); card.addView(label); EditText input = new EditText(this); + input.setId(android.view.View.generateViewId()); + label.setLabelFor(input.getId()); input.setText(value); input.setTextSize(14); input.setTextColor(getColor(com.deepseekharness.app.R.color.text)); diff --git a/app/src/main/java/com/deepseekharness/app/ui/AgentTasksActivity.java b/app/src/main/java/com/deepseekharness/app/ui/AgentTasksActivity.java index 0d14b24f7..bc8f70afb 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/AgentTasksActivity.java +++ b/app/src/main/java/com/deepseekharness/app/ui/AgentTasksActivity.java @@ -39,7 +39,8 @@ protected void onCreate(Bundle saved) { "View actual conversation progress. Stop affects only the selected task. Shows up to 32 active tasks and the 5 most recent ended states.")); UiNavigation.addHeader(this, page.root, UiText.choose("Agent 任务", "Agent tasks"), this::finish); page.content.removeViewAt(0); - tasks = page.content; + tasks = page.column(); + page.content.addView(tasks); setContentView(page.root); } @@ -81,11 +82,11 @@ private void render() { tasks.removeAllViews(); timings.clear(); if (rows.isEmpty()) { - tasks.addView(page.text(UiText.choose("暂无任务记录", "No task records"), 14, R.color.text)); + tasks.addView(page.text(UiText.choose("暂无任务记录", "No task records"), 16, R.color.text)); return; } for (var row : rows) { - var card = page.card(); + var card = page.card(tasks); card.addView(page.text(AgentTaskNotifications.phase(row), 16, R.color.text)); var elapsed = page.text(timing(row), 12, R.color.text); timings.put(row.key(), elapsed); diff --git a/app/src/main/java/com/deepseekharness/app/ui/AutomaticBackupActivity.java b/app/src/main/java/com/deepseekharness/app/ui/AutomaticBackupActivity.java index 50f51d560..a0fe17485 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/AutomaticBackupActivity.java +++ b/app/src/main/java/com/deepseekharness/app/ui/AutomaticBackupActivity.java @@ -58,6 +58,7 @@ private void render() { "On by default. Saves conversations, attachments, settings and plugins, without copying Ubuntu. Use manual backup for personal projects.")); UiNavigation.addHeader(this, page.root, t("自动备份", "Automatic backups"), this::finish); var prefs = AutomaticBackups.prefs(this); + page.label(t("备份计划", "Backup schedule")); LinearLayout settings = page.card(); var enabled = new com.google.android.material.materialswitch.MaterialSwitch(this); enabled.setText(t("自动备份", "Automatic backups")); @@ -164,7 +165,9 @@ private void render() { localHint.setPadding(0, page.dp(10), 0, 0); settings.addView(localHint); state = page.text("", 12, R.color.text_secondary); + state.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE); page.content.addView(state); + page.label(t("本机副本", "Local copies")); copies = page.card(); page.button( page.footer, diff --git a/app/src/main/java/com/deepseekharness/app/ui/BackgroundTasksActivity.java b/app/src/main/java/com/deepseekharness/app/ui/BackgroundTasksActivity.java index 5bd17bd60..815e348ba 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/BackgroundTasksActivity.java +++ b/app/src/main/java/com/deepseekharness/app/ui/BackgroundTasksActivity.java @@ -50,9 +50,11 @@ protected void onCreate(Bundle saved) { "View active operations and progress. Open the related page to cancel safely.")); UiNavigation.addHeader(this, page.root, t("后台任务", "Background tasks"), this::finish); page.content.removeViewAt(0); - status = page.text("", 13, R.color.text); + page.label(t("当前进度", "Current progress")); + status = page.text("", 15, R.color.text); status.setTextIsSelectable(true); page.card().addView(status); + page.label(t("打开对应页面", "Open the related page")); var links = page.card(); page.button( links, diff --git a/app/src/main/java/com/deepseekharness/app/ui/BrowserOriginRestoreActivity.java b/app/src/main/java/com/deepseekharness/app/ui/BrowserOriginRestoreActivity.java index 19898887a..caf75b97a 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/BrowserOriginRestoreActivity.java +++ b/app/src/main/java/com/deepseekharness/app/ui/BrowserOriginRestoreActivity.java @@ -33,7 +33,14 @@ public void onCreate(Bundle state) { super.onCreate(state); container = new LinearLayout(this); container.setOrientation(LinearLayout.VERTICAL); + container.setBackgroundColor(getColor(com.deepseekharness.app.R.color.surface)); + UiNavigation.addHeader( + this, container, UiText.choose("恢复旧端口草稿", "Restore previous-port drafts"), this::finish); status = new TextView(this); + status.setTextSize(14); + status.setTextColor(getColor(com.deepseekharness.app.R.color.text)); + status.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); + status.setTextIsSelectable(true); status.setPadding(20, 20, 20, 20); container.addView(status); setContentView(container); diff --git a/app/src/main/java/com/deepseekharness/app/ui/CardPage.java b/app/src/main/java/com/deepseekharness/app/ui/CardPage.java index 979fbb247..427d7cedf 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/CardPage.java +++ b/app/src/main/java/com/deepseekharness/app/ui/CardPage.java @@ -6,7 +6,7 @@ import android.widget.*; import com.deepseekharness.app.R; -/** 原生内页共用卡片、分组和固定页脚,业务事件由页面自己绑定。 */ +/** 原生内页共用卡片、分组和随正文滚动的操作区,业务事件由页面自己绑定。 */ final class CardPage { final Context context; final LinearLayout root, content, footer; @@ -18,21 +18,25 @@ final class CardPage { root.setBackgroundColor(context.getColor(R.color.surface)); scroll = new ScrollView(context); scroll.setFillViewport(true); - content = column(); + content = new PageColumn(context); content.setPadding(dp(18), dp(16), dp(18), dp(24)); - scroll.addView(content); root.addView(scroll, new LinearLayout.LayoutParams(-1, 0, 1)); TextView heading = text(title, 24, R.color.text); heading.setTypeface(null, android.graphics.Typeface.BOLD); + androidx.core.view.ViewCompat.setAccessibilityHeading(heading, true); content.addView(heading); if (!subtitle.isEmpty()) { - TextView sub = text(subtitle, 12, R.color.text_secondary); + TextView sub = text(subtitle, 14, R.color.text_secondary); sub.setPadding(0, dp(7), 0, dp(18)); content.addView(sub); } - footer = column(); + footer = new PageColumn(context); footer.setPadding(dp(18), dp(8), dp(18), dp(12)); - root.addView(footer); + // 多个长操作与正文一起滚动,短屏和大字体不让页脚挤掉表单。 + LinearLayout sections = column(); + sections.addView(content); + sections.addView(footer); + scroll.addView(sections); } int dp(int n) { @@ -48,75 +52,56 @@ LinearLayout column() { TextView text(String value, int size, int color) { TextView view = new TextView(context); view.setText(value); - view.setTextSize(size); + view.setTextSize(Math.max(13, size)); + view.setLineSpacing(dp(3), 1); view.setIncludeFontPadding(false); view.setTextColor(context.getColor(color)); return view; } LinearLayout card() { + return card(content); + } + + LinearLayout card(LinearLayout parent) { LinearLayout card = column(); - card.setBackgroundResource(R.drawable.bg_card); card.setPadding(dp(16), dp(14), dp(16), dp(14)); LinearLayout.LayoutParams p = new LinearLayout.LayoutParams(-1, -2); p.bottomMargin = dp(14); - content.addView(card, p); + parent.addView(com.deepseekharness.app.ui.miuix.MiuixUi.card(context, card), p); return card; } void label(String title) { - TextView label = text(title, 11, R.color.text_muted); - label.setPadding(0, dp(6), 0, dp(8)); + TextView label = text(title, 14, R.color.text_secondary); + label.setTypeface(null, android.graphics.Typeface.BOLD); + androidx.core.view.ViewCompat.setAccessibilityHeading(label, true); + label.setPadding(0, dp(12), 0, dp(8)); content.addView(label); } void kv(LinearLayout card, String label, String value) { - LinearLayout row = new LinearLayout(context); + LinearLayout row = column(); row.setGravity(Gravity.CENTER_VERTICAL); - row.setMinimumHeight(dp(38)); - row.addView(text(label, 12, R.color.text_secondary), new LinearLayout.LayoutParams(0, -2, 1)); - TextView content = text(value, 12, R.color.text); - content.setMaxWidth(dp(210)); - content.setGravity(Gravity.END); - row.addView(content); + row.setPadding(0, dp(8), 0, dp(8)); + row.addView(text(label, 13, R.color.text_secondary)); + TextView content = text(value, 15, R.color.text); + content.setTextIsSelectable(true); + content.setPadding(0, dp(4), 0, 0); + row.addView(content, new LinearLayout.LayoutParams(-1, -2)); card.addView(row); } void entry(LinearLayout card, String title, String subtitle, int icon, Runnable action) { - LinearLayout row = new LinearLayout(context); - row.setGravity(Gravity.CENTER_VERTICAL); - row.setPadding(0, dp(10), 0, dp(10)); - row.setMinimumHeight(dp(64)); - row.setBackgroundResource(R.drawable.bg_action_plain); - row.setFocusable(true); - row.setOnClickListener(v -> action.run()); - ImageView mark = new ImageView(context); - mark.setImageResource(icon); - mark.setImageTintList( - android.content.res.ColorStateList.valueOf(context.getColor(R.color.primary))); - mark.setBackgroundResource(R.drawable.bg_logo); - mark.setPadding(dp(7), dp(7), dp(7), dp(7)); - row.addView(mark, new LinearLayout.LayoutParams(dp(32), dp(32))); - LinearLayout words = column(); - TextView heading = text(title, 13, R.color.text); - heading.setTypeface(null, android.graphics.Typeface.BOLD); - words.addView(heading); - TextView detail = text(subtitle, 11, R.color.text_secondary); - detail.setPadding(0, dp(5), 0, 0); - words.addView(detail); - LinearLayout.LayoutParams p = new LinearLayout.LayoutParams(0, -2, 1); - p.leftMargin = dp(12); - row.addView(words, p); - ImageView arrow = new ImageView(context); - arrow.setImageResource(R.drawable.ic_ui2_chevron); - row.addView(arrow, new LinearLayout.LayoutParams(dp(16), dp(16))); - card.addView(row); + card.addView( + com.deepseekharness.app.ui.miuix.MiuixUi.entry(context, title, subtitle, icon, action)); } Button button(LinearLayout parent, String title, boolean primary, Runnable action) { var button = new androidx.appcompat.widget.AppCompatButton(context); button.setText(title); - button.setTextSize(13); + button.setTextSize(15); + button.setPadding(dp(16), dp(12), dp(16), dp(12)); button.setAllCaps(false); button.setMinHeight(dp(48)); button.setBackgroundResource(primary ? R.drawable.bg_btn_primary : R.drawable.bg_btn); diff --git a/app/src/main/java/com/deepseekharness/app/ui/CardSheet.java b/app/src/main/java/com/deepseekharness/app/ui/CardSheet.java index 93c179544..29c959a62 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/CardSheet.java +++ b/app/src/main/java/com/deepseekharness/app/ui/CardSheet.java @@ -6,7 +6,7 @@ import android.widget.*; import com.deepseekharness.app.R; -/** 内容短时收紧,长时滚动正文并保留底部操作。 */ +/** 内容短时收紧,长时让正文和操作一起滚动,整体高度受窗口限制。 */ final class CardSheet { static androidx.appcompat.app.AlertDialog create(Context context, CardPage page) { page.root.setBackgroundResource(R.drawable.bg_card); diff --git a/app/src/main/java/com/deepseekharness/app/ui/ColdSetupSpeedActivity.java b/app/src/main/java/com/deepseekharness/app/ui/ColdSetupSpeedActivity.java index 0b8460ad9..a37fb7bfa 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/ColdSetupSpeedActivity.java +++ b/app/src/main/java/com/deepseekharness/app/ui/ColdSetupSpeedActivity.java @@ -35,10 +35,12 @@ public void handleOnBackPressed() { "在独立目录完整执行首次安装,保留当前环境、数据和设置。开始时会正常停止 DSH 和终端;测速期间请保持应用在前台。", "Run a complete first install in an isolated directory while keeping the existing environment, data and settings. DSH and terminals stop normally before timing. Keep the app in the foreground.")); setContentView(page.root); + page.label(UiText.choose("计时与结果", "Timing and results")); status = page.text( com.deepseekharness.app.core.ColdSetupTiming.report(this, false), 14, R.color.text); - page.content.addView(status); + status.setTextIsSelectable(true); + page.card().addView(status); start = page.button(page.footer, UiText.choose("开始测速", "Start timing"), true, this::run); back = page.button( diff --git a/app/src/main/java/com/deepseekharness/app/ui/CommunityActivity.java b/app/src/main/java/com/deepseekharness/app/ui/CommunityActivity.java index bd22aab72..e01010a56 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/CommunityActivity.java +++ b/app/src/main/java/com/deepseekharness/app/ui/CommunityActivity.java @@ -28,11 +28,9 @@ protected void onCreate(Bundle saved) { root.setBackgroundColor(getColor(R.color.surface)); LinearLayout bar = new LinearLayout(this); bar.setGravity(android.view.Gravity.CENTER_VERTICAL); - Button back = new Button(this); - back.setText(UiText.choose("返回", "Back")); - back.setBackgroundResource(R.drawable.bg_action_plain); - back.setTextColor(getColor(R.color.primary)); - bar.addView(back); + android.widget.ImageButton back = UiNavigation.back(this, this::leave); + int touch = Math.round(48 * getResources().getDisplayMetrics().density); + bar.addView(back, new LinearLayout.LayoutParams(touch, touch)); TextView title = new TextView(this); title.setText(UiText.choose("社区插件生态", "Community plugins")); title.setTextSize(19); @@ -42,11 +40,13 @@ protected void onCreate(Bundle saved) { reload.setText(UiText.choose("重试", "Retry")); reload.setBackgroundResource(R.drawable.bg_action_plain); reload.setTextColor(getColor(R.color.primary)); + reload.setMinHeight(touch); bar.addView(reload); root.addView(bar); status = new TextView(this); status.setPadding(16, 8, 16, 8); status.setTextColor(getColor(R.color.text_muted)); + status.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE); root.addView(status); browser = new WebView(this); browser.setBackgroundColor(getColor(R.color.surface)); @@ -93,7 +93,6 @@ public void onReceivedError( }); root.addView(browser, new LinearLayout.LayoutParams(-1, 0, 1)); setContentView(root); - back.setOnClickListener(v -> leave()); reload.setOnClickListener( v -> browser.loadUrl(com.deepseekharness.app.util.Constants.SITE_URL)); getOnBackPressedDispatcher() diff --git a/app/src/main/java/com/deepseekharness/app/ui/ConfigFragment.java b/app/src/main/java/com/deepseekharness/app/ui/ConfigFragment.java index 2edab8b05..e416dd502 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/ConfigFragment.java +++ b/app/src/main/java/com/deepseekharness/app/ui/ConfigFragment.java @@ -253,8 +253,15 @@ public View onCreateView( try { if (!automaticPort.isChecked()) com.deepseekharness.app.util.ConfigInput.port(port.getText().toString()); + port.setError(null); + apiKey.setError( + apiKey.getText().length() > 16384 + ? com.deepseekharness.app.util.UiText.choose( + "API Key 过长,请检查输入。", "API key is too long. Check the value.") + : null); save.setEnabled(apiKey.getText().length() <= 16384); } catch (IllegalArgumentException invalid) { + port.setError(invalid.getMessage()); save.setEnabled(false); } }; diff --git a/app/src/main/java/com/deepseekharness/app/ui/EnvironmentRecoveryFragment.java b/app/src/main/java/com/deepseekharness/app/ui/EnvironmentRecoveryFragment.java index 9f44e86cf..97877012a 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/EnvironmentRecoveryFragment.java +++ b/app/src/main/java/com/deepseekharness/app/ui/EnvironmentRecoveryFragment.java @@ -15,45 +15,30 @@ public final class EnvironmentRecoveryFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle saved) { - LinearLayout content = new LinearLayout(requireContext()); - content.setOrientation(LinearLayout.VERTICAL); - int pad = Math.round(20 * getResources().getDisplayMetrics().density); - content.setPadding(pad, pad, pad, pad); - TextView title = new TextView(requireContext()); - title.setText(com.deepseekharness.app.util.UiText.text("运行环境需要恢复")); - title.setTextSize(20); - title.setTextColor(requireContext().getColor(R.color.text)); - content.addView(title); - TextView hint = new TextView(requireContext()); - hint.setText( - com.deepseekharness.app.util.UiText.text( - "可以在设置中修改配置、查看数据维护记录和下载日志。环境恢复完成后,再使用插件和终端。个人数据与原环境继续保留。\n")); - hint.setTextSize(15); - hint.setTextColor(requireContext().getColor(R.color.text_secondary)); - content.addView(hint); - Button emergency = new androidx.appcompat.widget.AppCompatButton(requireContext()); - emergency.setText( - com.deepseekharness.app.util.UiText.choose("启动应急 DSH", "Start emergency DSH")); - emergency.setTextSize(15); - emergency.setBackgroundResource(R.drawable.bg_btn_primary); - emergency.setTextColor(requireContext().getColorStateList(R.color.button_primary_text)); - emergency.setOnClickListener( - v -> startActivity(new Intent(requireContext(), RecoveryActivity.class))); - content.addView(emergency, new LinearLayout.LayoutParams(-1, -2)); - Button repair = new Button(requireContext()); - repair.setText(com.deepseekharness.app.util.UiText.text("查看维护进度与恢复选项")); - repair.setOnClickListener( - v -> + CardPage page = + new CardPage( + requireContext(), + com.deepseekharness.app.util.UiText.text("运行环境需要恢复"), + com.deepseekharness.app.util.UiText.text( + "可以在设置中修改配置、查看数据维护记录和下载日志。环境恢复完成后,再使用插件和终端。个人数据与原环境继续保留。")); + LinearLayout actions = page.card(); + page.button( + actions, + com.deepseekharness.app.util.UiText.choose("启动应急 DSH", "Start emergency DSH"), + true, + () -> startActivity(new Intent(requireContext(), RecoveryActivity.class))); + page.button( + actions, + com.deepseekharness.app.util.UiText.text("查看维护进度与恢复选项"), + false, + () -> startActivity( new Intent(requireContext(), ExtractActivity.class).putExtra("review_only", true))); - content.addView(repair, new LinearLayout.LayoutParams(-1, -2)); - Button logs = new Button(requireContext()); - logs.setText(com.deepseekharness.app.util.UiText.text("查看并下载诊断日志")); - logs.setOnClickListener(v -> startActivity(DiagnosticActivity.downloadLogs(requireContext()))); - content.addView(logs, new LinearLayout.LayoutParams(-1, -2)); - android.widget.ScrollView scroll = new android.widget.ScrollView(requireContext()); - scroll.setFillViewport(true); - scroll.addView(content); - return scroll; + page.button( + actions, + com.deepseekharness.app.util.UiText.text("查看并下载诊断日志"), + false, + () -> startActivity(DiagnosticActivity.downloadLogs(requireContext()))); + return page.root; } } diff --git a/app/src/main/java/com/deepseekharness/app/ui/LaunchFragment.java b/app/src/main/java/com/deepseekharness/app/ui/LaunchFragment.java index b2d5852b4..a2433a16f 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/LaunchFragment.java +++ b/app/src/main/java/com/deepseekharness/app/ui/LaunchFragment.java @@ -8,7 +8,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; +import com.deepseekharness.app.ui.miuix.MiuixLaunch; import android.widget.TextView; import android.widget.Toast; @@ -83,61 +83,58 @@ public View onCreateView( @NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.fragment_launch, container, false); + MiuixLaunch v = new MiuixLaunch(requireContext()); controller = HarnessController.get(requireContext()); final Activity activity = requireActivity(); - TextView status = v.findViewById(R.id.launch_status); - Button start = v.findViewById(R.id.launch_start); - Button restart = v.findViewById(R.id.launch_open); - Button stop = v.findViewById(R.id.launch_stop); - lanAddrText = v.findViewById(R.id.lan_addr); - launchLog = v.findViewById(R.id.launch_log); + MiuixLaunch.Label status = v.status; + MiuixLaunch.Action start = v.start; + MiuixLaunch.Action restart = v.restart; + MiuixLaunch.Action stop = v.stop; + lanAddrText = v.lanAddress; + launchLog = v.logText; logRevision = -1; logUrl = ""; - v.findViewById(R.id.launch_download_logs) - .setOnClickListener(x -> startActivity(DiagnosticActivity.downloadLogs(requireContext()))); - - v.findViewById(R.id.launch_models) - .setOnClickListener( - x -> startActivity(new Intent(requireContext(), ModelSetupActivity.class))); - v.findViewById(R.id.launch_address) - .setOnClickListener( - x -> { - String local = controller.getWebAuthUrl(); - String addresses = - local.isEmpty() - ? com.deepseekharness.app.util.UiText.choose( - "启动后可查看访问地址。", "Start DSH to view access addresses.") - : local; - if (lanAddrText != null && lanAddrText.getVisibility() == View.VISIBLE) - addresses += "\n\n" + lanAddrText.getText(); - var dialog = - new DshaDialogBuilder(requireContext()) - .setTitle(R.string.ui2_address) - .setMessage(addresses) - .setNegativeButton(android.R.string.cancel, null); - if (!local.isEmpty()) - dialog.setPositiveButton( - com.deepseekharness.app.util.UiText.choose("复制本机地址", "Copy local URL"), - (d, w) -> copyAddr("DSH", local)); - dialog.show(); - }); + v.downloadLogs.setOnClickListener( + x -> startActivity(DiagnosticActivity.downloadLogs(requireContext()))); + + v.models.setOnClickListener( + x -> startActivity(new Intent(requireContext(), ModelSetupActivity.class))); + v.address.setOnClickListener( + x -> { + String local = controller.getWebAuthUrl(); + String addresses = + local.isEmpty() + ? com.deepseekharness.app.util.UiText.choose( + "启动后可查看访问地址。", "Start DSH to view access addresses.") + : local; + if (lanAddrText != null && lanAddrText.getVisibility() == View.VISIBLE) + addresses += "\n\n" + lanAddrText.getText(); + var dialog = + new DshaDialogBuilder(requireContext()) + .setTitle(R.string.ui2_address) + .setMessage(addresses) + .setNegativeButton(android.R.string.cancel, null); + if (!local.isEmpty()) + dialog.setPositiveButton( + com.deepseekharness.app.util.UiText.choose("复制本机地址", "Copy local URL"), + (d, w) -> copyAddr("DSH", local)); + dialog.show(); + }); restart.setText(R.string.ui2_restart); - v.findViewById(R.id.launch_recovery).setOnClickListener(x -> showRecovery()); - v.findViewById(R.id.launch_safe) - .setOnClickListener( - x -> - new com.deepseekharness.app.ui.DshaDialogBuilder(requireContext()) - .setTitle(com.deepseekharness.app.util.UiText.text("安全启动 Web?")) - .setMessage( - com.deepseekharness.app.util.UiText.text( - "停止当前启动,只加载官方基础界面。原插件开关、会话、模型和配置保留;普通重启后回到原配置。安全界面不加载移动插件等扩展。")) - .setNegativeButton(com.deepseekharness.app.util.UiText.text("取消"), null) - .setPositiveButton( - com.deepseekharness.app.util.UiText.text("安全启动"), - (dialog, which) -> doStart(activity, status, start, true)) - .show()); + v.recovery.setOnClickListener(x -> showRecovery()); + v.safe.setOnClickListener( + x -> + new com.deepseekharness.app.ui.DshaDialogBuilder(requireContext()) + .setTitle(com.deepseekharness.app.util.UiText.text("安全启动 Web?")) + .setMessage( + com.deepseekharness.app.util.UiText.text( + "停止当前启动,只加载官方基础界面。原插件开关、会话、模型和配置保留;普通重启后回到原配置。安全界面不加载移动插件等扩展。")) + .setNegativeButton(com.deepseekharness.app.util.UiText.text("取消"), null) + .setPositiveButton( + com.deepseekharness.app.util.UiText.text("安全启动"), + (dialog, which) -> doStart(activity, status, start, true)) + .show()); // 启动按钮:未就绪时是「启动」;鉴权链接就绪后自动变为「进入」,点击进 WebUI。 start.setOnClickListener( @@ -190,11 +187,12 @@ public View onCreateView( } /** 启动 dsh:记录启动时刻,鉴权链接就绪后把「启动」变「进入」并输出 URL 到日志。 */ - private void doStart(Activity activity, TextView status, Button start) { + private void doStart(Activity activity, MiuixLaunch.Label status, MiuixLaunch.Action start) { doStart(activity, status, start, false); } - private void doStart(Activity activity, TextView status, Button start, boolean safeMode) { + private void doStart( + Activity activity, MiuixLaunch.Label status, MiuixLaunch.Action start, boolean safeMode) { if (!safeMode && (controller.isStarting() || controller.isStopping())) return; if (startRefusal != null && startRefusal.isShowing()) return; final View root = getView(); @@ -319,8 +317,7 @@ private void enterWeb() { final long generation = webEntryGeneration(); final long request = ++enterRequest; enteringWeb = true; - ((TextView) root.findViewById(R.id.launch_status)) - .setText(com.deepseekharness.app.util.UiText.text("正在验证 Web 访问权限…")); + ((MiuixLaunch) root).status.setText(com.deepseekharness.app.util.UiText.text("正在验证 Web 访问权限…")); refreshRunState(); try { new Thread( @@ -355,8 +352,9 @@ private void enterWeb() { } try { openWebEntry(activity, url, authCookie); - ((TextView) root.findViewById(R.id.launch_status)) - .setText(com.deepseekharness.app.util.UiText.text("鉴权成功,正在打开 Web…")); + ((MiuixLaunch) root) + .status.setText( + com.deepseekharness.app.util.UiText.text("鉴权成功,正在打开 Web…")); refreshLanAddr(); } catch (RuntimeException error) { finishWebEntry( @@ -396,7 +394,7 @@ private void finishWebEntry(View root, String message) { webEntryFailure = message == null ? "" : message; webEntryFailureGeneration = webEntryGeneration(); webEntryFailureUrl = webEntryUrl(); - TextView status = root.findViewById(R.id.launch_status); + MiuixLaunch.Label status = ((MiuixLaunch) root).status; status.setText(com.deepseekharness.app.util.UiStateText.render(webEntryFailure)); status.setVisibility(webEntryFailure.isEmpty() ? View.GONE : View.VISIBLE); refreshRunState(); @@ -422,7 +420,7 @@ private void clearWebEntryFailure() { private void updateLog(String text) { View root = getView(); if (root == null || launchLog == null) return; - LogScrollView scroll = root.findViewById(R.id.launch_log_scroll); + LogScrollView scroll = ((MiuixLaunch) root).logScroll; boolean follow = scroll.shouldFollowEnd(); launchLog.setText(text); if (follow) scroll.followEndAfterLayout(); @@ -441,8 +439,8 @@ public void onPause() { // 页面不可见时撤销自动进入:用户离开了启动页,回来不该被"突袭"打开 Web。 pendingAutoEnter = false; if (enteringWeb && getView() != null) { - ((TextView) getView().findViewById(R.id.launch_status)) - .setText(com.deepseekharness.app.util.UiText.text("返回后可重新进入 Web")); + ((MiuixLaunch) getView()) + .status.setText(com.deepseekharness.app.util.UiText.text("返回后可重新进入 Web")); } invalidateWebEntry(); ui.removeCallbacks(refreshState); @@ -463,8 +461,8 @@ private void refreshRunState() { try { View root = getView(); if (root == null) return; - TextView runState = root.findViewById(R.id.launch_run_state); - Button start = root.findViewById(R.id.launch_start); + MiuixLaunch.Label runState = ((MiuixLaunch) root).runState; + MiuixLaunch.Action start = ((MiuixLaunch) root).start; if (runState == null) return; boolean starting = controller.isStarting(); boolean stopping = controller.isStopping(); @@ -513,12 +511,11 @@ private void refreshRunState() { : controller.isUserStopped() ? getString(R.string.ui2_stopped) : getString(R.string.ui2_stopped)); - ((TextView) root.findViewById(R.id.launch_port)) - .setText(ready ? String.valueOf(controller.getWebPort()) : "—"); - ((TextView) root.findViewById(R.id.launch_environment)) - .setText(EnvironmentUiStatus.get(requireContext()).ready ? "READY" : "—"); - ((TextView) root.findViewById(R.id.launch_subtitle)) - .setText( + ((MiuixLaunch) root).port.setText(ready ? String.valueOf(controller.getWebPort()) : "—"); + ((MiuixLaunch) root) + .environment.setText(EnvironmentUiStatus.get(requireContext()).ready ? "READY" : "—"); + ((MiuixLaunch) root) + .subtitle.setText( ready ? (controller.isWebCompatibilityFallback() ? "proot" @@ -526,8 +523,8 @@ private void refreshRunState() { + " · dsh " + com.deepseekharness.app.util.Constants.DSH_VERSION : getString(R.string.ui2_launch_hint)); - root.findViewById(R.id.launch_status) - .setVisibility( + ((MiuixLaunch) root) + .status.setVisibility( enteringWeb || !webEntryFailure.isEmpty() || starting @@ -537,8 +534,8 @@ private void refreshRunState() { ? View.VISIBLE : View.GONE); if (starting) - ((TextView) root.findViewById(R.id.launch_status)) - .setText( + ((MiuixLaunch) root) + .status.setText( com.deepseekharness.app.util.UiText.format( "%s · 本阶段 %s 秒 · 总计 %s 秒\n%s", trace.stage, @@ -548,14 +545,13 @@ private void refreshRunState() { ? com.deepseekharness.app.util.UiText.text("下方实时显示启动输出;等待不会自动终止。") : com.deepseekharness.app.util.UiText.text("检测到插件或配置异常,可查看恢复选项。"))); else if (!webEntryFailure.isEmpty()) - ((TextView) root.findViewById(R.id.launch_status)) - .setText(com.deepseekharness.app.util.UiStateText.render(webEntryFailure)); + ((MiuixLaunch) root) + .status.setText(com.deepseekharness.app.util.UiStateText.render(webEntryFailure)); else if (expiredEntryFailure) - ((TextView) root.findViewById(R.id.launch_status)) - .setText(com.deepseekharness.app.util.UiStateText.render(trace.stage)); - root.findViewById(R.id.launch_busy) - .setVisibility(starting || stopping ? View.VISIBLE : View.GONE); - Button recovery = root.findViewById(R.id.launch_recovery); + ((MiuixLaunch) root) + .status.setText(com.deepseekharness.app.util.UiStateText.render(trace.stage)); + ((MiuixLaunch) root).busy.setVisibility(starting || stopping ? View.VISIBLE : View.GONE); + MiuixLaunch.Action recovery = ((MiuixLaunch) root).recovery; int failures = controller.config().getWebFailures(); recovery.setVisibility(View.VISIBLE); recovery.setText(com.deepseekharness.app.util.UiText.text("恢复选项")); @@ -575,15 +571,15 @@ else if (expiredEntryFailure) : getString(R.string.ui2_start)); start.setEnabled(!enteringWeb && !starting && !stopping); } - Button restart = root.findViewById(R.id.launch_open); + MiuixLaunch.Action restart = ((MiuixLaunch) root).restart; if (restart != null) restart.setEnabled(!starting && !stopping); - Button stop = root.findViewById(R.id.launch_stop); + MiuixLaunch.Action stop = ((MiuixLaunch) root).stop; if (stop != null) stop.setEnabled(!stopping); if (EnvironmentUiStatus.get(requireContext()).recovery) { runState.setText(com.deepseekharness.app.util.UiText.text("环境需要恢复")); if (start != null) start.setEnabled(false); if (restart != null) restart.setEnabled(false); - root.findViewById(R.id.launch_safe).setEnabled(false); + ((MiuixLaunch) root).safe.setEnabled(false); recovery.setVisibility(View.VISIBLE); recovery.setText(com.deepseekharness.app.util.UiText.text("环境维护")); recovery.setContentDescription(com.deepseekharness.app.util.UiText.text("查看环境维护与恢复选项")); @@ -593,7 +589,7 @@ else if (expiredEntryFailure) new Intent(requireContext(), ExtractActivity.class) .putExtra("review_only", true))); } else { - root.findViewById(R.id.launch_safe).setEnabled(!starting && !stopping); + ((MiuixLaunch) root).safe.setEnabled(!starting && !stopping); recovery.setOnClickListener(v -> showRecovery()); } } catch (RuntimeException failure) { @@ -602,7 +598,7 @@ else if (expiredEntryFailure) getContext(), "UI_REFRESH_FAILED", failure.getClass().getSimpleName()); View view = getView(); if (view != null) { - TextView state = view.findViewById(R.id.launch_run_state); + MiuixLaunch.Label state = ((MiuixLaunch) view).runState; if (state != null) state.setText( com.deepseekharness.app.util.UiText.choose( @@ -615,7 +611,8 @@ private void showRecovery() { startActivity(new Intent(requireContext(), StartupRecoveryActivity.class)); } - private void offerForceStop(Activity activity, View root, TextView status, long generation) { + private void offerForceStop( + Activity activity, View root, MiuixLaunch.Label status, long generation) { // 进程与文件身份核验放在工作线程;本次候选只捕获一次,由对话框局部持有至确认。 new Thread( () -> { diff --git a/app/src/main/java/com/deepseekharness/app/ui/LogScrollView.java b/app/src/main/java/com/deepseekharness/app/ui/LogScrollView.java index a02652f14..1fe1fc58d 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/LogScrollView.java +++ b/app/src/main/java/com/deepseekharness/app/ui/LogScrollView.java @@ -1,6 +1,7 @@ package com.deepseekharness.app.ui; import android.content.Context; +import android.graphics.Canvas; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; @@ -29,6 +30,20 @@ protected void onMeasure(int widthSpec, int heightSpec) { widthSpec, View.MeasureSpec.makeMeasureSpec(available, View.MeasureSpec.EXACTLY)); } + @Override + protected void dispatchDraw(Canvas canvas) { + // Compose 的 AndroidView 宿主不保证裁剪,日志框自行限制子控件的绘制范围。 + // Canvas 已带滚动位移,裁剪区域也须带上滚动量,才能保留正在查看的日志。 + int saved = canvas.save(); + try { + canvas.clipRect( + getScrollX(), getScrollY(), getScrollX() + getWidth(), getScrollY() + getHeight()); + super.dispatchDraw(canvas); + } finally { + canvas.restoreToCount(saved); + } + } + @Override public boolean dispatchTouchEvent(MotionEvent event) { ViewParent parent = getParent(); diff --git a/app/src/main/java/com/deepseekharness/app/ui/MainActivity.java b/app/src/main/java/com/deepseekharness/app/ui/MainActivity.java index b2f636c20..0b067a379 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/MainActivity.java +++ b/app/src/main/java/com/deepseekharness/app/ui/MainActivity.java @@ -12,7 +12,7 @@ import com.deepseekharness.app.R; import com.deepseekharness.app.core.ConfigStore; import com.deepseekharness.app.core.HarnessController; -import com.google.android.material.bottomnavigation.BottomNavigationView; +import com.deepseekharness.app.ui.miuix.MiuixNavigation; /** * 主界面外壳:启动门禁 + 底部导航(启动 / 插件 / 设置 / 终端)+ 顶栏标题 + 关于入口。 @@ -154,11 +154,11 @@ public void onFragmentResumed( ((android.widget.ImageView) quickEntries.getChildAt(0)).setImageResource(R.drawable.ic_about); quickEntries.setOnClickListener(v -> AboutDialog.show(this)); - BottomNavigationView nav = findViewById(R.id.bottom_nav); + MiuixNavigation nav = findViewById(R.id.bottom_nav); nav.setOnItemSelectedListener( - item -> { + id -> { // 当前根页面再次点选时保留输入和滚动;从子页返回或外部打开插件仍执行导航。 - if (nav.getSelectedItemId() == item.getItemId() + if (nav.getSelectedItemId() == id && getSupportFragmentManager().getBackStackEntryCount() == 0 && getSupportFragmentManager().findFragmentById(R.id.fragment_container) != null && !(getSupportFragmentManager().findFragmentById(R.id.fragment_container) @@ -169,7 +169,6 @@ && getSupportFragmentManager().findFragmentById(R.id.fragment_container) != null .popBackStackImmediate( null, androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE); Fragment f; - int id = item.getItemId(); if (id == R.id.nav_launch) { f = new LaunchFragment(); if (pendingOpenWeb) { @@ -291,7 +290,7 @@ protected void onPause() { protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); - BottomNavigationView nav = findViewById(R.id.bottom_nav); + MiuixNavigation nav = findViewById(R.id.bottom_nav); if (nav != null && intent.getBooleanExtra("open_plugins", false)) nav.setSelectedItemId(R.id.nav_plugins); if (nav != null && intent.getBooleanExtra("open_launch", false)) { @@ -349,7 +348,7 @@ private void showThemePreference() { } private void consumeTaskTarget(Intent intent) { - BottomNavigationView nav = findViewById(R.id.bottom_nav); + MiuixNavigation nav = findViewById(R.id.bottom_nav); if (nav == null) return; if (intent.getBooleanExtra("open_terminal", false)) { intent.removeExtra("open_terminal"); @@ -379,7 +378,7 @@ private void consumeTaskTarget(Intent intent) { private void consumeOpenWeb(Intent intent) { if (intent == null || !intent.getBooleanExtra("open_web", false)) return; intent.removeExtra("open_web"); - BottomNavigationView nav = findViewById(R.id.bottom_nav); + MiuixNavigation nav = findViewById(R.id.bottom_nav); if (nav == null) return; pendingOpenWeb = true; if (nav.getSelectedItemId() != R.id.nav_launch) { diff --git a/app/src/main/java/com/deepseekharness/app/ui/ModelSetupActivity.java b/app/src/main/java/com/deepseekharness/app/ui/ModelSetupActivity.java index e757897d1..0eec5afc9 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/ModelSetupActivity.java +++ b/app/src/main/java/com/deepseekharness/app/ui/ModelSetupActivity.java @@ -177,10 +177,9 @@ private void clearBody() { private LinearLayout card(String title) { LinearLayout c = page.column(); c.setPadding(page.dp(16), page.dp(14), page.dp(16), page.dp(14)); - c.setBackgroundResource(R.drawable.bg_card); LinearLayout.LayoutParams p = new LinearLayout.LayoutParams(-1, -2); p.bottomMargin = page.dp(12); - body.addView(c, p); + body.addView(com.deepseekharness.app.ui.miuix.MiuixUi.card(this, c), p); if (!title.isEmpty()) { TextView heading = page.text(title, 16, R.color.text); heading.setTypeface(null, android.graphics.Typeface.BOLD); @@ -337,12 +336,15 @@ private void showEditor() { namespace(s(draft.entry, "settingsNs")).getAsJsonObject("schema"), protocolPath)); if (!draft.protocol.isEmpty() && !protocols.contains(draft.protocol)) protocols.add(draft.protocol); - label(connection, t("连接协议", "Protocol")); + TextView protocolLabel = label(connection, t("连接协议", "Protocol")); protocol = new DshaSelectView(this); + protocol.setId(View.generateViewId()); + protocolLabel.setLabelFor(protocol.getId()); + protocol.setMinimumHeight(page.dp(56)); protocol.setPrompt(t("连接协议", "Protocol")); protocol.setAdapter(new ArrayAdapter<>(this, R.layout.item_data_choice, protocols)); protocol.setSelection(Math.max(0, protocols.indexOf(draft.protocol))); - connection.addView(protocol, new LinearLayout.LayoutParams(-1, page.dp(50))); + connection.addView(protocol, new LinearLayout.LayoutParams(-1, -2)); key = field( connection, @@ -371,16 +373,19 @@ private void showEditor() { save.setEnabled(!Boolean.TRUE.equals(repository.busy.getValue())); } - private void label(LinearLayout parent, String title) { + private TextView label(LinearLayout parent, String title) { TextView view = page.text(title, 12, R.color.text_secondary); view.setPadding(0, page.dp(12), 0, page.dp(7)); parent.addView(view); + return view; } private EditText field( LinearLayout parent, String title, String hint, String value, boolean secret) { - label(parent, title); + TextView caption = label(parent, title); EditText input = new EditText(this); + input.setId(View.generateViewId()); + caption.setLabelFor(input.getId()); input.setTextSize(14); input.setTextColor(getColor(R.color.text)); input.setHintTextColor(getColor(R.color.text_muted)); diff --git a/app/src/main/java/com/deepseekharness/app/ui/NativeDataActivity.java b/app/src/main/java/com/deepseekharness/app/ui/NativeDataActivity.java index 458042f43..b76047b46 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/NativeDataActivity.java +++ b/app/src/main/java/com/deepseekharness/app/ui/NativeDataActivity.java @@ -138,7 +138,7 @@ protected void onCreate(Bundle saved) { ScrollView scroll = new ScrollView(this); scroll.setFillViewport(true); scroll.setBackgroundColor(getColor(R.color.surface)); - page = new LinearLayout(this); + page = new PageColumn(this); page.setOrientation(LinearLayout.VERTICAL); int pad = dp(20); page.setPadding(pad, dp(12), pad, pad); @@ -187,6 +187,7 @@ protected void onCreate(Bundle saved) { 13); status = text("", 14); status.setTextIsSelectable(true); + status.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); status.setBackgroundResource(R.drawable.bg_card); status.setPadding(dp(16), dp(12), dp(16), dp(12)); section(t("选择数据范围", "Choose data scope")); @@ -1146,7 +1147,8 @@ private TextView text(String value, int size) { private Button button(String label, Runnable click) { Button button = new androidx.appcompat.widget.AppCompatButton(this); - button.setTextSize(13); + button.setTextSize(15); + button.setPadding(dp(12), dp(12), dp(12), dp(12)); button.setBackgroundResource(R.drawable.bg_action_plain); button.setTextColor(getColor(R.color.primary)); button.setText(label); @@ -1165,10 +1167,9 @@ private void section(String title) { body = new LinearLayout(this); body.setOrientation(LinearLayout.VERTICAL); body.setPadding(dp(14), dp(10), dp(14), dp(14)); - body.setBackgroundResource(R.drawable.bg_card); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(-1, -2); params.topMargin = dp(16); - page.addView(body, params); + page.addView(com.deepseekharness.app.ui.miuix.MiuixUi.card(this, body), params); text(title, 16).setTypeface(null, android.graphics.Typeface.BOLD); } diff --git a/app/src/main/java/com/deepseekharness/app/ui/OnboardingUi.java b/app/src/main/java/com/deepseekharness/app/ui/OnboardingUi.java index 2c3f17c54..2c9c63383 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/OnboardingUi.java +++ b/app/src/main/java/com/deepseekharness/app/ui/OnboardingUi.java @@ -5,14 +5,14 @@ import android.widget.*; import com.deepseekharness.app.R; -/** 首次引导共用排版。内容可滚动,底部主操作始终可见。 */ +/** 首次引导共用排版。说明与操作一起滚动,大字体横屏仍可读完并继续。 */ final class OnboardingUi { final Activity activity; final LinearLayout page, body, footer; OnboardingUi(Activity owner, String step, String title, String subtitle) { activity = owner; - page = new LinearLayout(owner); + page = new PageColumn(owner); page.setOrientation(LinearLayout.VERTICAL); page.setBackgroundColor(owner.getColor(R.color.surface)); page.setPadding(dp(24), dp(18), dp(24), dp(18)); @@ -21,13 +21,16 @@ final class OnboardingUi { TextView brand = text("DSHA", 18, R.color.text); brand.setTypeface(null, android.graphics.Typeface.BOLD); header.addView(brand, new LinearLayout.LayoutParams(0, -2, 1)); - header.addView(text(step, 11, R.color.text_muted)); + header.addView(text(step, 13, R.color.text_muted)); page.addView(header); ScrollView scroll = new ScrollView(owner); body = new LinearLayout(owner); body.setOrientation(LinearLayout.VERTICAL); body.setPadding(0, dp(24), 0, dp(16)); - scroll.addView(body); + LinearLayout sections = new LinearLayout(owner); + sections.setOrientation(LinearLayout.VERTICAL); + sections.addView(body); + scroll.addView(sections); page.addView(scroll, new LinearLayout.LayoutParams(-1, 0, 1)); TextView heading = text(title, 27, R.color.text); heading.setTypeface(null, android.graphics.Typeface.BOLD); @@ -35,7 +38,7 @@ final class OnboardingUi { note(subtitle); footer = new LinearLayout(owner); footer.setOrientation(LinearLayout.VERTICAL); - page.addView(footer); + sections.addView(footer); owner.setContentView(page); } @@ -65,14 +68,13 @@ void row(String label, String value) { LinearLayout card = new LinearLayout(activity); card.setOrientation(LinearLayout.VERTICAL); card.setPadding(dp(16), dp(14), dp(16), dp(14)); - card.setBackgroundResource(R.drawable.bg_card); - card.addView(text(label, 11, R.color.text_muted)); + card.addView(text(label, 13, R.color.text_secondary)); TextView content = text(value, 16, R.color.text); content.setPadding(0, dp(7), 0, 0); card.addView(content); LinearLayout.LayoutParams p = new LinearLayout.LayoutParams(-1, -2); p.bottomMargin = dp(10); - body.addView(card, p); + body.addView(com.deepseekharness.app.ui.miuix.MiuixUi.card(activity, card), p); } void primary(String title, Runnable action) { diff --git a/app/src/main/java/com/deepseekharness/app/ui/OverlayFragment.java b/app/src/main/java/com/deepseekharness/app/ui/OverlayFragment.java index c1373109a..e0c487052 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/OverlayFragment.java +++ b/app/src/main/java/com/deepseekharness/app/ui/OverlayFragment.java @@ -33,6 +33,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa requireContext(), UiText.choose("悬浮条", "Floating status"), UiText.choose("在其他应用中,轻量查看任务进展。", "Follow progress while using other apps.")); + ui.label(UiText.choose("实时预览与权限", "Live preview and access")); LinearLayout preview = ui.card(); enabled = toggle( @@ -78,6 +79,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa }); previewAction.setId(R.id.overlay_preview_button); previewAction.setContentDescription(UiText.choose("显示系统悬浮条预览", "Show system overlay preview")); + ui.label(UiText.choose("外观", "Appearance")); LinearLayout appearance = ui.card(); opacityLabel = ui.text("", 13, R.color.text); appearance.addView(opacityLabel); @@ -139,6 +141,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa : saved.getInt("font")) - 6); appearance.addView(font, new LinearLayout.LayoutParams(-1, ui.dp(48))); + ui.label(UiText.choose("显示内容", "Visible content")); LinearLayout behavior = ui.card(); reasoning = toggle( diff --git a/app/src/main/java/com/deepseekharness/app/ui/PageColumn.java b/app/src/main/java/com/deepseekharness/app/ui/PageColumn.java new file mode 100644 index 000000000..869ab85b3 --- /dev/null +++ b/app/src/main/java/com/deepseekharness/app/ui/PageColumn.java @@ -0,0 +1,28 @@ +package com.deepseekharness.app.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.LinearLayout; + +/** 阅读和表单区域:平板上限制行长,保留原来的上下边距。 */ +public final class PageColumn extends LinearLayout { + public PageColumn(Context context) { + this(context, null); + } + + public PageColumn(Context context, AttributeSet attrs) { + super(context, attrs); + setOrientation(VERTICAL); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + if (MeasureSpec.getMode(widthSpec) != MeasureSpec.UNSPECIFIED) { + float density = getResources().getDisplayMetrics().density; + int width = MeasureSpec.getSize(widthSpec); + int gutter = Math.max(Math.round(20 * density), (width - Math.round(720 * density)) / 2); + setPaddingRelative(gutter, getPaddingTop(), gutter, getPaddingBottom()); + } + super.onMeasure(widthSpec, heightSpec); + } +} diff --git a/app/src/main/java/com/deepseekharness/app/ui/RecoveryActivity.java b/app/src/main/java/com/deepseekharness/app/ui/RecoveryActivity.java index 3b11d2eaf..2e75f63b5 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/RecoveryActivity.java +++ b/app/src/main/java/com/deepseekharness/app/ui/RecoveryActivity.java @@ -48,7 +48,9 @@ protected void onCreate(Bundle saved) { R.color.text_secondary)); status = page.text("", 15, R.color.text); status.setTextIsSelectable(true); + status.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); intro.addView(status); + page.label(t("应急会话", "Emergency session")); LinearLayout actions = page.card(); actions.addView( page.text( @@ -61,6 +63,11 @@ protected void onCreate(Bundle saved) { temporaryKey.setSingleLine(true); temporaryKey.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); temporaryKey.setSaveEnabled(false); + temporaryKey.setContentDescription( + t("本次应急会话的临时 API Key", "Temporary API key for this emergency session")); + temporaryKey.setMinimumHeight(page.dp(56)); + temporaryKey.setPadding(page.dp(12), page.dp(12), page.dp(12), page.dp(12)); + temporaryKey.setBackgroundResource(R.drawable.bg_input); temporaryKey.setTextSize(15); temporaryKey.setHint(t("留空时尝试已有可读凭据", "Leave blank to try an existing readable key")); actions.addView(temporaryKey, new LinearLayout.LayoutParams(-1, -2)); @@ -125,8 +132,10 @@ protected void onCreate(Bundle saved) { page.content.addView(operation); page.button( actions, t("查看无需模型的修复选项", "Review repairs without a model"), false, this::repairOptions); + page.label(t("待确认的修复", "Repairs awaiting confirmation")); proposals = page.card(); renderPlans(new JSONArray()); + page.label(t("诊断与数据保护", "Diagnostics and data protection")); LinearLayout tools = page.card(); page.button( tools, diff --git a/app/src/main/java/com/deepseekharness/app/ui/RecoveryWebActivity.java b/app/src/main/java/com/deepseekharness/app/ui/RecoveryWebActivity.java index c07a2470b..e78b56261 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/RecoveryWebActivity.java +++ b/app/src/main/java/com/deepseekharness/app/ui/RecoveryWebActivity.java @@ -51,12 +51,13 @@ protected void onCreate(Bundle saved) { root.setOrientation(LinearLayout.VERTICAL); root.setBackgroundColor(getColor(R.color.surface)); setContentView(root); - LinearLayout toolbar = new LinearLayout(this); + LinearLayout toolbar = new ActionRow(this); toolbar.setOrientation(LinearLayout.HORIZONTAL); root.addView(toolbar, new LinearLayout.LayoutParams(-1, -2)); Button repairs = new androidx.appcompat.widget.AppCompatButton(this); repairs.setText(UiText.choose("应急 DSH · 修复提案", "Emergency DSH · Repairs")); repairs.setTextSize(14); + repairs.setMinHeight(Math.round(48 * getResources().getDisplayMetrics().density)); repairs.setAllCaps(false); repairs.setBackgroundResource(R.drawable.bg_btn); repairs.setTextColor(getColorStateList(R.color.button_text)); @@ -66,6 +67,7 @@ protected void onCreate(Bundle saved) { retry.setText(UiText.choose("重载", "Reload")); retry.setContentDescription(UiText.choose("重新加载应急页面", "Reload emergency page")); retry.setTextSize(14); + retry.setMinHeight(Math.round(48 * getResources().getDisplayMetrics().density)); retry.setAllCaps(false); retry.setBackgroundResource(R.drawable.bg_action_plain); retry.setTextColor(getColorStateList(R.color.button_text)); diff --git a/app/src/main/java/com/deepseekharness/app/ui/RetainedDataActivity.java b/app/src/main/java/com/deepseekharness/app/ui/RetainedDataActivity.java index 0d6eed5cb..159b01104 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/RetainedDataActivity.java +++ b/app/src/main/java/com/deepseekharness/app/ui/RetainedDataActivity.java @@ -57,7 +57,7 @@ protected void onCreate(Bundle saved) { model = new ViewModelProvider(this).get(Model.class); ScrollView scroll = new ScrollView(this); scroll.setBackgroundColor(getColor(R.color.surface)); - LinearLayout body = new LinearLayout(this); + LinearLayout body = new PageColumn(this); body.setOrientation(LinearLayout.VERTICAL); int p = (int) (20 * getResources().getDisplayMetrics().density); body.setPadding(p, p, p, p); @@ -99,6 +99,10 @@ protected void onCreate(Bundle saved) { if (model.control != null) model.control.cancel(); }); report = new TextView(this); + report.setTextColor(getColor(R.color.text)); + report.setTextSize(14); + report.setTextIsSelectable(true); + report.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE); body.addView(report); pageStatus = new TextView(this); pageStatus.setTextColor(getColor(R.color.text_secondary)); @@ -255,7 +259,6 @@ private void render(RetainedCatalogue.Page page) { } LinearLayout card = new LinearLayout(this); card.setOrientation(LinearLayout.VERTICAL); - card.setBackgroundResource(R.drawable.bg_polished_card); int pad = (int) (12 * getResources().getDisplayMetrics().density); card.setPadding(pad, pad, pad, pad); CheckBox selected = new CheckBox(this); @@ -275,7 +278,7 @@ private void render(RetainedCatalogue.Page page) { button(card, t("查看与操作", "Details and actions"), () -> details(entry)); LinearLayout.LayoutParams layout = new LinearLayout.LayoutParams(-1, -2); layout.bottomMargin = pad; - rows.addView(card, layout); + rows.addView(com.deepseekharness.app.ui.miuix.MiuixUi.card(this, card), layout); } if (model.page > 0) button( diff --git a/app/src/main/java/com/deepseekharness/app/ui/SettingsFragment.java b/app/src/main/java/com/deepseekharness/app/ui/SettingsFragment.java index 6e91b80b7..202d04619 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/SettingsFragment.java +++ b/app/src/main/java/com/deepseekharness/app/ui/SettingsFragment.java @@ -2,291 +2,103 @@ import android.content.Intent; import android.os.Bundle; -import android.util.TypedValue; -import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.TextView; -import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; - import com.deepseekharness.app.R; +import com.deepseekharness.app.core.ConfigStore; import com.deepseekharness.app.core.HarnessController; +import com.deepseekharness.app.ui.miuix.MiuixSettings; +import com.deepseekharness.app.util.UiLanguagePreference; +import com.deepseekharness.app.util.UiText; -import java.util.function.Supplier; - -/** - * 设置页:模块入口(安装/配置/数据与备份)+ 其他(更新/自检/重新解压/关于)。 - */ +/** Miuix 设置界面;业务入口、语言选择与省电服务沿用既有实现。 */ public class SettingsFragment extends Fragment { - - private static final TabOption[] TAB_OPTIONS = { - new TabOption("安装", "安装与修复运行环境", InstallFragment::new, R.drawable.ic_ui2_box), - new TabOption("配置", "接口、显示与运行", ConfigFragment::new, R.drawable.ic_settings), - new TabOption("数据与备份", "备份恢复 · 文件共享", WorkspaceFragment::new, R.drawable.ic_ui2_folder), - new TabOption( - "设备能力授权", "Root · Shizuku · ADB · 权限", DeviceGrantsFragment::new, R.drawable.ic_ui_shield), - }; - @Nullable @Override public View onCreateView( @NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.fragment_settings, container, false); - - LinearLayout tabs = v.findViewById(R.id.settings_tabs); - for (int i = 0; i < TAB_OPTIONS.length; i++) { - LinearLayout.LayoutParams spacing = new LinearLayout.LayoutParams(-1, -2); - if (i > 0) spacing.topMargin = 0; - tabs.addView(buildRow(i), spacing); - } - LinearLayout power = v.findViewById(R.id.settings_power); - power.setOrientation(LinearLayout.HORIZONTAL); - power.setGravity(Gravity.CENTER_VERTICAL); - power.setPadding(dp(13), dp(6), dp(13), dp(6)); - power.setMinimumHeight(dp(60)); - LinearLayout words = new LinearLayout(requireContext()); - words.setOrientation(LinearLayout.VERTICAL); - power.addView(words, new LinearLayout.LayoutParams(0, -2, 1)); - TextView heading = new TextView(requireContext()); - heading.setText(com.deepseekharness.app.util.UiText.text("省电模式")); - heading.setTextSize(13); - heading.setTextColor(requireContext().getColor(R.color.text)); - heading.setIncludeFontPadding(false); - words.addView(heading); - TextView powerHint = new TextView(requireContext()); - powerHint.setTextSize(11); - powerHint.setTextColor(requireContext().getColor(R.color.text_muted)); - powerHint.setPadding(0, dp(4), dp(8), 0); - powerHint.setIncludeFontPadding(false); - words.addView(powerHint); - com.google.android.material.materialswitch.MaterialSwitch eco = - new com.google.android.material.materialswitch.MaterialSwitch(requireContext()); - eco.setMinHeight(dp(48)); - eco.setContentDescription(com.deepseekharness.app.util.UiText.text("省电模式")); - com.deepseekharness.app.core.ConfigStore config = - HarnessController.get(requireContext()).config(); - eco.setChecked(config.isEcoMode()); - power.addView(eco); - java.util.function.Consumer describe = - enabled -> - powerHint.setText( - enabled - ? com.deepseekharness.app.util.UiText.text("熄屏空闲 1 分钟后减少保活;有任务时继续运行。") - : com.deepseekharness.app.util.UiText.text("持续保持运行,适合长时间任务。")); - describe.accept(config.isEcoMode()); - eco.setOnCheckedChangeListener( - (button, checked) -> { + ConfigStore config = HarnessController.get(requireContext()).config(); + String current = + "en".equals(config.getUiLanguage()) + ? "English" + : UiText.choose("简体中文", "Simplified Chinese"); + String summary = + UiLanguagePreference.followsSystem(config.getUiLanguagePreference()) + ? UiText.format("跟随系统 · %s", current) + : current; + return new MiuixSettings( + requireContext(), + com.deepseekharness.app.BuildConfig.VERSION_NAME, + summary, + config.isEcoMode(), + this::open, + checked -> { config.setEcoMode(checked); com.deepseekharness.app.HarnessService.refreshPowerMode(requireContext()); - describe.accept(checked); }); + } - String version = "unknown"; - try { - version = - requireContext() - .getPackageManager() - .getPackageInfo(requireContext().getPackageName(), 0) - .versionName; - } catch (Exception ignored) { + void open(int id) { + if (!isAdded() || getParentFragmentManager().isStateSaved()) return; + if (id == R.id.settings_appearance) { + selectLanguage(); + return; } - TextView ver = v.findViewById(R.id.settings_ver); - ver.setText(com.deepseekharness.app.util.UiText.format("DSHA v%s · MIT 许可", version)); - TextView updateSub = v.findViewById(R.id.settings_update_sub); - updateSub.setText(com.deepseekharness.app.util.UiText.text("稳定版与预览版更新")); - - v.findViewById(R.id.settings_about) - .setOnClickListener( - x -> - UiMotion.page(requireContext(), getParentFragmentManager().beginTransaction()) - .replace(R.id.fragment_container, new AboutFragment()) - .addToBackStack("settings") - .commit()); - v.findViewById(R.id.settings_update).setOnClickListener(x -> checkUpdate()); - v.findViewById(R.id.settings_selftest).setOnClickListener(x -> runSelftest()); + if (id == R.id.settings_update || id == R.id.settings_selftest) { + startActivity( + new Intent( + requireContext(), + id == R.id.settings_update ? UpdateActivity.class : DiagnosticActivity.class)); + return; + } + Fragment page; + if (id == R.id.settings_install) page = new InstallFragment(); + else if (id == R.id.settings_config) page = new ConfigFragment(); + else if (id == R.id.settings_data) page = new WorkspaceFragment(); + else if (id == R.id.settings_device) page = new DeviceGrantsFragment(); + else if (id == R.id.settings_overlay) page = new OverlayFragment(); + else if (id == R.id.settings_about) page = new AboutFragment(); + else return; + UiMotion.page(requireContext(), getParentFragmentManager().beginTransaction()) + .replace(R.id.fragment_container, page) + .addToBackStack("settings") + .commit(); + } - // 语言入口:标题在中文界面下也带英文「Language」(只写「语言 · 简体中文」时, - // 非中文用户根本认不出这是语言开关);摘要直接显示当前生效语言。 - // 放在「常用设置」分组最前面,不用滚动就能看到。 + private void selectLanguage() { + ConfigStore config = HarnessController.get(requireContext()).config(); String preference = config.getUiLanguagePreference(); - boolean followSystem = - com.deepseekharness.app.util.UiLanguagePreference.followsSystem(preference); - String effective = config.getUiLanguage(); - String[] optionValues = { - com.deepseekharness.app.util.UiLanguagePreference.SYSTEM, - com.deepseekharness.app.util.UiLanguagePreference.ZH, - com.deepseekharness.app.util.UiLanguagePreference.EN + String[] values = { + UiLanguagePreference.SYSTEM, UiLanguagePreference.ZH, UiLanguagePreference.EN }; - String[] optionLabels = { - com.deepseekharness.app.util.UiText.choose("跟随系统", "Follow system"), - com.deepseekharness.app.util.UiText.choose("简体中文", "Simplified Chinese"), - "English" + String[] labels = { + UiText.choose("跟随系统", "Follow system"), UiText.choose("简体中文", "Simplified Chinese"), "English" }; - int checked = followSystem ? 0 : ("en".equals(preference) ? 2 : 1); - // 摘要始终显示当前**生效**语言的名字:跟随系统时补上来由,用户一眼能看出实际结果。 - String currentLabel = - "en".equals(effective) - ? com.deepseekharness.app.util.UiText.choose("English", "English") - : com.deepseekharness.app.util.UiText.choose("简体中文", "Simplified Chinese"); - String summary = - followSystem - ? com.deepseekharness.app.util.UiText.format("跟随系统 · %s", currentLabel) - : currentLabel; - LinearLayout appearance = v.findViewById(R.id.settings_appearance); - TextView languageSummary = v.findViewById(R.id.settings_language); - languageSummary.setText(summary); - View.OnClickListener openLanguageDialog = - x -> - new com.deepseekharness.app.ui.DshaDialogBuilder(requireContext()) - .setTitle( - com.deepseekharness.app.util.UiText.choose( - "界面语言 / Interface language", "Interface language")) - .setSingleChoiceItems( - optionLabels, - checked, - (dialog, which) -> { - String selected = optionValues[which]; - if (dialog instanceof android.app.Dialog) - ((android.app.Dialog) dialog) - .setOnDismissListener( - ignored -> - new android.os.Handler(android.os.Looper.getMainLooper()) - .post( - () -> - LanguageController.select( - requireContext(), selected))); - dialog.dismiss(); - }) - .setNegativeButton(com.deepseekharness.app.util.UiText.choose("取消", "Cancel"), null) - .show(); - appearance.setOnClickListener(openLanguageDialog); - languageSummary.setOnClickListener(openLanguageDialog); - v.findViewById(R.id.settings_overlay) - .setOnClickListener( - x -> - UiMotion.page(requireContext(), getParentFragmentManager().beginTransaction()) - .replace(R.id.fragment_container, new OverlayFragment()) - .addToBackStack("settings") - .commit()); - return v; - } - - private void runSelftest() { - startActivity(new Intent(requireContext(), DiagnosticActivity.class)); - } - - private void checkUpdate() { - startActivity(new Intent(requireContext(), UpdateActivity.class)); - } - - private LinearLayout buildRow(final int index) { - TabOption opt = TAB_OPTIONS[index]; - LinearLayout row = new LinearLayout(requireContext()); - row.setOrientation(LinearLayout.HORIZONTAL); - row.setGravity(Gravity.CENTER_VERTICAL); - row.setPadding(dp(13), dp(11), dp(13), dp(11)); - row.setMinimumHeight(dp(64)); - row.setFocusable(true); - // 用主题的 selectableItemBackground(Material ripple),不用 Holo 的黄色 list_selector - TypedValue tv = new TypedValue(); - requireContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, tv, true); - row.setBackgroundResource( - index == TAB_OPTIONS.length - 1 ? R.drawable.bg_action_plain : R.drawable.bg_ui2_row); - - LinearLayout body = new LinearLayout(requireContext()); - body.setOrientation(LinearLayout.VERTICAL); - LinearLayout.LayoutParams bodyParams = - new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f); - bodyParams.leftMargin = dp(12); - bodyParams.rightMargin = dp(8); - body.setLayoutParams(bodyParams); - - TextView title = new TextView(requireContext()); - title.setText( - index == 0 - ? getString(R.string.ui2_install_environment) - : index == 1 - ? getString(R.string.ui2_runtime_config) - : com.deepseekharness.app.util.UiText.text(opt.title)); - title.setTextSize(13); - title.setTextColor(requireContext().getColor(R.color.text)); - title.setTypeface(title.getTypeface(), android.graphics.Typeface.BOLD); - - TextView sub = new TextView(requireContext()); - sub.setText(com.deepseekharness.app.util.UiText.text(opt.sub)); - sub.setTextSize(11); - sub.setTextColor(requireContext().getColor(R.color.text_muted)); - LinearLayout.LayoutParams slp = - new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - slp.topMargin = dp(2); - sub.setLayoutParams(slp); - - body.addView(title); - body.addView(sub); - - TextView chev = new TextView(requireContext()); - chev.setText(com.deepseekharness.app.util.UiText.text("›")); - chev.setTextSize(18); - chev.setTextColor(requireContext().getColor(R.color.text_muted)); - - android.widget.ImageView icon = new android.widget.ImageView(requireContext()); - icon.setImageResource(opt.icon); - icon.setImageTintList( - android.content.res.ColorStateList.valueOf(requireContext().getColor(R.color.primary))); - icon.setBackgroundResource(R.drawable.bg_logo); - icon.setPadding(dp(7), dp(7), dp(7), dp(7)); - icon.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - row.addView(icon, new LinearLayout.LayoutParams(dp(32), dp(32))); - row.addView(body); - row.addView(chev); - row.setOnClickListener( - v -> { - if (opt.activity != null) { - startActivity(new Intent(requireContext(), opt.activity)); - } else { - UiMotion.page(requireContext(), getParentFragmentManager().beginTransaction()) - .replace(R.id.fragment_container, opt.factory.get()) - .addToBackStack("settings") - .commit(); - } - }); - return row; - } - - private int dp(int v) { - return Math.round(v * getResources().getDisplayMetrics().density); - } - - private static final class TabOption { - final String title; - final String sub; - final Supplier factory; - final Class activity; - final int icon; - - TabOption(String title, String sub, Supplier factory, int icon) { - this.title = title; - this.sub = sub; - this.factory = factory; - this.activity = null; - this.icon = icon; - } - - TabOption(String title, String sub, Class activity, int icon) { - this.title = title; - this.sub = sub; - this.factory = null; - this.activity = activity; - this.icon = icon; - } + int checked = + UiLanguagePreference.followsSystem(preference) ? 0 : ("en".equals(preference) ? 2 : 1); + // 先关闭弹窗再让 AppCompat 重建界面,后台终端和 Web 不参与语言切换。 + android.content.Context context = requireContext(); + new DshaDialogBuilder(context) + .setTitle(UiText.choose("界面语言 / Interface language", "Interface language")) + .setSingleChoiceItems( + labels, + checked, + (dialog, which) -> { + if (dialog instanceof android.app.Dialog) + ((android.app.Dialog) dialog) + .setOnDismissListener( + ignored -> + new android.os.Handler(android.os.Looper.getMainLooper()) + .post(() -> LanguageController.select(context, values[which]))); + dialog.dismiss(); + }) + .setNegativeButton(UiText.choose("取消", "Cancel"), null) + .show(); } } diff --git a/app/src/main/java/com/deepseekharness/app/ui/SkillsActivity.java b/app/src/main/java/com/deepseekharness/app/ui/SkillsActivity.java index a4b61e6fd..bd4c45129 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/SkillsActivity.java +++ b/app/src/main/java/com/deepseekharness/app/ui/SkillsActivity.java @@ -91,7 +91,7 @@ protected void onCreate(Bundle saved) { ScrollView scroll = new ScrollView(this); scroll.setFillViewport(true); scroll.setBackgroundColor(getColor(R.color.surface)); - LinearLayout body = new LinearLayout(this); + LinearLayout body = new PageColumn(this); body.setOrientation(LinearLayout.VERTICAL); int padding = dp(20); body.setPadding(padding, padding, padding, padding); @@ -121,6 +121,7 @@ protected void onCreate(Bundle saved) { refreshButton = button(body, t("刷新技能", "Refresh skills"), () -> start(null)); cancelButton = button(body, t("取消读取", "Cancel reading"), model::cancel); status = text(body, "", 14, R.color.text); + status.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE); status.setTextIsSelectable(true); listStatus = text(body, "", 14, R.color.text_secondary); rows = new LinearLayout(this); @@ -186,11 +187,18 @@ private void render(SkillImports.Catalogue catalogue) { 14, R.color.text_secondary); for (SkillImports.Entry entry : catalogue.entries) { - TextView name = text(rows, UiText.raw(entry.document.name), 18, R.color.text); + LinearLayout card = new LinearLayout(this); + card.setOrientation(LinearLayout.VERTICAL); + card.setPadding(dp(16), dp(8), dp(16), dp(16)); + LinearLayout.LayoutParams spacing = new LinearLayout.LayoutParams(-1, -2); + spacing.bottomMargin = dp(12); + rows.addView(com.deepseekharness.app.ui.miuix.MiuixUi.card(this, card), spacing); + TextView name = text(card, UiText.raw(entry.document.name), 18, R.color.text); name.setTypeface(null, android.graphics.Typeface.BOLD); - text(rows, UiText.raw(entry.document.description), 14, R.color.text_secondary); - text(rows, UiText.raw(entry.guestPath()), 12, R.color.text_secondary); - button(rows, t("查看 SKILL.md", "View SKILL.md"), () -> show(entry)); + text(card, UiText.raw(entry.document.description), 14, R.color.text_secondary); + text(card, UiText.raw(entry.guestPath()), 13, R.color.text_secondary) + .setTextIsSelectable(true); + button(card, t("查看 SKILL.md", "View SKILL.md"), () -> show(entry)); } } diff --git a/app/src/main/java/com/deepseekharness/app/ui/StartupRecoveryLayout.java b/app/src/main/java/com/deepseekharness/app/ui/StartupRecoveryLayout.java index f09b935d0..5ab2d17f0 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/StartupRecoveryLayout.java +++ b/app/src/main/java/com/deepseekharness/app/ui/StartupRecoveryLayout.java @@ -23,7 +23,7 @@ private static String t(String zh, String en) { root = new ScrollView(context); root.setFillViewport(true); root.setBackgroundColor(context.getColor(R.color.surface)); - content = column(); + content = new PageColumn(context); content.setPadding(dp(20), dp(12), dp(20), dp(20)); root.addView(content); View backRow = action(content, t("返回", "Back"), R.drawable.ic_recovery_right, back, false); @@ -75,7 +75,7 @@ private static String t(String zh, String en) { "Keeps three healthy-start and three pre-repair snapshots. Saves configuration only, without sessions, workspaces, or dependency directories."), R.drawable.ic_recovery_database); plugins = card(t("插件卸载", "Remove plugins"), "", R.drawable.ic_recovery_document); - pluginCard = (View) plugins.getParent(); + pluginCard = (View) plugins.getParent().getParent(); pluginCard.setVisibility(View.GONE); } @@ -101,10 +101,9 @@ private ImageView icon(int resource, int color) { private LinearLayout card(String title, String subtitle, int icon) { LinearLayout card = column(); card.setPadding(dp(12), dp(12), dp(12), dp(12)); - card.setBackgroundResource(R.drawable.bg_card); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(-1, -2); params.topMargin = dp(12); - content.addView(card, params); + content.addView(com.deepseekharness.app.ui.miuix.MiuixUi.card(context, card), params); LinearLayout heading = new LinearLayout(context); heading.setGravity(Gravity.TOP); card.addView(heading); diff --git a/app/src/main/java/com/deepseekharness/app/ui/StorageActivity.java b/app/src/main/java/com/deepseekharness/app/ui/StorageActivity.java index d61168839..8bd9ee79c 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/StorageActivity.java +++ b/app/src/main/java/com/deepseekharness/app/ui/StorageActivity.java @@ -29,6 +29,7 @@ protected void onCreate(Bundle saved) { "完整 Linux 环境约占 1 GB。历史环境和回退副本另行保留;它们不等于聊天数据。以下为文件大小估算。", "The full Linux environment uses about 1 GB. Older environments and rollback copies are retained separately from chats. Sizes below are estimates.")); UiNavigation.addHeader(this, page.root, t("存储与文件", "Storage and files"), this::finish); + page.label(t("工作目录与访问", "Workspace and access")); var files = page.card(); page.kv( files, @@ -46,8 +47,10 @@ protected void onCreate(Bundle saved) { t( "在文件管理器中添加本地存储,选择 DocumentsProvider → DSHA。找不到 DSHA 时,先打开本 App 后重试。", "Add local storage in your file manager and select DocumentsProvider → DSHA. Open this app first if DSHA is missing."))); + page.label(t("空间占用", "Storage usage")); rows = page.card(); state = page.text("", 12, R.color.text_secondary); + state.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE); page.content.addView(state); clean = page.button( diff --git a/app/src/main/java/com/deepseekharness/app/ui/StorageHelpActivity.java b/app/src/main/java/com/deepseekharness/app/ui/StorageHelpActivity.java index 6fad3a117..3e22542ad 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/StorageHelpActivity.java +++ b/app/src/main/java/com/deepseekharness/app/ui/StorageHelpActivity.java @@ -28,7 +28,7 @@ protected void onCreate(Bundle saved) { this, UiText.text("存储、备份与重建说明"), UiText.text("先看保留范围,再选择操作。打开本页不会修改文件或停止 DSH。")); UiNavigation.addHeader(this, page.root, UiText.text("存储、备份与重建说明"), this::finish); page.content.removeViewAt(0); - page.root.removeView(page.footer); + page.footer.setVisibility(android.view.View.GONE); LinearLayout start = section("只想腾空间,从这里开始"); paragraph( diff --git a/app/src/main/java/com/deepseekharness/app/ui/TerminalTabBar.java b/app/src/main/java/com/deepseekharness/app/ui/TerminalTabBar.java index 7c1637377..34f4b64c6 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/TerminalTabBar.java +++ b/app/src/main/java/com/deepseekharness/app/ui/TerminalTabBar.java @@ -33,7 +33,14 @@ static void render(View root, TerminalTabs.ReadOnly tabs, Actions actions current != null && current.id == tab.id ? R.color.primary : R.color.text_secondary)); - name.setTextSize(13); + name.setTextSize(14); + name.setSelected(current != null && current.id == tab.id); + androidx.core.view.ViewCompat.setStateDescription( + name, + current != null && current.id == tab.id + ? com.deepseekharness.app.util.UiText.choose("当前终端", "Current terminal") + : null); + name.setBackgroundResource(R.drawable.bg_action_plain); name.setIncludeFontPadding(false); name.setGravity(Gravity.CENTER); name.setSingleLine(); @@ -50,12 +57,13 @@ static void render(View root, TerminalTabs.ReadOnly tabs, Actions actions close.setGravity(Gravity.CENTER); close.setTextColor(root.getContext().getColor(R.color.text_secondary)); close.setMinHeight(dp(root, 48)); + close.setBackgroundResource(R.drawable.bg_action_plain); close.setContentDescription( com.deepseekharness.app.util.UiText.format("关闭终端 %s", tab.number)); close.setEnabled(!tab.isClosing()); close.setAlpha(tab.isClosing() ? 0.4f : 1f); close.setOnClickListener(v -> actions.close(tab.id)); - chip.addView(close, new LinearLayout.LayoutParams(dp(root, 48), dp(root, 48))); + chip.addView(close, new LinearLayout.LayoutParams(dp(root, 48), -2)); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(-2, -2); lp.setMarginEnd(dp(root, 6)); row.addView(chip, lp); diff --git a/app/src/main/java/com/deepseekharness/app/ui/UiNavigation.java b/app/src/main/java/com/deepseekharness/app/ui/UiNavigation.java index e0ed1c2c6..fa9a88000 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/UiNavigation.java +++ b/app/src/main/java/com/deepseekharness/app/ui/UiNavigation.java @@ -33,7 +33,8 @@ static void addHeader(Context context, LinearLayout root, String title, Runnable label.setTextSize(18); label.setTextColor(context.getColor(R.color.text)); label.setTypeface(null, android.graphics.Typeface.BOLD); - row.addView(label); + androidx.core.view.ViewCompat.setAccessibilityHeading(label, true); + row.addView(label, new LinearLayout.LayoutParams(0, -2, 1)); root.addView(row, 0); } diff --git a/app/src/main/java/com/deepseekharness/app/ui/WorkspaceFilesActivity.java b/app/src/main/java/com/deepseekharness/app/ui/WorkspaceFilesActivity.java index e28f37e39..1f49dc5cb 100644 --- a/app/src/main/java/com/deepseekharness/app/ui/WorkspaceFilesActivity.java +++ b/app/src/main/java/com/deepseekharness/app/ui/WorkspaceFilesActivity.java @@ -183,14 +183,15 @@ protected void onCreate(Bundle saved) { body.setPadding(dp(12), dp(6), dp(12), dp(12)); setContentView(body); UiNavigation.addHeader(this, body, UiText.choose("作品文件导出", "Export work files"), this::finish); + LinearLayout details = new PageColumn(this); TextView note = text( UiText.choose( "选择工作目录里的作品文件,保存到手机或分享给其他应用。无需 Root;这是普通文件导出,完整数据备份另有入口。", "Choose a file from your workspace to save on the device or share with another app. No root is needed. Full data backups have a separate entry."), 13); - body.addView(note); - LinearLayout navigation = new LinearLayout(this); + details.addView(note); + LinearLayout navigation = new ActionRow(this); workspace = button(UiText.choose("工作目录", "Workspace"), () -> load(workspaceId, 0, true)); root = button("DSHA", () -> load("root")); up = button(UiText.choose("上一级", "Up"), this::parent); @@ -200,14 +201,14 @@ protected void onCreate(Bundle saved) { params.setMargins(dp(2), dp(6), dp(2), dp(6)); navigation.addView(button, params); } - body.addView(navigation); + details.addView(navigation); location = text("", 13); location.setTextIsSelectable(true); - body.addView(location); + details.addView(location); status = text("", 13); status.setTextIsSelectable(true); - status.setMaxLines(5); - body.addView(status); + status.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE); + details.addView(status); cancel = button( UiText.choose("取消导出", "Cancel export"), @@ -215,11 +216,12 @@ protected void onCreate(Bundle saved) { BackupControl control = model.control; if (control != null) control.cancel(); }); - body.addView(cancel); + details.addView(cancel); files = new ListView(this); + files.addHeaderView(details, null, false); files.setDividerHeight(dp(1)); body.addView(files, new LinearLayout.LayoutParams(-1, 0, 1)); - pagination = new LinearLayout(this); + pagination = new ActionRow(this); previousPage = button( UiText.choose("上一页", "Previous page"), @@ -234,7 +236,7 @@ protected void onCreate(Bundle saved) { files.setOnItemClickListener( (parent, view, position, id) -> { if (model.working.get() || model.choosingDestination) return; - Entry entry = (Entry) files.getAdapter().getItem(position); + if (!(files.getItemAtPosition(position) instanceof Entry entry)) return; if (entry.directory) load(entry.id); else actions(entry); }); diff --git a/app/src/main/java/com/deepseekharness/app/ui/miuix/MiuixLaunch.kt b/app/src/main/java/com/deepseekharness/app/ui/miuix/MiuixLaunch.kt new file mode 100644 index 000000000..0d7fecdc2 --- /dev/null +++ b/app/src/main/java/com/deepseekharness/app/ui/miuix/MiuixLaunch.kt @@ -0,0 +1,169 @@ +package com.deepseekharness.app.ui.miuix + +import android.content.Context +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import com.deepseekharness.app.R +import com.deepseekharness.app.ui.LogScrollView +import top.yukonga.miuix.kmp.basic.Button +import top.yukonga.miuix.kmp.basic.ButtonDefaults +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator +import top.yukonga.miuix.kmp.basic.SmallTitle +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.theme.MiuixTheme + +/** 启动页的可观察显示状态;由 LaunchFragment 更新,不启动或停止任何后台任务。 */ +class MiuixLaunch(context: Context) : MiuixView(context) { + open class Label(private val context: Context, value: String = "") { + var text by mutableStateOf(value) + var visibility by mutableIntStateOf(View.VISIBLE) + fun setText(resource: Int) { text = context.getString(resource) } + } + + class Action(private val owner: View, resource: Int) : Label(owner.context, owner.context.getString(resource)) { + var enabled by mutableStateOf(true) + var contentDescription by mutableStateOf("") + private var listener: View.OnClickListener? = null + fun setOnClickListener(value: View.OnClickListener?) { listener = value } + fun click() { if (enabled && visibility == View.VISIBLE) listener?.onClick(owner) } + } + + @JvmField val status = Label(context) + @JvmField val runState = Label(context, context.getString(R.string.ui2_stopped)) + @JvmField val subtitle = Label(context, context.getString(R.string.ui2_launch_hint)) + @JvmField val port = Label(context, "—") + @JvmField val environment = Label(context, "—") + @JvmField val busy = Label(context).apply { visibility = View.GONE } + @JvmField val start = Action(this, R.string.ui2_start) + @JvmField val restart = Action(this, R.string.ui2_restart) + @JvmField val stop = Action(this, R.string.ui2_stop) + @JvmField val safe = Action(this, R.string.ui_m0007) + @JvmField val recovery = Action(this, R.string.ui_m0011) + @JvmField val models = Action(this, R.string.ui2_models) + @JvmField val address = Action(this, R.string.ui2_address) + @JvmField val downloadLogs = Action(this, R.string.ui_m0046) + + // 日志保留原生可选中文本和“仅在底部时跟随”的滚动容器。 + @JvmField val logScroll = LogScrollView(context, null).apply { + id = R.id.launch_log_scroll + setBackgroundResource(R.drawable.bg_ui2_console) + isFillViewport = true + } + @JvmField val logText = TextView(context).apply { + id = R.id.launch_log + textSize = 12f + setTextColor(context.getColor(R.color.ui2_log_text)) + typeface = android.graphics.Typeface.MONOSPACE + setTextIsSelectable(true) + val inset = (12 * resources.displayMetrics.density).toInt() + setPadding(inset, inset, inset, inset) + } + @JvmField val lanAddress = TextView(context).apply { + id = R.id.lan_addr + textSize = 12f + setTextColor(context.getColor(R.color.primary)) + setTextIsSelectable(true) + visibility = View.GONE + } + + init { + id = R.id.launch_panel + logScroll.addView(LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + addView(lanAddress) + addView(logText) + }) + } + + @Composable override fun Content() { + DshaTheme { + Column(Modifier.fillMaxSize().background(MiuixTheme.colorScheme.surface) + .verticalScroll(rememberScrollState()).padding(20.dp)) { + Card(Modifier.fillMaxWidth(), insideMargin = PaddingValues(20.dp)) { + Text(runState.text, fontSize = 24.sp, fontWeight = FontWeight.Bold, + modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite }) + Text(subtitle.text, modifier = Modifier.padding(top = 8.dp), + color = MiuixTheme.colorScheme.onSurfaceSecondary) + if (status.visibility == View.VISIBLE && status.text.isNotEmpty()) { + Text(status.text, fontSize = 13.sp, modifier = Modifier.padding(top = 12.dp), + color = MiuixTheme.colorScheme.onSurfaceSecondary) + } + if (busy.visibility == View.VISIBLE) { + InfiniteProgressIndicator(Modifier.padding(top = 12.dp).size(24.dp)) + } + ActionButton(start, Modifier.fillMaxWidth().padding(top = 20.dp), primary = true) + ActionRow(restart, stop) + } + Row(Modifier.fillMaxWidth().padding(top = 16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Card(Modifier.weight(1f), insideMargin = PaddingValues(16.dp)) { + Text(port.text, fontSize = 22.sp, fontWeight = FontWeight.Medium) + Text(stringResource(R.string.ui2_port), fontSize = 13.sp, + color = MiuixTheme.colorScheme.onSurfaceSecondary) + } + Card(Modifier.weight(1f), insideMargin = PaddingValues(16.dp)) { + Text(environment.text, fontSize = 22.sp, fontWeight = FontWeight.Medium) + Text(stringResource(R.string.ui2_environment), fontSize = 13.sp, + color = MiuixTheme.colorScheme.onSurfaceSecondary) + } + } + SmallTitle(stringResource(R.string.ui2_shortcuts)) + Card(Modifier.fillMaxWidth()) { + Entry(models.text, stringResource(R.string.ui2_models_sub), R.drawable.ic_settings, models::click) + Entry(address.text, stringResource(R.string.ui2_address_sub), R.drawable.ic_ui_link, address::click) + } + Row(Modifier.fillMaxWidth().padding(top = 20.dp), horizontalArrangement = Arrangement.SpaceBetween) { + Text(stringResource(R.string.ui2_logs), modifier = Modifier.weight(1f).padding(top = 12.dp), + color = MiuixTheme.colorScheme.onSurfaceSecondary) + ActionButton(downloadLogs) + } + AndroidView(factory = { logScroll }, modifier = Modifier.fillMaxWidth().height(180.dp).padding(top = 8.dp)) + ActionRow(safe, recovery) + } + } + } + + @Composable private fun ActionRow(left: Action, right: Action) { + // 大字体时改为上下排列,避免两列按钮文字挤压和操作目标过窄。 + val scale = androidx.compose.ui.platform.LocalDensity.current.fontScale + if (scale > 1.3f) { + ActionButton(left, Modifier.fillMaxWidth().padding(top = 10.dp)) + ActionButton(right, Modifier.fillMaxWidth().padding(top = 10.dp)) + } else { + Row(Modifier.fillMaxWidth().padding(top = 10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp)) { + ActionButton(left, Modifier.weight(1f)) + ActionButton(right, Modifier.weight(1f)) + } + } + } + + @Composable private fun ActionButton(action: Action, modifier: Modifier = Modifier, primary: Boolean = false) { + if (action.visibility != View.VISIBLE) return + Button( + onClick = action::click, + enabled = action.enabled, + minHeight = 48.dp, + modifier = modifier.semantics { + if (action.contentDescription.isNotEmpty()) contentDescription = action.contentDescription + }, + colors = if (primary) ButtonDefaults.buttonColorsPrimary() else ButtonDefaults.buttonColors(), + ) { Text(action.text, textAlign = TextAlign.Center) } + } +} diff --git a/app/src/main/java/com/deepseekharness/app/ui/miuix/MiuixNavigation.kt b/app/src/main/java/com/deepseekharness/app/ui/miuix/MiuixNavigation.kt new file mode 100644 index 000000000..8926862b6 --- /dev/null +++ b/app/src/main/java/com/deepseekharness/app/ui/miuix/MiuixNavigation.kt @@ -0,0 +1,75 @@ +package com.deepseekharness.app.ui.miuix + +import android.content.Context +import android.os.Bundle +import android.os.Parcelable +import android.util.AttributeSet +import androidx.compose.runtime.* +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.layout.heightIn +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import com.deepseekharness.app.R +import java.util.function.IntPredicate +import top.yukonga.miuix.kmp.basic.NavigationBar +import top.yukonga.miuix.kmp.basic.NavigationBarItem + +/** 只负责选中态与绘制;页面切换仍同步交给 MainActivity 的 FragmentManager。 */ +class MiuixNavigation @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + MiuixView(context, attrs) { + private var selected by mutableIntStateOf(R.id.nav_launch) + private var listener: IntPredicate? = null + + fun getSelectedItemId(): Int = selected + + fun setSelectedItemId(id: Int) { + if (id !in ids) return + // 先回调再提交选中态,保留原导航对重复点击、外部通知和拒绝导航的语义。 + if (listener?.test(id) != false) selected = id + } + + fun setOnItemSelectedListener(value: IntPredicate) { listener = value } + + override fun onSaveInstanceState(): Parcelable = Bundle().apply { + putParcelable("parent", super.onSaveInstanceState()) + putInt("selected", selected) + } + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state !is Bundle) { super.onRestoreInstanceState(state); return } + super.onRestoreInstanceState(state.getParcelable("parent")) + selected = state.getInt("selected", R.id.nav_launch).takeIf { it in ids } ?: R.id.nav_launch + } + + @Composable override fun Content() { + DshaTheme { + // ModernAndroidUi 已负责系统栏和键盘避让,组件不能重复加底部留白。 + NavigationBar(modifier = Modifier.selectableGroup(), defaultWindowInsetsPadding = false, showDivider = false) { + ids.indices.forEach { index -> + NavigationBarItem( + modifier = Modifier + .heightIn(min = (64 * LocalDensity.current.fontScale.coerceAtLeast(1f)).dp) + .selectable(selected == ids[index], role = Role.Tab, + onClick = { setSelectedItemId(ids[index]) }), + selected = selected == ids[index], + onClick = { setSelectedItemId(ids[index]) }, + icon = ImageVector.vectorResource(icons[index]), + label = stringResource(labels[index]), + ) + } + } + } + } + + private companion object { + val ids = intArrayOf(R.id.nav_launch, R.id.nav_plugins, R.id.nav_settings, R.id.nav_terminal) + val icons = intArrayOf(R.drawable.ic_launch, R.drawable.ic_plugins, R.drawable.ic_settings, R.drawable.ic_terminal) + val labels = intArrayOf(R.string.ui_m0005, R.string.ui_m0016, R.string.ui_m0023, R.string.ui_m0021) + } +} diff --git a/app/src/main/java/com/deepseekharness/app/ui/miuix/MiuixPanel.kt b/app/src/main/java/com/deepseekharness/app/ui/miuix/MiuixPanel.kt new file mode 100644 index 000000000..8429d0fc5 --- /dev/null +++ b/app/src/main/java/com/deepseekharness/app/ui/miuix/MiuixPanel.kt @@ -0,0 +1,38 @@ +package com.deepseekharness.app.ui.miuix + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import top.yukonga.miuix.kmp.basic.Card + +/** XML 表单的 Miuix 卡片。原控件仍在 View 树中,查找、输入恢复及监听器立即可用。 */ +class MiuixPanel @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + FrameLayout(context, attrs) { + private val surface = object : MiuixView(context) { + @Composable override fun Content() { + DshaTheme { Card(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) } } + } + } + + init { + surface.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + addView(surface, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + // 背景不参与 wrap_content 高度计算,否则滚动页面首卡会撑满整屏。 + val params = surface.layoutParams + params.width = 0 + params.height = 0 + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + params.width = LayoutParams.MATCH_PARENT + params.height = LayoutParams.MATCH_PARENT + surface.measure(MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)) + } +} diff --git a/app/src/main/java/com/deepseekharness/app/ui/miuix/MiuixSettings.kt b/app/src/main/java/com/deepseekharness/app/ui/miuix/MiuixSettings.kt new file mode 100644 index 000000000..ab2dd5c92 --- /dev/null +++ b/app/src/main/java/com/deepseekharness/app/ui/miuix/MiuixSettings.kt @@ -0,0 +1,87 @@ +package com.deepseekharness.app.ui.miuix + +import android.content.Context +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.deepseekharness.app.R +import com.deepseekharness.app.util.UiText +import java.util.function.Consumer +import java.util.function.IntConsumer +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.SmallTitle +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.extra.SuperSwitch +import top.yukonga.miuix.kmp.theme.MiuixTheme + +/** 设置页面只呈现选项;导航、持久化和服务更新由原 Java 页面处理。 */ +class MiuixSettings( + context: Context, + private val version: String, + val language: String, + eco: Boolean, + private val open: IntConsumer, + private val changeEco: Consumer, +) : MiuixView(context) { + init { id = R.id.settings_page } + + private var ecoEnabled by mutableStateOf(eco) + + @Composable override fun Content() { + DshaTheme { + Column(Modifier.fillMaxSize().background(MiuixTheme.colorScheme.surface) + .verticalScroll(rememberScrollState()).padding(20.dp)) { + Text(stringResource(R.string.ui2_settings), fontSize = 30.sp, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.ui2_settings_hint), + modifier = Modifier.padding(top = 8.dp, bottom = 12.dp), + color = MiuixTheme.colorScheme.onSurfaceSecondary) + SmallTitle(stringResource(R.string.ui2_workspace)) + Card(Modifier.fillMaxWidth()) { + Entry(stringResource(R.string.ui2_install_environment), UiText.text("安装与修复运行环境"), + R.drawable.ic_ui2_box) { open.accept(R.id.settings_install) } + Entry(stringResource(R.string.ui2_runtime_config), UiText.text("接口、显示与运行"), + R.drawable.ic_settings) { open.accept(R.id.settings_config) } + Entry(UiText.text("数据与备份"), UiText.text("备份恢复 · 文件共享"), + R.drawable.ic_ui2_folder) { open.accept(R.id.settings_data) } + Entry(UiText.text("设备能力授权"), UiText.text("Root · Shizuku · ADB · 权限"), + R.drawable.ic_ui_shield) { open.accept(R.id.settings_device) } + } + SmallTitle(stringResource(R.string.ui2_experience)) + Card(Modifier.fillMaxWidth()) { + SuperSwitch( + checked = ecoEnabled, + onCheckedChange = { value -> changeEco.accept(value); ecoEnabled = value }, + title = UiText.text("省电模式"), + summary = UiText.text(if (ecoEnabled) "熄屏空闲 1 分钟后减少保活;有任务时继续运行。" + else "持续保持运行,适合长时间任务。"), + ) + Entry(stringResource(R.string.ui_language_title), language, + R.drawable.ic_ui_language) { open.accept(R.id.settings_appearance) } + Entry(stringResource(R.string.ui2_overlay), stringResource(R.string.ui2_overlay_sub), + R.drawable.ic_ui_chat) { open.accept(R.id.settings_overlay) } + } + SmallTitle(stringResource(R.string.ui2_support)) + Card(Modifier.fillMaxWidth()) { + Entry(stringResource(R.string.ui2_updates), UiText.text("稳定版与预览版更新"), + R.drawable.ic_recovery_refresh) { open.accept(R.id.settings_update) } + Entry(stringResource(R.string.ui2_diagnostics), stringResource(R.string.ui2_diagnostics_sub), + R.drawable.ic_ui_shield) { open.accept(R.id.settings_selftest) } + Entry(stringResource(R.string.ui2_about), stringResource(R.string.ui2_about_sub), + R.drawable.ic_about) { open.accept(R.id.settings_about) } + } + Text(UiText.format("DSHA v%s · MIT 许可", version), + modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp), + fontSize = 12.sp, textAlign = TextAlign.Center, + color = MiuixTheme.colorScheme.onSurfaceSecondary) + } + } + } +} diff --git a/app/src/main/java/com/deepseekharness/app/ui/miuix/MiuixUi.kt b/app/src/main/java/com/deepseekharness/app/ui/miuix/MiuixUi.kt new file mode 100644 index 000000000..e4dab81b0 --- /dev/null +++ b/app/src/main/java/com/deepseekharness/app/ui/miuix/MiuixUi.kt @@ -0,0 +1,86 @@ +package com.deepseekharness.app.ui.miuix + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.AbstractComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.deepseekharness.app.R +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.LocalContentColor +import top.yukonga.miuix.kmp.theme.darkColorScheme +import top.yukonga.miuix.kmp.theme.lightColorScheme + +/** 与 Java 页面共用资源色和 AppCompat 的明暗选择,不另存一份主题偏好。 */ +@Composable +internal fun DshaTheme(content: @Composable () -> Unit) { + val base = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme() + MiuixTheme(colors = base.copy( + primary = colorResource(R.color.primary), + onPrimary = colorResource(R.color.accent_on), + background = colorResource(R.color.card), + onBackground = colorResource(R.color.text), + surface = colorResource(R.color.surface), + onSurface = colorResource(R.color.text), + surfaceVariant = colorResource(R.color.card), + surfaceContainer = colorResource(R.color.card), + onSurfaceContainer = colorResource(R.color.text), + onSurfaceSecondary = colorResource(R.color.text_secondary), + onSurfaceVariantSummary = colorResource(R.color.text_secondary), + outline = colorResource(R.color.line), + )) { + CompositionLocalProvider(LocalContentColor provides MiuixTheme.colorScheme.onSurface) { + content() + } + } +} + +/** Compose 内容随所属 Activity/Fragment 的视图生命周期销毁,不持有后台业务。 */ +abstract class MiuixView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, +) : AbstractComposeView(context, attrs) { + init { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + } +} + +/** 供已有 Java 管理页复用的卡片和入口;原表单及其监听器保留在卡片内部。 */ +object MiuixUi { + @JvmStatic + fun card(context: Context, content: LinearLayout): View = MiuixPanel(context).apply { + addView(content, android.widget.FrameLayout.LayoutParams(-1, -2)) + } + + @JvmStatic + fun entry(context: Context, title: String, summary: String, icon: Int, action: Runnable): View = + object : MiuixView(context) { + @Composable override fun Content() { + DshaTheme { Entry(title, summary, icon, action::run) } + } + } +} + +@Composable +internal fun Entry(title: String, summary: String, icon: Int, action: () -> Unit) { + SuperArrow( + title = title, + summary = summary, + startAction = { + Icon(painterResource(icon), contentDescription = null, + tint = MiuixTheme.colorScheme.primary, + modifier = Modifier.padding(end = 14.dp).size(24.dp)) + }, + onClick = action, + ) +} diff --git a/app/src/main/java/com/deepseekharness/app/vscreen/VirtualScreenActivity.java b/app/src/main/java/com/deepseekharness/app/vscreen/VirtualScreenActivity.java index ef6d44ad5..03b34d636 100644 --- a/app/src/main/java/com/deepseekharness/app/vscreen/VirtualScreenActivity.java +++ b/app/src/main/java/com/deepseekharness/app/vscreen/VirtualScreenActivity.java @@ -35,16 +35,15 @@ private static String t(String zh, String en) { protected void onCreate(Bundle saved) { super.onCreate(saved); manager = VirtualScreenManager.from(this); - LinearLayout root = new LinearLayout(this); + LinearLayout root = new com.deepseekharness.app.ui.PageColumn(this); root.setOrientation(LinearLayout.VERTICAL); root.setPadding(dp(12), dp(10), dp(12), dp(12)); root.setBackgroundResource(R.color.surface); - LinearLayout navigation = new LinearLayout(this); + LinearLayout navigation = new com.deepseekharness.app.ui.ActionRow(this); navigation.addView( - button(t("返回", "Back"), this::finish), new LinearLayout.LayoutParams(0, dp(40), 1)); + button(t("返回", "Back"), this::finish), new LinearLayout.LayoutParams(0, -2, 1)); navigation.addView( - button(t("使用说明", "How to use"), this::showUsage), - new LinearLayout.LayoutParams(0, dp(40), 1)); + button(t("使用说明", "How to use"), this::showUsage), new LinearLayout.LayoutParams(0, -2, 1)); root.addView(navigation); state = label( @@ -52,9 +51,14 @@ protected void onCreate(Bundle saved) { "先连接设备通道,再点“创建竖屏”或“创建横屏”。", "Connect a device channel, then tap Create portrait or Create landscape."), 14); - root.addView(state); - detail = label("", 11); - root.addView(detail); + LinearLayout status = new LinearLayout(this); + status.setOrientation(LinearLayout.VERTICAL); + status.setPadding(dp(16), dp(12), dp(16), dp(12)); + status.addView(state); + state.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); + detail = label("", 13); + status.addView(detail); + root.addView(com.deepseekharness.app.ui.miuix.MiuixUi.card(this, status)); preview = new VirtualScreenPreviewView(this); preview.listener = new VirtualScreenPreviewView.Touch() { @@ -108,35 +112,35 @@ public void onStartTrackingTouch(SeekBar b) {} public void onStopTrackingTouch(SeekBar b) {} }); - root.addView(size, new LinearLayout.LayoutParams(-1, dp(32))); - LinearLayout orientation = new LinearLayout(this); + root.addView(size, new LinearLayout.LayoutParams(-1, dp(48))); + LinearLayout orientation = new com.deepseekharness.app.ui.ActionRow(this); orientation.addView( button(t("创建竖屏", "Create portrait"), () -> create("portrait")), - new LinearLayout.LayoutParams(0, dp(40), 1)); + new LinearLayout.LayoutParams(0, -2, 1)); orientation.addView( button(t("创建横屏", "Create landscape"), () -> create("landscape")), - new LinearLayout.LayoutParams(0, dp(40), 1)); - root.addView(orientation); - LinearLayout tools = new LinearLayout(this); + new LinearLayout.LayoutParams(0, -2, 1)); + root.addView(com.deepseekharness.app.ui.miuix.MiuixUi.card(this, orientation)); + LinearLayout tools = new com.deepseekharness.app.ui.ActionRow(this); tools.setGravity(Gravity.CENTER); tools.addView( button(t("应用", "App"), () -> toggle(packagePanel)), - new LinearLayout.LayoutParams(0, dp(40), 1)); + new LinearLayout.LayoutParams(0, -2, 1)); tools.addView( button(t("键盘", "Keyboard"), () -> toggle(textPanel)), - new LinearLayout.LayoutParams(0, dp(40), 1)); + new LinearLayout.LayoutParams(0, -2, 1)); tools.addView( - button(t("控件树", "AI tree"), this::showTree), new LinearLayout.LayoutParams(0, dp(40), 1)); + button(t("控件树", "AI tree"), this::showTree), new LinearLayout.LayoutParams(0, -2, 1)); tools.addView( button(t("关闭", "Close"), () -> run(() -> manager.close())), - new LinearLayout.LayoutParams(0, dp(40), 1)); + new LinearLayout.LayoutParams(0, -2, 1)); root.addView(tools); - LinearLayout apps = new LinearLayout(this); + LinearLayout apps = new com.deepseekharness.app.ui.ActionRow(this); apps.setOrientation(LinearLayout.HORIZONTAL); packageInput = edit(t("选择应用后会自动填入包名", "Choose an app to fill its package")); - apps.addView(packageInput, new LinearLayout.LayoutParams(0, dp(44), 1)); + apps.addView(packageInput, new LinearLayout.LayoutParams(0, -2, 1)); apps.addView( - button(t("选择", "Choose"), this::chooseApp), new LinearLayout.LayoutParams(dp(76), dp(44))); + button(t("选择", "Choose"), this::chooseApp), new LinearLayout.LayoutParams(dp(76), -2)); apps.addView( button( t("启动", "Launch"), @@ -144,14 +148,14 @@ public void onStopTrackingTouch(SeekBar b) {} String pkg = packageInput.getText().toString().trim(); run(() -> manager.launch(pkg)); }), - new LinearLayout.LayoutParams(dp(76), dp(44))); + new LinearLayout.LayoutParams(dp(76), -2)); packagePanel = apps; apps.setVisibility(View.GONE); root.addView(apps); - LinearLayout keyboard = new LinearLayout(this); + LinearLayout keyboard = new com.deepseekharness.app.ui.ActionRow(this); keyboard.setOrientation(LinearLayout.HORIZONTAL); textInput = edit(t("本机键盘输入(中文需无障碍)", "Keyboard input (accessibility for Unicode)")); - keyboard.addView(textInput, new LinearLayout.LayoutParams(0, dp(44), 1)); + keyboard.addView(textInput, new LinearLayout.LayoutParams(0, -2, 1)); keyboard.addView( button( t("发送", "Send"), @@ -160,7 +164,7 @@ public void onStopTrackingTouch(SeekBar b) {} long seq = frameSeq; run(() -> manager.type(gen, seq, text)); }), - new LinearLayout.LayoutParams(dp(88), dp(44))); + new LinearLayout.LayoutParams(dp(88), -2)); Button enter = button( "↵", @@ -169,7 +173,8 @@ public void onStopTrackingTouch(SeekBar b) {} long seq = frameSeq; run(() -> manager.action("key", gen, seq, "keycode=66")); }); - keyboard.addView(enter, new LinearLayout.LayoutParams(dp(44), dp(44))); + enter.setContentDescription(t("发送回车键", "Send Enter key")); + keyboard.addView(enter, new LinearLayout.LayoutParams(dp(48), -2)); textPanel = keyboard; keyboard.setVisibility(View.GONE); root.addView(keyboard); @@ -266,17 +271,19 @@ private void showTree() { private EditText edit(String hint) { EditText e = new EditText(this); e.setSingleLine(true); - e.setTextSize(12); + e.setTextSize(14); e.setHint(hint); - e.setMinHeight(dp(44)); - e.setPadding(dp(8), 0, dp(8), 0); + e.setContentDescription(hint); + e.setBackgroundResource(R.drawable.bg_input); + e.setMinHeight(dp(56)); + e.setPadding(dp(12), dp(12), dp(12), dp(12)); return e; } private TextView label(String text, int sp) { TextView v = new TextView(this); v.setText(text); - v.setTextSize(sp); + v.setTextSize(Math.max(13, sp)); v.setTextColor(getColor(R.color.text)); v.setPadding(0, dp(3), 0, dp(3)); return v; @@ -286,9 +293,9 @@ private Button button(String text, Runnable click) { Button b = new androidx.appcompat.widget.AppCompatButton(this); b.setText(text); b.setAllCaps(false); - b.setTextSize(12); - b.setMinHeight(dp(40)); - b.setPadding(dp(5), 0, dp(5), 0); + b.setTextSize(14); + b.setMinHeight(dp(48)); + b.setPadding(dp(12), dp(8), dp(12), dp(8)); b.setBackgroundResource(R.drawable.bg_btn); b.setTextColor(getColorStateList(R.color.button_text)); b.setOnClickListener(v -> click.run()); diff --git a/app/src/main/res/layout/activity_adb_pair.xml b/app/src/main/res/layout/activity_adb_pair.xml index 809e19285..1f2770c16 100644 --- a/app/src/main/res/layout/activity_adb_pair.xml +++ b/app/src/main/res/layout/activity_adb_pair.xml @@ -1,34 +1,40 @@ - - - - - - - - - - - - -