From e46b28e14eee30a0baf0ae6d3cf55dbd7210d47a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 26 Jul 2026 13:05:30 +0000 Subject: [PATCH] music: left panel is a drill-down list navigator (never a grid) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MusicBrowser now lists a folder's children as single-column list items and drills via the shared channel (libraries → artists → albums). When the current path is an album leaf it lists the album's siblings and highlights it, so you can switch albums from the left while the right shows the tracklist. Handles the non-uniform library layouts via the manifest's tracks count. Co-Authored-By: Claude Opus 4.8 --- .../src/apps/Music/MusicBrowser.tsx | 114 ++++++++++++++---- 1 file changed, 92 insertions(+), 22 deletions(-) diff --git a/src/workspaces/officerdev/src/apps/Music/MusicBrowser.tsx b/src/workspaces/officerdev/src/apps/Music/MusicBrowser.tsx index 943b96b9..bbd1d2e7 100644 --- a/src/workspaces/officerdev/src/apps/Music/MusicBrowser.tsx +++ b/src/workspaces/officerdev/src/apps/Music/MusicBrowser.tsx @@ -1,26 +1,68 @@ import { useState, useEffect } from 'react'; import { useClient } from 'hooks/useClient'; import { usePanelChannel } from 'hooks/usePanelChannel'; -import { Library, Music2 } from 'lucide-react'; -import { MUSIC_ROOT, MUSIC_CWD_CHANNEL, type LsResult } from './shared'; +import { Library, Music2, ChevronLeft, Folder } from 'lucide-react'; +import { + MUSIC_ROOT, + MUSIC_CWD_CHANNEL, + toRel, + type LsResult, + type Manifest, + type ManifestAlbum, +} from './shared'; -// Left panel of the /music workspace — the library selector. Publishes the chosen path to the -// 'music:cwd' channel; MusicDetail (right panel) renders it. +// Left panel of the /music workspace — a single-column drill-down LIST navigator (libraries → +// artists → albums as list items; never a grid). Publishes the selected path to the 'music:cwd' +// channel; MusicDetail (right panel) renders the rich detail (covers/grids/tracklist). export const MusicBrowser = () => { const { get } = useClient(); const [cwd, setCwd] = usePanelChannel(MUSIC_CWD_CHANNEL, null); + const [manifest, setManifest] = useState>({}); const [libraries, setLibraries] = useState([]); + const [folders, setFolders] = useState([]); useEffect(() => { + get('/music/manifest') + .then((m) => setManifest(m.albums)) + .catch(() => setManifest({})); get(`/file-browser/ls?path=${encodeURIComponent(MUSIC_ROOT)}`) .then((r) => setLibraries(r.entries.filter((e) => e.type === 'directory').map((e) => e.name).sort())) .catch(() => setLibraries([])); }, []); - const currentLib = cwd ? cwd.split('/')[1] : null; + // The container folder whose children we list = the current folder, or its parent when the current + // path is an album leaf (so its siblings stay listed while the right shows the tracklist). + const rel = toRel(cwd); + const isAlbum = (manifest[rel]?.tracks ?? 0) > 0; + const navFolder = !cwd ? null : isAlbum ? cwd.split('/').slice(0, -1).join('/') : cwd; + const selected = cwd ? cwd.split('/').pop() : null; + + useEffect(() => { + if (!navFolder) { + setFolders([]); + return; + } + let cancelled = false; + get(`/file-browser/ls?path=${encodeURIComponent(navFolder)}`) + .then((r) => { + if (!cancelled) setFolders(r.entries.filter((e) => e.type === 'directory').map((e) => e.name).sort()); + }) + .catch(() => {}); + return () => { + cancelled = true; + }; + }, [navFolder]); + + const up = () => { + if (!navFolder) return; + const parts = navFolder.split('/'); + setCwd(parts.length <= 2 ? null : parts.slice(0, -1).join('/')); + }; + + const crumbs = navFolder ? navFolder.slice(MUSIC_ROOT.length + 1).split('/') : []; return ( -
+
-
- Libraries -
- {libraries.map((lib) => ( - - ))} - {!libraries.length && No libraries} + + {!navFolder ? ( + <> +
+ Libraries +
+ {libraries.map((lib) => ( + + ))} + {!libraries.length && No libraries} + + ) : ( + <> + + {folders.map((f) => ( + + ))} + {!folders.length && No subfolders} + + )}
); };