diff --git a/src/Settings/Settings.ts b/src/Settings/Settings.ts index 00b2f8d97..5872b5c68 100644 --- a/src/Settings/Settings.ts +++ b/src/Settings/Settings.ts @@ -1,6 +1,7 @@ import { ISelfInitializer, ISelfLoading } from "../types"; import { OwnedAugmentationsOrderSetting, PurchaseAugmentationsOrderSetting } from "./SettingEnums"; import { defaultTheme, ITheme } from "./Themes"; +import { defaultStyles, IStyleSettings } from "./Styles"; import { WordWrapOptions } from '../ScriptEditor/ui/Options'; /** @@ -114,7 +115,12 @@ interface IDefaultSettings { * Theme colors */ theme: ITheme; - + + /* + * Interface styles + */ + styles: IStyleSettings; + /* * Use GiB instead of GB */ @@ -171,6 +177,7 @@ export const defaultSettings: IDefaultSettings = { UseIEC60027_2: false, theme: defaultTheme, + styles: defaultStyles, }; /** @@ -209,6 +216,7 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = { MonacoWordWrap: 'off', theme: { ...defaultTheme }, + styles: { ...defaultStyles }, init() { Object.assign(Settings, defaultSettings); }, @@ -216,6 +224,8 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = { const save = JSON.parse(saveString); Object.assign(Settings.theme, save.theme); delete save.theme; + Object.assign(Settings.styles, save.styles); + delete save.styles; Object.assign(Settings, save); }, }; diff --git a/src/Settings/Styles.ts b/src/Settings/Styles.ts new file mode 100644 index 000000000..f231d9237 --- /dev/null +++ b/src/Settings/Styles.ts @@ -0,0 +1,11 @@ +import React from "react"; + +export interface IStyleSettings { + fontFamily: React.CSSProperties["fontFamily"]; + lineHeight: React.CSSProperties["lineHeight"]; +} + +export const defaultStyles: IStyleSettings = { + lineHeight: 1.5, + fontFamily: "Lucida Console, Lucida Sans Unicode, Fira Mono, Consolas, Courier New, Courier, monospace, Times New Roman" +} diff --git a/src/ui/GameRoot.tsx b/src/ui/GameRoot.tsx index 068b99b32..30e2958c8 100644 --- a/src/ui/GameRoot.tsx +++ b/src/ui/GameRoot.tsx @@ -77,6 +77,7 @@ import { enterBitNode } from "../RedPill"; import { Context } from "./Context"; import { RecoveryMode, RecoveryRoot } from "./React/RecoveryRoot"; import { AchievementsRoot } from "../Achievements/AchievementsRoot"; +import { Settings } from "../Settings/Settings"; const htmlLocation = location; diff --git a/src/ui/React/GameOptionsRoot.tsx b/src/ui/React/GameOptionsRoot.tsx index 37c36df7b..cff885893 100644 --- a/src/ui/React/GameOptionsRoot.tsx +++ b/src/ui/React/GameOptionsRoot.tsx @@ -28,6 +28,8 @@ import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal"; import { dialogBoxCreate } from "./DialogBox"; import { ConfirmationModal } from "./ConfirmationModal"; import { ThemeEditorModal } from "./ThemeEditorModal"; +import { StyleEditorModal } from "./StyleEditorModal"; + import { SnackbarEvents } from "./Snackbar"; import { Settings } from "../../Settings/Settings"; @@ -91,6 +93,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement { const [diagnosticOpen, setDiagnosticOpen] = useState(false); const [deleteGameOpen, setDeleteOpen] = useState(false); const [themeEditorOpen, setThemeEditorOpen] = useState(false); + const [styleEditorOpen, setStyleEditorOpen] = useState(false); const [softResetOpen, setSoftResetOpen] = useState(false); const [importSaveOpen, setImportSaveOpen] = useState(false); const [importData, setImportData] = useState(null); @@ -756,8 +759,6 @@ export function GameOptionsRoot(props: IProps): React.ReactElement { onConfirm={props.softReset} confirmationText={"This will perform the same action as installing Augmentations, are you sure?"} /> - - @@ -768,7 +769,10 @@ export function GameOptionsRoot(props: IProps): React.ReactElement { > + + + @@ -805,6 +809,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement { confirmationText={"Really delete your game? (It's permanent!)"} /> setThemeEditorOpen(false)} /> + setStyleEditorOpen(false)} /> ); } diff --git a/src/ui/React/StyleEditorModal.tsx b/src/ui/React/StyleEditorModal.tsx new file mode 100644 index 000000000..bd5d6820d --- /dev/null +++ b/src/ui/React/StyleEditorModal.tsx @@ -0,0 +1,157 @@ +import React, { useEffect, useState } from "react"; +import { Modal } from "./Modal"; + +import Button from "@mui/material/Button"; +import ButtonGroup from "@mui/material/ButtonGroup"; +import Typography from "@mui/material/Typography"; +import Paper from "@mui/material/Paper"; +import TextField from "@mui/material/TextField"; +import ReplyIcon from "@mui/icons-material/Reply"; +import SaveIcon from '@mui/icons-material/Save'; + +import { ThemeEvents } from "./Theme"; +import { Settings } from "../../Settings/Settings"; +import { IStyleSettings, defaultStyles } from "../../Settings/Styles"; +import { Tooltip } from "@mui/material"; + +interface IProps { + open: boolean; + onClose: () => void; +} + +interface FontFamilyProps { + value: React.CSSProperties["fontFamily"]; + onChange: (newValue: React.CSSProperties["fontFamily"], error?: string) => void; + refreshId: number; +} + +function FontFamilyField({ value, onChange, refreshId } : FontFamilyProps): React.ReactElement { + const [errorText, setErrorText] = useState(); + const [fontFamily, setFontFamily] = useState(value); + + function update(newValue: React.CSSProperties["fontFamily"]): void { + setFontFamily(newValue); + if (!newValue) { + setErrorText('Must have a value'); + } else { + setErrorText(''); + } + } + + function onTextChange(event: React.ChangeEvent): void { + update(event.target.value); + } + + useEffect(() => onChange(fontFamily, errorText), [fontFamily]); + useEffect(() => update(value), [refreshId]); + + return ( + + ) +} + +interface LineHeightProps { + value: React.CSSProperties["lineHeight"]; + onChange: (newValue: React.CSSProperties["lineHeight"], error?: string) => void; + refreshId: number; +} + +function LineHeightField({ value, onChange, refreshId } : LineHeightProps): React.ReactElement { + const [errorText, setErrorText] = useState(); + const [lineHeight, setLineHeight] = useState(value); + + function update(newValue: React.CSSProperties["lineHeight"]): void { + setLineHeight(newValue); + if (!newValue) { + setErrorText('Must have a value'); + } else if (isNaN(Number(newValue))) { + setErrorText('Must be a number'); + } else { + setErrorText(''); + } + } + + function onTextChange(event: React.ChangeEvent): void { + update(event.target.value); + } + + useEffect(() => onChange(lineHeight, errorText), [lineHeight]); + useEffect(() => update(value), [refreshId]); + + return ( + + ) +} + +export function StyleEditorModal(props: IProps): React.ReactElement { + const [refreshId, setRefreshId] = useState(0); + const [error, setError] = useState(); + const [customStyle, setCustomStyle] = useState({ + ...Settings.styles, + }); + + function persistToSettings(styles: IStyleSettings): void { + Object.assign(Settings.styles, styles); + ThemeEvents.emit(); + } + + function saveStyles(): void { + persistToSettings(customStyle); + } + + function setDefaults(): void { + const styles = {...defaultStyles} + setCustomStyle(styles); + persistToSettings(styles); + setRefreshId(refreshId + 1); + } + + function update(styles: IStyleSettings, errorMessage?: string): void { + setError(errorMessage); + if (!errorMessage) { + setCustomStyle(styles); + } + } + + return ( + + Styles Editor + + WARNING: Changing styles may mess up the interface. Drastic changes are NOT recommended. + + + update({ ...customStyle, fontFamily: value }, error)} /> +
+ update({ ...customStyle, lineHeight: value }, error)} /> +
+ + + + + + +
+
+ ); +} diff --git a/src/ui/React/Theme.tsx b/src/ui/React/Theme.tsx index f25c554b1..fa28f6793 100644 --- a/src/ui/React/Theme.tsx +++ b/src/ui/React/Theme.tsx @@ -107,8 +107,7 @@ export function refreshTheme(): void { }, }, typography: { - fontFamily: - "Lucida Console, Lucida Sans Unicode, Fira Mono, Consolas, Courier New, Courier, monospace, Times New Roman", + fontFamily: Settings.styles.fontFamily, button: { textTransform: "none", }, @@ -160,6 +159,17 @@ export function refreshTheme(): void { }, }, }, + + MuiButtonGroup: { + styleOverrides: { + root: { + '& .MuiButton-root:not(:last-of-type)': { + marginRight: '1px', + } + } + } + }, + MuiButton: { styleOverrides: { root: { @@ -193,6 +203,11 @@ export function refreshTheme(): void { defaultProps: { color: "primary", }, + styleOverrides: { + root: { + lineHeight: Settings.styles.lineHeight, + } + } }, MuiMenu: { styleOverrides: {