Show all categories in empty state of search screen

This commit is contained in:
Torsten Grote committed 2026-04-28 09:11:14 -03:00
1 parent 85b27fe1ed
commit 37dc5bec0f
8 files changed
+167 -166

No files matched your search

@@ -52,7 +52,9 @@ constructor(
private val collator = Collator.getInstance(Locale.getDefault())
private val _searchResults = MutableStateFlow<SearchResults?>(null)
private val _savedSearches = MutableStateFlow<List<SavedSearch>?>(null)
private val categories =
private var searchJob: SearchJob? = null
val categories =
db.getRepositoryDao().getLiveCategories().asFlow().map { categories ->
categories
.map { category ->
@@ -60,8 +62,6 @@ constructor(
}
.sortedWith { c1, c2 -> collator.compare(c1.name, c2.name) }
}
private var searchJob: SearchJob? = null
val searchResults = _searchResults.asStateFlow()
val savedSearches = _savedSearches.asStateFlow()
@@ -73,6 +73,7 @@ fun Main(onListeningForIntent: () -> Unit = {}) {
GlobalSearch(
searchResults = viewModel.searchResults.collectAsStateWithLifecycle().value,
savedSearches = viewModel.savedSearchesFlow.collectAsStateWithLifecycle().value,
categories = viewModel.categories.collectAsStateWithLifecycle(null).value,
onSearch = viewModel::search,
onClearSavedSearches = viewModel::onClearSearchHistory,
onNav = { navKey -> navigator.navigate(navKey) },
@@ -17,6 +17,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.tooling.preview.Preview
import org.fdroid.search.SavedSearch
import org.fdroid.ui.FDroidContent
import org.fdroid.ui.categories.CategoryItem
import org.fdroid.ui.navigation.NavigationKey
import org.fdroid.ui.utils.appListItems
import org.fdroid.ui.utils.categoryItems
@@ -27,6 +28,7 @@ fun GlobalSearch(
textFieldState: TextFieldState = rememberTextFieldState(),
searchResults: SearchResults?,
savedSearches: List<SavedSearch>? = null,
categories: List<CategoryItem>? = null,
onSearch: suspend (String) -> Unit,
onClearSavedSearches: () -> Unit,
onNav: (NavigationKey) -> Unit,
@@ -60,6 +62,7 @@ fun GlobalSearch(
searchResults = searchResults,
textFieldState = textFieldState,
savedSearches = savedSearches,
categories = categories,
onClearSavedSearches = onClearSavedSearches,
onNav = onNav,
)
@@ -72,7 +75,15 @@ fun GlobalSearch(
private fun AppsSearchLoadingPreview() {
FDroidContent {
val textFieldState = rememberTextFieldState("foo bar")
Box(Modifier.fillMaxSize()) { GlobalSearch(textFieldState, null, null, {}, {}, {}, {}, {}) }
Box(Modifier.fillMaxSize()) { GlobalSearch(
textFieldState,
null,
onSearch = {},
onClearSavedSearches = {},
onNav = {},
onBack = {},
onSearchCleared = {},
) }
}
}
@@ -89,6 +100,7 @@ private fun AppsSearchEmptyStatePreview() {
textFieldState = textFieldState,
searchResults = null,
savedSearches = savedSearches,
categories = categoryItems,
onSearch = {},
onClearSavedSearches = {},
onNav = {},
@@ -1,17 +1,11 @@
package org.fdroid.ui.search
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
@@ -23,6 +17,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
@@ -32,51 +27,41 @@ import org.fdroid.R
import org.fdroid.search.SavedSearch
import org.fdroid.ui.FDroidContent
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
fun PastSearches(
fun LazyListScope.pastSearches(
savedSearches: List<SavedSearch>,
onSearch: (String) -> Unit,
onClearSavedSearches: () -> Unit,
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(),
) {
LazyColumn(modifier = modifier.fillMaxSize(), contentPadding = paddingValues) {
item {
Row {
Text(
text = stringResource(R.string.search_history),
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp).weight(1f),
item {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResource(R.string.search_history),
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(horizontal = 16.dp).weight(1f),
)
TextButton(onClick = onClearSavedSearches, modifier = Modifier.padding(horizontal = 8.dp)) {
Text(stringResource(R.string.clear))
}
}
}
items(savedSearches) { item ->
ListItem(
leadingContent = {
Icon(
Icons.Default.History,
contentDescription = null,
modifier =
Modifier.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(8.dp),
)
TextButton(
onClick = onClearSavedSearches,
modifier = Modifier.padding(horizontal = 8.dp),
) {
Text(stringResource(R.string.clear))
}
}
},
onClick = { onSearch(item.query) },
modifier = Modifier.fillMaxWidth().animateItem(),
) {
Text(item.query)
}
items(savedSearches) { item ->
ListItem(
leadingContent = {
Icon(
Icons.Default.History,
contentDescription = null,
modifier =
Modifier.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(8.dp),
)
},
onClick = { onSearch(item.query) },
modifier = Modifier.fillMaxWidth().animateItem(),
) {
Text(item.query)
}
}
item { Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) }
item { Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) }
}
}
@@ -86,10 +71,8 @@ private fun Preview() {
FDroidContent {
val savedSearches =
listOf(SavedSearch(1, "foo"), SavedSearch(2, "foo bar"), SavedSearch(3, "foobar"))
PastSearches(
savedSearches = savedSearches,
onClearSavedSearches = {},
onSearch = {},
)
LazyColumn {
pastSearches(savedSearches = savedSearches, onClearSavedSearches = {}, onSearch = {})
}
}
}
@@ -47,6 +47,7 @@ fun SearchResults(
searchResults: SearchResults?,
textFieldState: TextFieldState,
savedSearches: List<SavedSearch>?,
categories: List<CategoryItem>?,
onClearSavedSearches: () -> Unit,
onNav: (NavigationKey) -> Unit,
modifier: Modifier = Modifier,
@@ -58,14 +59,19 @@ fun SearchResults(
if (textFieldState.text.isSearchable()) {
BigLoadingIndicator(modifier.padding(paddingValues).imePadding())
} else {
if (!savedSearches.isNullOrEmpty()) {
PastSearches(
savedSearches = savedSearches,
onSearch = { textFieldState.edit { replace(0, length, it) } },
onClearSavedSearches = onClearSavedSearches,
modifier = modifier,
paddingValues = paddingValues,
)
LazyColumn(modifier = modifier.fillMaxSize(), contentPadding = paddingValues) {
if (!savedSearches.isNullOrEmpty()) {
pastSearches(
savedSearches = savedSearches,
onSearch = { textFieldState.edit { replace(0, length, it) } },
onClearSavedSearches = onClearSavedSearches,
)
}
if (!categories.isNullOrEmpty()) {
item { CategoriesFlowRow(categories, onNav, modifier = Modifier.padding(top = 16.dp)) }
}
item { Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) }
item { Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) }
}
}
} else if (searchResults.apps.isEmpty() && textFieldState.text.isSearchable()) {
@@ -121,8 +127,12 @@ fun SearchResults(
}
@Composable
private fun CategoriesFlowRow(categories: List<CategoryItem>, onNav: (NavigationKey) -> Unit) {
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
private fun CategoriesFlowRow(
categories: List<CategoryItem>,
onNav: (NavigationKey) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier.padding(horizontal = 8.dp)) {
Text(
text = stringResource(R.string.main_menu__categories),
style = MaterialTheme.typography.labelLarge,
@@ -16,6 +16,7 @@ constructor(app: Application, private val searchManager: SearchManager) : Androi
val searchResults = searchManager.searchResults
val savedSearchesFlow = searchManager.savedSearches
val categories = searchManager.categories
suspend fun search(term: String) = if (term.isSearchable()) searchManager.search(term) else Unit
@@ -13,112 +13,107 @@ import org.fdroid.ui.utils.categoryItems
@Composable
@PreviewTest
@Preview(showBackground = true, showSystemUi = true)
fun GlobalSearchEmptyTest() =
ScreenshotTest {
val textFieldState = rememberTextFieldState()
GlobalSearch(
textFieldState = textFieldState,
searchResults = null,
savedSearches = emptyList(),
onSearch = {},
onClearSavedSearches = {},
onNav = {},
onBack = {},
onSearchCleared = {},
)
}
fun GlobalSearchEmptyTest() = ScreenshotTest {
val textFieldState = rememberTextFieldState()
GlobalSearch(
textFieldState = textFieldState,
searchResults = null,
savedSearches = emptyList(),
onSearch = {},
onClearSavedSearches = {},
onNav = {},
onBack = {},
onSearchCleared = {},
)
}
@Composable
@PreviewTest
@Preview(showBackground = true, showSystemUi = true)
fun GlobalSearchEmptyStateTest() =
ScreenshotTest {
val savedSearches =
listOf(SavedSearch(1, "Browser"), SavedSearch(2, "media player"), SavedSearch(3, "email app"))
val textFieldState = rememberTextFieldState()
GlobalSearch(
textFieldState = textFieldState,
searchResults = null,
savedSearches = savedSearches,
onSearch = {},
onClearSavedSearches = {},
onNav = {},
onBack = {},
onSearchCleared = {},
)
}
fun GlobalSearchEmptyStateTest() = ScreenshotTest {
val savedSearches =
listOf(SavedSearch(1, "Browser"), SavedSearch(2, "media player"), SavedSearch(3, "email app"))
val textFieldState = rememberTextFieldState()
GlobalSearch(
textFieldState = textFieldState,
searchResults = null,
savedSearches = savedSearches,
categories = categoryItems,
onSearch = {},
onClearSavedSearches = {},
onNav = {},
onBack = {},
onSearchCleared = {},
)
}
@Composable
@PreviewTest
@Preview(showBackground = true, showSystemUi = true)
fun GlobalSearchLoadingTest() =
ScreenshotTest {
val textFieldState = rememberTextFieldState("foo bar")
GlobalSearch(
textFieldState = textFieldState,
searchResults = null,
savedSearches = emptyList(),
onSearch = {},
onClearSavedSearches = {},
onNav = {},
onBack = {},
onSearchCleared = {},
)
}
fun GlobalSearchLoadingTest() = ScreenshotTest {
val textFieldState = rememberTextFieldState("foo bar")
GlobalSearch(
textFieldState = textFieldState,
searchResults = null,
savedSearches = emptyList(),
onSearch = {},
onClearSavedSearches = {},
onNav = {},
onBack = {},
onSearchCleared = {},
)
}
@Composable
@PreviewTest
@Preview(showBackground = true, showSystemUi = true)
fun GlobalSearchNoResultsTest() =
ScreenshotTest {
val textFieldState = rememberTextFieldState("foo")
GlobalSearch(
textFieldState = textFieldState,
searchResults = SearchResults(emptyList(), emptyList()),
savedSearches = emptyList(),
onSearch = {},
onClearSavedSearches = {},
onNav = {},
onBack = {},
onSearchCleared = {},
)
}
fun GlobalSearchNoResultsTest() = ScreenshotTest {
val textFieldState = rememberTextFieldState("foo")
GlobalSearch(
textFieldState = textFieldState,
searchResults = SearchResults(emptyList(), emptyList()),
savedSearches = emptyList(),
onSearch = {},
onClearSavedSearches = {},
onNav = {},
onBack = {},
onSearchCleared = {},
)
}
@Composable
@PreviewTest
@Preview(showBackground = true, showSystemUi = true)
fun GlobalSearchResultsTest() =
ScreenshotTest {
val textFieldState = rememberTextFieldState("foo bar")
GlobalSearch(
textFieldState = textFieldState,
searchResults = SearchResults(appListItems, categoryItems.subList(0, 4)),
savedSearches = emptyList(),
onSearch = {},
onClearSavedSearches = {},
onNav = {},
onBack = {},
onSearchCleared = {},
)
}
fun GlobalSearchResultsTest() = ScreenshotTest {
val textFieldState = rememberTextFieldState("foo bar")
GlobalSearch(
textFieldState = textFieldState,
searchResults = SearchResults(appListItems, categoryItems.subList(0, 4)),
savedSearches = emptyList(),
onSearch = {},
onClearSavedSearches = {},
onNav = {},
onBack = {},
onSearchCleared = {},
)
}
@Composable
@PreviewTest
@Preview(showBackground = true, showSystemUi = true)
fun GlobalSearchOnlyCategoriesTest() =
ScreenshotTest {
val textFieldState = rememberTextFieldState("foo bar")
GlobalSearch(
textFieldState = textFieldState,
searchResults = SearchResults(emptyList(), categoryItems.subList(5, 9)),
savedSearches = emptyList(),
onSearch = {},
onClearSavedSearches = {},
onNav = {},
onBack = {},
onSearchCleared = {},
)
}
fun GlobalSearchOnlyCategoriesTest() = ScreenshotTest {
val textFieldState = rememberTextFieldState("foo bar")
GlobalSearch(
textFieldState = textFieldState,
searchResults = SearchResults(emptyList(), categoryItems.subList(5, 9)),
savedSearches = emptyList(),
onSearch = {},
onClearSavedSearches = {},
onNav = {},
onBack = {},
onSearchCleared = {},
)
}
@Composable
@PreviewTest
@@ -127,17 +122,16 @@ fun GlobalSearchOnlyCategoriesTest() =
showSystemUi = true,
uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL,
)
fun GlobalSearchResultsNightTest() =
ScreenshotTest {
val textFieldState = rememberTextFieldState("foo bar")
GlobalSearch(
textFieldState = textFieldState,
searchResults = SearchResults(appListItems, categoryItems.subList(0, 4)),
savedSearches = emptyList(),
onSearch = {},
onClearSavedSearches = {},
onNav = {},
onBack = {},
onSearchCleared = {},
)
}
fun GlobalSearchResultsNightTest() = ScreenshotTest {
val textFieldState = rememberTextFieldState("foo bar")
GlobalSearch(
textFieldState = textFieldState,
searchResults = SearchResults(appListItems, categoryItems.subList(0, 4)),
savedSearches = emptyList(),
onSearch = {},
onClearSavedSearches = {},
onNav = {},
onBack = {},
onSearchCleared = {},
)
}
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6eb1c82199e7d0ace698bdc95b523bef51a761eaa22698dfefae3ff5f2419ca3
size 56180
oid sha256:e926d11d01b2d8786e9af5077f60d47b51d23605e016559c94b998c85edd44f0
size 119038