[优化] 合并下载和原本的书架管理界面
This commit is contained in:
10 files changed
+858
-161
No files matched your search
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
+421
-116
@@ -1,36 +1,58 @@
|
||||
package io.legado.app.ui.book.cache
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.MenuOpen
|
||||
import androidx.compose.material.icons.filled.Bookmarks
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.SelectAll
|
||||
import androidx.compose.material.icons.filled.Stop
|
||||
import androidx.compose.material.icons.filled.Upload
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.FloatingActionButtonMenu
|
||||
import androidx.compose.material3.FloatingActionButtonMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ToggleFloatingActionButton
|
||||
import androidx.compose.material3.ToggleFloatingActionButtonDefaults.animateIcon
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import io.legado.app.R
|
||||
@@ -39,31 +61,27 @@ import io.legado.app.data.entities.Book
|
||||
import io.legado.app.help.book.getExportFileName
|
||||
import io.legado.app.help.book.isLocal
|
||||
import io.legado.app.help.book.tryParesExportFileName
|
||||
import io.legado.app.lib.dialogs.SelectItem
|
||||
import io.legado.app.service.ExportBookService
|
||||
import io.legado.app.ui.about.AppLogSheet
|
||||
import io.legado.app.ui.file.HandleFileContract
|
||||
import io.legado.app.ui.book.info.GroupSelectSheet
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.AppLinearProgressIndicator
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.AppFloatingActionButton
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.button.SmallTonalIconButton
|
||||
import io.legado.app.ui.widget.components.button.TopBarActionButton
|
||||
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
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.PillDivider
|
||||
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
||||
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.icon.AppIcons
|
||||
import io.legado.app.ui.widget.components.list.ListScaffold
|
||||
import io.legado.app.ui.widget.components.list.ListUiState
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.OptionCard
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.OptionSheet
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
||||
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
||||
import io.legado.app.utils.ACache
|
||||
import io.legado.app.utils.FileDoc
|
||||
import io.legado.app.utils.checkWrite
|
||||
@@ -71,8 +89,24 @@ import io.legado.app.utils.isContentScheme
|
||||
import io.legado.app.utils.startService
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import io.legado.app.utils.verificationField
|
||||
import io.legado.app.help.config.LocalConfig
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
data class CacheFabAction(
|
||||
val icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
val label: String,
|
||||
val action: () -> Unit
|
||||
)
|
||||
|
||||
private data class CacheListState(
|
||||
override val items: List<Book> = emptyList(),
|
||||
override val selectedIds: Set<Any> = emptySet(),
|
||||
override val searchKey: String = "",
|
||||
override val isSearch: Boolean = false,
|
||||
override val isLoading: Boolean = false
|
||||
) : ListUiState<Book>
|
||||
|
||||
@Composable
|
||||
fun CacheRouteScreen(
|
||||
groupId: Long,
|
||||
@@ -85,6 +119,7 @@ fun CacheRouteScreen(
|
||||
CacheScreen(viewModel = viewModel, onBackClick = onBackClick)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
private fun CacheScreen(
|
||||
viewModel: CacheViewModel,
|
||||
@@ -92,18 +127,25 @@ private fun CacheScreen(
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
var showGroupMenu by remember { mutableStateOf(false) }
|
||||
var showFilePickerSheet by remember { mutableStateOf(false) }
|
||||
var showDownloadConfirmDialog by remember { mutableStateOf(false) }
|
||||
var showDownloadAllConfirmDialog by remember { mutableStateOf(false) }
|
||||
var showBatchDownloadConfirmDialog by remember { mutableStateOf(false) }
|
||||
var showExportTypeDialog by remember { mutableStateOf(false) }
|
||||
var showExportFileNameDialog by remember { mutableStateOf(false) }
|
||||
var showCharsetDialog by remember { mutableStateOf(false) }
|
||||
var showLogSheet by remember { mutableStateOf(false) }
|
||||
var showGroupSelectSheet by remember { mutableStateOf(false) }
|
||||
var showDeleteBookConfirmDialog by remember { mutableStateOf(false) }
|
||||
var showCustomExportDialog by remember { mutableStateOf(false) }
|
||||
var pendingMoveGroupBookUrl by remember { mutableStateOf<String?>(null) }
|
||||
var groupPickerCurrentGroupId by remember { mutableLongStateOf(0L) }
|
||||
var moreMenuBookUrl by remember { mutableStateOf<String?>(null) }
|
||||
var pendingDeleteBookUrls by remember { mutableStateOf<Set<String>>(emptySet()) }
|
||||
var fabMenuExpanded by rememberSaveable { mutableStateOf(false) }
|
||||
var pendingExportBookUrl by remember { mutableStateOf<String?>(null) }
|
||||
var pendingExportAll by remember { mutableStateOf(false) }
|
||||
var pendingExportSelection by remember { mutableStateOf<Set<String>>(emptySet()) }
|
||||
var customExportPath by remember { mutableStateOf("") }
|
||||
var customExportBook by remember { mutableStateOf<Book?>(null) }
|
||||
var customExportAllChapter by remember { mutableStateOf(false) }
|
||||
@@ -113,9 +155,13 @@ private fun CacheScreen(
|
||||
var customEpisodeExportNameInput by remember { mutableStateOf(state.exportConfig.episodeExportFileName) }
|
||||
var exportFileNameInput by remember { mutableStateOf(state.exportConfig.bookExportFileName.orEmpty()) }
|
||||
var exportCharsetInput by remember { mutableStateOf(state.exportConfig.exportCharset) }
|
||||
var isSearchMode by remember { mutableStateOf(false) }
|
||||
var searchKey by remember { mutableStateOf("") }
|
||||
var selectedBookUrls by remember { mutableStateOf<Set<String>>(emptySet()) }
|
||||
var deleteOriginalBookFile by remember { mutableStateOf(LocalConfig.deleteBookOriginal) }
|
||||
val exportBookPathKey = remember { "exportBookPath" }
|
||||
val exportTypes = remember { arrayListOf("txt", "epub") }
|
||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val exportFolderText = stringResource(R.string.export_folder)
|
||||
val exportAllText = stringResource(R.string.export_all)
|
||||
val exportChapterIndexText = stringResource(R.string.export_chapter_index)
|
||||
@@ -135,11 +181,72 @@ private fun CacheScreen(
|
||||
书名:《{name}》-作者:{author}_备注
|
||||
输出:书名:《三体》-作者:刘慈欣_备注
|
||||
""".trimIndent()
|
||||
val booksByUrl = remember(state.books) { state.books.associateBy { it.bookUrl } }
|
||||
val userGroups = remember(state.groupList) { state.groupList.filter { it.groupId > 0L } }
|
||||
|
||||
val exportDir = rememberLauncherForActivityResult(HandleFileContract()) { result ->
|
||||
val groupNameResolver: (Book) -> String = remember(userGroups, context) {
|
||||
{ book ->
|
||||
if (book.group <= 0L) {
|
||||
context.getString(R.string.no_group)
|
||||
} else {
|
||||
val groups = userGroups.filter {
|
||||
(book.group and it.groupId) > 0L
|
||||
}
|
||||
if (groups.isEmpty()) context.getString(R.string.no_group)
|
||||
else groups.joinToString("、") { it.groupName }
|
||||
}
|
||||
}
|
||||
}
|
||||
val filteredBooks = remember(state.books, searchKey, isSearchMode, groupNameResolver) {
|
||||
if (!isSearchMode || searchKey.isBlank()) {
|
||||
state.books
|
||||
} else {
|
||||
val key = searchKey.trim()
|
||||
state.books.filter { book ->
|
||||
book.name.contains(key, true) ||
|
||||
book.getRealAuthor().contains(key, true) ||
|
||||
book.originName.contains(key, true) ||
|
||||
groupNameResolver(book).contains(key, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
val listUiState = remember(filteredBooks, selectedBookUrls, searchKey, isSearchMode) {
|
||||
CacheListState(
|
||||
items = filteredBooks,
|
||||
selectedIds = selectedBookUrls.mapTo(linkedSetOf()) { it as Any },
|
||||
searchKey = searchKey,
|
||||
isSearch = isSearchMode,
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
val inSelectionMode = selectedBookUrls.isNotEmpty()
|
||||
val hasLocalBookInDeleteTarget = remember(state.books, pendingDeleteBookUrls) {
|
||||
state.books.any { pendingDeleteBookUrls.contains(it.bookUrl) && it.isLocal }
|
||||
}
|
||||
val clearSelection = {
|
||||
selectedBookUrls = emptySet()
|
||||
}
|
||||
val toggleBookSelection: (Book) -> Unit = { book ->
|
||||
selectedBookUrls = if (selectedBookUrls.contains(book.bookUrl)) {
|
||||
selectedBookUrls - book.bookUrl
|
||||
} else {
|
||||
selectedBookUrls + book.bookUrl
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(enabled = selectedBookUrls.isNotEmpty()) {
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
LaunchedEffect(state.books) {
|
||||
val visibleBookUrls = booksByUrl.keys
|
||||
selectedBookUrls = selectedBookUrls.intersect(visibleBookUrls)
|
||||
}
|
||||
|
||||
val exportDir = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
|
||||
var isReadyPath = false
|
||||
var dirPath = ""
|
||||
result.uri?.let { uri ->
|
||||
uri?.let {
|
||||
if (uri.isContentScheme()) {
|
||||
ACache.get().put(exportBookPathKey, uri.toString())
|
||||
dirPath = uri.toString()
|
||||
@@ -153,6 +260,14 @@ private fun CacheScreen(
|
||||
}
|
||||
}
|
||||
if (!isReadyPath) return@rememberLauncherForActivityResult
|
||||
if (pendingExportSelection.isNotEmpty()) {
|
||||
pendingExportSelection.forEach { bookUrl ->
|
||||
booksByUrl[bookUrl]?.let { book ->
|
||||
startExport(context, dirPath, book, state.exportConfig.exportType)
|
||||
}
|
||||
}
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
if (pendingExportAll) {
|
||||
state.books.forEach { book ->
|
||||
startExport(context, dirPath, book, state.exportConfig.exportType)
|
||||
@@ -160,7 +275,7 @@ private fun CacheScreen(
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
val bookUrl = pendingExportBookUrl ?: return@rememberLauncherForActivityResult
|
||||
val book = state.books.firstOrNull { it.bookUrl == bookUrl } ?: return@rememberLauncherForActivityResult
|
||||
val book = booksByUrl[bookUrl] ?: return@rememberLauncherForActivityResult
|
||||
if (state.exportConfig.enableCustomExport) {
|
||||
customExportPath = dirPath
|
||||
customExportBook = book
|
||||
@@ -184,9 +299,14 @@ private fun CacheScreen(
|
||||
}
|
||||
}
|
||||
|
||||
fun selectExportFolder(bookUrl: String? = null, forAll: Boolean = false) {
|
||||
fun selectExportFolder(
|
||||
bookUrl: String? = null,
|
||||
forAll: Boolean = false,
|
||||
selection: Set<String> = emptySet()
|
||||
) {
|
||||
pendingExportBookUrl = bookUrl
|
||||
pendingExportAll = forAll
|
||||
pendingExportSelection = selection
|
||||
showFilePickerSheet = true
|
||||
}
|
||||
|
||||
@@ -219,16 +339,99 @@ private fun CacheScreen(
|
||||
}
|
||||
}
|
||||
|
||||
AppScaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
GlassMediumFlexibleTopAppBar(
|
||||
title = state.groupName ?: stringResource(R.string.offline_cache),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = {
|
||||
TopBarNavigationButton(onClick = onBackClick)
|
||||
fun exportSelected() {
|
||||
if (selectedBookUrls.isEmpty()) return
|
||||
val path = ACache.get().getAsString(exportBookPathKey)
|
||||
if (path.isNullOrEmpty() || !FileDoc.fromDir(path).checkWrite()) {
|
||||
selectExportFolder(selection = selectedBookUrls)
|
||||
} else {
|
||||
selectedBookUrls.forEach { bookUrl ->
|
||||
booksByUrl[bookUrl]?.let { book ->
|
||||
startExport(context, path, book, state.exportConfig.exportType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fun resolveSelectionGroupMask(): Long {
|
||||
val targetBooks = selectedBookUrls.mapNotNull { booksByUrl[it] }
|
||||
if (targetBooks.isEmpty()) return 0L
|
||||
val firstGroup = targetBooks.first().group.coerceAtLeast(0L)
|
||||
return if (targetBooks.all { it.group == firstGroup }) firstGroup else 0L
|
||||
}
|
||||
val fabItems = listOf(
|
||||
CacheFabAction(
|
||||
Icons.Default.SelectAll,
|
||||
stringResource(R.string.select_all)
|
||||
) {
|
||||
selectedBookUrls = state.books.mapTo(hashSetOf()) { it.bookUrl }
|
||||
},
|
||||
actions = {
|
||||
CacheFabAction(
|
||||
Icons.Default.Refresh,
|
||||
stringResource(R.string.revert_selection)
|
||||
) {
|
||||
val visibleBookUrls = booksByUrl.keys
|
||||
selectedBookUrls = visibleBookUrls - selectedBookUrls
|
||||
},
|
||||
CacheFabAction(
|
||||
Icons.Default.Download,
|
||||
"缓存选中"
|
||||
) {
|
||||
if (selectedBookUrls.isNotEmpty()) {
|
||||
showBatchDownloadConfirmDialog = true
|
||||
}
|
||||
},
|
||||
CacheFabAction(
|
||||
Icons.Default.Bookmarks,
|
||||
stringResource(R.string.move_to_group)
|
||||
) {
|
||||
if (selectedBookUrls.isNotEmpty()) {
|
||||
groupPickerCurrentGroupId = resolveSelectionGroupMask()
|
||||
pendingMoveGroupBookUrl = null
|
||||
showGroupSelectSheet = true
|
||||
}
|
||||
},
|
||||
CacheFabAction(
|
||||
Icons.Default.Upload,
|
||||
"导出选中"
|
||||
) {
|
||||
exportSelected()
|
||||
},
|
||||
CacheFabAction(
|
||||
Icons.Default.Delete,
|
||||
stringResource(R.string.clear_cache)
|
||||
) {
|
||||
viewModel.dispatch(CacheIntent.ClearCachesForBooks(selectedBookUrls))
|
||||
clearSelection()
|
||||
},
|
||||
CacheFabAction(
|
||||
Icons.Default.Delete,
|
||||
stringResource(R.string.delete)
|
||||
) {
|
||||
if (selectedBookUrls.isNotEmpty()) {
|
||||
pendingDeleteBookUrls = selectedBookUrls
|
||||
deleteOriginalBookFile = LocalConfig.deleteBookOriginal
|
||||
showDeleteBookConfirmDialog = true
|
||||
}
|
||||
}
|
||||
)
|
||||
val listState = rememberLazyListState()
|
||||
ListScaffold(
|
||||
title = if (inSelectionMode) {
|
||||
"已选 ${selectedBookUrls.size}/${filteredBooks.size}"
|
||||
} else {
|
||||
state.groupName ?: stringResource(R.string.offline_cache)
|
||||
},
|
||||
state = listUiState,
|
||||
onBackClick = onBackClick,
|
||||
onSearchToggle = { active ->
|
||||
isSearchMode = active
|
||||
if (!active) {
|
||||
searchKey = ""
|
||||
}
|
||||
},
|
||||
onSearchQueryChange = { searchKey = it },
|
||||
searchPlaceholder = "筛选书名/作者/书源/分组",
|
||||
topBarActions = {
|
||||
if (state.groupList.isNotEmpty()) {
|
||||
TopBarActionButton(
|
||||
onClick = { showGroupMenu = true },
|
||||
@@ -241,7 +444,7 @@ private fun CacheScreen(
|
||||
) { dismiss ->
|
||||
state.groupList.forEach { group ->
|
||||
RoundDropdownMenuItem(
|
||||
text = "${group.groupName}",
|
||||
text = group.groupName,
|
||||
isSelected = group.groupId == state.groupId,
|
||||
onClick = {
|
||||
dismiss()
|
||||
@@ -251,19 +454,12 @@ private fun CacheScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
TopBarActionButton(
|
||||
onClick = { showMenu = true },
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = null
|
||||
)
|
||||
RoundDropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
},
|
||||
dropDownMenuContent = { dismiss ->
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.download_all),
|
||||
onClick = {
|
||||
showMenu = false
|
||||
dismiss()
|
||||
if (state.isDownloadRunning) {
|
||||
viewModel.dispatch(CacheIntent.StopDownload)
|
||||
} else {
|
||||
@@ -273,17 +469,17 @@ private fun CacheScreen(
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.export_all),
|
||||
onClick = { showMenu = false; exportAll() }
|
||||
onClick = { dismiss(); exportAll() }
|
||||
)
|
||||
PillDivider()
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.export_folder),
|
||||
onClick = { showMenu = false; selectExportFolder() }
|
||||
onClick = { dismiss(); selectExportFolder() }
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.export_file_name),
|
||||
onClick = {
|
||||
showMenu = false
|
||||
dismiss()
|
||||
exportFileNameInput = state.exportConfig.bookExportFileName.orEmpty()
|
||||
showExportFileNameDialog = true
|
||||
}
|
||||
@@ -291,14 +487,14 @@ private fun CacheScreen(
|
||||
RoundDropdownMenuItem(
|
||||
text = "${stringResource(R.string.export_type)} (${exportTypes.getOrElse(state.exportConfig.exportType) { exportTypes[0] }})",
|
||||
onClick = {
|
||||
showMenu = false
|
||||
dismiss()
|
||||
showExportTypeDialog = true
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = "${stringResource(R.string.export_charset)} (${state.exportConfig.exportCharset})",
|
||||
onClick = {
|
||||
showMenu = false
|
||||
dismiss()
|
||||
exportCharsetInput = state.exportConfig.exportCharset
|
||||
showCharsetDialog = true
|
||||
}
|
||||
@@ -308,7 +504,7 @@ private fun CacheScreen(
|
||||
text = "替换净化",
|
||||
isSelected = state.exportConfig.exportUseReplace,
|
||||
onClick = {
|
||||
showMenu = false
|
||||
dismiss()
|
||||
viewModel.dispatch(
|
||||
CacheIntent.SetExportUseReplace(!state.exportConfig.exportUseReplace)
|
||||
)
|
||||
@@ -318,7 +514,7 @@ private fun CacheScreen(
|
||||
text = "自定义导出",
|
||||
isSelected = state.exportConfig.enableCustomExport,
|
||||
onClick = {
|
||||
showMenu = false
|
||||
dismiss()
|
||||
viewModel.dispatch(
|
||||
CacheIntent.SetEnableCustomExport(!state.exportConfig.enableCustomExport)
|
||||
)
|
||||
@@ -328,7 +524,7 @@ private fun CacheScreen(
|
||||
text = "导出包含章节名",
|
||||
isSelected = !state.exportConfig.exportNoChapterName,
|
||||
onClick = {
|
||||
showMenu = false
|
||||
dismiss()
|
||||
viewModel.dispatch(
|
||||
CacheIntent.SetExportNoChapterName(!state.exportConfig.exportNoChapterName)
|
||||
)
|
||||
@@ -338,7 +534,7 @@ private fun CacheScreen(
|
||||
text = "导出到WebDav",
|
||||
isSelected = state.exportConfig.exportToWebDav,
|
||||
onClick = {
|
||||
showMenu = false
|
||||
dismiss()
|
||||
viewModel.dispatch(
|
||||
CacheIntent.SetExportToWebDav(!state.exportConfig.exportToWebDav)
|
||||
)
|
||||
@@ -348,7 +544,7 @@ private fun CacheScreen(
|
||||
text = "导出插图文件",
|
||||
isSelected = state.exportConfig.exportPictureFile,
|
||||
onClick = {
|
||||
showMenu = false
|
||||
dismiss()
|
||||
viewModel.dispatch(
|
||||
CacheIntent.SetExportPictureFile(!state.exportConfig.exportPictureFile)
|
||||
)
|
||||
@@ -358,7 +554,7 @@ private fun CacheScreen(
|
||||
text = "并行导出",
|
||||
isSelected = state.exportConfig.parallelExportBook,
|
||||
onClick = {
|
||||
showMenu = false
|
||||
dismiss()
|
||||
viewModel.dispatch(
|
||||
CacheIntent.SetParallelExportBook(!state.exportConfig.parallelExportBook)
|
||||
)
|
||||
@@ -368,33 +564,55 @@ private fun CacheScreen(
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.log),
|
||||
onClick = {
|
||||
showMenu = false
|
||||
dismiss()
|
||||
showLogSheet = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
AppFloatingActionButton(
|
||||
onClick = {
|
||||
if (state.isDownloadRunning) {
|
||||
viewModel.dispatch(CacheIntent.StopDownload)
|
||||
} else {
|
||||
showDownloadConfirmDialog = true
|
||||
FloatingActionButtonMenu(
|
||||
modifier = Modifier.offset(x = 16.dp, y = 16.dp),
|
||||
expanded = fabMenuExpanded,
|
||||
button = {
|
||||
ToggleFloatingActionButton(
|
||||
modifier = Modifier
|
||||
.animateFloatingActionButton(
|
||||
visible = true,
|
||||
alignment = Alignment.BottomEnd,
|
||||
)
|
||||
.focusRequester(focusRequester),
|
||||
checked = fabMenuExpanded,
|
||||
onCheckedChange = { fabMenuExpanded = !fabMenuExpanded },
|
||||
) {
|
||||
val imageVector by remember {
|
||||
derivedStateOf {
|
||||
if (checkedProgress > 0.5f) Icons.Filled.Close else Icons.AutoMirrored.Filled.MenuOpen
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = "Menu",
|
||||
modifier = Modifier.animateIcon({ checkedProgress }),
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (state.isDownloadRunning) Icons.Default.Stop else Icons.Default.Download,
|
||||
contentDescription = null
|
||||
fabItems.forEach { (icon, label, action) ->
|
||||
FloatingActionButtonMenuItem(
|
||||
onClick = {
|
||||
action()
|
||||
fabMenuExpanded = false
|
||||
},
|
||||
icon = { Icon(icon, contentDescription = null) },
|
||||
text = { Text(text = label) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
val renderVersion by rememberUpdatedState(state.cacheVersion)
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
start = 12.dp,
|
||||
@@ -404,69 +622,75 @@ private fun CacheScreen(
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(state.books, key = { it.bookUrl }) { book ->
|
||||
items(filteredBooks, key = { it.bookUrl }) { book ->
|
||||
val cacheCount = remember(renderVersion, book.bookUrl) {
|
||||
viewModel.getCacheChapters(book.bookUrl)?.size ?: 0
|
||||
}
|
||||
val isDownloading = remember(renderVersion, book.bookUrl) {
|
||||
viewModel.isBookDownloading(book.bookUrl)
|
||||
}
|
||||
val isSelected = selectedBookUrls.contains(book.bookUrl)
|
||||
val exportMsg = remember(renderVersion, book.bookUrl) {
|
||||
ExportBookService.exportMsg[book.bookUrl]
|
||||
}
|
||||
val exportProgress = remember(renderVersion, book.bookUrl) {
|
||||
ExportBookService.exportProgress[book.bookUrl]
|
||||
NormalCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { toggleBookSelection(book) },
|
||||
onLongClick = { toggleBookSelection(book) },
|
||||
containerColor = if (isSelected) {
|
||||
LegadoTheme.colorScheme.secondaryContainer
|
||||
} else {
|
||||
LegadoTheme.colorScheme.surfaceContainerLow
|
||||
}
|
||||
GlassCard(
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainerLow
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp)
|
||||
.adaptiveHorizontalPadding(vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
AppText(
|
||||
text = book.name,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
style = LegadoTheme.typography.titleSmallEmphasized,
|
||||
maxLines = 1
|
||||
)
|
||||
TextCard(text = stringResource(R.string.download_count, cacheCount, book.totalChapterNum))
|
||||
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))
|
||||
}
|
||||
AppText(
|
||||
text = stringResource(R.string.author_show, book.getRealAuthor()),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
AppText(
|
||||
}
|
||||
TextCard(
|
||||
text = if (book.isLocal) {
|
||||
stringResource(R.string.local_book)
|
||||
} else {
|
||||
stringResource(R.string.download_count, cacheCount, book.totalChapterNum)
|
||||
}
|
||||
)
|
||||
if (isDownloading && book.totalChapterNum > 0) {
|
||||
AppLinearProgressIndicator(
|
||||
progress = (cacheCount.toFloat() / book.totalChapterNum.toFloat()).coerceIn(0f, 1f),
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp)
|
||||
stringResource(
|
||||
R.string.download_count,
|
||||
cacheCount,
|
||||
book.totalChapterNum
|
||||
)
|
||||
}
|
||||
if (exportMsg != null) {
|
||||
AppText(text = exportMsg, modifier = Modifier.padding(top = 6.dp))
|
||||
} else if (exportProgress != null && book.totalChapterNum > 0) {
|
||||
AppLinearProgressIndicator(
|
||||
progress = (exportProgress.toFloat() / book.totalChapterNum.toFloat()).coerceIn(0f, 1f),
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 6.dp)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
SmallTonalIconButton(
|
||||
onClick = {
|
||||
if (!book.isLocal) {
|
||||
@@ -474,23 +698,49 @@ private fun CacheScreen(
|
||||
}
|
||||
},
|
||||
imageVector = if (isDownloading) Icons.Default.Stop else Icons.Default.Download,
|
||||
contentDescription = null
|
||||
contentDescription = "download"
|
||||
)
|
||||
SmallTonalIconButton(
|
||||
onClick = { exportBook(book) },
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
imageVector = Icons.Default.Upload,
|
||||
contentDescription = null
|
||||
contentDescription = "upload"
|
||||
)
|
||||
SmallTonalIconButton(
|
||||
onClick = {
|
||||
viewModel.dispatch(CacheIntent.DeleteBookDownload(book.bookUrl))
|
||||
viewModel.dispatch(CacheIntent.ClearBookCache(book))
|
||||
pendingMoveGroupBookUrl = book.bookUrl
|
||||
groupPickerCurrentGroupId = book.group.coerceAtLeast(0L)
|
||||
showGroupSelectSheet = true
|
||||
},
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = null
|
||||
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 = {
|
||||
pendingDeleteBookUrls = setOf(book.bookUrl)
|
||||
deleteOriginalBookFile = LocalConfig.deleteBookOriginal
|
||||
showDeleteBookConfirmDialog = true
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = "删除缓存",
|
||||
onClick = {
|
||||
viewModel.dispatch(CacheIntent.ClearCachesForBooks(setOf(book.bookUrl)))
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -504,35 +754,90 @@ private fun CacheScreen(
|
||||
title = exportFolderText,
|
||||
onSelectSysDir = {
|
||||
showFilePickerSheet = false
|
||||
val default = arrayListOf<SelectItem<Int>>()
|
||||
val path = ACache.get().getAsString(exportBookPathKey)
|
||||
if (!path.isNullOrEmpty()) default.add(SelectItem(path, -1))
|
||||
exportDir.launch {
|
||||
mode = HandleFileContract.DIR_SYS
|
||||
title = exportFolderText
|
||||
otherActions = default
|
||||
requestCode = 0
|
||||
}
|
||||
exportDir.launch(null)
|
||||
}
|
||||
)
|
||||
|
||||
AppAlertDialog(
|
||||
show = showDownloadConfirmDialog,
|
||||
onDismissRequest = { showDownloadConfirmDialog = false },
|
||||
show = showBatchDownloadConfirmDialog,
|
||||
onDismissRequest = { showBatchDownloadConfirmDialog = false },
|
||||
title = stringResource(R.string.draw),
|
||||
text = stringResource(R.string.sure_cache_book),
|
||||
confirmText = stringResource(android.R.string.ok),
|
||||
onConfirm = {
|
||||
showDownloadConfirmDialog = false
|
||||
showBatchDownloadConfirmDialog = false
|
||||
viewModel.dispatch(
|
||||
CacheIntent.StartDownloadForVisibleBooks(
|
||||
books = state.books,
|
||||
CacheIntent.DownloadBooks(
|
||||
bookUrls = selectedBookUrls,
|
||||
downloadAllChapters = false
|
||||
)
|
||||
)
|
||||
},
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = { showDownloadConfirmDialog = false }
|
||||
onDismiss = { showBatchDownloadConfirmDialog = false }
|
||||
)
|
||||
|
||||
AppAlertDialog(
|
||||
show = showDeleteBookConfirmDialog,
|
||||
onDismissRequest = { showDeleteBookConfirmDialog = false },
|
||||
title = stringResource(R.string.draw),
|
||||
content = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
AppText(text = stringResource(R.string.sure_del))
|
||||
if (hasLocalBookInDeleteTarget) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Checkbox(
|
||||
checked = deleteOriginalBookFile,
|
||||
onCheckedChange = { checked ->
|
||||
deleteOriginalBookFile = checked
|
||||
}
|
||||
)
|
||||
AppText(
|
||||
text = stringResource(R.string.delete_book_file),
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmText = stringResource(android.R.string.ok),
|
||||
onConfirm = {
|
||||
showDeleteBookConfirmDialog = false
|
||||
LocalConfig.deleteBookOriginal = deleteOriginalBookFile
|
||||
viewModel.dispatch(
|
||||
CacheIntent.DeleteBooks(
|
||||
bookUrls = pendingDeleteBookUrls,
|
||||
deleteOriginal = deleteOriginalBookFile
|
||||
)
|
||||
)
|
||||
pendingDeleteBookUrls = emptySet()
|
||||
clearSelection()
|
||||
},
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = { showDeleteBookConfirmDialog = false }
|
||||
)
|
||||
|
||||
GroupSelectSheet(
|
||||
show = showGroupSelectSheet,
|
||||
currentGroupId = groupPickerCurrentGroupId,
|
||||
onDismissRequest = { showGroupSelectSheet = false },
|
||||
onConfirm = { groupId ->
|
||||
val moveSet = pendingMoveGroupBookUrl?.let { setOf(it) } ?: selectedBookUrls
|
||||
val targetGroupId = groupId.coerceAtLeast(0L)
|
||||
viewModel.dispatch(
|
||||
CacheIntent.MoveBooksToGroup(
|
||||
bookUrls = moveSet,
|
||||
groupId = targetGroupId
|
||||
)
|
||||
)
|
||||
pendingMoveGroupBookUrl = null
|
||||
groupPickerCurrentGroupId = 0L
|
||||
showGroupSelectSheet = false
|
||||
clearSelection()
|
||||
}
|
||||
)
|
||||
|
||||
AppAlertDialog(
|
||||
|
||||
+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,16 +333,17 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleBookDownload(book: Book) {
|
||||
if (book.isLocal) return
|
||||
@@ -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}本"
|
||||
}
|
||||
} 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,11 +65,14 @@ fun TextCard(
|
||||
tint = finalContentColor,
|
||||
modifier = Modifier.size(iconSize)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(spacing))
|
||||
}
|
||||
|
||||
if (icon != null && text != null)
|
||||
Spacer(modifier = Modifier.width(spacing))
|
||||
|
||||
text?.let {
|
||||
AnimatedTextLine(
|
||||
text = text,
|
||||
text = it,
|
||||
style = textStyle,
|
||||
color = finalContentColor,
|
||||
maxLines = 2,
|
||||
@@ -77,5 +80,6 @@ fun TextCard(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user