mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 11:01:07 +00:00
Stores: every owned card shows its size
Epic's and Amazon's library answers carry no size, so their cards showed none. A card on screen, or close to it, now has its size looked up once - Epic's from the build manifest (the files this device installs), Amazon's from its download manifest - three at a time in the background, and kept in the store's prefs where the library reads it. The size sits at the right of the title strip in the title's own type; nothing shows until it is known, and a marker keeps the slot.
This commit is contained in:
1 parent
f5c343bde0
commit
3c64c6b3b8
3 files changed
+94
-2
No files matched your search
@@ -0,0 +1,74 @@
|
||||
package com.droiddeck.launcher.stores
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import com.droiddeck.launcher.stores.amazon.AmazonApiClient
|
||||
import com.droiddeck.launcher.stores.amazon.AmazonCredentialStore
|
||||
import com.droiddeck.launcher.stores.amazon.AmazonManifest
|
||||
import com.droiddeck.launcher.stores.amazon.AmazonPrefs
|
||||
import com.droiddeck.launcher.stores.epic.EpicApiClient
|
||||
import com.droiddeck.launcher.stores.epic.EpicCredentialStore
|
||||
import com.droiddeck.launcher.stores.epic.EpicDownloadManager
|
||||
import com.droiddeck.launcher.stores.epic.EpicInstallTags
|
||||
import com.droiddeck.launcher.stores.epic.EpicPrefs
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Install sizes for owned Epic and Amazon games, whose library answers carry none: Epic's from the
|
||||
* game's build manifest (the files this device installs), Amazon's from its download manifest.
|
||||
* Asked for a card as it comes on screen, three at a time in the background, once per game - the
|
||||
* answer is kept in the store's prefs (`size_<id>`, where the library reads it too). Until it is
|
||||
* known the card shows nothing.
|
||||
*/
|
||||
object StoreSizes {
|
||||
private const val TAG = "StoreSizes"
|
||||
|
||||
/** Sizes found this run, by card key; the cards read them. */
|
||||
val known = mutableStateMapOf<String, Long>()
|
||||
|
||||
private val asked = ConcurrentHashMap.newKeySet<String>()
|
||||
private val pool = Executors.newFixedThreadPool(3, com.droiddeck.launcher.stores.download.DownloadQueue.workerFactory("sizes"))
|
||||
|
||||
fun size(item: CatalogItem): Long = known[item.key] ?: item.sizeBytes
|
||||
|
||||
/** A card for [item] is on screen (or about to be): find its size if nothing has yet. */
|
||||
fun request(context: Context, item: CatalogItem) {
|
||||
if (!item.owned || item.sizeBytes > 0 || item.store == Store.GOG) return
|
||||
if (known.containsKey(item.key) || !asked.add(item.key)) return
|
||||
val app = context.applicationContext
|
||||
pool.execute {
|
||||
val bytes = runCatching { fetch(app, item) }.onFailure { Log.w(TAG, "${item.key}: ${it.javaClass.simpleName}") }.getOrNull() ?: 0L
|
||||
if (bytes > 0) {
|
||||
when (item.store) {
|
||||
Store.EPIC -> EpicPrefs.get(app).edit().putLong("size_${item.id}", bytes).apply()
|
||||
Store.AMAZON -> AmazonPrefs.get(app).edit().putLong("size_${item.id}", bytes).apply()
|
||||
Store.GOG -> {}
|
||||
}
|
||||
StoresState.post { known[item.key] = bytes }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetch(context: Context, item: CatalogItem): Long = when (item.store) {
|
||||
Store.EPIC -> {
|
||||
val token = EpicCredentialStore.getValidAccessToken(context) ?: return 0L
|
||||
val ns = item.extra["namespace"].orEmpty()
|
||||
val catalogItemId = item.extra["catalogItemId"].orEmpty()
|
||||
if (ns.isEmpty() || catalogItemId.isEmpty()) return 0L
|
||||
val json = EpicApiClient.getManifestApiJson(token, ns, catalogItemId, item.id) ?: return 0L
|
||||
val bytes = EpicDownloadManager.downloadManifest(json, EpicDownloadManager.parseCdnUrls(json)) ?: return 0L
|
||||
val manifest = EpicDownloadManager.parseManifest(bytes) ?: return 0L
|
||||
EpicDownloadManager.resolveInstallFiles(manifest, EpicInstallTags.tagsForDevice()).sumOf { it.fileSize() }
|
||||
}
|
||||
Store.AMAZON -> {
|
||||
val token = AmazonCredentialStore.getValidAccessToken(context) ?: return 0L
|
||||
val entitlement = item.extra["entitlementId"].orEmpty().ifEmpty { return 0L }
|
||||
val spec = AmazonApiClient.getGameDownload(token, entitlement) ?: return 0L
|
||||
val bytes = AmazonApiClient.getBytes(AmazonApiClient.appendPath(spec.downloadUrl, "manifest.proto"), token) ?: return 0L
|
||||
AmazonManifest.parse(bytes).totalInstallSize
|
||||
}
|
||||
Store.GOG -> 0L
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
@@ -492,8 +494,18 @@ private fun GameCard(item: CatalogItem, store: Store, s: FrontEndState, a: Front
|
||||
val installed = item.id in installedKeys
|
||||
val download = StoresState.download("${store.id}:${item.id}")?.takeIf { it.isActive }
|
||||
val open = { onOpen(item.key) }
|
||||
// A size the library did not give is looked up once the card is on screen or close to it.
|
||||
val screen = with(androidx.compose.ui.platform.LocalDensity.current) { androidx.compose.ui.platform.LocalConfiguration.current.screenHeightDp.dp.toPx() }
|
||||
val ahead = screen / 2
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().paneItem("card:${item.key}").then(if (first) Modifier.firstTile() else Modifier).graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.onGloballyPositioned { c ->
|
||||
if (item.owned && item.sizeBytes <= 0) {
|
||||
val b = c.boundsInWindow()
|
||||
if (b.bottom > -ahead && b.top < screen + ahead && b.width > 0f) com.droiddeck.launcher.stores.StoreSizes.request(ctx, item)
|
||||
}
|
||||
}
|
||||
.paneItem("card:${item.key}").then(if (first) Modifier.firstTile() else Modifier).graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
.clip(RoundedCornerShape(10.dp)).glideBorder(hot, RoundedCornerShape(10.dp), pal.signal, pal.line)
|
||||
.hoverable(src).clickable(interactionSource = src, indication = LocalIndication.current, onClick = open).controllerConfirm(onClick = open),
|
||||
) {
|
||||
@@ -565,7 +577,8 @@ private fun CardMarker(item: CatalogItem, installed: Boolean, download: com.droi
|
||||
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 = Color.White.copy(alpha = 0.8f), maxLines = 1)
|
||||
// The size in the title's own type; nothing until it is known.
|
||||
item.owned -> com.droiddeck.launcher.stores.StoreSizes.size(item).takeIf { it > 0 }?.let { Text(formatBytes(it), fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.White, 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)
|
||||
|
||||
@@ -153,6 +153,11 @@ without it and Setup says "Stores engine not built").
|
||||
- **Cloud saves device-proven (AYANEO Pocket FIT, `6a175f1`, ELDERBORN on GOG):** the launch logged
|
||||
`cloud gog 1732383191 down result=ok files=5 bytes=237499 reason=downloaded` (template found) and
|
||||
the in-game slot shows the cloud save (Catacombs: Tutorial part 1, 00:08:40).
|
||||
- **Every owned card shows its size** at the right of its title strip, in the title's type: GOG's from
|
||||
its catalog; Epic's (build manifest, the files this device installs) and Amazon's (download
|
||||
manifest) looked up once a card is on screen or close, three at a time in the background, kept in
|
||||
the store's prefs (`size_<id>`). Nothing shows until it is known; a marker (installed, Resume, a
|
||||
download's percent) keeps the slot.
|
||||
- **Two tabs, Store and Library** (the All count mixed owned games with whatever shelves had loaded:
|
||||
GOG Library 33 / All 120): Library's dropdown filters All (every owned game, the default, the
|
||||
tab's count) or Installed, and the tab reads the filter ("Library (33)", "Installed (1)"); the old
|
||||
|
||||
Reference in new issue
Block a user