import type { WidgetRegistryMeta, PlayerTrack } from 'officerdev'; import { useMusicPlayer } from 'officerdev'; import { useState, useEffect } from 'react'; import { Music, ChevronLeft, Play, Folder } from 'lucide-react'; import { useClient } from 'hooks/useClient'; import { Widget } from 'widgets/Widget'; // Music Player widget — a BROWSER over the library. Top-level dirs of ~/Music are "libraries" (tabs); // within a library you drill through folders (Artist → Albums → …) until a folder has tracks (songs). // Playback is owned by the app-wide player (useMusicPlayer): selecting a track hands it a queue. const MUSIC_ROOT = 'Music'; type DirEntry = { name: string; type: 'directory' | 'file'; size: number; modifiedAt: number }; type LsResult = { path: string; rootDir?: string; entries: DirEntry[] }; type Track = { file: string; title?: string; artist?: string }; type AlbumMeta = { path: string; cover?: string; tracks: Track[] }; const AUDIO_EXT = new Set(['mp3', 'flac', 'm4a', 'aac', 'ogg', 'opus', 'wav', 'wma', 'mp4']); const isAudio = (n: string) => { const d = n.lastIndexOf('.'); return d >= 0 && AUDIO_EXT.has(n.slice(d + 1).toLowerCase()); }; export const MusicPlayer = () => { const { token, get } = useClient(); // base '/api' const player = useMusicPlayer(); const [libraries, setLibraries] = useState([]); const [library, setLibrary] = useState(null); const [cwd, setCwd] = useState(MUSIC_ROOT); // home-relative path const [dirs, setDirs] = useState([]); const [songs, setSongs] = useState([]); const [loading, setLoading] = useState(false); // Top-level libraries (once). useEffect(() => { 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([])); }, []); // Current folder contents when cwd changes. useEffect(() => { if (!library) return; let cancelled = false; setLoading(true); setDirs([]); setSongs([]); get(`/file-browser/ls?path=${encodeURIComponent(cwd)}`) .then(async (r) => { if (cancelled) return; setDirs( r.entries .filter((e) => e.type === 'directory') .map((e) => e.name) .sort(), ); const audio = r.entries.filter((e) => e.type === 'file' && isAudio(e.name)).map((e) => e.name); if (audio.length) { const rel = cwd.slice(MUSIC_ROOT.length + 1); // /<…> try { const meta = await get(`/music/meta?path=${encodeURIComponent(rel)}`); if (!cancelled) setSongs(meta.tracks); } catch { if (!cancelled) setSongs(audio.sort().map((f) => ({ file: f }))); } } }) .catch(() => {}) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [cwd, library]); const relToMusic = cwd.slice(MUSIC_ROOT.length + 1); // '' at root, else /<…> const breadcrumb = relToMusic ? relToMusic.split('/') : []; const coverUrl = (rel: string) => `/api/music/cover?path=${encodeURIComponent(rel)}${token ? `&token=${encodeURIComponent(token)}` : ''}`; const selectLibrary = (lib: string) => { setLibrary(lib); setCwd(`${MUSIC_ROOT}/${lib}`); }; const enter = (name: string) => setCwd(`${cwd}/${name}`); const goUp = () => { const parts = cwd.split('/'); if (parts.length <= 2) { setLibrary(null); setCwd(MUSIC_ROOT); return; } setCwd(parts.slice(0, -1).join('/')); }; const play = (i: number) => { const queue: PlayerTrack[] = songs.map((t) => ({ albumRel: relToMusic, file: t.file, title: t.title, artist: t.artist, })); player.playQueue(queue, i); }; const isCurrent = (file: string) => player.current?.albumRel === relToMusic && player.current?.file === file; return ( {/* Library tabs */}
{libraries.map((lib) => ( ))} {!libraries.length && No libraries}
{!library ? (
Pick a library
) : (
{/* breadcrumb / back */} {/* album header (cover + play-all) when the folder has songs */} {songs.length > 0 && (
{ (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; }} />

{breadcrumb[breadcrumb.length - 1] ?? library}

{breadcrumb[breadcrumb.length - 2] ?? ''}

)} {/* folders + songs */}
{dirs.map((d) => ( ))} {songs.map((t, i) => ( ))} {loading && Loading…} {!loading && !dirs.length && !songs.length && ( Empty )}
)}
); }; export const widgetRegistryMetas: WidgetRegistryMeta[] = [ { key: 'music-player', name: 'Music Player', icon: Music, component: MusicPlayer }, ];