compose: search: Bring back filters for search results

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-08-25 15:58:41 +08:00
parent 8ddba3d52c
commit 6cdb744099
15 changed files with 239 additions and 373 deletions

View File

@@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.extensions
import com.aurora.Constants.PACKAGE_NAME_GMS
import com.aurora.gplayapi.data.models.App
fun App.requiresGMS() = dependencies.dependentPackages.contains(PACKAGE_NAME_GMS)

View File

@@ -29,8 +29,8 @@ import coil3.compose.AsyncImage
import coil3.compose.LocalAsyncImagePreviewHandler import coil3.compose.LocalAsyncImagePreviewHandler
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.crossfade import coil3.request.crossfade
import com.aurora.Constants.PACKAGE_NAME_GMS
import com.aurora.extensions.bodyVerySmall import com.aurora.extensions.bodyVerySmall
import com.aurora.extensions.requiresGMS
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.AppPreviewProvider
@@ -110,7 +110,7 @@ private fun buildExtras(app: App): List<String> {
add(stringResource(R.string.details_no_ads)) add(stringResource(R.string.details_no_ads))
} }
if (app.dependencies.dependentPackages.contains(PACKAGE_NAME_GMS)) { if (app.requiresGMS()) {
add(stringResource(R.string.details_gsf_dependent)) add(stringResource(R.string.details_gsf_dependent))
} }
} }

View File

@@ -44,10 +44,10 @@ import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.LocalAsyncImagePreviewHandler import coil3.compose.LocalAsyncImagePreviewHandler
import com.aurora.Constants.PACKAGE_NAME_GMS
import com.aurora.Constants.SHARE_URL import com.aurora.Constants.SHARE_URL
import com.aurora.extensions.appInfo import com.aurora.extensions.appInfo
import com.aurora.extensions.browse import com.aurora.extensions.browse
import com.aurora.extensions.requiresGMS
import com.aurora.extensions.share import com.aurora.extensions.share
import com.aurora.extensions.toast import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
@@ -348,7 +348,7 @@ private fun ScreenContentApp(
} }
AppCompatibility( AppCompatibility(
needsGms = app.dependencies.dependentPackages.contains(PACKAGE_NAME_GMS), needsGms = app.requiresGMS(),
plexusScores = plexusScores plexusScores = plexusScores
) )

View File

@@ -5,14 +5,23 @@
package com.aurora.store.compose.ui.search package com.aurora.store.compose.ui.search
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
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.text.input.clearText import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.material3.AppBarWithSearch import androidx.compose.material3.AppBarWithSearch
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExpandedDockedSearchBar import androidx.compose.material3.ExpandedDockedSearchBar
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -27,8 +36,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
@@ -36,6 +48,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.tooling.preview.PreviewScreenSizes
@@ -57,6 +70,7 @@ import com.aurora.store.compose.composables.app.AppListComposable
import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.compose.preview.coilPreviewProvider import com.aurora.store.compose.preview.coilPreviewProvider
import com.aurora.store.compose.ui.details.AppDetailsScreen import com.aurora.store.compose.ui.details.AppDetailsScreen
import com.aurora.store.data.model.SearchFilter
import com.aurora.store.viewmodel.search.SearchViewModel import com.aurora.store.viewmodel.search.SearchViewModel
import kotlinx.coroutines.android.awaitFrame import kotlinx.coroutines.android.awaitFrame
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@@ -74,7 +88,8 @@ fun SearchScreen(onNavigateUp: () -> Unit, viewModel: SearchViewModel = hiltView
results = results, results = results,
onNavigateUp = onNavigateUp, onNavigateUp = onNavigateUp,
onSearch = { query -> viewModel.search(query) }, onSearch = { query -> viewModel.search(query) },
onFetchSuggestions = { query -> viewModel.fetchSuggestions(query) } onFetchSuggestions = { query -> viewModel.fetchSuggestions(query) },
onFilter = { filter -> viewModel.filterResults(filter) }
) )
} }
@@ -84,7 +99,8 @@ private fun ScreenContent(
results: LazyPagingItems<App> = flowOf(PagingData.empty<App>()).collectAsLazyPagingItems(), results: LazyPagingItems<App> = flowOf(PagingData.empty<App>()).collectAsLazyPagingItems(),
onNavigateUp: () -> Unit = {}, onNavigateUp: () -> Unit = {},
onFetchSuggestions: (String) -> Unit = {}, onFetchSuggestions: (String) -> Unit = {},
onSearch: (String) -> Unit = {} onSearch: (String) -> Unit = {},
onFilter: (filter: SearchFilter) -> Unit = {}
) { ) {
val textFieldState = rememberTextFieldState() val textFieldState = rememberTextFieldState()
val searchBarState = rememberSearchBarState() val searchBarState = rememberSearchBarState()
@@ -104,12 +120,6 @@ private fun ScreenContent(
.collectLatest { query -> onFetchSuggestions(query) } .collectLatest { query -> onFetchSuggestions(query) }
} }
fun closeDetailPane() {
coroutineScope.launch {
scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, null)
}
}
fun showDetailPane(packageName: String) { fun showDetailPane(packageName: String) {
coroutineScope.launch { coroutineScope.launch {
scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, packageName) scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, packageName)
@@ -192,18 +202,27 @@ private fun ScreenContent(
} }
else -> { else -> {
LazyColumn( Column(
modifier = Modifier modifier = Modifier
.padding(paddingValues) .padding(paddingValues)
.fillMaxSize() .fillMaxSize()
.padding(vertical = dimensionResource(R.dimen.padding_medium)) .padding(vertical = dimensionResource(R.dimen.padding_medium))
) { ) {
items(count = results.itemCount, key = results.itemKey { it.id }) { index -> if (textFieldState.text.isNotBlank() && results.itemCount > 0) {
results[index]?.let { app -> FilterHeader(onFilter = onFilter)
AppListComposable( }
app = app,
onClick = { showDetailPane(app.packageName) } LazyColumn {
) items(
count = results.itemCount,
key = results.itemKey { it.id }
) { index ->
results[index]?.let { app ->
AppListComposable(
app = app,
onClick = { showDetailPane(app.packageName) }
)
}
} }
} }
} }
@@ -245,6 +264,129 @@ private fun ScreenContent(
) )
} }
@Composable
private fun FilterHeader(onFilter: (filter: SearchFilter) -> Unit) {
var activeFilter by rememberSaveable { mutableStateOf(SearchFilter()) }
val filters = listOf(
R.string.action_filter_rating,
R.string.app_info_downloads,
R.string.details_free,
R.string.action_filter_no_ads,
R.string.action_filter_no_gms
)
@Composable
fun NonExpandableFilterChip(@StringRes filter: Int, isSelected: Boolean) {
FilterChip(
onClick = {
activeFilter = when (filter) {
R.string.details_free -> activeFilter.copy(isFree = !activeFilter.isFree)
R.string.action_filter_no_ads -> activeFilter.copy(noAds = !activeFilter.noAds)
else -> activeFilter.copy(noGMS = !activeFilter.noGMS)
}
onFilter(activeFilter)
},
label = { Text(text = stringResource(filter)) },
selected = isSelected,
leadingIcon = {
if (isSelected) {
Icon(
painter = painterResource(R.drawable.ic_check),
contentDescription = stringResource(filter)
)
}
},
)
}
@Composable
fun ExpandableFilterChip(@StringRes filter: Int, isSelected: Boolean) {
var isExpanded by rememberSaveable { mutableStateOf(false) }
Box {
FilterChip(
onClick = { isExpanded = !isExpanded },
label = { Text(text = stringResource(filter)) },
selected = isSelected,
leadingIcon = {
if (isSelected) {
Icon(
painter = painterResource(R.drawable.ic_check),
contentDescription = stringResource(filter)
)
}
},
trailingIcon = {
Icon(
painter = painterResource(R.drawable.ic_arrow_drop_down),
contentDescription = stringResource(filter)
)
},
)
DropdownMenu(expanded = isExpanded, onDismissRequest = { isExpanded = false }) {
val downloadLabels = stringArrayResource(R.array.filterDownloadsLabels)
val downloadValues = stringArrayResource(R.array.filterDownloadsValues)
val ratingLabels = stringArrayResource(R.array.filterRatingLabels)
val ratingValues = stringArrayResource(R.array.filterRatingValues)
val options = when (filter) {
R.string.action_filter_rating -> ratingLabels.zip(ratingValues).toMap()
R.string.app_info_downloads -> downloadLabels.zip(downloadValues).toMap()
else -> emptyMap()
}
options.forEach { (key, value) ->
DropdownMenuItem(
text = { Text(text = key) },
onClick = {
activeFilter = when (filter) {
R.string.action_filter_rating -> {
activeFilter.copy(minRating = value.toFloat())
}
else -> activeFilter.copy(minInstalls = value.toLong())
}
onFilter(activeFilter)
isExpanded = false
}
)
}
}
}
}
LazyRow(
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_normal))
) {
items(items = filters, key = { item -> item }) { filter ->
val isSelected = when (filter) {
R.string.action_filter_rating -> activeFilter.minRating > 0.0
R.string.app_info_downloads -> activeFilter.minInstalls > 0
R.string.details_free -> activeFilter.isFree
R.string.action_filter_no_ads -> activeFilter.noAds
R.string.action_filter_no_gms -> activeFilter.noGMS
else -> false
}
when (filter) {
R.string.details_free,
R.string.action_filter_no_ads,
R.string.action_filter_no_gms -> {
NonExpandableFilterChip(filter = filter, isSelected = isSelected)
}
R.string.action_filter_rating,
R.string.app_info_downloads -> {
ExpandableFilterChip(filter = filter, isSelected = isSelected)
}
}
}
}
}
@PreviewScreenSizes @PreviewScreenSizes
@Composable @Composable
private fun SearchScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) { private fun SearchScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {

View File

@@ -1,32 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.data.model
import kotlinx.serialization.Serializable
@Serializable
data class Filter(
val appsWithAds: Boolean = true,
val appsWithIAP: Boolean = true,
val paidApps: Boolean = true,
val gsfDependentApps: Boolean = true,
val rating: Float = 0.0f,
val downloads: Int = 0
)

View File

@@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.data.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
/**
* Filter for list with search results
*/
@Parcelize
@Serializable
data class SearchFilter(
val noAds: Boolean = false,
val isFree: Boolean = false,
val noGMS: Boolean = false,
val minRating: Float = 0F,
val minInstalls: Long = 0
) : Parcelable

View File

@@ -1,54 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.data.providers
import android.content.Context
import com.aurora.store.data.model.Filter
import com.aurora.store.util.Preferences
import com.aurora.store.util.remove
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.serialization.json.Json
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class FilterProvider @Inject constructor(
private val json: Json,
@ApplicationContext private val context: Context
) {
companion object {
const val PREFERENCE_FILTER = "PREFERENCE_FILTER"
}
init {
// Clean any last saved filter
context.remove(PREFERENCE_FILTER)
}
fun getSavedFilter(): Filter {
val rawFilter = Preferences.getString(context, PREFERENCE_FILTER)
return json.decodeFromString<Filter>(rawFilter.ifEmpty { "{}" })
}
fun saveFilter(filter: Filter) {
Preferences.putString(context, PREFERENCE_FILTER, json.encodeToString(filter))
}
}

View File

@@ -1,110 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.ui.sheets
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import com.aurora.store.R
import com.aurora.store.data.model.Filter
import com.aurora.store.databinding.SheetFilterBinding
import com.aurora.store.viewmodel.sheets.FilterViewModel
import com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class FilterSheet : BaseDialogSheet<SheetFilterBinding>() {
private val viewModel: FilterViewModel by viewModels()
private lateinit var filter: Filter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
filter = viewModel.filterProvider.getSavedFilter()
attachSingleChips()
attachMultipleChips()
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
viewModel.filterProvider.saveFilter(filter)
}
private fun attachSingleChips() {
binding.filterGfs.apply {
isChecked = filter.gsfDependentApps
setOnCheckedChangeListener { _, checked ->
isChecked = checked
filter = filter.copy(gsfDependentApps = checked)
}
}
binding.filterPaid.apply {
isChecked = filter.paidApps
setOnCheckedChangeListener { _, checked ->
isChecked = checked
filter = filter.copy(paidApps = checked)
}
}
binding.filterAds.apply {
isChecked = filter.appsWithAds
setOnCheckedChangeListener { _, checked ->
isChecked = checked
filter = filter.copy(appsWithAds = checked)
}
}
}
private fun attachMultipleChips() {
val downloadLabels = resources.getStringArray(R.array.filterDownloadsLabels)
val downloadValues = resources.getStringArray(R.array.filterDownloadsValues)
val ratingLabels = resources.getStringArray(R.array.filterRatingLabels)
val ratingValues = resources.getStringArray(R.array.filterRatingValues)
downloadLabels.forEachIndexed { index, value ->
val chip = Chip(requireContext())
chip.id = index
chip.text = value
chip.textColors
chip.isChecked = filter.downloads == downloadValues[index].toInt()
binding.downloadChips.addView(chip)
}
binding.downloadChips.setOnCheckedStateChangeListener { _, checkedIds ->
filter = filter.copy(downloads = downloadValues[checkedIds[0]].toInt())
}
ratingLabels.forEachIndexed { index, value ->
val chip = Chip(requireContext())
chip.id = index
chip.text = value
chip.isChecked = filter.rating == ratingValues[index].toFloat()
binding.ratingChips.addView(chip)
}
binding.ratingChips.setOnCheckedStateChangeListener { _, checkedIds ->
filter = filter.copy(rating = ratingValues[checkedIds[0]].toFloat())
}
}
}

View File

@@ -11,7 +11,7 @@ import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aurora.Constants import com.aurora.Constants
import com.aurora.Constants.PACKAGE_NAME_GMS import com.aurora.extensions.requiresGMS
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Review import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.data.models.details.TestingProgramStatus import com.aurora.gplayapi.data.models.details.TestingProgramStatus
@@ -162,9 +162,7 @@ class AppDetailsViewModel @Inject constructor(
fetchDataSafetyReport(packageName) fetchDataSafetyReport(packageName)
fetchSuggestions() fetchSuggestions()
fetchExodusPrivacyReport(packageName) fetchExodusPrivacyReport(packageName)
if (app.value!!.dependencies.dependentPackages.contains(PACKAGE_NAME_GMS)) { if (app.value!!.requiresGMS()) fetchPlexusReport(packageName)
fetchPlexusReport(packageName)
}
} }
} }

View File

@@ -1,20 +1,7 @@
/* /*
* Aurora Store * SPDX-FileCopyrightText: 2021 Rahul Kumar Patel <whyorean@gmail.com>
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com> * SPDX-FileCopyrightText: 2025 The Calyx Institute
* * SPDX-License-Identifier: GPL-3.0-or-later
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
package com.aurora.store.viewmodel.search package com.aurora.store.viewmodel.search
@@ -24,21 +11,27 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.cachedIn import androidx.paging.cachedIn
import androidx.paging.filter
import com.aurora.extensions.requiresGMS
import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.SearchSuggestEntry
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.SearchHelper import com.aurora.gplayapi.helpers.SearchHelper
import com.aurora.gplayapi.helpers.contracts.SearchContract import com.aurora.gplayapi.helpers.contracts.SearchContract
import com.aurora.gplayapi.helpers.web.WebSearchHelper import com.aurora.gplayapi.helpers.web.WebSearchHelper
import com.aurora.store.data.model.SearchFilter
import com.aurora.store.data.paging.GenericPagingSource.Companion.createPager import com.aurora.store.data.paging.GenericPagingSource.Companion.createPager
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@@ -57,8 +50,24 @@ class SearchViewModel @Inject constructor(
private val _suggestions = MutableStateFlow<List<SearchSuggestEntry>>(emptyList()) private val _suggestions = MutableStateFlow<List<SearchSuggestEntry>>(emptyList())
val suggestions = _suggestions.asStateFlow() val suggestions = _suggestions.asStateFlow()
private val _filter = MutableStateFlow(SearchFilter())
private val _apps = MutableStateFlow<PagingData<App>>(PagingData.empty()) private val _apps = MutableStateFlow<PagingData<App>>(PagingData.empty())
val apps = _apps.asStateFlow() val apps = combine(_filter, _apps) { filter, pagingData ->
pagingData.filter { app ->
when {
filter.noAds && app.containsAds -> false
filter.isFree && !app.isFree -> false
filter.noGMS && app.requiresGMS() -> false
app.rating.average < filter.minRating -> false
app.installs < filter.minInstalls -> false
else -> true
}
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), PagingData.empty())
fun filterResults(filter: SearchFilter) {
_filter.value = filter
}
fun search(query: String) { fun search(query: String) {
var nextBundleUrl: String? = null var nextBundleUrl: String? = null

View File

@@ -1,14 +0,0 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.viewmodel.sheets
import androidx.lifecycle.ViewModel
import com.aurora.store.data.providers.FilterProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class FilterViewModel @Inject constructor(val filterProvider: FilterProvider) : ViewModel()

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,600L280,400L680,400L480,600Z"/>
</vector>

View File

@@ -1,113 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ Aurora Store is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 2 of the License, or
~ (at your option) any later version.
~
~ Aurora Store is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
~
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@drawable/divider"
android:orientation="vertical"
android:padding="@dimen/padding_normal"
android:showDividers="middle">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action_filter_rating"
android:textAppearance="@style/TextAppearance.Aurora.SubTitle" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/rating_chips"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:chipSpacingHorizontal="@dimen/margin_small"
app:chipSpacingVertical="@dimen/margin_xsmall"
app:selectionRequired="true"
app:singleSelection="true" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action_filter_downloads"
android:textAppearance="@style/TextAppearance.Aurora.SubTitle" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/download_chips"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:chipSpacingHorizontal="@dimen/margin_small"
app:chipSpacingVertical="@dimen/margin_xsmall"
app:selectionRequired="true"
app:singleSelection="true" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action_filter_misc"
android:textAppearance="@style/TextAppearance.Aurora.SubTitle" />
<com.google.android.material.chip.ChipGroup
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/filter_gfs"
style="@style/Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action_filter_gsf_dependent_apps" />
<com.google.android.material.chip.Chip
android:id="@+id/filter_paid"
style="@style/Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action_filter_paid_apps" />
<com.google.android.material.chip.Chip
android:id="@+id/filter_ads"
style="@style/Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action_filter_apps_with_ads" />
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
</FrameLayout>

View File

@@ -134,7 +134,6 @@
<item>3.0</item> <item>3.0</item>
<item>4.0</item> <item>4.0</item>
<item>4.5</item> <item>4.5</item>
<item>4.7</item>
</string-array> </string-array>
<string-array name="filterRatingLabels" translatable="false"> <string-array name="filterRatingLabels" translatable="false">
@@ -144,15 +143,10 @@
<item> 3★ + </item> <item> 3★ + </item>
<item> 4★ + </item> <item> 4★ + </item>
<item> 4.5★ + </item> <item> 4.5★ + </item>
<item> 4.7★ + </item>
</string-array> </string-array>
<string-array name="filterDownloadsValues" translatable="false"> <string-array name="filterDownloadsValues" translatable="false">
<item>0</item> <item>0</item>
<item>100</item>
<item>1000</item>
<item>10000</item>
<item>50000</item>
<item>100000</item> <item>100000</item>
<item>1000000</item> <item>1000000</item>
<item>10000000</item> <item>10000000</item>
@@ -162,10 +156,6 @@
<string-array name="filterDownloadsLabels" translatable="false"> <string-array name="filterDownloadsLabels" translatable="false">
<item>@string/action_filter_all</item> <item>@string/action_filter_all</item>
<item> 100 + </item>
<item> 1K + </item>
<item> 10K + </item>
<item> 50K + </item>
<item> 100K + </item> <item> 100K + </item>
<item> 1M + </item> <item> 1M + </item>
<item> 10M + </item> <item> 10M + </item>

View File

@@ -534,4 +534,6 @@
<!-- SearchScreen --> <!-- SearchScreen -->
<string name="select_app_for_details">Select an app for more details</string> <string name="select_app_for_details">Select an app for more details</string>
<string name="action_filter_no_ads">No advertisements</string>
<string name="action_filter_no_gms">No play services</string>
</resources> </resources>