换源时,不会再因为打开浏览器而导致重走换源流程
换源界面新增多选分组和书源功能
This commit is contained in:
13 files changed
+508
-210
No files matched your search
@@ -49,7 +49,8 @@
|
|||||||
"PowerShell(.\\\\gradlew.bat test 2>&1 | Select-Object -Last 10)",
|
"PowerShell(.\\\\gradlew.bat test 2>&1 | Select-Object -Last 10)",
|
||||||
"Bash(npx skills *)",
|
"Bash(npx skills *)",
|
||||||
"Bash(npx *)",
|
"Bash(npx *)",
|
||||||
"PowerShell(.\\\\gradlew.bat :app:compileAppDebugKotlin 2>&1 | Select-Object -Last 20)"
|
"PowerShell(.\\\\gradlew.bat :app:compileAppDebugKotlin 2>&1 | Select-Object -Last 20)",
|
||||||
|
"Bash(cmd /c \"gradlew.bat :app:compileAppDebugKotlin\")"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,6 +172,16 @@ interface BookSourceDao {
|
|||||||
)
|
)
|
||||||
fun getEnabledPartByGroup(group: String): List<BookSourcePart>
|
fun getEnabledPartByGroup(group: String): List<BookSourcePart>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""select * from book_sources_part
|
||||||
|
where bookSourceGroup = :group
|
||||||
|
or bookSourceGroup like :group || ',%'
|
||||||
|
or bookSourceGroup like '%,' || :group
|
||||||
|
or bookSourceGroup like '%,' || :group || ',%'
|
||||||
|
order by customOrder asc"""
|
||||||
|
)
|
||||||
|
fun getPartByGroup(group: String): List<BookSourcePart>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""select * from book_sources
|
"""select * from book_sources
|
||||||
where bookUrlPattern != 'NONE' and bookSourceType = :type order by customOrder asc"""
|
where bookUrlPattern != 'NONE' and bookSourceType = :type order by customOrder asc"""
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ class BookGroupRepository(private val bookGroupDao: BookGroupDao) {
|
|||||||
return bookGroupDao.flowAll()
|
return bookGroupDao.flowAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun flowSelect(): Flow<List<BookGroup>> {
|
||||||
|
return bookGroupDao.flowSelect()
|
||||||
|
}
|
||||||
|
|
||||||
fun flowShow(): Flow<List<BookGroup>> {
|
fun flowShow(): Flow<List<BookGroup>> {
|
||||||
return bookGroupDao.show.asFlow()
|
return bookGroupDao.show.asFlow()
|
||||||
}
|
}
|
||||||
|
|||||||
+88
-10
@@ -1,14 +1,42 @@
|
|||||||
package io.legado.app.ui.book.changesource
|
package io.legado.app.ui.book.changesource
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import io.legado.app.data.appDb
|
||||||
|
import io.legado.app.data.entities.BookSourcePart
|
||||||
import io.legado.app.data.entities.SearchBook
|
import io.legado.app.data.entities.SearchBook
|
||||||
|
import io.legado.app.data.repository.SearchRepository
|
||||||
|
import io.legado.app.ui.book.search.SearchScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
class ChangeBookSourceComposeViewModel(application: Application) :
|
class ChangeBookSourceComposeViewModel(
|
||||||
ChangeBookSourceViewModel(application) {
|
application: Application,
|
||||||
|
private val searchRepository: SearchRepository,
|
||||||
|
) : ChangeBookSourceViewModel(application) {
|
||||||
|
|
||||||
val searchGroup: String
|
val enabledGroups = searchRepository.enabledGroups
|
||||||
get() = ChangeSourceConfig.searchGroup
|
val enabledSources = searchRepository.enabledSources
|
||||||
|
|
||||||
|
val searchScope = SearchScope(ChangeSourceConfig.searchScope)
|
||||||
|
|
||||||
|
data class ScopeUiState(
|
||||||
|
val isAll: Boolean,
|
||||||
|
val isSource: Boolean,
|
||||||
|
val displayNames: List<String>,
|
||||||
|
val sourceUrls: List<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
private val _scopeUiState = MutableStateFlow(
|
||||||
|
ScopeUiState(
|
||||||
|
isAll = searchScope.isAll(),
|
||||||
|
isSource = searchScope.isSource(),
|
||||||
|
displayNames = searchScope.displayNames,
|
||||||
|
sourceUrls = searchScope.sourceUrls
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val scopeUiState = _scopeUiState.asStateFlow()
|
||||||
|
|
||||||
val checkAuthor: Boolean
|
val checkAuthor: Boolean
|
||||||
get() = ChangeSourceConfig.checkAuthor
|
get() = ChangeSourceConfig.checkAuthor
|
||||||
@@ -22,12 +50,6 @@ class ChangeBookSourceComposeViewModel(application: Application) :
|
|||||||
val loadWordCount: Boolean
|
val loadWordCount: Boolean
|
||||||
get() = ChangeSourceConfig.loadWordCount
|
get() = ChangeSourceConfig.loadWordCount
|
||||||
|
|
||||||
fun onSearchGroupSelected(group: String) {
|
|
||||||
if (ChangeSourceConfig.searchGroup == group) return
|
|
||||||
ChangeSourceConfig.searchGroup = group
|
|
||||||
if (refresh()) startSearch()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onCheckAuthorChange(enabled: Boolean) {
|
fun onCheckAuthorChange(enabled: Boolean) {
|
||||||
if (ChangeSourceConfig.checkAuthor == enabled) return
|
if (ChangeSourceConfig.checkAuthor == enabled) return
|
||||||
ChangeSourceConfig.checkAuthor = enabled
|
ChangeSourceConfig.checkAuthor = enabled
|
||||||
@@ -62,4 +84,60 @@ class ChangeBookSourceComposeViewModel(application: Application) :
|
|||||||
val currentScore = ObservableSourceConfig.getBookScore(searchBook)
|
val currentScore = ObservableSourceConfig.getBookScore(searchBook)
|
||||||
setBookScore(searchBook, if (currentScore > 0) 0 else 1)
|
setBookScore(searchBook, if (currentScore > 0) 0 else 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun selectAllScope() {
|
||||||
|
searchScope.update("")
|
||||||
|
saveScope()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleScopeGroup(groupName: String) {
|
||||||
|
if (searchScope.isSource()) {
|
||||||
|
searchScope.update("")
|
||||||
|
}
|
||||||
|
val selected = searchScope.displayNames.toMutableSet()
|
||||||
|
if (selected.contains(groupName)) {
|
||||||
|
selected.remove(groupName)
|
||||||
|
} else {
|
||||||
|
selected.add(groupName)
|
||||||
|
}
|
||||||
|
searchScope.update(selected.toList())
|
||||||
|
saveScope()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleScopeSource(source: BookSourcePart) {
|
||||||
|
val selectedUrls = if (searchScope.isSource()) {
|
||||||
|
searchScope.sourceUrls.toMutableSet()
|
||||||
|
} else {
|
||||||
|
mutableSetOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedUrls.contains(source.bookSourceUrl)) {
|
||||||
|
selectedUrls.remove(source.bookSourceUrl)
|
||||||
|
} else {
|
||||||
|
selectedUrls.add(source.bookSourceUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedUrls.isEmpty()) {
|
||||||
|
searchScope.update("")
|
||||||
|
} else {
|
||||||
|
val selectedSources = appDb.bookSourceDao.allEnabledPart.filter {
|
||||||
|
selectedUrls.contains(it.bookSourceUrl)
|
||||||
|
}
|
||||||
|
searchScope.updateSources(selectedSources)
|
||||||
|
}
|
||||||
|
saveScope()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveScope() {
|
||||||
|
ChangeSourceConfig.searchScope = searchScope.toString()
|
||||||
|
_scopeUiState.update {
|
||||||
|
ScopeUiState(
|
||||||
|
isAll = searchScope.isAll(),
|
||||||
|
isSource = searchScope.isSource(),
|
||||||
|
displayNames = searchScope.displayNames,
|
||||||
|
sourceUrls = searchScope.sourceUrls
|
||||||
|
)
|
||||||
|
}
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -38,6 +38,7 @@ import kotlinx.coroutines.asCoroutineDispatcher
|
|||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -97,6 +98,8 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
|
|||||||
.thenBy { it.originOrder }
|
.thenBy { it.originOrder }
|
||||||
}
|
}
|
||||||
private var task: Job? = null
|
private var task: Job? = null
|
||||||
|
private var isPaused = false
|
||||||
|
private var wasSearching = false
|
||||||
val bookMap = ConcurrentHashMap<String, Book>()
|
val bookMap = ConcurrentHashMap<String, Book>()
|
||||||
val searchDataFlow = callbackFlow {
|
val searchDataFlow = callbackFlow {
|
||||||
|
|
||||||
@@ -119,13 +122,14 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getDbSearchBooks().let {
|
|
||||||
searchBooks.clear()
|
|
||||||
searchBooks.addAll(it)
|
|
||||||
trySend(arrayOf(searchBooks))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchBooks.isEmpty()) {
|
if (searchBooks.isEmpty()) {
|
||||||
|
getDbSearchBooks().let {
|
||||||
|
searchBooks.addAll(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trySend(arrayOf(searchBooks))
|
||||||
|
|
||||||
|
if (searchBooks.isEmpty() && !_isSearching.value) {
|
||||||
startSearch()
|
startSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,18 +211,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
|
|||||||
bookMap.clear()
|
bookMap.clear()
|
||||||
tocMapChapterCount = 0
|
tocMapChapterCount = 0
|
||||||
_changeSourceProgress.value = 0 to ""
|
_changeSourceProgress.value = 0 to ""
|
||||||
val searchGroup = AppConfig.searchGroup
|
bookSourceParts.addAll(io.legado.app.ui.book.search.SearchScope(ChangeSourceConfig.searchScope).getBookSourceParts())
|
||||||
if (searchGroup.isBlank()) {
|
|
||||||
bookSourceParts.addAll(appDb.bookSourceDao.allEnabledPart)
|
|
||||||
} else {
|
|
||||||
val sources = appDb.bookSourceDao.getEnabledPartByGroup(searchGroup)
|
|
||||||
if (sources.isEmpty()) {
|
|
||||||
AppConfig.searchGroup = ""
|
|
||||||
bookSourceParts.addAll(appDb.bookSourceDao.allEnabledPart)
|
|
||||||
} else {
|
|
||||||
bookSourceParts.addAll(sources)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
initSearchPool()
|
initSearchPool()
|
||||||
search()
|
search()
|
||||||
}
|
}
|
||||||
@@ -250,6 +243,9 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
|
|||||||
searchStateData.postValue(true)
|
searchStateData.postValue(true)
|
||||||
_isSearching.value = true
|
_isSearching.value = true
|
||||||
}.mapParallel(threadCount) {
|
}.mapParallel(threadCount) {
|
||||||
|
while (isPaused) {
|
||||||
|
kotlinx.coroutines.delay(100)
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
withTimeout(60000L) {
|
withTimeout(60000L) {
|
||||||
search(it)
|
search(it)
|
||||||
@@ -465,6 +461,17 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
|
|||||||
searchPool?.close()
|
searchPool?.close()
|
||||||
searchStateData.postValue(false)
|
searchStateData.postValue(false)
|
||||||
_isSearching.value = false
|
_isSearching.value = false
|
||||||
|
wasSearching = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pause() {
|
||||||
|
isPaused = true
|
||||||
|
wasSearching = _isSearching.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resume() {
|
||||||
|
isPaused = false
|
||||||
|
wasSearching = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getToc(
|
fun getToc(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package io.legado.app.ui.book.changesource
|
package io.legado.app.ui.book.changesource
|
||||||
|
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
|
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
|
||||||
import io.legado.app.ui.config.prefDelegate
|
import io.legado.app.ui.config.prefDelegate
|
||||||
|
|
||||||
object ChangeSourceConfig {
|
object ChangeSourceConfig {
|
||||||
@@ -10,6 +11,11 @@ object ChangeSourceConfig {
|
|||||||
defaultValue = ""
|
defaultValue = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var searchScope by prefDelegate(
|
||||||
|
key = "changeSourceSearchScope",
|
||||||
|
defaultValue = ""
|
||||||
|
)
|
||||||
|
|
||||||
var checkAuthor by prefDelegate(
|
var checkAuthor by prefDelegate(
|
||||||
key = PreferKey.changeSourceCheckAuthor,
|
key = PreferKey.changeSourceCheckAuthor,
|
||||||
defaultValue = false
|
defaultValue = false
|
||||||
@@ -29,4 +35,61 @@ object ChangeSourceConfig {
|
|||||||
key = PreferKey.changeSourceLoadWordCount,
|
key = PreferKey.changeSourceLoadWordCount,
|
||||||
defaultValue = false
|
defaultValue = false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var migrateChapters by prefDelegate(
|
||||||
|
key = "migrateChapters",
|
||||||
|
defaultValue = true
|
||||||
|
)
|
||||||
|
var migrateReadingProgress by prefDelegate(
|
||||||
|
key = "migrateReadingProgress",
|
||||||
|
defaultValue = true
|
||||||
|
)
|
||||||
|
var migrateGroup by prefDelegate(
|
||||||
|
key = "migrateGroup",
|
||||||
|
defaultValue = true
|
||||||
|
)
|
||||||
|
var migrateCover by prefDelegate(
|
||||||
|
key = "migrateCover",
|
||||||
|
defaultValue = true
|
||||||
|
)
|
||||||
|
var migrateCategory by prefDelegate(
|
||||||
|
key = "migrateCategory",
|
||||||
|
defaultValue = true
|
||||||
|
)
|
||||||
|
var migrateRemark by prefDelegate(
|
||||||
|
key = "migrateRemark",
|
||||||
|
defaultValue = true
|
||||||
|
)
|
||||||
|
var migrateReadConfig by prefDelegate(
|
||||||
|
key = "migrateReadConfig",
|
||||||
|
defaultValue = true
|
||||||
|
)
|
||||||
|
var deleteDownloadedChapters by prefDelegate(
|
||||||
|
key = "deleteDownloadedChapters",
|
||||||
|
defaultValue = false
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getMigrationOptions(): ChangeSourceMigrationOptions {
|
||||||
|
return ChangeSourceMigrationOptions(
|
||||||
|
migrateChapters = migrateChapters,
|
||||||
|
migrateReadingProgress = migrateReadingProgress,
|
||||||
|
migrateGroup = migrateGroup,
|
||||||
|
migrateCover = migrateCover,
|
||||||
|
migrateCategory = migrateCategory,
|
||||||
|
migrateRemark = migrateRemark,
|
||||||
|
migrateReadConfig = migrateReadConfig,
|
||||||
|
deleteDownloadedChapters = deleteDownloadedChapters,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMigrationOptions(options: ChangeSourceMigrationOptions) {
|
||||||
|
migrateChapters = options.migrateChapters
|
||||||
|
migrateReadingProgress = options.migrateReadingProgress
|
||||||
|
migrateGroup = options.migrateGroup
|
||||||
|
migrateCover = options.migrateCover
|
||||||
|
migrateCategory = options.migrateCategory
|
||||||
|
migrateRemark = options.migrateRemark
|
||||||
|
migrateReadConfig = options.migrateReadConfig
|
||||||
|
deleteDownloadedChapters = options.deleteDownloadedChapters
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+21
-7
@@ -30,13 +30,27 @@ fun ChangeSourceMigrationOptionsSheet(
|
|||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onConfirm: (ChangeSourceMigrationOptions) -> Unit,
|
onConfirm: (ChangeSourceMigrationOptions) -> Unit,
|
||||||
) {
|
) {
|
||||||
var migrateReadingProgress by rememberSaveable(show) { mutableStateOf(true) }
|
var migrateReadingProgress by rememberSaveable(show) {
|
||||||
var migrateGroup by rememberSaveable(show) { mutableStateOf(true) }
|
mutableStateOf(ChangeSourceConfig.migrateReadingProgress)
|
||||||
var migrateCover by rememberSaveable(show) { mutableStateOf(true) }
|
}
|
||||||
var migrateCategory by rememberSaveable(show) { mutableStateOf(true) }
|
var migrateGroup by rememberSaveable(show) {
|
||||||
var migrateRemark by rememberSaveable(show) { mutableStateOf(true) }
|
mutableStateOf(ChangeSourceConfig.migrateGroup)
|
||||||
var migrateReadConfig by rememberSaveable(show) { mutableStateOf(true) }
|
}
|
||||||
var deleteDownloadedChapters by rememberSaveable(show) { mutableStateOf(false) }
|
var migrateCover by rememberSaveable(show) {
|
||||||
|
mutableStateOf(ChangeSourceConfig.migrateCover)
|
||||||
|
}
|
||||||
|
var migrateCategory by rememberSaveable(show) {
|
||||||
|
mutableStateOf(ChangeSourceConfig.migrateCategory)
|
||||||
|
}
|
||||||
|
var migrateRemark by rememberSaveable(show) {
|
||||||
|
mutableStateOf(ChangeSourceConfig.migrateRemark)
|
||||||
|
}
|
||||||
|
var migrateReadConfig by rememberSaveable(show) {
|
||||||
|
mutableStateOf(ChangeSourceConfig.migrateReadConfig)
|
||||||
|
}
|
||||||
|
var deleteDownloadedChapters by rememberSaveable(show) {
|
||||||
|
mutableStateOf(ChangeSourceConfig.deleteDownloadedChapters)
|
||||||
|
}
|
||||||
|
|
||||||
AppModalBottomSheet(
|
AppModalBottomSheet(
|
||||||
show = show,
|
show = show,
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
@@ -282,12 +283,16 @@ private fun BookInfoScreenContent(
|
|||||||
onDismissRequest = { onIntent(BookInfoIntent.DismissSheet) },
|
onDismissRequest = { onIntent(BookInfoIntent.DismissSheet) },
|
||||||
onSelect = { onIntent(BookInfoIntent.SelectCover(it)) },
|
onSelect = { onIntent(BookInfoIntent.SelectCover(it)) },
|
||||||
)
|
)
|
||||||
BookInfoSheet.GroupPicker -> GroupSelectSheet(
|
BookInfoSheet.GroupPicker -> {
|
||||||
show = currentSheet == BookInfoSheet.GroupPicker,
|
val groups by koinInject<io.legado.app.data.repository.BookGroupRepository>().flowSelect().collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
currentGroupId = state.book?.group ?: 0L,
|
GroupSelectSheet(
|
||||||
onDismissRequest = { onIntent(BookInfoIntent.DismissSheet) },
|
show = currentSheet == BookInfoSheet.GroupPicker,
|
||||||
onConfirm = { onIntent(BookInfoIntent.SelectGroup(it)) },
|
groups = groups,
|
||||||
)
|
currentGroupId = state.book?.group ?: 0L,
|
||||||
|
onDismissRequest = { onIntent(BookInfoIntent.DismissSheet) },
|
||||||
|
onConfirm = { onIntent(BookInfoIntent.SelectGroup(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
BookInfoSheet.SourcePicker -> state.book?.let { book ->
|
BookInfoSheet.SourcePicker -> state.book?.let { book ->
|
||||||
ChangeSourceSheet(
|
ChangeSourceSheet(
|
||||||
show = currentSheet == BookInfoSheet.SourcePicker,
|
show = currentSheet == BookInfoSheet.SourcePicker,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import androidx.compose.foundation.lazy.grid.items
|
|||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.FilterList
|
import androidx.compose.material.icons.filled.FilterList
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
@@ -36,6 +37,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -45,19 +47,22 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
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 androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
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.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.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.ChangeSourceConfig
|
||||||
import io.legado.app.ui.book.changesource.ChangeSourceMigrationOptionsSheet
|
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
|
||||||
@@ -65,6 +70,7 @@ import io.legado.app.ui.book.source.manage.BookSourceActivity
|
|||||||
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.EmptyMessage
|
||||||
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.ConfirmDismissButtonsRow
|
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
|
||||||
import io.legado.app.ui.widget.components.button.MediumIconButton
|
import io.legado.app.ui.widget.components.button.MediumIconButton
|
||||||
@@ -76,6 +82,7 @@ import io.legado.app.ui.widget.components.cover.CoilBookCover
|
|||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||||
|
import io.legado.app.ui.widget.components.tabRow.AppTabRow
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
import io.legado.app.utils.StartActivityContract
|
import io.legado.app.utils.StartActivityContract
|
||||||
import io.legado.app.utils.startActivity
|
import io.legado.app.utils.startActivity
|
||||||
@@ -118,11 +125,11 @@ fun WebFileSheet(
|
|||||||
@Composable
|
@Composable
|
||||||
fun GroupSelectSheet(
|
fun GroupSelectSheet(
|
||||||
show: Boolean,
|
show: Boolean,
|
||||||
|
groups: List<BookGroup>,
|
||||||
currentGroupId: Long,
|
currentGroupId: Long,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onConfirm: (Long) -> Unit,
|
onConfirm: (Long) -> Unit,
|
||||||
) {
|
) {
|
||||||
val groups by appDb.bookGroupDao.flowSelect().collectAsStateWithLifecycle(initialValue = emptyList())
|
|
||||||
var selectedGroupId by remember(currentGroupId) { mutableLongStateOf(currentGroupId) }
|
var selectedGroupId by remember(currentGroupId) { mutableLongStateOf(currentGroupId) }
|
||||||
var editingGroup by remember { mutableStateOf<BookGroup?>(null) }
|
var editingGroup by remember { mutableStateOf<BookGroup?>(null) }
|
||||||
|
|
||||||
@@ -138,7 +145,7 @@ fun GroupSelectSheet(
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
items(groups, key = { it.groupId }) { group ->
|
items(groups, key = { it.groupId }) { group ->
|
||||||
val isSelected = selectedGroupId and group.groupId > 0
|
val isSelected = (selectedGroupId and group.groupId) != 0L
|
||||||
SelectionItemCard(
|
SelectionItemCard(
|
||||||
title = group.groupName,
|
title = group.groupName,
|
||||||
isSelected = isSelected,
|
isSelected = isSelected,
|
||||||
@@ -194,7 +201,7 @@ fun ChangeCoverSheet(
|
|||||||
onSelect: (String) -> Unit,
|
onSelect: (String) -> Unit,
|
||||||
viewModel: ChangeCoverViewModel = koinViewModel(key = "cover-$name-$author"),
|
viewModel: ChangeCoverViewModel = koinViewModel(key = "cover-$name-$author"),
|
||||||
) {
|
) {
|
||||||
val items by viewModel.dataFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
val items by viewModel.dataFlow.collectAsStateWithLifecycle(initialValue = emptyList<SearchBook>())
|
||||||
val isSearching by viewModel.isSearching.collectAsStateWithLifecycle()
|
val isSearching by viewModel.isSearching.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(name, author) {
|
LaunchedEffect(name, author) {
|
||||||
@@ -244,22 +251,25 @@ fun ChangeSourceSheet(
|
|||||||
viewModel: ChangeBookSourceComposeViewModel = koinViewModel(key = "source-${oldBook.bookUrl}"),
|
viewModel: ChangeBookSourceComposeViewModel = koinViewModel(key = "source-${oldBook.bookUrl}"),
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val items by viewModel.searchDataFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
val items by viewModel.searchDataFlow.collectAsStateWithLifecycle(initialValue = emptyList<SearchBook>())
|
||||||
val isSearching by viewModel.isSearching.collectAsStateWithLifecycle()
|
val isSearching by viewModel.isSearching.collectAsStateWithLifecycle()
|
||||||
val progress by viewModel.changeSourceProgress.collectAsStateWithLifecycle()
|
val progress by viewModel.changeSourceProgress.collectAsStateWithLifecycle()
|
||||||
val groups by appDb.bookSourceDao.flowEnabledGroups().collectAsStateWithLifecycle(initialValue = emptyList())
|
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("") }
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
val selectedGroup = viewModel.searchGroup
|
|
||||||
val checkAuthor = viewModel.checkAuthor
|
val checkAuthor = viewModel.checkAuthor
|
||||||
val loadInfo = viewModel.loadInfo
|
val loadInfo = viewModel.loadInfo
|
||||||
val loadToc = viewModel.loadToc
|
val loadToc = viewModel.loadToc
|
||||||
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 showMigrationOptions by remember { mutableStateOf(false) }
|
||||||
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 showFilterSheet by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var scopeSheetTab by rememberSaveable { mutableIntStateOf(0) }
|
||||||
val bookAddedToShelfText = stringResource(R.string.book_added_to_shelf)
|
val bookAddedToShelfText = stringResource(R.string.book_added_to_shelf)
|
||||||
|
|
||||||
val editSourceResult = rememberLauncherForActivityResult(StartActivityContract(BookSourceEditActivity::class.java)) {
|
val editSourceResult = rememberLauncherForActivityResult(StartActivityContract(BookSourceEditActivity::class.java)) {
|
||||||
@@ -270,6 +280,21 @@ fun ChangeSourceSheet(
|
|||||||
LaunchedEffect(oldBook.bookUrl) {
|
LaunchedEffect(oldBook.bookUrl) {
|
||||||
viewModel.initData(oldBook.name, oldBook.author, oldBook, false)
|
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) {
|
DisposableEffect(oldBook.bookUrl) {
|
||||||
onDispose {
|
onDispose {
|
||||||
viewModel.stopSearch()
|
viewModel.stopSearch()
|
||||||
@@ -323,10 +348,17 @@ fun ChangeSourceSheet(
|
|||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
text = stringResource(R.string.book_source_manage),
|
||||||
|
onClick = {
|
||||||
|
context.startActivity<BookSourceActivity>()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MediumIconButton(
|
MediumIconButton(
|
||||||
onClick = { context.startActivity<BookSourceActivity>() },
|
onClick = { showMigrationOptions = true },
|
||||||
imageVector = Icons.Outlined.Settings
|
imageVector = Icons.Outlined.Settings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -337,35 +369,10 @@ fun ChangeSourceSheet(
|
|||||||
onClick = { viewModel.startOrStopSearch() },
|
onClick = { viewModel.startOrStopSearch() },
|
||||||
imageVector = if (isSearching) Icons.Default.PauseCircleOutline else Icons.Default.Refresh,
|
imageVector = if (isSearching) Icons.Default.PauseCircleOutline else Icons.Default.Refresh,
|
||||||
)
|
)
|
||||||
Box {
|
MediumIconButton(
|
||||||
MediumIconButton(
|
onClick = { showFilterSheet = true },
|
||||||
onClick = { showFilterMenu = true },
|
imageVector = Icons.Default.FilterList
|
||||||
imageVector = Icons.Default.FilterList
|
)
|
||||||
)
|
|
||||||
RoundDropdownMenu(
|
|
||||||
expanded = showFilterMenu,
|
|
||||||
onDismissRequest = { showFilterMenu = false }
|
|
||||||
) { dismiss ->
|
|
||||||
RoundDropdownMenuItem(
|
|
||||||
text = stringResource(R.string.all_source),
|
|
||||||
isSelected = selectedGroup.isBlank(),
|
|
||||||
onClick = {
|
|
||||||
viewModel.onSearchGroupSelected("")
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
groups.forEach { group ->
|
|
||||||
RoundDropdownMenuItem(
|
|
||||||
text = group,
|
|
||||||
isSelected = selectedGroup == group,
|
|
||||||
onClick = {
|
|
||||||
viewModel.onSearchGroupSelected(group)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@@ -389,95 +396,116 @@ fun ChangeSourceSheet(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
}
|
}
|
||||||
LazyColumn(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
items(items, key = { it.bookUrl + it.origin }) { item ->
|
if (items.isEmpty()) {
|
||||||
val bookScore by remember(item.origin, item.name, item.author) {
|
Box(
|
||||||
viewModel.bookScoreFlow(item)
|
modifier = Modifier.fillMaxWidth().padding(vertical = 40.dp),
|
||||||
}.collectAsStateWithLifecycle()
|
contentAlignment = Alignment.Center
|
||||||
SelectionItemCard(
|
) {
|
||||||
title = item.originName,
|
EmptyMessage(
|
||||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
message = stringResource(R.string.search_empty)
|
||||||
selectedContainerColor = LegadoTheme.colorScheme.primaryContainer.copy(alpha = 0.32f),
|
|
||||||
leadingContent = {
|
|
||||||
MediumIconButton(
|
|
||||||
onClick = {
|
|
||||||
viewModel.onBookScoreClick(item)
|
|
||||||
},
|
|
||||||
imageVector = 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 ->
|
|
||||||
pendingMigration = PendingSourceMigration(source, book, toc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onDismiss()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} 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 = {
|
||||||
|
MediumIconButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.onBookScoreClick(item)
|
||||||
|
},
|
||||||
|
imageVector = 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))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
@@ -485,19 +513,24 @@ fun ChangeSourceSheet(
|
|||||||
val performAction: (SearchBook, Boolean) -> Unit = { searchBook, replace ->
|
val performAction: (SearchBook, Boolean) -> Unit = { searchBook, replace ->
|
||||||
loadingAction = true
|
loadingAction = true
|
||||||
val book = viewModel.bookMap[searchBook.primaryStr()] ?: searchBook.toBook()
|
val book = viewModel.bookMap[searchBook.primaryStr()] ?: searchBook.toBook()
|
||||||
viewModel.getToc(book, { toc, source ->
|
viewModel.getToc(
|
||||||
loadingAction = false
|
book,
|
||||||
if (replace) {
|
onSuccess = { toc, source ->
|
||||||
pendingMigration = PendingSourceMigration(source, book, toc)
|
loadingAction = false
|
||||||
} else {
|
if (replace) {
|
||||||
onAddAsNew(book, toc)
|
onReplace(source, book, toc, ChangeSourceConfig.getMigrationOptions())
|
||||||
context.toastOnUi(bookAddedToShelfText)
|
onDismissRequest()
|
||||||
|
} else {
|
||||||
|
onAddAsNew(book, toc)
|
||||||
|
context.toastOnUi(bookAddedToShelfText)
|
||||||
|
}
|
||||||
|
actionBook = null
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
loadingAction = false
|
||||||
|
context.toastOnUi(if (replace) "换源失败" else "添加书籍失败")
|
||||||
}
|
}
|
||||||
actionBook = null
|
)
|
||||||
}, {
|
|
||||||
loadingAction = false
|
|
||||||
context.toastOnUi(if (replace) "换源失败" else "添加书籍失败")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AppAlertDialog(
|
AppAlertDialog(
|
||||||
@@ -531,30 +564,103 @@ fun ChangeSourceSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
val migration = pendingMigration
|
|
||||||
ChangeSourceMigrationOptionsSheet(
|
ChangeSourceMigrationOptionsSheet(
|
||||||
show = migration != null,
|
show = showMigrationOptions,
|
||||||
title = "换源选项",
|
title = "换源选项",
|
||||||
subtitle = migration?.let {
|
onDismissRequest = { showMigrationOptions = false },
|
||||||
val sameNameAuthor = oldBook.isSameNameAuthor(it.book)
|
|
||||||
if (sameNameAuthor && oldBook.origin != it.book.origin) {
|
|
||||||
"检测到书名、作者相同但书源不同,可选择本次要迁移的数据。"
|
|
||||||
} else {
|
|
||||||
"选择本次替换当前书籍时要迁移的数据。"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDismissRequest = { pendingMigration = null },
|
|
||||||
onConfirm = { options ->
|
onConfirm = { options ->
|
||||||
val pending = pendingMigration ?: return@ChangeSourceMigrationOptionsSheet
|
ChangeSourceConfig.setMigrationOptions(options)
|
||||||
onReplace(pending.source, pending.book, pending.toc, options)
|
showMigrationOptions = false
|
||||||
pendingMigration = null
|
|
||||||
onDismissRequest()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AppModalBottomSheet(
|
||||||
|
show = showFilterSheet,
|
||||||
|
onDismissRequest = { showFilterSheet = false },
|
||||||
|
title = stringResource(R.string.search_select_group),
|
||||||
|
endAction = {
|
||||||
|
MediumIconButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.startSearch()
|
||||||
|
showFilterSheet = false
|
||||||
|
},
|
||||||
|
imageVector = Icons.Default.Check
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
SelectionItemCard(
|
||||||
|
title = stringResource(R.string.all_source),
|
||||||
|
isSelected = scopeState.isAll,
|
||||||
|
containerColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.6f),
|
||||||
|
inSelectionMode = true,
|
||||||
|
onToggleSelection = {
|
||||||
|
viewModel.selectAllScope()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
AppTabRow(
|
||||||
|
tabTitles = listOf(
|
||||||
|
stringResource(R.string.group),
|
||||||
|
stringResource(R.string.book_source),
|
||||||
|
),
|
||||||
|
selectedTabIndex = scopeSheetTab,
|
||||||
|
onTabSelected = { scopeSheetTab = it },
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
|
||||||
|
if (scopeSheetTab == 0) {
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
items(groups, key = { it }) {
|
||||||
|
val selected = !scopeState.isSource && scopeState.displayNames.contains(it)
|
||||||
|
SelectionItemCard(
|
||||||
|
title = it,
|
||||||
|
isSelected = selected,
|
||||||
|
containerColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.6f),
|
||||||
|
inSelectionMode = true,
|
||||||
|
onToggleSelection = {
|
||||||
|
viewModel.toggleScopeGroup(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (enabledSources.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.search_empty),
|
||||||
|
style = LegadoTheme.typography.bodyMedium,
|
||||||
|
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 12.dp),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
items(enabledSources, key = { it.bookSourceUrl }) {
|
||||||
|
val selected = scopeState.sourceUrls.contains(it.bookSourceUrl)
|
||||||
|
SelectionItemCard(
|
||||||
|
title = it.bookSourceName,
|
||||||
|
subtitle = it.bookSourceGroup?.takeIf { group -> group.isNotBlank() },
|
||||||
|
containerColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.6f),
|
||||||
|
isSelected = selected,
|
||||||
|
inSelectionMode = true,
|
||||||
|
onToggleSelection = {
|
||||||
|
viewModel.toggleScopeSource(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class PendingSourceMigration(
|
|
||||||
val source: BookSource,
|
|
||||||
val book: Book,
|
|
||||||
val toc: List<BookChapter>,
|
|
||||||
)
|
|
||||||
@@ -20,6 +20,7 @@ import io.legado.app.domain.usecase.ChangeBookSourceUseCase
|
|||||||
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
|
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.data.repository.BookGroupRepository
|
||||||
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
||||||
import io.legado.app.exception.NoBooksDirException
|
import io.legado.app.exception.NoBooksDirException
|
||||||
import io.legado.app.exception.NoStackTraceException
|
import io.legado.app.exception.NoStackTraceException
|
||||||
@@ -68,9 +69,12 @@ class BookInfoViewModel(
|
|||||||
private val remoteBookRepository: RemoteBookRepository,
|
private val remoteBookRepository: RemoteBookRepository,
|
||||||
private val readRecordRepository: ReadRecordRepository,
|
private val readRecordRepository: ReadRecordRepository,
|
||||||
private val changeBookSourceUseCase: ChangeBookSourceUseCase,
|
private val changeBookSourceUseCase: ChangeBookSourceUseCase,
|
||||||
private val clearBookCacheUseCase: ClearBookCacheUseCase
|
private val clearBookCacheUseCase: ClearBookCacheUseCase,
|
||||||
|
private val bookGroupRepository: BookGroupRepository,
|
||||||
) : BaseViewModel(application) {
|
) : BaseViewModel(application) {
|
||||||
|
|
||||||
|
val allGroups = bookGroupRepository.flowAll()
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(BookInfoUiState())
|
private val _uiState = MutableStateFlow(BookInfoUiState())
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ 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.appDb
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
|
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.BookSourcePart
|
import io.legado.app.data.entities.BookSourcePart
|
||||||
import io.legado.app.domain.usecase.BatchChangeSourcePreviewItem
|
import io.legado.app.domain.usecase.BatchChangeSourcePreviewItem
|
||||||
@@ -1056,6 +1057,7 @@ private fun BookshelfManageScreen(
|
|||||||
|
|
||||||
GroupSelectSheet(
|
GroupSelectSheet(
|
||||||
show = showGroupSelectSheet,
|
show = showGroupSelectSheet,
|
||||||
|
groups = state.groupList,
|
||||||
currentGroupId = groupPickerCurrentGroupId,
|
currentGroupId = groupPickerCurrentGroupId,
|
||||||
onDismissRequest = { showGroupSelectSheet = false },
|
onDismissRequest = { showGroupSelectSheet = false },
|
||||||
onConfirm = { groupId ->
|
onConfirm = { groupId ->
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import androidx.compose.runtime.DisposableEffect
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
@@ -103,7 +104,7 @@ fun SearchScreen(
|
|||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
var queryInput by rememberSaveable { mutableStateOf(state.query) }
|
var queryInput by rememberSaveable { mutableStateOf(state.query) }
|
||||||
var scopeSheetTab by rememberSaveable { mutableStateOf(0) }
|
var scopeSheetTab by rememberSaveable { mutableIntStateOf(0) }
|
||||||
var ignoreNextDebouncedQuery by rememberSaveable { mutableStateOf<String?>(null) }
|
var ignoreNextDebouncedQuery by rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
val showSuggestionPanel = state.showSuggestions
|
val showSuggestionPanel = state.showSuggestions
|
||||||
val latestQuery by rememberUpdatedState(state.query)
|
val latestQuery by rememberUpdatedState(state.query)
|
||||||
@@ -259,6 +260,13 @@ fun SearchScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
|
TopBarActionButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.onIntent(SearchIntent.OpenSourceManage)
|
||||||
|
},
|
||||||
|
imageVector = AppIcons.Settings,
|
||||||
|
contentDescription = stringResource(R.string.book_source_manage)
|
||||||
|
)
|
||||||
TopBarAnimatedActionButton(
|
TopBarAnimatedActionButton(
|
||||||
checked = state.isPrecisionSearch,
|
checked = state.isPrecisionSearch,
|
||||||
onCheckedChange = { checked ->
|
onCheckedChange = { checked ->
|
||||||
@@ -289,13 +297,6 @@ fun SearchScreen(
|
|||||||
activeText = stringResource(R.string.screen),
|
activeText = stringResource(R.string.screen),
|
||||||
inactiveText = stringResource(R.string.screen),
|
inactiveText = stringResource(R.string.screen),
|
||||||
)
|
)
|
||||||
TopBarActionButton(
|
|
||||||
onClick = {
|
|
||||||
viewModel.onIntent(SearchIntent.OpenSourceManage)
|
|
||||||
},
|
|
||||||
imageVector = AppIcons.Settings,
|
|
||||||
contentDescription = stringResource(R.string.book_source_manage)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -968,8 +968,11 @@ private fun BookshelfOverlays(
|
|||||||
onDismissRequest = { viewModel.dismissOverlay() }
|
onDismissRequest = { viewModel.dismissOverlay() }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val groups by viewModel.allGroupsFlow.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
GroupSelectSheet(
|
GroupSelectSheet(
|
||||||
show = activeOverlay == BookshelfOverlay.GroupSelectSheet,
|
show = activeOverlay == BookshelfOverlay.GroupSelectSheet,
|
||||||
|
groups = groups,
|
||||||
currentGroupId = 0L,
|
currentGroupId = 0L,
|
||||||
onDismissRequest = { viewModel.dismissOverlay() },
|
onDismissRequest = { viewModel.dismissOverlay() },
|
||||||
onConfirm = { groupId ->
|
onConfirm = { groupId ->
|
||||||
|
|||||||
Reference in new issue
Block a user