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.
This commit is contained in:
The412Banner committed 2026-10-08 22:22:28 -04:00
1 parent e835f5c523
commit 1379396b99
4 files changed
+50 -3

No files matched your search

@@ -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<InstalledStoreGame>, library: List<CatalogItem>): List<CatalogItem> {
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}"
}
@@ -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),
),
@@ -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)
}
}
+3
View File
@@ -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