diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index 5e063b4b..c6927ff3 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -68,16 +68,31 @@ jobs:
run: chmod +x gradlew
- name: Run unit tests, lint, and assemble
- run: ./gradlew :app:testDebugUnitTest :app:lintDebug :app:assembleDebug --no-daemon --console=plain
+ shell: bash
+ run: |
+ set +e
+ ./gradlew :app:testDebugUnitTest :app:lintDebug :app:assembleDebug :app:assembleDebugAndroidTest --build-cache --parallel --no-daemon --console=plain
+ status=$?
+ if [[ $status -ne 0 && -f app/build/reports/lint-results-debug.txt ]]; then
+ echo "::group::Full Android Lint report"
+ cat app/build/reports/lint-results-debug.txt
+ echo "::endgroup::"
+ fi
+ exit $status
- - name: Upload debug APK
+ - name: Upload test APKs
uses: actions/upload-artifact@v7
with:
- name: openIME-debug
- path: app/build/outputs/apk/debug/app-debug.apk
+ name: openIME-test-apks
+ path: |
+ app/build/outputs/apk/debug/app-debug.apk
+ app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk
if-no-files-found: error
+ compression-level: 0
+ retention-days: 1
compatibility:
+ needs: verify
name: Compatibility API ${{ matrix.api-level }}
runs-on: ubuntu-latest
timeout-minutes: 45
@@ -87,47 +102,43 @@ jobs:
api-level: [29, 31]
steps:
- - name: Checkout source and LFS files
- uses: actions/checkout@v7
- with:
- lfs: true
-
- - name: Set up JDK 17
- uses: actions/setup-java@v6
- with:
- distribution: temurin
- java-version: '17'
- cache: gradle
+ - name: Enable KVM group perms
+ shell: bash
+ run: |
+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+ sudo udevadm control --reload-rules
+ sudo udevadm trigger --name-match=kvm
- name: Set up Android SDK
uses: android-actions/setup-android@v4
with:
packages: 'platform-tools'
- - name: Install Android build components
- shell: bash
- run: |
- sdkmanager "platforms;android-36" \
- "build-tools;35.0.0" \
- "ndk;27.0.12077973" \
- "cmake;3.22.1"
+ - name: Download prebuilt test APKs
+ uses: actions/download-artifact@v7
+ with:
+ name: openIME-test-apks
+ path: ci-apks
- - name: Verify LFS objects
+ - name: Prepare instrumentation runner
shell: bash
run: |
- git lfs checkout
- if grep -RIl --exclude-dir=.git "version https://git-lfs.github.com/spec/v1" app/libs app/src/main/assets/models; then
- echo "A Git LFS pointer was left unresolved" >&2
- exit 1
- fi
-
- - name: Fetch pinned librime dependencies
- shell: bash
- run: bash scripts/fetch_rime_deps.sh
-
- - name: Ensure Gradle wrapper is executable
- shell: bash
- run: chmod +x gradlew
+ cat > ci-run-instrumentation.sh <<'EOF'
+ #!/usr/bin/env bash
+ set -euo pipefail
+ api_level="$1"
+ mkdir -p ci-reports
+ app_apk="$(find ci-apks -type f -name 'app-debug.apk' -print -quit)"
+ test_apk="$(find ci-apks -type f -name 'app-debug-androidTest.apk' -print -quit)"
+ test -n "$app_apk"
+ test -n "$test_apk"
+ adb install -r "$app_apk"
+ adb install -r "$test_apk"
+ adb shell am instrument -w -r \
+ llc.slacker.openime.test/androidx.test.runner.AndroidJUnitRunner \
+ | tee "ci-reports/instrumentation-api-${api_level}.txt"
+ EOF
+ chmod +x ci-run-instrumentation.sh
- name: Run compatibility instrumentation
uses: reactivecircus/android-emulator-runner@v2
@@ -138,14 +149,14 @@ jobs:
profile: pixel_2
disable-animations: true
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
- script: ./gradlew :app:connectedDebugAndroidTest --no-daemon --console=plain
+ script: bash ci-run-instrumentation.sh ${{ matrix.api-level }}
- - name: Upload compatibility test reports
+ - name: Upload compatibility test report
if: always()
uses: actions/upload-artifact@v7
with:
name: compatibility-api-${{ matrix.api-level }}
- path: |
- app/build/reports/androidTests/connected
- app/build/outputs/androidTest-results/connected
+ path: ci-reports
if-no-files-found: warn
+ compression-level: 0
+ retention-days: 7
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..5575bf61
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,179 @@
+name: Android Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ name: Build signed arm64 release
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+
+ steps:
+ - name: Checkout source and LFS files
+ uses: actions/checkout@v7
+ with:
+ lfs: true
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v6
+ with:
+ distribution: temurin
+ java-version: '17'
+ cache: gradle
+
+ - name: Set up Android SDK
+ uses: android-actions/setup-android@v4
+ with:
+ packages: 'platform-tools'
+
+ - name: Install Android build components
+ shell: bash
+ run: |
+ sdkmanager "platforms;android-36" \
+ "build-tools;35.0.0" \
+ "ndk;27.0.12077973" \
+ "cmake;3.22.1"
+
+ - name: Verify LFS objects
+ shell: bash
+ run: |
+ git lfs checkout
+ if grep -RIl --exclude-dir=.git "version https://git-lfs.github.com/spec/v1" app/libs app/src/main/assets/models; then
+ echo "A Git LFS pointer was left unresolved" >&2
+ exit 1
+ fi
+
+ - name: Fetch pinned librime dependencies
+ shell: bash
+ run: bash scripts/fetch_rime_deps.sh
+
+ - name: Ensure Gradle wrapper is executable
+ shell: bash
+ run: chmod +x gradlew
+
+ - name: Validate signing secrets
+ shell: bash
+ env:
+ OPENIME_KEYSTORE_B64: ${{ secrets.OPENIME_KEYSTORE_B64 }}
+ OPENIME_KEYSTORE_PASSWORD: ${{ secrets.OPENIME_KEYSTORE_PASSWORD }}
+ OPENIME_KEY_ALIAS: ${{ secrets.OPENIME_KEY_ALIAS }}
+ OPENIME_KEY_PASSWORD: ${{ secrets.OPENIME_KEY_PASSWORD }}
+ run: |
+ for name in \
+ OPENIME_KEYSTORE_B64 \
+ OPENIME_KEYSTORE_PASSWORD \
+ OPENIME_KEY_ALIAS \
+ OPENIME_KEY_PASSWORD
+ do
+ if [[ -z "${!name}" ]]; then
+ echo "Missing required release signing secret: ${name}" >&2
+ exit 1
+ fi
+ done
+
+ - name: Decode release keystore
+ shell: bash
+ env:
+ OPENIME_KEYSTORE_B64: ${{ secrets.OPENIME_KEYSTORE_B64 }}
+ run: |
+ printf '%s' "$OPENIME_KEYSTORE_B64" \
+ | base64 --decode \
+ > "$RUNNER_TEMP/openime-release.jks"
+ chmod 600 "$RUNNER_TEMP/openime-release.jks"
+
+ - name: Run tests, lint, and release build
+ shell: bash
+ env:
+ OPENIME_KEYSTORE_PATH: ${{ runner.temp }}/openime-release.jks
+ OPENIME_KEYSTORE_PASSWORD: ${{ secrets.OPENIME_KEYSTORE_PASSWORD }}
+ OPENIME_KEY_ALIAS: ${{ secrets.OPENIME_KEY_ALIAS }}
+ OPENIME_KEY_PASSWORD: ${{ secrets.OPENIME_KEY_PASSWORD }}
+ run: |
+ ./gradlew \
+ :app:testDebugUnitTest \
+ :app:lintRelease \
+ :app:assembleRelease \
+ --no-daemon \
+ --console=plain
+
+ - name: Verify signed arm64 artifact
+ id: artifact
+ shell: bash
+ run: |
+ APK="$(find app/build/outputs/apk/release -maxdepth 1 -type f -name 'app-release*.apk' | head -n 1)"
+ if [[ -z "$APK" || ! -f "$APK" ]]; then
+ echo "Release APK not found" >&2
+ exit 1
+ fi
+
+ "$ANDROID_HOME/build-tools/35.0.0/apksigner" verify --verbose "$APK"
+
+ mapfile -t ABIS < <(
+ unzip -Z1 "$APK" \
+ | awk -F/ '/^lib\/[^/]+\/[^/]+$/ {print $2}' \
+ | sort -u
+ )
+ if [[ "${#ABIS[@]}" -ne 1 || "${ABIS[0]}" != "arm64-v8a" ]]; then
+ echo "Release APK must contain only arm64-v8a; found: ${ABIS[*]:-none}" >&2
+ exit 1
+ fi
+
+ VERSION_NAME="$(
+ sed -n 's/^[[:space:]]*versionName = "\(.*\)"/\1/p' app/build.gradle.kts \
+ | head -n 1
+ )"
+ if [[ -z "$VERSION_NAME" ]]; then
+ echo "Unable to read versionName" >&2
+ exit 1
+ fi
+ if [[ "$GITHUB_REF_NAME" != "v$VERSION_NAME" ]]; then
+ echo "Tag $GITHUB_REF_NAME does not match versionName $VERSION_NAME" >&2
+ exit 1
+ fi
+
+ OUTPUT="openIME-v${VERSION_NAME}-arm64-release.apk"
+ cp "$APK" "$OUTPUT"
+ sha256sum "$OUTPUT" > SHA256SUMS.txt
+ cp THIRD_PARTY_NOTICES.md THIRD_PARTY_NOTICES.release.md
+ echo "apk=$OUTPUT" >> "$GITHUB_OUTPUT"
+ echo "version=$VERSION_NAME" >> "$GITHUB_OUTPUT"
+
+ - name: Create release notes
+ shell: bash
+ run: |
+ cat > RELEASE_NOTES.md <<'EOF'
+ ## 更新内容
+ - 此版本包含标签所对应提交中的 openIME 功能、稳定性与界面更新。
+
+ ## 已知问题
+ - 首次安装时,完整 Rime 词典可能需要完成首次部署后才达到稳定候选表现。
+ - 发布 APK 仅包含 arm64-v8a;模拟器继续使用 Debug 构建。
+
+ ## 校验方式
+ 下载 APK 后,在同一目录执行:
+
+ ```bash
+ sha256sum -c SHA256SUMS.txt
+ ```
+
+ 校验通过后再安装。
+ EOF
+
+ - name: Publish GitHub Release
+ shell: bash
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ gh release create "$GITHUB_REF_NAME" \
+ "${{ steps.artifact.outputs.apk }}" \
+ SHA256SUMS.txt \
+ THIRD_PARTY_NOTICES.release.md \
+ --verify-tag \
+ --title "openIME $GITHUB_REF_NAME" \
+ --notes-file RELEASE_NOTES.md
diff --git a/.gitignore b/.gitignore
index b9dcf657..c70d5c04 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,6 +24,8 @@ artifacts/
# Local screenshots and UI dumps kept outside the source tree
*.png
+!docs/images/
+!docs/images/*.png
*.xml
!app/src/**/*.xml
!app/src/main/assets/
diff --git a/README.md b/README.md
index 5c142a90..35d8ee71 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,34 @@
# openIME
-独立的 Android 系统输入法,包名为 `llc.slacker.openime`,应用显示名为
-`openIME`。它不依赖 `Minis for Android`,也不与其他输入法共享进程、数据或服务。
+
+
+
+
+本地优先的 Android 中文输入法。
+
+openIME 是独立 Android 系统输入法,包名 `llc.slacker.openime`。拼音候选、用户学习和语音识别均在设备内运行;应用不声明 `INTERNET` 权限。
[](https://github.com/Slacker-LLC/openIME/actions/workflows/android.yml)
+## 下载
+
+正式版本从 [GitHub Releases](https://github.com/Slacker-LLC/openIME/releases/latest) 下载。发布 APK 使用固定发布签名,文件名为 `openIME-v{versionName}-arm64-release.apk`,同一 Release 同时提供 `SHA256SUMS.txt`。
+
+当前正式发布包只包含 `arm64-v8a`;开发用 Debug APK 仍保留 `x86_64`,用于模拟器回归。
+
+## 安装四步
+
+1. **启用**:安装 APK 后打开 openIME,进入系统输入法设置并启用 `openIME`。
+2. **切换**:回到引导页,打开系统输入法选择器并切换到 `openIME`。
+3. **授权(可选)**:需要本地语音输入时授权麦克风;不授权可直接跳过。
+4. **试打**:在引导页输入框确认键盘、候选和上屏链路正常。
+
+语音输入长按空格达到系统长按阈值后开始录音,松手后识别并上屏。
+
+## 截图
+
+真实截图由 `scripts/visual_matrix_regression.ps1` 和视觉验收流程生成。仓库不提交占位图;26 键、九键与设置页的浅色/深色截图将在真实视觉矩阵验收后写入 `docs/images/`。
+
## 项目定位
openIME 的界面、输入法引擎和本地语音链路均在同一个独立 APK 内运行:
@@ -36,7 +60,7 @@ InputConnection
- Emoji、符号、剪贴板、文本编辑、浮动键盘和设置面板;手写目前仅保留笔迹采集 UI,识别引擎尚未接入,正式入口默认隐藏。
- 根据输入法窗口实际可用宽度动态计算列宽与间距;宽屏限制内容最大宽度并居中,
系统底部区域通过 WindowInsets 处理。
-- 空格短按输入空格或提交首选,长按约 150 ms 进入唯一的语音输入流程;删除键上滑清空。
+- 空格短按输入空格或提交首选,长按达到系统长按时长(`ViewConfiguration.getLongPressTimeout()`)进入唯一的语音输入流程;删除键上滑清空。
- 选词或首选上屏后统一清除拼音、候选与 Rime composition,随后删除键只处理目标输入框。
- Android 密码编辑器的隐私边界、麦克风权限失败回退和本地模型校验。
@@ -94,10 +118,9 @@ Windows PowerShell:
.\scripts\build_ascii.ps1
```
-APK 输出为 `app/build/outputs/apk/debug/app-debug.apk`。本地交付副本可放在被忽略的
-`artifacts/openIME-1.0-debug.apk`,不会提交到源码仓库。
+Debug APK 输出为 `app/build/outputs/apk/debug/app-debug.apk`,仅用于开发与回归;正式分发使用 GitHub Release 中固定签名的 arm64 APK。
-## 安装并启用
+开发设备可用 ADB 安装 Debug APK:
```bash
adb install -r app/build/outputs/apk/debug/app-debug.apk
@@ -105,8 +128,7 @@ adb shell ime enable --user 0 llc.slacker.openime/.LocalVoiceImeService
adb shell ime set --user 0 llc.slacker.openime/.LocalVoiceImeService
```
-也可以打开 APK 的设置页,按系统提示启用 `openIME`。调试测试 Activity 和 E2E
-Receiver 只存在于 debug 变体,不会成为正式输入法的公共控制入口。
+调试测试 Activity 和 E2E Receiver 只存在于 debug 变体,不会成为正式输入法的公共控制入口。
## 验证
@@ -148,12 +170,15 @@ bash scripts/verify_linux.sh emulator-5554
- [适配与坐标规范](docs/COORDINATE_SYSTEM.md)
- [测试流程 SOP](docs/TEST_SOP.md)
- [脚本说明](scripts/README.md)
+- [发布流程](docs/RELEASE.md)
## 隐私与安全
-语音 PCM 只在当前会话的有界内存缓冲区中处理,结束、取消或失败时清空;模型和
-词典随 APK 提供。密码输入框不写入候选、剪贴板或日志,但允许用户从剪贴板粘贴;发现安全问题请不要直接
-创建公开 Issue,先按 [SECURITY.md](SECURITY.md) 联系维护者。
+**本应用不含联网权限,数据只存在本机。**
+
+语音 PCM 只在当前会话的有界内存缓冲区中处理,结束、取消或失败时清空;模型和词典随 APK 提供。密码输入框不写入候选、剪贴板或日志,但允许用户从剪贴板粘贴。设置中的“关于与数据”可导出/合并导入用户数据;剪贴板历史不导出。卸载会清除本机全部数据,包括学习的用户词库。
+
+发现安全问题请不要直接创建公开 Issue,先按 [SECURITY.md](SECURITY.md) 联系维护者。
## 许可证
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
index 48a9386d..09dba908 100644
--- a/THIRD_PARTY_NOTICES.md
+++ b/THIRD_PARTY_NOTICES.md
@@ -1,19 +1,26 @@
-# 第三方资源
+# 第三方资源与分发核对
-## Microsoft Fluent Emoji
+本文件记录 openIME 实际随源码或 APK 分发的主要第三方代码、数据和模型。主项目许可证仍由项目所有者决定,本文件不为 openIME 本身授予许可证。
-`app/src/main/assets/emoji/fluent/` 中的 3D 表情资源来自 Microsoft 的
-[fluentui-emoji](https://github.com/microsoft/fluentui-emoji) 项目,按其 MIT License 使用。
+| 组件 | 仓库内位置 | 上游/来源 | 许可证 | 分发核对 |
+|---|---|---|---|---|
+| librime | `app/src/main/cpp/vendor/librime/` | https://github.com/rime/librime | BSD-3-Clause | vendored `LICENSE` 保留;APK 同时包含 `assets/licenses/librime-BSD-3-Clause.txt` |
+| OpenCC | `app/src/main/cpp/vendor/OpenCC/` | https://github.com/BYVoid/OpenCC | Apache-2.0 | vendored `LICENSE` 保留;APK 同时包含 `assets/licenses/OpenCC-Apache-2.0.txt` |
+| Snappy | `app/src/main/cpp/vendor/snappy/` | https://github.com/google/snappy | BSD-3-Clause | vendored `COPYING` 保留;APK 同时包含 `assets/licenses/snappy-BSD-3-Clause.txt` |
+| Rime Ice 词典 | `app/src/main/assets/rime-data/openime_dicts/`、`pinyin_phrases.tsv` | https://github.com/iDvel/rime-ice,固定提交 `75e6572bebc05b49021e842949ce947882e3e4b2` | GPL-3.0-only | 已核对;完整文本随 APK/源码位于 `app/src/main/assets/licenses/rime-ice-GPL-3.0.txt` |
+| Rime 基础数据 | `app/src/main/assets/rime/` | Rime / luna-pinyin / essay 等上游数据 | 以各目录 `AUTHORS` / 上游许可为准 | 已保留上游 AUTHORS;发布前不得删除这些归属文件 |
+| sherpa-onnx Android runtime | `app/libs/sherpa-onnx-1.13.6.aar` | https://github.com/k2-fsa/sherpa-onnx | Apache-2.0 | APK 包含 `assets/licenses/sherpa-onnx-Apache-2.0.txt` |
+| 中英双语 Streaming Paraformer INT8 语音模型 | `app/src/main/assets/models/voice/bilingual-paraformer/` | https://huggingface.co/csukuangfj/sherpa-onnx-streaming-paraformer-bilingual-zh-en | Apache-2.0 | 只打包 `encoder.int8.onnx`、`decoder.int8.onnx` 和 `tokens.txt`;APK 包含 `assets/licenses/paraformer-model-Apache-2.0.txt`,`manifest.json` 固定模型 ID/版本与文件哈希 |
+| Microsoft Fluent Emoji | `app/src/main/assets/emoji/fluent/` | https://github.com/microsoft/fluentui-emoji | MIT | 仅打包当前实际使用的基础情绪表情资源;APK 包含 `assets/licenses/fluent-emoji-MIT.txt` |
-应用只打包「Smileys & Emotion」中的基础黄豆脸/情绪表情;没有把手势、动物、食物或贴图资源打进 APK。
+## 发布前检查
-## Rime Ice 中文词库
+- 不要删除 vendored 源码目录中的 LICENSE、COPYING、AUTHORS 或 NOTICE 类文件。
+- `app/src/main/assets/licenses/` 下的第三方许可文本必须继续随 APK 打包,包括 Rime Ice、librime、OpenCC、Snappy、sherpa-onnx、Paraformer 模型和 Fluent Emoji。
+- 语音 runtime 与模型升级时,重新核对**具体版本/模型**的许可证,不要只沿用本文件旧结论。
+- 内置词库来源或固定提交变化时,同步更新本文件、`docs/LICENSING.md` 和 APK 内许可证文件。
+- 主项目 `LICENSE` 在项目所有者决定前保持缺失;README 对主项目许可状态的现有表述保持不变。
-`app/src/main/assets/rime-data/openime_dicts/` 中的 `8105`、`base`、`ext`、
-`others` 词库,以及由它们生成的 `app/src/main/assets/pinyin_phrases.tsv` 高频子集,
-来自 [rime-ice](https://github.com/iDvel/rime-ice),固定于提交
-`75e6572bebc05b49021e842949ce947882e3e4b2`,按 GPL-3.0-only 使用。
+## 备注
-完整许可证随 APK 和源码保存在
-`app/src/main/assets/licenses/rime-ice-GPL-3.0.txt`。openIME 没有引入体积大、
-部署耗时更长的 `tencent` 自动注音词表。
+Git LFS、GitHub Release 或 APK 打包方式不会改变第三方作品本身的许可证义务。此清单只记录仓库当前已核对的信息,不替代各上游许可证原文。
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 9773dee8..70da241e 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -5,6 +5,21 @@ if (customBuildDir != null) {
layout.buildDirectory.set(file(customBuildDir))
}
+fun releaseValue(name: String): String? =
+ providers.gradleProperty(name).orNull
+ ?: providers.environmentVariable(name).orNull
+
+val releaseKeystorePath = releaseValue("OPENIME_KEYSTORE_PATH")
+val releaseKeystorePassword = releaseValue("OPENIME_KEYSTORE_PASSWORD")
+val releaseKeyAlias = releaseValue("OPENIME_KEY_ALIAS")
+val releaseKeyPassword = releaseValue("OPENIME_KEY_PASSWORD")
+val releaseSigningReady = listOf(
+ releaseKeystorePath,
+ releaseKeystorePassword,
+ releaseKeyAlias,
+ releaseKeyPassword,
+).all { !it.isNullOrBlank() }
+
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
@@ -23,11 +38,39 @@ android {
versionCode = 4
versionName = "1.0.3"
- ndk {
- // Physical phones in scope are arm64; x86_64 keeps the existing
- // emulator regression path available without shipping 32-bit
- // native Rime binaries we do not need.
- abiFilters += listOf("arm64-v8a", "x86_64")
+ }
+
+ signingConfigs {
+ if (releaseSigningReady) {
+ create("release") {
+ storeFile = file(releaseKeystorePath!!)
+ storePassword = releaseKeystorePassword
+ keyAlias = releaseKeyAlias
+ keyPassword = releaseKeyPassword
+ }
+ }
+ }
+
+ buildTypes {
+ getByName("debug") {
+ ndk {
+ abiFilters.clear()
+ abiFilters += listOf("arm64-v8a", "x86_64")
+ }
+ }
+ getByName("release") {
+ isMinifyEnabled = false
+ ndk {
+ abiFilters.clear()
+ abiFilters += "arm64-v8a"
+ }
+ if (releaseSigningReady) {
+ signingConfig = signingConfigs.getByName("release")
+ } else {
+ logger.warn(
+ "openIME release signing is not configured; assembleRelease will produce an unsigned APK",
+ )
+ }
}
}
@@ -59,6 +102,7 @@ android {
dependencies {
implementation(files("libs/sherpa-onnx-1.13.6.aar"))
testImplementation("junit:junit:4.13.2")
+ testImplementation("org.json:json:20260814")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test:runner:1.6.2")
}
diff --git a/app/src/androidTest/java/llc/slacker/openime/AuditInteractionInstrumentedTest.kt b/app/src/androidTest/java/llc/slacker/openime/AuditInteractionInstrumentedTest.kt
index b7b4c43f..66b1d782 100644
--- a/app/src/androidTest/java/llc/slacker/openime/AuditInteractionInstrumentedTest.kt
+++ b/app/src/androidTest/java/llc/slacker/openime/AuditInteractionInstrumentedTest.kt
@@ -35,6 +35,7 @@ class AuditInteractionInstrumentedTest {
val finals = mutableListOf()
val characters = mutableListOf()
val fuzzyChanges = mutableListOf()
+ val textEdits = mutableListOf()
var starts = 0
var stops = 0
var cancels = 0
@@ -66,6 +67,7 @@ class AuditInteractionInstrumentedTest {
"onSpace" -> { spaces++; null }
"onBackspace" -> { backspaces++; null }
"onClearAll" -> { clears++; null }
+ "onTextEdit" -> { textEdits.add(args!![0] as String); null }
"onFloatingKeyboardChanged" -> {
keyboard.setFloatingWindowMode(args!![0] as Boolean)
null
@@ -450,11 +452,11 @@ class AuditInteractionInstrumentedTest {
assertTrue("${theme.label} must be selectable from settings", chip != null)
assertTrue("${theme.label} must expose a 48dp target", chip!!.minimumHeight >= keyboard.resources.displayMetrics.density * 48f)
}
- val cyberpunk = keyboard.findTestTarget(ImeTheme.CYBERPUNK.label)
- assertTrue(cyberpunk!!.performClick())
+ val ios = keyboard.findTestTarget(ImeTheme.IOS.label)
+ assertTrue(ios!!.performClick())
assertTrue(
"Selected theme must expose its accessible state",
- keyboard.findTestTarget(ImeTheme.CYBERPUNK.label)!!.contentDescription.toString().contains("已选中"),
+ keyboard.findTestTarget(ImeTheme.IOS.label)!!.contentDescription.toString().contains("已选中"),
)
true
}
@@ -483,6 +485,8 @@ class AuditInteractionInstrumentedTest {
mainHandler = Handler(Looper.getMainLooper()),
windowProvider = { null },
keyboardHeightPx = { keyboard.measuredHeight.takeIf { it > 0 } },
+ floatingWidthPercent = { 88 },
+ floatingOpacityPercent = { 100 },
)
controller.enable()
assertTrue(controller.enabled)
@@ -588,6 +592,10 @@ class AuditInteractionInstrumentedTest {
harness.awaitMain {
val expand = keyboard.findViewWithTag("candidate-expand")
assertTrue("Empty composition must not expose a dead overflow action", !expand.isEnabled)
+ assertTrue(
+ "Empty candidate overflow must use the shared disabled alpha",
+ expand.alpha == ImeSurfacePolicy.DISABLED_ALPHA,
+ )
assertTrue(expand.contentDescription.toString().contains("暂无更多候选"))
keyboard.renderState(ImeState(composition = "ni", candidates = listOf("你")))
assertTrue("Candidate overflow must become available with a result", expand.isEnabled)
@@ -625,6 +633,10 @@ class AuditInteractionInstrumentedTest {
keyboard.renderState(ImeState(passwordField = true))
val clipboard = keyboard.findViewWithTag("clipboard-toolbar")
assertFalse("Password fields must disable clipboard history", clipboard.isEnabled)
+ assertTrue(
+ "Password clipboard action must use the shared disabled alpha",
+ clipboard.alpha == ImeSurfacePolicy.DISABLED_ALPHA,
+ )
assertTrue(clipboard.contentDescription.toString().contains("密码输入中不可用"))
assertTrue("Sensitive editors must not list clipboard in tools", run {
keyboard.showPanel(Panel.TOOLS)
@@ -1047,6 +1059,70 @@ class AuditInteractionInstrumentedTest {
}
}
+ @Test
+ fun spaceJitterDoesNotEnterCursorModeAndHorizontalSwipeDoes() = withKeyboard { harness, recorder, keyboard ->
+ val density = keyboard.resources.displayMetrics.density
+ harness.awaitMain {
+ val origin = keyPoint(keyboard, "key-space")
+ var downTime = SystemClock.uptimeMillis()
+ pointers(keyboard, downTime, MotionEvent.ACTION_DOWN, listOf(origin))
+ SystemClock.sleep(40L)
+ pointers(
+ keyboard,
+ downTime,
+ MotionEvent.ACTION_MOVE,
+ listOf(origin.copy(x = origin.x + 3f * density)),
+ )
+ SystemClock.sleep(40L)
+ pointers(keyboard, downTime, MotionEvent.ACTION_UP, listOf(origin.copy(x = origin.x + 3f * density)))
+ assertTrue("A small jitter must not move the cursor", recorder.textEdits.isEmpty())
+ val spacesAfterJitter = recorder.spaces
+
+ val swipeOrigin = keyPoint(keyboard, "key-space")
+ downTime = SystemClock.uptimeMillis()
+ pointers(keyboard, downTime, MotionEvent.ACTION_DOWN, listOf(swipeOrigin))
+ pointers(
+ keyboard,
+ downTime,
+ MotionEvent.ACTION_MOVE,
+ listOf(swipeOrigin.copy(x = swipeOrigin.x + 19f * density)),
+ )
+ pointers(
+ keyboard,
+ downTime,
+ MotionEvent.ACTION_MOVE,
+ listOf(swipeOrigin.copy(x = swipeOrigin.x + 31f * density)),
+ )
+ pointers(
+ keyboard,
+ downTime,
+ MotionEvent.ACTION_MOVE,
+ listOf(swipeOrigin.copy(x = swipeOrigin.x + 43f * density)),
+ )
+ pointers(
+ keyboard,
+ downTime,
+ MotionEvent.ACTION_MOVE,
+ listOf(swipeOrigin.copy(x = swipeOrigin.x + 30f * density)),
+ )
+ pointers(
+ keyboard,
+ downTime,
+ MotionEvent.ACTION_UP,
+ listOf(swipeOrigin.copy(x = swipeOrigin.x + 30f * density)),
+ )
+
+ assertEquals(
+ "A deliberate drag must move across characters and reverse direction",
+ listOf("right", "right", "left"),
+ recorder.textEdits,
+ )
+ assertEquals("Cursor movement must not commit a space", spacesAfterJitter, recorder.spaces)
+ assertEquals("Cursor movement must not arm voice", 0, recorder.starts)
+ true
+ }
+ }
+
@Test
fun spaceOwnerPointerUpBeforeLongPressCancelsPendingArmWithoutGhostSpace() = withKeyboard { harness, recorder, keyboard ->
lateinit var owner: Finger
diff --git a/app/src/androidTest/java/llc/slacker/openime/NineKeyChineseInstrumentedTest.kt b/app/src/androidTest/java/llc/slacker/openime/NineKeyChineseInstrumentedTest.kt
index 740a85b7..e6b3a46e 100644
--- a/app/src/androidTest/java/llc/slacker/openime/NineKeyChineseInstrumentedTest.kt
+++ b/app/src/androidTest/java/llc/slacker/openime/NineKeyChineseInstrumentedTest.kt
@@ -5,6 +5,7 @@ import android.view.ViewGroup
import android.widget.TextView
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.After
+import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
@@ -49,6 +50,36 @@ class NineKeyChineseInstrumentedTest {
assertComposition("zhe")
}
+ @Test
+ fun ambiguousRailShowsInputtablePinyinAndSelectionKeepsCompositionActive() {
+ tapDigits("64")
+
+ val paths = harness.awaitMain { activity ->
+ val root = activity.findViewById(android.R.id.content) ?: return@awaitMain emptyList()
+ val content = findView(root) { it.tag == "nine-symbol-scroll-content" } as? ViewGroup
+ ?: return@awaitMain emptyList()
+ (0 until content.childCount).mapNotNull { index ->
+ (content.getChildAt(index) as? TextView)?.text?.toString()
+ }
+ }
+ assertTrue("64 must keep ni as an available path: $paths", "ni" in paths)
+ assertTrue("64 must keep mi as an available path: $paths", "mi" in paths)
+ assertFalse("64 must not show the non-Pinyin spelling oh: $paths", "oh" in paths)
+
+ val selected = harness.awaitMain { activity ->
+ val root = activity.findViewById(android.R.id.content) ?: return@awaitMain false
+ val content = findView(root) { it.tag == "nine-symbol-scroll-content" } as? ViewGroup
+ ?: return@awaitMain false
+ val choice = (0 until content.childCount)
+ .mapNotNull { content.getChildAt(it) as? TextView }
+ .firstOrNull { it.text.toString() == "mi" }
+ ?: return@awaitMain false
+ choice.performClick()
+ }
+ assertTrue("The mi path must be independently selectable", selected)
+ assertComposition("mi")
+ }
+
private fun enterChineseNineKey() {
harness.awaitMain { activity ->
val root = activity.findViewById(android.R.id.content) ?: return@awaitMain null
diff --git a/app/src/androidTest/java/llc/slacker/openime/RimeNineKeyInstrumentedTest.kt b/app/src/androidTest/java/llc/slacker/openime/RimeNineKeyInstrumentedTest.kt
index f0641f30..52bf7b61 100644
--- a/app/src/androidTest/java/llc/slacker/openime/RimeNineKeyInstrumentedTest.kt
+++ b/app/src/androidTest/java/llc/slacker/openime/RimeNineKeyInstrumentedTest.kt
@@ -1,9 +1,12 @@
package llc.slacker.openime
import android.os.SystemClock
+import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith
@@ -56,4 +59,47 @@ class RimeNineKeyInstrumentedTest {
rime.shutdown()
}
}
+
+ @Test
+ fun auditProductionPresetsAgainstNativeRimePreeditWithoutPresetLayer() {
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ assumeTrue(
+ "manual production preset audit; pass -e runPresetAudit true",
+ InstrumentationRegistry.getArguments().getString("runPresetAudit") == "true",
+ )
+ val context = instrumentation.targetContext
+ val originalFuzzy = ImeSettingsRepository.loadFuzzy(context)
+ ImeSettingsRepository.saveFuzzy(context, false)
+ val rime = RimeEngine(context = context)
+ try {
+ rime.start()
+ val deadline = SystemClock.elapsedRealtime() + 1_200_000L
+ while (!rime.isReady && rime.errorMessage.isBlank() && SystemClock.elapsedRealtime() < deadline) {
+ SystemClock.sleep(100L)
+ }
+ assertTrue("librime failed to start: ${rime.errorMessage}", rime.isReady)
+
+ val report = NineKeyPresets.combinations.map { (digits, expectedPaths) ->
+ val snapshot = RimeNative.nativeSetInput(digits).orEmpty()
+ val preedit = snapshot.getOrNull(1).orEmpty()
+ val normalized = preedit
+ .lowercase()
+ .filter { it in 'a'..'z' || it == 'ü' }
+ val matches = expectedPaths.any { expected ->
+ expected.lowercase().filter { it in 'a'..'z' || it == 'ü' } == normalized
+ }
+ Log.i(
+ "OpenImeNineKeyAudit",
+ "digits=$digits preedit=$preedit expected=${expectedPaths.joinToString("|")} matches=$matches",
+ )
+ digits to matches
+ }
+ assertEquals(NineKeyPresets.combinations.size, report.size)
+ } finally {
+ ImeSettingsRepository.saveFuzzy(context, originalFuzzy)
+ rime.invalidateSettingsCache()
+ rime.shutdown()
+ }
+ }
+
}
diff --git a/app/src/androidTest/java/llc/slacker/openime/SetupFormActionInstrumentedTest.kt b/app/src/androidTest/java/llc/slacker/openime/SetupFormActionInstrumentedTest.kt
index 533bf06b..319b09bd 100644
--- a/app/src/androidTest/java/llc/slacker/openime/SetupFormActionInstrumentedTest.kt
+++ b/app/src/androidTest/java/llc/slacker/openime/SetupFormActionInstrumentedTest.kt
@@ -42,7 +42,10 @@ class SetupFormActionInstrumentedTest {
?: error("save button missing")
assertFalse("Blank quick phrase must not be saveable", save.isEnabled)
- assertTrue("Disabled save action must look disabled", save.alpha < 1f)
+ assertTrue(
+ "Disabled save action must use the shared disabled alpha",
+ save.alpha == ImeSurfacePolicy.DISABLED_ALPHA,
+ )
assertTrue(
"Disabled save action must explain the missing content",
save.contentDescription.toString().contains("不可用"),
@@ -65,8 +68,11 @@ class SetupFormActionInstrumentedTest {
harness.launch(intent = intent)
harness.awaitMain { activity ->
val category = activity.findViewById(R.id.quick_phrase_category_editor)
+ val code = activity.findViewById(R.id.quick_phrase_code_editor)
val phrase = activity.findViewById(R.id.quick_phrase_text_editor)
category.onEditorAction(EditorInfo.IME_ACTION_NEXT)
+ assertSame(code, activity.currentFocus)
+ code.onEditorAction(EditorInfo.IME_ACTION_NEXT)
assertSame(phrase, activity.currentFocus)
phrase.onEditorAction(EditorInfo.IME_ACTION_DONE)
}
diff --git a/app/src/androidTest/java/llc/slacker/openime/TextEditControlsInstrumentedTest.kt b/app/src/androidTest/java/llc/slacker/openime/TextEditControlsInstrumentedTest.kt
index c466d41c..90fd4720 100644
--- a/app/src/androidTest/java/llc/slacker/openime/TextEditControlsInstrumentedTest.kt
+++ b/app/src/androidTest/java/llc/slacker/openime/TextEditControlsInstrumentedTest.kt
@@ -102,7 +102,10 @@ class TextEditControlsInstrumentedTest {
assertNotNull("password editor must still show $label", control)
assertFalse("password $label must not remain clickable", control!!.isClickable)
assertFalse("password $label must expose an enabled state", control.isEnabled)
- assertTrue("password $label should look unavailable", control.alpha < 1f)
+ assertTrue(
+ "password $label should use the shared disabled alpha",
+ control.alpha == ImeSurfacePolicy.DISABLED_ALPHA,
+ )
val description = control.contentDescription?.toString().orEmpty()
assertTrue(
"password $label must remain named when unavailable",
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 184638ee..60b250f3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,7 +8,8 @@
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
- android:icon="@drawable/ic_launcher"
+ android:icon="@mipmap/ic_launcher"
+ android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
@@ -27,21 +28,26 @@
+
+
0
+ 1
+ 2
+and@@ 3
+筑 4
+陨 5
+眺 6
+塘 7
+檩 8
+衷 9
+氧 10
+孔 11
+阖 12
+邠 13
+坎 14
+喵 15
+曰 16
+鼠 17
+隐 18
+腊 19
+族 20
+矧 21
+敉 22
+俜 23
+似 24
+怫 25
+塔 26
+price 27
+春 28
+罍 29
+娅 30
+棉 31
+弃 32
+茱 33
+应 34
+汈 35
+擦 36
+贺 37
+鹇 38
+these 39
+迅 40
+诬 41
+do@@ 42
+盍 43
+秕 44
+啃 45
+颟 46
+辑 47
+彘 48
+ps 49
+斜 50
+瞭 51
+铟 52
+漭 53
+蹇 54
+旆 55
+窳 56
+臊 57
+览 58
+嘿 59
+淖 60
+尴 61
+袆 62
+斧 63
+筹 64
+媵 65
+挞 66
+臧 67
+齐 68
+璨 69
+笥 70
+滂 71
+即 72
+愔 73
+思 74
+gr@@ 75
+幅 76
+祛 77
+箬 78
+礁 79
+茅 80
+北 81
+澡 82
+俭 83
+蘅 84
+ing 85
+肺 86
+肢 87
+巢 88
+九 89
+蠓 90
+路 91
+藻 92
+沱 93
+ness 94
+璐 95
+积 96
+寞 97
+栳 98
+舆 99
+医 100
+眷 101
+岳 102
+勘 103
+璃 104
+黔 105
+犇 106
+哎 107
+罡 108
+k 109
+丝 110
+de 111
+跣 112
+梦 113
+需 114
+毅 115
+峡 116
+竞 117
+砦 118
+研 119
+眙 120
+滋 121
+鹳 122
+肝 123
+阼 124
+per 125
+忱 126
+乏 127
+废 128
+邦 129
+輶 130
+驯 131
+夫 132
+寳 133
+忪 134
+崑 135
+睾 136
+逗 137
+峰 138
+越 139
+狗 140
+蟒 141
+笆 142
+适 143
+洇 144
+缶 145
+ore 146
+辎 147
+粤 148
+蹴 149
+黄 150
+浞 151
+comp@@ 152
+犀 153
+藏 154
+本 155
+嗖 156
+黻 157
+這 158
+绰 159
+鉏 160
+麼 161
+喆 162
+袪 163
+刚 164
+侦 165
+ic@@ 166
+骧 167
+瓫 168
+柤 169
+桃 170
+鲴 171
+褙 172
+韵 173
+妓 174
+甍 175
+to@@ 176
+轱 177
+塑 178
+坯 179
+貌 180
+n@@ 181
+蕞 182
+疡 183
+伏 184
+酉 185
+暇 186
+霖 187
+了 188
+萸 189
+嘶 190
+nu@@ 191
+挼 192
+by 193
+聩 194
+袓 195
+嶓 196
+桎 197
+抖 198
+攥 199
+鞬 200
+毳 201
+旗 202
+庸 203
+呋 204
+诲 205
+tting 206
+狭 207
+魄 208
+伎 209
+喋 210
+樱 211
+翎 212
+怯 213
+场 214
+睽 215
+盒 216
+times 217
+鲳 218
+爆 219
+绵 220
+皋 221
+尢 222
+嘎 223
+渝 224
+迂 225
+嘁 226
+袷 227
+始 228
+奚 229
+台 230
+禄 231
+挢 232
+座 233
+绀 234
+漱 235
+龃 236
+tu@@ 237
+榫 238
+诛 239
+minu@@ 240
+萼 241
+裛 242
+玠 243
+谵 244
+亳 245
+副 246
+any@@ 247
+诊 248
+唑 249
+頫 250
+斿 251
+赁 252
+骇 253
+训 254
+母 255
+床 256
+微 257
+椰 258
+迢 259
+埃 260
+辏 261
+汩 262
+叟 263
+辔 264
+隙 265
+遵 266
+pp@@ 267
+翕 268
+佬 269
+栖 270
+踉 271
+皕 272
+苏 273
+痂 274
+奋 275
+阄 276
+悌 277
+点 278
+碡 279
+茎 280
+睫 281
+闫 282
+few 283
+籁 284
+孰 285
+拥 286
+for 287
+曾 288
+疲 289
+辞 290
+赖 291
+姆 292
+与 293
+诰 294
+怨 295
+沙 296
+with@@ 297
+睱 298
+谓 299
+晰 300
+嗌 301
+id 302
+aga@@ 303
+実 304
+魏 305
+鲟 306
+寅 307
+滗 308
+珲 309
+腑 310
+冠 311
+夺 312
+娶 313
+宇 314
+侩 315
+筘 316
+an 317
+磺 318
+邛 319
+着 320
+踵 321
+ite 322
+狝 323
+ele@@ 324
+蓼 325
+猿 326
+豆 327
+蔷 328
+沽 329
+去 330
+铥 331
+癀 332
+站 333
+甪 334
+璧 335
+范 336
+哓 337
+菏 338
+龠 339
+岷 340
+嫉 341
+拧 342
+札 343
+戒 344
+琏 345
+绪 346
+澌 347
+楠 348
+莱 349
+cer@@ 350
+here 351
+别 352
+帻 353
+嗉 354
+假 355
+拽 356
+髭 357
+穰 358
+勋 359
+栓 360
+塬 361
+ou@@ 362
+橫 363
+刻 364
+侣 365
+鎉 366
+bre@@ 367
+趸 368
+稳 369
+岌 370
+拎 371
+落 372
+岙 373
+氨 374
+桴 375
+鬶 376
+clu@@ 377
+蚣 378
+肌 379
+讹 380
+骼 381
+忧 382
+雹 383
+0@@ 384
+算 385
+腔 386
+璇 387
+酣 388
+锭 389
+蟾 390
+逦 391
+椟 392
+频 393
+ts 394
+矫 395
+拨 396
+珅 397
+侨 398
+蚨 399
+皯 400
+翛 401
+儋 402
+恳 403
+瀍 404
+敌 405
+砬 406
+奁 407
+耵 408
+烂 409
+绿 410
+缆 411
+辣 412
+time 413
+蔗 414
+too 415
+圾 416
+骞 417
+慥 418
+啶 419
+帔 420
+楢 421
+bi@@ 422
+蚤 423
+浛 424
+ine 425
+綦 426
+old 427
+肩 428
+擎 429
+ling 430
+瘆 431
+娲 432
+prob@@ 433
+暑 434
+鲨 435
+焚 436
+剽 437
+玚 438
+乔 439
+纪 440
+bo@@ 441
+熏 442
+毗 443
+鳙 444
+鞥 445
+絜 446
+糠 447
+菔 448
+廛 449
+谪 450
+冬 451
+遐 452
+衽 453
+ich 454
+柿 455
+峭 456
+渴 457
+亓 458
+荠 459
+蝠 460
+扆 461
+鄞 462
+诎 463
+尙 464
+摸 465
+牙 466
+薤 467
+da@@ 468
+w 469
+呜 470
+陂 471
+磻 472
+匹 473
+while 474
+迁 475
+良 476
+郤 477
+ct@@ 478
+蚕 479
+浥 480
+鲋 481
+腱 482
+pres@@ 483
+氕 484
+颉 485
+夥 486
+ter@@ 487
+左 488
+侉 489
+妍 490
+嚒 491
+殖 492
+私 493
+bas@@ 494
+锱 495
+篪 496
+吻 497
+鄅 498
+鳗 499
+疳 500
+cor@@ 501
+毛 502
+歼 503
+邵 504
+圪 505
+inn 506
+舛 507
+埗 508
+貉 509
+帐 510
+妮 511
+ged 512
+窒 513
+put 514
+诉 515
+堌 516
+气 517
+國 518
+摩 519
+沫 520
+谁 521
+转 522
+语 523
+琵 524
+羊 525
+檠 526
+慎 527
+踮 528
+啾 529
+瞻 530
+山 531
+播 532
+筌 533
+财 534
+飚 535
+苟 536
+扣 537
+mis@@ 538
+桌 539
+侑 540
+jo@@ 541
+ke@@ 542
+冶 543
+滔 544
+蠹 545
+呓 546
+捷 547
+证 548
+崞 549
+阵 550
+掐 551
+劳 552
+皆 553
+巧 554
+肏 555
+肆 556
+葆 557
+檄 558
+画 559
+狨 560
+谧 561
+傑 562
+诱 563
+纭 564
+荥 565
+厥 566
+bri@@ 567
+绁 568
+睇 569
+布 570
+江 571
+噎 572
+灞 573
+鄏 574
+煊 575
+蒹 576
+厮 577
+馓 578
+狴 579
+碍 580
+穷 581
+lar@@ 582
+食 583
+迩 584
+meeting 585
+tter 586
+趣 587
+辜 588
+椁 589
+汽 590
+燠 591
+ate 592
+礴 593
+骈 594
+will 595
+not 596
+煽 597
+嗳 598
+秆 599
+勤 600
+陆 601
+键 602
+墎 603
+官 604
+蘧 605
+酤 606
+唐 607
+颓 608
+仝 609
+iting 610
+田 611
+楫 612
+瑁 613
+鄢 614
+our 615
+诘 616
+venue 617
+霜 618
+镏 619
+痫 620
+娟 621
+婢 622
+埋 623
+汕 624
+铋 625
+徭 626
+隰 627
+猊 628
+卞 629
+慑 630
+said 631
+裘 632
+bus@@ 633
+召 634
+粱 635
+返 636
+缨 637
+纻 638
+磋 639
+炴 640
+凊 641
+曝 642
+兀 643
+洼 644
+杓 645
+榆 646
+hotels 647
+睹 648
+糯 649
+窘 650
+葩 651
+帮 652
+荷 653
+塌 654
+矍 655
+圯 656
+er@@ 657
+蚪 658
+篼 659
+咳 660
+吸 661
+喃 662
+岩 663
+嚟 664
+谱 665
+崆 666
+蟭 667
+wal@@ 668
+姗 669
+谛 670
+东 671
+菱 672
+ction 673
+肫 674
+祥 675
+述 676
+璋 677
+仙 678
+唇 679
+硕 680
+嘉 681
+醒 682
+兔 683
+恒 684
+银 685
+収 686
+cre@@ 687
+弭 688
+曌 689
+螯 690
+苡 691
+疾 692
+鸱 693
+权 694
+搔 695
+途 696
+茂 697
+皑 698
+补 699
+肃 700
+ns 701
+market 702
+讠 703
+阈 704
+机 705
+苒 706
+髙 707
+is@@ 708
+幢 709
+郐 710
+萎 711
+帛 712
+烖 713
+襟 714
+崟 715
+溶 716
+螺 717
+铲 718
+仉 719
+舌 720
+敖 721
+倍 722
+锏 723
+鸦 724
+夌 725
+埌 726
+腰 727
+雠 728
+ol@@ 729
+涙 730
+翳 731
+夹 732
+疏 733
+el 734
+驭 735
+鹯 736
+瑜 737
+薛 738
+奇 739
+娴 740
+靳 741
+镥 742
+伍 743
+暨 744
+what 745
+萧 746
+皴 747
+烦 748
+敷 749
+样 750
+ri@@ 751
+睆 752
+伦 753
+後 754
+祺 755
+贾 756
+汹 757
+箸 758
+掂 759
+械 760
+ob@@ 761
+赜 762
+黎 763
+潵 764
+鲽 765
+牒 766
+罐 767
+inter@@ 768
+冰 769
+成 770
+薳 771
+if 772
+堡 773
+搜 774
+漪 775
+赕 776
+症 777
+参 778
+鲫 779
+克 780
+浴 781
+薹 782
+梫 783
+调 784
+劢 785
+茹 786
+飐 787
+r 788
+her 789
+鲊 790
+妪 791
+惟 792
+榜 793
+谒 794
+梃 795
+twenty 796
+ni@@ 797
+猥 798
+兴 799
+氓 800
+肘 801
+饕 802
+陵 803
+within 804
+轲 805
+cted 806
+単 807
+嚗 808
+唼 809
+inte@@ 810
+钋 811
+抒 812
+郓 813
+牖 814
+蔻 815
+邂 816
+懦 817
+邻 818
+茸 819
+辈 820
+种 821
+黹 822
+hard 823
+悦 824
+综 825
+ved 826
+oo@@ 827
+used 828
+溱 829
+藁 830
+士 831
+猝 832
+芹 833
+char@@ 834
+we 835
+呒 836
+ss@@ 837
+岐 838
+桖 839
+迟 840
+荞 841
+哏 842
+仵 843
+抟 844
+淇 845
+il 846
+电 847
+褀 848
+果 849
+bir@@ 850
+ges 851
+暻 852
+椊 853
+吴 854
+sal@@ 855
+獐 856
+煲 857
+箥 858
+爰 859
+呣 860
+邶 861
+尻 862
+水 863
+sh@@ 864
+fore 865
+mat@@ 866
+浅 867
+枘 868
+稷 869
+艨 870
+唧 871
+can@@ 872
+eng 873
+know 874
+桠 875
+亵 876
+tou@@ 877
+伥 878
+虏 879
+綮 880
+sha@@ 881
+腌 882
+蔓 883
+莶 884
+戬 885
+鋆 886
+酥 887
+璜 888
+阐 889
+贶 890
+远 891
+验 892
+dre@@ 893
+赎 894
+惭 895
+裴 896
+晌 897
+铡 898
+愆 899
+隧 900
+祐 901
+貔 902
+片 903
+al 904
+抾 905
+柰 906
+探 907
+喈 908
+蟮 909
+潎 910
+批 911
+胴 912
+郁 913
+蓿 914
+稿 915
+傲 916
+垒 917
+甙 918
+靼 919
+chic@@ 920
+效 921
+栗 922
+齁 923
+仲 924
+担 925
+har@@ 926
+仟 927
+砂 928
+i 929
+舰 930
+吝 931
+慆 932
+芸 933
+why 934
+tic 935
+哥 936
+佩 937
+铀 938
+妊 939
+x 940
+刍 941
+殄 942
+躬 943
+莙 944
+客 945
+拚 946
+葬 947
+哼 948
+婵 949
+ance 950
+棺 951
+没 952
+崎 953
+曛 954
+宛 955
+斌 956
+掠 957
+醇 958
+庾 959
+黥 960
+乗 961
+旉 962
+铰 963
+寨 964
+钨 965
+沥 966
+gh@@ 967
+吇 968
+嬖 969
+厨 970
+箔 971
+ah 972
+譞 973
+鲖 974
+蒗 975
+銶 976
+角 977
+脏 978
+夬 979
+who 980
+毂 981
+赓 982
+嘢 983
+蹶 984
+駃 985
+avenue 986
+呃 987
+that's 988
+訾 989
+蒜 990
+就 991
+抿 992
+霫 993
+que@@ 994
+牁 995
+叻 996
+绌 997
+捣 998
+埭 999
+蛩 1000
+迤 1001
+ir 1002
+最 1003
+不 1004
+鹚 1005
+钧 1006
+晃 1007
+钍 1008
+岍 1009
+烯 1010
+授 1011
+笨 1012
+馔 1013
+甸 1014
+啰 1015
+赌 1016
+蠼 1017
+荆 1018
+濉 1019
+摹 1020
+剔 1021
+浪 1022
+瓦 1023
+涤 1024
+阬 1025
+eng@@ 1026
+墨 1027
+鲢 1028
+老 1029
+拮 1030
+轿 1031
+弈 1032
+秸 1033
+ken 1034
+省 1035
+穹 1036
+跨 1037
+芤 1038
+剰 1039
+湍 1040
+吥 1041
+喧 1042
+借 1043
+伯 1044
+咋 1045
+噢 1046
+剩 1047
+略 1048
+图 1049
+毕 1050
+爻 1051
+箭 1052
+ans@@ 1053
+no 1054
+缣 1055
+fic@@ 1056
+必 1057
+礻 1058
+视 1059
+侔 1060
+乸 1061
+缎 1062
+比 1063
+殉 1064
+禅 1065
+蹈 1066
+茶 1067
+沔 1068
+腹 1069
+更 1070
+倢 1071
+骑 1072
+俦 1073
+一 1074
+巉 1075
+糌 1076
+there 1077
+笾 1078
+泺 1079
+虫 1080
+随 1081
+室 1082
+谙 1083
+淞 1084
+even 1085
+嘌 1086
+掉 1087
+进 1088
+栈 1089
+隋 1090
+钳 1091
+饲 1092
+裾 1093
+搞 1094
+朽 1095
+嚏 1096
+垱 1097
+倘 1098
+sy@@ 1099
+蒂 1100
+訚 1101
+火 1102
+葱 1103
+踹 1104
+only 1105
+den@@ 1106
+胰 1107
+曦 1108
+汨 1109
+奴 1110
+院 1111
+晶 1112
+臇 1113
+赭 1114
+蚵 1115
+便 1116
+藜 1117
+鍪 1118
+穆 1119
+尿 1120
+find 1121
+偾 1122
+项 1123
+嬅 1124
+济 1125
+area 1126
+皿 1127
+蹽 1128
+af@@ 1129
+曈 1130
+ger 1131
+袭 1132
+温 1133
+包 1134
+惎 1135
+枝 1136
+槁 1137
+跑 1138
+汇 1139
+嫦 1140
+崒 1141
+颇 1142
+丐 1143
+丛 1144
+哠 1145
+鲲 1146
+佯 1147
+疱 1148
+來 1149
+彝 1150
+件 1151
+鸫 1152
+张 1153
+缋 1154
+檎 1155
+港 1156
+尸 1157
+comm@@ 1158
+瘘 1159
+囍 1160
+锅 1161
+惫 1162
+衔 1163
+蔚 1164
+龚 1165
+酱 1166
+ina 1167
+尚 1168
+孪 1169
+蔵 1170
+帧 1171
+弯 1172
+迄 1173
+訇 1174
+恕 1175
+紡 1176
+吱 1177
+觐 1178
+印 1179
+need 1180
+叭 1181
+茫 1182
+汶 1183
+邢 1184
+磅 1185
+焜 1186
+蜣 1187
+米 1188
+俎 1189
+ath 1190
+蛔 1191
+组 1192
+壹 1193
+诈 1194
+ing@@ 1195
+希 1196
+茨 1197
+砧 1198
+has 1199
+蝶 1200
+矛 1201
+拖 1202
+乍 1203
+浇 1204
+another 1205
+输 1206
+朗 1207
+殡 1208
+壶 1209
+灿 1210
+礌 1211
+钡 1212
+瓤 1213
+序 1214
+误 1215
+毖 1216
+静 1217
+鸾 1218
+墚 1219
+璟 1220
+咱 1221
+惘 1222
+化 1223
+腾 1224
+苍 1225
+苼 1226
+七 1227
+芾 1228
+囝 1229
+淄 1230
+馆 1231
+榉 1232
+荸 1233
+摧 1234
+醋 1235
+缦 1236
+帘 1237
+蛋 1238
+曙 1239
+萩 1240
+莉 1241
+犸 1242
+拜 1243
+特 1244
+蕊 1245
+并 1246
+冼 1247
+埝 1248
+茴 1249
+佶 1250
+噶 1251
+ked 1252
+port 1253
+柠 1254
+吶 1255
+竿 1256
+鞧 1257
+糙 1258
+栻 1259
+褂 1260
+杉 1261
+陛 1262
+shi@@ 1263
+朋 1264
+升 1265
+钛 1266
+拭 1267
+walk 1268
+钱 1269
+岸 1270
+衲 1271
+若 1272
+燕 1273
+墩 1274
+戛 1275
+ations 1276
+诳 1277
+冨 1278
+强 1279
+掌 1280
+腺 1281
+淤 1282
+鼍 1283
+妥 1284
+亥 1285
+俵 1286
+鹩 1287
+占 1288
+佤 1289
+棋 1290
+does 1291
+tes 1292
+拒 1293
+劼 1294
+绩 1295
+ren@@ 1296
+货 1297
+g@@ 1298
+深 1299
+钯 1300
+棬 1301
+墟 1302
+疼 1303
+骊 1304
+摅 1305
+祧 1306
+兊 1307
+坠 1308
+int 1309
+use 1310
+泞 1311
+赦 1312
+甾 1313
+葺 1314
+辘 1315
+炆 1316
+旭 1317
+鸯 1318
+茆 1319
+融 1320
+艄 1321
+晖 1322
+钺 1323
+勉 1324
+嘘 1325
+龛 1326
+蕙 1327
+渀 1328
+钞 1329
+写 1330
+弋 1331
+颦 1332
+灌 1333
+埚 1334
+鲷 1335
+亡 1336
+矩 1337
+轰 1338
+a 1339
+单 1340
+觚 1341
+呯 1342
+祏 1343
+rec@@ 1344
+逢 1345
+憧 1346
+蒽 1347
+內 1348
+乡 1349
+鸠 1350
+卜 1351
+庄 1352
+仰 1353
+how 1354
+铓 1355
+踝 1356
+隆 1357
+避 1358
+豌 1359
+low@@ 1360
+ak 1361
+劣 1362
+哺 1363
+头 1364
+proble@@ 1365
+es 1366
+说 1367
+哇 1368
+折 1369
+祝 1370
+偻 1371
+揆 1372
+的 1373
+盎 1374
+初 1375
+骝 1376
+荻 1377
+饷 1378
+耽 1379
+莸 1380
+just 1381
+簰 1382
+现 1383
+pl@@ 1384
+籍 1385
+珉 1386
+蕲 1387
+臌 1388
+闪 1389
+崮 1390
+gra@@ 1391
+琯 1392
+圆 1393
+瓴 1394
+赬 1395
+镧 1396
+被 1397
+共 1398
+芯 1399
+蚧 1400
+stu@@ 1401
+mee@@ 1402
+沧 1403
+伲 1404
+觌 1405
+筏 1406
+庑 1407
+still 1408
+题 1409
+wat@@ 1410
+4 1411
+绱 1412
+入 1413
+亚 1414
+sho@@ 1415
+珫 1416
+饴 1417
+點 1418
+than 1419
+good 1420
+l@@ 1421
+梁 1422
+忿 1423
+荐 1424
+躺 1425
+蹡 1426
+呕 1427
+圩 1428
+唷 1429
+陌 1430
+ue 1431
+鲭 1432
+碗 1433
+怪 1434
+飘 1435
+country 1436
+粑 1437
+怹 1438
+飕 1439
+烨 1440
+吹 1441
+嵇 1442
+驺 1443
+纰 1444
+in@@ 1445
+间 1446
+馈 1447
+榑 1448
+窜 1449
+泗 1450
+硪 1451
+躏 1452
+th 1453
+耸 1454
+贞 1455
+wom@@ 1456
+排 1457
+箩 1458
+绽 1459
+舵 1460
+焉 1461
+振 1462
+镶 1463
+thirty 1464
+闲 1465
+摁 1466
+堰 1467
+牵 1468
+栋 1469
+堤 1470
+馀 1471
+盟 1472
+t 1473
+旄 1474
+凇 1475
+洣 1476
+録 1477
+韭 1478
+por@@ 1479
+孑 1480
+茄 1481
+闺 1482
+淀 1483
+坡 1484
+烟 1485
+洺 1486
+gre@@ 1487
+敦 1488
+哉 1489
+到 1490
+ding 1491
+遑 1492
+钒 1493
+壳 1494
+lo 1495
+纾 1496
+砲 1497
+灶 1498
+lee 1499
+玘 1500
+up 1501
+梵 1502
+旖 1503
+佗 1504
+竽 1505
+绋 1506
+砩 1507
+酒 1508
+苯 1509
+焕 1510
+祚 1511
+苁 1512
+嗓 1513
+ail@@ 1514
+殽 1515
+om@@ 1516
+棨 1517
+翼 1518
+墼 1519
+萄 1520
+垭 1521
+碱 1522
+cts 1523
+渲 1524
+矱 1525
+掇 1526
+best 1527
+锃 1528
+谶 1529
+喜 1530
+雌 1531
+辊 1532
+啀 1533
+嗞 1534
+谢 1535
+疹 1536
+玎 1537
+唤 1538
+兆 1539
+彳 1540
+溧 1541
+丕 1542
+棒 1543
+桁 1544
+樓 1545
+跟 1546
+蝼 1547
+哭 1548
+啭 1549
+替 1550
+乩 1551
+箪 1552
+城 1553
+朾 1554
+ear@@ 1555
+鲌 1556
+ship 1557
+吕 1558
+粉 1559
+舜 1560
+伛 1561
+觏 1562
+燮 1563
+铊 1564
+硝 1565
+撤 1566
+瘝 1567
+thanks 1568
+锵 1569
+圣 1570
+contin@@ 1571
+侬 1572
+浮 1573
+棵 1574
+歭 1575
+ici@@ 1576
+珞 1577
+褔 1578
+券 1579
+演 1580
+箫 1581
+缵 1582
+篾 1583
+鲮 1584
+砒 1585
+含 1586
+郡 1587
+快 1588
+栏 1589
+瘟 1590
+饤 1591
+tw@@ 1592
+拃 1593
+盹 1594
+壕 1595
+桯 1596
+嗪 1597
+鞨 1598
+甏 1599
+锫 1600
+涕 1601
+冕 1602
+鄣 1603
+淌 1604
+辰 1605
+唿 1606
+暲 1607
+蚀 1608
+跋 1609
+郸 1610
+镀 1611
+ku@@ 1612
+赔 1613
+姺 1614
+课 1615
+础 1616
+耷 1617
+涪 1618
+day 1619
+笳 1620
+away 1621
+稞 1622
+鹈 1623
+珍 1624
+毯 1625
+酮 1626
+汀 1627
+梆 1628
+嫫 1629
+准 1630
+ces 1631
+巷 1632
+晋 1633
+肉 1634
+莆 1635
+痢 1636
+缗 1637
+怜 1638
+鄙 1639
+搠 1640
+fri@@ 1641
+仳 1642
+该 1643
+宓 1644
+珂 1645
+圉 1646
+弨 1647
+悬 1648
+buil@@ 1649
+绸 1650
+太 1651
+外 1652
+祢 1653
+蓍 1654
+圹 1655
+侓 1656
+跸 1657
+谊 1658
+获 1659
+髈 1660
+迮 1661
+鹤 1662
+卦 1663
+嗻 1664
+佐 1665
+愠 1666
+媲 1667
+殍 1668
+齉 1669
+妹 1670
+残 1671
+嗄 1672
+钾 1673
+court 1674
+踞 1675
+脯 1676
+菖 1677
+琼 1678
+傻 1679
+三 1680
+虿 1681
+唪 1682
+逶 1683
+鲤 1684
+镌 1685
+肇 1686
+弘 1687
+李 1688
+履 1689
+恩 1690
+蒌 1691
+夙 1692
+环 1693
+坒 1694
+gar@@ 1695
+ans 1696
+嘣 1697
+嵯 1698
+命 1699
+酢 1700
+屏 1701
+鈇 1702
+麟 1703
+旨 1704
+旼 1705
+疮 1706
+with 1707
+解 1708
+屈 1709
+趴 1710
+蠛 1711
+密 1712
+瞩 1713
+屎 1714
+显 1715
+魁 1716
+衯 1717
+钇 1718
+酩 1719
+鳌 1720
+戆 1721
+芈 1722
+十 1723
+gu@@ 1724
+陪 1725
+黑 1726
+缌 1727
+ch@@ 1728
+摇 1729
+梨 1730
+胼 1731
+撷 1732
+疤 1733
+砟 1734
+el@@ 1735
+唛 1736
+芪 1737
+速 1738
+ol 1739
+细 1740
+馥 1741
+犰 1742
+bal@@ 1743
+鲇 1744
+韂 1745
+焰 1746
+胗 1747
+粹 1748
+枌 1749
+嵬 1750
+古 1751
+she 1752
+through 1753
+筛 1754
+翀 1755
+协 1756
+se 1757
+魃 1758
+格 1759
+mes 1760
+晥 1761
+跱 1762
+掺 1763
+阕 1764
+智 1765
+松 1766
+st@@ 1767
+靠 1768
+斟 1769
+粒 1770
+舞 1771
+瀣 1772
+棅 1773
+茭 1774
+韫 1775
+鐎 1776
+灵 1777
+龢 1778
+卷 1779
+lion 1780
+曹 1781
+哒 1782
+皝 1783
+哲 1784
+pe@@ 1785
+患 1786
+逸 1787
+涠 1788
+蛰 1789
+佣 1790
+猇 1791
+狈 1792
+nine 1793
+囫 1794
+风 1795
+态 1796
+慈 1797
+慜 1798
+俨 1799
+汲 1800
+肛 1801
+隶 1802
+坩 1803
+赍 1804
+海 1805
+癫 1806
+my 1807
+委 1808
+ill 1809
+胤 1810
+覩 1811
+臬 1812
+矶 1813
+炷 1814
+衬 1815
+前 1816
+馊 1817
+伽 1818
+艳 1819
+妗 1820
+肠 1821
+檗 1822
+soon 1823
+氙 1824
+琅 1825
+谏 1826
+light 1827
+変 1828
+seven@@ 1829
+旸 1830
+芭 1831
+en@@ 1832
+烧 1833
+诃 1834
+攘 1835
+陧 1836
+觅 1837
+铑 1838
+氐 1839
+余 1840
+night 1841
+hou@@ 1842
+鹀 1843
+膜 1844
+炙 1845
+抨 1846
+珊 1847
+ses 1848
+漩 1849
+both 1850
+桉 1851
+笺 1852
+鎛 1853
+led 1854
+披 1855
+膛 1856
+蜻 1857
+菽 1858
+娼 1859
+団 1860
+揽 1861
+测 1862
+f@@ 1863
+芎 1864
+吅 1865
+sion 1866
+遹 1867
+瓘 1868
+慕 1869
+他 1870
+鄩 1871
+矽 1872
+thou@@ 1873
+沒 1874
+唁 1875
+匿 1876
+设 1877
+嵖 1878
+髹 1879
+ine@@ 1880
+恸 1881
+窣 1882
+-@@ 1883
+街 1884
+膝 1885
+碑 1886
+national 1887
+it@@ 1888
+瘢 1889
+ci@@ 1890
+侮 1891
+l 1892
+陉 1893
+照 1894
+原 1895
+厐 1896
+悚 1897
+答 1898
+犷 1899
+罔 1900
+绘 1901
+敞 1902
+ys 1903
+捆 1904
+殚 1905
+填 1906
+挟 1907
+tal@@ 1908
+萌 1909
+卑 1910
+甃 1911
+吉 1912
+蜮 1913
+帑 1914
+笖 1915
+new 1916
+昺 1917
+诤 1918
+襜 1919
+矗 1920
+藠 1921
+苓 1922
+th@@ 1923
+哝 1924
+its 1925
+蚋 1926
+ran@@ 1927
+澳 1928
+eight 1929
+贱 1930
+傕 1931
+亦 1932
+续 1933
+槐 1934
+筚 1935
+追 1936
+醺 1937
+錾 1938
+蹒 1939
+玟 1940
+look 1941
+圃 1942
+颗 1943
+旎 1944
+圮 1945
+绷 1946
+op@@ 1947
+咙 1948
+槃 1949
+冫 1950
+乳 1951
+鸣 1952
+柴 1953
+蚴 1954
+擞 1955
+锴 1956
+姣 1957
+惯 1958
+管 1959
+奎 1960
+ra@@ 1961
+瞠 1962
+侍 1963
+恵 1964
+岬 1965
+喎 1966
+摭 1967
+卺 1968
+wee@@ 1969
+羧 1970
+cep@@ 1971
+fron@@ 1972
+妁 1973
+很 1974
+禹 1975
+巯 1976
+夼 1977
+鄫 1978
+re@@ 1979
+动 1980
+迓 1981
+狐 1982
+瑕 1983
+棹 1984
+屹 1985
+皈 1986
+陔 1987
+殛 1988
+仿 1989
+蝥 1990
+缘 1991
+镫 1992
+品 1993
+ase 1994
+row 1995
+缜 1996
+stance 1997
+予 1998
+custom@@ 1999
+抬 2000
+鞶 2001
+蛘 2002
+埏 2003
+漂 2004
+凝 2005
+虻 2006
+姒 2007
+痒 2008
+邝 2009
+ss 2010
+战 2011
+悴 2012
+spe@@ 2013
+羝 2014
+吮 2015
+锗 2016
+湘 2017
+端 2018
+淸 2019
+孢 2020
+3 2021
+郴 2022
+卬 2023
+fif@@ 2024
+濯 2025
+射 2026
+簏 2027
+锌 2028
+啖 2029
+懑 2030
+霪 2031
+棻 2032
+簺 2033
+怅 2034
+g 2035
+毎 2036
+犳 2037
+ffe@@ 2038
+镉 2039
+閦 2040
+吲 2041
+驹 2042
+are@@ 2043
+埇 2044
+心 2045
+漴 2046
+娃 2047
+侯 2048
+蔽 2049
+值 2050
+鲧 2051
+fr@@ 2052
+ful 2053
+嘹 2054
+滥 2055
+騠 2056
+ility 2057
+喹 2058
+悉 2059
+嗽 2060
+些 2061
+硁 2062
+mer@@ 2063
+磊 2064
+霆 2065
+麝 2066
+曲 2067
+蜃 2068
+police 2069
+镩 2070
+笪 2071
+苾 2072
+靑 2073
+凼 2074
+多 2075
+质 2076
+缇 2077
+嗫 2078
+沏 2079
+ened 2080
+花 2081
+诹 2082
+尉 2083
+珥 2084
+崩 2085
+ld 2086
+x@@ 2087
+揎 2088
+纷 2089
+缂 2090
+轹 2091
+庙 2092
+渚 2093
+鸪 2094
+乒 2095
+惧 2096
+peop@@ 2097
+歌 2098
+唾 2099
+樘 2100
+膺 2101
+fro@@ 2102
+哂 2103
+腼 2104
+霄 2105
+坞 2106
+霰 2107
+掎 2108
+娿 2109
+镬 2110
+巨 2111
+碇 2112
+藩 2113
+活 2114
+荤 2115
+团 2116
+缪 2117
+钵 2118
+飞 2119
+儡 2120
+苤 2121
+貊 2122
+柄 2123
+蓠 2124
+防 2125
+贮 2126
+碾 2127
+狞 2128
+艏 2129
+喏 2130
+稚 2131
+映 2132
+i'm 2133
+谤 2134
+蜿 2135
+车 2136
+乂 2137
+寕 2138
+啧 2139
+虔 2140
+ster@@ 2141
+垣 2142
+嗛 2143
+讪 2144
+ves 2145
+again 2146
+隗 2147
+帜 2148
+嗾 2149
+绂 2150
+公 2151
+卮 2152
+抱 2153
+仕 2154
+以 2155
+栘 2156
+拊 2157
+萤 2158
+him 2159
+荪 2160
+淬 2161
+7 2162
+鋹 2163
+敢 2164
+颖 2165
+ment 2166
+嫩 2167
+棕 2168
+show 2169
+跩 2170
+out@@ 2171
+汤 2172
+迕 2173
+榨 2174
+暗 2175
+糍 2176
+晡 2177
+9 2178
+稂 2179
+曼 2180
+蒺 2181
+ture 2182
+鬄 2183
+逅 2184
+岚 2185
+芟 2186
+昶 2187
+埤 2188
+幺 2189
+猖 2190
+伙 2191
+pub@@ 2192
+南 2193
+荨 2194
+趁 2195
+淑 2196
+嘲 2197
+悔 2198
+藉 2199
+争 2200
+渔 2201
+pool 2202
+簟 2203
+谀 2204
+噘 2205
+窀 2206
+祟 2207
+阜 2208
+涸 2209
+掖 2210
+癃 2211
+疑 2212
+搢 2213
+漏 2214
+锉 2215
+钹 2216
+耱 2217
+踢 2218
+骎 2219
+稣 2220
+锲 2221
+繇 2222
+缊 2223
+劈 2224
+啻 2225
+蕴 2226
+仔 2227
+昝 2228
+且 2229
+滚 2230
+柢 2231
+镊 2232
+响 2233
+凰 2234
+噗 2235
+瑴 2236
+嗔 2237
+简 2238
+蜇 2239
+有 2240
+豢 2241
+ap@@ 2242
+啓 2243
+翅 2244
+愤 2245
+peri@@ 2246
+蚶 2247
+弄 2248
+禨 2249
+蚡 2250
+坝 2251
+换 2252
+纨 2253
+蹑 2254
+for@@ 2255
+草 2256
+荛 2257
+懈 2258
+奉 2259
+鳊 2260
+疗 2261
+搂 2262
+串 2263
+幸 2264
+岽 2265
+牍 2266
+蝰 2267
+絶 2268
+秣 2269
+缴 2270
+at 2271
+网 2272
+嗑 2273
+岗 2274
+绊 2275
+圳 2276
+恁 2277
+反 2278
+方 2279
+癞 2280
+煞 2281
+雪 2282
+尤 2283
+鐧 2284
+麒 2285
+黡 2286
+殷 2287
+都 2288
+则 2289
+剃 2290
+揄 2291
+毐 2292
+噱 2293
+fi 2294
+氹 2295
+泠 2296
+樾 2297
+迳 2298
+嫚 2299
+齿 2300
+殳 2301
+墒 2302
+役 2303
+晟 2304
+咔 2305
+芃 2306
+睁 2307
+柽 2308
+戍 2309
+屺 2310
+虱 2311
+韦 2312
+涅 2313
+姚 2314
+鋈 2315
+sure 2316
+既 2317
+涯 2318
+甯 2319
+嘤 2320
+硞 2321
+som@@ 2322
+惴 2323
+狻 2324
+堑 2325
+屉 2326
+愿 2327
+li@@ 2328
+行 2329
+谲 2330
+嶂 2331
+峣 2332
+碜 2333
+暂 2334
+h@@ 2335
+鏖 2336
+瘊 2337
+蜈 2338
+浈 2339
+萦 2340
+职 2341
+蚊 2342
+汴 2343
+people 2344
+妱 2345
+鸰 2346
+易 2347
+芜 2348
+挪 2349
+影 2350
+竹 2351
+洸 2352
+烀 2353
+鹘 2354
+胜 2355
+兵 2356
+咧 2357
+楷 2358
+币 2359
+妖 2360
+ant 2361
+臣 2362
+桩 2363
+创 2364
+囹 2365
+na@@ 2366
+鞑 2367
+楂 2368
+逡 2369
+惆 2370
+卿 2371
+闱 2372
+耀 2373
+那 2374
+童 2375
+钰 2376
+玮 2377
+郄 2378
+昏 2379
+乘 2380
+钩 2381
+晳 2382
+笼 2383
+核 2384
+芙 2385
+小 2386
+忋 2387
+区 2388
+as@@ 2389
+颢 2390
+our@@ 2391
+that 2392
+稻 2393
+销 2394
+韶 2395
+刑 2396
+延 2397
+k@@ 2398
+teen 2399
+幄 2400
+pic@@ 2401
+叱 2402
+骷 2403
+棰 2404
+羁 2405
+垝 2406
+犴 2407
+媱 2408
+兄 2409
+尓 2410
+乞 2411
+鲦 2412
+划 2413
+壬 2414
+芡 2415
+hotel 2416
+佃 2417
+氯 2418
+您 2419
+颌 2420
+汝 2421
+缫 2422
+幂 2423
+竣 2424
+喾 2425
+疥 2426
+long 2427
+广 2428
+镂 2429
+酫 2430
+ings 2431
+ood 2432
+柊 2433
+唣 2434
+辽 2435
+稀 2436
+襞 2437
+讼 2438
+篱 2439
+坻 2440
+袂 2441
+华 2442
+自 2443
+歧 2444
+昂 2445
+摺 2446
+gh 2447
+聿 2448
+犟 2449
+敛 2450
+牺 2451
+旳 2452
+锥 2453
+玛 2454
+低 2455
+鄮 2456
+漳 2457
+叠 2458
+川 2459
+呼 2460
+where 2461
+戳 2462
+嗮 2463
+琦 2464
+厓 2465
+窠 2466
+cas@@ 2467
+舷 2468
+甦 2469
+凛 2470
+谖 2471
+旷 2472
+沌 2473
+狒 2474
+溉 2475
+绍 2476
+劲 2477
+滟 2478
+in 2479
+褊 2480
+fam@@ 2481
+楽 2482
+金 2483
+磕 2484
+see 2485
+斩 2486
+佛 2487
+壅 2488
+境 2489
+诂 2490
+around 2491
+羑 2492
+浆 2493
+矜 2494
+铈 2495
+provi@@ 2496
+藐 2497
+伉 2498
+阶 2499
+哀 2500
+潼 2501
+精 2502
+像 2503
+凶 2504
+琇 2505
+秧 2506
+涂 2507
+豫 2508
+镒 2509
+蒟 2510
+叹 2511
+颜 2512
+莫 2513
+阀 2514
+痕 2515
+爬 2516
+嬲 2517
+滓 2518
+牮 2519
+沐 2520
+璈 2521
+窸 2522
+湮 2523
+喊 2524
+徘 2525
+而 2526
+仞 2527
+蛆 2528
+吵 2529
+栟 2530
+郯 2531
+谄 2532
+膑 2533
+垯 2534
+恰 2535
+筠 2536
+淝 2537
+剌 2538
+vie@@ 2539
+估 2540
+first 2541
+渊 2542
+鶗 2543
+缬 2544
+踺 2545
+呦 2546
+宄 2547
+颎 2548
+蔼 2549
+挒 2550
+亹 2551
+墉 2552
+倧 2553
+梪 2554
+猱 2555
+顼 2556
+泫 2557
+鸳 2558
+赠 2559
+聋 2560
+鬲 2561
+隽 2562
+胚 2563
+驱 2564
+丶 2565
+邪 2566
+鲚 2567
+韩 2568
+婆 2569
+sed 2570
+it 2571
+审 2572
+屠 2573
+众 2574
+翩 2575
+铺 2576
+磨 2577
+醲 2578
+瘼 2579
+佑 2580
+霹 2581
+臀 2582
+坮 2583
+俯 2584
+舸 2585
+辍 2586
+谗 2587
+甥 2588
+祭 2589
+tell 2590
+商 2591
+ace 2592
+宾 2593
+骡 2594
+浍 2595
+冉 2596
+肾 2597
+im@@ 2598
+win@@ 2599
+甬 2600
+蹚 2601
+粕 2602
+脖 2603
+遽 2604
+next 2605
+expe@@ 2606
+榕 2607
+蹂 2608
+邹 2609
+stru@@ 2610
+沁 2611
+宸 2612
+旮 2613
+锁 2614
+侂 2615
+拢 2616
+辫 2617
+仁 2618
+be 2619
+洱 2620
+摘 2621
+律 2622
+预 2623
+徕 2624
+鬣 2625
+挠 2626
+戟 2627
+嘴 2628
+杖 2629
+骍 2630
+劵 2631
+哮 2632
+雁 2633
+擀 2634
+鴐 2635
+衎 2636
+芮 2637
+据 2638
+霭 2639
+com@@ 2640
+俗 2641
+伝 2642
+ory 2643
+轭 2644
+博 2645
+谐 2646
+孺 2647
+te@@ 2648
+锹 2649
+瞥 2650
+导 2651
+糜 2652
+堙 2653
+乾 2654
+搌 2655
+鏐 2656
+你 2657
+con@@ 2658
+琍 2659
+art 2660
+徇 2661
+塞 2662
+讽 2663
+瞄 2664
+rence 2665
+溢 2666
+卉 2667
+逞 2668
+阮 2669
+阊 2670
+婊 2671
+mil@@ 2672
+专 2673
+姜 2674
+浉 2675
+府 2676
+sing@@ 2677
+嗵 2678
+哨 2679
+砺 2680
+吋 2681
+闹 2682
+败 2683
+居 2684
+娓 2685
+ce 2686
+囟 2687
+楼 2688
+元 2689
+鲥 2690
+嗙 2691
+tely 2692
+帷 2693
+還 2694
+懋 2695
+欷 2696
+ong 2697
+郝 2698
+丨 2699
+breakfast 2700
+崴 2701
+橼 2702
+停 2703
+沾 2704
+under@@ 2705
+tion@@ 2706
+非 2707
+堵 2708
+仆 2709
+铗 2710
+难 2711
+蛑 2712
+狙 2713
+找 2714
+熠 2715
+over@@ 2716
+檀 2717
+鸩 2718
+檐 2719
+彀 2720
+蟋 2721
+腚 2722
+槟 2723
+泄 2724
+舅 2725
+痼 2726
+秤 2727
+氆 2728
+罄 2729
+啼 2730
+啡 2731
+冽 2732
+疎 2733
+嵨 2734
+吿 2735
+航 2736
+采 2737
+mb@@ 2738
+裟 2739
+檿 2740
+辆 2741
+眍 2742
+溃 2743
+can 2744
+唻 2745
+媖 2746
+佺 2747
+狰 2748
+仪 2749
+rent 2750
+沓 2751
+话 2752
+霾 2753
+婷 2754
+雨 2755
+eigh@@ 2756
+白 2757
+瞧 2758
+澎 2759
+洞 2760
+阔 2761
+ta@@ 2762
+侧 2763
+躇 2764
+莘 2765
+骏 2766
+宰 2767
+縠 2768
+birth 2769
+萃 2770
+men@@ 2771
+秉 2772
+轮 2773
+刹 2774
+fl@@ 2775
+鸮 2776
+忾 2777
+胖 2778
+攫 2779
+磁 2780
+飧 2781
+鲈 2782
+邙 2783
+阌 2784
+皂 2785
+危 2786
+搹 2787
+靺 2788
+唔 2789
+撴 2790
+柝 2791
+垮 2792
+膈 2793
+辋 2794
+榷 2795
+邘 2796
+锂 2797
+戚 2798
+蔹 2799
+粝 2800
+翊 2801
+攵 2802
+悯 2803
+涝 2804
+媞 2805
+俤 2806
+镲 2807
+梳 2808
+蓥 2809
+艾 2810
+guest 2811
+顿 2812
+譬 2813
+兹 2814
+囚 2815
+倌 2816
+遣 2817
+朔 2818
+such 2819
+篷 2820
+囷 2821
+宫 2822
+戊 2823
+嵎 2824
+娉 2825
+箅 2826
+檫 2827
+玷 2828
+please 2829
+彰 2830
+蜓 2831
+怃 2832
+癯 2833
+怛 2834
+镇 2835
+还 2836
+诿 2837
+庞 2838
+开 2839
+节 2840
+卒 2841
+逵 2842
+颔 2843
+杰 2844
+蘸 2845
+楚 2846
+颍 2847
+吐 2848
+堃 2849
+澜 2850
+弧 2851
+流 2852
+堍 2853
+严 2854
+焱 2855
+on 2856
+纬 2857
+巽 2858
+确 2859
+子 2860
+紙 2861
+沭 2862
+戞 2863
+屙 2864
+胭 2865
+劫 2866
+珧 2867
+信 2868
+樵 2869
+讴 2870
+豺 2871
+叽 2872
+钎 2873
+霁 2874
+瀛 2875
+糟 2876
+噌 2877
+豝 2878
+湜 2879
+洎 2880
+菌 2881
+悆 2882
+ree 2883
+凯 2884
+徜 2885
+郏 2886
+today 2887
+勾 2888
+嬉 2889
+螵 2890
+戕 2891
+璞 2892
+忝 2893
+俞 2894
+言 2895
+庵 2896
+贼 2897
+费 2898
+kind 2899
+扁 2900
+骁 2901
+咪 2902
+凿 2903
+讳 2904
+掊 2905
+ated 2906
+苄 2907
+鳝 2908
+噍 2909
+茧 2910
+govern@@ 2911
+筼 2912
+颋 2913
+愛 2914
+渭 2915
+踟 2916
+罪 2917
+汔 2918
+踩 2919
+陽 2920
+疽 2921
+闵 2922
+我 2923
+蒡 2924
+缠 2925
+曺 2926
+婪 2927
+农 2928
+露 2929
+染 2930
+sent 2931
+氽 2932
+et 2933
+咷 2934
+圧 2935
+咀 2936
+site 2937
+sti@@ 2938
+梗 2939
+water 2940
+舔 2941
+嚣 2942
+蜉 2943
+逖 2944
+湄 2945
+栅 2946
+刳 2947
+薢 2948
+ally 2949
+诸 2950
+藕 2951
+钔 2952
+伋 2953
+莜 2954
+硬 2955
+窟 2956
+sa@@ 2957
+愚 2958
+蟪 2959
+秩 2960
+雯 2961
+褚 2962
+鹎 2963
+泃 2964
+ner 2965
+ast 2966
+菜 2967
+晦 2968
+枨 2969
+偲 2970
+嚩 2971
+遴 2972
+su@@ 2973
+掸 2974
+千 2975
+馄 2976
+功 2977
+胺 2978
+rep@@ 2979
+涡 2980
+ther 2981
+孩 2982
+液 2983
+狲 2984
+业 2985
+巡 2986
+脍 2987
+甚 2988
+珜 2989
+郜 2990
+蔑 2991
+疔 2992
+庚 2993
+硌 2994
+裉 2995
+骘 2996
+sequ@@ 2997
+迎 2998
+盖 2999
+噪 3000
+尺 3001
+咒 3002
+蜕 3003
+店 3004
+镐 3005
+蝉 3006
+宝 3007
+卍 3008
+弩 3009
+学 3010
+猁 3011
+犊 3012
+妄 3013
+葭 3014
+every@@ 3015
+螋 3016
+馃 3017
+ating 3018
+壮 3019
+熟 3020
+rela@@ 3021
+嗬 3022
+约 3023
+锞 3024
+呫 3025
+护 3026
+磒 3027
+疙 3028
+羞 3029
+绦 3030
+铳 3031
+掕 3032
+宗 3033
+荀 3034
+玢 3035
+ser@@ 3036
+啦 3037
+氪 3038
+盯 3039
+疸 3040
+鬐 3041
+绚 3042
+锡 3043
+鬻 3044
+瓮 3045
+麸 3046
+旱 3047
+娱 3048
+敕 3049
+跄 3050
+烘 3051
+蠕 3052
+te 3053
+诽 3054
+重 3055
+翠 3056
+珑 3057
+慰 3058
+鲍 3059
+勣 3060
+袱 3061
+瑙 3062
+tly 3063
+庆 3064
+government 3065
+荦 3066
+阗 3067
+烫 3068
+倓 3069
+俏 3070
+鸹 3071
+倦 3072
+ound 3073
+co@@ 3074
+竟 3075
+腋 3076
+昙 3077
+濂 3078
+啋 3079
+揶 3080
+泣 3081
+郾 3082
+垍 3083
+轳 3084
+某 3085
+酎 3086
+板 3087
+晤 3088
+廑 3089
+奶 3090
+醴 3091
+镑 3092
+讣 3093
+缤 3094
+龅 3095
+畿 3096
+脁 3097
+ma@@ 3098
+醢 3099
+嗟 3100
+丗 3101
+殿 3102
+魅 3103
+熨 3104
+wr@@ 3105
+嚷 3106
+彤 3107
+栎 3108
+americ@@ 3109
+谡 3110
+泽 3111
+柬 3112
+髎 3113
+盆 3114
+诅 3115
+瘁 3116
+萘 3117
+喁 3118
+媒 3119
+忸 3120
+阍 3121
+曡 3122
+裰 3123
+锦 3124
+something 3125
+犋 3126
+爱 3127
+煺 3128
+揉 3129
+苇 3130
+嘈 3131
+胪 3132
+铁 3133
+屁 3134
+颂 3135
+锩 3136
+骅 3137
+渌 3138
+邱 3139
+脬 3140
+滏 3141
+罹 3142
+散 3143
+鼹 3144
+父 3145
+摔 3146
+边 3147
+申 3148
+苛 3149
+敝 3150
+冈 3151
+蕈 3152
+郕 3153
+耨 3154
+闳 3155
+逄 3156
+拔 3157
+将 3158
+fe 3159
+鲂 3160
+颙 3161
+0 3162
+room 3163
+胙 3164
+澍 3165
+媚 3166
+廪 3167
+量 3168
+贩 3169
+镕 3170
+nine@@ 3171
+恚 3172
+鹾 3173
+旃 3174
+铵 3175
+堺 3176
+剑 3177
+ket 3178
+支 3179
+墙 3180
+洒 3181
+俳 3182
+ors 3183
+诐 3184
+黟 3185
+珩 3186
+跻 3187
+浸 3188
+孟 3189
+mp@@ 3190
+狷 3191
+踌 3192
+渎 3193
+逝 3194
+颁 3195
+务 3196
+羹 3197
+羖 3198
+阽 3199
+跪 3200
+褒 3201
+乜 3202
+择 3203
+盩 3204
+鸭 3205
+抗 3206
+递 3207
+褪 3208
+怂 3209
+cou@@ 3210
+蟠 3211
+cen@@ 3212
+傜 3213
+砍 3214
+鲔 3215
+蹙 3216
+restaurant 3217
+杆 3218
+茵 3219
+尊 3220
+耻 3221
+淮 3222
+躜 3223
+蜡 3224
+嘱 3225
+谩 3226
+蝗 3227
+堞 3228
+姨 3229
+happ@@ 3230
+铿 3231
+楝 3232
+park 3233
+力 3234
+殊 3235
+畹 3236
+say 3237
+练 3238
+纱 3239
+溪 3240
+虮 3241
+篑 3242
+蜱 3243
+惜 3244
+跌 3245
+啁 3246
+溜 3247
+饼 3248
+裥 3249
+勇 3250
+柱 3251
+惨 3252
+陟 3253
+殪 3254
+安 3255
+徼 3256
+纛 3257
+痍 3258
+谨 3259
+取 3260
+犒 3261
+鄄 3262
+粟 3263
+眨 3264
+坤 3265
+妤 3266
+剟 3267
+雉 3268
+傩 3269
+嫄 3270
+嘧 3271
+咽 3272
+愀 3273
+簸 3274
+赏 3275
+箾 3276
+龈 3277
+放 3278
+菅 3279
+坚 3280
+奠 3281
+黢 3282
+琴 3283
+潞 3284
+朝 3285
+tually 3286
+铞 3287
+乇 3288
+醪 3289
+潍 3290
+槊 3291
+纯 3292
+瀹 3293
+诋 3294
+慢 3295
+奸 3296
+嘭 3297
+揠 3298
+昃 3299
+革 3300
+司 3301
+ce@@ 3302
+趵 3303
+醮 3304
+碘 3305
+器 3306
+澶 3307
+知 3308
+& 3309
+厢 3310
+啷 3311
+晔 3312
+炜 3313
+ook 3314
+斐 3315
+盏 3316
+妻 3317
+娣 3318
+燋 3319
+窈 3320
+法 3321
+also 3322
+菸 3323
+炽 3324
+选 3325
+埕 3326
+击 3327
+滤 3328
+铖 3329
+觎 3330
+莩 3331
+le@@ 3332
+匏 3333
+踅 3334
+酸 3335
+鹑 3336
+enjoy 3337
+闰 3338
+毡 3339
+祆 3340
+身 3341
+郊 3342
+笄 3343
+乖 3344
+甩 3345
+like 3346
+否 3347
+厖 3348
+爹 3349
+牦 3350
+起 3351
+僔 3352
+钓 3353
+浚 3354
+忙 3355
+get 3356
+冯 3357
+樊 3358
+识 3359
+蹉 3360
+鑫 3361
+畚 3362
+滕 3363
+掮 3364
+尾 3365
+war@@ 3366
+桶 3367
+瓢 3368
+毫 3369
+膊 3370
+髌 3371
+钗 3372
+桼 3373
+碲 3374
+辨 3375
+唉 3376
+竲 3377
+痰 3378
+膻 3379
+锖 3380
+嘡 3381
+雩 3382
+版 3383
+昧 3384
+敬 3385
+蕨 3386
+伴 3387
+徍 3388
+襀 3389
+盘 3390
+憷 3391
+涑 3392
+different 3393
+after 3394
+虽 3395
+何 3396
+煎 3397
+宽 3398
+ori@@ 3399
+攀 3400
+冢 3401
+零 3402
+樯 3403
+哌 3404
+瓣 3405
+馍 3406
+唰 3407
+炪 3408
+旰 3409
+厔 3410
+葵 3411
+痞 3412
+己 3413
+靓 3414
+凄 3415
+服 3416
+烤 3417
+仮 3418
+恨 3419
+喳 3420
+贯 3421
+郞 3422
+饪 3423
+铄 3424
+滠 3425
+蠢 3426
+薅 3427
+齑 3428
+褓 3429
+黼 3430
+涿 3431
+n't 3432
+磬 3433
+匾 3434
+沂 3435
+镳 3436
+nice 3437
+浯 3438
+悝 3439
+淫 3440
+捅 3441
+箨 3442
+瞀 3443
+勠 3444
+屐 3445
+蹁 3446
+蹦 3447
+槿 3448
+类 3449
+栉 3450
+脘 3451
+页 3452
+桕 3453
+脊 3454
+欲 3455
+蝽 3456
+勃 3457
+坷 3458
+酶 3459
+售 3460
+縻 3461
+欺 3462
+膏 3463
+词 3464
+兢 3465
+楸 3466
+娡 3467
+娩 3468
+陡 3469
+ple 3470
+尧 3471
+幷 3472
+豉 3473
+桫 3474
+滁 3475
+麋 3476
+罘 3477
+朕 3478
+耗 3479
+汉 3480
+登 3481
+较 3482
+逾 3483
+蔫 3484
+赳 3485
+秭 3486
+咫 3487
+斑 3488
+跚 3489
+舒 3490
+莞 3491
+闾 3492
+氤 3493
+骸 3494
+槩 3495
+瓶 3496
+餐 3497
+瞪 3498
+沉 3499
+朱 3500
+had 3501
+鲣 3502
+嵪 3503
+nothing 3504
+嫡 3505
+恍 3506
+衢 3507
+轴 3508
+杈 3509
+赂 3510
+津 3511
+red 3512
+拤 3513
+狠 3514
+卣 3515
+蚱 3516
+疆 3517
+捞 3518
+婉 3519
+固 3520
+梶 3521
+垸 3522
+逋 3523
+髡 3524
+晓 3525
+骶 3526
+季 3527
+炀 3528
+喷 3529
+垛 3530
+蜂 3531
+sts 3532
+阝 3533
+未 3534
+熬 3535
+绲 3536
+坟 3537
+苈 3538
+are 3539
+寮 3540
+吧 3541
+皖 3542
+捌 3543
+炤 3544
+b 3545
+the 3546
+哈 3547
+鞍 3548
+ir@@ 3549
+掘 3550
+tation 3551
+噙 3552
+酊 3553
+忄 3554
+硼 3555
+耕 3556
+偎 3557
+雎 3558
+磴 3559
+锺 3560
+over 3561
+侃 3562
+婚 3563
+吗 3564
+竺 3565
+man 3566
+也 3567
+苣 3568
+绛 3569
+冤 3570
+呈 3571
+孱 3572
+缭 3573
+埼 3574
+犬 3575
+麦 3576
+蓐 3577
+技 3578
+夜 3579
+tri@@ 3580
+杭 3581
+佝 3582
+莨 3583
+di@@ 3584
+毙 3585
+贿 3586
+猎 3587
+桡 3588
+so 3589
+氖 3590
+叼 3591
+哚 3592
+濠 3593
+湾 3594
+全 3595
+读 3596
+盐 3597
+钚 3598
+鬘 3599
+万 3600
+鲠 3601
+貂 3602
+鋐 3603
+堼 3604
+茺 3605
+拟 3606
+牡 3607
+蝮 3608
+镢 3609
+嚯 3610
+束 3611
+喱 3612
+彿 3613
+col@@ 3614
+仃 3615
+涌 3616
+蚯 3617
+妩 3618
+箢 3619
+隹 3620
+亰 3621
+疃 3622
+嬗 3623
+喇 3624
+攒 3625
+am 3626
+听 3627
+fe@@ 3628
+zero 3629
+痛 3630
+诗 3631
+干 3632
+疫 3633
+嬴 3634
+降 3635
+签 3636
+丈 3637
+sm@@ 3638
+髑 3639
+劬 3640
+萏 3641
+诀 3642
+镵 3643
+坭 3644
+咤 3645
+池 3646
+榼 3647
+岕 3648
+崃 3649
+ined 3650
+chu@@ 3651
+糸 3652
+祸 3653
+猜 3654
+婿 3655
+搋 3656
+咛 3657
+箴 3658
+辚 3659
+悄 3660
+荒 3661
+挑 3662
+托 3663
+drive 3664
+撇 3665
+莒 3666
+鄜 3667
+冒 3668
+稹 3669
+户 3670
+ould 3671
+铤 3672
+翃 3673
+try 3674
+圊 3675
+艿 3676
+桐 3677
+兒 3678
+揿 3679
+堋 3680
+档 3681
+ink 3682
+email 3683
+犍 3684
+铍 3685
+招 3686
+鳇 3687
+敲 3688
+雳 3689
+奌 3690
+裝 3691
+戏 3692
+哗 3693
+栱 3694
+哆 3695
+ds 3696
+浙 3697
+岂 3698
+挝 3699
+莲 3700
+腩 3701
+杬 3702
+促 3703
+斥 3704
+蛱 3705
+诓 3706
+炖 3707
+璘 3708
+怵 3709
+礽 3710
+咬 3711
+珐 3712
+韡 3713
+邺 3714
+祀 3715
+皇 3716
+渑 3717
+困 3718
+潜 3719
+添 3720
+ter 3721
+che@@ 3722
+如 3723
+萹 3724
+熙 3725
+扇 3726
+亭 3727
+亍 3728
+瘗 3729
+舍 3730
+皌 3731
+诌 3732
+虢 3733
+欣 3734
+掰 3735
+棘 3736
+岞 3737
+毽 3738
+卵 3739
+罴 3740
+疋 3741
+ali@@ 3742
+整 3743
+i@@ 3744
+紫 3745
+mu@@ 3746
+涘 3747
+携 3748
+奏 3749
+cri@@ 3750
+馘 3751
+翘 3752
+抚 3753
+筝 3754
+玄 3755
+霎 3756
+铮 3757
+澹 3758
+嫰 3759
+夏 3760
+咿 3761
+围 3762
+従 3763
+猾 3764
+pre@@ 3765
+逊 3766
+糖 3767
+漫 3768
+聍 3769
+及 3770
+瘵 3771
+绢 3772
+棱 3773
+笈 3774
+铯 3775
+釭 3776
+恼 3777
+癎 3778
+苢 3779
+斁 3780
+醣 3781
+植 3782
+鱼 3783
+涓 3784
+z 3785
+目 3786
+par@@ 3787
+撬 3788
+戎 3789
+偓 3790
+已 3791
+打 3792
+busine@@ 3793
+桦 3794
+庇 3795
+坪 3796
+problem 3797
+舾 3798
+centr@@ 3799
+fifty 3800
+梧 3801
+𫖯 3802
+restaurants 3803
+beau@@ 3804
+fac@@ 3805
+谈 3806
+腙 3807
+阉 3808
+孛 3809
+秘 3810
+庶 3811
+畴 3812
+例 3813
+ang 3814
+幛 3815
+下 3816
+溅 3817
+彗 3818
+魟 3819
+诏 3820
+鸢 3821
+邽 3822
+瘪 3823
+ay 3824
+胱 3825
+勒 3826
+槲 3827
+橹 3828
+男 3829
+ide@@ 3830
+治 3831
+锒 3832
+祯 3833
+慌 3834
+佥 3835
+苔 3836
+暴 3837
+时 3838
+撩 3839
+俶 3840
+屋 3841
+抠 3842
+嫘 3843
+浔 3844
+鲻 3845
+暌 3846
+窿 3847
+炒 3848
+溇 3849
+髯 3850
+簿 3851
+嶖 3852
+峒 3853
+si@@ 3854
+觥 3855
+午 3856
+联 3857
+ty 3858
+猫 3859
+腭 3860
+晻 3861
+袝 3862
+戾 3863
+波 3864
+啐 3865
+戈 3866
+蒎 3867
+麓 3868
+汾 3869
+茉 3870
+埔 3871
+蛳 3872
+徳 3873
+鸶 3874
+艇 3875
+顶 3876
+髻 3877
+徒 3878
+豨 3879
+碳 3880
+欻 3881
+薰 3882
+hi 3883
+呲 3884
+舳 3885
+劾 3886
+形 3887
+' 3888
+渥 3889
+羔 3890
+枇 3891
+模 3892
+弁 3893
+坏 3894
+徊 3895
+馗 3896
+刨 3897
+璀 3898
+锤 3899
+钤 3900
+囔 3901
+拦 3902
+剐 3903
+揸 3904
+滈 3905
+缚 3906
+ach@@ 3907
+斯 3908
+诒 3909
+寂 3910
+裒 3911
+同 3912
+should 3913
+蒴 3914
+窑 3915
+示 3916
+戗 3917
+录 3918
+恶 3919
+培 3920
+迦 3921
+邮 3922
+熜 3923
+泊 3924
+乌 3925
+篌 3926
+center 3927
+癸 3928
+昔 3929
+牠 3930
+濛 3931
+噻 3932
+苋 3933
+ang@@ 3934
+give 3935
+姥 3936
+祖 3937
+搬 3938
+悠 3939
+瓒 3940
+嫱 3941
+wit@@ 3942
+畏 3943
+神 3944
+湟 3945
+扃 3946
+桀 3947
+醭 3948
+谘 3949
+虾 3950
+玳 3951
+斡 3952
+寥 3953
+攻 3954
+忑 3955
+man@@ 3956
+僻 3957
+money 3958
+飊 3959
+国 3960
+吠 3961
+扦 3962
+j@@ 3963
+幹 3964
+泛 3965
+摈 3966
+匼 3967
+廉 3968
+通 3969
+袍 3970
+楹 3971
+搦 3972
+缮 3973
+ft 3974
+岱 3975
+scho@@ 3976
+丹 3977
+蜗 3978
+蓓 3979
+卟 3980
+倥 3981
+喽 3982
+蛾 3983
+殭 3984
+绣 3985
+chan@@ 3986
+震 3987
+棍 3988
+潋 3989
+葫 3990
+嚎 3991
+ed 3992
+漉 3993
+阻 3994
+俐 3995
+德 3996
+様 3997
+酝 3998
+倒 3999
+橱 4000
+send 4001
+谯 4002
+嗐 4003
+署 4004
+贴 4005
+搭 4006
+坦 4007
+蹯 4008
+緛 4009
+队 4010
+死 4011
+閟 4012
+岭 4013
+倪 4014
+诜 4015
+偱 4016
+醐 4017
+痊 4018
+社 4019
+凡 4020
+畯 4021
+摒 4022
+迭 4023
+措 4024
+挌 4025
+媛 4026
+same 4027
+蚓 4028
+擤 4029
+澈 4030
+眈 4031
+剪 4032
+嫣 4033
+鹱 4034
+娆 4035
+凸 4036
+厅 4037
+臼 4038
+枭 4039
+炎 4040
+烊 4041
+掀 4042
+洄 4043
+僳 4044
+velo@@ 4045
+觉 4046
+逼 4047
+have 4048
+皎 4049
+酯 4050
+怦 4051
+叶 4052
+统 4053
+thir@@ 4054
+从 4055
+纡 4056
+les 4057
+术 4058
+径 4059
+征 4060
+come 4061
+躅 4062
+此 4063
+甜 4064
+践 4065
+br@@ 4066
+鎸 4067
+讲 4068
+玲 4069
+衣 4070
+麾 4071
+枥 4072
+拈 4073
+腮 4074
+reas@@ 4075
+芩 4076
+鹖 4077
+more 4078
+儆 4079
+愕 4080
+淏 4081
+躄 4082
+玕 4083
+届 4084
+永 4085
+哟 4086
+虓 4087
+sit@@ 4088
+宴 4089
+夷 4090
+梭 4091
+紧 4092
+瘰 4093
+recei@@ 4094
+匐 4095
+号 4096
+from 4097
+况 4098
+畋 4099
+谜 4100
+莽 4101
+锣 4102
+pas@@ 4103
+驽 4104
+昱 4105
+喟 4106
+v@@ 4107
+呀 4108
+'re 4109
+阒 4110
+踬 4111
+认 4112
+订 4113
+黝 4114
+筻 4115
+seven 4116
+们 4117
+舱 4118
+揭 4119
+妾 4120
+礼 4121
+高 4122
+传 4123
+香 4124
+谬 4125
+篆 4126
+逮 4127
+玑 4128
+眄 4129
+驿 4130
+all@@ 4131
+嗅 4132
+五 4133
+淹 4134
+裨 4135
+咕 4136
+焖 4137
+氚 4138
+ari@@ 4139
+崤 4140
+接 4141
+沈 4142
+寰 4143
+轻 4144
+旒 4145
+维 4146
+test 4147
+麴 4148
+枱 4149
+挚 4150
+句 4151
+驾 4152
+筷 4153
+坂 4154
+须 4155
+鼩 4156
+炳 4157
+推 4158
+姿 4159
+溴 4160
+庠 4161
+箻 4162
+燹 4163
+拂 4164
+呱 4165
+愫 4166
+袤 4167
+睑 4168
+眩 4169
+冷 4170
+葸 4171
+but 4172
+睢 4173
+掣 4174
+唠 4175
+陈 4176
+喉 4177
+晬 4178
+溯 4179
+艺 4180
+苦 4181
+腐 4182
+蟹 4183
+燃 4184
+候 4185
+伸 4186
+萝 4187
+汭 4188
+6@@ 4189
+僭 4190
+蘖 4191
+瘫 4192
+书 4193
+控 4194
+乪 4195
+溘 4196
+痦 4197
+lo@@ 4198
+蝣 4199
+啂 4200
+缩 4201
+part@@ 4202
+只 4203
+施 4204
+鸨 4205
+鎏 4206
+悕 4207
+诵 4208
+孬 4209
+獾 4210
+玩 4211
+汪 4212
+ary 4213
+吁 4214
+拄 4215
+诟 4216
+扰 4217
+鼎 4218
+珪 4219
+橇 4220
+隍 4221
+close 4222
+姑 4223
+ty@@ 4224
+迹 4225
+骆 4226
+崐 4227
+go@@ 4228
+勺 4229
+倜 4230
+ble 4231
+盥 4232
+剂 4233
+qu@@ 4234
+圻 4235
+荧 4236
+荣 4237
+疵 4238
+酪 4239
+稗 4240
+幔 4241
+井 4242
+蛭 4243
+泔 4244
+定 4245
+q 4246
+楱 4247
+刈 4248
+使 4249
+潟 4250
+螅 4251
+铒 4252
+巇 4253
+呖 4254
+垂 4255
+啉 4256
+戮 4257
+佉 4258
+寓 4259
+翚 4260
+啪 4261
+fa@@ 4262
+w@@ 4263
+绐 4264
+抛 4265
+謷 4266
+忐 4267
+趿 4268
+位 4269
+螨 4270
+last 4271
+迫 4272
+爷 4273
+碎 4274
+廊 4275
+啮 4276
+柯 4277
+酰 4278
+烜 4279
+煳 4280
+粲 4281
+求 4282
+楞 4283
+考 4284
+挲 4285
+触 4286
+荽 4287
+荫 4288
+疴 4289
+遨 4290
+仫 4291
+瑭 4292
+扩 4293
+恭 4294
+藦 4295
+羣 4296
+味 4297
+缀 4298
+享 4299
+枣 4300
+宠 4301
+浊 4302
+back 4303
+扪 4304
+篥 4305
+sou@@ 4306
+涉 4307
+son 4308
+翔 4309
+讫 4310
+理 4311
+玙 4312
+栩 4313
+闩 4314
+龄 4315
+亢 4316
+桄 4317
+sil 4318
+幡 4319
+婀 4320
+脚 4321
+疠 4322
+歔 4323
+兰 4324
+査 4325
+镗 4326
+涎 4327
+霏 4328
+待 4329
+忻 4330
+警 4331
+科 4332
+脐 4333
+琚 4334
+真 4335
+own 4336
+醚 4337
+绡 4338
+ices 4339
+嶷 4340
+胶 4341
+u@@ 4342
+峦 4343
+箜 4344
+丰 4345
+痨 4346
+衡 4347
+锋 4348
+宦 4349
+皦 4350
+感 4351
+矮 4352
+爨 4353
+亟 4354
+装 4355
+鹁 4356
+房 4357
+捡 4358
+乐 4359
+胫 4360
+彧 4361
+kil@@ 4362
+葛 4363
+氏 4364
+捋 4365
+戢 4366
+牢 4367
+5 4368
+dis@@ 4369
+园 4370
+孓 4371
+柃 4372
+榄 4373
+喺 4374
+悛 4375
+俢 4376
+c 4377
+缢 4378
+富 4379
+je@@ 4380
+竭 4381
+way 4382
+拙 4383
+牧 4384
+阋 4385
+究 4386
+钘 4387
+濮 4388
+皙 4389
+问 4390
+告 4391
+鞅 4392
+燚 4393
+肴 4394
+螭 4395
+篁 4396
+当 4397
+洮 4398
+渠 4399
+码 4400
+辗 4401
+慨 4402
+崇 4403
+诫 4404
+意 4405
+姤 4406
+飓 4407
+噬 4408
+铐 4409
+寡 4410
+咨 4411
+伺 4412
+殓 4413
+容 4414
+蚁 4415
+柏 4416
+枧 4417
+瑗 4418
+挛 4419
+绔 4420
+月 4421
+绾 4422
+隘 4423
+僖 4424
+纇 4425
+握 4426
+耜 4427
+舐 4428
+tom@@ 4429
+旅 4430
+翮 4431
+products 4432
+缑 4433
+肄 4434
+闿 4435
+硫 4436
+砚 4437
+蜚 4438
+bu@@ 4439
+鍉 4440
+晗 4441
+阃 4442
+衾 4443
+蝴 4444
+丽 4445
+髅 4446
+煜 4447
+习 4448
+窃 4449
+芑 4450
+櫈 4451
+ru@@ 4452
+deta@@ 4453
+谌 4454
+俄 4455
+桨 4456
+芦 4457
+泵 4458
+企 4459
+粽 4460
+揣 4461
+领 4462
+qui@@ 4463
+鄚 4464
+肱 4465
+尼 4466
+滦 4467
+椽 4468
+癜 4469
+甄 4470
+local 4471
+fol@@ 4472
+偿 4473
+丸 4474
+涞 4475
+铜 4476
+du@@ 4477
+噜 4478
+倬 4479
+珣 4480
+圜 4481
+墅 4482
+car@@ 4483
+撂 4484
+栀 4485
+评 4486
+螽 4487
+蘑 4488
+扬 4489
+挨 4490
+轵 4491
+殇 4492
+酺 4493
+high 4494
+ach 4495
+鲛 4496
+啥 4497
+think 4498
+don't 4499
+表 4500
+cho@@ 4501
+盈 4502
+啊 4503
+怔 4504
+滉 4505
+艮 4506
+徽 4507
+撸 4508
+淯 4509
+茈 4510
+硚 4511
+桢 4512
+魍 4513
+潸 4514
+at@@ 4515
+仄 4516
+鹭 4517
+沅 4518
+操 4519
+炭 4520
+砝 4521
+之 4522
+讷 4523
+吔 4524
+疰 4525
+葚 4526
+夭 4527
+跶 4528
+蘼 4529
+鲩 4530
+滴 4531
+菘 4532
+滨 4533
+加 4534
+倾 4535
+体 4536
+茕 4537
+髀 4538
+雅 4539
+ile 4540
+欠 4541
+崧 4542
+囤 4543
+僦 4544
+守 4545
+辂 4546
+棂 4547
+se@@ 4548
+捶 4549
+扛 4550
+铠 4551
+铩 4552
+京 4553
+熘 4554
+洛 4555
+娄 4556
+the@@ 4557
+痿 4558
+箱 4559
+驮 4560
+礅 4561
+郃 4562
+谣 4563
+ack@@ 4564
+晴 4565
+res 4566
+lot 4567
+纲 4568
+诖 4569
+ways 4570
+谋 4571
+煌 4572
+绗 4573
+炼 4574
+卤 4575
+屄 4576
+界 4577
+拶 4578
+ar 4579
+遠 4580
+周 4581
+驩 4582
+肖 4583
+跖 4584
+莳 4585
+级 4586
+咣 4587
+hu@@ 4588
+啕 4589
+歀 4590
+忤 4591
+临 4592
+many 4593
+募 4594
+ned 4595
+their 4596
+睿 4597
+冥 4598
+久 4599
+巍 4600
+堇 4601
+洽 4602
+郎 4603
+董 4604
+this 4605
+毒 4606
+醉 4607
+ton 4608
+诶 4609
+旺 4610
+蛉 4611
+规 4612
+琶 4613
+騑 4614
+捉 4615
+imp@@ 4616
+尥 4617
+拗 4618
+e@@ 4619
+歆 4620
+刀 4621
+跆 4622
+three 4623
+锢 4624
+侏 4625
+拳 4626
+扤 4627
+饨 4628
+剥 4629
+潡 4630
+俊 4631
+嗤 4632
+穑 4633
+地 4634
+情 4635
+憔 4636
+惹 4637
+奢 4638
+inclu@@ 4639
+mo@@ 4640
+铭 4641
+至 4642
+浐 4643
+祎 4644
+really 4645
+帆 4646
+dri@@ 4647
+楮 4648
+邬 4649
+弛 4650
+篮 4651
+兮 4652
+up@@ 4653
+慧 4654
+腆 4655
+碴 4656
+挂 4657
+计 4658
+豚 4659
+滑 4660
+indi@@ 4661
+into 4662
+褭 4663
+酞 4664
+t@@ 4665
+符 4666
+彼 4667
+涔 4668
+body 4669
+ins 4670
+漶 4671
+峪 4672
+down 4673
+缓 4674
+剀 4675
+福 4676
+叮 4677
+wa@@ 4678
+失 4679
+隅 4680
+怄 4681
+扳 4682
+great 4683
+泰 4684
+娌 4685
+孙 4686
+彬 4687
+毁 4688
+蛲 4689
+萱 4690
+泻 4691
+舀 4692
+dge 4693
+瞅 4694
+vo@@ 4695
+咆 4696
+祜 4697
+枋 4698
+憩 4699
+文 4700
+gro@@ 4701
+倮 4702
+鹄 4703
+all 4704
+槎 4705
+忆 4706
+絯 4707
+裢 4708
+two 4709
+兑 4710
+明 4711
+遥 4712
+窍 4713
+吃 4714
+it's 4715
+启 4716
+妽 4717
+郅 4718
+ies 4719
+centre 4720
+吒 4721
+亿 4722
+雲 4723
+ask 4724
+叩 4725
+媜 4726
+蚰 4727
+奄 4728
+垡 4729
+椿 4730
+篡 4731
+聘 4732
+窖 4733
+垩 4734
+diffe@@ 4735
+购 4736
+褥 4737
+豸 4738
+腠 4739
+咦 4740
+眦 4741
+敏 4742
+鼐 4743
+昉 4744
+顽 4745
+琊 4746
+砌 4747
+僮 4748
+乱 4749
+瞒 4750
+撑 4751
+羲 4752
+纳 4753
+赞 4754
+焐 4755
+橛 4756
+嵩 4757
+陀 4758
+楔 4759
+牛 4760
+鹊 4761
+帽 4762
+may 4763
+偬 4764
+丫 4765
+兜 4766
+砰 4767
+濡 4768
+獬 4769
+好 4770
+腿 4771
+靶 4772
+木 4773
+唸 4774
+纹 4775
+裹 4776
+cl@@ 4777
+hund@@ 4778
+优 4779
+猲 4780
+焯 4781
+岘 4782
+辱 4783
+丞 4784
+查 4785
+秽 4786
+胀 4787
+鳖 4788
+岈 4789
+喒 4790
+默 4791
+见 4792
+forty 4793
+纥 4794
+泚 4795
+驳 4796
+铃 4797
+萋 4798
+筱 4799
+蛏 4800
+琲 4801
+鸵 4802
+这 4803
+链 4804
+read@@ 4805
+垵 4806
+踦 4807
+奭 4808
+別 4809
+软 4810
+盲 4811
+黛 4812
+纤 4813
+chil@@ 4814
+扱 4815
+狡 4816
+贽 4817
+贻 4818
+鳏 4819
+篓 4820
+der@@ 4821
+滢 4822
+嵌 4823
+妺 4824
+臭 4825
+谥 4826
+夯 4827
+韬 4828
+惺 4829
+检 4830
+峨 4831
+ms 4832
+胛 4833
+恫 4834
+构 4835
+鹂 4836
+刺 4837
+讦 4838
+唬 4839
+梈 4840
+捯 4841
+由 4842
+志 4843
+达 4844
+梢 4845
+歉 4846
+al@@ 4847
+淳 4848
+般 4849
+恐 4850
+獗 4851
+央 4852
+喔 4853
+蟀 4854
+伞 4855
+sh 4856
+'s 4857
+姮 4858
+痈 4859
+ck 4860
+俾 4861
+髫 4862
+蜒 4863
+愧 4864
+呆 4865
+mer 4866
+桤 4867
+鬏 4868
+型 4869
+四 4870
+哖 4871
+龋 4872
+鳔 4873
+骗 4874
+砷 4875
+泷 4876
+猷 4877
+茯 4878
+茁 4879
+cha@@ 4880
+small 4881
+倨 4882
+乎 4883
+奕 4884
+鹅 4885
+痉 4886
+奥 4887
+辙 4888
+sting 4889
+冲 4890
+赅 4891
+泅 4892
+羰 4893
+悲 4894
+焘 4895
+瞵 4896
+紊 4897
+卖 4898
+take 4899
+鳐 4900
+菊 4901
+姸 4902
+辐 4903
+嘻 4904
+fast 4905
+囿 4906
+凫 4907
+纵 4908
+咝 4909
+箧 4910
+well 4911
+钬 4912
+he@@ 4913
+辕 4914
+鹜 4915
+unk@@ 4916
+诔 4917
+泐 4918
+世 4919
+镖 4920
+骄 4921
+ve 4922
+寸 4923
+pay 4924
+鴂 4925
+睐 4926
+氲 4927
+牟 4928
+洏 4929
+尔 4930
+quo@@ 4931
+椭 4932
+芨 4933
+险 4934
+耳 4935
+柁 4936
+牾 4937
+蚂 4938
+much 4939
+most 4940
+now 4941
+singapore 4942
+丙 4943
+洋 4944
+燊 4945
+啵 4946
+胬 4947
+媾 4948
+碟 4949
+濆 4950
+陇 4951
+扥 4952
+势 4953
+慷 4954
+笋 4955
+泥 4956
+鄘 4957
+hote@@ 4958
+罕 4959
+沤 4960
+葡 4961
+蠡 4962
+产 4963
+鲵 4964
+凉 4965
+萆 4966
+籽 4967
+胞 4968
+哦 4969
+侠 4970
+晚 4971
+咵 4972
+杯 4973
+邗 4974
+甑 4975
+胁 4976
+历 4977
+嘏 4978
+喘 4979
+is 4980
+霉 4981
+仇 4982
+鳀 4983
+鉴 4984
+猛 4985
+纂 4986
+决 4987
+阏 4988
+饯 4989
+宣 4990
+象 4991
+儿 4992
+猗 4993
+瀑 4994
+荚 4995
+满 4996
+茳 4997
+酷 4998
+鼬 4999
+旁 5000
+屡 5001
+榇 5002
+s@@ 5003
+呷 5004
+驻 5005
+薮 5006
+鹏 5007
+馋 5008
+鹪 5009
+纠 5010
+thank 5011
+轶 5012
+渺 5013
+镁 5014
+cost 5015
+汐 5016
+谅 5017
+甫 5018
+publi@@ 5019
+储 5020
+抹 5021
+杨 5022
+裳 5023
+歙 5024
+鏊 5025
+犼 5026
+蹐 5027
+愦 5028
+断 5029
+able 5030
+ind 5031
+狂 5032
+雒 5033
+妧 5034
+巴 5035
+嘟 5036
+筵 5037
+喤 5038
+舫 5039
+刃 5040
+fi@@ 5041
+company 5042
+郿 5043
+瞢 5044
+褰 5045
+清 5046
+巿 5047
+苕 5048
+内 5049
+暾 5050
+垠 5051
+戌 5052
+溟 5053
+剋 5054
+葶 5055
+猡 5056
+蔺 5057
+岖 5058
+邕 5059
+any 5060
+pri@@ 5061
+卸 5062
+基 5063
+馒 5064
+出 5065
+柞 5066
+溍 5067
+吨 5068
+蜥 5069
+炟 5070
+徂 5071
+inve@@ 5072
+桂 5073
+frien@@ 5074
+铪 5075
+庖 5076
+廋 5077
+誉 5078
+嗡 5079
+忘 5080
+i'@@ 5081
+螃 5082
+胯 5083
+un@@ 5084
+铨 5085
+酹 5086
+鷃 5087
+wi 5088
+眚 5089
+跤 5090
+汁 5091
+针 5092
+钏 5093
+雀 5094
+限 5095
+面 5096
+蛐 5097
+黃 5098
+脩 5099
+叛 5100
+枷 5101
+故 5102
+sk@@ 5103
+编 5104
+菥 5105
+勐 5106
+汗 5107
+胃 5108
+洗 5109
+洹 5110
+ers 5111
+髦 5112
+宏 5113
+聊 5114
+嵫 5115
+囎 5116
+淠 5117
+垧 5118
+欧 5119
+oms 5120
+揵 5121
+before 5122
+畈 5123
+饬 5124
+秦 5125
+莓 5126
+努 5127
+炔 5128
+匠 5129
+际 5130
+蕉 5131
+蝇 5132
+po@@ 5133
+匽 5134
+year 5135
+橥 5136
+pping 5137
+注 5138
+承 5139
+琨 5140
+又 5141
+玉 5142
+彖 5143
+缸 5144
+怒 5145
+遏 5146
+坍 5147
+sta@@ 5148
+痹 5149
+埆 5150
+叵 5151
+氟 5152
+脂 5153
+昭 5154
+ments 5155
+宕 5156
+盅 5157
+惩 5158
+馁 5159
+绕 5160
+遢 5161
+扫 5162
+蔡 5163
+崾 5164
+阎 5165
+葳 5166
+殂 5167
+潢 5168
+每 5169
+ro@@ 5170
+靖 5171
+主 5172
+tional 5173
+闸 5174
+涖 5175
+第 5176
+n 5177
+镅 5178
+蜜 5179
+能 5180
+衄 5181
+猹 5182
+鹰 5183
+俘 5184
+豪 5185
+人 5186
+痱 5187
+蓑 5188
+骒 5189
+所 5190
+绨 5191
+粦 5192
+望 5193
+减 5194
+魔 5195
+释 5196
+ars 5197
+逑 5198
+clo@@ 5199
+帏 5200
+遘 5201
+许 5202
+誓 5203
+牤 5204
+刷 5205
+扒 5206
+峻 5207
+9@@ 5208
+昕 5209
+轩 5210
+嬷 5211
+them 5212
+窗 5213
+蹄 5214
+骰 5215
+湓 5216
+堨 5217
+巂 5218
+婧 5219
+骖 5220
+螬 5221
+酋 5222
+ag@@ 5223
+闷 5224
+滞 5225
+郧 5226
+朴 5227
+supp@@ 5228
+倡 5229
+腴 5230
+褴 5231
+极 5232
+漷 5233
+佾 5234
+煨 5235
+沚 5236
+肓 5237
+experi@@ 5238
+亮 5239
+堕 5240
+对 5241
+秋 5242
+every 5243
+铅 5244
+蛴 5245
+擅 5246
+a@@ 5247
+鳃 5248
+保 5249
+ze 5250
+弓 5251
+趄 5252
+翦 5253
+涛 5254
+盗 5255
+道 5256
+遮 5257
+屿 5258
+绥 5259
+cont@@ 5260
+部 5261
+den 5262
+付 5263
+诮 5264
+粘 5265
+镔 5266
+溏 5267
+啬 5268
+贫 5269
+轼 5270
+猩 5271
+薯 5272
+胳 5273
+抻 5274
+than@@ 5275
+king 5276
+燎 5277
+笫 5278
+鸂 5279
+蓊 5280
+峋 5281
+ent 5282
+搡 5283
+磡 5284
+昌 5285
+妈 5286
+玺 5287
+叫 5288
+済 5289
+彩 5290
+lim 5291
+孀 5292
+囧 5293
+we@@ 5294
+绒 5295
+驸 5296
+架 5297
+斛 5298
+资 5299
+碣 5300
+濞 5301
+冿 5302
+吡 5303
+作 5304
+because 5305
+晏 5306
+餍 5307
+瞾 5308
+栊 5309
+蹿 5310
+荩 5311
+尖 5312
+依 5313
+氍 5314
+炝 5315
+抡 5316
+中 5317
+潏 5318
+潦 5319
+mon@@ 5320
+域 5321
+杩 5322
+兼 5323
+捎 5324
+怩 5325
+正 5326
+f 5327
+near 5328
+刮 5329
+搪 5330
+鸿 5331
+俺 5332
+耪 5333
+牯 5334
+瞎 5335
+鬼 5336
+惠 5337
+bro@@ 5338
+沛 5339
+浃 5340
+pe 5341
+釜 5342
+vi@@ 5343
+jal@@ 5344
+拴 5345
+冇 5346
+暎 5347
+魉 5348
+痘 5349
+盔 5350
+枚 5351
+秾 5352
+杲 5353
+郛 5354
+僚 5355
+昞 5356
+足 5357
+sen@@ 5358
+family 5359
+岿 5360
+榖 5361
+蛊 5362
+绯 5363
+村 5364
+鹃 5365
+钼 5366
+拓 5367
+dly 5368
+苫 5369
+锄 5370
+跎 5371
+碰 5372
+钝 5373
+淆 5374
+鳟 5375
+浓 5376
+柇 5377
+桷 5378
+愬 5379
+裔 5380
+邃 5381
+恂 5382
+楙 5383
+沨 5384
+ban@@ 5385
+ssi@@ 5386
+鳞 5387
+褛 5388
+钌 5389
+y 5390
+搀 5391
+骺 5392
+坊 5393
+娘 5394
+鄯 5395
+袈 5396
+匡 5397
+厦 5398
+艘 5399
+障 5400
+舂 5401
+蹼 5402
+材 5403
+胡 5404
+痄 5405
+凖 5406
+憋 5407
+ll 5408
+睥 5409
+窦 5410
+砕 5411
+雇 5412
+個 5413
+籀 5414
+饥 5415
+价 5416
+笞 5417
+鞴 5418
+筊 5419
+树 5420
+揖 5421
+派 5422
+垄 5423
+檬 5424
+眠 5425
+偌 5426
+闯 5427
+姘 5428
+少 5429
+破 5430
+怿 5431
+扼 5432
+恽 5433
+岔 5434
+亲 5435
+垕 5436
+踱 5437
+泳 5438
+截 5439
+轫 5440
+烽 5441
+够 5442
+武 5443
+差 5444
+声 5445
+可 5446
+绫 5447
+巅 5448
+邯 5449
+椎 5450
+硭 5451
+畀 5452
+常 5453
+沸 5454
+榧 5455
+忡 5456
+妫 5457
+皮 5458
+讯 5459
+铣 5460
+轾 5461
+衙 5462
+蒙 5463
+蕃 5464
+things 5465
+腓 5466
+坐 5467
+鞘 5468
+榻 5469
+霈 5470
+垆 5471
+ous 5472
+粳 5473
+枹 5474
+薨 5475
+胆 5476
+龙 5477
+绺 5478
+ning 5479
+med 5480
+挹 5481
+诨 5482
+译 5483
+但 5484
+半 5485
+辩 5486
+芊 5487
+变 5488
+翻 5489
+尹 5490
+靸 5491
+酬 5492
+咶 5493
+暧 5494
+姝 5495
+貘 5496
+墓 5497
+匙 5498
+been 5499
+墀 5500
+芗 5501
+sc@@ 5502
+妲 5503
+狮 5504
+岫 5505
+ants 5506
+谔 5507
+鞭 5508
+婶 5509
+one 5510
+踏 5511
+讵 5512
+land 5513
+幼 5514
+軎 5515
+晕 5516
+wi@@ 5517
+瑞 5518
+供 5519
+啯 5520
+朵 5521
+枓 5522
+奘 5523
+咩 5524
+訢 5525
+臆 5526
+硗 5527
+帖 5528
+dent 5529
+戽 5530
+瘿 5531
+段 5532
+傅 5533
+oun@@ 5534
+枞 5535
+属 5536
+野 5537
+芫 5538
+诣 5539
+钐 5540
+堀 5541
+uring 5542
+试 5543
+衍 5544
+馏 5545
+龇 5546
+讧 5547
+stre@@ 5548
+妒 5549
+驴 5550
+著 5551
+槭 5552
+洧 5553
+side 5554
+氾 5555
+孵 5556
+狎 5557
+歩 5558
+囯 5559
+愎 5560
+过 5561
+pu@@ 5562
+right 5563
+赋 5564
+阳 5565
+mber 5566
+谕 5567
+town 5568
+盾 5569
+救 5570
+饱 5571
+loc@@ 5572
+spa 5573
+橡 5574
+剅 5575
+蒲 5576
+姐 5577
+厕 5578
+铚 5579
+套 5580
+弗 5581
+栴 5582
+鹨 5583
+糨 5584
+騕 5585
+雷 5586
+裁 5587
+拌 5588
+鼻 5589
+some 5590
+搐 5591
+均 5592
+蓣 5593
+ice 5594
+卢 5595
+ad@@ 5596
+瘌 5597
+湶 5598
+氩 5599
+谰 5600
+缙 5601
+ard 5602
+栾 5603
+迨 5604
+浣 5605
+秏 5606
+萨 5607
+always 5608
+污 5609
+踔 5610
+杏 5611
+ded 5612
+髽 5613
+秀 5614
+湉 5615
+粪 5616
+展 5617
+槻 5618
+垓 5619
+赫 5620
+惮 5621
+lar 5622
+薷 5623
+迈 5624
+p 5625
+塾 5626
+ga@@ 5627
+ey 5628
+ho@@ 5629
+恺 5630
+晩 5631
+泸 5632
+茚 5633
+嗰 5634
+绳 5635
+lit@@ 5636
+渍 5637
+遇 5638
+洑 5639
+晞 5640
+溽 5641
+ying 5642
+枸 5643
+无 5644
+逍 5645
+珰 5646
+纣 5647
+罩 5648
+ons 5649
+嶃 5650
+瑚 5651
+裙 5652
+搛 5653
+6 5654
+咾 5655
+唱 5656
+瑠 5657
+睃 5658
+於 5659
+din@@ 5660
+伟 5661
+骓 5662
+函 5663
+瘤 5664
+僆 5665
+鞣 5666
+兕 5667
+伄 5668
+柚 5669
+席 5670
+峄 5671
+鄱 5672
+抔 5673
+an@@ 5674
+物 5675
+loca@@ 5676
+穸 5677
+懊 5678
+2 5679
+amer@@ 5680
+稼 5681
+叉 5682
+bra@@ 5683
+拇 5684
+str@@ 5685
+res@@ 5686
+沆 5687
+饰 5688
+pen@@ 5689
+厘 5690
+傈 5691
+ality 5692
+廷 5693
+式 5694
+夔 5695
+督 5696
+雍 5697
+晨 5698
+剿 5699
+赶 5700
+疖 5701
+砜 5702
+归 5703
+min@@ 5704
+徙 5705
+阚 5706
+tions 5707
+女 5708
+营 5709
+邅 5710
+做 5711
+伊 5712
+er 5713
+筜 5714
+蛎 5715
+business 5716
+顾 5717
+瘸 5718
+洪 5719
+钕 5720
+觯 5721
+政 5722
+劝 5723
+撮 5724
+break@@ 5725
+氛 5726
+漾 5727
+sor@@ 5728
+址 5729
+帼 5730
+增 5731
+ation 5732
+瞋 5733
+稊 5734
+慊 5735
+肯 5736
+躁 5737
+糗 5738
+美 5739
+豕 5740
+us 5741
+炸 5742
+饽 5743
+挎 5744
+挫 5745
+枢 5746
+dr@@ 5747
+铉 5748
+奖 5749
+槌 5750
+绮 5751
+擂 5752
+虬 5753
+钽 5754
+胍 5755
+凌 5756
+辟 5757
+虹 5758
+铂 5759
+杂 5760
+砗 5761
+赪 5762
+潇 5763
+合 5764
+鰕 5765
+吭 5766
+迷 5767
+滘 5768
+弹 5769
+ff 5770
+痤 5771
+蛄 5772
+觱 5773
+杳 5774
+嘬 5775
+偕 5776
+赘 5777
+烩 5778
+孖 5779
+钉 5780
+龉 5781
+钪 5782
+睬 5783
+撵 5784
+big 5785
+笠 5786
+俅 5787
+锯 5788
+呛 5789
+欹 5790
+cle@@ 5791
+珈 5792
+浑 5793
+墕 5794
+hel@@ 5795
+彪 5796
+姊 5797
+拆 5798
+雄 5799
+hi@@ 5800
+钴 5801
+壑 5802
+幇 5803
+卧 5804
+痧 5805
+澴 5806
+长 5807
+船 5808
+六 5809
+继 5810
+棷 5811
+尞 5812
+窨 5813
+燥 5814
+膦 5815
+镝 5816
+裸 5817
+办 5818
+resta@@ 5819
+廆 5820
+宅 5821
+蓉 5822
+娑 5823
+猪 5824
+阁 5825
+蟆 5826
+恢 5827
+讥 5828
+蜾 5829
+of@@ 5830
+怏 5831
+笕 5832
+瞽 5833
+赤 5834
+岁 5835
+禺 5836
+昀 5837
+她 5838
+钫 5839
+慝 5840
+滩 5841
+ack 5842
+跃 5843
+贰 5844
+巳 5845
+寿 5846
+铙 5847
+pro@@ 5848
+朊 5849
+mor@@ 5850
+箦 5851
+丧 5852
+ready 5853
+止 5854
+跺 5855
+缉 5856
+桅 5857
+论 5858
+摽 5859
+状 5860
+驷 5861
+咂 5862
+铢 5863
+绎 5864
+叨 5865
+遁 5866
+锨 5867
+里 5868
+笊 5869
+缰 5870
+嵊 5871
+膘 5872
+帕 5873
+泱 5874
+鳜 5875
+嫒 5876
+仅 5877
+捕 5878
+徵 5879
+each 5880
+vil@@ 5881
+媪 5882
+袁 5883
+裕 5884
+佻 5885
+赙 5886
+末 5887
+笔 5888
+啲 5889
+轺 5890
+掞 5891
+彭 5892
+氢 5893
+got 5894
+缁 5895
+蠊 5896
+攉 5897
+寻 5898
+骋 5899
+噤 5900
+ar@@ 5901
+棣 5902
+疣 5903
+豳 5904
+祠 5905
+衩 5906
+匀 5907
+王 5908
+觫 5909
+庭 5910
+亶 5911
+篇 5912
+铎 5913
+咖 5914
+禛 5915
+艹 5916
+哐 5917
+hope 5918
+系 5919
+娇 5920
+窄 5921
+髓 5922
+阱 5923
+懿 5924
+右 5925
+离 5926
+穴 5927
+渣 5928
+邨 5929
+肸 5930
+杞 5931
+肚 5932
+垗 5933
+幕 5934
+ct 5935
+苊 5936
+隼 5937
+wh@@ 5938
+宋 5939
+煤 5940
+柸 5941
+镋 5942
+耘 5943
+kes 5944
+激 5945
+痣 5946
+work 5947
+吽 5948
+彟 5949
+株 5950
+筐 5951
+贸 5952
+尕 5953
+ere 5954
+潬 5955
+矣 5956
+縢 5957
+瑄 5958
+油 5959
+満 5960
+漆 5961
+rooms 5962
+霞 5963
+咏 5964
+e 5965
+珮 5966
+仡 5967
+眶 5968
+醛 5969
+票 5970
+鼓 5971
+突 5972
+ia 5973
+hundred 5974
+瓯 5975
+尬 5976
+no@@ 5977
+西 5978
+潴 5979
+楪 5980
+傍 5981
+蜘 5982
+yes 5983
+蛀 5984
+sig@@ 5985
+gs 5986
+筮 5987
+因 5988
+谳 5989
+bur@@ 5990
+跹 5991
+鲑 5992
+惑 5993
+肋 5994
+dy 5995
+螟 5996
+霓 5997
+眼 5998
+郇 5999
+宙 6000
+畲 6001
+刬 6002
+翟 6003
+诧 6004
+跫 6005
+or 6006
+颅 6007
+跷 6008
+淅 6009
+臑 6010
+楦 6011
+屯 6012
+走 6013
+羸 6014
+you 6015
+咘 6016
+als 6017
+斤 6018
+畔 6019
+晧 6020
+忠 6021
+肥 6022
+实 6023
+凋 6024
+褶 6025
+ba@@ 6026
+摆 6027
+熊 6028
+篝 6029
+乃 6030
+绑 6031
+沟 6032
+瞿 6033
+哜 6034
+舶 6035
+甘 6036
+事 6037
+瑛 6038
+踧 6039
+黯 6040
+鲞 6041
+cal@@ 6042
+璩 6043
+enty 6044
+剧 6045
+洿 6046
+孳 6047
+祓 6048
+天 6049
+岑 6050
+拾 6051
+nineteen 6052
+cause 6053
+愇 6054
+翁 6055
+瞟 6056
+昆 6057
+弢 6058
+摛 6059
+义 6060
+咸 6061
+勮 6062
+伪 6063
+瑱 6064
+橄 6065
+丘 6066
+铧 6067
+怖 6068
+终 6069
+膨 6070
+5@@ 6071
+扮 6072
+to 6073
+猕 6074
+琬 6075
+群 6076
+叁 6077
+狄 6078
+眵 6079
+烹 6080
+蚝 6081
+儒 6082
+援 6083
+ber@@ 6084
+1 6085
+掾 6086
+裂 6087
+藓 6088
+r@@ 6089
+squ@@ 6090
+鲁 6091
+潺 6092
+晷 6093
+消 6094
+鸸 6095
+觊 6096
+诡 6097
+却 6098
+as 6099
+弼 6100
+脰 6101
+铷 6102
+册 6103
+懵 6104
+啜 6105
+涣 6106
+扈 6107
+垚 6108
+森 6109
+赵 6110
+垫 6111
+镪 6112
+怼 6113
+峁 6114
+惚 6115
+缄 6116
+ass 6117
+诼 6118
+tive 6119
+never 6120
+昇 6121
+ence 6122
+耍 6123
+异 6124
+朐 6125
+枕 6126
+襻 6127
+契 6128
+祈 6129
+骜 6130
+羯 6131
+豁 6132
+瘕 6133
+贲 6134
+禥 6135
+哔 6136
+祁 6137
+螫 6138
+锆 6139
+孕 6140
+st 6141
+忏 6142
+掏 6143
+昵 6144
+哪 6145
+鹞 6146
+疍 6147
+遄 6148
+蠲 6149
+恹 6150
+狩 6151
+憾 6152
+悫 6153
+弦 6154
+脾 6155
+soci@@ 6156
+爸 6157
+home 6158
+奔 6159
+懂 6160
+爿 6161
+帙 6162
+襦 6163
+除 6164
+ters 6165
+ted 6166
+景 6167
+昚 6168
+eas@@ 6169
+ven@@ 6170
+蹩 6171
+ac@@ 6172
+歇 6173
+fas@@ 6174
+抄 6175
+牌 6176
+浦 6177
+榭 6178
+lan@@ 6179
+巺 6180
+刁 6181
+secon@@ 6182
+顒 6183
+个 6184
+鲒 6185
+嗨 6186
+out 6187
+凤 6188
+褫 6189
+瑮 6190
+珏 6191
+惦 6192
+斝 6193
+监 6194
+土 6195
+did 6196
+date 6197
+陋 6198
+眉 6199
+湴 6200
+谎 6201
+撖 6202
+礞 6203
+抑 6204
+旻 6205
+倩 6206
+蟑 6207
+produ@@ 6208
+忍 6209
+free 6210
+瓜 6211
+康 6212
+晾 6213
+偏 6214
+麇 6215
+缱 6216
+唏 6217
+杪 6218
+戥 6219
+坳 6220
+皲 6221
+拘 6222
+簋 6223
+怆 6224
+负 6225
+陬 6226
+埽 6227
+皞 6228
+素 6229
+锐 6230
+苘 6231
+菀 6232
+嚜 6233
+免 6234
+嫌 6235
+靡 6236
+d 6237
+尪 6238
+颧 6239
+苷 6240
+de@@ 6241
+inc@@ 6242
+策 6243
+浠 6244
+窾 6245
+么 6246
+壁 6247
+pi@@ 6248
+荟 6249
+愣 6250
+砫 6251
+巻 6252
+ke 6253
+旌 6254
+咉 6255
+矢 6256
+婺 6257
+绝 6258
+线 6259
+奂 6260
+袋 6261
+喻 6262
+贡 6263
+鍒 6264
+逛 6265
+days 6266
+疟 6267
+筇 6268
+髃 6269
+锛 6270
+俱 6271
+缅 6272
+锊 6273
+骟 6274
+belie@@ 6275
+跏 6276
+岜 6277
+睚 6278
+蜊 6279
+铝 6280
+闭 6281
+麈 6282
+趺 6283
+锜 6284
+彻 6285
+缟 6286
+甭 6287
+苞 6288
+钥 6289
+ph@@ 6290
+ef@@ 6291
+灼 6292
+銮 6293
+塍 6294
+淙 6295
+鸷 6296
+撼 6297
+浒 6298
+though 6299
+或 6300
+av@@ 6301
+荇 6302
+要 6303
+road 6304
+wn 6305
+纺 6306
+师 6307
+裇 6308
+颚 6309
+end 6310
+汜 6311
+驰 6312
+8 6313
+椹 6314
+牂 6315
+ties 6316
+侪 6317
+樉 6318
+扎 6319
+吞 6320
+蕤 6321
+垦 6322
+茜 6323
+泌 6324
+wor@@ 6325
+艋 6326
+次 6327
+珙 6328
+建 6329
+切 6330
+盱 6331
+簌 6332
+bus 6333
+ten@@ 6334
+check 6335
+硅 6336
+概 6337
+袒 6338
+厚 6339
+馐 6340
+橐 6341
+涵 6342
+抢 6343
+兖 6344
+颠 6345
+扑 6346
+渐 6347
+底 6348
+獒 6349
+瘴 6350
+喝 6351
+期 6352
+踪 6353
+镭 6354
+糕 6355
+翡 6356
+养 6357
+苺 6358
+gi@@ 6359
+顷 6360
+蚜 6361
+琛 6362
+县 6363
+槽 6364
+滹 6365
+钙 6366
+ple@@ 6367
+叙 6368
+囵 6369
+煮 6370
+杵 6371
+铛 6372
+丢 6373
+任 6374
+咹 6375
+莎 6376
+请 6377
+喂 6378
+旬 6379
+枫 6380
+颊 6381
+圄 6382
+霍 6383
+耆 6384
+阅 6385
+嵴 6386
+鱿 6387
+倭 6388
+me 6389
+咎 6390
+sing 6391
+眸 6392
+羽 6393
+二 6394
+橦 6395
+菉 6396
+巩 6397
+柑 6398
+赧 6399
+暮 6400
+d@@ 6401
+损 6402
+world 6403
+隻 6404
+鑹 6405
+捧 6406
+轨 6407
+按 6408
+啄 6409
+聒 6410
+炅 6411
+湣 6412
+詹 6413
+饧 6414
+柒 6415
+恃 6416
+掬 6417
+爽 6418
+料 6419
+窎 6420
+歹 6421
+刿 6422
+完 6423
+餮 6424
+鵀 6425
+是 6426
+榈 6427
+透 6428
+粗 6429
+俛 6430
+音 6431
+份 6432
+置 6433
+砊 6434
+蓬 6435
+蘘 6436
+缛 6437
+going 6438
+遯 6439
+your 6440
+ever@@ 6441
+制 6442
+淼 6443
+冏 6444
+妆 6445
+钣 6446
+怡 6447
+vern@@ 6448
+z@@ 6449
+弇 6450
+砥 6451
+蜴 6452
+饸 6453
+冮 6454
+衫 6455
+韘 6456
+寖 6457
+虐 6458
+玹 6459
+3@@ 6460
+tho@@ 6461
+闼 6462
+旋 6463
+煿 6464
+嶝 6465
+涟 6466
+龌 6467
+ko@@ 6468
+ge@@ 6469
+遛 6470
+郢 6471
+褡 6472
+薪 6473
+邓 6474
+嫁 6475
+叔 6476
+嫤 6477
+漠 6478
+gue@@ 6479
+藿 6480
+臾 6481
+屣 6482
+ily 6483
+早 6484
+痴 6485
+埴 6486
+囊 6487
+俩 6488
+ours 6489
+蓦 6490
+渗 6491
+锟 6492
+朦 6493
+附 6494
+赴 6495
+make 6496
+灾 6497
+汛 6498
+埸 6499
+吾 6500
+栲 6501
+翰 6502
+磔 6503
+汰 6504
+椋 6505
+锸 6506
+搴 6507
+蒐 6508
+韧 6509
+撒 6510
+纽 6511
+笙 6512
+order 6513
+坼 6514
+伷 6515
+val@@ 6516
+烬 6517
+梡 6518
+瑶 6519
+陷 6520
+go 6521
+绤 6522
+琉 6523
+湖 6524
+硷 6525
+熹 6526
+觇 6527
+侥 6528
+往 6529
+牲 6530
+租 6531
+矾 6532
+祉 6533
+跂 6534
+age 6535
+刘 6536
+缃 6537
+复 6538
+阂 6539
+刊 6540
+am@@ 6541
+苻 6542
+步 6543
+cess 6544
+蓖 6545
+he 6546
+喙 6547
+畜 6548
+鄠 6549
+让 6550
+雱 6551
+噫 6552
+sel@@ 6553
+钜 6554
+胥 6555
+蹬 6556
+help 6557
+捻 6558
+芽 6559
+肷 6560
+掳 6561
+旧 6562
+謦 6563
+甲 6564
+ort 6565
+搽 6566
+哧 6567
+骢 6568
+吼 6569
+口 6570
+倚 6571
+肽 6572
+憨 6573
+竦 6574
+product 6575
+棚 6576
+黍 6577
+砾 6578
+愈 6579
+昊 6580
+阑 6581
+魂 6582
+持 6583
+献 6584
+pa@@ 6585
+凳 6586
+厩 6587
+佚 6588
+鲱 6589
+粮 6590
+鹋 6591
+姬 6592
+抉 6593
+籇 6594
+part 6595
+found 6596
+嗍 6597
+提 6598
+蛇 6599
+籓 6600
+盼 6601
+裱 6602
+暄 6603
+妞 6604
+喑 6605
+惶 6606
+wha@@ 6607
+窥 6608
+ks 6609
+橘 6610
+绞 6611
+菁 6612
+饭 6613
+ste@@ 6614
+猞 6615
+芘 6616
+搧 6617
+tain 6618
+street 6619
+呔 6620
+懆 6621
+袼 6622
+肼 6623
+on@@ 6624
+亨 6625
+谑 6626
+椅 6627
+莹 6628
+校 6629
+躔 6630
+颈 6631
+圬 6632
+匕 6633
+mail 6634
+税 6635
+年 6636
+脆 6637
+郦 6638
+拿 6639
+fo@@ 6640
+鑱 6641
+改 6642
+la 6643
+轸 6644
+掩 6645
+芒 6646
+can't 6647
+彺 6648
+觑 6649
+邴 6650
+埵 6651
+莴 6652
+伢 6653
+腕 6654
+阴 6655
+tre@@ 6656
+眇 6657
+责 6658
+青 6659
+钠 6660
+point 6661
+僵 6662
+庹 6663
+具 6664
+迠 6665
+穗 6666
+逃 6667
+源 6668
+咻 6669
+今 6670
+绻 6671
+发 6672
+嗝 6673
+逆 6674
+侈 6675
+抓 6676
+羌 6677
+款 6678
+耧 6679
+锰 6680
+罢 6681
+跗 6682
+汞 6683
+赚 6684
+累 6685
+掷 6686
+啫 6687
+膳 6688
+捭 6689
+禀 6690
+崂 6691
+栝 6692
+掴 6693
+讶 6694
+y@@ 6695
+瑾 6696
+账 6697
+挈 6698
+鹬 6699
+儁 6700
+鳎 6701
+骛 6702
+釉 6703
+矬 6704
+潮 6705
+ort@@ 6706
+病 6707
+日 6708
+o 6709
+憍 6710
+镍 6711
+蜛 6712
+壤 6713
+葑 6714
+聚 6715
+灭 6716
+立 6717
+怙 6718
+m@@ 6719
+诙 6720
+躲 6721
+泡 6722
+would 6723
+缺 6724
+横 6725
+违 6726
+蛛 6727
+ls 6728
+鹮 6729
+鲐 6730
+茌 6731
+罚 6732
+ques@@ 6733
+畦 6734
+厍 6735
+缳 6736
+劭 6737
+拐 6738
+萍 6739
+债 6740
+偈 6741
+镛 6742
+踽 6743
+趋 6744
+扭 6745
+椒 6746
+痔 6747
+史 6748
+称 6749
+thing 6750
+q@@ 6751
+fre@@ 6752
+剁 6753
+beach 6754
+舢 6755
+黠 6756
+执 6757
+饵 6758
+欤 6759
+骥 6760
+蓄 6761
+邈 6762
+疚 6763
+'@@ 6764
+吓 6765
+撄 6766
+苌 6767
+蜢 6768
+mar@@ 6769
+桓 6770
+made 6771
+煦 6772
+襁 6773
+垃 6774
+姓 6775
+送 6776
+柜 6777
+埪 6778
+怠 6779
+蚍 6780
+little 6781
+殴 6782
+炕 6783
+蛙 6784
+伱 6785
+禁 6786
+睺 6787
+捍 6788
+颀 6789
+猬 6790
+蟊 6791
+岢 6792
+o@@ 6793
+纩 6794
+妨 6795
+阡 6796
+率 6797
+蒋 6798
+寐 6799
+孚 6800
+想 6801
+洁 6802
+沮 6803
+铦 6804
+镟 6805
+潲 6806
+杮 6807
+tal 6808
+镯 6809
+虞 6810
+鶒 6811
+椐 6812
+簦 6813
+锈 6814
+顗 6815
+仑 6816
+额 6817
+掼 6818
+耿 6819
+荑 6820
+撙 6821
+谍 6822
+沃 6823
+惊 6824
+碉 6825
+皓 6826
+峯 6827
+炮 6828
+厶 6829
+拼 6830
+得 6831
+帅 6832
+蜎 6833
+莠 6834
+近 6835
+诩 6836
+喓 6837
+sequence 6838
+愉 6839
+ure 6840
+枯 6841
+thous@@ 6842
+槅 6843
+移 6844
+菡 6845
+菪 6846
+ten 6847
+癍 6848
+珀 6849
+湎 6850
+瓿 6851
+名 6852
+妇 6853
+再 6854
+傺 6855
+蚬 6856
+荡 6857
+孽 6858
+匜 6859
+堠 6860
+荜 6861
+绉 6862
+刽 6863
+des 6864
+1@@ 6865
+留 6866
+裎 6867
+赐 6868
+ving 6869
+光 6870
+ly 6871
+睟 6872
+遂 6873
+琮 6874
+俟 6875
+岛 6876
+蛹 6877
+蒿 6878
+nor@@ 6879
+钦 6880
+麻 6881
+纫 6882
+coun@@ 6883
+杷 6884
+宿 6885
+吣 6886
+蝎 6887
+湃 6888
+惬 6889
+enjo@@ 6890
+卯 6891
+牸 6892
+诠 6893
+谷 6894
+摞 6895
+螈 6896
+然 6897
+盂 6898
+mple 6899
+萁 6900
+灰 6901
+畊 6902
+when 6903
+烈 6904
+俸 6905
+衿 6906
+龟 6907
+谭 6908
+唳 6909
+楀 6910
+痪 6911
+曳 6912
+羿 6913
+罂 6914
+脱 6915
+廿 6916
+sur@@ 6917
+拝 6918
+洵 6919
+湿 6920
+鸽 6921
+冂 6922
+跛 6923
+school 6924
+尡 6925
+挡 6926
+疭 6927
+议 6928
+self 6929
+拍 6930
+溺 6931
+垅 6932
+瞳 6933
+笑 6934
+thousand 6935
+乓 6936
+噼 6937
+lie@@ 6938
+沿 6939
+俣 6940
+灏 6941
+赈 6942
+砭 6943
+造 6944
+标 6945
+loo@@ 6946
+罅 6947
+鳣 6948
+翥 6949
+铬 6950
+诞 6951
+酌 6952
+佟 6953
+歘 6954
+瑀 6955
+眬 6956
+璁 6957
+拣 6958
+u 6959
+颤 6960
+笏 6961
+ur@@ 6962
+蝌 6963
+蛟 6964
+number 6965
+馕 6966
+place 6967
+伐 6968
+热 6969
+ver@@ 6970
+淜 6971
+嚼 6972
+鼢 6973
+謩 6974
+趟 6975
+恬 6976
+搅 6977
+伫 6978
+杌 6979
+琐 6980
+崽 6981
+苗 6982
+其 6983
+熄 6984
+在 6985
+泾 6986
+毓 6987
+very 6988
+琥 6989
+孤 6990
+扞 6991
+恪 6992
+t's 6993
+埙 6994
+do 6995
+攮 6996
+昴 6997
+靥 6998
+feel 6999
+酗 7000
+蕫 7001
+der 7002
+焙 7003
+gen@@ 7004
+辛 7005
+觞 7006
+平 7007
+椤 7008
+耦 7009
+黜 7010
+寇 7011
+醍 7012
+耶 7013
+撰 7014
+蔌 7015
+曜 7016
+谂 7017
+烛 7018
+旦 7019
+kno@@ 7020
+爪 7021
+絺 7022
+欢 7023
+圈 7024
+臂 7025
+ch 7026
+谆 7027
+荏 7028
+巫 7029
+擐 7030
+嵋 7031
+霸 7032
+ames 7033
+wel@@ 7034
+叆 7035
+傧 7036
+tra@@ 7037
+隳 7038
+唆 7039
+澧 7040
+焦 7041
+窭 7042
+剜 7043
+菇 7044
+锇 7045
+锑 7046
+枳 7047
+娈 7048
+侗 7049
+橙 7050
+愊 7051
+泯 7052
+茼 7053
+荙 7054
+遭 7055
+蟥 7056
+捱 7057
+挤 7058
+蹊 7059
+暹 7060
+ity 7061
+珠 7062
+驼 7063
+嗷 7064
+贪 7065
+扔 7066
+硒 7067
+沣 7068
+鸡 7069
+蕖 7070
+仓 7071
+looking 7072
+冦 7073
+邳 7074
+赝 7075
+嵛 7076
+chi@@ 7077
+典 7078
+桧 7079
+about 7080
+茏 7081
+es@@ 7082
+缒 7083
+蕻 7084
+闽 7085
+独 7086
+骨 7087
+尰 7088
+鲼 7089
+und@@ 7090
+详 7091
+姩 7092
+脉 7093
+腥 7094
+拯 7095
+婕 7096
+杜 7097
+俑 7098
+经 7099
+they 7100
+距 7101
+僧 7102
+辅 7103
+骂 7104
+睛 7105
+夤 7106
+door 7107
+捊 7108
+靴 7109
+庅 7110
+劓 7111
+舣 7112
+饿 7113
+幻 7114
+ry 7115
+悱 7116
+畎 7117
+先 7118
+畠 7119
+ye@@ 7120
+those 7121
+菲 7122
+榔 7123
+邑 7124
+city 7125
+泖 7126
+夕 7127
+better 7128
+吏 7129
+酡 7130
+蕾 7131
+犯 7132
+磲 7133
+锝 7134
+鹌 7135
+鹫 7136
+you@@ 7137
+耐 7138
+梯 7139
+鸬 7140
+榛 7141
+笛 7142
+睡 7143
+歪 7144
+遍 7145
+飒 7146
+笃 7147
+蓝 7148
+于 7149
+欸 7150
+为 7151
+琎 7152
+宁 7153
+ki@@ 7154
+tle 7155
+直 7156
+漕 7157
+years 7158
+烙 7159
+捏 7160
+馅 7161
+re 7162
+诺 7163
+tur@@ 7164
+deci@@ 7165
+励 7166
+佘 7167
+bl@@ 7168
+of 7169
+噩 7170
+阪 7171
+梿 7172
+孃 7173
+杻 7174
+覃 7175
+瞆 7176
+瑷 7177
+观 7178
+循 7179
+泪 7180
+six@@ 7181
+集 7182
+cust@@ 7183
+瘠 7184
+尽 7185
+鱀 7186
+虎 7187
+来 7188
+other 7189
+衮 7190
+芷 7191
+卓 7192
+擒 7193
+锶 7194
+玻 7195
+lu@@ 7196
+缝 7197
+瀬 7198
+缯 7199
+嫖 7200
+偭 7201
+运 7202
+厣 7203
+顺 7204
+璝 7205
+锬 7206
+悼 7207
+off 7208
+苑 7209
+恤 7210
+囡 7211
+锻 7212
+舁 7213
+粼 7214
+丑 7215
+字 7216
+酽 7217
+鲜 7218
+豹 7219
+厝 7220
+琤 7221
+谞 7222
+簖 7223
+癣 7224
+蚌 7225
+退 7226
+戡 7227
+云 7228
+颞 7229
+cur@@ 7230
+腻 7231
+呗 7232
+沼 7233
+骤 7234
+崭 7235
+扯 7236
+怍 7237
+app@@ 7238
+存 7239
+害 7240
+色 7241
+踣 7242
+淘 7243
+嗦 7244
+獠 7245
+怗 7246
+砸 7247
+镜 7248
+毹 7249
+table 7250
+@ 7251
+鲀 7252
+寤 7253
+桥 7254
+嘞 7255
+髁 7256
+哙 7257
+织 7258
+嗒 7259
+麂 7260
+let 7261
+偁 7262
+苎 7263
+燧 7264
+rou@@ 7265
+哃 7266
+鄂 7267
+翌 7268
+酚 7269
+咄 7270
+嗥 7271
+蒸 7272
+虚 7273
+囗 7274
+条 7275
+鹕 7276
+挖 7277
+per@@ 7278
+矿 7279
+珦 7280
+囨 7281
+冻 7282
+佰 7283
+洲 7284
+漓 7285
+菹 7286
+镱 7287
+尝 7288
+low 7289
+苜 7290
+剞 7291
+could 7292
+埒 7293
+铼 7294
+结 7295
+侵 7296
+星 7297
+then 7298
+埠 7299
+峤 7300
+晒 7301
+睦 7302
+搓 7303
+時 7304
+铫 7305
+偃 7306
+might 7307
+氦 7308
+呻 7309
+淦 7310
+ge 7311
+缧 7312
+脑 7313
+膴 7314
+英 7315
+pla@@ 7316
+案 7317
+生 7318
+顔 7319
+记 7320
+艰 7321
+tic@@ 7322
+砖 7323
+药 7324
+宪 7325
+涩 7326
+鞠 7327
+潆 7328
+郭 7329
+宵 7330
+砀 7331
+蟛 7332
+岀 7333
+赃 7334
+蛤 7335
+粢 7336
+驶 7337
+膂 7338
+垤 7339
+nee@@ 7340
+逐 7341
+芳 7342
+螂 7343
+超 7344
+change 7345
+j 7346
+锔 7347
+箓 7348
+笤 7349
+弱 7350
+畸 7351
+分 7352
+攸 7353
+蛸 7354
+烷 7355
+脒 7356
+昨 7357
+咴 7358
+羚 7359
+芍 7360
+卲 7361
+噔 7362
+飖 7363
+蜍 7364
+some@@ 7365
+崦 7366
+嬛 7367
+仍 7368
+柳 7369
+鹗 7370
+増 7371
+乙 7372
+嫂 7373
+骚 7374
+巾 7375
+憬 7376
+刭 7377
+顸 7378
+磙 7379
+伃 7380
+孜 7381
+碌 7382
+堐 7383
+祗 7384
+鳉 7385
+汊 7386
+ab@@ 7387
+袄 7388
+诚 7389
+剡 7390
+锷 7391
+锕 7392
+益 7393
+箕 7394
+瘾 7395
+restaur@@ 7396
+鹣 7397
+瘩 7398
+矸 7399
+his 7400
+灸 7401
+勖 7402
+括 7403
+呢 7404
+漯 7405
+罟 7406
+嚓 7407
+焗 7408
+tru@@ 7409
+赛 7410
+伶 7411
+垢 7412
+擘 7413
+镠 7414
+惰 7415
+贬 7416
+惙 7417
+捺 7418
+sto@@ 7419
+犁 7420
+were 7421
+艉 7422
+佞 7423
+配 7424
+耙 7425
+猢 7426
+遗 7427
+豊 7428
+急 7429
+蓟 7430
+泓 7431
+屑 7432
+焊 7433
+筲 7434
+缈 7435
+柔 7436
+蜀 7437
+珺 7438
+怕 7439
+淡 7440
+. 7441
+靛 7442
+载 7443
+堎 7444
+潥 7445
+懒 7446
+邸 7447
+惇 7448
+粥 7449
+斫 7450
+漼 7451
+re's 7452
+簇 7453
+修 7454
+梏 7455
+tr@@ 7456
+肟 7457
+葙 7458
+鲸 7459
+呐 7460
+游 7461
+廖 7462
+收 7463
+鳅 7464
+覆 7465
+琳 7466
+熵 7467
+翱 7468
+民 7469
+飨 7470
+螳 7471
+媳 7472
+辖 7473
+旯 7474
+榴 7475
+瑰 7476
+嗯 7477
+渤 7478
+ic 7479
+熔 7480
+暝 7481
+柘 7482
+鬟 7483
+斋 7484
+夸 7485
+晢 7486
+氰 7487
+炫 7488
+dn't 7489
+押 7490
+撺 7491
+烁 7492
+偶 7493
+貅 7494
+冀 7495
+胎 7496
+糊 7497
+润 7498
+咭 7499
+各 7500
+摐 7501
+錞 7502
+垌 7503
+插 7504
+舯 7505
+嶙 7506
+筋 7507
+住 7508
+酆 7509
+镰 7510
+工 7511
+嗲 7512
+铱 7513
+暖 7514
+off@@ 7515
+茔 7516
+髂 7517
+挺 7518
+瞑 7519
+伤 7520
+忌 7521
+僬 7522
+萜 7523
+普 7524
+that@@ 7525
+爵 7526
+稍 7527
+瓠 7528
+崛 7529
+um 7530
+磐 7531
+投 7532
+畅 7533
+缕 7534
+楯 7535
+悒 7536
+判 7537
+筢 7538
+馇 7539
+芰 7540
+遒 7541
+黩 7542
+迪 7543
+蝾 7544
+腈 7545
+才 7546
+峙 7547
+悟 7548
+禩 7549
+惋 7550
+five 7551
+局 7552
+砘 7553
+ver 7554
+恋 7555
+斓 7556
+蹀 7557
+犹 7558
+绶 7559
+ep 7560
+喛 7561
+亩 7562
+嵘 7563
+汆 7564
+狸 7565
+迸 7566
+盉 7567
+line 7568
+妣 7569
+躐 7570
+琢 7571
+赣 7572
+le 7573
+疬 7574
+穿 7575
+焓 7576
+跳 7577
+匪 7578
+feat@@ 7579
+鹦 7580
+璠 7581
+蔸 7582
+百 7583
+愁 7584
+芋 7585
+吙 7586
+谚 7587
+熳 7588
+哩 7589
+ad 7590
+轧 7591
+祇 7592
+泬 7593
+瘳 7594
+列 7595
+簃 7596
+谦 7597
+lea@@ 7598
+垴 7599
+啤 7600
+滇 7601
+饮 7602
+瞰 7603
+獭 7604
+倏 7605
+肿 7606
+宜 7607
+涮 7608
+礓 7609
+数 7610
+屌 7611
+濒 7612
+蟜 7613
+上 7614
+詝 7615
+褐 7616
+眭 7617
+菟 7618
+瘅 7619
+蝙 7620
+眜 7621
+羡 7622
+硖 7623
+背 7624
+芣 7625
+歃 7626
+红 7627
+血 7628
+混 7629
+橞 7630
+樽 7631
+纶 7632
+圭 7633
+stay 7634
+冗 7635
+竑 7636
+玥 7637
+它 7638
+怎 7639
+薜 7640
+殆 7641
+看 7642
+剖 7643
+棠 7644
+瘙 7645
+be@@ 7646
+裤 7647
+篙 7648
+带 7649
+助 7650
+骀 7651
+锍 7652
+廓 7653
+库 7654
+绅 7655
+劂 7656
+蝻 7657
+锘 7658
+濩 7659
+等 7660
+魑 7661
+酿 7662
+阆 7663
+弑 7664
+悖 7665
+姹 7666
+傀 7667
+which 7668
+股 7669
+湲 7670
+贝 7671
+相 7672
+茑 7673
+荔 7674
+钲 7675
+致 7676
+沦 7677
+另 7678
+桑 7679
+佳 7680
+班 7681
+昼 7682
+埂 7683
+剕 7684
+箐 7685
+岵 7686
+涨 7687
+柩 7688
+两 7689
+勰 7690
+氮 7691
+稽 7692
+匝 7693
+ting 7694
+瘀 7695
+缔 7696
+鹉 7697
+雕 7698
+蛮 7699
+镞 7700
+魈 7701
+謇 7702
+me@@ 7703
+set 7704
+豇 7705
+h 7706
+秫 7707
+荬 7708
+赢 7709
+沩 7710
+向 7711
+石 7712
+踖 7713
+迥 7714
+受 7715
+抅 7716
+镘 7717
+嚭 7718
+双 7719
+锚 7720
+ex@@ 7721
+辉 7722
+谠 7723
+綉 7724
+ws 7725
+吟 7726
+湔 7727
+ll@@ 7728
+市 7729
+娥 7730
+堢 7731
+跽 7732
+处 7733
+厂 7734
+朓 7735
+笸 7736
+悭 7737
+孥 7738
+嘀 7739
+恻 7740
+皱 7741
+塄 7742
+who@@ 7743
+妙 7744
+栽 7745
+瓷 7746
+稔 7747
+碧 7748
+哽 7749
+pol@@ 7750
+徨 7751
+s 7752
+邋 7753
+息 7754
+樨 7755
+鼾 7756
+拱 7757
+谟 7758
+墡 7759
+匆 7760
+speci@@ 7761
+ha@@ 7762
+庐 7763
+鹿 7764
+ak@@ 7765
+镣 7766
+m 7767
+唯 7768
+州 7769
+筒 7770
+性 7771
+髋 7772
+哄 7773
+镡 7774
+贇 7775
+and 7776
+菩 7777
+姻 7778
+聪 7779
+tion 7780
+催 7781
+醌 7782
+唢 7783
+雾 7784
+堂 7785
+芝 7786
+擢 7787
+匈 7788
+悸 7789
+钢 7790
+gy 7791
+徉 7792
+渡 7793
+誊 7794
+ti@@ 7795
+军 7796
+铸 7797
+和 7798
+沄 7799
+兽 7800
+短 7801
+育 7802
+韪 7803
+邀 7804
+谴 7805
+耋 7806
+浩 7807
+public 7808
+胝 7809
+猴 7810
+six 7811
+黧 7812
+being 7813
+弊 7814
+捂 7815
+ical 7816
+癖 7817
+堆 7818
+伧 7819
+割 7820
+引 7821
+la@@ 7822
+鼙 7823
+逻 7824
+堪 7825
+cap@@ 7826
+首 7827
+谝 7828
+瘦 7829
+硐 7830
+郑 7831
+凑 7832
+fu@@ 7833
+b@@ 7834
+睨 7835
+骠 7836
+佼 7837
+篦 7838
+贵 7839
+铌 7840
+act 7841
+呑 7842
+竖 7843
+莅 7844
+陕 7845
+飙 7846
+黏 7847
+镆 7848
+屦 7849
+仨 7850
+敫 7851
+马 7852
+埜 7853
+垇 7854
+阇 7855
+耒 7856
+笩 7857
+蓂 7858
+妃 7859
+讨 7860
+see@@ 7861
+吊 7862
+踊 7863
+弥 7864
+梅 7865
+坑 7866
+渫 7867
+au@@ 7868
+赡 7869
+坛 7870
+苹 7871
+don@@ 7872
+察 7873
+俚 7874
+fir@@ 7875
+濑 7876
+块 7877
+浏 7878
+唵 7879
+婴 7880
+恙 7881
+隈 7882
+麽 7883
+ca@@ 7884
+莼 7885
+御 7886
+鞋 7887
+禧 7888
+衰 7889
+潩 7890
+茬 7891
+ven 7892
+瞌 7893
+鸥 7894
+凭 7895
+咯 7896
+饺 7897
+耄 7898
+uni@@ 7899
+瑟 7900
+煟 7901
+舟 7902
+尅 7903
+帚 7904
+鼋 7905
+嫔 7906
+婄 7907
+赟 7908
+棼 7909
+沪 7910
+秃 7911
+粧 7912
+roo@@ 7913
+蚳 7914
+亏 7915
+备 7916
+压 7917
+悍 7918
+嚅 7919
+fin@@ 7920
+交 7921
+癔 7922
+跞 7923
+莪 7924
+闻 7925
+罾 7926
+炁 7927
+钻 7928
+鹆 7929
+友 7930
+傣 7931
+糁 7932
+service 7933
+邾 7934
+窝 7935
+扉 7936
+鲡 7937
+盛 7938
+玖 7939
+卫 7940
+ne@@ 7941
+眛 7942
+鸟 7943
+党 7944
+杼 7945
+碓 7946
+篚 7947
+裆 7948
+钟 7949
+大 7950
+介 7951
+揩 7952
+砻 7953
+卅 7954
+弟 7955
+颛 7956
+嫪 7957
+代 7958
+会 7959
+悻 7960
+者 7961
+箍 7962
+番 7963
+疯 7964
+sp@@ 7965
+聃 7966
+瀚 7967
+挥 7968
+拷 7969
+氡 7970
+璎 7971
+肪 7972
+tan 7973
+ul@@ 7974
+蠃 7975
+龊 7976
+家 7977
+挽 7978
+must 7979
+蹭 7980
+樟 7981
+令 7982
+饶 7983
+buy 7984
+忖 7985
+娠 7986
+手 7987
+house 7988
+贷 7989
+嘚 7990
+莺 7991
+茛 7992
+宥 7993
+刼 7994
+傥 7995
+rest 7996
+厄 7997
+洙 7998
+牝 7999
+林 8000
+铆 8001
+觳 8002
+嬢 8003
+芥 8004
+充 8005
+絮 8006
+陲 8007
+zer@@ 8008
+藤 8009
+air@@ 8010
+贤 8011
+趾 8012
+孝 8013
+袖 8014
+氇 8015
+章 8016
+柷 8017
+恓 8018
+麹 8019
+胧 8020
+came 8021
+休 8022
+舲 8023
+籼 8024
+惕 8025
+泼 8026
+馨 8027
+町 8028
+wer 8029
+鹧 8030
+咚 8031
+崔 8032
+嵂 8033
+蝈 8034
+ils 8035
+湛 8036
+报 8037
+蜷 8038
+削 8039
+根 8040
+恣 8041
+呙 8042
+郫 8043
+把 8044
+窕 8045
+癌 8046
+xt 8047
+榘 8048
+傫 8049
+用 8050
+撕 8051
+蚩 8052
+菰 8053
+崖 8054
+酐 8055
+襄 8056
+cu@@ 8057
+呤 8058
+帯 8059
+炊 8060
+揲 8061
+啸 8062
+鲶 8063
+ve@@ 8064
+艟 8065
+嗜 8066
+ili@@ 8067
+凹 8068
+球 8069
+鬓 8070
+荼 8071
+给 8072
+亘 8073
+醯 8074
+燔 8075
+鳕 8076
+侄 8077
+簪 8078
+洳 8079
+嘛 8080
+稠 8081
+'t 8082
+氘 8083
+暍 8084
+指 8085
+泆 8086
+总 8087
+offer 8088
+糅 8089
+膀 8090
+毋 8091
+蹲 8092
+煅 8093
+桔 8094
+什 8095
+浜 8096
+八 8097
+涧 8098
+跬 8099
+祷 8100
+湫 8101
+脸 8102
+描 8103
+pr@@ 8104
+狱 8105
+赉 8106
+砼 8107
+袜 8108
+馑 8109
+枪 8110
+碁 8111
+娜 8112
+辇 8113
+颐 8114
+捩 8115
+峥 8116
+聆 8117
+善 8118
+询 8119
+疝 8120
+坜 8121
+碚 8122
+缥 8123
+鼯 8124
+钮 8125
+腘 8126
+溥 8127
+脔 8128
+殁 8129
+肮 8130
+泮 8131
+捐 8132
+槛 8133
+吆 8134
+河 8135
+溆 8136
+度 8137
+殒 8138
+骕 8139
+饹 8140
+cour@@ 8141
+臜 8142
+空 8143
+2@@ 8144
+允 8145
+枉 8146
+蒯 8147
+or@@ 8148
+趔 8149
+菠 8150
+栌 8151
+view 8152
+possi@@ 8153
+仗 8154
+鳄 8155
+摊 8156
+寝 8157
+躯 8158
+湝 8159
+后 8160
+删 8161
+揪 8162
+徐 8163
+寒 8164
+坨 8165
+挣 8166
+举 8167
+氅 8168
+臻 8169
+饔 8170
+丁 8171
+彦 8172
+泉 8173
+曷 8174
+衅 8175
+ei@@ 8176
+呸 8177
+狍 8178
+甓 8179
+禽 8180
+薄 8181
+shipping 8182
+ght 8183
+慵 8184
+幽 8185
+匣 8186
+炬 8187
+访 8188
+four 8189
+秒 8190
+樗 8191
+陶 8192
+脓 8193
+枰 8194
+磷 8195
+哕 8196
+君 8197
+脲 8198
+灯 8199
+回 8200
+索 8201
+璺 8202
+邡 8203
+愍 8204
+薏 8205
+辄 8206
+珽 8207
+離 8208
+鬃 8209
+赊 8210
+枲 8211
+邰 8212
+佷 8213
+颏 8214
+寺 8215
+抵 8216
+鲅 8217
+刎 8218
+給 8219
+戴 8220
+溲 8221
+袢 8222
+less 8223
+逯 8224
+封 8225
+food 8226
+匮 8227
+关 8228
+尘 8229
+皤 8230
+鳍 8231
+搏 8232
+幌 8233
+连 8234
+杠 8235
+蹋 8236
+彷 8237
+虺 8238
+怀 8239
+雏 8240
+钿 8241
+澄 8242
+扶 8243
+错 8244
+樋 8245
+黉 8246
+胄 8247
+薇 8248
+阿 8249
+墁 8250
+觜 8251
+利 8252
+汵 8253
+羟 8254
+杀 8255
+淋 8256
+蓁 8257
+腧 8258
+哞 8259
+was 8260
+钊 8261
+胸 8262
+艽 8263
+撞 8264
+虑 8265
+煋 8266
+飗 8267
+throu@@ 8268
+门 8269
+玦 8270
+琪 8271
+煸 8272
+酵 8273
+婌 8274
+serv@@ 8275
+硿 8276
+净 8277
+夐 8278
+撅 8279
+頠 8280
+want 8281
+犄 8282
+厉 8283
+p@@ 8284
+en 8285
+斗 8286
+层 8287
+玫 8288
+life 8289
+潘 8290
+骐 8291
+臃 8292
+谮 8293
+殃 8294
+厌 8295
+摄 8296
+磾 8297
+v 8298
+dress 8299
+络 8300
+囱 8301
+眯 8302
+忽 8303
+壸 8304
+咐 8305
+搁 8306
+肤 8307
+魇 8308
+芬 8309
+窡 8310
+拉 8311
+纮 8312
+楣 8313
+蔬 8314
+寄 8315
+妯 8316
+教 8317
+俪 8318
+颡 8319
+碛 8320
+互 8321
+奈 8322
+憎 8323
+炉 8324
+蹰 8325
+聂 8326
+员 8327
+呶 8328
+瞬 8329
+il@@ 8330
+恿 8331
+阙 8332
+卡 8333
+mi@@ 8334
+禾 8335
+椴 8336
+yo@@ 8337
+帀 8338
+醵 8339
+帝 8340
+隔 8341
+忒 8342
+哑 8343
+効 8344
+楗 8345
+鼱 8346
+塽 8347
+苴 8348
+蜞 8349
+健 8350
+醅 8351
+ju@@ 8352
+新 8353
+程 8354
+茗 8355
+琰 8356
+几 8357
+揍 8358
+匍 8359
+砣 8360
+禳 8361
+罗 8362
+勿 8363
+擗 8364
+畛 8365
+框 8366
+泒 8367
+析 8368
+沢 8369
+偷 8370
+繁 8371
+嗣 8372
+呵 8373
+念 8374
+so@@ 8375
+溷 8376
+曩 8377
+spon@@ 8378
+狼 8379
+倔 8380
+威 8381
+潭 8382
+踯 8383
+晁 8384
+吩 8385
+袅 8386
+喀 8387
+洌 8388
+炯 8389
+纸 8390
+抽 8391
+簧 8392
+c@@ 8393
+买 8394
+吖 8395
+俬 8396
+梓 8397
+叡 8398
+祼 8399
+烃 8400
+荃 8401
+眀 8402
+ 8403
diff --git a/app/src/main/assets/models/voice/bilingual-zipformer/decoder-epoch-99-avg-1.onnx b/app/src/main/assets/models/voice/bilingual-zipformer/decoder-epoch-99-avg-1.onnx
deleted file mode 100644
index ac9909ad..00000000
--- a/app/src/main/assets/models/voice/bilingual-zipformer/decoder-epoch-99-avg-1.onnx
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:2e3b5ec371f8899ee6acd829fd753ba45772df57a91bdf37cde3136354e7db7d
-size 13876452
diff --git a/app/src/main/assets/models/voice/bilingual-zipformer/encoder-epoch-99-avg-1.int8.onnx b/app/src/main/assets/models/voice/bilingual-zipformer/encoder-epoch-99-avg-1.int8.onnx
deleted file mode 100644
index 2be64b88..00000000
--- a/app/src/main/assets/models/voice/bilingual-zipformer/encoder-epoch-99-avg-1.int8.onnx
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:8fa764187a261844f859d7143ebaa563af5d10adfece4c18a8f414c88cba2a9b
-size 181895032
diff --git a/app/src/main/assets/models/voice/bilingual-zipformer/joiner-epoch-99-avg-1.int8.onnx b/app/src/main/assets/models/voice/bilingual-zipformer/joiner-epoch-99-avg-1.int8.onnx
deleted file mode 100644
index 3a445a3b..00000000
--- a/app/src/main/assets/models/voice/bilingual-zipformer/joiner-epoch-99-avg-1.int8.onnx
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:1ed689c5ed19dbaa725d9d191bb4822b5f4855a39e1ffd28cbc1f340d25b2ee0
-size 3228404
diff --git a/app/src/main/assets/models/voice/bilingual-zipformer/tokens.txt b/app/src/main/assets/models/voice/bilingual-zipformer/tokens.txt
deleted file mode 100644
index 980dd6cd..00000000
--- a/app/src/main/assets/models/voice/bilingual-zipformer/tokens.txt
+++ /dev/null
@@ -1,6257 +0,0 @@
- 0
- 1
- 2
-▁AS 3
-▁ONE 4
-▁OF 5
-▁A 6
-▁COMP 7
-AN 8
-Y 9
-▁SOME 10
-▁HUNDRED 11
-▁AND 12
-▁FI 13
-F 14
-T 15
-▁ME 16
-N 17
-▁WO 18
-M 19
-EN 20
-▁NOT 21
-▁LA 22
-BO 23
-UR 24
-ING 25
-▁UNDER 26
-▁ANY 27
-▁SU 28
-S 29
-P 30
-IC 31
-ION 32
-▁ 33
-LU 34
-AC 35
-▁THAT 36
-▁THE 37
-▁CO 38
-▁CHA 39
-CE 40
-RY 41
-▁THOUGH 42
-▁SH 43
-IN 44
-B 45
-J 46
-E 47
-C 48
-▁MUCH 49
-▁PO 50
-UL 51
-AR 52
-▁PRE 53
-U 54
-DI 55
-▁AT 56
-▁WHICH 57
-▁POINT 58
-▁I 59
-▁JU 60
-D 61
-GE 62
-▁E 63
-▁HAD 64
-▁CA 65
-▁IN 66
-▁MY 67
-▁DI 68
-RE 69
-CTION 70
-▁WAS 71
-L 72
-MO 73
-ATE 74
-▁THERE 75
-▁BEEN 76
-▁HE 77
-IT 78
-TED 79
-▁T 80
-RI 81
-VI 82
-AL 83
-▁B 84
-LE 85
-ISH 86
-▁OR 87
-▁SO 88
-▁IT 89
-▁RA 90
-TE 91
-▁PRO 92
-G 93
-▁BUT 94
-▁THIS 95
-▁EX 96
-A 97
-ER 98
-ATED 99
-▁EN 100
-TI 101
-LY 102
-OW 103
-▁TO 104
-▁PA 105
-R 106
-IM 107
-ON 108
-▁PUBLIC 109
-▁GU 110
-IL 111
-▁APP 112
-ED 113
-▁UN 114
-ENT 115
-▁MOST 116
-▁DE 117
-TER 118
-▁MAN 119
-▁ON 120
-▁BY 121
-▁NO 122
-▁MEAN 123
-▁NUMBER 124
-O 125
-▁BELIEVE 126
-▁RI 127
-CH 128
-▁SECOND 129
-▁OTHER 130
-▁K 131
-▁WILL 132
-▁DO 133
-▁WELL 134
-▁SEE 135
-UND 136
-▁JO 137
-KE 138
-▁BE 139
-▁BO 140
-OK 141
-▁COULD 142
-▁THEM 143
-▁THESE 144
-▁P 145
-AGE 146
-▁SHA 147
-ME 148
-IOUS 149
-▁IS 150
-▁ONLY 151
-▁WORD 152
-▁RE 153
-K 154
-I 155
-▁SHOULD 156
-▁HAVE 157
-OR 158
-ITY 159
-▁WHAT 160
-LL 161
-▁SP 162
-TA 163
-NE 164
-OUS 165
-▁COM 166
-US 167
-TION 168
-▁HAS 169
-▁S 170
-TH 171
-▁MI 172
-RO 173
-▁GOOD 174
-▁FRIEND 175
-▁LE 176
-W 177
-ES 178
-▁WITH 179
-▁WHO 180
-▁THINK 181
-▁MU 182
-IG 183
-▁MO 184
-▁MIGHT 185
-▁AN 186
-QU 187
-ATION 188
-▁FROM 189
-▁NA 190
-TURE 191
-▁ALL 192
-IES 193
-▁LET 194
-▁TIME 195
-▁WHEN 196
-▁EVEN 197
-▁NEED 198
-▁O 199
-VE 200
-▁WI 201
-FUL 202
-▁NE 203
-LI 204
-AD 205
-ERS 206
-▁BEFORE 207
-▁WORK 208
-▁LIKE 209
-▁HAND 210
-▁THEN 211
-SH 212
-▁WERE 213
-OM 214
-▁KNOW 215
-EL 216
-MA 217
-▁OVER 218
-▁LO 219
-TING 220
-▁LI 221
-OL 222
-▁HA 223
-MP 224
-LA 225
-ABLE 226
-MB 227
-▁WE 228
-▁ST 229
-ET 230
-▁IF 231
-▁WA 232
-▁NEW 233
-IR 234
-▁FA 235
-▁G 236
-IAN 237
-ND 238
-VER 239
-IS 240
-▁DIS 241
-CO 242
-UN 243
-▁SEVEN 244
-▁TH 245
-▁AFTER 246
-WARD 247
-▁RO 248
-▁WOULD 249
-▁SA 250
-▁FOR 251
-▁FE 252
-▁LONG 253
-▁STILL 254
-▁CON 255
-X 256
-▁HERE 257
-▁EVERYTHING 258
-▁SE 259
-ANT 260
-ALLY 261
-RU 262
-▁THAN 263
-ANCE 264
-▁ARE 265
-▁NEXT 266
-▁DOWN 267
-▁CHI 268
-▁MA 269
-CK 270
-▁DR 271
-Z 272
-▁F 273
-ROW 274
-▁GO 275
-▁INTO 276
-INE 277
-▁SIX 278
-▁YEAR 279
-▁HIS 280
-CI 281
-ENCE 282
-▁CLOSE 283
-▁LAST 284
-▁C 285
-▁MORE 286
-▁THOUSAND 287
-LO 288
-▁UP 289
-▁WANT 290
-▁JA 291
-▁GA 292
-▁THROUGH 293
-▁PLACE 294
-▁HU 295
-MAN 296
-UGH 297
-IST 298
-▁HO 299
-▁BETTER 300
-▁THEIR 301
-▁VERY 302
-NG 303
-▁ANOTHER 304
-▁UM 305
-AS 306
-▁TE 307
-V 308
-▁HER 309
-▁HIGH 310
-QUI 311
-▁BECAUSE 312
-▁SHOW 313
-▁WHERE 314
-▁DAY 315
-▁BRO 316
-▁OLD 317
-MENT 318
-▁HARD 319
-▁THOSE 320
-CU 321
-AT 322
-▁INTEREST 323
-▁EVERY 324
-HE 325
-▁GIVE 326
-▁FACT 327
-▁FOUND 328
-▁HEAR 329
-▁NEVER 330
-▁CAN 331
-▁COME 332
-▁SORT 333
-ITION 334
-EST 335
-IGHT 336
-SHIP 337
-SIDE 338
-▁GREAT 339
-▁MAR 340
-▁GET 341
-▁OUT 342
-▁STA 343
-OP 344
-AIN 345
-▁PI 346
-RA 347
-ID 348
-▁DA 349
-▁LOOK 350
-END 351
-▁BA 352
-VO 353
-H 354
-▁LITTLE 355
-▁THEY 356
-▁ALWAYS 357
-CA 358
-▁HOUSE 359
-TIC 360
-IVE 361
-▁RU 362
-HO 363
-▁RIGHT 364
-ICAL 365
-HA 366
-▁AGAIN 367
-▁EXP 368
-IBLE 369
-▁CERTAIN 370
-▁SHE 371
-▁PRI 372
-▁PLAY 373
-▁VA 374
-LIC 375
-▁TWO 376
-▁THREE 377
-▁DOES 378
-▁YOU 379
-▁TALK 380
-▁ABOUT 381
-UT 382
-▁WITHOUT 383
-▁PEOPLE 384
-FF 385
-▁DON 386
-▁HAPPEN 387
-▁PERSON 388
-▁MADE 389
-▁PART 390
-▁HOW 391
-▁WHY 392
-▁MAKE 393
-▁HIM 394
-▁STATE 395
-▁GRA 396
-▁TRA 397
-FOR 398
-▁LIFE 399
-▁PER 400
-AM 401
-▁DU 402
-PORT 403
-IA 404
-▁BUSINESS 405
-▁UNDERSTAND 406
-▁PLAN 407
-▁KEEP 408
-▁GOT 409
-▁MONEY 410
-▁OWN 411
-▁MANY 412
-ONE 413
-▁BU 414
-▁REALLY 415
-▁COURSE 416
-▁THINGS 417
-▁SAID 418
-▁US 419
-▁WAY 420
-▁TAKE 421
-▁WORLD 422
-▁THING 423
-▁SAY 424
-▁TA 425
-▁SOMETHING 426
-▁LEARN 427
-DE 428
-▁QUESTION 429
-DAY 430
-▁NOW 431
-▁BACK 432
-▁YOUR 433
-ACH 434
-▁SPEAK 435
-▁TURN 436
-ATIVE 437
-▁OUR 438
-▁JE 439
-▁VI 440
-▁HI 441
-▁OP 442
-▁WEEK 443
-▁IDEA 444
-▁AWAY 445
-▁COUNT 446
-NESS 447
-▁REASON 448
-PH 449
-▁TWENTY 450
-▁QUITE 451
-▁CHANGE 452
-▁LOVE 453
-DUC 454
-▁SAME 455
-▁ENOUGH 456
-▁YES 457
-▁FEEL 458
-▁FIRST 459
-▁WHILE 460
-IZE 461
-▁ANYTHING 462
-▁DID 463
-▁KIND 464
-▁POWER 465
-▁JUST 466
-IF 467
-▁PH 468
-▁ANSWER 469
-▁FIND 470
-▁THANK 471
-▁BUILD 472
-▁GOING 473
-▁CAME 474
-▁TOGETHER 475
-▁IMPORTANT 476
-▁HELP 477
-▁FOUR 478
-▁DIFFERENT 479
-▁AROUND 480
-▁AMERICA 481
-▁ALSO 482
-▁NINE 483
-▁AH 484
-▁LOT 485
-▁BETWEEN 486
-▁START 487
-▁SCHOOL 488
-▁SYSTEM 489
-▁MAYBE 490
-▁ACTUALLY 491
-▁PROBLEM 492
-Q 493
-▁MR 494
-▁YEAH 495
-▁OKAY 496
-以 497
-后 498
-你 499
-是 500
-男 501
-孩 502
-子 503
-曾 504
-丽 505
-婷 506
-妈 507
-很 508
-想 509
-兰 510
-州 511
-哪 512
-有 513
-买 514
-路 515
-虎 516
-汽 517
-车 518
-的 519
-我 520
-家 521
-狗 522
-叫 523
-什 524
-么 525
-名 526
-字 527
-现 528
-在 529
-网 530
-络 531
-怎 532
-样 533
-鞍 534
-山 535
-到 536
-郑 537
-大 538
-巴 539
-上 540
-没 541
-厕 542
-所 543
-英 544
-文 545
-给 546
-一 547
-个 548
-惊 549
-喜 550
-看 551
-日 552
-程 553
-表 554
-形 555
-容 556
-人 557
-强 558
-势 559
-面 560
-前 561
-不 562
-敢 563
-吭 564
-声 565
-词 566
-儿 567
-些 568
-办 569
-法 570
-说 571
-话 572
-唱 573
-首 574
-老 575
-与 576
-海 577
-婆 578
-笨 579
-蛋 580
-为 581
-欢 582
-女 583
-呢 584
-杭 585
-里 586
-可 587
-打 588
-炮 589
-朋 590
-友 591
-啊 592
-对 593
-练 594
-语 595
-听 596
-力 597
-帮 598
-助 599
-歌 600
-请 601
-推 602
-荐 603
-几 604
-谢 605
-明 606
-天 607
-早 608
-七 609
-点 610
-起 611
-床 612
-时 613
-候 614
-睡 615
-觉 616
-会 617
-了 618
-吗 619
-查 620
-本 621
-地 622
-气 623
-公 624
-快 625
-吧 626
-注 627
-意 628
-安 629
-全 630
-要 631
-错 632
-过 633
-四 634
-川 635
-再 636
-讲 637
-笑 638
-好 639
-午 640
-连 641
-烟 642
-台 643
-轮 644
-船 645
-史 646
-记 647
-商 648
-君 649
-列 650
-传 651
-孙 652
-鞅 653
-乃 654
-遂 655
-西 656
-入 657
-秦 658
-翻 659
-译 660
-做 661
-下 662
-提 663
-醒 664
-发 665
-送 666
-排 667
-班 668
-邮 669
-件 670
-刘 671
-俊 672
-峰 673
-电 674
-播 675
-放 676
-曲 677
-最 678
-炫 679
-民 680
-族 681
-风 682
-还 683
-年 684
-中 685
-泰 686
-拉 687
-石 688
-光 689
-剑 690
-任 691
-务 692
-玛 693
-雅 694
-预 695
-言 696
-真 697
-视 698
-频 699
-爱 700
-范 701
-冰 702
-出 703
-演 704
-剧 705
-六 706
-三 707
-十 708
-分 709
-退 710
-当 711
-模 712
-式 713
-附 714
-近 715
-洗 716
-浴 717
-方 718
-交 719
-和 720
-玩 721
-见 722
-工 723
-作 724
-干 725
-就 726
-能 727
-疖 728
-吃 729
-饭 730
-或 731
-者 732
-其 733
-他 734
-东 735
-国 736
-移 737
-动 738
-通 739
-信 740
-限 741
-司 742
-介 743
-绍 744
-讯 745
-录 746
-载 747
-装 748
-跟 749
-位 750
-置 751
-离 752
-莞 753
-长 754
-站 755
-多 756
-远 757
-福 758
-建 759
-高 760
-速 761
-服 762
-少 763
-把 764
-这 765
-定 766
-成 767
-彩 768
-铃 769
-手 770
-机 771
-别 772
-忘 773
-盒 774
-拿 775
-回 776
-校 777
-区 778
-万 779
-口 780
-坐 781
-冷 782
-漠 783
-醉 784
-红 785
-颜 786
-来 787
-猪 788
-张 789
-波 790
-炎 791
-亚 792
-纶 793
-媳 794
-妇 795
-马 796
-志 797
-华 798
-短 799
-清 800
-图 801
-片 802
-生 803
-五 804
-钟 805
-开 806
-启 807
-蓝 808
-牙 809
-锂 810
-池 811
-初 812
-次 813
-充 814
-让 815
-今 816
-号 817
-顷 818
-等 819
-于 820
-平 821
-也 822
-呀 823
-聊 824
-问 825
-主 826
-结 827
-婚 828
-恭 829
-情 830
-流 831
-返 832
-洒 833
-热 834
-泪 835
-诗 836
-那 837
-去 838
-眠 839
-药 840
-功 841
-使 842
-用 843
-象 844
-间 845
-估 846
-计 847
-厚 848
-德 849
-物 850
-思 851
-搞 852
-们 853
-只 854
-知 855
-道 856
-奥 857
-特 858
-曼 859
-越 860
-野 861
-比 862
-较 863
-重 864
-新 865
-陈 866
-奕 867
-迅 868
-泉 869
-湾 870
-票 871
-呵 872
-活 873
-经 874
-历 875
-摇 876
-痴 877
-此 878
-刻 879
-呈 880
-报 881
-脑 882
-总 883
-着 884
-她 885
-阿 886
-斯 887
-顿 888
-丁 889
-影 890
-终 891
-幻 892
-雨 893
-否 894
-带 895
-伞 896
-休 897
-息 898
-值 899
-百 900
-每 901
-月 902
-找 903
-扎 904
-屯 905
-线 906
-仙 907
-奇 908
-侠 909
-业 910
-写 911
-完 912
-姐 913
-稍 914
-烤 915
-鹅 916
-肠 917
-团 918
-狩 919
-猎 920
-美 921
-忍 922
-向 923
-冲 924
-常 925
-熟 926
-度 927
-折 928
-扣 929
-二 930
-乘 931
-教 932
-实 933
-认 934
-证 935
-考 936
-试 937
-答 938
-案 939
-费 940
-脸 941
-自 942
-恋 943
-广 944
-府 945
-待 946
-赶 947
-集 948
-告 949
-诉 950
-太 951
-懂 952
-坏 953
-泡 954
-妞 955
-伊 956
-更 957
-健 958
-康 959
-检 960
-乙 961
-肝 962
-厦 963
-门 964
-急 965
-控 966
-毒 967
-产 968
-头 969
-脚 970
-轻 971
-感 972
-空 973
-订 974
-从 975
-碑 976
-店 977
-北 978
-京 979
-火 980
-鸡 981
-白 982
-态 983
-啥 984
-幺 985
-哈 986
-尔 987
-滨 988
-至 989
-硬 990
-座 991
-换 992
-姿 993
-亲 994
-级 995
-紫 996
-秋 997
-叶 998
-刀 999
-爆 1000
-率 1001
-馨 1002
-予 1003
-帅 1004
-汇 1005
-询 1006
-小 1007
-唉 1008
-性 1009
-挺 1010
-厉 1011
-害 1012
-八 1013
-米 1014
-跑 1015
-步 1016
-达 1017
-标 1018
-规 1019
-准 1020
-码 1021
-音 1022
-拨 1023
-器 1024
-油 1025
-价 1026
-卡 1027
-尿 1028
-防 1029
-杀 1030
-死 1031
-倍 1032
-晋 1033
-映 1034
-部 1035
-韩 1036
-己 1037
-右 1038
-键 1039
-编 1040
-辑 1041
-您 1042
-房 1043
-租 1044
-具 1045
-体 1046
-牌 1047
-瑞 1048
-士 1049
-浪 1050
-琴 1051
-简 1052
-永 1053
-拥 1054
-灿 1055
-烂 1056
-朝 1057
-阳 1058
-解 1059
-赢 1060
-事 1061
-滚 1062
-周 1063
-吕 1064
-忠 1065
-页 1066
-心 1067
-份 1068
-被 1069
-龙 1070
-肖 1071
-半 1072
-喊 1073
-改 1074
-堵 1075
-饿 1076
-瞌 1077
-叔 1078
-戏 1079
-兴 1080
-化 1081
-备 1082
-萍 1083
-乡 1084
-钓 1085
-鱼 1086
-岛 1087
-消 1088
-忻 1089
-襄 1090
-县 1091
-况 1092
-得 1093
-王 1094
-属 1095
-假 1096
-期 1097
-像 1098
-脾 1099
-零 1100
-九 1101
-关 1102
-晚 1103
-陪 1104
-危 1105
-苦 1106
-难 1107
-例 1108
-顺 1109
-序 1110
-盖 1111
-茨 1112
-邦 1113
-故 1114
-警 1115
-戒 1116
-搜 1117
-索 1118
-黄 1119
-照 1120
-底 1121
-识 1122
-武 1123
-汉 1124
-第 1125
-博 1126
-基 1127
-尼 1128
-删 1129
-除 1130
-沃 1131
-狐 1132
-场 1133
-卖 1134
-便 1135
-宜 1136
-营 1137
-厅 1138
-资 1139
-料 1140
-哥 1141
-句 1142
-员 1143
-随 1144
-导 1145
-航 1146
-学 1147
-星 1148
-脱 1149
-毛 1150
-膏 1151
-种 1152
-乐 1153
-贵 1154
-妃 1155
-酒 1156
-内 1157
-条 1158
-联 1159
-系 1160
-希 1161
-望 1162
-非 1163
-苹 1164
-果 1165
-银 1166
-登 1167
-户 1168
-密 1169
-师 1170
-沟 1171
-莫 1172
-才 1173
-喝 1174
-累 1175
-彭 1176
-裘 1177
-莹 1178
-珠 1179
-徐 1180
-管 1181
-爸 1182
-敏 1183
-腰 1184
-闪 1185
-舟 1186
-凯 1187
-肯 1188
-水 1189
-济 1190
-闻 1191
-约 1192
-伦 1193
-乱 1194
-舞 1195
-春 1196
-叮 1197
-咚 1198
-昨 1199
-又 1200
-宵 1201
-省 1202
-途 1203
-无 1204
-院 1205
-吉 1206
-农 1207
-股 1208
-胶 1209
-布 1210
-谁 1211
-免 1212
-疫 1213
-逗 1214
-闹 1215
-闭 1216
-青 1217
-景 1218
-花 1219
-园 1220
-富 1221
-桥 1222
-哭 1223
-节 1224
-树 1225
-茂 1226
-盛 1227
-边 1228
-余 1229
-姚 1230
-走 1231
-原 1232
-行 1233
-都 1234
-旅 1235
-馆 1236
-宾 1237
-根 1238
-修 1239
-理 1240
-厂 1241
-板 1242
-诊 1243
-专 1244
-复 1245
-鹰 1246
-队 1247
-桌 1248
-财 1249
-港 1250
-色 1251
-诚 1252
-勿 1253
-扰 1254
-持 1255
-孟 1256
-古 1257
-医 1258
-研 1259
-究 1260
-取 1261
-卫 1262
-目 1263
-铁 1264
-麻 1265
-将 1266
-浏 1267
-览 1268
-湿 1269
-朱 1270
-沱 1271
-合 1272
-江 1273
-何 1274
-祈 1275
-祷 1276
-义 1277
-酷 1278
-派 1279
-如 1280
-咩 1281
-正 1282
-算 1283
-楼 1284
-距 1285
-震 1286
-借 1287
-政 1288
-策 1289
-温 1290
-宝 1291
-沙 1292
-角 1293
-肚 1294
-疼 1295
-林 1296
-宿 1297
-舍 1298
-阴 1299
-晴 1300
-圆 1301
-缺 1302
-微 1303
-辽 1304
-刚 1305
-牛 1306
-偷 1307
-吓 1308
-跳 1309
-城 1310
-吴 1311
-称 1312
-呼 1313
-爷 1314
-埋 1315
-怨 1316
-缠 1317
-柴 1318
-钱 1319
-极 1320
-先 1321
-辞 1322
-职 1323
-哦 1324
-啦 1325
-售 1326
-保 1327
-黑 1328
-客 1329
-立 1330
-足 1331
-之 1332
-鬼 1333
-留 1334
-辟 1335
-邪 1336
-谱 1337
-减 1338
-肥 1339
-皇 1340
-萨 1341
-舅 1342
-幽 1343
-默 1344
-细 1345
-胞 1346
-溪 1347
-懒 1348
-书 1349
-杨 1350
-慧 1351
-芝 1352
-屁 1353
-画 1354
-晨 1355
-宁 1356
-侦 1357
-探 1358
-柯 1359
-南 1360
-涛 1361
-应 1362
-该 1363
-弑 1364
-神 1365
-魔 1366
-创 1367
-世 1368
-游 1369
-犯 1370
-已 1371
-泽 1372
-村 1373
-变 1374
-奏 1375
-杰 1376
-偶 1377
-命 1378
-乔 1379
-恩 1380
-并 1381
-阅 1382
-读 1383
-左 1384
-般 1385
-低 1386
-调 1387
-阔 1388
-烦 1389
-球 1390
-峡 1391
-界 1392
-霆 1393
-锋 1394
-柏 1395
-需 1396
-艺 1397
-术 1398
-弄 1399
-章 1400
-悲 1401
-咒 1402
-版 1403
-统 1404
-羊 1405
-肉 1406
-斤 1407
-嘿 1408
-郁 1409
-闷 1410
-进 1411
-姨 1412
-庆 1413
-威 1414
-两 1415
-岸 1416
-普 1417
-琪 1418
-玫 1419
-瑰 1420
-香 1421
-碟 1422
-眼 1423
-收 1424
-湖 1425
-禽 1426
-涮 1427
-汪 1428
-盘 1429
-夹 1430
-存 1431
-枕 1432
-指 1433
-针 1434
-仓 1435
-库 1436
-餐 1437
-块 1438
-咪 1439
-毕 1440
-薪 1441
-赛 1442
-纪 1443
-末 1444
-典 1445
-娱 1446
-傻 1447
-嘲 1448
-彪 1449
-升 1450
-润 1451
-核 1452
-遍 1453
-接 1454
-元 1455
-鼓 1456
-屿 1457
-爹 1458
-胡 1459
-雪 1460
-招 1461
-喂 1462
-齿 1463
-侣 1464
-土 1465
-豆 1466
-铿 1467
-锵 1468
-聪 1469
-但 1470
-飞 1471
-鹤 1472
-壁 1473
-摞 1474
-久 1475
-冬 1476
-骏 1477
-然 1478
-讨 1479
-论 1480
-腾 1481
-易 1482
-筋 1483
-转 1484
-弯 1485
-耳 1486
-齐 1487
-阶 1488
-革 1489
-代 1490
-许 1491
-圣 1492
-诞 1493
-吸 1494
-血 1495
-燕 1496
-松 1497
-鼠 1498
-确 1499
-凤 1500
-凰 1501
-由 1502
-翔 1503
-段 1504
-超 1505
-精 1506
-支 1507
-扶 1508
-室 1509
-包 1510
-菜 1511
-田 1512
-骂 1513
-洋 1514
-舒 1515
-衡 1516
-款 1517
-嘴 1518
-菲 1519
-嘛 1520
-嗨 1521
-鸟 1522
-玉 1523
-数 1524
-贝 1525
-郝 1526
-漫 1527
-诺 1528
-衣 1529
-嫖 1530
-娼 1531
-穿 1532
-骨 1533
-拆 1534
-伤 1535
-获 1536
-奖 1537
-稿 1538
-锅 1539
-购 1540
-治 1541
-痘 1542
-遗 1543
-疤 1544
-痕 1545
-饱 1546
-寻 1547
-瓷 1548
-妹 1549
-徽 1550
-参 1551
-格 1552
-题 1553
-凭 1554
-住 1555
-剖 1556
-腹 1557
-祝 1558
-贺 1559
-加 1560
-跪 1561
-潞 1562
-云 1563
-端 1564
-木 1565
-烁 1566
-朵 1567
-赵 1568
-潘 1569
-津 1570
-滋 1571
-燥 1572
-利 1573
-负 1574
-昆 1575
-因 1576
-森 1577
-及 1578
-病 1579
-固 1580
-市 1581
-烧 1582
-番 1583
-茄 1584
-炒 1585
-而 1586
-凉 1587
-冒 1588
-量 1589
-夸 1590
-尾 1591
-崔 1592
-另 1593
-处 1594
-铺 1595
-沈 1596
-哎 1597
-身 1598
-哟 1599
-习 1600
-虞 1601
-瞧 1602
-烈 1603
-皮 1604
-鞋 1605
-深 1606
-圳 1607
-委 1608
-胖 1609
-猴 1610
-军 1611
-素 1612
-楷 1613
-补 1614
-偿 1615
-屏 1616
-散 1617
-效 1618
-丹 1619
-念 1620
-绝 1621
-艳 1622
-够 1623
-狼 1624
-且 1625
-龄 1626
-乌 1627
-蓉 1628
-厘 1629
-含 1630
-庚 1631
-澈 1632
-犬 1633
-致 1634
-运 1635
-慢 1636
-钻 1637
-李 1638
-轩 1639
-育 1640
-项 1641
-咱 1642
-误 1643
-弟 1644
-依 1645
-尽 1646
-河 1647
-夫 1648
-沁 1649
-始 1650
-芳 1651
-禺 1652
-旧 1653
-坑 1654
-胜 1655
-酱 1656
-漂 1657
-亮 1658
-战 1659
-斗 1660
-严 1661
-娟 1662
-逼 1663
-添 1664
-盈 1665
-萝 1666
-莉 1667
-肌 1668
-唐 1669
-兵 1670
-辆 1671
-双 1672
-佛 1673
-傅 1674
-劲 1675
-直 1676
-测 1677
-苏 1678
-迁 1679
-沭 1680
-祥 1681
-婴 1682
-品 1683
-销 1684
-禹 1685
-哲 1686
-嗯 1687
-趟 1688
-拐 1689
-金 1690
-满 1691
-套 1692
-倒 1693
-千 1694
-迎 1695
-淇 1696
-驾 1697
-拟 1698
-良 1699
-揭 1700
-杯 1701
-淄 1702
-睛 1703
-制 1704
-枪 1705
-抢 1706
-狸 1707
-泥 1708
-造 1709
-哇 1710
-羯 1711
-庙 1712
-逃 1713
-朗 1714
-领 1715
-悟 1716
-湛 1717
-贸 1718
-垃 1719
-圾 1720
-软 1721
-莲 1722
-味 1723
-旺 1724
-旦 1725
-潮 1726
-奶 1727
-央 1728
-惜 1729
-续 1730
-咨 1731
-茅 1732
-父 1733
-母 1734
-笔 1735
-封 1736
-同 1737
-黎 1738
-共 1739
-科 1740
-相 1741
-镇 1742
-贤 1743
-宏 1744
-洲 1745
-瓦 1746
-寨 1747
-受 1748
-梦 1749
-呗 1750
-苍 1751
-丘 1752
-避 1753
-孕 1754
-灰 1755
-尤 1756
-击 1757
-腮 1758
-腺 1759
-兆 1760
-坨 1761
-屎 1762
-忧 1763
-草 1764
-赠 1765
-外 1766
-险 1767
-某 1768
-磁 1769
-贷 1770
-反 1771
-罚 1772
-昌 1773
-饰 1774
-辉 1775
-权 1776
-材 1777
-炯 1778
-签 1779
-追 1780
-求 1781
-催 1782
-痛 1783
-盱 1784
-眙 1785
-围 1786
-淘 1787
-幸 1788
-扫 1789
-旭 1790
-切 1791
-磋 1792
-冠 1793
-征 1794
-郎 1795
-骗 1796
-哄 1797
-释 1798
-蒙 1799
-涯 1800
-碧 1801
-斜 1802
-嫁 1803
-幕 1804
-哼 1805
-钢 1806
-碳 1807
-纤 1808
-维 1809
-决 1810
-妻 1811
-未 1812
-绳 1813
-断 1814
-寺 1815
-妙 1816
-伯 1817
-蹲 1818
-宅 1819
-吵 1820
-娃 1821
-兄 1822
-廷 1823
-夺 1824
-社 1825
-示 1826
-按 1827
-饺 1828
-甜 1829
-蜜 1830
-咖 1831
-啡 1832
-优 1833
-逛 1834
-街 1835
-著 1836
-杂 1837
-址 1838
-荷 1839
-塘 1840
-拜 1841
-币 1842
-迟 1843
-货 1844
-粉 1845
-刮 1846
-破 1847
-射 1848
-狂 1849
-苗 1850
-罗 1851
-设 1852
-困 1853
-湘 1854
-潭 1855
-评 1856
-娘 1857
-涉 1858
-采 1859
-芙 1860
-夜 1861
-捷 1862
-斩 1863
-摩 1864
-托 1865
-泳 1866
-琳 1867
-律 1868
-官 1869
-劫 1870
-蛮 1871
-替 1872
-架 1873
-悬 1874
-浮 1875
-窗 1876
-顶 1877
-敦 1878
-善 1879
-哉 1880
-桂 1881
-勇 1882
-荆 1883
-镜 1884
-监 1885
-怕 1886
-呐 1887
-劳 1888
-莱 1889
-狮 1890
-宽 1891
-袋 1892
-囊 1893
-秀 1894
-卸 1895
-链 1896
-嵩 1897
-韭 1898
-葆 1899
-额 1900
-翼 1901
-忙 1902
-瓶 1903
-梅 1904
-堰 1905
-粒 1906
-汤 1907
-谋 1908
-樊 1909
-恨 1910
-愿 1911
-锡 1912
-申 1913
-护 1914
-庄 1915
-临 1916
-源 1917
-环 1918
-境 1919
-礼 1920
-恐 1921
-晒 1922
-虫 1923
-划 1924
-鸣 1925
-怖 1926
-伍 1927
-佰 1928
-岁 1929
-组 1930
-响 1931
-类 1932
-韶 1933
-克 1934
-洛 1935
-玲 1936
-裤 1937
-柔 1938
-疆 1939
-篮 1940
-伟 1941
-扔 1942
-掉 1943
-媒 1944
-涨 1945
-透 1946
-纯 1947
-怀 1948
-坊 1949
-麦 1950
-菠 1951
-养 1952
-晕 1953
-群 1954
-展 1955
-厌 1956
-拒 1957
-单 1958
-静 1959
-刷 1960
-插 1961
-肛 1962
-互 1963
-蘑 1964
-菇 1965
-姑 1966
-桐 1967
-辛 1968
-察 1969
-毫 1970
-质 1971
-差 1972
-翰 1973
-爽 1974
-欣 1975
-议 1976
-铜 1977
-籍 1978
-争 1979
-喆 1980
-孔 1981
-堤 1982
-薇 1983
-茵 1984
-席 1985
-琼 1986
-杠 1987
-衔 1988
-概 1989
-往 1990
-邢 1991
-惠 1992
-烫 1993
-绑 1994
-崇 1995
-帝 1996
-据 1997
-貌 1998
-似 1999
-胸 2000
-罩 2001
-构 2002
-尊 2003
-秘 2004
-它 2005
-详 2006
-悠 2007
-闲 2008
-违 2009
-陆 2010
-割 2011
-绩 2012
-企 2013
-绥 2014
-辐 2015
-舌 2016
-寂 2017
-寞 2018
-宇 2019
-携 2020
-拳 2021
-观 2022
-魏 2023
-郭 2024
-磊 2025
-副 2026
-梁 2027
-斌 2028
-须 2029
-僧 2030
-徒 2031
-季 2032
-灯 2033
-梯 2034
-墙 2035
-付 2036
-坦 2037
-殊 2038
-曹 2039
-操 2040
-捡 2041
-赤 2042
-盗 2043
-废 2044
-蒋 2045
-浙 2046
-食 2047
-咯 2048
-童 2049
-坡 2050
-剪 2051
-唯 2052
-疗 2053
-状 2054
-暴 2055
-缓 2056
-誉 2057
-衰 2058
-宋 2059
-娜 2060
-雄 2061
-谛 2062
-糖 2063
-羽 2064
-棋 2065
-滩 2066
-佳 2067
-臭 2068
-帆 2069
-岳 2070
-疲 2071
-惫 2072
-滴 2073
-倾 2074
-盆 2075
-谷 2076
-施 2077
-晶 2078
-赚 2079
-澡 2080
-遇 2081
-鲁 2082
-祭 2083
-灶 2084
-独 2085
-谈 2086
-承 2087
-蜀 2088
-丰 2089
-归 2090
-辜 2091
-扇 2092
-渴 2093
-羡 2094
-慕 2095
-裸 2096
-宗 2097
-纬 2098
-亦 2099
-儒 2100
-霸 2101
-翡 2102
-翠 2103
-芭 2104
-抱 2105
-歉 2106
-邱 2107
-夏 2108
-隆 2109
-灵 2110
-珍 2111
-浩 2112
-乾 2113
-坤 2114
-培 2115
-训 2116
-压 2117
-偏 2118
-骤 2119
-熙 2120
-葬 2121
-姆 2122
-兽 2123
-筑 2124
-丝 2125
-若 2126
-诡 2127
-异 2128
-侯 2129
-摆 2130
-俗 2131
-缚 2132
-束 2133
-愁 2134
-盟 2135
-却 2136
-显 2137
-肤 2138
-茹 2139
-荣 2140
-增 2141
-宫 2142
-局 2143
-适 2144
-楚 2145
-驻 2146
-纽 2147
-秒 2148
-辣 2149
-虾 2150
-甘 2151
-肃 2152
-粕 2153
-喻 2154
-敬 2155
-谨 2156
-慎 2157
-竭 2158
-止 2159
-际 2160
-寓 2161
-勤 2162
-挫 2163
-泣 2164
-奈 2165
-圭 2166
-焰 2167
-猩 2168
-守 2169
-允 2170
-兔 2171
-篇 2172
-敌 2173
-辕 2174
-猫 2175
-柠 2176
-檬 2177
-橘 2178
-卜 2179
-妓 2180
-既 2181
-闯 2182
-胆 2183
-刁 2184
-竟 2185
-竞 2186
-冯 2187
-陇 2188
-赣 2189
-呆 2190
-滞 2191
-停 2192
-邯 2193
-郸 2194
-域 2195
-徕 2196
-患 2197
-甲 2198
-亡 2199
-鼻 2200
-背 2201
-戴 2202
-幼 2203
-伙 2204
-括 2205
-邓 2206
-谐 2207
-担 2208
-浑 2209
-抖 2210
-耍 2211
-综 2212
-失 2213
-蕾 2214
-鸭 2215
-莘 2216
-选 2217
-糸 2218
-桶 2219
-弃 2220
-暗 2221
-卓 2222
-榜 2223
-拼 2224
-壅 2225
-丈 2226
-锈 2227
-恢 2228
-刺 2229
-嘻 2230
-顾 2231
-投 2232
-晓 2233
-巨 2234
-抽 2235
-档 2236
-乳 2237
-迪 2238
-蠢 2239
-裹 2240
-唤 2241
-焦 2242
-择 2243
-俺 2244
-技 2245
-暧 2246
-昧 2247
-怪 2248
-坛 2249
-眉 2250
-嘉 2251
-逸 2252
-课 2253
-栏 2254
-撬 2255
-框 2256
-液 2257
-凝 2258
-暑 2259
-型 2260
-烘 2261
-簿 2262
-扬 2263
-汁 2264
-诸 2265
-迹 2266
-禁 2267
-株 2268
-泸 2269
-屠 2270
-宰 2271
-忽 2272
-炼 2273
-必 2274
-妆 2275
-飘 2276
-鹿 2277
-敲 2278
-拾 2279
-躺 2280
-歇 2281
-狠 2282
-沾 2283
-畅 2284
-镕 2285
-贪 2286
-污 2287
-斧 2288
-巡 2289
-弹 2290
-盐 2291
-枝 2292
-渝 2293
-壑 2294
-郴 2295
-落 2296
-牡 2297
-钛 2298
-剩 2299
-俄 2300
-抵 2301
-押 2302
-郊 2303
-弱 2304
-授 2305
-蟹 2306
-糕 2307
-败 2308
-各 2309
-伴 2310
-享 2311
-居 2312
-障 2313
-棕 2314
-旁 2315
-屌 2316
-绒 2317
-酸 2318
-隔 2319
-瞄 2320
-俩 2321
-柳 2322
-册 2323
-弊 2324
-逮 2325
-绵 2326
-挣 2327
-闵 2328
-勒 2329
-陶 2330
-寒 2331
-吻 2332
-桃 2333
-悍 2334
-绮 2335
-贞 2336
-疾 2337
-诫 2338
-菌 2339
-则 2340
-谭 2341
-咏 2342
-麟 2343
-棠 2344
-抬 2345
-棺 2346
-均 2347
-纸 2348
-碱 2349
-沧 2350
-董 2351
-挤 2352
-虚 2353
-钠 2354
-胃 2355
-躁 2356
-智 2357
-畔 2358
-墨 2359
-堂 2360
-喔 2361
-宣 2362
-丑 2363
-嚣 2364
-辈 2365
-孤 2366
-鞭 2367
-验 2368
-夕 2369
-印 2370
-欧 2371
-阵 2372
-咋 2373
-驹 2374
-挂 2375
-轿 2376
-拍 2377
-洁 2378
-凑 2379
-蕉 2380
-诱 2381
-惑 2382
-颐 2383
-箭 2384
-樱 2385
-辖 2386
-捕 2387
-炸 2388
-斋 2389
-恒 2390
-沉 2391
-侧 2392
-跌 2393
-暇 2394
-掌 2395
-筷 2396
-彬 2397
-稚 2398
-傲 2399
-腔 2400
-藏 2401
-浦 2402
-瓣 2403
-捆 2404
-卧 2405
-欠 2406
-犀 2407
-甩 2408
-敷 2409
-衍 2410
-谅 2411
-积 2412
-怡 2413
-阁 2414
-趣 2415
-掰 2416
-耽 2417
-蒜 2418
-菱 2419
-葛 2420
-聚 2421
-露 2422
-帐 2423
-紧 2424
-郓 2425
-聘 2426
-桑 2427
-众 2428
-圈 2429
-渡 2430
-鲜 2431
-杜 2432
-甫 2433
-遵 2434
-骚 2435
-吹 2436
-蚊 2437
-塔 2438
-赏 2439
-荒 2440
-欺 2441
-揍 2442
-锁 2443
-恼 2444
-忐 2445
-忑 2446
-输 2447
-描 2448
-触 2449
-糊 2450
-涂 2451
-熊 2452
-妮 2453
-抄 2454
-裙 2455
-塑 2456
-橡 2457
-阜 2458
-獒 2459
-励 2460
-黔 2461
-臣 2462
-憔 2463
-悴 2464
-昂 2465
-党 2466
-沐 2467
-浓 2468
-灾 2469
-捐 2470
-柿 2471
-瑟 2472
-翁 2473
-侨 2474
-督 2475
-振 2476
-鹏 2477
-乒 2478
-乓 2479
-巷 2480
-贡 2481
-祖 2482
-即 2483
-绿 2484
-搭 2485
-配 2486
-骑 2487
-届 2488
-举 2489
-伸 2490
-整 2491
-突 2492
-陌 2493
-糟 2494
-惩 2495
-硕 2496
-茫 2497
-趋 2498
-仁 2499
-钙 2500
-雕 2501
-井 2502
-撒 2503
-岩 2504
-悄 2505
-搁 2506
-浅 2507
-救 2508
-饮 2509
-佩 2510
-赌 2511
-涕 2512
-薯 2513
-令 2514
-泌 2515
-蔬 2516
-批 2517
-攀 2518
-怜 2519
-淮 2520
-寝 2521
-填 2522
-卿 2523
-萱 2524
-寄 2525
-窝 2526
-纳 2527
-洱 2528
-惹 2529
-锦 2530
-浒 2531
-欲 2532
-棉 2533
-箱 2534
-仅 2535
-述 2536
-摸 2537
-纲 2538
-澳 2539
-染 2540
-兼 2541
-岭 2542
-淋 2543
-肿 2544
-旗 2545
-嘞 2546
-乖 2547
-酮 2548
-颠 2549
-覆 2550
-誓 2551
-递 2552
-蛟 2553
-占 2554
-乎 2555
-融 2556
-甸 2557
-幂 2558
-钥 2559
-匙 2560
-酬 2561
-皆 2562
-胎 2563
-腐 2564
-痿 2565
-绣 2566
-枫 2567
-蝴 2568
-蝶 2569
-抛 2570
-撞 2571
-植 2572
-僵 2573
-尸 2574
-巾 2575
-煌 2576
-逊 2577
-引 2578
-兑 2579
-荫 2580
-朔 2581
-丢 2582
-扩 2583
-摄 2584
-龟 2585
-鑫 2586
-谦 2587
-豪 2588
-噬 2589
-眷 2590
-挑 2591
-仲 2592
-穷 2593
-玻 2594
-璃 2595
-岗 2596
-姥 2597
-横 2598
-蚌 2599
-埠 2600
-邀 2601
-蔚 2602
-虹 2603
-降 2604
-疣 2605
-鱿 2606
-喵 2607
-囧 2608
-茶 2609
-猜 2610
-玮 2611
-莎 2612
-冼 2613
-榕 2614
-媛 2615
-瓜 2616
-煮 2617
-耕 2618
-镶 2619
-虽 2620
-驳 2621
-霍 2622
-仗 2623
-窍 2624
-魅 2625
-访 2626
-邻 2627
-抗 2628
-莆 2629
-涵 2630
-筒 2631
-疯 2632
-赖 2633
-豌 2634
-碍 2635
-症 2636
-卤 2637
-翅 2638
-膀 2639
-蓬 2640
-咸 2641
-尚 2642
-瘦 2643
-缸 2644
-爬 2645
-鄂 2646
-塞 2647
-稻 2648
-召 2649
-荡 2650
-桨 2651
-税 2652
-呃 2653
-渠 2654
-骥 2655
-伏 2656
-枥 2657
-邑 2658
-净 2659
-弦 2660
-蔽 2661
-诀 2662
-咳 2663
-嗽 2664
-芯 2665
-储 2666
-缘 2667
-冻 2668
-厨 2669
-鉴 2670
-擦 2671
-棒 2672
-损 2673
-暂 2674
-殖 2675
-焊 2676
-募 2677
-邵 2678
-饶 2679
-梭 2680
-鄙 2681
-骄 2682
-蔡 2683
-辄 2684
-努 2685
-洽 2686
-宙 2687
-鲈 2688
-葫 2689
-芦 2690
-梧 2691
-燎 2692
-缴 2693
-薄 2694
-执 2695
-垫 2696
-靠 2697
-拢 2698
-萧 2699
-醋 2700
-脊 2701
-慰 2702
-攻 2703
-狱 2704
-吝 2705
-啬 2706
-煤 2707
-楞 2708
-脏 2709
-迷 2710
-椒 2711
-侄 2712
-璇 2713
-耐 2714
-庵 2715
-帽 2716
-崎 2717
-峻 2718
-援 2719
-娶 2720
-丫 2721
-犹 2722
-豫 2723
-罪 2724
-恶 2725
-陛 2726
-樟 2727
-截 2728
-巧 2729
-驰 2730
-轨 2731
-继 2732
-葱 2733
-蘸 2734
-汕 2735
-蜘 2736
-蛛 2737
-聋 2738
-俱 2739
-捉 2740
-卢 2741
-骆 2742
-氓 2743
-耶 2744
-仔 2745
-激 2746
-渊 2747
-钾 2748
-暖 2749
-钰 2750
-裁 2751
-判 2752
-略 2753
-墓 2754
-洪 2755
-凌 2756
-符 2757
-壮 2758
-陵 2759
-挥 2760
-夷 2761
-尘 2762
-沪 2763
-榆 2764
-涧 2765
-析 2766
-孝 2767
-弘 2768
-椅 2769
-贴 2770
-蛇 2771
-浣 2772
-镯 2773
-枣 2774
-佐 2775
-柑 2776
-谓 2777
-洞 2778
-漳 2779
-撕 2780
-叉 2781
-诛 2782
-糯 2783
-粽 2784
-碎 2785
-幅 2786
-赘 2787
-浆 2788
-循 2789
-偕 2790
-诙 2791
-阚 2792
-摘 2793
-串 2794
-悉 2795
-蜕 2796
-残 2797
-诅 2798
-祁 2799
-仪 2800
-璐 2801
-瑶 2802
-楠 2803
-崂 2804
-供 2805
-掖 2806
-椎 2807
-铆 2808
-钉 2809
-铐 2810
-镣 2811
-栋 2812
-潇 2813
-抓 2814
-屋 2815
-鸦 2816
-玄 2817
-芜 2818
-钨 2819
-毯 2820
-矿 2821
-缩 2822
-酶 2823
-焕 2824
-埃 2825
-霞 2826
-噢 2827
-韵 2828
-艾 2829
-虐 2830
-俘 2831
-颗 2832
-巩 2833
-牵 2834
-汝 2835
-搅 2836
-廉 2837
-啤 2838
-苑 2839
-辍 2840
-缝 2841
-纫 2842
-膜 2843
-娄 2844
-倩 2845
-魂 2846
-姜 2847
-彻 2848
-扉 2849
-镁 2850
-氢 2851
-铝 2852
-淀 2853
-雹 2854
-妍 2855
-鼎 2856
-碗 2857
-亭 2858
-闽 2859
-献 2860
-耻 2861
-畜 2862
-蚯 2863
-蚓 2864
-杆 2865
-靓 2866
-颖 2867
-瘾 2868
-腿 2869
-咧 2870
-嗦 2871
-忆 2872
-卑 2873
-鸽 2874
-藤 2875
-滑 2876
-蝇 2877
-蚂 2878
-蚁 2879
-迢 2880
-蝎 2881
-斑 2882
-赞 2883
-氧 2884
-姝 2885
-擎 2886
-憋 2887
-屈 2888
-讶 2889
-袜 2890
-吐 2891
-秽 2892
-哑 2893
-蓦 2894
-阑 2895
-珊 2896
-披 2897
-巫 2898
-妖 2899
-坪 2900
-疏 2901
-抒 2902
-炉 2903
-舰 2904
-贱 2905
-搬 2906
-遥 2907
-燃 2908
-咽 2909
-喉 2910
-熔 2911
-婵 2912
-奔 2913
-汗 2914
-蓄 2915
-辰 2916
-肩 2917
-洮 2918
-琅 2919
-径 2920
-廊 2921
-姬 2922
-衬 2923
-雯 2924
-滁 2925
-泗 2926
-筹 2927
-诵 2928
-奴 2929
-跨 2930
-娴 2931
-绯 2932
-惯 2933
-谎 2934
-蹈 2935
-潜 2936
-搂 2937
-逆 2938
-钞 2939
-辅 2940
-凶 2941
-橱 2942
-柜 2943
-婕 2944
-矮 2945
-邹 2946
-嫂 2947
-饼 2948
-撸 2949
-壶 2950
-握 2951
-鸳 2952
-鸯 2953
-寸 2954
-堕 2955
-哀 2956
-械 2957
-蜈 2958
-蚣 2959
-袁 2960
-鸿 2961
-穆 2962
-泊 2963
-衮 2964
-弗 2965
-雷 2966
-谜 2967
-俞 2968
-灏 2969
-毅 2970
-迈 2971
-蜂 2972
-辨 2973
-沂 2974
-灭 2975
-腊 2976
-脍 2977
-炙 2978
-卦 2979
-霄 2980
-扯 2981
-泾 2982
-脂 2983
-肪 2984
-淹 2985
-灌 2986
-辱 2987
-丸 2988
-账 2989
-秤 2990
-褐 2991
-芬 2992
-窖 2993
-慈 2994
-益 2995
-亿 2996
-颈 2997
-糜 2998
-隋 2999
-霉 3000
-署 3001
-狄 3002
-酪 3003
-旋 3004
-蔷 3005
-皱 3006
-纹 3007
-枯 3008
-粤 3009
-拔 3010
-菩 3011
-驱 3012
-咦 3013
-掀 3014
-菊 3015
-涩 3016
-耀 3017
-娥 3018
-奘 3019
-眯 3020
-芊 3021
-绪 3022
-沛 3023
-锐 3024
-姓 3025
-氏 3026
-垂 3027
-迫 3028
-絮 3029
-藕 3030
-捎 3031
-蓓 3032
-沫 3033
-奎 3034
-贩 3035
-泵 3036
-疑 3037
-岐 3038
-拓 3039
-詹 3040
-韦 3041
-粥 3042
-瞎 3043
-层 3044
-寿 3045
-淑 3046
-琦 3047
-履 3048
-痣 3049
-蔓 3050
-延 3051
-措 3052
-氰 3053
-胺 3054
-炭 3055
-鸥 3056
-谊 3057
-宛 3058
-悦 3059
-谣 3060
-茎 3061
-堆 3062
-鲤 3063
-坚 3064
-澎 3065
-溜 3066
-贫 3067
-擅 3068
-锌 3069
-竹 3070
-苟 3071
-磷 3072
-庐 3073
-嵌 3074
-潍 3075
-悚 3076
-岑 3077
-稀 3078
-奋 3079
-呦 3080
-梳 3081
-伐 3082
-芒 3083
-吶 3084
-凡 3085
-臂 3086
-驴 3087
-殿 3088
-雁 3089
-粹 3090
-凋 3091
-葵 3092
-烛 3093
-肾 3094
-尝 3095
-磨 3096
-晏 3097
-甄 3098
-嬛 3099
-盼 3100
-肇 3101
-咬 3102
-洼 3103
-匪 3104
-啰 3105
-硅 3106
-铅 3107
-矛 3108
-盾 3109
-贼 3110
-霜 3111
-螺 3112
-漏 3113
-帕 3114
-杉 3115
-矶 3116
-耗 3117
-责 3118
-靖 3119
-呸 3120
-驶 3121
-吞 3122
-睁 3123
-笼 3124
-茉 3125
-赈 3126
-纱 3127
-艘 3128
-炖 3129
-仿 3130
-瞬 3131
-嵊 3132
-澄 3133
-丞 3134
-摔 3135
-宠 3136
-爪 3137
-笋 3138
-庭 3139
-蜡 3140
-戈 3141
-锻 3142
-粗 3143
-糙 3144
-混 3145
-荚 3146
-曙 3147
-凄 3148
-抚 3149
-瀑 3150
-挖 3151
-掘 3152
-垮 3153
-奢 3154
-侈 3155
-揉 3156
-穹 3157
-钦 3158
-蛙 3159
-荧 3160
-悔 3161
-彦 3162
-忏 3163
-祸 3164
-攒 3165
-慌 3166
-簸 3167
-箕 3168
-繁 3169
-尖 3170
-芋 3171
-铠 3172
-沿 3173
-扮 3174
-隐 3175
-促 3176
-庾 3177
-葡 3178
-萄 3179
-硝 3180
-溶 3181
-淡 3182
-炅 3183
-昕 3184
-尧 3185
-妊 3186
-娠 3187
-曰 3188
-祛 3189
-枚 3190
-卒 3191
-陕 3192
-昭 3193
-龚 3194
-债 3195
-嗓 3196
-陷 3197
-阱 3198
-庞 3199
-盲 3200
-侵 3201
-匣 3202
-愤 3203
-怒 3204
-瞅 3205
-遭 3206
-脉 3207
-馒 3208
-愉 3209
-栗 3210
-鲍 3211
-挎 3212
-匆 3213
-缕 3214
-昵 3215
-鳄 3216
-阙 3217
-坟 3218
-捏 3219
-喽 3220
-雀 3221
-贯 3222
-苇 3223
-鹄 3224
-愈 3225
-裂 3226
-伪 3227
-劣 3228
-歹 3229
-溅 3230
-雌 3231
-猛 3232
-逞 3233
-饥 3234
-愚 3235
-牧 3236
-碰 3237
-帜 3238
-佝 3239
-偻 3240
-讪 3241
-馍 3242
-役 3243
-栈 3244
-唾 3245
-缆 3246
-袄 3247
-闸 3248
-织 3249
-筐 3250
-婉 3251
-昏 3252
-拖 3253
-毙 3254
-咙 3255
-褪 3256
-驼 3257
-壳 3258
-孽 3259
-审 3260
-脖 3261
-恳 3262
-孢 3263
-矫 3264
-臻 3265
-兖 3266
-俏 3267
-棍 3268
-唻 3269
-肘 3270
-俭 3271
-冕 3272
-葩 3273
-佑 3274
-鬓 3275
-柚 3276
-赴 3277
-崖 3278
-塌 3279
-厢 3280
-窈 3281
-窕 3282
-逑 3283
-卷 3284
-拂 3285
-蟑 3286
-螂 3287
-契 3288
-羞 3289
-函 3290
-逐 3291
-拌 3292
-肺 3293
-阻 3294
-纵 3295
-痰 3296
-狙 3297
-惋 3298
-枰 3299
-崽 3300
-胚 3301
-骡 3302
-萎 3303
-泄 3304
-呜 3305
-籁 3306
-濮 3307
-阆 3308
-琵 3309
-琶 3310
-跃 3311
-筝 3312
-勃 3313
-楂 3314
-奉 3315
-础 3316
-吆 3317
-壹 3318
-飙 3319
-虑 3320
-脆 3321
-黛 3322
-栓 3323
-逻 3324
-螃 3325
-轰 3326
-仑 3327
-券 3328
-逢 3329
-疮 3330
-私 3331
-窃 3332
-儋 3333
-泼 3334
-熬 3335
-焚 3336
-梨 3337
-吟 3338
-棱 3339
-稳 3340
-翘 3341
-祠 3342
-遮 3343
-瘤 3344
-稣 3345
-唇 3346
-阖 3347
-堡 3348
-禾 3349
-钗 3350
-爵 3351
-赐 3352
-绕 3353
-粘 3354
-癌 3355
-矜 3356
-虱 3357
-婧 3358
-坝 3359
-菏 3360
-隶 3361
-尺 3362
-滕 3363
-竿 3364
-恰 3365
-喱 3366
-冤 3367
-枉 3368
-叠 3369
-穴 3370
-搏 3371
-窦 3372
-栀 3373
-踪 3374
-昼 3375
-氯 3376
-陋 3377
-铭 3378
-禅 3379
-屑 3380
-巢 3381
-咻 3382
-喇 3383
-叭 3384
-棵 3385
-吊 3386
-诈 3387
-娇 3388
-绘 3389
-圩 3390
-仰 3391
-疙 3392
-瘩 3393
-桦 3394
-妾 3395
-丧 3396
-昊 3397
-湄 3398
-靴 3399
-迭 3400
-劝 3401
-溧 3402
-靡 3403
-梗 3404
-倪 3405
-刍 3406
-芽 3407
-篱 3408
-笆 3409
-漯 3410
-镖 3411
-协 3412
-叙 3413
-汾 3414
-豚 3415
-锷 3416
-瑙 3417
-瑜 3418
-伽 3419
-彰 3420
-扒 3421
-麝 3422
-赔 3423
-焉 3424
-亏 3425
-煅 3426
-翱 3427
-哽 3428
-煦 3429
-喷 3430
-舱 3431
-惨 3432
-哗 3433
-躲 3434
-佘 3435
-憾 3436
-旷 3437
-芹 3438
-簧 3439
-疹 3440
-簇 3441
-羹 3442
-刊 3443
-鹦 3444
-鹉 3445
-狡 3446
-猾 3447
-锯 3448
-呛 3449
-泛 3450
-汶 3451
-毗 3452
-衫 3453
-猕 3454
-祺 3455
-悸 3456
-昙 3457
-莽 3458
-杏 3459
-钮 3460
-叛 3461
-锄 3462
-砸 3463
-囤 3464
-犁 3465
-溃 3466
-疡 3467
-迦 3468
-轲 3469
-噜 3470
-犒 3471
-薰 3472
-薛 3473
-哺 3474
-竖 3475
-氟 3476
-渐 3477
-柒 3478
-贰 3479
-捌 3480
-傍 3481
-抹 3482
-褶 3483
-仇 3484
-偎 3485
-馅 3486
-旱 3487
-渭 3488
-昔 3489
-癣 3490
-挚 3491
-姻 3492
-炽 3493
-嘎 3494
-矢 3495
-汀 3496
-馋 3497
-淌 3498
-莓 3499
-貂 3500
-啧 3501
-茱 3502
-萸 3503
-涌 3504
-臀 3505
-恃 3506
-蒲 3507
-朴 3508
-嘟 3509
-扁 3510
-剂 3511
-歧 3512
-啪 3513
-啵 3514
-匠 3515
-帖 3516
-痒 3517
-睿 3518
-踢 3519
-衿 3520
-叽 3521
-崩 3522
-顽 3523
-嫌 3524
-扛 3525
-浔 3526
-拯 3527
-戊 3528
-戚 3529
-蛔 3530
-醇 3531
-笛 3532
-氛 3533
-沦 3534
-婊 3535
-仍 3536
-镍 3537
-渤 3538
-舶 3539
-哆 3540
-睹 3541
-萤 3542
-弧 3543
-辙 3544
-旯 3545
-纷 3546
-熄 3547
-挽 3548
-帘 3549
-蒸 3550
-橄 3551
-榄 3552
-滥 3553
-掩 3554
-兮 3555
-庸 3556
-玟 3557
-垦 3558
-惟 3559
-朕 3560
-脯 3561
-歪 3562
-吾 3563
-碘 3564
-锰 3565
-矾 3566
-拙 3567
-践 3568
-纠 3569
-赡 3570
-暨 3571
-凳 3572
-雾 3573
-缔 3574
-啫 3575
-毁 3576
-宥 3577
-邛 3578
-崃 3579
-禧 3580
-醛 3581
-滤 3582
-嘀 3583
-缪 3584
-萌 3585
-芥 3586
-胀 3587
-鲨 3588
-腩 3589
-勾 3590
-裳 3591
-雍 3592
-蹭 3593
-匹 3594
-髓 3595
-砍 3596
-孰 3597
-辩 3598
-唰 3599
-慷 3600
-慨 3601
-畏 3602
-坠 3603
-钝 3604
-箫 3605
-愧 3606
-劈 3607
-嘶 3608
-粮 3609
-轼 3610
-蟒 3611
-翊 3612
-澧 3613
-揽 3614
-烹 3615
-饪 3616
-踏 3617
-弛 3618
-婢 3619
-奸 3620
-掏 3621
-泓 3622
-袖 3623
-笈 3624
-刑 3625
-俑 3626
-浇 3627
-骊 3628
-蛀 3629
-蚤 3630
-杵 3631
-兹 3632
-晰 3633
-癫 3634
-痫 3635
-逝 3636
-炬 3637
-讼 3638
-陂 3639
-蚕 3640
-绸 3641
-槽 3642
-纨 3643
-牢 3644
-晃 3645
-窄 3646
-蒂 3647
-湃 3648
-硫 3649
-眨 3650
-耸 3651
-浠 3652
-梵 3653
-纺 3654
-贾 3655
-膨 3656
-阀 3657
-堀 3658
-扭 3659
-捂 3660
-扑 3661
-椭 3662
-鳟 3663
-丙 3664
-烯 3665
-冈 3666
-衷 3667
-牟 3668
-郫 3669
-畴 3670
-腥 3671
-亩 3672
-淤 3673
-禄 3674
-倘 3675
-烷 3676
-仆 3677
-刨 3678
-炜 3679
-挨 3680
-鳅 3681
-奚 3682
-峪 3683
-呻 3684
-佣 3685
-渔 3686
-肢 3687
-霏 3688
-旨 3689
-爰 3690
-吨 3691
-珑 3692
-隽 3693
-橙 3694
-箍 3695
-岚 3696
-啸 3697
-倌 3698
-剃 3699
-御 3700
-沸 3701
-棘 3702
-瘫 3703
-痪 3704
-仕 3705
-闺 3706
-炳 3707
-乏 3708
-拱 3709
-墅 3710
-铢 3711
-痤 3712
-琥 3713
-珈 3714
-荟 3715
-翩 3716
-搓 3717
-阮 3718
-芸 3719
-抠 3720
-弓 3721
-锣 3722
-赫 3723
-挡 3724
-侃 3725
-诶 3726
-沽 3727
-绫 3728
-濑 3729
-龈 3730
-乞 3731
-丐 3732
-宴 3733
-馁 3734
-牲 3735
-闰 3736
-亢 3737
-辫 3738
-铲 3739
-嫦 3740
-卵 3741
-佚 3742
-谬 3743
-倡 3744
-抑 3745
-赋 3746
-跆 3747
-削 3748
-氮 3749
-嫩 3750
-噻 3751
-蜗 3752
-鹂 3753
-靶 3754
-妥 3755
-衢 3756
-腻 3757
-砖 3758
-翎 3759
-拈 3760
-卉 3761
-皂 3762
-曦 3763
-荔 3764
-晤 3765
-曜 3766
-趵 3767
-纣 3768
-捞 3769
-蕲 3770
-猿 3771
-榈 3772
-憎 3773
-媚 3774
-绞 3775
-峙 3776
-饲 3777
-瑾 3778
-寡 3779
-釜 3780
-凸 3781
-凹 3782
-嫉 3783
-妒 3784
-婪 3785
-驸 3786
-荤 3787
-弥 3788
-蹦 3789
-驮 3790
-汞 3791
-唠 3792
-叨 3793
-袈 3794
-裟 3795
-毽 3796
-蔗 3797
-蹄 3798
-犍 3799
-珞 3800
-谚 3801
-煎 3802
-腋 3803
-瞳 3804
-丛 3805
-挪 3806
-榴 3807
-钩 3808
-梓 3809
-骁 3810
-烙 3811
-舜 3812
-暮 3813
-擀 3814
-兜 3815
-癜 3816
-姗 3817
-藜 3818
-擒 3819
-歼 3820
-冉 3821
-倚 3822
-漱 3823
-嫣 3824
-椰 3825
-隘 3826
-掐 3827
-栾 3828
-巍 3829
-咔 3830
-稽 3831
-惆 3832
-怅 3833
-镑 3834
-娲 3835
-芷 3836
-藻 3837
-伺 3838
-忌 3839
-桔 3840
-绅 3841
-坂 3842
-澜 3843
-嚓 3844
-苔 3845
-诣 3846
-倔 3847
-酿 3848
-槟 3849
-榔 3850
-粪 3851
-渺 3852
-馗 3853
-峨 3854
-碚 3855
-阎 3856
-巅 3857
-颊 3858
-戬 3859
-吒 3860
-鸵 3861
-岂 3862
-廖 3863
-娅 3864
-旬 3865
-猥 3866
-琐 3867
-扈 3868
-滔 3869
-枷 3870
-崴 3871
-捣 3872
-泻 3873
-甙 3874
-俯 3875
-撑 3876
-芮 3877
-舆 3878
-邂 3879
-逅 3880
-宪 3881
-晖 3882
-岔 3883
-哒 3884
-酵 3885
-痔 3886
-苓 3887
-捶 3888
-睫 3889
-裕 3890
-彤 3891
-潢 3892
-酉 3893
-聂 3894
-氨 3895
-嗷 3896
-皎 3897
-焖 3898
-袭 3899
-惦 3900
-惘 3901
-隙 3902
-彝 3903
-鞘 3904
-厄 3905
-殷 3906
-罕 3907
-嚏 3908
-拇 3909
-尹 3910
-蔻 3911
-颂 3912
-皖 3913
-霖 3914
-屉 3915
-崛 3916
-砣 3917
-穗 3918
-枸 3919
-杞 3920
-竣 3921
-勋 3922
-坍 3923
-溢 3924
-廓 3925
-煽 3926
-囚 3927
-涪 3928
-墩 3929
-琢 3930
-胳 3931
-膊 3932
-彼 3933
-陀 3934
-汹 3935
-柱 3936
-颁 3937
-闫 3938
-熠 3939
-叹 3940
-婿 3941
-娩 3942
-藓 3943
-岷 3944
-婺 3945
-桓 3946
-赁 3947
-罢 3948
-姊 3949
-瓢 3950
-桩 3951
-淫 3952
-堪 3953
-艰 3954
-枢 3955
-枞 3956
-晗 3957
-泷 3958
-逍 3959
-筱 3960
-烽 3961
-渍 3962
-蒿 3963
-殴 3964
-玖 3965
-罐 3966
-剿 3967
-喀 3968
-磕 3969
-铵 3970
-蕊 3971
-篓 3972
-痞 3973
-磅 3974
-礴 3975
-磐 3976
-拘 3977
-瘙 3978
-惕 3979
-孜 3980
-杖 3981
-撇 3982
-敖 3983
-踩 3984
-刹 3985
-蹿 3986
-坎 3987
-氦 3988
-汨 3989
-垣 3990
-垢 3991
-胁 3992
-趴 3993
-苷 3994
-镒 3995
-幢 3996
-鞠 3997
-逾 3998
-鬃 3999
-尉 4000
-韧 4001
-锤 4002
-嘘 4003
-呷 4004
-噎 4005
-煲 4006
-恍 4007
-粱 4008
-亳 4009
-鳞 4010
-懦 4011
-酚 4012
-酞 4013
-哨 4014
-祀 4015
-刃 4016
-蕴 4017
-晟 4018
-菀 4019
-甬 4020
-鼾 4021
-鳖 4022
-螳 4023
-稼 4024
-栽 4025
-蝗 4026
-颌 4027
-咀 4028
-掠 4029
-嘱 4030
-甚 4031
-菅 4032
-奂 4033
-讽 4034
-秸 4035
-釉 4036
-坞 4037
-雇 4038
-绢 4039
-捧 4040
-狈 4041
-桀 4042
-骜 4043
-摊 4044
-臆 4045
-竺 4046
-栅 4047
-贬 4048
-飒 4049
-浸 4050
-噩 4051
-晾 4052
-绐 4053
-殡 4054
-挠 4055
-於 4056
-茁 4057
-瞪 4058
-窠 4059
-汰 4060
-魁 4061
-忒 4062
-璋 4063
-怠 4064
-莺 4065
-冶 4066
-绰 4067
-邈 4068
-圻 4069
-湮 4070
-亨 4071
-躬 4072
-砂 4073
-鹭 4074
-浊 4075
-楹 4076
-珉 4077
-撵 4078
-筏 4079
-荨 4080
-鳝 4081
-沥 4082
-邳 4083
-殉 4084
-憨 4085
-啼 4086
-熏 4087
-蜃 4088
-毋 4089
-彗 4090
-噪 4091
-绛 4092
-祟 4093
-蝙 4094
-蝠 4095
-漆 4096
-酰 4097
-锑 4098
-栖 4099
-肆 4100
-邕 4101
-弋 4102
-绽 4103
-嚼 4104
-霹 4105
-雳 4106
-谍 4107
-恹 4108
-怏 4109
-倦 4110
-轶 4111
-曛 4112
-疚 4113
-棚 4114
-漕 4115
-浃 4116
-勘 4117
-暄 4118
-趁 4119
-斥 4120
-苞 4121
-膳 4122
-赎 4123
-崭 4124
-笙 4125
-摁 4126
-嗅 4127
-瞒 4128
-舵 4129
-铸 4130
-咫 4131
-涅 4132
-瘪 4133
-潼 4134
-粑 4135
-漾 4136
-噶 4137
-鸠 4138
-铉 4139
-豹 4140
-遛 4141
-襟 4142
-壤 4143
-甭 4144
-吮 4145
-耒 4146
-钊 4147
-泞 4148
-拦 4149
-昱 4150
-腑 4151
-惧 4152
-韬 4153
-焗 4154
-窘 4155
-喳 4156
-溏 4157
-鲛 4158
-慵 4159
-菁 4160
-攥 4161
-埔 4162
-呕 4163
-蓑 4164
-笠 4165
-孑 4166
-咕 4167
-觐 4168
-漓 4169
-碾 4170
-浜 4171
-嬉 4172
-迂 4173
-笃 4174
-勉 4175
-锥 4176
-篷 4177
-亥 4178
-龌 4179
-龊 4180
-煞 4181
-蓟 4182
-皓 4183
-惰 4184
-勺 4185
-缨 4186
-峥 4187
-苯 4188
-豁 4189
-颓 4190
-拽 4191
-啄 4192
-麒 4193
-雎 4194
-鲢 4195
-睬 4196
-渣 4197
-唔 4198
-桧 4199
-癞 4200
-蛤 4201
-蟆 4202
-撩 4203
-酯 4204
-戳 4205
-舔 4206
-孺 4207
-怂 4208
-恿 4209
-臃 4210
-戟 4211
-惭 4212
-耿 4213
-徵 4214
-柬 4215
-朽 4216
-磺 4217
-媲 4218
-懿 4219
-悼 4220
-绎 4221
-缅 4222
-茜 4223
-瞻 4224
-炀 4225
-脓 4226
-罄 4227
-秃 4228
-拎 4229
-譬 4230
-榉 4231
-拭 4232
-玥 4233
-崆 4234
-峒 4235
-胛 4236
-糗 4237
-佗 4238
-佬 4239
-袍 4240
-炊 4241
-仞 4242
-霎 4243
-掺 4244
-匀 4245
-姹 4246
-妯 4247
-娌 4248
-帷 4249
-岢 4250
-柄 4251
-阪 4252
-玺 4253
-窑 4254
-肽 4255
-涡 4256
-窟 4257
-阉 4258
-硼 4259
-蛳 4260
-呤 4261
-砚 4262
-偌 4263
-贿 4264
-芗 4265
-蹊 4266
-跷 4267
-雏 4268
-膝 4269
-嗜 4270
-扦 4271
-涟 4272
-殆 4273
-郡 4274
-洵 4275
-酋 4276
-匡 4277
-胤 4278
-撤 4279
-辗 4280
-冀 4281
-捺 4282
-吏 4283
-衩 4284
-腕 4285
-灸 4286
-绔 4287
-瓯 4288
-蜻 4289
-蜓 4290
-窜 4291
-躯 4292
-髦 4293
-诏 4294
-缄 4295
-筠 4296
-沌 4297
-酐 4298
-皋 4299
-隧 4300
-鹊 4301
-傀 4302
-儡 4303
-诲 4304
-嘏 4305
-寅 4306
-骇 4307
-喧 4308
-癀 4309
-瑚 4310
-碉 4311
-羔 4312
-掂 4313
-痹 4314
-孚 4315
-绡 4316
-馊 4317
-虏 4318
-悖 4319
-漪 4320
-琉 4321
-缉 4322
-冥 4323
-饯 4324
-蔺 4325
-瘆 4326
-榨 4327
-盯 4328
-鄞 4329
-妨 4330
-哐 4331
-寇 4332
-鹃 4333
-卞 4334
-喘 4335
-藩 4336
-踹 4337
-粟 4338
-陨 4339
-遣 4340
-鳌 4341
-烨 4342
-抉 4343
-臧 4344
-墉 4345
-疽 4346
-拷 4347
-赃 4348
-哮 4349
-馥 4350
-砰 4351
-拗 4352
-汐 4353
-矣 4354
-沅 4355
-裴 4356
-阐 4357
-蟋 4358
-蟀 4359
-蚀 4360
-恁 4361
-恙 4362
-蝉 4363
-荀 4364
-彧 4365
-銮 4366
-侮 4367
-驿 4368
-婶 4369
-檀 4370
-哩 4371
-镐 4372
-轴 4373
-扳 4374
-飓 4375
-麓 4376
-牺 4377
-垛 4378
-稞 4379
-桴 4380
-痧 4381
-揣 4382
-殇 4383
-邬 4384
-撅 4385
-邸 4386
-鼬 4387
-剥 4388
-胥 4389
-撼 4390
-溟 4391
-鄱 4392
-鲫 4393
-觅 4394
-犊 4395
-恕 4396
-铂 4397
-褔 4398
-淼 4399
-骝 4400
-藉 4401
-裔 4402
-痨 4403
-颤 4404
-尴 4405
-尬 4406
-癖 4407
-拄 4408
-蠕 4409
-虻 4410
-迄 4411
-攸 4412
-浚 4413
-盔 4414
-肮 4415
-侬 4416
-锏 4417
-憧 4418
-憬 4419
-镰 4420
-懈 4421
-挟 4422
-缤 4423
-涎 4424
-睾 4425
-惶 4426
-褚 4427
-藐 4428
-眺 4429
-艇 4430
-昀 4431
-妄 4432
-祗 4433
-壬 4434
-浯 4435
-衲 4436
-來 4437
-黯 4438
-芩 4439
-敞 4440
-绊 4441
-娣 4442
-掷 4443
-茯 4444
-琍 4445
-蛹 4446
-钧 4447
-瘘 4448
-蜥 4449
-蜴 4450
-唬 4451
-驭 4452
-阂 4453
-诃 4454
-疟 4455
-潦 4456
-谀 4457
-肱 4458
-黏 4459
-甥 4460
-眶 4461
-秩 4462
-庇 4463
-钏 4464
-咝 4465
-肴 4466
-宸 4467
-湟 4468
-沣 4469
-煊 4470
-盂 4471
-弈 4472
-瞩 4473
-聆 4474
-疥 4475
-腼 4476
-腆 4477
-胭 4478
-匕 4479
-讳 4480
-戮 4481
-茧 4482
-趾 4483
-亵 4484
-吖 4485
-漩 4486
-逵 4487
-寰 4488
-滇 4489
-渎 4490
-寮 4491
-嘁 4492
-珂 4493
-珀 4494
-稠 4495
-羌 4496
-徘 4497
-徊 4498
-苛 4499
-蕨 4500
-薏 4501
-苡 4502
-戌 4503
-卯 4504
-馈 4505
-溥 4506
-熹 4507
-屡 4508
-巳 4509
-璜 4510
-铮 4511
-踊 4512
-锚 4513
-濠 4514
-噫 4515
-怦 4516
-蓥 4517
-碌 4518
-霓 4519
-牦 4520
-妤 4521
-屹 4522
-缈 4523
-蹉 4524
-驷 4525
-菡 4526
-谔 4527
-琛 4528
-吡 4529
-喹 4530
-呲 4531
-溺 4532
-鳗 4533
-慑 4534
-秆 4535
-骋 4536
-脐 4537
-涤 4538
-荞 4539
-淅 4540
-罘 4541
-焱 4542
-孵 4543
-斟 4544
-酌 4545
-痊 4546
-秉 4547
-砌 4548
-瘁 4549
-胱 4550
-笫 4551
-燮 4552
-衅 4553
-腱 4554
-垒 4555
-锟 4556
-缀 4557
-疵 4558
-墟 4559
-盏 4560
-舂 4561
-侗 4562
-琨 4563
-唧 4564
-怆 4565
-沮 4566
-敛 4567
-瑕 4568
-奠 4569
-汴 4570
-衙 4571
-歆 4572
-嘹 4573
-饽 4574
-拧 4575
-濒 4576
-锭 4577
-嬴 4578
-吱 4579
-靳 4580
-眸 4581
-渲 4582
-睦 4583
-蝼 4584
-瞿 4585
-剁 4586
-紊 4587
-翟 4588
-攘 4589
-蹂 4590
-躏 4591
-淞 4592
-跎 4593
-侍 4594
-铛 4595
-绷 4596
-仟 4597
-瀚 4598
-赉 4599
-俪 4600
-魄 4601
-吼 4602
-酗 4603
-嚒 4604
-彷 4605
-徨 4606
-煜 4607
-曝 4608
-嗑 4609
-俅 4610
-嵘 4611
-隍 4612
-唆 4613
-郜 4614
-栩 4615
-尻 4616
-咗 4617
-茗 4618
-疱 4619
-斐 4620
-菘 4621
-芎 4622
-帼 4623
-枭 4624
-矩 4625
-仨 4626
-幄 4627
-鲸 4628
-猬 4629
-梢 4630
-槐 4631
-璧 4632
-坷 4633
-逯 4634
-踝 4635
-濡 4636
-樵 4637
-肓 4638
-劵 4639
-羚 4640
-髫 4641
-笄 4642
-俾 4643
-匿 4644
-帛 4645
-孀 4646
-焙 4647
-瘟 4648
-籽 4649
-萦 4650
-灼 4651
-箴 4652
-筵 4653
-窒 4654
-裆 4655
-旎 4656
-砝 4657
-妲 4658
-恺 4659
-覃 4660
-寐 4661
-酝 4662
-啃 4663
-塬 4664
-醴 4665
-蜿 4666
-蜒 4667
-愣 4668
-恤 4669
-撂 4670
-瘸 4671
-檐 4672
-琰 4673
-狒 4674
-摧 4675
-诠 4676
-孪 4677
-嘚 4678
-鼹 4679
-囡 4680
-茴 4681
-噤 4682
-僻 4683
-钕 4684
-锴 4685
-渗 4686
-嗫 4687
-撮 4688
-缭 4689
-粼 4690
-咄 4691
-挝 4692
-蛾 4693
-恪 4694
-皙 4695
-莒 4696
-叼 4697
-诽 4698
-妩 4699
-叱 4700
-咤 4701
-挞 4702
-萼 4703
-饵 4704
-澹 4705
-惺 4706
-呶 4707
-铤 4708
-佟 4709
-丕 4710
-靛 4711
-伶 4712
-涣 4713
-桢 4714
-狭 4715
-卅 4716
-蟠 4717
-蟾 4718
-朦 4719
-胧 4720
-咆 4721
-滦 4722
-岖 4723
-篙 4724
-痍 4725
-胰 4726
-谏 4727
-坳 4728
-樯 4729
-橹 4730
-孬 4731
-潴 4732
-厥 4733
-椐 4734
-谩 4735
-恬 4736
-琬 4737
-遁 4738
-褥 4739
-咎 4740
-羁 4741
-苣 4742
-殁 4743
-懵 4744
-褒 4745
-蜚 4746
-蛊 4747
-筛 4748
-耙 4749
-耨 4750
-嬷 4751
-驯 4752
-赅 4753
-畲 4754
-滢 4755
-伎 4756
-庹 4757
-踉 4758
-戎 4759
-膛 4760
-嗡 4761
-吔 4762
-唏 4763
-喏 4764
-哧 4765
-缇 4766
-蚝 4767
-璀 4768
-璨 4769
-捅 4770
-妁 4771
-曳 4772
-吩 4773
-咐 4774
-罂 4775
-垌 4776
-揪 4777
-壕 4778
-跺 4779
-辘 4780
-轳 4781
-噔 4782
-斓 4783
-厮 4784
-叁 4785
-仄 4786
-沼 4787
-鸢 4788
-醪 4789
-郢 4790
-圃 4791
-碜 4792
-鲅 4793
-嚯 4794
-淳 4795
-迩 4796
-诋 4797
-鬟 4798
-汲 4799
-艮 4800
-跤 4801
-麋 4802
-橇 4803
-悱 4804
-恻 4805
-啷 4806
-惮 4807
-樨 4808
-毓 4809
-裱 4810
-堇 4811
-埸 4812
-叵 4813
-腚 4814
-畀 4815
-钼 4816
-赦 4817
-悯 4818
-谴 4819
-稷 4820
-嘢 4821
-盎 4822
-跶 4823
-窥 4824
-瑄 4825
-谤 4826
-柘 4827
-垄 4828
-蠡 4829
-邝 4830
-娆 4831
-俐 4832
-铷 4833
-肋 4834
-涿 4835
-俎 4836
-捜 4837
-罡 4838
-嗝 4839
-唛 4840
-酣 4841
-鹬 4842
-瑀 4843
-帚 4844
-镭 4845
-搽 4846
-钣 4847
-蜇 4848
-嗞 4849
-颉 4850
-耘 4851
-忡 4852
-噼 4853
-睐 4854
-簋 4855
-镚 4856
-朐 4857
-戛 4858
-扪 4859
-鹩 4860
-稹 4861
-嗣 4862
-睇 4863
-弩 4864
-侥 4865
-绚 4866
-虔 4867
-溴 4868
-毂 4869
-漉 4870
-郧 4871
-杈 4872
-埭 4873
-哝 4874
-纾 4875
-箔 4876
-蚍 4877
-呋 4878
-喃 4879
-旌 4880
-袅 4881
-嫡 4882
-2 4883
-睢 4884
-榭 4885
-濉 4886
-雉 4887
-糍 4888
-谙 4889
-坻 4890
-遨 4891
-囔 4892
-鹜 4893
-垩 4894
-嵋 4895
-葑 4896
-叻 4897
-剌 4898
-铀 4899
-鲟 4900
-珏 4901
-唑 4902
-拴 4903
-乍 4904
-镊 4905
-歩 4906
-姘 4907
-戍 4908
-娈 4909
-槿 4910
-魇 4911
-叩 4912
-啾 4913
-腈 4914
-骞 4915
-殃 4916
-髋 4917
-嶙 4918
-璟 4919
-嚷 4920
-鹳 4921
-嗬 4922
-梆 4923
-晁 4924
-龛 4925
-嚎 4926
-熨 4927
-倭 4928
-峦 4929
-蜍 4930
-桉 4931
-齁 4932
-搀 4933
-铬 4934
-刽 4935
-謝 4936
-沒 4937
-簪 4938
-邺 4939
-嵬 4940
-馄 4941
-饨 4942
-蜢 4943
-嗒 4944
-芨 4945
-弶 4946
-晞 4947
-搔 4948
-昴 4949
-夙 4950
-徙 4951
-霾 4952
-嗖 4953
-碴 4954
-秧 4955
-芍 4956
-匝 4957
-泫 4958
-琯 4959
-扼 4960
-砒 4961
-栎 4962
-卟 4963
-琊 4964
-怯 4965
-侩 4966
-峯 4967
-忿 4968
-藁 4969
-蹼 4970
-毡 4971
-埤 4972
-膘 4973
-噗 4974
-阕 4975
-嘭 4976
-椿 4977
-涸 4978
-祯 4979
-芵 4980
-螨 4981
-寥 4982
-梶 4983
-嘈 4984
-泠 4985
-侏 4986
-棂 4987
-缶 4988
-捋 4989
-钜 4990
-璞 4991
-媞 4992
-唢 4993
-邰 4994
-蚱 4995
-薜 4996
-牒 4997
-缥 4998
-咿 4999
-遐 5000
-蕙 5001
-惬 5002
-惚 5003
-硚 5004
-麽 5005
-踌 5006
-褂 5007
-蜉 5008
-蝣 5009
-腌 5010
-熘 5011
-缮 5012
-锢 5013
-犽 5014
-蹬 5015
-皈 5016
-剔 5017
-芪 5018
-妪 5019
-钇 5020
-仃 5021
-荏 5022
-苒 5023
-塾 5024
-阡 5025
-瑨 5026
-冢 5027
-匈 5028
-庶 5029
-荃 5030
-茬 5031
-妗 5032
-暹 5033
-犷 5034
-嵴 5035
-鳃 5036
-羲 5037
-岱 5038
-烩 5039
-勐 5040
-霁 5041
-厝 5042
-飚 5043
-瀛 5044
-炕 5045
-桅 5046
-垓 5047
-晌 5048
-黒 5049
-蚩 5050
-夔 5051
-垚 5052
-烊 5053
-眀 5054
-荼 5055
-蘼 5056
-尅 5057
-舫 5058
-拣 5059
-蹋 5060
-劭 5061
-耆 5062
-陡 5063
-樽 5064
-谒 5065
-觞 5066
-箩 5067
-槛 5068
-傈 5069
-僳 5070
-爻 5071
-皑 5072
-滘 5073
-嬅 5074
-丶 5075
-邋 5076
-遢 5077
-讴 5078
-隅 5079
-邃 5080
-谑 5081
-哔 5082
-矬 5083
-姣 5084
-凛 5085
-冽 5086
-殒 5087
-眈 5088
-鹧 5089
-鸪 5090
-飕 5091
-亘 5092
-篝 5093
-嘅 5094
-乜 5095
-黜 5096
-颇 5097
-鄄 5098
-蔫 5099
-贻 5100
-猝 5101
-绌 5102
-芈 5103
-隼 5104
-戆 5105
-鹫 5106
-霑 5107
-宕 5108
-凇 5109
-铨 5110
-町 5111
-礁 5112
-蕃 5113
-淖 5114
-搐 5115
-饴 5116
-榛 5117
-晔 5118
-祢 5119
-酥 5120
-丨 5121
-赂 5122
-噘 5123
-黍 5124
-幌 5125
-骅 5126
-黝 5127
-帧 5128
-胯 5129
-埙 5130
-敕 5131
-涓 5132
-掣 5133
-圪 5134
-榻 5135
-濛 5136
-擞 5137
-篡 5138
-榷 5139
-亟 5140
-渌 5141
-锹 5142
-啐 5143
-捍 5144
-嘣 5145
-跻 5146
-桠 5147
-贮 5148
-蛰 5149
-猖 5150
-骸 5151
-溉 5152
-铎 5153
-吁 5154
-溯 5155
-踞 5156
-俨 5157
-茌 5158
-蒯 5159
-篆 5160
-膺 5161
-垭 5162
-匮 5163
-撰 5164
-擂 5165
-倜 5166
-傥 5167
-蔑 5168
-弼 5169
-珮 5170
-颢 5171
-钿 5172
-迸 5173
-凿 5174
-湫 5175
-焯 5176
-硒 5177
-畈 5178
-觑 5179
-揶 5180
-禀 5181
-宦 5182
-杷 5183
-讷 5184
-踮 5185
-掳 5186
-窿 5187
-捻 5188
-褴 5189
-褛 5190
-瑛 5191
-胫 5192
-喋 5193
-沓 5194
-汛 5195
-掴 5196
-魉 5197
-馀 5198
-隗 5199
-咘 5200
-呱 5201
-獭 5202
-畊 5203
-莜 5204
-祐 5205
-轧 5206
-魍 5207
-昶 5208
-诓 5209
-囗 5210
-莠 5211
-岌 5212
-潸 5213
-涞 5214
-綦 5215
-畸 5216
-阄 5217
-遏 5218
-啶 5219
-冇 5220
-懋 5221
-煨 5222
-羱 5223
-诟 5224
-枳 5225
-鲶 5226
-燊 5227
-猷 5228
-铄 5229
-缰 5230
-搪 5231
-赊 5232
-诩 5233
-佼 5234
-钵 5235
-谌 5236
-嬗 5237
-砥 5238
-砺 5239
-觊 5240
-觎 5241
-颅 5242
-怵 5243
-疸 5244
-锆 5245
-缢 5246
-棣 5247
-蛎 5248
-鄯 5249
-茸 5250
-谶 5251
-蹶 5252
-侑 5253
-滂 5254
-襁 5255
-褓 5256
-杳 5257
-臊 5258
-摒 5259
-袂 5260
-掸 5261
-鹞 5262
-忱 5263
-湉 5264
-汩 5265
-剽 5266
-槌 5267
-塍 5268
-喟 5269
-讹 5270
-抡 5271
-烃 5272
-咁 5273
-珺 5274
-槎 5275
-砼 5276
-泯 5277
-泮 5278
-遴 5279
-匾 5280
-沏 5281
-悌 5282
-麾 5283
-垡 5284
-鏖 5285
-垅 5286
-斛 5287
-镂 5288
-骷 5289
-髅 5290
-豺 5291
-诿 5292
-狰 5293
-狞 5294
-泱 5295
-榫 5296
-嗤 5297
-瞥 5298
-揄 5299
-哌 5300
-婀 5301
-恸 5302
-蛐 5303
-镀 5304
-霈 5305
-钒 5306
-踱 5307
-淆 5308
-薹 5309
-纭 5310
-瘠 5311
-戾 5312
-夭 5313
-铰 5314
-渚 5315
-犇 5316
-舀 5317
-傣 5318
-獗 5319
-瞭 5320
-兢 5321
-犟 5322
-袒 5323
-铖 5324
-颚 5325
-徜 5326
-徉 5327
-囍 5328
-酆 5329
-铡 5330
-睽 5331
-裨 5332
-饕 5333
-躇 5334
-噱 5335
-赓 5336
-懊 5337
-蟊 5338
-趸 5339
-鄢 5340
-埝 5341
-椟 5342
-粳 5343
-跛 5344
-莴 5345
-娉 5346
-嗄 5347
-邙 5348
-渑 5349
-佶 5350
-颍 5351
-溆 5352
-诧 5353
-抨 5354
-憷 5355
-涠 5356
-痼 5357
-砀 5358
-剐 5359
-缙 5360
-鞑 5361
-坭 5362
-烬 5363
-唁 5364
-臼 5365
-瓮 5366
-袱 5367
-珩 5368
-蝌 5369
-蚪 5370
-诬 5371
-迥 5372
-楸 5373
-皿 5374
-蜷 5375
-遑 5376
-啖 5377
-篪 5378
-崮 5379
-讧 5380
-盹 5381
-瞑 5382
-鲳 5383
-谟 5384
-拮 5385
-琏 5386
-瞰 5387
-憩 5388
-馏 5389
-炷 5390
-眩 5391
-羿 5392
-洙 5393
-珲 5394
-愫 5395
-佯 5396
-舸 5397
-祎 5398
-旮 5399
-翌 5400
-畿 5401
-桎 5402
-梏 5403
-钳 5404
-鳍 5405
-犸 5406
-祉 5407
-缜 5408
-硌 5409
-殓 5410
-砾 5411
-酩 5412
-酊 5413
-兀 5414
-矸 5415
-髙 5416
-疝 5417
-膑 5418
-哂 5419
-僚 5420
-耷 5421
-窨 5422
-孳 5423
-鲠 5424
-淝 5425
-搡 5426
-伢 5427
-鲷 5428
-谕 5429
-頫 5430
-泺 5431
-谧 5432
-煳 5433
-萁 5434
-馕 5435
-鹌 5436
-鹑 5437
-钴 5438
-埇 5439
-摈 5440
-踵 5441
-冗 5442
-铣 5443
-萃 5444
-忤 5445
-揩 5446
-铧 5447
-矗 5448
-闾 5449
-柞 5450
-貉 5451
-撺 5452
-掇 5453
-灞 5454
-醍 5455
-痱 5456
-粲 5457
-糠 5458
-讣 5459
-蹴 5460
-茆 5461
-螈 5462
-旻 5463
-蔼 5464
-咣 5465
-麸 5466
-涝 5467
-渥 5468
-垤 5469
-咭 5470
-玳 5471
-瑁 5472
-郏 5473
-纂 5474
-扞 5475
-峭 5476
-铩 5477
-锨 5478
-坩 5479
-埚 5480
-瑭 5481
-札 5482
-舛 5483
-臬 5484
-郯 5485
-晦 5486
-耄 5487
-耋 5488
-俚 5489
-鲭 5490
-柩 5491
-黟 5492
-骼 5493
-蛆 5494
-跋 5495
-俸 5496
-幡 5497
-愕 5498
-噙 5499
-峋 5500
-厩 5501
-夯 5502
-擢 5503
-枋 5504
-葳 5505
-偃 5506
-赝 5507
-昝 5508
-镉 5509
-嫔 5510
-潋 5511
-娓 5512
-郅 5513
-瘀 5514
-奄 5515
-荇 5516
-咂 5517
-痉 5518
-挛 5519
-祚 5520
-庖 5521
-纰 5522
-簌 5523
-淬 5524
-掮 5525
-俟 5526
-臾 5527
-雒 5528
-吋 5529
-颧 5530
-嗔 5531
-诘 5532
-焘 5533
-獾 5534
-氤 5535
-氲 5536
-鲲 5537
-麂 5538
-罹 5539
-澍 5540
-镳 5541
-囱 5542
-玷 5543
-嗳 5544
-擘 5545
-濂 5546
-逡 5547
-骛 5548
-镔 5549
-湍 5550
-讥 5551
-蹁 5552
-跹 5553
-淦 5554
-骰 5555
-疃 5556
-腓 5557
-嵇 5558
-怄 5559
-谯 5560
-啕 5561
-坯 5562
-钎 5563
-锒 5564
-伉 5565
-佻 5566
-腴 5567
-怼 5568
-浐 5569
-摹 5570
-僮 5571
-芾 5572
-矍 5573
-泔 5574
-蚬 5575
-屐 5576
-翕 5577
-唿 5578
-苋 5579
-氪 5580
-楔 5581
-莪 5582
-掬 5583
-舷 5584
-骐 5585
-嗲 5586
-荻 5587
-缱 5588
-绻 5589
-嫚 5590
-铟 5591
-饷 5592
-醐 5593
-伫 5594
-澶 5595
-郇 5596
-蹚 5597
-藿 5598
-鳕 5599
-蝈 5600
-钯 5601
-铍 5602
-骠 5603
-盅 5604
-蜊 5605
-腭 5606
-谘 5607
-孛 5608
-豇 5609
-囫 5610
-囵 5611
-抿 5612
-楣 5613
-廾 5614
-貔 5615
-貅 5616
-蛉 5617
-猹 5618
-蚴 5619
-轱 5620
-葚 5621
-胗 5622
-鸮 5623
-篦 5624
-谆 5625
-篑 5626
-莅 5627
-砷 5628
-蝾 5629
-疴 5630
-葺 5631
-瘴 5632
-滹 5633
-砭 5634
-噌 5635
-鸾 5636
-珙 5637
-碣 5638
-餮 5639
-荸 5640
-荠 5641
-犄 5642
-歙 5643
-樾 5644
-淙 5645
-痢 5646
-濯 5647
-轫 5648
-琮 5649
-啜 5650
-闳 5651
-椁 5652
-蓼 5653
-垴 5654
-唷 5655
-炔 5656
-峁 5657
-囹 5658
-尕 5659
-嗪 5660
-缎 5661
-拚 5662
-稔 5663
-牍 5664
-赳 5665
-忪 5666
-菖 5667
-佃 5668
-埂 5669
-宓 5670
-瞠 5671
-洹 5672
-锲 5673
-睑 5674
-攫 5675
-竽 5676
-蹩 5677
-慜 5678
-锉 5679
-羧 5680
-崧 5681
-醺 5682
-舐 5683
-讫 5684
-熵 5685
-▁GONNA 5686
-瘢 5687
-秭 5688
-跄 5689
-绀 5690
-懑 5691
-弭 5692
-萋 5693
-篁 5694
-缛 5695
-茭 5696
-吠 5697
-鲑 5698
-幔 5699
-潺 5700
-鹈 5701
-鹕 5702
-椴 5703
-哕 5704
-剜 5705
-湎 5706
-玑 5707
-槃 5708
-暌 5709
-蹒 5710
-跚 5711
-恣 5712
-磬 5713
-悭 5714
-劾 5715
-唳 5716
-绉 5717
-枇 5718
-蜱 5719
-瞟 5720
-膈 5721
-磴 5722
-嶂 5723
-苫 5724
-邡 5725
-骈 5726
-惴 5727
-硖 5728
-鳜 5729
-羸 5730
-秣 5731
-殚 5732
-桷 5733
-罔 5734
-颦 5735
-桁 5736
-鸩 5737
-孱 5738
-伥 5739
-愎 5740
-圄 5741
-贲 5742
-旖 5743
-荥 5744
-徇 5745
-镌 5746
-偈 5747
-敝 5748
-刎 5749
-跬 5750
-欸 5751
-髌 5752
-椤 5753
-觥 5754
-踟 5755
-斡 5756
-陉 5757
-谡 5758
-龅 5759
-鸨 5760
-豢 5761
-豉 5762
-悻 5763
-曈 5764
-茼 5765
-谗 5766
-忖 5767
-牯 5768
-痂 5769
-虢 5770
-馓 5771
-跖 5772
-聿 5773
-箅 5774
-塅 5775
-丼 5776
-獐 5777
-肏 5778
-逄 5779
-钡 5780
-叒 5781
-霭 5782
-鲮 5783
-凫 5784
-鹥 5785
-鳙 5786
-玦 5787
-蒡 5788
-嘬 5789
-鹗 5790
-鬄 5791
-鎏 5792
-嘤 5793
-绦 5794
-涔 5795
-齑 5796
-蒌 5797
-墘 5798
-俠 5799
-蛭 5800
-薅 5801
-叕 5802
-砧 5803
-嘧 5804
-媺 5805
-蚵 5806
-楽 5807
-浄 5808
-厍 5809
-鳊 5810
-泂 5811
-龋 5812
-瓒 5813
-瑧 5814
-邨 5815
-峣 5816
-蚺 5817
-鲉 5818
-滟 5819
-堑 5820
-豳 5821
-骧 5822
-艹 5823
-柾 5824
-鬣 5825
-眦 5826
-畦 5827
-虬 5828
-睨 5829
-飨 5830
-蘖 5831
-羟 5832
-瓤 5833
-岫 5834
-惇 5835
-鲵 5836
-痦 5837
-笤 5838
-憙 5839
-痩 5840
-煋 5841
-媤 5842
-佤 5843
-羮 5844
-鏊 5845
-昇 5846
-蛱 5847
-珅 5848
-庋 5849
-搵 5850
-旸 5851
-岿 5852
-亓 5853
-揸 5854
-谂 5855
-淠 5856
-糅 5857
-儆 5858
-苕 5859
-刿 5860
-呒 5861
-岙 5862
-荜 5863
-玧 5864
-鄠 5865
-讬 5866
-祕 5867
-箦 5868
-醚 5869
-膻 5870
-笕 5871
-蛏 5872
-哞 5873
-饸 5874
-饹 5875
-愻 5876
-汫 5877
-鹇 5878
-栉 5879
-沇 5880
-擤 5881
-徳 5882
-黢 5883
-狍 5884
-錫 5885
-暝 5886
-機 5887
-鉅 5888
-菓 5889
-廋 5890
-橛 5891
-羣 5892
-笊 5893
-魃 5894
-掼 5895
-魑 5896
-靥 5897
-酔 5898
-铱 5899
-峄 5900
-哋 5901
-畹 5902
-鍪 5903
-髀 5904
-嚄 5905
-秾 5906
-苾 5907
-孓 5908
-汆 5909
-嗟 5910
-锺 5911
-睥 5912
-炝 5913
-怔 5914
-咛 5915
-巉 5916
-墒 5917
-岘 5918
-禛 5919
-陟 5920
-皲 5921
-萘 5922
-妣 5923
-芃 5924
-煸 5925
-郦 5926
-蒗 5927
-仝 5928
-抻 5929
-苜 5930
-蓿 5931
-鎵 5932
-減 5933
-燧 5934
-娭 5935
-毑 5936
-诂 5937
-烔 5938
-猗 5939
-哏 5940
-氙 5941
-匯 5942
-颛 5943
-鞣 5944
-笺 5945
-枖 5946
-忾 5947
-黉 5948
-埯 5949
-敩 5950
-玏 5951
-钺 5952
-纥 5953
-佈 5954
-沖 5955
-蚜 5956
-莨 5957
-菟 5958
-麇 5959
-沤 5960
-耦 5961
-赭 5962
-祊 5963
-璄 5964
-旼 5965
-浞 5966
-痄 5967
-蔸 5968
-璎 5969
-屛 5970
-錤 5971
-弢 5972
-绨 5973
-員 5974
-誕 5975
-祜 5976
-勍 5977
-浉 5978
-娑 5979
-呓 5980
-啉 5981
-嗐 5982
-弁 5983
-绺 5984
-撷 5985
-崑 5986
-诌 5987
-標 5988
-甯 5989
-俣 5990
-趔 5991
-趄 5992
-垠 5993
-赟 5994
-馐 5995
-畑 5996
-給 5997
-幣 5998
-產 5999
-恵 6000
-併 6001
-蒹 6002
-葭 6003
-後 6004
-瀍 6005
-愠 6006
-莛 6007
-蝰 6008
-鹮 6009
-逶 6010
-侪 6011
-蒽 6012
-巽 6013
-瓴 6014
-鲱 6015
-薙 6016
-過 6017
-億 6018
-車 6019
-鲇 6020
-淨 6021
-嗎 6022
-诨 6023
-靚 6024
-內 6025
-糁 6026
-錾 6027
-刈 6028
-滯 6029
-炆 6030
-徂 6031
-傩 6032
-鲺 6033
-叟 6034
-埗 6035
-篠 6036
-焐 6037
-暻 6038
-盃 6039
-髻 6040
-樘 6041
-墈 6042
-菉 6043
-巯 6044
-嘌 6045
-遒 6046
-鼋 6047
-匍 6048
-匐 6049
-臜 6050
-馔 6051
-鲎 6052
-獠 6053
-蟇 6054
-栄 6055
-騎 6056
-賽 6057
-場 6058
-幾 6059
-鐘 6060
-镛 6061
-鸶 6062
-镆 6063
-窸 6064
-庠 6065
-蒺 6066
-溱 6067
-倮 6068
-楪 6069
-帀 6070
-躶 6071
-洰 6072
-圉 6073
-圊 6074
-捨 6075
-谝 6076
-呔 6077
-勖 6078
-揖 6079
-喈 6080
-霰 6081
-觋 6082
-嫪 6083
-毐 6084
-繇 6085
-珐 6086
-馃 6087
-孃 6088
-逖 6089
-骶 6090
-喬 6091
-奧 6092
-風 6093
-裵 6094
-胍 6095
-確 6096
-揠 6097
-榀 6098
-聒 6099
-谪 6100
-歘 6101
-粿 6102
-舾 6103
-聩 6104
-嫘 6105
-砟 6106
-侉 6107
-捯 6108
-饬 6109
-囏 6110
-喙 6111
-笥 6112
-燿 6113
-鮀 6114
-芡 6115
-蛄 6116
-铳 6117
-挲 6118
-笞 6119
-廿 6120
-蠹 6121
-湋 6122
-暎 6123
-霙 6124
-颔 6125
-苁 6126
-啮 6127
-囖 6128
-寤 6129
-炟 6130
-乩 6131
-熥 6132
-桡 6133
-阈 6134
-孖 6135
-鐢 6136
-衾 6137
-怍 6138
-沆 6139
-囿 6140
-胬 6141
-陲 6142
-缦 6143
-誇 6144
-醮 6145
-箬 6146
-盥 6147
-鹘 6148
-诳 6149
-氡 6150
-狎 6151
-枧 6152
-谄 6153
-芣 6154
-苢 6155
-俤 6156
-誊 6157
-殄 6158
-辋 6159
-係 6160
-迤 6161
-謦 6162
-簰 6163
-滓 6164
-嬢 6165
-倏 6166
-睺 6167
-滏 6168
-脘 6169
-嗙 6170
-谥 6171
-歃 6172
-锃 6173
-欻 6174
-挼 6175
-襙 6176
-檄 6177
-龇 6178
-楫 6179
-咵 6180
-徭 6181
-闱 6182
-嚅 6183
-鳑 6184
-鲏 6185
-佞 6186
-箜 6187
-篌 6188
-蹑 6189
-喑 6190
-胄 6191
-鞥 6192
-蟥 6193
-骢 6194
-蹙 6195
-柰 6196
-蕤 6197
-癸 6198
-哙 6199
-睚 6200
-绾 6201
-篾 6202
-鳏 6203
-谲 6204
-袤 6205
-翳 6206
-蹰 6207
-槊 6208
-黠 6209
-姒 6210
-锱 6211
-猢 6212
-狲 6213
-粝 6214
-戕 6215
-茕 6216
-瀣 6217
-踽 6218
-绶 6219
-媾 6220
-舢 6221
-螯 6222
-茏 6223
-廪 6224
-诰 6225
-辇 6226
-琚 6227
-汜 6228
-洇 6229
-還 6230
-遽 6231
-槁 6232
-靼 6233
-髡 6234
-鸬 6235
-鹚 6236
-捭 6237
-黩 6238
-俶 6239
-個 6240
-圜 6241
-颞 6242
-苻 6243
-恽 6244
-腧 6245
-甾 6246
-辎 6247
-顼 6248
-阗 6249
-鬻 6250
-鬶 6251
-沔 6252
-狃 6253
-#0 6254
-#1 6255
-#2 6256
diff --git a/app/src/main/assets/models/voice/manifest.json b/app/src/main/assets/models/voice/manifest.json
index 0f17a4bb..c2aa644e 100644
--- a/app/src/main/assets/models/voice/manifest.json
+++ b/app/src/main/assets/models/voice/manifest.json
@@ -1,16 +1,15 @@
{
- "modelId": "sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20-int8",
- "modelVersion": "2023-02-20-int8",
+ "modelId": "sherpa-onnx-streaming-paraformer-bilingual-zh-en-int8",
+ "modelVersion": "hf-8e40c43-int8",
"language": "zh-CN,en-US",
- "modelType": "zipformer",
+ "modelType": "paraformer",
"engineVersion": "sherpa-onnx-v1.13.6",
- "fileHash": "f2b835dfe8231bccc08692b4d8751ff50c2360b9b39a7501136a45e1bd97db85",
+ "fileHash": "b2a084021f5b4aa5293e750e0733b6a8819d37ddd6452518faffa0b3408c5263",
"supportsPunctuation": false,
- "requiredMemory": 420000000,
+ "requiredMemory": 520000000,
"files": [
- "models/voice/bilingual-zipformer/decoder-epoch-99-avg-1.onnx",
- "models/voice/bilingual-zipformer/encoder-epoch-99-avg-1.int8.onnx",
- "models/voice/bilingual-zipformer/joiner-epoch-99-avg-1.int8.onnx",
- "models/voice/bilingual-zipformer/tokens.txt"
+ "models/voice/bilingual-paraformer/decoder.int8.onnx",
+ "models/voice/bilingual-paraformer/encoder.int8.onnx",
+ "models/voice/bilingual-paraformer/tokens.txt"
]
}
diff --git a/app/src/main/assets/models/voice/trusted-downloads.tsv b/app/src/main/assets/models/voice/trusted-downloads.tsv
index fe60b815..55e8d2ca 100644
--- a/app/src/main/assets/models/voice/trusted-downloads.tsv
+++ b/app/src/main/assets/models/voice/trusted-downloads.tsv
@@ -1,3 +1,3 @@
# openIME trusted downloaded voice model catalog v1
# publishermanifest_fingerprint_sha256file_sizes_in_manifest_order
-Slacker-LLC 12bf0e2a9d5090bdb935c90941a0b3ef6269346860398fed3e45cef91940a525 13876452,181895032,3228404,56317
+Slacker-LLC 46e714643e31aa2cba5524cc69bcda2a3c4c2d7e6f826d025c2d9d81473d4366 71664561,165462184,75756
diff --git a/app/src/main/cpp/local_rime_jni.cc b/app/src/main/cpp/local_rime_jni.cc
index 88f1be7f..e4c6f817 100644
--- a/app/src/main/cpp/local_rime_jni.cc
+++ b/app/src/main/cpp/local_rime_jni.cc
@@ -2,11 +2,16 @@
#include
#include
+#include
#include
#include
#include
#include
+#include
+#include
+#include
+#include
namespace {
@@ -163,6 +168,27 @@ jobjectArray make_strings(JNIEnv* env, const std::vector& values) {
return result;
}
+bool candidate_is_user_learned(size_t index) {
+ if (!g_session) return false;
+ auto session = rime::Service::instance().GetSession(g_session);
+ if (!session) return false;
+ auto* context = session->context();
+ if (!context || context->composition().empty()) return false;
+ auto candidate = context->composition().back().GetCandidateAt(index);
+ if (!candidate) return false;
+
+ const auto genuine_candidates =
+ rime::Candidate::GetGenuineCandidates(candidate);
+ for (const auto& genuine : genuine_candidates) {
+ if (!genuine) continue;
+ const auto& type = genuine->type();
+ if (type == "user_phrase" || type == "user_table") {
+ return true;
+ }
+ }
+ return false;
+}
+
std::string take_commit() {
if (!g_api || !g_session) return {};
RIME_STRUCT(RimeCommit, commit);
@@ -233,6 +259,42 @@ bool select_schema_locked(const std::string& schema_id) {
g_api->select_schema(g_session, schema_id.c_str());
}
+bool safe_user_dict_name(const std::string& name) {
+ return !name.empty() && name.size() <= 128 && name != "." && name != ".." &&
+ name.find('/') == std::string::npos &&
+ name.find('\\') == std::string::npos;
+}
+
+std::string suspend_session_locked() {
+ std::string schema;
+ if (g_api && g_session && g_api->get_current_schema) {
+ char current[128] = {0};
+ if (g_api->get_current_schema(g_session, current, sizeof(current))) {
+ schema = current;
+ }
+ }
+ if (g_api && g_session) {
+ g_api->destroy_session(g_session);
+ }
+ g_session = 0;
+ return schema;
+}
+
+bool resume_session_locked(const std::string& schema) {
+ if (!g_api) return false;
+ g_session = g_api->create_session();
+ if (!g_session) return false;
+ const bool selected =
+ (!schema.empty() && select_schema_locked(schema)) ||
+ select_schema_locked("luna_pinyin_simp") ||
+ select_schema_locked("luna_pinyin");
+ if (!selected) {
+ g_api->destroy_session(g_session);
+ g_session = 0;
+ }
+ return selected;
+}
+
void shutdown_locked() {
if (g_api && g_session) g_api->destroy_session(g_session);
g_session = 0;
@@ -330,6 +392,87 @@ Java_llc_slacker_openime_RimeNative_nativeSelectCandidate(
return utf8_to_jstring(env, finish_selection());
}
+extern "C" JNIEXPORT jboolean JNICALL
+Java_llc_slacker_openime_RimeNative_nativeIsUserLearnedCandidate(
+ JNIEnv*, jclass, jint index) {
+ std::lock_guard lock(g_mutex);
+ if (!g_api || !g_session || index < 0) return JNI_FALSE;
+ return candidate_is_user_learned(static_cast(index))
+ ? JNI_TRUE
+ : JNI_FALSE;
+}
+
+extern "C" JNIEXPORT jboolean JNICALL
+Java_llc_slacker_openime_RimeNative_nativeDeleteCandidate(
+ JNIEnv*, jclass, jint index) {
+ std::lock_guard lock(g_mutex);
+ if (!g_api || !g_session || index < 0 || !g_api->delete_candidate) {
+ return JNI_FALSE;
+ }
+ return g_api->delete_candidate(
+ g_session,
+ static_cast(index))
+ ? JNI_TRUE
+ : JNI_FALSE;
+}
+
+extern "C" JNIEXPORT jobjectArray JNICALL
+Java_llc_slacker_openime_RimeNative_nativeExportUserDictionaries(
+ JNIEnv* env, jclass, jstring target_dir) {
+ std::lock_guard lock(g_mutex);
+ if (!g_api || !g_session) return nullptr;
+
+ const std::string target = jstring_to_utf8(env, target_dir);
+ if (target.empty()) return nullptr;
+ std::error_code error;
+ std::filesystem::create_directories(target, error);
+ if (error) return nullptr;
+
+ const std::string schema = suspend_session_locked();
+ std::vector exported;
+ {
+ rime::UserDictManager manager(&rime::Service::instance().deployer());
+ rime::UserDictList dictionaries;
+ manager.GetUserDictList(&dictionaries);
+ size_t index = 0;
+ bool failed = false;
+ for (const auto& name : dictionaries) {
+ const std::filesystem::path path =
+ std::filesystem::path(target) /
+ ("dict-" + std::to_string(index++) + ".userdb.txt");
+ const int count = manager.Export(name, path);
+ if (count < 0) {
+ failed = true;
+ break;
+ }
+ exported.emplace_back(name + "\t" + path.string());
+ }
+ const bool resumed = resume_session_locked(schema);
+ if (failed || !resumed) return nullptr;
+ }
+ return make_strings(env, exported);
+}
+
+extern "C" JNIEXPORT jint JNICALL
+Java_llc_slacker_openime_RimeNative_nativeImportUserDictionary(
+ JNIEnv* env, jclass, jstring dict_name, jstring source_file) {
+ std::lock_guard lock(g_mutex);
+ if (!g_api || !g_session) return -1;
+
+ const std::string name = jstring_to_utf8(env, dict_name);
+ const std::string source = jstring_to_utf8(env, source_file);
+ if (!safe_user_dict_name(name) || source.empty()) return -1;
+
+ const std::string schema = suspend_session_locked();
+ int imported = -1;
+ {
+ rime::UserDictManager manager(&rime::Service::instance().deployer());
+ imported = manager.Import(name, std::filesystem::path(source));
+ }
+ resume_session_locked(schema);
+ return imported;
+}
+
extern "C" JNIEXPORT jstring JNICALL
Java_llc_slacker_openime_RimeNative_nativeCommitFirst(
JNIEnv* env, jclass) {
diff --git a/app/src/main/java/llc/slacker/openime/AboutDataActivity.kt b/app/src/main/java/llc/slacker/openime/AboutDataActivity.kt
new file mode 100644
index 00000000..9f6bd606
--- /dev/null
+++ b/app/src/main/java/llc/slacker/openime/AboutDataActivity.kt
@@ -0,0 +1,378 @@
+package llc.slacker.openime
+
+import android.app.Activity
+import android.app.AlertDialog
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.WindowInsets
+import android.widget.LinearLayout
+import android.widget.ScrollView
+import android.widget.TextView
+import android.widget.Toast
+import java.io.File
+
+class AboutDataActivity : Activity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val content = LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ setPadding(
+ dp(ImeSpacingTokens.XL_DP),
+ dp(ImeSpacingTokens.LG_DP),
+ dp(ImeSpacingTokens.XL_DP),
+ dp(ImeSpacingTokens.XXL_DP),
+ )
+ addView(
+ SetupUi.activityTopBar(
+ context = this@AboutDataActivity,
+ title = "关于与数据",
+ onBack = ::finish,
+ ),
+ LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ dp(ImeGeometryTokens.TOP_BAR_HEIGHT_DP),
+ ),
+ )
+ addView(
+ infoCard(
+ title = "openIME " + versionName(),
+ body = "本应用不含联网权限,数据只存在本机。",
+ ),
+ wrap().apply { topMargin = dp(ImeSpacingTokens.MD_DP) },
+ )
+ addView(
+ infoCard(
+ title = "用户数据",
+ body = "JSON 导出包含常用语、自定义符号、备用用户词条和设置项;剪贴板历史不导出。Rime 自动学习词库在已加载时一并导出,并在导入时按词库合并。",
+ ),
+ wrap().apply { topMargin = dp(ImeSpacingTokens.MD_DP) },
+ )
+
+ val actions = LinearLayout(this@AboutDataActivity).apply {
+ orientation = LinearLayout.HORIZONTAL
+ addView(
+ SetupUi.primaryButton(this@AboutDataActivity, "导出") {
+ requestExportDocument()
+ },
+ LinearLayout.LayoutParams(
+ 0,
+ dp(ImeGeometryTokens.PRIMARY_ROW_HEIGHT_DP),
+ 1f,
+ ).apply { marginEnd = dp(ImeSpacingTokens.SM_DP) },
+ )
+ addView(
+ SetupUi.secondaryButton(this@AboutDataActivity, "导入") {
+ requestImportDocument()
+ },
+ LinearLayout.LayoutParams(
+ 0,
+ dp(ImeGeometryTokens.PRIMARY_ROW_HEIGHT_DP),
+ 1f,
+ ),
+ )
+ }
+ addView(
+ actions,
+ LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ dp(ImeGeometryTokens.PRIMARY_ROW_HEIGHT_DP),
+ ).apply { topMargin = dp(ImeSpacingTokens.LG_DP) },
+ )
+ addView(
+ infoCard(
+ title = "卸载前",
+ body = "卸载会清除本机全部数据,包括学习的用户词库。卸载前可先导出用户数据。",
+ ),
+ wrap().apply { topMargin = dp(ImeSpacingTokens.LG_DP) },
+ )
+ }
+
+ setContentView(
+ ScrollView(this).apply {
+ setBackgroundColor(getColor(R.color.setup_page_bg))
+ isFillViewport = true
+ setOnApplyWindowInsetsListener { view, insets ->
+ if (android.os.Build.VERSION.SDK_INT >= 30) {
+ val bars = insets.getInsets(
+ WindowInsets.Type.systemBars() or
+ WindowInsets.Type.displayCutout(),
+ )
+ view.setPadding(bars.left, bars.top, bars.right, bars.bottom)
+ } else {
+ @Suppress("DEPRECATION")
+ view.setPadding(
+ insets.systemWindowInsetLeft,
+ insets.systemWindowInsetTop,
+ insets.systemWindowInsetRight,
+ insets.systemWindowInsetBottom,
+ )
+ }
+ insets
+ }
+ addView(content)
+ },
+ )
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (resultCode != RESULT_OK) return
+ val uri = data?.data ?: return
+ when (requestCode) {
+ REQUEST_EXPORT -> exportTo(uri)
+ REQUEST_IMPORT -> importFrom(uri)
+ }
+ }
+
+ private fun requestExportDocument() {
+ startActivityForResult(
+ Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = "application/json"
+ putExtra(Intent.EXTRA_TITLE, "openIME-user-data.json")
+ },
+ REQUEST_EXPORT,
+ )
+ }
+
+ private fun requestImportDocument() {
+ startActivityForResult(
+ Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = "application/json"
+ },
+ REQUEST_IMPORT,
+ )
+ }
+
+ private fun exportTo(uri: Uri) {
+ val service = LocalVoiceImeService.activeInstance
+ val rimeDir = File(cacheDir, "rime-user-export").apply {
+ deleteRecursively()
+ mkdirs()
+ }
+ if (service == null) {
+ confirmExportWithoutRime(uri)
+ return
+ }
+ val queued = service.exportRimeUserData(rimeDir) { dictionaries ->
+ if (dictionaries == null) {
+ rimeDir.deleteRecursively()
+ runOnUiThread {
+ showError("Rime 自动学习词库导出失败,未生成不完整备份。")
+ }
+ return@exportRimeUserData
+ }
+ runCatching {
+ writeArchive(uri, UserDataRepository.snapshot(this, dictionaries))
+ }.onSuccess {
+ runOnUiThread {
+ Toast.makeText(this, "用户数据已导出", Toast.LENGTH_SHORT).show()
+ }
+ }.onFailure { error ->
+ runOnUiThread {
+ showError("导出失败:" + (error.message ?: "无法写入文件"))
+ }
+ }
+ rimeDir.deleteRecursively()
+ }
+ if (!queued) {
+ rimeDir.deleteRecursively()
+ confirmExportWithoutRime(uri)
+ }
+ }
+
+ private fun confirmExportWithoutRime(uri: Uri) {
+ val dialog = AlertDialog.Builder(this)
+ .setTitle("自动学习词库尚未加载")
+ .setMessage("继续导出的 JSON 不包含 Rime 自动学习词库。先切换到 openIME 并等待输入法就绪后再导出,可包含这部分数据。")
+ .setNegativeButton("取消", null)
+ .setPositiveButton("继续导出") { _, _ ->
+ Thread {
+ runCatching {
+ writeArchive(uri, UserDataRepository.snapshot(this))
+ }.onSuccess {
+ runOnUiThread {
+ Toast.makeText(this, "用户数据已导出", Toast.LENGTH_SHORT).show()
+ }
+ }.onFailure { error ->
+ runOnUiThread {
+ showError("导出失败:" + (error.message ?: "无法写入文件"))
+ }
+ }
+ }.start()
+ }
+ .create()
+ dialog.setOnShowListener { SetupUi.styleDialog(dialog, this) }
+ dialog.show()
+ }
+
+ private fun writeArchive(uri: Uri, archive: UserDataArchive) {
+ val output = contentResolver.openOutputStream(uri, "wt")
+ ?: error("无法打开目标文件")
+ output.bufferedWriter(Charsets.UTF_8).use { writer ->
+ writer.write(UserDataArchiveCodec.encode(archive))
+ }
+ }
+
+ private fun importFrom(uri: Uri) {
+ Thread {
+ val result = runCatching {
+ val input = contentResolver.openInputStream(uri)
+ ?: error("无法打开所选文件")
+ val archive = input.bufferedReader(Charsets.UTF_8).use { reader ->
+ UserDataArchiveCodec.decode(reader.readText())
+ }
+ archive to UserDataRepository.preview(this, archive)
+ }
+ runOnUiThread {
+ result.onSuccess { pair ->
+ confirmImport(pair.first, pair.second)
+ }.onFailure { error ->
+ showError(
+ "无法读取 openIME 用户数据:" +
+ (error.message ?: "文件格式错误"),
+ )
+ }
+ }
+ }.start()
+ }
+
+ private fun confirmImport(
+ archive: UserDataArchive,
+ preview: UserDataImportPreview,
+ ) {
+ val settingsLine = if (preview.settingsWillChange) "设置项将更新。" else "设置项无变化。"
+ val rimeLine =
+ if (preview.rimeDictionaryFiles > 0) {
+ "Rime 自动学习词库 " + preview.rimeDictionaryFiles + " 个文件将合并。"
+ } else {
+ "文件中没有 Rime 自动学习词库。"
+ }
+ val message =
+ "将新增常用语 " + preview.quickPhrasesToAdd +
+ " 条、自定义符号 " + preview.customSymbolsToAdd +
+ " 条、备用用户词 " + preview.userPhrasesToAdd +
+ " 条。" + settingsLine + rimeLine + "现有条目不会删除。"
+ val dialog = AlertDialog.Builder(this)
+ .setTitle("导入用户数据")
+ .setMessage(message)
+ .setNegativeButton("取消", null)
+ .setPositiveButton("导入") { _, _ -> performImport(archive) }
+ .create()
+ dialog.setOnShowListener { SetupUi.styleDialog(dialog, this) }
+ dialog.show()
+ }
+
+ private fun performImport(archive: UserDataArchive) {
+ val rime = archive.rimeUserDictionaries
+ if (rime.isEmpty()) {
+ Thread {
+ UserDataRepository.importKotlinData(this, archive)
+ LocalVoiceImeService.activeInstance?.refreshPersistedSettingsFromActivity()
+ runOnUiThread {
+ Toast.makeText(this, "用户数据已导入", Toast.LENGTH_SHORT).show()
+ }
+ }.start()
+ return
+ }
+
+ val service = LocalVoiceImeService.activeInstance
+ if (service == null) {
+ showError("文件包含 Rime 自动学习词库。请先切换到 openIME,等待输入法就绪后再导入。")
+ return
+ }
+ val sourceDir = File(cacheDir, "rime-user-import").apply {
+ deleteRecursively()
+ mkdirs()
+ }
+ val queued = service.importRimeUserData(sourceDir, rime) { imported ->
+ if (imported == null) {
+ sourceDir.deleteRecursively()
+ runOnUiThread {
+ showError("Rime 自动学习词库未全部导入;常用语、符号和设置未修改,已成功写入的 Rime 词条可能保留。")
+ }
+ return@importRimeUserData
+ }
+ UserDataRepository.importKotlinData(this, archive)
+ service.refreshPersistedSettingsFromActivity()
+ sourceDir.deleteRecursively()
+ runOnUiThread {
+ Toast.makeText(
+ this,
+ "用户数据已导入,Rime 合并 " + imported + " 条词条",
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+ }
+ if (!queued) {
+ sourceDir.deleteRecursively()
+ showError("Rime 自动学习词库尚未就绪,请稍后重试。")
+ }
+ }
+
+ private fun infoCard(title: String, body: String): LinearLayout =
+ LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ setPadding(
+ dp(ImeSpacingTokens.LG_DP),
+ dp(ImeSpacingTokens.LG_DP),
+ dp(ImeSpacingTokens.LG_DP),
+ dp(ImeSpacingTokens.LG_DP),
+ )
+ background = SetupUi.rounded(
+ getColor(R.color.setup_surface),
+ dp(ImeGeometryTokens.CARD_RADIUS_DP).toFloat(),
+ getColor(R.color.setup_input_line),
+ )
+ addView(
+ TextView(this@AboutDataActivity).apply {
+ text = title
+ textSize = ImeTypographyTokens.TITLE_SP
+ setTextColor(getColor(R.color.setup_title))
+ setTypeface(typeface, android.graphics.Typeface.BOLD)
+ },
+ wrap(),
+ )
+ addView(
+ TextView(this@AboutDataActivity).apply {
+ text = body
+ textSize = ImeTypographyTokens.BODY_SP
+ setTextColor(getColor(R.color.setup_body))
+ setPadding(0, dp(ImeSpacingTokens.SM_DP), 0, 0)
+ },
+ wrap(),
+ )
+ }
+
+ @Suppress("DEPRECATION")
+ private fun versionName(): String =
+ packageManager.getPackageInfo(packageName, 0).versionName.orEmpty()
+ .ifBlank { "未知版本" }
+
+ private fun showError(message: String) {
+ val dialog = AlertDialog.Builder(this)
+ .setTitle("操作未完成")
+ .setMessage(message)
+ .setPositiveButton("关闭", null)
+ .create()
+ dialog.setOnShowListener { SetupUi.styleDialog(dialog, this) }
+ dialog.show()
+ }
+
+ private fun wrap() = LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ )
+
+ private fun dp(value: Int): Int = SetupUi.dp(this, value)
+
+ private companion object {
+ const val REQUEST_EXPORT = 8101
+ const val REQUEST_IMPORT = 8102
+ }
+}
diff --git a/app/src/main/java/llc/slacker/openime/BackspaceGestureController.kt b/app/src/main/java/llc/slacker/openime/BackspaceGestureController.kt
index 00cf41e9..eabce336 100644
--- a/app/src/main/java/llc/slacker/openime/BackspaceGestureController.kt
+++ b/app/src/main/java/llc/slacker/openime/BackspaceGestureController.kt
@@ -7,16 +7,19 @@ import android.view.ViewConfiguration
import kotlin.math.abs
/**
- * Owns the held-backspace gesture: repeat delete, upward clear-all arming,
- * hysteresis, and pointer/session state. Editor mutations remain callbacks.
+ * Owns the held-backspace gesture: repeat delete, upward clear-all, and the
+ * one-shot downward restore that is available immediately after a successful
+ * clear gesture. Editor mutations remain callbacks.
*/
internal class BackspaceGestureController(
private val toPx: (Int) -> Int,
private val onDeleteOne: () -> Unit,
private val onClearAll: () -> Unit,
+ private val onUndoClear: () -> Boolean,
private val onPressFeedback: () -> Unit,
private val onHapticFeedback: () -> Unit,
private val onShowClearPopup: (View) -> Unit,
+ private val onShowUndoPopup: (View) -> Unit,
private val onHidePopup: () -> Unit,
) {
private val handler = Handler(Looper.getMainLooper())
@@ -29,6 +32,8 @@ internal class BackspaceGestureController(
private set
private var clearArmed = false
+ private var undoArmed = false
+ private var undoAvailable = false
private var repeatStarted = false
private var repeatSuspended = false
private var startX = 0f
@@ -39,8 +44,9 @@ internal class BackspaceGestureController(
private val repeatAction = object : Runnable {
override fun run() {
- if (!active || clearArmed) return
+ if (!active || clearArmed || undoArmed) return
repeatStarted = true
+ undoAvailable = false
onDeleteOne()
handler.postDelayed(this, REPEAT_INTERVAL_MS)
}
@@ -59,6 +65,7 @@ internal class BackspaceGestureController(
active = true
clearArmed = false
+ undoArmed = false
repeatStarted = false
repeatSuspended = false
startX = rawX
@@ -74,7 +81,7 @@ internal class BackspaceGestureController(
onPressFeedback()
val startRepeat = Runnable {
- if (active && !clearArmed) repeatAction.run()
+ if (active && !clearArmed && !undoArmed) repeatAction.run()
}
repeatStartAction = startRepeat
handler.postDelayed(startRepeat, longPressTimeoutMs)
@@ -83,31 +90,61 @@ internal class BackspaceGestureController(
fun update(rawX: Float, rawY: Float) {
if (!active) return
val upward = startY - rawY
+ val downward = rawY - startY
val horizontal = abs(rawX - startX)
- // Suspend repeat-delete as soon as the gesture clearly points upward,
- // so a slow clear swipe stays atomic instead of deleting on the way.
- if (upward >= toPx(8) && horizontal <= toPx(96)) {
+ // Suspend repeat-delete as soon as the gesture clearly becomes a
+ // vertical command. Otherwise a slow swipe could mutate text before
+ // clear/restore is armed.
+ val verticalCommand =
+ horizontal <= toPx(96) &&
+ (upward >= toPx(8) || (undoAvailable && downward >= toPx(8)))
+ if (verticalCommand) {
repeatSuspended = true
repeatStartAction?.let(handler::removeCallbacks)
handler.removeCallbacks(repeatAction)
- } else if (repeatSuspended) {
+ } else if (repeatSuspended && !clearArmed && !undoArmed) {
repeatSuspended = false
repeatStartAction?.let {
- handler.postDelayed(it, if (repeatStarted) REPEAT_INTERVAL_MS else longPressTimeoutMs)
+ handler.postDelayed(
+ it,
+ if (repeatStarted) REPEAT_INTERVAL_MS else longPressTimeoutMs,
+ )
}
}
- val shouldArm = if (clearArmed) {
+ val shouldUndo = undoAvailable && if (undoArmed) {
+ downward > toPx(16) && horizontal <= toPx(120)
+ } else {
+ downward >= toPx(36) && horizontal <= toPx(96)
+ }
+ if (shouldUndo != undoArmed) {
+ undoArmed = shouldUndo
+ if (undoArmed) {
+ clearArmed = false
+ clearPreviewChanged?.invoke(false)
+ repeatStartAction?.let(handler::removeCallbacks)
+ handler.removeCallbacks(repeatAction)
+ anchor?.let(onShowUndoPopup)
+ onHapticFeedback()
+ return
+ } else {
+ onHidePopup()
+ onHapticFeedback()
+ }
+ }
+ if (undoArmed) return
+
+ val shouldClear = if (clearArmed) {
upward > toPx(16) && horizontal <= toPx(120)
} else {
- upward >= toPx(36) && horizontal <= toPx(96)
+ upward >= toPx(56) && horizontal <= toPx(96)
}
- if (shouldArm == clearArmed) return
+ if (shouldClear == clearArmed) return
- clearArmed = shouldArm
- clearPreviewChanged?.invoke(shouldArm)
- if (shouldArm) {
+ clearArmed = shouldClear
+ clearPreviewChanged?.invoke(shouldClear)
+ if (shouldClear) {
repeatStartAction?.let(handler::removeCallbacks)
handler.removeCallbacks(repeatAction)
anchor?.let(onShowClearPopup)
@@ -119,8 +156,9 @@ internal class BackspaceGestureController(
fun finish(commit: Boolean) {
if (!active) return
+ val restoreClear = commit && undoArmed
val clearAll = commit && clearArmed
- val deleteOnce = commit && !clearArmed && !repeatStarted
+ val deleteOnce = commit && !clearArmed && !undoArmed && !repeatStarted
handler.removeCallbacks(repeatAction)
repeatStartAction?.let(handler::removeCallbacks)
@@ -134,6 +172,7 @@ internal class BackspaceGestureController(
pointerId = -1
clearPreviewChanged?.invoke(false)
clearArmed = false
+ undoArmed = false
repeatStarted = false
repeatSuspended = false
anchor = null
@@ -141,11 +180,23 @@ internal class BackspaceGestureController(
onHidePopup()
when {
+ restoreClear -> {
+ onHapticFeedback()
+ onUndoClear()
+ // The gateway independently validates whether the one-shot
+ // snapshot is still legal. Either way this gesture consumes
+ // our local affordance so stale undo is never offered twice.
+ undoAvailable = false
+ }
clearAll -> {
onHapticFeedback()
onClearAll()
+ undoAvailable = true
+ }
+ deleteOnce -> {
+ undoAvailable = false
+ onDeleteOne()
}
- deleteOnce -> onDeleteOne()
}
}
@@ -156,6 +207,7 @@ internal class BackspaceGestureController(
handler.removeCallbacksAndMessages(null)
repeatStartAction = null
pointerId = -1
+ undoAvailable = false
}
private companion object {
diff --git a/app/src/main/java/llc/slacker/openime/BackspaceKeyFactory.kt b/app/src/main/java/llc/slacker/openime/BackspaceKeyFactory.kt
index c7b572b8..f0b9210b 100644
--- a/app/src/main/java/llc/slacker/openime/BackspaceKeyFactory.kt
+++ b/app/src/main/java/llc/slacker/openime/BackspaceKeyFactory.kt
@@ -28,7 +28,7 @@ internal class BackspaceKeyFactory(
fun build(): ImeKeyView =
createBaseKey(onDeleteOne).apply {
tag = "key-backspace"
- contentDescription = "删除,向上滑清空"
+ contentDescription = "删除,向上滑清空;清空后向下滑撤回"
accessibilityDelegate = object : View.AccessibilityDelegate() {
override fun onInitializeAccessibilityNodeInfo(
host: View,
@@ -59,8 +59,8 @@ internal class BackspaceKeyFactory(
}
val clearHint = TextView(context).apply {
- text = "↑ 清空"
- textSize = 7.5f
+ text = "清空"
+ textSize = ImeTypographyTokens.CAPTION_SP
gravity = Gravity.CENTER
includeFontPadding = false
alpha = 0.72f
diff --git a/app/src/main/java/llc/slacker/openime/CandidateBarController.kt b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt
index bf644a2c..071e504a 100644
--- a/app/src/main/java/llc/slacker/openime/CandidateBarController.kt
+++ b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt
@@ -1,18 +1,23 @@
package llc.slacker.openime
+import android.app.AlertDialog
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.os.Build
+import android.os.SystemClock
import android.view.Gravity
+import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator
import android.widget.HorizontalScrollView
+import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
+import kotlin.math.abs
/**
* Owns candidate presentation: strip diff/reuse, scroll preservation,
@@ -25,7 +30,7 @@ internal class CandidateBarController(
private val context: Context,
private val row: LinearLayout,
private val scroll: HorizontalScrollView,
- private val expandButton: TextView,
+ private val expandButton: ImageView,
private val overlay: LinearLayout,
private val keyboardBody: LinearLayout,
private val toPx: (Int) -> Int,
@@ -38,11 +43,59 @@ internal class CandidateBarController(
private val statefulBackground: (Int, Int, Int) -> Drawable,
private val onFeedback: () -> Unit,
private val onCandidateSelected: (String) -> Unit,
+ private val onCandidateLongPressed: (String) -> Unit,
) {
private var renderedCandidates: List? = null
private var renderedComposition: String? = null
private var renderedExpandedCandidates: List? = null
private var renderedExpandedComposition: String? = null
+ private var candidateTouchDownX = 0f
+ private var candidateTouchDownY = 0f
+ private var candidateTouchActive = false
+ private var scrollingActiveUntilMs = 0L
+
+ init {
+ // Keep ordinary horizontal scrolling native. A deliberate downward
+ // swipe on the candidate strip is only an alternate affordance for
+ // the existing expand button, so it does not create another state
+ // owner or candidate-navigation path.
+ scroll.setOnTouchListener { _, event ->
+ when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ candidateTouchActive = true
+ candidateTouchDownX = event.x
+ candidateTouchDownY = event.y
+ }
+ MotionEvent.ACTION_UP -> {
+ candidateTouchActive = false
+ val dx = event.x - candidateTouchDownX
+ val dy = event.y - candidateTouchDownY
+ if (
+ !expandedOpen &&
+ expandButton.isEnabled &&
+ dy >= toPx(CANDIDATE_EXPAND_SWIPE_DP) &&
+ dy > abs(dx) * 1.15f
+ ) {
+ expandButton.performClick()
+ }
+ }
+ MotionEvent.ACTION_CANCEL -> {
+ candidateTouchActive = false
+ candidateTouchDownX = 0f
+ candidateTouchDownY = 0f
+ }
+ }
+ false
+ }
+ scroll.setOnScrollChangeListener { _, _, _, _, _ ->
+ scrollingActiveUntilMs =
+ SystemClock.uptimeMillis() + SCROLL_SETTLE_WINDOW_MS
+ }
+ }
+
+ fun isInteractionActive(): Boolean =
+ candidateTouchActive ||
+ SystemClock.uptimeMillis() < scrollingActiveUntilMs
var expandedOpen: Boolean = false
private set
@@ -116,7 +169,8 @@ internal class CandidateBarController(
) {
val canExpandOrClose = expandedOpen || hasCandidates
expandButton.isEnabled = canExpandOrClose
- expandButton.alpha = if (canExpandOrClose) 1f else 0.38f
+ expandButton.alpha =
+ if (canExpandOrClose) 1f else ImeSurfacePolicy.DISABLED_ALPHA
if (!canExpandOrClose) {
expandButton.contentDescription = "暂无更多候选"
if (android.os.Build.VERSION.SDK_INT >= 30) {
@@ -133,7 +187,7 @@ internal class CandidateBarController(
compositionPreview: String,
) {
if (!open) {
- expandButton.text = "⌄"
+ expandButton.rotation = 0f
expandButton.contentDescription = "展开更多候选"
overlay.animate().cancel()
keyboardBody.animate().cancel()
@@ -172,7 +226,7 @@ internal class CandidateBarController(
renderedExpandedCandidates = candidates.toList()
renderedExpandedComposition = compositionPreview
expandedOpen = true
- expandButton.text = "⌃"
+ expandButton.rotation = 180f
expandButton.contentDescription = "收起候选"
if (Build.VERSION.SDK_INT >= 30) {
expandButton.stateDescription = "已展开"
@@ -267,7 +321,7 @@ internal class CandidateBarController(
expandedOpen = false
renderedExpandedCandidates = null
renderedExpandedComposition = null
- expandButton.text = "⌄"
+ expandButton.rotation = 0f
expandButton.contentDescription = "展开更多候选"
}
@@ -281,7 +335,7 @@ internal class CandidateBarController(
isClickable = true
addView(
TextView(context).apply {
- textSize = 14f
+ textSize = ImeTypographyTokens.CANDIDATE_SP
maxLines = 1
includeFontPadding = false
setPadding(toPx(12), 0, toPx(12), 0)
@@ -305,19 +359,56 @@ internal class CandidateBarController(
word.typeface = if (index == 0) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
val palette = tokens()
- word.setTextColor(if (index == 0) palette.keyText else palette.candidateText)
+ val normalBackground =
+ if (index == 0) ImeSurfacePolicy.selectedSurface(palette) else Color.TRANSPARENT
+ word.setTextColor(
+ if (index == 0) ImeSurfacePolicy.selectedText(palette) else palette.candidateText,
+ )
item.background = statefulBackground(
- if (index == 0) palette.keyBackground else Color.TRANSPARENT,
- palette.keyPressedBackground,
+ normalBackground,
+ if (index == 0) {
+ ImeSurfacePolicy.pressedSurface(normalBackground, palette)
+ } else {
+ ImeSurfacePolicy.pressedSurface(palette.candidateBackground, palette)
+ },
toPx(ImeGeometryTokens.KEY_RADIUS_DP),
)
+ item.setOnTouchListener { _, event ->
+ when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN -> candidateTouchActive = true
+ MotionEvent.ACTION_UP,
+ MotionEvent.ACTION_CANCEL,
+ -> candidateTouchActive = false
+ }
+ false
+ }
item.setOnClickListener {
onFeedback()
onCandidateSelected(candidate)
}
+ item.setOnLongClickListener {
+ onFeedback()
+ onCandidateLongPressed(candidate)
+ true
+ }
+ }
+
+ fun confirmCandidateDeletion(candidate: String, onConfirm: () -> Unit) {
+ val dialog = AlertDialog.Builder(context)
+ .setTitle("删除此词")
+ .setMessage("从个人学习词中删除“$candidate”?")
+ .setPositiveButton("删除") { _, _ -> onConfirm() }
+ .setNegativeButton("取消", null)
+ .create()
+ dialog.setOnShowListener {
+ SetupUi.styleDialog(dialog, context, destructivePositive = true)
+ }
+ dialog.show()
}
private companion object {
const val STRIP_LIMIT = 24
+ const val CANDIDATE_EXPAND_SWIPE_DP = 36
+ const val SCROLL_SETTLE_WINDOW_MS = 96L
}
}
diff --git a/app/src/main/java/llc/slacker/openime/CandidateEngine.kt b/app/src/main/java/llc/slacker/openime/CandidateEngine.kt
index 520119db..998910c2 100644
--- a/app/src/main/java/llc/slacker/openime/CandidateEngine.kt
+++ b/app/src/main/java/llc/slacker/openime/CandidateEngine.kt
@@ -339,6 +339,87 @@ class CandidateEngine(externalPinyin: Map> = emptyMap()) {
private fun fuzzyVariants(py: String): List = pinyinFuzzyVariants(py)
+ private fun typoCandidatesFor(
+ pinyin: String,
+ fuzzy: Boolean,
+ ): List {
+ val result = linkedSetOf()
+
+ if (fuzzy) {
+ fuzzyVariants(pinyin).forEach { variant ->
+ result.addAll(ImeData.phraseDict[variant].orEmpty().take(2))
+ result.addAll(pinyinDict[variant].orEmpty().take(2))
+ }
+ }
+ result.addAll(ImeData.phraseDict[pinyin].orEmpty().take(4))
+ result.addAll(pinyinDict[pinyin].orEmpty().take(4))
+
+ // Prefix lookup is indexed and bounded; unlike getCandidates(), this
+ // path never performs segmentation or initial-index expansion for each
+ // neighboring-key variant.
+ phrasePrefixIndex.lookup(pinyin)
+ .values
+ .asSequence()
+ .flatMap { it.asSequence() }
+ .filter { it.isNotEmpty() }
+ .take(4)
+ .forEach(result::add)
+
+ var index = lowerBound(sortedPinyinKeys, pinyin)
+ var scanned = 0
+ while (index < sortedPinyinKeys.size && scanned < 4) {
+ val key = sortedPinyinKeys[index]
+ if (!key.startsWith(pinyin)) break
+ pinyinDict[key].orEmpty().firstOrNull()?.let(result::add)
+ index += 1
+ scanned += 1
+ }
+
+ return result
+ .filter { candidate -> candidate.any { ch -> ch.code > 0x7f } }
+ .take(8)
+ }
+
+ /**
+ * Recover common 26-key slips by substituting exactly one physically
+ * adjacent QWERTY key. This is intentionally conservative: it never
+ * changes the user's composition, never chains multiple corrections, and
+ * only contributes real non-ASCII candidates behind the normal result.
+ */
+ fun getAdjacentKeyCorrections(
+ rawPinyin: String,
+ fuzzy: Boolean = false,
+ limit: Int = 24,
+ ): List {
+ val input = rawPinyin.lowercase().trim()
+ if (
+ input.length !in 2..12 ||
+ input.any { it !in 'a'..'z' } ||
+ limit <= 0
+ ) {
+ return emptyList()
+ }
+
+ val result = linkedSetOf()
+ for (index in input.indices.reversed()) {
+ val original = input[index]
+ QWERTY_NEIGHBORS[original].orEmpty().forEach { replacement ->
+ val variant = buildString(input.length) {
+ append(input, 0, index)
+ append(replacement)
+ append(input, index + 1, input.length)
+ }
+ typoCandidatesFor(variant, fuzzy)
+ .take(2)
+ .forEach { candidate ->
+ result += candidate
+ }
+ if (result.size >= limit) return result.take(limit)
+ }
+ }
+ return result.take(limit)
+ }
+
fun get9KeyCandidates(numberStr: String): NineKeyResult {
val digits = numberStr
.take(MAX_NINE_KEY_DIGITS)
@@ -492,6 +573,35 @@ class CandidateEngine(externalPinyin: Map> = emptyMap()) {
private const val MAX_NINE_MATCHES = 12
private const val MAX_LOCAL_RESOLVE_LENGTH = 32
+ private val QWERTY_NEIGHBORS = mapOf(
+ 'q' to "wa",
+ 'w' to "qase",
+ 'e' to "wsdr",
+ 'r' to "edft",
+ 't' to "rfgy",
+ 'y' to "tghu",
+ 'u' to "yhji",
+ 'i' to "ujko",
+ 'o' to "iklp",
+ 'p' to "ol",
+ 'a' to "qwsz",
+ 's' to "awedxz",
+ 'd' to "serfcx",
+ 'f' to "drtgvc",
+ 'g' to "ftyhbv",
+ 'h' to "gyujnb",
+ 'j' to "huikmn",
+ 'k' to "jiolm",
+ 'l' to "kop",
+ 'z' to "asx",
+ 'x' to "zsdc",
+ 'c' to "xdfv",
+ 'v' to "cfgb",
+ 'b' to "vghn",
+ 'n' to "bhjm",
+ 'm' to "njk",
+ )
+
/**
* Correct common legacy table mistakes without trusting the table at
* runtime. Every value still passes nineKeyDigitsForPinyin() before it
diff --git a/app/src/main/java/llc/slacker/openime/CandidatePipeline.kt b/app/src/main/java/llc/slacker/openime/CandidatePipeline.kt
index 6744cb70..54d3fc04 100644
--- a/app/src/main/java/llc/slacker/openime/CandidatePipeline.kt
+++ b/app/src/main/java/llc/slacker/openime/CandidatePipeline.kt
@@ -45,14 +45,22 @@ class CandidatePipeline internal constructor(
val displayPinyinPaths: List = emptyList(),
)
- private val nineKeyDecoder = NineKeyLocalDecoder(engine)
+ private val nineKeyDecoder: NineKeyLocalDecoder = run {
+ val startedAt = System.nanoTime()
+ NineKeyLocalDecoder(engine).also {
+ NineKeyPerformanceTrace.recordDecoderConstruction(
+ elapsedNs = System.nanoTime() - startedAt,
+ threadName = Thread.currentThread().name,
+ )
+ }
+ }
override fun candidatesFor(
mode: KeyboardMode,
composition: String,
fuzzy: Boolean,
): List = when (mode) {
- KeyboardMode.PINYIN_26 -> engine.getCandidates(composition, fuzzy)
+ KeyboardMode.PINYIN_26 -> pinyin26Candidates(composition, fuzzy)
KeyboardMode.ENGLISH_26 -> englishCandidates(composition)
KeyboardMode.PINYIN_9 -> if (composition.length <= 32) {
engine.getCandidates(composition, fuzzy)
@@ -62,6 +70,24 @@ class CandidatePipeline internal constructor(
KeyboardMode.DIGITS -> emptyList()
}
+ private fun pinyin26Candidates(
+ composition: String,
+ fuzzy: Boolean,
+ ): List {
+ val normal = engine.getCandidates(composition, fuzzy)
+ val meaningfulNormal = normal.count { candidate ->
+ candidate != composition && candidate.any { it.code > 0x7f }
+ }
+ if (meaningfulNormal >= TYPO_CORRECTION_MIN_NORMAL_CANDIDATES) {
+ return normal.take(MAX_CANDIDATES)
+ }
+
+ return (normal + engine.getAdjacentKeyCorrections(composition, fuzzy))
+ .filter { it.isNotEmpty() }
+ .distinct()
+ .take(MAX_CANDIDATES)
+ }
+
/**
* English completion is advisory. The exact text the user typed must stay
* candidate #1 because punctuation, space and mode changes commit that
@@ -199,6 +225,7 @@ class CandidatePipeline internal constructor(
companion object {
private const val MAX_CANDIDATES = 96
private const val PER_PATH_CANDIDATES = 24
+ private const val TYPO_CORRECTION_MIN_NORMAL_CANDIDATES = 8
fun nineKeyDigitsFor(pinyin: String): String? =
NineKeyLocalDecoder.digitsForPinyin(pinyin)
diff --git a/app/src/main/java/llc/slacker/openime/CandidateQueryCoordinator.kt b/app/src/main/java/llc/slacker/openime/CandidateQueryCoordinator.kt
index 72590f33..c9819df0 100644
--- a/app/src/main/java/llc/slacker/openime/CandidateQueryCoordinator.kt
+++ b/app/src/main/java/llc/slacker/openime/CandidateQueryCoordinator.kt
@@ -8,6 +8,7 @@ import java.util.concurrent.atomic.AtomicLong
internal data class CandidateNativeQueryResult(
val choices: List,
val latencyMs: Long,
+ val resultLatencyMs: Long = latencyMs,
)
internal data class CandidateQueryTicket(
@@ -63,6 +64,7 @@ internal class CandidateQueryCoordinator(
) -> Unit,
): CandidateQueryTicket {
val request = generation.incrementAndGet()
+ val requestedAt = SystemClock.elapsedRealtime()
val queryInputs = rimeInputs
.asSequence()
.map { it.trim() }
@@ -96,7 +98,14 @@ internal class CandidateQueryCoordinator(
mainHandler.post {
if (!isCurrent(request, queryInputs)) return@post
- onResult(request, queryInputs, query)
+ onResult(
+ request,
+ queryInputs,
+ query.copy(
+ resultLatencyMs =
+ SystemClock.elapsedRealtime() - requestedAt,
+ ),
+ )
}
}
diff --git a/app/src/main/java/llc/slacker/openime/ClipboardPanelController.kt b/app/src/main/java/llc/slacker/openime/ClipboardPanelController.kt
index 743afabf..ebeae88a 100644
--- a/app/src/main/java/llc/slacker/openime/ClipboardPanelController.kt
+++ b/app/src/main/java/llc/slacker/openime/ClipboardPanelController.kt
@@ -209,7 +209,7 @@ internal class ClipboardPanelController(
private fun addRefreshAction(column: LinearLayout) {
column.addView(
- createPanelButton("重新读取", 12f, true).apply {
+ createPanelButton("重新读取", ImeTypographyTokens.CAPTION_SP, true).apply {
tag = "clipboard-refresh"
contentDescription = "重新读取剪贴板"
setOnClickListener {
@@ -226,7 +226,7 @@ internal class ClipboardPanelController(
private fun renderQuickPhrases(column: LinearLayout) {
column.addView(
- createPanelButton("新增常用语", 13f, true).apply {
+ createPanelButton("新增常用语", ImeTypographyTokens.BODY_SP, true).apply {
tag = "quick-phrase-add"
setOnClickListener {
onFeedback()
@@ -270,18 +270,24 @@ internal class ClipboardPanelController(
orientation = LinearLayout.HORIZONTAL
tag = "phrase-card"
addView(
- createKey(phrase.text, 13f) {
+ createKey(phrase.text, ImeTypographyTokens.BODY_SP) {
onCharacter(phrase.text)
}.apply {
setPadding(toPx(12), 0, toPx(12), 0)
tag = "phrase:${phrase.id}"
+ contentDescription =
+ if (phrase.inputCode.isBlank()) {
+ "常用语:${phrase.text}"
+ } else {
+ "常用语:${phrase.text},输入码${phrase.inputCode}"
+ }
},
LinearLayout.LayoutParams(0, toPx(ImeGeometryTokens.TOUCH_TARGET_DP), 1f).apply {
marginEnd = toPx(5)
},
)
addView(
- createPanelButton("编辑", 11f, true).apply {
+ createPanelButton("编辑", ImeTypographyTokens.CAPTION_SP, true).apply {
tag = "phrase-edit:${phrase.id}"
setOnClickListener {
onFeedback()
@@ -293,7 +299,7 @@ internal class ClipboardPanelController(
},
)
addView(
- createPanelButton("删除", 11f, true).apply {
+ createPanelButton("删除", ImeTypographyTokens.CAPTION_SP, true).apply {
tag = "phrase-delete:${phrase.id}"
setOnClickListener {
onFeedback()
@@ -368,7 +374,7 @@ internal class ClipboardPanelController(
meta.addView(
createPanelButton(
if (entry.pinned) "取消置顶" else "置顶",
- 10f,
+ ImeTypographyTokens.CAPTION_SP,
true,
).apply {
tag = "clip-pin:${entry.text}"
@@ -381,7 +387,7 @@ internal class ClipboardPanelController(
wrapParams(),
)
meta.addView(
- createPanelButton("使用", 10f, true).apply {
+ createPanelButton("使用", ImeTypographyTokens.CAPTION_SP, true).apply {
tag = "clip-use:${entry.text}"
setOnClickListener {
onFeedback()
diff --git a/app/src/main/java/llc/slacker/openime/EmojiCellFactory.kt b/app/src/main/java/llc/slacker/openime/EmojiCellFactory.kt
index 8fe478a6..db2515d5 100644
--- a/app/src/main/java/llc/slacker/openime/EmojiCellFactory.kt
+++ b/app/src/main/java/llc/slacker/openime/EmojiCellFactory.kt
@@ -31,7 +31,7 @@ internal class EmojiCellFactory(
}
val fallback = TextView(context).apply {
text = emoji
- textSize = 21f
+ textSize = ImeTypographyTokens.KEY_LETTER_SP
gravity = Gravity.CENTER
includeFontPadding = false
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
diff --git a/app/src/main/java/llc/slacker/openime/FloatingKeyboardController.kt b/app/src/main/java/llc/slacker/openime/FloatingKeyboardController.kt
index 19951feb..c3efa443 100644
--- a/app/src/main/java/llc/slacker/openime/FloatingKeyboardController.kt
+++ b/app/src/main/java/llc/slacker/openime/FloatingKeyboardController.kt
@@ -78,7 +78,7 @@ internal class FloatingKeyboardController(
)
mainDock.clipToOutline = true
} else {
- mainDock.setBackgroundColor(tokens.expandedBackground)
+ mainDock.setBackgroundColor(tokens.keyboardBackground)
mainDock.clipToOutline = false
}
}
diff --git a/app/src/main/java/llc/slacker/openime/FloatingWindowController.kt b/app/src/main/java/llc/slacker/openime/FloatingWindowController.kt
index d1516b8b..365dd65d 100644
--- a/app/src/main/java/llc/slacker/openime/FloatingWindowController.kt
+++ b/app/src/main/java/llc/slacker/openime/FloatingWindowController.kt
@@ -17,6 +17,8 @@ internal class FloatingWindowController(
private val mainHandler: Handler,
private val windowProvider: () -> Window?,
private val keyboardHeightPx: () -> Int?,
+ private val floatingWidthPercent: () -> Int,
+ private val floatingOpacityPercent: () -> Int,
private val debugLog: (String) -> Unit = {},
) {
var enabled: Boolean = false
@@ -28,6 +30,7 @@ internal class FloatingWindowController(
private var baseWidth: Int? = null
private var baseHeight: Int? = null
private var baseSoftInputMode: Int? = null
+ private var baseAlpha: Float? = null
fun enable(resetPosition: Boolean = x == 0 && y == 0) {
enabled = true
@@ -61,6 +64,7 @@ internal class FloatingWindowController(
attrs.width = baseWidth ?: WindowManager.LayoutParams.MATCH_PARENT
attrs.height = baseHeight ?: WindowManager.LayoutParams.WRAP_CONTENT
baseSoftInputMode?.let { attrs.softInputMode = it }
+ baseAlpha?.let { attrs.alpha = it }
attrs.x = 0
attrs.y = 0
imeWindow.attributes = attrs
@@ -77,6 +81,7 @@ internal class FloatingWindowController(
baseWidth = attrs.width
baseHeight = attrs.height
baseSoftInputMode = attrs.softInputMode
+ baseAlpha = attrs.alpha
}
val (screenWidth, screenHeight) = displaySize()
@@ -96,6 +101,7 @@ internal class FloatingWindowController(
attrs.width = desiredWidth
attrs.height = currentHeight
attrs.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
+ attrs.alpha = floatingOpacityPercent().coerceIn(82, 100) / 100f
attrs.x = x.coerceIn(bounds[0], bounds[1])
attrs.y = y.coerceIn(bounds[2], bounds[3])
x = attrs.x
@@ -127,6 +133,7 @@ internal class FloatingWindowController(
attrs.width = width
attrs.height = height
attrs.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
+ attrs.alpha = floatingOpacityPercent().coerceIn(82, 100) / 100f
attrs.x = x
attrs.y = y
imeWindow.attributes = attrs
@@ -149,20 +156,20 @@ internal class FloatingWindowController(
}
private fun floatingWidth(screenWidth: Int): Int {
- val landscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
- val preferred = if (landscape) {
- dp(ImeGeometryTokens.FLOATING_LANDSCAPE_WIDTH_DP)
- } else {
- (screenWidth * 0.88f).toInt()
- }
+ val landscape =
+ resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+ val percent = floatingWidthPercent().coerceIn(72, 96)
+ val preferred = (screenWidth * percent / 100f).toInt()
val maximum = dp(
- if (landscape) ImeGeometryTokens.FLOATING_LANDSCAPE_WIDTH_DP else 400,
+ if (landscape) ImeGeometryTokens.FLOATING_LANDSCAPE_WIDTH_DP else 420,
)
+ val available = (screenWidth - dp(16)).coerceAtLeast(dp(1))
+ val minimum = minOf(dp(if (landscape) 300 else 280), available)
return minOf(
preferred,
maximum,
- (screenWidth - dp(16)).coerceAtLeast(dp(1)),
- ).coerceAtLeast(dp(320))
+ available,
+ ).coerceAtLeast(minimum)
}
private fun displaySize(): Pair {
diff --git a/app/src/main/java/llc/slacker/openime/ImeData.kt b/app/src/main/java/llc/slacker/openime/ImeData.kt
index 756bc78a..ca847652 100644
--- a/app/src/main/java/llc/slacker/openime/ImeData.kt
+++ b/app/src/main/java/llc/slacker/openime/ImeData.kt
@@ -525,7 +525,7 @@ object ImeData {
)
val keypad9Combinations: Map> = mapOf(
- "64" to listOf("ni", "mi", "oh"),
+ "64" to listOf("ni", "mi"),
"64426" to listOf("nihao"),
"9664" to listOf("yong"),
"426" to listOf("gong", "hao"),
diff --git a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt
index 063a18b3..8dac18cc 100644
--- a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt
+++ b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt
@@ -32,7 +32,8 @@ internal object ImeGeometryTokens {
const val SWITCH_KNOB_DP = 20
const val SWITCH_PADDING_DP = 4
const val SWITCH_KNOB_TRAVEL_DP = SWITCH_WIDTH_DP - SWITCH_PADDING_DP * 2 - SWITCH_KNOB_DP
- const val KEY_ROW_GAP_DP = 6
+ const val KEY_GAP_DP = 6
+ const val KEY_ROW_GAP_DP = KEY_GAP_DP
const val KEY_SIDE_MARGIN_DP = 2
const val KEY_POPUP_HEIGHT_DP = 76
const val KEY_POPUP_MIN_WIDTH_DP = 40
@@ -43,9 +44,24 @@ internal object ImeGeometryTokens {
// The toolbar itself is compact; the top zone still reserves the larger
// composed height so typing never moves the keyboard window.
const val TOOLBAR_HEIGHT_DP = TOUCH_TARGET_DP
+ const val TOP_BAR_HEIGHT_DP = 56
+ const val ICON_SIZE_DP = 24
+ const val STEP_MARK_SIZE_DP = 28
+ const val HERO_MARK_SIZE_DP = 72
const val COMPOSED_TOP_ZONE_HEIGHT_DP = 70
}
+/** Shared spacing scale for every non-keyboard layout. */
+internal object ImeSpacingTokens {
+ const val XXS_DP = 2
+ const val XS_DP = 4
+ const val SM_DP = 8
+ const val MD_DP = 12
+ const val LG_DP = 16
+ const val XL_DP = 24
+ const val XXL_DP = 32
+}
+
/** Shared motion timing for native IME surfaces. */
internal object ImeMotionTokens {
const val POPUP_ENTER_MS = 80L
@@ -54,13 +70,20 @@ internal object ImeMotionTokens {
const val STANDARD_TRANSITION_MS = 160L
}
-/** Shared native text roles; keyboard glyph sizing remains renderer-specific. */
+/** Six text roles shared by the keyboard and every app surface. */
internal object ImeTypographyTokens {
- const val PANEL_TITLE_SP = 13f
- const val PANEL_BODY_SP = 13f
- const val PANEL_NOTE_SP = 12f
const val CAPTION_SP = 11f
- const val CANDIDATE_SP = 12f
+ const val BODY_SP = 14f
+ const val TITLE_SP = 16f
+ const val CANDIDATE_SP = 18f
+ const val KEY_LETTER_SP = 21f
+ const val DISPLAY_SP = 28f
+
+ // Compatibility names for existing callers; every alias resolves to the
+ // canonical six-step scale above.
+ const val PANEL_TITLE_SP = TITLE_SP
+ const val PANEL_BODY_SP = BODY_SP
+ const val PANEL_NOTE_SP = BODY_SP
}
/**
@@ -72,11 +95,7 @@ internal object ImeTypographyTokens {
* compatibility, but the product intentionally exposes only this skin.
*/
enum class ImeTheme(val key: String, val label: String) {
- IOS("theme-ios", "iOS 极简玻璃"),
- DARK("theme-dark", "午夜深色"),
- CYBERPUNK("theme-cyberpunk", "赛博霓虹"),
- CLASSIC("theme-classic", "经典桌面"),
- MACOS("theme-macos", "macOS Tahoe 极简"),
+ IOS("theme-ios", "iOS 极简"),
;
data class Tokens(
@@ -103,7 +122,19 @@ enum class ImeTheme(val key: String, val label: String) {
val toolCardBackground: Int,
val panelHeadBackground: Int,
val destructive: Int = Color.parseColor("#F4212E"),
- )
+ val success: Int = Color.parseColor("#1F8A4C"),
+ val onAccent: Int = Color.parseColor("#07131D"),
+ val textSecondaryRole: Int? = null,
+ ) {
+ val canvas: Int get() = keyboardBackground
+ val surface: Int get() = toolbarBackground
+ val outline: Int get() = border
+ val text: Int get() = keyText
+ val textSecondary: Int get() = textSecondaryRole ?: keySecondaryText
+ val accent: Int get() = primary
+ val danger: Int get() = destructive
+ val accentPressed: Int get() = ImeSurfacePolicy.primaryPressed(this)
+ }
fun tokens(
appearance: ImeAppearance = ImeAppearance.DARK,
@@ -116,47 +147,22 @@ enum class ImeTheme(val key: String, val label: String) {
ImeAppearance.LIGHT -> false
ImeAppearance.DARK -> true
}
- val base = when (this) {
- // minis_ime_dual_theme_renderer.html 的 Dark Obsidian / Light Crystal
- // 调色板。390 × 296 只是设计基准,尺寸仍由原生 View 的实际窗口计算。
- IOS -> if (useDark) {
- Tokens(
- c("#afc6ff"), c("#181a1e"), c("#202228"), c("#202228"), c("#e8e9ed"),
- c("#303238"), c("#e8e9ed"), c("#b3b7c2"), c("#25272c"), c("#d7dae2"), c("#3a3d44"),
- c("#25272c"), c("#343740"), c("#25272c"), c("#181a1e"), c("#202228"),
- c("#303238"), c("#e8e9ed"), c("#25272c"), c("#d7dae2"), c("#25272c"), c("#202228"),
- )
- } else {
- Tokens(
- c("#1D9BF0"), c("#e6e7eb"), c("#f0f1f3"), c("#f0f1f3"), c("#202124"),
- c("#fafafb"), c("#202124"), c("#555b66"), c("#d2d4db"), c("#374151"), c("#e3e5e9"),
- c("#f5f6f8"), c("#c8cbd2"), c("#d2d4db"), c("#e6e7eb"), c("#f0f1f3"),
- c("#fafafb"), c("#202124"), c("#d2d4db"), c("#374151"), c("#f8f9fa"), c("#f0f1f3"),
- )
- }
- DARK -> Tokens(
- c("#3b82f6"), c("#1e293b"), c("#0f172a"), c("#0f172a"), c("#f1f5f9"),
- c("#334155"), c("#f8fafc"), c("#94a3b8"), c("#1e293b"), c("#94a3b8"), c("#475569"),
- c("#1e3a8a"), Color.argb(20, 255, 255, 255), c("#475569"), c("#0f172a"), c("#0f172a"),
- c("#e2e8f0"), c("#0f172a"), c("#475569"), c("#f1f5f9"), c("#1e293b"), c("#0f172a"),
- )
- CYBERPUNK -> Tokens(
- c("#ff007f"), c("#0d0e1f"), c("#12142e"), c("#080918"), c("#00f0ff"),
- c("#161838"), c("#00f0ff"), c("#ffe600"), c("#090a18"), c("#00f0ff"), c("#ff007f"),
- c("#2a0845"), Color.argb(76, 0, 240, 255), c("#161838"), c("#080918"), c("#080918"),
- c("#161838"), c("#00f0ff"), c("#090a18"), c("#ffe600"), c("#12142e"), c("#0d0e1f"),
- )
- CLASSIC -> Tokens(
- c("#e11d48"), c("#f3f4f6"), c("#ffffff"), c("#ffffff"), c("#111827"),
- c("#ffffff"), c("#111827"), c("#9ca3af"), c("#e5e7eb"), c("#374151"), c("#d1d5db"),
- c("#fee2e2"), c("#d1d5db"), c("#e5e7eb"), c("#ffffff"), c("#ffffff"),
- c("#ffffff"), c("#111827"), c("#e5e7eb"), c("#111827"), c("#f8fafc"), c("#e2e8f0"),
+ val base = if (useDark) {
+ Tokens(
+ c("#6EC3F7"), c("#1C1C1E"), c("#242426"), c("#262628"), c("#F2F2F7"),
+ c("#3A3A3C"), c("#F2F2F7"), c("#AEAEB2"), c("#2C2C2E"), c("#F2F2F7"), c("#4A4A4D"),
+ c("#242426"), c("#48484A"), c("#2C2C2E"), c("#202022"), c("#242426"),
+ c("#3A3A3C"), c("#F2F2F7"), c("#2C2C2E"), c("#F2F2F7"), c("#303033"), c("#242426"),
+ success = c("#5BD08A"),
)
- MACOS -> Tokens(
- c("#0ea5e9"), c("#e2e8f0"), c("#ffffff"), c("#ffffff"), c("#1e293b"),
- c("#ffffff"), c("#0f172a"), c("#64748b"), c("#cbd5e1"), c("#475569"), c("#94a3b8"),
- c("#e0f2fe"), Color.argb(15, 0, 0, 0), c("#cbd5e1"), c("#ffffff"), c("#ffffff"),
- c("#ffffff"), c("#0f172a"), c("#cbd5e1"), c("#0f172a"), c("#f1f5f9"), c("#e2e8f0"),
+ } else {
+ Tokens(
+ c("#1D9BF0"), c("#D5D8DE"), c("#EEF0F3"), c("#F7F8FA"), c("#1F2023"),
+ c("#FFFFFF"), c("#1C1C1E"), c("#6E6E73"), c("#C5C9D1"), c("#2C2D31"), c("#DDE1E7"),
+ c("#F2F3F5"), c("#B7BCC5"), c("#C5C9D1"), c("#F1F2F4"), c("#F8F9FA"),
+ c("#FFFFFF"), c("#1C1C1E"), c("#C5C9D1"), c("#2C2D31"), c("#FFFFFF"), c("#E4E7EB"),
+ success = c("#1F8A4C"),
+ textSecondaryRole = c("#6D6D72"),
)
}
val accent = accentOverride ?: return base
@@ -164,6 +170,114 @@ enum class ImeTheme(val key: String, val label: String) {
}
}
+/**
+ * Derived interaction roles. Base palettes own neutral hierarchy; these
+ * functions derive selected/pressed/disabled surfaces so every panel uses the
+ * same state language instead of inventing another gray.
+ */
+internal object ImeSurfacePolicy {
+ const val DISABLED_ALPHA = 0.42f
+
+ fun isDark(tokens: ImeTheme.Tokens): Boolean =
+ ImeContrastPolicy.relativeLuminance(tokens.keyboardBackground) < 0.16
+
+ fun selectedSurface(tokens: ImeTheme.Tokens): Int =
+ ImeDrawableFactory.blend(
+ tokens.primary,
+ tokens.candidateBackground,
+ if (isDark(tokens)) 0.24f else 0.12f,
+ )
+
+ fun selectedText(tokens: ImeTheme.Tokens): Int =
+ if (
+ ImeContrastPolicy.contrastRatio(tokens.primary, selectedSurface(tokens)) >= 4.5
+ ) {
+ tokens.primary
+ } else {
+ tokens.keyText
+ }
+
+ fun pressedSurface(base: Int, tokens: ImeTheme.Tokens): Int =
+ ImeDrawableFactory.blend(
+ tokens.keyText,
+ base,
+ if (isDark(tokens)) 0.12f else 0.08f,
+ )
+
+ fun primaryPressed(tokens: ImeTheme.Tokens): Int =
+ adjustHslLightness(
+ tokens.primary,
+ if (isDark(tokens)) -0.06f else -0.08f,
+ )
+
+ private fun adjustHslLightness(color: Int, delta: Float): Int {
+ val r = Color.red(color) / 255f
+ val g = Color.green(color) / 255f
+ val b = Color.blue(color) / 255f
+ val max = maxOf(r, g, b)
+ val min = minOf(r, g, b)
+ var h = 0f
+ var s = 0f
+ val l = (max + min) / 2f
+ val d = max - min
+ if (d != 0f) {
+ s = d / (1f - kotlin.math.abs(2f * l - 1f))
+ h = when (max) {
+ r -> 60f * (((g - b) / d) % 6f)
+ g -> 60f * (((b - r) / d) + 2f)
+ else -> 60f * (((r - g) / d) + 4f)
+ }
+ if (h < 0f) h += 360f
+ }
+ val targetL = (l + delta).coerceIn(0f, 1f)
+ val chroma = (1f - kotlin.math.abs(2f * targetL - 1f)) * s
+ val x = chroma * (1f - kotlin.math.abs((h / 60f) % 2f - 1f))
+ val m = targetL - chroma / 2f
+ val (rr, gg, bb) = when {
+ h < 60f -> Triple(chroma, x, 0f)
+ h < 120f -> Triple(x, chroma, 0f)
+ h < 180f -> Triple(0f, chroma, x)
+ h < 240f -> Triple(0f, x, chroma)
+ h < 300f -> Triple(x, 0f, chroma)
+ else -> Triple(chroma, 0f, x)
+ }
+ return Color.rgb(
+ ((rr + m) * 255f).toInt().coerceIn(0, 255),
+ ((gg + m) * 255f).toInt().coerceIn(0, 255),
+ ((bb + m) * 255f).toInt().coerceIn(0, 255),
+ )
+ }
+
+ fun subtleAccentSurface(tokens: ImeTheme.Tokens): Int =
+ ImeDrawableFactory.blend(
+ tokens.primary,
+ tokens.toolCardBackground,
+ if (isDark(tokens)) 0.18f else 0.08f,
+ )
+
+ fun destructiveSurface(tokens: ImeTheme.Tokens): Int =
+ ImeDrawableFactory.blend(
+ tokens.destructive,
+ tokens.toolCardBackground,
+ if (isDark(tokens)) 0.18f else 0.10f,
+ )
+
+ fun destructiveText(tokens: ImeTheme.Tokens): Int {
+ val surface = destructiveSurface(tokens)
+ return if (ImeContrastPolicy.contrastRatio(tokens.destructive, surface) >= 4.5) {
+ tokens.destructive
+ } else {
+ ImeDrawableFactory.contrastText(surface)
+ }
+ }
+
+ fun divider(tokens: ImeTheme.Tokens): Int =
+ ImeDrawableFactory.withAlpha(
+ tokens.border,
+ if (isDark(tokens)) 150 else 180,
+ )
+}
+
/** Shared WCAG contrast decisions for every native surface. */
internal object ImeContrastPolicy {
private const val DARK_CONTENT = 0xff07131d.toInt()
@@ -204,3 +318,39 @@ internal object ImeFocusRingPolicy {
ImeContrastPolicy.contrastText(background)
}
}
+
+/** User-selectable accents share the same color source as the rest of the IME. */
+object AccentPalette {
+ const val DEFAULT = "#1D9BF0"
+ val presets = listOf(
+ "#1D9BF0" to "蓝色",
+ "#FFD400" to "黄色",
+ "#F91880" to "粉色",
+ "#7856FF" to "紫色",
+ "#FF7A00" to "橙色",
+ "#00BA7C" to "绿色",
+ "#00C2D7" to "青色",
+ "#38BDF8" to "天蓝",
+ "#5865F2" to "靛蓝",
+ "#9B5DE5" to "深紫",
+ "#E94FB8" to "洋红",
+ "#F4212E" to "红色",
+ "#FF5A5F" to "珊瑚红",
+ "#F59E0B" to "琥珀",
+ "#84CC16" to "青柠",
+ "#22C55E" to "翠绿",
+ "#10CFA0" to "薄荷",
+ "#14B8A6" to "蓝绿",
+ )
+
+ fun parse(value: String?): Int = runCatching {
+ val raw = normalize(value).removePrefix("#")
+ (0xFF000000L or raw.toLong(16)).toInt()
+ }.getOrDefault((0xFF000000L or DEFAULT.removePrefix("#").toLong(16)).toInt())
+
+ fun normalize(value: String?): String {
+ val raw = value.orEmpty().trim()
+ val hex = if (raw.startsWith("#")) raw else "#$raw"
+ return if (hex.matches(Regex("#?[0-9a-fA-F]{6}"))) hex.uppercase() else DEFAULT
+ }
+}
diff --git a/app/src/main/java/llc/slacker/openime/ImeDrawableFactory.kt b/app/src/main/java/llc/slacker/openime/ImeDrawableFactory.kt
index b2f169df..884525aa 100644
--- a/app/src/main/java/llc/slacker/openime/ImeDrawableFactory.kt
+++ b/app/src/main/java/llc/slacker/openime/ImeDrawableFactory.kt
@@ -80,6 +80,22 @@ internal object ImeDrawableFactory {
}
}
+ /**
+ * Mix two opaque UI colors without bringing in another color library.
+ * amount=0 returns background; amount=1 returns foreground.
+ */
+ fun blend(foreground: Int, background: Int, amount: Float): Int {
+ val t = amount.coerceIn(0f, 1f)
+ fun channel(front: Int, back: Int): Int =
+ (back + (front - back) * t).toInt().coerceIn(0, 255)
+ return Color.argb(
+ channel(Color.alpha(foreground), Color.alpha(background)),
+ channel(Color.red(foreground), Color.red(background)),
+ channel(Color.green(foreground), Color.green(background)),
+ channel(Color.blue(foreground), Color.blue(background)),
+ )
+ }
+
fun withAlpha(color: Int, alpha: Int): Int = Color.argb(
alpha.coerceIn(0, 255),
Color.red(color),
diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyView.kt
index dfe18273..c1649360 100644
--- a/app/src/main/java/llc/slacker/openime/ImeKeyView.kt
+++ b/app/src/main/java/llc/slacker/openime/ImeKeyView.kt
@@ -27,7 +27,7 @@ class ImeKeyView(
text: String = "",
secondary: String? = null,
iconRes: Int = 0,
- mainTextSize: Float = 17f,
+ mainTextSize: Float = ImeTypographyTokens.KEY_LETTER_SP,
fitMainText: Boolean = false,
) : FrameLayout(context) {
@@ -153,7 +153,7 @@ class ImeKeyView(
secondaryTextView = secondary?.takeIf { it.isNotEmpty() }?.let { sub ->
TextView(context).apply {
this.text = sub
- textSize = 9f
+ textSize = ImeTypographyTokens.CAPTION_SP
gravity = Gravity.CENTER
isAllCaps = false
includeFontPadding = false
@@ -166,8 +166,8 @@ class ImeKeyView(
addView(
view,
FrameLayout.LayoutParams(
- dp(if (text.isEmpty()) 18 else 13),
- dp(if (text.isEmpty()) 18 else 13),
+ dp(if (text.isEmpty()) 20 else 16),
+ dp(if (text.isEmpty()) 20 else 16),
).apply {
gravity = if (text.isEmpty()) {
Gravity.CENTER
@@ -241,6 +241,10 @@ class ImeKeyView(
}
}
+ fun setSecondaryAlpha(alpha: Float) {
+ secondaryTextView?.alpha = alpha.coerceIn(0f, 1f)
+ }
+
fun allowTwoLineLabel() {
mainTextView?.maxLines = 2
}
diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt
index ab4809ab..9f4bf482 100644
--- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt
+++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt
@@ -9,6 +9,7 @@ import android.media.AudioManager
import android.os.Build
import android.os.Handler
import android.os.Looper
+import android.os.SystemClock
import android.text.TextUtils
import android.view.Gravity
import android.view.HapticFeedbackConstants
@@ -41,6 +42,8 @@ open class ImeKeyboardView(
fun onCharacter(char: String)
fun onBackspace()
fun onClearAll()
+ fun onUndoClear(): Boolean = false
+ fun onUndoClearExpired() {}
fun onSpace()
fun onFloatingKeyboardChanged(floating: Boolean)
fun onFloatingKeyboardDragged(deltaX: Float, deltaY: Float)
@@ -72,6 +75,7 @@ open class ImeKeyboardView(
onCompositionChanged(composition, candidates)
}
fun onCandidateSelected(candidate: String)
+ fun onCandidateLongPressed(candidate: String) {}
/**
* An association ("联想") chip is rendered only *after* a candidate was
@@ -95,6 +99,10 @@ open class ImeKeyboardView(
fun onPopupChanged(enabled: Boolean)
fun onFuzzyChanged(enabled: Boolean)
fun onSkinChanged(opacity: Int, radius: Int, fontSize: Int, primaryColor: String) {}
+ fun onHandednessChanged(handedness: ImeHandedness) {}
+ fun onKeyboardHeightChanged(percent: Int) {}
+ fun onFloatingStyleChanged(widthPercent: Int, opacityPercent: Int) {}
+ fun onOpenAboutData() {}
}
/** Visual class marker for white keys (nine/digits grid). */
@@ -114,9 +122,11 @@ open class ImeKeyboardView(
toPx = ::dp,
onDeleteOne = ::performBackspaceOnce,
onClearAll = listener::onClearAll,
+ onUndoClear = listener::onUndoClear,
onPressFeedback = ::feedback,
onHapticFeedback = ::hapticFeedback,
onShowClearPopup = { anchor -> showPopup(anchor, "清空") },
+ onShowUndoPopup = { anchor -> showPopup(anchor, "撤回") },
onHidePopup = ::hidePopup,
)
private val backspaceKeyFactory: BackspaceKeyFactory by lazy {
@@ -129,7 +139,7 @@ open class ImeKeyboardView(
text = "",
func = true,
secondary = null,
- mainTextSizeOverride = 15f,
+ mainTextSizeOverride = ImeTypographyTokens.BODY_SP,
iconRes = R.drawable.ic_backspace,
onTap = onTap,
)
@@ -151,6 +161,9 @@ open class ImeKeyboardView(
onCancelPreviewChanged = { cancelling ->
voicePanelController.setCancelPreview(cancelling)
},
+ onCursorStep = { direction ->
+ listener.onTextEdit(if (direction < 0) "left" else "right")
+ },
)
private val spaceVoiceKeyFactory: SpaceVoiceKeyFactory by lazy {
SpaceVoiceKeyFactory(
@@ -160,7 +173,7 @@ open class ImeKeyboardView(
text = label,
func = true,
secondary = null,
- mainTextSizeOverride = 14f,
+ mainTextSizeOverride = ImeTypographyTokens.BODY_SP,
iconRes = R.drawable.ic_mic,
onTap = {
if (!insertIntoInlineEditor(" ")) onTap()
@@ -215,9 +228,14 @@ open class ImeKeyboardView(
private var appliedOrientation = resources.configuration.orientation
private var appliedFontScale = resources.configuration.fontScale
private var appliedDensityDpi = resources.displayMetrics.densityDpi
+ private var keyboardHandedness = ImeSettingsRepository.loadHandedness(context)
+ private var keyboardHeightPercent = ImeSettingsRepository.loadKeyboardHeightPercent(context)
+ private var floatingWidthPercent = ImeSettingsRepository.loadFloatingWidthPercent(context)
+ private var floatingOpacityPercent = ImeSettingsRepository.loadFloatingOpacityPercent(context)
private var layoutMetrics = KeyboardLayoutMetrics(
landscape = appliedOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE,
fontScale = appliedFontScale,
+ heightPercent = keyboardHeightPercent,
)
private var lastTextMode = KeyboardMode.PINYIN_26
private var preferredChineseMode = ImeSettingsRepository.loadPreferredChineseMode(context)
@@ -454,6 +472,7 @@ open class ImeKeyboardView(
previousPanel = { panelBackStack.lastOrNull() },
onBack = ::closePanelToKeyboard,
onFeedback = ::feedback,
+ standalone = standalonePanel,
)
}
private val panelRenderer: ImePanelRenderer by lazy {
@@ -558,6 +577,10 @@ open class ImeKeyboardView(
currentSkinRadius = { skinRadius },
currentSkinFontSize = { skinFontSize },
currentSkinColor = { skinPrimaryColor },
+ currentHandedness = { keyboardHandedness },
+ currentKeyboardHeightPercent = { keyboardHeightPercent },
+ currentFloatingWidthPercent = { floatingWidthPercent },
+ currentFloatingOpacityPercent = { floatingOpacityPercent },
onThemeSelected = ::setTheme,
onAppearanceSelected = { selected ->
setAppearance(selected)
@@ -577,7 +600,11 @@ open class ImeKeyboardView(
)
applyTheme()
},
+ onHandednessChanged = ::setHandedness,
+ onKeyboardHeightChanged = ::setKeyboardHeightPercent,
+ onFloatingStyleChanged = ::setFloatingStyle,
onShowFuzzySettings = { showPanel(Panel.FUZZY_SETTINGS) },
+ onOpenAboutData = listener::onOpenAboutData,
onFeedback = ::feedback,
applyTheme = ::applyTheme,
onHierarchyRebuilt = ::onViewHierarchyRebuilt,
@@ -803,6 +830,7 @@ open class ImeKeyboardView(
layoutMetrics = KeyboardLayoutMetrics(
landscape = appliedOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE,
fontScale = appliedFontScale,
+ heightPercent = keyboardHeightPercent,
)
if (!geometryChanged) return
// Do not yank the user out of an open panel.
@@ -923,6 +951,31 @@ open class ImeKeyboardView(
*/
private fun updateResponsiveGeometry(measuredWidthPx: Int) {
if (measuredWidthPx <= 0) return
+
+ val dockWidthPx =
+ if (floatingKeyboardController.enabled || keyboardHandedness == ImeHandedness.STANDARD) {
+ measuredWidthPx
+ } else {
+ (measuredWidthPx * 0.82f).toInt()
+ .coerceAtLeast(dp(280))
+ .coerceAtMost(measuredWidthPx)
+ }
+
+ (mainDock.layoutParams as? FrameLayout.LayoutParams)?.let { params ->
+ if (dockWidthPx == measuredWidthPx) {
+ params.width = FrameLayout.LayoutParams.MATCH_PARENT
+ params.gravity = Gravity.TOP
+ } else {
+ params.width = dockWidthPx
+ params.gravity = Gravity.TOP or if (keyboardHandedness == ImeHandedness.LEFT) {
+ Gravity.START
+ } else {
+ Gravity.END
+ }
+ }
+ mainDock.layoutParams = params
+ }
+
if (floatingKeyboardController.enabled) {
// A configuration pass can briefly report the physical display
// width before WindowManager applies the floating window bounds.
@@ -948,7 +1001,7 @@ open class ImeKeyboardView(
}
val minimumInset = dp(5)
val maxWidth = dp(maxContentWidthDp)
- contentInsetPx = maxOf(minimumInset, (measuredWidthPx - maxWidth) / 2)
+ contentInsetPx = maxOf(minimumInset, (dockWidthPx - maxWidth) / 2)
keyboardBody.setPadding(
contentInsetPx,
dp(6),
@@ -956,7 +1009,7 @@ open class ImeKeyboardView(
dp(16),
)
keyboardBody.findViewWithTag("key-row-secondary")?.let { row ->
- val rowWidth = ((measuredWidthPx - contentInsetPx * 2) * 0.9f).toInt()
+ val rowWidth = ((dockWidthPx - contentInsetPx * 2) * 0.9f).toInt()
val params = row.layoutParams as? LinearLayout.LayoutParams
if (params != null) {
params.gravity = Gravity.CENTER_HORIZONTAL
@@ -994,6 +1047,8 @@ open class ImeKeyboardView(
updateTopZone(composition.text?.isNotEmpty() == true)
listener.onCandidateExpanded(open)
},
+ onUndoClear = { listener.onUndoClear() },
+ onUndoClearExpired = listener::onUndoClearExpired,
)
candidateBarController = CandidateBarController(
context = context,
@@ -1011,6 +1066,11 @@ open class ImeKeyboardView(
}.apply {
allowTwoLineLabel()
contentDescription = "候选:$candidate"
+ setOnLongClickListener {
+ feedback()
+ listener.onCandidateLongPressed(candidate)
+ true
+ }
}
},
createEmptyLabel = { title("暂无候选", small = true) },
@@ -1025,6 +1085,7 @@ open class ImeKeyboardView(
statefulBackground = ::statefulRounded,
onFeedback = ::feedback,
onCandidateSelected = listener::onCandidateSelected,
+ onCandidateLongPressed = listener::onCandidateLongPressed,
)
mainDock.addView(
topZone,
@@ -1207,6 +1268,9 @@ open class ImeKeyboardView(
}
stopVoiceIfActive()
panel = Panel.NONE
+ if (renderedMode != mode) {
+ renderModeBody()
+ }
expandedPanel.animate().cancel()
expandedPanel.visibility = View.GONE
mainDock.animate().cancel()
@@ -1317,7 +1381,7 @@ open class ImeKeyboardView(
val available = !passwordField
clipboardButton.isEnabled = available
clipboardButton.isClickable = available
- clipboardButton.alpha = if (available) 1f else 0.38f
+ clipboardButton.alpha = if (available) 1f else ImeSurfacePolicy.DISABLED_ALPHA
clipboardButton.contentDescription = if (available) {
"剪贴板"
} else {
@@ -1391,6 +1455,45 @@ open class ImeKeyboardView(
* several times. Keep the individual setters for user actions, but use
* this atomic boundary whenever a persisted snapshot is loaded.
*/
+ private fun setHandedness(next: ImeHandedness) {
+ if (keyboardHandedness == next) return
+ keyboardHandedness = next
+ ImeSettingsRepository.saveHandedness(context, next)
+ listener.onHandednessChanged(next)
+ updateResponsiveGeometry(width)
+ }
+
+ private fun setFloatingStyle(widthPercent: Int, opacityPercent: Int) {
+ val width = widthPercent.coerceIn(72, 96)
+ val opacity = opacityPercent.coerceIn(82, 100)
+ if (floatingWidthPercent == width && floatingOpacityPercent == opacity) return
+ floatingWidthPercent = width
+ floatingOpacityPercent = opacity
+ ImeSettingsRepository.saveFloatingWidthPercent(context, width)
+ ImeSettingsRepository.saveFloatingOpacityPercent(context, opacity)
+ listener.onFloatingStyleChanged(width, opacity)
+ }
+
+ private fun setKeyboardHeightPercent(percent: Int) {
+ val bounded = percent.coerceIn(92, 120)
+ if (keyboardHeightPercent == bounded) return
+ keyboardHeightPercent = bounded
+ ImeSettingsRepository.saveKeyboardHeightPercent(context, bounded)
+ listener.onKeyboardHeightChanged(bounded)
+ renderedMode = null
+ layoutMetrics = KeyboardLayoutMetrics(
+ landscape = appliedOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE,
+ fontScale = appliedFontScale,
+ heightPercent = keyboardHeightPercent,
+ )
+ applyDynamicHeights()
+ if (!standalonePanel && panel == Panel.NONE) {
+ renderModeBody()
+ } else {
+ requestLayout()
+ }
+ }
+
internal fun applyPersistedSettings(
newTheme: ImeTheme,
newAppearance: ImeAppearance,
@@ -1404,6 +1507,15 @@ open class ImeKeyboardView(
primaryColor: String,
) {
val normalizedColor = AccentPalette.normalize(primaryColor)
+ val persistedHandedness = ImeSettingsRepository.loadHandedness(context)
+ val persistedHeight = ImeSettingsRepository.loadKeyboardHeightPercent(context)
+ val persistedFloatingWidth = ImeSettingsRepository.loadFloatingWidthPercent(context)
+ val persistedFloatingOpacity = ImeSettingsRepository.loadFloatingOpacityPercent(context)
+ val floatingStyleChanged =
+ floatingWidthPercent != persistedFloatingWidth ||
+ floatingOpacityPercent != persistedFloatingOpacity
+ val heightChanged = keyboardHeightPercent != persistedHeight
+ val geometryChanged = keyboardHandedness != persistedHandedness || heightChanged
val visualChanged = theme != newTheme ||
appearance != newAppearance ||
skinOpacity != opacity ||
@@ -1421,9 +1533,52 @@ open class ImeKeyboardView(
skinRadius = radius
skinFontSize = fontSize
skinPrimaryColor = normalizedColor
+ if (geometryChanged) {
+ keyboardHandedness = persistedHandedness
+ keyboardHeightPercent = persistedHeight
+ if (heightChanged) renderedMode = null
+ layoutMetrics = KeyboardLayoutMetrics(
+ landscape = appliedOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE,
+ fontScale = appliedFontScale,
+ heightPercent = keyboardHeightPercent,
+ )
+ applyDynamicHeights()
+ if (!standalonePanel && panel == Panel.NONE) {
+ renderModeBody()
+ } else {
+ requestLayout()
+ }
+ updateResponsiveGeometry(width)
+ }
+ if (floatingStyleChanged) {
+ floatingWidthPercent = persistedFloatingWidth
+ floatingOpacityPercent = persistedFloatingOpacity
+ listener.onFloatingStyleChanged(
+ floatingWidthPercent,
+ floatingOpacityPercent,
+ )
+ }
if (visualChanged) applyTheme()
}
+ fun isCandidateInteractionActive(): Boolean =
+ ::candidateBarController.isInitialized &&
+ candidateBarController.isInteractionActive()
+
+ fun confirmCandidateDeletion(candidate: String, onConfirm: () -> Unit) {
+ if (::candidateBarController.isInitialized) {
+ candidateBarController.confirmCandidateDeletion(candidate, onConfirm)
+ }
+ }
+
+ fun showClearUndo() {
+ if (::topZone.isInitialized) topZone.showUndoClear()
+ }
+
+ fun hideClearUndo(discardSnapshot: Boolean = false) {
+ if (::topZone.isInitialized) topZone.hideUndoClear(discardSnapshot)
+ }
+
fun setAppearance(newAppearance: ImeAppearance) {
if (appearance == newAppearance) return
appearance = newAppearance
@@ -1706,6 +1861,7 @@ open class ImeKeyboardView(
composition = composition,
onCommit = listener::onCharacter,
onFeedback = ::feedback,
+ onRailChanged = ::applyThemeToSubtree,
).also { nineKeySymbolRailController = it }
}
@@ -1759,8 +1915,9 @@ open class ImeKeyboardView(
MotionEvent.ACTION_MOVE -> {
val index = event.findPointerIndex(spaceVoiceGestureController.pointerId)
if (index < 0) return handled
+ val pointerX = event.rawX + event.getX(index) - event.x
val pointerY = event.rawY + event.getY(index) - event.y
- spaceVoiceGestureController.move(pointerY)
+ spaceVoiceGestureController.move(pointerX, pointerY)
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_POINTER_UP -> {
if (event.actionMasked == MotionEvent.ACTION_POINTER_UP &&
@@ -1899,6 +2056,7 @@ open class ImeKeyboardView(
.putExtra(QuickPhraseEditActivity.EXTRA_ID, phrase?.id ?: 0L)
.putExtra(QuickPhraseEditActivity.EXTRA_CATEGORY, phrase?.category.orEmpty())
.putExtra(QuickPhraseEditActivity.EXTRA_TEXT, phrase?.text.orEmpty())
+ .putExtra(QuickPhraseEditActivity.EXTRA_INPUT_CODE, phrase?.inputCode.orEmpty())
context.startActivity(intent)
}
@@ -1916,7 +2074,7 @@ open class ImeKeyboardView(
private fun sectionTitle(textValue: String): TextView = TextView(context).apply {
text = textValue
- textSize = 12f
+ textSize = ImeTypographyTokens.TITLE_SP
includeFontPadding = false
setPadding(dp(4), dp(2), 0, dp(8))
tag = "panel-section-title"
@@ -2078,12 +2236,18 @@ open class ImeKeyboardView(
preferredSuffix: String? = null,
cursorPosition: Int? = null,
) {
+ val resolveStartedAt = SystemClock.elapsedRealtimeNanos()
val resolution = requireCandidateProvider().resolveNineKey(
digits = digits,
segmentPrefix = lastNineSegmentPrefix,
preferredSuffix = preferredSuffix,
fuzzy = fuzzyEnabled,
)
+ NineKeyPerformanceTrace.recordResolve(
+ digitLength = digits.length,
+ elapsedNs = SystemClock.elapsedRealtimeNanos() - resolveStartedAt,
+ threadName = Thread.currentThread().name,
+ )
val preview = resolution.preview
val pinyinPaths = resolution.pinyinPaths
val candidates = resolution.candidates
@@ -2392,7 +2556,11 @@ open class ImeKeyboardView(
text = text,
secondary = if (func) null else secondary,
iconRes = iconRes,
- mainTextSize = mainTextSizeOverride ?: (if (func) 15f else 20f),
+ mainTextSize = mainTextSizeOverride ?: if (func) {
+ ImeTypographyTokens.BODY_SP
+ } else {
+ ImeTypographyTokens.KEY_LETTER_SP
+ },
fitMainText = func || text.length > 2,
).apply {
tag = "key:$text"
@@ -2453,7 +2621,7 @@ open class ImeKeyboardView(
private fun title(text: String, small: Boolean = false) = TextView(context).apply {
this.text = text
- textSize = if (small) 12f else 16f
+ textSize = if (small) ImeTypographyTokens.BODY_SP else ImeTypographyTokens.TITLE_SP
setPadding(0, 0, 0, dp(4))
}
@@ -2480,15 +2648,15 @@ open class ImeKeyboardView(
protected fun applyTheme() {
val t = currentThemeTokens()
setBackgroundColor(t.keyboardBackground)
- mainDock.setBackgroundColor(t.expandedBackground)
+ mainDock.setBackgroundColor(t.keyboardBackground)
keyboardBody.setBackgroundColor(t.keyboardBackground)
topZone.setBackgroundColor(t.toolbarBackground)
expandedPanel.setBackgroundColor(t.expandedBackground)
candidateOverlay.setBackgroundColor(t.expandedBackground)
themeApplier.apply(this, t)
composition.setTextColor(t.keySecondaryText)
- topZone.candidateExpandButton.setTextColor(t.keySecondaryText)
- topZone.candidateEmojiButton.setTextColor(t.keySecondaryText)
+ topZone.candidateExpandButton.imageTintList = android.content.res.ColorStateList.valueOf(t.keySecondaryText)
+ topZone.candidateEmojiButton.imageTintList = android.content.res.ColorStateList.valueOf(t.keySecondaryText)
floatingKeyboardController.applyTheme(t)
inlineVoicePresenter.refreshPalette()
}
diff --git a/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt b/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt
index aaabe335..221efa85 100644
--- a/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt
+++ b/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt
@@ -2,6 +2,7 @@ package llc.slacker.openime
import android.content.Context
import android.content.Intent
+import android.graphics.Typeface
import android.os.Build
import android.view.Gravity
import android.view.View
@@ -91,7 +92,7 @@ internal class ImePanelRenderer(
val row = LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL }
chunk.forEach { (modeValue, label) ->
row.addView(
- createKey(label, true, 13f) { onModeSelected(modeValue) }.apply {
+ createKey(label, true, ImeTypographyTokens.BODY_SP) { onModeSelected(modeValue) }.apply {
val selected = currentMode() == modeValue
tag = if (selected) "tab-active" else "keyboard-choice"
contentDescription = "$label,${if (selected) "已选中" else "未选中"}"
@@ -253,7 +254,7 @@ internal class ImePanelRenderer(
).forEach { (button, label) ->
button ?: return@forEach
button.isEnabled = hasStrokes
- button.alpha = if (hasStrokes) 1f else 0.42f
+ button.alpha = if (hasStrokes) 1f else ImeSurfacePolicy.DISABLED_ALPHA
button.contentDescription = if (hasStrokes) label else "$label(暂无笔画)"
if (Build.VERSION.SDK_INT >= 30) {
button.stateDescription = if (hasStrokes) "可用" else "不可用"
@@ -274,7 +275,7 @@ internal class ImePanelRenderer(
is HandwritingResult.Success -> {
result.candidates.forEach { candidate ->
candidateRow.addView(
- createKey(candidate, false, 15f) { onCharacter(candidate) },
+ createKey(candidate, false, ImeTypographyTokens.TITLE_SP) { onCharacter(candidate) },
wrapParams(),
)
}
@@ -295,8 +296,8 @@ internal class ImePanelRenderer(
val actions = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
}
- undoButton = createKey("撤销", true, 13f) { pad.undo() }
- clearButton = createKey("清空", true, 13f) { pad.clear() }
+ undoButton = createKey("撤销", true, ImeTypographyTokens.BODY_SP) { pad.undo() }
+ clearButton = createKey("清空", true, ImeTypographyTokens.BODY_SP) { pad.clear() }
actions.addView(
undoButton,
LinearLayout.LayoutParams(0, toPx(ImeGeometryTokens.TOUCH_TARGET_DP), 1f).apply { marginEnd = toPx(6) },
@@ -306,7 +307,7 @@ internal class ImePanelRenderer(
LinearLayout.LayoutParams(0, toPx(ImeGeometryTokens.TOUCH_TARGET_DP), 1f).apply { marginEnd = toPx(6) },
)
actions.addView(
- createKey("空格", true, 13f, onSpace),
+ createKey("空格", true, ImeTypographyTokens.BODY_SP, onSpace),
LinearLayout.LayoutParams(0, toPx(ImeGeometryTokens.TOUCH_TARGET_DP), 1f),
)
refreshStrokeActions(false)
@@ -442,7 +443,7 @@ internal class ImePanelRenderer(
if (symbolCategory == "自定义") {
body.addView(
- createPanelButton("管理自定义符号", 12f, true).apply {
+ createPanelButton("管理自定义符号", ImeTypographyTokens.BODY_SP, true).apply {
contentDescription = "管理自定义符号"
setOnClickListener {
onFeedback()
@@ -482,7 +483,7 @@ internal class ImePanelRenderer(
createKey(
symbol,
false,
- if (symbol.length > 2) 12f else 17f,
+ ImeTypographyTokens.BODY_SP,
) { onSymbolSelected(symbol) },
gridCellParams(48, 6, 6),
)
@@ -607,6 +608,7 @@ internal class ImePanelRenderer(
minimumHeight = toPx(ImeGeometryTokens.TOUCH_TARGET_DP)
setPadding(toPx(10), 0, toPx(10), 0)
tag = if (active) "tab-active" else "panel-tab"
+ typeface = if (active) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
contentDescription = "$label,${if (active) "已选中" else "未选中"}"
if (Build.VERSION.SDK_INT >= 30) {
stateDescription = if (active) "已选中" else "未选中"
diff --git a/app/src/main/java/llc/slacker/openime/ImeSettingsActivity.kt b/app/src/main/java/llc/slacker/openime/ImeSettingsActivity.kt
index 40a07e4c..edece6d3 100644
--- a/app/src/main/java/llc/slacker/openime/ImeSettingsActivity.kt
+++ b/app/src/main/java/llc/slacker/openime/ImeSettingsActivity.kt
@@ -1,6 +1,7 @@
package llc.slacker.openime
import android.app.Activity
+import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
@@ -196,4 +197,21 @@ class ImeSettingsActivity : Activity(), ImeKeyboardView.Listener {
refreshWindowChrome()
refreshLiveIme()
}
+ override fun onHandednessChanged(handedness: ImeHandedness) {
+ ImeSettingsRepository.saveHandedness(this, handedness)
+ refreshLiveIme()
+ }
+ override fun onKeyboardHeightChanged(percent: Int) {
+ ImeSettingsRepository.saveKeyboardHeightPercent(this, percent)
+ refreshLiveIme()
+ }
+ override fun onFloatingStyleChanged(widthPercent: Int, opacityPercent: Int) {
+ ImeSettingsRepository.saveFloatingWidthPercent(this, widthPercent)
+ ImeSettingsRepository.saveFloatingOpacityPercent(this, opacityPercent)
+ refreshLiveIme()
+ }
+
+ override fun onOpenAboutData() {
+ startActivity(Intent(this, AboutDataActivity::class.java))
+ }
}
diff --git a/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt b/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt
index 49e13220..d407ca7f 100644
--- a/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt
+++ b/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt
@@ -2,6 +2,12 @@ package llc.slacker.openime
import android.content.Context
+enum class ImeHandedness(val label: String) {
+ STANDARD("标准"),
+ LEFT("左手"),
+ RIGHT("右手"),
+}
+
/** Lightweight persistent IME settings. */
object ImeSettingsRepository {
@@ -17,6 +23,10 @@ object ImeSettingsRepository {
private const val KEY_SKIN_FONT = "skin_font"
private const val KEY_SKIN_COLOR = "skin_color"
private const val KEY_PREFERRED_CHINESE_MODE = "preferred_chinese_mode"
+ private const val KEY_HANDEDNESS = "handedness"
+ private const val KEY_KEYBOARD_HEIGHT = "keyboard_height_percent"
+ private const val KEY_FLOATING_WIDTH = "floating_width_percent"
+ private const val KEY_FLOATING_OPACITY = "floating_opacity_percent"
/**
* The user's preferred Chinese layout (26-key vs 9-key). Only PINYIN_26 and
@@ -38,19 +48,63 @@ object ImeSettingsRepository {
.edit().putString(KEY_PREFERRED_CHINESE_MODE, mode.name).apply()
}
- /**
- * Every ImeTheme ships a complete token set in ImeDesignTokens, but the
- * getter used to hard-code IOS, which made four finished skins unreachable
- * and made a persisted choice impossible to restore.
- */
- fun loadTheme(context: Context): ImeTheme =
+ fun loadHandedness(context: Context): ImeHandedness =
runCatching {
- ImeTheme.valueOf(
+ ImeHandedness.valueOf(
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
- .getString(KEY_THEME, ImeTheme.IOS.name)
- ?: ImeTheme.IOS.name,
+ .getString(KEY_HANDEDNESS, ImeHandedness.STANDARD.name)
+ ?: ImeHandedness.STANDARD.name,
)
- }.getOrDefault(ImeTheme.IOS)
+ }.getOrDefault(ImeHandedness.STANDARD)
+
+ fun saveHandedness(context: Context, handedness: ImeHandedness) {
+ context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
+ .edit().putString(KEY_HANDEDNESS, handedness.name).apply()
+ }
+
+ fun loadKeyboardHeightPercent(context: Context): Int =
+ context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
+ .getInt(KEY_KEYBOARD_HEIGHT, 100)
+ .coerceIn(92, 120)
+
+ fun saveKeyboardHeightPercent(context: Context, percent: Int) {
+ context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
+ .edit().putInt(KEY_KEYBOARD_HEIGHT, percent.coerceIn(92, 120)).apply()
+ }
+
+ fun loadFloatingWidthPercent(context: Context): Int =
+ context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
+ .getInt(KEY_FLOATING_WIDTH, 88)
+ .coerceIn(72, 96)
+
+ fun saveFloatingWidthPercent(context: Context, percent: Int) {
+ context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
+ .edit().putInt(KEY_FLOATING_WIDTH, percent.coerceIn(72, 96)).apply()
+ }
+
+ fun loadFloatingOpacityPercent(context: Context): Int =
+ context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
+ .getInt(KEY_FLOATING_OPACITY, 100)
+ .coerceIn(82, 100)
+
+ fun saveFloatingOpacityPercent(context: Context, percent: Int) {
+ context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
+ .edit().putInt(KEY_FLOATING_OPACITY, percent.coerceIn(82, 100)).apply()
+ }
+
+ internal fun parseTheme(value: String?): ImeTheme =
+ runCatching { ImeTheme.valueOf(value ?: ImeTheme.IOS.name) }
+ .getOrDefault(ImeTheme.IOS)
+
+ /**
+ * Historical theme names can still exist in SharedPreferences after an
+ * upgrade. They intentionally fall back to the only supported skin.
+ */
+ fun loadTheme(context: Context): ImeTheme =
+ parseTheme(
+ context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
+ .getString(KEY_THEME, ImeTheme.IOS.name),
+ )
fun saveTheme(context: Context, theme: ImeTheme) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
@@ -133,38 +187,3 @@ object ImeSettingsRepository {
.apply()
}
}
-
-
-object AccentPalette {
- const val DEFAULT = "#1D9BF0"
- val presets = listOf(
- "#1D9BF0" to "蓝色",
- "#FFD400" to "黄色",
- "#F91880" to "粉色",
- "#7856FF" to "紫色",
- "#FF7A00" to "橙色",
- "#00BA7C" to "绿色",
- "#00C2D7" to "青色",
- "#38BDF8" to "天蓝",
- "#5865F2" to "靛蓝",
- "#9B5DE5" to "深紫",
- "#E94FB8" to "洋红",
- "#F4212E" to "红色",
- "#FF5A5F" to "珊瑚红",
- "#F59E0B" to "琥珀",
- "#84CC16" to "青柠",
- "#22C55E" to "翠绿",
- "#10CFA0" to "薄荷",
- "#14B8A6" to "蓝绿",
- )
-
- fun parse(value: String?): Int = runCatching {
- android.graphics.Color.parseColor(normalize(value))
- }.getOrDefault(android.graphics.Color.parseColor(DEFAULT))
-
- fun normalize(value: String?): String {
- val raw = value.orEmpty().trim()
- val hex = if (raw.startsWith("#")) raw else "#$raw"
- return if (hex.matches(Regex("#?[0-9a-fA-F]{6}"))) hex.uppercase() else DEFAULT
- }
-}
diff --git a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt
index 6164f2b1..a009fd97 100644
--- a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt
+++ b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt
@@ -2,6 +2,8 @@ package llc.slacker.openime
import android.content.res.ColorStateList
import android.graphics.Color
+import android.graphics.drawable.InsetDrawable
+import android.graphics.drawable.LayerDrawable
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
@@ -83,14 +85,20 @@ internal class ImeThemeApplier(
else -> t.keyBackground
}
val pressedColor = when {
- primary -> ImeDrawableFactory.dim(color, 0.88f)
- side -> ImeDrawableFactory.dim(t.sideKeyBackground, 0.88f)
- function -> ImeDrawableFactory.dim(t.functionKeyBackground, 0.88f)
- else -> t.keyPressedBackground
+ primary -> ImeSurfacePolicy.primaryPressed(t)
+ side -> ImeSurfacePolicy.pressedSurface(t.sideKeyBackground, t)
+ function -> ImeSurfacePolicy.pressedSurface(t.functionKeyBackground, t)
+ else -> ImeSurfacePolicy.pressedSurface(color, t)
}
view.applyMainTextScale(keyMainTextScale())
- view.background = statefulRounded(color, pressedColor, skinRadiusPx())
+ val keyFace = statefulRounded(color, pressedColor, skinRadiusPx())
+ val keyEdge = ImeDrawableFactory.rounded(pressedColor, skinRadiusPx())
+ val keyCap = LayerDrawable(arrayOf(keyEdge, keyFace)).apply {
+ setLayerInset(1, 0, 0, 0, toPx(1).coerceAtLeast(1))
+ }
+ val halfGap = toPx(ImeGeometryTokens.KEY_GAP_DP) / 2
+ view.background = InsetDrawable(keyCap, halfGap, halfGap, halfGap, halfGap)
view.background?.alpha =
skinOpacity().coerceIn(70, 100) * 255 / 100
view.elevation = 0f
@@ -118,23 +126,24 @@ internal class ImeThemeApplier(
private fun applyLinearLayout(view: LinearLayout, t: ImeTheme.Tokens) {
when (view.tag) {
"candidate-first-row" -> {
+ val selected = ImeSurfacePolicy.selectedSurface(t)
view.background = statefulRounded(
- t.keyBackground,
- t.keyPressedBackground,
+ selected,
+ ImeSurfacePolicy.pressedSurface(selected, t),
toPx(ImeGeometryTokens.KEY_RADIUS_DP),
)
}
"candidate-row" -> {
view.background = statefulRounded(
Color.TRANSPARENT,
- t.keyPressedBackground,
+ ImeSurfacePolicy.pressedSurface(t.candidateBackground, t),
toPx(ImeGeometryTokens.KEY_RADIUS_DP),
)
}
"setting-row" -> if (view.isClickable) {
view.background = statefulRounded(
Color.TRANSPARENT,
- t.keyPressedBackground,
+ ImeSurfacePolicy.pressedSurface(t.toolCardBackground, t),
toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
)
}
@@ -150,18 +159,32 @@ internal class ImeThemeApplier(
view.background = ImeDrawableFactory.rounded(
t.toolCardBackground,
toPx(ImeGeometryTokens.CARD_RADIUS_DP),
+ ImeSurfacePolicy.divider(t),
+ toPx(1).coerceAtLeast(1),
)
}
"clip-card" -> {
view.background = ImeDrawableFactory.rounded(
t.toolCardBackground,
toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
+ ImeSurfacePolicy.divider(t),
+ toPx(1).coerceAtLeast(1),
)
}
"panel-head" -> {
view.background = ImeDrawableFactory.rounded(
t.panelHeadBackground,
toPx(ImeGeometryTokens.CARD_RADIUS_DP),
+ ImeSurfacePolicy.divider(t),
+ toPx(1).coerceAtLeast(1),
+ )
+ }
+ "toolbar-row" -> view.setBackgroundColor(t.toolbarBackground)
+ "compose-zone" -> view.setBackgroundColor(t.candidateBackground)
+ "candidate-field" -> {
+ view.background = ImeDrawableFactory.rounded(
+ t.candidateBackground,
+ toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
)
}
else -> {
@@ -171,7 +194,7 @@ internal class ImeThemeApplier(
) {
view.background = statefulRounded(
t.toolCardBackground,
- t.keyPressedBackground,
+ ImeSurfacePolicy.pressedSurface(t.toolCardBackground, t),
toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
)
}
@@ -185,7 +208,7 @@ internal class ImeThemeApplier(
) {
view.background = statefulRounded(
t.toolCardBackground,
- t.keyPressedBackground,
+ ImeSurfacePolicy.pressedSurface(t.toolCardBackground, t),
toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
)
}
@@ -202,36 +225,26 @@ internal class ImeThemeApplier(
private fun applyImageView(view: ImageView, t: ImeTheme.Tokens) {
when {
+ view.tag == "candidate-emoji" || view.tag == "candidate-expand" -> {
+ view.imageTintList = ColorStateList.valueOf(t.keySecondaryText)
+ view.background = statefulRounded(
+ Color.TRANSPARENT,
+ ImeSurfacePolicy.pressedSurface(t.candidateBackground, t),
+ toPx(ImeGeometryTokens.KEY_RADIUS_DP),
+ )
+ }
view.tag == "setting-icon" -> {
- val icon = t.primary
- view.imageTintList = ColorStateList.valueOf(icon)
- val dark =
- ImeDrawableFactory.contrastText(t.keyboardBackground) ==
- Color.WHITE
+ view.imageTintList = ColorStateList.valueOf(t.primary)
view.background = ImeDrawableFactory.rounded(
- if (dark) {
- Color.argb(
- 42,
- Color.red(icon),
- Color.green(icon),
- Color.blue(icon),
- )
- } else {
- Color.argb(
- 24,
- Color.red(icon),
- Color.green(icon),
- Color.blue(icon),
- )
- },
+ ImeSurfacePolicy.subtleAccentSurface(t),
toPx(ImeGeometryTokens.KEY_RADIUS_DP),
)
}
view.tag == "key-panel-back" -> {
- view.imageTintList = ColorStateList.valueOf(t.keyText)
+ view.imageTintList = ColorStateList.valueOf(t.keySecondaryText)
view.background = statefulRounded(
t.panelHeadBackground,
- ImeDrawableFactory.dim(t.panelHeadBackground),
+ ImeSurfacePolicy.pressedSurface(t.panelHeadBackground, t),
toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
)
}
@@ -239,11 +252,11 @@ internal class ImeThemeApplier(
view.parent is LinearLayout &&
(view.parent as LinearLayout).tag == "toolbar-row"
) || hasAncestorTag(view, "tools-panel") -> {
- view.imageTintList = ColorStateList.valueOf(t.keyText)
+ view.imageTintList = ColorStateList.valueOf(t.keySecondaryText)
if (view.isClickable) {
view.background = statefulRounded(
Color.TRANSPARENT,
- t.keyPressedBackground,
+ ImeSurfacePolicy.pressedSurface(t.toolbarBackground, t),
toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
)
}
@@ -268,15 +281,30 @@ internal class ImeThemeApplier(
tag == "backspace-clear-hint" ->
view.setTextColor(t.keySecondaryText)
+ tag == "undo-clear-action" -> {
+ view.setTextColor(t.primary)
+ view.background = statefulRounded(
+ Color.TRANSPARENT,
+ ImeSurfacePolicy.subtleAccentSurface(t),
+ toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
+ )
+ }
+
tag == "candidate-first" ->
view.setTextColor(t.keyText)
tag == "candidate-word" ->
view.setTextColor(t.candidateText)
- tag == "panel-note" ->
+ tag == "panel-note" ||
+ tag == "setting-value" ||
+ tag == "setting-chevron" ||
+ tag == "panel-section-title" ->
view.setTextColor(t.keySecondaryText)
+ tag == "setting-label" ->
+ view.setTextColor(t.keyText)
+
tag == "panel-error" -> {
val error = t.destructive
view.setTextColor(error)
@@ -292,10 +320,11 @@ internal class ImeThemeApplier(
}
tag == "tab-active" -> {
- view.setTextColor(ImeDrawableFactory.contrastText(t.primary))
+ val selected = ImeSurfacePolicy.selectedSurface(t)
+ view.setTextColor(ImeSurfacePolicy.selectedText(t))
view.background = statefulRounded(
- t.primary,
- ImeDrawableFactory.dim(t.primary),
+ selected,
+ ImeSurfacePolicy.pressedSurface(selected, t),
toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
)
}
@@ -304,7 +333,7 @@ internal class ImeThemeApplier(
view.setTextColor(t.keySecondaryText)
view.background = statefulRounded(
t.panelHeadBackground,
- ImeDrawableFactory.dim(t.panelHeadBackground),
+ ImeSurfacePolicy.pressedSurface(t.panelHeadBackground, t),
toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
)
}
@@ -313,7 +342,7 @@ internal class ImeThemeApplier(
view.setTextColor(ImeDrawableFactory.contrastText(t.primary))
view.background = statefulRounded(
t.primary,
- ImeDrawableFactory.dim(t.primary),
+ ImeSurfacePolicy.primaryPressed(t),
toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
)
}
@@ -322,12 +351,11 @@ internal class ImeThemeApplier(
tag == "clipboard-refresh" ||
tag?.startsWith("clip-pin:") == true ||
tag?.startsWith("clip-use:") == true ||
- tag?.startsWith("phrase-edit:") == true ||
- tag?.startsWith("phrase-delete:") == true -> {
+ tag?.startsWith("phrase-edit:") == true -> {
view.setTextColor(t.keyText)
view.background = statefulRounded(
t.panelHeadBackground,
- ImeDrawableFactory.dim(t.panelHeadBackground),
+ ImeSurfacePolicy.pressedSurface(t.panelHeadBackground, t),
toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
)
}
@@ -336,18 +364,19 @@ internal class ImeThemeApplier(
view.setTextColor(t.keyText)
view.background = statefulRounded(
t.panelHeadBackground,
- ImeDrawableFactory.dim(t.panelHeadBackground),
+ ImeSurfacePolicy.pressedSurface(t.panelHeadBackground, t),
toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
)
}
- tag == "clipboard-retention-destructive" -> {
- view.setTextColor(
- ImeDrawableFactory.contrastText(t.destructive),
- )
+ tag == "clipboard-retention-destructive" ||
+ tag == "clipboard-clear-confirm" ||
+ tag?.startsWith("phrase-delete:") == true -> {
+ val destructiveSurface = ImeSurfacePolicy.destructiveSurface(t)
+ view.setTextColor(ImeSurfacePolicy.destructiveText(t))
view.background = statefulRounded(
- t.destructive,
- ImeDrawableFactory.dim(t.destructive, 0.86f),
+ destructiveSurface,
+ ImeSurfacePolicy.pressedSurface(destructiveSurface, t),
toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
)
}
@@ -357,16 +386,26 @@ internal class ImeThemeApplier(
view.setTextColor(t.keyText)
view.background = statefulRounded(
Color.TRANSPARENT,
- t.keyPressedBackground,
+ ImeSurfacePolicy.pressedSurface(t.sideKeyBackground, t),
+ toPx(ImeGeometryTokens.KEY_RADIUS_DP),
+ )
+ }
+
+ tag == "nine-pinyin-path-selected" -> {
+ val selected = ImeSurfacePolicy.selectedSurface(t)
+ view.setTextColor(ImeSurfacePolicy.selectedText(t))
+ view.background = statefulRounded(
+ selected,
+ ImeSurfacePolicy.pressedSurface(selected, t),
toPx(ImeGeometryTokens.KEY_RADIUS_DP),
)
}
tag == "nine-pinyin-path-filter" -> {
- view.setTextColor(ImeDrawableFactory.contrastText(t.primary))
+ view.setTextColor(t.sideKeyText)
view.background = statefulRounded(
- t.primary,
- ImeDrawableFactory.dim(t.primary, 0.86f),
+ Color.TRANSPARENT,
+ ImeSurfacePolicy.pressedSurface(t.sideKeyBackground, t),
toPx(ImeGeometryTokens.KEY_RADIUS_DP),
)
}
@@ -386,13 +425,13 @@ internal class ImeThemeApplier(
view.background = if (customSelected) {
statefulRounded(
t.primary,
- ImeDrawableFactory.dim(t.primary),
+ ImeSurfacePolicy.primaryPressed(t),
toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
)
} else {
statefulRounded(
t.panelHeadBackground,
- ImeDrawableFactory.dim(t.panelHeadBackground),
+ ImeSurfacePolicy.pressedSurface(t.panelHeadBackground, t),
toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
)
}
@@ -411,7 +450,7 @@ internal class ImeThemeApplier(
view.setTextColor(t.keyText)
view.background = statefulRounded(
t.panelHeadBackground,
- ImeDrawableFactory.dim(t.panelHeadBackground),
+ ImeSurfacePolicy.pressedSurface(t.panelHeadBackground, t),
toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
)
}
@@ -422,8 +461,8 @@ internal class ImeThemeApplier(
tag == "candidate-emoji" || tag == "candidate-expand" -> {
view.setTextColor(t.keySecondaryText)
view.background = statefulRounded(
- t.panelHeadBackground,
- ImeDrawableFactory.dim(t.panelHeadBackground),
+ Color.TRANSPARENT,
+ ImeSurfacePolicy.pressedSurface(t.candidateBackground, t),
toPx(ImeGeometryTokens.KEY_RADIUS_DP),
)
}
@@ -443,7 +482,7 @@ internal class ImeThemeApplier(
view.setTextColor(t.candidateText)
view.background = statefulRounded(
t.toolCardBackground,
- ImeDrawableFactory.dim(t.toolCardBackground),
+ ImeSurfacePolicy.pressedSurface(t.toolCardBackground, t),
toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
)
}
@@ -452,12 +491,12 @@ internal class ImeThemeApplier(
view.setTextColor(t.keySecondaryText)
tag == "voice-mic" -> {
- view.setTextColor(
- ImeDrawableFactory.contrastText(t.primary),
- )
+ val onAccent = ImeDrawableFactory.contrastText(t.primary)
+ view.setTextColor(onAccent)
+ view.compoundDrawableTintList = ColorStateList.valueOf(onAccent)
view.background = statefulRounded(
t.primary,
- ImeDrawableFactory.dim(t.primary, 0.88f),
+ ImeSurfacePolicy.primaryPressed(t),
toPx(ImeGeometryTokens.CONTROL_RADIUS_DP),
)
}
@@ -479,14 +518,14 @@ internal class ImeThemeApplier(
"emoji-cell" -> {
view.background = statefulRounded(
Color.TRANSPARENT,
- t.keyPressedBackground,
+ ImeSurfacePolicy.pressedSurface(t.sideKeyBackground, t),
toPx(ImeGeometryTokens.KEY_RADIUS_DP),
)
}
"accent-swatch" -> {
view.background = statefulRounded(
Color.TRANSPARENT,
- t.keyPressedBackground,
+ ImeSurfacePolicy.pressedSurface(t.sideKeyBackground, t),
toPx(ImeGeometryTokens.KEY_RADIUS_DP),
)
}
@@ -517,8 +556,20 @@ internal class ImeThemeApplier(
toPx(ImeGeometryTokens.PILL_RADIUS_DP),
)
}
+ "toggle-knob" -> {
+ val parent = view.parent as? FrameLayout
+ val seed = parent?.contentDescription?.toString()
+ ?.substringBefore(',')
+ .orEmpty()
+ val trackColor =
+ if (toggleState(seed)) t.primary else t.panelHeadBackground
+ view.background = ImeDrawableFactory.rounded(
+ ImeDrawableFactory.contrastText(trackColor),
+ toPx(ImeGeometryTokens.PILL_RADIUS_DP),
+ )
+ }
"setting-divider" ->
- view.setBackgroundColor(t.border)
+ view.setBackgroundColor(ImeSurfacePolicy.divider(t))
}
}
diff --git a/app/src/main/java/llc/slacker/openime/ImeTopZone.kt b/app/src/main/java/llc/slacker/openime/ImeTopZone.kt
index 3adc4f10..a7f5bd70 100644
--- a/app/src/main/java/llc/slacker/openime/ImeTopZone.kt
+++ b/app/src/main/java/llc/slacker/openime/ImeTopZone.kt
@@ -42,6 +42,8 @@ internal class ImeTopZone(
onHideKeyboard: () -> Unit,
onTools: () -> Unit,
onExpandCandidates: () -> Unit,
+ private val onUndoClear: () -> Unit,
+ private val onUndoClearExpired: () -> Unit,
) : LinearLayout(context) {
val toolbarRow = LinearLayout(context)
val composeZone = LinearLayout(context)
@@ -50,12 +52,19 @@ internal class ImeTopZone(
val candidateRow = LinearLayout(context)
val candidateScroll = HorizontalScrollView(context)
val associationRow = LinearLayout(context)
- val candidateExpandButton = TextView(context)
- val candidateEmojiButton = TextView(context)
+ val candidateExpandButton = ImageView(context)
+ val candidateEmojiButton = ImageView(context)
val voiceInlineZone = LinearLayout(context)
val voiceInlineIcon = ImageView(context)
val voiceInlineStatus = TextView(context)
val voiceInlineWaves = mutableListOf()
+ val undoClearAction = TextView(context)
+ private val hideUndoClearRunnable = Runnable {
+ if (undoClearAction.visibility == View.VISIBLE) {
+ undoClearAction.visibility = View.GONE
+ onUndoClearExpired()
+ }
+ }
init {
tag = "ime_toolbar"
@@ -112,6 +121,32 @@ internal class ImeTopZone(
1f,
).apply { marginStart = toPx(4) },
)
+ undoClearAction.apply {
+ tag = "undo-clear-action"
+ text = "撤销"
+ textSize = ImeTypographyTokens.BODY_SP
+ gravity = Gravity.CENTER
+ includeFontPadding = false
+ minWidth = toPx(ImeGeometryTokens.TOUCH_TARGET_DP)
+ minimumHeight = toPx(ImeGeometryTokens.TOUCH_TARGET_DP)
+ visibility = View.GONE
+ isClickable = true
+ isFocusable = true
+ contentDescription = "撤销清空"
+ setOnClickListener {
+ removeCallbacks(hideUndoClearRunnable)
+ visibility = View.GONE
+ onFeedback()
+ onUndoClear()
+ }
+ }
+ toolbarRow.addView(
+ undoClearAction,
+ LinearLayout.LayoutParams(
+ toPx(64),
+ toPx(ImeGeometryTokens.TOUCH_TARGET_DP),
+ ),
+ )
toolbarRow.addView(
toolbarIcon(R.drawable.ic_keyboard_hide, "收起键盘", "keyboard-hide", onHideKeyboard),
touchTargetParams(),
@@ -136,7 +171,7 @@ internal class ImeTopZone(
composition.apply {
tag = "pinyin-composition-editor"
contentDescription = "可编辑拼音预编辑"
- textSize = 13f
+ textSize = ImeTypographyTokens.BODY_SP
gravity = Gravity.CENTER_VERTICAL or Gravity.START
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
setSingleLine(true)
@@ -196,9 +231,8 @@ internal class ImeTopZone(
candidateEmojiButton.apply {
tag = "candidate-emoji"
- text = "☺"
- textSize = 17f
- gravity = Gravity.CENTER
+ setImageResource(R.drawable.ic_emoji)
+ scaleType = ImageView.ScaleType.CENTER_INSIDE
contentDescription = "表情"
setPadding(toPx(7), 0, toPx(7), 0)
isClickable = true
@@ -212,9 +246,8 @@ internal class ImeTopZone(
candidateExpandButton.apply {
tag = "candidate-expand"
- text = "⌄"
- textSize = 15f
- gravity = Gravity.CENTER
+ setImageResource(R.drawable.ic_chevron_down)
+ scaleType = ImageView.ScaleType.CENTER_INSIDE
contentDescription = "展开更多候选"
setPadding(toPx(7), 0, toPx(7), 0)
isClickable = true
@@ -270,7 +303,7 @@ internal class ImeTopZone(
voiceInlineStatus.apply {
tag = "voice-inline-status"
text = "正在聆听…"
- textSize = 14f
+ textSize = ImeTypographyTokens.BODY_SP
setTextColor(Color.WHITE)
includeFontPadding = false
maxLines = 1
@@ -321,6 +354,20 @@ internal class ImeTopZone(
)
}
+ fun showUndoClear() {
+ removeCallbacks(hideUndoClearRunnable)
+ undoClearAction.visibility = View.VISIBLE
+ undoClearAction.bringToFront()
+ postDelayed(hideUndoClearRunnable, CLEAR_UNDO_VISIBLE_MS)
+ }
+
+ fun hideUndoClear(discardSnapshot: Boolean = false) {
+ removeCallbacks(hideUndoClearRunnable)
+ val wasVisible = undoClearAction.visibility == View.VISIBLE
+ undoClearAction.visibility = View.GONE
+ if (discardSnapshot && wasVisible) onUndoClearExpired()
+ }
+
fun setContentInset(contentInsetPx: Int) {
toolbarRow.setPadding(contentInsetPx + toPx(10), 0, contentInsetPx + toPx(10), 0)
composition.setPadding(contentInsetPx + toPx(14), toPx(3), contentInsetPx + toPx(14), 0)
@@ -365,4 +412,8 @@ internal class ImeTopZone(
toPx(ImeGeometryTokens.TOUCH_TARGET_DP),
toPx(ImeGeometryTokens.TOUCH_TARGET_DP),
)
+
+ private companion object {
+ const val CLEAR_UNDO_VISIBLE_MS = 5_000L
+ }
}
diff --git a/app/src/main/java/llc/slacker/openime/InputConnectionGateway.kt b/app/src/main/java/llc/slacker/openime/InputConnectionGateway.kt
index f3e1bded..1ff15afb 100644
--- a/app/src/main/java/llc/slacker/openime/InputConnectionGateway.kt
+++ b/app/src/main/java/llc/slacker/openime/InputConnectionGateway.kt
@@ -42,6 +42,7 @@ class InputConnectionGateway(
private val context: Context?,
private val connection: () -> InputConnection?,
private val isPassword: () -> Boolean = { false },
+ private val nowMs: () -> Long = { System.nanoTime() / 1_000_000L },
) {
data class CursorSnapshot(
@@ -63,13 +64,44 @@ class InputConnectionGateway(
}
}
+ private data class ClearUndoSnapshot(
+ val text: String,
+ val selectionStart: Int,
+ val selectionEnd: Int,
+ val connection: InputConnection,
+ val expiresAtMs: Long,
+ )
+
+ private var clearUndoSnapshot: ClearUndoSnapshot? = null
+
+ private fun invalidateClearUndo() {
+ clearUndoSnapshot = null
+ }
+
+ fun discardClearUndo() {
+ invalidateClearUndo()
+ }
+
+ fun hasClearUndo(): Boolean {
+ if (isPassword()) return false
+ val snapshot = clearUndoSnapshot ?: return false
+ val current = connection() ?: return false
+ if (snapshot.connection !== current || nowMs() > snapshot.expiresAtMs) {
+ invalidateClearUndo()
+ return false
+ }
+ return true
+ }
+
fun commitText(text: String) {
if (text.isEmpty()) return
+ invalidateClearUndo()
connection()?.commitText(text, 1)
}
fun setComposingText(text: String) {
if (isPassword()) return
+ if (text.isNotEmpty()) invalidateClearUndo()
val ic = connection() ?: return
if (text.isEmpty()) {
ic.finishComposingText()
@@ -108,6 +140,7 @@ class InputConnectionGateway(
}
fun deleteBackwards() {
+ invalidateClearUndo()
val ic = connection() ?: return
if (deleteSelection()) return
@@ -133,6 +166,7 @@ class InputConnectionGateway(
/** Delete the active selection without falling back to one-character delete. */
fun deleteSelection(): Boolean {
+ invalidateClearUndo()
val ic = connection() ?: return false
if (isPassword()) {
if (knownSelectionStart >= 0 && knownSelectionEnd >= 0 && knownSelectionStart != knownSelectionEnd) {
@@ -188,6 +222,7 @@ class InputConnectionGateway(
}
fun deleteForwards() {
+ invalidateClearUndo()
val ic = connection() ?: return
if (deleteSelection()) return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -228,6 +263,7 @@ class InputConnectionGateway(
fun clearAllText(): Boolean {
if (isPassword()) return false
val ic = connection() ?: return false
+ clearUndoSnapshot = null
ic.beginBatchEdit()
return try {
val originalSelection = selectionBeforeDestructiveSelectAll(ic)
@@ -241,6 +277,7 @@ class InputConnectionGateway(
restoreSelectionAfterFailedClear(ic, originalSelection)
} else {
ic.finishComposingText()
+ rememberClearUndo(selected, originalSelection, ic)
}
return cleared
}
@@ -259,6 +296,7 @@ class InputConnectionGateway(
restoreSelectionAfterFailedClear(ic, originalSelection)
} else {
ic.finishComposingText()
+ rememberClearUndo(selectedWindow.text, originalSelection, ic)
}
return cleared
}
@@ -278,6 +316,7 @@ class InputConnectionGateway(
restoreSelectionAfterFailedClear(ic, originalSelection)
} else {
ic.finishComposingText()
+ rememberClearUndo(window.text, originalSelection, ic)
}
cleared
} finally {
@@ -285,11 +324,91 @@ class InputConnectionGateway(
}
}
+ /**
+ * Restore only the most recent successful full-document clear. Any normal
+ * edit invalidates the snapshot, and restoration is rejected unless the
+ * target document is still empty.
+ */
+ fun restoreLastClear(): Boolean {
+ if (isPassword()) return false
+ val snapshot = clearUndoSnapshot ?: return false
+ val ic = connection() ?: return false
+ if (snapshot.connection !== ic || nowMs() > snapshot.expiresAtMs) {
+ invalidateClearUndo()
+ return false
+ }
+
+ val window = extractedWindow(ic)
+ val documentEmpty = if (window?.isCompleteDocument == true) {
+ window.text.isEmpty()
+ } else {
+ val before = runCatching { ic.getTextBeforeCursor(1, 0)?.isEmpty() == true }
+ .getOrDefault(false)
+ val after = runCatching { ic.getTextAfterCursor(1, 0)?.isEmpty() == true }
+ .getOrDefault(false)
+ val selected = runCatching { ic.getSelectedText(0)?.isEmpty() != false }
+ .getOrDefault(false)
+ before && after && selected
+ }
+ if (!documentEmpty) {
+ invalidateClearUndo()
+ return false
+ }
+
+ ic.beginBatchEdit()
+ return try {
+ val committed = runCatching { ic.commitText(snapshot.text, 1) }.getOrDefault(false)
+ if (!committed) return false
+ ic.finishComposingText()
+ val start = snapshot.selectionStart.coerceIn(0, snapshot.text.length)
+ val end = snapshot.selectionEnd.coerceIn(0, snapshot.text.length)
+ runCatching { ic.setSelection(start, end) }
+ knownSelectionStart = start
+ knownSelectionEnd = end
+ invalidateClearUndo()
+ true
+ } finally {
+ ic.endBatchEdit()
+ }
+ }
+
+ private fun rememberClearUndo(
+ text: String,
+ selection: SelectionSnapshot?,
+ ic: InputConnection,
+ ) {
+ if (text.isEmpty()) return
+ val start: Int
+ val end: Int
+ when (selection) {
+ is SelectionSnapshot.Absolute -> {
+ start = selection.start
+ end = selection.end
+ }
+ is SelectionSnapshot.Relative -> {
+ start = selection.cursor
+ end = selection.cursor
+ }
+ null -> {
+ start = text.length
+ end = text.length
+ }
+ }
+ clearUndoSnapshot = ClearUndoSnapshot(
+ text = text,
+ selectionStart = start.coerceIn(0, text.length),
+ selectionEnd = end.coerceIn(0, text.length),
+ connection = ic,
+ expiresAtMs = nowMs() + CLEAR_UNDO_TIMEOUT_MS,
+ )
+ }
+
fun performEditorAction(action: Int) {
connection()?.performEditorAction(action)
}
fun sendKeyDownUp(keyCode: Int) {
+ invalidateClearUndo()
val ic = connection() ?: return
sendKeyDownUp(ic, keyCode)
}
@@ -495,14 +614,17 @@ class InputConnectionGateway(
clip.takeIf { it.itemCount > 0 }?.getItemAt(0)?.coerceToText(safeContext)?.toString().orEmpty()
}.getOrDefault("")
if (text.isEmpty()) return ""
+ invalidateClearUndo()
val committed = connection()?.commitText(text, 1) == true
if (!committed) return ""
onPasted(clip)
return text
}
- fun commitContent(info: InputContentInfo): Boolean =
- connection()?.commitContent(info, 0, null) == true
+ fun commitContent(info: InputContentInfo): Boolean {
+ invalidateClearUndo()
+ return connection()?.commitContent(info, 0, null) == true
+ }
private fun selectionSnapshot(): SelectionSnapshot? {
if (isPassword()) return null
@@ -605,5 +727,6 @@ class InputConnectionGateway(
private companion object {
const val FALLBACK_WINDOW_CHARS = 8_192
+ const val CLEAR_UNDO_TIMEOUT_MS = 5_000L
}
}
diff --git a/app/src/main/java/llc/slacker/openime/KeyPopupController.kt b/app/src/main/java/llc/slacker/openime/KeyPopupController.kt
index cfe832f5..f3e75237 100644
--- a/app/src/main/java/llc/slacker/openime/KeyPopupController.kt
+++ b/app/src/main/java/llc/slacker/openime/KeyPopupController.kt
@@ -27,9 +27,27 @@ internal class KeyPopupController(
private val feedback: () -> Unit,
private val onSymbolSelected: (String) -> Unit,
) {
+ private val previewPopup = TextView(host.context).apply {
+ visibility = View.GONE
+ includeFontPadding = false
+ maxLines = 1
+ ellipsize = TextUtils.TruncateAt.END
+ gravity = Gravity.CENTER
+ setPadding(dp(8), dp(6), dp(8), dp(6))
+ elevation = dp(2).toFloat()
+ }
private var popupView: View? = null
private var keepAfterKeyUp = false
+ init {
+ host.addView(
+ previewPopup,
+ FrameLayout.LayoutParams(1, 1).apply {
+ gravity = Gravity.TOP or Gravity.START
+ },
+ )
+ }
+
fun show(anchor: View, text: String) {
hide()
keepAfterKeyUp = false
@@ -45,23 +63,35 @@ internal class KeyPopupController(
val popupHeight = dp(
if (text == "清空") 36 else ImeGeometryTokens.KEY_POPUP_HEIGHT_DP,
)
- val popup = TextView(host.context).apply {
+ val popupBackground = if (text == "清空") t.destructive else t.keyBackground
+ previewPopup.apply {
this.text = text
- textSize = if (text.length > 1) 13f else 16f
- includeFontPadding = false
- maxLines = 1
- ellipsize = TextUtils.TruncateAt.END
- gravity = Gravity.CENTER
- setPadding(dp(8), dp(6), dp(8), dp(6))
- val popupBackground = if (text == "清空") t.destructive else t.keyBackground
+ textSize = if (text.length > 1) {
+ ImeTypographyTokens.BODY_SP
+ } else {
+ ImeTypographyTokens.DISPLAY_SP
+ }
setTextColor(if (text == "清空") contrastText(popupBackground) else t.keyText)
- background = rounded(popupBackground, dp(ImeGeometryTokens.CONTROL_RADIUS_DP))
- elevation = dp(2).toFloat()
+ background =
+ if (text == "清空") {
+ rounded(popupBackground, dp(ImeGeometryTokens.CONTROL_RADIUS_DP))
+ } else {
+ ImeDrawableFactory.rounded(
+ popupBackground,
+ dp(ImeGeometryTokens.CONTROL_RADIUS_DP),
+ ImeSurfacePolicy.divider(t),
+ dp(1).coerceAtLeast(1),
+ )
+ }
}
- placeAbove(anchor, popup, popupWidth, popupHeight)
- popupView = popup
- animateIn(popup, popupWidth, popupHeight)
+ positionAttachedPopup(anchor, previewPopup, popupWidth, popupHeight)
+ previewPopup.visibility = View.VISIBLE
+ previewPopup.alpha = 1f
+ previewPopup.scaleX = 1f
+ previewPopup.scaleY = 1f
+ previewPopup.bringToFront()
+ popupView = previewPopup
}
fun showChoices(anchor: View, choices: List) {
@@ -73,7 +103,12 @@ internal class KeyPopupController(
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER
setPadding(dp(5), dp(5), dp(5), dp(5))
- background = rounded(t.keyBackground, dp(ImeGeometryTokens.CONTROL_RADIUS_DP))
+ background = ImeDrawableFactory.rounded(
+ t.keyBackground,
+ dp(ImeGeometryTokens.CONTROL_RADIUS_DP),
+ ImeSurfacePolicy.divider(t),
+ dp(1).coerceAtLeast(1),
+ )
elevation = dp(2).toFloat()
contentDescription = "长按符号选择"
}
@@ -82,13 +117,13 @@ internal class KeyPopupController(
row.addView(
TextView(host.context).apply {
text = symbol
- textSize = 16f
+ textSize = ImeTypographyTokens.BODY_SP
includeFontPadding = false
gravity = Gravity.CENTER
setTextColor(t.keyText)
background = statefulRounded(
Color.TRANSPARENT,
- t.keyPressedBackground,
+ ImeSurfacePolicy.pressedSurface(t.keyBackground, t),
dp(ImeGeometryTokens.KEY_RADIUS_DP),
)
isClickable = true
@@ -133,15 +168,45 @@ internal class KeyPopupController(
}
fun hide() {
- popupView?.let {
- it.animate().cancel()
- host.removeView(it)
+ popupView?.let { popup ->
+ popup.animate().cancel()
+ if (popup === previewPopup) {
+ popup.visibility = View.GONE
+ } else {
+ host.removeView(popup)
+ }
}
popupView = null
keepAfterKeyUp = false
}
+ private fun positionAttachedPopup(
+ anchor: View,
+ popup: View,
+ popupWidth: Int,
+ popupHeight: Int,
+ ) {
+ val placement = placementAbove(anchor, popupWidth, popupHeight)
+ popup.layoutParams = FrameLayout.LayoutParams(popupWidth, popupHeight).apply {
+ gravity = Gravity.TOP or Gravity.START
+ leftMargin = placement.first
+ topMargin = placement.second
+ }
+ }
+
private fun placeAbove(anchor: View, popup: View, popupWidth: Int, popupHeight: Int) {
+ val placement = placementAbove(anchor, popupWidth, popupHeight)
+ host.addView(
+ popup,
+ FrameLayout.LayoutParams(popupWidth, popupHeight).apply {
+ gravity = Gravity.TOP or Gravity.START
+ leftMargin = placement.first
+ topMargin = placement.second
+ },
+ )
+ }
+
+ private fun placementAbove(anchor: View, popupWidth: Int, popupHeight: Int): Pair {
val anchorLocation = IntArray(2)
val rootLocation = IntArray(2)
anchor.getLocationOnScreen(anchorLocation)
@@ -156,15 +221,7 @@ internal class KeyPopupController(
val top = (
anchorTop - popupHeight - dp(ImeGeometryTokens.KEY_POPUP_VERTICAL_GAP_DP)
).coerceAtLeast(dp(4))
-
- host.addView(
- popup,
- FrameLayout.LayoutParams(popupWidth, popupHeight).apply {
- gravity = Gravity.TOP or Gravity.START
- leftMargin = left
- topMargin = top
- },
- )
+ return left to top
}
private fun animateIn(view: View, popupWidth: Int, popupHeight: Int) {
diff --git a/app/src/main/java/llc/slacker/openime/KeyboardGeometry.kt b/app/src/main/java/llc/slacker/openime/KeyboardGeometry.kt
index a94f03c6..34ea84b0 100644
--- a/app/src/main/java/llc/slacker/openime/KeyboardGeometry.kt
+++ b/app/src/main/java/llc/slacker/openime/KeyboardGeometry.kt
@@ -53,6 +53,7 @@ data class NormalizedBounds(
internal data class KeyboardLayoutMetrics(
val landscape: Boolean,
val fontScale: Float,
+ val heightPercent: Int = 100,
) {
val keyRowHeightDp: Int = run {
val base = if (landscape) {
@@ -63,7 +64,8 @@ internal data class KeyboardLayoutMetrics(
val fontGrow = ((fontScale - 1f).coerceAtLeast(0f) * 12f)
.toInt()
.coerceAtMost(12)
- base + fontGrow
+ val scaled = ((base + fontGrow) * heightPercent.coerceIn(92, 120) / 100f).toInt()
+ scaled.coerceAtLeast(if (landscape) 38 else 44)
}
val nineGridHeightDp: Int =
@@ -82,7 +84,10 @@ internal data class KeyboardLayoutMetrics(
keyRowHeightDp * 4 +
ImeGeometryTokens.KEY_ROW_GAP_DP * 3 +
22
- maxOf(if (landscape) 264 else 302, derived)
+ val baseMinimum = if (landscape) 264 else 302
+ val scaledMinimum =
+ (baseMinimum * heightPercent.coerceIn(92, 120) / 100f).toInt()
+ maxOf(scaledMinimum, derived)
}
val keyboardBodyHeightDp: Int = imeHeightDp - topZoneHeightDp
diff --git a/app/src/main/java/llc/slacker/openime/LocalAudioVoiceBackend.kt b/app/src/main/java/llc/slacker/openime/LocalAudioVoiceBackend.kt
index dd00d980..05bd6cf1 100644
--- a/app/src/main/java/llc/slacker/openime/LocalAudioVoiceBackend.kt
+++ b/app/src/main/java/llc/slacker/openime/LocalAudioVoiceBackend.kt
@@ -7,9 +7,11 @@ import android.content.pm.PackageManager
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
+import android.os.SystemClock
import android.util.Log
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong
import kotlin.math.sqrt
@@ -179,6 +181,10 @@ class LocalAudioVoiceBackend(
}
private val startGeneration = AtomicLong(0L)
private val stopRequestedGeneration = AtomicLong(-1L)
+ private val stopRequestedAtMs = AtomicLong(0L)
+ private val stopExecutor = Executors.newSingleThreadScheduledExecutor { runnable ->
+ Thread(runnable, "local-voice-tail-stop").apply { isDaemon = true }
+ }
private val routeManager = VoiceAudioRouteManager(context)
override fun isAvailable(): Boolean = runtimeProvider.isExpectedAvailable()
@@ -200,6 +206,7 @@ class LocalAudioVoiceBackend(
val traceToken = runtimeProvider.performanceTraceToken() ?: VoicePerformanceTrace.begin()
val generation = startGeneration.incrementAndGet()
stopRequestedGeneration.set(-1L)
+ stopRequestedAtMs.set(0L)
startExecutor.execute {
initializeSession(generation, languageTag, events, traceToken)
}
@@ -282,7 +289,11 @@ class LocalAudioVoiceBackend(
return
}
session.stopRequested.set(stopRequestedGeneration.get() == generation)
- if (session.stopRequested.get()) VoicePerformanceTrace.markVoiceRelease(traceToken)
+ if (session.stopRequested.get()) {
+ session.releaseAtMs = stopRequestedAtMs.get().takeIf { it > 0L }
+ ?: SystemClock.elapsedRealtime()
+ VoicePerformanceTrace.markVoiceRelease(traceToken)
+ }
val modelEvents = object : VoiceRecognitionEvents {
override fun onPartial(text: String) {
@@ -321,7 +332,7 @@ class LocalAudioVoiceBackend(
}
startCaptureThread(session)
events.onReady()
- if (session.stopRequested.get()) requestCaptureStop(session)
+ if (session.stopRequested.get()) scheduleCaptureStop(session, generation)
// Waiting for verification/model mapping is safe here: capture is
// already live and the bounded ring keeps the spoken prefix.
@@ -353,10 +364,13 @@ class LocalAudioVoiceBackend(
runtimeProvider.performanceTraceToken()?.let(VoicePerformanceTrace::markVoiceRelease)
}
val generation = startGeneration.get()
+ val releasedAt = SystemClock.elapsedRealtime()
stopRequestedGeneration.set(generation)
+ stopRequestedAtMs.set(releasedAt)
if (current == null) return
+ current.releaseAtMs = releasedAt
current.stopRequested.set(true)
- requestCaptureStop(current)
+ if (current.running.get()) scheduleCaptureStop(current, generation)
if (current.modelReady.get()) startInferenceThread(current)
// If the first model load is still running, initializeSession keeps
// this session alive, then drains the buffered PCM and emits one final.
@@ -365,6 +379,7 @@ class LocalAudioVoiceBackend(
override fun cancel() {
startGeneration.incrementAndGet()
stopRequestedGeneration.set(-1L)
+ stopRequestedAtMs.set(0L)
val current = synchronized(lock) {
val value = session
session = null
@@ -405,6 +420,24 @@ class LocalAudioVoiceBackend(
session.captureThread?.interrupt()
}
+ private fun scheduleCaptureStop(session: Session, generation: Long) {
+ if (!session.captureStopScheduled.compareAndSet(false, true)) return
+ stopExecutor.schedule(
+ {
+ if (
+ isCurrent(generation, session) &&
+ session.stopRequested.get() &&
+ !session.cancelled.get() &&
+ !session.failed.get()
+ ) {
+ requestCaptureStop(session)
+ }
+ },
+ TAIL_CAPTURE_MILLIS,
+ TimeUnit.MILLISECONDS,
+ )
+ }
+
private fun startCaptureThread(session: Session) {
session.captureFinished.set(false)
session.captureThread = Thread({ captureLoop(session) }, "local-voice-capture")
@@ -432,7 +465,7 @@ class LocalAudioVoiceBackend(
session.degraded.compareAndSet(false, true)
) {
Log.w(
- "OpenImeVoicePerf",
+ TAG,
"PCM pipeline degraded droppedPcmSamples=${session.ring.droppedSamples}",
)
}
@@ -490,7 +523,27 @@ class LocalAudioVoiceBackend(
!session.cancelled.get() &&
session.finished.compareAndSet(false, true)
) {
- val raw = voiceSession.inputFinished().ifBlank { session.lastPartial }
+ val finalizeStartedAt = SystemClock.elapsedRealtime()
+ val remainingBeforeFinish = session.ring.size
+ Log.i(
+ TAG,
+ "inputFinished begin releaseAtMs=${session.releaseAtMs} " +
+ "nowMs=$finalizeStartedAt ringRemainingSamples=$remainingBeforeFinish " +
+ "droppedSamples=${session.ring.droppedSamples} " +
+ "lastPartialLength=${session.lastPartial.length}",
+ )
+ val finalRaw = voiceSession.inputFinished()
+ val usedOuterFallback = finalRaw.isBlank()
+ val raw = finalRaw.ifBlank { session.lastPartial }
+ val finalizeEndedAt = SystemClock.elapsedRealtime()
+ Log.i(
+ TAG,
+ "inputFinished end releaseAtMs=${session.releaseAtMs} " +
+ "nowMs=$finalizeEndedAt finalLength=${finalRaw.length} " +
+ "resolvedLength=${raw.length} lastPartialLength=${session.lastPartial.length} " +
+ "fallback=$usedOuterFallback " +
+ "releaseToFinalMs=${if (session.releaseAtMs > 0L) finalizeEndedAt - session.releaseAtMs else -1L}",
+ )
VoicePerformanceTrace.markFinalAsr(session.traceToken)
val punctuated = if (raw.isBlank()) raw else voiceSession.punctuate(raw) ?: raw
val final = VoiceCorrectionRepository.apply(punctuated)
@@ -544,8 +597,16 @@ class LocalAudioVoiceBackend(
val finished = AtomicBoolean(false)
val failed = AtomicBoolean(false)
val degraded = AtomicBoolean(false)
+ val captureStopScheduled = AtomicBoolean(false)
var captureThread: Thread? = null
var inferenceThread: Thread? = null
+ @Volatile
+ var releaseAtMs: Long = 0L
var lastPartial: String = ""
}
+
+ private companion object {
+ const val TAG = "OpenImeVoicePerf"
+ const val TAIL_CAPTURE_MILLIS = 300L
+ }
}
diff --git a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt
index a48ff85d..cf7b58fe 100644
--- a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt
+++ b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt
@@ -1,5 +1,6 @@
package llc.slacker.openime
+import android.content.Intent
import android.inputmethodservice.InputMethodService
import android.os.Handler
import android.os.Looper
@@ -11,6 +12,7 @@ import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.view.inputmethod.InputMethodSubtype
+import java.io.File
/**
* Native system IME service. The view is a thin native renderer; all candidate
@@ -23,13 +25,15 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can
val nativeCount: Int = 0,
val fallbackCount: Int = 0,
val nativeLatencyMs: Long = 0L,
+ val resultLatencyMs: Long = 0L,
val pathCount: Int = 0,
val finalCandidateSource: String = "none",
) {
fun asLogFields(): String =
"learnedCount=$learnedCount nativeCount=$nativeCount " +
"fallbackCount=$fallbackCount nativeLatencyMs=$nativeLatencyMs " +
- "pathCount=$pathCount finalCandidateSource=$finalCandidateSource"
+ "resultLatencyMs=$resultLatencyMs pathCount=$pathCount " +
+ "finalCandidateSource=$finalCandidateSource"
}
private var keyboardView: ImeKeyboardView? = null
@@ -47,6 +51,8 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can
mainHandler = mainHandler,
windowProvider = { getWindow().window },
keyboardHeightPx = { keyboardView?.measuredHeight },
+ floatingWidthPercent = { ImeSettingsRepository.loadFloatingWidthPercent(this) },
+ floatingOpacityPercent = { ImeSettingsRepository.loadFloatingOpacityPercent(this) },
debugLog = { message ->
if (verboseLogging) Log.d(TAG, message)
},
@@ -189,6 +195,10 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can
override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) {
super.onStartInput(attribute, restarting)
+ if (!restarting) {
+ gateway.discardClearUndo()
+ keyboardView?.hideClearUndo()
+ }
reloadPersistedSettings()
voiceCorrectionTracker.clear()
val previousRimeInputs = candidateQueries.activeInputs
@@ -282,9 +292,13 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can
)
}
- /** Apply settings changed by the standalone preferences Activity immediately. */
+ /** Apply settings changed by standalone Activities on the IME main thread. */
internal fun refreshPersistedSettingsFromActivity() {
- reloadPersistedSettings()
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ reloadPersistedSettings()
+ } else {
+ mainHandler.post(::reloadPersistedSettings)
+ }
}
override fun onCurrentInputMethodSubtypeChanged(newSubtype: InputMethodSubtype) {
@@ -661,13 +675,47 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can
// editor. Otherwise a late native result can restore the just-cleared
// pre-edit on the very next key press.
clearImeCompositionState(render = false)
- if (!gateway.clearAllText()) {
+ val cleared = gateway.clearAllText()
+ if (!cleared) {
android.widget.Toast.makeText(this, "当前应用未能清空全部文本", android.widget.Toast.LENGTH_SHORT).show()
}
voiceCorrectionTracker.clear()
voiceComposing = false
state = state.copy(voiceState = VoiceUiState())
keyboardView?.renderState(state)
+ if (cleared && gateway.hasClearUndo()) {
+ keyboardView?.showClearUndo()
+ } else {
+ keyboardView?.hideClearUndo()
+ }
+ }
+
+ override fun onUndoClear(): Boolean {
+ if (state.passwordField) return false
+ val restored = gateway.restoreLastClear()
+ if (!restored) {
+ showTextEditFeedback("没有可撤回的清空内容")
+ return false
+ }
+ voiceCorrectionTracker.clear()
+ voiceComposing = false
+ state = state.copy(
+ composition = "",
+ candidates = emptyList(),
+ expandedCandidates = emptyList(),
+ voiceState = VoiceUiState(),
+ )
+ lastComposition = ""
+ renderedCandidateSnapshot = null
+ keyboardView?.clearAssociationCandidates()
+ keyboardView?.hideClearUndo()
+ keyboardView?.renderState(state)
+ return true
+ }
+
+ override fun onUndoClearExpired() {
+ gateway.discardClearUndo()
+ keyboardView?.hideClearUndo()
}
override fun onFloatingKeyboardChanged(floating: Boolean) {
@@ -679,6 +727,10 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can
floatingWindow.drag(deltaX, deltaY)
}
+ override fun onFloatingStyleChanged(widthPercent: Int, opacityPercent: Int) {
+ if (floatingWindow.enabled) floatingWindow.reapply()
+ }
+
override fun onSpace() {
prepareForManualInput()
if (keyboardView?.insertIntoInlineEditor(" ") == true) return
@@ -926,6 +978,87 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can
selectCandidate(candidate)
}
+ override fun onCandidateLongPressed(candidate: String) {
+ val composition = lastComposition
+ if (
+ state.passwordField ||
+ composition.isBlank() ||
+ candidate.isBlank() ||
+ !allowsPersonalizedLearning()
+ ) {
+ return
+ }
+
+ if (
+ QuickPhraseRepository.candidatesForInputCode(
+ context = this,
+ rawCode = composition,
+ exactOnly = false,
+ limit = 16,
+ ).contains(candidate)
+ ) {
+ return
+ }
+
+ val modeAtRequest = state.keyboardMode
+
+ fun refreshAfterDelete() {
+ if (lastComposition != composition || state.keyboardMode != modeAtRequest) return
+ val fallback = fallbackCandidatesFor(composition, modeAtRequest)
+ handleCompositionChanged(
+ composition = composition,
+ candidates = fallback,
+ rimeInputs = listOf(composition),
+ )
+ }
+
+ fun confirmDelete(onConfirm: () -> Unit) {
+ if (lastComposition != composition || state.keyboardMode != modeAtRequest) return
+ keyboardView?.confirmCandidateDeletion(candidate, onConfirm)
+ }
+
+ if (!rime.isReady) {
+ if (!UserPhraseRepository.contains(composition, candidate)) return
+ confirmDelete {
+ if (UserPhraseRepository.forget(composition, candidate)) {
+ PersonalizationRepository.forget(candidate)
+ refreshAfterDelete()
+ showTextEditFeedback("已删除个人学习词")
+ }
+ }
+ return
+ }
+
+ val queued = rime.isUserLearnedCandidate(composition, candidate) { learned ->
+ mainHandler.post {
+ if (
+ !learned ||
+ lastComposition != composition ||
+ state.keyboardMode != modeAtRequest
+ ) {
+ return@post
+ }
+ confirmDelete {
+ val deleteQueued = rime.deleteCandidate(composition, candidate) { deleted ->
+ mainHandler.post {
+ if (deleted) {
+ PersonalizationRepository.forget(candidate)
+ refreshAfterDelete()
+ showTextEditFeedback("已删除个人学习词")
+ }
+ }
+ }
+ if (!deleteQueued) {
+ showTextEditFeedback("暂时无法修改个人词")
+ }
+ }
+ }
+ }
+ if (!queued) {
+ return
+ }
+ }
+
/**
* Association ("联想") chips are produced only after a commit, so there is
* no composition for [selectCandidate] to match against. Commit the word
@@ -1095,8 +1228,26 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can
commitFirstCandidate()
}
- private fun fallbackCandidatesFor(composition: String, mode: KeyboardMode): List =
- candidatePipeline.candidatesFor(mode, composition, state.fuzzyPinyinEnabled)
+ private fun fallbackCandidatesFor(
+ composition: String,
+ mode: KeyboardMode,
+ ): List {
+ val normal = candidatePipeline.candidatesFor(
+ mode,
+ composition,
+ state.fuzzyPinyinEnabled,
+ )
+ if (mode != KeyboardMode.PINYIN_26 && mode != KeyboardMode.ENGLISH_26) {
+ return normal
+ }
+ val quickPhrases = QuickPhraseRepository.candidatesForInputCode(
+ context = this,
+ rawCode = composition,
+ exactOnly = false,
+ limit = 8,
+ )
+ return (quickPhrases + normal).distinct().take(MAX_CANDIDATES)
+ }
/** The extra learner is only a repeated-choice fallback while Rime is unavailable. */
private fun immediateCandidates(composition: String, fallback: List): List {
@@ -1129,12 +1280,23 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can
mode = mode,
rimeInputs = rimeInputs,
onResult = result@{ request, queryInputs, query ->
- if (
- state.keyboardMode != mode ||
- lastComposition != composition
- ) return@result
-
- val native = query.choices
+ fun applyWhenCandidateSurfaceIdle() {
+ if (
+ state.keyboardMode != mode ||
+ lastComposition != composition ||
+ candidateQueries.currentGeneration() != request
+ ) {
+ return
+ }
+ if (keyboardView?.isCandidateInteractionActive() == true) {
+ mainHandler.postDelayed(
+ { applyWhenCandidateSurfaceIdle() },
+ CANDIDATE_REFRESH_IDLE_POLL_MS,
+ )
+ return
+ }
+
+ val native = query.choices
// Once Rime returns candidates, its mature dictionary and
// userdb ordering replace the transient Kotlin preview. The
// fallback is retained only when native has no answer.
@@ -1144,7 +1306,30 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can
emptyList()
}
val finalCandidates = if (native.isNotEmpty()) {
- native.map { it.text }
+ val nativeText = native.map { it.text }
+ val exactQuickPhrases =
+ if (mode == KeyboardMode.PINYIN_26 || mode == KeyboardMode.ENGLISH_26) {
+ QuickPhraseRepository.candidatesForInputCode(
+ context = this,
+ rawCode = composition,
+ exactOnly = true,
+ limit = 8,
+ )
+ } else {
+ emptyList()
+ }
+ if (exactQuickPhrases.isNotEmpty()) {
+ (exactQuickPhrases + nativeText + fallback)
+ .distinct()
+ .take(MAX_CANDIDATES)
+ } else if (
+ mode == KeyboardMode.PINYIN_26 &&
+ nativeText.size < TYPO_CORRECTION_NATIVE_THRESHOLD
+ ) {
+ (nativeText + fallback).distinct().take(MAX_CANDIDATES)
+ } else {
+ nativeText
+ }
} else {
(learned + fallback).distinct().take(MAX_CANDIDATES)
}
@@ -1158,6 +1343,7 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can
nativeCount = native.size,
fallbackCount = fallback.distinct().size,
nativeLatencyMs = query.latencyMs,
+ resultLatencyMs = query.resultLatencyMs,
pathCount = queryInputs.size,
finalCandidateSource = when {
native.isNotEmpty() -> "native"
@@ -1178,6 +1364,8 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can
nativeReferences = nativeReferences,
)
keyboardView?.renderState(state)
+ }
+ applyWhenCandidateSurfaceIdle()
},
)
@@ -1316,11 +1504,35 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can
renderedCandidateSnapshot = null
}
+ internal fun exportRimeUserData(
+ targetDir: File,
+ onComplete: (List?) -> Unit,
+ ): Boolean =
+ ::rime.isInitialized &&
+ rime.exportUserDictionaries(targetDir, onComplete)
+
+ internal fun importRimeUserData(
+ sourceDir: File,
+ dictionaries: List,
+ onComplete: (Int?) -> Unit,
+ ): Boolean =
+ ::rime.isInitialized &&
+ rime.importUserDictionaries(sourceDir, dictionaries, onComplete)
+
+ override fun onOpenAboutData() {
+ startActivity(
+ Intent(this, AboutDataActivity::class.java)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
+ )
+ }
+
internal companion object {
const val TAG = "OpenIme"
+ const val CANDIDATE_REFRESH_IDLE_POLL_MS = 16L
const val MAX_RIME_INPUT_LENGTH = 256
const val MAX_RIME_NINE_KEY_PATHS = 6
const val MAX_CANDIDATES = 96
+ const val TYPO_CORRECTION_NATIVE_THRESHOLD = 8
@Volatile
var activeInstance: LocalVoiceImeService? = null
}
diff --git a/app/src/main/java/llc/slacker/openime/MainActivity.kt b/app/src/main/java/llc/slacker/openime/MainActivity.kt
index a3461d09..2ecc0b66 100644
--- a/app/src/main/java/llc/slacker/openime/MainActivity.kt
+++ b/app/src/main/java/llc/slacker/openime/MainActivity.kt
@@ -3,9 +3,12 @@ package llc.slacker.openime
import android.Manifest
import android.app.Activity
import android.content.Intent
+import android.content.res.ColorStateList
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Build
+import android.text.Editable
+import android.text.TextWatcher
import android.view.HapticFeedbackConstants
import android.view.View
import android.view.WindowInsets
@@ -13,10 +16,9 @@ import android.provider.Settings
import android.net.Uri
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
+import android.widget.ImageView
import android.widget.EditText
import android.widget.Toast
-import android.graphics.Color
-import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.StateListDrawable
/** Match the selected IME by the exact package component, never by substring. */
@@ -58,15 +60,60 @@ class MainActivity : Activity() {
startActivity(Intent(this, ImeSettingsActivity::class.java))
}
setupClick(findViewById(R.id.voice_permission)) {
- if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) return@setupClick
- val requested = getPreferences(MODE_PRIVATE).getBoolean("microphone_requested", false)
+ if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
+ return@setupClick
+ }
+ val prefs = getPreferences(MODE_PRIVATE)
+ prefs.edit().putBoolean("microphone_skipped", false).apply()
+ val requested = prefs.getBoolean("microphone_requested", false)
if (requested && !shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
- startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:$packageName")))
+ startActivity(
+ Intent(
+ Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+ Uri.parse("package:$packageName"),
+ ),
+ )
} else {
- getPreferences(MODE_PRIVATE).edit().putBoolean("microphone_requested", true).apply()
+ prefs.edit().putBoolean("microphone_requested", true).apply()
requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), 1)
}
}
+ setupClick(findViewById(R.id.voice_permission_skip)) {
+ getPreferences(MODE_PRIVATE)
+ .edit()
+ .putBoolean("microphone_skipped", true)
+ .apply()
+ refreshSetupState()
+ }
+ setupClick(findViewById(R.id.test_step)) {
+ val input = findViewById(R.id.test_input)
+ input.requestFocus()
+ getSystemService(InputMethodManager::class.java).showSoftInput(
+ input,
+ InputMethodManager.SHOW_IMPLICIT,
+ )
+ }
+ findViewById(R.id.test_input).addTextChangedListener(
+ object : TextWatcher {
+ override fun beforeTextChanged(
+ s: CharSequence?,
+ start: Int,
+ count: Int,
+ after: Int,
+ ) = Unit
+
+ override fun onTextChanged(
+ s: CharSequence?,
+ start: Int,
+ before: Int,
+ count: Int,
+ ) {
+ refreshSetupState()
+ }
+
+ override fun afterTextChanged(s: Editable?) = Unit
+ },
+ )
}
override fun onResume() {
@@ -84,15 +131,30 @@ class MainActivity : Activity() {
val enabled = status.enabled
val selected = status.selected
val accent = AccentPalette.parse(ImeSettingsRepository.loadSkinColor(this))
- findViewById(R.id.test_input).let { view ->
- view.background = SetupUi.focusRingBackground(this)
- SetupUi.styleCursor(this, view)
- }
- findViewById(R.id.status).setText(when {
- selected -> R.string.setup_ready
- enabled -> R.string.setup_choose
- else -> R.string.setup_enable
- })
+ val prefs = getPreferences(MODE_PRIVATE)
+ val microphoneGranted =
+ checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
+ val microphoneRequested = prefs.getBoolean("microphone_requested", false)
+ val microphoneSkipped = prefs.getBoolean("microphone_skipped", false)
+ val microphoneDeniedPermanently =
+ microphoneRequested &&
+ !microphoneGranted &&
+ !shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)
+ val testInput = findViewById(R.id.test_input)
+ val testDone = testInput.text?.isNotBlank() == true
+
+ testInput.background = SetupUi.focusRingBackground(this)
+ SetupUi.styleCursor(this, testInput)
+
+ findViewById(R.id.status).setText(
+ when {
+ !enabled -> R.string.setup_enable
+ !selected -> R.string.setup_choose
+ testDone -> R.string.setup_ready
+ else -> R.string.test_step
+ },
+ )
+
styleStep(
row = findViewById(R.id.open_ime_settings),
mark = findViewById(R.id.open_ime_settings_mark),
@@ -105,6 +167,7 @@ class MainActivity : Activity() {
markText = "1",
accent = accent,
)
+
styleStep(
row = findViewById(R.id.choose_ime),
mark = findViewById(R.id.choose_ime_mark),
@@ -119,36 +182,81 @@ class MainActivity : Activity() {
)
findViewById(R.id.choose_ime).apply {
isEnabled = enabled
- alpha = when {
- selected -> 0.72f
- enabled -> 1f
- else -> 0.55f
- }
+ alpha = if (enabled) 1f else ImeSurfacePolicy.DISABLED_ALPHA
}
- val microphoneGranted = checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
+
+ val voiceDone = microphoneGranted || microphoneSkipped
+ val voiceActive = selected && !voiceDone
+ val voiceActiveText =
+ if (microphoneDeniedPermanently) {
+ getString(R.string.voice_permission_settings)
+ } else {
+ getString(R.string.voice_permission_enable)
+ }
+ val voiceDoneText =
+ if (microphoneGranted) {
+ getString(R.string.voice_permission_ready)
+ } else {
+ getString(R.string.voice_permission_skipped)
+ }
+ styleStep(
+ row = findViewById(R.id.voice_permission),
+ mark = findViewById(R.id.voice_permission_mark),
+ label = findViewById(R.id.voice_permission_label),
+ chevron = findViewById(R.id.voice_permission_chevron),
+ done = voiceDone,
+ active = voiceActive,
+ doneText = voiceDoneText,
+ activeText = voiceActiveText,
+ markText = "3",
+ accent = accent,
+ )
findViewById(R.id.voice_permission).apply {
- isEnabled = !microphoneGranted
- alpha = if (microphoneGranted) 0.72f else 1f
- background = SetupUi.mutedPillBackground(this@MainActivity)
+ isEnabled = selected && !microphoneGranted
+ alpha = if (selected) 1f else ImeSurfacePolicy.DISABLED_ALPHA
}
- findViewById(R.id.voice_permission_label).apply {
- setText(if (microphoneGranted) R.string.voice_permission_ready else R.string.voice_permission_enable)
- setTextColor(getColor(if (microphoneGranted) R.color.setup_muted_text else R.color.setup_title))
+ findViewById(R.id.voice_permission_description).setText(
+ when {
+ microphoneGranted -> R.string.voice_permission_ready_description
+ microphoneDeniedPermanently -> R.string.voice_permission_denied
+ else -> R.string.voice_permission_description
+ },
+ )
+ findViewById(R.id.voice_permission_skip).apply {
+ visibility =
+ if (selected && !microphoneGranted && !microphoneSkipped) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+ isEnabled = visibility == View.VISIBLE
+ background = SetupUi.secondaryBackground(this@MainActivity)
}
- findViewById(R.id.voice_permission_chevron).visibility =
- if (microphoneGranted) View.GONE else View.VISIBLE
- findViewById(R.id.voice_permission).contentDescription = getString(
- if (microphoneGranted) R.string.voice_permission_ready else R.string.voice_permission_enable,
+
+ styleStep(
+ row = findViewById(R.id.test_step),
+ mark = findViewById(R.id.test_step_mark),
+ label = findViewById(R.id.test_step_label),
+ chevron = null,
+ done = testDone,
+ active = selected && !testDone,
+ doneText = getString(R.string.test_step_done),
+ activeText = getString(R.string.test_step),
+ markText = "4",
+ accent = accent,
)
- if (Build.VERSION.SDK_INT >= 30) {
- findViewById(R.id.voice_permission).stateDescription =
- if (microphoneGranted) "已授权" else "未授权,可选"
+ findViewById(R.id.test_step).apply {
+ isEnabled = selected
+ alpha = if (selected) 1f else ImeSurfacePolicy.DISABLED_ALPHA
}
+ testInput.isEnabled = selected
+ testInput.alpha = if (selected) 1f else ImeSurfacePolicy.DISABLED_ALPHA
+
val ready = enabled && selected
findViewById(R.id.open_app_settings).apply {
isEnabled = ready
- alpha = if (ready) 1f else 0.45f
- background = SetupUi.mutedPillBackground(this@MainActivity)
+ alpha = if (ready) 1f else ImeSurfacePolicy.DISABLED_ALPHA
+ background = SetupUi.secondaryBackground(this@MainActivity)
contentDescription = getString(
if (ready) R.string.open_app_settings else R.string.setup_need_switch,
)
@@ -158,7 +266,6 @@ class MainActivity : Activity() {
}
}
-
private fun isImeReady(): Boolean {
val status = imeStatus()
return status.enabled && status.selected
@@ -186,7 +293,7 @@ class MainActivity : Activity() {
row: View,
mark: TextView,
label: TextView,
- chevron: TextView?,
+ chevron: ImageView?,
done: Boolean,
active: Boolean,
doneText: String,
@@ -198,9 +305,9 @@ class MainActivity : Activity() {
// the system picker or input-method settings after initial setup.
row.isEnabled = true
row.background = when {
- active -> primaryPill(accent)
- done -> completedPill(accent)
- else -> SetupUi.mutedPillBackground(this)
+ active -> SetupUi.buttonBackground(this, accent)
+ done -> completedBackground(accent)
+ else -> SetupUi.secondaryBackground(this)
}
label.text = if (done) doneText else activeText
label.setTextColor(
@@ -211,7 +318,16 @@ class MainActivity : Activity() {
// A completed step is easier to scan as a result than as an old
// step number. Keep the number for the current step so the flow still
// reads as 1 -> 2 while the completed state reads as a check.
- mark.text = if (done) "✓" else markText
+ mark.text = if (done) "" else markText
+ mark.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ if (done) R.drawable.ic_check else 0,
+ 0,
+ 0,
+ 0,
+ )
+ mark.compoundDrawableTintList = ColorStateList.valueOf(
+ if (done) contrastText(getColor(R.color.setup_ready)) else accent,
+ )
mark.setBackgroundResource(
when {
active -> R.drawable.bg_setup_mark_active
@@ -220,19 +336,13 @@ class MainActivity : Activity() {
},
)
mark.setTextColor(
- if (active) accent else if (done) getColor(R.color.setup_ready) else accent,
+ when {
+ active -> accent
+ done -> contrastText(getColor(R.color.setup_ready))
+ else -> accent
+ },
)
- if (active) {
- mark.background = rounded(
- contrastText(accent),
- ImeGeometryTokens.BADGE_RADIUS_DP.toFloat(),
- )
- } else if (done) {
- mark.setBackgroundResource(R.drawable.bg_setup_mark_done)
- } else {
- mark.setBackgroundResource(R.drawable.bg_setup_mark)
- }
- chevron?.setTextColor(
+ chevron?.imageTintList = ColorStateList.valueOf(
if (active) contrastText(accent) else getColor(R.color.setup_body),
)
chevron?.visibility = if (done) View.GONE else View.VISIBLE
@@ -251,60 +361,32 @@ class MainActivity : Activity() {
}
}
- private fun primaryPill(color: Int): StateListDrawable {
- val pressed = dim(color, 0.86f)
+ private fun completedBackground(accent: Int): StateListDrawable {
+ val surface = getColor(R.color.setup_muted)
+ val tint = ImeDrawableFactory.blend(surface, accent, 0.10f)
+ val pressed = ImeDrawableFactory.blend(surface, accent, 0.16f)
+ val radius = SetupUi.dp(this, ImeGeometryTokens.CONTROL_RADIUS_DP).toFloat()
return StateListDrawable().apply {
addState(
intArrayOf(android.R.attr.state_pressed),
- rounded(pressed, ImeGeometryTokens.SETUP_PILL_RADIUS_DP.toFloat()),
+ ImeDrawableFactory.rounded(pressed, radius),
)
addState(
intArrayOf(android.R.attr.state_focused),
- rounded(color, ImeGeometryTokens.SETUP_PILL_RADIUS_DP.toFloat(), contrastText(color)),
- )
- addState(intArrayOf(), rounded(color, ImeGeometryTokens.SETUP_PILL_RADIUS_DP.toFloat()))
- }
- }
-
- private fun completedPill(accent: Int): StateListDrawable {
- val surface = getColor(R.color.setup_muted)
- val tint = blend(surface, accent, 0.10f)
- val pressed = blend(surface, accent, 0.16f)
- return StateListDrawable().apply {
- addState(
- intArrayOf(android.R.attr.state_pressed),
- rounded(pressed, ImeGeometryTokens.SETUP_PILL_RADIUS_DP.toFloat()),
+ ImeDrawableFactory.rounded(
+ color = tint,
+ radiusPx = radius,
+ strokeColor = accent,
+ strokeWidthPx = SetupUi.dp(this@MainActivity, 1),
+ ),
)
addState(
- intArrayOf(android.R.attr.state_focused),
- rounded(tint, ImeGeometryTokens.SETUP_PILL_RADIUS_DP.toFloat(), accent),
+ intArrayOf(),
+ ImeDrawableFactory.rounded(tint, radius),
)
- addState(intArrayOf(), rounded(tint, ImeGeometryTokens.SETUP_PILL_RADIUS_DP.toFloat()))
}
}
- private fun rounded(color: Int, radiusDp: Float, strokeColor: Int? = null): GradientDrawable = GradientDrawable().apply {
- shape = GradientDrawable.RECTANGLE
- setColor(color)
- cornerRadius = radiusDp * resources.displayMetrics.density
- strokeColor?.let { setStroke(resources.displayMetrics.density.toInt().coerceAtLeast(1), it) }
- }
-
- private fun dim(color: Int, factor: Float): Int = Color.rgb(
- (Color.red(color) * factor).toInt().coerceIn(0, 255),
- (Color.green(color) * factor).toInt().coerceIn(0, 255),
- (Color.blue(color) * factor).toInt().coerceIn(0, 255),
- )
-
- private fun blend(base: Int, tint: Int, amount: Float): Int {
- val ratio = amount.coerceIn(0f, 1f)
- return Color.rgb(
- (Color.red(base) * (1f - ratio) + Color.red(tint) * ratio).toInt(),
- (Color.green(base) * (1f - ratio) + Color.green(tint) * ratio).toInt(),
- (Color.blue(base) * (1f - ratio) + Color.blue(tint) * ratio).toInt(),
- )
- }
-
private fun contrastText(background: Int): Int {
return ImeContrastPolicy.contrastText(background)
}
diff --git a/app/src/main/java/llc/slacker/openime/NineKeyLocalDecoder.kt b/app/src/main/java/llc/slacker/openime/NineKeyLocalDecoder.kt
index 18b23f33..97d3b74d 100644
--- a/app/src/main/java/llc/slacker/openime/NineKeyLocalDecoder.kt
+++ b/app/src/main/java/llc/slacker/openime/NineKeyLocalDecoder.kt
@@ -149,21 +149,15 @@ internal class NineKeyLocalDecoder(
?.pinyin
?.take(bounded.length)
?.takeIf { digitsForPinyin(it) == bounded }
- val preferredFallbackPreview = continuationBase?.let { base ->
- val baseDigits = digitsForPinyin(base) ?: return@let null
- if (!bounded.startsWith(baseDigits) || bounded.length <= baseDigits.length) {
- return@let null
- }
- base + fallbackLetters(bounded.removePrefix(baseDigits))
- }
val preview = when {
stable != null -> stable
continuous != null -> continuous
+ bounded.length == 1 -> bounded
+ paths.isEmpty() -> bounded
continuationBase != null && validPreset.isEmpty() && prefixPreview != null -> prefixPreview
- validPreset.isEmpty() && preferredFallbackPreview != null -> preferredFallbackPreview
paths.isNotEmpty() -> paths.first()
prefixPreview != null -> prefixPreview
- else -> fallbackLetters(bounded)
+ else -> bounded
}
previousDigits = bounded
@@ -340,24 +334,6 @@ internal class NineKeyLocalDecoder(
private fun isDisplayCandidate(value: String): Boolean =
value.isNotBlank() && value.none(Char::isDigit) && value.any { it.code > 0x7f }
- private fun fallbackLetters(digits: String): String = buildString(digits.length) {
- digits.forEach { digit ->
- append(
- when (digit) {
- '2' -> 'a'
- '3' -> 'd'
- '4' -> 'g'
- '5' -> 'j'
- '6' -> 'm'
- '7' -> 'p'
- '8' -> 't'
- '9' -> 'w'
- else -> return@forEach
- },
- )
- }
- }
-
companion object {
const val MAX_DIGITS = 64
private const val MAX_PATHS = 12
diff --git a/app/src/main/java/llc/slacker/openime/NineKeyPerformanceTrace.kt b/app/src/main/java/llc/slacker/openime/NineKeyPerformanceTrace.kt
new file mode 100644
index 00000000..2816c0be
--- /dev/null
+++ b/app/src/main/java/llc/slacker/openime/NineKeyPerformanceTrace.kt
@@ -0,0 +1,65 @@
+package llc.slacker.openime
+
+import android.util.Log
+import java.util.Locale
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.math.ceil
+
+internal object NineKeyPerformanceTrace {
+ private const val TAG = "OpenImeNineKeyPerf"
+ private const val WINDOW_SIZE = 20
+
+ private val constructionCount = AtomicInteger(0)
+ private val lock = Any()
+ private val resolveSamplesNs = LinkedHashMap>()
+
+ fun recordDecoderConstruction(elapsedNs: Long, threadName: String) {
+ val ordinal = constructionCount.incrementAndGet()
+ safeLog(
+ "decoder-init ordinal=${ordinal} first=${ordinal == 1} " +
+ "elapsedMs=${formatMs(elapsedNs)} thread=$threadName " +
+ "phase=CandidatePipeline.init",
+ )
+ }
+
+ fun recordResolve(digitLength: Int, elapsedNs: Long, threadName: String) {
+ if (digitLength !in 1..NineKeyLocalDecoder.MAX_DIGITS) return
+ val summary = synchronized(lock) {
+ val samples = resolveSamplesNs.getOrPut(digitLength) { ArrayList(WINDOW_SIZE) }
+ samples += elapsedNs
+ if (samples.size < WINDOW_SIZE) return@synchronized null
+ val window = samples.takeLast(WINDOW_SIZE).sorted()
+ ResolveSummary(
+ p50Ns = percentile(window, 0.50),
+ p95Ns = percentile(window, 0.95),
+ ).also {
+ samples.clear()
+ }
+ } ?: return
+
+ safeLog(
+ "resolve length=$digitLength samples=$WINDOW_SIZE " +
+ "p50Ms=${formatMs(summary.p50Ns)} " +
+ "p95Ms=${formatMs(summary.p95Ns)} thread=$threadName",
+ )
+ }
+
+ private fun safeLog(message: String) {
+ runCatching { Log.d(TAG, message) }
+ }
+
+ private fun percentile(sorted: List, quantile: Double): Long {
+ if (sorted.isEmpty()) return 0L
+ val index = (ceil(sorted.size * quantile).toInt() - 1)
+ .coerceIn(0, sorted.lastIndex)
+ return sorted[index]
+ }
+
+ private fun formatMs(valueNs: Long): String =
+ String.format(Locale.US, "%.3f", valueNs / 1_000_000.0)
+
+ private data class ResolveSummary(
+ val p50Ns: Long,
+ val p95Ns: Long,
+ )
+}
diff --git a/app/src/main/java/llc/slacker/openime/NineKeyPresets.kt b/app/src/main/java/llc/slacker/openime/NineKeyPresets.kt
index 7c3ca24e..3b896c5c 100644
--- a/app/src/main/java/llc/slacker/openime/NineKeyPresets.kt
+++ b/app/src/main/java/llc/slacker/openime/NineKeyPresets.kt
@@ -3,7 +3,7 @@ package llc.slacker.openime
/** Small high-confidence 9-key overrides. Every entry is code-validated in tests. */
internal object NineKeyPresets {
val combinations: Map> = linkedMapOf(
- "64" to listOf("ni", "mi", "oh"),
+ "64" to listOf("ni", "mi"),
"64426" to listOf("nihao"),
"9664" to listOf("yong"),
"426" to listOf("hao"),
diff --git a/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt b/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt
index 48a0621a..afdf59d7 100644
--- a/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt
+++ b/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt
@@ -1,28 +1,39 @@
package llc.slacker.openime
import android.content.Context
+import android.graphics.Typeface
import android.os.Build
import android.text.Editable
import android.text.TextWatcher
import android.view.Gravity
+import android.view.View
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
/**
- * Owns the Chinese 9-key side symbol rail and ambiguity filter presentation.
+ * Owns the Chinese 9-key side rail.
*
- * The renderer receives the built rail directly. Composition updates also use
- * explicit editor/rail references rather than searching the whole View tree.
+ * With no ambiguity it behaves as the normal vertically scrollable symbol
+ * rail. Once one digit sequence maps to multiple Pinyin paths, the same rail
+ * becomes a directly selectable Pinyin list. This mirrors mature 9-key IMEs:
+ * the user sees all useful spellings at once instead of cycling one hidden
+ * option through a single button.
*/
internal class NineKeySymbolRailController(
private val context: Context,
private val composition: EditText,
private val onCommit: (String) -> Unit,
private val onFeedback: () -> Unit,
+ private val onRailChanged: (View) -> Unit,
) {
+ private enum class RailMode { SYMBOLS, PINYIN }
+
private var rail: ScrollView? = null
+ private var railMode = RailMode.SYMBOLS
+ private var renderedChoices: List = emptyList()
+ private var renderedSelected: String? = null
init {
composition.addTextChangedListener(object : TextWatcher {
@@ -51,35 +62,36 @@ internal class NineKeySymbolRailController(
context = context,
railTag = NINE_RAIL_TAG,
contentTag = NINE_CONTENT_TAG,
- contentDescription = "九键常用符号,上下滑动查看更多",
+ contentDescription = "九键拼音筛选或常用符号,上下滑动查看更多",
symbols = commonSymbols(),
tagPrefix = "punct:",
onCommit = onCommit,
onFeedback = onFeedback,
).also {
rail = it
+ railMode = RailMode.SYMBOLS
+ renderedChoices = emptyList()
+ renderedSelected = null
refreshPinyinFilters()
}
+ /**
+ * A symbol edit may happen while the Pinyin filter is visible. Re-resolve
+ * the current composition first; symbols are repopulated only when the
+ * rail is actually in symbol mode.
+ */
fun refreshSymbols() {
- val current = rail ?: return
- val content = current.getChildAt(0) as? LinearLayout ?: return
- val header = content.findViewWithTag(FILTER_TAG)
- SymbolRailRenderer.populate(
- scroll = current,
- symbols = commonSymbols(),
- tagPrefix = "punct:",
- preservedHeader = header,
- onCommit = onCommit,
- onFeedback = onFeedback,
- )
- refreshPinyinFilters()
+ if (railMode == RailMode.SYMBOLS) {
+ renderSymbols(force = true)
+ } else {
+ refreshPinyinFilters()
+ }
}
private fun refreshPinyinFilters() {
val text = composition.text?.toString().orEmpty()
if (text.isBlank()) {
- setPinyinFilters(emptyList(), null) {}
+ renderSymbols()
return
}
@@ -89,86 +101,142 @@ internal class NineKeySymbolRailController(
val digits = CandidatePipeline.nineKeyDigitsFor(suffix)
val code = digits?.let { NineKeyLocalDecoder.nativeCode(prefix, it) }
if (code.isNullOrEmpty()) {
- setPinyinFilters(emptyList(), null) {}
+ renderSymbols()
+ return
+ }
+
+ val resolver = context as? CandidateResolver
+ if (resolver == null) {
+ renderSymbols()
return
}
- val resolver = context as? CandidateResolver ?: return
val choices = resolver.nineKeyPathsFor(code)
- val selected = resolver.selectedNineKeyPathFor(code) ?: text
- setPinyinFilters(
- filters = choices,
+ .asSequence()
+ .map { it.trim() }
+ .filter { it.isNotEmpty() }
+ .distinct()
+ .take(MAX_VISIBLE_PATHS)
+ .toList()
+
+ // One spelling needs no disambiguation. Keep the valuable symbol rail
+ // available instead of occupying the whole column with redundant UI.
+ if (choices.size <= 1) {
+ renderSymbols()
+ return
+ }
+
+ val selected = resolver.selectedNineKeyPathFor(code)
+ ?.takeIf { it in choices }
+ ?: text.takeIf { it in choices }
+ ?: choices.first()
+
+ renderPinyinChoices(
+ choices = choices,
selected = selected,
) { chosen ->
resolver.selectNineKeyPath(code, chosen)
- if (chosen == composition.text?.toString()) return@setPinyinFilters
+ if (chosen == composition.text?.toString()) return@renderPinyinChoices
composition.setText(chosen)
composition.setSelection(chosen.length)
}
}
- private fun setPinyinFilters(
- filters: List,
- selected: String?,
+ private fun renderSymbols(force: Boolean = false) {
+ val scroll = rail ?: return
+ if (railMode == RailMode.SYMBOLS && !force) return
+
+ SymbolRailRenderer.populate(
+ scroll = scroll,
+ symbols = commonSymbols(),
+ tagPrefix = "punct:",
+ onCommit = onCommit,
+ onFeedback = onFeedback,
+ )
+ railMode = RailMode.SYMBOLS
+ renderedChoices = emptyList()
+ renderedSelected = null
+ if (scroll.scrollY != 0) scroll.post { scroll.scrollTo(0, 0) }
+ scroll.contentDescription = "九键常用符号,上下滑动查看更多"
+ onRailChanged(scroll)
+ }
+
+ private fun renderPinyinChoices(
+ choices: List,
+ selected: String,
onSelect: (String) -> Unit,
) {
val scroll = rail ?: return
- val content = scroll.getChildAt(0) as? LinearLayout ?: return
- val choices = filters
- .asSequence()
- .map { it.trim() }
- .filter { it.isNotEmpty() }
- .distinct()
- .take(8)
- .toList()
-
- val existing = content.findViewWithTag(FILTER_TAG)
- if (choices.size <= 1) {
- if (existing != null) content.removeView(existing)
+ if (
+ railMode == RailMode.PINYIN &&
+ renderedChoices == choices &&
+ renderedSelected == selected
+ ) {
return
}
+ val content = scroll.getChildAt(0) as? LinearLayout ?: return
+ val inheritedTextColor = (0 until content.childCount)
+ .asSequence()
+ .mapNotNull { content.getChildAt(it) as? TextView }
+ .firstOrNull()
+ ?.currentTextColor
- var active = selected?.takeIf { it in choices } ?: choices.first()
- val view = existing ?: TextView(content.context).apply {
- tag = FILTER_TAG
- textSize = 12f
- gravity = Gravity.CENTER
- isClickable = true
- isFocusable = true
- minimumHeight = SymbolRailRenderer.cellHeightPx(content.context)
- val inherited = (0 until content.childCount)
- .asSequence()
- .mapNotNull { content.getChildAt(it) as? TextView }
- .firstOrNull { it.tag != FILTER_TAG }
- ?.currentTextColor
- inherited?.let(::setTextColor)
+ content.removeAllViews()
+ choices.forEachIndexed { index, choice ->
+ val active = choice == selected
content.addView(
- this,
- 0,
- SymbolRailRenderer.cellParams(content.context, withGap = true),
+ TextView(content.context).apply {
+ text = displayPath(choice)
+ textSize = ImeTypographyTokens.BODY_SP
+ gravity = Gravity.CENTER
+ tag = if (active) SELECTED_FILTER_TAG else FILTER_TAG
+ contentDescription =
+ if (active) {
+ "九键拼音${speakablePath(choice)},已选择"
+ } else {
+ "九键拼音${speakablePath(choice)},双击选择"
+ }
+ if (Build.VERSION.SDK_INT >= 30) {
+ stateDescription = if (active) "已选择" else "未选择"
+ }
+ isSelected = active
+ typeface = if (active) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
+ isClickable = true
+ isFocusable = true
+ maxLines = 2
+ minimumHeight = SymbolRailRenderer.cellHeightPx(content.context)
+ inheritedTextColor?.let(::setTextColor)
+ setOnClickListener {
+ onFeedback()
+ onSelect(choice)
+ }
+ },
+ SymbolRailRenderer.cellParams(
+ content.context,
+ withGap = index < choices.lastIndex,
+ ),
)
}
- fun updatePresentation() {
- view.text = active.replace(" ", "·") + " ›"
- view.contentDescription =
- "九键拼音筛选,当前${active.replace(" ", "、")},点击切换"
- if (Build.VERSION.SDK_INT >= 30) {
- view.stateDescription = "当前${active.replace(" ", "、")}"
- }
+ railMode = RailMode.PINYIN
+ renderedChoices = choices.toList()
+ renderedSelected = selected
+ scroll.contentDescription = "九键拼音筛选,上下滑动查看更多"
+ onRailChanged(scroll)
+ val selectedIndex = choices.indexOf(selected).coerceAtLeast(0)
+ scroll.post {
+ val target = content.getChildAt(selectedIndex) ?: return@post
+ val viewport = scroll.height.coerceAtLeast(SymbolRailRenderer.cellHeightPx(content.context))
+ val targetY = (target.top - (viewport - target.height) / 2).coerceAtLeast(0)
+ scroll.scrollTo(0, targetY)
}
+ }
- updatePresentation()
- view.setOnClickListener {
- onFeedback()
- val current = choices.indexOf(active).coerceAtLeast(0)
- active = choices[(current + 1) % choices.size]
- updatePresentation()
- onSelect(active)
- }
+ private fun displayPath(path: String): String =
+ path.replace(" ", "·").replace("|", "·").replace("'", "·")
- if (scroll.scrollY != 0) scroll.post { scroll.scrollTo(0, 0) }
- }
+ private fun speakablePath(path: String): String =
+ path.replace(" ", "、").replace("|", "、").replace("'", "、")
private fun commonSymbols(): List =
CustomSymbolRepository.load(context)
@@ -185,5 +253,7 @@ internal class NineKeySymbolRailController(
const val NINE_RAIL_TAG = "nine-punct-stack"
const val NINE_CONTENT_TAG = "nine-symbol-scroll-content"
const val FILTER_TAG = "nine-pinyin-path-filter"
+ const val SELECTED_FILTER_TAG = "nine-pinyin-path-selected"
+ const val MAX_VISIBLE_PATHS = 16
}
}
diff --git a/app/src/main/java/llc/slacker/openime/NumericKeyboardRenderer.kt b/app/src/main/java/llc/slacker/openime/NumericKeyboardRenderer.kt
index 5756e2ca..6c06f63b 100644
--- a/app/src/main/java/llc/slacker/openime/NumericKeyboardRenderer.kt
+++ b/app/src/main/java/llc/slacker/openime/NumericKeyboardRenderer.kt
@@ -71,13 +71,13 @@ internal class NumericKeyboardRenderer(
),
)
left.addView(
- createKey("符号", true, 13f, onShowSymbols).apply {
+ createKey("符号", true, ImeTypographyTokens.BODY_SP, onShowSymbols).apply {
markSideKey(this)
},
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
toPx(keyRowHeightDp()),
- ).apply { topMargin = toPx(ImeGeometryTokens.KEY_ROW_GAP_DP) },
+ ),
)
container.addView(left, adaptiveColumnParams(1f))
@@ -89,7 +89,7 @@ internal class NumericKeyboardRenderer(
val row = LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL }
rowDigits.forEach { digit ->
row.addView(
- createKey(digit, false, 22f) { onCommitCharacter(digit) }.apply {
+ createKey(digit, false, ImeTypographyTokens.KEY_LETTER_SP) { onCommitCharacter(digit) }.apply {
tag = "key:$digit"
markWhiteKey(this)
},
@@ -101,11 +101,7 @@ internal class NumericKeyboardRenderer(
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
toPx(keyRowHeightDp()),
- ).apply {
- if (rowIndex < DIGIT_ROWS.lastIndex) {
- bottomMargin = toPx(ImeGeometryTokens.KEY_ROW_GAP_DP)
- }
- },
+ ),
)
}
@@ -121,7 +117,7 @@ internal class NumericKeyboardRenderer(
orientation = LinearLayout.HORIZONTAL
}
centerBottom.addView(
- createKey("返回", true, 14f, onReturnToText).apply {
+ createKey("返回", true, ImeTypographyTokens.BODY_SP, onReturnToText).apply {
tag = "key:mode"
markSideKey(this)
},
@@ -130,7 +126,7 @@ internal class NumericKeyboardRenderer(
centerBottom.addView(
if (phone) {
val value = literal("key-space", "*")
- createKey(value, true, 14f) { onCommitCharacter(value) }.apply {
+ createKey(value, true, ImeTypographyTokens.BODY_SP) { onCommitCharacter(value) }.apply {
tag = "key-space"
markWhiteKey(this)
}
@@ -142,7 +138,7 @@ internal class NumericKeyboardRenderer(
flexKeyParams(),
)
centerBottom.addView(
- createKey(literal("key:.", "."), false, 22f) {
+ createKey(literal("key:.", "."), false, ImeTypographyTokens.KEY_LETTER_SP) {
onCommitCharacter(literal("key:.", "."))
}.apply {
tag = "key:."
@@ -165,26 +161,26 @@ internal class NumericKeyboardRenderer(
}
side.addView(
createBackspaceKey().apply { markSideKey(this) },
- sideKeyParams(includeBottomGap = true),
+ sideKeyParams(),
)
side.addView(
- createKey("0", false, 22f) { onCommitCharacter("0") }.apply {
+ createKey("0", false, ImeTypographyTokens.KEY_LETTER_SP) { onCommitCharacter("0") }.apply {
tag = "key:0"
markSideKey(this)
},
- sideKeyParams(includeBottomGap = true),
+ sideKeyParams(),
)
side.addView(
- createKey(literal("key:@", "@"), true, 15f) {
+ createKey(literal("key:@", "@"), true, ImeTypographyTokens.BODY_SP) {
onCommitCharacter(literal("key:@", "@"))
}.apply {
tag = "key:@"
markSideKey(this)
},
- sideKeyParams(includeBottomGap = true),
+ sideKeyParams(),
)
side.addView(
- createKey(enterLabel, true, 13f, onEnter).apply {
+ createKey(enterLabel, true, ImeTypographyTokens.BODY_SP, onEnter).apply {
tag = "key-enter"
markSideKey(this)
},
@@ -227,28 +223,20 @@ internal class NumericKeyboardRenderer(
0,
toPx(keyRowHeightDp()),
weight,
- ).apply {
- marginStart = toPx(ImeGeometryTokens.KEY_SIDE_MARGIN_DP)
- marginEnd = toPx(ImeGeometryTokens.KEY_SIDE_MARGIN_DP)
- }
+ )
- private fun sideKeyParams(includeBottomGap: Boolean = false) =
+ private fun sideKeyParams() =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
toPx(keyRowHeightDp()),
- ).apply {
- if (includeBottomGap) bottomMargin = toPx(ImeGeometryTokens.KEY_ROW_GAP_DP)
- }
+ )
private fun adaptiveColumnParams(weight: Float) =
LinearLayout.LayoutParams(
0,
toPx(nineBodyHeightDp()),
weight,
- ).apply {
- marginStart = toPx(ImeGeometryTokens.KEY_SIDE_MARGIN_DP)
- marginEnd = toPx(ImeGeometryTokens.KEY_SIDE_MARGIN_DP)
- }
+ )
private companion object {
const val DIGITS_RAIL_TAG = "digits-symbol-scroll"
diff --git a/app/src/main/java/llc/slacker/openime/PanelHeaderFactory.kt b/app/src/main/java/llc/slacker/openime/PanelHeaderFactory.kt
index 49117412..d06a755e 100644
--- a/app/src/main/java/llc/slacker/openime/PanelHeaderFactory.kt
+++ b/app/src/main/java/llc/slacker/openime/PanelHeaderFactory.kt
@@ -17,9 +17,24 @@ internal class PanelHeaderFactory(
private val previousPanel: () -> Panel?,
private val onBack: () -> Boolean,
private val onFeedback: () -> Unit,
+ private val standalone: Boolean = false,
) {
fun create(name: String): LinearLayout {
val backTarget = previousPanel()?.let(::titleFor) ?: "键盘"
+ if (standalone) {
+ return SetupUi.activityTopBar(
+ context = context,
+ title = name,
+ backContentDescription = "返回$backTarget",
+ ) {
+ onFeedback()
+ onBack()
+ }.apply {
+ tag = "panel-head"
+ getChildAt(0)?.tag = "key-panel-back"
+ getChildAt(1)?.tag = "panel-title"
+ }
+ }
return LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
diff --git a/app/src/main/java/llc/slacker/openime/PersonalizationRepository.kt b/app/src/main/java/llc/slacker/openime/PersonalizationRepository.kt
index 07c36c3b..bc44d132 100644
--- a/app/src/main/java/llc/slacker/openime/PersonalizationRepository.kt
+++ b/app/src/main/java/llc/slacker/openime/PersonalizationRepository.kt
@@ -70,6 +70,16 @@ object PersonalizationRepository {
}
}
+ fun forget(text: String) {
+ val value = normalize(text)
+ if (value.isEmpty()) return
+ synchronized(lock) {
+ if (entries.remove(value) != null) {
+ saveLocked()
+ }
+ }
+ }
+
fun clear() {
synchronized(lock) {
entries.clear()
diff --git a/app/src/main/java/llc/slacker/openime/Pinyin26KeyboardRenderer.kt b/app/src/main/java/llc/slacker/openime/Pinyin26KeyboardRenderer.kt
index 76df459f..04df68a0 100644
--- a/app/src/main/java/llc/slacker/openime/Pinyin26KeyboardRenderer.kt
+++ b/app/src/main/java/llc/slacker/openime/Pinyin26KeyboardRenderer.kt
@@ -69,7 +69,7 @@ internal class Pinyin26KeyboardRenderer(
val bottom = rowHost()
bottom.addView(
- createKey("123", true, null, 15f, 0, onDigits),
+ createKey("123", true, null, ImeTypographyTokens.BODY_SP, 0, onDigits),
flexKeyParams(weights.leftOuter),
)
val punctuation = if (english) "." else ","
@@ -78,7 +78,7 @@ internal class Pinyin26KeyboardRenderer(
punctuation,
true,
null,
- 15f,
+ ImeTypographyTokens.BODY_SP,
0,
) { onCommitCharacter(punctuation) },
flexKeyParams(weights.leftInner),
@@ -88,22 +88,22 @@ internal class Pinyin26KeyboardRenderer(
flexKeyParams(weights.space),
)
bottom.addView(
- createKey("中/英", true, null, 14f, 0, onModeSwitch).apply {
+ createKey("中/英", true, null, ImeTypographyTokens.BODY_SP, 0, onModeSwitch).apply {
tag = "key:mode"
},
flexKeyParams(weights.rightInner),
)
bottom.addView(
- createKey(enterLabel, true, null, 15f, 0, onEnter).apply {
+ createKey(enterLabel, true, null, ImeTypographyTokens.BODY_SP, 0, onEnter).apply {
tag = "key-enter"
},
flexKeyParams(weights.rightOuter),
)
- keyboardBody.addView(bottom, rowParams(includeBottomGap = false))
+ keyboardBody.addView(bottom, rowParams())
}
private fun segmentKey(): ImeKeyView =
- createKey("分词", true, "@#/", 12f, 0, onPinyinSegment).apply {
+ createKey("分词", true, "@#/", ImeTypographyTokens.BODY_SP, 0, onPinyinSegment).apply {
tag = "key-segment"
contentDescription = "分词,长按输入@井号或斜杠"
setOnLongClickListener {
@@ -139,14 +139,13 @@ internal class Pinyin26KeyboardRenderer(
base
}
val secondary = if (english) null else DIGIT_HINTS[character]
- return createKey(main, false, secondary, 20f, 0) {
+ return createKey(main, false, secondary, ImeTypographyTokens.KEY_LETTER_SP, 0) {
onLetter(base)
}.apply {
tag = "key:$base"
- // 26-key keeps the surface visually clean while preserving the
- // long-press digit action.
if (secondary != null) {
- setSecondaryVisible(false)
+ setSecondaryVisible(true)
+ setSecondaryAlpha(0.4f)
setOnLongClickListener {
onCommitCharacter(secondary)
true
@@ -161,23 +160,18 @@ internal class Pinyin26KeyboardRenderer(
layoutParams = rowParams()
}
- private fun rowParams(includeBottomGap: Boolean = true) =
+ private fun rowParams() =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
toPx(keyRowHeightDp()),
- ).apply {
- if (includeBottomGap) bottomMargin = toPx(ImeGeometryTokens.KEY_ROW_GAP_DP)
- }
+ )
private fun flexKeyParams(weight: Float = 1f) =
LinearLayout.LayoutParams(
0,
toPx(keyRowHeightDp()),
weight,
- ).apply {
- marginStart = toPx(ImeGeometryTokens.KEY_SIDE_MARGIN_DP)
- marginEnd = toPx(ImeGeometryTokens.KEY_SIDE_MARGIN_DP)
- }
+ )
private companion object {
val ROWS = listOf("qwertyuiop", "asdfghjkl", "zxcvbnm")
diff --git a/app/src/main/java/llc/slacker/openime/Pinyin9KeyboardRenderer.kt b/app/src/main/java/llc/slacker/openime/Pinyin9KeyboardRenderer.kt
index 635c9a76..b31724b8 100644
--- a/app/src/main/java/llc/slacker/openime/Pinyin9KeyboardRenderer.kt
+++ b/app/src/main/java/llc/slacker/openime/Pinyin9KeyboardRenderer.kt
@@ -58,13 +58,13 @@ internal class Pinyin9KeyboardRenderer(
),
)
left.addView(
- createKey("符号", true, null, 13f, onShowSymbols).apply {
+ createKey("符号", true, null, ImeTypographyTokens.BODY_SP, onShowSymbols).apply {
markSideKey(this)
},
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
toPx(keyRowHeightDp()),
- ).apply { topMargin = toPx(ImeGeometryTokens.KEY_ROW_GAP_DP) },
+ ),
)
container.addView(left, adaptiveColumnParams(1f))
@@ -80,7 +80,7 @@ internal class Pinyin9KeyboardRenderer(
orientation = LinearLayout.HORIZONTAL
}
centerBottom.addView(
- createKey("123", true, null, 13f, onDigits).apply {
+ createKey("123", true, null, ImeTypographyTokens.BODY_SP, onDigits).apply {
markSideKey(this)
},
flexKeyParams(0.925f),
@@ -90,7 +90,7 @@ internal class Pinyin9KeyboardRenderer(
flexKeyParams(3.4f),
)
centerBottom.addView(
- createKey("中/英", true, null, 13f, onModeSwitch).apply {
+ createKey("中/英", true, null, ImeTypographyTokens.BODY_SP, onModeSwitch).apply {
tag = "key:mode"
markSideKey(this)
},
@@ -111,16 +111,16 @@ internal class Pinyin9KeyboardRenderer(
}
side.addView(
createBackspaceKey().apply { markSideKey(this) },
- sideKeyParams(keyRowHeightDp(), includeBottomGap = true),
+ sideKeyParams(keyRowHeightDp()),
)
side.addView(
- createKey("重输", true, null, 13f, onRetranslate).apply {
+ createKey("重输", true, null, ImeTypographyTokens.BODY_SP, onRetranslate).apply {
markSideKey(this)
},
- sideKeyParams(keyRowHeightDp(), includeBottomGap = true),
+ sideKeyParams(keyRowHeightDp()),
)
side.addView(
- createKey(enterLabel, true, null, 13f, onEnter).apply {
+ createKey(enterLabel, true, null, ImeTypographyTokens.BODY_SP, onEnter).apply {
tag = "key-enter"
markSideKey(this)
},
@@ -147,7 +147,7 @@ internal class Pinyin9KeyboardRenderer(
if (segmentation) "分词" else subLabel,
false,
if (segmentation) "@#/" else null,
- if (segmentation) 12f else 17f,
+ if (segmentation) ImeTypographyTokens.BODY_SP else ImeTypographyTokens.KEY_LETTER_SP,
) {
if (segmentation) onPinyinSegment() else onNineKey(digit)
}.apply {
@@ -176,9 +176,7 @@ internal class Pinyin9KeyboardRenderer(
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
toPx(keyRowHeightDp()),
- ).apply {
- if (rowIndex < 2) bottomMargin = toPx(ImeGeometryTokens.KEY_ROW_GAP_DP)
- },
+ ),
)
}
return grid
@@ -189,30 +187,20 @@ internal class Pinyin9KeyboardRenderer(
0,
toPx(keyRowHeightDp()),
weight,
- ).apply {
- marginStart = toPx(ImeGeometryTokens.KEY_SIDE_MARGIN_DP)
- marginEnd = toPx(ImeGeometryTokens.KEY_SIDE_MARGIN_DP)
- }
+ )
- private fun sideKeyParams(
- heightDp: Int,
- includeBottomGap: Boolean = false,
- ) = LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- toPx(heightDp),
- ).apply {
- if (includeBottomGap) bottomMargin = toPx(ImeGeometryTokens.KEY_ROW_GAP_DP)
- }
+ private fun sideKeyParams(heightDp: Int) =
+ LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ toPx(heightDp),
+ )
private fun adaptiveColumnParams(weight: Float) =
LinearLayout.LayoutParams(
0,
toPx(nineBodyHeightDp()),
weight,
- ).apply {
- marginStart = toPx(ImeGeometryTokens.KEY_SIDE_MARGIN_DP)
- marginEnd = toPx(ImeGeometryTokens.KEY_SIDE_MARGIN_DP)
- }
+ )
private companion object {
val NINE_KEYS = listOf(
diff --git a/app/src/main/java/llc/slacker/openime/QuickPhraseEditActivity.kt b/app/src/main/java/llc/slacker/openime/QuickPhraseEditActivity.kt
index 09668a0a..e44cb403 100644
--- a/app/src/main/java/llc/slacker/openime/QuickPhraseEditActivity.kt
+++ b/app/src/main/java/llc/slacker/openime/QuickPhraseEditActivity.kt
@@ -1,19 +1,15 @@
package llc.slacker.openime
import android.app.Activity
-import android.content.res.ColorStateList
import android.os.Build
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
-import android.util.TypedValue
import android.view.Gravity
-import android.view.HapticFeedbackConstants
import android.view.View
import android.view.WindowInsets
import android.view.inputmethod.EditorInfo
import android.widget.EditText
-import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
@@ -25,13 +21,16 @@ class QuickPhraseEditActivity : Activity() {
const val EXTRA_ID = "quick_phrase_id"
const val EXTRA_CATEGORY = "quick_phrase_category"
const val EXTRA_TEXT = "quick_phrase_text"
+ const val EXTRA_INPUT_CODE = "quick_phrase_input_code"
}
private val density by lazy { resources.displayMetrics.density }
private lateinit var categoryEdit: EditText
+ private lateinit var codeEdit: EditText
private lateinit var phraseEdit: EditText
private var phraseId = 0L
private var initialCategory = ""
+ private var initialCode = ""
private var initialPhrase = ""
private var savedScrollY = 0
private var savedFocusId = R.id.quick_phrase_text_editor
@@ -47,13 +46,19 @@ class QuickPhraseEditActivity : Activity() {
?: R.id.quick_phrase_text_editor
initialCategory = savedInstanceState?.getString("baseline_category")
?: intent.getStringExtra(EXTRA_CATEGORY).orEmpty()
+ initialCode = savedInstanceState?.getString("baseline_code")
+ ?: intent.getStringExtra(EXTRA_INPUT_CODE).orEmpty()
initialPhrase = savedInstanceState?.getString("baseline_phrase")
?: intent.getStringExtra(EXTRA_TEXT).orEmpty()
val category = savedInstanceState?.getString("draft_category") ?: initialCategory
+ val code = savedInstanceState?.getString("draft_code") ?: initialCode
val phrase = savedInstanceState?.getString("draft_phrase") ?: initialPhrase
- render(category, phrase)
- if (savedFocusId == R.id.quick_phrase_category_editor) categoryEdit.requestFocus()
- else phraseEdit.requestFocus()
+ render(category, code, phrase)
+ when (savedFocusId) {
+ R.id.quick_phrase_category_editor -> categoryEdit.requestFocus()
+ R.id.quick_phrase_code_editor -> codeEdit.requestFocus()
+ else -> phraseEdit.requestFocus()
+ }
if (Build.VERSION.SDK_INT >= 33) {
backCallback = OnBackInvokedCallback { requestClose() }
onBackInvokedDispatcher.registerOnBackInvokedCallback(
@@ -79,8 +84,10 @@ class QuickPhraseEditActivity : Activity() {
override fun onSaveInstanceState(outState: Bundle) {
outState.putString("baseline_category", initialCategory)
+ outState.putString("baseline_code", initialCode)
outState.putString("baseline_phrase", initialPhrase)
outState.putString("draft_category", categoryEdit.text.toString())
+ outState.putString("draft_code", codeEdit.text.toString())
outState.putString("draft_phrase", phraseEdit.text.toString())
outState.putInt("focused_field", currentFocus?.id ?: savedFocusId)
outState.putInt(
@@ -90,35 +97,12 @@ class QuickPhraseEditActivity : Activity() {
super.onSaveInstanceState(outState)
}
- private fun render(category: String, phrase: String) {
- val accent = SetupUi.accent(this)
- val title = TextView(this).apply {
- text = if (phraseId > 0L) "编辑常用语" else "新增常用语"
- textSize = 22f
- setTextColor(getColor(R.color.setup_title))
- setTypeface(typeface, android.graphics.Typeface.BOLD)
- gravity = Gravity.CENTER_VERTICAL
- includeFontPadding = false
- if (Build.VERSION.SDK_INT >= 28) setAccessibilityHeading(true)
- }
- val header = LinearLayout(this).apply {
- orientation = LinearLayout.HORIZONTAL
- gravity = Gravity.CENTER_VERTICAL
- addView(ImageButton(this@QuickPhraseEditActivity).apply {
- setImageResource(R.drawable.ic_arrow_back)
- imageTintList = ColorStateList.valueOf(accent)
- contentDescription = "返回"
- setMinimumWidth(dp(ImeGeometryTokens.TOUCH_TARGET_DP))
- setMinimumHeight(dp(ImeGeometryTokens.TOUCH_TARGET_DP))
- isClickable = true
- isFocusable = true
- applySelectableBackground(this)
- setOnClickListener {
- it.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
- requestClose()
- }
- }, LinearLayout.LayoutParams(dp(ImeGeometryTokens.TOUCH_TARGET_DP), dp(56)))
- addView(title, LinearLayout.LayoutParams(0, dp(56), 1f))
+ private fun render(category: String, code: String, phrase: String) {
+ val header = SetupUi.activityTopBar(
+ context = this,
+ title = if (phraseId > 0L) "编辑常用语" else "新增常用语",
+ ) {
+ requestClose()
}
categoryEdit = EditText(this).apply {
@@ -127,9 +111,20 @@ class QuickPhraseEditActivity : Activity() {
setText(category)
setSingleLine(true)
imeOptions = EditorInfo.IME_ACTION_NEXT
- textSize = 16f
+ textSize = ImeTypographyTokens.BODY_SP
}
SetupUi.styleInput(this, categoryEdit)
+ codeEdit = EditText(this).apply {
+ id = R.id.quick_phrase_code_editor
+ hint = "例如:dz、mail、addr"
+ setText(code)
+ setSingleLine(true)
+ inputType = android.text.InputType.TYPE_CLASS_TEXT or
+ android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
+ imeOptions = EditorInfo.IME_ACTION_NEXT
+ textSize = ImeTypographyTokens.BODY_SP
+ }
+ SetupUi.styleInput(this, codeEdit)
phraseEdit = EditText(this).apply {
id = R.id.quick_phrase_text_editor
hint = "输入常用语"
@@ -138,7 +133,7 @@ class QuickPhraseEditActivity : Activity() {
maxLines = 8
gravity = Gravity.TOP or Gravity.START
imeOptions = EditorInfo.IME_ACTION_DONE
- textSize = 17f
+ textSize = ImeTypographyTokens.BODY_SP
}
SetupUi.styleInput(this, phraseEdit)
val save = SetupUi.primaryButton(this, "保存") {
@@ -150,6 +145,7 @@ class QuickPhraseEditActivity : Activity() {
phraseId,
categoryEdit.text.toString(),
phraseEdit.text.toString(),
+ inputCode = codeEdit.text.toString(),
) != null
) {
finish()
@@ -158,7 +154,7 @@ class QuickPhraseEditActivity : Activity() {
fun refreshSaveState() {
val valid = phraseEdit.text.toString().isNotBlank()
save.isEnabled = valid
- save.alpha = if (valid) 1f else 0.38f
+ save.alpha = if (valid) 1f else ImeSurfacePolicy.DISABLED_ALPHA
save.contentDescription = if (valid) {
"保存"
} else {
@@ -179,6 +175,14 @@ class QuickPhraseEditActivity : Activity() {
requestClose()
}
categoryEdit.setOnEditorActionListener { _, actionId, _ ->
+ if (actionId == EditorInfo.IME_ACTION_NEXT) {
+ codeEdit.requestFocus()
+ true
+ } else {
+ false
+ }
+ }
+ codeEdit.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_NEXT) {
phraseEdit.requestFocus()
true
@@ -215,6 +219,23 @@ class QuickPhraseEditActivity : Activity() {
LinearLayout.LayoutParams.MATCH_PARENT,
dp(ImeGeometryTokens.FIELD_HEIGHT_DP),
).apply { bottomMargin = dp(14) })
+ addView(fieldLabel("输入码(可选)", R.id.quick_phrase_code_editor), LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ ))
+ addView(codeEdit, LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ dp(ImeGeometryTokens.FIELD_HEIGHT_DP),
+ ).apply { bottomMargin = dp(14) })
+ addView(TextView(this@QuickPhraseEditActivity).apply {
+ text = "设置后,输入至少 2 个字符的短码即可在候选栏召回这条常用语。"
+ textSize = ImeTypographyTokens.BODY_SP
+ setTextColor(getColor(R.color.setup_body))
+ setPadding(dp(4), 0, dp(4), dp(12))
+ }, LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ ))
addView(fieldLabel("常用语内容", R.id.quick_phrase_text_editor), LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT,
@@ -225,7 +246,7 @@ class QuickPhraseEditActivity : Activity() {
).apply { bottomMargin = dp(8) })
addView(TextView(this@QuickPhraseEditActivity).apply {
text = "保存后会在剪贴板面板中按分类显示,可直接点选输入。"
- textSize = 12f
+ textSize = ImeTypographyTokens.BODY_SP
setTextColor(getColor(R.color.setup_body))
setPadding(dp(4), 0, dp(4), dp(12))
}, LinearLayout.LayoutParams(
@@ -242,8 +263,8 @@ class QuickPhraseEditActivity : Activity() {
setPadding(dp(20), dp(24), dp(20), dp(24))
addView(header, LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
- dp(56),
- ).apply { bottomMargin = dp(12) })
+ dp(ImeGeometryTokens.TOP_BAR_HEIGHT_DP),
+ ).apply { bottomMargin = dp(ImeSpacingTokens.MD_DP) })
addView(form, LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT,
@@ -276,15 +297,19 @@ class QuickPhraseEditActivity : Activity() {
private fun fieldLabel(label: String, targetId: Int) = TextView(this).apply {
text = label
- textSize = 12f
+ textSize = ImeTypographyTokens.BODY_SP
setTextColor(getColor(R.color.setup_body))
setPadding(dp(4), 0, dp(4), dp(4))
labelFor = targetId
}
private fun requestClose() {
- if (!::categoryEdit.isInitialized || !::phraseEdit.isInitialized ||
- (categoryEdit.text.toString() == initialCategory && phraseEdit.text.toString() == initialPhrase)
+ if (!::categoryEdit.isInitialized || !::codeEdit.isInitialized || !::phraseEdit.isInitialized ||
+ (
+ categoryEdit.text.toString() == initialCategory &&
+ codeEdit.text.toString() == initialCode &&
+ phraseEdit.text.toString() == initialPhrase
+ )
) {
finish()
return
@@ -305,16 +330,5 @@ class QuickPhraseEditActivity : Activity() {
dialog.show()
}
- private fun applySelectableBackground(view: View) {
- val value = TypedValue()
- if (
- theme.resolveAttribute(
- android.R.attr.selectableItemBackgroundBorderless,
- value,
- true,
- ) && value.resourceId != 0
- ) {
- view.setBackgroundResource(value.resourceId)
- }
- }
+
}
diff --git a/app/src/main/java/llc/slacker/openime/QuickPhraseRepository.kt b/app/src/main/java/llc/slacker/openime/QuickPhraseRepository.kt
index f413db15..88cc5396 100644
--- a/app/src/main/java/llc/slacker/openime/QuickPhraseRepository.kt
+++ b/app/src/main/java/llc/slacker/openime/QuickPhraseRepository.kt
@@ -8,17 +8,34 @@ data class QuickPhrase(
val id: Long,
val category: String,
val text: String,
+ val inputCode: String = "",
)
/** Persistent user-editable quick phrases used by the clipboard panel. */
object QuickPhraseRepository {
private const val PREFS = "ime_quick_phrases"
private const val KEY_ITEMS = "items"
+ private val cacheLock = Any()
+ private var cachedRaw: String? = null
+ private var cachedItems: List? = null
fun load(context: Context): List {
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
- val raw = prefs.getString(KEY_ITEMS, null) ?: return defaults()
- return runCatching {
+ val raw = prefs.getString(KEY_ITEMS, null)
+ synchronized(cacheLock) {
+ if (cachedRaw == raw && cachedItems != null) {
+ return cachedItems.orEmpty()
+ }
+ }
+ if (raw == null) {
+ val defaults = defaults()
+ synchronized(cacheLock) {
+ cachedRaw = null
+ cachedItems = defaults
+ }
+ return defaults
+ }
+ val parsed = runCatching {
val array = JSONArray(raw)
buildList {
for (index in 0 until array.length()) {
@@ -30,27 +47,71 @@ object QuickPhraseRepository {
id = item.optLong("id", index.toLong() + 1L),
category = item.optString("category", "常用").ifBlank { "常用" },
text = text,
+ inputCode = normalizeInputCode(item.optString("input_code", "")),
),
)
}
}
}
}.getOrElse { defaults() }
+ synchronized(cacheLock) {
+ cachedRaw = raw
+ cachedItems = parsed
+ }
+ return parsed
}
- fun upsert(context: Context, id: Long, category: String, text: String): QuickPhrase? {
+ fun upsert(
+ context: Context,
+ id: Long,
+ category: String,
+ text: String,
+ inputCode: String? = null,
+ ): QuickPhrase? {
val value = text.trim()
if (value.isEmpty()) return null
val items = load(context).toMutableList()
val safeCategory = category.trim().ifBlank { "常用" }
val actualId = if (id > 0L) id else nextId(items)
- val updated = QuickPhrase(actualId, safeCategory, value)
+ val existing = items.firstOrNull { it.id == actualId }
+ val safeCode = if (inputCode == null) {
+ existing?.inputCode.orEmpty()
+ } else {
+ normalizeInputCode(inputCode)
+ }
+ val updated = QuickPhrase(actualId, safeCategory, value, safeCode)
val index = items.indexOfFirst { it.id == actualId }
if (index >= 0) items[index] = updated else items.add(updated)
save(context, items)
return updated
}
+ fun candidatesForInputCode(
+ context: Context,
+ rawCode: String,
+ exactOnly: Boolean = false,
+ limit: Int = 8,
+ ): List {
+ val code = normalizeInputCode(rawCode)
+ if (code.length < 2 || limit <= 0) return emptyList()
+ val phrases = load(context)
+ val exact = phrases.asSequence()
+ .filter { it.inputCode == code }
+ .map { it.text }
+ val prefix = if (exactOnly) {
+ emptySequence()
+ } else {
+ phrases.asSequence()
+ .filter { it.inputCode.length > code.length && it.inputCode.startsWith(code) }
+ .map { it.text }
+ }
+ return (exact + prefix)
+ .filter { it.isNotBlank() }
+ .distinct()
+ .take(limit)
+ .toList()
+ }
+
fun remove(context: Context, id: Long) {
save(context, load(context).filterNot { it.id == id })
}
@@ -63,12 +124,19 @@ object QuickPhraseRepository {
id = (categoryIndex + 1L) * 10_000L + phraseIndex + 1L,
category = category,
text = text,
+ inputCode = "",
),
)
}
}
}
+ private fun normalizeInputCode(value: String): String =
+ value.trim()
+ .lowercase()
+ .filter { it in 'a'..'z' || it in '0'..'9' || it == '_' || it == '-' }
+ .take(24)
+
private fun nextId(items: List): Long =
(items.maxOfOrNull { it.id } ?: 0L) + 1L
@@ -79,12 +147,18 @@ object QuickPhraseRepository {
JSONObject()
.put("id", item.id)
.put("category", item.category)
- .put("text", item.text),
+ .put("text", item.text)
+ .put("input_code", item.inputCode),
)
}
+ val raw = array.toString()
+ synchronized(cacheLock) {
+ cachedRaw = raw
+ cachedItems = items.toList()
+ }
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.edit()
- .putString(KEY_ITEMS, array.toString())
+ .putString(KEY_ITEMS, raw)
.apply()
}
}
diff --git a/app/src/main/java/llc/slacker/openime/RimeEngine.kt b/app/src/main/java/llc/slacker/openime/RimeEngine.kt
index 4194f79c..e36bf1d9 100644
--- a/app/src/main/java/llc/slacker/openime/RimeEngine.kt
+++ b/app/src/main/java/llc/slacker/openime/RimeEngine.kt
@@ -290,6 +290,148 @@ class RimeEngine(
return ""
}
+ fun isUserLearnedCandidate(
+ input: String,
+ candidate: String,
+ onComplete: (Boolean) -> Unit,
+ ): Boolean {
+ val normalized = RimeInputNormalizer.normalize(input)
+ val visible = candidate.trim()
+ if (!isReady || normalized.isBlank() || visible.isEmpty()) return false
+
+ return mutationQueue.submit {
+ val learned = synchronized(lock) {
+ if (!isReady) {
+ false
+ } else {
+ runCatching {
+ if (!syncSchemaFromSettingsLocked()) return@runCatching false
+ val snapshot = RimeNative.nativeSetInput(normalized)
+ val entry = snapshotCandidateEntries(snapshot)
+ .firstOrNull { it.text == visible }
+ ?: return@runCatching false
+ RimeNative.nativeIsUserLearnedCandidate(entry.nativeIndex)
+ }.getOrDefault(false)
+ }
+ }
+ onComplete(learned)
+ }
+ }
+
+ /**
+ * Delete one user-learned candidate without blocking the IME thread.
+ * Built-in dictionary entries are not deletable and report false.
+ */
+ fun deleteCandidate(
+ input: String,
+ candidate: String,
+ onComplete: (Boolean) -> Unit,
+ ): Boolean {
+ val normalized = RimeInputNormalizer.normalize(input)
+ val visible = candidate.trim()
+ if (!isReady || normalized.isBlank() || visible.isEmpty()) return false
+
+ return mutationQueue.submit {
+ val deleted = synchronized(lock) {
+ if (!isReady) {
+ false
+ } else {
+ runCatching {
+ if (!syncSchemaFromSettingsLocked()) return@runCatching false
+ val snapshot = RimeNative.nativeSetInput(normalized)
+ val entry = snapshotCandidateEntries(snapshot)
+ .firstOrNull { it.text == visible }
+ ?: return@runCatching false
+ if (!RimeNative.nativeIsUserLearnedCandidate(entry.nativeIndex)) {
+ return@runCatching false
+ }
+ RimeNative.nativeDeleteCandidate(entry.nativeIndex)
+ }.getOrDefault(false)
+ }
+ }
+ onComplete(deleted)
+ }
+ }
+
+ internal fun exportUserDictionaries(
+ targetDir: File,
+ onComplete: (List?) -> Unit,
+ ): Boolean {
+ if (!isReady) return false
+ return mutationQueue.submit {
+ val exported: List? = synchronized(lock) {
+ if (!isReady) return@synchronized null
+ try {
+ if (!syncSchemaFromSettingsLocked()) return@synchronized null
+ if (!targetDir.exists() && !targetDir.mkdirs()) {
+ return@synchronized null
+ }
+ val mappings =
+ RimeNative.nativeExportUserDictionaries(targetDir.absolutePath)
+ ?: return@synchronized null
+ val result = mutableListOf()
+ for (mapping in mappings) {
+ val separator = mapping.indexOf('\t')
+ if (separator <= 0 || separator >= mapping.lastIndex) {
+ return@synchronized null
+ }
+ val name = mapping.substring(0, separator).trim()
+ val path = mapping.substring(separator + 1)
+ val file = File(path)
+ if (
+ !UserDataArchiveCodec.isSafeRimeDictionaryName(name) ||
+ !file.isFile
+ ) {
+ return@synchronized null
+ }
+ result += RimeUserDictionaryArchive(
+ name = name,
+ content = file.readText(),
+ )
+ }
+ result
+ } catch (error: Exception) {
+ Log.w(TAG, "Rime user dictionary export failed", error)
+ null
+ }
+ }
+ onComplete(exported)
+ }
+ }
+
+ internal fun importUserDictionaries(
+ sourceDir: File,
+ dictionaries: List,
+ onComplete: (Int?) -> Unit,
+ ): Boolean {
+ if (!isReady) return false
+ return mutationQueue.submit {
+ val imported = synchronized(lock) {
+ if (!isReady) {
+ null
+ } else {
+ runCatching {
+ if (!syncSchemaFromSettingsLocked()) return@runCatching null
+ sourceDir.mkdirs()
+ var total = 0
+ dictionaries.forEachIndexed { index, item ->
+ val file = File(sourceDir, "dict-$index.userdb.txt")
+ file.writeText(item.content)
+ val count = RimeNative.nativeImportUserDictionary(
+ item.name,
+ file.absolutePath,
+ )
+ if (count < 0) return@runCatching null
+ total += count
+ }
+ total
+ }.getOrNull()
+ }
+ }
+ onComplete(imported)
+ }
+ }
+
fun commitFirst(input: String, allowLearning: Boolean = true): String {
if (!allowLearning) return candidates(input).firstOrNull().orEmpty()
val normalized = RimeInputNormalizer.normalize(input)
diff --git a/app/src/main/java/llc/slacker/openime/RimeNative.kt b/app/src/main/java/llc/slacker/openime/RimeNative.kt
index b35d1650..764d57ac 100644
--- a/app/src/main/java/llc/slacker/openime/RimeNative.kt
+++ b/app/src/main/java/llc/slacker/openime/RimeNative.kt
@@ -26,6 +26,23 @@ internal object RimeNative {
@JvmStatic
external fun nativeSelectCandidate(index: Int): String?
+ /** True only when the active visible candidate is backed by Rime user data. */
+ @JvmStatic
+ external fun nativeIsUserLearnedCandidate(index: Int): Boolean
+
+ /** Remove a deletable candidate from the active Rime user dictionary. */
+ @JvmStatic
+ external fun nativeDeleteCandidate(index: Int): Boolean
+
+ /** Export every available Rime user dictionary as UTF-8 text files. */
+ @JvmStatic
+ external fun nativeExportUserDictionaries(targetDir: String): Array?
+
+ /** Merge one UTF-8 user dictionary snapshot back into the named Rime dictionary. */
+ @JvmStatic
+ external fun nativeImportUserDictionary(dictName: String, sourceFile: String): Int
+
+
@JvmStatic
external fun nativeCommitFirst(): String?
diff --git a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt
index 48fa0f2b..ffffd5a3 100644
--- a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt
+++ b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt
@@ -51,11 +51,19 @@ internal class SettingsPanelController(
private val currentSkinRadius: () -> Int,
private val currentSkinFontSize: () -> Int,
private val currentSkinColor: () -> String,
+ private val currentHandedness: () -> ImeHandedness,
+ private val currentKeyboardHeightPercent: () -> Int,
+ private val currentFloatingWidthPercent: () -> Int,
+ private val currentFloatingOpacityPercent: () -> Int,
private val onThemeSelected: (ImeTheme) -> Unit,
private val onAppearanceSelected: (ImeAppearance) -> Unit,
private val onToggleChanged: (String, Boolean) -> Unit,
private val onSkinChanged: (Int, Int, Int, String) -> Unit,
+ private val onHandednessChanged: (ImeHandedness) -> Unit,
+ private val onKeyboardHeightChanged: (Int) -> Unit,
+ private val onFloatingStyleChanged: (Int, Int) -> Unit,
private val onShowFuzzySettings: () -> Unit,
+ private val onOpenAboutData: () -> Unit,
private val onFeedback: () -> Unit,
private val applyTheme: () -> Unit,
private val onHierarchyRebuilt: () -> Unit,
@@ -84,7 +92,7 @@ internal class SettingsPanelController(
createHeader("偏好设置"),
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
- toPx(ImeGeometryTokens.TOUCH_TARGET_DP),
+ toPx(ImeGeometryTokens.TOP_BAR_HEIGHT_DP),
),
)
} else {
@@ -138,6 +146,55 @@ internal class SettingsPanelController(
chipParams(),
)
+ content.addView(createSectionTitle("键盘布局"), wrapParams())
+ content.addView(
+ createChipScroll(
+ ImeHandedness.entries.map { it.label },
+ currentHandedness().label,
+ ) { label ->
+ ImeHandedness.entries.firstOrNull { it.label == label }?.let { selected ->
+ onHandednessChanged(selected)
+ renderSettings(reusePanel = true)
+ }
+ },
+ chipParams(),
+ )
+ content.addView(
+ settingGroup(
+ settingsSlider(
+ "键盘高度",
+ 92,
+ 120,
+ currentKeyboardHeightPercent(),
+ onKeyboardHeightChanged,
+ ),
+ ),
+ groupParams(),
+ )
+
+ content.addView(createSectionTitle("浮动键盘"), wrapParams())
+ content.addView(
+ settingGroup(
+ settingsSlider(
+ "浮动宽度",
+ 72,
+ 96,
+ currentFloatingWidthPercent(),
+ ) { width ->
+ onFloatingStyleChanged(width, currentFloatingOpacityPercent())
+ },
+ settingsSlider(
+ "浮动透明度",
+ 82,
+ 100,
+ currentFloatingOpacityPercent(),
+ ) { opacity ->
+ onFloatingStyleChanged(currentFloatingWidthPercent(), opacity)
+ },
+ ),
+ groupParams(),
+ )
+
content.addView(createSectionTitle("强调色"), wrapParams())
content.addView(
accentColorRow(),
@@ -200,6 +257,18 @@ internal class SettingsPanelController(
groupParams(),
)
+ content.addView(createSectionTitle("数据"), wrapParams())
+ content.addView(
+ settingGroup(
+ settingNavigationRow(
+ "关于与数据",
+ "版本、隐私、导出与导入",
+ onOpenAboutData,
+ ),
+ ),
+ groupParams(),
+ )
+
scroll.addView(
content,
ViewGroup.LayoutParams(
@@ -340,6 +409,7 @@ internal class SettingsPanelController(
"触感震动" -> R.drawable.ic_vibration
"按键气泡" -> R.drawable.ic_bubble
"模糊音与智能纠错", "启用模糊音" -> R.drawable.ic_tune
+ "关于与数据" -> R.drawable.ic_info
else -> R.drawable.ic_tune
},
)
@@ -382,9 +452,9 @@ internal class SettingsPanelController(
LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER_VERTICAL
- addView(labelText(label, 14f), wrapParams())
+ addView(labelText(label, ImeTypographyTokens.BODY_SP), wrapParams())
addView(
- labelText(sub, 11f).apply {
+ labelText(sub, ImeTypographyTokens.CAPTION_SP).apply {
setPadding(0, toPx(3), 0, 0)
},
wrapParams(),
@@ -431,9 +501,9 @@ internal class SettingsPanelController(
LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER_VERTICAL
- addView(labelText(label, 14f), wrapParams())
+ addView(labelText(label, ImeTypographyTokens.BODY_SP), wrapParams())
addView(
- labelText(sub, 11f).apply {
+ labelText(sub, ImeTypographyTokens.CAPTION_SP).apply {
setPadding(0, toPx(3), 0, 0)
},
wrapParams(),
@@ -447,10 +517,16 @@ internal class SettingsPanelController(
),
)
addView(
- TextView(context).apply {
- text = "›"
- textSize = 18f
- gravity = Gravity.CENTER
+ ImageView(context).apply {
+ setImageResource(R.drawable.ic_arrow_back)
+ rotation = 180f
+ imageTintList = ColorStateList.valueOf(
+ ImeTheme.IOS.tokens(
+ appearance = currentAppearance(),
+ accentOverride = AccentPalette.parse(currentSkinColor()),
+ ).keySecondaryText,
+ )
+ scaleType = ImageView.ScaleType.CENTER_INSIDE
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
tag = "setting-chevron"
},
@@ -470,10 +546,7 @@ internal class SettingsPanelController(
).apply {
gravity = Gravity.START or Gravity.CENTER_VERTICAL
}
- background = ImeDrawableFactory.rounded(
- Color.WHITE,
- toPx(ImeGeometryTokens.PILL_RADIUS_DP),
- )
+ tag = "toggle-knob"
translationX =
if (isOn) toPx(ImeGeometryTokens.SWITCH_KNOB_TRAVEL_DP).toFloat()
else 0f
@@ -592,11 +665,14 @@ internal class SettingsPanelController(
)
if (selected) {
addView(
- TextView(context).apply {
- text = "✓"
- textSize = ImeTypographyTokens.PANEL_BODY_SP
- gravity = Gravity.CENTER
- includeFontPadding = false
+ ImageView(context).apply {
+ setImageResource(R.drawable.ic_check)
+ imageTintList = ColorStateList.valueOf(
+ ImeDrawableFactory.contrastText(
+ AccentPalette.parse(hex),
+ ),
+ )
+ scaleType = ImageView.ScaleType.CENTER_INSIDE
tag = "accent-selected-mark:$hex"
importantForAccessibility =
View.IMPORTANT_FOR_ACCESSIBILITY_NO
@@ -767,7 +843,8 @@ internal class SettingsPanelController(
),
)
val valueView = TextView(context).apply {
- textSize = 12f
+ tag = "setting-value"
+ textSize = ImeTypographyTokens.BODY_SP
gravity = Gravity.CENTER
includeFontPadding = false
minWidth = toPx(ImeGeometryTokens.TOUCH_TARGET_DP)
@@ -777,6 +854,7 @@ internal class SettingsPanelController(
"圆角" -> " dp"
"不透明度" -> "%"
"按键字号" -> " sp"
+ "键盘高度", "浮动宽度", "浮动透明度" -> "%"
else -> ""
}
val seekBar = SeekBar(context).apply {
@@ -853,6 +931,7 @@ internal class SettingsPanelController(
text = value
textSize = size
includeFontPadding = false
+ tag = if (size <= 11.5f) "panel-note" else "setting-label"
}
private fun chipParams() =
diff --git a/app/src/main/java/llc/slacker/openime/SetupUi.kt b/app/src/main/java/llc/slacker/openime/SetupUi.kt
index b5dc172c..1bda3365 100644
--- a/app/src/main/java/llc/slacker/openime/SetupUi.kt
+++ b/app/src/main/java/llc/slacker/openime/SetupUi.kt
@@ -5,16 +5,19 @@ import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
-import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.StateListDrawable
import android.os.Build
+import android.util.TypedValue
+import android.view.Gravity
+import android.view.HapticFeedbackConstants
+import android.widget.ImageButton
+import android.widget.LinearLayout
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
/** Shared visual primitives for the non-IME setup and editor screens. */
object SetupUi {
- private const val DESTRUCTIVE = "#F4212E"
fun dp(context: Context, value: Int): Int =
(value * context.resources.displayMetrics.density).toInt()
@@ -22,79 +25,107 @@ object SetupUi {
fun accent(context: Context): Int =
AccentPalette.parse(ImeSettingsRepository.loadSkinColor(context))
+ private fun tokens(context: Context): ImeTheme.Tokens {
+ val nightMask =
+ context.resources.configuration.uiMode and
+ android.content.res.Configuration.UI_MODE_NIGHT_MASK
+ return ImeTheme.IOS.tokens(
+ appearance = ImeSettingsRepository.loadAppearance(context),
+ systemDark = nightMask == android.content.res.Configuration.UI_MODE_NIGHT_YES,
+ accentOverride = accent(context),
+ )
+ }
+
fun contrastText(background: Int): Int = ImeDrawableFactory.contrastText(background)
+ fun rounded(
+ color: Int,
+ radiusPx: Float,
+ strokeColor: Int? = null,
+ strokeWidthPx: Int = 1,
+ ) = ImeDrawableFactory.rounded(
+ color = color,
+ radiusPx = radiusPx,
+ strokeColor = strokeColor,
+ strokeWidthPx = strokeWidthPx,
+ )
+
fun dim(color: Int, factor: Float): Int =
ImeDrawableFactory.dim(color, factor, preserveAlpha = false)
- fun rounded(color: Int, radiusDp: Float, strokeColor: Int? = null): GradientDrawable =
- ImeDrawableFactory.rounded(
- color = color,
- radiusPx = radiusDp,
- strokeColor = strokeColor,
- strokeWidthPx = 1,
- )
-
fun buttonBackground(
context: Context,
color: Int,
radiusDp: Float = ImeGeometryTokens.CONTROL_RADIUS_DP.toFloat(),
): StateListDrawable {
- val pressed = dim(color, 0.86f)
+ val pressed =
+ if (color == accent(context)) tokens(context).accentPressed else dim(color, 0.86f)
return StateListDrawable().apply {
addState(
intArrayOf(android.R.attr.state_enabled, android.R.attr.state_pressed),
- rounded(pressed, dp(context, radiusDp.toInt()).toFloat()),
+ ImeDrawableFactory.rounded(
+ pressed,
+ dp(context, radiusDp.toInt()).toFloat(),
+ ),
)
addState(
intArrayOf(android.R.attr.state_enabled, android.R.attr.state_focused),
- rounded(
- color,
- dp(context, radiusDp.toInt()).toFloat(),
- contrastText(color),
+ ImeDrawableFactory.rounded(
+ color = color,
+ radiusPx = dp(context, radiusDp.toInt()).toFloat(),
+ strokeColor = contrastText(color),
+ strokeWidthPx = dp(context, 1),
),
)
addState(
intArrayOf(-android.R.attr.state_enabled),
- rounded(
+ ImeDrawableFactory.rounded(
context.getColor(R.color.setup_muted),
dp(context, radiusDp.toInt()).toFloat(),
),
)
addState(
intArrayOf(),
- rounded(color, dp(context, radiusDp.toInt()).toFloat()),
+ ImeDrawableFactory.rounded(
+ color,
+ dp(context, radiusDp.toInt()).toFloat(),
+ ),
)
}
}
/** Secondary rounded-rectangle control used by setup actions. */
- fun mutedPillBackground(context: Context): StateListDrawable {
+ fun secondaryBackground(context: Context): StateListDrawable {
val surface = context.getColor(R.color.setup_muted)
val pressed = context.getColor(R.color.setup_muted_pressed)
val accent = accent(context)
- val radius = dp(context, ImeGeometryTokens.SETUP_PILL_RADIUS_DP).toFloat()
+ val radius = dp(context, ImeGeometryTokens.CONTROL_RADIUS_DP).toFloat()
return StateListDrawable().apply {
addState(
intArrayOf(android.R.attr.state_enabled, android.R.attr.state_pressed),
- rounded(pressed, radius),
+ ImeDrawableFactory.rounded(pressed, radius),
)
addState(
intArrayOf(android.R.attr.state_focused),
- rounded(surface, radius, accent),
+ ImeDrawableFactory.rounded(
+ color = surface,
+ radiusPx = radius,
+ strokeColor = accent,
+ strokeWidthPx = dp(context, 1),
+ ),
)
addState(
intArrayOf(-android.R.attr.state_enabled),
- rounded(dim(surface, 0.72f), radius),
+ ImeDrawableFactory.rounded(dim(surface, 0.72f), radius),
)
- addState(intArrayOf(), rounded(surface, radius))
+ addState(intArrayOf(), ImeDrawableFactory.rounded(surface, radius))
}
}
fun secondaryButton(context: Context, label: String, onClick: () -> Unit): Button =
Button(context).apply {
text = label
- textSize = 12f
+ textSize = ImeTypographyTokens.BODY_SP
isAllCaps = false
minHeight = dp(context, 48)
minWidth = 0
@@ -115,20 +146,38 @@ object SetupUi {
return StateListDrawable().apply {
addState(
intArrayOf(android.R.attr.state_pressed),
- rounded(dim(surface, 0.94f), radius, line),
+ ImeDrawableFactory.rounded(
+ color = dim(surface, 0.94f),
+ radiusPx = radius,
+ strokeColor = line,
+ strokeWidthPx = dp(context, 1),
+ ),
)
addState(
intArrayOf(android.R.attr.state_focused),
- rounded(surface, radius, accent(context)),
+ ImeDrawableFactory.rounded(
+ color = surface,
+ radiusPx = radius,
+ strokeColor = accent(context),
+ strokeWidthPx = dp(context, 1),
+ ),
+ )
+ addState(
+ intArrayOf(),
+ ImeDrawableFactory.rounded(
+ color = surface,
+ radiusPx = radius,
+ strokeColor = line,
+ strokeWidthPx = dp(context, 1),
+ ),
)
- addState(intArrayOf(), rounded(surface, radius, line))
}
}
fun primaryButton(context: Context, label: String, onClick: () -> Unit): Button =
Button(context).apply {
text = label
- textSize = 15f
+ textSize = ImeTypographyTokens.BODY_SP
isAllCaps = false
minHeight = dp(context, ImeGeometryTokens.PRIMARY_ROW_HEIGHT_DP)
background = buttonBackground(
@@ -178,13 +227,18 @@ object SetupUi {
radiusPx = radius,
focusedStrokeColor = accent,
defaultStrokeColor = null,
- strokeWidthPx = dp(context, 2),
+ strokeWidthPx = dp(context, ImeSpacingTokens.XXS_DP),
)
}
fun styleInput(context: Context, input: EditText) {
input.background = inputBackground(context)
- input.setPadding(dp(context, 16), dp(context, 8), dp(context, 16), dp(context, 8))
+ input.setPadding(
+ dp(context, ImeSpacingTokens.LG_DP),
+ dp(context, ImeSpacingTokens.SM_DP),
+ dp(context, ImeSpacingTokens.LG_DP),
+ dp(context, ImeSpacingTokens.SM_DP),
+ )
input.setTextColor(context.getColor(R.color.setup_title))
input.setHintTextColor(context.getColor(R.color.setup_muted_text))
styleCursor(context, input)
@@ -204,6 +258,73 @@ object SetupUi {
}
}
+ fun activityTopBar(
+ context: Context,
+ title: String,
+ backContentDescription: String = "返回",
+ onBack: () -> Unit,
+ ): LinearLayout =
+ LinearLayout(context).apply {
+ orientation = LinearLayout.HORIZONTAL
+ gravity = Gravity.CENTER_VERTICAL
+ minimumHeight = dp(context, ImeGeometryTokens.TOP_BAR_HEIGHT_DP)
+
+ addView(
+ ImageButton(context).apply {
+ setImageResource(R.drawable.ic_arrow_back)
+ imageTintList = ColorStateList.valueOf(accent(context))
+ scaleType = android.widget.ImageView.ScaleType.CENTER
+ setPadding(
+ dp(context, ImeSpacingTokens.MD_DP),
+ dp(context, ImeSpacingTokens.LG_DP),
+ dp(context, ImeSpacingTokens.MD_DP),
+ dp(context, ImeSpacingTokens.LG_DP),
+ )
+ contentDescription = backContentDescription
+ minimumWidth = dp(context, ImeGeometryTokens.TOUCH_TARGET_DP)
+ minimumHeight = dp(context, ImeGeometryTokens.TOUCH_TARGET_DP)
+ isClickable = true
+ isFocusable = true
+ val selectable = TypedValue()
+ if (
+ context.theme.resolveAttribute(
+ android.R.attr.selectableItemBackgroundBorderless,
+ selectable,
+ true,
+ ) && selectable.resourceId != 0
+ ) {
+ setBackgroundResource(selectable.resourceId)
+ } else {
+ background = null
+ }
+ setOnClickListener {
+ performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
+ onBack()
+ }
+ },
+ LinearLayout.LayoutParams(
+ dp(context, ImeGeometryTokens.TOUCH_TARGET_DP),
+ dp(context, ImeGeometryTokens.TOP_BAR_HEIGHT_DP),
+ ),
+ )
+ addView(
+ TextView(context).apply {
+ text = title
+ textSize = ImeTypographyTokens.TITLE_SP
+ setTextColor(context.getColor(R.color.setup_title))
+ setTypeface(typeface, android.graphics.Typeface.BOLD)
+ gravity = Gravity.CENTER_VERTICAL
+ includeFontPadding = false
+ if (Build.VERSION.SDK_INT >= 28) setAccessibilityHeading(true)
+ },
+ LinearLayout.LayoutParams(
+ 0,
+ dp(context, ImeGeometryTokens.TOP_BAR_HEIGHT_DP),
+ 1f,
+ ),
+ )
+ }
+
fun styleDialog(dialog: AlertDialog, context: Context, destructivePositive: Boolean = false) {
val accent = accent(context)
val alertTitleId = context.resources.getIdentifier("alertTitle", "id", "android")
@@ -216,12 +337,12 @@ object SetupUi {
context.getColor(R.color.setup_body),
)
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(
- if (destructivePositive) Color.parseColor(DESTRUCTIVE) else accent,
+ if (destructivePositive) tokens(context).danger else accent,
)
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(accent)
dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.setTextColor(accent)
dialog.window?.setBackgroundDrawable(
- rounded(
+ ImeDrawableFactory.rounded(
context.getColor(R.color.setup_surface),
dp(context, ImeGeometryTokens.DIALOG_RADIUS_DP).toFloat(),
),
diff --git a/app/src/main/java/llc/slacker/openime/SpaceVoiceGestureController.kt b/app/src/main/java/llc/slacker/openime/SpaceVoiceGestureController.kt
index d96b4ace..e69b4299 100644
--- a/app/src/main/java/llc/slacker/openime/SpaceVoiceGestureController.kt
+++ b/app/src/main/java/llc/slacker/openime/SpaceVoiceGestureController.kt
@@ -4,13 +4,15 @@ import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.ViewConfiguration
+import kotlin.math.abs
/**
* Owns the touch state for the combined space/voice key.
*
- * A short press remains the key's normal click. Holding past Android's
- * configured long-press timeout arms voice; sliding upward toggles cancel
- * preview, and release either stops or cancels the active voice gesture.
+ * A short press remains space. A deliberate horizontal drag enters cursor
+ * movement before the long-press timeout. Holding still past Android's
+ * configured long-press timeout arms voice; sliding upward then toggles the
+ * voice cancel preview.
*/
internal class SpaceVoiceGestureController(
private val toPx: (Int) -> Int,
@@ -20,6 +22,7 @@ internal class SpaceVoiceGestureController(
private val onVoiceStop: () -> Unit,
private val onVoiceCancel: () -> Unit,
private val onCancelPreviewChanged: (Boolean) -> Unit,
+ private val onCursorStep: (Int) -> Unit,
) {
private val handler = Handler(Looper.getMainLooper())
private val longPressTimeoutMs = ViewConfiguration.getLongPressTimeout().toLong()
@@ -33,12 +36,18 @@ internal class SpaceVoiceGestureController(
var pointerId: Int = -1
private set
+ var cursorMode: Boolean = false
+ private set
+
+ private var downX = 0f
private var downY = 0f
+ private var lastX = 0f
+ private var cursorTravel = 0f
private var cancelPreview = false
private var anchor: View? = null
private val armVoice = Runnable {
- if (!trackingTouch || active || !canStartVoice()) return@Runnable
+ if (!trackingTouch || active || cursorMode || !canStartVoice()) return@Runnable
active = true
cancelPreview = false
onArmFeedback()
@@ -48,25 +57,64 @@ internal class SpaceVoiceGestureController(
fun begin(
anchor: View,
pointerId: Int,
+ rawX: Float,
rawY: Float,
) {
reset()
trackingTouch = true
active = false
+ cursorMode = false
this.anchor = anchor
this.pointerId = pointerId
+ downX = rawX
downY = rawY
+ lastX = rawX
+ cursorTravel = 0f
cancelPreview = false
handler.postDelayed(armVoice, longPressTimeoutMs)
}
/**
- * @return true once voice owns the gesture, so the normal key click is
- * suppressed for MOVE/UP after the long press has armed.
+ * @return true after either cursor movement or voice owns the gesture.
*/
- fun move(rawY: Float): Boolean {
- if (!trackingTouch || !active) return false
- val cancelNow = downY - rawY >= toPx(CANCEL_DISTANCE_DP)
+ fun move(rawX: Float, rawY: Float): Boolean {
+ if (!trackingTouch) return false
+
+ if (!active) {
+ val horizontal = rawX - downX
+ val vertical = rawY - downY
+ if (
+ !cursorMode &&
+ abs(horizontal) >= toPx(CURSOR_MODE_START_DP) &&
+ abs(horizontal) > abs(vertical) * 1.25f
+ ) {
+ cursorMode = true
+ cursorTravel = 0f
+ lastX = rawX
+ handler.removeCallbacks(armVoice)
+ anchor?.isPressed = false
+ }
+
+ if (cursorMode) {
+ cursorTravel += rawX - lastX
+ lastX = rawX
+ val step = toPx(CURSOR_STEP_DP).coerceAtLeast(1).toFloat()
+ while (abs(cursorTravel) >= step) {
+ val direction = if (cursorTravel > 0f) 1 else -1
+ onCursorStep(direction)
+ cursorTravel -= step * direction
+ }
+ return true
+ }
+ return false
+ }
+
+ val upwardDistance = downY - rawY
+ val cancelNow = if (cancelPreview) {
+ upwardDistance >= toPx(CANCEL_EXIT_DISTANCE_DP)
+ } else {
+ upwardDistance >= toPx(CANCEL_ENTER_DISTANCE_DP)
+ }
if (cancelNow != cancelPreview) {
cancelPreview = cancelNow
onCancelPreviewChanged(cancelNow)
@@ -75,20 +123,39 @@ internal class SpaceVoiceGestureController(
}
/**
- * @return true when an armed voice gesture consumed this release; false
- * for a short press so the normal View click can commit a space.
+ * @return true when cursor/voice owned this release; false for a short tap
+ * so the normal View click can commit one space.
*/
fun finish(cancelled: Boolean): Boolean {
if (!trackingTouch) return false
handler.removeCallbacks(armVoice)
+
+ if (cursorMode) {
+ trackingTouch = false
+ active = false
+ cursorMode = false
+ pointerId = -1
+ downX = 0f
+ downY = 0f
+ lastX = 0f
+ cursorTravel = 0f
+ cancelPreview = false
+ anchor = null
+ return true
+ }
+
val wasActive = active
val shouldCancel = wasActive && (cancelPreview || cancelled)
if (wasActive) anchor?.isPressed = false
trackingTouch = false
active = false
+ cursorMode = false
pointerId = -1
+ downX = 0f
downY = 0f
+ lastX = 0f
+ cursorTravel = 0f
cancelPreview = false
anchor = null
@@ -107,8 +174,12 @@ internal class SpaceVoiceGestureController(
if (active) anchor?.isPressed = false
trackingTouch = false
active = false
+ cursorMode = false
pointerId = -1
+ downX = 0f
downY = 0f
+ lastX = 0f
+ cursorTravel = 0f
cancelPreview = false
anchor = null
}
@@ -119,6 +190,9 @@ internal class SpaceVoiceGestureController(
}
private companion object {
- const val CANCEL_DISTANCE_DP = 48
+ const val CANCEL_ENTER_DISTANCE_DP = 72
+ const val CANCEL_EXIT_DISTANCE_DP = 48
+ const val CURSOR_MODE_START_DP = 18
+ const val CURSOR_STEP_DP = 12
}
}
diff --git a/app/src/main/java/llc/slacker/openime/SpaceVoiceKeyFactory.kt b/app/src/main/java/llc/slacker/openime/SpaceVoiceKeyFactory.kt
index 6a076468..3d6de4c8 100644
--- a/app/src/main/java/llc/slacker/openime/SpaceVoiceKeyFactory.kt
+++ b/app/src/main/java/llc/slacker/openime/SpaceVoiceKeyFactory.kt
@@ -29,7 +29,7 @@ internal class SpaceVoiceKeyFactory(
}
}.apply {
tag = "key-space"
- contentDescription = "$label,点击空格,长按语音输入"
+ contentDescription = "$label,点击空格,左右滑动移动光标,长按语音输入"
setOnLongClickListener {
if (!canStartVoice()) return@setOnLongClickListener true
@@ -59,6 +59,7 @@ internal class SpaceVoiceKeyFactory(
gestureController.begin(
anchor = view,
pointerId = event.getPointerId(event.actionIndex),
+ rawX = event.rawX,
rawY = event.rawY,
)
false
@@ -71,8 +72,9 @@ internal class SpaceVoiceKeyFactory(
if (wasTracking) suppressNextTap = true
wasTracking
} else {
+ val pointerX = event.rawX + event.getX(index) - event.x
val pointerY = event.rawY + event.getY(index) - event.y
- gestureController.move(pointerY)
+ gestureController.move(pointerX, pointerY)
}
}
MotionEvent.ACTION_POINTER_UP -> {
diff --git a/app/src/main/java/llc/slacker/openime/SymbolManagerActivity.kt b/app/src/main/java/llc/slacker/openime/SymbolManagerActivity.kt
index 06762f1c..bd7295a0 100644
--- a/app/src/main/java/llc/slacker/openime/SymbolManagerActivity.kt
+++ b/app/src/main/java/llc/slacker/openime/SymbolManagerActivity.kt
@@ -2,21 +2,17 @@ package llc.slacker.openime
import android.app.Activity
import android.content.ClipData
-import android.content.res.ColorStateList
import android.os.Build
import android.os.Bundle
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
-import android.util.TypedValue
import android.view.DragEvent
import android.view.Gravity
-import android.view.HapticFeedbackConstants
import android.view.View
import android.view.WindowInsets
import android.view.inputmethod.EditorInfo
import android.widget.EditText
-import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
@@ -91,38 +87,21 @@ class SymbolManagerActivity : Activity() {
orientation = LinearLayout.VERTICAL
setPadding(dp(20), dp(24), dp(20), dp(24))
}
- val title = TextView(this).apply {
- text = "自定义符号"
- textSize = 22f
- setTextColor(getColor(R.color.setup_title))
- setTypeface(typeface, android.graphics.Typeface.BOLD)
- gravity = Gravity.CENTER_VERTICAL
- includeFontPadding = false
- if (Build.VERSION.SDK_INT >= 28) setAccessibilityHeading(true)
- }
- val header = LinearLayout(this).apply {
- orientation = LinearLayout.HORIZONTAL
- gravity = Gravity.CENTER_VERTICAL
- addView(ImageButton(this@SymbolManagerActivity).apply {
- setImageResource(R.drawable.ic_arrow_back)
- imageTintList = ColorStateList.valueOf(accent)
- contentDescription = "返回"
- setMinimumWidth(dp(ImeGeometryTokens.TOUCH_TARGET_DP))
- setMinimumHeight(dp(ImeGeometryTokens.TOUCH_TARGET_DP))
- isClickable = true
- isFocusable = true
- applySelectableBackground(this)
- setOnClickListener {
- it.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
- requestClose()
- }
- }, LinearLayout.LayoutParams(dp(ImeGeometryTokens.TOUCH_TARGET_DP), dp(56)))
- addView(title, LinearLayout.LayoutParams(0, dp(56), 1f))
+ val header = SetupUi.activityTopBar(
+ context = this,
+ title = "自定义符号",
+ ) {
+ requestClose()
}
- content.addView(header, fullHeight(56).apply { bottomMargin = dp(8) })
+ content.addView(
+ header,
+ fullHeight(ImeGeometryTokens.TOP_BAR_HEIGHT_DP).apply {
+ bottomMargin = dp(ImeSpacingTokens.SM_DP)
+ },
+ )
content.addView(TextView(this).apply {
text = "可添加、分类、固定和删除。点击箭头调整顺序,也可长按符号行拖动排序。"
- textSize = 13f
+ textSize = ImeTypographyTokens.BODY_SP
setTextColor(getColor(R.color.setup_body))
setLineSpacing(dp(2).toFloat(), 1f)
setPadding(0, 0, 0, dp(12))
@@ -133,7 +112,7 @@ class SymbolManagerActivity : Activity() {
hint = "分组,例如:常用箭头"
setSingleLine(true)
imeOptions = EditorInfo.IME_ACTION_NEXT
- textSize = 16f
+ textSize = ImeTypographyTokens.BODY_SP
setText(draftGroup)
}
SetupUi.styleInput(this, groupEdit)
@@ -144,7 +123,7 @@ class SymbolManagerActivity : Activity() {
hint = "符号,例如:⇢ 或 自定义文本"
setSingleLine(true)
imeOptions = EditorInfo.IME_ACTION_DONE
- textSize = 20f
+ textSize = ImeTypographyTokens.BODY_SP
setText(draftSymbol)
}
SetupUi.styleInput(this, symbolEdit)
@@ -208,7 +187,7 @@ class SymbolManagerActivity : Activity() {
content.addView(save, fullHeight(ImeGeometryTokens.PRIMARY_ROW_HEIGHT_DP).apply { bottomMargin = dp(20) })
content.addView(TextView(this).apply {
text = "已保存符号"
- textSize = 16f
+ textSize = ImeTypographyTokens.TITLE_SP
setTextColor(getColor(R.color.setup_title))
setTypeface(typeface, android.graphics.Typeface.BOLD)
setPadding(0, 0, 0, dp(8))
@@ -219,7 +198,7 @@ class SymbolManagerActivity : Activity() {
if (symbols.isEmpty()) {
content.addView(TextView(this).apply {
text = "还没有保存的自定义符号;在上方填写后点击保存符号。"
- textSize = 13f
+ textSize = ImeTypographyTokens.BODY_SP
setTextColor(getColor(R.color.setup_body))
setPadding(dp(4), dp(4), dp(4), dp(8))
tag = "symbol-empty-state"
@@ -229,7 +208,7 @@ class SymbolManagerActivity : Activity() {
.forEach { (group, groupedSymbols) ->
content.addView(TextView(this).apply {
text = group
- textSize = 14f
+ textSize = ImeTypographyTokens.TITLE_SP
setTextColor(accent)
setTypeface(typeface, android.graphics.Typeface.BOLD)
setPadding(0, dp(8), 0, dp(4))
@@ -279,7 +258,7 @@ class SymbolManagerActivity : Activity() {
)
addView(TextView(this@SymbolManagerActivity).apply {
text = item.symbol
- textSize = 21f
+ textSize = ImeTypographyTokens.KEY_LETTER_SP
setTextColor(getColor(R.color.setup_title))
maxLines = 2
ellipsize = TextUtils.TruncateAt.END
@@ -288,7 +267,7 @@ class SymbolManagerActivity : Activity() {
}, fullWrap())
addView(TextView(this@SymbolManagerActivity).apply {
text = if (item.pinned) "已固定" else "未固定"
- textSize = 12f
+ textSize = ImeTypographyTokens.CAPTION_SP
setTextColor(if (item.pinned) accent else getColor(R.color.setup_body))
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
}, fullWrap())
@@ -387,7 +366,7 @@ class SymbolManagerActivity : Activity() {
private fun fieldLabel(label: String, targetId: Int) = TextView(this).apply {
text = label
- textSize = 12f
+ textSize = ImeTypographyTokens.BODY_SP
setTextColor(getColor(R.color.setup_body))
setPadding(dp(4), 0, dp(4), dp(4))
labelFor = targetId
@@ -421,19 +400,6 @@ class SymbolManagerActivity : Activity() {
groupEdit.text.toString() != initialGroup ||
symbolEdit.text.toString() != initialSymbol
- private fun applySelectableBackground(view: View) {
- val value = TypedValue()
- if (
- theme.resolveAttribute(
- android.R.attr.selectableItemBackgroundBorderless,
- value,
- true,
- ) && value.resourceId != 0
- ) {
- view.setBackgroundResource(value.resourceId)
- }
- }
-
private fun fullWrap() = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT,
diff --git a/app/src/main/java/llc/slacker/openime/SymbolRailRenderer.kt b/app/src/main/java/llc/slacker/openime/SymbolRailRenderer.kt
index 53c1e7ef..3a0fcd72 100644
--- a/app/src/main/java/llc/slacker/openime/SymbolRailRenderer.kt
+++ b/app/src/main/java/llc/slacker/openime/SymbolRailRenderer.kt
@@ -110,7 +110,7 @@ internal object SymbolRailRenderer {
onFeedback: () -> Unit,
): TextView = TextView(context).apply {
text = symbol
- textSize = if (symbol.length > 2) 12f else 17f
+ textSize = ImeTypographyTokens.BODY_SP
gravity = Gravity.CENTER
tag = "$tagPrefix$symbol"
contentDescription = symbol
diff --git a/app/src/main/java/llc/slacker/openime/TextEditorPanelController.kt b/app/src/main/java/llc/slacker/openime/TextEditorPanelController.kt
index a469aae7..8b80808d 100644
--- a/app/src/main/java/llc/slacker/openime/TextEditorPanelController.kt
+++ b/app/src/main/java/llc/slacker/openime/TextEditorPanelController.kt
@@ -56,7 +56,7 @@ internal class TextEditorPanelController(
orientation = LinearLayout.HORIZONTAL
}
QUICK_ACTIONS.forEach { (label, action) ->
- val control = createKey(label, 10f) { onTextEdit(action) }.apply {
+ val control = createKey(label, ImeTypographyTokens.CAPTION_SP) { onTextEdit(action) }.apply {
tag = "textedit-action:$action"
}
actionControls[action] = ActionControl(label, control)
@@ -85,7 +85,7 @@ internal class TextEditorPanelController(
center: Boolean = false,
): TextView = createPanelButton(
label ?: "",
- if (center) 9f else 14f,
+ if (center) ImeTypographyTokens.CAPTION_SP else ImeTypographyTokens.BODY_SP,
!center,
).apply {
if (action != null) {
@@ -104,9 +104,9 @@ internal class TextEditorPanelController(
}
listOf(
- listOf(cell(), cell("▲", "up"), cell()),
- listOf(cell("◀", "left"), cell(center = true), cell("▶", "right")),
- listOf(cell(), cell("▼", "down"), cell()),
+ listOf(cell(), cell("上", "up"), cell()),
+ listOf(cell("左", "left"), cell(center = true), cell("右", "right")),
+ listOf(cell(), cell("下", "down"), cell()),
).forEach { rowItems ->
val row = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
@@ -193,7 +193,7 @@ internal class TextEditorPanelController(
val unavailable = reason != null
control.view.isEnabled = !unavailable
control.view.isClickable = !unavailable
- control.view.alpha = if (unavailable) 0.38f else 1f
+ control.view.alpha = if (unavailable) ImeSurfacePolicy.DISABLED_ALPHA else 1f
control.view.contentDescription =
if (unavailable && Build.VERSION.SDK_INT < 30) {
"${control.label},不可用:$reason"
diff --git a/app/src/main/java/llc/slacker/openime/UserDataArchive.kt b/app/src/main/java/llc/slacker/openime/UserDataArchive.kt
new file mode 100644
index 00000000..3b1bb6f3
--- /dev/null
+++ b/app/src/main/java/llc/slacker/openime/UserDataArchive.kt
@@ -0,0 +1,433 @@
+package llc.slacker.openime
+
+import android.content.Context
+import org.json.JSONArray
+import org.json.JSONObject
+
+internal data class ArchiveQuickPhrase(
+ val category: String,
+ val text: String,
+ val inputCode: String,
+)
+
+internal data class ArchiveCustomSymbol(
+ val group: String,
+ val symbol: String,
+ val pinned: Boolean,
+)
+
+internal data class ArchiveUserPhrase(
+ val code: String,
+ val text: String,
+ val frequency: Int,
+ val lastUsed: Long,
+)
+
+internal data class ArchiveSettings(
+ val theme: String,
+ val appearance: String,
+ val sound: Boolean,
+ val haptic: Boolean,
+ val popup: Boolean,
+ val fuzzy: Boolean,
+ val skinOpacity: Int,
+ val skinRadius: Int,
+ val skinFont: Int,
+ val skinColor: String,
+ val preferredChineseMode: String,
+ val handedness: String,
+ val keyboardHeightPercent: Int,
+ val floatingWidthPercent: Int,
+ val floatingOpacityPercent: Int,
+)
+
+internal data class RimeUserDictionaryArchive(
+ val name: String,
+ val content: String,
+)
+
+internal data class UserDataArchive(
+ val schemaVersion: Int = UserDataArchiveCodec.SCHEMA_VERSION,
+ val quickPhrases: List,
+ val customSymbols: List,
+ val userPhrases: List,
+ val settings: ArchiveSettings,
+ val rimeUserDictionaries: List = emptyList(),
+)
+
+internal data class UserDataImportPreview(
+ val quickPhrasesToAdd: Int,
+ val customSymbolsToAdd: Int,
+ val userPhrasesToAdd: Int,
+ val settingsWillChange: Boolean,
+ val rimeDictionaryFiles: Int,
+) {
+ val totalKotlinItemsToAdd: Int
+ get() = quickPhrasesToAdd + customSymbolsToAdd + userPhrasesToAdd
+}
+
+internal object UserDataArchiveCodec {
+ const val SCHEMA_VERSION = 1
+
+ fun encode(value: UserDataArchive): String {
+ val root = JSONObject()
+ .put("schema_version", value.schemaVersion)
+ .put("quick_phrases", JSONArray().apply {
+ value.quickPhrases.forEach { item ->
+ put(
+ JSONObject()
+ .put("category", item.category)
+ .put("text", item.text)
+ .put("input_code", item.inputCode),
+ )
+ }
+ })
+ .put("custom_symbols", JSONArray().apply {
+ value.customSymbols.forEach { item ->
+ put(
+ JSONObject()
+ .put("group", item.group)
+ .put("symbol", item.symbol)
+ .put("pinned", item.pinned),
+ )
+ }
+ })
+ .put("user_phrases", JSONArray().apply {
+ value.userPhrases.forEach { item ->
+ put(
+ JSONObject()
+ .put("code", item.code)
+ .put("text", item.text)
+ .put("frequency", item.frequency)
+ .put("last_used", item.lastUsed),
+ )
+ }
+ })
+ .put("settings", settingsToJson(value.settings))
+ .put("rime_user_dictionaries", JSONArray().apply {
+ value.rimeUserDictionaries.forEach { item ->
+ put(
+ JSONObject()
+ .put("name", item.name)
+ .put("content", item.content),
+ )
+ }
+ })
+ return root.toString(2)
+ }
+
+ fun decode(raw: String): UserDataArchive {
+ val root = JSONObject(raw)
+ val version = root.optInt("schema_version", -1)
+ require(version == SCHEMA_VERSION) {
+ "Unsupported openIME data schema: $version"
+ }
+ return UserDataArchive(
+ schemaVersion = version,
+ quickPhrases = root.optJSONArray("quick_phrases").toObjects { item ->
+ ArchiveQuickPhrase(
+ category = item.optString("category", "常用").ifBlank { "常用" },
+ text = item.optString("text", "").trim(),
+ inputCode = item.optString("input_code", "").trim(),
+ )
+ }.filter { it.text.isNotEmpty() },
+ customSymbols = root.optJSONArray("custom_symbols").toObjects { item ->
+ ArchiveCustomSymbol(
+ group = item.optString("group", "自定义").ifBlank { "自定义" },
+ symbol = item.optString("symbol", "").trim(),
+ pinned = item.optBoolean("pinned", false),
+ )
+ }.filter { it.symbol.isNotEmpty() },
+ userPhrases = root.optJSONArray("user_phrases").toObjects { item ->
+ ArchiveUserPhrase(
+ code = item.optString("code", "").trim(),
+ text = item.optString("text", "").trim(),
+ frequency = item.optInt("frequency", 1).coerceAtLeast(1),
+ lastUsed = item.optLong("last_used", 0L).coerceAtLeast(0L),
+ )
+ }.filter { it.code.isNotEmpty() && it.text.isNotEmpty() },
+ settings = settingsFromJson(root.optJSONObject("settings") ?: JSONObject()),
+ rimeUserDictionaries =
+ root.optJSONArray("rime_user_dictionaries").toObjects { item ->
+ RimeUserDictionaryArchive(
+ name = item.optString("name", "").trim(),
+ content = item.optString("content", ""),
+ )
+ }.filter {
+ isSafeRimeDictionaryName(it.name) && it.content.isNotEmpty()
+ }.distinctBy { it.name },
+ )
+ }
+
+ internal fun isSafeRimeDictionaryName(name: String): Boolean =
+ name.length in 1..128 &&
+ name != "." &&
+ name != ".." &&
+ name.none { char -> char == '/' || char == '\\' || char == '\u0000' }
+
+ private fun settingsToJson(value: ArchiveSettings): JSONObject =
+ JSONObject()
+ .put("theme", value.theme)
+ .put("appearance", value.appearance)
+ .put("sound", value.sound)
+ .put("haptic", value.haptic)
+ .put("popup", value.popup)
+ .put("fuzzy", value.fuzzy)
+ .put("skin_opacity", value.skinOpacity)
+ .put("skin_radius", value.skinRadius)
+ .put("skin_font", value.skinFont)
+ .put("skin_color", value.skinColor)
+ .put("preferred_chinese_mode", value.preferredChineseMode)
+ .put("handedness", value.handedness)
+ .put("keyboard_height_percent", value.keyboardHeightPercent)
+ .put("floating_width_percent", value.floatingWidthPercent)
+ .put("floating_opacity_percent", value.floatingOpacityPercent)
+
+ private fun settingsFromJson(value: JSONObject): ArchiveSettings =
+ ArchiveSettings(
+ theme = value.optString("theme", ImeTheme.IOS.name),
+ appearance = value.optString("appearance", ImeAppearance.SYSTEM.name),
+ sound = value.optBoolean("sound", true),
+ haptic = value.optBoolean("haptic", true),
+ popup = value.optBoolean("popup", false),
+ fuzzy = value.optBoolean("fuzzy", false),
+ skinOpacity = value.optInt("skin_opacity", 95).coerceIn(70, 100),
+ skinRadius = value.optInt("skin_radius", 8).coerceIn(0, 24),
+ skinFont = value.optInt("skin_font", 17).coerceIn(14, 22),
+ skinColor = AccentPalette.normalize(value.optString("skin_color", AccentPalette.DEFAULT)),
+ preferredChineseMode = value.optString(
+ "preferred_chinese_mode",
+ KeyboardMode.PINYIN_26.name,
+ ),
+ handedness = value.optString("handedness", ImeHandedness.STANDARD.name),
+ keyboardHeightPercent =
+ value.optInt("keyboard_height_percent", 100).coerceIn(92, 120),
+ floatingWidthPercent =
+ value.optInt("floating_width_percent", 88).coerceIn(72, 96),
+ floatingOpacityPercent =
+ value.optInt("floating_opacity_percent", 100).coerceIn(82, 100),
+ )
+
+ private inline fun JSONArray?.toObjects(block: (JSONObject) -> T): List {
+ if (this == null) return emptyList()
+ return buildList {
+ for (index in 0 until length()) {
+ optJSONObject(index)?.let { add(block(it)) }
+ }
+ }
+ }
+}
+
+internal object UserDataArchiveMerger {
+ fun preview(existing: UserDataArchive, incoming: UserDataArchive): UserDataImportPreview {
+ val existingQuick = existing.quickPhrases.mapTo(HashSet(), ::quickKey)
+ val existingSymbols = existing.customSymbols.mapTo(HashSet(), ::symbolKey)
+ val existingUser = existing.userPhrases.mapTo(HashSet(), ::userPhraseKey)
+ return UserDataImportPreview(
+ quickPhrasesToAdd = incoming.quickPhrases.distinctBy(::quickKey)
+ .count { quickKey(it) !in existingQuick },
+ customSymbolsToAdd = incoming.customSymbols.distinctBy(::symbolKey)
+ .count { symbolKey(it) !in existingSymbols },
+ userPhrasesToAdd = incoming.userPhrases.distinctBy(::userPhraseKey)
+ .count { userPhraseKey(it) !in existingUser },
+ settingsWillChange = incoming.settings != existing.settings,
+ rimeDictionaryFiles = incoming.rimeUserDictionaries
+ .map { it.name }
+ .distinct()
+ .size,
+ )
+ }
+
+ fun merge(existing: UserDataArchive, incoming: UserDataArchive): UserDataArchive {
+ val quick = existing.quickPhrases.toMutableList()
+ val quickKeys = quick.mapTo(HashSet(), ::quickKey)
+ incoming.quickPhrases.forEach { item ->
+ if (quickKeys.add(quickKey(item))) quick += item
+ }
+
+ val symbols = existing.customSymbols.toMutableList()
+ val symbolKeys = symbols.mapTo(HashSet(), ::symbolKey)
+ incoming.customSymbols.forEach { item ->
+ if (symbolKeys.add(symbolKey(item))) symbols += item
+ }
+
+ val users = LinkedHashMap()
+ existing.userPhrases.forEach { users[userPhraseKey(it)] = it }
+ incoming.userPhrases.forEach { item ->
+ val key = userPhraseKey(item)
+ val current = users[key]
+ users[key] = if (current == null) {
+ item
+ } else {
+ current.copy(
+ frequency = maxOf(current.frequency, item.frequency),
+ lastUsed = maxOf(current.lastUsed, item.lastUsed),
+ )
+ }
+ }
+
+ val rime = LinkedHashMap()
+ existing.rimeUserDictionaries.forEach { rime[it.name] = it }
+ incoming.rimeUserDictionaries.forEach { rime[it.name] = it }
+
+ return UserDataArchive(
+ quickPhrases = quick,
+ customSymbols = symbols,
+ userPhrases = users.values.toList(),
+ settings = incoming.settings,
+ rimeUserDictionaries = rime.values.toList(),
+ )
+ }
+
+ private fun quickKey(value: ArchiveQuickPhrase): String =
+ listOf(value.category.trim(), value.text.trim(), value.inputCode.trim().lowercase())
+ .joinToString("\u0000")
+
+ private fun symbolKey(value: ArchiveCustomSymbol): String =
+ listOf(value.group.trim(), value.symbol.trim()).joinToString("\u0000")
+
+ private fun userPhraseKey(value: ArchiveUserPhrase): String =
+ listOf(value.code.trim().lowercase(), value.text.trim()).joinToString("\u0000")
+}
+
+internal object UserDataRepository {
+ fun snapshot(
+ context: Context,
+ rimeUserDictionaries: List = emptyList(),
+ ): UserDataArchive {
+ UserPhraseRepository.configure(context)
+ return UserDataArchive(
+ quickPhrases = QuickPhraseRepository.load(context).map {
+ ArchiveQuickPhrase(
+ category = it.category,
+ text = it.text,
+ inputCode = it.inputCode,
+ )
+ },
+ customSymbols = CustomSymbolRepository.load(context).map {
+ ArchiveCustomSymbol(
+ group = it.group,
+ symbol = it.symbol,
+ pinned = it.pinned,
+ )
+ },
+ userPhrases = UserPhraseRepository.exportArchiveEntries(),
+ settings = ArchiveSettings(
+ theme = ImeSettingsRepository.loadTheme(context).name,
+ appearance = ImeSettingsRepository.loadAppearance(context).name,
+ sound = ImeSettingsRepository.loadSound(context),
+ haptic = ImeSettingsRepository.loadHaptic(context),
+ popup = ImeSettingsRepository.loadPopup(context),
+ fuzzy = ImeSettingsRepository.loadFuzzy(context),
+ skinOpacity = ImeSettingsRepository.loadSkinOpacity(context),
+ skinRadius = ImeSettingsRepository.loadSkinRadius(context),
+ skinFont = ImeSettingsRepository.loadSkinFont(context),
+ skinColor = ImeSettingsRepository.loadSkinColor(context),
+ preferredChineseMode =
+ ImeSettingsRepository.loadPreferredChineseMode(context).name,
+ handedness = ImeSettingsRepository.loadHandedness(context).name,
+ keyboardHeightPercent =
+ ImeSettingsRepository.loadKeyboardHeightPercent(context),
+ floatingWidthPercent =
+ ImeSettingsRepository.loadFloatingWidthPercent(context),
+ floatingOpacityPercent =
+ ImeSettingsRepository.loadFloatingOpacityPercent(context),
+ ),
+ rimeUserDictionaries = rimeUserDictionaries,
+ )
+ }
+
+ fun preview(context: Context, incoming: UserDataArchive): UserDataImportPreview =
+ UserDataArchiveMerger.preview(snapshot(context), incoming)
+
+ fun importKotlinData(context: Context, incoming: UserDataArchive): UserDataImportPreview {
+ val preview = preview(context, incoming)
+
+ val quickKeys = QuickPhraseRepository.load(context)
+ .mapTo(HashSet()) {
+ Triple(it.category.trim(), it.text.trim(), it.inputCode.trim().lowercase())
+ }
+ incoming.quickPhrases.forEach { item ->
+ val key = Triple(
+ item.category.trim(),
+ item.text.trim(),
+ item.inputCode.trim().lowercase(),
+ )
+ if (quickKeys.add(key)) {
+ QuickPhraseRepository.upsert(
+ context = context,
+ id = 0L,
+ category = item.category,
+ text = item.text,
+ inputCode = item.inputCode,
+ )
+ }
+ }
+
+ val symbolKeys = CustomSymbolRepository.load(context)
+ .mapTo(HashSet()) { it.group.trim() to it.symbol.trim() }
+ incoming.customSymbols.forEach { item ->
+ val key = item.group.trim() to item.symbol.trim()
+ if (symbolKeys.add(key)) {
+ val added = CustomSymbolRepository.upsert(
+ context = context,
+ id = 0L,
+ group = item.group,
+ symbol = item.symbol,
+ )
+ if (item.pinned && added != null) {
+ CustomSymbolRepository.togglePinned(context, added.id)
+ }
+ }
+ }
+
+ UserPhraseRepository.mergeArchiveEntries(incoming.userPhrases)
+ applySettings(context, incoming.settings)
+ return preview
+ }
+
+ private fun applySettings(context: Context, value: ArchiveSettings) {
+ ImeSettingsRepository.saveTheme(
+ context,
+ runCatching { ImeTheme.valueOf(value.theme) }.getOrDefault(ImeTheme.IOS),
+ )
+ ImeSettingsRepository.saveAppearance(
+ context,
+ runCatching { ImeAppearance.valueOf(value.appearance) }
+ .getOrDefault(ImeAppearance.SYSTEM),
+ )
+ ImeSettingsRepository.saveSound(context, value.sound)
+ ImeSettingsRepository.saveHaptic(context, value.haptic)
+ ImeSettingsRepository.savePopup(context, value.popup)
+ ImeSettingsRepository.saveFuzzy(context, value.fuzzy)
+ ImeSettingsRepository.saveSkin(
+ context,
+ opacity = value.skinOpacity.coerceIn(70, 100),
+ radius = value.skinRadius.coerceIn(0, 24),
+ fontSize = value.skinFont.coerceIn(14, 22),
+ primaryColor = value.skinColor,
+ )
+ ImeSettingsRepository.savePreferredChineseMode(
+ context,
+ runCatching { KeyboardMode.valueOf(value.preferredChineseMode) }
+ .getOrDefault(KeyboardMode.PINYIN_26),
+ )
+ ImeSettingsRepository.saveHandedness(
+ context,
+ runCatching { ImeHandedness.valueOf(value.handedness) }
+ .getOrDefault(ImeHandedness.STANDARD),
+ )
+ ImeSettingsRepository.saveKeyboardHeightPercent(
+ context,
+ value.keyboardHeightPercent,
+ )
+ ImeSettingsRepository.saveFloatingWidthPercent(
+ context,
+ value.floatingWidthPercent,
+ )
+ ImeSettingsRepository.saveFloatingOpacityPercent(
+ context,
+ value.floatingOpacityPercent,
+ )
+ }
+}
diff --git a/app/src/main/java/llc/slacker/openime/UserPhraseRepository.kt b/app/src/main/java/llc/slacker/openime/UserPhraseRepository.kt
index eac84f16..46ceb480 100644
--- a/app/src/main/java/llc/slacker/openime/UserPhraseRepository.kt
+++ b/app/src/main/java/llc/slacker/openime/UserPhraseRepository.kt
@@ -96,6 +96,31 @@ object UserPhraseRepository {
}
}
+ fun contains(code: String, text: String): Boolean {
+ val key = normalize(code)
+ val value = text.trim()
+ if (key.isEmpty() || value.isEmpty()) return false
+ synchronized(lock) {
+ return entries.containsKey("$key\u0000$value")
+ }
+ }
+
+ /**
+ * Forget one exact fallback-learned phrase. This repository is used only
+ * while librime is unavailable, so deleting here never mutates the native
+ * Rime user database.
+ */
+ fun forget(code: String, text: String): Boolean {
+ val key = normalize(code)
+ val value = text.trim()
+ if (key.isEmpty() || value.isEmpty()) return false
+ synchronized(lock) {
+ val removed = entries.remove("$key\u0000$value") != null
+ if (removed) scheduleSaveLocked()
+ return removed
+ }
+ }
+
/** Write the latest immutable snapshot synchronously when a lifecycle owner needs durability now. */
fun flush() {
synchronized(persistenceLock) {
@@ -166,6 +191,57 @@ object UserPhraseRepository {
}
}
+ internal fun exportArchiveEntries(): List =
+ synchronized(lock) {
+ entries.values
+ .sortedWith(
+ compareByDescending { it.lastUsed }
+ .thenByDescending { it.frequency },
+ )
+ .map { entry ->
+ ArchiveUserPhrase(
+ code = entry.code,
+ text = entry.text,
+ frequency = entry.frequency,
+ lastUsed = entry.lastUsed,
+ )
+ }
+ }
+
+ internal fun mergeArchiveEntries(incoming: List): Int {
+ var added = 0
+ synchronized(lock) {
+ incoming.forEach { item ->
+ val code = normalize(item.code)
+ val text = item.text.trim()
+ if (code.isEmpty() || text.isEmpty() || text == code) return@forEach
+ val key = "$code\u0000$text"
+ val current = entries[key]
+ if (current == null) {
+ entries[key] = Entry(
+ code = code,
+ text = text,
+ frequency = item.frequency.coerceIn(1, 1_000_000),
+ lastUsed = item.lastUsed.coerceAtLeast(0L),
+ )
+ added += 1
+ } else {
+ current.frequency = maxOf(
+ current.frequency,
+ item.frequency.coerceIn(1, 1_000_000),
+ )
+ current.lastUsed = maxOf(
+ current.lastUsed,
+ item.lastUsed.coerceAtLeast(0L),
+ )
+ }
+ }
+ trimLocked()
+ if (incoming.isNotEmpty()) scheduleSaveLocked()
+ }
+ return added
+ }
+
fun clear() {
synchronized(persistenceLock) {
synchronized(lock) {
diff --git a/app/src/main/java/llc/slacker/openime/VoicePanelController.kt b/app/src/main/java/llc/slacker/openime/VoicePanelController.kt
index 081df889..aef63828 100644
--- a/app/src/main/java/llc/slacker/openime/VoicePanelController.kt
+++ b/app/src/main/java/llc/slacker/openime/VoicePanelController.kt
@@ -93,7 +93,7 @@ internal class VoicePanelController(
refreshLanguageControl()
onInlineState("正在准备麦克风…", false, false, null)
- setMicState("⏹", "语音输入进行中,松开空格结束,上滑取消")
+ setMicState(R.drawable.ic_stop, "语音输入进行中,松开空格结束,上滑取消")
setGestureHint(
"松开空格上屏 · 上滑取消",
"松开空格结束语音并自动上屏,上滑取消",
@@ -111,7 +111,7 @@ internal class VoicePanelController(
fun stop() {
if (!active || stopRequested) return
listener.stopVoiceRecognition()
- setMicState("🎤", "正在整理语音识别结果,请稍候")
+ setMicState(R.drawable.ic_mic, "正在整理语音识别结果,请稍候")
setGestureHint("整理识别结果…", "正在整理语音识别结果,请稍候")
stopRequested = true
refreshLanguageControl()
@@ -130,7 +130,7 @@ internal class VoicePanelController(
refreshLanguageControl()
listener.cancelVoiceRecognition()
recognizedText = ""
- setMicState("🎤", "语音状态,已取消,仅支持长按空格启动")
+ setMicState(R.drawable.ic_mic, "语音状态,已取消,仅支持长按空格启动")
setGestureHint(
"长按空格开始",
"长按空格开始语音,松开自动上屏,上滑取消",
@@ -145,7 +145,7 @@ internal class VoicePanelController(
fun setCancelPreview(cancelling: Boolean) {
cancelPreview = cancelling
if (cancelling) {
- setMicState("⏹", "取消语音输入中,松开将丢弃本次语音")
+ setMicState(R.drawable.ic_stop, "取消语音输入中,松开将丢弃本次语音")
setGestureHint(
"上滑取消 · 松开丢弃",
"继续上滑取消语音,松开将丢弃本次语音",
@@ -154,7 +154,7 @@ internal class VoicePanelController(
setModelStatus("取消状态 · 松开将丢弃")
onInlineState("松开取消", true, false, null)
} else {
- setMicState("⏹", "语音输入进行中,松开空格结束,上滑取消")
+ setMicState(R.drawable.ic_stop, "语音输入进行中,松开空格结束,上滑取消")
setGestureHint(
"松开空格上屏 · 上滑取消",
"松开空格结束语音并自动上屏,上滑取消",
@@ -205,7 +205,7 @@ internal class VoicePanelController(
if (stopRequested) {
setModelStatus("正在整理识别结果…")
- setMicState("⏹", "正在整理语音识别结果,请稍候")
+ setMicState(R.drawable.ic_stop, "正在整理语音识别结果,请稍候")
setGestureHint(
"整理识别结果…",
"正在整理语音识别结果,请稍候",
@@ -213,7 +213,7 @@ internal class VoicePanelController(
onInlineState("正在识别…", false, false, null)
} else {
setModelStatus("正在聆听 · 松开空格结束")
- setMicState("⏹", "语音输入进行中,松开空格结束,上滑取消")
+ setMicState(R.drawable.ic_stop, "语音输入进行中,松开空格结束,上滑取消")
setGestureHint(
"松开空格上屏 · 上滑取消",
"松开空格结束语音并自动上屏,上滑取消",
@@ -239,7 +239,7 @@ internal class VoicePanelController(
eventGeneration++
if (text.isNotBlank()) recognizedText = text
setTranscript(text)
- setMicState("🎤", "语音状态,已完成识别,仅支持长按空格启动")
+ setMicState(R.drawable.ic_mic, "语音状态,已完成识别,仅支持长按空格启动")
setGestureHint(
"长按空格开始",
"长按空格开始语音,松开自动上屏,上滑取消",
@@ -303,7 +303,7 @@ internal class VoicePanelController(
eventGeneration++
recognizedText = ""
setTranscript(message)
- setMicState("🎤", "语音状态,识别失败,仅支持长按空格重试")
+ setMicState(R.drawable.ic_mic, "语音状态,识别失败,仅支持长按空格重试")
setGestureHint(
"长按空格开始",
"长按空格重新开始语音,松开自动上屏,上滑取消",
@@ -329,7 +329,7 @@ internal class VoicePanelController(
expandedPanel.post {
if (generation != eventGeneration || cancelled) return@post
if (active && !stopRequested) {
- setMicState("⏹", "语音输入进行中,松开空格结束,上滑取消")
+ setMicState(R.drawable.ic_stop, "语音输入进行中,松开空格结束,上滑取消")
setGestureHint(
"松开空格上屏 · 上滑取消",
"松开空格结束语音并自动上屏,上滑取消",
@@ -342,7 +342,7 @@ internal class VoicePanelController(
null,
)
} else {
- setMicState("⏹", "正在整理语音识别结果,请稍候")
+ setMicState(R.drawable.ic_stop, "正在整理语音识别结果,请稍候")
setGestureHint(
"整理识别结果…",
"正在整理语音识别结果,请稍候",
@@ -365,7 +365,7 @@ internal class VoicePanelController(
return@post
}
modelPrepared = true
- setMicState("⏹", "语音输入进行中,松开空格结束,上滑取消")
+ setMicState(R.drawable.ic_stop, "语音输入进行中,松开空格结束,上滑取消")
setGestureHint(
"松开空格上屏 · 上滑取消",
"松开空格结束语音并自动上屏,上滑取消",
@@ -391,7 +391,7 @@ internal class VoicePanelController(
private fun syncViewPresentation() {
when {
active && stopRequested -> {
- setMicState("🎤", "正在整理语音识别结果,请稍候")
+ setMicState(R.drawable.ic_mic, "正在整理语音识别结果,请稍候")
setGestureHint(
"整理识别结果…",
"正在整理语音识别结果,请稍候",
@@ -402,7 +402,7 @@ internal class VoicePanelController(
)
}
active -> {
- setMicState("⏹", "语音输入进行中,松开空格结束,上滑取消")
+ setMicState(R.drawable.ic_stop, "语音输入进行中,松开空格结束,上滑取消")
setGestureHint(
"松开空格上屏 · 上滑取消",
"松开空格结束语音并自动上屏,上滑取消",
@@ -415,8 +415,8 @@ internal class VoicePanelController(
}
}
- private fun setMicState(icon: String, description: String) {
- view?.setMicState(icon, description)
+ private fun setMicState(iconRes: Int, description: String) {
+ view?.setMicState(iconRes, description)
}
private fun setGestureHint(label: String, description: String) {
diff --git a/app/src/main/java/llc/slacker/openime/VoicePanelView.kt b/app/src/main/java/llc/slacker/openime/VoicePanelView.kt
index 236a870a..7563b3b9 100644
--- a/app/src/main/java/llc/slacker/openime/VoicePanelView.kt
+++ b/app/src/main/java/llc/slacker/openime/VoicePanelView.kt
@@ -63,7 +63,7 @@ internal class VoicePanelView(
transcript.apply {
text = "只需长按空格;松开自动上屏,上滑取消"
- textSize = 16f
+ textSize = ImeTypographyTokens.BODY_SP
gravity = Gravity.CENTER_VERTICAL
maxLines = 2
ellipsize = TextUtils.TruncateAt.END
@@ -103,7 +103,7 @@ internal class VoicePanelView(
}
languageButton = createButton(
LANGUAGES[languageIndex].first,
- 13f,
+ ImeTypographyTokens.BODY_SP,
true,
).apply {
tag = "voice-language"
@@ -124,8 +124,10 @@ internal class VoicePanelView(
),
)
- micButton = createButton("🎤", 18f, false).apply {
+ micButton = createButton("", ImeTypographyTokens.BODY_SP, false).apply {
tag = "voice-mic"
+ setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_mic, 0, 0, 0)
+ compoundDrawablePadding = 0
isEnabled = false
contentDescription = "语音状态,当前未开始,仅支持长按空格启动"
}
@@ -137,7 +139,7 @@ internal class VoicePanelView(
),
)
- gestureHint = createButton("长按空格开始", 13f, true).apply {
+ gestureHint = createButton("长按空格开始", ImeTypographyTokens.BODY_SP, true).apply {
tag = "voice-gesture-hint"
isEnabled = false
contentDescription = "长按空格开始语音,松开自动上屏,上滑取消"
@@ -165,12 +167,12 @@ internal class VoicePanelView(
fun refreshLanguageControl(locked: Boolean) {
languageButton.isEnabled = !locked
languageButton.isClickable = !locked
- languageButton.alpha = if (locked) 0.52f else 1f
+ languageButton.alpha = if (locked) ImeSurfacePolicy.DISABLED_ALPHA else 1f
updateLanguagePresentation(locked)
}
- fun setMicState(icon: String, description: String) {
- micButton.text = icon
+ fun setMicState(iconRes: Int, description: String) {
+ micButton.setCompoundDrawablesRelativeWithIntrinsicBounds(iconRes, 0, 0, 0)
micButton.contentDescription = description
}
diff --git a/app/src/main/java/llc/slacker/openime/VoiceRecognitionBackend.kt b/app/src/main/java/llc/slacker/openime/VoiceRecognitionBackend.kt
index cb4a1942..a6b71cf3 100644
--- a/app/src/main/java/llc/slacker/openime/VoiceRecognitionBackend.kt
+++ b/app/src/main/java/llc/slacker/openime/VoiceRecognitionBackend.kt
@@ -1,12 +1,13 @@
package llc.slacker.openime
import android.content.Context
+import android.util.Log
import com.k2fsa.sherpa.onnx.FeatureConfig
import com.k2fsa.sherpa.onnx.OnlineModelConfig
+import com.k2fsa.sherpa.onnx.OnlineParaformerModelConfig
import com.k2fsa.sherpa.onnx.OnlineRecognizer
import com.k2fsa.sherpa.onnx.OnlineRecognizerConfig
import com.k2fsa.sherpa.onnx.OnlineStream
-import com.k2fsa.sherpa.onnx.OnlineTransducerModelConfig
import java.io.File
import java.util.concurrent.atomic.AtomicBoolean
@@ -156,7 +157,7 @@ object EmbeddedVoiceRuntimeFactory {
selection: VoiceModelSelection,
): EmbeddedVoiceModelRuntime? {
val manifest = selection.manifest ?: return null
- if (manifest.modelType != "zipformer") return null
+ if (manifest.modelType != "paraformer") return null
val downloadedRoot = File(context.filesDir, VOICE_MODEL_DOWNLOADED_DIR)
if (resolveSherpaRuntimeModelFiles(selection, downloadedRoot) == null) return null
return SherpaOnnxStreamingRuntime(context, selection)
@@ -164,7 +165,7 @@ object EmbeddedVoiceRuntimeFactory {
}
/**
- * Streaming Zipformer runtime for either signed APK assets or a verified
+ * Streaming Paraformer runtime for either signed APK assets or a verified
* app-private downloaded package. Model loading is lazy and the recognizer is
* reused until the selected source changes or the lifecycle releases it.
*/
@@ -174,6 +175,7 @@ private class SherpaOnnxStreamingRuntime(
) : StreamingEmbeddedVoiceModelRuntime {
companion object {
private const val SAMPLE_RATE = LocalVoiceAudioSpec.SAMPLE_RATE
+ private const val PARAFORMER_TAIL_PADDING_SAMPLES = SAMPLE_RATE * 3 / 10
}
private val runtimeLock = Any()
@@ -188,7 +190,7 @@ private class SherpaOnnxStreamingRuntime(
override val isReady: Boolean
get() = synchronized(runtimeLock) {
val manifest = selection.manifest
- manifest?.modelType == "zipformer" && modelFiles != null
+ manifest?.modelType == "paraformer" && modelFiles != null
}
override fun preload() {
@@ -256,21 +258,17 @@ private class SherpaOnnxStreamingRuntime(
val config = OnlineRecognizerConfig(
featConfig = FeatureConfig(sampleRate = SAMPLE_RATE, featureDim = 80),
modelConfig = OnlineModelConfig(
- transducer = OnlineTransducerModelConfig(
+ paraformer = OnlineParaformerModelConfig(
encoder = files.encoder,
decoder = files.decoder,
- joiner = files.joiner,
),
tokens = files.tokens,
numThreads = 2,
provider = "cpu",
- modelType = "zipformer",
- modelingUnit = "cjkchar",
+ modelType = "paraformer",
),
enableEndpoint = false,
- decodingMethod = "modified_beam_search",
- maxActivePaths = 4,
- hotwordsScore = 1.8f,
+ decodingMethod = "greedy_search",
)
val assetManager = if (files.storage == SherpaRuntimeStorage.ASSETS) {
context.assets
@@ -285,12 +283,10 @@ private class SherpaOnnxStreamingRuntime(
events: VoiceRecognitionEvents,
): SherpaSession {
val currentRecognizer = ensureRecognizerLocked()
- val hotwords = VoiceHotwordProvider.current()
- val stream = if (hotwords.isBlank()) {
- currentRecognizer.createStream()
- } else {
- currentRecognizer.createStream(hotwords)
- }
+ // sherpa-onnx dynamic hotword graphs are implemented by the
+ // transducer recognizer. Streaming Paraformer uses a plain stream;
+ // openIME still applies its local post-ASR correction repository.
+ val stream = currentRecognizer.createStream()
return SherpaSession(
streamLease = VoiceStreamLease(stream) { it.release() },
languageTag = languageTag,
@@ -322,10 +318,21 @@ private class SherpaOnnxStreamingRuntime(
checkOpenLocked()
val currentRecognizer = checkNotNull(recognizer) { "语音识别器已释放" }
val currentStream = streamLease.value
+ // sherpa-onnx's streaming Paraformer examples append 300 ms
+ // silence before InputFinished so the last chunk can flush.
+ currentStream.acceptWaveform(FloatArray(PARAFORMER_TAIL_PADDING_SAMPLES), SAMPLE_RATE)
currentStream.inputFinished()
val decoded = decodeReadyLocked(currentRecognizer, currentStream)
- val final = currentRecognizer.getResult(currentStream).text.trim()
- .ifBlank { lastPartial }
+ val recognizerFinal = currentRecognizer.getResult(currentStream).text.trim()
+ val usedFallback = recognizerFinal.isBlank() && lastPartial.isNotBlank()
+ if (usedFallback) {
+ Log.i(
+ "OpenImeVoicePerf",
+ "sherpa inputFinished fallback=true finalLength=${recognizerFinal.length} " +
+ "lastPartialLength=${lastPartial.length}",
+ )
+ }
+ val final = recognizerFinal.ifBlank { lastPartial }
lastPartial = final
final to decoded
}
diff --git a/app/src/main/java/llc/slacker/openime/VoiceRuntimeModelSource.kt b/app/src/main/java/llc/slacker/openime/VoiceRuntimeModelSource.kt
index 20bd567a..913807a8 100644
--- a/app/src/main/java/llc/slacker/openime/VoiceRuntimeModelSource.kt
+++ b/app/src/main/java/llc/slacker/openime/VoiceRuntimeModelSource.kt
@@ -4,10 +4,9 @@ import java.io.File
import java.nio.file.Files
internal const val VOICE_MODEL_DOWNLOADED_DIR = "voice-models"
-internal const val SHERPA_MODEL_ROOT = "models/voice/bilingual-zipformer"
-internal const val SHERPA_ENCODER = "$SHERPA_MODEL_ROOT/encoder-epoch-99-avg-1.int8.onnx"
-internal const val SHERPA_DECODER = "$SHERPA_MODEL_ROOT/decoder-epoch-99-avg-1.onnx"
-internal const val SHERPA_JOINER = "$SHERPA_MODEL_ROOT/joiner-epoch-99-avg-1.int8.onnx"
+internal const val SHERPA_MODEL_ROOT = "models/voice/bilingual-paraformer"
+internal const val SHERPA_ENCODER = "$SHERPA_MODEL_ROOT/encoder.int8.onnx"
+internal const val SHERPA_DECODER = "$SHERPA_MODEL_ROOT/decoder.int8.onnx"
internal const val SHERPA_TOKENS = "$SHERPA_MODEL_ROOT/tokens.txt"
internal enum class SherpaRuntimeStorage {
@@ -19,7 +18,6 @@ internal data class SherpaRuntimeModelFiles(
val storage: SherpaRuntimeStorage,
val encoder: String,
val decoder: String,
- val joiner: String,
val tokens: String,
)
@@ -38,8 +36,8 @@ internal fun resolveSherpaRuntimeModelFiles(
downloadedRoot: File,
): SherpaRuntimeModelFiles? {
val manifest = selection.manifest ?: return null
- if (manifest.modelType != "zipformer") return null
- val required = listOf(SHERPA_ENCODER, SHERPA_DECODER, SHERPA_JOINER, SHERPA_TOKENS)
+ if (manifest.modelType != "paraformer") return null
+ val required = listOf(SHERPA_ENCODER, SHERPA_DECODER, SHERPA_TOKENS)
if (!manifest.files.containsAll(required)) return null
return when (selection.source) {
@@ -47,7 +45,6 @@ internal fun resolveSherpaRuntimeModelFiles(
storage = SherpaRuntimeStorage.ASSETS,
encoder = SHERPA_ENCODER,
decoder = SHERPA_DECODER,
- joiner = SHERPA_JOINER,
tokens = SHERPA_TOKENS,
)
@@ -69,7 +66,6 @@ internal fun resolveSherpaRuntimeModelFiles(
storage = SherpaRuntimeStorage.FILES,
encoder = resolve(SHERPA_ENCODER),
decoder = resolve(SHERPA_DECODER),
- joiner = resolve(SHERPA_JOINER),
tokens = resolve(SHERPA_TOKENS),
)
}.getOrNull()
diff --git a/app/src/main/res/drawable/bg_setup_card.xml b/app/src/main/res/drawable/bg_setup_card.xml
new file mode 100644
index 00000000..06b5cff6
--- /dev/null
+++ b/app/src/main/res/drawable/bg_setup_card.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_setup_mark.xml b/app/src/main/res/drawable/bg_setup_mark.xml
index e3bf82d7..1997b25b 100644
--- a/app/src/main/res/drawable/bg_setup_mark.xml
+++ b/app/src/main/res/drawable/bg_setup_mark.xml
@@ -1,5 +1,6 @@
-
-
-
+
+
+
diff --git a/app/src/main/res/drawable/bg_setup_mark_active.xml b/app/src/main/res/drawable/bg_setup_mark_active.xml
index 1377d0ec..3fb11ae5 100644
--- a/app/src/main/res/drawable/bg_setup_mark_active.xml
+++ b/app/src/main/res/drawable/bg_setup_mark_active.xml
@@ -1,5 +1,5 @@
-
-
-
+
+
diff --git a/app/src/main/res/drawable/bg_setup_mark_done.xml b/app/src/main/res/drawable/bg_setup_mark_done.xml
index f603b24a..79be4f9b 100644
--- a/app/src/main/res/drawable/bg_setup_mark_done.xml
+++ b/app/src/main/res/drawable/bg_setup_mark_done.xml
@@ -1,5 +1,5 @@
-
-
-
+
+
diff --git a/app/src/main/res/drawable/bg_setup_muted_pill.xml b/app/src/main/res/drawable/bg_setup_muted.xml
similarity index 100%
rename from app/src/main/res/drawable/bg_setup_muted_pill.xml
rename to app/src/main/res/drawable/bg_setup_muted.xml
diff --git a/app/src/main/res/drawable/bg_setup_primary_pill.xml b/app/src/main/res/drawable/bg_setup_primary.xml
similarity index 100%
rename from app/src/main/res/drawable/bg_setup_primary_pill.xml
rename to app/src/main/res/drawable/bg_setup_primary.xml
diff --git a/app/src/main/res/drawable/ic_backspace.xml b/app/src/main/res/drawable/ic_backspace.xml
index 54f03480..8ce73a5e 100644
--- a/app/src/main/res/drawable/ic_backspace.xml
+++ b/app/src/main/res/drawable/ic_backspace.xml
@@ -1,6 +1,6 @@
+
+
diff --git a/app/src/main/res/drawable/ic_caps_lock.xml b/app/src/main/res/drawable/ic_caps_lock.xml
index c399a5fa..a161eda3 100644
--- a/app/src/main/res/drawable/ic_caps_lock.xml
+++ b/app/src/main/res/drawable/ic_caps_lock.xml
@@ -1,6 +1,6 @@
+
+
diff --git a/app/src/main/res/drawable/ic_chevron_down.xml b/app/src/main/res/drawable/ic_chevron_down.xml
new file mode 100644
index 00000000..e84e73eb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_chevron_down.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_clipboard.xml b/app/src/main/res/drawable/ic_clipboard.xml
index da312e9c..bd260caf 100644
--- a/app/src/main/res/drawable/ic_clipboard.xml
+++ b/app/src/main/res/drawable/ic_clipboard.xml
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml
new file mode 100644
index 00000000..113b04b0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_close.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml
index 70cb6c85..ad93744e 100644
--- a/app/src/main/res/drawable/ic_edit.xml
+++ b/app/src/main/res/drawable/ic_edit.xml
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_emoji.xml b/app/src/main/res/drawable/ic_emoji.xml
index c72b8a1f..f8fbb147 100644
--- a/app/src/main/res/drawable/ic_emoji.xml
+++ b/app/src/main/res/drawable/ic_emoji.xml
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_game.xml b/app/src/main/res/drawable/ic_game.xml
index e3d50d91..b644590b 100644
--- a/app/src/main/res/drawable/ic_game.xml
+++ b/app/src/main/res/drawable/ic_game.xml
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_grid.xml b/app/src/main/res/drawable/ic_grid.xml
index d5ac16f1..5c8f1cb2 100644
--- a/app/src/main/res/drawable/ic_grid.xml
+++ b/app/src/main/res/drawable/ic_grid.xml
@@ -1,7 +1,9 @@
-
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
diff --git a/app/src/main/res/drawable/ic_handwriting.xml b/app/src/main/res/drawable/ic_handwriting.xml
index dc6adb3a..a2eab4df 100644
--- a/app/src/main/res/drawable/ic_handwriting.xml
+++ b/app/src/main/res/drawable/ic_handwriting.xml
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml
new file mode 100644
index 00000000..878f0ece
--- /dev/null
+++ b/app/src/main/res/drawable/ic_info.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_keyboard.xml b/app/src/main/res/drawable/ic_keyboard.xml
index 0c48ec6a..decb2778 100644
--- a/app/src/main/res/drawable/ic_keyboard.xml
+++ b/app/src/main/res/drawable/ic_keyboard.xml
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_keyboard_hide.xml b/app/src/main/res/drawable/ic_keyboard_hide.xml
index 12a38a02..969d51c4 100644
--- a/app/src/main/res/drawable/ic_keyboard_hide.xml
+++ b/app/src/main/res/drawable/ic_keyboard_hide.xml
@@ -4,10 +4,7 @@
android:viewportWidth="24"
android:viewportHeight="24">
+ android:fillColor="#FF000000"
+ android:fillType="evenOdd"
+ android:pathData="M3,3h18v12H3zM5,6h2v2H5zM9,6h2v2H9zM13,6h2v2h-2zM17,6h2v2h-2zM6,10h12v2H6zM7,17h10l-5,5z" />
diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml
deleted file mode 100644
index 44226a42..00000000
--- a/app/src/main/res/drawable/ic_launcher.xml
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 00000000..fc620fcf
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_mic.xml b/app/src/main/res/drawable/ic_mic.xml
index cfd8d10c..94769cde 100644
--- a/app/src/main/res/drawable/ic_mic.xml
+++ b/app/src/main/res/drawable/ic_mic.xml
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_more.xml b/app/src/main/res/drawable/ic_more.xml
index 6021c998..05f4c07d 100644
--- a/app/src/main/res/drawable/ic_more.xml
+++ b/app/src/main/res/drawable/ic_more.xml
@@ -1,5 +1,9 @@
-
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml
index 67eb13bc..8f789b5c 100644
--- a/app/src/main/res/drawable/ic_settings.xml
+++ b/app/src/main/res/drawable/ic_settings.xml
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_shift.xml b/app/src/main/res/drawable/ic_shift.xml
index d8be8b5b..d05525b9 100644
--- a/app/src/main/res/drawable/ic_shift.xml
+++ b/app/src/main/res/drawable/ic_shift.xml
@@ -1,6 +1,6 @@
+
+
diff --git a/app/src/main/res/drawable/ic_symbols.xml b/app/src/main/res/drawable/ic_symbols.xml
index 50990c96..2a6870d3 100644
--- a/app/src/main/res/drawable/ic_symbols.xml
+++ b/app/src/main/res/drawable/ic_symbols.xml
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 97df6522..e80e5be9 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -9,254 +9,323 @@
+ android:orientation="vertical"
+ android:paddingStart="@dimen/ime_space_xl"
+ android:paddingTop="@dimen/ime_space_xxl"
+ android:paddingEnd="@dimen/ime_space_xl"
+ android:paddingBottom="@dimen/ime_space_xxl">
-
-
-
+ android:importantForAccessibility="no"
+ android:src="@drawable/ic_brand_mark" />
+ android:textSize="@dimen/ime_text_display"
+ android:textStyle="bold" />
+ android:textSize="@dimen/ime_text_body" />
+ android:textSize="@dimen/ime_text_body" />
+ android:paddingStart="@dimen/ime_space_lg"
+ android:paddingEnd="@dimen/ime_space_lg">
+
+ android:textSize="@dimen/ime_text_body"
+ android:textStyle="bold" />
+
-
+
+
+ android:layout_width="@dimen/setup_icon_size"
+ android:layout_height="@dimen/setup_icon_size"
+ android:importantForAccessibility="no"
+ android:rotation="180"
+ android:src="@drawable/ic_arrow_back"
+ android:tint="@color/setup_body" />
+ android:paddingStart="@dimen/ime_space_lg"
+ android:paddingEnd="@dimen/ime_space_lg">
+
+ android:textSize="@dimen/ime_text_body"
+ android:textStyle="bold" />
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:textSize="@dimen/ime_text_body" />
+
+
+ android:padding="@dimen/ime_space_sm">
+
+
+
+
+
+
+
+
+ android:minLines="3"
+ android:textColor="@color/setup_title"
+ android:textColorHint="@color/setup_muted_text"
+ android:textSize="@dimen/ime_text_title" />
+
+ android:textSize="@dimen/ime_text_body" />
+ android:paddingStart="@dimen/ime_space_lg"
+ android:paddingEnd="@dimen/ime_space_lg">
+
-
-
+ android:textSize="@dimen/ime_text_title"
+ android:textStyle="bold" />
-
-
-
+
-
-
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 00000000..5c84730c
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 00000000..5c84730c
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml
new file mode 100644
index 00000000..c1428fb0
--- /dev/null
+++ b/app/src/main/res/values-en/strings.xml
@@ -0,0 +1,36 @@
+
+
+ openIME
+ Chinese
+ English
+ Local Pinyin input method\nDictionary and voice recognition run on-device
+ Try typing here
+ Enable
+ Enabled
+ Switch
+ Switched to openIME
+ Enable openIME to start typing
+ Enabled. Switch to openIME to type
+ Ready. Try typing below
+ Type nihao to get 你好. Tap a candidate to commit it, or press Space to choose the first candidate.
+ Voice input needs microphone permission. Audio is processed only on this device and is not uploaded. You can grant permission now or skip it and continue testing.
+ Microphone permission was not granted, so voice input is unavailable. Grant it to speak by holding Space.
+ Allow
+ Open system settings
+ Skip
+ Skipped. You can grant permission at any time
+ Allowed
+ Microphone permission is granted. Hold Space to speak, then release it to recognize your speech.
+ Try typing
+ Typing test complete
+ Open preferences
+ Enable and switch to openIME before opening preferences
+ Accent color
+ Custom
+ Enter a 6-digit hexadecimal color, for example 5B6B7A
+ Preferences
+ Preferences
+ Quick phrases
+ Custom symbols
+ About & data
+
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index 0bc75d2c..b29c2e4f 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -1,19 +1,20 @@
- #121417
- #1C1F24
- #F3F4F6
- #9AA3B2
- #1D9BF0
- #1688D0
+ #1C1C1E
+ #242426
+ #F2F2F7
+ #AEAEB2
+ #6EC3F7
+ #50B6F5
#07131D
- #262A31
- #30353E
- #8B93A3
+ #242426
+ #303033
+ #AEAEB2
#5BD08A
- #1B3A28
+ #242426
#5BD08A
- #243056
- #3A404A
- #243056
+ #242426
+ #48484A
+ #242426
+ #6EC3F7
diff --git a/app/src/main/res/values-v31/themes.xml b/app/src/main/res/values-v31/themes.xml
new file mode 100644
index 00000000..a013ae1b
--- /dev/null
+++ b/app/src/main/res/values-v31/themes.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 92572005..47c231ab 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,19 +1,21 @@
- #F4F6FA
- #FFFFFFFF
- #1C1D21
- #6B7280
+ #D5D8DE
+ #EEF0F3
+ #1C1C1E
+ #6D6D72
#1D9BF0
- #1688D0
+ #0E85D5
#07131D
- #EEF1F6
- #E3E7EE
- #9AA3B2
+ #EEF0F3
+ #DDE1E7
+ #6D6D72
#1F8A4C
- #E8F7EE
+ #EEF0F3
#1F8A4C
- #E8EEFF
- #D7DCE5
- #E8EEFF
+ #EEF0F3
+ #B7BCC5
+ #EEF0F3
+ #1D9BF0
+ #1D9BF0
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..f6c3e14a
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,23 @@
+
+
+ 11sp
+ 14sp
+ 16sp
+ 18sp
+ 21sp
+ 28sp
+
+ 2dp
+ 4dp
+ 8dp
+ 12dp
+ 16dp
+ 24dp
+ 32dp
+ 48dp
+ 56dp
+ 24dp
+ 28dp
+ 16dp
+ 72dp
+
diff --git a/app/src/main/res/values/editor_ids.xml b/app/src/main/res/values/editor_ids.xml
index a931bebc..7f77112f 100644
--- a/app/src/main/res/values/editor_ids.xml
+++ b/app/src/main/res/values/editor_ids.xml
@@ -1,6 +1,7 @@
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 967177ce..db440796 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -5,21 +5,32 @@
英文
本地拼音输入法\n词库和语音都在设备内运行
在这里试打
- 第一步 启用 openIME
- 已启用,可再次打开
- 第二步 切换到 openIME
- 当前正在使用,可切换
+ 启用
+ 已启用
+ 切换
+ 已切换到 openIME
启用后即可开始输入
已启用,切换后就能打字
已就绪,在下方试打
输入 nihao 得到 你好。点候选上屏,点空格选首选。
- 语音可选。授权后长按空格说话,不授权也能打字。
- 开启本地语音
- 麦克风已授权
+ 语音输入需要麦克风权限。音频只在本机处理,不会上传。可选择授权,也可以跳过后继续试打。
+ 未获得麦克风授权,语音输入无法使用。授权后即可长按空格说话。
+ 授权
+ 前往系统设置
+ 跳过
+ 已跳过,可随时授权
+ 已授权
+ 麦克风已授权,语音输入可以使用。长按空格开始说话,松手即可识别。
+ 试打
+ 试打完成
打开偏好设置
先启用并切换到 openIME,再进入设置
强调色
自定义
输入 6 位十六进制颜色,例如 5B6B7A
偏好设置
+ 偏好设置
+ 常用语
+ 自定义符号
+ 关于与数据
diff --git a/app/src/test/java/llc/slacker/openime/ContrastTest.kt b/app/src/test/java/llc/slacker/openime/ContrastTest.kt
new file mode 100644
index 00000000..c794ae33
--- /dev/null
+++ b/app/src/test/java/llc/slacker/openime/ContrastTest.kt
@@ -0,0 +1,43 @@
+package llc.slacker.openime
+
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.pow
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ContrastTest {
+ @Test
+ fun canonicalTextPairsMeetWcagAa() {
+ listOf(
+ "#1C1C1E" to "#FFFFFF",
+ "#6D6D72" to "#EEF0F3",
+ "#1F2023" to "#F7F8FA",
+ "#F2F2F7" to "#3A3A3C",
+ "#AEAEB2" to "#242426",
+ "#07131D" to "#1D9BF0",
+ "#07131D" to "#6EC3F7",
+ ).forEach { (foreground, background) ->
+ val ratio = contrastRatio(foreground, background)
+ assertTrue(
+ "$foreground on $background must be >= 4.5:1 but was $ratio",
+ ratio >= 4.5,
+ )
+ }
+ }
+
+ private fun contrastRatio(first: String, second: String): Double {
+ val a = luminance(first)
+ val b = luminance(second)
+ return (max(a, b) + 0.05) / (min(a, b) + 0.05)
+ }
+
+ private fun luminance(hex: String): Double {
+ val value = hex.removePrefix("#")
+ fun component(offset: Int): Double {
+ val raw = value.substring(offset, offset + 2).toInt(16) / 255.0
+ return if (raw <= 0.04045) raw / 12.92 else ((raw + 0.055) / 1.055).pow(2.4)
+ }
+ return 0.2126 * component(0) + 0.7152 * component(2) + 0.0722 * component(4)
+ }
+}
diff --git a/app/src/test/java/llc/slacker/openime/IconLintTest.kt b/app/src/test/java/llc/slacker/openime/IconLintTest.kt
new file mode 100644
index 00000000..e32d0469
--- /dev/null
+++ b/app/src/test/java/llc/slacker/openime/IconLintTest.kt
@@ -0,0 +1,58 @@
+package llc.slacker.openime
+
+import java.io.File
+import org.junit.Assert.fail
+import org.junit.Test
+
+class IconLintTest {
+ @Test
+ fun productIconsUseCanonicalVectorContract() {
+ val root = repoRoot()
+ val violations = mutableListOf()
+ File(root, "app/src/main/res/drawable")
+ .listFiles()
+ .orEmpty()
+ .filter {
+ it.isFile &&
+ it.extension == "xml" &&
+ it.name.startsWith("ic_") &&
+ !it.name.startsWith("ic_launcher") &&
+ it.name != "ic_brand_mark.xml"
+ }
+ .forEach { file ->
+ val xml = file.readText()
+ fun requireContains(value: String, message: String) {
+ if (!xml.contains(value)) violations += "${file.name}: $message"
+ }
+ requireContains("""android:width="24dp"""", "width must be 24dp")
+ requireContains("""android:height="24dp"""", "height must be 24dp")
+ requireContains("""android:viewportWidth="24"""", "viewportWidth must be 24")
+ requireContains("""android:viewportHeight="24"""", "viewportHeight must be 24")
+ if (xml.contains("android:strokeWidth=")) {
+ violations += "${file.name}: strokeWidth is forbidden"
+ }
+ val colors = Regex("""android:(?:fillColor|strokeColor)="(#[0-9A-Fa-f]{8})"""")
+ .findAll(xml)
+ .map { it.groupValues[1].uppercase() }
+ .toList()
+ if (colors.isEmpty()) {
+ violations += "${file.name}: icon must define a fill color"
+ } else if (colors.any { it != "#FF000000" }) {
+ violations += "${file.name}: all vector colors must be #FF000000, got $colors"
+ }
+ }
+
+ if (violations.isNotEmpty()) {
+ fail("Icon contract violations:\n" + violations.joinToString("\n"))
+ }
+ }
+
+ private fun repoRoot(): File {
+ var root = File(System.getProperty("user.dir")).absoluteFile
+ repeat(5) {
+ if (File(root, "app/src/main").isDirectory) return root
+ root = root.parentFile ?: return@repeat
+ }
+ error("Cannot locate repository root")
+ }
+}
diff --git a/app/src/test/java/llc/slacker/openime/ImeThemePersistenceTest.kt b/app/src/test/java/llc/slacker/openime/ImeThemePersistenceTest.kt
new file mode 100644
index 00000000..64704ab2
--- /dev/null
+++ b/app/src/test/java/llc/slacker/openime/ImeThemePersistenceTest.kt
@@ -0,0 +1,18 @@
+package llc.slacker.openime
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ImeThemePersistenceTest {
+ @Test
+ fun legacyThemeValuesFallBackToIos() {
+ listOf("DARK", "CYBERPUNK", "CLASSIC", "MACOS", "unknown", null).forEach { stored ->
+ assertEquals(ImeTheme.IOS, ImeSettingsRepository.parseTheme(stored))
+ }
+ }
+
+ @Test
+ fun iosThemeValueStillRestoresNormally() {
+ assertEquals(ImeTheme.IOS, ImeSettingsRepository.parseTheme("IOS"))
+ }
+}
diff --git a/app/src/test/java/llc/slacker/openime/InputConnectionGatewayTest.kt b/app/src/test/java/llc/slacker/openime/InputConnectionGatewayTest.kt
index f7415050..247a6fa4 100644
--- a/app/src/test/java/llc/slacker/openime/InputConnectionGatewayTest.kt
+++ b/app/src/test/java/llc/slacker/openime/InputConnectionGatewayTest.kt
@@ -315,6 +315,54 @@ class InputConnectionGatewayTest {
assertTrue(fake.events.none { it.startsWith("delete") })
}
+ @Test
+ fun clearUndoExpiresAfterFiveSeconds() {
+ var now = 1_000L
+ val fake = FakeInputConnection(
+ selectedText = "可恢复文本",
+ contextMenuResult = true,
+ extractedText = extracted("", 0, 0, 0),
+ )
+ val gateway = InputConnectionGateway(null, { fake }, nowMs = { now })
+
+ assertTrue(gateway.clearAllText())
+ assertTrue(gateway.hasClearUndo())
+
+ now += 5_001L
+ assertFalse(gateway.hasClearUndo())
+ assertFalse(gateway.restoreLastClear())
+ }
+
+ @Test
+ fun clearUndoIsBoundToTheOriginalInputConnection() {
+ val first = FakeInputConnection(
+ selectedText = "第一个输入框",
+ contextMenuResult = true,
+ extractedText = extracted("", 0, 0, 0),
+ )
+ val second = FakeInputConnection(extractedText = extracted("", 0, 0, 0))
+ var current: InputConnection = first
+ val gateway = InputConnectionGateway(null, { current }, nowMs = { 1_000L })
+
+ assertTrue(gateway.clearAllText())
+ assertTrue(gateway.hasClearUndo())
+
+ current = second
+ assertFalse(gateway.hasClearUndo())
+ assertFalse(gateway.restoreLastClear())
+ assertTrue(second.events.none { it.startsWith("commit:") })
+ }
+
+ @Test
+ fun passwordClearNeverCreatesUndoSnapshot() {
+ val fake = FakeInputConnection(selectedText = "secret", contextMenuResult = true)
+ val gateway = InputConnectionGateway(null, { fake }, isPassword = { true })
+
+ assertFalse(gateway.clearAllText())
+ assertFalse(gateway.hasClearUndo())
+ assertFalse(gateway.restoreLastClear())
+ }
+
@Test
fun failedClearAllRestoresSelectionWithoutPreDeletingContent() {
val fake = FakeInputConnection(
diff --git a/app/src/test/java/llc/slacker/openime/NineKeyChineseTest.kt b/app/src/test/java/llc/slacker/openime/NineKeyChineseTest.kt
index deba0819..39629931 100644
--- a/app/src/test/java/llc/slacker/openime/NineKeyChineseTest.kt
+++ b/app/src/test/java/llc/slacker/openime/NineKeyChineseTest.kt
@@ -35,6 +35,12 @@ class NineKeyChineseTest {
}
}
+ @Test
+ fun incompleteDigitOnlyPrefixesStayAsDigitsInsteadOfFakeLetters() {
+ assertEquals("2", resolve("2").preview)
+ assertEquals("6", resolve("6").preview)
+ }
+
@Test
fun commonChineseSequencesResolveToStableExpectedPreview() {
assertEquals("ni", resolve("64").preview)
diff --git a/app/src/test/java/llc/slacker/openime/NoDecorativeGlyphTest.kt b/app/src/test/java/llc/slacker/openime/NoDecorativeGlyphTest.kt
new file mode 100644
index 00000000..7de71821
--- /dev/null
+++ b/app/src/test/java/llc/slacker/openime/NoDecorativeGlyphTest.kt
@@ -0,0 +1,49 @@
+package llc.slacker.openime
+
+import java.io.File
+import org.junit.Assert.fail
+import org.junit.Test
+
+class NoDecorativeGlyphTest {
+ @Test
+ fun interactiveUiDoesNotUseUnicodeAsIcons() {
+ val root = repoRoot()
+ val forbidden = listOf("⌄", "⌃", "✓", "⏹", "🎤", "☺", "›", "▲", "◀", "▶", "▼")
+ val excluded = setOf("ImeData.kt")
+ val violations = File(root, "app/src/main/java")
+ .walkTopDown()
+ .filter { it.isFile && it.extension == "kt" && it.name !in excluded }
+ .flatMap { file ->
+ file.readLines().asSequence().mapIndexedNotNull { index, line ->
+ val glyph = forbidden.firstOrNull(line::contains)
+ if (glyph != null) "${file.relativeTo(root)}:${index + 1}: $glyph" else null
+ }
+ }
+ .toMutableList()
+
+ File(root, "app/src/main/res/layout")
+ .walkTopDown()
+ .filter { it.isFile && it.extension == "xml" }
+ .forEach { file ->
+ file.readLines().forEachIndexed { index, line ->
+ val glyph = forbidden.firstOrNull(line::contains)
+ if (glyph != null) {
+ violations += "${file.relativeTo(root)}:${index + 1}: $glyph"
+ }
+ }
+ }
+
+ if (violations.isNotEmpty()) {
+ fail("Interactive UI glyphs must use vector drawables:\n" + violations.joinToString("\n"))
+ }
+ }
+
+ private fun repoRoot(): File {
+ var root = File(System.getProperty("user.dir")).absoluteFile
+ repeat(5) {
+ if (File(root, "app/src/main").isDirectory) return root
+ root = root.parentFile ?: return@repeat
+ }
+ error("Cannot locate repository root")
+ }
+}
diff --git a/app/src/test/java/llc/slacker/openime/NoRawColorTest.kt b/app/src/test/java/llc/slacker/openime/NoRawColorTest.kt
new file mode 100644
index 00000000..8b61d3de
--- /dev/null
+++ b/app/src/test/java/llc/slacker/openime/NoRawColorTest.kt
@@ -0,0 +1,36 @@
+package llc.slacker.openime
+
+import java.io.File
+import org.junit.Assert.fail
+import org.junit.Test
+
+class NoRawColorTest {
+ @Test
+ fun productionKotlinDoesNotDefineRawColorsOutsideDesignTokens() {
+ val root = repoRoot()
+ val violations = File(root, "app/src/main/java")
+ .walkTopDown()
+ .filter { it.isFile && it.extension == "kt" && it.name != "ImeDesignTokens.kt" }
+ .flatMap { file ->
+ file.readLines().asSequence().mapIndexedNotNull { index, line ->
+ val hasHex = Regex("""#[0-9A-Fa-f]{6,8}""").containsMatchIn(line)
+ val hasParse = line.contains("Color.parseColor(")
+ if (hasHex || hasParse) "${file.relativeTo(root)}:${index + 1}: ${line.trim()}" else null
+ }
+ }
+ .toList()
+
+ if (violations.isNotEmpty()) {
+ fail("Raw production colors must live in ImeDesignTokens.kt:\n" + violations.joinToString("\n"))
+ }
+ }
+
+ private fun repoRoot(): File {
+ var root = File(System.getProperty("user.dir")).absoluteFile
+ repeat(5) {
+ if (File(root, "app/src/main").isDirectory) return root
+ root = root.parentFile ?: return@repeat
+ }
+ error("Cannot locate repository root")
+ }
+}
diff --git a/app/src/test/java/llc/slacker/openime/NoRawTextSizeTest.kt b/app/src/test/java/llc/slacker/openime/NoRawTextSizeTest.kt
new file mode 100644
index 00000000..9b595bca
--- /dev/null
+++ b/app/src/test/java/llc/slacker/openime/NoRawTextSizeTest.kt
@@ -0,0 +1,74 @@
+package llc.slacker.openime
+
+import java.io.File
+import org.junit.Assert.fail
+import org.junit.Test
+
+class NoRawTextSizeTest {
+ @Test
+ fun productionKotlinUsesTypographyTokens() {
+ val root = repoRoot()
+ val violations = mutableListOf()
+ File(root, "app/src/main/java")
+ .walkTopDown()
+ .filter { it.isFile && it.extension == "kt" && it.name != "ImeDesignTokens.kt" }
+ .forEach { file ->
+ val lines = file.readLines()
+ lines.forEachIndexed { index, line ->
+ if (Regex("""textSize\s*=\s*\d""").containsMatchIn(line)) {
+ violations += "${file.relativeTo(root)}:${index + 1}: ${line.trim()}"
+ }
+ if (
+ listOf("createKey(", "createButton(", "createPanelButton(", "labelText(")
+ .any(line::contains)
+ ) {
+ val window = lines.subList(index, minOf(lines.size, index + 6)).joinToString(" ")
+ val rawHelperSize = Regex(
+ """(?:createKey|createButton|createPanelButton|labelText)\([^)]*,\s*\d+(?:\.\d+)?f\b""",
+ )
+ if (rawHelperSize.containsMatchIn(window)) {
+ violations += "${file.relativeTo(root)}:${index + 1}: ${line.trim()}"
+ }
+ }
+ }
+ }
+
+ if (violations.isNotEmpty()) {
+ fail("Raw text sizes must use ImeTypographyTokens:\n" + violations.distinct().joinToString("\n"))
+ }
+ }
+
+ @Test
+ fun layoutXmlUsesTypographyDimens() {
+ val root = repoRoot()
+ val violations = File(root, "app/src/main/res/layout")
+ .walkTopDown()
+ .filter { it.isFile && it.extension == "xml" }
+ .flatMap { file ->
+ file.readLines().asSequence().mapIndexedNotNull { index, line ->
+ if (
+ line.contains("android:textSize=") &&
+ !line.contains("@dimen/ime_text_")
+ ) {
+ "${file.relativeTo(root)}:${index + 1}: ${line.trim()}"
+ } else {
+ null
+ }
+ }
+ }
+ .toList()
+
+ if (violations.isNotEmpty()) {
+ fail("Layout text sizes must use ime_text_* dimens:\n" + violations.joinToString("\n"))
+ }
+ }
+
+ private fun repoRoot(): File {
+ var root = File(System.getProperty("user.dir")).absoluteFile
+ repeat(5) {
+ if (File(root, "app/src/main").isDirectory) return root
+ root = root.parentFile ?: return@repeat
+ }
+ error("Cannot locate repository root")
+ }
+}
diff --git a/app/src/test/java/llc/slacker/openime/TokenDriftTest.kt b/app/src/test/java/llc/slacker/openime/TokenDriftTest.kt
new file mode 100644
index 00000000..b2b4f6b0
--- /dev/null
+++ b/app/src/test/java/llc/slacker/openime/TokenDriftTest.kt
@@ -0,0 +1,118 @@
+package llc.slacker.openime
+
+import java.io.File
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class TokenDriftTest {
+ @Test
+ fun canonicalTokenScaleDoesNotDrift() {
+ val source = source("app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt")
+ listOf(
+ "const val CAPTION_SP = 11f",
+ "const val BODY_SP = 14f",
+ "const val TITLE_SP = 16f",
+ "const val CANDIDATE_SP = 18f",
+ "const val KEY_LETTER_SP = 21f",
+ "const val DISPLAY_SP = 28f",
+ "const val XXS_DP = 2",
+ "const val XS_DP = 4",
+ "const val SM_DP = 8",
+ "const val MD_DP = 12",
+ "const val LG_DP = 16",
+ "const val XL_DP = 24",
+ "const val XXL_DP = 32",
+ "const val KEY_GAP_DP = 6",
+ ).forEach { expected ->
+ assertTrue("Missing canonical token: $expected", source.contains(expected))
+ }
+
+ val enumBlock = source.substringAfter("enum class ImeTheme").substringBefore("data class Tokens")
+ assertTrue(enumBlock.contains("IOS("))
+ listOf("DARK(", "CYBERPUNK(", "CLASSIC(", "MACOS(").forEach {
+ assertFalse("Legacy theme must stay removed: $it", enumBlock.contains(it))
+ }
+ }
+
+ @Test
+ fun setupResourcePaletteMatchesNativeTokens() {
+ val light = source("app/src/main/res/values/colors.xml")
+ val dark = source("app/src/main/res/values-night/colors.xml")
+
+ mapOf(
+ "setup_page_bg" to "#D5D8DE",
+ "setup_surface" to "#EEF0F3",
+ "setup_title" to "#1C1C1E",
+ "setup_body" to "#6D6D72",
+ "setup_primary" to "#1D9BF0",
+ "setup_input_line" to "#B7BCC5",
+ ).forEach { (name, value) ->
+ assertTrue("$name drifted from the light IME palette", light.contains("$value"))
+ }
+ mapOf(
+ "setup_page_bg" to "#1C1C1E",
+ "setup_surface" to "#242426",
+ "setup_title" to "#F2F2F7",
+ "setup_body" to "#AEAEB2",
+ "setup_primary" to "#6EC3F7",
+ "setup_input_line" to "#48484A",
+ ).forEach { (name, value) ->
+ assertTrue("$name drifted from the dark IME palette", dark.contains("$value"))
+ }
+ }
+
+ @Test
+ fun setupDimensionsMirrorDesignTokens() {
+ val dimens = source("app/src/main/res/values/dimens.xml")
+ listOf(
+ """2dp""",
+ """4dp""",
+ """8dp""",
+ """12dp""",
+ """16dp""",
+ """24dp""",
+ """32dp""",
+ """48dp""",
+ """56dp""",
+ """24dp""",
+ """28dp""",
+ """16dp""",
+ """72dp""",
+ ).forEach { expected ->
+ assertTrue("Resource dimension drifted: $expected", dimens.contains(expected))
+ }
+ }
+
+ @Test
+ fun productVectorIconsUseOneCanvas() {
+ val drawableDir = file("app/src/main/res/drawable")
+ drawableDir.listFiles()
+ .orEmpty()
+ .filter {
+ it.name.startsWith("ic_") &&
+ it.extension == "xml" &&
+ !it.name.startsWith("ic_launcher") &&
+ it.name != "ic_brand_mark.xml"
+ }
+ .forEach { icon ->
+ val xml = icon.readText()
+ assertTrue("${icon.name} width must be 24dp", xml.contains("android:width=\"24dp\""))
+ assertTrue("${icon.name} height must be 24dp", xml.contains("android:height=\"24dp\""))
+ assertTrue("${icon.name} viewport width must be 24", xml.contains("android:viewportWidth=\"24\""))
+ assertTrue("${icon.name} viewport height must be 24", xml.contains("android:viewportHeight=\"24\""))
+ }
+ }
+
+ private fun source(path: String): String = file(path).readText()
+
+ private fun file(path: String): File {
+ var root = File(System.getProperty("user.dir")).absoluteFile
+ repeat(5) {
+ val candidate = File(root, path)
+ if (candidate.isFile || candidate.isDirectory) return candidate
+ root = root.parentFile ?: return@repeat
+ }
+ error("Cannot locate repository path: $path from ${System.getProperty("user.dir")}")
+ }
+}
diff --git a/app/src/test/java/llc/slacker/openime/UserDataArchiveTest.kt b/app/src/test/java/llc/slacker/openime/UserDataArchiveTest.kt
new file mode 100644
index 00000000..48eeee74
--- /dev/null
+++ b/app/src/test/java/llc/slacker/openime/UserDataArchiveTest.kt
@@ -0,0 +1,123 @@
+package llc.slacker.openime
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class UserDataArchiveTest {
+ private val settings = ArchiveSettings(
+ theme = ImeTheme.IOS.name,
+ appearance = ImeAppearance.SYSTEM.name,
+ sound = true,
+ haptic = false,
+ popup = true,
+ fuzzy = false,
+ skinOpacity = 92,
+ skinRadius = 10,
+ skinFont = 18,
+ skinColor = "#1D9BF0",
+ preferredChineseMode = KeyboardMode.PINYIN_9.name,
+ handedness = ImeHandedness.LEFT.name,
+ keyboardHeightPercent = 104,
+ floatingWidthPercent = 84,
+ floatingOpacityPercent = 96,
+ )
+
+ @Test
+ fun jsonRoundTripPreservesAllExportedSections() {
+ val archive = UserDataArchive(
+ quickPhrases = listOf(
+ ArchiveQuickPhrase("工作", "收到,我稍后处理。", "sh"),
+ ),
+ customSymbols = listOf(
+ ArchiveCustomSymbol("箭头", "⇢", pinned = true),
+ ),
+ userPhrases = listOf(
+ ArchiveUserPhrase("ni hao", "你好呀", frequency = 7, lastUsed = 1234L),
+ ),
+ settings = settings,
+ rimeUserDictionaries = listOf(
+ RimeUserDictionaryArchive("luna_pinyin", "# Rime user dictionary\n"),
+ ),
+ )
+
+ val decoded = UserDataArchiveCodec.decode(UserDataArchiveCodec.encode(archive))
+
+ assertEquals(archive, decoded)
+ }
+
+ @Test
+ fun decodeDropsRimeDictionaryNamesThatCanEscapeTheUserDataDirectory() {
+ val archive = UserDataArchive(
+ quickPhrases = emptyList(),
+ customSymbols = emptyList(),
+ userPhrases = emptyList(),
+ settings = settings,
+ rimeUserDictionaries = listOf(
+ RimeUserDictionaryArchive("luna_pinyin", "safe"),
+ RimeUserDictionaryArchive("luna_pinyin", "duplicate"),
+ RimeUserDictionaryArchive("../outside", "unsafe"),
+ RimeUserDictionaryArchive("nested/path", "unsafe"),
+ RimeUserDictionaryArchive("nested\\path", "unsafe"),
+ ),
+ )
+
+ val decoded = UserDataArchiveCodec.decode(UserDataArchiveCodec.encode(archive))
+
+ assertEquals(
+ listOf("luna_pinyin"),
+ decoded.rimeUserDictionaries.map { it.name },
+ )
+ assertTrue(UserDataArchiveCodec.isSafeRimeDictionaryName("luna_pinyin"))
+ assertFalse(UserDataArchiveCodec.isSafeRimeDictionaryName("../outside"))
+ }
+
+ @Test
+ fun mergeAddsNewRecordsWithoutDroppingExistingRecords() {
+ val existing = UserDataArchive(
+ quickPhrases = listOf(
+ ArchiveQuickPhrase("常用", "原有常用语", "old"),
+ ),
+ customSymbols = listOf(
+ ArchiveCustomSymbol("常用", "→", pinned = false),
+ ),
+ userPhrases = listOf(
+ ArchiveUserPhrase("ni", "你", frequency = 5, lastUsed = 100L),
+ ),
+ settings = settings,
+ )
+ val incoming = UserDataArchive(
+ quickPhrases = listOf(
+ ArchiveQuickPhrase("常用", "原有常用语", "old"),
+ ArchiveQuickPhrase("工作", "新增常用语", "new"),
+ ),
+ customSymbols = listOf(
+ ArchiveCustomSymbol("常用", "→", pinned = true),
+ ArchiveCustomSymbol("数学", "≈", pinned = false),
+ ),
+ userPhrases = listOf(
+ ArchiveUserPhrase("ni", "你", frequency = 9, lastUsed = 200L),
+ ArchiveUserPhrase("hao", "好", frequency = 3, lastUsed = 300L),
+ ),
+ settings = settings.copy(sound = false),
+ )
+
+ val preview = UserDataArchiveMerger.preview(existing, incoming)
+ val merged = UserDataArchiveMerger.merge(existing, incoming)
+
+ assertEquals(1, preview.quickPhrasesToAdd)
+ assertEquals(1, preview.customSymbolsToAdd)
+ assertEquals(1, preview.userPhrasesToAdd)
+ assertTrue(preview.settingsWillChange)
+ assertEquals(2, merged.quickPhrases.size)
+ assertEquals(2, merged.customSymbols.size)
+ assertEquals(2, merged.userPhrases.size)
+ assertTrue(merged.quickPhrases.any { it.text == "原有常用语" })
+ assertTrue(merged.customSymbols.any { it.symbol == "→" })
+ val learned = merged.userPhrases.single { it.code == "ni" && it.text == "你" }
+ assertEquals(9, learned.frequency)
+ assertEquals(200L, learned.lastUsed)
+ assertFalse(merged.settings.sound)
+ }
+}
diff --git a/app/src/test/java/llc/slacker/openime/VoiceModelTrustPolicyTest.kt b/app/src/test/java/llc/slacker/openime/VoiceModelTrustPolicyTest.kt
index 2929c30d..df69d4f6 100644
--- a/app/src/test/java/llc/slacker/openime/VoiceModelTrustPolicyTest.kt
+++ b/app/src/test/java/llc/slacker/openime/VoiceModelTrustPolicyTest.kt
@@ -17,22 +17,10 @@ class VoiceModelTrustPolicyTest {
File("app/src/main/assets"),
).firstOrNull { it.isDirectory } ?: error("missing test asset root")
- private fun officialManifest(): VoiceModelManifest = VoiceModelManifest(
- modelId = "sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20-int8",
- modelVersion = "2023-02-20-int8",
- language = "zh-CN,en-US",
- modelType = "zipformer",
- engineVersion = "sherpa-onnx-v1.13.6",
- fileHash = "f2b835dfe8231bccc08692b4d8751ff50c2360b9b39a7501136a45e1bd97db85",
- supportsPunctuation = false,
- requiredMemory = 420_000_000L,
- files = listOf(
- "models/voice/bilingual-zipformer/decoder-epoch-99-avg-1.onnx",
- "models/voice/bilingual-zipformer/encoder-epoch-99-avg-1.int8.onnx",
- "models/voice/bilingual-zipformer/joiner-epoch-99-avg-1.int8.onnx",
- "models/voice/bilingual-zipformer/tokens.txt",
- ),
- )
+ private fun officialManifest(): VoiceModelManifest =
+ VoiceModelManifest.parse(
+ File(assetRoot(), "models/voice/manifest.json").readText(),
+ )
private fun sampleManifest(): VoiceModelManifest = VoiceModelManifest(
modelId = "test-model",
@@ -50,8 +38,13 @@ class VoiceModelTrustPolicyTest {
fun bundledCatalogTrustsOnlyExactOfficialManifestAndLayout() {
val manifest = officialManifest()
assertEquals(
- "12bf0e2a9d5090bdb935c90941a0b3ef6269346860398fed3e45cef91940a525",
- manifest.trustFingerprint(),
+ "sherpa-onnx-streaming-paraformer-bilingual-zh-en-int8",
+ manifest.modelId,
+ )
+ assertEquals("paraformer", manifest.modelType)
+ assertEquals(
+ listOf(SHERPA_DECODER, SHERPA_ENCODER, SHERPA_TOKENS),
+ manifest.files,
)
val catalog = TrustedVoiceModelCatalog.parse(
File(assetRoot(), "models/voice/trusted-downloads.tsv").readText(),
@@ -59,10 +52,6 @@ class VoiceModelTrustPolicyTest {
val entry = catalog.entryFor(manifest)
assertNotNull(entry)
assertEquals("Slacker-LLC", entry?.publisher)
- assertEquals(
- listOf(13_876_452L, 181_895_032L, 3_228_404L, 56_317L),
- entry?.fileSizes,
- )
assertTrue(entry?.matchesLayout(manifest, assetRoot()) == true)
assertEquals(null, catalog.entryFor(manifest.copy(requiredMemory = 1L)))
diff --git a/app/src/test/java/llc/slacker/openime/VoiceRuntimeModelSourceTest.kt b/app/src/test/java/llc/slacker/openime/VoiceRuntimeModelSourceTest.kt
index 3171a450..f26beed8 100644
--- a/app/src/test/java/llc/slacker/openime/VoiceRuntimeModelSourceTest.kt
+++ b/app/src/test/java/llc/slacker/openime/VoiceRuntimeModelSourceTest.kt
@@ -15,7 +15,7 @@ class VoiceRuntimeModelSourceTest {
modelId = modelId,
modelVersion = "1",
language = "zh-CN,en-US",
- modelType = "zipformer",
+ modelType = "paraformer",
engineVersion = "sherpa-test",
fileHash = "0".repeat(64),
supportsPunctuation = false,
@@ -23,7 +23,6 @@ class VoiceRuntimeModelSourceTest {
files = listOf(
SHERPA_DECODER,
SHERPA_ENCODER,
- SHERPA_JOINER,
SHERPA_TOKENS,
),
)
@@ -42,7 +41,6 @@ class VoiceRuntimeModelSourceTest {
assertEquals(SherpaRuntimeStorage.ASSETS, source?.storage)
assertEquals(SHERPA_ENCODER, source?.encoder)
assertEquals(SHERPA_DECODER, source?.decoder)
- assertEquals(SHERPA_JOINER, source?.joiner)
assertEquals(SHERPA_TOKENS, source?.tokens)
} finally {
downloadedRoot.deleteRecursively()
@@ -70,10 +68,9 @@ class VoiceRuntimeModelSourceTest {
assertEquals(SherpaRuntimeStorage.FILES, source?.storage)
assertEquals(File(packageRoot, SHERPA_ENCODER).canonicalPath, File(source!!.encoder).canonicalPath)
assertEquals(File(packageRoot, SHERPA_DECODER).canonicalPath, File(source.decoder).canonicalPath)
- assertEquals(File(packageRoot, SHERPA_JOINER).canonicalPath, File(source.joiner).canonicalPath)
assertEquals(File(packageRoot, SHERPA_TOKENS).canonicalPath, File(source.tokens).canonicalPath)
assertTrue(
- listOf(source.encoder, source.decoder, source.joiner, source.tokens)
+ listOf(source.encoder, source.decoder, source.tokens)
.all { File(it).isFile },
)
} finally {
diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md
new file mode 100644
index 00000000..5d9ba25f
--- /dev/null
+++ b/docs/DECISIONS.md
@@ -0,0 +1,11 @@
+# Visual language decisions
+
+- 2026-09-29|浅色次级文字对比度|保留键盘原始 `keySecondaryText=#6E6E73`,新增页面角色 `textSecondaryRole=#6D6D72`|原值对 `surface=#EEF0F3` 约 4.44:1,低于规范 4.5:1;只修页面角色,避免改变键盘既有视觉。
+- 2026-09-29|候选展开图标状态|复用 `ic_chevron_down`,展开时旋转 180°|保持单一矢量资产,不再用 Unicode 上下箭头。
+- 2026-09-29|Setup XML 色板|仅保留启动前必须使用的资源色,并由 TokenDriftTest 与 Kotlin token 对齐|Android XML 在 Kotlin 初始化前需要资源颜色,不能完全移除。
+- 2026-09-29|退格连删节奏|继续保留现有 60ms 匀速重复|视觉语言 v1 明确要求本轮不改,留作后续交互专项。
+- 2026-09-29|语音松手尾部|松手后保留 300ms 采集窗口,并用 generation/session 所有权阻止旧会话影响新会话|修复 AudioRecord 尾部被立即截断,同时保持取消即时生效。
+- 2026-09-29|九键解码线程策略|先记录 `publishNineKeyDigits -> resolveNineKey` 的 20 次窗口 P50/P95;未取得中低端机 P95 前保持现有线程,不提前迁移|只有 P95 超过 8ms 才按规范迁到 `CandidateQueryCoordinator`。
+- 2026-09-29|九键首帧与 Rime 刷新|候选按压或滚动期间暂缓应用异步 Rime 结果;同时记录从请求到结果可应用的端到端延迟|防止手指下的候选列表重排;是否在 Rime 就绪时跳过首帧回退,等实测 P95 是否低于 40ms 后再定。
+- 2026-09-29|Rime 用户词库导出|vendored librime 1.17.0 已编入 levers 模块,`UserDictManager::Export/Import` 可在关闭用户库会话后导出/合并 UTF-8 快照|用户数据 JSON 在 Rime 会话已加载时包含自动学习词库;不可用时必须明确提示,不静默遗漏。
+- 2026-09-29|流式语音模型|默认模型切换为 `sherpa-onnx-streaming-paraformer-bilingual-zh-en` 的 INT8 encoder/decoder;运行时使用 `OnlineParaformerModelConfig` + `greedy_search`,结束时补 300ms 静音;不再向 Paraformer stream 传 transducer-only 动态 hotwords|优先降低模型体积和保持中英流式识别,同时遵循 sherpa-onnx v1.13.6 官方 Paraformer 配置。
diff --git a/docs/LICENSING.md b/docs/LICENSING.md
index 4f8aa6d9..5a73bb59 100644
--- a/docs/LICENSING.md
+++ b/docs/LICENSING.md
@@ -24,6 +24,16 @@
`app/src/main/assets/licenses/rime-ice-GPL-3.0.txt`,来源明细见根目录
`THIRD_PARTY_NOTICES.md`。
-语音 runtime 以 `app/libs/sherpa-onnx-1.13.6.aar` 提供,语音模型位于
-`app/src/main/assets/models/voice/`。发布 APK 前应同时核对 runtime、模型和词典的
-上游许可证与再分发条件;Git LFS 只负责文件存储,不改变文件的许可证。
+语音 runtime 以 `app/libs/sherpa-onnx-1.13.6.aar` 提供,上游
+`k2-fsa/sherpa-onnx` 使用 Apache-2.0。内置中英双语 Streaming Paraformer 模型
+`csukuangfj/sherpa-onnx-streaming-paraformer-bilingual-zh-en`
+模型卡标记为 Apache-2.0;正式包只使用其 INT8 encoder/decoder。
+具体来源、文件位置和发布核对项统一记录在根目录 `THIRD_PARTY_NOTICES.md`。
+Git LFS 只负责文件存储,不改变文件的许可证。
+
+
+## APK 内许可证
+
+正式 APK 在 `assets/licenses/` 内携带主要第三方许可证文本,包括 librime、OpenCC、Snappy、Rime Ice、sherpa-onnx runtime、当前 Paraformer 模型和 Fluent Emoji。根目录 `THIRD_PARTY_NOTICES.md` 记录组件、来源、版本或固定提交与对应文件位置。
+
+发布工作流同时把 `THIRD_PARTY_NOTICES.md` 作为 GitHub Release 附件上传,便于在 APK 外直接查看。
diff --git a/docs/LOCAL_VOICE_MODEL.md b/docs/LOCAL_VOICE_MODEL.md
index bef975b7..2bc1edb8 100644
--- a/docs/LOCAL_VOICE_MODEL.md
+++ b/docs/LOCAL_VOICE_MODEL.md
@@ -34,7 +34,7 @@ app/src/main/assets/models/voice/
"modelId": "paraformer-zh-en-int8",
"modelVersion": "...",
"language": "zh-CN,en-US",
- "modelType": "streaming-paraformer",
+ "modelType": "paraformer",
"engineVersion": "...",
"fileHash": "sha256",
"supportsPunctuation": true,
@@ -67,11 +67,15 @@ app/src/main/assets/models/voice/
## 当前交付状态
-当前 APK 已内置官方 `sherpa-onnx v1.13.6` native runtime 和中英双语流式
-Zipformer INT8 模型,模型路径为 `models/voice/bilingual-zipformer/`。模型使用
-`OnlineRecognizer` 按 16 kHz PCM 流式解码,键盘出现时异步预热,10 秒冷却期内
-中文或英文语音段复用同一个已加载识别器。当前内置的是纯识别模型,`punctuate()` 保留了独立
-标点模型的扩展边界;没有标点模型时会安全回退原始识别文字。
+当前 APK 使用官方 `sherpa-onnx v1.13.6` native runtime 和
+`sherpa-onnx-streaming-paraformer-bilingual-zh-en` 的 INT8 encoder/decoder,
+模型路径为 `models/voice/bilingual-paraformer/`。运行时通过
+`OnlineParaformerModelConfig`、`modelType="paraformer"` 和
+`greedy_search` 按 16 kHz PCM 流式解码;键盘出现时异步预热,10 秒冷却期内
+中文或英文语音段复用同一个已加载识别器。结束语音段时追加 300 ms 静音尾垫,
+再调用 `inputFinished()`,与 sherpa-onnx 官方流式 Paraformer 示例一致。
+当前内置的是纯识别模型,`punctuate()` 保留独立标点扩展边界;没有标点模型时
+安全回退原始识别文字。
模型包的每个文件都在 `manifest.json` 的 SHA-256 清单内,APK 启动时只选择校验
通过的内置包。下载模型仍然必须走 `VoiceModelRepository` 的校验和原子切换,
@@ -79,8 +83,9 @@ Zipformer INT8 模型,模型路径为 `models/voice/bilingual-zipformer/`。
## 个性化与性能边界
-- 重复选择的本地用户词会生成有上限的 sherpa 动态 hotwords;当前内置模型缺少
- `bpe.vocab`,因此只对可可靠编码的中文/中英混合词做上下文增强,普通英文识别不受影响。
+- Streaming Paraformer 不走 sherpa-onnx 的 transducer hotword graph,因此不再把
+ `VoiceHotwordProvider` 动态热词传给 native stream;本地 `VoiceCorrectionRepository`
+ 的识别后纠正仍保留。
- 用户在语音上屏后立即删除并改正的文本会形成私有 `VoiceCorrectionRepository` 对;
后续相同 ASR 原结果先应用本地纠正,改正目标也会回流动态热词。
- 密码框不进入热词或纠错学习;日志不记录 PCM、转写、热词、纠错内容。
diff --git a/docs/README.md b/docs/README.md
index 3e756425..d71a1d67 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -14,6 +14,7 @@
- [TEST_SOP.md](TEST_SOP.md):L0~L3 正式测试流程。
- [TEST_SOP_CHECKLIST.md](TEST_SOP_CHECKLIST.md):多设备与人工交互验收清单。
- [LICENSING.md](LICENSING.md):主项目与第三方组件许可证边界。
+- [RELEASE.md](RELEASE.md):固定签名、arm64 正式包、标签发布、校验和与第三方清单。
旧的按 PR/分支推进的审计状态文档已删除。当前状态以 GitHub 分支/PR/CI 为准,长期执行顺序只维护在 `REPAIR_PLAN.md`,避免两份计划互相冲突。
diff --git a/docs/RELEASE.md b/docs/RELEASE.md
new file mode 100644
index 00000000..ab0cb24e
--- /dev/null
+++ b/docs/RELEASE.md
@@ -0,0 +1,52 @@
+# 发布流程
+
+## 发布产物
+
+正式发布只生成 `arm64-v8a` APK,文件名为:
+
+```text
+openIME-v{versionName}-arm64-release.apk
+```
+
+同一 GitHub Release 同时上传:
+
+- `SHA256SUMS.txt`
+- `THIRD_PARTY_NOTICES.release.md`
+
+Debug 构建继续保留 `arm64-v8a + x86_64`,用于真机与模拟器回归。
+
+## 固定发布签名
+
+GitHub Actions 需要以下四个 repository secrets:
+
+- `OPENIME_KEYSTORE_B64`:发布 keystore 的 Base64 内容。
+- `OPENIME_KEYSTORE_PASSWORD`
+- `OPENIME_KEY_ALIAS`
+- `OPENIME_KEY_PASSWORD`
+
+本地 release 构建也可使用同名环境变量;其中 `OPENIME_KEYSTORE_PATH` 指向本机 keystore 文件,不使用 Base64。
+
+keystore 不进入仓库,不提交到 Issue、PR、Actions artifact 或 Release。
+
+## 发布步骤
+
+1. 更新 `app/build.gradle.kts` 的 `versionCode` 和 `versionName`。
+2. 在目标提交上完成 JVM 单测、Lint、Debug 构建和设备回归。
+3. 创建与 `versionName` 完全一致的标签,例如 `versionName = "1.0.3"` 对应 `v1.0.3`。
+4. 推送标签。
+5. `.github/workflows/release.yml` 自动执行单测、`lintRelease`、`assembleRelease`、APK 签名校验和 ABI 校验。
+6. 工作流生成 SHA-256,并创建 GitHub Release。
+
+标签与 `versionName` 不一致、签名 secrets 缺失、APK 未签名或包含非 `arm64-v8a` native ABI 时,发布任务会失败。
+
+## 社交预览
+
+仓库内提供 `docs/images/social-preview.png`(1280×640),由 `scripts/generate_brand_assets.py` 生成。GitHub 的仓库 Social preview 不是源码文件配置项,因此需要仓库管理员在 **Settings → General → Social preview** 上传该 PNG;这一步不能通过当前仓库代码提交自动完成。
+
+## 发布前检查
+
+- `AndroidManifest.xml` 不含 `INTERNET` 权限。
+- `android:allowBackup="false"` 保持不变。
+- `THIRD_PARTY_NOTICES.md` 与 `app/src/main/assets/licenses/` 同步。
+- 语音模型、词库或第三方 runtime 版本变化时重新核对对应许可证。
+- 不从 Debug keystore 发布。
diff --git a/docs/images/openime-brand.png b/docs/images/openime-brand.png
new file mode 100644
index 00000000..c43c509c
Binary files /dev/null and b/docs/images/openime-brand.png differ
diff --git a/docs/images/social-preview.png b/docs/images/social-preview.png
new file mode 100644
index 00000000..1c8eb0b4
Binary files /dev/null and b/docs/images/social-preview.png differ
diff --git a/scripts/generate_brand_assets.py b/scripts/generate_brand_assets.py
new file mode 100755
index 00000000..5f087dae
--- /dev/null
+++ b/scripts/generate_brand_assets.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+"""Generate checked-in openIME brand PNG assets.
+
+Requires Pillow:
+ python -m pip install Pillow
+"""
+
+from pathlib import Path
+
+from PIL import Image, ImageDraw, ImageFont
+
+ROOT = Path(__file__).resolve().parents[1]
+OUTPUT = ROOT / "docs" / "images"
+ACCENT = "#6EC3F7"
+DARK = "#1C1C1E"
+TEXT = "#F2F2F7"
+
+
+def draw_mark(image: Image.Image, box: tuple[int, int, int, int], fill, cutout) -> None:
+ draw = ImageDraw.Draw(image)
+ x0, y0, x1, y1 = box
+ scale = (x1 - x0) / 48.0
+
+ def point(x: float, y: float) -> tuple[float, float]:
+ return x0 + x * scale, y0 + y * scale
+
+ draw.rounded_rectangle(
+ [*point(4, 4), *point(44, 44)],
+ radius=11 * scale,
+ fill=fill,
+ )
+ draw.polygon(
+ [
+ point(17, 11),
+ point(31, 11),
+ point(31, 14),
+ point(26, 14),
+ point(26, 34),
+ point(31, 34),
+ point(31, 37),
+ point(17, 37),
+ point(17, 34),
+ point(22, 34),
+ point(22, 14),
+ point(17, 14),
+ ],
+ fill=cutout,
+ )
+
+
+def load_font(size: int) -> ImageFont.ImageFont:
+ candidates = (
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
+ "/usr/share/fonts/truetype/liberation2/LiberationSans-Bold.ttf",
+ "C:/Windows/Fonts/arialbd.ttf",
+ )
+ for candidate in candidates:
+ path = Path(candidate)
+ if path.is_file():
+ return ImageFont.truetype(str(path), size)
+ raise RuntimeError("No supported bold font found; install DejaVu Sans or Liberation Sans")
+
+
+def generate_brand_png() -> None:
+ image = Image.new("RGBA", (288, 288), (0, 0, 0, 0))
+ draw_mark(image, (0, 0, 288, 288), ACCENT, (0, 0, 0, 0))
+ image.save(OUTPUT / "openime-brand.png", optimize=True)
+
+
+def generate_social_preview() -> None:
+ image = Image.new("RGB", (1280, 640), DARK)
+ draw_mark(image, (520, 105, 760, 345), ACCENT, DARK)
+
+ draw = ImageDraw.Draw(image)
+ label = "openIME"
+ font = load_font(92)
+ bounds = draw.textbbox((0, 0), label, font=font)
+ width = bounds[2] - bounds[0]
+ draw.text(((1280 - width) / 2, 400), label, font=font, fill=TEXT)
+
+ image.save(OUTPUT / "social-preview.png", optimize=True)
+
+
+def main() -> None:
+ OUTPUT.mkdir(parents=True, exist_ok=True)
+ generate_brand_png()
+ generate_social_preview()
+ print(OUTPUT / "openime-brand.png")
+ print(OUTPUT / "social-preview.png")
+
+
+if __name__ == "__main__":
+ main()