Merge pull request #2707 from MartinFournier/feature/theme-browser

Add Theme Browser accessible from GameOptions
This commit is contained in:
hydroflame 2022-01-26 00:46:17 -05:00 committed by GitHub
commit 7a5dfd0e72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 1034 additions and 658 deletions

@ -8,4 +8,8 @@ module.exports = {
'.cypress', 'node_modules', 'dist', '.cypress', 'node_modules', 'dist',
], ],
testEnvironment: "jsdom", testEnvironment: "jsdom",
moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js"
}
}; };

@ -71,6 +71,7 @@
"electron": "^14.0.2", "electron": "^14.0.2",
"electron-packager": "^15.4.0", "electron-packager": "^15.4.0",
"eslint": "^7.24.0", "eslint": "^7.24.0",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^6.3.3", "fork-ts-checker-webpack-plugin": "^6.3.3",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"http-server": "^13.0.1", "http-server": "^13.0.1",

@ -11,6 +11,7 @@ cp index.html .package
cp -r electron/* .package cp -r electron/* .package
cp -r dist/ext .package/dist cp -r dist/ext .package/dist
cp -r dist/icons .package/dist cp -r dist/icons .package/dist
cp -r dist/images .package/dist
# The css files # The css files
cp dist/vendor.css .package/dist cp dist/vendor.css .package/dist

@ -1,2 +1,8 @@
// Defined by webpack on startup or compilation // Defined by webpack on startup or compilation
declare let __COMMIT_HASH__: string; declare let __COMMIT_HASH__: string;
// When using file-loader, we'll get a path to the resource
declare module "*.png" {
const value: string;
export default value;
}

@ -4,9 +4,9 @@ import { IPlayer } from "../PersonObjects/IPlayer";
import { getRamCost } from "../Netscript/RamCostGenerator"; import { getRamCost } from "../Netscript/RamCostGenerator";
import { GameInfo, IStyleSettings, UserInterface as IUserInterface, UserInterfaceTheme } from "../ScriptEditor/NetscriptDefinitions"; import { GameInfo, IStyleSettings, UserInterface as IUserInterface, UserInterfaceTheme } from "../ScriptEditor/NetscriptDefinitions";
import { Settings } from "../Settings/Settings"; import { Settings } from "../Settings/Settings";
import { ThemeEvents } from "../ui/React/Theme"; import { ThemeEvents } from "../Themes/ui/Theme";
import { defaultTheme } from "../Settings/Themes"; import { defaultTheme } from "../Themes/Themes";
import { defaultStyles } from "../Settings/Styles"; import { defaultStyles } from "../Themes/Styles";
import { CONSTANTS } from "../Constants"; import { CONSTANTS } from "../Constants";
import { hash } from "../hash/hash"; import { hash } from "../hash/hash";

@ -1,7 +1,7 @@
import { ISelfInitializer, ISelfLoading } from "../types"; import { ISelfInitializer, ISelfLoading } from "../types";
import { OwnedAugmentationsOrderSetting, PurchaseAugmentationsOrderSetting } from "./SettingEnums"; import { OwnedAugmentationsOrderSetting, PurchaseAugmentationsOrderSetting } from "./SettingEnums";
import { defaultTheme, ITheme } from "./Themes"; import { defaultTheme, ITheme } from "../Themes/Themes";
import { defaultStyles } from "./Styles"; import { defaultStyles } from "../Themes/Styles";
import { WordWrapOptions } from "../ScriptEditor/ui/Options"; import { WordWrapOptions } from "../ScriptEditor/ui/Options";
import { OverviewSettings } from "../ui/React/Overview"; import { OverviewSettings } from "../ui/React/Overview";
import { IStyleSettings } from "../ScriptEditor/NetscriptDefinitions"; import { IStyleSettings } from "../ScriptEditor/NetscriptDefinitions";

@ -1,613 +0,0 @@
import { IMap } from "../types";
export interface ITheme {
[key: string]: string | undefined;
primarylight: string;
primary: string;
primarydark: string;
successlight: string;
success: string;
successdark: string;
errorlight: string;
error: string;
errordark: string;
secondarylight: string;
secondary: string;
secondarydark: string;
warninglight: string;
warning: string;
warningdark: string;
infolight: string;
info: string;
infodark: string;
welllight: string;
well: string;
white: string;
black: string;
hp: string;
money: string;
hack: string;
combat: string;
cha: string;
int: string;
rep: string;
disabled: string;
backgroundprimary: string;
backgroundsecondary: string;
button: string;
}
export interface IPredefinedTheme {
colors: ITheme;
name?: string;
credit?: string;
description?: string;
reference?: string;
}
export const defaultTheme: ITheme = {
primarylight: "#0f0",
primary: "#0c0",
primarydark: "#090",
successlight: "#0f0",
success: "#0c0",
successdark: "#090",
errorlight: "#f00",
error: "#c00",
errordark: "#900",
secondarylight: "#AAA",
secondary: "#888",
secondarydark: "#666",
warninglight: "#ff0",
warning: "#cc0",
warningdark: "#990",
infolight: "#69f",
info: "#36c",
infodark: "#039",
welllight: "#444",
well: "#222",
white: "#fff",
black: "#000",
hp: "#dd3434",
money: "#ffd700",
hack: "#adff2f",
combat: "#faffdf",
cha: "#a671d1",
int: "#6495ed",
rep: "#faffdf",
disabled: "#66cfbc",
backgroundprimary: "#000",
backgroundsecondary: "#000",
button: "#333",
};
export const getPredefinedThemes = (): IMap<IPredefinedTheme> => ({
Default: {
colors: defaultTheme,
},
Monokai: {
description: "Monokai'ish",
colors: {
primarylight: "#FFF",
primary: "#F8F8F2",
primarydark: "#FAFAEB",
successlight: "#ADE146",
success: "#A6E22E",
successdark: "#98E104",
errorlight: "#FF69A0",
error: "#F92672",
errordark: "#D10F56",
secondarylight: "#AAA",
secondary: "#888",
secondarydark: "#666",
warninglight: "#E1D992",
warning: "#E6DB74",
warningdark: "#EDDD54",
infolight: "#92E1F1",
info: "#66D9EF",
infodark: "#31CDED",
welllight: "#444",
well: "#222",
white: "#fff",
black: "#000",
hp: "#F92672",
money: "#E6DB74",
hack: "#A6E22E",
combat: "#75715E",
cha: "#AE81FF",
int: "#66D9EF",
rep: "#E69F66",
disabled: "#66cfbc",
backgroundprimary: "#272822",
backgroundsecondary: "#1B1C18",
button: "#333",
},
},
Warmer: {
credit: "hexnaught",
description: "Warmer, softer theme",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/921999581020028938",
colors: {
primarylight: "#EA9062",
primary: "#DD7B4A",
primarydark: "#D3591C",
successlight: "#6ACF6A",
success: "#43BF43",
successdark: "#3E913E",
errorlight: "#C15757",
error: "#B34141",
errordark: "#752525",
secondarylight: "#AAA",
secondary: "#888",
secondarydark: "#666",
warninglight: "#E6E69D",
warning: "#DADA56",
warningdark: "#A1A106",
infolight: "#69f",
info: "#36c",
infodark: "#039",
welllight: "#444",
well: "#222",
white: "#fff",
black: "#000",
hp: "#dd3434",
money: "#ffd700",
hack: "#adff2f",
combat: "#faffdf",
cha: "#AD84CF",
int: "#6495ed",
rep: "#faffdf",
disabled: "#76C6B7",
backgroundprimary: "#000",
backgroundsecondary: "#000",
button: "#333",
},
},
"Dark+": {
credit: "LoganMD",
description: "VSCode Dark+",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/921999975867617310",
colors: {
primarylight: "#E0E0BC",
primary: "#CCCCAE",
primarydark: "#B8B89C",
successlight: "#00F000",
success: "#00D200",
successdark: "#00B400",
errorlight: "#F00000",
error: "#C80000",
errordark: "#A00000",
secondarylight: "#B4AEAE",
secondary: "#969090",
secondarydark: "#787272",
warninglight: "#F0F000",
warning: "#C8C800",
warningdark: "#A0A000",
infolight: "#69f",
info: "#36c",
infodark: "#039",
welllight: "#444",
well: "#222",
white: "#fff",
black: "#1E1E1E",
hp: "#dd3434",
money: "#ffd700",
hack: "#adff2f",
combat: "#faffdf",
cha: "#a671d1",
int: "#6495ed",
rep: "#faffdf",
disabled: "#66cfbc",
backgroundprimary: "#1E1E1E",
backgroundsecondary: "#252525",
button: "#333",
},
},
"Mayukai Dark": {
credit: "Festive Noire",
description: "Mayukai Dark-esque",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922037502334889994",
colors: {
primarylight: "#DDDFC5",
primary: "#CDCFB6",
primarydark: "#9D9F8C",
successlight: "#00EF00",
success: "#00A500",
successdark: "#007A00",
errorlight: "#F92672",
error: "#CA1C5C",
errordark: "#90274A",
secondarylight: "#AAA",
secondary: "#888",
secondarydark: "#666",
warninglight: "#D3D300",
warning: "#cc0",
warningdark: "#990",
infolight: "#69f",
info: "#36c",
infodark: "#039",
welllight: "#444",
well: "#00010A",
white: "#fff",
black: "#020509",
hp: "#dd3434",
money: "#ffd700",
hack: "#8CCF27",
combat: "#faffdf",
cha: "#a671d1",
int: "#6495ed",
rep: "#faffdf",
disabled: "#66cfbc",
backgroundprimary: "#080C11",
backgroundsecondary: "#03080F",
button: "#00010A",
},
},
Purple: {
credit: "zer0ney",
description: "Essentially all defaults except for purple replacing the main colors",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922091815849570395",
colors: {
primarylight: "#BA55D3",
primary: "#9370DB",
primarydark: "#8A2BE2",
successlight: "#BA55D3",
success: "#9370DB",
successdark: "#8A2BE2",
errorlight: "#f00",
error: "#c00",
errordark: "#900",
secondarylight: "#AAA",
secondary: "#888",
secondarydark: "#666",
warninglight: "#ff0",
warning: "#cc0",
warningdark: "#990",
infolight: "#69f",
info: "#36c",
infodark: "#039",
welllight: "#444",
well: "#222",
white: "#fff",
black: "#000",
hp: "#dd3434",
money: "#ffd700",
hack: "#adff2f",
combat: "#faffdf",
cha: "#a671d1",
int: "#6495ed",
rep: "#faffdf",
disabled: "#66cfbc",
backgroundprimary: "#000",
backgroundsecondary: "#000",
button: "#333",
},
},
"Smooth Green": {
credit: "Swidt",
description: "A nice green theme that doesn't hurt your eyes.",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922243957986033725",
colors: {
primarylight: "#E0E0BC",
primary: "#B0D9A3",
primarydark: "#B8B89C",
successlight: "#00F000",
success: "#6BC16B",
successdark: "#00B400",
errorlight: "#F00000",
error: "#3D713D",
errordark: "#A00000",
secondarylight: "#B4AEAE",
secondary: "#8FAF85",
secondarydark: "#787272",
warninglight: "#F0F000",
warning: "#38F100",
warningdark: "#A0A000",
infolight: "#69f",
info: "#36c",
infodark: "#039",
welllight: "#444",
well: "#2F3C2B",
white: "#fff",
black: "#1E1E1E",
hp: "#dd3434",
money: "#4AA52E",
hack: "#adff2f",
combat: "#faffdf",
cha: "#a671d1",
int: "#6495ed",
rep: "#35A135",
disabled: "#66cfbc",
backgroundprimary: "#1E1E1E",
backgroundsecondary: "#252525",
button: "#2F3C2B",
},
},
Dracula: {
credit: "H3draut3r",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922296307836678144",
colors: {
primarylight: "#7082B8",
primary: "#F8F8F2",
primarydark: "#FF79C6",
successlight: "#0f0",
success: "#0c0",
successdark: "#090",
errorlight: "#FD4545",
error: "#FF2D2D",
errordark: "#C62424",
secondarylight: "#AAA",
secondary: "#8BE9FD",
secondarydark: "#666",
warninglight: "#FFC281",
warning: "#FFB86C",
warningdark: "#E6A055",
infolight: "#A0A0FF",
info: "#7070FF",
infodark: "#4040FF",
welllight: "#44475A",
well: "#363948",
white: "#fff",
black: "#282A36",
hp: "#D34448",
money: "#50FA7B",
hack: "#F1FA8C",
combat: "#BD93F9",
cha: "#FF79C6",
int: "#6495ed",
rep: "#faffdf",
disabled: "#66cfbc",
backgroundprimary: "#282A36",
backgroundsecondary: "#21222C",
button: "#21222C",
},
},
"Dark Blue": {
credit: "Saynt_Garmo",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/923084732718264340",
colors: {
primarylight: "#023DDE",
primary: "#4A41C8",
primarydark: "#005299",
successlight: "#00FF00",
success: "#D1DAD1",
successdark: "#BFCABF",
errorlight: "#f00",
error: "#c00",
errordark: "#900",
secondarylight: "#AAA",
secondary: "#888",
secondarydark: "#666",
warninglight: "#ff0",
warning: "#cc0",
warningdark: "#990",
infolight: "#69f",
info: "#36c",
infodark: "#039",
welllight: "#444",
well: "#040505",
white: "#fff",
black: "#000000",
hp: "#dd3434",
money: "#ffd700",
hack: "#adff2f",
combat: "#faffdf",
cha: "#a671d1",
int: "#6495ed",
rep: "#faffdf",
disabled: "#66cfbc",
backgroundprimary: "#091419",
backgroundsecondary: "#000000",
button: "#000000",
},
},
Discord: {
credit: "Thermite",
description: "Discord inspired theme",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924305252017143818",
colors: {
primarylight: "#7389DC",
primary: "#7389DC",
primarydark: "#5964F1",
successlight: "#00CC00",
success: "#20DF20",
successdark: "#0CB80C",
errorlight: "#EA5558",
error: "#EC4145",
errordark: "#E82528",
secondarylight: "#C3C3C3",
secondary: "#9C9C9C",
secondarydark: "#4E4E4E",
warninglight: "#ff0",
warning: "#cc0",
warningdark: "#990",
infolight: "#69f",
info: "#36c",
infodark: "#1C4FB3",
welllight: "#999999",
well: "#35383C",
white: "#FFFFFF",
black: "#202225",
hp: "#FF5656",
money: "#43FF43",
hack: "#FFAB3D",
combat: "#8A90FD",
cha: "#FF51D9",
int: "#6495ed",
rep: "#FFFF30",
disabled: "#474B51",
backgroundprimary: "#2F3136",
backgroundsecondary: "#35393E",
button: "#333",
},
},
"One Dark": {
credit: "Dexalt142",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924650660694208512",
colors: {
primarylight: "#98C379",
primary: "#98C379",
primarydark: "#98C379",
successlight: "#98C379",
success: "#98C379",
successdark: "#98C379",
errorlight: "#E06C75",
error: "#BE5046",
errordark: "#BE5046",
secondarylight: "#AAA",
secondary: "#888",
secondarydark: "#666",
warninglight: "#E5C07B",
warning: "#E5C07B",
warningdark: "#D19A66",
infolight: "#61AFEF",
info: "#61AFEF",
infodark: "#61AFEF",
welllight: "#4B5263",
well: "#282C34",
white: "#ABB2BF",
black: "#282C34",
hp: "#E06C75",
money: "#E5C07B",
hack: "#98C379",
combat: "#ABB2BF",
cha: "#C678DD",
int: "#61AFEF",
rep: "#ABB2BF",
disabled: "#56B6C2",
backgroundprimary: "#282C34",
backgroundsecondary: "#21252B",
button: "#4B5263",
},
},
"Muted Gold & Blue": {
credit: "Sloth",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924672660758208563",
colors: {
primarylight: "#E3B54A",
primary: "#CAA243",
primarydark: "#7E6937",
successlight: "#82FF82",
success: "#6FDA6F",
successdark: "#64C364",
errorlight: "#FD5555",
error: "#D84A4A",
errordark: "#AC3939",
secondarylight: "#D8D0B8",
secondary: "#B1AA95",
secondarydark: "#736E5E",
warninglight: "#ff0",
warning: "#cc0",
warningdark: "#990",
infolight: "#69f",
info: "#36c",
infodark: "#039",
welllight: "#444",
well: "#111111",
white: "#fff",
black: "#070300",
hp: "#dd3434",
money: "#ffd700",
hack: "#adff2f",
combat: "#faffdf",
cha: "#a671d1",
int: "#6495ed",
rep: "#faffdf",
disabled: "#66cfbc",
backgroundprimary: "#0A0A0E",
backgroundsecondary: "#0E0E10",
button: "#222222",
},
},
"Default Lite": {
credit: "NmuGmu",
description: "Less eye-straining default theme",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/925263801564151888",
colors: {
primarylight: "#28CF28",
primary: "#21A821",
primarydark: "#177317",
successlight: "#1CFF1C",
success: "#16CA16",
successdark: "#0D910D",
errorlight: "#FF3B3B",
error: "#C32D2D",
errordark: "#8E2121",
secondarylight: "#B3B3B3",
secondary: "#838383",
secondarydark: "#676767",
warninglight: "#FFFF3A",
warning: "#C3C32A",
warningdark: "#8C8C1E",
infolight: "#64CBFF",
info: "#3399CC",
infodark: "#246D91",
welllight: "#404040",
well: "#1C1C1C",
white: "#C3C3C3",
black: "#0A0B0B",
hp: "#C62E2E",
money: "#D6BB27",
hack: "#ADFF2F",
combat: "#E8EDCD",
cha: "#8B5FAF",
int: "#537CC8",
rep: "#E8EDCD",
disabled: "#5AB5A5",
backgroundprimary: "#0C0D0E",
backgroundsecondary: "#121415",
button: "#252829",
},
},
Light: {
credit: "matt",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/926114005456658432",
colors: {
primarylight: "#535353",
primary: "#1A1A1A",
primarydark: "#0d0d0d",
successlight: "#63c439",
success: "#428226",
successdark: "#2E5A1B",
errorlight: "#df7051",
error: "#C94824",
errordark: "#91341B",
secondarylight: "#b3b3b3",
secondary: "#9B9B9B",
secondarydark: "#7A7979",
warninglight: "#e8d464",
warning: "#C6AD20",
warningdark: "#9F8A16",
infolight: "#6299cf",
info: "#3778B7",
infodark: "#30689C",
welllight: "#f9f9f9",
well: "#eaeaea",
white: "#F7F7F7",
black: "#F7F7F7",
hp: "#BF5C41",
money: "#E1B121",
hack: "#47BC38",
combat: "#656262",
cha: "#A568AC",
int: "#889BCF",
rep: "#656262",
disabled: "#70B4BF",
backgroundprimary: "#F7F7F7",
backgroundsecondary: "#f9f9f9",
button: "#eaeaea",
},
},
});

18
src/Themes/README.md Normal file

@ -0,0 +1,18 @@
# Themes
Feel free to contribute a new theme by submitting a pull request to the game!
See [CONTRIBUTING.md](/CONTRIBUTING.md) for details.
## How create a new theme
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. 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`
## Other resources
There is an external script called `theme-browser` which may include more themes than those shown here. Head over the [bitpacker](https://github.com/davidsiems/bitpacker) repository for details.

56
src/Themes/Themes.ts Normal file

@ -0,0 +1,56 @@
import { IMap } from "../types";
import * as predefined from "./data";
export interface ITheme {
[key: string]: string | undefined;
primarylight: string;
primary: string;
primarydark: string;
successlight: string;
success: string;
successdark: string;
errorlight: string;
error: string;
errordark: string;
secondarylight: string;
secondary: string;
secondarydark: string;
warninglight: string;
warning: string;
warningdark: string;
infolight: string;
info: string;
infodark: string;
welllight: string;
well: string;
white: string;
black: string;
hp: string;
money: string;
hack: string;
combat: string;
cha: string;
int: string;
rep: string;
disabled: string;
backgroundprimary: string;
backgroundsecondary: string;
button: string;
}
export interface IPredefinedTheme {
colors: ITheme;
name: string;
credit: string;
screenshot: string;
description: string;
reference?: string;
}
export const defaultTheme: ITheme = {
...predefined.Default.colors,
};
export const getPredefinedThemes = (): IMap<IPredefinedTheme> => ({
...predefined,
});

@ -0,0 +1,45 @@
import { IPredefinedTheme } from "../../Themes";
import img1 from "./screenshot.png";
export const Theme: IPredefinedTheme = {
name: "Dark Blue",
description: "Very dark with a blue/purplelly primary",
credit: "Saynt_Garmo",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/923084732718264340",
screenshot: img1,
colors: {
primarylight: "#023DDE",
primary: "#4A41C8",
primarydark: "#005299",
successlight: "#00FF00",
success: "#D1DAD1",
successdark: "#BFCABF",
errorlight: "#f00",
error: "#c00",
errordark: "#900",
secondarylight: "#AAA",
secondary: "#888",
secondarydark: "#666",
warninglight: "#ff0",
warning: "#cc0",
warningdark: "#990",
infolight: "#69f",
info: "#36c",
infodark: "#039",
welllight: "#444",
well: "#040505",
white: "#fff",
black: "#000000",
hp: "#dd3434",
money: "#ffd700",
hack: "#adff2f",
combat: "#faffdf",
cha: "#a671d1",
int: "#6495ed",
rep: "#faffdf",
disabled: "#66cfbc",
backgroundprimary: "#091419",
backgroundsecondary: "#000000",
button: "#000000",
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

@ -0,0 +1,45 @@
import { IPredefinedTheme } from "../../Themes";
import img1 from "./screenshot.png";
export const Theme: IPredefinedTheme = {
name: "Dark+",
credit: "LoganMD",
description: "VSCode Dark+",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/921999975867617310",
screenshot: img1,
colors: {
primarylight: "#E0E0BC",
primary: "#CCCCAE",
primarydark: "#B8B89C",
successlight: "#00F000",
success: "#00D200",
successdark: "#00B400",
errorlight: "#F00000",
error: "#C80000",
errordark: "#A00000",
secondarylight: "#B4AEAE",
secondary: "#969090",
secondarydark: "#787272",
warninglight: "#F0F000",
warning: "#C8C800",
warningdark: "#A0A000",
infolight: "#69f",
info: "#36c",
infodark: "#039",
welllight: "#444",
well: "#222",
white: "#fff",
black: "#1E1E1E",
hp: "#dd3434",
money: "#ffd700",
hack: "#adff2f",
combat: "#faffdf",
cha: "#a671d1",
int: "#6495ed",
rep: "#faffdf",
disabled: "#66cfbc",
backgroundprimary: "#1E1E1E",
backgroundsecondary: "#252525",
button: "#333",
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

@ -0,0 +1,45 @@
import { IPredefinedTheme } from "../../Themes";
import img1 from "./screenshot.png";
export const Theme: IPredefinedTheme = {
name: "Default-lite",
description: "Less eye-straining default theme",
credit: "NmuGmu",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/925263801564151888",
screenshot: img1,
colors: {
primarylight: "#28CF28",
primary: "#21A821",
primarydark: "#177317",
successlight: "#1CFF1C",
success: "#16CA16",
successdark: "#0D910D",
errorlight: "#FF3B3B",
error: "#C32D2D",
errordark: "#8E2121",
secondarylight: "#B3B3B3",
secondary: "#838383",
secondarydark: "#676767",
warninglight: "#FFFF3A",
warning: "#C3C32A",
warningdark: "#8C8C1E",
infolight: "#64CBFF",
info: "#3399CC",
infodark: "#246D91",
welllight: "#404040",
well: "#1C1C1C",
white: "#C3C3C3",
black: "#0A0B0B",
hp: "#C62E2E",
money: "#D6BB27",
hack: "#ADFF2F",
combat: "#E8EDCD",
cha: "#8B5FAF",
int: "#537CC8",
rep: "#E8EDCD",
disabled: "#5AB5A5",
backgroundprimary: "#0C0D0E",
backgroundsecondary: "#121415",
button: "#252829",
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

@ -0,0 +1,44 @@
import { IPredefinedTheme } from "../../Themes";
import img1 from "./screenshot.png";
export const Theme: IPredefinedTheme = {
name: 'Default',
description: 'Default game theme, most supported',
credit: 'hydroflame',
screenshot: img1,
colors: {
primarylight: "#0f0",
primary: "#0c0",
primarydark: "#090",
successlight: "#0f0",
success: "#0c0",
successdark: "#090",
errorlight: "#f00",
error: "#c00",
errordark: "#900",
secondarylight: "#AAA",
secondary: "#888",
secondarydark: "#666",
warninglight: "#ff0",
warning: "#cc0",
warningdark: "#990",
infolight: "#69f",
info: "#36c",
infodark: "#039",
welllight: "#444",
well: "#222",
white: "#fff",
black: "#000",
hp: "#dd3434",
money: "#ffd700",
hack: "#adff2f",
combat: "#faffdf",
cha: "#a671d1",
int: "#6495ed",
rep: "#faffdf",
disabled: "#66cfbc",
backgroundprimary: "#000",
backgroundsecondary: "#000",
button: "#333",
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

@ -0,0 +1,45 @@
import { IPredefinedTheme } from "../../Themes";
import img1 from "./screenshot.png";
export const Theme: IPredefinedTheme = {
name: "Discord-like",
description: "Discord inspired theme",
credit: "Thermite",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924305252017143818",
screenshot: img1,
colors: {
primarylight: "#7389DC",
primary: "#7389DC",
primarydark: "#5964F1",
successlight: "#00CC00",
success: "#20DF20",
successdark: "#0CB80C",
errorlight: "#EA5558",
error: "#EC4145",
errordark: "#E82528",
secondarylight: "#C3C3C3",
secondary: "#9C9C9C",
secondarydark: "#4E4E4E",
warninglight: "#ff0",
warning: "#cc0",
warningdark: "#990",
infolight: "#69f",
info: "#36c",
infodark: "#1C4FB3",
welllight: "#999999",
well: "#35383C",
white: "#FFFFFF",
black: "#202225",
hp: "#FF5656",
money: "#43FF43",
hack: "#FFAB3D",
combat: "#8A90FD",
cha: "#FF51D9",
int: "#6495ed",
rep: "#FFFF30",
disabled: "#474B51",
backgroundprimary: "#2F3136",
backgroundsecondary: "#35393E",
button: "#333",
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

@ -0,0 +1,45 @@
import { IPredefinedTheme } from "../../Themes";
import img1 from "./screenshot.png";
export const Theme: IPredefinedTheme = {
name: "Dracula",
description: "Dracula Look-alike",
credit: "H3draut3r",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922296307836678144",
screenshot: img1,
colors: {
primarylight: "#7082B8",
primary: "#F8F8F2",
primarydark: "#FF79C6",
successlight: "#0f0",
success: "#0c0",
successdark: "#090",
errorlight: "#FD4545",
error: "#FF2D2D",
errordark: "#C62424",
secondarylight: "#AAA",
secondary: "#8BE9FD",
secondarydark: "#666",
warninglight: "#FFC281",
warning: "#FFB86C",
warningdark: "#E6A055",
infolight: "#A0A0FF",
info: "#7070FF",
infodark: "#4040FF",
welllight: "#44475A",
well: "#363948",
white: "#fff",
black: "#282A36",
hp: "#D34448",
money: "#50FA7B",
hack: "#F1FA8C",
combat: "#BD93F9",
cha: "#FF79C6",
int: "#6495ed",
rep: "#faffdf",
disabled: "#66cfbc",
backgroundprimary: "#282A36",
backgroundsecondary: "#21222C",
button: "#21222C",
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

14
src/Themes/data/index.ts Normal file

@ -0,0 +1,14 @@
export { Theme as Default } from "./default";
export { Theme as DefaultLite } from "./default-lite";
export { Theme as Monokai } from "./monokai-ish";
export { Theme as Warmer } from "./warmer";
export { Theme as DarkPlus } from "./dark-plus";
export { Theme as MayukaiDark } from "./mayukai-dark";
export { Theme as Purple } from "./purple";
export { Theme as SmoothGreen } from "./smooth-green";
export { Theme as Dracula } from "./dracula";
export { Theme as DarkBlue } from "./dark-blue";
export { Theme as DiscordLike } from "./discord-like";
export { Theme as OneDark } from "./one-dark";
export { Theme as MutedGoldBlue } from "./muted-gold-blue";
export { Theme as Light } from "./light";

@ -0,0 +1,45 @@
import { IPredefinedTheme } from "../../Themes";
import img1 from "./screenshot.png";
export const Theme: IPredefinedTheme = {
name: "Light",
description: "Cobbled Together Light Theme",
credit: "matt",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/926114005456658432",
screenshot: img1,
colors: {
primarylight: "#535353",
primary: "#1A1A1A",
primarydark: "#0d0d0d",
successlight: "#63c439",
success: "#428226",
successdark: "#2E5A1B",
errorlight: "#df7051",
error: "#C94824",
errordark: "#91341B",
secondarylight: "#b3b3b3",
secondary: "#9B9B9B",
secondarydark: "#7A7979",
warninglight: "#e8d464",
warning: "#C6AD20",
warningdark: "#9F8A16",
infolight: "#6299cf",
info: "#3778B7",
infodark: "#30689C",
welllight: "#f9f9f9",
well: "#eaeaea",
white: "#F7F7F7",
black: "#F7F7F7",
hp: "#BF5C41",
money: "#E1B121",
hack: "#47BC38",
combat: "#656262",
cha: "#A568AC",
int: "#889BCF",
rep: "#656262",
disabled: "#70B4BF",
backgroundprimary: "#F7F7F7",
backgroundsecondary: "#f9f9f9",
button: "#eaeaea",
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

@ -0,0 +1,45 @@
import { IPredefinedTheme } from "../../Themes";
import img1 from "./screenshot.png";
export const Theme: IPredefinedTheme = {
name: "Mayukai Dark",
description: "Mayukai Dark-esque",
credit: "Festive Noire",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922037502334889994",
screenshot: img1,
colors: {
primarylight: "#DDDFC5",
primary: "#CDCFB6",
primarydark: "#9D9F8C",
successlight: "#00EF00",
success: "#00A500",
successdark: "#007A00",
errorlight: "#F92672",
error: "#CA1C5C",
errordark: "#90274A",
secondarylight: "#AAA",
secondary: "#888",
secondarydark: "#666",
warninglight: "#D3D300",
warning: "#cc0",
warningdark: "#990",
infolight: "#69f",
info: "#36c",
infodark: "#039",
welllight: "#444",
well: "#00010A",
white: "#fff",
black: "#020509",
hp: "#dd3434",
money: "#ffd700",
hack: "#8CCF27",
combat: "#faffdf",
cha: "#a671d1",
int: "#6495ed",
rep: "#faffdf",
disabled: "#66cfbc",
backgroundprimary: "#080C11",
backgroundsecondary: "#03080F",
button: "#00010A",
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

@ -0,0 +1,44 @@
import { IPredefinedTheme } from "../../Themes";
import img1 from "./screenshot.png";
export const Theme: IPredefinedTheme = {
name: "Monokai'ish",
description: "Monokai'ish",
credit: "eltea",
screenshot: img1,
colors: {
primarylight: "#FFF",
primary: "#F8F8F2",
primarydark: "#FAFAEB",
successlight: "#ADE146",
success: "#A6E22E",
successdark: "#98E104",
errorlight: "#FF69A0",
error: "#F92672",
errordark: "#D10F56",
secondarylight: "#AAA",
secondary: "#888",
secondarydark: "#666",
warninglight: "#E1D992",
warning: "#E6DB74",
warningdark: "#EDDD54",
infolight: "#92E1F1",
info: "#66D9EF",
infodark: "#31CDED",
welllight: "#444",
well: "#222",
white: "#fff",
black: "#000",
hp: "#F92672",
money: "#E6DB74",
hack: "#A6E22E",
combat: "#75715E",
cha: "#AE81FF",
int: "#66D9EF",
rep: "#E69F66",
disabled: "#66cfbc",
backgroundprimary: "#272822",
backgroundsecondary: "#1B1C18",
button: "#333",
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

@ -0,0 +1,45 @@
import { IPredefinedTheme } from "../../Themes";
import img1 from "./screenshot.png";
export const Theme: IPredefinedTheme = {
name: "Muted Gold & Blue",
description: "Muted gold with blue accents.",
credit: "Sloth",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924672660758208563",
screenshot: img1,
colors: {
primarylight: "#E3B54A",
primary: "#CAA243",
primarydark: "#7E6937",
successlight: "#82FF82",
success: "#6FDA6F",
successdark: "#64C364",
errorlight: "#FD5555",
error: "#D84A4A",
errordark: "#AC3939",
secondarylight: "#D8D0B8",
secondary: "#B1AA95",
secondarydark: "#736E5E",
warninglight: "#ff0",
warning: "#cc0",
warningdark: "#990",
infolight: "#69f",
info: "#36c",
infodark: "#039",
welllight: "#444",
well: "#111111",
white: "#fff",
black: "#070300",
hp: "#dd3434",
money: "#ffd700",
hack: "#adff2f",
combat: "#faffdf",
cha: "#a671d1",
int: "#6495ed",
rep: "#faffdf",
disabled: "#66cfbc",
backgroundprimary: "#0A0A0E",
backgroundsecondary: "#0E0E10",
button: "#222222",
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

@ -0,0 +1,45 @@
import { IPredefinedTheme } from "../../Themes";
import img1 from "./screenshot.png";
export const Theme: IPredefinedTheme = {
name: "One Dark",
description: "Dark with a greenish tint",
credit: "Dexalt142",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924650660694208512",
screenshot: img1,
colors: {
primarylight: "#98C379",
primary: "#98C379",
primarydark: "#98C379",
successlight: "#98C379",
success: "#98C379",
successdark: "#98C379",
errorlight: "#E06C75",
error: "#BE5046",
errordark: "#BE5046",
secondarylight: "#AAA",
secondary: "#888",
secondarydark: "#666",
warninglight: "#E5C07B",
warning: "#E5C07B",
warningdark: "#D19A66",
infolight: "#61AFEF",
info: "#61AFEF",
infodark: "#61AFEF",
welllight: "#4B5263",
well: "#282C34",
white: "#ABB2BF",
black: "#282C34",
hp: "#E06C75",
money: "#E5C07B",
hack: "#98C379",
combat: "#ABB2BF",
cha: "#C678DD",
int: "#61AFEF",
rep: "#ABB2BF",
disabled: "#56B6C2",
backgroundprimary: "#282C34",
backgroundsecondary: "#21252B",
button: "#4B5263",
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

@ -0,0 +1,45 @@
import { IPredefinedTheme } from "../../Themes";
import img1 from "./screenshot.png";
export const Theme: IPredefinedTheme = {
name: "Purple",
credit: "zer0ney",
description: "Essentially all defaults except for purple replacing the main colors",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922091815849570395",
screenshot: img1,
colors: {
primarylight: "#BA55D3",
primary: "#9370DB",
primarydark: "#8A2BE2",
successlight: "#BA55D3",
success: "#9370DB",
successdark: "#8A2BE2",
errorlight: "#f00",
error: "#c00",
errordark: "#900",
secondarylight: "#AAA",
secondary: "#888",
secondarydark: "#666",
warninglight: "#ff0",
warning: "#cc0",
warningdark: "#990",
infolight: "#69f",
info: "#36c",
infodark: "#039",
welllight: "#444",
well: "#222",
white: "#fff",
black: "#000",
hp: "#dd3434",
money: "#ffd700",
hack: "#adff2f",
combat: "#faffdf",
cha: "#a671d1",
int: "#6495ed",
rep: "#faffdf",
disabled: "#66cfbc",
backgroundprimary: "#000",
backgroundsecondary: "#000",
button: "#333",
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

@ -0,0 +1,45 @@
import { IPredefinedTheme } from "../../Themes";
import img1 from "./screenshot.png";
export const Theme: IPredefinedTheme = {
name: "Smooth Green",
description: "A nice green theme that doesn't hurt your eyes.",
credit: "Swidt",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922243957986033725",
screenshot: img1,
colors: {
primarylight: "#E0E0BC",
primary: "#B0D9A3",
primarydark: "#B8B89C",
successlight: "#00F000",
success: "#6BC16B",
successdark: "#00B400",
errorlight: "#F00000",
error: "#3D713D",
errordark: "#A00000",
secondarylight: "#B4AEAE",
secondary: "#8FAF85",
secondarydark: "#787272",
warninglight: "#F0F000",
warning: "#38F100",
warningdark: "#A0A000",
infolight: "#69f",
info: "#36c",
infodark: "#039",
welllight: "#444",
well: "#2F3C2B",
white: "#fff",
black: "#1E1E1E",
hp: "#dd3434",
money: "#4AA52E",
hack: "#adff2f",
combat: "#faffdf",
cha: "#a671d1",
int: "#6495ed",
rep: "#35A135",
disabled: "#66cfbc",
backgroundprimary: "#1E1E1E",
backgroundsecondary: "#252525",
button: "#2F3C2B",
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

@ -0,0 +1,45 @@
import { IPredefinedTheme } from "../../Themes";
import img1 from "./screenshot.png";
export const Theme: IPredefinedTheme = {
name: "Warmer",
credit: "hexnaught",
description: "Warmer, softer theme",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/921999581020028938",
screenshot: img1,
colors: {
primarylight: "#EA9062",
primary: "#DD7B4A",
primarydark: "#D3591C",
successlight: "#6ACF6A",
success: "#43BF43",
successdark: "#3E913E",
errorlight: "#C15757",
error: "#B34141",
errordark: "#752525",
secondarylight: "#AAA",
secondary: "#888",
secondarydark: "#666",
warninglight: "#E6E69D",
warning: "#DADA56",
warningdark: "#A1A106",
infolight: "#69f",
info: "#36c",
infodark: "#039",
welllight: "#444",
well: "#222",
white: "#fff",
black: "#000",
hp: "#dd3434",
money: "#ffd700",
hack: "#adff2f",
combat: "#faffdf",
cha: "#AD84CF",
int: "#6495ed",
rep: "#faffdf",
disabled: "#76C6B7",
backgroundprimary: "#000",
backgroundsecondary: "#000",
button: "#333",
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

@ -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 (
<>
<Tooltip title="The style editor allows you to modify certain CSS rules used by the game.">
<Button startIcon={<TextFormatIcon />} onClick={() => setStyleEditorOpen(true)}>
Style Editor
</Button>
</Tooltip>
<StyleEditorModal open={styleEditorOpen} onClose={() => setStyleEditorOpen(false)} />
</>
);
}

@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Modal } from "./Modal"; import { Modal } from "../../ui/React/Modal";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import ButtonGroup from "@mui/material/ButtonGroup"; import ButtonGroup from "@mui/material/ButtonGroup";
@ -11,7 +11,7 @@ import SaveIcon from "@mui/icons-material/Save";
import { ThemeEvents } from "./Theme"; import { ThemeEvents } from "./Theme";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { defaultStyles } from "../../Settings/Styles"; import { defaultStyles } from "../Styles";
import { Tooltip } from "@mui/material"; import { Tooltip } from "@mui/material";
import { IStyleSettings } from "../../ScriptEditor/NetscriptDefinitions"; import { IStyleSettings } from "../../ScriptEditor/NetscriptDefinitions";

@ -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<string | undefined>();
const predefinedThemes = getPredefinedThemes();
const themes = (predefinedThemes &&
Object.entries(predefinedThemes).map(([key, templateTheme]) => (
<ThemeEntry
key={key}
theme={templateTheme}
onActivated={() => 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 "<strong>{theme.name}</strong>"
<Button
sx={{ ml: 1 }}
color="secondary"
size="small"
onClick={() => {
Object.assign(Settings.theme, previousColors);
ThemeEvents.emit();
}}
>
UNDO
</Button>
</>,
"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 (
<Box sx={{ mx: 2 }}>
<Typography variant="h4">Theme Browser</Typography>
<Paper sx={{ px: 2, py: 1, my: 1 }}>
<ThemeCollaborate />
<ButtonGroup sx={{ mb: 2, display: "block" }}>
<ThemeEditorButton router={router} />
<StyleEditorButton />
</ButtonGroup>
<Box sx={{ display: "flex", flexWrap: "wrap" }}>{themes}</Box>
<Modal open={modalOpen} onClose={handleCloseZoom}>
<img src={modalImageSrc} style={{ width: "100%" }} />
</Modal>
</Paper>
</Box>
);
}

@ -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 (
<>
<Typography sx={{ my: 1 }}>
If you've created a theme that you believe should be added in game's theme browser, feel free to{" "}
<Link href="https://github.com/danielyxie/bitburner/tree/dev/src/Themes/README.md" target="_blank">
create a pull request
</Link>
.
</Typography>
<Typography sx={{ my: 1 }}>
Head over to the{" "}
<Link href="https://discord.com/channels/415207508303544321/921991895230611466" target="_blank">
theme-sharing
</Link>{" "}
discord channel for more.
</Typography>
</>
);
}

@ -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 (
<>
<Tooltip title="The theme editor allows you to modify the colors the game uses.">
<Button id="bb-theme-editor-button" startIcon={<ColorizeIcon />} onClick={() => setThemeEditorOpen(true)}>
Theme Editor
</Button>
</Tooltip>
<ThemeEditorModal open={themeEditorOpen} onClose={() => setThemeEditorOpen(false)} router={router} />
</>
);
}

@ -1,6 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Modal } from "./Modal"; import { Modal } from "../../ui/React/Modal";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import ButtonGroup from "@mui/material/ButtonGroup";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
@ -8,15 +9,19 @@ import TextField from "@mui/material/TextField";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import ReplyIcon from "@mui/icons-material/Reply"; import ReplyIcon from "@mui/icons-material/Reply";
import PaletteSharpIcon from "@mui/icons-material/PaletteSharp"; import PaletteSharpIcon from "@mui/icons-material/PaletteSharp";
import HistoryIcon from '@mui/icons-material/History';
import { Color, ColorPicker } from "material-ui-color"; import { Color, ColorPicker } from "material-ui-color";
import { ThemeEvents } from "./Theme"; import { ThemeEvents } from "./Theme";
import { Settings, defaultSettings } from "../../Settings/Settings"; import { Settings, defaultSettings } from "../../Settings/Settings";
import { getPredefinedThemes } from "../../Settings/Themes"; import { defaultTheme } from "../Themes";
import { UserInterfaceTheme } from "../../ScriptEditor/NetscriptDefinitions"; import { UserInterfaceTheme } from "../../ScriptEditor/NetscriptDefinitions";
import { IRouter } from "../../ui/Router";
import { ThemeCollaborate } from "./ThemeCollaborate";
interface IProps { interface IProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
router: IRouter;
} }
interface IColorEditorProps { interface IColorEditorProps {
@ -68,28 +73,6 @@ export function ThemeEditorModal(props: IProps): React.ReactElement {
...Settings.theme, ...Settings.theme,
}); });
const predefinedThemes = getPredefinedThemes();
const themes = predefinedThemes && Object.entries(predefinedThemes)
.map(([key, templateTheme]) => {
const name = templateTheme.name || key;
let inner = <Typography>{name}</Typography>;
let toolTipTitle;
if (templateTheme.credit) {
toolTipTitle = <Typography>{templateTheme.description || name} <em>by {templateTheme.credit}</em></Typography>;
} else if (templateTheme.description) {
toolTipTitle = <Typography>{templateTheme.description}</Typography>;
}
if (toolTipTitle) {
inner = <Tooltip title={toolTipTitle}>{inner}</Tooltip>
}
return (
<Button onClick={() => setTemplateTheme(templateTheme.colors)}
startIcon={<PaletteSharpIcon />} key={key} sx={{ mr: 1, mb: 1 }}>
{inner}
</Button>
);
}) || <></>;
function setTheme(theme: UserInterfaceTheme): void { function setTheme(theme: UserInterfaceTheme): void {
setCustomTheme(theme); setCustomTheme(theme);
Object.assign(Settings.theme, theme); Object.assign(Settings.theme, theme);
@ -372,8 +355,18 @@ export function ThemeEditorModal(props: IProps): React.ReactElement {
/> />
<> <>
<Typography sx={{ my: 1 }}>Backup your theme or share it with others by copying the string above.</Typography> <Typography sx={{ my: 1 }}>Backup your theme or share it with others by copying the string above.</Typography>
<Typography sx={{ my: 1 }}>Replace the current theme with a pre-built template using the buttons below.</Typography> <ThemeCollaborate />
{themes} <ButtonGroup>
<Tooltip title="Reverts all modification back to the default theme. This is permanent.">
<Button onClick={() => setTemplateTheme(defaultTheme)}
startIcon={<HistoryIcon />}>
Revert to Default
</Button>
</Tooltip>
<Tooltip title="Move over to the theme browser's page to use one of our predefined themes.">
<Button startIcon={<PaletteSharpIcon />} onClick={() => props.router.toThemeBrowser()}>See more themes</Button>
</Tooltip>
</ButtonGroup>
</> </>
</Paper> </Paper>
</Modal> </Modal>

@ -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 (
<Card key={theme.screenshot} sx={{ width: 400, mr: 1, mb: 1 }}>
<CardHeader
action={
<Tooltip title="Use this theme">
<Button startIcon={<PaletteSharpIcon />} onClick={onActivated} variant="outlined">
Use
</Button>
</Tooltip>
}
title={theme.name}
subheader={
<>
by {theme.credit}{" "}
{theme.reference && (
<>
(
<Link href={theme.reference} target="_blank">
ref
</Link>
)
</>
)}
</>
}
sx={{
color: Settings.theme.primary,
"& .MuiCardHeader-subheader": {
color: Settings.theme.secondarydark,
},
"& .MuiButton-outlined": {
backgroundColor: "transparent",
},
}}
/>
<CardMedia
component="img"
width="400"
image={theme.screenshot}
alt={`Theme Screenshot of "${theme.name}"`}
sx={{
borderTop: `1px solid ${Settings.theme.welllight}`,
borderBottom: `1px solid ${Settings.theme.welllight}`,
cursor: "zoom-in",
}}
onClick={() => onImageClick(theme.screenshot)}
/>
<CardContent>
<Typography
variant="body2"
color="text.secondary"
sx={{
color: Settings.theme.primarydark,
}}
>
{theme.description}
</Typography>
</CardContent>
</Card>
);
}

@ -30,7 +30,7 @@ import { Player } from "./Player";
import { saveObject, loadGame } from "./SaveObject"; import { saveObject, loadGame } from "./SaveObject";
import { initForeignServers } from "./Server/AllServers"; import { initForeignServers } from "./Server/AllServers";
import { Settings } from "./Settings/Settings"; import { Settings } from "./Settings/Settings";
import { ThemeEvents } from "./ui/React/Theme"; import { ThemeEvents } from "./Themes/ui/Theme";
import { updateSourceFileFlags } from "./SourceFile/SourceFileFlags"; import { updateSourceFileFlags } from "./SourceFile/SourceFileFlags";
import { initSymbolToStockMap, processStockPrices } from "./StockMarket/StockMarket"; import { initSymbolToStockMap, processStockPrices } from "./StockMarket/StockMarket";
import { Terminal } from "./Terminal"; import { Terminal } from "./Terminal";

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { TTheme as Theme, ThemeEvents, refreshTheme } from "./ui/React/Theme"; import { TTheme as Theme, ThemeEvents, refreshTheme } from "./Themes/ui/Theme";
import { LoadingScreen } from "./ui/LoadingScreen"; import { LoadingScreen } from "./ui/LoadingScreen";
import { initElectron } from "./Electron"; import { initElectron } from "./Electron";
initElectron(); initElectron();

@ -79,6 +79,7 @@ import { RecoveryMode, RecoveryRoot } from "./React/RecoveryRoot";
import { AchievementsRoot } from "../Achievements/AchievementsRoot"; import { AchievementsRoot } from "../Achievements/AchievementsRoot";
import { ErrorBoundary } from "./ErrorBoundary"; import { ErrorBoundary } from "./ErrorBoundary";
import { Settings } from "../Settings/Settings"; import { Settings } from "../Settings/Settings";
import { ThemeBrowser } from "../Themes/ui/ThemeBrowser";
const htmlLocation = location; const htmlLocation = location;
@ -194,6 +195,9 @@ export let Router: IRouter = {
toAchievements: () => { toAchievements: () => {
throw new Error("Router called before initialization"); throw new Error("Router called before initialization");
}, },
toThemeBrowser: () => {
throw new Error("Router called before initialization");
},
}; };
function determineStartPage(player: IPlayer): Page { function determineStartPage(player: IPlayer): Page {
@ -307,6 +311,9 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
toAchievements: () => { toAchievements: () => {
setPage(Page.Achievements); setPage(Page.Achievements);
}, },
toThemeBrowser: () => {
setPage(Page.ThemeBrowser);
}
}; };
useEffect(() => { useEffect(() => {
@ -471,6 +478,7 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
mainPage = ( mainPage = (
<GameOptionsRoot <GameOptionsRoot
player={player} player={player}
router={Router}
save={() => saveObject.saveGame()} save={() => saveObject.saveGame()}
export={() => { export={() => {
// Apply the export bonus before saving the game // Apply the export bonus before saving the game
@ -503,6 +511,10 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
mainPage = <AchievementsRoot />; mainPage = <AchievementsRoot />;
break; break;
} }
case Page.ThemeBrowser: {
mainPage = <ThemeBrowser router={Router} />;
break;
}
} }
return ( return (

@ -22,12 +22,11 @@ import TextField from "@mui/material/TextField";
import DownloadIcon from "@mui/icons-material/Download"; import DownloadIcon from "@mui/icons-material/Download";
import UploadIcon from "@mui/icons-material/Upload"; import UploadIcon from "@mui/icons-material/Upload";
import SaveIcon from "@mui/icons-material/Save"; import SaveIcon from "@mui/icons-material/Save";
import PaletteIcon from '@mui/icons-material/Palette';
import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal"; import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal";
import { dialogBoxCreate } from "./DialogBox"; import { dialogBoxCreate } from "./DialogBox";
import { ConfirmationModal } from "./ConfirmationModal"; import { ConfirmationModal } from "./ConfirmationModal";
import { ThemeEditorModal } from "./ThemeEditorModal";
import { StyleEditorModal } from "./StyleEditorModal";
import { SnackbarEvents } from "./Snackbar"; import { SnackbarEvents } from "./Snackbar";
@ -37,6 +36,9 @@ import { formatTime } from "../../utils/helpers/formatTime";
import { OptionSwitch } from "./OptionSwitch"; import { OptionSwitch } from "./OptionSwitch";
import { DeleteGameButton } from "./DeleteGameButton"; import { DeleteGameButton } from "./DeleteGameButton";
import { SoftResetButton } from "./SoftResetButton"; 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) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
@ -50,6 +52,7 @@ const useStyles = makeStyles((theme: Theme) =>
interface IProps { interface IProps {
player: IPlayer; player: IPlayer;
router: IRouter;
save: () => void; save: () => void;
export: () => void; export: () => void;
forceKill: () => void; forceKill: () => void;
@ -74,8 +77,6 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat); const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat);
const [locale, setLocale] = useState(Settings.Locale); const [locale, setLocale] = useState(Settings.Locale);
const [diagnosticOpen, setDiagnosticOpen] = useState(false); const [diagnosticOpen, setDiagnosticOpen] = useState(false);
const [themeEditorOpen, setThemeEditorOpen] = useState(false);
const [styleEditorOpen, setStyleEditorOpen] = useState(false);
const [importSaveOpen, setImportSaveOpen] = useState(false); const [importSaveOpen, setImportSaveOpen] = useState(false);
const [importData, setImportData] = useState<ImportData | null>(null); const [importData, setImportData] = useState<ImportData | null>(null);
@ -636,9 +637,14 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
<Button onClick={() => setDiagnosticOpen(true)}>Diagnose files</Button> <Button onClick={() => setDiagnosticOpen(true)}>Diagnose files</Button>
</Tooltip> </Tooltip>
</Box> </Box>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}> <Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr" }}>
<Button onClick={() => setThemeEditorOpen(true)}>Theme editor</Button> <Tooltip title="Head to the theme browser to see a collection of prebuilt themes.">
<Button onClick={() => setStyleEditorOpen(true)}>Style editor</Button> <Button startIcon={<PaletteIcon />} onClick={() => props.router.toThemeBrowser()}>
Theme Browser
</Button>
</Tooltip>
<ThemeEditorButton router={props.router} />
<StyleEditorButton />
</Box> </Box>
<Box> <Box>
<Link href="https://github.com/danielyxie/bitburner/issues/new" target="_blank"> <Link href="https://github.com/danielyxie/bitburner/issues/new" target="_blank">
@ -663,8 +669,6 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
</Box> </Box>
</Grid> </Grid>
<FileDiagnosticModal open={diagnosticOpen} onClose={() => setDiagnosticOpen(false)} /> <FileDiagnosticModal open={diagnosticOpen} onClose={() => setDiagnosticOpen(false)} />
<ThemeEditorModal open={themeEditorOpen} onClose={() => setThemeEditorOpen(false)} />
<StyleEditorModal open={styleEditorOpen} onClose={() => setStyleEditorOpen(false)} />
</div> </div>
); );
} }

@ -14,6 +14,10 @@ const useStyles = makeStyles(() => ({
snackbar: { snackbar: {
// Log popup z-index increments, so let's add a padding to be well above them. // Log popup z-index increments, so let's add a padding to be well above them.
zIndex: `${logBoxBaseZIndex + 1000} !important` as any, 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 { export function Snackbar(): React.ReactElement {
const { enqueueSnackbar, closeSnackbar } = useSnackbar(); const { enqueueSnackbar, closeSnackbar } = useSnackbar();

@ -37,6 +37,7 @@ export enum Page {
StaneksGift, StaneksGift,
Recovery, Recovery,
Achievements, Achievements,
ThemeBrowser,
} }
export interface ScriptEditorRouteOptions { export interface ScriptEditorRouteOptions {
@ -82,4 +83,5 @@ export interface IRouter {
toLocation(location: Location): void; toLocation(location: Location): void;
toStaneksGift(): void; toStaneksGift(): void;
toAchievements(): void; toAchievements(): void;
toThemeBrowser(): void;
} }

@ -0,0 +1 @@
module.exports = 'test-file-stub';

@ -156,6 +156,14 @@ module.exports = (env, argv) => {
test: /\.s?css$/, test: /\.s?css$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"], use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
}, },
{
test: /\.(png|jpe?g|gif|jp2|webp)$/,
loader: 'file-loader',
options: {
name: '[contenthash].[ext]',
outputPath: 'dist/images',
},
},
], ],
}, },
optimization: { optimization: {