mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-17 23:08:36 +02:00
UI: Warn player if they are editing and saving files on non-home servers (#1968)
This commit is contained in:
@@ -25,7 +25,7 @@ import { PromptEvent } from "../../ui/React/PromptManager";
|
|||||||
|
|
||||||
import { useRerender } from "../../ui/React/hooks";
|
import { useRerender } from "../../ui/React/hooks";
|
||||||
|
|
||||||
import { dirty, getServerCode, makeModel } from "./utils";
|
import { isUnsavedFile, getServerCode, makeModel } from "./utils";
|
||||||
import { OpenScript } from "./OpenScript";
|
import { OpenScript } from "./OpenScript";
|
||||||
import { Tabs } from "./Tabs";
|
import { Tabs } from "./Tabs";
|
||||||
import { Toolbar } from "./Toolbar";
|
import { Toolbar } from "./Toolbar";
|
||||||
@@ -38,6 +38,9 @@ import { RamCalculationErrorCode } from "../../Script/RamCalculationErrorCodes";
|
|||||||
import { hasScriptExtension, isLegacyScript, type ScriptFilePath } from "../../Paths/ScriptFilePath";
|
import { hasScriptExtension, isLegacyScript, type ScriptFilePath } from "../../Paths/ScriptFilePath";
|
||||||
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
|
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
|
||||||
import type { BaseServer } from "../../Server/BaseServer";
|
import type { BaseServer } from "../../Server/BaseServer";
|
||||||
|
import { SpecialServers } from "../../Server/data/SpecialServers";
|
||||||
|
import { SnackbarEvents } from "../../ui/React/Snackbar";
|
||||||
|
import { ToastVariant } from "@enums";
|
||||||
|
|
||||||
// Extend acorn-walk to support TypeScript nodes.
|
// Extend acorn-walk to support TypeScript nodes.
|
||||||
extendAcornWalkForTypeScriptNodes(walk.base);
|
extendAcornWalkForTypeScriptNodes(walk.base);
|
||||||
@@ -206,13 +209,7 @@ function Root(props: IProps): React.ReactElement {
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
saveScript(currentScript);
|
||||||
const server = GetServer(currentScript.hostname);
|
|
||||||
if (server === null) throw new Error("Server should not be null but it is.");
|
|
||||||
server.writeToContentFile(currentScript.path, currentScript.code);
|
|
||||||
if (Settings.SaveGameOnFileSave) {
|
|
||||||
saveObject.saveGame().catch((error) => exceptionAlert(error));
|
|
||||||
}
|
|
||||||
rerender();
|
rerender();
|
||||||
}, [rerender]);
|
}, [rerender]);
|
||||||
|
|
||||||
@@ -363,7 +360,13 @@ function Root(props: IProps): React.ReactElement {
|
|||||||
|
|
||||||
function saveScript(scriptToSave: OpenScript): void {
|
function saveScript(scriptToSave: OpenScript): void {
|
||||||
const server = GetServer(scriptToSave.hostname);
|
const server = GetServer(scriptToSave.hostname);
|
||||||
if (!server) throw new Error("Server should not be null but it is.");
|
if (!server) {
|
||||||
|
throw new Error("Server should not be null but it is.");
|
||||||
|
}
|
||||||
|
// Show a warning message if the file is on a non-home server.
|
||||||
|
if (scriptToSave.hostname !== SpecialServers.Home) {
|
||||||
|
SnackbarEvents.emit("You saved a file on a non-home server!", ToastVariant.WARNING, 3000);
|
||||||
|
}
|
||||||
// This server helper already handles overwriting, etc.
|
// This server helper already handles overwriting, etc.
|
||||||
server.writeToContentFile(scriptToSave.path, scriptToSave.code);
|
server.writeToContentFile(scriptToSave.path, scriptToSave.code);
|
||||||
if (Settings.SaveGameOnFileSave) {
|
if (Settings.SaveGameOnFileSave) {
|
||||||
@@ -411,7 +414,7 @@ function Root(props: IProps): React.ReactElement {
|
|||||||
const savedScriptCode = closingScript.code;
|
const savedScriptCode = closingScript.code;
|
||||||
const wasCurrentScript = openScripts[index] === currentScript;
|
const wasCurrentScript = openScripts[index] === currentScript;
|
||||||
|
|
||||||
if (dirty(openScripts, index)) {
|
if (isUnsavedFile(openScripts, index)) {
|
||||||
PromptEvent.emit({
|
PromptEvent.emit({
|
||||||
txt: `Do you want to save changes to ${closingScript.path} on ${closingScript.hostname}?`,
|
txt: `Do you want to save changes to ${closingScript.path} on ${closingScript.hostname}?`,
|
||||||
resolve: (result: boolean | string) => {
|
resolve: (result: boolean | string) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { DraggableProvided } from "react-beautiful-dnd";
|
|||||||
|
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
import SyncIcon from "@mui/icons-material/Sync";
|
import SyncIcon from "@mui/icons-material/Sync";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
@@ -11,9 +12,10 @@ import { Settings } from "../../Settings/Settings";
|
|||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
provided: DraggableProvided;
|
provided: DraggableProvided;
|
||||||
title: string;
|
fullPath: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
isExternal: boolean;
|
isExternal: boolean;
|
||||||
|
isUnsaved: boolean;
|
||||||
|
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -24,7 +26,7 @@ const tabMargin = 5;
|
|||||||
const tabIconWidth = 25;
|
const tabIconWidth = 25;
|
||||||
const tabIconHeight = 38.5;
|
const tabIconHeight = 38.5;
|
||||||
|
|
||||||
export function Tab({ provided, title, isActive, isExternal, onClick, onClose, onUpdate }: IProps) {
|
export function Tab({ provided, fullPath, isActive, isExternal, isUnsaved, onClick, onClose, onUpdate }: IProps) {
|
||||||
const colorProps = isActive
|
const colorProps = isActive
|
||||||
? {
|
? {
|
||||||
background: Settings.theme.button,
|
background: Settings.theme.button,
|
||||||
@@ -37,8 +39,35 @@ export function Tab({ provided, title, isActive, isExternal, onClick, onClose, o
|
|||||||
color: Settings.theme.secondary,
|
color: Settings.theme.secondary,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let tabTitle;
|
||||||
|
let tooltipTitle;
|
||||||
|
if (isUnsaved) {
|
||||||
|
// Show a blinking "*" character to notify the player that this file is dirtied.
|
||||||
|
tabTitle = (
|
||||||
|
<>
|
||||||
|
<Typography component="span" color={Settings.theme.warning}>
|
||||||
|
*{" "}
|
||||||
|
</Typography>
|
||||||
|
{fullPath}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tabTitle = fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
if (isExternal) {
|
if (isExternal) {
|
||||||
colorProps.color = Settings.theme.info;
|
colorProps.color = Settings.theme.warning;
|
||||||
|
// Show a warning message if this file is on a non-home server.
|
||||||
|
tooltipTitle = (
|
||||||
|
<Typography component="span" color={Settings.theme.warning}>
|
||||||
|
{tabTitle}
|
||||||
|
<br />
|
||||||
|
This file is on a non-home server. You will lose all files on non-home servers when they are deleted or
|
||||||
|
recreated (install augmentations, soft reset, deleted by NS APIs, etc.).
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tooltipTitle = tabTitle;
|
||||||
}
|
}
|
||||||
const iconButtonStyle = {
|
const iconButtonStyle = {
|
||||||
maxWidth: tabIconWidth,
|
maxWidth: tabIconWidth,
|
||||||
@@ -71,12 +100,14 @@ export function Tab({ provided, title, isActive, isExternal, onClick, onClose, o
|
|||||||
border: "1px solid " + Settings.theme.well,
|
border: "1px solid " + Settings.theme.well,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip title={title}>
|
<Tooltip title={tooltipTitle}>
|
||||||
<Button
|
<Button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (e.button === 1) onClose();
|
if (e.button === 1) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
minHeight: tabIconHeight,
|
minHeight: tabIconHeight,
|
||||||
@@ -84,7 +115,7 @@ export function Tab({ provided, title, isActive, isExternal, onClick, onClose, o
|
|||||||
...colorProps,
|
...colorProps,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ overflow: "hidden", direction: "rtl", textOverflow: "ellipsis" }}>{title}</span>
|
<span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>{tabTitle}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Overwrite editor content with saved file content">
|
<Tooltip title="Overwrite editor content with saved file content">
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ import SearchIcon from "@mui/icons-material/Search";
|
|||||||
import { useBoolean, useRerender } from "../../ui/React/hooks";
|
import { useBoolean, useRerender } from "../../ui/React/hooks";
|
||||||
import { Settings } from "../../Settings/Settings";
|
import { Settings } from "../../Settings/Settings";
|
||||||
|
|
||||||
import { dirty, reorder } from "./utils";
|
import { isUnsavedFile, reorder } from "./utils";
|
||||||
import { OpenScript } from "./OpenScript";
|
import { OpenScript } from "./OpenScript";
|
||||||
import { Tab } from "./Tab";
|
import { Tab } from "./Tab";
|
||||||
|
import { SpecialServers } from "../../Server/data/SpecialServers";
|
||||||
|
|
||||||
const tabsMaxWidth = 1640;
|
const tabsMaxWidth = 1640;
|
||||||
const searchWidth = 180;
|
const searchWidth = 180;
|
||||||
@@ -119,22 +120,16 @@ export function Tabs({ scripts, currentScript, onTabClick, onTabClose, onTabUpda
|
|||||||
{filteredScripts.map(({ script, originalIndex }, index) => {
|
{filteredScripts.map(({ script, originalIndex }, index) => {
|
||||||
const { path: fileName, hostname } = script;
|
const { path: fileName, hostname } = script;
|
||||||
const isActive = currentScript?.path === script.path && currentScript.hostname === script.hostname;
|
const isActive = currentScript?.path === script.path && currentScript.hostname === script.hostname;
|
||||||
|
const fullPath = `${hostname}:/${fileName}`;
|
||||||
const title = `${hostname}:~${fileName.startsWith("/") ? "" : "/"}${fileName} ${dirty(scripts, index)}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable
|
<Draggable key={fullPath} draggableId={fullPath} index={index} disableInteractiveElementBlocking>
|
||||||
key={fileName + hostname}
|
|
||||||
draggableId={fileName + hostname}
|
|
||||||
index={index}
|
|
||||||
disableInteractiveElementBlocking
|
|
||||||
>
|
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<Tab
|
<Tab
|
||||||
provided={provided}
|
provided={provided}
|
||||||
title={title}
|
fullPath={fullPath}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
isExternal={hostname !== "home"}
|
isExternal={hostname !== SpecialServers.Home}
|
||||||
|
isUnsaved={isUnsavedFile(scripts, index)}
|
||||||
onClick={() => onTabClick(originalIndex)}
|
onClick={() => onTabClick(originalIndex)}
|
||||||
onClose={() => onTabClose(originalIndex)}
|
onClose={() => onTabClose(originalIndex)}
|
||||||
onUpdate={() => onTabUpdate(originalIndex)}
|
onUpdate={() => onTabUpdate(originalIndex)}
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ function getServerCode(scripts: OpenScript[], index: number): string | null {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dirty(scripts: OpenScript[], index: number): string {
|
function isUnsavedFile(scripts: OpenScript[], index: number): boolean {
|
||||||
const openScript = scripts[index];
|
const openScript = scripts[index];
|
||||||
const serverData = getServerCode(scripts, index);
|
const serverData = getServerCode(scripts, index);
|
||||||
if (serverData === null) return " *";
|
if (serverData === null) {
|
||||||
return serverData !== openScript.code ? " *" : "";
|
return true;
|
||||||
|
}
|
||||||
|
return serverData !== openScript.code;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reorder(list: unknown[], startIndex: number, endIndex: number): void {
|
function reorder(list: unknown[], startIndex: number, endIndex: number): void {
|
||||||
@@ -60,4 +62,4 @@ function makeModel(hostname: string, filename: string, code: string): editor.ITe
|
|||||||
return editor.createModel(code, language, uri);
|
return editor.createModel(code, language, uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getServerCode, dirty, reorder, makeModel };
|
export { getServerCode, isUnsavedFile, reorder, makeModel };
|
||||||
|
|||||||
Reference in New Issue
Block a user