diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index ac136af40..eb41c5028 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -241,6 +241,7 @@ object PreferKey { const val bookshelfTitleCenter = "bookshelfTitleCenter" const val bookshelfTitleMaxLines = "bookshelfTitleMaxLines" const val bookshelfCoverShadow = "bookshelfCoverShadow" + const val bookshelfSearchActionDirectToSearch = "bookshelfSearchActionDirectToSearch" const val exploreLayoutGridLandscape = "exploreLayoutGridLandscape" diff --git a/app/src/main/java/io/legado/app/di/appModule.kt b/app/src/main/java/io/legado/app/di/appModule.kt index f5a489437..47f656e01 100644 --- a/app/src/main/java/io/legado/app/di/appModule.kt +++ b/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.changesource.ChangeBookSourceComposeViewModel 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.group.GroupViewModel 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.rule.TxtTocRuleViewModel 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.otherConfig.OtherConfigViewModel import io.legado.app.ui.config.readConfig.ReadConfigViewModel @@ -124,7 +124,7 @@ val appModule = module { singleOf(::ResolveBookShelfStateUseCase) singleOf(::ShrinkDatabaseUseCase) singleOf(::WebDavBackupUseCase) - singleOf(::CacheConfig) + singleOf(::BookshelfManageScreenConfig) single { DirectLinkUploadRepository() } single { AppStartupRepository(get()) } @@ -188,12 +188,12 @@ val appModule = module { viewModelOf(::RssViewModel) viewModelOf(::SearchViewModel) viewModel { - CacheViewModel( + BookshelfManageScreenViewModel( application = get(), bookDao = get(), bookGroupDao = get(), bookChapterDao = get(), - cacheConfig = get(), + bookshelfManageScreenConfig = get(), batchCacheDownloadUseCase = get(), cacheBookChaptersUseCase = get(), changeBookSourceUseCase = get(), diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt index 91278f28f..9b7537745 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt +++ b/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 showOptionsMenu 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 origin = it.data?.getStringExtra("origin") ?: return@rememberLauncherForActivityResult @@ -490,7 +491,7 @@ fun ChangeSourceSheet( pendingMigration = PendingSourceMigration(source, book, toc) } else { onAddAsNew(book, toc) - context.toastOnUi(context.getString(R.string.book_added_to_shelf)) + context.toastOnUi(bookAddedToShelfText) } actionBook = null }, { diff --git a/app/src/main/java/io/legado/app/ui/book/cache/CacheScreen.kt b/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageScreen.kt similarity index 83% rename from app/src/main/java/io/legado/app/ui/book/cache/CacheScreen.kt rename to app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageScreen.kt index dc1fce079..d11ac8994 100644 --- a/app/src/main/java/io/legado/app/ui/book/cache/CacheScreen.kt +++ b/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.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.widget.components.button.MediumIconButton import org.koin.androidx.compose.koinViewModel +import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState -data class CacheFabAction( +data class BookshelfManageFabAction( val icon: androidx.compose.ui.graphics.vector.ImageVector, val label: String, val action: () -> Unit ) -private data class CacheListState( +private data class BookshelfManageListState( override val items: List = emptyList(), override val selectedIds: Set = emptySet(), override val searchKey: String = "", @@ -137,21 +138,21 @@ private data class CacheListState( ) : ListUiState @Composable -fun CacheRouteScreen( +fun BookshelfManageRouteScreen( groupId: Long, onBackClick: () -> Unit, - viewModel: CacheViewModel = koinViewModel() + viewModel: BookshelfManageScreenViewModel = koinViewModel() ) { 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) @Composable -private fun CacheScreen( - viewModel: CacheViewModel, +private fun BookshelfManageScreen( + viewModel: BookshelfManageScreenViewModel, onBackClick: () -> Unit ) { val state by viewModel.uiState.collectAsStateWithLifecycle() @@ -203,6 +204,7 @@ private fun CacheScreen( val exportFileNameText = stringResource(R.string.export_file_name) val resultAnalyzedText = stringResource(R.string.result_analyzed) val errorScopeInputText = stringResource(R.string.error_scope_input) + val noGroupText = stringResource(R.string.no_group) val exportFileNameHintText = "书名:《{name}》 作者:{author}" val exportFileNameHelpText = """ 支持变量:{name}(书名)、{author}(作者)、{group}(分组)、{source}(书源)、{remark}(备注)。 @@ -218,15 +220,15 @@ private fun CacheScreen( val booksByUrl = remember(state.books) { state.books.associateBy { it.bookUrl } } 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 -> if (book.group <= 0L) { - context.getString(R.string.no_group) + noGroupText } else { val groups = userGroups.filter { (book.group and it.groupId) > 0L } - if (groups.isEmpty()) context.getString(R.string.no_group) + if (groups.isEmpty()) noGroupText else groups.joinToString("、") { it.groupName } } } @@ -245,7 +247,7 @@ private fun CacheScreen( } } val listUiState = remember(filteredBooks, selectedBookUrls, searchKey, isSearchMode) { - CacheListState( + BookshelfManageListState( items = filteredBooks, selectedIds = selectedBookUrls.mapTo(linkedSetOf()) { it as Any }, searchKey = searchKey, @@ -327,9 +329,9 @@ private fun CacheScreen( LaunchedEffect(Unit) { viewModel.effects.collect { effect -> when (effect) { - is CacheEffect.ShowMessage -> context.toastOnUi(effect.message) - is CacheEffect.NotifyBookChanged -> Unit - is CacheEffect.OpenBookInfo -> context.startActivity { + is BookshelfManageScreenEffect.ShowMessage -> context.toastOnUi(effect.message) + is BookshelfManageScreenEffect.NotifyBookChanged -> Unit + is BookshelfManageScreenEffect.OpenBookInfo -> context.startActivity { putExtra("name", effect.name) putExtra("author", effect.author) putExtra("bookUrl", effect.bookUrl) @@ -398,20 +400,20 @@ private fun CacheScreen( return if (targetBooks.all { it.group == firstGroup }) firstGroup else 0L } val fabItems = listOf( - CacheFabAction( + BookshelfManageFabAction( Icons.Default.SelectAll, stringResource(R.string.select_all) ) { selectedBookUrls = state.books.mapTo(hashSetOf()) { it.bookUrl } }, - CacheFabAction( + BookshelfManageFabAction( Icons.Default.Refresh, stringResource(R.string.revert_selection) ) { val visibleBookUrls = booksByUrl.keys selectedBookUrls = visibleBookUrls - selectedBookUrls }, - CacheFabAction( + BookshelfManageFabAction( Icons.Default.Download, "缓存选中" ) { @@ -419,7 +421,7 @@ private fun CacheScreen( showBatchDownloadConfirmDialog = true } }, - CacheFabAction( + BookshelfManageFabAction( Icons.Default.Refresh, "批量换源" ) { @@ -427,7 +429,7 @@ private fun CacheScreen( showBatchSourcePickerSheet = true } }, - CacheFabAction( + BookshelfManageFabAction( Icons.Default.Bookmarks, stringResource(R.string.move_to_group) ) { @@ -437,20 +439,20 @@ private fun CacheScreen( showGroupSelectSheet = true } }, - CacheFabAction( + BookshelfManageFabAction( Icons.Default.Upload, "导出选中" ) { exportSelected() }, - CacheFabAction( + BookshelfManageFabAction( Icons.Default.Delete, stringResource(R.string.clear_cache) ) { - viewModel.dispatch(CacheIntent.ClearCachesForBooks(selectedBookUrls)) + viewModel.dispatch(BookshelfManageScreenIntent.ClearCachesForBooks(selectedBookUrls)) clearSelection() }, - CacheFabAction( + BookshelfManageFabAction( Icons.Default.Delete, stringResource(R.string.delete) ) { @@ -462,6 +464,17 @@ private fun CacheScreen( } ) 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( title = if (inSelectionMode) { "已选 ${selectedBookUrls.size}/${filteredBooks.size}" @@ -495,7 +508,7 @@ private fun CacheScreen( isSelected = group.groupId == state.groupId, onClick = { dismiss() - viewModel.dispatch(CacheIntent.ChangeGroup(group.groupId)) + viewModel.dispatch(BookshelfManageScreenIntent.ChangeGroup(group.groupId)) } ) } @@ -508,7 +521,7 @@ private fun CacheScreen( onClick = { dismiss() if (state.isDownloadRunning) { - viewModel.dispatch(CacheIntent.StopDownload) + viewModel.dispatch(BookshelfManageScreenIntent.StopDownload) } else { showDownloadAllConfirmDialog = true } @@ -553,7 +566,7 @@ private fun CacheScreen( onClick = { dismiss() viewModel.dispatch( - CacheIntent.SetExportUseReplace(!state.exportConfig.exportUseReplace) + BookshelfManageScreenIntent.SetExportUseReplace(!state.exportConfig.exportUseReplace) ) } ) @@ -563,7 +576,7 @@ private fun CacheScreen( onClick = { dismiss() viewModel.dispatch( - CacheIntent.SetEnableCustomExport(!state.exportConfig.enableCustomExport) + BookshelfManageScreenIntent.SetEnableCustomExport(!state.exportConfig.enableCustomExport) ) } ) @@ -573,7 +586,7 @@ private fun CacheScreen( onClick = { dismiss() viewModel.dispatch( - CacheIntent.SetExportNoChapterName(!state.exportConfig.exportNoChapterName) + BookshelfManageScreenIntent.SetExportNoChapterName(!state.exportConfig.exportNoChapterName) ) } ) @@ -583,7 +596,7 @@ private fun CacheScreen( onClick = { dismiss() viewModel.dispatch( - CacheIntent.SetExportToWebDav(!state.exportConfig.exportToWebDav) + BookshelfManageScreenIntent.SetExportToWebDav(!state.exportConfig.exportToWebDav) ) } ) @@ -593,7 +606,7 @@ private fun CacheScreen( onClick = { dismiss() viewModel.dispatch( - CacheIntent.SetExportPictureFile(!state.exportConfig.exportPictureFile) + BookshelfManageScreenIntent.SetExportPictureFile(!state.exportConfig.exportPictureFile) ) } ) @@ -603,7 +616,7 @@ private fun CacheScreen( onClick = { dismiss() 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) { 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 .fillMaxWidth() - .adaptiveHorizontalPadding(vertical = 12.dp), - verticalArrangement = Arrangement.SpaceBetween - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - 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)) - } - } - TextCard( - text = if (book.isLocal) { - stringResource(R.string.local_book) + .then( + if (canReorderBooks) { + Modifier.longPressDraggableHandle() } else { - stringResource( - R.string.download_count, - cacheCount, - book.totalChapterNum - ) + Modifier } - ) + ), + onClick = { toggleBookSelection(book) }, + onLongClick = { toggleBookSelection(book) }, + containerColor = if (isSelected) { + LegadoTheme.colorScheme.surfaceContainerHigh + } else { + LegadoTheme.colorScheme.surfaceContainerLow } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .adaptiveHorizontalPadding(vertical = 12.dp), + verticalArrangement = Arrangement.SpaceBetween ) { - 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" - ) - 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 = "换源", - onClick = { - singleChangeSourceBook = book - dismiss() - } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + AppText( + text = book.name, + style = LegadoTheme.typography.titleSmallEmphasized, + maxLines = 1 ) - RoundDropdownMenuItem( - text = "删除书籍", - onClick = { - pendingDeleteBookUrls = setOf(book.bookUrl) - deleteOriginalBookFile = state.deleteBookOriginal - showDeleteBookConfirmDialog = true - dismiss() - } + AppText( + text = book.getRealAuthor(), + style = LegadoTheme.typography.bodySmall ) - RoundDropdownMenuItem( - text = "删除缓存", - onClick = { - viewModel.dispatch(CacheIntent.ClearCachesForBooks(setOf(book.bookUrl))) - dismiss() - } + 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 + ) + } + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + SmallTonalIconButton( + onClick = { + if (!book.isLocal) { + viewModel.dispatch(BookshelfManageScreenIntent.ToggleBookDownload(book)) + } + }, + imageVector = if (isDownloading) Icons.Default.Stop else Icons.Default.Download, + contentDescription = "download" + ) + 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 = "换源", + 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 }, onReplace = { source, newBook, toc, options -> viewModel.dispatch( - CacheIntent.ChangeBookSource( + BookshelfManageScreenIntent.ChangeBookSource( oldBookUrl = book.bookUrl, source = source, book = newBook, @@ -842,7 +873,7 @@ private fun CacheScreen( onDismissRequest = { pendingBatchSources = emptyList() }, onConfirm = { options -> viewModel.dispatch( - CacheIntent.BatchChangeBookSource( + BookshelfManageScreenIntent.BatchChangeBookSource( bookUrls = selectedBookUrls, sources = pendingBatchSources, options = options, @@ -856,18 +887,18 @@ private fun CacheScreen( BatchChangePreviewSheet( show = state.batchChangePreviewItems.isNotEmpty(), items = state.batchChangePreviewItems, - onDismissRequest = { viewModel.dispatch(CacheIntent.DismissBatchChangePreview) }, + onDismissRequest = { viewModel.dispatch(BookshelfManageScreenIntent.DismissBatchChangePreview) }, onOpenBook = { book, inBookshelf -> - viewModel.dispatch(CacheIntent.OpenBookInfoPreview(book, inBookshelf)) + viewModel.dispatch(BookshelfManageScreenIntent.OpenBookInfoPreview(book, inBookshelf)) }, 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 -> - viewModel.dispatch(CacheIntent.AddPreviewItemToShelf(bookUrl)) + viewModel.dispatch(BookshelfManageScreenIntent.AddPreviewItemToShelf(bookUrl)) }, onShowOtherSources = { item -> otherSourcePreviewItem = item }, - onMigrateAll = { viewModel.dispatch(CacheIntent.MigrateAllPreviewItems) }, + onMigrateAll = { viewModel.dispatch(BookshelfManageScreenIntent.MigrateAllPreviewItems) }, ) manualSearchPreviewBook?.let { book -> @@ -877,7 +908,7 @@ private fun CacheScreen( onDismissRequest = { manualSearchPreviewBook = null }, onReplace = { source, newBook, toc, _ -> viewModel.dispatch( - CacheIntent.UpdatePreviewItem( + BookshelfManageScreenIntent.UpdatePreviewItem( oldBookUrl = book.bookUrl, source = source, book = newBook, @@ -896,11 +927,11 @@ private fun CacheScreen( item = otherSourcePreviewItem, onDismissRequest = { otherSourcePreviewItem = null }, onSelect = { oldBookUrl, index -> - viewModel.dispatch(CacheIntent.SelectPreviewCandidate(oldBookUrl, index)) + viewModel.dispatch(BookshelfManageScreenIntent.SelectPreviewCandidate(oldBookUrl, index)) otherSourcePreviewItem = null }, 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, onDismissRequest = { if (!state.isChangingSource) { - viewModel.dispatch(CacheIntent.DismissChangeSourceStatus) + viewModel.dispatch(BookshelfManageScreenIntent.DismissChangeSourceStatus) } }, title = stringResource(R.string.change_source_batch), @@ -936,7 +967,7 @@ private fun CacheScreen( onConfirm = if (state.isChangingSource) { null } else { - { viewModel.dispatch(CacheIntent.DismissChangeSourceStatus) } + { viewModel.dispatch(BookshelfManageScreenIntent.DismissChangeSourceStatus) } }, ) @@ -959,7 +990,7 @@ private fun CacheScreen( onConfirm = { showBatchDownloadConfirmDialog = false viewModel.dispatch( - CacheIntent.DownloadBooks( + BookshelfManageScreenIntent.DownloadBooks( bookUrls = selectedBookUrls, downloadAllChapters = false ) @@ -999,7 +1030,7 @@ private fun CacheScreen( onConfirm = { showDeleteBookConfirmDialog = false viewModel.dispatch( - CacheIntent.DeleteBooks( + BookshelfManageScreenIntent.DeleteBooks( bookUrls = pendingDeleteBookUrls, deleteOriginal = deleteOriginalBookFile ) @@ -1019,7 +1050,7 @@ private fun CacheScreen( val moveSet = pendingMoveGroupBookUrl?.let { setOf(it) } ?: selectedBookUrls val targetGroupId = groupId.coerceAtLeast(0L) viewModel.dispatch( - CacheIntent.MoveBooksToGroup( + BookshelfManageScreenIntent.MoveBooksToGroup( bookUrls = moveSet, groupId = targetGroupId ) @@ -1040,7 +1071,7 @@ private fun CacheScreen( onConfirm = { showDownloadAllConfirmDialog = false viewModel.dispatch( - CacheIntent.StartDownloadForVisibleBooks( + BookshelfManageScreenIntent.StartDownloadForVisibleBooks( books = state.books, downloadAllChapters = true ) @@ -1060,7 +1091,7 @@ private fun CacheScreen( icon = if (type == "epub") Icons.Default.Upload else Icons.Default.Download, text = type, onClick = { - viewModel.dispatch(CacheIntent.SetExportType(index)) + viewModel.dispatch(BookshelfManageScreenIntent.SetExportType(index)) showExportTypeDialog = false } ) @@ -1085,7 +1116,7 @@ private fun CacheScreen( }, confirmText = stringResource(android.R.string.ok), onConfirm = { - viewModel.dispatch(CacheIntent.SetBookExportFileName(exportFileNameInput)) + viewModel.dispatch(BookshelfManageScreenIntent.SetBookExportFileName(exportFileNameInput)) showExportFileNameDialog = false }, dismissText = stringResource(android.R.string.cancel), @@ -1107,7 +1138,7 @@ private fun CacheScreen( confirmText = stringResource(android.R.string.ok), onConfirm = { viewModel.dispatch( - CacheIntent.SetExportCharset( + BookshelfManageScreenIntent.SetExportCharset( exportCharsetInput.ifBlank { "UTF-8" } ) ) @@ -1228,7 +1259,7 @@ private fun CacheScreen( customEpisodeExportNameInput ) ) { - viewModel.dispatch(CacheIntent.SetEpisodeExportFileName(customEpisodeExportNameInput)) + viewModel.dispatch(BookshelfManageScreenIntent.SetEpisodeExportFileName(customEpisodeExportNameInput)) } val epubSize = customEpubSizeInput.toIntOrNull()?.coerceAtLeast(1) ?: 1 context.startService { diff --git a/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt b/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageScreenViewModel.kt similarity index 65% rename from app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt rename to app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageScreenViewModel.kt index c3ec40e17..995d6d328 100644 --- a/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt +++ b/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 androidx.lifecycle.viewModelScope @@ -29,10 +29,12 @@ import io.legado.app.help.book.removeType import io.legado.app.model.CacheBook import io.legado.app.help.config.LocalConfig 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.main.bookshelf.toLightBook import io.legado.app.utils.cnCompare +import io.legado.app.utils.move import kotlinx.coroutines.Job import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableSharedFlow @@ -44,7 +46,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlin.math.max -data class CacheExportConfig( +data class BookshelfManageScreenExportConfig( val exportUseReplace: Boolean = true, val enableCustomExport: Boolean = false, val exportNoChapterName: Boolean = false, @@ -57,11 +59,13 @@ data class CacheExportConfig( val episodeExportFileName: String = "" ) -data class CacheUiState( +data class BookshelfManageScreenUiState( val groupId: Long = -1, val groupName: String? = null, val groupList: List = emptyList(), val books: List = emptyList(), + val bookSort: Int = BookshelfConfig.bookshelfSort, + val bookSortOrder: Int = BookshelfConfig.bookshelfSortOrder, val isDownloadRunning: Boolean = false, val isChangingSource: Boolean = false, val changeSourceProgress: String? = null, @@ -71,74 +75,75 @@ data class CacheUiState( val batchChangeOptions: ChangeSourceMigrationOptions = ChangeSourceMigrationOptions(), val cacheVersion: Long = 0, 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( val books: List, 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, val groupId: Long) : CacheIntent - data class DeleteBooks(val bookUrls: Set, val deleteOriginal: Boolean) : CacheIntent - data class ClearCachesForBooks(val bookUrls: Set) : CacheIntent - data class DownloadBooks(val bookUrls: Set, 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, val groupId: Long) : BookshelfManageScreenIntent + data class DeleteBooks(val bookUrls: Set, val deleteOriginal: Boolean) : BookshelfManageScreenIntent + data class ClearCachesForBooks(val bookUrls: Set) : BookshelfManageScreenIntent + data class MoveBookOrder(val fromIndex: Int, val toIndex: Int) : BookshelfManageScreenIntent + data class DownloadBooks(val bookUrls: Set, val downloadAllChapters: Boolean) : BookshelfManageScreenIntent data class ChangeBookSource( val oldBookUrl: String, val source: BookSource, val book: Book, val chapters: List, val options: ChangeSourceMigrationOptions, - ) : CacheIntent + ) : BookshelfManageScreenIntent data class BatchChangeBookSource( val bookUrls: Set, val sources: List, 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( val oldBookUrl: String, val source: BookSource, val book: Book, val chapters: List, - ) : 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, private val bookDao: BookDao, private val bookGroupDao: BookGroupDao, private val bookChapterDao: BookChapterDao, - val cacheConfig: CacheConfig, + val bookshelfManageScreenConfig: BookshelfManageScreenConfig, private val batchCacheDownloadUseCase: BatchCacheDownloadUseCase, private val cacheBookChaptersUseCase: CacheBookChaptersUseCase, private val changeBookSourceUseCase: ChangeBookSourceUseCase, @@ -147,10 +152,10 @@ class CacheViewModel( private val updateBooksGroupUseCase: UpdateBooksGroupUseCase ) : BaseViewModel(application) { - private val _uiState = MutableStateFlow(CacheUiState()) + private val _uiState = MutableStateFlow(BookshelfManageScreenUiState()) val uiState = _uiState.asStateFlow() - private val _effects = MutableSharedFlow(extraBufferCapacity = 32) + private val _effects = MutableSharedFlow(extraBufferCapacity = 32) val effects = _effects.asSharedFlow() private val cacheChapters = hashMapOf>() @@ -158,24 +163,25 @@ class CacheViewModel( private var groupsJob: Job? = null private var observersStarted = false - fun dispatch(intent: CacheIntent) { + fun dispatch(intent: BookshelfManageScreenIntent) { 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.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.source, intent.book, @@ -183,35 +189,35 @@ class CacheViewModel( intent.options ) - is CacheIntent.BatchChangeBookSource -> batchChangeBookSource( + is BookshelfManageScreenIntent.BatchChangeBookSource -> batchChangeBookSource( intent.bookUrls, intent.sources, 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.candidateIndex ) - is CacheIntent.UpdatePreviewItem -> updatePreviewItem( + is BookshelfManageScreenIntent.UpdatePreviewItem -> updatePreviewItem( intent.oldBookUrl, intent.source, intent.book, 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.inBookshelf ) - CacheIntent.MigrateAllPreviewItems -> migrateAllPreviewItems() + BookshelfManageScreenIntent.MigrateAllPreviewItems -> migrateAllPreviewItems() - CacheIntent.DismissChangeSourceStatus -> { + BookshelfManageScreenIntent.DismissChangeSourceStatus -> { _uiState.update { it.copy( changeSourceProgress = null, @@ -221,59 +227,59 @@ class CacheViewModel( } } - CacheIntent.DismissBatchChangePreview -> { + BookshelfManageScreenIntent.DismissBatchChangePreview -> { _uiState.update { it.copy(batchChangePreviewItems = emptyList()) } } - is CacheIntent.SetExportUseReplace -> { - cacheConfig.exportUseReplace = intent.enabled + is BookshelfManageScreenIntent.SetExportUseReplace -> { + bookshelfManageScreenConfig.exportUseReplace = intent.enabled syncExportConfig() 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() } - is CacheIntent.SetExportNoChapterName -> { - cacheConfig.exportNoChapterName = intent.enabled + is BookshelfManageScreenIntent.SetExportNoChapterName -> { + bookshelfManageScreenConfig.exportNoChapterName = intent.enabled syncExportConfig() } - is CacheIntent.SetExportToWebDav -> { - cacheConfig.exportToWebDav = intent.enabled + is BookshelfManageScreenIntent.SetExportToWebDav -> { + bookshelfManageScreenConfig.exportToWebDav = intent.enabled syncExportConfig() } - is CacheIntent.SetExportPictureFile -> { - cacheConfig.exportPictureFile = intent.enabled + is BookshelfManageScreenIntent.SetExportPictureFile -> { + bookshelfManageScreenConfig.exportPictureFile = intent.enabled syncExportConfig() } - is CacheIntent.SetParallelExportBook -> { - cacheConfig.parallelExportBook = intent.enabled + is BookshelfManageScreenIntent.SetParallelExportBook -> { + bookshelfManageScreenConfig.parallelExportBook = intent.enabled syncExportConfig() } - is CacheIntent.SetExportType -> { - cacheConfig.exportType = intent.type + is BookshelfManageScreenIntent.SetExportType -> { + bookshelfManageScreenConfig.exportType = intent.type syncExportConfig() } - is CacheIntent.SetExportCharset -> { - cacheConfig.exportCharset = intent.charset + is BookshelfManageScreenIntent.SetExportCharset -> { + bookshelfManageScreenConfig.exportCharset = intent.charset syncExportConfig() } - is CacheIntent.SetBookExportFileName -> { - cacheConfig.bookExportFileName = intent.fileName + is BookshelfManageScreenIntent.SetBookExportFileName -> { + bookshelfManageScreenConfig.bookExportFileName = intent.fileName syncExportConfig() } - is CacheIntent.SetEpisodeExportFileName -> { - cacheConfig.episodeExportFileName = intent.fileName + is BookshelfManageScreenIntent.SetEpisodeExportFileName -> { + bookshelfManageScreenConfig.episodeExportFileName = intent.fileName syncExportConfig() } } @@ -316,18 +322,38 @@ class CacheViewModel( booksJob = viewModelScope.launch { bookDao.flowBookShelfByGroup(groupId).map { books -> 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) } - 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) } } @@ -372,17 +398,17 @@ class CacheViewModel( private fun syncExportConfig() { _uiState.update { 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 { updateBooksGroupUseCase.replaceGroup(bookUrls, safeGroupId) }.onError { - _effects.tryEmit(CacheEffect.ShowMessage("移动分组失败\n${it.localizedMessage}")) + _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("移动分组失败\n${it.localizedMessage}")) } } @@ -475,9 +501,9 @@ class CacheViewModel( }.onSuccess { deletedBookUrls -> _uiState.update { it.copy(deleteBookOriginal = deleteOriginal) } deletedBookUrls.forEach { cacheChapters.remove(it) } - _effects.tryEmit(CacheEffect.ShowMessage("删除成功")) + _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("删除成功")) }.onError { - _effects.tryEmit(CacheEffect.ShowMessage("删除失败\n${it.localizedMessage}")) + _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("删除失败\n${it.localizedMessage}")) } } @@ -490,9 +516,33 @@ class CacheViewModel( cacheChapters[bookUrl] = hashSetOf() emitBookChanged(bookUrl) } - _effects.tryEmit(CacheEffect.ShowMessage("缓存已清理")) + _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("缓存已清理")) }.onError { - _effects.tryEmit(CacheEffect.ShowMessage("清理缓存失败\n${it.localizedMessage}")) + _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 { + _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("排序保存失败\n${it.localizedMessage}")) } } @@ -506,13 +556,13 @@ class CacheViewModel( ) }.onSuccess { count -> if (count > 0) { - _effects.tryEmit(CacheEffect.ShowMessage("已加入缓存队列: $count 本")) + _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("已加入缓存队列: $count 本")) } else { - _effects.tryEmit(CacheEffect.ShowMessage("没有可缓存的书籍")) + _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("没有可缓存的书籍")) } syncDownloadRunning() }.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[result.book.bookUrl] = hashSetOf() emitBookChanged(result.book.bookUrl) - _effects.tryEmit(CacheEffect.ShowMessage("换源完成")) + _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("换源完成")) }.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() removePreviewItem(oldBookUrl) emitBookChanged(result.book.bookUrl) - _effects.tryEmit(CacheEffect.ShowMessage("迁移完成")) + _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("迁移完成")) }.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()) candidate.book }.onSuccess { - _effects.tryEmit(CacheEffect.ShowMessage("已添加到书架")) + _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("已添加到书架")) }.onError { - _effects.tryEmit(CacheEffect.ShowMessage("添加书籍失败\n${it.localizedMessage}")) + _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("添加书籍失败\n${it.localizedMessage}")) } } @@ -717,7 +767,7 @@ class CacheViewModel( } book }.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 { cacheChapters.clear() _uiState.update { it.copy(batchChangePreviewItems = emptyList()) } - _effects.tryEmit(CacheEffect.ShowMessage("批量迁移完成")) + _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("批量迁移完成")) }.onError { - _effects.tryEmit(CacheEffect.ShowMessage("批量迁移失败\n${it.localizedMessage}")) + _effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("批量迁移失败\n${it.localizedMessage}")) }.onFinally { _uiState.update { it.copy( @@ -781,7 +831,7 @@ class CacheViewModel( private fun emitBookChanged(bookUrl: String) { _uiState.update { it.copy(cacheVersion = it.cacheVersion + 1) } - _effects.tryEmit(CacheEffect.NotifyBookChanged(bookUrl)) + _effects.tryEmit(BookshelfManageScreenEffect.NotifyBookChanged(bookUrl)) } } diff --git a/app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt b/app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt index 4c8be692b..33eca4350 100644 --- a/app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt +++ b/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 bookshelfSearchActionDirectToSearch by prefDelegate( + PreferKey.bookshelfSearchActionDirectToSearch, + true + ) + /** * 启动时自动刷新书架 */ diff --git a/app/src/main/java/io/legado/app/ui/config/cacheConfig/CacheConfig.kt b/app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfManageScreenConfig.kt similarity index 84% rename from app/src/main/java/io/legado/app/ui/config/cacheConfig/CacheConfig.kt rename to app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfManageScreenConfig.kt index 121cdba97..e466205c3 100644 --- a/app/src/main/java/io/legado/app/ui/config/cacheConfig/CacheConfig.kt +++ b/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.data.dao.BookGroupDao import io.legado.app.ui.config.prefDelegate -class CacheConfig( +class BookshelfManageScreenConfig( private val bookGroupDao: BookGroupDao ) { @@ -28,9 +28,10 @@ class CacheConfig( var parallelExportBook by prefDelegate(PreferKey.parallelExportBook, false) - var bookshelfSort by prefDelegate(PreferKey.bookshelfSort, 0) - fun getBookSortByGroupId(groupId: Long): Int { - return bookGroupDao.getByID(groupId)?.getRealBookSort() ?: bookshelfSort + return bookGroupDao.getByID(groupId)?.getRealBookSort() ?: BookshelfConfig.bookshelfSort } + + val bookshelfSortOrder: Int + get() = BookshelfConfig.bookshelfSortOrder } diff --git a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt index 0949edda8..1cc6a8127 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt +++ b/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.SearchViewModel 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.config.ConfigNavScreen import io.legado.app.ui.config.ConfigTag @@ -161,7 +161,7 @@ open class MainActivity : BaseComposeActivity() { } } - fun createCacheIntent( + fun createBookshelfManageScreenIntent( context: Context, groupId: Long = -1L ): Intent { @@ -171,6 +171,11 @@ open class MainActivity : BaseComposeActivity() { } } + fun createCacheIntent( + context: Context, + groupId: Long = -1L + ): Intent = createBookshelfManageScreenIntent(context, groupId) + fun createSearchIntent( context: Context, key: String? = null, @@ -463,7 +468,7 @@ open class MainActivity : BaseComposeActivity() { } entry { route -> - CacheRouteScreen( + BookshelfManageRouteScreen( groupId = route.groupId, onBackClick = { navigateBack(backStack) } ) diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt index 20d702c85..93e37f325 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt +++ b/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.ui.config.bookshelfConfig.BookshelfConfig 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.cover.CoilBookCover import io.legado.app.ui.widget.components.cover.BookshelfCover @@ -55,6 +56,7 @@ fun BookshelfItem( title: String, modifier: Modifier = Modifier, isSelected: Boolean = false, + titleEnd: @Composable (() -> Unit)? = null, subTitle: String? = null, desc: String? = null, extra: @Composable (RowScope.() -> Unit)? = null, @@ -63,12 +65,12 @@ fun BookshelfItem( titleMaxLines: Int = 2, coverShadow: Boolean = false, onClick: () -> Unit, - onLongClick: () -> Unit + onLongClick: (() -> Unit)? ) { val selectedColor = if (isSelected) { - LegadoTheme.colorScheme.secondaryContainer.copy(alpha = 0.55f) + LegadoTheme.colorScheme.secondaryContainer } else { - Color.Transparent + LegadoTheme.colorScheme.surface } if (isGrid) { Box( @@ -142,59 +144,66 @@ fun BookshelfItem( } else { // 列表布局 Column { - Row( + GlassCard( modifier = modifier .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( - text = it, - style = LegadoTheme.typography.bodySmall, + text = title, + style = LegadoTheme.typography.titleMediumEmphasized, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(top = 2.dp) + modifier = Modifier.weight(1f) ) + titleEnd?.invoke() } - if (!isCompact) { - desc?.let { + subTitle?.let { AppText( text = it, style = LegadoTheme.typography.bodySmall, maxLines = 1, 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, modifier: Modifier = Modifier, onClick: () -> Unit, - onLongClick: () -> Unit + onLongClick: (() -> Unit)? ) { BookshelfItem( isGrid = true, @@ -362,7 +371,7 @@ fun BookGroupItemList( coverShadow: Boolean = false, modifier: Modifier = Modifier, onClick: () -> Unit, - onLongClick: () -> Unit + onLongClick: (() -> Unit)? ) { BookshelfItem( isGrid = false, @@ -397,9 +406,10 @@ fun BookItem( isSearchMode: Boolean = false, searchKey: String = "", onClick: () -> Unit, - onLongClick: () -> Unit + onLongClick: (() -> Unit)? ) { val unreadCount = book.getUnreadChapterNum() + val unreadText = if (BookshelfConfig.showUnread && unreadCount > 0) unreadCount.toString() else null val bookTypeLabel = if (BookshelfConfig.showTip) { when { book.isAudio -> stringResource(R.string.audio) @@ -427,6 +437,16 @@ fun BookItem( isCompact = isCompact, isSelected = isSelected, modifier = modifier, + titleEnd = if (layoutMode == 0 && unreadText != null) { + { + TextCard( + text = unreadText, + cornerRadius = 4.dp, + horizontalPadding = 4.dp, + verticalPadding = 0.dp + ) + } + } else null, cover = { modifier -> BookshelfCover( name = book.name, @@ -434,7 +454,7 @@ fun BookItem( path = book.getDisplayCover(), isUpdating = isUpdating, 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, leftBottomText = matchedSourceLabel ?: bookTypeLabel ) @@ -445,19 +465,19 @@ fun BookItem( } else { book.author }, - desc = stringResource(R.string.read_dur_progress, book.durChapterTitle ?: ""), + desc = book.durChapterTitle ?: "", extra = { if (BookshelfConfig.showLastUpdateTime && !book.isLocal) { AppText( 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) ) } AppText( text = book.latestChapterTitle ?: "", - style = LegadoTheme.typography.bodySmall, + style = LegadoTheme.typography.labelSmallEmphasized.copy(color = LegadoTheme.colorScheme.outline), maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f) diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigSheet.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigSheet.kt index 6fab6b72d..2b864e1ee 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigSheet.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigSheet.kt @@ -191,6 +191,13 @@ fun BookshelfConfigSheet( onCheckedChange = { BookshelfConfig.bookshelfCoverShadow = it } ) + CompactSwitchSettingItem( + title = "搜索按钮优先打开筛选栏", + checked = BookshelfConfig.bookshelfSearchActionDirectToSearch, + color = MaterialTheme.colorScheme.surface, + onCheckedChange = { BookshelfConfig.bookshelfSearchActionDirectToSearch = it } + ) + // Switches CompactSwitchSettingItem( title = stringResource(R.string.show_unread), diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt index 8d624f92d..b193dbc4e 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt +++ b/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.items 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.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.FormatListBulleted import androidx.compose.material.icons.filled.Bookmarks 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.Edit 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.Save import androidx.compose.material.icons.filled.SelectAll +import androidx.compose.material.icons.filled.UploadFile import androidx.compose.material.icons.filled.Wifi import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api @@ -61,10 +64,12 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp 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.ThemeResolver 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.adaptiveHorizontalPaddingTab 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.tabRow.AppTabRow 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.toastOnUi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel +import sh.calvin.reorderable.ReorderableItem +import sh.calvin.reorderable.rememberReorderableLazyGridState @OptIn( ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, @@ -315,10 +324,6 @@ fun BookshelfScreen( if (isLandscape) BookshelfConfig.bookshelfLayoutGridLandscape else BookshelfConfig.bookshelfLayoutGridPortrait val bookshelfLayoutList = 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 editStickySummary = if (isEditMode) { BookshelfEditStickySummary( @@ -337,9 +342,13 @@ fun BookshelfScreen( state = uiState, showSearchAction = true, 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) }, @@ -441,14 +450,14 @@ fun BookshelfScreen( leadingIcon = { Icon(Icons.Default.Edit, null) } ) RoundDropdownMenuItem( - text = stringResource(R.string.cache_export), + text = stringResource(R.string.bookshelf_management), onClick = { val groupId = uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L onNavigateToCache(groupId) dismiss() }, - leadingIcon = { Icon(Icons.Default.Download, null) } + leadingIcon = { Icon(Icons.Default.Bookmarks, null) } ) RoundDropdownMenuItem( text = stringResource(R.string.export_bookshelf), @@ -456,11 +465,12 @@ fun BookshelfScreen( showExportSheet = true dismiss() }, - leadingIcon = { Icon(Icons.Default.ImportExport, null) } + leadingIcon = { Icon(Icons.Default.UploadFile, null) } ) RoundDropdownMenuItem( text = stringResource(R.string.import_bookshelf), - onClick = { showImportSheet = true; dismiss() } + onClick = { showImportSheet = true; dismiss() }, + leadingIcon = { Icon(Icons.Default.CloudDownload, null) } ) RoundDropdownMenuItem( text = stringResource(R.string.log), @@ -633,12 +643,11 @@ fun BookshelfScreen( FastScrollLazyVerticalGrid( columns = GridCells.Fixed(folderColumns.coerceAtLeast(1)), modifier = Modifier - .fillMaxSize() - .padding(horizontal = if (isGridMode) gridInnerHorizontalPadding else 0.dp), - contentPadding = adaptiveContentPadding( + .fillMaxSize(), + contentPadding = adaptiveContentPaddingBookshelf( top = paddingValues.calculateTopPadding(), 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), horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp), @@ -701,7 +710,9 @@ fun BookshelfScreen( bookshelfLayoutList = bookshelfLayoutList, isEditMode = isEditMode, selectedBookUrls = selectedBookUrls, + canReorderBooks = false, onToggleBookSelection = { toggleBookSelection(it.bookUrl) }, + onSaveBookOrder = {}, onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) }, onBookClick = onBookClick, onBookLongClick = onBookLongClick @@ -725,15 +736,21 @@ fun BookshelfScreen( uiState = uiState, bookshelfLayoutMode = bookshelfLayoutMode, 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, isEditMode: Boolean, selectedBookUrls: Set, + canReorderBooks: Boolean, onToggleBookSelection: (BookShelfItem) -> Unit, + onSaveBookOrder: (books: List) -> Unit, onGlobalSearch: () -> Unit, onBookClick: (BookShelfItem) -> Unit, onBookLongClick: (BookShelfItem) -> Unit @@ -998,51 +1017,112 @@ fun BookshelfPage( if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) 12.dp else 16.dp val gridContentHorizontalPadding = totalHorizontalPadding / 2 val gridInnerHorizontalPadding = totalHorizontalPadding / 2 + val hapticFeedback = LocalHapticFeedback.current + var draggingBooks by remember { mutableStateOf?>(null) } + var pendingSavedBooks by remember { mutableStateOf?>(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( columns = GridCells.Fixed(columns.coerceAtLeast(1)), + state = gridState, modifier = Modifier - .fillMaxSize() - .padding(horizontal = if (isGridMode) gridInnerHorizontalPadding else 0.dp), - contentPadding = adaptiveContentPadding( + .fillMaxSize(), + contentPadding = adaptiveContentPaddingBookshelf( top = paddingValues.calculateTopPadding(), 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), horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp), showFastScroll = BookshelfConfig.showBookshelfFastScroller ) { - items(books, key = { it.bookUrl }) { book -> + items(displayBooks, key = { it.bookUrl }) { book -> 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) + 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 { - onBookClick(book) + { + if (isEditMode) { + onToggleBookSelection(book) + } else { + onBookLongClick(book) + } + } } - }, - onLongClick = { - if (isEditMode) { - onToggleBookSelection(book) - } else { - onBookLongClick(book) - } - } - ) + ) + } } } } diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt index e45b9184e..236435142 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt @@ -392,6 +392,24 @@ class BookshelfViewModel( } } + fun saveBookOrder(reorderedBooks: List) { + 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, downloadAllChapters: Boolean = false) { if (bookUrls.isEmpty()) return execute { diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssSortRouteScreen.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssSortRouteScreen.kt index 35ced7b12..4ec799923 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssSortRouteScreen.kt +++ b/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 redirectPolicy by remember(sourceUrl) { mutableStateOf(RedirectPolicy.ALLOW_ALL) } 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 readRecords by remember { mutableStateOf>(emptyList()) } @@ -104,7 +106,7 @@ fun RssSortRouteScreen( val variable = withContext(Dispatchers.IO) { source.getVariable() } activity?.showDialogFragment( VariableDialog( - context.getString(R.string.set_source_variable), + setSourceVariableText, source.getKey(), variable, comment @@ -141,7 +143,7 @@ fun RssSortRouteScreen( viewModel.rssSource?.sourceUrl ?: sourceUrl.orEmpty() } if (openOrigin.isBlank()) { - context.toastOnUi(context.getString(R.string.error)) + context.toastOnUi(errorText) } else { onOpenRead(record.title, openOrigin, null, record.record) } diff --git a/app/src/main/java/io/legado/app/ui/theme/AdaptivePadding.kt b/app/src/main/java/io/legado/app/ui/theme/AdaptivePadding.kt index e5653a135..3b4bfe99e 100644 --- a/app/src/main/java/io/legado/app/ui/theme/AdaptivePadding.kt +++ b/app/src/main/java/io/legado/app/ui/theme/AdaptivePadding.kt @@ -78,3 +78,19 @@ fun adaptiveContentPadding( 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 + ) +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/button/TopBarButton.kt b/app/src/main/java/io/legado/app/ui/widget/components/button/TopBarButton.kt index 805b213ee..decd71847 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/button/TopBarButton.kt +++ b/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 fun TopBarActionButton( onClick: () -> Unit, diff --git a/app/src/main/java/io/legado/app/ui/widget/components/lazylist/VerticalFastScroller.kt b/app/src/main/java/io/legado/app/ui/widget/components/lazylist/VerticalFastScroller.kt index c26987158..d56d2efc8 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/lazylist/VerticalFastScroller.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/lazylist/VerticalFastScroller.kt @@ -279,7 +279,7 @@ fun VerticalGridFastScroller( contentPadding: PaddingValues, modifier: Modifier = Modifier, thumbAllowed: () -> Boolean = { true }, - thumbColor: Color = MaterialTheme.colorScheme.primary, + thumbColor: Color = LegadoTheme.colorScheme.primary, topContentPadding: Dp = Dp.Hairline, bottomContentPadding: Dp = Dp.Hairline, endContentPadding: Dp = Dp.Hairline,