diff --git a/.github/workflows/monkey-ci.yml b/.github/workflows/monkey-ci.yml index 7c4f7684df9a..b19099926350 100644 --- a/.github/workflows/monkey-ci.yml +++ b/.github/workflows/monkey-ci.yml @@ -242,6 +242,7 @@ jobs: - 'frontend/static/themes/**' - 'frontend/static/webfonts/**' - 'frontend/static/challenges/**' + - 'frontend/static/sounds/**' - name: Set up Node.js uses: actions/setup-node@v4 diff --git a/frontend/__tests__/controllers/preset-controller.spec.ts b/frontend/__tests__/controllers/preset-controller.spec.ts index 816e9edab7e3..ed1ed72e3ab4 100644 --- a/frontend/__tests__/controllers/preset-controller.spec.ts +++ b/frontend/__tests__/controllers/preset-controller.spec.ts @@ -10,6 +10,7 @@ import * as Persistence from "../../src/ts/config/persistence"; import * as Notifications from "../../src/ts/states/notifications"; import * as TestLogic from "../../src/ts/test/test-logic"; import * as Tags from "../../src/ts/collections/tags"; +import * as Presets from "../../src/ts/collections/presets"; describe("PresetController", () => { describe("apply", () => { @@ -20,6 +21,7 @@ describe("PresetController", () => { // })); const dbGetSnapshotMock = vi.spyOn(DB, "getSnapshot"); + const getPresetMock = vi.spyOn(Presets.__nonReactive, "getPreset"); const configApplyMock = vi.spyOn(Lifecycle, "applyConfig"); const configSaveFullConfigMock = vi.spyOn( Persistence, @@ -41,6 +43,7 @@ describe("PresetController", () => { beforeEach(() => { [ dbGetSnapshotMock, + getPresetMock, configApplyMock, configSaveFullConfigMock, configGetConfigChangesMock, @@ -51,6 +54,7 @@ describe("PresetController", () => { tagsSaveActiveMock, ].forEach((it) => it.mockClear()); + dbGetSnapshotMock.mockReturnValue({} as any); configApplyMock.mockResolvedValue(); }); @@ -88,6 +92,9 @@ describe("PresetController", () => { }); it("should ignore unknown preset", async () => { + //GIVEN + getPresetMock.mockReturnValue(undefined); + //WHEN await PresetController.apply("unknown"); //THEN @@ -143,7 +150,8 @@ describe("PresetController", () => { _id: "1", ...partialPreset, } as any; - dbGetSnapshotMock.mockReturnValue({ presets: [preset] } as any); + dbGetSnapshotMock.mockReturnValue({} as any); + getPresetMock.mockReturnValue(preset as any); return preset; }; }); diff --git a/frontend/scripts/check-assets.ts b/frontend/scripts/check-assets.ts index 80f83819b7b5..f1246caa68fd 100644 --- a/frontend/scripts/check-assets.ts +++ b/frontend/scripts/check-assets.ts @@ -21,6 +21,7 @@ import { z } from "zod"; import { ChallengeSchema, Challenge } from "@monkeytype/schemas/challenges"; import { LayoutObject, LayoutObjectSchema } from "@monkeytype/schemas/layouts"; import { QuoteDataSchema, QuoteData } from "@monkeytype/schemas/quotes"; +import { clickSoundConfig } from "../src/ts/constants/sounds"; class Problems { private type: string; @@ -421,6 +422,54 @@ async function validateThemes(): Promise { } } +async function validateSounds(): Promise { + const problems = new Problems("Sounds", { + _additional: + "Sound files present but missing in frontend/src/ts/constants/sounds", + }); + + const soundFiles = new Set( + fs + .readdirSync("./static/sounds") + .filter((it) => it.startsWith("click")) + .flatMap((folder) => + fs + .readdirSync(`./static/sounds/${folder}`) + .map((it) => `${folder}/${it}`), + ), + ); + + //missing sound files + + Object.entries(clickSoundConfig).forEach(([key, value]) => { + value + .map((file) => file.substring("../sounds/".length)) + .filter((it) => !soundFiles.has(it)) + .forEach((file) => + problems.add( + "click" + key, + `missing file frontend/static/sounds/${file}`, + ), + ); + }); + + //additional files + const expectedSoundFiles = new Set( + Object.values(clickSoundConfig).flatMap((it) => + it.map((file) => file.substring("../sounds/".length)), + ), + ); + [...soundFiles] + .filter((name) => !expectedSoundFiles.has(name)) + .forEach((file) => problems.add("_additional", file)); + + console.log(problems.toString()); + + if (problems.hasError()) { + throw new Error("sounds with errors"); + } +} + type Validator = () => Promise; async function main(): Promise { @@ -436,11 +485,13 @@ async function main(): Promise { challenges: [validateChallenges], fonts: [validateFonts], themes: [validateThemes], + sounds: [validateSounds], others: [ validateChallenges, validateLayouts, validateFonts, validateThemes, + validateSounds, ], }; diff --git a/frontend/src/ts/collections/presets.ts b/frontend/src/ts/collections/presets.ts new file mode 100644 index 000000000000..a06a7318895b --- /dev/null +++ b/frontend/src/ts/collections/presets.ts @@ -0,0 +1,205 @@ +import { Preset } from "@monkeytype/schemas/presets"; +import { queryCollectionOptions } from "@tanstack/query-db-collection"; +import { + createCollection, + createOptimisticAction, + useLiveQuery, +} from "@tanstack/solid-db"; +import Ape from "../ape"; +import { queryClient } from "../queries"; +import { baseKey } from "../queries/utils/keys"; +import { ConfigGroupName } from "@monkeytype/schemas/configs"; +import { tempId } from "./utils/misc"; + +export type PresetItem = Preset; + +const queryKeys = { + root: () => [...baseKey("presets", { isUserSpecific: true })], +}; + +// oxlint-disable-next-line typescript/explicit-function-return-type +export function usePresetsLiveQuery() { + return useLiveQuery((q) => { + return q + .from({ preset: presetsCollection }) + .orderBy(({ preset }) => preset.name, "asc"); + }); +} + +const presetsCollection = createCollection( + queryCollectionOptions({ + staleTime: Infinity, + startSync: true, + queryKey: queryKeys.root(), + + queryClient, + getKey: (it) => it._id, + queryFn: async () => { + return [] as PresetItem[]; + }, + }), +); + +type ActionType = { + addPreset: { + name: string; + config: Preset["config"]; + settingGroups: ConfigGroupName[] | undefined; + }; + editPreset: { + presetId: string; + name: string; + config?: Preset["config"]; + settingGroups?: ConfigGroupName[] | null; + }; + deletePreset: { + presetId: string; + }; +}; + +const actions = { + addPreset: createOptimisticAction({ + onMutate: ({ name, config, settingGroups }) => { + presetsCollection.insert({ + _id: tempId(), + name: name.replace(/_/g, " "), + config, + settingGroups, + }); + }, + mutationFn: async ({ name, config, settingGroups }) => { + const response = await Ape.presets.add({ + body: { + name: name.replace(/ /g, "_"), + config, + ...(settingGroups !== undefined && { settingGroups }), + }, + }); + if (response.status !== 200) { + throw new Error(`Failed to add preset: ${response.body.message}`); + } + + const newPreset = { + _id: response.body.data.presetId, + name: name.replace(/_/g, " "), + config, + settingGroups, + }; + + presetsCollection.utils.writeInsert(newPreset); + }, + }), + editPreset: createOptimisticAction({ + onMutate: ({ presetId, name, config, settingGroups }) => { + presetsCollection.update(presetId, (preset) => { + preset.name = name.replace(/_/g, " "); + + if (config !== undefined) { + preset.config = config; + } + if (settingGroups !== undefined) { + preset.settingGroups = settingGroups; + } + }); + }, + mutationFn: async ({ presetId, name, config, settingGroups }) => { + const existing = presetsCollection.get(presetId); + + if (existing === undefined) { + throw new Error("Preset not found"); + } + + const response = await Ape.presets.save({ + body: { + _id: presetId, + name: name.replace(/ /g, "_"), + ...(config !== undefined && { + config: config, + settingGroups: settingGroups, + }), + }, + }); + if (response.status !== 200) { + throw new Error(`Failed to edit preset: ${response.body.message}`); + } + + // if this is missing getPreset is out of sync + presetsCollection.utils.writeUpdate({ + _id: presetId, + name: name.replace(/_/g, " "), + ...(config !== undefined && { config }), + ...(settingGroups !== undefined && { settingGroups }), + }); + }, + }), + deletePreset: createOptimisticAction({ + onMutate: ({ presetId }) => { + presetsCollection.delete(presetId); + }, + mutationFn: async ({ presetId }) => { + const response = await Ape.presets.delete({ + params: { presetId }, + }); + if (response.status !== 200) { + throw new Error(`Failed to delete preset: ${response.body.message}`); + } + presetsCollection.utils.writeDelete(presetId); + }, + }), +}; + +// --- Public API --- + +function getPresets(): PresetItem[] { + return [...presetsCollection.values()].sort((a, b) => + a.name.localeCompare(b.name), + ); +} + +function getPreset(id: string): PresetItem | undefined { + return presetsCollection.get(id); +} + +export function fillPresetsCollection(presets: Preset[]): void { + const presetItems = presets.map((preset) => ({ + _id: preset._id, + name: preset.name.replace(/_/g, " "), + config: preset.config, + settingGroups: preset.settingGroups, + })); + + presetsCollection.utils.writeBatch(() => { + presetItems.forEach((item) => { + presetsCollection.utils.writeInsert(item); + }); + }); +} + +export async function addPreset( + params: ActionType["addPreset"], +): Promise { + const transaction = actions.addPreset(params); + await transaction.isPersisted.promise; +} + +export async function editPreset( + params: ActionType["editPreset"], +): Promise { + const transaction = actions.editPreset(params); + await transaction.isPersisted.promise; +} + +export async function deletePreset( + params: ActionType["deletePreset"], +): Promise { + const transaction = actions.deletePreset(params); + await transaction.isPersisted.promise; +} + +/** + * Used for non reactive access. Do not use in Solid components. + */ +export const __nonReactive = { + getPresets, + getPreset, +}; diff --git a/frontend/src/ts/commandline/lists/presets.ts b/frontend/src/ts/commandline/lists/presets.ts index 8720745f0dfe..5a6f5c4b870b 100644 --- a/frontend/src/ts/commandline/lists/presets.ts +++ b/frontend/src/ts/commandline/lists/presets.ts @@ -1,10 +1,10 @@ -import * as DB from "../../db"; import * as ModesNotice from "../../elements/modes-notice"; import * as Settings from "../../pages/settings"; import * as PresetController from "../../controllers/preset-controller"; import * as EditPresetPopup from "../../modals/edit-preset"; import { isAuthenticated } from "../../states/core"; import { Command, CommandsSubgroup } from "../types"; +import { __nonReactive } from "../../collections/presets"; const subgroup: CommandsSubgroup = { title: "Presets...", @@ -28,15 +28,13 @@ const commands: Command[] = [ ]; function update(): void { - const snapshot = DB.getSnapshot(); + const presets = __nonReactive.getPresets(); subgroup.list = []; - if (!snapshot?.presets || snapshot.presets.length === 0) return; - snapshot.presets.forEach((preset) => { - const dis = preset.display; - + if (presets.length === 0) return; + presets.forEach((preset) => { subgroup.list.push({ id: "applyPreset" + preset._id, - display: dis, + display: preset.name, exec: async (): Promise => { Settings.setEventDisabled(true); await PresetController.apply(preset._id); diff --git a/frontend/src/ts/constants/default-snapshot.ts b/frontend/src/ts/constants/default-snapshot.ts index 7ab69aeaa7ab..17ca8074d841 100644 --- a/frontend/src/ts/constants/default-snapshot.ts +++ b/frontend/src/ts/constants/default-snapshot.ts @@ -7,7 +7,6 @@ import { ModifiableTestActivityCalendar, TestActivityCalendar, } from "../elements/test-activity-calendar"; -import { Preset } from "@monkeytype/schemas/presets"; import { Language } from "@monkeytype/schemas/languages"; import { ConnectionStatus } from "@monkeytype/schemas/connections"; @@ -69,17 +68,12 @@ export type Snapshot = Omit< maxStreak: number; isPremium: boolean; streakHourOffset?: number; - presets: SnapshotPreset[]; xp: number; testActivity?: ModifiableTestActivityCalendar; testActivityByYear?: { [key: string]: TestActivityCalendar }; connections: Record; }; -export type SnapshotPreset = Preset & { - display: string; -}; - const defaultSnap = { personalBests: { time: {}, @@ -94,7 +88,6 @@ const defaultSnap = { isPremium: false, config: getDefaultConfig(), customThemes: [], - presets: [], banned: undefined, verified: undefined, lbMemory: { time: { 15: { english: 0 }, 60: { english: 0 } } }, diff --git a/frontend/src/ts/constants/sounds.ts b/frontend/src/ts/constants/sounds.ts new file mode 100644 index 000000000000..122c30d4d37d --- /dev/null +++ b/frontend/src/ts/constants/sounds.ts @@ -0,0 +1,75 @@ +import { PlaySoundOnClick } from "@monkeytype/schemas/configs"; + +export const soundsConfig: SoundConfigType = { + 1: { numberOfSounds: 3 }, + 2: { numberOfSounds: 3 }, + 3: { numberOfSounds: 3 }, + 4: { numberOfSounds: 6 }, + 5: { numberOfSounds: 6 }, + 6: { numberOfSounds: 3 }, + 7: { numberOfSounds: 3 }, + 8: { oscillatorType: "sine" }, + 9: { oscillatorType: "sawtooth" }, + 10: { oscillatorType: "square" }, + 11: { oscillatorType: "triangle" }, + 12: { validNotes: ["C", "D", "E", "G", "A"] }, + 13: { validNotes: ["C", "D", "E", "Gb", "Ab", "Bb"] }, + 14: { numberOfSounds: 8 }, + 15: { numberOfSounds: 5 }, + 16: { numberOfSounds: 8 }, +}; + +export type ClickSoundConfig = { + numberOfSounds: number; +}; + +export type SupportedOscillatorTypes = Exclude; +export type OscillatorSoundConfig = { + oscillatorType: SupportedOscillatorTypes; +}; + +export type ScaleSoundConfig = { + validNotes: ValidNotes[]; +}; + +export type SoundConfigType = Record< + Exclude, + ClickSoundConfig | OscillatorSoundConfig | ScaleSoundConfig +>; + +export type ValidNotes = + | "C" + | "Db" + | "D" + | "Eb" + | "E" + | "F" + | "Gb" + | "G" + | "Ab" + | "A" + | "Bb" + | "B"; + +type ClickSoundConfigType = Partial< + Record, string[]> +>; + +export const clickSoundConfig: ClickSoundConfigType = + extractClickSounds(soundsConfig); + +function extractClickSounds( + shortConfig: SoundConfigType, +): ClickSoundConfigType { + return Object.fromEntries( + Object.entries(shortConfig) + .filter(([_, cfg]) => "numberOfSounds" in cfg) + .map(([key, cfg]) => { + const config = cfg as ClickSoundConfig; + const fullConfig = new Array(config.numberOfSounds) + .fill(0) + .map((_, index) => `../sounds/click${key}/${index + 1}.wav`); + return [key, fullConfig]; + }), + ); +} diff --git a/frontend/src/ts/controllers/preset-controller.ts b/frontend/src/ts/controllers/preset-controller.ts index 7475af259362..39b498e8ae76 100644 --- a/frontend/src/ts/controllers/preset-controller.ts +++ b/frontend/src/ts/controllers/preset-controller.ts @@ -1,27 +1,22 @@ -import { Preset } from "@monkeytype/schemas/presets"; - import { Config } from "../config/store"; import { applyConfig } from "../config/lifecycle"; import * as DB from "../db"; -import { - showNoticeNotification, - showSuccessNotification, -} from "../states/notifications"; +import { showSuccessNotification } from "../states/notifications"; import * as TestLogic from "../test/test-logic"; import { clearActiveTags, setTagActive, saveActiveToLocalStorage, } from "../collections/tags"; -import { SnapshotPreset } from "../constants/default-snapshot"; import { saveFullConfigToLocalStorage } from "../config/persistence"; import * as ModesNotice from "../elements/modes-notice"; +import { __nonReactive, type PresetItem } from "../collections/presets"; export async function apply(_id: string): Promise { const snapshot = DB.getSnapshot(); if (!snapshot) return; - const presetToApply = snapshot.presets?.find((preset) => preset._id === _id); + const presetToApply = __nonReactive.getPreset(_id); if (presetToApply === undefined) { return; } @@ -52,21 +47,7 @@ export async function apply(_id: string): Promise { showSuccessNotification("Preset applied", { durationMs: 2000 }); saveFullConfigToLocalStorage(); } -function isPartialPreset(preset: SnapshotPreset): boolean { - return preset.settingGroups !== undefined && preset.settingGroups !== null; -} - -export async function getPreset(_id: string): Promise { - const snapshot = DB.getSnapshot(); - if (!snapshot) { - return; - } - - const preset = snapshot.presets?.find((preset) => preset._id === _id); - if (preset === undefined) { - showNoticeNotification("Preset not found"); - return; - } - return preset; +function isPartialPreset(preset: PresetItem): boolean { + return preset.settingGroups !== undefined && preset.settingGroups !== null; } diff --git a/frontend/src/ts/controllers/sound-controller.ts b/frontend/src/ts/controllers/sound-controller.ts index 24e01e6b397a..5630efc12914 100644 --- a/frontend/src/ts/controllers/sound-controller.ts +++ b/frontend/src/ts/controllers/sound-controller.ts @@ -1,516 +1,131 @@ import { Config } from "../config/store"; import { configEvent } from "../events/config"; import { randomElementFromArray } from "../utils/arrays"; -import { randomIntFromRange } from "@monkeytype/util/numbers"; import { leftState, rightState } from "../test/shift-tracker"; import { capsState } from "../test/caps-warning"; import { showErrorNotification } from "../states/notifications"; import type { Howl } from "howler"; -import { PlaySoundOnClick } from "@monkeytype/schemas/configs"; +import { + PlaySoundOnClick, + PlaySoundOnError, +} from "@monkeytype/schemas/configs"; +import { + clickSoundConfig, + ScaleSoundConfig, + SoundConfigType, + soundsConfig, + SupportedOscillatorTypes, + ValidNotes, +} from "../constants/sounds"; + +let howlerModulePromise: Promise | null = null; +async function getHowlerModule(): Promise { + howlerModulePromise ??= import("howler"); + return howlerModulePromise; +} + +let initPromise: Promise | null = null; +const loadedBundles: Set = new Set(); + +const howlers: Record> = {}; -async function gethowler(): Promise { - return await import("howler"); +async function getHowl(src: string): Promise { + howlers[src] ??= (async () => { + const { Howl } = await getHowlerModule(); + return new Howl({ src }); + })(); + + return howlers[src]; } -type ClickSounds = Record< - string, - { - sounds: Howl[]; - counter: number; - }[] ->; - -type ErrorSounds = Record< - string, - { - sounds: Howl[]; - counter: number; - }[] ->; +type ErrorSounds = Record, Howl[]>; let errorSounds: ErrorSounds | null = null; -let clickSounds: ClickSounds | null = null; let timeWarning: Howl | null = null; let fartReverb: Howl | null = null; async function initTimeWarning(): Promise { - const Howl = (await gethowler()).Howl; if (timeWarning !== null) return; - timeWarning = new Howl({ - src: "../sound/timeWarning.wav", - }); + timeWarning = await getHowl("../sounds/timeWarning.wav"); } async function initFartReverb(): Promise { - const Howl = (await gethowler()).Howl; if (fartReverb !== null) return; - fartReverb = new Howl({ - src: "../sound/fart-reverb.wav", - }); + fartReverb = await getHowl("../sounds/fart-reverb.wav"); } async function initErrorSound(): Promise { - const Howl = (await gethowler()).Howl; if (errorSounds !== null) return; errorSounds = { - 1: [ - { - sounds: [ - new Howl({ src: "../sound/error1/error1_1.wav" }), - new Howl({ src: "../sound/error1/error1_1.wav" }), - ], - counter: 0, - }, - ], - 2: [ - { - sounds: [ - new Howl({ src: "../sound/error2/error2_1.wav" }), - new Howl({ src: "../sound/error2/error2_1.wav" }), - ], - counter: 0, - }, - ], - 3: [ - { - sounds: [ - new Howl({ src: "../sound/error3/error3_1.wav" }), - new Howl({ src: "../sound/error3/error3_1.wav" }), - ], - counter: 0, - }, - ], + 1: [await getHowl("../sounds/error1/1.wav")], + 2: [await getHowl("../sounds/error2/1.wav")], + 3: [await getHowl("../sounds/error3/1.wav")], 4: [ - { - sounds: [ - new Howl({ src: "../sound/error4/error4_1.wav" }), - new Howl({ src: "../sound/error4/error4_1.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/error4/error4_2.wav" }), - new Howl({ src: "../sound/error4/error4_2.wav" }), - ], - counter: 0, - }, + await getHowl("../sounds/error4/1.wav"), + await getHowl("../sounds/error4/2.wav"), ], }; - Howler.volume(Config.soundVolume); + (await getHowlerModule()).Howler.volume(Config.soundVolume); } async function init(): Promise { - const Howl = (await gethowler()).Howl; - if (clickSounds !== null) return; - clickSounds = { - 1: [ - { - sounds: [ - new Howl({ src: "../sound/click1/click1_1.wav" }), - new Howl({ src: "../sound/click1/click1_1.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click1/click1_2.wav" }), - new Howl({ src: "../sound/click1/click1_2.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click1/click1_3.wav" }), - new Howl({ src: "../sound/click1/click1_3.wav" }), - ], - counter: 0, - }, - ], - 2: [ - { - sounds: [ - new Howl({ src: "../sound/click2/click2_1.wav" }), - new Howl({ src: "../sound/click2/click2_1.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click2/click2_2.wav" }), - new Howl({ src: "../sound/click2/click2_2.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click2/click2_3.wav" }), - new Howl({ src: "../sound/click2/click2_3.wav" }), - ], - counter: 0, - }, - ], - 3: [ - { - sounds: [ - new Howl({ src: "../sound/click3/click3_1.wav" }), - new Howl({ src: "../sound/click3/click3_1.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click3/click3_2.wav" }), - new Howl({ src: "../sound/click3/click3_2.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click3/click3_3.wav" }), - new Howl({ src: "../sound/click3/click3_3.wav" }), - ], - counter: 0, - }, - ], - 4: [ - { - sounds: [ - new Howl({ src: "../sound/click4/click4_1.wav" }), - new Howl({ src: "../sound/click4/click4_11.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click4/click4_2.wav" }), - new Howl({ src: "../sound/click4/click4_22.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click4/click4_3.wav" }), - new Howl({ src: "../sound/click4/click4_33.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click4/click4_4.wav" }), - new Howl({ src: "../sound/click4/click4_44.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click4/click4_5.wav" }), - new Howl({ src: "../sound/click4/click4_55.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click4/click4_6.wav" }), - new Howl({ src: "../sound/click4/click4_66.wav" }), - ], - counter: 0, - }, - ], - 5: [ - { - sounds: [ - new Howl({ src: "../sound/click5/click5_1.wav" }), - new Howl({ src: "../sound/click5/click5_11.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click5/click5_2.wav" }), - new Howl({ src: "../sound/click5/click5_22.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click5/click5_3.wav" }), - new Howl({ src: "../sound/click5/click5_33.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click5/click5_4.wav" }), - new Howl({ src: "../sound/click5/click5_44.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click5/click5_5.wav" }), - new Howl({ src: "../sound/click5/click5_55.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click5/click5_6.wav" }), - new Howl({ src: "../sound/click5/click5_66.wav" }), - ], - counter: 0, - }, - ], - 6: [ - { - sounds: [ - new Howl({ src: "../sound/click6/click6_1.wav" }), - new Howl({ src: "../sound/click6/click6_11.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click6/click6_2.wav" }), - new Howl({ src: "../sound/click6/click6_22.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click6/click6_3.wav" }), - new Howl({ src: "../sound/click6/click6_33.wav" }), - ], - counter: 0, - }, - ], - 7: [ - { - sounds: [ - new Howl({ src: "../sound/click7/click7_1.wav" }), - new Howl({ src: "../sound/click7/click7_11.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click7/click7_2.wav" }), - new Howl({ src: "../sound/click7/click7_22.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click7/click7_3.wav" }), - new Howl({ src: "../sound/click7/click7_33.wav" }), - ], - counter: 0, - }, - ], - 14: [ - { - sounds: [ - new Howl({ src: "../sound/click14/click14_1.wav" }), - new Howl({ src: "../sound/click14/click14_1.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click14/click14_2.wav" }), - new Howl({ src: "../sound/click14/click14_2.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click14/click14_3.wav" }), - new Howl({ src: "../sound/click14/click14_3.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click14/click14_4.wav" }), - new Howl({ src: "../sound/click14/click14_4.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click14/click14_5.wav" }), - new Howl({ src: "../sound/click14/click14_5.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click14/click14_6.wav" }), - new Howl({ src: "../sound/click14/click14_6.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click14/click14_7.wav" }), - new Howl({ src: "../sound/click14/click14_7.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click14/click14_8.wav" }), - new Howl({ src: "../sound/click14/click14_8.wav" }), - ], - counter: 0, - }, - ], - 15: [ - { - sounds: [ - new Howl({ src: "../sound/click15/click15_1.wav" }), - new Howl({ src: "../sound/click15/click15_1.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click15/click15_2.wav" }), - new Howl({ src: "../sound/click15/click15_2.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click15/click15_3.wav" }), - new Howl({ src: "../sound/click15/click15_3.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click15/click15_4.wav" }), - new Howl({ src: "../sound/click15/click15_4.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click15/click15_5.wav" }), - new Howl({ src: "../sound/click15/click15_5.wav" }), - ], - counter: 0, - }, - ], - 16: [ - { - sounds: [ - new Howl({ src: "../sound/click16/click16_1.wav" }), - new Howl({ src: "../sound/click16/click16_1.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click16/click16_2.wav" }), - new Howl({ src: "../sound/click16/click16_2.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click16/click16_3.wav" }), - new Howl({ src: "../sound/click16/click16_3.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click16/click16_4.wav" }), - new Howl({ src: "../sound/click16/click16_4.wav" }), - ], - counter: 0, - }, - // { - // sounds: [ - // new Howl({ src: "../sound/click16/click16_5.wav" }), - // new Howl({ src: "../sound/click16/click16_5.wav" }), - // ], - // counter: 0, - // }, - // { - // sounds: [ - // new Howl({ src: "../sound/click16/click16_6.wav" }), - // new Howl({ src: "../sound/click16/click16_6.wav" }), - // ], - // counter: 0, - // }, - // { - // sounds: [ - // new Howl({ src: "../sound/click16/click16_7.wav" }), - // new Howl({ src: "../sound/click16/click16_7.wav" }), - // ], - // counter: 0, - // }, - { - sounds: [ - new Howl({ src: "../sound/click16/click16_8.wav" }), - new Howl({ src: "../sound/click16/click16_8.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click16/click16_9.wav" }), - new Howl({ src: "../sound/click16/click16_9.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click16/click16_10.wav" }), - new Howl({ src: "../sound/click16/click16_10.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click16/click16_11.wav" }), - new Howl({ src: "../sound/click16/click16_11.wav" }), - ], - counter: 0, - }, - ], - }; - Howler.volume(Config.soundVolume); + initPromise ??= (async () => { + const { Howler } = await getHowlerModule(); + Howler.volume(Config.soundVolume); + })(); + + await initPromise; + + //preload error sounds + await initErrorSound(); + + //preload sounds + const clickId = Config.playSoundOnClick; + if (clickId === "off") return; + + if (!loadedBundles.has(clickId)) { + loadedBundles.add(clickId); + + const config = clickSoundConfig[clickId]; + + if (config === undefined) return; + + await Promise.all(config.flatMap(getHowl)); + } } -export async function previewClick(val: PlaySoundOnClick): Promise { - if (["8", "9", "10", "11"].includes(val)) { - playNote("KeyQ", clickSoundIdsToOscillatorType[val as DynamicClickSounds]); +export async function previewClick(clickId: PlaySoundOnClick): Promise { + if (clickId === "off") return; + + const config = soundsConfig[clickId]; + + if ("oscillatorType" in config) { + playNote({ codeOverride: "KeyQ", oscillatorType: config.oscillatorType }); return; } - if (["12", "13"].includes(val)) { - scaleConfigurations[val as "12" | "13"].preview(); + if ("validNotes" in config) { + scaleConfigurations[clickId]?.preview(); return; } - if (clickSounds === null) await init(); + await init(); - const safeClickSounds = clickSounds as ClickSounds; - - const clickSoundIds = Object.keys(safeClickSounds); - if (!clickSoundIds.includes(val)) return; + const safeClickSounds = clickSoundConfig[clickId]; + if (safeClickSounds === undefined || safeClickSounds[0] === undefined) { + return; + } - safeClickSounds?.[val]?.[0]?.sounds[0]?.seek(0); - safeClickSounds?.[val]?.[0]?.sounds[0]?.play(); + const howl = await getHowl(safeClickSounds[0]); + howl.seek(0); + howl.play(); } -export async function previewError(val: string): Promise { +export async function previewError(val: PlaySoundOnError): Promise { + if (val === "off") return; if (errorSounds === null) await initErrorSound(); const safeErrorSounds = errorSounds as ErrorSounds; @@ -518,8 +133,8 @@ export async function previewError(val: string): Promise { const errorSoundIds = Object.keys(safeErrorSounds); if (!errorSoundIds.includes(val)) return; - errorSounds?.[val]?.[0]?.sounds[0]?.seek(0); - errorSounds?.[val]?.[0]?.sounds[0]?.play(); + errorSounds?.[val]?.[0]?.seek(0); + errorSounds?.[val]?.[0]?.play(); } let currentCode = "KeyA"; @@ -528,7 +143,7 @@ document.addEventListener("keydown", (event) => { currentCode = event.code || "KeyA"; }); -const notes = { +const notes: Record = { C: [16.35, 32.7, 65.41, 130.81, 261.63, 523.25, 1046.5, 2093.0, 4186.01], Db: [17.32, 34.65, 69.3, 138.59, 277.18, 554.37, 1108.73, 2217.46, 4434.92], D: [18.35, 36.71, 73.42, 146.83, 293.66, 587.33, 1174.66, 2349.32, 4698.64], @@ -543,8 +158,7 @@ const notes = { B: [30.87, 61.74, 123.47, 246.94, 493.88, 987.77, 1975.53, 3951.07], } as const; -type ValidNotes = keyof typeof notes; -type ValidFrequencies = (typeof notes)[ValidNotes]; +type ValidFrequencies = number[]; type GetNoteFrequencyCallback = (octave: number) => number; @@ -597,19 +211,6 @@ const codeToNote: Record = { BracketRight: bindToNote(notes.G, 2), }; -type DynamicClickSounds = Extract; -type SupportedOscillatorTypes = Exclude; - -const clickSoundIdsToOscillatorType: Record< - DynamicClickSounds, - SupportedOscillatorTypes -> = { - "8": "sine", - "9": "sawtooth", - "10": "square", - "11": "triangle", -}; - let audioCtx: AudioContext | undefined | null; function initAudioContext(): void { @@ -628,20 +229,13 @@ function initAudioContext(): void { } } -type ValidScales = "pentatonic" | "wholetone"; - -const scales: Record = { - pentatonic: ["C", "D", "E", "G", "A"], - wholetone: ["C", "D", "E", "Gb", "Ab", "Bb"], -}; - type ScaleData = { octave: number; // current octave of scale direction: number; // whether scale is ascending or descending position: number; // current position in scale }; -function createPreviewScale(scaleName: ValidScales): () => void { +function createPreviewScale(validNotes: ValidNotes[]): () => void { // We use a JavaScript closure to create a preview function that can be called multiple times and progress through the scale const scale: ScaleData = { position: 0, @@ -650,13 +244,12 @@ function createPreviewScale(scaleName: ValidScales): () => void { }; return async () => { - if (clickSounds === null) await init(); - playScale(scaleName, scale); + await init(); + playScale(validNotes, scale); }; } type ScaleMeta = { - name: ValidScales; preview: ReturnType; meta: ScaleData; }; @@ -667,30 +260,17 @@ const defaultScaleData: ScaleData = { direction: 1, }; -export const scaleConfigurations: Record< - Extract, - ScaleMeta -> = { - "12": { - name: "pentatonic", - preview: createPreviewScale("pentatonic"), - meta: defaultScaleData, - }, - "13": { - name: "wholetone", - preview: createPreviewScale("wholetone"), - meta: defaultScaleData, - }, -}; +type ScaleConfigurationType = Partial>; + +export const scaleConfigurations: ScaleConfigurationType = + extractScaleSounds(soundsConfig); -function playScale(scale: ValidScales, scaleMeta: ScaleData): void { +function playScale(validNotes: ValidNotes[], scaleMeta: ScaleData): void { if (audioCtx === undefined) { initAudioContext(); } if (!audioCtx) return; - const randomNote = randomIntFromRange(0, scales[scale].length - 1); - if (Math.random() < 0.5) { scaleMeta.octave += scaleMeta.direction; } @@ -702,7 +282,7 @@ function playScale(scale: ValidScales, scaleMeta: ScaleData): void { scaleMeta.direction = 1; } - const note = scales[scale][randomNote] as ValidNotes; + const note = randomElementFromArray(validNotes); const currentFrequency = notes[note][scaleMeta.octave] as number; @@ -736,20 +316,20 @@ export async function playFartReverb(): Promise { } export async function clearAllSounds(): Promise { - const Howl = (await gethowler()).Howler; - Howl.stop(); + const { Howler } = await getHowlerModule(); + Howler.stop(); } -function playNote( - codeOverride?: string, - oscillatorTypeOverride?: SupportedOscillatorTypes, -): void { +function playNote(options: { + codeOverride?: string; + oscillatorType: SupportedOscillatorTypes; +}): void { if (audioCtx === undefined) { initAudioContext(); } if (!audioCtx) return; - currentCode = codeOverride ?? currentCode; + currentCode = options.codeOverride ?? currentCode; if (!(currentCode in codeToNote)) { return; } @@ -761,11 +341,7 @@ function playNote( const oscillatorNode = audioCtx.createOscillator(); const gainNode = audioCtx.createGain(); - oscillatorNode.type = - oscillatorTypeOverride ?? - clickSoundIdsToOscillatorType[ - Config.playSoundOnClick as DynamicClickSounds - ]; + oscillatorNode.type = options.oscillatorType; gainNode.gain.value = Config.soundVolume / 10; oscillatorNode.connect(gainNode); @@ -778,33 +354,31 @@ function playNote( } export async function playClick(codeOverride?: string): Promise { - if (Config.playSoundOnClick === "off") return; - - if (Config.playSoundOnClick in scaleConfigurations) { - const { name, meta } = - scaleConfigurations[ - Config.playSoundOnClick as keyof typeof scaleConfigurations - ]; - playScale(name, meta); + const val = Config.playSoundOnClick; + if (val === "off") return; + + const config = soundsConfig[val]; + + if ("oscillatorType" in config) { + playNote({ codeOverride, oscillatorType: config.oscillatorType }); return; } - if (Config.playSoundOnClick in clickSoundIdsToOscillatorType) { - playNote(codeOverride ?? undefined); + if ("validNotes" in config) { + const scaleConfig = scaleConfigurations[val]; + if (scaleConfig === undefined) { + throw new Error("missing scale config"); + } + playScale(config.validNotes, scaleConfig.meta); return; } - if (clickSounds === null) await init(); - - const sounds = (clickSounds as ClickSounds)[Config.playSoundOnClick]; + await init(); + const sounds = clickSoundConfig[val]; if (sounds === undefined) throw new Error("Invalid click sound ID"); - const randomSound = randomElementFromArray(sounds); - const soundToPlay = randomSound.sounds[randomSound.counter] as Howl; - - randomSound.counter++; - if (randomSound.counter === 2) randomSound.counter = 0; + const soundToPlay = await getHowl(randomSound); soundToPlay.seek(0); soundToPlay.play(); } @@ -817,25 +391,42 @@ export async function playError(): Promise { if (sounds === undefined) throw new Error("Invalid error sound ID"); const randomSound = randomElementFromArray(sounds); - const soundToPlay = randomSound.sounds[randomSound.counter] as Howl; - - randomSound.counter++; - if (randomSound.counter === 2) randomSound.counter = 0; - soundToPlay.seek(0); - soundToPlay.play(); + randomSound.seek(0); + randomSound.play(); } -function setVolume(val: number): void { +async function setVolume(val: number): Promise { try { + const { Howler } = await getHowlerModule(); Howler.volume(val); } catch (e) { // } } +function extractScaleSounds( + shortConfig: SoundConfigType, +): ScaleConfigurationType { + return Object.fromEntries( + Object.entries(shortConfig) + .filter(([_, cfg]) => "validNotes" in cfg) + .map(([key, cfg]) => { + const config = cfg as ScaleSoundConfig; + + return [ + key, + { + preview: createPreviewScale(config.validNotes), + meta: { ...defaultScaleData }, + } as ScaleMeta, + ]; + }), + ); +} + configEvent.subscribe(({ key, newValue }) => { if (key === "playSoundOnClick" && newValue !== "off") void init(); if (key === "soundVolume") { - setVolume(newValue); + void setVolume(newValue); } }); diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index de2084a2a397..8538ccd13799 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -22,7 +22,6 @@ import { import { getDefaultSnapshot, Snapshot, - SnapshotPreset, SnapshotResult, } from "./constants/default-snapshot"; import { getFirstDayOfTheWeek } from "./utils/date-and-time"; @@ -44,6 +43,7 @@ import { setXpBarData } from "./states/header"; import { FunboxMetadata } from "@monkeytype/funbox"; import { fillTagsCollection, __nonReactive } from "./collections/tags"; import { updateTagsInFilterStorage } from "./states/result-filters"; +import { fillPresetsCollection } from "./collections/presets"; let dbSnapshot: Snapshot | undefined; const firstDayOfTheWeek = getFirstDayOfTheWeek(); @@ -198,34 +198,13 @@ export async function initSnapshot(): Promise { snap.customThemes = userData.customThemes ?? []; fillTagsCollection(userData.tags ?? []); - updateTagsInFilterStorage(userData.tags?.map((it) => it._id) ?? []); + fillPresetsCollection(presetsData ?? []); - if (presetsData !== undefined && presetsData !== null) { - const presetsWithDisplay = presetsData.map((preset) => { - return { - ...preset, - display: preset.name.replace(/_/g, " "), - }; - }) as SnapshotPreset[]; - snap.presets = presetsWithDisplay; - - snap.presets = snap.presets?.sort( - (a: SnapshotPreset, b: SnapshotPreset) => { - if (a.name > b.name) { - return 1; - } else if (a.name < b.name) { - return -1; - } else { - return 0; - } - }, - ); - } + fillResultFilterPresetsCollection(userData.resultFilterPresets ?? []); + updateTagsInFilterStorage(userData.tags?.map((it) => it._id) ?? []); snap.connections = convertConnections(connectionsData); - fillResultFilterPresetsCollection(userData.resultFilterPresets ?? []); - dbSnapshot = snap; return dbSnapshot; diff --git a/frontend/src/ts/event-handlers/settings.ts b/frontend/src/ts/event-handlers/settings.ts index e893fe8a1e22..e1a327e8cc6b 100644 --- a/frontend/src/ts/event-handlers/settings.ts +++ b/frontend/src/ts/event-handlers/settings.ts @@ -24,7 +24,7 @@ settingsPage?.qs(".section.presets")?.on("click", (e) => { EditPresetPopup.show("add"); } else if (target.classList.contains("editButton")) { const presetid = target.parentElement?.getAttribute("data-id"); - const name = target.parentElement?.getAttribute("data-display"); + const name = target.parentElement?.getAttribute("data-name"); if ( presetid === undefined || name === undefined || @@ -41,7 +41,7 @@ settingsPage?.qs(".section.presets")?.on("click", (e) => { EditPresetPopup.show("edit", presetid, name); } else if (target.classList.contains("removeButton")) { const presetid = target.parentElement?.getAttribute("data-id"); - const name = target.parentElement?.getAttribute("data-display"); + const name = target.parentElement?.getAttribute("data-name"); if ( presetid === undefined || name === undefined || diff --git a/frontend/src/ts/modals/edit-preset.ts b/frontend/src/ts/modals/edit-preset.ts index 240bcfa6b6f7..8433942dcbb4 100644 --- a/frontend/src/ts/modals/edit-preset.ts +++ b/frontend/src/ts/modals/edit-preset.ts @@ -1,6 +1,4 @@ -import Ape from "../ape"; -import * as DB from "../db"; -import { __nonReactive } from "../collections/tags"; +import { __nonReactive as __nonReactiveTags } from "../collections/tags"; import { showLoaderBar, hideLoaderBar } from "../states/loader-bar"; import * as Settings from "../pages/settings"; import { @@ -14,7 +12,12 @@ import { PresetType, PresetTypeSchema, } from "@monkeytype/schemas/presets"; -import { getPreset } from "../controllers/preset-controller"; +import { + __nonReactive as __nonReactivePresets, + addPreset, + editPreset, + deletePreset, +} from "../collections/presets"; import { ConfigGroupName, ConfigGroupNameSchema, @@ -22,7 +25,6 @@ import { Config as ConfigType, } from "@monkeytype/schemas/configs"; import { getDefaultConfig } from "../constants/default-config"; -import { SnapshotPreset } from "../constants/default-snapshot"; import { ValidatedHtmlInputElement } from "../elements/input-validation"; import { ElementWithUtils } from "../utils/dom"; import { configMetadata } from "../config/metadata"; @@ -80,7 +82,7 @@ export function show(action: string, id?: string, name?: string): void { modalEl.qsr("label.changePresetToCurrentCheckbox").show(); modalEl.qsr(".presetNameTitle").show(); state.setPresetToCurrent = false; - await updateEditPresetUI(); + updateEditPresetUI(); } else if ( action === "remove" && id !== undefined && @@ -106,11 +108,11 @@ export function show(action: string, id?: string, name?: string): void { }); } -async function initializeEditState(id: string): Promise { +function initializeEditState(id: string): void { for (const key of state.checkboxes.keys()) { state.checkboxes.set(key, false); } - const edittedPreset = await getPreset(id); + const edittedPreset = __nonReactivePresets.getPreset(id); if (edittedPreset === undefined) { showErrorNotification("Preset not found"); return; @@ -148,9 +150,9 @@ function addCheckboxListeners(): void { const presetToCurrentCheckbox = modalEl.qsr( `.changePresetToCurrentCheckbox input`, ); - presetToCurrentCheckbox.on("change", async () => { + presetToCurrentCheckbox.on("change", () => { state.setPresetToCurrent = presetToCurrentCheckbox.isChecked() as boolean; - await updateEditPresetUI(); + updateEditPresetUI(); }); } @@ -202,14 +204,14 @@ function updateUI(): void { modalEl.qsr(".partialPresetGroups").hide(); } } -async function updateEditPresetUI(): Promise { +function updateEditPresetUI(): void { const modalEl = modal.getModal(); if (state.setPresetToCurrent) { modalEl .qsr("label.changePresetToCurrentCheckbox input") .setChecked(true); const presetId = modalEl.getAttribute("data-preset-id") as string; - await initializeEditState(presetId); + initializeEditState(presetId); modalEl.qsr(".inputs").show(); modalEl.qsr(".presetType").show(); } else { @@ -237,8 +239,6 @@ async function apply(): Promise { .qsr("label.changePresetToCurrentCheckbox input") .isChecked(); - const snapshotPresets = DB.getSnapshot()?.presets ?? []; - if (action === null || action === "") { return; } @@ -277,84 +277,43 @@ async function apply(): Promise { showLoaderBar(); - if (action === "add") { - const configChanges = getConfigChanges(); - const activeSettingGroups = getActiveSettingGroupsFromState(); - const response = await Ape.presets.add({ - body: { + try { + if (action === "add") { + const configChanges = getConfigChanges(); + const activeSettingGroups = getActiveSettingGroupsFromState(); + await addPreset({ name: presetName, config: configChanges, - ...(state.presetType === "partial" && { - settingGroups: activeSettingGroups, - }), - }, - }); - - if (response.status !== 200 || response.body.data === null) { - showErrorNotification("Failed to add preset: " + response.body.message); - } else { + settingGroups: + state.presetType === "partial" ? activeSettingGroups : undefined, + }); showSuccessNotification("Preset added", { durationMs: 2000 }); - snapshotPresets.push({ - name: presetName, - config: configChanges, - ...(state.presetType === "partial" && { - settingGroups: activeSettingGroups, - }), - display: presetName.replace(/_/g, " "), - _id: response.body.data.presetId, - } as SnapshotPreset); - } - } else if (action === "edit") { - const preset = snapshotPresets.find( - (preset: SnapshotPreset) => preset._id === presetId, - ) as SnapshotPreset; - if (preset === undefined) { - showErrorNotification("Preset not found"); - return; - } - const configChanges = getConfigChanges(); - const activeSettingGroups: ConfigGroupName[] | null = - state.presetType === "partial" ? getActiveSettingGroupsFromState() : null; - const response = await Ape.presets.save({ - body: { - _id: presetId, + } else if (action === "edit") { + const existing = __nonReactivePresets.getPreset(presetId); + if (existing === undefined) { + showErrorNotification("Preset not found"); + return; + } + const configChanges = getConfigChanges(); + const activeSettingGroups: ConfigGroupName[] | null = + state.presetType === "partial" + ? getActiveSettingGroupsFromState() + : null; + await editPreset({ + presetId, name: presetName, - ...(updateConfig && { - config: configChanges, - settingGroups: activeSettingGroups, - }), - }, - }); - - if (response.status !== 200) { - showErrorNotification("Failed to edit preset", { response }); - } else { + config: updateConfig ? configChanges : undefined, + settingGroups: updateConfig ? activeSettingGroups : undefined, + }); showSuccessNotification("Preset updated"); - - preset.name = presetName; - preset.display = presetName.replace(/_/g, " "); - if (updateConfig) { - preset.config = configChanges; - if (state.presetType === "partial") { - preset.settingGroups = getActiveSettingGroupsFromState(); - } else { - preset.settingGroups = null; - } - } - } - } else if (action === "remove") { - const response = await Ape.presets.delete({ params: { presetId } }); - - if (response.status !== 200) { - showErrorNotification("Failed to remove preset", { response }); - } else { + } else if (action === "remove") { + await deletePreset({ presetId }); showSuccessNotification("Preset removed"); - snapshotPresets.forEach((preset: SnapshotPreset, index: number) => { - if (preset._id === presetId) { - snapshotPresets.splice(index, 1); - } - }); } + } catch (e) { + showErrorNotification( + e instanceof Error ? e.message : "Failed to update preset", + ); } void Settings.update(); @@ -398,7 +357,7 @@ function getConfigChanges(): Partial { state.presetType === "partial" ? getPartialConfigChanges(getConfigChangesFromConfig()) : getConfigChangesFromConfig(); - const activeTagIds: string[] = __nonReactive + const activeTagIds: string[] = __nonReactiveTags .getActiveTags() .map((tag) => tag._id); diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index c3e93fd4ec14..70c3dbf347a3 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -9,7 +9,7 @@ import * as Strings from "../utils/strings"; import * as DB from "../db"; import * as Funbox from "../test/funbox/funbox"; import { - __nonReactive, + __nonReactive as __nonReactiveTags, toggleTagActive, useTagsLiveQuery, } from "../collections/tags"; @@ -37,7 +37,10 @@ import { } from "@monkeytype/schemas/configs"; import { getAllFunboxes, checkCompatibility } from "@monkeytype/funbox"; import { getActiveFunboxNames } from "../test/funbox/list"; -import { SnapshotPreset } from "../constants/default-snapshot"; +import { + __nonReactive as __nonReactivePresets, + usePresetsLiveQuery, +} from "../collections/presets"; import { LayoutsList } from "../constants/layouts"; import { DataArrayPartial, Optgroup, OptionOptional } from "slim-select/store"; import { ThemesList, ThemeWithName } from "../constants/themes"; @@ -529,7 +532,7 @@ createEffectOn(activeTags, refreshTagsSettingsSection); function refreshTagsSettingsSection(): void { if (isAuthenticated() && DB.getSnapshot()) { const tagsEl = qs(".pageSettings .section.tags .tagsList")?.empty(); - __nonReactive.getTags().forEach((tag) => { + __nonReactiveTags.getTags().forEach((tag) => { // let tagPbString = "No PB found"; // if (tag.pb !== undefined && tag.pb > 0) { // tagPbString = `PB: ${tag.pb}`; @@ -561,15 +564,18 @@ function refreshTagsSettingsSection(): void { } } +const presetsQuery = usePresetsLiveQuery(); +createEffectOn(presetsQuery, refreshPresetsSettingsSection); + function refreshPresetsSettingsSection(): void { if (isAuthenticated() && DB.getSnapshot()) { const presetsEl = qs( ".pageSettings .section.presets .presetsList", )?.empty(); - DB.getSnapshot()?.presets?.forEach((preset: SnapshotPreset) => { + __nonReactivePresets.getPresets().forEach((preset) => { presetsEl?.appendHtml(` -
- +
+ @@ -577,7 +583,7 @@ function refreshPresetsSettingsSection(): void {
- + `); }); qs(".pageSettings .section.presets")?.show(); diff --git a/frontend/static/sound/click16/click16_5.wav b/frontend/static/sound/click16/click16_5.wav deleted file mode 100644 index 71df277a5d18..000000000000 Binary files a/frontend/static/sound/click16/click16_5.wav and /dev/null differ diff --git a/frontend/static/sound/click16/click16_6.wav b/frontend/static/sound/click16/click16_6.wav deleted file mode 100644 index 4076a647942e..000000000000 Binary files a/frontend/static/sound/click16/click16_6.wav and /dev/null differ diff --git a/frontend/static/sound/click16/click16_7.wav b/frontend/static/sound/click16/click16_7.wav deleted file mode 100644 index 44919e80e80f..000000000000 Binary files a/frontend/static/sound/click16/click16_7.wav and /dev/null differ diff --git a/frontend/static/sound/click4/click4_11.wav b/frontend/static/sound/click4/click4_11.wav deleted file mode 100644 index b03acd4db372..000000000000 Binary files a/frontend/static/sound/click4/click4_11.wav and /dev/null differ diff --git a/frontend/static/sound/click4/click4_22.wav b/frontend/static/sound/click4/click4_22.wav deleted file mode 100644 index e8af2f633575..000000000000 Binary files a/frontend/static/sound/click4/click4_22.wav and /dev/null differ diff --git a/frontend/static/sound/click4/click4_33.wav b/frontend/static/sound/click4/click4_33.wav deleted file mode 100644 index 604449d5df77..000000000000 Binary files a/frontend/static/sound/click4/click4_33.wav and /dev/null differ diff --git a/frontend/static/sound/click4/click4_44.wav b/frontend/static/sound/click4/click4_44.wav deleted file mode 100644 index b2939d493ef3..000000000000 Binary files a/frontend/static/sound/click4/click4_44.wav and /dev/null differ diff --git a/frontend/static/sound/click4/click4_55.wav b/frontend/static/sound/click4/click4_55.wav deleted file mode 100644 index 145976c7c6c3..000000000000 Binary files a/frontend/static/sound/click4/click4_55.wav and /dev/null differ diff --git a/frontend/static/sound/click4/click4_66.wav b/frontend/static/sound/click4/click4_66.wav deleted file mode 100644 index ec0d5379ad15..000000000000 Binary files a/frontend/static/sound/click4/click4_66.wav and /dev/null differ diff --git a/frontend/static/sound/click5/click5_11.wav b/frontend/static/sound/click5/click5_11.wav deleted file mode 100644 index 5051ae1bec1f..000000000000 Binary files a/frontend/static/sound/click5/click5_11.wav and /dev/null differ diff --git a/frontend/static/sound/click5/click5_22.wav b/frontend/static/sound/click5/click5_22.wav deleted file mode 100644 index a34e3025c280..000000000000 Binary files a/frontend/static/sound/click5/click5_22.wav and /dev/null differ diff --git a/frontend/static/sound/click5/click5_33.wav b/frontend/static/sound/click5/click5_33.wav deleted file mode 100644 index e365207f495e..000000000000 Binary files a/frontend/static/sound/click5/click5_33.wav and /dev/null differ diff --git a/frontend/static/sound/click5/click5_44.wav b/frontend/static/sound/click5/click5_44.wav deleted file mode 100644 index a6536ff9ee01..000000000000 Binary files a/frontend/static/sound/click5/click5_44.wav and /dev/null differ diff --git a/frontend/static/sound/click5/click5_55.wav b/frontend/static/sound/click5/click5_55.wav deleted file mode 100644 index 7182d01b7d71..000000000000 Binary files a/frontend/static/sound/click5/click5_55.wav and /dev/null differ diff --git a/frontend/static/sound/click5/click5_66.wav b/frontend/static/sound/click5/click5_66.wav deleted file mode 100644 index 60a2b3a13906..000000000000 Binary files a/frontend/static/sound/click5/click5_66.wav and /dev/null differ diff --git a/frontend/static/sound/click6/click6_22.wav b/frontend/static/sound/click6/click6_22.wav deleted file mode 100644 index aacf41a62b3d..000000000000 Binary files a/frontend/static/sound/click6/click6_22.wav and /dev/null differ diff --git a/frontend/static/sound/click6/click6_3.wav b/frontend/static/sound/click6/click6_3.wav deleted file mode 100644 index aacf41a62b3d..000000000000 Binary files a/frontend/static/sound/click6/click6_3.wav and /dev/null differ diff --git a/frontend/static/sound/click6/click6_33.wav b/frontend/static/sound/click6/click6_33.wav deleted file mode 100644 index aacf41a62b3d..000000000000 Binary files a/frontend/static/sound/click6/click6_33.wav and /dev/null differ diff --git a/frontend/static/sound/click7/click7_22.wav b/frontend/static/sound/click7/click7_22.wav deleted file mode 100644 index 1521655b95b9..000000000000 Binary files a/frontend/static/sound/click7/click7_22.wav and /dev/null differ diff --git a/frontend/static/sound/click7/click7_3.wav b/frontend/static/sound/click7/click7_3.wav deleted file mode 100644 index 1521655b95b9..000000000000 Binary files a/frontend/static/sound/click7/click7_3.wav and /dev/null differ diff --git a/frontend/static/sound/click7/click7_33.wav b/frontend/static/sound/click7/click7_33.wav deleted file mode 100644 index 1521655b95b9..000000000000 Binary files a/frontend/static/sound/click7/click7_33.wav and /dev/null differ diff --git a/frontend/static/sound/click1/click1_1.wav b/frontend/static/sounds/click1/1.wav similarity index 100% rename from frontend/static/sound/click1/click1_1.wav rename to frontend/static/sounds/click1/1.wav diff --git a/frontend/static/sound/click1/click1_2.wav b/frontend/static/sounds/click1/2.wav similarity index 100% rename from frontend/static/sound/click1/click1_2.wav rename to frontend/static/sounds/click1/2.wav diff --git a/frontend/static/sound/click1/click1_3.wav b/frontend/static/sounds/click1/3.wav similarity index 100% rename from frontend/static/sound/click1/click1_3.wav rename to frontend/static/sounds/click1/3.wav diff --git a/frontend/static/sound/click14/click14_1.wav b/frontend/static/sounds/click14/1.wav similarity index 100% rename from frontend/static/sound/click14/click14_1.wav rename to frontend/static/sounds/click14/1.wav diff --git a/frontend/static/sound/click14/click14_2.wav b/frontend/static/sounds/click14/2.wav similarity index 100% rename from frontend/static/sound/click14/click14_2.wav rename to frontend/static/sounds/click14/2.wav diff --git a/frontend/static/sound/click14/click14_3.wav b/frontend/static/sounds/click14/3.wav similarity index 100% rename from frontend/static/sound/click14/click14_3.wav rename to frontend/static/sounds/click14/3.wav diff --git a/frontend/static/sound/click14/click14_4.wav b/frontend/static/sounds/click14/4.wav similarity index 100% rename from frontend/static/sound/click14/click14_4.wav rename to frontend/static/sounds/click14/4.wav diff --git a/frontend/static/sound/click14/click14_5.wav b/frontend/static/sounds/click14/5.wav similarity index 100% rename from frontend/static/sound/click14/click14_5.wav rename to frontend/static/sounds/click14/5.wav diff --git a/frontend/static/sound/click14/click14_6.wav b/frontend/static/sounds/click14/6.wav similarity index 100% rename from frontend/static/sound/click14/click14_6.wav rename to frontend/static/sounds/click14/6.wav diff --git a/frontend/static/sound/click14/click14_7.wav b/frontend/static/sounds/click14/7.wav similarity index 100% rename from frontend/static/sound/click14/click14_7.wav rename to frontend/static/sounds/click14/7.wav diff --git a/frontend/static/sound/click14/click14_8.wav b/frontend/static/sounds/click14/8.wav similarity index 100% rename from frontend/static/sound/click14/click14_8.wav rename to frontend/static/sounds/click14/8.wav diff --git a/frontend/static/sound/click15/click15_1.wav b/frontend/static/sounds/click15/1.wav similarity index 100% rename from frontend/static/sound/click15/click15_1.wav rename to frontend/static/sounds/click15/1.wav diff --git a/frontend/static/sound/click15/click15_2.wav b/frontend/static/sounds/click15/2.wav similarity index 100% rename from frontend/static/sound/click15/click15_2.wav rename to frontend/static/sounds/click15/2.wav diff --git a/frontend/static/sound/click15/click15_3.wav b/frontend/static/sounds/click15/3.wav similarity index 100% rename from frontend/static/sound/click15/click15_3.wav rename to frontend/static/sounds/click15/3.wav diff --git a/frontend/static/sound/click15/click15_4.wav b/frontend/static/sounds/click15/4.wav similarity index 100% rename from frontend/static/sound/click15/click15_4.wav rename to frontend/static/sounds/click15/4.wav diff --git a/frontend/static/sound/click15/click15_5.wav b/frontend/static/sounds/click15/5.wav similarity index 100% rename from frontend/static/sound/click15/click15_5.wav rename to frontend/static/sounds/click15/5.wav diff --git a/frontend/static/sound/click16/click16_1.wav b/frontend/static/sounds/click16/1.wav similarity index 100% rename from frontend/static/sound/click16/click16_1.wav rename to frontend/static/sounds/click16/1.wav diff --git a/frontend/static/sound/click16/click16_2.wav b/frontend/static/sounds/click16/2.wav similarity index 100% rename from frontend/static/sound/click16/click16_2.wav rename to frontend/static/sounds/click16/2.wav diff --git a/frontend/static/sound/click16/click16_3.wav b/frontend/static/sounds/click16/3.wav similarity index 100% rename from frontend/static/sound/click16/click16_3.wav rename to frontend/static/sounds/click16/3.wav diff --git a/frontend/static/sound/click16/click16_4.wav b/frontend/static/sounds/click16/4.wav similarity index 100% rename from frontend/static/sound/click16/click16_4.wav rename to frontend/static/sounds/click16/4.wav diff --git a/frontend/static/sound/click16/click16_9.wav b/frontend/static/sounds/click16/5.wav similarity index 100% rename from frontend/static/sound/click16/click16_9.wav rename to frontend/static/sounds/click16/5.wav diff --git a/frontend/static/sound/click16/click16_10.wav b/frontend/static/sounds/click16/6.wav similarity index 100% rename from frontend/static/sound/click16/click16_10.wav rename to frontend/static/sounds/click16/6.wav diff --git a/frontend/static/sound/click16/click16_11.wav b/frontend/static/sounds/click16/7.wav similarity index 100% rename from frontend/static/sound/click16/click16_11.wav rename to frontend/static/sounds/click16/7.wav diff --git a/frontend/static/sound/click16/click16_8.wav b/frontend/static/sounds/click16/8.wav similarity index 100% rename from frontend/static/sound/click16/click16_8.wav rename to frontend/static/sounds/click16/8.wav diff --git a/frontend/static/sound/click2/click2_1.wav b/frontend/static/sounds/click2/1.wav similarity index 100% rename from frontend/static/sound/click2/click2_1.wav rename to frontend/static/sounds/click2/1.wav diff --git a/frontend/static/sound/click2/click2_2.wav b/frontend/static/sounds/click2/2.wav similarity index 100% rename from frontend/static/sound/click2/click2_2.wav rename to frontend/static/sounds/click2/2.wav diff --git a/frontend/static/sound/click2/click2_3.wav b/frontend/static/sounds/click2/3.wav similarity index 100% rename from frontend/static/sound/click2/click2_3.wav rename to frontend/static/sounds/click2/3.wav diff --git a/frontend/static/sound/click3/click3_1.wav b/frontend/static/sounds/click3/1.wav similarity index 100% rename from frontend/static/sound/click3/click3_1.wav rename to frontend/static/sounds/click3/1.wav diff --git a/frontend/static/sound/click3/click3_2.wav b/frontend/static/sounds/click3/2.wav similarity index 100% rename from frontend/static/sound/click3/click3_2.wav rename to frontend/static/sounds/click3/2.wav diff --git a/frontend/static/sound/click3/click3_3.wav b/frontend/static/sounds/click3/3.wav similarity index 100% rename from frontend/static/sound/click3/click3_3.wav rename to frontend/static/sounds/click3/3.wav diff --git a/frontend/static/sound/click4/click4_1.wav b/frontend/static/sounds/click4/1.wav similarity index 100% rename from frontend/static/sound/click4/click4_1.wav rename to frontend/static/sounds/click4/1.wav diff --git a/frontend/static/sound/click4/click4_2.wav b/frontend/static/sounds/click4/2.wav similarity index 100% rename from frontend/static/sound/click4/click4_2.wav rename to frontend/static/sounds/click4/2.wav diff --git a/frontend/static/sound/click4/click4_3.wav b/frontend/static/sounds/click4/3.wav similarity index 100% rename from frontend/static/sound/click4/click4_3.wav rename to frontend/static/sounds/click4/3.wav diff --git a/frontend/static/sound/click4/click4_4.wav b/frontend/static/sounds/click4/4.wav similarity index 100% rename from frontend/static/sound/click4/click4_4.wav rename to frontend/static/sounds/click4/4.wav diff --git a/frontend/static/sound/click4/click4_5.wav b/frontend/static/sounds/click4/5.wav similarity index 100% rename from frontend/static/sound/click4/click4_5.wav rename to frontend/static/sounds/click4/5.wav diff --git a/frontend/static/sound/click4/click4_6.wav b/frontend/static/sounds/click4/6.wav similarity index 100% rename from frontend/static/sound/click4/click4_6.wav rename to frontend/static/sounds/click4/6.wav diff --git a/frontend/static/sound/click5/click5_1.wav b/frontend/static/sounds/click5/1.wav similarity index 100% rename from frontend/static/sound/click5/click5_1.wav rename to frontend/static/sounds/click5/1.wav diff --git a/frontend/static/sound/click5/click5_2.wav b/frontend/static/sounds/click5/2.wav similarity index 100% rename from frontend/static/sound/click5/click5_2.wav rename to frontend/static/sounds/click5/2.wav diff --git a/frontend/static/sound/click5/click5_3.wav b/frontend/static/sounds/click5/3.wav similarity index 100% rename from frontend/static/sound/click5/click5_3.wav rename to frontend/static/sounds/click5/3.wav diff --git a/frontend/static/sound/click5/click5_4.wav b/frontend/static/sounds/click5/4.wav similarity index 100% rename from frontend/static/sound/click5/click5_4.wav rename to frontend/static/sounds/click5/4.wav diff --git a/frontend/static/sound/click5/click5_5.wav b/frontend/static/sounds/click5/5.wav similarity index 100% rename from frontend/static/sound/click5/click5_5.wav rename to frontend/static/sounds/click5/5.wav diff --git a/frontend/static/sound/click5/click5_6.wav b/frontend/static/sounds/click5/6.wav similarity index 100% rename from frontend/static/sound/click5/click5_6.wav rename to frontend/static/sounds/click5/6.wav diff --git a/frontend/static/sound/click6/click6_1.wav b/frontend/static/sounds/click6/1.wav similarity index 100% rename from frontend/static/sound/click6/click6_1.wav rename to frontend/static/sounds/click6/1.wav diff --git a/frontend/static/sound/click6/click6_11.wav b/frontend/static/sounds/click6/2.wav similarity index 100% rename from frontend/static/sound/click6/click6_11.wav rename to frontend/static/sounds/click6/2.wav diff --git a/frontend/static/sound/click6/click6_2.wav b/frontend/static/sounds/click6/3.wav similarity index 100% rename from frontend/static/sound/click6/click6_2.wav rename to frontend/static/sounds/click6/3.wav diff --git a/frontend/static/sound/click7/click7_1.wav b/frontend/static/sounds/click7/1.wav similarity index 100% rename from frontend/static/sound/click7/click7_1.wav rename to frontend/static/sounds/click7/1.wav diff --git a/frontend/static/sound/click7/click7_11.wav b/frontend/static/sounds/click7/2.wav similarity index 100% rename from frontend/static/sound/click7/click7_11.wav rename to frontend/static/sounds/click7/2.wav diff --git a/frontend/static/sound/click7/click7_2.wav b/frontend/static/sounds/click7/3.wav similarity index 100% rename from frontend/static/sound/click7/click7_2.wav rename to frontend/static/sounds/click7/3.wav diff --git a/frontend/static/sound/error1/error1_1.wav b/frontend/static/sounds/error1/1.wav similarity index 100% rename from frontend/static/sound/error1/error1_1.wav rename to frontend/static/sounds/error1/1.wav diff --git a/frontend/static/sound/error2/error2_1.wav b/frontend/static/sounds/error2/1.wav similarity index 100% rename from frontend/static/sound/error2/error2_1.wav rename to frontend/static/sounds/error2/1.wav diff --git a/frontend/static/sound/error3/error3_1.wav b/frontend/static/sounds/error3/1.wav similarity index 100% rename from frontend/static/sound/error3/error3_1.wav rename to frontend/static/sounds/error3/1.wav diff --git a/frontend/static/sound/error4/error4_1.wav b/frontend/static/sounds/error4/1.wav similarity index 100% rename from frontend/static/sound/error4/error4_1.wav rename to frontend/static/sounds/error4/1.wav diff --git a/frontend/static/sound/error4/error4_2.wav b/frontend/static/sounds/error4/2.wav similarity index 100% rename from frontend/static/sound/error4/error4_2.wav rename to frontend/static/sounds/error4/2.wav diff --git a/frontend/static/sound/fart-reverb.wav b/frontend/static/sounds/fart-reverb.wav similarity index 100% rename from frontend/static/sound/fart-reverb.wav rename to frontend/static/sounds/fart-reverb.wav diff --git a/frontend/static/sound/timeWarning.wav b/frontend/static/sounds/timeWarning.wav similarity index 100% rename from frontend/static/sound/timeWarning.wav rename to frontend/static/sounds/timeWarning.wav