From e9eee6b72c4b5e0428bb6432e430e4a62edecd78 Mon Sep 17 00:00:00 2001 From: HapeLee <63206378+HapeLee@users.noreply.github.com> Date: Wed, 1 Jul 2026 00:35:11 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E8=A7=84=E5=88=99?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=99=A8=E7=8A=B6=E6=80=81=E4=B8=8E=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../io/legado/app/data/dao/ReplaceRuleDao.kt | 10 +++--- .../data/repository/ReplaceRuleRepository.kt | 16 ++++----- .../legado/app/ui/replace/ReplaceEditRoute.kt | 4 ++- .../app/ui/replace/ReplaceRuleActivity.kt | 2 +- .../app/ui/replace/ReplaceRuleContract.kt | 31 +++++++++++++++-- .../app/ui/replace/ReplaceRuleScreen.kt | 32 +++++++++++------- .../app/ui/replace/ReplaceRuleViewModel.kt | 28 +++++++++++++--- .../app/ui/replace/edit/ReplaceEditScreen.kt | 10 ++++-- .../app/ui/replace/ReplaceRuleStateTest.kt | 33 +++++++++++++++++++ 9 files changed, 129 insertions(+), 37 deletions(-) create mode 100644 app/src/test/java/io/legado/app/ui/replace/ReplaceRuleStateTest.kt diff --git a/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt b/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt index e5d535d98..7fb365e98 100644 --- a/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt @@ -47,16 +47,16 @@ interface ReplaceRuleDao { @Query("SELECT * FROM replace_rules WHERE `group` LIKE :key OR name LIKE :key OR pattern LIKE :key OR replacement LIKE :key OR scope LIKE :key ORDER BY name COLLATE NOCASE DESC") fun flowSearchNameDesc(key: String): Flow> - @Query("SELECT * FROM replace_rules WHERE `group` LIKE :key ORDER BY sortOrder ASC") + @Query("SELECT * FROM replace_rules WHERE `group` LIKE '%' || :key || '%' ORDER BY sortOrder ASC") fun flowGroupSearchAsc(key: String): Flow> - @Query("SELECT * FROM replace_rules WHERE `group` LIKE :key ORDER BY sortOrder DESC") + @Query("SELECT * FROM replace_rules WHERE `group` LIKE '%' || :key || '%' ORDER BY sortOrder DESC") fun flowGroupSearchDesc(key: String): Flow> - @Query("SELECT * FROM replace_rules WHERE `group` LIKE :key ORDER BY name COLLATE NOCASE ASC") + @Query("SELECT * FROM replace_rules WHERE `group` LIKE '%' || :key || '%' ORDER BY name COLLATE NOCASE ASC") fun flowGroupSearchNameAsc(key: String): Flow> - @Query("SELECT * FROM replace_rules WHERE `group` LIKE :key ORDER BY name COLLATE NOCASE DESC") + @Query("SELECT * FROM replace_rules WHERE `group` LIKE '%' || :key || '%' ORDER BY name COLLATE NOCASE DESC") fun flowGroupSearchNameDesc(key: String): Flow> // === 未分组 === @@ -75,7 +75,7 @@ interface ReplaceRuleDao { @Query("SELECT * FROM replace_rules where `group` like :key or name like :key or pattern like :key or replacement like :key or scope like :key ORDER BY sortOrder ASC") fun flowSearch(key: String): Flow> - @Query("SELECT * FROM replace_rules where `group` like :key ORDER BY sortOrder ASC") + @Query("SELECT * FROM replace_rules where `group` like '%' || :key || '%' ORDER BY sortOrder ASC") fun flowGroupSearch(key: String): Flow> @Query("select `group` from replace_rules where `group` is not null and `group` <> ''") diff --git a/app/src/main/java/io/legado/app/data/repository/ReplaceRuleRepository.kt b/app/src/main/java/io/legado/app/data/repository/ReplaceRuleRepository.kt index d43497ee7..c8102c7bc 100644 --- a/app/src/main/java/io/legado/app/data/repository/ReplaceRuleRepository.kt +++ b/app/src/main/java/io/legado/app/data/repository/ReplaceRuleRepository.kt @@ -37,6 +37,12 @@ class ReplaceRuleRepository { } } + suspend fun setEnabled(id: Long, enabled: Boolean) { + withContext(Dispatchers.IO) { + appDb.replaceRuleDao.updateEnabled(id, enabled) + } + } + suspend fun insert(vararg rule: ReplaceRule) { withContext(Dispatchers.IO) { appDb.replaceRuleDao.insert(*rule) @@ -125,19 +131,13 @@ class ReplaceRuleRepository { suspend fun enableByIds(ids: Set) = withContext(Dispatchers.IO) { if (ids.isEmpty()) return@withContext - - val rules = appDb.replaceRuleDao.getByIds(ids) - val updated = rules.map { it.copy(isEnabled = true) } - appDb.replaceRuleDao.update(*updated.toTypedArray()) + appDb.replaceRuleDao.updateEnabled(ids.toList(), true) } suspend fun disableByIds(ids: Set) = withContext(Dispatchers.IO) { if (ids.isEmpty()) return@withContext - - val rules = appDb.replaceRuleDao.getByIds(ids) - val updated = rules.map { it.copy(isEnabled = false) } - appDb.replaceRuleDao.update(*updated.toTypedArray()) + appDb.replaceRuleDao.updateEnabled(ids.toList(), false) } suspend fun deleteByIds(ids: Set) = diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceEditRoute.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceEditRoute.kt index 61ebbf550..949a09d01 100644 --- a/app/src/main/java/io/legado/app/ui/replace/ReplaceEditRoute.kt +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceEditRoute.kt @@ -2,6 +2,7 @@ package io.legado.app.ui.replace import androidx.navigation3.runtime.NavKey import kotlinx.serialization.Serializable +import java.util.UUID @Serializable object ReplaceRuleRoute : NavKey @@ -13,5 +14,6 @@ data class ReplaceEditRoute( val isRegex: Boolean = false, val scope: String? = null, val isScopeTitle: Boolean = false, - val isScopeContent: Boolean = false + val isScopeContent: Boolean = false, + val sessionId: String = UUID.randomUUID().toString() ) : NavKey diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt index 5b7742d8e..31e595bdd 100644 --- a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt @@ -144,7 +144,7 @@ class ReplaceRuleActivity : BaseComposeActivity() { entry { route -> val viewModel: ReplaceEditViewModel = koinViewModel( - key = "replace_edit_${System.identityHashCode(route)}" + key = "replace_edit_${route.sessionId}" ) { parametersOf(route) } ReplaceEditRouteScreen( diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleContract.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleContract.kt index 796496c68..32cf2bcc9 100644 --- a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleContract.kt +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleContract.kt @@ -14,14 +14,39 @@ data class ReplaceRuleItemUi( val name: String, val isEnabled: Boolean, val group: String?, - val rule: ReplaceRule -) : SelectableItem + val pattern: String, + val replacement: String, + val scope: String?, + val scopeTitle: Boolean, + val scopeContent: Boolean, + val excludeScope: String?, + val isRegex: Boolean, + val timeoutMillisecond: Long, + val order: Int +) : SelectableItem { + fun toEntity() = ReplaceRule( + id = id, + name = name, + group = group, + pattern = pattern, + replacement = replacement, + scope = scope, + scopeTitle = scopeTitle, + scopeContent = scopeContent, + excludeScope = excludeScope, + isEnabled = isEnabled, + isRegex = isRegex, + timeoutMillisecond = timeoutMillisecond, + order = order + ) +} data class ReplaceRuleUiState( override val items: List = emptyList(), override val selectedIds: Set = emptySet(), override val searchKey: String = "", val sortMode: String = "desc", + val selectedGroup: String? = null, val interaction: InteractionState = InteractionState() ) : ListUiState { override val isSearch: Boolean get() = interaction.isSearchMode @@ -44,7 +69,7 @@ sealed interface ReplaceRuleIntent { data class MoveItem(val from: Int, val to: Int) : ReplaceRuleIntent data object SaveSortOrder : ReplaceRuleIntent data class DeleteRule(val rule: ReplaceRule) : ReplaceRuleIntent - data class SetRuleEnabled(val rule: ReplaceRule, val enabled: Boolean) : ReplaceRuleIntent + data class SetRuleEnabled(val id: Long, val enabled: Boolean) : ReplaceRuleIntent data class CopyRule(val rule: ReplaceRule) : ReplaceRuleIntent data class ImportSource(val text: String) : ReplaceRuleIntent data object CancelImport : ReplaceRuleIntent diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt index 591c49d2f..892ed583e 100644 --- a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.material3.animateFloatingActionButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -127,8 +126,11 @@ fun ReplaceRuleScreen( var showDeleteRuleDialog by remember { mutableStateOf(null) } var showGroupManageSheet by remember { mutableStateOf(false) } - var selectedTabIndex by remember { mutableIntStateOf(0) } val tabItems = remember(groups) { listOf("全部") + groups } + val selectedTabIndex = state.selectedGroup + ?.let(tabItems::indexOf) + ?.takeIf { it >= 0 } + ?: 0 val reorderableState = rememberReorderableLazyListState(listState) { from, to -> onIntent(ReplaceRuleIntent.MoveItem(from.index, to.index)) @@ -231,10 +233,8 @@ fun ReplaceRuleScreen( } } - LaunchedEffect(groups) { - val maxIndex = groups.size - if (selectedTabIndex > maxIndex) { - selectedTabIndex = 0 + LaunchedEffect(groups, state.selectedGroup) { + if (state.selectedGroup != null && state.selectedGroup !in groups) { onIntent(ReplaceRuleIntent.SetGroup("全部")) } } @@ -331,7 +331,6 @@ fun ReplaceRuleScreen( tabTitles = tabItems, selectedTabIndex = selectedTabIndex, onTabSelected = { index -> - selectedTabIndex = index onIntent(ReplaceRuleIntent.SetGroup(tabItems[index])) } ) @@ -422,13 +421,13 @@ fun ReplaceRuleScreen( onIntent(ReplaceRuleIntent.ToggleSelection(ui.id)) }, onEnabledChange = { enabled -> - onIntent(ReplaceRuleIntent.SetRuleEnabled(ui.rule, enabled)) + onIntent(ReplaceRuleIntent.SetRuleEnabled(ui.id, enabled)) }, onClickEdit = { onNavigateToEdit( ReplaceEditRoute( id = ui.id, - pattern = ui.rule.pattern + pattern = ui.pattern ) ) }, @@ -436,15 +435,24 @@ fun ReplaceRuleScreen( dropdownContent = { dismiss -> RoundDropdownMenuItem( text = stringResource(R.string.move_to_top), - onClick = { onIntent(ReplaceRuleIntent.ToTop(ui.rule)); dismiss() } + onClick = { + onIntent(ReplaceRuleIntent.ToTop(ui.toEntity())) + dismiss() + } ) RoundDropdownMenuItem( text = stringResource(R.string.move_to_bottom), - onClick = { onIntent(ReplaceRuleIntent.ToBottom(ui.rule)); dismiss() } + onClick = { + onIntent(ReplaceRuleIntent.ToBottom(ui.toEntity())) + dismiss() + } ) RoundDropdownMenuItem( text = stringResource(R.string.delete), - onClick = { showDeleteRuleDialog = ui.rule; dismiss() } + onClick = { + showDeleteRuleDialog = ui.toEntity() + dismiss() + } ) } ) diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt index 922214185..dead85a1f 100644 --- a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt @@ -87,7 +87,7 @@ class ReplaceRuleViewModel( is ReplaceRuleIntent.MoveItem -> moveItemInList(intent.from, intent.to) ReplaceRuleIntent.SaveSortOrder -> saveSortOrder() is ReplaceRuleIntent.DeleteRule -> delete(intent.rule) - is ReplaceRuleIntent.SetRuleEnabled -> update(intent.rule.copy(isEnabled = intent.enabled)) + is ReplaceRuleIntent.SetRuleEnabled -> setEnabled(intent.id, intent.enabled) is ReplaceRuleIntent.CopyRule -> { /* not implemented for ReplaceRule */ } is ReplaceRuleIntent.ImportSource -> importSource(intent.text) ReplaceRuleIntent.CancelImport -> cancelImport() @@ -160,6 +160,7 @@ class ReplaceRuleViewModel( selectedIds = selectedIds, searchKey = _searchKey.value, sortMode = _sortMode.value, + selectedGroup = _group.value, interaction = InteractionState( isSearchMode = isSearch, isUploading = isUploading || (importState is BaseImportUiState.Loading), @@ -168,8 +169,23 @@ class ReplaceRuleViewModel( ) } - override fun ReplaceRule.toUiItem() = ReplaceRuleItemUi(id, name, isEnabled, group, this) - override fun ruleItemToEntity(item: ReplaceRuleItemUi): ReplaceRule = item.rule + override fun ReplaceRule.toUiItem() = ReplaceRuleItemUi( + id = id, + name = name, + isEnabled = isEnabled, + group = group, + pattern = pattern, + replacement = replacement, + scope = scope, + scopeTitle = scopeTitle, + scopeContent = scopeContent, + excludeScope = excludeScope, + isRegex = isRegex, + timeoutMillisecond = timeoutMillisecond, + order = order + ) + + override fun ruleItemToEntity(item: ReplaceRuleItemUi): ReplaceRule = item.toEntity() override suspend fun generateJson(entities: List): String = GSON.toJson(entities) @@ -248,13 +264,15 @@ class ReplaceRuleViewModel( private fun saveSortOrder() { val currentLocal = _localItems.value ?: return viewModelScope.launch { - repository.moveOrder(currentLocal.map { it.rule }, _sortMode.value == "desc") + repository.moveOrder(currentLocal.map { it.toEntity() }, _sortMode.value == "desc") _localItems.value = null } } - private fun update(vararg rule: ReplaceRule) = viewModelScope.launch { repository.update(*rule) } + private fun setEnabled(id: Long, enabled: Boolean) = + viewModelScope.launch { repository.setEnabled(id, enabled) } + private fun delete(rule: ReplaceRule) = viewModelScope.launch { repository.delete(rule) } fun enableSelectionByIds(ids: Set) = viewModelScope.launch { repository.enableByIds(ids) } fun disableSelectionByIds(ids: Set) = diff --git a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt index c33a93444..95ba1d1a5 100644 --- a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt +++ b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt @@ -47,6 +47,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex @@ -110,6 +111,11 @@ fun ReplaceEditScreen( val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior() var showMenu by remember { mutableStateOf(false) } val isKeyboardVisible by keyboardAsState() + val focusManager = LocalFocusManager.current + val onSave = { + focusManager.clearFocus(force = true) + onIntent(ReplaceEditIntent.Save) + } AppScaffold( modifier = Modifier @@ -127,7 +133,7 @@ fun ReplaceEditScreen( exit = fadeOut() ) { TopBarActionButton( - onClick = { onIntent(ReplaceEditIntent.Save) }, + onClick = onSave, imageVector = Icons.Default.Save, contentDescription = stringResource(R.string.action_save) ) @@ -168,7 +174,7 @@ fun ReplaceEditScreen( visible = !isKeyboardVisible, alignment = Alignment.BottomEnd, ), - onClick = { onIntent(ReplaceEditIntent.Save) }, + onClick = onSave, tooltipText = stringResource(R.string.action_save), icon = Icons.Default.Save ) diff --git a/app/src/test/java/io/legado/app/ui/replace/ReplaceRuleStateTest.kt b/app/src/test/java/io/legado/app/ui/replace/ReplaceRuleStateTest.kt new file mode 100644 index 000000000..75a3d1f6b --- /dev/null +++ b/app/src/test/java/io/legado/app/ui/replace/ReplaceRuleStateTest.kt @@ -0,0 +1,33 @@ +package io.legado.app.ui.replace + +import org.junit.Assert.assertNotEquals +import org.junit.Test + +class ReplaceRuleStateTest { + + @Test + fun editedRuleContentChangesListItemEquality() { + val item = ReplaceRuleItemUi( + id = 1L, + name = "rule", + isEnabled = true, + group = null, + pattern = "before", + replacement = "", + scope = null, + scopeTitle = false, + scopeContent = true, + excludeScope = null, + isRegex = true, + timeoutMillisecond = 3000L, + order = 1 + ) + + assertNotEquals(item, item.copy(pattern = "after")) + } + + @Test + fun newEditRoutesUseDifferentSessions() { + assertNotEquals(ReplaceEditRoute(), ReplaceEditRoute()) + } +}