Эх сурвалжийг харах

fix: 修正规则编辑器状态与更新逻辑

HapeLee 4 сар өмнө
parent
commit
e9eee6b72c

+ 5 - 5
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<List<ReplaceRule>>
 
-    @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<List<ReplaceRule>>
 
-    @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<List<ReplaceRule>>
 
-    @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<List<ReplaceRule>>
 
-    @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<List<ReplaceRule>>
 
     // === 未分组 ===
@@ -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<List<ReplaceRule>>
 
-    @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<List<ReplaceRule>>
 
     @Query("select `group` from replace_rules where `group` is not null and `group` <> ''")

+ 8 - 8
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<Long>) =
         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<Long>) =
         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<Long>) =

+ 3 - 1
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

+ 1 - 1
app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt

@@ -144,7 +144,7 @@ class ReplaceRuleActivity : BaseComposeActivity() {
 
                     entry<ReplaceEditRoute> { route ->
                         val viewModel: ReplaceEditViewModel = koinViewModel(
-                            key = "replace_edit_${System.identityHashCode(route)}"
+                            key = "replace_edit_${route.sessionId}"
                         ) { parametersOf(route) }
 
                         ReplaceEditRouteScreen(

+ 28 - 3
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<Long>
+    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<Long> {
+    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<ReplaceRuleItemUi> = emptyList(),
     override val selectedIds: Set<Long> = emptySet(),
     override val searchKey: String = "",
     val sortMode: String = "desc",
+    val selectedGroup: String? = null,
     val interaction: InteractionState = InteractionState()
 ) : ListUiState<ReplaceRuleItemUi> {
     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

+ 20 - 12
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<ReplaceRule?>(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()
+                                }
                             )
                         }
                     )

+ 23 - 5
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<ReplaceRule>): 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<Long>) = viewModelScope.launch { repository.enableByIds(ids) }
     fun disableSelectionByIds(ids: Set<Long>) =

+ 8 - 2
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
             )

+ 33 - 0
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())
+    }
+}