mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-26 09:33:49 +01:00
Added the ability to open multiple script files at a time
Fixed a bug where ram would stay the same when switching between scripts Fixed bug where updating ram could cause the GUI to crash The editor will now go to the last edited line when switching tabs. Tidied up the script editor UI
This commit is contained in:
parent
eb002d655a
commit
2265f797c4
@ -1,7 +1,8 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||
import Editor from "@monaco-editor/react";
|
||||
import Editor, { Monaco } from "@monaco-editor/react";
|
||||
import * as monaco from "monaco-editor";
|
||||
type IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor;
|
||||
type ITextModel = monaco.editor.ITextModel;
|
||||
import { OptionsModal } from "./OptionsModal";
|
||||
import { Options } from "./Options";
|
||||
import { isValidFilePath } from "../../Terminal/DirectoryHelpers";
|
||||
@ -34,6 +35,10 @@ import IconButton from "@mui/material/IconButton";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
|
||||
import libSource from "!!raw-loader!../NetscriptDefinitions.d.ts";
|
||||
import { cssNumber } from "cypress/types/jquery";
|
||||
import { buttonBaseClasses } from "@mui/material";
|
||||
import { fromPairs } from "cypress/types/lodash";
|
||||
import { StringMatcher } from "cypress/types/net-stubbing";
|
||||
|
||||
let symbolsLoaded = false;
|
||||
let symbols: string[] = [];
|
||||
@ -80,22 +85,32 @@ interface IProps {
|
||||
// https://github.com/threehams/typescript-error-guide/blob/master/stories/components/Editor.tsx#L11-L39
|
||||
// https://blog.checklyhq.com/customizing-monaco/
|
||||
|
||||
// These variables are used to reload a script when it's clicked on. Because we
|
||||
// won't have references to the old script.
|
||||
let lastFilename = "";
|
||||
let lastCode = "";
|
||||
let hostname = "";
|
||||
let lastPosition: monaco.Position | null = null;
|
||||
// Holds all the data for a open script
|
||||
class openScript {
|
||||
fileName: string;
|
||||
code: string;
|
||||
hostname: string;
|
||||
lastPosition: monaco.Position;
|
||||
model: ITextModel;
|
||||
|
||||
constructor(fileName: string, code: string, hostname: string, lastPosition: monaco.Position, model: ITextModel) {
|
||||
this.fileName = fileName;
|
||||
this.code = code;
|
||||
this.hostname = hostname;
|
||||
this.lastPosition = lastPosition;
|
||||
this.model = model;
|
||||
}
|
||||
}
|
||||
|
||||
const openScripts = new Array<openScript>(); // Holds all open scripts
|
||||
let currentScript = {} as openScript; // Script currently being viewed
|
||||
|
||||
export function Root(props: IProps): React.ReactElement {
|
||||
const editorRef = useRef<IStandaloneCodeEditor | null>(null);
|
||||
const [filename, setFilename] = useState(props.filename ? props.filename : lastFilename);
|
||||
const [code, setCode] = useState<string>(props.filename ? props.code : lastCode);
|
||||
const monacoRef = useRef<Monaco | null>(null);
|
||||
const [filename, setFilename] = useState(props.filename);
|
||||
const [code, setCode] = useState<string>(props.code);
|
||||
const [decorations, setDecorations] = useState<string[]>([]);
|
||||
hostname = props.filename ? props.hostname : hostname;
|
||||
if (hostname === "") {
|
||||
hostname = props.player.getCurrentServer().hostname;
|
||||
}
|
||||
const [ram, setRAM] = useState("RAM: ???");
|
||||
const [updatingRam, setUpdatingRam] = useState(false);
|
||||
const [optionsOpen, setOptionsOpen] = useState(false);
|
||||
@ -105,6 +120,8 @@ export function Root(props: IProps): React.ReactElement {
|
||||
fontSize: Settings.MonacoFontSize,
|
||||
});
|
||||
|
||||
|
||||
|
||||
const debouncedSetRAM = useMemo(
|
||||
() =>
|
||||
debounce((s) => {
|
||||
@ -114,54 +131,21 @@ export function Root(props: IProps): React.ReactElement {
|
||||
[],
|
||||
);
|
||||
|
||||
// store the last known state in case we need to restart without nano.
|
||||
useEffect(() => {
|
||||
if (props.filename === undefined) return;
|
||||
lastFilename = props.filename;
|
||||
lastCode = props.code;
|
||||
lastPosition = null;
|
||||
}, []);
|
||||
|
||||
function save(): void {
|
||||
if (editorRef.current !== null) {
|
||||
const position = editorRef.current.getPosition();
|
||||
if (position !== null) {
|
||||
CursorPositions.saveCursor(filename, {
|
||||
row: position.lineNumber,
|
||||
column: position.column,
|
||||
});
|
||||
}
|
||||
}
|
||||
lastPosition = null;
|
||||
|
||||
// this is duplicate code with saving later.
|
||||
if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalTypeScript) {
|
||||
//Make sure filename + code properly follow tutorial
|
||||
if (filename !== "n00dles.script") {
|
||||
if (currentScript.fileName !== "n00dles.script") {
|
||||
dialogBoxCreate("Leave the script name as 'n00dles.script'!");
|
||||
return;
|
||||
}
|
||||
if (code.replace(/\s/g, "").indexOf("while(true){hack('n00dles');}") == -1) {
|
||||
if (currentScript.code.replace(/\s/g, "").indexOf("while(true){hack('n00dles');}") == -1) {
|
||||
dialogBoxCreate("Please copy and paste the code from the tutorial!");
|
||||
return;
|
||||
}
|
||||
|
||||
//Save the script
|
||||
const server = GetServer(hostname);
|
||||
if (server === null) throw new Error("Server should not be null but it is.");
|
||||
let found = false;
|
||||
for (let i = 0; i < server.scripts.length; i++) {
|
||||
if (filename == server.scripts[i].filename) {
|
||||
server.scripts[i].saveScript(filename, code, hostname, server.scripts);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
const script = new Script();
|
||||
script.saveScript(filename, code, hostname, server.scripts);
|
||||
server.scripts.push(script);
|
||||
}
|
||||
saveScript(currentScript);
|
||||
|
||||
iTutorialNextStep();
|
||||
|
||||
@ -169,45 +153,43 @@ export function Root(props: IProps): React.ReactElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (filename == "") {
|
||||
if (currentScript.fileName == "") {
|
||||
dialogBoxCreate("You must specify a filename!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidFilePath(filename)) {
|
||||
if (!isValidFilePath(currentScript.fileName)) {
|
||||
dialogBoxCreate(
|
||||
"Script filename can contain only alphanumerics, hyphens, and underscores, and must end with an extension.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const server = GetServer(hostname);
|
||||
const server = GetServer(currentScript.hostname);
|
||||
if (server === null) throw new Error("Server should not be null but it is.");
|
||||
if (isScriptFilename(filename)) {
|
||||
if (isScriptFilename(currentScript.fileName)) {
|
||||
//If the current script already exists on the server, overwrite it
|
||||
for (let i = 0; i < server.scripts.length; i++) {
|
||||
if (filename == server.scripts[i].filename) {
|
||||
server.scripts[i].saveScript(filename, code, props.player.currentServer, server.scripts);
|
||||
if (currentScript.fileName == server.scripts[i].filename) {
|
||||
server.scripts[i].saveScript(currentScript.fileName, currentScript.code, props.player.currentServer, server.scripts);
|
||||
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
||||
props.router.toTerminal();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//If the current script does NOT exist, create a new one
|
||||
const script = new Script();
|
||||
script.saveScript(filename, code, props.player.currentServer, server.scripts);
|
||||
script.saveScript(currentScript.fileName, currentScript.code, props.player.currentServer, server.scripts);
|
||||
server.scripts.push(script);
|
||||
} else if (filename.endsWith(".txt")) {
|
||||
} else if (currentScript.fileName.endsWith(".txt")) {
|
||||
for (let i = 0; i < server.textFiles.length; ++i) {
|
||||
if (server.textFiles[i].fn === filename) {
|
||||
server.textFiles[i].write(code);
|
||||
if (server.textFiles[i].fn === currentScript.fileName) {
|
||||
server.textFiles[i].write(currentScript.code);
|
||||
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
||||
props.router.toTerminal();
|
||||
return;
|
||||
}
|
||||
}
|
||||
const textFile = new TextFile(filename, code);
|
||||
const textFile = new TextFile(currentScript.fileName, currentScript.code);
|
||||
server.textFiles.push(textFile);
|
||||
} else {
|
||||
dialogBoxCreate("Invalid filename. Must be either a script (.script, .js, or .ns) or " + " or text file (.txt)");
|
||||
@ -215,7 +197,6 @@ export function Root(props: IProps): React.ReactElement {
|
||||
}
|
||||
|
||||
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
||||
props.router.toTerminal();
|
||||
}
|
||||
|
||||
function beautify(): void {
|
||||
@ -223,14 +204,9 @@ export function Root(props: IProps): React.ReactElement {
|
||||
editorRef.current.getAction("editor.action.formatDocument").run();
|
||||
}
|
||||
|
||||
function onFilenameChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
lastFilename = event.target.value;
|
||||
setFilename(event.target.value);
|
||||
}
|
||||
|
||||
function infLoop(newCode: string): void {
|
||||
if (editorRef.current === null) return;
|
||||
if (!filename.endsWith(".ns") && !filename.endsWith(".js")) return;
|
||||
if (!currentScript.fileName.endsWith(".ns") && !currentScript.fileName.endsWith(".js")) return;
|
||||
const awaitWarning = checkInfiniteLoop(newCode);
|
||||
if (awaitWarning !== -1) {
|
||||
const newDecorations = editorRef.current.deltaDecorations(decorations, [
|
||||
@ -259,20 +235,18 @@ export function Root(props: IProps): React.ReactElement {
|
||||
|
||||
function updateCode(newCode?: string): void {
|
||||
if (newCode === undefined) return;
|
||||
lastCode = newCode;
|
||||
setCode(newCode);
|
||||
updateRAM(newCode);
|
||||
currentScript.code = newCode;
|
||||
try {
|
||||
if (editorRef.current !== null) {
|
||||
lastPosition = editorRef.current.getPosition();
|
||||
infLoop(newCode);
|
||||
}
|
||||
} catch (err) {}
|
||||
} catch (err) { }
|
||||
}
|
||||
|
||||
// calculate it once the first time the file is loaded.
|
||||
useEffect(() => {
|
||||
updateRAM(code);
|
||||
updateRAM(currentScript.code);
|
||||
}, []);
|
||||
|
||||
async function updateRAM(newCode: string): Promise<void> {
|
||||
@ -314,8 +288,64 @@ export function Root(props: IProps): React.ReactElement {
|
||||
return () => document.removeEventListener("keydown", maybeSave);
|
||||
});
|
||||
|
||||
function onMount(editor: IStandaloneCodeEditor): void {
|
||||
// Generates a new model for the script
|
||||
function regenerateModel(script: openScript) {
|
||||
if (monacoRef.current !== null) {
|
||||
script.model = monacoRef.current.editor.createModel(script.code, 'javascript');
|
||||
}
|
||||
}
|
||||
|
||||
// Sets the currently viewed script
|
||||
function setCurrentScript(script: openScript) {
|
||||
// Update last position
|
||||
if (editorRef.current !== null) {
|
||||
if (currentScript !== null) {
|
||||
var currentPosition = editorRef.current.getPosition();
|
||||
if (currentPosition !== null) {
|
||||
currentScript.lastPosition = currentPosition;
|
||||
}
|
||||
}
|
||||
|
||||
editorRef.current.setModel(script.model);
|
||||
currentScript = script;
|
||||
editorRef.current.setPosition(currentScript.lastPosition);
|
||||
editorRef.current.revealLine(currentScript.lastPosition.lineNumber);
|
||||
updateRAM(currentScript.code);
|
||||
}
|
||||
}
|
||||
|
||||
// Gets a currently opened script
|
||||
function getOpenedScript(fileName: string, hostname: string) {
|
||||
for (const script of openScripts) {
|
||||
if (script.fileName === fileName && script.hostname === hostname) {
|
||||
return script;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function saveScript(script: openScript) {
|
||||
const server = GetServer(script.hostname);
|
||||
if (server === null) throw new Error("Server should not be null but it is.");
|
||||
let found = false;
|
||||
for (let i = 0; i < server.scripts.length; i++) {
|
||||
if (script.fileName == server.scripts[i].filename) {
|
||||
server.scripts[i].saveScript(script.fileName, script.code, script.hostname, server.scripts);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
const newScript = new Script();
|
||||
newScript.saveScript(script.fileName, script.code, script.hostname, server.scripts);
|
||||
server.scripts.push(newScript);
|
||||
}
|
||||
}
|
||||
|
||||
function onMount(editor: IStandaloneCodeEditor, monaco: Monaco): void {
|
||||
editorRef.current = editor;
|
||||
monacoRef.current = monaco;
|
||||
if (editorRef.current === null) return;
|
||||
const position = CursorPositions.getCursor(filename);
|
||||
if (position.row !== -1)
|
||||
@ -323,12 +353,44 @@ export function Root(props: IProps): React.ReactElement {
|
||||
lineNumber: position.row,
|
||||
column: position.column,
|
||||
});
|
||||
else if (lastPosition !== null)
|
||||
editorRef.current.setPosition({
|
||||
lineNumber: lastPosition.lineNumber,
|
||||
column: lastPosition.column + 1,
|
||||
});
|
||||
editorRef.current.focus();
|
||||
|
||||
const script = getOpenedScript(filename, props.player.getCurrentServer().hostname);
|
||||
|
||||
// Check if script is already opened, if so switch to that model
|
||||
if (script !== null) {
|
||||
if (script.model.isDisposed()) {
|
||||
regenerateModel(script);
|
||||
}
|
||||
|
||||
setCurrentScript(script);
|
||||
} else {
|
||||
if (filename !== undefined) {
|
||||
// Create new model
|
||||
if (monacoRef.current !== null) {
|
||||
var newScript = new openScript(filename, code, props.player.getCurrentServer().hostname, new monaco.Position(0, 0), monacoRef.current.editor.createModel(code, 'javascript'));
|
||||
setCurrentScript(newScript);
|
||||
openScripts.push(newScript);
|
||||
}
|
||||
} else {
|
||||
// Script Editor was opened by the sidebar button
|
||||
if (currentScript.model !== undefined) {
|
||||
if (currentScript.model.isDisposed()) {
|
||||
// Create new model, old one was disposed of
|
||||
regenerateModel(currentScript);
|
||||
}
|
||||
|
||||
setCurrentScript(currentScript);
|
||||
} else {
|
||||
// Create a new temporary file
|
||||
if (monacoRef.current !== null) {
|
||||
var newScript = new openScript('NewFile.ns', '', props.player.getCurrentServer().hostname, new monaco.Position(0, 0), monacoRef.current.editor.createModel('', 'javascript'));
|
||||
setCurrentScript(newScript);
|
||||
openScripts.push(newScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function beforeMount(monaco: any): void {
|
||||
@ -368,26 +430,119 @@ export function Root(props: IProps): React.ReactElement {
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(source, "netscript.d.ts");
|
||||
loadThemes(monaco);
|
||||
}
|
||||
|
||||
// Change tab highlight from old tab to new tab
|
||||
function changeTabButtonColor(oldButtonFileName: string, oldButtonHostname: string, newButtonFileName: string, newButtonHostname: string) {
|
||||
const oldTabButton = document.getElementById('tabButton' + oldButtonFileName + oldButtonHostname);
|
||||
if (oldTabButton !== null) {
|
||||
oldTabButton.style.backgroundColor = '';
|
||||
}
|
||||
|
||||
const oldTabCloseButton = document.getElementById('tabCloseButton' + oldButtonFileName + oldButtonHostname);
|
||||
if (oldTabCloseButton !== null) {
|
||||
oldTabCloseButton.style.backgroundColor = '';
|
||||
}
|
||||
|
||||
const newTabButton = document.getElementById('tabButton' + newButtonFileName + newButtonHostname);
|
||||
if (newTabButton !== null) {
|
||||
newTabButton.style.backgroundColor = '#173b2d';
|
||||
}
|
||||
|
||||
const newTabCloseButton = document.getElementById('tabCloseButton' + newButtonFileName + newButtonHostname);
|
||||
if (newTabCloseButton !== null) {
|
||||
newTabCloseButton.style.backgroundColor = '#173b2d';
|
||||
}
|
||||
}
|
||||
|
||||
// Called when a script tab was clicked
|
||||
function onTabButtonClick(e: React.MouseEvent<HTMLButtonElement>) {
|
||||
const valSplit = e.currentTarget.value.split(':');
|
||||
const fileName = valSplit[0];
|
||||
const hostname = valSplit[1];
|
||||
|
||||
// Change tab highlight from old tab to new tab
|
||||
changeTabButtonColor(currentScript.fileName, currentScript.hostname, fileName, hostname)
|
||||
|
||||
|
||||
// Update current script
|
||||
const clickedScript = getOpenedScript(fileName, hostname);
|
||||
|
||||
if (clickedScript !== null) {
|
||||
if (clickedScript.model.isDisposed()) {
|
||||
regenerateModel(clickedScript);
|
||||
}
|
||||
|
||||
setCurrentScript(clickedScript);
|
||||
}
|
||||
}
|
||||
|
||||
// Called when a script tab close button was clicked
|
||||
function onCloseButtonClick(e: React.MouseEvent<HTMLButtonElement>) {
|
||||
const valSplit = e.currentTarget.value.split(':');
|
||||
const fileName = valSplit[0];
|
||||
const hostname = valSplit[1];
|
||||
|
||||
const scriptToClose = getOpenedScript(fileName, hostname);
|
||||
|
||||
// Save and remove script from openScripts
|
||||
if (scriptToClose !== null) {
|
||||
saveScript(scriptToClose);
|
||||
|
||||
openScripts.splice(openScripts.indexOf(scriptToClose), 1);
|
||||
}
|
||||
|
||||
if (openScripts.length === 0) {
|
||||
// No other scripts are open, create a new temporary file
|
||||
if (monacoRef.current !== null) {
|
||||
const newScript = new openScript("NewFile.ns", '', props.player.getCurrentServer().hostname, new monacoRef.current.Position(0, 0), monacoRef.current.editor.createModel('', 'javascript'));
|
||||
|
||||
setCurrentScript(newScript)
|
||||
openScripts.push(newScript);
|
||||
|
||||
// Create new tab button for temporary file
|
||||
const element = (<div style={{ paddingRight: '5px' }}><Button style={{ backgroundColor: '#173b2d' }} value={newScript.fileName + ':' + newScript.hostname} onClick={onTabButtonClick}>{newScript.fileName}</Button><Button value={newScript.fileName + ':' + newScript.hostname} onClick={onCloseButtonClick} style={{ maxWidth: '20px', minWidth: '20px', backgroundColor: '#173b2d' }}>x</Button></div>)
|
||||
|
||||
// Modify button for temp file
|
||||
var parent = e.currentTarget.parentElement;
|
||||
if (parent !== null) {
|
||||
(parent.children[0] as HTMLButtonElement).value = 'NewFile.ns:home';
|
||||
(parent.children[0] as HTMLButtonElement).textContent = 'NewFile.ns';
|
||||
e.currentTarget.value = 'NewFile.ns:home';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (openScripts[0].model.isDisposed()) {
|
||||
regenerateModel(openScripts[0]);
|
||||
}
|
||||
|
||||
changeTabButtonColor(currentScript.fileName, currentScript.hostname, openScripts[0].fileName, openScripts[0].hostname);
|
||||
|
||||
setCurrentScript(openScripts[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a button for each open script
|
||||
const scriptButtons = [];
|
||||
for (let i = 0; i < openScripts.length; i++) {
|
||||
if (openScripts[i].fileName !== '') {
|
||||
const fileName2 = openScripts[i].fileName;
|
||||
const hostname = openScripts[i].hostname;
|
||||
if (openScripts[i].fileName === currentScript.fileName && openScripts[i].hostname === currentScript.hostname) {
|
||||
// Set special background color for current script tab button
|
||||
scriptButtons.push(<div id={'scriptEditorTab' + fileName2 + hostname} key={'tabButton' + i} style={{ paddingRight: '5px' }}><Button id={'tabButton' + openScripts[i].fileName + openScripts[i].hostname} style={{ backgroundColor: '#173b2d' }} value={fileName2 + ':' + hostname} onClick={onTabButtonClick}>{openScripts[i].fileName}</Button><Button id={'tabCloseButton' + openScripts[i].fileName + openScripts[i].hostname} value={fileName2 + ':' + hostname} onClick={onCloseButtonClick} style={{ maxWidth: '20px', minWidth: '20px', backgroundColor: '#173b2d' }}>x</Button></div>)
|
||||
} else {
|
||||
scriptButtons.push(<div id={'scriptEditorTab' + fileName2 + hostname} key={'tabButton' + i} style={{ paddingRight: '5px' }}><Button id={'tabButton' + openScripts[i].fileName + openScripts[i].hostname} value={fileName2 + ':' + hostname} onClick={onTabButtonClick}>{openScripts[i].fileName}</Button><Button id={'tabCloseButton' + openScripts[i].fileName + openScripts[i].hostname} value={fileName2 + ':' + hostname} onClick={onCloseButtonClick} style={{ maxWidth: '20px', minWidth: '20px' }}>x</Button></div>)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 370px 71%, 725px 85.1%, 1085px 90%, 1300px 91.7%
|
||||
// fuck around in desmos until you find a function
|
||||
const p = 11000 / -window.innerHeight + 100;
|
||||
return (
|
||||
<>
|
||||
<Box display="flex" flexDirection="row" alignItems="center">
|
||||
<TextField
|
||||
placeholder="filename"
|
||||
type="text"
|
||||
tabIndex={1}
|
||||
value={filename}
|
||||
onChange={onFilenameChange}
|
||||
InputProps={{ startAdornment: <Typography>{hostname}:~/</Typography> }}
|
||||
/>
|
||||
<IconButton onClick={() => setOptionsOpen(true)}>
|
||||
<>
|
||||
<SettingsIcon />
|
||||
options
|
||||
</>
|
||||
</IconButton>
|
||||
{scriptButtons}
|
||||
</Box>
|
||||
<Editor
|
||||
beforeMount={beforeMount}
|
||||
@ -402,11 +557,11 @@ export function Root(props: IProps): React.ReactElement {
|
||||
/>
|
||||
<Box display="flex" flexDirection="row" sx={{ m: 1 }} alignItems="center">
|
||||
<Button onClick={beautify}>Beautify</Button>
|
||||
<Typography color={updatingRam ? "secondary" : "primary"} sx={{ mx: 1 }}>
|
||||
<Typography color={updatingRam ? "secondary" : "primary"} sx={{ mx: 1 }}>
|
||||
{ram}
|
||||
</Typography>
|
||||
<Button onClick={save}>Save & Close (Ctrl/Cmd + b)</Button>
|
||||
<Typography sx={{ mx: 1 }}>
|
||||
<Button onClick={save}>Save (Ctrl/Cmd + b)</Button>
|
||||
<Typography style={{ marginLeft: '50%' }} sx={{ mx: 1 }}>
|
||||
{" "}
|
||||
Documentation:{" "}
|
||||
<Link target="_blank" href="https://bitburner.readthedocs.io/en/latest/index.html">
|
||||
@ -417,6 +572,12 @@ export function Root(props: IProps): React.ReactElement {
|
||||
Full
|
||||
</Link>
|
||||
</Typography>
|
||||
<IconButton style={{ marginLeft: 'auto' }} onClick={() => setOptionsOpen(true)}>
|
||||
<>
|
||||
<SettingsIcon />
|
||||
options
|
||||
</>
|
||||
</IconButton>
|
||||
</Box>
|
||||
<OptionsModal
|
||||
open={optionsOpen}
|
||||
@ -435,4 +596,4 @@ export function Root(props: IProps): React.ReactElement {
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user