HapeLee 1 год назад
Родитель
Сommit
0513fc143f

+ 29 - 2
app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt

@@ -8,6 +8,7 @@ import android.os.Bundle
 import android.view.Menu
 import android.view.MenuItem
 import android.view.MotionEvent
+import androidx.activity.result.contract.ActivityResultContracts
 import androidx.activity.viewModels
 import androidx.appcompat.widget.SearchView
 import androidx.fragment.app.Fragment
@@ -22,12 +23,12 @@ import io.legado.app.help.book.isLocalTxt
 import io.legado.app.help.config.AppConfig
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.model.CacheBook
-//import io.legado.app.lib.theme.accentColor
-//import io.legado.app.lib.theme.primaryTextColor
 import io.legado.app.model.ReadBook
 import io.legado.app.ui.about.AppLogDialog
 import io.legado.app.ui.book.toc.rule.TxtTocRuleDialog
 import io.legado.app.ui.file.HandleFileContract
+import io.legado.app.ui.replace.ReplaceRuleActivity
+import io.legado.app.ui.replace.edit.ReplaceEditActivity
 import io.legado.app.ui.widget.dialog.WaitDialog
 import io.legado.app.utils.gone
 import io.legado.app.utils.hideSoftInput
@@ -57,6 +58,12 @@ class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>(),
             }
         }
     }
+    private val replaceActivity =
+        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+            if (it.resultCode == RESULT_OK) {
+                viewModel.replaceRuleChanged()
+            }
+        }
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -143,6 +150,25 @@ class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>(),
 
     override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
         when (item.itemId) {
+            R.id.menu_replace -> {
+                val scopes = arrayListOf<String>()
+                viewModel.bookData.value?.name?.let { scopes.add(it) }
+                viewModel.bookSource?.bookSourceUrl?.let { scopes.add(it) }
+                replaceActivity.launch(
+                    ReplaceEditActivity.startIntent(
+                        this,
+                        pattern = "text",
+                        scope = scopes.joinToString(";"),
+                        isScopeTitle = true,
+                        isScopeContent = false
+                    )
+                )
+                return true
+            }
+            R.id.menu_replace_show -> {
+                replaceActivity.launch(Intent(this, ReplaceRuleActivity::class.java))
+                return true
+            }
             R.id.menu_toc_regex -> showDialogFragment(
                 TxtTocRuleDialog(viewModel.bookData.value?.tocUrl)
             )
@@ -210,6 +236,7 @@ class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>(),
         }
     }
 
+
     fun locateToChapter(index: Int) {
         val tag = "android:switcher:${binding.viewPager.id}:0"
         val chapterFragment = supportFragmentManager.findFragmentByTag(tag) as? ChapterListFragment

+ 12 - 0
app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt

@@ -9,7 +9,9 @@ import io.legado.app.base.BaseViewModel
 import io.legado.app.constant.AppLog
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
+import io.legado.app.data.entities.BookSource
 import io.legado.app.exception.NoStackTraceException
+import io.legado.app.help.book.ContentProcessor
 import io.legado.app.model.ReadBook
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.utils.FileDoc
@@ -21,15 +23,25 @@ import io.legado.app.utils.writeText
 
 class TocViewModel(application: Application) : BaseViewModel(application) {
     var bookUrl: String = ""
+    var bookSource: BookSource? = null
     var bookData = MutableLiveData<Book>()
     var chapterListCallBack: ChapterListCallBack? = null
     var bookMarkCallBack: BookmarkCallBack? = null
     var searchKey: String? = null
+    fun replaceRuleChanged() {
+        execute {
+            ReadBook.book?.let {
+                ContentProcessor.get(it.name, it.origin).upReplaceRules()
+                ReadBook.loadContent(resetPageOffset = false)
+            }
+        }
+    }
 
     fun initBook(bookUrl: String) {
         this.bookUrl = bookUrl
         execute {
             appDb.bookDao.getBook(bookUrl)?.let {
+                bookSource = appDb.bookSourceDao.getBookSource(it.origin)
                 bookData.postValue(it)
             }
         }

+ 171 - 61
app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt

@@ -5,6 +5,7 @@ import android.content.Intent
 import android.os.Bundle
 import android.view.Menu
 import android.view.MenuItem
+import android.widget.ArrayAdapter
 import android.widget.EditText
 import androidx.activity.viewModels
 import androidx.lifecycle.lifecycleScope
@@ -20,6 +21,9 @@ import io.legado.app.utils.sendToClip
 import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat
 import io.legado.app.utils.showHelp
 import io.legado.app.utils.viewbindingdelegate.viewBinding
+import androidx.core.content.edit
+import io.legado.app.lib.dialogs.alert
+import io.legado.app.utils.toastOnUi
 
 /**
  * 编辑替换规则
@@ -28,28 +32,74 @@ class ReplaceEditActivity :
     VMBaseActivity<ActivityReplaceEditBinding, ReplaceEditViewModel>(),
     KeyboardToolPop.CallBack {
 
-    companion object {
+    object GroupManager {
+        private const val PREF_NAME = "replace_groups"
+        private const val KEY_CUSTOM_GROUPS = "custom_groups"
+        private const val KEY_LAST_SELECTED = "last_selected_group"
+
+        private val presetGroups = listOf(
+            "默认", "地名", "国家", "企业", "人名", "敏感词", "生僻字", "乱码或广告"
+        )
+
+        private fun prefs(context: Context) =
+            context.getSharedPreferences(PREF_NAME, MODE_PRIVATE)
+
+        fun getAllGroups(context: Context): List<String> =
+            (getCustomGroups(context) + presetGroups).distinct()
+
+        fun getCustomGroups(context: Context): List<String> {
+            val json = prefs(context).getString(KEY_CUSTOM_GROUPS, "[]") ?: "[]"
+            return GSON.fromJson(json, Array<String>::class.java).toList()
+        }
+
+        fun saveCustomGroups(context: Context, groups: List<String>) {
+            prefs(context).edit { putString(KEY_CUSTOM_GROUPS, GSON.toJson(groups)) }
+        }
+
+        fun addCustomGroup(context: Context, group: String) {
+            if (group.isBlank() || presetGroups.contains(group)) return
+            val custom = getCustomGroups(context).toMutableList()
+            if (group !in custom) {
+                custom += group
+                saveCustomGroups(context, custom)
+            }
+        }
+
+        fun removeCustomGroups(context: Context, groupsToRemove: List<String>) {
+            if (groupsToRemove.isEmpty()) return
+            val updated = getCustomGroups(context).filterNot { it in groupsToRemove }
+            saveCustomGroups(context, updated)
+        }
+
+        fun getLastSelectedGroup(context: Context): String =
+            prefs(context).getString(KEY_LAST_SELECTED, "默认") ?: "默认"
 
+        fun setLastSelectedGroup(context: Context, group: String) {
+            prefs(context).edit { putString(KEY_LAST_SELECTED, group) }
+        }
+    }
+
+    companion object {
         fun startIntent(
             context: Context,
             id: Long = -1,
             pattern: String? = null,
             isRegex: Boolean = false,
-            scope: String? = null
-        ): Intent {
-            val intent = Intent(context, ReplaceEditActivity::class.java)
-            intent.putExtra("id", id)
-            intent.putExtra("pattern", pattern)
-            intent.putExtra("isRegex", isRegex)
-            intent.putExtra("scope", scope)
-            return intent
+            scope: String? = null,
+            isScopeTitle: Boolean = false,
+            isScopeContent: Boolean = false
+        ): Intent = Intent(context, ReplaceEditActivity::class.java).apply {
+            putExtra("id", id)
+            putExtra("pattern", pattern)
+            putExtra("isRegex", isRegex)
+            putExtra("scope", scope)
+            putExtra("isScopeTitle", isScopeTitle)
+            putExtra("isScopeContent", isScopeContent)
         }
-
     }
 
     override val binding by viewBinding(ActivityReplaceEditBinding::inflate)
     override val viewModel by viewModels<ReplaceEditViewModel>()
-
     private val softKeyboardTool by lazy {
         KeyboardToolPop(this, lifecycleScope, binding.root, this)
     }
@@ -58,9 +108,12 @@ class ReplaceEditActivity :
         super.onCreate(savedInstanceState)
         softKeyboardTool.attachToWindow(window)
         initView()
-        viewModel.initData(intent) {
-            upReplaceView(it)
-        }
+        viewModel.initData(intent, ::upReplaceView)
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        softKeyboardTool.dismiss()
     }
 
     override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
@@ -76,66 +129,126 @@ class ReplaceEditActivity :
             }
 
             R.id.menu_copy_rule -> sendToClip(GSON.toJson(getReplaceRule()))
-            R.id.menu_paste_rule -> viewModel.pasteRule {
-                upReplaceView(it)
-            }
+            R.id.menu_paste_rule -> viewModel.pasteRule(::upReplaceView)
         }
         return true
     }
 
-    override fun onDestroy() {
-        super.onDestroy()
-        softKeyboardTool.dismiss()
+    private fun initView() = binding.run {
+        ivHelp.setOnClickListener { showHelp("regexHelp") }
+        ivManageGroups.setOnClickListener { showGroupManageDialog() }
+
+        root.setOnApplyWindowInsetsListenerCompat { _, insets ->
+            softKeyboardTool.initialPadding = insets.imeHeight
+            insets
+        }
+
+        setupGroupAutoComplete()
+    }
+
+    private fun setupGroupAutoComplete() = binding.run {
+        val allGroups = GroupManager.getAllGroups(this@ReplaceEditActivity)
+        val adapter = ArrayAdapter(
+            this@ReplaceEditActivity,
+            android.R.layout.simple_dropdown_item_1line,
+            allGroups
+        )
+        etGroup.setAdapter(adapter)
+
+        etGroup.setText(GroupManager.getLastSelectedGroup(this@ReplaceEditActivity), false)
+
+        etGroup.setOnFocusChangeListener { _, hasFocus ->
+            if (!hasFocus) saveCurrentGroup()
+        }
+
+        etGroup.setOnItemClickListener { parent, _, position, _ ->
+            val selected = parent.getItemAtPosition(position) as String
+            GroupManager.setLastSelectedGroup(this@ReplaceEditActivity, selected)
+        }
+    }
+
+    private fun refreshGroupList() {
+        val allGroups = GroupManager.getAllGroups(this)
+        val adapter = ArrayAdapter(
+            this,
+            android.R.layout.simple_dropdown_item_1line,
+            allGroups
+        )
+        binding.etGroup.setAdapter(adapter)
     }
 
-    private fun initView() {
-        binding.ivHelp.setOnClickListener {
-            showHelp("regexHelp")
+    private fun saveCurrentGroup() {
+        val current = binding.etGroup.text.toString().trim()
+        if (current.isNotBlank()) {
+            GroupManager.addCustomGroup(this, current)
+            GroupManager.setLastSelectedGroup(this, current)
         }
-        binding.root.setOnApplyWindowInsetsListenerCompat { _, windowInsets ->
-            softKeyboardTool.initialPadding = windowInsets.imeHeight
-            windowInsets
+    }
+
+    private fun showGroupManageDialog() {
+        val allGroups = GroupManager.getCustomGroups(this).toMutableList()
+        if (allGroups.isEmpty()) {
+            toastOnUi("暂无自定义分组")
+            return
+        }
+
+        val checked = BooleanArray(allGroups.size)
+        alert(title = getString(R.string.group_manage)) {
+            multiChoiceItems(allGroups.toTypedArray(), checked) { _, _, _ -> }
+            negativeButton("关闭")
+            neutralButton("删除所选") {
+                val toDelete = allGroups.filterIndexed { i, _ -> checked[i] }
+                if (toDelete.isNotEmpty()) {
+                    GroupManager.removeCustomGroups(this@ReplaceEditActivity, toDelete)
+                    refreshGroupList()
+                    toastOnUi("已删除 ${toDelete.size} 个分组")
+                } else {
+                    toastOnUi("未选择任何分组")
+                }
+            }
         }
     }
 
-    private fun upReplaceView(replaceRule: ReplaceRule) = binding.run {
-        etName.setText(replaceRule.name)
-        etGroup.setText(replaceRule.group)
-        etReplaceRule.setText(replaceRule.pattern)
-        cbUseRegex.isChecked = replaceRule.isRegex
-        etReplaceTo.setText(replaceRule.replacement)
-        cbScopeTitle.isChecked = replaceRule.scopeTitle
-        cbScopeContent.isChecked = replaceRule.scopeContent
-        etScope.setText(replaceRule.scope)
-        etExcludeScope.setText(replaceRule.excludeScope)
-        etTimeout.setText(replaceRule.timeoutMillisecond.toString())
+    private fun upReplaceView(rule: ReplaceRule) = binding.run {
+        etName.setText(rule.name)
+        val group = rule.group.takeUnless { it.isNullOrBlank() }
+            ?: GroupManager.getLastSelectedGroup(this@ReplaceEditActivity)
+        etGroup.setText(group, false)
+        etReplaceRule.setText(rule.pattern)
+        cbUseRegex.isChecked = rule.isRegex
+        etReplaceTo.setText(rule.replacement)
+        cbScopeTitle.isChecked = rule.scopeTitle
+        cbScopeContent.isChecked = rule.scopeContent
+        etScope.setText(rule.scope)
+        etExcludeScope.setText(rule.excludeScope)
+        etTimeout.setText(rule.timeoutMillisecond.toString())
     }
 
     private fun getReplaceRule(): ReplaceRule = binding.run {
-        val replaceRule: ReplaceRule = viewModel.replaceRule ?: ReplaceRule()
-        replaceRule.name = etName.text.toString()
-        replaceRule.group = etGroup.text.toString()
-        replaceRule.pattern = etReplaceRule.text.toString()
-        replaceRule.isRegex = cbUseRegex.isChecked
-        replaceRule.replacement = etReplaceTo.text.toString()
-        replaceRule.scopeTitle = cbScopeTitle.isChecked
-        replaceRule.scopeContent = cbScopeContent.isChecked
-        replaceRule.scope = etScope.text.toString()
-        replaceRule.excludeScope = etExcludeScope.text.toString()
-        replaceRule.timeoutMillisecond = etTimeout.text.toString().ifEmpty { "3000" }.toLong()
-        return replaceRule
-    }
-
-    override fun helpActions(): List<SelectItem<String>> {
-        return arrayListOf(
-            SelectItem("正则教程", "regexHelp")
-        )
+        saveCurrentGroup()
+        val rule = viewModel.replaceRule ?: ReplaceRule()
+
+        rule.name = etName.text.toString()
+        rule.group = etGroup.text.toString().trim().ifBlank {
+            GroupManager.getLastSelectedGroup(this@ReplaceEditActivity)
+        }
+        rule.pattern = etReplaceRule.text.toString()
+        rule.isRegex = cbUseRegex.isChecked
+        rule.replacement = etReplaceTo.text.toString()
+        rule.scopeTitle = cbScopeTitle.isChecked
+        rule.scopeContent = cbScopeContent.isChecked
+        rule.scope = etScope.text.toString()
+        rule.excludeScope = etExcludeScope.text.toString()
+        rule.timeoutMillisecond = etTimeout.text.toString().ifEmpty { "3000" }.toLong()
+
+        rule
     }
 
+    override fun helpActions(): List<SelectItem<String>> =
+        listOf(SelectItem("正则教程", "regexHelp"))
+
     override fun onHelpActionSelect(action: String) {
-        when (action) {
-            "regexHelp" -> showHelp("regexHelp")
-        }
+        if (action == "regexHelp") showHelp("regexHelp")
     }
 
     override fun sendText(text: String) {
@@ -144,15 +257,12 @@ class ReplaceEditActivity :
         if (view is EditText) {
             val start = view.selectionStart
             val end = view.selectionEnd
-            //获取EditText的文字
             val edit = view.editableText
             if (start < 0 || start >= edit.length) {
                 edit.append(text)
             } else {
-                //光标所在位置插入文字
                 edit.replace(start, end, text)
             }
         }
     }
-
 }

+ 5 - 1
app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt

@@ -22,11 +22,15 @@ class ReplaceEditViewModel(application: Application) : BaseViewModel(application
                 val pattern = intent.getStringExtra("pattern") ?: ""
                 val isRegex = intent.getBooleanExtra("isRegex", false)
                 val scope = intent.getStringExtra("scope")
+                val isScopeTitle = intent.getBooleanExtra("isScopeTitle", false)
+                val isScopeContent = intent.getBooleanExtra("isScopeContent", false)
                 ReplaceRule(
                     name = pattern,
                     pattern = pattern,
                     isRegex = isRegex,
-                    scope = scope
+                    scope = scope,
+                    scopeTitle = isScopeTitle,
+                    scopeContent = isScopeContent
                 )
             }
         }.onFinally {

+ 9 - 1
app/src/main/res/layout/activity_replace_edit.xml

@@ -43,15 +43,23 @@
                 android:id="@+id/til_group"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
+                style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
                 android:hint="@string/group">
 
-                <com.google.android.material.textfield.TextInputEditText
+                <com.google.android.material.textfield.MaterialAutoCompleteTextView
                     android:id="@+id/et_group"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
+                    android:text="@string/text_default"
                     tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
             </com.google.android.material.textfield.TextInputLayout>
 
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/iv_manage_groups"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/group_manage"/>
+
             <com.google.android.material.textfield.TextInputLayout
                 android:id="@+id/til_replace_rule"
                 android:layout_width="match_parent"

+ 8 - 0
app/src/main/res/menu/book_toc.xml

@@ -42,6 +42,14 @@
             android:title="@string/reverse_toc"
             app:showAsAction="never" />
 
+        <item
+            android:id="@+id/menu_replace"
+            android:title="@string/replace" />
+
+        <item
+            android:id="@+id/menu_replace_show"
+            android:title="@string/replace_rule" />
+
         <item
             android:id="@+id/menu_use_replace"
             android:checkable="true"