import React, { useCallback, useRef, useState } from 'react'; import { IcCheck, IcClose } from '../res/icons'; import { useSpotifyStore } from '../state'; import { useSpotifyGestures } from '../hooks/useSpotifyGestures'; import { useSpotifyStrings } from '../hooks/useSpotifyStrings'; const TIMER_OPTIONS = [5, 10, 15, 30, 45, 60] as const; const DISMISS_THRESHOLD = 80; export interface SleepTimerSheetProps { backdropProps?: React.HTMLAttributes; onClose: () => void; } /** * 睡眠定时器底部面板:播放器 overlay 与队列内嵌共用同一实现。 * 状态来自 `settings.playback`,与 store 单一数据源一致。 */ export const SleepTimerSheet: React.FC = ({ backdropProps, onClose }) => { const s = useSpotifyStrings(); const { bindTap } = useSpotifyGestures(); const sleepTimer = useSpotifyStore(st => st.settings.playback.sleepTimer); const sleepFade = useSpotifyStore(st => st.settings.playback.sleepFade); const updateSettings = useSpotifyStore(st => st.updateSettings); const dragStartY = useRef(0); const barRef = useRef(null); const [dragOffsetY, setDragOffsetY] = useState(0); const [isDragging, setIsDragging] = useState(false); const onPointerDown = useCallback((e: React.PointerEvent) => { dragStartY.current = e.clientY; setIsDragging(true); barRef.current?.setPointerCapture(e.pointerId); }, []); const onPointerMove = useCallback( (e: React.PointerEvent) => { if (!isDragging) return; const dy = Math.max(0, e.clientY - dragStartY.current); setDragOffsetY(dy); }, [isDragging], ); const onPointerUp = useCallback( (e: React.PointerEvent) => { if (barRef.current?.hasPointerCapture(e.pointerId)) { barRef.current.releasePointerCapture(e.pointerId); } if (!isDragging) return; setIsDragging(false); if (dragOffsetY >= DISMISS_THRESHOLD) { setDragOffsetY(0); onClose(); return; } setDragOffsetY(0); }, [dragOffsetY, isDragging, onClose], ); return ( <>
e.buttons === 0 && onPointerUp(e)} onPointerUp={onPointerUp} onPointerCancel={onPointerUp} >

{s.player_sleep_timer}

{TIMER_OPTIONS.map(min => (
{ updateSettings('playback', { sleepTimer: min }); onClose(); }, }, )} className="flex justify-between items-center py-3 active:bg-white/5 rounded px-2 cursor-pointer" > {min} {s.player_minutes} {sleepTimer === min && }
))}
{ updateSettings('playback', { sleepTimer: 0 }); onClose(); }, }, )} className="flex justify-between items-center py-3 active:bg-white/5 rounded px-2 cursor-pointer" > {s.player_close_timer} {sleepTimer === 0 && }
{s.player_fade_music}
{s.player_fade_music_desc}
{ updateSettings('playback', { sleepFade: !sleepFade }); }, }, )} className={`w-12 h-7 rounded-full relative transition-colors cursor-pointer ${sleepFade ? 'bg-app-primary' : 'bg-gray-600'}`} >
); };