[优化] 合并下载和原本的书架管理界面
This commit is contained in:
@@ -17,7 +17,11 @@ import io.legado.app.data.repository.SearchRepository
|
||||
import io.legado.app.data.repository.SearchRepositoryImpl
|
||||
import io.legado.app.data.repository.SearchContentRepository
|
||||
import io.legado.app.data.repository.UploadRepository
|
||||
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
|
||||
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
||||
import io.legado.app.domain.usecase.DeleteBooksUseCase
|
||||
import io.legado.app.domain.usecase.ExploreKindUiUseCase
|
||||
import io.legado.app.domain.usecase.UpdateBooksGroupUseCase
|
||||
import io.legado.app.help.coil.CoverFetcher
|
||||
import io.legado.app.help.coil.CoverInterceptor
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
@@ -67,6 +71,7 @@ val appModule = module {
|
||||
single { get<AppDatabase>().bookDao }
|
||||
single { get<AppDatabase>().bookChapterDao }
|
||||
single { get<AppDatabase>().bookGroupDao }
|
||||
single { get<AppDatabase>().bookSourceDao }
|
||||
|
||||
singleOf(::ReadRecordRepository)
|
||||
singleOf(::BookRepository)
|
||||
@@ -74,6 +79,10 @@ val appModule = module {
|
||||
singleOf(::SearchContentRepository)
|
||||
singleOf(::RemoteBookRepository)
|
||||
singleOf(::ExploreKindUiUseCase)
|
||||
singleOf(::BatchCacheDownloadUseCase)
|
||||
singleOf(::ClearBookCacheUseCase)
|
||||
singleOf(::DeleteBooksUseCase)
|
||||
singleOf(::UpdateBooksGroupUseCase)
|
||||
singleOf(::CacheConfig)
|
||||
|
||||
single<UploadRepository> { DirectLinkUploadRepository() }
|
||||
@@ -125,7 +134,19 @@ val appModule = module {
|
||||
viewModelOf(::ExploreViewModel)
|
||||
viewModelOf(::RssViewModel)
|
||||
viewModelOf(::SearchViewModel)
|
||||
viewModelOf(::CacheViewModel)
|
||||
viewModel {
|
||||
CacheViewModel(
|
||||
application = get(),
|
||||
bookDao = get(),
|
||||
bookGroupDao = get(),
|
||||
bookChapterDao = get(),
|
||||
cacheConfig = get(),
|
||||
batchCacheDownloadUseCase = get(),
|
||||
clearBookCacheUseCase = get(),
|
||||
deleteBooksUseCase = get(),
|
||||
updateBooksGroupUseCase = get()
|
||||
)
|
||||
}
|
||||
|
||||
viewModel { (route: ReplaceEditRoute) ->
|
||||
ReplaceEditViewModel(
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package io.legado.app.domain.usecase
|
||||
|
||||
import android.content.Context
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.data.dao.BookDao
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.model.CacheBook
|
||||
|
||||
class BatchCacheDownloadUseCase(
|
||||
private val bookDao: BookDao
|
||||
) {
|
||||
|
||||
suspend fun execute(
|
||||
context: Context,
|
||||
bookUrls: Set<String>,
|
||||
downloadAllChapters: Boolean,
|
||||
skipAudioBooks: Boolean = false
|
||||
): Int {
|
||||
if (bookUrls.isEmpty()) return 0
|
||||
var count = 0
|
||||
bookUrls.forEach { bookUrl ->
|
||||
val book = bookDao.getBook(bookUrl) ?: return@forEach
|
||||
if (startIfNeeded(context, book, downloadAllChapters, skipAudioBooks)) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
fun execute(
|
||||
context: Context,
|
||||
books: List<Book>,
|
||||
downloadAllChapters: Boolean,
|
||||
skipAudioBooks: Boolean = false
|
||||
): Int {
|
||||
if (books.isEmpty()) return 0
|
||||
var count = 0
|
||||
books.forEach { book ->
|
||||
if (startIfNeeded(context, book, downloadAllChapters, skipAudioBooks)) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
private fun startIfNeeded(
|
||||
context: Context,
|
||||
book: Book,
|
||||
downloadAllChapters: Boolean,
|
||||
skipAudioBooks: Boolean
|
||||
): Boolean {
|
||||
if (book.type and BookType.local > 0) return false
|
||||
if (skipAudioBooks && book.type and BookType.audio > 0) return false
|
||||
val indices = if (downloadAllChapters) {
|
||||
(0..book.lastChapterIndex).toList()
|
||||
} else {
|
||||
(book.durChapterIndex..book.lastChapterIndex).toList()
|
||||
}
|
||||
if (indices.isEmpty()) return false
|
||||
CacheBook.start(context, book, indices)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.legado.app.domain.usecase
|
||||
|
||||
import io.legado.app.data.dao.BookDao
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.help.book.BookHelp
|
||||
|
||||
class ClearBookCacheUseCase(
|
||||
private val bookDao: BookDao
|
||||
) {
|
||||
|
||||
fun execute(book: Book): String {
|
||||
BookHelp.clearCache(book)
|
||||
return book.bookUrl
|
||||
}
|
||||
|
||||
suspend fun execute(bookUrls: Set<String>): List<String> {
|
||||
if (bookUrls.isEmpty()) return emptyList()
|
||||
return bookUrls.mapNotNull { bookUrl ->
|
||||
bookDao.getBook(bookUrl)?.let { execute(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package io.legado.app.domain.usecase
|
||||
|
||||
import io.legado.app.data.dao.BookChapterDao
|
||||
import io.legado.app.data.dao.BookDao
|
||||
import io.legado.app.data.dao.BookSourceDao
|
||||
import io.legado.app.help.book.isLocal
|
||||
import io.legado.app.model.SourceCallBack
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
|
||||
class DeleteBooksUseCase(
|
||||
private val bookDao: BookDao,
|
||||
private val bookChapterDao: BookChapterDao,
|
||||
private val bookSourceDao: BookSourceDao
|
||||
) {
|
||||
|
||||
suspend fun execute(bookUrls: Set<String>, deleteOriginal: Boolean): List<String> {
|
||||
if (bookUrls.isEmpty()) return emptyList()
|
||||
val books = bookUrls.mapNotNull { bookDao.getBook(it) }
|
||||
books.forEach { book ->
|
||||
if (book.isLocal) {
|
||||
LocalBook.deleteBook(book, deleteOriginal)
|
||||
} else {
|
||||
val source = bookSourceDao.getBookSource(book.origin)
|
||||
SourceCallBack.callBackBook(SourceCallBack.DEL_BOOK_SHELF, source, book)
|
||||
}
|
||||
bookChapterDao.delByBook(book.bookUrl)
|
||||
}
|
||||
if (books.isNotEmpty()) {
|
||||
bookDao.delete(*books.toTypedArray())
|
||||
}
|
||||
return books.map { it.bookUrl }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package io.legado.app.domain.usecase
|
||||
|
||||
import io.legado.app.data.dao.BookDao
|
||||
|
||||
class UpdateBooksGroupUseCase(
|
||||
private val bookDao: BookDao
|
||||
) {
|
||||
|
||||
suspend fun replaceGroup(bookUrls: Set<String>, groupId: Long) {
|
||||
updateGroups(bookUrls) { groupId }
|
||||
}
|
||||
|
||||
suspend fun updateGroups(bookUrls: Set<String>, transform: (Long) -> Long) {
|
||||
if (bookUrls.isEmpty()) return
|
||||
val updateBooks = bookUrls.mapNotNull { bookUrl ->
|
||||
bookDao.getBook(bookUrl)?.let { book ->
|
||||
val targetGroup = transform(book.group)
|
||||
if (targetGroup == book.group) {
|
||||
null
|
||||
} else {
|
||||
book.copy(group = targetGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updateBooks.isNotEmpty()) {
|
||||
bookDao.update(*updateBooks.toTypedArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
+563
-258
File diff suppressed because it is too large
Load Diff
+87
-12
@@ -9,6 +9,10 @@ import io.legado.app.data.dao.BookGroupDao
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.BookGroup
|
||||
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
|
||||
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
||||
import io.legado.app.domain.usecase.DeleteBooksUseCase
|
||||
import io.legado.app.domain.usecase.UpdateBooksGroupUseCase
|
||||
import io.legado.app.help.book.BookHelp
|
||||
import io.legado.app.help.book.isAudio
|
||||
import io.legado.app.help.book.isLocal
|
||||
@@ -61,6 +65,10 @@ sealed interface 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
|
||||
data class SetExportUseReplace(val enabled: Boolean) : CacheIntent
|
||||
data class SetEnableCustomExport(val enabled: Boolean) : CacheIntent
|
||||
data class SetExportNoChapterName(val enabled: Boolean) : CacheIntent
|
||||
@@ -83,7 +91,11 @@ class CacheViewModel(
|
||||
private val bookDao: BookDao,
|
||||
private val bookGroupDao: BookGroupDao,
|
||||
private val bookChapterDao: BookChapterDao,
|
||||
val cacheConfig: CacheConfig
|
||||
val cacheConfig: CacheConfig,
|
||||
private val batchCacheDownloadUseCase: BatchCacheDownloadUseCase,
|
||||
private val clearBookCacheUseCase: ClearBookCacheUseCase,
|
||||
private val deleteBooksUseCase: DeleteBooksUseCase,
|
||||
private val updateBooksGroupUseCase: UpdateBooksGroupUseCase
|
||||
) : BaseViewModel(application) {
|
||||
|
||||
private val _uiState = MutableStateFlow(CacheUiState())
|
||||
@@ -110,6 +122,10 @@ class CacheViewModel(
|
||||
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.SetExportUseReplace -> {
|
||||
cacheConfig.exportUseReplace = intent.enabled
|
||||
syncExportConfig()
|
||||
@@ -317,15 +333,16 @@ class CacheViewModel(
|
||||
}
|
||||
|
||||
private fun startDownloadForVisibleBooks(books: List<Book>, downloadAllChapters: Boolean) {
|
||||
books.forEach { book ->
|
||||
val indices = if (downloadAllChapters) {
|
||||
(0..book.lastChapterIndex).toList()
|
||||
} else {
|
||||
(book.durChapterIndex..book.lastChapterIndex).toList()
|
||||
}
|
||||
CacheBook.start(context, book, indices)
|
||||
execute {
|
||||
batchCacheDownloadUseCase.execute(
|
||||
context = context,
|
||||
books = books,
|
||||
downloadAllChapters = downloadAllChapters,
|
||||
skipAudioBooks = true
|
||||
)
|
||||
}.onFinally {
|
||||
syncDownloadRunning()
|
||||
}
|
||||
syncDownloadRunning()
|
||||
}
|
||||
|
||||
private fun toggleBookDownload(book: Book) {
|
||||
@@ -338,10 +355,68 @@ class CacheViewModel(
|
||||
syncDownloadRunning()
|
||||
}
|
||||
|
||||
private fun moveBooksToGroup(bookUrls: Set<String>, groupId: Long) {
|
||||
if (bookUrls.isEmpty()) return
|
||||
val safeGroupId = groupId.coerceAtLeast(0L)
|
||||
execute {
|
||||
updateBooksGroupUseCase.replaceGroup(bookUrls, safeGroupId)
|
||||
}.onError {
|
||||
_effects.tryEmit(CacheEffect.ShowMessage("移动分组失败\n${it.localizedMessage}"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteBooks(bookUrls: Set<String>, deleteOriginal: Boolean) {
|
||||
if (bookUrls.isEmpty()) return
|
||||
execute {
|
||||
deleteBooksUseCase.execute(bookUrls, deleteOriginal)
|
||||
}.onSuccess { deletedBookUrls ->
|
||||
deletedBookUrls.forEach { cacheChapters.remove(it) }
|
||||
_effects.tryEmit(CacheEffect.ShowMessage("删除成功"))
|
||||
}.onError {
|
||||
_effects.tryEmit(CacheEffect.ShowMessage("删除失败\n${it.localizedMessage}"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearCachesForBooks(bookUrls: Set<String>) {
|
||||
if (bookUrls.isEmpty()) return
|
||||
execute {
|
||||
clearBookCacheUseCase.execute(bookUrls)
|
||||
}.onSuccess { clearedBookUrls ->
|
||||
clearedBookUrls.forEach { bookUrl ->
|
||||
cacheChapters[bookUrl] = hashSetOf()
|
||||
emitBookChanged(bookUrl)
|
||||
}
|
||||
_effects.tryEmit(CacheEffect.ShowMessage("缓存已清理"))
|
||||
}.onError {
|
||||
_effects.tryEmit(CacheEffect.ShowMessage("清理缓存失败\n${it.localizedMessage}"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadBooks(bookUrls: Set<String>, downloadAllChapters: Boolean) {
|
||||
if (bookUrls.isEmpty()) return
|
||||
execute {
|
||||
batchCacheDownloadUseCase.execute(
|
||||
context = context,
|
||||
bookUrls = bookUrls,
|
||||
downloadAllChapters = downloadAllChapters,
|
||||
skipAudioBooks = true
|
||||
)
|
||||
}.onSuccess { count ->
|
||||
if (count > 0) {
|
||||
_effects.tryEmit(CacheEffect.ShowMessage("已加入缓存队列: $count 本"))
|
||||
} else {
|
||||
_effects.tryEmit(CacheEffect.ShowMessage("没有可缓存的书籍"))
|
||||
}
|
||||
syncDownloadRunning()
|
||||
}.onError {
|
||||
_effects.tryEmit(CacheEffect.ShowMessage("批量缓存失败\n${it.localizedMessage}"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearCacheForBook(book: Book) {
|
||||
BookHelp.clearCache(book)
|
||||
cacheChapters[book.bookUrl] = hashSetOf()
|
||||
emitBookChanged(book.bookUrl)
|
||||
val bookUrl = clearBookCacheUseCase.execute(book)
|
||||
cacheChapters[bookUrl] = hashSetOf()
|
||||
emitBookChanged(bookUrl)
|
||||
}
|
||||
|
||||
private fun emitBookChanged(bookUrl: String) {
|
||||
|
||||
@@ -6,16 +6,20 @@ import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
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
|
||||
@@ -79,13 +83,16 @@ import io.legado.app.ui.theme.adaptiveHorizontalPaddingTab
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton
|
||||
import io.legado.app.ui.widget.components.button.TopBarActionButton
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.card.NormalCard
|
||||
import io.legado.app.ui.widget.components.card.TextCard
|
||||
import io.legado.app.ui.widget.components.divider.PillHeaderDivider
|
||||
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
||||
import io.legado.app.ui.widget.components.icon.AppIcons
|
||||
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
|
||||
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyVerticalGrid
|
||||
import io.legado.app.ui.widget.components.list.ListScaffold
|
||||
import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
|
||||
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
|
||||
@@ -124,6 +131,7 @@ fun BookshelfScreen(
|
||||
var showLogSheet by remember { mutableStateOf(false) }
|
||||
var showGroupMenu by remember { mutableStateOf(false) }
|
||||
var showGroupSelectSheet by remember { mutableStateOf(false) }
|
||||
var showBatchDownloadConfirmDialog by remember { mutableStateOf(false) }
|
||||
var isEditMode by remember { mutableStateOf(false) }
|
||||
var selectedBookUrls by remember { mutableStateOf<Set<String>>(emptySet()) }
|
||||
|
||||
@@ -289,18 +297,14 @@ fun BookshelfScreen(
|
||||
else -> stringResource(R.string.bookshelf)
|
||||
}
|
||||
val title = if (isEditMode) {
|
||||
"$currentGroupBookCount · ${selectedBookUrls.size} 本"
|
||||
stringResource(R.string.bookshelf)
|
||||
} else if (uiState.upBooksCount > 0) {
|
||||
"$baseTitle (${uiState.upBooksCount})"
|
||||
} else {
|
||||
baseTitle
|
||||
}
|
||||
val subtitle = if (isEditMode) {
|
||||
if (bookGroupStyle == 0) {
|
||||
"共${allGroupsBookCount}本"
|
||||
} else {
|
||||
"${currentGroupName ?: stringResource(R.string.bookshelf)} · 共${allGroupsBookCount}本"
|
||||
}
|
||||
"共${allGroupsBookCount}本"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -323,13 +327,22 @@ fun BookshelfScreen(
|
||||
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(
|
||||
selectedCount = selectedBookUrls.size,
|
||||
currentGroupTotalCount = currentGroupBookCount,
|
||||
groupName = currentGroupName,
|
||||
showGroupName = bookGroupStyle != 0
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
ListScaffold(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
state = uiState,
|
||||
onBackClick = if (isEditMode) exitEditMode else null,
|
||||
backNavigationIcon = if (isEditMode) AppIcons.Close else AppIcons.Back,
|
||||
showSearchAction = true,
|
||||
onSearchToggle = { active ->
|
||||
viewModel.setSearchMode(active)
|
||||
@@ -353,7 +366,7 @@ fun BookshelfScreen(
|
||||
}
|
||||
},
|
||||
topBarActions = {
|
||||
if (isEditMode) {
|
||||
AnimatedVisibility(visible = isEditMode) {
|
||||
TopBarActionButton(
|
||||
onClick = {
|
||||
selectedBookUrls = uiState.items.mapTo(hashSetOf()) { it.bookUrl }
|
||||
@@ -361,6 +374,8 @@ fun BookshelfScreen(
|
||||
imageVector = Icons.Default.SelectAll,
|
||||
contentDescription = stringResource(R.string.select_all)
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = isEditMode) {
|
||||
TopBarActionButton(
|
||||
onClick = {
|
||||
val visibleBookUrls = uiState.items.mapTo(hashSetOf()) { it.bookUrl }
|
||||
@@ -369,6 +384,19 @@ fun BookshelfScreen(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
contentDescription = stringResource(R.string.revert_selection)
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = isEditMode) {
|
||||
TopBarActionButton(
|
||||
onClick = {
|
||||
if (selectedBookUrls.isNotEmpty()) {
|
||||
showBatchDownloadConfirmDialog = true
|
||||
}
|
||||
},
|
||||
imageVector = Icons.Default.Download,
|
||||
contentDescription = stringResource(R.string.action_download)
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = isEditMode) {
|
||||
TopBarActionButton(
|
||||
onClick = {
|
||||
if (selectedBookUrls.isNotEmpty()) {
|
||||
@@ -719,6 +747,97 @@ fun BookshelfScreen(
|
||||
}
|
||||
}
|
||||
|
||||
TopFloatingStickyItem(
|
||||
item = editStickySummary,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = paddingValues.calculateTopPadding() + 6.dp),
|
||||
) { summary ->
|
||||
Box {
|
||||
NormalCard(
|
||||
cornerRadius = 32.dp,
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainer,
|
||||
contentColor = LegadoTheme.colorScheme.onCardContainer
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(all = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextCard(
|
||||
icon = AppIcons.Close,
|
||||
backgroundColor = LegadoTheme.colorScheme.surfaceContainerHighest,
|
||||
cornerRadius = 16.dp,
|
||||
verticalPadding = 8.dp,
|
||||
horizontalPadding = 8.dp,
|
||||
onClick = exitEditMode
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
AppText(
|
||||
text = "已选${summary.selectedCount}本",
|
||||
style = LegadoTheme.typography.labelSmallEmphasized
|
||||
)
|
||||
AppText(
|
||||
text = " · 共${summary.currentGroupTotalCount}本",
|
||||
style = LegadoTheme.typography.labelSmallEmphasized
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
if (summary.showGroupName && !summary.groupName.isNullOrBlank()) {
|
||||
TextCard(
|
||||
text = summary.groupName,
|
||||
textStyle = LegadoTheme.typography.labelSmallEmphasized,
|
||||
backgroundColor = LegadoTheme.colorScheme.surfaceContainerHighest,
|
||||
cornerRadius = 16.dp,
|
||||
verticalPadding = 8.dp,
|
||||
horizontalPadding = 12.dp,
|
||||
onClick = { showGroupMenu = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (summary.showGroupName) {
|
||||
RoundDropdownMenu(
|
||||
expanded = showGroupMenu,
|
||||
onDismissRequest = { showGroupMenu = false }
|
||||
) { dismiss ->
|
||||
uiState.groups.forEach { group ->
|
||||
RoundDropdownMenuItem(
|
||||
text = group.groupName,
|
||||
onClick = {
|
||||
val targetIndex =
|
||||
uiState.groups.indexOfFirst { it.groupId == group.groupId }
|
||||
if (targetIndex >= 0) {
|
||||
scope.launch {
|
||||
if (pagerState.currentPage != targetIndex) {
|
||||
pagerState.animateScrollToPage(targetIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (uiState.isSearch || uiState.selectedGroupId != group.groupId) {
|
||||
viewModel.changeGroup(group.groupId)
|
||||
}
|
||||
if (bookGroupStyle == 2) {
|
||||
isInFolderRoot = false
|
||||
}
|
||||
dismiss()
|
||||
},
|
||||
trailingIcon = {
|
||||
if (currentMenuGroupId == group.groupId) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PullToRefreshDefaults.LoadingIndicator(
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = isRefreshing,
|
||||
@@ -794,6 +913,20 @@ fun BookshelfScreen(
|
||||
onDismissRequest = { showLogSheet = false }
|
||||
)
|
||||
|
||||
AppAlertDialog(
|
||||
show = showBatchDownloadConfirmDialog,
|
||||
onDismissRequest = { showBatchDownloadConfirmDialog = false },
|
||||
title = stringResource(R.string.draw),
|
||||
text = stringResource(R.string.sure_cache_book),
|
||||
confirmText = stringResource(android.R.string.ok),
|
||||
onConfirm = {
|
||||
showBatchDownloadConfirmDialog = false
|
||||
viewModel.downloadBooks(selectedBookUrls)
|
||||
},
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = { showBatchDownloadConfirmDialog = false }
|
||||
)
|
||||
|
||||
if (uiState.isLoading) {
|
||||
Dialog(onDismissRequest = {}) {
|
||||
NormalCard(
|
||||
@@ -818,6 +951,13 @@ fun BookshelfScreen(
|
||||
}
|
||||
}
|
||||
|
||||
private data class BookshelfEditStickySummary(
|
||||
val selectedCount: Int,
|
||||
val currentGroupTotalCount: Int,
|
||||
val groupName: String?,
|
||||
val showGroupName: Boolean,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun BookshelfPage(
|
||||
paddingValues: PaddingValues,
|
||||
|
||||
@@ -19,6 +19,8 @@ import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.data.entities.BookSourcePart
|
||||
import io.legado.app.data.repository.BookGroupRepository
|
||||
import io.legado.app.data.repository.UploadRepository
|
||||
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
|
||||
import io.legado.app.domain.usecase.UpdateBooksGroupUseCase
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.help.book.BookHelp
|
||||
import io.legado.app.help.book.addType
|
||||
@@ -91,7 +93,9 @@ import kotlin.math.min
|
||||
class BookshelfViewModel(
|
||||
application: Application,
|
||||
private val bookGroupRepository: BookGroupRepository,
|
||||
private val uploadRepository: UploadRepository
|
||||
private val uploadRepository: UploadRepository,
|
||||
private val batchCacheDownloadUseCase: BatchCacheDownloadUseCase,
|
||||
private val updateBooksGroupUseCase: UpdateBooksGroupUseCase
|
||||
) : BaseViewModel(application) {
|
||||
var addBookJob: Coroutine<*>? = null
|
||||
|
||||
@@ -377,33 +381,34 @@ class BookshelfViewModel(
|
||||
}
|
||||
|
||||
fun moveBooksToGroup(bookUrls: Set<String>, groupId: Long) {
|
||||
updateBooksGroup(bookUrls) { groupId }
|
||||
}
|
||||
|
||||
private fun updateBooksGroup(
|
||||
bookUrls: Set<String>,
|
||||
transform: (Long) -> Long
|
||||
) {
|
||||
if (bookUrls.isEmpty()) return
|
||||
execute {
|
||||
val updateBooks = bookUrls.mapNotNull { url ->
|
||||
appDb.bookDao.getBook(url)?.let { book ->
|
||||
val targetGroup = transform(book.group)
|
||||
if (targetGroup != book.group) {
|
||||
book.copy(group = targetGroup)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updateBooks.isNotEmpty()) {
|
||||
appDb.bookDao.update(*updateBooks.toTypedArray())
|
||||
}
|
||||
updateBooksGroupUseCase.replaceGroup(bookUrls, groupId)
|
||||
}.onError {
|
||||
context.toastOnUi("更新分组失败\n${it.localizedMessage}")
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadBooks(bookUrls: Set<String>, downloadAllChapters: Boolean = false) {
|
||||
if (bookUrls.isEmpty()) return
|
||||
execute {
|
||||
batchCacheDownloadUseCase.execute(
|
||||
context = context,
|
||||
bookUrls = bookUrls,
|
||||
downloadAllChapters = downloadAllChapters,
|
||||
skipAudioBooks = true
|
||||
)
|
||||
}.onSuccess { count ->
|
||||
if (count > 0) {
|
||||
context.toastOnUi("已加入缓存队列: $count 本")
|
||||
} else {
|
||||
context.toastOnUi(R.string.no_download)
|
||||
}
|
||||
}.onError {
|
||||
context.toastOnUi("批量缓存失败\n${it.localizedMessage}")
|
||||
}
|
||||
}
|
||||
|
||||
fun gotoTop() {
|
||||
scrollTrigger.tryEmit(Unit)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import io.legado.app.ui.widget.components.text.AnimatedTextLine
|
||||
@Composable
|
||||
fun TextCard(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
text: String? = null,
|
||||
icon: ImageVector? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
backgroundColor: Color? = null,
|
||||
@@ -65,16 +65,20 @@ fun TextCard(
|
||||
tint = finalContentColor,
|
||||
modifier = Modifier.size(iconSize)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(spacing))
|
||||
}
|
||||
|
||||
AnimatedTextLine(
|
||||
text = text,
|
||||
style = textStyle,
|
||||
color = finalContentColor,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (icon != null && text != null)
|
||||
Spacer(modifier = Modifier.width(spacing))
|
||||
|
||||
text?.let {
|
||||
AnimatedTextLine(
|
||||
text = it,
|
||||
style = textStyle,
|
||||
color = finalContentColor,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user