增加搜素分源布局
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.data.entities.SearchBook
|
||||||
import io.legado.app.domain.gateway.BookSearchGateway
|
import io.legado.app.domain.gateway.BookSearchGateway
|
||||||
import io.legado.app.domain.model.BookSearchScope
|
import io.legado.app.domain.model.BookSearchScope
|
||||||
|
import io.legado.app.domain.model.MatchMode
|
||||||
import io.legado.app.exception.NoStackTraceException
|
import io.legado.app.exception.NoStackTraceException
|
||||||
import io.legado.app.model.webBook.WebBook
|
import io.legado.app.model.webBook.WebBook
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
@@ -30,7 +31,7 @@ data class BookSearchRequest(
|
|||||||
val keyword: String,
|
val keyword: String,
|
||||||
val page: Int,
|
val page: Int,
|
||||||
val scope: BookSearchScope,
|
val scope: BookSearchScope,
|
||||||
val precision: Boolean,
|
val matchMode: MatchMode,
|
||||||
val concurrency: Int,
|
val concurrency: Int,
|
||||||
val types: Set<Int>? = null,
|
val types: Set<Int>? = null,
|
||||||
)
|
)
|
||||||
@@ -103,7 +104,7 @@ class SearchBooksUseCase(
|
|||||||
throw NoStackTraceException("可搜索书源为空")
|
throw NoStackTraceException("可搜索书源为空")
|
||||||
}
|
}
|
||||||
|
|
||||||
val merger = SearchResultMerger(keyword, request.precision)
|
val merger = SearchResultMerger(keyword, request.matchMode)
|
||||||
val concurrency = request.concurrency.coerceAtLeast(1)
|
val concurrency = request.concurrency.coerceAtLeast(1)
|
||||||
var hasMore = false
|
var hasMore = false
|
||||||
var processedSources = 0
|
var processedSources = 0
|
||||||
@@ -116,7 +117,7 @@ class SearchBooksUseCase(
|
|||||||
.flatMapMerge(concurrency) { searchableSource ->
|
.flatMapMerge(concurrency) { searchableSource ->
|
||||||
flow {
|
flow {
|
||||||
control.awaitResumed()
|
control.awaitResumed()
|
||||||
emit(searchSource(searchableSource, keyword, request.page, request.precision))
|
emit(searchSource(searchableSource, keyword, request.page, request.matchMode))
|
||||||
}.flowOn(Dispatchers.IO)
|
}.flowOn(Dispatchers.IO)
|
||||||
}
|
}
|
||||||
.collect { result ->
|
.collect { result ->
|
||||||
@@ -177,7 +178,7 @@ class SearchBooksUseCase(
|
|||||||
searchableSource: SearchableSource,
|
searchableSource: SearchableSource,
|
||||||
keyword: String,
|
keyword: String,
|
||||||
page: Int,
|
page: Int,
|
||||||
precision: Boolean,
|
matchMode: MatchMode,
|
||||||
): SourceSearchResult {
|
): SourceSearchResult {
|
||||||
return try {
|
return try {
|
||||||
val source = searchableSource.source
|
val source = searchableSource.source
|
||||||
@@ -191,7 +192,7 @@ class SearchBooksUseCase(
|
|||||||
keyword,
|
keyword,
|
||||||
page,
|
page,
|
||||||
filter = { name, author ->
|
filter = { name, author ->
|
||||||
!precision ||
|
matchMode == MatchMode.DEFAULT ||
|
||||||
name.contains(keyword, ignoreCase = true) ||
|
name.contains(keyword, ignoreCase = true) ||
|
||||||
author.contains(keyword, ignoreCase = true)
|
author.contains(keyword, ignoreCase = true)
|
||||||
}
|
}
|
||||||
@@ -222,7 +223,7 @@ class SearchBooksUseCase(
|
|||||||
|
|
||||||
private class SearchResultMerger(
|
private class SearchResultMerger(
|
||||||
private val keyword: String,
|
private val keyword: String,
|
||||||
private val precision: Boolean,
|
private val matchMode: MatchMode,
|
||||||
) {
|
) {
|
||||||
private companion object {
|
private companion object {
|
||||||
const val MAX_RETAINED_SEARCH_RESULTS = 1000
|
const val MAX_RETAINED_SEARCH_RESULTS = 1000
|
||||||
@@ -267,9 +268,13 @@ class SearchBooksUseCase(
|
|||||||
book.name.equals(keyword, ignoreCase = true) ||
|
book.name.equals(keyword, ignoreCase = true) ||
|
||||||
book.author.equals(keyword, ignoreCase = true) -> equalBooks
|
book.author.equals(keyword, ignoreCase = true) -> equalBooks
|
||||||
book.name.contains(keyword, ignoreCase = true) ||
|
book.name.contains(keyword, ignoreCase = true) ||
|
||||||
book.author.contains(keyword, ignoreCase = true) -> containsBooks
|
book.author.contains(
|
||||||
!precision -> otherBooks
|
keyword,
|
||||||
else -> null
|
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.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Check
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
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.data.entities.BookSourcePart
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.widget.components.SearchBar
|
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.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.modalBottomSheet.AppModalBottomSheet
|
||||||
import io.legado.app.ui.widget.components.tabRow.AppTabRow
|
import io.legado.app.ui.widget.components.tabRow.AppTabRow
|
||||||
|
|
||||||
@@ -68,9 +67,9 @@ fun ScopeSelectSheet(
|
|||||||
title = title,
|
title = title,
|
||||||
endAction = onConfirm?.let {
|
endAction = onConfirm?.let {
|
||||||
{
|
{
|
||||||
MediumIconButton(
|
MediumPlainButton(
|
||||||
onClick = it,
|
onClick = it,
|
||||||
imageVector = Icons.Default.Check
|
icon = AppIcons.Settings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class SearchActivity : BaseComposeActivity() {
|
|||||||
},
|
},
|
||||||
onOpenSourceManage = {
|
onOpenSourceManage = {
|
||||||
startActivity<BookSourceActivity>()
|
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.SearchBook
|
||||||
import io.legado.app.data.entities.SearchKeyword
|
import io.legado.app.data.entities.SearchKeyword
|
||||||
import io.legado.app.domain.model.BookShelfState
|
import io.legado.app.domain.model.BookShelfState
|
||||||
|
import io.legado.app.domain.model.MatchMode
|
||||||
import io.legado.app.ui.main.bookshelf.BookShelfItem
|
import io.legado.app.ui.main.bookshelf.BookShelfItem
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
data class SearchResultItemUi(
|
data class SearchResultItemUi(
|
||||||
@@ -17,17 +20,17 @@ data class SearchResultItemUi(
|
|||||||
data class SearchUiState(
|
data class SearchUiState(
|
||||||
val query: String = "",
|
val query: String = "",
|
||||||
val committedQuery: String = "",
|
val committedQuery: String = "",
|
||||||
val results: List<SearchResultItemUi> = emptyList(),
|
val results: ImmutableList<SearchResultItemUi> = persistentListOf(),
|
||||||
val history: List<SearchKeyword> = emptyList(),
|
val history: ImmutableList<SearchKeyword> = persistentListOf(),
|
||||||
val bookshelfHints: List<BookShelfItem> = emptyList(),
|
val bookshelfHints: ImmutableList<BookShelfItem> = persistentListOf(),
|
||||||
val enabledGroups: List<String> = emptyList(),
|
val enabledGroups: ImmutableList<String> = persistentListOf(),
|
||||||
val enabledSources: List<BookSourcePart> = emptyList(),
|
val enabledSources: ImmutableList<BookSourcePart> = persistentListOf(),
|
||||||
val scopeDisplay: String = "",
|
val scopeDisplay: String = "",
|
||||||
val scopeDisplayNames: List<String> = emptyList(),
|
val scopeDisplayNames: ImmutableList<String> = persistentListOf(),
|
||||||
val selectedScopeSourceUrls: Set<String> = emptySet(),
|
val selectedScopeSourceUrls: Set<String> = emptySet(),
|
||||||
val isAllScope: Boolean = true,
|
val isAllScope: Boolean = true,
|
||||||
val isSourceScope: Boolean = false,
|
val isSourceScope: Boolean = false,
|
||||||
val isPrecisionSearch: Boolean = false,
|
val matchMode: MatchMode = MatchMode.DEFAULT,
|
||||||
val isSearching: Boolean = false,
|
val isSearching: Boolean = false,
|
||||||
val isManualStop: Boolean = false,
|
val isManualStop: Boolean = false,
|
||||||
val hasMore: Boolean = true,
|
val hasMore: Boolean = true,
|
||||||
@@ -35,17 +38,24 @@ data class SearchUiState(
|
|||||||
val totalSources: Int = 0,
|
val totalSources: Int = 0,
|
||||||
val selectedSourceTypes: Set<Int> = emptySet(),
|
val selectedSourceTypes: Set<Int> = emptySet(),
|
||||||
val showScopeSheet: Boolean = false,
|
val showScopeSheet: Boolean = false,
|
||||||
val showTypeSheet: Boolean = false,
|
val showSettingsSheet: Boolean = false,
|
||||||
val showClearHistoryDialog: Boolean = false,
|
val showClearHistoryDialog: Boolean = false,
|
||||||
val showSuggestions: Boolean = true,
|
val showSuggestions: Boolean = true,
|
||||||
val emptyScopeAction: SearchEmptyScopeAction? = null,
|
val emptyScopeAction: SearchEmptyScopeAction? = null,
|
||||||
val savedScrollIndex: Int = 0,
|
val savedScrollIndex: Int = 0,
|
||||||
val savedScrollOffset: 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(
|
data class SearchEmptyScopeAction(
|
||||||
val scopeDisplay: String,
|
val scopeDisplay: String,
|
||||||
val wasPrecisionSearch: Boolean,
|
val wasMatchMode: MatchMode,
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed interface SearchIntent {
|
sealed interface SearchIntent {
|
||||||
@@ -60,17 +70,22 @@ sealed interface SearchIntent {
|
|||||||
data class UseHistoryKeyword(val keyword: String) : SearchIntent
|
data class UseHistoryKeyword(val keyword: String) : SearchIntent
|
||||||
data class OpenSearchBook(val book: SearchBook, val sharedCoverKey: String?) : SearchIntent
|
data class OpenSearchBook(val book: SearchBook, val sharedCoverKey: String?) : SearchIntent
|
||||||
data class OpenBookshelfBook(val book: BookShelfItem) : 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 DeleteHistory(val item: SearchKeyword) : SearchIntent
|
||||||
data class SetClearHistoryDialogVisible(val visible: Boolean) : SearchIntent
|
data class SetClearHistoryDialogVisible(val visible: Boolean) : SearchIntent
|
||||||
data object ConfirmClearHistory : SearchIntent
|
data object ConfirmClearHistory : SearchIntent
|
||||||
data class SetScopeSheetVisible(val visible: Boolean) : 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 class ToggleSourceType(val type: Int) : SearchIntent
|
||||||
data object SelectAllScope : SearchIntent
|
data object SelectAllScope : SearchIntent
|
||||||
data class ToggleScopeGroup(val groupName: String) : SearchIntent
|
data class ToggleScopeGroup(val groupName: String) : SearchIntent
|
||||||
data class ToggleScopeSource(val source: BookSourcePart) : SearchIntent
|
data class ToggleScopeSource(val source: BookSourcePart) : SearchIntent
|
||||||
data class RemoveScopeItem(val scopeName: String) : 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 ConfirmEmptyScopeAction : SearchIntent
|
||||||
data object DismissEmptyScopeAction : SearchIntent
|
data object DismissEmptyScopeAction : SearchIntent
|
||||||
data object OpenSourceManage : 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.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
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.Book
|
||||||
import androidx.compose.material.icons.filled.Close
|
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.Layers
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.filled.Stop
|
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.SearchBook
|
||||||
import io.legado.app.data.entities.SearchKeyword
|
import io.legado.app.data.entities.SearchKeyword
|
||||||
import io.legado.app.domain.model.BookShelfState
|
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.bookCoverSharedElementKey
|
||||||
import io.legado.app.ui.main.bookshelf.BookShelfItem
|
import io.legado.app.ui.main.bookshelf.BookShelfItem
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
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.theme.adaptiveHorizontalPadding
|
||||||
import io.legado.app.ui.widget.components.AppFloatingActionButton
|
import io.legado.app.ui.widget.components.AppFloatingActionButton
|
||||||
import io.legado.app.ui.widget.components.AppScaffold
|
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.SearchBar
|
||||||
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.book.SearchBookListItem
|
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.book.SearchBookPreviewSheet
|
||||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
import io.legado.app.ui.widget.components.button.SmallPlainButton
|
||||||
import io.legado.app.ui.widget.components.button.SmallTextButton
|
|
||||||
import io.legado.app.ui.widget.components.card.NormalCard
|
import io.legado.app.ui.widget.components.card.NormalCard
|
||||||
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||||
import io.legado.app.ui.widget.components.icon.AppIcon
|
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.list.TopFloatingStickyItem
|
||||||
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.progressIndicator.AppCircularProgressIndicator
|
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.text.AppText
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
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.GlassTopAppBarDefaults
|
||||||
import io.legado.app.ui.widget.components.topbar.M3GlassScrollBehavior
|
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.TopBarAnimatedActionButton
|
||||||
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
@@ -100,6 +103,8 @@ fun SearchScreen(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
val searchLayoutMode by viewModel.searchLayoutMode.collectAsStateWithLifecycle()
|
||||||
|
val isSourceGroupedMode = searchLayoutMode == 1
|
||||||
var previewBook by remember { mutableStateOf<SearchBook?>(null) }
|
var previewBook by remember { mutableStateOf<SearchBook?>(null) }
|
||||||
var previewSharedCoverKey by remember { mutableStateOf<String?>(null) }
|
var previewSharedCoverKey by remember { mutableStateOf<String?>(null) }
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
@@ -149,13 +154,15 @@ fun SearchScreen(
|
|||||||
state.hasMore,
|
state.hasMore,
|
||||||
state.isManualStop,
|
state.isManualStop,
|
||||||
state.showSuggestions,
|
state.showSuggestions,
|
||||||
|
isSourceGroupedMode,
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
shouldLoadMore &&
|
shouldLoadMore &&
|
||||||
!state.isSearching &&
|
!state.isSearching &&
|
||||||
state.hasMore &&
|
state.hasMore &&
|
||||||
!state.isManualStop &&
|
!state.isManualStop &&
|
||||||
!state.showSuggestions
|
!state.showSuggestions &&
|
||||||
|
!isSourceGroupedMode
|
||||||
) {
|
) {
|
||||||
viewModel.onIntent(SearchIntent.LoadMore)
|
viewModel.onIntent(SearchIntent.LoadMore)
|
||||||
}
|
}
|
||||||
@@ -261,32 +268,15 @@ 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 = isSourceGroupedMode || state.matchMode == MatchMode.EXACT || state.selectedSourceTypes.isNotEmpty(),
|
||||||
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(),
|
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
viewModel.onIntent(SearchIntent.SetTypeSheetVisible(true))
|
viewModel.onIntent(SearchIntent.SetSettingsSheetVisible(true))
|
||||||
},
|
},
|
||||||
iconChecked = Icons.Default.Layers,
|
iconChecked = AppIcons.Settings,
|
||||||
iconUnchecked = Icons.Default.Layers,
|
iconUnchecked = AppIcons.Settings,
|
||||||
activeText = "搜素类型",
|
activeText = stringResource(R.string.setting),
|
||||||
inactiveText = "搜素类型",
|
inactiveText = stringResource(R.string.setting),
|
||||||
)
|
)
|
||||||
TopBarAnimatedActionButton(
|
TopBarAnimatedActionButton(
|
||||||
checked = !state.isAllScope,
|
checked = !state.isAllScope,
|
||||||
@@ -296,7 +286,7 @@ fun SearchScreen(
|
|||||||
iconChecked = AppIcons.Filter,
|
iconChecked = AppIcons.Filter,
|
||||||
iconUnchecked = AppIcons.Filter,
|
iconUnchecked = AppIcons.Filter,
|
||||||
activeText = stringResource(R.string.screen),
|
activeText = stringResource(R.string.screen),
|
||||||
inactiveText = stringResource(R.string.screen),
|
inactiveText = stringResource(R.string.screen)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
@@ -315,12 +305,13 @@ fun SearchScreen(
|
|||||||
placeholder = searchLabel,
|
placeholder = searchLabel,
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (queryInput.isNotEmpty()) {
|
if (queryInput.isNotEmpty()) {
|
||||||
TopBarActionButton(
|
SmallPlainButton(
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
queryInput = ""
|
queryInput = ""
|
||||||
viewModel.onIntent(SearchIntent.UpdateQuery(""))
|
viewModel.onIntent(SearchIntent.UpdateQuery(""))
|
||||||
},
|
},
|
||||||
imageVector = AppIcons.Close,
|
icon = AppIcons.Close,
|
||||||
contentDescription = stringResource(R.string.clear)
|
contentDescription = stringResource(R.string.clear)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -394,6 +385,76 @@ fun SearchScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state.results.isNotEmpty()) {
|
if (state.results.isNotEmpty()) {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = listState,
|
state = listState,
|
||||||
@@ -401,7 +462,7 @@ fun SearchScreen(
|
|||||||
top = 48.dp,
|
top = 48.dp,
|
||||||
bottom = 8.dp
|
bottom = 8.dp
|
||||||
),
|
),
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
items = state.results,
|
items = state.results,
|
||||||
@@ -443,6 +504,8 @@ fun SearchScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TopFloatingStickyItem(
|
TopFloatingStickyItem(
|
||||||
item = if (showResultCountCard || showSourceProgressCard) {
|
item = if (showResultCountCard || showSourceProgressCard) {
|
||||||
@@ -502,7 +565,7 @@ fun SearchScreen(
|
|||||||
},
|
},
|
||||||
title = stringResource(R.string.draw),
|
title = stringResource(R.string.draw),
|
||||||
textProvider = {
|
textProvider = {
|
||||||
if (wasPrecisionSearch) {
|
if (wasMatchMode == MatchMode.EXACT) {
|
||||||
stringResource(R.string.search_empty_scope_disable_precision, scopeDisplay)
|
stringResource(R.string.search_empty_scope_disable_precision, scopeDisplay)
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.search_empty_scope_switch_all, scopeDisplay)
|
stringResource(R.string.search_empty_scope_switch_all, scopeDisplay)
|
||||||
@@ -525,15 +588,72 @@ fun SearchScreen(
|
|||||||
sources = state.enabledSources,
|
sources = state.enabledSources,
|
||||||
selectedSources = state.selectedScopeSourceUrls,
|
selectedSources = state.selectedScopeSourceUrls,
|
||||||
onToggleSource = { viewModel.onIntent(SearchIntent.ToggleScopeSource(it)) },
|
onToggleSource = { viewModel.onIntent(SearchIntent.ToggleScopeSource(it)) },
|
||||||
isSourceScope = state.isSourceScope
|
isSourceScope = state.isSourceScope,
|
||||||
|
onConfirm = { viewModel.onIntent(SearchIntent.OpenSourceManage) },
|
||||||
)
|
)
|
||||||
|
|
||||||
AppModalBottomSheet(
|
AppModalBottomSheet(
|
||||||
show = state.showTypeSheet,
|
show = state.showSettingsSheet,
|
||||||
onDismissRequest = { viewModel.onIntent(SearchIntent.SetTypeSheetVisible(false)) },
|
onDismissRequest = { viewModel.onIntent(SearchIntent.SetSettingsSheetVisible(false)) },
|
||||||
title = "搜素类型",
|
title = stringResource(R.string.setting),
|
||||||
|
) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
) {
|
) {
|
||||||
Column {
|
|
||||||
SelectionItemCard(
|
SelectionItemCard(
|
||||||
title = stringResource(R.string.all),
|
title = stringResource(R.string.all),
|
||||||
isSelected = state.selectedSourceTypes.isEmpty(),
|
isSelected = state.selectedSourceTypes.isEmpty(),
|
||||||
@@ -548,8 +668,6 @@ fun SearchScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
0 to stringResource(R.string.noval),
|
0 to stringResource(R.string.noval),
|
||||||
2 to stringResource(R.string.manga),
|
2 to stringResource(R.string.manga),
|
||||||
@@ -564,7 +682,7 @@ fun SearchScreen(
|
|||||||
viewModel.onIntent(SearchIntent.ToggleSourceType(type))
|
viewModel.onIntent(SearchIntent.ToggleSourceType(type))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
@@ -588,6 +706,24 @@ fun SearchScreen(
|
|||||||
viewModel.onAddToShelf(book)
|
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(
|
private data class SearchFloatingSummary(
|
||||||
@@ -653,10 +789,10 @@ private fun SearchSuggestionPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state.history.isNotEmpty()) {
|
if (state.history.isNotEmpty()) {
|
||||||
SmallTextButton(
|
SmallPlainButton(
|
||||||
onClick = onClearHistory,
|
onClick = onClearHistory,
|
||||||
text = stringResource(R.string.clear_all),
|
text = stringResource(R.string.clear_all),
|
||||||
imageVector = Icons.Default.Close
|
icon = Icons.Default.Close
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -685,9 +821,9 @@ private fun SearchSuggestionPanel(
|
|||||||
title = history.word,
|
title = history.word,
|
||||||
onToggleSelection = { onUseHistory(history.word) },
|
onToggleSelection = { onUseHistory(history.word) },
|
||||||
trailingAction = {
|
trailingAction = {
|
||||||
SmallIconButton(
|
SmallPlainButton(
|
||||||
onClick = { onDeleteHistory(history) },
|
onClick = { onDeleteHistory(history) },
|
||||||
imageVector = Icons.Default.Close,
|
icon = Icons.Default.Close,
|
||||||
contentDescription = stringResource(R.string.delete)
|
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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import io.legado.app.constant.PreferKey
|
|
||||||
import io.legado.app.data.entities.BookSourcePart
|
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.local.preferences.LocalPreferencesKeys
|
||||||
|
import io.legado.app.data.local.preferences.LocalPreferencesRepository
|
||||||
import io.legado.app.data.repository.SearchRepository
|
import io.legado.app.data.repository.SearchRepository
|
||||||
import io.legado.app.domain.model.BookSearchScope
|
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.AddToBookshelfUseCase
|
||||||
import io.legado.app.domain.usecase.BookSearchControl
|
import io.legado.app.domain.usecase.BookSearchControl
|
||||||
import io.legado.app.domain.usecase.BookSearchRequest
|
import io.legado.app.domain.usecase.BookSearchRequest
|
||||||
import io.legado.app.domain.usecase.BookShelfKey
|
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.ResolveBookShelfStateUseCase
|
||||||
import io.legado.app.domain.usecase.SearchBooksUseCase
|
import io.legado.app.domain.usecase.SearchBooksUseCase
|
||||||
import io.legado.app.domain.usecase.SearchRunEvent
|
import io.legado.app.domain.usecase.SearchRunEvent
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.ui.config.otherConfig.OtherConfig
|
import io.legado.app.ui.config.otherConfig.OtherConfig
|
||||||
import io.legado.app.utils.getPrefBoolean
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import io.legado.app.utils.putPrefBoolean
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import splitties.init.appCtx
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class SearchViewModel(
|
class SearchViewModel(
|
||||||
private val repository: SearchRepository,
|
private val repository: SearchRepository,
|
||||||
private val resolveBookShelfStateUseCase: ResolveBookShelfStateUseCase,
|
private val resolveBookShelfStateUseCase: ResolveBookShelfStateUseCase,
|
||||||
private val searchBooksUseCase: SearchBooksUseCase,
|
private val searchBooksUseCase: SearchBooksUseCase,
|
||||||
|
private val exploreBooksUseCase: ExploreBooksUseCase,
|
||||||
private val addToBookshelfUseCase: AddToBookshelfUseCase,
|
private val addToBookshelfUseCase: AddToBookshelfUseCase,
|
||||||
|
private val localPreferencesRepository: LocalPreferencesRepository,
|
||||||
) : ViewModel() {
|
) : 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(
|
private val _uiState = MutableStateFlow(
|
||||||
SearchUiState(
|
SearchUiState(
|
||||||
isPrecisionSearch = appCtx.getPrefBoolean(PreferKey.precisionSearch),
|
|
||||||
scopeDisplay = SearchScope(AppConfig.searchScope).display,
|
scopeDisplay = SearchScope(AppConfig.searchScope).display,
|
||||||
scopeDisplayNames = SearchScope(AppConfig.searchScope).displayNames,
|
scopeDisplayNames = SearchScope(AppConfig.searchScope).displayNames.toImmutableList(),
|
||||||
isAllScope = SearchScope(AppConfig.searchScope).isAll(),
|
isAllScope = SearchScope(AppConfig.searchScope).isAll(),
|
||||||
isSourceScope = SearchScope(AppConfig.searchScope).isSource(),
|
isSourceScope = SearchScope(AppConfig.searchScope).isSource(),
|
||||||
)
|
)
|
||||||
@@ -72,6 +95,7 @@ class SearchViewModel(
|
|||||||
observeBookshelf()
|
observeBookshelf()
|
||||||
observeQueryHistory()
|
observeQueryHistory()
|
||||||
observeQueryBookshelfHints()
|
observeQueryBookshelfHints()
|
||||||
|
observeMatchMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAddToShelf(book: SearchBook) {
|
fun onAddToShelf(book: SearchBook) {
|
||||||
@@ -153,8 +177,8 @@ class SearchViewModel(
|
|||||||
_uiState.update { it.copy(showScopeSheet = intent.visible) }
|
_uiState.update { it.copy(showScopeSheet = intent.visible) }
|
||||||
}
|
}
|
||||||
|
|
||||||
is SearchIntent.SetTypeSheetVisible -> {
|
is SearchIntent.SetSettingsSheetVisible -> {
|
||||||
_uiState.update { it.copy(showTypeSheet = intent.visible) }
|
_uiState.update { it.copy(showSettingsSheet = intent.visible) }
|
||||||
}
|
}
|
||||||
|
|
||||||
is SearchIntent.ToggleSourceType -> {
|
is SearchIntent.ToggleSourceType -> {
|
||||||
@@ -184,9 +208,12 @@ class SearchViewModel(
|
|||||||
syncScopeState(restartSearch = true, oldScope = oldScope)
|
syncScopeState(restartSearch = true, oldScope = oldScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
is SearchIntent.TogglePrecision -> {
|
is SearchIntent.SetMatchMode -> {
|
||||||
appCtx.putPrefBoolean(PreferKey.precisionSearch, intent.enabled)
|
viewModelScope.launch {
|
||||||
_uiState.update { it.copy(isPrecisionSearch = intent.enabled) }
|
localPreferencesRepository.updatePreference(
|
||||||
|
LocalPreferencesKeys.MATCH_MODE, intent.mode.value
|
||||||
|
)
|
||||||
|
}
|
||||||
restartCommittedSearchIfNeeded()
|
restartCommittedSearchIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +224,61 @@ class SearchViewModel(
|
|||||||
|
|
||||||
SearchIntent.OpenSourceManage -> emitEffect(SearchEffect.OpenSourceManage)
|
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 -> {
|
is SearchIntent.SaveScrollState -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -233,7 +315,7 @@ class SearchViewModel(
|
|||||||
repository.enabledGroups
|
repository.enabledGroups
|
||||||
.catch { emit(emptyList()) }
|
.catch { emit(emptyList()) }
|
||||||
.collect { groups ->
|
.collect { groups ->
|
||||||
_uiState.update { it.copy(enabledGroups = groups) }
|
_uiState.update { it.copy(enabledGroups = groups.toImmutableList()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,7 +325,7 @@ class SearchViewModel(
|
|||||||
repository.enabledSources
|
repository.enabledSources
|
||||||
.catch { emit(emptyList()) }
|
.catch { emit(emptyList()) }
|
||||||
.collect { sources ->
|
.collect { sources ->
|
||||||
_uiState.update { it.copy(enabledSources = sources) }
|
_uiState.update { it.copy(enabledSources = sources.toImmutableList()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,7 +337,7 @@ class SearchViewModel(
|
|||||||
.collect { keys ->
|
.collect { keys ->
|
||||||
bookshelfKeys.value = keys
|
bookshelfKeys.value = keys
|
||||||
_uiState.update { state ->
|
_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) }
|
.flatMapLatest { repository.searchHistory(it) }
|
||||||
.catch { emit(emptyList()) }
|
.catch { emit(emptyList()) }
|
||||||
.collect { history ->
|
.collect { history ->
|
||||||
_uiState.update { it.copy(history = history) }
|
_uiState.update { it.copy(history = history.toImmutableList()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,7 +364,15 @@ class SearchViewModel(
|
|||||||
.flatMapLatest { repository.searchBookshelf(it) }
|
.flatMapLatest { repository.searchBookshelf(it) }
|
||||||
.catch { emit(emptyList()) }
|
.catch { emit(emptyList()) }
|
||||||
.collect { books ->
|
.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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,7 +409,7 @@ class SearchViewModel(
|
|||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
committedQuery = keyword,
|
committedQuery = keyword,
|
||||||
results = emptyList(),
|
results = persistentListOf(),
|
||||||
isManualStop = false,
|
isManualStop = false,
|
||||||
hasMore = true,
|
hasMore = true,
|
||||||
processedSources = 0,
|
processedSources = 0,
|
||||||
@@ -362,7 +452,7 @@ class SearchViewModel(
|
|||||||
keyword = keyword,
|
keyword = keyword,
|
||||||
page = page,
|
page = page,
|
||||||
scope = BookSearchScope(searchScope.toString()),
|
scope = BookSearchScope(searchScope.toString()),
|
||||||
precision = _uiState.value.isPrecisionSearch,
|
matchMode = _uiState.value.matchMode,
|
||||||
concurrency = OtherConfig.threadCount,
|
concurrency = OtherConfig.threadCount,
|
||||||
types = _uiState.value.selectedSourceTypes.takeIf { it.isNotEmpty() },
|
types = _uiState.value.selectedSourceTypes.takeIf { it.isNotEmpty() },
|
||||||
),
|
),
|
||||||
@@ -395,7 +485,7 @@ class SearchViewModel(
|
|||||||
it.copy(
|
it.copy(
|
||||||
results = buildSearchResultItems(
|
results = buildSearchResultItems(
|
||||||
shelf = bookshelfKeys.value,
|
shelf = bookshelfKeys.value,
|
||||||
),
|
).toImmutableList(),
|
||||||
processedSources = event.processedSources,
|
processedSources = event.processedSources,
|
||||||
totalSources = event.totalSources,
|
totalSources = event.totalSources,
|
||||||
)
|
)
|
||||||
@@ -407,7 +497,7 @@ class SearchViewModel(
|
|||||||
val emptyAction = if (searchResultBooks.isEmpty() && event.isEmpty && !searchScope.isAll()) {
|
val emptyAction = if (searchResultBooks.isEmpty() && event.isEmpty && !searchScope.isAll()) {
|
||||||
SearchEmptyScopeAction(
|
SearchEmptyScopeAction(
|
||||||
scopeDisplay = searchScope.display,
|
scopeDisplay = searchScope.display,
|
||||||
wasPrecisionSearch = state.isPrecisionSearch,
|
wasMatchMode = state.matchMode,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -441,7 +531,7 @@ class SearchViewModel(
|
|||||||
it.copy(
|
it.copy(
|
||||||
query = "",
|
query = "",
|
||||||
committedQuery = "",
|
committedQuery = "",
|
||||||
results = emptyList(),
|
results = persistentListOf(),
|
||||||
processedSources = 0,
|
processedSources = 0,
|
||||||
totalSources = 0,
|
totalSources = 0,
|
||||||
isSearching = false,
|
isSearching = false,
|
||||||
@@ -497,9 +587,12 @@ class SearchViewModel(
|
|||||||
val action = _uiState.value.emptyScopeAction ?: return
|
val action = _uiState.value.emptyScopeAction ?: return
|
||||||
_uiState.update { it.copy(emptyScopeAction = null) }
|
_uiState.update { it.copy(emptyScopeAction = null) }
|
||||||
|
|
||||||
if (action.wasPrecisionSearch) {
|
if (action.wasMatchMode == MatchMode.EXACT) {
|
||||||
appCtx.putPrefBoolean(PreferKey.precisionSearch, false)
|
viewModelScope.launch {
|
||||||
_uiState.update { it.copy(isPrecisionSearch = false) }
|
localPreferencesRepository.updatePreference(
|
||||||
|
LocalPreferencesKeys.MATCH_MODE, MatchMode.DEFAULT.value
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
searchScope.update("")
|
searchScope.update("")
|
||||||
syncScopeState()
|
syncScopeState()
|
||||||
@@ -523,7 +616,7 @@ class SearchViewModel(
|
|||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
scopeDisplay = searchScope.display,
|
scopeDisplay = searchScope.display,
|
||||||
scopeDisplayNames = searchScope.displayNames,
|
scopeDisplayNames = searchScope.displayNames.toImmutableList(),
|
||||||
selectedScopeSourceUrls = searchScope.sourceUrls.toSet(),
|
selectedScopeSourceUrls = searchScope.sourceUrls.toSet(),
|
||||||
isAllScope = searchScope.isAll(),
|
isAllScope = searchScope.isAll(),
|
||||||
isSourceScope = searchScope.isSource(),
|
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) {
|
private fun emitEffect(effect: SearchEffect) {
|
||||||
_effects.tryEmit(effect)
|
_effects.tryEmit(effect)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,9 @@ import androidx.compose.material3.Card
|
|||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import io.legado.app.ui.widget.components.AppFloatingActionButton
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import io.legado.app.ui.widget.components.progressIndicator.AppLinearProgressIndicator
|
|
||||||
import androidx.compose.material3.ListItemDefaults
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.animateFloatingActionButton
|
import androidx.compose.material3.animateFloatingActionButton
|
||||||
import androidx.compose.runtime.Composable
|
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.data.entities.SearchContentHistory
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
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.AppScaffold
|
||||||
import io.legado.app.ui.widget.components.EmptyMessage
|
import io.legado.app.ui.widget.components.EmptyMessage
|
||||||
import io.legado.app.ui.widget.components.SearchBar
|
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.MediumOutlinedButton
|
||||||
import io.legado.app.ui.widget.components.button.SmallAnimatedActionButton
|
import io.legado.app.ui.widget.components.button.SmallPlainButton
|
||||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
import io.legado.app.ui.widget.components.button.SmallToggleButton
|
||||||
import io.legado.app.ui.widget.components.topbar.TopBarAnimatedActionButton
|
import io.legado.app.ui.widget.components.button.ToggleStyle
|
||||||
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
|
||||||
import io.legado.app.ui.widget.components.card.TextCard
|
import io.legado.app.ui.widget.components.card.TextCard
|
||||||
import io.legado.app.ui.widget.components.icon.AppIcon
|
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.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.text.AppText
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
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.GlassTopAppBarDefaults
|
||||||
|
import io.legado.app.ui.widget.components.topbar.TopBarAnimatedActionButton
|
||||||
|
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
@@ -302,14 +303,14 @@ fun SearchHistoryList(
|
|||||||
color = LegadoTheme.colorScheme.primary,
|
color = LegadoTheme.colorScheme.primary,
|
||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
SmallAnimatedActionButton(
|
SmallToggleButton(
|
||||||
modifier = Modifier.align(Alignment.CenterEnd),
|
modifier = Modifier.align(Alignment.CenterEnd),
|
||||||
checked = onlyThisBook,
|
checked = onlyThisBook,
|
||||||
onCheckedChange = { onToggleScope() },
|
onCheckedChange = { onToggleScope() },
|
||||||
|
style = ToggleStyle.Tonal,
|
||||||
iconChecked = Icons.Default.Book,
|
iconChecked = Icons.Default.Book,
|
||||||
iconUnchecked = Icons.Default.CollectionsBookmark,
|
icon = Icons.Default.CollectionsBookmark,
|
||||||
activeText = "仅本书",
|
text = "仅本书"
|
||||||
inactiveText = "所有记录"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,9 +339,9 @@ fun SearchHistoryList(
|
|||||||
Icon(Icons.Default.History, contentDescription = null)
|
Icon(Icons.Default.History, contentDescription = null)
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
SmallIconButton(
|
SmallPlainButton(
|
||||||
onClick = { onDeleteHistory(item) },
|
onClick = { onDeleteHistory(item) },
|
||||||
imageVector = Icons.Default.Close,
|
icon = Icons.Default.Close,
|
||||||
contentDescription = "删除"
|
contentDescription = "删除"
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -361,7 +362,7 @@ fun SearchHistoryList(
|
|||||||
MediumOutlinedButton(
|
MediumOutlinedButton(
|
||||||
onClick = onClearHistory,
|
onClick = onClearHistory,
|
||||||
modifier = Modifier.fillMaxWidth(0.6f),
|
modifier = Modifier.fillMaxWidth(0.6f),
|
||||||
imageVector = Icons.Outlined.DeleteSweep,
|
icon = Icons.Outlined.DeleteSweep,
|
||||||
text = "清除搜索历史"
|
text = "清除搜索历史"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user