Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -76,33 +76,40 @@ class CustomGameAppScreen : BaseAppScreen() {

// Check for all SteamGridDB images in the game folder
// Hero view uses horizontal grid (grid_hero)
// A user-supplied "coverh"/"cover" image takes priority over SteamGridDB.
// coverh = horizontal cover
val heroImageUrl = remember(gameFolderPath) {
gameFolderPath?.let { path ->
val folder = File(path)
findSteamGridDBImage(folder, "grid_hero")
CustomGameScanner.findHeroCoverInFolder(folder)
?: findSteamGridDBImage(folder, "grid_hero")
}
}

// Capsule view uses vertical grid (grid_capsule)
// A user-supplied "coverv"/"cover" image takes priority over SteamGridDB.
// coverv = vertical cover
val capsuleUrl = remember(gameFolderPath) {
gameFolderPath?.let { path ->
val folder = File(path)
findSteamGridDBImage(folder, "grid_capsule")
CustomGameScanner.findCapsuleCoverInFolder(folder)
?: findSteamGridDBImage(folder, "grid_capsule")
}
}

// Header view uses heroes endpoint (hero, but not grid_hero)
// This is also a horizontal banner, so the user "coverh"/"cover" applies here too.
val headerUrl = remember(gameFolderPath) {
gameFolderPath?.let { path ->
val folder = File(path)
// Find hero image but exclude grid_hero
folder.listFiles()?.firstOrNull { file ->
file.name.startsWith("steamgriddb_hero") &&
!file.name.contains("grid") &&
(file.name.endsWith(".png", ignoreCase = true) ||
file.name.endsWith(".jpg", ignoreCase = true) ||
file.name.endsWith(".webp", ignoreCase = true))
}?.let { Uri.fromFile(it).toString() }
CustomGameScanner.findHeroCoverInFolder(folder)
?: folder.listFiles()?.firstOrNull { file ->
file.name.startsWith("steamgriddb_hero") &&
!file.name.contains("grid") &&
(file.name.endsWith(".png", ignoreCase = true) ||
file.name.endsWith(".jpg", ignoreCase = true) ||
file.name.endsWith(".webp", ignoreCase = true))
}?.let { Uri.fromFile(it).toString() }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -436,10 +436,18 @@ internal fun getGridImageUrl(
GameSource.CUSTOM_GAME -> {
val primary = when (paneType) {
PaneType.GRID_CAPSULE ->
findSteamGridDBImage("grid_capsule") ?: appInfo.capsuleImageUrl
// Capsule (vertical): user "coverv"/"cover" wins over SteamGridDB capsule.
CustomGameScanner.findCapsuleCoverForCustomGame(appInfo.appId)
?: findSteamGridDBImage("grid_capsule")
?: appInfo.capsuleImageUrl
PaneType.GRID_HERO ->
findSteamGridDBImage("grid_hero") ?: appInfo.headerImageUrl
// Hero (horizontal): user "coverh"/"cover" wins over SteamGridDB hero.
CustomGameScanner.findHeroCoverForCustomGame(appInfo.appId)
?: findSteamGridDBImage("grid_hero")
?: appInfo.headerImageUrl
else -> {
// Default/carousel banner is also a horizontal hero view.
val heroCover = CustomGameScanner.findHeroCoverForCustomGame(appInfo.appId)
val gameFolderPath = CustomGameScanner.getFolderPathFromAppId(appInfo.appId)
val heroUrl = gameFolderPath?.let { path ->
val folder = File(path)
Expand All @@ -454,7 +462,7 @@ internal fun getGridImageUrl(
}
heroFile?.let { android.net.Uri.fromFile(it).toString() }
}
heroUrl ?: appInfo.headerImageUrl
heroCover ?: heroUrl ?: appInfo.headerImageUrl
}
}
GridImageUrls(primary = primary)
Expand Down
61 changes: 61 additions & 0 deletions app/src/main/java/app/gamenative/utils/CustomGameScanner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,67 @@ object CustomGameScanner {
return fromHeuristic
}

/**
* Finds a user-supplied cover image for a Custom Game's CAPSULE (vertical box-art) view.
*
* Priority: "coverv" (capsule-specific) first, then the generic "cover".
* Only the game's MAIN folder is searched — child folders are intentionally ignored.
* Supported extensions, in preference order: png, jpg, jpeg, webp.
*
* @return a file:// URI string usable directly as an image URL, or null if none exists.
*/
fun findCapsuleCoverForCustomGame(appId: String): String? {
val folderPath = getFolderPathFromAppId(appId) ?: return null
return findCapsuleCoverInFolder(File(folderPath))
}

/** Folder-based variant of [findCapsuleCoverForCustomGame]. */
fun findCapsuleCoverInFolder(folder: File): String? =
findCoverByBaseNames(folder, listOf("coverv", "cover"))

/**
* Finds a user-supplied cover image for a Custom Game's HERO (horizontal banner) view.
*
* Priority: "coverh" (hero-specific) first, then the generic "cover".
* Only the game's MAIN folder is searched — child folders are intentionally ignored.
* Supported extensions, in preference order: png, jpg, jpeg, webp.
*
* @return a file:// URI string usable directly as an image URL, or null if none exists.
*/
fun findHeroCoverForCustomGame(appId: String): String? {
val folderPath = getFolderPathFromAppId(appId) ?: return null
return findHeroCoverInFolder(File(folderPath))
}

/** Folder-based variant of [findHeroCoverForCustomGame]. */
fun findHeroCoverInFolder(folder: File): String? =
findCoverByBaseNames(folder, listOf("coverh", "cover"))

/**
* Scans ONLY the top level of [folder] for a file whose name matches one of [baseNames]
* (case-insensitive) with a supported image extension. [baseNames] are tried in order, so
* earlier entries take priority (e.g. "coverv" before the generic "cover"). Within a single
* base name, extensions are preferred png > jpg/jpeg > webp.
*
* @return a file:// URI string, or null if no matching file exists.
*/
private fun findCoverByBaseNames(folder: File, baseNames: List<String>): String? {
if (!folder.exists() || !folder.isDirectory) return null
val extensions = listOf("png", "jpg", "jpeg", "webp")
val files = folder.listFiles { f -> f.isFile } ?: return null
Comment thread
Catpotatos marked this conversation as resolved.
for (base in baseNames) {
val match = files.filter { file ->
extensions.any { ext -> file.name.equals("$base.$ext", ignoreCase = true) }
}.minByOrNull { file ->
// Rank by extension preference so e.g. cover.png wins over cover.jpg.
val ext = file.name.substringAfterLast('.', "").lowercase()
extensions.indexOf(ext).let { if (it == -1) Int.MAX_VALUE else it }
}
if (match != null) return Uri.fromFile(match).toString()
}
return null
}

// Shared helper for .ico/.png heuristic
private fun findNearbyImageIcon(folder: File, uniqueExeRel: String?): String? {
fun File.icoFiles(): List<File> = this.listFiles { f ->
Expand Down
Loading