fix:修复词典规则页状态同步问题

This commit is contained in:
HapeLee
2026-06-13 12:39:07 +08:00
parent 3833ca1f7c
commit ecea5607d7
4 changed files with 200 additions and 71 deletions
@@ -10,7 +10,7 @@ class DictRuleActivity : BaseComposeActivity() {
@Composable
override fun Content() {
AppTheme {
DictRuleScreen(onBackClick = { finish() })
DictRuleRouteScreen(onBackClick = { finish() })
}
}
@@ -0,0 +1,67 @@
package io.legado.app.ui.dict.rule
import android.net.Uri
import androidx.compose.runtime.Stable
import io.legado.app.data.entities.DictRule
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
import io.legado.app.ui.widget.components.list.InteractionState
import io.legado.app.ui.widget.components.list.ListUiState
import io.legado.app.ui.widget.components.list.SelectableItem
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
@Stable
data class DictRuleItemUi(
override val id: String,
val urlRule: String,
val showRule: String,
val isEnabled: Boolean,
val rule: DictRule
) : SelectableItem<String>
@Stable
data class DictRuleUiState(
override val items: ImmutableList<DictRuleItemUi> = persistentListOf(),
override val selectedIds: ImmutableSet<String> = persistentSetOf(),
override val searchKey: String = "",
val interaction: InteractionState = InteractionState()
) : ListUiState<DictRuleItemUi> {
override val isSearch: Boolean get() = interaction.isSearchMode
override val isLoading: Boolean get() = interaction.isUploading
}
sealed interface DictRuleIntent {
data class SetSearchMode(val active: Boolean) : DictRuleIntent
data class UpdateSearchQuery(val query: String) : DictRuleIntent
data object ClearSelection : DictRuleIntent
data object SelectAll : DictRuleIntent
data object InvertSelection : DictRuleIntent
data class SetSelection(val ids: Set<String>) : DictRuleIntent
data class ToggleSelection(val id: String) : DictRuleIntent
data object EnableSelection : DictRuleIntent
data object DisableSelection : DictRuleIntent
data object DeleteSelection : DictRuleIntent
data object UploadSelection : DictRuleIntent
data class ExportSelection(val uri: Uri) : DictRuleIntent
data class MoveItem(val from: Int, val to: Int) : DictRuleIntent
data object SaveSortOrder : DictRuleIntent
data class SaveRule(val rule: DictRule, val isNew: Boolean) : DictRuleIntent
data class DeleteRule(val rule: DictRule) : DictRuleIntent
data class SetRuleEnabled(val rule: DictRule, val enabled: Boolean) : DictRuleIntent
data class CopyRule(val rule: DictRule) : DictRuleIntent
data class ImportSource(val text: String) : DictRuleIntent
data object CancelImport : DictRuleIntent
data class ToggleImportSelection(val index: Int) : DictRuleIntent
data class ToggleImportAll(val isSelected: Boolean) : DictRuleIntent
data class UpdateImportItem(val index: Int, val rule: DictRule) : DictRuleIntent
data object SaveImportedRules : DictRuleIntent
}
sealed interface DictRuleEffect
data class DictRuleRenderState(
val uiState: DictRuleUiState,
val importState: BaseImportUiState<DictRule>,
)
@@ -16,7 +16,6 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -43,27 +42,51 @@ import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.icon.AppIcons
import io.legado.app.ui.widget.components.importComponents.BatchImportDialog
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.rules.RuleEditFields
import io.legado.app.ui.widget.components.rules.RuleEditSheet
import io.legado.app.ui.widget.components.rules.RuleListScaffold
import kotlinx.coroutines.flow.Flow
import org.koin.androidx.compose.koinViewModel
import sh.calvin.reorderable.rememberReorderableLazyListState
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun DictRuleScreen(
fun DictRuleRouteScreen(
viewModel: DictRuleViewModel = koinViewModel(),
onBackClick: () -> Unit
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val importState by viewModel.importState.collectAsStateWithLifecycle()
DictRuleScreen(
state = uiState,
importState = importState,
events = viewModel.events,
onIntent = viewModel::onIntent,
onPasteRule = viewModel::pasteRule,
onBackClick = onBackClick,
)
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun DictRuleScreen(
state: DictRuleUiState,
importState: BaseImportUiState<DictRule>,
events: Flow<BaseRuleEvent>,
onIntent: (DictRuleIntent) -> Unit,
onPasteRule: () -> DictRule?,
onBackClick: () -> Unit,
) {
val context = LocalContext.current
val uiState by viewModel.uiState.collectAsState()
val rules = uiState.items
val selectedIds = uiState.selectedIds
val rules = state.items
val selectedIds = state.selectedIds
val inSelectionMode = selectedIds.isNotEmpty()
val listState = rememberLazyListState()
@@ -79,16 +102,15 @@ fun DictRuleScreen(
val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
viewModel.moveItemInList(from.index, to.index)
onIntent(DictRuleIntent.MoveItem(from.index, to.index))
hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
}
val clipboardManager = LocalClipboard.current
val snackbarHostState = remember { SnackbarHostState() }
val importState by viewModel.importState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
events.collect { event ->
when (event) {
is BaseRuleEvent.ShowSnackbar -> {
val result = snackbarHostState.showSnackbar(
@@ -117,7 +139,7 @@ fun DictRuleScreen(
uri?.let {
context.contentResolver.openInputStream(it)?.use { stream ->
val text = stream.reader().readText()
viewModel.importSource(text)
onIntent(DictRuleIntent.ImportSource(text))
}
}
}
@@ -126,7 +148,7 @@ fun DictRuleScreen(
val exportDoc = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/json"),
onResult = { uri ->
uri?.let { viewModel.exportToUri(it, rules, selectedIds) }
uri?.let { onIntent(DictRuleIntent.ExportSelection(it)) }
}
)
@@ -136,7 +158,7 @@ fun DictRuleScreen(
onDismissRequest = { showUrlInput = false },
onConfirm = {
showUrlInput = false
viewModel.importSource(it)
onIntent(DictRuleIntent.ImportSource(it))
}
)
@@ -150,7 +172,7 @@ fun DictRuleScreen(
},
onUpload = {
showExportSheet = false
viewModel.uploadSelectedRules(selectedIds, rules)
onIntent(DictRuleIntent.UploadSelection)
},
allowExtensions = arrayOf("json")
)
@@ -174,11 +196,11 @@ fun DictRuleScreen(
BatchImportDialog(
title = "导入词典规则",
importState = importState,
onDismissRequest = { viewModel.cancelImport() },
onToggleItem = { viewModel.toggleImportSelection(it) },
onToggleAll = { viewModel.toggleImportAll(it) },
onUpdateItem = { index, rule -> viewModel.updateImportItem(index, rule) },
onConfirm = { viewModel.saveImportedRules() },
onDismissRequest = { onIntent(DictRuleIntent.CancelImport) },
onToggleItem = { onIntent(DictRuleIntent.ToggleImportSelection(it)) },
onToggleAll = { onIntent(DictRuleIntent.ToggleImportAll(it)) },
onUpdateItem = { index, rule -> onIntent(DictRuleIntent.UpdateImportItem(index, rule)) },
onConfirm = { onIntent(DictRuleIntent.SaveImportedRules) },
itemTitle = { rule -> rule.name },
itemSubtitle = { rule ->
rule.urlRule.takeIf { it.isNotBlank() }
@@ -187,7 +209,7 @@ fun DictRuleScreen(
LaunchedEffect(reorderableState.isAnyItemDragging) {
if (!reorderableState.isAnyItemDragging) {
viewModel.saveSortOrder()
onIntent(DictRuleIntent.SaveSortOrder)
}
}
@@ -197,7 +219,7 @@ fun DictRuleScreen(
title = stringResource(R.string.delete),
confirmText = stringResource(R.string.ok),
onConfirm = { rule ->
viewModel.delete(rule)
onIntent(DictRuleIntent.DeleteRule(rule))
showDeleteRuleDialog = null
},
dismissText = stringResource(R.string.cancel),
@@ -215,16 +237,12 @@ fun DictRuleScreen(
editingRule = null
},
onSave = { updatedRule ->
if (editingRule == null) {
viewModel.insert(updatedRule)
} else {
viewModel.update(updatedRule)
}
onIntent(DictRuleIntent.SaveRule(updatedRule, isNew = editingRule == null))
showEditSheet = false
editingRule = null
},
onCopy = { viewModel.copyRule(it) },
onPaste = { viewModel.pasteRule() },
onCopy = { onIntent(DictRuleIntent.CopyRule(it)) },
onPaste = onPasteRule,
toFields = { r ->
RuleEditFields(
name = r?.name ?: "",
@@ -247,27 +265,24 @@ fun DictRuleScreen(
RuleListScaffold(
title = "字典规则",
state = uiState,
state = state,
onBackClick = { onBackClick() },
onSearchToggle = { active ->
viewModel.setSearchMode(active)
onIntent(DictRuleIntent.SetSearchMode(active))
},
onSearchQueryChange = { viewModel.setSearchKey(it) },
onSearchQueryChange = { onIntent(DictRuleIntent.UpdateSearchQuery(it)) },
searchPlaceholder = stringResource(R.string.replace_purify_search),
onClearSelection = { viewModel.setSelection(emptySet()) },
onSelectAll = { viewModel.setSelection(rules.map { it.id }.toSet()) },
onClearSelection = { onIntent(DictRuleIntent.ClearSelection) },
onSelectAll = { onIntent(DictRuleIntent.SelectAll) },
onSelectInvert = {
val allIds = rules.map { it.id }.toSet()
viewModel.setSelection(allIds - selectedIds)
onIntent(DictRuleIntent.InvertSelection)
},
selectionSecondaryActions = listOf(
ActionItem(text = stringResource(R.string.enable), onClick = {
viewModel.enableSelectionByIds(selectedIds)
viewModel.setSelection(emptySet())
onIntent(DictRuleIntent.EnableSelection)
}),
ActionItem(text = stringResource(R.string.disable_selection), onClick = {
viewModel.disableSelectionByIds(selectedIds)
viewModel.setSelection(emptySet())
onIntent(DictRuleIntent.DisableSelection)
}),
ActionItem(
text = stringResource(R.string.export),
@@ -275,8 +290,8 @@ fun DictRuleScreen(
),
onDeleteSelected = { ids ->
@Suppress("UNCHECKED_CAST")
viewModel.delSelectionByIds(ids as Set<String>)
viewModel.setSelection(emptySet())
onIntent(DictRuleIntent.SetSelection(ids as Set<String>))
onIntent(DictRuleIntent.DeleteSelection)
},
onAddClick = {
editingRule = null
@@ -311,8 +326,10 @@ fun DictRuleScreen(
isEnabled = item.isEnabled,
isSelected = selectedIds.contains(item.id),
inSelectionMode = inSelectionMode,
onToggleSelection = { viewModel.toggleSelection(item.id) },
onEnabledChange = { enabled -> viewModel.update(item.rule.copy(enabled = enabled)) },
onToggleSelection = { onIntent(DictRuleIntent.ToggleSelection(item.id)) },
onEnabledChange = { enabled ->
onIntent(DictRuleIntent.SetRuleEnabled(item.rule, enabled))
},
onClickEdit = { editingRule = item.rule; showEditSheet = true },
trailingAction = {
SmallPlainButton(
@@ -328,7 +345,7 @@ fun DictRuleScreen(
listState = listState,
items = rules,
selectedIds = selectedIds,
onSelectionChange = { viewModel.setSelection(it) },
onSelectionChange = { onIntent(DictRuleIntent.SetSelection(it)) },
idProvider = { it.id },
modifier = Modifier
.fillMaxHeight()
@@ -1,7 +1,6 @@
package io.legado.app.ui.dict.rule
import android.app.Application
import androidx.compose.runtime.Immutable
import androidx.lifecycle.viewModelScope
import io.legado.app.base.BaseRuleViewModel
import io.legado.app.data.entities.DictRule
@@ -9,8 +8,6 @@ import io.legado.app.data.repository.DictRuleRepository
import io.legado.app.data.repository.UploadRepository
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
import io.legado.app.ui.widget.components.list.InteractionState
import io.legado.app.ui.widget.components.list.ListUiState
import io.legado.app.ui.widget.components.list.SelectableItem
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray
import io.legado.app.utils.fromJsonObject
@@ -19,31 +16,14 @@ import io.legado.app.utils.isJsonArray
import io.legado.app.utils.isJsonObject
import io.legado.app.utils.sendToClip
import io.legado.app.utils.toastOnUi
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Immutable
data class DictRuleItemUi(
override val id: String,
val urlRule: String,
val showRule: String,
val isEnabled: Boolean,
val rule: DictRule
) : SelectableItem<String>
data class DictRuleUiState(
override val items: List<DictRuleItemUi> = emptyList(),
override val selectedIds: Set<String> = emptySet(),
override val searchKey: String = "",
val interaction: InteractionState = InteractionState()
) : ListUiState<DictRuleItemUi> {
override val isSearch: Boolean get() = interaction.isSearchMode
override val isLoading: Boolean get() = interaction.isUploading
}
class DictRuleViewModel(
application: Application,
uploadRepository: UploadRepository
@@ -56,9 +36,65 @@ class DictRuleViewModel(
override val rawDataFlow: Flow<List<DictRule>> = repository.flowAll()
override fun filterData(data: List<DictRule>, key: String): List<DictRule> {
val filtered = if (key.isEmpty()) data
else data.filter { it.name.contains(key, ignoreCase = true) }
fun onIntent(intent: DictRuleIntent) {
when (intent) {
is DictRuleIntent.SetSearchMode -> setSearchMode(intent.active)
is DictRuleIntent.UpdateSearchQuery -> setSearchKey(intent.query)
DictRuleIntent.ClearSelection -> setSelection(emptySet())
DictRuleIntent.SelectAll -> selectAll()
DictRuleIntent.InvertSelection -> invertSelection()
is DictRuleIntent.SetSelection -> setSelection(intent.ids)
is DictRuleIntent.ToggleSelection -> toggleSelection(intent.id)
DictRuleIntent.EnableSelection -> {
enableSelectionByIds(uiState.value.selectedIds)
setSelection(emptySet())
}
DictRuleIntent.DisableSelection -> {
disableSelectionByIds(uiState.value.selectedIds)
setSelection(emptySet())
}
DictRuleIntent.DeleteSelection -> {
delSelectionByIds(uiState.value.selectedIds)
setSelection(emptySet())
}
DictRuleIntent.UploadSelection -> {
val state = uiState.value
uploadSelectedRules(state.selectedIds, state.items)
}
is DictRuleIntent.ExportSelection -> {
val state = uiState.value
exportToUri(intent.uri, state.items, state.selectedIds)
}
is DictRuleIntent.MoveItem -> moveItemInList(intent.from, intent.to)
DictRuleIntent.SaveSortOrder -> saveSortOrder()
is DictRuleIntent.SaveRule -> {
if (intent.isNew) {
insert(intent.rule)
} else {
update(intent.rule)
}
}
is DictRuleIntent.DeleteRule -> delete(intent.rule)
is DictRuleIntent.SetRuleEnabled -> update(intent.rule.copy(enabled = intent.enabled))
is DictRuleIntent.CopyRule -> copyRule(intent.rule)
is DictRuleIntent.ImportSource -> importSource(intent.text)
DictRuleIntent.CancelImport -> cancelImport()
is DictRuleIntent.ToggleImportSelection -> toggleImportSelection(intent.index)
is DictRuleIntent.ToggleImportAll -> toggleImportAll(intent.isSelected)
is DictRuleIntent.UpdateImportItem -> updateImportItem(intent.index, intent.rule)
DictRuleIntent.SaveImportedRules -> saveImportedRules()
}
}
override fun filterData(
data: List<DictRule>,
searchKey: String,
groupFilter: String
): List<DictRule> {
val key = groupFilter.ifEmpty { searchKey }
val filtered = if (key.isEmpty()) data else {
data.filter { it.name.contains(key, ignoreCase = true) }
}
return filtered.sortedBy { it.sortNumber }
}
@@ -70,8 +106,8 @@ class DictRuleViewModel(
importState: BaseImportUiState<DictRule>
): DictRuleUiState {
return DictRuleUiState(
items = items,
selectedIds = selectedIds,
items = items.toImmutableList(),
selectedIds = selectedIds.toImmutableSet(),
searchKey = _searchKey.value,
interaction = InteractionState(
isSearchMode = isSearch,
@@ -141,6 +177,15 @@ class DictRuleViewModel(
}
}
private fun selectAll() {
setSelection(uiState.value.items.map { it.id }.toSet())
}
private fun invertSelection() {
val state = uiState.value
setSelection(state.items.map { it.id }.toSet() - state.selectedIds)
}
fun update(vararg rule: DictRule) = viewModelScope.launch { repository.update(*rule) }
fun insert(vararg rule: DictRule) = viewModelScope.launch { repository.insert(*rule) }
fun delete(vararg dictRule: DictRule) = viewModelScope.launch { repository.delete(*dictRule) }
@@ -162,4 +207,4 @@ class DictRuleViewModel(
null
}
}
}
}