From 0d242b9bc0403790af36b316d8d00fd2d169fa19 Mon Sep 17 00:00:00 2001 From: kunfei Date: Tue, 21 Feb 2023 09:46:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/assets/help/dictRuleHelp.md | 7 + .../association/BaseAssociationViewModel.kt | 2 + .../ui/association/ImportDictRuleDialog.kt | 185 ++++++++++++++++++ .../ui/association/ImportDictRuleViewModel.kt | 112 +++++++++++ .../ui/association/OnLineImportActivity.kt | 6 + .../app/ui/dict/rule/DictRuleActivity.kt | 61 +++++- 6 files changed, 371 insertions(+), 2 deletions(-) create mode 100644 app/src/main/assets/help/dictRuleHelp.md create mode 100644 app/src/main/java/io/legado/app/ui/association/ImportDictRuleDialog.kt create mode 100644 app/src/main/java/io/legado/app/ui/association/ImportDictRuleViewModel.kt diff --git a/app/src/main/assets/help/dictRuleHelp.md b/app/src/main/assets/help/dictRuleHelp.md new file mode 100644 index 000000000..7574187b7 --- /dev/null +++ b/app/src/main/assets/help/dictRuleHelp.md @@ -0,0 +1,7 @@ +## 字典规则说明 + +* 字典规则是用在正文文字选择菜单字典里的规则,通常用来做翻译或者查找 +* urlRule + * 同书源的url规则 +* showRule + * 用来提取显示到对话框里面内容的规则 \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/BaseAssociationViewModel.kt b/app/src/main/java/io/legado/app/ui/association/BaseAssociationViewModel.kt index b12b55def..0ad391e61 100644 --- a/app/src/main/java/io/legado/app/ui/association/BaseAssociationViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/association/BaseAssociationViewModel.kt @@ -30,6 +30,8 @@ abstract class BaseAssociationViewModel(application: Application) : BaseViewMode successLive.postValue(Pair("replaceRule", json)) json.contains("themeName") -> successLive.postValue(Pair("theme", json)) + json.contains("urlRule") && json.contains("showRule") -> + successLive.postValue(Pair("dictRule", json)) json.contains("name") && json.contains("rule") -> successLive.postValue(Pair("txtRule", json)) json.contains("name") && json.contains("url") -> diff --git a/app/src/main/java/io/legado/app/ui/association/ImportDictRuleDialog.kt b/app/src/main/java/io/legado/app/ui/association/ImportDictRuleDialog.kt new file mode 100644 index 000000000..f7ae3c04f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportDictRuleDialog.kt @@ -0,0 +1,185 @@ +package io.legado.app.ui.association + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.DictRule +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemSourceImportBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.dialog.CodeDialog +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.utils.* +import io.legado.app.utils.viewbindingdelegate.viewBinding +import splitties.views.onClick + +class ImportDictRuleDialog() : BaseDialogFragment(R.layout.dialog_recycler_view), + CodeDialog.Callback { + + constructor(source: String, finishOnDismiss: Boolean = false) : this() { + arguments = Bundle().apply { + putString("source", source) + putBoolean("finishOnDismiss", finishOnDismiss) + } + } + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val viewModel by viewModels() + private val adapter by lazy { SourcesAdapter(requireContext()) } + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + if (arguments?.getBoolean("finishOnDismiss") == true) { + activity?.finish() + } + } + + @SuppressLint("NotifyDataSetChanged") + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.import_tts) + binding.rotateLoading.visible() + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + binding.tvCancel.visible() + binding.tvCancel.setOnClickListener { + dismissAllowingStateLoss() + } + binding.tvOk.visible() + binding.tvOk.setOnClickListener { + val waitDialog = WaitDialog(requireContext()) + waitDialog.show() + viewModel.importSelect { + waitDialog.dismiss() + dismissAllowingStateLoss() + } + } + binding.tvFooterLeft.visible() + binding.tvFooterLeft.setOnClickListener { + val selectAll = viewModel.isSelectAll + viewModel.selectStatus.forEachIndexed { index, b -> + if (b != !selectAll) { + viewModel.selectStatus[index] = !selectAll + } + } + adapter.notifyDataSetChanged() + upSelectText() + } + viewModel.errorLiveData.observe(this) { + binding.rotateLoading.gone() + binding.tvMsg.apply { + text = it + visible() + } + } + viewModel.successLiveData.observe(this) { + binding.rotateLoading.gone() + if (it > 0) { + adapter.setItems(viewModel.allSources) + upSelectText() + } else { + binding.tvMsg.apply { + setText(R.string.wrong_format) + visible() + } + } + } + val source = arguments?.getString("source") + if (source.isNullOrEmpty()) { + dismiss() + return + } + viewModel.importSource(source) + } + + private fun upSelectText() { + if (viewModel.isSelectAll) { + binding.tvFooterLeft.text = getString( + R.string.select_cancel_count, + viewModel.selectCount, + viewModel.allSources.size + ) + } else { + binding.tvFooterLeft.text = getString( + R.string.select_all_count, + viewModel.selectCount, + viewModel.allSources.size + ) + } + } + + inner class SourcesAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemSourceImportBinding { + return ItemSourceImportBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemSourceImportBinding, + item: DictRule, + payloads: MutableList + ) { + binding.apply { + cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition] + cbSourceName.text = item.name + val localSource = viewModel.checkSources[holder.layoutPosition] + tvSourceState.text = when (localSource) { + null -> "新增" + else -> "已有" + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemSourceImportBinding) { + binding.apply { + cbSourceName.setOnCheckedChangeListener { buttonView, isChecked -> + if (buttonView.isPressed) { + viewModel.selectStatus[holder.layoutPosition] = isChecked + upSelectText() + } + } + root.onClick { + cbSourceName.isChecked = !cbSourceName.isChecked + viewModel.selectStatus[holder.layoutPosition] = cbSourceName.isChecked + upSelectText() + } + tvOpen.setOnClickListener { + val source = viewModel.allSources[holder.layoutPosition] + showDialogFragment( + CodeDialog( + GSON.toJson(source), + disableEdit = false, + requestId = holder.layoutPosition.toString() + ) + ) + } + } + } + + } + + override fun onCodeSave(code: String, requestId: String?) { + requestId?.toInt()?.let { + GSON.fromJsonObject(code).getOrNull()?.let { source -> + viewModel.allSources[it] = source + adapter.setItem(it, source) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportDictRuleViewModel.kt b/app/src/main/java/io/legado/app/ui/association/ImportDictRuleViewModel.kt new file mode 100644 index 000000000..99cd58bcb --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportDictRuleViewModel.kt @@ -0,0 +1,112 @@ +package io.legado.app.ui.association + +import android.app.Application +import androidx.lifecycle.MutableLiveData +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppConst +import io.legado.app.data.appDb +import io.legado.app.data.entities.DictRule +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.http.newCallResponseBody +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.text +import io.legado.app.utils.* + +class ImportDictRuleViewModel(app: Application) : BaseViewModel(app) { + + val errorLiveData = MutableLiveData() + val successLiveData = MutableLiveData() + + val allSources = arrayListOf() + val checkSources = arrayListOf() + val selectStatus = arrayListOf() + + val isSelectAll: Boolean + get() { + selectStatus.forEach { + if (!it) { + return false + } + } + return true + } + + val selectCount: Int + get() { + var count = 0 + selectStatus.forEach { + if (it) { + count++ + } + } + return count + } + + fun importSelect(finally: () -> Unit) { + execute { + val selectSource = arrayListOf() + selectStatus.forEachIndexed { index, b -> + if (b) { + selectSource.add(allSources[index]) + } + } + appDb.dictRuleDao.upsert(*selectSource.toTypedArray()) + }.onFinally { + finally.invoke() + } + } + + fun importSource(text: String) { + execute { + importSourceAwait(text.trim()) + }.onError { + it.printOnDebug() + errorLiveData.postValue(it.localizedMessage ?: "") + }.onSuccess { + comparisonSource() + } + } + + private suspend fun importSourceAwait(text: String) { + when { + text.isJsonObject() -> { + GSON.fromJsonObject(text).getOrThrow()?.let { + allSources.add(it) + } + } + text.isJsonArray() -> GSON.fromJsonArray(text).getOrThrow()?.let { items -> + allSources.addAll(items) + } + text.isAbsUrl() -> { + importSourceUrl(text) + } + else -> throw NoStackTraceException(context.getString(R.string.wrong_format)) + } + } + + private suspend fun importSourceUrl(url: String) { + okHttpClient.newCallResponseBody { + if (url.endsWith("#requestWithoutUA")) { + url(url.substringBeforeLast("#requestWithoutUA")) + header(AppConst.UA_NAME, "null") + } else { + url(url) + } + }.text().let { + importSourceAwait(it) + } + } + + private fun comparisonSource() { + execute { + allSources.forEach { + val source = appDb.dictRuleDao.getByName(it.name) + checkSources.add(source) + selectStatus.add(source == null) + } + successLiveData.postValue(allSources.size) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/OnLineImportActivity.kt b/app/src/main/java/io/legado/app/ui/association/OnLineImportActivity.kt index 3d6e9bd45..1a222843a 100644 --- a/app/src/main/java/io/legado/app/ui/association/OnLineImportActivity.kt +++ b/app/src/main/java/io/legado/app/ui/association/OnLineImportActivity.kt @@ -40,6 +40,9 @@ class OnLineImportActivity : "txtRule" -> showDialogFragment( ImportTxtTocRuleDialog(it.second, true) ) + "dictRule" -> showDialogFragment( + ImportDictRuleDialog(it.second, true) + ) } } viewModel.errorLive.observe(this) { @@ -67,6 +70,9 @@ class OnLineImportActivity : "/httpTTS" -> showDialogFragment( ImportHttpTtsDialog(url, true) ) + "/dictRule" -> showDialogFragment( + ImportDictRuleDialog(url, true) + ) "/theme" -> showDialogFragment( ImportThemeDialog(url, true) ) diff --git a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleActivity.kt b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleActivity.kt index 19e5a9ae9..36d9b0a0c 100644 --- a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleActivity.kt +++ b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleActivity.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.dict.rule +import android.annotation.SuppressLint import android.os.Bundle import android.view.Menu import android.view.MenuItem @@ -16,7 +17,9 @@ import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.help.DirectLinkUpload import io.legado.app.lib.dialogs.alert import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.association.ImportDictRuleDialog import io.legado.app.ui.document.HandleFileContract +import io.legado.app.ui.qrcode.QrCodeResult import io.legado.app.ui.widget.SelectActionBar import io.legado.app.ui.widget.recycler.DragSelectTouchHelper import io.legado.app.ui.widget.recycler.ItemTouchCallback @@ -32,8 +35,25 @@ class DictRuleActivity : VMBaseActivity() 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) { @@ -102,6 +122,12 @@ class DictRuleActivity : VMBaseActivity showDialogFragment() R.id.menu_import_default -> viewModel.importDefault() + 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_help -> {} } return super.onCompatOptionsItemSelected(item) @@ -162,5 +188,36 @@ class DictRuleActivity : VMBaseActivity = aCache + .getAsString(importRecordKey) + ?.splitNotBlank(",") + ?.toMutableList() ?: mutableListOf() + alert(titleResource = R.string.import_replace_rule_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 (!cacheUrls.contains(it)) { + cacheUrls.add(0, it) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) + } + showDialogFragment( + ImportDictRuleDialog(it) + ) + } + } + cancelButton() + } + } } \ No newline at end of file