From 1379396b99ae82e04fb7c77b8a88cb303c2eddaf Mon Sep 17 00:00:00 2001 From: The412Banner Date: Thu, 8 Oct 2026 22:22:28 -0400 Subject: [PATCH] Stores: the Installed tab draws what its count counts The tab's count came from the installs on disk and its grid from the library filtered by id, so two installs could read Installed (2) over an empty grid. Both now come from installedCards: each install joined to its library item by store id, or by title when the ids differ, else a card made from the sidecar's own title and art. --- .../droiddeck/launcher/stores/StoreCatalog.kt | 15 ++++++++++ .../com/droiddeck/launcher/ui/StoresPage.kt | 7 +++-- .../launcher/stores/InstalledCardsTest.kt | 28 +++++++++++++++++++ docs/progress-log.md | 3 ++ 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 app/src/test/java/com/droiddeck/launcher/stores/InstalledCardsTest.kt diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreCatalog.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreCatalog.kt index 96320d7b0..79d3bb56b 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreCatalog.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreCatalog.kt @@ -52,6 +52,21 @@ class StoreShelves( } /** A game a store installed, as found on disk through its sidecar. */ +/** + * The Installed tab's cards for [store], from the installs on disk - the same source as its count: + * each joined to its library item by store id (by title when the ids differ), else a card made + * from the sidecar's own title and art, so an install shows even before the library has loaded. + */ +fun installedCards(store: Store, installed: List, library: List): List { + val byId = library.associateBy { it.id } + val byTitle = library.associateBy { it.title.lowercase() } + return installed.filter { it.sidecar.store == store }.map { game -> + val s = game.sidecar + byId[s.id] ?: byTitle[s.title.lowercase()] + ?: CatalogItem(store, s.id, s.title, imageUrl = s.hero ?: s.cover, tallImageUrl = s.cover ?: s.hero, owned = true) + }.distinctBy { it.id }.sortedBy { it.title.lowercase() } +} + class InstalledStoreGame(val sidecar: StoreGameSidecar, val folder: java.io.File) { val key: String get() = "${sidecar.store.id}:${sidecar.id}" } 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 54b60f3c0..798f739f3 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt @@ -280,17 +280,18 @@ private fun Storefront( val colors = MaterialTheme.colorScheme val library = StoresState.library[store] ?: emptyList() val shelves = StoresState.shelves[store] - val installedKeys = remember(StoresState.installed) { StoresState.installed.filter { it.sidecar.store == store }.map { it.sidecar.id }.toSet() } + // The Installed tab, its count and every card's installed mark come from the installs on disk. + val installedItems = remember(StoresState.installed, library) { com.droiddeck.launcher.stores.installedCards(store, StoresState.installed, library) } + val installedKeys = remember(installedItems) { installedItems.map { it.id }.toSet() + StoresState.installed.filter { it.sidecar.store == store }.map { it.sidecar.id } } val q = query.trim().lowercase() fun matches(i: CatalogItem) = q.isEmpty() || i.title.lowercase().contains(q) // Everything this store knows about: the library plus whatever the shelves brought, each title once. val everything = remember(library, shelves) { (library + (shelves?.all ?: emptyList())).distinctBy { it.id } } - val installedItems = library.filter { it.id in installedKeys } Rise(1) { SubTabs( listOf( "store" to stringResource(R.string.stores_tab_store), - "installed" to stringResource(R.string.stores_tab_count, stringResource(R.string.stores_tab_installed), installedKeys.size), + "installed" to stringResource(R.string.stores_tab_count, stringResource(R.string.stores_tab_installed), installedItems.size), "library" to stringResource(R.string.stores_tab_count, stringResource(R.string.stores_tab_library), library.size), "all" to stringResource(R.string.stores_tab_count, stringResource(R.string.stores_tab_all), everything.size), ), diff --git a/app/src/test/java/com/droiddeck/launcher/stores/InstalledCardsTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/InstalledCardsTest.kt new file mode 100644 index 000000000..090280f77 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/stores/InstalledCardsTest.kt @@ -0,0 +1,28 @@ +package com.droiddeck.launcher.stores + +import java.io.File +import org.junit.Assert.assertEquals +import org.junit.Test + +/** The Installed tab draws what its count counts: the installs on disk. */ +class InstalledCardsTest { + private fun install(store: Store, id: String, title: String) = + InstalledStoreGame(StoreGameSidecar(store, id, title, "game.exe", cover = "https://img/$id.jpg"), File("/x/$id")) + + @Test fun everyInstallIsACardWhetherOrNotTheLibraryHasLoaded() { + val installed = listOf(install(Store.GOG, "1", "DOOM + DOOM II"), install(Store.GOG, "2", "DOOM I Enhanced"), install(Store.EPIC, "e", "DOOMBLADE")) + // Library not loaded: cards from the sidecars. + val bare = installedCards(Store.GOG, installed, emptyList()) + assertEquals(listOf("DOOM + DOOM II", "DOOM I Enhanced"), bare.map { it.title }) + assertEquals("https://img/1.jpg", bare.first().tallImageUrl) + // Loaded: the library item wins, by id, or by title when the ids differ. + val library = listOf( + CatalogItem(Store.GOG, "1", "DOOM + DOOM II", imageUrl = "wide1", owned = true), + CatalogItem(Store.GOG, "999", "DOOM I Enhanced", imageUrl = "wide2", owned = true), + CatalogItem(Store.GOG, "3", "Not installed", imageUrl = null, owned = true), + ) + val joined = installedCards(Store.GOG, installed, library) + assertEquals(listOf("wide1", "wide2"), joined.map { it.imageUrl }) + assertEquals(1, installedCards(Store.EPIC, installed, library).size) + } +} diff --git a/docs/progress-log.md b/docs/progress-log.md index ba7e529f9..668d367bc 100644 --- a/docs/progress-log.md +++ b/docs/progress-log.md @@ -144,6 +144,9 @@ without it and Setup says "Stores engine not built"). (`token`, `__token__`, `f_token`, `hdnts`, access/refresh/id tokens, `code=`) and Authorization / Cookie headers are blanked outside URLs too. `StoreLogTest` covers a GOG secure link and an Epic Akamai URL. The engine's own logcat lines (tag `EpicNative` etc.) are written on the native side. +- **Installed tab empty under "Installed (2)" (device):** the count came from the installs on disk, + the grid from the library filtered by id. Both now come from `installedCards`: each install joined + to its library item by id (or title), else a card from the sidecar's title and art. - **GOG rows (device, DOOM + DOOM II):** bytes read "0 B" instead of blank; GOG and Amazon report their size on disk from the manifest; their Install stage counts its finishing steps (record and launcher, art, Steam) instead of sitting empty - GOG writes files in place while downloading, so