Przeglądaj źródła

[优化] 优化书架界面,增加开关以控制搜索按钮优先打开搜索页还是筛选栏

HapeLee 6 miesięcy temu
rodzic
commit
7e27686fbf

+ 1 - 0
app/src/main/java/io/legado/app/constant/PreferKey.kt

@@ -241,6 +241,7 @@ object PreferKey {
     const val bookshelfTitleCenter = "bookshelfTitleCenter"
     const val bookshelfTitleCenter = "bookshelfTitleCenter"
     const val bookshelfTitleMaxLines = "bookshelfTitleMaxLines"
     const val bookshelfTitleMaxLines = "bookshelfTitleMaxLines"
     const val bookshelfCoverShadow = "bookshelfCoverShadow"
     const val bookshelfCoverShadow = "bookshelfCoverShadow"
+    const val bookshelfSearchActionDirectToSearch = "bookshelfSearchActionDirectToSearch"
 
 
     const val exploreLayoutGridLandscape = "exploreLayoutGridLandscape"
     const val exploreLayoutGridLandscape = "exploreLayoutGridLandscape"
 
 

+ 5 - 5
app/src/main/java/io/legado/app/di/appModule.kt

@@ -58,7 +58,7 @@ import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
 import io.legado.app.ui.book.changecover.ChangeCoverViewModel
 import io.legado.app.ui.book.changecover.ChangeCoverViewModel
 import io.legado.app.ui.book.changesource.ChangeBookSourceComposeViewModel
 import io.legado.app.ui.book.changesource.ChangeBookSourceComposeViewModel
 import io.legado.app.ui.book.changesource.ChangeBookSourceViewModel
 import io.legado.app.ui.book.changesource.ChangeBookSourceViewModel
-import io.legado.app.ui.book.cache.CacheViewModel
+import io.legado.app.ui.book.manage.BookshelfManageScreenViewModel
 import io.legado.app.ui.book.explore.ExploreShowViewModel
 import io.legado.app.ui.book.explore.ExploreShowViewModel
 import io.legado.app.ui.book.group.GroupViewModel
 import io.legado.app.ui.book.group.GroupViewModel
 import io.legado.app.ui.book.import.local.ImportBookViewModel
 import io.legado.app.ui.book.import.local.ImportBookViewModel
@@ -72,7 +72,7 @@ import io.legado.app.ui.book.searchContent.SearchContentViewModel
 import io.legado.app.ui.book.toc.TocViewModel
 import io.legado.app.ui.book.toc.TocViewModel
 import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
 import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
 import io.legado.app.ui.config.backupConfig.BackupConfigViewModel
 import io.legado.app.ui.config.backupConfig.BackupConfigViewModel
-import io.legado.app.ui.config.cacheConfig.CacheConfig
+import io.legado.app.ui.config.bookshelfConfig.BookshelfManageScreenConfig
 import io.legado.app.ui.config.coverConfig.CoverConfigViewModel
 import io.legado.app.ui.config.coverConfig.CoverConfigViewModel
 import io.legado.app.ui.config.otherConfig.OtherConfigViewModel
 import io.legado.app.ui.config.otherConfig.OtherConfigViewModel
 import io.legado.app.ui.config.readConfig.ReadConfigViewModel
 import io.legado.app.ui.config.readConfig.ReadConfigViewModel
@@ -124,7 +124,7 @@ val appModule = module {
     singleOf(::ResolveBookShelfStateUseCase)
     singleOf(::ResolveBookShelfStateUseCase)
     singleOf(::ShrinkDatabaseUseCase)
     singleOf(::ShrinkDatabaseUseCase)
     singleOf(::WebDavBackupUseCase)
     singleOf(::WebDavBackupUseCase)
-    singleOf(::CacheConfig)
+    singleOf(::BookshelfManageScreenConfig)
 
 
     single<UploadRepository> { DirectLinkUploadRepository() }
     single<UploadRepository> { DirectLinkUploadRepository() }
     single<AppStartupGateway> { AppStartupRepository(get()) }
     single<AppStartupGateway> { AppStartupRepository(get()) }
@@ -188,12 +188,12 @@ val appModule = module {
     viewModelOf(::RssViewModel)
     viewModelOf(::RssViewModel)
     viewModelOf(::SearchViewModel)
     viewModelOf(::SearchViewModel)
     viewModel {
     viewModel {
-        CacheViewModel(
+        BookshelfManageScreenViewModel(
             application = get(),
             application = get(),
             bookDao = get(),
             bookDao = get(),
             bookGroupDao = get(),
             bookGroupDao = get(),
             bookChapterDao = get(),
             bookChapterDao = get(),
-            cacheConfig = get(),
+            bookshelfManageScreenConfig = get(),
             batchCacheDownloadUseCase = get(),
             batchCacheDownloadUseCase = get(),
             cacheBookChaptersUseCase = get(),
             cacheBookChaptersUseCase = get(),
             changeBookSourceUseCase = get(),
             changeBookSourceUseCase = get(),

+ 2 - 1
app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt

@@ -260,6 +260,7 @@ fun ChangeSourceSheet(
     var loadingAction by remember { mutableStateOf(false) }
     var loadingAction by remember { mutableStateOf(false) }
     var showOptionsMenu by rememberSaveable { mutableStateOf(false) }
     var showOptionsMenu by rememberSaveable { mutableStateOf(false) }
     var showFilterMenu by rememberSaveable { mutableStateOf(false) }
     var showFilterMenu by rememberSaveable { mutableStateOf(false) }
+    val bookAddedToShelfText = stringResource(R.string.book_added_to_shelf)
 
 
     val editSourceResult = rememberLauncherForActivityResult(StartActivityContract(BookSourceEditActivity::class.java)) {
     val editSourceResult = rememberLauncherForActivityResult(StartActivityContract(BookSourceEditActivity::class.java)) {
         val origin = it.data?.getStringExtra("origin") ?: return@rememberLauncherForActivityResult
         val origin = it.data?.getStringExtra("origin") ?: return@rememberLauncherForActivityResult
@@ -490,7 +491,7 @@ fun ChangeSourceSheet(
                 pendingMigration = PendingSourceMigration(source, book, toc)
                 pendingMigration = PendingSourceMigration(source, book, toc)
             } else {
             } else {
                 onAddAsNew(book, toc)
                 onAddAsNew(book, toc)
-                context.toastOnUi(context.getString(R.string.book_added_to_shelf))
+                context.toastOnUi(bookAddedToShelfText)
             }
             }
             actionBook = null
             actionBook = null
         }, {
         }, {

+ 184 - 153
app/src/main/java/io/legado/app/ui/book/cache/CacheScreen.kt → app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageScreen.kt

@@ -1,4 +1,4 @@
-package io.legado.app.ui.book.cache
+package io.legado.app.ui.book.manage
 
 
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.focusRequester
 import androidx.activity.compose.BackHandler
 import androidx.activity.compose.BackHandler
@@ -120,15 +120,16 @@ import io.legado.app.utils.verificationField
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import io.legado.app.ui.widget.components.button.MediumIconButton
 import io.legado.app.ui.widget.components.button.MediumIconButton
 import org.koin.androidx.compose.koinViewModel
 import org.koin.androidx.compose.koinViewModel
+import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.rememberReorderableLazyListState
 import sh.calvin.reorderable.rememberReorderableLazyListState
 
 
-data class CacheFabAction(
+data class BookshelfManageFabAction(
     val icon: androidx.compose.ui.graphics.vector.ImageVector,
     val icon: androidx.compose.ui.graphics.vector.ImageVector,
     val label: String,
     val label: String,
     val action: () -> Unit
     val action: () -> Unit
 )
 )
 
 
-private data class CacheListState(
+private data class BookshelfManageListState(
     override val items: List<Book> = emptyList(),
     override val items: List<Book> = emptyList(),
     override val selectedIds: Set<Any> = emptySet(),
     override val selectedIds: Set<Any> = emptySet(),
     override val searchKey: String = "",
     override val searchKey: String = "",
@@ -137,21 +138,21 @@ private data class CacheListState(
 ) : ListUiState<Book>
 ) : ListUiState<Book>
 
 
 @Composable
 @Composable
-fun CacheRouteScreen(
+fun BookshelfManageRouteScreen(
     groupId: Long,
     groupId: Long,
     onBackClick: () -> Unit,
     onBackClick: () -> Unit,
-    viewModel: CacheViewModel = koinViewModel()
+    viewModel: BookshelfManageScreenViewModel = koinViewModel()
 ) {
 ) {
     LaunchedEffect(groupId) {
     LaunchedEffect(groupId) {
-        viewModel.dispatch(CacheIntent.Initialize(groupId))
+        viewModel.dispatch(BookshelfManageScreenIntent.Initialize(groupId))
     }
     }
-    CacheScreen(viewModel = viewModel, onBackClick = onBackClick)
+    BookshelfManageScreen(viewModel = viewModel, onBackClick = onBackClick)
 }
 }
 
 
 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 @Composable
-private fun CacheScreen(
-    viewModel: CacheViewModel,
+private fun BookshelfManageScreen(
+    viewModel: BookshelfManageScreenViewModel,
     onBackClick: () -> Unit
     onBackClick: () -> Unit
 ) {
 ) {
     val state by viewModel.uiState.collectAsStateWithLifecycle()
     val state by viewModel.uiState.collectAsStateWithLifecycle()
@@ -203,6 +204,7 @@ private fun CacheScreen(
     val exportFileNameText = stringResource(R.string.export_file_name)
     val exportFileNameText = stringResource(R.string.export_file_name)
     val resultAnalyzedText = stringResource(R.string.result_analyzed)
     val resultAnalyzedText = stringResource(R.string.result_analyzed)
     val errorScopeInputText = stringResource(R.string.error_scope_input)
     val errorScopeInputText = stringResource(R.string.error_scope_input)
+    val noGroupText = stringResource(R.string.no_group)
     val exportFileNameHintText = "书名:《{name}》 作者:{author}"
     val exportFileNameHintText = "书名:《{name}》 作者:{author}"
     val exportFileNameHelpText = """
     val exportFileNameHelpText = """
 支持变量:{name}(书名)、{author}(作者)、{group}(分组)、{source}(书源)、{remark}(备注)。
 支持变量:{name}(书名)、{author}(作者)、{group}(分组)、{source}(书源)、{remark}(备注)。
@@ -218,15 +220,15 @@ private fun CacheScreen(
     val booksByUrl = remember(state.books) { state.books.associateBy { it.bookUrl } }
     val booksByUrl = remember(state.books) { state.books.associateBy { it.bookUrl } }
     val userGroups = remember(state.groupList) { state.groupList.filter { it.groupId > 0L } }
     val userGroups = remember(state.groupList) { state.groupList.filter { it.groupId > 0L } }
 
 
-    val groupNameResolver: (Book) -> String = remember(userGroups, context) {
+    val groupNameResolver: (Book) -> String = remember(userGroups, noGroupText) {
         { book ->
         { book ->
             if (book.group <= 0L) {
             if (book.group <= 0L) {
-                context.getString(R.string.no_group)
+                noGroupText
             } else {
             } else {
                 val groups = userGroups.filter {
                 val groups = userGroups.filter {
                     (book.group and it.groupId) > 0L
                     (book.group and it.groupId) > 0L
                 }
                 }
-                if (groups.isEmpty()) context.getString(R.string.no_group)
+                if (groups.isEmpty()) noGroupText
                 else groups.joinToString("、") { it.groupName }
                 else groups.joinToString("、") { it.groupName }
             }
             }
         }
         }
@@ -245,7 +247,7 @@ private fun CacheScreen(
         }
         }
     }
     }
     val listUiState = remember(filteredBooks, selectedBookUrls, searchKey, isSearchMode) {
     val listUiState = remember(filteredBooks, selectedBookUrls, searchKey, isSearchMode) {
-        CacheListState(
+        BookshelfManageListState(
             items = filteredBooks,
             items = filteredBooks,
             selectedIds = selectedBookUrls.mapTo(linkedSetOf()) { it as Any },
             selectedIds = selectedBookUrls.mapTo(linkedSetOf()) { it as Any },
             searchKey = searchKey,
             searchKey = searchKey,
@@ -327,9 +329,9 @@ private fun CacheScreen(
     LaunchedEffect(Unit) {
     LaunchedEffect(Unit) {
         viewModel.effects.collect { effect ->
         viewModel.effects.collect { effect ->
             when (effect) {
             when (effect) {
-                is CacheEffect.ShowMessage -> context.toastOnUi(effect.message)
-                is CacheEffect.NotifyBookChanged -> Unit
-                is CacheEffect.OpenBookInfo -> context.startActivity<BookInfoActivity> {
+                is BookshelfManageScreenEffect.ShowMessage -> context.toastOnUi(effect.message)
+                is BookshelfManageScreenEffect.NotifyBookChanged -> Unit
+                is BookshelfManageScreenEffect.OpenBookInfo -> context.startActivity<BookInfoActivity> {
                     putExtra("name", effect.name)
                     putExtra("name", effect.name)
                     putExtra("author", effect.author)
                     putExtra("author", effect.author)
                     putExtra("bookUrl", effect.bookUrl)
                     putExtra("bookUrl", effect.bookUrl)
@@ -398,20 +400,20 @@ private fun CacheScreen(
         return if (targetBooks.all { it.group == firstGroup }) firstGroup else 0L
         return if (targetBooks.all { it.group == firstGroup }) firstGroup else 0L
     }
     }
     val fabItems = listOf(
     val fabItems = listOf(
-        CacheFabAction(
+        BookshelfManageFabAction(
             Icons.Default.SelectAll,
             Icons.Default.SelectAll,
             stringResource(R.string.select_all)
             stringResource(R.string.select_all)
         ) {
         ) {
             selectedBookUrls = state.books.mapTo(hashSetOf()) { it.bookUrl }
             selectedBookUrls = state.books.mapTo(hashSetOf()) { it.bookUrl }
         },
         },
-        CacheFabAction(
+        BookshelfManageFabAction(
             Icons.Default.Refresh,
             Icons.Default.Refresh,
             stringResource(R.string.revert_selection)
             stringResource(R.string.revert_selection)
         ) {
         ) {
             val visibleBookUrls = booksByUrl.keys
             val visibleBookUrls = booksByUrl.keys
             selectedBookUrls = visibleBookUrls - selectedBookUrls
             selectedBookUrls = visibleBookUrls - selectedBookUrls
         },
         },
-        CacheFabAction(
+        BookshelfManageFabAction(
             Icons.Default.Download,
             Icons.Default.Download,
             "缓存选中"
             "缓存选中"
         ) {
         ) {
@@ -419,7 +421,7 @@ private fun CacheScreen(
                 showBatchDownloadConfirmDialog = true
                 showBatchDownloadConfirmDialog = true
             }
             }
         },
         },
-        CacheFabAction(
+        BookshelfManageFabAction(
             Icons.Default.Refresh,
             Icons.Default.Refresh,
             "批量换源"
             "批量换源"
         ) {
         ) {
@@ -427,7 +429,7 @@ private fun CacheScreen(
                 showBatchSourcePickerSheet = true
                 showBatchSourcePickerSheet = true
             }
             }
         },
         },
-        CacheFabAction(
+        BookshelfManageFabAction(
             Icons.Default.Bookmarks,
             Icons.Default.Bookmarks,
             stringResource(R.string.move_to_group)
             stringResource(R.string.move_to_group)
         ) {
         ) {
@@ -437,20 +439,20 @@ private fun CacheScreen(
                 showGroupSelectSheet = true
                 showGroupSelectSheet = true
             }
             }
         },
         },
-        CacheFabAction(
+        BookshelfManageFabAction(
             Icons.Default.Upload,
             Icons.Default.Upload,
             "导出选中"
             "导出选中"
         ) {
         ) {
             exportSelected()
             exportSelected()
         },
         },
-        CacheFabAction(
+        BookshelfManageFabAction(
             Icons.Default.Delete,
             Icons.Default.Delete,
             stringResource(R.string.clear_cache)
             stringResource(R.string.clear_cache)
         ) {
         ) {
-            viewModel.dispatch(CacheIntent.ClearCachesForBooks(selectedBookUrls))
+            viewModel.dispatch(BookshelfManageScreenIntent.ClearCachesForBooks(selectedBookUrls))
             clearSelection()
             clearSelection()
         },
         },
-        CacheFabAction(
+        BookshelfManageFabAction(
             Icons.Default.Delete,
             Icons.Default.Delete,
             stringResource(R.string.delete)
             stringResource(R.string.delete)
         ) {
         ) {
@@ -462,6 +464,17 @@ private fun CacheScreen(
         }
         }
     )
     )
     val listState = rememberLazyListState()
     val listState = rememberLazyListState()
+    val canReorderBooks = state.bookSort == 3 && !isSearchMode && selectedBookUrls.isEmpty()
+    val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
+        if (canReorderBooks) {
+            viewModel.dispatch(
+                BookshelfManageScreenIntent.MoveBookOrder(
+                    fromIndex = from.index,
+                    toIndex = to.index
+                )
+            )
+        }
+    }
     ListScaffold(
     ListScaffold(
         title = if (inSelectionMode) {
         title = if (inSelectionMode) {
             "已选 ${selectedBookUrls.size}/${filteredBooks.size}"
             "已选 ${selectedBookUrls.size}/${filteredBooks.size}"
@@ -495,7 +508,7 @@ private fun CacheScreen(
                             isSelected = group.groupId == state.groupId,
                             isSelected = group.groupId == state.groupId,
                             onClick = {
                             onClick = {
                                 dismiss()
                                 dismiss()
-                                viewModel.dispatch(CacheIntent.ChangeGroup(group.groupId))
+                                viewModel.dispatch(BookshelfManageScreenIntent.ChangeGroup(group.groupId))
                             }
                             }
                         )
                         )
                     }
                     }
@@ -508,7 +521,7 @@ private fun CacheScreen(
                 onClick = {
                 onClick = {
                     dismiss()
                     dismiss()
                     if (state.isDownloadRunning) {
                     if (state.isDownloadRunning) {
-                        viewModel.dispatch(CacheIntent.StopDownload)
+                        viewModel.dispatch(BookshelfManageScreenIntent.StopDownload)
                     } else {
                     } else {
                         showDownloadAllConfirmDialog = true
                         showDownloadAllConfirmDialog = true
                     }
                     }
@@ -553,7 +566,7 @@ private fun CacheScreen(
                 onClick = {
                 onClick = {
                     dismiss()
                     dismiss()
                     viewModel.dispatch(
                     viewModel.dispatch(
-                        CacheIntent.SetExportUseReplace(!state.exportConfig.exportUseReplace)
+                        BookshelfManageScreenIntent.SetExportUseReplace(!state.exportConfig.exportUseReplace)
                     )
                     )
                 }
                 }
             )
             )
@@ -563,7 +576,7 @@ private fun CacheScreen(
                 onClick = {
                 onClick = {
                     dismiss()
                     dismiss()
                     viewModel.dispatch(
                     viewModel.dispatch(
-                        CacheIntent.SetEnableCustomExport(!state.exportConfig.enableCustomExport)
+                        BookshelfManageScreenIntent.SetEnableCustomExport(!state.exportConfig.enableCustomExport)
                     )
                     )
                 }
                 }
             )
             )
@@ -573,7 +586,7 @@ private fun CacheScreen(
                 onClick = {
                 onClick = {
                     dismiss()
                     dismiss()
                     viewModel.dispatch(
                     viewModel.dispatch(
-                        CacheIntent.SetExportNoChapterName(!state.exportConfig.exportNoChapterName)
+                        BookshelfManageScreenIntent.SetExportNoChapterName(!state.exportConfig.exportNoChapterName)
                     )
                     )
                 }
                 }
             )
             )
@@ -583,7 +596,7 @@ private fun CacheScreen(
                 onClick = {
                 onClick = {
                     dismiss()
                     dismiss()
                     viewModel.dispatch(
                     viewModel.dispatch(
-                        CacheIntent.SetExportToWebDav(!state.exportConfig.exportToWebDav)
+                        BookshelfManageScreenIntent.SetExportToWebDav(!state.exportConfig.exportToWebDav)
                     )
                     )
                 }
                 }
             )
             )
@@ -593,7 +606,7 @@ private fun CacheScreen(
                 onClick = {
                 onClick = {
                     dismiss()
                     dismiss()
                     viewModel.dispatch(
                     viewModel.dispatch(
-                        CacheIntent.SetExportPictureFile(!state.exportConfig.exportPictureFile)
+                        BookshelfManageScreenIntent.SetExportPictureFile(!state.exportConfig.exportPictureFile)
                     )
                     )
                 }
                 }
             )
             )
@@ -603,7 +616,7 @@ private fun CacheScreen(
                 onClick = {
                 onClick = {
                     dismiss()
                     dismiss()
                     viewModel.dispatch(
                     viewModel.dispatch(
-                        CacheIntent.SetParallelExportBook(!state.exportConfig.parallelExportBook)
+                        BookshelfManageScreenIntent.SetParallelExportBook(!state.exportConfig.parallelExportBook)
                     )
                     )
                 }
                 }
             )
             )
@@ -680,119 +693,137 @@ private fun CacheScreen(
                 val exportMsg = remember(renderVersion, book.bookUrl) {
                 val exportMsg = remember(renderVersion, book.bookUrl) {
                     ExportBookService.exportMsg[book.bookUrl]
                     ExportBookService.exportMsg[book.bookUrl]
                 }
                 }
-                NormalCard(
-                    modifier = Modifier.fillMaxWidth(),
-                    onClick = { toggleBookSelection(book) },
-                    onLongClick = { toggleBookSelection(book) },
-                    containerColor = if (isSelected) {
-                        LegadoTheme.colorScheme.surfaceContainerHigh
-                    } else {
-                        LegadoTheme.colorScheme.surfaceContainerLow
-                    }
+                ReorderableItem(
+                    state = reorderableState,
+                    key = book.bookUrl,
+                    enabled = canReorderBooks
                 ) {
                 ) {
-                    Column(
+                    NormalCard(
                         modifier = Modifier
                         modifier = Modifier
                             .fillMaxWidth()
                             .fillMaxWidth()
-                            .adaptiveHorizontalPadding(vertical = 12.dp),
-                        verticalArrangement = Arrangement.SpaceBetween
+                            .then(
+                                if (canReorderBooks) {
+                                    Modifier.longPressDraggableHandle()
+                                } else {
+                                    Modifier
+                                }
+                            ),
+                        onClick = { toggleBookSelection(book) },
+                        onLongClick = { toggleBookSelection(book) },
+                        containerColor = if (isSelected) {
+                            LegadoTheme.colorScheme.surfaceContainerHigh
+                        } else {
+                            LegadoTheme.colorScheme.surfaceContainerLow
+                        }
                     ) {
                     ) {
-                        Row(
-                            modifier = Modifier.fillMaxWidth(),
-                            horizontalArrangement = Arrangement.spacedBy(8.dp)
+                        Column(
+                            modifier = Modifier
+                                .fillMaxWidth()
+                                .adaptiveHorizontalPadding(vertical = 12.dp),
+                            verticalArrangement = Arrangement.SpaceBetween
                         ) {
                         ) {
-                            Column(
-                                modifier = Modifier.weight(1f),
-                                verticalArrangement = Arrangement.spacedBy(4.dp)
+                            Row(
+                                modifier = Modifier.fillMaxWidth(),
+                                horizontalArrangement = Arrangement.spacedBy(8.dp)
                             ) {
                             ) {
-                                AppText(
-                                    text = book.name,
-                                    style = LegadoTheme.typography.titleSmallEmphasized,
-                                    maxLines = 1
-                                )
-                                AppText(
-                                    text = book.getRealAuthor(),
-                                    style = LegadoTheme.typography.bodySmall
-                                )
-                                AppText(
-                                    text = "${groupNameResolver(book)} | ${book.originName.ifBlank { book.origin }}",
-                                    style = LegadoTheme.typography.labelSmallEmphasized.copy(color = LegadoTheme.colorScheme.primary)
-                                )
-                                if (exportMsg != null) {
-                                    AppText(text = exportMsg, modifier = Modifier.padding(top = 2.dp))
-                                }
-                            }
-                            TextCard(
-                                text = if (book.isLocal) {
-                                    stringResource(R.string.local_book)
-                                } else {
-                                    stringResource(
-                                        R.string.download_count,
-                                        cacheCount,
-                                        book.totalChapterNum
+                                Column(
+                                    modifier = Modifier.weight(1f),
+                                    verticalArrangement = Arrangement.spacedBy(4.dp)
+                                ) {
+                                    AppText(
+                                        text = book.name,
+                                        style = LegadoTheme.typography.titleSmallEmphasized,
+                                        maxLines = 1
+                                    )
+                                    AppText(
+                                        text = book.getRealAuthor(),
+                                        style = LegadoTheme.typography.bodySmall
+                                    )
+                                    AppText(
+                                        text = "${groupNameResolver(book)} | ${book.originName.ifBlank { book.origin }}",
+                                        style = LegadoTheme.typography.labelSmallEmphasized.copy(color = LegadoTheme.colorScheme.primary)
                                     )
                                     )
+                                    if (exportMsg != null) {
+                                        AppText(text = exportMsg, modifier = Modifier.padding(top = 2.dp))
+                                    }
                                 }
                                 }
-                            )
-                        }
-                        Row(
-                            modifier = Modifier.fillMaxWidth(),
-                            horizontalArrangement = Arrangement.End
-                        ) {
-                            Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
-                                SmallTonalIconButton(
-                                    onClick = {
-                                        if (!book.isLocal) {
-                                            viewModel.dispatch(CacheIntent.ToggleBookDownload(book))
-                                        }
-                                    },
-                                    imageVector = if (isDownloading) Icons.Default.Stop else Icons.Default.Download,
-                                    contentDescription = "download"
+                                TextCard(
+                                    text = if (book.isLocal) {
+                                        stringResource(R.string.local_book)
+                                    } else {
+                                        stringResource(
+                                            R.string.download_count,
+                                            cacheCount,
+                                            book.totalChapterNum
+                                        )
+                                    }
                                 )
                                 )
-                                SmallTonalIconButton(
-                                    onClick = { exportBook(book) },
-                                    imageVector = Icons.Default.Upload,
-                                    contentDescription = "upload"
-                                )
-                                SmallTonalIconButton(
-                                    onClick = {
-                                        pendingMoveGroupBookUrl = book.bookUrl
-                                        groupPickerCurrentGroupId = book.group.coerceAtLeast(0L)
-                                        showGroupSelectSheet = true
-                                    },
-                                    imageVector = Icons.Default.Bookmarks,
-                                    contentDescription = "group"
-                                )
-                                SmallTonalIconButton(
-                                    onClick = { moreMenuBookUrl = book.bookUrl },
-                                    imageVector = Icons.Default.MoreVert,
-                                    contentDescription = "more"
-                                )
-                                RoundDropdownMenu(
-                                    expanded = moreMenuBookUrl == book.bookUrl,
-                                    onDismissRequest = { moreMenuBookUrl = null }
-                                ) { dismiss ->
-                                    RoundDropdownMenuItem(
-                                        text = "换源",
+                            }
+                            Row(
+                                modifier = Modifier.fillMaxWidth(),
+                                horizontalArrangement = Arrangement.End
+                            ) {
+                                Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+                                    SmallTonalIconButton(
                                         onClick = {
                                         onClick = {
-                                            singleChangeSourceBook = book
-                                            dismiss()
-                                        }
+                                            if (!book.isLocal) {
+                                                viewModel.dispatch(BookshelfManageScreenIntent.ToggleBookDownload(book))
+                                            }
+                                        },
+                                        imageVector = if (isDownloading) Icons.Default.Stop else Icons.Default.Download,
+                                        contentDescription = "download"
                                     )
                                     )
-                                    RoundDropdownMenuItem(
-                                        text = "删除书籍",
-                                        onClick = {
-                                            pendingDeleteBookUrls = setOf(book.bookUrl)
-                                            deleteOriginalBookFile = state.deleteBookOriginal
-                                            showDeleteBookConfirmDialog = true
-                                            dismiss()
-                                        }
+                                    SmallTonalIconButton(
+                                        onClick = { exportBook(book) },
+                                        imageVector = Icons.Default.Upload,
+                                        contentDescription = "upload"
                                     )
                                     )
-                                    RoundDropdownMenuItem(
-                                        text = "删除缓存",
+                                    SmallTonalIconButton(
                                         onClick = {
                                         onClick = {
-                                            viewModel.dispatch(CacheIntent.ClearCachesForBooks(setOf(book.bookUrl)))
-                                            dismiss()
-                                        }
+                                            pendingMoveGroupBookUrl = book.bookUrl
+                                            groupPickerCurrentGroupId = book.group.coerceAtLeast(0L)
+                                            showGroupSelectSheet = true
+                                        },
+                                        imageVector = Icons.Default.Bookmarks,
+                                        contentDescription = "group"
+                                    )
+                                    SmallTonalIconButton(
+                                        onClick = { moreMenuBookUrl = book.bookUrl },
+                                        imageVector = Icons.Default.MoreVert,
+                                        contentDescription = "more"
                                     )
                                     )
+                                    RoundDropdownMenu(
+                                        expanded = moreMenuBookUrl == book.bookUrl,
+                                        onDismissRequest = { moreMenuBookUrl = null }
+                                    ) { dismiss ->
+                                        RoundDropdownMenuItem(
+                                            text = "换源",
+                                            onClick = {
+                                                singleChangeSourceBook = book
+                                                dismiss()
+                                            }
+                                        )
+                                        RoundDropdownMenuItem(
+                                            text = "删除书籍",
+                                            onClick = {
+                                                pendingDeleteBookUrls = setOf(book.bookUrl)
+                                                deleteOriginalBookFile = state.deleteBookOriginal
+                                                showDeleteBookConfirmDialog = true
+                                                dismiss()
+                                            }
+                                        )
+                                        RoundDropdownMenuItem(
+                                            text = "删除缓存",
+                                            onClick = {
+                                                viewModel.dispatch(
+                                                    BookshelfManageScreenIntent.ClearCachesForBooks(
+                                                        setOf(book.bookUrl)
+                                                    )
+                                                )
+                                                dismiss()
+                                            }
+                                        )
+                                    }
                                 }
                                 }
                             }
                             }
                         }
                         }
@@ -809,7 +840,7 @@ private fun CacheScreen(
             onDismissRequest = { singleChangeSourceBook = null },
             onDismissRequest = { singleChangeSourceBook = null },
             onReplace = { source, newBook, toc, options ->
             onReplace = { source, newBook, toc, options ->
                 viewModel.dispatch(
                 viewModel.dispatch(
-                    CacheIntent.ChangeBookSource(
+                    BookshelfManageScreenIntent.ChangeBookSource(
                         oldBookUrl = book.bookUrl,
                         oldBookUrl = book.bookUrl,
                         source = source,
                         source = source,
                         book = newBook,
                         book = newBook,
@@ -842,7 +873,7 @@ private fun CacheScreen(
         onDismissRequest = { pendingBatchSources = emptyList() },
         onDismissRequest = { pendingBatchSources = emptyList() },
         onConfirm = { options ->
         onConfirm = { options ->
             viewModel.dispatch(
             viewModel.dispatch(
-                CacheIntent.BatchChangeBookSource(
+                BookshelfManageScreenIntent.BatchChangeBookSource(
                     bookUrls = selectedBookUrls,
                     bookUrls = selectedBookUrls,
                     sources = pendingBatchSources,
                     sources = pendingBatchSources,
                     options = options,
                     options = options,
@@ -856,18 +887,18 @@ private fun CacheScreen(
     BatchChangePreviewSheet(
     BatchChangePreviewSheet(
         show = state.batchChangePreviewItems.isNotEmpty(),
         show = state.batchChangePreviewItems.isNotEmpty(),
         items = state.batchChangePreviewItems,
         items = state.batchChangePreviewItems,
-        onDismissRequest = { viewModel.dispatch(CacheIntent.DismissBatchChangePreview) },
+        onDismissRequest = { viewModel.dispatch(BookshelfManageScreenIntent.DismissBatchChangePreview) },
         onOpenBook = { book, inBookshelf ->
         onOpenBook = { book, inBookshelf ->
-            viewModel.dispatch(CacheIntent.OpenBookInfoPreview(book, inBookshelf))
+            viewModel.dispatch(BookshelfManageScreenIntent.OpenBookInfoPreview(book, inBookshelf))
         },
         },
         onManualSearch = { book -> manualSearchPreviewBook = book },
         onManualSearch = { book -> manualSearchPreviewBook = book },
-        onSkip = { bookUrl -> viewModel.dispatch(CacheIntent.SkipPreviewItem(bookUrl)) },
-        onMigrate = { bookUrl -> viewModel.dispatch(CacheIntent.MigratePreviewItem(bookUrl)) },
+        onSkip = { bookUrl -> viewModel.dispatch(BookshelfManageScreenIntent.SkipPreviewItem(bookUrl)) },
+        onMigrate = { bookUrl -> viewModel.dispatch(BookshelfManageScreenIntent.MigratePreviewItem(bookUrl)) },
         onAddToShelf = { bookUrl ->
         onAddToShelf = { bookUrl ->
-            viewModel.dispatch(CacheIntent.AddPreviewItemToShelf(bookUrl))
+            viewModel.dispatch(BookshelfManageScreenIntent.AddPreviewItemToShelf(bookUrl))
         },
         },
         onShowOtherSources = { item -> otherSourcePreviewItem = item },
         onShowOtherSources = { item -> otherSourcePreviewItem = item },
-        onMigrateAll = { viewModel.dispatch(CacheIntent.MigrateAllPreviewItems) },
+        onMigrateAll = { viewModel.dispatch(BookshelfManageScreenIntent.MigrateAllPreviewItems) },
     )
     )
 
 
     manualSearchPreviewBook?.let { book ->
     manualSearchPreviewBook?.let { book ->
@@ -877,7 +908,7 @@ private fun CacheScreen(
             onDismissRequest = { manualSearchPreviewBook = null },
             onDismissRequest = { manualSearchPreviewBook = null },
             onReplace = { source, newBook, toc, _ ->
             onReplace = { source, newBook, toc, _ ->
                 viewModel.dispatch(
                 viewModel.dispatch(
-                    CacheIntent.UpdatePreviewItem(
+                    BookshelfManageScreenIntent.UpdatePreviewItem(
                         oldBookUrl = book.bookUrl,
                         oldBookUrl = book.bookUrl,
                         source = source,
                         source = source,
                         book = newBook,
                         book = newBook,
@@ -896,11 +927,11 @@ private fun CacheScreen(
         item = otherSourcePreviewItem,
         item = otherSourcePreviewItem,
         onDismissRequest = { otherSourcePreviewItem = null },
         onDismissRequest = { otherSourcePreviewItem = null },
         onSelect = { oldBookUrl, index ->
         onSelect = { oldBookUrl, index ->
-            viewModel.dispatch(CacheIntent.SelectPreviewCandidate(oldBookUrl, index))
+            viewModel.dispatch(BookshelfManageScreenIntent.SelectPreviewCandidate(oldBookUrl, index))
             otherSourcePreviewItem = null
             otherSourcePreviewItem = null
         },
         },
         onOpenBook = { book ->
         onOpenBook = { book ->
-            viewModel.dispatch(CacheIntent.OpenBookInfoPreview(book, false))
+            viewModel.dispatch(BookshelfManageScreenIntent.OpenBookInfoPreview(book, false))
         }
         }
     )
     )
 
 
@@ -908,7 +939,7 @@ private fun CacheScreen(
         show = state.isChangingSource || state.changeSourceError != null,
         show = state.isChangingSource || state.changeSourceError != null,
         onDismissRequest = {
         onDismissRequest = {
             if (!state.isChangingSource) {
             if (!state.isChangingSource) {
-                viewModel.dispatch(CacheIntent.DismissChangeSourceStatus)
+                viewModel.dispatch(BookshelfManageScreenIntent.DismissChangeSourceStatus)
             }
             }
         },
         },
         title = stringResource(R.string.change_source_batch),
         title = stringResource(R.string.change_source_batch),
@@ -936,7 +967,7 @@ private fun CacheScreen(
         onConfirm = if (state.isChangingSource) {
         onConfirm = if (state.isChangingSource) {
             null
             null
         } else {
         } else {
-            { viewModel.dispatch(CacheIntent.DismissChangeSourceStatus) }
+            { viewModel.dispatch(BookshelfManageScreenIntent.DismissChangeSourceStatus) }
         },
         },
     )
     )
 
 
@@ -959,7 +990,7 @@ private fun CacheScreen(
         onConfirm = {
         onConfirm = {
             showBatchDownloadConfirmDialog = false
             showBatchDownloadConfirmDialog = false
             viewModel.dispatch(
             viewModel.dispatch(
-                CacheIntent.DownloadBooks(
+                BookshelfManageScreenIntent.DownloadBooks(
                     bookUrls = selectedBookUrls,
                     bookUrls = selectedBookUrls,
                     downloadAllChapters = false
                     downloadAllChapters = false
                 )
                 )
@@ -999,7 +1030,7 @@ private fun CacheScreen(
         onConfirm = {
         onConfirm = {
             showDeleteBookConfirmDialog = false
             showDeleteBookConfirmDialog = false
             viewModel.dispatch(
             viewModel.dispatch(
-                CacheIntent.DeleteBooks(
+                BookshelfManageScreenIntent.DeleteBooks(
                     bookUrls = pendingDeleteBookUrls,
                     bookUrls = pendingDeleteBookUrls,
                     deleteOriginal = deleteOriginalBookFile
                     deleteOriginal = deleteOriginalBookFile
                 )
                 )
@@ -1019,7 +1050,7 @@ private fun CacheScreen(
             val moveSet = pendingMoveGroupBookUrl?.let { setOf(it) } ?: selectedBookUrls
             val moveSet = pendingMoveGroupBookUrl?.let { setOf(it) } ?: selectedBookUrls
             val targetGroupId = groupId.coerceAtLeast(0L)
             val targetGroupId = groupId.coerceAtLeast(0L)
             viewModel.dispatch(
             viewModel.dispatch(
-                CacheIntent.MoveBooksToGroup(
+                BookshelfManageScreenIntent.MoveBooksToGroup(
                     bookUrls = moveSet,
                     bookUrls = moveSet,
                     groupId = targetGroupId
                     groupId = targetGroupId
                 )
                 )
@@ -1040,7 +1071,7 @@ private fun CacheScreen(
         onConfirm = {
         onConfirm = {
             showDownloadAllConfirmDialog = false
             showDownloadAllConfirmDialog = false
             viewModel.dispatch(
             viewModel.dispatch(
-                CacheIntent.StartDownloadForVisibleBooks(
+                BookshelfManageScreenIntent.StartDownloadForVisibleBooks(
                     books = state.books,
                     books = state.books,
                     downloadAllChapters = true
                     downloadAllChapters = true
                 )
                 )
@@ -1060,7 +1091,7 @@ private fun CacheScreen(
                 icon = if (type == "epub") Icons.Default.Upload else Icons.Default.Download,
                 icon = if (type == "epub") Icons.Default.Upload else Icons.Default.Download,
                 text = type,
                 text = type,
                 onClick = {
                 onClick = {
-                    viewModel.dispatch(CacheIntent.SetExportType(index))
+                    viewModel.dispatch(BookshelfManageScreenIntent.SetExportType(index))
                     showExportTypeDialog = false
                     showExportTypeDialog = false
                 }
                 }
             )
             )
@@ -1085,7 +1116,7 @@ private fun CacheScreen(
         },
         },
         confirmText = stringResource(android.R.string.ok),
         confirmText = stringResource(android.R.string.ok),
         onConfirm = {
         onConfirm = {
-            viewModel.dispatch(CacheIntent.SetBookExportFileName(exportFileNameInput))
+            viewModel.dispatch(BookshelfManageScreenIntent.SetBookExportFileName(exportFileNameInput))
             showExportFileNameDialog = false
             showExportFileNameDialog = false
         },
         },
         dismissText = stringResource(android.R.string.cancel),
         dismissText = stringResource(android.R.string.cancel),
@@ -1107,7 +1138,7 @@ private fun CacheScreen(
         confirmText = stringResource(android.R.string.ok),
         confirmText = stringResource(android.R.string.ok),
         onConfirm = {
         onConfirm = {
             viewModel.dispatch(
             viewModel.dispatch(
-                CacheIntent.SetExportCharset(
+                BookshelfManageScreenIntent.SetExportCharset(
                     exportCharsetInput.ifBlank { "UTF-8" }
                     exportCharsetInput.ifBlank { "UTF-8" }
                 )
                 )
             )
             )
@@ -1228,7 +1259,7 @@ private fun CacheScreen(
                     customEpisodeExportNameInput
                     customEpisodeExportNameInput
                 )
                 )
             ) {
             ) {
-                viewModel.dispatch(CacheIntent.SetEpisodeExportFileName(customEpisodeExportNameInput))
+                viewModel.dispatch(BookshelfManageScreenIntent.SetEpisodeExportFileName(customEpisodeExportNameInput))
             }
             }
             val epubSize = customEpubSizeInput.toIntOrNull()?.coerceAtLeast(1) ?: 1
             val epubSize = customEpubSizeInput.toIntOrNull()?.coerceAtLeast(1) ?: 1
             context.startService<ExportBookService> {
             context.startService<ExportBookService> {

+ 177 - 127
app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt → app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageScreenViewModel.kt

@@ -1,4 +1,4 @@
-package io.legado.app.ui.book.cache
+package io.legado.app.ui.book.manage
 
 
 import android.app.Application
 import android.app.Application
 import androidx.lifecycle.viewModelScope
 import androidx.lifecycle.viewModelScope
@@ -29,10 +29,12 @@ import io.legado.app.help.book.removeType
 import io.legado.app.model.CacheBook
 import io.legado.app.model.CacheBook
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.service.ExportBookService
 import io.legado.app.service.ExportBookService
-import io.legado.app.ui.config.cacheConfig.CacheConfig
+import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
+import io.legado.app.ui.config.bookshelfConfig.BookshelfManageScreenConfig
 import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.ui.main.bookshelf.toLightBook
 import io.legado.app.ui.main.bookshelf.toLightBook
 import io.legado.app.utils.cnCompare
 import io.legado.app.utils.cnCompare
+import io.legado.app.utils.move
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableSharedFlow
@@ -44,7 +46,7 @@ import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import kotlin.math.max
 import kotlin.math.max
 
 
-data class CacheExportConfig(
+data class BookshelfManageScreenExportConfig(
     val exportUseReplace: Boolean = true,
     val exportUseReplace: Boolean = true,
     val enableCustomExport: Boolean = false,
     val enableCustomExport: Boolean = false,
     val exportNoChapterName: Boolean = false,
     val exportNoChapterName: Boolean = false,
@@ -57,11 +59,13 @@ data class CacheExportConfig(
     val episodeExportFileName: String = ""
     val episodeExportFileName: String = ""
 )
 )
 
 
-data class CacheUiState(
+data class BookshelfManageScreenUiState(
     val groupId: Long = -1,
     val groupId: Long = -1,
     val groupName: String? = null,
     val groupName: String? = null,
     val groupList: List<BookGroup> = emptyList(),
     val groupList: List<BookGroup> = emptyList(),
     val books: List<Book> = emptyList(),
     val books: List<Book> = emptyList(),
+    val bookSort: Int = BookshelfConfig.bookshelfSort,
+    val bookSortOrder: Int = BookshelfConfig.bookshelfSortOrder,
     val isDownloadRunning: Boolean = false,
     val isDownloadRunning: Boolean = false,
     val isChangingSource: Boolean = false,
     val isChangingSource: Boolean = false,
     val changeSourceProgress: String? = null,
     val changeSourceProgress: String? = null,
@@ -71,74 +75,75 @@ data class CacheUiState(
     val batchChangeOptions: ChangeSourceMigrationOptions = ChangeSourceMigrationOptions(),
     val batchChangeOptions: ChangeSourceMigrationOptions = ChangeSourceMigrationOptions(),
     val cacheVersion: Long = 0,
     val cacheVersion: Long = 0,
     val deleteBookOriginal: Boolean = LocalConfig.deleteBookOriginal,
     val deleteBookOriginal: Boolean = LocalConfig.deleteBookOriginal,
-    val exportConfig: CacheExportConfig = CacheExportConfig()
+    val exportConfig: BookshelfManageScreenExportConfig = BookshelfManageScreenExportConfig()
 )
 )
 
 
-sealed interface CacheIntent {
-    data class Initialize(val groupId: Long) : CacheIntent
-    data class ChangeGroup(val groupId: Long) : CacheIntent
+sealed interface BookshelfManageScreenIntent {
+    data class Initialize(val groupId: Long) : BookshelfManageScreenIntent
+    data class ChangeGroup(val groupId: Long) : BookshelfManageScreenIntent
     data class StartDownloadForVisibleBooks(
     data class StartDownloadForVisibleBooks(
         val books: List<Book>,
         val books: List<Book>,
         val downloadAllChapters: Boolean
         val downloadAllChapters: Boolean
-    ) : CacheIntent
-    data object StopDownload : CacheIntent
-    data class ToggleBookDownload(val book: Book) : CacheIntent
-    data class DeleteBookDownload(val bookUrl: String) : CacheIntent
-    data class ClearBookCache(val book: Book) : CacheIntent
-    data class MoveBooksToGroup(val bookUrls: Set<String>, val groupId: Long) : CacheIntent
-    data class DeleteBooks(val bookUrls: Set<String>, val deleteOriginal: Boolean) : CacheIntent
-    data class ClearCachesForBooks(val bookUrls: Set<String>) : CacheIntent
-    data class DownloadBooks(val bookUrls: Set<String>, val downloadAllChapters: Boolean) : CacheIntent
+    ) : BookshelfManageScreenIntent
+    data object StopDownload : BookshelfManageScreenIntent
+    data class ToggleBookDownload(val book: Book) : BookshelfManageScreenIntent
+    data class DeleteBookDownload(val bookUrl: String) : BookshelfManageScreenIntent
+    data class ClearBookCache(val book: Book) : BookshelfManageScreenIntent
+    data class MoveBooksToGroup(val bookUrls: Set<String>, val groupId: Long) : BookshelfManageScreenIntent
+    data class DeleteBooks(val bookUrls: Set<String>, val deleteOriginal: Boolean) : BookshelfManageScreenIntent
+    data class ClearCachesForBooks(val bookUrls: Set<String>) : BookshelfManageScreenIntent
+    data class MoveBookOrder(val fromIndex: Int, val toIndex: Int) : BookshelfManageScreenIntent
+    data class DownloadBooks(val bookUrls: Set<String>, val downloadAllChapters: Boolean) : BookshelfManageScreenIntent
     data class ChangeBookSource(
     data class ChangeBookSource(
         val oldBookUrl: String,
         val oldBookUrl: String,
         val source: BookSource,
         val source: BookSource,
         val book: Book,
         val book: Book,
         val chapters: List<BookChapter>,
         val chapters: List<BookChapter>,
         val options: ChangeSourceMigrationOptions,
         val options: ChangeSourceMigrationOptions,
-    ) : CacheIntent
+    ) : BookshelfManageScreenIntent
     data class BatchChangeBookSource(
     data class BatchChangeBookSource(
         val bookUrls: Set<String>,
         val bookUrls: Set<String>,
         val sources: List<BookSource>,
         val sources: List<BookSource>,
         val options: ChangeSourceMigrationOptions,
         val options: ChangeSourceMigrationOptions,
-    ) : CacheIntent
-    data class MigratePreviewItem(val oldBookUrl: String) : CacheIntent
-    data class SkipPreviewItem(val oldBookUrl: String) : CacheIntent
-    data class SelectPreviewCandidate(val oldBookUrl: String, val candidateIndex: Int) : CacheIntent
+    ) : BookshelfManageScreenIntent
+    data class MigratePreviewItem(val oldBookUrl: String) : BookshelfManageScreenIntent
+    data class SkipPreviewItem(val oldBookUrl: String) : BookshelfManageScreenIntent
+    data class SelectPreviewCandidate(val oldBookUrl: String, val candidateIndex: Int) : BookshelfManageScreenIntent
     data class UpdatePreviewItem(
     data class UpdatePreviewItem(
         val oldBookUrl: String,
         val oldBookUrl: String,
         val source: BookSource,
         val source: BookSource,
         val book: Book,
         val book: Book,
         val chapters: List<BookChapter>,
         val chapters: List<BookChapter>,
-    ) : CacheIntent
-    data class AddPreviewItemToShelf(val oldBookUrl: String) : CacheIntent
-    data class OpenBookInfoPreview(val book: Book, val inBookshelf: Boolean) : CacheIntent
-    data object MigrateAllPreviewItems : CacheIntent
-    data object DismissChangeSourceStatus : CacheIntent
-    data object DismissBatchChangePreview : CacheIntent
-    data class SetExportUseReplace(val enabled: Boolean) : CacheIntent
-    data class SetEnableCustomExport(val enabled: Boolean) : CacheIntent
-    data class SetExportNoChapterName(val enabled: Boolean) : CacheIntent
-    data class SetExportToWebDav(val enabled: Boolean) : CacheIntent
-    data class SetExportPictureFile(val enabled: Boolean) : CacheIntent
-    data class SetParallelExportBook(val enabled: Boolean) : CacheIntent
-    data class SetExportType(val type: Int) : CacheIntent
-    data class SetExportCharset(val charset: String) : CacheIntent
-    data class SetBookExportFileName(val fileName: String?) : CacheIntent
-    data class SetEpisodeExportFileName(val fileName: String) : CacheIntent
+    ) : BookshelfManageScreenIntent
+    data class AddPreviewItemToShelf(val oldBookUrl: String) : BookshelfManageScreenIntent
+    data class OpenBookInfoPreview(val book: Book, val inBookshelf: Boolean) : BookshelfManageScreenIntent
+    data object MigrateAllPreviewItems : BookshelfManageScreenIntent
+    data object DismissChangeSourceStatus : BookshelfManageScreenIntent
+    data object DismissBatchChangePreview : BookshelfManageScreenIntent
+    data class SetExportUseReplace(val enabled: Boolean) : BookshelfManageScreenIntent
+    data class SetEnableCustomExport(val enabled: Boolean) : BookshelfManageScreenIntent
+    data class SetExportNoChapterName(val enabled: Boolean) : BookshelfManageScreenIntent
+    data class SetExportToWebDav(val enabled: Boolean) : BookshelfManageScreenIntent
+    data class SetExportPictureFile(val enabled: Boolean) : BookshelfManageScreenIntent
+    data class SetParallelExportBook(val enabled: Boolean) : BookshelfManageScreenIntent
+    data class SetExportType(val type: Int) : BookshelfManageScreenIntent
+    data class SetExportCharset(val charset: String) : BookshelfManageScreenIntent
+    data class SetBookExportFileName(val fileName: String?) : BookshelfManageScreenIntent
+    data class SetEpisodeExportFileName(val fileName: String) : BookshelfManageScreenIntent
 }
 }
 
 
-sealed interface CacheEffect {
-    data class NotifyBookChanged(val bookUrl: String) : CacheEffect
-    data class ShowMessage(val message: String) : CacheEffect
-    data class OpenBookInfo(val bookUrl: String, val name: String, val author: String) : CacheEffect
+sealed interface BookshelfManageScreenEffect {
+    data class NotifyBookChanged(val bookUrl: String) : BookshelfManageScreenEffect
+    data class ShowMessage(val message: String) : BookshelfManageScreenEffect
+    data class OpenBookInfo(val bookUrl: String, val name: String, val author: String) : BookshelfManageScreenEffect
 }
 }
 
 
-class CacheViewModel(
+class BookshelfManageScreenViewModel(
     application: Application,
     application: Application,
     private val bookDao: BookDao,
     private val bookDao: BookDao,
     private val bookGroupDao: BookGroupDao,
     private val bookGroupDao: BookGroupDao,
     private val bookChapterDao: BookChapterDao,
     private val bookChapterDao: BookChapterDao,
-    val cacheConfig: CacheConfig,
+    val bookshelfManageScreenConfig: BookshelfManageScreenConfig,
     private val batchCacheDownloadUseCase: BatchCacheDownloadUseCase,
     private val batchCacheDownloadUseCase: BatchCacheDownloadUseCase,
     private val cacheBookChaptersUseCase: CacheBookChaptersUseCase,
     private val cacheBookChaptersUseCase: CacheBookChaptersUseCase,
     private val changeBookSourceUseCase: ChangeBookSourceUseCase,
     private val changeBookSourceUseCase: ChangeBookSourceUseCase,
@@ -147,10 +152,10 @@ class CacheViewModel(
     private val updateBooksGroupUseCase: UpdateBooksGroupUseCase
     private val updateBooksGroupUseCase: UpdateBooksGroupUseCase
 ) : BaseViewModel(application) {
 ) : BaseViewModel(application) {
 
 
-    private val _uiState = MutableStateFlow(CacheUiState())
+    private val _uiState = MutableStateFlow(BookshelfManageScreenUiState())
     val uiState = _uiState.asStateFlow()
     val uiState = _uiState.asStateFlow()
 
 
-    private val _effects = MutableSharedFlow<CacheEffect>(extraBufferCapacity = 32)
+    private val _effects = MutableSharedFlow<BookshelfManageScreenEffect>(extraBufferCapacity = 32)
     val effects = _effects.asSharedFlow()
     val effects = _effects.asSharedFlow()
 
 
     private val cacheChapters = hashMapOf<String, HashSet<String>>()
     private val cacheChapters = hashMapOf<String, HashSet<String>>()
@@ -158,24 +163,25 @@ class CacheViewModel(
     private var groupsJob: Job? = null
     private var groupsJob: Job? = null
     private var observersStarted = false
     private var observersStarted = false
 
 
-    fun dispatch(intent: CacheIntent) {
+    fun dispatch(intent: BookshelfManageScreenIntent) {
         when (intent) {
         when (intent) {
-            is CacheIntent.Initialize -> initialize(intent.groupId)
-            is CacheIntent.ChangeGroup -> changeGroup(intent.groupId)
-            is CacheIntent.StartDownloadForVisibleBooks -> startDownloadForVisibleBooks(
+            is BookshelfManageScreenIntent.Initialize -> initialize(intent.groupId)
+            is BookshelfManageScreenIntent.ChangeGroup -> changeGroup(intent.groupId)
+            is BookshelfManageScreenIntent.StartDownloadForVisibleBooks -> startDownloadForVisibleBooks(
                 intent.books,
                 intent.books,
                 intent.downloadAllChapters
                 intent.downloadAllChapters
             )
             )
 
 
-            CacheIntent.StopDownload -> CacheBook.stop(context)
-            is CacheIntent.ToggleBookDownload -> toggleBookDownload(intent.book)
-            is CacheIntent.DeleteBookDownload -> CacheBook.remove(context, intent.bookUrl)
-            is CacheIntent.ClearBookCache -> clearCacheForBook(intent.book)
-            is CacheIntent.MoveBooksToGroup -> moveBooksToGroup(intent.bookUrls, intent.groupId)
-            is CacheIntent.DeleteBooks -> deleteBooks(intent.bookUrls, intent.deleteOriginal)
-            is CacheIntent.ClearCachesForBooks -> clearCachesForBooks(intent.bookUrls)
-            is CacheIntent.DownloadBooks -> downloadBooks(intent.bookUrls, intent.downloadAllChapters)
-            is CacheIntent.ChangeBookSource -> changeBookSource(
+            BookshelfManageScreenIntent.StopDownload -> CacheBook.stop(context)
+            is BookshelfManageScreenIntent.ToggleBookDownload -> toggleBookDownload(intent.book)
+            is BookshelfManageScreenIntent.DeleteBookDownload -> CacheBook.remove(context, intent.bookUrl)
+            is BookshelfManageScreenIntent.ClearBookCache -> clearCacheForBook(intent.book)
+            is BookshelfManageScreenIntent.MoveBooksToGroup -> moveBooksToGroup(intent.bookUrls, intent.groupId)
+            is BookshelfManageScreenIntent.DeleteBooks -> deleteBooks(intent.bookUrls, intent.deleteOriginal)
+            is BookshelfManageScreenIntent.ClearCachesForBooks -> clearCachesForBooks(intent.bookUrls)
+            is BookshelfManageScreenIntent.MoveBookOrder -> moveBookOrder(intent.fromIndex, intent.toIndex)
+            is BookshelfManageScreenIntent.DownloadBooks -> downloadBooks(intent.bookUrls, intent.downloadAllChapters)
+            is BookshelfManageScreenIntent.ChangeBookSource -> changeBookSource(
                 intent.oldBookUrl,
                 intent.oldBookUrl,
                 intent.source,
                 intent.source,
                 intent.book,
                 intent.book,
@@ -183,35 +189,35 @@ class CacheViewModel(
                 intent.options
                 intent.options
             )
             )
 
 
-            is CacheIntent.BatchChangeBookSource -> batchChangeBookSource(
+            is BookshelfManageScreenIntent.BatchChangeBookSource -> batchChangeBookSource(
                 intent.bookUrls,
                 intent.bookUrls,
                 intent.sources,
                 intent.sources,
                 intent.options
                 intent.options
             )
             )
 
 
-            is CacheIntent.MigratePreviewItem -> migratePreviewItem(intent.oldBookUrl)
-            is CacheIntent.SkipPreviewItem -> skipPreviewItem(intent.oldBookUrl)
-            is CacheIntent.SelectPreviewCandidate -> selectPreviewCandidate(
+            is BookshelfManageScreenIntent.MigratePreviewItem -> migratePreviewItem(intent.oldBookUrl)
+            is BookshelfManageScreenIntent.SkipPreviewItem -> skipPreviewItem(intent.oldBookUrl)
+            is BookshelfManageScreenIntent.SelectPreviewCandidate -> selectPreviewCandidate(
                 intent.oldBookUrl,
                 intent.oldBookUrl,
                 intent.candidateIndex
                 intent.candidateIndex
             )
             )
 
 
-            is CacheIntent.UpdatePreviewItem -> updatePreviewItem(
+            is BookshelfManageScreenIntent.UpdatePreviewItem -> updatePreviewItem(
                 intent.oldBookUrl,
                 intent.oldBookUrl,
                 intent.source,
                 intent.source,
                 intent.book,
                 intent.book,
                 intent.chapters
                 intent.chapters
             )
             )
 
 
-            is CacheIntent.AddPreviewItemToShelf -> addPreviewItemToShelf(intent.oldBookUrl)
-            is CacheIntent.OpenBookInfoPreview -> openBookInfoPreview(
+            is BookshelfManageScreenIntent.AddPreviewItemToShelf -> addPreviewItemToShelf(intent.oldBookUrl)
+            is BookshelfManageScreenIntent.OpenBookInfoPreview -> openBookInfoPreview(
                 intent.book,
                 intent.book,
                 intent.inBookshelf
                 intent.inBookshelf
             )
             )
 
 
-            CacheIntent.MigrateAllPreviewItems -> migrateAllPreviewItems()
+            BookshelfManageScreenIntent.MigrateAllPreviewItems -> migrateAllPreviewItems()
 
 
-            CacheIntent.DismissChangeSourceStatus -> {
+            BookshelfManageScreenIntent.DismissChangeSourceStatus -> {
                 _uiState.update {
                 _uiState.update {
                     it.copy(
                     it.copy(
                         changeSourceProgress = null,
                         changeSourceProgress = null,
@@ -221,59 +227,59 @@ class CacheViewModel(
                 }
                 }
             }
             }
 
 
-            CacheIntent.DismissBatchChangePreview -> {
+            BookshelfManageScreenIntent.DismissBatchChangePreview -> {
                 _uiState.update { it.copy(batchChangePreviewItems = emptyList()) }
                 _uiState.update { it.copy(batchChangePreviewItems = emptyList()) }
             }
             }
 
 
-            is CacheIntent.SetExportUseReplace -> {
-                cacheConfig.exportUseReplace = intent.enabled
+            is BookshelfManageScreenIntent.SetExportUseReplace -> {
+                bookshelfManageScreenConfig.exportUseReplace = intent.enabled
                 syncExportConfig()
                 syncExportConfig()
                 val msg = if (intent.enabled) "替换净化功能已开启" else "替换净化功能已关闭"
                 val msg = if (intent.enabled) "替换净化功能已开启" else "替换净化功能已关闭"
-                _effects.tryEmit(CacheEffect.ShowMessage(msg))
+                _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage(msg))
             }
             }
 
 
-            is CacheIntent.SetEnableCustomExport -> {
-                cacheConfig.enableCustomExport = intent.enabled
+            is BookshelfManageScreenIntent.SetEnableCustomExport -> {
+                bookshelfManageScreenConfig.enableCustomExport = intent.enabled
                 syncExportConfig()
                 syncExportConfig()
             }
             }
 
 
-            is CacheIntent.SetExportNoChapterName -> {
-                cacheConfig.exportNoChapterName = intent.enabled
+            is BookshelfManageScreenIntent.SetExportNoChapterName -> {
+                bookshelfManageScreenConfig.exportNoChapterName = intent.enabled
                 syncExportConfig()
                 syncExportConfig()
             }
             }
 
 
-            is CacheIntent.SetExportToWebDav -> {
-                cacheConfig.exportToWebDav = intent.enabled
+            is BookshelfManageScreenIntent.SetExportToWebDav -> {
+                bookshelfManageScreenConfig.exportToWebDav = intent.enabled
                 syncExportConfig()
                 syncExportConfig()
             }
             }
 
 
-            is CacheIntent.SetExportPictureFile -> {
-                cacheConfig.exportPictureFile = intent.enabled
+            is BookshelfManageScreenIntent.SetExportPictureFile -> {
+                bookshelfManageScreenConfig.exportPictureFile = intent.enabled
                 syncExportConfig()
                 syncExportConfig()
             }
             }
 
 
-            is CacheIntent.SetParallelExportBook -> {
-                cacheConfig.parallelExportBook = intent.enabled
+            is BookshelfManageScreenIntent.SetParallelExportBook -> {
+                bookshelfManageScreenConfig.parallelExportBook = intent.enabled
                 syncExportConfig()
                 syncExportConfig()
             }
             }
 
 
-            is CacheIntent.SetExportType -> {
-                cacheConfig.exportType = intent.type
+            is BookshelfManageScreenIntent.SetExportType -> {
+                bookshelfManageScreenConfig.exportType = intent.type
                 syncExportConfig()
                 syncExportConfig()
             }
             }
 
 
-            is CacheIntent.SetExportCharset -> {
-                cacheConfig.exportCharset = intent.charset
+            is BookshelfManageScreenIntent.SetExportCharset -> {
+                bookshelfManageScreenConfig.exportCharset = intent.charset
                 syncExportConfig()
                 syncExportConfig()
             }
             }
 
 
-            is CacheIntent.SetBookExportFileName -> {
-                cacheConfig.bookExportFileName = intent.fileName
+            is BookshelfManageScreenIntent.SetBookExportFileName -> {
+                bookshelfManageScreenConfig.bookExportFileName = intent.fileName
                 syncExportConfig()
                 syncExportConfig()
             }
             }
 
 
-            is CacheIntent.SetEpisodeExportFileName -> {
-                cacheConfig.episodeExportFileName = intent.fileName
+            is BookshelfManageScreenIntent.SetEpisodeExportFileName -> {
+                bookshelfManageScreenConfig.episodeExportFileName = intent.fileName
                 syncExportConfig()
                 syncExportConfig()
             }
             }
         }
         }
@@ -316,18 +322,38 @@ class CacheViewModel(
         booksJob = viewModelScope.launch {
         booksJob = viewModelScope.launch {
             bookDao.flowBookShelfByGroup(groupId).map { books ->
             bookDao.flowBookShelfByGroup(groupId).map { books ->
                 val booksDownload = books.filter { !it.isAudio }.map { it.toLightBook() }
                 val booksDownload = books.filter { !it.isAudio }.map { it.toLightBook() }
-                when (cacheConfig.getBookSortByGroupId(groupId)) {
-                    1 -> booksDownload.sortedByDescending { it.latestChapterTime }
-                    2 -> booksDownload.sortedWith { o1, o2 -> o1.name.cnCompare(o2.name) }
-                    3 -> booksDownload.sortedBy { it.order }
-                    4 -> booksDownload.sortedByDescending {
+                val bookSort = bookshelfManageScreenConfig.getBookSortByGroupId(groupId)
+                val isDescending = bookshelfManageScreenConfig.bookshelfSortOrder == 1
+                bookSort to when (bookSort) {
+                    1 -> if (isDescending) booksDownload.sortedByDescending { it.latestChapterTime }
+                    else booksDownload.sortedBy { it.latestChapterTime }
+
+                    2 -> if (isDescending) {
+                        booksDownload.sortedWith { o1, o2 -> o2.name.cnCompare(o1.name) }
+                    } else {
+                        booksDownload.sortedWith { o1, o2 -> o1.name.cnCompare(o2.name) }
+                    }
+
+                    3 -> if (isDescending) booksDownload.sortedByDescending { it.order }
+                    else booksDownload.sortedBy { it.order }
+
+                    4 -> if (isDescending) booksDownload.sortedByDescending {
+                        max(it.latestChapterTime, it.durChapterTime)
+                    } else booksDownload.sortedBy {
                         max(it.latestChapterTime, it.durChapterTime)
                         max(it.latestChapterTime, it.durChapterTime)
                     }
                     }
 
 
-                    else -> booksDownload.sortedByDescending { it.durChapterTime }
+                    else -> if (isDescending) booksDownload.sortedByDescending { it.durChapterTime }
+                    else booksDownload.sortedBy { it.durChapterTime }
+                }
+            }.collect { (bookSort, books) ->
+                _uiState.update {
+                    it.copy(
+                        books = books,
+                        bookSort = bookSort,
+                        bookSortOrder = bookshelfManageScreenConfig.bookshelfSortOrder
+                    )
                 }
                 }
-            }.collect { books ->
-                _uiState.update { it.copy(books = books) }
                 loadCacheFiles(books)
                 loadCacheFiles(books)
             }
             }
         }
         }
@@ -372,17 +398,17 @@ class CacheViewModel(
     private fun syncExportConfig() {
     private fun syncExportConfig() {
         _uiState.update {
         _uiState.update {
             it.copy(
             it.copy(
-                exportConfig = CacheExportConfig(
-                    exportUseReplace = cacheConfig.exportUseReplace,
-                    enableCustomExport = cacheConfig.enableCustomExport,
-                    exportNoChapterName = cacheConfig.exportNoChapterName,
-                    exportToWebDav = cacheConfig.exportToWebDav,
-                    exportPictureFile = cacheConfig.exportPictureFile,
-                    parallelExportBook = cacheConfig.parallelExportBook,
-                    exportType = cacheConfig.exportType,
-                    exportCharset = cacheConfig.exportCharset,
-                    bookExportFileName = cacheConfig.bookExportFileName,
-                    episodeExportFileName = cacheConfig.episodeExportFileName
+                exportConfig = BookshelfManageScreenExportConfig(
+                    exportUseReplace = bookshelfManageScreenConfig.exportUseReplace,
+                    enableCustomExport = bookshelfManageScreenConfig.enableCustomExport,
+                    exportNoChapterName = bookshelfManageScreenConfig.exportNoChapterName,
+                    exportToWebDav = bookshelfManageScreenConfig.exportToWebDav,
+                    exportPictureFile = bookshelfManageScreenConfig.exportPictureFile,
+                    parallelExportBook = bookshelfManageScreenConfig.parallelExportBook,
+                    exportType = bookshelfManageScreenConfig.exportType,
+                    exportCharset = bookshelfManageScreenConfig.exportCharset,
+                    bookExportFileName = bookshelfManageScreenConfig.bookExportFileName,
+                    episodeExportFileName = bookshelfManageScreenConfig.episodeExportFileName
                 )
                 )
             )
             )
         }
         }
@@ -463,7 +489,7 @@ class CacheViewModel(
         execute {
         execute {
             updateBooksGroupUseCase.replaceGroup(bookUrls, safeGroupId)
             updateBooksGroupUseCase.replaceGroup(bookUrls, safeGroupId)
         }.onError {
         }.onError {
-            _effects.tryEmit(CacheEffect.ShowMessage("移动分组失败\n${it.localizedMessage}"))
+            _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("移动分组失败\n${it.localizedMessage}"))
         }
         }
     }
     }
 
 
@@ -475,9 +501,9 @@ class CacheViewModel(
         }.onSuccess { deletedBookUrls ->
         }.onSuccess { deletedBookUrls ->
             _uiState.update { it.copy(deleteBookOriginal = deleteOriginal) }
             _uiState.update { it.copy(deleteBookOriginal = deleteOriginal) }
             deletedBookUrls.forEach { cacheChapters.remove(it) }
             deletedBookUrls.forEach { cacheChapters.remove(it) }
-            _effects.tryEmit(CacheEffect.ShowMessage("删除成功"))
+            _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("删除成功"))
         }.onError {
         }.onError {
-            _effects.tryEmit(CacheEffect.ShowMessage("删除失败\n${it.localizedMessage}"))
+            _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("删除失败\n${it.localizedMessage}"))
         }
         }
     }
     }
 
 
@@ -490,9 +516,33 @@ class CacheViewModel(
                 cacheChapters[bookUrl] = hashSetOf()
                 cacheChapters[bookUrl] = hashSetOf()
                 emitBookChanged(bookUrl)
                 emitBookChanged(bookUrl)
             }
             }
-            _effects.tryEmit(CacheEffect.ShowMessage("缓存已清理"))
+            _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("缓存已清理"))
+        }.onError {
+            _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("清理缓存失败\n${it.localizedMessage}"))
+        }
+    }
+
+    private fun moveBookOrder(fromIndex: Int, toIndex: Int) {
+        val books = uiState.value.books
+        if (fromIndex !in books.indices || toIndex !in books.indices || fromIndex == toIndex) {
+            return
+        }
+        val reorderedBooks = books.toMutableList().apply { move(fromIndex, toIndex) }
+        val isDescending = uiState.value.bookSortOrder == 1
+        val maxOrder = reorderedBooks.size
+        reorderedBooks.forEachIndexed { index, book ->
+            book.order = if (isDescending) maxOrder - index else index + 1
+        }
+        _uiState.update {
+            it.copy(
+                books = reorderedBooks,
+                cacheVersion = it.cacheVersion + 1
+            )
+        }
+        execute {
+            bookDao.update(*reorderedBooks.toTypedArray())
         }.onError {
         }.onError {
-            _effects.tryEmit(CacheEffect.ShowMessage("清理缓存失败\n${it.localizedMessage}"))
+            _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("排序保存失败\n${it.localizedMessage}"))
         }
         }
     }
     }
 
 
@@ -506,13 +556,13 @@ class CacheViewModel(
             )
             )
         }.onSuccess { count ->
         }.onSuccess { count ->
             if (count > 0) {
             if (count > 0) {
-                _effects.tryEmit(CacheEffect.ShowMessage("已加入缓存队列: $count 本"))
+                _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("已加入缓存队列: $count 本"))
             } else {
             } else {
-                _effects.tryEmit(CacheEffect.ShowMessage("没有可缓存的书籍"))
+                _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("没有可缓存的书籍"))
             }
             }
             syncDownloadRunning()
             syncDownloadRunning()
         }.onError {
         }.onError {
-            _effects.tryEmit(CacheEffect.ShowMessage("批量缓存失败\n${it.localizedMessage}"))
+            _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("批量缓存失败\n${it.localizedMessage}"))
         }
         }
     }
     }
 
 
@@ -531,9 +581,9 @@ class CacheViewModel(
             cacheChapters.remove(result.oldBookUrl)
             cacheChapters.remove(result.oldBookUrl)
             cacheChapters[result.book.bookUrl] = hashSetOf()
             cacheChapters[result.book.bookUrl] = hashSetOf()
             emitBookChanged(result.book.bookUrl)
             emitBookChanged(result.book.bookUrl)
-            _effects.tryEmit(CacheEffect.ShowMessage("换源完成"))
+            _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("换源完成"))
         }.onError {
         }.onError {
-            _effects.tryEmit(CacheEffect.ShowMessage("换源失败\n${it.localizedMessage}"))
+            _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("换源失败\n${it.localizedMessage}"))
         }
         }
     }
     }
 
 
@@ -626,9 +676,9 @@ class CacheViewModel(
             cacheChapters[result.book.bookUrl] = hashSetOf()
             cacheChapters[result.book.bookUrl] = hashSetOf()
             removePreviewItem(oldBookUrl)
             removePreviewItem(oldBookUrl)
             emitBookChanged(result.book.bookUrl)
             emitBookChanged(result.book.bookUrl)
-            _effects.tryEmit(CacheEffect.ShowMessage("迁移完成"))
+            _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("迁移完成"))
         }.onError {
         }.onError {
-            _effects.tryEmit(CacheEffect.ShowMessage("迁移失败\n${it.localizedMessage}"))
+            _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("迁移失败\n${it.localizedMessage}"))
         }
         }
     }
     }
 
 
@@ -704,9 +754,9 @@ class CacheViewModel(
             bookChapterDao.insert(*candidate.chapters.toTypedArray())
             bookChapterDao.insert(*candidate.chapters.toTypedArray())
             candidate.book
             candidate.book
         }.onSuccess {
         }.onSuccess {
-            _effects.tryEmit(CacheEffect.ShowMessage("已添加到书架"))
+            _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("已添加到书架"))
         }.onError {
         }.onError {
-            _effects.tryEmit(CacheEffect.ShowMessage("添加书籍失败\n${it.localizedMessage}"))
+            _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("添加书籍失败\n${it.localizedMessage}"))
         }
         }
     }
     }
 
 
@@ -717,7 +767,7 @@ class CacheViewModel(
             }
             }
             book
             book
         }.onSuccess {
         }.onSuccess {
-            _effects.tryEmit(CacheEffect.OpenBookInfo(it.bookUrl, it.name, it.author))
+            _effects.tryEmit(BookshelfManageScreenEffect.OpenBookInfo(it.bookUrl, it.name, it.author))
         }
         }
     }
     }
 
 
@@ -755,9 +805,9 @@ class CacheViewModel(
         }.onSuccess {
         }.onSuccess {
             cacheChapters.clear()
             cacheChapters.clear()
             _uiState.update { it.copy(batchChangePreviewItems = emptyList()) }
             _uiState.update { it.copy(batchChangePreviewItems = emptyList()) }
-            _effects.tryEmit(CacheEffect.ShowMessage("批量迁移完成"))
+            _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("批量迁移完成"))
         }.onError {
         }.onError {
-            _effects.tryEmit(CacheEffect.ShowMessage("批量迁移失败\n${it.localizedMessage}"))
+            _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("批量迁移失败\n${it.localizedMessage}"))
         }.onFinally {
         }.onFinally {
             _uiState.update {
             _uiState.update {
                 it.copy(
                 it.copy(
@@ -781,7 +831,7 @@ class CacheViewModel(
 
 
     private fun emitBookChanged(bookUrl: String) {
     private fun emitBookChanged(bookUrl: String) {
         _uiState.update { it.copy(cacheVersion = it.cacheVersion + 1) }
         _uiState.update { it.copy(cacheVersion = it.cacheVersion + 1) }
-        _effects.tryEmit(CacheEffect.NotifyBookChanged(bookUrl))
+        _effects.tryEmit(BookshelfManageScreenEffect.NotifyBookChanged(bookUrl))
     }
     }
 
 
 }
 }

+ 8 - 0
app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt

@@ -138,6 +138,14 @@ object BookshelfConfig {
      */
      */
     var bookshelfCoverShadow by prefDelegate(PreferKey.bookshelfCoverShadow, false)
     var bookshelfCoverShadow by prefDelegate(PreferKey.bookshelfCoverShadow, false)
 
 
+    /**
+     * 书架搜索按钮是否直接跳转搜索页
+     */
+    var bookshelfSearchActionDirectToSearch by prefDelegate(
+        PreferKey.bookshelfSearchActionDirectToSearch,
+        true
+    )
+
     /**
     /**
      * 启动时自动刷新书架
      * 启动时自动刷新书架
      */
      */

+ 6 - 5
app/src/main/java/io/legado/app/ui/config/cacheConfig/CacheConfig.kt → app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfManageScreenConfig.kt

@@ -1,10 +1,10 @@
-package io.legado.app.ui.config.cacheConfig
+package io.legado.app.ui.config.bookshelfConfig
 
 
 import io.legado.app.constant.PreferKey
 import io.legado.app.constant.PreferKey
 import io.legado.app.data.dao.BookGroupDao
 import io.legado.app.data.dao.BookGroupDao
 import io.legado.app.ui.config.prefDelegate
 import io.legado.app.ui.config.prefDelegate
 
 
-class CacheConfig(
+class BookshelfManageScreenConfig(
     private val bookGroupDao: BookGroupDao
     private val bookGroupDao: BookGroupDao
 ) {
 ) {
 
 
@@ -28,9 +28,10 @@ class CacheConfig(
 
 
     var parallelExportBook by prefDelegate(PreferKey.parallelExportBook, false)
     var parallelExportBook by prefDelegate(PreferKey.parallelExportBook, false)
 
 
-    var bookshelfSort by prefDelegate(PreferKey.bookshelfSort, 0)
-
     fun getBookSortByGroupId(groupId: Long): Int {
     fun getBookSortByGroupId(groupId: Long): Int {
-        return bookGroupDao.getByID(groupId)?.getRealBookSort() ?: bookshelfSort
+        return bookGroupDao.getByID(groupId)?.getRealBookSort() ?: BookshelfConfig.bookshelfSort
     }
     }
+
+    val bookshelfSortOrder: Int
+        get() = BookshelfConfig.bookshelfSortOrder
 }
 }

+ 8 - 3
app/src/main/java/io/legado/app/ui/main/MainActivity.kt

@@ -47,7 +47,7 @@ import io.legado.app.ui.book.search.SearchIntent
 import io.legado.app.ui.book.search.SearchScreen
 import io.legado.app.ui.book.search.SearchScreen
 import io.legado.app.ui.book.search.SearchViewModel
 import io.legado.app.ui.book.search.SearchViewModel
 import io.legado.app.ui.book.source.manage.BookSourceActivity
 import io.legado.app.ui.book.source.manage.BookSourceActivity
-import io.legado.app.ui.book.cache.CacheRouteScreen
+import io.legado.app.ui.book.manage.BookshelfManageRouteScreen
 import io.legado.app.ui.book.read.ReadBookActivity
 import io.legado.app.ui.book.read.ReadBookActivity
 import io.legado.app.ui.config.ConfigNavScreen
 import io.legado.app.ui.config.ConfigNavScreen
 import io.legado.app.ui.config.ConfigTag
 import io.legado.app.ui.config.ConfigTag
@@ -161,7 +161,7 @@ open class MainActivity : BaseComposeActivity() {
             }
             }
         }
         }
 
 
-        fun createCacheIntent(
+        fun createBookshelfManageScreenIntent(
             context: Context,
             context: Context,
             groupId: Long = -1L
             groupId: Long = -1L
         ): Intent {
         ): Intent {
@@ -171,6 +171,11 @@ open class MainActivity : BaseComposeActivity() {
             }
             }
         }
         }
 
 
+        fun createCacheIntent(
+            context: Context,
+            groupId: Long = -1L
+        ): Intent = createBookshelfManageScreenIntent(context, groupId)
+
         fun createSearchIntent(
         fun createSearchIntent(
             context: Context,
             context: Context,
             key: String? = null,
             key: String? = null,
@@ -463,7 +468,7 @@ open class MainActivity : BaseComposeActivity() {
                 }
                 }
 
 
                 entry<MainRouteCache> { route ->
                 entry<MainRouteCache> { route ->
-                    CacheRouteScreen(
+                    BookshelfManageRouteScreen(
                         groupId = route.groupId,
                         groupId = route.groupId,
                         onBackClick = { navigateBack(backStack) }
                         onBackClick = { navigateBack(backStack) }
                     )
                     )

+ 67 - 47
app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt

@@ -35,6 +35,7 @@ import io.legado.app.constant.BookType
 import io.legado.app.data.entities.BookGroup
 import io.legado.app.data.entities.BookGroup
 import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
 import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.cover.CoilBookCover
 import io.legado.app.ui.widget.components.cover.CoilBookCover
 import io.legado.app.ui.widget.components.cover.BookshelfCover
 import io.legado.app.ui.widget.components.cover.BookshelfCover
@@ -55,6 +56,7 @@ fun BookshelfItem(
     title: String,
     title: String,
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
     isSelected: Boolean = false,
     isSelected: Boolean = false,
+    titleEnd: @Composable (() -> Unit)? = null,
     subTitle: String? = null,
     subTitle: String? = null,
     desc: String? = null,
     desc: String? = null,
     extra: @Composable (RowScope.() -> Unit)? = null,
     extra: @Composable (RowScope.() -> Unit)? = null,
@@ -63,12 +65,12 @@ fun BookshelfItem(
     titleMaxLines: Int = 2,
     titleMaxLines: Int = 2,
     coverShadow: Boolean = false,
     coverShadow: Boolean = false,
     onClick: () -> Unit,
     onClick: () -> Unit,
-    onLongClick: () -> Unit
+    onLongClick: (() -> Unit)?
 ) {
 ) {
     val selectedColor = if (isSelected) {
     val selectedColor = if (isSelected) {
-        LegadoTheme.colorScheme.secondaryContainer.copy(alpha = 0.55f)
+        LegadoTheme.colorScheme.secondaryContainer
     } else {
     } else {
-        Color.Transparent
+        LegadoTheme.colorScheme.surface
     }
     }
     if (isGrid) {
     if (isGrid) {
         Box(
         Box(
@@ -142,59 +144,66 @@ fun BookshelfItem(
     } else {
     } else {
         // 列表布局
         // 列表布局
         Column {
         Column {
-            Row(
+            GlassCard(
                 modifier = modifier
                 modifier = modifier
                     .fillMaxWidth()
                     .fillMaxWidth()
-                    .clip(MaterialTheme.shapes.small)
-                    .background(selectedColor)
-                    .combinedClickable(
-                        onClick = onClick,
-                        onLongClick = onLongClick
-                    )
-                    .padding(horizontal = 16.dp, vertical = 8.dp),
-                verticalAlignment = Alignment.CenterVertically
+                    .padding(all = 4.dp),
+                cornerRadius = 8.dp,
+                containerColor = selectedColor,
+                onClick = onClick,
+                onLongClick = onLongClick
             ) {
             ) {
-                cover(
-                    Modifier
-                        .width(if (!isCompact) 80.dp else 56.dp)
-                        .padding(end = 12.dp)
-                )
-                Column(
-                    modifier = Modifier.weight(1f),
-                    verticalArrangement = Arrangement.Center
+                Row(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .padding(all = 4.dp),
+                    verticalAlignment = Alignment.CenterVertically
                 ) {
                 ) {
-                    AppText(
-                        text = title,
-                        style = LegadoTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
-                        maxLines = if (!isCompact) 2 else 1,
-                        overflow = TextOverflow.Ellipsis
+                    cover(
+                        Modifier
+                            .width(if (!isCompact) 80.dp else 56.dp)
+                            .padding(end = 12.dp)
                     )
                     )
-                    subTitle?.let {
+                    Column(
+                        modifier = Modifier.weight(1f),
+                        verticalArrangement = Arrangement.Center
+                    ) {
+                    Row(
+                        verticalAlignment = Alignment.CenterVertically
+                    ) {
                         AppText(
                         AppText(
-                            text = it,
-                            style = LegadoTheme.typography.bodySmall,
+                            text = title,
+                            style = LegadoTheme.typography.titleMediumEmphasized,
                             maxLines = 1,
                             maxLines = 1,
                             overflow = TextOverflow.Ellipsis,
                             overflow = TextOverflow.Ellipsis,
-                            modifier = Modifier.padding(top = 2.dp)
+                            modifier = Modifier.weight(1f)
                         )
                         )
+                        titleEnd?.invoke()
                     }
                     }
-                    if (!isCompact) {
-                        desc?.let {
+                        subTitle?.let {
                             AppText(
                             AppText(
                                 text = it,
                                 text = it,
                                 style = LegadoTheme.typography.bodySmall,
                                 style = LegadoTheme.typography.bodySmall,
                                 maxLines = 1,
                                 maxLines = 1,
                                 overflow = TextOverflow.Ellipsis,
                                 overflow = TextOverflow.Ellipsis,
-                                modifier = Modifier.padding(top = 2.dp)
                             )
                             )
                         }
                         }
-                    }
-                    extra?.let {
-                        Row(
-                            modifier = Modifier.padding(top = 2.dp),
-                            verticalAlignment = Alignment.CenterVertically,
-                            content = it
-                        )
+                        if (!isCompact) {
+                            desc?.let {
+                                AppText(
+                                    text = it,
+                                    style = LegadoTheme.typography.labelSmallEmphasized,
+                                    maxLines = 1,
+                                    overflow = TextOverflow.Ellipsis
+                                )
+                            }
+                        }
+                        extra?.let {
+                            Row(
+                                verticalAlignment = Alignment.CenterVertically,
+                                content = it
+                            )
+                        }
                     }
                     }
                 }
                 }
             }
             }
@@ -325,7 +334,7 @@ fun BookGroupItemGrid(
     coverShadow: Boolean = false,
     coverShadow: Boolean = false,
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
     onClick: () -> Unit,
     onClick: () -> Unit,
-    onLongClick: () -> Unit
+    onLongClick: (() -> Unit)?
 ) {
 ) {
     BookshelfItem(
     BookshelfItem(
         isGrid = true,
         isGrid = true,
@@ -362,7 +371,7 @@ fun BookGroupItemList(
     coverShadow: Boolean = false,
     coverShadow: Boolean = false,
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
     onClick: () -> Unit,
     onClick: () -> Unit,
-    onLongClick: () -> Unit
+    onLongClick: (() -> Unit)?
 ) {
 ) {
     BookshelfItem(
     BookshelfItem(
         isGrid = false,
         isGrid = false,
@@ -397,9 +406,10 @@ fun BookItem(
     isSearchMode: Boolean = false,
     isSearchMode: Boolean = false,
     searchKey: String = "",
     searchKey: String = "",
     onClick: () -> Unit,
     onClick: () -> Unit,
-    onLongClick: () -> Unit
+    onLongClick: (() -> Unit)?
 ) {
 ) {
     val unreadCount = book.getUnreadChapterNum()
     val unreadCount = book.getUnreadChapterNum()
+    val unreadText = if (BookshelfConfig.showUnread && unreadCount > 0) unreadCount.toString() else null
     val bookTypeLabel = if (BookshelfConfig.showTip) {
     val bookTypeLabel = if (BookshelfConfig.showTip) {
         when {
         when {
             book.isAudio -> stringResource(R.string.audio)
             book.isAudio -> stringResource(R.string.audio)
@@ -427,6 +437,16 @@ fun BookItem(
         isCompact = isCompact,
         isCompact = isCompact,
         isSelected = isSelected,
         isSelected = isSelected,
         modifier = modifier,
         modifier = modifier,
+        titleEnd = if (layoutMode == 0 && unreadText != null) {
+            {
+                TextCard(
+                    text = unreadText,
+                    cornerRadius = 4.dp,
+                    horizontalPadding = 4.dp,
+                    verticalPadding = 0.dp
+                )
+            }
+        } else null,
         cover = { modifier ->
         cover = { modifier ->
             BookshelfCover(
             BookshelfCover(
                 name = book.name,
                 name = book.name,
@@ -434,7 +454,7 @@ fun BookItem(
                 path = book.getDisplayCover(),
                 path = book.getDisplayCover(),
                 isUpdating = isUpdating,
                 isUpdating = isUpdating,
                 modifier = modifier,
                 modifier = modifier,
-                badgeText = if (BookshelfConfig.showUnread && unreadCount > 0) unreadCount.toString() else null,
+                badgeText = if (layoutMode != 0) unreadText else null,
                 showBadgeDot = BookshelfConfig.showUnread && BookshelfConfig.showUnreadNew && book.isNew,
                 showBadgeDot = BookshelfConfig.showUnread && BookshelfConfig.showUnreadNew && book.isNew,
                 leftBottomText = matchedSourceLabel ?: bookTypeLabel
                 leftBottomText = matchedSourceLabel ?: bookTypeLabel
             )
             )
@@ -445,19 +465,19 @@ fun BookItem(
         } else {
         } else {
             book.author
             book.author
         },
         },
-        desc = stringResource(R.string.read_dur_progress, book.durChapterTitle ?: ""),
+        desc = book.durChapterTitle ?: "",
         extra = {
         extra = {
             if (BookshelfConfig.showLastUpdateTime && !book.isLocal) {
             if (BookshelfConfig.showLastUpdateTime && !book.isLocal) {
                 AppText(
                 AppText(
                     text = book.latestChapterTime.toTimeAgo(),
                     text = book.latestChapterTime.toTimeAgo(),
-                    style = LegadoTheme.typography.bodySmall,
-                    color = if (layoutMode != 0 || !isCompact) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary,
+                    style = LegadoTheme.typography.labelSmallEmphasized,
+                    color = if (layoutMode != 0 || !isCompact) MaterialTheme.colorScheme.outline else MaterialTheme.colorScheme.outline,
                     modifier = Modifier.padding(end = 4.dp)
                     modifier = Modifier.padding(end = 4.dp)
                 )
                 )
             }
             }
             AppText(
             AppText(
                 text = book.latestChapterTitle ?: "",
                 text = book.latestChapterTitle ?: "",
-                style = LegadoTheme.typography.bodySmall,
+                style = LegadoTheme.typography.labelSmallEmphasized.copy(color = LegadoTheme.colorScheme.outline),
                 maxLines = 1,
                 maxLines = 1,
                 overflow = TextOverflow.Ellipsis,
                 overflow = TextOverflow.Ellipsis,
                 modifier = Modifier.weight(1f)
                 modifier = Modifier.weight(1f)

+ 7 - 0
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigSheet.kt

@@ -191,6 +191,13 @@ fun BookshelfConfigSheet(
                 onCheckedChange = { BookshelfConfig.bookshelfCoverShadow = it }
                 onCheckedChange = { BookshelfConfig.bookshelfCoverShadow = it }
             )
             )
 
 
+            CompactSwitchSettingItem(
+                title = "搜索按钮优先打开筛选栏",
+                checked = BookshelfConfig.bookshelfSearchActionDirectToSearch,
+                color = MaterialTheme.colorScheme.surface,
+                onCheckedChange = { BookshelfConfig.bookshelfSearchActionDirectToSearch = it }
+            )
+
             // Switches
             // Switches
             CompactSwitchSettingItem(
             CompactSwitchSettingItem(
                 title = stringResource(R.string.show_unread),
                 title = stringResource(R.string.show_unread),

+ 136 - 56
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt

@@ -23,12 +23,14 @@ import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.items
 import androidx.compose.foundation.lazy.grid.items
 import androidx.compose.foundation.lazy.grid.itemsIndexed
 import androidx.compose.foundation.lazy.grid.itemsIndexed
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
 import androidx.compose.foundation.pager.HorizontalPager
 import androidx.compose.foundation.pager.HorizontalPager
 import androidx.compose.foundation.pager.rememberPagerState
 import androidx.compose.foundation.pager.rememberPagerState
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
 import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
 import androidx.compose.material.icons.filled.Bookmarks
 import androidx.compose.material.icons.filled.Bookmarks
 import androidx.compose.material.icons.filled.Check
 import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.CloudDownload
 import androidx.compose.material.icons.filled.Download
 import androidx.compose.material.icons.filled.Download
 import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material.icons.filled.GridView
 import androidx.compose.material.icons.filled.GridView
@@ -38,6 +40,7 @@ import androidx.compose.material.icons.filled.Link
 import androidx.compose.material.icons.filled.Refresh
 import androidx.compose.material.icons.filled.Refresh
 import androidx.compose.material.icons.filled.Save
 import androidx.compose.material.icons.filled.Save
 import androidx.compose.material.icons.filled.SelectAll
 import androidx.compose.material.icons.filled.SelectAll
+import androidx.compose.material.icons.filled.UploadFile
 import androidx.compose.material.icons.filled.Wifi
 import androidx.compose.material.icons.filled.Wifi
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3Api
@@ -61,10 +64,12 @@ import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
 import androidx.compose.ui.platform.ClipEntry
 import androidx.compose.ui.platform.ClipEntry
 import androidx.compose.ui.platform.LocalClipboard
 import androidx.compose.ui.platform.LocalClipboard
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalHapticFeedback
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.Dialog
 import androidx.compose.ui.window.Dialog
@@ -77,6 +82,7 @@ import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.theme.adaptiveContentPadding
+import io.legado.app.ui.theme.adaptiveContentPaddingBookshelf
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import io.legado.app.ui.theme.adaptiveHorizontalPaddingTab
 import io.legado.app.ui.theme.adaptiveHorizontalPaddingTab
 import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.EmptyMessage
@@ -96,12 +102,15 @@ import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 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 io.legado.app.utils.move
 import io.legado.app.utils.readText
 import io.legado.app.utils.readText
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import org.koin.androidx.compose.koinViewModel
 import org.koin.androidx.compose.koinViewModel
+import sh.calvin.reorderable.ReorderableItem
+import sh.calvin.reorderable.rememberReorderableLazyGridState
 
 
 @OptIn(
 @OptIn(
     ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
     ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
@@ -315,10 +324,6 @@ fun BookshelfScreen(
         if (isLandscape) BookshelfConfig.bookshelfLayoutGridLandscape else BookshelfConfig.bookshelfLayoutGridPortrait
         if (isLandscape) BookshelfConfig.bookshelfLayoutGridLandscape else BookshelfConfig.bookshelfLayoutGridPortrait
     val bookshelfLayoutList =
     val bookshelfLayoutList =
         if (isLandscape) BookshelfConfig.bookshelfLayoutListLandscape else BookshelfConfig.bookshelfLayoutListPortrait
         if (isLandscape) BookshelfConfig.bookshelfLayoutListLandscape else BookshelfConfig.bookshelfLayoutListPortrait
-    val totalHorizontalPadding =
-        if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) 12.dp else 16.dp
-    val gridContentHorizontalPadding = totalHorizontalPadding / 2
-    val gridInnerHorizontalPadding = totalHorizontalPadding / 2
     val currentMenuGroupId = if (uiState.isSearch) uiState.selectedGroupId else currentTabGroupId
     val currentMenuGroupId = if (uiState.isSearch) uiState.selectedGroupId else currentTabGroupId
     val editStickySummary = if (isEditMode) {
     val editStickySummary = if (isEditMode) {
         BookshelfEditStickySummary(
         BookshelfEditStickySummary(
@@ -337,9 +342,13 @@ fun BookshelfScreen(
         state = uiState,
         state = uiState,
         showSearchAction = true,
         showSearchAction = true,
         onSearchToggle = { active ->
         onSearchToggle = { active ->
-            viewModel.setSearchMode(active)
-            if (!active && uiState.selectedGroupId != currentTabGroupId) {
-                viewModel.changeGroup(currentTabGroupId)
+            if (BookshelfConfig.bookshelfSearchActionDirectToSearch) {
+                onNavigateToSearch(uiState.searchKey.trim())
+            } else {
+                viewModel.setSearchMode(active)
+                if (!active && uiState.selectedGroupId != currentTabGroupId) {
+                    viewModel.changeGroup(currentTabGroupId)
+                }
             }
             }
         },
         },
         onSearchQueryChange = { viewModel.setSearchKey(it) },
         onSearchQueryChange = { viewModel.setSearchKey(it) },
@@ -441,14 +450,14 @@ fun BookshelfScreen(
                     leadingIcon = { Icon(Icons.Default.Edit, null) }
                     leadingIcon = { Icon(Icons.Default.Edit, null) }
                 )
                 )
                 RoundDropdownMenuItem(
                 RoundDropdownMenuItem(
-                    text = stringResource(R.string.cache_export),
+                    text = stringResource(R.string.bookshelf_management),
                     onClick = {
                     onClick = {
                         val groupId =
                         val groupId =
                             uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L
                             uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L
                         onNavigateToCache(groupId)
                         onNavigateToCache(groupId)
                         dismiss()
                         dismiss()
                     },
                     },
-                    leadingIcon = { Icon(Icons.Default.Download, null) }
+                    leadingIcon = { Icon(Icons.Default.Bookmarks, null) }
                 )
                 )
                 RoundDropdownMenuItem(
                 RoundDropdownMenuItem(
                     text = stringResource(R.string.export_bookshelf),
                     text = stringResource(R.string.export_bookshelf),
@@ -456,11 +465,12 @@ fun BookshelfScreen(
                         showExportSheet = true
                         showExportSheet = true
                         dismiss()
                         dismiss()
                     },
                     },
-                    leadingIcon = { Icon(Icons.Default.ImportExport, null) }
+                    leadingIcon = { Icon(Icons.Default.UploadFile, null) }
                 )
                 )
                 RoundDropdownMenuItem(
                 RoundDropdownMenuItem(
                     text = stringResource(R.string.import_bookshelf),
                     text = stringResource(R.string.import_bookshelf),
-                    onClick = { showImportSheet = true; dismiss() }
+                    onClick = { showImportSheet = true; dismiss() },
+                    leadingIcon = { Icon(Icons.Default.CloudDownload, null) }
                 )
                 )
                 RoundDropdownMenuItem(
                 RoundDropdownMenuItem(
                     text = stringResource(R.string.log),
                     text = stringResource(R.string.log),
@@ -633,12 +643,11 @@ fun BookshelfScreen(
                     FastScrollLazyVerticalGrid(
                     FastScrollLazyVerticalGrid(
                         columns = GridCells.Fixed(folderColumns.coerceAtLeast(1)),
                         columns = GridCells.Fixed(folderColumns.coerceAtLeast(1)),
                         modifier = Modifier
                         modifier = Modifier
-                            .fillMaxSize()
-                            .padding(horizontal = if (isGridMode) gridInnerHorizontalPadding else 0.dp),
-                        contentPadding = adaptiveContentPadding(
+                            .fillMaxSize(),
+                        contentPadding = adaptiveContentPaddingBookshelf(
                             top = paddingValues.calculateTopPadding(),
                             top = paddingValues.calculateTopPadding(),
                             bottom = 120.dp,
                             bottom = 120.dp,
-                            horizontal = if (isGridMode) gridContentHorizontalPadding else 0.dp
+                            horizontal = if (isGridMode) 8.dp else 4.dp
                         ),
                         ),
                         verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
                         verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
                         horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
                         horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
@@ -701,7 +710,9 @@ fun BookshelfScreen(
                             bookshelfLayoutList = bookshelfLayoutList,
                             bookshelfLayoutList = bookshelfLayoutList,
                             isEditMode = isEditMode,
                             isEditMode = isEditMode,
                             selectedBookUrls = selectedBookUrls,
                             selectedBookUrls = selectedBookUrls,
+                            canReorderBooks = false,
                             onToggleBookSelection = { toggleBookSelection(it.bookUrl) },
                             onToggleBookSelection = { toggleBookSelection(it.bookUrl) },
+                            onSaveBookOrder = {},
                             onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
                             onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
                             onBookClick = onBookClick,
                             onBookClick = onBookClick,
                             onBookLongClick = onBookLongClick
                             onBookLongClick = onBookLongClick
@@ -725,15 +736,21 @@ fun BookshelfScreen(
                                     uiState = uiState,
                                     uiState = uiState,
                                     bookshelfLayoutMode = bookshelfLayoutMode,
                                     bookshelfLayoutMode = bookshelfLayoutMode,
                                     bookshelfLayoutGrid = bookshelfLayoutGrid,
                                     bookshelfLayoutGrid = bookshelfLayoutGrid,
-                                bookshelfLayoutList = bookshelfLayoutList,
-                                isEditMode = isEditMode,
-                                selectedBookUrls = selectedBookUrls,
-                                onToggleBookSelection = { toggleBookSelection(it.bookUrl) },
-                                onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
-                                onBookClick = onBookClick,
-                                onBookLongClick = onBookLongClick
-                            )
-                        }
+                                    bookshelfLayoutList = bookshelfLayoutList,
+                                    isEditMode = isEditMode,
+                                    selectedBookUrls = selectedBookUrls,
+                                    canReorderBooks = isEditMode &&
+                                            !uiState.isSearch &&
+                                            group.getRealBookSort() == 3,
+                                    onToggleBookSelection = { toggleBookSelection(it.bookUrl) },
+                                    onSaveBookOrder = { reorderedBooks ->
+                                        viewModel.saveBookOrder(reorderedBooks)
+                                    },
+                                    onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
+                                    onBookClick = onBookClick,
+                                    onBookLongClick = onBookLongClick
+                                )
+                            }
                         }
                         }
                     }
                     }
                 }
                 }
@@ -960,7 +977,9 @@ fun BookshelfPage(
     bookshelfLayoutList: Int,
     bookshelfLayoutList: Int,
     isEditMode: Boolean,
     isEditMode: Boolean,
     selectedBookUrls: Set<String>,
     selectedBookUrls: Set<String>,
+    canReorderBooks: Boolean,
     onToggleBookSelection: (BookShelfItem) -> Unit,
     onToggleBookSelection: (BookShelfItem) -> Unit,
+    onSaveBookOrder: (books: List<BookShelfItem>) -> Unit,
     onGlobalSearch: () -> Unit,
     onGlobalSearch: () -> Unit,
     onBookClick: (BookShelfItem) -> Unit,
     onBookClick: (BookShelfItem) -> Unit,
     onBookLongClick: (BookShelfItem) -> Unit
     onBookLongClick: (BookShelfItem) -> Unit
@@ -998,51 +1017,112 @@ fun BookshelfPage(
         if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) 12.dp else 16.dp
         if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) 12.dp else 16.dp
     val gridContentHorizontalPadding = totalHorizontalPadding / 2
     val gridContentHorizontalPadding = totalHorizontalPadding / 2
     val gridInnerHorizontalPadding = totalHorizontalPadding / 2
     val gridInnerHorizontalPadding = totalHorizontalPadding / 2
+    val hapticFeedback = LocalHapticFeedback.current
+    var draggingBooks by remember { mutableStateOf<List<BookShelfItem>?>(null) }
+    var pendingSavedBooks by remember { mutableStateOf<List<BookShelfItem>?>(null) }
+    val displayBooks = draggingBooks ?: pendingSavedBooks ?: books
+    LaunchedEffect(books, pendingSavedBooks, canReorderBooks) {
+        if (!canReorderBooks) {
+            draggingBooks = null
+            pendingSavedBooks = null
+            return@LaunchedEffect
+        }
+        val pending = pendingSavedBooks ?: return@LaunchedEffect
+        if (books.map { it.bookUrl } == pending.map { it.bookUrl }) {
+            pendingSavedBooks = null
+        }
+    }
+    val gridState = rememberLazyGridState()
+    val reorderableState = rememberReorderableLazyGridState(gridState) { from, to ->
+        if (canReorderBooks) {
+            draggingBooks = displayBooks.toMutableList().apply {
+                move(from.index, to.index)
+            }
+            hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
+        }
+    }
+    LaunchedEffect(reorderableState.isAnyItemDragging) {
+        if (!reorderableState.isAnyItemDragging) {
+            draggingBooks?.let { reorderedBooks ->
+                pendingSavedBooks = reorderedBooks
+                onSaveBookOrder(reorderedBooks)
+                draggingBooks = null
+            }
+        }
+    }
     FastScrollLazyVerticalGrid(
     FastScrollLazyVerticalGrid(
         columns = GridCells.Fixed(columns.coerceAtLeast(1)),
         columns = GridCells.Fixed(columns.coerceAtLeast(1)),
+        state = gridState,
         modifier = Modifier
         modifier = Modifier
-            .fillMaxSize()
-            .padding(horizontal = if (isGridMode) gridInnerHorizontalPadding else 0.dp),
-        contentPadding = adaptiveContentPadding(
+            .fillMaxSize(),
+        contentPadding = adaptiveContentPaddingBookshelf(
             top = paddingValues.calculateTopPadding(),
             top = paddingValues.calculateTopPadding(),
             bottom = 120.dp,
             bottom = 120.dp,
-            horizontal = if (isGridMode) gridContentHorizontalPadding else 0.dp
+            horizontal = if (isGridMode) 8.dp else 4.dp
         ),
         ),
         verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
         verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
         horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
         horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
         showFastScroll = BookshelfConfig.showBookshelfFastScroller
         showFastScroll = BookshelfConfig.showBookshelfFastScroller
     ) {
     ) {
-        items(books, key = { it.bookUrl }) { book ->
+        items(displayBooks, key = { it.bookUrl }) { book ->
             val isSelected = selectedBookUrls.contains(book.bookUrl)
             val isSelected = selectedBookUrls.contains(book.bookUrl)
-            BookItem(
-                book = book,
-                modifier = Modifier.animateItem(),
-                layoutMode = bookshelfLayoutMode,
-                isSelected = isSelected,
-                gridStyle = BookshelfConfig.bookshelfGridLayout,
-                isCompact = BookshelfConfig.bookshelfLayoutCompact,
-                isUpdating = uiState.updatingBooks.contains(book.bookUrl),
-                titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
-                titleCenter = BookshelfConfig.bookshelfTitleCenter,
-                titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
-                coverShadow = BookshelfConfig.bookshelfCoverShadow,
-                isSearchMode = uiState.isSearch,
-                searchKey = uiState.searchKey,
-                onClick = {
-                    if (isEditMode) {
-                        onToggleBookSelection(book)
-                    } else {
-                        onBookClick(book)
-                    }
-                },
-                onLongClick = {
-                    if (isEditMode) {
-                        onToggleBookSelection(book)
+            ReorderableItem(
+                state = reorderableState,
+                key = book.bookUrl,
+                enabled = canReorderBooks
+            ) {
+                BookItem(
+                    book = book,
+                    modifier = Modifier.then(
+                        if (canReorderBooks) {
+                            Modifier.longPressDraggableHandle(
+                                onDragStarted = {
+                                    draggingBooks = displayBooks
+                                    hapticFeedback.performHapticFeedback(
+                                        HapticFeedbackType.GestureThresholdActivate
+                                    )
+                                },
+                                onDragStopped = {
+                                    hapticFeedback.performHapticFeedback(
+                                        HapticFeedbackType.GestureEnd
+                                    )
+                                }
+                            )
+                        } else {
+                            Modifier
+                        }
+                    ),
+                    layoutMode = bookshelfLayoutMode,
+                    isSelected = isSelected,
+                    gridStyle = BookshelfConfig.bookshelfGridLayout,
+                    isCompact = BookshelfConfig.bookshelfLayoutCompact,
+                    isUpdating = uiState.updatingBooks.contains(book.bookUrl),
+                    titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
+                    titleCenter = BookshelfConfig.bookshelfTitleCenter,
+                    titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
+                    coverShadow = BookshelfConfig.bookshelfCoverShadow,
+                    isSearchMode = uiState.isSearch,
+                    searchKey = uiState.searchKey,
+                    onClick = {
+                        if (isEditMode) {
+                            onToggleBookSelection(book)
+                        } else {
+                            onBookClick(book)
+                        }
+                    },
+                    onLongClick = if (canReorderBooks) {
+                        null
                     } else {
                     } else {
-                        onBookLongClick(book)
+                        {
+                            if (isEditMode) {
+                                onToggleBookSelection(book)
+                            } else {
+                                onBookLongClick(book)
+                            }
+                        }
                     }
                     }
-                }
-            )
+                )
+            }
         }
         }
     }
     }
 }
 }

+ 18 - 0
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt

@@ -392,6 +392,24 @@ class BookshelfViewModel(
         }
         }
     }
     }
 
 
+    fun saveBookOrder(reorderedBooks: List<BookShelfItem>) {
+        if (reorderedBooks.isEmpty()) return
+        val isDescending = BookshelfConfig.bookshelfSortOrder == 1
+        val maxOrder = reorderedBooks.size
+        execute {
+            val updates = reorderedBooks.mapIndexedNotNull { index, book ->
+                appDb.bookDao.getBook(book.bookUrl)?.apply {
+                    order = if (isDescending) maxOrder - index else index + 1
+                }
+            }
+            if (updates.isNotEmpty()) {
+                appDb.bookDao.update(*updates.toTypedArray())
+            }
+        }.onError {
+            context.toastOnUi("排序保存失败\n${it.localizedMessage}")
+        }
+    }
+
     fun downloadBooks(bookUrls: Set<String>, downloadAllChapters: Boolean = false) {
     fun downloadBooks(bookUrls: Set<String>, downloadAllChapters: Boolean = false) {
         if (bookUrls.isEmpty()) return
         if (bookUrls.isEmpty()) return
         execute {
         execute {

+ 4 - 2
app/src/main/java/io/legado/app/ui/rss/article/RssSortRouteScreen.kt

@@ -46,6 +46,8 @@ fun RssSortRouteScreen(
     var articleStyle by remember(sourceUrl) { mutableIntStateOf(0) }
     var articleStyle by remember(sourceUrl) { mutableIntStateOf(0) }
     var redirectPolicy by remember(sourceUrl) { mutableStateOf(RedirectPolicy.ALLOW_ALL) }
     var redirectPolicy by remember(sourceUrl) { mutableStateOf(RedirectPolicy.ALLOW_ALL) }
     var screenTitle by remember(sourceUrl) { mutableStateOf("") }
     var screenTitle by remember(sourceUrl) { mutableStateOf("") }
+    val setSourceVariableText = stringResource(R.string.set_source_variable)
+    val errorText = stringResource(R.string.error)
 
 
     var showReadRecordSheet by remember { mutableStateOf(false) }
     var showReadRecordSheet by remember { mutableStateOf(false) }
     var readRecords by remember { mutableStateOf<List<RssReadRecord>>(emptyList()) }
     var readRecords by remember { mutableStateOf<List<RssReadRecord>>(emptyList()) }
@@ -104,7 +106,7 @@ fun RssSortRouteScreen(
                 val variable = withContext(Dispatchers.IO) { source.getVariable() }
                 val variable = withContext(Dispatchers.IO) { source.getVariable() }
                 activity?.showDialogFragment(
                 activity?.showDialogFragment(
                     VariableDialog(
                     VariableDialog(
-                        context.getString(R.string.set_source_variable),
+                        setSourceVariableText,
                         source.getKey(),
                         source.getKey(),
                         variable,
                         variable,
                         comment
                         comment
@@ -141,7 +143,7 @@ fun RssSortRouteScreen(
                 viewModel.rssSource?.sourceUrl ?: sourceUrl.orEmpty()
                 viewModel.rssSource?.sourceUrl ?: sourceUrl.orEmpty()
             }
             }
             if (openOrigin.isBlank()) {
             if (openOrigin.isBlank()) {
-                context.toastOnUi(context.getString(R.string.error))
+                context.toastOnUi(errorText)
             } else {
             } else {
                 onOpenRead(record.title, openOrigin, null, record.record)
                 onOpenRead(record.title, openOrigin, null, record.record)
             }
             }

+ 16 - 0
app/src/main/java/io/legado/app/ui/theme/AdaptivePadding.kt

@@ -78,3 +78,19 @@ fun adaptiveContentPadding(
         end = horizontal
         end = horizontal
     )
     )
 }
 }
+
+@Composable
+fun adaptiveContentPaddingBookshelf(
+    top: Dp,
+    bottom: Dp,
+    horizontal: Dp
+): PaddingValues {
+    val adjustedTop = if (ThemeResolver.isMiuixEngine(composeEngine)) top + 12.dp else top + 8.dp
+    val horizontal = if (ThemeResolver.isMiuixEngine(composeEngine)) 12.dp + horizontal else 4.dp + horizontal
+    return PaddingValues(
+        top = adjustedTop,
+        bottom = bottom,
+        start = horizontal,
+        end = horizontal
+    )
+}

+ 0 - 21
app/src/main/java/io/legado/app/ui/widget/components/button/TopBarButton.kt

@@ -136,27 +136,6 @@ fun TopBarNavigationButton(
     }
     }
 }
 }
 
 
-@Deprecated(
-    message = "Use TopBarNavigationButton for consistent TopBar naming.",
-    replaceWith = ReplaceWith("TopBarNavigationButton(onClick, modifier, imageVector, contentDescription, style)")
-)
-@Composable
-fun TopbarNavigationButton(
-    onClick: () -> Unit,
-    modifier: Modifier = Modifier,
-    imageVector: ImageVector = AppIcons.Back,
-    contentDescription: String? = stringResource(id = R.string.back),
-    style: TopBarButtonVariant = TopBarButtonVariant.Filled
-) {
-    TopBarNavigationButton(
-        onClick = onClick,
-        modifier = modifier,
-        imageVector = imageVector,
-        contentDescription = contentDescription,
-        style = style
-    )
-}
-
 @Composable
 @Composable
 fun TopBarActionButton(
 fun TopBarActionButton(
     onClick: () -> Unit,
     onClick: () -> Unit,

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

@@ -279,7 +279,7 @@ fun VerticalGridFastScroller(
     contentPadding: PaddingValues,
     contentPadding: PaddingValues,
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
     thumbAllowed: () -> Boolean = { true },
     thumbAllowed: () -> Boolean = { true },
-    thumbColor: Color = MaterialTheme.colorScheme.primary,
+    thumbColor: Color = LegadoTheme.colorScheme.primary,
     topContentPadding: Dp = Dp.Hairline,
     topContentPadding: Dp = Dp.Hairline,
     bottomContentPadding: Dp = Dp.Hairline,
     bottomContentPadding: Dp = Dp.Hairline,
     endContentPadding: Dp = Dp.Hairline,
     endContentPadding: Dp = Dp.Hairline,