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 ( + <> + + + + 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 ( + <> + + + + 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} + + + + + + + + + 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 ( + + + + + } + 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 { - - - + + + + + + @@ -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; }