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>
Refresh stale doc comments that didn't keep pace with the refactor:
- `global.css` r-v2-asset-fade — list AssetList / AssetStrip / FileRow
/ ScreenshotsTab / SoundtrackPanel as the consumers (was: just
AssetList / AssetStrip).
- `SaveDataTab.vue` section-head comment — drop the "subtitle" mention
(subtitle was removed in this PR).
- `FilesTab.vue` file header — section header now hosts Upload only;
Download-all / Copy-link moved to the selection toolbar (select-all
then act). Updated the file-level summary and the content-column
bullet list to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move every upload affordance out of the sidebar's collapsible action
panel into a section header at the top of each panel, matching
ScreenshotsSubtab. Sidebars become navigation-only.
Touched subtabs:
- SaveDataTab (Saves + States) — Upload per Mine section header.
- MediaTab Manual — header hosts the entry selector + Upload only;
Delete + Redownload move into the PDF viewer's toolbar via new
PdfViewer `deletable` / `redownloadable` props.
- MediaTab Soundtrack — header with just Upload.
- MediaTab — reorder subtabs so Screenshots sits above Soundtrack.
- FilesTab — header with Upload only (Download all / Copy link
dropped; equivalent via select-all + selection toolbar).
- ScreenshotsSubtab + SaveDataTab — drop the redundant "Private to
you until you make them public" subtitle and i18n keys.
Add a distinct entrance animation for dense, data-row content:
`r-v2-asset-fade` (fade + 14px rise, no scale, no overshoot, 320ms,
24ms stagger). Applied to AssetList / AssetStrip rows, FileRow,
screenshot thumbnails and soundtrack track rows so they cascade in
on first paint, separate from the bouncier `r-v2-card-fade` reserved
for hero cards.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* i18n `rom.cant-toggle-visibility` is interpolated with `{error}` in
SaveDataTab but the strings dropped the placeholder, hiding the
backend error detail from the snackbar. Add `{error}` to every locale
(en_US source + es_ES translated, others English placeholder).
* `RSliderBtnGroup` badge visibility check did not handle the string
`"0"` / `""` cases allowed by the `string | number | null` type.
Extract a `showBadge` helper and use it in both the router-link and
RBtn branches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Notes already paints the lock button primary when private and muted when
public; bring screenshots and the new saves/states toggles to the same
binding so the three visibility surfaces read consistently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend:
- Resolve the acting user from the authenticated socket session on
connect instead of trusting the client-supplied user_id, so a client
can no longer spoof a "now playing" session for another user. Only
rom_id/device_id come from the payload.
- Emit activity:update/clear through the already-initialised socket
server instead of opening (and leaking) a fresh AsyncRedisManager per
REST heartbeat.
- Collapse get_all_active's per-key GET into a single MGET.
- Drop the pure pass-through _build_activity_entry helper.
Frontend:
- Remove all activity emits from the v1 EmulatorJS Player; the v2 shell
is the single driver of the activity lifecycle.
- Remove activity from the v1 UI entirely (Activity view, ActivityBtn,
ActivePlayers on game details, navigation, and the now-v2-only route).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `scan:scanning_rom` socket emit strips `sibling_roms` from the rom
payload, so a freshly-scanned rom can reach SiblingBadge with the field
undefined (despite the schema typing it as required). Reading
`.length`/`.map` then threw `TypeError: Cannot read properties of
undefined (reading 'length')`, crashing the gallery during a scan while
"Group ROMs" was enabled.
Default `sibling_roms` to an empty list (a `siblings` computed). REST
payloads always include it (possibly empty), so only the socket path was
affected.
Closes#3567
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Introduced AssetList.vue for displaying a vertical list of saves with detailed information.
- Added AssetStrip.vue for a horizontal strip of state tiles, supporting selection and management modes.
- Created stories for AssetStrip to demonstrate various states and scenarios.
- Enhanced RSliderBtnGroup to include a badge for displaying counts next to labels.
- Updated EmulatorJS.vue to utilize the new AssetList and AssetStrip components, including badge counts for saves and states.
- Introduced new API endpoints for updating visibility of saves and states.
- Added `is_public` property to `SaveSchema` and `StateSchema`.
- Created new models for user saves and states with visibility attributes.
- Updated the `SaveDataTab` component to differentiate between "Mine" and "Community" sections.
- Implemented visibility toggle functionality for user saves and states.
- Enhanced localization files to include new strings for visibility actions.
- device/init returns a relative verification_path; the client joins it
with its own origin
- Render the v2 approval screen via the named v2 router outlet (was blank)
- DevicePair: RSpinner, keyboard-accessible scope chips, scrollable scopes;
DevicePairShell uses always-light overlay tokens (no hex)
Add activity.json translations for every non-en_US locale and add the
matching `activity` key (added with the activity feature but left
English-only) to each common.json, so check_i18n_locales.py passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Lazy-load DebugOverlay via defineAsyncComponent so its chunk (and the
vueuse perf hooks it pulls in) stays out of the default bundle until the
developer toggle is enabled.
- Add useDebugMode unit tests mirroring useCrtMode: default-off, persisted
restore, write-through, and singleton sharing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- main.py: await the cancelled log-forwarder task (suppressing
CancelledError) so its pubsub/lock cleanup finishes before shutdown.
- forwarder: only heartbeat the Redis lock while we still own it; if a
stall let another worker take it, relinquish forwarding to avoid
duplicate lines (the outer loop re-contends).
- endpoints/logs.py: derive MAX_LOG_LIMIT from LOG_BUFFER_SIZE so the
REST backfill never drifts from the producer's ring buffer.
- Logs.vue: append the download <a> to the DOM before click() (matches
the Patcher pattern) for cross-browser reliability.
- Add tests/endpoints/test_logs.py: non-admin 403, limit clamping to
[1, MAX_LOG_LIMIT], oldest-first ordering, and malformed-entry skip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Introduced a new logs view for admin users, allowing real-time monitoring of backend logs.
- Implemented a log entry streaming mechanism using Socket.IO.
- Added filtering and searching capabilities for log entries.
- Created localized log messages in Spanish, French, Hungarian, Italian, Japanese, Korean, Polish, Portuguese, Romanian, Russian, Simplified Chinese, and Traditional Chinese.
- Updated router and sidebar components to include the new logs route.
- Enhanced user interface with tooltips and buttons for copying and downloading logs.