[优化] 优化了替换净化置顶置底逻辑,回归了时间排序下的拖动排序

This commit is contained in:
HapeLee
2025-12-27 04:21:36 +08:00
parent e2e980269c
commit 9d9edb2376
11 changed files with 218 additions and 132 deletions
+2
View File
@@ -382,4 +382,6 @@ dependencies {
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
implementation(libs.reorderable)
}
@@ -35,16 +35,16 @@ interface ReplaceRuleDao {
@Query("SELECT * FROM replace_rules ORDER BY name COLLATE NOCASE DESC")
fun flowAllNameDesc(): Flow<List<ReplaceRule>>
@Query("SELECT * FROM replace_rules WHERE `group` LIKE :key OR name LIKE :key ORDER BY sortOrder ASC")
@Query("SELECT * FROM replace_rules WHERE `group` LIKE :key OR name LIKE :key OR pattern LIKE :key OR replacement LIKE :key OR scope LIKE :key ORDER BY sortOrder ASC")
fun flowSearchAsc(key: String): Flow<List<ReplaceRule>>
@Query("SELECT * FROM replace_rules WHERE `group` LIKE :key OR name LIKE :key ORDER BY sortOrder DESC")
@Query("SELECT * FROM replace_rules WHERE `group` LIKE :key OR name LIKE :key OR pattern LIKE :key OR replacement LIKE :key OR scope LIKE :key ORDER BY sortOrder DESC")
fun flowSearchDesc(key: String): Flow<List<ReplaceRule>>
@Query("SELECT * FROM replace_rules WHERE `group` LIKE :key OR name LIKE :key ORDER BY name COLLATE NOCASE ASC")
@Query("SELECT * FROM replace_rules WHERE `group` LIKE :key OR name LIKE :key OR pattern LIKE :key OR replacement LIKE :key OR scope LIKE :key ORDER BY name COLLATE NOCASE ASC")
fun flowSearchNameAsc(key: String): Flow<List<ReplaceRule>>
@Query("SELECT * FROM replace_rules WHERE `group` LIKE :key OR name LIKE :key ORDER BY name COLLATE NOCASE DESC")
@Query("SELECT * FROM replace_rules WHERE `group` LIKE :key OR name LIKE :key OR pattern LIKE :key OR replacement LIKE :key OR scope LIKE :key ORDER BY name COLLATE NOCASE DESC")
fun flowSearchNameDesc(key: String): Flow<List<ReplaceRule>>
@Query("SELECT * FROM replace_rules WHERE `group` LIKE :key ORDER BY sortOrder ASC")
@@ -72,7 +72,7 @@ interface ReplaceRuleDao {
@Query("SELECT * FROM replace_rules WHERE `group` IS NULL OR trim(`group`) = '' OR trim(`group`) LIKE '%未分组%' ORDER BY name COLLATE NOCASE DESC")
fun flowNoGroupNameDesc(): Flow<List<ReplaceRule>>
@Query("SELECT * FROM replace_rules where `group` like :key or name like :key ORDER BY sortOrder ASC")
@Query("SELECT * FROM replace_rules where `group` like :key or name like :key or pattern like :key or replacement like :key or scope like :key ORDER BY sortOrder ASC")
fun flowSearch(key: String): Flow<List<ReplaceRule>>
@Query("SELECT * FROM replace_rules where `group` like :key ORDER BY sortOrder ASC")
@@ -186,4 +186,4 @@ interface ReplaceRuleDao {
dealGroups(list)
}.flowOn(IO)
}
}
}
@@ -45,16 +45,24 @@ class ReplaceRuleRepository {
}
}
suspend fun toTop(rule: ReplaceRule) {
suspend fun toTop(rule: ReplaceRule, isDesc: Boolean = false) {
withContext(Dispatchers.IO) {
rule.order = -1
if (isDesc) {
rule.order = appDb.replaceRuleDao.maxOrder + 1
} else {
rule.order = appDb.replaceRuleDao.minOrder - 1
}
appDb.replaceRuleDao.update(rule)
}
}
suspend fun toBottom(rule: ReplaceRule) {
suspend fun toBottom(rule: ReplaceRule, isDesc: Boolean = false) {
withContext(Dispatchers.IO) {
rule.order = -2
if (isDesc) {
rule.order = appDb.replaceRuleDao.minOrder - 1
} else {
rule.order = appDb.replaceRuleDao.maxOrder + 1
}
appDb.replaceRuleDao.update(rule)
}
}
@@ -154,32 +162,59 @@ class ReplaceRuleRepository {
appDb.replaceRuleDao.delete(*rules.toTypedArray())
}
suspend fun topByIds(ids: Set<Long>) =
suspend fun topByIds(ids: Set<Long>, isDesc: Boolean = false) =
withContext(Dispatchers.IO) {
if (ids.isEmpty()) return@withContext
val rules = appDb.replaceRuleDao.getByIds(ids)
if (isDesc) {
var maxOrder = appDb.replaceRuleDao.maxOrder
val updated = rules.map {
maxOrder++
it.copy(order = maxOrder)
}
appDb.replaceRuleDao.update(*updated.toTypedArray())
} else {
var minOrder = appDb.replaceRuleDao.minOrder
val updated = rules.map {
minOrder--
it.copy(order = minOrder)
}
appDb.replaceRuleDao.update(*updated.toTypedArray())
}
}
suspend fun bottomByIds(ids: Set<Long>, isDesc: Boolean = false) =
withContext(Dispatchers.IO) {
if (ids.isEmpty()) return@withContext
val rules = appDb.replaceRuleDao.getByIds(ids)
val updated = rules.map { it.copy(order = -1) }
appDb.replaceRuleDao.update(*updated.toTypedArray())
if (isDesc) {
var minOrder = appDb.replaceRuleDao.minOrder
val updated = rules.map {
minOrder--
it.copy(order = minOrder)
}
appDb.replaceRuleDao.update(*updated.toTypedArray())
} else {
var maxOrder = appDb.replaceRuleDao.maxOrder
val updated = rules.map {
maxOrder++
it.copy(order = maxOrder)
}
appDb.replaceRuleDao.update(*updated.toTypedArray())
}
}
suspend fun bottomByIds(ids: Set<Long>) =
withContext(Dispatchers.IO) {
if (ids.isEmpty()) return@withContext
val rules = appDb.replaceRuleDao.getByIds(ids)
val updated = rules.map { it.copy(order = -2) }
appDb.replaceRuleDao.update(*updated.toTypedArray())
}
suspend fun moveOrder(currentRules: List<ReplaceRuleItemUi>) {
suspend fun moveOrder(currentRules: List<ReplaceRuleItemUi>, isDesc: Boolean = false) {
withContext(Dispatchers.IO) {
val size = currentRules.size
val updatedRules = currentRules.mapIndexed { index, itemUi ->
itemUi.rule.copy(order = index + 1)
val order = if (isDesc) size - index else index + 1
itemUi.rule.copy(order = order)
}
appDb.replaceRuleDao.update(*updatedRules.toTypedArray())
}
}
}
}
@@ -14,7 +14,6 @@ import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.KeyboardArrowDown
@@ -34,8 +33,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.legado.app.data.entities.Bookmark
import io.legado.app.ui.widget.components.FastScrollLazyColumn
import io.legado.app.ui.widget.components.Scroller
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
import io.legado.app.ui.widget.components.lazylist.Scroller
import io.legado.app.ui.widget.components.SearchBarSection
import org.koin.androidx.compose.koinViewModel
@@ -9,12 +9,10 @@ import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -43,11 +41,9 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@@ -59,7 +55,7 @@ import io.legado.app.data.repository.UploadRepository
import io.legado.app.ui.replace.edit.ReplaceEditActivity
import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.AnimatedText
import io.legado.app.ui.widget.components.FastScrollLazyColumn
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
import io.legado.app.ui.widget.components.SearchBarSection
import io.legado.app.ui.widget.components.SelectionBottomBar
import io.legado.app.ui.widget.components.exportComponents.FilePickerSheet
@@ -71,6 +67,8 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
ExperimentalMaterial3ExpressiveApi::class
@@ -118,6 +116,16 @@ fun ReplaceRuleScreen(
var isUploading by remember { mutableStateOf(false) }
val clipboardManager = LocalClipboard.current
val hapticFeedback = LocalHapticFeedback.current
val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
viewModel.moveItemInList(from.index, to.index)
hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
}
val canReorder = remember(uiState.sortMode) {
uiState.sortMode == "asc" || uiState.sortMode == "desc"
}
val importDoc = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(),
onResult = { uri ->
@@ -177,11 +185,9 @@ fun ReplaceRuleScreen(
.map { it.rule }
val json = Gson().toJson(rulesToExport)
// [新增] 设置加载状态
isUploading = true
try {
// 使用 runCatching 处理上传
runCatching {
uploadRepository.upload(
fileName = "exportReplaceRule.json",
@@ -246,6 +252,12 @@ fun ReplaceRuleScreen(
}
}
LaunchedEffect(reorderableState.isAnyItemDragging) {
if (!reorderableState.isAnyItemDragging) {
viewModel.saveSortOrder()
}
}
if (showGroupManageSheet) {
GroupManageBottomSheet(
groups = groups,
@@ -386,11 +398,23 @@ fun ReplaceRuleScreen(
)
DropdownMenuItem(
text = { Text("名称升序") },
onClick = { viewModel.setSortMode("name_asc"); showMenu = false }
onClick = {
viewModel.setSortMode("name_asc")
showMenu = false
scope.launch {
snackbarHostState.showSnackbar("非时间排序模式下将禁用拖动")
}
}
)
DropdownMenuItem(
text = { Text("名称降序") },
onClick = { viewModel.setSortMode("name_desc"); showMenu = false }
onClick = {
viewModel.setSortMode("name_desc")
showMenu = false
scope.launch {
snackbarHostState.showSnackbar("非时间排序模式下将禁用拖动")
}
}
)
}
}
@@ -432,7 +456,7 @@ fun ReplaceRuleScreen(
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 12.dp),
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
@@ -473,30 +497,43 @@ fun ReplaceRuleScreen(
) {
items(rules, key = { it.id }) { ui ->
val isSelected = selectedRuleIds.contains(ui.id)
ReplaceRuleItem(
modifier = Modifier
.padding(horizontal = 12.dp)
.animateItem(),
name = ui.name,
isEnabled = ui.isEnabled,
isSelected = isSelected,
inSelectionMode = inSelectionMode,
onEnabledChange = { enabled ->
viewModel.update(ui.rule.copy(isEnabled = enabled))
},
onDelete = { showDeleteRuleDialog = ui.rule },
onToTop = { viewModel.toTop(ui.rule) },
onToBottom = { viewModel.toBottom(ui.rule) },
onToggleSelection = {
viewModel.toggleSelection(ui.id)
},
onClickEdit = {
context.startActivity(
ReplaceEditActivity.startIntent(context, ui.id)
)
}
)
ReorderableItem(
state = reorderableState,
key = ui.id
) { _ ->
ReplaceRuleItem(
modifier = Modifier
.padding(horizontal = 12.dp)
.then(
if (canReorder) {
Modifier.longPressDraggableHandle(
interactionSource = remember { MutableInteractionSource() }
)
} else {
Modifier
}
)
.animateItem(),
name = ui.name,
isEnabled = ui.isEnabled,
isSelected = isSelected,
inSelectionMode = inSelectionMode,
onEnabledChange = { enabled ->
viewModel.update(ui.rule.copy(isEnabled = enabled))
},
onDelete = { showDeleteRuleDialog = ui.rule },
onToTop = { viewModel.toTop(ui.rule) },
onToBottom = { viewModel.toBottom(ui.rule) },
onToggleSelection = {
viewModel.toggleSelection(ui.id)
},
onClickEdit = {
context.startActivity(
ReplaceEditActivity.startIntent(context, ui.id)
)
}
)
}
}
}
if (inSelectionMode) {
@@ -692,42 +729,65 @@ private fun GroupManageBottomSheet(
sheetState = sheetState
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(stringResource(R.string.group_manage), style = MaterialTheme.typography.titleLarge)
Text(
modifier = Modifier.padding(bottom = 16.dp),
text = stringResource(R.string.group_manage),
style = MaterialTheme.typography.titleMedium
)
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
items(groups) { group ->
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Card(modifier = Modifier.fillMaxWidth()) {
if (editingGroup == group) {
OutlinedTextField(
value = updatedGroupName,
onValueChange = { updatedGroupName = it },
modifier = Modifier.weight(1f)
)
IconButton(onClick = {
viewModel.upGroup(group, updatedGroupName)
editingGroup = null
}) {
Icon(Icons.Default.Check, contentDescription = stringResource(id = R.string.ok))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
OutlinedTextField(
value = updatedGroupName,
onValueChange = { updatedGroupName = it },
modifier = Modifier.weight(1f)
)
IconButton(onClick = {
viewModel.upGroup(group, updatedGroupName)
editingGroup = null
}) {
Icon(
Icons.Default.Check,
contentDescription = stringResource(id = R.string.ok)
)
}
}
} else {
Text(group, modifier = Modifier.weight(1f))
Row {
IconButton(onClick = {
editingGroup = group
updatedGroupName = group
}) {
Icon(Icons.Default.Edit, contentDescription = stringResource(id = R.string.edit))
ListItem(
headlineContent = { Text(group) },
trailingContent = {
Row {
IconButton(onClick = {
editingGroup = group
updatedGroupName = group
}) {
Icon(
Icons.Default.Edit,
contentDescription = stringResource(id = R.string.edit)
)
}
IconButton(onClick = { viewModel.delGroup(group) }) {
Icon(
Icons.Default.Delete,
contentDescription = stringResource(id = R.string.delete)
)
}
}
}
IconButton(onClick = { viewModel.delGroup(group) }) {
Icon(Icons.Default.Delete, contentDescription = stringResource(id = R.string.delete))
}
}
)
}
}
}
@@ -785,6 +845,7 @@ fun ReplaceRuleItem(
AnimatedContent(targetState = name, label = "RuleNameAnimation") { targetName ->
Text(
text = targetName,
style = MaterialTheme.typography.titleMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
@@ -102,34 +102,10 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application
baseFlow
.map { rules ->
val comparator = when (sortMode) {
"asc" -> compareBy<ReplaceRule> {
when (it.order) {
-1 -> Long.MIN_VALUE
-2 -> Long.MAX_VALUE
else -> it.order.toLong()
}
}
"desc" -> compareByDescending<ReplaceRule> {
when (it.order) {
-1 -> Long.MAX_VALUE
-2 -> Long.MIN_VALUE
else -> it.order.toLong()
}
}
"name_asc" -> compareBy<ReplaceRule> {
when (it.order) {
-1 -> Long.MIN_VALUE
-2 -> Long.MAX_VALUE
else -> 0L
}
}.thenBy { it.name.lowercase() }
"name_desc" -> compareBy<ReplaceRule> {
when (it.order) {
-1 -> Long.MIN_VALUE
-2 -> Long.MAX_VALUE
else -> 0L
}
}.thenByDescending { it.name.lowercase() }
"asc" -> compareBy<ReplaceRule> { it.order.toLong() }
"desc" -> compareByDescending<ReplaceRule> { it.order.toLong() }
"name_asc" -> compareBy<ReplaceRule> { it.name.lowercase() }
"name_desc" -> compareByDescending<ReplaceRule> { it.name.lowercase() }
else -> null
}
@@ -154,7 +130,7 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application
_sortMode,
_searchKey,
repository.flowGroups(),
ruleUiFlow
_uiRules
) { sortMode, searchKey, groups, rules ->
ReplaceRuleUiState(
sortMode = sortMode,
@@ -169,6 +145,14 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application
initialValue = ReplaceRuleUiState(isLoading = true)
)
init {
viewModelScope.launch {
ruleUiFlow.collect { rules ->
_uiRules.value = rules
}
}
}
fun setSortMode(mode: String) {
_sortMode.value = mode
context.putPrefString(PreferKey.replaceSortMode, mode)
@@ -196,13 +180,13 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application
fun toTop(rule: ReplaceRule) {
execute {
repository.toTop(rule)
repository.toTop(rule, _sortMode.value == "desc")
}
}
fun toBottom(rule: ReplaceRule) {
execute {
repository.toBottom(rule)
repository.toBottom(rule, _sortMode.value == "desc")
}
}
@@ -244,13 +228,13 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application
fun topSelectByIds(ids: Set<Long>) {
execute {
repository.topByIds(ids)
repository.topByIds(ids, _sortMode.value == "desc")
}
}
fun bottomSelectByIds(ids: Set<Long>) {
execute {
repository.bottomByIds(ids)
repository.bottomByIds(ids, _sortMode.value == "desc")
}
}
@@ -284,8 +268,9 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application
fun saveSortOrder() {
val currentRules = _uiRules.value
val isDesc = _sortMode.value == "desc"
execute {
repository.moveOrder(currentRules)
repository.moveOrder(currentRules, isDesc)
}
}
@@ -420,4 +405,4 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application
}
}
}
}
@@ -40,14 +40,14 @@ fun FilePickerSheet(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 32.dp),
.padding(bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier.padding(bottom = 16.dp),
text = stringResource(R.string.select_operation),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
Row(
@@ -1,4 +1,4 @@
package io.legado.app.ui.widget.components
package io.legado.app.ui.widget.components.lazylist
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
@@ -1,4 +1,4 @@
package io.legado.app.ui.widget.components
package io.legado.app.ui.widget.components.lazylist
import android.view.ViewConfiguration
import androidx.compose.animation.core.Animatable
@@ -46,7 +46,7 @@ import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastLastOrNull
import androidx.compose.ui.util.fastMaxBy
import io.legado.app.ui.widget.components.Scroller.STICKY_HEADER_KEY_PREFIX
import io.legado.app.ui.widget.components.lazylist.Scroller.STICKY_HEADER_KEY_PREFIX
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collectLatest
+1 -1
View File
@@ -42,7 +42,7 @@
<string name="replace_purify_desc">配置替换净化规则</string>
<string name="not_available">暂无</string>
<string name="enable">启用</string>
<string name="replace_purify_search">替换净化 - 搜索</string>
<string name="replace_purify_search">搜索</string>
<string name="bookshelf">书架</string>
<string name="favorites">收藏夹</string>
<string name="favorite">收藏</string>
+4
View File
@@ -72,6 +72,8 @@ zxingLite = "3.3.0"
koin-bom = "4.1.1"
biometricKtx = "1.1.0"
reorderable = "3.0.0"
[libraries]
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanistWebview" }
@@ -214,6 +216,8 @@ renderscript-intrinsics-replacement-toolkit = { module = "com.github.TomasValent
zxing-lite = { module = "com.github.jenly1314:zxing-lite", version.ref = "zxingLite" }
androidx-biometric-ktx = { group = "androidx.biometric", name = "biometric-ktx", version.ref = "biometricKtx" }
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
[bundles]
coroutines = ["kotlinx-coroutines-core", "kotlinx-coroutines-android"]
androidTest = ["androidx-espresso-core", "androidx-junit", "androidx-runner"]