mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-13 04:09:26 +00:00
fix(v3): refresh Songs grid after a Settings rescan / DLC-folder change (#624)
Reported on macOS: on a fresh install, pointing at a DLC folder in Settings and running a scan showed NO songs until an app restart. The scan itself was fine — _background_scan re-reads config.json fresh, so it scans the new folder and populates the library — but the v3 Songs 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 initiate (only its own upload path self-refreshes via watchUploadScan). So its cached, pre-DLC (empty) DOM/snapshot survived a sidebar return until a full reload (restart). Fix: the rescan handlers now emit `library:changed` (static/app.js). The v3 grid listens and reloads if it's the active screen, else sets `_libraryDirty` so the next onV3SongsScreenEnter does a full re-fetch — a short-circuit placed ahead of every cached-DOM fast-path so it can't restore the stale grid. Tests: tests/js/v3_library_refresh.test.js guards the emit + the reload/dirty wiring (DOM/event glue isn't headlessly unit-testable; end-to-end wants an in-app run of the reporter's flow: set DLC in Settings → scan → Songs populate without restart). Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
byrongamatos
parent
d841813e0b
commit
b103a722ce
@@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- **v3 library: exact artist/album filters + scroll/page-depth restore** (feedBack#857). The v3 Songs toolbar gains Artist and Album dropdowns (Album populates from the selected artist and stays disabled until one is chosen), backed by new exact, case-insensitive (`COLLATE NOCASE`) `artist` / `album` query params threaded through `MetadataDB._build_where` → `query_page` / `query_artists` / `query_stats` and the `/api/library`, `/api/library/artists`, `/api/library/stats` endpoints (the free-text `q` search stays fuzzy and composes with the exact filters). The artist/album catalog is fetched independently of the active artist/album selection so the dropdowns always list the full set for the current provider/search. The toolbar is now sticky so filter controls stay reachable when browsing deep libraries, and returning from the player restores the previous scroll position **and** the loaded infinite-scroll page depth via a `sessionStorage` snapshot keyed by a filter/sort/view state hash (invalidated whenever those change, so a filter change still resets to the top). Tests: `tests/test_library_filters.py` (backend artist/album filters), `tests/js/v3_songs_scroll.test.js` (state-hash + snapshot helpers).
|
- **v3 library: exact artist/album filters + scroll/page-depth restore** (feedBack#857). The v3 Songs toolbar gains Artist and Album dropdowns (Album populates from the selected artist and stays disabled until one is chosen), backed by new exact, case-insensitive (`COLLATE NOCASE`) `artist` / `album` query params threaded through `MetadataDB._build_where` → `query_page` / `query_artists` / `query_stats` and the `/api/library`, `/api/library/artists`, `/api/library/stats` endpoints (the free-text `q` search stays fuzzy and composes with the exact filters). The artist/album catalog is fetched independently of the active artist/album selection so the dropdowns always list the full set for the current provider/search. The toolbar is now sticky so filter controls stay reachable when browsing deep libraries, and returning from the player restores the previous scroll position **and** the loaded infinite-scroll page depth via a `sessionStorage` snapshot keyed by a filter/sort/view state hash (invalidated whenever those change, so a filter change still resets to the top). Tests: `tests/test_library_filters.py` (backend artist/album filters), `tests/js/v3_songs_scroll.test.js` (state-hash + snapshot helpers).
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- **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/<f>/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: 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/<f>/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).
|
- **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).
|
||||||
- **Built-in diagnostic sloppak rebranded "Slopsmith" → "FeedBack" in the song name.** PR #586 renamed the file to `feedBack-diagnostic-basic-guitar.sloppak` but never regenerated the archive, so the manifest inside still carried `title: Slopsmith Diagnostic — Basic Guitar` / `artist: Slopsmith` (and the same heading in `DIAGNOSTIC.md`) — the stale name testers saw in the library/player and the onboarding calibration step, even though the build script, server, and docs all already say "FeedBack Diagnostic — Basic Guitar". Regenerated `docs/diagnostics/feedBack-diagnostic-basic-guitar.sloppak` from `docs/diagnostics/build_diagnostic_basic_guitar.py` so the committed artifact matches its source generator (title/artist/heading now "FeedBack"; chart, stem, and `diagnostic:` metadata unchanged). No code change — the rename in #586 just needed the rebuild.
|
- **Built-in diagnostic sloppak rebranded "Slopsmith" → "FeedBack" in the song name.** PR #586 renamed the file to `feedBack-diagnostic-basic-guitar.sloppak` but never regenerated the archive, so the manifest inside still carried `title: Slopsmith Diagnostic — Basic Guitar` / `artist: Slopsmith` (and the same heading in `DIAGNOSTIC.md`) — the stale name testers saw in the library/player and the onboarding calibration step, even though the build script, server, and docs all already say "FeedBack Diagnostic — Basic Guitar". Regenerated `docs/diagnostics/feedBack-diagnostic-basic-guitar.sloppak` from `docs/diagnostics/build_diagnostic_basic_guitar.py` so the committed artifact matches its source generator (title/artist/heading now "FeedBack"; chart, stem, and `diagnostic:` metadata unchanged). No code change — the rename in #586 just needed the rebuild.
|
||||||
|
|||||||
@@ -4543,6 +4543,9 @@ async function rescanLibrary() {
|
|||||||
_treeStats = null;
|
_treeStats = null;
|
||||||
_tuningNames = null; // re-fetch on next drawer open
|
_tuningNames = null; // re-fetch on next drawer open
|
||||||
loadLibrary();
|
loadLibrary();
|
||||||
|
// Tell the v3 Songs grid the library changed so it reloads instead of
|
||||||
|
// keeping a cached (e.g. pre-DLC, empty) grid until an app restart.
|
||||||
|
if (window.feedBack) window.feedBack.emit('library:changed', { reason: 'rescan' });
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
@@ -4571,6 +4574,9 @@ async function fullRescanLibrary() {
|
|||||||
_treeStats = null;
|
_treeStats = null;
|
||||||
_tuningNames = null; // re-fetch on next drawer open
|
_tuningNames = null; // re-fetch on next drawer open
|
||||||
loadLibrary();
|
loadLibrary();
|
||||||
|
// Tell the v3 Songs grid the library changed so it reloads instead of
|
||||||
|
// keeping a cached (e.g. pre-DLC, empty) grid until an app restart.
|
||||||
|
if (window.feedBack) window.feedBack.emit('library:changed', { reason: 'rescan' });
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -338,6 +338,12 @@
|
|||||||
// tracks filenames scored while the library was off-screen, applied on enter.
|
// tracks filenames scored while the library was off-screen, applied on enter.
|
||||||
const _dirtyScores = new Set();
|
const _dirtyScores = new Set();
|
||||||
|
|
||||||
|
// Set when a library scan / DLC-folder change happened while this screen was
|
||||||
|
// off (or showing a stale, e.g. pre-DLC empty, grid). The grid's cached DOM /
|
||||||
|
// snapshot would otherwise survive a sidebar return, so we force a full
|
||||||
|
// re-fetch on the next entry. (feedBack — "No DLC until restart".)
|
||||||
|
let _libraryDirty = false;
|
||||||
|
|
||||||
function repaintAccuracy(key) {
|
function repaintAccuracy(key) {
|
||||||
const apply = (el, variant) => {
|
const apply = (el, variant) => {
|
||||||
if (el.getAttribute('data-fn') !== key) return;
|
if (el.getAttribute('data-fn') !== key) return;
|
||||||
@@ -1049,6 +1055,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onV3SongsScreenEnter() {
|
async function onV3SongsScreenEnter() {
|
||||||
|
// A library scan / DLC-folder change marked the grid stale — re-fetch
|
||||||
|
// from scratch instead of restoring a cached (possibly empty, pre-DLC)
|
||||||
|
// snapshot. Must win over every fast-path below.
|
||||||
|
if (_libraryDirty) { _libraryDirty = false; await reload(); return; }
|
||||||
// Pull in any scores recorded while the library was off-screen (the usual
|
// Pull in any scores recorded while the library was off-screen (the usual
|
||||||
// play→return flow) before the fast-paths below restore the cached DOM,
|
// play→return flow) before the fast-paths below restore the cached DOM,
|
||||||
// so the just-played song's badge is current. The full render() path
|
// so the just-played song's badge is current. The full render() path
|
||||||
@@ -1202,5 +1212,16 @@
|
|||||||
const active = document.querySelector('.screen.active');
|
const active = document.querySelector('.screen.active');
|
||||||
if (active && active.id === 'v3-songs') applyScoreRefresh();
|
if (active && active.id === 'v3-songs') applyScoreRefresh();
|
||||||
});
|
});
|
||||||
|
// A library scan (rescan / full rescan from Settings, or a DLC-folder
|
||||||
|
// change) can add or remove songs while this grid is cached — the
|
||||||
|
// Settings rescan only refreshed the classic library, so the v3 grid
|
||||||
|
// stayed on its pre-scan (e.g. empty, pre-DLC) state until an app
|
||||||
|
// restart. Reload now if we're showing; otherwise mark dirty so the next
|
||||||
|
// entry re-fetches instead of restoring the stale snapshot.
|
||||||
|
sm.on('library:changed', () => {
|
||||||
|
const active = document.querySelector('.screen.active');
|
||||||
|
if (active && active.id === 'v3-songs') { _libraryDirty = false; reload(); }
|
||||||
|
else _libraryDirty = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Regression guard for "No DLC until restart": a library scan triggered from
|
||||||
|
// Settings (rescan / full rescan, e.g. right after pointing at a DLC folder)
|
||||||
|
// reloaded only the classic library — the v3 Songs grid kept its cached
|
||||||
|
// (pre-DLC, empty) state until an app restart.
|
||||||
|
//
|
||||||
|
// The fix wires a `library:changed` event (emitted by the rescan handlers in
|
||||||
|
// app.js) to a reload in static/v3/songs.js. That's DOM/event glue, not a pure
|
||||||
|
// function, so these are source-level guards that the wiring isn't dropped; the
|
||||||
|
// end-to-end behavior is verified in-app / by a browser test.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const root = path.join(__dirname, '..', '..');
|
||||||
|
const SONGS = fs.readFileSync(path.join(root, 'static', 'v3', 'songs.js'), 'utf8');
|
||||||
|
const APP = fs.readFileSync(path.join(root, 'static', 'app.js'), 'utf8');
|
||||||
|
|
||||||
|
test('app.js emits library:changed when a Settings rescan completes', () => {
|
||||||
|
assert.match(APP, /emit\(\s*['"]library:changed['"]/,
|
||||||
|
'a completed rescan must broadcast library:changed for the v3 grid');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('songs.js handles library:changed — reload when active, else mark dirty', () => {
|
||||||
|
const m = SONGS.match(/sm\.on\(\s*['"]library:changed['"][\s\S]{0,500}?\}\);/);
|
||||||
|
assert.ok(m, 'songs.js must subscribe to library:changed');
|
||||||
|
assert.match(m[0], /reload\(\)/, 'reloads the grid when the screen is active');
|
||||||
|
assert.match(m[0], /_libraryDirty\s*=\s*true/, 'marks dirty when off-screen');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('onV3SongsScreenEnter forces a reload when the library is dirty', () => {
|
||||||
|
const m = SONGS.match(/function onV3SongsScreenEnter\(\)[\s\S]{0,400}?\{/);
|
||||||
|
assert.ok(m, 'onV3SongsScreenEnter present');
|
||||||
|
// The dirty check must short-circuit to a reload before the cached-DOM
|
||||||
|
// fast-paths get a chance to restore the stale grid.
|
||||||
|
assert.match(SONGS, /if\s*\(_libraryDirty\)\s*\{[^}]*reload\(\)[^}]*return;/,
|
||||||
|
'a dirty library must force a full reload on entry, ahead of any fast-path');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user