mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-04-29 03:06:57 -04:00
Add search filtering to My Apps
This commit is contained in:
@@ -33,6 +33,7 @@ import org.fdroid.database.AppListSortOrder
|
||||
import org.fdroid.fdroid.ui.theme.FDroidContent
|
||||
import org.fdroid.next.R
|
||||
import org.fdroid.ui.apps.MyApps
|
||||
import org.fdroid.ui.apps.MyAppsInfo
|
||||
import org.fdroid.ui.apps.MyAppsViewModel
|
||||
import org.fdroid.ui.details.AppDetails
|
||||
import org.fdroid.ui.details.AppDetailsViewModel
|
||||
@@ -104,10 +105,18 @@ fun Main(onListeningForIntent: () -> Unit = {}) {
|
||||
},
|
||||
) {
|
||||
val myAppsViewModel = hiltViewModel<MyAppsViewModel>()
|
||||
val myAppsModel =
|
||||
myAppsViewModel.myAppsModel.collectAsStateWithLifecycle().value
|
||||
val myAppsInfo = object : MyAppsInfo {
|
||||
override val model =
|
||||
myAppsViewModel.myAppsModel.collectAsStateWithLifecycle().value
|
||||
|
||||
override fun refresh() = myAppsViewModel.refresh()
|
||||
override fun changeSortOrder(sort: AppListSortOrder) =
|
||||
myAppsViewModel.changeSortOrder(sort)
|
||||
|
||||
override fun search(query: String) = myAppsViewModel.search(query)
|
||||
}
|
||||
MyApps(
|
||||
myAppsModel = myAppsModel,
|
||||
myAppsInfo = myAppsInfo,
|
||||
currentPackageName = if (isBigScreen) {
|
||||
(backStack.last() as? NavigationKey.AppDetails)?.packageName
|
||||
} else null,
|
||||
@@ -115,9 +124,7 @@ fun Main(onListeningForIntent: () -> Unit = {}) {
|
||||
backStack.add(NavigationKey.AppDetails(it))
|
||||
},
|
||||
onNav = { backStack.add(it) },
|
||||
onRefresh = myAppsViewModel::refresh,
|
||||
isBigScreen = isBigScreen,
|
||||
onSortChanged = myAppsViewModel::changeSortOrder,
|
||||
)
|
||||
}
|
||||
entry<NavigationKey.AppDetails>(
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package org.fdroid.ui.apps
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -11,6 +14,7 @@ import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccessTime
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.SortByAlpha
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
@@ -30,11 +34,13 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||
@@ -45,6 +51,7 @@ import org.fdroid.fdroid.ui.theme.FDroidContent
|
||||
import org.fdroid.next.R
|
||||
import org.fdroid.ui.BottomBar
|
||||
import org.fdroid.ui.NavigationKey
|
||||
import org.fdroid.ui.lists.TopSearchBar
|
||||
import org.fdroid.ui.utils.BigLoadingIndicator
|
||||
import org.fdroid.ui.utils.Names
|
||||
import org.fdroid.ui.utils.getPreviewVersion
|
||||
@@ -53,29 +60,46 @@ import java.util.concurrent.TimeUnit.DAYS
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
fun MyApps(
|
||||
myAppsModel: MyAppsModel,
|
||||
myAppsInfo: MyAppsInfo,
|
||||
currentPackageName: String?,
|
||||
onAppItemClick: (String) -> Unit,
|
||||
onNav: (NavKey) -> Unit,
|
||||
onSortChanged: (AppListSortOrder) -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
isBigScreen: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val myAppsModel = myAppsInfo.model
|
||||
LifecycleStartEffect(myAppsModel) {
|
||||
onRefresh()
|
||||
myAppsInfo.refresh()
|
||||
onStopOrDispose { }
|
||||
}
|
||||
val updatableApps = myAppsModel.appUpdates
|
||||
val installedApps = myAppsModel.installedApps
|
||||
val scrollBehavior = enterAlwaysScrollBehavior(rememberTopAppBarState())
|
||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||
val onSearchCleared = { myAppsInfo.search("") }
|
||||
// when search bar is shown, back button closes it again
|
||||
BackHandler(enabled = searchActive) {
|
||||
searchActive = false
|
||||
onSearchCleared()
|
||||
}
|
||||
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
if (searchActive) {
|
||||
TopSearchBar(onSearch = myAppsInfo::search, onSearchCleared) {
|
||||
onBackPressedDispatcher?.onBackPressed()
|
||||
}
|
||||
} else TopAppBar(
|
||||
title = {
|
||||
Text(stringResource(R.string.menu_apps_my))
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { searchActive = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Search,
|
||||
contentDescription = stringResource(R.string.menu_search),
|
||||
)
|
||||
}
|
||||
var sortByMenuExpanded by remember { mutableStateOf(false) }
|
||||
IconButton(onClick = { sortByMenuExpanded = !sortByMenuExpanded }) {
|
||||
Icon(Icons.Filled.MoreVert, null)
|
||||
@@ -96,7 +120,7 @@ fun MyApps(
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onSortChanged(AppListSortOrder.NAME)
|
||||
myAppsInfo.changeSortOrder(AppListSortOrder.NAME)
|
||||
sortByMenuExpanded = false
|
||||
},
|
||||
)
|
||||
@@ -112,7 +136,7 @@ fun MyApps(
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onSortChanged(LAST_UPDATED)
|
||||
myAppsInfo.changeSortOrder(LAST_UPDATED)
|
||||
sortByMenuExpanded = false
|
||||
},
|
||||
)
|
||||
@@ -127,7 +151,20 @@ fun MyApps(
|
||||
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
if (updatableApps == null && installedApps == null) BigLoadingIndicator()
|
||||
else LazyColumn(
|
||||
else if (updatableApps.isNullOrEmpty() && installedApps.isNullOrEmpty()) {
|
||||
Text(
|
||||
text = if (searchActive) {
|
||||
stringResource(R.string.search_my_apps_no_results)
|
||||
} else {
|
||||
stringResource(R.string.my_apps_empty)
|
||||
},
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
)
|
||||
} else LazyColumn(
|
||||
modifier
|
||||
.padding(paddingValues)
|
||||
.then(
|
||||
@@ -212,19 +249,24 @@ fun MyApps(
|
||||
@Preview
|
||||
@Composable
|
||||
fun MyAppsLoadingPreview() {
|
||||
val info = object : MyAppsInfo {
|
||||
override val model = MyAppsModel(
|
||||
appUpdates = null,
|
||||
installedApps = null,
|
||||
sortOrder = AppListSortOrder.NAME,
|
||||
)
|
||||
|
||||
override fun refresh() {}
|
||||
override fun changeSortOrder(sort: AppListSortOrder) {}
|
||||
override fun search(query: String) {}
|
||||
}
|
||||
FDroidContent {
|
||||
MyApps(
|
||||
myAppsModel = MyAppsModel(
|
||||
appUpdates = null,
|
||||
installedApps = null,
|
||||
sortOrder = AppListSortOrder.NAME,
|
||||
),
|
||||
myAppsInfo = info,
|
||||
currentPackageName = null,
|
||||
onAppItemClick = {},
|
||||
onNav = {},
|
||||
onSortChanged = { },
|
||||
isBigScreen = false,
|
||||
onRefresh = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -272,13 +314,16 @@ fun MyAppsPreview() {
|
||||
sortOrder = AppListSortOrder.NAME,
|
||||
)
|
||||
MyApps(
|
||||
myAppsModel = model,
|
||||
myAppsInfo = object : MyAppsInfo {
|
||||
override val model = model
|
||||
override fun refresh() {}
|
||||
override fun changeSortOrder(sort: AppListSortOrder) {}
|
||||
override fun search(query: String) {}
|
||||
},
|
||||
currentPackageName = null,
|
||||
onAppItemClick = {},
|
||||
onNav = {},
|
||||
onSortChanged = { },
|
||||
isBigScreen = false,
|
||||
onRefresh = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
16
next/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt
Normal file
16
next/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package org.fdroid.ui.apps
|
||||
|
||||
import org.fdroid.database.AppListSortOrder
|
||||
|
||||
interface MyAppsInfo {
|
||||
val model: MyAppsModel
|
||||
fun refresh()
|
||||
fun changeSortOrder(sort: AppListSortOrder)
|
||||
fun search(query: String)
|
||||
}
|
||||
|
||||
data class MyAppsModel(
|
||||
val appUpdates: List<AppUpdateItem>? = null,
|
||||
val installedApps: List<InstalledAppItem>? = null,
|
||||
val sortOrder: AppListSortOrder = AppListSortOrder.NAME,
|
||||
)
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.fdroid.database.AppListSortOrder
|
||||
import org.fdroid.ui.utils.normalize
|
||||
import java.text.Collator
|
||||
import java.util.Locale
|
||||
|
||||
@@ -13,26 +14,37 @@ import java.util.Locale
|
||||
fun MyAppsPresenter(
|
||||
appUpdatesFlow: StateFlow<List<AppUpdateItem>?>,
|
||||
installedAppsFlow: StateFlow<List<InstalledAppItem>?>,
|
||||
searchQueryFlow: StateFlow<String>,
|
||||
sortOrderFlow: StateFlow<AppListSortOrder>,
|
||||
): MyAppsModel {
|
||||
val appUpdates = appUpdatesFlow.collectAsState().value
|
||||
val installedApps = installedAppsFlow.collectAsState().value
|
||||
val searchQuery = searchQueryFlow.collectAsState().value.normalize()
|
||||
val sortOrder = sortOrderFlow.collectAsState().value
|
||||
val packageNames = appUpdates?.map { it.packageName } ?: emptyList()
|
||||
val collator = Collator.getInstance(Locale.getDefault())
|
||||
|
||||
val updates = if (searchQuery.isBlank()) appUpdates else appUpdates?.filter {
|
||||
it.name.normalize().contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
val installed = if (searchQuery.isBlank()) installedApps else installedApps?.filter {
|
||||
it.name.normalize().contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
return MyAppsModel(
|
||||
appUpdates = when (sortOrder) {
|
||||
AppListSortOrder.NAME -> appUpdates?.sortedWith { a1, a2 ->
|
||||
AppListSortOrder.NAME -> updates?.sortedWith { a1, a2 ->
|
||||
// storing collator.getCollationKey() and using that could be an optimization
|
||||
collator.compare(a1.name, a2.name)
|
||||
}
|
||||
AppListSortOrder.LAST_UPDATED -> appUpdates?.sortedByDescending { it.update.added }
|
||||
AppListSortOrder.LAST_UPDATED -> updates?.sortedByDescending { it.update.added }
|
||||
},
|
||||
installedApps = installedApps?.filter {
|
||||
installedApps = installed?.filter {
|
||||
// filter out apps already in updates
|
||||
it.packageName !in packageNames
|
||||
}?.let { apps ->
|
||||
when (sortOrder) {
|
||||
AppListSortOrder.NAME -> apps.sortedWith { a1, a2 ->
|
||||
// storing collator.getCollationKey() and using that could be an optimization
|
||||
collator.compare(a1.name, a2.name)
|
||||
}
|
||||
AppListSortOrder.LAST_UPDATED -> apps.sortedByDescending { it.lastUpdated }
|
||||
@@ -41,9 +53,3 @@ fun MyAppsPresenter(
|
||||
sortOrder = sortOrder,
|
||||
)
|
||||
}
|
||||
|
||||
data class MyAppsModel(
|
||||
val appUpdates: List<AppUpdateItem>? = null,
|
||||
val installedApps: List<InstalledAppItem>? = null,
|
||||
val sortOrder: AppListSortOrder = AppListSortOrder.NAME,
|
||||
)
|
||||
|
||||
@@ -40,14 +40,13 @@ class MyAppsViewModel @Inject constructor(
|
||||
private val repoManager: RepoManager,
|
||||
) : AndroidViewModel(app) {
|
||||
|
||||
val updates = updatesManager.updates
|
||||
val numUpdates = updatesManager.numUpdates
|
||||
private val localeList = LocaleListCompat.getDefault()
|
||||
private val scope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main)
|
||||
|
||||
private val updates = updatesManager.updates
|
||||
private val installedApps = MutableStateFlow<List<InstalledAppItem>?>(null)
|
||||
private var installedAppsLiveData =
|
||||
db.getAppDao().getInstalledAppListItems(application.packageManager)
|
||||
private val sortOrder = savedStateHandle.getMutableStateFlow("sort", AppListSortOrder.NAME)
|
||||
private val scope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main)
|
||||
private val localeList = LocaleListCompat.getDefault()
|
||||
private val installedAppsObserver = Observer<List<AppListItem>> { list ->
|
||||
installedApps.value = list.map { app ->
|
||||
InstalledAppItem(
|
||||
@@ -61,10 +60,13 @@ class MyAppsViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
private val searchQuery = savedStateHandle.getMutableStateFlow<String>("query", "")
|
||||
private val sortOrder = savedStateHandle.getMutableStateFlow("sort", AppListSortOrder.NAME)
|
||||
val myAppsModel: StateFlow<MyAppsModel> = scope.launchMolecule(mode = ContextClock) {
|
||||
MyAppsPresenter(
|
||||
appUpdatesFlow = updates,
|
||||
installedAppsFlow = installedApps,
|
||||
searchQueryFlow = searchQuery,
|
||||
sortOrderFlow = sortOrder,
|
||||
)
|
||||
}
|
||||
@@ -77,6 +79,10 @@ class MyAppsViewModel @Inject constructor(
|
||||
installedAppsLiveData.removeObserver(installedAppsObserver)
|
||||
}
|
||||
|
||||
fun search(query: String) {
|
||||
searchQuery.value = query
|
||||
}
|
||||
|
||||
fun changeSortOrder(sort: AppListSortOrder) {
|
||||
sortOrder.value = sort
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
<string name="no_app_selected">No app selected</string>
|
||||
<string name="no_repository_selected">No repository selected</string>
|
||||
|
||||
<string name="my_apps_empty">No apps installed.\n\nInstall apps and they will appear here.</string>
|
||||
|
||||
<string name="app_list_new">New apps</string>
|
||||
<string name="app_list_recently_updated">Recently updated</string>
|
||||
<string name="app_list_all">All apps</string>
|
||||
@@ -21,6 +23,7 @@
|
||||
<string name="search_placeholder">Search…</string>
|
||||
<string name="search_no_results">No apps found.\n\nTry to use less search terms or add/enable more repositories.</string>
|
||||
<string name="search_filter_no_results">No matching apps.\n\nTry to use less search terms or remove filters.</string>
|
||||
<string name="search_my_apps_no_results">No matching apps.\n\nTry to use less search terms.</string>
|
||||
<string name="sort_by_name">Sort by name</string>
|
||||
<string name="sort_by_latest">Sort by latest</string>
|
||||
<!-- Used for filtering lists of apps by certain criteria -->
|
||||
|
||||
Reference in New Issue
Block a user