Просмотр исходного кода

refactor: 重构TXT目录规则界面为MVI架构

HapeLee 4 месяцев назад
Родитель
Сommit
dd2190c091

+ 64 - 49
app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt

@@ -14,10 +14,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.SnackbarResult
 import androidx.compose.material3.SnackbarResult
-import androidx.compose.material3.rememberModalBottomSheetState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.remember
@@ -44,29 +42,57 @@ 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.filePicker.FilePickerSheet
 import io.legado.app.ui.widget.components.icon.AppIcons
 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.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.importComponents.SourceInputDialog
 import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
 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.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.rules.RuleEditFields
 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.RuleEditSheet
 import io.legado.app.ui.widget.components.rules.RuleListScaffold
 import io.legado.app.ui.widget.components.rules.RuleListScaffold
+import kotlinx.coroutines.flow.Flow
 import org.koin.androidx.compose.koinViewModel
 import org.koin.androidx.compose.koinViewModel
 import sh.calvin.reorderable.rememberReorderableLazyListState
 import sh.calvin.reorderable.rememberReorderableLazyListState
 
 
 @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
 @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 @Composable
-fun TxtRuleScreen(
+fun TxtRuleRouteScreen(
     viewModel: TxtTocRuleViewModel = koinViewModel(),
     viewModel: TxtTocRuleViewModel = koinViewModel(),
     initialRule: String? = null,
     initialRule: String? = null,
     onPickRule: ((String) -> Unit)? = null,
     onPickRule: ((String) -> Unit)? = null,
     onBackClick: () -> Unit
     onBackClick: () -> Unit
 ) {
 ) {
+    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+    val importState by viewModel.importState.collectAsStateWithLifecycle()
+
+    TxtRuleScreen(
+        state = uiState,
+        importState = importState,
+        events = viewModel.events,
+        onIntent = viewModel::onIntent,
+        onPasteRule = viewModel::pasteRule,
+        initialRule = initialRule,
+        onPickRule = onPickRule,
+        onBackClick = onBackClick,
+    )
+}
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun TxtRuleScreen(
+    state: TxtTocRuleUiState,
+    importState: BaseImportUiState<TxtTocRule>,
+    events: Flow<BaseRuleEvent>,
+    onIntent: (TxtTocRuleIntent) -> Unit,
+    onPasteRule: () -> TxtTocRule?,
+    initialRule: String? = null,
+    onPickRule: ((String) -> Unit)? = null,
+    onBackClick: () -> Unit
+) {
 
 
     val context = LocalContext.current
     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 isPickMode = onPickRule != null
     val isPickMode = onPickRule != null
     val inSelectionMode = if (isPickMode) false else selectedIds.isNotEmpty()
     val inSelectionMode = if (isPickMode) false else selectedIds.isNotEmpty()
 
 
@@ -82,17 +108,15 @@ fun TxtRuleScreen(
     var showExportSheet by remember { mutableStateOf(false) }
     var showExportSheet by remember { mutableStateOf(false) }
 
 
     val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
     val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
-        viewModel.moveItemInList(from.index, to.index)
+        onIntent(TxtTocRuleIntent.MoveItem(from.index, to.index))
         hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
         hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
     }
     }
 
 
     val clipboardManager = LocalClipboard.current
     val clipboardManager = LocalClipboard.current
     val snackbarHostState = remember { SnackbarHostState() }
     val snackbarHostState = remember { SnackbarHostState() }
-    val importState by viewModel.importState.collectAsStateWithLifecycle()
-    val sheetState = rememberModalBottomSheetState()
 
 
     LaunchedEffect(Unit) {
     LaunchedEffect(Unit) {
-        viewModel.events.collect { event ->
+        events.collect { event ->
             when (event) {
             when (event) {
                 is BaseRuleEvent.ShowSnackbar -> {
                 is BaseRuleEvent.ShowSnackbar -> {
                     val result = snackbarHostState.showSnackbar(
                     val result = snackbarHostState.showSnackbar(
@@ -121,7 +145,7 @@ fun TxtRuleScreen(
             uri?.let {
             uri?.let {
                 context.contentResolver.openInputStream(it)?.use { stream ->
                 context.contentResolver.openInputStream(it)?.use { stream ->
                     val text = stream.reader().readText()
                     val text = stream.reader().readText()
-                    viewModel.importSource(text)
+                    onIntent(TxtTocRuleIntent.ImportSource(text))
                 }
                 }
             }
             }
         }
         }
@@ -130,7 +154,7 @@ fun TxtRuleScreen(
     val exportDoc = rememberLauncherForActivityResult(
     val exportDoc = rememberLauncherForActivityResult(
         contract = ActivityResultContracts.CreateDocument("application/json"),
         contract = ActivityResultContracts.CreateDocument("application/json"),
         onResult = { uri ->
         onResult = { uri ->
-            uri?.let { viewModel.exportToUri(it, rules, selectedIds) }
+            uri?.let { onIntent(TxtTocRuleIntent.ExportSelection(it)) }
         }
         }
     )
     )
 
 
@@ -140,7 +164,7 @@ fun TxtRuleScreen(
         onDismissRequest = { showUrlInput = false },
         onDismissRequest = { showUrlInput = false },
         onConfirm = {
         onConfirm = {
             showUrlInput = false
             showUrlInput = false
-            viewModel.importSource(it)
+            onIntent(TxtTocRuleIntent.ImportSource(it))
         }
         }
     )
     )
 
 
@@ -154,7 +178,7 @@ fun TxtRuleScreen(
         },
         },
         onUpload = {
         onUpload = {
             showExportSheet = false
             showExportSheet = false
-            viewModel.uploadSelectedRules(selectedIds, rules)
+            onIntent(TxtTocRuleIntent.UploadSelection)
         },
         },
         allowExtensions = arrayOf("json")
         allowExtensions = arrayOf("json")
     )
     )
@@ -178,11 +202,11 @@ fun TxtRuleScreen(
     BatchImportDialog(
     BatchImportDialog(
         title = stringResource(R.string.import_txt_toc_rule),
         title = stringResource(R.string.import_txt_toc_rule),
         importState = importState,
         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(TxtTocRuleIntent.CancelImport) },
+        onToggleItem = { onIntent(TxtTocRuleIntent.ToggleImportSelection(it)) },
+        onToggleAll = { onIntent(TxtTocRuleIntent.ToggleImportAll(it)) },
+        onUpdateItem = { index, rule -> onIntent(TxtTocRuleIntent.UpdateImportItem(index, rule)) },
+        onConfirm = { onIntent(TxtTocRuleIntent.SaveImportedRules) },
         itemTitle = { rule -> rule.name },
         itemTitle = { rule -> rule.name },
         itemSubtitle = { rule ->
         itemSubtitle = { rule ->
             rule.rule.takeIf { it.isNotBlank() }
             rule.rule.takeIf { it.isNotBlank() }
@@ -191,7 +215,7 @@ fun TxtRuleScreen(
 
 
     LaunchedEffect(reorderableState.isAnyItemDragging) {
     LaunchedEffect(reorderableState.isAnyItemDragging) {
         if (!reorderableState.isAnyItemDragging) {
         if (!reorderableState.isAnyItemDragging) {
-            viewModel.saveSortOrder()
+            onIntent(TxtTocRuleIntent.SaveSortOrder)
         }
         }
     }
     }
 
 
@@ -202,7 +226,7 @@ fun TxtRuleScreen(
         text = stringResource(R.string.sure_del),
         text = stringResource(R.string.sure_del),
         confirmText = stringResource(R.string.ok),
         confirmText = stringResource(R.string.ok),
         onConfirm = { rule ->
         onConfirm = { rule ->
-            viewModel.delete(rule)
+            onIntent(TxtTocRuleIntent.DeleteRule(rule))
             showDeleteRuleDialog = null
             showDeleteRuleDialog = null
         },
         },
         dismissText = stringResource(R.string.cancel),
         dismissText = stringResource(R.string.cancel),
@@ -220,17 +244,12 @@ fun TxtRuleScreen(
             editingRule = null
             editingRule = null
         },
         },
         onSave = { updatedRule ->
         onSave = { updatedRule ->
-            //TODO:我很想把他改为自增主键,但为了兼容性日后再�?
-            if (editingRule == null) {
-                viewModel.insert(updatedRule)
-            } else {
-                viewModel.update(updatedRule)
-            }
+            onIntent(TxtTocRuleIntent.SaveRule(updatedRule, isNew = editingRule == null))
             showEditSheet = false
             showEditSheet = false
             editingRule = null
             editingRule = null
         },
         },
-        onCopy = { viewModel.copyRule(it) },
-        onPaste = { viewModel.pasteRule() },
+        onCopy = { onIntent(TxtTocRuleIntent.CopyRule(it)) },
+        onPaste = onPasteRule,
         toFields = { r ->
         toFields = { r ->
             RuleEditFields(
             RuleEditFields(
                 name = r?.name ?: "",
                 name = r?.name ?: "",
@@ -257,27 +276,22 @@ fun TxtRuleScreen(
         } else {
         } else {
             stringResource(R.string.txt_toc_rule)
             stringResource(R.string.txt_toc_rule)
         },
         },
-        state = uiState,
+        state = state,
         onBackClick = { onBackClick() },
         onBackClick = { onBackClick() },
         onSearchToggle = { active ->
         onSearchToggle = { active ->
-            viewModel.setSearchMode(active)
+            onIntent(TxtTocRuleIntent.SetSearchMode(active))
         },
         },
-        onSearchQueryChange = { viewModel.setSearchKey(it) },
+        onSearchQueryChange = { onIntent(TxtTocRuleIntent.UpdateSearchQuery(it)) },
         searchPlaceholder = stringResource(R.string.replace_purify_search),
         searchPlaceholder = stringResource(R.string.replace_purify_search),
-        onClearSelection = { viewModel.setSelection(emptySet()) },
-        onSelectAll = { viewModel.setSelection(rules.map { it.id }.toSet()) },
-        onSelectInvert = {
-            val allIds = rules.map { it.id }.toSet()
-            viewModel.setSelection(allIds - selectedIds)
-        },
+        onClearSelection = { onIntent(TxtTocRuleIntent.ClearSelection) },
+        onSelectAll = { onIntent(TxtTocRuleIntent.SelectAll) },
+        onSelectInvert = { onIntent(TxtTocRuleIntent.InvertSelection) },
         selectionSecondaryActions = listOf(
         selectionSecondaryActions = listOf(
             ActionItem(text = stringResource(R.string.enable), onClick = {
             ActionItem(text = stringResource(R.string.enable), onClick = {
-                viewModel.enableSelectionByIds(selectedIds)
-                viewModel.setSelection(emptySet())
+                onIntent(TxtTocRuleIntent.EnableSelection)
             }),
             }),
             ActionItem(text = stringResource(R.string.disable_selection), onClick = {
             ActionItem(text = stringResource(R.string.disable_selection), onClick = {
-                viewModel.disableSelectionByIds(selectedIds)
-                viewModel.setSelection(emptySet())
+                onIntent(TxtTocRuleIntent.DisableSelection)
             }),
             }),
             ActionItem(
             ActionItem(
                 text = stringResource(R.string.export),
                 text = stringResource(R.string.export),
@@ -285,8 +299,8 @@ fun TxtRuleScreen(
         ),
         ),
         onDeleteSelected = { ids ->
         onDeleteSelected = { ids ->
             @Suppress("UNCHECKED_CAST")
             @Suppress("UNCHECKED_CAST")
-            viewModel.delSelectionByIds(ids as Set<Long>)
-            viewModel.setSelection(emptySet())
+            onIntent(TxtTocRuleIntent.SetSelection(ids as Set<Long>))
+            onIntent(TxtTocRuleIntent.DeleteSelection)
         },
         },
         onAddClick = {
         onAddClick = {
             editingRule = null
             editingRule = null
@@ -302,7 +316,7 @@ fun TxtRuleScreen(
     ) { paddingValues ->
     ) { paddingValues ->
         Box(
         Box(
             modifier = Modifier
             modifier = Modifier
-            .fillMaxSize()
+                .fillMaxSize()
         ) {
         ) {
             FastScrollLazyColumn(
             FastScrollLazyColumn(
                 modifier = Modifier.fillMaxSize(),
                 modifier = Modifier.fillMaxSize(),
@@ -334,10 +348,12 @@ fun TxtRuleScreen(
                                 onPickRule.invoke(item.rule.rule)
                                 onPickRule.invoke(item.rule.rule)
                                 onBackClick()
                                 onBackClick()
                             } else {
                             } else {
-                                viewModel.toggleSelection(item.id)
+                                onIntent(TxtTocRuleIntent.ToggleSelection(item.id))
                             }
                             }
                         },
                         },
-                        onEnabledChange = { enabled -> viewModel.update(item.rule.copy(enable = enabled)) },
+                        onEnabledChange = { enabled ->
+                            onIntent(TxtTocRuleIntent.SetRuleEnabled(item.rule, enabled))
+                        },
                         onClickEdit = { editingRule = item.rule; showEditSheet = true },
                         onClickEdit = { editingRule = item.rule; showEditSheet = true },
                         trailingAction = {
                         trailingAction = {
                             SmallPlainButton(
                             SmallPlainButton(
@@ -353,7 +369,7 @@ fun TxtRuleScreen(
                     listState = listState,
                     listState = listState,
                     items = rules,
                     items = rules,
                     selectedIds = selectedIds,
                     selectedIds = selectedIds,
-                    onSelectionChange = { viewModel.setSelection(it) },
+                    onSelectionChange = { onIntent(TxtTocRuleIntent.SetSelection(it)) },
                     idProvider = { it.id },
                     idProvider = { it.id },
                     modifier = Modifier
                     modifier = Modifier
                         .fillMaxHeight()
                         .fillMaxHeight()
@@ -364,4 +380,3 @@ fun TxtRuleScreen(
         }
         }
     }
     }
 }
 }
-

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleActivity.kt

@@ -13,7 +13,7 @@ class TxtTocRuleActivity : BaseComposeActivity() {
         val initialRule = intent.getStringExtra("tocRegex")
         val initialRule = intent.getStringExtra("tocRegex")
 
 
         AppTheme {
         AppTheme {
-            TxtRuleScreen(
+            TxtRuleRouteScreen(
                 initialRule = initialRule,
                 initialRule = initialRule,
                 onPickRule = if (initialRule != null) { rule ->
                 onPickRule = if (initialRule != null) { rule ->
                     val data = Intent().apply {
                     val data = Intent().apply {

+ 62 - 0
app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleContract.kt

@@ -0,0 +1,62 @@
+package io.legado.app.ui.book.toc.rule
+
+import android.net.Uri
+import androidx.compose.runtime.Immutable
+import io.legado.app.data.entities.TxtTocRule
+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 TxtTocRuleItemUi(
+    override val id: Long,
+    val name: String,
+    val isEnabled: Boolean,
+    val rule: TxtTocRule,
+    val example: String = ""
+) : SelectableItem<Long>
+
+data class TxtTocRuleUiState(
+    override val items: List<TxtTocRuleItemUi> = emptyList(),
+    override val selectedIds: Set<Long> = emptySet(),
+    override val searchKey: String = "",
+    val interaction: InteractionState = InteractionState()
+) : ListUiState<TxtTocRuleItemUi> {
+    override val isSearch: Boolean get() = interaction.isSearchMode
+    override val isLoading: Boolean get() = interaction.isUploading
+}
+
+sealed interface TxtTocRuleIntent {
+    data class SetSearchMode(val active: Boolean) : TxtTocRuleIntent
+    data class UpdateSearchQuery(val query: String) : TxtTocRuleIntent
+    data object ClearSelection : TxtTocRuleIntent
+    data object SelectAll : TxtTocRuleIntent
+    data object InvertSelection : TxtTocRuleIntent
+    data class SetSelection(val ids: Set<Long>) : TxtTocRuleIntent
+    data class ToggleSelection(val id: Long) : TxtTocRuleIntent
+    data object EnableSelection : TxtTocRuleIntent
+    data object DisableSelection : TxtTocRuleIntent
+    data object DeleteSelection : TxtTocRuleIntent
+    data object UploadSelection : TxtTocRuleIntent
+    data class ExportSelection(val uri: Uri) : TxtTocRuleIntent
+    data class MoveItem(val from: Int, val to: Int) : TxtTocRuleIntent
+    data object SaveSortOrder : TxtTocRuleIntent
+    data class SaveRule(val rule: TxtTocRule, val isNew: Boolean) : TxtTocRuleIntent
+    data class DeleteRule(val rule: TxtTocRule) : TxtTocRuleIntent
+    data class SetRuleEnabled(val rule: TxtTocRule, val enabled: Boolean) : TxtTocRuleIntent
+    data class CopyRule(val rule: TxtTocRule) : TxtTocRuleIntent
+    data class ImportSource(val text: String) : TxtTocRuleIntent
+    data object CancelImport : TxtTocRuleIntent
+    data class ToggleImportSelection(val index: Int) : TxtTocRuleIntent
+    data class ToggleImportAll(val isSelected: Boolean) : TxtTocRuleIntent
+    data class UpdateImportItem(val index: Int, val rule: TxtTocRule) : TxtTocRuleIntent
+    data object SaveImportedRules : TxtTocRuleIntent
+}
+
+sealed interface TxtTocRuleEffect
+
+data class TxtTocRuleRenderState(
+    val uiState: TxtTocRuleUiState,
+    val importState: BaseImportUiState<TxtTocRule>,
+)

+ 62 - 29
app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleViewModel.kt

@@ -1,7 +1,6 @@
 package io.legado.app.ui.book.toc.rule
 package io.legado.app.ui.book.toc.rule
 
 
 import android.app.Application
 import android.app.Application
-import androidx.compose.runtime.Immutable
 import androidx.lifecycle.viewModelScope
 import androidx.lifecycle.viewModelScope
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.base.BaseRuleViewModel
 import io.legado.app.base.BaseRuleViewModel
@@ -10,8 +9,6 @@ import io.legado.app.data.repository.TxtTocRuleRepository
 import io.legado.app.data.repository.UploadRepository
 import io.legado.app.data.repository.UploadRepository
 import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
 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.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.GSON
 import io.legado.app.utils.fromJsonArray
 import io.legado.app.utils.fromJsonArray
 import io.legado.app.utils.fromJsonObject
 import io.legado.app.utils.fromJsonObject
@@ -21,28 +18,9 @@ import io.legado.app.utils.isJsonObject
 import io.legado.app.utils.sendToClip
 import io.legado.app.utils.sendToClip
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 
 
-@Immutable
-data class TxtTocRuleItemUi(
-    override val id: Long,
-    val name: String,
-    val isEnabled: Boolean,
-    val rule: TxtTocRule,
-    val example: String = ""
-) : SelectableItem<Long>
-
-data class TxtTocRuleUiState(
-    override val items: List<TxtTocRuleItemUi> = emptyList(),
-    override val selectedIds: Set<Long> = emptySet(),
-    override val searchKey: String = "",
-    val interaction: InteractionState = InteractionState()
-) : ListUiState<TxtTocRuleItemUi> {
-    override val isSearch: Boolean get() = interaction.isSearchMode
-    override val isLoading: Boolean get() = interaction.isUploading
-}
-
-
 class TxtTocRuleViewModel(
 class TxtTocRuleViewModel(
     application: Application,
     application: Application,
     uploadRepository: UploadRepository
     uploadRepository: UploadRepository
@@ -55,9 +33,54 @@ class TxtTocRuleViewModel(
 
 
     override val rawDataFlow: Flow<List<TxtTocRule>> = repository.flowAll()
     override val rawDataFlow: Flow<List<TxtTocRule>> = repository.flowAll()
 
 
+    fun onIntent(intent: TxtTocRuleIntent) {
+        when (intent) {
+            is TxtTocRuleIntent.SetSearchMode -> setSearchMode(intent.active)
+            is TxtTocRuleIntent.UpdateSearchQuery -> setSearchKey(intent.query)
+            TxtTocRuleIntent.ClearSelection -> setSelection(emptySet())
+            TxtTocRuleIntent.SelectAll -> selectAll()
+            TxtTocRuleIntent.InvertSelection -> invertSelection()
+            is TxtTocRuleIntent.SetSelection -> setSelection(intent.ids)
+            is TxtTocRuleIntent.ToggleSelection -> toggleSelection(intent.id)
+            TxtTocRuleIntent.EnableSelection -> {
+                enableSelectionByIds(uiState.value.selectedIds)
+                setSelection(emptySet())
+            }
+            TxtTocRuleIntent.DisableSelection -> {
+                disableSelectionByIds(uiState.value.selectedIds)
+                setSelection(emptySet())
+            }
+            TxtTocRuleIntent.DeleteSelection -> {
+                delSelectionByIds(uiState.value.selectedIds)
+                setSelection(emptySet())
+            }
+            TxtTocRuleIntent.UploadSelection -> {
+                val state = uiState.value
+                uploadSelectedRules(state.selectedIds, state.items)
+            }
+            is TxtTocRuleIntent.ExportSelection -> {
+                val state = uiState.value
+                exportToUri(intent.uri, state.items, state.selectedIds)
+            }
+            is TxtTocRuleIntent.MoveItem -> moveItemInList(intent.from, intent.to)
+            TxtTocRuleIntent.SaveSortOrder -> saveSortOrder()
+            is TxtTocRuleIntent.SaveRule -> save(intent.rule)
+            is TxtTocRuleIntent.DeleteRule -> delete(intent.rule)
+            is TxtTocRuleIntent.SetRuleEnabled -> update(intent.rule.copy(enable = intent.enabled))
+            is TxtTocRuleIntent.CopyRule -> copyRule(intent.rule)
+            is TxtTocRuleIntent.ImportSource -> importSource(intent.text)
+            TxtTocRuleIntent.CancelImport -> cancelImport()
+            is TxtTocRuleIntent.ToggleImportSelection -> toggleImportSelection(intent.index)
+            is TxtTocRuleIntent.ToggleImportAll -> toggleImportAll(intent.isSelected)
+            is TxtTocRuleIntent.UpdateImportItem -> updateImportItem(intent.index, intent.rule)
+            TxtTocRuleIntent.SaveImportedRules -> saveImportedRules()
+        }
+    }
+
     override fun TxtTocRule.toUiItem() =
     override fun TxtTocRule.toUiItem() =
         TxtTocRuleItemUi(id, name, enable, this, example = example ?: "")
         TxtTocRuleItemUi(id, name, enable, this, example = example ?: "")
 
 
+    @Suppress("DEPRECATION")
     override fun filterData(data: List<TxtTocRule>, key: String): List<TxtTocRule> {
     override fun filterData(data: List<TxtTocRule>, key: String): List<TxtTocRule> {
         val filtered = if (key.isEmpty()) data
         val filtered = if (key.isEmpty()) data
         else data.filter { it.name.contains(key, ignoreCase = true) }
         else data.filter { it.name.contains(key, ignoreCase = true) }
@@ -83,7 +106,7 @@ class TxtTocRuleViewModel(
         )
         )
     }
     }
 
 
-    fun saveSortOrder() {
+    private fun saveSortOrder() {
         val currentLocal = _localItems.value ?: return
         val currentLocal = _localItems.value ?: return
         viewModelScope.launch {
         viewModelScope.launch {
             repository.saveOrder(currentLocal.map { it.rule })
             repository.saveOrder(currentLocal.map { it.rule })
@@ -91,7 +114,7 @@ class TxtTocRuleViewModel(
         }
         }
     }
     }
 
 
-    fun save(rule: TxtTocRule) {
+    private fun save(rule: TxtTocRule) {
         viewModelScope.launch {
         viewModelScope.launch {
             if (rule.id == 0L) {
             if (rule.id == 0L) {
                 repository.insert(rule)
                 repository.insert(rule)
@@ -101,9 +124,10 @@ class TxtTocRuleViewModel(
         }
         }
     }
     }
 
 
-    fun update(vararg rules: TxtTocRule) = viewModelScope.launch { repository.update(*rules) }
-    fun insert(vararg rules: TxtTocRule) = viewModelScope.launch { repository.insert(*rules) }
-    fun delete(vararg rules: TxtTocRule) = viewModelScope.launch { repository.delete(*rules) }
+    private fun update(vararg rules: TxtTocRule) = viewModelScope.launch { repository.update(*rules) }
+    private fun insert(vararg rules: TxtTocRule) = viewModelScope.launch { repository.insert(*rules) }
+    private fun delete(vararg rules: TxtTocRule) = viewModelScope.launch { repository.delete(*rules) }
+
     fun enableSelectionByIds(ids: Set<Long>) =
     fun enableSelectionByIds(ids: Set<Long>) =
         viewModelScope.launch { repository.enableByIds(ids, true) }
         viewModelScope.launch { repository.enableByIds(ids, true) }
 
 
@@ -112,6 +136,15 @@ class TxtTocRuleViewModel(
 
 
     fun delSelectionByIds(ids: Set<Long>) = viewModelScope.launch { repository.deleteByIds(ids) }
     fun delSelectionByIds(ids: Set<Long>) = viewModelScope.launch { repository.deleteByIds(ids) }
 
 
+    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)
+    }
+
     override suspend fun generateJson(entities: List<TxtTocRule>): String = GSON.toJson(entities)
     override suspend fun generateJson(entities: List<TxtTocRule>): String = GSON.toJson(entities)
 
 
     override fun parseImportRules(text: String): List<TxtTocRule> {
     override fun parseImportRules(text: String): List<TxtTocRule> {
@@ -141,7 +174,7 @@ class TxtTocRuleViewModel(
         }
         }
     }
     }
 
 
-    fun copyRule(rule: TxtTocRule) {
+    private fun copyRule(rule: TxtTocRule) {
         context.sendToClip(GSON.toJson(rule))
         context.sendToClip(GSON.toJson(rule))
     }
     }