Skip to content

feat: ui/ux overhaul from ObfuscatedVoid & Phobos + remaining merge conflict resolutions#591

Closed
xXJSONDeruloXx wants to merge 59 commits intoutkarshdalal:masterfrom
xXJSONDeruloXx:feat/ui-ux-overhaul
Closed

feat: ui/ux overhaul from ObfuscatedVoid & Phobos + remaining merge conflict resolutions#591
xXJSONDeruloXx wants to merge 59 commits intoutkarshdalal:masterfrom
xXJSONDeruloXx:feat/ui-ux-overhaul

Conversation

@xXJSONDeruloXx
Copy link
Copy Markdown
Contributor

@xXJSONDeruloXx xXJSONDeruloXx commented Feb 20, 2026

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

    • Tabs per source with counts (Steam/GoG/Epic/Amazon), Installed tab, improved search, and a draggable scrollbar.
    • Persisted sorting via stable string keys (name A–Z/Z–A, recently played, size, installed‑first) with migration from legacy ordinals.
    • Controller-first: GamepadActionBar, Quick/System menus, gradient focus borders, and centralized input prompt icons (see THIRD_PARTY_NOTICES).
    • Connection status banner with offline mode, retry, and dismiss (persists across config); animated BootingSplash with progress; updated dark theme with transparent status/navigation bars.
  • Bug Fixes

    • Non‑Steam art now shows; compatibility/install/store info visible even without images.
    • Navigation: restored GamepadActionBar, better focus recovery and back behavior; L/R bumpers switch tabs on empty/transition states; START gated behind enablement.
    • Stability: fixed card‑entry crash, moved Compose state off IO, added @volatile to pagination, resolved LibraryViewModel race, and removed double preLaunchApp.
    • Correctness: Installed filters apply across Steam/GoG/Epic/Amazon; counts include Amazon; use LibraryItem.isInstalled; compute button states before key handlers; preserve scroll and banner dismissal; redacted GOG codes.

Written for commit 6f156cc. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Immersive fullscreen, library tabs & sorting, quick menu, gamepad action bar, compatibility badges, draggable scrollbar, progress-aware boot splash, options panel, game options panel, many new composables.
  • UI/UX

    • Redesigned library, settings and system menus, updated search bar, connection status banner with retry/continue offline, responsive/window-size helpers, new theme/color system, expanded localized strings and centralized input icons.
  • Bug Fixes

    • Safer QR login handling, improved supporters/error states, more robust connection lifecycle handling.
  • Documentation

    • Added third‑party notices.
  • Chores

    • Ignore local SQLite DB file.

ObfuscatedVoid and others added 7 commits December 23, 2025 02:11
…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).
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 20, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Large 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

Cohort / File(s) Summary
Project metadata
/.gitignore, THIRD_PARTY_NOTICES
Added kls_database.db to .gitignore; added third‑party asset notices (Kenney Input Prompts).
App entry & app-wide plumbing
app/src/main/java/app/gamenative/MainActivity.kt, app/src/main/java/app/gamenative/PluviaApp.kt, app/src/main/java/app/gamenative/PrefManager.kt
MainActivity: immersive/fullscreen handling and onWindowFocusChanged; PluviaApp: isSupabaseInitialized(); PrefManager: new librarySortOption preference.
Models & enums
app/src/main/java/app/gamenative/data/LibraryItem.kt, app/src/main/java/app/gamenative/ui/enums/*
LibraryItem adds sizeBytes and isInstalled; new enums added: ConnectionState, LibraryTab, SortOption; minor HomeDestination annotation tweak.
ViewModels & app state
app/src/main/java/app/gamenative/ui/model/*.kt
MainViewModel: SavedStateHandle, persisted route, connection lifecycle and connection APIs (startConnecting/continueOffline/retry), listener cleanup; LibraryViewModel: tab/sort APIs, size aggregation and unified sorting/filtering; UserLoginViewModel: snackEvents flow and QR retry.
Login & top-level navigation
app/src/main/java/app/gamenative/PluviaApp.kt, app/src/main/java/app/gamenative/ui/PluviaMain.kt, app/src/main/java/app/gamenative/ui/screen/login/UserLoginScreen.kt
PluviaMain: centralized game resolution, navigation adjustments, ConnectionStatusBanner overlay; UserLoginScreen: accepts ConnectionState, simplified retry signature; login flows updated to reflect connection state.
Library UI — major refactor
app/src/main/java/app/gamenative/ui/screen/library/*, .../components/*
Large refactor: LazyVerticalGrid-based list, new PaneType/layouts, tab bar, options panel, LibraryOptionsPanel, GameOptionsPanel, QuickMenu, SystemMenu, GamepadActionBar, Gamepad-focused navigation, many signature changes to accept sort/tab callbacks and layout.
Library item visuals & helpers
app/src/main/java/app/gamenative/ui/screen/library/components/*
Introduced ListViewCard/GridViewCard, GameSourceIcon, image URL helpers, InstallStatusBadge, compatibility wiring; LibraryBottomSheet removed.
New/reworked UI components
app/src/main/java/app/gamenative/ui/component/*
Added CompatibilityBadge, ConnectionStatusBanner, Scrollbar, OptionListItem/OptionRadioItem, QuickMenu, GamepadActionBar and related composables.
Booting, splash & supporters dialogs
app/src/main/java/app/gamenative/ui/components/BootingSplash.kt, .../dialog/SupportersDialog.kt
BootingSplash API changed to progress-based signature and richer animations; SupportersDialog adds supabase init guard and error state handling.
Steam service & per-app screens
app/src/main/java/app/gamenative/service/SteamService.kt, .../appscreen/SteamAppScreen.kt
SteamService: null-safety guard for QR login flow; SteamAppScreen: formatting/signature trailing-comma changes and removal of a migration dialog path.
Theming & responsive utilities
app/src/main/java/app/gamenative/ui/theme/*, app/src/main/java/app/gamenative/ui/util/WindowSize.kt, app/src/main/res/values/strings.xml
New PluviaColors/PluviaTheme, many new color tokens and settings tile helpers; window-size/adaptive utilities; many new string resources.
Icons registry
app/src/main/java/app/gamenative/ui/icons/InputIcons.kt
Large static registry of input icon drawable references (Xbox, Keyboard, Mouse, Touch).

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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • phobos665

Poem

"I hopped through code with ears held high,
I stitched new badges, menus, sky.
Tabs and banners, quick‑menus bloom,
I nibbled bugs and cleared the room.
A carrot toast — the UI sings, hop on and try! 🐇"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.02% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: a UI/UX overhaul with conflict resolutions. It directly relates to the large changeset covering library UI redesign, controller support, connection handling, and visual updates.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

- 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
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 | 🔴 Critical

Bug: preLaunchApp is invoked twice on the Success path.

Inside the consumePendingLaunchRequest()?.let block, the Success branch already calls setLaunchedAppId, setBootToContainer, and preLaunchApp (lines 344–354). However, lines 357–367 call them again unconditionally after the when block (still within the let). The NotFound branch escapes via return@let, so only Success is affected — it will trigger a double launch.

The second block (lines 357–367) looks like a leftover from before the resolveGameAppId refactor.

🐛 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 | 🔴 Critical

Fix 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 | 🟠 Major

Auth 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 | 🟡 Minor

Remove unused coroutineScope parameter from handleGogAuthentication and handleEpicAuthentication.

Both functions declare a coroutineScope parameter 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 coroutineScope parameter from function signatures at lines 99–106 and 151–158.

Update call sites (lines 287, 328, 357) to remove the coroutineScope argument.

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 | 🔴 Critical

Build 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 means Preview_AppScreen is being parsed as a local function nested inside AppScreenContent, indicating that AppScreenContent'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 | 🟡 Minor

Hardcoded 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 via stringResource(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 | 🔴 Critical

Build failure: safePaddingModifier resolves to Any instead of Modifier.

The if branch (lines 233–237) computes a Dp value (topPadding) but never wraps it in a Modifier, while the else branch returns Modifier. The inferred common type is Any, 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 | 🔴 Critical

Remove duplicate imports causing build failures.

The import block contains duplicate imports:

  • rememberCoroutineScope and snapshotFlow (lines 16–17 and 61–62; already covered by the wildcard androidx.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.LocalContext

Remove 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 | 🟡 Minor

Unconditional applyImmersiveMode() re-application conflicts with SetSystemUIVisibility event handler.

The onSetSystemUi callback (line 90-92) calls AppUtils.hideSystemUI(this, !it.visible), which respects the SetSystemUIVisibility event's intent to show bars when visible=true. However, applyImmersiveMode() unconditionally hides bars in both onResume (line 273) and onWindowFocusChanged when hasFocus=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.getAppInfoOf won't resolve names for non-Steam games.

In the NotFound branch, SteamService.getAppInfoOf(resolution.gameId) is used to retrieve the game name regardless of game source. For GOG, Epic, or Custom games this will likely return null, 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 | 🟡 Minor

Silent 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 LaunchedEffect inside the panel's AnimatedVisibility block 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 AnimatedVisibility content for the panel (after the Column):

// 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.clickable and an explicit Role.Button semantic:

🛡️ 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 | 🟡 Minor

Hardcoded "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.Person as the "closed" dropdown indicator is misleading.

When the status picker is closed, Icons.Default.Person suggests profile navigation rather than a collapsible dropdown. Consider Icons.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.selectable passes focus state instead of selection state — accessibility bug.

selected = isFocused causes selectable to 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 from isFocused through the backgroundColor computation; selected should 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 | 🟡 Minor

Resolve TODO on "Go Offline" code path before shipping.

The // TODO: test this comment 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

fetchKofiSupporters swallows exceptions, so hasError won't trigger on network failures.

Looking at SupportersUtils.kt (lines 20-31), fetchKofiSupporters catches all exceptions internally and returns emptyList(). This means the catch block 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, fetchKofiSupporters should propagate exceptions (or return a Result).

🤖 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 | 🟡 Minor

Missing @DrawableRes on alias properties.

start and select are aliases for menu and view respectively, but they lack their own @DrawableRes annotation. 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 | 🟡 Minor

Unused scale animation for PaneType.LIST.

The targetScale computes 1.015f for the focused LIST case, and a spring animation runs on the scale value (lines 95–102). However, the scale is never passed to ListViewCard (lines 105–115), which doesn't accept a scale parameter. Only GridViewCard receives the animated scale value. This means the animation executes but has no effect when in list mode.

Remove the LIST branch from targetScale (defaulting to 1f for list) or pass scale to ListViewCard if 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 | 🟡 Minor

Unused variable and duplicate log statement.

Line 223: isSteamConnected is 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 | 🟡 Minor

Hardcoded 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 uses R.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 | 🟡 Minor

Remove the asymmetry in onSteamConnected: set isSteamConnected = true for all connection events, not just auto-login.

The handler only sets isSteamConnected = true when isAutoLoggingIn is true (line 111), but the disconnect handlers (onSteamDisconnected at line 119 and onRemoteDisconnected at line 125) always set it to false. This asymmetry means manual connections leave isSteamConnected = false in UserLoginState until the next disconnect occurs.

While MainViewModel maintains the authoritative connection state symmetrically (setting isSteamConnected = true on all connection events and false on all disconnects), UserLoginViewModel maintains 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_PLAYED sort is identical to INSTALLED_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 // TODO comment 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 | 🟡 Minor

Inconsistent state reads — use currentState instead of _state.value.

Line 307 captures val currentState = _state.value, but lines 480–502 re-read _state.value for currentTab and source flags. If another coroutine updates _state between those reads, the filter logic may use a mix of old and new state. Use the already-captured currentState for 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

remember keys are missing showGOGInLibrary and showEpicInLibrary.

The lambda reads state.showGOGInLibrary and state.showEpicInLibrary (lines 153–154), but the remember keys on line 150 only list showSteamInLibrary and showCustomGamesInLibrary. 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 | 🟡 Minor

Brand 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 | 🟡 Minor

First tip is missing the "Tip: " prefix present on all other entries

Line 95 reads "Booting may take a few minutes on first launch" while every other tip in the list starts with "Tip: ". The Crossfade renders 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.blur is unsupported on Android < 12 (app supports minSdk 26)

The app targets minSdk 26 (Android 8.0), but Modifier.blur requires API 31+. On Android 8–11 devices, the blur modifier is silently ignored, causing the glow Text to 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 Shadow modifier 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.

Comment thread app/src/main/java/app/gamenative/ui/components/BootingSplash.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/model/MainViewModel.kt
Comment thread app/src/main/java/app/gamenative/ui/PluviaMain.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/screen/library/components/LibraryTabBar.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/screen/library/components/SystemMenu.kt Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

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.

Comment thread app/src/main/java/app/gamenative/MainActivity.kt
Comment thread app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListPane.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/component/QuickMenu.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/enums/SortOption.kt
Comment thread app/src/main/java/app/gamenative/ui/component/Scrollbar.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/screen/library/components/GameOptionsPanel.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/component/OptionListItem.kt
Comment thread app/src/main/java/app/gamenative/ui/screen/library/components/SystemMenu.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/screen/library/components/LibraryTabBar.kt Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

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.

Comment thread app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

preLaunchApp is called twice for every successful pending-launch after login — remove the duplicate block.

When resolveGameAppId returns Success, the Success branch (lines 343–353) already calls viewModel.setLaunchedAppId, viewModel.setBootToContainer, and preLaunchApp. Because the Success branch does not return@let, execution falls through to lines 356–366, which repeat the exact same three calls unconditionally. The result is that preLaunchApp runs twice: the loading dialog fires twice, cloud-sync/download runs twice, and viewModel.launchApp (→ navController.navigate(XServer)) is invoked twice.

The NotFound branch correctly guards against this via return@let, but Success is 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 | 🟠 Major

Assign sizeFromStore on the Main dispatcher.

sizeFromStore is mutated inside withContext(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: scope shadows the outer rememberCoroutineScope() 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: isInstalled is always true at this point — && isInstalled is redundant.

The early return at line 176 guarantees isInstalled == true before reaching line 183, making the && isInstalled guard 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.

Comment thread app/src/main/java/app/gamenative/ui/PluviaMain.kt
Comment thread app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt Outdated
- 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)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 for LibraryTab — fully qualified references throughout.

LibraryTab is 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.

paginationCurrentPage and lastPageInCurrentFilter are plain var fields written inside onFilterApps (Dispatchers.IO). Multiple callers — the three init flow collectors and event handlers — can invoke onFilterApps concurrently, creating a data race.

A lightweight fix is to move these into the MutableStateFlow<LibraryState> (which is already thread-safe), or protect them with a Mutex/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: Inconsistent appId prefix 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}").

Comment thread app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt
Comment thread app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt
…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)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 | 🟠 Major

Old 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 AppScreenContent is called, stacking the entire old UI above the new one. Additionally, lines 756–776 declare statusMessageFlow, statusMessageState, statusMessage, and timeLeftText inside an if (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: CompactIconButton misuses selectable for a stateless action button.

selectable(selected = false, indication = null, ...) with a constant false is semantically wrong — selectable implies 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, buttonEnabled are 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.

Comment thread app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt
…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
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 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.

getNetworkCapabilities is called once at composition time. If the device connects/disconnects Wi-Fi after landing on this screen, hasInternet, wifiConnected, and the derived wifiAllowed/buttonEnabled values 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-backed Flow (e.g. via ConnectivityManager.registerNetworkCallback exposed as a callbackFlow) 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: Extract remember out of the string template for readability.

Embedding a remember call inside a string interpolation is valid but hurts readability. A local val above the Text call 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: downloadProgress as a remember key re-derives sizeText on every download tick unnecessarily.

downloadProgress isn't actually referenced inside the remember lambda — only downloadInfo?.getBytesProgress() is. Dropping it from the key list means sizeText is still recomputed whenever downloadInfo changes, 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
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

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.

Comment thread app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt Outdated
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.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

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.

Comment thread app/src/main/java/app/gamenative/ui/PluviaMain.kt Outdated
Track previous connection state to only reset dismissed flag when
state actually changes, not on initial composition after rotation.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

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.

Comment thread app/src/main/java/app/gamenative/ui/screen/library/components/LibraryTabBar.kt Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

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
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

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.

Comment thread app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt Outdated
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.
# Conflicts:
#	app/src/main/java/app/gamenative/MainActivity.kt
Comment on lines +87 to +88
enter = fadeIn(animationSpec = tween(durationMillis = 400)),
exit = fadeOut(animationSpec = tween(durationMillis = 300)),
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This animation doesn't seem to be doing anything. Maybe it's currently hidden behind the UI?

Copy link
Copy Markdown
Owner

@utkarshdalal utkarshdalal Feb 25, 2026

Choose a reason for hiding this comment

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

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.

Comment on lines +88 to +100
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,
)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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.

Comment on lines -200 to -207
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)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

this change looks a bit dangerous to me

Comment on lines -200 to -207
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)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Seems dangerous to remove these two. @ObfuscatedVoid do you know what the reason was to remove them?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Note to self to check GridViewCard class

Comment on lines -221 to -253
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
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Some functionality has been lost here, need to bring it back

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Need to add translations for all languages

@github-project-automation github-project-automation Bot moved this from PR Needs Review to Done in Open Source Feb 27, 2026
@xXJSONDeruloXx xXJSONDeruloXx deleted the feat/ui-ux-overhaul branch March 1, 2026 17:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants