From 40677e7b2b90fa45fb8c860856cdbd5ff757975e Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:35:27 +0800 Subject: [PATCH 001/193] fix: make nine-key pinyin paths directly selectable --- .../openime/NineKeySymbolRailController.kt | 179 +++++++++++------- 1 file changed, 108 insertions(+), 71 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt b/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt index 48a0621a..cdea5757 100644 --- a/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt +++ b/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt @@ -11,10 +11,13 @@ 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, @@ -51,7 +54,7 @@ internal class NineKeySymbolRailController( context = context, railTag = NINE_RAIL_TAG, contentTag = NINE_CONTENT_TAG, - contentDescription = "九键常用符号,上下滑动查看更多", + contentDescription = "九键拼音筛选或常用符号,上下滑动查看更多", symbols = commonSymbols(), tagPrefix = "punct:", onCommit = onCommit, @@ -61,25 +64,19 @@ internal class NineKeySymbolRailController( 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() } private fun refreshPinyinFilters() { val text = composition.text?.toString().orEmpty() if (text.isBlank()) { - setPinyinFilters(emptyList(), null) {} + renderSymbols() return } @@ -89,86 +86,124 @@ 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() { + val scroll = rail ?: return + SymbolRailRenderer.populate( + scroll = scroll, + symbols = commonSymbols(), + tagPrefix = "punct:", + onCommit = onCommit, + onFeedback = onFeedback, + ) + if (scroll.scrollY != 0) scroll.post { scroll.scrollTo(0, 0) } + scroll.contentDescription = "九键常用符号,上下滑动查看更多" + } + + 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 + val inheritedTextColor = (0 until content.childCount) .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) - return - } + .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 = 12f + 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 + 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(" ", "、")}" - } + scroll.contentDescription = "九键拼音筛选,上下滑动查看更多" + 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 +220,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 } } From 77e81b14fa1c4d1920c0fd28d2d9baeeec17b4f6 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:35:50 +0800 Subject: [PATCH 002/193] style: distinguish selected nine-key pinyin path --- .../main/java/llc/slacker/openime/ImeThemeApplier.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt index 6164f2b1..45966ca0 100644 --- a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt +++ b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt @@ -362,7 +362,7 @@ internal class ImeThemeApplier( ) } - tag == "nine-pinyin-path-filter" -> { + tag == "nine-pinyin-path-selected" -> { view.setTextColor(ImeDrawableFactory.contrastText(t.primary)) view.background = statefulRounded( t.primary, @@ -371,6 +371,15 @@ internal class ImeThemeApplier( ) } + tag == "nine-pinyin-path-filter" -> { + view.setTextColor(t.sideKeyText) + view.background = statefulRounded( + Color.TRANSPARENT, + t.keyPressedBackground, + toPx(ImeGeometryTokens.KEY_RADIUS_DP), + ) + } + tag == "accent-custom" -> { val customSelected = AccentPalette.presets.none { AccentPalette.normalize(it.first) == From b826a6117e0a6d2bbaf8172f19adf1503aaf53a7 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:37:03 +0800 Subject: [PATCH 003/193] feat: expand candidates with a downward strip gesture --- .../slacker/openime/CandidateBarController.kt | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/CandidateBarController.kt b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt index bf644a2c..8dc18c4f 100644 --- a/app/src/main/java/llc/slacker/openime/CandidateBarController.kt +++ b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt @@ -6,6 +6,7 @@ import android.graphics.Typeface import android.graphics.drawable.Drawable import android.os.Build import android.view.Gravity +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.animation.DecelerateInterpolator @@ -13,6 +14,7 @@ import android.widget.HorizontalScrollView import android.widget.LinearLayout import android.widget.ScrollView import android.widget.TextView +import kotlin.math.abs /** * Owns candidate presentation: strip diff/reuse, scroll preservation, @@ -43,6 +45,40 @@ internal class CandidateBarController( private var renderedComposition: String? = null private var renderedExpandedCandidates: List? = null private var renderedExpandedComposition: String? = null + private var candidateTouchDownX = 0f + private var candidateTouchDownY = 0f + + 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 -> { + candidateTouchDownX = event.x + candidateTouchDownY = event.y + } + MotionEvent.ACTION_UP -> { + 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 -> { + candidateTouchDownX = 0f + candidateTouchDownY = 0f + } + } + false + } + } var expandedOpen: Boolean = false private set @@ -319,5 +355,6 @@ internal class CandidateBarController( private companion object { const val STRIP_LIMIT = 24 + const val CANDIDATE_EXPAND_SWIPE_DP = 36 } } From e596ef624f542ba6b970ec96dbed158dc690b88b Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:38:22 +0800 Subject: [PATCH 004/193] style: deepen default keyboard surface hierarchy --- .../llc/slacker/openime/ImeDesignTokens.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt index 063a18b3..37c6a2e0 100644 --- a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt +++ b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt @@ -120,18 +120,25 @@ enum class ImeTheme(val key: String, val label: String) { // minis_ime_dual_theme_renderer.html 的 Dark Obsidian / Light Crystal // 调色板。390 × 296 只是设计基准,尺寸仍由原生 View 的实际窗口计算。 IOS -> if (useDark) { + // Dark mode is a separate neutral scale rather than an + // inverted light palette. The keyboard base stays darkest, + // controls sit one surface above it, and typing keys occupy + // the clearest foreground plane. 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"), + c("#7FAAFF"), c("#16181C"), c("#1D2025"), c("#1F2228"), c("#F2F3F5"), + c("#2B2E34"), c("#F2F3F5"), c("#ADB2BC"), c("#22252B"), c("#D8DCE3"), c("#3A3E46"), + c("#22252B"), c("#343840"), c("#22252B"), c("#181B20"), c("#1F2228"), + c("#2B2E34"), c("#F2F3F5"), c("#22252B"), c("#D8DCE3"), c("#22252B"), c("#1D2025"), ) } else { + // Light mode deliberately separates the cool-gray keyboard + // base, secondary controls and near-white typing surfaces. + // Text remains neutral instead of using absolute black. 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"), + c("#1D9BF0"), c("#D9DCE2"), c("#F1F2F4"), c("#F5F6F8"), c("#202124"), + c("#FFFFFF"), c("#202124"), c("#5E6570"), c("#C8CCD4"), c("#343A44"), c("#E2E5EA"), + c("#F7F8FA"), c("#BCC1C9"), c("#C8CCD4"), c("#F5F6F8"), c("#F7F8FA"), + c("#FFFFFF"), c("#202124"), c("#C8CCD4"), c("#343A44"), c("#FFFFFF"), c("#EEF0F3"), ) } DARK -> Tokens( From 166c749c95be16fd706d3d8770b7b7ba8785574c Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:38:41 +0800 Subject: [PATCH 005/193] style: restore restrained key surface depth --- app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt index 45966ca0..a8c22d8f 100644 --- a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt +++ b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt @@ -93,7 +93,11 @@ internal class ImeThemeApplier( view.background = statefulRounded(color, pressedColor, skinRadiusPx()) view.background?.alpha = skinOpacity().coerceIn(70, 100) * 255 / 100 - view.elevation = 0f + // Keep shadows restrained: ordinary typing surfaces need a slight + // separation from the keyboard deck, while side/function controls + // stay visually embedded in that deck. + view.elevation = + if (side || function) 0f else toPx(1).toFloat() when { primary -> { From f54facf61f07f987e00d9cdd79cdb43d19b8c84c Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:40:44 +0800 Subject: [PATCH 006/193] feat: add space-drag cursor movement --- .../openime/SpaceVoiceGestureController.kt | 88 ++++++++++++++++--- 1 file changed, 78 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/SpaceVoiceGestureController.kt b/app/src/main/java/llc/slacker/openime/SpaceVoiceGestureController.kt index d96b4ace..b363cd35 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,24 +57,58 @@ 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 + 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 cancelNow = downY - rawY >= toPx(CANCEL_DISTANCE_DP) if (cancelNow != cancelPreview) { cancelPreview = cancelNow @@ -75,20 +118,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 +169,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 } @@ -120,5 +186,7 @@ internal class SpaceVoiceGestureController( private companion object { const val CANCEL_DISTANCE_DP = 48 + const val CURSOR_MODE_START_DP = 18 + const val CURSOR_STEP_DP = 12 } } From 2f1fcb5b5a6931cb2b07e62da3b8174b404ab2a4 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:40:57 +0800 Subject: [PATCH 007/193] feat: route horizontal space gestures to cursor mode --- .../main/java/llc/slacker/openime/SpaceVoiceKeyFactory.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 -> { From ac193a36de4638b7462c4c324756fbd4ae478363 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:41:13 +0800 Subject: [PATCH 008/193] feat: wire space cursor gestures to editor movement --- app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index ab4809ab..b59910a2 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -151,6 +151,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( From 3380f540e14ba92713f1cb2469705d470eec4da1 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:43:01 +0800 Subject: [PATCH 009/193] feat: expose Rime candidate deletion through JNI --- app/src/main/java/llc/slacker/openime/RimeNative.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/RimeNative.kt b/app/src/main/java/llc/slacker/openime/RimeNative.kt index b35d1650..9ea3c9ec 100644 --- a/app/src/main/java/llc/slacker/openime/RimeNative.kt +++ b/app/src/main/java/llc/slacker/openime/RimeNative.kt @@ -26,6 +26,10 @@ internal object RimeNative { @JvmStatic external fun nativeSelectCandidate(index: Int): String? + /** Remove a deletable candidate from the active Rime user dictionary. */ + @JvmStatic + external fun nativeDeleteCandidate(index: Int): Boolean + @JvmStatic external fun nativeCommitFirst(): String? From 1729928ae773f21c32f8e906512d78ab221e8b6e Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:43:15 +0800 Subject: [PATCH 010/193] feat: delete learned Rime candidates by absolute index --- app/src/main/cpp/local_rime_jni.cc | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/main/cpp/local_rime_jni.cc b/app/src/main/cpp/local_rime_jni.cc index 88f1be7f..aef3ba0a 100644 --- a/app/src/main/cpp/local_rime_jni.cc +++ b/app/src/main/cpp/local_rime_jni.cc @@ -330,6 +330,20 @@ Java_llc_slacker_openime_RimeNative_nativeSelectCandidate( return utf8_to_jstring(env, finish_selection()); } +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 jstring JNICALL Java_llc_slacker_openime_RimeNative_nativeCommitFirst( JNIEnv* env, jclass) { From e60e7933ee837cd3ef37aa554f36ecd7ccf693b7 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:43:41 +0800 Subject: [PATCH 011/193] feat: delete learned Rime candidates off the IME thread --- .../java/llc/slacker/openime/RimeEngine.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/RimeEngine.kt b/app/src/main/java/llc/slacker/openime/RimeEngine.kt index 4194f79c..40957dea 100644 --- a/app/src/main/java/llc/slacker/openime/RimeEngine.kt +++ b/app/src/main/java/llc/slacker/openime/RimeEngine.kt @@ -290,6 +290,38 @@ class RimeEngine( return "" } + /** + * 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 + RimeNative.nativeDeleteCandidate(entry.nativeIndex) + }.getOrDefault(false) + } + } + onComplete(deleted) + } + } + fun commitFirst(input: String, allowLearning: Boolean = true): String { if (!allowLearning) return candidates(input).firstOrNull().orEmpty() val normalized = RimeInputNormalizer.normalize(input) From b6a5e0deab951e78f4867dc9967b76cb4a4f45d7 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:43:53 +0800 Subject: [PATCH 012/193] feat: forget fallback learned phrases --- .../llc/slacker/openime/UserPhraseRepository.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/UserPhraseRepository.kt b/app/src/main/java/llc/slacker/openime/UserPhraseRepository.kt index eac84f16..49b6eca5 100644 --- a/app/src/main/java/llc/slacker/openime/UserPhraseRepository.kt +++ b/app/src/main/java/llc/slacker/openime/UserPhraseRepository.kt @@ -96,6 +96,22 @@ object UserPhraseRepository { } } + /** + * 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) { From d2b8274f29bdd8e4374f480108aa8af432bf5d3e Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:44:07 +0800 Subject: [PATCH 013/193] feat: expose long-press candidate management --- .../main/java/llc/slacker/openime/CandidateBarController.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/CandidateBarController.kt b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt index 8dc18c4f..4ba92d53 100644 --- a/app/src/main/java/llc/slacker/openime/CandidateBarController.kt +++ b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt @@ -40,6 +40,7 @@ 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 @@ -351,6 +352,11 @@ internal class CandidateBarController( onFeedback() onCandidateSelected(candidate) } + item.setOnLongClickListener { + onFeedback() + onCandidateLongPressed(candidate) + true + } } private companion object { From 346400201486efc79004b30bb94f39c9deb4bf61 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:44:25 +0800 Subject: [PATCH 014/193] feat: wire candidate long-press management --- app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index b59910a2..8bee9ad4 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -72,6 +72,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 @@ -1014,6 +1015,11 @@ open class ImeKeyboardView( }.apply { allowTwoLineLabel() contentDescription = "候选:$candidate" + setOnLongClickListener { + feedback() + listener.onCandidateLongPressed(candidate) + true + } } }, createEmptyLabel = { title("暂无候选", small = true) }, @@ -1028,6 +1034,7 @@ open class ImeKeyboardView( statefulBackground = ::statefulRounded, onFeedback = ::feedback, onCandidateSelected = listener::onCandidateSelected, + onCandidateLongPressed = listener::onCandidateLongPressed, ) mainDock.addView( topZone, From 81728a4a45cce254483457a5cf3ca703191288de Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:45:14 +0800 Subject: [PATCH 015/193] feat: delete learned candidates on long press --- .../slacker/openime/LocalVoiceImeService.kt | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt index a48ff85d..bde2f32c 100644 --- a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt +++ b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt @@ -926,6 +926,53 @@ 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 + } + + 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), + ) + } + + if (!rime.isReady) { + if (UserPhraseRepository.forget(composition, candidate)) { + refreshAfterDelete() + showTextEditFeedback("已移除个人候选") + } else { + showTextEditFeedback("该候选不是个人学习词") + } + return + } + + val queued = rime.deleteCandidate(composition, candidate) { deleted -> + mainHandler.post { + if (deleted) { + refreshAfterDelete() + showTextEditFeedback("已移除个人候选") + } else { + showTextEditFeedback("系统词条不可删除") + } + } + } + if (!queued) { + showTextEditFeedback("暂时无法修改个人词") + } + } + /** * Association ("联想") chips are produced only after a commit, so there is * no composition for [selectCandidate] to match against. Commit the word From 59163a70ec9951fa5135c2dcaea02aeb3862b23b Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:46:42 +0800 Subject: [PATCH 016/193] feat: add conservative adjacent-key pinyin correction --- .../llc/slacker/openime/CandidateEngine.kt | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/CandidateEngine.kt b/app/src/main/java/llc/slacker/openime/CandidateEngine.kt index 520119db..152052c4 100644 --- a/app/src/main/java/llc/slacker/openime/CandidateEngine.kt +++ b/app/src/main/java/llc/slacker/openime/CandidateEngine.kt @@ -339,6 +339,51 @@ class CandidateEngine(externalPinyin: Map> = emptyMap()) { private fun fuzzyVariants(py: String): List = pinyinFuzzyVariants(py) + /** + * 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) + } + getCandidates(variant, fuzzy) + .asSequence() + .filter { candidate -> + candidate != variant && + candidate.any { ch -> ch.code > 0x7f } + } + .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 +537,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 From 7b19f8db9e6db4965b641a0094178f1ec96125ea Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:47:02 +0800 Subject: [PATCH 017/193] feat: merge adjacent-key corrections behind normal pinyin --- .../llc/slacker/openime/CandidatePipeline.kt | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/CandidatePipeline.kt b/app/src/main/java/llc/slacker/openime/CandidatePipeline.kt index 6744cb70..8b58592d 100644 --- a/app/src/main/java/llc/slacker/openime/CandidatePipeline.kt +++ b/app/src/main/java/llc/slacker/openime/CandidatePipeline.kt @@ -52,7 +52,7 @@ class CandidatePipeline internal constructor( 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 +62,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 +217,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) From eb5e877585257e24f32a7e582f9ce47b9a0f4e95 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:49:12 +0800 Subject: [PATCH 018/193] feat: keep a safe one-shot snapshot for clear undo --- .../slacker/openime/InputConnectionGateway.kt | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/InputConnectionGateway.kt b/app/src/main/java/llc/slacker/openime/InputConnectionGateway.kt index f3e1bded..966a3760 100644 --- a/app/src/main/java/llc/slacker/openime/InputConnectionGateway.kt +++ b/app/src/main/java/llc/slacker/openime/InputConnectionGateway.kt @@ -63,13 +63,27 @@ class InputConnectionGateway( } } + private data class ClearUndoSnapshot( + val text: String, + val selectionStart: Int, + val selectionEnd: Int, + ) + + private var clearUndoSnapshot: ClearUndoSnapshot? = null + + private fun invalidateClearUndo() { + clearUndoSnapshot = null + } + 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 +122,7 @@ class InputConnectionGateway( } fun deleteBackwards() { + invalidateClearUndo() val ic = connection() ?: return if (deleteSelection()) return @@ -133,6 +148,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) { @@ -228,6 +244,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 +258,7 @@ class InputConnectionGateway( restoreSelectionAfterFailedClear(ic, originalSelection) } else { ic.finishComposingText() + rememberClearUndo(selected, originalSelection) } return cleared } @@ -259,6 +277,7 @@ class InputConnectionGateway( restoreSelectionAfterFailedClear(ic, originalSelection) } else { ic.finishComposingText() + rememberClearUndo(selectedWindow.text, originalSelection) } return cleared } @@ -278,6 +297,7 @@ class InputConnectionGateway( restoreSelectionAfterFailedClear(ic, originalSelection) } else { ic.finishComposingText() + rememberClearUndo(window.text, originalSelection) } cleared } finally { @@ -285,6 +305,78 @@ 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 + + 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?, + ) { + 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), + ) + } + fun performEditorAction(action: Int) { connection()?.performEditorAction(action) } @@ -495,6 +587,7 @@ 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) From 05f171c31b43783b31afa09f9e8da16d347ebdb4 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:50:08 +0800 Subject: [PATCH 019/193] feat: add guarded downward restore after clear gesture --- .../openime/BackspaceGestureController.kt | 84 +++++++++++++++---- 1 file changed, 68 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/BackspaceGestureController.kt b/app/src/main/java/llc/slacker/openime/BackspaceGestureController.kt index 00cf41e9..aa81b7b6 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) } - 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 { From 932652c526d33546319df1af734ed4857ae2a8f2 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:50:32 +0800 Subject: [PATCH 020/193] feat: wire guarded clear restore into backspace gesture --- app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index 8bee9ad4..8524dc7b 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -41,6 +41,7 @@ open class ImeKeyboardView( fun onCharacter(char: String) fun onBackspace() fun onClearAll() + fun onUndoClear(): Boolean = false fun onSpace() fun onFloatingKeyboardChanged(floating: Boolean) fun onFloatingKeyboardDragged(deltaX: Float, deltaY: Float) @@ -115,9 +116,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 { From 5e6152977b3ba6ab7a65b3d8e3b01510ab99bdc2 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:50:44 +0800 Subject: [PATCH 021/193] a11y: describe backspace clear restore gesture --- app/src/main/java/llc/slacker/openime/BackspaceKeyFactory.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/BackspaceKeyFactory.kt b/app/src/main/java/llc/slacker/openime/BackspaceKeyFactory.kt index c7b572b8..6cdcb5a3 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, From 71655e6ac325ad924212f35c7158e5d6a9069409 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:50:57 +0800 Subject: [PATCH 022/193] feat: restore last cleared editor snapshot --- .../slacker/openime/LocalVoiceImeService.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt index bde2f32c..d74bdb3b 100644 --- a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt +++ b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt @@ -670,6 +670,28 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can keyboardView?.renderState(state) } + 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?.renderState(state) + return true + } + override fun onFloatingKeyboardChanged(floating: Boolean) { keyboardView?.setFloatingWindowMode(floating) if (floating) floatingWindow.enable() else floatingWindow.restore() From b501ec7bfc912b1193cb8418f346efbdf10ef757 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:52:19 +0800 Subject: [PATCH 023/193] perf: keep adjacent-key correction off full candidate pipeline --- .../llc/slacker/openime/CandidateEngine.kt | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/CandidateEngine.kt b/app/src/main/java/llc/slacker/openime/CandidateEngine.kt index 152052c4..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,47 @@ 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 @@ -368,12 +409,7 @@ class CandidateEngine(externalPinyin: Map> = emptyMap()) { append(replacement) append(input, index + 1, input.length) } - getCandidates(variant, fuzzy) - .asSequence() - .filter { candidate -> - candidate != variant && - candidate.any { ch -> ch.code > 0x7f } - } + typoCandidatesFor(variant, fuzzy) .take(2) .forEach { candidate -> result += candidate From 5a0ab2a7a60d95dbe9d62d06e0c5ec82dc025e89 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:52:30 +0800 Subject: [PATCH 024/193] fix: invalidate clear restore on all text mutations --- .../java/llc/slacker/openime/InputConnectionGateway.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/InputConnectionGateway.kt b/app/src/main/java/llc/slacker/openime/InputConnectionGateway.kt index 966a3760..7e576ad1 100644 --- a/app/src/main/java/llc/slacker/openime/InputConnectionGateway.kt +++ b/app/src/main/java/llc/slacker/openime/InputConnectionGateway.kt @@ -204,6 +204,7 @@ class InputConnectionGateway( } fun deleteForwards() { + invalidateClearUndo() val ic = connection() ?: return if (deleteSelection()) return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { @@ -382,6 +383,7 @@ class InputConnectionGateway( } fun sendKeyDownUp(keyCode: Int) { + invalidateClearUndo() val ic = connection() ?: return sendKeyDownUp(ic, keyCode) } @@ -594,8 +596,10 @@ class InputConnectionGateway( 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 From 81b7ba135e9af1bc27e1035cec5c5a9e2c8cf206 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:54:26 +0800 Subject: [PATCH 025/193] perf: retain nine-key rail state and theme dynamic choices --- .../openime/NineKeySymbolRailController.kt | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt b/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt index cdea5757..84e4c44b 100644 --- a/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt +++ b/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt @@ -5,6 +5,7 @@ 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 @@ -24,8 +25,14 @@ internal class NineKeySymbolRailController( 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 { @@ -61,6 +68,9 @@ internal class NineKeySymbolRailController( onFeedback = onFeedback, ).also { rail = it + railMode = RailMode.SYMBOLS + renderedChoices = emptyList() + renderedSelected = null refreshPinyinFilters() } @@ -70,7 +80,11 @@ internal class NineKeySymbolRailController( * rail is actually in symbol mode. */ fun refreshSymbols() { - refreshPinyinFilters() + if (railMode == RailMode.SYMBOLS) { + renderSymbols(force = true) + } else { + refreshPinyinFilters() + } } private fun refreshPinyinFilters() { @@ -127,8 +141,10 @@ internal class NineKeySymbolRailController( } } - private fun renderSymbols() { + private fun renderSymbols(force: Boolean = false) { val scroll = rail ?: return + if (railMode == RailMode.SYMBOLS && !force) return + SymbolRailRenderer.populate( scroll = scroll, symbols = commonSymbols(), @@ -136,8 +152,12 @@ internal class NineKeySymbolRailController( 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( @@ -146,6 +166,13 @@ internal class NineKeySymbolRailController( onSelect: (String) -> Unit, ) { val scroll = rail ?: return + if ( + railMode == RailMode.PINYIN && + renderedChoices == choices && + renderedSelected == selected + ) { + return + } val content = scroll.getChildAt(0) as? LinearLayout ?: return val inheritedTextColor = (0 until content.childCount) .asSequence() @@ -189,7 +216,11 @@ internal class NineKeySymbolRailController( ) } + 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 From 79b092f7c26f3c0b8b6abaa8baed714369d01f76 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:55:01 +0800 Subject: [PATCH 026/193] style: theme dynamic nine-key rail content --- app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index 8524dc7b..4d283092 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -1719,6 +1719,7 @@ open class ImeKeyboardView( composition = composition, onCommit = listener::onCharacter, onFeedback = ::feedback, + onRailChanged = ::applyThemeToSubtree, ).also { nineKeySymbolRailController = it } } From 466c5e6c95940b34c5872dd01f875fba5943c500 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:56:12 +0800 Subject: [PATCH 027/193] fix: retain typo recovery when native pinyin is sparse --- .../java/llc/slacker/openime/LocalVoiceImeService.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt index d74bdb3b..e006e8ee 100644 --- a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt +++ b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt @@ -1213,7 +1213,15 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can emptyList() } val finalCandidates = if (native.isNotEmpty()) { - native.map { it.text } + val nativeText = native.map { it.text } + 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) } @@ -1390,6 +1398,7 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can 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 } From 53eee81ad9229dba5f9b73186cc72b468571d963 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:56:22 +0800 Subject: [PATCH 028/193] feat: forget removed candidate personalization --- .../llc/slacker/openime/PersonalizationRepository.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) 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() From 1155b0cac03455e3eb525d569d09ec247ae9a3d6 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:56:34 +0800 Subject: [PATCH 029/193] fix: remove deleted candidates from personalization bias --- app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt index e006e8ee..5f95c17f 100644 --- a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt +++ b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt @@ -972,6 +972,7 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can if (!rime.isReady) { if (UserPhraseRepository.forget(composition, candidate)) { + PersonalizationRepository.forget(candidate) refreshAfterDelete() showTextEditFeedback("已移除个人候选") } else { @@ -983,6 +984,7 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can val queued = rime.deleteCandidate(composition, candidate) { deleted -> mainHandler.post { if (deleted) { + PersonalizationRepository.forget(candidate) refreshAfterDelete() showTextEditFeedback("已移除个人候选") } else { From ca5f496b6af90a139eb1b5b98530d85d2ae91d09 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:58:36 +0800 Subject: [PATCH 030/193] feat: persist one-hand and keyboard height preferences --- .../slacker/openime/ImeSettingsRepository.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt b/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt index 49e13220..6efc19ca 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,8 @@ 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" /** * The user's preferred Chinese layout (26-key vs 9-key). Only PINYIN_26 and @@ -38,6 +46,30 @@ object ImeSettingsRepository { .edit().putString(KEY_PREFERRED_CHINESE_MODE, mode.name).apply() } + fun loadHandedness(context: Context): ImeHandedness = + runCatching { + ImeHandedness.valueOf( + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .getString(KEY_HANDEDNESS, ImeHandedness.STANDARD.name) + ?: ImeHandedness.STANDARD.name, + ) + }.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() + } + /** * Every ImeTheme ships a complete token set in ImeDesignTokens, but the * getter used to hard-code IOS, which made four finished skins unreachable From bcf809096270f5d2a59642288914baa9fb286848 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:58:50 +0800 Subject: [PATCH 031/193] feat: make keyboard row geometry height-adjustable --- app/src/main/java/llc/slacker/openime/KeyboardGeometry.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/KeyboardGeometry.kt b/app/src/main/java/llc/slacker/openime/KeyboardGeometry.kt index a94f03c6..802d7edb 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 = From 4a8ef4b6e64059086d91972f5faf3b991e6602e6 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:59:21 +0800 Subject: [PATCH 032/193] feat: apply one-hand width and adjustable keyboard height --- .../llc/slacker/openime/ImeKeyboardView.kt | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index 4d283092..94ee0b7d 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -222,9 +222,12 @@ 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 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) @@ -810,6 +813,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. @@ -930,6 +934,22 @@ open class ImeKeyboardView( */ private fun updateResponsiveGeometry(measuredWidthPx: Int) { if (measuredWidthPx <= 0) return + + (mainDock.layoutParams as? FrameLayout.LayoutParams)?.let { params -> + if (floatingKeyboardController.enabled || keyboardHandedness == ImeHandedness.STANDARD) { + params.width = FrameLayout.LayoutParams.MATCH_PARENT + params.gravity = Gravity.TOP + } else { + params.width = (measuredWidthPx * 0.82f).toInt().coerceAtLeast(dp(280)) + 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. @@ -1404,6 +1424,29 @@ 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) + updateResponsiveGeometry(width) + } + + private fun setKeyboardHeightPercent(percent: Int) { + val bounded = percent.coerceIn(92, 120) + if (keyboardHeightPercent == bounded) return + keyboardHeightPercent = bounded + ImeSettingsRepository.saveKeyboardHeightPercent(context, bounded) + layoutMetrics = KeyboardLayoutMetrics( + landscape = appliedOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE, + fontScale = appliedFontScale, + heightPercent = keyboardHeightPercent, + ) + applyDynamicHeights() + if (!standalonePanel) { + if (panel == Panel.NONE) renderModeBody() else renderPanel(panel) + } + } + internal fun applyPersistedSettings( newTheme: ImeTheme, newAppearance: ImeAppearance, @@ -1417,6 +1460,10 @@ open class ImeKeyboardView( primaryColor: String, ) { val normalizedColor = AccentPalette.normalize(primaryColor) + val persistedHandedness = ImeSettingsRepository.loadHandedness(context) + val persistedHeight = ImeSettingsRepository.loadKeyboardHeightPercent(context) + val geometryChanged = keyboardHandedness != persistedHandedness || + keyboardHeightPercent != persistedHeight val visualChanged = theme != newTheme || appearance != newAppearance || skinOpacity != opacity || @@ -1434,6 +1481,20 @@ open class ImeKeyboardView( skinRadius = radius skinFontSize = fontSize skinPrimaryColor = normalizedColor + if (geometryChanged) { + keyboardHandedness = persistedHandedness + keyboardHeightPercent = persistedHeight + layoutMetrics = KeyboardLayoutMetrics( + landscape = appliedOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE, + fontScale = appliedFontScale, + heightPercent = keyboardHeightPercent, + ) + applyDynamicHeights() + if (!standalonePanel) { + if (panel == Panel.NONE) renderModeBody() else renderPanel(panel) + } + updateResponsiveGeometry(width) + } if (visualChanged) applyTheme() } From 8e6725048471d705ec2894377a1725f0962899df Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:00:07 +0800 Subject: [PATCH 033/193] feat: expose one-hand and keyboard height controls --- .../openime/SettingsPanelController.kt | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt index 48fa0f2b..30a7624c 100644 --- a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt +++ b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt @@ -51,10 +51,14 @@ 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 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 onShowFuzzySettings: () -> Unit, private val onFeedback: () -> Unit, private val applyTheme: () -> Unit, @@ -138,6 +142,32 @@ 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( accentColorRow(), @@ -777,6 +807,7 @@ internal class SettingsPanelController( "圆角" -> " dp" "不透明度" -> "%" "按键字号" -> " sp" + "键盘高度" -> "%" else -> "" } val seekBar = SeekBar(context).apply { From 3168ebedf81b94a05675aee6cd8a9d056a89c972 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:00:23 +0800 Subject: [PATCH 034/193] feat: wire keyboard layout preferences into live IME --- app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index 94ee0b7d..6fe9e238 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -568,6 +568,8 @@ open class ImeKeyboardView( currentSkinRadius = { skinRadius }, currentSkinFontSize = { skinFontSize }, currentSkinColor = { skinPrimaryColor }, + currentHandedness = { keyboardHandedness }, + currentKeyboardHeightPercent = { keyboardHeightPercent }, onThemeSelected = ::setTheme, onAppearanceSelected = { selected -> setAppearance(selected) @@ -587,6 +589,8 @@ open class ImeKeyboardView( ) applyTheme() }, + onHandednessChanged = ::setHandedness, + onKeyboardHeightChanged = ::setKeyboardHeightPercent, onShowFuzzySettings = { showPanel(Panel.FUZZY_SETTINGS) }, onFeedback = ::feedback, applyTheme = ::applyTheme, From ba881be65304fee6fdfd5e385c2a22340ca1d2d6 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:01:05 +0800 Subject: [PATCH 035/193] fix: derive one-hand geometry from narrowed keyboard width --- .../llc/slacker/openime/ImeKeyboardView.kt | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index 6fe9e238..e11c5fda 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -939,12 +939,21 @@ open class ImeKeyboardView( private fun updateResponsiveGeometry(measuredWidthPx: Int) { if (measuredWidthPx <= 0) return - (mainDock.layoutParams as? FrameLayout.LayoutParams)?.let { params -> + 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 = (measuredWidthPx * 0.82f).toInt().coerceAtLeast(dp(280)) + params.width = dockWidthPx params.gravity = Gravity.TOP or if (keyboardHandedness == ImeHandedness.LEFT) { Gravity.START } else { @@ -979,7 +988,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), @@ -987,7 +996,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 @@ -1446,8 +1455,10 @@ open class ImeKeyboardView( heightPercent = keyboardHeightPercent, ) applyDynamicHeights() - if (!standalonePanel) { - if (panel == Panel.NONE) renderModeBody() else renderPanel(panel) + if (!standalonePanel && panel == Panel.NONE) { + renderModeBody() + } else { + requestLayout() } } @@ -1494,8 +1505,10 @@ open class ImeKeyboardView( heightPercent = keyboardHeightPercent, ) applyDynamicHeights() - if (!standalonePanel) { - if (panel == Panel.NONE) renderModeBody() else renderPanel(panel) + if (!standalonePanel && panel == Panel.NONE) { + renderModeBody() + } else { + requestLayout() } updateResponsiveGeometry(width) } From 14b8641f74cc53f7c248e0d6ad523c4d58a5c16b Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:03:35 +0800 Subject: [PATCH 036/193] fix: scale total IME height with row height preference --- app/src/main/java/llc/slacker/openime/KeyboardGeometry.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/KeyboardGeometry.kt b/app/src/main/java/llc/slacker/openime/KeyboardGeometry.kt index 802d7edb..34ea84b0 100644 --- a/app/src/main/java/llc/slacker/openime/KeyboardGeometry.kt +++ b/app/src/main/java/llc/slacker/openime/KeyboardGeometry.kt @@ -84,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 From a8920166c53bfef78d487a7f7f581fde137ee957 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:04:44 +0800 Subject: [PATCH 037/193] feat: add stable input-code editor id --- app/src/main/res/values/editor_ids.xml | 1 + 1 file changed, 1 insertion(+) 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 @@ + From 42006931063ec453716ea3f8108c225aeedf6b98 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:05:07 +0800 Subject: [PATCH 038/193] feat: add backward-compatible quick phrase input codes --- .../slacker/openime/QuickPhraseRepository.kt | 54 +++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/QuickPhraseRepository.kt b/app/src/main/java/llc/slacker/openime/QuickPhraseRepository.kt index f413db15..d5326fab 100644 --- a/app/src/main/java/llc/slacker/openime/QuickPhraseRepository.kt +++ b/app/src/main/java/llc/slacker/openime/QuickPhraseRepository.kt @@ -8,6 +8,7 @@ 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. */ @@ -30,6 +31,7 @@ object QuickPhraseRepository { id = item.optLong("id", index.toLong() + 1L), category = item.optString("category", "常用").ifBlank { "常用" }, text = text, + inputCode = normalizeInputCode(item.optString("input_code", "")), ), ) } @@ -38,19 +40,57 @@ object QuickPhraseRepository { }.getOrElse { defaults() } } - 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 +103,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,7 +126,8 @@ object QuickPhraseRepository { JSONObject() .put("id", item.id) .put("category", item.category) - .put("text", item.text), + .put("text", item.text) + .put("input_code", item.inputCode), ) } context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) From 43564cbb8aada31cb334138e4392132088d213a6 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:05:46 +0800 Subject: [PATCH 039/193] feat: edit quick phrase input codes --- .../openime/QuickPhraseEditActivity.kt | 64 +++++++++++++++++-- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/QuickPhraseEditActivity.kt b/app/src/main/java/llc/slacker/openime/QuickPhraseEditActivity.kt index 09668a0a..c9849716 100644 --- a/app/src/main/java/llc/slacker/openime/QuickPhraseEditActivity.kt +++ b/app/src/main/java/llc/slacker/openime/QuickPhraseEditActivity.kt @@ -25,13 +25,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 +50,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 +88,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,7 +101,7 @@ class QuickPhraseEditActivity : Activity() { super.onSaveInstanceState(outState) } - private fun render(category: String, phrase: String) { + private fun render(category: String, code: String, phrase: String) { val accent = SetupUi.accent(this) val title = TextView(this).apply { text = if (phraseId > 0L) "编辑常用语" else "新增常用语" @@ -130,6 +141,17 @@ class QuickPhraseEditActivity : Activity() { textSize = 16f } 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 = 16f + } + SetupUi.styleInput(this, codeEdit) phraseEdit = EditText(this).apply { id = R.id.quick_phrase_text_editor hint = "输入常用语" @@ -150,6 +172,7 @@ class QuickPhraseEditActivity : Activity() { phraseId, categoryEdit.text.toString(), phraseEdit.text.toString(), + inputCode = codeEdit.text.toString(), ) != null ) { finish() @@ -179,6 +202,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 +246,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 = 12f + 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, @@ -283,8 +331,12 @@ class QuickPhraseEditActivity : Activity() { } 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 From f1dcca4191efee9d78aa8c7b777b721824e8de46 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:05:58 +0800 Subject: [PATCH 040/193] feat: pass quick phrase input code to editor --- app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index e11c5fda..15ae1c26 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -1991,6 +1991,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) } From a1759851b3805ca22cce3dc2c0463df7b4eef87f Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:06:17 +0800 Subject: [PATCH 041/193] feat: surface quick phrases from user-defined input codes --- .../slacker/openime/LocalVoiceImeService.kt | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt index 5f95c17f..5d8f92a8 100644 --- a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt +++ b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt @@ -1166,8 +1166,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 { @@ -1216,7 +1234,22 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can } val finalCandidates = if (native.isNotEmpty()) { val nativeText = native.map { it.text } - if ( + 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 ) { From 7ec21f3066d4320b64c2d4aacc6b56eaa8a579d8 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:06:43 +0800 Subject: [PATCH 042/193] a11y: expose quick phrase input codes --- .../java/llc/slacker/openime/ClipboardPanelController.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ClipboardPanelController.kt b/app/src/main/java/llc/slacker/openime/ClipboardPanelController.kt index 743afabf..bc4a0e1a 100644 --- a/app/src/main/java/llc/slacker/openime/ClipboardPanelController.kt +++ b/app/src/main/java/llc/slacker/openime/ClipboardPanelController.kt @@ -275,6 +275,12 @@ internal class ClipboardPanelController( }.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) From 3d084189676060626d67dd50f767337a2acf5b8d Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:08:10 +0800 Subject: [PATCH 043/193] fix: keep quick phrases out of Rime deletion flow --- .../java/llc/slacker/openime/LocalVoiceImeService.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt index 5d8f92a8..f6da73ed 100644 --- a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt +++ b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt @@ -959,6 +959,18 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can return } + if ( + QuickPhraseRepository.candidatesForInputCode( + context = this, + rawCode = composition, + exactOnly = false, + limit = 16, + ).contains(candidate) + ) { + showTextEditFeedback("这是常用语,请在常用语面板中编辑或删除") + return + } + val modeAtRequest = state.keyboardMode fun refreshAfterDelete() { if (lastComposition != composition || state.keyboardMode != modeAtRequest) return From 4f949fc3193b9721e88b5e042c6f89fbb8dcc00b Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:08:52 +0800 Subject: [PATCH 044/193] fix: rebuild keys after height changes made inside settings --- app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index 15ae1c26..6a6635e5 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -1253,6 +1253,9 @@ open class ImeKeyboardView( } stopVoiceIfActive() panel = Panel.NONE + if (renderedMode != mode) { + renderModeBody() + } expandedPanel.animate().cancel() expandedPanel.visibility = View.GONE mainDock.animate().cancel() @@ -1449,6 +1452,7 @@ open class ImeKeyboardView( if (keyboardHeightPercent == bounded) return keyboardHeightPercent = bounded ImeSettingsRepository.saveKeyboardHeightPercent(context, bounded) + renderedMode = null layoutMetrics = KeyboardLayoutMetrics( landscape = appliedOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE, fontScale = appliedFontScale, @@ -1477,8 +1481,8 @@ open class ImeKeyboardView( val normalizedColor = AccentPalette.normalize(primaryColor) val persistedHandedness = ImeSettingsRepository.loadHandedness(context) val persistedHeight = ImeSettingsRepository.loadKeyboardHeightPercent(context) - val geometryChanged = keyboardHandedness != persistedHandedness || - keyboardHeightPercent != persistedHeight + val heightChanged = keyboardHeightPercent != persistedHeight + val geometryChanged = keyboardHandedness != persistedHandedness || heightChanged val visualChanged = theme != newTheme || appearance != newAppearance || skinOpacity != opacity || @@ -1499,6 +1503,7 @@ open class ImeKeyboardView( if (geometryChanged) { keyboardHandedness = persistedHandedness keyboardHeightPercent = persistedHeight + if (heightChanged) renderedMode = null layoutMetrics = KeyboardLayoutMetrics( landscape = appliedOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE, fontScale = appliedFontScale, From a0c162764e22833d0e6078a4a2bc5c404a355e31 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:09:30 +0800 Subject: [PATCH 045/193] perf: cache parsed quick phrases on candidate hot path --- .../slacker/openime/QuickPhraseRepository.kt | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/QuickPhraseRepository.kt b/app/src/main/java/llc/slacker/openime/QuickPhraseRepository.kt index d5326fab..88cc5396 100644 --- a/app/src/main/java/llc/slacker/openime/QuickPhraseRepository.kt +++ b/app/src/main/java/llc/slacker/openime/QuickPhraseRepository.kt @@ -15,11 +15,27 @@ data class QuickPhrase( 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()) { @@ -38,6 +54,11 @@ object QuickPhraseRepository { } } }.getOrElse { defaults() } + synchronized(cacheLock) { + cachedRaw = raw + cachedItems = parsed + } + return parsed } fun upsert( @@ -130,9 +151,14 @@ object QuickPhraseRepository { .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() } } From 9c33065498cea42884bae5ebe544a8fbb7e02f3f Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:10:30 +0800 Subject: [PATCH 046/193] feat: notify hosts of keyboard layout preference changes --- app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index 6a6635e5..bda56e49 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -97,6 +97,8 @@ 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) {} } /** Visual class marker for white keys (nine/digits grid). */ @@ -1444,6 +1446,7 @@ open class ImeKeyboardView( if (keyboardHandedness == next) return keyboardHandedness = next ImeSettingsRepository.saveHandedness(context, next) + listener.onHandednessChanged(next) updateResponsiveGeometry(width) } @@ -1452,6 +1455,7 @@ open class ImeKeyboardView( 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, From 0b9dc767738d1b4e3c662f1c73b408e0809fa0f3 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:10:41 +0800 Subject: [PATCH 047/193] feat: sync layout preferences from settings activity --- .../main/java/llc/slacker/openime/ImeSettingsActivity.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeSettingsActivity.kt b/app/src/main/java/llc/slacker/openime/ImeSettingsActivity.kt index 40a07e4c..8dad6e62 100644 --- a/app/src/main/java/llc/slacker/openime/ImeSettingsActivity.kt +++ b/app/src/main/java/llc/slacker/openime/ImeSettingsActivity.kt @@ -196,4 +196,12 @@ 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() + } } From f2abe540bd156e1933dfd473bd36c2191d23790d Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:11:50 +0800 Subject: [PATCH 048/193] feat: persist floating keyboard size and opacity --- .../slacker/openime/ImeSettingsRepository.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt b/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt index 6efc19ca..440de9b1 100644 --- a/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt +++ b/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt @@ -25,6 +25,8 @@ object ImeSettingsRepository { 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 @@ -70,6 +72,26 @@ object ImeSettingsRepository { .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() + } + /** * Every ImeTheme ships a complete token set in ImeDesignTokens, but the * getter used to hard-code IOS, which made four finished skins unreachable From ca3d10ecf3e5688685ccc23a4546caccbbc180ca Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:12:10 +0800 Subject: [PATCH 049/193] feat: apply floating keyboard size and window opacity --- .../openime/FloatingWindowController.kt | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/FloatingWindowController.kt b/app/src/main/java/llc/slacker/openime/FloatingWindowController.kt index d1516b8b..f9ce8b5f 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,19 @@ 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 minimum = dp(if (landscape) 300 else 280) return minOf( preferred, maximum, (screenWidth - dp(16)).coerceAtLeast(dp(1)), - ).coerceAtLeast(dp(320)) + ).coerceAtLeast(minimum.coerceAtMost(screenWidth)) } private fun displaySize(): Pair { From c96e0c2c4424844a218fc4cbe7981084e3698187 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:12:43 +0800 Subject: [PATCH 050/193] feat: expose floating keyboard size and opacity controls --- .../openime/SettingsPanelController.kt | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt index 30a7624c..2b217932 100644 --- a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt +++ b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt @@ -53,12 +53,15 @@ internal class SettingsPanelController( 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 onFeedback: () -> Unit, private val applyTheme: () -> Unit, @@ -168,6 +171,29 @@ internal class SettingsPanelController( 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(), @@ -807,7 +833,7 @@ internal class SettingsPanelController( "圆角" -> " dp" "不透明度" -> "%" "按键字号" -> " sp" - "键盘高度" -> "%" + "键盘高度", "浮动宽度", "浮动透明度" -> "%" else -> "" } val seekBar = SeekBar(context).apply { From 7c8011a25e5954f645c6be29cd3931665f462b04 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:13:08 +0800 Subject: [PATCH 051/193] feat: wire floating style preferences through keyboard settings --- .../llc/slacker/openime/ImeKeyboardView.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index bda56e49..d17fa93b 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -99,6 +99,7 @@ open class ImeKeyboardView( fun onSkinChanged(opacity: Int, radius: Int, fontSize: Int, primaryColor: String) {} fun onHandednessChanged(handedness: ImeHandedness) {} fun onKeyboardHeightChanged(percent: Int) {} + fun onFloatingStyleChanged(widthPercent: Int, opacityPercent: Int) {} } /** Visual class marker for white keys (nine/digits grid). */ @@ -226,6 +227,8 @@ open class ImeKeyboardView( 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, @@ -572,6 +575,8 @@ open class ImeKeyboardView( currentSkinColor = { skinPrimaryColor }, currentHandedness = { keyboardHandedness }, currentKeyboardHeightPercent = { keyboardHeightPercent }, + currentFloatingWidthPercent = { floatingWidthPercent }, + currentFloatingOpacityPercent = { floatingOpacityPercent }, onThemeSelected = ::setTheme, onAppearanceSelected = { selected -> setAppearance(selected) @@ -593,6 +598,7 @@ open class ImeKeyboardView( }, onHandednessChanged = ::setHandedness, onKeyboardHeightChanged = ::setKeyboardHeightPercent, + onFloatingStyleChanged = ::setFloatingStyle, onShowFuzzySettings = { showPanel(Panel.FUZZY_SETTINGS) }, onFeedback = ::feedback, applyTheme = ::applyTheme, @@ -1450,6 +1456,17 @@ open class ImeKeyboardView( 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 @@ -1485,6 +1502,11 @@ open class ImeKeyboardView( 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 || @@ -1521,6 +1543,14 @@ open class ImeKeyboardView( } updateResponsiveGeometry(width) } + if (floatingStyleChanged) { + floatingWidthPercent = persistedFloatingWidth + floatingOpacityPercent = persistedFloatingOpacity + listener.onFloatingStyleChanged( + floatingWidthPercent, + floatingOpacityPercent, + ) + } if (visualChanged) applyTheme() } From afd448b60c3c13304e5324c4ee948e75a83b83fc Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:13:24 +0800 Subject: [PATCH 052/193] feat: reapply live floating window style changes --- .../main/java/llc/slacker/openime/LocalVoiceImeService.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt index f6da73ed..97fb4131 100644 --- a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt +++ b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt @@ -47,6 +47,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) }, @@ -701,6 +703,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 From 28c10da9a2046d1f0a8474a1483f66473c1027d3 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:13:34 +0800 Subject: [PATCH 053/193] feat: sync floating style from settings activity --- app/src/main/java/llc/slacker/openime/ImeSettingsActivity.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeSettingsActivity.kt b/app/src/main/java/llc/slacker/openime/ImeSettingsActivity.kt index 8dad6e62..d753a0e7 100644 --- a/app/src/main/java/llc/slacker/openime/ImeSettingsActivity.kt +++ b/app/src/main/java/llc/slacker/openime/ImeSettingsActivity.kt @@ -204,4 +204,9 @@ class ImeSettingsActivity : Activity(), ImeKeyboardView.Listener { ImeSettingsRepository.saveKeyboardHeightPercent(this, percent) refreshLiveIme() } + override fun onFloatingStyleChanged(widthPercent: Int, opacityPercent: Int) { + ImeSettingsRepository.saveFloatingWidthPercent(this, widthPercent) + ImeSettingsRepository.saveFloatingOpacityPercent(this, opacityPercent) + refreshLiveIme() + } } From 50acdabea3c26bc161380c3690231f83684f72f5 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:13:53 +0800 Subject: [PATCH 054/193] fix: preserve floating window edge margin on small displays --- .../java/llc/slacker/openime/FloatingWindowController.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/FloatingWindowController.kt b/app/src/main/java/llc/slacker/openime/FloatingWindowController.kt index f9ce8b5f..365dd65d 100644 --- a/app/src/main/java/llc/slacker/openime/FloatingWindowController.kt +++ b/app/src/main/java/llc/slacker/openime/FloatingWindowController.kt @@ -163,12 +163,13 @@ internal class FloatingWindowController( val maximum = dp( if (landscape) ImeGeometryTokens.FLOATING_LANDSCAPE_WIDTH_DP else 420, ) - val minimum = dp(if (landscape) 300 else 280) + 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(minimum.coerceAtMost(screenWidth)) + available, + ).coerceAtLeast(minimum) } private fun displaySize(): Pair { From 9eee5523085dc9855db5d0742b61023c26e7ce1c Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:27:37 +0800 Subject: [PATCH 055/193] style: add shared semantic color blending primitive --- .../llc/slacker/openime/ImeDrawableFactory.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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), From c51d87a6feae9b655cef4d035b8103bd00af851d Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:28:05 +0800 Subject: [PATCH 056/193] style: establish semantic surface hierarchy for light and dark modes --- .../llc/slacker/openime/ImeDesignTokens.kt | 64 ++++++++++++++++--- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt index 37c6a2e0..4ff0e2cc 100644 --- a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt +++ b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt @@ -125,20 +125,20 @@ enum class ImeTheme(val key: String, val label: String) { // controls sit one surface above it, and typing keys occupy // the clearest foreground plane. Tokens( - c("#7FAAFF"), c("#16181C"), c("#1D2025"), c("#1F2228"), c("#F2F3F5"), - c("#2B2E34"), c("#F2F3F5"), c("#ADB2BC"), c("#22252B"), c("#D8DCE3"), c("#3A3E46"), - c("#22252B"), c("#343840"), c("#22252B"), c("#181B20"), c("#1F2228"), - c("#2B2E34"), c("#F2F3F5"), c("#22252B"), c("#D8DCE3"), c("#22252B"), c("#1D2025"), + c("#0A84FF"), 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"), ) } else { // Light mode deliberately separates the cool-gray keyboard // base, secondary controls and near-white typing surfaces. // Text remains neutral instead of using absolute black. Tokens( - c("#1D9BF0"), c("#D9DCE2"), c("#F1F2F4"), c("#F5F6F8"), c("#202124"), - c("#FFFFFF"), c("#202124"), c("#5E6570"), c("#C8CCD4"), c("#343A44"), c("#E2E5EA"), - c("#F7F8FA"), c("#BCC1C9"), c("#C8CCD4"), c("#F5F6F8"), c("#F7F8FA"), - c("#FFFFFF"), c("#202124"), c("#C8CCD4"), c("#343A44"), c("#FFFFFF"), c("#EEF0F3"), + c("#007AFF"), 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"), ) } DARK -> Tokens( @@ -171,6 +171,54 @@ 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 subtleAccentSurface(tokens: ImeTheme.Tokens): Int = + ImeDrawableFactory.blend( + tokens.primary, + tokens.toolCardBackground, + if (isDark(tokens)) 0.18f else 0.08f, + ) + + 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() From d45230fa9643f946a53ea474bdb6341a432ba3fa Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:28:55 +0800 Subject: [PATCH 057/193] style: unify selected pressed and layered surface states --- .../llc/slacker/openime/ImeThemeApplier.kt | 47 ++++++++++++------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt index a8c22d8f..398ecc54 100644 --- a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt +++ b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt @@ -122,23 +122,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), ) } @@ -168,6 +169,14 @@ internal class ImeThemeApplier( toPx(ImeGeometryTokens.CARD_RADIUS_DP), ) } + "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 -> { if ( (view.tag as? String)?.startsWith("tool:") == true && @@ -175,7 +184,7 @@ internal class ImeThemeApplier( ) { view.background = statefulRounded( t.toolCardBackground, - t.keyPressedBackground, + ImeSurfacePolicy.pressedSurface(t.toolCardBackground, t), toPx(ImeGeometryTokens.CONTROL_RADIUS_DP), ) } @@ -189,7 +198,7 @@ internal class ImeThemeApplier( ) { view.background = statefulRounded( t.toolCardBackground, - t.keyPressedBackground, + ImeSurfacePolicy.pressedSurface(t.toolCardBackground, t), toPx(ImeGeometryTokens.CONTROL_RADIUS_DP), ) } @@ -235,7 +244,7 @@ internal class ImeThemeApplier( view.imageTintList = ColorStateList.valueOf(t.keyText) view.background = statefulRounded( t.panelHeadBackground, - ImeDrawableFactory.dim(t.panelHeadBackground), + ImeSurfacePolicy.pressedSurface(t.panelHeadBackground, t), toPx(ImeGeometryTokens.CONTROL_RADIUS_DP), ) } @@ -247,7 +256,7 @@ internal class ImeThemeApplier( if (view.isClickable) { view.background = statefulRounded( Color.TRANSPARENT, - t.keyPressedBackground, + ImeSurfacePolicy.pressedSurface(t.toolbarBackground, t), toPx(ImeGeometryTokens.CONTROL_RADIUS_DP), ) } @@ -296,10 +305,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), ) } @@ -361,16 +371,17 @@ 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" -> { - 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, 0.86f), + selected, + ImeSurfacePolicy.pressedSurface(selected, t), toPx(ImeGeometryTokens.KEY_RADIUS_DP), ) } @@ -435,8 +446,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), ) } @@ -531,7 +542,7 @@ internal class ImeThemeApplier( ) } "setting-divider" -> - view.setBackgroundColor(t.border) + view.setBackgroundColor(ImeSurfacePolicy.divider(t)) } } From 99b06f1fa3139cd3b9c91e5ea69c4e6d2b2e2642 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:29:35 +0800 Subject: [PATCH 058/193] style: derive consistent primary pressed state --- app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt index 4ff0e2cc..bf492ccb 100644 --- a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt +++ b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt @@ -205,6 +205,13 @@ internal object ImeSurfacePolicy { if (isDark(tokens)) 0.12f else 0.08f, ) + fun primaryPressed(tokens: ImeTheme.Tokens): Int = + ImeDrawableFactory.blend( + if (isDark(tokens)) Color.WHITE else Color.BLACK, + tokens.primary, + if (isDark(tokens)) 0.10f else 0.12f, + ) + fun subtleAccentSurface(tokens: ImeTheme.Tokens): Int = ImeDrawableFactory.blend( tokens.primary, From c42e9a09f477a8f134d3030eda830a1c06760023 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:29:49 +0800 Subject: [PATCH 059/193] style: remove ad hoc dim states from themed controls --- .../llc/slacker/openime/ImeThemeApplier.kt | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt index 398ecc54..a1ce3ffd 100644 --- a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt +++ b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt @@ -83,10 +83,10 @@ 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()) @@ -318,7 +318,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), ) } @@ -327,7 +327,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), ) } @@ -341,7 +341,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), ) } @@ -350,7 +350,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), ) } @@ -390,7 +390,7 @@ internal class ImeThemeApplier( view.setTextColor(t.sideKeyText) view.background = statefulRounded( Color.TRANSPARENT, - t.keyPressedBackground, + ImeSurfacePolicy.pressedSurface(t.sideKeyBackground, t), toPx(ImeGeometryTokens.KEY_RADIUS_DP), ) } @@ -435,7 +435,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), ) } @@ -467,7 +467,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), ) } @@ -481,7 +481,7 @@ internal class ImeThemeApplier( ) view.background = statefulRounded( t.primary, - ImeDrawableFactory.dim(t.primary, 0.88f), + ImeSurfacePolicy.primaryPressed(t), toPx(ImeGeometryTokens.CONTROL_RADIUS_DP), ) } @@ -503,14 +503,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), ) } From a876bd89fdfeaa9c136b503b9a619ab4dd567a7f Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:30:14 +0800 Subject: [PATCH 060/193] style: finish semantic pressed states in theme applier --- app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt index a1ce3ffd..239b6a92 100644 --- a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt +++ b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt @@ -361,7 +361,7 @@ internal class ImeThemeApplier( ) view.background = statefulRounded( t.destructive, - ImeDrawableFactory.dim(t.destructive, 0.86f), + ImeSurfacePolicy.pressedSurface(t.destructive, t), toPx(ImeGeometryTokens.CONTROL_RADIUS_DP), ) } @@ -410,13 +410,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), ) } From 7e797233030c01beb34f40f5edaca3f933cecb96 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:30:34 +0800 Subject: [PATCH 061/193] style: align candidate emphasis and disabled state with semantic palette --- .../slacker/openime/CandidateBarController.kt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/CandidateBarController.kt b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt index 4ba92d53..887e6eba 100644 --- a/app/src/main/java/llc/slacker/openime/CandidateBarController.kt +++ b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt @@ -153,7 +153,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) { @@ -342,10 +343,18 @@ 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.setOnClickListener { From 5b6b245ef33c535236bc219f06a67b326a495351 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:31:02 +0800 Subject: [PATCH 062/193] style: give key popups elevated bordered surfaces --- .../llc/slacker/openime/KeyPopupController.kt | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/KeyPopupController.kt b/app/src/main/java/llc/slacker/openime/KeyPopupController.kt index cfe832f5..a0b77832 100644 --- a/app/src/main/java/llc/slacker/openime/KeyPopupController.kt +++ b/app/src/main/java/llc/slacker/openime/KeyPopupController.kt @@ -55,7 +55,17 @@ internal class KeyPopupController( setPadding(dp(8), dp(6), dp(8), dp(6)) val popupBackground = if (text == "清空") t.destructive else t.keyBackground setTextColor(if (text == "清空") contrastText(popupBackground) else t.keyText) - background = rounded(popupBackground, dp(ImeGeometryTokens.CONTROL_RADIUS_DP)) + 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), + ) + } elevation = dp(2).toFloat() } @@ -73,7 +83,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 = "长按符号选择" } @@ -88,7 +103,7 @@ internal class KeyPopupController( setTextColor(t.keyText) background = statefulRounded( Color.TRANSPARENT, - t.keyPressedBackground, + ImeSurfacePolicy.pressedSurface(t.keyBackground, t), dp(ImeGeometryTokens.KEY_RADIUS_DP), ) isClickable = true From 79c79cb25193c119357711200ec6e3e1cf78f3d1 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:31:56 +0800 Subject: [PATCH 063/193] style: reinforce selected chips without relying on color alone --- app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt b/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt index aaabe335..560d8ce2 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 @@ -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 "未选中" From 06879462a83efe5c34529fef6e060428c7f1ff4f Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:32:00 +0800 Subject: [PATCH 064/193] style: reinforce selected nine-key path without relying on color alone --- .../java/llc/slacker/openime/NineKeySymbolRailController.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt b/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt index 84e4c44b..436befe2 100644 --- a/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt +++ b/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt @@ -1,6 +1,7 @@ package llc.slacker.openime import android.content.Context +import android.graphics.Typeface import android.os.Build import android.text.Editable import android.text.TextWatcher @@ -199,6 +200,7 @@ internal class NineKeySymbolRailController( stateDescription = if (active) "已选择" else "未选择" } isSelected = active + typeface = if (active) Typeface.DEFAULT_BOLD else Typeface.DEFAULT isClickable = true isFocusable = true maxLines = 2 From 655aa7f5f6b5e1d8bb215ceb03aa3eaa87423f8a Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:33:41 +0800 Subject: [PATCH 065/193] style: tag settings text by semantic hierarchy --- .../main/java/llc/slacker/openime/SettingsPanelController.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt index 2b217932..055fd439 100644 --- a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt +++ b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt @@ -823,6 +823,7 @@ internal class SettingsPanelController( ), ) val valueView = TextView(context).apply { + tag = "setting-value" textSize = 12f gravity = Gravity.CENTER includeFontPadding = false @@ -910,6 +911,7 @@ internal class SettingsPanelController( text = value textSize = size includeFontPadding = false + tag = if (size <= 11.5f) "panel-note" else "setting-label" } private fun chipParams() = From eb742ecca0360fc9e6f2452b86491707d4478332 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:33:58 +0800 Subject: [PATCH 066/193] style: add hairline card boundaries and text hierarchy --- .../java/llc/slacker/openime/ImeThemeApplier.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt index 239b6a92..0468af41 100644 --- a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt +++ b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt @@ -155,18 +155,24 @@ 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) @@ -241,7 +247,7 @@ internal class ImeThemeApplier( ) } view.tag == "key-panel-back" -> { - view.imageTintList = ColorStateList.valueOf(t.keyText) + view.imageTintList = ColorStateList.valueOf(t.keySecondaryText) view.background = statefulRounded( t.panelHeadBackground, ImeSurfacePolicy.pressedSurface(t.panelHeadBackground, t), @@ -287,9 +293,15 @@ internal class ImeThemeApplier( 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) From 080b64c6cf6608233f59044725cef3387d3e2773 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:34:22 +0800 Subject: [PATCH 067/193] style: unify toolbar and settings icon color roles --- .../llc/slacker/openime/ImeThemeApplier.kt | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt index 0468af41..d7132e95 100644 --- a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt +++ b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt @@ -222,27 +222,9 @@ internal class ImeThemeApplier( private fun applyImageView(view: ImageView, t: ImeTheme.Tokens) { when { 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), ) } @@ -258,7 +240,7 @@ 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, From 18cdc3bd6a8fff812f31554e0408c4e24ec8f374 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:35:11 +0800 Subject: [PATCH 068/193] style: remove hardcoded switch knob color --- .../main/java/llc/slacker/openime/SettingsPanelController.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt index 055fd439..26ac60d7 100644 --- a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt +++ b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt @@ -526,10 +526,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 From 1e3b73b7af46e26a71bed43affe603c094dda949 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:35:23 +0800 Subject: [PATCH 069/193] style: theme switch knob from track contrast --- .../main/java/llc/slacker/openime/ImeThemeApplier.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt index d7132e95..784ddbf5 100644 --- a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt +++ b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt @@ -535,6 +535,18 @@ 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(ImeSurfacePolicy.divider(t)) } From 573228ebfc91eb52f6a3453617be6ae602677074 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:35:49 +0800 Subject: [PATCH 070/193] style: keep docked keyboard deck on one background plane --- app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index d17fa93b..4258a954 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -2612,7 +2612,7 @@ 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) From 78d6619a8f52ea06ef28ddc881feaf892b6fbf06 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:35:52 +0800 Subject: [PATCH 071/193] style: restore keyboard deck color when leaving floating mode --- .../main/java/llc/slacker/openime/FloatingKeyboardController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 } } From d692f226ddbe89ac85ced52e5f6730e051415070 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:37:02 +0800 Subject: [PATCH 072/193] style: derive restrained destructive color roles --- .../java/llc/slacker/openime/ImeDesignTokens.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt index bf492ccb..b1383d9d 100644 --- a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt +++ b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt @@ -219,6 +219,22 @@ internal object ImeSurfacePolicy { 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, From a4489e78b0617ae6fecfd117e65f0934882418a7 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:37:16 +0800 Subject: [PATCH 073/193] style: distinguish destructive actions with restrained semantic color --- .../java/llc/slacker/openime/ImeThemeApplier.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt index 784ddbf5..1d24bc3e 100644 --- a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt +++ b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt @@ -330,8 +330,7 @@ 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, @@ -349,13 +348,14 @@ internal class ImeThemeApplier( ) } - 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, - ImeSurfacePolicy.pressedSurface(t.destructive, t), + destructiveSurface, + ImeSurfacePolicy.pressedSurface(destructiveSurface, t), toPx(ImeGeometryTokens.CONTROL_RADIUS_DP), ) } From 872f435d6c6551aa8f47317af33e096375c8442d Mon Sep 17 00:00:00 2001 From: limuzi013 <128580527+limuzi013@users.noreply.github.com> Date: Tue, 29 Sep 2026 01:44:45 +0800 Subject: [PATCH 074/193] fix: correct IME gesture and input path regressions --- .../AuditInteractionInstrumentedTest.kt | 10 ++++++ .../openime/NineKeyChineseInstrumentedTest.kt | 31 +++++++++++++++++++ .../SetupFormActionInstrumentedTest.kt | 8 ++++- .../TextEditControlsInstrumentedTest.kt | 5 ++- .../main/java/llc/slacker/openime/ImeData.kt | 2 +- .../llc/slacker/openime/ImeKeyboardView.kt | 5 +-- .../llc/slacker/openime/ImePanelRenderer.kt | 2 +- .../llc/slacker/openime/NineKeyPresets.kt | 2 +- .../openime/QuickPhraseEditActivity.kt | 2 +- .../openime/TextEditorPanelController.kt | 2 +- .../llc/slacker/openime/VoicePanelView.kt | 2 +- 11 files changed, 61 insertions(+), 10 deletions(-) diff --git a/app/src/androidTest/java/llc/slacker/openime/AuditInteractionInstrumentedTest.kt b/app/src/androidTest/java/llc/slacker/openime/AuditInteractionInstrumentedTest.kt index b7b4c43f..0e82ef48 100644 --- a/app/src/androidTest/java/llc/slacker/openime/AuditInteractionInstrumentedTest.kt +++ b/app/src/androidTest/java/llc/slacker/openime/AuditInteractionInstrumentedTest.kt @@ -483,6 +483,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 +590,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 +631,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) 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/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/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/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index 4258a954..434c505c 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -1374,7 +1374,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 { @@ -1890,8 +1890,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 && diff --git a/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt b/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt index 560d8ce2..503ac3b9 100644 --- a/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt +++ b/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt @@ -254,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 "不可用" 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/QuickPhraseEditActivity.kt b/app/src/main/java/llc/slacker/openime/QuickPhraseEditActivity.kt index c9849716..547eb00f 100644 --- a/app/src/main/java/llc/slacker/openime/QuickPhraseEditActivity.kt +++ b/app/src/main/java/llc/slacker/openime/QuickPhraseEditActivity.kt @@ -181,7 +181,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 { diff --git a/app/src/main/java/llc/slacker/openime/TextEditorPanelController.kt b/app/src/main/java/llc/slacker/openime/TextEditorPanelController.kt index a469aae7..d656aee2 100644 --- a/app/src/main/java/llc/slacker/openime/TextEditorPanelController.kt +++ b/app/src/main/java/llc/slacker/openime/TextEditorPanelController.kt @@ -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/VoicePanelView.kt b/app/src/main/java/llc/slacker/openime/VoicePanelView.kt index 236a870a..0e515819 100644 --- a/app/src/main/java/llc/slacker/openime/VoicePanelView.kt +++ b/app/src/main/java/llc/slacker/openime/VoicePanelView.kt @@ -165,7 +165,7 @@ 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) } From 694fbba17f89e161d1acfc3ca7713c5b5814dc60 Mon Sep 17 00:00:00 2001 From: limuzi013 <128580527+limuzi013@users.noreply.github.com> Date: Tue, 29 Sep 2026 02:00:55 +0800 Subject: [PATCH 075/193] test: cover space cursor drag gesture --- .../AuditInteractionInstrumentedTest.kt | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/app/src/androidTest/java/llc/slacker/openime/AuditInteractionInstrumentedTest.kt b/app/src/androidTest/java/llc/slacker/openime/AuditInteractionInstrumentedTest.kt index 0e82ef48..91f8d545 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 @@ -1057,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 From d5491e584d245469232095ac29a5ccc280f705dc Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:40:46 +0800 Subject: [PATCH 076/193] fix: preserve voice tail after release --- .../slacker/openime/LocalAudioVoiceBackend.kt | 71 +++++++++++++++++-- 1 file changed, 66 insertions(+), 5 deletions(-) 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 + } } From 6bcfd2c2ee5824f4bf023e79cbc541f3b4b033f3 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:41:03 +0800 Subject: [PATCH 077/193] chore: log sherpa final fallback --- .../llc/slacker/openime/VoiceRecognitionBackend.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/VoiceRecognitionBackend.kt b/app/src/main/java/llc/slacker/openime/VoiceRecognitionBackend.kt index cb4a1942..d0bd386e 100644 --- a/app/src/main/java/llc/slacker/openime/VoiceRecognitionBackend.kt +++ b/app/src/main/java/llc/slacker/openime/VoiceRecognitionBackend.kt @@ -1,6 +1,7 @@ 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.OnlineRecognizer @@ -324,8 +325,16 @@ private class SherpaOnnxStreamingRuntime( val currentStream = streamLease.value 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 } From 87abd86c4589849cb200dbd6172d69f23783d890 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:41:14 +0800 Subject: [PATCH 078/193] fix: add voice cancel hysteresis --- .../llc/slacker/openime/SpaceVoiceGestureController.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/SpaceVoiceGestureController.kt b/app/src/main/java/llc/slacker/openime/SpaceVoiceGestureController.kt index b363cd35..e69b4299 100644 --- a/app/src/main/java/llc/slacker/openime/SpaceVoiceGestureController.kt +++ b/app/src/main/java/llc/slacker/openime/SpaceVoiceGestureController.kt @@ -109,7 +109,12 @@ internal class SpaceVoiceGestureController( return false } - val cancelNow = downY - rawY >= toPx(CANCEL_DISTANCE_DP) + 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) @@ -185,7 +190,8 @@ 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 } From 8e510b267e63e84318427e444ad035c6aef9218d Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:41:23 +0800 Subject: [PATCH 079/193] docs: align voice hold timing with Android --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c142a90..5e641c04 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ InputConnection - Emoji、符号、剪贴板、文本编辑、浮动键盘和设置面板;手写目前仅保留笔迹采集 UI,识别引擎尚未接入,正式入口默认隐藏。 - 根据输入法窗口实际可用宽度动态计算列宽与间距;宽屏限制内容最大宽度并居中, 系统底部区域通过 WindowInsets 处理。 -- 空格短按输入空格或提交首选,长按约 150 ms 进入唯一的语音输入流程;删除键上滑清空。 +- 空格短按输入空格或提交首选,长按达到系统长按时长(`ViewConfiguration.getLongPressTimeout()`)进入唯一的语音输入流程;删除键上滑清空。 - 选词或首选上屏后统一清除拼音、候选与 Rime composition,随后删除键只处理目标输入框。 - Android 密码编辑器的隐私边界、麦克风权限失败回退和本地模型校验。 From 8b3df93bbae304fc1308fe43315ba0d7fefcf8e0 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:44:15 +0800 Subject: [PATCH 080/193] feat: define canonical IME visual tokens --- .../llc/slacker/openime/ImeDesignTokens.kt | 93 ++++++++++++++++--- 1 file changed, 81 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt index b1383d9d..b42a2358 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 @@ -46,6 +47,17 @@ internal object ImeGeometryTokens { 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 +66,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 } /** @@ -103,7 +122,18 @@ 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 canvas: Int get() = keyboardBackground + val surface: Int get() = toolbarBackground + val outline: Int get() = border + val text: Int get() = keyText + val textSecondary: Int get() = keySecondaryText + val accent: Int get() = primary + val danger: Int get() = destructive + val accentPressed: Int get() = ImeSurfacePolicy.primaryPressed(this) + } fun tokens( appearance: ImeAppearance = ImeAppearance.DARK, @@ -125,20 +155,22 @@ enum class ImeTheme(val key: String, val label: String) { // controls sit one surface above it, and typing keys occupy // the clearest foreground plane. Tokens( - c("#0A84FF"), c("#1C1C1E"), c("#242426"), c("#262628"), c("#F2F2F7"), + 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"), ) } else { // Light mode deliberately separates the cool-gray keyboard // base, secondary controls and near-white typing surfaces. // Text remains neutral instead of using absolute black. Tokens( - c("#007AFF"), c("#D5D8DE"), c("#EEF0F3"), c("#F7F8FA"), c("#1F2023"), + 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"), ) } DARK -> Tokens( @@ -206,12 +238,49 @@ internal object ImeSurfacePolicy { ) fun primaryPressed(tokens: ImeTheme.Tokens): Int = - ImeDrawableFactory.blend( - if (isDark(tokens)) Color.WHITE else Color.BLACK, + adjustHslLightness( tokens.primary, - if (isDark(tokens)) 0.10f else 0.12f, + 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, From b2417eb50e2eed0e14d711b4b034f83f28a7d0a1 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:44:38 +0800 Subject: [PATCH 081/193] style: align light setup colors with IME tokens --- app/src/main/res/values/colors.xml | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 92572005..045b8738 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 + #6E6E73 #1D9BF0 - #1688D0 + #0E86D6 #07131D - #EEF1F6 - #E3E7EE - #9AA3B2 + #EEF0F3 + #DDE1E7 + #6E6E73 #1F8A4C - #E8F7EE + #EEF0F3 #1F8A4C - #E8EEFF - #D7DCE5 - #E8EEFF + #EEF0F3 + #B7BCC5 + #EEF0F3 + #1D9BF0 + #1D9BF0 From ec976d39a10d7662369e4768fd3369e7a94e0339 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:44:41 +0800 Subject: [PATCH 082/193] style: align dark setup colors with IME tokens --- app/src/main/res/values-night/colors.xml | 27 ++++++++++++------------ 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 0bc75d2c..e5cfbd0b 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 + #51B7F5 #07131D - #262A31 - #30353E - #8B93A3 + #242426 + #303033 + #AEAEB2 #5BD08A - #1B3A28 + #242426 #5BD08A - #243056 - #3A404A - #243056 + #242426 + #48484A + #242426 + #6EC3F7 From e6d57aeebb2a1e7edd6a8b95e85c05b93b55ad69 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:45:03 +0800 Subject: [PATCH 083/193] refactor: remove unused keyboard themes --- .../llc/slacker/openime/ImeDesignTokens.kt | 69 ++++--------------- 1 file changed, 15 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt index b42a2358..8902b6fa 100644 --- a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt +++ b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt @@ -91,11 +91,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( @@ -146,56 +142,21 @@ 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) { - // Dark mode is a separate neutral scale rather than an - // inverted light palette. The keyboard base stays darkest, - // controls sit one surface above it, and typing keys occupy - // the clearest foreground plane. - 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"), - ) - } else { - // Light mode deliberately separates the cool-gray keyboard - // base, secondary controls and near-white typing surfaces. - // Text remains neutral instead of using absolute black. - 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"), - ) - } - 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"), + 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"), ) - 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"), - ) - 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"), ) } val accent = accentOverride ?: return base From 2cddaf3bf18b09b66da87ceb16f5660fe5d7e97e Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:45:19 +0800 Subject: [PATCH 084/193] fix: fall back from legacy theme values --- .../slacker/openime/ImeSettingsRepository.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt b/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt index 440de9b1..7a66f58e 100644 --- a/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt +++ b/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt @@ -92,19 +92,19 @@ object ImeSettingsRepository { .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) + /** - * 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. + * 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 = - runCatching { - ImeTheme.valueOf( - context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) - .getString(KEY_THEME, ImeTheme.IOS.name) - ?: ImeTheme.IOS.name, - ) - }.getOrDefault(ImeTheme.IOS) + 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) From 712911e2de82474030309a379b61ac03ed865303 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:45:31 +0800 Subject: [PATCH 085/193] test: cover legacy theme fallback --- .../slacker/openime/ImeThemePersistenceTest.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 app/src/test/java/llc/slacker/openime/ImeThemePersistenceTest.kt 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")) + } +} From f8e1dac6e021b18cda7fb344b6ba54d11c130c35 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:45:43 +0800 Subject: [PATCH 086/193] test: align theme selector with IOS-only policy --- .../llc/slacker/openime/AuditInteractionInstrumentedTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/llc/slacker/openime/AuditInteractionInstrumentedTest.kt b/app/src/androidTest/java/llc/slacker/openime/AuditInteractionInstrumentedTest.kt index 91f8d545..66b1d782 100644 --- a/app/src/androidTest/java/llc/slacker/openime/AuditInteractionInstrumentedTest.kt +++ b/app/src/androidTest/java/llc/slacker/openime/AuditInteractionInstrumentedTest.kt @@ -452,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 } From 3541be84822632291ed295be0fab9622d372f9ff Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:46:47 +0800 Subject: [PATCH 087/193] style: normalize typography in ImeKeyView --- app/src/main/java/llc/slacker/openime/ImeKeyView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyView.kt index dfe18273..6bd0a744 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyView.kt @@ -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 From e509314692740c9a53beaa058594504a166c1461 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:46:50 +0800 Subject: [PATCH 088/193] style: normalize typography in SymbolManagerActivity --- .../slacker/openime/SymbolManagerActivity.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/SymbolManagerActivity.kt b/app/src/main/java/llc/slacker/openime/SymbolManagerActivity.kt index 06762f1c..4ee4221f 100644 --- a/app/src/main/java/llc/slacker/openime/SymbolManagerActivity.kt +++ b/app/src/main/java/llc/slacker/openime/SymbolManagerActivity.kt @@ -93,7 +93,7 @@ class SymbolManagerActivity : Activity() { } val title = TextView(this).apply { text = "自定义符号" - textSize = 22f + textSize = ImeTypographyTokens.TITLE_SP setTextColor(getColor(R.color.setup_title)) setTypeface(typeface, android.graphics.Typeface.BOLD) gravity = Gravity.CENTER_VERTICAL @@ -122,7 +122,7 @@ class SymbolManagerActivity : Activity() { content.addView(header, fullHeight(56).apply { bottomMargin = dp(8) }) 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 +133,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 +144,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 +208,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 +219,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 +229,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 +279,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 +288,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 +387,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 From 5cc8db55c7ecd1306714c256c8affc2a4d8ac664 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:46:53 +0800 Subject: [PATCH 089/193] style: normalize typography in ImeTopZone --- app/src/main/java/llc/slacker/openime/ImeTopZone.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeTopZone.kt b/app/src/main/java/llc/slacker/openime/ImeTopZone.kt index 3adc4f10..bfafc8ff 100644 --- a/app/src/main/java/llc/slacker/openime/ImeTopZone.kt +++ b/app/src/main/java/llc/slacker/openime/ImeTopZone.kt @@ -136,7 +136,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) @@ -197,7 +197,7 @@ internal class ImeTopZone( candidateEmojiButton.apply { tag = "candidate-emoji" text = "☺" - textSize = 17f + textSize = ImeTypographyTokens.BODY_SP gravity = Gravity.CENTER contentDescription = "表情" setPadding(toPx(7), 0, toPx(7), 0) @@ -213,7 +213,7 @@ internal class ImeTopZone( candidateExpandButton.apply { tag = "candidate-expand" text = "⌄" - textSize = 15f + textSize = ImeTypographyTokens.BODY_SP gravity = Gravity.CENTER contentDescription = "展开更多候选" setPadding(toPx(7), 0, toPx(7), 0) @@ -270,7 +270,7 @@ internal class ImeTopZone( voiceInlineStatus.apply { tag = "voice-inline-status" text = "正在聆听…" - textSize = 14f + textSize = ImeTypographyTokens.BODY_SP setTextColor(Color.WHITE) includeFontPadding = false maxLines = 1 From a647f1255b3167369e83466eb3684cfd66d75bfc Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:46:56 +0800 Subject: [PATCH 090/193] style: normalize typography in SetupUi --- app/src/main/java/llc/slacker/openime/SetupUi.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/SetupUi.kt b/app/src/main/java/llc/slacker/openime/SetupUi.kt index b5dc172c..11df1cbb 100644 --- a/app/src/main/java/llc/slacker/openime/SetupUi.kt +++ b/app/src/main/java/llc/slacker/openime/SetupUi.kt @@ -94,7 +94,7 @@ object SetupUi { 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 @@ -128,7 +128,7 @@ object SetupUi { 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( From 56b266dca28025e98155d0e8c76d827cf139df71 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:46:59 +0800 Subject: [PATCH 091/193] style: normalize typography in VoicePanelView --- app/src/main/java/llc/slacker/openime/VoicePanelView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/VoicePanelView.kt b/app/src/main/java/llc/slacker/openime/VoicePanelView.kt index 0e515819..68f54baf 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 From 4edfd0d73512a8182bad22d449f12c627fb83006 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:47:02 +0800 Subject: [PATCH 092/193] style: normalize typography in QuickPhraseEditActivity --- .../llc/slacker/openime/QuickPhraseEditActivity.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/QuickPhraseEditActivity.kt b/app/src/main/java/llc/slacker/openime/QuickPhraseEditActivity.kt index 547eb00f..cb54c068 100644 --- a/app/src/main/java/llc/slacker/openime/QuickPhraseEditActivity.kt +++ b/app/src/main/java/llc/slacker/openime/QuickPhraseEditActivity.kt @@ -105,7 +105,7 @@ class QuickPhraseEditActivity : Activity() { val accent = SetupUi.accent(this) val title = TextView(this).apply { text = if (phraseId > 0L) "编辑常用语" else "新增常用语" - textSize = 22f + textSize = ImeTypographyTokens.TITLE_SP setTextColor(getColor(R.color.setup_title)) setTypeface(typeface, android.graphics.Typeface.BOLD) gravity = Gravity.CENTER_VERTICAL @@ -138,7 +138,7 @@ 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 { @@ -149,7 +149,7 @@ class QuickPhraseEditActivity : Activity() { inputType = android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS imeOptions = EditorInfo.IME_ACTION_NEXT - textSize = 16f + textSize = ImeTypographyTokens.BODY_SP } SetupUi.styleInput(this, codeEdit) phraseEdit = EditText(this).apply { @@ -160,7 +160,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, "保存") { @@ -256,7 +256,7 @@ class QuickPhraseEditActivity : Activity() { ).apply { bottomMargin = dp(14) }) addView(TextView(this@QuickPhraseEditActivity).apply { text = "设置后,输入至少 2 个字符的短码即可在候选栏召回这条常用语。" - textSize = 12f + textSize = ImeTypographyTokens.BODY_SP setTextColor(getColor(R.color.setup_body)) setPadding(dp(4), 0, dp(4), dp(12)) }, LinearLayout.LayoutParams( @@ -273,7 +273,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( @@ -324,7 +324,7 @@ 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 From b6af835aa9cf54470a57146d2fb9d5d3a168851e Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:47:05 +0800 Subject: [PATCH 093/193] style: normalize typography in KeyPopupController --- app/src/main/java/llc/slacker/openime/KeyPopupController.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/KeyPopupController.kt b/app/src/main/java/llc/slacker/openime/KeyPopupController.kt index a0b77832..51516df4 100644 --- a/app/src/main/java/llc/slacker/openime/KeyPopupController.kt +++ b/app/src/main/java/llc/slacker/openime/KeyPopupController.kt @@ -47,7 +47,7 @@ internal class KeyPopupController( ) val popup = TextView(host.context).apply { this.text = text - textSize = if (text.length > 1) 13f else 16f + textSize = if (text.length > 1) ImeTypographyTokens.BODY_SP else ImeTypographyTokens.DISPLAY_SP includeFontPadding = false maxLines = 1 ellipsize = TextUtils.TruncateAt.END @@ -97,7 +97,7 @@ 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) From 90073bcbc58db7b8ea814acd18259d826bffa0fa Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:47:09 +0800 Subject: [PATCH 094/193] style: normalize typography in SettingsPanelController --- .../main/java/llc/slacker/openime/SettingsPanelController.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt index 26ac60d7..62e54931 100644 --- a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt +++ b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt @@ -505,7 +505,7 @@ internal class SettingsPanelController( addView( TextView(context).apply { text = "›" - textSize = 18f + textSize = ImeTypographyTokens.BODY_SP gravity = Gravity.CENTER importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO tag = "setting-chevron" @@ -821,7 +821,7 @@ internal class SettingsPanelController( ) val valueView = TextView(context).apply { tag = "setting-value" - textSize = 12f + textSize = ImeTypographyTokens.BODY_SP gravity = Gravity.CENTER includeFontPadding = false minWidth = toPx(ImeGeometryTokens.TOUCH_TARGET_DP) From 9b860535ff9cdf4bab1d17681f28acf633f60bb8 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:47:12 +0800 Subject: [PATCH 095/193] style: normalize typography in SymbolRailRenderer --- app/src/main/java/llc/slacker/openime/SymbolRailRenderer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From a3f15c0703b2b1214089ca933931566648827149 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:47:15 +0800 Subject: [PATCH 096/193] style: normalize typography in EmojiCellFactory --- app/src/main/java/llc/slacker/openime/EmojiCellFactory.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 4860a1f55ddff39c51c380b20a0dbc7067a0ae7d Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:47:58 +0800 Subject: [PATCH 097/193] style: normalize typography in BackspaceKeyFactory --- app/src/main/java/llc/slacker/openime/BackspaceKeyFactory.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/BackspaceKeyFactory.kt b/app/src/main/java/llc/slacker/openime/BackspaceKeyFactory.kt index 6cdcb5a3..10225df8 100644 --- a/app/src/main/java/llc/slacker/openime/BackspaceKeyFactory.kt +++ b/app/src/main/java/llc/slacker/openime/BackspaceKeyFactory.kt @@ -60,7 +60,7 @@ internal class BackspaceKeyFactory( val clearHint = TextView(context).apply { text = "↑ 清空" - textSize = 7.5f + textSize = ImeTypographyTokens.CAPTION_SP gravity = Gravity.CENTER includeFontPadding = false alpha = 0.72f From 3dc82287cc1883513614a08551883e87ab6af53f Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:48:01 +0800 Subject: [PATCH 098/193] style: normalize typography in CandidateBarController --- app/src/main/java/llc/slacker/openime/CandidateBarController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/CandidateBarController.kt b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt index 887e6eba..32c45ace 100644 --- a/app/src/main/java/llc/slacker/openime/CandidateBarController.kt +++ b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt @@ -319,7 +319,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) From fe37165bf0fd895473c4e751d28af7d00e609868 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:48:06 +0800 Subject: [PATCH 099/193] style: normalize typography in ImeKeyboardView --- app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index 434c505c..adadd566 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -2049,7 +2049,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" @@ -2586,7 +2586,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)) } From 8293687a8755d36bcd53e6e86de6d5599357ff5f Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:48:09 +0800 Subject: [PATCH 100/193] style: normalize typography in NineKeySymbolRailController --- .../java/llc/slacker/openime/NineKeySymbolRailController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt b/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt index 436befe2..afdf59d7 100644 --- a/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt +++ b/app/src/main/java/llc/slacker/openime/NineKeySymbolRailController.kt @@ -187,7 +187,7 @@ internal class NineKeySymbolRailController( content.addView( TextView(content.context).apply { text = displayPath(choice) - textSize = 12f + textSize = ImeTypographyTokens.BODY_SP gravity = Gravity.CENTER tag = if (active) SELECTED_FILTER_TAG else FILTER_TAG contentDescription = From 80d3e3113667f88aad9caa349fc36da4c0458804 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:48:39 +0800 Subject: [PATCH 101/193] refactor: move accent palette into design tokens --- .../slacker/openime/ImeSettingsRepository.kt | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt b/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt index 7a66f58e..d407ca7f 100644 --- a/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt +++ b/app/src/main/java/llc/slacker/openime/ImeSettingsRepository.kt @@ -187,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 - } -} From aaad8c06168e6a34555e704a8e95da901c1e9c7b Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:48:42 +0800 Subject: [PATCH 102/193] feat: centralize accent palette colors --- .../llc/slacker/openime/ImeDesignTokens.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt index 8902b6fa..a3330cc0 100644 --- a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt +++ b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt @@ -312,3 +312,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 + } +} From 0e44918b7c589eb30e21682af2f3b467f19e9aa4 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:48:58 +0800 Subject: [PATCH 103/193] refactor: make setup colors token-driven --- .../main/java/llc/slacker/openime/SetupUi.kt | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/SetupUi.kt b/app/src/main/java/llc/slacker/openime/SetupUi.kt index 11df1cbb..394ebd1b 100644 --- a/app/src/main/java/llc/slacker/openime/SetupUi.kt +++ b/app/src/main/java/llc/slacker/openime/SetupUi.kt @@ -14,7 +14,6 @@ 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,6 +21,17 @@ 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 dim(color: Int, factor: Float): Int = @@ -40,7 +50,8 @@ object SetupUi { 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), @@ -216,7 +227,7 @@ 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) From e0543639cdd6d76861bfda00d0e66bc5ac406626 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:50:04 +0800 Subject: [PATCH 104/193] style: normalize ic_mic.xml vector size --- app/src/main/res/drawable/ic_mic.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From b5505ba299cfde4ad166c19dbea1cc854e187a4c Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:50:08 +0800 Subject: [PATCH 105/193] style: normalize ic_edit.xml vector size --- app/src/main/res/drawable/ic_edit.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 7a7b958d9aa14083116cd584a8c08115b41a2a24 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:50:11 +0800 Subject: [PATCH 106/193] style: normalize ic_game.xml vector size --- app/src/main/res/drawable/ic_game.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From d4ffcfc98add25e3bdd785504ce840e13c96f84c Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:50:15 +0800 Subject: [PATCH 107/193] style: normalize ic_shift.xml vector size --- app/src/main/res/drawable/ic_shift.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 @@ Date: Tue, 29 Sep 2026 08:50:23 +0800 Subject: [PATCH 108/193] style: normalize ic_emoji.xml vector size --- app/src/main/res/drawable/ic_emoji.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 4c5f4028b4dc0dc9115bd600709447d4803b6a48 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:50:26 +0800 Subject: [PATCH 109/193] style: normalize ic_symbols.xml vector size --- app/src/main/res/drawable/ic_symbols.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 77e7279b436e469f4b562ec8c250a6cba0dd18a4 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:50:29 +0800 Subject: [PATCH 110/193] style: normalize ic_settings.xml vector size --- app/src/main/res/drawable/ic_settings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From d56274ca38eee63ade9640694b6fe2860957882c Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:50:32 +0800 Subject: [PATCH 111/193] style: normalize ic_keyboard.xml vector size --- app/src/main/res/drawable/ic_keyboard.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 3fa2aee2e3640ef779b5442800d585c39ae6beb4 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:50:40 +0800 Subject: [PATCH 112/193] style: normalize ic_caps_lock.xml vector size --- app/src/main/res/drawable/ic_caps_lock.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 @@ Date: Tue, 29 Sep 2026 08:50:43 +0800 Subject: [PATCH 113/193] style: normalize ic_backspace.xml vector size --- app/src/main/res/drawable/ic_backspace.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 @@ Date: Tue, 29 Sep 2026 08:50:46 +0800 Subject: [PATCH 114/193] style: normalize ic_clipboard.xml vector size --- app/src/main/res/drawable/ic_clipboard.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From eed916dc9d2bccbd430079efed332c34647430c2 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:50:48 +0800 Subject: [PATCH 115/193] style: normalize ic_handwriting.xml vector size --- app/src/main/res/drawable/ic_handwriting.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 3c7b4aea461e5e0eda21b9d532412e64d5ecb437 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:51:03 +0800 Subject: [PATCH 116/193] style: standardize ic_more.xml icon --- app/src/main/res/drawable/ic_more.xml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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"> + From 188de319d8940af0ad8d7304237af6d11b488707 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:51:06 +0800 Subject: [PATCH 117/193] style: standardize ic_grid.xml icon --- app/src/main/res/drawable/ic_grid.xml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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"> + From 43e5f0986d2eb1733f9d5363abaf238e14ea2804 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:51:09 +0800 Subject: [PATCH 118/193] style: standardize ic_keyboard_hide.xml icon --- app/src/main/res/drawable/ic_keyboard_hide.xml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) 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" /> From 21663bdafcb6c28e27728a76f4411d6f987dc6bd Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:51:20 +0800 Subject: [PATCH 119/193] style: add ic_chevron_down.xml vector icon --- app/src/main/res/drawable/ic_chevron_down.xml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 app/src/main/res/drawable/ic_chevron_down.xml 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 @@ + + + From d62dc2a0edca5f094c47b6e63c4778cccfae79dc Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:51:23 +0800 Subject: [PATCH 120/193] style: add ic_stop.xml vector icon --- app/src/main/res/drawable/ic_stop.xml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 app/src/main/res/drawable/ic_stop.xml diff --git a/app/src/main/res/drawable/ic_stop.xml b/app/src/main/res/drawable/ic_stop.xml new file mode 100644 index 00000000..9a4b6f22 --- /dev/null +++ b/app/src/main/res/drawable/ic_stop.xml @@ -0,0 +1,7 @@ + + + From 295ac496e9f9605e735430c9ee52df63e4227a6e Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:51:25 +0800 Subject: [PATCH 121/193] style: add ic_check.xml vector icon --- app/src/main/res/drawable/ic_check.xml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 app/src/main/res/drawable/ic_check.xml diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 00000000..33b44b49 --- /dev/null +++ b/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,8 @@ + + + From 6af6c90a1fa6b71cd41c27b41e46e95affef8b91 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:51:28 +0800 Subject: [PATCH 122/193] style: add ic_close.xml vector icon --- app/src/main/res/drawable/ic_close.xml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 app/src/main/res/drawable/ic_close.xml 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 @@ + + + From 04a04d4e28537d937c068653ec213f6e285e7d47 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:54:13 +0800 Subject: [PATCH 123/193] style: replace candidate glyphs with vector icons --- .../main/java/llc/slacker/openime/ImeTopZone.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeTopZone.kt b/app/src/main/java/llc/slacker/openime/ImeTopZone.kt index bfafc8ff..f1afc438 100644 --- a/app/src/main/java/llc/slacker/openime/ImeTopZone.kt +++ b/app/src/main/java/llc/slacker/openime/ImeTopZone.kt @@ -50,8 +50,8 @@ 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) @@ -196,9 +196,8 @@ internal class ImeTopZone( candidateEmojiButton.apply { tag = "candidate-emoji" - text = "☺" - textSize = ImeTypographyTokens.BODY_SP - 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 +211,8 @@ internal class ImeTopZone( candidateExpandButton.apply { tag = "candidate-expand" - text = "⌄" - textSize = ImeTypographyTokens.BODY_SP - gravity = Gravity.CENTER + setImageResource(R.drawable.ic_chevron_down) + scaleType = ImageView.ScaleType.CENTER_INSIDE contentDescription = "展开更多候选" setPadding(toPx(7), 0, toPx(7), 0) isClickable = true From f4720188a010d2d539a438a92ae99e698c9db27e Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:54:16 +0800 Subject: [PATCH 124/193] refactor: use vector expand control --- .../main/java/llc/slacker/openime/CandidateBarController.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/CandidateBarController.kt b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt index 32c45ace..0433bc21 100644 --- a/app/src/main/java/llc/slacker/openime/CandidateBarController.kt +++ b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt @@ -11,6 +11,7 @@ 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 @@ -27,7 +28,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, @@ -171,7 +172,6 @@ internal class CandidateBarController( compositionPreview: String, ) { if (!open) { - expandButton.text = "⌄" expandButton.contentDescription = "展开更多候选" overlay.animate().cancel() keyboardBody.animate().cancel() @@ -305,7 +305,6 @@ internal class CandidateBarController( expandedOpen = false renderedExpandedCandidates = null renderedExpandedComposition = null - expandButton.text = "⌄" expandButton.contentDescription = "展开更多候选" } From 7bf67507aa483b0002ec4280d62a6b33ae42ac55 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:54:34 +0800 Subject: [PATCH 125/193] fix: tint candidate vector controls --- app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index adadd566..db7d2aaf 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -2620,8 +2620,8 @@ open class ImeKeyboardView( 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() } From 8cb09e032395e7c63e9be2cb070e724c56f1df5c Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:54:37 +0800 Subject: [PATCH 126/193] style: theme candidate vector controls --- app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt index 1d24bc3e..b4af47c2 100644 --- a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt +++ b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt @@ -221,6 +221,14 @@ 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" -> { view.imageTintList = ColorStateList.valueOf(t.primary) view.background = ImeDrawableFactory.rounded( From 5ab8d386ae4f6482f56f933eec1ab6cf04d875cb Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:54:51 +0800 Subject: [PATCH 127/193] style: replace voice glyph with vector icon --- app/src/main/java/llc/slacker/openime/VoicePanelView.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/VoicePanelView.kt b/app/src/main/java/llc/slacker/openime/VoicePanelView.kt index 68f54baf..1c869284 100644 --- a/app/src/main/java/llc/slacker/openime/VoicePanelView.kt +++ b/app/src/main/java/llc/slacker/openime/VoicePanelView.kt @@ -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 = "语音状态,当前未开始,仅支持长按空格启动" } @@ -169,8 +171,8 @@ internal class VoicePanelView( 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 } From b6cf27357769464ad488d459c3b9e0a4962f54d7 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:54:55 +0800 Subject: [PATCH 128/193] refactor: drive voice state with vector resources --- .../slacker/openime/VoicePanelController.kt | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) 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) { From e5bf032a8f9115e07ca00174b135a2c073e7e98a Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:55:03 +0800 Subject: [PATCH 129/193] style: tint voice vector icon with accent contrast --- app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt index b4af47c2..f196b5bc 100644 --- a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt +++ b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt @@ -478,9 +478,9 @@ 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, ImeSurfacePolicy.primaryPressed(t), From 6d4c916845e9948519a8af3ebfd634719e935979 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:55:14 +0800 Subject: [PATCH 130/193] style: replace text-editor glyph arrows with labels --- .../java/llc/slacker/openime/TextEditorPanelController.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/TextEditorPanelController.kt b/app/src/main/java/llc/slacker/openime/TextEditorPanelController.kt index d656aee2..f3212922 100644 --- a/app/src/main/java/llc/slacker/openime/TextEditorPanelController.kt +++ b/app/src/main/java/llc/slacker/openime/TextEditorPanelController.kt @@ -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 From 77e7414719d682a55ce426a884d75466ef9bd639 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:55:17 +0800 Subject: [PATCH 131/193] style: remove decorative backspace glyph --- app/src/main/java/llc/slacker/openime/BackspaceKeyFactory.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/BackspaceKeyFactory.kt b/app/src/main/java/llc/slacker/openime/BackspaceKeyFactory.kt index 10225df8..f0b9210b 100644 --- a/app/src/main/java/llc/slacker/openime/BackspaceKeyFactory.kt +++ b/app/src/main/java/llc/slacker/openime/BackspaceKeyFactory.kt @@ -59,7 +59,7 @@ internal class BackspaceKeyFactory( } val clearHint = TextView(context).apply { - text = "↑ 清空" + text = "清空" textSize = ImeTypographyTokens.CAPTION_SP gravity = Gravity.CENTER includeFontPadding = false From d56640396d9a6eb8b37b04c9e4854332dfcd9859 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:55:52 +0800 Subject: [PATCH 132/193] style: replace settings glyphs with vector icons --- .../openime/SettingsPanelController.kt | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt index 62e54931..8944b1d9 100644 --- a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt +++ b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt @@ -503,10 +503,16 @@ internal class SettingsPanelController( ), ) addView( - TextView(context).apply { - text = "›" - textSize = ImeTypographyTokens.BODY_SP - 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" }, @@ -645,11 +651,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 From 76239d17bbf9e9a3d80be827ce9f55757176a349 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:55:55 +0800 Subject: [PATCH 133/193] style: use vector check for completed setup step --- .../main/java/llc/slacker/openime/MainActivity.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/MainActivity.kt b/app/src/main/java/llc/slacker/openime/MainActivity.kt index a3461d09..ecd13c88 100644 --- a/app/src/main/java/llc/slacker/openime/MainActivity.kt +++ b/app/src/main/java/llc/slacker/openime/MainActivity.kt @@ -3,6 +3,7 @@ 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 @@ -211,7 +212,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 (active) accent else if (done) getColor(R.color.setup_ready) else accent, + ) mark.setBackgroundResource( when { active -> R.drawable.bg_setup_mark_active From 17319b9a124e21b06d15277b6b16bed2e672bd60 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:56:37 +0800 Subject: [PATCH 134/193] feat: add XML typography tokens --- app/src/main/res/values/dimens.xml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 app/src/main/res/values/dimens.xml diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..008a3cbf --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,9 @@ + + + 11sp + 14sp + 16sp + 18sp + 21sp + 28sp + From 300bc679bbd7faded54edd057b4635296413882f Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:56:51 +0800 Subject: [PATCH 135/193] style: tokenize setup typography and chevrons --- app/src/main/res/layout/activity_main.xml | 72 +++++++++++------------ 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 97df6522..336a2855 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -37,7 +37,7 @@ android:gravity="center" android:text="@string/app_name" android:textColor="@color/setup_title" - android:textSize="28sp" + android:textSize="@dimen/ime_text_display" android:textStyle="bold" android:letterSpacing="-0.02" android:accessibilityHeading="true" /> @@ -49,7 +49,7 @@ android:gravity="center" android:text="@string/main_description" android:textColor="@color/setup_body" - android:textSize="15sp" + android:textSize="@dimen/ime_text_body" android:lineSpacingExtra="3dp" /> - @@ -129,7 +129,7 @@ android:gravity="center" android:text="2" android:textColor="@color/setup_primary" - android:textSize="14sp" + android:textSize="@dimen/ime_text_body" android:textStyle="bold" android:importantForAccessibility="no" /> - @@ -174,14 +174,14 @@ android:maxLines="6" android:importantForAutofill="no" android:minHeight="72dp" - android:textSize="17sp" /> + android:textSize="@dimen/ime_text_title" /> @@ -204,15 +204,15 @@ android:layout_weight="1" android:text="@string/open_app_settings" android:textColor="@color/setup_title" - android:textSize="16sp" + android:textSize="@dimen/ime_text_title" android:textStyle="bold" android:importantForAccessibility="no" /> - @@ -236,16 +236,16 @@ android:layout_weight="1" android:text="@string/voice_permission_enable" android:textColor="@color/setup_title" - android:textSize="16sp" + android:textSize="@dimen/ime_text_title" android:textStyle="bold" android:importantForAccessibility="no" /> - @@ -256,7 +256,7 @@ android:gravity="center" android:text="@string/voice_permission_description" android:textColor="@color/setup_body" - android:textSize="13sp" + android:textSize="@dimen/ime_text_body" android:lineSpacingExtra="2dp" /> From a168d5285b0a9cbc6f6d4d27791d8ed222242579 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:57:02 +0800 Subject: [PATCH 136/193] fix: update setup chevrons to ImageView --- app/src/main/java/llc/slacker/openime/MainActivity.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/MainActivity.kt b/app/src/main/java/llc/slacker/openime/MainActivity.kt index ecd13c88..23eb500c 100644 --- a/app/src/main/java/llc/slacker/openime/MainActivity.kt +++ b/app/src/main/java/llc/slacker/openime/MainActivity.kt @@ -14,6 +14,7 @@ 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 @@ -136,7 +137,7 @@ class MainActivity : Activity() { 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_chevron).visibility = + 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, @@ -187,7 +188,7 @@ class MainActivity : Activity() { row: View, mark: TextView, label: TextView, - chevron: TextView?, + chevron: ImageView?, done: Boolean, active: Boolean, doneText: String, @@ -242,7 +243,7 @@ class MainActivity : Activity() { } 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 From 24d2aad8a5ee09d06e6cdb6a62c746e1424f0c12 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:58:02 +0800 Subject: [PATCH 137/193] style: tokenize voice control typography --- app/src/main/java/llc/slacker/openime/VoicePanelView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/VoicePanelView.kt b/app/src/main/java/llc/slacker/openime/VoicePanelView.kt index 1c869284..b512d329 100644 --- a/app/src/main/java/llc/slacker/openime/VoicePanelView.kt +++ b/app/src/main/java/llc/slacker/openime/VoicePanelView.kt @@ -139,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 = "长按空格开始语音,松开自动上屏,上滑取消" From 1debd56fbf9f8700df1e33477a81fd392f45ae09 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:58:06 +0800 Subject: [PATCH 138/193] style: tokenize settings row typography --- .../java/llc/slacker/openime/SettingsPanelController.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt index 8944b1d9..20c4354c 100644 --- a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt +++ b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt @@ -438,9 +438,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(), @@ -487,9 +487,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(), From ecc44d01c4eed24cd0f34c2f828d69386d6c96da Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:58:10 +0800 Subject: [PATCH 139/193] style: tokenize panel action typography --- app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt b/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt index 503ac3b9..74527f1d 100644 --- a/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt +++ b/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt @@ -443,7 +443,7 @@ internal class ImePanelRenderer( if (symbolCategory == "自定义") { body.addView( - createPanelButton("管理自定义符号", 12f, true).apply { + createPanelButton("管理自定义符号", ImeTypographyTokens.BODY_SP, true).apply { contentDescription = "管理自定义符号" setOnClickListener { onFeedback() From 25564338aaf3ae8ae05f0155244ffa6f10db935e Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:58:45 +0800 Subject: [PATCH 140/193] fix: rotate candidate chevron instead of swapping glyphs --- .../main/java/llc/slacker/openime/CandidateBarController.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/CandidateBarController.kt b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt index 0433bc21..bca90122 100644 --- a/app/src/main/java/llc/slacker/openime/CandidateBarController.kt +++ b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt @@ -172,6 +172,7 @@ internal class CandidateBarController( compositionPreview: String, ) { if (!open) { + expandButton.rotation = 0f expandButton.contentDescription = "展开更多候选" overlay.animate().cancel() keyboardBody.animate().cancel() @@ -210,7 +211,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 = "已展开" @@ -305,6 +306,7 @@ internal class CandidateBarController( expandedOpen = false renderedExpandedCandidates = null renderedExpandedComposition = null + expandButton.rotation = 0f expandButton.contentDescription = "展开更多候选" } From f4169d7f3ae6654421a8f7f833217d24eb455155 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:58:47 +0800 Subject: [PATCH 141/193] style: tokenize voice language typography --- app/src/main/java/llc/slacker/openime/VoicePanelView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/VoicePanelView.kt b/app/src/main/java/llc/slacker/openime/VoicePanelView.kt index b512d329..7563b3b9 100644 --- a/app/src/main/java/llc/slacker/openime/VoicePanelView.kt +++ b/app/src/main/java/llc/slacker/openime/VoicePanelView.kt @@ -103,7 +103,7 @@ internal class VoicePanelView( } languageButton = createButton( LANGUAGES[languageIndex].first, - 13f, + ImeTypographyTokens.BODY_SP, true, ).apply { tag = "voice-language" From c54c002835b02c6da86189151aa44b7b0ba94511 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:58:50 +0800 Subject: [PATCH 142/193] style: tokenize text-editor typography --- .../java/llc/slacker/openime/TextEditorPanelController.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/TextEditorPanelController.kt b/app/src/main/java/llc/slacker/openime/TextEditorPanelController.kt index f3212922..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) { From 073ca9cb9c5ed66e6deef0bfd726852f8f7d86da Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:58:54 +0800 Subject: [PATCH 143/193] style: tokenize panel key typography --- .../java/llc/slacker/openime/ImePanelRenderer.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt b/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt index 74527f1d..221efa85 100644 --- a/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt +++ b/app/src/main/java/llc/slacker/openime/ImePanelRenderer.kt @@ -92,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 "未选中"}" @@ -275,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(), ) } @@ -296,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) }, @@ -307,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) @@ -483,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), ) From 2f61db7872868aa4da5f24542097a7509b5b49f1 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:59:46 +0800 Subject: [PATCH 144/193] fix: raise light secondary text contrast --- app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt index a3330cc0..94100ed7 100644 --- a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt +++ b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt @@ -153,7 +153,7 @@ enum class ImeTheme(val key: String, val label: String) { } 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("#FFFFFF"), c("#1C1C1E"), c("#6D6D72"), 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"), From bf44b1da3217a9377af2e6ed6e910d3b144bfccf Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:59:49 +0800 Subject: [PATCH 145/193] style: sync light secondary text color --- app/src/main/res/values/colors.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 045b8738..78d641ea 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,13 +3,13 @@ #D5D8DE #EEF0F3 #1C1C1E - #6E6E73 + #6D6D72 #1D9BF0 #0E86D6 #07131D #EEF0F3 #DDE1E7 - #6E6E73 + #6D6D72 #1F8A4C #EEF0F3 #1F8A4C From 2d2235f7fb315dadda6230e20011d17b0d72f411 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:00:31 +0800 Subject: [PATCH 146/193] test: lock canonical visual tokens --- .../llc/slacker/openime/TokenDriftTest.kt | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 app/src/test/java/llc/slacker/openime/TokenDriftTest.kt 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..493a0409 --- /dev/null +++ b/app/src/test/java/llc/slacker/openime/TokenDriftTest.kt @@ -0,0 +1,91 @@ +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 productVectorIconsUseOneCanvas() { + val drawableDir = file("app/src/main/res/drawable") + drawableDir.listFiles() + .orEmpty() + .filter { it.name.startsWith("ic_") && it.extension == "xml" && it.name != "ic_launcher.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")}") + } +} From 306cdc3950524af5151df580d064141347159127 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:00:49 +0800 Subject: [PATCH 147/193] test: forbid raw production colors --- .../llc/slacker/openime/NoRawColorTest.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 app/src/test/java/llc/slacker/openime/NoRawColorTest.kt 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") + } +} From 142dca7e719600e78f6158b0683ede85d3534f11 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:00:51 +0800 Subject: [PATCH 148/193] test: forbid raw production text sizes --- .../llc/slacker/openime/NoRawTextSizeTest.kt | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 app/src/test/java/llc/slacker/openime/NoRawTextSizeTest.kt 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..5873e155 --- /dev/null +++ b/app/src/test/java/llc/slacker/openime/NoRawTextSizeTest.kt @@ -0,0 +1,71 @@ +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(" ") + if (Regex(""",\s*\d+(?:\.\d+)?f\b""").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") + } +} From b9fa20181a5d68eb7e34a82586c88e3bd37a3024 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:01:13 +0800 Subject: [PATCH 149/193] test: enforce visual contrast budget --- .../java/llc/slacker/openime/ContrastTest.kt | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 app/src/test/java/llc/slacker/openime/ContrastTest.kt 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) + } +} From 51b58ddf13282a82e79dcfa9e0f9b25c1768889a Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:01:16 +0800 Subject: [PATCH 150/193] test: forbid decorative unicode icons --- .../slacker/openime/NoDecorativeGlyphTest.kt | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 app/src/test/java/llc/slacker/openime/NoDecorativeGlyphTest.kt 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") + } +} From 85ba4acc8aa32968f0da5ae4f00ef64e652a0368 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:04:00 +0800 Subject: [PATCH 151/193] test: enforce vector icon contract --- .../java/llc/slacker/openime/IconLintTest.kt | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 app/src/test/java/llc/slacker/openime/IconLintTest.kt 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") + } +} From f32fd677e3d2f74ea16fd35bcca9d9924af52d93 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:04:32 +0800 Subject: [PATCH 152/193] style: rename primary setup background --- .../main/res/drawable/bg_setup_primary.xml | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/src/main/res/drawable/bg_setup_primary.xml diff --git a/app/src/main/res/drawable/bg_setup_primary.xml b/app/src/main/res/drawable/bg_setup_primary.xml new file mode 100644 index 00000000..d9d18036 --- /dev/null +++ b/app/src/main/res/drawable/bg_setup_primary.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + From ab633715cf1d722cf29ff86086e5b6e52f2a1387 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:04:35 +0800 Subject: [PATCH 153/193] style: rename muted setup background --- app/src/main/res/drawable/bg_setup_muted.xml | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/src/main/res/drawable/bg_setup_muted.xml diff --git a/app/src/main/res/drawable/bg_setup_muted.xml b/app/src/main/res/drawable/bg_setup_muted.xml new file mode 100644 index 00000000..bc86c2f2 --- /dev/null +++ b/app/src/main/res/drawable/bg_setup_muted.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + From 08e22e49cec1a8d6d69b3f983e610fae52233e6b Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:04:43 +0800 Subject: [PATCH 154/193] style: use renamed setup backgrounds --- app/src/main/res/layout/activity_main.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 336a2855..7e718d43 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -68,7 +68,7 @@ android:layout_width="match_parent" android:layout_height="56dp" android:layout_marginTop="18dp" - android:background="@drawable/bg_setup_primary_pill" + android:background="@drawable/bg_setup_primary" android:clickable="true" android:contentDescription="@string/open_ime_settings" android:focusable="true" @@ -113,7 +113,7 @@ android:layout_width="match_parent" android:layout_height="56dp" android:layout_marginTop="12dp" - android:background="@drawable/bg_setup_muted_pill" + android:background="@drawable/bg_setup_muted" android:clickable="true" android:contentDescription="@string/choose_ime" android:focusable="true" @@ -190,7 +190,7 @@ android:layout_width="match_parent" android:layout_height="56dp" android:layout_marginTop="16dp" - android:background="@drawable/bg_setup_muted_pill" + android:background="@drawable/bg_setup_muted" android:clickable="true" android:contentDescription="@string/open_app_settings" android:focusable="true" @@ -221,7 +221,7 @@ android:layout_width="match_parent" android:layout_height="56dp" android:layout_marginTop="16dp" - android:background="@drawable/bg_setup_muted_pill" + android:background="@drawable/bg_setup_muted" android:clickable="true" android:contentDescription="@string/voice_permission_enable" android:focusable="true" From 9b559ae56efb9e9e3fea2671b3e3b3cf67c10676 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:04:54 +0800 Subject: [PATCH 155/193] chore: remove legacy bg_setup_primary_pill.xml name --- .../res/drawable/bg_setup_primary_pill.xml | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 app/src/main/res/drawable/bg_setup_primary_pill.xml diff --git a/app/src/main/res/drawable/bg_setup_primary_pill.xml b/app/src/main/res/drawable/bg_setup_primary_pill.xml deleted file mode 100644 index d9d18036..00000000 --- a/app/src/main/res/drawable/bg_setup_primary_pill.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - From 7edc4d0d033b3631df37b5c3b21d7aeeac8442ae Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:04:58 +0800 Subject: [PATCH 156/193] chore: remove legacy bg_setup_muted_pill.xml name --- .../main/res/drawable/bg_setup_muted_pill.xml | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 app/src/main/res/drawable/bg_setup_muted_pill.xml diff --git a/app/src/main/res/drawable/bg_setup_muted_pill.xml b/app/src/main/res/drawable/bg_setup_muted_pill.xml deleted file mode 100644 index bc86c2f2..00000000 --- a/app/src/main/res/drawable/bg_setup_muted_pill.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - From a1c065594682e945b263f495d1548a1c2083b89d Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:05:45 +0800 Subject: [PATCH 157/193] refactor: remove duplicate setup shape API --- .../main/java/llc/slacker/openime/SetupUi.kt | 82 ++++++++++++------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/SetupUi.kt b/app/src/main/java/llc/slacker/openime/SetupUi.kt index 394ebd1b..bd846834 100644 --- a/app/src/main/java/llc/slacker/openime/SetupUi.kt +++ b/app/src/main/java/llc/slacker/openime/SetupUi.kt @@ -5,7 +5,6 @@ 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.widget.Button @@ -37,14 +36,6 @@ object SetupUi { 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, @@ -55,50 +46,62 @@ object SetupUi { 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)) } } @@ -126,13 +129,31 @@ 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)) } } @@ -189,13 +210,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) @@ -232,7 +258,7 @@ object SetupUi { 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(), ), From 6b42db42fc3f01c6f4e7a7fe21385b334b8bdf68 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:06:14 +0800 Subject: [PATCH 158/193] refactor: remove legacy setup pill helpers --- .../java/llc/slacker/openime/MainActivity.kt | 72 ++++++------------- 1 file changed, 21 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/MainActivity.kt b/app/src/main/java/llc/slacker/openime/MainActivity.kt index 23eb500c..82267a69 100644 --- a/app/src/main/java/llc/slacker/openime/MainActivity.kt +++ b/app/src/main/java/llc/slacker/openime/MainActivity.kt @@ -17,8 +17,6 @@ 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. */ @@ -131,7 +129,7 @@ class MainActivity : Activity() { findViewById(R.id.voice_permission).apply { isEnabled = !microphoneGranted alpha = if (microphoneGranted) 0.72f else 1f - background = SetupUi.mutedPillBackground(this@MainActivity) + background = SetupUi.secondaryBackground(this@MainActivity) } findViewById(R.id.voice_permission_label).apply { setText(if (microphoneGranted) R.string.voice_permission_ready else R.string.voice_permission_enable) @@ -150,7 +148,7 @@ class MainActivity : Activity() { findViewById(R.id.open_app_settings).apply { isEnabled = ready alpha = if (ready) 1f else 0.45f - background = SetupUi.mutedPillBackground(this@MainActivity) + background = SetupUi.secondaryBackground(this@MainActivity) contentDescription = getString( if (ready) R.string.open_app_settings else R.string.setup_need_switch, ) @@ -200,9 +198,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( @@ -234,9 +232,9 @@ class MainActivity : Activity() { if (active) accent else if (done) getColor(R.color.setup_ready) else accent, ) if (active) { - mark.background = rounded( + mark.background = ImeDrawableFactory.rounded( contrastText(accent), - ImeGeometryTokens.BADGE_RADIUS_DP.toFloat(), + SetupUi.dp(this, ImeGeometryTokens.BADGE_RADIUS_DP).toFloat(), ) } else if (done) { mark.setBackgroundResource(R.drawable.bg_setup_mark_done) @@ -262,60 +260,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) } From e376b9ec869365af3f8dc139e72850bcda11e61a Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:07:49 +0800 Subject: [PATCH 159/193] fix: preserve keyboard secondary color while meeting page contrast --- app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt index 94100ed7..3119a17a 100644 --- a/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt +++ b/app/src/main/java/llc/slacker/openime/ImeDesignTokens.kt @@ -120,12 +120,13 @@ enum class ImeTheme(val key: String, val label: String) { 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() = keySecondaryText + val textSecondary: Int get() = textSecondaryRole ?: keySecondaryText val accent: Int get() = primary val danger: Int get() = destructive val accentPressed: Int get() = ImeSurfacePolicy.primaryPressed(this) @@ -153,10 +154,11 @@ enum class ImeTheme(val key: String, val label: String) { } else { Tokens( c("#1D9BF0"), c("#D5D8DE"), c("#EEF0F3"), c("#F7F8FA"), c("#1F2023"), - c("#FFFFFF"), c("#1C1C1E"), c("#6D6D72"), c("#C5C9D1"), c("#2C2D31"), c("#DDE1E7"), + 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 From 33198a5043dd43f26a4a3ff8ddbcd7c4b13eb56b Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:07:57 +0800 Subject: [PATCH 160/193] style: sync light accent pressed token --- app/src/main/res/values/colors.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 78d641ea..47c231ab 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -5,7 +5,7 @@ #1C1C1E #6D6D72 #1D9BF0 - #0E86D6 + #0E85D5 #07131D #EEF0F3 #DDE1E7 From 313fff0e93e854ef0ba1baf7864491b4ea3b0b02 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:08:04 +0800 Subject: [PATCH 161/193] style: sync dark accent pressed token --- app/src/main/res/values-night/colors.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index e5cfbd0b..b29c2e4f 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -5,7 +5,7 @@ #F2F2F7 #AEAEB2 #6EC3F7 - #51B7F5 + #50B6F5 #07131D #242426 #303033 From 025980901ed8fa44ff84f64dbe4b107286352aaf Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:08:23 +0800 Subject: [PATCH 162/193] docs: record visual language implementation decisions --- docs/DECISIONS.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/DECISIONS.md diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md new file mode 100644 index 00000000..4f690229 --- /dev/null +++ b/docs/DECISIONS.md @@ -0,0 +1,7 @@ +# 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 尾部被立即截断,同时保持取消即时生效。 From 173d4dbe6a51d86e4bcaf1633fae8e6604719526 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:05:52 +0800 Subject: [PATCH 163/193] fix: remove keyboard key dead zones --- .../llc/slacker/openime/ImeThemeApplier.kt | 5 ++- .../openime/NumericKeyboardRenderer.kt | 30 +++++----------- .../openime/Pinyin26KeyboardRenderer.kt | 13 +++---- .../openime/Pinyin9KeyboardRenderer.kt | 34 ++++++------------- 4 files changed, 28 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt index f196b5bc..161d7ca3 100644 --- a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt +++ b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt @@ -2,6 +2,7 @@ package llc.slacker.openime import android.content.res.ColorStateList import android.graphics.Color +import android.graphics.drawable.InsetDrawable import android.view.View import android.view.ViewGroup import android.widget.FrameLayout @@ -90,7 +91,9 @@ internal class ImeThemeApplier( } view.applyMainTextScale(keyMainTextScale()) - view.background = statefulRounded(color, pressedColor, skinRadiusPx()) + val keyFace = statefulRounded(color, pressedColor, skinRadiusPx()) + val halfGap = toPx(ImeGeometryTokens.KEY_GAP_DP) / 2 + view.background = InsetDrawable(keyFace, halfGap, halfGap, halfGap, halfGap) view.background?.alpha = skinOpacity().coerceIn(70, 100) * 255 / 100 // Keep shadows restrained: ordinary typing surfaces need a slight diff --git a/app/src/main/java/llc/slacker/openime/NumericKeyboardRenderer.kt b/app/src/main/java/llc/slacker/openime/NumericKeyboardRenderer.kt index 5756e2ca..7d62cb78 100644 --- a/app/src/main/java/llc/slacker/openime/NumericKeyboardRenderer.kt +++ b/app/src/main/java/llc/slacker/openime/NumericKeyboardRenderer.kt @@ -77,7 +77,7 @@ internal class NumericKeyboardRenderer( LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, toPx(keyRowHeightDp()), - ).apply { topMargin = toPx(ImeGeometryTokens.KEY_ROW_GAP_DP) }, + ), ) container.addView(left, adaptiveColumnParams(1f)) @@ -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) - } - }, + ), ) } @@ -165,14 +161,14 @@ internal class NumericKeyboardRenderer( } side.addView( createBackspaceKey().apply { markSideKey(this) }, - sideKeyParams(includeBottomGap = true), + sideKeyParams(), ) side.addView( createKey("0", false, 22f) { onCommitCharacter("0") }.apply { tag = "key:0" markSideKey(this) }, - sideKeyParams(includeBottomGap = true), + sideKeyParams(), ) side.addView( createKey(literal("key:@", "@"), true, 15f) { @@ -181,7 +177,7 @@ internal class NumericKeyboardRenderer( tag = "key:@" markSideKey(this) }, - sideKeyParams(includeBottomGap = true), + sideKeyParams(), ) side.addView( createKey(enterLabel, true, 13f, onEnter).apply { @@ -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/Pinyin26KeyboardRenderer.kt b/app/src/main/java/llc/slacker/openime/Pinyin26KeyboardRenderer.kt index 76df459f..5de704de 100644 --- a/app/src/main/java/llc/slacker/openime/Pinyin26KeyboardRenderer.kt +++ b/app/src/main/java/llc/slacker/openime/Pinyin26KeyboardRenderer.kt @@ -99,7 +99,7 @@ internal class Pinyin26KeyboardRenderer( }, flexKeyParams(weights.rightOuter), ) - keyboardBody.addView(bottom, rowParams(includeBottomGap = false)) + keyboardBody.addView(bottom, rowParams()) } private fun segmentKey(): ImeKeyView = @@ -161,23 +161,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..0c0b1e92 100644 --- a/app/src/main/java/llc/slacker/openime/Pinyin9KeyboardRenderer.kt +++ b/app/src/main/java/llc/slacker/openime/Pinyin9KeyboardRenderer.kt @@ -64,7 +64,7 @@ internal class Pinyin9KeyboardRenderer( LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, toPx(keyRowHeightDp()), - ).apply { topMargin = toPx(ImeGeometryTokens.KEY_ROW_GAP_DP) }, + ), ) container.addView(left, adaptiveColumnParams(1f)) @@ -111,13 +111,13 @@ internal class Pinyin9KeyboardRenderer( } side.addView( createBackspaceKey().apply { markSideKey(this) }, - sideKeyParams(keyRowHeightDp(), includeBottomGap = true), + sideKeyParams(keyRowHeightDp()), ) side.addView( createKey("重输", true, null, 13f, onRetranslate).apply { markSideKey(this) }, - sideKeyParams(keyRowHeightDp(), includeBottomGap = true), + sideKeyParams(keyRowHeightDp()), ) side.addView( createKey(enterLabel, true, null, 13f, onEnter).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( From 414871cf86e1173471672ec45fa6df1cabb3e4dd Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:06:15 +0800 Subject: [PATCH 164/193] style: replace key shadows with bottom edge --- .../java/llc/slacker/openime/ImeThemeApplier.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt index 161d7ca3..595ff058 100644 --- a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt +++ b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt @@ -3,6 +3,7 @@ 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 @@ -92,15 +93,15 @@ internal class ImeThemeApplier( view.applyMainTextScale(keyMainTextScale()) 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(keyFace, halfGap, halfGap, halfGap, halfGap) + view.background = InsetDrawable(keyCap, halfGap, halfGap, halfGap, halfGap) view.background?.alpha = skinOpacity().coerceIn(70, 100) * 255 / 100 - // Keep shadows restrained: ordinary typing surfaces need a slight - // separation from the keyboard deck, while side/function controls - // stay visually embedded in that deck. - view.elevation = - if (side || function) 0f else toPx(1).toFloat() + view.elevation = 0f when { primary -> { From 5301e89987e64c5687ba81fee61bb27dfde52472 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:08:01 +0800 Subject: [PATCH 165/193] style: unify keyboard typography and digit hints --- .../main/java/llc/slacker/openime/ImeKeyView.kt | 10 +++++++--- .../java/llc/slacker/openime/ImeKeyboardView.kt | 10 +++++++--- .../slacker/openime/NumericKeyboardRenderer.kt | 16 ++++++++-------- .../slacker/openime/Pinyin26KeyboardRenderer.kt | 17 ++++++++--------- .../slacker/openime/Pinyin9KeyboardRenderer.kt | 12 ++++++------ 5 files changed, 36 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyView.kt index 6bd0a744..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) { @@ -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 db7d2aaf..07f02cdb 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -136,7 +136,7 @@ open class ImeKeyboardView( text = "", func = true, secondary = null, - mainTextSizeOverride = 15f, + mainTextSizeOverride = ImeTypographyTokens.BODY_SP, iconRes = R.drawable.ic_backspace, onTap = onTap, ) @@ -170,7 +170,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() @@ -2525,7 +2525,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" diff --git a/app/src/main/java/llc/slacker/openime/NumericKeyboardRenderer.kt b/app/src/main/java/llc/slacker/openime/NumericKeyboardRenderer.kt index 7d62cb78..6c06f63b 100644 --- a/app/src/main/java/llc/slacker/openime/NumericKeyboardRenderer.kt +++ b/app/src/main/java/llc/slacker/openime/NumericKeyboardRenderer.kt @@ -71,7 +71,7 @@ internal class NumericKeyboardRenderer( ), ) left.addView( - createKey("符号", true, 13f, onShowSymbols).apply { + createKey("符号", true, ImeTypographyTokens.BODY_SP, onShowSymbols).apply { markSideKey(this) }, LinearLayout.LayoutParams( @@ -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) }, @@ -117,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) }, @@ -126,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) } @@ -138,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:." @@ -164,14 +164,14 @@ internal class NumericKeyboardRenderer( 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(), ) side.addView( - createKey(literal("key:@", "@"), true, 15f) { + createKey(literal("key:@", "@"), true, ImeTypographyTokens.BODY_SP) { onCommitCharacter(literal("key:@", "@")) }.apply { tag = "key:@" @@ -180,7 +180,7 @@ internal class NumericKeyboardRenderer( sideKeyParams(), ) side.addView( - createKey(enterLabel, true, 13f, onEnter).apply { + createKey(enterLabel, true, ImeTypographyTokens.BODY_SP, onEnter).apply { tag = "key-enter" markSideKey(this) }, diff --git a/app/src/main/java/llc/slacker/openime/Pinyin26KeyboardRenderer.kt b/app/src/main/java/llc/slacker/openime/Pinyin26KeyboardRenderer.kt index 5de704de..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,13 +88,13 @@ 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), @@ -103,7 +103,7 @@ internal class Pinyin26KeyboardRenderer( } 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 diff --git a/app/src/main/java/llc/slacker/openime/Pinyin9KeyboardRenderer.kt b/app/src/main/java/llc/slacker/openime/Pinyin9KeyboardRenderer.kt index 0c0b1e92..b31724b8 100644 --- a/app/src/main/java/llc/slacker/openime/Pinyin9KeyboardRenderer.kt +++ b/app/src/main/java/llc/slacker/openime/Pinyin9KeyboardRenderer.kt @@ -58,7 +58,7 @@ internal class Pinyin9KeyboardRenderer( ), ) left.addView( - createKey("符号", true, null, 13f, onShowSymbols).apply { + createKey("符号", true, null, ImeTypographyTokens.BODY_SP, onShowSymbols).apply { markSideKey(this) }, LinearLayout.LayoutParams( @@ -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) }, @@ -114,13 +114,13 @@ internal class Pinyin9KeyboardRenderer( sideKeyParams(keyRowHeightDp()), ) side.addView( - createKey("重输", true, null, 13f, onRetranslate).apply { + createKey("重输", true, null, ImeTypographyTokens.BODY_SP, onRetranslate).apply { markSideKey(this) }, 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 { From ffdf7cb86940eea81e4bd8400cc091595a1436f0 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:08:46 +0800 Subject: [PATCH 166/193] perf: reuse instant key preview popup --- .../llc/slacker/openime/KeyPopupController.kt | 90 ++++++++++++++----- 1 file changed, 66 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/KeyPopupController.kt b/app/src/main/java/llc/slacker/openime/KeyPopupController.kt index 51516df4..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,15 +63,14 @@ 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) ImeTypographyTokens.BODY_SP else ImeTypographyTokens.DISPLAY_SP - 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 = if (text == "清空") { @@ -66,12 +83,15 @@ internal class KeyPopupController( dp(1).coerceAtLeast(1), ) } - elevation = dp(2).toFloat() } - 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) { @@ -148,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) @@ -171,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) { From f122e20f3fbb99348a7efc222593392189dd5dc2 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:14:17 +0800 Subject: [PATCH 167/193] feat: add timed clear undo affordance --- .../openime/BackspaceGestureController.kt | 2 +- .../llc/slacker/openime/ImeKeyboardView.kt | 11 ++++ .../llc/slacker/openime/ImeThemeApplier.kt | 9 ++++ .../java/llc/slacker/openime/ImeTopZone.kt | 53 +++++++++++++++++++ .../slacker/openime/InputConnectionGateway.kt | 33 ++++++++++-- .../slacker/openime/LocalVoiceImeService.kt | 18 ++++++- .../openime/InputConnectionGatewayTest.kt | 48 +++++++++++++++++ 7 files changed, 169 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/BackspaceGestureController.kt b/app/src/main/java/llc/slacker/openime/BackspaceGestureController.kt index aa81b7b6..eabce336 100644 --- a/app/src/main/java/llc/slacker/openime/BackspaceGestureController.kt +++ b/app/src/main/java/llc/slacker/openime/BackspaceGestureController.kt @@ -138,7 +138,7 @@ internal class BackspaceGestureController( val shouldClear = if (clearArmed) { upward > toPx(16) && horizontal <= toPx(120) } else { - upward >= toPx(36) && horizontal <= toPx(96) + upward >= toPx(56) && horizontal <= toPx(96) } if (shouldClear == clearArmed) return diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index 07f02cdb..7982b224 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -42,6 +42,7 @@ open class ImeKeyboardView( fun onBackspace() fun onClearAll() fun onUndoClear(): Boolean = false + fun onUndoClearExpired() {} fun onSpace() fun onFloatingKeyboardChanged(floating: Boolean) fun onFloatingKeyboardDragged(deltaX: Float, deltaY: Float) @@ -1042,6 +1043,8 @@ open class ImeKeyboardView( updateTopZone(composition.text?.isNotEmpty() == true) listener.onCandidateExpanded(open) }, + onUndoClear = { listener.onUndoClear() }, + onUndoClearExpired = listener::onUndoClearExpired, ) candidateBarController = CandidateBarController( context = context, @@ -1554,6 +1557,14 @@ open class ImeKeyboardView( if (visualChanged) applyTheme() } + 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 diff --git a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt index 595ff058..a009fd97 100644 --- a/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt +++ b/app/src/main/java/llc/slacker/openime/ImeThemeApplier.kt @@ -281,6 +281,15 @@ 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) diff --git a/app/src/main/java/llc/slacker/openime/ImeTopZone.kt b/app/src/main/java/llc/slacker/openime/ImeTopZone.kt index f1afc438..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) @@ -56,6 +58,13 @@ internal class ImeTopZone( 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(), @@ -319,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) @@ -363,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 7e576ad1..dc51839b 100644 --- a/app/src/main/java/llc/slacker/openime/InputConnectionGateway.kt +++ b/app/src/main/java/llc/slacker/openime/InputConnectionGateway.kt @@ -4,6 +4,7 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.os.Build +import android.os.SystemClock import android.view.KeyEvent import android.view.inputmethod.ExtractedText import android.view.inputmethod.ExtractedTextRequest @@ -42,6 +43,7 @@ class InputConnectionGateway( private val context: Context?, private val connection: () -> InputConnection?, private val isPassword: () -> Boolean = { false }, + private val nowMs: () -> Long = { SystemClock.elapsedRealtime() }, ) { data class CursorSnapshot( @@ -67,6 +69,8 @@ class InputConnectionGateway( val text: String, val selectionStart: Int, val selectionEnd: Int, + val connection: InputConnection, + val expiresAtMs: Long, ) private var clearUndoSnapshot: ClearUndoSnapshot? = null @@ -75,6 +79,21 @@ class InputConnectionGateway( 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() @@ -259,7 +278,7 @@ class InputConnectionGateway( restoreSelectionAfterFailedClear(ic, originalSelection) } else { ic.finishComposingText() - rememberClearUndo(selected, originalSelection) + rememberClearUndo(selected, originalSelection, ic) } return cleared } @@ -278,7 +297,7 @@ class InputConnectionGateway( restoreSelectionAfterFailedClear(ic, originalSelection) } else { ic.finishComposingText() - rememberClearUndo(selectedWindow.text, originalSelection) + rememberClearUndo(selectedWindow.text, originalSelection, ic) } return cleared } @@ -298,7 +317,7 @@ class InputConnectionGateway( restoreSelectionAfterFailedClear(ic, originalSelection) } else { ic.finishComposingText() - rememberClearUndo(window.text, originalSelection) + rememberClearUndo(window.text, originalSelection, ic) } cleared } finally { @@ -315,6 +334,10 @@ class InputConnectionGateway( 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) { @@ -353,6 +376,7 @@ class InputConnectionGateway( private fun rememberClearUndo( text: String, selection: SelectionSnapshot?, + ic: InputConnection, ) { if (text.isEmpty()) return val start: Int @@ -375,6 +399,8 @@ class InputConnectionGateway( text = text, selectionStart = start.coerceIn(0, text.length), selectionEnd = end.coerceIn(0, text.length), + connection = ic, + expiresAtMs = nowMs() + CLEAR_UNDO_TIMEOUT_MS, ) } @@ -702,5 +728,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/LocalVoiceImeService.kt b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt index 97fb4131..0a7a34d5 100644 --- a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt +++ b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt @@ -191,6 +191,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 @@ -663,13 +667,19 @@ 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 { @@ -690,10 +700,16 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can lastComposition = "" renderedCandidateSnapshot = null keyboardView?.clearAssociationCandidates() + keyboardView?.hideClearUndo() keyboardView?.renderState(state) return true } + override fun onUndoClearExpired() { + gateway.discardClearUndo() + keyboardView?.hideClearUndo() + } + override fun onFloatingKeyboardChanged(floating: Boolean) { keyboardView?.setFloatingWindowMode(floating) if (floating) floatingWindow.enable() else floatingWindow.restore() 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( From 794a183e170db8833d788d4d0e84f41a223f0a10 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:48:35 +0800 Subject: [PATCH 168/193] fix: gate candidate deletion to learned words --- app/src/main/cpp/local_rime_jni.cc | 34 +++++++++++++ .../slacker/openime/CandidateBarController.kt | 14 ++++++ .../llc/slacker/openime/ImeKeyboardView.kt | 6 +++ .../slacker/openime/LocalVoiceImeService.kt | 50 +++++++++++++------ .../java/llc/slacker/openime/RimeEngine.kt | 31 ++++++++++++ .../java/llc/slacker/openime/RimeNative.kt | 4 ++ .../slacker/openime/UserPhraseRepository.kt | 9 ++++ 7 files changed, 133 insertions(+), 15 deletions(-) diff --git a/app/src/main/cpp/local_rime_jni.cc b/app/src/main/cpp/local_rime_jni.cc index aef3ba0a..e48afa34 100644 --- a/app/src/main/cpp/local_rime_jni.cc +++ b/app/src/main/cpp/local_rime_jni.cc @@ -7,6 +7,9 @@ #include #include +#include +#include +#include namespace { @@ -163,6 +166,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); @@ -330,6 +354,16 @@ 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) { diff --git a/app/src/main/java/llc/slacker/openime/CandidateBarController.kt b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt index bca90122..0d2863d8 100644 --- a/app/src/main/java/llc/slacker/openime/CandidateBarController.kt +++ b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt @@ -1,5 +1,6 @@ package llc.slacker.openime +import android.app.AlertDialog import android.content.Context import android.graphics.Color import android.graphics.Typeface @@ -369,6 +370,19 @@ internal class CandidateBarController( } } + 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 diff --git a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index 7982b224..9f3e3c45 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -1557,6 +1557,12 @@ open class ImeKeyboardView( if (visualChanged) applyTheme() } + fun confirmCandidateDeletion(candidate: String, onConfirm: () -> Unit) { + if (::candidateBarController.isInitialized) { + candidateBarController.confirmCandidateDeletion(candidate, onConfirm) + } + } + fun showClearUndo() { if (::topZone.isInitialized) topZone.showUndoClear() } diff --git a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt index 0a7a34d5..b133a24e 100644 --- a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt +++ b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt @@ -989,11 +989,11 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can limit = 16, ).contains(candidate) ) { - showTextEditFeedback("这是常用语,请在常用语面板中编辑或删除") return } val modeAtRequest = state.keyboardMode + fun refreshAfterDelete() { if (lastComposition != composition || state.keyboardMode != modeAtRequest) return val fallback = fallbackCandidatesFor(composition, modeAtRequest) @@ -1004,30 +1004,50 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can ) } + fun confirmDelete(onConfirm: () -> Unit) { + if (lastComposition != composition || state.keyboardMode != modeAtRequest) return + keyboardView?.confirmCandidateDeletion(candidate, onConfirm) + } + if (!rime.isReady) { - if (UserPhraseRepository.forget(composition, candidate)) { - PersonalizationRepository.forget(candidate) - refreshAfterDelete() - showTextEditFeedback("已移除个人候选") - } else { - showTextEditFeedback("该候选不是个人学习词") + if (!UserPhraseRepository.contains(composition, candidate)) return + confirmDelete { + if (UserPhraseRepository.forget(composition, candidate)) { + PersonalizationRepository.forget(candidate) + refreshAfterDelete() + showTextEditFeedback("已删除个人学习词") + } } return } - val queued = rime.deleteCandidate(composition, candidate) { deleted -> + val queued = rime.isUserLearnedCandidate(composition, candidate) { learned -> mainHandler.post { - if (deleted) { - PersonalizationRepository.forget(candidate) - refreshAfterDelete() - showTextEditFeedback("已移除个人候选") - } else { - showTextEditFeedback("系统词条不可删除") + 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) { - showTextEditFeedback("暂时无法修改个人词") + return } } diff --git a/app/src/main/java/llc/slacker/openime/RimeEngine.kt b/app/src/main/java/llc/slacker/openime/RimeEngine.kt index 40957dea..5b566466 100644 --- a/app/src/main/java/llc/slacker/openime/RimeEngine.kt +++ b/app/src/main/java/llc/slacker/openime/RimeEngine.kt @@ -290,6 +290,34 @@ 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. @@ -314,6 +342,9 @@ class RimeEngine( 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) } diff --git a/app/src/main/java/llc/slacker/openime/RimeNative.kt b/app/src/main/java/llc/slacker/openime/RimeNative.kt index 9ea3c9ec..0e3cabe7 100644 --- a/app/src/main/java/llc/slacker/openime/RimeNative.kt +++ b/app/src/main/java/llc/slacker/openime/RimeNative.kt @@ -26,6 +26,10 @@ 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 diff --git a/app/src/main/java/llc/slacker/openime/UserPhraseRepository.kt b/app/src/main/java/llc/slacker/openime/UserPhraseRepository.kt index 49b6eca5..8eb700e2 100644 --- a/app/src/main/java/llc/slacker/openime/UserPhraseRepository.kt +++ b/app/src/main/java/llc/slacker/openime/UserPhraseRepository.kt @@ -96,6 +96,15 @@ 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 From 6929cb02b0f2657fe21f98a0794179d453df0377 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:57:14 +0800 Subject: [PATCH 169/193] fix: stabilize and instrument nine-key input --- .../openime/RimeNineKeyInstrumentedTest.kt | 46 +++++++++++++ .../slacker/openime/CandidateBarController.kt | 24 +++++++ .../llc/slacker/openime/CandidatePipeline.kt | 10 ++- .../openime/CandidateQueryCoordinator.kt | 11 +++- .../llc/slacker/openime/ImeKeyboardView.kt | 11 ++++ .../slacker/openime/LocalVoiceImeService.kt | 31 +++++++-- .../slacker/openime/NineKeyLocalDecoder.kt | 29 +-------- .../openime/NineKeyPerformanceTrace.kt | 65 +++++++++++++++++++ .../llc/slacker/openime/NineKeyChineseTest.kt | 6 ++ docs/DECISIONS.md | 2 + 10 files changed, 199 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/llc/slacker/openime/NineKeyPerformanceTrace.kt diff --git a/app/src/androidTest/java/llc/slacker/openime/RimeNineKeyInstrumentedTest.kt b/app/src/androidTest/java/llc/slacker/openime/RimeNineKeyInstrumentedTest.kt index f0641f30..594d1e28 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", + instrumentation.arguments.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/main/java/llc/slacker/openime/CandidateBarController.kt b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt index 0d2863d8..071e504a 100644 --- a/app/src/main/java/llc/slacker/openime/CandidateBarController.kt +++ b/app/src/main/java/llc/slacker/openime/CandidateBarController.kt @@ -6,6 +6,7 @@ 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 @@ -50,6 +51,8 @@ internal class CandidateBarController( 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 @@ -59,10 +62,12 @@ internal class CandidateBarController( 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 ( @@ -75,14 +80,23 @@ internal class CandidateBarController( } } 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 @@ -359,6 +373,15 @@ internal class CandidateBarController( }, 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) @@ -386,5 +409,6 @@ internal class CandidateBarController( 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/CandidatePipeline.kt b/app/src/main/java/llc/slacker/openime/CandidatePipeline.kt index 8b58592d..54d3fc04 100644 --- a/app/src/main/java/llc/slacker/openime/CandidatePipeline.kt +++ b/app/src/main/java/llc/slacker/openime/CandidatePipeline.kt @@ -45,7 +45,15 @@ 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, 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/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index 9f3e3c45..ff4bd5fc 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 @@ -1557,6 +1558,10 @@ open class ImeKeyboardView( if (visualChanged) applyTheme() } + fun isCandidateInteractionActive(): Boolean = + ::candidateBarController.isInitialized && + candidateBarController.isInteractionActive() + fun confirmCandidateDeletion(candidate: String, onConfirm: () -> Unit) { if (::candidateBarController.isInitialized) { candidateBarController.confirmCandidateDeletion(candidate, onConfirm) @@ -2228,12 +2233,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 diff --git a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt index b133a24e..a31f9b4c 100644 --- a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt +++ b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt @@ -23,13 +23,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 @@ -1272,12 +1274,23 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can mode = mode, rimeInputs = rimeInputs, onResult = result@{ request, queryInputs, query -> - if ( - state.keyboardMode != mode || - lastComposition != composition - ) return@result + 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 + 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. @@ -1324,6 +1337,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" @@ -1343,7 +1357,9 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can candidates = finalCandidates, nativeReferences = nativeReferences, ) - keyboardView?.renderState(state) + keyboardView?.renderState(state) + } + applyWhenCandidateSurfaceIdle() }, ) @@ -1484,6 +1500,7 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can 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 diff --git a/app/src/main/java/llc/slacker/openime/NineKeyLocalDecoder.kt b/app/src/main/java/llc/slacker/openime/NineKeyLocalDecoder.kt index 18b23f33..daa50ecc 100644 --- a/app/src/main/java/llc/slacker/openime/NineKeyLocalDecoder.kt +++ b/app/src/main/java/llc/slacker/openime/NineKeyLocalDecoder.kt @@ -149,21 +149,14 @@ 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 + 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 +333,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/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/docs/DECISIONS.md b/docs/DECISIONS.md index 4f690229..d3a90663 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -5,3 +5,5 @@ - 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 后再定。 From 956b088bdeadfae535b4abe83c71b5b400c1a600 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:58:45 +0800 Subject: [PATCH 170/193] feat: add adaptive openIME brand assets --- app/src/main/AndroidManifest.xml | 3 ++- app/src/main/res/drawable/ic_brand_mark.xml | 10 ++++++++++ app/src/main/res/drawable/ic_launcher.xml | 1 - .../main/res/drawable/ic_launcher_foreground.xml | 10 ++++++++++ app/src/main/res/mipmap-anydpi/ic_launcher.xml | 6 ++++++ .../main/res/mipmap-anydpi/ic_launcher_round.xml | 6 ++++++ app/src/main/res/values-v31/themes.xml | 15 +++++++++++++++ 7 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable/ic_brand_mark.xml delete mode 100644 app/src/main/res/drawable/ic_launcher.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/mipmap-anydpi/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi/ic_launcher_round.xml create mode 100644 app/src/main/res/values-v31/themes.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 184638ee..8a4003b8 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"> diff --git a/app/src/main/res/drawable/ic_brand_mark.xml b/app/src/main/res/drawable/ic_brand_mark.xml new file mode 100644 index 00000000..28d4895c --- /dev/null +++ b/app/src/main/res/drawable/ic_brand_mark.xml @@ -0,0 +1,10 @@ + + + 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/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-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 @@ + + + + From 8c3f5d65674d10cbb42272cdea80748872fff22a Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:26:12 +0800 Subject: [PATCH 171/193] feat: rebuild setup flow and unify activity headers --- app/src/main/AndroidManifest.xml | 6 +- .../llc/slacker/openime/ImeDesignTokens.kt | 4 + .../llc/slacker/openime/ImeKeyboardView.kt | 1 + .../java/llc/slacker/openime/MainActivity.kt | 193 +++++++--- .../llc/slacker/openime/PanelHeaderFactory.kt | 15 + .../openime/QuickPhraseEditActivity.kt | 54 +-- .../openime/SettingsPanelController.kt | 2 +- .../main/java/llc/slacker/openime/SetupUi.kt | 72 ++++ .../slacker/openime/SymbolManagerActivity.kt | 56 +-- app/src/main/res/drawable/bg_setup_card.xml | 9 + app/src/main/res/drawable/bg_setup_mark.xml | 7 +- .../res/drawable/bg_setup_mark_active.xml | 6 +- .../main/res/drawable/bg_setup_mark_done.xml | 6 +- app/src/main/res/layout/activity_main.xml | 329 +++++++++++------- app/src/main/res/values-en/strings.xml | 9 + app/src/main/res/values/dimens.xml | 14 + app/src/main/res/values/strings.xml | 24 +- .../llc/slacker/openime/TokenDriftTest.kt | 29 +- 18 files changed, 548 insertions(+), 288 deletions(-) create mode 100644 app/src/main/res/drawable/bg_setup_card.xml create mode 100644 app/src/main/res/values-en/strings.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8a4003b8..6b4dee0c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,19 +28,19 @@ (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,35 +182,80 @@ 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.secondaryBackground(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 + 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 @@ -219,7 +326,7 @@ class MainActivity : Activity() { 0, ) mark.compoundDrawableTintList = ColorStateList.valueOf( - if (active) accent else if (done) getColor(R.color.setup_ready) else accent, + if (done) contrastText(getColor(R.color.setup_ready)) else accent, ) mark.setBackgroundResource( when { @@ -229,18 +336,12 @@ 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 = ImeDrawableFactory.rounded( - contrastText(accent), - SetupUi.dp(this, 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?.imageTintList = ColorStateList.valueOf( if (active) contrastText(accent) else getColor(R.color.setup_body), ) 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/QuickPhraseEditActivity.kt b/app/src/main/java/llc/slacker/openime/QuickPhraseEditActivity.kt index cb54c068..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 @@ -102,34 +98,11 @@ class QuickPhraseEditActivity : Activity() { } private fun render(category: String, code: String, phrase: String) { - val accent = SetupUi.accent(this) - val title = TextView(this).apply { - text = if (phraseId > 0L) "编辑常用语" else "新增常用语" - textSize = ImeTypographyTokens.TITLE_SP - 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)) + val header = SetupUi.activityTopBar( + context = this, + title = if (phraseId > 0L) "编辑常用语" else "新增常用语", + ) { + requestClose() } categoryEdit = EditText(this).apply { @@ -290,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, @@ -357,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/SettingsPanelController.kt b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt index 20c4354c..c78dcee3 100644 --- a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt +++ b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt @@ -91,7 +91,7 @@ internal class SettingsPanelController( createHeader("偏好设置"), LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, - toPx(ImeGeometryTokens.TOUCH_TARGET_DP), + toPx(ImeGeometryTokens.TOP_BAR_HEIGHT_DP), ), ) } else { diff --git a/app/src/main/java/llc/slacker/openime/SetupUi.kt b/app/src/main/java/llc/slacker/openime/SetupUi.kt index bd846834..39a5a6c2 100644 --- a/app/src/main/java/llc/slacker/openime/SetupUi.kt +++ b/app/src/main/java/llc/slacker/openime/SetupUi.kt @@ -7,6 +7,11 @@ import android.graphics.Color import android.graphics.drawable.ColorDrawable 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 @@ -241,6 +246,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") diff --git a/app/src/main/java/llc/slacker/openime/SymbolManagerActivity.kt b/app/src/main/java/llc/slacker/openime/SymbolManagerActivity.kt index 4ee4221f..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,35 +87,18 @@ class SymbolManagerActivity : Activity() { orientation = LinearLayout.VERTICAL setPadding(dp(20), dp(24), dp(20), dp(24)) } - val title = TextView(this).apply { - text = "自定义符号" - textSize = ImeTypographyTokens.TITLE_SP - 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 = ImeTypographyTokens.BODY_SP @@ -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/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/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 7e718d43..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: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:textStyle="bold" /> + + android:textStyle="bold" /> + + android:src="@drawable/ic_arrow_back" + android:tint="@color/setup_body" /> + android:paddingStart="@dimen/ime_space_lg" + android:paddingEnd="@dimen/ime_space_lg"> + + android:textStyle="bold" /> + + android:textStyle="bold" /> + + android:src="@drawable/ic_arrow_back" + android:tint="@color/setup_body" /> + + + + + + + + + + + + + + + + + android:padding="@dimen/ime_space_sm"> + + + + + + + + + + android:textSize="@dimen/ime_text_body" /> + android:paddingStart="@dimen/ime_space_lg" + android:paddingEnd="@dimen/ime_space_lg"> + - - + android:textStyle="bold" /> - - + android:src="@drawable/ic_arrow_back" + android:tint="@color/setup_body" /> - - 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..da09ae7a --- /dev/null +++ b/app/src/main/res/values-en/strings.xml @@ -0,0 +1,9 @@ + + + openIME + Chinese + English + Preferences + Quick phrases + Custom symbols + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 008a3cbf..f6c3e14a 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -6,4 +6,18 @@ 18sp 21sp 28sp + + 2dp + 4dp + 8dp + 12dp + 16dp + 24dp + 32dp + 48dp + 56dp + 24dp + 28dp + 16dp + 72dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 967177ce..2706425b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,21 +5,31 @@ 英文 本地拼音输入法\n词库和语音都在设备内运行 在这里试打 - 第一步 启用 openIME - 已启用,可再次打开 - 第二步 切换到 openIME - 当前正在使用,可切换 + 启用 + 已启用 + 切换 + 已切换到 openIME 启用后即可开始输入 已启用,切换后就能打字 已就绪,在下方试打 输入 nihao 得到 你好。点候选上屏,点空格选首选。 - 语音可选。授权后长按空格说话,不授权也能打字。 - 开启本地语音 - 麦克风已授权 + 语音输入需要麦克风权限。音频只在本机处理,不会上传。可选择授权,也可以跳过后继续试打。 + 未获得麦克风授权,语音输入无法使用。授权后即可长按空格说话。 + 授权 + 前往系统设置 + 跳过 + 已跳过,可随时授权 + 已授权 + 麦克风已授权,语音输入可以使用。长按空格开始说话,松手即可识别。 + 试打 + 试打完成 打开偏好设置 先启用并切换到 openIME,再进入设置 强调色 自定义 输入 6 位十六进制颜色,例如 5B6B7A 偏好设置 + 偏好设置 + 常用语 + 自定义符号 diff --git a/app/src/test/java/llc/slacker/openime/TokenDriftTest.kt b/app/src/test/java/llc/slacker/openime/TokenDriftTest.kt index 493a0409..b2b4f6b0 100644 --- a/app/src/test/java/llc/slacker/openime/TokenDriftTest.kt +++ b/app/src/test/java/llc/slacker/openime/TokenDriftTest.kt @@ -62,12 +62,39 @@ class TokenDriftTest { } } + @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 != "ic_launcher.xml" } + .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\"")) From ce1ee90bfe1b312285c620198ac953fe9c681dd2 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:39:47 +0800 Subject: [PATCH 172/193] feat: add local user data import and export --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 5 + app/src/main/cpp/local_rime_jni.cc | 85 ++++ .../llc/slacker/openime/AboutDataActivity.kt | 371 +++++++++++++++ .../llc/slacker/openime/ImeKeyboardView.kt | 2 + .../slacker/openime/ImeSettingsActivity.kt | 5 + .../slacker/openime/LocalVoiceImeService.kt | 24 + .../java/llc/slacker/openime/RimeEngine.kt | 72 +++ .../java/llc/slacker/openime/RimeNative.kt | 9 + .../openime/SettingsPanelController.kt | 14 + .../llc/slacker/openime/UserDataArchive.kt | 425 ++++++++++++++++++ .../slacker/openime/UserPhraseRepository.kt | 51 +++ app/src/main/res/drawable/ic_info.xml | 9 + app/src/main/res/values-en/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../slacker/openime/UserDataArchiveTest.kt | 97 ++++ docs/DECISIONS.md | 1 + 17 files changed, 1173 insertions(+) create mode 100644 app/src/main/java/llc/slacker/openime/AboutDataActivity.kt create mode 100644 app/src/main/java/llc/slacker/openime/UserDataArchive.kt create mode 100644 app/src/main/res/drawable/ic_info.xml create mode 100644 app/src/test/java/llc/slacker/openime/UserDataArchiveTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9773dee8..7d59099d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -59,6 +59,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/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6b4dee0c..60b250f3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -43,6 +43,11 @@ android:label="@string/activity_label_symbols" android:windowSoftInputMode="adjustResize" /> + + #include +#include #include #include #include @@ -9,6 +10,7 @@ #include #include #include +#include #include namespace { @@ -257,6 +259,36 @@ bool select_schema_locked(const std::string& schema_id) { g_api->select_schema(g_session, schema_id.c_str()); } +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; @@ -378,6 +410,59 @@ Java_llc_slacker_openime_RimeNative_nativeDeleteCandidate( : 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 make_strings(env, {}); + + const std::string target = jstring_to_utf8(env, target_dir); + if (target.empty()) return make_strings(env, {}); + std::error_code error; + std::filesystem::create_directories(target, error); + if (error) return make_strings(env, {}); + + 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; + 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) { + exported.emplace_back(name + "\t" + path.string()); + } + } + } + resume_session_locked(schema); + 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 (name.empty() || 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..2f7882fb --- /dev/null +++ b/app/src/main/java/llc/slacker/openime/AboutDataActivity.kt @@ -0,0 +1,371 @@ +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 -> + 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 自动学习词库导入失败,其他数据未修改。") + } + 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/ImeKeyboardView.kt b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt index 0d2e2128..9f4bf482 100644 --- a/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt +++ b/app/src/main/java/llc/slacker/openime/ImeKeyboardView.kt @@ -102,6 +102,7 @@ open class ImeKeyboardView( 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). */ @@ -603,6 +604,7 @@ open class ImeKeyboardView( onKeyboardHeightChanged = ::setKeyboardHeightPercent, onFloatingStyleChanged = ::setFloatingStyle, onShowFuzzySettings = { showPanel(Panel.FUZZY_SETTINGS) }, + onOpenAboutData = listener::onOpenAboutData, onFeedback = ::feedback, applyTheme = ::applyTheme, onHierarchyRebuilt = ::onViewHierarchyRebuilt, diff --git a/app/src/main/java/llc/slacker/openime/ImeSettingsActivity.kt b/app/src/main/java/llc/slacker/openime/ImeSettingsActivity.kt index d753a0e7..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 @@ -209,4 +210,8 @@ class ImeSettingsActivity : Activity(), ImeKeyboardView.Listener { ImeSettingsRepository.saveFloatingOpacityPercent(this, opacityPercent) refreshLiveIme() } + + override fun onOpenAboutData() { + startActivity(Intent(this, AboutDataActivity::class.java)) + } } diff --git a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt index a31f9b4c..61477399 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 @@ -1498,6 +1500,28 @@ 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 diff --git a/app/src/main/java/llc/slacker/openime/RimeEngine.kt b/app/src/main/java/llc/slacker/openime/RimeEngine.kt index 5b566466..b95c3598 100644 --- a/app/src/main/java/llc/slacker/openime/RimeEngine.kt +++ b/app/src/main/java/llc/slacker/openime/RimeEngine.kt @@ -353,6 +353,78 @@ class RimeEngine( } } + fun exportUserDictionaries( + targetDir: File, + onComplete: (List) -> Unit, + ): Boolean { + if (!isReady) return false + return mutationQueue.submit { + val exported = synchronized(lock) { + if (!isReady) { + emptyList() + } else { + runCatching { + if (!syncSchemaFromSettingsLocked()) return@runCatching emptyList() + targetDir.mkdirs() + RimeNative.nativeExportUserDictionaries(targetDir.absolutePath) + .orEmpty() + .mapNotNull { mapping -> + val separator = mapping.indexOf('\t') + if (separator <= 0 || separator >= mapping.lastIndex) { + return@mapNotNull null + } + val name = mapping.substring(0, separator).trim() + val path = mapping.substring(separator + 1) + val file = File(path) + if (name.isEmpty() || !file.isFile) { + null + } else { + RimeUserDictionaryArchive( + name = name, + content = file.readText(), + ) + } + } + }.getOrDefault(emptyList()) + } + } + onComplete(exported) + } + } + + 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 0e3cabe7..764d57ac 100644 --- a/app/src/main/java/llc/slacker/openime/RimeNative.kt +++ b/app/src/main/java/llc/slacker/openime/RimeNative.kt @@ -34,6 +34,15 @@ internal object RimeNative { @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 c78dcee3..ffffd5a3 100644 --- a/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt +++ b/app/src/main/java/llc/slacker/openime/SettingsPanelController.kt @@ -63,6 +63,7 @@ internal class SettingsPanelController( 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, @@ -256,6 +257,18 @@ internal class SettingsPanelController( groupParams(), ) + content.addView(createSectionTitle("数据"), wrapParams()) + content.addView( + settingGroup( + settingNavigationRow( + "关于与数据", + "版本、隐私、导出与导入", + onOpenAboutData, + ), + ), + groupParams(), + ) + scroll.addView( content, ViewGroup.LayoutParams( @@ -396,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 }, ) 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..1f3b3849 --- /dev/null +++ b/app/src/main/java/llc/slacker/openime/UserDataArchive.kt @@ -0,0 +1,425 @@ +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 { it.name.isNotEmpty() && it.content.isNotEmpty() }, + ) + } + + 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 8eb700e2..46ceb480 100644 --- a/app/src/main/java/llc/slacker/openime/UserPhraseRepository.kt +++ b/app/src/main/java/llc/slacker/openime/UserPhraseRepository.kt @@ -191,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/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/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index da09ae7a..79020316 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -6,4 +6,5 @@ Preferences Quick phrases Custom symbols + About & data diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2706425b..db440796 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,4 +32,5 @@ 偏好设置 常用语 自定义符号 + 关于与数据 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..a7d86e22 --- /dev/null +++ b/app/src/test/java/llc/slacker/openime/UserDataArchiveTest.kt @@ -0,0 +1,97 @@ +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 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/docs/DECISIONS.md b/docs/DECISIONS.md index d3a90663..932e1dc9 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -7,3 +7,4 @@ - 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 会话已加载时包含自动学习词库;不可用时必须明确提示,不静默遗漏。 From bff922aae340526dbf5170a1e01e8ccb6be2e6d0 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:43:47 +0800 Subject: [PATCH 173/193] build: add signed arm64 release pipeline --- .github/workflows/release.yml | 177 ++++++++++++++++++++++++++++++++++ app/build.gradle.kts | 53 +++++++++- 2 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..b96165a0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,177 @@ +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 + 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 \ + --verify-tag \ + --title "openIME $GITHUB_REF_NAME" \ + --notes-file RELEASE_NOTES.md diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7d59099d..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", + ) + } } } From dd7209600e75f367a3d1da9eb03c697d3c2bb298 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:50:11 +0800 Subject: [PATCH 174/193] docs: align distribution branding and notices --- .gitignore | 2 + README.md | 43 ++++++++++---- THIRD_PARTY_NOTICES.md | 33 ++++++----- docs/LICENSING.md | 9 ++- docs/images/openime-brand.png | Bin 0 -> 1073 bytes docs/images/social-preview.png | Bin 0 -> 10153 bytes scripts/generate_brand_assets.py | 93 +++++++++++++++++++++++++++++++ 7 files changed, 154 insertions(+), 26 deletions(-) create mode 100644 docs/images/openime-brand.png create mode 100644 docs/images/social-preview.png create mode 100755 scripts/generate_brand_assets.py 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 5e641c04..bb8cc1d2 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,33 @@ # openIME -独立的 Android 系统输入法,包名为 `llc.slacker.openime`,应用显示名为 -`openIME`。它不依赖 `Minis for Android`,也不与其他输入法共享进程、数据或服务。 +

+ openIME +

+ +

本地优先的 Android 中文输入法。

+ +openIME 是独立 Android 系统输入法,包名 `llc.slacker.openime`。拼音候选、用户学习和语音识别均在设备内运行;应用不声明 `INTERNET` 权限。 [![Android CI](https://github.com/Slacker-LLC/openIME/actions/workflows/android.yml/badge.svg)](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. **授权(可选)**:需要本地语音输入时授权麦克风;不授权也可以正常打字。 + +引导页还提供“试打”输入框。语音输入长按空格达到系统长按阈值后开始录音,松手后识别并上屏。 + +## 截图 + +真实截图由 `scripts/visual_matrix_regression.ps1` 和视觉验收流程生成。仓库不提交占位图;26 键、九键与设置页的浅色/深色截图将在真实视觉矩阵验收后写入 `docs/images/`。 + ## 项目定位 openIME 的界面、输入法引擎和本地语音链路均在同一个独立 APK 内运行: @@ -94,10 +117,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 +127,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 变体,不会成为正式输入法的公共控制入口。 ## 验证 @@ -151,9 +172,11 @@ bash scripts/verify_linux.sh emulator-5554 ## 隐私与安全 -语音 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..1811aefb 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` | +| OpenCC | `app/src/main/cpp/vendor/OpenCC/` | https://github.com/BYVoid/OpenCC | Apache-2.0 | 已核对;许可证全文保留在 vendored 目录 `LICENSE` | +| Snappy | `app/src/main/cpp/vendor/snappy/` | https://github.com/google/snappy | BSD-3-Clause | 已核对;版权与许可保留在 `COPYING` | +| 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 | 已核对上游 runtime 许可证;发布时保留本清单及上游归属 | +| 中英双语 Zipformer 语音模型 | `app/src/main/assets/models/voice/bilingual-zipformer/` | sherpa-onnx 模型 `sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20`;原模型 https://huggingface.co/csukuangfj/k2fsa-zipformer-chinese-english-mixed | Apache-2.0 | 已核对模型卡许可证;`manifest.json` 固定模型 ID/版本与文件哈希 | +| Microsoft Fluent Emoji | `app/src/main/assets/emoji/fluent/` | https://github.com/microsoft/fluentui-emoji | MIT | 仅打包当前实际使用的基础情绪表情资源 | -应用只打包「Smileys & Emotion」中的基础黄豆脸/情绪表情;没有把手势、动物、食物或贴图资源打进 APK。 +## 发布前检查 -## Rime Ice 中文词库 +- 不要删除 vendored 源码目录中的 LICENSE、COPYING、AUTHORS 或 NOTICE 类文件。 +- `app/src/main/assets/licenses/rime-ice-GPL-3.0.txt` 必须继续随发布资产保留。 +- 语音 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/docs/LICENSING.md b/docs/LICENSING.md index 4f8aa6d9..0e750420 100644 --- a/docs/LICENSING.md +++ b/docs/LICENSING.md @@ -24,6 +24,9 @@ `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。内置中英双语 Zipformer 模型 +`sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20` 的原模型 +`csukuangfj/k2fsa-zipformer-chinese-english-mixed` 模型卡标记为 Apache-2.0。 +具体来源、文件位置和发布核对项统一记录在根目录 `THIRD_PARTY_NOTICES.md`。 +Git LFS 只负责文件存储,不改变文件的许可证。 diff --git a/docs/images/openime-brand.png b/docs/images/openime-brand.png new file mode 100644 index 0000000000000000000000000000000000000000..c43c509cbf3e3a56275d3ac66cc17a45f19e60b0 GIT binary patch literal 1073 zcmeAS@N?(olHy`uVBq!ia0y~yU{nBM4mJh`29f4<4Gav-KRjI=Ln>~)y}NO$l7WDW z%Skr6xnn~Fa5iF z!x6SXA%%mCjzGe30*gn5LkEjTiGVVrketAxFASXgPAU$En4L`Q9S$`Md<}iXYt%w6aq@`s1D{6bn$37!v5%8prr{=A=9HRJQKb$OMYJ^Bh)d0p_Bi} zUE}+Vj7%wi9q!d?w=*g|;bg05inLE)jdW;WP5Ij(n!?B^2GkKYft4k~;TnI0qnN;k zxs#P&WPacO^KE@al%9e-lR&1wfkHMTM4Q7sCME#|1#O3Y{~K%9>{>4-@PLzzL*T#~ zhM)h~ziobWFUp}oT|&Wufr*9V!u1+P-rv8z|J~Za05qaM{-l&ut$pOj)d{wzH#RUZ z0+l#`C;4W?ti;| z-S^{{)lY-MJE5XM&0h6DIHQMmR8BW_lvYHZNXGV8Tz9 z%}ol`edA7%ZyfqQr2V{QQ1QF-6&hhE@a>L zW$c47<~!qgtM`50<>~wDTjr0M$^7p7KKHrLb)D;+yC<6JDs;4aXb}hm-MO=8E+G)x z;OiD|>Mihb;f2C70>Ni@?#yW|*N*9K7f-yfKu*+kS5f1VSD` zi9mQGw!^LPUcJ-$rzvGBsLw?2bAb3;e_QE4?dy* zuf}+Uh?2wed5Jg&Iabda@7_s{$2Vy^w?lAxFDh-PgkY0jJ9wX>51V|Q8lj0jaUY?z zbp}DXKl1VZ+95(MX|zIp46)@2)4*T@B2o_?jX~L`ZZ7xO zu5$D9pbSJQrVNFbH>=hVM&&9CHnlUax54>bRCQ1RpYmG{jn$@{$N_h0fe_L>tOtChF%0;ubY{;&;wub6$a*>PHAf$x?O0;yhk;dtHs=4T}BMoF*{G zk0n>`HZ81PGtl`Xi2r%!n1gdg2MsR%M8>aFqdBqCbokE^8!RN3@0(2ReJNox2I(Th zZ!#caiX5moydT>UcD2#}4CCR)5w3X-Klj3~(p6n{oXiPe9K}xglLL?b0cni6OLkFY zBO4_9Hi5aB>0)Z^{pbO=fa9pY;n<1_aQ!161YfdQ>sKYiKE@MhfXq0gzNt;-+16jQ z`MIOtfAf>E-+)q6>`eUIGqb%O@pHyNf z_@8?2(_7mjpMHmm12O$Tf?EDzA3XT>mHx*jcCvHDIbblDwcCGj8w~Z{sl2wyW7~PW zHVNv1Q`*cnu*;i+t(d$md3r&kKb*nf7P1qt`qNQ=mF9oA?DvNJzlX?XV1wg-h4^0r z=67@UCJ=wy-EgRX&$Hg*ab!sN-dJCuiBo>ME2}!g92dalrS=9tFYCu9R5TnKaZqf_ z&6_uOunVVZC7yNJBJDKs0Zl4l;tDeTR)}BK&$U@{MhLI~m3v=G(n;${OmyN&AHG(m z%#w~G2Nj~?@ZrNFNy(`K%e7Zh^qd-EPCsHNA9(P=`&7oe+1Yi9I;Yyw(3dVbne8B( zr}{!K+se>=+`OW&w!ch;<0^TFniD&@y1;<7^K;pF>!}T5kD{Q+p7v4Q!3;bj#*4H$e-8Np%WZX zPP&pJZBiA58QlS>%4lZ*IY#yi}{Wl$Sq{^&C5Q& z(D5m@X-nt(OawOiyGW!`Y&m<;W14+s_VKrfD2M|ydi_5&O> zZ#-yAUz(H}>nZkWq}-gUQiD%?)Z4^WJ|J6 zu1$AvaBz|~^T1$od4=|u`g)|pAt@>RS#@>wE3MBsWpdCPtB;%}}y3Im+6k|+nueo5mM;+ zFRzYUw@!UeFTUUp2YCrUBioU6v-!R5!rD?Z6LSy~6Vv?s{4P%EtUDI1p+fqtsfL6` z74B4n)w#LBs$%ZA*jQ6Dv$Uf5q;o;hJH0|4Jb17{q~eeU&MHOgKw$P1i%d1fS&Y^h zCTS--$5sq%z5)YdVX>y>W-M{Rtm;{cZsv{grrcgvv$;of81}&(lTE{sM=nRxd#%>g zEKzP>B=lpds&qI-O-espD}yMSFT<)`*nFv_bb?7A@B5rlQu11C^x3^TlkMnBH0&tw zZ+2DPt_D;g(z5inV{q@c+(Z2t=tE~AX0l*c8Xl%MXQ z?rA~(O@4QJB`$=iy? zyeIRQ6E3#x;+AumAF?>Cfm$1Dj2oWs&UITDf#`tf{KTGYSh!rlZY*xw)5RdNa;;3^ z6>)4Z0BO~hT8z1i7>>Y|A=XurucRE-h!(%y;eTGF_&xs;K^C^-HTK$Pv{v7)-8@W1 zMFrDgcH!JPw~f_Ad@l)kcLJ|(5$!he<{TQe(yzkA%*-=4Uw2~ApNh+MtVL&(j$1Ak zrZK$Tk>xd&UL_$ul4adC6KrfICMK5KCGNaDh25IeYgrr8Uo&yb(vX*fCluj z5Ylt4TC6&S_{&|H4Hv6 zbnRE-MZY9_cCO-PVq#)N%YuZ(jbq@Wt2v>O1|AdNZVpyyJPrs57~2o*3SqQ-WR>%b zl~wn6vxU>dx5Sq@q0;8nJAivc(6}myR(4?nO#}1JnA2qp7RXnUx0ARZ0XxP-0+V$5 z@6OxPyF&ha8prJ4Itg?rjng ztmBfdMRugLQ^$CG*qBf|{JChc$^WpLYQo*1z$7F*a@NyywP>LZ$%{GsI4j#436!#Q zj@|F+Q%>=rY6Z3g00?P=Qn7t-alCo`ZwC16;^aoJYak=ww17J;!|; zmaCo?3Naan%MHI%^EeV*ky>D7dBCR^ODfew;WE7lIa0lzWdUa(D{9)jefLgo`TL-D zlGe#=5wof9b5YVx&&GryaWnx#Jf4Zwz?1;JoNLGi$ndGTCqXdATg^_k$`YUOMf*F- zO4@WCij6-ZBs3lCCoGJ{H9XwGKGl&u`Npp$@$%>yd?_RZMbAr%HQ^GfUs%i$!H5L} zC9C>d)etVP#dL_=kViW6pLqh7C)=d>EL_65nrN73t`;m#EBfquSW=Bc;pOjdr_-4J zx-l0}Uoux2v9VG`(s@UPp6xrZFp4#*>v;ogw|ln)dMVi$*b`#wFuy zpv&F#?c2A;xT*&wo)A)Hg!zedv#K6Q3q*XcjAes2mux|fO?T4e_+4Q}?v2yAy^`pS z$!lFzdnHB+78gg%-DhPS$2jr>@-Pa!ckg~(EoOS1-$RVRD6a$> zTr>R;uQqo4o&G0%{P%P-ZdBd?#2+Y&xT&eB$jC?l{LoN`T7+bA-#sH)k7#|Me^JR! zEXf<<3I$m+aQ9tETwb#uGb;Sp)AX_(@H11z9{6bIj+`c~4tBWdpfKTjW zIy5 zYhOvc^cGgGiIB>TEY`+`(68a(-mC?Y*w7>uijenX<+8nwlB|OjJ}Tr!$k1BLL8GpT!1j(>u zI>(;4X1IYR5xz~dBs))iM=g#uM#;E9cJU?56vk^{fYOb*DsMm}Tovw3IZ)^02#E=e zQUx$XKN{GvyC_-l;!p6h4p>dK@R+AJn%3*NWMSMtlO9^MNYLE}oA zky8^{9BrrHz^*=5z!3S%E!4;@uh=KA##hMPtaLAQD%F`~4oSfkC z=a0DvfxNce_EirCs{(wQo--fzM~_CmJ?-)$x~B-@^`S^}wsmfd#6Zc)zz#Ht5G$YD z<;>i#H&-^ws<`LAQUkCxu0H&9+r_IKvdE>UUq@F5xlZ>W50Ep5xeqC zDgC|^qV@ybuaP_x05xX5jpfyM(m~qY&F{bdW zda9vtZ%%KG4@-3*kBSi|D{D)!M*umq$`Z_xtmM?5-vaudhKAfF z=#d7Djw#e?b-`g__-a8kjadbD-CUiYJA5NGv);xqv5AIcezWzc?2$zG7`jKY+T-9F z?8<;&Lzpc9)|Gy4S4C#M9JA$WJ(6^4Ot^ZIvoGQKsG?Qx1|QTKD*k!W!MDkAg6k

+1%$rmSPU3zr)^pKV^GcvBcrw*NiY=65U z=Hru%9JL4&s=S-cm7qT!di8;R+&No+?X4;>pl|t#ZBLQgaCHcU3QDD+SPPh@sN@b% z%#mPTTP#ruTuEpCWxQE-gO+?8*0mA2)X~py4~h%%Js!DRjjr+ccuB3txsr(J?S^l4 zGnrA(WyZ>Ub<&IsgDHD6`S=>8uAJYr|!eL8M}{HXtGUy zOAKlcK=2zt?^uh$B9d`M4EwO2Ln{yBSJq>B>#I8_#-{QPRM!}AFBf3E*z%awk;Fx2 zqISb55X?oeZ`LoN)2J>l+LD<8EkgwqW67kDY6M!%##F4e0^p3s%O-IgV*wyd)sw zp(FO}j)Q~4%@T!6moNKyML>ep&$+eBy5bbB9jBLN*?&8?J327%Qu%an#n0;0fD%#h ziPIz|7W5!});iRWzIj!YRJ*pKUugT`6UtoadPBm(*cZi29{S&DxUBi<;2g^s`S&P> z$<7z}Xkt$qlR_kYBEui$F{^@sfkA`FIW{p)sDwVfl+UwjF;y=QtY2Z4eRjeh#`_X; z>6P+*K{L;1Y? z14bZN$X%$5IVQ_IP$6m_*mu(5kmbcgyu7UT@xY^}bxwMo$aSSJ(#tMqlJn(JdB)Oq zgrEHh*GSq$TH39%XtCQJ-#)Y*CoqC|O^|oEb3fbsOT3wumX^&-|A6d-s8N|qMJ4&g z)^>tQcH68h8KX9*3W+<&#`%*YL{EE}3f&nuoD zRTTfjb#{<%I2}XI3+EpjdZKDr^vjizzM@8?a1FC5Fh2-!5e0a_tr5BuTWbA0*2b579SE-+jxN44Eb&|g zoqeMm`j_$*fni~}j^mxD+xEyfdvZw7bbH~;V}hz}+B0}1?sz?TMqHb;rCvKb0lHDl z48`WCf-A<`96)*>oiX0@NqX1k6g*bSfd$gV_~xRbaiOMsNyTRZs^%i zo5Uypw5~O1Lq}q!U^4gao?9RdK#tQE#DTU`nki*wW`^3%G0|v|coYIc_Kw=)MJai;+ExYZR+u&2Fs;7I6ldlNjo{94XuRi}9L{h=7 zj$#>^F+Diyh!bo3r5LetuPtrC!1Dt-$8R<*Lgp46x?G)Y)dGGtfI_Kk zP4DXXfkae5ckannTLP7CSVV&o3;1N|Q#d z-+pZ?DJi)OEh0i=K%M7Iz6GT$0q?_?pp&+PYS%v3<@S_f_UgOq>+5&wQy7dHab5Xe z<1sgsEeA)V+9bwsZ-5r$!^OPTvR?3@^`L(gBbZ~|hN_Fy2^AsrrR4UkuM7sb;OcrZ zEi|Aob#Rz|rxsykcRFpMP6i7_&9$O8_wL<8l2#J~Pp(64TNA!OZj6c;6}c`=gtWI8 z;%A6!SvJB{-G-~VK~eJ6t#4NOCWgO5&ASBUuF~I z4+|Ns;~ThdurV_?$13@OhcR$s0Of@XH_455*3Ttg1TOS?0Jw&^vH-dlh6Rz$Z@-?b zAOw;XJ++#*V{F`5N47pkGYh*7G}#Ww^{@7QyiXR2W%NiUM|S8ey3T&=)rfS(72--3 zzVKO#OGrrBtXsixK&5)hebF*RKzm?tFeu16{@uG)C@^j|zk_a(ZF`2fD>j7OqLz0P z^>9|k$a!^UTkGsGx}aV+qr$bS2!$0$WI23S=4%loG;F>{xi7vBpbN50^3;a+R zNZ}ph$+0uR?iuJ<=P4nN<*6fdX+t@kl|FL@sZAsOKHRj&KH6$&eS1JN2#hmfwUxZ! zb#wFSyrtF%DCXU*j4PpkuvU*QVQ(~qUdw8Ah#*ga-|B)f21#0qv7YWO#6#G9$D%RG zjAXC9So%oym8>=|pHQlX{=$Xu`CD)Bffx*Wp;qdKh04U?1s!9rVNvwEhy9Ym?W%7K7;)Du83>!*~v)S8)l2cSD17G7P(30fgi0^|igd9U|go z?9RT9Y>dI3+Lq2-+dId%Kn~fT5i0EEu`-)1Y1?C9>T$|*WfqDXxutJ!0#DUI6JfPN zBvr<9#SR)Ed=_{pVDuMX>*vliLz0Ekk1AD}d>3@473iuJ_Ox5#9XsIH-S}&m2(Aq` zzir1z*!nfp`dk%~IQ|$EifMbtNNvO;y1lQ$^GjapNn0$AVR}4pvb-ewF_Dt*vwinT z3qwKZd1dmQ6=X*_&lMMJq3u|MV3?2xQlapBky|s_`A-Ynq1hBaA=;K|D0BUD%<$JR zc6%A9%25@p_a$$4T=h>&iHX?{HJ5pq;VH<8llHu|4Rv8jv=n_E~QXG=*JqStFebaqX*rIB`9L|!cF z?1iodawGjZ9GTpTi7pCNV$DSkpNBc774xiup-?<#y5RiW!A59bKpQz`?@8493LoaZ zF7zaThoMD|J>NpX#=-&}4G&PEAYt}NJ6;{ER8!Ek21yFp^eTu88plDX`J+AOpPUQY z_*O#Ha9=)^3wDD(hK@*?%y&qJu$Fs;FRS1c&)MNHpktrn}(Ckt5p4hs&V}Sy90$<2?2G1rOw50LYi>j&xvgy1jD7fCxx} z$^(gCFJ8O|*q1P`UU)5n3C~BBdZQj5h0BvNKuC^dV=REyY2pp2$jYj!(UB3Z6L#c= zzOiTewU(q1TnWJj?gTEovXQT3irf~!$8edze>~d;Qc_@$HPq)z5t*8rns!k4LXied zhmoP7>lV0<4D$sbLuk(VElswC-=HA_%=}V1688SB+{G#LGo{=`&a7%fdBqJ>(3VaX$WlPFOB{+~S>?yw^ z{ja$T1Tp*H@E!T0ip-b4H-y?m5nR2w!e5-pe{bGR1OE3MBWpd0>Y!;Slc+SqYuGR1 NoRa#PR0WfJ{{=t;2_FCe literal 0 HcmV?d00001 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() From c2fcc349a054a486694612d38a4b097b330d2e61 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:07:04 +0800 Subject: [PATCH 175/193] docs: complete release compliance and distribution notes --- .github/workflows/release.yml | 2 + README.md | 8 ++- THIRD_PARTY_NOTICES.md | 14 ++--- .../assets/licenses/OpenCC-Apache-2.0.txt | 56 +++++++++++++++++++ .../main/assets/licenses/fluent-emoji-MIT.txt | 21 +++++++ .../assets/licenses/librime-BSD-3-Clause.txt | 30 ++++++++++ .../licenses/sherpa-onnx-Apache-2.0.txt | 56 +++++++++++++++++++ .../assets/licenses/snappy-BSD-3-Clause.txt | 54 ++++++++++++++++++ .../licenses/zipformer-model-Apache-2.0.txt | 56 +++++++++++++++++++ docs/LICENSING.md | 7 +++ docs/README.md | 1 + docs/RELEASE.md | 52 +++++++++++++++++ 12 files changed, 347 insertions(+), 10 deletions(-) create mode 100644 app/src/main/assets/licenses/OpenCC-Apache-2.0.txt create mode 100644 app/src/main/assets/licenses/fluent-emoji-MIT.txt create mode 100644 app/src/main/assets/licenses/librime-BSD-3-Clause.txt create mode 100644 app/src/main/assets/licenses/sherpa-onnx-Apache-2.0.txt create mode 100644 app/src/main/assets/licenses/snappy-BSD-3-Clause.txt create mode 100644 app/src/main/assets/licenses/zipformer-model-Apache-2.0.txt create mode 100644 docs/RELEASE.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b96165a0..5575bf61 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -140,6 +140,7 @@ jobs: 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" @@ -172,6 +173,7 @@ jobs: 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/README.md b/README.md index bb8cc1d2..35d8ee71 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,14 @@ openIME 是独立 Android 系统输入法,包名 `llc.slacker.openime`。拼 当前正式发布包只包含 `arm64-v8a`;开发用 Debug APK 仍保留 `x86_64`,用于模拟器回归。 -## 安装三步 +## 安装四步 1. **启用**:安装 APK 后打开 openIME,进入系统输入法设置并启用 `openIME`。 2. **切换**:回到引导页,打开系统输入法选择器并切换到 `openIME`。 -3. **授权(可选)**:需要本地语音输入时授权麦克风;不授权也可以正常打字。 +3. **授权(可选)**:需要本地语音输入时授权麦克风;不授权可直接跳过。 +4. **试打**:在引导页输入框确认键盘、候选和上屏链路正常。 -引导页还提供“试打”输入框。语音输入长按空格达到系统长按阈值后开始录音,松手后识别并上屏。 +语音输入长按空格达到系统长按阈值后开始录音,松手后识别并上屏。 ## 截图 @@ -169,6 +170,7 @@ bash scripts/verify_linux.sh emulator-5554 - [适配与坐标规范](docs/COORDINATE_SYSTEM.md) - [测试流程 SOP](docs/TEST_SOP.md) - [脚本说明](scripts/README.md) +- [发布流程](docs/RELEASE.md) ## 隐私与安全 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 1811aefb..44e5913e 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -4,19 +4,19 @@ | 组件 | 仓库内位置 | 上游/来源 | 许可证 | 分发核对 | |---|---|---|---|---| -| librime | `app/src/main/cpp/vendor/librime/` | https://github.com/rime/librime | BSD-3-Clause | 已核对;许可证全文保留在 vendored 目录 `LICENSE` | -| OpenCC | `app/src/main/cpp/vendor/OpenCC/` | https://github.com/BYVoid/OpenCC | Apache-2.0 | 已核对;许可证全文保留在 vendored 目录 `LICENSE` | -| Snappy | `app/src/main/cpp/vendor/snappy/` | https://github.com/google/snappy | BSD-3-Clause | 已核对;版权与许可保留在 `COPYING` | +| 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 | 已核对上游 runtime 许可证;发布时保留本清单及上游归属 | -| 中英双语 Zipformer 语音模型 | `app/src/main/assets/models/voice/bilingual-zipformer/` | sherpa-onnx 模型 `sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20`;原模型 https://huggingface.co/csukuangfj/k2fsa-zipformer-chinese-english-mixed | Apache-2.0 | 已核对模型卡许可证;`manifest.json` 固定模型 ID/版本与文件哈希 | -| Microsoft Fluent Emoji | `app/src/main/assets/emoji/fluent/` | https://github.com/microsoft/fluentui-emoji | MIT | 仅打包当前实际使用的基础情绪表情资源 | +| 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` | +| 中英双语 Zipformer 语音模型 | `app/src/main/assets/models/voice/bilingual-zipformer/` | sherpa-onnx 模型 `sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20`;原模型 https://huggingface.co/csukuangfj/k2fsa-zipformer-chinese-english-mixed | Apache-2.0 | APK 包含 `assets/licenses/zipformer-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` | ## 发布前检查 - 不要删除 vendored 源码目录中的 LICENSE、COPYING、AUTHORS 或 NOTICE 类文件。 -- `app/src/main/assets/licenses/rime-ice-GPL-3.0.txt` 必须继续随发布资产保留。 +- `app/src/main/assets/licenses/` 下的第三方许可文本必须继续随 APK 打包,包括 Rime Ice、librime、OpenCC、Snappy、sherpa-onnx、Zipformer 模型和 Fluent Emoji。 - 语音 runtime 与模型升级时,重新核对**具体版本/模型**的许可证,不要只沿用本文件旧结论。 - 内置词库来源或固定提交变化时,同步更新本文件、`docs/LICENSING.md` 和 APK 内许可证文件。 - 主项目 `LICENSE` 在项目所有者决定前保持缺失;README 对主项目许可状态的现有表述保持不变。 diff --git a/app/src/main/assets/licenses/OpenCC-Apache-2.0.txt b/app/src/main/assets/licenses/OpenCC-Apache-2.0.txt new file mode 100644 index 00000000..9f7a310e --- /dev/null +++ b/app/src/main/assets/licenses/OpenCC-Apache-2.0.txt @@ -0,0 +1,56 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and + + 2. You must cause any modified files to carry prominent notices stating that You changed the files; and + + 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + diff --git a/app/src/main/assets/licenses/fluent-emoji-MIT.txt b/app/src/main/assets/licenses/fluent-emoji-MIT.txt new file mode 100644 index 00000000..9e841e7a --- /dev/null +++ b/app/src/main/assets/licenses/fluent-emoji-MIT.txt @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/app/src/main/assets/licenses/librime-BSD-3-Clause.txt b/app/src/main/assets/licenses/librime-BSD-3-Clause.txt new file mode 100644 index 00000000..f05b7c29 --- /dev/null +++ b/app/src/main/assets/licenses/librime-BSD-3-Clause.txt @@ -0,0 +1,30 @@ +Copyright (c) 2014, RIME Developers +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/app/src/main/assets/licenses/sherpa-onnx-Apache-2.0.txt b/app/src/main/assets/licenses/sherpa-onnx-Apache-2.0.txt new file mode 100644 index 00000000..9f7a310e --- /dev/null +++ b/app/src/main/assets/licenses/sherpa-onnx-Apache-2.0.txt @@ -0,0 +1,56 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and + + 2. You must cause any modified files to carry prominent notices stating that You changed the files; and + + 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + diff --git a/app/src/main/assets/licenses/snappy-BSD-3-Clause.txt b/app/src/main/assets/licenses/snappy-BSD-3-Clause.txt new file mode 100644 index 00000000..bd0e5971 --- /dev/null +++ b/app/src/main/assets/licenses/snappy-BSD-3-Clause.txt @@ -0,0 +1,54 @@ +Copyright 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=== + +Some of the benchmark data in testdata/ is licensed differently: + + - fireworks.jpeg is Copyright 2013 Steinar H. Gunderson, and + is licensed under the Creative Commons Attribution 3.0 license + (CC-BY-3.0). See https://creativecommons.org/licenses/by/3.0/ + for more information. + + - kppkn.gtb is taken from the Gaviota chess tablebase set, and + is licensed under the MIT License. See + https://sites.google.com/site/gaviotachessengine/Home/endgame-tablebases-1 + for more information. + + - paper-100k.pdf is an excerpt (bytes 92160 to 194560) from the paper + “Combinatorial Modeling of Chromatin Features Quantitatively Predicts DNA + Replication Timing in _Drosophila_” by Federico Comoglio and Renato Paro, + which is licensed under the CC-BY license. See + http://www.ploscompbiol.org/static/license for more ifnormation. + + - alice29.txt, asyoulik.txt, plrabn12.txt and lcet10.txt are from Project + Gutenberg. The first three have expired copyrights and are in the public + domain; the latter does not have expired copyright, but is still in the + public domain according to the license information + (http://www.gutenberg.org/ebooks/53). diff --git a/app/src/main/assets/licenses/zipformer-model-Apache-2.0.txt b/app/src/main/assets/licenses/zipformer-model-Apache-2.0.txt new file mode 100644 index 00000000..9f7a310e --- /dev/null +++ b/app/src/main/assets/licenses/zipformer-model-Apache-2.0.txt @@ -0,0 +1,56 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and + + 2. You must cause any modified files to carry prominent notices stating that You changed the files; and + + 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + diff --git a/docs/LICENSING.md b/docs/LICENSING.md index 0e750420..be8d5a7d 100644 --- a/docs/LICENSING.md +++ b/docs/LICENSING.md @@ -30,3 +30,10 @@ `csukuangfj/k2fsa-zipformer-chinese-english-mixed` 模型卡标记为 Apache-2.0。 具体来源、文件位置和发布核对项统一记录在根目录 `THIRD_PARTY_NOTICES.md`。 Git LFS 只负责文件存储,不改变文件的许可证。 + + +## APK 内许可证 + +正式 APK 在 `assets/licenses/` 内携带主要第三方许可证文本,包括 librime、OpenCC、Snappy、Rime Ice、sherpa-onnx runtime、当前 Zipformer 模型和 Fluent Emoji。根目录 `THIRD_PARTY_NOTICES.md` 记录组件、来源、版本或固定提交与对应文件位置。 + +发布工作流同时把 `THIRD_PARTY_NOTICES.md` 作为 GitHub Release 附件上传,便于在 APK 外直接查看。 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..eb73c818 --- /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.svg` 作为 1280×640 源文件。GitHub 的仓库 Social preview 不是源码文件配置项,因此需要仓库管理员在 **Settings → General → Social preview** 上传由该 SVG 导出的 PNG/JPEG;这一步不能通过当前仓库代码提交自动完成。 + +## 发布前检查 + +- `AndroidManifest.xml` 不含 `INTERNET` 权限。 +- `android:allowBackup="false"` 保持不变。 +- `THIRD_PARTY_NOTICES.md` 与 `app/src/main/assets/licenses/` 同步。 +- 语音模型、词库或第三方 runtime 版本变化时重新核对对应许可证。 +- 不从 Debug keystore 发布。 From c94ff2175fe0cec8593b428b8c0439b8e62b40d1 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:08:58 +0800 Subject: [PATCH 176/193] docs: fix social preview release path --- docs/RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/RELEASE.md b/docs/RELEASE.md index eb73c818..ab0cb24e 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -41,7 +41,7 @@ keystore 不进入仓库,不提交到 Issue、PR、Actions artifact 或 Releas ## 社交预览 -仓库内提供 `docs/images/social-preview.svg` 作为 1280×640 源文件。GitHub 的仓库 Social preview 不是源码文件配置项,因此需要仓库管理员在 **Settings → General → Social preview** 上传由该 SVG 导出的 PNG/JPEG;这一步不能通过当前仓库代码提交自动完成。 +仓库内提供 `docs/images/social-preview.png`(1280×640),由 `scripts/generate_brand_assets.py` 生成。GitHub 的仓库 Social preview 不是源码文件配置项,因此需要仓库管理员在 **Settings → General → Social preview** 上传该 PNG;这一步不能通过当前仓库代码提交自动完成。 ## 发布前检查 From 6524fbe6ddb4ae9cc0fc2b49fdb011c820f7451d Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:10:23 +0800 Subject: [PATCH 177/193] fix: marshal imported settings refresh to main thread --- .../main/java/llc/slacker/openime/LocalVoiceImeService.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt index 61477399..fc27254a 100644 --- a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt +++ b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt @@ -292,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) { From 4186d5fc536e6179e4c9afbe87c3f221c3dbc076 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:11:37 +0800 Subject: [PATCH 178/193] fix: validate imported Rime dictionary names --- .../main/java/llc/slacker/openime/UserDataArchive.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/llc/slacker/openime/UserDataArchive.kt b/app/src/main/java/llc/slacker/openime/UserDataArchive.kt index 1f3b3849..5ee98f4c 100644 --- a/app/src/main/java/llc/slacker/openime/UserDataArchive.kt +++ b/app/src/main/java/llc/slacker/openime/UserDataArchive.kt @@ -153,10 +153,18 @@ internal object UserDataArchiveCodec { name = item.optString("name", "").trim(), content = item.optString("content", ""), ) - }.filter { it.name.isNotEmpty() && it.content.isNotEmpty() }, + }.filter { + isSafeRimeDictionaryName(it.name) && it.content.isNotEmpty() + }, ) } + 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) From a597a5589ac15f6eff566c64d1205d50a86ad959 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:11:41 +0800 Subject: [PATCH 179/193] fix: reject unsafe native user dictionary names --- app/src/main/cpp/local_rime_jni.cc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/cpp/local_rime_jni.cc b/app/src/main/cpp/local_rime_jni.cc index 83164de4..0a979d3f 100644 --- a/app/src/main/cpp/local_rime_jni.cc +++ b/app/src/main/cpp/local_rime_jni.cc @@ -259,6 +259,12 @@ 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) { @@ -451,7 +457,7 @@ Java_llc_slacker_openime_RimeNative_nativeImportUserDictionary( const std::string name = jstring_to_utf8(env, dict_name); const std::string source = jstring_to_utf8(env, source_file); - if (name.empty() || source.empty()) return -1; + if (!safe_user_dict_name(name) || source.empty()) return -1; const std::string schema = suspend_session_locked(); int imported = -1; From 4db4546d755e0db65f122405b755481f9c35a705 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:11:45 +0800 Subject: [PATCH 180/193] test: cover unsafe Rime dictionary names --- .../slacker/openime/UserDataArchiveTest.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/src/test/java/llc/slacker/openime/UserDataArchiveTest.kt b/app/src/test/java/llc/slacker/openime/UserDataArchiveTest.kt index a7d86e22..f12f8be8 100644 --- a/app/src/test/java/llc/slacker/openime/UserDataArchiveTest.kt +++ b/app/src/test/java/llc/slacker/openime/UserDataArchiveTest.kt @@ -47,6 +47,31 @@ class UserDataArchiveTest { assertEquals(archive, decoded) } + @Test + fun decodeDropsRimeDictionaryNamesThatCanEscapeTheUserDataDirectory() { + val archive = UserDataArchive( + quickPhrases = emptyList(), + customSymbols = emptyList(), + userPhrases = emptyList(), + settings = settings, + rimeUserDictionaries = listOf( + RimeUserDictionaryArchive("luna_pinyin", "safe"), + 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( From 3c4aa4c11a25aab4dd05bc39b0bce9c7d035e237 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:15:59 +0800 Subject: [PATCH 181/193] fix: make Rime data archive fail closed --- app/src/main/cpp/local_rime_jni.cc | 16 +++++--- .../llc/slacker/openime/AboutDataActivity.kt | 9 ++++- .../slacker/openime/LocalVoiceImeService.kt | 2 +- .../java/llc/slacker/openime/RimeEngine.kt | 37 ++++++++++++------- .../llc/slacker/openime/UserDataArchive.kt | 2 +- .../slacker/openime/UserDataArchiveTest.kt | 1 + 6 files changed, 44 insertions(+), 23 deletions(-) diff --git a/app/src/main/cpp/local_rime_jni.cc b/app/src/main/cpp/local_rime_jni.cc index 0a979d3f..e4c6f817 100644 --- a/app/src/main/cpp/local_rime_jni.cc +++ b/app/src/main/cpp/local_rime_jni.cc @@ -420,13 +420,13 @@ 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 make_strings(env, {}); + if (!g_api || !g_session) return nullptr; const std::string target = jstring_to_utf8(env, target_dir); - if (target.empty()) return make_strings(env, {}); + if (target.empty()) return nullptr; std::error_code error; std::filesystem::create_directories(target, error); - if (error) return make_strings(env, {}); + if (error) return nullptr; const std::string schema = suspend_session_locked(); std::vector exported; @@ -435,17 +435,21 @@ Java_llc_slacker_openime_RimeNative_nativeExportUserDictionaries( 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) { - exported.emplace_back(name + "\t" + path.string()); + 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; } - resume_session_locked(schema); return make_strings(env, exported); } diff --git a/app/src/main/java/llc/slacker/openime/AboutDataActivity.kt b/app/src/main/java/llc/slacker/openime/AboutDataActivity.kt index 2f7882fb..9f6bd606 100644 --- a/app/src/main/java/llc/slacker/openime/AboutDataActivity.kt +++ b/app/src/main/java/llc/slacker/openime/AboutDataActivity.kt @@ -160,6 +160,13 @@ class AboutDataActivity : Activity() { 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 { @@ -287,7 +294,7 @@ class AboutDataActivity : Activity() { if (imported == null) { sourceDir.deleteRecursively() runOnUiThread { - showError("Rime 自动学习词库导入失败,其他数据未修改。") + showError("Rime 自动学习词库未全部导入;常用语、符号和设置未修改,已成功写入的 Rime 词条可能保留。") } return@importRimeUserData } diff --git a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt index fc27254a..bce26bd4 100644 --- a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt +++ b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt @@ -1506,7 +1506,7 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can internal fun exportRimeUserData( targetDir: File, - onComplete: (List) -> Unit, + onComplete: (List?) -> Unit, ): Boolean = ::rime.isInitialized && rime.exportUserDictionaries(targetDir, onComplete) diff --git a/app/src/main/java/llc/slacker/openime/RimeEngine.kt b/app/src/main/java/llc/slacker/openime/RimeEngine.kt index b95c3598..6f53f236 100644 --- a/app/src/main/java/llc/slacker/openime/RimeEngine.kt +++ b/app/src/main/java/llc/slacker/openime/RimeEngine.kt @@ -355,37 +355,46 @@ class RimeEngine( fun exportUserDictionaries( targetDir: File, - onComplete: (List) -> Unit, + onComplete: (List?) -> Unit, ): Boolean { if (!isReady) return false return mutationQueue.submit { val exported = synchronized(lock) { if (!isReady) { - emptyList() + null } else { runCatching { - if (!syncSchemaFromSettingsLocked()) return@runCatching emptyList() - targetDir.mkdirs() - RimeNative.nativeExportUserDictionaries(targetDir.absolutePath) - .orEmpty() - .mapNotNull { mapping -> + if (!syncSchemaFromSettingsLocked()) return@runCatching null + if (!targetDir.exists() && !targetDir.mkdirs()) { + return@runCatching null + } + val mappings = + RimeNative.nativeExportUserDictionaries(targetDir.absolutePath) + ?: return@runCatching null + buildList { + mappings.forEach { mapping -> val separator = mapping.indexOf('\t') if (separator <= 0 || separator >= mapping.lastIndex) { - return@mapNotNull null + return@runCatching null } val name = mapping.substring(0, separator).trim() val path = mapping.substring(separator + 1) val file = File(path) - if (name.isEmpty() || !file.isFile) { - null - } else { + if ( + !UserDataArchiveCodec.isSafeRimeDictionaryName(name) || + !file.isFile + ) { + return@runCatching null + } + add( RimeUserDictionaryArchive( name = name, content = file.readText(), - ) - } + ), + ) } - }.getOrDefault(emptyList()) + } + }.getOrNull() } } onComplete(exported) diff --git a/app/src/main/java/llc/slacker/openime/UserDataArchive.kt b/app/src/main/java/llc/slacker/openime/UserDataArchive.kt index 5ee98f4c..3b1bb6f3 100644 --- a/app/src/main/java/llc/slacker/openime/UserDataArchive.kt +++ b/app/src/main/java/llc/slacker/openime/UserDataArchive.kt @@ -155,7 +155,7 @@ internal object UserDataArchiveCodec { ) }.filter { isSafeRimeDictionaryName(it.name) && it.content.isNotEmpty() - }, + }.distinctBy { it.name }, ) } diff --git a/app/src/test/java/llc/slacker/openime/UserDataArchiveTest.kt b/app/src/test/java/llc/slacker/openime/UserDataArchiveTest.kt index f12f8be8..48eeee74 100644 --- a/app/src/test/java/llc/slacker/openime/UserDataArchiveTest.kt +++ b/app/src/test/java/llc/slacker/openime/UserDataArchiveTest.kt @@ -56,6 +56,7 @@ class UserDataArchiveTest { settings = settings, rimeUserDictionaries = listOf( RimeUserDictionaryArchive("luna_pinyin", "safe"), + RimeUserDictionaryArchive("luna_pinyin", "duplicate"), RimeUserDictionaryArchive("../outside", "unsafe"), RimeUserDictionaryArchive("nested/path", "unsafe"), RimeUserDictionaryArchive("nested\\path", "unsafe"), From d09325e3c6cf59d8702eaf8df2d941ab38e15b5e Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:53:12 +0800 Subject: [PATCH 182/193] feat: switch local voice runtime to INT8 streaming Paraformer --- .github/workflows/migrate-voice-model.yml | 60 +++++++ THIRD_PARTY_NOTICES.md | 4 +- ....0.txt => paraformer-model-Apache-2.0.txt} | 0 .../openime/VoiceRecognitionBackend.kt | 32 ++-- .../openime/VoiceRuntimeModelSource.kt | 14 +- .../openime/VoiceModelTrustPolicyTest.kt | 33 ++-- .../openime/VoiceRuntimeModelSourceTest.kt | 7 +- docs/DECISIONS.md | 1 + docs/LICENSING.md | 8 +- docs/LOCAL_VOICE_MODEL.md | 21 ++- scripts/migrate_voice_model.py | 160 ++++++++++++++++++ 11 files changed, 273 insertions(+), 67 deletions(-) create mode 100644 .github/workflows/migrate-voice-model.yml rename app/src/main/assets/licenses/{zipformer-model-Apache-2.0.txt => paraformer-model-Apache-2.0.txt} (100%) create mode 100755 scripts/migrate_voice_model.py diff --git a/.github/workflows/migrate-voice-model.yml b/.github/workflows/migrate-voice-model.yml new file mode 100644 index 00000000..34b897e6 --- /dev/null +++ b/.github/workflows/migrate-voice-model.yml @@ -0,0 +1,60 @@ +name: One-shot voice model migration + +on: + push: + branches: + - feat/visual-language-v1 + paths: + - .github/workflows/migrate-voice-model.yml + +permissions: + contents: write + +jobs: + migrate: + name: Commit INT8 Paraformer through Git LFS + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout branch without smudging old LFS model + uses: actions/checkout@v7 + with: + fetch-depth: 0 + lfs: false + + - name: Enable Git LFS + shell: bash + run: git lfs install --local + + - name: Download official sherpa-onnx model package + shell: bash + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/paraformer" + curl --fail --location --retry 4 --retry-all-errors \ + https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-paraformer-bilingual-zh-en.tar.bz2 \ + --output "$RUNNER_TEMP/paraformer/model.tar.bz2" + tar -xjf "$RUNNER_TEMP/paraformer/model.tar.bz2" \ + -C "$RUNNER_TEMP/paraformer" + + - name: Verify and prepare INT8 model + shell: bash + run: | + python3 scripts/migrate_voice_model.py \ + "$RUNNER_TEMP/paraformer/sherpa-onnx-streaming-paraformer-bilingual-zh-en" + + - name: Commit model and generated trust metadata + shell: bash + run: | + set -euo pipefail + git config user.name "openIME release bot" + git config user.email "128580527+Slacker-LLC@users.noreply.github.com" + git add -A app/src/main/assets/models/voice + git lfs ls-files + if git diff --cached --quiet; then + echo "Voice model is already migrated." + exit 0 + fi + git commit -m "chore: replace voice model with INT8 Paraformer" + git push origin "HEAD:${GITHUB_REF_NAME}" diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 44e5913e..09dba908 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -10,13 +10,13 @@ | 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` | -| 中英双语 Zipformer 语音模型 | `app/src/main/assets/models/voice/bilingual-zipformer/` | sherpa-onnx 模型 `sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20`;原模型 https://huggingface.co/csukuangfj/k2fsa-zipformer-chinese-english-mixed | Apache-2.0 | APK 包含 `assets/licenses/zipformer-model-Apache-2.0.txt`;`manifest.json` 固定模型 ID/版本与文件哈希 | +| 中英双语 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` | ## 发布前检查 - 不要删除 vendored 源码目录中的 LICENSE、COPYING、AUTHORS 或 NOTICE 类文件。 -- `app/src/main/assets/licenses/` 下的第三方许可文本必须继续随 APK 打包,包括 Rime Ice、librime、OpenCC、Snappy、sherpa-onnx、Zipformer 模型和 Fluent Emoji。 +- `app/src/main/assets/licenses/` 下的第三方许可文本必须继续随 APK 打包,包括 Rime Ice、librime、OpenCC、Snappy、sherpa-onnx、Paraformer 模型和 Fluent Emoji。 - 语音 runtime 与模型升级时,重新核对**具体版本/模型**的许可证,不要只沿用本文件旧结论。 - 内置词库来源或固定提交变化时,同步更新本文件、`docs/LICENSING.md` 和 APK 内许可证文件。 - 主项目 `LICENSE` 在项目所有者决定前保持缺失;README 对主项目许可状态的现有表述保持不变。 diff --git a/app/src/main/assets/licenses/zipformer-model-Apache-2.0.txt b/app/src/main/assets/licenses/paraformer-model-Apache-2.0.txt similarity index 100% rename from app/src/main/assets/licenses/zipformer-model-Apache-2.0.txt rename to app/src/main/assets/licenses/paraformer-model-Apache-2.0.txt diff --git a/app/src/main/java/llc/slacker/openime/VoiceRecognitionBackend.kt b/app/src/main/java/llc/slacker/openime/VoiceRecognitionBackend.kt index d0bd386e..a6b71cf3 100644 --- a/app/src/main/java/llc/slacker/openime/VoiceRecognitionBackend.kt +++ b/app/src/main/java/llc/slacker/openime/VoiceRecognitionBackend.kt @@ -4,10 +4,10 @@ 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 @@ -157,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) @@ -165,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. */ @@ -175,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() @@ -189,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() { @@ -257,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 @@ -286,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, @@ -323,6 +318,9 @@ 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 recognizerFinal = currentRecognizer.getResult(currentStream).text.trim() 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/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 index 932e1dc9..5d9ba25f 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -8,3 +8,4 @@ - 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 be8d5a7d..5a73bb59 100644 --- a/docs/LICENSING.md +++ b/docs/LICENSING.md @@ -25,15 +25,15 @@ `THIRD_PARTY_NOTICES.md`。 语音 runtime 以 `app/libs/sherpa-onnx-1.13.6.aar` 提供,上游 -`k2-fsa/sherpa-onnx` 使用 Apache-2.0。内置中英双语 Zipformer 模型 -`sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20` 的原模型 -`csukuangfj/k2fsa-zipformer-chinese-english-mixed` 模型卡标记为 Apache-2.0。 +`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、当前 Zipformer 模型和 Fluent Emoji。根目录 `THIRD_PARTY_NOTICES.md` 记录组件、来源、版本或固定提交与对应文件位置。 +正式 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/scripts/migrate_voice_model.py b/scripts/migrate_voice_model.py new file mode 100755 index 00000000..d5840ce1 --- /dev/null +++ b/scripts/migrate_voice_model.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Install the pinned INT8 streaming Paraformer model into openIME assets.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import shutil +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +ASSETS = ROOT / "app" / "src" / "main" / "assets" +VOICE_ROOT = ASSETS / "models" / "voice" +TARGET_ROOT = VOICE_ROOT / "bilingual-paraformer" +OLD_ROOT = VOICE_ROOT / "bilingual-zipformer" + +MODEL_ID = "sherpa-onnx-streaming-paraformer-bilingual-zh-en-int8" +MODEL_VERSION = "hf-8e40c43-int8" +MODEL_TYPE = "paraformer" +ENGINE_VERSION = "sherpa-onnx-v1.13.6" +LANGUAGE = "zh-CN,en-US" +REQUIRED_MEMORY = 520_000_000 + +EXPECTED_SHA256 = { + "encoder.int8.onnx": "81a70226a8934e6ed92aa1d4fc486b428b5398e2f2619ed4897b7294cab90e9a", + "decoder.int8.onnx": "f3cca9f77bb9d93c8fcbfb63ae617b6b1ee96818df3aa3b151c40658fe38594f", +} + +TARGET_FILES = [ + "models/voice/bilingual-paraformer/decoder.int8.onnx", + "models/voice/bilingual-paraformer/encoder.int8.onnx", + "models/voice/bilingual-paraformer/tokens.txt", +] + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def update_framed(digest, value: str) -> None: + data = value.encode("utf-8") + digest.update(str(len(data)).encode("ascii")) + digest.update(b":") + digest.update(data) + + +def manifest_fingerprint(manifest: dict[str, object]) -> str: + digest = hashlib.sha256() + digest.update(b"openime-voice-manifest-v1") + values = [ + str(manifest["modelId"]), + str(manifest["modelVersion"]), + str(manifest["language"]), + str(manifest["modelType"]), + str(manifest["engineVersion"]), + str(manifest["fileHash"]), + "1" if manifest["supportsPunctuation"] else "0", + str(manifest["requiredMemory"]), + str(len(manifest["files"])), + ] + for value in values: + update_framed(digest, value) + for path in manifest["files"]: + update_framed(digest, str(path)) + return digest.hexdigest() + + +def verify_source(source: Path) -> None: + for filename, expected in EXPECTED_SHA256.items(): + path = source / filename + if not path.is_file(): + raise SystemExit(f"missing upstream model file: {path}") + actual = sha256_file(path) + if actual != expected: + raise SystemExit( + f"{filename} SHA-256 mismatch: expected {expected}, got {actual}" + ) + + tokens = source / "tokens.txt" + if not tokens.is_file(): + raise SystemExit(f"missing upstream token file: {tokens}") + lines = tokens.read_text(encoding="utf-8").splitlines() + if len(lines) != 8404: + raise SystemExit(f"unexpected tokens.txt line count: {len(lines)}") + if lines[:3] != [" 0", " 1", " 2"]: + raise SystemExit("unexpected tokens.txt header") + + +def install(source: Path) -> None: + verify_source(source) + + if TARGET_ROOT.exists(): + shutil.rmtree(TARGET_ROOT) + TARGET_ROOT.mkdir(parents=True) + + for filename in ("decoder.int8.onnx", "encoder.int8.onnx", "tokens.txt"): + shutil.copy2(source / filename, TARGET_ROOT / filename) + + if OLD_ROOT.exists(): + shutil.rmtree(OLD_ROOT) + + aggregate = hashlib.sha256() + for relative in sorted(TARGET_FILES): + path = ASSETS / relative + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + aggregate.update(chunk) + + manifest = { + "modelId": MODEL_ID, + "modelVersion": MODEL_VERSION, + "language": LANGUAGE, + "modelType": MODEL_TYPE, + "engineVersion": ENGINE_VERSION, + "fileHash": aggregate.hexdigest(), + "supportsPunctuation": False, + "requiredMemory": REQUIRED_MEMORY, + "files": TARGET_FILES, + } + + (VOICE_ROOT / "manifest.json").write_text( + json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + + sizes = [str((ASSETS / relative).stat().st_size) for relative in TARGET_FILES] + fingerprint = manifest_fingerprint(manifest) + catalog = ( + "# openIME trusted downloaded voice model catalog v1\n" + "# publishermanifest_fingerprint_sha256file_sizes_in_manifest_order\n" + f"Slacker-LLC\t{fingerprint}\t{','.join(sizes)}\n" + ) + (VOICE_ROOT / "trusted-downloads.tsv").write_text(catalog, encoding="utf-8") + + print(json.dumps(manifest, ensure_ascii=False, indent=2)) + print(f"manifest fingerprint: {fingerprint}") + print(f"trusted sizes: {','.join(sizes)}") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "source", + type=Path, + help="Extracted sherpa-onnx-streaming-paraformer-bilingual-zh-en directory", + ) + args = parser.parse_args() + source = args.source.resolve() + if not source.is_dir(): + raise SystemExit(f"source directory does not exist: {source}") + install(source) + + +if __name__ == "__main__": + main() From ef1db2daf89e2b919011cfb8040a7a6d6d2c3dcb Mon Sep 17 00:00:00 2001 From: openIME release bot <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 04:54:45 +0000 Subject: [PATCH 183/193] chore: replace voice model with INT8 Paraformer --- .../bilingual-paraformer/decoder.int8.onnx | 3 + .../bilingual-paraformer/encoder.int8.onnx | 3 + .../voice/bilingual-paraformer/tokens.txt | 8404 +++++++++++++++++ .../decoder-epoch-99-avg-1.onnx | 3 - .../encoder-epoch-99-avg-1.int8.onnx | 3 - .../joiner-epoch-99-avg-1.int8.onnx | 3 - .../voice/bilingual-zipformer/tokens.txt | 6257 ------------ .../main/assets/models/voice/manifest.json | 17 +- .../assets/models/voice/trusted-downloads.tsv | 2 +- 9 files changed, 8419 insertions(+), 6276 deletions(-) create mode 100644 app/src/main/assets/models/voice/bilingual-paraformer/decoder.int8.onnx create mode 100644 app/src/main/assets/models/voice/bilingual-paraformer/encoder.int8.onnx create mode 100644 app/src/main/assets/models/voice/bilingual-paraformer/tokens.txt delete mode 100644 app/src/main/assets/models/voice/bilingual-zipformer/decoder-epoch-99-avg-1.onnx delete mode 100644 app/src/main/assets/models/voice/bilingual-zipformer/encoder-epoch-99-avg-1.int8.onnx delete mode 100644 app/src/main/assets/models/voice/bilingual-zipformer/joiner-epoch-99-avg-1.int8.onnx delete mode 100644 app/src/main/assets/models/voice/bilingual-zipformer/tokens.txt diff --git a/app/src/main/assets/models/voice/bilingual-paraformer/decoder.int8.onnx b/app/src/main/assets/models/voice/bilingual-paraformer/decoder.int8.onnx new file mode 100644 index 00000000..18c36b66 --- /dev/null +++ b/app/src/main/assets/models/voice/bilingual-paraformer/decoder.int8.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3cca9f77bb9d93c8fcbfb63ae617b6b1ee96818df3aa3b151c40658fe38594f +size 71664561 diff --git a/app/src/main/assets/models/voice/bilingual-paraformer/encoder.int8.onnx b/app/src/main/assets/models/voice/bilingual-paraformer/encoder.int8.onnx new file mode 100644 index 00000000..b5b21f16 --- /dev/null +++ b/app/src/main/assets/models/voice/bilingual-paraformer/encoder.int8.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81a70226a8934e6ed92aa1d4fc486b428b5398e2f2619ed4897b7294cab90e9a +size 165462184 diff --git a/app/src/main/assets/models/voice/bilingual-paraformer/tokens.txt b/app/src/main/assets/models/voice/bilingual-paraformer/tokens.txt new file mode 100644 index 00000000..57bc045d --- /dev/null +++ b/app/src/main/assets/models/voice/bilingual-paraformer/tokens.txt @@ -0,0 +1,8404 @@ + 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 From 33e8bfaa2db915fd2f9ae29ecbbf590315303bc5 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:56:08 +0800 Subject: [PATCH 184/193] chore: remove completed voice model migration workflow --- .github/workflows/migrate-voice-model.yml | 60 ----------------------- 1 file changed, 60 deletions(-) delete mode 100644 .github/workflows/migrate-voice-model.yml diff --git a/.github/workflows/migrate-voice-model.yml b/.github/workflows/migrate-voice-model.yml deleted file mode 100644 index 34b897e6..00000000 --- a/.github/workflows/migrate-voice-model.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: One-shot voice model migration - -on: - push: - branches: - - feat/visual-language-v1 - paths: - - .github/workflows/migrate-voice-model.yml - -permissions: - contents: write - -jobs: - migrate: - name: Commit INT8 Paraformer through Git LFS - runs-on: ubuntu-latest - timeout-minutes: 30 - - steps: - - name: Checkout branch without smudging old LFS model - uses: actions/checkout@v7 - with: - fetch-depth: 0 - lfs: false - - - name: Enable Git LFS - shell: bash - run: git lfs install --local - - - name: Download official sherpa-onnx model package - shell: bash - run: | - set -euo pipefail - mkdir -p "$RUNNER_TEMP/paraformer" - curl --fail --location --retry 4 --retry-all-errors \ - https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-paraformer-bilingual-zh-en.tar.bz2 \ - --output "$RUNNER_TEMP/paraformer/model.tar.bz2" - tar -xjf "$RUNNER_TEMP/paraformer/model.tar.bz2" \ - -C "$RUNNER_TEMP/paraformer" - - - name: Verify and prepare INT8 model - shell: bash - run: | - python3 scripts/migrate_voice_model.py \ - "$RUNNER_TEMP/paraformer/sherpa-onnx-streaming-paraformer-bilingual-zh-en" - - - name: Commit model and generated trust metadata - shell: bash - run: | - set -euo pipefail - git config user.name "openIME release bot" - git config user.email "128580527+Slacker-LLC@users.noreply.github.com" - git add -A app/src/main/assets/models/voice - git lfs ls-files - if git diff --cached --quiet; then - echo "Voice model is already migrated." - exit 0 - fi - git commit -m "chore: replace voice model with INT8 Paraformer" - git push origin "HEAD:${GITHUB_REF_NAME}" From 8b4cb12b43e8db697a539e20364e39c41855c773 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:58:36 +0800 Subject: [PATCH 185/193] fix: restore archive build after data export changes --- .../java/llc/slacker/openime/RimeEngine.kt | 70 +++++++++---------- .../main/java/llc/slacker/openime/SetupUi.kt | 12 ++++ 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/llc/slacker/openime/RimeEngine.kt b/app/src/main/java/llc/slacker/openime/RimeEngine.kt index 6f53f236..e36bf1d9 100644 --- a/app/src/main/java/llc/slacker/openime/RimeEngine.kt +++ b/app/src/main/java/llc/slacker/openime/RimeEngine.kt @@ -353,55 +353,53 @@ class RimeEngine( } } - fun exportUserDictionaries( + internal fun exportUserDictionaries( targetDir: File, onComplete: (List?) -> Unit, ): Boolean { if (!isReady) return false return mutationQueue.submit { - val exported = synchronized(lock) { - if (!isReady) { - null - } else { - runCatching { - if (!syncSchemaFromSettingsLocked()) return@runCatching null - if (!targetDir.exists() && !targetDir.mkdirs()) { - return@runCatching null + 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 mappings = - RimeNative.nativeExportUserDictionaries(targetDir.absolutePath) - ?: return@runCatching null - buildList { - mappings.forEach { mapping -> - val separator = mapping.indexOf('\t') - if (separator <= 0 || separator >= mapping.lastIndex) { - return@runCatching 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@runCatching null - } - add( - RimeUserDictionaryArchive( - name = name, - content = file.readText(), - ), - ) - } + 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 } - }.getOrNull() + result += RimeUserDictionaryArchive( + name = name, + content = file.readText(), + ) + } + result + } catch (error: Exception) { + Log.w(TAG, "Rime user dictionary export failed", error) + null } } onComplete(exported) } } - fun importUserDictionaries( + internal fun importUserDictionaries( sourceDir: File, dictionaries: List, onComplete: (Int?) -> Unit, diff --git a/app/src/main/java/llc/slacker/openime/SetupUi.kt b/app/src/main/java/llc/slacker/openime/SetupUi.kt index 39a5a6c2..1bda3365 100644 --- a/app/src/main/java/llc/slacker/openime/SetupUi.kt +++ b/app/src/main/java/llc/slacker/openime/SetupUi.kt @@ -38,6 +38,18 @@ object SetupUi { 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) From 0d5704dd555c7f71cc8bae142297271e1b3b6e77 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:51:04 +0800 Subject: [PATCH 186/193] fix: clear remaining CI regressions --- .../slacker/openime/RimeNineKeyInstrumentedTest.kt | 2 +- .../slacker/openime/ClipboardPanelController.kt | 14 +++++++------- .../llc/slacker/openime/InputConnectionGateway.kt | 3 +-- .../llc/slacker/openime/NineKeyLocalDecoder.kt | 1 + .../java/llc/slacker/openime/NoRawTextSizeTest.kt | 5 ++++- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/app/src/androidTest/java/llc/slacker/openime/RimeNineKeyInstrumentedTest.kt b/app/src/androidTest/java/llc/slacker/openime/RimeNineKeyInstrumentedTest.kt index 594d1e28..52bf7b61 100644 --- a/app/src/androidTest/java/llc/slacker/openime/RimeNineKeyInstrumentedTest.kt +++ b/app/src/androidTest/java/llc/slacker/openime/RimeNineKeyInstrumentedTest.kt @@ -65,7 +65,7 @@ class RimeNineKeyInstrumentedTest { val instrumentation = InstrumentationRegistry.getInstrumentation() assumeTrue( "manual production preset audit; pass -e runPresetAudit true", - instrumentation.arguments.getString("runPresetAudit") == "true", + InstrumentationRegistry.getArguments().getString("runPresetAudit") == "true", ) val context = instrumentation.targetContext val originalFuzzy = ImeSettingsRepository.loadFuzzy(context) diff --git a/app/src/main/java/llc/slacker/openime/ClipboardPanelController.kt b/app/src/main/java/llc/slacker/openime/ClipboardPanelController.kt index bc4a0e1a..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,7 +270,7 @@ 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) @@ -287,7 +287,7 @@ internal class ClipboardPanelController( }, ) addView( - createPanelButton("编辑", 11f, true).apply { + createPanelButton("编辑", ImeTypographyTokens.CAPTION_SP, true).apply { tag = "phrase-edit:${phrase.id}" setOnClickListener { onFeedback() @@ -299,7 +299,7 @@ internal class ClipboardPanelController( }, ) addView( - createPanelButton("删除", 11f, true).apply { + createPanelButton("删除", ImeTypographyTokens.CAPTION_SP, true).apply { tag = "phrase-delete:${phrase.id}" setOnClickListener { onFeedback() @@ -374,7 +374,7 @@ internal class ClipboardPanelController( meta.addView( createPanelButton( if (entry.pinned) "取消置顶" else "置顶", - 10f, + ImeTypographyTokens.CAPTION_SP, true, ).apply { tag = "clip-pin:${entry.text}" @@ -387,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/InputConnectionGateway.kt b/app/src/main/java/llc/slacker/openime/InputConnectionGateway.kt index dc51839b..1ff15afb 100644 --- a/app/src/main/java/llc/slacker/openime/InputConnectionGateway.kt +++ b/app/src/main/java/llc/slacker/openime/InputConnectionGateway.kt @@ -4,7 +4,6 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.os.Build -import android.os.SystemClock import android.view.KeyEvent import android.view.inputmethod.ExtractedText import android.view.inputmethod.ExtractedTextRequest @@ -43,7 +42,7 @@ class InputConnectionGateway( private val context: Context?, private val connection: () -> InputConnection?, private val isPassword: () -> Boolean = { false }, - private val nowMs: () -> Long = { SystemClock.elapsedRealtime() }, + private val nowMs: () -> Long = { System.nanoTime() / 1_000_000L }, ) { data class CursorSnapshot( diff --git a/app/src/main/java/llc/slacker/openime/NineKeyLocalDecoder.kt b/app/src/main/java/llc/slacker/openime/NineKeyLocalDecoder.kt index daa50ecc..97d3b74d 100644 --- a/app/src/main/java/llc/slacker/openime/NineKeyLocalDecoder.kt +++ b/app/src/main/java/llc/slacker/openime/NineKeyLocalDecoder.kt @@ -152,6 +152,7 @@ internal class NineKeyLocalDecoder( val preview = when { stable != null -> stable continuous != null -> continuous + bounded.length == 1 -> bounded paths.isEmpty() -> bounded continuationBase != null && validPreset.isEmpty() && prefixPreview != null -> prefixPreview paths.isNotEmpty() -> paths.first() diff --git a/app/src/test/java/llc/slacker/openime/NoRawTextSizeTest.kt b/app/src/test/java/llc/slacker/openime/NoRawTextSizeTest.kt index 5873e155..9b595bca 100644 --- a/app/src/test/java/llc/slacker/openime/NoRawTextSizeTest.kt +++ b/app/src/test/java/llc/slacker/openime/NoRawTextSizeTest.kt @@ -23,7 +23,10 @@ class NoRawTextSizeTest { .any(line::contains) ) { val window = lines.subList(index, minOf(lines.size, index + 6)).joinToString(" ") - if (Regex(""",\s*\d+(?:\.\d+)?f\b""").containsMatchIn(window)) { + val rawHelperSize = Regex( + """(?:createKey|createButton|createPanelButton|labelText)\([^)]*,\s*\d+(?:\.\d+)?f\b""", + ) + if (rawHelperSize.containsMatchIn(window)) { violations += "${file.relativeTo(root)}:${index + 1}: ${line.trim()}" } } From 0004c58744a36ee02ccb4936c404f9b7198017ff Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:00:40 +0800 Subject: [PATCH 187/193] fix: clear lint indentation regression --- .github/workflows/android.yml | 12 +++++++++++- .../java/llc/slacker/openime/LocalVoiceImeService.kt | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 5e063b4b..aeb4b32a 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -68,7 +68,17 @@ 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 --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 uses: actions/upload-artifact@v7 diff --git a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt index bce26bd4..cf7b58fe 100644 --- a/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt +++ b/app/src/main/java/llc/slacker/openime/LocalVoiceImeService.kt @@ -1363,8 +1363,8 @@ class LocalVoiceImeService : InputMethodService(), ImeKeyboardView.Listener, Can candidates = finalCandidates, nativeReferences = nativeReferences, ) - keyboardView?.renderState(state) - } + keyboardView?.renderState(state) + } applyWhenCandidateSurfaceIdle() }, ) From a7360f5952c8917f63a28abfb93d5db6a6fd7336 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:12:42 +0800 Subject: [PATCH 188/193] fix: complete English UI resources --- app/src/main/res/values-en/strings.xml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 79020316..c1428fb0 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -3,6 +3,32 @@ 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 From 69ca70a67573868c05b41ac392258e866f14a9b9 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:16:33 +0800 Subject: [PATCH 189/193] ci: reuse prebuilt APKs for compatibility tests --- .github/workflows/android.yml | 73 +++++++++++++---------------------- 1 file changed, 27 insertions(+), 46 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index aeb4b32a..fd408ee8 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -71,7 +71,7 @@ jobs: shell: bash run: | set +e - ./gradlew :app:testDebugUnitTest :app:lintDebug :app:assembleDebug --no-daemon --console=plain + ./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" @@ -80,14 +80,19 @@ jobs: 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 @@ -97,47 +102,16 @@ 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: 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: Download prebuilt test APKs + uses: actions/download-artifact@v8 + with: + name: openIME-test-apks + path: ci-apks - name: Run compatibility instrumentation uses: reactivecircus/android-emulator-runner@v2 @@ -148,14 +122,21 @@ 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 - - - name: Upload compatibility test reports + script: | + set -euo pipefail + mkdir -p ci-reports + adb install -r ci-apks/app-debug.apk + adb install -r ci-apks/app-debug-androidTest.apk + adb shell am instrument -w -r \ + llc.slacker.openime.test/androidx.test.runner.AndroidJUnitRunner \ + | tee ci-reports/instrumentation-api-${{ matrix.api-level }}.txt + + - 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 From 37ddd63caeb65e6921035e7f41d859504598b168 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:17:05 +0800 Subject: [PATCH 190/193] ci: harden prebuilt APK reuse --- .github/workflows/android.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index fd408ee8..6805aac7 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -108,7 +108,7 @@ jobs: packages: 'platform-tools' - name: Download prebuilt test APKs - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: name: openIME-test-apks path: ci-apks @@ -125,8 +125,12 @@ jobs: script: | set -euo pipefail mkdir -p ci-reports - adb install -r ci-apks/app-debug.apk - adb install -r ci-apks/app-debug-androidTest.apk + 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-${{ matrix.api-level }}.txt From e26894594503c4dc123df016764cbae588fc0420 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:46:41 +0800 Subject: [PATCH 191/193] ci: fix instrumentation shell and enable KVM --- .github/workflows/android.yml | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 6805aac7..9278f4e0 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -102,6 +102,13 @@ jobs: api-level: [29, 31] steps: + - 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: @@ -123,17 +130,27 @@ jobs: disable-animations: true emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none script: | - set -euo pipefail + set -eu mkdir -p ci-reports + report="ci-reports/instrumentation-api-${{ matrix.api-level }}.txt" 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 \ + if adb shell am instrument -w -r \ llc.slacker.openime.test/androidx.test.runner.AndroidJUnitRunner \ - | tee ci-reports/instrumentation-api-${{ matrix.api-level }}.txt + >"$report" 2>&1; then + status=0 + else + status=$? + fi + cat "$report" + if grep -Eq 'FAILURES!!!|INSTRUMENTATION_FAILED|Process crashed' "$report"; then + exit 1 + fi + exit "$status" - name: Upload compatibility test report if: always() From 5e4817e6a76982ed69d3037e165161e8a7d81beb Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:56:16 +0800 Subject: [PATCH 192/193] ci: run instrumentation in one bash process --- .github/workflows/android.yml | 43 +++++++++++++++++------------------ 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 9278f4e0..c6927ff3 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -120,6 +120,26 @@ jobs: name: openIME-test-apks path: ci-apks + - name: Prepare instrumentation runner + shell: bash + run: | + 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 with: @@ -129,28 +149,7 @@ jobs: profile: pixel_2 disable-animations: true emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - script: | - set -eu - mkdir -p ci-reports - report="ci-reports/instrumentation-api-${{ matrix.api-level }}.txt" - 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" - if adb shell am instrument -w -r \ - llc.slacker.openime.test/androidx.test.runner.AndroidJUnitRunner \ - >"$report" 2>&1; then - status=0 - else - status=$? - fi - cat "$report" - if grep -Eq 'FAILURES!!!|INSTRUMENTATION_FAILED|Process crashed' "$report"; then - exit 1 - fi - exit "$status" + script: bash ci-run-instrumentation.sh ${{ matrix.api-level }} - name: Upload compatibility test report if: always() From 7f324ee4332c0902c6118fd169ee59a1c82f26c1 Mon Sep 17 00:00:00 2001 From: "muzi.li" <128580527+Slacker-LLC@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:22:41 +0800 Subject: [PATCH 193/193] chore: remove completed voice model migration script --- scripts/migrate_voice_model.py | 160 --------------------------------- 1 file changed, 160 deletions(-) delete mode 100755 scripts/migrate_voice_model.py diff --git a/scripts/migrate_voice_model.py b/scripts/migrate_voice_model.py deleted file mode 100755 index d5840ce1..00000000 --- a/scripts/migrate_voice_model.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python3 -"""Install the pinned INT8 streaming Paraformer model into openIME assets.""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import shutil -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -ASSETS = ROOT / "app" / "src" / "main" / "assets" -VOICE_ROOT = ASSETS / "models" / "voice" -TARGET_ROOT = VOICE_ROOT / "bilingual-paraformer" -OLD_ROOT = VOICE_ROOT / "bilingual-zipformer" - -MODEL_ID = "sherpa-onnx-streaming-paraformer-bilingual-zh-en-int8" -MODEL_VERSION = "hf-8e40c43-int8" -MODEL_TYPE = "paraformer" -ENGINE_VERSION = "sherpa-onnx-v1.13.6" -LANGUAGE = "zh-CN,en-US" -REQUIRED_MEMORY = 520_000_000 - -EXPECTED_SHA256 = { - "encoder.int8.onnx": "81a70226a8934e6ed92aa1d4fc486b428b5398e2f2619ed4897b7294cab90e9a", - "decoder.int8.onnx": "f3cca9f77bb9d93c8fcbfb63ae617b6b1ee96818df3aa3b151c40658fe38594f", -} - -TARGET_FILES = [ - "models/voice/bilingual-paraformer/decoder.int8.onnx", - "models/voice/bilingual-paraformer/encoder.int8.onnx", - "models/voice/bilingual-paraformer/tokens.txt", -] - - -def sha256_file(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def update_framed(digest, value: str) -> None: - data = value.encode("utf-8") - digest.update(str(len(data)).encode("ascii")) - digest.update(b":") - digest.update(data) - - -def manifest_fingerprint(manifest: dict[str, object]) -> str: - digest = hashlib.sha256() - digest.update(b"openime-voice-manifest-v1") - values = [ - str(manifest["modelId"]), - str(manifest["modelVersion"]), - str(manifest["language"]), - str(manifest["modelType"]), - str(manifest["engineVersion"]), - str(manifest["fileHash"]), - "1" if manifest["supportsPunctuation"] else "0", - str(manifest["requiredMemory"]), - str(len(manifest["files"])), - ] - for value in values: - update_framed(digest, value) - for path in manifest["files"]: - update_framed(digest, str(path)) - return digest.hexdigest() - - -def verify_source(source: Path) -> None: - for filename, expected in EXPECTED_SHA256.items(): - path = source / filename - if not path.is_file(): - raise SystemExit(f"missing upstream model file: {path}") - actual = sha256_file(path) - if actual != expected: - raise SystemExit( - f"{filename} SHA-256 mismatch: expected {expected}, got {actual}" - ) - - tokens = source / "tokens.txt" - if not tokens.is_file(): - raise SystemExit(f"missing upstream token file: {tokens}") - lines = tokens.read_text(encoding="utf-8").splitlines() - if len(lines) != 8404: - raise SystemExit(f"unexpected tokens.txt line count: {len(lines)}") - if lines[:3] != [" 0", " 1", " 2"]: - raise SystemExit("unexpected tokens.txt header") - - -def install(source: Path) -> None: - verify_source(source) - - if TARGET_ROOT.exists(): - shutil.rmtree(TARGET_ROOT) - TARGET_ROOT.mkdir(parents=True) - - for filename in ("decoder.int8.onnx", "encoder.int8.onnx", "tokens.txt"): - shutil.copy2(source / filename, TARGET_ROOT / filename) - - if OLD_ROOT.exists(): - shutil.rmtree(OLD_ROOT) - - aggregate = hashlib.sha256() - for relative in sorted(TARGET_FILES): - path = ASSETS / relative - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - aggregate.update(chunk) - - manifest = { - "modelId": MODEL_ID, - "modelVersion": MODEL_VERSION, - "language": LANGUAGE, - "modelType": MODEL_TYPE, - "engineVersion": ENGINE_VERSION, - "fileHash": aggregate.hexdigest(), - "supportsPunctuation": False, - "requiredMemory": REQUIRED_MEMORY, - "files": TARGET_FILES, - } - - (VOICE_ROOT / "manifest.json").write_text( - json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", - encoding="utf-8", - ) - - sizes = [str((ASSETS / relative).stat().st_size) for relative in TARGET_FILES] - fingerprint = manifest_fingerprint(manifest) - catalog = ( - "# openIME trusted downloaded voice model catalog v1\n" - "# publishermanifest_fingerprint_sha256file_sizes_in_manifest_order\n" - f"Slacker-LLC\t{fingerprint}\t{','.join(sizes)}\n" - ) - (VOICE_ROOT / "trusted-downloads.tsv").write_text(catalog, encoding="utf-8") - - print(json.dumps(manifest, ensure_ascii=False, indent=2)) - print(f"manifest fingerprint: {fingerprint}") - print(f"trusted sizes: {','.join(sizes)}") - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument( - "source", - type=Path, - help="Extracted sherpa-onnx-streaming-paraformer-bilingual-zh-en directory", - ) - args = parser.parse_args() - source = args.source.resolve() - if not source.is_dir(): - raise SystemExit(f"source directory does not exist: {source}") - install(source) - - -if __name__ == "__main__": - main()