diff --git a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt index 8d25b810c..6032e7f2a 100644 --- a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt +++ b/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.SnackbarHostState import androidx.compose.material3.SnackbarResult -import androidx.compose.material3.rememberModalBottomSheetState 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 @@ -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.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 TxtRuleScreen( +fun TxtRuleRouteScreen( viewModel: TxtTocRuleViewModel = koinViewModel(), initialRule: String? = null, onPickRule: ((String) -> Unit)? = null, 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, + events: Flow, + onIntent: (TxtTocRuleIntent) -> Unit, + onPasteRule: () -> TxtTocRule?, + initialRule: String? = null, + onPickRule: ((String) -> Unit)? = null, + 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 isPickMode = onPickRule != null val inSelectionMode = if (isPickMode) false else selectedIds.isNotEmpty() @@ -82,17 +108,15 @@ fun TxtRuleScreen( var showExportSheet by remember { mutableStateOf(false) } val reorderableState = rememberReorderableLazyListState(listState) { from, to -> - viewModel.moveItemInList(from.index, to.index) + onIntent(TxtTocRuleIntent.MoveItem(from.index, to.index)) hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) } val clipboardManager = LocalClipboard.current val snackbarHostState = remember { SnackbarHostState() } - val importState by viewModel.importState.collectAsStateWithLifecycle() - val sheetState = rememberModalBottomSheetState() LaunchedEffect(Unit) { - viewModel.events.collect { event -> + events.collect { event -> when (event) { is BaseRuleEvent.ShowSnackbar -> { val result = snackbarHostState.showSnackbar( @@ -121,7 +145,7 @@ fun TxtRuleScreen( uri?.let { context.contentResolver.openInputStream(it)?.use { stream -> val text = stream.reader().readText() - viewModel.importSource(text) + onIntent(TxtTocRuleIntent.ImportSource(text)) } } } @@ -130,7 +154,7 @@ fun TxtRuleScreen( val exportDoc = rememberLauncherForActivityResult( contract = ActivityResultContracts.CreateDocument("application/json"), onResult = { uri -> - uri?.let { viewModel.exportToUri(it, rules, selectedIds) } + uri?.let { onIntent(TxtTocRuleIntent.ExportSelection(it)) } } ) @@ -140,7 +164,7 @@ fun TxtRuleScreen( onDismissRequest = { showUrlInput = false }, onConfirm = { showUrlInput = false - viewModel.importSource(it) + onIntent(TxtTocRuleIntent.ImportSource(it)) } ) @@ -154,7 +178,7 @@ fun TxtRuleScreen( }, onUpload = { showExportSheet = false - viewModel.uploadSelectedRules(selectedIds, rules) + onIntent(TxtTocRuleIntent.UploadSelection) }, allowExtensions = arrayOf("json") ) @@ -178,11 +202,11 @@ fun TxtRuleScreen( BatchImportDialog( title = stringResource(R.string.import_txt_toc_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(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 }, itemSubtitle = { rule -> rule.rule.takeIf { it.isNotBlank() } @@ -191,7 +215,7 @@ fun TxtRuleScreen( LaunchedEffect(reorderableState.isAnyItemDragging) { if (!reorderableState.isAnyItemDragging) { - viewModel.saveSortOrder() + onIntent(TxtTocRuleIntent.SaveSortOrder) } } @@ -202,7 +226,7 @@ fun TxtRuleScreen( text = stringResource(R.string.sure_del), confirmText = stringResource(R.string.ok), onConfirm = { rule -> - viewModel.delete(rule) + onIntent(TxtTocRuleIntent.DeleteRule(rule)) showDeleteRuleDialog = null }, dismissText = stringResource(R.string.cancel), @@ -220,17 +244,12 @@ fun TxtRuleScreen( editingRule = null }, onSave = { updatedRule -> - //TODO:我很想把他改为自增主键,但为了兼容性日后再�? - if (editingRule == null) { - viewModel.insert(updatedRule) - } else { - viewModel.update(updatedRule) - } + onIntent(TxtTocRuleIntent.SaveRule(updatedRule, isNew = editingRule == null)) showEditSheet = false editingRule = null }, - onCopy = { viewModel.copyRule(it) }, - onPaste = { viewModel.pasteRule() }, + onCopy = { onIntent(TxtTocRuleIntent.CopyRule(it)) }, + onPaste = onPasteRule, toFields = { r -> RuleEditFields( name = r?.name ?: "", @@ -257,27 +276,22 @@ fun TxtRuleScreen( } else { stringResource(R.string.txt_toc_rule) }, - state = uiState, + state = state, onBackClick = { onBackClick() }, 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), - 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( ActionItem(text = stringResource(R.string.enable), onClick = { - viewModel.enableSelectionByIds(selectedIds) - viewModel.setSelection(emptySet()) + onIntent(TxtTocRuleIntent.EnableSelection) }), ActionItem(text = stringResource(R.string.disable_selection), onClick = { - viewModel.disableSelectionByIds(selectedIds) - viewModel.setSelection(emptySet()) + onIntent(TxtTocRuleIntent.DisableSelection) }), ActionItem( text = stringResource(R.string.export), @@ -285,8 +299,8 @@ fun TxtRuleScreen( ), onDeleteSelected = { ids -> @Suppress("UNCHECKED_CAST") - viewModel.delSelectionByIds(ids as Set) - viewModel.setSelection(emptySet()) + onIntent(TxtTocRuleIntent.SetSelection(ids as Set)) + onIntent(TxtTocRuleIntent.DeleteSelection) }, onAddClick = { editingRule = null @@ -302,7 +316,7 @@ fun TxtRuleScreen( ) { paddingValues -> Box( modifier = Modifier - .fillMaxSize() + .fillMaxSize() ) { FastScrollLazyColumn( modifier = Modifier.fillMaxSize(), @@ -334,10 +348,12 @@ fun TxtRuleScreen( onPickRule.invoke(item.rule.rule) onBackClick() } 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 }, trailingAction = { SmallPlainButton( @@ -353,7 +369,7 @@ fun TxtRuleScreen( listState = listState, items = rules, selectedIds = selectedIds, - onSelectionChange = { viewModel.setSelection(it) }, + onSelectionChange = { onIntent(TxtTocRuleIntent.SetSelection(it)) }, idProvider = { it.id }, modifier = Modifier .fillMaxHeight() @@ -364,4 +380,3 @@ fun TxtRuleScreen( } } } - diff --git a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleActivity.kt b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleActivity.kt index 3b9557ad9..806224f68 100644 --- a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleActivity.kt +++ b/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") AppTheme { - TxtRuleScreen( + TxtRuleRouteScreen( initialRule = initialRule, onPickRule = if (initialRule != null) { rule -> val data = Intent().apply { diff --git a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleContract.kt b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleContract.kt new file mode 100644 index 000000000..da6873977 --- /dev/null +++ b/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 + +data class TxtTocRuleUiState( + override val items: List = emptyList(), + override val selectedIds: Set = emptySet(), + override val searchKey: String = "", + val interaction: InteractionState = InteractionState() +) : ListUiState { + override val isSearch: Boolean get() = interaction.isSearchMode + override val isLoading: Boolean get() = interaction.isUploading +} + +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) : 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, +) diff --git a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleViewModel.kt b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleViewModel.kt index 53a400196..1c25d538f 100644 --- a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleViewModel.kt +++ b/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 import android.app.Application -import androidx.compose.runtime.Immutable import androidx.lifecycle.viewModelScope import io.legado.app.R 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.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 @@ -21,28 +18,9 @@ import io.legado.app.utils.isJsonObject import io.legado.app.utils.sendToClip import io.legado.app.utils.toastOnUi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.update 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 - -data class TxtTocRuleUiState( - override val items: List = emptyList(), - override val selectedIds: Set = emptySet(), - override val searchKey: String = "", - val interaction: InteractionState = InteractionState() -) : ListUiState { - override val isSearch: Boolean get() = interaction.isSearchMode - override val isLoading: Boolean get() = interaction.isUploading -} - - class TxtTocRuleViewModel( application: Application, uploadRepository: UploadRepository @@ -55,9 +33,54 @@ class TxtTocRuleViewModel( override val rawDataFlow: Flow> = 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() = TxtTocRuleItemUi(id, name, enable, this, example = example ?: "") + @Suppress("DEPRECATION") override fun filterData(data: List, key: String): List { val filtered = if (key.isEmpty()) data 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 viewModelScope.launch { repository.saveOrder(currentLocal.map { it.rule }) @@ -91,7 +114,7 @@ class TxtTocRuleViewModel( } } - fun save(rule: TxtTocRule) { + private fun save(rule: TxtTocRule) { viewModelScope.launch { if (rule.id == 0L) { 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) = viewModelScope.launch { repository.enableByIds(ids, true) } @@ -112,6 +136,15 @@ class TxtTocRuleViewModel( fun delSelectionByIds(ids: Set) = 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): String = GSON.toJson(entities) override fun parseImportRules(text: String): List { @@ -141,7 +174,7 @@ class TxtTocRuleViewModel( } } - fun copyRule(rule: TxtTocRule) { + private fun copyRule(rule: TxtTocRule) { context.sendToClip(GSON.toJson(rule)) }