Skip to content

Commit f745720

Browse files
committed
feat(android): improve ime learning and connection resilience
1 parent f428ff6 commit f745720

16 files changed

Lines changed: 764 additions & 46 deletions
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.clipsync.app.core
2+
3+
object PinyinPreferenceStoreCodec {
4+
5+
fun encodeLearnedWeights(weights: Map<String, Int>, maxEntries: Int = DEFAULT_MAX_ENTRIES): String {
6+
return weights.entries
7+
.sortedByDescending { it.value }
8+
.take(maxEntries)
9+
.joinToString(separator = "\n") { entry ->
10+
"${entry.key}\t${entry.value}"
11+
}
12+
}
13+
14+
fun decodeLearnedWeights(raw: String): Map<String, Int> {
15+
if (raw.isBlank()) return emptyMap()
16+
17+
return raw.lineSequence()
18+
.mapNotNull { line ->
19+
val parts = line.split('\t')
20+
if (parts.size != 2) {
21+
return@mapNotNull null
22+
}
23+
val value = parts[1].toIntOrNull() ?: return@mapNotNull null
24+
parts[0] to value
25+
}
26+
.toMap()
27+
}
28+
29+
fun encodePinnedPhrases(phrases: Map<String, String>, maxEntries: Int = DEFAULT_MAX_ENTRIES): String {
30+
return phrases.entries
31+
.sortedBy { it.key }
32+
.take(maxEntries)
33+
.joinToString(separator = "\n") { entry ->
34+
"${entry.key}\t${entry.value}"
35+
}
36+
}
37+
38+
fun decodePinnedPhrases(raw: String): Map<String, String> {
39+
if (raw.isBlank()) return emptyMap()
40+
41+
return raw.lineSequence()
42+
.mapNotNull { line ->
43+
val parts = line.split('\t', limit = 2)
44+
if (parts.size != 2) {
45+
return@mapNotNull null
46+
}
47+
parts[0] to parts[1]
48+
}
49+
.toMap()
50+
}
51+
52+
private const val DEFAULT_MAX_ENTRIES = 128
53+
}

clipSync-android/app/src/main/java/com/clipsync/app/core/SettingsManager.kt

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class SettingsManager(private val context: Context) {
2929
private val DEVICE_NAME_KEY = stringPreferencesKey("device_name")
3030
private val SYNC_ENABLED_KEY = booleanPreferencesKey("sync_enabled")
3131
private val ENCRYPTION_ENABLED_KEY = booleanPreferencesKey("encryption_enabled")
32+
private val IME_LEARNED_WEIGHTS_KEY = stringPreferencesKey("ime_learned_weights")
33+
private val IME_PINNED_PHRASES_KEY = stringPreferencesKey("ime_pinned_phrases")
3234

3335
// Default values
3436
private val defaultWsUrl = "ws://8.141.100.238:8080/ws"
@@ -171,6 +173,75 @@ class SettingsManager(private val context: Context) {
171173
}
172174
}
173175

176+
// ─── IME Learned Weights ───
177+
178+
val imeLearnedWeightsFlow: Flow<Map<String, Int>> = context.dataStore.data.map { prefs ->
179+
PinyinPreferenceStoreCodec.decodeLearnedWeights(prefs[IME_LEARNED_WEIGHTS_KEY].orEmpty())
180+
}
181+
182+
suspend fun getImeLearnedWeights(): Map<String, Int> = imeLearnedWeightsFlow.first()
183+
184+
suspend fun incrementImeLearnedWeight(pinyin: String, text: String) {
185+
val key = "${pinyin}|${text}"
186+
context.dataStore.edit { prefs ->
187+
val current = PinyinPreferenceStoreCodec
188+
.decodeLearnedWeights(prefs[IME_LEARNED_WEIGHTS_KEY].orEmpty())
189+
.toMutableMap()
190+
current[key] = (current[key] ?: 0) + 1
191+
prefs[IME_LEARNED_WEIGHTS_KEY] = PinyinPreferenceStoreCodec.encodeLearnedWeights(current)
192+
}
193+
}
194+
195+
suspend fun pinImeLearnedWeight(pinyin: String, text: String) {
196+
val key = "${pinyin}|${text}"
197+
context.dataStore.edit { prefs ->
198+
val current = PinyinPreferenceStoreCodec
199+
.decodeLearnedWeights(prefs[IME_LEARNED_WEIGHTS_KEY].orEmpty())
200+
.toMutableMap()
201+
current[key] = MAX_IME_PINNED_WEIGHT
202+
prefs[IME_LEARNED_WEIGHTS_KEY] = PinyinPreferenceStoreCodec.encodeLearnedWeights(current)
203+
}
204+
}
205+
206+
suspend fun removeImeLearnedWeight(pinyin: String, text: String) {
207+
val key = "${pinyin}|${text}"
208+
context.dataStore.edit { prefs ->
209+
val current = PinyinPreferenceStoreCodec
210+
.decodeLearnedWeights(prefs[IME_LEARNED_WEIGHTS_KEY].orEmpty())
211+
.toMutableMap()
212+
current.remove(key)
213+
prefs[IME_LEARNED_WEIGHTS_KEY] = PinyinPreferenceStoreCodec.encodeLearnedWeights(current)
214+
}
215+
}
216+
217+
// ─── IME Pinned Phrases ───
218+
219+
val imePinnedPhrasesFlow: Flow<Map<String, String>> = context.dataStore.data.map { prefs ->
220+
PinyinPreferenceStoreCodec.decodePinnedPhrases(prefs[IME_PINNED_PHRASES_KEY].orEmpty())
221+
}
222+
223+
suspend fun getImePinnedPhrases(): Map<String, String> = imePinnedPhrasesFlow.first()
224+
225+
suspend fun setImePinnedPhrase(pinyin: String, text: String) {
226+
context.dataStore.edit { prefs ->
227+
val current = PinyinPreferenceStoreCodec
228+
.decodePinnedPhrases(prefs[IME_PINNED_PHRASES_KEY].orEmpty())
229+
.toMutableMap()
230+
current[pinyin] = text
231+
prefs[IME_PINNED_PHRASES_KEY] = PinyinPreferenceStoreCodec.encodePinnedPhrases(current)
232+
}
233+
}
234+
235+
suspend fun removeImePinnedPhrase(pinyin: String) {
236+
context.dataStore.edit { prefs ->
237+
val current = PinyinPreferenceStoreCodec
238+
.decodePinnedPhrases(prefs[IME_PINNED_PHRASES_KEY].orEmpty())
239+
.toMutableMap()
240+
current.remove(pinyin)
241+
prefs[IME_PINNED_PHRASES_KEY] = PinyinPreferenceStoreCodec.encodePinnedPhrases(current)
242+
}
243+
}
244+
174245
// ─── Clear all settings ───
175246

176247
suspend fun clearAll() {
@@ -184,4 +255,8 @@ class SettingsManager(private val context: Context) {
184255
suspend fun isLoggedIn(): Boolean {
185256
return getToken().isNotEmpty() && getUsername().isNotEmpty()
186257
}
258+
259+
private companion object {
260+
const val MAX_IME_PINNED_WEIGHT = 10
261+
}
187262
}

0 commit comments

Comments
 (0)