diff --git a/src/components/views/DevtoolsView.tsx b/src/components/views/DevtoolsView.tsx index ce73030..44b948e 100644 --- a/src/components/views/DevtoolsView.tsx +++ b/src/components/views/DevtoolsView.tsx @@ -12,7 +12,7 @@ interface DevTool { const DEV_TOOLS: DevTool[] = [ { id: "pck", name: "PCK Editor", view: "pck-editor", comingSoon: false }, { id: "arc", name: "ARC Editor", view: "arc-editor", comingSoon: false }, - { id: "loc", name: "LOC Editor", view: "devtools", comingSoon: false } + { id: "loc", name: "LOC Editor", view: "loc-editor", comingSoon: false } ]; export default function DevtoolsView() { diff --git a/src/components/views/LocEditorView.tsx b/src/components/views/LocEditorView.tsx new file mode 100644 index 0000000..dcce7a8 --- /dev/null +++ b/src/components/views/LocEditorView.tsx @@ -0,0 +1,272 @@ +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 { LocFile, LocLanguage } from "../../types/arc"; +export default function LocEditorView() { + const { setActiveView } = useUI(); + const { playPressSound, playBackSound } = useAudio(); + const { animationsEnabled } = useConfig(); + const [loc, setLoc] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const [selectedLocLangIdx, setSelectedLocLangIdx] = useState(0); + const [notification, setNotification] = useState<{ message: string, type: "success" | "error" } | null>(null); + const [isLocEditModalOpen, setIsLocEditModalOpen] = useState<{ langIdx: number, strIdx: number, isNew: boolean } | null>(null); + const fileInputRef = useRef(null); + 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 parsedLoc = ArcService.parseLOC(new Uint8Array(buffer)); + setLoc(parsedLoc); + setSelectedLocLangIdx(0); + showNotification(`Loaded ${file.name}`); + } catch (err) { + console.error("Failed to parse LOC", err); + showNotification("Failed to parse LOC", "error"); + } + e.target.value = ""; + }; + + const handleSaveLoc = () => { + if (!loc) return; + playPressSound(); + const buffer = ArcService.serializeLOC(loc); + const blob = new Blob([buffer as any]); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "languages.loc"; + a.click(); + URL.revokeObjectURL(url); + showNotification("LOC Saved Successfully"); + }; + + 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 ( + + +
+
+

LOC Editor

+ {loc && editing loaded loc} +
+
+ + +
+
+ + {!loc ? ( +
+ +

Open a LOC 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" + /> + +
+
+ + + + + + + + + + {filteredLocStrings.map((str) => ( + + + + + + ))} + +
Key / IndexValueActions
+ {currentLocLang?.isStatic ? str.originalIdx : str.key} + {str.value} +
+ + +
+
+
+
+
+ )} +
+ +
+ + {notification && ( + + + {notification.message} + + + )} + + {isLocEditModalOpen && ( + setIsLocEditModalOpen(null)} + onConfirm={handleLocStringEdit} + /> + )} +
+ ); +} + +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}
+ )} +
+ +