mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-16 06:18:42 +02:00
UI: Add indicator of RFA connection status to overview panel (#2497)
This commit is contained in:
@@ -1,40 +0,0 @@
|
||||
import { Typography } from "@mui/material";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import WifiIcon from "@mui/icons-material/Wifi";
|
||||
import WifiOffIcon from "@mui/icons-material/WifiOff";
|
||||
|
||||
interface baubleProps {
|
||||
isConnected: () => boolean;
|
||||
}
|
||||
|
||||
export const ConnectionBauble = (props: baubleProps): React.ReactElement => {
|
||||
const [connection, setConnection] = useState(props.isConnected());
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setConnection(props.isConnected());
|
||||
}, 1000);
|
||||
return () => clearInterval(timer);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography>
|
||||
Status:
|
||||
<Typography component="span" color={connection ? "primary" : "error"}>
|
||||
{connection ? (
|
||||
<>
|
||||
Online
|
||||
<WifiIcon />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Offline
|
||||
<WifiOffIcon />
|
||||
</>
|
||||
)}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, Container, Typography } from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { GameOptionsSidebar } from "./GameOptionsSidebar";
|
||||
import { GameplayPage } from "./GameplayPage";
|
||||
import { InterfacePage } from "./InterfacePage";
|
||||
@@ -8,14 +8,7 @@ import { NumericDisplayPage } from "./NumericDisplayOptions";
|
||||
import { RemoteAPIPage } from "./RemoteAPIPage";
|
||||
import { SystemPage } from "./SystemPage";
|
||||
import { KeyBindingPage } from "./KeyBindingPage";
|
||||
|
||||
interface IProps {
|
||||
save: () => void;
|
||||
export: () => void;
|
||||
forceKill: () => void;
|
||||
softReset: () => void;
|
||||
reactivateTutorial: () => void;
|
||||
}
|
||||
import { EventEmitter } from "../../utils/EventEmitter";
|
||||
|
||||
export type OptionsTabName =
|
||||
| "System"
|
||||
@@ -26,6 +19,15 @@ export type OptionsTabName =
|
||||
| "Remote API"
|
||||
| "Key Binding";
|
||||
|
||||
interface IProps {
|
||||
tab?: OptionsTabName;
|
||||
save: () => void;
|
||||
export: () => void;
|
||||
forceKill: () => void;
|
||||
softReset: () => void;
|
||||
reactivateTutorial: () => void;
|
||||
}
|
||||
|
||||
const tabs: Record<OptionsTabName, React.ReactNode> = {
|
||||
System: <SystemPage />,
|
||||
Interface: <InterfacePage />,
|
||||
@@ -36,8 +38,19 @@ const tabs: Record<OptionsTabName, React.ReactNode> = {
|
||||
"Key Binding": <KeyBindingPage />,
|
||||
};
|
||||
|
||||
export const GameOptionsPageEvents = new EventEmitter<[OptionsTabName]>();
|
||||
|
||||
export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
const [currentTab, setCurrentTab] = useState<OptionsTabName>("System");
|
||||
const [currentTab, setCurrentTab] = useState<OptionsTabName>(props.tab ?? "System");
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
GameOptionsPageEvents.subscribe((tab: OptionsTabName) => {
|
||||
setCurrentTab(tab);
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container disableGutters maxWidth="lg" sx={{ mx: 0 }}>
|
||||
<Typography variant="h4">Options</Typography>
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { getRecordKeys } from "../../Types/Record";
|
||||
import { Modal } from "../../ui/React/Modal";
|
||||
import { SimplePage } from "../../ui/Enums";
|
||||
import { ComplexPage } from "../../ui/Enums";
|
||||
import { KEY } from "../../utils/KeyboardEventKey";
|
||||
import {
|
||||
areDifferentKeyCombinations,
|
||||
@@ -248,7 +248,7 @@ function SettingUpKeyBindingModal({
|
||||
|
||||
export function KeyBindingPage(): React.ReactElement {
|
||||
const [popupOpen, setPopupOpen] = useState(false);
|
||||
const [keyBindingType, setKeyBindingType] = useState<KeyBindingType>(SimplePage.Options);
|
||||
const [keyBindingType, setKeyBindingType] = useState<KeyBindingType>(ComplexPage.Options);
|
||||
const [isPrimary, setIsPrimary] = useState(true);
|
||||
|
||||
const showModal = (keyBindingType: KeyBindingType, isPrimary: boolean) => {
|
||||
|
||||
@@ -2,8 +2,8 @@ import React, { useState } from "react";
|
||||
import { Button, TextField, Tooltip, Typography } from "@mui/material";
|
||||
import { GameOptionsPage } from "./GameOptionsPage";
|
||||
import { isValidConnectionHostname, isValidConnectionPort, Settings } from "../../Settings/Settings";
|
||||
import { ConnectionBauble } from "./ConnectionBauble";
|
||||
import { isRemoteFileApiConnectionLive, newRemoteFileApiConnection } from "../../RemoteFileAPI/RemoteFileAPI";
|
||||
import { RemoteFileApiConnectionStatus } from "./RemoteFileApiConnectionStatus";
|
||||
import { newRemoteFileApiConnection } from "../../RemoteFileAPI/RemoteFileAPI";
|
||||
import { OptionSwitch } from "../../ui/React/OptionSwitch";
|
||||
import { DocumentationLink } from "../../ui/React/DocumentationLink";
|
||||
|
||||
@@ -69,7 +69,7 @@ export const RemoteAPIPage = (): React.ReactElement => {
|
||||
<Typography>
|
||||
<DocumentationLink page="programming/remote_api.md">Documentation</DocumentationLink>
|
||||
</Typography>
|
||||
<ConnectionBauble isConnected={isRemoteFileApiConnectionLive} />
|
||||
<RemoteFileApiConnectionStatus showIcon={false} />
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
|
||||
49
src/GameOptions/ui/RemoteFileApiConnectionStatus.tsx
Normal file
49
src/GameOptions/ui/RemoteFileApiConnectionStatus.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Box, IconButton, Tooltip, Typography } from "@mui/material";
|
||||
import { getRemoteFileApiConnectionStatus } from "../../RemoteFileAPI/RemoteFileAPI";
|
||||
import OnlinePredictionIcon from "@mui/icons-material/OnlinePrediction";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { Router } from "../../ui/GameRoot";
|
||||
import { Page } from "../../ui/Router";
|
||||
import { RemoteFileApiConnectionEvents } from "../../RemoteFileAPI/Remote";
|
||||
|
||||
export const RemoteFileApiConnectionStatus = ({ showIcon }: { showIcon: boolean }): React.ReactElement => {
|
||||
const [rfaConnectionStatus, setRfaConnectionStatus] = useState(getRemoteFileApiConnectionStatus());
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
RemoteFileApiConnectionEvents.subscribe((status) => {
|
||||
setRfaConnectionStatus(status);
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
let color;
|
||||
switch (rfaConnectionStatus) {
|
||||
case "Online":
|
||||
color = Settings.theme.success;
|
||||
break;
|
||||
case "Offline":
|
||||
color = Settings.theme.error;
|
||||
break;
|
||||
case "Reconnecting":
|
||||
color = Settings.theme.warning;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box style={{ display: "flex", flex: 1, justifyContent: "flex-start", alignItems: "center" }}>
|
||||
{showIcon ? (
|
||||
<IconButton aria-label="Remote API status" onClick={() => Router.toPage(Page.Options, { tab: "Remote API" })}>
|
||||
<Tooltip title={`Remote API: ${rfaConnectionStatus}`}>
|
||||
<OnlinePredictionIcon style={{ fontSize: "30px", color }} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
) : (
|
||||
<Typography>
|
||||
Status: <span style={{ color }}>{rfaConnectionStatus}</span>
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -3,6 +3,8 @@ import { RFARequestHandler } from "./MessageHandlers";
|
||||
import { SnackbarEvents } from "../ui/React/Snackbar";
|
||||
import { ToastVariant } from "@enums";
|
||||
import { Settings } from "../Settings/Settings";
|
||||
import { EventEmitter } from "../utils/EventEmitter";
|
||||
import type { getRemoteFileApiConnectionStatus } from "./RemoteFileAPI";
|
||||
|
||||
function showErrorMessage(address: string, detail: string) {
|
||||
SnackbarEvents.emit(`Error with websocket ${address}, details: ${detail}`, ToastVariant.ERROR, 5000);
|
||||
@@ -10,10 +12,13 @@ function showErrorMessage(address: string, detail: string) {
|
||||
|
||||
const eventCodeWhenIntentionallyStoppingConnection = 3000;
|
||||
|
||||
export const RemoteFileApiConnectionEvents = new EventEmitter<[ReturnType<typeof getRemoteFileApiConnectionStatus>]>();
|
||||
|
||||
export class Remote {
|
||||
connection?: WebSocket;
|
||||
ipaddr: string;
|
||||
port: number;
|
||||
reconnecting = false;
|
||||
|
||||
constructor(ip: string, port: number) {
|
||||
this.ipaddr = ip;
|
||||
@@ -22,6 +27,7 @@ export class Remote {
|
||||
|
||||
public stopConnection(): void {
|
||||
this.connection?.close(eventCodeWhenIntentionallyStoppingConnection);
|
||||
RemoteFileApiConnectionEvents.emit("Offline");
|
||||
}
|
||||
|
||||
public startConnection(autoConnectAttempt = 1): void {
|
||||
@@ -54,6 +60,7 @@ export class Remote {
|
||||
ToastVariant.SUCCESS,
|
||||
2000,
|
||||
);
|
||||
RemoteFileApiConnectionEvents.emit("Online");
|
||||
});
|
||||
this.connection.addEventListener("close", (event) => {
|
||||
/**
|
||||
@@ -75,6 +82,7 @@ export class Remote {
|
||||
}
|
||||
|
||||
if (Settings.RemoteFileApiReconnectionDelay > 0) {
|
||||
this.reconnecting = true;
|
||||
setTimeout(() => {
|
||||
if (autoConnectAttempt === 1) {
|
||||
SnackbarEvents.emit(`Attempting to auto connect Remote API`, ToastVariant.WARNING, 2000);
|
||||
@@ -85,6 +93,10 @@ export class Remote {
|
||||
|
||||
this.startConnection(attempts);
|
||||
}, Settings.RemoteFileApiReconnectionDelay * 1000);
|
||||
RemoteFileApiConnectionEvents.emit("Reconnecting");
|
||||
} else {
|
||||
this.reconnecting = false;
|
||||
RemoteFileApiConnectionEvents.emit("Offline");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Settings } from "../Settings/Settings";
|
||||
import { Remote } from "./Remote";
|
||||
|
||||
let server: Remote;
|
||||
let server: Remote | undefined;
|
||||
|
||||
export function newRemoteFileApiConnection(): void {
|
||||
if (server) server.stopConnection();
|
||||
@@ -14,5 +14,15 @@ export function newRemoteFileApiConnection(): void {
|
||||
}
|
||||
|
||||
export function isRemoteFileApiConnectionLive(): boolean {
|
||||
return server && server.connection != undefined && server.connection.readyState == 1;
|
||||
return server !== undefined && server.connection !== undefined && server.connection.readyState === 1;
|
||||
}
|
||||
|
||||
export function getRemoteFileApiConnectionStatus(): "Online" | "Offline" | "Reconnecting" {
|
||||
if (isRemoteFileApiConnectionLive()) {
|
||||
return "Online";
|
||||
}
|
||||
if (server?.reconnecting) {
|
||||
return "Reconnecting";
|
||||
}
|
||||
return "Offline";
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
|
||||
|
||||
const clickPage = useCallback(
|
||||
(page: Page) => {
|
||||
if (page == Page.ScriptEditor || page == Page.Documentation) {
|
||||
if (page == Page.ScriptEditor || page == Page.Documentation || page == Page.Options) {
|
||||
Router.toPage(page, {});
|
||||
} else if (isSimplePage(page)) {
|
||||
Router.toPage(page);
|
||||
@@ -212,7 +212,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
|
||||
case SimplePage.Milestones:
|
||||
case ComplexPage.Documentation:
|
||||
case SimplePage.Achievements:
|
||||
case SimplePage.Options:
|
||||
case ComplexPage.Options:
|
||||
return true;
|
||||
case SimplePage.StaneksGift:
|
||||
return canStaneksGift;
|
||||
|
||||
@@ -27,7 +27,6 @@ export enum SimplePage {
|
||||
Hacknet = "Hacknet",
|
||||
Infiltration = "Infiltration",
|
||||
Milestones = "Milestones",
|
||||
Options = "Options",
|
||||
Grafting = "Grafting",
|
||||
Sleeves = "Sleeves",
|
||||
Stats = "Stats",
|
||||
@@ -52,6 +51,7 @@ export enum ComplexPage {
|
||||
Location = "Location",
|
||||
ImportSave = "Import Save",
|
||||
Documentation = "Documentation",
|
||||
Options = "Options",
|
||||
// LoadingScreen is a special state that should never be returned to after the initial game load. To enforce this, it
|
||||
// is constructed as a ComplexPage with no PageContext, and thus toPage() cannot be used (since no overload will fit
|
||||
// it).
|
||||
|
||||
@@ -29,7 +29,7 @@ import { CorporationRoot } from "../Corporation/ui/CorporationRoot";
|
||||
import { InfiltrationRoot } from "../Infiltration/ui/InfiltrationRoot";
|
||||
import { GraftingRoot } from "../PersonObjects/Grafting/ui/GraftingRoot";
|
||||
import { WorkInProgressRoot } from "./WorkInProgressRoot";
|
||||
import { GameOptionsRoot } from "../GameOptions/ui/GameOptionsRoot";
|
||||
import { GameOptionsPageEvents, GameOptionsRoot } from "../GameOptions/ui/GameOptionsRoot";
|
||||
import { SleeveRoot } from "../PersonObjects/Sleeve/ui/SleeveRoot";
|
||||
import { HacknetRoot } from "../Hacknet/ui/HacknetRoot";
|
||||
import { GenericLocation } from "../Locations/ui/GenericLocation";
|
||||
@@ -257,6 +257,14 @@ export function GameRoot(): React.ReactElement {
|
||||
prestigeWorkerScripts();
|
||||
calculateAchievements();
|
||||
break;
|
||||
case Page.Options:
|
||||
// If the current page is "Options" and something calls Router.toPage("Options", { tab: "Foo" }) to switch the
|
||||
// tab, we need to emit an event to tell GameOptionsRoot to set its currentTab state. Changing the tab in the
|
||||
// properties of GameOptionsRoot does not set the state.
|
||||
if (Router.page() === Page.Options && context && "tab" in context && context.tab != null) {
|
||||
GameOptionsPageEvents.emit(context.tab);
|
||||
}
|
||||
break;
|
||||
}
|
||||
setNextPage({ page, ...context } as PageWithContext);
|
||||
},
|
||||
@@ -425,6 +433,7 @@ export function GameRoot(): React.ReactElement {
|
||||
case Page.Options: {
|
||||
mainPage = (
|
||||
<GameOptionsRoot
|
||||
tab={pageWithContext.tab}
|
||||
save={() => {
|
||||
saveObject.saveGame().catch((error) => exceptionAlert(error));
|
||||
}}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { isCompanyWork } from "../../Work/CompanyWork";
|
||||
import { isCrimeWork } from "../../Work/CrimeWork";
|
||||
import { EventEmitter } from "../../utils/EventEmitter";
|
||||
import { useRerender } from "./hooks";
|
||||
import { RemoteFileApiConnectionStatus } from "../../GameOptions/ui/RemoteFileApiConnectionStatus";
|
||||
|
||||
export const OverviewEventEmitter = new EventEmitter();
|
||||
|
||||
@@ -183,6 +184,7 @@ export function CharacterOverview({ parentOpen, save, killScripts }: OverviewPro
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</Box>
|
||||
<RemoteFileApiConnectionStatus showIcon={true} />
|
||||
<Box sx={{ display: "flex", flex: 1, justifyContent: "flex-end", alignItems: "center" }}>
|
||||
<IconButton aria-label="kill all scripts" onClick={() => setKillOpen(true)}>
|
||||
<Tooltip title="Kill all running scripts">
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { TextFilePath } from "../Paths/TextFilePath";
|
||||
import type { Faction } from "../Faction/Faction";
|
||||
import type { Location } from "../Locations/Location";
|
||||
import type { SaveData } from "../types";
|
||||
import type { OptionsTabName } from "../GameOptions/ui/GameOptionsRoot";
|
||||
import { ComplexPage, SimplePage } from "./Enums";
|
||||
|
||||
// Using the same name as both type and object to mimic enum-like behavior.
|
||||
@@ -24,6 +25,8 @@ export type PageContext<T extends Page> = T extends ComplexPage.BitVerse
|
||||
? { saveData: SaveData; automatic?: boolean }
|
||||
: T extends ComplexPage.Documentation
|
||||
? { docPage?: string }
|
||||
: T extends ComplexPage.Options
|
||||
? { tab?: OptionsTabName }
|
||||
: never;
|
||||
|
||||
export type PageWithContext =
|
||||
@@ -34,6 +37,7 @@ export type PageWithContext =
|
||||
| ({ page: ComplexPage.Location } & PageContext<ComplexPage.Location>)
|
||||
| ({ page: ComplexPage.ImportSave } & PageContext<ComplexPage.ImportSave>)
|
||||
| ({ page: ComplexPage.Documentation } & PageContext<ComplexPage.Documentation>)
|
||||
| ({ page: ComplexPage.Options } & PageContext<ComplexPage.Options>)
|
||||
| { page: ComplexPage.LoadingScreen }
|
||||
| { page: SimplePage };
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export const GoToPageKeyBindingTypes = [
|
||||
SimplePage.Milestones,
|
||||
ComplexPage.Documentation,
|
||||
SimplePage.Achievements,
|
||||
SimplePage.Options,
|
||||
ComplexPage.Options,
|
||||
...SpoilerKeyBindingTypes,
|
||||
] as const;
|
||||
|
||||
@@ -213,7 +213,7 @@ export const DefaultKeyBindings: Record<KeyBindingType, [KeyCombination | null,
|
||||
null,
|
||||
],
|
||||
[SimplePage.Achievements]: [null, null],
|
||||
[SimplePage.Options]: [
|
||||
[ComplexPage.Options]: [
|
||||
{
|
||||
control: false,
|
||||
alt: true,
|
||||
|
||||
Reference in New Issue
Block a user