diff --git a/frontend/src/html/pages/settings.html b/frontend/src/html/pages/settings.html index 07919efa3..c77a28756 100644 --- a/frontend/src/html/pages/settings.html +++ b/frontend/src/html/pages/settings.html @@ -705,6 +705,25 @@ +
+
+ + play time warning + +
+
+ Play a short warning sound if you are close to the end of a timed test. +
+
+ + + + + +
+
diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts index a05e1b584..ef4f1bc18 100644 --- a/frontend/src/ts/commandline/lists.ts +++ b/frontend/src/ts/commandline/lists.ts @@ -24,6 +24,7 @@ import QuickEndCommands from "./lists/quick-end"; import OppositeShiftModeCommands from "./lists/opposite-shift-mode"; import SoundOnErrorCommands from "./lists/sound-on-error"; import SoundVolumeCommands from "./lists/sound-volume"; +import TimeWarningCommands from "./lists/time-warning"; import FlipTestColorsCommands from "./lists/flip-test-colors"; import SmoothLineScrollCommands from "./lists/smooth-line-scroll"; import AlwaysShowDecimalCommands from "./lists/always-show-decimal"; @@ -242,6 +243,7 @@ export const commands: CommandsSubgroup = { ...SoundVolumeCommands, ...SoundOnClickCommands, ...SoundOnErrorCommands, + ...TimeWarningCommands, //caret ...SmoothCaretCommands, diff --git a/frontend/src/ts/commandline/lists/time-warning.ts b/frontend/src/ts/commandline/lists/time-warning.ts new file mode 100644 index 000000000..4a021ca39 --- /dev/null +++ b/frontend/src/ts/commandline/lists/time-warning.ts @@ -0,0 +1,37 @@ +import { + PlayTimeWarning, + PlayTimeWarningSchema, +} from "@monkeytype/schemas/configs"; +import * as UpdateConfig from "../../config"; +import * as SoundController from "../../controllers/sound-controller"; +import { Command, CommandsSubgroup } from "../types"; + +const subgroup: CommandsSubgroup = { + title: "Time warning...", + configKey: "playTimeWarning", + list: (Object.keys(PlayTimeWarningSchema.Values) as PlayTimeWarning[]).map( + (time) => ({ + id: `setPlayTimeWarning${time}`, + display: + time === "off" ? "off" : `${time} second${time !== "1" ? "s" : ""}`, + configValue: time, + exec: (): void => { + UpdateConfig.setPlayTimeWarning(time); + if (time !== "off") { + void SoundController.playTimeWarning(); + } + }, + }) + ), +}; + +const commands: Command[] = [ + { + id: "changePlayTimeWarning", + display: "Time warning...", + icon: "fa-exclamation-triangle", + subgroup, + }, +]; + +export default commands; diff --git a/frontend/src/ts/config.ts b/frontend/src/ts/config.ts index 5738b650f..8d15c52b7 100644 --- a/frontend/src/ts/config.ts +++ b/frontend/src/ts/config.ts @@ -394,6 +394,10 @@ const configMetadata: ConfigMetadata = { displayString: "play sound on error", changeRequiresRestart: false, }, + playTimeWarning: { + displayString: "play time warning", + changeRequiresRestart: false, + }, // caret smoothCaret: { @@ -855,6 +859,13 @@ export function setSoundVolume( return genericSet("soundVolume", val, nosave); } +export function setPlayTimeWarning( + value: ConfigSchemas.PlayTimeWarning, + nosave?: boolean +): boolean { + return genericSet("playTimeWarning", value, nosave); +} + //difficulty export function setDifficulty( diff: ConfigSchemas.Difficulty, diff --git a/frontend/src/ts/constants/default-config.ts b/frontend/src/ts/constants/default-config.ts index 7ae2db109..4ab407952 100644 --- a/frontend/src/ts/constants/default-config.ts +++ b/frontend/src/ts/constants/default-config.ts @@ -101,6 +101,7 @@ const obj = { tapeMode: "off", tapeMargin: 50, maxLineWidth: 0, + playTimeWarning: "off", } as Config; export function getDefaultConfig(): Config { diff --git a/frontend/src/ts/controllers/sound-controller.ts b/frontend/src/ts/controllers/sound-controller.ts index aa2a01275..6d2e140d4 100644 --- a/frontend/src/ts/controllers/sound-controller.ts +++ b/frontend/src/ts/controllers/sound-controller.ts @@ -33,6 +33,16 @@ type ErrorSounds = Record< let errorSounds: ErrorSounds | null = null; let clickSounds: ClickSounds | null = null; +let timeWarning: Howl | null = null; + +async function initTimeWarning(): Promise { + const Howl = (await gethowler()).Howl; + if (timeWarning !== null) return; + timeWarning = new Howl({ + src: "../sound/timeWarning.wav", + }); +} + async function initErrorSound(): Promise { const Howl = (await gethowler()).Howl; if (errorSounds !== null) return; @@ -610,6 +620,14 @@ function playScale(scale: ValidScales, scaleMeta: ScaleData): void { oscillatorNode.stop(audioCtx.currentTime + 2); } +export async function playTimeWarning(): Promise { + if (timeWarning === null) await initTimeWarning(); + const soundToPlay = timeWarning as Howl; + soundToPlay.stop(); + soundToPlay.seek(0); + soundToPlay.play(); +} + export function playNote( codeOverride?: string, oscillatorTypeOverride?: SupportedOscillatorTypes diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index 6ce4190b5..260b18c7b 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -285,6 +285,14 @@ async function initGroups(): Promise { UpdateConfig.setSoundVolume, "range" ) as SettingsGroup; + groups["playTimeWarning"] = new SettingsGroup( + "playTimeWarning", + UpdateConfig.setPlayTimeWarning, + "button", + () => { + if (Config.playTimeWarning !== "off") void Sound.playTimeWarning(); + } + ) as SettingsGroup; groups["playSoundOnError"] = new SettingsGroup( "playSoundOnError", UpdateConfig.setPlaySoundOnError, diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts index b9b2db08a..60dbd97ae 100644 --- a/frontend/src/ts/test/test-timer.ts +++ b/frontend/src/ts/test/test-timer.ts @@ -17,6 +17,7 @@ import * as Time from "../states/time"; import * as TimerEvent from "../observables/timer-event"; import * as LayoutfluidFunboxTimer from "../test/funbox/layoutfluid-funbox-timer"; import { KeymapLayout, Layout } from "@monkeytype/schemas/configs"; +import * as SoundController from "../controllers/sound-controller"; type TimerStats = { dateNow: number; @@ -175,6 +176,26 @@ function checkIfTimeIsUp(): void { if (timerDebug) console.timeEnd("times up check"); } +function playTimeWarning(): void { + if (timerDebug) console.time("play timer warning"); + + let maxTime = undefined; + + if (Config.mode === "time") { + maxTime = Config.time; + } else if (Config.mode === "custom" && CustomText.getLimitMode() === "time") { + maxTime = CustomText.getLimitValue(); + } + + if ( + maxTime !== undefined && + Time.get() === maxTime - parseInt(Config.playTimeWarning, 10) + ) { + void SoundController.playTimeWarning(); + } + if (timerDebug) console.timeEnd("play timer warning"); +} + // --------------------------------------- let timerStats: TimerStats[] = []; @@ -188,6 +209,7 @@ async function timerStep(): Promise { Time.increment(); premid(); updateTimer(); + if (Config.playTimeWarning !== "off") playTimeWarning(); const wpmAndRaw = calculateWpmRaw(); const acc = calculateAcc(); monkey(wpmAndRaw); diff --git a/frontend/static/sound/timeWarning.wav b/frontend/static/sound/timeWarning.wav new file mode 100644 index 000000000..85c272711 Binary files /dev/null and b/frontend/static/sound/timeWarning.wav differ diff --git a/packages/schemas/src/configs.ts b/packages/schemas/src/configs.ts index 932482d98..340f8e8be 100644 --- a/packages/schemas/src/configs.ts +++ b/packages/schemas/src/configs.ts @@ -355,6 +355,13 @@ export const CustomBackgroundSchema = z .or(z.literal("")); export type CustomBackground = z.infer; +export const PlayTimeWarningSchema = z + .enum(["off", "1", "3", "5", "10"]) + .describe( + "How many seconds before the end of the test to play a warning sound." + ); +export type PlayTimeWarning = z.infer; + export const ConfigSchema = z .object({ // test @@ -402,6 +409,7 @@ export const ConfigSchema = z soundVolume: SoundVolumeSchema, playSoundOnClick: PlaySoundOnClickSchema, playSoundOnError: PlaySoundOnErrorSchema, + playTimeWarning: PlayTimeWarningSchema, // caret smoothCaret: SmoothCaretSchema, @@ -536,6 +544,7 @@ export const ConfigGroupsLiteral = { soundVolume: "sound", playSoundOnClick: "sound", playSoundOnError: "sound", + playTimeWarning: "sound", //caret smoothCaret: "caret",