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;