From 92790f36827d8a0ee181e486f8d757c94b377035 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 21 Jul 2025 13:50:33 +0200 Subject: [PATCH] refactor(config): config metadata (@miodec) (#6753) Create config metadata object Move all the special code on config change to config listeners Create a generic set function which will work with the metadata object to update any config key Update all setters to use the generic set. (Later probably only use the generic settter and remove all the specific ones) Also orders config groups and config schema. --------- Co-authored-by: Christian Fehmer --- .../__tests__/api/controllers/config.spec.ts | 2 +- .../__tests__/api/controllers/preset.spec.ts | 6 +- frontend/__tests__/root/config.spec.ts | 528 +++- frontend/__tests__/utils/url-handler.spec.ts | 2 +- .../src/ts/commandline/lists/quote-length.ts | 12 +- frontend/src/ts/config.ts | 2348 ++++++----------- .../src/ts/controllers/theme-controller.ts | 46 +- frontend/src/ts/db.ts | 2 +- frontend/src/ts/elements/keymap.ts | 32 + frontend/src/ts/modals/mobile-test-config.ts | 26 +- frontend/src/ts/modals/quote-search.ts | 3 +- frontend/src/ts/pages/account.ts | 9 +- frontend/src/ts/test/caret.ts | 17 +- frontend/src/ts/test/pace-caret.ts | 15 + frontend/src/ts/test/test-config.ts | 18 +- frontend/src/ts/test/test-logic.ts | 21 +- frontend/src/ts/test/test-ui.ts | 3 + frontend/src/ts/test/words-generator.ts | 6 +- frontend/src/ts/ui.ts | 24 +- frontend/src/ts/utils/config.ts | 42 + frontend/src/ts/utils/url-handler.ts | 2 +- packages/contracts/src/schemas/configs.ts | 306 ++- 22 files changed, 1737 insertions(+), 1733 deletions(-) diff --git a/backend/__tests__/api/controllers/config.spec.ts b/backend/__tests__/api/controllers/config.spec.ts index 3cbf6dc89..650a5d021 100644 --- a/backend/__tests__/api/controllers/config.spec.ts +++ b/backend/__tests__/api/controllers/config.spec.ts @@ -100,8 +100,8 @@ describe("ConfigController", () => { expect(body).toStrictEqual({ message: "Invalid request data schema", validationErrors: [ - `"autoSwitchTheme" Expected boolean, received string`, `"confidenceMode" Invalid enum value. Expected 'off' | 'on' | 'max', received 'pretty'`, + `"autoSwitchTheme" Expected boolean, received string`, ], }); diff --git a/backend/__tests__/api/controllers/preset.spec.ts b/backend/__tests__/api/controllers/preset.spec.ts index 3d9f5eb80..901587145 100644 --- a/backend/__tests__/api/controllers/preset.spec.ts +++ b/backend/__tests__/api/controllers/preset.spec.ts @@ -249,8 +249,8 @@ describe("PresetController", () => { expect(body).toStrictEqual({ message: "Invalid request data schema", validationErrors: [ - `"config.autoSwitchTheme" Expected boolean, received string`, `"config.confidenceMode" Invalid enum value. Expected 'off' | 'on' | 'max', received 'pretty'`, + `"config.autoSwitchTheme" Expected boolean, received string`, `"config" Unrecognized key(s) in object: 'extra'`, `Unrecognized key(s) in object: '_id', 'extra'`, ], @@ -427,9 +427,9 @@ describe("PresetController", () => { expect(body).toStrictEqual({ message: "Invalid request data schema", validationErrors: [ - `"settingGroups.0" Invalid enum value. Expected 'test' | 'behavior' | 'input' | 'sound' | 'caret' | 'appearance' | 'theme' | 'hideElements' | 'ads' | 'hidden', received 'mappers'`, - `"config.autoSwitchTheme" Expected boolean, received string`, + `"settingGroups.0" Invalid enum value. Expected 'test' | 'behavior' | 'input' | 'sound' | 'caret' | 'appearance' | 'theme' | 'hideElements' | 'hidden' | 'ads', received 'mappers'`, `"config.confidenceMode" Invalid enum value. Expected 'off' | 'on' | 'max', received 'pretty'`, + `"config.autoSwitchTheme" Expected boolean, received string`, `"config" Unrecognized key(s) in object: 'extra'`, `Unrecognized key(s) in object: 'extra'`, ], diff --git a/frontend/__tests__/root/config.spec.ts b/frontend/__tests__/root/config.spec.ts index 08059aa43..c3f4c2427 100644 --- a/frontend/__tests__/root/config.spec.ts +++ b/frontend/__tests__/root/config.spec.ts @@ -1,12 +1,499 @@ import * as Config from "../../src/ts/config"; - +import * as Misc from "../../src/ts/utils/misc"; import { CustomThemeColors, FunboxName, + ConfigKey, + Config as ConfigType, } from "@monkeytype/contracts/schemas/configs"; import { randomBytes } from "crypto"; +import { vi } from "vitest"; +import * as FunboxValidation from "../../src/ts/test/funbox/funbox-validation"; +import * as ConfigValidation from "../../src/ts/config-validation"; +import * as ConfigEvent from "../../src/ts/observables/config-event"; +import * as DB from "../../src/ts/db"; +import * as AccountButton from "../../src/ts/elements/account-button"; +import * as Notifications from "../../src/ts/elements/notifications"; + +type TestsByConfig = Partial<{ + [K in keyof ConfigType]: (T & { value: ConfigType[K] })[]; +}>; + +const { configMetadata, replaceConfig, getConfig } = Config.__testing; describe("Config", () => { + const isDevEnvironmentMock = vi.spyOn(Misc, "isDevEnvironment"); + beforeEach(() => isDevEnvironmentMock.mockReset()); + + describe("configMeta", () => { + afterAll(() => { + replaceConfig({}); + vi.resetModules(); + }); + it("should have changeRequiresRestart defined", () => { + const configsRequiringRestarts = Object.entries(configMetadata) + .filter(([_key, value]) => value.changeRequiresRestart === true) + .map(([key]) => key) + .sort(); + + expect(configsRequiringRestarts).toEqual( + [ + "punctuation", + "numbers", + "words", + "time", + "mode", + "quoteLength", + "language", + "difficulty", + "minWpmCustomSpeed", + "minWpm", + "minAcc", + "minAccCustom", + "minBurst", + "minBurstCustomSpeed", + "britishEnglish", + "funbox", + "customLayoutfluid", + "strictSpace", + "stopOnError", + "lazyMode", + "layout", + "codeUnindentOnBackspace", + ].sort() + ); + }); + + it("should have triggerResize defined", () => { + const configsWithTriggeResize = Object.entries(configMetadata) + .filter(([_key, value]) => value.triggerResize === true) + .map(([key]) => key) + .sort(); + + expect(configsWithTriggeResize).toEqual( + ["fontSize", "keymapSize", "maxLineWidth", "tapeMode"].sort() + ); + }); + + it("should throw if config key in not found in metadata", () => { + expect(() => { + Config.genericSet("nonExistentKey" as ConfigKey, true); + }).toThrowError( + `Config metadata for key "nonExistentKey" is not defined.` + ); + }); + + describe("overrideValue", () => { + const testCases: TestsByConfig<{ + given?: Partial; + expected: Partial; + }> = { + punctuation: [ + { value: true, expected: { punctuation: true } }, + { + value: true, + given: { mode: "quote" }, + expected: { punctuation: false }, + }, + ], + numbers: [ + { value: true, expected: { numbers: true } }, + { + value: true, + given: { mode: "quote" }, + expected: { numbers: false }, + }, + ], + customLayoutfluid: [ + { + value: ["qwerty", "qwerty", "qwertz"], + expected: { customLayoutfluid: ["qwerty", "qwertz"] }, + }, + ], + customPolyglot: [ + { + value: ["english", "polish", "english"], + expected: { customPolyglot: ["english", "polish"] }, + }, + ], + keymapSize: [ + { value: 1, expected: { keymapSize: 1 } }, + { value: 1.234, expected: { keymapSize: 1.2 } }, + { value: 0.4, expected: { keymapSize: 0.5 } }, + { value: 3.6, expected: { keymapSize: 3.5 } }, + ], + customBackground: [ + { + value: " https://example.com/test.jpg ", + expected: { customBackground: "https://example.com/test.jpg" }, + }, + ], + accountChart: [ + { + value: ["on", "off", "off", "off"], + expected: { accountChart: ["on", "off", "off", "off"] }, + }, + { + value: ["off", "off", "off", "off"], + given: { accountChart: ["on", "off", "off", "off"] }, + expected: { accountChart: ["off", "on", "off", "off"] }, + }, + { + value: ["off", "off", "on", "on"], + given: { accountChart: ["off", "on", "off", "off"] }, + expected: { accountChart: ["on", "off", "on", "on"] }, + }, + ], + }; + + it.for( + Object.entries(testCases).flatMap(([key, value]) => + value.flatMap((it) => ({ key: key as ConfigKey, ...it })) + ) + )( + `$key value=$value given=$given expect=$expected`, + ({ key, value, given, expected }) => { + //GIVEN + replaceConfig(given ?? {}); + + //WHEN + Config.genericSet(key, value as any); + + //THEN + expect(getConfig()).toMatchObject(expected); + } + ); + }); + + describe("isBlocked", () => { + const testCases: TestsByConfig<{ + given?: Partial; + fail?: true; + }> = { + funbox: [ + { + value: "gibberish" as any, + given: { mode: "quote" }, + fail: true, + }, + ], + showAllLines: [ + { value: true, given: { tapeMode: "off" } }, + { value: false, given: { tapeMode: "word" } }, + { value: true, given: { tapeMode: "word" }, fail: true }, + ], + }; + + it.for( + Object.entries(testCases).flatMap(([key, value]) => + value.flatMap((it) => ({ key: key as ConfigKey, ...it })) + ) + )( + `$key value=$value given=$given fail=$fail`, + ({ key, value, given, fail }) => { + //GIVEN + replaceConfig(given ?? {}); + + //WHEN + const applied = Config.genericSet(key, value as any); + + //THEN + expect(applied).toEqual(!fail); + } + ); + }); + + describe("overrideConfig", () => { + const testCases: TestsByConfig<{ + given: Partial; + expected?: Partial; + }> = { + mode: [ + { value: "time", given: { numbers: true, punctuation: true } }, + { + value: "custom", + given: { numbers: true, punctuation: true }, + expected: { numbers: false, punctuation: false }, + }, + { + value: "quote", + given: { numbers: true, punctuation: true }, + expected: { numbers: false, punctuation: false }, + }, + { + value: "zen", + given: { numbers: true, punctuation: true }, + expected: { numbers: false, punctuation: false }, + }, + ], + numbers: [{ value: false, given: { mode: "quote" } }], + freedomMode: [ + { + value: false, + given: { confidenceMode: "on" }, + expected: { confidenceMode: "on" }, + }, + { + value: true, + given: { confidenceMode: "on" }, + expected: { confidenceMode: "off" }, + }, + ], + stopOnError: [ + { + value: "off", + given: { confidenceMode: "on" }, + expected: { confidenceMode: "on" }, + }, + { + value: "word", + given: { confidenceMode: "on" }, + expected: { confidenceMode: "off" }, + }, + ], + confidenceMode: [ + { + value: "off", + given: { freedomMode: true, stopOnError: "word" }, + expected: { freedomMode: true, stopOnError: "word" }, + }, + { + value: "on", + given: { freedomMode: true, stopOnError: "word" }, + expected: { freedomMode: false, stopOnError: "off" }, + }, + ], + tapeMode: [ + { + value: "off", + given: { showAllLines: true }, + expected: { showAllLines: true }, + }, + { + value: "letter", + given: { showAllLines: true }, + expected: { showAllLines: false }, + }, + ], + theme: [ + { + value: "8008", + given: { customTheme: true }, + expected: { customTheme: false }, + }, + ], + }; + + it.for( + Object.entries(testCases).flatMap(([key, value]) => + value.flatMap((it) => ({ key: key as ConfigKey, ...it })) + ) + )( + `$key value=$value given=$given expected=$expected`, + ({ key, value, given, expected }) => { + //GIVEN + replaceConfig(given); + + //WHEN + Config.genericSet(key, value as any); + + //THEN + expect(getConfig()).toMatchObject(expected ?? {}); + } + ); + }); + + describe("test with mocks", () => { + const canSetConfigWithCurrentFunboxesMock = vi.spyOn( + FunboxValidation, + "canSetConfigWithCurrentFunboxes" + ); + const isConfigValueValidMock = vi.spyOn( + ConfigValidation, + "isConfigValueValid" + ); + const dispatchConfigEventMock = vi.spyOn(ConfigEvent, "dispatch"); + const dbSaveConfigMock = vi.spyOn(DB, "saveConfig"); + const accountButtonLoadingMock = vi.spyOn(AccountButton, "loading"); + const notificationAddMock = vi.spyOn(Notifications, "add"); + const miscReloadAfterMock = vi.spyOn(Misc, "reloadAfter"); + + const mocks = [ + canSetConfigWithCurrentFunboxesMock, + isConfigValueValidMock, + dispatchConfigEventMock, + dbSaveConfigMock, + accountButtonLoadingMock, + notificationAddMock, + miscReloadAfterMock, + ]; + + beforeEach(async () => { + vi.useFakeTimers(); + mocks.forEach((it) => it.mockReset()); + + vi.mock("../../src/ts/test/test-state", () => ({ + isActive: true, + })); + + isConfigValueValidMock.mockReturnValue(true); + canSetConfigWithCurrentFunboxesMock.mockReturnValue(true); + dbSaveConfigMock.mockResolvedValue(); + }); + + afterAll(() => { + mocks.forEach((it) => it.mockRestore()); + vi.useRealTimers(); + }); + + it("cannot set if funbox disallows", () => { + //GIVEN + canSetConfigWithCurrentFunboxesMock.mockReturnValue(false); + + //WHEN / THEN + expect(Config.genericSet("numbers", true)).toBe(false); + }); + + it("fails if config is invalid", () => { + //GIVEN + isConfigValueValidMock.mockReturnValue(false); + + //WHEN / THEN + expect(Config.genericSet("numbers", "off" as any)).toBe(false); + }); + + it("dispatches event on set", () => { + //GIVEN + replaceConfig({ numbers: false }); + + //WHEN + Config.genericSet("numbers", true, true); + + //THEN + + expect(dispatchConfigEventMock).toHaveBeenCalledWith( + "numbers", + true, + true, + false + ); + }); + + it("saves to localstorage if nosave=false", async () => { + //GIVEN + replaceConfig({ numbers: false }); + + //WHEN + Config.genericSet("numbers", true); + + //THEN + //wait for debounce + await vi.advanceTimersByTimeAsync(2500); + + //show loading + expect(accountButtonLoadingMock).toHaveBeenNthCalledWith(1, true); + + //save + expect(dbSaveConfigMock).toHaveBeenCalledWith({ numbers: true }); + + //hide loading + expect(accountButtonLoadingMock).toHaveBeenNthCalledWith(2, false); + + //send event + expect(dispatchConfigEventMock).toHaveBeenCalledWith( + "saveToLocalStorage", + expect.stringContaining("numbers") + ); + }); + it("does not save to localstorage if nosave=true", async () => { + //GIVEN + + replaceConfig({ numbers: false }); + + //WHEN + Config.genericSet("numbers", true, true); + + //THEN + //wait for debounce + await vi.advanceTimersByTimeAsync(2500); + + expect(accountButtonLoadingMock).not.toHaveBeenCalled(); + expect(dbSaveConfigMock).not.toHaveBeenCalled(); + + expect(dispatchConfigEventMock).not.toHaveBeenCalledWith( + "saveToLocalStorage", + expect.any(String) + ); + }); + it("calls afterSet", () => { + //GIVEN + isDevEnvironmentMock.mockReturnValue(false); + replaceConfig({ ads: "off" }); + + //WHEN + Config.genericSet("ads", "sellout"); + + //THEN + expect(notificationAddMock).toHaveBeenCalledWith( + "Ad settings changed. Refreshing...", + 0 + ); + expect(miscReloadAfterMock).toHaveBeenCalledWith(3); + }); + + it("fails if test is active and funbox no_quit", () => { + //GIVEN + replaceConfig({ funbox: ["no_quit"], numbers: false }); + + //WHEN + expect(Config.genericSet("numbers", true, true)).toBe(false); + + //THEN + expect(notificationAddMock).toHaveBeenCalledWith( + "No quit funbox is active. Please finish the test.", + 0, + { + important: true, + } + ); + }); + + it("sends configEvents for overrideConfigs", () => { + //GIVEN + replaceConfig({ + confidenceMode: "off", + freedomMode: true, + stopOnError: "letter", + }); + + //WHEN + Config.genericSet("confidenceMode", "max"); + + //THEN + expect(dispatchConfigEventMock).toHaveBeenCalledWith( + "freedomMode", + false, + true, + true + ); + + expect(dispatchConfigEventMock).toHaveBeenCalledWith( + "stopOnError", + "off", + true, + "letter" + ); + + expect(dispatchConfigEventMock).toHaveBeenCalledWith( + "confidenceMode", + "max", + false, + "off" + ); + }); + }); + }); + it("setMode", () => { expect(Config.setMode("zen")).toBe(true); expect(Config.setMode("invalid" as any)).toBe(false); @@ -33,7 +520,7 @@ describe("Config", () => { it("setAccountChart", () => { expect(Config.setAccountChart(["on", "off", "off", "on"])).toBe(true); //arrays not having 4 values will get [on, on, on, on] as default - expect(Config.setAccountChart(["on", "off"] as any)).toBe(true); + expect(Config.setAccountChart(["on", "off"] as any)).toBe(false); expect(Config.setAccountChart(["on", "off", "on", "true"] as any)).toBe( false ); @@ -199,13 +686,13 @@ describe("Config", () => { //invalid values being "auto-fixed" expect(Config.setKeymapSize(0)).toBe(true); - expect(Config.default.keymapSize).toBe(0.5); + expect(getConfig().keymapSize).toBe(0.5); expect(Config.setKeymapSize(4)).toBe(true); - expect(Config.default.keymapSize).toBe(3.5); + expect(getConfig().keymapSize).toBe(3.5); expect(Config.setKeymapSize(1.25)).toBe(true); - expect(Config.default.keymapSize).toBe(1.3); + expect(getConfig().keymapSize).toBe(1.3); expect(Config.setKeymapSize(1.24)).toBe(true); - expect(Config.default.keymapSize).toBe(1.2); + expect(getConfig().keymapSize).toBe(1.2); }); it("setCustomBackgroundSize", () => { expect(Config.setCustomBackgroundSize("contain")).toBe(true); @@ -214,8 +701,10 @@ describe("Config", () => { }); it("setCustomBackgroundFilter", () => { expect(Config.setCustomBackgroundFilter([0, 1, 2, 3])).toBe(true); - //gets converted - expect(Config.setCustomBackgroundFilter([0, 1, 2, 3, 4] as any)).toBe(true); + + expect(Config.setCustomBackgroundFilter([0, 1, 2, 3, 4] as any)).toBe( + false + ); expect(Config.setCustomBackgroundFilter([] as any)).toBe(false); expect(Config.setCustomBackgroundFilter(["invalid"] as any)).toBe(false); expect(Config.setCustomBackgroundFilter([1, 2, 3, 4, 5, 6] as any)).toBe( @@ -231,9 +720,7 @@ describe("Config", () => { it("setCustomThemeColors", () => { expect(Config.setCustomThemeColors(customThemeColors(10))).toBe(true); - //gets converted - expect(Config.setCustomThemeColors(customThemeColors(9))).toBe(true); - + expect(Config.setCustomThemeColors(customThemeColors(9))).toBe(false); expect(Config.setCustomThemeColors([] as any)).toBe(false); expect(Config.setCustomThemeColors(["invalid"] as any)).toBe(false); expect(Config.setCustomThemeColors(customThemeColors(5))).toBe(false); @@ -258,7 +745,7 @@ describe("Config", () => { }); it("setAccountChart", () => { expect(Config.setAccountChart(["on", "off", "off", "on"])).toBe(true); - expect(Config.setAccountChart(["on", "off"] as any)).toBe(true); + expect(Config.setAccountChart(["on", "off"] as any)).toBe(false); expect(Config.setAccountChart(["on", "off", "on", "true"] as any)).toBe( false ); @@ -358,8 +845,6 @@ describe("Config", () => { expect(Config.setMinAccCustom(0)).toBe(true); expect(Config.setMinAccCustom(1)).toBe(true); expect(Config.setMinAccCustom(11.11)).toBe(true); - //gets converted - expect(Config.setMinAccCustom(120)).toBe(true); expect(Config.setMinAccCustom("invalid" as any)).toBe(false); expect(Config.setMinAccCustom(-1)).toBe(false); @@ -376,19 +861,12 @@ describe("Config", () => { expect(Config.setTimeConfig(0)).toBe(true); expect(Config.setTimeConfig(1)).toBe(true); - //gets converted - expect(Config.setTimeConfig("invalid" as any)).toBe(true); - expect(Config.setTimeConfig(-1)).toBe(true); - expect(Config.setTimeConfig(11.11)).toBe(false); }); it("setWordCount", () => { expect(Config.setWordCount(0)).toBe(true); expect(Config.setWordCount(1)).toBe(true); - //gets converted - expect(Config.setWordCount(-1)).toBe(true); - expect(Config.setWordCount("invalid" as any)).toBe(false); expect(Config.setWordCount(11.11)).toBe(false); }); @@ -486,12 +964,14 @@ describe("Config", () => { expect(Config.setCustomBackground("invalid")).toBe(false); }); it("setQuoteLength", () => { - expect(Config.setQuoteLength(0)).toBe(true); - expect(Config.setQuoteLength(-3)).toBe(true); - expect(Config.setQuoteLength(3)).toBe(true); + expect(Config.setQuoteLength([0])).toBe(true); + expect(Config.setQuoteLength([-3])).toBe(true); + expect(Config.setQuoteLength([3])).toBe(true); expect(Config.setQuoteLength(-4 as any)).toBe(false); expect(Config.setQuoteLength(4 as any)).toBe(false); + expect(Config.setQuoteLength(3 as any)).toBe(false); + expect(Config.setQuoteLength(2 as any)).toBe(false); expect(Config.setQuoteLength([0, -3, 2])).toBe(true); diff --git a/frontend/__tests__/utils/url-handler.spec.ts b/frontend/__tests__/utils/url-handler.spec.ts index 6321d8131..18504b90c 100644 --- a/frontend/__tests__/utils/url-handler.spec.ts +++ b/frontend/__tests__/utils/url-handler.spec.ts @@ -119,7 +119,7 @@ describe("url-handler", () => { //THEN expect(setModeMock).toHaveBeenCalledWith("quote", true); - expect(setQuoteLengthMock).toHaveBeenCalledWith(-2, false); + expect(setQuoteLengthMock).toHaveBeenCalledWith([-2], false); expect(setSelectedQuoteIdMock).toHaveBeenCalledWith(512); expect(restartTestMock).toHaveBeenCalled(); }); diff --git a/frontend/src/ts/commandline/lists/quote-length.ts b/frontend/src/ts/commandline/lists/quote-length.ts index 740e1022a..a9d4a3acb 100644 --- a/frontend/src/ts/commandline/lists/quote-length.ts +++ b/frontend/src/ts/commandline/lists/quote-length.ts @@ -19,7 +19,7 @@ const commands: Command[] = [ configValue: [0, 1, 2, 3], exec: (): void => { UpdateConfig.setMode("quote"); - UpdateConfig.setQuoteLength([0, 1, 2, 3]); + UpdateConfig.setQuoteLengthAll(); TestLogic.restart(); }, }, @@ -30,7 +30,7 @@ const commands: Command[] = [ configValueMode: "include", exec: (): void => { UpdateConfig.setMode("quote"); - UpdateConfig.setQuoteLength(0); + UpdateConfig.setQuoteLength([0]); TestLogic.restart(); }, }, @@ -41,7 +41,7 @@ const commands: Command[] = [ configValueMode: "include", exec: (): void => { UpdateConfig.setMode("quote"); - UpdateConfig.setQuoteLength(1); + UpdateConfig.setQuoteLength([1]); TestLogic.restart(); }, }, @@ -52,7 +52,7 @@ const commands: Command[] = [ configValueMode: "include", exec: (): void => { UpdateConfig.setMode("quote"); - UpdateConfig.setQuoteLength(2); + UpdateConfig.setQuoteLength([2]); TestLogic.restart(); }, }, @@ -63,7 +63,7 @@ const commands: Command[] = [ configValueMode: "include", exec: (): void => { UpdateConfig.setMode("quote"); - UpdateConfig.setQuoteLength(3); + UpdateConfig.setQuoteLength([3]); TestLogic.restart(); }, }, @@ -77,7 +77,7 @@ const commands: Command[] = [ }, exec: (): void => { UpdateConfig.setMode("quote"); - UpdateConfig.setQuoteLength(-3); + UpdateConfig.setQuoteLength([-3]); TestLogic.restart(); }, }, diff --git a/frontend/src/ts/config.ts b/frontend/src/ts/config.ts index a161b2ed9..345a2a040 100644 --- a/frontend/src/ts/config.ts +++ b/frontend/src/ts/config.ts @@ -1,10 +1,6 @@ import * as DB from "./db"; -import * as OutOfFocus from "./test/out-of-focus"; import * as Notifications from "./elements/notifications"; -import { - isConfigValueValidBoolean, - isConfigValueValid, -} from "./config-validation"; +import { isConfigValueValid } from "./config-validation"; import * as ConfigEvent from "./observables/config-event"; import { isAuthenticated } from "./firebase"; import * as AccountButton from "./elements/account-button"; @@ -23,16 +19,14 @@ import { } from "./utils/misc"; import * as ConfigSchemas from "@monkeytype/contracts/schemas/configs"; import { Config, FunboxName } from "@monkeytype/contracts/schemas/configs"; -import { Mode, ModeSchema } from "@monkeytype/contracts/schemas/shared"; -import { - Language, - LanguageSchema, -} from "@monkeytype/contracts/schemas/languages"; +import { Mode } from "@monkeytype/contracts/schemas/shared"; +import { Language } from "@monkeytype/contracts/schemas/languages"; import { LocalStorageWithSchema } from "./utils/local-storage-with-schema"; import { migrateConfig } from "./utils/config"; import { roundTo1 } from "@monkeytype/util/numbers"; import { getDefaultConfig } from "./constants/default-config"; import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json"; +import { ZodSchema } from "zod"; import * as TestState from "./test/test-state"; const configLS = new LocalStorageWithSchema({ @@ -49,11 +43,11 @@ const configLS = new LocalStorageWithSchema({ }, }); -const config = { +let config = { ...getDefaultConfig(), }; -let configToSend = {} as Config; +let configToSend: Partial = {}; const saveToDatabase = debounce(1000, () => { if (Object.keys(configToSend).length > 0) { AccountButton.loading(true); @@ -92,6 +86,20 @@ export function saveFullConfigToLocalStorage(noDbCheck = false): void { ConfigEvent.dispatch("saveToLocalStorage", stringified); } +// type ConfigMetadata = Partial< +// Record< +// ConfigSchemas.ConfigKey, +// { +// configKey: ConfigSchemas.ConfigKey; +// schema: z.ZodTypeAny; +// displayString?: string; +// preventSet: ( +// value: ConfigSchemas.Config[keyof ConfigSchemas.Config] +// ) => boolean; +// } +// > +// >; + function isConfigChangeBlocked(): boolean { if (TestState.isActive && config.funbox.includes("no_quit")) { Notifications.add("No quit funbox is active. Please finish the test.", 0, { @@ -102,138 +110,749 @@ function isConfigChangeBlocked(): boolean { return false; } -//numbers -export function setNumbers(numb: boolean, nosave?: boolean): boolean { - if (isConfigChangeBlocked()) return false; +// type SetBlock = { +// [K in keyof ConfigSchemas.Config]?: ConfigSchemas.Config[K][]; +// }; - if (!isConfigValueValidBoolean("numbers", numb)) return false; +// type RequiredConfig = { +// [K in keyof ConfigSchemas.Config]?: ConfigSchemas.Config[K]; +// }; - if (!canSetConfigWithCurrentFunboxes("numbers", numb, config.funbox)) { +export type ConfigMetadata = { + [K in keyof ConfigSchemas.Config]: { + /** + * Optional display string for the config key. + */ + displayString?: string; + /** + * Should the config change trigger a resize event? handled in ui.ts:108 + */ + triggerResize?: true; + + /** + * Is a test restart required after this config change? + */ + changeRequiresRestart: boolean; + /** + * Optional function that checks if the config value is blocked from being set. + * Returns true if setting the config value should be blocked. + * @param value - The value being set for the config key. + */ + isBlocked?: (value: ConfigSchemas.Config[K]) => boolean; + /** + * Optional function to override the value before setting it. + * Returns the modified value. + * @param value - The value being set for the config key. + * @param currentValue - The current value of the config key. + */ + overrideValue?: ( + value: ConfigSchemas.Config[K], + currentValue: ConfigSchemas.Config[K] + ) => ConfigSchemas.Config[K]; + /** + * Optional function to override other config values before this one is set. + * Returns an object with the config keys and their new values. + * @param value - The value being set for the config key. + */ + overrideConfig?: ( + value: ConfigSchemas.Config[K] + ) => Partial; + /** + * Optional function that is called after the config value is set. + * It can be used to perform additional actions, like reloading the page. + * @param nosave - If true, the change is not saved to localStorage or database. + */ + afterSet?: (nosave: boolean) => void; + }; +}; + +//todo: +// maybe have generic set somehow handle test restarting +// maybe add config group to each metadata object? all though its already defined in ConfigGroupsLiteral + +const configMetadata: ConfigMetadata = { + // test + punctuation: { + changeRequiresRestart: true, + overrideValue: (value) => { + if (config.mode === "quote") { + return false; + } + return value; + }, + }, + numbers: { + changeRequiresRestart: true, + overrideValue: (value) => { + if (config.mode === "quote") { + return false; + } + return value; + }, + }, + words: { + displayString: "word count", + changeRequiresRestart: true, + }, + time: { + changeRequiresRestart: true, + displayString: "time", + }, + mode: { + changeRequiresRestart: true, + overrideConfig: (value) => { + if (value === "custom" || value === "quote" || value === "zen") { + return { + numbers: false, + punctuation: false, + }; + } + return {}; + }, + afterSet: () => { + if (config.mode === "zen" && config.paceCaret !== "off") { + Notifications.add(`Pace caret will not work with zen mode.`, 0); + } + }, + }, + quoteLength: { + displayString: "quote length", + changeRequiresRestart: true, + }, + language: { + displayString: "language", + changeRequiresRestart: true, + }, + burstHeatmap: { + displayString: "burst heatmap", + changeRequiresRestart: false, + }, + + // behavior + difficulty: { + changeRequiresRestart: true, + }, + quickRestart: { + displayString: "quick restart", + changeRequiresRestart: false, + }, + repeatQuotes: { + displayString: "repeat quotes", + changeRequiresRestart: false, + }, + blindMode: { + displayString: "blind mode", + changeRequiresRestart: false, + }, + alwaysShowWordsHistory: { + displayString: "always show words history", + changeRequiresRestart: false, + }, + singleListCommandLine: { + displayString: "single list command line", + changeRequiresRestart: false, + }, + minWpm: { + displayString: "min speed", + changeRequiresRestart: true, + }, + minWpmCustomSpeed: { + displayString: "min speed custom", + changeRequiresRestart: true, + }, + minAcc: { + displayString: "min accuracy", + changeRequiresRestart: true, + }, + minAccCustom: { + displayString: "min accuracy custom", + changeRequiresRestart: true, + }, + minBurst: { + displayString: "min burst", + changeRequiresRestart: true, + }, + minBurstCustomSpeed: { + displayString: "min burst custom speed", + changeRequiresRestart: true, + }, + britishEnglish: { + displayString: "british english", + changeRequiresRestart: true, + }, + funbox: { + changeRequiresRestart: true, + isBlocked: (value) => { + for (const funbox of config.funbox) { + if (!canSetFunboxWithConfig(funbox, config)) { + Notifications.add( + `${value}" cannot be enabled with the current config`, + 0 + ); + return true; + } + } + return false; + }, + }, + customLayoutfluid: { + displayString: "custom layoutfluid", + changeRequiresRestart: true, + overrideValue: (value) => { + return Array.from(new Set(value)); + }, + }, + customPolyglot: { + displayString: "custom polyglot", + changeRequiresRestart: false, + overrideValue: (value) => { + return Array.from(new Set(value)); + }, + }, + + // input + freedomMode: { + changeRequiresRestart: false, + displayString: "freedom mode", + overrideConfig: (value) => { + if (value) { + return { + confidenceMode: "off", + }; + } + return {}; + }, + }, + strictSpace: { + displayString: "strict space", + changeRequiresRestart: true, + }, + oppositeShiftMode: { + displayString: "opposite shift mode", + changeRequiresRestart: false, + }, + stopOnError: { + displayString: "stop on error", + changeRequiresRestart: true, + overrideConfig: (value) => { + if (value !== "off") { + return { + confidenceMode: "off", + }; + } + return {}; + }, + }, + confidenceMode: { + displayString: "confidence mode", + changeRequiresRestart: false, + overrideConfig: (value) => { + if (value !== "off") { + return { + freedomMode: false, + stopOnError: "off", + }; + } + return {}; + }, + }, + quickEnd: { + displayString: "quick end", + changeRequiresRestart: false, + }, + indicateTypos: { + displayString: "indicate typos", + changeRequiresRestart: false, + }, + hideExtraLetters: { + displayString: "hide extra letters", + changeRequiresRestart: false, + }, + lazyMode: { + displayString: "lazy mode", + changeRequiresRestart: true, + }, + layout: { + displayString: "layout", + changeRequiresRestart: true, + }, + codeUnindentOnBackspace: { + displayString: "code unindent on backspace", + changeRequiresRestart: true, + }, + + // sound + soundVolume: { + displayString: "sound volume", + changeRequiresRestart: false, + }, + playSoundOnClick: { + displayString: "play sound on click", + changeRequiresRestart: false, + }, + playSoundOnError: { + displayString: "play sound on error", + changeRequiresRestart: false, + }, + + // caret + smoothCaret: { + displayString: "smooth caret", + changeRequiresRestart: false, + }, + caretStyle: { + displayString: "caret style", + changeRequiresRestart: false, + }, + paceCaret: { + displayString: "pace caret", + changeRequiresRestart: false, + isBlocked: (value) => { + if (document.readyState === "complete") { + if ((value === "pb" || value === "tagPb") && !isAuthenticated()) { + Notifications.add( + `Pace caret "pb" and "tag pb" are unavailable without an account`, + 0 + ); + return true; + } + } + return false; + }, + }, + paceCaretCustomSpeed: { + displayString: "pace caret custom speed", + changeRequiresRestart: false, + }, + paceCaretStyle: { + displayString: "pace caret style", + changeRequiresRestart: false, + }, + repeatedPace: { + displayString: "repeated pace", + changeRequiresRestart: false, + }, + + // appearance + timerStyle: { + displayString: "timer style", + changeRequiresRestart: false, + }, + liveSpeedStyle: { + displayString: "live speed style", + changeRequiresRestart: false, + }, + liveAccStyle: { + displayString: "live accuracy style", + changeRequiresRestart: false, + }, + liveBurstStyle: { + displayString: "live burst style", + changeRequiresRestart: false, + }, + timerColor: { + displayString: "timer color", + changeRequiresRestart: false, + }, + timerOpacity: { + displayString: "timer opacity", + changeRequiresRestart: false, + }, + highlightMode: { + displayString: "highlight mode", + changeRequiresRestart: false, + }, + tapeMode: { + triggerResize: true, + changeRequiresRestart: false, + displayString: "tape mode", + overrideConfig: (value) => { + if (value !== "off") { + return { + showAllLines: false, + }; + } + return {}; + }, + }, + tapeMargin: { + displayString: "tape margin", + changeRequiresRestart: false, + overrideValue: (value) => { + //TODO move to migration after settings validation + if (value < 10) { + value = 10; + } + if (value > 90) { + value = 90; + } + return value; + }, + }, + smoothLineScroll: { + displayString: "smooth line scroll", + changeRequiresRestart: false, + }, + showAllLines: { + changeRequiresRestart: false, + displayString: "show all lines", + isBlocked: (value) => { + if (value && config.tapeMode !== "off") { + Notifications.add("Show all lines doesn't support tape mode.", 0); + return true; + } + return false; + }, + }, + alwaysShowDecimalPlaces: { + displayString: "always show decimal places", + changeRequiresRestart: false, + }, + typingSpeedUnit: { + displayString: "typing speed unit", + changeRequiresRestart: false, + }, + startGraphsAtZero: { + displayString: "start graphs at zero", + changeRequiresRestart: false, + }, + maxLineWidth: { + changeRequiresRestart: false, + triggerResize: true, + displayString: "max line width", + overrideValue: (value) => { + //TODO move to migration after settings validation + if (value < 20 && value !== 0) { + value = 20; + } + if (value > 1000) { + value = 1000; + } + return value; + }, + }, + fontSize: { + changeRequiresRestart: false, + triggerResize: true, + displayString: "font size", + overrideValue: (value) => { + //TODO move to migration after settings validation + if (value < 0) { + value = 1; + } + return value; + }, + }, + fontFamily: { + displayString: "font family", + changeRequiresRestart: false, + }, + keymapMode: { + displayString: "keymap mode", + changeRequiresRestart: false, + }, + keymapLayout: { + displayString: "keymap layout", + changeRequiresRestart: false, + }, + keymapStyle: { + displayString: "keymap style", + changeRequiresRestart: false, + }, + keymapLegendStyle: { + displayString: "keymap legend style", + changeRequiresRestart: false, + }, + keymapShowTopRow: { + displayString: "keymap show top row", + changeRequiresRestart: false, + }, + keymapSize: { + triggerResize: true, + changeRequiresRestart: false, + displayString: "keymap size", + overrideValue: (value) => { + if (value < 0.5) value = 0.5; + if (value > 3.5) value = 3.5; + return roundTo1(value); + }, + }, + + // theme + flipTestColors: { + displayString: "flip test colors", + changeRequiresRestart: false, + }, + colorfulMode: { + displayString: "colorful mode", + changeRequiresRestart: false, + }, + customBackground: { + displayString: "custom background", + changeRequiresRestart: false, + overrideValue: (value) => { + return value.trim(); + }, + }, + customBackgroundSize: { + displayString: "custom background size", + changeRequiresRestart: false, + }, + customBackgroundFilter: { + displayString: "custom background filter", + changeRequiresRestart: false, + }, + autoSwitchTheme: { + displayString: "auto switch theme", + changeRequiresRestart: false, + }, + themeLight: { + displayString: "theme light", + changeRequiresRestart: false, + }, + themeDark: { + displayString: "theme dark", + changeRequiresRestart: false, + }, + randomTheme: { + changeRequiresRestart: false, + displayString: "random theme", + isBlocked: (value) => { + if (value === "custom") { + const snapshot = DB.getSnapshot(); + if (!isAuthenticated()) { + Notifications.add( + "Random theme 'custom' is unavailable without an account", + 0 + ); + return true; + } + if (!snapshot) { + Notifications.add( + "Random theme 'custom' requires a snapshot to be set", + 0 + ); + return true; + } + if (snapshot?.customThemes?.length === 0) { + Notifications.add( + "Random theme 'custom' requires at least one custom theme to be saved", + 0 + ); + return true; + } + } + return false; + }, + }, + favThemes: { + displayString: "favorite themes", + changeRequiresRestart: false, + }, + theme: { + changeRequiresRestart: false, + overrideConfig: () => { + return { + customTheme: false, + }; + }, + }, + customTheme: { + displayString: "custom theme", + changeRequiresRestart: false, + }, + customThemeColors: { + displayString: "custom theme colors", + changeRequiresRestart: false, + }, + + // hide elements + showKeyTips: { + displayString: "show key tips", + changeRequiresRestart: false, + }, + showOutOfFocusWarning: { + displayString: "show out of focus warning", + changeRequiresRestart: false, + }, + capsLockWarning: { + displayString: "caps lock warning", + changeRequiresRestart: false, + }, + showAverage: { + displayString: "show average", + changeRequiresRestart: false, + }, + + // other (hidden) + accountChart: { + displayString: "account chart", + changeRequiresRestart: false, + overrideValue: (value, currentValue) => { + // if both speed and accuracy are off, set opposite to on + // i dedicate this fix to AshesOfAFallen and our 2 collective brain cells + if (value[0] === "off" && value[1] === "off") { + const changedIndex = value[0] === currentValue[0] ? 0 : 1; + value[changedIndex] = "on"; + } + return value; + }, + }, + monkey: { + displayString: "monkey", + changeRequiresRestart: false, + }, + monkeyPowerLevel: { + displayString: "monkey power level", + changeRequiresRestart: false, + }, + + // ads + ads: { + changeRequiresRestart: false, + isBlocked: (value) => { + if (value !== "off" && isDevEnvironment()) { + Notifications.add("Ads are disabled in development mode.", 0); + return true; + } + return false; + }, + afterSet: (nosave) => { + if (!nosave && !isDevEnvironment()) { + reloadAfter(3); + Notifications.add("Ad settings changed. Refreshing...", 0); + } + }, + }, +}; + +export function genericSet( + key: T, + value: ConfigSchemas.Config[T], + nosave: boolean = false +): boolean { + const metadata = configMetadata[key] as ConfigMetadata[T]; + if (metadata === undefined) { + throw new Error(`Config metadata for key "${key}" is not defined.`); + } + + const previousValue = config[key]; + + if ( + metadata.changeRequiresRestart && + TestState.isActive && + config.funbox.includes("no_quit") + ) { + Notifications.add("No quit funbox is active. Please finish the test.", 0, { + important: true, + }); return false; } - if (config.mode === "quote") { - numb = false; + // if (metadata.setBlock) { + // let block = false; + // for (const blockKey of typedKeys(metadata.setBlock)) { + // const blockValues = metadata.setBlock[blockKey] ?? []; + // if ( + // config[blockKey] !== undefined && + // (blockValues as Array<(typeof config)[typeof blockKey]>).includes( + // config[blockKey] + // ) + // ) { + // block = true; + // break; + // } + // } + // if (block) { + // return false; + // } + // } + + if (metadata.isBlocked?.(value)) { + return false; } - config.numbers = numb; - saveToLocalStorage("numbers", nosave); - ConfigEvent.dispatch("numbers", config.numbers); + + if (metadata.overrideValue) { + value = metadata.overrideValue(value, config[key]); + } + + const schema = ConfigSchemas.ConfigSchema.shape[key] as ZodSchema; + + if (!isConfigValueValid(metadata.displayString ?? key, value, schema)) { + return false; + } + + if (!canSetConfigWithCurrentFunboxes(key, value, config.funbox)) { + return false; + } + + if (metadata.overrideConfig) { + const targetConfig = metadata.overrideConfig(value); + + for (const targetKey of typedKeys(targetConfig)) { + const targetValue = targetConfig[ + targetKey + ] as ConfigSchemas.Config[keyof typeof configMetadata]; + + if (config[targetKey] === targetValue) { + continue; // no need to set if the value is already the same + } + + const set = genericSet(targetKey, targetValue, true); + if (!set) { + throw new Error( + `Failed to set config key "${targetKey}" with value "${targetValue}" for ${metadata.displayString} config override.` + ); + } + } + } + + config[key] = value; + if (!nosave) saveToLocalStorage(key, nosave); + ConfigEvent.dispatch(key, value, nosave, previousValue); + + if (metadata.triggerResize && !nosave) { + $(window).trigger("resize"); + } + + metadata.afterSet?.(nosave || false); return true; } +//numbers +export function setNumbers(numb: boolean, nosave?: boolean): boolean { + return genericSet("numbers", numb, nosave); +} + //punctuation export function setPunctuation(punc: boolean, nosave?: boolean): boolean { - if (isConfigChangeBlocked()) return false; - - if (!isConfigValueValidBoolean("punctuation", punc)) return false; - - if (!canSetConfigWithCurrentFunboxes("punctuation", punc, config.funbox)) { - return false; - } - - if (config.mode === "quote") { - punc = false; - } - config.punctuation = punc; - saveToLocalStorage("punctuation", nosave); - ConfigEvent.dispatch("punctuation", config.punctuation); - - return true; + return genericSet("punctuation", punc, nosave); } export function setMode(mode: Mode, nosave?: boolean): boolean { - if (isConfigChangeBlocked()) return false; - - if (!isConfigValueValid("mode", mode, ModeSchema)) { - return false; - } - - if (!canSetConfigWithCurrentFunboxes("mode", mode, config.funbox)) { - return false; - } - - const previous = config.mode; - config.mode = mode; - if (config.mode === "custom") { - setPunctuation(false, true); - setNumbers(false, true); - } else if (config.mode === "quote") { - setPunctuation(false, true); - setNumbers(false, true); - } else if (config.mode === "zen") { - if (config.paceCaret !== "off") { - Notifications.add(`Pace caret will not work with zen mode.`, 0); - } - } - saveToLocalStorage("mode", nosave); - ConfigEvent.dispatch("mode", config.mode, nosave, previous); - - return true; + return genericSet("mode", mode, nosave); } export function setPlaySoundOnError( val: ConfigSchemas.PlaySoundOnError, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "play sound on error", - val, - ConfigSchemas.PlaySoundOnErrorSchema - ) - ) { - return false; - } - - config.playSoundOnError = val; - saveToLocalStorage("playSoundOnError", nosave); - ConfigEvent.dispatch("playSoundOnError", config.playSoundOnError); - - return true; + return genericSet("playSoundOnError", val, nosave); } export function setPlaySoundOnClick( val: ConfigSchemas.PlaySoundOnClick, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "play sound on click", - val, - ConfigSchemas.PlaySoundOnClickSchema - ) - ) { - return false; - } - - config.playSoundOnClick = val; - saveToLocalStorage("playSoundOnClick", nosave); - ConfigEvent.dispatch("playSoundOnClick", config.playSoundOnClick); - - return true; + return genericSet("playSoundOnClick", val, nosave); } export function setSoundVolume( val: ConfigSchemas.SoundVolume, nosave?: boolean ): boolean { - if (val < 0 || val > 1) { - Notifications.add("Sound volume must be between 0 and 1", 0); - val = 0.5; - } - - if ( - !isConfigValueValid("sound volume", val, ConfigSchemas.SoundVolumeSchema) - ) { - return false; - } - - config.soundVolume = val; - saveToLocalStorage("soundVolume", nosave); - ConfigEvent.dispatch("soundVolume", config.soundVolume); - - return true; + return genericSet("soundVolume", val, nosave); } //difficulty @@ -241,17 +860,7 @@ export function setDifficulty( diff: ConfigSchemas.Difficulty, nosave?: boolean ): boolean { - if (isConfigChangeBlocked()) return false; - - if (!isConfigValueValid("difficulty", diff, ConfigSchemas.DifficultySchema)) { - return false; - } - - config.difficulty = diff; - saveToLocalStorage("difficulty", nosave); - ConfigEvent.dispatch("difficulty", config.difficulty, nosave); - - return true; + return genericSet("difficulty", diff, nosave); } //set fav themes @@ -259,42 +868,14 @@ export function setFavThemes( themes: ConfigSchemas.FavThemes, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "favorite themes", - themes, - ConfigSchemas.FavThemesSchema - ) - ) { - return false; - } - config.favThemes = themes; - saveToLocalStorage("favThemes", nosave); - ConfigEvent.dispatch("favThemes", config.favThemes); - - return true; + return genericSet("favThemes", themes, nosave); } export function setFunbox( funbox: ConfigSchemas.Funbox, nosave?: boolean ): boolean { - if (isConfigChangeBlocked()) return false; - - if (!isConfigValueValid("funbox", funbox, ConfigSchemas.FunboxSchema)) - return false; - - for (const funbox of config.funbox) { - if (!canSetFunboxWithConfig(funbox, config)) { - return false; - } - } - - config.funbox = funbox; - saveToLocalStorage("funbox", nosave); - ConfigEvent.dispatch("funbox", config.funbox); - - return true; + return genericSet("funbox", funbox, nosave); } export function toggleFunbox(funbox: FunboxName, nosave?: boolean): boolean { @@ -325,123 +906,42 @@ export function toggleFunbox(funbox: FunboxName, nosave?: boolean): boolean { } export function setBlindMode(blind: boolean, nosave?: boolean): boolean { - if (!isConfigValueValidBoolean("blind mode", blind)) return false; - - config.blindMode = blind; - saveToLocalStorage("blindMode", nosave); - ConfigEvent.dispatch("blindMode", config.blindMode, nosave); - - return true; + return genericSet("blindMode", blind, nosave); } export function setAccountChart( array: ConfigSchemas.AccountChart, nosave?: boolean ): boolean { - if (array.length !== 4) { - array = ["on", "on", "on", "on"]; - } - - if ( - !isConfigValueValid( - "account chart", - array, - ConfigSchemas.AccountChartSchema - ) - ) { - return false; - } - - // if both speed and accuracy are off, set speed to on - // i dedicate this fix to AshesOfAFallen and our 2 collective brain cells - if (array[0] === "off" && array[1] === "off") { - array[0] = "on"; - } - - config.accountChart = array; - saveToLocalStorage("accountChart", nosave); - ConfigEvent.dispatch("accountChart", config.accountChart); - - return true; + return genericSet("accountChart", array, nosave); } export function setStopOnError( soe: ConfigSchemas.StopOnError, nosave?: boolean ): boolean { - if (isConfigChangeBlocked()) return false; - - if ( - !isConfigValueValid("stop on error", soe, ConfigSchemas.StopOnErrorSchema) - ) { - return false; - } - - config.stopOnError = soe; - if (config.stopOnError !== "off") { - config.confidenceMode = "off"; - saveToLocalStorage("confidenceMode", nosave); - } - saveToLocalStorage("stopOnError", nosave); - ConfigEvent.dispatch("stopOnError", config.stopOnError, nosave); - - return true; + return genericSet("stopOnError", soe, nosave); } export function setAlwaysShowDecimalPlaces( val: boolean, nosave?: boolean ): boolean { - if (!isConfigValueValidBoolean("always show decimal places", val)) { - return false; - } - - config.alwaysShowDecimalPlaces = val; - saveToLocalStorage("alwaysShowDecimalPlaces", nosave); - ConfigEvent.dispatch( - "alwaysShowDecimalPlaces", - config.alwaysShowDecimalPlaces - ); - - return true; + return genericSet("alwaysShowDecimalPlaces", val, nosave); } export function setTypingSpeedUnit( val: ConfigSchemas.TypingSpeedUnit, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "typing speed unit", - val, - ConfigSchemas.TypingSpeedUnitSchema - ) - ) { - return false; - } - config.typingSpeedUnit = val; - saveToLocalStorage("typingSpeedUnit", nosave); - ConfigEvent.dispatch("typingSpeedUnit", config.typingSpeedUnit, nosave); - - return true; + return genericSet("typingSpeedUnit", val, nosave); } export function setShowOutOfFocusWarning( val: boolean, nosave?: boolean ): boolean { - if (!isConfigValueValidBoolean("show out of focus warning", val)) { - return false; - } - - config.showOutOfFocusWarning = val; - if (!config.showOutOfFocusWarning) { - OutOfFocus.hide(); - } - saveToLocalStorage("showOutOfFocusWarning", nosave); - ConfigEvent.dispatch("showOutOfFocusWarning", config.showOutOfFocusWarning); - - return true; + return genericSet("showOutOfFocusWarning", val, nosave); } //pace caret @@ -449,59 +949,18 @@ export function setPaceCaret( val: ConfigSchemas.PaceCaret, nosave?: boolean ): boolean { - if (!isConfigValueValid("pace caret", val, ConfigSchemas.PaceCaretSchema)) { - return false; - } - - if (document.readyState === "complete") { - if ((val === "pb" || val === "tagPb") && !isAuthenticated()) { - Notifications.add( - `Pace caret "pb" and "tag pb" are unavailable without an account`, - 0 - ); - return false; - } - } - // if (config.mode === "zen" && val !== "off") { - // Notifications.add(`Can't use pace caret with zen mode.`, 0); - // val = "off"; - // } - config.paceCaret = val; - saveToLocalStorage("paceCaret", nosave); - ConfigEvent.dispatch("paceCaret", config.paceCaret, nosave); - - return true; + return genericSet("paceCaret", val, nosave); } export function setPaceCaretCustomSpeed( val: ConfigSchemas.PaceCaretCustomSpeed, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "pace caret custom speed", - val, - ConfigSchemas.PaceCaretCustomSpeedSchema - ) - ) { - return false; - } - - config.paceCaretCustomSpeed = val; - saveToLocalStorage("paceCaretCustomSpeed", nosave); - ConfigEvent.dispatch("paceCaretCustomSpeed", config.paceCaretCustomSpeed); - - return true; + return genericSet("paceCaretCustomSpeed", val, nosave); } export function setRepeatedPace(pace: boolean, nosave?: boolean): boolean { - if (!isConfigValueValidBoolean("repeated pace", pace)) return false; - - config.repeatedPace = pace; - saveToLocalStorage("repeatedPace", nosave); - ConfigEvent.dispatch("repeatedPace", config.repeatedPace); - - return true; + return genericSet("repeatedPace", pace, nosave); } //min wpm @@ -509,46 +968,14 @@ export function setMinWpm( minwpm: ConfigSchemas.MinimumWordsPerMinute, nosave?: boolean ): boolean { - if (isConfigChangeBlocked()) return false; - - if ( - !isConfigValueValid( - "min speed", - minwpm, - ConfigSchemas.MinimumWordsPerMinuteSchema - ) - ) { - return false; - } - - config.minWpm = minwpm; - saveToLocalStorage("minWpm", nosave); - ConfigEvent.dispatch("minWpm", config.minWpm, nosave); - - return true; + return genericSet("minWpm", minwpm, nosave); } export function setMinWpmCustomSpeed( val: ConfigSchemas.MinWpmCustomSpeed, nosave?: boolean ): boolean { - if (isConfigChangeBlocked()) return false; - - if ( - !isConfigValueValid( - "min speed custom", - val, - ConfigSchemas.MinWpmCustomSpeedSchema - ) - ) { - return false; - } - - config.minWpmCustomSpeed = val; - saveToLocalStorage("minWpmCustomSpeed", nosave); - ConfigEvent.dispatch("minWpmCustomSpeed", config.minWpmCustomSpeed); - - return true; + return genericSet("minWpmCustomSpeed", val, nosave); } //min acc @@ -556,40 +983,14 @@ export function setMinAcc( min: ConfigSchemas.MinimumAccuracy, nosave?: boolean ): boolean { - if (isConfigChangeBlocked()) return false; - - if (!isConfigValueValid("min acc", min, ConfigSchemas.MinimumAccuracySchema)) - return false; - - config.minAcc = min; - saveToLocalStorage("minAcc", nosave); - ConfigEvent.dispatch("minAcc", config.minAcc, nosave); - - return true; + return genericSet("minAcc", min, nosave); } export function setMinAccCustom( val: ConfigSchemas.MinimumAccuracyCustom, nosave?: boolean ): boolean { - if (isConfigChangeBlocked()) return false; - - //migrate legacy configs - if (val > 100) val = 100; - if ( - !isConfigValueValid( - "min acc custom", - val, - ConfigSchemas.MinimumAccuracyCustomSchema - ) - ) - return false; - - config.minAccCustom = val; - saveToLocalStorage("minAccCustom", nosave); - ConfigEvent.dispatch("minAccCustom", config.minAccCustom); - - return true; + return genericSet("minAccCustom", val, nosave); } //min burst @@ -597,40 +998,14 @@ export function setMinBurst( min: ConfigSchemas.MinimumBurst, nosave?: boolean ): boolean { - if (isConfigChangeBlocked()) return false; - - if (!isConfigValueValid("min burst", min, ConfigSchemas.MinimumBurstSchema)) { - return false; - } - - config.minBurst = min; - saveToLocalStorage("minBurst", nosave); - ConfigEvent.dispatch("minBurst", config.minBurst, nosave); - - return true; + return genericSet("minBurst", min, nosave); } export function setMinBurstCustomSpeed( val: ConfigSchemas.MinimumBurstCustomSpeed, nosave?: boolean ): boolean { - if (isConfigChangeBlocked()) return false; - - if ( - !isConfigValueValid( - "min burst custom speed", - val, - ConfigSchemas.MinimumBurstCustomSpeedSchema - ) - ) { - return false; - } - - config.minBurstCustomSpeed = val; - saveToLocalStorage("minBurstCustomSpeed", nosave); - ConfigEvent.dispatch("minBurstCustomSpeed", config.minBurstCustomSpeed); - - return true; + return genericSet("minBurstCustomSpeed", val, nosave); } //always show words history @@ -638,15 +1013,7 @@ export function setAlwaysShowWordsHistory( val: boolean, nosave?: boolean ): boolean { - if (!isConfigValueValidBoolean("always show words history", val)) { - return false; - } - - config.alwaysShowWordsHistory = val; - saveToLocalStorage("alwaysShowWordsHistory", nosave); - ConfigEvent.dispatch("alwaysShowWordsHistory", config.alwaysShowWordsHistory); - - return true; + return genericSet("alwaysShowWordsHistory", val, nosave); } //single list command line @@ -654,130 +1021,46 @@ export function setSingleListCommandLine( option: ConfigSchemas.SingleListCommandLine, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "single list command line", - option, - ConfigSchemas.SingleListCommandLineSchema - ) - ) { - return false; - } - - config.singleListCommandLine = option; - saveToLocalStorage("singleListCommandLine", nosave); - ConfigEvent.dispatch("singleListCommandLine", config.singleListCommandLine); - - return true; + return genericSet("singleListCommandLine", option, nosave); } //caps lock warning export function setCapsLockWarning(val: boolean, nosave?: boolean): boolean { - if (!isConfigValueValidBoolean("caps lock warning", val)) return false; - - config.capsLockWarning = val; - saveToLocalStorage("capsLockWarning", nosave); - ConfigEvent.dispatch("capsLockWarning", config.capsLockWarning); - - return true; + return genericSet("capsLockWarning", val, nosave); } export function setShowAllLines(sal: boolean, nosave?: boolean): boolean { - if (!isConfigValueValidBoolean("show all lines", sal)) return false; - - if (sal && config.tapeMode !== "off") { - Notifications.add("Show all lines doesn't support tape mode", 0); - return false; - } - - config.showAllLines = sal; - saveToLocalStorage("showAllLines", nosave); - ConfigEvent.dispatch("showAllLines", config.showAllLines, nosave); - - return true; + return genericSet("showAllLines", sal, nosave); } export function setQuickEnd(qe: boolean, nosave?: boolean): boolean { - if (!isConfigValueValidBoolean("quick end", qe)) return false; - - config.quickEnd = qe; - saveToLocalStorage("quickEnd", nosave); - ConfigEvent.dispatch("quickEnd", config.quickEnd); - - return true; + return genericSet("quickEnd", qe, nosave); } export function setAds(val: ConfigSchemas.Ads, nosave?: boolean): boolean { - if (!isConfigValueValid("ads", val, ConfigSchemas.AdsSchema)) { - return false; - } - - if (isDevEnvironment()) { - val = "off"; - console.debug("Ads are disabled in dev environment"); - } - - config.ads = val; - saveToLocalStorage("ads", nosave); - if (!nosave && !isDevEnvironment()) { - reloadAfter(3); - Notifications.add("Ad settings changed. Refreshing...", 0); - } - ConfigEvent.dispatch("ads", config.ads); - - return true; + return genericSet("ads", val, nosave); } export function setRepeatQuotes( val: ConfigSchemas.RepeatQuotes, nosave?: boolean ): boolean { - if ( - !isConfigValueValid("repeat quotes", val, ConfigSchemas.RepeatQuotesSchema) - ) { - return false; - } - - config.repeatQuotes = val; - saveToLocalStorage("repeatQuotes", nosave); - ConfigEvent.dispatch("repeatQuotes", config.repeatQuotes); - - return true; + return genericSet("repeatQuotes", val, nosave); } //flip colors export function setFlipTestColors(flip: boolean, nosave?: boolean): boolean { - if (!isConfigValueValidBoolean("flip test colors", flip)) return false; - - config.flipTestColors = flip; - saveToLocalStorage("flipTestColors", nosave); - ConfigEvent.dispatch("flipTestColors", config.flipTestColors); - - return true; + return genericSet("flipTestColors", flip, nosave); } //extra color export function setColorfulMode(extra: boolean, nosave?: boolean): boolean { - if (!isConfigValueValidBoolean("colorful mode", extra)) return false; - - config.colorfulMode = extra; - saveToLocalStorage("colorfulMode", nosave); - ConfigEvent.dispatch("colorfulMode", config.colorfulMode); - - return true; + return genericSet("colorfulMode", extra, nosave); } //strict space export function setStrictSpace(val: boolean, nosave?: boolean): boolean { - if (isConfigChangeBlocked()) return false; - - if (!isConfigValueValidBoolean("strict space", val)) return false; - - config.strictSpace = val; - saveToLocalStorage("strictSpace", nosave); - ConfigEvent.dispatch("strictSpace", config.strictSpace); - - return true; + return genericSet("strictSpace", val, nosave); } //opposite shift space @@ -785,340 +1068,99 @@ export function setOppositeShiftMode( val: ConfigSchemas.OppositeShiftMode, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "opposite shift mode", - val, - ConfigSchemas.OppositeShiftModeSchema - ) - ) { - return false; - } - - config.oppositeShiftMode = val; - saveToLocalStorage("oppositeShiftMode", nosave); - ConfigEvent.dispatch("oppositeShiftMode", config.oppositeShiftMode); - - return true; + return genericSet("oppositeShiftMode", val, nosave); } export function setCaretStyle( caretStyle: ConfigSchemas.CaretStyle, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "caret style", - caretStyle, - ConfigSchemas.CaretStyleSchema - ) - ) { - return false; - } - - config.caretStyle = caretStyle; - $("#caret").removeClass("off"); - $("#caret").removeClass("default"); - $("#caret").removeClass("underline"); - $("#caret").removeClass("outline"); - $("#caret").removeClass("block"); - $("#caret").removeClass("carrot"); - $("#caret").removeClass("banana"); - - if (caretStyle === "off") { - $("#caret").addClass("off"); - } else if (caretStyle === "default") { - $("#caret").addClass("default"); - } else if (caretStyle === "block") { - $("#caret").addClass("block"); - } else if (caretStyle === "outline") { - $("#caret").addClass("outline"); - } else if (caretStyle === "underline") { - $("#caret").addClass("underline"); - } else if (caretStyle === "carrot") { - $("#caret").addClass("carrot"); - } else if (caretStyle === "banana") { - $("#caret").addClass("banana"); - } - saveToLocalStorage("caretStyle", nosave); - ConfigEvent.dispatch("caretStyle", config.caretStyle); - - return true; + return genericSet("caretStyle", caretStyle, nosave); } export function setPaceCaretStyle( caretStyle: ConfigSchemas.CaretStyle, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "pace caret style", - caretStyle, - ConfigSchemas.CaretStyleSchema - ) - ) { - return false; - } - - config.paceCaretStyle = caretStyle; - $("#paceCaret").removeClass("off"); - $("#paceCaret").removeClass("default"); - $("#paceCaret").removeClass("underline"); - $("#paceCaret").removeClass("outline"); - $("#paceCaret").removeClass("block"); - $("#paceCaret").removeClass("carrot"); - $("#paceCaret").removeClass("banana"); - - if (caretStyle === "default") { - $("#paceCaret").addClass("default"); - } else if (caretStyle === "block") { - $("#paceCaret").addClass("block"); - } else if (caretStyle === "outline") { - $("#paceCaret").addClass("outline"); - } else if (caretStyle === "underline") { - $("#paceCaret").addClass("underline"); - } else if (caretStyle === "carrot") { - $("#paceCaret").addClass("carrot"); - } else if (caretStyle === "banana") { - $("#paceCaret").addClass("banana"); - } - saveToLocalStorage("paceCaretStyle", nosave); - ConfigEvent.dispatch("paceCaretStyle", config.paceCaretStyle); - - return true; + return genericSet("paceCaretStyle", caretStyle, nosave); } export function setShowAverage( value: ConfigSchemas.ShowAverage, nosave?: boolean ): boolean { - if ( - !isConfigValueValid("show average", value, ConfigSchemas.ShowAverageSchema) - ) { - return false; - } - - config.showAverage = value; - saveToLocalStorage("showAverage", nosave); - ConfigEvent.dispatch("showAverage", config.showAverage, nosave); - - return true; + return genericSet("showAverage", value, nosave); } export function setHighlightMode( mode: ConfigSchemas.HighlightMode, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "highlight mode", - mode, - ConfigSchemas.HighlightModeSchema - ) - ) { - return false; - } - - if (!canSetConfigWithCurrentFunboxes("highlightMode", mode, config.funbox)) { - return false; - } - - config.highlightMode = mode; - saveToLocalStorage("highlightMode", nosave); - ConfigEvent.dispatch("highlightMode", config.highlightMode); - - return true; + return genericSet("highlightMode", mode, nosave); } export function setTapeMode( mode: ConfigSchemas.TapeMode, nosave?: boolean ): boolean { - if (!isConfigValueValid("tape mode", mode, ConfigSchemas.TapeModeSchema)) { - return false; - } - - if (mode !== "off" && config.showAllLines) { - setShowAllLines(false, true); - } - - config.tapeMode = mode; - saveToLocalStorage("tapeMode", nosave); - ConfigEvent.dispatch("tapeMode", config.tapeMode); - - return true; + return genericSet("tapeMode", mode, nosave); } export function setTapeMargin( value: ConfigSchemas.TapeMargin, nosave?: boolean ): boolean { - if (value < 10) { - value = 10; - } - if (value > 90) { - value = 90; - } - - if ( - !isConfigValueValid("tape margin", value, ConfigSchemas.TapeMarginSchema) - ) { - return false; - } - - config.tapeMargin = value; - - saveToLocalStorage("tapeMargin", nosave); - ConfigEvent.dispatch("tapeMargin", config.tapeMargin, nosave); - - // trigger a resize event to update the layout - handled in ui.ts:108 - $(window).trigger("resize"); - - return true; + return genericSet("tapeMargin", value, nosave); } export function setHideExtraLetters(val: boolean, nosave?: boolean): boolean { - if (!isConfigValueValidBoolean("hide extra letters", val)) return false; - - config.hideExtraLetters = val; - saveToLocalStorage("hideExtraLetters", nosave); - ConfigEvent.dispatch("hideExtraLetters", config.hideExtraLetters); - - return true; + return genericSet("hideExtraLetters", val, nosave); } export function setTimerStyle( style: ConfigSchemas.TimerStyle, nosave?: boolean ): boolean { - if ( - !isConfigValueValid("timer style", style, ConfigSchemas.TimerStyleSchema) - ) { - return false; - } - - config.timerStyle = style; - saveToLocalStorage("timerStyle", nosave); - ConfigEvent.dispatch("timerStyle", config.timerStyle); - - return true; + return genericSet("timerStyle", style, nosave); } export function setLiveSpeedStyle( style: ConfigSchemas.LiveSpeedAccBurstStyle, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "live speed style", - style, - ConfigSchemas.LiveSpeedAccBurstStyleSchema - ) - ) { - return false; - } - - config.liveSpeedStyle = style; - saveToLocalStorage("liveSpeedStyle", nosave); - ConfigEvent.dispatch("liveSpeedStyle", config.timerStyle); - - return true; + return genericSet("liveSpeedStyle", style, nosave); } export function setLiveAccStyle( style: ConfigSchemas.LiveSpeedAccBurstStyle, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "live acc style", - style, - ConfigSchemas.LiveSpeedAccBurstStyleSchema - ) - ) { - return false; - } - - config.liveAccStyle = style; - saveToLocalStorage("liveAccStyle", nosave); - ConfigEvent.dispatch("liveAccStyle", config.timerStyle); - - return true; + return genericSet("liveAccStyle", style, nosave); } export function setLiveBurstStyle( style: ConfigSchemas.LiveSpeedAccBurstStyle, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "live burst style", - style, - ConfigSchemas.LiveSpeedAccBurstStyleSchema - ) - ) { - return false; - } - - config.liveBurstStyle = style; - saveToLocalStorage("liveBurstStyle", nosave); - ConfigEvent.dispatch("liveBurstStyle", config.timerStyle); - - return true; + return genericSet("liveBurstStyle", style, nosave); } export function setTimerColor( color: ConfigSchemas.TimerColor, nosave?: boolean ): boolean { - if ( - !isConfigValueValid("timer color", color, ConfigSchemas.TimerColorSchema) - ) { - return false; - } - - config.timerColor = color; - - saveToLocalStorage("timerColor", nosave); - ConfigEvent.dispatch("timerColor", config.timerColor); - - return true; + return genericSet("timerColor", color, nosave); } export function setTimerOpacity( opacity: ConfigSchemas.TimerOpacity, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "timer opacity", - opacity, - ConfigSchemas.TimerOpacitySchema - ) - ) { - return false; - } - - config.timerOpacity = opacity; - saveToLocalStorage("timerOpacity", nosave); - ConfigEvent.dispatch("timerOpacity", config.timerOpacity); - - return true; + return genericSet("timerOpacity", opacity, nosave); } //key tips export function setKeyTips(keyTips: boolean, nosave?: boolean): boolean { - if (!isConfigValueValidBoolean("key tips", keyTips)) return false; - - config.showKeyTips = keyTips; - if (config.showKeyTips) { - $("footer .keyTips").removeClass("hidden"); - } else { - $("footer .keyTips").addClass("hidden"); - } - saveToLocalStorage("showKeyTips", nosave); - ConfigEvent.dispatch("showKeyTips", config.showKeyTips); - - return true; + return genericSet("showKeyTips", keyTips, nosave); } //mode @@ -1126,100 +1168,25 @@ export function setTimeConfig( time: ConfigSchemas.TimeConfig, nosave?: boolean ): boolean { - if (isConfigChangeBlocked()) return false; - - time = isNaN(time) || time < 0 ? getDefaultConfig().time : time; - if (!isConfigValueValid("time", time, ConfigSchemas.TimeConfigSchema)) - return false; - - if (!canSetConfigWithCurrentFunboxes("words", time, config.funbox)) { - return false; - } - - config.time = time; - saveToLocalStorage("time", nosave); - ConfigEvent.dispatch("time", config.time); - - return true; + return genericSet("time", time, nosave); } export function setQuoteLength( - len: ConfigSchemas.QuoteLength[] | ConfigSchemas.QuoteLength, - nosave?: boolean, - multipleMode?: boolean + len: ConfigSchemas.QuoteLengthConfig, + nosave?: boolean ): boolean { - if (isConfigChangeBlocked()) return false; + return genericSet("quoteLength", len, nosave); +} - if (Array.isArray(len)) { - if ( - !isConfigValueValid( - "quote length", - len, - ConfigSchemas.QuoteLengthConfigSchema - ) - ) { - return false; - } - - //config load - if (len.length === 1 && len[0] === -1) len = [1]; - config.quoteLength = len; - } else { - if ( - !isConfigValueValid("quote length", len, ConfigSchemas.QuoteLengthSchema) - ) { - return false; - } - - if (!Array.isArray(config.quoteLength)) config.quoteLength = []; - if (len === null || isNaN(len) || len < -3 || len > 3) { - len = 1; - } - len = parseInt(len.toString()) as ConfigSchemas.QuoteLength; - - if (len === -1) { - config.quoteLength = [0, 1, 2, 3]; - } else if (multipleMode && len >= 0) { - if (!config.quoteLength.includes(len)) { - config.quoteLength.push(len); - } else { - if (config.quoteLength.length > 1) { - config.quoteLength = config.quoteLength.filter((ql) => ql !== len); - } - } - } else { - config.quoteLength = [len]; - } - } - // if (!nosave) setMode("quote", nosave); - saveToLocalStorage("quoteLength", nosave); - ConfigEvent.dispatch("quoteLength", config.quoteLength); - - return true; +export function setQuoteLengthAll(nosave?: boolean): boolean { + return genericSet("quoteLength", [0, 1, 2, 3], nosave); } export function setWordCount( wordCount: ConfigSchemas.WordCount, nosave?: boolean ): boolean { - if (isConfigChangeBlocked()) return false; - - wordCount = - wordCount < 0 || wordCount > 100000 ? getDefaultConfig().words : wordCount; - - if (!isConfigValueValid("words", wordCount, ConfigSchemas.WordCountSchema)) - return false; - - if (!canSetConfigWithCurrentFunboxes("words", wordCount, config.funbox)) { - return false; - } - - config.words = wordCount; - - saveToLocalStorage("words", nosave); - ConfigEvent.dispatch("words", config.words); - - return true; + return genericSet("words", wordCount, nosave); } //caret @@ -1227,65 +1194,23 @@ export function setSmoothCaret( mode: ConfigSchemas.SmoothCaret, nosave?: boolean ): boolean { - if ( - !isConfigValueValid("smooth caret", mode, ConfigSchemas.SmoothCaretSchema) - ) { - return false; - } - config.smoothCaret = mode; - if (mode === "off") { - $("#caret").css("animation-name", "caretFlashHard"); - } else { - $("#caret").css("animation-name", "caretFlashSmooth"); - } - - saveToLocalStorage("smoothCaret", nosave); - ConfigEvent.dispatch("smoothCaret", config.smoothCaret); - - return true; + return genericSet("smoothCaret", mode, nosave); } export function setCodeUnindentOnBackspace( mode: boolean, nosave?: boolean ): boolean { - if (!isConfigValueValidBoolean("code unindent on backspace", mode)) { - return false; - } - config.codeUnindentOnBackspace = mode; - - saveToLocalStorage("codeUnindentOnBackspace", nosave); - ConfigEvent.dispatch( - "codeUnindentOnBackspace", - config.codeUnindentOnBackspace, - nosave - ); - return true; + return genericSet("codeUnindentOnBackspace", mode, nosave); } export function setStartGraphsAtZero(mode: boolean, nosave?: boolean): boolean { - if (!isConfigValueValidBoolean("start graphs at zero", mode)) { - return false; - } - - config.startGraphsAtZero = mode; - saveToLocalStorage("startGraphsAtZero", nosave); - ConfigEvent.dispatch("startGraphsAtZero", config.startGraphsAtZero); - - return true; + return genericSet("startGraphsAtZero", mode, nosave); } //linescroll export function setSmoothLineScroll(mode: boolean, nosave?: boolean): boolean { - if (!isConfigValueValidBoolean("smooth line scroll", mode)) { - return false; - } - - config.smoothLineScroll = mode; - saveToLocalStorage("smoothLineScroll", nosave); - ConfigEvent.dispatch("smoothLineScroll", config.smoothLineScroll); - - return true; + return genericSet("smoothLineScroll", mode, nosave); } //quick restart @@ -1293,21 +1218,7 @@ export function setQuickRestartMode( mode: ConfigSchemas.QuickRestart, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "quick restart mode", - mode, - ConfigSchemas.QuickRestartSchema - ) - ) { - return false; - } - - config.quickRestart = mode; - saveToLocalStorage("quickRestart", nosave); - ConfigEvent.dispatch("quickRestart", config.quickRestart); - - return true; + return genericSet("quickRestart", mode, nosave); } //font family @@ -1315,717 +1226,197 @@ export function setFontFamily( font: ConfigSchemas.FontFamily, nosave?: boolean ): boolean { - if (!isConfigValueValid("font family", font, ConfigSchemas.FontFamilySchema)) - return false; - - if (font === "") { - font = "roboto_mono"; - Notifications.add( - "Empty input received, reverted to the default font.", - 0, - { - customTitle: "Custom font", - } - ); - } - if (!font || !/^[0-9a-zA-Z_.\-#+()]+$/.test(font)) { - Notifications.add(`Invalid font name value: "${font}".`, -1, { - customTitle: "Custom font", - duration: 3, - }); - return false; - } - config.fontFamily = font; - document.documentElement.style.setProperty( - "--font", - `"${font.replace(/_/g, " ")}", "Roboto Mono", "Vazirmatn", monospace` - ); - saveToLocalStorage("fontFamily", nosave); - ConfigEvent.dispatch("fontFamily", config.fontFamily); - - return true; + return genericSet("fontFamily", font, nosave); } //freedom export function setFreedomMode(freedom: boolean, nosave?: boolean): boolean { - if (!isConfigValueValidBoolean("freedom mode", freedom)) return false; - - if (freedom === null || freedom === undefined) { - freedom = false; - } - config.freedomMode = freedom; - if (config.freedomMode && config.confidenceMode !== "off") { - config.confidenceMode = "off"; - } - saveToLocalStorage("freedomMode", nosave); - ConfigEvent.dispatch("freedomMode", config.freedomMode); - - return true; + return genericSet("freedomMode", freedom, nosave); } export function setConfidenceMode( cm: ConfigSchemas.ConfidenceMode, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "confidence mode", - cm, - ConfigSchemas.ConfidenceModeSchema - ) - ) { - return false; - } - - config.confidenceMode = cm; - if (config.confidenceMode !== "off") { - config.freedomMode = false; - config.stopOnError = "off"; - saveToLocalStorage("freedomMode", nosave); - saveToLocalStorage("stopOnError", nosave); - } - saveToLocalStorage("confidenceMode", nosave); - ConfigEvent.dispatch("confidenceMode", config.confidenceMode, nosave); - - return true; + return genericSet("confidenceMode", cm, nosave); } export function setIndicateTypos( value: ConfigSchemas.IndicateTypos, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "indicate typos", - value, - ConfigSchemas.IndicateTyposSchema - ) - ) { - return false; - } - - config.indicateTypos = value; - saveToLocalStorage("indicateTypos", nosave); - ConfigEvent.dispatch("indicateTypos", config.indicateTypos); - - return true; + return genericSet("indicateTypos", value, nosave); } export function setAutoSwitchTheme( boolean: boolean, nosave?: boolean ): boolean { - if (!isConfigValueValidBoolean("auto switch theme", boolean)) { - return false; - } - - boolean = boolean ?? getDefaultConfig().autoSwitchTheme; - config.autoSwitchTheme = boolean; - saveToLocalStorage("autoSwitchTheme", nosave); - ConfigEvent.dispatch("autoSwitchTheme", config.autoSwitchTheme); - - return true; + return genericSet("autoSwitchTheme", boolean, nosave); } export function setCustomTheme(boolean: boolean, nosave?: boolean): boolean { - if (!isConfigValueValidBoolean("custom theme", boolean)) return false; - - config.customTheme = boolean; - saveToLocalStorage("customTheme", nosave); - ConfigEvent.dispatch("customTheme", config.customTheme); - - return true; + return genericSet("customTheme", boolean, nosave); } export function setTheme( name: ConfigSchemas.ThemeName, nosave?: boolean ): boolean { - if (!isConfigValueValid("theme", name, ConfigSchemas.ThemeNameSchema)) - return false; - - config.theme = name; - if (config.customTheme) setCustomTheme(false); - saveToLocalStorage("theme", nosave); - ConfigEvent.dispatch("theme", config.theme); - - return true; + return genericSet("theme", name, nosave); } export function setThemeLight( name: ConfigSchemas.ThemeName, nosave?: boolean ): boolean { - if (!isConfigValueValid("theme light", name, ConfigSchemas.ThemeNameSchema)) - return false; - - config.themeLight = name; - saveToLocalStorage("themeLight", nosave); - ConfigEvent.dispatch("themeLight", config.themeLight, nosave); - - return true; + return genericSet("themeLight", name, nosave); } export function setThemeDark( name: ConfigSchemas.ThemeName, nosave?: boolean ): boolean { - if (!isConfigValueValid("theme dark", name, ConfigSchemas.ThemeNameSchema)) - return false; - - config.themeDark = name; - saveToLocalStorage("themeDark", nosave); - ConfigEvent.dispatch("themeDark", config.themeDark, nosave); - - return true; -} - -function setThemes( - theme: ConfigSchemas.ThemeName, - customState: boolean, - customThemeColors: ConfigSchemas.CustomThemeColors, - autoSwitchTheme: boolean, - nosave?: boolean -): boolean { - if (!isConfigValueValid("themes", theme, ConfigSchemas.ThemeNameSchema)) - return false; - - //@ts-expect-error config used to have 9 - if (customThemeColors.length === 9) { - //color missing - if (customState) { - Notifications.add( - "Missing sub alt color. Please edit it in the custom theme settings and save your changes.", - 0, - { - duration: 7, - } - ); - } - customThemeColors.splice(4, 0, "#000000"); - } - - config.customThemeColors = customThemeColors; - config.theme = theme; - config.customTheme = customState; - config.autoSwitchTheme = autoSwitchTheme; - saveToLocalStorage("theme", nosave); - ConfigEvent.dispatch("setThemes", customState); - - return true; + return genericSet("themeDark", name, nosave); } export function setRandomTheme( val: ConfigSchemas.RandomTheme, nosave?: boolean ): boolean { - if ( - !isConfigValueValid("random theme", val, ConfigSchemas.RandomThemeSchema) - ) { - return false; - } - - if (val === "custom") { - if (!isAuthenticated()) { - config.randomTheme = val; - return false; - } - if (!DB.getSnapshot()) return true; - if (DB.getSnapshot()?.customThemes?.length === 0) { - Notifications.add("You need to create a custom theme first", 0); - config.randomTheme = "off"; - return false; - } - } - - config.randomTheme = val; - saveToLocalStorage("randomTheme", nosave); - ConfigEvent.dispatch("randomTheme", config.randomTheme); - - return true; + return genericSet("randomTheme", val, nosave); } export function setBritishEnglish(val: boolean, nosave?: boolean): boolean { - if (isConfigChangeBlocked()) return false; - - if (!isConfigValueValidBoolean("british english", val)) return false; - - if (!val) { - val = false; - } - config.britishEnglish = val; - saveToLocalStorage("britishEnglish", nosave); - ConfigEvent.dispatch("britishEnglish", config.britishEnglish); - - return true; + return genericSet("britishEnglish", val, nosave); } export function setLazyMode(val: boolean, nosave?: boolean): boolean { - if (isConfigChangeBlocked()) return false; - - if (!isConfigValueValidBoolean("lazy mode", val)) return false; - - if (!val) { - val = false; - } - config.lazyMode = val; - saveToLocalStorage("lazyMode", nosave); - ConfigEvent.dispatch("lazyMode", config.lazyMode, nosave); - - return true; + return genericSet("lazyMode", val, nosave); } export function setCustomThemeColors( colors: ConfigSchemas.CustomThemeColors, nosave?: boolean ): boolean { - // migrate existing configs missing sub alt color - // @ts-expect-error legacy configs - if (colors.length === 9) { - //color missing - Notifications.add( - "Missing sub alt color. Please edit it in the custom theme settings and save your changes.", - 0, - { - duration: 7, - } - ); - colors.splice(4, 0, "#000000"); - } - - if ( - !isConfigValueValid( - "custom theme colors", - colors, - ConfigSchemas.CustomThemeColorsSchema - ) - ) { - return false; - } - - if (colors !== undefined) { - config.customThemeColors = colors; - // ThemeController.set("custom"); - // applyCustomThemeColors(); - } - saveToLocalStorage("customThemeColors", nosave); - ConfigEvent.dispatch("customThemeColors", config.customThemeColors, nosave); - - return true; + return genericSet("customThemeColors", colors, nosave); } export function setLanguage(language: Language, nosave?: boolean): boolean { - if (isConfigChangeBlocked()) return false; - - if (!isConfigValueValid("language", language, LanguageSchema)) return false; - - config.language = language; - saveToLocalStorage("language", nosave); - ConfigEvent.dispatch("language", config.language); - - return true; + return genericSet("language", language, nosave); } export function setMonkey(monkey: boolean, nosave?: boolean): boolean { - if (!isConfigValueValidBoolean("monkey", monkey)) return false; - - config.monkey = monkey; - saveToLocalStorage("monkey", nosave); - ConfigEvent.dispatch("monkey", config.monkey); - - return true; + return genericSet("monkey", monkey, nosave); } export function setKeymapMode( mode: ConfigSchemas.KeymapMode, nosave?: boolean ): boolean { - if ( - !isConfigValueValid("keymap mode", mode, ConfigSchemas.KeymapModeSchema) - ) { - return false; - } - - $(".activeKey").removeClass("activeKey"); - $(".keymapKey").attr("style", ""); - config.keymapMode = mode; - saveToLocalStorage("keymapMode", nosave); - ConfigEvent.dispatch("keymapMode", config.keymapMode, nosave); - - return true; + return genericSet("keymapMode", mode, nosave); } export function setKeymapLegendStyle( style: ConfigSchemas.KeymapLegendStyle, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "keymap legend style", - style, - ConfigSchemas.KeymapLegendStyleSchema - ) - ) { - return false; - } - - // Remove existing styles - const keymapLegendStyles = ["lowercase", "uppercase", "blank", "dynamic"]; - keymapLegendStyles.forEach((name) => { - $(".keymapLegendStyle").removeClass(name); - }); - - style = style || "lowercase"; - - // Mutate the keymap in the DOM, if it exists. - // 1. Remove everything - $(".keymapKey > .letter").css("display", ""); - $(".keymapKey > .letter").css("text-transform", ""); - - // 2. Append special styles onto the DOM elements - if (style === "uppercase") { - $(".keymapKey > .letter").css("text-transform", "capitalize"); - } - if (style === "blank") { - $(".keymapKey > .letter").css("display", "none"); - } - - // Update and save to cookie for persistence - $(".keymapLegendStyle").addClass(style); - config.keymapLegendStyle = style; - saveToLocalStorage("keymapLegendStyle", nosave); - ConfigEvent.dispatch("keymapLegendStyle", config.keymapLegendStyle); - - return true; + return genericSet("keymapLegendStyle", style, nosave); } export function setKeymapStyle( style: ConfigSchemas.KeymapStyle, nosave?: boolean ): boolean { - if ( - !isConfigValueValid("keymap style", style, ConfigSchemas.KeymapStyleSchema) - ) { - return false; - } - - style = style || "staggered"; - config.keymapStyle = style; - saveToLocalStorage("keymapStyle", nosave); - ConfigEvent.dispatch("keymapStyle", config.keymapStyle); - - return true; + return genericSet("keymapStyle", style, nosave); } export function setKeymapLayout( layout: ConfigSchemas.KeymapLayout, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "keymap layout", - layout, - ConfigSchemas.KeymapLayoutSchema - ) - ) - return false; - - config.keymapLayout = layout; - saveToLocalStorage("keymapLayout", nosave); - ConfigEvent.dispatch("keymapLayout", config.keymapLayout); - - return true; + return genericSet("keymapLayout", layout, nosave); } export function setKeymapShowTopRow( show: ConfigSchemas.KeymapShowTopRow, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "keymapShowTopRow", - show, - ConfigSchemas.KeymapShowTopRowSchema - ) - ) { - return false; - } - - config.keymapShowTopRow = show; - saveToLocalStorage("keymapShowTopRow", nosave); - ConfigEvent.dispatch("keymapShowTopRow", config.keymapShowTopRow); - - return true; + return genericSet("keymapShowTopRow", show, nosave); } export function setKeymapSize( keymapSize: ConfigSchemas.KeymapSize, nosave?: boolean ): boolean { - //auto-fix values to avoid validation errors - if (keymapSize < 0.5) keymapSize = 0.5; - if (keymapSize > 3.5) keymapSize = 3.5; - keymapSize = roundTo1(keymapSize); - - if ( - !isConfigValueValid( - "keymap size", - keymapSize, - ConfigSchemas.KeymapSizeSchema - ) - ) { - return false; - } - - config.keymapSize = keymapSize; - - $("#keymap").css("zoom", keymapSize); - - saveToLocalStorage("keymapSize", nosave); - ConfigEvent.dispatch("keymapSize", config.keymapSize, nosave); - - // trigger a resize event to update the layout - handled in ui.ts:108 - $(window).trigger("resize"); - - return true; + return genericSet("keymapSize", keymapSize, nosave); } export function setLayout( layout: ConfigSchemas.Layout, nosave?: boolean ): boolean { - if (isConfigChangeBlocked()) return false; - - if (!isConfigValueValid("layout", layout, ConfigSchemas.LayoutSchema)) - return false; - - config.layout = layout; - saveToLocalStorage("layout", nosave); - ConfigEvent.dispatch("layout", config.layout, nosave); - - return true; + return genericSet("layout", layout, nosave); } -// export function setSavedLayout(layout: string, nosave?: boolean): boolean { -// if (layout === null || layout === undefined) { -// layout = "qwerty"; -// } -// config.savedLayout = layout; -// setLayout(layout, nosave); - -// return true; -// } - export function setFontSize( fontSize: ConfigSchemas.FontSize, nosave?: boolean ): boolean { - if (fontSize < 0) { - fontSize = 1; - } - - if ( - !isConfigValueValid("font size", fontSize, ConfigSchemas.FontSizeSchema) - ) { - return false; - } - - config.fontSize = fontSize; - - $("#caret, #paceCaret, #liveStatsMini, #typingTest, #wordsInput").css( - "fontSize", - fontSize + "rem" - ); - - saveToLocalStorage("fontSize", nosave); - ConfigEvent.dispatch("fontSize", config.fontSize, nosave); - - // trigger a resize event to update the layout - handled in ui.ts:108 - if (!nosave) $(window).trigger("resize"); - - return true; + return genericSet("fontSize", fontSize, nosave); } export function setMaxLineWidth( maxLineWidth: ConfigSchemas.MaxLineWidth, nosave?: boolean ): boolean { - if (maxLineWidth < 20 && maxLineWidth !== 0) { - maxLineWidth = 20; - } - if (maxLineWidth > 1000) { - maxLineWidth = 1000; - } - - if ( - !isConfigValueValid( - "max line width", - maxLineWidth, - ConfigSchemas.MaxLineWidthSchema - ) - ) { - return false; - } - - config.maxLineWidth = maxLineWidth; - - saveToLocalStorage("maxLineWidth", nosave); - ConfigEvent.dispatch("maxLineWidth", config.maxLineWidth, nosave); - - // trigger a resize event to update the layout - handled in ui.ts:108 - $(window).trigger("resize"); - - return true; + return genericSet("maxLineWidth", maxLineWidth, nosave); } export function setCustomBackground( value: ConfigSchemas.CustomBackground, nosave?: boolean ): boolean { - value = value.trim(); - if ( - !isConfigValueValid( - "custom background", - value, - ConfigSchemas.CustomBackgroundSchema - ) - ) - return false; - - config.customBackground = value; - saveToLocalStorage("customBackground", nosave); - ConfigEvent.dispatch("customBackground", config.customBackground); - - return true; + return genericSet("customBackground", value, nosave); } export function setCustomLayoutfluid( value: ConfigSchemas.CustomLayoutFluid, nosave?: boolean ): boolean { - if (isConfigChangeBlocked()) return false; - - // Remove duplicates - const deduped = Array.from(new Set(value)); - if ( - !isConfigValueValid( - "layoutfluid", - deduped, - ConfigSchemas.CustomLayoutFluidSchema - ) - ) { - return false; - } - - config.customLayoutfluid = deduped; - saveToLocalStorage("customLayoutfluid", nosave); - ConfigEvent.dispatch("customLayoutfluid", config.customLayoutfluid); - - return true; + return genericSet("customLayoutfluid", value, nosave); } export function setCustomPolyglot( value: ConfigSchemas.CustomPolyglot, nosave?: boolean ): boolean { - if (isConfigChangeBlocked()) return false; - - // remove duplicates - const deduped = Array.from(new Set(value)); - if ( - !isConfigValueValid( - "customPolyglot", - deduped, - ConfigSchemas.CustomPolyglotSchema - ) - ) - return false; - - config.customPolyglot = deduped; - saveToLocalStorage("customPolyglot", nosave); - ConfigEvent.dispatch("customPolyglot", config.customPolyglot); - - return true; + return genericSet("customPolyglot", value, nosave); } export function setCustomBackgroundSize( value: ConfigSchemas.CustomBackgroundSize, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "custom background size", - value, - ConfigSchemas.CustomBackgroundSizeSchema - ) - ) { - return false; - } - - config.customBackgroundSize = value; - saveToLocalStorage("customBackgroundSize", nosave); - ConfigEvent.dispatch("customBackgroundSize", config.customBackgroundSize); - - return true; + return genericSet("customBackgroundSize", value, nosave); } export function setCustomBackgroundFilter( array: ConfigSchemas.CustomBackgroundFilter, nosave?: boolean ): boolean { - // @ts-expect-error this used to be 5 - // need to convert existing configs using five values down to four - if (array.length === 5) { - array = [array[0], array[1], array[2], array[3]]; - } - - if ( - !isConfigValueValid( - "custom background filter", - array, - ConfigSchemas.CustomBackgroundFilterSchema - ) - ) { - return false; - } - - config.customBackgroundFilter = array; - saveToLocalStorage("customBackgroundFilter", nosave); - ConfigEvent.dispatch("customBackgroundFilter", config.customBackgroundFilter); - - return true; + return genericSet("customBackgroundFilter", array, nosave); } + export function setMonkeyPowerLevel( level: ConfigSchemas.MonkeyPowerLevel, nosave?: boolean ): boolean { - if ( - !isConfigValueValid( - "monkey power level", - level, - ConfigSchemas.MonkeyPowerLevelSchema - ) - ) { - return false; - } - config.monkeyPowerLevel = level; - saveToLocalStorage("monkeyPowerLevel", nosave); - ConfigEvent.dispatch("monkeyPowerLevel", config.monkeyPowerLevel); - - return true; + return genericSet("monkeyPowerLevel", level, nosave); } export function setBurstHeatmap(value: boolean, nosave?: boolean): boolean { - if (!isConfigValueValidBoolean("burst heatmap", value)) return false; - - if (!value) { - value = false; - } - config.burstHeatmap = value; - saveToLocalStorage("burstHeatmap", nosave); - ConfigEvent.dispatch("burstHeatmap", config.burstHeatmap); - - return true; + return genericSet("burstHeatmap", value, nosave); } export async function apply( @@ -2043,97 +1434,10 @@ export async function apply( } }); if (configObj !== undefined && configObj !== null) { - setAds(configObj.ads, true); - setThemeLight(configObj.themeLight, true); - setThemeDark(configObj.themeDark, true); - setThemes( - configObj.theme, - configObj.customTheme, - configObj.customThemeColors, - configObj.autoSwitchTheme, - true - ); - setCustomLayoutfluid(configObj.customLayoutfluid, true); - setCustomPolyglot(configObj.customPolyglot, true); - setCustomBackground(configObj.customBackground, true); - setCustomBackgroundSize(configObj.customBackgroundSize, true); - setCustomBackgroundFilter(configObj.customBackgroundFilter, true); - setQuickRestartMode(configObj.quickRestart, true); - setKeyTips(configObj.showKeyTips, true); - setTimeConfig(configObj.time, true); - setQuoteLength(configObj.quoteLength, true); - setWordCount(configObj.words, true); - setLanguage(configObj.language, true); - setLayout(configObj.layout, true); - setFontSize(configObj.fontSize, true); - setMaxLineWidth(configObj.maxLineWidth, true); - setFreedomMode(configObj.freedomMode, true); - setCaretStyle(configObj.caretStyle, true); - setPaceCaretStyle(configObj.paceCaretStyle, true); - setDifficulty(configObj.difficulty, true); - setBlindMode(configObj.blindMode, true); - setQuickEnd(configObj.quickEnd, true); - setFlipTestColors(configObj.flipTestColors, true); - setColorfulMode(configObj.colorfulMode, true); - setConfidenceMode(configObj.confidenceMode, true); - setIndicateTypos(configObj.indicateTypos, true); - setTimerStyle(configObj.timerStyle, true); - setLiveSpeedStyle(configObj.liveSpeedStyle, true); - setLiveAccStyle(configObj.liveAccStyle, true); - setLiveBurstStyle(configObj.liveBurstStyle, true); - setTimerColor(configObj.timerColor, true); - setTimerOpacity(configObj.timerOpacity, true); - setKeymapMode(configObj.keymapMode, true); - setKeymapStyle(configObj.keymapStyle, true); - setKeymapLegendStyle(configObj.keymapLegendStyle, true); - setKeymapLayout(configObj.keymapLayout, true); - setKeymapShowTopRow(configObj.keymapShowTopRow, true); - setKeymapSize(configObj.keymapSize, true); - setFontFamily(configObj.fontFamily, true); - setSmoothCaret(configObj.smoothCaret, true); - setCodeUnindentOnBackspace(configObj.codeUnindentOnBackspace, true); - setSmoothLineScroll(configObj.smoothLineScroll, true); - setAlwaysShowDecimalPlaces(configObj.alwaysShowDecimalPlaces, true); - setAlwaysShowWordsHistory(configObj.alwaysShowWordsHistory, true); - setSingleListCommandLine(configObj.singleListCommandLine, true); - setCapsLockWarning(configObj.capsLockWarning, true); - setPlaySoundOnError(configObj.playSoundOnError, true); - setPlaySoundOnClick(configObj.playSoundOnClick, true); - setSoundVolume(configObj.soundVolume, true); - setStopOnError(configObj.stopOnError, true); - setFavThemes(configObj.favThemes, true); - setFunbox(configObj.funbox, true); - setRandomTheme(configObj.randomTheme, true); - setShowAllLines(configObj.showAllLines, true); - setShowOutOfFocusWarning(configObj.showOutOfFocusWarning, true); - setPaceCaret(configObj.paceCaret, true); - setPaceCaretCustomSpeed(configObj.paceCaretCustomSpeed, true); - setRepeatedPace(configObj.repeatedPace, true); - setAccountChart(configObj.accountChart, true); - setMinBurst(configObj.minBurst, true); - setMinBurstCustomSpeed(configObj.minBurstCustomSpeed, true); - setMinWpm(configObj.minWpm, true); - setMinWpmCustomSpeed(configObj.minWpmCustomSpeed, true); - setMinAcc(configObj.minAcc, true); - setMinAccCustom(configObj.minAccCustom, true); - setHighlightMode(configObj.highlightMode, true); - setTypingSpeedUnit(configObj.typingSpeedUnit, true); - setHideExtraLetters(configObj.hideExtraLetters, true); - setStartGraphsAtZero(configObj.startGraphsAtZero, true); - setStrictSpace(configObj.strictSpace, true); - setOppositeShiftMode(configObj.oppositeShiftMode, true); - setMode(configObj.mode, true); - setNumbers(configObj.numbers, true); - setPunctuation(configObj.punctuation, true); - setMonkey(configObj.monkey, true); - setRepeatQuotes(configObj.repeatQuotes, true); - setMonkeyPowerLevel(configObj.monkeyPowerLevel, true); - setBurstHeatmap(configObj.burstHeatmap, true); - setBritishEnglish(configObj.britishEnglish, true); - setLazyMode(configObj.lazyMode, true); - setShowAverage(configObj.showAverage, true); - setTapeMode(configObj.tapeMode, true); - setTapeMargin(configObj.tapeMargin, true); + for (const configKey of typedKeys(configObj)) { + const configValue = configObj[configKey]; + genericSet(configKey, configValue, true); + } ConfigEvent.dispatch( "configApplied", @@ -2205,3 +1509,11 @@ const { promise: loadPromise, resolve: loadDone } = promiseWithResolvers(); export { loadPromise }; export default config; +export const __testing = { + configMetadata, + replaceConfig: (setConfig: Partial): void => { + config = { ...getDefaultConfig(), ...setConfig }; + configToSend = {} as Config; + }, + getConfig: () => config, +}; diff --git a/frontend/src/ts/controllers/theme-controller.ts b/frontend/src/ts/controllers/theme-controller.ts index 3ff1c8f5e..4ae10593d 100644 --- a/frontend/src/ts/controllers/theme-controller.ts +++ b/frontend/src/ts/controllers/theme-controller.ts @@ -415,7 +415,36 @@ window } }); +let ignoreConfigEvent = false; + ConfigEvent.subscribe(async (eventKey, eventValue, nosave) => { + if (eventKey === "fullConfigChange") { + ignoreConfigEvent = true; + } + if (eventKey === "fullConfigChangeFinished") { + ignoreConfigEvent = false; + + await clearRandom(); + await clearPreview(false); + if (Config.autoSwitchTheme) { + if (window.matchMedia?.("(prefers-color-scheme: dark)").matches) { + await set(Config.themeDark, true); + } else { + await set(Config.themeLight, true); + } + } else { + if (Config.customTheme) { + await set("custom"); + } else { + await set(Config.theme); + } + } + } + + // this is here to prevent calling set / preview multiple times during a full config loading + // once the full config is loaded, we can apply everything once + if (ignoreConfigEvent) return; + if (eventKey === "randomTheme") { void changeThemeList(); } @@ -430,23 +459,6 @@ ConfigEvent.subscribe(async (eventKey, eventValue, nosave) => { await clearPreview(false); await set(eventValue as string); } - if (eventKey === "setThemes") { - await clearRandom(); - await clearPreview(false); - if (Config.autoSwitchTheme) { - if (window.matchMedia?.("(prefers-color-scheme: dark)").matches) { - await set(Config.themeDark, true); - } else { - await set(Config.themeLight, true); - } - } else { - if (eventValue as boolean) { - await set("custom"); - } else { - await set(Config.theme); - } - } - } if (eventKey === "randomTheme" && eventValue === "off") await clearRandom(); if (eventKey === "customBackground") applyCustomBackground(); if (eventKey === "customBackgroundSize") applyCustomBackgroundSize(); diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index ec9ae73eb..c6174292b 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -930,7 +930,7 @@ export async function updateLbMemory( } } -export async function saveConfig(config: Config): Promise { +export async function saveConfig(config: Partial): Promise { if (isAuthenticated()) { const response = await Ape.configs.save({ body: config }); if (response.status !== 200) { diff --git a/frontend/src/ts/elements/keymap.ts b/frontend/src/ts/elements/keymap.ts index efb87d07e..0c8ca511c 100644 --- a/frontend/src/ts/elements/keymap.ts +++ b/frontend/src/ts/elements/keymap.ts @@ -595,8 +595,40 @@ ConfigEvent.subscribe((eventKey, newValue) => { void refresh(); } if (eventKey === "keymapMode") { + $(".activeKey").removeClass("activeKey"); + $(".keymapKey").attr("style", ""); newValue === "off" ? hide() : show(); } + if (eventKey === "keymapSize") { + $("#keymap").css("zoom", newValue as string); + } + if (eventKey === "keymapLegendStyle") { + let style = newValue as string; + + // Remove existing styles + const keymapLegendStyles = ["lowercase", "uppercase", "blank", "dynamic"]; + keymapLegendStyles.forEach((name) => { + $(".keymapLegendStyle").removeClass(name); + }); + + style = style || "lowercase"; + + // Mutate the keymap in the DOM, if it exists. + // 1. Remove everything + $(".keymapKey > .letter").css("display", ""); + $(".keymapKey > .letter").css("text-transform", ""); + + // 2. Append special styles onto the DOM elements + if (style === "uppercase") { + $(".keymapKey > .letter").css("text-transform", "capitalize"); + } + if (style === "blank") { + $(".keymapKey > .letter").css("display", "none"); + } + + // Update and save to cookie for persistence + $(".keymapLegendStyle").addClass(style); + } }); KeymapEvent.subscribe((mode, key, correct) => { diff --git a/frontend/src/ts/modals/mobile-test-config.ts b/frontend/src/ts/modals/mobile-test-config.ts index 049224d55..7ceac9dad 100644 --- a/frontend/src/ts/modals/mobile-test-config.ts +++ b/frontend/src/ts/modals/mobile-test-config.ts @@ -6,7 +6,10 @@ import * as CustomTestDurationPopup from "./custom-test-duration"; import * as QuoteSearchModal from "./quote-search"; import * as CustomTextPopup from "./custom-text"; import AnimatedModal from "../utils/animated-modal"; -import { QuoteLength } from "@monkeytype/contracts/schemas/configs"; +import { + QuoteLength, + QuoteLengthConfig, +} from "@monkeytype/contracts/schemas/configs"; import { Mode } from "@monkeytype/contracts/schemas/shared"; function update(): void { @@ -126,22 +129,25 @@ async function setup(modalEl: HTMLElement): Promise { for (const button of quoteGroupButtons) { button.addEventListener("click", (e) => { const target = e.currentTarget as HTMLElement; - const len = parseInt(target.getAttribute("data-quoteLength") ?? "0", 10); + const len = parseInt( + target.getAttribute("data-quoteLength") ?? "0", + 10 + ) as QuoteLength; if (len === -2) { void QuoteSearchModal.show({ modalChain: modal, }); } else { - let newVal: number[] | number = len; - if (len === -1) { - newVal = [0, 1, 2, 3]; + let arr: QuoteLengthConfig = []; + + if ((e as MouseEvent).shiftKey) { + arr = [...Config.quoteLength, len]; + } else { + arr = [len]; } - UpdateConfig.setQuoteLength( - newVal as QuoteLength | QuoteLength[], - false, - (e as MouseEvent).shiftKey - ); + + UpdateConfig.setQuoteLength(arr, false); ManualRestart.set(); TestLogic.restart(); } diff --git a/frontend/src/ts/modals/quote-search.ts b/frontend/src/ts/modals/quote-search.ts index 8267e6044..04880f0b6 100644 --- a/frontend/src/ts/modals/quote-search.ts +++ b/frontend/src/ts/modals/quote-search.ts @@ -21,7 +21,6 @@ import * as TestState from "../test/test-state"; import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; import * as TestLogic from "../test/test-logic"; import { createErrorMessage } from "../utils/misc"; -import { QuoteLength } from "@monkeytype/contracts/schemas/configs"; const searchServiceCache: Record> = {}; @@ -326,7 +325,7 @@ function apply(val: number): void { ); } if (val !== null && !isNaN(val) && val >= 0) { - UpdateConfig.setQuoteLength(-2 as QuoteLength, false); + UpdateConfig.setQuoteLength([-2], false); TestState.setSelectedQuoteId(val); ManualRestart.set(); } else { diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts index 66f40a4b8..a4c05b585 100644 --- a/frontend/src/ts/pages/account.ts +++ b/frontend/src/ts/pages/account.ts @@ -39,6 +39,7 @@ import { findLineByLeastSquares } from "../utils/numbers"; import defaultResultFilters from "../constants/default-result-filters"; import { SnapshotResult } from "../constants/default-snapshot"; import Ape from "../ape"; +import { AccountChart } from "@monkeytype/contracts/schemas/configs"; let filterDebug = false; //toggle filterdebug @@ -1122,25 +1123,25 @@ function sortAndRefreshHistory( } $(".pageAccount button.toggleResultsOnChart").on("click", () => { - const newValue = Config.accountChart; + const newValue = [...Config.accountChart] as AccountChart; newValue[0] = newValue[0] === "on" ? "off" : "on"; UpdateConfig.setAccountChart(newValue); }); $(".pageAccount button.toggleAccuracyOnChart").on("click", () => { - const newValue = Config.accountChart; + const newValue = [...Config.accountChart] as AccountChart; newValue[1] = newValue[1] === "on" ? "off" : "on"; UpdateConfig.setAccountChart(newValue); }); $(".pageAccount button.toggleAverage10OnChart").on("click", () => { - const newValue = Config.accountChart; + const newValue = [...Config.accountChart] as AccountChart; newValue[2] = newValue[2] === "on" ? "off" : "on"; UpdateConfig.setAccountChart(newValue); }); $(".pageAccount button.toggleAverage100OnChart").on("click", () => { - const newValue = Config.accountChart; + const newValue = [...Config.accountChart] as AccountChart; newValue[3] = newValue[3] === "on" ? "off" : "on"; UpdateConfig.setAccountChart(newValue); }); diff --git a/frontend/src/ts/test/caret.ts b/frontend/src/ts/test/caret.ts index 200a2d1fb..89b3a008e 100644 --- a/frontend/src/ts/test/caret.ts +++ b/frontend/src/ts/test/caret.ts @@ -270,11 +270,26 @@ export async function updatePosition(noAnim = false): Promise { } } +function updateStyle(): void { + caret.style.width = ""; + caret.classList.remove( + ...["off", "default", "underline", "outline", "block", "carrot", "banana"] + ); + caret.classList.add(Config.caretStyle); +} + subscribe((eventKey) => { if (eventKey === "caretStyle") { - caret.style.width = ""; + updateStyle(); void updatePosition(true); } + if (eventKey === "smoothCaret") { + if (Config.smoothCaret === "off") { + caret.style.animationName = "caretFlashHard"; + } else { + caret.style.animationName = "caretFlashSmooth"; + } + } }); export function show(noAnim = false): void { diff --git a/frontend/src/ts/test/pace-caret.ts b/frontend/src/ts/test/pace-caret.ts index bff3c9988..a57ca1867 100644 --- a/frontend/src/ts/test/pace-caret.ts +++ b/frontend/src/ts/test/pace-caret.ts @@ -322,6 +322,21 @@ export function start(): void { void update(performance.now() + (settings?.spc ?? 0) * 1000); } +function updateStyle(): void { + const paceCaret = $("#paceCaret"); + paceCaret.removeClass([ + "off", + "default", + "underline", + "outline", + "block", + "carrot", + "banana", + ]); + paceCaret.addClass(Config.paceCaretStyle); +} + ConfigEvent.subscribe((eventKey) => { if (eventKey === "paceCaret") void init(); + if (eventKey === "paceCaretStyle") updateStyle(); }); diff --git a/frontend/src/ts/test/test-config.ts b/frontend/src/ts/test/test-config.ts index 97216774b..3f5c2950e 100644 --- a/frontend/src/ts/test/test-config.ts +++ b/frontend/src/ts/test/test-config.ts @@ -7,6 +7,7 @@ import Config from "../config"; import * as ConfigEvent from "../observables/config-event"; import * as ActivePage from "../states/active-page"; import { applyReducedMotion } from "../utils/misc"; +import { areUnsortedArraysEqual } from "../utils/arrays"; export function show(): void { $("#testConfig").removeClass("invisible"); @@ -225,11 +226,18 @@ export function updateExtras(key: string, value: ConfigValue): void { ).addClass("active"); } else if (key === "quoteLength") { $("#testConfig .quoteLength .textButton").removeClass("active"); - (value as QuoteLength[]).forEach((ql) => { - $( - "#testConfig .quoteLength .textButton[quoteLength='" + ql + "']" - ).addClass("active"); - }); + + if (areUnsortedArraysEqual(value as QuoteLength[], [0, 1, 2, 3])) { + $("#testConfig .quoteLength .textButton[quoteLength='-1']").addClass( + "active" + ); + } else { + (value as QuoteLength[]).forEach((ql) => { + $( + "#testConfig .quoteLength .textButton[quoteLength='" + ql + "']" + ).addClass("active"); + }); + } } else if (key === "numbers") { if (value === false) { $("#testConfig .numbersMode.textButton").removeClass("active"); diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 3c810f2c1..7a7ec4c67 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -58,7 +58,10 @@ import * as KeymapEvent from "../observables/keymap-event"; import * as LayoutfluidFunboxTimer from "../test/funbox/layoutfluid-funbox-timer"; import * as ArabicLazyMode from "../states/arabic-lazy-mode"; import Format from "../utils/format"; -import { QuoteLength } from "@monkeytype/contracts/schemas/configs"; +import { + QuoteLength, + QuoteLengthConfig, +} from "@monkeytype/contracts/schemas/configs"; import { Mode } from "@monkeytype/contracts/schemas/shared"; import { CompletedEvent, @@ -461,7 +464,7 @@ export async function init(): Promise { if (Config.mode === "quote") { if (Config.quoteLength.includes(-3) && !isAuthenticated()) { - UpdateConfig.setQuoteLength(-1); + UpdateConfig.setQuoteLengthAll(); } } @@ -1442,14 +1445,20 @@ $(".pageTest").on("click", "#testConfig .time .textButton", (e) => { $(".pageTest").on("click", "#testConfig .quoteLength .textButton", (e) => { if (TestUI.testRestarting) return; - let len: QuoteLength | QuoteLength[] = parseInt( + const len = parseInt( $(e.currentTarget).attr("quoteLength") ?? "1" ) as QuoteLength; + if (len !== -2) { - if (len === -1) { - len = [0, 1, 2, 3]; + let arr: QuoteLengthConfig = []; + + if (e.shiftKey) { + arr = [...Config.quoteLength, len]; + } else { + arr = [len]; } - if (UpdateConfig.setQuoteLength(len, false, e.shiftKey)) { + + if (UpdateConfig.setQuoteLength(arr, false)) { ManualRestart.set(); restart(); } diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 55bac876c..081022115 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -1756,4 +1756,7 @@ ConfigEvent.subscribe((key, value) => { if (key === "timerColor") { updateLiveStatsColor(value as TimerColor); } + if (key === "showOutOfFocusWarning" && value === false) { + OutOfFocus.hide(); + } }); diff --git a/frontend/src/ts/test/words-generator.ts b/frontend/src/ts/test/words-generator.ts index a2af3e80d..b3731697f 100644 --- a/frontend/src/ts/test/words-generator.ts +++ b/frontend/src/ts/test/words-generator.ts @@ -525,7 +525,7 @@ async function getQuoteWordList( TestState.selectedQuoteId ); if (targetQuote === undefined) { - UpdateConfig.setQuoteLength(-1); + UpdateConfig.setQuoteLengthAll(); throw new WordGenError( `Quote ${TestState.selectedQuoteId} does not exist` ); @@ -536,14 +536,14 @@ async function getQuoteWordList( Config.language ); if (randomQuote === null) { - UpdateConfig.setQuoteLength(-1); + UpdateConfig.setQuoteLengthAll(); throw new WordGenError("No favorite quotes found"); } rq = randomQuote; } else { const randomQuote = QuotesController.getRandomQuote(); if (randomQuote === null) { - UpdateConfig.setQuoteLength(-1); + UpdateConfig.setQuoteLengthAll(); throw new WordGenError("No quotes found for selected quote length"); } rq = randomQuote; diff --git a/frontend/src/ts/ui.ts b/frontend/src/ts/ui.ts index a593fc15c..9b2f1c564 100644 --- a/frontend/src/ts/ui.ts +++ b/frontend/src/ts/ui.ts @@ -117,6 +117,28 @@ $(window).on("resize", () => { debouncedEvent(); }); -ConfigEvent.subscribe((eventKey) => { +ConfigEvent.subscribe((eventKey, value) => { if (eventKey === "quickRestart") updateKeytips(); + if (eventKey === "showKeyTips") { + if (Config.showKeyTips) { + $("footer .keyTips").removeClass("hidden"); + } else { + $("footer .keyTips").addClass("hidden"); + } + } + if (eventKey === "fontSize") { + $("#caret, #paceCaret, #liveStatsMini, #typingTest, #wordsInput").css( + "fontSize", + value + "rem" + ); + } + if (eventKey === "fontFamily") { + document.documentElement.style.setProperty( + "--font", + `"${(value as string).replace( + /_/g, + " " + )}", "Roboto Mono", "Vazirmatn", monospace` + ); + } }); diff --git a/frontend/src/ts/utils/config.ts b/frontend/src/ts/utils/config.ts index 0ad6a97ed..9f805c3c2 100644 --- a/frontend/src/ts/utils/config.ts +++ b/frontend/src/ts/utils/config.ts @@ -156,5 +156,47 @@ export function replaceLegacyValues( configObj.fontSize = newValue; } + if ( + Array.isArray(configObj.accountChart) && + configObj.accountChart.length !== 4 + ) { + configObj.accountChart = ["on", "on", "on", "on"]; + } + + if ( + typeof configObj.minAccCustom === "number" && + configObj.minAccCustom > 100 + ) { + configObj.minAccCustom = 100; + } + + if ( + Array.isArray(configObj.customThemeColors) && + //@ts-expect-error legacy configs + configObj.customThemeColors.length === 9 + ) { + // migrate existing configs missing sub alt color + const colors = configObj.customThemeColors; + colors.splice(4, 0, "#000000"); + configObj.customThemeColors = colors; + } + + if ( + Array.isArray(configObj.customBackgroundFilter) && + //@ts-expect-error legacy configs + configObj.customBackgroundFilter.length === 5 + ) { + const arr = configObj.customBackgroundFilter; + configObj.customBackgroundFilter = [arr[0], arr[1], arr[2], arr[3]]; + } + + if (typeof configObj.quoteLength === "number") { + if (configObj.quoteLength === -1) { + configObj.quoteLength = [0, 1, 2, 3]; + } else { + configObj.quoteLength = [configObj.quoteLength]; + } + } + return configObj; } diff --git a/frontend/src/ts/utils/url-handler.ts b/frontend/src/ts/utils/url-handler.ts index 034d4e0d4..5c2b4c31f 100644 --- a/frontend/src/ts/utils/url-handler.ts +++ b/frontend/src/ts/utils/url-handler.ts @@ -188,7 +188,7 @@ export function loadTestSettingsFromUrl(getOverride?: string): void { } else if (mode === "words") { UpdateConfig.setWordCount(parseInt(de[1], 10), true); } else if (mode === "quote") { - UpdateConfig.setQuoteLength(-2, false); + UpdateConfig.setQuoteLength([-2], false); TestState.setSelectedQuoteId(parseInt(de[1], 10)); ManualRestart.set(); } diff --git a/packages/contracts/src/schemas/configs.ts b/packages/contracts/src/schemas/configs.ts index 08fda8157..932482d98 100644 --- a/packages/contracts/src/schemas/configs.ts +++ b/packages/contracts/src/schemas/configs.ts @@ -13,7 +13,6 @@ export type QuickRestart = z.infer; export const QuoteLengthSchema = z.union([ z.literal(-3), z.literal(-2), - z.literal(-1), z.literal(0), z.literal(1), z.literal(2), @@ -21,7 +20,19 @@ export const QuoteLengthSchema = z.union([ ]); export type QuoteLength = z.infer; -export const QuoteLengthConfigSchema = z.array(QuoteLengthSchema); +export const QuoteLengthConfigSchema = z + .array(QuoteLengthSchema) + .describe( + [ + "|value|description|\n|-|-|", + "|-3|Favorite quotes|", + "|-2|Quote search|", + "|0|Short quotes|", + "|1|Medium quotes|", + "|2|Long quotes|", + "|3|Thicc quotes|", + ].join("\n") + ); export type QuoteLengthConfig = z.infer; export const CaretStyleSchema = z.enum([ @@ -346,18 +357,7 @@ export type CustomBackground = z.infer; export const ConfigSchema = z .object({ - theme: ThemeNameSchema, - themeLight: ThemeNameSchema, - themeDark: ThemeNameSchema, - autoSwitchTheme: z.boolean(), - customTheme: z.boolean(), - //customThemeId: token().nonnegative().max(24), - customThemeColors: CustomThemeColorsSchema, - favThemes: FavThemesSchema, - showKeyTips: z.boolean(), - smoothCaret: SmoothCaretSchema, - codeUnindentOnBackspace: z.boolean(), - quickRestart: QuickRestartSchema, + // test punctuation: z.boolean(), numbers: z.boolean(), words: WordCountSchema, @@ -365,76 +365,105 @@ export const ConfigSchema = z mode: Shared.ModeSchema, quoteLength: QuoteLengthConfigSchema, language: LanguageSchema, - fontSize: FontSizeSchema, - freedomMode: z.boolean(), + burstHeatmap: z.boolean(), + + // behavior difficulty: DifficultySchema, + quickRestart: QuickRestartSchema, + repeatQuotes: RepeatQuotesSchema, blindMode: z.boolean(), - quickEnd: z.boolean(), - caretStyle: CaretStyleSchema, - paceCaretStyle: CaretStyleSchema, - flipTestColors: z.boolean(), - layout: LayoutSchema, + alwaysShowWordsHistory: z.boolean(), + singleListCommandLine: SingleListCommandLineSchema, + minWpm: MinimumWordsPerMinuteSchema, + minWpmCustomSpeed: MinWpmCustomSpeedSchema, + minAcc: MinimumAccuracySchema, + minAccCustom: MinimumAccuracyCustomSchema, + minBurst: MinimumBurstSchema, + minBurstCustomSpeed: MinimumBurstCustomSpeedSchema, + britishEnglish: z.boolean(), funbox: FunboxSchema, + customLayoutfluid: CustomLayoutFluidSchema, + customPolyglot: CustomPolyglotSchema, + + // input + freedomMode: z.boolean(), + strictSpace: z.boolean(), + oppositeShiftMode: OppositeShiftModeSchema, + stopOnError: StopOnErrorSchema, confidenceMode: ConfidenceModeSchema, + quickEnd: z.boolean(), indicateTypos: IndicateTyposSchema, + hideExtraLetters: z.boolean(), + lazyMode: z.boolean(), + layout: LayoutSchema, + codeUnindentOnBackspace: z.boolean(), + + // sound + soundVolume: SoundVolumeSchema, + playSoundOnClick: PlaySoundOnClickSchema, + playSoundOnError: PlaySoundOnErrorSchema, + + // caret + smoothCaret: SmoothCaretSchema, + caretStyle: CaretStyleSchema, + paceCaret: PaceCaretSchema, + paceCaretCustomSpeed: PaceCaretCustomSpeedSchema, + paceCaretStyle: CaretStyleSchema, + repeatedPace: z.boolean(), + + // appearance timerStyle: TimerStyleSchema, liveSpeedStyle: LiveSpeedAccBurstStyleSchema, liveAccStyle: LiveSpeedAccBurstStyleSchema, liveBurstStyle: LiveSpeedAccBurstStyleSchema, - colorfulMode: z.boolean(), - randomTheme: RandomThemeSchema, timerColor: TimerColorSchema, timerOpacity: TimerOpacitySchema, - stopOnError: StopOnErrorSchema, - showAllLines: z.boolean(), - keymapMode: KeymapModeSchema, - keymapStyle: KeymapStyleSchema, - keymapLegendStyle: KeymapLegendStyleSchema, - keymapLayout: KeymapLayoutSchema, - keymapShowTopRow: KeymapShowTopRowSchema, - keymapSize: KeymapSizeSchema, - fontFamily: FontFamilySchema, - smoothLineScroll: z.boolean(), - alwaysShowDecimalPlaces: z.boolean(), - alwaysShowWordsHistory: z.boolean(), - singleListCommandLine: SingleListCommandLineSchema, - capsLockWarning: z.boolean(), - playSoundOnError: PlaySoundOnErrorSchema, - playSoundOnClick: PlaySoundOnClickSchema, - soundVolume: SoundVolumeSchema, - startGraphsAtZero: z.boolean(), - showOutOfFocusWarning: z.boolean(), - paceCaret: PaceCaretSchema, - paceCaretCustomSpeed: PaceCaretCustomSpeedSchema, - repeatedPace: z.boolean(), - accountChart: AccountChartSchema, - minWpm: MinimumWordsPerMinuteSchema, - minWpmCustomSpeed: MinWpmCustomSpeedSchema, highlightMode: HighlightModeSchema, tapeMode: TapeModeSchema, tapeMargin: TapeMarginSchema, + smoothLineScroll: z.boolean(), + showAllLines: z.boolean(), + alwaysShowDecimalPlaces: z.boolean(), typingSpeedUnit: TypingSpeedUnitSchema, - ads: AdsSchema, - hideExtraLetters: z.boolean(), - strictSpace: z.boolean(), - minAcc: MinimumAccuracySchema, - minAccCustom: MinimumAccuracyCustomSchema, - monkey: z.boolean(), - repeatQuotes: RepeatQuotesSchema, - oppositeShiftMode: OppositeShiftModeSchema, + startGraphsAtZero: z.boolean(), + maxLineWidth: MaxLineWidthSchema, + fontSize: FontSizeSchema, + fontFamily: FontFamilySchema, + keymapMode: KeymapModeSchema, + keymapLayout: KeymapLayoutSchema, + keymapStyle: KeymapStyleSchema, + keymapLegendStyle: KeymapLegendStyleSchema, + keymapShowTopRow: KeymapShowTopRowSchema, + keymapSize: KeymapSizeSchema, + + // theme + flipTestColors: z.boolean(), + colorfulMode: z.boolean(), customBackground: CustomBackgroundSchema, customBackgroundSize: CustomBackgroundSizeSchema, customBackgroundFilter: CustomBackgroundFilterSchema, - customLayoutfluid: CustomLayoutFluidSchema, - monkeyPowerLevel: MonkeyPowerLevelSchema, - minBurst: MinimumBurstSchema, - minBurstCustomSpeed: MinimumBurstCustomSpeedSchema, - burstHeatmap: z.boolean(), - britishEnglish: z.boolean(), - lazyMode: z.boolean(), + autoSwitchTheme: z.boolean(), + themeLight: ThemeNameSchema, + themeDark: ThemeNameSchema, + randomTheme: RandomThemeSchema, + favThemes: FavThemesSchema, + theme: ThemeNameSchema, + customTheme: z.boolean(), + customThemeColors: CustomThemeColorsSchema, + + // hide elements + showKeyTips: z.boolean(), + showOutOfFocusWarning: z.boolean(), + capsLockWarning: z.boolean(), showAverage: ShowAverageSchema, - maxLineWidth: MaxLineWidthSchema, - customPolyglot: CustomPolyglotSchema, + + // other (hidden) + accountChart: AccountChartSchema, + monkey: z.boolean(), + monkeyPowerLevel: MonkeyPowerLevelSchema, + + // ads + ads: AdsSchema, } satisfies Record) .strict(); @@ -455,24 +484,14 @@ export const ConfigGroupNameSchema = z.enum([ "appearance", "theme", "hideElements", - "ads", "hidden", + "ads", ]); export type ConfigGroupName = z.infer; export const ConfigGroupsLiteral = { - theme: "theme", - themeLight: "theme", - themeDark: "theme", - autoSwitchTheme: "theme", - customTheme: "theme", - customThemeColors: "theme", - favThemes: "theme", - showKeyTips: "hideElements", - smoothCaret: "caret", - codeUnindentOnBackspace: "input", - quickRestart: "behavior", + //test punctuation: "test", numbers: "test", words: "test", @@ -480,76 +499,105 @@ export const ConfigGroupsLiteral = { mode: "test", quoteLength: "test", language: "test", - fontSize: "appearance", - freedomMode: "input", + burstHeatmap: "test", + + //behavior difficulty: "behavior", + quickRestart: "behavior", + repeatQuotes: "behavior", blindMode: "behavior", - quickEnd: "input", - caretStyle: "caret", - paceCaretStyle: "caret", - flipTestColors: "theme", - layout: "input", - funbox: "behavior", + alwaysShowWordsHistory: "behavior", + singleListCommandLine: "behavior", + minWpm: "behavior", + minWpmCustomSpeed: "behavior", + minAcc: "behavior", + minAccCustom: "behavior", + minBurst: "behavior", + minBurstCustomSpeed: "behavior", + britishEnglish: "behavior", + funbox: "behavior", //todo: maybe move to test? + customLayoutfluid: "behavior", + customPolyglot: "behavior", + + //input + freedomMode: "input", + strictSpace: "input", + oppositeShiftMode: "input", + stopOnError: "input", confidenceMode: "input", + quickEnd: "input", indicateTypos: "input", + hideExtraLetters: "input", + lazyMode: "input", + layout: "input", + codeUnindentOnBackspace: "input", + + //sound + soundVolume: "sound", + playSoundOnClick: "sound", + playSoundOnError: "sound", + + //caret + smoothCaret: "caret", + caretStyle: "caret", + paceCaret: "caret", + paceCaretCustomSpeed: "caret", + paceCaretStyle: "caret", + repeatedPace: "caret", + + //appearance timerStyle: "appearance", liveSpeedStyle: "appearance", liveAccStyle: "appearance", liveBurstStyle: "appearance", - colorfulMode: "theme", - randomTheme: "theme", timerColor: "appearance", timerOpacity: "appearance", - stopOnError: "input", - showAllLines: "appearance", - keymapMode: "appearance", - keymapStyle: "appearance", - keymapLegendStyle: "appearance", - keymapLayout: "appearance", - keymapShowTopRow: "appearance", - keymapSize: "appearance", - fontFamily: "appearance", - smoothLineScroll: "appearance", - alwaysShowDecimalPlaces: "appearance", - alwaysShowWordsHistory: "behavior", - singleListCommandLine: "behavior", - capsLockWarning: "hideElements", - playSoundOnError: "sound", - playSoundOnClick: "sound", - soundVolume: "sound", - startGraphsAtZero: "appearance", - showOutOfFocusWarning: "hideElements", - paceCaret: "caret", - paceCaretCustomSpeed: "caret", - repeatedPace: "caret", - accountChart: "hidden", - minWpm: "behavior", - minWpmCustomSpeed: "behavior", highlightMode: "appearance", tapeMode: "appearance", tapeMargin: "appearance", + smoothLineScroll: "appearance", + showAllLines: "appearance", + alwaysShowDecimalPlaces: "appearance", typingSpeedUnit: "appearance", - ads: "ads", - hideExtraLetters: "input", - strictSpace: "input", - minAcc: "behavior", - minAccCustom: "behavior", - monkey: "hidden", - repeatQuotes: "behavior", - oppositeShiftMode: "input", + startGraphsAtZero: "appearance", + maxLineWidth: "appearance", + fontSize: "appearance", + fontFamily: "appearance", + keymapMode: "appearance", + keymapLayout: "appearance", + keymapStyle: "appearance", + keymapLegendStyle: "appearance", + keymapShowTopRow: "appearance", + keymapSize: "appearance", + + //theme + flipTestColors: "theme", + colorfulMode: "theme", customBackground: "theme", customBackgroundSize: "theme", customBackgroundFilter: "theme", - customLayoutfluid: "behavior", - monkeyPowerLevel: "hidden", - minBurst: "behavior", - minBurstCustomSpeed: "behavior", - burstHeatmap: "test", - britishEnglish: "behavior", - lazyMode: "input", + autoSwitchTheme: "theme", + themeLight: "theme", + themeDark: "theme", + randomTheme: "theme", + favThemes: "theme", + theme: "theme", + customTheme: "theme", + customThemeColors: "theme", + + //hide elements + showKeyTips: "hideElements", + showOutOfFocusWarning: "hideElements", + capsLockWarning: "hideElements", showAverage: "hideElements", - maxLineWidth: "appearance", - customPolyglot: "behavior", + + //other + accountChart: "hidden", + monkey: "hidden", + monkeyPowerLevel: "hidden", + + //ads + ads: "ads", } as const satisfies Record; export type ConfigGroups = typeof ConfigGroupsLiteral;