This commit is contained in:
HapeLee committed 2025-06-04 13:33:51 +08:00
1 parent b86d90021c
commit 6c0757c91b
972 files changed
+6885 -7321

No files matched your search

@@ -0,0 +1,80 @@
package io.legato.kazusa.ui.dict
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import com.google.android.material.tabs.TabLayout
import io.legato.kazusa.R
import io.legato.kazusa.base.BaseDialogFragment
import io.legato.kazusa.data.entities.DictRule
import io.legato.kazusa.databinding.DialogDictBinding
//import io.legado.app.lib.theme.accentColor
//import io.legado.app.lib.theme.backgroundColor
import io.legato.kazusa.utils.setHtml
import io.legato.kazusa.utils.setLayout
import io.legato.kazusa.utils.toastOnUi
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
/**
* 词典
*/
class DictDialog() : BaseDialogFragment(R.layout.dialog_dict) {
constructor(word: String) : this() {
arguments = Bundle().apply {
putString("word", word)
}
}
private val viewModel by viewModels<DictViewModel>()
private val binding by viewBinding(DialogDictBinding::bind)
private var word: String? = null
override fun onStart() {
super.onStart()
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
binding.tvDict.movementMethod = LinkMovementMethod()
word = arguments?.getString("word")
if (word.isNullOrEmpty()) {
toastOnUi(R.string.cannot_empty)
dismiss()
return
}
// binding.tabLayout.setBackgroundColor(backgroundColor)
// binding.tabLayout.setSelectedTabIndicatorColor(accentColor)
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabReselected(tab: TabLayout.Tab) {
}
override fun onTabUnselected(tab: TabLayout.Tab) {
}
override fun onTabSelected(tab: TabLayout.Tab) {
val dictRule = tab.tag as DictRule
binding.rotateLoading.visible()
viewModel.dict(dictRule, word!!) {
binding.rotateLoading.inVisible()
binding.tvDict.setHtml(it)
}
}
})
viewModel.initData {
it.forEach {
binding.tabLayout.addTab(binding.tabLayout.newTab().apply {
text = it.name
tag = it
})
}
}
}
}
@@ -0,0 +1,37 @@
package io.legato.kazusa.ui.dict
import android.app.Application
import io.legato.kazusa.base.BaseViewModel
import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.DictRule
import io.legato.kazusa.help.coroutine.Coroutine
class DictViewModel(application: Application) : BaseViewModel(application) {
private var dictJob: Coroutine<String>? = null
fun initData(onSuccess: (List<DictRule>) -> Unit) {
execute {
appDb.dictRuleDao.enabled
}.onSuccess {
onSuccess.invoke(it)
}
}
fun dict(
dictRule: DictRule,
word: String,
onFinally: (String) -> Unit
) {
dictJob?.cancel()
dictJob = execute {
dictRule.search(word)
}.onSuccess {
onFinally.invoke(it)
}.onError {
onFinally.invoke(it.localizedMessage ?: "ERROR")
}
}
}
@@ -0,0 +1,250 @@
package io.legato.kazusa.ui.dict.rule
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.viewModels
import androidx.appcompat.widget.PopupMenu
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import io.legato.kazusa.R
import io.legato.kazusa.base.VMBaseActivity
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.DictRule
import io.legato.kazusa.databinding.ActivityDictRuleBinding
import io.legato.kazusa.databinding.DialogEditTextBinding
import io.legato.kazusa.help.DirectLinkUpload
import io.legato.kazusa.lib.dialogs.alert
//import io.legado.app.lib.theme.primaryColor
import io.legato.kazusa.ui.association.ImportDictRuleDialog
import io.legato.kazusa.ui.file.HandleFileContract
import io.legato.kazusa.ui.qrcode.QrCodeResult
import io.legato.kazusa.ui.widget.SelectActionBar
import io.legato.kazusa.ui.widget.recycler.DragSelectTouchHelper
import io.legato.kazusa.ui.widget.recycler.ItemTouchCallback
import io.legato.kazusa.ui.widget.recycler.VerticalDivider
import io.legato.kazusa.utils.ACache
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.isAbsUrl
import io.legato.kazusa.utils.launch
import io.legato.kazusa.utils.readText
import io.legato.kazusa.utils.sendToClip
import io.legato.kazusa.utils.showDialogFragment
import io.legato.kazusa.utils.showHelp
import io.legato.kazusa.utils.splitNotBlank
import io.legato.kazusa.utils.toastOnUi
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
class DictRuleActivity : VMBaseActivity<ActivityDictRuleBinding, DictRuleViewModel>(),
PopupMenu.OnMenuItemClickListener,
SelectActionBar.CallBack,
DictRuleAdapter.CallBack {
override val viewModel by viewModels<DictRuleViewModel>()
override val binding by viewBinding(ActivityDictRuleBinding::inflate)
private val importRecordKey = "dictRuleUrls"
private val adapter by lazy { DictRuleAdapter(this, this) }
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
it ?: return@registerForActivityResult
showDialogFragment(
ImportDictRuleDialog(it)
)
}
private val importDoc = registerForActivityResult(HandleFileContract()) {
kotlin.runCatching {
it.uri?.readText(this)?.let {
showDialogFragment(
ImportDictRuleDialog(it)
)
}
}.onFailure {
toastOnUi("readTextError:${it.localizedMessage}")
}
}
private val exportResult = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri ->
alert(R.string.export_success) {
if (uri.toString().isAbsUrl()) {
setMessage(DirectLinkUpload.getSummary())
}
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.hint = getString(R.string.path)
editView.setText(uri.toString())
}
customView { alertBinding.root }
okButton {
sendToClip(uri.toString())
}
}
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
initRecyclerView()
initSelectActionView()
observeDictRuleData()
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.dict_rule, menu)
return super.onCompatCreateOptionsMenu(menu)
}
private fun initRecyclerView() {
//binding.recyclerView.setEdgeEffectColor(primaryColor)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter
binding.recyclerView.addItemDecoration(VerticalDivider(this))
val itemTouchCallback = ItemTouchCallback(adapter)
itemTouchCallback.isCanDrag = true
val dragSelectTouchHelper: DragSelectTouchHelper =
DragSelectTouchHelper(adapter.dragSelectCallback).setSlideArea(16, 50)
dragSelectTouchHelper.attachToRecyclerView(binding.recyclerView)
// When this page is opened, it is in selection mode
dragSelectTouchHelper.activeSlideSelect()
// Note: need judge selection first, so add ItemTouchHelper after it.
ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView)
}
private fun initSelectActionView() {
binding.selectActionBar.setMainActionText(R.string.delete)
binding.selectActionBar.inflateMenu(R.menu.dict_rule_sel)
binding.selectActionBar.setOnMenuItemClickListener(this)
binding.selectActionBar.setCallBack(this)
}
private fun observeDictRuleData() {
lifecycleScope.launch {
appDb.dictRuleDao.flowAll().catch {
AppLog.put("字典规则获取数据失败\n${it.localizedMessage}", it)
}.flowOn(IO).collect {
adapter.setItems(it, adapter.diffItemCallBack)
}
}
}
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_add -> showDialogFragment<DictRuleEditDialog>()
R.id.menu_import_local -> importDoc.launch {
mode = HandleFileContract.FILE
allowExtensions = arrayOf("txt", "json")
}
R.id.menu_import_onLine -> showImportDialog()
R.id.menu_import_qr -> qrCodeResult.launch()
R.id.menu_import_default -> viewModel.importDefault()
R.id.menu_help -> showHelp("dictRuleHelp")
}
return super.onCompatOptionsItemSelected(item)
}
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_enable_selection -> {
viewModel.enableSelection(*adapter.selection.toTypedArray())
}
R.id.menu_disable_selection -> {
viewModel.disableSelection(*adapter.selection.toTypedArray())
}
R.id.menu_export_selection -> exportResult.launch {
mode = HandleFileContract.EXPORT
fileData = HandleFileContract.FileData(
"exportDictRule.json",
GSON.toJson(adapter.selection).toByteArray(),
"application/json"
)
}
}
return true
}
override fun onClickSelectBarMainAction() {
viewModel.delete(*adapter.selection.toTypedArray())
}
override fun selectAll(selectAll: Boolean) {
if (selectAll) {
adapter.selectAll()
} else {
adapter.revertSelection()
}
}
override fun revertSelection() {
adapter.revertSelection()
}
override fun update(vararg rule: DictRule) {
viewModel.update(*rule)
}
override fun delete(rule: DictRule) {
alert(R.string.draw) {
setMessage(getString(R.string.sure_del) + "\n" + rule.name)
noButton()
yesButton {
viewModel.delete(rule)
}
}
}
override fun edit(rule: DictRule) {
showDialogFragment(DictRuleEditDialog(rule.name))
}
override fun upOrder() {
viewModel.upSortNumber()
}
override fun upCountView() {
binding.selectActionBar.upCountView(
adapter.selection.size,
adapter.itemCount
)
}
@SuppressLint("InflateParams")
private fun showImportDialog() {
val aCache = ACache.get(cacheDir = false)
val cacheUrls: MutableList<String> = aCache
.getAsString(importRecordKey)
?.splitNotBlank(",")
?.toMutableList() ?: mutableListOf()
alert(titleResource = R.string.import_on_line) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.hint = "url"
editView.setFilterValues(cacheUrls)
editView.delCallBack = {
cacheUrls.remove(it)
aCache.put(importRecordKey, cacheUrls.joinToString(","))
}
}
customView { alertBinding.root }
okButton {
val text = alertBinding.editView.text?.toString()
text?.let {
if (it.isAbsUrl() && !cacheUrls.contains(it)) {
cacheUrls.add(0, it)
aCache.put(importRecordKey, cacheUrls.joinToString(","))
}
showDialogFragment(
ImportDictRuleDialog(it)
)
}
}
cancelButton()
}
}
}
@@ -0,0 +1,209 @@
package io.legato.kazusa.ui.dict.rule
import android.content.Context
import android.os.Bundle
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import io.legato.kazusa.base.adapter.ItemViewHolder
import io.legato.kazusa.base.adapter.RecyclerAdapter
import io.legato.kazusa.data.entities.DictRule
import io.legato.kazusa.databinding.ItemDictRuleBinding
//import io.legado.app.lib.theme.backgroundColor
import io.legato.kazusa.ui.widget.recycler.DragSelectTouchHelper
import io.legato.kazusa.ui.widget.recycler.ItemTouchCallback
class DictRuleAdapter(context: Context, var callBack: CallBack) :
RecyclerAdapter<DictRule, ItemDictRuleBinding>(context),
ItemTouchCallback.Callback {
private val selected = linkedSetOf<DictRule>()
val selection: List<DictRule>
get() {
return getItems().filter {
selected.contains(it)
}
}
val diffItemCallBack = object : DiffUtil.ItemCallback<DictRule>() {
override fun areItemsTheSame(oldItem: DictRule, newItem: DictRule): Boolean {
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: DictRule, newItem: DictRule): Boolean {
if (oldItem.name != newItem.name) {
return false
}
if (oldItem.enabled != newItem.enabled) {
return false
}
return true
}
override fun getChangePayload(oldItem: DictRule, newItem: DictRule): Any? {
val payload = Bundle()
if (oldItem.name != newItem.name) {
payload.putBoolean("upName", true)
}
if (oldItem.enabled != newItem.enabled) {
payload.putBoolean("enabled", newItem.enabled)
}
if (payload.isEmpty) {
return null
}
return payload
}
}
fun selectAll() {
getItems().forEach {
selected.add(it)
}
notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null)))
callBack.upCountView()
}
fun revertSelection() {
getItems().forEach {
if (selected.contains(it)) {
selected.remove(it)
} else {
selected.add(it)
}
}
notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null)))
callBack.upCountView()
}
override fun getViewBinding(parent: ViewGroup): ItemDictRuleBinding {
return ItemDictRuleBinding.inflate(inflater, parent, false)
}
override fun onCurrentListChanged() {
callBack.upCountView()
}
override fun convert(
holder: ItemViewHolder,
binding: ItemDictRuleBinding,
item: DictRule,
payloads: MutableList<Any>
) {
binding.run {
if (payloads.isEmpty()) {
//root.setBackgroundColor(ColorUtils.withAlpha(context.backgroundColor, 0.5f))
cbName.text = item.name
swtEnabled.isChecked = item.enabled
cbName.isChecked = selected.contains(item)
} else {
for (i in payloads.indices) {
val bundle = payloads[i] as Bundle
bundle.keySet().forEach {
when (it) {
"selected" -> cbName.isChecked = selected.contains(item)
"upName" -> cbName.text = item.name
"enabled" -> swtEnabled.isChecked = item.enabled
}
}
}
}
}
}
override fun registerListener(holder: ItemViewHolder, binding: ItemDictRuleBinding) {
binding.apply {
swtEnabled.setOnCheckedChangeListener { buttonView, isChecked ->
if (buttonView.isPressed) {
getItem(holder.layoutPosition)?.let {
it.enabled = isChecked
callBack.update(it)
}
}
}
cbName.setOnClickListener {
getItem(holder.layoutPosition)?.let {
if (cbName.isChecked) {
selected.add(it)
} else {
selected.remove(it)
}
}
callBack.upCountView()
}
ivEdit.setOnClickListener {
getItem(holder.layoutPosition)?.let {
callBack.edit(it)
}
}
ivDelete.setOnClickListener {
getItem(holder.layoutPosition)?.let {
callBack.delete(it)
}
}
}
}
override fun swap(srcPosition: Int, targetPosition: Int): Boolean {
val srcItem = getItem(srcPosition)
val targetItem = getItem(targetPosition)
if (srcItem != null && targetItem != null) {
if (srcItem.sortNumber == targetItem.sortNumber) {
callBack.upOrder()
} else {
val srcOrder = srcItem.sortNumber
srcItem.sortNumber = targetItem.sortNumber
targetItem.sortNumber = srcOrder
movedItems.add(srcItem)
movedItems.add(targetItem)
}
}
swapItem(srcPosition, targetPosition)
return true
}
private val movedItems = linkedSetOf<DictRule>()
override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
if (movedItems.isNotEmpty()) {
callBack.update(*movedItems.toTypedArray())
movedItems.clear()
}
}
val dragSelectCallback: DragSelectTouchHelper.Callback =
object : DragSelectTouchHelper.AdvanceCallback<DictRule>(Mode.ToggleAndReverse) {
override fun currentSelectedId(): MutableSet<DictRule> {
return selected
}
override fun getItemId(position: Int): DictRule {
return getItem(position)!!
}
override fun updateSelectState(position: Int, isSelected: Boolean): Boolean {
getItem(position)?.let {
if (isSelected) {
selected.add(it)
} else {
selected.remove(it)
}
notifyItemChanged(position, bundleOf(Pair("selected", null)))
callBack.upCountView()
return true
}
return false
}
}
interface CallBack {
fun update(vararg rule: DictRule)
fun delete(rule: DictRule)
fun edit(rule: DictRule)
fun upOrder()
fun upCountView()
}
}
@@ -0,0 +1,122 @@
package io.legato.kazusa.ui.dict.rule
import android.app.Application
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.viewModels
import io.legato.kazusa.R
import io.legato.kazusa.base.BaseBottomSheetDialogFragment
import io.legato.kazusa.base.BaseViewModel
import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.DictRule
import io.legato.kazusa.databinding.DialogDictRuleEditBinding
//import io.legado.app.lib.theme.primaryColor
import io.legato.kazusa.utils.*
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
class DictRuleEditDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_dict_rule_edit),
Toolbar.OnMenuItemClickListener {
val viewModel by viewModels<DictRuleEditViewModel>()
val binding by viewBinding(DialogDictRuleEditBinding::bind)
constructor(name: String) : this() {
arguments = Bundle().apply {
putString("name", name)
}
}
override fun onStart() {
super.onStart()
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
//binding.toolBar.setBackgroundColor(primaryColor)
binding.toolBar.inflateMenu(R.menu.dict_rule_edit)
binding.toolBar.menu.applyTint(requireContext())
binding.toolBar.setOnMenuItemClickListener(this)
viewModel.initData(arguments?.getString("name")) {
upRuleView(viewModel.dictRule)
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_save -> viewModel.save(getDictRule()) {
dismissAllowingStateLoss()
}
R.id.menu_copy_rule -> viewModel.copyRule(getDictRule())
R.id.menu_paste_rule -> viewModel.pasteRule {
upRuleView(it)
}
}
return true
}
private fun upRuleView(dictRule: DictRule?) {
binding.tvRuleName.setText(dictRule?.name)
binding.tvUrlRule.setText(dictRule?.urlRule)
binding.tvShowRule.setText(dictRule?.showRule)
}
private fun getDictRule(): DictRule {
val dictRule = viewModel.dictRule?.copy() ?: DictRule()
dictRule.name = binding.tvRuleName.text.toString()
dictRule.urlRule = binding.tvUrlRule.text.toString()
dictRule.showRule = binding.tvShowRule.text.toString()
return dictRule
}
class DictRuleEditViewModel(application: Application) : BaseViewModel(application) {
var dictRule: DictRule? = null
fun initData(name: String?, onFinally: () -> Unit) {
execute {
if (dictRule == null && name != null) {
dictRule = appDb.dictRuleDao.getByName(name)
}
}.onFinally {
onFinally.invoke()
}
}
fun save(newDictRule: DictRule, onFinally: () -> Unit) {
execute {
dictRule?.let {
appDb.dictRuleDao.delete(it)
}
appDb.dictRuleDao.insert(newDictRule)
dictRule = newDictRule
}.onFinally {
onFinally.invoke()
}
}
fun copyRule(dictRule: DictRule) {
context.sendToClip(GSON.toJson(dictRule))
}
fun pasteRule(success: (DictRule) -> Unit) {
val text = context.getClipText()
if (text.isNullOrBlank()) {
context.toastOnUi("剪贴板没有内容")
return
}
execute {
GSON.fromJsonObject<DictRule>(text).getOrThrow()
}.onSuccess {
success.invoke(it)
}.onError {
context.toastOnUi("格式不对")
}
}
}
}
@@ -0,0 +1,64 @@
package io.legato.kazusa.ui.dict.rule
import android.app.Application
import io.legato.kazusa.base.BaseViewModel
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.DictRule
import io.legato.kazusa.help.DefaultData
import io.legato.kazusa.utils.toastOnUi
class DictRuleViewModel(application: Application) : BaseViewModel(application) {
fun update(vararg dictRule: DictRule) {
execute {
appDb.dictRuleDao.update(*dictRule)
}.onError {
val msg = "更新字典规则出错\n${it.localizedMessage}"
AppLog.put(msg, it)
context.toastOnUi(msg)
}
}
fun delete(vararg dictRule: DictRule) {
execute {
appDb.dictRuleDao.delete(*dictRule)
}.onError {
val msg = "删除字典规则出错\n${it.localizedMessage}"
AppLog.put(msg, it)
context.toastOnUi(msg)
}
}
fun upSortNumber() {
execute {
val rules = appDb.dictRuleDao.all
for ((index, rule) in rules.withIndex()) {
rule.sortNumber = index + 1
}
appDb.dictRuleDao.insert(*rules.toTypedArray())
}
}
fun enableSelection(vararg dictRule: DictRule) {
execute {
val array = dictRule.map { it.copy(enabled = true) }.toTypedArray()
appDb.dictRuleDao.insert(*array)
}
}
fun disableSelection(vararg dictRule: DictRule) {
execute {
val array = dictRule.map { it.copy(enabled = false) }.toTypedArray()
appDb.dictRuleDao.insert(*array)
}
}
fun importDefault() {
execute {
DefaultData.importDefaultDictRules()
}
}
}