Skip to content

Conversation

@Maaggel
Copy link

@Maaggel Maaggel commented Nov 25, 2025

Custom Media Support & Image handeling improvements

This is an updated version of the custom media feature that is up to date with the current master branch.

Features

  • Custom Media Support: Users can now set custom images (hero, capsule, header, logo, icon) for games. Custom images take priority over SteamGridDB and Steam URLs.
  • Centralized Image Loading: Introduced GameImageUtils to handle image retrieval with priority: Custom media → SteamGridDB → Steam URLs.
  • Steamlined and optimized exe icon extraction: The ico from exe extraction ex. was run automatically, and was way too extensive, trying to find exe files in subfolders, other ico files etc. This has been optimized to use existing functionality to find the active exe file and only extract from this, and only when activily fetching images.
    The extracted icon is now also named "gameicon.extracted.ico" instead of a dynamic name. This means we can skip scanning for an extracted icon.
  • Updated custom game add dialog: Adding text stating the user can fetch images after adding the game.
  • **"LoadingToast()": Added "LoadingToast" functionality, to have a loader shown with success message while fetching images

Bug Fixes

  • Image Loading Spinner: Fixed loading spinner size constraint on game list - now limited to max 64dp and properly centered to prevent oversized spinners.
  • Steam Game Uninstall Navigation: Added small delay before navigating back to game list after uninstalling a Steam game, ensuring file system updates complete and preventing blank list issues.
  • String Resources: Moved hardcoded strings for custom game deletion to strings.xml for proper internationalization.

Technical Improvements

  • Media Version Tracking: Implemented mediaVersionFlow to trigger UI refresh when custom media is updated or reset when done fetching from SteamGridDB

Files Changed

  • CustomMediaUtils.kt - Custom media management utilities
  • GameImageUtils.kt - Centralized image loading logic
  • Images.kt - Loading spinner size constraints
  • SteamAppScreen.kt - Uninstall navigation delay
  • CustomGameAppScreen.kt - String resource migration
  • strings.xml - Added new string resources

Video examples

Adding new custom game, showing the new text in the prompt, fetching SteamGridDB images and using custom media

Record_2025-11-25-12-47-12.1.mp4


Changing custom media on existing custom game
Record_2025-11-25-12-41-57.mp4


Using custom media on Steam game
Record_2025-11-25-13-10-00.mp4

Summary by CodeRabbit

  • New Features

    • Media tab in game settings to pick/reset custom logo, icon, header, capsule and hero with live previews and cache-busting.
    • Bottom-aligned loading toast with completion feedback.
  • Bug Fixes

    • Improved loading visuals (spinner vs progress bar) and more reliable image refreshes after media updates.
  • Refactor

    • Centralized image resolution and unified media handling across screens.
  • Localization

    • Added strings for media management and custom-game deletion flows.

✏️ Tip: You can customize this high-level summary in your review settings.

Mikkel Bjørnmose Bundgaard added 2 commits November 24, 2025 23:48
@coderabbitai
Copy link

coderabbitai bot commented Nov 25, 2025

Walkthrough

Centralized custom media handling and image-resolution added with new utilities, UI components, and scanner refactors: CustomMediaUtils and GameImageUtils, media-aware UI (ContainerConfigDialog Media tab, LoadingToast), SteamGridDB header support, updated image-fetch flows and media-version cache-busting.

Changes

Cohort / File(s) Summary
Media utilities & cache
app/src/main/java/app/gamenative/utils/CustomMediaUtils.kt, app/src/main/java/app/gamenative/utils/GameImageUtils.kt
New CustomMediaUtils manages per-app custom images, save/reset/check functions and mediaVersionFlow; GameImageUtils resolves images (custom → SteamGridDB → steamUrl) and exposes notifyImagesRefreshed/getGameImage plus bustCache helper.
SteamGridDB changes
app/src/main/java/app/gamenative/utils/SteamGridDB.kt
Expanded fetch logic to include header images (460x215); fetch returns Triple; ImageFetchResult shape updated; added findSteamGridDBImage* helpers and updated callers.
Custom game scanner refactor
app/src/main/java/app/gamenative/utils/CustomGameScanner.kt
Removed default-folder/scan helpers; added extractedGameIconFileName, extractIconFromExecutable, findIconFileForCustomGame; changed exe-selection heuristics and reduced public surface.
UI components (loading & dialogs)
app/src/main/java/app/gamenative/ui/component/dialog/LoadingDialog.kt, app/src/main/java/app/gamenative/ui/component/dialog/LoadingToast.kt, app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt
LoadingDialog: indeterminate now shows CircularProgressIndicator; LoadingToast: new bottom toast with done/fade behavior; ContainerConfigDialog: public API extended with media params and added Media tab + private MediaSection composable.
App screens & integration
app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt, app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt, app/src/main/java/app/gamenative/ui/screen/library/appscreen/CustomGameAppScreen.kt, app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt
BaseAppScreen: added companion StateFlows for fetchingImages and done messages and LoadingToast wiring; Steam/Custom/Library screens now use GameImageUtils.getGameImage, observe mediaVersionFlow; GameDisplayInfo extended with logoUrl, capsuleUrl, headerUrl; header-first selection in LibraryAppScreen.
Library item rendering & image helpers
app/src/main/java/app/gamenative/ui/screen/library/components/LibraryAppItem.kt, app/src/main/java/app/gamenative/ui/util/Images.kt
LibraryAppItem switched to media-version-aware image models and bustCache() usage; Images.kt wraps loading UI in constrained Box with centered CircularProgressIndicator.
Strings & localization
app/src/main/res/values/strings.xml
Added strings for custom game deletion, image-fetching status, and a media-management string set (logo/icon/header/capsule/hero and related labels); media strings were inserted twice (duplicate entries).

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as Library / App Screen
    participant Base as BaseAppScreen
    participant GIU as GameImageUtils
    participant CMU as CustomMediaUtils
    participant SGD as SteamGridDB
    participant Scanner as CustomGameScanner
    participant Toast as LoadingToast

    User->>UI: Request "Refresh images"
    UI->>Base: setFetchingImages(appId, true)
    Base->>Toast: show loading (visible=true)

    rect rgba(200,230,255,0.12)
      note right of Base: For each image type
      Base->>GIU: getGameImage(libraryItem, imageType, steamUrl)
      GIU->>CMU: check hasCustom<Image>(gameId)
      alt custom exists
        CMU-->>GIU: return custom URI (bustCache(mediaVersion))
        GIU-->>Base: custom URI
      else
        GIU->>SGD: query SteamGridDB by imageType/appId
        SGD-->>GIU: grid/header/hero path or null
        alt SteamGridDB found
          GIU-->>Base: SteamGridDB URI
        else
          GIU->>Scanner: findIconFileForCustomGame / extractIconFromExecutable
          Scanner-->>GIU: local icon path or null
          GIU-->>Base: local path or steamUrl
        end
      end
    end

    Base->>CMU: notifyImagesRefreshed()
    CMU->>CMU: increment mediaVersionFlow -> UIs recompose

    Base->>Base: setFetchingImagesDone(appId, "…images fetched")
    Base->>Toast: show doneMessage -> fade out
    Toast->>Base: onDismiss -> clear fetching/done state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Reasoning: broad, heterogeneous changes across IO, image processing, UI, and data-shape updates; not a full-system rewrite but several areas need distinct validation.
  • Pay extra attention to:
    • File I/O, bitmap scaling, and error handling in CustomMediaUtils
    • Correct unpacking/usage of SteamGridDB Triple results across call sites
    • mediaVersionFlow and bustCache URL mutation semantics
    • BaseAppScreen companion StateFlow lifecycle and LoadingToast dismissal race conditions
    • Duplicate string resources in strings.xml and any unresolved references to removed CustomGameScanner helpers

Possibly related PRs

Poem

🐇 I hopped and cached every frame,
Saved logos, headers, hero name,
A toast will spin, then show a check—
Media versions bust the deck.
Hop on, devs — the images came! 🎨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.88% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main changes: adding custom media support and improving image handling across the codebase.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67d50e5 and b3f6f6a.

📒 Files selected for processing (2)
  • app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt (9 hunks)
  • app/src/main/res/values/strings.xml (3 hunks)
🔇 Additional comments (6)
app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt (2)

148-243: LGTM: MediaSection implementation is solid.

The MediaSection composable correctly handles media management with proper parameter passing, null safety, and reactive updates via remember(mediaVersion, gameId).


2022-2188: Media tab structure is well-organized, pending libraryItem fix.

The Media tab implementation properly:

  • Observes mediaVersionFlow for reactive updates
  • Uses dedicated MediaSection for each image type (logo, icon, header, capsule, hero)
  • Applies appropriate aspect ratios and content scales
  • Passes correct callbacks to CustomMediaUtils

However, the media previews will only work correctly once the libraryItem construction is fixed to include gameId (see previous comment).

app/src/main/res/values/strings.xml (4)

93-94: LGTM: Custom game deletion strings properly externalized.

These strings correctly use positional format specifiers and follow Android localization best practices.


278-278: LGTM: Helpful addition to user guidance.

The updated message properly informs users about the image fetching capability, improving the user experience by setting clear expectations.


282-282: LGTM: Loading feedback string improves UX.

The new loading state message provides helpful feedback during image fetch operations and maintains consistency with the existing success message pattern.


420-441: I was unable to access the repository to verify the claim about duplicate string resources due to a clone failure. Without direct access to the app/src/main/res/values/strings.xml file, I cannot definitively determine whether duplicate string definitions exist elsewhere in the file beyond the visible snippet.

Inconsistency between AI summary and provided code snippet cannot be verified.

The AI summary claims duplicate media management string entries exist, but only one set is visible in lines 420-441. Since duplicate string resource keys would cause Android build failures, this should be verified against the actual file. The repository was inaccessible during verification, so manual inspection of the complete strings.xml file is recommended.

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (16)
app/src/main/java/app/gamenative/ui/util/Images.kt (1)

47-54: Centered, bounded spinner implementation looks good; consider reuse for consistency

Wrapping the loader in a Box with contentAlignment = Alignment.Center and constraining CircularProgressIndicator via sizeIn(maxWidth = 64.dp, maxHeight = 64.dp) cleanly fixes the oversized/uncentered spinner problem while respecting the avatar’s size and clipping.

If you want consistent behavior across the app, you could extract a small helper composable (e.g., @Composable fun CenteredLoadingIndicator(maxSize: Dp = 64.dp)) and reuse it in SteamIconImage and EmoticonImage as well, but that’s optional polish rather than required for this PR.

app/src/main/java/app/gamenative/utils/CustomGameScanner.kt (2)

26-26: Consider making this a const val or private const val.

The extractedGameIconFileName appears to be a constant value that shouldn't change at runtime. Using var allows accidental mutation.

-    var extractedGameIconFileName = "gameicon.extracted.ico"
+    const val extractedGameIconFileName = "gameicon.extracted.ico"

409-419: Consider deprecating the context overload if unused.

The second overload explicitly ignores the context parameter. If this is only for backward compatibility, consider annotating with @Deprecated to guide future callers toward the simpler signature, or remove it if no callers exist.

+    @Deprecated(
+        message = "Context parameter is unused. Use findIconFileForCustomGame(appId) instead.",
+        replaceWith = ReplaceWith("findIconFileForCustomGame(appId)")
+    )
     fun findIconFileForCustomGame(context: Context, appId: String): String? {
         return findIconFileForCustomGame(appId)
     }
app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt (1)

245-273: LGTM! Header-first image approach with proper fallback chain.

The logic correctly implements the priority: headerUrlheroImageUrl → gradient fallback. The CoilImage still handles loading failures gracefully.

Minor: The comment on line 268 says "no header image" but the condition actually means "no header OR hero image available."

             } else {
-                // Fallback gradient background when no header image
+                // Fallback gradient background when no header or hero image available
                 Surface(
app/src/main/java/app/gamenative/ui/component/dialog/LoadingToast.kt (1)

137-146: Redundant null coalescing operator.

On line 142, currentMessage ?: message has a redundant fallback. The targetState on line 138 evaluates to either doneMessage (when showDone && doneMessage != null) or message - both are non-null String values. The ?: message is unnecessary.

                             Crossfade(
                                 targetState = if (showDone && doneMessage != null) doneMessage else message,
                                 label = "messageTransition"
                             ) { currentMessage ->
                                 Text(
-                                    text = currentMessage ?: message,
+                                    text = currentMessage,
                                     color = Color.White,
                                     style = MaterialTheme.typography.bodyMedium
                                 )
                             }
app/src/main/java/app/gamenative/utils/GameImageUtils.kt (1)

53-74: Unnecessary icon lookup when imageType is not "icon".

Lines 56-61 compute the icon variable (potentially calling SteamGridDB.findSteamGridDBImageByAppId and CustomGameScanner.findIconFileForCustomGame) even when the requested imageType is not "icon". This adds unnecessary overhead for non-icon requests.

Consider moving the icon-specific logic inside the when branch:

         // 2. Check SteamGridDB images
         if (appId != null) {
-            // Find icon from SteamGridDB
-            var icon = SteamGridDB.findSteamGridDBImageByAppId(appId, "icon")
-
-            // If no icon found, find icon from custom game scanner (extracted from exe file)
-            if (libraryItem.gameSource == GameSource.CUSTOM_GAME && icon == null) {
-                icon = CustomGameScanner.findIconFileForCustomGame(appId)
-            }
-            
-            // Check if the game has a custom icon
             val steamGridUri = when (imageType) {
                 "hero", "grid_hero" -> SteamGridDB.findSteamGridDBImageByAppId(appId, "grid_hero")
                 "capsule", "grid_capsule" -> SteamGridDB.findSteamGridDBImageByAppId(appId, "grid_capsule")
                 "header" -> SteamGridDB.findSteamGridDBImageByAppId(appId, "header")
                 "logo" -> SteamGridDB.findSteamGridDBImageByAppId(appId, "logo")
-                "icon" -> icon
+                "icon" -> {
+                    // Find icon from SteamGridDB, fallback to extracted icon for custom games
+                    SteamGridDB.findSteamGridDBImageByAppId(appId, "icon")
+                        ?: if (libraryItem.gameSource == GameSource.CUSTOM_GAME) {
+                            CustomGameScanner.findIconFileForCustomGame(appId)
+                        } else null
+                }
                 else -> null
             }
app/src/main/java/app/gamenative/ui/screen/library/components/LibraryAppItem.kt (2)

159-176: Icon resolution pipeline is sound; watch IO cost of findIconFileForCustomGame

The priority chain CustomMediaUtils.getCustomIconUri(gameId) → CustomGameScanner.findIconFileForCustomGame(...) → clientIconUrl plus bustCache(iconModel, mediaVersion) correctly ensures custom icons win and that UI refreshes when mediaVersion changes. If findIconFileForCustomGame does filesystem scanning, consider caching its result or moving that work off the main thread, since this runs during composition for each list item.


69-70: Grid/hero/header image resolution via GameImageUtils is correct; tighten Steam fallback handling

Using mediaVersion plus GameImageUtils.getGameImage for grid_capsule, grid_hero, and header and passing the Steam CDN URLs as steamUrl matches the new centralized image rules and allows cache busting via bustCache(baseModel, mediaVersion). Two follow‑ups you may want to consider:

  • steamAssetsUrl can likely be an internal const val rather than a mutable var.
  • For GameSource.CUSTOM_GAME, ensure GameImageUtils.getGameImage ignores or safely handles the constructed Steam URLs; otherwise custom games may waste requests on invalid Steam asset IDs.

Also applies to: 179-221

app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt (2)

141-241: MediaSection abstraction is solid and matches UX goals

MediaSection cleanly encapsulates preview rendering, “choose image” and “remove custom image” actions, and mediaVersion-aware bustCache behavior. The success/failure toasts and hasModel detection for both String and Uri cover the common cases well. Only minor nit: you can use the already-imported rememberLauncherForActivityResult/ActivityResultContracts.GetContent instead of the fully qualified names, but that’s purely stylistic.


245-259: New media-related parameters and tab are wired reasonably; gate Media tab when no game/app context

Extending ContainerConfigDialog with optional media*Url, gameId, and appId parameters plus the “Media” tab is backwards-compatible thanks to defaults. When gameId/appId are null (e.g., generic/default container config), the Media tab currently shows static placeholders with no actions, which may confuse users. Consider only including "Media" in tabs when both gameId and appId are non-null.

Also applies to: 827-828

app/src/main/java/app/gamenative/utils/SteamGridDB.kt (1)

193-207: Orientation detection can rely on JSON dimensions to avoid extra bitmap work

You now read width/height from the grid JSON and also call isImageHorizontal(imageBytes) for every non-hero-size image. Since orientation is just width > height when those values are present, you can avoid decoding image bounds from the full byte array in most cases. That would reduce CPU and memory pressure during large grid fetches.

Consider:

-                // Check dimensions from JSON (more efficient than downloading first)
-                val width = imageObj.optInt("width", 0)
-                val height = imageObj.optInt("height", 0)
+                val width = imageObj.optInt("width", 0)
+                val height = imageObj.optInt("height", 0)
@@
-                // Determine orientation for non-header images
-                val isHorizontal = isImageHorizontal(imageBytes)
+                // Determine orientation for non-hero-size images; prefer JSON dims
+                val isHorizontal = when {
+                    width > 0 && height > 0 -> width > height
+                    else -> isImageHorizontal(imageBytes)
+                }

This keeps the bitmap-based fallback for edge cases but avoids redundant decoding when the API already tells you the size.

Also applies to: 236-265

app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt (2)

44-68: Consider importing flow types and using atomic operations for state.

The fully qualified kotlinx.coroutines.flow.MutableStateFlow and kotlinx.coroutines.flow.StateFlow references could be simplified by adding imports at the top of the file for cleaner code.

Additionally, while the current implementation works for this use case, note that notifyMediaChanged() style increments or concurrent calls to setFetchingImages/setFetchingImagesDone could theoretically interleave. For simple fetching-state tracking this is acceptable, but if you later need stricter guarantees, consider using update { } instead of direct assignment.


297-362: Unstructured coroutine won't be cancelled if user navigates away.

CoroutineScope(Dispatchers.IO).launch creates an unmanaged coroutine. If the user navigates away from this screen mid-fetch, the coroutine continues running and may emit state updates to a stale context or show toasts after navigation.

Consider using a structured scope tied to the composable lifecycle (e.g., rememberCoroutineScope()) or a ViewModel scope for better lifecycle management.

-                    CoroutineScope(Dispatchers.IO).launch {
+                    uiScope.launch(Dispatchers.IO) {

Note: This would require passing uiScope into buildCommonMenuOptions or restructuring the menu option callbacks.

app/src/main/java/app/gamenative/utils/CustomMediaUtils.kt (3)

144-150: Consider adding BitmapFactory.Options to limit decode size for large images.

Decoding arbitrarily large images without inSampleSize can cause OOM on devices with limited memory. Consider calculating an appropriate sample size based on the target dimensions before decoding:

private fun decodeBitmap(context: Context, uri: Uri, maxDim: Int = 2048): Bitmap? {
    return try {
        context.contentResolver.openInputStream(uri).use { ins ->
            if (ins == null) return null
            // First pass: get dimensions
            val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
            BitmapFactory.decodeStream(ins, null, opts)
        }
        // Calculate sample size and decode
        context.contentResolver.openInputStream(uri).use { ins ->
            if (ins == null) return null
            val opts = BitmapFactory.Options().apply {
                inSampleSize = calculateInSampleSize(outWidth, outHeight, maxDim, maxDim)
            }
            BitmapFactory.decodeStream(ins, null, opts)
        }
    } catch (t: Throwable) { Timber.w(t, "decodeBitmap failed"); null }
}

185-197: Null-safety: parentFile can technically be null.

While mediaDirFor should always create a valid parent, File.parentFile can return null for root paths or malformed paths. Adding a null-safe call prevents potential NPE:

 private fun saveJpeg(bmp: Bitmap, file: File) {
-    if (!file.parentFile.exists()) file.parentFile.mkdirs()
+    file.parentFile?.let { if (!it.exists()) it.mkdirs() }
     FileOutputStream(file).use { fos ->
         bmp.compress(Bitmap.CompressFormat.JPEG, 90, fos)
     }
 }

 private fun savePng(bmp: Bitmap, file: File) {
-    if (!file.parentFile.exists()) file.parentFile.mkdirs()
+    file.parentFile?.let { if (!it.exists()) it.mkdirs() }
     FileOutputStream(file).use { fos ->
         bmp.compress(Bitmap.CompressFormat.PNG, 100, fos)
     }
 }

63-82: Silent exception swallowing - consider logging deletion failures.

runCatching silently swallows any exceptions during file deletion. While deletion failures are often benign, logging them could help diagnose issues when custom media doesn't reset as expected:

 fun resetCustomHeader(appId: Int) {
-    runCatching { getCustomHeaderFile(appId).delete() }
+    runCatching { getCustomHeaderFile(appId).delete() }
+        .onFailure { Timber.w(it, "Failed to delete custom header for appId=$appId") }
     notifyMediaChanged()
 }

Apply similar pattern to other reset functions.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b0835ff and 6fade35.

📒 Files selected for processing (14)
  • app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt (9 hunks)
  • app/src/main/java/app/gamenative/ui/component/dialog/LoadingDialog.kt (2 hunks)
  • app/src/main/java/app/gamenative/ui/component/dialog/LoadingToast.kt (1 hunks)
  • app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt (2 hunks)
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt (9 hunks)
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/CustomGameAppScreen.kt (6 hunks)
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (5 hunks)
  • app/src/main/java/app/gamenative/ui/screen/library/components/LibraryAppItem.kt (5 hunks)
  • app/src/main/java/app/gamenative/ui/util/Images.kt (3 hunks)
  • app/src/main/java/app/gamenative/utils/CustomGameScanner.kt (7 hunks)
  • app/src/main/java/app/gamenative/utils/CustomMediaUtils.kt (1 hunks)
  • app/src/main/java/app/gamenative/utils/GameImageUtils.kt (1 hunks)
  • app/src/main/java/app/gamenative/utils/SteamGridDB.kt (12 hunks)
  • app/src/main/res/values/strings.xml (3 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.

Applied to files:

  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.

Applied to files:

  • app/src/main/java/app/gamenative/utils/SteamGridDB.kt
📚 Learning: 2025-09-19T17:13:01.017Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.

Applied to files:

  • app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt
🧬 Code graph analysis (2)
app/src/main/java/app/gamenative/ui/screen/library/components/LibraryAppItem.kt (1)
app/src/main/java/app/gamenative/ui/util/Images.kt (1)
  • ListItemImage (28-62)
app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt (1)
app/src/main/java/app/gamenative/ui/component/dialog/LoadingToast.kt (1)
  • LoadingToast (47-153)
🔇 Additional comments (18)
app/src/main/res/values/strings.xml (2)

352-373: Comprehensive media management strings well-organized.

The media management strings are properly structured with consistent naming conventions, descriptive hints for developers, and appropriate fallback messages. All new strings follow Android resource conventions and support future localization.


90-91: No duplicate string definitions found—strings are properly defined.

Verification confirms there are no duplicate string key definitions in the full strings.xml file. The three strings mentioned (lines 90–91 and 226) are each defined exactly once, and no other duplicate name attributes exist in the file. The AI summary's reference to duplicate entries appears to be outdated.

app/src/main/java/app/gamenative/ui/component/dialog/LoadingDialog.kt (1)

46-58: LGTM! Clean separation of indeterminate and determinate progress states.

The conditional logic properly handles both progress modes: CircularProgressIndicator for indeterminate (progress < 0) and LinearProgressIndicator with percentage for determinate progress. The layout with spacers provides consistent visual spacing.

app/src/main/java/app/gamenative/utils/CustomGameScanner.kt (2)

43-59: Good improvement to filter out non-game executables.

Adding unitycrashhandler to the exclusion list alongside unins prevents false positives when scanning for game executables. The lambda-based listFiles filtering is idiomatic Kotlin.


337-391: Well-structured icon extraction with proper error handling.

The function correctly handles multiple failure scenarios (missing folder, missing executable, extraction failure) with appropriate logging. The try-catch ensures exceptions don't crash the app.

One minor style observation: the ?: run { ... return false } pattern on line 364-368 works but is slightly unconventional. An alternative is:

if (exeRelPath.isEmpty()) {
    exeRelPath = findUniqueExeRelativeToFolder(gameFolder) ?: run {
        Timber.tag("CustomGameScanner").w("Could not find executable for game launch: $appId")
        return false
    }
}

This is fine as-is though.

app/src/main/java/app/gamenative/ui/component/dialog/LoadingToast.kt (1)

58-75: State management looks correct for typical usage patterns.

The LaunchedEffect(doneMessage) triggers the done animation when doneMessage becomes non-null, and LaunchedEffect(visible) properly resets state when visibility toggles off. This ensures the toast can be reused across multiple loading operations.

app/src/main/java/app/gamenative/utils/GameImageUtils.kt (2)

11-19: Good abstraction for triggering UI refresh.

The notifyImagesRefreshed() function provides a clear, semantic API that abstracts the underlying CustomMediaUtils.notifyMediaChanged() implementation detail. This makes the calling code more readable.


40-51: LGTM! Custom media priority is correctly implemented.

The function properly checks for custom media first (when gameId is available) and returns early if found, implementing the documented priority: Custom media → SteamGridDB → Steam URLs.

app/src/main/java/app/gamenative/ui/screen/library/appscreen/CustomGameAppScreen.kt (3)

82-119: Centralized image resolution for custom games looks consistent

Using mediaVersion plus remember(mediaVersion, libraryItem) to resolve heroImageUrl, capsuleUrl, headerUrl, logoUrl, and iconUrl via GameImageUtils.getGameImage is coherent with the new global pipeline and ensures UI refreshes when custom media changes. Wiring these into GameDisplayInfo (including the new fields) looks correct for custom games.

Also applies to: 148-165


231-237: Simplified icon extraction hook is appropriate

Delegating post-fetch icon extraction to CustomGameScanner.extractIconFromExecutable(context, libraryItem.appId) keeps onAfterFetchImages focused and aligned with the centralized icon-handling strategy described in the PR. No functional regressions evident here.


361-397: Deletion toasts now correctly use string resources

Switching the delete-success, delete-failure, and button labels to stringResource/context.getString (custom_game_deleted, custom_game_delete_failed, delete_app, cancel) improves localization and keeps copy centralized. Logic and formatting parameters look correct.

app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (2)

182-224: Steam image resolution is correctly centralized and mediaVersion-aware

Observing CustomMediaUtils.mediaVersionFlow and feeding libraryItem plus Steam URLs into GameImageUtils.getGameImage for grid_hero, icon, logo, grid_capsule, and header integrates Steam games cleanly into the same media priority pipeline as custom games. Wiring logoUrl, capsuleUrl, and headerUrl into GameDisplayInfo looks consistent with the new data model.

Also applies to: 287-303


1054-1081: Post-uninstall delay before navigating back is reasonable

Adding a delay(250) on the main dispatcher before calling onBack() after a successful uninstall gives the filesystem/UI a moment to settle so the library view can refresh accurately. Because this is a suspending delay in a coroutine, it shouldn’t block the main thread.

app/src/main/java/app/gamenative/utils/SteamGridDB.kt (1)

413-457: New header existence checks and SteamGridDB image-finder helpers look good

Including existingHeader in the “all images already exist” shortcut and returning both gridHeroPath and headerPath in ImageFetchResult aligns with the rest of the media pipeline. The new findSteamGridDBImage and findSteamGridDBImageByAppId helpers are straightforward and flexible (imageType + optional excludePattern), and returning Uri.fromFile(...).toString() is appropriate for consumers like Coil.

Also applies to: 540-579

app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt (2)

473-491: LGTM - LoadingToast integration is well-structured.

The state observation and toast visibility logic correctly handles the fetching and done states. The onDismiss callback properly clears both states to prevent stale UI.


652-658: LGTM - Media URL parameters for dialog.

Passing the media URLs to ContainerConfigDialog enables richer display of game images in the configuration dialog, aligning with the PR's custom media support goals.

app/src/main/java/app/gamenative/utils/CustomMediaUtils.kt (2)

210-231: LGTM - Cache-busting helper is well-implemented.

The function correctly handles different model types (String, Uri) and appends version parameters only to supported URL schemes. The fallback for unknown types is appropriate.


38-49: LGTM - Media versioning pattern for cache invalidation.

The StateFlow-based versioning is a clean approach to trigger UI refreshes when custom media changes. The singleton pattern is appropriate for app-wide media management.

Comment on lines +82 to +86
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(1000f) // High z-index to appear on top
) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential touch blocking on underlying UI.

The Box with fillMaxSize() covers the entire screen. While it's transparent, it may still intercept touch events, preventing interaction with the UI below the toast. Consider using Modifier.wrapContentSize() with alignment, or adding .clickable(enabled = false, onClick = {}) with indication = null to pass through touches.

         Box(
             modifier = Modifier
-                .fillMaxSize()
+                .fillMaxSize()
+                .then(Modifier) // Allow touch passthrough - the toast should not block interaction
                 .zIndex(1000f) // High z-index to appear on top
-        ) {
+        ).let { /* Consider using pointerInput to pass through touches */ }

Alternative approach using wrapContentSize:

Box(
    modifier = Modifier
        .fillMaxSize()
        .zIndex(1000f),
    contentAlignment = Alignment.BottomCenter
) {
    // Move AnimatedVisibility content here without the nested fillMaxSize Box
}
🤖 Prompt for AI Agents
In app/src/main/java/app/gamenative/ui/component/dialog/LoadingToast.kt around
lines 82-86, the Box currently uses Modifier.fillMaxSize() which covers the
whole screen and can intercept touch events; replace it with
Modifier.wrapContentSize(alignment = Alignment.BottomCenter) and move the
AnimatedVisibility content into that Box so it only occupies the toast area, or
if you need to keep full-size use .clickable(enabled = false, onClick = {}) with
indication = null (and no interactionSource) so touches pass through; ensure
zIndex remains if needed and remove any nested full-size Box that still
intercepts input.

Comment on lines +152 to +168
private fun centerCropResize(src: Bitmap, targetW: Int, targetH: Int): Bitmap {
val srcW = src.width
val srcH = src.height
val scale = maxOf(targetW.toFloat() / srcW, targetH.toFloat() / srcH)
val scaledW = (srcW * scale).toInt()
val scaledH = (srcH * scale).toInt()
val scaled = Bitmap.createScaledBitmap(src, scaledW, scaledH, true)
val x = (scaledW - targetW) / 2
val y = (scaledH - targetH) / 2
return Bitmap.createBitmap(
scaled,
x.coerceAtLeast(0),
y.coerceAtLeast(0),
targetW.coerceAtMost(scaled.width),
targetH.coerceAtMost(scaled.height)
)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Memory leak: intermediate scaled bitmap is not recycled.

Bitmap.createScaledBitmap creates a new bitmap that should be recycled after Bitmap.createBitmap extracts the cropped region. Without recycling, large images can accumulate and cause OOM.

 private fun centerCropResize(src: Bitmap, targetW: Int, targetH: Int): Bitmap {
     val srcW = src.width
     val srcH = src.height
     val scale = maxOf(targetW.toFloat() / srcW, targetH.toFloat() / srcH)
     val scaledW = (srcW * scale).toInt()
     val scaledH = (srcH * scale).toInt()
     val scaled = Bitmap.createScaledBitmap(src, scaledW, scaledH, true)
     val x = (scaledW - targetW) / 2
     val y = (scaledH - targetH) / 2
-    return Bitmap.createBitmap(
+    val result = Bitmap.createBitmap(
         scaled,
         x.coerceAtLeast(0),
         y.coerceAtLeast(0),
         targetW.coerceAtMost(scaled.width),
         targetH.coerceAtMost(scaled.height)
     )
+    if (scaled != src && scaled != result) {
+        scaled.recycle()
+    }
+    return result
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun centerCropResize(src: Bitmap, targetW: Int, targetH: Int): Bitmap {
val srcW = src.width
val srcH = src.height
val scale = maxOf(targetW.toFloat() / srcW, targetH.toFloat() / srcH)
val scaledW = (srcW * scale).toInt()
val scaledH = (srcH * scale).toInt()
val scaled = Bitmap.createScaledBitmap(src, scaledW, scaledH, true)
val x = (scaledW - targetW) / 2
val y = (scaledH - targetH) / 2
return Bitmap.createBitmap(
scaled,
x.coerceAtLeast(0),
y.coerceAtLeast(0),
targetW.coerceAtMost(scaled.width),
targetH.coerceAtMost(scaled.height)
)
}
private fun centerCropResize(src: Bitmap, targetW: Int, targetH: Int): Bitmap {
val srcW = src.width
val srcH = src.height
val scale = maxOf(targetW.toFloat() / srcW, targetH.toFloat() / srcH)
val scaledW = (srcW * scale).toInt()
val scaledH = (srcH * scale).toInt()
val scaled = Bitmap.createScaledBitmap(src, scaledW, scaledH, true)
val x = (scaledW - targetW) / 2
val y = (scaledH - targetH) / 2
val result = Bitmap.createBitmap(
scaled,
x.coerceAtLeast(0),
y.coerceAtLeast(0),
targetW.coerceAtMost(scaled.width),
targetH.coerceAtMost(scaled.height)
)
if (scaled != src && scaled != result) {
scaled.recycle()
}
return result
}
🤖 Prompt for AI Agents
In app/src/main/java/app/gamenative/utils/CustomMediaUtils.kt around lines 152
to 168, the intermediate 'scaled' bitmap created by Bitmap.createScaledBitmap is
never recycled which can cause OOM; after creating the final cropped bitmap,
check whether the result is a different instance than 'scaled' and if so call
scaled.recycle() (or otherwise recycle scaled when it is no longer needed),
ensuring you don't recycle the bitmap you return; wrap the creation and
recycling logic so the scaled bitmap is always recycled on all code paths (e.g.,
try/finally) while leaving the returned bitmap intact.

Comment on lines 141 to 146
/**
* Download and save an image from a URL
* @return Pair of (file path, isHorizontal) or null if failed
*/
private suspend fun downloadAndSaveImage(
imageUrl: String,
gameFolder: File,
fileName: String
): Pair<String, Boolean?>? = withContext(Dispatchers.IO) {
try {
// Download the image
val imageRequest = Request.Builder()
.url(imageUrl)
.build()

val imageResponse = httpClient.newCall(imageRequest).execute()

if (!imageResponse.isSuccessful) {
Timber.tag("SteamGridDB").w("Failed to download image from $imageUrl - HTTP ${imageResponse.code}")
return@withContext null
}

val imageBytes = imageResponse.body?.bytes() ?: return@withContext null

// Determine orientation
val isHorizontal = isImageHorizontal(imageBytes)

// Determine file extension from URL
val extension = when {
imageUrl.contains(".png", ignoreCase = true) -> ".png"
imageUrl.contains(".jpg", ignoreCase = true) -> ".jpg"
imageUrl.contains(".jpeg", ignoreCase = true) -> ".jpg"
imageUrl.contains(".webp", ignoreCase = true) -> ".webp"
else -> ".png" // Default to PNG
}

val outputFile = File(gameFolder, "$fileName$extension")

// Save to file
FileOutputStream(outputFile).use { it.write(imageBytes) }

Timber.tag("SteamGridDB").i("Saved image to ${outputFile.absolutePath} (horizontal: $isHorizontal)")
return@withContext Pair(outputFile.absolutePath, isHorizontal)
} catch (e: Exception) {
Timber.tag("SteamGridDB").e(e, "Error downloading image from $imageUrl")
return@withContext null
}
}

/**
* Fetch grids for a game, find horizontal and vertical images, and save them separately.
* Fetch grids for a game, find horizontal, vertical, and header-sized images, and save them separately.
* @param gameId The SteamGridDB game ID
* @param gameFolder The folder where images should be saved
* @return Pair of (heroPath, capsulePath) where hero is horizontal grid and capsule is vertical grid
* @return Triple of (heroPath, capsulePath, headerPath) where hero is horizontal grid, capsule is vertical grid, and header is 460x215 image
*/
Copy link

@coderabbitai coderabbitai bot Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Header fetching via fetchAndSaveImage is broken for imageType = "header"

fetchGameImages now does:

val headerPath = headerPathFromGrids ?: if (!existingHeader) {
    fetchAndSaveImage(searchResult.gameId, gameFolder, "header")
} else { ... }

But fetchAndSaveImage only handles "hero" and "logo":

val endpoint = when (imageType) {
    "hero" -> HEROES_ENDPOINT
    "logo" -> LOGOS_ENDPOINT
    else -> {
        Timber.w("Unknown image type: $imageType (grids should use fetchGrids)")
        return@withContext null
    }
}

So the "header" call always hits the else branch, logs an “Unknown image type: header” warning, and returns null. As a result, header images will never be fetched from the heroes endpoint when grids don’t supply one, and existingHeader will stay false until a header is created some other way.

A minimal fix is to teach fetchAndSaveImage about "header" and name the file consistently:

-        try {
-            val endpoint = when (imageType) {
-                "hero" -> HEROES_ENDPOINT
-                "logo" -> LOGOS_ENDPOINT
-                else -> {
-                    Timber.tag("SteamGridDB").w("Unknown image type: $imageType (grids should use fetchGrids)")
-                    return@withContext null
-                }
-            }
+        try {
+            val endpoint = when (imageType) {
+                "hero" -> HEROES_ENDPOINT
+                "header" -> HEROES_ENDPOINT
+                "logo" -> LOGOS_ENDPOINT
+                else -> {
+                    Timber.tag("SteamGridDB").w("Unknown image type: $imageType (grids should use fetchGrids)")
+                    return@withContext null
+                }
+            }
@@
-            val fileName = "steamgriddb_${imageType}$extension"
+            val fileName = when (imageType) {
+                "header" -> "steamgriddb_header$extension"
+                "hero" -> "steamgriddb_hero$extension"
+                else -> "steamgriddb_${imageType}$extension"
+            }

This preserves backwards compatibility with any existing steamgriddb_hero* files while ensuring new headers are stored under the steamgriddb_header* prefix expected by the existingHeader checks and findSteamGridDBImage.

Also applies to: 182-281, 392-526

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Maaggel, I think there may be some confusion here. The issue CodeRabbit raised isn't about gameId or appId extraction—it's about the imageType parameter handling.

The problem is that in fetchGameImages(), you're calling:

fetchAndSaveImage(searchResult.gameId, gameFolder, "header")

But inside fetchAndSaveImage(), the imageType switch statement only handles "hero" and "logo":

val endpoint = when (imageType) {
    "hero" -> HEROES_ENDPOINT
    "logo" -> LOGOS_ENDPOINT
    else -> {
        Timber.tag("SteamGridDB").w("Unknown image type: $imageType (grids should use fetchGrids)")
        return@withContext null
    }
}

So when you pass "header" as the imageType, it will hit the else branch, log a warning "Unknown image type: header", and return null. This means header images will never be fetched via fetchAndSaveImage() when grids don't provide one.

Would you like to apply the fix CodeRabbit suggested—adding "header" to the switch statement and mapping it to HEROES_ENDPOINT?


🧠 Learnings used
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt (1)

2022-2034: Bug: LibraryItem still missing gameId – custom media previews will fail.

This issue was flagged in the previous review but remains unresolved. The LibraryItem is constructed with only appId and gameSource, leaving gameId at its default value. Because CustomMediaUtils helpers (like saveCustomLogo, hasCustomLogo) key custom media by gameId: Int, and GameImageUtils.getGameImage relies on libraryItem.gameId, the previews won't display custom media that was just saved.

Apply this diff to include gameId and ensure previews refresh correctly:

-                val libraryItem = remember(appId) {
-                    if (appId != null && appId.isNotEmpty()) {
+                val libraryItem = remember(appId, gameId) {
+                    if (appId != null && appId.isNotEmpty() && gameId != null) {
                         val gameSource = if (appId.startsWith("CUSTOM_GAME_")) {
                             GameSource.CUSTOM_GAME
                         } else {
                             GameSource.STEAM
                         }
                         LibraryItem(
                             appId = appId,
+                            gameId = gameId,
                             gameSource = gameSource
                         )
                     } else null
                 }
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt (1)

2037-2045: Consider including media URLs in remember keys for completeness.

The remember blocks for media models currently key on (mediaVersion, libraryItem). While this covers the primary update triggers, the steamUrl parameter (e.g., mediaLogoUrl) is not included. If the parent passes different URLs during the dialog's lifetime, the preview won't update until mediaVersion or libraryItem changes.

For example, for the logo model:

-                val currentLogoModel: Any? = remember(mediaVersion, libraryItem) {
+                val currentLogoModel: Any? = remember(mediaVersion, libraryItem, mediaLogoUrl) {
                     if (libraryItem != null) {
                         GameImageUtils.getGameImage(
                             libraryItem = libraryItem,
                             imageType = "logo",
                             steamUrl = mediaLogoUrl
                         )
                     } else mediaLogoUrl
                 }

Apply similar changes for icon, header, capsule, and hero models with their respective URL parameters.

Also applies to: 2064-2072, 2091-2099, 2118-2126, 2151-2159

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6fade35 and 67d50e5.

📒 Files selected for processing (5)
  • app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt (9 hunks)
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt (8 hunks)
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/CustomGameAppScreen.kt (6 hunks)
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (5 hunks)
  • app/src/main/res/values/strings.xml (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/CustomGameAppScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt
🔇 Additional comments (4)
app/src/main/res/values/strings.xml (2)

93-94: String additions look good; review pending on media duplicates.

The new strings for custom game deletion feedback (custom_game_deleted, custom_game_delete_failed) are appropriately named and formatted. The update to add_custom_game_dialog_message effectively communicates to users that images can be fetched later. The base_app_images_fetching string aligns well with the LoadingToast feature.

Once the media string duplication concern is resolved, these changes will be complete.

Also applies to: 278-278, 282-282


420-441: The repository clone failed. Let me analyze the information already provided in the review comment to determine the status of this issue.

Looking at the review comment structure:

  1. Provided code context: The snippet labeled "Snippet showing the final state of code at these lines" (lines 420-441) shows a single media management block with 20 string resources.

  2. AI summary claim: The comment states the AI summary "indicates media management strings were added in two insertion points, resulting in duplicate entries" - but this summary text is not actually shown in the provided context.

  3. Critical observation: The code snippet is explicitly labeled as showing the "final state" at lines 420-441. If duplicates existed in the final state, they would appear in this excerpt or immediately before/after it.

Based on the evidence available in the review comment itself:

No duplicate media string keys are visible in the provided final-state code excerpt. The media management block (lines 420-441) contains each key name exactly once. The strings are well-formed and complete.

The inconsistency between the AI summary's claim and the provided code suggests either:

  • The AI summary was incorrect or mischaracterized the changes
  • Duplicates were present in an earlier state but have been corrected
  • The summary refers to a different aspect not captured in the code excerpt

The duplicate media management strings concern cannot be verified from the available code context, but no duplicates are visible in the final-state code excerpt provided.

The review comment's core verification request cannot be fully satisfied without access to the complete file or git diff. The provided excerpt alone shows well-formed, non-duplicate string resources. If actual duplicates exist elsewhere in the file, they would need to be identified through direct file inspection.

app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt (2)

143-243: MediaSection implementation looks solid.

The composable properly handles nullable gameId, includes cache-busting via mediaVersion, and provides appropriate user feedback with Toast messages. The null guard at line 205 correctly protects all operations requiring a valid gameId.


254-260: LGTM: Backward-compatible signature extension.

The new optional parameters for media URLs, gameId, and appId are properly defaulted to null, maintaining backward compatibility while enabling the Media tab functionality.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR to address, but I think it'd be really good to pull the components of this dialog out as it's getting absolutely massive

Comment on lines +2028 to +2033
if (appId != null && appId.isNotEmpty()) {
val gameSource = if (appId.startsWith("CUSTOM_GAME_")) {
GameSource.CUSTOM_GAME
} else {
GameSource.STEAM
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to find a nicer way of doing this once we bring GOG support


// Small delay to ensure file system updates are complete
// before navigating back (list will auto-refresh when displayed)
delay(250)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't want to use these delays. you should emit app events instead.

Comment on lines +46 to +53
if (progress < 0) {
// Show spinner for indeterminate progress
CircularProgressIndicator()
Spacer(modifier = Modifier.height(16.dp))
}
Text(message)
Spacer(modifier = Modifier.height(16.dp))
if (progress >= 0) {
Spacer(modifier = Modifier.height(16.dp))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants