mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-05-19 14:10:38 -04:00
improve search and filtering code
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
96
basic/src/main/java/org/fdroid/basic/MainViewModel.kt
Normal file
96
basic/src/main/java/org/fdroid/basic/MainViewModel.kt
Normal 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 }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ fun AppDetailsPreview() {
|
||||
packageName = "foo",
|
||||
name = "bar",
|
||||
summary = "This is a nice app!",
|
||||
isNew = false,
|
||||
)
|
||||
AppDetails(item)
|
||||
}
|
||||
|
||||
79
basic/src/main/java/org/fdroid/basic/ui/main/apps/AppItem.kt
Normal file
79
basic/src/main/java/org/fdroid/basic/ui/main/apps/AppItem.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -8,4 +8,5 @@ class AppNavigationItem(
|
||||
val packageName: String,
|
||||
val name: String,
|
||||
val summary: String,
|
||||
val isNew: Boolean,
|
||||
): Parcelable
|
||||
|
||||
@@ -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
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>,
|
||||
)
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user