Folder Library now uses the standard data-fn/data-v3-play markup so the song_preview plugin handles hover-previews, the same way it does for grid and list views. Removes ~200 lines of preview-specific code: dedicated audio element, indicator rendering, toolbar toggle, and backend manifest checking. The preview experience is now unified across all library surfaces.
Signed-off-by: Kyle <kyle.j.t@live.co.uk>
Expose a has_preview boolean from pack manifests and use it to gate hover previews. backend: plugins/folder_library/routes.py now (optionally) reads sloppak.load_manifest to set m['has_preview'] (guarded so plugin still loads without sloppak). frontend: plugins/folder_library/screen.js skips preview requests when song.has_preview is false and removes the HEAD-probe + previewMissing cache. docs: plugins/folder_library/CLAUDE.md updated to document has_preview and the preview behavior. This prevents unnecessary HEAD/audio 404s and console noise.
Signed-off-by: Kyle <kyle.j.t@live.co.uk>
Remove audio member resolution from folder_library and delegate hover-preview audio to the canonical song_preview plugin endpoint. This separates concerns: folder_library now only builds the preview URL with the filename, while song_preview handles manifest resolution (preview: key, stem fallback) and Range support.
Changes:
- Remove _audio_member() resolver and audio_member field from pack metadata
- Update _previewUrl() to point to /api/plugins/song_preview/audio?file=<filename>
- Add _previewMissing cache to avoid re-requesting packs with no preview
- Add HEAD probe to check preview availability before playing
- Add test coverage for _previewUrl with special character encoding
Signed-off-by: Kyle <kyle.j.t@live.co.uk>
Make the hover-preview feature opt-out instead of opt-in (defaults to on). Increase dwell delay from 500ms to 800ms to prevent accidental triggers while clicking/dragging. Replace 4-bar equalizer indicator with 9-bar waveform with staggered animation and fade-in entrance. Add guards to prevent preview during drag operations. Update button styling and all user-facing docs to reflect new defaults.
Signed-off-by: Kyle <kyle.j.t@live.co.uk>
Move audio member selection logic from frontend to backend. The frontend was probing multiple candidate audio files with error/retry logic, often resulting in 404s. Now the backend resolves the best available audio file (preview.ogg, stems/full.ogg, or any stem) during metadata extraction and includes it in song metadata as `audio_member`. The frontend makes a single, guaranteed request instead of multiple probes. Simplifies the preview flow and improves reliability.
Signed-off-by: Kyle <kyle.j.t@live.co.uk>
Implement optional audio preview when hovering over songs in the Folders view. Features include:
- Toolbar toggle button (off by default) to enable/disable hover preview
- ~0.5s dwell delay to avoid accidental audio playback while scrolling
- Dedicated <audio> element (never touches main player)
- Equalizer indicator animation overlay on song art while playing
- Per-surface localStorage persistence
- Automatic fallback through preview.ogg → stems/full.ogg → stems/audio.mp3
Bump Folder Library from 1.8.0 to 1.9.0. Remove 'auto play on hover' from roadmap as it's now implemented.
Signed-off-by: Kyle <kyle.j.t@live.co.uk>
* perf(folder_library): render only the songs on screen (#965)
A song list rendered EVERY song it held. On a flat 50,944-song library that is
one <div> with 50,938 children and ~1,300,000 DOM nodes — ~4.2 GB of renderer
RSS, for a screen the user may not even be looking at (it was built while the
visible screen was v3-home).
It is not just this plugin's problem. A million-node document poisons unrelated
code: any `document.querySelector` that MISSES has to walk the whole tree before
returning null. That is exactly how song_preview's per-frame menu check ended up
consuming ~50% of the renderer and dropping the app to 2.7 fps
(feedBack-plugin-song-preview#7 fixes the per-frame walk; this fixes the tree it
was walking).
So render only what is on screen. Rows are uniform height (grid cards uniform
size), so the window is pure arithmetic — no per-row observers. Off-window songs
are represented by padding ON THE LIST rather than spacer elements: a spacer div
would become a grid ITEM in grid view and shift the columns, whereas padding
behaves identically in both layouts. Lists at or below VIRTUAL_MIN (200) render
in full exactly as before, so normal folders are untouched.
Two ordering fixes this forced, both real bugs waiting to happen:
- Both expand handlers populated the list BEFORE showing it. A windowed list
measures a real row and the scroller viewport, and both are zero under
display:none. Show first, then populate.
- _render() now tears down the previous render's scroll listeners. Without it
they survive against detached nodes and leak on every re-render.
Verified in real Chromium over CDP with 50,000 rows — the DOM glue, not just the
maths:
at top rendered= 25 rows scrollHeight=2,200,000px [0..24]
scroll 500k rendered= 31 rows scrollHeight=2,200,000px [11357..11387]
scroll 1,100k rendered= 31 rows scrollHeight=2,200,000px [24994..25024]
scroll to end rendered= 25 rows scrollHeight=2,200,000px [49975..49999]
25-31 rows in the DOM instead of 50,000; scroll height exact and constant (the
scrollbar stays honest); the last row lands on song 49,999.
Tests: _visibleWindow is pure and exposed via __test — top/middle/bottom/past-
the-end windows, the grid row-packing case, the padding-plus-rendered-equals-
total invariant that keeps the list from changing height as you scroll, and the
degenerate zero-height case (a list still display:none) falling back to
render-everything rather than to an empty list. eslint clean; full JS suite
1186/1186.
* fix(folder_library): re-window on resize and on show/hide (PR #967 review)
CodeRabbit caught two real bugs in the first pass. Both are mine.
1. GRID RESIZE. perRow and rows were captured once when the list was filled, but
paint() also runs on resize — and resizing changes the grid's column count.
The window maths then sliced against the OLD column count: wrong songs on
screen, and padding sized for a row count the layout no longer had (so the
scrollbar lied). metrics() now recomputes perRow/itemH/rows together on every
paint, so the geometry can never disagree with itself.
2. STALE WINDOWS ON SHOW/HIDE. paint() only ran on scroll and resize. Expanding
or collapsing any section moves every list below it, and a windowed list's
contents are a function of its POSITION — so those lists kept the window from
their old position and showed blank padding where songs should be until the
user happened to scroll. Both toggles now call _repaintVirtualLists().
Re-opening an already-populated section had the same flaw.
Collapsed lists also kept doing layout work on every scroll tick. paint() now
bails early when the list is display:none or detached, and forgets its last
window so re-showing repaints from scratch instead of short-circuiting on a
stale memo.
Tests: grid re-window on a column-count change, the padding+rendered=rows
invariant at two different perRow values, and a test that PINS THE FAILURE MODE —
a mismatched perRow/rows pair must not silently look correct. 12/12.
Re-validated the DOM glue in real Chromium with 50k rows (25-31 rows rendered,
scroll height exact). eslint clean; JS 1189/1189; pytest 2597 passed.
CHANGELOG entry added (also flagged).
Adds the bundled Folder Library plugin (browse the DLC library by its on-disk
folder tree, in-app folder CRUD, drag-and-drop + dialog song moves, sort/filter,
live search), wired into the classic v2 toolbar and the v3 Songs page.
Includes the screen.js IIFE dedup (unified surface factory) and review fixes:
path-traversal guard on /song/move, folder-delete data-loss fix, plural
/api/plugins/<id> namespace, loose-folder song recognition, error-text escaping,
v3 setLibView null-guard, and tests.
Co-authored-by: Kyle <kyle.j.t@live.co.uk>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>