From 43a4372ba37e9494c2711bd14bcb7beef7b80ae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 28 Jul 2026 13:16:11 +0000 Subject: [PATCH] web /music: reindex now refreshes BOTH panels in place MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MusicBrowser and MusicDetail panels each kept their OWN local `manifest` (and folder-listing) state, fetched in their own effects. The reindex (↻) button lives in MusicBrowser and only refreshed its own copy — MusicDetail (the right panel showing the tracklist/grid) never heard about it, so newly-indexed content only appeared after navigating (which re-ran its effects). Add a shared `music:resync` panel channel: when a reindex completes, MusicBrowser bumps it to a fresh nonce, and both panels re-run their manifest/libraries/folder fetches. In MusicDetail the fresh manifest object identity also re-triggers the [cwd, manifest] listing effect, so the open album's meta/tracklist and any folder grid refresh in place — no navigation required. Drops the browser's now-redundant hand-refresh of its own state. Co-Authored-By: Claude Opus 4.8 --- .../src/apps/Music/MusicBrowser.tsx | 45 +++++++++++----- .../officerdev/src/apps/Music/MusicDetail.tsx | 53 ++++++++++++++++--- .../officerdev/src/apps/Music/shared.ts | 12 ++++- 3 files changed, 87 insertions(+), 23 deletions(-) diff --git a/src/workspaces/officerdev/src/apps/Music/MusicBrowser.tsx b/src/workspaces/officerdev/src/apps/Music/MusicBrowser.tsx index 05a372d7..ecfdf683 100644 --- a/src/workspaces/officerdev/src/apps/Music/MusicBrowser.tsx +++ b/src/workspaces/officerdev/src/apps/Music/MusicBrowser.tsx @@ -6,6 +6,7 @@ import { MUSIC_ROOT, MUSIC_CWD_CHANNEL, MUSIC_FAV_CHANNEL, + MUSIC_RESYNC_CHANNEL, coverUrl, fuzzyMatch, toRel, @@ -32,7 +33,10 @@ const RowThumb = ({ src, fallback }: { src: string | null; fallback: ReactNode } // 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(); + 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' @@ -41,12 +45,14 @@ export const MusicBrowser = () => { const { get, post, token } = useClient(); const [cwd, setCwd] = usePanelChannel(MUSIC_CWD_CHANNEL, null); const [favOpen, setFavOpen] = usePanelChannel(MUSIC_FAV_CHANNEL, false); + const [resync, setResync] = usePanelChannel(MUSIC_RESYNC_CHANNEL, 0); const [manifest, setManifest] = useState>({}); const [libraries, setLibraries] = useState([]); const [folders, setFolders] = useState([]); const [query, setQuery] = useState(''); const [reindexing, setReindexing] = useState(false); + // Refetch manifest + libraries on mount and whenever a reindex bumps the resync nonce. useEffect(() => { get('/music/manifest') .then((m) => setManifest(m.albums)) @@ -54,7 +60,7 @@ export const MusicBrowser = () => { get(`/file-browser/ls?path=${encodeURIComponent(MUSIC_ROOT)}`) .then((r) => setLibraries(visibleDirs(r))) .catch(() => setLibraries([])); - }, []); + }, [resync]); // 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). @@ -77,22 +83,19 @@ export const MusicBrowser = () => { return () => { cancelled = true; }; - }, [navFolder]); + }, [navFolder, resync]); // Start each folder unfiltered. useEffect(() => setQuery(''), [navFolder]); - // Trigger a server-side library rebuild, then refresh the manifest + current listing. + // Trigger a server-side library rebuild, then bump the resync nonce so BOTH panels refetch their + // manifest / listings / meta (a fresh Date.now() value guarantees the effects re-run). const reindex = async () => { if (reindexing) return; setReindexing(true); try { await post('/music/reindex'); - const m = await get('/music/manifest').catch(() => null); - if (m) setManifest(m.albums); - const target = navFolder ?? MUSIC_ROOT; - const r = await get(`/file-browser/ls?path=${encodeURIComponent(target)}`).catch(() => null); - if (r) navFolder ? setFolders(visibleDirs(r)) : setLibraries(visibleDirs(r)); + setResync(Date.now()); } finally { setReindexing(false); } @@ -129,7 +132,10 @@ export const MusicBrowser = () => { title="Favorites" className="flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-md hover:bg-muted" > - + @@ -143,7 +149,11 @@ export const MusicBrowser = () => { className="min-w-0 flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none" /> {query && ( - )} @@ -198,15 +208,22 @@ export const MusicBrowser = () => { type="button" onClick={() => setCwd(`${navFolder}/${f}`)} className={`flex items-center gap-3 truncate rounded-md px-2 py-2 text-left text-base ${ - selected === f ? 'bg-muted text-foreground' : 'text-muted-foreground hover:bg-muted/60 hover:text-foreground' + selected === f + ? 'bg-muted text-foreground' + : 'text-muted-foreground hover:bg-muted/60 hover:text-foreground' }`} > - } /> + } + /> {f} ))} {!shownFolders.length && ( - {query ? 'No matches' : 'No subfolders'} + + {query ? 'No matches' : 'No subfolders'} + )} diff --git a/src/workspaces/officerdev/src/apps/Music/MusicDetail.tsx b/src/workspaces/officerdev/src/apps/Music/MusicDetail.tsx index 901800f6..2e21f17b 100644 --- a/src/workspaces/officerdev/src/apps/Music/MusicDetail.tsx +++ b/src/workspaces/officerdev/src/apps/Music/MusicDetail.tsx @@ -10,6 +10,7 @@ import { MUSIC_ROOT, MUSIC_CWD_CHANNEL, MUSIC_FAV_CHANNEL, + MUSIC_RESYNC_CHANNEL, TYPE_ORDER, coverUrl, fmtDuration, @@ -33,6 +34,7 @@ export const MusicDetail = () => { const player = useMusicPlayer(); const [cwd, setCwd] = usePanelChannel(MUSIC_CWD_CHANNEL, null); const [favOpen, setFavOpen] = usePanelChannel(MUSIC_FAV_CHANNEL, false); + const [resync] = usePanelChannel(MUSIC_RESYNC_CHANNEL, 0); const [manifest, setManifest] = useState>({}); const [libraries, setLibraries] = useState([]); @@ -63,14 +65,24 @@ export const MusicDetail = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [cwd]); + // Refetch manifest + libraries on mount and whenever a reindex bumps the resync nonce. A fresh manifest + // object identity also re-runs the [cwd, manifest] listing effect below, refreshing the folder grid / + // album meta for whatever is currently open — so the right panel updates in place, no nav required. 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' && !e.name.startsWith('.')).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([])); - }, []); + }, [resync]); const rel = toRel(cwd); const childRel = (name: string) => (rel ? `${rel}/${name}` : name); @@ -90,7 +102,12 @@ export const MusicDetail = () => { get(`/file-browser/ls?path=${encodeURIComponent(cwd)}`) .then(async (r) => { if (cancelled) return; - setFolders(r.entries.filter((e) => e.type === 'directory' && !e.name.startsWith('.')).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 { @@ -128,7 +145,12 @@ export const MusicDetail = () => { const playAlbum = async (albumRel: string, startIndex = 0) => { try { const meta = await get(`/music/meta?path=${encodeURIComponent(albumRel)}`); - const queue: PlayerTrack[] = sortTracks(meta.tracks).map((t) => ({ albumRel, file: t.file, title: t.title, artist: t.artist })); + const queue: PlayerTrack[] = sortTracks(meta.tracks).map((t) => ({ + albumRel, + file: t.file, + title: t.title, + artist: t.artist, + })); player.playQueue(queue, startIndex); } catch { /* ignore */ @@ -139,7 +161,8 @@ export const MusicDetail = () => { const queue: PlayerTrack[] = album.map((t) => ({ albumRel: rel, file: t.file, title: t.title, artist: t.artist })); player.playQueue(queue, i); }; - const isCurrent = (albumRel: string, file: string) => player.current?.albumRel === albumRel && player.current?.file === file; + const isCurrent = (albumRel: string, file: string) => + player.current?.albumRel === albumRel && player.current?.file === file; const crumbs = rel ? rel.split('/') : []; @@ -173,7 +196,13 @@ export const MusicDetail = () => { )} {playable && ( - + )} ); @@ -273,7 +302,13 @@ export const MusicDetail = () => { {dur && {dur}} - + ); })} @@ -290,7 +325,9 @@ export const MusicDetail = () => { {TYPE_ORDER.filter((type) => folders.some((f) => (disco.albums[f] ?? 'Other') === type)).map((type) => (
-

{type === 'Studio' ? 'Studio Albums' : type}

+

+ {type === 'Studio' ? 'Studio Albums' : type} +

{folders .filter((f) => (disco.albums[f] ?? 'Other') === type) diff --git a/src/workspaces/officerdev/src/apps/Music/shared.ts b/src/workspaces/officerdev/src/apps/Music/shared.ts index 4340e4c3..2195d6be 100644 --- a/src/workspaces/officerdev/src/apps/Music/shared.ts +++ b/src/workspaces/officerdev/src/apps/Music/shared.ts @@ -4,6 +4,9 @@ export const MUSIC_ROOT = 'Music'; export const MUSIC_CWD_CHANNEL = 'music:cwd'; export const MUSIC_FAV_CHANNEL = 'music:favorites'; +// Bumped (to a fresh nonce) when a library reindex finishes, so BOTH panels re-run their manifest / +// listing / meta fetches — otherwise only the panel that triggered the reindex refreshes. +export const MUSIC_RESYNC_CHANNEL = 'music:resync'; // Album folders are named "[year] Album Name" → display as "Album Name" + year. const ALBUM_NAME_RE = /^\[(\d{4})\]\s*(.+)$/; @@ -16,7 +19,14 @@ export type DirEntry = { name: string; type: 'directory' | 'file'; size: number; export type LsResult = { entries: DirEntry[] }; export type ManifestAlbum = { v: string; cover: boolean; tracks: number; disco?: boolean }; export type Manifest = { albums: Record }; -export type Track = { file: string; title?: string; artist?: string; albumArtist?: string; track?: string; durationSec?: number }; +export type Track = { + file: string; + title?: string; + artist?: string; + albumArtist?: string; + track?: string; + durationSec?: number; +}; export type AlbumMeta = { path: string; cover?: string; tracks: Track[] }; /** Seconds → "m:ss" (or "h:mm:ss" once past an hour); '' when unknown. */