更加强劲的替换管理 #179

This commit is contained in:
HapeLee
2025-10-31 00:51:50 +08:00
parent cdd23b426c
commit 0513fc143f
6 changed files with 233 additions and 64 deletions
@@ -8,6 +8,7 @@ import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.MotionEvent import android.view.MotionEvent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment 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.help.config.AppConfig
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.model.CacheBook 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.model.ReadBook
import io.legado.app.ui.about.AppLogDialog import io.legado.app.ui.about.AppLogDialog
import io.legado.app.ui.book.toc.rule.TxtTocRuleDialog import io.legado.app.ui.book.toc.rule.TxtTocRuleDialog
import io.legado.app.ui.file.HandleFileContract 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.ui.widget.dialog.WaitDialog
import io.legado.app.utils.gone import io.legado.app.utils.gone
import io.legado.app.utils.hideSoftInput 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -143,6 +150,25 @@ class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>(),
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { 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( R.id.menu_toc_regex -> showDialogFragment(
TxtTocRuleDialog(viewModel.bookData.value?.tocUrl) TxtTocRuleDialog(viewModel.bookData.value?.tocUrl)
) )
@@ -210,6 +236,7 @@ class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>(),
} }
} }
fun locateToChapter(index: Int) { fun locateToChapter(index: Int) {
val tag = "android:switcher:${binding.viewPager.id}:0" val tag = "android:switcher:${binding.viewPager.id}:0"
val chapterFragment = supportFragmentManager.findFragmentByTag(tag) as? ChapterListFragment val chapterFragment = supportFragmentManager.findFragmentByTag(tag) as? ChapterListFragment
@@ -9,7 +9,9 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.book.ContentProcessor
import io.legado.app.model.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.model.localBook.LocalBook import io.legado.app.model.localBook.LocalBook
import io.legado.app.utils.FileDoc import io.legado.app.utils.FileDoc
@@ -21,15 +23,25 @@ import io.legado.app.utils.writeText
class TocViewModel(application: Application) : BaseViewModel(application) { class TocViewModel(application: Application) : BaseViewModel(application) {
var bookUrl: String = "" var bookUrl: String = ""
var bookSource: BookSource? = null
var bookData = MutableLiveData<Book>() var bookData = MutableLiveData<Book>()
var chapterListCallBack: ChapterListCallBack? = null var chapterListCallBack: ChapterListCallBack? = null
var bookMarkCallBack: BookmarkCallBack? = null var bookMarkCallBack: BookmarkCallBack? = null
var searchKey: String? = 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) { fun initBook(bookUrl: String) {
this.bookUrl = bookUrl this.bookUrl = bookUrl
execute { execute {
appDb.bookDao.getBook(bookUrl)?.let { appDb.bookDao.getBook(bookUrl)?.let {
bookSource = appDb.bookSourceDao.getBookSource(it.origin)
bookData.postValue(it) bookData.postValue(it)
} }
} }
@@ -5,6 +5,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.ArrayAdapter
import android.widget.EditText import android.widget.EditText
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope 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.setOnApplyWindowInsetsListenerCompat
import io.legado.app.utils.showHelp import io.legado.app.utils.showHelp
import io.legado.app.utils.viewbindingdelegate.viewBinding 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>(), VMBaseActivity<ActivityReplaceEditBinding, ReplaceEditViewModel>(),
KeyboardToolPop.CallBack { 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( fun startIntent(
context: Context, context: Context,
id: Long = -1, id: Long = -1,
pattern: String? = null, pattern: String? = null,
isRegex: Boolean = false, isRegex: Boolean = false,
scope: String? = null scope: String? = null,
): Intent { isScopeTitle: Boolean = false,
val intent = Intent(context, ReplaceEditActivity::class.java) isScopeContent: Boolean = false
intent.putExtra("id", id) ): Intent = Intent(context, ReplaceEditActivity::class.java).apply {
intent.putExtra("pattern", pattern) putExtra("id", id)
intent.putExtra("isRegex", isRegex) putExtra("pattern", pattern)
intent.putExtra("scope", scope) putExtra("isRegex", isRegex)
return intent putExtra("scope", scope)
putExtra("isScopeTitle", isScopeTitle)
putExtra("isScopeContent", isScopeContent)
} }
} }
override val binding by viewBinding(ActivityReplaceEditBinding::inflate) override val binding by viewBinding(ActivityReplaceEditBinding::inflate)
override val viewModel by viewModels<ReplaceEditViewModel>() override val viewModel by viewModels<ReplaceEditViewModel>()
private val softKeyboardTool by lazy { private val softKeyboardTool by lazy {
KeyboardToolPop(this, lifecycleScope, binding.root, this) KeyboardToolPop(this, lifecycleScope, binding.root, this)
} }
@@ -58,9 +108,12 @@ class ReplaceEditActivity :
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
softKeyboardTool.attachToWindow(window) softKeyboardTool.attachToWindow(window)
initView() initView()
viewModel.initData(intent) { viewModel.initData(intent, ::upReplaceView)
upReplaceView(it) }
}
override fun onDestroy() {
super.onDestroy()
softKeyboardTool.dismiss()
} }
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
@@ -76,66 +129,126 @@ class ReplaceEditActivity :
} }
R.id.menu_copy_rule -> sendToClip(GSON.toJson(getReplaceRule())) R.id.menu_copy_rule -> sendToClip(GSON.toJson(getReplaceRule()))
R.id.menu_paste_rule -> viewModel.pasteRule { R.id.menu_paste_rule -> viewModel.pasteRule(::upReplaceView)
upReplaceView(it)
}
} }
return true return true
} }
override fun onDestroy() { private fun initView() = binding.run {
super.onDestroy() ivHelp.setOnClickListener { showHelp("regexHelp") }
softKeyboardTool.dismiss() ivManageGroups.setOnClickListener { showGroupManageDialog() }
root.setOnApplyWindowInsetsListenerCompat { _, insets ->
softKeyboardTool.initialPadding = insets.imeHeight
insets
}
setupGroupAutoComplete()
} }
private fun initView() { private fun setupGroupAutoComplete() = binding.run {
binding.ivHelp.setOnClickListener { val allGroups = GroupManager.getAllGroups(this@ReplaceEditActivity)
showHelp("regexHelp") 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()
} }
binding.root.setOnApplyWindowInsetsListenerCompat { _, windowInsets ->
softKeyboardTool.initialPadding = windowInsets.imeHeight etGroup.setOnItemClickListener { parent, _, position, _ ->
windowInsets val selected = parent.getItemAtPosition(position) as String
GroupManager.setLastSelectedGroup(this@ReplaceEditActivity, selected)
} }
} }
private fun upReplaceView(replaceRule: ReplaceRule) = binding.run { private fun refreshGroupList() {
etName.setText(replaceRule.name) val allGroups = GroupManager.getAllGroups(this)
etGroup.setText(replaceRule.group) val adapter = ArrayAdapter(
etReplaceRule.setText(replaceRule.pattern) this,
cbUseRegex.isChecked = replaceRule.isRegex android.R.layout.simple_dropdown_item_1line,
etReplaceTo.setText(replaceRule.replacement) allGroups
cbScopeTitle.isChecked = replaceRule.scopeTitle )
cbScopeContent.isChecked = replaceRule.scopeContent binding.etGroup.setAdapter(adapter)
etScope.setText(replaceRule.scope) }
etExcludeScope.setText(replaceRule.excludeScope)
etTimeout.setText(replaceRule.timeoutMillisecond.toString()) private fun saveCurrentGroup() {
val current = binding.etGroup.text.toString().trim()
if (current.isNotBlank()) {
GroupManager.addCustomGroup(this, current)
GroupManager.setLastSelectedGroup(this, current)
}
}
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(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 { private fun getReplaceRule(): ReplaceRule = binding.run {
val replaceRule: ReplaceRule = viewModel.replaceRule ?: ReplaceRule() saveCurrentGroup()
replaceRule.name = etName.text.toString() val rule = viewModel.replaceRule ?: ReplaceRule()
replaceRule.group = etGroup.text.toString()
replaceRule.pattern = etReplaceRule.text.toString() rule.name = etName.text.toString()
replaceRule.isRegex = cbUseRegex.isChecked rule.group = etGroup.text.toString().trim().ifBlank {
replaceRule.replacement = etReplaceTo.text.toString() GroupManager.getLastSelectedGroup(this@ReplaceEditActivity)
replaceRule.scopeTitle = cbScopeTitle.isChecked }
replaceRule.scopeContent = cbScopeContent.isChecked rule.pattern = etReplaceRule.text.toString()
replaceRule.scope = etScope.text.toString() rule.isRegex = cbUseRegex.isChecked
replaceRule.excludeScope = etExcludeScope.text.toString() rule.replacement = etReplaceTo.text.toString()
replaceRule.timeoutMillisecond = etTimeout.text.toString().ifEmpty { "3000" }.toLong() rule.scopeTitle = cbScopeTitle.isChecked
return replaceRule 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>> { override fun helpActions(): List<SelectItem<String>> =
return arrayListOf( listOf(SelectItem("正则教程", "regexHelp"))
SelectItem("正则教程", "regexHelp")
)
}
override fun onHelpActionSelect(action: String) { override fun onHelpActionSelect(action: String) {
when (action) { if (action == "regexHelp") showHelp("regexHelp")
"regexHelp" -> showHelp("regexHelp")
}
} }
override fun sendText(text: String) { override fun sendText(text: String) {
@@ -144,15 +257,12 @@ class ReplaceEditActivity :
if (view is EditText) { if (view is EditText) {
val start = view.selectionStart val start = view.selectionStart
val end = view.selectionEnd val end = view.selectionEnd
//获取EditText的文字
val edit = view.editableText val edit = view.editableText
if (start < 0 || start >= edit.length) { if (start < 0 || start >= edit.length) {
edit.append(text) edit.append(text)
} else { } else {
//光标所在位置插入文字
edit.replace(start, end, text) edit.replace(start, end, text)
} }
} }
} }
} }
@@ -22,11 +22,15 @@ class ReplaceEditViewModel(application: Application) : BaseViewModel(application
val pattern = intent.getStringExtra("pattern") ?: "" val pattern = intent.getStringExtra("pattern") ?: ""
val isRegex = intent.getBooleanExtra("isRegex", false) val isRegex = intent.getBooleanExtra("isRegex", false)
val scope = intent.getStringExtra("scope") val scope = intent.getStringExtra("scope")
val isScopeTitle = intent.getBooleanExtra("isScopeTitle", false)
val isScopeContent = intent.getBooleanExtra("isScopeContent", false)
ReplaceRule( ReplaceRule(
name = pattern, name = pattern,
pattern = pattern, pattern = pattern,
isRegex = isRegex, isRegex = isRegex,
scope = scope scope = scope,
scopeTitle = isScopeTitle,
scopeContent = isScopeContent
) )
} }
}.onFinally { }.onFinally {
@@ -43,15 +43,23 @@
android:id="@+id/til_group" android:id="@+id/til_group"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:hint="@string/group"> android:hint="@string/group">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/et_group" android:id="@+id/et_group"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/text_default"
tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" /> tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
</com.google.android.material.textfield.TextInputLayout> </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 <com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_replace_rule" android:id="@+id/til_replace_rule"
android:layout_width="match_parent" android:layout_width="match_parent"
+8
View File
@@ -42,6 +42,14 @@
android:title="@string/reverse_toc" android:title="@string/reverse_toc"
app:showAsAction="never" /> 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 <item
android:id="@+id/menu_use_replace" android:id="@+id/menu_use_replace"
android:checkable="true" android:checkable="true"