diff --git a/src/NetscriptFunctions/UserInterface.ts b/src/NetscriptFunctions/UserInterface.ts
index ead063a9f..7b342a9a7 100644
--- a/src/NetscriptFunctions/UserInterface.ts
+++ b/src/NetscriptFunctions/UserInterface.ts
@@ -6,7 +6,7 @@ import { GameInfo, IStyleSettings, UserInterface as IUserInterface, UserInterfac
import { Settings } from "../Settings/Settings";
import { ThemeEvents } from "../Themes/ui/Theme";
import { defaultTheme } from "../Themes/Themes";
-import { defaultStyles } from "../Settings/Styles";
+import { defaultStyles } from "../Themes/Styles";
import { CONSTANTS } from "../Constants";
import { hash } from "../hash/hash";
diff --git a/src/Settings/Settings.ts b/src/Settings/Settings.ts
index d0563f604..77c29fdb9 100644
--- a/src/Settings/Settings.ts
+++ b/src/Settings/Settings.ts
@@ -1,7 +1,7 @@
import { ISelfInitializer, ISelfLoading } from "../types";
import { OwnedAugmentationsOrderSetting, PurchaseAugmentationsOrderSetting } from "./SettingEnums";
import { defaultTheme, ITheme } from "../Themes/Themes";
-import { defaultStyles } from "./Styles";
+import { defaultStyles } from "../Themes/Styles";
import { WordWrapOptions } from "../ScriptEditor/ui/Options";
import { OverviewSettings } from "../ui/React/Overview";
import { IStyleSettings } from "../ScriptEditor/NetscriptDefinitions";
diff --git a/src/Themes/README.md b/src/Themes/README.md
index 0638cf74b..3dffe710d 100644
--- a/src/Themes/README.md
+++ b/src/Themes/README.md
@@ -8,7 +8,8 @@ See [CONTRIBUTING.md](/CONTRIBUTING.md) for details.
1. Duplicate one of the folders in `/src/Themes/data` and give it a new name (keep the hyphenated format)
2. Modify the data in the new `/src/Themes/data/new-folder/index.ts` file
-3. Add the import/export into the `/src/Themes/data/index.ts` file
+3. Replace the screenshot.png with one of your theme
+4. Add the import/export into the `/src/Themes/data/index.ts` file
The themes are ordered according to the export order in `index.ts`
diff --git a/src/Settings/Styles.ts b/src/Themes/Styles.ts
similarity index 100%
rename from src/Settings/Styles.ts
rename to src/Themes/Styles.ts
diff --git a/src/Themes/ui/StyleEditorButton.tsx b/src/Themes/ui/StyleEditorButton.tsx
new file mode 100644
index 000000000..4fc7648ea
--- /dev/null
+++ b/src/Themes/ui/StyleEditorButton.tsx
@@ -0,0 +1,19 @@
+import React, { useState } from "react";
+import Button from "@mui/material/Button";
+import Tooltip from "@mui/material/Tooltip";
+import TextFormatIcon from "@mui/icons-material/TextFormat";
+import { StyleEditorModal } from "./StyleEditorModal";
+
+export function StyleEditorButton(): React.ReactElement {
+ const [styleEditorOpen, setStyleEditorOpen] = useState(false);
+ return (
+ <>
+
+ } onClick={() => setStyleEditorOpen(true)}>
+ Style Editor
+
+
+ setStyleEditorOpen(false)} />
+ >
+ );
+}
diff --git a/src/ui/React/StyleEditorModal.tsx b/src/Themes/ui/StyleEditorModal.tsx
similarity index 97%
rename from src/ui/React/StyleEditorModal.tsx
rename to src/Themes/ui/StyleEditorModal.tsx
index b8213f199..b5b7bec4c 100644
--- a/src/ui/React/StyleEditorModal.tsx
+++ b/src/Themes/ui/StyleEditorModal.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
-import { Modal } from "./Modal";
+import { Modal } from "../../ui/React/Modal";
import Button from "@mui/material/Button";
import ButtonGroup from "@mui/material/ButtonGroup";
@@ -9,9 +9,9 @@ import TextField from "@mui/material/TextField";
import ReplyIcon from "@mui/icons-material/Reply";
import SaveIcon from "@mui/icons-material/Save";
-import { ThemeEvents } from "../../Themes/ui/Theme";
+import { ThemeEvents } from "./Theme";
import { Settings } from "../../Settings/Settings";
-import { defaultStyles } from "../../Settings/Styles";
+import { defaultStyles } from "../Styles";
import { Tooltip } from "@mui/material";
import { IStyleSettings } from "../../ScriptEditor/NetscriptDefinitions";
diff --git a/src/Themes/ui/ThemeBrowser.tsx b/src/Themes/ui/ThemeBrowser.tsx
new file mode 100644
index 000000000..cef35e63d
--- /dev/null
+++ b/src/Themes/ui/ThemeBrowser.tsx
@@ -0,0 +1,93 @@
+import React, { useEffect, useState } from "react";
+import Typography from "@mui/material/Typography";
+import Paper from "@mui/material/Paper";
+import { ThemeEvents } from "./Theme";
+import { Settings } from "../../Settings/Settings";
+import { getPredefinedThemes, IPredefinedTheme } from "../Themes";
+import { Box, ButtonGroup, Button } from "@mui/material";
+import { IRouter } from "../../ui/Router";
+import { ThemeEditorButton } from "./ThemeEditorButton";
+import { StyleEditorButton } from "./StyleEditorButton";
+import { ThemeEntry } from "./ThemeEntry";
+import { ThemeCollaborate } from "./ThemeCollaborate";
+import { Modal } from "../../ui/React/Modal";
+import { SnackbarEvents } from "../../ui/React/Snackbar";
+
+interface IProps {
+ router: IRouter;
+}
+
+// Everything dies when the theme gets reloaded, so we'll keep the current scroll to not jump around.
+let previousScrollY = 0;
+
+export function ThemeBrowser({ router }: IProps): React.ReactElement {
+ const [modalOpen, setModalOpen] = useState(false);
+ const [modalImageSrc, setModalImageSrc] = useState();
+ const predefinedThemes = getPredefinedThemes();
+ const themes = (predefinedThemes &&
+ Object.entries(predefinedThemes).map(([key, templateTheme]) => (
+ setTheme(templateTheme)}
+ onImageClick={handleZoom}
+ />
+ ))) || <>>;
+
+ function setTheme(theme: IPredefinedTheme): void {
+ previousScrollY = window.scrollY;
+ const previousColors = { ...Settings.theme };
+ Object.assign(Settings.theme, theme.colors);
+ ThemeEvents.emit();
+ SnackbarEvents.emit(
+ <>
+ Updated theme to "{theme.name}"
+
+ >,
+ "info",
+ 30000,
+ );
+ }
+
+ function handleZoom(src: string): void {
+ previousScrollY = window.scrollY;
+ setModalImageSrc(src);
+ setModalOpen(true);
+ }
+
+ function handleCloseZoom(): void {
+ previousScrollY = window.scrollY;
+ setModalOpen(false);
+ }
+
+ useEffect(() => {
+ requestAnimationFrame(() => window.scrollTo(0, previousScrollY));
+ });
+
+ return (
+
+ Theme Browser
+
+
+
+
+
+
+ {themes}
+
+
+
+
+
+ );
+}
diff --git a/src/Themes/ui/ThemeCollaborate.tsx b/src/Themes/ui/ThemeCollaborate.tsx
new file mode 100644
index 000000000..9058e64c5
--- /dev/null
+++ b/src/Themes/ui/ThemeCollaborate.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+import Typography from "@mui/material/Typography";
+import { Link } from "@mui/material";
+
+export function ThemeCollaborate(): React.ReactElement {
+ return (
+ <>
+
+ If you've created a theme that you believe should be added in game's theme browser, feel free to{" "}
+
+ create a pull request
+
+ .
+
+
+ Head over to the{" "}
+
+ theme-sharing
+ {" "}
+ discord channel for more.
+
+ >
+ );
+}
diff --git a/src/Themes/ui/ThemeEditorButton.tsx b/src/Themes/ui/ThemeEditorButton.tsx
new file mode 100644
index 000000000..7decbad68
--- /dev/null
+++ b/src/Themes/ui/ThemeEditorButton.tsx
@@ -0,0 +1,24 @@
+import React, { useState } from "react";
+import Button from "@mui/material/Button";
+import Tooltip from "@mui/material/Tooltip";
+import { ThemeEditorModal } from "./ThemeEditorModal";
+import { IRouter } from "../../ui/Router";
+import ColorizeIcon from "@mui/icons-material/Colorize";
+
+interface IProps {
+ router: IRouter;
+}
+
+export function ThemeEditorButton({ router }: IProps): React.ReactElement {
+ const [themeEditorOpen, setThemeEditorOpen] = useState(false);
+ return (
+ <>
+
+ } onClick={() => setThemeEditorOpen(true)}>
+ Theme Editor
+
+
+ setThemeEditorOpen(false)} router={router} />
+ >
+ );
+}
diff --git a/src/Themes/ui/ThemeEditorModal.tsx b/src/Themes/ui/ThemeEditorModal.tsx
index da16628c3..986f7420d 100644
--- a/src/Themes/ui/ThemeEditorModal.tsx
+++ b/src/Themes/ui/ThemeEditorModal.tsx
@@ -1,6 +1,7 @@
import React, { useState } from "react";
import { Modal } from "../../ui/React/Modal";
import Button from "@mui/material/Button";
+import ButtonGroup from "@mui/material/ButtonGroup";
import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip";
import Paper from "@mui/material/Paper";
@@ -8,15 +9,19 @@ import TextField from "@mui/material/TextField";
import IconButton from "@mui/material/IconButton";
import ReplyIcon from "@mui/icons-material/Reply";
import PaletteSharpIcon from "@mui/icons-material/PaletteSharp";
+import HistoryIcon from '@mui/icons-material/History';
import { Color, ColorPicker } from "material-ui-color";
import { ThemeEvents } from "./Theme";
import { Settings, defaultSettings } from "../../Settings/Settings";
-import { getPredefinedThemes } from "../Themes";
+import { defaultTheme } from "../Themes";
import { UserInterfaceTheme } from "../../ScriptEditor/NetscriptDefinitions";
+import { IRouter } from "../../ui/Router";
+import { ThemeCollaborate } from "./ThemeCollaborate";
interface IProps {
open: boolean;
onClose: () => void;
+ router: IRouter;
}
interface IColorEditorProps {
@@ -68,28 +73,6 @@ export function ThemeEditorModal(props: IProps): React.ReactElement {
...Settings.theme,
});
- const predefinedThemes = getPredefinedThemes();
- const themes = predefinedThemes && Object.entries(predefinedThemes)
- .map(([key, templateTheme]) => {
- const name = templateTheme.name || key;
- let inner = {name};
- let toolTipTitle;
- if (templateTheme.credit) {
- toolTipTitle = {templateTheme.description || name} by {templateTheme.credit};
- } else if (templateTheme.description) {
- toolTipTitle = {templateTheme.description};
- }
- if (toolTipTitle) {
- inner = {inner}
- }
- return (
-
- );
- }) || <>>;
-
function setTheme(theme: UserInterfaceTheme): void {
setCustomTheme(theme);
Object.assign(Settings.theme, theme);
@@ -372,8 +355,18 @@ export function ThemeEditorModal(props: IProps): React.ReactElement {
/>
<>
Backup your theme or share it with others by copying the string above.
- Replace the current theme with a pre-built template using the buttons below.
- {themes}
+
+
+
+
+
+
+ } onClick={() => props.router.toThemeBrowser()}>See more themes
+
+
>
diff --git a/src/Themes/ui/ThemeEntry.tsx b/src/Themes/ui/ThemeEntry.tsx
new file mode 100644
index 000000000..019f54e59
--- /dev/null
+++ b/src/Themes/ui/ThemeEntry.tsx
@@ -0,0 +1,77 @@
+import React from "react";
+import Typography from "@mui/material/Typography";
+import Tooltip from "@mui/material/Tooltip";
+import PaletteSharpIcon from "@mui/icons-material/PaletteSharp";
+import { Settings } from "../../Settings/Settings";
+import { IPredefinedTheme } from "../Themes";
+import { Link, Card, CardHeader, CardContent, CardMedia, Button } from "@mui/material";
+
+interface IProps {
+ theme: IPredefinedTheme;
+ onActivated: () => void;
+ onImageClick: (src: string) => void;
+}
+
+export function ThemeEntry({ theme, onActivated, onImageClick }: IProps): React.ReactElement {
+ if (!theme) return <>>;
+ return (
+
+
+ } onClick={onActivated} variant="outlined">
+ Use
+
+
+ }
+ title={theme.name}
+ subheader={
+ <>
+ by {theme.credit}{" "}
+ {theme.reference && (
+ <>
+ (
+
+ ref
+
+ )
+ >
+ )}
+ >
+ }
+ sx={{
+ color: Settings.theme.primary,
+ "& .MuiCardHeader-subheader": {
+ color: Settings.theme.secondarydark,
+ },
+ "& .MuiButton-outlined": {
+ backgroundColor: "transparent",
+ },
+ }}
+ />
+ onImageClick(theme.screenshot)}
+ />
+
+
+ {theme.description}
+
+
+
+ );
+}
diff --git a/src/ui/GameRoot.tsx b/src/ui/GameRoot.tsx
index 2e7a265cc..86c64c018 100644
--- a/src/ui/GameRoot.tsx
+++ b/src/ui/GameRoot.tsx
@@ -79,6 +79,7 @@ import { RecoveryMode, RecoveryRoot } from "./React/RecoveryRoot";
import { AchievementsRoot } from "../Achievements/AchievementsRoot";
import { ErrorBoundary } from "./ErrorBoundary";
import { Settings } from "../Settings/Settings";
+import { ThemeBrowser } from "../Themes/ui/ThemeBrowser";
const htmlLocation = location;
@@ -194,6 +195,9 @@ export let Router: IRouter = {
toAchievements: () => {
throw new Error("Router called before initialization");
},
+ toThemeBrowser: () => {
+ throw new Error("Router called before initialization");
+ },
};
function determineStartPage(player: IPlayer): Page {
@@ -307,6 +311,9 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
toAchievements: () => {
setPage(Page.Achievements);
},
+ toThemeBrowser: () => {
+ setPage(Page.ThemeBrowser);
+ }
};
useEffect(() => {
@@ -471,6 +478,7 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
mainPage = (
saveObject.saveGame()}
export={() => {
// Apply the export bonus before saving the game
@@ -503,6 +511,10 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
mainPage = ;
break;
}
+ case Page.ThemeBrowser: {
+ mainPage = ;
+ break;
+ }
}
return (
diff --git a/src/ui/React/GameOptionsRoot.tsx b/src/ui/React/GameOptionsRoot.tsx
index acc1835bb..4d331fba5 100644
--- a/src/ui/React/GameOptionsRoot.tsx
+++ b/src/ui/React/GameOptionsRoot.tsx
@@ -22,12 +22,11 @@ import TextField from "@mui/material/TextField";
import DownloadIcon from "@mui/icons-material/Download";
import UploadIcon from "@mui/icons-material/Upload";
import SaveIcon from "@mui/icons-material/Save";
+import PaletteIcon from '@mui/icons-material/Palette';
import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal";
import { dialogBoxCreate } from "./DialogBox";
import { ConfirmationModal } from "./ConfirmationModal";
-import { ThemeEditorModal } from "../../Themes/ui/ThemeEditorModal";
-import { StyleEditorModal } from "./StyleEditorModal";
import { SnackbarEvents } from "./Snackbar";
@@ -37,6 +36,9 @@ import { formatTime } from "../../utils/helpers/formatTime";
import { OptionSwitch } from "./OptionSwitch";
import { DeleteGameButton } from "./DeleteGameButton";
import { SoftResetButton } from "./SoftResetButton";
+import { IRouter } from "../Router";
+import { ThemeEditorButton } from "../../Themes/ui/ThemeEditorButton";
+import { StyleEditorButton } from "../../Themes/ui/StyleEditorButton";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -50,6 +52,7 @@ const useStyles = makeStyles((theme: Theme) =>
interface IProps {
player: IPlayer;
+ router: IRouter;
save: () => void;
export: () => void;
forceKill: () => void;
@@ -74,8 +77,6 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat);
const [locale, setLocale] = useState(Settings.Locale);
const [diagnosticOpen, setDiagnosticOpen] = useState(false);
- const [themeEditorOpen, setThemeEditorOpen] = useState(false);
- const [styleEditorOpen, setStyleEditorOpen] = useState(false);
const [importSaveOpen, setImportSaveOpen] = useState(false);
const [importData, setImportData] = useState(null);
@@ -642,9 +643,14 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
-
-
-
+
+
+ } onClick={() => props.router.toThemeBrowser()}>
+ Theme Browser
+
+
+
+
@@ -669,8 +675,6 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
setDiagnosticOpen(false)} />
- setThemeEditorOpen(false)} />
- setStyleEditorOpen(false)} />
);
}
diff --git a/src/ui/React/Snackbar.tsx b/src/ui/React/Snackbar.tsx
index 68b98e945..d1648b027 100644
--- a/src/ui/React/Snackbar.tsx
+++ b/src/ui/React/Snackbar.tsx
@@ -14,6 +14,10 @@ const useStyles = makeStyles(() => ({
snackbar: {
// Log popup z-index increments, so let's add a padding to be well above them.
zIndex: `${logBoxBaseZIndex + 1000} !important` as any,
+
+ "& .MuiAlert-icon": {
+ alignSelf: 'center',
+ },
}
}));
@@ -27,7 +31,7 @@ export function SnackbarProvider(props: IProps): React.ReactElement {
);
}
-export const SnackbarEvents = new EventEmitter<[string, "success" | "warning" | "error" | "info", number]>();
+export const SnackbarEvents = new EventEmitter<[string | React.ReactNode, "success" | "warning" | "error" | "info", number]>();
export function Snackbar(): React.ReactElement {
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
diff --git a/src/ui/Router.ts b/src/ui/Router.ts
index 4056a44ac..7ebeb31d8 100644
--- a/src/ui/Router.ts
+++ b/src/ui/Router.ts
@@ -37,6 +37,7 @@ export enum Page {
StaneksGift,
Recovery,
Achievements,
+ ThemeBrowser,
}
export interface ScriptEditorRouteOptions {
@@ -82,4 +83,5 @@ export interface IRouter {
toLocation(location: Location): void;
toStaneksGift(): void;
toAchievements(): void;
+ toThemeBrowser(): void;
}