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