[优化] 更牛逼的换源设置
This commit is contained in:
9 files changed
+1367
-14
No files matched your search
@@ -39,6 +39,7 @@ import io.legado.app.domain.repository.BookDomainRepository
|
|||||||
import io.legado.app.domain.usecase.AppStartupMaintenanceUseCase
|
import io.legado.app.domain.usecase.AppStartupMaintenanceUseCase
|
||||||
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
|
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
|
||||||
import io.legado.app.domain.usecase.CacheBookChaptersUseCase
|
import io.legado.app.domain.usecase.CacheBookChaptersUseCase
|
||||||
|
import io.legado.app.domain.usecase.ChangeBookSourceUseCase
|
||||||
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
||||||
import io.legado.app.domain.usecase.DeleteBooksUseCase
|
import io.legado.app.domain.usecase.DeleteBooksUseCase
|
||||||
import io.legado.app.domain.usecase.GetReadingProgressUseCase
|
import io.legado.app.domain.usecase.GetReadingProgressUseCase
|
||||||
@@ -113,6 +114,7 @@ val appModule = module {
|
|||||||
singleOf(::AppStartupMaintenanceUseCase)
|
singleOf(::AppStartupMaintenanceUseCase)
|
||||||
singleOf(::BatchCacheDownloadUseCase)
|
singleOf(::BatchCacheDownloadUseCase)
|
||||||
singleOf(::CacheBookChaptersUseCase)
|
singleOf(::CacheBookChaptersUseCase)
|
||||||
|
singleOf(::ChangeBookSourceUseCase)
|
||||||
singleOf(::ClearBookCacheUseCase)
|
singleOf(::ClearBookCacheUseCase)
|
||||||
singleOf(::DeleteBooksUseCase)
|
singleOf(::DeleteBooksUseCase)
|
||||||
singleOf(::GetReadingProgressUseCase)
|
singleOf(::GetReadingProgressUseCase)
|
||||||
@@ -194,6 +196,7 @@ val appModule = module {
|
|||||||
cacheConfig = get(),
|
cacheConfig = get(),
|
||||||
batchCacheDownloadUseCase = get(),
|
batchCacheDownloadUseCase = get(),
|
||||||
cacheBookChaptersUseCase = get(),
|
cacheBookChaptersUseCase = get(),
|
||||||
|
changeBookSourceUseCase = get(),
|
||||||
clearBookCacheUseCase = get(),
|
clearBookCacheUseCase = get(),
|
||||||
deleteBooksUseCase = get(),
|
deleteBooksUseCase = get(),
|
||||||
updateBooksGroupUseCase = get()
|
updateBooksGroupUseCase = get()
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
package io.legado.app.domain.usecase
|
||||||
|
|
||||||
|
import io.legado.app.constant.AppLog
|
||||||
|
import io.legado.app.constant.BookType
|
||||||
|
import io.legado.app.data.dao.BookChapterDao
|
||||||
|
import io.legado.app.data.dao.BookDao
|
||||||
|
import io.legado.app.data.entities.Book
|
||||||
|
import io.legado.app.data.entities.BookChapter
|
||||||
|
import io.legado.app.data.entities.BookSource
|
||||||
|
import io.legado.app.help.book.BookHelp
|
||||||
|
import io.legado.app.help.book.ContentProcessor
|
||||||
|
import io.legado.app.help.book.isLocal
|
||||||
|
import io.legado.app.help.book.removeType
|
||||||
|
import io.legado.app.model.ReadBook
|
||||||
|
import io.legado.app.model.webBook.WebBook
|
||||||
|
import io.legado.app.utils.mapAsync
|
||||||
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
import kotlinx.coroutines.flow.toList
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
|
data class ChangeSourceMigrationOptions(
|
||||||
|
val migrateChapters: Boolean = true,
|
||||||
|
val migrateReadingProgress: Boolean = true,
|
||||||
|
val migrateGroup: Boolean = true,
|
||||||
|
val migrateCover: Boolean = true,
|
||||||
|
val migrateCategory: Boolean = true,
|
||||||
|
val migrateRemark: Boolean = true,
|
||||||
|
val migrateReadConfig: Boolean = true,
|
||||||
|
val deleteDownloadedChapters: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ChangeBookSourceResult(
|
||||||
|
val oldBookUrl: String,
|
||||||
|
val book: Book,
|
||||||
|
val chapters: List<BookChapter>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BatchChangeBookSourceResult(
|
||||||
|
val changedCount: Int,
|
||||||
|
val failedCount: Int,
|
||||||
|
val skippedCount: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BatchChangeSourceCandidate(
|
||||||
|
val source: BookSource,
|
||||||
|
val book: Book,
|
||||||
|
val chapters: List<BookChapter>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BatchChangeSourcePreviewItem(
|
||||||
|
val oldBook: Book,
|
||||||
|
val candidates: List<BatchChangeSourceCandidate> = emptyList(),
|
||||||
|
val selectedCandidateIndex: Int = 0,
|
||||||
|
val status: BatchChangeSourcePreviewStatus = BatchChangeSourcePreviewStatus.NotFound,
|
||||||
|
) {
|
||||||
|
val selectedCandidate: BatchChangeSourceCandidate?
|
||||||
|
get() = candidates.getOrNull(selectedCandidateIndex)
|
||||||
|
|
||||||
|
val canMigrate: Boolean
|
||||||
|
get() = status == BatchChangeSourcePreviewStatus.Matched && selectedCandidate != null
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class BatchChangeSourcePreviewStatus {
|
||||||
|
Matched,
|
||||||
|
NotFound,
|
||||||
|
Skipped,
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChangeBookSourceUseCase(
|
||||||
|
private val bookDao: BookDao,
|
||||||
|
private val bookChapterDao: BookChapterDao,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun applyMigration(
|
||||||
|
oldBook: Book,
|
||||||
|
newBook: Book,
|
||||||
|
chapters: List<BookChapter>,
|
||||||
|
options: ChangeSourceMigrationOptions,
|
||||||
|
): Book {
|
||||||
|
oldBook.applyMigrationTo(newBook, chapters, options)
|
||||||
|
newBook.removeType(BookType.updateError)
|
||||||
|
return newBook
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeTo(
|
||||||
|
oldBook: Book,
|
||||||
|
newBook: Book,
|
||||||
|
chapters: List<BookChapter>,
|
||||||
|
options: ChangeSourceMigrationOptions,
|
||||||
|
): ChangeBookSourceResult {
|
||||||
|
val oldBookUrl = oldBook.bookUrl
|
||||||
|
applyMigration(oldBook, newBook, chapters, options)
|
||||||
|
if (options.deleteDownloadedChapters) {
|
||||||
|
BookHelp.clearCache(oldBook)
|
||||||
|
} else if (oldBook.bookUrl != newBook.bookUrl) {
|
||||||
|
BookHelp.updateCacheFolder(oldBook, newBook)
|
||||||
|
}
|
||||||
|
bookChapterDao.delByBook(oldBook.bookUrl)
|
||||||
|
bookDao.delete(oldBook)
|
||||||
|
bookDao.insert(newBook)
|
||||||
|
if (options.migrateChapters) {
|
||||||
|
bookChapterDao.insert(*chapters.toTypedArray())
|
||||||
|
ReadBook.onChapterListUpdated(newBook)
|
||||||
|
}
|
||||||
|
return ChangeBookSourceResult(oldBookUrl, newBook, chapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun batchChangeTo(
|
||||||
|
books: List<Book>,
|
||||||
|
source: BookSource,
|
||||||
|
options: ChangeSourceMigrationOptions,
|
||||||
|
onProgress: (current: Int, total: Int, bookName: String) -> Unit,
|
||||||
|
): BatchChangeBookSourceResult {
|
||||||
|
var changedCount = 0
|
||||||
|
var failedCount = 0
|
||||||
|
var skippedCount = 0
|
||||||
|
books.forEachIndexed { index, book ->
|
||||||
|
onProgress(index + 1, books.size, book.name)
|
||||||
|
if (book.isLocal || book.origin == source.bookSourceUrl) {
|
||||||
|
skippedCount++
|
||||||
|
return@forEachIndexed
|
||||||
|
}
|
||||||
|
val newBook = WebBook.preciseSearchAwait(source, book.name, book.author)
|
||||||
|
.onFailure {
|
||||||
|
AppLog.put("搜索书籍出错\n${it.localizedMessage}", it, true)
|
||||||
|
}.getOrNull()
|
||||||
|
if (newBook == null) {
|
||||||
|
failedCount++
|
||||||
|
return@forEachIndexed
|
||||||
|
}
|
||||||
|
val infoLoaded = kotlin.runCatching {
|
||||||
|
if (newBook.tocUrl.isEmpty()) {
|
||||||
|
WebBook.getBookInfoAwait(source, newBook)
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
AppLog.put("获取书籍详情出错\n${it.localizedMessage}", it, true)
|
||||||
|
}.isSuccess
|
||||||
|
if (!infoLoaded) {
|
||||||
|
failedCount++
|
||||||
|
return@forEachIndexed
|
||||||
|
}
|
||||||
|
val chapters = WebBook.getChapterListAwait(source, newBook)
|
||||||
|
.onFailure {
|
||||||
|
AppLog.put("获取目录出错\n${it.localizedMessage}", it, true)
|
||||||
|
}.getOrNull()
|
||||||
|
if (chapters == null) {
|
||||||
|
failedCount++
|
||||||
|
} else {
|
||||||
|
changeTo(book, newBook, chapters, options)
|
||||||
|
changedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return BatchChangeBookSourceResult(changedCount, failedCount, skippedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun prepareBatchChange(
|
||||||
|
books: List<Book>,
|
||||||
|
sources: List<BookSource>,
|
||||||
|
concurrency: Int,
|
||||||
|
onProgress: (current: Int, total: Int, bookName: String) -> Unit,
|
||||||
|
): List<BatchChangeSourcePreviewItem> {
|
||||||
|
val progress = AtomicInteger(0)
|
||||||
|
return books.withIndex().asFlow()
|
||||||
|
.mapAsync(concurrency.coerceAtLeast(1)) { indexedBook ->
|
||||||
|
val book = indexedBook.value
|
||||||
|
onProgress(progress.incrementAndGet(), books.size, book.name)
|
||||||
|
val previewItem = if (book.isLocal) {
|
||||||
|
BatchChangeSourcePreviewItem(
|
||||||
|
oldBook = book,
|
||||||
|
status = BatchChangeSourcePreviewStatus.Skipped
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val candidates = arrayListOf<BatchChangeSourceCandidate>()
|
||||||
|
sources.filterNot { it.bookSourceUrl == book.origin }.forEach { source ->
|
||||||
|
findBookInSource(book, source)?.let { (newBook, chapters) ->
|
||||||
|
candidates.add(
|
||||||
|
BatchChangeSourceCandidate(
|
||||||
|
source = source,
|
||||||
|
book = newBook,
|
||||||
|
chapters = chapters,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (candidates.isEmpty()) {
|
||||||
|
BatchChangeSourcePreviewItem(oldBook = book)
|
||||||
|
} else {
|
||||||
|
BatchChangeSourcePreviewItem(
|
||||||
|
oldBook = book,
|
||||||
|
candidates = candidates,
|
||||||
|
status = BatchChangeSourcePreviewStatus.Matched
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
indexedBook.index to previewItem
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
.sortedBy { it.first }
|
||||||
|
.map { it.second }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changePreviewItems(
|
||||||
|
items: List<BatchChangeSourcePreviewItem>,
|
||||||
|
options: ChangeSourceMigrationOptions,
|
||||||
|
): BatchChangeBookSourceResult {
|
||||||
|
var changedCount = 0
|
||||||
|
var failedCount = 0
|
||||||
|
var skippedCount = 0
|
||||||
|
items.forEach { item ->
|
||||||
|
if (item.status == BatchChangeSourcePreviewStatus.Skipped) {
|
||||||
|
skippedCount++
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
val candidate = item.selectedCandidate
|
||||||
|
if (candidate == null) {
|
||||||
|
failedCount++
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
changeTo(item.oldBook, candidate.book, candidate.chapters, options)
|
||||||
|
changedCount++
|
||||||
|
}
|
||||||
|
return BatchChangeBookSourceResult(changedCount, failedCount, skippedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun findBookInSource(
|
||||||
|
oldBook: Book,
|
||||||
|
source: BookSource,
|
||||||
|
): Pair<Book, List<BookChapter>>? {
|
||||||
|
val newBook = WebBook.preciseSearchAwait(source, oldBook.name, oldBook.author)
|
||||||
|
.onFailure {
|
||||||
|
AppLog.put("搜索书籍出错\n${it.localizedMessage}", it, true)
|
||||||
|
}.getOrNull() ?: return null
|
||||||
|
val infoLoaded = kotlin.runCatching {
|
||||||
|
if (newBook.tocUrl.isEmpty()) {
|
||||||
|
WebBook.getBookInfoAwait(source, newBook)
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
AppLog.put("获取书籍详情出错\n${it.localizedMessage}", it, true)
|
||||||
|
}.isSuccess
|
||||||
|
if (!infoLoaded) return null
|
||||||
|
val chapters = WebBook.getChapterListAwait(source, newBook)
|
||||||
|
.onFailure {
|
||||||
|
AppLog.put("获取目录出错\n${it.localizedMessage}", it, true)
|
||||||
|
}.getOrNull() ?: return null
|
||||||
|
return newBook to chapters
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Book.applyMigrationTo(
|
||||||
|
newBook: Book,
|
||||||
|
chapters: List<BookChapter>,
|
||||||
|
options: ChangeSourceMigrationOptions,
|
||||||
|
) {
|
||||||
|
newBook.totalChapterNum = chapters.size
|
||||||
|
if (options.migrateReadingProgress && chapters.isNotEmpty()) {
|
||||||
|
newBook.durChapterIndex = BookHelp
|
||||||
|
.getDurChapter(durChapterIndex, durChapterTitle, chapters, totalChapterNum)
|
||||||
|
.coerceIn(0, chapters.lastIndex)
|
||||||
|
newBook.durChapterTitle = chapters[newBook.durChapterIndex].getDisplayTitle(
|
||||||
|
ContentProcessor.get(newBook.name, newBook.origin).getTitleReplaceRules(),
|
||||||
|
getUseReplaceRule()
|
||||||
|
)
|
||||||
|
newBook.durChapterPos = durChapterPos
|
||||||
|
newBook.durChapterTime = durChapterTime
|
||||||
|
} else {
|
||||||
|
newBook.durChapterIndex = 0
|
||||||
|
newBook.durChapterTitle = chapters.firstOrNull()?.getDisplayTitle(
|
||||||
|
ContentProcessor.get(newBook.name, newBook.origin).getTitleReplaceRules(),
|
||||||
|
getUseReplaceRule()
|
||||||
|
)
|
||||||
|
newBook.durChapterPos = 0
|
||||||
|
newBook.durChapterTime = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
if (options.migrateGroup) {
|
||||||
|
newBook.group = group
|
||||||
|
newBook.order = order
|
||||||
|
}
|
||||||
|
if (options.migrateCover) {
|
||||||
|
newBook.customCoverUrl = customCoverUrl
|
||||||
|
}
|
||||||
|
if (options.migrateCategory) {
|
||||||
|
newBook.customTag = customTag
|
||||||
|
}
|
||||||
|
if (options.migrateRemark) {
|
||||||
|
newBook.customIntro = customIntro
|
||||||
|
newBook.remark = remark
|
||||||
|
}
|
||||||
|
newBook.canUpdate = canUpdate
|
||||||
|
if (config.fixedType) {
|
||||||
|
newBook.type = type
|
||||||
|
}
|
||||||
|
if (options.migrateReadConfig) {
|
||||||
|
newBook.readConfig = readConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,16 +4,23 @@ import androidx.compose.ui.focus.focusRequester
|
|||||||
import androidx.activity.compose.BackHandler
|
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.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
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.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
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.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
@@ -23,12 +30,18 @@ import androidx.compose.material.icons.filled.Bookmarks
|
|||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Download
|
import androidx.compose.material.icons.filled.Download
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.SelectAll
|
import androidx.compose.material.icons.filled.SelectAll
|
||||||
|
import androidx.compose.material.icons.filled.SkipNext
|
||||||
import androidx.compose.material.icons.filled.Stop
|
import androidx.compose.material.icons.filled.Stop
|
||||||
import androidx.compose.material.icons.filled.Upload
|
import androidx.compose.material.icons.filled.Upload
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.FloatingActionButtonMenu
|
import androidx.compose.material3.FloatingActionButtonMenu
|
||||||
import androidx.compose.material3.FloatingActionButtonMenuItem
|
import androidx.compose.material3.FloatingActionButtonMenuItem
|
||||||
@@ -57,21 +70,33 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.constant.IntentAction
|
import io.legado.app.constant.IntentAction
|
||||||
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
|
import io.legado.app.data.entities.BookSource
|
||||||
|
import io.legado.app.data.entities.BookSourcePart
|
||||||
|
import io.legado.app.domain.usecase.BatchChangeSourcePreviewItem
|
||||||
|
import io.legado.app.domain.usecase.BatchChangeSourcePreviewStatus
|
||||||
import io.legado.app.help.book.getExportFileName
|
import io.legado.app.help.book.getExportFileName
|
||||||
import io.legado.app.help.book.isLocal
|
import io.legado.app.help.book.isLocal
|
||||||
import io.legado.app.help.book.tryParesExportFileName
|
import io.legado.app.help.book.tryParesExportFileName
|
||||||
import io.legado.app.service.ExportBookService
|
import io.legado.app.service.ExportBookService
|
||||||
import io.legado.app.ui.about.AppLogSheet
|
import io.legado.app.ui.about.AppLogSheet
|
||||||
|
import io.legado.app.ui.book.changesource.ChangeSourceMigrationOptionsSheet
|
||||||
|
import io.legado.app.ui.book.info.BookInfoActivity
|
||||||
|
import io.legado.app.ui.book.info.ChangeSourceSheet
|
||||||
import io.legado.app.ui.book.info.GroupSelectSheet
|
import io.legado.app.ui.book.info.GroupSelectSheet
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.widget.components.AppLinearProgressIndicator
|
import io.legado.app.ui.widget.components.AppLinearProgressIndicator
|
||||||
import io.legado.app.ui.widget.components.AppTextField
|
import io.legado.app.ui.widget.components.AppTextField
|
||||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
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.SmallTonalIconButton
|
||||||
|
import io.legado.app.ui.widget.components.button.SmallTonalTextButton
|
||||||
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.card.NormalCard
|
import io.legado.app.ui.widget.components.card.NormalCard
|
||||||
|
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
|
||||||
|
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||||
import io.legado.app.ui.widget.components.card.TextCard
|
import io.legado.app.ui.widget.components.card.TextCard
|
||||||
|
import io.legado.app.ui.widget.components.cover.CoilBookCover
|
||||||
import io.legado.app.ui.widget.components.divider.PillDivider
|
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.filePicker.FilePickerSheet
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||||
@@ -80,17 +105,22 @@ 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.ListScaffold
|
||||||
import io.legado.app.ui.widget.components.list.ListUiState
|
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.OptionCard
|
||||||
|
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||||
import io.legado.app.ui.widget.components.modalBottomSheet.OptionSheet
|
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.text.AppText
|
||||||
import io.legado.app.utils.ACache
|
import io.legado.app.utils.ACache
|
||||||
import io.legado.app.utils.FileDoc
|
import io.legado.app.utils.FileDoc
|
||||||
import io.legado.app.utils.checkWrite
|
import io.legado.app.utils.checkWrite
|
||||||
import io.legado.app.utils.isContentScheme
|
import io.legado.app.utils.isContentScheme
|
||||||
|
import io.legado.app.utils.move
|
||||||
|
import io.legado.app.utils.startActivity
|
||||||
import io.legado.app.utils.startService
|
import io.legado.app.utils.startService
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import io.legado.app.utils.verificationField
|
import io.legado.app.utils.verificationField
|
||||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||||
|
import io.legado.app.ui.widget.components.button.MediumIconButton
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
|
|
||||||
data class CacheFabAction(
|
data class CacheFabAction(
|
||||||
val icon: androidx.compose.ui.graphics.vector.ImageVector,
|
val icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
@@ -137,6 +167,11 @@ private fun CacheScreen(
|
|||||||
var showGroupSelectSheet by remember { mutableStateOf(false) }
|
var showGroupSelectSheet by remember { mutableStateOf(false) }
|
||||||
var showDeleteBookConfirmDialog by remember { mutableStateOf(false) }
|
var showDeleteBookConfirmDialog by remember { mutableStateOf(false) }
|
||||||
var showCustomExportDialog by remember { mutableStateOf(false) }
|
var showCustomExportDialog by remember { mutableStateOf(false) }
|
||||||
|
var showBatchSourcePickerSheet by remember { mutableStateOf(false) }
|
||||||
|
var pendingBatchSources by remember { mutableStateOf<List<BookSource>>(emptyList()) }
|
||||||
|
var singleChangeSourceBook by remember { mutableStateOf<Book?>(null) }
|
||||||
|
var manualSearchPreviewBook by remember { mutableStateOf<Book?>(null) }
|
||||||
|
var otherSourcePreviewItem by remember { mutableStateOf<BatchChangeSourcePreviewItem?>(null) }
|
||||||
var pendingMoveGroupBookUrl by remember { mutableStateOf<String?>(null) }
|
var pendingMoveGroupBookUrl by remember { mutableStateOf<String?>(null) }
|
||||||
var groupPickerCurrentGroupId by remember { mutableLongStateOf(0L) }
|
var groupPickerCurrentGroupId by remember { mutableLongStateOf(0L) }
|
||||||
var moreMenuBookUrl by remember { mutableStateOf<String?>(null) }
|
var moreMenuBookUrl by remember { mutableStateOf<String?>(null) }
|
||||||
@@ -294,6 +329,11 @@ private fun CacheScreen(
|
|||||||
when (effect) {
|
when (effect) {
|
||||||
is CacheEffect.ShowMessage -> context.toastOnUi(effect.message)
|
is CacheEffect.ShowMessage -> context.toastOnUi(effect.message)
|
||||||
is CacheEffect.NotifyBookChanged -> Unit
|
is CacheEffect.NotifyBookChanged -> Unit
|
||||||
|
is CacheEffect.OpenBookInfo -> context.startActivity<BookInfoActivity> {
|
||||||
|
putExtra("name", effect.name)
|
||||||
|
putExtra("author", effect.author)
|
||||||
|
putExtra("bookUrl", effect.bookUrl)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -379,6 +419,14 @@ private fun CacheScreen(
|
|||||||
showBatchDownloadConfirmDialog = true
|
showBatchDownloadConfirmDialog = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
CacheFabAction(
|
||||||
|
Icons.Default.Refresh,
|
||||||
|
"批量换源"
|
||||||
|
) {
|
||||||
|
if (selectedBookUrls.isNotEmpty()) {
|
||||||
|
showBatchSourcePickerSheet = true
|
||||||
|
}
|
||||||
|
},
|
||||||
CacheFabAction(
|
CacheFabAction(
|
||||||
Icons.Default.Bookmarks,
|
Icons.Default.Bookmarks,
|
||||||
stringResource(R.string.move_to_group)
|
stringResource(R.string.move_to_group)
|
||||||
@@ -722,6 +770,13 @@ private fun CacheScreen(
|
|||||||
expanded = moreMenuBookUrl == book.bookUrl,
|
expanded = moreMenuBookUrl == book.bookUrl,
|
||||||
onDismissRequest = { moreMenuBookUrl = null }
|
onDismissRequest = { moreMenuBookUrl = null }
|
||||||
) { dismiss ->
|
) { dismiss ->
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
text = "换源",
|
||||||
|
onClick = {
|
||||||
|
singleChangeSourceBook = book
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
RoundDropdownMenuItem(
|
RoundDropdownMenuItem(
|
||||||
text = "删除书籍",
|
text = "删除书籍",
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -747,6 +802,144 @@ private fun CacheScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
singleChangeSourceBook?.let { book ->
|
||||||
|
ChangeSourceSheet(
|
||||||
|
show = true,
|
||||||
|
oldBook = book,
|
||||||
|
onDismissRequest = { singleChangeSourceBook = null },
|
||||||
|
onReplace = { source, newBook, toc, options ->
|
||||||
|
viewModel.dispatch(
|
||||||
|
CacheIntent.ChangeBookSource(
|
||||||
|
oldBookUrl = book.bookUrl,
|
||||||
|
source = source,
|
||||||
|
book = newBook,
|
||||||
|
chapters = toc,
|
||||||
|
options = options,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
singleChangeSourceBook = null
|
||||||
|
},
|
||||||
|
onAddAsNew = { _, _ ->
|
||||||
|
context.toastOnUi("请在书籍详情页添加为新书")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BookSourcePickerSheet(
|
||||||
|
show = showBatchSourcePickerSheet,
|
||||||
|
title = "选择目标书源",
|
||||||
|
onDismissRequest = { showBatchSourcePickerSheet = false },
|
||||||
|
onConfirm = { sources ->
|
||||||
|
pendingBatchSources = sources
|
||||||
|
showBatchSourcePickerSheet = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ChangeSourceMigrationOptionsSheet(
|
||||||
|
show = pendingBatchSources.isNotEmpty(),
|
||||||
|
title = "批量换源选项",
|
||||||
|
subtitle = "将对已选 ${selectedBookUrls.size} 本书执行换源;选项只对本次操作生效。",
|
||||||
|
onDismissRequest = { pendingBatchSources = emptyList() },
|
||||||
|
onConfirm = { options ->
|
||||||
|
viewModel.dispatch(
|
||||||
|
CacheIntent.BatchChangeBookSource(
|
||||||
|
bookUrls = selectedBookUrls,
|
||||||
|
sources = pendingBatchSources,
|
||||||
|
options = options,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pendingBatchSources = emptyList()
|
||||||
|
clearSelection()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
BatchChangePreviewSheet(
|
||||||
|
show = state.batchChangePreviewItems.isNotEmpty(),
|
||||||
|
items = state.batchChangePreviewItems,
|
||||||
|
onDismissRequest = { viewModel.dispatch(CacheIntent.DismissBatchChangePreview) },
|
||||||
|
onOpenBook = { book, inBookshelf ->
|
||||||
|
viewModel.dispatch(CacheIntent.OpenBookInfoPreview(book, inBookshelf))
|
||||||
|
},
|
||||||
|
onManualSearch = { book -> manualSearchPreviewBook = book },
|
||||||
|
onSkip = { bookUrl -> viewModel.dispatch(CacheIntent.SkipPreviewItem(bookUrl)) },
|
||||||
|
onMigrate = { bookUrl -> viewModel.dispatch(CacheIntent.MigratePreviewItem(bookUrl)) },
|
||||||
|
onAddToShelf = { bookUrl ->
|
||||||
|
viewModel.dispatch(CacheIntent.AddPreviewItemToShelf(bookUrl))
|
||||||
|
},
|
||||||
|
onShowOtherSources = { item -> otherSourcePreviewItem = item },
|
||||||
|
onMigrateAll = { viewModel.dispatch(CacheIntent.MigrateAllPreviewItems) },
|
||||||
|
)
|
||||||
|
|
||||||
|
manualSearchPreviewBook?.let { book ->
|
||||||
|
ChangeSourceSheet(
|
||||||
|
show = true,
|
||||||
|
oldBook = book,
|
||||||
|
onDismissRequest = { manualSearchPreviewBook = null },
|
||||||
|
onReplace = { source, newBook, toc, _ ->
|
||||||
|
viewModel.dispatch(
|
||||||
|
CacheIntent.UpdatePreviewItem(
|
||||||
|
oldBookUrl = book.bookUrl,
|
||||||
|
source = source,
|
||||||
|
book = newBook,
|
||||||
|
chapters = toc,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
manualSearchPreviewBook = null
|
||||||
|
},
|
||||||
|
onAddAsNew = { _, _ ->
|
||||||
|
context.toastOnUi("请先选择替换候选后再新增至书架")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OtherSourceOptionsSheet(
|
||||||
|
item = otherSourcePreviewItem,
|
||||||
|
onDismissRequest = { otherSourcePreviewItem = null },
|
||||||
|
onSelect = { oldBookUrl, index ->
|
||||||
|
viewModel.dispatch(CacheIntent.SelectPreviewCandidate(oldBookUrl, index))
|
||||||
|
otherSourcePreviewItem = null
|
||||||
|
},
|
||||||
|
onOpenBook = { book ->
|
||||||
|
viewModel.dispatch(CacheIntent.OpenBookInfoPreview(book, false))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AppAlertDialog(
|
||||||
|
show = state.isChangingSource || state.changeSourceError != null,
|
||||||
|
onDismissRequest = {
|
||||||
|
if (!state.isChangingSource) {
|
||||||
|
viewModel.dispatch(CacheIntent.DismissChangeSourceStatus)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = stringResource(R.string.change_source_batch),
|
||||||
|
content = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
if (state.isChangingSource) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
AppText(
|
||||||
|
text = state.changeSourceError ?: state.changeSourceProgress ?: "准备中",
|
||||||
|
style = LegadoTheme.typography.bodyMedium,
|
||||||
|
color = if (state.changeSourceError == null) {
|
||||||
|
LegadoTheme.colorScheme.onSurface
|
||||||
|
} else {
|
||||||
|
LegadoTheme.colorScheme.error
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmText = stringResource(android.R.string.ok),
|
||||||
|
onConfirm = if (state.isChangingSource) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
{ viewModel.dispatch(CacheIntent.DismissChangeSourceStatus) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
FilePickerSheet(
|
FilePickerSheet(
|
||||||
show = showFilePickerSheet,
|
show = showFilePickerSheet,
|
||||||
onDismissRequest = { showFilePickerSheet = false },
|
onDismissRequest = { showFilePickerSheet = false },
|
||||||
@@ -1056,6 +1249,383 @@ private fun CacheScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BookSourcePickerSheet(
|
||||||
|
show: Boolean,
|
||||||
|
title: String,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onConfirm: (List<BookSource>) -> Unit,
|
||||||
|
) {
|
||||||
|
val sources by appDb.bookSourceDao.flowEnabled()
|
||||||
|
.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
var searchKey by rememberSaveable(show) { mutableStateOf("") }
|
||||||
|
var selectedSources by remember(show) { mutableStateOf<List<BookSourcePart>>(emptyList()) }
|
||||||
|
val selectedUrls = remember(selectedSources) {
|
||||||
|
selectedSources.mapTo(hashSetOf()) { it.bookSourceUrl }
|
||||||
|
}
|
||||||
|
val selectedListState = rememberLazyListState()
|
||||||
|
val reorderableState = rememberReorderableLazyListState(selectedListState) { from, to ->
|
||||||
|
selectedSources = selectedSources.toMutableList().apply {
|
||||||
|
move(from.index, to.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val filteredSources = remember(sources, searchKey) {
|
||||||
|
if (searchKey.isBlank()) {
|
||||||
|
sources
|
||||||
|
} else {
|
||||||
|
val key = searchKey.trim()
|
||||||
|
sources.filter { source ->
|
||||||
|
source.bookSourceName.contains(key, true) ||
|
||||||
|
source.bookSourceGroup.orEmpty().contains(key, true) ||
|
||||||
|
source.bookSourceUrl.contains(key, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val availableSources = remember(filteredSources, selectedUrls) {
|
||||||
|
filteredSources.filterNot { selectedUrls.contains(it.bookSourceUrl) }
|
||||||
|
}
|
||||||
|
|
||||||
|
AppModalBottomSheet(
|
||||||
|
show = show,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = title,
|
||||||
|
endAction = {
|
||||||
|
SmallTonalTextButton(
|
||||||
|
text = stringResource(android.R.string.ok),
|
||||||
|
imageVector = Icons.Default.PlayArrow,
|
||||||
|
onClick = {
|
||||||
|
onConfirm(selectedSources.mapNotNull { it.getBookSource() })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
AppTextField(
|
||||||
|
value = searchKey,
|
||||||
|
onValueChange = { searchKey = it },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
label = stringResource(R.string.screen),
|
||||||
|
backgroundColor = LegadoTheme.colorScheme.surface,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
if (selectedSources.isNotEmpty()) {
|
||||||
|
AppText(
|
||||||
|
text = "已选书源(长按拖拽排序)",
|
||||||
|
style = LegadoTheme.typography.labelMedium,
|
||||||
|
color = LegadoTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
LazyColumn(
|
||||||
|
state = selectedListState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(max = 280.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
items(selectedSources, key = { it.bookSourceUrl }) { source ->
|
||||||
|
ReorderableSelectionItem(
|
||||||
|
state = reorderableState,
|
||||||
|
key = source.bookSourceUrl,
|
||||||
|
title = source.bookSourceName,
|
||||||
|
subtitle = source.bookSourceGroup,
|
||||||
|
isSelected = true,
|
||||||
|
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
onToggleSelection = {
|
||||||
|
selectedSources = selectedSources.filterNot {
|
||||||
|
it.bookSourceUrl == source.bookSourceUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
AppText(
|
||||||
|
text = "可选书源",
|
||||||
|
style = LegadoTheme.typography.labelMedium,
|
||||||
|
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(max = 560.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
items(availableSources, key = { it.bookSourceUrl }) { source ->
|
||||||
|
SourcePickerItem(
|
||||||
|
source = source,
|
||||||
|
isSelected = false,
|
||||||
|
onClick = {
|
||||||
|
selectedSources = selectedSources + source
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SourcePickerItem(
|
||||||
|
source: BookSourcePart,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
SelectionItemCard(
|
||||||
|
title = source.bookSourceName,
|
||||||
|
supportingContent = {
|
||||||
|
AppText(
|
||||||
|
text = source.bookSourceGroup.orEmpty().ifBlank { source.bookSourceUrl },
|
||||||
|
style = LegadoTheme.typography.bodySmall,
|
||||||
|
maxLines = 2,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
isSelected = isSelected,
|
||||||
|
onToggleSelection = onClick,
|
||||||
|
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BatchChangePreviewSheet(
|
||||||
|
show: Boolean,
|
||||||
|
items: List<BatchChangeSourcePreviewItem>,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onOpenBook: (Book, Boolean) -> Unit,
|
||||||
|
onManualSearch: (Book) -> Unit,
|
||||||
|
onSkip: (String) -> Unit,
|
||||||
|
onMigrate: (String) -> Unit,
|
||||||
|
onAddToShelf: (String) -> Unit,
|
||||||
|
onShowOtherSources: (BatchChangeSourcePreviewItem) -> Unit,
|
||||||
|
onMigrateAll: () -> Unit,
|
||||||
|
) {
|
||||||
|
AppModalBottomSheet(
|
||||||
|
show = show,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = "批量换源预览",
|
||||||
|
endAction = {
|
||||||
|
SmallTonalTextButton(
|
||||||
|
text = "迁移全部",
|
||||||
|
imageVector = Icons.Default.PlayArrow,
|
||||||
|
onClick = onMigrateAll,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(max = 640.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
items(items, key = { it.oldBook.bookUrl }) { item ->
|
||||||
|
BatchChangePreviewRow(
|
||||||
|
item = item,
|
||||||
|
onOpenBook = onOpenBook,
|
||||||
|
onManualSearch = onManualSearch,
|
||||||
|
onSkip = onSkip,
|
||||||
|
onMigrate = onMigrate,
|
||||||
|
onAddToShelf = onAddToShelf,
|
||||||
|
onShowOtherSources = onShowOtherSources,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun BatchChangePreviewRow(
|
||||||
|
item: BatchChangeSourcePreviewItem,
|
||||||
|
onOpenBook: (Book, Boolean) -> Unit,
|
||||||
|
onManualSearch: (Book) -> Unit,
|
||||||
|
onSkip: (String) -> Unit,
|
||||||
|
onMigrate: (String) -> Unit,
|
||||||
|
onAddToShelf: (String) -> Unit,
|
||||||
|
onShowOtherSources: (BatchChangeSourcePreviewItem) -> Unit,
|
||||||
|
) {
|
||||||
|
val candidate = item.selectedCandidate
|
||||||
|
NormalCard(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
) {
|
||||||
|
PreviewBookInfo(
|
||||||
|
title = "原书籍",
|
||||||
|
book = item.oldBook,
|
||||||
|
chapterCount = item.oldBook.totalChapterNum,
|
||||||
|
onClick = { onOpenBook(item.oldBook, true) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
PreviewBookInfo(
|
||||||
|
title = if (item.status == BatchChangeSourcePreviewStatus.Skipped) {
|
||||||
|
statusText(item.status)
|
||||||
|
} else {
|
||||||
|
candidate?.source?.bookSourceName ?: statusText(item.status)
|
||||||
|
},
|
||||||
|
book = if (item.status == BatchChangeSourcePreviewStatus.Skipped) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
candidate?.book
|
||||||
|
},
|
||||||
|
chapterCount = if (item.status == BatchChangeSourcePreviewStatus.Skipped) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
candidate?.chapters?.size
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
candidate?.book?.let { onOpenBook(it, false) }
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
MediumIconButton(
|
||||||
|
imageVector = Icons.Default.Search,
|
||||||
|
onClick = { onManualSearch(item.oldBook) },
|
||||||
|
)
|
||||||
|
SmallTonalTextButton(
|
||||||
|
text = "不迁移",
|
||||||
|
imageVector = Icons.Default.SkipNext,
|
||||||
|
onClick = { onSkip(item.oldBook.bookUrl) },
|
||||||
|
)
|
||||||
|
SmallTonalTextButton(
|
||||||
|
text = "开始迁移",
|
||||||
|
imageVector = Icons.Default.PlayArrow,
|
||||||
|
onClick = { onMigrate(item.oldBook.bookUrl) },
|
||||||
|
)
|
||||||
|
SmallTonalTextButton(
|
||||||
|
text = "新增至书架",
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
onClick = { onAddToShelf(item.oldBook.bookUrl) },
|
||||||
|
)
|
||||||
|
if (item.candidates.size > 1) {
|
||||||
|
SmallTonalTextButton(
|
||||||
|
text = "查看其他源信息",
|
||||||
|
imageVector = Icons.Default.Info,
|
||||||
|
onClick = { onShowOtherSources(item) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PreviewBookInfo(
|
||||||
|
title: String,
|
||||||
|
book: Book?,
|
||||||
|
chapterCount: Int?,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.clickable(enabled = book != null, onClick = onClick),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
if (book != null) {
|
||||||
|
CoilBookCover(
|
||||||
|
name = book.name,
|
||||||
|
author = book.author,
|
||||||
|
path = book.getDisplayCover(),
|
||||||
|
sourceOrigin = book.origin,
|
||||||
|
modifier = Modifier.width(54.dp),
|
||||||
|
)
|
||||||
|
AppText(
|
||||||
|
text = book.name,
|
||||||
|
style = LegadoTheme.typography.labelMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
AppText(
|
||||||
|
text = "${book.getRealAuthor()} · ${chapterCount ?: 0}章",
|
||||||
|
style = LegadoTheme.typography.labelSmall,
|
||||||
|
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(modifier = Modifier.size(54.dp))
|
||||||
|
AppText(
|
||||||
|
text = title,
|
||||||
|
style = LegadoTheme.typography.labelMedium,
|
||||||
|
color = LegadoTheme.colorScheme.error,
|
||||||
|
maxLines = 2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun OtherSourceOptionsSheet(
|
||||||
|
item: BatchChangeSourcePreviewItem?,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onSelect: (String, Int) -> Unit,
|
||||||
|
onOpenBook: (Book) -> Unit,
|
||||||
|
) {
|
||||||
|
AppModalBottomSheet(
|
||||||
|
show = item != null,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = "其他源信息",
|
||||||
|
) {
|
||||||
|
val currentItem = item
|
||||||
|
if (currentItem != null) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(max = 560.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
items(currentItem.candidates.indices.toList(), key = { it }) { index ->
|
||||||
|
val candidate = currentItem.candidates[index]
|
||||||
|
SelectionItemCard(
|
||||||
|
title = candidate.source.bookSourceName,
|
||||||
|
subtitle = "${candidate.book.name} · ${candidate.chapters.size}章",
|
||||||
|
supportingContent = {
|
||||||
|
AppText(
|
||||||
|
text = candidate.book.getRealAuthor(),
|
||||||
|
style = LegadoTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
isSelected = index == currentItem.selectedCandidateIndex,
|
||||||
|
onToggleSelection = { onSelect(currentItem.oldBook.bookUrl, index) },
|
||||||
|
trailingAction = {
|
||||||
|
SmallTonalIconButton(
|
||||||
|
onClick = { onOpenBook(candidate.book) },
|
||||||
|
imageVector = Icons.Default.Info,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun statusText(status: BatchChangeSourcePreviewStatus): String {
|
||||||
|
return when (status) {
|
||||||
|
BatchChangeSourcePreviewStatus.Matched -> "已匹配"
|
||||||
|
BatchChangeSourcePreviewStatus.NotFound -> "未找到"
|
||||||
|
BatchChangeSourcePreviewStatus.Skipped -> "不迁移"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun startExport(
|
private fun startExport(
|
||||||
context: android.content.Context,
|
context: android.content.Context,
|
||||||
path: String,
|
path: String,
|
||||||
|
|||||||
@@ -6,21 +6,31 @@ import io.legado.app.base.BaseViewModel
|
|||||||
import io.legado.app.data.dao.BookChapterDao
|
import io.legado.app.data.dao.BookChapterDao
|
||||||
import io.legado.app.data.dao.BookDao
|
import io.legado.app.data.dao.BookDao
|
||||||
import io.legado.app.data.dao.BookGroupDao
|
import io.legado.app.data.dao.BookGroupDao
|
||||||
|
import io.legado.app.data.appDb
|
||||||
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.data.entities.BookSource
|
||||||
|
import io.legado.app.constant.BookType
|
||||||
|
import io.legado.app.domain.usecase.BatchChangeSourceCandidate
|
||||||
|
import io.legado.app.domain.usecase.BatchChangeSourcePreviewItem
|
||||||
|
import io.legado.app.domain.usecase.BatchChangeSourcePreviewStatus
|
||||||
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
|
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
|
||||||
import io.legado.app.domain.usecase.CacheBookChaptersUseCase
|
import io.legado.app.domain.usecase.CacheBookChaptersUseCase
|
||||||
|
import io.legado.app.domain.usecase.ChangeBookSourceUseCase
|
||||||
|
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
|
||||||
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
||||||
import io.legado.app.domain.usecase.DeleteBooksUseCase
|
import io.legado.app.domain.usecase.DeleteBooksUseCase
|
||||||
import io.legado.app.domain.usecase.UpdateBooksGroupUseCase
|
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
|
||||||
|
import io.legado.app.help.book.removeType
|
||||||
import io.legado.app.model.CacheBook
|
import io.legado.app.model.CacheBook
|
||||||
import io.legado.app.help.config.LocalConfig
|
import io.legado.app.help.config.LocalConfig
|
||||||
import io.legado.app.service.ExportBookService
|
import io.legado.app.service.ExportBookService
|
||||||
import io.legado.app.ui.config.cacheConfig.CacheConfig
|
import io.legado.app.ui.config.cacheConfig.CacheConfig
|
||||||
|
import io.legado.app.ui.config.otherConfig.OtherConfig
|
||||||
import io.legado.app.ui.main.bookshelf.toLightBook
|
import io.legado.app.ui.main.bookshelf.toLightBook
|
||||||
import io.legado.app.utils.cnCompare
|
import io.legado.app.utils.cnCompare
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -53,6 +63,12 @@ data class CacheUiState(
|
|||||||
val groupList: List<BookGroup> = emptyList(),
|
val groupList: List<BookGroup> = emptyList(),
|
||||||
val books: List<Book> = emptyList(),
|
val books: List<Book> = emptyList(),
|
||||||
val isDownloadRunning: Boolean = false,
|
val isDownloadRunning: Boolean = false,
|
||||||
|
val isChangingSource: Boolean = false,
|
||||||
|
val changeSourceProgress: String? = null,
|
||||||
|
val changeSourceMessage: String? = null,
|
||||||
|
val changeSourceError: String? = null,
|
||||||
|
val batchChangePreviewItems: List<BatchChangeSourcePreviewItem> = emptyList(),
|
||||||
|
val batchChangeOptions: ChangeSourceMigrationOptions = ChangeSourceMigrationOptions(),
|
||||||
val cacheVersion: Long = 0,
|
val cacheVersion: Long = 0,
|
||||||
val deleteBookOriginal: Boolean = LocalConfig.deleteBookOriginal,
|
val deleteBookOriginal: Boolean = LocalConfig.deleteBookOriginal,
|
||||||
val exportConfig: CacheExportConfig = CacheExportConfig()
|
val exportConfig: CacheExportConfig = CacheExportConfig()
|
||||||
@@ -73,6 +89,32 @@ sealed interface CacheIntent {
|
|||||||
data class DeleteBooks(val bookUrls: Set<String>, val deleteOriginal: Boolean) : CacheIntent
|
data class DeleteBooks(val bookUrls: Set<String>, val deleteOriginal: Boolean) : CacheIntent
|
||||||
data class ClearCachesForBooks(val bookUrls: Set<String>) : CacheIntent
|
data class ClearCachesForBooks(val bookUrls: Set<String>) : CacheIntent
|
||||||
data class DownloadBooks(val bookUrls: Set<String>, val downloadAllChapters: Boolean) : CacheIntent
|
data class DownloadBooks(val bookUrls: Set<String>, val downloadAllChapters: Boolean) : CacheIntent
|
||||||
|
data class ChangeBookSource(
|
||||||
|
val oldBookUrl: String,
|
||||||
|
val source: BookSource,
|
||||||
|
val book: Book,
|
||||||
|
val chapters: List<BookChapter>,
|
||||||
|
val options: ChangeSourceMigrationOptions,
|
||||||
|
) : CacheIntent
|
||||||
|
data class BatchChangeBookSource(
|
||||||
|
val bookUrls: Set<String>,
|
||||||
|
val sources: List<BookSource>,
|
||||||
|
val options: ChangeSourceMigrationOptions,
|
||||||
|
) : CacheIntent
|
||||||
|
data class MigratePreviewItem(val oldBookUrl: String) : CacheIntent
|
||||||
|
data class SkipPreviewItem(val oldBookUrl: String) : CacheIntent
|
||||||
|
data class SelectPreviewCandidate(val oldBookUrl: String, val candidateIndex: Int) : CacheIntent
|
||||||
|
data class UpdatePreviewItem(
|
||||||
|
val oldBookUrl: String,
|
||||||
|
val source: BookSource,
|
||||||
|
val book: Book,
|
||||||
|
val chapters: List<BookChapter>,
|
||||||
|
) : CacheIntent
|
||||||
|
data class AddPreviewItemToShelf(val oldBookUrl: String) : CacheIntent
|
||||||
|
data class OpenBookInfoPreview(val book: Book, val inBookshelf: Boolean) : CacheIntent
|
||||||
|
data object MigrateAllPreviewItems : CacheIntent
|
||||||
|
data object DismissChangeSourceStatus : CacheIntent
|
||||||
|
data object DismissBatchChangePreview : CacheIntent
|
||||||
data class SetExportUseReplace(val enabled: Boolean) : CacheIntent
|
data class 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
|
||||||
@@ -88,6 +130,7 @@ sealed interface CacheIntent {
|
|||||||
sealed interface CacheEffect {
|
sealed interface CacheEffect {
|
||||||
data class NotifyBookChanged(val bookUrl: String) : CacheEffect
|
data class NotifyBookChanged(val bookUrl: String) : CacheEffect
|
||||||
data class ShowMessage(val message: String) : CacheEffect
|
data class ShowMessage(val message: String) : CacheEffect
|
||||||
|
data class OpenBookInfo(val bookUrl: String, val name: String, val author: String) : CacheEffect
|
||||||
}
|
}
|
||||||
|
|
||||||
class CacheViewModel(
|
class CacheViewModel(
|
||||||
@@ -98,6 +141,7 @@ class CacheViewModel(
|
|||||||
val cacheConfig: CacheConfig,
|
val cacheConfig: CacheConfig,
|
||||||
private val batchCacheDownloadUseCase: BatchCacheDownloadUseCase,
|
private val batchCacheDownloadUseCase: BatchCacheDownloadUseCase,
|
||||||
private val cacheBookChaptersUseCase: CacheBookChaptersUseCase,
|
private val cacheBookChaptersUseCase: CacheBookChaptersUseCase,
|
||||||
|
private val changeBookSourceUseCase: ChangeBookSourceUseCase,
|
||||||
private val clearBookCacheUseCase: ClearBookCacheUseCase,
|
private val clearBookCacheUseCase: ClearBookCacheUseCase,
|
||||||
private val deleteBooksUseCase: DeleteBooksUseCase,
|
private val deleteBooksUseCase: DeleteBooksUseCase,
|
||||||
private val updateBooksGroupUseCase: UpdateBooksGroupUseCase
|
private val updateBooksGroupUseCase: UpdateBooksGroupUseCase
|
||||||
@@ -131,6 +175,56 @@ class CacheViewModel(
|
|||||||
is CacheIntent.DeleteBooks -> deleteBooks(intent.bookUrls, intent.deleteOriginal)
|
is CacheIntent.DeleteBooks -> deleteBooks(intent.bookUrls, intent.deleteOriginal)
|
||||||
is CacheIntent.ClearCachesForBooks -> clearCachesForBooks(intent.bookUrls)
|
is CacheIntent.ClearCachesForBooks -> clearCachesForBooks(intent.bookUrls)
|
||||||
is CacheIntent.DownloadBooks -> downloadBooks(intent.bookUrls, intent.downloadAllChapters)
|
is CacheIntent.DownloadBooks -> downloadBooks(intent.bookUrls, intent.downloadAllChapters)
|
||||||
|
is CacheIntent.ChangeBookSource -> changeBookSource(
|
||||||
|
intent.oldBookUrl,
|
||||||
|
intent.source,
|
||||||
|
intent.book,
|
||||||
|
intent.chapters,
|
||||||
|
intent.options
|
||||||
|
)
|
||||||
|
|
||||||
|
is CacheIntent.BatchChangeBookSource -> batchChangeBookSource(
|
||||||
|
intent.bookUrls,
|
||||||
|
intent.sources,
|
||||||
|
intent.options
|
||||||
|
)
|
||||||
|
|
||||||
|
is CacheIntent.MigratePreviewItem -> migratePreviewItem(intent.oldBookUrl)
|
||||||
|
is CacheIntent.SkipPreviewItem -> skipPreviewItem(intent.oldBookUrl)
|
||||||
|
is CacheIntent.SelectPreviewCandidate -> selectPreviewCandidate(
|
||||||
|
intent.oldBookUrl,
|
||||||
|
intent.candidateIndex
|
||||||
|
)
|
||||||
|
|
||||||
|
is CacheIntent.UpdatePreviewItem -> updatePreviewItem(
|
||||||
|
intent.oldBookUrl,
|
||||||
|
intent.source,
|
||||||
|
intent.book,
|
||||||
|
intent.chapters
|
||||||
|
)
|
||||||
|
|
||||||
|
is CacheIntent.AddPreviewItemToShelf -> addPreviewItemToShelf(intent.oldBookUrl)
|
||||||
|
is CacheIntent.OpenBookInfoPreview -> openBookInfoPreview(
|
||||||
|
intent.book,
|
||||||
|
intent.inBookshelf
|
||||||
|
)
|
||||||
|
|
||||||
|
CacheIntent.MigrateAllPreviewItems -> migrateAllPreviewItems()
|
||||||
|
|
||||||
|
CacheIntent.DismissChangeSourceStatus -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
changeSourceProgress = null,
|
||||||
|
changeSourceMessage = null,
|
||||||
|
changeSourceError = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheIntent.DismissBatchChangePreview -> {
|
||||||
|
_uiState.update { it.copy(batchChangePreviewItems = emptyList()) }
|
||||||
|
}
|
||||||
|
|
||||||
is CacheIntent.SetExportUseReplace -> {
|
is CacheIntent.SetExportUseReplace -> {
|
||||||
cacheConfig.exportUseReplace = intent.enabled
|
cacheConfig.exportUseReplace = intent.enabled
|
||||||
syncExportConfig()
|
syncExportConfig()
|
||||||
@@ -422,6 +516,259 @@ class CacheViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun changeBookSource(
|
||||||
|
oldBookUrl: String,
|
||||||
|
source: BookSource,
|
||||||
|
book: Book,
|
||||||
|
chapters: List<BookChapter>,
|
||||||
|
options: ChangeSourceMigrationOptions,
|
||||||
|
) {
|
||||||
|
execute {
|
||||||
|
val oldBook = bookDao.getBook(oldBookUrl) ?: return@execute null
|
||||||
|
changeBookSourceUseCase.changeTo(oldBook, book, chapters, options)
|
||||||
|
}.onSuccess { result ->
|
||||||
|
result ?: return@onSuccess
|
||||||
|
cacheChapters.remove(result.oldBookUrl)
|
||||||
|
cacheChapters[result.book.bookUrl] = hashSetOf()
|
||||||
|
emitBookChanged(result.book.bookUrl)
|
||||||
|
_effects.tryEmit(CacheEffect.ShowMessage("换源完成"))
|
||||||
|
}.onError {
|
||||||
|
_effects.tryEmit(CacheEffect.ShowMessage("换源失败\n${it.localizedMessage}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun batchChangeBookSource(
|
||||||
|
bookUrls: Set<String>,
|
||||||
|
sources: List<BookSource>,
|
||||||
|
options: ChangeSourceMigrationOptions,
|
||||||
|
) {
|
||||||
|
if (bookUrls.isEmpty()) {
|
||||||
|
_uiState.update { it.copy(changeSourceError = "未选择书籍") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (sources.isEmpty()) {
|
||||||
|
_uiState.update { it.copy(changeSourceError = "未选择书源") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
execute {
|
||||||
|
val concurrency = OtherConfig.threadCount.coerceIn(1, 4)
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isChangingSource = true,
|
||||||
|
changeSourceProgress = "0 / ${bookUrls.size}",
|
||||||
|
changeSourceMessage = "开始查找:${bookUrls.size} 本,${sources.size} 个书源,并发 $concurrency",
|
||||||
|
changeSourceError = null,
|
||||||
|
batchChangeOptions = options,
|
||||||
|
batchChangePreviewItems = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val books = bookUrls.mapNotNull { bookDao.getBook(it) }
|
||||||
|
changeBookSourceUseCase.prepareBatchChange(
|
||||||
|
books = books,
|
||||||
|
sources = sources,
|
||||||
|
concurrency = concurrency,
|
||||||
|
) { current, total, bookName ->
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(changeSourceProgress = "$current / $total $bookName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onSuccess { previewItems ->
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
batchChangePreviewItems = previewItems,
|
||||||
|
isChangingSource = false,
|
||||||
|
changeSourceProgress = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val matchedCount = previewItems.count { it.canMigrate }
|
||||||
|
val skippedCount = previewItems.count {
|
||||||
|
it.status == BatchChangeSourcePreviewStatus.Skipped
|
||||||
|
}
|
||||||
|
val notFoundCount = previewItems.size - matchedCount - skippedCount
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
changeSourceMessage = "查找完成:可迁移 $matchedCount 本,未找到 $notFoundCount 本,跳过 $skippedCount 本",
|
||||||
|
changeSourceError = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.onError {
|
||||||
|
val progress = uiState.value.changeSourceProgress.orEmpty()
|
||||||
|
_uiState.update { state ->
|
||||||
|
state.copy(
|
||||||
|
changeSourceError = "批量换源查找失败${if (progress.isBlank()) "" else "\n进度:$progress"}\n${it.localizedMessage}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.onFinally {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isChangingSource = false,
|
||||||
|
changeSourceProgress = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migratePreviewItem(oldBookUrl: String) {
|
||||||
|
val item = uiState.value.batchChangePreviewItems.firstOrNull {
|
||||||
|
it.oldBook.bookUrl == oldBookUrl
|
||||||
|
} ?: return
|
||||||
|
val candidate = item.selectedCandidate ?: return
|
||||||
|
execute {
|
||||||
|
val oldBook = bookDao.getBook(oldBookUrl) ?: item.oldBook
|
||||||
|
changeBookSourceUseCase.changeTo(
|
||||||
|
oldBook = oldBook,
|
||||||
|
newBook = candidate.book,
|
||||||
|
chapters = candidate.chapters,
|
||||||
|
options = uiState.value.batchChangeOptions,
|
||||||
|
)
|
||||||
|
}.onSuccess { result ->
|
||||||
|
cacheChapters.remove(result.oldBookUrl)
|
||||||
|
cacheChapters[result.book.bookUrl] = hashSetOf()
|
||||||
|
removePreviewItem(oldBookUrl)
|
||||||
|
emitBookChanged(result.book.bookUrl)
|
||||||
|
_effects.tryEmit(CacheEffect.ShowMessage("迁移完成"))
|
||||||
|
}.onError {
|
||||||
|
_effects.tryEmit(CacheEffect.ShowMessage("迁移失败\n${it.localizedMessage}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun skipPreviewItem(oldBookUrl: String) {
|
||||||
|
_uiState.update { state ->
|
||||||
|
state.copy(
|
||||||
|
batchChangePreviewItems = state.batchChangePreviewItems.map { item ->
|
||||||
|
if (item.oldBook.bookUrl == oldBookUrl) {
|
||||||
|
item.copy(status = BatchChangeSourcePreviewStatus.Skipped)
|
||||||
|
} else {
|
||||||
|
item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectPreviewCandidate(oldBookUrl: String, candidateIndex: Int) {
|
||||||
|
_uiState.update { state ->
|
||||||
|
state.copy(
|
||||||
|
batchChangePreviewItems = state.batchChangePreviewItems.map { item ->
|
||||||
|
if (item.oldBook.bookUrl == oldBookUrl) {
|
||||||
|
item.copy(
|
||||||
|
selectedCandidateIndex = candidateIndex.coerceIn(
|
||||||
|
0,
|
||||||
|
(item.candidates.size - 1).coerceAtLeast(0)
|
||||||
|
),
|
||||||
|
status = BatchChangeSourcePreviewStatus.Matched
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePreviewItem(
|
||||||
|
oldBookUrl: String,
|
||||||
|
source: BookSource,
|
||||||
|
book: Book,
|
||||||
|
chapters: List<BookChapter>,
|
||||||
|
) {
|
||||||
|
_uiState.update { state ->
|
||||||
|
state.copy(
|
||||||
|
batchChangePreviewItems = state.batchChangePreviewItems.map { item ->
|
||||||
|
if (item.oldBook.bookUrl == oldBookUrl) {
|
||||||
|
item.copy(
|
||||||
|
candidates = listOf(BatchChangeSourceCandidate(source, book, chapters)) +
|
||||||
|
item.candidates,
|
||||||
|
selectedCandidateIndex = 0,
|
||||||
|
status = BatchChangeSourcePreviewStatus.Matched
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addPreviewItemToShelf(oldBookUrl: String) {
|
||||||
|
val item = uiState.value.batchChangePreviewItems.firstOrNull {
|
||||||
|
it.oldBook.bookUrl == oldBookUrl
|
||||||
|
} ?: return
|
||||||
|
val candidate = item.selectedCandidate ?: return
|
||||||
|
execute {
|
||||||
|
candidate.book.removeType(BookType.notShelf)
|
||||||
|
if (candidate.book.order == 0) {
|
||||||
|
candidate.book.order = bookDao.minOrder - 1
|
||||||
|
}
|
||||||
|
bookDao.insert(candidate.book)
|
||||||
|
bookChapterDao.insert(*candidate.chapters.toTypedArray())
|
||||||
|
candidate.book
|
||||||
|
}.onSuccess {
|
||||||
|
_effects.tryEmit(CacheEffect.ShowMessage("已添加到书架"))
|
||||||
|
}.onError {
|
||||||
|
_effects.tryEmit(CacheEffect.ShowMessage("添加书籍失败\n${it.localizedMessage}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openBookInfoPreview(book: Book, inBookshelf: Boolean) {
|
||||||
|
execute {
|
||||||
|
if (!inBookshelf) {
|
||||||
|
appDb.searchBookDao.insert(book.toSearchBook())
|
||||||
|
}
|
||||||
|
book
|
||||||
|
}.onSuccess {
|
||||||
|
_effects.tryEmit(CacheEffect.OpenBookInfo(it.bookUrl, it.name, it.author))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removePreviewItem(oldBookUrl: String) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
batchChangePreviewItems = it.batchChangePreviewItems.filterNot { item ->
|
||||||
|
item.oldBook.bookUrl == oldBookUrl
|
||||||
|
},
|
||||||
|
cacheVersion = it.cacheVersion + 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrateAllPreviewItems() {
|
||||||
|
val items = uiState.value.batchChangePreviewItems.filter { it.canMigrate }
|
||||||
|
if (items.isEmpty()) return
|
||||||
|
execute {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(isChangingSource = true, changeSourceProgress = "0 / ${items.size}")
|
||||||
|
}
|
||||||
|
items.forEachIndexed { index, item ->
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(changeSourceProgress = "${index + 1} / ${items.size} ${item.oldBook.name}")
|
||||||
|
}
|
||||||
|
val candidate = item.selectedCandidate ?: return@forEachIndexed
|
||||||
|
val oldBook = bookDao.getBook(item.oldBook.bookUrl) ?: item.oldBook
|
||||||
|
changeBookSourceUseCase.changeTo(
|
||||||
|
oldBook = oldBook,
|
||||||
|
newBook = candidate.book,
|
||||||
|
chapters = candidate.chapters,
|
||||||
|
options = uiState.value.batchChangeOptions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.onSuccess {
|
||||||
|
cacheChapters.clear()
|
||||||
|
_uiState.update { it.copy(batchChangePreviewItems = emptyList()) }
|
||||||
|
_effects.tryEmit(CacheEffect.ShowMessage("批量迁移完成"))
|
||||||
|
}.onError {
|
||||||
|
_effects.tryEmit(CacheEffect.ShowMessage("批量迁移失败\n${it.localizedMessage}"))
|
||||||
|
}.onFinally {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isChangingSource = false,
|
||||||
|
changeSourceProgress = null,
|
||||||
|
cacheVersion = it.cacheVersion + 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun clearCacheForBook(book: Book) {
|
private fun clearCacheForBook(book: Book) {
|
||||||
execute {
|
execute {
|
||||||
clearBookCacheUseCase.execute(book.bookUrl)
|
clearBookCacheUseCase.execute(book.bookUrl)
|
||||||
|
|||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
package io.legado.app.ui.book.changesource
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
|
||||||
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
|
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
|
||||||
|
import io.legado.app.ui.widget.components.checkBox.CheckboxItem
|
||||||
|
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||||
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChangeSourceMigrationOptionsSheet(
|
||||||
|
show: Boolean,
|
||||||
|
title: String,
|
||||||
|
subtitle: String? = null,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onConfirm: (ChangeSourceMigrationOptions) -> Unit,
|
||||||
|
) {
|
||||||
|
var migrateReadingProgress by rememberSaveable(show) { mutableStateOf(true) }
|
||||||
|
var migrateGroup by rememberSaveable(show) { mutableStateOf(true) }
|
||||||
|
var migrateCover by rememberSaveable(show) { mutableStateOf(true) }
|
||||||
|
var migrateCategory by rememberSaveable(show) { mutableStateOf(true) }
|
||||||
|
var migrateRemark by rememberSaveable(show) { mutableStateOf(true) }
|
||||||
|
var migrateReadConfig by rememberSaveable(show) { mutableStateOf(true) }
|
||||||
|
var deleteDownloadedChapters by rememberSaveable(show) { mutableStateOf(false) }
|
||||||
|
|
||||||
|
AppModalBottomSheet(
|
||||||
|
show = show,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = title,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
subtitle?.takeIf { it.isNotBlank() }?.let {
|
||||||
|
AppText(
|
||||||
|
text = it,
|
||||||
|
style = LegadoTheme.typography.bodySmall,
|
||||||
|
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CheckboxItem("阅读进度", checked = migrateReadingProgress) {
|
||||||
|
migrateReadingProgress = it
|
||||||
|
}
|
||||||
|
if (migrateReadingProgress) {
|
||||||
|
AppText(
|
||||||
|
text = "若新源总章节比进度更少,阅读进度将调整至最后一章。",
|
||||||
|
style = LegadoTheme.typography.bodySmall,
|
||||||
|
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CheckboxItem("分组和排序", checked = migrateGroup) { migrateGroup = it }
|
||||||
|
CheckboxItem("自定义封面", checked = migrateCover) { migrateCover = it }
|
||||||
|
CheckboxItem("分类与标签", checked = migrateCategory) { migrateCategory = it }
|
||||||
|
CheckboxItem("备注和自定义简介", checked = migrateRemark) { migrateRemark = it }
|
||||||
|
CheckboxItem("阅读设置", checked = migrateReadConfig) { migrateReadConfig = it }
|
||||||
|
CheckboxItem("删除已下载章节", checked = deleteDownloadedChapters) {
|
||||||
|
deleteDownloadedChapters = it
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
ConfirmDismissButtonsRow(
|
||||||
|
onDismiss = onDismissRequest,
|
||||||
|
onConfirm = {
|
||||||
|
onConfirm(
|
||||||
|
ChangeSourceMigrationOptions(
|
||||||
|
migrateChapters = true,
|
||||||
|
migrateReadingProgress = migrateReadingProgress,
|
||||||
|
migrateGroup = migrateGroup,
|
||||||
|
migrateCover = migrateCover,
|
||||||
|
migrateCategory = migrateCategory,
|
||||||
|
migrateRemark = migrateRemark,
|
||||||
|
migrateReadConfig = migrateReadConfig,
|
||||||
|
deleteDownloadedChapters = deleteDownloadedChapters,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
dismissText = stringResource(R.string.cancel),
|
||||||
|
confirmText = stringResource(android.R.string.ok),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ 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.BookSource
|
import io.legado.app.data.entities.BookSource
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecordTimelineDay
|
import io.legado.app.data.entities.readRecord.ReadRecordTimelineDay
|
||||||
|
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
|
||||||
|
|
||||||
data class BookInfoUiState(
|
data class BookInfoUiState(
|
||||||
val book: Book? = null,
|
val book: Book? = null,
|
||||||
@@ -85,6 +86,7 @@ sealed interface BookInfoIntent {
|
|||||||
val source: BookSource,
|
val source: BookSource,
|
||||||
val book: Book,
|
val book: Book,
|
||||||
val toc: List<BookChapter>,
|
val toc: List<BookChapter>,
|
||||||
|
val options: ChangeSourceMigrationOptions,
|
||||||
) : BookInfoIntent
|
) : BookInfoIntent
|
||||||
data class AddSourceAsNewBook(
|
data class AddSourceAsNewBook(
|
||||||
val book: Book,
|
val book: Book,
|
||||||
|
|||||||
@@ -274,8 +274,8 @@ private fun BookInfoScreenContent(
|
|||||||
show = currentSheet == BookInfoSheet.SourcePicker,
|
show = currentSheet == BookInfoSheet.SourcePicker,
|
||||||
oldBook = book,
|
oldBook = book,
|
||||||
onDismissRequest = { onIntent(BookInfoIntent.DismissSheet) },
|
onDismissRequest = { onIntent(BookInfoIntent.DismissSheet) },
|
||||||
onReplace = { source, newBook, toc ->
|
onReplace = { source, newBook, toc, options ->
|
||||||
onIntent(BookInfoIntent.ReplaceWithSource(source, newBook, toc))
|
onIntent(BookInfoIntent.ReplaceWithSource(source, newBook, toc, options))
|
||||||
},
|
},
|
||||||
onAddAsNew = { newBook, toc ->
|
onAddAsNew = { newBook, toc ->
|
||||||
onIntent(BookInfoIntent.AddSourceAsNewBook(newBook, toc))
|
onIntent(BookInfoIntent.AddSourceAsNewBook(newBook, toc))
|
||||||
|
|||||||
@@ -54,8 +54,11 @@ 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.data.entities.BookSource
|
import io.legado.app.data.entities.BookSource
|
||||||
import io.legado.app.data.entities.SearchBook
|
import io.legado.app.data.entities.SearchBook
|
||||||
|
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
|
||||||
|
import io.legado.app.help.book.isSameNameAuthor
|
||||||
import io.legado.app.ui.book.changecover.ChangeCoverViewModel
|
import io.legado.app.ui.book.changecover.ChangeCoverViewModel
|
||||||
import io.legado.app.ui.book.changesource.ChangeBookSourceComposeViewModel
|
import io.legado.app.ui.book.changesource.ChangeBookSourceComposeViewModel
|
||||||
|
import io.legado.app.ui.book.changesource.ChangeSourceMigrationOptionsSheet
|
||||||
import io.legado.app.ui.book.group.GroupEditSheet
|
import io.legado.app.ui.book.group.GroupEditSheet
|
||||||
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||||
import io.legado.app.ui.book.source.manage.BookSourceActivity
|
import io.legado.app.ui.book.source.manage.BookSourceActivity
|
||||||
@@ -236,7 +239,7 @@ fun ChangeSourceSheet(
|
|||||||
show: Boolean,
|
show: Boolean,
|
||||||
oldBook: Book,
|
oldBook: Book,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onReplace: (BookSource, Book, List<BookChapter>) -> Unit,
|
onReplace: (BookSource, Book, List<BookChapter>, ChangeSourceMigrationOptions) -> Unit,
|
||||||
onAddAsNew: (Book, List<BookChapter>) -> Unit,
|
onAddAsNew: (Book, List<BookChapter>) -> Unit,
|
||||||
viewModel: ChangeBookSourceComposeViewModel = koinViewModel(key = "source-${oldBook.bookUrl}"),
|
viewModel: ChangeBookSourceComposeViewModel = koinViewModel(key = "source-${oldBook.bookUrl}"),
|
||||||
) {
|
) {
|
||||||
@@ -253,6 +256,7 @@ fun ChangeSourceSheet(
|
|||||||
val loadWordCount = viewModel.loadWordCount
|
val loadWordCount = viewModel.loadWordCount
|
||||||
var actionBook by remember { mutableStateOf<SearchBook?>(null) }
|
var actionBook by remember { mutableStateOf<SearchBook?>(null) }
|
||||||
var mismatchBook by remember { mutableStateOf<SearchBook?>(null) }
|
var mismatchBook by remember { mutableStateOf<SearchBook?>(null) }
|
||||||
|
var pendingMigration by remember { mutableStateOf<PendingSourceMigration?>(null) }
|
||||||
var loadingAction by remember { mutableStateOf(false) }
|
var loadingAction by remember { mutableStateOf(false) }
|
||||||
var showOptionsMenu by rememberSaveable { mutableStateOf(false) }
|
var showOptionsMenu by rememberSaveable { mutableStateOf(false) }
|
||||||
var showFilterMenu by rememberSaveable { mutableStateOf(false) }
|
var showFilterMenu by rememberSaveable { mutableStateOf(false) }
|
||||||
@@ -311,7 +315,7 @@ fun ChangeSourceSheet(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
RoundDropdownMenuItem(
|
RoundDropdownMenuItem(
|
||||||
text = "字数对比",
|
text = "显示更多信息",
|
||||||
isSelected = loadWordCount,
|
isSelected = loadWordCount,
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.onLoadWordCountChange(!loadWordCount)
|
viewModel.onLoadWordCountChange(!loadWordCount)
|
||||||
@@ -464,7 +468,7 @@ fun ChangeSourceSheet(
|
|||||||
viewModel.del(item)
|
viewModel.del(item)
|
||||||
if (oldBook.bookUrl == item.bookUrl) {
|
if (oldBook.bookUrl == item.bookUrl) {
|
||||||
viewModel.autoChangeSource(oldBook.type) { book, toc, source ->
|
viewModel.autoChangeSource(oldBook.type) { book, toc, source ->
|
||||||
onReplace(source, book, toc)
|
pendingMigration = PendingSourceMigration(source, book, toc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onDismiss()
|
onDismiss()
|
||||||
@@ -483,8 +487,7 @@ fun ChangeSourceSheet(
|
|||||||
viewModel.getToc(book, { toc, source ->
|
viewModel.getToc(book, { toc, source ->
|
||||||
loadingAction = false
|
loadingAction = false
|
||||||
if (replace) {
|
if (replace) {
|
||||||
onReplace(source, book, toc)
|
pendingMigration = PendingSourceMigration(source, book, toc)
|
||||||
onDismissRequest()
|
|
||||||
} else {
|
} else {
|
||||||
onAddAsNew(book, toc)
|
onAddAsNew(book, toc)
|
||||||
context.toastOnUi(context.getString(R.string.book_added_to_shelf))
|
context.toastOnUi(context.getString(R.string.book_added_to_shelf))
|
||||||
@@ -527,4 +530,30 @@ fun ChangeSourceSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
val migration = pendingMigration
|
||||||
|
ChangeSourceMigrationOptionsSheet(
|
||||||
|
show = migration != null,
|
||||||
|
title = "换源选项",
|
||||||
|
subtitle = migration?.let {
|
||||||
|
val sameNameAuthor = oldBook.isSameNameAuthor(it.book)
|
||||||
|
if (sameNameAuthor && oldBook.origin != it.book.origin) {
|
||||||
|
"检测到书名、作者相同但书源不同,可选择本次要迁移的数据。"
|
||||||
|
} else {
|
||||||
|
"选择本次替换当前书籍时要迁移的数据。"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onDismissRequest = { pendingMigration = null },
|
||||||
|
onConfirm = { options ->
|
||||||
|
val pending = pendingMigration ?: return@ChangeSourceMigrationOptionsSheet
|
||||||
|
onReplace(pending.source, pending.book, pending.toc, options)
|
||||||
|
pendingMigration = null
|
||||||
|
onDismissRequest()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class PendingSourceMigration(
|
||||||
|
val source: BookSource,
|
||||||
|
val book: Book,
|
||||||
|
val toc: List<BookChapter>,
|
||||||
|
)
|
||||||
@@ -16,6 +16,8 @@ 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.BookSource
|
import io.legado.app.data.entities.BookSource
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecordTimelineDay
|
import io.legado.app.data.entities.readRecord.ReadRecordTimelineDay
|
||||||
|
import io.legado.app.domain.usecase.ChangeBookSourceUseCase
|
||||||
|
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
|
||||||
import io.legado.app.data.repository.ReadRecordRepository
|
import io.legado.app.data.repository.ReadRecordRepository
|
||||||
import io.legado.app.data.repository.RemoteBookRepository
|
import io.legado.app.data.repository.RemoteBookRepository
|
||||||
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
||||||
@@ -65,6 +67,7 @@ class BookInfoViewModel(
|
|||||||
application: Application,
|
application: Application,
|
||||||
private val remoteBookRepository: RemoteBookRepository,
|
private val remoteBookRepository: RemoteBookRepository,
|
||||||
private val readRecordRepository: ReadRecordRepository,
|
private val readRecordRepository: ReadRecordRepository,
|
||||||
|
private val changeBookSourceUseCase: ChangeBookSourceUseCase,
|
||||||
private val clearBookCacheUseCase: ClearBookCacheUseCase
|
private val clearBookCacheUseCase: ClearBookCacheUseCase
|
||||||
) : BaseViewModel(application) {
|
) : BaseViewModel(application) {
|
||||||
|
|
||||||
@@ -171,7 +174,7 @@ class BookInfoViewModel(
|
|||||||
|
|
||||||
is BookInfoIntent.ReplaceWithSource -> {
|
is BookInfoIntent.ReplaceWithSource -> {
|
||||||
dismissSheet()
|
dismissSheet()
|
||||||
changeTo(intent.source, intent.book, intent.toc)
|
changeTo(intent.source, intent.book, intent.toc, intent.options)
|
||||||
}
|
}
|
||||||
|
|
||||||
is BookInfoIntent.AddSourceAsNewBook -> {
|
is BookInfoIntent.AddSourceAsNewBook -> {
|
||||||
@@ -605,16 +608,20 @@ class BookInfoViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>) {
|
fun changeTo(
|
||||||
|
source: BookSource,
|
||||||
|
book: Book,
|
||||||
|
toc: List<BookChapter>,
|
||||||
|
options: ChangeSourceMigrationOptions,
|
||||||
|
) {
|
||||||
changeSourceCoroutine?.cancel()
|
changeSourceCoroutine?.cancel()
|
||||||
changeSourceCoroutine = execute {
|
changeSourceCoroutine = execute {
|
||||||
|
val oldBook = currentBook ?: return@execute book
|
||||||
bookSource = source
|
bookSource = source
|
||||||
currentBook?.migrateTo(book, toc)
|
|
||||||
if (inBookshelf) {
|
if (inBookshelf) {
|
||||||
book.removeType(BookType.updateError)
|
changeBookSourceUseCase.changeTo(oldBook, book, toc, options)
|
||||||
currentBook?.delete()
|
} else {
|
||||||
appDb.bookDao.insert(book)
|
changeBookSourceUseCase.applyMigration(oldBook, book, toc, options)
|
||||||
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
|
||||||
}
|
}
|
||||||
book
|
book
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
|
|||||||
Reference in new issue
Block a user