import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { GripVertical, Music2 } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; import { useDragDrop, useDragSource } from '../../contexts/DragDropContext'; import { useAuthStore } from '../../store/authStore'; import type { LyricsSourceId } from '../../store/authStoreTypes'; const LYRICS_SOURCE_LABEL_KEYS: Record = { server: 'settings.lyricsSourceServer', lrclib: 'settings.lyricsSourceLrclib', netease: 'settings.lyricsSourceNetease', }; type LyricsDropTarget = { idx: number; before: boolean } | null; function LyricsSourceGripHandle({ idx, label }: { idx: number; label: string }) { const { t } = useTranslation(); const { onMouseDown } = useDragSource(() => ({ data: JSON.stringify({ type: 'lyrics_source_reorder', index: idx }), label, })); return ( ); } export function LyricsSourcesCustomizer() { const { t } = useTranslation(); const lyricsSources = useAuthStore(useShallow(s => s.lyricsSources)); const setLyricsSources = useAuthStore(s => s.setLyricsSources); const lyricsMode = useAuthStore(s => s.lyricsMode); const setLyricsMode = useAuthStore(s => s.setLyricsMode); const lyricsStaticOnly = useAuthStore(s => s.lyricsStaticOnly); const setLyricsStaticOnly = useAuthStore(s => s.setLyricsStaticOnly); const { isDragging: isPsyDragging } = useDragDrop(); // useState (not useRef) so the listener-effect re-runs when the container // gets unmounted/remounted by the {lyricsMode === 'standard'} wrapper. const [containerEl, setContainerEl] = useState(null); const [dropTarget, setDropTarget] = useState(null); const dropTargetRef = useRef(null); const sourcesRef = useRef(lyricsSources); sourcesRef.current = lyricsSources; useEffect(() => { if (!isPsyDragging) { dropTargetRef.current = null; setDropTarget(null); } }, [isPsyDragging]); useEffect(() => { if (!containerEl) return; const onPsyDrop = (e: Event) => { const detail = (e as CustomEvent).detail; if (!detail?.data) return; let parsed: { type?: string; index?: number }; try { parsed = JSON.parse(detail.data as string); } catch { return; } if (parsed.type !== 'lyrics_source_reorder' || parsed.index == null) return; const fromIdx = parsed.index; const target = dropTargetRef.current; dropTargetRef.current = null; setDropTarget(null); if (!target) return; const insertBefore = target.before ? target.idx : target.idx + 1; if (insertBefore === fromIdx || insertBefore === fromIdx + 1) return; const next = [...sourcesRef.current]; const [moved] = next.splice(fromIdx, 1); next.splice(insertBefore > fromIdx ? insertBefore - 1 : insertBefore, 0, moved); setLyricsSources(next); }; containerEl.addEventListener('psy-drop', onPsyDrop); return () => containerEl.removeEventListener('psy-drop', onPsyDrop); }, [containerEl, setLyricsSources]); const handleMouseMove = (e: React.MouseEvent) => { if (!isPsyDragging || !containerEl) return; const rows = containerEl.querySelectorAll('[data-lyrics-idx]'); let target: LyricsDropTarget = null; for (const row of rows) { const rect = row.getBoundingClientRect(); const idx = Number(row.dataset.lyricsIdx); if (e.clientY < rect.top + rect.height / 2) { target = { idx, before: true }; break; } target = { idx, before: false }; } dropTargetRef.current = target; setDropTarget(target); }; const toggleSource = (id: LyricsSourceId) => { setLyricsSources(sourcesRef.current.map(s => s.id === id ? { ...s, enabled: !s.enabled } : s)); }; return (

{t('settings.lyricsSourcesTitle')}

{t('settings.lyricsSourcesDesc')}

{/* Mode switch — standard three-provider pipeline vs. YouLyPlus karaoke. YouLyPlus misses silently fall back to the standard pipeline. */}
{t('settings.lyricsModeLyricsplus')}
{t('settings.lyricsModeLyricsplusDesc')}
{t('settings.lyricsModeStandard')}
{t('settings.lyricsModeStandardDesc')}
{lyricsMode === 'standard' && (
{lyricsSources.map((src, i) => { const label = t(LYRICS_SOURCE_LABEL_KEYS[src.id]); const isBefore = isPsyDragging && dropTarget?.idx === i && dropTarget.before; const isAfter = isPsyDragging && dropTarget?.idx === i && !dropTarget.before; return (
{label}
); })}
)} {/* Static-only toggle — suppresses line/word tracking in both modes. */}
{t('settings.lyricsStaticOnly')}
{t('settings.lyricsStaticOnlyDesc')}
); }