Files
behavior3editor/src/components/workspace.tsx
T
2024-07-23 17:44:18 +08:00

433 lines
14 KiB
TypeScript

import { useSetting } from "@/contexts/setting-context";
import { EditorStore, useWorkspace } from "@/contexts/workspace-context";
import { modal } from "@/misc/hooks";
import { Hotkey, isHotkeyPressed, isMacos, setInputFocus, useHotkeys } from "@/misc/keys";
import Path from "@/misc/path";
import { app } from "@electron/remote";
import { Button, Flex, Layout, Space, Tabs, Tag, Tooltip } from "antd";
import { FC, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { FaExclamationTriangle } from "react-icons/fa";
import { PiTreeStructureFill } from "react-icons/pi";
import { VscNewFolder, VscRepo } from "react-icons/vsc";
import useForceUpdate from "use-force-update";
import { useWindowSize } from "usehooks-ts";
import { Editor } from "./editor";
import { Explorer } from "./explorer";
import { Inspector } from "./inspector";
import { TitleBar } from "./titlebar";
const { Header, Content, Sider } = Layout;
export const Workspace: FC = () => {
const workspace = {
buildProject: useWorkspace((state) => state.buildProject),
close: useWorkspace((state) => state.close),
createProject: useWorkspace((state) => state.createProject),
edit: useWorkspace((state) => state.edit),
editing: useWorkspace((state) => state.editing),
editors: useWorkspace((state) => state.editors),
fileTree: useWorkspace((state) => state.fileTree),
find: useWorkspace((state) => state.find),
init: useWorkspace((state) => state.init),
isShowingSearch: useWorkspace((state) => state.isShowingSearch),
onShowingSearch: useWorkspace((state) => state.onShowingSearch),
openProject: useWorkspace((state) => state.openProject),
save: useWorkspace((state) => state.save),
};
const settings = {
recent: useSetting((state) => state.recent),
};
const [isShowingSave, setShowingSave] = useState(false);
const { t } = useTranslation();
const forceUpdate = useForceUpdate();
const { width = 0, height = 0 } = useWindowSize();
const keysRef = useHotkeys<HTMLDivElement>(
[
Hotkey.Save,
Hotkey.CloseEditor,
Hotkey.SearchTree,
Hotkey.Build,
Hotkey.SearchNode,
Hotkey.JumpNode,
],
(event) => {
if (isHotkeyPressed(Hotkey.Save)) {
event.preventDefault();
workspace.save();
} else if (isHotkeyPressed(Hotkey.CloseEditor)) {
if (workspace.editing) {
event.preventDefault();
if (workspace.editing.unsave) {
showSaveDialog(workspace.editing);
} else {
workspace.close(workspace.editing.path);
}
}
keysRef.current?.focus();
} else if (isHotkeyPressed(Hotkey.SearchTree)) {
event.preventDefault();
workspace.onShowingSearch(true);
} else if (isHotkeyPressed(Hotkey.Build)) {
event.preventDefault();
workspace.buildProject();
} else if (isHotkeyPressed(Hotkey.SearchNode)) {
event.preventDefault();
workspace.editing?.dispatch("searchNode");
} else if (isHotkeyPressed(Hotkey.JumpNode)) {
event.preventDefault();
workspace.editing?.dispatch("jumpNode");
}
}
);
useEffect(() => {
if (!workspace.isShowingSearch && !isShowingSave) {
keysRef.current?.focus();
}
}, [workspace.isShowingSearch]);
const showSaveDialog = (editor: EditorStore) => {
if (isShowingSave) {
return;
}
setShowingSave(true);
const alert = modal.confirm({
centered: true,
content: (
<Flex vertical gap="middle">
<div>
<FaExclamationTriangle style={{ fontSize: "60px", color: "#FADB14" }} />
</div>
<div>{t("workspace.saveOnClose", { name: Path.basename(editor.path) })}</div>
</Flex>
),
footer: (
<Flex vertical gap="middle" style={{ paddingTop: "30px" }}>
<Flex vertical gap="6px">
<Button
type="primary"
onClick={() => {
editor.dispatch("save");
workspace.close(editor.path);
alert.destroy();
keysRef.current?.focus();
setShowingSave(false);
}}
>
{t("save")}
</Button>
<Button
danger
onClick={() => {
workspace.close(editor.path);
alert.destroy();
keysRef.current?.focus();
setShowingSave(false);
}}
>
{t("donotSave")}
</Button>
</Flex>
<Button
onClick={() => {
alert.destroy();
keysRef.current?.focus();
setShowingSave(false);
}}
>
{t("cancel")}
</Button>
</Flex>
),
});
};
const showSaveAllDialog = (unsaves: EditorStore[]) => {
if (isShowingSave) {
return;
}
setShowingSave(true);
const alert = modal.confirm({
centered: true,
content: (
<Flex vertical gap="middle">
<div>
<FaExclamationTriangle style={{ fontSize: "60px", color: "#FADB14" }} />
</div>
{unsaves.length === 1 && (
<div>{t("workspace.saveOnClose", { name: Path.basename(unsaves[0].path) })}</div>
)}
{unsaves.length > 1 && (
<>
<div>{t("workspace.saveAllOnClose", { count: unsaves.length })}</div>
<Flex vertical style={{ fontSize: "12px" }}>
{unsaves.map((editor) => (
<div>{Path.basename(editor.path)}</div>
))}
</Flex>
</>
)}
</Flex>
),
footer: (
<Flex vertical gap="middle" style={{ paddingTop: "30px" }}>
<Flex vertical gap="6px">
<Button
type="primary"
onClick={() => {
unsaves.forEach((editor) => editor.dispatch("save"));
alert.destroy();
window.close();
setShowingSave(false);
}}
>
{unsaves.length > 1 ? t("saveAll") : t("save")}
</Button>
<Button
danger
onClick={() => {
workspace.editors.length = 0;
alert.destroy();
window.close();
setShowingSave(false);
}}
>
{t("donotSave")}
</Button>
</Flex>
<Button
onClick={() => {
alert.destroy();
setShowingSave(false);
}}
>
{t("cancel")}
</Button>
</Flex>
),
});
};
window.onbeforeunload = (e) => {
const unsaves = workspace.editors.filter((editor) => editor.unsave);
if (unsaves.length) {
showSaveAllDialog(unsaves);
return false;
}
};
return (
<Layout
className="b3-workspace"
tabIndex={-1}
ref={keysRef}
style={{ width: width, height: height }}
>
<Header
style={{
padding: "0px",
height: "fit-content",
borderBottom: `1px solid var(--b3-color-border)`,
}}
>
<TitleBar />
</Header>
<Layout
hasSider
style={{ overflow: "hidden" }}
onFocus={(e) => {
setInputFocus(e.target);
}}
>
{workspace.fileTree && (
<Sider
width={300}
style={{
height: "100%",
borderRight: `1px solid var(--b3-color-border)`,
}}
>
<Explorer />
</Sider>
)}
<Content>
{!workspace.fileTree && (
<Flex vertical align="center" style={{ height: "100%" }}>
<Flex
vertical
style={{
fontSize: "15px",
width: "fit-content",
height: "100%",
paddingTop: "50px",
paddingBottom: "50px",
}}
>
<Flex
align="center"
gap="5px"
style={{ fontSize: "40px", fontWeight: "600", marginBottom: "10px" }}
>
<PiTreeStructureFill size="50px" />
<div>Behavior3 Editor</div>
</Flex>
<Flex vertical style={{ paddingLeft: "55px", paddingBottom: "15px" }}>
<div style={{ fontSize: "22px", fontWeight: "500" }}>{t("start")}</div>
<Flex
align="center"
gap="5px"
style={{
width: "fit-content",
fontWeight: "500",
cursor: "pointer",
color: "var(--ant-color-primary)",
}}
onClick={() => workspace.createProject()}
>
<VscNewFolder size="20px" />
{t("createProject")}
</Flex>
<Flex
align="center"
gap="5px"
style={{
fontWeight: "500",
width: "fit-content",
cursor: "pointer",
color: "var(--ant-color-primary)",
}}
onClick={() => workspace.openProject()}
>
<VscRepo size="19px" />
{t("openProject")}
</Flex>
</Flex>
<Flex vertical style={{ paddingLeft: "55px" }}>
<div style={{ fontSize: "22px", fontWeight: "500" }}>{t("recent")}</div>
<div style={{ overflow: "auto", height: "100%" }}>
{settings.recent.map((path) => {
const homedir = app.getPath("home");
return (
<div key={path}>
<span
onClick={() => workspace.openProject(path)}
style={{
color: "var(--ant-color-primary)",
fontWeight: "500",
cursor: "pointer",
marginRight: "15px",
}}
>
{Path.basename(path)}
</span>
<span>{Path.dirname(path).replace(homedir, "~")}</span>
</div>
);
})}
</div>
</Flex>
</Flex>
</Flex>
)}
{workspace.editors.length === 0 && (
<Flex vertical align="center" justify="center" style={{ height: "100%" }}>
<Flex
vertical
gap="10px"
style={{
color: "gray",
fontSize: "15px",
width: "fit-content",
paddingTop: "50px",
marginRight: "150px",
paddingBottom: "50px",
}}
>
{[
{ label: t("searchFile"), hotkeys: isMacos ? "⌘ P" : "Ctrl + P" },
{ label: t("build"), hotkeys: isMacos ? "⌘ B" : "Ctrl + B" },
{ label: t("searchNode"), hotkeys: isMacos ? "⌘ F" : "Ctrl + F" },
{ label: t("jumpNode"), hotkeys: isMacos ? "⌘ G" : "Ctrl + G" },
{ label: t("insertNode"), hotkeys: "Enter" },
{ label: t("deleteNode"), hotkeys: "Backspace" },
].map((v) => (
<Flex
key={v.label}
gap="20px"
style={{
minWidth: "200px",
}}
>
<span style={{ width: "150px", textAlign: "end" }}>{v.label}</span>
<Space size={5}>
{v.hotkeys.split(" ").map((key, index) => (
<div key={index}>
{key === "+" && <div>+</div>}
{key !== "+" && (
<Tag style={{ color: "gray", fontSize: "14px", marginRight: 0 }}>
{key}
</Tag>
)}
</div>
))}
</Space>
</Flex>
))}
</Flex>
</Flex>
)}
{workspace.editors.length > 0 && (
<Tabs
hideAdd
type="editable-card"
activeKey={workspace.editing?.path}
onEdit={(activeKey, action) => {
if (action === "remove") {
const path = activeKey as string;
const editor = workspace.find(path);
if (editor && editor.unsave) {
showSaveDialog(editor);
} else {
workspace.close(path);
}
}
}}
onChange={(activeKey) => {
workspace.edit(activeKey);
}}
items={workspace.editors.map((v) => {
return {
label: (
<Tooltip
arrow={false}
placement="bottom"
mouseEnterDelay={1}
// mouseLeaveDelay={100}
color="#010409"
overlayStyle={{ userSelect: "none", WebkitUserSelect: "none" }}
autoAdjustOverflow={true}
overlayInnerStyle={{
width: "fit-content",
border: "1px solid var(--b3-color-border)",
borderRadius: "4px",
}}
title={<div style={{ width: "max-content" }}>{v.path}</div>}
>
{`${Path.basename(v.path)}${v.unsave ? "*" : ""}`}
</Tooltip>
),
key: v.path,
children: <Editor data={v} onUpdate={forceUpdate} />,
};
})}
/>
)}
</Content>
<Inspector />
</Layout>
</Layout>
);
};