refactor: 重构替换规则界面为MVI架构
This commit is contained in:
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.ui.NavDisplay
|
||||
import io.legado.app.base.BaseComposeActivity
|
||||
import io.legado.app.ui.replace.edit.ReplaceEditScreen
|
||||
import io.legado.app.ui.replace.edit.ReplaceEditRouteScreen
|
||||
import io.legado.app.ui.replace.edit.ReplaceEditViewModel
|
||||
import io.legado.app.ui.theme.AppTheme
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -136,7 +136,7 @@ class ReplaceRuleActivity : BaseComposeActivity() {
|
||||
},
|
||||
entryProvider = entryProvider {
|
||||
entry<ReplaceRuleRoute> {
|
||||
ReplaceRuleScreen(
|
||||
ReplaceRuleRouteScreen(
|
||||
onBackClick = { finish() },
|
||||
onNavigateToEdit = { route -> backStack.add(route) }
|
||||
)
|
||||
@@ -147,7 +147,7 @@ class ReplaceRuleActivity : BaseComposeActivity() {
|
||||
key = "replace_edit_${System.identityHashCode(route)}"
|
||||
) { parametersOf(route) }
|
||||
|
||||
ReplaceEditScreen(
|
||||
ReplaceEditRouteScreen(
|
||||
viewModel = viewModel,
|
||||
onBack = {
|
||||
if (backStack.size > 1) {
|
||||
|
||||
@@ -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.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -15,7 +14,6 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.FileOpen
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
@@ -57,7 +55,6 @@ import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
|
||||
import io.legado.app.ui.widget.components.divider.PillDivider
|
||||
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
||||
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
||||
import io.legado.app.ui.widget.components.importComponents.BatchImportDialog
|
||||
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
|
||||
@@ -66,16 +63,47 @@ import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||
import io.legado.app.ui.widget.components.rules.RuleListScaffold
|
||||
import io.legado.app.ui.widget.components.tabRow.AppTabRow
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
|
||||
ExperimentalMaterial3ExpressiveApi::class
|
||||
)
|
||||
@Composable
|
||||
fun ReplaceRuleRouteScreen(
|
||||
viewModel: ReplaceRuleViewModel = koinViewModel(),
|
||||
onBackClick: () -> Unit,
|
||||
onNavigateToEdit: (ReplaceEditRoute) -> Unit,
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val importState by viewModel.importState.collectAsStateWithLifecycle()
|
||||
val groups by viewModel.allGroups.collectAsStateWithLifecycle()
|
||||
|
||||
ReplaceRuleScreen(
|
||||
state = uiState,
|
||||
importState = importState,
|
||||
events = viewModel.events,
|
||||
groups = groups,
|
||||
onIntent = viewModel::onIntent,
|
||||
onBackClick = onBackClick,
|
||||
onNavigateToEdit = onNavigateToEdit,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
|
||||
ExperimentalMaterial3ExpressiveApi::class
|
||||
)
|
||||
@Composable
|
||||
fun ReplaceRuleScreen(
|
||||
viewModel: ReplaceRuleViewModel = koinViewModel(),
|
||||
state: ReplaceRuleUiState,
|
||||
importState: BaseImportUiState<ReplaceRule>,
|
||||
events: Flow<BaseRuleEvent>,
|
||||
groups: List<String>,
|
||||
onIntent: (ReplaceRuleIntent) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
onNavigateToEdit: (ReplaceEditRoute) -> Unit,
|
||||
) {
|
||||
@@ -83,10 +111,8 @@ fun ReplaceRuleScreen(
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val rules = uiState.items
|
||||
val groups by viewModel.allGroups.collectAsStateWithLifecycle()
|
||||
val selectedIds = uiState.selectedIds
|
||||
val rules = state.items
|
||||
val selectedIds = state.selectedIds
|
||||
val inSelectionMode = selectedIds.isNotEmpty()
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
@@ -101,18 +127,16 @@ fun ReplaceRuleScreen(
|
||||
var showDeleteRuleDialog by remember { mutableStateOf<ReplaceRule?>(null) }
|
||||
var showGroupManageSheet by remember { mutableStateOf(false) }
|
||||
|
||||
val importState by viewModel.importState.collectAsStateWithLifecycle()
|
||||
|
||||
var selectedTabIndex by remember { mutableIntStateOf(0) }
|
||||
val tabItems = remember(groups) { listOf("全部") + groups }
|
||||
|
||||
val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
|
||||
viewModel.moveItemInList(from.index, to.index)
|
||||
onIntent(ReplaceRuleIntent.MoveItem(from.index, to.index))
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
|
||||
}
|
||||
|
||||
val canReorder = remember(uiState.sortMode) {
|
||||
uiState.sortMode == "asc" || uiState.sortMode == "desc"
|
||||
val canReorder = remember(state.sortMode) {
|
||||
state.sortMode == "asc" || state.sortMode == "desc"
|
||||
}
|
||||
|
||||
val importDoc = rememberLauncherForActivityResult(
|
||||
@@ -121,7 +145,7 @@ fun ReplaceRuleScreen(
|
||||
uri?.let {
|
||||
context.contentResolver.openInputStream(it)?.use { stream ->
|
||||
val text = stream.reader().readText()
|
||||
viewModel.importSource(text)
|
||||
onIntent(ReplaceRuleIntent.ImportSource(text))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,7 +154,7 @@ fun ReplaceRuleScreen(
|
||||
val exportDoc = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument("application/json"),
|
||||
onResult = { uri ->
|
||||
uri?.let { viewModel.exportToUri(it, rules, selectedIds) }
|
||||
uri?.let { onIntent(ReplaceRuleIntent.ExportSelection(it)) }
|
||||
}
|
||||
)
|
||||
|
||||
@@ -140,7 +164,7 @@ fun ReplaceRuleScreen(
|
||||
onDismissRequest = { showUrlInput = false },
|
||||
onConfirm = {
|
||||
showUrlInput = false
|
||||
viewModel.importSource(it)
|
||||
onIntent(ReplaceRuleIntent.ImportSource(it))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -168,7 +192,7 @@ fun ReplaceRuleScreen(
|
||||
},
|
||||
onUpload = {
|
||||
showExportSheet = false
|
||||
viewModel.uploadSelectedRules(selectedIds, rules)
|
||||
onIntent(ReplaceRuleIntent.UploadSelection)
|
||||
},
|
||||
allowExtensions = arrayOf("json")
|
||||
)
|
||||
@@ -176,11 +200,11 @@ fun ReplaceRuleScreen(
|
||||
BatchImportDialog(
|
||||
title = stringResource(R.string.import_replace_rule),
|
||||
importState = importState,
|
||||
onDismissRequest = { viewModel.cancelImport() },
|
||||
onToggleItem = { viewModel.toggleImportSelection(it) },
|
||||
onToggleAll = { viewModel.toggleImportAll(it) },
|
||||
onUpdateItem = { index, rule -> viewModel.updateImportItem(index, rule) },
|
||||
onConfirm = { viewModel.saveImportedRules() },
|
||||
onDismissRequest = { onIntent(ReplaceRuleIntent.CancelImport) },
|
||||
onToggleItem = { onIntent(ReplaceRuleIntent.ToggleImportSelection(it)) },
|
||||
onToggleAll = { onIntent(ReplaceRuleIntent.ToggleImportAll(it)) },
|
||||
onUpdateItem = { index, rule -> onIntent(ReplaceRuleIntent.UpdateImportItem(index, rule)) },
|
||||
onConfirm = { onIntent(ReplaceRuleIntent.SaveImportedRules) },
|
||||
topBarActions = {},
|
||||
itemTitle = { rule -> rule.name },
|
||||
itemSubtitle = { rule ->
|
||||
@@ -189,7 +213,7 @@ fun ReplaceRuleScreen(
|
||||
)
|
||||
|
||||
if (importState is BaseImportUiState.Loading) {
|
||||
Dialog(onDismissRequest = { viewModel.cancelImport() }) { LoadingIndicator() }
|
||||
Dialog(onDismissRequest = { onIntent(ReplaceRuleIntent.CancelImport) }) { LoadingIndicator() }
|
||||
}
|
||||
|
||||
LaunchedEffect(importState) {
|
||||
@@ -197,13 +221,13 @@ fun ReplaceRuleScreen(
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(it.msg)
|
||||
}
|
||||
viewModel.cancelImport()
|
||||
onIntent(ReplaceRuleIntent.CancelImport)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(reorderableState.isAnyItemDragging) {
|
||||
if (!reorderableState.isAnyItemDragging) {
|
||||
viewModel.saveSortOrder()
|
||||
onIntent(ReplaceRuleIntent.SaveSortOrder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,12 +235,12 @@ fun ReplaceRuleScreen(
|
||||
val maxIndex = groups.size
|
||||
if (selectedTabIndex > maxIndex) {
|
||||
selectedTabIndex = 0
|
||||
viewModel.setGroup("全部")
|
||||
onIntent(ReplaceRuleIntent.SetGroup("全部"))
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.events.collect { event ->
|
||||
events.collect { event ->
|
||||
when (event) {
|
||||
is BaseRuleEvent.ShowSnackbar -> {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
@@ -244,8 +268,8 @@ fun ReplaceRuleScreen(
|
||||
show = showGroupManageSheet,
|
||||
groups = groups,
|
||||
onDismissRequest = { showGroupManageSheet = false },
|
||||
onUpdateGroup = { old, new -> viewModel.upGroup(old, new) },
|
||||
onDeleteGroup = { viewModel.delGroup(it) }
|
||||
onUpdateGroup = { old, new -> onIntent(ReplaceRuleIntent.UpGroup(old, new)) },
|
||||
onDeleteGroup = { onIntent(ReplaceRuleIntent.DeleteGroup(it)) }
|
||||
)
|
||||
|
||||
|
||||
@@ -255,7 +279,7 @@ fun ReplaceRuleScreen(
|
||||
title = stringResource(R.string.delete),
|
||||
confirmText = stringResource(R.string.ok),
|
||||
onConfirm = { rule ->
|
||||
viewModel.delete(rule)
|
||||
onIntent(ReplaceRuleIntent.DeleteRule(rule))
|
||||
showDeleteRuleDialog = null
|
||||
},
|
||||
dismissText = stringResource(R.string.cancel),
|
||||
@@ -264,42 +288,30 @@ fun ReplaceRuleScreen(
|
||||
|
||||
RuleListScaffold(
|
||||
title = "替换规则",
|
||||
state = uiState,
|
||||
state = state,
|
||||
onBackClick = { onBackClick() },
|
||||
onSearchToggle = { viewModel.setSearchMode(!uiState.isSearch) },
|
||||
onSearchQueryChange = { viewModel.setSearchKey(it) },
|
||||
onSearchToggle = { onIntent(ReplaceRuleIntent.SetSearchMode(!state.isSearch)) },
|
||||
onSearchQueryChange = { onIntent(ReplaceRuleIntent.UpdateSearchQuery(it)) },
|
||||
searchPlaceholder = stringResource(R.string.replace_purify_search),
|
||||
onClearSelection = { viewModel.setSelection(emptySet()) },
|
||||
onSelectAll = { viewModel.setSelection(rules.map { it.id }.toSet()) },
|
||||
onSelectInvert = { viewModel.setSelection(rules.map { it.id }.toSet() - selectedIds) },
|
||||
onClearSelection = { onIntent(ReplaceRuleIntent.ClearSelection) },
|
||||
onSelectAll = { onIntent(ReplaceRuleIntent.SelectAll) },
|
||||
onSelectInvert = { onIntent(ReplaceRuleIntent.InvertSelection) },
|
||||
selectionSecondaryActions = listOf(
|
||||
ActionItem(
|
||||
text = stringResource(R.string.enable),
|
||||
onClick = {
|
||||
viewModel.enableSelectionByIds(selectedIds)
|
||||
viewModel.setSelection(emptySet())
|
||||
}
|
||||
onClick = { onIntent(ReplaceRuleIntent.EnableSelection) }
|
||||
),
|
||||
ActionItem(
|
||||
text = stringResource(R.string.disable_selection),
|
||||
onClick = {
|
||||
viewModel.disableSelectionByIds(selectedIds)
|
||||
viewModel.setSelection(emptySet())
|
||||
}
|
||||
onClick = { onIntent(ReplaceRuleIntent.DisableSelection) }
|
||||
),
|
||||
ActionItem(
|
||||
text = stringResource(R.string.to_top),
|
||||
onClick = {
|
||||
viewModel.topSelectByIds(selectedIds)
|
||||
viewModel.setSelection(emptySet())
|
||||
}
|
||||
onClick = { onIntent(ReplaceRuleIntent.TopSelectByIds(selectedIds)) }
|
||||
),
|
||||
ActionItem(
|
||||
text = stringResource(R.string.to_bottom),
|
||||
onClick = {
|
||||
viewModel.bottomSelectByIds(selectedIds)
|
||||
viewModel.setSelection(emptySet())
|
||||
}
|
||||
onClick = { onIntent(ReplaceRuleIntent.BottomSelectByIds(selectedIds)) }
|
||||
),
|
||||
ActionItem(
|
||||
text = stringResource(R.string.export),
|
||||
@@ -308,8 +320,8 @@ fun ReplaceRuleScreen(
|
||||
),
|
||||
onDeleteSelected = { ids ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
viewModel.delSelectionByIds( | ||||