import { useState, type ReactNode } from 'react'; import { Link, useNavigate } from 'react-router'; import { useClient } from 'hooks/useClient'; import { usePanelChannel } from 'hooks/usePanelChannel'; import { Heart, User, Disc3, Music, ChevronRight, X } from 'lucide-react'; import { useMusicPlayer, type PlayerTrack } from './useMusicPlayer'; import { MusicHeart } from './MusicHeart'; import { useMusicFavorites } from './useMusicFavorites'; import { MUSIC_FAV_CHANNEL, coverUrl, musicPath, parseAlbumName, sortTracks, toRel, type AlbumMeta, type FavoriteKind, } from './shared'; // The user's favorited artists / albums / tracks, grouped — shown in the right panel. Keys follow the // favorites convention: album/artist are music-relative ("Albums/…"), tracks are home paths // ("Music/…/file"). An album/artist row is a link into the library; a track row plays, so it stays a // button — it mutates rather than navigates, even though it also moves the library to the album. export const FavoritesView = () => { const { get, token } = useClient(); const navigate = useNavigate(); const player = useMusicPlayer(); const { favorites } = useMusicFavorites(); const [, setFavOpen] = usePanelChannel(MUSIC_FAV_CHANNEL, false); const playTrack = async (homePath: string) => { const cut = homePath.lastIndexOf('/'); const albumHome = homePath.slice(0, cut); const file = homePath.slice(cut + 1); const albumRel = toRel(albumHome); try { const meta = await get(`/music/meta?path=${encodeURIComponent(albumRel)}`); const q: PlayerTrack[] = sortTracks(meta.tracks).map((t) => ({ albumRel, file: t.file, title: t.title, artist: t.artist, })); player.playQueue( q, Math.max( 0, q.findIndex((t) => t.file === file), ), ); } catch { player.playQueue([{ albumRel, file }], 0); } navigate(musicPath(albumRel)); setFavOpen(false); }; const empty = !favorites.artists.length && !favorites.albums.length && !favorites.tracks.length; return (

Favorites

{empty ? (

No favorites yet. Click the heart on any artist, album or track.

) : (
{favorites.artists.map((key) => { const segs = key.split('/'); return ( } title={segs[segs.length - 1] ?? key} to={musicPath(key)} onClick={() => setFavOpen(false)} chevron /> ); })}
{favorites.albums.map((key) => { const segs = key.split('/'); const { title, year } = parseAlbumName(segs[segs.length - 1] ?? key); const artist = segs.length >= 3 ? segs[1] : ''; return ( } title={title} subtitle={[artist, year].filter(Boolean).join(' · ')} to={musicPath(key)} onClick={() => setFavOpen(false)} chevron /> ); })}
{favorites.tracks.map((key) => { const segs = key.split('/'); const base = segs[segs.length - 1] ?? key; const albumRel = toRel(key.slice(0, Math.max(0, key.lastIndexOf('/')))); const album = segs.length >= 2 ? parseAlbumName(segs[segs.length - 2]!).title : ''; return ( } title={base.replace(/\.[^/.]+$/, '')} subtitle={album} onClick={() => playTrack(key)} /> ); })}
)}
); }; const Section = ({ title, count, children }: { title: string; count: number; children: ReactNode }) => count ? (

{title} {count}

{children}
) : null; // `to` makes the row an anchor (an album or artist, which is a place); without it the row is a button // (a track, which plays). The heart stays a sibling either way — it must not be inside either one. const FavRow = ({ kind, favKey, cover, fallback, title, subtitle, to, onClick, chevron, }: { kind: FavoriteKind; favKey: string; cover: string; fallback: ReactNode; title: string; subtitle?: string; to?: string; onClick: () => void; chevron?: boolean; }) => { const [failed, setFailed] = useState(false); const inner = ( <>
{cover && !failed ? ( setFailed(true)} /> ) : ( fallback )}
{title} {subtitle ? {subtitle} : null}
); const cls = 'flex min-w-0 flex-1 cursor-pointer items-center gap-3 text-left'; return (
{to ? ( {inner} ) : ( )} {chevron ? : null}
); };