diff --git a/CHANGELOG.md b/CHANGELOG.md index ed349aa..22fdcb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - **Input-setup wizard no longer collapses an audio device's driver-type variants into one entry.** On Windows the desktop engine enumerates the same interface once per host API (ASIO / Windows Audio / DirectSound), and the wizard's audio picker (`plugins/input_setup/screen.js`) de-duped the source list by display **label** — so the variants (which share a name) collapsed to a single choice, silently keeping whichever sorted first (often *not* the low-latency ASIO one the player wants). The audio-input capability already collapses true duplicates by `logicalSourceKey` (`_visibleInputSources` in `static/capabilities/audio-session.js`), and the variants each have a **distinct** key, so the wizard's extra label-collapse was redundant for real dupes and destructive for these — it also could drop the variant that was actually `selected`. Removed it; the picker now lists every selectable input. Pairs with feedBack-desktop's change to label each source with its driver type (e.g. "Focusrite (ASIO)") so the now-distinct entries are legible. +- **3D Highway FPS counter no longer hides behind the v3 "Up Next" pill.** The on-highway FPS readout (Settings → Graphics → 3D Highway → Show FPS counter) is pinned to the top-right of the highway overlay — the same corner the v3 player chrome stacks its persistent **Up Next** pill and live-performance HUD into, on a higher layer that paints over the canvas. So the readout sat *behind* that chrome and couldn't be read — precisely when a tester had turned it on to judge performance (it also made the separate "Up Next won't turn off" complaint worse, since the default-on pill covered the counter regardless). The counter now stays top-right but drops just **below** whichever of that chrome is showing: `highway_3d`'s `screen.js` measures the lowest visible top-right v3 HUD element (`#v3-upnext` / `#v3-live-performance-hud` / `#hud-time`) and floors the FPS box's Y beneath it. Element refs are resolved once and cached (no per-frame `querySelector`, per the plugin perf rules) and only consulted while the counter is actually drawn; gated on `window.feedBack.uiVersion === 'v3'` so the classic (v2) UI is byte-for-byte unaffected. `plugins/highway_3d/plugin.json` version → `3.30.1` (cache-buster). (For reading raw perf numbers unobstructed, the core perf HUD — `localStorage.highwayPerfHud='1'` — still renders above all chrome and additionally shows the adaptive render-scale.) - **v3 Songs grid now refreshes after a Settings rescan / DLC-folder change — no app restart needed.** On a fresh install, pointing at a DLC folder in Settings and running a scan left the Songs section empty until a restart (the scan *did* populate the library — `_background_scan` re-reads `config.json` fresh — but the v3 grid never reloaded). The Settings **Rescan / Full Rescan** handlers only refreshed the classic (v2) library via `loadLibrary()`; the v3 grid (`static/v3/songs.js`) had no listener for a scan it didn't start itself (only its own upload path self-refreshed via `watchUploadScan`), so its cached, pre-DLC (empty) DOM/snapshot survived a sidebar return until a full reload. The rescan handlers now emit a **`library:changed`** event (`static/app.js`); the v3 grid listens and **reloads if it's the active screen, else marks itself dirty** so the next entry does a full re-fetch instead of restoring the stale snapshot (a `_libraryDirty` short-circuit ahead of every cached-DOM fast-path in `onV3SongsScreenEnter`). Tests: `tests/js/v3_library_refresh.test.js` (the emit + the reload/dirty wiring). - **Edit Metadata modal: the Year is now editable.** You could set a year when authoring a pak but the Songs → Edit Metadata modal had no Year field, so it could never be changed afterward. The backend (`POST /api/song//meta`) already accepted and normalized `year` (writes it into the file via `songmeta`, survives a rescan) — only the UI omitted it. Added a **Year** input to `openEditModal()` (populated from the song's existing year) and included `year` in `saveEditModal()`'s POST body (`static/app.js`). Both the v3 card menu and the legacy edit button already pass the year through, so both surfaces get the field. - **Edit Metadata modal no longer closes when a click-drag is released on the backdrop.** Selecting text inside a field and releasing the mouse past the modal's edge dismissed the form without warning (the `click` event's target resolved to the backdrop), discarding the edit. Backdrop dismissal now requires the **mousedown to have started on the backdrop** too — tracked per-modal and decided by a new pure `_editModalShouldClose(clickTarget, modalEl, downOnBackdrop)` helper (`static/app.js`). Cancel / ✕ still close on a normal click. Tests: `tests/js/edit_metadata_modal.test.js` (year in the POST body + the backdrop-close decision table). diff --git a/plugins/highway_3d/plugin.json b/plugins/highway_3d/plugin.json index f2005bc..13b88f5 100644 --- a/plugins/highway_3d/plugin.json +++ b/plugins/highway_3d/plugin.json @@ -1,7 +1,7 @@ { "id": "highway_3d", "name": "3D Highway", - "version": "3.30.0", + "version": "3.30.1", "type": "visualization", "bundled": true, "script": "screen.js", diff --git a/plugins/highway_3d/screen.js b/plugins/highway_3d/screen.js index b0a8101..fb3109a 100644 --- a/plugins/highway_3d/screen.js +++ b/plugins/highway_3d/screen.js @@ -3325,6 +3325,42 @@ let _fpsEma = 0; let _fpsDisplay = 0; let _fpsLastSampleT = 0; + // The FPS readout is pinned top-right of the highway overlay — the same + // corner the v3 player chrome stacks its persistent "Up Next" pill and + // live-performance HUD into, on a higher layer that paints over the + // canvas. So out of the box the readout sits *behind* that chrome and + // can't be read (exactly when you've turned it on to judge perf). Rather + // than relocate it (testers look top-right), we drop it just BELOW + // whichever of that chrome is showing. Refs are resolved once and cached + // — never a per-frame querySelector (see CLAUDE.md "never run DOM queries + // on a per-frame path") — and re-resolved only when a node detaches. + let _v3HudEls = null; + // Returns the bottom edge (in overlay-canvas px, which are 1:1 CSS px on + // this overlay) of the lowest visible top-right v3 chrome element, or 0 + // when none apply (classic v2 UI, or all hidden). Only called while the + // FPS readout is actually drawn, so the layout reads cost nothing in the + // common (counter-off) case. + function _v3TopRightChromeBottom() { + if (typeof document === 'undefined' || !highwayCanvas) return 0; + // Only the v3 chrome stacks persistent HUD elements over the canvas's + // top-right. Gate on the documented detector so this is a strict no-op + // in classic v2 (where 'hud-time' also exists but sits elsewhere). + if (!(window.feedBack && window.feedBack.uiVersion === 'v3')) return 0; + if (!_v3HudEls || _v3HudEls.some((el) => el && !el.isConnected)) { + _v3HudEls = ['v3-upnext', 'v3-live-performance-hud', 'hud-time'] + .map((id) => document.getElementById(id)); + } + const top = highwayCanvas.getBoundingClientRect().top; + let maxBottom = 0; + for (const el of _v3HudEls) { + // offsetParent === null ⇒ display:none (a `.hidden` pill/HUD) or + // not laid out — don't duck under something that isn't shown. + if (!el || el.offsetParent === null) continue; + const b = el.getBoundingClientRect().bottom - top; + if (b > maxBottom) maxBottom = b; + } + return maxBottom; + } let _diagChord = null; // Chord diagram render cache. Keys: static layout inputs joined as a // string. Values: OffscreenCanvas (or ) rendered at opacity=1 @@ -14557,7 +14593,13 @@ const _fpsBoxW = Math.ceil(_fpsMetrics.width) + _fpsPadX * 2; const _fpsBoxH = 14 + _fpsPadY * 2; const _fpsE = 8; - const _fpsBaseY = Math.round(Math.max(_fpsE + H * 0.06, lyricsBottom + _fpsE)); + // Keep it top-right but below the v3 Up Next pill / live HUD + // (whichever is showing) so the readout is never occluded. + const _fpsBaseY = Math.round(Math.max( + _fpsE + H * 0.06, + lyricsBottom + _fpsE, + _v3TopRightChromeBottom() + _fpsE, + )); const _fpsX = W - 8 - _fpsBoxW; const _fpsY = _fpsBaseY + cornerStack['tr']; lyricsCtx.fillStyle = 'rgba(0,0,0,0.55)';