diff --git a/app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultListItemTest.kt b/app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultListItemTest.kt index 0a538c4ce12fae9d24f2bcb3ef9f3f7245fec4e8..081ef5f5daeb4cdb04f4bee400f060b4c701b25c 100644 --- a/app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultListItemTest.kt +++ b/app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultListItemTest.kt @@ -54,7 +54,6 @@ class SearchResultListItemTest { onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = {}, - onPrivacyClick = {}, ) } } @@ -70,7 +69,6 @@ class SearchResultListItemTest { fun metadataAndClicks_areWiredCorrectly() { var itemClicks = 0 var primaryClicks = 0 - var privacyClicks = 0 composeRule.setContent { AppTheme(darkTheme = false) { @@ -81,11 +79,6 @@ class SearchResultListItemTest { author = "Signal LLC", ratingText = "4.5", showRating = true, - sourceTag = "Play Store", - showSourceTag = true, - privacyScore = "06/10", - showPrivacyScore = true, - isPrivacyLoading = false, primaryAction = PrimaryActionUiState( label = "Install", enabled = true, @@ -96,7 +89,6 @@ class SearchResultListItemTest { onItemClick = { itemClicks += 1 }, onPrimaryActionClick = { primaryClicks += 1 }, onShowMoreClick = {}, - onPrivacyClick = { privacyClicks += 1 }, ) } } @@ -105,9 +97,7 @@ class SearchResultListItemTest { composeRule.onNodeWithText("Signal").assertIsDisplayed() composeRule.onNodeWithText("Signal LLC").assertIsDisplayed() composeRule.onNodeWithText("4.5").assertIsDisplayed() - composeRule.onNodeWithText("Play Store").assertIsDisplayed() composeRule.onNodeWithText("Install").assertIsDisplayed() - composeRule.onNodeWithText("06/10").assertIsDisplayed() composeRule.onAllNodesWithTag(SearchResultListItemTestTags.SHOW_MORE) .assertCountEquals(0) @@ -115,18 +105,15 @@ class SearchResultListItemTest { .performClick() composeRule.onNodeWithTag(SearchResultListItemTestTags.PRIMARY_BUTTON) .performClick() - composeRule.onNodeWithTag(SearchResultListItemTestTags.PRIVACY_BADGE) - .performClick() composeRule.runOnIdle { assertEquals(1, itemClicks) assertEquals(1, primaryClicks) - assertEquals(1, privacyClicks) } } @Test - fun hidesRatingAndSourceTag_whenDisabled() { + fun hidesRating_whenDisabled() { composeRule.setContent { AppTheme(darkTheme = false) { Surface(color = MaterialTheme.colorScheme.background) { @@ -136,11 +123,6 @@ class SearchResultListItemTest { author = "Anonymous", ratingText = "4.9", showRating = false, - sourceTag = "Play Store", - showSourceTag = false, - privacyScore = "", - showPrivacyScore = false, - isPrivacyLoading = false, primaryAction = PrimaryActionUiState( label = "Install", enabled = true, @@ -151,14 +133,12 @@ class SearchResultListItemTest { onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = {}, - onPrivacyClick = {}, ) } } } composeRule.onAllNodesWithText("4.9").assertCountEquals(0) - composeRule.onAllNodesWithText("Play Store").assertCountEquals(0) } @Test @@ -175,11 +155,6 @@ class SearchResultListItemTest { author = "Author", ratingText = "", showRating = false, - sourceTag = "", - showSourceTag = false, - privacyScore = "", - showPrivacyScore = false, - isPrivacyLoading = false, primaryAction = PrimaryActionUiState( label = "Install", enabled = true, @@ -191,7 +166,6 @@ class SearchResultListItemTest { onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = { showMoreClicks += 1 }, - onPrivacyClick = {}, ) } } @@ -210,7 +184,7 @@ class SearchResultListItemTest { } @Test - fun inProgressPrimaryAction_andPrivacyLoading_showSpinners() { + fun inProgressPrimaryAction_showsSpinner() { composeRule.setContent { AppTheme(darkTheme = false) { Surface(color = MaterialTheme.colorScheme.background) { @@ -220,11 +194,6 @@ class SearchResultListItemTest { author = "Author", ratingText = "", showRating = false, - sourceTag = "", - showSourceTag = false, - privacyScore = "07/10", - showPrivacyScore = true, - isPrivacyLoading = true, primaryAction = PrimaryActionUiState( label = "", enabled = true, @@ -235,7 +204,6 @@ class SearchResultListItemTest { onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = {}, - onPrivacyClick = {}, ) } } @@ -243,9 +211,6 @@ class SearchResultListItemTest { composeRule.onNodeWithTag(SearchResultListItemTestTags.PRIMARY_PROGRESS) .assertIsDisplayed() - composeRule.onNodeWithTag(SearchResultListItemTestTags.PRIVACY_PROGRESS) - .assertIsDisplayed() - composeRule.onAllNodesWithText("07/10").assertCountEquals(0) } private fun sampleApp(name: String) = Application(name = name) @@ -254,11 +219,6 @@ class SearchResultListItemTest { author = "", ratingText = "", showRating = false, - sourceTag = "", - showSourceTag = false, - privacyScore = "", - showPrivacyScore = false, - isPrivacyLoading = false, primaryAction = PrimaryActionUiState( label = "", enabled = false, @@ -274,21 +234,11 @@ class SearchResultListItemTest { author: String, ratingText: String, showRating: Boolean, - sourceTag: String, - showSourceTag: Boolean, - privacyScore: String, - showPrivacyScore: Boolean, - isPrivacyLoading: Boolean, primaryAction: PrimaryActionUiState, ): SearchResultListItemState = SearchResultListItemState( author = author, ratingText = ratingText, showRating = showRating, - sourceTag = sourceTag, - showSourceTag = showSourceTag, - privacyScore = privacyScore, - showPrivacyScore = showPrivacyScore, - isPrivacyLoading = isPrivacyLoading, primaryAction = primaryAction, iconUrl = null, placeholderResId = null, diff --git a/app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultsContentTest.kt b/app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultsContentTest.kt index b673259f9b27d323f9c2d41fd7313035afa03171..5362d403615507854067c2c4127e9de6f99b3521 100644 --- a/app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultsContentTest.kt +++ b/app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultsContentTest.kt @@ -204,51 +204,6 @@ class SearchResultsContentTest { composeRule.onAllNodesWithText("Open App").assertCountEquals(0) } - @Test - fun refreshError_showsRetry() { - val pagingData = PagingData.empty( - sourceLoadStates = loadStates(refresh = LoadState.Error(RuntimeException("boom"))) - ) - - renderSearchResults( - tabs = listOf(SearchTabType.OPEN_SOURCE), - selectedTab = SearchTabType.OPEN_SOURCE, - fossPagingData = pagingData, - ) - - composeRule.onNodeWithText( - composeRule.activity.getString(R.string.search_error) - ).assertIsDisplayed() - composeRule.onNodeWithText( - composeRule.activity.getString(R.string.retry) - ).assertIsDisplayed() - } - - @Test - fun appendError_showsFooterRetryWithResults() { - val pagingData = PagingData.from( - listOf(sampleApp("Loaded App")), - sourceLoadStates = loadStates( - refresh = LoadState.NotLoading(endOfPaginationReached = false), - append = LoadState.Error(RuntimeException("append boom")) - ) - ) - - renderSearchResults( - tabs = listOf(SearchTabType.OPEN_SOURCE), - selectedTab = SearchTabType.OPEN_SOURCE, - fossPagingData = pagingData, - ) - - composeRule.onNodeWithText("Loaded App").assertIsDisplayed() - composeRule.onNodeWithText( - composeRule.activity.getString(R.string.search_error) - ).assertIsDisplayed() - composeRule.onNodeWithText( - composeRule.activity.getString(R.string.retry) - ).assertIsDisplayed() - } - @Test fun emptyResults_showsPlaceholder() { val pagingData = PagingData.empty( @@ -256,7 +211,7 @@ class SearchResultsContentTest { refresh = LoadState.NotLoading(endOfPaginationReached = true) ) ) - val noAppsText = composeRule.activity.getString(R.string.no_apps_found) + val noAppsText = composeRule.activity.getString(R.string.search_empty_results_body) renderSearchResults( tabs = listOf(SearchTabType.OPEN_SOURCE), @@ -269,7 +224,7 @@ class SearchResultsContentTest { @Test fun emptyResults_resetOnNewQuery_showsRefreshLoading() { - val noAppsText = composeRule.activity.getString(R.string.no_apps_found) + val noAppsText = composeRule.activity.getString(R.string.search_empty_results_body) val emptyPagingData = PagingData.empty( sourceLoadStates = loadStates( refresh = LoadState.NotLoading(endOfPaginationReached = true) diff --git a/app/src/androidTest/java/foundation/e/apps/ui/compose/components/search/SearchErrorStateTest.kt b/app/src/androidTest/java/foundation/e/apps/ui/compose/components/search/SearchErrorStateTest.kt index 8662ae8b33f969bb2d7e606ef122c90fb6461450..6ace263ca2ec6bea744b03487106f958d6b86aec 100644 --- a/app/src/androidTest/java/foundation/e/apps/ui/compose/components/search/SearchErrorStateTest.kt +++ b/app/src/androidTest/java/foundation/e/apps/ui/compose/components/search/SearchErrorStateTest.kt @@ -41,16 +41,13 @@ class SearchErrorStateTest { composeRule.setContent { AppTheme(darkTheme = false) { Surface(color = MaterialTheme.colorScheme.background) { - SearchErrorState(onRetry = {}, fullScreen = true) + SearchErrorState(errorTitleStringRes = R.string.search_error_title_opensource, fullScreen = true) } } } composeRule.onNodeWithText( - composeRule.activity.getString(R.string.search_error) - ).assertIsDisplayed() - composeRule.onNodeWithText( - composeRule.activity.getString(R.string.retry) + composeRule.activity.getString(R.string.search_error_message) ).assertIsDisplayed() } @@ -59,16 +56,13 @@ class SearchErrorStateTest { composeRule.setContent { AppTheme(darkTheme = false) { Surface(color = MaterialTheme.colorScheme.background) { - SearchErrorState(onRetry = {}, fullScreen = false) + SearchErrorState(errorTitleStringRes = R.string.search_error_title_opensource, fullScreen = false) } } } composeRule.onNodeWithText( - composeRule.activity.getString(R.string.search_error) - ).assertIsDisplayed() - composeRule.onNodeWithText( - composeRule.activity.getString(R.string.retry) + composeRule.activity.getString(R.string.search_error_message) ).assertIsDisplayed() } } diff --git a/app/src/androidTest/java/foundation/e/apps/ui/compose/screens/SearchTopBarTest.kt b/app/src/androidTest/java/foundation/e/apps/ui/compose/screens/SearchTopBarTest.kt index d6bc044b4695e1444ba6354fddbe19bbacf823cd..d1a35e2f2a61e8bbd05385447bbca05cac7949d1 100644 --- a/app/src/androidTest/java/foundation/e/apps/ui/compose/screens/SearchTopBarTest.kt +++ b/app/src/androidTest/java/foundation/e/apps/ui/compose/screens/SearchTopBarTest.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import foundation.e.apps.R import foundation.e.apps.ui.search.v2.SearchUiState +import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test @@ -53,16 +54,13 @@ class SearchTopBarTest { val composeRule = createAndroidComposeRule() @Test - fun typingQuery_expandsSearchBar_andUpdatesQueryState() { + fun typingQuery_updatesQueryState() { val recorder = SearchTopBarRecorder() val hintText = composeRule.activity.getString(R.string.search_hint) composeRule.setContent { SearchTopBarTestContent( initialQuery = "", - suggestions = emptyList(), - initialExpanded = false, - showSuggestions = false, recorder = recorder, ) } @@ -78,21 +76,17 @@ class SearchTopBarTest { .performTextInput("camera") composeRule.runOnIdle { - assertTrue(recorder.expandedChanges.contains(true)) - assertTrue(recorder.queryChanges.contains("camera")) + assertEquals("camera", recorder.queryChanges.lastOrNull()) } } @Test - fun submitQuery_collapsesSearchBar_andClearsFocus() { + fun submitQuery_clearsFocus_andCallsSubmit() { val recorder = SearchTopBarRecorder() composeRule.setContent { SearchTopBarTestContent( initialQuery = "vpn", - suggestions = emptyList(), - initialExpanded = true, - showSuggestions = false, recorder = recorder, ) } @@ -103,7 +97,6 @@ class SearchTopBarTest { inputField.performImeAction() composeRule.runOnIdle { - assertTrue(recorder.expandedChanges.contains(false)) assertTrue(recorder.searchSubmissions.contains("vpn")) } @@ -112,15 +105,12 @@ class SearchTopBarTest { } @Test - fun clearButton_clearsQuery_keepsExpanded_andFocusesInput() { + fun clearButton_clearsQuery_andFocusesInput() { val recorder = SearchTopBarRecorder() composeRule.setContent { SearchTopBarTestContent( initialQuery = "maps", - suggestions = emptyList(), - initialExpanded = true, - showSuggestions = false, recorder = recorder, ) } @@ -134,7 +124,6 @@ class SearchTopBarTest { composeRule.runOnIdle { assertTrue(recorder.clearTapped) - assertTrue(recorder.expandedChanges.contains(true)) } composeRule.onNodeWithTag(SearchTopBarTestTags.INPUT_FIELD) @@ -148,9 +137,6 @@ class SearchTopBarTest { composeRule.setContent { SearchTopBarTestContent( initialQuery = "news", - suggestions = emptyList(), - initialExpanded = true, - showSuggestions = false, recorder = recorder, ) } @@ -169,75 +155,11 @@ class SearchTopBarTest { composeRule.onNodeWithTag(SearchTopBarTestTags.INPUT_FIELD) .assertIsNotFocused() } - - @Test - fun suggestions_callbackFires_whenSuggestionSelected() { - // Material3 SearchBar renders dropdown in a popup window inaccessible to standard - // Compose test APIs. This test verifies the callback logic is wired correctly - // by simulating a suggestion selection through the callback. - val recorder = SearchTopBarRecorder() - val suggestions = listOf("camera", "camera apps", "camera pro") - var simulateSuggestionClick: ((String) -> Unit)? = null - - composeRule.setContent { - var query by remember { mutableStateOf("cam") } - var expanded by remember { mutableStateOf(true) } - val focusRequester = remember { FocusRequester() } - val focusManager = LocalFocusManager.current - - // Capture the suggestion select callback for manual invocation - simulateSuggestionClick = { suggestion -> - expanded = false - focusManager.clearFocus() - recorder.suggestionSelections.add(suggestion) - recorder.expandedChanges.add(false) - } - - SearchTopBar( - uiState = SearchUiState( - query = query, - suggestions = suggestions, - ), - expanded = expanded, - showSuggestions = expanded, - focusRequester = focusRequester, - focusManager = focusManager, - onQueryChange = { query = it }, - onClearQuery = { query = "" }, - onSearchSubmit = {}, - onSuggestionSelect = { suggestion -> - expanded = false - focusManager.clearFocus() - recorder.suggestionSelections.add(suggestion) - recorder.expandedChanges.add(false) - }, - onExpandedChange = { expanded = it }, - onBack = {}, - modifier = Modifier.fillMaxSize(), - ) - } - - // Verify the SearchBar is displayed and expanded - composeRule.onNodeWithTag(SearchTopBarTestTags.SEARCH_BAR) - .assertIsDisplayed() - - // Simulate suggestion selection via callback - composeRule.runOnIdle { - simulateSuggestionClick?.invoke("camera apps") - } - - composeRule.runOnIdle { - assertTrue(recorder.expandedChanges.contains(false)) - assertTrue(recorder.suggestionSelections.contains("camera apps")) - } - } } private class SearchTopBarRecorder { val queryChanges = mutableListOf() val searchSubmissions = mutableListOf() - val suggestionSelections = mutableListOf() - val expandedChanges = mutableListOf() var clearTapped = false var backTapped = false } @@ -245,23 +167,16 @@ private class SearchTopBarRecorder { @Composable private fun SearchTopBarTestContent( initialQuery: String, - suggestions: List, - initialExpanded: Boolean, - showSuggestions: Boolean, recorder: SearchTopBarRecorder, ) { var query by remember { mutableStateOf(initialQuery) } - var expanded by remember { mutableStateOf(initialExpanded) } val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current SearchTopBar( uiState = SearchUiState( query = query, - suggestions = suggestions, ), - expanded = expanded, - showSuggestions = showSuggestions && expanded, focusRequester = focusRequester, focusManager = focusManager, onQueryChange = { updatedQuery -> @@ -275,13 +190,6 @@ private fun SearchTopBarTestContent( onSearchSubmit = { submittedQuery -> recorder.searchSubmissions.add(submittedQuery) }, - onSuggestionSelect = { suggestion -> - recorder.suggestionSelections.add(suggestion) - }, - onExpandedChange = { isExpanded -> - expanded = isExpanded - recorder.expandedChanges.add(isExpanded) - }, onBack = { recorder.backTapped = true }, diff --git a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt index f551b6af90dcf7d7483c0a987ae8cdf028b52afa..2b2877f51f10dba5597ffa7553db9d08bcab956d 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt @@ -20,7 +20,6 @@ package foundation.e.apps.ui.application import android.annotation.SuppressLint import android.content.Intent -import android.graphics.Color import android.graphics.drawable.Drawable import android.os.Bundle import android.text.Html @@ -85,6 +84,7 @@ import kotlinx.coroutines.withContext import timber.log.Timber import java.util.Locale import javax.inject.Inject +import foundation.e.elib.R as eR @AndroidEntryPoint class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { @@ -868,8 +868,8 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private fun MaterialButton.disableInstallButton(buttonStringID: Int) { isEnabled = false text = context.getString(buttonStringID) - strokeColor = ContextCompat.getColorStateList(context, R.color.light_grey) - setTextColor(context.getColor(R.color.light_grey)) + strokeColor = ContextCompat.getColorStateList(context, eR.color.e_disabled_color) + setTextColor(context.getColor(eR.color.e_disabled_color)) backgroundTintList = ContextCompat.getColorStateList(context, android.R.color.transparent) } @@ -877,8 +877,8 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private fun MaterialButton.enableInstallButton(buttonStringID: Int) { isEnabled = true text = context.getString(buttonStringID) - strokeColor = ContextCompat.getColorStateList(context, R.color.colorAccent) - setTextColor(context.getColor(R.color.colorAccent)) + strokeColor = ContextCompat.getColorStateList(context, eR.color.e_accent) + setTextColor(context.getColor(eR.color.e_accent)) backgroundTintList = ContextCompat.getColorStateList(context, android.R.color.transparent) } @@ -914,9 +914,9 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } else { getString(R.string.update) } - setTextColor(Color.WHITE) + setTextColor(ContextCompat.getColor(view.context, eR.color.e_background)) backgroundTintList = - ContextCompat.getColorStateList(view.context, R.color.colorAccent) + ContextCompat.getColorStateList(view.context, eR.color.e_accent) setOnClickListener { if (mainActivityViewModel.checkUnsupportedApplication(application, activity)) { return@setOnClickListener @@ -941,9 +941,9 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { appSize.visibility = View.VISIBLE installButton.apply { enableInstallButton(R.string.open) - setTextColor(Color.WHITE) + setTextColor(ContextCompat.getColor(view.context, eR.color.e_background)) backgroundTintList = - ContextCompat.getColorStateList(view.context, R.color.colorAccent) + ContextCompat.getColorStateList(view.context, eR.color.e_accent) setOnClickListener { if (application.is_pwa) { pwaManager.launchPwa(application) diff --git a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt index 812071cd0234d2b05c280d34e25453f6dc7fdb92..4d02f72e86b28d8abacb060a725b534793f62171 100644 --- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt @@ -58,6 +58,7 @@ import foundation.e.apps.utils.disableInstallButton import foundation.e.apps.utils.enableInstallButton import timber.log.Timber import javax.inject.Singleton +import foundation.e.elib.R as eR @Singleton class ApplicationListRVAdapter( private val applicationInstaller: ApplicationInstaller, @@ -433,7 +434,7 @@ class ApplicationListRVAdapter( materialButton.enableInstallButton() materialButton.text = materialButton.context.getString(R.string.install) materialButton.strokeColor = - ContextCompat.getColorStateList(holder.itemView.context, R.color.light_grey) + ContextCompat.getColorStateList(holder.itemView.context, eR.color.e_accent) applicationListItemBinding.progressBarInstall.visibility = View.GONE } else -> { diff --git a/app/src/main/java/foundation/e/apps/ui/compose/components/SearchInitialState.kt b/app/src/main/java/foundation/e/apps/ui/compose/components/SearchInitialState.kt index be1e7a5361bcf53f02e3c8ec7ed2311425117ff4..f79484b6d7029cea2f773327a167f9950d42f686 100644 --- a/app/src/main/java/foundation/e/apps/ui/compose/components/SearchInitialState.kt +++ b/app/src/main/java/foundation/e/apps/ui/compose/components/SearchInitialState.kt @@ -32,9 +32,11 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource 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.ui.compose.theme.AppTheme @@ -46,20 +48,22 @@ fun SearchInitialState(modifier: Modifier = Modifier) { ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), ) { Icon( imageVector = Icons.Outlined.Search, contentDescription = null, - tint = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.45f), + tint = colorResource(R.color.e_disabled_color), modifier = Modifier .padding(bottom = 4.dp) .size(72.dp), ) Text( text = stringResource(id = R.string.search_hint), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.72f), + style = MaterialTheme.typography.bodyLarge.copy( + fontSize = 18.sp + ), + color = colorResource(R.color.e_primary_text_color), ) } } diff --git a/app/src/main/java/foundation/e/apps/ui/compose/components/SearchPlaceholder.kt b/app/src/main/java/foundation/e/apps/ui/compose/components/SearchPlaceholder.kt index 60c415be545545916342b80fa9cae4f775f3bfe4..637406723da127df08f95924b859985220666226 100644 --- a/app/src/main/java/foundation/e/apps/ui/compose/components/SearchPlaceholder.kt +++ b/app/src/main/java/foundation/e/apps/ui/compose/components/SearchPlaceholder.kt @@ -18,22 +18,20 @@ package foundation.e.apps.ui.compose.components -import androidx.compose.foundation.Image +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.padding -import androidx.compose.foundation.layout.size import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign.Companion.Center import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -41,7 +39,7 @@ import foundation.e.apps.R import foundation.e.apps.ui.compose.theme.AppTheme @Composable -fun SearchPlaceholder(modifier: Modifier = Modifier) { +fun SearchPlaceholder(@StringRes stringResource: Int, modifier: Modifier = Modifier) { Box( modifier = modifier .fillMaxSize(), @@ -51,21 +49,23 @@ fun SearchPlaceholder(modifier: Modifier = Modifier) { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp), ) { - Image( - painter = painterResource(id = R.drawable.ic_error_circular), - contentDescription = stringResource(id = R.string.menu_search), - contentScale = ContentScale.Fit, - modifier = Modifier - .padding(bottom = 4.dp) - .size(96.dp), - ) Text( - text = stringResource(id = R.string.no_apps_found), + text = stringResource(id = stringResource), style = MaterialTheme.typography.bodyMedium.copy( fontSize = 18.sp ), - color = colorResource(id = R.color.light_grey), - textAlign = androidx.compose.ui.text.style.TextAlign.Center, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.87f), + textAlign = Center, + ) + Text( + text = stringResource(R.string.search_empty_results_body), + modifier = Modifier.padding(horizontal = 32.dp), + textAlign = Center, + style = MaterialTheme.typography.bodyMedium.copy( + fontSize = 14.sp, + fontWeight = FontWeight.Normal + ), + color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.38f) ) } } @@ -75,6 +75,6 @@ fun SearchPlaceholder(modifier: Modifier = Modifier) { @Composable private fun SearchPlaceholderPreview() { AppTheme(darkTheme = true) { - SearchPlaceholder() + SearchPlaceholder(stringResource = R.string.search_empty_results_title_playstore) } } diff --git a/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultListItem.kt b/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultListItem.kt index c764aadcf9073b6f1fbcf468984a8f47b1a25842..618ccc8788db5800ac5853981af68585da8ddcb4 100644 --- a/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultListItem.kt +++ b/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultListItem.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -33,6 +34,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -47,17 +49,20 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import coil.compose.rememberImagePainter import foundation.e.apps.R import foundation.e.apps.data.application.data.Application import foundation.e.apps.ui.compose.state.InstallButtonAction import foundation.e.apps.ui.compose.theme.AppTheme +import foundation.e.elib.R as eR @Composable fun SearchResultListItem( @@ -66,7 +71,6 @@ fun SearchResultListItem( onItemClick: (Application) -> Unit, onPrimaryActionClick: (Application) -> Unit, onShowMoreClick: (Application) -> Unit, - onPrivacyClick: (Application) -> Unit, modifier: Modifier = Modifier, ) { if (uiState.isPlaceholder) { @@ -117,23 +121,8 @@ fun SearchResultListItem( overflow = TextOverflow.Ellipsis, ) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { - if (uiState.showRating) { - RatingChip(ratingText = uiState.ratingText) - } else { - // keep layout predictable; hide rating when absent - Spacer(modifier = Modifier.width(0.dp)) - } - - if (uiState.showSourceTag) { - SourceTag(text = uiState.sourceTag) - } else { - // design PNG omits source tag; kept togglable for legacy parity - Spacer(modifier = Modifier.width(0.dp)) - } + if (uiState.showRating) { + RatingChip(ratingText = uiState.ratingText) } } @@ -141,10 +130,6 @@ fun SearchResultListItem( uiState = uiState.primaryAction, onPrimaryClick = { onPrimaryActionClick(application) }, onShowMoreClick = { onShowMoreClick(application) }, - privacyScore = uiState.privacyScore, - showPrivacyScore = uiState.showPrivacyScore, - isPrivacyLoading = uiState.isPrivacyLoading, - onPrivacyClick = { onPrivacyClick(application) }, ) } } @@ -191,74 +176,11 @@ private fun RatingChip(ratingText: String) { } } -@Composable -private fun SourceTag(text: String) { - Text( - text = text, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSecondaryContainer, - modifier = Modifier - .background( - color = MaterialTheme.colorScheme.secondaryContainer, - shape = MaterialTheme.shapes.small, - ) - .padding(horizontal = 8.dp, vertical = 4.dp), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) -} - -@Composable -private fun PrivacyBadge( - privacyScore: String, - isVisible: Boolean, - isLoading: Boolean, - onClick: () -> Unit, -) { - if (!isVisible) { - return - } else { - // proceed to render the badge - } - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .testTag(SearchResultListItemTestTags.PRIVACY_BADGE) - .clickable(onClick = onClick), - ) { - Image( - painter = painterResource(id = R.drawable.ic_lock), - contentDescription = stringResource(id = R.string.privacy_score), - modifier = Modifier.size(16.dp), - ) - Spacer(modifier = Modifier.width(4.dp)) - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier - .size(16.dp) - .testTag(SearchResultListItemTestTags.PRIVACY_PROGRESS), - strokeWidth = 2.dp, - ) - } else { - Text( - text = privacyScore, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onBackground, - ) - } - } -} - @Composable private fun PrimaryActionArea( uiState: PrimaryActionUiState, onPrimaryClick: () -> Unit, onShowMoreClick: () -> Unit, - privacyScore: String, - showPrivacyScore: Boolean, - isPrivacyLoading: Boolean, - onPrivacyClick: () -> Unit, ) { if (uiState.showMore) { Text( @@ -270,40 +192,44 @@ private fun PrimaryActionArea( .clickable(onClick = onShowMoreClick), ) return - } else { - // render the primary action button } - val accentColor = MaterialTheme.colorScheme.tertiary - - val labelTextColor = when { - uiState.isFilledStyle -> MaterialTheme.colorScheme.onPrimary - else -> accentColor - } + Column( + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.Center, + ) { + val accentColor = MaterialTheme.colorScheme.tertiary + val labelTextColor = when { + uiState.isFilledStyle -> colorResource(eR.color.e_background) + uiState.isDisabledStyle -> colorResource(eR.color.e_disabled_color) + else -> accentColor + } - val buttonContent: @Composable () -> Unit = { - val showSpinner = uiState.isInProgress && uiState.label.isBlank() - if (showSpinner) { - CircularProgressIndicator( - modifier = Modifier - .size(16.dp) - .testTag(SearchResultListItemTestTags.PRIMARY_PROGRESS), - strokeWidth = 2.dp, - color = labelTextColor, - ) - } else { - Text( - text = uiState.label, - maxLines = 1, - overflow = TextOverflow.Clip, - color = labelTextColor, - ) + val buttonContent: @Composable () -> Unit = { + val showSpinner = uiState.isInProgress && uiState.label.isBlank() + if (showSpinner) { + CircularProgressIndicator( + modifier = Modifier + .size(16.dp) + .testTag(SearchResultListItemTestTags.PRIMARY_PROGRESS), + strokeWidth = 2.dp, + color = labelTextColor, + ) + } else { + Text( + text = uiState.label, + modifier = Modifier.padding(horizontal = 4.dp), + maxLines = 1, + overflow = TextOverflow.Clip, + color = labelTextColor, + letterSpacing = 1.5.sp + ) + } } - } - Column(horizontalAlignment = Alignment.End) { val borderColor = when { uiState.isFilledStyle -> Color.Transparent + uiState.isDisabledStyle -> colorResource(eR.color.e_disabled_color) uiState.enabled -> accentColor else -> accentColor.copy(alpha = 0.38f) } @@ -311,7 +237,8 @@ private fun PrimaryActionArea( onClick = onPrimaryClick, enabled = uiState.enabled, modifier = Modifier - .height(40.dp) + .widthIn(min = 88.dp) + .height(30.dp) .testTag(SearchResultListItemTestTags.PRIMARY_BUTTON), shape = RoundedCornerShape(4.dp), colors = ButtonDefaults.buttonColors( @@ -324,23 +251,12 @@ private fun PrimaryActionArea( uiState.isFilledStyle -> accentColor.copy(alpha = 0.12f) else -> Color.Transparent }, - disabledContentColor = labelTextColor.copy(alpha = 0.38f), ), border = BorderStroke(1.dp, borderColor), - contentPadding = ButtonDefaults.ContentPadding, + contentPadding = PaddingValues.Zero, ) { buttonContent() } - - if (showPrivacyScore) { - Spacer(modifier = Modifier.height(8.dp)) - PrivacyBadge( - privacyScore = privacyScore, - isVisible = true, - isLoading = isPrivacyLoading, - onClick = onPrivacyClick, - ) - } } } @@ -353,7 +269,7 @@ private fun PlaceholderRow(modifier: Modifier = Modifier) { .testTag(SearchResultListItemTestTags.PLACEHOLDER), contentAlignment = Alignment.Center, ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.colorScheme.tertiary) } } @@ -361,11 +277,6 @@ data class SearchResultListItemState( val author: String, val ratingText: String, val showRating: Boolean, - val sourceTag: String, - val showSourceTag: Boolean, - val privacyScore: String, - val showPrivacyScore: Boolean, - val isPrivacyLoading: Boolean, val primaryAction: PrimaryActionUiState, val iconUrl: String? = null, val placeholderResId: Int?, @@ -377,8 +288,10 @@ data class PrimaryActionUiState( val enabled: Boolean, val isInProgress: Boolean, val isFilledStyle: Boolean, + val isDisabledStyle: Boolean = false, val showMore: Boolean = false, val actionIntent: InstallButtonAction = InstallButtonAction.NoOp, + val progressFraction: Float = 0f, ) internal object SearchResultListItemTestTags { @@ -387,8 +300,6 @@ internal object SearchResultListItemTestTags { const val SHOW_MORE = "search_result_item_show_more" const val PRIMARY_BUTTON = "search_result_item_primary_button" const val PRIMARY_PROGRESS = "search_result_item_primary_progress" - const val PRIVACY_BADGE = "search_result_item_privacy_badge" - const val PRIVACY_PROGRESS = "search_result_item_privacy_progress" } // --- Previews --- @@ -401,8 +312,7 @@ private fun SearchResultListItemPreviewInstall() { SearchResultListItem( application = sampleApp(name = "iMe: AI Messenger"), uiState = sampleState( - rating = "4.4", - privacy = "06/10", + rating = "4.3", primary = PrimaryActionUiState( label = "Install", enabled = true, @@ -414,7 +324,6 @@ private fun SearchResultListItemPreviewInstall() { onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = {}, - onPrivacyClick = {}, ) } } @@ -429,9 +338,8 @@ private fun SearchResultListItemPreviewOpen() { application = sampleApp(name = "This is a very long app name"), uiState = sampleState( rating = "4.3", - privacy = "10/10", primary = PrimaryActionUiState( - label = "Open", + label = "ThisIsALongButtonName", enabled = true, isInProgress = false, isFilledStyle = true, @@ -441,7 +349,6 @@ private fun SearchResultListItemPreviewOpen() { onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = {}, - onPrivacyClick = {}, ) } } @@ -452,18 +359,12 @@ private fun sampleApp(name: String) = Application(name = name) @Composable private fun sampleState( rating: String, - privacy: String, primary: PrimaryActionUiState, ): SearchResultListItemState = SearchResultListItemState( author = "This is a very long author name which can take multiple lines", ratingText = rating, showRating = true, - sourceTag = "Open-source", // PNG omits this; kept for legacy data - showSourceTag = true, - privacyScore = privacy, - showPrivacyScore = true, - isPrivacyLoading = false, primaryAction = primary, isPlaceholder = false, iconUrl = null, diff --git a/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultsContent.kt b/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultsContent.kt index 154b4af3b0c70fc755b3e242900044d0eaca8de4..0c7b428b772975ff9c33091f37878ff4e6770c96 100644 --- a/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultsContent.kt +++ b/app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultsContent.kt @@ -30,6 +30,7 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -76,7 +77,6 @@ fun SearchResultsContent( onResultClick: (Application) -> Unit = {}, onPrimaryActionClick: (Application, InstallButtonAction) -> Unit = { _, _ -> }, onShowMoreClick: (Application) -> Unit = {}, - onPrivacyClick: (Application) -> Unit = {}, installButtonStateProvider: (Application) -> InstallButtonState, ) { when { @@ -97,7 +97,6 @@ fun SearchResultsContent( onResultClick = onResultClick, onPrimaryActionClick = onPrimaryActionClick, onShowMoreClick = onShowMoreClick, - onPrivacyClick = onPrivacyClick, installButtonStateProvider = installButtonStateProvider, modifier = modifier.fillMaxSize(), ) @@ -161,7 +160,6 @@ fun SearchResultsContent( onResultClick = onResultClick, onPrimaryActionClick = onPrimaryActionClick, onShowMoreClick = onShowMoreClick, - onPrivacyClick = onPrivacyClick, installButtonStateProvider = installButtonStateProvider, modifier = Modifier.fillMaxSize(), ) @@ -183,30 +181,37 @@ private fun SearchTabPage( onResultClick: (Application) -> Unit, onPrimaryActionClick: (Application, InstallButtonAction) -> Unit, onShowMoreClick: (Application) -> Unit, - onPrivacyClick: (Application) -> Unit, installButtonStateProvider: (Application) -> InstallButtonState, modifier: Modifier = Modifier, ) { + val items: LazyPagingItems? + val emptyResultsStringResource: Int + val errorTitleStringResource: Int + when (tab) { + SearchTabType.COMMON_APPS -> { + items = playStoreItems + emptyResultsStringResource = R.string.search_empty_results_title_playstore + errorTitleStringResource = R.string.search_error_title_playstore + } + SearchTabType.OPEN_SOURCE -> { - PagingSearchResultList( - items = fossItems, - searchVersion = searchVersion, - tab = tab, - getScrollPosition = getScrollPosition, - onScrollPositionChange = onScrollPositionChange, - onItemClick = onResultClick, - onPrimaryActionClick = onPrimaryActionClick, - onShowMoreClick = onShowMoreClick, - onPrivacyClick = onPrivacyClick, - installButtonStateProvider = installButtonStateProvider, - modifier = modifier, - ) + items = fossItems + emptyResultsStringResource = R.string.search_empty_results_title_open_source + errorTitleStringResource = R.string.search_error_title_opensource } SearchTabType.PWA -> { + items = pwaItems + emptyResultsStringResource = R.string.search_empty_results_title_pwa + errorTitleStringResource = R.string.search_error_title_pwa + } + } + + when (tab) { + SearchTabType.OPEN_SOURCE, SearchTabType.PWA -> { PagingSearchResultList( - items = pwaItems, + items = items, searchVersion = searchVersion, tab = tab, getScrollPosition = getScrollPosition, @@ -214,23 +219,25 @@ private fun SearchTabPage( onItemClick = onResultClick, onPrimaryActionClick = onPrimaryActionClick, onShowMoreClick = onShowMoreClick, - onPrivacyClick = onPrivacyClick, installButtonStateProvider = installButtonStateProvider, + emptyResultsStringResource = emptyResultsStringResource, + errorTitleStringResource = errorTitleStringResource, modifier = modifier, ) } SearchTabType.COMMON_APPS -> { PagingPlayStoreResultList( - items = playStoreItems, + items = items, searchVersion = searchVersion, getScrollPosition = getScrollPosition, onScrollPositionChange = onScrollPositionChange, onItemClick = onResultClick, onPrimaryActionClick = onPrimaryActionClick, onShowMoreClick = onShowMoreClick, - onPrivacyClick = onPrivacyClick, installButtonStateProvider = installButtonStateProvider, + emptyResultsStringResource = emptyResultsStringResource, + errorTitleStringResource = errorTitleStringResource, modifier = modifier, ) } @@ -246,8 +253,9 @@ private fun PagingPlayStoreResultList( onItemClick: (Application) -> Unit, onPrimaryActionClick: (Application, InstallButtonAction) -> Unit, onShowMoreClick: (Application) -> Unit, - onPrivacyClick: (Application) -> Unit, installButtonStateProvider: (Application) -> InstallButtonState, + emptyResultsStringResource: Int, + errorTitleStringResource: Int, modifier: Modifier = Modifier, ) { val lazyItems = items ?: return @@ -295,11 +303,7 @@ private fun PagingPlayStoreResultList( } val initialLoadError = refreshError != null && !hasLoadedCurrentQuery.value - val showFooterError = hasLoadedCurrentQuery.value && listOf( - refreshError, - appendError, - prependError - ).any { it != null } + val isEmpty = !isRefreshing && refreshError == null && appendError == null && prependError == null && lazyItems.itemCount == 0 @@ -311,7 +315,7 @@ private fun PagingPlayStoreResultList( initialLoadError -> { SearchErrorState( - onRetry = { lazyItems.retry() }, + errorTitleStringRes = errorTitleStringResource, modifier = Modifier.fillMaxSize(), fullScreen = true, ) @@ -319,6 +323,7 @@ private fun PagingPlayStoreResultList( isEmpty -> { SearchPlaceholder( + stringResource = emptyResultsStringResource, modifier = Modifier .fillMaxWidth() .align(Alignment.Center) @@ -355,7 +360,6 @@ private fun PagingPlayStoreResultList( ) }, onShowMoreClick = onShowMoreClick, - onPrivacyClick = onPrivacyClick, modifier = Modifier.fillMaxWidth(), ) } else { @@ -371,22 +375,13 @@ private fun PagingPlayStoreResultList( .padding(vertical = 16.dp), contentAlignment = Alignment.Center ) { - CircularProgressIndicator(modifier = Modifier.size(24.dp)) + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = MaterialTheme.colorScheme.tertiary, + ) } } } - - if (showFooterError) { - item(key = "error_footer_play_store") { - SearchErrorState( - onRetry = { lazyItems.retry() }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - fullScreen = false, - ) - } - } } } } @@ -403,8 +398,9 @@ private fun PagingSearchResultList( onItemClick: (Application) -> Unit, onPrimaryActionClick: (Application, InstallButtonAction) -> Unit, onShowMoreClick: (Application) -> Unit, - onPrivacyClick: (Application) -> Unit, installButtonStateProvider: (Application) -> InstallButtonState, + emptyResultsStringResource: Int, + errorTitleStringResource: Int, modifier: Modifier = Modifier, ) { val lazyItems = items ?: return @@ -448,11 +444,7 @@ private fun PagingSearchResultList( } val initialLoadError = refreshError != null && !hasLoadedCurrentQuery.value - val showFooterError = hasLoadedCurrentQuery.value && listOf( - refreshError, - appendError, - prependError - ).any { it != null } + val isEmpty = !isRefreshing && refreshError == null && appendError == null && prependError == null && lazyItems.itemCount == 0 @@ -466,7 +458,7 @@ private fun PagingSearchResultList( initialLoadError -> { SearchErrorState( - onRetry = { lazyItems.retry() }, + errorTitleStringRes = errorTitleStringResource, modifier = Modifier.fillMaxSize(), fullScreen = true, ) @@ -474,6 +466,7 @@ private fun PagingSearchResultList( isEmpty -> { SearchPlaceholder( + stringResource = emptyResultsStringResource, modifier = Modifier .fillMaxWidth() .align(Alignment.Center) @@ -511,7 +504,6 @@ private fun PagingSearchResultList( ) }, onShowMoreClick = onShowMoreClick, - onPrivacyClick = onPrivacyClick, modifier = Modifier.fillMaxWidth(), ) } else { @@ -530,23 +522,12 @@ private fun PagingSearchResultList( CircularProgressIndicator( modifier = Modifier .size(24.dp) - .testTag(SearchResultsContentTestTags.APPEND_LOADER) + .testTag(SearchResultsContentTestTags.APPEND_LOADER), + color = MaterialTheme.colorScheme.tertiary, ) } } } - - if (showFooterError) { - item(key = "error_footer") { - SearchErrorState( - onRetry = { lazyItems.retry() }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - fullScreen = false, - ) - } - } } } } @@ -560,11 +541,6 @@ private fun Application.toSearchResultUiState(buttonState: InstallButtonState): author = "", ratingText = "", showRating = false, - sourceTag = "", - showSourceTag = false, - privacyScore = "", - showPrivacyScore = false, - isPrivacyLoading = false, primaryAction = PrimaryActionUiState( label = "", enabled = false, @@ -588,17 +564,10 @@ private fun Application.toSearchResultUiState(buttonState: InstallButtonState): 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 = PrimaryActionUiState( label = buttonState.label.text ?: buttonState.progressPercentText @@ -607,8 +576,10 @@ private fun Application.toSearchResultUiState(buttonState: InstallButtonState): enabled = buttonState.enabled, isInProgress = buttonState.isInProgress(), isFilledStyle = buttonState.style == InstallButtonStyle.AccentFill, + isDisabledStyle = buttonState.style == InstallButtonStyle.Disabled, showMore = false, actionIntent = buttonState.actionIntent, + progressFraction = buttonState.progressFraction, ), iconUrl = iconUrl, placeholderResId = null, diff --git a/app/src/main/java/foundation/e/apps/ui/compose/components/SearchTabs.kt b/app/src/main/java/foundation/e/apps/ui/compose/components/SearchTabs.kt index a9d810c47b1bb5c55aacea966663c09c2890dd41..cef3cc0d6d4b9703faf9a7b1838f2cf17176e798 100644 --- a/app/src/main/java/foundation/e/apps/ui/compose/components/SearchTabs.kt +++ b/app/src/main/java/foundation/e/apps/ui/compose/components/SearchTabs.kt @@ -45,13 +45,14 @@ fun SearchTabs( modifier: Modifier = Modifier, ) { SecondaryTabRow( - modifier = modifier, + modifier = modifier.padding(horizontal = 16.dp), selectedTabIndex = selectedIndex, indicator = { if (selectedIndex in tabs.indices) { SecondaryIndicator( modifier = Modifier.tabIndicatorOffset(selectedIndex), - color = MaterialTheme.colorScheme.primary, + height = 2.dp, + color = MaterialTheme.colorScheme.tertiary, ) } }, @@ -64,14 +65,14 @@ fun SearchTabs( Tab( selected = index == selectedIndex, onClick = { onTabSelect(tab, index) }, - selectedContentColor = MaterialTheme.colorScheme.primary, - unselectedContentColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.64f), + selectedContentColor = MaterialTheme.colorScheme.tertiary, + unselectedContentColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.60f), ) { Text( text = label, modifier = Modifier.padding(vertical = 12.dp), style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.SemiBold, + fontWeight = FontWeight.Medium, letterSpacing = 0.4.sp, ), ) diff --git a/app/src/main/java/foundation/e/apps/ui/compose/components/search/SearchErrorState.kt b/app/src/main/java/foundation/e/apps/ui/compose/components/search/SearchErrorState.kt index 964253ab57bf5b3de81dd21633f1a1c029817c6e..3d62653f1054148bebc315dbb6f560a58d1d54c7 100644 --- a/app/src/main/java/foundation/e/apps/ui/compose/components/search/SearchErrorState.kt +++ b/app/src/main/java/foundation/e/apps/ui/compose/components/search/SearchErrorState.kt @@ -18,28 +18,30 @@ package foundation.e.apps.ui.compose.components.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.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign 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.ui.compose.theme.AppTheme @Composable fun SearchErrorState( - onRetry: () -> Unit, + @StringRes errorTitleStringRes: Int, modifier: Modifier = Modifier, fullScreen: Boolean = true, ) { @@ -49,31 +51,29 @@ fun SearchErrorState( modifier.fillMaxWidth() } - val contentPadding = if (fullScreen) { - PaddingValues(all = 24.dp) - } else { - PaddingValues(horizontal = 16.dp, vertical = 12.dp) - } - Box( modifier = containerModifier, contentAlignment = Alignment.Center ) { Column( modifier = Modifier - .fillMaxWidth() - .padding(contentPadding), + .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp), ) { Text( - text = stringResource(id = R.string.search_error), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onBackground, + text = stringResource(id = errorTitleStringRes), + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onPrimary, + ) + Text( + modifier = Modifier.padding(horizontal = 24.dp), + textAlign = TextAlign.Center, + text = stringResource(id = R.string.search_error_message), + fontSize = 15.sp, + color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.38F), ) - Button(onClick = onRetry) { - Text(text = stringResource(id = R.string.retry)) - } } } } @@ -82,14 +82,9 @@ fun SearchErrorState( @Composable private fun SearchErrorStateFullScreenPreview() { AppTheme { - SearchErrorState(onRetry = {}, fullScreen = true) - } -} - -@Preview(showBackground = true) -@Composable -private fun SearchErrorStateFooterPreview() { - AppTheme { - SearchErrorState(onRetry = {}, fullScreen = false) + SearchErrorState( + errorTitleStringRes = R.string.search_error_title_opensource, + fullScreen = true + ) } } diff --git a/app/src/main/java/foundation/e/apps/ui/compose/components/search/SearchLoading.kt b/app/src/main/java/foundation/e/apps/ui/compose/components/search/SearchLoading.kt index 89fd22650e9b021ec2e23441d6c98223437adaa8..ca86e176021fa64dc356559be5ee62a9d95f4c7d 100644 --- a/app/src/main/java/foundation/e/apps/ui/compose/components/search/SearchLoading.kt +++ b/app/src/main/java/foundation/e/apps/ui/compose/components/search/SearchLoading.kt @@ -21,6 +21,7 @@ package foundation.e.apps.ui.compose.components.search import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import foundation.e.apps.data.application.data.Application @@ -35,7 +36,7 @@ fun SearchShimmerList(modifier: Modifier = Modifier) { .fillMaxSize(), contentAlignment = androidx.compose.ui.Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.colorScheme.tertiary) } } @@ -47,7 +48,6 @@ fun SearchResultListItemPlaceholder(modifier: Modifier = Modifier) { onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = {}, - onPrivacyClick = {}, modifier = modifier ) } @@ -56,11 +56,6 @@ private fun placeholderState() = SearchResultListItemState( author = "", ratingText = "", showRating = false, - sourceTag = "", - showSourceTag = false, - privacyScore = "", - showPrivacyScore = false, - isPrivacyLoading = false, primaryAction = PrimaryActionUiState( label = "", enabled = false, diff --git a/app/src/main/java/foundation/e/apps/ui/compose/screens/SearchScreen.kt b/app/src/main/java/foundation/e/apps/ui/compose/screens/SearchScreen.kt index 38cefd2b215bb0a3318f6a4f289dab3e2c2c1002..9f7898a4fd2efddc2f728d77222d4b6581d5f47a 100644 --- a/app/src/main/java/foundation/e/apps/ui/compose/screens/SearchScreen.kt +++ b/app/src/main/java/foundation/e/apps/ui/compose/screens/SearchScreen.kt @@ -18,11 +18,19 @@ package foundation.e.apps.ui.compose.screens +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column 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.itemsIndexed +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -69,7 +77,6 @@ fun SearchScreen( onScrollPositionChange: (SearchTabType, Int, Int) -> Unit = { _, _, _ -> }, onResultClick: (Application) -> Unit = {}, onShowMoreClick: (Application) -> Unit = {}, - onPrivacyClick: (Application) -> Unit = {}, onPrimaryAction: (Application, InstallButtonAction) -> Unit = { _, _ -> }, installButtonStateProvider: (Application) -> InstallButtonState, ) { @@ -78,9 +85,7 @@ fun SearchScreen( val lifecycleOwner = LocalLifecycleOwner.current val focusRequester = remember { FocusRequester() } val shouldAutoFocus = !uiState.hasSubmittedSearch - var isSearchExpanded by rememberSaveable { mutableStateOf(shouldAutoFocus) } var hasRequestedInitialFocus by rememberSaveable { mutableStateOf(false) } - val showSuggestions = isSearchExpanded && uiState.isSuggestionVisible LaunchedEffect(lifecycleOwner, shouldAutoFocus) { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { @@ -90,44 +95,34 @@ fun SearchScreen( focusRequester.requestFocus() keyboardController?.show() } - } else { - // Intentionally no-op; the initial focus request has already happened. } } } LaunchedEffect(uiState.hasSubmittedSearch) { if (uiState.hasSubmittedSearch) { - isSearchExpanded = false focusManager.clearFocus() - } else { - // Intentionally no-op; results are not active so focus remains unchanged. } } Scaffold( modifier = modifier, topBar = { - SearchTopBar( - uiState = uiState, - expanded = isSearchExpanded, - showSuggestions = showSuggestions, - focusRequester = focusRequester, - focusManager = focusManager, - onQueryChange = onQueryChange, - onClearQuery = onClearQuery, - onSearchSubmit = onSubmitSearch, - onSuggestionSelect = onSuggestionSelect, - onExpandedChange = { expanded -> - isSearchExpanded = expanded - if (expanded) { - focusRequester.requestFocus() - } else { - focusManager.clearFocus() - } - }, - onBack = onBackClick, - ) + Column { + SearchTopBar( + uiState = uiState, + focusRequester = focusRequester, + focusManager = focusManager, + onQueryChange = onQueryChange, + onClearQuery = onClearQuery, + onSearchSubmit = onSubmitSearch, + onBack = onBackClick, + ) + HorizontalDivider( + color = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.padding(top = 8.dp), + ) + } }, ) { innerPadding -> Column( @@ -141,33 +136,70 @@ fun SearchScreen( val shouldShowResults = uiState.hasSubmittedSearch && uiState.selectedTab != null && uiState.availableTabs.isNotEmpty() + val shouldShowSuggestions = + !uiState.hasSubmittedSearch && uiState.isSuggestionVisible && uiState.suggestions.isNotEmpty() - if (shouldShowResults) { - SearchResultsContent( - tabs = uiState.availableTabs, - selectedTab = uiState.selectedTab!!, - fossItems = fossItems, - pwaItems = pwaItems, - playStoreItems = playStoreItems, - searchVersion = searchVersion, - getScrollPosition = getScrollPosition, - onScrollPositionChange = onScrollPositionChange, - onTabSelect = onTabSelect, - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - onResultClick = onResultClick, - onPrimaryActionClick = onPrimaryAction, - onShowMoreClick = onShowMoreClick, - onPrivacyClick = onPrivacyClick, - installButtonStateProvider = installButtonStateProvider, - ) - } else { - SearchInitialState( - modifier = Modifier - .fillMaxWidth(), - ) + when { + shouldShowResults -> { + SearchResultsContent( + tabs = uiState.availableTabs, + selectedTab = uiState.selectedTab!!, + fossItems = fossItems, + pwaItems = pwaItems, + playStoreItems = playStoreItems, + searchVersion = searchVersion, + getScrollPosition = getScrollPosition, + onScrollPositionChange = onScrollPositionChange, + onTabSelect = onTabSelect, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + onResultClick = onResultClick, + onPrimaryActionClick = onPrimaryAction, + onShowMoreClick = onShowMoreClick, + installButtonStateProvider = installButtonStateProvider, + ) + } + shouldShowSuggestions -> { + SuggestionList( + suggestions = uiState.suggestions, + onSuggestionSelect = { suggestion -> + focusManager.clearFocus() + onSuggestionSelect(suggestion) + }, + modifier = Modifier.fillMaxWidth(), + ) + } + else -> { + SearchInitialState( + modifier = Modifier.fillMaxSize(), + ) + } } } } } + +@Composable +private fun SuggestionList( + suggestions: List, + onSuggestionSelect: (String) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn(modifier = modifier) { + itemsIndexed( + items = suggestions, + key = { _, suggestion -> suggestion }, + ) { _, suggestion -> + ListItem( + modifier = Modifier + .fillMaxWidth() + .clickable { onSuggestionSelect(suggestion) }, + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.background, + ), + headlineContent = { Text(text = suggestion) }, + ) + } + } +} diff --git a/app/src/main/java/foundation/e/apps/ui/compose/screens/SearchTopBar.kt b/app/src/main/java/foundation/e/apps/ui/compose/screens/SearchTopBar.kt index 458de70f5f0e2a73a1542f35aa58ec1a84864ebb..4b9baea6bcaab9fa856ee23d0c21a0fffb49500c 100644 --- a/app/src/main/java/foundation/e/apps/ui/compose/screens/SearchTopBar.kt +++ b/app/src/main/java/foundation/e/apps/ui/compose/screens/SearchTopBar.kt @@ -18,33 +18,25 @@ package foundation.e.apps.ui.compose.screens -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Search import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text -import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource @@ -59,15 +51,11 @@ import foundation.e.apps.ui.search.v2.SearchUiState @Composable fun SearchTopBar( uiState: SearchUiState, - expanded: Boolean, - showSuggestions: Boolean, focusRequester: FocusRequester, focusManager: FocusManager, onQueryChange: (String) -> Unit, onClearQuery: () -> Unit, onSearchSubmit: (String) -> Unit, - onSuggestionSelect: (String) -> Unit, - onExpandedChange: (Boolean) -> Unit, onBack: () -> Unit, modifier: Modifier = Modifier, ) { @@ -75,14 +63,15 @@ fun SearchTopBar( modifier = modifier .fillMaxWidth() .testTag(SearchTopBarTestTags.SEARCH_BAR), + shape = RectangleShape, + shadowElevation = 0.dp, + tonalElevation = 0.dp, colors = SearchBarDefaults.colors( - containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp), - dividerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), + containerColor = MaterialTheme.colorScheme.background, + dividerColor = MaterialTheme.colorScheme.tertiary, ), - expanded = expanded, - onExpandedChange = { isExpanded -> - onExpandedChange(isExpanded) - }, + expanded = false, + onExpandedChange = {}, inputField = { SearchBarDefaults.InputField( modifier = Modifier @@ -90,19 +79,18 @@ fun SearchTopBar( .focusRequester(focusRequester) .testTag(SearchTopBarTestTags.INPUT_FIELD), query = uiState.query, - onQueryChange = { query -> - onExpandedChange(true) - onQueryChange(query) - }, + onQueryChange = onQueryChange, onSearch = { query -> - onExpandedChange(false) focusManager.clearFocus() onSearchSubmit(query) }, - expanded = expanded, - onExpandedChange = { isExpanded -> - onExpandedChange(isExpanded) - }, + expanded = false, + onExpandedChange = {}, + colors = SearchBarDefaults.inputFieldColors( + unfocusedContainerColor = MaterialTheme.colorScheme.background, + focusedContainerColor = MaterialTheme.colorScheme.background, + cursorColor = MaterialTheme.colorScheme.tertiary, + ), placeholder = { Text(text = stringResource(id = R.string.search_hint)) }, leadingIcon = { IconButton( @@ -115,6 +103,7 @@ fun SearchTopBar( Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(id = R.string.search_back_button), + modifier = Modifier.size(28.dp), ) } }, @@ -124,7 +113,6 @@ fun SearchTopBar( modifier = Modifier.testTag(SearchTopBarTestTags.CLEAR_BUTTON), onClick = { onClearQuery() - onExpandedChange(true) focusRequester.requestFocus() }, ) { @@ -137,52 +125,7 @@ fun SearchTopBar( }, ) }, - ) { - if (showSuggestions) { - SuggestionList( - suggestions = uiState.suggestions, - onSuggestionSelect = { suggestion -> - onExpandedChange(false) - focusManager.clearFocus() - onSuggestionSelect(suggestion) - }, - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)), - ) - } - } -} - -@Composable -private fun SuggestionList( - suggestions: List, - onSuggestionSelect: (String) -> Unit, - modifier: Modifier = Modifier, -) { - LazyColumn( - modifier = modifier.testTag(SearchTopBarTestTags.SUGGESTIONS_LIST), - ) { - itemsIndexed( - items = suggestions, - key = { _, suggestion -> suggestion }, - ) { index, suggestion -> - ListItem( - modifier = Modifier - .fillMaxWidth() - .clickable { onSuggestionSelect(suggestion) } - .testTag("${SearchTopBarTestTags.SUGGESTION_ITEM_PREFIX}$index"), - headlineContent = { Text(text = suggestion) }, - leadingContent = { - Icon( - imageVector = Icons.Filled.Search, - contentDescription = stringResource(id = R.string.menu_search), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - }, - ) - } - } + ) {} } internal object SearchTopBarTestTags { @@ -190,8 +133,6 @@ internal object SearchTopBarTestTags { const val INPUT_FIELD = "search_top_bar_input_field" const val BACK_BUTTON = "search_top_bar_back_button" const val CLEAR_BUTTON = "search_top_bar_clear_button" - const val SUGGESTIONS_LIST = "search_top_bar_suggestions_list" - const val SUGGESTION_ITEM_PREFIX = "search_top_bar_suggestion_" } @Preview(showBackground = true) @@ -200,7 +141,6 @@ private fun SearchTopBarPreview() { AppTheme(darkTheme = true) { val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current - var expanded by remember { mutableStateOf(true) } val sampleState = SearchUiState( query = "browser", suggestions = listOf( @@ -221,15 +161,11 @@ private fun SearchTopBarPreview() { SearchTopBar( uiState = sampleState, - expanded = expanded, - showSuggestions = expanded && sampleState.suggestions.isNotEmpty(), focusRequester = focusRequester, focusManager = focusManager, - onQueryChange = { expanded = true }, - onClearQuery = { expanded = true }, - onSearchSubmit = { expanded = false }, - onSuggestionSelect = { expanded = false }, - onExpandedChange = { expanded = it }, + onQueryChange = {}, + onClearQuery = {}, + onSearchSubmit = {}, onBack = {}, ) } diff --git a/app/src/main/java/foundation/e/apps/ui/compose/state/InstallButtonState.kt b/app/src/main/java/foundation/e/apps/ui/compose/state/InstallButtonState.kt index 5aa16d20bdc255dfc07627d09e06b3940e7c3e4e..6a9b07e3de4a440c3ac0fcfb1c1d5d722db56498 100644 --- a/app/src/main/java/foundation/e/apps/ui/compose/state/InstallButtonState.kt +++ b/app/src/main/java/foundation/e/apps/ui/compose/state/InstallButtonState.kt @@ -31,6 +31,7 @@ data class InstallButtonState( val style: InstallButtonStyle = InstallButtonStyle.AccentOutline, val showProgressBar: Boolean = false, val progressPercentText: String? = null, + val progressFraction: Float = 0f, val actionIntent: InstallButtonAction = InstallButtonAction.NoOp, @StringRes val snackbarMessageId: Int? = null, val dialogType: InstallDialogType? = null, diff --git a/app/src/main/java/foundation/e/apps/ui/compose/state/InstallButtonStateMapper.kt b/app/src/main/java/foundation/e/apps/ui/compose/state/InstallButtonStateMapper.kt index b24d7f013a45b531cd2c9232d2a0eefc48792703..4152b2000b530d21b1a27d0f168e71fe8d77de00 100644 --- a/app/src/main/java/foundation/e/apps/ui/compose/state/InstallButtonStateMapper.kt +++ b/app/src/main/java/foundation/e/apps/ui/compose/state/InstallButtonStateMapper.kt @@ -32,7 +32,8 @@ fun mapAppToInstallState(input: InstallButtonStateInput): InstallButtonState { Status.INSTALLED -> mapInstalled() Status.UPDATABLE -> mapUpdatable(input) Status.UNAVAILABLE -> mapUnavailable(input) - Status.QUEUED, Status.AWAITING, Status.DOWNLOADING, Status.DOWNLOADED -> mapDownloading(input, status) + Status.QUEUED, Status.AWAITING -> mapQueued(status) + Status.DOWNLOADING, Status.DOWNLOADED -> mapDownloading(input, status) Status.INSTALLING -> mapInstalling(status) Status.BLOCKED -> mapBlocked(input) Status.INSTALLATION_ISSUE -> mapInstallationIssue(input) @@ -136,13 +137,27 @@ private fun mapUnavailablePaid(input: InstallButtonStateInput): InstallButtonSta } } +private fun mapQueued(status: Status): InstallButtonState { + return InstallButtonState( + label = ButtonLabel(resId = R.string.cancel), + progressPercentText = null, + enabled = true, + style = buildStyleFor(status, enabled = true), + actionIntent = InstallButtonAction.CancelDownload, + statusTag = StatusTag.Downloading, + rawStatus = status, + ) +} + private fun mapDownloading(input: InstallButtonStateInput, status: Status): InstallButtonState { + val fraction = input.progressPercent + ?.coerceIn(PROGRESS_MIN, PROGRESS_MAX) + ?.div(PROGRESS_MAX_FLOAT) + ?: 0f return InstallButtonState( - label = ButtonLabel( - resId = if (input.percentLabel == null) R.string.cancel else null, - text = input.percentLabel, - ), + label = ButtonLabel(resId = R.string.cancel), progressPercentText = input.percentLabel, + progressFraction = fraction, enabled = true, style = buildStyleFor(status, enabled = true), actionIntent = InstallButtonAction.CancelDownload, @@ -154,8 +169,8 @@ private fun mapDownloading(input: InstallButtonStateInput, status: Status): Inst private fun mapInstalling(status: Status): InstallButtonState { return InstallButtonState( label = ButtonLabel(resId = R.string.installing), - enabled = false, - style = buildStyleFor(status, enabled = false), + enabled = true, + style = buildStyleFor(status, enabled = true), actionIntent = InstallButtonAction.NoOp, statusTag = StatusTag.Installing, rawStatus = status, @@ -208,6 +223,7 @@ private fun buildDefaultBlockedLabel(app: Application): ButtonLabel { private fun buildStyleFor(status: Status, enabled: Boolean): InstallButtonStyle { return when { + status == Status.QUEUED || status == Status.INSTALLING -> InstallButtonStyle.Disabled status == Status.INSTALLED || status == Status.UPDATABLE -> { if (enabled) InstallButtonStyle.AccentFill else InstallButtonStyle.Disabled } @@ -216,3 +232,7 @@ private fun buildStyleFor(status: Status, enabled: Boolean): InstallButtonStyle else -> InstallButtonStyle.Disabled } } + +private const val PROGRESS_MIN = 0 +private const val PROGRESS_MAX = 100 +private const val PROGRESS_MAX_FLOAT = 100f diff --git a/app/src/main/java/foundation/e/apps/ui/compose/theme/AppTheme.kt b/app/src/main/java/foundation/e/apps/ui/compose/theme/AppTheme.kt index f7e7dea080bbd61ca3fce5afac51ee260d692608..155f8d3b624007f6e0d65981cc5301e6cf35b05a 100644 --- a/app/src/main/java/foundation/e/apps/ui/compose/theme/AppTheme.kt +++ b/app/src/main/java/foundation/e/apps/ui/compose/theme/AppTheme.kt @@ -29,7 +29,7 @@ import foundation.e.elib.R as eR fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { val colorScheme = if (darkTheme) { darkColorScheme( - primary = colorResource(eR.color.e_action_bar_dark), + primary = colorResource(eR.color.e_accent_dark), secondary = colorResource(eR.color.e_action_bar_dark), tertiary = colorResource(eR.color.e_accent_dark), background = colorResource(eR.color.e_background_dark), @@ -41,7 +41,7 @@ fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () ) } else { lightColorScheme( - primary = colorResource(eR.color.e_action_bar_light), + primary = colorResource(eR.color.e_accent_light), secondary = colorResource(eR.color.e_action_bar_light), tertiary = colorResource(eR.color.e_accent_light), background = colorResource(eR.color.e_background_light), @@ -53,5 +53,5 @@ fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () ) } - MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content) + MaterialTheme(colorScheme = colorScheme, content = content) } diff --git a/app/src/main/java/foundation/e/apps/ui/compose/theme/Typography.kt b/app/src/main/java/foundation/e/apps/ui/compose/theme/Typography.kt index 363f5d87d04218e4a9421a6438d9776fe305143d..239541fb6a30e7cf744a56ab5a467a6220f41003 100644 --- a/app/src/main/java/foundation/e/apps/ui/compose/theme/Typography.kt +++ b/app/src/main/java/foundation/e/apps/ui/compose/theme/Typography.kt @@ -17,7 +17,6 @@ */ package foundation.e.apps.ui.compose.theme -import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -26,14 +25,6 @@ import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp // Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - ) -) val titleStyle = TextStyle( fontFamily = FontFamily.SansSerif, diff --git a/app/src/main/java/foundation/e/apps/ui/search/v2/SearchFragmentV2.kt b/app/src/main/java/foundation/e/apps/ui/search/v2/SearchFragmentV2.kt index 3e472f97c09aa602397a12af99d0cb918f520e62..c9f2bd1e59b4a5aa24ecc5dbdc2287c01ab8caed 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/v2/SearchFragmentV2.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/v2/SearchFragmentV2.kt @@ -48,9 +48,11 @@ import foundation.e.apps.ui.AppProgressViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.compose.screens.SearchScreen +import foundation.e.apps.ui.compose.state.ButtonLabel import foundation.e.apps.ui.compose.state.InstallButtonAction import foundation.e.apps.ui.compose.state.InstallButtonState import foundation.e.apps.ui.compose.state.InstallButtonStateInput +import foundation.e.apps.ui.compose.state.InstallButtonStyle import foundation.e.apps.ui.compose.state.PurchaseState import foundation.e.apps.ui.compose.state.mapAppToInstallState import foundation.e.apps.ui.compose.theme.AppTheme @@ -101,16 +103,20 @@ class SearchFragmentV2 : Fragment(R.layout.fragment_search_v2) { val downloadProgress by appProgressViewModel.downloadProgress.observeAsState() val progressPercentMap by searchViewModel.progressPercentByKey.collectAsState() val statusByKey by searchViewModel.statusByKey.collectAsState() + val pendingInstalls by searchViewModel.pendingInstalls.collectAsState() val selfPackageName = requireContext().packageName DownloadProgressEffect(downloadProgress) val installButtonStateProvider = buildInstallButtonStateProvider( - user = user, - isAnonymous = isAnonymous, - progressPercentMap = progressPercentMap, - statusByKey = statusByKey, - selfPackageName = selfPackageName, + InstallButtonProviderParams( + user = user, + isAnonymous = isAnonymous, + progressPercentMap = progressPercentMap, + statusByKey = statusByKey, + pendingInstalls = pendingInstalls, + selfPackageName = selfPackageName, + ) ) SearchScreen( @@ -148,33 +154,41 @@ class SearchFragmentV2 : Fragment(R.layout.fragment_search_v2) { } private fun buildInstallButtonStateProvider( - user: User, - isAnonymous: Boolean, - progressPercentMap: Map, - statusByKey: Map, - selfPackageName: String, + params: InstallButtonProviderParams, ): (Application) -> InstallButtonState { return { app -> val progressKey = progressKeyFor(app) - val progressPercent = progressPercentMap[progressKey] - val overrideStatus = statusByKey[progressKey] + val progressPercent = params.progressPercentMap[progressKey] + val overrideStatus = params.statusByKey[progressKey] + val isPending = progressKey in params.pendingInstalls val purchaseState = purchaseStateFor(app) val isBlocked = appInfoFetchViewModel.isAppInBlockedList(app) val isUnsupported = isUnsupportedApp(app) - mapInstallButtonState( - app = app, - installButtonContext = InstallButtonContext( - user = user, - isAnonymous = isAnonymous, - isUnsupported = isUnsupported, - purchaseState = purchaseState, - progressPercent = progressPercent, - overrideStatus = overrideStatus, - isBlocked = isBlocked, - selfPackageName = selfPackageName, + // Show disabled state while waiting for install to register + if (isPending && (overrideStatus == null || overrideStatus == Status.UNAVAILABLE)) { + InstallButtonState( + label = ButtonLabel(), + enabled = true, + style = InstallButtonStyle.Disabled, + showProgressBar = true, + actionIntent = InstallButtonAction.NoOp, ) - ) + } else { + mapInstallButtonState( + app = app, + installButtonContext = InstallButtonContext( + user = params.user, + isAnonymous = params.isAnonymous, + isUnsupported = isUnsupported, + purchaseState = purchaseState, + progressPercent = progressPercent, + overrideStatus = overrideStatus, + isBlocked = isBlocked, + selfPackageName = params.selfPackageName, + ) + ) + } } } @@ -234,6 +248,15 @@ class SearchFragmentV2 : Fragment(R.layout.fragment_search_v2) { val selfPackageName: String, ) + private data class InstallButtonProviderParams( + val user: User, + val isAnonymous: Boolean, + val progressPercentMap: Map, + val statusByKey: Map, + val pendingInstalls: Set, + val selfPackageName: String, + ) + private fun copyProgress(progress: DownloadProgress): DownloadProgress { return DownloadProgress( totalSizeBytes = progress.totalSizeBytes.toMutableMap(), @@ -247,15 +270,20 @@ class SearchFragmentV2 : Fragment(R.layout.fragment_search_v2) { when (action) { InstallButtonAction.Install, InstallButtonAction.UpdateSelfConfirm -> { + searchViewModel.markPendingInstall(app) mainActivityViewModel.verifyUiFilter(app) { if (mainActivityViewModel.shouldShowPaidAppsSnackBar(app)) { + searchViewModel.clearPendingInstall(app) return@verifyUiFilter } mainActivityViewModel.getApplication(app) } } - InstallButtonAction.CancelDownload -> mainActivityViewModel.cancelDownload(app) + InstallButtonAction.CancelDownload -> { + searchViewModel.clearPendingInstall(app) + mainActivityViewModel.cancelDownload(app) + } InstallButtonAction.OpenAppOrPwa -> { if (app.is_pwa) { mainActivityViewModel.launchPwa(app) diff --git a/app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt b/app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt index b093d55eee6750f5ce29033d1bbcb129e9971b54..a152850ff725fd89a0e8a902241daa6dd17088fb 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt @@ -79,7 +79,7 @@ data class ScrollPosition( ) @HiltViewModel -@Suppress("LongParameterList") +@Suppress("LongParameterList", "TooManyFunctions") class SearchViewModelV2 @Inject constructor( private val appLoungePreference: AppLoungePreference, cleanApkSearchPagingUseCase: CleanApkSearchPagingUseCase, @@ -110,6 +110,9 @@ class SearchViewModelV2 @Inject constructor( private val _statusByKey = MutableStateFlow>(emptyMap()) val statusByKey: StateFlow> = _statusByKey.asStateFlow() + private val _pendingInstalls = MutableStateFlow>(emptySet()) + val pendingInstalls: StateFlow> = _pendingInstalls.asStateFlow() + val fossPagingFlow = cleanApkSearchPagingUseCase( requests = searchRequests, source = Source.OPEN_SOURCE, @@ -141,7 +144,10 @@ class SearchViewModelV2 @Inject constructor( fun onQueryChanged(newQuery: String) { _uiState.update { current -> - current.copy(query = newQuery) + current.copy( + query = newQuery, + hasSubmittedSearch = if (newQuery.isNotBlank()) false else current.hasSubmittedSearch, + ) } suggestionJob?.cancel() @@ -340,6 +346,19 @@ class SearchViewModelV2 @Inject constructor( _statusByKey.update { current -> if (current[key] != app.status) current + (key to app.status) else current } + if (app.status != Status.UNAVAILABLE) { + _pendingInstalls.update { it - key } + } + } + + fun markPendingInstall(app: Application) { + val key = keyFor(app) + _pendingInstalls.update { it + key } + } + + fun clearPendingInstall(app: Application) { + val key = keyFor(app) + _pendingInstalls.update { it - key } } private fun keyFor(app: Application): String { diff --git a/app/src/main/java/foundation/e/apps/utils/MaterialButtonUtils.kt b/app/src/main/java/foundation/e/apps/utils/MaterialButtonUtils.kt index cde1c27a8086f17f1f42b8f32f0da49c1661d7a4..45a1ec1e1c09ee27aaaa7014452f1506f835e0a3 100644 --- a/app/src/main/java/foundation/e/apps/utils/MaterialButtonUtils.kt +++ b/app/src/main/java/foundation/e/apps/utils/MaterialButtonUtils.kt @@ -17,11 +17,11 @@ package foundation.e.apps.utils -import android.graphics.Color import androidx.core.content.ContextCompat import com.google.android.material.button.MaterialButton import foundation.e.apps.R import foundation.e.apps.data.enums.Status +import foundation.e.elib.R as eR fun MaterialButton.disableInstallButton(status: Status? = null) { toggleEnableMaterialButton(false, status) @@ -49,16 +49,16 @@ private fun MaterialButton.getBackgroundTintList(status: Status?) = private fun MaterialButton.getStrokeColor( isEnabled: Boolean, ) = if (isEnabled) { - ContextCompat.getColorStateList(this.context, R.color.colorAccent) + ContextCompat.getColorStateList(this.context, eR.color.e_accent) } else { - ContextCompat.getColorStateList(this.context, R.color.light_grey) + ContextCompat.getColorStateList(this.context, eR.color.e_disabled_color) } private fun MaterialButton.setButtonTextColor(isEnabled: Boolean, status: Status?) = if (isEnabled && (status == Status.INSTALLED || status == Status.UPDATABLE)) { - setTextColor(Color.WHITE) + setTextColor(context.getColor(eR.color.e_background)) } else if (isEnabled) { - setTextColor(context.getColor(R.color.colorAccent)) + setTextColor(context.getColor(eR.color.e_accent)) } else { - setTextColor(context.getColor(R.color.light_grey)) + setTextColor(context.getColor(eR.color.e_disabled_color)) } diff --git a/app/src/main/res/drawable/ic_all_apps_updated.xml b/app/src/main/res/drawable/ic_all_apps_updated.xml index 6a417cc3630c87946abbdbcc8eeb9d1fb88cf23d..1794fcf64b9a3248ea7ed076afbe96ac5cee6ecc 100644 --- a/app/src/main/res/drawable/ic_all_apps_updated.xml +++ b/app/src/main/res/drawable/ic_all_apps_updated.xml @@ -16,11 +16,11 @@ --> diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml index 2a31b2ef3ef4a1f17571798a11dda953af514246..ddc3a31470defa0ba9bccba98825ad9058658bd4 100644 --- a/app/src/main/res/drawable/ic_arrow_back.xml +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="?attr/colorControlNormal" + android:tint="@color/e_primary_text_color" android:autoMirrored="true"> + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 5d8706c335dd1a366e626ca0ba9cdd8c608e82fc..82c1b470368c095fa12d4f529ed9e1ade5a7582c 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -23,24 +23,46 @@ android:layout_height="match_parent" tools:context=".ui.MainActivity"> - + tools:visibility="visible"> + + + + + + + + diff --git a/app/src/main/res/layout/fragment_application_download.xml b/app/src/main/res/layout/fragment_application_download.xml index e34b6ecabacd3bfb5d1cdf9728c44e5faf34c4e2..e063b764667f3ad0b9edd8a5711c0f4c561669d7 100644 --- a/app/src/main/res/layout/fragment_application_download.xml +++ b/app/src/main/res/layout/fragment_application_download.xml @@ -85,14 +85,7 @@ app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" style="@style/InstallButtonStyle" - android:layout_width="120dp" - android:layout_height="43dp" - android:text="@string/install" - android:textAllCaps="false" - android:textSize="18sp" - app:autoSizeTextType="uniform" - app:cornerRadius="4dp" - /> + android:text="@string/install" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_updates.xml b/app/src/main/res/layout/fragment_updates.xml index fbefcc623bbc09fde7fed5aa4d872243d9eb8a97..e461455a68553f3146c87745bdd188f39f990b61 100644 --- a/app/src/main/res/layout/fragment_updates.xml +++ b/app/src/main/res/layout/fragment_updates.xml @@ -92,9 +92,12 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:drawableTop="@drawable/ic_all_apps_updated" + android:drawablePadding="10dp" android:text="@string/all_apps_updated" android:textAlignment="center" + android:textColor="@color/e_primary_text_color" android:textSize="18sp" + android:textAppearance="@style/TextAppearance.Material3.BodyLarge" android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/home_child_list_item.xml b/app/src/main/res/layout/home_child_list_item.xml index cfb79c302ab4fb8201f472f34d9460a20ca15c60..d75ac853f18e10c369392a65f2dc4635f3d2dab6 100644 --- a/app/src/main/res/layout/home_child_list_item.xml +++ b/app/src/main/res/layout/home_child_list_item.xml @@ -66,14 +66,8 @@ + android:text="@string/install" /> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 73457fec9d2a9bc98c7ebee287ebc83d69deede0..298307ec0a6e071c0478f7fea1c44b21d741c6a5 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -21,6 +21,14 @@ Aktualisierungen Einstellungen Keine Apps gefunden … + Apps vorübergehend nicht verfügbar + Open Source vorübergehend nicht verfügbar + Web Apps vorübergehend nicht verfügbar + Dieser Katalog ist derzeit nicht erreichbar. Andere Apps-Quellen können weiterhin funktionieren. + Keine Ergebnisse in Apps + Keine Ergebnisse in Open Source + Keine Ergebnisse in Web Apps + Andere Apps-Quellen können Ergebnisse liefern. Bitte prüfe die anderen Tabs/deine Einstellungen. Anwendungen Spiele Kategorien-Symbol @@ -48,7 +56,7 @@ Benachrichtige, wenn App-Aktualisierungen verfügbar sind Zeige Anwendungen an: Zeige quelloffene Apps an - Zeige allgemeine Apps an + Zeige Apps an Zeige PWAs an Konto Nutzungsbedingungen @@ -92,7 +100,8 @@ Aktualisierungen App-Aktualisierungen werden nicht automatisch installiert Auf ein unlimitiertes Netzwerk warten - Keine Verbindung möglich. Bitte überprüfe die Internetverbindung und versuche es erneut + Keine Verbindung + Bitte überprüfe deine Internetverbindung und versuche es erneut. Start App-Suche Zurück @@ -204,7 +213,7 @@ Allgemeine Apps nicht verfügbar Quelloffene Apps und PWA nicht verfügbar - Beim Laden der allgemeinen Apps ist ein Fehler aufgetreten. Nur quelloffene Apps und PWA sind momentan verfügbar. + Beim Laden der Apps ist ein Fehler aufgetreten. Nur quelloffene Apps und Web Apps sind momentan verfügbar. Beim Laden von PWA und quelloffenen Apps ist ein Fehler aufgetreten. Nur allgemeine Apps sind momentan verfügbar. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 1cb811082278ab9e7c6bbdf0990552a94241761b..23ece824bfb65e2e9fccdfa143b5880218cd836c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -37,6 +37,14 @@ Atrás Borrar búsqueda No se encontraron aplicaciones… + Apps temporalmente no disponibles + Open Source temporalmente no disponible + Web Apps temporalmente no disponibles + No podemos acceder a este catálogo ahora mismo. Otras fuentes de Apps pueden seguir funcionando. + Sin resultados en Apps + Sin resultados en Open Source + Sin resultados en Web Apps + Otras fuentes de Apps pueden ofrecer resultados, revisa las otras pestañas/tu configuración. Juegos Código abierto PWA @@ -62,7 +70,7 @@ Descargar e instalar actualizaciones en segundo plano Mostrar actualizaciones disponibles Mostrar aplicaciones: - Mostrar aplicaciones comunes + Mostrar Apps Mostrar aplicaciones de código abierto Mostrar PWA Cuenta @@ -96,7 +104,8 @@ Descargas Actualizaciones Las actualizaciones no se instalarán automáticamente - ¡No se pudo conectar! Chequea tu conexión a internet e inténtalo de nuevo + Sin conexión + Por favor, comprueba tu conexión a internet e inténtalo de nuevo. Esperando una red sin contador Inicio Aplicaciones @@ -203,7 +212,7 @@ Aplicaciones comunes no disponibles Aplicaciones de código abierto y PWA no disponibles - Se produjo un error al cargar las aplicaciones comunes. Solo aplicaciones de código abierto y PWA están disponibles por ahora. + Se produjo un error al cargar Apps. Solo aplicaciones de código abierto y Web Apps están disponibles por ahora. Hubo un error al cargar las aplicaciones PWA y de código abierto. Solo aplicaciones comunes están disponibles por ahora. App Lounge se cerrará durante la instalación de la actualización. Por favor, abstente de hacer nada en hasta que la actualización (descarga + instalación) haya terminado. Podrás usarla en un par de minutos como máximo. ¡Advertencia de actualización! diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index a7a99e44d4693bf7e3da0a2176dee53da9ef4cdb..f3da14a320480629046c4a9eb69368d902fd2c79 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -56,7 +56,7 @@ Tili Näytä PWAt Näytä avoimen lähdekoodin sovellukset - Näytä yleiset sovellukset + Näytä sovellukset Näytä sovellukset: Näytä ilmoitus kun sovelluspäivityksiä on saatavilla Näytä saatavilla olevat päivitykset @@ -93,7 +93,7 @@ VAHVISTA Kun klikkaat vahvista, pääset sovelluksen Google Play -sivulle ja voit suorittaa oston loppuun selaimellasi. Klikkaa vahvista ostaaksesi %1$s hintaan %2$s. Osta %1$s - Yhteyden muodostaminen ei onnistu! Tarkista internetyhteytesi ja yritä uudelleen + Tarkista internetyhteytesi ja yritä uudelleen. Odotetaan mittaamatonta verkkoa Sovelluspäivityksiä ei asenneta automaattisesti Sovelluspäivitykset asennetaan automaattisesti @@ -168,7 +168,7 @@ Olet liian nuori asentaaksesi %1$s. Tarkista vanhemmaltasi että ikäryhmäsi on oikea, tai sammuta lapsilukko voidaksesi asentaa sen. Tässä sovelluksessa saattaa olla sopimatonta sisältöä. Tarkistetaan päivityksiä... - Yleisten sovellusten lataamisessa tapahtui virhe. Vain Avoimen lähdekoodin sovellukset ja PWA-sovellukset ovat saatavilla tällä hetkellä. + Sovellusten lataamisessa tapahtui virhe. Vain Avoimen lähdekoodin sovellukset ja Web Apps ovat saatavilla tällä hetkellä. Yleiset sovellukset ei saatavilla Tapahtui virhe ladattaessa PWA- ja Avoimen lähdekoodin sovelluksia. Vain Yleiset sovellukset ovat saatavilla tällä hetkellä. [%1$s] Rajoitettu sovellus diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index bc5f01263f8baca665cc51af6007f7e5f3b01656..144da352c697896b9fd735c912ee16365c39c3b9 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -38,7 +38,7 @@ Afficher les mises à jour disponibles Afficher une notification lorsque des mises à jour d\'applications sont disponibles Afficher les applications : - Afficher les applications répandues + Afficher les Apps Afficher les PWAs Compte Conditions de Service @@ -87,9 +87,18 @@ Téléchargements Mises à jour Les mises à jour d\'application ne seront pas installées automatiquement - Connexion impossible ! Merci de vérifier votre connexion internet puis réessayer + Aucune connexion + Veuillez vérifier votre connexion Internet et réessayer. Accueil Aucune application trouvée… + Apps temporairement indisponibles + Open Source temporairement indisponible + Web Apps temporairement indisponibles + Nous ne pouvons pas joindre ce catalogue pour l\'instant. D\'autres sources d\'Apps peuvent encore fonctionner. + Aucun résultat dans Apps + Aucun résultat dans Open Source + Aucun résultat dans Web Apps + D\'autres sources d\'Apps peuvent fournir des résultats, veuillez vérifier les autres onglets/vos paramètres. Connexion avec un compte Google Choisissez comment vous connecter Se connecter avec Google uniquement dans App Lounge @@ -203,7 +212,7 @@ Applications communes indisponibles Applications Open Source et PWA indisponibles - Une erreur est survenue lors du chargement des applications communes. Seules les applications Open Source et PWA sont disponibles pour l’instant. + Une erreur est survenue lors du chargement des Apps. Seules les applications Open Source et les Web Apps sont disponibles pour l’instant. Une erreur est survenue lors du chargement des applications PWA et Open Source. Seules les applications communes sont disponibles pour l\'instant. App Lounge sera fermé pendant l\'installation de la mise à jour. Veuillez ne rien faire tant que la mise à jour (téléchargement + installation) n\'est pas terminée. Vous pourrez y accéder dans quelques minutes au maximum. diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 5e6902d1f8b6cbf6644c21f8b8f1d2ffb9cfe99e..4ac0aa978c8634c3096bb969a581ccaaa69fb828 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -49,7 +49,7 @@ Birta tiltækar uppfærslur Birta tilkynningu þegar uppfærslur eru tiltækar Birta forrit: - Birta algeng forrit + Birta forrit Birta opinn hugbúnað Um hugbúnaðinn Útgáfa App Lounge @@ -123,7 +123,7 @@ Öll forrit eru af nýjustu útgáfu Vinsæl PWA-vefforrit Vinsælir PWA-leikir - Næ ekki að tengjast! Athugaðu internettenginguna þína og prófaðu svo aftur + Athugaðu internettenginguna þína og reyndu aftur. Féll á tímamörkum við að sækja forrit! Einhver vandamál í netkerfinu koma í veg fyrir að hægt sé að sækja öll forrit. \n @@ -194,7 +194,7 @@ Forrit með opnum grunnkóða og PWA óaðgengileg Villa kom upp við að hlaða inn forritum með opnum grunnkóða og PWA. Aðeins algeng forrit eru tiltæk í augnablikinu. Algeng forrit óaðgengileg - Villa kom upp við að hlaða inn algengum forritum. Aðeins forrit með opnum grunnkóða og PWA eru tiltæk í augnablikinu. + Villa kom upp við að hlaða inn forritum. Aðeins forrit með opnum grunnkóða og Web Apps eru tiltæk í augnablikinu. App Lounge verður lokað af kerfinu á meðan það uppfærir sjálft sig. Vertu helst ekki að gera neitt annað þangað til uppfærslu App Lounge er lokið. Það verður svo aðgengilegt aftur eftir örfáar mínútur. Veldu hvernig á að skrá þig inn diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index d1e0c320d4a73bacabe2ede24e3d0a526e7f5a29..c790787c99c3976199dffb3f0e95baa1b27c2c97 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -32,6 +32,14 @@ Indietro Cancella ricerca Non ho trovato App… + Apps temporaneamente non disponibili + Open Source temporaneamente non disponibile + Web Apps temporaneamente non disponibili + Non riusciamo a raggiungere questo catalogo al momento. Altre sorgenti di Apps potrebbero funzionare. + Nessun risultato in Apps + Nessun risultato in Open Source + Nessun risultato in Web Apps + Altre sorgenti di Apps potrebbero fornire risultati, controlla le altre schede/le impostazioni. App Giochi Icona Categorie @@ -62,7 +70,7 @@ Scarica ed installa gli aggiornamenti App in background Mostra gli aggiornamenti disponibili Mostra App: - Mostra tutte le App + Mostra App Mostra App open-source Mostra le PWA Account @@ -99,7 +107,8 @@ Gli aggiornamenti App non verranno installati automaticamente Attesa connessione WiFi Le App a pagamento non sono ancora supportate da App Lounge. Un pò di pazienza e lo saranno. - Non riesco a connettermi! Verifica la connessione internet e ritenta + Nessuna connessione + Controlla la tua connessione internet e riprova. Mostra una notifica quando sono disponibili aggiornamenti App Installa Annulla @@ -203,7 +212,7 @@ App comuni non disponibili App Open Source e PWA non disponibili - Si è verificato un errore durante il caricamento delle app comuni. Solo le app Open Source e PWA sono disponibili per ora. + Si è verificato un errore durante il caricamento delle App. Solo le app Open Source e le Web Apps sono disponibili per ora. Si è verificato un errore durante il caricamento delle app PWA e Open Source. Solo le app comuni sono disponibili per ora. Sto raccogliendo la valutazione di tutte le app che hai installato. L\'applicazione potrebbe contenere nudità, bestemmie, insulti, violenza, estrema sessualità, politicamente scorretto o altri argomenti potenzialmente disturbanti. Questo è rilevante specialmente in ambienti tipo lavoro, scuola, ambienti religiosi e familiari. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index deee7ecfd96baca0cfc25d91e81b2fb091cfe1f3..7e184bd660fa3c1c01807cf835ea0d7066f403d0 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -111,7 +111,7 @@ 売上が上位のゲーム トレンド上位のアプリ トレンド上位のゲーム - 一般的なアプリを表示 + アプリを表示 一般的なアプリを表示するにはログインが必要です。 このアプリは後で利用可能になります! オープンソースのアプリのみが許可されている間はGoogle Playのアプリは表示できません。 @@ -124,7 +124,7 @@ %sの追加のファイル PWAの人気アプリ PWAの人気ゲーム - 接続できません!インターネットの接続を確認して、再度試してください + インターネット接続を確認して、もう一度お試しください。 %1$sのアップデートが%2$sに完了しました。 アプリのアップデートは自動的にインストールされます アプリのアップデートは自動的にインストールされません @@ -187,7 +187,7 @@ インストール済みアプリのコンテンツ評価を取得中です。 一般アプリを利用できません オープンソースアプリと PWA を利用できません - 一般アプリの読み込み中にエラーが発生しました。現在はオープンソースアプリと PWA のみ利用できます。 + アプリの読み込み中にエラーが発生しました。現在はオープンソースアプリと Web Apps のみ利用できます。 PWA とオープンソースアプリの読み込み中にエラーが発生しました。現在は一般アプリのみ利用できます。 アプリは利用できません このアプリはインストールできません。これは通常、所在地(地域)やアカウントの年齢設定(年齢確認や年齢/コンテンツ評価を含む)に基づくコンテンツ制限が原因です。 diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 235591a0f8c5213760060604c719ee74a6482be4..be7629088461420331e9ce1be6be5b21f234ee5a 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -44,7 +44,7 @@ \n \nÅpne instillinger for å kun se etter åpen kildekode-applikasjoner eller PWAer. Tidsavbrudd ved henting av applikasjoner! - Kan ikke koble til! Kontroller internettilgangen og prøv igjen + Kontroller internettilkoblingen din og prøv igjen. Oppdatering av %1$s applikasjoner ble fullført %2$s. Venter på ubegrenset nettverk Applikasjonsoppdateringer vil ikke bli installert automatisk @@ -139,7 +139,7 @@ Oppdater applikasjoner installert av andre butikker Oppdater applikasjoner som er installert fra andre app-butikker. \nSlike applikasjoner vil bli forsøkt oppdatert fra vanlige applikasjoner og kategorien åpen kildekode. - Vis vanlige applikasjoner + Vis applikasjoner Du må logge inn for å kunne se vanlige applikasjoner. Personvernscore Vurderinger @@ -192,7 +192,7 @@ Samler innholdsvurdering for alle applikasjonene du har installert. Vanlige applikasjoner er ikke tilgjengelige Åpen kildekode-applikasjoner og PWA er ikke tilgjengelige - Det oppstod en feil under innlasting av en applikasjon. Bare åpen kildekode-applikasjoner og PWA er tilgjengelige for øyeblikket. + Det oppstod en feil under innlasting av applikasjoner. Bare åpen kildekode-applikasjoner og Web Apps er tilgjengelige for øyeblikket. Det oppstod en feil under innlasting av PWA- og åpen kildekode-applikasjoner. Bare vanlige applikasjoner er tilgjengelige for øyeblikket. Advarsel om oppdatering! diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 813255c9cc933f9911f22ed25425ff4052329a82..bc44312dab3f131068961103ed83a727d35068eb 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -33,7 +33,7 @@ Fout tijdens het laden van apps. Meer info Instellingen openen - Kan niet verbinden! Kijk alstublieft je internet verbinding na en probeer opnieuw + Controleer je internetverbinding en probeer het opnieuw. Update van %1$s apps voltooid op %2$s. Wachten op niet-gemeten netwerk App updates zullen niet automatisch geïnstalleerd worden @@ -97,7 +97,7 @@ Over Toon PWA\'s Toon open source apps - Toon algemene apps + Toon apps Toon applicaties: Toon een melding wanneer er app updates beschikbaar zijn Toon beschikbare updates @@ -193,7 +193,7 @@ Attentie, update! Algemene apps niet beschikbaar Open source apps en PWA niet beschikbaar - Er is een fout opgetreden tijdens het laden van de algemene apps. Enkel opensource apps en PWA\'s zijn beschikbaar. + Er is een fout opgetreden tijdens het laden van apps. Enkel opensource apps en Web Apps zijn beschikbaar. Er is een fout opgetreden tijdens het laden van PWA en opensource apps. Enkel algemene apps zijn beschikbaar. App Lounge zal afgesloten worden tijdens de update. Voer a.u.b. geen andere taken uit totdat de update is voltooid (download + installatie). Binnen maximum enkele minuten zal je terug toegang hebben. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index b5a9b9372f80453fd2f82f31dd9a156a93b80636..0dcadf67c21ab2a20d1ec19917aa32c2d17ad20d 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -32,7 +32,7 @@ Mostrar atualizações disponíveis Mostrar uma notificação quando houver atualizações de aplicativos disponíveis Mostrar aplicativos: - Mostrar aplicativos comuns + Mostrar aplicativos Mostrar aplicativos de código aberto Mostrar PWAs Por favor, selecione pelo menos uma fonte de aplicativos. @@ -103,7 +103,7 @@ Aviso de Conteúdo Aplicativos comuns indisponíveis Aplicativos de Código Aberto e PWA indisponíveis - Ocorreu um erro ao carregar aplicativos Comuns. Somente aplicativos de Código Aberto e PWA estão disponíveis no momento. + Ocorreu um erro ao carregar aplicativos. Somente aplicativos de Código Aberto e Web Apps estão disponíveis no momento. Ocorreu um erro ao carregar aplicativos PWA e de Código Aberto. Por enquanto, somente os aplicativos Comuns estão disponíveis. Recomendamos criar uma conta do Google dedicada para o App Lounge e, em seguida, fazer login com ela. Isso oferece o melhor equilíbrio entre privacidade e conveniência. Como alternativa, você pode usar o modo Anônimo. Os aplicativos pagos não podem ser instalados no modo anônimo. Faça login na sua conta do Google para instalar aplicativos pagos. @@ -118,7 +118,7 @@ Seu aplicativo será baixado automaticamente nesse dispositivo Você é muito jovem para poder instalar %1$s. Por favor, verifique com seus pais se sua faixa etária está correta ou desative o controle dos pais para poder instalá-lo. - Não é possível conectar! Verifique sua conexão com a Internet e tente novamente + Verifique sua conexão com a internet e tente novamente. Atualização de %1$s aplicativos concluída em %2$s. CONFIRMAR Semanal diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c1f51152df693cd938e9f6a5ab1924316816a78c..e0a937028f9c64b5e281db45c942fcd66eb76709 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -29,7 +29,7 @@ Показать только Open Source приложения PWA и Open Source приложения Open Source - Показать обычные приложения + Показать приложения Показать приложения: Показывать уведомление когда для приложения доступны обновления Показывать доступные обновления @@ -100,7 +100,7 @@ \n \nОткройте настройки, чтобы искать только Open Source приложения или PWA. Тайм-аут загрузки приложений! - Не удается подключиться! Пожалуйста, проверьте подключение к Интернету и повторите попытку + Проверьте подключение к интернету и попробуйте снова. Обновление приложений %1$s завершено на %2$s. В ожидании безлимитной сети Обновления приложений не будут устанавливаться автоматически @@ -196,7 +196,7 @@ App Lounge будет закрыт на время установки обновления. Пожалуйста, не используйте приложение, пока обновление (загрузка и установка) не будет завершено. Вы сможете получить к нему доступ в течение нескольких минут. Обычные приложения недоступны Приложения с открытым исходным кодом и PWA недоступны - При загрузке обычных приложений произошла ошибка. В настоящее время доступны только приложения с открытым исходным кодом и PWA. + При загрузке приложений произошла ошибка. В настоящее время доступны только приложения с открытым исходным кодом и Web Apps. При загрузке PWA и приложений с открытым исходным кодом произошла ошибка. В настоящее время доступны только обычные приложения. Выберите способ входа diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 60432be8b153aed729e643efd1562fdd4ba12eaa..b2adfc279f43b4a1d8ddf4bc3acb29056293df37 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -66,7 +66,7 @@ Takéto aplikácie sa budú aktualizovať z kategórií bežných a open source aplikácií. Zobraziť upozornenie, keď sú dostupné aktualizácie aplikácií Zobraziť aplikácie: - Zobraziť bežné aplikácie + Zobraziť aplikácie Zobraziť open source aplikácie Zobraziť PWA O aplikácii @@ -152,7 +152,7 @@ Takéto aplikácie sa budú aktualizovať z kategórií bežných a open source Aktualizácie aplikácií sa nebudú inštalovať automaticky Čaká sa na nemeranú sieť Aktualizácia %1$s aplikácií bola dokončená %2$s. - Nedá sa pripojiť! Skontrolujte internetové pripojenie a skúste to znova + Skontrolujte internetové pripojenie a skúste to znova. Vypršal časový limit pri načítavaní aplikácií! Niektorý sieťový problém bráni načítaniu všetkých aplikácií. @@ -202,7 +202,7 @@ Stlačte Skúsiť znova. Získava sa hodnotenie obsahu pre všetky nainštalované aplikácie. Bežné aplikácie nie sú dostupné Open Source aplikácie a PWA nie sú dostupné - Pri načítavaní bežných aplikácií došlo k chybe. Zatiaľ sú dostupné iba Open Source aplikácie a PWA. + Pri načítavaní aplikácií došlo k chybe. Zatiaľ sú dostupné iba Open Source aplikácie a Web Apps. Pri načítavaní PWA a Open Source aplikácií došlo k chybe. Zatiaľ sú dostupné iba bežné aplikácie. Aplikácia nie je dostupná Táto aplikácia nie je dostupná na inštaláciu. Zvyčajne je to spôsobené obsahovými obmedzeniami na základe vašej polohy (regiónu) alebo vekových nastavení účtu (vrátane overenia veku a vekového/obsahového hodnotenia). diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index a85f2669322d48962d83bb785a0945e476c3e029..9dfb5cc4c9cc0fb6c0b4bb2fc8abe022e1c76fc4 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -38,7 +38,7 @@ Hämta och installera appuppdateringar i bakgrunden Visa tillgängliga uppdateringar Visa appar: - Visa vanliga appar + Visa appar Om Kopierad Konto @@ -117,7 +117,7 @@ Appuppdateringar kommer installera automatiskt Appuppdateringar kommer inte att installeras automatiskt Mer info - Kan inte ansluta! Kontrollera din internetanslutning och försök igen + Kontrollera din internetanslutning och försök igen. Öppna inställningar Det anonyma kontot du använder är inte tillgängligt. Uppdatera sessionen för att få ett annat. UPPADATERA SESSIONEN @@ -194,7 +194,7 @@ Ett fel uppstod vid inläsning av PWA-appar och appar med öppen källkod. Endast vanliga appar är tillgängliga just nu. Vanliga appar är inte tillgängliga Appar med öppen källkod och PWA-appar är inte tillgängliga - Ett fil uppstod vid inläsning av vanliga appar. Endast appar med öppen källkod och PWA-appar är tillgängliga just nu. + Ett fil uppstod vid inläsning av appar. Endast appar med öppen källkod och Web Apps är tillgängliga just nu. App Lounge kommer stängas under dess uppdatering. Undvik att göra något i App Lounge till dess att uppdateringen (hämtning + installation) är färdig. Du kommer få åtkomst om några minuter. Välj hur du vill logga in diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 2baf2b1b80d5523776f74ed6ea6f0c11eed5ad88..c858b3e3f30266adde67d439676f6061c5e344be 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -26,7 +26,7 @@ Güncellemeleri otomatik olarak yükle Uygulama güncellemelerini arka planda indirin ve yükleyin Uygulamaları Göster: - Yaygın uygulamaları göster + Uygulamaları göster Açık kaynaklı uygulamaları göster PWA\'ları göster Hakkında @@ -134,7 +134,7 @@ %1$s yükleyebilmek için çok küçüksünüz. Lütfen yaş grubunuzun doğru olup olmadığını ebeveyninizle kontrol edin veya yükleyebilmek için ebeveyn kontrolünü devre dışı bırakın. Bu uygulama uygunsuz içerik içerebilir. Ücretli uygulamalar anonim modda yüklenemez. Ücretli uygulamaları yüklemek için lütfen Google hesabınıza giriş yapın. - Bağlantı kurulamıyor! Lütfen internet bağlantınızı kontrol edin ve tekrar deneyin + Lütfen internet bağlantınızı kontrol edin ve tekrar deneyin. Uygulamaları getirirken zaman aşımı! Bazı ağ sorunları tüm uygulamaların getirilmesini engelliyor. Ayarları Aç @@ -188,7 +188,7 @@ \"%1$s\" seçildiğinde tarayıcınızda uygulamanın paket adı önceden doldurulmuş bir sekme açılır.<br /><br />Exodus tarafından analizi başlatmak için \"Perform analysis\" seçeneğine tıklayın.<br /><br />\"See the report\" düğmesi göründüğünde (uygulamaya bağlı olarak biraz zaman alabilir) sekmeyi kapatıp %2$s içindeki uygulama açıklamasına geri dönebilirsiniz; burada Gizlilik Skoru\'nu görmelisiniz. Bazen Exodus uygulamayı analiz edemeyebilir.<br /><br />Not: skorun uygulama açıklamasında görünmesi 10 dakikaya kadar sürebilir. Ortak uygulamalar kullanılamıyor Açık kaynak uygulamalar ve PWA kullanılamıyor - Ortak uygulamalar yüklenirken bir hata oluştu. Şimdilik yalnızca Açık Kaynak uygulamalar ve PWA kullanılabilir. + Uygulamalar yüklenirken bir hata oluştu. Şimdilik yalnızca Açık Kaynak uygulamalar ve Web Apps kullanılabilir. PWA ve Açık Kaynak uygulamalar yüklenirken bir hata oluştu. Şimdilik yalnızca Ortak uygulamalar kullanılabilir. Uygulama kullanılamıyor Bu uygulama yükleme için kullanılabilir değil. Bu genellikle konumunuza (bölge) veya hesabın yaş ayarlarına (yaş doğrulama ve yaş/içerik derecelendirmesi dahil) dayalı içerik kısıtlamalarından kaynaklanır. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 278190640cfc7896e69303394ffc58964f945708..74712e25abf10facf5b2fedc0cfa498500be89d2 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -27,7 +27,7 @@ Про застосунок Показати прогресивні вебзастосунки Показати застосунки з відкритим кодом - Показати поширені застосунки + Показати застосунки Показати застосунки: Автоматично встановлювати оновлення Надсилати сповіщення коли доступні оновлення додатків @@ -145,7 +145,7 @@ \n \nВідкрийте налаштування, щоб перевірити опції \"Тільки застосунки з відкритим кодом\" та \"Тільки прогресивні вебзастосунки\". Час запиту на отримання застосунків вичерпано! - Неможливо приєднатись до мережі! Будь ласка, перевірте налаштування Вашої мережі та спробуйте знову + Перевірте підключення до інтернету та спробуйте ще раз. Оновлення застосунків %1$s було завершено %2$s. Це оновлення не може бути застосоване, тому що не співпадають сигнатури версії %1$s та версії, що встановлена на Вашому телефоні. Щоб виправити це, Ви можете видалити %1$s, а потім знову встановити з Магазину.

Примітка: Це повідомлення не показуватиметься знову.
Щотижня @@ -206,7 +206,7 @@ Отримання оцінки вмісту для всіх встановлених вами застосунків. Поширені застосунки недоступні Застосунки з відкритим кодом і PWA недоступні - Під час завантаження поширених застосунків сталася помилка. Наразі доступні лише застосунки з відкритим кодом і PWA. + Під час завантаження застосунків сталася помилка. Наразі доступні лише застосунки з відкритим кодом і Web Apps. Під час завантаження PWA та застосунків з відкритим кодом сталася помилка. Наразі доступні лише поширені застосунки. Застосунок недоступний Цей застосунок недоступний для встановлення. Зазвичай це пов\'язано з обмеженнями вмісту залежно від вашого місцезнаходження (регіону) або вікових налаштувань облікового запису (включно з перевіркою віку та віковим/контентним рейтингом). diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 69a761f4f7f730f54f56896be91567924358d8e6..801e2ceb4f5c360eb2a38c15aeb08a28a31d8ee4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,11 +31,18 @@ Search for an app Back Clear search - APPS - OPEN SOURCE - WEB APPS + APPS + OPEN SOURCE + WEB APPS No apps found… - Error in search + Apps temporarily unavailable + Open Source temporarily unavailable + Web Apps temporarily unavailable + We can’t reach this catalog right now. Other app sources may still work. + No results in Apps + No results in Open Source + No results in Web Apps + Other app sources may be able to provide results, please check the other tabs/your settings. Applications @@ -78,7 +85,7 @@ Show available updates Show a notification when app updates are available Show applications: - Show common apps + Show Apps Show open source apps Show PWAs About @@ -188,7 +195,8 @@ updateUnmeteredOnly updateAppsFromOtherStores - Can\'t connect! Please check your internet connection and try again + No connection + Please check your internet connection and try again. Timeout fetching applications! Some network issue is preventing fetching all applications. @@ -243,7 +251,7 @@ Common apps unavailable Open Source apps and PWA unavailable - An error occurred while loading Common apps. Only Open Source apps and PWA are available for now. + An error occurred while loading Apps. Only Open Source and Web apps are available for now. An error occurred while loading PWA and Open Source apps. Only Common apps are available for now. diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 8c002522f05ac3678b315f212b362980e1028f05..2c8baa0e91662e216830c385e5c02b3b87f69c76 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -52,8 +52,9 @@ - \ No newline at end of file + diff --git a/app/src/main/res/xml/settings_preferences.xml b/app/src/main/res/xml/settings_preferences.xml index be838f89966c426a4386e7e334f145a29ace5553..d368da48e7551d4612e5c1997be39447c81b5fcf 100644 --- a/app/src/main/res/xml/settings_preferences.xml +++ b/app/src/main/res/xml/settings_preferences.xml @@ -96,7 +96,7 @@ diff --git a/app/src/test/java/foundation/e/apps/ui/compose/state/InstallButtonStateMapperTest.kt b/app/src/test/java/foundation/e/apps/ui/compose/state/InstallButtonStateMapperTest.kt index f81b483da06ef55aca47f350604a655a95de7ef6..7e70eaf0bb0fcee01eb07f63d141c0ba966334b7 100644 --- a/app/src/test/java/foundation/e/apps/ui/compose/state/InstallButtonStateMapperTest.kt +++ b/app/src/test/java/foundation/e/apps/ui/compose/state/InstallButtonStateMapperTest.kt @@ -183,14 +183,15 @@ class InstallButtonStateMapperTest { } @Test - fun downloading_with_progress_shows_percent_and_cancel_intent() { + fun downloading_with_progress_shows_cancel_label_and_progress_fraction() { val state = mapAppToInstallState( input = defaultInput( app = baseApp(Status.DOWNLOADING), progressPercent = PROGRESS_PERCENT_VALID, ), ) - assertEquals("${PROGRESS_PERCENT_VALID}%", state.label.text) + assertEquals(R.string.cancel, state.label.resId) + assertEquals(PROGRESS_PERCENT_VALID / 100f, state.progressFraction) assertEquals(InstallButtonAction.CancelDownload, state.actionIntent) assertEquals(StatusTag.Downloading, state.statusTag) } @@ -204,6 +205,16 @@ class InstallButtonStateMapperTest { assertEquals(InstallButtonAction.CancelDownload, state.actionIntent) } + @Test + fun queued_maps_to_disabled_style() { + val state = mapAppToInstallState( + input = defaultInput(app = baseApp(Status.QUEUED)), + ) + assertEquals(R.string.cancel, state.label.resId) + assertEquals(InstallButtonStyle.Disabled, state.style) + assertTrue(state.enabled) + } + @Test fun downloading_progress_below_zero_uses_cancel_label() { val state = mapAppToInstallState( @@ -234,7 +245,7 @@ class InstallButtonStateMapperTest { input = defaultInput(app = baseApp(Status.INSTALLING)), ) assertEquals(R.string.installing, state.label.resId) - assertFalse(state.enabled) + assertTrue(state.enabled) assertEquals(StatusTag.Installing, state.statusTag) } diff --git a/app/src/test/java/foundation/e/apps/ui/search/v2/SearchViewModelV2Test.kt b/app/src/test/java/foundation/e/apps/ui/search/v2/SearchViewModelV2Test.kt index 37e54d24ce685272700fdc5ae4e200ef7259174f..f403ea96105b19d5b73fac9eba98845159c73dbe 100644 --- a/app/src/test/java/foundation/e/apps/ui/search/v2/SearchViewModelV2Test.kt +++ b/app/src/test/java/foundation/e/apps/ui/search/v2/SearchViewModelV2Test.kt @@ -206,6 +206,18 @@ class SearchViewModelV2Test { assertEquals("", state.query) } + @Test + fun `typing after submit resets hasSubmittedSearch`() = runTest { + viewModel.onSearchSubmitted("query") + assertTrue(viewModel.uiState.value.hasSubmittedSearch) + + viewModel.onQueryChanged("new") + + val state = viewModel.uiState.value + assertFalse(state.hasSubmittedSearch) + assertEquals("new", state.query) + } + @Test fun `clear query after submit retains tabs and results`() = runTest { viewModel.onSearchSubmitted("query") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7ed02cfb7f245bad38eb7628b05734c064c23286..ec1b556bcb0966e2ea48c2e5f08f6718f767a079 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ appcompat = "1.7.0" bcpgJdk15on = "1.60" coil = "1.4.0" coilCompose = "1.4.0" -composeBom = "2025.12.01" +composeBom = "2026.02.00" constraintlayout = "2.2.0" core = "1.6.1" coreKtx = "1.15.0"