feat(locallibrary): Folders tab complete — flat toolbar + tree two-pane

Flat mode now has the full toolbar: search, sort (artist/title/year),
group-by (off/letter/artist) and a grid/list view toggle, driven by a
derive_folders() pass that mirrors favorites::derive_albums over the
directory-grouped album set (folders-visible / folders-grouped).

Tree mode is a new two-pane filesystem browser: a flattened FolderNode
list in the left rail (lazy one-level expand via list_folder_children,
collapse drops the descendant block, recursive track counts gate the
chevron) and a detail pane on the right showing the selected folder's
direct tracks plus immediate subfolders, with Play all (recursive) and
per-track play scoped to the folder's direct list. A flat/tree segmented
toggle switches modes; the tree loads its roots lazily on first switch.

Slint has no native TreeView, so the tree is a depth-indented ListView of
visible nodes spliced on expand/collapse. No pills (ADR-008): underline
tabs, small-radius rectangles and an accent selection bar.

cc
This commit is contained in:
vicrodh committed 2026-05-29 09:17:47 -06:00
1 parent c36b99b8e8
commit d676a2f5a4
5 files changed
+1056 -37

No files matched your search

+47 -1
View File
@@ -347,6 +347,18 @@ export struct LocalArtistItem {
subtitle: string,
}
// One visible row in the Folders tree (flattened-model lazy tree — Slint has
// no native TreeView). Folders expand to fetch children lazily.
export struct FolderNode {
path: string, // absolute filesystem path (the node id)
segment: string, // last path component (display)
depth: int, // indent level
is-folder: bool, // folder vs. track row
expanded: bool, // folder open?
can-expand: bool, // has children (track_count_under > 0)
track-count: int, // recursive track count under a folder
}
export global LocalLibraryState {
// "albums" | "artists" | "folders" | "tracks"
in-out property <string> active-tab: "albums";
@@ -388,10 +400,29 @@ export global LocalLibraryState {
in-out property <int> tracks-next-offset: 0;
in-out property <string> tracks-search: "";
// --- Folders tab (flat) — albums grouped by directory (full load) ---
// --- Folders tab ---------------------------------------------------
// flat = album grid grouped by directory; tree = two-pane lazy filesystem.
in-out property <string> folders-view-mode: "flat"; // "flat" | "tree"
// Flat mode: `folders` is the full set (artwork target); `folders-visible`
// / `folders-grouped` are the search/sort/group-derived render sets.
in property <[AlbumCardItem]> folders: [];
in property <bool> folders-loading: false;
in property <bool> folders-load-failed: false;
in property <[AlbumCardItem]> folders-visible: [];
in property <[DiscoverSection]> folders-grouped: [];
in-out property <string> folders-search: "";
in-out property <string> folders-sort: "title-asc"; // title-asc|title-desc|artist-asc|year-desc
in-out property <string> folders-group: "off"; // off | alpha | artist
in-out property <string> folders-grid-view: "grid"; // grid | list
// Tree mode: a flattened visible-node list + the selected folder's detail
// (its direct tracks; subfolders shown as a grid).
in property <[FolderNode]> folder-tree: [];
in property <bool> folder-tree-loading: false;
in-out property <string> folders-selected-path: "";
in property <string> folders-selected-name: "";
in property <[FolderNode]> folder-detail-subfolders: [];
in property <[TrackItem]> folder-detail-tracks: [];
in property <bool> folder-detail-loading: false;
// --- Artists tab — name + counts grid (full load; images later) -----
in property <[LocalArtistItem]> artists: [];
@@ -429,6 +460,21 @@ export global LocalLibraryActions {
callback tracks-retry();
// Per-row play/queue — local playback lands with the source-aware slice.
callback track-action(string /* track id */, string /* action */);
// --- Folders tab ---
callback folders-search(string /* query */);
callback folders-set-sort(string /* sort key */);
callback folders-set-group(string /* off|alpha|artist */);
callback folders-set-mode(string /* flat|tree */);
// Tree: expand/collapse a folder node (lazy child fetch on expand).
callback folders-toggle-node(string /* path */, bool /* now-expanded */);
// Tree: select a folder -> load its detail (direct tracks + subfolders).
callback folders-select(string /* path */, string /* segment */);
// Tree: play everything under a folder (recursive).
callback folders-play-node(string /* path */);
// Tree: play one track from the selected folder's direct tracks (the
// folder's direct list becomes the queue, starting at this track).
callback folders-play-track(string /* track id */);
}
// Global toast state, driven by crate::toast. The Toast overlay (mounted