diff --git a/frontend/src/html/pages/settings.html b/frontend/src/html/pages/settings.html index b005e6c37f7c..e56ddfa10eb5 100644 --- a/frontend/src/html/pages/settings.html +++ b/frontend/src/html/pages/settings.html @@ -826,6 +826,30 @@ +
+
+ + tape margin +
+
+ When in tape mode, set the carets position from the left edge of the + typing test as a percentage (for example, 50% centers it). +
+
+
+ + +
+
+
diff --git a/frontend/src/styles/test.scss b/frontend/src/styles/test.scss index 3544dbeaf49a..abd02fa6a891 100644 --- a/frontend/src/styles/test.scss +++ b/frontend/src/styles/test.scss @@ -1254,6 +1254,8 @@ } } #liveStatsMini { + width: 0; + justify-content: start; height: 0; margin-left: 0.25em; display: flex; diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts index 87358a2f98da..100032beff17 100644 --- a/frontend/src/ts/commandline/lists.ts +++ b/frontend/src/ts/commandline/lists.ts @@ -51,6 +51,7 @@ import TimerColorCommands from "./lists/timer-color"; import TimerOpacityCommands from "./lists/timer-opacity"; import HighlightModeCommands from "./lists/highlight-mode"; import TapeModeCommands from "./lists/tape-mode"; +import TapeMarginCommands from "./lists/tape-margin"; import BritishEnglishCommands from "./lists/british-english"; import KeymapModeCommands from "./lists/keymap-mode"; import KeymapStyleCommands from "./lists/keymap-style"; @@ -273,6 +274,7 @@ export const commands: CommandsSubgroup = { ...TimerOpacityCommands, ...HighlightModeCommands, ...TapeModeCommands, + ...TapeMarginCommands, ...SmoothLineScrollCommands, ...ShowAllLinesCommands, ...TypingSpeedUnitCommands, diff --git a/frontend/src/ts/commandline/lists/tape-margin.ts b/frontend/src/ts/commandline/lists/tape-margin.ts new file mode 100644 index 000000000000..b183c9599220 --- /dev/null +++ b/frontend/src/ts/commandline/lists/tape-margin.ts @@ -0,0 +1,19 @@ +import Config, * as UpdateConfig from "../../config"; +import { Command } from "../types"; + +const commands: Command[] = [ + { + id: "changeTapeMargin", + display: "Tape margin...", + icon: "fa-tape", + input: true, + defaultValue: (): string => { + return Config.tapeMargin.toString(); + }, + exec: ({ input }): void => { + if (input === undefined || input === "") return; + UpdateConfig.setTapeMargin(parseFloat(input)); + }, + }, +]; +export default commands; diff --git a/frontend/src/ts/config.ts b/frontend/src/ts/config.ts index 81d1c104215e..6d5442600c65 100644 --- a/frontend/src/ts/config.ts +++ b/frontend/src/ts/config.ts @@ -918,6 +918,34 @@ export function setTapeMode( return true; } +export function setTapeMargin( + value: ConfigSchemas.TapeMargin, + nosave?: boolean +): boolean { + if (value < 10) { + value = 10; + } + if (value > 90) { + value = 90; + } + + if ( + !isConfigValueValid("max line width", 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; +} + export function setHideExtraLetters(val: boolean, nosave?: boolean): boolean { if (!isConfigValueValidBoolean("hide extra letters", val)) return false; @@ -2053,6 +2081,7 @@ export async function apply( setLazyMode(configObj.lazyMode, true); setShowAverage(configObj.showAverage, true); setTapeMode(configObj.tapeMode, true); + setTapeMargin(configObj.tapeMargin, true); ConfigEvent.dispatch( "configApplied", diff --git a/frontend/src/ts/constants/default-config.ts b/frontend/src/ts/constants/default-config.ts index bbfcf729101a..df27f01f8719 100644 --- a/frontend/src/ts/constants/default-config.ts +++ b/frontend/src/ts/constants/default-config.ts @@ -101,6 +101,7 @@ const obj = { lazyMode: false, showAverage: "off", tapeMode: "off", + tapeMargin: 10, maxLineWidth: 0, } as Config; diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index 2f1cf76b0847..539e0035837d 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -379,6 +379,11 @@ async function initGroups(): Promise { UpdateConfig.setTapeMode, "button" ) as SettingsGroup; + groups["tapeMargin"] = new SettingsGroup( + "tapeMargin", + UpdateConfig.setTapeMargin, + "button" + ) as SettingsGroup; groups["timerOpacity"] = new SettingsGroup( "timerOpacity", UpdateConfig.setTimerOpacity, @@ -694,6 +699,10 @@ async function fillSettingsPage(): Promise { Config.customLayoutfluid.replace(/#/g, " ") ); + $(".pageSettings .section[data-config-name='tapeMargin'] input").val( + Config.tapeMargin + ); + setEventDisabled(true); if (!groupsInitialized) { await initGroups(); @@ -1159,6 +1168,42 @@ $( } }); +$( + ".pageSettings .section[data-config-name='tapeMargin'] .inputAndButton button.save" +).on("click", () => { + const didConfigSave = UpdateConfig.setTapeMargin( + parseFloat( + $( + ".pageSettings .section[data-config-name='tapeMargin'] .inputAndButton input" + ).val() as string + ) + ); + if (didConfigSave) { + Notifications.add("Saved", 1, { + duration: 1, + }); + } +}); + +$( + ".pageSettings .section[data-config-name='tapeMargin'] .inputAndButton input" +).on("keypress", (e) => { + if (e.key === "Enter") { + const didConfigSave = UpdateConfig.setTapeMargin( + parseFloat( + $( + ".pageSettings .section[data-config-name='tapeMargin'] .inputAndButton input" + ).val() as string + ) + ); + if (didConfigSave) { + Notifications.add("Saved", 1, { + duration: 1, + }); + } + } +}); + $( ".pageSettings .section[data-config-name='maxLineWidth'] .inputAndButton button.save" ).on("click", () => { diff --git a/frontend/src/ts/test/caret.ts b/frontend/src/ts/test/caret.ts index 40108f7d39aa..ad34b3df3a48 100644 --- a/frontend/src/ts/test/caret.ts +++ b/frontend/src/ts/test/caret.ts @@ -106,8 +106,10 @@ function getTargetPositionLeft( } else { const wordsWrapperWidth = $(document.querySelector("#wordsWrapper") as HTMLElement).width() ?? 0; + const tapeMargin = wordsWrapperWidth * (Config.tapeMargin / 100); + result = - wordsWrapperWidth / 2 - + tapeMargin - (fullWidthCaret && isLanguageRightToLeft ? fullWidthCaretWidth : 0); if (Config.tapeMode === "word" && inputLen > 0) { diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 6fe21d6f4afb..e18c02056c96 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -184,13 +184,14 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => { if (eventKey === "tapeMode" && !nosave) { if (eventValue === "off") { $("#words").css("margin-left", "unset"); - $("#liveStatsMini").css("display", "").css("justify-content", ""); } else { scrollTape(); - $("#liveStatsMini") - .css("display", "flex") - .css("justify-content", "center"); } + updateLiveStatsMargin(); + } + + if (eventKey === "tapeMargin" && !nosave) { + updateLiveStatsMargin(); } if (typeof eventValue !== "boolean") return; @@ -474,7 +475,7 @@ export async function updateWordsInputPosition(initial = false): Promise { if (Config.tapeMode !== "off") { el.style.top = targetTop + "px"; - el.style.left = "50%"; + el.style.left = Config.tapeMargin + "%"; return; } @@ -949,7 +950,7 @@ export function scrollTape(): void { .stop(true, false) .animate( { - marginLeft: "50%", + marginLeft: Config.tapeMargin + "%", }, SlowTimer.get() ? 0 : 125 ); @@ -1000,7 +1001,9 @@ export function scrollTape(): void { } } } - const newMargin = wordsWrapperWidth / 2 - (fullWordsWidth + currentWordWidth); + + const tapeMargin = wordsWrapperWidth * (Config.tapeMargin / 100); + const newMargin = tapeMargin - (fullWordsWidth + currentWordWidth); if (Config.smoothLineScroll) { $("#words") .stop(true, false) @@ -1478,6 +1481,20 @@ function updateWordsWidth(): void { } } +function updateLiveStatsMargin(): void { + if (Config.tapeMode === "off") { + $("#liveStatsMini").css({ + "justify-content": "start", + "margin-left": "unset", + }); + } else { + $("#liveStatsMini").css({ + "justify-content": "center", + "margin-left": Config.tapeMargin + "%", + }); + } +} + function updateLiveStatsOpacity(value: TimerOpacity): void { $("#barTimerProgress").css("opacity", parseFloat(value as string)); $("#liveStatsTextTop").css("opacity", parseFloat(value as string)); diff --git a/packages/contracts/src/schemas/configs.ts b/packages/contracts/src/schemas/configs.ts index 0a31ba41671f..00cda9be96d8 100644 --- a/packages/contracts/src/schemas/configs.ts +++ b/packages/contracts/src/schemas/configs.ts @@ -158,6 +158,9 @@ export type HighlightMode = z.infer; export const TapeModeSchema = z.enum(["off", "letter", "word"]); export type TapeMode = z.infer; +export const TapeMarginSchema = z.number().min(10).max(90); +export type TapeMargin = z.infer; + export const TypingSpeedUnitSchema = z.enum([ "wpm", "cpm", @@ -354,6 +357,7 @@ export const ConfigSchema = z minWpmCustomSpeed: MinWpmCustomSpeedSchema, highlightMode: HighlightModeSchema, tapeMode: TapeModeSchema, + tapeMargin: TapeMarginSchema, typingSpeedUnit: TypingSpeedUnitSchema, ads: AdsSchema, hideExtraLetters: z.boolean(),