- Default orientation now faces right (initialYaw flipped).
- Idle auto-spin actually resumes: the time-based check moved into the RAF
loop, since a computed reading performance.now() cached and never restarted
after the first interaction. Quiet window is 2s.
- Flick momentum without a JS decay loop: the release velocity is handed to
the box as one extra rotation and a CSS ease-out curve coasts it to a stop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019itLXRfJXGGbhPY3JyqnuN
Read the front (box-2D) and spine image natural ratios on mount, not only on
the load event — a cached cover decodes before the listener binds, which left
the box stuck at the default ratio instead of the real box-2D proportions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019itLXRfJXGGbhPY3JyqnuN
Add user-select: none to the box and pointer-events: none to the faces so a
drag always lands on the root (which owns the rotation listeners) and never
starts a text / image selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019itLXRfJXGGbhPY3JyqnuN
Adds an RBox3D primitive that builds a rotatable, fake-3D game box from
three flat ScreenScraper scans (front, back, spine) using CSS 3D
transforms. Box proportions derive from the images themselves; it rotates
via pointer drag, arrow keys / gamepad D-pad, and the right analog stick,
drifts gently when idle, and honours prefers-reduced-motion.
The game detail hero (CoverColumn) upgrades to the spinning box when the
"3D box" boxart style is selected and the rom has the full set of faces,
falling back to the flat cover otherwise.
Backend: persist the box-2D-side (spine) scan locally, mirroring the
existing box-2D-back handling — new BOX2D_SIDE media type + box2d_side_path
on ss_metadata, opt-in via scan.media.
- RBox3D primitive + Storybook story (controls + keyboard-rotation play())
- useBoxFaces composable resolving the three faces + a `complete` gate
- box3d-alt i18n key across all locales
- backend BOX2D_SIDE persistence + tests
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019itLXRfJXGGbhPY3JyqnuN
Show a "where you left off" screenshot on the Home continue-playing rail and
the live-activity board, with a small cover-art thumbnail (PIP) in the corner
so the game stays identifiable. Both render at the image's natural aspect.
Backend:
- New shared util `continue_playing_screenshot(rom, latest_save)` resolving the
image in priority order: latest save's screenshot, then title screen, then
first gameplay screenshot (None → frontend falls back to cover art).
- `SimpleRomSchema.screenshot_path` populated only on the `last_played` query;
`get_latest_saves_for_roms` batch handler (+ tests).
- ActivityEntry / ActivityEntrySchema gain `screenshot_path`, computed from the
session player's latest save in both the socket and REST heartbeat paths.
Frontend:
- New shared `CoverArtPip.vue` (bottom-right 2D cover thumbnail), reused by
GameCard and ActivityCard.
- Home continue-playing rail uses `screenshot_path` + PIP, natural aspect (no
forced hero/style).
- Activity board: screenshot-forward cover + PIP, and a wrapping flex layout so
cards share a uniform height with natural-ratio widths (gallery-card
behavior).
- GameCover only keys the measured ratio by rom id for the rom's own cover, so
a `coverSrc` override (screenshot) never pollutes the gallery's ratio cache.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clicking Play at the center of a gallery cover now runs the shared-element
view transition into the /ejs (EmulatorJS) or /ruffle hero, matching the
card → details morph.
- useGameActions gains an optional `coverEl` resolver; `play()` wraps the
navigation in `morphTransition` (cover → player hero, same `rom-cover-<id>`
tag the player paints statically) and awaits the push so the snapshot is
taken after the player renders. GameCard supplies its GameCover box.
- The player heroes only seeded `rom` from `currentRom` (set via GameDetails),
so a direct gallery→play left `rom` null and the `v-if`-gated hero never
rendered — nothing to morph into. Seed a lightweight `heroSeed` SimpleRom
from the gallery store (new `galleryRoms.getRomById`) so the cover paints
its morph tag immediately; `rom` fills in on mount. Play is disabled until
the full payload loads.
- Enable hover-motion on both player heroes so the cover spin / hover video
work there too.
- Arcade systems (arcade / neogeoaes / neogeomvs) skip the cartridge slot-in
animation (new `isArcadeSystem`).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework useCoverAnimation to mirror v1's useGameAnimation, which composes
two motions on different CSS properties so they don't fight: the spin
owns `transform: rotate` (set per-frame) while the disc/cartridge slot-in
owns `margin-top` (eased in CSS with an overshoot). A single transform for
both couldn't ease the slide independently of the per-frame spin.
- CD launch spins at max while sliding the cover down into the drive;
cartridge launch seats fully into its bay (1/3 height).
- Drop the cartridge hover animation entirely — matches v1, which only
spins discs and plays the hover video on hover.
- Enable hover-motion on the EmulatorJS and Ruffle player heroes so the
hover spin / video work there too.
- Fix a `.ame-cover__img` typo (missing "g") that was silently breaking
the cover image base styles, and ease `margin` in CSS on `.game-cover__img`
so the slide composes with the bloom reveal instead of overriding it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The re-pack debounce in useGalleryCoverRatios was raised from 150ms to
350ms (c5d844e1) to avoid a re-pack storm during fast scroll, but the
test still advanced fake timers by 150ms and expected the debounced
ratioVersion bump to have fired — so it asserted 1 and got 0. Advance
by the new 350ms delay at all three sites. Pre-existing failure on
master, unrelated to this branch's changes; bundled here to unblock CI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gate automatic account creation on OIDC login behind a new OIDC_ALLOW_REGISTRATION environment variable. Defaults to true, preserving the current auto-provisioning behavior; set it to false to run OIDC in an "existing users only" mode, where a login from an email without an existing RomM account is rejected with a 403 instead of silently creating one. Existing users are unaffected either way.
Adds the config constant, env.template and docs entries, and tests covering the enabled/disabled and existing-user paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Added "total-sessions" key to various language JSON files for activity localization.
- Updated ActivityCard component to reflect changes in the activity view.
- Enhanced Activity view to display a live session counter with a tooltip.
- Introduced virtual scroll debugging to monitor performance in the gallery.
- Adjusted layout styles for better responsiveness and visual consistency across components.
Cover hugs the title side of its fixed-width column (and the skeleton
matches), so the empty space sits on the left and the cover reads as
attached to its title.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With natural-aspect covers, the list-row cover lived inside the title cell
and its variable width pushed each row's title/meta to a different x. Add a
dedicated fixed-width (64px) cover column to the shared list grid template —
big enough for any portrait→square game cover — so the title column starts
at the same x on every row. Header and skeleton pick it up via the shared
column config; the cover renders left-aligned at its natural aspect.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Gallery list view: drop the `fixed` opt-out on its GameCard so the
row thumbnail renders at the cover's natural aspect (fixed height,
natural width) like everywhere else. Removes the now-unused `fixed`
prop / class / CSS branch from GameCard.
- CoverPlaceholder: the title now scales with the cover box (container
query units, clamp(8px, 8cqmin, 18px), em padding) instead of a fixed
12px — readable on a tiny list thumb, proportionate on the detail hero.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The overview's screenshots and How-Long-To-Beat blocks rendered with no
label, unlike the related-games sections. Wrap both in the same labelled
section pattern (renamed `overview-tab__related-*` → `__section-*` since it
now covers non-related blocks too). The HLTB heading is gated on a
`hasHltb` computed so it doesn't show when the strip is empty.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The edit dialog's hero used a fixed 240px cover column with the cover
centered, so a natural-width cover left variable leftover space — making
the gap to the fields vary by cover shape. Size the column to the cover
(`auto`) so the gap is exactly the grid gap for any cover, and give it a
touch more room (24px).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend natural-aspect cover rendering beyond the gallery to the rest of
the app (continue-playing stays a fixed 16:9 hero, by request).
- GameCard size tiers now render fixed-height / natural-width like the
default card (size just picks the height); covers EditRomDialog (lg)
and ManageCollectionsDialog (xs) for free. Adds a `fixed` opt-out for
dense aligned tables.
- GameListRow (compact list view) opts into `fixed` so its cover column
stays uniform for row alignment.
- Compact fixed-column thumbnails (ScanPlatformRow, DeleteRomDialog,
RefreshMetadataDialog, ActivityCard) stop cropping (object-fit
cover → contain): the whole cover shows at its true aspect while the
slot stays uniform.
Deliberately unchanged: collection mosaics (composite collage art),
save/state screenshots (16:9, not covers), and the provider cover-picker
comparison grids (uniform tiles aid side-by-side selection).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swap the hand-rolled RImg + useCoverArt cover resolution in
RandomPickWidget for the shared GameCover, which measures the image's
natural ratio. The thumbnail now renders at a fixed 70px height with
natural width (no crop), matching the gallery, and the widget's cover
plumbing (coverSrc / coverContain / placeholder fallback) collapses into
GameCover.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the measured-ratio map, debounced ratioVersion, onCardRatio handler,
and ratioAt resolver out of GalleryShell into useGalleryCoverRatios. The
composable owns its debounce-timer cleanup (onBeforeUnmount), so the shell
no longer tracks ratioBumpTimer. Behaviour is unchanged; adds a unit test
for the dedup / debounce / position→rom→ratio mapping.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tighten the multi-line comments added across GameCover/GameCard/
GalleryShell/useGalleryVirtualItems/useResponsiveColumns and the
packFlowRows test to one or two lines each, keeping the load-bearing
rationale and dropping the prose.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Since rows became variable-length contiguous runs, rowIndex was hardcoded
to 0 at both build sites and read nowhere. Remove it so a future reader
doesn't mistake 0 for a meaningful index.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ratioByRomId is keyed by rom id so a re-visited platform/collection
re-packs instantly without re-waiting on image loads. It's never pruned,
but each entry is two numbers (a few hundred KB even at tens of thousands
of distinct ROMs), so an LRU ceiling isn't worth it. Document the choice
so it reads as deliberate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The flow-packer sizes a row from cardHeight * ratio (floating point) and
the browser rounds rendered widths, so a "just fits" row can land a hair
over the container. With shrink enabled, fixed-height cards absorb that by
narrowing — cropping the cover via object-fit. Pin shrink to 0 on the
row's children: trades the rare sub-pixel crop for a hair of ragged
overflow, and keeps loading skeletons (default shrink: 1) at packed width.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The platform aspect_ratio setting is dropped from the UI and the API
(platform update body + response schema) — nothing consumed it for
rendering, and covers now size to their image's natural aspect.
- SettingsTab: remove the cover-style / aspect-ratio picker (and its
now-dead helpers, CSS, and unused imports); collapse to a single column.
- update_platform: drop the `aspect_ratio` body field; PlatformSchema no
longer returns it; utils/platforms stops seeding the default.
- Regenerate the affected frontend types (PlatformSchema, update body).
The DB column stays (out of the update/response scope; dropping it would
be a separate destructive migration) but is no longer read or written
through the API.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stop forcing a fixed box ratio on gallery covers. Cards now share one
height and vary in width to match each cover image's natural aspect, with
no cropping.
- GameCover measures the rendered image's natural ratio on load and drives
its own aspect-ratio from it (style ratio kept only as a pre-load seed);
emits the measured ratio.
- GameCard default (gallery) card: fixed art height, natural width; size
tiers and hero keep their fixed footprints. Forwards the ratio event.
- Gallery grid becomes flow-packed wrapping rows: useGalleryVirtualItems
greedily packs same-height / natural-width cards per row (ragged right),
measuring ratios client-side (cached by rom id, debounced re-pack). Row
height stays uniform so RVirtualScroller, AlphaStrip, scroll restoration
and grid-nav are unchanged.
- useResponsiveColumns additionally exposes usableWidth for width packing.
- Unit tests for packFlowRows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-user "remove from Continue Playing" option existed in v1's
AdminMenu but was missing in v2. The API, store action
(removeFromContinuePlaying) and data shape were already shared; only
the v2 UI layer and action handler were absent.
- useGameActions: add removeFromContinuePlaying() (clears last_played,
prunes the cached continue-playing list, snackbar feedback) plus a
canRemoveFromContinuePlaying gate (only when the ROM has last_played).
- GameActionsList: surface the action as an RMenuItem in the more-menu.
- locales: add snackbar-removed-from-playing and
snackbar-remove-from-playing-failed to all locales (translated for
es/de/fr/it/pt/ru, English placeholder elsewhere).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>