feedBack/plugins/folder_library
Byron Gamatos 8ef97708ef
perf(folder_library): render only the songs on screen — 1.3M DOM nodes -> ~30 (#965) (#967)
* 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).
2026-07-14 21:37:22 +02:00
..
tests perf(folder_library): render only the songs on screen — 1.3M DOM nodes -> ~30 (#965) (#967) 2026-07-14 21:37:22 +02:00
CLAUDE.md feat(folder_library): Folder Library core plugin (#610) 2026-06-27 16:03:54 +02:00
plugin.json feat(folder_library): Folder Library core plugin (#610) 2026-06-27 16:03:54 +02:00
README.md feat(folder_library): Folder Library core plugin (#610) 2026-06-27 16:03:54 +02:00
routes.py feat(folder_library): Folder Library core plugin (#610) 2026-06-27 16:03:54 +02:00
screen.html feat(folder_library): Folder Library core plugin (#610) 2026-06-27 16:03:54 +02:00
screen.js perf(folder_library): render only the songs on screen — 1.3M DOM nodes -> ~30 (#965) (#967) 2026-07-14 21:37:22 +02:00

Folder Library — FeedBack Plugin

Core plugin Platform

A FeedBack (fee[dB]ack) plugin that organizes your .sloppak / .feedpak DLC songs into a folder tree, grouped by the folders on disk. Browse your whole library visually with album art, nest folders as deep as you like, switch between list and grid layouts, and manage folders without ever leaving the app.


Screenshots

Grid view Grid view — album art cards with title and artist

Grid search Live search filters instantly across all folders

List view List view — compact rows with album art thumbnails and duration

New folder Create and manage folders directly in the UI


Status — migrating to core. Folder Library is being reworked from a standalone plugin into a bundled core plugin, and several previously-shipped features are not currently wired up in core (see the Roadmap). The list below reflects what works today; if something here is wrong, it's because this rework is still in progress.

Features

  • List & Grid views — toggle between a compact list with thumbnails or a full album art card grid
  • Album art — pulls art automatically for every song in both views
  • One-click playback — click any song to start playing immediately
  • Sort options — sort songs by title, artist, duration, year, tuning, or recently added with an asc/desc toggle
  • Advanced filters — filter by arrangements, stems, lyrics, and tuning with include and exclude support
  • Folder management — create, rename, and delete folders without leaving the plugin
  • Nested subfolders — organize as deep as you want; create a subfolder inside any folder, expand/collapse a whole branch in one click
  • Collapsible folders — expand/collapse individual folders, plus Expand All / Collapse All
  • Move songs — reassign any song to a different folder on the fly; press Esc to cancel
  • Drag-and-drop — drag songs between folders (including into nested folders) with smooth auto-scroll; press Esc to cancel
  • Fast with big libraries — folder song lists render lazily and metadata is cached so reopening folders is instant

Installation

Folder Library ships bundled with FeedBack as a core plugin ("bundled": true), so there's nothing to install — the Folders screen appears in the navbar under Plugins automatically.


Usage

Action How
Switch to grid view Click the grid icon in the toolbar
Switch to list view Click the list icon in the toolbar
Play a song Click any song row or card
Sort songs Use the sort dropdown in the toolbar
Toggle sort direction Click the arrow button next to the sort dropdown
Open filters Click the filter icon in the toolbar
Filter by arrangement/stem Open filters → click a pill to include; click to exclude
Clear all filters Open filters → click "Clear all"
Create a folder Click the folder+ icon in the toolbar
Create a subfolder Hover a folder header → click the new-subfolder icon
Rename a folder Hover the folder header → click the pencil icon
Delete a folder Hover the folder header → click the trash icon (songs move up to Unsorted)
Move a song Hover the song row → click the folder icon
Drag a song to a folder Click and hold a song → drag to a folder header or body (nested folders work too)
Cancel a drag Press Esc while holding a song
Cancel a move dialog Press Esc in the move prompt
Expand / collapse a folder Click the folder header
Expand / collapse all subfolders Use the expand/collapse-children buttons on a folder with subfolders

Changelog

Folder Library started life as a standalone plugin with its own version line, but it's now a bundled core plugin that ships with FeedBack. Its changes are tracked alongside the app in the repo-root CHANGELOG.md, and it versions with the app rather than on its own. The Features section above reflects what's in the current build.


Roadmap

  • Auto play song on hover (with an on/off toggle)
  • Bulk move — select multiple songs and move them at once
  • Thumbnail performance — faster loading and smoother scrolling with large song libraries
  • Adjustable thumbnail and row sizes — resize song cards and list rows to suit your preference
  • Custom themes — switch between colour schemes to match your style
  • Favoriting songs
  • Editing song metadata

Contributing

Pull requests are welcome. For major changes please open an issue first to discuss what you'd like to change.

  1. Fork the repo
  2. Create a feature branch (git checkout -b feature/your-feature)
  3. Commit your changes
  4. Push to the branch and open a pull request