-
Notifications
You must be signed in to change notification settings - Fork 79
Custom Game media support #229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
WalkthroughAdds per-game custom media support: new MediaUtils for saving/resizing/serving media and cache-busting, moves image composables to utils, adds Media UI (new tab, previews, pick/reset flows), wires media into library screens/items, cleans up media on app deletion, updates imports and string resources. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as Dialog/Screen
participant Picker as Content Picker
participant Media as MediaUtils
participant FS as FileSystem
User->>UI: Open Media tab / pick media
UI->>Picker: Launch GetContent
Picker->>User: User selects image
Picker->>UI: Return Uri
UI->>Media: saveCustom[Type](context, appId, uri)
Media->>FS: decodeBitmap(uri)
Media->>Media: resize/crop to target dims
Media->>FS: save JPEG/PNG file
Media->>Media: notifyMediaChanged() (mediaVersionFlow++)
Media-->>UI: success
UI->>Media: collect mediaVersionFlow (collectAsState)
UI->>UI: bustCache(model, mediaVersion) -> reload preview
UI->>User: show updated preview
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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 (3)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
722-728: Consider logging media deletion failures and narrowing the catch clause.The custom media cleanup logic is wrapped in a broad catch that silently suppresses all failures. While this "best effort" approach is reasonable for cleanup operations, consider these refinements:
- Log failures to aid debugging orphaned media files
- Narrow
catch (_: Throwable)tocatch (e: Exception)to avoid catching serious errors likeOutOfMemoryError- Optionally, delete media before the directory to prevent orphaned media if directory deletion fails
Apply this diff:
val appDirPath = getAppDirPath(appId) +// Delete custom media first to avoid orphans if directory deletion fails +try { + app.gamenative.utils.MediaUtils.deleteAllMediaFor(appId) +} catch (e: Exception) { + Timber.w(e, "Failed to delete custom media for app $appId") +} + val removed = File(appDirPath).deleteRecursively() -// Also remove any custom media for this game stored under app data -try { - app.gamenative.utils.MediaUtils.deleteAllMediaFor(appId) -} catch (_: Throwable) { } - return removedapp/src/main/java/app/gamenative/ui/screen/library/components/LibraryAppItem.kt (2)
143-173: Consider collecting mediaVersionFlow once to avoid redundant subscriptions.The
mediaVersionFlowis collected twice in this composable (lines 144 and 157) within the LIST and GRID branches. EachcollectAsStatecreates a separate flow subscription, which is unnecessary since both branches are part of the same composition scope.Hoist the collection to the composable scope:
+ // Observe media changes to refresh images immediately when user updates or resets + val mediaVersion by app.gamenative.utils.MediaUtils.mediaVersionFlow.collectAsState(initial = 0) + // Game icon Box( modifier = Modifier .clip(RoundedCornerShape(12.dp)), ) { if (paneType == PaneType.LIST) { - // Observe media changes to refresh icon immediately when user updates or resets - val mediaVersion by app.gamenative.utils.MediaUtils.mediaVersionFlow.collectAsState(initial = 0) val iconModel: Any? = remember(mediaVersion, appInfo.gameId) { app.gamenative.utils.MediaUtils.getCustomIconUri(appInfo.gameId) ?: appInfo.clientIconUrl } ListItemImage( modifier = Modifier.size(56.dp), imageModifier = Modifier.clip(RoundedCornerShape(10.dp)), image = { app.gamenative.utils.bustCache(iconModel, mediaVersion) } ) } else { val aspectRatio = if (paneType == PaneType.GRID_CAPSULE) { 2/3f } else { 460/215f } - // Observe media changes to refresh images immediately when user updates or resets - val mediaVersion by app.gamenative.utils.MediaUtils.mediaVersionFlow.collectAsState(initial = 0) - - val baseModel: Any? = if (paneType == PaneType.GRID_CAPSULE) {
143-173: Prefer importing MediaUtils to reduce verbosity.The fully qualified name
app.gamenative.utils.MediaUtils.mediaVersionFlowis repeated multiple times. ImportingMediaUtilswould improve readability.Add the import at the top of the file:
+import app.gamenative.utils.MediaUtilsThen simplify the references:
- val mediaVersion by app.gamenative.utils.MediaUtils.mediaVersionFlow.collectAsState(initial = 0) + val mediaVersion by MediaUtils.mediaVersionFlow.collectAsState(initial = 0) val iconModel: Any? = remember(mediaVersion, appInfo.gameId) { - app.gamenative.utils.MediaUtils.getCustomIconUri(appInfo.gameId) + MediaUtils.getCustomIconUri(appInfo.gameId) ?: appInfo.clientIconUrl }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
app/src/main/java/app/gamenative/service/SteamService.kt(1 hunks)app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt(6 hunks)app/src/main/java/app/gamenative/ui/component/dialog/GamesListDialog.kt(1 hunks)app/src/main/java/app/gamenative/ui/component/dialog/ProfileDialog.kt(1 hunks)app/src/main/java/app/gamenative/ui/component/topbar/AccountButton.kt(1 hunks)app/src/main/java/app/gamenative/ui/screen/chat/ChatScreen.kt(1 hunks)app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt(3 hunks)app/src/main/java/app/gamenative/ui/screen/library/components/LibraryAppItem.kt(3 hunks)app/src/main/java/app/gamenative/ui/util/Images.kt(0 hunks)app/src/main/java/app/gamenative/utils/MediaUtils.kt(1 hunks)
💤 Files with no reviewable changes (1)
- app/src/main/java/app/gamenative/ui/util/Images.kt
🧰 Additional context used
🧠 Learnings (4)
📚 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
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 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/service/SteamService.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/service/SteamService.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/ui/screen/library/components/LibraryAppItem.kt (1)
app/src/main/java/app/gamenative/utils/MediaUtils.kt (1)
ListItemImage(238-263)
🔇 Additional comments (9)
app/src/main/java/app/gamenative/ui/component/topbar/AccountButton.kt (1)
23-23: Import migration verified and approved.The import path change for
SteamIconImagefromapp.gamenative.ui.util.SteamIconImagetoapp.gamenative.utils.SteamIconImageis correct and properly supported by the codebase. The definition exists inapp/src/main/java/app/gamenative/utils/MediaUtils.kt, confirming the refactoring is consistent with the utility reorganization.app/src/main/java/app/gamenative/ui/component/dialog/GamesListDialog.kt (1)
42-42: LGTM! Clean import path update.The import path change for
ListItemImagefromui.utiltoutilsaligns with the broader package reorganization to centralize image utilities.app/src/main/java/app/gamenative/ui/component/dialog/ProfileDialog.kt (1)
45-45: LGTM! Clean import refactoring.The import path update aligns with the PR's reorganization of image utilities from
ui.utiltoutilspackage. TheSteamIconImageusage remains unchanged and consistent.app/src/main/java/app/gamenative/ui/screen/chat/ChatScreen.kt (1)
79-79: Import migration verified as complete and consistent.The import path change is correct. All files importing
ListItemImagehave been updated to use the newapp.gamenative.utilslocation, with no remaining imports from the oldui.utilpackage. The component definition exists atMediaUtils.kt:239and is properly utilized in GamesListDialog, ChatScreen, and LibraryAppItem.app/src/main/java/app/gamenative/ui/screen/library/components/LibraryAppItem.kt (2)
63-63: Import path migration aligns with PR objectives.The change from
app.gamenative.ui.util.ListItemImagetoapp.gamenative.utils.ListItemImagecorrectly reflects the migration of rendering helpers fromui.utiltoutilsas described in the PR summary.
145-168: Custom media resolution with Steam CDN fallback is well-implemented.The logic correctly prefers custom media URIs (icon, capsule, header) when available and falls back to Steam-hosted assets. The use of
rememberwithmediaVersionas a key ensures recomputation when media changes, andbustCacheforces Coil to reload images.app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt (3)
953-962: Hero image refresh implementation is correct and consistent.The hero image loading correctly observes
mediaVersionFlow, prefers custom hero URIs fromMediaUtils.getCustomHeroUri, falls back toappInfo.getHeroUrl(), and appliesbustCacheto force immediate refresh when media changes. This pattern is consistent with the implementation inLibraryAppItem.kt.
520-526: Media parameters correctly passed to ContainerConfigDialog.Passing
mediaHeroUrl,mediaLogoUrl,mediaCapsuleUrl,mediaHeaderUrl,mediaIconUrl, andgameIdenables the media management features within the container configuration dialog, as described in the PR objectives.
55-55: Import addition supports media version flow observation.The
collectAsStateimport is correctly added to support observingmediaVersionFlowfor reactive image updates.
app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt
Show resolved
Hide resolved
app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt
Outdated
Show resolved
Hide resolved
| CoilImage( | ||
| modifier = Modifier | ||
| .widthIn(min = 150.dp, max = 300.dp), | ||
| imageModel = { app.gamenative.utils.bustCache(currentLogoModel, mediaVersion) }, | ||
| imageOptions = ImageOptions(contentScale = ContentScale.Fit), | ||
| previewPlaceholder = painterResource(app.gamenative.R.drawable.testliblogo), | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing accessibility content descriptions for images.
The CoilImage components in the media previews lack content descriptions, making them inaccessible to screen reader users.
Add content descriptions to the ImageOptions:
CoilImage(
modifier = Modifier.widthIn(min = 150.dp, max = 300.dp),
imageModel = { app.gamenative.utils.bustCache(currentLogoModel, mediaVersion) },
- imageOptions = ImageOptions(contentScale = ContentScale.Fit),
+ imageOptions = ImageOptions(
+ contentScale = ContentScale.Fit,
+ contentDescription = "Game logo preview"
+ ),
previewPlaceholder = painterResource(app.gamenative.R.drawable.testliblogo),
)Also applies to: 1820-1827, 1903-1910, 1986-1999, 2074-2087
🤖 Prompt for AI Agents
In app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt
around lines 1741-1747 (and similarly update the other occurrences at 1820-1827,
1903-1910, 1986-1999, 2074-2087), the CoilImage calls are missing accessibility
content descriptions; add a contentDescription to the ImageOptions for each
image (or pass a contentDescription prop accepted by CoilImage) — use a
meaningful, localized string resource (e.g., string.resource name like
R.string.logo_preview_description) or use contentDescription = null only for
purely decorative images; update the ImageOptions constructors to include
contentDescription = "..." (or reference to stringResource) so screen readers
can announce the images.
| private val _mediaVersion = kotlinx.coroutines.flow.MutableStateFlow(0) | ||
| val mediaVersionFlow: kotlinx.coroutines.flow.StateFlow<Int> = _mediaVersion | ||
| fun notifyMediaChanged() { _mediaVersion.value = _mediaVersion.value + 1 } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make the media version increment atomic.
notifyMediaChanged does a read–modify–write on _mediaVersion.value. When two coroutines call this at nearly the same time, one increment can be lost, leaving observers stuck on the old version and skipping a needed cache bust. MutableStateFlow.update { it + 1 } performs the increment atomically and avoids this race. Please move to update (or getAndUpdate) so every media change bumps the version reliably. (zenn.dev)
🤖 Prompt for AI Agents
In app/src/main/java/app/gamenative/utils/MediaUtils.kt around lines 39 to 41,
notifyMediaChanged currently performs a non-atomic read–modify–write on
_mediaVersion.value which can lose increments under concurrency; replace that
with an atomic update call on the MutableStateFlow (e.g., _mediaVersion.update {
it + 1 } or use getAndUpdate) so the increment is performed atomically and no
version bumps are lost, keeping mediaVersionFlow observers reliable.
There was a problem hiding this 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)
1741-1747: Add accessibility content descriptions for screen readers.The
CoilImagefor the logo preview lacks acontentDescription, making it inaccessible to screen reader users. This issue also applies to the Icon (lines 1820-1827), Hero (lines 1903-1910), Capsule (lines 1986-1999), and Header (lines 2074-2087) images.Add
contentDescriptionto eachImageOptions:CoilImage( modifier = Modifier.widthIn(min = 150.dp, max = 300.dp), imageModel = { app.gamenative.utils.bustCache(currentLogoModel, mediaVersion) }, - imageOptions = ImageOptions(contentScale = ContentScale.Fit), + imageOptions = ImageOptions( + contentScale = ContentScale.Fit, + contentDescription = context.getString(R.string.media_logo_preview_description) + ), previewPlaceholder = painterResource(app.gamenative.R.drawable.testliblogo), )Apply similar fixes to the other four media sections (Icon, Hero, Capsule, Header) using appropriate string resources for each (e.g.,
R.string.media_icon_preview_description,R.string.media_hero_preview_description, etc.).
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt (1)
1720-2131: Consider extracting a reusable composable to reduce ~385 lines of duplication.The five media sections (Logo, Icon, Hero, Capsule, Header) follow an identical pattern with only minor variations in titles, dimensions, and method names. This duplication makes the code harder to maintain—any bug fix or enhancement requires changes in five places.
Extract a reusable
MediaSectioncomposable:@Composable private fun MediaSection( titleRes: Int, hintRes: Int, noMediaTitleRes: Int, gameId: Int?, mediaVersion: Int, currentModel: Any?, placeholderRes: Int, imageModifier: Modifier, imageOptions: ImageOptions, hasCustomMedia: (Int) -> Boolean, saveCustomMedia: (Context, Int, Uri) -> Boolean, resetCustomMedia: (Int) -> Unit, ) { val context = LocalContext.current Text( text = stringResource(titleRes), color = Color.White, style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp) ) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { if (currentModel != null && (currentModel as? String)?.isNotBlank() != false) { CoilImage( modifier = imageModifier, imageModel = { app.gamenative.utils.bustCache(currentModel, mediaVersion) }, imageOptions = imageOptions, previewPlaceholder = painterResource(placeholderRes), ) } else { SettingsCenteredLabel( colors = settingsTileColors(), title = { Text(text = stringResource(noMediaTitleRes)) }, subtitle = { Text(text = stringResource(R.string.media_open_specific)) }, ) } } } Text( text = stringResource(hintRes), color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) ) if (gameId != null) { val isCustom = remember(mediaVersion, gameId) { hasCustomMedia(gameId) } val picker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> if (uri != null) { val ok = saveCustomMedia(context, gameId, uri) val mediaName = stringResource(titleRes) Toast.makeText( context, if (ok) context.getString(R.string.media_updated, mediaName) else context.getString(R.string.media_update_failed, mediaName.lowercase()), Toast.LENGTH_SHORT ).show() } } Row( modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp), horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally) ) { Button(onClick = { picker.launch("image/*") }) { Text(stringResource(R.string.media_choose_image)) } if (isCustom) { OutlinedButton( onClick = { resetCustomMedia(gameId) Toast.makeText(context, context.getString(R.string.media_reverted), Toast.LENGTH_SHORT).show() }, ) { Text(stringResource(R.string.media_reset_to_default)) } } } } Spacer(modifier = Modifier.padding(8.dp)) HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) }Then replace each section with a single call:
MediaSection( titleRes = R.string.media_logo_title, hintRes = R.string.media_logo_hint, noMediaTitleRes = R.string.media_no_logo, gameId = gameId, mediaVersion = mediaVersion, currentModel = currentLogoModel, placeholderRes = R.drawable.testliblogo, imageModifier = Modifier.widthIn(min = 150.dp, max = 300.dp), imageOptions = ImageOptions(contentScale = ContentScale.Fit), hasCustomMedia = MediaUtils::hasCustomLogo, saveCustomMedia = MediaUtils::saveCustomLogo, resetCustomMedia = MediaUtils::resetCustomLogo, )This reduces ~385 lines to ~80 lines (5 calls × ~16 lines each) plus one ~60-line helper, improving maintainability significantly.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt(6 hunks)app/src/main/res/values/strings.xml(1 hunks)
🔇 Additional comments (5)
app/src/main/res/values/strings.xml (2)
124-145: Well-structured media string additions.The new media-related strings are well-organized with a clear section comment. All titles, hints, action labels, and status messages follow consistent naming conventions and provide helpful context (e.g., specific dimension recommendations in hints). The use of
%splaceholders inmedia_updatedandmedia_update_failedaligns with Android localization best practices for dynamic content.
143-145: Verified: Status message placeholders are correctly formatted in calling code.The search results confirm that
media_updatedandmedia_update_failedare properly used withcontext.getString()formatting across all five call sites in ContainerConfigDialog.kt. Each usage passes the appropriate media type string (Logo, Icon, Hero, Capsule, Header) as the format argument, ensuring users see messages like "Logo updated" rather than raw%splaceholders.app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt (3)
146-151: LGTM: Function signature extensions maintain backward compatibility.The six new nullable parameters with default values ensure existing callers continue to work while enabling the new media management features.
715-715: Media tab placement is logical.Inserting "Media" before "Advanced" groups related settings appropriately.
1715-1717: Excellent reactive media refresh pattern.Observing
mediaVersionFlowwithcollectAsStateensures previews update immediately when media changes, providing a smooth user experience without requiring manual cache clearing.
There was a problem hiding this 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)
171-176: Missing accessibility content description for images.The
CoilImagelacks acontentDescriptioninImageOptions, making it inaccessible to screen reader users. This issue was flagged in a previous review and remains unresolved.Add a
contentDescriptionparameter toImageOptions:CoilImage( modifier = imageModifier, imageModel = { app.gamenative.utils.bustCache(currentModel, mediaVersion) }, - imageOptions = ImageOptions(contentScale = imageContentScale), + imageOptions = ImageOptions( + contentScale = imageContentScale, + contentDescription = stringResource(titleRes) + " preview" + ), previewPlaceholder = painterResource(placeholderRes), )Alternatively, if you prefer localized descriptions, add dedicated string resources (e.g.,
R.string.media_logo_preview_description) and pass them via a newcontentDescriptionRes: Intparameter toMediaSection.
🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt (2)
170-170: Simplify the null/blank check for clarity.The expression
(currentModel as? String)?.isNotBlank() != falseis correct but convoluted. It works becausenull != falseevaluates totrue, but this is not immediately obvious to readers.Consider a more explicit check:
-if (currentModel != null && (currentModel as? String)?.isNotBlank() != false) { +val hasValidModel = currentModel != null && (currentModel as? String)?.isNotBlank() != false +if (hasValidModel) {Or even clearer:
-if (currentModel != null && (currentModel as? String)?.isNotBlank() != false) { +val isValidModel = when (currentModel) { + null -> false + is String -> currentModel.isNotBlank() + else -> true +} +if (isValidModel) {
1818-1824: Extract repeated currentModel computation into a helper function.The pattern for computing
currentModelis repeated identically for all five media types (Logo, Icon, Hero, Capsule, Header). This creates unnecessary duplication and makes the code harder to maintain.Extract a helper function at the top of the Media tab block:
@Composable fun rememberMediaModel( mediaVersion: Int, gameId: Int?, getCustomUri: (Int) -> android.net.Uri?, fallbackUrl: String? ): Any? = remember(mediaVersion, gameId) { gameId?.let { getCustomUri(it) ?: fallbackUrl } ?: fallbackUrl }Then simplify each media section:
-val currentLogoModel: Any? = remember(mediaVersion, gameId) { - val gid = gameId - if (gid != null) { - val custom = app.gamenative.utils.MediaUtils.getCustomLogoUri(gid) - custom ?: mediaLogoUrl - } else mediaLogoUrl -} +val currentLogoModel = rememberMediaModel( + mediaVersion, gameId, + app.gamenative.utils.MediaUtils::getCustomLogoUri, + mediaLogoUrl +)Apply the same pattern to Icon, Hero, Capsule, and Header.
Also applies to: 1841-1847, 1866-1872, 1891-1897, 1922-1928
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt(7 hunks)
🔇 Additional comments (2)
app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt (2)
137-232: Good refactor: MediaSection eliminates code duplication.The
MediaSectioncomposable successfully addresses the previous review's concern about ~400 lines of duplicated media UI. This reusable component consolidates the common pattern for all five media types (Logo, Icon, Hero, Capsule, Header) and makes the codebase more maintainable.The use of string resources for localization and the parameterization of media-specific details (image modifiers, content scales, MediaUtils functions) demonstrate good design.
1812-1952: Media tab integration looks solid.The Media tab is well-integrated:
- Correctly observes
mediaVersionFlowfor reactive updates- Falls back gracefully to Steam CDN URLs when custom media is absent
- Provides appropriate per-media controls (pick/reset) conditioned on
gameId- Uses media-specific modifiers and content scales for proper presentation
The implementation aligns with the PR objectives to support custom media without requiring app restarts.
|
Tested this locally on my own device (Oneplus 12). Worked well with multiple images tested per category, also it worked with the correct dimensions in each of the listing styles too. |
|
I'm pretty happy with this, although it'd be good to get your take. I've tested it on my OP12 device and worked well. |
|
I'll take a look! Looks like it has a conflict, maybe string was added. I can take a look at that. |
Implemented “custom media support” for games
Adds full support for user-defined Logo, Icon, Hero Image, Capsule image and Header, allowing players to personalize how each game is visually represented throughout the app, and adding game media if it's missing.
The feature:
What’s included
MediaUtils.ktutility file:mediaVersionFlowLibraryAppScreen,GamesListDialog,ProfileDialog, etc.) to useMediaUtilsHow it works
mediaVersionFlow, forcing all relevant Coil images to refresh.Highlights
Screenshots etc.:



Summary by CodeRabbit
New Features
Bug Fixes