music (web): click the dock track info to open its album in /music
The bottom player dock's cover + title/artist is now a button — clicking it sets the music:cwd channel to the playing album and navigates to /music, landing on that album's tracklist (current track highlighted). Uses the global panel channel + react-router navigate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
|
import { usePanelChannel } from 'hooks/usePanelChannel';
|
||||||
import { Play, Pause, SkipBack, SkipForward, X, Volume2, VolumeX } from 'lucide-react';
|
import { Play, Pause, SkipBack, SkipForward, X, Volume2, VolumeX } from 'lucide-react';
|
||||||
import { useSeekBar, SeekBar } from '../apps/FileViewer/renderers/SeekBar';
|
import { useSeekBar, SeekBar } from '../apps/FileViewer/renderers/SeekBar';
|
||||||
import { MusicHeart } from '../apps/Music/MusicHeart';
|
import { MusicHeart } from '../apps/Music/MusicHeart';
|
||||||
import { sortTracks, trackHomePath, type AlbumMeta, type NowPlaying } from '../apps/Music/shared';
|
import { MUSIC_ROOT, MUSIC_CWD_CHANNEL, sortTracks, trackHomePath, type AlbumMeta, type NowPlaying } from '../apps/Music/shared';
|
||||||
import { useMusicPlayer, type PlayerTrack } from './useMusicPlayer';
|
import { useMusicPlayer, type PlayerTrack } from './useMusicPlayer';
|
||||||
|
|
||||||
// Mounted once in the persistent DashboardLayout (outside <Routes>), so it owns the single <audio>
|
// Mounted once in the persistent DashboardLayout (outside <Routes>), so it owns the single <audio>
|
||||||
@@ -15,6 +17,8 @@ const fmt = (s: number): string =>
|
|||||||
|
|
||||||
export const MusicPlayerHost = () => {
|
export const MusicPlayerHost = () => {
|
||||||
const { token, get, put } = useClient();
|
const { token, get, put } = useClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [, setCwd] = usePanelChannel<string | null>(MUSIC_CWD_CHANNEL, null);
|
||||||
const { current, index, queue, playing, toggle, next, prev, setPlaying, close, loadQueue } = useMusicPlayer();
|
const { current, index, queue, playing, toggle, next, prev, setPlaying, close, loadQueue } = useMusicPlayer();
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
const [position, setPosition] = useState(0);
|
const [position, setPosition] = useState(0);
|
||||||
@@ -128,6 +132,13 @@ export const MusicPlayerHost = () => {
|
|||||||
const subtitle = current?.artist ?? current?.albumRel.split('/').slice(-2, -1)[0] ?? '';
|
const subtitle = current?.artist ?? current?.albumRel.split('/').slice(-2, -1)[0] ?? '';
|
||||||
const pct = duration ? (position / duration) * 100 : 0;
|
const pct = duration ? (position / duration) * 100 : 0;
|
||||||
|
|
||||||
|
// Clicking the track info jumps the /music library to the playing album (and navigates there).
|
||||||
|
const openCurrentAlbum = () => {
|
||||||
|
if (!current) return;
|
||||||
|
setCwd(`${MUSIC_ROOT}/${current.albumRel}`);
|
||||||
|
navigate('/music');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<audio
|
<audio
|
||||||
@@ -148,21 +159,28 @@ export const MusicPlayerHost = () => {
|
|||||||
/>
|
/>
|
||||||
{current && (
|
{current && (
|
||||||
<div className="fixed inset-x-0 bottom-0 z-50 flex items-center gap-3 border-t border-border bg-card/95 px-4 py-2 backdrop-blur">
|
<div className="fixed inset-x-0 bottom-0 z-50 flex items-center gap-3 border-t border-border bg-card/95 px-4 py-2 backdrop-blur">
|
||||||
{/* cover + info */}
|
{/* cover + info — click to open this album in /music */}
|
||||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded bg-muted">
|
<button
|
||||||
<img
|
type="button"
|
||||||
src={coverUrl(current.albumRel)}
|
onClick={openCurrentAlbum}
|
||||||
alt=""
|
title="Show in library"
|
||||||
className="h-full w-full object-cover"
|
className="flex min-w-0 items-center gap-3 rounded text-left transition-opacity hover:opacity-80"
|
||||||
onError={(e) => {
|
>
|
||||||
(e.currentTarget as HTMLImageElement).style.visibility = 'hidden';
|
<div className="flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded bg-muted">
|
||||||
}}
|
<img
|
||||||
/>
|
src={coverUrl(current.albumRel)}
|
||||||
</div>
|
alt=""
|
||||||
<div className="hidden w-44 shrink-0 sm:block">
|
className="h-full w-full object-cover"
|
||||||
<p className="truncate text-sm font-medium text-foreground">{current.title ?? current.file}</p>
|
onError={(e) => {
|
||||||
<p className="truncate text-xs text-muted-foreground">{subtitle}</p>
|
(e.currentTarget as HTMLImageElement).style.visibility = 'hidden';
|
||||||
</div>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="hidden w-44 shrink-0 sm:block">
|
||||||
|
<p className="truncate text-sm font-medium text-foreground">{current.title ?? current.file}</p>
|
||||||
|
<p className="truncate text-xs text-muted-foreground">{subtitle}</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* transport */}
|
{/* transport */}
|
||||||
<div className="flex shrink-0 items-center gap-1">
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user