diff --git a/src/workspaces/officerdev/src/apps/Music/MusicBrowser.tsx b/src/workspaces/officerdev/src/apps/Music/MusicBrowser.tsx index bbd1d2e7..da506926 100644 --- a/src/workspaces/officerdev/src/apps/Music/MusicBrowser.tsx +++ b/src/workspaces/officerdev/src/apps/Music/MusicBrowser.tsx @@ -1,21 +1,42 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, type ReactNode } from 'react'; import { useClient } from 'hooks/useClient'; import { usePanelChannel } from 'hooks/usePanelChannel'; import { Library, Music2, ChevronLeft, Folder } from 'lucide-react'; import { MUSIC_ROOT, MUSIC_CWD_CHANNEL, + coverUrl, toRel, type LsResult, type Manifest, type ManifestAlbum, } from './shared'; +// A row's leading thumbnail: the folder's indexed cover (its folder.jpg/cover.jpg, server-compressed), +// falling back to an icon when it has none or the image fails to load. +const RowThumb = ({ src, fallback }: { src: string | null; fallback: ReactNode }) => { + const [failed, setFailed] = useState(false); + useEffect(() => setFailed(false), [src]); + return ( +
+ {src && !failed ? ( + setFailed(true)} /> + ) : ( + fallback + )} +
+ ); +}; + +// Hidden files/folders (dotfiles like .claude, .git) never belong in the library listing. +const visibleDirs = (r: LsResult) => + r.entries.filter((e) => e.type === 'directory' && !e.name.startsWith('.')).map((e) => e.name).sort(); + // 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 { get, token } = useClient(); const [cwd, setCwd] = usePanelChannel(MUSIC_CWD_CHANNEL, null); const [manifest, setManifest] = useState>({}); const [libraries, setLibraries] = useState([]); @@ -26,7 +47,7 @@ export const MusicBrowser = () => { .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())) + .then((r) => setLibraries(visibleDirs(r))) .catch(() => setLibraries([])); }, []); @@ -45,7 +66,7 @@ export const MusicBrowser = () => { 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()); + if (!cancelled) setFolders(visibleDirs(r)); }) .catch(() => {}); return () => { @@ -60,6 +81,7 @@ export const MusicBrowser = () => { }; const crumbs = navFolder ? navFolder.slice(MUSIC_ROOT.length + 1).split('/') : []; + const coverFor = (childRel: string) => (manifest[childRel]?.cover ? coverUrl(childRel, token) : null); return (
@@ -68,8 +90,8 @@ export const MusicBrowser = () => { onClick={() => setCwd(null)} className="flex items-center gap-2 px-2 pb-2 text-left text-foreground" > - - Music + + Music {!navFolder ? ( @@ -77,17 +99,20 @@ export const MusicBrowser = () => {
Libraries
- {libraries.map((lib) => ( - - ))} - {!libraries.length && No libraries} +
+ {libraries.map((lib) => ( + + ))} + {!libraries.length && No libraries} +
) : ( <> @@ -99,20 +124,22 @@ export const MusicBrowser = () => { {crumbs.join(' / ')} - {folders.map((f) => ( - - ))} - {!folders.length && No subfolders} +
+ {folders.map((f) => ( + + ))} + {!folders.length && No subfolders} +
)}
diff --git a/src/workspaces/officerdev/src/apps/Music/MusicDetail.tsx b/src/workspaces/officerdev/src/apps/Music/MusicDetail.tsx index 75dd1a36..1a760950 100644 --- a/src/workspaces/officerdev/src/apps/Music/MusicDetail.tsx +++ b/src/workspaces/officerdev/src/apps/Music/MusicDetail.tsx @@ -58,7 +58,7 @@ export const MusicDetail = () => { .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())) + .then((r) => setLibraries(r.entries.filter((e) => e.type === 'directory' && !e.name.startsWith('.')).map((e) => e.name).sort())) .catch(() => setLibraries([])); }, []); @@ -80,7 +80,7 @@ export const MusicDetail = () => { get(`/file-browser/ls?path=${encodeURIComponent(cwd)}`) .then(async (r) => { if (cancelled) return; - setFolders(r.entries.filter((e) => e.type === 'directory').map((e) => e.name).sort()); + setFolders(r.entries.filter((e) => e.type === 'directory' && !e.name.startsWith('.')).map((e) => e.name).sort()); const audio = r.entries.filter((e) => e.type === 'file' && isAudio(e.name)).map((e) => e.name); if (audio.length) { try {