compose: Initial migration of searching logic to compose

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-08-13 15:50:36 +08:00
parent 458b984a06
commit 62e81da258
18 changed files with 327 additions and 100 deletions

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables.app
package com.aurora.store.compose.composables
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
@@ -33,7 +33,7 @@ import com.aurora.store.R
* @param onAction Callback when action button is clicked
*/
@Composable
fun NoAppComposable(
fun ErrorComposable(
modifier: Modifier = Modifier,
@DrawableRes icon: Int,
@StringRes message: Int,
@@ -69,8 +69,8 @@ fun NoAppComposable(
@Preview(showBackground = true)
@Composable
private fun NoAppComposablePreview() {
NoAppComposable(
private fun ErrorComposablePreview() {
ErrorComposable(
icon = R.drawable.ic_updates,
message = R.string.details_no_updates,
actionMessage = R.string.check_updates

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables.app
package com.aurora.store.compose.composables
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -24,7 +24,7 @@ import com.aurora.store.R
*/
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
fun AppProgressComposable(modifier: Modifier = Modifier) {
fun ProgressComposable(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxSize()
@@ -39,6 +39,6 @@ fun AppProgressComposable(modifier: Modifier = Modifier) {
@Preview(showBackground = true)
@Composable
private fun AppProgressComposablePreview() {
AppProgressComposable()
private fun ProgressComposablePreview() {
ProgressComposable()
}

View File

@@ -41,13 +41,13 @@ import com.aurora.store.R
fun SearchSuggestionComposable(
modifier: Modifier = Modifier,
searchSuggestEntry: SearchSuggestEntry,
onClick: () -> Unit = {},
onAction: () -> Unit = {}
onClick: (query: String) -> Unit = {},
onAction: (query: String) -> Unit = {}
) {
Row(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.clickable(onClick = { onClick(searchSuggestEntry.title) })
.padding(dimensionResource(R.dimen.padding_medium)),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
@@ -55,7 +55,7 @@ fun SearchSuggestionComposable(
Row(
modifier = Modifier
.weight(1F)
.clickable(onClick = onClick),
.clickable(onClick = { onClick(searchSuggestEntry.title) }),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
) {
AsyncImage(
@@ -81,7 +81,7 @@ fun SearchSuggestionComposable(
overflow = TextOverflow.Ellipsis
)
}
IconButton(onClick = onAction) {
IconButton(onClick = { onAction(searchSuggestEntry.title) }) {
Icon(
painter = painterResource(R.drawable.ic_search_append),
contentDescription = null

View File

@@ -42,7 +42,10 @@ fun TopAppBarComposable(
navigationIcon = {
if (onNavigateUp != null) {
IconButton(onClick = onNavigateUp) {
Icon(painter = navigationIcon, contentDescription = null)
Icon(
painter = navigationIcon,
contentDescription = stringResource(R.string.action_back)
)
}
}
},

View File

@@ -14,6 +14,7 @@ import androidx.navigation.toRoute
import com.aurora.store.compose.ui.commons.BlacklistScreen
import com.aurora.store.compose.ui.details.AppDetailsScreen
import com.aurora.store.compose.ui.dev.DevProfileScreen
import com.aurora.store.compose.ui.search.SearchScreen
/**
* Navigation graph for compose screens
@@ -37,6 +38,10 @@ fun NavGraph(navHostController: NavHostController, startDestination: Screen) {
BlacklistScreen(onNavigateUp = { onNavigateUp() })
}
composable<Screen.Search> {
SearchScreen(onNavigateUp = { onNavigateUp() })
}
composable<Screen.AppDetails> { backstackEntry ->
val appDetails = backstackEntry.toRoute<Screen.AppDetails>()
AppDetailsScreen(

View File

@@ -37,6 +37,9 @@ sealed class Screen(
@Serializable
data class AppDetails(val packageName: String) : Screen()
@Serializable
data object Search : Screen()
/**
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
*/

View File

@@ -53,8 +53,8 @@ import com.aurora.store.R
import com.aurora.store.compose.composables.HeaderComposable
import com.aurora.store.compose.composables.TopAppBarComposable
import com.aurora.store.compose.composables.app.AppListComposable
import com.aurora.store.compose.composables.app.AppProgressComposable
import com.aurora.store.compose.composables.app.NoAppComposable
import com.aurora.store.compose.composables.ProgressComposable
import com.aurora.store.compose.composables.ErrorComposable
import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.compose.preview.coilPreviewProvider
import com.aurora.store.compose.menu.AppDetailsMenu
@@ -101,7 +101,7 @@ fun AppDetailsScreen(
val plexusScores by viewModel.plexusScores.collectAsStateWithLifecycle()
val suggestions by viewModel.suggestions.collectAsStateWithLifecycle()
LaunchedEffect(key1 = Unit) { viewModel.fetchAppDetails(packageName) }
LaunchedEffect(key1 = packageName) { viewModel.fetchAppDetails(packageName) }
with(app) {
when {
@@ -155,7 +155,7 @@ private fun ScreenContentLoading(onNavigateUp: () -> Unit = {}) {
Scaffold(
topBar = { TopAppBarComposable(onNavigateUp = onNavigateUp) }
) { paddingValues ->
AppProgressComposable(modifier = Modifier.padding(paddingValues))
ProgressComposable(modifier = Modifier.padding(paddingValues))
}
}
@@ -167,7 +167,7 @@ private fun ScreenContentError(onNavigateUp: () -> Unit = {}) {
Scaffold(
topBar = { TopAppBarComposable(onNavigateUp = onNavigateUp) }
) { paddingValues ->
NoAppComposable(
ErrorComposable(
modifier = Modifier.padding(paddingValues),
icon = R.drawable.ic_apps_outage,
message = R.string.toast_app_unavailable

View File

@@ -29,12 +29,14 @@ import coil3.compose.LocalAsyncImagePreviewHandler
import com.aurora.extensions.adaptiveNavigationIcon
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.compose.composables.ErrorComposable
import com.aurora.store.compose.composables.ProgressComposable
import com.aurora.store.compose.composables.TopAppBarComposable
import com.aurora.store.compose.composables.app.AppListComposable
import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.compose.preview.coilPreviewProvider
import com.aurora.store.viewmodel.details.DevProfileViewModel
import com.aurora.store.viewmodel.search.SearchResultViewModel
import com.aurora.store.viewmodel.search.SearchViewModel
import kotlinx.coroutines.flow.flowOf
import kotlin.random.Random
@@ -61,7 +63,7 @@ fun DevProfileScreen(
publisherId: String,
onNavigateUp: () -> Unit,
onNavigateToAppDetails: (packageName: String) -> Unit,
viewModel: SearchResultViewModel = hiltViewModel()
viewModel: SearchViewModel = hiltViewModel()
) {
val apps = viewModel.apps.collectAsLazyPagingItems()
@@ -93,9 +95,15 @@ private fun ScreenContent(
}
) { paddingValues ->
when (apps.loadState.refresh) {
is LoadState.Loading -> {}
is LoadState.Loading -> ProgressComposable()
is LoadState.Error -> {}
is LoadState.Error -> {
ErrorComposable(
modifier = Modifier.padding(paddingValues),
icon = R.drawable.ic_disclaimer,
message = R.string.error
)
}
else -> {
LazyColumn(

View File

@@ -0,0 +1,248 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.search
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.DockedSearchBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.LoadState
import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey
import coil3.annotation.ExperimentalCoilApi
import coil3.compose.LocalAsyncImagePreviewHandler
import com.aurora.gplayapi.SearchSuggestEntry
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.compose.composables.ErrorComposable
import com.aurora.store.compose.composables.ProgressComposable
import com.aurora.store.compose.composables.SearchSuggestionComposable
import com.aurora.store.compose.composables.app.AppListComposable
import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.compose.preview.coilPreviewProvider
import com.aurora.store.compose.ui.details.AppDetailsScreen
import com.aurora.store.viewmodel.search.SearchViewModel
import kotlinx.coroutines.android.awaitFrame
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlin.random.Random
@Composable
fun SearchScreen(onNavigateUp: () -> Unit, viewModel: SearchViewModel = hiltViewModel()) {
val suggestions by viewModel.suggestions.collectAsStateWithLifecycle()
val results = viewModel.apps.collectAsLazyPagingItems()
ScreenContent(
suggestions = suggestions,
results = results,
onNavigateUp = onNavigateUp,
onSearch = { query -> viewModel.newSearch(query) },
onFetchSuggestions = { query -> viewModel.fetchSuggestions(query) }
)
}
@Composable
@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3Api::class)
private fun ScreenContent(
suggestions: List<SearchSuggestEntry> = emptyList(),
results: LazyPagingItems<App> = flowOf(PagingData.empty<App>()).collectAsLazyPagingItems(),
onNavigateUp: () -> Unit = {},
onFetchSuggestions: (String) -> Unit = {},
onSearch: (String) -> Unit = {}
) {
var currentQuery by rememberSaveable { mutableStateOf("") }
var isExpanded by rememberSaveable { mutableStateOf(true) }
val focusRequester = remember { FocusRequester() }
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<String>()
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(focusRequester) {
awaitFrame()
focusRequester.requestFocus()
}
fun closeDetailPane() {
coroutineScope.launch {
scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, null)
}
}
fun showDetailPane(packageName: String) {
coroutineScope.launch {
scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, packageName)
}
}
fun onRequestSuggestions(query: String) {
currentQuery = query.trim()
onFetchSuggestions(query.trim())
}
fun onRequestSearch(query: String) {
currentQuery = query.trim()
isExpanded = false
onSearch(query.trim())
}
@Composable
fun SearchBar() {
DockedSearchBar(
expanded = isExpanded,
onExpandedChange = { expanded -> isExpanded = expanded },
inputField = {
SearchBarDefaults.InputField(
modifier = Modifier.focusRequester(focusRequester),
query = currentQuery,
onQueryChange = { query -> onRequestSuggestions(query) },
onSearch = { query -> onRequestSearch(query) },
expanded = isExpanded,
onExpandedChange = { expanded -> isExpanded = expanded },
leadingIcon = {
IconButton(onClick = onNavigateUp) {
Icon(
painter = painterResource(R.drawable.ic_arrow_back),
contentDescription = stringResource(R.string.action_back)
)
}
},
trailingIcon = {
if (currentQuery.isNotBlank() && isExpanded) {
IconButton(onClick = { currentQuery = "" }) {
Icon(
painter = painterResource(R.drawable.ic_cancel),
contentDescription = stringResource(R.string.action_clear)
)
}
}
},
placeholder = { Text(text = stringResource(R.string.search_hint)) }
)
}
) {
suggestions.forEach { suggestion ->
SearchSuggestionComposable(
searchSuggestEntry = suggestion,
onClick = { query -> onRequestSearch(query) }
)
}
}
}
@Composable
fun ListPane() {
Scaffold(
topBar = {
TopAppBar(title = { SearchBar() })
}
) { paddingValues ->
when (results.loadState.refresh) {
is LoadState.Loading -> ProgressComposable()
is LoadState.Error -> {
ErrorComposable(
modifier = Modifier.padding(paddingValues),
icon = R.drawable.ic_disclaimer,
message = R.string.error
)
}
else -> {
LazyColumn(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(vertical = dimensionResource(R.dimen.padding_medium))
) {
items(count = results.itemCount, key = results.itemKey { it.id }) { index ->
results[index]?.let { app ->
AppListComposable(
app = app,
onClick = { showDetailPane(app.packageName) }
)
}
}
}
}
}
}
}
@Composable
fun DetailPane() {
with(scaffoldNavigator.currentDestination?.contentKey) {
when {
this != null -> {
AppDetailsScreen(
packageName = this,
onNavigateUp = ::closeDetailPane,
onNavigateToAppDetails = { packageName -> showDetailPane(packageName) }
)
}
else -> {
if (currentQuery.isNotBlank() && results.itemCount > 0) {
ErrorComposable(
icon = R.drawable.ic_round_search,
message = R.string.select_app_for_details
)
}
}
}
}
}
NavigableListDetailPaneScaffold(
navigator = scaffoldNavigator,
listPane = { AnimatedPane { ListPane() } },
detailPane = { AnimatedPane { DetailPane() } }
)
}
@PreviewScreenSizes
@Composable
@OptIn(ExperimentalCoilApi::class)
private fun SearchScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
val apps = List(10) { app.copy(id = Random.nextInt()) }
val results = flowOf(PagingData.from(apps)).collectAsLazyPagingItems()
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides coilPreviewProvider) {
ScreenContent(results = results)
}
}

View File

@@ -31,8 +31,10 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.navigation.fragment.findNavController
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.aurora.extensions.navigate
import com.aurora.store.MobileNavigationDirections
import com.aurora.store.R
import com.aurora.store.compose.navigation.Screen
import com.aurora.store.databinding.FragmentAppsGamesBinding
import com.aurora.store.util.Preferences
import com.aurora.store.view.ui.commons.BaseFragment
@@ -114,7 +116,7 @@ class AppsContainerFragment : BaseFragment<FragmentAppsGamesBinding>() {
}.attach()
binding.searchFab.setOnClickListener {
findNavController().navigate(R.id.searchSuggestionFragment)
requireContext().navigate(Screen.Search)
}
}

View File

@@ -41,14 +41,14 @@ import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
import com.aurora.store.view.epoxy.controller.GenericCarouselController
import com.aurora.store.view.epoxy.controller.SearchCarouselController
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.search.SearchResultViewModel
import com.aurora.store.viewmodel.search.SearchViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class SearchResultsFragment : BaseFragment<FragmentSearchResultBinding>(),
GenericCarouselController.Callbacks {
private val viewModel: SearchResultViewModel by activityViewModels()
private val viewModel: SearchViewModel by activityViewModels()
private val controller = SearchCarouselController(this)
private var query: String

View File

@@ -146,6 +146,10 @@ class AppDetailsViewModel @Inject constructor(
fun fetchAppDetails(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
// Reset current details to show latest ones
_app.value = App("")
_suggestions.value = emptyList()
_app.value = appDetailsHelper.getAppByPackageName(packageName).copy(
isInstalled = PackageUtil.isInstalled(context, packageName)
)

View File

@@ -25,6 +25,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.aurora.gplayapi.SearchSuggestEntry
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster
@@ -48,13 +49,13 @@ import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
@HiltViewModel
class SearchResultViewModel @Inject constructor(
class SearchViewModel @Inject constructor(
private val authProvider: AuthProvider,
private val searchHelper: SearchHelper,
private val webSearchHelper: WebSearchHelper
) : ViewModel() {
private val TAG = SearchResultViewModel::class.java.simpleName
private val TAG = SearchViewModel::class.java.simpleName
val liveData: MutableLiveData<ViewState> = MutableLiveData()
@@ -63,6 +64,9 @@ class SearchResultViewModel @Inject constructor(
private val contract: SearchContract
get() = if (authProvider.isAnonymous) webSearchHelper else searchHelper
private val _suggestions = MutableStateFlow<List<SearchSuggestEntry>>(emptyList())
val suggestions = _suggestions.asStateFlow()
private val _apps = MutableStateFlow<PagingData<App>>(PagingData.empty())
val apps = _apps.asStateFlow()
@@ -119,6 +123,12 @@ class SearchResultViewModel @Inject constructor(
.launchIn(viewModelScope)
}
fun fetchSuggestions(query: String) {
viewModelScope.launch(Dispatchers.IO) {
_suggestions.value = contract.searchSuggestions(query)
}
}
@Synchronized
fun search(query: String) {
viewModelScope.launch(Dispatchers.IO) {

View File

@@ -1,26 +1,11 @@
<?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/>.
~
~ 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:tint="?android:attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path

View File

@@ -1,26 +1,11 @@
<?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/>.
~
~ 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:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
<path

View File

@@ -1,26 +1,11 @@
<?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/>.
~
~ 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:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
<path

View File

@@ -1,28 +1,14 @@
<?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/>.
~
~ 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="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:fillColor="@android:color/white"
android:pathData="M15.5,14h-0.79l-0.28,-0.27c1.2,-1.4 1.82,-3.31 1.48,-5.34 -0.47,-2.78 -2.79,-5 -5.59,-5.34 -4.23,-0.52 -7.79,3.04 -7.27,7.27 0.34,2.8 2.56,5.12 5.34,5.59 2.03,0.34 3.94,-0.28 5.34,-1.48l0.27,0.28v0.79l4.25,4.25c0.41,0.41 1.08,0.41 1.49,0 0.41,-0.41 0.41,-1.08 0,-1.49L15.5,14zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
</vector>

View File

@@ -531,4 +531,7 @@
<!-- DownloadWorker -->
<string name="verification_failed">Failed to verify downloaded files</string>
<!-- SearchScreen -->
<string name="select_app_for_details">Select an app for more details</string>
</resources>