增加搜素分源布局
This commit is contained in:
@@ -6,6 +6,7 @@ import io.legado.app.data.entities.BookSourcePart
|
||||
import io.legado.app.data.entities.SearchBook
|
||||
import io.legado.app.domain.gateway.BookSearchGateway
|
||||
import io.legado.app.domain.model.BookSearchScope
|
||||
import io.legado.app.domain.model.MatchMode
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -30,7 +31,7 @@ data class BookSearchRequest(
|
||||
val keyword: String,
|
||||
val page: Int,
|
||||
val scope: BookSearchScope,
|
||||
val precision: Boolean,
|
||||
val matchMode: MatchMode,
|
||||
val concurrency: Int,
|
||||
val types: Set<Int>? = null,
|
||||
)
|
||||
@@ -103,7 +104,7 @@ class SearchBooksUseCase(
|
||||
throw NoStackTraceException("可搜索书源为空")
|
||||
}
|
||||
|
||||
val merger = SearchResultMerger(keyword, request.precision)
|
||||
val merger = SearchResultMerger(keyword, request.matchMode)
|
||||
val concurrency = request.concurrency.coerceAtLeast(1)
|
||||
var hasMore = false
|
||||
var processedSources = 0
|
||||
@@ -116,7 +117,7 @@ class SearchBooksUseCase(
|
||||
.flatMapMerge(concurrency) { searchableSource ->
|
||||
flow {
|
||||
control.awaitResumed()
|
||||
emit(searchSource(searchableSource, keyword, request.page, request.precision))
|
||||
emit(searchSource(searchableSource, keyword, request.page, request.matchMode))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
}
|
||||
.collect { result ->
|
||||
@@ -177,7 +178,7 @@ class SearchBooksUseCase(
|
||||
searchableSource: SearchableSource,
|
||||
keyword: String,
|
||||
page: Int,
|
||||
precision: Boolean,
|
||||
matchMode: MatchMode,
|
||||
): SourceSearchResult {
|
||||
return try {
|
||||
val source = searchableSource.source
|
||||
@@ -191,7 +192,7 @@ class SearchBooksUseCase(
|
||||
keyword,
|
||||
page,
|
||||
filter = { name, author ->
|
||||
!precision ||
|
||||
matchMode == MatchMode.DEFAULT ||
|
||||
name.contains(keyword, ignoreCase = true) ||
|
||||
author.contains(keyword, ignoreCase = true)
|
||||
}
|
||||
@@ -222,7 +223,7 @@ class SearchBooksUseCase(
|
||||
|
||||
private class SearchResultMerger(
|
||||
private val keyword: String,
|
||||
private val precision: Boolean,
|
||||
private val matchMode: MatchMode,
|
||||
) {
|
||||
private companion object {
|
||||
const val MAX_RETAINED_SEARCH_RESULTS = 1000
|
||||
@@ -267,9 +268,13 @@ class SearchBooksUseCase(
|
||||
book.name.equals(keyword, ignoreCase = true) ||
|
||||
book.author.equals(keyword, ignoreCase = true) -> equalBooks
|
||||
book.name.contains(keyword, ignoreCase = true) ||
|
||||
book.author.contains(keyword, ignoreCase = true) -> containsBooks
|
||||
!precision -> otherBooks
|
||||
else -> null
|
||||
book.author.contains(
|
||||
keyword,
|
||||
ignoreCase = true
|
||||
) -> if (matchMode == MatchMode.EXACT) null else containsBooks
|
||||
|
||||
matchMode != MatchMode.DEFAULT -> null
|
||||
else -> otherBooks
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ import androidx.compose.foundation.layout.heightIn
|
||||
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.Check
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -27,8 +25,9 @@ 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.MediumIconButton
|
||||
import io.legado.app.ui.widget.components.button.MediumPlainButton
|
||||
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||
import io.legado.app.ui.widget.components.icon.AppIcons
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
import io.legado.app.ui.widget.components.tabRow.AppTabRow
|
||||
|
||||
@@ -68,9 +67,9 @@ fun ScopeSelectSheet(
|
||||
title = title,
|
||||
endAction = onConfirm?.let {
|
||||
{
|
||||
MediumIconButton(
|
||||
MediumPlainButton(
|
||||
onClick = it,
|
||||
imageVector = Icons.Default.Check
|
||||
icon = AppIcons.Settings
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class SearchActivity : BaseComposeActivity() {
|
||||
},
|
||||
onOpenSourceManage = {
|
||||
startActivity<BookSourceActivity>()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@ import io.legado.app.data.entities.BookSourcePart
|
||||
import io.legado.app.data.entities.SearchBook
|
||||
import io.legado.app.data.entities.SearchKeyword
|
||||
import io.legado.app.domain.model.BookShelfState
|
||||
import io.legado.app.domain.model.MatchMode
|
||||
import io.legado.app.ui.main.bookshelf.BookShelfItem
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Stable
|
||||
data class SearchResultItemUi(
|
||||
@@ -17,17 +20,17 @@ data class SearchResultItemUi(
|
||||
data class SearchUiState(
|
||||
val query: String = "",
|
||||
val committedQuery: String = "",
|
||||
val results: List<SearchResultItemUi> = emptyList(),
|
||||
val history: List<SearchKeyword> = emptyList(),
|
||||
val bookshelfHints: List<BookShelfItem> = emptyList(),
|
||||
val enabledGroups: List<String> = emptyList(),
|
||||
val enabledSources: List<BookSourcePart> = emptyList(),
|
||||
val results: ImmutableList<SearchResultItemUi> = persistentListOf(),
|
||||
val history: ImmutableList<SearchKeyword> = persistentListOf(),
|
||||
val bookshelfHints: ImmutableList<BookShelfItem> = persistentListOf(),
|
||||
val enabledGroups: ImmutableList<String> = persistentListOf(),
|
||||
val enabledSources: ImmutableList<BookSourcePart> = persistentListOf(),
|
||||
val scopeDisplay: String = "",
|
||||
val scopeDisplayNames: List<String> = emptyList(),
|
||||
val scopeDisplayNames: ImmutableList<String> = persistentListOf(),
|
||||
val selectedScopeSourceUrls: Set<String> = emptySet(),
|
||||
val isAllScope: Boolean = true,
|
||||
val isSourceScope: Boolean = false,
|
||||
val isPrecisionSearch: Boolean = false,
|
||||
val matchMode: MatchMode = MatchMode.DEFAULT,
|
||||
val isSearching: Boolean = false,
|
||||
val isManualStop: Boolean = false,
|
||||
val hasMore: Boolean = true,
|
||||
@@ -35,17 +38,24 @@ data class SearchUiState(
|
||||
val totalSources: Int = 0,
|
||||
val selectedSourceTypes: Set<Int> = emptySet(),
|
||||
val showScopeSheet: Boolean = false,
|
||||
val showTypeSheet: Boolean = false,
|
||||
val showSettingsSheet: Boolean = false,
|
||||
val showClearHistoryDialog: Boolean = false,
|
||||
val showSuggestions: Boolean = true,
|
||||
val emptyScopeAction: SearchEmptyScopeAction? = null,
|
||||
val savedScrollIndex: Int = 0,
|
||||
val savedScrollOffset: Int = 0,
|
||||
val expandedSourceUrl: String? = null,
|
||||
val expandedSourceName: String? = null,
|
||||
val expandedSourceBooks: ImmutableList<SearchBook> = persistentListOf(),
|
||||
val expandedSourceLoading: Boolean = false,
|
||||
val expandedSourceEnd: Boolean = false,
|
||||
val expandedSourceError: String? = null,
|
||||
val expandedSourcePage: Int = 1,
|
||||
)
|
||||
|
||||
data class SearchEmptyScopeAction(
|
||||
val scopeDisplay: String,
|
||||
val wasPrecisionSearch: Boolean,
|
||||
val wasMatchMode: MatchMode,
|
||||
)
|
||||
|
||||
sealed interface SearchIntent {
|
||||
@@ -60,17 +70,22 @@ sealed interface SearchIntent {
|
||||
data class UseHistoryKeyword(val keyword: String) : SearchIntent
|
||||
data class OpenSearchBook(val book: SearchBook, val sharedCoverKey: String?) : SearchIntent
|
||||
data class OpenBookshelfBook(val book: BookShelfItem) : SearchIntent
|
||||
data class ExpandSource(val sourceUrl: String, val sourceName: String) : SearchIntent
|
||||
data object DismissExpandedSource : SearchIntent
|
||||
data object LoadMoreExpandedSource : SearchIntent
|
||||
data class OpenExpandedSourceBook(val book: SearchBook, val sharedCoverKey: String?) :
|
||||
SearchIntent
|
||||
data class DeleteHistory(val item: SearchKeyword) : SearchIntent
|
||||
data class SetClearHistoryDialogVisible(val visible: Boolean) : SearchIntent
|
||||
data object ConfirmClearHistory : SearchIntent
|
||||
data class SetScopeSheetVisible(val visible: Boolean) : SearchIntent
|
||||
data class SetTypeSheetVisible(val visible: Boolean) : SearchIntent
|
||||
data class SetSettingsSheetVisible(val visible: Boolean) : SearchIntent
|
||||
data class ToggleSourceType(val type: Int) : SearchIntent
|
||||
data object SelectAllScope : SearchIntent
|
||||
data class ToggleScopeGroup(val groupName: String) : SearchIntent
|
||||
data class ToggleScopeSource(val source: BookSourcePart) : SearchIntent
|
||||
data class RemoveScopeItem(val scopeName: String) : SearchIntent
|
||||
data class TogglePrecision(val enabled: Boolean) : SearchIntent
|
||||
data class SetMatchMode(val mode: MatchMode) : SearchIntent
|
||||
data object ConfirmEmptyScopeAction : SearchIntent
|
||||
data object DismissEmptyScopeAction : SearchIntent
|
||||
data object OpenSourceManage : SearchIntent
|
||||
|
||||
@@ -20,8 +20,10 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
|
||||
import androidx.compose.material.icons.filled.Book
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.GridView
|
||||
import androidx.compose.material.icons.filled.Layers
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Stop
|
||||
@@ -54,6 +56,7 @@ import io.legado.app.R
|
||||
import io.legado.app.data.entities.SearchBook
|
||||
import io.legado.app.data.entities.SearchKeyword
|
||||
import io.legado.app.domain.model.BookShelfState
|
||||
import io.legado.app.domain.model.MatchMode
|
||||
import io.legado.app.ui.main.bookCoverSharedElementKey
|
||||
import io.legado.app.ui.main.bookshelf.BookShelfItem
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
@@ -63,12 +66,12 @@ import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import io.legado.app.ui.widget.components.AppFloatingActionButton
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.LoadMoreFooter
|
||||
import io.legado.app.ui.widget.components.SearchBar
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.book.SearchBookListItem
|
||||
import io.legado.app.ui.widget.components.book.SearchBookPreviewSheet
|
||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||
import io.legado.app.ui.widget.components.button.SmallTextButton
|
||||
import io.legado.app.ui.widget.components.button.SmallPlainButton
|
||||
import io.legado.app.ui.widget.components.card.NormalCard
|
||||
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||
@@ -76,11 +79,11 @@ import io.legado.app.ui.widget.components.icon.AppIcons
|
||||
import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
|
||||
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.settingItem.CompactDropdownSettingItem
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
||||
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
||||
import io.legado.app.ui.widget.components.topbar.M3GlassScrollBehavior
|
||||
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
|
||||
import io.legado.app.ui.widget.components.topbar.TopBarAnimatedActionButton
|
||||
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||
import io.legado.app.utils.toastOnUi
|
||||
@@ -100,6 +103,8 @@ fun SearchScreen(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val searchLayoutMode by viewModel.searchLayoutMode.collectAsStateWithLifecycle()
|
||||
val isSourceGroupedMode = searchLayoutMode == 1
|
||||
var previewBook by remember { mutableStateOf<SearchBook?>(null) }
|
||||
var previewSharedCoverKey by remember { mutableStateOf<String?>(null) }
|
||||
val listState = rememberLazyListState()
|
||||
@@ -149,13 +154,15 @@ fun SearchScreen(
|
||||
state.hasMore,
|
||||
state.isManualStop,
|
||||
state.showSuggestions,
|
||||
isSourceGroupedMode,
|
||||
) {
|
||||
if (
|
||||
shouldLoadMore &&
|
||||
!state.isSearching &&
|
||||
state.hasMore &&
|
||||
!state.isManualStop &&
|
||||
!state.showSuggestions
|
||||
!state.showSuggestions &&
|
||||
!isSourceGroupedMode
|
||||
) {
|
||||
viewModel.onIntent(SearchIntent.LoadMore)
|
||||
}
|
||||
@@ -261,32 +268,15 @@ fun SearchScreen(
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
TopBarActionButton(
|
||||
onClick = {
|
||||
viewModel.onIntent(SearchIntent.OpenSourceManage)
|
||||
},
|
||||
imageVector = AppIcons.Settings,
|
||||
contentDescription = stringResource(R.string.book_source_manage)
|
||||
)
|
||||
TopBarAnimatedActionButton(
|
||||
checked = state.isPrecisionSearch,
|
||||
onCheckedChange = { checked ->
|
||||
viewModel.onIntent(SearchIntent.TogglePrecision(checked))
|
||||
},
|
||||
iconChecked = AppIcons.PrecisionSearch,
|
||||
iconUnchecked = AppIcons.UnPrecisionSearch,
|
||||
activeText = stringResource(R.string.precision_search),
|
||||
inactiveText = stringResource(R.string.search),
|
||||
)
|
||||
TopBarAnimatedActionButton(
|
||||
checked = state.selectedSourceTypes.isNotEmpty(),
|
||||
checked = isSourceGroupedMode || state.matchMode == MatchMode.EXACT || state.selectedSourceTypes.isNotEmpty(),
|
||||
onCheckedChange = {
|
||||
viewModel.onIntent(SearchIntent.SetTypeSheetVisible(true))
|
||||
viewModel.onIntent(SearchIntent.SetSettingsSheetVisible(true))
|
||||
},
|
||||
iconChecked = Icons.Default.Layers,
|
||||
iconUnchecked = Icons.Default.Layers,
|
||||
activeText = "搜素类型",
|
||||
inactiveText = "搜素类型",
|
||||
iconChecked = AppIcons.Settings,
|
||||
iconUnchecked = AppIcons.Settings,
|
||||
activeText = stringResource(R.string.setting),
|
||||
inactiveText = stringResource(R.string.setting),
|
||||
)
|
||||
TopBarAnimatedActionButton(
|
||||
checked = !state.isAllScope,
|
||||
@@ -296,7 +286,7 @@ fun SearchScreen(
|
||||
iconChecked = AppIcons.Filter,
|
||||
iconUnchecked = AppIcons.Filter,
|
||||
activeText = stringResource(R.string.screen),
|
||||
inactiveText = stringResource(R.string.screen),
|
||||
inactiveText = stringResource(R.string.screen)
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
@@ -315,12 +305,13 @@ fun SearchScreen(
|
||||
placeholder = searchLabel,
|
||||
trailingIcon = {
|
||||
if (queryInput.isNotEmpty()) {
|
||||
TopBarActionButton(
|
||||
SmallPlainButton(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
onClick = {
|
||||
queryInput = ""
|
||||
viewModel.onIntent(SearchIntent.UpdateQuery(""))
|
||||
},
|
||||
imageVector = AppIcons.Close,
|
||||
icon = AppIcons.Close,
|
||||
contentDescription = stringResource(R.string.clear)
|
||||
)
|
||||
}
|
||||
@@ -394,52 +385,124 @@ fun SearchScreen(
|
||||
}
|
||||
|
||||
if (state.results.isNotEmpty()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
contentPadding = adaptiveContentPaddingOnlyVertical(
|
||||
top = 48.dp,
|
||||
bottom = 8.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = state.results,
|
||||
key = { index, item -> "${item.book.origin}:${item.book.bookUrl}:$index" }
|
||||
) { index, item ->
|
||||
val sharedCoverKey = bookCoverSharedElementKey(
|
||||
item.book.bookUrl,
|
||||
"search:${item.book.origin}:$index"
|
||||
)
|
||||
SearchBookListItem(
|
||||
book = item.book,
|
||||
shelfState = item.shelfState,
|
||||
onClick = {
|
||||
viewModel.onIntent(
|
||||
SearchIntent.OpenSearchBook(
|
||||
item.book,
|
||||
sharedCoverKey
|
||||
)
|
||||
)
|
||||
},
|
||||
onLongClick = { book, coverKey ->
|
||||
previewBook = book
|
||||
previewSharedCoverKey = coverKey
|
||||
},
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
}
|
||||
val sourceGroupedResults = remember(state.results) {
|
||||
state.results
|
||||
.groupBy { it.book.origin }
|
||||
.map { (origin, books) ->
|
||||
SourceGroup(
|
||||
origin = origin,
|
||||
sourceName = books.firstOrNull()?.book?.originName?.takeIf { it.isNotBlank() }
|
||||
?: origin,
|
||||
items = books
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SearchResultFooter(
|
||||
isSearching = state.isSearching,
|
||||
hasMore = state.hasMore,
|
||||
hasResult = true,
|
||||
committedQuery = state.committedQuery,
|
||||
onLoadMore = { viewModel.onIntent(SearchIntent.LoadMore) },
|
||||
)
|
||||
AnimatedContent(
|
||||
targetState = isSourceGroupedMode,
|
||||
label = "SearchLayoutTransition",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) { isSourceGrouped ->
|
||||
if (isSourceGrouped) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = adaptiveContentPaddingOnlyVertical(
|
||||
top = 48.dp,
|
||||
bottom = 8.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
sourceGroupedResults.forEachIndexed { groupIndex, group ->
|
||||
item(key = "header_${group.origin}") {
|
||||
SearchSourceSection(
|
||||
sourceName = group.sourceName,
|
||||
items = group.items,
|
||||
onClickBook = { book, coverKey ->
|
||||
viewModel.onIntent(
|
||||
SearchIntent.OpenSearchBook(
|
||||
book,
|
||||
coverKey
|
||||
)
|
||||
)
|
||||
},
|
||||
onLongClickBook = { book, coverKey ->
|
||||
previewBook = book
|
||||
previewSharedCoverKey = coverKey
|
||||
},
|
||||
onViewAll = {
|
||||
viewModel.onIntent(
|
||||
SearchIntent.ExpandSource(
|
||||
group.origin,
|
||||
group.sourceName
|
||||
)
|
||||
)
|
||||
},
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sourceSectionIndex = groupIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SearchResultFooter(
|
||||
isSearching = state.isSearching,
|
||||
hasMore = state.hasMore,
|
||||
hasResult = true,
|
||||
committedQuery = state.committedQuery,
|
||||
onLoadMore = { viewModel.onIntent(SearchIntent.LoadMore) },
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
contentPadding = adaptiveContentPaddingOnlyVertical(
|
||||
top = 48.dp,
|
||||
bottom = 8.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = state.results,
|
||||
key = { index, item -> "${item.book.origin}:${item.book.bookUrl}:$index" }
|
||||
) { index, item ->
|
||||
val sharedCoverKey = bookCoverSharedElementKey(
|
||||
item.book.bookUrl,
|
||||
"search:${item.book.origin}:$index"
|
||||
)
|
||||
SearchBookListItem(
|
||||
book = item.book,
|
||||
shelfState = item.shelfState,
|
||||
onClick = {
|
||||
viewModel.onIntent(
|
||||
SearchIntent.OpenSearchBook(
|
||||
item.book,
|
||||
sharedCoverKey
|
||||
)
|
||||
)
|
||||
},
|
||||
onLongClick = { book, coverKey ->
|
||||
previewBook = book
|
||||
previewSharedCoverKey = coverKey
|
||||
},
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SearchResultFooter(
|
||||
isSearching = state.isSearching,
|
||||
hasMore = state.hasMore,
|
||||
hasResult = true,
|
||||
committedQuery = state.committedQuery,
|
||||
onLoadMore = { viewModel.onIntent(SearchIntent.LoadMore) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -502,7 +565,7 @@ fun SearchScreen(
|
||||
},
|
||||
title = stringResource(R.string.draw),
|
||||
textProvider = {
|
||||
if (wasPrecisionSearch) {
|
||||
if (wasMatchMode == MatchMode.EXACT) {
|
||||
stringResource(R.string.search_empty_scope_disable_precision, scopeDisplay)
|
||||
} else {
|
||||
stringResource(R.string.search_empty_scope_switch_all, scopeDisplay)
|
||||
@@ -525,46 +588,101 @@ fun SearchScreen(
|
||||
sources = state.enabledSources,
|
||||
selectedSources = state.selectedScopeSourceUrls,
|
||||
onToggleSource = { viewModel.onIntent(SearchIntent.ToggleScopeSource(it)) },
|
||||
isSourceScope = state.isSourceScope
|
||||
isSourceScope = state.isSourceScope,
|
||||
onConfirm = { viewModel.onIntent(SearchIntent.OpenSourceManage) },
|
||||
)
|
||||
|
||||
AppModalBottomSheet(
|
||||
show = state.showTypeSheet,
|
||||
onDismissRequest = { viewModel.onIntent(SearchIntent.SetTypeSheetVisible(false)) },
|
||||
title = "搜素类型",
|
||||
show = state.showSettingsSheet,
|
||||
onDismissRequest = { viewModel.onIntent(SearchIntent.SetSettingsSheetVisible(false)) },
|
||||
title = stringResource(R.string.setting),
|
||||
) {
|
||||
Column {
|
||||
SelectionItemCard(
|
||||
title = stringResource(R.string.all),
|
||||
isSelected = state.selectedSourceTypes.isEmpty(),
|
||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
inSelectionMode = true,
|
||||
onToggleSelection = {
|
||||
if (state.selectedSourceTypes.isNotEmpty()) {
|
||||
state.selectedSourceTypes.forEach {
|
||||
viewModel.onIntent(SearchIntent.ToggleSourceType(it))
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
CompactDropdownSettingItem(
|
||||
title = stringResource(R.string.layout_mode),
|
||||
selectedValue = searchLayoutMode.toString(),
|
||||
displayEntries = arrayOf(
|
||||
stringResource(R.string.search_layout_source_grouped),
|
||||
stringResource(R.string.search_layout_list)
|
||||
),
|
||||
entryValues = arrayOf("1", "0"),
|
||||
imageVector = if (isSourceGroupedMode) Icons.Default.GridView else Icons.AutoMirrored.Outlined.FormatListBulleted,
|
||||
onValueChange = { newValue ->
|
||||
if (newValue.toInt() != searchLayoutMode) {
|
||||
viewModel.toggleSearchLayout()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
CompactDropdownSettingItem(
|
||||
title = stringResource(R.string.precision_search),
|
||||
selectedValue = state.matchMode.value.toString(),
|
||||
displayEntries = arrayOf(
|
||||
stringResource(R.string.precision_search),
|
||||
stringResource(R.string.search)
|
||||
),
|
||||
entryValues = arrayOf(
|
||||
MatchMode.EXACT.value.toString(),
|
||||
MatchMode.DEFAULT.value.toString()
|
||||
),
|
||||
imageVector = if (state.matchMode == MatchMode.EXACT) AppIcons.PrecisionSearch else AppIcons.UnPrecisionSearch,
|
||||
onValueChange = { newValue ->
|
||||
val mode = MatchMode.of(newValue.toInt())
|
||||
if (mode != state.matchMode) {
|
||||
viewModel.onIntent(SearchIntent.SetMatchMode(mode))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
listOf(
|
||||
0 to stringResource(R.string.noval),
|
||||
2 to stringResource(R.string.manga),
|
||||
1 to stringResource(R.string.audio),
|
||||
).forEach { (type, label) ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AppIcon(Icons.Default.Layers, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
AppText(
|
||||
text = "搜索类型",
|
||||
style = LegadoTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
SelectionItemCard(
|
||||
title = label,
|
||||
isSelected = state.selectedSourceTypes.contains(type),
|
||||
title = stringResource(R.string.all),
|
||||
isSelected = state.selectedSourceTypes.isEmpty(),
|
||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
inSelectionMode = true,
|
||||
onToggleSelection = {
|
||||
viewModel.onIntent(SearchIntent.ToggleSourceType(type))
|
||||
if (state.selectedSourceTypes.isNotEmpty()) {
|
||||
state.selectedSourceTypes.forEach {
|
||||
viewModel.onIntent(SearchIntent.ToggleSourceType(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
listOf(
|
||||
0 to stringResource(R.string.noval),
|
||||
2 to stringResource(R.string.manga),
|
||||
1 to stringResource(R.string.audio),
|
||||
).forEach { (type, label) ->
|
||||
SelectionItemCard(
|
||||
title = label,
|
||||
isSelected = state.selectedSourceTypes.contains(type),
|
||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
inSelectionMode = true,
|
||||
onToggleSelection = {
|
||||
viewModel.onIntent(SearchIntent.ToggleSourceType(type))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
@@ -588,6 +706,24 @@ fun SearchScreen(
|
||||
viewModel.onAddToShelf(book)
|
||||
},
|
||||
)
|
||||
|
||||
ExpandedSourceSheet(
|
||||
show = state.expandedSourceUrl != null,
|
||||
sourceName = state.expandedSourceName ?: "",
|
||||
books = state.expandedSourceBooks,
|
||||
isLoading = state.expandedSourceLoading,
|
||||
isEnd = state.expandedSourceEnd,
|
||||
errorMsg = state.expandedSourceError,
|
||||
onDismiss = { viewModel.onIntent(SearchIntent.DismissExpandedSource) },
|
||||
onLoadMore = { viewModel.onIntent(SearchIntent.LoadMoreExpandedSource) },
|
||||
onBookClick = { book, coverKey ->
|
||||
viewModel.onIntent(SearchIntent.OpenExpandedSourceBook(book, coverKey))
|
||||
},
|
||||
onBookLongClick = { book, coverKey ->
|
||||
previewBook = book
|
||||
previewSharedCoverKey = coverKey
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private data class SearchFloatingSummary(
|
||||
@@ -653,10 +789,10 @@ private fun SearchSuggestionPanel(
|
||||
}
|
||||
|
||||
if (state.history.isNotEmpty()) {
|
||||
SmallTextButton(
|
||||
SmallPlainButton(
|
||||
onClick = onClearHistory,
|
||||
text = stringResource(R.string.clear_all),
|
||||
imageVector = Icons.Default.Close
|
||||
icon = Icons.Default.Close
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -685,9 +821,9 @@ private fun SearchSuggestionPanel(
|
||||
title = history.word,
|
||||
onToggleSelection = { onUseHistory(history.word) },
|
||||
trailingAction = {
|
||||
SmallIconButton(
|
||||
SmallPlainButton(
|
||||
onClick = { onDeleteHistory(history) },
|
||||
imageVector = Icons.Default.Close,
|
||||
icon = Icons.Default.Close,
|
||||
contentDescription = stringResource(R.string.delete)
|
||||
)
|
||||
}
|
||||
@@ -747,3 +883,76 @@ private fun SearchResultFooter(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class SourceGroup(
|
||||
val origin: String,
|
||||
val sourceName: String,
|
||||
val items: List<SearchResultItemUi>,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun ExpandedSourceSheet(
|
||||
show: Boolean,
|
||||
sourceName: String,
|
||||
books: List<SearchBook>,
|
||||
isLoading: Boolean,
|
||||
isEnd: Boolean,
|
||||
errorMsg: String?,
|
||||
onDismiss: () -> Unit,
|
||||
onLoadMore: () -> Unit,
|
||||
onBookClick: (SearchBook, String?) -> Unit,
|
||||
onBookLongClick: ((SearchBook, String?) -> Unit)? = null,
|
||||
) {
|
||||
AppModalBottomSheet(
|
||||
show = show,
|
||||
onDismissRequest = onDismiss,
|
||||
title = sourceName,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val shouldLoadMore by remember {
|
||||
derivedStateOf {
|
||||
val total = listState.layoutInfo.totalItemsCount
|
||||
val last = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
|
||||
total > 0 && last >= total - 3
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(shouldLoadMore, isLoading, isEnd) {
|
||||
if (shouldLoadMore && !isLoading && !isEnd) {
|
||||
onLoadMore()
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = adaptiveContentPaddingOnlyVertical(
|
||||
top = 8.dp,
|
||||
bottom = 16.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
items(
|
||||
items = books,
|
||||
key = { it.bookUrl },
|
||||
) { book ->
|
||||
SearchBookListItem(
|
||||
book = book,
|
||||
shelfState = BookShelfState.NOT_IN_SHELF,
|
||||
onClick = { onBookClick(book, null) },
|
||||
onLongClick = onBookLongClick,
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
LoadMoreFooter(
|
||||
isLoading = isLoading,
|
||||
errorMsg = errorMsg,
|
||||
isEnd = isEnd,
|
||||
onRetry = onLoadMore,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,50 +2,73 @@ package io.legado.app.ui.book.search
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.data.entities.BookSourcePart
|
||||
import io.legado.app.data.entities.SearchBook
|
||||
import io.legado.app.data.local.preferences.LocalPreferencesKeys
|
||||
import io.legado.app.data.local.preferences.LocalPreferencesRepository
|
||||
import io.legado.app.data.repository.SearchRepository
|
||||
import io.legado.app.domain.model.BookSearchScope
|
||||
import io.legado.app.domain.model.MatchMode
|
||||
import io.legado.app.domain.usecase.AddToBookshelfUseCase
|
||||
import io.legado.app.domain.usecase.BookSearchControl
|
||||
import io.legado.app.domain.usecase.BookSearchRequest
|
||||
import io.legado.app.domain.usecase.BookShelfKey
|
||||
import io.legado.app.domain.usecase.ExploreBooksUseCase
|
||||
import io.legado.app.domain.usecase.ResolveBookShelfStateUseCase
|
||||
import io.legado.app.domain.usecase.SearchBooksUseCase
|
||||
import io.legado.app.domain.usecase.SearchRunEvent
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.ui.config.otherConfig.OtherConfig
|
||||
import io.legado.app.utils.getPrefBoolean
|
||||
import io.legado.app.utils.putPrefBoolean
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import splitties.init.appCtx
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class SearchViewModel(
|
||||
private val repository: SearchRepository,
|
||||
private val resolveBookShelfStateUseCase: ResolveBookShelfStateUseCase,
|
||||
private val searchBooksUseCase: SearchBooksUseCase,
|
||||
private val exploreBooksUseCase: ExploreBooksUseCase,
|
||||
private val addToBookshelfUseCase: AddToBookshelfUseCase,
|
||||
private val localPreferencesRepository: LocalPreferencesRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
val searchLayoutMode = localPreferencesRepository
|
||||
.getPreference(LocalPreferencesKeys.SEARCH_LAYOUT_MODE, 0)
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0)
|
||||
|
||||
fun toggleSearchLayout() {
|
||||
viewModelScope.launch {
|
||||
val newMode = if (searchLayoutMode.value == 0) 1 else 0
|
||||
localPreferencesRepository.updatePreference(
|
||||
LocalPreferencesKeys.SEARCH_LAYOUT_MODE, newMode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val matchModeFlow = localPreferencesRepository
|
||||
.getPreference(LocalPreferencesKeys.MATCH_MODE, MatchMode.DEFAULT.value)
|
||||
.distinctUntilChanged()
|
||||
.map { MatchMode.of(it) }
|
||||
|
||||
private val _uiState = MutableStateFlow(
|
||||
SearchUiState(
|
||||
isPrecisionSearch = appCtx.getPrefBoolean(PreferKey.precisionSearch),
|
||||
scopeDisplay = SearchScope(AppConfig.searchScope).display,
|
||||
scopeDisplayNames = SearchScope(AppConfig.searchScope).displayNames,
|
||||
scopeDisplayNames = SearchScope(AppConfig.searchScope).displayNames.toImmutableList(),
|
||||
isAllScope = SearchScope(AppConfig.searchScope).isAll(),
|
||||
isSourceScope = SearchScope(AppConfig.searchScope).isSource(),
|
||||
)
|
||||
@@ -72,6 +95,7 @@ class SearchViewModel(
|
||||
observeBookshelf()
|
||||
observeQueryHistory()
|
||||
observeQueryBookshelfHints()
|
||||
observeMatchMode()
|
||||
}
|
||||
|
||||
fun onAddToShelf(book: SearchBook) {
|
||||
@@ -153,8 +177,8 @@ class SearchViewModel(
|
||||
_uiState.update { it.copy(showScopeSheet = intent.visible) }
|
||||
}
|
||||
|
||||
is SearchIntent.SetTypeSheetVisible -> {
|
||||
_uiState.update { it.copy(showTypeSheet = intent.visible) }
|
||||
is SearchIntent.SetSettingsSheetVisible -> {
|
||||
_uiState.update { it.copy(showSettingsSheet = intent.visible) }
|
||||
}
|
||||
|
||||
is SearchIntent.ToggleSourceType -> {
|
||||
@@ -184,9 +208,12 @@ class SearchViewModel(
|
||||
syncScopeState(restartSearch = true, oldScope = oldScope)
|
||||
}
|
||||
|
||||
is SearchIntent.TogglePrecision -> {
|
||||
appCtx.putPrefBoolean(PreferKey.precisionSearch, intent.enabled)
|
||||
_uiState.update { it.copy(isPrecisionSearch = intent.enabled) }
|
||||
is SearchIntent.SetMatchMode -> {
|
||||
viewModelScope.launch {
|
||||
localPreferencesRepository.updatePreference(
|
||||
LocalPreferencesKeys.MATCH_MODE, intent.mode.value
|
||||
)
|
||||
}
|
||||
restartCommittedSearchIfNeeded()
|
||||
}
|
||||
|
||||
@@ -197,6 +224,61 @@ class SearchViewModel(
|
||||
|
||||
SearchIntent.OpenSourceManage -> emitEffect(SearchEffect.OpenSourceManage)
|
||||
|
||||
is SearchIntent.ExpandSource -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
expandedSourceUrl = intent.sourceUrl,
|
||||
expandedSourceName = intent.sourceName,
|
||||
expandedSourceBooks = persistentListOf(),
|
||||
expandedSourceLoading = true,
|
||||
expandedSourceEnd = false,
|
||||
expandedSourceError = null,
|
||||
expandedSourcePage = 1,
|
||||
)
|
||||
}
|
||||
loadExpandedSourcePage(intent.sourceUrl, page = 1)
|
||||
}
|
||||
|
||||
SearchIntent.DismissExpandedSource -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
expandedSourceUrl = null,
|
||||
expandedSourceName = null,
|
||||
expandedSourceBooks = persistentListOf(),
|
||||
expandedSourceLoading = false,
|
||||
expandedSourceEnd = false,
|
||||
expandedSourceError = null,
|
||||
expandedSourcePage = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SearchIntent.LoadMoreExpandedSource -> {
|
||||
val state = _uiState.value
|
||||
val sourceUrl = state.expandedSourceUrl ?: return
|
||||
if (state.expandedSourceLoading || state.expandedSourceEnd) return
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
expandedSourceLoading = true,
|
||||
expandedSourceError = null
|
||||
)
|
||||
}
|
||||
loadExpandedSourcePage(sourceUrl, page = state.expandedSourcePage)
|
||||
}
|
||||
|
||||
is SearchIntent.OpenExpandedSourceBook -> {
|
||||
emitEffect(
|
||||
SearchEffect.OpenBookInfo(
|
||||
name = intent.book.name,
|
||||
author = intent.book.author,
|
||||
bookUrl = intent.book.bookUrl,
|
||||
origin = intent.book.origin,
|
||||
coverPath = intent.book.coverUrl,
|
||||
sharedCoverKey = intent.sharedCoverKey,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is SearchIntent.SaveScrollState -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
@@ -233,7 +315,7 @@ class SearchViewModel(
|
||||
repository.enabledGroups
|
||||
.catch { emit(emptyList()) }
|
||||
.collect { groups ->
|
||||
_uiState.update { it.copy(enabledGroups = groups) }
|
||||
_uiState.update { it.copy(enabledGroups = groups.toImmutableList()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,7 +325,7 @@ class SearchViewModel(
|
||||
repository.enabledSources
|
||||
.catch { emit(emptyList()) }
|
||||
.collect { sources ->
|
||||
_uiState.update { it.copy(enabledSources = sources) }
|
||||
_uiState.update { it.copy(enabledSources = sources.toImmutableList()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,7 +337,7 @@ class SearchViewModel(
|
||||
.collect { keys ->
|
||||
bookshelfKeys.value = keys
|
||||
_uiState.update { state ->
|
||||
state.copy(results = state.results.withShelfState(keys))
|
||||
state.copy(results = state.results.withShelfState(keys).toImmutableList())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,7 +351,7 @@ class SearchViewModel(
|
||||
.flatMapLatest { repository.searchHistory(it) }
|
||||
.catch { emit(emptyList()) }
|
||||
.collect { history ->
|
||||
_uiState.update { it.copy(history = history) }
|
||||
_uiState.update { it.copy(history = history.toImmutableList()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,11 +364,19 @@ class SearchViewModel(
|
||||
.flatMapLatest { repository.searchBookshelf(it) }
|
||||
.catch { emit(emptyList()) }
|
||||
.collect { books ->
|
||||
_uiState.update { it.copy(bookshelfHints = books) }
|
||||
_uiState.update { it.copy(bookshelfHints = books.toImmutableList()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeMatchMode() {
|
||||
viewModelScope.launch {
|
||||
matchModeFlow.collect { mode ->
|
||||
_uiState.update { it.copy(matchMode = mode) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateQuery(query: String, showSuggestions: Boolean) {
|
||||
val currentState = _uiState.value
|
||||
val isSameQuery = currentState.query == query
|
||||
@@ -319,7 +409,7 @@ class SearchViewModel(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
committedQuery = keyword,
|
||||
results = emptyList(),
|
||||
results = persistentListOf(),
|
||||
isManualStop = false,
|
||||
hasMore = true,
|
||||
processedSources = 0,
|
||||
@@ -362,7 +452,7 @@ class SearchViewModel(
|
||||
keyword = keyword,
|
||||
page = page,
|
||||
scope = BookSearchScope(searchScope.toString()),
|
||||
precision = _uiState.value.isPrecisionSearch,
|
||||
matchMode = _uiState.value.matchMode,
|
||||
concurrency = OtherConfig.threadCount,
|
||||
types = _uiState.value.selectedSourceTypes.takeIf { it.isNotEmpty() },
|
||||
),
|
||||
@@ -395,7 +485,7 @@ class SearchViewModel(
|
||||
it.copy(
|
||||
results = buildSearchResultItems(
|
||||
shelf = bookshelfKeys.value,
|
||||
),
|
||||
).toImmutableList(),
|
||||
processedSources = event.processedSources,
|
||||
totalSources = event.totalSources,
|
||||
)
|
||||
@@ -407,7 +497,7 @@ class SearchViewModel(
|
||||
val emptyAction = if (searchResultBooks.isEmpty() && event.isEmpty && !searchScope.isAll()) {
|
||||
SearchEmptyScopeAction(
|
||||
scopeDisplay = searchScope.display,
|
||||
wasPrecisionSearch = state.isPrecisionSearch,
|
||||
wasMatchMode = state.matchMode,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -441,7 +531,7 @@ class SearchViewModel(
|
||||
it.copy(
|
||||
query = "",
|
||||
committedQuery = "",
|
||||
results = emptyList(),
|
||||
results = persistentListOf(),
|
||||
processedSources = 0,
|
||||
totalSources = 0,
|
||||
isSearching = false,
|
||||
@@ -497,9 +587,12 @@ class SearchViewModel(
|
||||
val action = _uiState.value.emptyScopeAction ?: return
|
||||
_uiState.update { it.copy(emptyScopeAction = null) }
|
||||
|
||||
if (action.wasPrecisionSearch) {
|
||||
appCtx.putPrefBoolean(PreferKey.precisionSearch, false)
|
||||
_uiState.update { it.copy(isPrecisionSearch = false) }
|
||||
if (action.wasMatchMode == MatchMode.EXACT) {
|
||||
viewModelScope.launch {
|
||||
localPreferencesRepository.updatePreference(
|
||||
LocalPreferencesKeys.MATCH_MODE, MatchMode.DEFAULT.value
|
||||
)
|
||||
}
|
||||
} else {
|
||||
searchScope.update("")
|
||||
syncScopeState()
|
||||
@@ -523,7 +616,7 @@ class SearchViewModel(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
scopeDisplay = searchScope.display,
|
||||
scopeDisplayNames = searchScope.displayNames,
|
||||
scopeDisplayNames = searchScope.displayNames.toImmutableList(),
|
||||
selectedScopeSourceUrls = searchScope.sourceUrls.toSet(),
|
||||
isAllScope = searchScope.isAll(),
|
||||
isSourceScope = searchScope.isSource(),
|
||||
@@ -571,6 +664,37 @@ class SearchViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadExpandedSourcePage(sourceUrl: String, page: Int) {
|
||||
viewModelScope.launch {
|
||||
val keyword = _uiState.value.committedQuery
|
||||
try {
|
||||
val result = exploreBooksUseCase.execute(
|
||||
sourceUrl = sourceUrl,
|
||||
moduleUrl = null,
|
||||
args = null,
|
||||
page = page,
|
||||
key = keyword,
|
||||
)
|
||||
val newBooks = result.books
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
expandedSourceBooks = (it.expandedSourceBooks + newBooks).toImmutableList(),
|
||||
expandedSourceLoading = false,
|
||||
expandedSourceEnd = newBooks.isEmpty(),
|
||||
expandedSourcePage = page + 1,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
expandedSourceLoading = false,
|
||||
expandedSourceError = e.message ?: "Unknown error",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitEffect(effect: SearchEffect) {
|
||||
_effects.tryEmit(effect)
|
||||
}
|
||||
|
||||
@@ -34,11 +34,9 @@ import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import io.legado.app.ui.widget.components.AppFloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import io.legado.app.ui.widget.components.progressIndicator.AppLinearProgressIndicator
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -57,20 +55,23 @@ import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.data.entities.SearchContentHistory
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import io.legado.app.ui.widget.components.AppFloatingActionButton
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
import io.legado.app.ui.widget.components.SearchBar
|
||||
import io.legado.app.ui.widget.components.button.MediumOutlinedButton
|
||||
import io.legado.app.ui.widget.components.button.SmallAnimatedActionButton
|
||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||
import io.legado.app.ui.widget.components.topbar.TopBarAnimatedActionButton
|
||||
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||
import io.legado.app.ui.widget.components.button.SmallPlainButton
|
||||
import io.legado.app.ui.widget.components.button.SmallToggleButton
|
||||
import io.legado.app.ui.widget.components.button.ToggleStyle
|
||||
import io.legado.app.ui.widget.components.card.TextCard
|
||||
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
|
||||
import io.legado.app.ui.widget.components.progressIndicator.AppLinearProgressIndicator
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
||||
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
||||
import io.legado.app.ui.widget.components.topbar.TopBarAnimatedActionButton
|
||||
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@@ -302,14 +303,14 @@ fun SearchHistoryList(
|
||||
color = LegadoTheme.colorScheme.primary,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
SmallAnimatedActionButton(
|
||||
SmallToggleButton(
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
checked = onlyThisBook,
|
||||
onCheckedChange = { onToggleScope() },
|
||||
style = ToggleStyle.Tonal,
|
||||
iconChecked = Icons.Default.Book,
|
||||
iconUnchecked = Icons.Default.CollectionsBookmark,
|
||||
activeText = "仅本书",
|
||||
inactiveText = "所有记录"
|
||||
icon = Icons.Default.CollectionsBookmark,
|
||||
text = "仅本书"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -338,9 +339,9 @@ fun SearchHistoryList(
|
||||
Icon(Icons.Default.History, contentDescription = null)
|
||||
},
|
||||
trailingContent = {
|
||||
SmallIconButton(
|
||||
SmallPlainButton(
|
||||
onClick = { onDeleteHistory(item) },
|
||||
imageVector = Icons.Default.Close,
|
||||
icon = Icons.Default.Close,
|
||||
contentDescription = "删除"
|
||||
)
|
||||
},
|
||||
@@ -361,7 +362,7 @@ fun SearchHistoryList(
|
||||
MediumOutlinedButton(
|
||||
onClick = onClearHistory,
|
||||
modifier = Modifier.fillMaxWidth(0.6f),
|
||||
imageVector = Icons.Outlined.DeleteSweep,
|
||||
icon = Icons.Outlined.DeleteSweep,
|
||||
text = "清除搜索历史"
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user