From dd4c4a294ca6000ddc02290986b33bfa01467376 Mon Sep 17 00:00:00 2001 From: The412Banner Date: Thu, 8 Oct 2026 20:20:02 -0400 Subject: [PATCH] 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. --- .../droiddeck/launcher/ui/FrontEndGames.kt | 8 +- .../droiddeck/launcher/ui/FrontEndSetup.kt | 6 +- .../droiddeck/launcher/ui/StoresDownloads.kt | 7 +- .../launcher/ui/StoresInstallDialog.kt | 7 +- .../com/droiddeck/launcher/ui/StoresPage.kt | 101 ++++++------------ .../droiddeck/launcher/ui/StoresWidgets.kt | 3 +- app/src/main/res/values/strings.xml | 25 +---- docs/progress-log.md | 12 +++ 8 files changed, 66 insertions(+), 103 deletions(-) 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