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} + + )}
); };