compose: search: Bring back filters for search results
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
11
app/src/main/java/com/aurora/extensions/App.kt
Normal file
11
app/src/main/java/com/aurora/extensions/App.kt
Normal 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)
|
||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
|
||||||
14
app/src/main/res/drawable/ic_arrow_drop_down.xml
Normal file
14
app/src/main/res/drawable/ic_arrow_drop_down.xml
Normal 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>
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user