import { useState, useCallback, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Upload, X, FileText, Trash2, AlertCircle, Music, Clock, Download } from "lucide-react"; import { Spinner } from "@/components/ui/spinner"; import { ReadEmbeddedLyrics, SelectLyricsFiles, ExtractLyricsToLRC } from "../../wailsjs/go/main/App"; import { toastWithSound as toast } from "@/lib/toast-with-sound"; import { OnFileDrop, OnFileDropOff } from "../../wailsjs/runtime/runtime"; interface LyricsFile { path: string; name: string; format: string; lyrics: string; source: string; synced: boolean; status: "loading" | "loaded" | "empty" | "error"; error?: string; } const SUPPORTED_EXTENSIONS = [".lrc", ".txt", ".flac", ".mp3", ".m4a", ".aac", ".opus", ".ogg"]; function getExtension(path: string): string { const lower = path.toLowerCase(); const dot = lower.lastIndexOf("."); return dot >= 0 ? lower.slice(dot) : ""; } export function LyricsManagerPage() { const [files, setFiles] = useState([]); const [selectedPath, setSelectedPath] = useState(null); const [isDragging, setIsDragging] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); const [extracting, setExtracting] = useState(false); useEffect(() => { const checkFullscreen = () => { setIsFullscreen(window.innerHeight >= window.screen.height * 0.9); }; checkFullscreen(); window.addEventListener("resize", checkFullscreen); window.addEventListener("focus", checkFullscreen); return () => { window.removeEventListener("resize", checkFullscreen); window.removeEventListener("focus", checkFullscreen); }; }, []); const addFiles = useCallback(async (paths: string[]) => { const validPaths = paths.filter((path) => SUPPORTED_EXTENSIONS.includes(getExtension(path))); if (validPaths.length === 0) { if (paths.length > 0) { toast.error("Unsupported files", { description: "Only LRC and audio files (FLAC, MP3, M4A) are supported.", }); } return; } const newPaths: string[] = []; setFiles((prev) => { const toAdd = validPaths.filter((path) => !prev.some((f) => f.path === path)); newPaths.push(...toAdd); const entries: LyricsFile[] = toAdd.map((path) => { const name = path.split(/[/\\]/).pop() || path; return { path, name, format: getExtension(path).slice(1), lyrics: "", source: "", synced: false, status: "loading" as const, }; }); if (entries.length === 0) { return prev; } return [...prev, ...entries]; }); for (const path of newPaths) { try { const result = await ReadEmbeddedLyrics(path); setFiles((prev) => prev.map((f) => { if (f.path !== path) return f; if (result.error) { return { ...f, status: "empty" as const, error: result.error }; } return { ...f, lyrics: result.lyrics, source: result.source, synced: result.synced, status: "loaded" as const, }; })); } catch (err) { setFiles((prev) => prev.map((f) => f.path === path ? { ...f, status: "error" as const, error: err instanceof Error ? err.message : "Failed to read lyrics" } : f)); } } setSelectedPath((prev) => prev ?? newPaths[0] ?? null); }, []); const handleSelectFiles = async () => { try { const selected = await SelectLyricsFiles(); if (selected && selected.length > 0) { addFiles(selected); } } catch (err) { toast.error("File Selection Failed", { description: err instanceof Error ? err.message : "Failed to select files", }); } }; const handleFileDrop = useCallback((_x: number, _y: number, paths: string[]) => { setIsDragging(false); if (paths.length === 0) return; addFiles(paths); }, [addFiles]); useEffect(() => { OnFileDrop((x, y, paths) => { handleFileDrop(x, y, paths); }, true); return () => { OnFileDropOff(); }; }, [handleFileDrop]); const removeFile = (path: string) => { setFiles((prev) => { const next = prev.filter((f) => f.path !== path); setSelectedPath((current) => { if (current !== path) return current; return next[0]?.path ?? null; }); return next; }); }; const clearFiles = () => { setFiles([]); setSelectedPath(null); }; const selectedFile = files.find((f) => f.path === selectedPath) || null; const extractFile = async (file: LyricsFile, overwrite: boolean) => { const result = await ExtractLyricsToLRC(file.path, overwrite); if (result.success) { return { ok: true as const, output: result.output_path }; } if (result.already_exists) { return { ok: false as const, alreadyExists: true, output: result.output_path }; } return { ok: false as const, error: result.error || "Failed to extract lyrics" }; }; const handleExtractSelected = async () => { if (!selectedFile || selectedFile.status !== "loaded") return; setExtracting(true); try { const result = await extractFile(selectedFile, false); if (result.ok) { toast.success("Lyrics extracted", { description: result.output }); } else if (result.alreadyExists) { toast.info("LRC already exists", { description: "A .lrc file with the same name already exists next to this file.", }); } else { toast.error("Extract failed", { description: result.error }); } } catch (err) { toast.error("Extract failed", { description: err instanceof Error ? err.message : "Unknown error", }); } finally { setExtracting(false); } }; const handleExtractAll = async () => { const extractable = files.filter((f) => f.status === "loaded"); if (extractable.length === 0) { toast.error("Nothing to extract", { description: "No files with embedded lyrics are loaded.", }); return; } setExtracting(true); let success = 0; let skipped = 0; let failed = 0; for (const file of extractable) { try { const result = await extractFile(file, false); if (result.ok) success++; else if (result.alreadyExists) skipped++; else failed++; } catch { failed++; } } setExtracting(false); if (success > 0) { toast.success("Lyrics extracted", { description: `${success} file(s) extracted${skipped > 0 ? `, ${skipped} skipped` : ""}${failed > 0 ? `, ${failed} failed` : ""}`, }); } else if (skipped > 0 && failed === 0) { toast.info("Already extracted", { description: `${skipped} .lrc file(s) already exist.`, }); } else { toast.error("Extract failed", { description: `${failed} file(s) failed to extract.`, }); } }; const embeddedLoadedCount = files.filter((f) => f.status === "loaded" && f.source === "embedded").length; return (

Lyrics Manager

{files.length > 0 && (
{embeddedLoadedCount > 0 && ()}
)}
{ e.preventDefault(); setIsDragging(true); }} onDragLeave={(e) => { e.preventDefault(); setIsDragging(false); }} onDrop={(e) => { e.preventDefault(); setIsDragging(false); }} style={{ "--wails-drop-target": "drop" } as React.CSSProperties}> {files.length === 0 ? (<>

{isDragging ? "Drop your files here" : "Drag and drop LRC or audio files here, or click the button below to select"}

Reads embedded lyrics from FLAC, MP3, M4A, Opus or plain LRC files

) : (
{files.map((file) => { const isActive = file.path === selectedPath; return (); })}
{!selectedFile ? (
Select a file to view its lyrics
) : selectedFile.status === "loading" ? (
Reading lyrics...
) : selectedFile.status === "error" || selectedFile.status === "empty" ? (

{selectedFile.name}

{selectedFile.error || "No lyrics found"}

) : (<>

{selectedFile.name}

{selectedFile.source === "lrc" ? (<> LRC) : (<> Embedded)} {selectedFile.synced ? "Synced" : "Plain"}
{selectedFile.source === "embedded" && ()}
{selectedFile.lyrics}
)}
)}
); }