improve search and filtering code

This commit is contained in:
Torsten Grote
2025-04-22 16:08:33 -03:00
parent 15ad121fc8
commit 632243def1
13 changed files with 374 additions and 162 deletions

View File

@@ -49,11 +49,13 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.androidx.compose.material3.adaptive.layout)
implementation(libs.androidx.compose.material3.adaptive.navigation)
implementation(libs.androidx.compose.material3.adaptive.navigation.suite.android)
implementation(libs.molecule.runtime)
testImplementation(libs.junit)

View File

@@ -4,14 +4,27 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.collectAsState
import androidx.lifecycle.viewmodel.compose.viewModel
import org.fdroid.basic.ui.main.Main
import org.fdroid.basic.ui.main.Sort
import org.fdroid.basic.ui.main.apps.FilterInfo
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Main()
val viewModel: MainViewModel = viewModel()
val filterInfo = object : FilterInfo {
override val model = viewModel.filterModel.collectAsState().value
override fun sortBy(sort: Sort) = viewModel.sortBy(sort)
override fun addCategory(category: String) = viewModel.addCategory(category)
override fun removeCategory(category: String) = viewModel.removeCategory(category)
override fun showOnlyInstalledApps(onlyInstalled: Boolean) =
viewModel.showOnlyInstalledApps(onlyInstalled)
}
Main(filterInfo)
}
}
}

View File

@@ -0,0 +1,96 @@
package org.fdroid.basic
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import app.cash.molecule.AndroidUiDispatcher
import app.cash.molecule.RecompositionMode.ContextClock
import app.cash.molecule.launchMolecule
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.update
import org.fdroid.basic.ui.main.NUM_ITEMS
import org.fdroid.basic.ui.main.Sort
import org.fdroid.basic.ui.main.apps.AppNavigationItem
import org.fdroid.basic.ui.main.apps.FilterModel
import org.fdroid.basic.ui.main.apps.FilterPresenter
class MainViewModel(app: Application) : AndroidViewModel(app) {
private val scope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main)
val categories = listOf(
app.getString(R.string.category_Time),
app.getString(R.string.category_Games),
app.getString(R.string.category_Money),
app.getString(R.string.category_Reading),
app.getString(R.string.category_Theming),
app.getString(R.string.category_Connectivity),
app.getString(R.string.category_Internet),
app.getString(R.string.category_Navigation),
app.getString(R.string.category_Multimedia),
app.getString(R.string.category_Phone_SMS),
app.getString(R.string.category_Science_Education),
app.getString(R.string.category_Security),
app.getString(R.string.category_Sports_Health),
app.getString(R.string.category_System),
app.getString(R.string.category_Writing),
)
val initialApps = buildList {
repeat(NUM_ITEMS) { i ->
val category = categories.getOrElse(i) { categories.random() }
val navItem = AppNavigationItem(
packageName = "$i",
name = "App $i",
summary = "Summary of the app • $category",
isNew = i > NUM_ITEMS - 4,
)
add(navItem)
}
}
private val _onlyInstalledApps = MutableStateFlow(false)
val onlyInstalledApps = _onlyInstalledApps.asStateFlow<Boolean>()
private val _sortBy = MutableStateFlow<Sort>(Sort.NAME)
val sortBy = _sortBy.asStateFlow<Sort>()
private val _addedCategories = MutableStateFlow<List<String>>(emptyList())
val addedCategories = _addedCategories.asStateFlow<List<String>>()
val filterModel: StateFlow<FilterModel> = scope.launchMolecule(mode = ContextClock) {
FilterPresenter(
appsFlow = flow { emit(initialApps) },
onlyInstalledAppsFlow = onlyInstalledApps,
sortByFlow = sortBy,
allCategories = categories,
addedCategoriesFlow = addedCategories,
)
}
fun sortBy(sort: Sort) {
_sortBy.update { sort }
}
fun addCategory(category: String) {
_addedCategories.update {
addedCategories.value.toMutableList().apply {
add(category)
}
}
}
fun removeCategory(category: String) {
_addedCategories.update {
addedCategories.value.toMutableList().apply {
remove(category)
}
}
}
fun showOnlyInstalledApps(onlyInstalled: Boolean) {
_onlyInstalledApps.update { onlyInstalled }
}
}

View File

@@ -3,6 +3,8 @@ package org.fdroid.basic.ui.main
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
@@ -18,16 +20,17 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.fdroid.basic.R
import org.fdroid.basic.ui.main.apps.AppDetails
import org.fdroid.basic.ui.main.apps.AppList
import org.fdroid.basic.ui.main.apps.AppNavigationItem
import org.fdroid.basic.ui.main.apps.AppsFilter
import org.fdroid.basic.ui.main.apps.AppsSearch
import org.fdroid.basic.ui.main.apps.FilterInfo
import org.fdroid.basic.ui.main.apps.FilterModel
import org.fdroid.fdroid.ui.theme.FDroidContent
enum class Sort {
@@ -39,7 +42,11 @@ const val NUM_ITEMS = 42
@Composable
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
fun Apps(modifier: Modifier) {
fun Apps(
apps: List<AppNavigationItem>,
filterInfo: FilterInfo,
modifier: Modifier,
) {
val navigator = rememberListDetailPaneScaffoldNavigator<AppNavigationItem>()
val scope = rememberCoroutineScope()
BackHandler(enabled = navigator.canNavigateBack()) {
@@ -56,51 +63,24 @@ fun Apps(modifier: Modifier) {
Column(
modifier.fillMaxSize()
) {
var filterExpanded by rememberSaveable { mutableStateOf(true) }
var sortBy by rememberSaveable { mutableStateOf(Sort.NAME) }
var onlyInstalledApps by rememberSaveable { mutableStateOf(false) }
val addedCategories = remember { mutableStateListOf<String>() }
var filterExpanded by rememberSaveable { mutableStateOf(false) }
val addedRepos = remember { mutableStateListOf<String>() }
val categories = listOf(
stringResource(R.string.category_Time),
stringResource(R.string.category_Games),
stringResource(R.string.category_Money),
stringResource(R.string.category_Reading),
stringResource(R.string.category_Theming),
stringResource(R.string.category_Connectivity),
stringResource(R.string.category_Internet),
stringResource(R.string.category_Navigation),
stringResource(R.string.category_Multimedia),
stringResource(R.string.category_Phone_SMS),
stringResource(R.string.category_Science_Education),
stringResource(R.string.category_Security),
stringResource(R.string.category_Sports_Health),
stringResource(R.string.category_System),
stringResource(R.string.category_Writing),
)
val showFilterBadge = addedRepos.isNotEmpty() ||
filterInfo.model.addedCategories.isNotEmpty() ||
filterInfo.model.onlyInstalledApps
AppsSearch(
onlyInstalledApps = onlyInstalledApps,
addedCategories = addedCategories,
addedRepos = addedRepos,
showFilterBadge = showFilterBadge,
toggleFilter = { filterExpanded = !filterExpanded },
)
) {
scope.launch { navigator.navigateTo(Detail, it) }
}
AppsFilter(
filterExpanded = filterExpanded,
sortBy = sortBy,
onlyInstalledApps = onlyInstalledApps,
addedCategories = addedCategories,
filter = filterInfo,
addedRepos = addedRepos,
categories = categories,
onSortByChanged = { sortBy = it },
toggleOnlyInstalledApps = {
onlyInstalledApps = !onlyInstalledApps
},
)
AppList(
onlyInstalledApps = onlyInstalledApps,
sortBy = sortBy,
addedCategories = addedCategories,
categories = categories,
apps = apps,
currentItem = if (isDetailVisible) {
navigator.currentDestination?.contentKey
} else {
@@ -118,7 +98,7 @@ fun Apps(modifier: Modifier) {
AppDetails(
appItem = it,
)
}
} ?: Text("No app selected", modifier = Modifier.padding(16.dp))
}
},
)
@@ -129,6 +109,26 @@ fun Apps(modifier: Modifier) {
@Composable
fun AppsPreview() {
FDroidContent {
Apps(Modifier)
val apps = listOf(
AppNavigationItem("", "foo", "bar", false),
AppNavigationItem("", "foo", "bar", false),
AppNavigationItem("", "foo", "bar", false),
)
val filterInfo = object : FilterInfo {
override val model = FilterModel(
isLoading = false,
apps = apps,
onlyInstalledApps = false,
sortBy = Sort.NAME,
allCategories = listOf("foo", "bar"),
addedCategories = emptyList(),
)
override fun sortBy(sort: Sort) {}
override fun addCategory(category: String) {}
override fun removeCategory(category: String) {}
override fun showOnlyInstalledApps(onlyInstalled: Boolean) {}
}
Apps(apps, filterInfo, Modifier)
}
}

View File

@@ -27,6 +27,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.window.core.layout.WindowWidthSizeClass
import org.fdroid.basic.R
import org.fdroid.basic.ui.main.apps.AppNavigationItem
import org.fdroid.basic.ui.main.apps.FilterInfo
import org.fdroid.basic.ui.main.apps.FilterModel
import org.fdroid.fdroid.ui.theme.FDroidContent
enum class AppDestinations(
@@ -38,7 +41,7 @@ enum class AppDestinations(
}
@Composable
fun Main() {
fun Main(filterInfo: FilterInfo) {
var currentDestination by rememberSaveable { mutableStateOf(AppDestinations.APPS) }
FDroidContent {
val adaptiveInfo = currentWindowAdaptiveInfo()
@@ -80,7 +83,11 @@ fun Main() {
}
}
) {
if (currentDestination == AppDestinations.APPS) Apps(Modifier)
if (currentDestination == AppDestinations.APPS) Apps(
apps = filterInfo.model.apps,
filterInfo = filterInfo,
modifier = Modifier,
)
else Text(
text = "TODO",
modifier = Modifier.safeDrawingPadding(),
@@ -93,5 +100,25 @@ fun Main() {
@PreviewScreenSizes
@Composable
fun MainPreview() {
Main()
val apps = listOf(
AppNavigationItem("", "foo", "bar", false),
AppNavigationItem("", "foo", "bar", false),
AppNavigationItem("", "foo", "bar", false),
)
val filterInfo = object : FilterInfo {
override val model = FilterModel(
isLoading = false,
apps = apps,
onlyInstalledApps = false,
sortBy = Sort.NAME,
allCategories = listOf("foo", "bar"),
addedCategories = emptyList(),
)
override fun sortBy(sort: Sort) {}
override fun addCategory(category: String) {}
override fun removeCategory(category: String) {}
override fun showOnlyInstalledApps(onlyInstalled: Boolean) {}
}
Main(filterInfo)
}

View File

@@ -49,6 +49,7 @@ fun AppDetailsPreview() {
packageName = "foo",
name = "bar",
summary = "This is a nice app!",
isNew = false,
)
AppDetails(item)
}

View File

@@ -0,0 +1,79 @@
package org.fdroid.basic.ui.main.apps
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Android
import androidx.compose.material.icons.filled.NewReleases
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.fdroid.fdroid.ui.theme.FDroidContent
@Composable
fun AppItem(
name: String,
summary: String,
isNew: Boolean,
isSelected: Boolean,
modifier: Modifier = Modifier,
) {
ListItem(
headlineContent = { Text(name) },
supportingContent = { Text(summary) },
leadingContent = {
BadgedBox(badge = {
if (isNew) Icon(
imageVector = Icons.Filled.NewReleases,
tint = MaterialTheme.colorScheme.primary,
contentDescription = null, modifier = Modifier.size(24.dp),
)
}) {
Icon(
Icons.Filled.Android,
tint = MaterialTheme.colorScheme.secondary,
contentDescription = null,
)
}
},
colors = ListItemDefaults.colors(
containerColor = if (isSelected) {
MaterialTheme.colorScheme.surfaceVariant
} else {
Color.Transparent
}
),
modifier = modifier,
)
}
@Preview
@Composable
fun AppItemPreview() {
FDroidContent {
AppItem("This is app 1", "It has summary 2", false, false)
}
}
@Preview
@Composable
fun AppItemPreviewNew() {
FDroidContent {
AppItem("This is app 1", "It has summary 2", true, false)
}
}
@Preview
@Composable
fun AppItemPreviewSelected() {
FDroidContent {
AppItem("This is app 1", "It has summary 2", false, true)
}
}

View File

@@ -1,6 +1,5 @@
package org.fdroid.basic.ui.main.apps
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
@@ -8,34 +7,19 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Android
import androidx.compose.material.icons.filled.NewReleases
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import org.fdroid.basic.ui.main.NUM_ITEMS
import org.fdroid.basic.ui.main.Sort
@Composable
fun AppList(
onlyInstalledApps: Boolean,
sortBy: Sort,
addedCategories: List<String>,
categories: List<String>,
apps: List<AppNavigationItem>,
currentItem: AppNavigationItem?,
onItemClick: (AppNavigationItem) -> Unit,
) {
@@ -47,62 +31,23 @@ fun AppList(
else Modifier.selectableGroup()
),
) {
repeat(NUM_ITEMS) { idx ->
if (onlyInstalledApps && idx % 2 > 0) return@repeat
val i = if (sortBy == Sort.NAME) idx else NUM_ITEMS - idx
val category = categories.getOrElse(i) { categories.random() }
if (addedCategories.isNotEmpty() && category !in addedCategories) return@repeat
item {
val navItem = AppNavigationItem(
packageName = "$i",
name = "App $i",
summary = "Summary of the app • $category",
items(apps) { navItem ->
val isSelected = currentItem?.packageName == navItem.packageName
val interactionModifier = if (currentItem == null) {
Modifier.clickable(
onClick = { onItemClick(navItem) }
)
val isSelected = currentItem?.packageName == navItem.packageName
val interactionModifier = if (currentItem == null) {
Modifier.clickable(
onClick = { onItemClick(navItem) }
)
} else {
Modifier.selectable(
selected = isSelected,
onClick = { onItemClick(navItem) }
)
}
ListItem(
headlineContent = { Text(navItem.name) },
supportingContent = { Text(navItem.summary) },
leadingContent = {
BadgedBox(badge = {
if (i <= 3) Icon(
imageVector = Icons.Filled.NewReleases,
tint = MaterialTheme.colorScheme.primary,
contentDescription = null, modifier = Modifier.size(24.dp),
)
}) {
Icon(
Icons.Filled.Android,
tint = MaterialTheme.colorScheme.secondary,
contentDescription = null,
)
}
},
colors = ListItemDefaults.colors(
containerColor = if (isSelected) {
MaterialTheme.colorScheme.surfaceVariant
} else {
Color.Transparent
}
),
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = 8.dp,
vertical = 4.dp
)
.then(interactionModifier)
} else {
Modifier.selectable(
selected = isSelected,
onClick = { onItemClick(navItem) }
)
}
val modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
.then(interactionModifier)
AppItem(navItem.name, navItem.summary, navItem.isNew, isSelected, modifier)
}
item {
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))

View File

@@ -8,4 +8,5 @@ class AppNavigationItem(
val packageName: String,
val name: String,
val summary: String,
val isNew: Boolean,
): Parcelable

View File

@@ -30,16 +30,20 @@ import androidx.compose.ui.unit.dp
import org.fdroid.basic.R
import org.fdroid.basic.ui.main.Sort
interface FilterInfo {
val model: FilterModel
fun sortBy(sort: Sort)
fun addCategory(category: String)
fun removeCategory(category: String)
fun showOnlyInstalledApps(onlyInstalled: Boolean)
}
@Composable
fun ColumnScope.AppsFilter(
filterExpanded: Boolean,
sortBy: Sort,
onlyInstalledApps: Boolean,
addedCategories: MutableList<String>,
filter: FilterInfo,
addedRepos: MutableList<String>,
categories: List<String>,
onSortByChanged: (Sort) -> Unit,
toggleOnlyInstalledApps: () -> Unit,
) {
AnimatedVisibility(filterExpanded) {
FlowRow(
@@ -50,7 +54,7 @@ fun ColumnScope.AppsFilter(
var sortByMenuExpanded by remember { mutableStateOf(false) }
var repoMenuExpanded by remember { mutableStateOf(false) }
var categoryMenuExpanded by remember { mutableStateOf(false) }
addedCategories.forEach { category ->
filter.model.addedCategories.forEach { category ->
FilterChip(
selected = true,
trailingIcon = {
@@ -64,7 +68,7 @@ fun ColumnScope.AppsFilter(
Text(category)
},
onClick = {
addedCategories.remove(category)
filter.removeCategory(category)
}
)
}
@@ -89,7 +93,7 @@ fun ColumnScope.AppsFilter(
FilterChip(
selected = false,
leadingIcon = {
val vector = when (sortBy) {
val vector = when (filter.model.sortBy) {
Sort.NAME -> Icons.Filled.SortByAlpha
Sort.LATEST -> Icons.Filled.AccessTime
}
@@ -99,7 +103,7 @@ fun ColumnScope.AppsFilter(
Icon(Icons.Filled.ArrowDropDown, null)
},
label = {
val s = when (sortBy) {
val s = when (filter.model.sortBy) {
Sort.NAME -> "Sort by name"
Sort.LATEST -> "Sort by latest"
}
@@ -114,7 +118,7 @@ fun ColumnScope.AppsFilter(
Icon(Icons.Filled.SortByAlpha, null)
},
onClick = {
onSortByChanged(Sort.NAME)
filter.sortBy(Sort.NAME)
sortByMenuExpanded = false
},
)
@@ -124,7 +128,7 @@ fun ColumnScope.AppsFilter(
Icon(Icons.Filled.AccessTime, null)
},
onClick = {
onSortByChanged(Sort.LATEST)
filter.sortBy(Sort.LATEST)
sortByMenuExpanded = false
},
)
@@ -133,8 +137,8 @@ fun ColumnScope.AppsFilter(
onClick = { sortByMenuExpanded = !sortByMenuExpanded },
)
FilterChip(
selected = onlyInstalledApps,
leadingIcon = if (onlyInstalledApps) {
selected = filter.model.onlyInstalledApps,
leadingIcon = if (filter.model.onlyInstalledApps) {
{
Icon(
imageVector = Icons.Filled.Done,
@@ -144,7 +148,7 @@ fun ColumnScope.AppsFilter(
}
} else null,
label = { Text(stringResource(R.string.app_installed)) },
onClick = toggleOnlyInstalledApps,
onClick = { filter.showOnlyInstalledApps(!filter.model.onlyInstalledApps) },
)
FilterChip(
selected = false,
@@ -161,11 +165,11 @@ fun ColumnScope.AppsFilter(
expanded = categoryMenuExpanded,
onDismissRequest = { categoryMenuExpanded = false },
) {
categories.forEach { category ->
filter.model.allCategories.forEach { category ->
DropdownMenuItem(
text = { Text(category) },
onClick = {
addedCategories.add(category)
filter.addCategory(category)
categoryMenuExpanded = false
},
)

View File

@@ -8,33 +8,24 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.ExpandedFullScreenSearchBar
import androidx.compose.material3.ExpandedDockedSearchBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopSearchBar
import androidx.compose.material3.rememberSearchBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun AppsSearch(
onlyInstalledApps: Boolean,
addedCategories: List<String>,
addedRepos: List<String>,
showFilterBadge: Boolean,
toggleFilter: () -> Unit,
onItemClick: (AppNavigationItem) -> Unit,
) {
val textFieldState = rememberTextFieldState()
val scrollBehavior = SearchBarDefaults.enterAlwaysSearchBarScrollBehavior()
@@ -45,8 +36,7 @@ fun AppsSearch(
searchBarState = searchBarState,
textFieldState = textFieldState,
toggleFilter = toggleFilter,
showFilterBadge = addedRepos.isNotEmpty() || addedCategories.isNotEmpty() ||
onlyInstalledApps,
showFilterBadge = showFilterBadge,
)
}
TopSearchBar(
@@ -56,26 +46,29 @@ fun AppsSearch(
windowInsets = WindowInsets.systemBars,
inputField = inputField,
)
ExpandedFullScreenSearchBar(
ExpandedDockedSearchBar(
state = searchBarState,
inputField = inputField,
) {
Column(Modifier.verticalScroll(rememberScrollState())) {
repeat(4) { idx ->
val resultText = "Suggestion $idx"
ListItem(headlineContent = { Text(resultText) },
supportingContent = { Text("Additional info") },
leadingContent = {
Icon(
Icons.Filled.Star,
contentDescription = null
)
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
repeat(4) { i ->
val navItem = AppNavigationItem(
packageName = "$i",
name = "App $i",
summary = "Summary of the app",
isNew = false,
)
AppItem(
name = navItem.name,
summary = navItem.summary,
isNew = navItem.isNew,
isSelected = false,
modifier = Modifier
.clickable {
textFieldState.setTextAndPlaceCursorAtEnd(resultText)
scope.launch { searchBarState.animateToCollapsed() }
scope.launch {
searchBarState.animateToCollapsed()
onItemClick(navItem)
}
}
.fillMaxWidth()
.padding(

View File

@@ -0,0 +1,49 @@
package org.fdroid.basic.ui.main.apps
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import org.fdroid.basic.ui.main.Sort
@Composable
fun FilterPresenter(
appsFlow: Flow<List<AppNavigationItem>>,
onlyInstalledAppsFlow: StateFlow<Boolean>,
sortByFlow: StateFlow<Sort>,
allCategories: List<String>,
addedCategoriesFlow: StateFlow<List<String>>,
): FilterModel {
val apps = appsFlow.collectAsState(null).value
val onlyInstalledApps = onlyInstalledAppsFlow.collectAsState().value
val sortBy = sortByFlow.collectAsState().value
val addedCategories = addedCategoriesFlow.collectAsState().value
val newApps = apps?.filter { app ->
if (onlyInstalledApps) app.packageName.toInt() % 2 > 0 else true
}?.filter { app ->
addedCategories.isEmpty() || addedCategories.any { app.summary.contains(it) }
} ?: emptyList()
return FilterModel(
isLoading = apps == null,
apps = if (sortBy == Sort.NAME) {
newApps.sortedBy { it.packageName.toInt() }
} else {
newApps.sortedByDescending { it.packageName.toInt() }
},
onlyInstalledApps = onlyInstalledApps,
sortBy = sortBy,
allCategories = allCategories,
addedCategories = addedCategories,
)
}
data class FilterModel(
val isLoading: Boolean,
val apps: List<AppNavigationItem>,
val onlyInstalledApps: Boolean,
val sortBy: Sort,
val allCategories: List<String>,
val addedCategories: List<String>,
)

View File

@@ -15,6 +15,7 @@ okhttp = "4.12.0"
room = "2.8.3"
glide = "5.0.5"
glideCompose = "1.0.0-beta08"
molecule = "2.2.0"
androidxCoreKtx = "1.17.0"
androidxAppcompat = "1.7.1"
@@ -120,6 +121,7 @@ glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref =
glide-compose = { module = "com.github.bumptech.glide:compose", version.ref = "glideCompose" }
glide-annotations = { module = "com.github.bumptech.glide:annotations", version.ref = "glide" }
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
guardianproject-netcipher = { module = "info.guardianproject.netcipher:netcipher", version.ref = "guardianprojectNetcipher" }
guardianproject-panic = { module = "info.guardianproject.panic:panic", version.ref = "guardianprojectPanic" }
nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" }