fix: 修正规则编辑器状态与更新逻辑
This commit is contained in:
@@ -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` <> ''")
|
||||
|
||||
@@ -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>) =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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>) =
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user