ac6b76bf98
"Start radio" in the track menu seeds the queue from a track; RadioController
(headless, in AppShell) tops it up via /radio/next as it drains. QueuePanel's
radio scaffolding is now live: the exploration slider drives familiar<->new and
each entry shows its reason ("why this is playing"). Adds the radio endpoints
(start/next/similar), radio context + actions on the queue slice, and the pick->
entry mapper. Degrades to metadata picks while ML is offline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
47 lines
1.3 KiB
TypeScript
47 lines
1.3 KiB
TypeScript
import { Outlet } from 'react-router';
|
|
import { Suspense } from 'react';
|
|
import { Sidebar } from './Sidebar';
|
|
import { PersistentPlayer } from '../player/PersistentPlayer';
|
|
import { NowPlaying } from '../player/NowPlaying';
|
|
import { QueuePanel } from '../player/QueuePanel';
|
|
import { RadioController } from '../player/RadioController';
|
|
import { TrackInfoDrawer } from '../track/TrackInfoDrawer';
|
|
import { AddToPlaylistDialog } from '../track/AddToPlaylistDialog';
|
|
import { LoadingSkeleton } from '../common/LoadingSkeleton';
|
|
|
|
export function AppShell() {
|
|
return (
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
height: '100vh',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
<div className="app-body">
|
|
<Sidebar />
|
|
<main className="app-main">
|
|
<div className="app-screen">
|
|
<Suspense
|
|
fallback={
|
|
<div style={{ padding: '2rem' }}>
|
|
<LoadingSkeleton />
|
|
</div>
|
|
}
|
|
>
|
|
<Outlet />
|
|
</Suspense>
|
|
</div>
|
|
</main>
|
|
<QueuePanel />
|
|
<TrackInfoDrawer />
|
|
</div>
|
|
<AddToPlaylistDialog />
|
|
<PersistentPlayer />
|
|
<NowPlaying />
|
|
<RadioController />
|
|
</div>
|
|
);
|
|
}
|