import { buildCoverArtUrl } from '../api/subsonicStreamUrl'; import { getNowPlaying } from '../api/subsonicScrobble'; import type { SubsonicNowPlaying } from '../api/subsonicTypes'; import React, { useState, useEffect, useRef, useCallback, useLayoutEffect } from 'react'; import { createPortal } from 'react-dom'; import { PlayCircle, User, Radio, RefreshCw } from 'lucide-react'; import { useAuthStore } from '../store/authStore'; import { usePlayerStore } from '../store/playerStore'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; export default function NowPlayingDropdown() { const { t } = useTranslation(); const navigate = useNavigate(); const [isOpen, setIsOpen] = useState(false); const [nowPlaying, setNowPlaying] = useState([]); const isPlaying = usePlayerStore(s => s.isPlaying); const ownUsername = useAuthStore(s => s.getActiveServer()?.username ?? ''); const [loading, setLoading] = useState(false); const [spinning, setSpinning] = useState(false); const triggerWrapRef = useRef(null); const panelRef = useRef(null); const [panelPos, setPanelPos] = useState<{ top: number; left: number }>({ top: 0, left: 0 }); const PANEL_WIDTH = 340; const updatePanelPos = useCallback(() => { const el = triggerWrapRef.current; if (!el) return; const r = el.getBoundingClientRect(); const margin = 8; const top = r.bottom + 10; const maxLeft = window.innerWidth - PANEL_WIDTH - margin; const left = Math.max(margin, Math.min(r.right - PANEL_WIDTH, maxLeft)); setPanelPos({ top, left }); }, []); const fetchNowPlaying = async () => { setLoading(true); try { const data = await getNowPlaying(); setNowPlaying(data); } catch (e) { console.error('Failed to load Now Playing', e); } finally { setLoading(false); } }; const handleRefresh = () => { setSpinning(true); fetchNowPlaying().finally(() => { setTimeout(() => setSpinning(false), 600); }); }; // Poll only while the dropdown is open AND the page is visible. useEffect(() => { if (!isOpen) return; fetchNowPlaying(); const id = setInterval(() => { if (document.visibilityState === 'visible') fetchNowPlaying(); }, 10000); return () => clearInterval(id); }, [isOpen]); useLayoutEffect(() => { if (!isOpen) return; updatePanelPos(); const onWin = () => updatePanelPos(); window.addEventListener('resize', onWin); window.addEventListener('scroll', onWin, true); return () => { window.removeEventListener('resize', onWin); window.removeEventListener('scroll', onWin, true); }; }, [isOpen, updatePanelPos]); // Click outside to close useEffect(() => { function handleClickOutside(event: MouseEvent) { const target = event.target as Node; if (triggerWrapRef.current?.contains(target)) return; if (panelRef.current?.contains(target)) return; setIsOpen(false); } document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); // For the current user, trust the local player state — the server keeps stale // "now playing" entries for minutes after playback stops. const visible = nowPlaying.filter(entry => entry.username === ownUsername ? isPlaying : true ); return (
{isOpen && createPortal(

{t('nowPlaying.title')}

{loading && visible.length === 0 ? (
{t('nowPlaying.loading')}
) : visible.length === 0 ? (
{t('nowPlaying.nobody')}
) : (
{visible.map((stream, idx) => (
{ if (stream.albumId) { setIsOpen(false); navigate(`/album/${stream.albumId}`); } }} style={{ display: 'flex', gap: '0.75rem', alignItems: 'center', background: 'var(--bg-hover)', padding: '0.5rem', borderRadius: '8px', cursor: stream.albumId ? 'pointer' : 'default' }} >
{stream.coverArt ? ( Cover ) : ( )}
{stream.title}
{stream.artist}
{stream.username} ({stream.playerName || 'Web'}) {stream.minutesAgo > 0 && • {t('nowPlaying.minutesAgo', { n: stream.minutesAgo })}}
))}
)}
, document.body )}
); }