refactor: 重构替换规则界面为MVI架构

This commit is contained in:
HapeLee committed 2026-06-27 02:27:43 +08:00
1 parent 5195d51b08
commit da6c02e2ca
7 files changed
+378 -185

No files matched your search

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