Jelajahi Sumber

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

HapeLee 10 bulan lalu
induk
melakukan
9d9edb2376

+ 2 - 0
app/build.gradle

@@ -382,4 +382,6 @@ dependencies {
     implementation(libs.koin.compose)
     implementation(libs.koin.compose.viewmodel)
 
+    implementation(libs.reorderable)
+
 }

+ 6 - 6
app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt

@@ -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)
     }
-}
+}

+ 49 - 14
app/src/main/java/io/legado/app/data/repository/ReplaceRuleRepository.kt

@@ -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)
-            val updated = rules.map { it.copy(order = -1) }
-            appDb.replaceRuleDao.update(*updated.toTypedArray())
+            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>) =
+    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 = -2) }
-            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 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())
         }
     }
 
-}
+}

+ 2 - 3
app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt

@@ -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
 

+ 123 - 62
app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt

@@ -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))
-                            }
-                        } else {
-                            Text(group, modifier = Modifier.weight(1f))
-                            Row {
+                            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 = {
-                                    editingGroup = group
-                                    updatedGroupName = group
+                                    viewModel.upGroup(group, updatedGroupName)
+                                    editingGroup = null
                                 }) {
-                                    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))
+                                    Icon(
+                                        Icons.Default.Check,
+                                        contentDescription = stringResource(id = R.string.ok)
+                                    )
                                 }
                             }
+                        } else {
+                            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)
+                                            )
+                                        }
+                                    }
+                                }
+                            )
                         }
                     }
                 }
@@ -785,6 +845,7 @@ fun ReplaceRuleItem(
                 AnimatedContent(targetState = name, label = "RuleNameAnimation") { targetName ->
                     Text(
                         text = targetName,
+                        style = MaterialTheme.typography.titleMedium,
                         maxLines = 2,
                         overflow = TextOverflow.Ellipsis
                     )

+ 20 - 35
app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt

@@ -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
         }
     }
 
-}
+}

+ 2 - 2
app/src/main/java/io/legado/app/ui/widget/components/exportComponents/FilePickerSheet.kt

@@ -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 - 1
app/src/main/java/io/legado/app/ui/widget/components/LazyList.kt → app/src/main/java/io/legado/app/ui/widget/components/lazylist/LazyList.kt

@@ -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

+ 2 - 2
app/src/main/java/io/legado/app/ui/widget/components/VerticalFastScroller.kt → app/src/main/java/io/legado/app/ui/widget/components/lazylist/VerticalFastScroller.kt

@@ -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
app/src/main/res/values-zh-rCN/strings.xml

@@ -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 - 0
gradle/libs.versions.toml

@@ -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"]