Parcourir la source

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

HapeLee il y a 5 mois
Parent
commit
ecea5607d7

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

+ 67 - 0
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<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>,
+)

+ 59 - 42
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<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()

+ 73 - 28
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<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
         }
     }
-}
+}