the music widget moves into the plugin, parked like cliamp

src/workspaces/widgets/MusicPlayer/ → plugins/music/widgets/, its export
dropped from the widgets package, and its registration removed from
WidgetRegistry. Plugins cannot contribute widgets and that mechanism is not
being invented now, so it is parked next to cliamp rather than left in a
workspace package the platform ships.

`../Widget` became `widgets/Widget` — the sibling import turned into that
package's declared export, which it already had.

This removes the last non-plugin consumer of officerdev/src/MusicPlayer, and
that matters more than the move. The argument for keeping the player in the
platform was that the widget imported useMusicPlayer and PlayerTrack from
officerdev, and the platform cannot import from a plugin — so the player STATE
had to stay whatever was decided about the UI, and the engine stayed with the
state.

That constraint is now gone. The complete remaining platform dependency on the
player is one line: DashboardLayout.tsx:66, `<MusicPlayerHost />`.

So the seam is no longer "the widget pins it". It is purely the global overlay
question, which is a platform gap about plugins owning a render slot outside
<Routes> — the same shape as plugins owning a websocket. Recorded rather than
acted on; the decision is the owner's and the previous reasoning for it no
longer holds.

bunx tsgo clean. 797 tests, 787 pass, same 7 pre-existing failures.
This commit is contained in:
2026-08-15 14:33:44 +00:00
parent 9af52fd754
commit f1bd75853d
3 changed files with 29 additions and 17 deletions
@@ -3,7 +3,7 @@ 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 '../Widget';
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).
@@ -36,7 +36,14 @@ export const MusicPlayer = () => {
// Top-level libraries (once).
useEffect(() => {
get<LsResult>(`/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')
.map((e) => e.name)
.sort(),
),
)
.catch(() => setLibraries([]));
}, []);
@@ -50,7 +57,12 @@ export const MusicPlayer = () => {
get<LsResult>(`/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());
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); // <library>/<…>
@@ -92,7 +104,12 @@ export const MusicPlayer = () => {
};
const play = (i: number) => {
const queue: PlayerTrack[] = songs.map((t) => ({ albumRel: relToMusic, file: t.file, title: t.title, artist: t.artist }));
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;
@@ -107,7 +124,9 @@ export const MusicPlayer = () => {
type="button"
onClick={() => selectLibrary(lib)}
className={`shrink-0 rounded-full px-2.5 py-1 text-xs ${
library === lib ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground hover:text-foreground'
library === lib
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:text-foreground'
}`}
>
{lib}
@@ -144,7 +163,9 @@ export const MusicPlayer = () => {
/>
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-semibold text-foreground">{breadcrumb[breadcrumb.length - 1] ?? library}</p>
<p className="truncate text-sm font-semibold text-foreground">
{breadcrumb[breadcrumb.length - 1] ?? library}
</p>
<p className="truncate text-xs text-muted-foreground">{breadcrumb[breadcrumb.length - 2] ?? ''}</p>
</div>
<button
@@ -3,20 +3,12 @@ import { widgetRegistryMetas as weatherMetas } from 'widgets/Weather';
import { widgetRegistryMetas as pomodoroMetas } from 'widgets/Pomodoro';
import { widgetRegistryMetas as dailyGoalsMetas } from 'widgets/DailyGoals';
import { widgetRegistryMetas as quickNotesMetas } from 'widgets/QuickNotes';
import { widgetRegistryMetas as musicPlayerMetas } from 'widgets/MusicPlayer';
import type { QueryClient } from '@tanstack/react-query';
import { globalQueryKey } from 'hooks/useGlobal';
import type { WidgetRegistry as WidgetRegistryMap } from './types';
import type { WidgetRegistryMeta } from './useWidgetRegistry';
const widgets = [
...clockMetas,
...weatherMetas,
...pomodoroMetas,
...dailyGoalsMetas,
...quickNotesMetas,
...musicPlayerMetas,
];
const widgets = [...clockMetas, ...weatherMetas, ...pomodoroMetas, ...dailyGoalsMetas, ...quickNotesMetas];
const metasToRegistry = (metas: WidgetRegistryMeta[]): WidgetRegistryMap =>
Object.fromEntries(metas.map(({ key, ...entry }) => [key, entry]));
+1 -2
View File
@@ -8,7 +8,6 @@
"./Pomodoro": "./Pomodoro/index.tsx",
"./DailyGoals": "./DailyGoals/index.tsx",
"./QuickNotes": "./QuickNotes/index.tsx",
"./Workspaces": "./Workspaces/index.tsx",
"./MusicPlayer": "./MusicPlayer/index.tsx"
"./Workspaces": "./Workspaces/index.tsx"
}
}