import { useState, useRef, useMemo } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { useUI, useAudio, useConfig } from "../../context/LauncherContext"; import { ArcService } from "../../services/ArcService"; import { ArcFile, ArcEntry, LocFile, LocLanguage } from "../../types/arc"; export const ArcEditorView: React.FC = () => { const { setActiveView } = useUI(); const { playPressSound, playBackSound } = useAudio(); const { animationsEnabled } = useConfig(); const [arc, setArc] = useState(null); const [loc, setLoc] = useState(null); const [activeTab, setActiveTab] = useState<"arc" | "loc">("arc"); const [searchTerm, setSearchTerm] = useState(""); const [selectedEntryIdx, setSelectedEntryIdx] = useState(null); const [selectedLocLangIdx, setSelectedLocLangIdx] = useState(0); const [notification, setNotification] = useState<{ message: string, type: "success" | "error" } | null>(null); const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isReplaceModalOpen, setIsReplaceModalOpen] = useState(false); const [isRenameModalOpen, setIsRenameModalOpen] = useState(false); const [isLocEditModalOpen, setIsLocEditModalOpen] = useState<{ langIdx: number, strIdx: number, isNew: boolean } | null>(null); const fileInputRef = useRef(null); const injectInputRef = useRef(null); const replaceInputRef = useRef(null); const filteredEntries = useMemo(() => { if (!arc) return []; return arc.entries.map((e, i) => ({ ...e, originalIdx: i })) .filter(e => e.filename.toLowerCase().includes(searchTerm.toLowerCase())); }, [arc, searchTerm]); const currentLocLang = useMemo(() => { if (!loc) return null; return loc.languages[selectedLocLangIdx] || null; }, [loc, selectedLocLangIdx]); const filteredLocStrings = useMemo(() => { if (!currentLocLang) return []; return currentLocLang.strings.map((s, i) => ({ ...s, originalIdx: i })) .filter(s => (s.key?.toLowerCase().includes(searchTerm.toLowerCase()) || s.value.toLowerCase().includes(searchTerm.toLowerCase()))); }, [currentLocLang, searchTerm]); const showNotification = (message: string, type: "success" | "error" = "success") => { setNotification({ message, type }); setTimeout(() => setNotification(null), 3000); }; const handleFileLoad = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; playPressSound(); const buffer = await file.arrayBuffer(); try { const parsed = await ArcService.readARC(buffer); parsed.name = file.name; setArc(parsed); const locEntry = parsed.entries.find(entry => entry.filename.toLowerCase() === "languages.loc"); if (locEntry) { try { const parsedLoc = ArcService.parseLOC(locEntry.data); setLoc(parsedLoc); } catch (err) { console.warn("Could not parse languages.loc", err); setLoc(null); } } else { setLoc(null); } setSelectedEntryIdx(null); showNotification(`Loaded ${file.name}`); } catch (err) { console.error("Failed to parse ARC", err); showNotification("Failed to parse ARC", "error"); } }; const handleSaveArc = () => { if (!arc) return; playPressSound(); const buffer = ArcService.serializeARC(arc); const blob = new Blob([buffer]); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = arc.name || "archive.arc"; a.click(); URL.revokeObjectURL(url); showNotification("ARC Saved Successfully"); }; const handleExtractEntry = (entry: ArcEntry) => { playPressSound(); const blob = new Blob([entry.data as any]); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = entry.filename.split("/").pop() || "asset"; a.click(); URL.revokeObjectURL(url); showNotification(`Extracted: ${entry.filename}`); }; const handleDeleteEntry = (idx: number) => { if (!arc) return; playBackSound(); const name = arc.entries[idx].filename; const newEntries = [...arc.entries]; newEntries.splice(idx, 1); setArc({ ...arc, entries: newEntries }); setSelectedEntryIdx(null); showNotification(`Deleted: ${name}`); }; const handleSaveLocToArc = () => { if (!loc || !arc) return; playPressSound(); const data = ArcService.serializeLOC(loc); const locIdx = arc.entries.findIndex(e => e.filename.toLowerCase() === "languages.loc"); const newEntries = [...arc.entries]; if (locIdx >= 0) { newEntries[locIdx] = { ...newEntries[locIdx], data, size: data.length }; showNotification("languages.loc updated in archive"); } else { newEntries.push({ filename: "languages.loc", ptr: 0, size: data.length, isCompressed: false, data }); showNotification("languages.loc added to archive"); } setArc({ ...arc, entries: newEntries }); }; const handleAddEntry = async (e: React.ChangeEvent) => { if (!arc) return; const file = e.target.files?.[0]; if (!file) return; playPressSound(); const buffer = await file.arrayBuffer(); const data = new Uint8Array(buffer); const newEntry: ArcEntry = { filename: file.name, ptr: 0, size: data.length, isCompressed: false, data }; setArc({ ...arc, entries: [...arc.entries, newEntry] }); e.target.value = ""; showNotification("Entry Added"); setIsAddModalOpen(false); }; const handleReplaceEntry = async (e: React.ChangeEvent) => { if (!arc || selectedEntryIdx === null) return; const file = e.target.files?.[0]; if (!file) return; playPressSound(); const buffer = await file.arrayBuffer(); const data = new Uint8Array(buffer); const newEntries = [...arc.entries]; newEntries[selectedEntryIdx] = { ...newEntries[selectedEntryIdx], data, size: data.length }; setArc({ ...arc, entries: newEntries }); e.target.value = ""; showNotification("Entry Replaced"); setIsReplaceModalOpen(false); }; const handleRenameEntry = (newPath: string, isCompressed: boolean) => { if (!arc || selectedEntryIdx === null) return; playPressSound(); const newEntries = [...arc.entries]; newEntries[selectedEntryIdx] = { ...newEntries[selectedEntryIdx], filename: newPath, isCompressed }; setArc({ ...arc, entries: newEntries }); setIsRenameModalOpen(false); showNotification("Entry Renamed"); }; const handleLocStringEdit = (langIdx: number, strIdx: number, isNew: boolean, key: string, value: string) => { if (!loc) return; playPressSound(); const newLoc = { ...loc }; const lang = newLoc.languages[langIdx]; if (isNew) { lang.strings.push(lang.isStatic ? { value } : { key, value }); } else { if (!lang.isStatic) lang.strings[strIdx].key = key; lang.strings[strIdx].value = value; } setLoc(newLoc); setIsLocEditModalOpen(null); showNotification(isNew ? "String Added" : "String Updated"); }; const handleLocStringDelete = (langIdx: number, strIdx: number) => { if (!loc) return; playBackSound(); const newLoc = { ...loc }; newLoc.languages[langIdx].strings.splice(strIdx, 1); setLoc(newLoc); showNotification("String Deleted"); }; return (

ARC Editor

{arc && editing: {arc.name}}
{!arc ? (

Open an ARC file to begin editing

) : (
{activeTab === "arc" ? (
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" />
{filteredEntries.map((entry) => ( ))}
Filename Offset Size Flags Actions
{entry.filename} 0x{entry.ptr.toString(16).toUpperCase().padStart(8, '0')} {(entry.size / 1024).toFixed(1)} KB {entry.isCompressed && ( zlib )}
) : (
{!loc ? (

No languages.loc found in archive

) : (
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" />
{filteredLocStrings.map((str) => ( ))}
Key / Index Value Actions
{currentLocLang?.isStatic ? str.originalIdx : str.key} {str.value}
)}
)}
)}
{notification && ( {notification.message} )} {isAddModalOpen && (

Add File to Archive

)} {isReplaceModalOpen && selectedEntryIdx !== null && (

Replace File Data

{arc?.entries[selectedEntryIdx].filename}

)} {isRenameModalOpen && selectedEntryIdx !== null && ( setIsRenameModalOpen(false)} onConfirm={handleRenameEntry} /> )} {isLocEditModalOpen && ( setIsLocEditModalOpen(null)} onConfirm={handleLocStringEdit} /> )}
); } function RenameModal({ initialName, initialCompressed, onClose, onConfirm }: { initialName: string, initialCompressed: boolean, onClose: () => void, onConfirm: (name: string, comp: boolean) => void }) { const [name, setName] = useState(initialName); const [comp, setComp] = useState(initialCompressed); return (

Rename Entry

setName(e.target.value)} className="w-full bg-black/40 border-2 border-[#373737] text-white px-4 py-3 outline-none focus:border-[#FFFF55] transition-colors" />
); } function LocEditModal({ data, lang, onClose, onConfirm }: { data: { langIdx: number, strIdx: number, isNew: boolean }, lang: LocLanguage, onClose: () => void, onConfirm: (langIdx: number, strIdx: number, isNew: boolean, key: string, val: string) => void }) { const [key, setKey] = useState(!data.isNew ? (lang.strings[data.strIdx].key || "") : ""); const [val, setVal] = useState(!data.isNew ? lang.strings[data.strIdx].value : ""); return (

{data.isNew ? "Add" : "Edit"} String

{!lang.isStatic ? (
setKey(e.target.value)} className="w-full bg-black/40 border-2 border-[#373737] text-white px-4 py-3 outline-none focus:border-[#FFFF55] transition-colors font-mono" />
) : (
Static entry - Index: {data.isNew ? lang.strings.length : data.strIdx}
)}