Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ data class Settings(
val lastUpdateCheck: Long = 0,
val onboardingDone: Boolean = false,
val searchHistory: List<String> = emptyList(),
/** App ids the user opened, newest first. Shown at the bottom of Search. */
val recentlyViewed: List<String> = emptyList(),
)

private val Context.settingStore: DataStore<Preferences> by preferencesDataStore("gothwad_store_settings")
Expand Down Expand Up @@ -116,6 +118,7 @@ class SettingsRepository(private val context: Context) {
// set cannot do that, so the terms live in one newline separated value.
val searchHistory = stringPreferencesKey("search_history_ordered")
val searchHistoryLegacy = stringSetPreferencesKey("search_history")
val recentlyViewed = stringPreferencesKey("recently_viewed_apps")
}

private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
Expand Down Expand Up @@ -152,6 +155,7 @@ class SettingsRepository(private val context: Context) {
lastUpdateCheck = prefs[Keys.lastUpdateCheck] ?: 0L,
onboardingDone = prefs[Keys.onboarding] ?: false,
searchHistory = readSearchHistory(prefs),
recentlyViewed = readRecentlyViewed(prefs),
)
}.stateIn(scope, SharingStarted.Eagerly, null)

Expand Down Expand Up @@ -205,6 +209,30 @@ class SettingsRepository(private val context: Context) {
prefs.remove(Keys.searchHistoryLegacy)
}

/** Records an opened app id so Search can offer "Recently viewed". */
suspend fun addRecentlyViewed(appId: String) = edit { prefs ->
val clean = appId.trim()
if (clean.isEmpty()) return@edit
val current = readRecentlyViewed(prefs).toMutableList()
current.remove(clean)
current.add(0, clean)
prefs[Keys.recentlyViewed] = current.take(10).joinToString("\n")
}

suspend fun clearRecentlyViewed() = edit { prefs ->
prefs.remove(Keys.recentlyViewed)
}

/** Newest first, capped to the ten ids the UI may ever need. */
private fun readRecentlyViewed(prefs: androidx.datastore.preferences.core.Preferences): List<String> =
prefs[Keys.recentlyViewed]
?.split('\n')
?.map { it.trim() }
?.filter { it.isNotEmpty() }
?.distinct()
?.take(10)
.orEmpty()

/** Newest first; falls back to the old unordered set for existing installs. */
private fun readSearchHistory(prefs: androidx.datastore.preferences.core.Preferences): List<String> {
val ordered = prefs[Keys.searchHistory]
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/gothwad/store/ui/StoreViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ class StoreViewModel : ViewModel() {
fun openApp(id: String) {
detailJob?.cancel()
_detail.value = UiState.Loading
// Powers the "Recently viewed" shelf at the bottom of Search.
viewModelScope.launch { settings.addRecentlyViewed(id) }
detailJob = viewModelScope.launch {
_detail.value = runCatching { repository.detail(id) }.fold(
onSuccess = { detail ->
Expand Down
8 changes: 5 additions & 3 deletions app/src/main/java/com/gothwad/store/ui/components/Chrome.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
Expand Down Expand Up @@ -300,8 +299,11 @@ fun GothwadBottomBar(
Row(
modifier = Modifier
.fillMaxWidth()
// Fixed height on purpose: the tab items use fillMaxHeight(), so
// a heightIn() row would size them to the Scaffold's maximum
// (the whole screen) and the bar would cover the header/content.
.windowInsetsPadding(WindowInsets.navigationBars)
.heightIn(min = 80.dp)
.height(72.dp)
.testTag("bottom_bar"),
verticalAlignment = Alignment.CenterVertically,
) {
Expand Down Expand Up @@ -340,7 +342,7 @@ private fun BottomTabItem(
.padding(vertical = 6.dp)
.testTag("tab_${spec.route}"),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(3.dp),
verticalArrangement = Arrangement.spacedBy(3.dp, Alignment.CenterVertically),
) {
Box(
modifier = Modifier
Expand Down
14 changes: 8 additions & 6 deletions app/src/main/java/com/gothwad/store/ui/components/Common.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.CloudOff
import androidx.compose.material.icons.filled.ErrorOutline
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.InstallMobile
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Button
Expand Down Expand Up @@ -139,8 +140,8 @@ fun VerifiedBadge() = PillLabel(
@Composable
fun UpdateBadge() = PillLabel(
text = "Update",
background = PlayColors.BlueLight,
contentColor = PlayColors.Blue,
background = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
icon = Icons.Filled.InstallMobile,
)

Expand Down Expand Up @@ -242,7 +243,7 @@ fun PlayChip(
label = "chipBorder",
)
val background = animateColorAsState(
targetValue = if (selected) GothwadTheme.extra.successContainer else Color.Transparent,
targetValue = if (selected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent,
label = "chipBackground",
)
Row(
Expand Down Expand Up @@ -287,7 +288,7 @@ fun SegmentedRow(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(50))
.background(if (selected) GothwadTheme.extra.successContainer else Color.Transparent)
.background(if (selected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent)
.clickable { onSelect(index) }
.padding(vertical = 9.dp)
.then(Modifier),
Expand Down Expand Up @@ -378,15 +379,16 @@ fun InfoBanner(
action: (@Composable () -> Unit)? = null,
) {
val (container, accent) = when (tone) {
BannerTone.Info -> GothwadTheme.extra.successContainer to GothwadTheme.extra.success
BannerTone.Info -> MaterialTheme.colorScheme.primaryContainer to MaterialTheme.colorScheme.primary
BannerTone.Warning -> GothwadTheme.extra.warningContainer to GothwadTheme.extra.warning
BannerTone.Error -> PlayColors.ErrorContainer to MaterialTheme.colorScheme.error
BannerTone.Success -> GothwadTheme.extra.successContainer to GothwadTheme.extra.success
}
val icon = when (tone) {
BannerTone.Info -> Icons.Filled.Info
BannerTone.Error -> Icons.Filled.ErrorOutline
BannerTone.Warning -> Icons.Filled.CloudOff
else -> Icons.Filled.CheckCircle
BannerTone.Success -> Icons.Filled.CheckCircle
}
Row(
modifier = modifier
Expand Down
13 changes: 6 additions & 7 deletions app/src/main/java/com/gothwad/store/ui/components/Rows.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,9 @@ import com.gothwad.store.data.model.StoreApp
import com.gothwad.store.data.model.StoreRelease
import com.gothwad.store.ui.InstallAction
import com.gothwad.store.ui.theme.GothwadTheme
import com.gothwad.store.ui.theme.PlayColors

/**
* The Play-store style install control: a green rounded button that turns into a
* The Play-store style install control: a brand-blue rounded button that turns into a
* progress ring while downloading, then into "Open" once installed.
*/
@Composable
Expand All @@ -72,7 +71,7 @@ fun InstallButton(
Modifier
.size(38.dp)
.clip(CircleShape)
.background(GothwadTheme.extra.successContainer)
.background(MaterialTheme.colorScheme.primaryContainer)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center,
) {
Expand All @@ -92,7 +91,7 @@ fun InstallButton(
Modifier
.size(38.dp)
.clip(CircleShape)
.background(GothwadTheme.extra.successContainer),
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center,
) {
ProgressRing(progress = action.progress.coerceAtLeast(0.02f))
Expand All @@ -104,7 +103,7 @@ fun InstallButton(
Modifier
.size(38.dp)
.clip(CircleShape)
.background(GothwadTheme.extra.successContainer),
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center,
) {
ProgressRing(progress = 0.02f)
Expand Down Expand Up @@ -203,7 +202,7 @@ fun ProgressRing(progress: Float, modifier: Modifier = Modifier) {
progress = { progress.coerceIn(0f, 1f) },
modifier = modifier.size(38.dp),
color = MaterialTheme.colorScheme.primary,
trackColor = GothwadTheme.extra.successContainer,
trackColor = MaterialTheme.colorScheme.primaryContainer,
strokeWidth = 2.5.dp,
)
}
Expand Down Expand Up @@ -432,7 +431,7 @@ fun BannerCard(
.clip(RoundedCornerShape(18.dp))
.background(
Brush.linearGradient(
listOf(PlayColors.BannerStart, PlayColors.BannerEnd),
listOf(GothwadTheme.extra.bannerStart, GothwadTheme.extra.bannerEnd),
),
)
.padding(18.dp),
Expand Down
24 changes: 13 additions & 11 deletions app/src/main/java/com/gothwad/store/ui/navigation/AppNavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ import com.gothwad.store.ui.screens.WishlistScreen
import com.gothwad.store.ui.screens.WriteReviewScreen

/**
* One NavHost for the whole app. The four tab destinations render the shared
* Play-style bottom bar; every other destination is pushed with a back arrow.
* One NavHost for the whole app. The five tab destinations (Home, Apps,
* Search, Games, Profile) render the shared bottom bar; every other
* destination is pushed as a sub-screen with a back arrow.
*/
@Composable
fun AppNavHost(
Expand All @@ -87,14 +88,13 @@ fun AppNavHost(
val currentRoute = backStackEntry?.destination?.route.orEmpty()

val onTab: (String) -> Unit = { route ->
if (route == Routes.SEARCH) {
navController.navigate(Routes.SEARCH) { launchSingleTop = true }
} else {
navController.navigate(route) {
popUpTo(Routes.HOME) { saveState = true }
launchSingleTop = true
restoreState = true
}
// Every tab (Search included) is a top-level destination, so they all
// use the same save/restore rules. Search used to be special-cased and
// was not wrapped in TabShell, which is why it had no bottom bar.
navController.navigate(route) {
popUpTo(Routes.HOME) { saveState = true }
launchSingleTop = true
restoreState = true
}
}

Expand Down Expand Up @@ -124,7 +124,9 @@ fun AppNavHost(
composable(Routes.ACCOUNT) {
TabShell(currentRoute, onTab, snackbarHostState) { AccountScreen(vm, navController) }
}
composable(Routes.SEARCH) { SearchScreen(vm, navController) }
composable(Routes.SEARCH) {
TabShell(currentRoute, onTab, snackbarHostState) { SearchScreen(vm, navController) }
}

// ── Catalogue ───────────────────────────────────────────────────
composable(
Expand Down
Loading
Loading