From da6c02e2ca815cdd373036fa7e75da901b266094 Mon Sep 17 00:00:00 2001 From: HapeLee <63206378+HapeLee@users.noreply.github.com> Date: Sat, 27 Jun 2026 02:16:57 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2=E8=A7=84=E5=88=99=E7=95=8C=E9=9D=A2=E4=B8=BAMVI?= =?UTF-8?q?=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/ui/replace/ReplaceRuleActivity.kt | 6 +- .../app/ui/replace/ReplaceRuleContract.kt | 67 +++++++++ .../app/ui/replace/ReplaceRuleScreen.kt | 142 ++++++++++-------- .../app/ui/replace/ReplaceRuleViewModel.kt | 115 +++++++++----- .../ui/replace/edit/ReplaceEditContract.kt | 47 ++++++ .../app/ui/replace/edit/ReplaceEditScreen.kt | 87 ++++++----- .../ui/replace/edit/ReplaceEditViewModel.kt | 101 +++++++------ 7 files changed, 379 insertions(+), 186 deletions(-) create mode 100644 app/src/main/java/io/legado/app/ui/replace/ReplaceRuleContract.kt create mode 100644 app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditContract.kt diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt index 73045b151..5b7742d8e 100644 --- a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt @@ -19,7 +19,7 @@ import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay import io.legado.app.base.BaseComposeActivity -import io.legado.app.ui.replace.edit.ReplaceEditScreen +import io.legado.app.ui.replace.edit.ReplaceEditRouteScreen import io.legado.app.ui.replace.edit.ReplaceEditViewModel import io.legado.app.ui.theme.AppTheme import kotlinx.serialization.json.Json @@ -136,7 +136,7 @@ class ReplaceRuleActivity : BaseComposeActivity() { }, entryProvider = entryProvider { entry { - ReplaceRuleScreen( + ReplaceRuleRouteScreen( onBackClick = { finish() }, onNavigateToEdit = { route -> backStack.add(route) } ) @@ -147,7 +147,7 @@ class ReplaceRuleActivity : BaseComposeActivity() { key = "replace_edit_${System.identityHashCode(route)}" ) { parametersOf(route) } - ReplaceEditScreen( + ReplaceEditRouteScreen( viewModel = viewModel, onBack = { if (backStack.size > 1) { diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleContract.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleContract.kt new file mode 100644 index 000000000..796496c68 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleContract.kt @@ -0,0 +1,67 @@ +package io.legado.app.ui.replace + +import android.net.Uri +import androidx.compose.runtime.Immutable +import io.legado.app.data.entities.ReplaceRule +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 + +@Immutable +data class ReplaceRuleItemUi( + override val id: Long, + val name: String, + val isEnabled: Boolean, + val group: String?, + val rule: ReplaceRule +) : SelectableItem + +data class ReplaceRuleUiState( + override val items: List = emptyList(), + override val selectedIds: Set = emptySet(), + override val searchKey: String = "", + val sortMode: String = "desc", + val interaction: InteractionState = InteractionState() +) : ListUiState { + override val isSearch: Boolean get() = interaction.isSearchMode + override val isLoading: Boolean get() = interaction.isUploading +} + +sealed interface ReplaceRuleIntent { + data class SetSearchMode(val active: Boolean) : ReplaceRuleIntent + data class UpdateSearchQuery(val query: String) : ReplaceRuleIntent + data object ClearSelection : ReplaceRuleIntent + data object SelectAll : ReplaceRuleIntent + data object InvertSelection : ReplaceRuleIntent + data class SetSelection(val ids: Set) : ReplaceRuleIntent + data class ToggleSelection(val id: Long) : ReplaceRuleIntent + data object EnableSelection : ReplaceRuleIntent + data object DisableSelection : ReplaceRuleIntent + data object DeleteSelection : ReplaceRuleIntent + data object UploadSelection : ReplaceRuleIntent + data class ExportSelection(val uri: Uri) : 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 CopyRule(val rule: ReplaceRule) : ReplaceRuleIntent + data class ImportSource(val text: String) : ReplaceRuleIntent + data object CancelImport : ReplaceRuleIntent + data class ToggleImportSelection(val index: Int) : ReplaceRuleIntent + data class ToggleImportAll(val isSelected: Boolean) : ReplaceRuleIntent + data class UpdateImportItem(val index: Int, val rule: ReplaceRule) : ReplaceRuleIntent + data object SaveImportedRules : ReplaceRuleIntent + // ReplaceRule-specific + data class SetGroup(val groupName: String?) : ReplaceRuleIntent + data class SetSortMode(val mode: String) : ReplaceRuleIntent + data class ToTop(val rule: ReplaceRule) : ReplaceRuleIntent + data class ToBottom(val rule: ReplaceRule) : ReplaceRuleIntent + data class TopSelectByIds(val ids: Set) : ReplaceRuleIntent + data class BottomSelectByIds(val ids: Set) : ReplaceRuleIntent + data class AddGroup(val group: String) : ReplaceRuleIntent + data class DeleteGroup(val group: String) : ReplaceRuleIntent + data class UpGroup(val oldGroup: String, val newGroup: String?) : ReplaceRuleIntent +} + +sealed interface ReplaceRuleEffect diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt index 858661169..b3f524e08 100644 --- a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt @@ -6,7 +6,6 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -15,7 +14,6 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.FileOpen import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -57,7 +55,6 @@ import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.card.ReorderableSelectionItem import io.legado.app.ui.widget.components.divider.PillDivider import io.legado.app.ui.widget.components.filePicker.FilePickerSheet -import io.legado.app.ui.widget.components.icon.AppIcon import io.legado.app.ui.widget.components.importComponents.BaseImportUiState import io.legado.app.ui.widget.components.importComponents.BatchImportDialog import io.legado.app.ui.widget.components.importComponents.SourceInputDialog @@ -66,16 +63,47 @@ import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem import io.legado.app.ui.widget.components.rules.RuleListScaffold import io.legado.app.ui.widget.components.tabRow.AppTabRow import io.legado.app.ui.widget.components.text.AppText +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import sh.calvin.reorderable.rememberReorderableLazyListState -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, +@OptIn( + ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, + ExperimentalMaterial3ExpressiveApi::class +) +@Composable +fun ReplaceRuleRouteScreen( + viewModel: ReplaceRuleViewModel = koinViewModel(), + onBackClick: () -> Unit, + onNavigateToEdit: (ReplaceEditRoute) -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val importState by viewModel.importState.collectAsStateWithLifecycle() + val groups by viewModel.allGroups.collectAsStateWithLifecycle() + + ReplaceRuleScreen( + state = uiState, + importState = importState, + events = viewModel.events, + groups = groups, + onIntent = viewModel::onIntent, + onBackClick = onBackClick, + onNavigateToEdit = onNavigateToEdit, + ) +} + +@OptIn( + ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class ) @Composable fun ReplaceRuleScreen( - viewModel: ReplaceRuleViewModel = koinViewModel(), + state: ReplaceRuleUiState, + importState: BaseImportUiState, + events: Flow, + groups: List, + onIntent: (ReplaceRuleIntent) -> Unit, onBackClick: () -> Unit, onNavigateToEdit: (ReplaceEditRoute) -> Unit, ) { @@ -83,10 +111,8 @@ fun ReplaceRuleScreen( val context = LocalContext.current val scope = rememberCoroutineScope() - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val rules = uiState.items - val groups by viewModel.allGroups.collectAsStateWithLifecycle() - val selectedIds = uiState.selectedIds + val rules = state.items + val selectedIds = state.selectedIds val inSelectionMode = selectedIds.isNotEmpty() val listState = rememberLazyListState() @@ -101,18 +127,16 @@ fun ReplaceRuleScreen( var showDeleteRuleDialog by remember { mutableStateOf(null) } var showGroupManageSheet by remember { mutableStateOf(false) } - val importState by viewModel.importState.collectAsStateWithLifecycle() - var selectedTabIndex by remember { mutableIntStateOf(0) } val tabItems = remember(groups) { listOf("全部") + groups } val reorderableState = rememberReorderableLazyListState(listState) { from, to -> - viewModel.moveItemInList(from.index, to.index) + onIntent(ReplaceRuleIntent.MoveItem(from.index, to.index)) hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) } - val canReorder = remember(uiState.sortMode) { - uiState.sortMode == "asc" || uiState.sortMode == "desc" + val canReorder = remember(state.sortMode) { + state.sortMode == "asc" || state.sortMode == "desc" } val importDoc = rememberLauncherForActivityResult( @@ -121,7 +145,7 @@ fun ReplaceRuleScreen( uri?.let { context.contentResolver.openInputStream(it)?.use { stream -> val text = stream.reader().readText() - viewModel.importSource(text) + onIntent(ReplaceRuleIntent.ImportSource(text)) } } } @@ -130,7 +154,7 @@ fun ReplaceRuleScreen( val exportDoc = rememberLauncherForActivityResult( contract = ActivityResultContracts.CreateDocument("application/json"), onResult = { uri -> - uri?.let { viewModel.exportToUri(it, rules, selectedIds) } + uri?.let { onIntent(ReplaceRuleIntent.ExportSelection(it)) } } ) @@ -140,7 +164,7 @@ fun ReplaceRuleScreen( onDismissRequest = { showUrlInput = false }, onConfirm = { showUrlInput = false - viewModel.importSource(it) + onIntent(ReplaceRuleIntent.ImportSource(it)) } ) @@ -168,7 +192,7 @@ fun ReplaceRuleScreen( }, onUpload = { showExportSheet = false - viewModel.uploadSelectedRules(selectedIds, rules) + onIntent(ReplaceRuleIntent.UploadSelection) }, allowExtensions = arrayOf("json") ) @@ -176,11 +200,11 @@ fun ReplaceRuleScreen( BatchImportDialog( title = stringResource(R.string.import_replace_rule), 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(ReplaceRuleIntent.CancelImport) }, + onToggleItem = { onIntent(ReplaceRuleIntent.ToggleImportSelection(it)) }, + onToggleAll = { onIntent(ReplaceRuleIntent.ToggleImportAll(it)) }, + onUpdateItem = { index, rule -> onIntent(ReplaceRuleIntent.UpdateImportItem(index, rule)) }, + onConfirm = { onIntent(ReplaceRuleIntent.SaveImportedRules) }, topBarActions = {}, itemTitle = { rule -> rule.name }, itemSubtitle = { rule -> @@ -189,7 +213,7 @@ fun ReplaceRuleScreen( ) if (importState is BaseImportUiState.Loading) { - Dialog(onDismissRequest = { viewModel.cancelImport() }) { LoadingIndicator() } + Dialog(onDismissRequest = { onIntent(ReplaceRuleIntent.CancelImport) }) { LoadingIndicator() } } LaunchedEffect(importState) { @@ -197,13 +221,13 @@ fun ReplaceRuleScreen( scope.launch { snackbarHostState.showSnackbar(it.msg) } - viewModel.cancelImport() + onIntent(ReplaceRuleIntent.CancelImport) } } LaunchedEffect(reorderableState.isAnyItemDragging) { if (!reorderableState.isAnyItemDragging) { - viewModel.saveSortOrder() + onIntent(ReplaceRuleIntent.SaveSortOrder) } } @@ -211,12 +235,12 @@ fun ReplaceRuleScreen( val maxIndex = groups.size if (selectedTabIndex > maxIndex) { selectedTabIndex = 0 - viewModel.setGroup("全部") + onIntent(ReplaceRuleIntent.SetGroup("全部")) } } LaunchedEffect(Unit) { - viewModel.events.collect { event -> + events.collect { event -> when (event) { is BaseRuleEvent.ShowSnackbar -> { val result = snackbarHostState.showSnackbar( @@ -244,8 +268,8 @@ fun ReplaceRuleScreen( show = showGroupManageSheet, groups = groups, onDismissRequest = { showGroupManageSheet = false }, - onUpdateGroup = { old, new -> viewModel.upGroup(old, new) }, - onDeleteGroup = { viewModel.delGroup(it) } + onUpdateGroup = { old, new -> onIntent(ReplaceRuleIntent.UpGroup(old, new)) }, + onDeleteGroup = { onIntent(ReplaceRuleIntent.DeleteGroup(it)) } ) @@ -255,7 +279,7 @@ fun ReplaceRuleScreen( title = stringResource(R.string.delete), confirmText = stringResource(R.string.ok), onConfirm = { rule -> - viewModel.delete(rule) + onIntent(ReplaceRuleIntent.DeleteRule(rule)) showDeleteRuleDialog = null }, dismissText = stringResource(R.string.cancel), @@ -264,42 +288,30 @@ fun ReplaceRuleScreen( RuleListScaffold( title = "替换规则", - state = uiState, + state = state, onBackClick = { onBackClick() }, - onSearchToggle = { viewModel.setSearchMode(!uiState.isSearch) }, - onSearchQueryChange = { viewModel.setSearchKey(it) }, + onSearchToggle = { onIntent(ReplaceRuleIntent.SetSearchMode(!state.isSearch)) }, + onSearchQueryChange = { onIntent(ReplaceRuleIntent.UpdateSearchQuery(it)) }, searchPlaceholder = stringResource(R.string.replace_purify_search), - onClearSelection = { viewModel.setSelection(emptySet()) }, - onSelectAll = { viewModel.setSelection(rules.map { it.id }.toSet()) }, - onSelectInvert = { viewModel.setSelection(rules.map { it.id }.toSet() - selectedIds) }, + onClearSelection = { onIntent(ReplaceRuleIntent.ClearSelection) }, + onSelectAll = { onIntent(ReplaceRuleIntent.SelectAll) }, + onSelectInvert = { onIntent(ReplaceRuleIntent.InvertSelection) }, selectionSecondaryActions = listOf( ActionItem( text = stringResource(R.string.enable), - onClick = { - viewModel.enableSelectionByIds(selectedIds) - viewModel.setSelection(emptySet()) - } + onClick = { onIntent(ReplaceRuleIntent.EnableSelection) } ), ActionItem( text = stringResource(R.string.disable_selection), - onClick = { - viewModel.disableSelectionByIds(selectedIds) - viewModel.setSelection(emptySet()) - } + onClick = { onIntent(ReplaceRuleIntent.DisableSelection) } ), ActionItem( text = stringResource(R.string.to_top), - onClick = { - viewModel.topSelectByIds(selectedIds) - viewModel.setSelection(emptySet()) - } + onClick = { onIntent(ReplaceRuleIntent.TopSelectByIds(selectedIds)) } ), ActionItem( text = stringResource(R.string.to_bottom), - onClick = { - viewModel.bottomSelectByIds(selectedIds) - viewModel.setSelection(emptySet()) - } + onClick = { onIntent(ReplaceRuleIntent.BottomSelectByIds(selectedIds)) } ), ActionItem( text = stringResource(R.string.export), @@ -308,8 +320,8 @@ fun ReplaceRuleScreen( ), onDeleteSelected = { ids -> @Suppress("UNCHECKED_CAST") - viewModel.delSelectionByIds(ids as Set) - viewModel.setSelection(emptySet()) + onIntent(ReplaceRuleIntent.SetSelection(ids as Set)) + onIntent(ReplaceRuleIntent.DeleteSelection) }, bottomContent = { if (tabItems.size > 1) { @@ -320,7 +332,7 @@ fun ReplaceRuleScreen( selectedTabIndex = selectedTabIndex, onTabSelected = { index -> selectedTabIndex = index - viewModel.setGroup(tabItems[index]) + onIntent(ReplaceRuleIntent.SetGroup(tabItems[index])) } ) } @@ -355,16 +367,16 @@ fun ReplaceRuleScreen( PillDivider() RoundDropdownMenuItem( text = "旧的在前", - onClick = { viewModel.setSortMode("asc"); dismiss() } + onClick = { onIntent(ReplaceRuleIntent.SetSortMode("asc")); dismiss() } ) RoundDropdownMenuItem( text = "新的在前", - onClick = { viewModel.setSortMode("desc"); dismiss() } + onClick = { onIntent(ReplaceRuleIntent.SetSortMode("desc")); dismiss() } ) RoundDropdownMenuItem( text = "名称升序", onClick = { - viewModel.setSortMode("name_asc") + onIntent(ReplaceRuleIntent.SetSortMode("name_asc")) dismiss() scope.launch { snackbarHostState.showSnackbar("当前排序模式下禁用拖动") @@ -374,7 +386,7 @@ fun ReplaceRuleScreen( RoundDropdownMenuItem( text = "名称降序", onClick = { - viewModel.setSortMode("name_desc") + onIntent(ReplaceRuleIntent.SetSortMode("name_desc")) dismiss() scope.launch { snackbarHostState.showSnackbar("当前排序模式下禁用拖动") @@ -407,10 +419,10 @@ fun ReplaceRuleScreen( inSelectionMode = inSelectionMode, canReorder = canReorder, onToggleSelection = { - viewModel.toggleSelection(ui.id) + onIntent(ReplaceRuleIntent.ToggleSelection(ui.id)) }, onEnabledChange = { enabled -> - viewModel.update(ui.rule.copy(isEnabled = enabled)) + onIntent(ReplaceRuleIntent.SetRuleEnabled(ui.rule, enabled)) }, onClickEdit = { onNavigateToEdit( @@ -424,11 +436,11 @@ fun ReplaceRuleScreen( dropdownContent = { dismiss -> RoundDropdownMenuItem( text = "移至顶部", - onClick = { viewModel.toTop(ui.rule); dismiss() } + onClick = { onIntent(ReplaceRuleIntent.ToTop(ui.rule)); dismiss() } ) RoundDropdownMenuItem( text = "移至底部", - onClick = { viewModel.toBottom(ui.rule); dismiss() } + onClick = { onIntent(ReplaceRuleIntent.ToBottom(ui.rule)); dismiss() } ) RoundDropdownMenuItem( text = "删除", @@ -443,7 +455,7 @@ fun ReplaceRuleScreen( listState = listState, items = rules, selectedIds = selectedIds, - onSelectionChange = { viewModel.setSelection(it) }, + onSelectionChange = { onIntent(ReplaceRuleIntent.SetSelection(it)) }, idProvider = { it.id }, modifier = Modifier .fillMaxHeight() diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt index 248d4ec26..922214185 100644 --- a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt @@ -1,7 +1,6 @@ package io.legado.app.ui.replace import android.app.Application -import androidx.compose.runtime.Immutable import androidx.lifecycle.viewModelScope import io.legado.app.base.BaseRuleEvent import io.legado.app.base.BaseRuleViewModel @@ -14,8 +13,6 @@ import io.legado.app.data.repository.UploadRepository import io.legado.app.help.ReplaceAnalyzer 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.getPrefString import io.legado.app.utils.isJsonArray @@ -37,27 +34,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -@Immutable -data class ReplaceRuleItemUi( - override val id: Long, - val name: String, - val isEnabled: Boolean, - val group: String?, - val rule: ReplaceRule -) : SelectableItem - -data class ReplaceRuleUiState( - override val items: List = emptyList(), - override val selectedIds: Set = emptySet(), - override val searchKey: String = "", - val sortMode: String = "desc", - val groups: List = emptyList(), - val interaction: InteractionState = InteractionState() -) : ListUiState { - override val isSearch: Boolean get() = interaction.isSearchMode - override val isLoading: Boolean get() = interaction.isUploading -} - class ReplaceRuleViewModel( application: Application, uploadRepository: UploadRepository @@ -68,6 +44,7 @@ class ReplaceRuleViewModel( ) { private val repository = ReplaceRuleRepository() private val _sortMode = MutableStateFlow(context.getPrefString(PreferKey.replaceSortMode, "desc") ?: "desc") + val sortMode = _sortMode.asStateFlow() private val _group = MutableStateFlow(null) val group = _group.asStateFlow() @@ -78,7 +55,60 @@ class ReplaceRuleViewModel( initialValue = emptyList() ) - fun setGroup(groupName: String?) { + fun onIntent(intent: ReplaceRuleIntent) { + when (intent) { + is ReplaceRuleIntent.SetSearchMode -> setSearchMode(intent.active) + is ReplaceRuleIntent.UpdateSearchQuery -> setSearchKey(intent.query) + ReplaceRuleIntent.ClearSelection -> setSelection(emptySet()) + ReplaceRuleIntent.SelectAll -> selectAll() + ReplaceRuleIntent.InvertSelection -> invertSelection() + is ReplaceRuleIntent.SetSelection -> setSelection(intent.ids) + is ReplaceRuleIntent.ToggleSelection -> toggleSelection(intent.id) + ReplaceRuleIntent.EnableSelection -> { + enableSelectionByIds(uiState.value.selectedIds) + setSelection(emptySet()) + } + ReplaceRuleIntent.DisableSelection -> { + disableSelectionByIds(uiState.value.selectedIds) + setSelection(emptySet()) + } + ReplaceRuleIntent.DeleteSelection -> { + delSelectionByIds(uiState.value.selectedIds) + setSelection(emptySet()) + } + ReplaceRuleIntent.UploadSelection -> { + val state = uiState.value + uploadSelectedRules(state.selectedIds, state.items) + } + is ReplaceRuleIntent.ExportSelection -> { + val state = uiState.value + exportToUri(intent.uri, state.items, state.selectedIds) + } + 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.CopyRule -> { /* not implemented for ReplaceRule */ } + is ReplaceRuleIntent.ImportSource -> importSource(intent.text) + ReplaceRuleIntent.CancelImport -> cancelImport() + is ReplaceRuleIntent.ToggleImportSelection -> toggleImportSelection(intent.index) + is ReplaceRuleIntent.ToggleImportAll -> toggleImportAll(intent.isSelected) + is ReplaceRuleIntent.UpdateImportItem -> updateImportItem(intent.index, intent.rule) + ReplaceRuleIntent.SaveImportedRules -> saveImportedRules() + // ReplaceRule-specific + is ReplaceRuleIntent.SetGroup -> setGroup(intent.groupName) + is ReplaceRuleIntent.SetSortMode -> setSortMode(intent.mode) + is ReplaceRuleIntent.ToTop -> toTop(intent.rule) + is ReplaceRuleIntent.ToBottom -> toBottom(intent.rule) + is ReplaceRuleIntent.TopSelectByIds -> topSelectByIds(intent.ids) + is ReplaceRuleIntent.BottomSelectByIds -> bottomSelectByIds(intent.ids) + is ReplaceRuleIntent.AddGroup -> addGroup(intent.group) + is ReplaceRuleIntent.DeleteGroup -> delGroup(intent.group) + is ReplaceRuleIntent.UpGroup -> upGroup(intent.oldGroup, intent.newGroup) + } + } + + private fun setGroup(groupName: String?) { _group.value = if (groupName == "全部" || groupName.isNullOrBlank()) { null } else { @@ -129,6 +159,7 @@ class ReplaceRuleViewModel( items = items, selectedIds = selectedIds, searchKey = _searchKey.value, + sortMode = _sortMode.value, interaction = InteractionState( isSearchMode = isSearch, isUploading = isUploading || (importState is BaseImportUiState.Loading), @@ -209,12 +240,12 @@ class ReplaceRuleViewModel( return if (comparator != null) rules.sortedWith(comparator) else rules } - fun setSortMode(mode: String) { + private fun setSortMode(mode: String) { _sortMode.value = mode context.putPrefString(PreferKey.replaceSortMode, mode) } - fun saveSortOrder() { + private fun saveSortOrder() { val currentLocal = _localItems.value ?: return viewModelScope.launch { repository.moveOrder(currentLocal.map { it.rule }, _sortMode.value == "desc") @@ -223,8 +254,8 @@ class ReplaceRuleViewModel( } - fun update(vararg rule: ReplaceRule) = viewModelScope.launch { repository.update(*rule) } - fun delete(rule: ReplaceRule) = viewModelScope.launch { repository.delete(rule) } + private fun update(vararg rule: ReplaceRule) = viewModelScope.launch { repository.update(*rule) } + private fun delete(rule: ReplaceRule) = viewModelScope.launch { repository.delete(rule) } fun enableSelectionByIds(ids: Set) = viewModelScope.launch { repository.enableByIds(ids) } fun disableSelectionByIds(ids: Set) = viewModelScope.launch { repository.disableByIds(ids) } @@ -234,22 +265,30 @@ class ReplaceRuleViewModel( _selectedIds.update { it - ids } } - fun addGroup(group: String) = viewModelScope.launch { repository.addGroup(group) } - fun delGroup(group: String) = viewModelScope.launch { repository.delGroup(group) } + private fun selectAll() { + setSelection(uiState.value.items.map { it.id }.toSet()) + } - fun toTop(rule: ReplaceRule) = + private fun invertSelection() { + val state = uiState.value + setSelection(state.items.map { it.id }.toSet() - state.selectedIds) + } + + private fun addGroup(group: String) = viewModelScope.launch { repository.addGroup(group) } + private fun delGroup(group: String) = viewModelScope.launch { repository.delGroup(group) } + + private fun toTop(rule: ReplaceRule) = viewModelScope.launch { repository.toTop(rule, _sortMode.value == "desc") } - fun toBottom(rule: ReplaceRule) = + private fun toBottom(rule: ReplaceRule) = viewModelScope.launch { repository.toBottom(rule, _sortMode.value == "desc") } - fun upOrder() = viewModelScope.launch { repository.upOrder() } - fun topSelectByIds(ids: Set) = + private fun topSelectByIds(ids: Set) = viewModelScope.launch { repository.topByIds(ids, _sortMode.value == "desc") } - fun bottomSelectByIds(ids: Set) = + private fun bottomSelectByIds(ids: Set) = viewModelScope.launch { repository.bottomByIds(ids, _sortMode.value == "desc") } - fun upGroup(oldGroup: String, newGroup: String?) = + private fun upGroup(oldGroup: String, newGroup: String?) = viewModelScope.launch { repository.upGroup(oldGroup, newGroup) } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditContract.kt b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditContract.kt new file mode 100644 index 000000000..6f7be8ab1 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditContract.kt @@ -0,0 +1,47 @@ +package io.legado.app.ui.replace.edit + +import androidx.compose.runtime.Stable + +@Stable +data class ReplaceEditUiState( + val id: Long = 0, + val name: String = "", + val group: String = "默认", + val pattern: String = "", + val replacement: String = "", + val isRegex: Boolean = false, + val scope: String = "", + val scopeTitle: Boolean = false, + val scopeContent: Boolean = false, + val excludeScope: String = "", + val timeout: String = "3000", + val allGroups: List = emptyList(), + val showGroupDialog: Boolean = false, + val activeField: ActiveField = ActiveField.None +) + +enum class ActiveField { Name, None, Pattern, Replacement, Scope, Exclude } + +sealed interface ReplaceEditIntent { + data class OnNameChange(val value: String) : ReplaceEditIntent + data class OnPatternChange(val value: String) : ReplaceEditIntent + data class OnReplacementChange(val value: String) : ReplaceEditIntent + data class OnScopeChange(val value: String) : ReplaceEditIntent + data class OnExcludeScopeChange(val value: String) : ReplaceEditIntent + data class OnGroupChange(val value: String) : ReplaceEditIntent + data class OnRegexChange(val value: Boolean) : ReplaceEditIntent + data class OnScopeTitleChange(val value: Boolean) : ReplaceEditIntent + data class OnScopeContentChange(val value: Boolean) : ReplaceEditIntent + data class OnTimeoutChange(val value: String) : ReplaceEditIntent + data class SetActiveField(val field: ActiveField) : ReplaceEditIntent + data class InsertTextAtCursor(val text: String) : ReplaceEditIntent + data class ToggleGroupDialog(val show: Boolean) : ReplaceEditIntent + data class DeleteGroups(val groups: List) : ReplaceEditIntent + data object CopyRule : ReplaceEditIntent + data object PasteRule : ReplaceEditIntent + data object Save : ReplaceEditIntent +} + +sealed interface ReplaceEditEffect { + data object NavigateBack : ReplaceEditEffect +} diff --git a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt index 9b5316134..ba5812fea 100644 --- a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt +++ b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt @@ -35,8 +35,8 @@ import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.animateFloatingActionButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -65,6 +65,9 @@ import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults import io.legado.app.ui.widget.components.topbar.TopBarActionButton import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest import org.koin.androidx.compose.koinViewModel @Composable @@ -75,12 +78,35 @@ fun keyboardAsState(): State { @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable -fun ReplaceEditScreen( +fun ReplaceEditRouteScreen( + viewModel: ReplaceEditViewModel = koinViewModel(), onBack: () -> Unit, onSaveSuccess: () -> Unit, - viewModel: ReplaceEditViewModel = koinViewModel() ) { - val state by viewModel.uiState.collectAsState() + val state by viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + viewModel.effects.collectLatest { effect -> + when (effect) { + ReplaceEditEffect.NavigateBack -> onSaveSuccess() + } + } + } + + ReplaceEditScreen( + state = state, + onIntent = viewModel::onIntent, + onBack = onBack, + ) +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun ReplaceEditScreen( + state: ReplaceEditUiState, + onIntent: (ReplaceEditIntent) -> Unit, + onBack: () -> Unit, +) { val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior() var showMenu by remember { mutableStateOf(false) } val isKeyboardVisible by keyboardAsState() @@ -101,9 +127,7 @@ fun ReplaceEditScreen( exit = fadeOut() ) { TopBarActionButton( - onClick = { - viewModel.save(onSaveSuccess) - }, + onClick = { onIntent(ReplaceEditIntent.Save) }, imageVector = Icons.Default.Save, contentDescription = "保存" ) @@ -121,14 +145,14 @@ fun ReplaceEditScreen( text = "复制规则", onClick = { showMenu = false - viewModel.copyRule() + onIntent(ReplaceEditIntent.CopyRule) } ) RoundDropdownMenuItem( text = "粘贴规则", onClick = { showMenu = false - viewModel.pasteRule(onSuccess = {}) + onIntent(ReplaceEditIntent.PasteRule) } ) } @@ -144,7 +168,7 @@ fun ReplaceEditScreen( visible = !isKeyboardVisible, alignment = Alignment.BottomEnd, ), - onClick = { viewModel.save(onSaveSuccess) }, + onClick = { onIntent(ReplaceEditIntent.Save) }, tooltipText = stringResource(R.string.action_save), icon = Icons.Default.Save ) @@ -168,7 +192,7 @@ fun ReplaceEditScreen( .zIndex(1f) ) { QuickInputBar( - onInsert = { text -> viewModel.insertTextAtCursor(text) } + onInsert = { text -> onIntent(ReplaceEditIntent.InsertTextAtCursor(text)) } ) } Column( @@ -181,13 +205,12 @@ fun ReplaceEditScreen( AppTextField( value = state.name, - onValueChange = viewModel::onNameChange, + onValueChange = { onIntent(ReplaceEditIntent.OnNameChange(it)) }, label = "规则名称", modifier = Modifier .fillMaxWidth() .onFocusChanged { - if (it.isFocused) viewModel.activeField = - ReplaceEditViewModel.ActiveField.Name + if (it.isFocused) onIntent(ReplaceEditIntent.SetActiveField(ActiveField.Name)) }, singleLine = true ) @@ -195,33 +218,31 @@ fun ReplaceEditScreen( GroupSelector( currentGroup = state.group, allGroups = state.allGroups, - onGroupChange = viewModel::onGroupChange, - onManageClick = { viewModel.toggleGroupDialog(true) } + onGroupChange = { onIntent(ReplaceEditIntent.OnGroupChange(it)) }, + onManageClick = { onIntent(ReplaceEditIntent.ToggleGroupDialog(true)) } ) AppTextField( value = state.pattern, - onValueChange = viewModel::onPatternChange, + onValueChange = { onIntent(ReplaceEditIntent.OnPatternChange(it)) }, label = "匹配规则", placeholder = { AppText("输入正则表达式或关键字") }, modifier = Modifier .fillMaxWidth() .onFocusChanged { - if (it.isFocused) viewModel.activeField = - ReplaceEditViewModel.ActiveField.Pattern + if (it.isFocused) onIntent(ReplaceEditIntent.SetActiveField(ActiveField.Pattern)) } ) AppTextField( value = state.replacement, - onValueChange = viewModel::onReplacementChange, + onValueChange = { onIntent(ReplaceEditIntent.OnReplacementChange(it)) }, label = "替换为", placeholder = { AppText("输入替换内容或捕获组") }, modifier = Modifier .fillMaxWidth() .onFocusChanged { - if (it.isFocused) viewModel.activeField = - ReplaceEditViewModel.ActiveField.Replacement + if (it.isFocused) onIntent(ReplaceEditIntent.SetActiveField(ActiveField.Replacement)) } ) @@ -234,7 +255,7 @@ fun ReplaceEditScreen( label = "标题", selected = state.scopeTitle, checkedContentDescription = "已选择", - onToggle = { viewModel.onScopeTitleChange(!state.scopeTitle) } + onToggle = { onIntent(ReplaceEditIntent.OnScopeTitleChange(!state.scopeTitle)) } ) Spacer(Modifier.width(8.dp)) @@ -243,7 +264,7 @@ fun ReplaceEditScreen( label = "内容", selected = state.scopeContent, checkedContentDescription = "已选择", - onToggle = { viewModel.onScopeContentChange(!state.scopeContent) } + onToggle = { onIntent(ReplaceEditIntent.OnScopeContentChange(!state.scopeContent)) } ) Spacer(Modifier.weight(1f)) @@ -252,40 +273,38 @@ fun ReplaceEditScreen( label = "使用正则", selected = state.isRegex, checkedContentDescription = "正则已启用", - onToggle = { viewModel.onRegexChange(!state.isRegex) } + onToggle = { onIntent(ReplaceEditIntent.OnRegexChange(!state.isRegex)) } ) } AppTextField( value = state.scope, - onValueChange = viewModel::onScopeChange, + onValueChange = { onIntent(ReplaceEditIntent.OnScopeChange(it)) }, label = "特定范围", placeholder = { AppText("指定规则适用的范围") }, modifier = Modifier .fillMaxWidth() .onFocusChanged { - if (it.isFocused) viewModel.activeField = - ReplaceEditViewModel.ActiveField.Scope + if (it.isFocused) onIntent(ReplaceEditIntent.SetActiveField(ActiveField.Scope)) } ) AppTextField( value = state.excludeScope, - onValueChange = viewModel::onExcludeScopeChange, + onValueChange = { onIntent(ReplaceEditIntent.OnExcludeScopeChange(it)) }, label = "排除范围", placeholder = { AppText("指定规则不适用的范围") }, modifier = Modifier .fillMaxWidth() .onFocusChanged { - if (it.isFocused) viewModel.activeField = - ReplaceEditViewModel.ActiveField.Exclude + if (it.isFocused) onIntent(ReplaceEditIntent.SetActiveField(ActiveField.Exclude)) } ) AppTextField( value = state.timeout, - onValueChange = viewModel::onTimeoutChange, + onValueChange = { onIntent(ReplaceEditIntent.OnTimeoutChange(it)) }, label = "超时 (ms)", placeholder = { AppText("3000") }, modifier = Modifier.fillMaxWidth() @@ -298,8 +317,8 @@ fun ReplaceEditScreen( ManageGroupDialog( show = state.showGroupDialog, groups = state.allGroups.filter { it != "默认" }, - onDismiss = { viewModel.toggleGroupDialog(false) }, - onDelete = { viewModel.deleteGroups(it) } + onDismiss = { onIntent(ReplaceEditIntent.ToggleGroupDialog(false)) }, + onDelete = { onIntent(ReplaceEditIntent.DeleteGroups(it)) } ) } } diff --git a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt index 4a3647426..0a90ef175 100644 --- a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt @@ -13,29 +13,14 @@ import io.legado.app.utils.getClipText import io.legado.app.utils.sendToClip import io.legado.app.utils.toastOnUi import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch - -data class ReplaceEditUiState( - val id: Long = 0, - val name: String = "", - val group: String = "默认", - val pattern: String = "", - val replacement: String = "", - val isRegex: Boolean = false, - val scope: String = "", - val scopeTitle: Boolean = false, - val scopeContent: Boolean = false, - val excludeScope: String = "", - val timeout: String = "3000", - val allGroups: List = emptyList(), - val showGroupDialog: Boolean = false -) - class ReplaceEditViewModel( private val app: Application, private val replaceRuleDao: ReplaceRuleDao, @@ -45,14 +30,36 @@ class ReplaceEditViewModel( private val _uiState = MutableStateFlow(ReplaceEditUiState()) val uiState = _uiState.asStateFlow() - var activeField: ActiveField = ActiveField.None - enum class ActiveField { Name, None, Pattern, Replacement, Scope, Exclude } + private val _effects = MutableSharedFlow(extraBufferCapacity = 16) + val effects = _effects.asSharedFlow() init { initData() observeGroups() } + fun onIntent(intent: ReplaceEditIntent) { + when (intent) { + is ReplaceEditIntent.OnNameChange -> onNameChange(intent.value) + is ReplaceEditIntent.OnPatternChange -> onPatternChange(intent.value) + is ReplaceEditIntent.OnReplacementChange -> onReplacementChange(intent.value) + is ReplaceEditIntent.OnScopeChange -> onScopeChange(intent.value) + is ReplaceEditIntent.OnExcludeScopeChange -> onExcludeScopeChange(intent.value) + is ReplaceEditIntent.OnGroupChange -> onGroupChange(intent.value) + is ReplaceEditIntent.OnRegexChange -> onRegexChange(intent.value) + is ReplaceEditIntent.OnScopeTitleChange -> onScopeTitleChange(intent.value) + is ReplaceEditIntent.OnScopeContentChange -> onScopeContentChange(intent.value) + is ReplaceEditIntent.OnTimeoutChange -> onTimeoutChange(intent.value) + is ReplaceEditIntent.SetActiveField -> setActiveField(intent.field) + is ReplaceEditIntent.InsertTextAtCursor -> insertTextAtCursor(intent.text) + is ReplaceEditIntent.ToggleGroupDialog -> toggleGroupDialog(intent.show) + is ReplaceEditIntent.DeleteGroups -> deleteGroups(intent.groups) + ReplaceEditIntent.CopyRule -> copyRule() + ReplaceEditIntent.PasteRule -> pasteRule() + ReplaceEditIntent.Save -> save() + } + } + private fun initData() { viewModelScope.launch { val id = route.id @@ -121,7 +128,7 @@ class ReplaceEditViewModel( return rule } - fun copyRule() { + private fun copyRule() { viewModelScope.launch(Dispatchers.Main) { val ruleToCopy = getReplaceRuleFromState() val json = GSON.toJson(ruleToCopy) @@ -130,7 +137,7 @@ class ReplaceEditViewModel( } } - fun pasteRule(onSuccess: () -> Unit) { + private fun pasteRule() { viewModelScope.launch(Dispatchers.IO) { try { val text = app.getClipText() @@ -143,7 +150,6 @@ class ReplaceEditViewModel( launch(Dispatchers.Main) { updateStateFromRule(pastedRule) - onSuccess() } } catch (e: Exception) { launch(Dispatchers.Main) { @@ -153,39 +159,44 @@ class ReplaceEditViewModel( } } - fun onNameChange(v: String) { + private fun onNameChange(v: String) { _uiState.update { it.copy(name = v) } - activeField = ActiveField.Name + setActiveField(ActiveField.Name) } - fun onScopeChange(v: String) { + private fun onScopeChange(v: String) { _uiState.update { it.copy(scope = v) } - activeField = ActiveField.Scope + setActiveField(ActiveField.Scope) } - fun onPatternChange(v: String) { + private fun onPatternChange(v: String) { _uiState.update { it.copy(pattern = v) } - activeField = ActiveField.Pattern + setActiveField(ActiveField.Pattern) } - fun onReplacementChange(v: String) { + private fun onReplacementChange(v: String) { _uiState.update { it.copy(replacement = v) } - activeField = ActiveField.Replacement + setActiveField(ActiveField.Replacement) } - fun onExcludeScopeChange(v: String) { + private fun onExcludeScopeChange(v: String) { _uiState.update { it.copy(excludeScope = v) } - activeField = ActiveField.Exclude + setActiveField(ActiveField.Exclude) } - fun onGroupChange(v: String) = _uiState.update { it.copy(group = v) } - fun onRegexChange(v: Boolean) = _uiState.update { it.copy(isRegex = v) } - fun onScopeTitleChange(v: Boolean) = _uiState.update { it.copy(scopeTitle = v) } - fun onScopeContentChange(v: Boolean) = _uiState.update { it.copy(scopeContent = v) } - fun onTimeoutChange(v: String) = _uiState.update { it.copy(timeout = v) } - fun toggleGroupDialog(show: Boolean) = _uiState.update { it.copy(showGroupDialog = show) } - fun insertTextAtCursor(text: String) { - val state = _uiState.value - when (activeField) { + + private fun onGroupChange(v: String) = _uiState.update { it.copy(group = v) } + private fun onRegexChange(v: Boolean) = _uiState.update { it.copy(isRegex = v) } + private fun onScopeTitleChange(v: Boolean) = _uiState.update { it.copy(scopeTitle = v) } + private fun onScopeContentChange(v: Boolean) = _uiState.update { it.copy(scopeContent = v) } + private fun onTimeoutChange(v: String) = _uiState.update { it.copy(timeout = v) } + private fun toggleGroupDialog(show: Boolean) = _uiState.update { it.copy(showGroupDialog = show) } + + private fun setActiveField(field: ActiveField) { + _uiState.update { it.copy(activeField = field) } + } + + private fun insertTextAtCursor(text: String) { + when (_uiState.value.activeField) { ActiveField.Name -> _uiState.update { it.copy(name = it.name + text) } ActiveField.Pattern -> _uiState.update { it.copy(pattern = it.pattern + text) } ActiveField.Replacement -> _uiState.update { it.copy(replacement = it.replacement + text) } @@ -195,7 +206,7 @@ class ReplaceEditViewModel( } } - fun save(onSuccess: () -> Unit) { + private fun save() { viewModelScope.launch(Dispatchers.IO) { val state = _uiState.value @@ -220,14 +231,12 @@ class ReplaceEditViewModel( replaceRuleDao.insert(rule) - launch(Dispatchers.Main) { - onSuccess() - } + _effects.tryEmit(ReplaceEditEffect.NavigateBack) } } - fun deleteGroups(groups: List) { + private fun deleteGroups(groups: List) { viewModelScope.launch { replaceRuleDao.clearGroups(groups) toggleGroupDialog(false)