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

This commit is contained in:
HapeLee
2026-04-26 15:13:50 +08:00
parent 2989fe0405
commit 7e27686fbf
16 changed files with 644 additions and 425 deletions
@@ -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"
@@ -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<UploadRepository> { DirectLinkUploadRepository() }
single<AppStartupGateway> { 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(),
@@ -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
}, {
@@ -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<Book> = emptyList(),
override val selectedIds: Set<Any> = emptySet(),
override val searchKey: String = "",
@@ -137,21 +138,21 @@ private data class CacheListState(
) : ListUiState<Book>
@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<BookInfoActivity> {
is BookshelfManageScreenEffect.ShowMessage -> context.toastOnUi(effect.message)
is BookshelfManageScreenEffect.NotifyBookChanged -> Unit
is BookshelfManageScreenEffect.OpenBookInfo -> context.startActivity<BookInfoActivity> {
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<ExportBookService> {
@@ -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<BookGroup> = emptyList(),
val books: List<Book> = 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<Book>,
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(
val oldBookUrl: String,
val source: BookSource,
val book: Book,
val chapters: List<BookChapter>,
val options: ChangeSourceMigrationOptions,
) : CacheIntent
) : BookshelfManageScreenIntent
data class BatchChangeBookSource(
val bookUrls: Set<String>,
val sources: List<BookSource>,
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<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,
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<CacheEffect>(extraBufferCapacity = 32)
private val _effects = MutableSharedFlow<BookshelfManageScreenEffect>(extraBufferCapacity = 32)
val effects = _effects.asSharedFlow()
private val cacheChapters = hashMapOf<String, HashSet<String>>()
@@ -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))
}
}
@@ -138,6 +138,14 @@ object BookshelfConfig {
*/
var bookshelfCoverShadow by prefDelegate(PreferKey.bookshelfCoverShadow, false)
/**
* 书架搜索按钮是否直接跳转搜索页
*/
var bookshelfSearchActionDirectToSearch by prefDelegate(
PreferKey.bookshelfSearchActionDirectToSearch,
true
)
/**
* 启动时自动刷新书架
*/
@@ -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
}
@@ -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<MainRouteCache> { route ->
CacheRouteScreen(
BookshelfManageRouteScreen(
groupId = route.groupId,
onBackClick = { navigateBack(backStack) }
)
@@ -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)
@@ -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),
@@ -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<String>,
canReorderBooks: Boolean,
onToggleBookSelection: (BookShelfItem) -> Unit,
onSaveBookOrder: (books: List<BookShelfItem>) -> 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<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(
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)
}
}
)
)
}
}
}
}
@@ -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) {
if (bookUrls.isEmpty()) return
execute {
@@ -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<List<RssReadRecord>>(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)
}
@@ -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
)
}
@@ -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,
@@ -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,