fix: 修复搜索范围选择行为
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
package io.legado.app.domain.model
|
||||
|
||||
import com.google.gson.GsonBuilder
|
||||
import io.legado.app.utils.fromJsonObject
|
||||
import io.legado.app.utils.splitNotBlank
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
data class BookSearchScope(val raw: String) {
|
||||
|
||||
@@ -39,11 +39,13 @@ data class BookSearchScope(val raw: String) {
|
||||
get() = groups.isEmpty() && sources.isEmpty()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ScopeSourceItem(
|
||||
val name: String,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class SerializedSearchScope(
|
||||
val type: String = "",
|
||||
val groups: List<String> = emptyList(),
|
||||
@@ -57,7 +59,9 @@ data class BookSearchScope(val raw: String) {
|
||||
return if (selected.isEmpty()) {
|
||||
""
|
||||
} else {
|
||||
scopeGson.toJson(SerializedSearchScope(type = TYPE_GROUP, groups = selected))
|
||||
scopeJson.encodeToString(
|
||||
SerializedSearchScope(type = TYPE_GROUP, groups = selected)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +70,9 @@ data class BookSearchScope(val raw: String) {
|
||||
return if (selected.isEmpty()) {
|
||||
""
|
||||
} else {
|
||||
scopeGson.toJson(SerializedSearchScope(type = TYPE_SOURCE, sources = selected))
|
||||
scopeJson.encodeToString(
|
||||
SerializedSearchScope(type = TYPE_SOURCE, sources = selected)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +90,9 @@ data class BookSearchScope(val raw: String) {
|
||||
val json = raw.trim()
|
||||
if (!json.startsWith("{") || !json.endsWith("}")) return null
|
||||
|
||||
return scopeGson.fromJsonObject<SerializedSearchScope>(json).getOrNull()?.let { scope ->
|
||||
return runCatching {
|
||||
scopeJson.decodeFromString<SerializedSearchScope>(json)
|
||||
}.getOrNull()?.let { scope ->
|
||||
when (scope.type) {
|
||||
TYPE_SOURCE -> ParsedSearchScope(
|
||||
sources = scope.sources.filter { it.url.isNotBlank() }
|
||||
@@ -124,7 +132,10 @@ data class BookSearchScope(val raw: String) {
|
||||
|
||||
private const val TYPE_GROUP = "group"
|
||||
private const val TYPE_SOURCE = "source"
|
||||
private val scopeGson = GsonBuilder().disableHtmlEscaping().create()
|
||||
private val scopeJson = Json {
|
||||
encodeDefaults = false
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import io.legado.app.R
|
||||
import io.legado.app.data.entities.BookSourcePart
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.SearchBar
|
||||
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
|
||||
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.icon.AppIcons
|
||||
@@ -46,9 +47,20 @@ fun ScopeSelectSheet(
|
||||
isSourceScope: Boolean = false,
|
||||
title: String = stringResource(R.string.search_select_group),
|
||||
onConfirm: (() -> Unit)? = null,
|
||||
onApplyScope: ((ScopeSelection) -> Unit)? = null,
|
||||
) {
|
||||
var scopeSheetTab by rememberSaveable(show) { mutableIntStateOf(if (isSourceScope) 1 else 0) }
|
||||
var filterText by rememberSaveable(show) { mutableStateOf("") }
|
||||
var draftIsAll by remember(show, isAll) { mutableStateOf(isAll) }
|
||||
var draftIsSourceScope by remember(show, isSourceScope) { mutableStateOf(isSourceScope) }
|
||||
var draftGroups by remember(show, selectedGroups) { mutableStateOf(selectedGroups.toSet()) }
|
||||
var draftSourceUrls by remember(show, selectedSources) { mutableStateOf(selectedSources.toSet()) }
|
||||
|
||||
val useDraftSelection = onApplyScope != null
|
||||
val currentIsAll = if (useDraftSelection) draftIsAll else isAll
|
||||
val currentIsSourceScope = if (useDraftSelection) draftIsSourceScope else isSourceScope
|
||||
val currentGroups = if (useDraftSelection) draftGroups else selectedGroups
|
||||
val currentSourceUrls = if (useDraftSelection) draftSourceUrls else selectedSources
|
||||
|
||||
val filteredGroups = remember(groups, filterText) {
|
||||
if (filterText.isBlank()) groups else groups.filter { it.contains(filterText, ignoreCase = true) }
|
||||
@@ -87,11 +99,18 @@ fun ScopeSelectSheet(
|
||||
|
||||
SelectionItemCard(
|
||||
title = stringResource(R.string.all_source),
|
||||
isSelected = isAll,
|
||||
isSelected = currentIsAll,
|
||||
containerColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.6f),
|
||||
inSelectionMode = true,
|
||||
onToggleSelection = {
|
||||
onSelectAll()
|
||||
if (useDraftSelection) {
|
||||
draftIsAll = true
|
||||
draftIsSourceScope = false
|
||||
draftGroups = emptySet()
|
||||
draftSourceUrls = emptySet()
|
||||
} else {
|
||||
onSelectAll()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -113,14 +132,27 @@ fun ScopeSelectSheet(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(filteredGroups, key = { it }) { groupName ->
|
||||
val selected = !isSourceScope && selectedGroups.contains(groupName)
|
||||
val selected = !currentIsSourceScope && currentGroups.contains(groupName)
|
||||
SelectionItemCard(
|
||||
title = groupName,
|
||||
isSelected = selected,
|
||||
containerColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.6f),
|
||||
inSelectionMode = true,
|
||||
onToggleSelection = {
|
||||
onToggleGroup(groupName)
|
||||
if (useDraftSelection) {
|
||||
val next = currentGroups.toMutableSet()
|
||||
if (!currentIsSourceScope && next.contains(groupName)) {
|
||||
next.remove(groupName)
|
||||
} else {
|
||||
next.add(groupName)
|
||||
}
|
||||
draftGroups = next
|
||||
draftSourceUrls = emptySet()
|
||||
draftIsSourceScope = false
|
||||
draftIsAll = next.isEmpty()
|
||||
} else {
|
||||
onToggleGroup(groupName)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -142,7 +174,7 @@ fun ScopeSelectSheet(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(filteredSources, key = { it.bookSourceUrl }) { source ->
|
||||
val selected = selectedSources.contains(source.bookSourceUrl)
|
||||
val selected = currentSourceUrls.contains(source.bookSourceUrl)
|
||||
SelectionItemCard(
|
||||
title = source.bookSourceName,
|
||||
subtitle = source.bookSourceGroup?.takeIf { group -> group.isNotBlank() },
|
||||
@@ -150,7 +182,20 @@ fun ScopeSelectSheet(
|
||||
isSelected = selected,
|
||||
inSelectionMode = true,
|
||||
onToggleSelection = {
|
||||
onToggleSource(source)
|
||||
if (useDraftSelection) {
|
||||
val next = currentSourceUrls.toMutableSet()
|
||||
if (next.contains(source.bookSourceUrl)) {
|
||||
next.remove(source.bookSourceUrl)
|
||||
} else {
|
||||
next.add(source.bookSourceUrl)
|
||||
}
|
||||
draftSourceUrls = next
|
||||
draftGroups = emptySet()
|
||||
draftIsSourceScope = next.isNotEmpty()
|
||||
draftIsAll = next.isEmpty()
|
||||
} else {
|
||||
onToggleSource(source)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -158,7 +203,36 @@ fun ScopeSelectSheet(
|
||||
}
|
||||
}
|
||||
|
||||
if (onApplyScope != null) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ConfirmDismissButtonsRow(
|
||||
onDismiss = onDismissRequest,
|
||||
onConfirm = {
|
||||
onApplyScope(
|
||||
ScopeSelection(
|
||||
groupNames = if (!draftIsSourceScope) draftGroups.toList() else emptyList(),
|
||||
sources = if (draftIsSourceScope) {
|
||||
sources.filter { draftSourceUrls.contains(it.bookSourceUrl) }
|
||||
} else {
|
||||
emptyList()
|
||||
},
|
||||
isSourceScope = draftIsSourceScope,
|
||||
)
|
||||
)
|
||||
onDismissRequest()
|
||||
},
|
||||
dismissText = stringResource(R.string.cancel),
|
||||
confirmText = stringResource(R.string.confirm),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ScopeSelection(
|
||||
val groupNames: List<String>,
|
||||
val sources: List<BookSourcePart>,
|
||||
val isSourceScope: Boolean,
|
||||
)
|
||||
|
||||
@@ -88,6 +88,11 @@ sealed interface SearchIntent {
|
||||
data class ToggleSourceType(val type: Int) : SearchIntent
|
||||
data object ClearAllSourceTypes : SearchIntent
|
||||
data object SelectAllScope : SearchIntent
|
||||
data class ApplyScopeSelection(
|
||||
val groupNames: List<String>,
|
||||
val sources: List<BookSourcePart>,
|
||||
val isSourceScope: Boolean,
|
||||
) : SearchIntent
|
||||
data class ToggleScopeGroup(val groupName: String) : SearchIntent
|
||||
data class ToggleScopeSource(val source: BookSourcePart) : SearchIntent
|
||||
data class RemoveScopeItem(val scopeName: String) : SearchIntent
|
||||
|
||||
@@ -70,7 +70,7 @@ data class SearchScope(private var scope: String) {
|
||||
if (scope.isEmpty()) {
|
||||
return appCtx.getString(R.string.all_source)
|
||||
}
|
||||
return scope
|
||||
return parsedScope().groupNames.joinToString(",")
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -112,6 +112,7 @@ fun SearchScreen(
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
var queryInput by rememberSaveable { mutableStateOf(state.query) }
|
||||
var ignoreNextDebouncedQuery by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var keepResultsPinnedToTop by rememberSaveable { mutableStateOf(true) }
|
||||
val showSuggestionPanel = state.showSuggestions
|
||||
val latestQuery by rememberUpdatedState(state.query)
|
||||
val scrollBehavior = if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
|
||||
@@ -243,6 +244,37 @@ fun SearchScreen(
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state.committedQuery) {
|
||||
keepResultsPinnedToTop = true
|
||||
}
|
||||
|
||||
LaunchedEffect(isSourceGroupedMode, listState, groupedListState) {
|
||||
snapshotFlow {
|
||||
val activeState = if (isSourceGroupedMode) groupedListState else listState
|
||||
Triple(
|
||||
activeState.firstVisibleItemIndex,
|
||||
activeState.firstVisibleItemScrollOffset,
|
||||
activeState.isScrollInProgress
|
||||
)
|
||||
}.collect { (index, offset, isScrollInProgress) ->
|
||||
if (index == 0 && offset == 0) {
|
||||
keepResultsPinnedToTop = true
|
||||
} else if (isScrollInProgress) {
|
||||
keepResultsPinnedToTop = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val firstResultKey = state.results.firstOrNull()?.let {
|
||||
"${it.book.origin}:${it.book.bookUrl}"
|
||||
}
|
||||
LaunchedEffect(firstResultKey, state.results.size, state.isSearching) {
|
||||
if (state.isSearching && keepResultsPinnedToTop && state.results.isNotEmpty()) {
|
||||
listState.scrollToItem(0)
|
||||
groupedListState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
val submitSearch: (String) -> Unit = { rawQuery ->
|
||||
val normalized = rawQuery.trim()
|
||||
if (normalized.isNotBlank()) {
|
||||
@@ -611,6 +643,15 @@ fun SearchScreen(
|
||||
onToggleSource = { viewModel.onIntent(SearchIntent.ToggleScopeSource(it)) },
|
||||
isSourceScope = state.isSourceScope,
|
||||
onConfirm = { viewModel.onIntent(SearchIntent.OpenSourceManage) },
|
||||
onApplyScope = { selection ->
|
||||
viewModel.onIntent(
|
||||
SearchIntent.ApplyScopeSelection(
|
||||
groupNames = selection.groupNames,
|
||||
sources = selection.sources,
|
||||
isSourceScope = selection.isSourceScope,
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
AppModalBottomSheet(
|
||||
|
||||
@@ -211,6 +211,7 @@ class SearchViewModel(
|
||||
syncScopeState(restartSearch = true, oldScope = oldScope)
|
||||
}
|
||||
|
||||
is SearchIntent.ApplyScopeSelection -> applyScopeSelection(intent)
|
||||
is SearchIntent.ToggleScopeGroup -> toggleScopeGroup(intent.groupName)
|
||||
is SearchIntent.ToggleScopeSource -> toggleScopeSource(intent.source)
|
||||
is SearchIntent.RemoveScopeItem -> {
|
||||
@@ -221,6 +222,7 @@ class SearchViewModel(
|
||||
}
|
||||
|
||||
is SearchIntent.SetMatchMode -> {
|
||||
_uiState.update { it.copy(matchMode = intent.mode) }
|
||||
viewModelScope.launch {
|
||||
localPreferencesRepository.updatePreference(
|
||||
LocalPreferencesKeys.MATCH_MODE, intent.mode.value
|
||||
@@ -427,7 +429,6 @@ class SearchViewModel(
|
||||
it.copy(
|
||||
query = query,
|
||||
showSuggestions = showSuggestions,
|
||||
isManualStop = false,
|
||||
emptyScopeAction = null,
|
||||
)
|
||||
}
|
||||
@@ -657,6 +658,7 @@ class SearchViewModel(
|
||||
_uiState.update { it.copy(emptyScopeAction = null) }
|
||||
|
||||
if (action.wasMatchMode == MatchMode.EXACT) {
|
||||
_uiState.update { it.copy(matchMode = MatchMode.DEFAULT) }
|
||||
viewModelScope.launch {
|
||||
localPreferencesRepository.updatePreference(
|
||||
LocalPreferencesKeys.MATCH_MODE, MatchMode.DEFAULT.value
|
||||
@@ -672,12 +674,29 @@ class SearchViewModel(
|
||||
}
|
||||
|
||||
private fun restartCommittedSearchIfNeeded() {
|
||||
val committed = _uiState.value.committedQuery
|
||||
if (committed.isNotBlank()) {
|
||||
val state = _uiState.value
|
||||
val committed = state.committedQuery
|
||||
if (
|
||||
committed.isNotBlank() &&
|
||||
state.query.trim() == committed &&
|
||||
!state.showSuggestions &&
|
||||
!state.isManualStop
|
||||
) {
|
||||
submitSearch(committed)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyScopeSelection(intent: SearchIntent.ApplyScopeSelection) {
|
||||
val oldScope = searchScope.toString()
|
||||
when {
|
||||
intent.isSourceScope -> searchScope.updateSources(intent.sources)
|
||||
intent.groupNames.isNotEmpty() -> searchScope.update(intent.groupNames)
|
||||
else -> searchScope.update("")
|
||||
}
|
||||
persistSearchScope()
|
||||
syncScopeState(restartSearch = true, oldScope = oldScope)
|
||||
}
|
||||
|
||||
private fun syncScopeState(
|
||||
restartSearch: Boolean = false,
|
||||
oldScope: String? = null,
|
||||
|
||||
Reference in New Issue
Block a user