更加强劲的替换管理 #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.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
@@ -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)
}
}
@@ -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 initView() {
binding.ivHelp.setOnClickListener {
showHelp("regexHelp")
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()
}
binding.root.setOnApplyWindowInsetsListenerCompat { _, windowInsets ->
softKeyboardTool.initialPadding = windowInsets.imeHeight
windowInsets
etGroup.setOnItemClickListener { parent, _, position, _ ->
val selected = parent.getItemAtPosition(position) as String
GroupManager.setLastSelectedGroup(this@ReplaceEditActivity, selected)
}
}
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 refreshGroupList() {
val allGroups = GroupManager.getAllGroups(this)
val adapter = ArrayAdapter(
this,
android.R.layout.simple_dropdown_item_1line,
allGroups
)
binding.etGroup.setAdapter(adapter)
}
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 {
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
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>> {
return arrayListOf(
SelectItem("正则教程", "regexHelp")
)
}
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)
}
}
}
}
@@ -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 {