|
@@ -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,
|
|
|
|
|
- sourceText = if (showSourceProgressCard) " · 进度 ${state.processedSources}/${state.totalSources}" else null,
|
|
|
|
|
|
|
+ resultText = if (showResultCountCard) stringResource(R.string.search_result_count, state.results.size) 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)
|
|
|
}
|
|
}
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|