From 3308e4e24d30b73742fe42c49b147c1aacc57740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 26 Jul 2026 10:29:14 +0000 Subject: [PATCH] music: Music Player widget + app-wide player dock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The widget browses the /api/music/* library (search albums → tracklist) and hands a queue to an app-wide player. The player (useMusicPlayer, useGlobal-backed) and its site-wide bottom dock (MusicPlayerHost) live in the persistent DashboardLayout, so playback survives route changes. Dock has cover/title/artist, drag-scrubbing (SeekBar), volume (persisted), and prev/play/next/close. The nav Dock slides up by MUSIC_DOCK_HEIGHT while the music dock is present. Co-Authored-By: Claude Opus 4.8 --- .../Dashboard/Layout/DashboardLayout.tsx | 3 +- .../Screens/Dashboard/Layout/Dock.tsx | 8 +- .../src/MusicPlayer/MusicPlayerHost.tsx | 163 ++++++++++++++++++ .../officerdev/src/MusicPlayer/index.ts | 2 + .../src/MusicPlayer/useMusicPlayer.ts | 41 +++++ .../src/WidgetRegistry/WidgetRegistry.tsx | 10 +- src/workspaces/officerdev/src/index.ts | 1 + src/workspaces/widgets/MusicPlayer/index.tsx | 158 +++++++++++++++++ src/workspaces/widgets/package.json | 3 +- 9 files changed, 385 insertions(+), 4 deletions(-) create mode 100644 src/workspaces/officerdev/src/MusicPlayer/MusicPlayerHost.tsx create mode 100644 src/workspaces/officerdev/src/MusicPlayer/index.ts create mode 100644 src/workspaces/officerdev/src/MusicPlayer/useMusicPlayer.ts create mode 100644 src/workspaces/widgets/MusicPlayer/index.tsx diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/DashboardLayout.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/DashboardLayout.tsx index fd631df7..7d59a627 100644 --- a/src/apps/officer-web/Screens/Dashboard/Layout/DashboardLayout.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Layout/DashboardLayout.tsx @@ -1,4 +1,4 @@ -import { useDock } from 'officerdev'; +import { useDock, MusicPlayerHost } from 'officerdev'; import { Background } from './Background'; import { Header } from './Header'; import { Dock, ALL_DOCK_ITEMS, DEFAULT_DOCK_PATHS } from './Dock'; @@ -23,6 +23,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) { {children} + ); }; diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx index 87a2fe2e..8fee719e 100644 --- a/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from 'react'; import { Link, useLocation } from 'react-router'; import type { LucideIcon } from 'lucide-react'; +import { useMusicPlayer, MUSIC_DOCK_HEIGHT } from 'officerdev'; export type DockItem = { label: string; @@ -34,6 +35,8 @@ export const Dock = ({ items, className }: DockProps) => { const [visible, setVisible] = useState(false); const dockRef = useRef(null); const location = useLocation(); + const { current } = useMusicPlayer(); + const musicDockPresent = !!current; const isActive = (to: string) => (to === '/' ? location.pathname === '/' : location.pathname.startsWith(to)); @@ -64,7 +67,10 @@ export const Dock = ({ items, className }: DockProps) => { backgroundColor: 'var(--dock-bg)', borderColor: 'var(--dock-border)', bottom: '16px', - transform: `translateX(-50%) translateY(${visible ? '0' : 'calc(100% + 24px)'})`, + // Slide up over the site-wide music dock when it's present so the nav dock clears it. + transform: `translateX(-50%) translateY(${ + visible ? (musicDockPresent ? `-${MUSIC_DOCK_HEIGHT}px` : '0') : 'calc(100% + 24px)' + })`, }} > {items.map((item, index) => { diff --git a/src/workspaces/officerdev/src/MusicPlayer/MusicPlayerHost.tsx b/src/workspaces/officerdev/src/MusicPlayer/MusicPlayerHost.tsx new file mode 100644 index 00000000..f463a80d --- /dev/null +++ b/src/workspaces/officerdev/src/MusicPlayer/MusicPlayerHost.tsx @@ -0,0 +1,163 @@ +import { useEffect, useRef, useState } from 'react'; +import { useClient } from 'hooks/useClient'; +import { Play, Pause, SkipBack, SkipForward, X, Volume2, VolumeX } from 'lucide-react'; +import { useSeekBar, SeekBar } from '../apps/FileViewer/renderers/SeekBar'; +import { useMusicPlayer, type PlayerTrack } from './useMusicPlayer'; + +// Mounted once in the persistent DashboardLayout (outside ), so it owns the single