Stores: cards are art and one line; no flavor text

A store card is its art and one line: the title, and right-aligned the
one value the tab does not already say - a check when installed, Resume,
the download percentage, the install size, or Free / price / discount -
with a thin bar over the art while downloading. The source chip, the In
library chip and the card's button are gone; every action lives on the
game page, which A on a card opens.

Setup > Stores and the cog popup keep only their control labels; the
download-controls caption, the Downloads footnote, the sign-in blurbs and
footnote, the engine note and the Install-to caption are gone, and empty
states are a few words. The Install-to dialog is the game's name over
"Install to" and the two tiles. A Custom game's hero has no eyebrow; its
chip says it.
This commit is contained in:
The412Banner committed 2026-10-08 20:20:02 -04:00
1 parent a0bd480576
commit dd4c4a294c
8 files changed
+66 -103

No files matched your search

@@ -222,8 +222,10 @@ private fun ManageSaves(g: Library.SteamGame, prefix: java.io.File, a: FrontEndA
private fun ColumnScope.GameHeroCopy(g: Library.SteamGame, titleSize: androidx.compose.ui.unit.TextUnit) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
SourceChip(g.source)
Text(
(lastPlayedText(g.lastPlayed) ?: libraryLabel(g)).uppercase(),
// A Custom game's chip already says what its eyebrow would; the line then stays empty.
val eyebrow = lastPlayedText(g.lastPlayed) ?: libraryLabel(g)
if (eyebrow.isNotEmpty()) Text(
eyebrow.uppercase(),
fontSize = 12.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 1.2.sp, color = LocalPalette.current.signal,
maxLines = 1, overflow = TextOverflow.Ellipsis,
)
@@ -344,7 +346,7 @@ private fun playedSpan(lastPlayed: Long): String? {
private fun libraryLabel(g: Library.SteamGame): String = when {
g.library != Library.ADDED -> if (g.library == "internal") stringResource(R.string.games_internal) else g.library
g.source != Library.ADDED -> stringResource(R.string.games_store_storage)
else -> stringResource(R.string.games_custom)
else -> ""
}
/**
@@ -371,16 +371,16 @@ internal fun SetupPanel(
SettingsGroup(stringResource(R.string.setup_stores)) {
ToggleRow(
host, "stores-enabled", stringResource(R.string.setup_stores_show),
stringResource(R.string.setup_stores_show_hint), s.gameStoresEnabled,
null, s.gameStoresEnabled,
) { a.onGameStoresEnabled(it) }
// The same rows as the Stores page's own cog, for whoever looks here first.
SettingsRow(stringResource(R.string.setup_stores_open_on), stringResource(R.string.setup_stores_open_on_hint)) {
SettingsRow(stringResource(R.string.setup_stores_open_on), null) {
SegmentedTabs(
listOf(SessionPrefs.STORES_OPEN_LIBRARY to stringResource(R.string.stores_tab_library), SessionPrefs.STORES_OPEN_STORE to stringResource(R.string.stores_tab_store)),
s.storesOpenTab,
) { a.onStoresOpenTab(it) }
}
SettingsRow(stringResource(R.string.setup_stores_speed), stringResource(R.string.setup_stores_speed_hint)) {
SettingsRow(stringResource(R.string.setup_stores_speed), null) {
SegmentedTabs(StoreDownloadTier.ALL.map { it.id to stringResource(it.label) }, s.gameStoresSpeedTier) { a.onGameStoresSpeedTier(it) }
}
StoresEngineRow()
@@ -187,13 +187,11 @@ private fun DownloadSettings(s: FrontEndState, a: FrontEndActions) {
fontSize = 11.sp, lineHeight = 16.sp, fontFamily = FontFamily.Monospace, color = Color(0xFF9BE27A),
)
}
Text(stringResource(R.string.stores_downloads_footnote), fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 12.dp))
}
/**
* The queue's two knobs on one line - the speed tier on the left, downloads at a time on the
* right - with one caption under them; the same control sits in the cog's popup. A narrow page
* puts the two controls on two tight lines.
* right; the same control sits in the cog's popup. A narrow page puts them on two tight lines.
*/
@Composable
internal fun DownloadControls(s: FrontEndState, a: FrontEndActions) {
@@ -216,7 +214,6 @@ internal fun DownloadControls(s: FrontEndState, a: FrontEndActions) {
if (maxWidth >= 460.dp) Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.Bottom) { tier(); count() }
else Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { tier(); count() }
}
Text(stringResource(R.string.stores_download_controls_note), fontSize = 12.sp, color = colors.onSurfaceVariant)
}
/**
@@ -232,7 +229,7 @@ internal fun StoresSettingsDialog(s: FrontEndState, a: FrontEndActions, onDismis
DialogHeader(stringResource(R.string.stores_settings_eyebrow), stringResource(R.string.stores_title))
Rise(1) {
Column {
SettingsRow(stringResource(R.string.setup_stores_open_on), stringResource(R.string.setup_stores_open_on_hint)) {
SettingsRow(stringResource(R.string.setup_stores_open_on), null) {
SegmentedTabs(
listOf(SessionPrefs.STORES_OPEN_LIBRARY to stringResource(R.string.stores_tab_library), SessionPrefs.STORES_OPEN_STORE to stringResource(R.string.stores_tab_store)),
s.storesOpenTab,
@@ -42,7 +42,7 @@ import com.droiddeck.launcher.stores.formatBytes
/**
* "Install to": a small card with one tile per place - internal storage, the card - each the
* action itself, with its free space. Asked only when a card is there. The tile picked last time
* action itself: icon, name, free space. Asked only when a card is there. The tile picked last time
* starts focused, so a pad confirms the usual place with one press; nothing is chosen for the user.
*/
@Composable
@@ -60,8 +60,8 @@ internal fun InstallWhereDialog(item: CatalogItem, onDismiss: () -> Unit) {
Rise(0) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
Column(Modifier.weight(1f)) {
Eyebrow(stringResource(R.string.stores_install_where))
Text(item.title, fontSize = 18.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
Eyebrow(item.title)
Text(stringResource(R.string.stores_install_where), fontSize = 18.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Text(
stringResource(R.string.common_cancel), fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = colors.onSurfaceVariant,
@@ -84,7 +84,6 @@ internal fun InstallWhereDialog(item: CatalogItem, onDismiss: () -> Unit) {
}
}
}
Text(stringResource(R.string.stores_install_where_note), fontSize = 12.sp, color = colors.onSurfaceVariant)
}
}
@@ -30,6 +30,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.outlined.Download
@@ -240,11 +241,6 @@ private fun SignInCard(store: Store) {
val colors = MaterialTheme.colorScheme
val pal = LocalPalette.current
val narrow = LocalNarrowPane.current
val hint = when (store) {
Store.GOG -> stringResource(R.string.stores_signin_hint_gog)
Store.EPIC -> stringResource(R.string.stores_signin_hint_epic)
Store.AMAZON -> stringResource(R.string.stores_signin_hint_amazon)
}
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(18.dp),
@@ -252,14 +248,11 @@ private fun SignInCard(store: Store) {
) {
StoreLogo(store, 56.dp)
Column(modifier = Modifier.weight(1f)) {
Text(stringResource(R.string.stores_signin_title, store.label), fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground)
Text(hint, fontSize = 13.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 2.dp))
Text(store.label, fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground)
}
if (!narrow) PrimaryButton(stringResource(R.string.stores_signin), main = true) { StoresState.signIn(ctx, store) }
}
if (narrow) Actions { PrimaryButton(stringResource(R.string.stores_signin), main = true) { StoresState.signIn(ctx, store) } }
Text(stringResource(R.string.stores_signin_footnote), fontSize = 12.sp, color = colors.onSurfaceVariant)
if (StoresState.engine == "") Note(stringResource(R.string.stores_engine_missing_note))
}
}
@@ -325,7 +318,7 @@ private fun Storefront(
GameCard(item, store, s, a, installedKeys, onOpen, first)
}
when (tab) {
"installed" -> Grid(installedItems.filter(::matches), stringResource(R.string.stores_installed_empty, store.label), card)
"installed" -> Grid(installedItems.filter(::matches), stringResource(R.string.stores_installed_empty), card)
"library" -> Grid(library.filter(::matches), if (library.isEmpty() && StoresState.status[store] != null) stringResource(R.string.stores_library_loading) else stringResource(R.string.stores_nothing_matches), card)
"all" -> Grid(everything.filter(::matches), stringResource(R.string.stores_nothing_matches), card)
else -> {
@@ -451,22 +444,40 @@ private fun GameCard(item: CatalogItem, store: Store, s: FrontEndState, a: Front
.clip(RoundedCornerShape(10.dp)).background(colors.surface).glideBorder(hot, RoundedCornerShape(10.dp), pal.signal, pal.line)
.hoverable(src).clickable(interactionSource = src, indication = LocalIndication.current, onClick = open).controllerConfirm(onClick = open),
) {
CardArt(item, Modifier.fillMaxWidth().aspectRatio(16f / 9f))
Column(verticalArrangement = Arrangement.spacedBy(5.dp), modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 6.dp, bottom = 8.dp)) {
Text(item.title, fontSize = 11.sp, lineHeight = 13.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, maxLines = 2, minLines = 2, overflow = TextOverflow.Ellipsis)
PriceLine(item, installed)
when {
installed -> CardButton(stringResource(R.string.stores_play), primary = true, icon = Icons.Filled.PlayArrow) { launchStoreGame(ctx, store, item.id, s, a) }
download != null -> {
ProgressBarThin(download.fraction, paused = download.state == DownloadState.PAUSED, verify = download.stage == DownloadStage.VERIFY)
CardButton(downloadLabel(download.state, download.stage, download.percent), primary = false) { }
}
item.owned && StoresState.isUnfinished(item) -> CardButton(stringResource(R.string.stores_resume_install), primary = true) { StoresState.requestInstall(ctx, item) }
item.owned -> CardButton(if (item.sizeBytes > 0) stringResource(R.string.stores_install_size, formatBytes(item.sizeBytes)) else stringResource(R.string.stores_install), primary = true) { StoresState.requestInstall(ctx, item) }
item.isFree -> CardButton(stringResource(R.string.stores_get_free), primary = true) { openStoreUrl(ctx, item) }
else -> CardButton(stringResource(R.string.stores_view_on, store.shortLabel), primary = false) { openStoreUrl(ctx, item) }
Box {
CardArt(item, Modifier.fillMaxWidth().aspectRatio(16f / 9f))
if (download != null) Box(Modifier.align(Alignment.BottomCenter).padding(horizontal = 6.dp, vertical = 5.dp)) {
ProgressBarThin(download.fraction, paused = download.state == DownloadState.PAUSED, verify = download.stage == DownloadStage.VERIFY)
}
}
// One line: the title, and on the right the one thing the tab does not already say.
Row(
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 7.dp),
) {
Text(item.title, fontSize = 11.sp, lineHeight = 13.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f))
CardMarker(item, installed, download)
}
}
}
/** The card's right-hand value: done, resume, progress, size, or the price. */
@Composable
private fun CardMarker(item: CatalogItem, installed: Boolean, download: com.droiddeck.launcher.stores.download.DownloadEntry?) {
val colors = MaterialTheme.colorScheme
val pal = LocalPalette.current
val small = 10.sp
when {
installed -> Icon(Icons.Filled.Check, stringResource(R.string.stores_installed_chip), tint = pal.good, modifier = Modifier.size(14.dp))
download != null -> Text("${download.percent}%", fontSize = small, fontWeight = FontWeight.Bold, color = pal.signal, maxLines = 1)
item.owned && StoresState.isUnfinished(item) -> Text(stringResource(R.string.stores_resume), fontSize = small, fontWeight = FontWeight.Bold, color = pal.signal, maxLines = 1)
item.owned -> if (item.sizeBytes > 0) Text(formatBytes(item.sizeBytes), fontSize = small, color = colors.onSurfaceVariant, maxLines = 1)
item.isFree -> Text(stringResource(R.string.stores_free), fontSize = small, fontWeight = FontWeight.Bold, color = pal.good, maxLines = 1)
item.isDiscounted -> Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
StateChip("-${item.discountPercent}%", ChipTone.DEAL, small = true)
Text(item.finalPrice, fontSize = small, fontWeight = FontWeight.Bold, color = colors.onBackground, maxLines = 1)
}
item.hasPrice -> Text(item.finalPrice, fontSize = small, fontWeight = FontWeight.Bold, color = colors.onBackground, maxLines = 1)
}
}
@@ -483,48 +494,6 @@ internal fun CardArt(item: CatalogItem, modifier: Modifier) {
}
}
/** "In library" / "Installed", a free label, or the deal with its old price struck through. */
@Composable
internal fun PriceLine(item: CatalogItem, installed: Boolean, small: Boolean = true) {
val colors = MaterialTheme.colorScheme
val pal = LocalPalette.current
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
when {
installed -> StateChip(stringResource(R.string.stores_installed_chip), ChipTone.OK, small)
item.owned -> StateChip(stringResource(R.string.stores_in_library), ChipTone.OK, small)
item.isFree -> Text(stringResource(R.string.stores_free), fontSize = if (small) 11.sp else 13.sp, fontWeight = FontWeight.Bold, color = pal.good)
item.isDiscounted -> {
StateChip("-${item.discountPercent}%", ChipTone.DEAL, small)
Text(item.originalPrice, fontSize = if (small) 10.sp else 12.sp, color = colors.onSurfaceVariant, textDecoration = TextDecoration.LineThrough, maxLines = 1)
Text(item.finalPrice, fontSize = if (small) 11.sp else 13.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, maxLines = 1)
}
item.hasPrice -> Text(item.finalPrice, fontSize = if (small) 11.sp else 13.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, maxLines = 1)
else -> Spacer(Modifier.height(if (small) 14.dp else 18.dp))
}
}
}
/** The card's one button: full width, small type. A press does not also open the card. */
@Composable
private fun CardButton(text: String, primary: Boolean, icon: androidx.compose.ui.graphics.vector.ImageVector? = null, onClick: () -> Unit) {
val colors = MaterialTheme.colorScheme
val pal = LocalPalette.current
val src = remember { MutableInteractionSource() }
val hot = rememberHot(src)
val shape = RoundedCornerShape(6.dp)
Row(
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
modifier = Modifier.fillMaxWidth().heightIn(min = 30.dp).clip(shape)
.background(if (primary) pal.signal else colors.surfaceVariant)
.glideBorder(hot, shape, if (primary) colors.onBackground else pal.signal, if (primary) Color.Transparent else pal.line)
.hoverable(src).clickable(interactionSource = src, indication = LocalIndication.current, role = Role.Button, onClick = onClick)
.controllerConfirm(onClick = onClick).padding(horizontal = 6.dp, vertical = 5.dp),
) {
if (icon != null) Icon(icon, null, tint = if (primary) pal.onSignal else colors.onBackground, modifier = Modifier.size(12.dp))
Text(text, fontSize = 10.sp, fontWeight = FontWeight.Bold, color = if (primary) pal.onSignal else colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
@Composable
internal fun ProgressBarThin(fraction: Float, paused: Boolean = false, verify: Boolean = false, height: Dp = 4.dp, done: Boolean = false) {
val pal = LocalPalette.current
@@ -13,8 +13,7 @@ internal fun StoresEngineRow() {
val version = StoresNative.version
SettingsRow(
stringResource(R.string.setup_stores_engine),
if (version != null) stringResource(R.string.setup_stores_engine_ready, version)
else stringResource(R.string.setup_stores_engine_missing),
version,
) {
Chip(if (version != null) stringResource(R.string.setup_stores_engine_chip_ready) else stringResource(R.string.setup_stores_engine_chip_missing), ok = version != null)
}
+5 -20
View File
@@ -1966,14 +1966,9 @@
<string name="source_added">Custom</string>
<string name="setup_stores">Stores</string>
<string name="setup_stores_show">Show Stores in the rail</string>
<string name="setup_stores_show_hint">GOG, Epic Games and Amazon Games libraries, with the download manager.</string>
<string name="setup_stores_open_on">Open a store on</string>
<string name="setup_stores_open_on_hint">Which tab a signed-in store opens when its chip is picked.</string>
<string name="setup_stores_speed">Download speed tier</string>
<string name="setup_stores_speed_hint">Also in Stores › Downloads.</string>
<string name="setup_stores_engine">Stores engine</string>
<string name="setup_stores_engine_ready">Native download engine %1$s.</string>
<string name="setup_stores_engine_missing">Stores engine not built: downloads run on the slower built-in fetcher.</string>
<string name="setup_stores_engine_chip_ready">● Engine ready</string>
<string name="setup_stores_engine_chip_missing">Not built</string>
<string name="stores_tier_balanced">Balanced</string>
@@ -1989,23 +1984,17 @@
<string name="stores_tab_all">All</string>
<string name="stores_tab_downloads">Downloads</string>
<string name="stores_tab_count">%1$s (%2$d)</string>
<string name="stores_signin_title">Sign in to %1$s</string>
<string name="stores_signin_hint_gog">GOG account (same login as GOG Galaxy). Opens GOG\'s own sign-in page.</string>
<string name="stores_signin_hint_epic">Epic account (web sign-in). Opens Epic\'s own sign-in page.</string>
<string name="stores_signin_hint_amazon">Amazon account (device sign-in). Opens Amazon\'s own sign-in page.</string>
<string name="stores_signin">Sign in</string>
<string name="stores_signin_footnote">Tokens stay in DroidDeck\'s private storage and are refreshed by the engine; nothing goes through a third party.</string>
<string name="stores_engine_missing_note">This build has no native download engine: installs still work, on the slower built-in fetcher.</string>
<string name="stores_signout_named">%1$s · sign out</string>
<string name="stores_search_catalog">Search the %1$s catalog…</string>
<string name="stores_search_installed">Search installed games…</string>
<string name="stores_search_library">Search your library…</string>
<string name="stores_search_all">Search everything on %1$s…</string>
<string name="stores_installed_empty">Nothing installed from %1$s yet. Install from Library.</string>
<string name="stores_installed_empty">Nothing installed</string>
<string name="stores_library_loading">Fetching your library…</string>
<string name="stores_nothing_matches">Nothing matches.</string>
<string name="stores_nothing_matches">No games</string>
<string name="stores_shelves_loading">Loading the store…</string>
<string name="stores_amazon_no_catalog">Amazon has no browsable catalog: claim games on Prime Gaming in a browser; they show up in Library after a sync.</string>
<string name="stores_amazon_no_catalog">No catalog</string>
<string name="stores_shelf_new">What\'s new</string>
<string name="stores_shelf_new_hint">Just landed</string>
<string name="stores_shelf_deals">Deals</string>
@@ -2026,6 +2015,7 @@
<string name="stores_installed_chip">Installed</string>
<string name="stores_in_library">In library</string>
<string name="stores_free">Free</string>
<string name="stores_resume">Resume</string>
<string name="stores_eyebrow_installed">installed</string>
<string name="stores_eyebrow_library">in your library</string>
<string name="stores_eyebrow_store">store</string>
@@ -2053,7 +2043,7 @@
<string name="stores_kv_launched_by_amazon">Steam and Proton · Amazon Games Services variables set</string>
<string name="stores_kv_steam">Steam</string>
<string name="stores_kv_steam_on">added to Steam on install (Deck UI + Steam Input)</string>
<string name="stores_downloads_empty">No downloads. Install something from a library.</string>
<string name="stores_downloads_empty">No downloads</string>
<string name="stores_dl_paused">Paused</string>
<string name="stores_dl_queued">Queued</string>
<string name="stores_dl_queued_at">Queued · #%1$d</string>
@@ -2073,17 +2063,12 @@
<string name="stores_stage_install">Install</string>
<string name="stores_stage_done">Done</string>
<string name="stores_download_manager">Download manager</string>
<string name="stores_tier_note">Caps how many chunks the engine fetches at once: Balanced 16 · Fast 32 · Max 96 in flight.</string>
<string name="stores_parallel">Downloads at a time</string>
<string name="stores_install_where">Install to</string>
<string name="stores_install_where_note">A card installs more slowly.</string>
<string name="stores_target_internal">Internal</string>
<string name="stores_target_free">%1$s free</string>
<string name="stores_download_controls_note">Balanced / Fast / Max pull harder from the store; 2–3 at a time share the link. Speed shown is the install rate.</string>
<string name="stores_parallel_note">One manager for all three stores; Steam\'s own downloads are unaffected.</string>
<string name="stores_engine_log">Engine log</string>
<string name="stores_log_idle">idle</string>
<string name="stores_downloads_footnote">Stages mirror the engine: manifest → chunks → verify → install. A paused download keeps its files and carries on where it stopped.</string>
<string name="stores_notification_channel">Store downloads</string>
<string name="stores_notification_channel_hint">Shows game download progress and keeps downloads running in the background</string>
<string name="stores_notification_one">Downloading</string>
+12
View File
@@ -131,6 +131,18 @@ without it and Setup says "Stores engine not built").
`nativeAssemble` both).
- **Install-to dialog compacted** (440 dp, two tiles that are the action, one-line caption, Cancel
in the title row); the cog popup is capped at 440 dp too.
- **Store cards are art + one line** (maintainer feedback): no source chip (the tab says the
store), no "In library" chip, no action button. The title sits left; right-aligned is a check
when installed, Resume, the download percentage, the install size, or Free / price / discount.
A thin bar over the art while downloading. Install, Resume, Uninstall, Get and Buy live on the
game page; A on a card opens it.
- **No flavor text** (standing rule from the maintainer): Setup › Stores and the cog popup keep
only control labels; the download-controls caption, the Downloads footnote, the sign-in blurbs
and footnote, the engine note and the Install-to caption are gone; empty states read "No games",
"Nothing installed", "No downloads", "No catalog". A Custom game's hero has no eyebrow (its chip
says it).
- **Logos reverted** (user decision, no third-party marks): back to the text source chips and the
coloured dots.
- **Device-proven on the AYANEO Pocket FIT, 2026-10-08 (GOG):** sign-in, the library (33 owned),
DOOM I Enhanced installed through the GOG engine to the SD Games root, shown in Games with the
GOG chip and real art, registered as a non-Steam game in the Steam client (grid art present), and