优化搜素逻辑

This commit is contained in:
HapeLee committed 2026-05-10 23:34:20 +08:00
1 parent 6a0954377a
commit 6a39b42fe5
8 files changed
+144 -64

No files matched your search

+6 -1
View File
@@ -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 {
fadeIn(animationSpec = tween(300)) togetherWith if (initialState.key is MainRouteExploreShow) {
fadeOut(animationSpec = tween(300)) fadeIn(animationSpec = tween(300)) togetherWith
fadeOut(animationSpec = tween(300))
} else null
} + NavDisplay.popTransitionSpec { } + NavDisplay.popTransitionSpec {
fadeIn(animationSpec = tween(300)) togetherWith if (targetState.key is MainRouteExploreShow) {
fadeOut(animationSpec = tween(300)) fadeIn(animationSpec = tween(300)) togetherWith
fadeOut(animationSpec = tween(300))
} else null
} + NavDisplay.predictivePopTransitionSpec { _ -> } + NavDisplay.predictivePopTransitionSpec { _ ->
fadeIn(animationSpec = tween(300)) togetherWith if (targetState.key is MainRouteExploreShow) {
fadeOut(animationSpec = tween(300)) fadeIn(animationSpec = tween(300)) togetherWith
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>
+4
View File
@@ -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>