feat: ui/ux overhaul from ObfuscatedVoid & Phobos + remaining merge conflict resolutions#591
feat: ui/ux overhaul from ObfuscatedVoid & Phobos + remaining merge conflict resolutions#591xXJSONDeruloXx wants to merge 59 commits intoutkarshdalal:masterfrom
Conversation
…ation, UI improvements (based on screensize) and implement SavedStateHandle
… into feat/ui-overhaul-resolved-without-xserver-screeen
Based on phobos665's conflict resolution branch, merged 16 newer master commits (GOG exe launch flow, custom game Remove button, PulseAudio fixes, OOM fix, vibration fix, FEXCore preset, scrollbars, portrait controllers, Korean string fix). Resolved 4 remaining import-only conflicts in SteamService.kt, PluviaMain.kt, SteamAppScreen.kt. Accepted LibraryBottomSheet.kt deletion (replaced by GameOptionsPanel/LibraryOptionsPanel).
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughLarge UI and state overhaul: many new Compose components, library UI refactor (list/grid, options, tabs), connection/login state handling and viewmodel changes, new enums/preferences/sort options, theming overhaul, window-size utilities, resource additions, and minor tooling/file updates (.gitignore, THIRD_PARTY_NOTICES). Changes
Sequence Diagram(s)sequenceDiagram
participant UI as Compose UI
participant VM as MainViewModel
participant Auth as UserLoginViewModel
participant Steam as SteamService
participant App as PluviaApp
UI->>VM: app start / request persisted route
VM->>App: getPersistedRoute()
alt user attempts login
UI->>Auth: submit credentials or QR
Auth->>Steam: start login (QR/cred)
Steam-->>Auth: login events (LogonStarted/Ended, Connected, Disconnected)
Auth-->>UI: update login UI / snackEvents
Auth->>VM: notify OnLogonEnded
VM->>VM: set connectionState (CONNECTED/DISCONNECTED/OFFLINE_MODE)
VM-->>UI: update ConnectionStatusBanner / navigation
else user continues offline or retries
UI->>VM: continueOffline / retryConnection
VM->>Steam: startConnect/retry or set offline
VM-->>UI: update ConnectionStatusBanner
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
- SteamService.kt: remove duplicate imports (Base64, EncryptedAppTicketDao, InputStream, OutputStream, TimeUnit) - PluviaMain.kt: remove stale ConnectingServersScreen import (class deleted) - SteamAppScreen.kt: remove duplicate imports (GPUInformation, Paths), remove GameMigrationDialog import/usage (not yet in this branch) - SystemMenu.kt: fix SteamFriend event handler types (Unit not SteamFriend?), fix LaunchedEffect key parameter - LibraryAppScreen.kt: add ContainerUtils import, fix brace structure for BoxScope alignment - LibraryScreen.kt: fix safePaddingModifier returning Modifier instead of Any
There was a problem hiding this comment.
Actionable comments posted: 8
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
app/src/main/java/app/gamenative/ui/PluviaMain.kt (1)
324-367:⚠️ Potential issue | 🔴 CriticalBug:
preLaunchAppis invoked twice on theSuccesspath.Inside the
consumePendingLaunchRequest()?.letblock, theSuccessbranch already callssetLaunchedAppId,setBootToContainer, andpreLaunchApp(lines 344–354). However, lines 357–367 call them again unconditionally after thewhenblock (still within thelet). TheNotFoundbranch escapes viareturn@let, so onlySuccessis affected — it will trigger a double launch.The second block (lines 357–367) looks like a leftover from before the
resolveGameAppIdrefactor.🐛 Proposed fix — remove the duplicate launch block
) } } - viewModel.setLaunchedAppId(launchRequest.appId) - viewModel.setBootToContainer(false) - preLaunchApp( - context = context, - appId = launchRequest.appId, - setLoadingDialogVisible = viewModel::setLoadingDialogVisible, - setLoadingProgress = viewModel::setLoadingDialogProgress, - setLoadingMessage = viewModel::setLoadingDialogMessage, - setMessageDialogState = setMessageDialogState, - onSuccess = viewModel::launchApp, - ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/PluviaMain.kt` around lines 324 - 367, The Success branch of the consumePendingLaunchRequest()?.let handling already calls viewModel.setLaunchedAppId, viewModel.setBootToContainer, and preLaunchApp, but the same three calls are duplicated immediately after the when block; remove the redundant unconditional block (the second occurrence of viewModel.setLaunchedAppId, viewModel.setBootToContainer, and preLaunchApp) so that preLaunchApp is only invoked inside the GameResolutionResult.Success branch (preserve the container config override and navigation logic in that Success case).app/src/main/java/app/gamenative/service/SteamService.kt (1)
11-166:⚠️ Potential issue | 🔴 CriticalFix duplicate/ambiguous imports to unblock compilation.
The import block contains duplicate entries (Base64, EncryptedAppTicketDao, InputStream, OutputStream, TimeUnit), which the build flags as ambiguous. Remove the duplicate block.
🛠️ Proposed fix
-import android.util.Base64 -import app.gamenative.db.dao.EncryptedAppTicketDao -import java.io.InputStream -import java.io.OutputStream -import java.util.concurrent.TimeUnit🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/service/SteamService.kt` around lines 11 - 166, The import block in SteamService.kt has duplicate imports (android.util.Base64, app.gamenative.db.dao.EncryptedAppTicketDao, java.io.InputStream, java.io.OutputStream, java.util.concurrent.TimeUnit) causing ambiguous import errors; edit the top-of-file imports to remove the repeated entries so each symbol is imported only once (keep one import line for Base64, EncryptedAppTicketDao, InputStream, OutputStream and TimeUnit), then rebuild to confirm compilation; also optionally tidy remaining imports (sort/remove unused) to prevent future duplicates.app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt (2)
348-351:⚠️ Potential issue | 🟠 MajorAuth code partially logged — sensitive credential exposure.
Line 351 logs the first 20 characters of the GOG OAuth auth code:
event.authCode.take(20).... Auth codes are short-lived but sensitive credentials that should not appear in logs, even partially. An attacker with log access could potentially use or replay the code before it expires.Remove or redact the auth code from the log statement.
Proposed fix
- Timber.i("[SettingsGOG]: ✓ Received GOG auth code event! Code: ${event.authCode.take(20)}...") + Timber.i("[SettingsGOG]: ✓ Received GOG auth code event!")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt` around lines 348 - 351, The Timber.i call inside DisposableEffect's onGOGAuthCodeReceived handler currently logs event.authCode (via AndroidEvent.GOGAuthCodeReceived), exposing sensitive credentials; update the onGOGAuthCodeReceived/Timbre log to remove or redact the auth code (e.g., log only that a GOG auth code was received or log non-sensitive metadata such as event source or a fixed "[REDACTED]" token) and ensure no other logs in DisposableEffect or onGOGAuthCodeReceived reference event.authCode.
99-106:⚠️ Potential issue | 🟡 MinorRemove unused
coroutineScopeparameter fromhandleGogAuthenticationandhandleEpicAuthentication.Both functions declare a
coroutineScopeparameter that is never used in their bodies. Since these are suspend functions, they inherit the coroutine context from their caller. Remove the unused parameter from both functions and update all call sites accordingly.Required changes
Remove
coroutineScopeparameter from function signatures at lines 99–106 and 151–158.Update call sites (lines 287, 328, 357) to remove the
coroutineScopeargument.Update KDoc comments (lines 93, 145) that reference the removed parameter.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt` around lines 99 - 106, The functions handleGogAuthentication and handleEpicAuthentication declare an unused coroutineScope parameter; remove coroutineScope from both suspend function signatures and update their KDoc to drop any mention of coroutineScope, then remove the coroutineScope argument at every call site where these functions are invoked so callers pass the remaining parameters only; ensure imports/overloads compile after signature changes and run a quick build to catch any remaining references.app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt (2)
1347-1399:⚠️ Potential issue | 🔴 CriticalBuild failure: Preview function is treated as a local function due to brace mismatch.
The pipeline reports
Modifier 'private' is not applicable to 'local function'at line 1353. This meansPreview_AppScreenis being parsed as a local function nested insideAppScreenContent, indicating thatAppScreenContent's closing brace is missing or misplaced. The extra}on line 1399 further confirms a structural brace issue.This needs a careful audit of the brace nesting throughout
AppScreenContent(starting at line 409) to ensure all blocks are properly closed before the Preview function.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt` around lines 1347 - 1399, The Preview_AppScreen function is being parsed as a local function because AppScreenContent's closing brace is missing/misplaced; audit the brace nesting inside the AppScreenContent composable (function name: AppScreenContent) and ensure every opened block (composables, scopes, if/when/for, lambdas) is properly closed before the Preview_AppScreen declaration, remove the extraneous closing brace currently after Preview_AppScreen, and re-run formatting/compile to confirm Preview_AppScreen can be declared as a top-level private composable.
780-784:⚠️ Potential issue | 🟡 MinorHardcoded user-facing strings "Calculating..." and "Unknown" should use string resources.
Lines 783/1020 use
"Calculating..."and lines 1165/1191 use"Unknown"as hardcoded English strings. These should be externalized viastringResource(R.string.*)for i18n support.Also applies to: 1019-1021
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt` around lines 780 - 784, The UI uses hardcoded strings "Calculating..." and "Unknown" in LibraryAppScreen.kt (the download/status text logic that sets statusText based on downloadProgress and the code path that falls back to "Unknown"); add string resources (e.g., R.string.calculating and R.string.unknown) to strings.xml and replace the literals with stringResource(R.string.calculating) and stringResource(R.string.unknown), making sure to import and call androidx.compose.ui.res.stringResource inside the composable scope where statusText is computed (e.g., in the downloadProgress branch and the fallback branch) so i18n works correctly.app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt (1)
228-246:⚠️ Potential issue | 🔴 CriticalBuild failure:
safePaddingModifierresolves toAnyinstead ofModifier.The
ifbranch (lines 233–237) computes aDpvalue (topPadding) but never wraps it in aModifier, while theelsebranch returnsModifier. The inferred common type isAny, which causes the pipeline error at line 246: "actual type is 'Any', but 'Modifier' was expected".Proposed fix
val safePaddingModifier = if (selectedLibraryItem != null) { val topPadding = if (PrefManager.hideStatusBarWhenNotInGame) { 0.dp } else { WindowInsets.statusBars.asPaddingValues().calculateTopPadding() } + Modifier.padding(top = topPadding) } else { Modifier }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt` around lines 228 - 246, safePaddingModifier is inferred as Any because the true branch computes a Dp (topPadding) but never returns a Modifier; change the true branch to return a Modifier (e.g., wrap topPadding with Modifier.padding(top = topPadding) or Modifier.offset(y = topPadding) as appropriate) so both branches return Modifier. Update the block that defines safePaddingModifier (referencing safePaddingModifier, PrefManager.hideStatusBarWhenNotInGame, and WindowInsets.statusBars.asPaddingValues().calculateTopPadding()) to return Modifier when selectedLibraryItem != null and keep the else returning Modifier; then use .then(safePaddingModifier) on the Box as-is.app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (1)
15-73:⚠️ Potential issue | 🔴 CriticalRemove duplicate imports causing build failures.
The import block contains duplicate imports:
rememberCoroutineScopeandsnapshotFlow(lines 16–17 and 61–62; already covered by the wildcardandroidx.compose.runtime.*at line 15)GPUInformation(lines 51 and 66)Paths(lines 54 and 72)pathString(lines 55 and 73)Remove all duplicate imports to resolve the build errors.
Proposed fix
import androidx.compose.runtime.* -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.platform.LocalContextRemove the duplicate imports at lines 61–62 (rememberCoroutineScope, snapshotFlow), line 66 (GPUInformation), line 72 (Paths), and line 73 (pathString).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt` around lines 15 - 73, The import block in SteamAppScreen.kt contains duplicated imports causing build failures; remove the redundant imports for rememberCoroutineScope and snapshotFlow (the extra androidx.compose.runtime imports), the duplicate GPUInformation import, and the repeated java.nio.file.Paths and kotlin.io.path.pathString imports so each symbol is imported only once (keep the existing wildcard androidx.compose.runtime.* and the single instances of GPUInformation, Paths and pathString). Locate the duplicates by searching for rememberCoroutineScope, snapshotFlow, GPUInformation, Paths, and pathString and delete the repeated import lines.
🟡 Minor comments (20)
app/src/main/java/app/gamenative/MainActivity.kt-270-274 (1)
270-274:⚠️ Potential issue | 🟡 MinorUnconditional
applyImmersiveMode()re-application conflicts withSetSystemUIVisibilityevent handler.The
onSetSystemUicallback (line 90-92) callsAppUtils.hideSystemUI(this, !it.visible), which respects theSetSystemUIVisibilityevent's intent to show bars whenvisible=true. However,applyImmersiveMode()unconditionally hides bars in bothonResume(line 273) andonWindowFocusChangedwhenhasFocus=true(line 458). This means any intentional bar-show state (e.g., when leaving the XServer screen) will be overridden whenever the window regains focus or the activity resumes.Consider gating the re-application on the current screen state or visibility intent, or track the desired visibility state so these lifecycle hooks respect SetSystemUIVisibility events.
Also applies to: 454-460
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/MainActivity.kt` around lines 270 - 274, onResume/onWindowFocusChanged are unconditionally calling applyImmersiveMode(), which overrides user-intended visibility set via onSetSystemUi; fix by introducing a shared flag (e.g., desiredSystemUiVisible:Boolean) updated inside onSetSystemUi (where AppUtils.hideSystemUI is called) and then gate calls to applyImmersiveMode() in onResume and onWindowFocusChanged to run only when desiredSystemUiVisible is false (or when the current screen requires immersive mode); alternatively modify applyImmersiveMode to accept a visibility hint and use that hint from the flag so lifecycle hooks respect SetSystemUIVisibility events and do not forcibly hide the bars.app/src/main/java/app/gamenative/ui/PluviaMain.kt-267-277 (1)
267-277:⚠️ Potential issue | 🟡 Minor
SteamService.getAppInfoOfwon't resolve names for non-Steam games.In the
NotFoundbranch,SteamService.getAppInfoOf(resolution.gameId)is used to retrieve the game name regardless of game source. For GOG, Epic, or Custom games this will likely returnnull, falling back to the generic"App ${event.appId}"string. Consider querying the appropriate service based on the game source for a better user-facing message.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/PluviaMain.kt` around lines 267 - 277, The NotFound branch uses SteamService.getAppInfoOf(resolution.gameId) which only covers Steam and yields a generic fallback for other sources; update the lookup to pick the correct service based on the game's source (e.g., check resolution.source or resolution.game.source) and call the appropriate resolver (SteamService.getAppInfoOf, GOGService.getAppInfoOf, EpicService.getAppInfoOf, or the custom game repository) to obtain a more accurate appName before falling back to "App ${event.appId}"; modify the code around GameResolutionResult.NotFound to perform this conditional lookup and use the resolved name in the Timber warning and MessageDialogState.app/src/main/java/app/gamenative/ui/component/QuickMenu.kt-237-245 (1)
237-245:⚠️ Potential issue | 🟡 MinorSilent focus swallow may leave gamepad users with no focused item.
When
requestFocus()throws (composition not ready), the exception is swallowed with no retry. For a component explicitly designed for controller navigation, a failed first-focus leaves the user with no focused item and no keyboard/gamepad entry point into the menu.Consider a small delay-and-retry or move the
LaunchedEffectinside the panel'sAnimatedVisibilityblock so it only fires once the content is composed:♻️ Proposed improvement
- LaunchedEffect(isVisible) { - if (isVisible) { - try { - firstItemFocusRequester.requestFocus() - } catch (_: Exception) { - // Focus request may fail if composition is not ready - } - } - }Move inside the
AnimatedVisibilitycontent for the panel (after theColumn):// Inside AnimatedVisibility content, after the items Column: LaunchedEffect(Unit) { // AnimatedVisibility guarantees the content is composed here firstItemFocusRequester.requestFocus() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/component/QuickMenu.kt` around lines 237 - 245, The current LaunchedEffect tied to isVisible swallows requestFocus() exceptions leaving gamepad users unfocused; update the behavior so focus is requested only after the panel content is composed by moving the LaunchedEffect from its current scope into the AnimatedVisibility content (i.e. run LaunchedEffect(Unit) inside the AnimatedVisibility block after the Column) and call firstItemFocusRequester.requestFocus() there; alternatively implement a small retry with delay inside that LaunchedEffect (catching exceptions and retrying a few times) so requestFocus() succeeds once composition is ready.app/src/main/java/app/gamenative/ui/component/QuickMenu.kt-288-294 (1)
288-294:⚠️ Potential issue | 🟡 Minor
Modifier.selectable()assigns incorrect TalkBack role ("radio button") to menu action items.
Modifier.selectable()is intended for elements that allow selection from a list of options; TalkBack will interpret "selected/unselected" as a radio-button-like pattern, which is semantically wrong for one-shot menu actions. Every item will be announced as "unselected, radio button" until focused, then "selected, radio button" — misleading for screen reader users.Replace with
Modifier.clickableand an explicitRole.Buttonsemantic:🛡️ Proposed fix
+import androidx.compose.foundation.clickable +import androidx.compose.ui.semantics.Role + .then( if (focusRequester != null) { Modifier.focusRequester(focusRequester) } else Modifier ) - .selectable( - selected = isFocused, - enabled = isEnabled, - interactionSource = interactionSource, - indication = null, - onClick = onClick - ) + .clickable( + enabled = isEnabled, + interactionSource = interactionSource, + indication = null, + role = Role.Button, + onClick = onClick + )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/component/QuickMenu.kt` around lines 288 - 294, The current use of Modifier.selectable(...) in QuickMenu.kt incorrectly exposes items as "radio button" to TalkBack; replace the selectable call with Modifier.clickable(...) and pass role = Role.Button while preserving enabled, onClick, interactionSource and indication (remove the selected parameter). Update the call to clickable(enabled = isEnabled, onClick = onClick, role = Role.Button, interactionSource = interactionSource, indication = null) and ensure Role is imported (androidx.compose.ui.semantics.Role).app/src/main/java/app/gamenative/ui/screen/library/components/SystemMenu.kt-449-451 (1)
449-451:⚠️ Potential issue | 🟡 MinorHardcoded
"User"fallback is not localisable.✨ Suggested change
- text = persona?.name ?: "User", + text = persona?.name ?: stringResource(R.string.default_user_name),Add the corresponding entry to
strings.xml:<string name="default_user_name">User</string>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/components/SystemMenu.kt` around lines 449 - 451, Replace the hard-coded fallback "User" in SystemMenu.kt (the Text composable that uses persona?.name ?: "User") with a localized string resource; add <string name="default_user_name">User</string> to strings.xml and change the fallback to use stringResource(R.string.default_user_name) (ensure import of androidx.compose.ui.res.stringResource) so persona?.name ?: stringResource(...) is used in the Text call.app/src/main/java/app/gamenative/ui/screen/library/components/SystemMenu.kt-479-486 (1)
479-486:⚠️ Potential issue | 🟡 Minor
Icons.Default.Personas the "closed" dropdown indicator is misleading.When the status picker is closed,
Icons.Default.Personsuggests profile navigation rather than a collapsible dropdown. ConsiderIcons.Default.KeyboardArrowDown(closed) /Icons.Default.KeyboardArrowUp(open) to communicate the affordance correctly.✨ Suggested change
- imageVector = if (showStatusPicker) Icons.Default.Close else Icons.Default.Person, + imageVector = if (showStatusPicker) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/components/SystemMenu.kt` around lines 479 - 486, The icon used in SystemMenu for the status picker is misleading: replace the current toggle logic that uses Icons.Default.Person (when closed) and Icons.Default.Close (when open) with the standard dropdown affordance icons — use Icons.Default.KeyboardArrowDown when showStatusPicker is false (closed) and Icons.Default.KeyboardArrowUp when showStatusPicker is true (open); keep the existing Icon parameters (contentDescription, Modifier.size(20.dp), tint) and only change the imageVector expression where the Icon is created in SystemMenu.app/src/main/java/app/gamenative/ui/screen/library/components/SystemMenu.kt-197-202 (1)
197-202:⚠️ Potential issue | 🟡 Minor
StatusOption.selectablepasses focus state instead of selection state — accessibility bug.
selected = isFocusedcausesselectableto report selection state as "whichever option currently has keyboard/gamepad focus," so TalkBack will never announce which persona status is actually active. The visual focus highlight already flows fromisFocusedthrough thebackgroundColorcomputation;selectedshould carry the real semantic value.🛡️ Proposed fix
.selectable( - selected = isFocused, + selected = isSelected, interactionSource = interactionSource, indication = null, onClick = onClick, )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/components/SystemMenu.kt` around lines 197 - 202, StatusOption currently passes the focus boolean into selectable (selected = isFocused), which misreports the semantic selected state to accessibility services; change the selectable call in StatusOption to pass the actual selection predicate (e.g., selected = isSelected or selected = (currentStatus == status)) instead of isFocused, leaving isFocused in the background/visual logic; keep interactionSource, indication, and onClick as-is so keyboard/gamepad focus visuals still use isFocused while TalkBack receives the true selection via the selected parameter on selectable.app/src/main/java/app/gamenative/ui/screen/library/components/SystemMenu.kt-590-596 (1)
590-596:⚠️ Potential issue | 🟡 MinorResolve TODO on "Go Offline" code path before shipping.
The
// TODO: test thiscomment at line 593 is on the "Go Offline" action that end-users can trigger — it should not be left unresolved in production code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/components/SystemMenu.kt` around lines 590 - 596, The "Go Offline" action in SystemMenu.kt still has a TODO; test and finalize this code path: verify SteamService.stop() completes without blocking the UI (wrap in coroutine/try-catch if it's suspend/throws), confirm onNavigateRoute(PluviaScreen.Home.route + "?offline=true") navigates and the offline query param is handled by the Home screen, and ensure onDismiss() is always called (use finally or runOnUiThread as appropriate); remove the TODO comment once validated and add minimal error handling/logging around SteamService.stop() if needed.app/src/main/java/app/gamenative/ui/component/dialog/SupportersDialog.kt-54-66 (1)
54-66:⚠️ Potential issue | 🟡 Minor
fetchKofiSupportersswallows exceptions, sohasErrorwon't trigger on network failures.Looking at
SupportersUtils.kt(lines 20-31),fetchKofiSupporterscatches all exceptions internally and returnsemptyList(). This means thecatchblock here (line 64) is effectively unreachable for fetch failures — a network error will silently show the "no supporters" UI instead of the error state.If distinguishing "fetch failed" from "genuinely no supporters" matters,
fetchKofiSupportersshould propagate exceptions (or return aResult).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/component/dialog/SupportersDialog.kt` around lines 54 - 66, SupportersDialog's catch block never sees network errors because SupportersUtils.fetchKofiSupporters currently swallows exceptions and returns an empty list; update fetchKofiSupporters (in SupportersUtils.kt) to either rethrow exceptions or return a Result/ sealed type that indicates success vs failure so callers can detect errors, then modify the caller in SupportersDialog.kt (the block that assigns supporters and sets hasError) to handle the propagated error case (set hasError = true on failure) instead of treating empty list as an error; reference the fetchKofiSupporters function and the hasError variable when making these changes.app/src/main/java/app/gamenative/ui/icons/InputIcons.kt-79-84 (1)
79-84:⚠️ Potential issue | 🟡 MinorMissing
@DrawableReson alias properties.
startandselectare aliases formenuandviewrespectively, but they lack their own@DrawableResannotation. Since the annotation has source retention and doesn't propagate through assignment, Android Lint won't recognize these as valid drawable resource IDs, potentially producing warnings at call sites.Proposed fix
`@DrawableRes` val menu = R.drawable.ic_input_xbox_button_menu + `@DrawableRes` val start = menu /** ⧉ View/Select button */ `@DrawableRes` val view = R.drawable.ic_input_xbox_button_view + `@DrawableRes` val select = view🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/icons/InputIcons.kt` around lines 79 - 84, Add the missing `@DrawableRes` annotations to the alias properties so Lint recognizes them as drawable resource IDs: annotate both start and select with `@DrawableRes` just like menu and view, e.g., update the properties start and select (aliases of menu and view) to include `@DrawableRes` before their declarations to ensure the annotation is present on those symbols.app/src/main/java/app/gamenative/ui/screen/library/components/LibraryAppItem.kt-88-115 (1)
88-115:⚠️ Potential issue | 🟡 MinorUnused scale animation for
PaneType.LIST.The
targetScalecomputes1.015ffor the focused LIST case, and a spring animation runs on thescalevalue (lines 95–102). However, thescaleis never passed toListViewCard(lines 105–115), which doesn't accept a scale parameter. OnlyGridViewCardreceives the animated scale value. This means the animation executes but has no effect when in list mode.Remove the LIST branch from
targetScale(defaulting to1ffor list) or pass scale toListViewCardif the subtle zoom was intended.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/components/LibraryAppItem.kt` around lines 88 - 115, The computed targetScale and animateFloatAsState produce an animated scale ("scale") but it is never applied to ListViewCard; update the code to either (A) remove the PaneType.LIST branch from targetScale so LIST uses 1f (no scale animation) by changing the when for targetScale to return 1f for PaneType.LIST, or (B) propagate the animated scale into the ListViewCard by adding a scale parameter to its call and updating ListViewCard's API/implementation to apply the scale (same as GridViewCard does); reference targetScale, animateFloatAsState, scale, ListViewCard, GridViewCard, and PaneType.LIST when making the change.app/src/main/java/app/gamenative/ui/model/UserLoginViewModel.kt-220-224 (1)
220-224:⚠️ Potential issue | 🟡 MinorUnused variable and duplicate log statement.
Line 223:
isSteamConnectedis declared but never read — this is dead code. Line 221 and 224 both log"Logged in? $isLoggedIn"with different Timber configurations; line 221 appears to be a leftover debug artifact.🧹 Proposed fix
val isLoggedIn = SteamService.isLoggedIn - Timber.d("Logged in? $isLoggedIn") - - val isSteamConnected = SteamService.isConnected Timber.tag("UserLoginViewModel").d("Logged in? $isLoggedIn")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/model/UserLoginViewModel.kt` around lines 220 - 224, Remove the dead local variable and duplicate log: delete the unused declaration isSteamConnected (from SteamService.isConnected) and remove the redundant Timber.d("Logged in? $isLoggedIn") call; keep a single, tagged log (Timber.tag("UserLoginViewModel").d(...)) that uses isLoggedIn from SteamService so only one meaningful log remains in UserLoginViewModel.app/src/main/java/app/gamenative/ui/screen/library/components/LibrarySearchBar.kt-199-204 (1)
199-204:⚠️ Potential issue | 🟡 MinorHardcoded content description string "Close search".
Line 201 uses a hardcoded English string instead of
stringResource(...). Other strings in this file are properly externalized (e.g., line 312 usesR.string.library_search_clear).🔤 Proposed fix
- contentDescription = "Close search", + contentDescription = stringResource(R.string.options_panel_close),Or add a dedicated string resource for "Close search" if one doesn't exist.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/components/LibrarySearchBar.kt` around lines 199 - 204, Replace the hardcoded contentDescription "Close search" in the Icon inside LibrarySearchBar (the Icon call using Icons.AutoMirrored.Filled.KeyboardArrowLeft) with a localized stringResource call (e.g., stringResource(R.string.library_search_close) or an existing suitable resource); if the string resource does not exist, add a new entry (library_search_close) to strings.xml and reference it via stringResource(...) so the UI text is externalized and localizable.app/src/main/java/app/gamenative/ui/model/UserLoginViewModel.kt-106-114 (1)
106-114:⚠️ Potential issue | 🟡 MinorRemove the asymmetry in
onSteamConnected: setisSteamConnected = truefor all connection events, not just auto-login.The handler only sets
isSteamConnected = truewhenisAutoLoggingInis true (line 111), but the disconnect handlers (onSteamDisconnectedat line 119 andonRemoteDisconnectedat line 125) always set it tofalse. This asymmetry means manual connections leaveisSteamConnected = falseinUserLoginStateuntil the next disconnect occurs.While
MainViewModelmaintains the authoritative connection state symmetrically (settingisSteamConnected = trueon all connection events andfalseon all disconnects),UserLoginViewModelmaintains its own inconsistent copy. This could lead to confusion or bugs during future refactoring.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/model/UserLoginViewModel.kt` around lines 106 - 114, The onSteamConnected handler currently only sets isSteamConnected = true when it.isAutoLoggingIn, causing asymmetry with the disconnect handlers; update the onSteamConnected lambda (the onSteamConnected: (SteamEvent.Connected) -> Unit) so that _loginState.update always sets isSteamConnected = true for every connection event, and still set isLoggingIn = true only when it.isAutoLoggingIn (i.e., move isSteamConnected = true outside or add a separate update path that unconditionally sets isSteamConnected = true on connection while preserving the conditional update to isLoggingIn).app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt-514-516 (1)
514-516:⚠️ Potential issue | 🟡 Minor
RECENTLY_PLAYEDsort is identical toINSTALLED_FIRST— placeholder or bug?Both branches use the same comparator (installed first → name ascending). If recently-played data isn't available yet, consider adding a
// TODOcomment explaining the placeholder, so it's clear this isn't the final behavior.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt` around lines 514 - 516, The RECENTLY_PLAYED branch currently duplicates the INSTALLED_FIRST comparator (SortOption.RECENTLY_PLAYED using compareBy<LibraryEntry> { if (entry.isInstalled) 0 else 1 }.thenBy { it.item.name.lowercase() }), so either implement the intended recently-played comparator (e.g., sort by lastPlayed timestamp on LibraryEntry, falling back to installed/name) or mark it as a deliberate placeholder by adding a clear TODO comment above the SortOption.RECENTLY_PLAYED branch explaining that recently-played ordering is not yet implemented and referencing the LibraryEntry.lastPlayed (or equivalent) field for future work.app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt-478-502 (1)
478-502:⚠️ Potential issue | 🟡 MinorInconsistent state reads — use
currentStateinstead of_state.value.Line 307 captures
val currentState = _state.value, but lines 480–502 re-read_state.valueforcurrentTaband source flags. If another coroutine updates_statebetween those reads, the filter logic may use a mix of old and new state. Use the already-capturedcurrentStatefor consistency.Proposed fix
- val currentTab = _state.value.currentTab - val includeSteam = if (currentTab == app.gamenative.ui.enums.LibraryTab.ALL) { - _state.value.showSteamInLibrary + val currentTab = currentState.currentTab + val includeSteam = if (currentTab == app.gamenative.ui.enums.LibraryTab.ALL) { + currentState.showSteamInLibrary } else { currentTab.showSteam } - val includeOpen = if (currentTab == app.gamenative.ui.enums.LibraryTab.ALL) { - _state.value.showCustomGamesInLibrary + val includeOpen = if (currentTab == app.gamenative.ui.enums.LibraryTab.ALL) { + currentState.showCustomGamesInLibrary } else { currentTab.showCustom } - val includeGOG = if (currentTab == app.gamenative.ui.enums.LibraryTab.ALL) { - _state.value.showGOGInLibrary + val includeGOG = if (currentTab == app.gamenative.ui.enums.LibraryTab.ALL) { + currentState.showGOGInLibrary } else { currentTab.showGoG } - val includeEpic = if (currentTab == app.gamenative.ui.enums.LibraryTab.ALL) { - _state.value.showEpicInLibrary + val includeEpic = if (currentTab == app.gamenative.ui.enums.LibraryTab.ALL) { + currentState.showEpicInLibrary } else { currentTab.showEpic }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt` around lines 478 - 502, The code re-reads shared state via _state.value when computing currentTab and includeSteam/includeOpen/includeGOG/includeEpic, risking inconsistent mixed-state views; replace those reads with the previously captured currentState (use currentState.currentTab and currentState.showSteamInLibrary / currentState.showCustomGamesInLibrary / currentState.showGOGInLibrary / currentState.showEpicInLibrary) and keep the branch logic that checks LibraryTab.ALL against currentTab so all filter decisions use the same snapshot.app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListPane.kt-150-159 (1)
150-159:⚠️ Potential issue | 🟡 Minor
rememberkeys are missingshowGOGInLibraryandshowEpicInLibrary.The lambda reads
state.showGOGInLibraryandstate.showEpicInLibrary(lines 153–154), but therememberkeys on line 150 only listshowSteamInLibraryandshowCustomGamesInLibrary. Toggling GOG or Epic sources won't recalculate the skeleton count.Proposed fix
- val totalSkeletonCount = remember(state.showSteamInLibrary, state.showCustomGamesInLibrary) { + val totalSkeletonCount = remember(state.showSteamInLibrary, state.showCustomGamesInLibrary, state.showGOGInLibrary, state.showEpicInLibrary) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListPane.kt` around lines 150 - 159, The remember call calculating totalSkeletonCount omits state.showGOGInLibrary and state.showEpicInLibrary from its keys, so changes to those toggles won't recompute the lambda; update the remember invocation for totalSkeletonCount to include state.showSteamInLibrary, state.showCustomGamesInLibrary, state.showGOGInLibrary and state.showEpicInLibrary as keys (so the lambda reading PrefManager.gogInstalledGamesCount and PrefManager.epicInstalledGamesCount is re-executed when those booleans change) while leaving the internal calculation and logging in the lambda unchanged.app/src/main/res/values/strings.xml-93-93 (1)
93-93:⚠️ Potential issue | 🟡 MinorBrand name capitalization: "GoG" → "GOG"
The official brand name is all-caps GOG. This is inconsistent with every other GOG reference in the file (e.g.,
gog_integration_title="GOG Integration (Alpha)"). Since this string appears as a visible tab label, users will notice the casing error.✏️ Proposed fix
- <string name="tab_gog">GoG</string> + <string name="tab_gog">GOG</string>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/res/values/strings.xml` at line 93, Tab label string "tab_gog" uses incorrect capitalization ("GoG") — update the string value for the resource named tab_gog to use the official all-caps brand "GOG" so it matches other references like gog_integration_title and appears consistently in the UI.app/src/main/java/app/gamenative/ui/components/BootingSplash.kt-95-95 (1)
95-95:⚠️ Potential issue | 🟡 MinorFirst tip is missing the
"Tip: "prefix present on all other entriesLine 95 reads
"Booting may take a few minutes on first launch"while every other tip in the list starts with"Tip: ". TheCrossfaderenders them verbatim, so users will see one visually inconsistent item during rotation.✏️ Proposed fix
- "Booting may take a few minutes on first launch", + "Tip: Booting may take a few minutes on first launch.",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/components/BootingSplash.kt` at line 95, In BootingSplash (the tips list used by the Crossfade renderer) one entry is missing the "Tip: " prefix; update the string currently set to "Booting may take a few minutes on first launch" to include the same "Tip: " prefix as the other entries so all tips render consistently in Crossfade; locate the tips collection in BootingSplash and prepend "Tip: " to that specific array/list item.app/src/main/java/app/gamenative/ui/components/BootingSplash.kt-163-175 (1)
163-175:⚠️ Potential issue | 🟡 Minor
Modifier.bluris unsupported on Android < 12 (app supports minSdk 26)The app targets minSdk 26 (Android 8.0), but
Modifier.blurrequires API 31+. On Android 8–11 devices, the blur modifier is silently ignored, causing the glowTextto render as a fully visible, semi-transparent cyan layer directly over the main text — creating a ghost/double-text artifact instead of a glow effect.If pre-Android 12 support is required, consider conditionally rendering the glow layer or replacing blur with an alternative approach (the main text already has a
Shadowmodifier at line 184).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/components/BootingSplash.kt` around lines 163 - 175, The glow Text using Modifier.blur in BootingSplash.kt (the "GameNative" composable with Modifier.blur and glowAlpha) relies on API 31+ and causes a ghost/double-text on Android <12; fix by making the glow layer conditional or providing a fallback: wrap the blurred Text in a runtime API check (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) and render it only on supported devices, and on older devices either omit that blurred Text or render a fallback glow using the existing Shadow on the main Text (or a manual radial gradient draw) to preserve the glow effect without using Modifier.blur.
There was a problem hiding this comment.
22 issues found across 52 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupDebug.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupDebug.kt:48">
P3: Preview guard skips PrefManager/WinlatorPrefManager init but preference values are still read immediately, so preview can still crash due to uninitialized dataStore.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/screen/settings/SettingsScreen.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/settings/SettingsScreen.kt:191">
P3: Hardcoded Color.White bypasses centralized theme colors. Team guidance requires UI colors to come from shared theme/resources for maintainability and dynamic theming.</violation>
</file>
<file name="app/src/main/java/app/gamenative/MainActivity.kt">
<violation number="1" location="app/src/main/java/app/gamenative/MainActivity.kt:134">
P2: applyImmersiveMode is now unconditionally re-hiding system bars on resume/focus, which can override SetSystemUIVisibility(visible=true) requests from UI and leave bars hidden on screens that expect them visible.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/screen/library/components/LibrarySearchBar.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/components/LibrarySearchBar.kt:201">
P3: Hardcoded UI string should be moved to string resources for localization/maintainability.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListPane.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListPane.kt:150">
P2: totalSkeletonCount is memoized without GOG/Epic dependencies, so toggling those sources won’t recompute the skeleton count.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/component/QuickMenu.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/component/QuickMenu.kt:142">
P2: Hardcoded overlay color bypasses theme resources; use a theme color (e.g., scrim) to keep overlays consistent across themes.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/enums/SortOption.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/enums/SortOption.kt:21">
P2: SortOption persistence uses enum ordinals, which will remap existing saved preferences if enum entries are reordered or new values are inserted. This risks silently changing user sort settings; use a stable id or name for persistence/migration instead.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryOptionsPanel.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryOptionsPanel.kt:101">
P3: Hardcoded scrim color bypasses theme resources; use a themed color (e.g., MaterialTheme color scheme) for maintainability and correct light/dark behavior.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/screen/library/components/GameOptionsPanel.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/components/GameOptionsPanel.kt:107">
P3: Hardcoded overlay color bypasses theme resources; use the themed scrim color for consistency and maintainability.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/component/Scrollbar.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/component/Scrollbar.kt:325">
P2: Hardcoded Color.White bypasses the app theme/color resources. Team guidance requires UI colors to be centralized for maintainability and theming; use a themed color or a parameter instead of a literal color.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/component/OptionListItem.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/component/OptionListItem.kt:62">
P3: OptionListItem and OptionRadioItem duplicate the same focus/animation and modifier setup (scale/background/border/content colors, selectable chain). Consider extracting the shared styling/animation logic into a common helper/composable to avoid divergence when UI tweaks are made.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/screen/library/components/SystemMenu.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/components/SystemMenu.kt:310">
P3: Hardcoded Color.Black scrim conflicts with the team’s no-hardcoded-colors guideline; use a theme-based scrim color instead so it adapts to theming.</violation>
<violation number="2" location="app/src/main/java/app/gamenative/ui/screen/library/components/SystemMenu.kt:450">
P2: User-facing strings are hardcoded in the composables (e.g., fallback name "User" and the selected checkmark), which breaks the project’s localization/resource pattern. Move these to string resources (or replace the checkmark with a proper icon).</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryTabBar.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryTabBar.kt:315">
P3: Hardcoded ShoulderButton labels/content descriptions ("L1"/"R1") should be string resources for localization and maintainability.</violation>
<violation number="2" location="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryTabBar.kt:447">
P2: Focusable UI elements disable focus with `focusProperties { canFocus = false }` while relying on `collectIsFocusedAsState()` for animations, preventing keyboard/DPAD/gamepad focus and making focus animations ineffective.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt:350">
P2: Installed-only tab logic is applied to Steam only; GOG/Epic filters still ignore `currentTab.installedOnly`, so Installed tab can show uninstalled GOG/Epic titles and incorrect counts.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListCard.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListCard.kt:256">
P2: Synchronous filesystem scan for each list item during composition can block UI on large libraries. Move icon lookup to background/cached load instead of calling findIconFileForCustomGame on the UI thread.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryGridCard.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryGridCard.kt:306">
P2: Synchronous directory scans (`File.listFiles()`) run during composition via `remember { getGridImageUrl(...) }`, which can block the UI for large custom game folders. Consider moving this file IO off the main thread or caching it outside composable rendering.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/screen/login/UserLoginScreen.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/login/UserLoginScreen.kt:277">
P2: selectedTabIndex is remembered once and never updates when userLoginState.loginScreen changes in the ViewModel, so the TabRow indicator can desync from the actual screen shown by Crossfade.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:1007">
P2: Download progress UI ignores the passed downloadInfo and always queries SteamService, which will be wrong for non‑Steam sources and can null out progress/status.</violation>
<violation number="2" location="app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:1282">
P2: optionsMenuVisible controls both DropdownMenu and GameOptionsPanel, so when true both menus render at once, causing duplicate/overlapping options UI. Consider removing or gating one of them.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/components/BootingSplash.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/components/BootingSplash.kt:46">
P2: Guard the infinite transition/animations behind the `visible` flag (or create them inside the AnimatedVisibility block) so hidden splash screens don’t keep scheduling vsync work.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
1 issue found across 6 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt:1565">
P2: Migration progress dialog was removed while migration still runs and updates state, leaving long-running migrations with no UI feedback (likely regression).</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
app/src/main/java/app/gamenative/ui/PluviaMain.kt (1)
343-366:⚠️ Potential issue | 🔴 Critical
preLaunchAppis called twice for every successful pending-launch after login — remove the duplicate block.When
resolveGameAppIdreturnsSuccess, theSuccessbranch (lines 343–353) already callsviewModel.setLaunchedAppId,viewModel.setBootToContainer, andpreLaunchApp. Because theSuccessbranch does notreturn@let, execution falls through to lines 356–366, which repeat the exact same three calls unconditionally. The result is thatpreLaunchAppruns twice: the loading dialog fires twice, cloud-sync/download runs twice, andviewModel.launchApp(→navController.navigate(XServer)) is invoked twice.The
NotFoundbranch correctly guards against this viareturn@let, butSuccessis missing the symmetric early exit.🐛 Proposed fix — remove the unreachable duplicate block
viewModel.setLaunchedAppId(launchRequest.appId) viewModel.setBootToContainer(false) preLaunchApp( context = context, appId = launchRequest.appId, setLoadingDialogVisible = viewModel::setLoadingDialogVisible, setLoadingProgress = viewModel::setLoadingDialogProgress, setLoadingMessage = viewModel::setLoadingDialogMessage, setMessageDialogState = setMessageDialogState, onSuccess = viewModel::launchApp, ) } } - viewModel.setLaunchedAppId(launchRequest.appId) - viewModel.setBootToContainer(false) - preLaunchApp( - context = context, - appId = launchRequest.appId, - setLoadingDialogVisible = viewModel::setLoadingDialogVisible, - setLoadingProgress = viewModel::setLoadingDialogProgress, - setLoadingMessage = viewModel::setLoadingDialogMessage, - setMessageDialogState = setMessageDialogState, - onSuccess = viewModel::launchApp, - ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/PluviaMain.kt` around lines 343 - 366, The Success branch of resolveGameAppId currently executes viewModel.setLaunchedAppId, viewModel.setBootToContainer, and preLaunchApp but then falls through to a duplicate block that repeats the same calls; remove the duplicate block (lines that again call viewModel.setLaunchedAppId, viewModel.setBootToContainer, and preLaunchApp) or alternatively add an early exit (return@let) at the end of the Success branch so preLaunchApp only runs once; locate the duplicate by searching for repeated calls to preLaunchApp and viewModel.setLaunchedAppId in the Success handling of resolveGameAppId.app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (1)
431-438:⚠️ Potential issue | 🟠 MajorAssign
sizeFromStoreon the Main dispatcher.
sizeFromStoreis mutated insidewithContext(Dispatchers.IO)(line 436), which violates Compose snapshot isolation. State mutations must occur on the Main dispatcher. Compute the size on IO, then assign on Main.🛠️ Suggested fix
LaunchedEffect(isInstalled, gameId) { if (!isInstalled) { // Load size from store asynchronously to avoid blocking UI - withContext(Dispatchers.IO) { - val size = DownloadService.getSizeFromStoreDisplay(gameId) - sizeFromStore = size - } + val size = withContext(Dispatchers.IO) { + DownloadService.getSizeFromStoreDisplay(gameId) + } + sizeFromStore = size } else { sizeFromStore = null } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt` around lines 431 - 438, The LaunchedEffect block mutates Compose state sizeFromStore from inside withContext(Dispatchers.IO), violating snapshot isolation; instead, call DownloadService.getSizeFromStoreDisplay(gameId) on the IO dispatcher (e.g. val size = withContext(Dispatchers.IO) { ... }) and then perform the assignment sizeFromStore = size on the Main dispatcher (i.e. after the withContext block within LaunchedEffect) so the state update happens on the main thread; adjust the LaunchedEffect handling of isInstalled/gameId accordingly.
🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/ui/PluviaMain.kt (2)
923-923:scopeshadows the outerrememberCoroutineScope()declared at line 202.Rename one of them (e.g.
containerConfigScope) to eliminate the shadowing and prevent accidental future misuse.🧹 Suggested rename
- val scope = rememberCoroutineScope() + val containerConfigScope = rememberCoroutineScope()And update the single usage at line 944:
- scope.launch { + containerConfigScope.launch {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/PluviaMain.kt` at line 923, The local val scope = rememberCoroutineScope() at the bottom of PluviaMain.kt shadows the outer rememberCoroutineScope() declared earlier; rename this inner variable (e.g. to containerConfigScope) and update its single usage (the coroutine launcher reference currently using scope at the later call site) to containerConfigScope to remove shadowing and avoid accidental misuse.
183-184:isInstalledis alwaystrueat this point —&& isInstalledis redundant.The early return at line 176 guarantees
isInstalled == truebefore reaching line 183, making the&& isInstalledguard a no-op.🧹 Suggested simplification
- val isSteamInstalled = gameSource == GameSource.STEAM && isInstalled - val isCustomGame = gameSource == GameSource.CUSTOM_GAME + val isSteamInstalled = gameSource == GameSource.STEAM + val isCustomGame = gameSource == GameSource.CUSTOM_GAME🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/PluviaMain.kt` around lines 183 - 184, The expression declaring isSteamInstalled contains a redundant check: remove the unnecessary "&& isInstalled" since isInstalled is guaranteed true at this point; change the assignment of isSteamInstalled to only check gameSource == GameSource.STEAM (leave isCustomGame as-is). Update the identifier in PluviaMain.kt where isSteamInstalled is defined so it no longer includes the isInstalled conjunct.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src/main/java/app/gamenative/ui/PluviaMain.kt`:
- Around line 266-268: The NotFound branch uses
SteamService.getAppInfoOf(resolution.gameId) which returns null for non‑Steam
games and causes user‑facing fallbacks like "App GOG_12345"; update the lookup
in GameResolutionResult.NotFound (both the current and pending-launch NotFound
branches) to resolve the name via the correct service based on the game's source
(use resolution.originalAppId or resolution.gameId to determine source) and only
if no service can provide a friendly name fall back to a neutral, user‑friendly
string such as "Unknown Game" or include the platform (e.g., "GOG game" or "Epic
game") instead of leaking internal container IDs; ensure you touch
SteamService.getAppInfoOf usage and the places referencing event.appId to apply
the platform-aware lookup.
In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt`:
- Around line 455-472: The KEYCODE_BUTTON_START handler in the handleKeyEvent
lambda currently ignores the UI enablement rules and directly calls
onPauseResumeClick or onDownloadInstallClick; compute a single canPrimaryAction
boolean using the same logic as the primary button (Wi‑Fi-only + wifiConnected,
download validity, etc.) and short‑circuit the START branch when
!canPrimaryAction so gamepad users cannot bypass restrictions; update the START
handling in handleKeyEvent (and the analogous gamepad handler later in the file)
to check canPrimaryAction before invoking onPauseResumeClick or
onDownloadInstallClick.
- Around line 789-1090: The nested download UI erroneously calls
SteamService.getAppDownloadInfo(displayInfo.gameId) instead of using the
downloadInfo parameter passed into AppScreenContent(), which forces Steam-only
behavior; update the block that defines statusMessageFlow, statusMessageState,
statusMessage, timeLeftText, sizeText, and any calls to
getBytesProgress()/getEstimatedTimeRemaining()/getStatusMessageFlow() to use the
existing downloadInfo parameter (and its methods) rather than
SteamService.getAppDownloadInfo(...), ensuring all referenced symbols
(downloadInfo, getStatusMessageFlow(), getEstimatedTimeRemaining(),
getBytesProgress(), displayInfo) consistently use the passed-in data source.
---
Outside diff comments:
In `@app/src/main/java/app/gamenative/ui/PluviaMain.kt`:
- Around line 343-366: The Success branch of resolveGameAppId currently executes
viewModel.setLaunchedAppId, viewModel.setBootToContainer, and preLaunchApp but
then falls through to a duplicate block that repeats the same calls; remove the
duplicate block (lines that again call viewModel.setLaunchedAppId,
viewModel.setBootToContainer, and preLaunchApp) or alternatively add an early
exit (return@let) at the end of the Success branch so preLaunchApp only runs
once; locate the duplicate by searching for repeated calls to preLaunchApp and
viewModel.setLaunchedAppId in the Success handling of resolveGameAppId.
In
`@app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt`:
- Around line 431-438: The LaunchedEffect block mutates Compose state
sizeFromStore from inside withContext(Dispatchers.IO), violating snapshot
isolation; instead, call DownloadService.getSizeFromStoreDisplay(gameId) on the
IO dispatcher (e.g. val size = withContext(Dispatchers.IO) { ... }) and then
perform the assignment sizeFromStore = size on the Main dispatcher (i.e. after
the withContext block within LaunchedEffect) so the state update happens on the
main thread; adjust the LaunchedEffect handling of isInstalled/gameId
accordingly.
---
Nitpick comments:
In `@app/src/main/java/app/gamenative/ui/PluviaMain.kt`:
- Line 923: The local val scope = rememberCoroutineScope() at the bottom of
PluviaMain.kt shadows the outer rememberCoroutineScope() declared earlier;
rename this inner variable (e.g. to containerConfigScope) and update its single
usage (the coroutine launcher reference currently using scope at the later call
site) to containerConfigScope to remove shadowing and avoid accidental misuse.
- Around line 183-184: The expression declaring isSteamInstalled contains a
redundant check: remove the unnecessary "&& isInstalled" since isInstalled is
guaranteed true at this point; change the assignment of isSteamInstalled to only
check gameSource == GameSource.STEAM (leave isCustomGame as-is). Update the
identifier in PluviaMain.kt where isSteamInstalled is defined so it no longer
includes the isInstalled conjunct.
- LibraryViewModel: apply currentTab.installedOnly filter to GOG and Epic games (was only applied to Steam, breaking INSTALLED tab) - LibraryAppScreen: use passed downloadInfo parameter instead of hardcoded SteamService call (fixes download progress for non-Steam) - LibraryAppScreen: remove old DropdownMenu that was rendering simultaneously with new GameOptionsPanel (duplicate menu UI)
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt (4)
358-363: Redundant sort: Steam apps are sorted here but re-sorted at line 534.The combined list is sorted with
sortComparator(line 534) after all entries are merged. This per-source sort is dead work since the combined sort overwrites it.Proposed fix — just remove the sort
- .sortedWith( - compareByDescending<SteamApp> { - downloadDirectorySet.contains(SteamService.getAppDirName(it)) - }.thenBy { it.name.lowercase() }, - ) .toList()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt` around lines 358 - 363, This per-source sorting using sortedWith(compareByDescending { downloadDirectorySet.contains(SteamService.getAppDirName(it)) }.thenBy { it.name.lowercase() }) in LibraryViewModel is redundant because the merged list is re-sorted later with sortComparator; remove the sort call (the sortedWith(...).toList()) for the Steam apps source so you yield the unsorted sequence (or plain toList()) and rely on the downstream sortComparator-based sort to order the combined list.
204-207: Missing import forLibraryTab— fully qualified references throughout.
LibraryTabis referenced fully qualified here (line 204) and again at lines 485, 490, 496, 502. Adding an import would clean up all five call sites:import app.gamenative.ui.enums.SortOption +import app.gamenative.ui.enums.LibraryTab🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt` around lines 204 - 207, Add an import for app.gamenative.ui.enums.LibraryTab and replace the fully-qualified references with the simple type name to clean up call sites; update the top of LibraryViewModel (where class LibraryViewModel is defined) to import LibraryTab and change usages in onTabChanged and the other occurrences (lines referencing LibraryTab at the methods around 485, 490, 496, 502) to use LibraryTab instead of app.gamenative.ui.enums.LibraryTab.
73-74: Pagination vars are mutated from concurrent IO coroutines without synchronization.
paginationCurrentPageandlastPageInCurrentFilterare plainvarfields written insideonFilterApps(Dispatchers.IO). Multiple callers — the threeinitflow collectors and event handlers — can invokeonFilterAppsconcurrently, creating a data race.A lightweight fix is to move these into the
MutableStateFlow<LibraryState>(which is already thread-safe), or protect them with aMutex/single-threaded dispatcher.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt` around lines 73 - 74, The two mutable fields paginationCurrentPage and lastPageInCurrentFilter are being written from onFilterApps (Dispatchers.IO) and can cause data races; move these counters into the existing MutableStateFlow<LibraryState> (add properties to LibraryState), update all reads/writes to use state.update/get on the MutableStateFlow, and remove the plain var declarations, or alternatively wrap all accesses in a single Mutex used by onFilterApps and other callers; ensure you update every reference (e.g., in onFilterApps, init flow collectors, and event handlers) to read/write the counters from the state flow (or via mutex-protected sections) so updates are thread-safe.
455-457: Nit: InconsistentappIdprefix for Epic entries.Steam (line 376) and GOG (line 422) use
"${GameSource.X.name}_${game.id}", but Epic hardcodes"EPIC_${game.id}". For consistency and resilience to enum renames:- appId = "EPIC_${game.id}", + appId = "${GameSource.EPIC.name}_${game.id}",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt` around lines 455 - 457, Library entries for Epic are using a hardcoded prefix ("EPIC_${game.id}") which is inconsistent with Steam/GOG; update the LibraryItem.appId assignment in LibraryViewModel (the block creating LibraryItem) to use the enum name (e.g., GameSource.EPIC.name) instead of a hardcoded string so it matches the other cases like Steam and GOG (produce "${GameSource.EPIC.name}_${game.id}").
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt`:
- Around line 482-506: The code re-reads _state.value for currentTab and source
flags causing a TOCTOU race; instead use the previously captured snapshot
currentState consistently for computing
includeSteam/includeOpen/includeGOG/includeEpic (and currentTab) so that
LibraryViewModel's filtering logic (variables currentState, currentTab,
includeSteam/includeOpen/includeGOG/includeEpic) uses the same immutable
snapshot and cannot mix values from concurrent updates.
- Around line 509-527: The RECENTLY_PLAYED comparator in LibraryViewModel (when
currentState.currentSortOption == SortOption.RECENTLY_PLAYED) is a stub that
only mirrors INSTALLED_FIRST and should instead use recency/timestamp fields
from LibraryEntry (or be disabled); also SIZE_SMALLEST and SIZE_LARGEST rely on
LibraryEntry.item.sizeBytes which is only set for Steam entries causing GOG/Epic
to cluster at 0. Update SortOption.RECENTLY_PLAYED to sort by a
lastPlayed/lastLaunched timestamp on LibraryEntry (falling back to
installed/name as needed) or remove/disable the option until timestamps exist,
and either guard size-based sorts to only apply when sizeBytes > 0 or add a
clear TODO comment in the SIZE_SMALLEST/SIZE_LARGEST branches noting the
Steam-only behavior (and optionally hide/disable those options in the UI until
sizes are populated).
---
Nitpick comments:
In `@app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt`:
- Around line 358-363: This per-source sorting using
sortedWith(compareByDescending {
downloadDirectorySet.contains(SteamService.getAppDirName(it)) }.thenBy {
it.name.lowercase() }) in LibraryViewModel is redundant because the merged list
is re-sorted later with sortComparator; remove the sort call (the
sortedWith(...).toList()) for the Steam apps source so you yield the unsorted
sequence (or plain toList()) and rely on the downstream sortComparator-based
sort to order the combined list.
- Around line 204-207: Add an import for app.gamenative.ui.enums.LibraryTab and
replace the fully-qualified references with the simple type name to clean up
call sites; update the top of LibraryViewModel (where class LibraryViewModel is
defined) to import LibraryTab and change usages in onTabChanged and the other
occurrences (lines referencing LibraryTab at the methods around 485, 490, 496,
502) to use LibraryTab instead of app.gamenative.ui.enums.LibraryTab.
- Around line 73-74: The two mutable fields paginationCurrentPage and
lastPageInCurrentFilter are being written from onFilterApps (Dispatchers.IO) and
can cause data races; move these counters into the existing
MutableStateFlow<LibraryState> (add properties to LibraryState), update all
reads/writes to use state.update/get on the MutableStateFlow, and remove the
plain var declarations, or alternatively wrap all accesses in a single Mutex
used by onFilterApps and other callers; ensure you update every reference (e.g.,
in onFilterApps, init flow collectors, and event handlers) to read/write the
counters from the state flow (or via mutex-protected sections) so updates are
thread-safe.
- Around line 455-457: Library entries for Epic are using a hardcoded prefix
("EPIC_${game.id}") which is inconsistent with Steam/GOG; update the
LibraryItem.appId assignment in LibraryViewModel (the block creating
LibraryItem) to use the enum name (e.g., GameSource.EPIC.name) instead of a
hardcoded string so it matches the other cases like Steam and GOG (produce
"${GameSource.EPIC.name}_${game.id}").
…button - Guard FocusRequester.requestFocus() in AppScreenContent to prevent IllegalStateException when PrimaryActionButton is not in composition (non-downloading layout uses regular Buttons without the requester) - Make library root Box focusable and recapture focus on tab change so L1/R1 bumper events keep working when grid has no focusable items - Add Search and Add Game buttons to ExpandedLibraryTabBar (were only present in CompactLibraryTabBar, invisible on wide-screen devices)
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt (1)
516-756:⚠️ Potential issue | 🟠 MajorOld non-parallax layout is still rendered unconditionally alongside the new parallax layout.
The outer
Column(fillMaxSize)at line 516 contains both:
- The original, unchanged hero section (
Box(weight=1f), lines 518–649) and action-button section (Column, lines 652–751) — no new lines in this block.- The new parallax
Column(fillMaxSize, verticalScroll)starting at line 777 that also renders a full hero, an integrated action bar, and info cards.Because neither is wrapped in a conditional, both render every time
AppScreenContentis called, stacking the entire old UI above the new one. Additionally, lines 756–776 declarestatusMessageFlow,statusMessageState,statusMessage, andtimeLeftTextinside anif (isDownloading)block but these variables are scoped to that block and never used — they are merge residue now that the new parallax layout re-declares the same variables at lines 997–1013.Recommended fix: Remove (or gate behind an
else) the legacy hero and action-button blocks (lines 518–776) so only the new parallax layout renders. The dead variable block at 756–776 should be deleted entirely.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt` around lines 516 - 756, The file still renders the old static hero/action UI (the top-level Column with the legacy Box(modifier = Modifier.weight(1f)) hero and the following action-buttons Column) alongside the new parallax Column(verticalScroll) so both UIs stack; also the scoped variables statusMessageFlow, statusMessageState and timeLeftText declared inside the old if (isDownloading) block are dead now. Remove the legacy hero Box(...) block and the legacy action-buttons Column(...) (or wrap them in an else branch so only one layout renders) and delete the dead scoped variables (statusMessageFlow, statusMessageState, statusMessage/timeLeftText) that were declared inside the old isDownloading block so only the new parallax Column and its variables remain.
🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/ui/screen/library/components/LibraryTabBar.kt (1)
217-244:CompactIconButtonmisusesselectablefor a stateless action button.
selectable(selected = false, indication = null, ...)with a constantfalseis semantically wrong —selectableimplies radio-group-style mutual exclusivity and is announced accordingly by accessibility services. A plain icon button should use.clickable(indication = null, interactionSource = ..., onClick = onClick)instead.♻️ Proposed fix
Box( modifier = modifier .size(36.dp) .clip(CircleShape) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) - .selectable( - selected = false, - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = onClick, - ), + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClick, + ),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/components/LibraryTabBar.kt` around lines 217 - 244, CompactIconButton incorrectly uses Modifier.selectable with a constant selected = false, which is semantically for selection controls; replace the selectable usage with Modifier.clickable(indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = onClick) inside the Box modifier in the CompactIconButton composable so the component behaves as a stateless action button and is announced correctly by accessibility services (keep the existing size/clip/background and Icon settings).app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt (1)
496-504:isResume,pauseResumeEnabled,isInstall,installEnabled,buttonEnabledare computed twice.These five variables are computed at lines 496–504 (outer scope, used by the new parallax action bar at lines 917–944) and again at lines 665–704 (inside the old content Column, with identical logic). The inner declarations shadow the outer ones. Once the legacy layout is removed (see structural duplication note above), the outer declarations are the authoritative source and the inner ones become dead.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt` around lines 496 - 504, The five computed booleans (isResume, pauseResumeEnabled, isInstall, installEnabled, buttonEnabled) are duplicated and shadowed: remove the inner set of declarations (the ones inside the old content Column) so the outer scope values are authoritative for both the parallax action bar and the content layout; update any references in the inner column to use the outer variables (or extract a small helper function to compute them once if you prefer) ensuring the names in LibraryAppScreen.kt (isResume, pauseResumeEnabled, isInstall, installEnabled, buttonEnabled) are not re-declared locally.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt`:
- Around line 519-528: The dialog buttons in LibraryScreen's "Add Custom Game"
alert (the Text("OK") and Text("Cancel") inside the AlertDialog tied to
showAddCustomGameDialog) use hardcoded strings; replace them with localized
resources by using stringResource(R.string.ok) and
stringResource(R.string.cancel) (or your own R.string.add_custom_game_ok /
R.string.add_custom_game_cancel) and add those keys to strings.xml, and ensure
the compose import for androidx.compose.ui.res.stringResource is present so the
Text composables read from resources instead of hardcoded literals.
---
Outside diff comments:
In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt`:
- Around line 516-756: The file still renders the old static hero/action UI (the
top-level Column with the legacy Box(modifier = Modifier.weight(1f)) hero and
the following action-buttons Column) alongside the new parallax
Column(verticalScroll) so both UIs stack; also the scoped variables
statusMessageFlow, statusMessageState and timeLeftText declared inside the old
if (isDownloading) block are dead now. Remove the legacy hero Box(...) block and
the legacy action-buttons Column(...) (or wrap them in an else branch so only
one layout renders) and delete the dead scoped variables (statusMessageFlow,
statusMessageState, statusMessage/timeLeftText) that were declared inside the
old isDownloading block so only the new parallax Column and its variables
remain.
---
Duplicate comments:
In
`@app/src/main/java/app/gamenative/ui/screen/library/components/LibraryTabBar.kt`:
- Around line 409-489: The focus-driven animation branches in ShoulderButton,
IconActionButton, and TabItem are dead because each composes calls
.focusProperties { canFocus = false } which prevents collectIsFocusedAsState()
from ever becoming true; remove the .focusProperties { canFocus = false } call
from the modifier chain in each of those composables (i.e., remove the
focusProperties invocation in the Modifier.then(...).focusProperties(...) chain
for ShoulderButton, IconActionButton, and TabItem) so that
interactionSource.collectIsFocusedAsState() can reflect real focus and enable
the existing animateFloatAsState scale/alpha, gradient, border, and tint logic
to run.
In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt`:
- Around line 1271-1279: GameOptionsPanel is currently placed after the outer
Box is closed which makes Modifier.align(Alignment.CenterEnd) fail because it
needs a BoxScope; move the GameOptionsPanel invocation (the composable that uses
isOpen = optionsMenuVisible, onDismiss = { optionsMenuVisible = false }, options
= optionsMenu.toList(), modifier = Modifier.align(Alignment.CenterEnd)) so it is
inside the outer Box block as a sibling to the Column (i.e., before the Box
closing brace) so Modifier.align has the proper BoxScope receiver.
- Around line 469-476: The KEYCODE_BUTTON_START handler currently calls
onPauseResumeClick() / onDownloadInstallClick() unconditionally; update the
KEYCODE_BUTTON_START branch to first evaluate the same enablement booleans used
elsewhere (pauseResumeEnabled, buttonEnabled, wifiAllowed and isValidToDownload)
and only call onPauseResumeClick() if pauseResumeEnabled && buttonEnabled (and
other existing guards), or call onDownloadInstallClick() only when
!isDownloading && !hasPartialDownload && buttonEnabled && isValidToDownload &&
(wifiAllowed as required); otherwise return false/do nothing. Ensure you
reference the same computed flags used in the surrounding code to keep behavior
consistent with the touch/button handlers.
---
Nitpick comments:
In
`@app/src/main/java/app/gamenative/ui/screen/library/components/LibraryTabBar.kt`:
- Around line 217-244: CompactIconButton incorrectly uses Modifier.selectable
with a constant selected = false, which is semantically for selection controls;
replace the selectable usage with Modifier.clickable(indication = null,
interactionSource = remember { MutableInteractionSource() }, onClick = onClick)
inside the Box modifier in the CompactIconButton composable so the component
behaves as a stateless action button and is announced correctly by accessibility
services (keep the existing size/clip/background and Icon settings).
In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt`:
- Around line 496-504: The five computed booleans (isResume, pauseResumeEnabled,
isInstall, installEnabled, buttonEnabled) are duplicated and shadowed: remove
the inner set of declarations (the ones inside the old content Column) so the
outer scope values are authoritative for both the parallax action bar and the
content layout; update any references in the inner column to use the outer
variables (or extract a small helper function to compute them once if you
prefer) ensuring the names in LibraryAppScreen.kt (isResume, pauseResumeEnabled,
isInstall, installEnabled, buttonEnabled) are not re-declared locally.
…yout for all states Remove the dual-layout pattern where a legacy static hero + Button/OutlinedButton layout rendered for all states while the new parallax layout only rendered during downloads. The parallax Column with integrated PrimaryActionButton action bar now renders unconditionally for all game states (idle, downloading, paused, installed). - Remove legacy hero Box(weight=1f) with static image - Remove legacy Button/OutlinedButton action buttons - Remove dead statusMessageFlow/timeLeftText variables - Remove if(isDownloading) conditional wrapping the parallax layout - Revert try-catch on FocusRequester (always in composition now) - Clean up ~10 unused imports
There was a problem hiding this comment.
🧹 Nitpick comments (3)
app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt (3)
416-423: Network connectivity is a stale snapshot — won't react to changes while the screen is open.
getNetworkCapabilitiesis called once at composition time. If the device connects/disconnects Wi-Fi after landing on this screen,hasInternet,wifiConnected, and the derivedwifiAllowed/buttonEnabledvalues stay stale until the next full recomposition. The primary-action button could appear enabled (or disabled) incorrectly for the lifetime of the visit.Consider deriving these values from a
collectAsState-backedFlow(e.g. viaConnectivityManager.registerNetworkCallbackexposed as acallbackFlow) so the UI reacts in real time.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt` around lines 416 - 423, The current NetworkCapabilities snapshot in LibraryAppScreen (where context/ connectivityManager/getNetworkCapabilities are used to compute hasInternet and wifiConnected) is static at composition time; replace this with a reactive Flow backed by ConnectivityManager.registerNetworkCallback (implement a callbackFlow that emits updated NetworkCapabilities or booleans) and consume it in the composable with collectAsState to derive hasInternet/wifiConnected (and consequently wifiAllowed/buttonEnabled) so the UI updates automatically when network status changes.
614-622: Extractrememberout of the string template for readability.Embedding a
remembercall inside a string interpolation is valid but hurts readability. A localvalabove theTextcall is idiomatic.♻️ Suggested refactor
+ val yearText = remember(displayInfo.releaseDate) { + if (displayInfo.releaseDate > 0) + SimpleDateFormat("yyyy", Locale.getDefault()).format(Date(displayInfo.releaseDate * 1000)) + else "" + } Text( - text = "${displayInfo.developer} • ${ - remember(displayInfo.releaseDate) { - if (displayInfo.releaseDate > 0) { - SimpleDateFormat("yyyy", Locale.getDefault()).format(Date(displayInfo.releaseDate * 1000)) - } else { - "" - } - } - }", + text = "${displayInfo.developer} • $yearText",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt` around lines 614 - 622, Move the remember block out of the string template to a local val for readability: create a val (e.g., releaseYear) above the Text call that uses remember(displayInfo.releaseDate) { if (displayInfo.releaseDate > 0) SimpleDateFormat("yyyy", Locale.getDefault()).format(Date(displayInfo.releaseDate * 1000)) else "" }, then update the Text's text interpolation to use "${displayInfo.developer} • $releaseYear" (references: displayInfo, remember, SimpleDateFormat, Date).
776-785:downloadProgressas arememberkey re-derivessizeTexton every download tick unnecessarily.
downloadProgressisn't actually referenced inside therememberlambda — onlydownloadInfo?.getBytesProgress()is. Dropping it from the key list meanssizeTextis still recomputed wheneverdownloadInfochanges, but not on every floating-point progress update.♻️ Suggested refactor
- val sizeText = remember(displayInfo.gameId, downloadProgress, downloadInfo) { + val sizeText = remember(displayInfo.gameId, downloadInfo) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt` around lines 776 - 785, The remember block for sizeText is using downloadProgress as a key which causes unnecessary recomputation on every tick; update the remember key list to include only the actual dependencies used inside the lambda (e.g., displayInfo.gameId and downloadInfo) and remove downloadProgress so sizeText is recomputed only when downloadInfo changes; locate the remember call that computes sizeText (references: sizeText, remember, downloadInfo, getBytesProgress(), formatBytes, downloadingText, displayInfo.gameId) and adjust its key set accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt`:
- Around line 454-461: The KEYCODE_BUTTON_START branch currently calls
onPauseResumeClick() or onDownloadInstallClick() unconditionally; change it to
first check the same enable flags used for UI buttons
(pauseResumeEnabled/buttonEnabled or isValidToDownload and wifiOnly+hasWifi
checks) and only invoke onPauseResumeClick when pauseResumeEnabled is true and
onDownloadInstallClick when buttonEnabled/isValidToDownload (and if wifi-only
mode requires hasWifi, verify that too); otherwise return false (no action).
Update the KEYCODE_BUTTON_START case to replicate the exact guard logic used by
the corresponding UI button handlers so gamepad input respects download/wifi
restrictions.
- Around line 951-977: The START GamepadAction is invoking
onDownloadInstallClick/onPauseResumeClick directly and bypasses the same
enablement checks used in handleKeyEvent; update the GamepadAction construction
in LibraryAppScreen (the GamepadActionBar START branch) to wrap the onClick
callbacks with the same guards: use canPrimaryAction for the install/run
branches and pauseResumeEnabled for the downloading/partial-download branches
and only invoke onDownloadInstallClick or onPauseResumeClick when the
corresponding guard is true (otherwise no-op). Ensure the enabled state mirrors
those guards as well so the START action is disabled when the guard is false.
---
Nitpick comments:
In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt`:
- Around line 416-423: The current NetworkCapabilities snapshot in
LibraryAppScreen (where context/ connectivityManager/getNetworkCapabilities are
used to compute hasInternet and wifiConnected) is static at composition time;
replace this with a reactive Flow backed by
ConnectivityManager.registerNetworkCallback (implement a callbackFlow that emits
updated NetworkCapabilities or booleans) and consume it in the composable with
collectAsState to derive hasInternet/wifiConnected (and consequently
wifiAllowed/buttonEnabled) so the UI updates automatically when network status
changes.
- Around line 614-622: Move the remember block out of the string template to a
local val for readability: create a val (e.g., releaseYear) above the Text call
that uses remember(displayInfo.releaseDate) { if (displayInfo.releaseDate > 0)
SimpleDateFormat("yyyy",
Locale.getDefault()).format(Date(displayInfo.releaseDate * 1000)) else "" },
then update the Text's text interpolation to use "${displayInfo.developer} •
$releaseYear" (references: displayInfo, remember, SimpleDateFormat, Date).
- Around line 776-785: The remember block for sizeText is using downloadProgress
as a key which causes unnecessary recomputation on every tick; update the
remember key list to include only the actual dependencies used inside the lambda
(e.g., displayInfo.gameId and downloadInfo) and remove downloadProgress so
sizeText is recomputed only when downloadInfo changes; locate the remember call
that computes sizeText (references: sizeText, remember, downloadInfo,
getBytesProgress(), formatBytes, downloadingText, displayInfo.gameId) and adjust
its key set accordingly.
- Fix double preLaunchApp call on Success path (PluviaMain.kt) - Remove duplicate imports alongside wildcard (SteamAppScreen.kt) - Move Compose state mutation off IO dispatcher (SteamAppScreen.kt) - Fix TOCTOU race: use captured currentState instead of _state.value (LibraryViewModel.kt) - Redact GOG auth code from logs (SettingsGroupInterface.kt) - Add @volatile to pagination vars for thread safety (LibraryViewModel.kt) - Add missing GOG/Epic keys to remember block (LibraryListPane.kt) - Gate gamepad START button behind enablement checks (LibraryAppScreen.kt) - Remove canFocus=false that killed focus animations (LibraryTabBar.kt) - Switch SortOption persistence from fragile ordinals to stable string keys (SortOption.kt, PrefManager.kt) - Move infinite animations inside AnimatedVisibility scope (BootingSplash.kt) - Use LibraryItem.isInstalled instead of stale remember (LibraryGridCard.kt) - Uncomment GameMigrationDialog now available in this branch (SteamAppScreen.kt)
- Extract remember from string template for readability (releaseYear) - Remove unnecessary downloadProgress key from sizeText remember - Gate GamepadActionBar START callbacks behind enablement checks
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:775">
P2: sizeText is memoized without a changing key; because DownloadInfo mutates in place, the remembered value can go stale and stop reflecting updated bytes during download.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
DownloadInfo mutates in place so downloadProgress is needed as a trigger to recompute byte-based size text during active downloads.
Variables pauseResumeEnabled and buttonEnabled were used in handleKeyEvent lambda but defined after it, causing unresolved reference errors.
Users can now dismiss the Steam connection banner to access navigation (tabs, settings, GOG/Epic games) when Steam is disconnected. Banner reappears when connection state changes.
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/src/main/java/app/gamenative/ui/PluviaMain.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/PluviaMain.kt:230">
P2: LaunchedEffect runs on initial composition after configuration changes, so it immediately resets the rememberSaveable dismissal state and causes the banner to reappear on rotation.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Track previous connection state to only reset dismissed flag when state actually changes, not on initial composition after rotation.
There was a problem hiding this comment.
1 issue found across 6 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryTabBar.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryTabBar.kt:185">
P3: Count label formatting is hardcoded (" ($count)") instead of using a localized/plural string resource, which breaks localization ordering and violates the team’s no‑hardcoded‑strings guidance.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
1 issue found across 8 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryCarouselPane.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryCarouselPane.kt:555">
P3: Hardcoded UI string in the debug tuner UI. Team guidelines require UI text to be defined in string resources for localization/maintainability, even in debug-only panels.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Add automatic focus restoration when overlays are dismissed: - Track previous overlay state to detect when they close - LaunchedEffect restores focus to carousel/grid after dismiss - Fallback onKeyEvent catches unhandled D-pad and restores focus - Focus goes to carousel if CAROUSEL layout, grid otherwise - D-pad now works immediately after closing menus
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt:319">
P2: Focus-restoration after closing system/options panels doesn’t account for an active search overlay, so it can steal focus from the search bar when those overlays close.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Remove LaunchedEffect that was triggering onSearchQuery when returning from game detail view. This was causing the entire list to reload and reset to the beginning, forcing users to scroll back through chunked loading to return to their previous position. The list still refreshes via: - Pull-to-refresh - LibraryInstallStatusChanged events - CustomGameImagesFetched events
- Restore focus when returning from game detail view - Restore focus after tab change (both empty and populated tabs) - Skip focus restoration when search overlay is active - Focus goes to carousel/grid content or root for empty tabs
…erved) Remove carousel layout feature for now as it's out of scope for this PR. The feature is preserved in the feat/carousel-layout-preserved branch for future re-introduction. Changes: - Remove LibraryCarouselPane.kt - Remove CAROUSEL from PaneType enum - Remove carousel option from LibraryOptionsPanel - Simplify focus restoration logic (grid-only)
Accept new autoApplyKnownConfig setting from upstream. Add missing SettingsSwitch import.
f25859e to
f698e23
Compare
# Conflicts: # app/src/main/java/app/gamenative/MainActivity.kt
| enter = fadeIn(animationSpec = tween(durationMillis = 400)), | ||
| exit = fadeOut(animationSpec = tween(durationMillis = 300)), |
There was a problem hiding this comment.
This animation doesn't seem to be doing anything. Maybe it's currently hidden behind the UI?
There was a problem hiding this comment.
I don't feel like I'm the biggest fan of this BootingSplash screen... I'll work on it. This is a note for myself.
| GameCompatibilityStatus.COMPATIBLE -> BadgeStyle( | ||
| icon = Icons.Rounded.Verified, | ||
| backgroundColor = colors.compatibilityGoodBackground.copy(alpha = 0.9f), | ||
| iconTint = colors.compatibilityGood, | ||
| labelResId = R.string.library_compatible, | ||
| ) | ||
|
|
||
| GameCompatibilityStatus.GPU_COMPATIBLE -> BadgeStyle( | ||
| icon = Icons.Rounded.Memory, | ||
| backgroundColor = colors.compatibilityPartialBackground.copy(alpha = 0.9f), | ||
| iconTint = colors.compatibilityPartial, | ||
| labelResId = R.string.library_gpu_compatible, | ||
| ) |
There was a problem hiding this comment.
Oddly, these two seem to be swapped - COMPATIBLE (which means there's a working config on another GPU/family) has a check mark, and GPU_COMPATIBLE (which means it works on this GPU/family) has a little GPU icon.
Should probably swap them and maybe have the COMPATIBLE one be a different colour or something. I'll check it.
| PluviaApp.events.on<SteamEvent.Disconnected, Unit>(onSteamDisconnected) | ||
| PluviaApp.events.on<SteamEvent.LogonStarted, Unit>(onLogonStarted) | ||
| PluviaApp.events.on<SteamEvent.LogonEnded, Unit>(onLogonEnded) | ||
| PluviaApp.events.on<AndroidEvent.BackPressed, Unit>(onBackPressed) | ||
| PluviaApp.events.on<SteamEvent.QrChallengeReceived, Unit>(onQrChallengeReceived) | ||
| PluviaApp.events.on<SteamEvent.QrAuthEnded, Unit>(onQrAuthEnded) | ||
| PluviaApp.events.on<SteamEvent.LoggedOut, Unit>(onLoggedOut) | ||
| PluviaApp.events.on<SteamEvent.RemotelyDisconnected, Unit>(onRemoteDisconnected) |
There was a problem hiding this comment.
this change looks a bit dangerous to me
| PluviaApp.events.on<SteamEvent.Disconnected, Unit>(onSteamDisconnected) | ||
| PluviaApp.events.on<SteamEvent.LogonStarted, Unit>(onLogonStarted) | ||
| PluviaApp.events.on<SteamEvent.LogonEnded, Unit>(onLogonEnded) | ||
| PluviaApp.events.on<AndroidEvent.BackPressed, Unit>(onBackPressed) | ||
| PluviaApp.events.on<SteamEvent.QrChallengeReceived, Unit>(onQrChallengeReceived) | ||
| PluviaApp.events.on<SteamEvent.QrAuthEnded, Unit>(onQrAuthEnded) | ||
| PluviaApp.events.on<SteamEvent.LoggedOut, Unit>(onLoggedOut) | ||
| PluviaApp.events.on<SteamEvent.RemotelyDisconnected, Unit>(onRemoteDisconnected) |
There was a problem hiding this comment.
Seems dangerous to remove these two. @ObfuscatedVoid do you know what the reason was to remove them?
There was a problem hiding this comment.
Appears that this file is just formatting changes? @xXJSONDeruloXx @phobos665 please tell me if you see something else (pointing out this file specifically because it's important)
There was a problem hiding this comment.
Note to self to check GridViewCard class
…tore not showing if no image
| Timber.tag("IntentLaunch").i("Processing pending launch request for app ${launchRequest.appId} (user is now logged in)") | ||
|
|
||
| // Extract game ID from appId (format: "STEAM_<id>" or "CUSTOM_GAME_<id>") | ||
| val gameId = ContainerUtils.extractGameIdFromContainerId(launchRequest.appId) | ||
| val gameSource = ContainerUtils.extractGameSourceFromContainerId(launchRequest.appId) | ||
|
|
||
| val isInstalled = when (gameSource) { | ||
| GameSource.STEAM -> { | ||
| SteamService.isAppInstalled(gameId) | ||
| } | ||
|
|
||
| GameSource.GOG -> { | ||
| GOGService.isGameInstalled(gameId.toString()) | ||
| } | ||
|
|
||
| GameSource.EPIC -> { | ||
| EpicService.isGameInstalled(gameId) | ||
| } | ||
|
|
||
| GameSource.AMAZON -> { | ||
| AmazonService.isGameInstalledByAppId(gameId) | ||
| Timber.tag("IntentLaunch") | ||
| .i("Processing pending launch request for app ${launchRequest.appId} (user is now logged in)") | ||
| when (val resolution = resolveGameAppId(launchRequest.appId)) { | ||
| is GameResolutionResult.NotFound -> { | ||
| val appName = resolveNotInstalledGameName(context, resolution.originalAppId, resolution.gameId) | ||
| Timber.tag("IntentLaunch").w("Game not installed: $appName (${launchRequest.appId})") | ||
| msgDialogState = MessageDialogState( | ||
| visible = true, | ||
| type = DialogType.SYNC_FAIL, | ||
| title = context.getString(R.string.game_not_installed_title), | ||
| message = context.getString(R.string.game_not_installed_message, appName), | ||
| dismissBtnText = context.getString(R.string.ok), | ||
| ) | ||
| return@let | ||
| } | ||
|
|
||
| GameSource.CUSTOM_GAME -> { | ||
| CustomGameScanner.isGameInstalled(gameId) | ||
| } | ||
| } | ||
|
|
||
| if (!isInstalled) { | ||
| val appName = SteamService.getAppInfoOf(gameId)?.name ?: "App ${launchRequest.appId}" | ||
| Timber.tag("IntentLaunch").w("Game not installed: $appName (${launchRequest.appId})") | ||
|
|
||
| // Show error message |
There was a problem hiding this comment.
Some functionality has been lost here, need to bring it back
There was a problem hiding this comment.
Need to add translations for all languages
Feat/UI ux overhaul utkarsh 3
from #395 and subsequent branch https://github.com/phobos665/GameNative/tree/feat/ui-overhaul-resolved-without-xserver-screeen plus merge conflict resolution to current upstream/master.
Hope I did this right for proper authorship, let me know if anything needed to better attribute! Opening as new PR per discussion with Phobos on Discord
Summary by cubic
Overhauled the library UI/UX to be controller-first, responsive, and easier to navigate. Adds source tabs with per-source counts (incl. Amazon), improved focus/navigation, transparent system bars, and clearer boot/download states, plus a merge with the latest upstream.
New Features
Bug Fixes
Written for commit 6f156cc. Summary will update on new commits.
Summary by CodeRabbit
New Features
UI/UX
Bug Fixes
Documentation
Chores