import { useState, useRef, useMemo } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { useUI, useAudio, useConfig } from "../../context/LauncherContext"; import { ColService } from "../../services/ColService"; import { ColFile } from "../../types/col"; function argbToHex(argb: number) { return (argb >>> 0).toString(16).padStart(8, '0').toUpperCase(); } function hexToArgb(hex: string) { const cleanHex = hex.replace("#", ""); return parseInt(cleanHex, 16) >>> 0; } export default function ColEditorView() { const { setActiveView } = useUI(); const { playPressSound, playBackSound } = useAudio(); const { animationsEnabled } = useConfig(); const [col, setCol] = useState(null); const [searchTerm, setSearchTerm] = useState(""); const [notification, setNotification] = useState<{ message: string, type: "success" | "error" } | null>(null); const fileInputRef = useRef(null); const [activeTab, setActiveTab] = useState<"colors" | "worldColors">("colors"); const showNotification = (message: string, type: "success" | "error" = "success") => { setNotification({ message, type }); setTimeout(() => setNotification(null), 3000); }; const currentColors = useMemo(() => { if (!col) return []; return col.colors.map((c, i) => ({ ...c, originalIdx: i })) .filter(c => c.name.toLowerCase().includes(searchTerm.toLowerCase())); }, [col, searchTerm]); const currentWorldColors = useMemo(() => { if (!col) return []; return col.worldColors.map((c, i) => ({ ...c, originalIdx: i })) .filter(c => c.name.toLowerCase().includes(searchTerm.toLowerCase())); }, [col, searchTerm]); const handleFileLoad = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; playPressSound(); const buffer = await file.arrayBuffer(); try { const parsedCol = ColService.readCOL(buffer); setCol(parsedCol); showNotification(`Loaded ${file.name}`); } catch (err: unknown) { console.error("Failed to parse COL", err); showNotification(err instanceof Error ? err.message : "Failed to parse COL", "error"); } e.target.value = ""; }; const handleSaveCol = () => { if (!col) return; playPressSound(); try { const buffer = ColService.serializeCOL(col); const blob = new Blob([buffer]); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "colours.col"; a.click(); URL.revokeObjectURL(url); showNotification("COL Saved Successfully"); } catch (err: unknown) { console.error("Failed to save COL", err); showNotification(err instanceof Error ? err.message : "Failed to save COL", "error"); } }; const handleUpdateColor = (idx: number, field: string, val: string | number) => { if (!col) return; const newCol = { ...col, colors: [...col.colors] }; newCol.colors[idx] = { ...newCol.colors[idx], [field]: val }; setCol(newCol); }; const handleUpdateWorldColor = (idx: number, field: string, val: string | number) => { if (!col) return; const newCol = { ...col, worldColors: [...col.worldColors] }; newCol.worldColors[idx] = { ...newCol.worldColors[idx], [field]: val }; setCol(newCol); }; const handleAddColor = () => { if (!col) return; playPressSound(); setCol({ ...col, colors: [{ name: "NewColor", color: 0xFFFFFFFF }, ...col.colors] }); showNotification("Color Added"); }; const handleAddWorldColor = () => { if (!col) return; playPressSound(); setCol({ ...col, worldColors: [{ name: "NewWorldColor", waterColor: 0xFFFFFFFF, underwaterColor: 0xFFFFFFFF, fogColor: 0xFFFFFFFF }, ...col.worldColors] }); showNotification("World Color Added"); }; const handleDeleteColor = (idx: number) => { if (!col) return; playBackSound(); const newCol = { ...col, colors: [...col.colors] }; newCol.colors.splice(idx, 1); setCol(newCol); }; const handleDeleteWorldColor = (idx: number) => { if (!col) return; playBackSound(); const newCol = { ...col, worldColors: [...col.worldColors] }; newCol.worldColors.splice(idx, 1); setCol(newCol); }; return (

COL Editor

{col && Version: {col.version}}
{!col ? (

Open a COL file to begin editing

) : (
setSearchTerm(e.target.value)} className="flex-1 bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors" />
{activeTab === "colors" && } {activeTab === "worldColors" && ( <> )} {activeTab === "colors" && currentColors.map((c) => ( ))} {activeTab === "worldColors" && currentWorldColors.map((w) => ( ))}
NameColor (ARGB)Water (ARGB) Underwater (ARGB) Fog (ARGB)Remove
handleUpdateColor(c.originalIdx, "name", e.target.value)} className="w-full bg-black/40 border border-[#373737] px-2 py-1 outline-none focus:border-[#FFFF55] text-white text-sm" />
{ const rgb = e.target.value.substring(1).toUpperCase(); const alpha = argbToHex(c.color).substring(0, 2); const newArgb = hexToArgb(`${alpha}${rgb}`); handleUpdateColor(c.originalIdx, "color", newArgb); }} className="w-8 h-8 p-0 cursor-pointer shrink-0 border border-[#373737] rounded-sm bg-transparent" /> { const s = e.target.value.replace(/[^0-9A-Fa-f]/g, ''); if (s.length <= 8) { const parsed = parseInt(s, 16); if (!isNaN(parsed)) handleUpdateColor(c.originalIdx, "color", parsed >>> 0); } }} className="bg-black/40 border border-[#373737] w-24 px-2 py-1 outline-none focus:border-[#FFFF55] text-white font-mono text-sm uppercase" maxLength={8} />
handleUpdateWorldColor(w.originalIdx, "name", e.target.value)} className="w-full bg-black/40 border border-[#373737] px-2 py-1 outline-none focus:border-[#FFFF55] text-white text-sm" />
{ const rgb = e.target.value.substring(1).toUpperCase(); const alpha = argbToHex(w.waterColor).substring(0, 2); handleUpdateWorldColor(w.originalIdx, "waterColor", hexToArgb(`${alpha}${rgb}`)); }} className="w-6 h-6 p-0 cursor-pointer shrink-0 border border-[#373737] rounded-sm bg-transparent" /> { const s = e.target.value.replace(/[^0-9A-Fa-f]/g, ''); if (s.length <= 8) { const parsed = parseInt(s, 16); if (!isNaN(parsed)) handleUpdateWorldColor(w.originalIdx, "waterColor", parsed >>> 0); } }} className="bg-black/40 border border-[#373737] w-20 px-2 py-1 outline-none focus:border-[#FFFF55] text-white font-mono text-xs uppercase" maxLength={8} />
{ const rgb = e.target.value.substring(1).toUpperCase(); const alpha = argbToHex(w.underwaterColor).substring(0, 2); handleUpdateWorldColor(w.originalIdx, "underwaterColor", hexToArgb(`${alpha}${rgb}`)); }} className="w-6 h-6 p-0 cursor-pointer shrink-0 border border-[#373737] rounded-sm bg-transparent" /> { const s = e.target.value.replace(/[^0-9A-Fa-f]/g, ''); if (s.length <= 8) { const parsed = parseInt(s, 16); if (!isNaN(parsed)) handleUpdateWorldColor(w.originalIdx, "underwaterColor", parsed >>> 0); } }} className="bg-black/40 border border-[#373737] w-20 px-2 py-1 outline-none focus:border-[#FFFF55] text-white font-mono text-xs uppercase" maxLength={8} />
{ const rgb = e.target.value.substring(1).toUpperCase(); const alpha = argbToHex(w.fogColor).substring(0, 2); handleUpdateWorldColor(w.originalIdx, "fogColor", hexToArgb(`${alpha}${rgb}`)); }} className="w-6 h-6 p-0 cursor-pointer shrink-0 border border-[#373737] rounded-sm bg-transparent" /> { const s = e.target.value.replace(/[^0-9A-Fa-f]/g, ''); if (s.length <= 8) { const parsed = parseInt(s, 16); if (!isNaN(parsed)) handleUpdateWorldColor(w.originalIdx, "fogColor", parsed >>> 0); } }} className="bg-black/40 border border-[#373737] w-20 px-2 py-1 outline-none focus:border-[#FFFF55] text-white font-mono text-xs uppercase" maxLength={8} />
)}
{notification && ( {notification.message} )}
); }