fix: 阅读时换源与并发数量
This commit is contained in:
@@ -17,13 +17,17 @@ import io.legado.app.ui.config.otherConfig.OtherConfig
|
||||
import io.legado.app.utils.internString
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
sealed interface ChangeSourceSearchEvent {
|
||||
data object Started : ChangeSourceSearchEvent
|
||||
@@ -50,8 +54,9 @@ class ChangeSourceSearchUseCase(
|
||||
// Shared state for TOC cache
|
||||
private val tocMap = ConcurrentHashMap<String, List<BookChapter>>()
|
||||
private val bookMap = ConcurrentHashMap<String, Book>()
|
||||
private var tocMapChapterCount = 0
|
||||
private val tocMapChapterCount = AtomicInteger(0)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun search(
|
||||
name: String,
|
||||
author: String,
|
||||
@@ -64,45 +69,64 @@ class ChangeSourceSearchUseCase(
|
||||
if (bookSourceParts.isEmpty()) {
|
||||
throw io.legado.app.exception.NoStackTraceException("启用书源为空")
|
||||
}
|
||||
val sources = bookSourceParts.mapNotNull { it.getBookSource() }
|
||||
if (sources.isEmpty()) {
|
||||
throw io.legado.app.exception.NoStackTraceException("启用书源为空")
|
||||
}
|
||||
|
||||
tocMap.clear()
|
||||
bookMap.clear()
|
||||
tocMapChapterCount = 0
|
||||
tocMapChapterCount.set(0)
|
||||
|
||||
emit(ChangeSourceSearchEvent.Started)
|
||||
|
||||
var processedSources = 0
|
||||
val totalSources = bookSourceParts.size
|
||||
var resultCount = 0
|
||||
val totalSources = sources.size
|
||||
val concurrency = threadCount.coerceAtLeast(1)
|
||||
|
||||
for (bs in bookSourceParts) {
|
||||
currentCoroutineContext().ensureActive()
|
||||
val source = bs.getBookSource() ?: continue
|
||||
try {
|
||||
withTimeout(60000L) {
|
||||
searchSource(
|
||||
source, name, author, oldBook, fromReadBookActivity,
|
||||
contentProcessor
|
||||
)
|
||||
}.forEach { searchBook ->
|
||||
sources.asFlow()
|
||||
.flatMapMerge(concurrency) { source ->
|
||||
flow {
|
||||
val books = try {
|
||||
withTimeout(60000L) {
|
||||
searchSource(
|
||||
source, name, author, oldBook, fromReadBookActivity,
|
||||
contentProcessor
|
||||
)
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
currentCoroutineContext().ensureActive()
|
||||
emptyList()
|
||||
}
|
||||
emit(ChangeSourceResult(source, books))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
}
|
||||
.collect { result ->
|
||||
currentCoroutineContext().ensureActive()
|
||||
result.books.forEach { searchBook ->
|
||||
resultCount++
|
||||
emit(ChangeSourceSearchEvent.Result(searchBook))
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}
|
||||
processedSources++
|
||||
emit(
|
||||
ChangeSourceSearchEvent.Progress(
|
||||
processedSources = processedSources,
|
||||
totalSources = totalSources,
|
||||
resultCount = 0,
|
||||
sourceName = source.bookSourceName,
|
||||
processedSources++
|
||||
emit(
|
||||
ChangeSourceSearchEvent.Progress(
|
||||
processedSources = processedSources,
|
||||
totalSources = totalSources,
|
||||
resultCount = resultCount,
|
||||
sourceName = result.source.bookSourceName,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
emit(ChangeSourceSearchEvent.Finished(isEmpty = true))
|
||||
emit(ChangeSourceSearchEvent.Finished(isEmpty = resultCount == 0))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
private data class ChangeSourceResult(
|
||||
val source: BookSource,
|
||||
val books: List<SearchBook>,
|
||||
)
|
||||
|
||||
private suspend fun searchSource(
|
||||
source: BookSource,
|
||||
name: String,
|
||||
@@ -191,8 +215,8 @@ class ChangeSourceSearchUseCase(
|
||||
for (chapter in chapters) {
|
||||
chapter.internString()
|
||||
}
|
||||
if (tocMapChapterCount < 30000) {
|
||||
tocMapChapterCount += chapters.size
|
||||
if (tocMapChapterCount.get() < 30000) {
|
||||
tocMapChapterCount.addAndGet(chapters.size)
|
||||
tocMap[book.primaryStr()] = chapters
|
||||
}
|
||||
bookMap[book.primaryStr()] = book
|
||||
|
||||
+15
-3
@@ -11,6 +11,7 @@ import io.legado.app.data.repository.SearchRepository
|
||||
import io.legado.app.domain.usecase.ChangeSourceSearchEvent
|
||||
import io.legado.app.domain.usecase.ChangeSourceSearchUseCase
|
||||
import io.legado.app.domain.usecase.GetChapterContentUseCase
|
||||
import io.legado.app.help.book.isWebFile
|
||||
import io.legado.app.help.book.primaryStr
|
||||
import io.legado.app.ui.book.search.SearchScope
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -57,8 +58,8 @@ class ChangeBookSourceComposeViewModel(
|
||||
private val _searchDataFlow = MutableStateFlow<List<SearchBook>>(emptyList())
|
||||
val searchDataFlow: StateFlow<List<SearchBook>> = _searchDataFlow.asStateFlow()
|
||||
|
||||
val totalSourceCount: Int
|
||||
get() = searchResults.size
|
||||
var totalSourceCount: Int = 0
|
||||
private set
|
||||
|
||||
fun getBookFromMap(key: String): Book? = bookMap[key]?.toBook()
|
||||
|
||||
@@ -71,6 +72,7 @@ class ChangeBookSourceComposeViewModel(
|
||||
// Internal state
|
||||
private var searchJob: Job? = null
|
||||
private var oldBook: Book? = null
|
||||
private var fromReadBookActivity: Boolean = false
|
||||
private var screenKey: String = ""
|
||||
private val searchResults = mutableListOf<SearchBook>()
|
||||
private val bookMap = mutableMapOf<String, SearchBook>()
|
||||
@@ -84,6 +86,7 @@ class ChangeBookSourceComposeViewModel(
|
||||
|
||||
fun initData(name: String, author: String, book: Book, fromReadBookActivity: Boolean) {
|
||||
this.oldBook = book
|
||||
this.fromReadBookActivity = fromReadBookActivity
|
||||
if (searchJob?.isActive != true) {
|
||||
startSearch()
|
||||
}
|
||||
@@ -95,6 +98,8 @@ class ChangeBookSourceComposeViewModel(
|
||||
searchResults.clear()
|
||||
bookMap.clear()
|
||||
tocMap.clear()
|
||||
totalSourceCount = 0
|
||||
_changeSourceProgress.value = 0 to ""
|
||||
_searchDataFlow.value = emptyList()
|
||||
|
||||
searchJob = viewModelScope.launch {
|
||||
@@ -103,7 +108,7 @@ class ChangeBookSourceComposeViewModel(
|
||||
author = book.author,
|
||||
scope = SearchScope(ChangeSourceConfig.searchScope),
|
||||
oldBook = book,
|
||||
fromReadBookActivity = false,
|
||||
fromReadBookActivity = fromReadBookActivity,
|
||||
).collect { event ->
|
||||
when (event) {
|
||||
is ChangeSourceSearchEvent.Started -> {
|
||||
@@ -111,6 +116,7 @@ class ChangeBookSourceComposeViewModel(
|
||||
}
|
||||
|
||||
is ChangeSourceSearchEvent.Progress -> {
|
||||
totalSourceCount = event.totalSources
|
||||
_changeSourceProgress.value = event.processedSources to event.sourceName
|
||||
}
|
||||
|
||||
@@ -196,6 +202,12 @@ class ChangeBookSourceComposeViewModel(
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
if (book.isWebFile) {
|
||||
val source = io.legado.app.data.appDb.bookSourceDao.getBookSource(book.origin)
|
||||
?: throw io.legado.app.exception.NoStackTraceException("书源不存在")
|
||||
onSuccess(emptyList(), source)
|
||||
return@launch
|
||||
}
|
||||
val (toc, source) = getChapterContentUseCase.getToc(book)
|
||||
tocMap[book.primaryStr()] = toc
|
||||
onSuccess(toc, source)
|
||||
|
||||
@@ -89,6 +89,7 @@ import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.button.series.SmallTonalButton
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.card.TextCard
|
||||
import io.legado.app.ui.widget.components.changeSource.ChangeSourceSheet
|
||||
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||
import io.legado.app.ui.widget.components.image.cover.BookCoverImage
|
||||
import io.legado.app.ui.widget.components.image.cover.CoilBookCover
|
||||
|
||||
@@ -76,6 +76,7 @@ import io.legado.app.ui.widget.components.button.series.MediumPlainButton
|
||||
import io.legado.app.ui.widget.components.button.series.SmallPlainButton
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||
import io.legado.app.ui.widget.components.changeSource.ChangeSourceSheet
|
||||
import io.legado.app.ui.widget.components.checkBox.AppCheckbox
|
||||
import io.legado.app.ui.widget.components.image.cover.CoilBookCover
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||
@@ -303,356 +304,3 @@ fun ChangeSourceSheet(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChangeSourceSheet(
|
||||
show: Boolean,
|
||||
oldBook: Book,
|
||||
onDismissRequest: () -> Unit,
|
||||
onReplace: (BookSource, Book, List<BookChapter>, ChangeSourceMigrationOptions) -> Unit,
|
||||
onAddAsNew: (Book, List<BookChapter>) -> Unit,
|
||||
viewModel: ChangeBookSourceComposeViewModel = koinViewModel(key = "source-${oldBook.bookUrl}"),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val items by viewModel.searchDataFlow.collectAsStateWithLifecycle(initialValue = emptyList<SearchBook>())
|
||||
val isSearching by viewModel.isSearching.collectAsStateWithLifecycle()
|
||||
val progress by viewModel.changeSourceProgress.collectAsStateWithLifecycle()
|
||||
val groups by viewModel.enabledGroups.collectAsStateWithLifecycle(initialValue = emptyList<String>())
|
||||
val enabledSources by viewModel.enabledSources.collectAsStateWithLifecycle(initialValue = emptyList<io.legado.app.data.entities.BookSourcePart>())
|
||||
val scopeState by viewModel.scopeUiState.collectAsStateWithLifecycle()
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
val checkAuthor = viewModel.checkAuthor
|
||||
val loadInfo = viewModel.loadInfo
|
||||
val loadToc = viewModel.loadToc
|
||||
val loadWordCount = viewModel.loadWordCount
|
||||
var actionBook by remember { mutableStateOf<SearchBook?>(null) }
|
||||
var mismatchBook by remember { mutableStateOf<SearchBook?>(null) }
|
||||
var showMigrationOptions by remember { mutableStateOf(false) }
|
||||
var loadingAction by remember { mutableStateOf(false) }
|
||||
var showOptionsMenu by rememberSaveable { mutableStateOf(false) }
|
||||
var showFilterSheet by rememberSaveable { mutableStateOf(false) }
|
||||
val bookAddedToShelfText = stringResource(R.string.book_added_to_shelf)
|
||||
|
||||
val editSourceResult = rememberLauncherForActivityResult(StartActivityContract(BookSourceEditActivity::class.java)) {
|
||||
val origin = it.data?.getStringExtra("origin") ?: return@rememberLauncherForActivityResult
|
||||
viewModel.startSearch(origin)
|
||||
}
|
||||
|
||||
LaunchedEffect(oldBook.bookUrl) {
|
||||
viewModel.initData(oldBook.name, oldBook.author, oldBook, false)
|
||||
}
|
||||
|
||||
DisposableEffect(lifecycleOwner, viewModel) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_RESUME -> viewModel.resume()
|
||||
Lifecycle.Event.ON_PAUSE -> viewModel.pause()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(oldBook.bookUrl) {
|
||||
onDispose {
|
||||
viewModel.stopSearch()
|
||||
}
|
||||
}
|
||||
|
||||
AppModalBottomSheet(
|
||||
show = show,
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = stringResource(R.string.book_source),
|
||||
startAction = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Box {
|
||||
MediumPlainButton(
|
||||
onClick = { showOptionsMenu = true },
|
||||
icon = Icons.Default.MoreVert
|
||||
)
|
||||
RoundDropdownMenu(
|
||||
expanded = showOptionsMenu,
|
||||
onDismissRequest = { showOptionsMenu = false }
|
||||
) { dismiss ->
|
||||
RoundDropdownMenuItem(
|
||||
text = "校验作者",
|
||||
isSelected = checkAuthor,
|
||||
onClick = {
|
||||
viewModel.onCheckAuthorChange(!checkAuthor)
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = "加载详情",
|
||||
isSelected = loadInfo,
|
||||
onClick = {
|
||||
viewModel.onLoadInfoChange(!loadInfo)
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = "加载目录",
|
||||
isSelected = loadToc,
|
||||
onClick = {
|
||||
viewModel.onLoadTocChange(!loadToc)
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = "显示更多信息",
|
||||
isSelected = loadWordCount,
|
||||
onClick = {
|
||||
viewModel.onLoadWordCountChange(!loadWordCount)
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.book_source_manage),
|
||||
onClick = {
|
||||
context.startActivity<BookSourceActivity>()
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
MediumPlainButton(
|
||||
onClick = { showMigrationOptions = true },
|
||||
icon = Icons.Outlined.Settings
|
||||
)
|
||||
}
|
||||
},
|
||||
endAction = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
MediumPlainButton(
|
||||
onClick = { viewModel.startOrStopSearch() },
|
||||
icon = if (isSearching) Icons.Default.PauseCircleOutline else Icons.Default.Refresh,
|
||||
)
|
||||
MediumPlainButton(
|
||||
onClick = { showFilterSheet = true },
|
||||
icon = Icons.Default.FilterList
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
AppTextField(
|
||||
value = searchQuery,
|
||||
backgroundColor = LegadoTheme.colorScheme.surface,
|
||||
onValueChange = {
|
||||
searchQuery = it
|
||||
viewModel.screen(it)
|
||||
},
|
||||
label = stringResource(R.string.screen),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
if (isSearching) {
|
||||
AppLinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
AppText(
|
||||
text = "${progress.first} / ${viewModel.totalSourceCount} · ${items.size}",
|
||||
style = LegadoTheme.typography.bodySmall
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
|
||||
if (items.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 40.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
EmptyMessage(
|
||||
message = stringResource(R.string.search_empty)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(items, key = { it.bookUrl + it.origin }) { item ->
|
||||
val bookScore by remember(item.origin, item.name, item.author) {
|
||||
viewModel.bookScoreFlow(item)
|
||||
}.collectAsStateWithLifecycle()
|
||||
SelectionItemCard(
|
||||
title = item.originName,
|
||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
selectedContainerColor = LegadoTheme.colorScheme.primaryContainer.copy(alpha = 0.32f),
|
||||
leadingContent = {
|
||||
MediumPlainButton(
|
||||
onClick = {
|
||||
viewModel.onBookScoreClick(item)
|
||||
},
|
||||
icon = Icons.Default.PushPin,
|
||||
tint = if (bookScore > 0) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.outline,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
AppText(
|
||||
text = item.author,
|
||||
style = LegadoTheme.typography.labelLargeEmphasized
|
||||
)
|
||||
AppText(
|
||||
text = item.getDisplayLastChapterTitle(),
|
||||
style = LegadoTheme.typography.labelMediumEmphasized
|
||||
)
|
||||
item.chapterWordCountText?.takeIf { loadWordCount }?.let {
|
||||
AppText(
|
||||
text = it,
|
||||
style = LegadoTheme.typography.labelSmallEmphasized,
|
||||
color = LegadoTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
isSelected = item.bookUrl == oldBook.bookUrl,
|
||||
onToggleSelection = {
|
||||
if (item.bookUrl != oldBook.bookUrl) {
|
||||
if (!item.sameBookTypeLocal(oldBook.type)) mismatchBook = item else actionBook =
|
||||
item
|
||||
}
|
||||
},
|
||||
dropdownContent = { onDismiss: () -> Unit ->
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.to_top),
|
||||
onClick = {
|
||||
viewModel.topSource(item)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = "置底",
|
||||
onClick = {
|
||||
viewModel.bottomSource(item)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.edit),
|
||||
onClick = {
|
||||
onDismiss()
|
||||
editSourceResult.launch { putExtra("sourceUrl", item.origin) }
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = "禁用",
|
||||
onClick = {
|
||||
viewModel.disableSource(item)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.delete),
|
||||
color = LegadoTheme.colorScheme.error,
|
||||
onClick = {
|
||||
viewModel.del(item)
|
||||
if (oldBook.bookUrl == item.bookUrl) {
|
||||
viewModel.autoChangeSource(oldBook.type) { book, toc, source ->
|
||||
onReplace(
|
||||
source,
|
||||
book,
|
||||
toc,
|
||||
ChangeSourceConfig.getMigrationOptions()
|
||||
)
|
||||
}
|
||||
}
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
val performAction: (SearchBook, Boolean) -> Unit = { searchBook, replace ->
|
||||
loadingAction = true
|
||||
val book = viewModel.getBookFromMap(searchBook.primaryStr()) ?: searchBook.toBook()
|
||||
viewModel.getToc(
|
||||
book,
|
||||
onSuccess = { toc, source ->
|
||||
loadingAction = false
|
||||
if (replace) {
|
||||
onReplace(source, book, toc, ChangeSourceConfig.getMigrationOptions())
|
||||
onDismissRequest()
|
||||
} else {
|
||||
onAddAsNew(book, toc)
|
||||
context.toastOnUi(bookAddedToShelfText)
|
||||
}
|
||||
actionBook = null
|
||||
},
|
||||
onError = {
|
||||
loadingAction = false
|
||||
context.toastOnUi(if (replace) "换源失败" else "添加书籍失败")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AppAlertDialog(
|
||||
data = mismatchBook,
|
||||
onDismissRequest = { mismatchBook = null },
|
||||
title = stringResource(R.string.book_type_different),
|
||||
text = stringResource(R.string.soure_change_source),
|
||||
confirmText = stringResource(android.R.string.ok),
|
||||
onConfirm = { searchBook ->
|
||||
actionBook = searchBook
|
||||
mismatchBook = null
|
||||
},
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = { mismatchBook = null }
|
||||
)
|
||||
AppAlertDialog(
|
||||
data = actionBook,
|
||||
onDismissRequest = { actionBook = null },
|
||||
title = stringResource(R.string.change_source_option_title),
|
||||
dismissText = stringResource(R.string.add_as_new_book),
|
||||
onDismiss = { actionBook?.let { performAction(it, false) } },
|
||||
confirmText = stringResource(R.string.replace_current_book),
|
||||
onConfirm = { performAction(it, true) }
|
||||
)
|
||||
AppAlertDialog(
|
||||
show = loadingAction,
|
||||
onDismissRequest = {},
|
||||
content = {
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
AppCircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
)
|
||||
ChangeSourceMigrationOptionsSheet(
|
||||
show = showMigrationOptions,
|
||||
title = "换源选项",
|
||||
onDismissRequest = { showMigrationOptions = false },
|
||||
onConfirm = { options ->
|
||||
ChangeSourceConfig.setMigrationOptions(options)
|
||||
showMigrationOptions = false
|
||||
}
|
||||
)
|
||||
|
||||
ScopeSelectSheet(
|
||||
show = showFilterSheet,
|
||||
onDismissRequest = { showFilterSheet = false },
|
||||
isAll = scopeState.isAll,
|
||||
onSelectAll = { viewModel.selectAllScope() },
|
||||
groups = groups,
|
||||
selectedGroups = scopeState.displayNames,
|
||||
onToggleGroup = { viewModel.toggleScopeGroup(it) },
|
||||
sources = enabledSources,
|
||||
selectedSources = scopeState.sourceUrls,
|
||||
onToggleSource = { viewModel.toggleScopeSource(it) },
|
||||
isSourceScope = scopeState.isSource,
|
||||
onConfirm = {
|
||||
viewModel.startSearch()
|
||||
showFilterSheet = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -260,6 +260,7 @@ sealed interface ReadBookIntent {
|
||||
data object ToggleTranslation : ReadBookIntent
|
||||
|
||||
// Change source
|
||||
data class ChangeSourceBook(val book: Book) : ReadBookIntent
|
||||
data class ChangeSource(val book: Book, val toc: List<BookChapter>) : ReadBookIntent
|
||||
data class AddSourceAsNewBook(val book: Book, val toc: List<BookChapter>) : ReadBookIntent
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import io.legado.app.R
|
||||
import io.legado.app.ui.book.info.ChangeSourceSheet
|
||||
import io.legado.app.ui.widget.components.log.AppLogSheet
|
||||
import io.legado.app.ui.book.read.sheet.BgTextConfigSheet
|
||||
import io.legado.app.ui.book.read.sheet.ChangeChapterSourceSheet
|
||||
@@ -36,6 +35,7 @@ import io.legado.app.ui.book.read.sheet.TitleBarIconSheet
|
||||
import io.legado.app.ui.book.read.sheet.ToolButtonConfigSheet
|
||||
import io.legado.app.ui.book.read.sheet.UnderlineConfigSheet
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.changeSource.ChangeSourceSheet
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
||||
@@ -397,11 +397,17 @@ fun ReadBookScreen(
|
||||
ChangeSourceSheet(
|
||||
show = true,
|
||||
oldBook = book,
|
||||
fromReadBookActivity = true,
|
||||
allowAddAsNew = false,
|
||||
dismissOnReplaceStart = true,
|
||||
onDismissRequest = { onIntent(ReadBookIntent.DismissSheet) },
|
||||
onReplace = { _, newBook, toc, _ ->
|
||||
onIntent(ReadBookIntent.DismissSheet)
|
||||
onIntent(ReadBookIntent.ChangeSource(newBook, toc))
|
||||
},
|
||||
onReplaceBook = { newBook ->
|
||||
onIntent(ReadBookIntent.ChangeSourceBook(newBook))
|
||||
},
|
||||
onAddAsNew = { newBook, toc ->
|
||||
onIntent(ReadBookIntent.DismissSheet)
|
||||
onIntent(ReadBookIntent.AddSourceAsNewBook(newBook, toc))
|
||||
|
||||
@@ -259,6 +259,7 @@ class ReadBookViewModel(
|
||||
is ReadBookIntent.RefreshContentAfter -> refreshContentAfter()
|
||||
is ReadBookIntent.ChangeReplaceRule -> changeReplaceRule(intent.enabled)
|
||||
is ReadBookIntent.ToggleTranslation -> toggleTranslation()
|
||||
is ReadBookIntent.ChangeSourceBook -> changeTo(intent.book)
|
||||
is ReadBookIntent.ChangeSource -> changeTo(intent.book, intent.toc)
|
||||
is ReadBookIntent.AddSourceAsNewBook -> addToBookshelf(intent.book, intent.toc)
|
||||
is ReadBookIntent.OpenChapterResult -> openChapter(intent.index, intent.chapterPos)
|
||||
@@ -1810,14 +1811,7 @@ class ReadBookViewModel(
|
||||
changeSourceCoroutine?.cancel()
|
||||
changeSourceCoroutine = execute {
|
||||
ReadBook.upMsg(context.getString(R.string.loading))
|
||||
ReadBook.book?.migrateTo(book, toc)
|
||||
book.removeType(BookType.updateError)
|
||||
ReadBook.book?.delete()
|
||||
appDb.bookDao.insert(book)
|
||||
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
||||
ReadBook.resetData(book)
|
||||
ReadBook.upMsg(null)
|
||||
ReadBook.loadContent(resetPageOffset = true)
|
||||
applyChangeSource(book, toc)
|
||||
}.onError {
|
||||
AppLog.put("换源失败\n$it", it, true)
|
||||
ReadBook.upMsg(null)
|
||||
@@ -1826,6 +1820,39 @@ class ReadBookViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeTo(book: Book) {
|
||||
changeSourceCoroutine?.cancel()
|
||||
changeSourceCoroutine = execute {
|
||||
ReadBook.upMsg(context.getString(R.string.loading))
|
||||
val source = appDb.bookSourceDao.getBookSource(book.origin)
|
||||
?: throw NoStackTraceException("书源不存在")
|
||||
if (book.tocUrl.isEmpty()) {
|
||||
WebBook.getBookInfoAwait(source, book)
|
||||
}
|
||||
val toc = WebBook.getChapterListAwait(source, book).getOrThrow()
|
||||
applyChangeSource(book, toc)
|
||||
}.onError {
|
||||
AppLog.put("换源失败\n$it", it, true)
|
||||
ReadBook.upMsg(null)
|
||||
}.onFinally {
|
||||
postEvent(EventBus.SOURCE_CHANGED, book.bookUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun applyChangeSource(book: Book, toc: List<BookChapter>) {
|
||||
if (toc.isEmpty()) {
|
||||
throw NoStackTraceException("换源目录为空")
|
||||
}
|
||||
ReadBook.book?.migrateTo(book, toc)
|
||||
book.removeType(BookType.updateError)
|
||||
ReadBook.book?.delete()
|
||||
appDb.bookDao.insert(book)
|
||||
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
||||
ReadBook.resetData(book)
|
||||
ReadBook.upMsg(null)
|
||||
ReadBook.loadContent(resetPageOffset = true)
|
||||
}
|
||||
|
||||
private fun autoChangeSource(name: String, author: String) {
|
||||
if (!AppConfig.autoChangeSource) return
|
||||
execute {
|
||||
|
||||
+446
@@ -0,0 +1,446 @@
|
||||
package io.legado.app.ui.widget.components.changeSource
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.PauseCircleOutline
|
||||
import androidx.compose.material.icons.filled.PushPin
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import io.legado.app.R
|
||||
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.data.entities.SearchBook
|
||||
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
|
||||
import io.legado.app.ui.book.changesource.ChangeBookSourceComposeViewModel
|
||||
import io.legado.app.ui.book.changesource.ChangeSourceConfig
|
||||
import io.legado.app.ui.book.changesource.ChangeSourceMigrationOptionsSheet
|
||||
import io.legado.app.ui.book.search.ScopeSelectSheet
|
||||
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||
import io.legado.app.ui.book.source.manage.BookSourceActivity
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.button.series.MediumPlainButton
|
||||
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||
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.modalBottomSheet.AppModalBottomSheet
|
||||
import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
|
||||
import io.legado.app.ui.widget.components.progressIndicator.AppLinearProgressIndicator
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.utils.StartActivityContract
|
||||
import io.legado.app.utils.startActivity
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun ChangeSourceSheet(
|
||||
show: Boolean,
|
||||
oldBook: Book,
|
||||
fromReadBookActivity: Boolean = false,
|
||||
allowAddAsNew: Boolean = true,
|
||||
dismissOnReplaceStart: Boolean = false,
|
||||
onDismissRequest: () -> Unit,
|
||||
onReplace: (BookSource, Book, List<BookChapter>, ChangeSourceMigrationOptions) -> Unit,
|
||||
onReplaceBook: ((Book) -> Unit)? = null,
|
||||
onAddAsNew: (Book, List<BookChapter>) -> Unit,
|
||||
viewModel: ChangeBookSourceComposeViewModel = koinViewModel(key = "source-${oldBook.bookUrl}"),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val items by viewModel.searchDataFlow.collectAsStateWithLifecycle(initialValue = emptyList<SearchBook>())
|
||||
val isSearching by viewModel.isSearching.collectAsStateWithLifecycle()
|
||||
val progress by viewModel.changeSourceProgress.collectAsStateWithLifecycle()
|
||||
val groups by viewModel.enabledGroups.collectAsStateWithLifecycle(initialValue = emptyList<String>())
|
||||
val enabledSources by viewModel.enabledSources.collectAsStateWithLifecycle(initialValue = emptyList<io.legado.app.data.entities.BookSourcePart>())
|
||||
val scopeState by viewModel.scopeUiState.collectAsStateWithLifecycle()
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
val checkAuthor = viewModel.checkAuthor
|
||||
val loadInfo = viewModel.loadInfo
|
||||
val loadToc = viewModel.loadToc
|
||||
val loadWordCount = viewModel.loadWordCount
|
||||
var actionBook by remember { mutableStateOf<SearchBook?>(null) }
|
||||
var mismatchBook by remember { mutableStateOf<SearchBook?>(null) }
|
||||
var showMigrationOptions by remember { mutableStateOf(false) }
|
||||
var loadingAction by remember { mutableStateOf(false) }
|
||||
var showOptionsMenu by rememberSaveable { mutableStateOf(false) }
|
||||
var showFilterSheet by rememberSaveable { mutableStateOf(false) }
|
||||
val bookAddedToShelfText = stringResource(R.string.book_added_to_shelf)
|
||||
|
||||
val editSourceResult = rememberLauncherForActivityResult(StartActivityContract(BookSourceEditActivity::class.java)) {
|
||||
val origin = it.data?.getStringExtra("origin") ?: return@rememberLauncherForActivityResult
|
||||
viewModel.startSearch(origin)
|
||||
}
|
||||
|
||||
LaunchedEffect(oldBook.bookUrl, fromReadBookActivity) {
|
||||
viewModel.initData(oldBook.name, oldBook.author, oldBook, fromReadBookActivity)
|
||||
}
|
||||
|
||||
DisposableEffect(lifecycleOwner, viewModel) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_RESUME -> viewModel.resume()
|
||||
Lifecycle.Event.ON_PAUSE -> viewModel.pause()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(oldBook.bookUrl) {
|
||||
onDispose {
|
||||
viewModel.stopSearch()
|
||||
}
|
||||
}
|
||||
|
||||
val performAction = fun(searchBook: SearchBook, replace: Boolean) {
|
||||
val book = viewModel.getBookFromMap(searchBook.primaryStr()) ?: searchBook.toBook()
|
||||
if (replace && dismissOnReplaceStart && onReplaceBook != null) {
|
||||
onDismissRequest()
|
||||
onReplaceBook(book)
|
||||
actionBook = null
|
||||
return
|
||||
}
|
||||
val dismissBeforeLoading = replace && dismissOnReplaceStart
|
||||
if (dismissBeforeLoading) {
|
||||
onDismissRequest()
|
||||
} else {
|
||||
loadingAction = true
|
||||
}
|
||||
viewModel.getToc(
|
||||
book,
|
||||
onSuccess = { toc, source ->
|
||||
loadingAction = false
|
||||
if (replace) {
|
||||
onReplace(source, book, toc, ChangeSourceConfig.getMigrationOptions())
|
||||
if (!dismissBeforeLoading) {
|
||||
onDismissRequest()
|
||||
}
|
||||
} else {
|
||||
onAddAsNew(book, toc)
|
||||
context.toastOnUi(bookAddedToShelfText)
|
||||
}
|
||||
actionBook = null
|
||||
},
|
||||
onError = {
|
||||
loadingAction = false
|
||||
context.toastOnUi(if (replace) "换源失败" else "添加书籍失败")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AppModalBottomSheet(
|
||||
show = show,
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = stringResource(R.string.book_source),
|
||||
startAction = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Box {
|
||||
MediumPlainButton(
|
||||
onClick = { showOptionsMenu = true },
|
||||
icon = Icons.Default.MoreVert
|
||||
)
|
||||
RoundDropdownMenu(
|
||||
expanded = showOptionsMenu,
|
||||
onDismissRequest = { showOptionsMenu = false }
|
||||
) { dismiss ->
|
||||
RoundDropdownMenuItem(
|
||||
text = "校验作者",
|
||||
isSelected = checkAuthor,
|
||||
onClick = {
|
||||
viewModel.onCheckAuthorChange(!checkAuthor)
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = "加载详情",
|
||||
isSelected = loadInfo,
|
||||
onClick = {
|
||||
viewModel.onLoadInfoChange(!loadInfo)
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = "加载目录",
|
||||
isSelected = loadToc,
|
||||
onClick = {
|
||||
viewModel.onLoadTocChange(!loadToc)
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = "显示更多信息",
|
||||
isSelected = loadWordCount,
|
||||
onClick = {
|
||||
viewModel.onLoadWordCountChange(!loadWordCount)
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.book_source_manage),
|
||||
onClick = {
|
||||
context.startActivity<BookSourceActivity>()
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
MediumPlainButton(
|
||||
onClick = { showMigrationOptions = true },
|
||||
icon = Icons.Outlined.Settings
|
||||
)
|
||||
}
|
||||
},
|
||||
endAction = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
MediumPlainButton(
|
||||
onClick = { viewModel.startOrStopSearch() },
|
||||
icon = if (isSearching) Icons.Default.PauseCircleOutline else Icons.Default.Refresh,
|
||||
)
|
||||
MediumPlainButton(
|
||||
onClick = { showFilterSheet = true },
|
||||
icon = Icons.Default.FilterList
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
AppTextField(
|
||||
value = searchQuery,
|
||||
backgroundColor = LegadoTheme.colorScheme.surface,
|
||||
onValueChange = {
|
||||
searchQuery = it
|
||||
viewModel.screen(it)
|
||||
},
|
||||
label = stringResource(R.string.screen),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
if (isSearching) {
|
||||
AppLinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
AppText(
|
||||
text = "${progress.first} / ${viewModel.totalSourceCount} · ${items.size}",
|
||||
style = LegadoTheme.typography.bodySmall
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
|
||||
if (items.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 40.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
EmptyMessage(
|
||||
message = stringResource(R.string.search_empty)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(items, key = { it.bookUrl + it.origin }) { item ->
|
||||
val bookScore by remember(item.origin, item.name, item.author) {
|
||||
viewModel.bookScoreFlow(item)
|
||||
}.collectAsStateWithLifecycle()
|
||||
SelectionItemCard(
|
||||
title = item.originName,
|
||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
selectedContainerColor = LegadoTheme.colorScheme.primaryContainer.copy(alpha = 0.32f),
|
||||
leadingContent = {
|
||||
MediumPlainButton(
|
||||
onClick = {
|
||||
viewModel.onBookScoreClick(item)
|
||||
},
|
||||
icon = Icons.Default.PushPin,
|
||||
tint = if (bookScore > 0) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.outline,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
AppText(
|
||||
text = item.author,
|
||||
style = LegadoTheme.typography.labelLargeEmphasized
|
||||
)
|
||||
AppText(
|
||||
text = item.getDisplayLastChapterTitle(),
|
||||
style = LegadoTheme.typography.labelMediumEmphasized
|
||||
)
|
||||
item.chapterWordCountText?.takeIf { loadWordCount }?.let {
|
||||
AppText(
|
||||
text = it,
|
||||
style = LegadoTheme.typography.labelSmallEmphasized,
|
||||
color = LegadoTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
isSelected = item.bookUrl == oldBook.bookUrl,
|
||||
onToggleSelection = {
|
||||
if (item.bookUrl != oldBook.bookUrl) {
|
||||
if (!item.sameBookTypeLocal(oldBook.type)) {
|
||||
mismatchBook = item
|
||||
} else if (allowAddAsNew) {
|
||||
actionBook = item
|
||||
} else {
|
||||
performAction(item, true)
|
||||
}
|
||||
}
|
||||
},
|
||||
dropdownContent = { onDismiss: () -> Unit ->
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.to_top),
|
||||
onClick = {
|
||||
viewModel.topSource(item)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = "置底",
|
||||
onClick = {
|
||||
viewModel.bottomSource(item)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.edit),
|
||||
onClick = {
|
||||
onDismiss()
|
||||
editSourceResult.launch { putExtra("sourceUrl", item.origin) }
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = "禁用",
|
||||
onClick = {
|
||||
viewModel.disableSource(item)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.delete),
|
||||
color = LegadoTheme.colorScheme.error,
|
||||
onClick = {
|
||||
viewModel.del(item)
|
||||
if (oldBook.bookUrl == item.bookUrl) {
|
||||
viewModel.autoChangeSource(oldBook.type) { book, toc, source ->
|
||||
onReplace(
|
||||
source,
|
||||
book,
|
||||
toc,
|
||||
ChangeSourceConfig.getMigrationOptions()
|
||||
)
|
||||
}
|
||||
}
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
AppAlertDialog(
|
||||
data = mismatchBook,
|
||||
onDismissRequest = { mismatchBook = null },
|
||||
title = stringResource(R.string.book_type_different),
|
||||
text = stringResource(R.string.soure_change_source),
|
||||
confirmText = stringResource(android.R.string.ok),
|
||||
onConfirm = { searchBook ->
|
||||
mismatchBook = null
|
||||
if (allowAddAsNew) {
|
||||
actionBook = searchBook
|
||||
} else {
|
||||
performAction(searchBook, true)
|
||||
}
|
||||
},
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = { mismatchBook = null }
|
||||
)
|
||||
if (allowAddAsNew) {
|
||||
AppAlertDialog(
|
||||
data = actionBook,
|
||||
onDismissRequest = { actionBook = null },
|
||||
title = stringResource(R.string.change_source_option_title),
|
||||
dismissText = stringResource(R.string.add_as_new_book),
|
||||
onDismiss = { actionBook?.let { performAction(it, false) } },
|
||||
confirmText = stringResource(R.string.replace_current_book),
|
||||
onConfirm = { performAction(it, true) }
|
||||
)
|
||||
}
|
||||
AppAlertDialog(
|
||||
show = loadingAction,
|
||||
onDismissRequest = {},
|
||||
content = {
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
AppCircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
)
|
||||
ChangeSourceMigrationOptionsSheet(
|
||||
show = showMigrationOptions,
|
||||
title = "换源选项",
|
||||
onDismissRequest = { showMigrationOptions = false },
|
||||
onConfirm = { options ->
|
||||
ChangeSourceConfig.setMigrationOptions(options)
|
||||
showMigrationOptions = false
|
||||
}
|
||||
)
|
||||
|
||||
ScopeSelectSheet(
|
||||
show = showFilterSheet,
|
||||
onDismissRequest = { showFilterSheet = false },
|
||||
isAll = scopeState.isAll,
|
||||
onSelectAll = { viewModel.selectAllScope() },
|
||||
groups = groups,
|
||||
selectedGroups = scopeState.displayNames,
|
||||
onToggleGroup = { viewModel.toggleScopeGroup(it) },
|
||||
sources = enabledSources,
|
||||
selectedSources = scopeState.sourceUrls,
|
||||
onToggleSource = { viewModel.toggleScopeSource(it) },
|
||||
isSourceScope = scopeState.isSource,
|
||||
onConfirm = {
|
||||
viewModel.startSearch()
|
||||
showFilterSheet = false
|
||||
}
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user