import React, { useRef } from 'react'; import { useLocale } from '@/os/locale'; import { IcAdd, IcHeart, IcAddCircle, IcLikedIndicator } from '../res/icons'; import { useSpotifyStore, selectLikedSongIds } from '../state'; import type { SpotifyPlaylist, SpotifyTrack } from '../types'; import { useSpotifyGestures } from '../hooks/useSpotifyGestures'; import { useSpotifyStrings } from '../hooks/useSpotifyStrings'; import { localizeSpotifyText } from '../utils/localizeSpotifyText'; interface Props { isOpen: boolean; track: SpotifyTrack; /** 与「加入歌单」同一套 UI;`saveLocation` 仅替换标题文案(如搜索页已保存曲目) */ titleVariant?: 'default' | 'saveLocation'; backdropProps?: React.HTMLAttributes; onSelect: (name: string) => void; onRemoveFromPlaylist?: (name: string) => void; onLike?: () => void; onClose: () => void; } export const AddToPlaylistSheet: React.FC = ({ isOpen, track, titleVariant = 'default', backdropProps, onSelect, onRemoveFromPlaylist, onLike, onClose, }) => { const dragStartY = useRef(0); const barRef = useRef(null); const { go } = useSpotifyGestures(); const locale = useLocale(); const isEnglish = locale === 'en'; const s = useSpotifyStrings(); // Drag state lives in refs so pointer callbacks stay stable const isDraggingRef = useRef(false); const dragOffsetRef = useRef(0); const [renderDragOffset, setRenderDragOffset] = React.useState(0); const DISMISS_THRESHOLD = 80; // Subscribe to store directly for live updates const customPlaylists = useSpotifyStore(s => s.customPlaylists); const likedSongIds = useSpotifyStore(selectLikedSongIds); const toggleLike = useSpotifyStore(s => s.toggleLike); const addTrackToPlaylist = useSpotifyStore(s => s.addTrackToPlaylist); const removeTrackFromPlaylist = useSpotifyStore(s => s.removeTrackFromPlaylist); const displayText = (value: string | undefined) => localizeSpotifyText(value, isEnglish); const trackLiked = likedSongIds.has(track.id, track); const unique = (() => { const s = new Set(); return (customPlaylists || []).filter(pl => { const k = (pl as any).title || pl.id; if (s.has(k)) return false; s.add(k); return true; }) as SpotifyPlaylist[]; })(); const checkInPlaylist = (title: string): boolean => { const pl = (customPlaylists || []).find(p => (p as any).title === title) as any; if (!pl) return false; const ids: string[] = pl.trackIds || []; const stored: any[] = pl.storedTracks || []; const tN = track.title.trim().toLowerCase(); const aN = track.artist.trim().toLowerCase(); return ids.includes(track.id) || stored.some((t: any) => t.title?.trim().toLowerCase() === tN && t.artist?.trim().toLowerCase() === aN); }; const containingPlaylists = unique.filter(pl => checkInPlaylist((pl as any).title)); const otherPlaylists = unique.filter(pl => !checkInPlaylist((pl as any).title)); const handlePlaylistClick = (pl: SpotifyPlaylist) => { const title = (pl as any).title; if (checkInPlaylist(title)) { // Find the actual stored track ID (might differ from track.id due to title+artist match) const plData = (customPlaylists || []).find(p => (p as any).title === title) as any; const ids: string[] = plData?.trackIds || []; const stored: any[] = plData?.storedTracks || []; const tN = track.title.trim().toLowerCase(); const aN = track.artist.trim().toLowerCase(); // Remove by exact ID first, then by title+artist match let idToRemove = ids.includes(track.id) ? track.id : ''; if (!idToRemove) { const matched = stored.find((t: any) => t.title?.trim().toLowerCase() === tN && t.artist?.trim().toLowerCase() === aN); if (matched) idToRemove = matched.id; } if (idToRemove) removeTrackFromPlaylist(title, idToRemove); onRemoveFromPlaylist?.(title); } else { addTrackToPlaylist(title, track); onSelect(title); } }; const handleCreateNew = () => { useSpotifyStore.setState({ pendingPlaylistTrack: track }); go('create.naming.directOpen'); }; const handleLike = () => { toggleLike(track); onLike?.(); }; const onPointerDown = React.useCallback((e: React.PointerEvent) => { dragStartY.current = e.clientY; isDraggingRef.current = true; dragOffsetRef.current = 0; barRef.current?.setPointerCapture(e.pointerId); }, []); const onPointerMove = React.useCallback((e: React.PointerEvent) => { if (!isDraggingRef.current) return; const dy = Math.max(0, e.clientY - dragStartY.current); const maxOffset = typeof window !== 'undefined' ? window.innerHeight : 1000; const clamped = Math.min(dy, maxOffset); dragOffsetRef.current = clamped; setRenderDragOffset(clamped); }, []); const onPointerUp = React.useCallback((e: React.PointerEvent) => { if (barRef.current?.hasPointerCapture(e.pointerId)) { barRef.current.releasePointerCapture(e.pointerId); } if (!isDraggingRef.current) return; isDraggingRef.current = false; const offset = dragOffsetRef.current; dragOffsetRef.current = 0; setRenderDragOffset(0); if (offset >= DISMISS_THRESHOLD) { onClose(); } }, [onClose]); React.useEffect(() => { if (isOpen) return; isDraggingRef.current = false; dragOffsetRef.current = 0; setRenderDragOffset(0); }, [isOpen]); if (!isOpen) return null; const PlaylistRow = ({ pl, inPlaylist }: { pl: SpotifyPlaylist; inPlaylist: boolean }) => ( ); return ( <>
e.buttons === 0 && onPointerUp(e)} onPointerUp={onPointerUp} onPointerCancel={onPointerUp} > {/* Drag handle */}
{/* Header */}
{titleVariant === 'saveLocation' ? s.save_location_title : s.add_to_playlist_title}
{/* Playlist list */}
{/* Playlists containing the track (on top) */} {containingPlaylists.map(pl => ( ))} {containingPlaylists.length > 0 && (otherPlaylists.length > 0 || onLike) && (
)} {/* Liked songs row */} {onLike && ( )} {/* Other playlists */} {otherPlaylists.map(pl => ( ))} {/* New playlist row */}
); };