Skip to content

Commit 43a2ab2

Browse files
committed
feat: support user dict management
1 parent f1452c9 commit 43a2ab2

File tree

15 files changed

+726
-0
lines changed

15 files changed

+726
-0
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2015 - 2025 Rime community
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*/
5+
6+
package com.osfans.trime.data.userdict
7+
8+
import com.osfans.trime.util.appContext
9+
import java.io.File
10+
import java.io.InputStream
11+
import java.io.OutputStream
12+
13+
object UserDictManager {
14+
fun restoreUserDict(stream: InputStream, snapshotFile: String): Result<Unit> {
15+
val tempFile = File(appContext.cacheDir, snapshotFile)
16+
try {
17+
tempFile.outputStream().use {
18+
stream.copyTo(it)
19+
}
20+
val success = restoreUserDict(tempFile.absolutePath)
21+
return if (success) {
22+
Result.success(Unit)
23+
} else {
24+
Result.failure(Exception("Failed to restore"))
25+
}
26+
} finally {
27+
tempFile.delete()
28+
}
29+
}
30+
31+
fun importUserDict(stream: InputStream, dictName: String, textFile: String): Result<Int> {
32+
val tempFile = File(appContext.cacheDir, textFile)
33+
try {
34+
tempFile.outputStream().use {
35+
stream.copyTo(it)
36+
}
37+
val count = importUserDict(dictName, tempFile.absolutePath)
38+
return if (count >= 0) {
39+
Result.success(count)
40+
} else {
41+
Result.failure(
42+
Exception("Failed to import from '$textFile' to '$dictName'"),
43+
)
44+
}
45+
} finally {
46+
tempFile.delete()
47+
}
48+
}
49+
50+
fun exportUserDict(dest: OutputStream, dictName: String, textFile: String): Result<Int> {
51+
val tempFile = File(appContext.cacheDir, textFile)
52+
try {
53+
val count = exportUserDict(dictName, tempFile.absolutePath)
54+
tempFile.inputStream().use {
55+
it.copyTo(dest)
56+
}
57+
return if (count >= 0) {
58+
Result.success(count)
59+
} else {
60+
Result.failure(
61+
Exception("Failed to export '$dictName' to '$textFile'"),
62+
)
63+
}
64+
} finally {
65+
tempFile.delete()
66+
}
67+
}
68+
69+
@JvmStatic
70+
external fun getUserDictList(): Array<String>
71+
72+
@JvmStatic
73+
external fun backupUserDict(dictName: String): Boolean
74+
75+
@JvmStatic
76+
external fun restoreUserDict(snapshotFile: String): Boolean
77+
78+
@JvmStatic
79+
external fun exportUserDict(dictName: String, textFile: String): Int
80+
81+
@JvmStatic
82+
external fun importUserDict(dictName: String, textFile: String): Int
83+
}

app/src/main/java/com/osfans/trime/ui/fragments/PrefFragment.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ class PrefFragment : PaddingPreferenceFragment() {
3636
findNavController().navigate(R.id.action_prefFragment_to_schemaListFragment)
3737
true
3838
}
39+
get<Preference>("pref_user_dict")?.setOnPreferenceClickListener {
40+
findNavController().navigate(R.id.action_prefFragment_to_userDictionaryFragment)
41+
true
42+
}
3943
get<Preference>("pref_user_data")?.setOnPreferenceClickListener {
4044
findNavController().navigate(R.id.action_prefFragment_to_profileFragment)
4145
true
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2015 - 2025 Rime community
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*/
5+
6+
package com.osfans.trime.ui.main.settings.userdict
7+
8+
import android.content.Context
9+
import android.view.ViewGroup
10+
import android.widget.ImageButton
11+
import androidx.recyclerview.widget.RecyclerView
12+
import com.chad.library.adapter4.BaseQuickAdapter
13+
14+
class UserDictListAdapter(
15+
override var items: List<String>,
16+
val initMoreButton: (ImageButton.(String) -> Unit) = {},
17+
) : BaseQuickAdapter<String, UserDictListAdapter.ViewHolder>() {
18+
19+
inner class ViewHolder(
20+
ui: UserDictListEntryUi,
21+
) : RecyclerView.ViewHolder(ui.root) {
22+
val nameText = ui.nameText
23+
val moreButton = ui.moreButton
24+
}
25+
26+
override fun onCreateViewHolder(
27+
context: Context,
28+
parent: ViewGroup,
29+
viewType: Int,
30+
): ViewHolder = ViewHolder(UserDictListEntryUi(context))
31+
32+
override fun onBindViewHolder(
33+
holder: ViewHolder,
34+
position: Int,
35+
item: String?,
36+
) {
37+
val name = item ?: return
38+
holder.nameText.text = name
39+
initMoreButton(holder.moreButton, name)
40+
}
41+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2015 - 2025 Rime community
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*/
5+
6+
package com.osfans.trime.ui.main.settings.userdict
7+
8+
import android.content.Context
9+
import android.view.ViewGroup
10+
import com.osfans.trime.R
11+
import splitties.dimensions.dp
12+
import splitties.resources.drawable
13+
import splitties.resources.resolveThemeAttribute
14+
import splitties.resources.styledColor
15+
import splitties.resources.styledDimenPxSize
16+
import splitties.resources.styledDrawable
17+
import splitties.views.backgroundColor
18+
import splitties.views.dsl.constraintlayout.before
19+
import splitties.views.dsl.constraintlayout.centerVertically
20+
import splitties.views.dsl.constraintlayout.constraintLayout
21+
import splitties.views.dsl.constraintlayout.endOfParent
22+
import splitties.views.dsl.constraintlayout.lParams
23+
import splitties.views.dsl.constraintlayout.matchConstraints
24+
import splitties.views.dsl.constraintlayout.startOfParent
25+
import splitties.views.dsl.core.Ui
26+
import splitties.views.dsl.core.add
27+
import splitties.views.dsl.core.imageButton
28+
import splitties.views.dsl.core.matchParent
29+
import splitties.views.dsl.core.textView
30+
import splitties.views.dsl.core.wrapContent
31+
import splitties.views.imageDrawable
32+
import splitties.views.setPaddingDp
33+
import splitties.views.textAppearance
34+
35+
class UserDictListEntryUi(
36+
override val ctx: Context,
37+
) : Ui {
38+
val nameText = textView {
39+
setPaddingDp(0, 16, 0, 16)
40+
textAppearance = ctx.resolveThemeAttribute(android.R.attr.textAppearanceListItem)
41+
}
42+
43+
val moreButton = imageButton {
44+
background = styledDrawable(android.R.attr.selectableItemBackground)
45+
imageDrawable = drawable(R.drawable.ic_baseline_more_horiz_24)
46+
}
47+
48+
override val root = constraintLayout {
49+
layoutParams = ViewGroup.LayoutParams(matchParent, wrapContent)
50+
backgroundColor = styledColor(android.R.attr.colorBackground)
51+
minHeight = styledDimenPxSize(android.R.attr.listPreferredItemHeightSmall)
52+
53+
val paddingStart = styledDimenPxSize(android.R.attr.listPreferredItemPaddingStart)
54+
add(
55+
nameText,
56+
lParams {
57+
width = matchConstraints
58+
height = wrapContent
59+
centerVertically()
60+
startOfParent(paddingStart)
61+
before(moreButton)
62+
},
63+
)
64+
add(
65+
moreButton,
66+
lParams {
67+
width = dp(53)
68+
height = matchConstraints
69+
centerVertically()
70+
endOfParent()
71+
},
72+
)
73+
}
74+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2015 - 2025 Rime community
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*/
5+
6+
package com.osfans.trime.ui.main.settings.userdict
7+
8+
import android.annotation.SuppressLint
9+
import android.content.Context
10+
import android.view.View
11+
import android.view.ViewGroup
12+
import android.widget.ImageButton
13+
import androidx.coordinatorlayout.widget.CoordinatorLayout
14+
import androidx.core.content.ContextCompat
15+
import androidx.core.view.ViewCompat
16+
import androidx.core.view.WindowInsetsCompat
17+
import androidx.core.view.doOnAttach
18+
import androidx.core.view.isVisible
19+
import androidx.core.view.updateLayoutParams
20+
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior
21+
import com.google.android.material.floatingactionbutton.FloatingActionButton
22+
import com.google.android.material.snackbar.BaseTransientBottomBar
23+
import com.google.android.material.snackbar.Snackbar
24+
import com.osfans.trime.R
25+
import splitties.dimensions.dp
26+
import splitties.resources.drawable
27+
import splitties.resources.styledColor
28+
import splitties.views.backgroundColor
29+
import splitties.views.bottomPadding
30+
import splitties.views.dsl.coordinatorlayout.coordinatorLayout
31+
import splitties.views.dsl.coordinatorlayout.defaultLParams
32+
import splitties.views.dsl.core.Ui
33+
import splitties.views.dsl.core.add
34+
import splitties.views.dsl.core.margin
35+
import splitties.views.dsl.core.matchParent
36+
import splitties.views.dsl.core.view
37+
import splitties.views.dsl.recyclerview.recyclerView
38+
import splitties.views.gravityEndBottom
39+
import splitties.views.imageDrawable
40+
import splitties.views.recyclerview.verticalLayoutManager
41+
import kotlin.math.min
42+
43+
class UserDictListUi(
44+
override val ctx: Context,
45+
entries: Array<String>,
46+
initMoreButton: (ImageButton.(String) -> Unit) = {},
47+
) : Ui {
48+
val fab =
49+
view(::FloatingActionButton) {
50+
imageDrawable =
51+
drawable(R.drawable.ic_baseline_add_24)!!.apply {
52+
setTint(styledColor(android.R.attr.colorForegroundInverse))
53+
}
54+
}
55+
56+
fun showSnackBar(text: String) {
57+
Snackbar.make(root, text, Snackbar.LENGTH_SHORT)
58+
.addCallback(
59+
object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
60+
override fun onShown(transientBottomBar: Snackbar) {
61+
// snackbar is invisible when it attached to parent,
62+
// but change visibility won't trigger `onDependentViewChanged`.
63+
// so we need to update fab position when snackbar fully shown
64+
// see [^1]
65+
fab.translationY = -transientBottomBar.view.height.toFloat()
66+
}
67+
},
68+
)
69+
.show()
70+
}
71+
72+
val adapter by lazy { UserDictListAdapter(entries.toList(), initMoreButton) }
73+
74+
private val list = recyclerView {
75+
layoutManager = verticalLayoutManager()
76+
adapter = this@UserDictListUi.adapter
77+
clipToPadding = false
78+
}
79+
80+
private fun updateViewMargin(insets: WindowInsetsCompat? = null) {
81+
val windowInsets = (insets ?: ViewCompat.getRootWindowInsets(root)) ?: return
82+
val navBars = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
83+
fab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
84+
bottomMargin = navBars.bottom + ctx.dp(16)
85+
}
86+
list.bottomPadding = navBars.bottom
87+
}
88+
89+
override val root = coordinatorLayout {
90+
backgroundColor = styledColor(android.R.attr.colorBackground)
91+
add(
92+
list,
93+
defaultLParams {
94+
height = matchParent
95+
width = matchParent
96+
},
97+
)
98+
add(
99+
fab,
100+
defaultLParams {
101+
gravity = gravityEndBottom
102+
margin = dp(16)
103+
behavior =
104+
object : HideBottomViewOnScrollBehavior<FloatingActionButton>() {
105+
@SuppressLint("RestrictedApi")
106+
override fun layoutDependsOn(
107+
parent: CoordinatorLayout,
108+
child: FloatingActionButton,
109+
dependency: View,
110+
): Boolean = dependency is Snackbar.SnackbarLayout
111+
112+
override fun onDependentViewChanged(
113+
parent: CoordinatorLayout,
114+
child: FloatingActionButton,
115+
dependency: View,
116+
): Boolean {
117+
// [^1]: snackbar is invisible when it attached to parent
118+
// update fab position only when snackbar is visible
119+
if (dependency.isVisible) {
120+
child.translationY = min(0f, dependency.translationY - dependency.height)
121+
return true
122+
}
123+
return false
124+
}
125+
126+
override fun onDependentViewRemoved(
127+
parent: CoordinatorLayout,
128+
child: FloatingActionButton,
129+
dependency: View,
130+
) {
131+
child.translationY = 0f
132+
}
133+
}
134+
},
135+
)
136+
doOnAttach { updateViewMargin() }
137+
ViewCompat.setOnApplyWindowInsetsListener(this) { _, windowInsets ->
138+
updateViewMargin(windowInsets)
139+
windowInsets
140+
}
141+
}
142+
}

0 commit comments

Comments
 (0)