kunfei %!s(int64=3) %!d(string=hai) anos
pai
achega
8514e9035c

+ 7 - 0
app/src/main/assets/help/dictRuleHelp.md

@@ -0,0 +1,7 @@
+## 字典规则说明
+
+* 字典规则是用在正文文字选择菜单字典里的规则,通常用来做翻译或者查找
+* urlRule
+    * 同书源的url规则
+* showRule
+    * 用来提取显示到对话框里面内容的规则

+ 2 - 0
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))
                 successLive.postValue(Pair("replaceRule", json))
             json.contains("themeName") ->
             json.contains("themeName") ->
                 successLive.postValue(Pair("theme", json))
                 successLive.postValue(Pair("theme", json))
+            json.contains("urlRule") && json.contains("showRule") ->
+                successLive.postValue(Pair("dictRule", json))
             json.contains("name") && json.contains("rule") ->
             json.contains("name") && json.contains("rule") ->
                 successLive.postValue(Pair("txtRule", json))
                 successLive.postValue(Pair("txtRule", json))
             json.contains("name") && json.contains("url") ->
             json.contains("name") && json.contains("url") ->

+ 185 - 0
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<ImportDictRuleViewModel>()
+    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<DictRule, ItemSourceImportBinding>(context) {
+
+        override fun getViewBinding(parent: ViewGroup): ItemSourceImportBinding {
+            return ItemSourceImportBinding.inflate(inflater, parent, false)
+        }
+
+        override fun convert(
+            holder: ItemViewHolder,
+            binding: ItemSourceImportBinding,
+            item: DictRule,
+            payloads: MutableList<Any>
+        ) {
+            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<DictRule>(code).getOrNull()?.let { source ->
+                viewModel.allSources[it] = source
+                adapter.setItem(it, source)
+            }
+        }
+    }
+}

+ 112 - 0
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<String>()
+    val successLiveData = MutableLiveData<Int>()
+
+    val allSources = arrayListOf<DictRule>()
+    val checkSources = arrayListOf<DictRule?>()
+    val selectStatus = arrayListOf<Boolean>()
+
+    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<DictRule>()
+            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<DictRule>(text).getOrThrow()?.let {
+                    allSources.add(it)
+                }
+            }
+            text.isJsonArray() -> GSON.fromJsonArray<DictRule>(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)
+        }
+    }
+
+}

+ 6 - 0
app/src/main/java/io/legado/app/ui/association/OnLineImportActivity.kt

@@ -40,6 +40,9 @@ class OnLineImportActivity :
                 "txtRule" -> showDialogFragment(
                 "txtRule" -> showDialogFragment(
                     ImportTxtTocRuleDialog(it.second, true)
                     ImportTxtTocRuleDialog(it.second, true)
                 )
                 )
+                "dictRule" -> showDialogFragment(
+                    ImportDictRuleDialog(it.second, true)
+                )
             }
             }
         }
         }
         viewModel.errorLive.observe(this) {
         viewModel.errorLive.observe(this) {
@@ -67,6 +70,9 @@ class OnLineImportActivity :
                 "/httpTTS" -> showDialogFragment(
                 "/httpTTS" -> showDialogFragment(
                     ImportHttpTtsDialog(url, true)
                     ImportHttpTtsDialog(url, true)
                 )
                 )
+                "/dictRule" -> showDialogFragment(
+                    ImportDictRuleDialog(url, true)
+                )
                 "/theme" -> showDialogFragment(
                 "/theme" -> showDialogFragment(
                     ImportThemeDialog(url, true)
                     ImportThemeDialog(url, true)
                 )
                 )

+ 59 - 2
app/src/main/java/io/legado/app/ui/dict/rule/DictRuleActivity.kt

@@ -1,5 +1,6 @@
 package io.legado.app.ui.dict.rule
 package io.legado.app.ui.dict.rule
 
 
+import android.annotation.SuppressLint
 import android.os.Bundle
 import android.os.Bundle
 import android.view.Menu
 import android.view.Menu
 import android.view.MenuItem
 import android.view.MenuItem
@@ -16,7 +17,9 @@ import io.legado.app.databinding.DialogEditTextBinding
 import io.legado.app.help.DirectLinkUpload
 import io.legado.app.help.DirectLinkUpload
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.theme.primaryColor
 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.document.HandleFileContract
+import io.legado.app.ui.qrcode.QrCodeResult
 import io.legado.app.ui.widget.SelectActionBar
 import io.legado.app.ui.widget.SelectActionBar
 import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
 import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
 import io.legado.app.ui.widget.recycler.ItemTouchCallback
 import io.legado.app.ui.widget.recycler.ItemTouchCallback
@@ -32,8 +35,25 @@ class DictRuleActivity : VMBaseActivity<ActivityDictRuleBinding, DictRuleViewMod
 
 
     override val viewModel by viewModels<DictRuleViewModel>()
     override val viewModel by viewModels<DictRuleViewModel>()
     override val binding by viewBinding(ActivityDictRuleBinding::inflate)
     override val binding by viewBinding(ActivityDictRuleBinding::inflate)
-
+    private val importRecordKey = "dictRuleUrls"
     private val adapter by lazy { DictRuleAdapter(this, this) }
     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()) {
     private val exportResult = registerForActivityResult(HandleFileContract()) {
         it.uri?.let { uri ->
         it.uri?.let { uri ->
             alert(R.string.export_success) {
             alert(R.string.export_success) {
@@ -102,6 +122,12 @@ class DictRuleActivity : VMBaseActivity<ActivityDictRuleBinding, DictRuleViewMod
         when (item.itemId) {
         when (item.itemId) {
             R.id.menu_create -> showDialogFragment<DictRuleEditDialog>()
             R.id.menu_create -> showDialogFragment<DictRuleEditDialog>()
             R.id.menu_import_default -> viewModel.importDefault()
             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 -> {}
             R.id.menu_help -> {}
         }
         }
         return super.onCompatOptionsItemSelected(item)
         return super.onCompatOptionsItemSelected(item)
@@ -162,5 +188,36 @@ class DictRuleActivity : VMBaseActivity<ActivityDictRuleBinding, DictRuleViewMod
         )
         )
     }
     }
 
 
-
+    @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_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()
+        }
+    }
 }
 }