diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndGames.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndGames.kt
index 60a325541..a309e012b 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndGames.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndGames.kt
@@ -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 -> ""
}
/**
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt
index e59de4b68..b7dcda7b1 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt
@@ -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()
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt
index d1fd5d8a4..491e7afe5 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt
@@ -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,
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt
index 140a59c6d..db61b6d07 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt
@@ -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)
}
}
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt
index b0d61149e..b859f0444 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt
@@ -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
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresWidgets.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresWidgets.kt
index ad6267f5b..b6d15dc61 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/StoresWidgets.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresWidgets.kt
@@ -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)
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bef0ecc41..a69150d80 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1966,14 +1966,9 @@
Custom
Stores
Show Stores in the rail
- GOG, Epic Games and Amazon Games libraries, with the download manager.
Open a store on
- Which tab a signed-in store opens when its chip is picked.
Download speed tier
- Also in Stores › Downloads.
Stores engine
- Native download engine %1$s.
- Stores engine not built: downloads run on the slower built-in fetcher.
● Engine ready
Not built
Balanced
@@ -1989,23 +1984,17 @@
All
Downloads
%1$s (%2$d)
- Sign in to %1$s
- GOG account (same login as GOG Galaxy). Opens GOG\'s own sign-in page.
- Epic account (web sign-in). Opens Epic\'s own sign-in page.
- Amazon account (device sign-in). Opens Amazon\'s own sign-in page.
Sign in
- Tokens stay in DroidDeck\'s private storage and are refreshed by the engine; nothing goes through a third party.
- This build has no native download engine: installs still work, on the slower built-in fetcher.
%1$s · sign out
Search the %1$s catalog…
Search installed games…
Search your library…
Search everything on %1$s…
- Nothing installed from %1$s yet. Install from Library.
+ Nothing installed
Fetching your library…
- Nothing matches.
+ No games
Loading the store…
- Amazon has no browsable catalog: claim games on Prime Gaming in a browser; they show up in Library after a sync.
+ No catalog
What\'s new
Just landed
Deals
@@ -2026,6 +2015,7 @@
Installed
In library
Free
+ Resume
installed
in your library
store
@@ -2053,7 +2043,7 @@
Steam and Proton · Amazon Games Services variables set
Steam
added to Steam on install (Deck UI + Steam Input)
- No downloads. Install something from a library.
+ No downloads
Paused
Queued
Queued · #%1$d
@@ -2073,17 +2063,12 @@
Install
Done
Download manager
- Caps how many chunks the engine fetches at once: Balanced 16 · Fast 32 · Max 96 in flight.
Downloads at a time
Install to
- A card installs more slowly.
Internal
%1$s free
- Balanced / Fast / Max pull harder from the store; 2–3 at a time share the link. Speed shown is the install rate.
- One manager for all three stores; Steam\'s own downloads are unaffected.
Engine log
idle
- Stages mirror the engine: manifest → chunks → verify → install. A paused download keeps its files and carries on where it stopped.
Store downloads
Shows game download progress and keeps downloads running in the background
Downloading
diff --git a/docs/progress-log.md b/docs/progress-log.md
index 4327a84cf..cef1ae0a0 100644
--- a/docs/progress-log.md
+++ b/docs/progress-log.md
@@ -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