feat: add generic GFF visual editor component
This commit is contained in:
@@ -1,17 +1,28 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { MonacoEditor } from "../components/editor/MonacoEditor";
|
||||
import { EditorTabs } from "../components/editor/EditorTabs";
|
||||
import { GffEditor } from "../components/gff/GffEditor";
|
||||
|
||||
const GFF_EXTENSIONS = [".uti.json", ".utc.json", ".are.json", ".dlg.json", ".utp.json", ".utm.json"];
|
||||
|
||||
function isGffFile(tabKey: string): boolean {
|
||||
return GFF_EXTENSIONS.some((ext) => tabKey.endsWith(ext));
|
||||
}
|
||||
|
||||
function repoFromTabKey(tabKey: string): string {
|
||||
const idx = tabKey.indexOf(":");
|
||||
return idx > 0 ? tabKey.slice(0, idx) : "";
|
||||
}
|
||||
|
||||
function filePathFromTabKey(tabKey: string): string {
|
||||
const idx = tabKey.indexOf(":");
|
||||
return idx > 0 ? tabKey.slice(idx + 1) : tabKey;
|
||||
}
|
||||
|
||||
interface EditorProps {
|
||||
editorState: ReturnType<typeof import("../hooks/useEditorState").useEditorState>;
|
||||
}
|
||||
|
||||
function displayName(tabKey: string): string {
|
||||
const parts = tabKey.split(":");
|
||||
const filePath = parts.length > 1 ? parts.slice(1).join(":") : tabKey;
|
||||
return filePath.split("/").pop() ?? filePath;
|
||||
}
|
||||
|
||||
export function Editor({ editorState }: EditorProps) {
|
||||
const {
|
||||
openTabs,
|
||||
@@ -21,8 +32,12 @@ export function Editor({ editorState }: EditorProps) {
|
||||
closeFile,
|
||||
updateContent,
|
||||
getContent,
|
||||
markClean,
|
||||
} = editorState;
|
||||
|
||||
// Track per-tab editor mode: "visual" or "raw". GFF files default to visual.
|
||||
const [editorModes, setEditorModes] = useState<Record<string, "visual" | "raw">>({});
|
||||
|
||||
const tabs = useMemo(
|
||||
() =>
|
||||
openTabs.map((path: string) => ({
|
||||
@@ -33,11 +48,13 @@ export function Editor({ editorState }: EditorProps) {
|
||||
);
|
||||
|
||||
const activeContent = activeTab ? (getContent(activeTab) ?? "") : "";
|
||||
const activeFilePath = activeTab
|
||||
? activeTab.includes(":")
|
||||
? activeTab.split(":").slice(1).join(":")
|
||||
: activeTab
|
||||
: "";
|
||||
const activeFilePath = activeTab ? filePathFromTabKey(activeTab) : "";
|
||||
const activeRepo = activeTab ? repoFromTabKey(activeTab) : "";
|
||||
|
||||
const isActiveGff = activeTab ? isGffFile(activeTab) : false;
|
||||
const activeMode = activeTab
|
||||
? editorModes[activeTab] ?? (isActiveGff ? "visual" : "raw")
|
||||
: "raw";
|
||||
|
||||
const handleChange = useCallback(
|
||||
(value: string) => {
|
||||
@@ -48,6 +65,80 @@ export function Editor({ editorState }: EditorProps) {
|
||||
[activeTab, updateContent],
|
||||
);
|
||||
|
||||
const handleSwitchToRaw = useCallback(() => {
|
||||
if (activeTab) {
|
||||
setEditorModes((prev) => ({ ...prev, [activeTab]: "raw" }));
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
const handleSwitchToVisual = useCallback(() => {
|
||||
if (activeTab) {
|
||||
setEditorModes((prev) => ({ ...prev, [activeTab]: "visual" }));
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
const handleGffSave = useCallback(
|
||||
(newContent: string) => {
|
||||
if (activeTab) {
|
||||
updateContent(activeTab, newContent);
|
||||
markClean(activeTab);
|
||||
}
|
||||
},
|
||||
[activeTab, updateContent, markClean],
|
||||
);
|
||||
|
||||
const renderEditor = () => {
|
||||
if (!activeTab) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p style={{ color: "var(--forge-text-secondary)" }} className="text-lg">
|
||||
Open a file from the File Explorer to start editing
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isActiveGff && activeMode === "visual") {
|
||||
return (
|
||||
<GffEditor
|
||||
key={activeTab}
|
||||
repo={activeRepo}
|
||||
filePath={activeFilePath}
|
||||
content={activeContent}
|
||||
onSave={handleGffSave}
|
||||
onSwitchToRaw={handleSwitchToRaw}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{isActiveGff && activeMode === "raw" && (
|
||||
<div
|
||||
className="flex shrink-0 items-center justify-end border-b px-4 py-1"
|
||||
style={{ borderColor: "var(--forge-border)", backgroundColor: "var(--forge-surface)" }}
|
||||
>
|
||||
<button
|
||||
onClick={handleSwitchToVisual}
|
||||
className="rounded px-3 py-1 text-xs transition-colors hover:opacity-80"
|
||||
style={{ backgroundColor: "var(--forge-bg)", color: "var(--forge-text-secondary)" }}
|
||||
>
|
||||
Switch to Visual Editor
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<MonacoEditor
|
||||
key={activeTab}
|
||||
filePath={activeFilePath}
|
||||
content={activeContent}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col" style={{ backgroundColor: "var(--forge-bg)" }}>
|
||||
<EditorTabs
|
||||
@@ -57,20 +148,7 @@ export function Editor({ editorState }: EditorProps) {
|
||||
onClose={closeFile}
|
||||
/>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{activeTab ? (
|
||||
<MonacoEditor
|
||||
key={activeTab}
|
||||
filePath={activeFilePath}
|
||||
content={activeContent}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p style={{ color: "var(--forge-text-secondary)" }} className="text-lg">
|
||||
Open a file from the File Explorer to start editing
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{renderEditor()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user