add save and reset button to playground
This commit is contained in:
parent
d04f61752d
commit
87f911e055
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useMonaco, type Monaco } from "@monaco-editor/react";
|
import { useMonaco, type Monaco } from "@monaco-editor/react";
|
||||||
import { useImmer, type ImmerHook, type Updater } from "use-immer";
|
import { useImmer, type Updater } from "use-immer";
|
||||||
|
|
||||||
import "@styles/playground.scss";
|
import "@styles/playground.scss";
|
||||||
|
|
||||||
|
@ -20,49 +20,85 @@ export type Directory = {
|
||||||
};
|
};
|
||||||
export type SetState<T> = React.Dispatch<React.SetStateAction<T>>;
|
export type SetState<T> = React.Dispatch<React.SetStateAction<T>>;
|
||||||
|
|
||||||
|
const FILE_STORAGE_KEY = "playground-files";
|
||||||
|
const DEFAULT_FILES = {
|
||||||
|
dirs: {
|
||||||
|
src: {
|
||||||
|
files: {
|
||||||
|
"main.shu": {
|
||||||
|
content: mainFileContent,
|
||||||
|
language: "shulkerscript",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dist: {},
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
"pack.toml": {
|
||||||
|
content: packTomlContent,
|
||||||
|
language: "toml",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function Playground() {
|
export default function Playground() {
|
||||||
const [rootDir, updateRootDir] = useImmer({
|
const [rootDir, updateRootDir] = useImmer(
|
||||||
dirs: {
|
getStorageOrDefault(FILE_STORAGE_KEY, DEFAULT_FILES) as Directory
|
||||||
src: {
|
);
|
||||||
files: {
|
|
||||||
"main.shu": {
|
|
||||||
content: mainFileContent,
|
|
||||||
language: "shulkerscript",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dist: {
|
|
||||||
files: {
|
|
||||||
"test.mcfunction": {
|
|
||||||
content: "",
|
|
||||||
language: "mcfunction",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
files: {
|
|
||||||
"pack.toml": {
|
|
||||||
content: packTomlContent,
|
|
||||||
language: "toml",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as Directory);
|
|
||||||
|
|
||||||
const [fileName, setFileName] = useState("src/main.shu");
|
const [fileName, setFileName] = useState("src/main.shu");
|
||||||
const file = getFile(rootDir, fileName)!;
|
const file = getFile(rootDir, fileName);
|
||||||
|
|
||||||
const onBuild: () => void = () => {
|
const onBuild = () => {
|
||||||
if (monaco) {
|
if (monaco) {
|
||||||
console.log(getFiles(monaco));
|
console.log(getFiles(monaco));
|
||||||
} else {
|
} else {
|
||||||
console.error("monaco has not loaded");
|
console.error("monaco has not loaded");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onZip: () => void = () => {
|
const onZip = () => {
|
||||||
if (monaco) {
|
if (monaco) {
|
||||||
loadFile(monaco, updateRootDir, {content: "zip"}, "dist/pack.zip");
|
loadFile(
|
||||||
|
monaco,
|
||||||
|
updateRootDir,
|
||||||
|
{ content: "zip" },
|
||||||
|
"dist/pack.zip"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error("onZip not set");}
|
console.error("onZip not set");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onSave = () => {
|
||||||
|
if (monaco) {
|
||||||
|
const currentFiles = getFiles(monaco);
|
||||||
|
updateRootDir((dir) => {
|
||||||
|
dir.dirs = currentFiles.dirs;
|
||||||
|
dir.files = currentFiles.files;
|
||||||
|
});
|
||||||
|
window.localStorage.setItem(
|
||||||
|
FILE_STORAGE_KEY,
|
||||||
|
JSON.stringify(currentFiles)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onReset = () => {
|
||||||
|
if (monaco) {
|
||||||
|
monaco.editor.getModels().forEach((model) => {
|
||||||
|
if (model.uri.path != "/src/main.shu") {
|
||||||
|
model.dispose();
|
||||||
|
} else {
|
||||||
|
model.setValue(mainFileContent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateRootDir((dir) => {
|
||||||
|
dir.dirs = DEFAULT_FILES.dirs;
|
||||||
|
dir.files = DEFAULT_FILES.files;
|
||||||
|
});
|
||||||
|
|
||||||
|
loadFiles(monaco, updateRootDir, DEFAULT_FILES);
|
||||||
|
setFileName("src/main.shu");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const monaco = useMonaco();
|
const monaco = useMonaco();
|
||||||
|
@ -82,14 +118,19 @@ export default function Playground() {
|
||||||
marginTop: "0.5cm",
|
marginTop: "0.5cm",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Header onBuild={onBuild} onZip={onZip} />
|
<Header
|
||||||
|
onSave={onSave}
|
||||||
|
onReset={onReset}
|
||||||
|
onBuild={onBuild}
|
||||||
|
onZip={onZip}
|
||||||
|
/>
|
||||||
<FileView
|
<FileView
|
||||||
className="file-view"
|
className="file-view"
|
||||||
root={rootDir}
|
root={rootDir}
|
||||||
fileName={fileName}
|
fileName={fileName}
|
||||||
setSelectedFileName={setFileName}
|
setSelectedFileName={setFileName}
|
||||||
/>
|
/>
|
||||||
<Editor fileName={fileName} file={file} />
|
<Editor fileName={fileName} file={file ?? undefined} />
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -111,7 +152,7 @@ function getFiles(monaco: Monaco): Directory {
|
||||||
dir.dirs[part] = {};
|
dir.dirs[part] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
dir = dir.dirs[part].files ?? {};
|
dir = dir.dirs[part];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dir.files) {
|
if (!dir.files) {
|
||||||
|
@ -147,12 +188,20 @@ function getFile(root: Directory, path: string): File | null {
|
||||||
return root.files?.[path] ?? null;
|
return root.files?.[path] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadFiles(monaco: Monaco, updater: Updater<Directory>, dir: Directory, prefix = "") {
|
function loadFiles(
|
||||||
|
monaco: Monaco,
|
||||||
|
updater: Updater<Directory>,
|
||||||
|
dir: Directory,
|
||||||
|
prefix = ""
|
||||||
|
) {
|
||||||
for (const [name, d] of Object.entries(dir.dirs ?? {})) {
|
for (const [name, d] of Object.entries(dir.dirs ?? {})) {
|
||||||
loadFiles(monaco, updater, d, prefix + name + "/");
|
loadFiles(monaco, updater, d, prefix + name + "/");
|
||||||
updater(dir => {
|
updater((dir) => {
|
||||||
let current = dir;
|
let current = dir;
|
||||||
for(const part of [...prefix.split("/").filter(s => s !== ""), name]) {
|
for (const part of [
|
||||||
|
...prefix.split("/").filter((s) => s !== ""),
|
||||||
|
name,
|
||||||
|
]) {
|
||||||
if (!current.dirs) {
|
if (!current.dirs) {
|
||||||
current.dirs = {};
|
current.dirs = {};
|
||||||
}
|
}
|
||||||
|
@ -166,17 +215,21 @@ function loadFiles(monaco: Monaco, updater: Updater<Directory>, dir: Directory,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadFile(monaco: Monaco, updater: Updater<Directory>, file: File, name: string) {
|
function loadFile(
|
||||||
|
monaco: Monaco,
|
||||||
|
updater: Updater<Directory>,
|
||||||
|
file: File,
|
||||||
|
name: string
|
||||||
|
) {
|
||||||
const uri = monaco.Uri.parse(name);
|
const uri = monaco.Uri.parse(name);
|
||||||
if (!monaco.editor.getModel(uri)) {
|
if (!monaco.editor.getModel(uri)) {
|
||||||
monaco.editor.createModel(file.content, file.language, uri);
|
monaco.editor.createModel(file.content, file.language, uri);
|
||||||
}
|
}
|
||||||
updater(dir => {
|
updater((dir) => {
|
||||||
let current = dir;
|
let current = dir;
|
||||||
const parts = name.split("/").filter(s => s !== "");
|
const parts = name.split("/").filter((s) => s !== "");
|
||||||
const last = parts.pop()!;
|
const last = parts.pop()!;
|
||||||
for(const part of parts) {
|
for (const part of parts) {
|
||||||
console.log(part);
|
|
||||||
if (!current.dirs) {
|
if (!current.dirs) {
|
||||||
current.dirs = {};
|
current.dirs = {};
|
||||||
}
|
}
|
||||||
|
@ -189,4 +242,11 @@ function loadFile(monaco: Monaco, updater: Updater<Directory>, file: File, name:
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getStorageOrDefault(key: string, def: any) {
|
||||||
|
const item = window.localStorage.getItem(key);
|
||||||
|
if (item) {
|
||||||
|
return JSON.parse(item);
|
||||||
|
} else {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,14 +12,14 @@ import { ThemeProvider } from "@mui/material";
|
||||||
|
|
||||||
import { customTheme } from "@utils/material-ui-theme";
|
import { customTheme } from "@utils/material-ui-theme";
|
||||||
|
|
||||||
export default function SplitButton({
|
export default function DropdownButton({
|
||||||
options,
|
options,
|
||||||
children,
|
visible,
|
||||||
onClick,
|
style,
|
||||||
}: {
|
}: {
|
||||||
options: [string, React.MouseEventHandler<HTMLLIElement>][];
|
options: [string, React.MouseEventHandler<HTMLLIElement>][];
|
||||||
children: React.ReactNode;
|
visible: [React.ReactNode, React.MouseEventHandler<HTMLButtonElement>][];
|
||||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
style?: React.CSSProperties;
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const anchorRef = React.useRef<HTMLDivElement>(null);
|
const anchorRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
@ -53,10 +53,11 @@ export default function SplitButton({
|
||||||
variant="contained"
|
variant="contained"
|
||||||
ref={anchorRef}
|
ref={anchorRef}
|
||||||
aria-label="Button group with a nested menu"
|
aria-label="Button group with a nested menu"
|
||||||
|
style={style}
|
||||||
>
|
>
|
||||||
<Button onClick={onClick}>
|
{visible.map(([children, onClick], index) => {
|
||||||
{children}
|
return <Button key={index} onClick={onClick}>{children}</Button>;
|
||||||
</Button>
|
})}
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
aria-controls={open ? "split-button-menu" : undefined}
|
aria-controls={open ? "split-button-menu" : undefined}
|
|
@ -13,7 +13,7 @@ export default function Editor({
|
||||||
file,
|
file,
|
||||||
}: {
|
}: {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
file: File;
|
file?: File;
|
||||||
}) {
|
}) {
|
||||||
const [highlighter, setHighlighter] = useState<Highlighter | null>(null);
|
const [highlighter, setHighlighter] = useState<Highlighter | null>(null);
|
||||||
|
|
||||||
|
@ -48,8 +48,8 @@ export default function Editor({
|
||||||
height="70vh"
|
height="70vh"
|
||||||
theme="vs-dark"
|
theme="vs-dark"
|
||||||
path={fileName}
|
path={fileName}
|
||||||
defaultLanguage={file.language}
|
defaultLanguage={file?.language}
|
||||||
defaultValue={file.content}
|
defaultValue={file?.content}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,15 +1,37 @@
|
||||||
import SplitButton from "./SplitButton";
|
import DropdownButton from "./DropdownButton";
|
||||||
|
|
||||||
export default function Header({onBuild, onZip}: {onBuild: () => void; onZip: () => void;}){
|
|
||||||
|
|
||||||
|
export default function Header({
|
||||||
|
onSave,
|
||||||
|
onReset,
|
||||||
|
onBuild,
|
||||||
|
onZip,
|
||||||
|
}: {
|
||||||
|
onSave: () => void;
|
||||||
|
onReset: () => void;
|
||||||
|
onBuild: () => void;
|
||||||
|
onZip: () => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<header style={{
|
<header
|
||||||
display: "flex",
|
style={{
|
||||||
justifyContent: "space-between",
|
display: "flex",
|
||||||
marginBottom: "0.5cm",
|
justifyContent: "space-between",
|
||||||
}}>
|
marginBottom: "0.5cm",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<h1 id="_top">Playground</h1>
|
<h1 id="_top">Playground</h1>
|
||||||
<SplitButton onClick={onBuild} options={[["Download zip", onZip]]}>Build</SplitButton>
|
<div className="buttons" style={{ height: "100%" }}>
|
||||||
|
<DropdownButton
|
||||||
|
style={{ height: "100%", marginRight: "0.5cm"}}
|
||||||
|
visible={[["Save", onSave]]}
|
||||||
|
options={[["Reset", onReset]]}
|
||||||
|
/>
|
||||||
|
<DropdownButton
|
||||||
|
style={{ height: "100%" }}
|
||||||
|
visible={[["Build", onBuild]]}
|
||||||
|
options={[["Download zip", onZip]]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue