From 0513fc143f18199ea8177cf070c0e769ed0440b7 Mon Sep 17 00:00:00 2001 From: HapeLee <1321903405@qq.com> Date: Fri, 31 Oct 2025 00:51:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E5=8A=A0=E5=BC=BA=E5=8A=B2=E7=9A=84?= =?UTF-8?q?=E6=9B=BF=E6=8D=A2=E7=AE=A1=E7=90=86=20#179?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../io/legado/app/ui/book/toc/TocActivity.kt | 31 ++- .../io/legado/app/ui/book/toc/TocViewModel.kt | 12 + .../ui/replace/edit/ReplaceEditActivity.kt | 230 +++++++++++++----- .../ui/replace/edit/ReplaceEditViewModel.kt | 6 +- .../main/res/layout/activity_replace_edit.xml | 10 +- app/src/main/res/menu/book_toc.xml | 8 + 6 files changed, 233 insertions(+), 64 deletions(-) diff --git a/app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt b/app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt index bb4e664b2..fdfd0ab3a 100644 --- a/app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt +++ b/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(), } } } + 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(), override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { + R.id.menu_replace -> { + val scopes = arrayListOf() + 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(), } } + fun locateToChapter(index: Int) { val tag = "android:switcher:${binding.viewPager.id}:0" val chapterFragment = supportFragmentManager.findFragmentByTag(tag) as? ChapterListFragment diff --git a/app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt b/app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt index 5ca389bbb..3e43e6217 100644 --- a/app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt +++ b/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() 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) } } diff --git a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt index 8ba5071c0..397c73807 100644 --- a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt +++ b/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(), 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 = + (getCustomGroups(context) + presetGroups).distinct() + + fun getCustomGroups(context: Context): List { + val json = prefs(context).getString(KEY_CUSTOM_GROUPS, "[]") ?: "[]" + return GSON.fromJson(json, Array::class.java).toList() + } + + fun saveCustomGroups(context: Context, groups: List) { + 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) { + 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() - 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> { - return arrayListOf( - SelectItem("正则教程", "regexHelp") - ) - } + override fun helpActions(): List> = + 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) } } } - } diff --git a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt index 36dc19f1d..9280aa40d 100644 --- a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt +++ b/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 { diff --git a/app/src/main/res/layout/activity_replace_edit.xml b/app/src/main/res/layout/activity_replace_edit.xml index c5768701b..ea8662250 100644 --- a/app/src/main/res/layout/activity_replace_edit.xml +++ b/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"> - + + + + + +