import { useState, useCallback, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { ToggleGroup, ToggleGroupItem, } from "@/components/ui/toggle-group"; import { Upload, X, CheckCircle2, AlertCircle, Trash2, FileMusic, WandSparkles, } from "lucide-react"; import { Spinner } from "@/components/ui/spinner"; import { ConvertAudio, SelectAudioFiles, SelectFolder, ListAudioFilesInDir, } from "../../wailsjs/go/main/App"; import { toastWithSound as toast } from "@/lib/toast-with-sound"; import { OnFileDrop, OnFileDropOff } from "../../wailsjs/runtime/runtime"; interface AudioFile { path: string; name: string; format: string; size: number; status: "pending" | "converting" | "success" | "error"; error?: string; outputPath?: string; } function formatFileSize(bytes: number): string { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; } const BITRATE_OPTIONS = [ { value: "320k", label: "320k" }, { value: "256k", label: "256k" }, { value: "192k", label: "192k" }, { value: "128k", label: "128k" }, ]; const M4A_CODEC_OPTIONS = [ { value: "aac", label: "AAC" }, { value: "alac", label: "ALAC" }, ]; const STORAGE_KEY = "spotiflac_audio_converter_state"; export function AudioConverterPage() { const [files, setFiles] = useState(() => { try { const saved = sessionStorage.getItem(STORAGE_KEY); if (saved) { const parsed = JSON.parse(saved); if (parsed.files && Array.isArray(parsed.files) && parsed.files.length > 0) { return parsed.files; } } } catch (err) { console.error("Failed to load saved state:", err); } return []; }); const [outputFormat, setOutputFormat] = useState<"mp3" | "m4a">(() => { try { const saved = sessionStorage.getItem(STORAGE_KEY); if (saved) { const parsed = JSON.parse(saved); if (parsed.outputFormat === "mp3" || parsed.outputFormat === "m4a") { return parsed.outputFormat; } } } catch (err) { } return "mp3"; }); const [bitrate, setBitrate] = useState(() => { try { const saved = sessionStorage.getItem(STORAGE_KEY); if (saved) { const parsed = JSON.parse(saved); if (parsed.bitrate) { return parsed.bitrate; } } } catch (err) { } return "320k"; }); const [m4aCodec, setM4aCodec] = useState<"aac" | "alac">(() => { try { const saved = sessionStorage.getItem(STORAGE_KEY); if (saved) { const parsed = JSON.parse(saved); if (parsed.m4aCodec === "aac" || parsed.m4aCodec === "alac") { return parsed.m4aCodec; } } } catch (err) { } return "aac"; }); const [converting, setConverting] = useState(false); const [isDragging, setIsDragging] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); const saveState = useCallback((stateToSave: { files: AudioFile[]; outputFormat: "mp3" | "m4a"; bitrate: string; m4aCodec: "aac" | "alac"; }) => { try { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(stateToSave)); } catch (err) { console.error("Failed to save state:", err); } }, []); useEffect(() => { saveState({ files, outputFormat, bitrate, m4aCodec }); }, [files, outputFormat, bitrate, m4aCodec, saveState]); useEffect(() => { if (files.length === 0) return; const allMP3 = files.every((f) => f.format === "mp3"); if (allMP3 && outputFormat !== "m4a") { setOutputFormat("m4a"); } const hasFlac = files.some((f) => f.format === "flac"); if (!hasFlac && m4aCodec === "alac") { setM4aCodec("aac"); } }, [files, outputFormat, m4aCodec]); const isFormatDisabled = files.length > 0 && files.every((f) => f.format === "mp3"); const hasFlacFiles = files.some((f) => f.format === "flac"); useEffect(() => { const checkFullscreen = () => { const isMaximized = window.innerHeight >= window.screen.height * 0.9; setIsFullscreen(isMaximized); }; checkFullscreen(); window.addEventListener("resize", checkFullscreen); window.addEventListener("focus", checkFullscreen); return () => { window.removeEventListener("resize", checkFullscreen); window.removeEventListener("focus", checkFullscreen); }; }, []); const handleSelectFiles = async () => { try { const selectedFiles = await SelectAudioFiles(); if (selectedFiles && selectedFiles.length > 0) { addFiles(selectedFiles); } } catch (err) { toast.error("File Selection Failed", { description: err instanceof Error ? err.message : "Failed to select files", }); } }; const handleSelectFolder = async () => { try { const selectedFolder = await SelectFolder(""); if (selectedFolder) { const folderFiles = await ListAudioFilesInDir(selectedFolder); if (folderFiles && folderFiles.length > 0) { addFiles(folderFiles.map((f) => f.path)); } else { toast.info("No audio files found", { description: "No FLAC or MP3 files found in the selected folder.", }); } } } catch (err) { toast.error("Folder Selection Failed", { description: err instanceof Error ? err.message : "Failed to select folder", }); } }; const addFiles = useCallback(async (paths: string[]) => { const validExtensions = [".mp3", ".flac"]; const m4aFiles = paths.filter((path) => { const ext = path.toLowerCase().slice(path.lastIndexOf(".")); return ext === ".m4a"; }); if (m4aFiles.length > 0) { toast.error("M4A files not supported", { description: "Only FLAC and MP3 files are supported as input. Please convert M4A files first.", }); } const GetFileSizes = (files: string[]): Promise> => (window as any)["go"]["main"]["App"]["GetFileSizes"](files); const validPaths = paths.filter((path) => { const ext = path.toLowerCase().slice(path.lastIndexOf(".")); return validExtensions.includes(ext); }); const fileSizes = validPaths.length > 0 ? await GetFileSizes(validPaths) : {}; setFiles((prev) => { const newFiles: AudioFile[] = validPaths .filter((path) => !prev.some((f) => f.path === path)) .map((path) => { const name = path.split(/[/\\]/).pop() || path; const ext = name.slice(name.lastIndexOf(".") + 1).toLowerCase(); return { path, name, format: ext, size: fileSizes[path] || 0, status: "pending" as const, }; }); if (newFiles.length > 0) { if (paths.length > newFiles.length) { const skipped = paths.length - newFiles.length; toast.info("Some files skipped", { description: `${skipped} file(s) were skipped (unsupported format or already added)`, }); } return [...prev, ...newFiles]; } if (paths.length > 0 && m4aFiles.length === 0) { toast.info("No new files added", { description: "All files were already added or have unsupported format", }); } return prev; }); }, []); const handleFileDrop = useCallback(async (_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) => prev.filter((f) => f.path !== path)); }; const clearFiles = () => { setFiles([]); }; const handleConvert = async () => { if (files.length === 0) { toast.error("No files selected", { description: "Please add audio files to convert", }); return; } setConverting(true); try { const inputPaths = files.map((f) => f.path); setFiles((prev) => prev.map((f) => { if (inputPaths.includes(f.path)) { return { ...f, status: "converting" as const, error: undefined }; } return f; })); const results = await ConvertAudio({ input_files: inputPaths, output_format: outputFormat, bitrate: bitrate, codec: outputFormat === "m4a" ? m4aCodec : "", }); setFiles((prev) => prev.map((f) => { const result = results.find((r) => r.input_file === f.path || r.input_file.toLowerCase() === f.path.toLowerCase()); if (result) { return { ...f, status: result.success ? "success" : "error", error: result.error, outputPath: result.output_file, }; } return f; })); const successCount = results.filter((r) => r.success).length; const failCount = results.filter((r) => !r.success).length; if (successCount > 0) { toast.success("Conversion Complete", { description: `Successfully converted ${successCount} file(s)${failCount > 0 ? `, ${failCount} failed` : ""}`, }); } else if (failCount > 0) { toast.error("Conversion Failed", { description: `All ${failCount} file(s) failed to convert`, }); } } catch (err) { toast.error("Conversion Error", { description: err instanceof Error ? err.message : "Unknown error", }); setFiles((prev) => prev.map((f) => ({ ...f, status: "error" as const, error: "Conversion failed" }))); } finally { setConverting(false); } }; const getStatusIcon = (status: AudioFile["status"]) => { switch (status) { case "converting": return ; case "success": return ; case "error": return ; default: return ; } }; const convertableCount = files.filter((f) => f.status === "pending" || f.status === "success").length; const successCount = files.filter((f) => f.status === "success").length; return (

Audio Converter

{files.length > 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 audio files here" : "Drag and drop audio files here, or click the button below to select"}

Supported formats: FLAC, MP3

) : (
{ if (value && !isFormatDisabled) setOutputFormat(value as "mp3" | "m4a"); }} disabled={isFormatDisabled}> {!isFormatDisabled && ( MP3 )} M4A
{outputFormat === "m4a" && hasFlacFiles && (
{ if (value) setM4aCodec(value as "aac" | "alac"); }}> {M4A_CODEC_OPTIONS.map((option) => ( {option.label} ))}
)} {!(outputFormat === "m4a" && m4aCodec === "alac") && (
{ if (value) setBitrate(value); }}> {BITRATE_OPTIONS.map((option) => ( {option.label} ))}
)}
{files.length} file(s) • {successCount} converted
{files.map((file) => (
{getStatusIcon(file.status)}

{file.name}

{file.error && (

{file.error}

)}
{formatFileSize(file.size)} {file.format} {file.status !== "converting" && ()}
))}
)}
); }