import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ListMusic, Plus } from 'lucide-react'; import { usePlaylistStore } from '../../store/playlistStore'; import { showToast } from '../../utils/ui/toast'; import { isSmartPlaylistName } from '../../utils/componentHelpers/contextMenuHelpers'; interface SingleProps { playlist: { id: string; name: string }; onDone: () => void; triggerId?: string; } export function SinglePlaylistToPlaylistSubmenu({ playlist, onDone, triggerId }: SingleProps) { const { t } = useTranslation(); const subRef = useRef(null); const [creating, setCreating] = useState(false); const [newName, setNewName] = useState(''); const newNameRef = useRef(null); const [flipLeft, setFlipLeft] = useState(false); const [flipUp, setFlipUp] = useState(false); const storePlaylists = usePlaylistStore((s) => s.playlists); const allPlaylists = useMemo(() => { return storePlaylists.filter( (p) => p.id !== playlist.id && !isSmartPlaylistName(p.name), ); }, [storePlaylists, playlist.id]); useLayoutEffect(() => { if (subRef.current) { const rect = subRef.current.getBoundingClientRect(); if (rect.right > window.innerWidth - 8) setFlipLeft(true); if (rect.bottom > window.innerHeight - 8) setFlipUp(true); } }, []); useEffect(() => { if (creating && newNameRef.current) newNameRef.current.focus(); }, [creating]); const handleCreate = async () => { if (!newName.trim()) return; const { createPlaylist } = await import('../../api/subsonicPlaylists'); try { const newPl = await createPlaylist(newName.trim(), []); if (newPl?.id) { await handleAddToNewPlaylist(newPl.id, newPl.name || newName.trim()); } setCreating(false); setNewName(''); } catch { showToast(t('playlists.createError'), 3000, 'error'); } }; const handleAddToNewPlaylist = async (targetId: string, targetName: string) => { const { getPlaylist, updatePlaylist } = await import('../../api/subsonicPlaylists'); const touchPlaylist = usePlaylistStore.getState().touchPlaylist; try { const { songs: sourceSongs } = await getPlaylist(playlist.id); if (sourceSongs.length > 0) { await updatePlaylist(targetId, sourceSongs.map((s: { id: string }) => s.id)); touchPlaylist(targetId); showToast(t('playlists.createAndAddSuccess', { count: sourceSongs.length, playlist: targetName }), 3000, 'info'); } onDone(); } catch { showToast(t('playlists.addToPlaylistError'), 4000, 'error'); onDone(); } }; const handleAdd = async (targetId: string, targetName: string) => { const { getPlaylist, updatePlaylist } = await import('../../api/subsonicPlaylists'); const touchPlaylist = usePlaylistStore.getState().touchPlaylist; try { const { songs: targetSongs } = await getPlaylist(targetId); const targetIds = new Set(targetSongs.map((s: { id: string }) => s.id)); const { songs: sourceSongs } = await getPlaylist(playlist.id); const newSongs = sourceSongs.filter((s: { id: string }) => !targetIds.has(s.id)); if (newSongs.length > 0) { newSongs.forEach((s: { id: string }) => targetIds.add(s.id)); await updatePlaylist(targetId, Array.from(targetIds)); touchPlaylist(targetId); showToast(t('playlists.addToPlaylistSuccess', { count: newSongs.length, playlist: targetName }), 3000, 'info'); } else { showToast(t('playlists.addToPlaylistNoNew', { playlist: targetName }), 3000, 'info'); } onDone(); } catch { showToast(t('playlists.addToPlaylistError'), 4000, 'error'); onDone(); } }; const subStyle: React.CSSProperties = flipLeft ? { right: '100%', left: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' } : { left: '100%', right: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' }; return (
{!creating ? (
{ e.stopPropagation(); setCreating(true); }}> {t('playlists.newPlaylist')}
) : (
e.stopPropagation()}> setNewName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') { setCreating(false); setNewName(''); } }} />
)}
{allPlaylists.length === 0 && (
{t('playlists.noOtherPlaylists')}
)} {allPlaylists.map(pl => (
handleAdd(pl.id, pl.name)} > {pl.name}
))}
); } interface MultiProps { playlists: { id: string; name: string }[]; onDone: () => void; triggerId?: string; } export function MultiPlaylistToPlaylistSubmenu({ playlists, onDone, triggerId }: MultiProps) { const { t } = useTranslation(); const subRef = useRef(null); const [creating, setCreating] = useState(false); const [newName, setNewName] = useState(''); const newNameRef = useRef(null); const [flipLeft, setFlipLeft] = useState(false); const [flipUp, setFlipUp] = useState(false); const storePlaylists = usePlaylistStore((s) => s.playlists); const allPlaylists = useMemo(() => { const selectedIds = new Set(playlists.map(p => p.id)); return storePlaylists.filter( (p) => !selectedIds.has(p.id) && !isSmartPlaylistName(p.name), ); }, [storePlaylists, playlists]); useLayoutEffect(() => { if (subRef.current) { const rect = subRef.current.getBoundingClientRect(); if (rect.right > window.innerWidth - 8) setFlipLeft(true); if (rect.bottom > window.innerHeight - 8) setFlipUp(true); } }, []); useEffect(() => { if (creating && newNameRef.current) newNameRef.current.focus(); }, [creating]); const handleCreate = async () => { if (!newName.trim()) return; const { createPlaylist } = await import('../../api/subsonicPlaylists'); try { const newPl = await createPlaylist(newName.trim(), []); if (newPl?.id) { await handleMergeToNewPlaylist(newPl.id, newPl.name || newName.trim()); } setCreating(false); setNewName(''); } catch { showToast(t('playlists.createError'), 3000, 'error'); } }; const handleMergeToNewPlaylist = async (targetId: string, targetName: string) => { const { getPlaylist, updatePlaylist } = await import('../../api/subsonicPlaylists'); const touchPlaylist = usePlaylistStore.getState().touchPlaylist; try { const targetIds = new Set(); let totalAdded = 0; for (const pl of playlists) { const { songs } = await getPlaylist(pl.id); const newSongs = songs.filter((s: { id: string }) => !targetIds.has(s.id)); if (newSongs.length > 0) { newSongs.forEach((s: { id: string }) => targetIds.add(s.id)); totalAdded += newSongs.length; } } if (totalAdded > 0) { await updatePlaylist(targetId, Array.from(targetIds)); touchPlaylist(targetId); showToast(t('playlists.createAndAddSuccess', { count: totalAdded, playlist: targetName }), 3000, 'info'); } onDone(); } catch { showToast(t('playlists.mergeError'), 4000, 'error'); onDone(); } }; const handleMerge = async (targetId: string, targetName: string) => { const { getPlaylist, updatePlaylist } = await import('../../api/subsonicPlaylists'); const touchPlaylist = usePlaylistStore.getState().touchPlaylist; try { const { songs: targetSongs } = await getPlaylist(targetId); const targetIds = new Set(targetSongs.map((s: { id: string }) => s.id)); let totalAdded = 0; for (const pl of playlists) { const { songs } = await getPlaylist(pl.id); const newSongs = songs.filter((s: { id: string }) => !targetIds.has(s.id)); if (newSongs.length > 0) { newSongs.forEach((s: { id: string }) => targetIds.add(s.id)); totalAdded += newSongs.length; } } if (totalAdded > 0) { await updatePlaylist(targetId, Array.from(targetIds)); touchPlaylist(targetId); showToast(t('playlists.mergeSuccess', { count: totalAdded, playlist: targetName }), 3000, 'info'); } else { showToast(t('playlists.mergeNoNewSongs'), 3000, 'info'); } onDone(); } catch { showToast(t('playlists.mergeError'), 4000, 'error'); onDone(); } }; const subStyle: React.CSSProperties = flipLeft ? { right: '100%', left: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' } : { left: '100%', right: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' }; return (
{!creating ? (
{ e.stopPropagation(); setCreating(true); }}> {t('playlists.newPlaylist')}
) : (
e.stopPropagation()}> setNewName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') { setCreating(false); setNewName(''); } }} />
)}
{allPlaylists.length === 0 && (
{t('playlists.noOtherPlaylists')}
)} {allPlaylists.map(pl => (
handleMerge(pl.id, pl.name)} > {pl.name}
))}
); }