import React, { useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { X, Moon, Sunrise } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { usePlayerStore } from '../store/playerStore'; import { useAuthStore } from '../store/authStore'; import { useShallow } from 'zustand/react/shallow'; import type { TFunction } from 'i18next'; import { formatPlaybackScheduleRemaining } from '../utils/format/playbackScheduleFormat'; import { formatClockTime } from '../utils/format/formatClockTime'; import { isValidPlaybackSchedulePreviewTimestamp, parsePlaybackDelayCustomMinutes, scheduleDelayMsFromSeconds, scheduleSecondsFromCustomMinutes, } from '../utils/playback/playbackScheduleDelay'; /** One tap = schedule; custom minutes still covers any duration. */ const PRESET_SECONDS = [30, 60, 120, 300, 600, 900, 1800, 3600] as const; function formatPresetLabel(seconds: number, t: TFunction): string { if (seconds < 60) return t('player.delayFmtSec', { n: seconds }); if (seconds < 3600) return t('player.delayFmtMin', { n: seconds / 60 }); return t('player.delayFmtHr', { n: seconds / 3600 }); } function computeAnchoredPanelStyle(anchorEl: HTMLElement): React.CSSProperties { const ar = anchorEl.getBoundingClientRect(); const mw = Math.min(360, Math.max(200, window.innerWidth - 32)); let left = ar.left + ar.width / 2 - mw / 2; const pad = 12; left = Math.max(pad, Math.min(left, window.innerWidth - mw - pad)); const gap = 10; return { position: 'fixed', left, bottom: window.innerHeight - ar.top + gap, width: mw, maxWidth: 360, margin: 0, maxHeight: 'min(72vh, calc(100vh - 24px))', overflowY: 'auto', }; } export interface PlaybackDelayModalProps { open: boolean; onClose: () => void; /** When set, panel is fixed just above this element (transport strip). */ anchorRef?: React.RefObject; } export default function PlaybackDelayModal({ open, onClose, anchorRef }: PlaybackDelayModalProps) { const { t, i18n } = useTranslation(); const { isPlaying, currentTrack, currentRadio, scheduledPauseAtMs, scheduledResumeAtMs, schedulePauseIn, scheduleResumeIn, clearScheduledPause, clearScheduledResume, } = usePlayerStore( useShallow(s => ({ isPlaying: s.isPlaying, currentTrack: s.currentTrack, currentRadio: s.currentRadio, scheduledPauseAtMs: s.scheduledPauseAtMs, scheduledResumeAtMs: s.scheduledResumeAtMs, schedulePauseIn: s.schedulePauseIn, scheduleResumeIn: s.scheduleResumeIn, clearScheduledPause: s.clearScheduledPause, clearScheduledResume: s.clearScheduledResume, })), ); const [nowTick, setNowTick] = useState(() => Date.now()); const [posTick, setPosTick] = useState(0); const [customMinutes, setCustomMinutes] = useState(''); /** Preset-seconds the user is currently hovering — drives the live "Pauses at HH:MM" preview. */ const [hoverSeconds, setHoverSeconds] = useState(null); const customInputRef = useRef(null); useEffect(() => { if (!open) return; setCustomMinutes(''); setHoverSeconds(null); }, [open]); // While modal is open, refresh the "now" tick every second so the live // preview clock stays accurate. useEffect(() => { if (!open) return; const id = window.setInterval(() => setNowTick(Date.now()), 1000); return () => window.clearInterval(id); }, [open]); useEffect(() => { if (!open) return; if (scheduledPauseAtMs == null && scheduledResumeAtMs == null) return; const id = window.setInterval(() => setNowTick(Date.now()), 500); return () => window.clearInterval(id); }, [open, scheduledPauseAtMs, scheduledResumeAtMs]); useEffect(() => { if (!open || !anchorRef) return; const bump = () => setPosTick(x => x + 1); window.addEventListener('resize', bump); window.addEventListener('scroll', bump, true); return () => { window.removeEventListener('resize', bump); window.removeEventListener('scroll', bump, true); }; }, [open, anchorRef]); useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [open, onClose]); const canPauseLater = isPlaying && (!!currentTrack || !!currentRadio); const canStartLater = !isPlaying && (!!currentTrack || !!currentRadio); const customSeconds = useMemo(() => { const minutes = parsePlaybackDelayCustomMinutes(customMinutes); if (minutes == null) return null; return scheduleSecondsFromCustomMinutes(minutes); }, [customMinutes]); const applyPause = (sec: number) => { schedulePauseIn(sec); onClose(); }; const applyStart = (sec: number) => { scheduleResumeIn(sec); onClose(); }; const useAnchor = !!anchorRef; const anchorEl = anchorRef?.current ?? null; void posTick; const anchoredPanelStyle = open && useAnchor && anchorEl ? computeAnchoredPanelStyle(anchorEl) : undefined; const heading = canPauseLater ? t('player.delayPauseSection') : canStartLater ? t('player.delayStartSection') : t('player.delayModalTitle'); // Mode determines icon + colour accent ("mood") of the modal. const mode: 'pause' | 'start' | 'idle' = canPauseLater ? 'pause' : canStartLater ? 'start' : 'idle'; const HeadingIcon = mode === 'pause' ? Moon : mode === 'start' ? Sunrise : null; // Live preview: seconds that would be applied right now if the user clicked. // Priority: hovered chip → typed custom minutes → nothing. const clockFormat = useAuthStore(s => s.clockFormat); const previewSeconds = hoverSeconds ?? customSeconds; const previewAtMs = previewSeconds != null ? nowTick + scheduleDelayMsFromSeconds(previewSeconds) : null; const previewClock = previewAtMs != null && isValidPlaybackSchedulePreviewTimestamp(previewAtMs) ? formatClockTime(previewAtMs, clockFormat, i18n.language) : null; if (!open) return null; const defaultPanelStyle: React.CSSProperties = { maxWidth: 360, width: 'min(360px, calc(100vw - 32px))' }; const panelStyle = anchoredPanelStyle ? { ...defaultPanelStyle, ...anchoredPanelStyle } : defaultPanelStyle; const scheduledAt = canPauseLater ? scheduledPauseAtMs : canStartLater ? scheduledResumeAtMs : null; const clearScheduled = canPauseLater ? clearScheduledPause : canStartLater ? clearScheduledResume : null; const cancelLabel = canPauseLater ? t('player.delayCancelPause') : t('player.delayCancelStart'); const apply = canPauseLater ? applyPause : applyStart; return createPortal(
e.stopPropagation()} style={panelStyle} >
{HeadingIcon && ( )}

{heading}

{(canPauseLater || canStartLater) && ( <> {scheduledAt != null && (
{t('player.delayIn')} {formatPlaybackScheduleRemaining(scheduledAt, nowTick)} {clearScheduled && ( )}
)}
setHoverSeconds(null)} > {PRESET_SECONDS.map(sec => ( ))}
setCustomMinutes(e.target.value)} aria-label={t('player.delayCustomMinutes')} />
{previewClock != null && ( <> {canPauseLater ? t('player.delayPreviewPause') : t('player.delayPreviewStart')} {previewClock} )}
)} {!canPauseLater && !canStartLater && (

{t('player.delayInactivePause')}

{t('player.delayInactiveStart')}

)}
, document.body, ); }