Skip to content

feat(nova): open game detail in its own window instead of a bottom sheet - #187

Merged
papi-ux merged 1 commit into
masterfrom
nova/game-detail-window
Aug 5, 2026
Merged

feat(nova): open game detail in its own window instead of a bottom sheet#187
papi-ux merged 1 commit into
masterfrom
nova/game-detail-window

Conversation

@papi-ux

@papi-ux papi-ux commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Summary

Opening a game raised NovaGameDetailSheet, a BottomSheetDialogFragment of just over 3,000 lines that mixed sheet plumbing with the Compose content it hosted. This gives the detail its own window and splits the file along the seam that was already there.

  • the Compose half moves verbatim into NovaGameDetailContent.kt, so the ordering the source guards assert is unchanged
  • the fragment half becomes NovaGameDetailActivity, which keeps the constructor lambdas the sheet was built with as properties of the same name and nullable shape. The 500-line body that used them needed context and lifecycle substitutions rather than a rewrite, so what ships is the code that was reviewed as a sheet
  • the window returns the launch it chose rather than performing it, so the stream starts from the library after the window is gone

Two payloads travel back, mirroring the two paths the sheet already had: artwork edits merge into the library list, and the launched game goes to the launch. An earlier draft passed only a game id and looked it back up in allGames, which silently dropped the MangoHUD choice made in that window — launchGame reads game.mangohud.

PolarisGame is @Serializable, so the game travels as JSON rather than through a hand-written mirror of PolarisGameJsonAdapter that could drift from it. The serialization runtime is already on the APK's runtime classpath via :shared:polaris:model; the added dependency only puts it on the app's compile classpath.

expandBottomSheet sized whatever the fragment's root view was, which was right for the detail sheet and incidental for the desktop-Steam dialog that also called it. That dialog is the only sheet the window still hosts, so the helper now sizes the content view it is handed.

Behaviour is otherwise unchanged. The cinematic restructure is separate.

Guard tests

The four suites that string-match the detail source now read both halves, Activity first, joined — Kotlin sources end in a newline, so no symbol can straddle the join and every ordering assertion still compares symbols within the same half. Where a guard's subject genuinely moved it was re-pointed, not relaxed:

Guard Was Now
sheet chrome the detail sheet's own host the desktop-Steam dialog it still hosts
mutation ownership library owns the artwork coordinator library and window each build one from the same factory
lifecycle gate viewLifecycleOwnerLiveData lifecycle.currentState
library merge inline onGameUpdated block onGameDetailResult

NovaGameDetailSheetComposeTest is renamed NovaGameDetailComposeTest. Three stale lint-baseline.xml entries for the deleted file are removed, and the one warning that moved with the code (UseKtx on SharedPreferences.edit) is fixed rather than re-baselined.

Exact candidate

  • Commit: d1f7e162
  • Base: 753dc381dfe70bcc9a30ebb2acb8ff76c7a4b353

Verification

Run against a clean build of the pushed tree:

  • :app:testNonRoot_gameDebugUnitTest — 1,170 tests, 0 failures/errors
  • :app:lintNonRoot_gameDebug -PlintFailOnError=true — CI's exact invocation, no errors
  • :app:assembleNonRoot_gameDebug
  • :app:assembleNonRoot_gameRelease
  • :app:assembleNonRoot_gameDebugAndroidTest
  • physical Retroid Pocket 6, shipping nonRoot_gameDebug flavor, class-filtered — NovaGameDetailComposeTest, 0 failures
  • on-device: opening a game gives topResumedActivity = NovaGameDetailActivity; B returns to the library
  • on-device: the desktop-Steam decision dialog still renders with Nova glass, correct sizing and its drag handle
  • on-device launch, instrumented twice: onLaunch game=ARC Raiders mirror=false virtual=falseresult game=ARC Raiders id=1BECC52E-… appId=1937920881, flags preserved, com.papi.nova.Game started. Diagnostics removed before commit
  • modified-file scan for secrets, debug logging and capture harnesses: zero hits

Force-push, and why

The first push of this branch was an incomplete tree: it carried the four new files but none of the modifications or deletions, so NovaGameDetailSheet.kt still existed alongside NovaGameDetailContent.kt and the serialization dependency was missing. CI caught it; my local runs did not, because my working tree was correct and only the commit was wrong.

The cause was a git stash / git stash pop run between staging and committing while checking the manifest's line endings. stash pop restores to the working tree but not the index, which silently unstaged everything. The commit has been amended with the complete tree, the staged deletions verified explicitly before committing, and the whole suite re-run against it.

Note for review

During the first hands-on launch I saw one launch resolve to a different game than the window had open. I could not reproduce it across two instrumented runs, both of which show the contract carrying the right game and flags. Two host-side conditions applied at the time and not afterwards: the game in question is not installed on the host — Steam answers the launch with a 51 GB install prompt — and there was a resumable session for the other game, which the Moonlight protocol prefers over starting a new app. The launch-decision code in onPrimaryLaunch is byte-identical to the sheet's. I am flagging it rather than filing it as fixed, because I have an explanation for the session but not for the toast text I saw.

The mirrorDesktop branch of the desktop-Steam decision has not been exercised on hardware; the re-runs had desktopSteamActive: false.

Opening a game raised a BottomSheetDialogFragment of just over 3,000 lines that
mixed sheet plumbing with the Compose content it hosted. This gives the detail its
own Activity and splits the file along the seam that was already there.

The Compose half moves verbatim into NovaGameDetailContent.kt so the ordering the
source guards assert is unchanged. The fragment half becomes NovaGameDetailActivity,
which keeps the constructor lambdas the sheet was built with as properties of the
same name and shape, so the 500-line body that used them needed context and
lifecycle substitutions rather than a rewrite.

The window returns the launch it chose rather than performing it, so the stream
starts from the library after the window is gone. Two payloads travel back, mirroring
the two paths the sheet already had: artwork edits merge into the library list, and
the launched game goes to the launch. Passing only an id and looking the game back up
would have dropped the MangoHUD choice made in that window.

PolarisGame is @serializable, so the game travels as JSON rather than through a
hand-written mirror of PolarisGameJsonAdapter that could drift from it. The
serialization runtime is already on the APK's runtime classpath via the shared model;
this only puts it on the app's compile classpath.

expandBottomSheet sized whatever the fragment's root view was, which was right for
the detail sheet and incidental for the desktop-Steam dialog that also called it.
That dialog is the only sheet the window still hosts, so the helper now sizes the
content view it is handed.

Behaviour is otherwise unchanged; the cinematic restructure is separate.
@papi-ux
papi-ux force-pushed the nova/game-detail-window branch from 55743c8 to d1f7e16 Compare August 5, 2026 21:31
@papi-ux
papi-ux merged commit c91c603 into master Aug 5, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant