From ecea5607d75d00e8b442129857e8e0466c1163ab Mon Sep 17 00:00:00 2001 From: HapeLee <63206378+HapeLee@users.noreply.github.com> Date: Sat, 13 Jun 2026 12:39:07 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E8=AF=8D?= =?UTF-8?q?=E5=85=B8=E8=A7=84=E5=88=99=E9=A1=B5=E7=8A=B6=E6=80=81=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/ui/dict/rule/DictRuleActivity.kt | 2 +- .../app/ui/dict/rule/DictRuleContract.kt | 67 ++++++++++++ .../legado/app/ui/dict/rule/DictRuleScreen.kt | 101 ++++++++++-------- .../app/ui/dict/rule/DictRuleViewModel.kt | 101 +++++++++++++----- 4 files changed, 200 insertions(+), 71 deletions(-) create mode 100644 app/src/main/java/io/legado/app/ui/dict/rule/DictRuleContract.kt diff --git a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleActivity.kt b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleActivity.kt index 69931665b..33b0387b3 100644 --- a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleActivity.kt +++ b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleActivity.kt @@ -10,7 +10,7 @@ class DictRuleActivity : BaseComposeActivity() { @Composable override fun Content() { AppTheme { - DictRuleScreen(onBackClick = { finish() }) + DictRuleRouteScreen(onBackClick = { finish() }) } } diff --git a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleContract.kt b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleContract.kt new file mode 100644 index 000000000..0a215316c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleContract.kt @@ -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 + +@Stable +data class DictRuleUiState( + override val items: ImmutableList = persistentListOf(), + override val selectedIds: ImmutableSet = persistentSetOf(), + override val searchKey: String = "", + val interaction: InteractionState = InteractionState() +) : ListUiState { + 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) : 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, +) diff --git a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt index cee923bc0..139b954c9 100644 --- a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt +++ b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt @@ -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, + events: Flow, + 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) - viewModel.setSelection(emptySet()) + onIntent(DictRuleIntent.SetSelection(ids as Set)) + 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() diff --git a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleViewModel.kt b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleViewModel.kt index 8fcd20ff7..1d4c34a30 100644 --- a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleViewModel.kt @@ -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 - -data class DictRuleUiState( - override val items: List = emptyList(), - override val selectedIds: Set = emptySet(), - override val searchKey: String = "", - val interaction: InteractionState = InteractionState() -) : ListUiState { - 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> = repository.flowAll() - override fun filterData(data: List, key: String): List { - 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, + searchKey: String, + groupFilter: String + ): List { + 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 ): 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 } } -} \ No newline at end of file +}