Loading app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultsContent.kt +168 −13 Original line number Diff line number Diff line Loading @@ -25,7 +25,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.Divider Loading @@ -45,6 +45,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import foundation.e.apps.R import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Ratings import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Status import foundation.e.apps.ui.compose.theme.AppTheme import foundation.e.apps.ui.search.v2.SearchTabType import kotlinx.coroutines.launch Loading @@ -53,8 +57,12 @@ import kotlinx.coroutines.launch fun SearchResultsContent( tabs: List<SearchTabType>, selectedTab: SearchTabType, resultsByTab: Map<SearchTabType, List<String>>, resultsByTab: Map<SearchTabType, List<Application>>, onTabSelected: (SearchTabType) -> Unit, onResultClick: (Application) -> Unit = {}, onPrimaryActionClick: (Application) -> Unit = {}, onShowMoreClick: (Application) -> Unit = {}, onPrivacyClick: (Application) -> Unit = {}, modifier: Modifier = Modifier, ) { if (tabs.isEmpty() || selectedTab !in tabs) { Loading Loading @@ -107,6 +115,10 @@ fun SearchResultsContent( val items = resultsByTab[tab].orEmpty() SearchResultList( items = items, onItemClick = onResultClick, onPrimaryActionClick = onPrimaryActionClick, onShowMoreClick = onShowMoreClick, onPrivacyClick = onPrivacyClick, modifier = Modifier.fillMaxSize(), ) } Loading Loading @@ -158,27 +170,130 @@ private fun SearchTabs( @Composable private fun SearchResultList( items: List<String>, items: List<Application>, onItemClick: (Application) -> Unit, onPrimaryActionClick: (Application) -> Unit, onShowMoreClick: (Application) -> Unit, onPrivacyClick: (Application) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn( modifier = modifier, verticalArrangement = Arrangement.spacedBy(12.dp), ) { items( itemsIndexed( items = items, key = { item -> item }, ) { item -> Text( text = item, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onBackground, key = { index, item -> item._id.takeIf { it.isNotBlank() } ?: item.package_name.takeIf { it.isNotBlank() } ?: "${item.name}-$index" }, ) { _, application -> SearchResultListItem( application = application, uiState = application.toSearchResultUiState(), onItemClick = onItemClick, onPrimaryActionClick = onPrimaryActionClick, onShowMoreClick = onShowMoreClick, onPrivacyClick = onPrivacyClick, modifier = Modifier.fillMaxWidth(), ) } } } @Composable private fun Application.toSearchResultUiState(): SearchResultListItemState { if (isPlaceHolder) { return SearchResultListItemState( author = "", ratingText = "", showRating = false, sourceTag = "", showSourceTag = false, privacyScore = "", showPrivacyScore = false, isPrivacyLoading = false, primaryAction = PrimaryActionUiState( label = "", enabled = false, isInProgress = false, isFilledStyle = true, ), iconUrl = null, placeholderResId = null, isPlaceholder = true, ) } val ratingText = when { source == Source.OPEN_SOURCE || source == Source.PWA || isSystemApp -> "" ratings.usageQualityScore >= 0 -> String.format("%.1f", ratings.usageQualityScore) else -> stringResource(id = R.string.not_available) } val sourceTagText = source.toString() return SearchResultListItemState( author = author.ifBlank { package_name }, ratingText = ratingText, showRating = ratingText.isNotBlank(), sourceTag = sourceTagText, showSourceTag = false, privacyScore = "", showPrivacyScore = false, // Privacy scores are disabled on Search per functional spec. isPrivacyLoading = false, primaryAction = resolvePrimaryActionState(this), iconUrl = icon_image_path.takeIf { it.isNotBlank() }, placeholderResId = null, isPlaceholder = false, ) } @Composable private fun resolvePrimaryActionState(application: Application): PrimaryActionUiState { val label = when (application.status) { Status.INSTALLED -> stringResource(id = R.string.open) Status.UPDATABLE -> stringResource(id = R.string.update) Status.INSTALLING -> stringResource(id = R.string.installing) Status.DOWNLOADING, Status.DOWNLOADED, Status.QUEUED, Status.AWAITING -> stringResource(id = R.string.cancel) Status.INSTALLATION_ISSUE -> stringResource(id = R.string.retry) Status.PURCHASE_NEEDED -> application.price.ifBlank { stringResource(id = R.string.install) } Status.BLOCKED -> stringResource(id = R.string.install) Status.UNAVAILABLE -> { if (!application.isFree && !application.isPurchased) { application.price.ifBlank { stringResource(id = R.string.install) } } else { stringResource(id = R.string.install) } } } val isInProgress = when (application.status) { Status.INSTALLING, Status.DOWNLOADING, Status.DOWNLOADED, Status.QUEUED, Status.AWAITING -> true else -> false } val isEnabled = when (application.status) { Status.INSTALLING -> false else -> true } return PrimaryActionUiState( label = label, enabled = isEnabled, isInProgress = isInProgress, isFilledStyle = true, showMore = false, ) } private fun formatPrivacyScore(score: Int): String { if (score < 0) return "" val clamped = score.coerceIn(0, 10) return String.format("%02d/10", clamped) } @StringRes private fun SearchTabType.toLabelRes(): Int = when (this) { SearchTabType.STANDARD_APPS -> R.string.search_tab_standard_apps Loading @@ -198,9 +313,49 @@ private fun SearchResultsContentPreview() { ), selectedTab = SearchTabType.OPEN_SOURCE, resultsByTab = mapOf( SearchTabType.STANDARD_APPS to listOf("Standard app 1 for Firefox", "Standard app 2 for Firefox"), SearchTabType.OPEN_SOURCE to listOf("Open source app 1 for Firefox"), SearchTabType.WEB_APPS to listOf("Web app 1 for Firefox", "Web app 2 for Firefox"), SearchTabType.STANDARD_APPS to listOf( Application( name = "Standard app 1 for Firefox", author = "Author 1", package_name = "com.example.standard1", ratings = Ratings(usageQualityScore = 4.4), source = Source.PLAY_STORE, status = Status.UNAVAILABLE, ), Application( name = "Standard app 2 for Firefox", author = "Author 2", package_name = "com.example.standard2", ratings = Ratings(usageQualityScore = 4.0), source = Source.PLAY_STORE, status = Status.DOWNLOADING, ), ), SearchTabType.OPEN_SOURCE to listOf( Application( name = "Open source app 1 for Firefox", author = "Open author", package_name = "org.example.foss1", source = Source.OPEN_SOURCE, status = Status.INSTALLED, ), ), SearchTabType.WEB_APPS to listOf( Application( name = "Web app 1 for Firefox", author = "Web team", package_name = "org.example.pwa1", source = Source.PWA, status = Status.UPDATABLE, ), Application( name = "Web app 2 for Firefox", author = "Web team", package_name = "org.example.pwa2", source = Source.PWA, status = Status.DOWNLOADING, ), ), ), onTabSelected = {}, ) Loading app/src/main/java/foundation/e/apps/ui/compose/screens/SearchScreen.kt +40 −3 Original line number Diff line number Diff line Loading @@ -32,6 +32,10 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Ratings import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Status import foundation.e.apps.ui.compose.components.SearchEmptyState import foundation.e.apps.ui.compose.components.SearchResultsContent import foundation.e.apps.ui.compose.components.SearchSuggestionsDropdown Loading @@ -49,6 +53,10 @@ fun SearchScreen( onSubmitSearch: (String) -> Unit, onSuggestionSelected: (String) -> Unit, onTabSelected: (SearchTabType) -> Unit, onResultClick: (Application) -> Unit = {}, onPrimaryActionClick: (Application) -> Unit = {}, onShowMoreClick: (Application) -> Unit = {}, onPrivacyClick: (Application) -> Unit = {}, ) { val focusManager = LocalFocusManager.current val focusRequester = remember { FocusRequester() } Loading Loading @@ -84,6 +92,10 @@ fun SearchScreen( selectedTab = state.selectedTab!!, resultsByTab = state.resultsByTab, onTabSelected = onTabSelected, onResultClick = onResultClick, onPrimaryActionClick = onPrimaryActionClick, onShowMoreClick = onShowMoreClick, onPrivacyClick = onPrivacyClick, modifier = Modifier .align(Alignment.TopStart) .fillMaxWidth() Loading Loading @@ -131,9 +143,34 @@ private fun SearchScreenPreview() { ), selectedTab = SearchTabType.STANDARD_APPS, resultsByTab = mapOf( SearchTabType.STANDARD_APPS to listOf("Standard app 1 for Telegram"), SearchTabType.OPEN_SOURCE to listOf("Open source app 1 for Telegram"), SearchTabType.WEB_APPS to listOf("Web app 1 for Telegram"), SearchTabType.STANDARD_APPS to listOf( Application( name = "Standard app 1 for Telegram", author = "Author 1", package_name = "com.example.telegram1", ratings = Ratings(usageQualityScore = 4.3), source = Source.PLAY_STORE, status = Status.UNAVAILABLE, ), ), SearchTabType.OPEN_SOURCE to listOf( Application( name = "Open source app 1 for Telegram", author = "FOSS author", package_name = "org.example.foss.telegram1", source = Source.OPEN_SOURCE, status = Status.INSTALLED, ), ), SearchTabType.WEB_APPS to listOf( Application( name = "Web app 1 for Telegram", author = "PWA author", package_name = "org.example.pwa.telegram1", source = Source.PWA, status = Status.UPDATABLE, ), ), ), hasSubmittedSearch = true, ), Loading app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt +42 −9 Original line number Diff line number Diff line Loading @@ -22,9 +22,13 @@ import android.content.SharedPreferences import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Ratings import foundation.e.apps.data.Constants.PREFERENCE_SHOW_FOSS import foundation.e.apps.data.Constants.PREFERENCE_SHOW_GPLAY import foundation.e.apps.data.Constants.PREFERENCE_SHOW_PWA import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Status import foundation.e.apps.data.preference.AppLoungePreference import kotlinx.coroutines.Job import kotlinx.coroutines.delay Loading @@ -36,7 +40,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject private const val SUGGESTION_DEBOUNCE_MS = 200L private const val FAKE_RESULTS_PER_TAB = 6 private const val FAKE_RESULTS_PER_TAB = 50 enum class SearchTabType { STANDARD_APPS, Loading @@ -50,7 +54,7 @@ data class SearchUiState( val isSuggestionVisible: Boolean = false, val availableTabs: List<SearchTabType> = emptyList(), val selectedTab: SearchTabType? = null, val resultsByTab: Map<SearchTabType, List<String>> = emptyMap(), val resultsByTab: Map<SearchTabType, List<Application>> = emptyMap(), val hasSubmittedSearch: Boolean = false, ) Loading Loading @@ -244,8 +248,8 @@ class SearchViewModelV2 @Inject constructor( private fun buildResultsForTabs( query: String, visibleTabs: List<SearchTabType>, existing: Map<SearchTabType, List<String>>, ): Map<SearchTabType, List<String>> { existing: Map<SearchTabType, List<Application>>, ): Map<SearchTabType, List<Application>> { if (query.isBlank()) return emptyMap() return buildMap { Loading @@ -256,14 +260,37 @@ class SearchViewModelV2 @Inject constructor( } } private fun generateFakeResultsFor(tab: SearchTabType, query: String): List<String> { private fun generateFakeResultsFor(tab: SearchTabType, query: String): List<Application> { val displayQuery = query.ifBlank { "Result" } val source = when (tab) { SearchTabType.STANDARD_APPS -> Source.PLAY_STORE SearchTabType.OPEN_SOURCE -> Source.OPEN_SOURCE SearchTabType.WEB_APPS -> Source.PWA } return (1..FAKE_RESULTS_PER_TAB).map { index -> when (tab) { SearchTabType.STANDARD_APPS -> "Standard app $index for $displayQuery" SearchTabType.OPEN_SOURCE -> "Open source app $index for $displayQuery" SearchTabType.WEB_APPS -> "Web app $index for $displayQuery" val packageName = when (tab) { SearchTabType.STANDARD_APPS -> "com.example.standard.$index" SearchTabType.OPEN_SOURCE -> "org.example.foss.$index" SearchTabType.WEB_APPS -> "org.example.pwa.$index" } Application( _id = "$tab-$index", name = "${tab.toReadable()} $index for $displayQuery", author = "Author $index", package_name = packageName, source = source, ratings = Ratings(usageQualityScore = 4.0 + (index % 3) * 0.1), is_pwa = tab == SearchTabType.WEB_APPS, status = when (index % 4) { 0 -> Status.UNAVAILABLE 1 -> Status.UPDATABLE 2 -> Status.INSTALLED else -> Status.DOWNLOADING }, price = if (index % 5 == 0) "$1.$index" else "", ) } } Loading @@ -275,3 +302,9 @@ class SearchViewModelV2 @Inject constructor( ) } } private fun SearchTabType.toReadable(): String = when (this) { SearchTabType.STANDARD_APPS -> "Standard app" SearchTabType.OPEN_SOURCE -> "Open source app" SearchTabType.WEB_APPS -> "Web app" } Loading
app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultsContent.kt +168 −13 Original line number Diff line number Diff line Loading @@ -25,7 +25,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.Divider Loading @@ -45,6 +45,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import foundation.e.apps.R import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Ratings import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Status import foundation.e.apps.ui.compose.theme.AppTheme import foundation.e.apps.ui.search.v2.SearchTabType import kotlinx.coroutines.launch Loading @@ -53,8 +57,12 @@ import kotlinx.coroutines.launch fun SearchResultsContent( tabs: List<SearchTabType>, selectedTab: SearchTabType, resultsByTab: Map<SearchTabType, List<String>>, resultsByTab: Map<SearchTabType, List<Application>>, onTabSelected: (SearchTabType) -> Unit, onResultClick: (Application) -> Unit = {}, onPrimaryActionClick: (Application) -> Unit = {}, onShowMoreClick: (Application) -> Unit = {}, onPrivacyClick: (Application) -> Unit = {}, modifier: Modifier = Modifier, ) { if (tabs.isEmpty() || selectedTab !in tabs) { Loading Loading @@ -107,6 +115,10 @@ fun SearchResultsContent( val items = resultsByTab[tab].orEmpty() SearchResultList( items = items, onItemClick = onResultClick, onPrimaryActionClick = onPrimaryActionClick, onShowMoreClick = onShowMoreClick, onPrivacyClick = onPrivacyClick, modifier = Modifier.fillMaxSize(), ) } Loading Loading @@ -158,27 +170,130 @@ private fun SearchTabs( @Composable private fun SearchResultList( items: List<String>, items: List<Application>, onItemClick: (Application) -> Unit, onPrimaryActionClick: (Application) -> Unit, onShowMoreClick: (Application) -> Unit, onPrivacyClick: (Application) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn( modifier = modifier, verticalArrangement = Arrangement.spacedBy(12.dp), ) { items( itemsIndexed( items = items, key = { item -> item }, ) { item -> Text( text = item, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onBackground, key = { index, item -> item._id.takeIf { it.isNotBlank() } ?: item.package_name.takeIf { it.isNotBlank() } ?: "${item.name}-$index" }, ) { _, application -> SearchResultListItem( application = application, uiState = application.toSearchResultUiState(), onItemClick = onItemClick, onPrimaryActionClick = onPrimaryActionClick, onShowMoreClick = onShowMoreClick, onPrivacyClick = onPrivacyClick, modifier = Modifier.fillMaxWidth(), ) } } } @Composable private fun Application.toSearchResultUiState(): SearchResultListItemState { if (isPlaceHolder) { return SearchResultListItemState( author = "", ratingText = "", showRating = false, sourceTag = "", showSourceTag = false, privacyScore = "", showPrivacyScore = false, isPrivacyLoading = false, primaryAction = PrimaryActionUiState( label = "", enabled = false, isInProgress = false, isFilledStyle = true, ), iconUrl = null, placeholderResId = null, isPlaceholder = true, ) } val ratingText = when { source == Source.OPEN_SOURCE || source == Source.PWA || isSystemApp -> "" ratings.usageQualityScore >= 0 -> String.format("%.1f", ratings.usageQualityScore) else -> stringResource(id = R.string.not_available) } val sourceTagText = source.toString() return SearchResultListItemState( author = author.ifBlank { package_name }, ratingText = ratingText, showRating = ratingText.isNotBlank(), sourceTag = sourceTagText, showSourceTag = false, privacyScore = "", showPrivacyScore = false, // Privacy scores are disabled on Search per functional spec. isPrivacyLoading = false, primaryAction = resolvePrimaryActionState(this), iconUrl = icon_image_path.takeIf { it.isNotBlank() }, placeholderResId = null, isPlaceholder = false, ) } @Composable private fun resolvePrimaryActionState(application: Application): PrimaryActionUiState { val label = when (application.status) { Status.INSTALLED -> stringResource(id = R.string.open) Status.UPDATABLE -> stringResource(id = R.string.update) Status.INSTALLING -> stringResource(id = R.string.installing) Status.DOWNLOADING, Status.DOWNLOADED, Status.QUEUED, Status.AWAITING -> stringResource(id = R.string.cancel) Status.INSTALLATION_ISSUE -> stringResource(id = R.string.retry) Status.PURCHASE_NEEDED -> application.price.ifBlank { stringResource(id = R.string.install) } Status.BLOCKED -> stringResource(id = R.string.install) Status.UNAVAILABLE -> { if (!application.isFree && !application.isPurchased) { application.price.ifBlank { stringResource(id = R.string.install) } } else { stringResource(id = R.string.install) } } } val isInProgress = when (application.status) { Status.INSTALLING, Status.DOWNLOADING, Status.DOWNLOADED, Status.QUEUED, Status.AWAITING -> true else -> false } val isEnabled = when (application.status) { Status.INSTALLING -> false else -> true } return PrimaryActionUiState( label = label, enabled = isEnabled, isInProgress = isInProgress, isFilledStyle = true, showMore = false, ) } private fun formatPrivacyScore(score: Int): String { if (score < 0) return "" val clamped = score.coerceIn(0, 10) return String.format("%02d/10", clamped) } @StringRes private fun SearchTabType.toLabelRes(): Int = when (this) { SearchTabType.STANDARD_APPS -> R.string.search_tab_standard_apps Loading @@ -198,9 +313,49 @@ private fun SearchResultsContentPreview() { ), selectedTab = SearchTabType.OPEN_SOURCE, resultsByTab = mapOf( SearchTabType.STANDARD_APPS to listOf("Standard app 1 for Firefox", "Standard app 2 for Firefox"), SearchTabType.OPEN_SOURCE to listOf("Open source app 1 for Firefox"), SearchTabType.WEB_APPS to listOf("Web app 1 for Firefox", "Web app 2 for Firefox"), SearchTabType.STANDARD_APPS to listOf( Application( name = "Standard app 1 for Firefox", author = "Author 1", package_name = "com.example.standard1", ratings = Ratings(usageQualityScore = 4.4), source = Source.PLAY_STORE, status = Status.UNAVAILABLE, ), Application( name = "Standard app 2 for Firefox", author = "Author 2", package_name = "com.example.standard2", ratings = Ratings(usageQualityScore = 4.0), source = Source.PLAY_STORE, status = Status.DOWNLOADING, ), ), SearchTabType.OPEN_SOURCE to listOf( Application( name = "Open source app 1 for Firefox", author = "Open author", package_name = "org.example.foss1", source = Source.OPEN_SOURCE, status = Status.INSTALLED, ), ), SearchTabType.WEB_APPS to listOf( Application( name = "Web app 1 for Firefox", author = "Web team", package_name = "org.example.pwa1", source = Source.PWA, status = Status.UPDATABLE, ), Application( name = "Web app 2 for Firefox", author = "Web team", package_name = "org.example.pwa2", source = Source.PWA, status = Status.DOWNLOADING, ), ), ), onTabSelected = {}, ) Loading
app/src/main/java/foundation/e/apps/ui/compose/screens/SearchScreen.kt +40 −3 Original line number Diff line number Diff line Loading @@ -32,6 +32,10 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Ratings import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Status import foundation.e.apps.ui.compose.components.SearchEmptyState import foundation.e.apps.ui.compose.components.SearchResultsContent import foundation.e.apps.ui.compose.components.SearchSuggestionsDropdown Loading @@ -49,6 +53,10 @@ fun SearchScreen( onSubmitSearch: (String) -> Unit, onSuggestionSelected: (String) -> Unit, onTabSelected: (SearchTabType) -> Unit, onResultClick: (Application) -> Unit = {}, onPrimaryActionClick: (Application) -> Unit = {}, onShowMoreClick: (Application) -> Unit = {}, onPrivacyClick: (Application) -> Unit = {}, ) { val focusManager = LocalFocusManager.current val focusRequester = remember { FocusRequester() } Loading Loading @@ -84,6 +92,10 @@ fun SearchScreen( selectedTab = state.selectedTab!!, resultsByTab = state.resultsByTab, onTabSelected = onTabSelected, onResultClick = onResultClick, onPrimaryActionClick = onPrimaryActionClick, onShowMoreClick = onShowMoreClick, onPrivacyClick = onPrivacyClick, modifier = Modifier .align(Alignment.TopStart) .fillMaxWidth() Loading Loading @@ -131,9 +143,34 @@ private fun SearchScreenPreview() { ), selectedTab = SearchTabType.STANDARD_APPS, resultsByTab = mapOf( SearchTabType.STANDARD_APPS to listOf("Standard app 1 for Telegram"), SearchTabType.OPEN_SOURCE to listOf("Open source app 1 for Telegram"), SearchTabType.WEB_APPS to listOf("Web app 1 for Telegram"), SearchTabType.STANDARD_APPS to listOf( Application( name = "Standard app 1 for Telegram", author = "Author 1", package_name = "com.example.telegram1", ratings = Ratings(usageQualityScore = 4.3), source = Source.PLAY_STORE, status = Status.UNAVAILABLE, ), ), SearchTabType.OPEN_SOURCE to listOf( Application( name = "Open source app 1 for Telegram", author = "FOSS author", package_name = "org.example.foss.telegram1", source = Source.OPEN_SOURCE, status = Status.INSTALLED, ), ), SearchTabType.WEB_APPS to listOf( Application( name = "Web app 1 for Telegram", author = "PWA author", package_name = "org.example.pwa.telegram1", source = Source.PWA, status = Status.UPDATABLE, ), ), ), hasSubmittedSearch = true, ), Loading
app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt +42 −9 Original line number Diff line number Diff line Loading @@ -22,9 +22,13 @@ import android.content.SharedPreferences import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Ratings import foundation.e.apps.data.Constants.PREFERENCE_SHOW_FOSS import foundation.e.apps.data.Constants.PREFERENCE_SHOW_GPLAY import foundation.e.apps.data.Constants.PREFERENCE_SHOW_PWA import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Status import foundation.e.apps.data.preference.AppLoungePreference import kotlinx.coroutines.Job import kotlinx.coroutines.delay Loading @@ -36,7 +40,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject private const val SUGGESTION_DEBOUNCE_MS = 200L private const val FAKE_RESULTS_PER_TAB = 6 private const val FAKE_RESULTS_PER_TAB = 50 enum class SearchTabType { STANDARD_APPS, Loading @@ -50,7 +54,7 @@ data class SearchUiState( val isSuggestionVisible: Boolean = false, val availableTabs: List<SearchTabType> = emptyList(), val selectedTab: SearchTabType? = null, val resultsByTab: Map<SearchTabType, List<String>> = emptyMap(), val resultsByTab: Map<SearchTabType, List<Application>> = emptyMap(), val hasSubmittedSearch: Boolean = false, ) Loading Loading @@ -244,8 +248,8 @@ class SearchViewModelV2 @Inject constructor( private fun buildResultsForTabs( query: String, visibleTabs: List<SearchTabType>, existing: Map<SearchTabType, List<String>>, ): Map<SearchTabType, List<String>> { existing: Map<SearchTabType, List<Application>>, ): Map<SearchTabType, List<Application>> { if (query.isBlank()) return emptyMap() return buildMap { Loading @@ -256,14 +260,37 @@ class SearchViewModelV2 @Inject constructor( } } private fun generateFakeResultsFor(tab: SearchTabType, query: String): List<String> { private fun generateFakeResultsFor(tab: SearchTabType, query: String): List<Application> { val displayQuery = query.ifBlank { "Result" } val source = when (tab) { SearchTabType.STANDARD_APPS -> Source.PLAY_STORE SearchTabType.OPEN_SOURCE -> Source.OPEN_SOURCE SearchTabType.WEB_APPS -> Source.PWA } return (1..FAKE_RESULTS_PER_TAB).map { index -> when (tab) { SearchTabType.STANDARD_APPS -> "Standard app $index for $displayQuery" SearchTabType.OPEN_SOURCE -> "Open source app $index for $displayQuery" SearchTabType.WEB_APPS -> "Web app $index for $displayQuery" val packageName = when (tab) { SearchTabType.STANDARD_APPS -> "com.example.standard.$index" SearchTabType.OPEN_SOURCE -> "org.example.foss.$index" SearchTabType.WEB_APPS -> "org.example.pwa.$index" } Application( _id = "$tab-$index", name = "${tab.toReadable()} $index for $displayQuery", author = "Author $index", package_name = packageName, source = source, ratings = Ratings(usageQualityScore = 4.0 + (index % 3) * 0.1), is_pwa = tab == SearchTabType.WEB_APPS, status = when (index % 4) { 0 -> Status.UNAVAILABLE 1 -> Status.UPDATABLE 2 -> Status.INSTALLED else -> Status.DOWNLOADING }, price = if (index % 5 == 0) "$1.$index" else "", ) } } Loading @@ -275,3 +302,9 @@ class SearchViewModelV2 @Inject constructor( ) } } private fun SearchTabType.toReadable(): String = when (this) { SearchTabType.STANDARD_APPS -> "Standard app" SearchTabType.OPEN_SOURCE -> "Open source app" SearchTabType.WEB_APPS -> "Web app" }