compose: Initial migration of searching logic to compose
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* 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.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
@@ -33,7 +33,7 @@ import com.aurora.store.R
|
|||||||
* @param onAction Callback when action button is clicked
|
* @param onAction Callback when action button is clicked
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun NoAppComposable(
|
fun ErrorComposable(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@DrawableRes icon: Int,
|
@DrawableRes icon: Int,
|
||||||
@StringRes message: Int,
|
@StringRes message: Int,
|
||||||
@@ -69,8 +69,8 @@ fun NoAppComposable(
|
|||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
private fun NoAppComposablePreview() {
|
private fun ErrorComposablePreview() {
|
||||||
NoAppComposable(
|
ErrorComposable(
|
||||||
icon = R.drawable.ic_updates,
|
icon = R.drawable.ic_updates,
|
||||||
message = R.string.details_no_updates,
|
message = R.string.details_no_updates,
|
||||||
actionMessage = R.string.check_updates
|
actionMessage = R.string.check_updates
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* 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.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -24,7 +24,7 @@ import com.aurora.store.R
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
fun AppProgressComposable(modifier: Modifier = Modifier) {
|
fun ProgressComposable(modifier: Modifier = Modifier) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -39,6 +39,6 @@ fun AppProgressComposable(modifier: Modifier = Modifier) {
|
|||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
private fun AppProgressComposablePreview() {
|
private fun ProgressComposablePreview() {
|
||||||
AppProgressComposable()
|
ProgressComposable()
|
||||||
}
|
}
|
||||||
@@ -41,13 +41,13 @@ import com.aurora.store.R
|
|||||||
fun SearchSuggestionComposable(
|
fun SearchSuggestionComposable(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
searchSuggestEntry: SearchSuggestEntry,
|
searchSuggestEntry: SearchSuggestEntry,
|
||||||
onClick: () -> Unit = {},
|
onClick: (query: String) -> Unit = {},
|
||||||
onAction: () -> Unit = {}
|
onAction: (query: String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = { onClick(searchSuggestEntry.title) })
|
||||||
.padding(dimensionResource(R.dimen.padding_medium)),
|
.padding(dimensionResource(R.dimen.padding_medium)),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
@@ -55,7 +55,7 @@ fun SearchSuggestionComposable(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1F)
|
.weight(1F)
|
||||||
.clickable(onClick = onClick),
|
.clickable(onClick = { onClick(searchSuggestEntry.title) }),
|
||||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
@@ -81,7 +81,7 @@ fun SearchSuggestionComposable(
|
|||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = onAction) {
|
IconButton(onClick = { onAction(searchSuggestEntry.title) }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_search_append),
|
painter = painterResource(R.drawable.ic_search_append),
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
|
|||||||
@@ -42,7 +42,10 @@ fun TopAppBarComposable(
|
|||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
if (onNavigateUp != null) {
|
if (onNavigateUp != null) {
|
||||||
IconButton(onClick = onNavigateUp) {
|
IconButton(onClick = onNavigateUp) {
|
||||||
Icon(painter = navigationIcon, contentDescription = null)
|
Icon(
|
||||||
|
painter = navigationIcon,
|
||||||
|
contentDescription = stringResource(R.string.action_back)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.navigation.toRoute
|
|||||||
import com.aurora.store.compose.ui.commons.BlacklistScreen
|
import com.aurora.store.compose.ui.commons.BlacklistScreen
|
||||||
import com.aurora.store.compose.ui.details.AppDetailsScreen
|
import com.aurora.store.compose.ui.details.AppDetailsScreen
|
||||||
import com.aurora.store.compose.ui.dev.DevProfileScreen
|
import com.aurora.store.compose.ui.dev.DevProfileScreen
|
||||||
|
import com.aurora.store.compose.ui.search.SearchScreen
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigation graph for compose screens
|
* Navigation graph for compose screens
|
||||||
@@ -37,6 +38,10 @@ fun NavGraph(navHostController: NavHostController, startDestination: Screen) {
|
|||||||
BlacklistScreen(onNavigateUp = { onNavigateUp() })
|
BlacklistScreen(onNavigateUp = { onNavigateUp() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable<Screen.Search> {
|
||||||
|
SearchScreen(onNavigateUp = { onNavigateUp() })
|
||||||
|
}
|
||||||
|
|
||||||
composable<Screen.AppDetails> { backstackEntry ->
|
composable<Screen.AppDetails> { backstackEntry ->
|
||||||
val appDetails = backstackEntry.toRoute<Screen.AppDetails>()
|
val appDetails = backstackEntry.toRoute<Screen.AppDetails>()
|
||||||
AppDetailsScreen(
|
AppDetailsScreen(
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ sealed class Screen(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class AppDetails(val packageName: String) : Screen()
|
data class AppDetails(val packageName: String) : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Search : Screen()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
|
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ import com.aurora.store.R
|
|||||||
import com.aurora.store.compose.composables.HeaderComposable
|
import com.aurora.store.compose.composables.HeaderComposable
|
||||||
import com.aurora.store.compose.composables.TopAppBarComposable
|
import com.aurora.store.compose.composables.TopAppBarComposable
|
||||||
import com.aurora.store.compose.composables.app.AppListComposable
|
import com.aurora.store.compose.composables.app.AppListComposable
|
||||||
import com.aurora.store.compose.composables.app.AppProgressComposable
|
import com.aurora.store.compose.composables.ProgressComposable
|
||||||
import com.aurora.store.compose.composables.app.NoAppComposable
|
import com.aurora.store.compose.composables.ErrorComposable
|
||||||
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.menu.AppDetailsMenu
|
import com.aurora.store.compose.menu.AppDetailsMenu
|
||||||
@@ -101,7 +101,7 @@ fun AppDetailsScreen(
|
|||||||
val plexusScores by viewModel.plexusScores.collectAsStateWithLifecycle()
|
val plexusScores by viewModel.plexusScores.collectAsStateWithLifecycle()
|
||||||
val suggestions by viewModel.suggestions.collectAsStateWithLifecycle()
|
val suggestions by viewModel.suggestions.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(key1 = Unit) { viewModel.fetchAppDetails(packageName) }
|
LaunchedEffect(key1 = packageName) { viewModel.fetchAppDetails(packageName) }
|
||||||
|
|
||||||
with(app) {
|
with(app) {
|
||||||
when {
|
when {
|
||||||
@@ -155,7 +155,7 @@ private fun ScreenContentLoading(onNavigateUp: () -> Unit = {}) {
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { TopAppBarComposable(onNavigateUp = onNavigateUp) }
|
topBar = { TopAppBarComposable(onNavigateUp = onNavigateUp) }
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
AppProgressComposable(modifier = Modifier.padding(paddingValues))
|
ProgressComposable(modifier = Modifier.padding(paddingValues))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ private fun ScreenContentError(onNavigateUp: () -> Unit = {}) {
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { TopAppBarComposable(onNavigateUp = onNavigateUp) }
|
topBar = { TopAppBarComposable(onNavigateUp = onNavigateUp) }
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
NoAppComposable(
|
ErrorComposable(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
icon = R.drawable.ic_apps_outage,
|
icon = R.drawable.ic_apps_outage,
|
||||||
message = R.string.toast_app_unavailable
|
message = R.string.toast_app_unavailable
|
||||||
|
|||||||
@@ -29,12 +29,14 @@ import coil3.compose.LocalAsyncImagePreviewHandler
|
|||||||
import com.aurora.extensions.adaptiveNavigationIcon
|
import com.aurora.extensions.adaptiveNavigationIcon
|
||||||
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.composables.ErrorComposable
|
||||||
|
import com.aurora.store.compose.composables.ProgressComposable
|
||||||
import com.aurora.store.compose.composables.TopAppBarComposable
|
import com.aurora.store.compose.composables.TopAppBarComposable
|
||||||
import com.aurora.store.compose.composables.app.AppListComposable
|
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.viewmodel.details.DevProfileViewModel
|
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 kotlinx.coroutines.flow.flowOf
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@@ -61,7 +63,7 @@ fun DevProfileScreen(
|
|||||||
publisherId: String,
|
publisherId: String,
|
||||||
onNavigateUp: () -> Unit,
|
onNavigateUp: () -> Unit,
|
||||||
onNavigateToAppDetails: (packageName: String) -> Unit,
|
onNavigateToAppDetails: (packageName: String) -> Unit,
|
||||||
viewModel: SearchResultViewModel = hiltViewModel()
|
viewModel: SearchViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
val apps = viewModel.apps.collectAsLazyPagingItems()
|
val apps = viewModel.apps.collectAsLazyPagingItems()
|
||||||
|
|
||||||
@@ -93,9 +95,15 @@ private fun ScreenContent(
|
|||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
when (apps.loadState.refresh) {
|
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 -> {
|
else -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,8 +31,10 @@ import androidx.fragment.app.viewModels
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
import com.aurora.extensions.navigate
|
||||||
import com.aurora.store.MobileNavigationDirections
|
import com.aurora.store.MobileNavigationDirections
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.navigation.Screen
|
||||||
import com.aurora.store.databinding.FragmentAppsGamesBinding
|
import com.aurora.store.databinding.FragmentAppsGamesBinding
|
||||||
import com.aurora.store.util.Preferences
|
import com.aurora.store.util.Preferences
|
||||||
import com.aurora.store.view.ui.commons.BaseFragment
|
import com.aurora.store.view.ui.commons.BaseFragment
|
||||||
@@ -114,7 +116,7 @@ class AppsContainerFragment : BaseFragment<FragmentAppsGamesBinding>() {
|
|||||||
}.attach()
|
}.attach()
|
||||||
|
|
||||||
binding.searchFab.setOnClickListener {
|
binding.searchFab.setOnClickListener {
|
||||||
findNavController().navigate(R.id.searchSuggestionFragment)
|
requireContext().navigate(Screen.Search)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.GenericCarouselController
|
||||||
import com.aurora.store.view.epoxy.controller.SearchCarouselController
|
import com.aurora.store.view.epoxy.controller.SearchCarouselController
|
||||||
import com.aurora.store.view.ui.commons.BaseFragment
|
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
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class SearchResultsFragment : BaseFragment<FragmentSearchResultBinding>(),
|
class SearchResultsFragment : BaseFragment<FragmentSearchResultBinding>(),
|
||||||
GenericCarouselController.Callbacks {
|
GenericCarouselController.Callbacks {
|
||||||
|
|
||||||
private val viewModel: SearchResultViewModel by activityViewModels()
|
private val viewModel: SearchViewModel by activityViewModels()
|
||||||
private val controller = SearchCarouselController(this)
|
private val controller = SearchCarouselController(this)
|
||||||
|
|
||||||
private var query: String
|
private var query: String
|
||||||
|
|||||||
@@ -146,6 +146,10 @@ class AppDetailsViewModel @Inject constructor(
|
|||||||
fun fetchAppDetails(packageName: String) {
|
fun fetchAppDetails(packageName: String) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
// Reset current details to show latest ones
|
||||||
|
_app.value = App("")
|
||||||
|
_suggestions.value = emptyList()
|
||||||
|
|
||||||
_app.value = appDetailsHelper.getAppByPackageName(packageName).copy(
|
_app.value = appDetailsHelper.getAppByPackageName(packageName).copy(
|
||||||
isInstalled = PackageUtil.isInstalled(context, packageName)
|
isInstalled = PackageUtil.isInstalled(context, packageName)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ 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 com.aurora.gplayapi.SearchSuggestEntry
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.gplayapi.data.models.StreamBundle
|
import com.aurora.gplayapi.data.models.StreamBundle
|
||||||
import com.aurora.gplayapi.data.models.StreamCluster
|
import com.aurora.gplayapi.data.models.StreamCluster
|
||||||
@@ -48,13 +49,13 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SearchResultViewModel @Inject constructor(
|
class SearchViewModel @Inject constructor(
|
||||||
private val authProvider: AuthProvider,
|
private val authProvider: AuthProvider,
|
||||||
private val searchHelper: SearchHelper,
|
private val searchHelper: SearchHelper,
|
||||||
private val webSearchHelper: WebSearchHelper
|
private val webSearchHelper: WebSearchHelper
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val TAG = SearchResultViewModel::class.java.simpleName
|
private val TAG = SearchViewModel::class.java.simpleName
|
||||||
|
|
||||||
val liveData: MutableLiveData<ViewState> = MutableLiveData()
|
val liveData: MutableLiveData<ViewState> = MutableLiveData()
|
||||||
|
|
||||||
@@ -63,6 +64,9 @@ class SearchResultViewModel @Inject constructor(
|
|||||||
private val contract: SearchContract
|
private val contract: SearchContract
|
||||||
get() = if (authProvider.isAnonymous) webSearchHelper else searchHelper
|
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())
|
private val _apps = MutableStateFlow<PagingData<App>>(PagingData.empty())
|
||||||
val apps = _apps.asStateFlow()
|
val apps = _apps.asStateFlow()
|
||||||
|
|
||||||
@@ -119,6 +123,12 @@ class SearchResultViewModel @Inject constructor(
|
|||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun fetchSuggestions(query: String) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_suggestions.value = contract.searchSuggestions(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun search(query: String) {
|
fun search(query: String) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
@@ -1,26 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
~ Aurora Store
|
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
|
||||||
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
~ SPDX-License-Identifier: Apache-2.0
|
||||||
~
|
|
||||||
~ 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/>.
|
|
||||||
~
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:tint="?android:attr/colorControlNormal"
|
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="24">
|
||||||
<path
|
<path
|
||||||
|
|||||||
@@ -1,26 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
~ Aurora Store
|
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
|
||||||
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
~ SPDX-License-Identifier: Apache-2.0
|
||||||
~
|
|
||||||
~ 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/>.
|
|
||||||
~
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:tint="?attr/colorControlNormal"
|
|
||||||
android:viewportWidth="960"
|
android:viewportWidth="960"
|
||||||
android:viewportHeight="960">
|
android:viewportHeight="960">
|
||||||
<path
|
<path
|
||||||
|
|||||||
@@ -1,26 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
~ Aurora Store
|
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
|
||||||
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
~ SPDX-License-Identifier: Apache-2.0
|
||||||
~
|
|
||||||
~ 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/>.
|
|
||||||
~
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:tint="?attr/colorControlNormal"
|
|
||||||
android:viewportWidth="960"
|
android:viewportWidth="960"
|
||||||
android:viewportHeight="960">
|
android:viewportHeight="960">
|
||||||
<path
|
<path
|
||||||
|
|||||||
@@ -1,28 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
~ Aurora Store
|
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
|
||||||
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
~ SPDX-License-Identifier: Apache-2.0
|
||||||
~
|
|
||||||
~ 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/>.
|
|
||||||
~
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="24">
|
||||||
<path
|
<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" />
|
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>
|
</vector>
|
||||||
|
|||||||
@@ -531,4 +531,7 @@
|
|||||||
|
|
||||||
<!-- DownloadWorker -->
|
<!-- DownloadWorker -->
|
||||||
<string name="verification_failed">Failed to verify downloaded files</string>
|
<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>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user