优化搜素逻辑
This commit is contained in:
8 files changed
+138
-58
No files matched your search
@@ -44,7 +44,12 @@
|
|||||||
"Bash(jar tf *)",
|
"Bash(jar tf *)",
|
||||||
"Read(//d/tmp/**)",
|
"Read(//d/tmp/**)",
|
||||||
"Bash(jar xf *)",
|
"Bash(jar xf *)",
|
||||||
"PowerShell(.\\\\gradlew.bat :app:compileAppDebugKotlin)"
|
"PowerShell(.\\\\gradlew.bat :app:compileAppDebugKotlin)",
|
||||||
|
"PowerShell(.\\\\gradlew.bat :app:compileAppDebugKotlin 2>&1 | Select-Object -Last 5)",
|
||||||
|
"PowerShell(.\\\\gradlew.bat test 2>&1 | Select-Object -Last 10)",
|
||||||
|
"Bash(npx skills *)",
|
||||||
|
"Bash(npx *)",
|
||||||
|
"PowerShell(.\\\\gradlew.bat :app:compileAppDebugKotlin 2>&1 | Select-Object -Last 20)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,13 +16,13 @@ import io.legado.app.utils.LogUtils
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.CoroutineStart
|
import kotlinx.coroutines.CoroutineStart
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
abstract class BaseService : LifecycleService() {
|
abstract class BaseService : LifecycleService() {
|
||||||
|
|
||||||
private val simpleName = this::class.simpleName.toString()
|
private val simpleName = this::class.simpleName.toString()
|
||||||
|
@Volatile
|
||||||
private var isForeground = false
|
private var isForeground = false
|
||||||
|
|
||||||
fun <T> execute(
|
fun <T> execute(
|
||||||
@@ -37,6 +37,8 @@ abstract class BaseService : LifecycleService() {
|
|||||||
@CallSuper
|
@CallSuper
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
startForegroundNotification()
|
||||||
|
isForeground = true
|
||||||
LifecycleHelp.onServiceCreate(this)
|
LifecycleHelp.onServiceCreate(this)
|
||||||
if (!AppConfig.permissionChecked) {
|
if (!AppConfig.permissionChecked) {
|
||||||
AppConfig.permissionChecked = true
|
AppConfig.permissionChecked = true
|
||||||
@@ -49,10 +51,6 @@ abstract class BaseService : LifecycleService() {
|
|||||||
LogUtils.d(simpleName) {
|
LogUtils.d(simpleName) {
|
||||||
"onStartCommand $intent ${intent?.toUri(0)}"
|
"onStartCommand $intent ${intent?.toUri(0)}"
|
||||||
}
|
}
|
||||||
if (!isForeground) {
|
|
||||||
startForegroundNotification()
|
|
||||||
isForeground = true
|
|
||||||
}
|
|
||||||
return super.onStartCommand(intent, flags, startId)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,11 +93,6 @@ abstract class BaseService : LifecycleService() {
|
|||||||
PermissionsCompat.Builder()
|
PermissionsCompat.Builder()
|
||||||
.addPermissions(Permissions.POST_NOTIFICATIONS)
|
.addPermissions(Permissions.POST_NOTIFICATIONS)
|
||||||
.rationale(R.string.notification_permission_rationale)
|
.rationale(R.string.notification_permission_rationale)
|
||||||
.onGranted {
|
|
||||||
if (lifecycleScope.isActive) {
|
|
||||||
startForegroundNotification()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.request()
|
.request()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
PermissionsCompat.Builder()
|
PermissionsCompat.Builder()
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
package io.legado.app.ui.book.search
|
package io.legado.app.ui.book.search
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
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.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.ui.main.bookshelf.BookShelfItem
|
import io.legado.app.ui.main.bookshelf.BookShelfItem
|
||||||
|
|
||||||
|
@Stable
|
||||||
data class SearchResultItemUi(
|
data class SearchResultItemUi(
|
||||||
val book: SearchBook,
|
val book: SearchBook,
|
||||||
val shelfState: BookShelfState = BookShelfState.NOT_IN_SHELF,
|
val shelfState: BookShelfState = BookShelfState.NOT_IN_SHELF,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Stable
|
||||||
data class SearchUiState(
|
data class SearchUiState(
|
||||||
val query: String = "",
|
val query: String = "",
|
||||||
val committedQuery: String = "",
|
val committedQuery: String = "",
|
||||||
@@ -34,6 +37,8 @@ data class SearchUiState(
|
|||||||
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 savedScrollOffset: Int = 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SearchEmptyScopeAction(
|
data class SearchEmptyScopeAction(
|
||||||
@@ -47,6 +52,7 @@ sealed interface SearchIntent {
|
|||||||
data object SubmitSearch : SearchIntent
|
data object SubmitSearch : SearchIntent
|
||||||
data object LoadMore : SearchIntent
|
data object LoadMore : SearchIntent
|
||||||
data object StopSearch : SearchIntent
|
data object StopSearch : SearchIntent
|
||||||
|
data object ClearSearchResults : SearchIntent
|
||||||
data object PauseEngine : SearchIntent
|
data object PauseEngine : SearchIntent
|
||||||
data object ResumeEngine : SearchIntent
|
data object ResumeEngine : SearchIntent
|
||||||
data class UseHistoryKeyword(val keyword: String) : SearchIntent
|
data class UseHistoryKeyword(val keyword: String) : SearchIntent
|
||||||
@@ -64,6 +70,7 @@ sealed interface 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
|
||||||
|
data class SaveScrollState(val index: Int, val offset: Int) : SearchIntent
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface SearchEffect {
|
sealed interface SearchEffect {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package io.legado.app.ui.book.search
|
package io.legado.app.ui.book.search
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibilityScope
|
import androidx.compose.animation.AnimatedVisibilityScope
|
||||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||||
import androidx.compose.animation.SharedTransitionScope
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -17,35 +17,22 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.items
|
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.filled.ArrowBack
|
|
||||||
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.FilterList
|
|
||||||
import androidx.compose.material.icons.filled.History
|
import androidx.compose.material.icons.filled.History
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.filled.Search
|
|
||||||
import androidx.compose.material.icons.filled.Settings
|
|
||||||
import androidx.compose.material.icons.filled.Stop
|
import androidx.compose.material.icons.filled.Stop
|
||||||
import androidx.compose.material3.AssistChip
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilterChip
|
|
||||||
import androidx.compose.material3.FloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
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.M3GlassScrollBehavior
|
import io.legado.app.ui.widget.components.topbar.M3GlassScrollBehavior
|
||||||
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.GlassTopAppBarScrollBehavior
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -63,8 +50,10 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.data.entities.SearchKeyword
|
import io.legado.app.data.entities.SearchKeyword
|
||||||
@@ -84,11 +73,9 @@ 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.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.button.ToggleChip
|
|
||||||
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
|
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.ui.widget.components.card.GlassCard
|
|
||||||
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.icon.AppIcons
|
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
|
||||||
@@ -113,6 +100,7 @@ fun SearchScreen(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
var queryInput by rememberSaveable { mutableStateOf(state.query) }
|
var queryInput by rememberSaveable { mutableStateOf(state.query) }
|
||||||
var scopeSheetTab by rememberSaveable { mutableStateOf(0) }
|
var scopeSheetTab by rememberSaveable { mutableStateOf(0) }
|
||||||
var ignoreNextDebouncedQuery by rememberSaveable { mutableStateOf<String?>(null) }
|
var ignoreNextDebouncedQuery by rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
@@ -190,9 +178,50 @@ fun SearchScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Activity lifecycle (e.g., Home button, switching apps)
|
||||||
|
DisposableEffect(lifecycleOwner, viewModel) {
|
||||||
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
|
when (event) {
|
||||||
|
Lifecycle.Event.ON_RESUME -> viewModel.onIntent(SearchIntent.ResumeEngine)
|
||||||
|
Lifecycle.Event.ON_PAUSE -> viewModel.onIntent(SearchIntent.PauseEngine)
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleOwner.lifecycle.addObserver(observer)
|
||||||
|
onDispose {
|
||||||
|
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Composition lifecycle (e.g., navigating to BookInfo and back)
|
||||||
DisposableEffect(viewModel) {
|
DisposableEffect(viewModel) {
|
||||||
onDispose {
|
onDispose {
|
||||||
viewModel.onIntent(SearchIntent.StopSearch)
|
viewModel.onIntent(SearchIntent.PauseEngine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(viewModel) {
|
||||||
|
viewModel.onIntent(SearchIntent.ResumeEngine)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save scroll position before composable leaves composition (e.g., navigating to BookInfo)
|
||||||
|
DisposableEffect(viewModel) {
|
||||||
|
onDispose {
|
||||||
|
val first = listState.firstVisibleItemIndex
|
||||||
|
val offset = listState.firstVisibleItemScrollOffset
|
||||||
|
if (first > 0 || offset > 0) {
|
||||||
|
viewModel.onIntent(SearchIntent.SaveScrollState(first, offset))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore scroll position when composable re-enters with saved state
|
||||||
|
LaunchedEffect(state.savedScrollIndex, state.savedScrollOffset) {
|
||||||
|
val idx = state.savedScrollIndex
|
||||||
|
val off = state.savedScrollOffset
|
||||||
|
if (idx > 0 || off > 0) {
|
||||||
|
listState.scrollToItem(idx, off)
|
||||||
|
viewModel.onIntent(SearchIntent.SaveScrollState(0, 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +236,11 @@ fun SearchScreen(
|
|||||||
viewModel.onIntent(SearchIntent.SubmitSearch)
|
viewModel.onIntent(SearchIntent.SubmitSearch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BackHandler {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
|
||||||
val searchLabel = stringResource(R.string.search_book_key)
|
val searchLabel = stringResource(R.string.search_book_key)
|
||||||
val showResultCountCard = state.committedQuery.isNotBlank() || state.isSearching
|
val showResultCountCard = state.committedQuery.isNotBlank() || state.isSearching
|
||||||
val showSourceProgressCard = state.totalSources > 0
|
val showSourceProgressCard = state.totalSources > 0
|
||||||
@@ -249,7 +283,7 @@ fun SearchScreen(
|
|||||||
viewModel.onIntent(SearchIntent.OpenSourceManage)
|
viewModel.onIntent(SearchIntent.OpenSourceManage)
|
||||||
},
|
},
|
||||||
imageVector = AppIcons.Settings,
|
imageVector = AppIcons.Settings,
|
||||||
contentDescription = "书源管理"
|
contentDescription = stringResource(R.string.book_source_manage)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
@@ -390,8 +424,8 @@ fun SearchScreen(
|
|||||||
TopFloatingStickyItem(
|
TopFloatingStickyItem(
|
||||||
item = if (showResultCountCard || showSourceProgressCard) {
|
item = if (showResultCountCard || showSourceProgressCard) {
|
||||||
SearchFloatingSummary(
|
SearchFloatingSummary(
|
||||||
resultText = if (showResultCountCard) "结果 ${state.results.size}" else null,
|
resultText = if (showResultCountCard) stringResource(R.string.search_result_count, state.results.size) else null,
|
||||||
sourceText = if (showSourceProgressCard) " · 进度 ${state.processedSources}/${state.totalSources}" else null,
|
sourceText = if (showSourceProgressCard) stringResource(R.string.search_source_progress, state.processedSources, state.totalSources) else null,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -451,9 +485,9 @@ fun SearchScreen(
|
|||||||
content = {
|
content = {
|
||||||
Text(
|
Text(
|
||||||
text = if (it.wasPrecisionSearch) {
|
text = if (it.wasPrecisionSearch) {
|
||||||
"${it.scopeDisplay}分组搜索结果为空,是否关闭精准搜索?"
|
stringResource(R.string.search_empty_scope_disable_precision, it.scopeDisplay)
|
||||||
} else {
|
} else {
|
||||||
"${it.scopeDisplay}分组搜索结果为空,是否切换到全部分组?"
|
stringResource(R.string.search_empty_scope_switch_all, it.scopeDisplay)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class SearchViewModel(
|
|||||||
|
|
||||||
private var searchJob: Job? = null
|
private var searchJob: Job? = null
|
||||||
private var currentSearchPage = 1
|
private var currentSearchPage = 1
|
||||||
|
private var wasSearching = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
syncScopeState()
|
syncScopeState()
|
||||||
@@ -79,8 +80,22 @@ class SearchViewModel(
|
|||||||
SearchIntent.SubmitSearch -> submitSearch()
|
SearchIntent.SubmitSearch -> submitSearch()
|
||||||
SearchIntent.LoadMore -> loadMore()
|
SearchIntent.LoadMore -> loadMore()
|
||||||
SearchIntent.StopSearch -> stopSearch()
|
SearchIntent.StopSearch -> stopSearch()
|
||||||
SearchIntent.PauseEngine -> searchControl.pause()
|
SearchIntent.ClearSearchResults -> clearSearchResults()
|
||||||
SearchIntent.ResumeEngine -> searchControl.resume()
|
SearchIntent.PauseEngine -> {
|
||||||
|
wasSearching = wasSearching || (searchJob?.isActive == true)
|
||||||
|
searchControl.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchIntent.ResumeEngine -> {
|
||||||
|
searchControl.resume()
|
||||||
|
if (wasSearching) {
|
||||||
|
val state = _uiState.value
|
||||||
|
if (state.committedQuery.isNotBlank() && searchJob?.isActive != true) {
|
||||||
|
startSearch(state.committedQuery, currentSearchPage)
|
||||||
|
}
|
||||||
|
wasSearching = false
|
||||||
|
}
|
||||||
|
}
|
||||||
is SearchIntent.UseHistoryKeyword -> {
|
is SearchIntent.UseHistoryKeyword -> {
|
||||||
updateQuery(intent.keyword, showSuggestions = false)
|
updateQuery(intent.keyword, showSuggestions = false)
|
||||||
submitSearch(intent.keyword)
|
submitSearch(intent.keyword)
|
||||||
@@ -151,6 +166,15 @@ class SearchViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
SearchIntent.OpenSourceManage -> emitEffect(SearchEffect.OpenSourceManage)
|
SearchIntent.OpenSourceManage -> emitEffect(SearchEffect.OpenSourceManage)
|
||||||
|
|
||||||
|
is SearchIntent.SaveScrollState -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
savedScrollIndex = intent.index,
|
||||||
|
savedScrollOffset = intent.offset,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,6 +323,7 @@ class SearchViewModel(
|
|||||||
private fun startSearch(keyword: String, page: Int) {
|
private fun startSearch(keyword: String, page: Int) {
|
||||||
searchJob?.cancel()
|
searchJob?.cancel()
|
||||||
searchControl.resume()
|
searchControl.resume()
|
||||||
|
wasSearching = true
|
||||||
searchJob = viewModelScope.launch {
|
searchJob = viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
searchBooksUseCase
|
searchBooksUseCase
|
||||||
@@ -369,6 +394,7 @@ class SearchViewModel(
|
|||||||
private fun stopSearch(manualStop: Boolean = true) {
|
private fun stopSearch(manualStop: Boolean = true) {
|
||||||
searchJob?.cancel()
|
searchJob?.cancel()
|
||||||
searchJob = null
|
searchJob = null
|
||||||
|
wasSearching = false
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isSearching = false,
|
isSearching = false,
|
||||||
@@ -377,6 +403,25 @@ class SearchViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun clearSearchResults() {
|
||||||
|
stopSearch(manualStop = true)
|
||||||
|
searchResultBooks.clear()
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
query = "",
|
||||||
|
committedQuery = "",
|
||||||
|
results = emptyList(),
|
||||||
|
processedSources = 0,
|
||||||
|
totalSources = 0,
|
||||||
|
isSearching = false,
|
||||||
|
isManualStop = false,
|
||||||
|
hasMore = true,
|
||||||
|
showSuggestions = true,
|
||||||
|
emptyScopeAction = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun toggleScopeGroup(groupName: String) {
|
private fun toggleScopeGroup(groupName: String) {
|
||||||
val oldScope = searchScope.toString()
|
val oldScope = searchScope.toString()
|
||||||
if (searchScope.isSource()) {
|
if (searchScope.isSource()) {
|
||||||
|
|||||||
@@ -622,7 +622,6 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
}
|
}
|
||||||
) { route ->
|
) { route ->
|
||||||
val searchViewModel = koinViewModel<SearchViewModel>()
|
val searchViewModel = koinViewModel<SearchViewModel>()
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
|
||||||
|
|
||||||
LaunchedEffect(route.key, route.scopeRaw, searchViewModel) {
|
LaunchedEffect(route.key, route.scopeRaw, searchViewModel) {
|
||||||
searchViewModel.onIntent(
|
searchViewModel.onIntent(
|
||||||
@@ -633,29 +632,12 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(lifecycleOwner, searchViewModel) {
|
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
|
||||||
when (event) {
|
|
||||||
Lifecycle.Event.ON_RESUME -> {
|
|
||||||
searchViewModel.onIntent(SearchIntent.ResumeEngine)
|
|
||||||
}
|
|
||||||
|
|
||||||
Lifecycle.Event.ON_PAUSE -> {
|
|
||||||
searchViewModel.onIntent(SearchIntent.PauseEngine)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lifecycleOwner.lifecycle.addObserver(observer)
|
|
||||||
onDispose {
|
|
||||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchScreen(
|
SearchScreen(
|
||||||
viewModel = searchViewModel,
|
viewModel = searchViewModel,
|
||||||
onBack = { navigateBack(backStack) },
|
onBack = {
|
||||||
|
searchViewModel.onIntent(SearchIntent.ClearSearchResults)
|
||||||
|
navigateBack(backStack)
|
||||||
|
},
|
||||||
onOpenBookInfo = { name, author, bookUrl ->
|
onOpenBookInfo = { name, author, bookUrl ->
|
||||||
navigateToRoute(
|
navigateToRoute(
|
||||||
backStack,
|
backStack,
|
||||||
@@ -715,14 +697,20 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
|
|
||||||
entry<MainRouteBookInfo>(
|
entry<MainRouteBookInfo>(
|
||||||
metadata = NavDisplay.transitionSpec {
|
metadata = NavDisplay.transitionSpec {
|
||||||
|
if (initialState.key is MainRouteExploreShow) {
|
||||||
fadeIn(animationSpec = tween(300)) togetherWith
|
fadeIn(animationSpec = tween(300)) togetherWith
|
||||||
fadeOut(animationSpec = tween(300))
|
fadeOut(animationSpec = tween(300))
|
||||||
|
} else null
|
||||||
} + NavDisplay.popTransitionSpec {
|
} + NavDisplay.popTransitionSpec {
|
||||||
|
if (targetState.key is MainRouteExploreShow) {
|
||||||
fadeIn(animationSpec = tween(300)) togetherWith
|
fadeIn(animationSpec = tween(300)) togetherWith
|
||||||
fadeOut(animationSpec = tween(300))
|
fadeOut(animationSpec = tween(300))
|
||||||
|
} else null
|
||||||
} + NavDisplay.predictivePopTransitionSpec { _ ->
|
} + NavDisplay.predictivePopTransitionSpec { _ ->
|
||||||
|
if (targetState.key is MainRouteExploreShow) {
|
||||||
fadeIn(animationSpec = tween(300)) togetherWith
|
fadeIn(animationSpec = tween(300)) togetherWith
|
||||||
fadeOut(animationSpec = tween(300))
|
fadeOut(animationSpec = tween(300))
|
||||||
|
} else null
|
||||||
}
|
}
|
||||||
) { route ->
|
) { route ->
|
||||||
val bookInfoViewModel = koinViewModel<BookInfoViewModel>()
|
val bookInfoViewModel = koinViewModel<BookInfoViewModel>()
|
||||||
|
|||||||
@@ -1474,4 +1474,8 @@
|
|||||||
<string name="bookshelf_empty_global_search">没有书籍,尝试全局搜索</string>
|
<string name="bookshelf_empty_global_search">没有书籍,尝试全局搜索</string>
|
||||||
<string name="global_search">全局搜索</string>
|
<string name="global_search">全局搜索</string>
|
||||||
<string name="cache_management">缓存管理</string>
|
<string name="cache_management">缓存管理</string>
|
||||||
|
<string name="search_result_count">结果 %1$d</string>
|
||||||
|
<string name="search_source_progress"> · 进度 %1$d/%2$d</string>
|
||||||
|
<string name="search_empty_scope_disable_precision">%1$s分组搜索结果为空,是否关闭精准搜索?</string>
|
||||||
|
<string name="search_empty_scope_switch_all">%1$s分组搜索结果为空,是否切换到全部分组?</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1518,4 +1518,8 @@
|
|||||||
<string name="bookshelf_empty_global_search">No books here. Try global search.</string>
|
<string name="bookshelf_empty_global_search">No books here. Try global search.</string>
|
||||||
<string name="global_search">Global search</string>
|
<string name="global_search">Global search</string>
|
||||||
<string name="cache_management">Cache management</string>
|
<string name="cache_management">Cache management</string>
|
||||||
|
<string name="search_result_count">Results %1$d</string>
|
||||||
|
<string name="search_source_progress"> · Progress %1$d/%2$d</string>
|
||||||
|
<string name="search_empty_scope_disable_precision">%1$s group search returned no results. Disable precise search?</string>
|
||||||
|
<string name="search_empty_scope_switch_all">%1$s group search returned no results. Switch to all groups?</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in new issue
Block a user