[优化] 合并下载和原本的书架管理界面
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.SearchRepositoryImpl
|
||||||
import io.legado.app.data.repository.SearchContentRepository
|
import io.legado.app.data.repository.SearchContentRepository
|
||||||
import io.legado.app.data.repository.UploadRepository
|
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.ExploreKindUiUseCase
|
||||||
|
import io.legado.app.domain.usecase.UpdateBooksGroupUseCase
|
||||||
import io.legado.app.help.coil.CoverFetcher
|
import io.legado.app.help.coil.CoverFetcher
|
||||||
import io.legado.app.help.coil.CoverInterceptor
|
import io.legado.app.help.coil.CoverInterceptor
|
||||||
import io.legado.app.help.http.okHttpClient
|
import io.legado.app.help.http.okHttpClient
|
||||||
@@ -67,6 +71,7 @@ val appModule = module {
|
|||||||
single { get<AppDatabase>().bookDao }
|
single { get<AppDatabase>().bookDao }
|
||||||
single { get<AppDatabase>().bookChapterDao }
|
single { get<AppDatabase>().bookChapterDao }
|
||||||
single { get<AppDatabase>().bookGroupDao }
|
single { get<AppDatabase>().bookGroupDao }
|
||||||
|
single { get<AppDatabase>().bookSourceDao }
|
||||||
|
|
||||||
singleOf(::ReadRecordRepository)
|
singleOf(::ReadRecordRepository)
|
||||||
singleOf(::BookRepository)
|
singleOf(::BookRepository)
|
||||||
@@ -74,6 +79,10 @@ val appModule = module {
|
|||||||
singleOf(::SearchContentRepository)
|
singleOf(::SearchContentRepository)
|
||||||
singleOf(::RemoteBookRepository)
|
singleOf(::RemoteBookRepository)
|
||||||
singleOf(::ExploreKindUiUseCase)
|
singleOf(::ExploreKindUiUseCase)
|
||||||
|
singleOf(::BatchCacheDownloadUseCase)
|
||||||
|
singleOf(::ClearBookCacheUseCase)
|
||||||
|
singleOf(::DeleteBooksUseCase)
|
||||||
|
singleOf(::UpdateBooksGroupUseCase)
|
||||||
singleOf(::CacheConfig)
|
singleOf(::CacheConfig)
|
||||||
|
|
||||||
single<UploadRepository> { DirectLinkUploadRepository() }
|
single<UploadRepository> { DirectLinkUploadRepository() }
|
||||||
@@ -125,7 +134,19 @@ val appModule = module {
|
|||||||
viewModelOf(::ExploreViewModel)
|
viewModelOf(::ExploreViewModel)
|
||||||
viewModelOf(::RssViewModel)
|
viewModelOf(::RssViewModel)
|
||||||
viewModelOf(::SearchViewModel)
|
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) ->
|
viewModel { (route: ReplaceEditRoute) ->
|
||||||
ReplaceEditViewModel(
|
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.Book
|
||||||
import io.legado.app.data.entities.BookChapter
|
import io.legado.app.data.entities.BookChapter
|
||||||
import io.legado.app.data.entities.BookGroup
|
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.BookHelp
|
||||||
import io.legado.app.help.book.isAudio
|
import io.legado.app.help.book.isAudio
|
||||||
import io.legado.app.help.book.isLocal
|
import io.legado.app.help.book.isLocal
|
||||||
@@ -61,6 +65,10 @@ sealed interface CacheIntent {
|
|||||||
data class ToggleBookDownload(val book: Book) : CacheIntent
|
data class ToggleBookDownload(val book: Book) : CacheIntent
|
||||||
data class DeleteBookDownload(val bookUrl: String) : CacheIntent
|
data class DeleteBookDownload(val bookUrl: String) : CacheIntent
|
||||||
data class ClearBookCache(val book: Book) : 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 SetExportUseReplace(val enabled: Boolean) : CacheIntent
|
||||||
data class SetEnableCustomExport(val enabled: Boolean) : CacheIntent
|
data class SetEnableCustomExport(val enabled: Boolean) : CacheIntent
|
||||||
data class SetExportNoChapterName(val enabled: Boolean) : CacheIntent
|
data class SetExportNoChapterName(val enabled: Boolean) : CacheIntent
|
||||||
@@ -83,7 +91,11 @@ class CacheViewModel(
|
|||||||
private val bookDao: BookDao,
|
private val bookDao: BookDao,
|
||||||
private val bookGroupDao: BookGroupDao,
|
private val bookGroupDao: BookGroupDao,
|
||||||
private val bookChapterDao: BookChapterDao,
|
private val bookChapterDao: BookChapterDao,
|
||||||
val cacheConfig: CacheConfig
|
val cacheConfig: CacheConfig,
|
||||||
|
private val batchCacheDownloadUseCase: BatchCacheDownloadUseCase,
|
||||||
|
private val clearBookCacheUseCase: ClearBookCacheUseCase,
|
||||||
|
private val deleteBooksUseCase: DeleteBooksUseCase,
|
||||||
|
private val updateBooksGroupUseCase: UpdateBooksGroupUseCase
|
||||||
) : BaseViewModel(application) {
|
) : BaseViewModel(application) {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(CacheUiState())
|
private val _uiState = MutableStateFlow(CacheUiState())
|
||||||
@@ -110,6 +122,10 @@ class CacheViewModel(
|
|||||||
is CacheIntent.ToggleBookDownload -> toggleBookDownload(intent.book)
|
is CacheIntent.ToggleBookDownload -> toggleBookDownload(intent.book)
|
||||||
is CacheIntent.DeleteBookDownload -> CacheBook.remove(context, intent.bookUrl)
|
is CacheIntent.DeleteBookDownload -> CacheBook.remove(context, intent.bookUrl)
|
||||||
is CacheIntent.ClearBookCache -> clearCacheForBook(intent.book)
|
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 -> {
|
is CacheIntent.SetExportUseReplace -> {
|
||||||
cacheConfig.exportUseReplace = intent.enabled
|
cacheConfig.exportUseReplace = intent.enabled
|
||||||
syncExportConfig()
|
syncExportConfig()
|
||||||
@@ -317,15 +333,16 @@ class CacheViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun startDownloadForVisibleBooks(books: List<Book>, downloadAllChapters: Boolean) {
|
private fun startDownloadForVisibleBooks(books: List<Book>, downloadAllChapters: Boolean) {
|
||||||
books.forEach { book ->
|
execute {
|
||||||
val indices = if (downloadAllChapters) {
|
batchCacheDownloadUseCase.execute(
|
||||||
(0..book.lastChapterIndex).toList()
|
context = context,
|
||||||
} else {
|
books = books,
|
||||||
(book.durChapterIndex..book.lastChapterIndex).toList()
|
downloadAllChapters = downloadAllChapters,
|
||||||
}
|
skipAudioBooks = true
|
||||||
CacheBook.start(context, book, indices)
|
)
|
||||||
|
}.onFinally {
|
||||||
|
syncDownloadRunning()
|
||||||
}
|
}
|
||||||
syncDownloadRunning()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleBookDownload(book: Book) {
|
private fun toggleBookDownload(book: Book) {
|
||||||
@@ -338,10 +355,68 @@ class CacheViewModel(
|
|||||||
syncDownloadRunning()
|
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) {
|
private fun clearCacheForBook(book: Book) {
|
||||||
BookHelp.clearCache(book)
|
val bookUrl = clearBookCacheUseCase.execute(book)
|
||||||
cacheChapters[book.bookUrl] = hashSetOf()
|
cacheChapters[bookUrl] = hashSetOf()
|
||||||
emitBookChanged(book.bookUrl)
|
emitBookChanged(bookUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emitBookChanged(bookUrl: String) {
|
private fun emitBookChanged(bookUrl: String) {
|
||||||
|
|||||||
@@ -6,16 +6,20 @@ import androidx.activity.compose.BackHandler
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
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.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||||
@@ -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.EmptyMessage
|
||||||
import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton
|
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.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.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.divider.PillHeaderDivider
|
||||||
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
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.icon.AppIcons
|
||||||
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
|
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.lazylist.FastScrollLazyVerticalGrid
|
||||||
import io.legado.app.ui.widget.components.list.ListScaffold
|
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.RoundDropdownMenu
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||||
import io.legado.app.ui.widget.components.tabRow.AppTabRow
|
import io.legado.app.ui.widget.components.tabRow.AppTabRow
|
||||||
@@ -124,6 +131,7 @@ fun BookshelfScreen(
|
|||||||
var showLogSheet by remember { mutableStateOf(false) }
|
var showLogSheet by remember { mutableStateOf(false) }
|
||||||
var showGroupMenu by remember { mutableStateOf(false) }
|
var showGroupMenu by remember { mutableStateOf(false) }
|
||||||
var showGroupSelectSheet by remember { mutableStateOf(false) }
|
var showGroupSelectSheet by remember { mutableStateOf(false) }
|
||||||
|
var showBatchDownloadConfirmDialog by remember { mutableStateOf(false) }
|
||||||
var isEditMode by remember { mutableStateOf(false) }
|
var isEditMode by remember { mutableStateOf(false) }
|
||||||
var selectedBookUrls by remember { mutableStateOf<Set<String>>(emptySet()) }
|
var selectedBookUrls by remember { mutableStateOf<Set<String>>(emptySet()) }
|
||||||
|
|
||||||
@@ -289,18 +297,14 @@ fun BookshelfScreen(
|
|||||||
else -> stringResource(R.string.bookshelf)
|
else -> stringResource(R.string.bookshelf)
|
||||||
}
|
}
|
||||||
val title = if (isEditMode) {
|
val title = if (isEditMode) {
|
||||||
"$currentGroupBookCount · ${selectedBookUrls.size} 本"
|
stringResource(R.string.bookshelf)
|
||||||
} else if (uiState.upBooksCount > 0) {
|
} else if (uiState.upBooksCount > 0) {
|
||||||
"$baseTitle (${uiState.upBooksCount})"
|
"$baseTitle (${uiState.upBooksCount})"
|
||||||
} else {
|
} else {
|
||||||
baseTitle
|
baseTitle
|
||||||
}
|
}
|
||||||
val subtitle = if (isEditMode) {
|
val subtitle = if (isEditMode) {
|
||||||
if (bookGroupStyle == 0) {
|
"共${allGroupsBookCount}本"
|
||||||
"共${allGroupsBookCount}本"
|
|
||||||
} else {
|
|
||||||
"${currentGroupName ?: stringResource(R.string.bookshelf)} · 共${allGroupsBookCount}本"
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -323,13 +327,22 @@ fun BookshelfScreen(
|
|||||||
if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) 12.dp else 16.dp
|
if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) 12.dp else 16.dp
|
||||||
val gridContentHorizontalPadding = totalHorizontalPadding / 2
|
val gridContentHorizontalPadding = totalHorizontalPadding / 2
|
||||||
val gridInnerHorizontalPadding = totalHorizontalPadding / 2
|
val gridInnerHorizontalPadding = totalHorizontalPadding / 2
|
||||||
|
val 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(
|
ListScaffold(
|
||||||
title = title,
|
title = title,
|
||||||
subtitle = subtitle,
|
subtitle = subtitle,
|
||||||
state = uiState,
|
state = uiState,
|
||||||
onBackClick = if (isEditMode) exitEditMode else null,
|
|
||||||
backNavigationIcon = if (isEditMode) AppIcons.Close else AppIcons.Back,
|
|
||||||
showSearchAction = true,
|
showSearchAction = true,
|
||||||
onSearchToggle = { active ->
|
onSearchToggle = { active ->
|
||||||
viewModel.setSearchMode(active)
|
viewModel.setSearchMode(active)
|
||||||
@@ -353,7 +366,7 @@ fun BookshelfScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
topBarActions = {
|
topBarActions = {
|
||||||
if (isEditMode) {
|
AnimatedVisibility(visible = isEditMode) {
|
||||||
TopBarActionButton(
|
TopBarActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
selectedBookUrls = uiState.items.mapTo(hashSetOf()) { it.bookUrl }
|
selectedBookUrls = uiState.items.mapTo(hashSetOf()) { it.bookUrl }
|
||||||
@@ -361,6 +374,8 @@ fun BookshelfScreen(
|
|||||||
imageVector = Icons.Default.SelectAll,
|
imageVector = Icons.Default.SelectAll,
|
||||||
contentDescription = stringResource(R.string.select_all)
|
contentDescription = stringResource(R.string.select_all)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
AnimatedVisibility(visible = isEditMode) {
|
||||||
TopBarActionButton(
|
TopBarActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
val visibleBookUrls = uiState.items.mapTo(hashSetOf()) { it.bookUrl }
|
val visibleBookUrls = uiState.items.mapTo(hashSetOf()) { it.bookUrl }
|
||||||
@@ -369,6 +384,19 @@ fun BookshelfScreen(
|
|||||||
imageVector = Icons.Default.Refresh,
|
imageVector = Icons.Default.Refresh,
|
||||||
contentDescription = stringResource(R.string.revert_selection)
|
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(
|
TopBarActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (selectedBookUrls.isNotEmpty()) {
|
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(
|
PullToRefreshDefaults.LoadingIndicator(
|
||||||
state = pullToRefreshState,
|
state = pullToRefreshState,
|
||||||
isRefreshing = isRefreshing,
|
isRefreshing = isRefreshing,
|
||||||
@@ -794,6 +913,20 @@ fun BookshelfScreen(
|
|||||||
onDismissRequest = { showLogSheet = false }
|
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) {
|
if (uiState.isLoading) {
|
||||||
Dialog(onDismissRequest = {}) {
|
Dialog(onDismissRequest = {}) {
|
||||||
NormalCard(
|
NormalCard(
|
||||||
@@ -818,6 +951,13 @@ fun BookshelfScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class BookshelfEditStickySummary(
|
||||||
|
val selectedCount: Int,
|
||||||
|
val currentGroupTotalCount: Int,
|
||||||
|
val groupName: String?,
|
||||||
|
val showGroupName: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BookshelfPage(
|
fun BookshelfPage(
|
||||||
paddingValues: PaddingValues,
|
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.entities.BookSourcePart
|
||||||
import io.legado.app.data.repository.BookGroupRepository
|
import io.legado.app.data.repository.BookGroupRepository
|
||||||
import io.legado.app.data.repository.UploadRepository
|
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.exception.NoStackTraceException
|
||||||
import io.legado.app.help.book.BookHelp
|
import io.legado.app.help.book.BookHelp
|
||||||
import io.legado.app.help.book.addType
|
import io.legado.app.help.book.addType
|
||||||
@@ -91,7 +93,9 @@ import kotlin.math.min
|
|||||||
class BookshelfViewModel(
|
class BookshelfViewModel(
|
||||||
application: Application,
|
application: Application,
|
||||||
private val bookGroupRepository: BookGroupRepository,
|
private val bookGroupRepository: BookGroupRepository,
|
||||||
private val uploadRepository: UploadRepository
|
private val uploadRepository: UploadRepository,
|
||||||
|
private val batchCacheDownloadUseCase: BatchCacheDownloadUseCase,
|
||||||
|
private val updateBooksGroupUseCase: UpdateBooksGroupUseCase
|
||||||
) : BaseViewModel(application) {
|
) : BaseViewModel(application) {
|
||||||
var addBookJob: Coroutine<*>? = null
|
var addBookJob: Coroutine<*>? = null
|
||||||
|
|
||||||
@@ -377,33 +381,34 @@ class BookshelfViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun moveBooksToGroup(bookUrls: Set<String>, groupId: Long) {
|
fun moveBooksToGroup(bookUrls: Set<String>, groupId: Long) {
|
||||||
updateBooksGroup(bookUrls) { groupId }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateBooksGroup(
|
|
||||||
bookUrls: Set<String>,
|
|
||||||
transform: (Long) -> Long
|
|
||||||
) {
|
|
||||||
if (bookUrls.isEmpty()) return
|
if (bookUrls.isEmpty()) return
|
||||||
execute {
|
execute {
|
||||||
val updateBooks = bookUrls.mapNotNull { url ->
|
updateBooksGroupUseCase.replaceGroup(bookUrls, groupId)
|
||||||
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())
|
|
||||||
}
|
|
||||||
}.onError {
|
}.onError {
|
||||||
context.toastOnUi("更新分组失败\n${it.localizedMessage}")
|
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() {
|
fun gotoTop() {
|
||||||
scrollTrigger.tryEmit(Unit)
|
scrollTrigger.tryEmit(Unit)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import io.legado.app.ui.widget.components.text.AnimatedTextLine
|
|||||||
@Composable
|
@Composable
|
||||||
fun TextCard(
|
fun TextCard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
text: String,
|
text: String? = null,
|
||||||
icon: ImageVector? = null,
|
icon: ImageVector? = null,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
backgroundColor: Color? = null,
|
backgroundColor: Color? = null,
|
||||||
@@ -65,16 +65,20 @@ fun TextCard(
|
|||||||
tint = finalContentColor,
|
tint = finalContentColor,
|
||||||
modifier = Modifier.size(iconSize)
|
modifier = Modifier.size(iconSize)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(spacing))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedTextLine(
|
if (icon != null && text != null)
|
||||||
text = text,
|
Spacer(modifier = Modifier.width(spacing))
|
||||||
style = textStyle,
|
|
||||||
color = finalContentColor,
|
text?.let {
|
||||||
maxLines = 2,
|
AnimatedTextLine(
|
||||||
overflow = TextOverflow.Ellipsis
|
text = it,
|
||||||
)
|
style = textStyle,
|
||||||
|
color = finalContentColor,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user