mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
import { memo, useMemo, useSyncExternalStore } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { X } from 'lucide-react';
|
|
import { usePlayerStore } from '@/store/playerStore';
|
|
import type { Track } from '@/store/playerStoreTypes';
|
|
import { resolveQueueTrack } from '@/utils/library/queueTrackView';
|
|
import {
|
|
getQueueResolverVersion,
|
|
subscribeQueueResolver,
|
|
} from '@/utils/library/queueTrackResolver';
|
|
import { formatTrackTime } from '@/utils/format/formatDuration';
|
|
|
|
interface Props {
|
|
onClose: () => void;
|
|
}
|
|
|
|
/**
|
|
* Semi-transparent "Up next" overlay for the fullscreen player — lists the
|
|
* upcoming queue (no blur, in keeping with the static player). Clicking a row
|
|
* jumps to that queue item (same-queue jump as the queue panel).
|
|
*/
|
|
export const FsQueueModal = memo(function FsQueueModal({ onClose }: Props) {
|
|
const { t } = useTranslation();
|
|
const queueItems = usePlayerStore(s => s.queueItems);
|
|
const queueIndex = usePlayerStore(s => s.queueIndex);
|
|
const playTrack = usePlayerStore(s => s.playTrack);
|
|
// Re-resolve as the resolver cache fills.
|
|
const version = useSyncExternalStore(subscribeQueueResolver, getQueueResolverVersion);
|
|
|
|
const upcoming = useMemo(() => {
|
|
const out: { track: Track; absIdx: number }[] = [];
|
|
for (let i = queueIndex + 1; i < queueItems.length; i++) {
|
|
const ref = queueItems[i];
|
|
if (ref) out.push({ track: resolveQueueTrack(ref), absIdx: i });
|
|
}
|
|
return out;
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [queueItems, queueIndex, version]);
|
|
|
|
return (
|
|
<div
|
|
className="fsq-backdrop"
|
|
onClick={onClose}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={t('player.fsUpNext')}
|
|
>
|
|
<div className="fsq-panel" onClick={e => e.stopPropagation()}>
|
|
<div className="fsq-header">
|
|
<span className="fsq-title">{t('player.fsUpNext')}</span>
|
|
<button className="fsq-close" onClick={onClose} aria-label={t('common.close')}>
|
|
<X size={18} />
|
|
</button>
|
|
</div>
|
|
<div className="fsq-list">
|
|
{upcoming.length === 0 ? (
|
|
<div className="fsq-empty">{t('player.fsQueueEmpty')}</div>
|
|
) : (
|
|
upcoming.map(({ track, absIdx }) => (
|
|
<button
|
|
key={`${track.id}:${absIdx}`}
|
|
className="fsq-item"
|
|
onClick={() => {
|
|
playTrack(track, undefined, undefined, undefined, absIdx);
|
|
onClose();
|
|
}}
|
|
>
|
|
<span className="fsq-item-pos">{absIdx + 1}</span>
|
|
<span className="fsq-item-info">
|
|
<span className="fsq-item-title">{track.title}</span>
|
|
<span className="fsq-item-artist">{track.artist}</span>
|
|
</span>
|
|
<span className="fsq-item-dur">{formatTrackTime(track.duration ?? 0)}</span>
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|