From da671337c544e3d24b5f8a08f5beeb470e3f8785 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Fri, 18 Apr 2025 16:48:35 +0200 Subject: [PATCH] feat(funbox): add polyglot (@fehmer) (#6454) Add polyglot funbox which let you practice on multiple languages at once in a single test. --------- Co-authored-by: Miodec --- frontend/src/html/pages/settings.html | 13 ++++ frontend/src/ts/commandline/commandline.ts | 34 +++++++++ frontend/src/ts/commandline/lists.ts | 13 ++++ frontend/src/ts/config.ts | 21 ++++++ frontend/src/ts/constants/default-config.ts | 1 + frontend/src/ts/elements/modes-notice.ts | 19 ++++- frontend/src/ts/event-handlers/test.ts | 6 ++ frontend/src/ts/pages/settings.ts | 73 ++++++++++++++----- .../src/ts/test/funbox/funbox-functions.ts | 10 +++ packages/contracts/src/schemas/configs.ts | 5 ++ packages/funbox/__test__/validation.spec.ts | 41 +++++++++++ packages/funbox/src/list.ts | 8 ++ packages/funbox/src/types.ts | 3 +- packages/funbox/src/validation.ts | 4 +- 14 files changed, 229 insertions(+), 22 deletions(-) diff --git a/frontend/src/html/pages/settings.html b/frontend/src/html/pages/settings.html index c504ca459..f85627c41 100644 --- a/frontend/src/html/pages/settings.html +++ b/frontend/src/html/pages/settings.html @@ -351,6 +351,19 @@
+
+
+ + polyglot languages +
+
+ Select which languages you want the polyglot funbox to use. +
+
+ +
+
+
` + ); + } + if (Config.difficulty === "expert") { $(".pageTest #testModesNotice").append( `` diff --git a/frontend/src/ts/event-handlers/test.ts b/frontend/src/ts/event-handlers/test.ts index 89a87a650..5bdcae658 100644 --- a/frontend/src/ts/event-handlers/test.ts +++ b/frontend/src/ts/event-handlers/test.ts @@ -21,6 +21,12 @@ $(".pageTest").on("click", "#testModesNotice .textButton", async (event) => { (await getCommandline()).show({ subgroupOverride: attr }); }); +$(".pageTest").on("click", "#testModesNotice .textButton", async (event) => { + const attr = $(event.currentTarget).attr("commandId"); + if (attr === undefined) return; + (await getCommandline()).show({ commandOverride: attr }); +}); + $(".pageTest").on("click", "#testConfig .wordCount .textButton", (e) => { const wrd = $(e.currentTarget).attr("wordCount"); if (wrd === "custom") { diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index dae48277a..909ef1f1b 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -32,11 +32,12 @@ import { import { getActiveFunboxNames } from "../test/funbox/list"; import { SnapshotPreset } from "../constants/default-snapshot"; import { LayoutsList } from "../constants/layouts"; -import { DataArrayPartial } from "slim-select/store"; +import { DataArrayPartial, Optgroup } from "slim-select/store"; type SettingsGroups = Record>; let customLayoutFluidSelect: SlimSelect | undefined; +let customPolyglotSelect: SlimSelect | undefined; export const groups: SettingsGroups = {}; @@ -476,21 +477,12 @@ async function fillSettingsPage(): Promise { ".pageSettings .section[data-config-name='language'] select" ) as Element; - let html = ""; - if (languageGroups) { - for (const group of languageGroups) { - html += ``; - for (const language of group.languages) { - const selected = language === Config.language ? "selected" : ""; - const text = Strings.getLanguageDisplayString(language); - html += ``; - } - html += ``; - } - } - element.innerHTML = html; new SlimSelect({ select: element, + data: getLanguageDropdownData( + languageGroups ?? [], + (language) => language === Config.language + ), settings: { searchPlaceholder: "search", }, @@ -687,11 +679,11 @@ async function fillSettingsPage(): Promise { Config.keymapSize ); - const customLayoutfluidElement = document.querySelector( - ".pageSettings .section[data-config-name='customLayoutfluid'] select" - ) as Element; - if (customLayoutFluidSelect === undefined) { + const customLayoutfluidElement = document.querySelector( + ".pageSettings .section[data-config-name='customLayoutfluid'] select" + ) as Element; + customLayoutFluidSelect = new SlimSelect({ select: customLayoutfluidElement, settings: { keepOrder: true }, @@ -706,6 +698,27 @@ async function fillSettingsPage(): Promise { }); } + if (customPolyglotSelect === undefined) { + const customPolyglotElement = document.querySelector( + ".pageSettings .section[data-config-name='customPolyglot'] select" + ) as Element; + + customPolyglotSelect = new SlimSelect({ + select: customPolyglotElement, + data: getLanguageDropdownData(languageGroups ?? [], (language) => + Config.customPolyglot.includes(language) + ), + events: { + afterChange: (newVal): void => { + const customPolyglot = newVal.map((it) => it.value); + if (customPolyglot.toSorted() !== Config.customPolyglot.toSorted()) { + void UpdateConfig.setCustomPolyglot(customPolyglot); + } + }, + }, + }); + } + $(".pageSettings .section[data-config-name='tapeMargin'] input").val( Config.tapeMargin ); @@ -911,6 +924,13 @@ export async function update(groupUpdate = true): Promise { ) { customLayoutFluidSelect.setData(getLayoutfluidDropdownData()); } + + if ( + customPolyglotSelect !== undefined && + customPolyglotSelect.getSelected() !== Config.customPolyglot + ) { + customPolyglotSelect.setSelected(Config.customPolyglot); + } } function toggleSettingsGroup(groupName: string): void { const groupEl = $(`.pageSettings .settingsGroup.${groupName}`); @@ -1359,6 +1379,23 @@ export function setEventDisabled(value: boolean): void { configEventDisabled = value; } +function getLanguageDropdownData( + languageGroups: JSONData.LanguageGroup[], + isActive: (val: string) => boolean +): DataArrayPartial { + return languageGroups.map( + (group) => + ({ + label: group.name, + options: group.languages.map((language) => ({ + text: Strings.getLanguageDisplayString(language), + value: language, + selected: isActive(language), + })), + } as Optgroup) + ); +} + function getLayoutfluidDropdownData(): DataArrayPartial { const customLayoutfluidActive = Config.customLayoutfluid.split("#"); return [ diff --git a/frontend/src/ts/test/funbox/funbox-functions.ts b/frontend/src/ts/test/funbox/funbox-functions.ts index 4a4da520b..103f11251 100644 --- a/frontend/src/ts/test/funbox/funbox-functions.ts +++ b/frontend/src/ts/test/funbox/funbox-functions.ts @@ -645,6 +645,16 @@ const list: Partial> = { return word.toUpperCase(); }, }, + polyglot: { + async withWords(_words) { + const promises = Config.customPolyglot.map(JSONData.getLanguage); + + const languages = await Promise.all(promises); + const wordSet = languages.flatMap((it) => it.words); + Arrays.shuffle(wordSet); + return new Wordset(wordSet); + }, + }, }; export function getFunboxFunctions(): Record { diff --git a/packages/contracts/src/schemas/configs.ts b/packages/contracts/src/schemas/configs.ts index d36e4995b..a810a7bb0 100644 --- a/packages/contracts/src/schemas/configs.ts +++ b/packages/contracts/src/schemas/configs.ts @@ -198,6 +198,9 @@ export type CustomBackgroundFilter = z.infer< export const CustomLayoutFluidSchema = z.string().regex(/^[0-9a-zA-Z_#]+$/); //TODO better regex export type CustomLayoutFluid = z.infer; +export const CustomPolyglotSchema = z.array(LanguageSchema).min(1); +export type CustomPolyglot = z.infer; + export const MonkeyPowerLevelSchema = z.enum(["off", "1", "2", "3", "4"]); export type MonkeyPowerLevel = z.infer; @@ -383,6 +386,7 @@ export const ConfigSchema = z lazyMode: z.boolean(), showAverage: ShowAverageSchema, maxLineWidth: MaxLineWidthSchema, + customPolyglot: CustomPolyglotSchema, } satisfies Record) .strict(); @@ -496,6 +500,7 @@ export const ConfigGroupsLiteral = { lazyMode: "input", showAverage: "hideElements", maxLineWidth: "appearance", + customPolyglot: "behavior", } as const satisfies Record; export type ConfigGroups = typeof ConfigGroupsLiteral; diff --git a/packages/funbox/__test__/validation.spec.ts b/packages/funbox/__test__/validation.spec.ts index 71978ba59..9aad4898e 100644 --- a/packages/funbox/__test__/validation.spec.ts +++ b/packages/funbox/__test__/validation.spec.ts @@ -114,5 +114,46 @@ describe("validation", () => { true ); }); + describe("should validate two funboxes modifying the wordset", () => { + const testCases = [ + { + firstFunction: "withWords", + secondFunction: "withWords", + compatible: false, + }, + { + firstFunction: "withWords", + secondFunction: "getWord", + compatible: false, + }, + { + firstFunction: "getWord", + secondFunction: "pullSection", + compatible: false, + }, + ]; + + it.for(testCases)( + `expect $firstFunction and $secondFunction to be compatible $compatible`, + ({ firstFunction, secondFunction, compatible }) => { + //GIVEN + getFunboxMock.mockReturnValueOnce([ + { + name: "plus_one", + frontendFunctions: [firstFunction], + } as FunboxMetadata, + { + name: "plus_two", + frontendFunctions: [secondFunction], + } as FunboxMetadata, + ]); + + //WHEN / THEN + expect(Validation.checkCompatibility(["plus_one", "plus_two"])).toBe( + compatible + ); + } + ); + }); }); }); diff --git a/packages/funbox/src/list.ts b/packages/funbox/src/list.ts index 5ea4a20c5..2334b3776 100644 --- a/packages/funbox/src/list.ts +++ b/packages/funbox/src/list.ts @@ -444,6 +444,14 @@ const list: Record = { frontendFunctions: ["alterText"], name: "ALL_CAPS", }, + polyglot: { + description: "Use words from multiple languages in a single test.", + canGetPb: false, + difficultyLevel: 1, + properties: ["ignoresLanguage"], + frontendFunctions: ["withWords"], + name: "polyglot", + }, }; // oxlint doesnt understand ts overloading diff --git a/packages/funbox/src/types.ts b/packages/funbox/src/types.ts index f9f065e85..9ce039460 100644 --- a/packages/funbox/src/types.ts +++ b/packages/funbox/src/types.ts @@ -41,7 +41,8 @@ export type FunboxName = | "ddoouubblleedd" | "instant_messaging" | "underscore_spaces" - | "ALL_CAPS"; + | "ALL_CAPS" + | "polyglot"; export type FunboxForcedConfig = Record; diff --git a/packages/funbox/src/validation.ts b/packages/funbox/src/validation.ts index 138b3573d..f6de2d000 100644 --- a/packages/funbox/src/validation.ts +++ b/packages/funbox/src/validation.ts @@ -19,8 +19,8 @@ export function checkCompatibility( const oneWordModifierMax = funboxesToCheck.filter( (f) => - f.frontendFunctions?.includes("getWord") ?? - f.frontendFunctions?.includes("pullSection") ?? + f.frontendFunctions?.includes("getWord") || + f.frontendFunctions?.includes("pullSection") || f.frontendFunctions?.includes("withWords") ).length <= 1; const oneWordOrderMax =