mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-04 12:34:53 +08:00
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 <jack@monkeytype.com>
This commit is contained in:
parent
a8ce609f0d
commit
da671337c5
14 changed files with 229 additions and 22 deletions
|
@ -351,6 +351,19 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="sectionSpacer"></div>
|
||||
<div class="section" data-config-name="customPolyglot">
|
||||
<div class="groupTitle">
|
||||
<i class="fas fa-language"></i>
|
||||
<span>polyglot languages</span>
|
||||
</div>
|
||||
<div class="text">
|
||||
Select which languages you want the polyglot funbox to use.
|
||||
</div>
|
||||
<div class="inputs">
|
||||
<select multiple></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sectionSpacer"></div>
|
||||
</div>
|
||||
<button id="group_input" class="text sectionGroupTitle" group="input">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
|
|
|
@ -53,6 +53,7 @@ function addCommandlineBackground(): void {
|
|||
|
||||
type ShowSettings = {
|
||||
subgroupOverride?: CommandsSubgroup | string;
|
||||
commandOverride?: string;
|
||||
singleListOverride?: boolean;
|
||||
};
|
||||
|
||||
|
@ -102,6 +103,25 @@ export function show(
|
|||
subgroupOverride = null;
|
||||
usingSingleList = Config.singleListCommandLine === "on";
|
||||
}
|
||||
|
||||
let showInputCommand: Command | undefined = undefined;
|
||||
|
||||
if (settings?.commandOverride !== undefined) {
|
||||
const command = (await getList()).filter(
|
||||
(c) => c.id === settings.commandOverride
|
||||
)[0];
|
||||
if (command === undefined) {
|
||||
Notifications.add(`Command ${settings.commandOverride} not found`, 0);
|
||||
} else if (command?.input !== true) {
|
||||
Notifications.add(
|
||||
`Command ${settings.commandOverride} is not an input command`,
|
||||
0
|
||||
);
|
||||
} else {
|
||||
showInputCommand = command;
|
||||
}
|
||||
}
|
||||
|
||||
if (settings?.singleListOverride) {
|
||||
usingSingleList = settings.singleListOverride;
|
||||
}
|
||||
|
@ -114,6 +134,20 @@ export function show(
|
|||
await updateActiveCommand();
|
||||
setTimeout(() => {
|
||||
keepActiveCommandInView();
|
||||
if (showInputCommand) {
|
||||
const escaped =
|
||||
showInputCommand.display.split("</i>")[1] ??
|
||||
showInputCommand.display;
|
||||
mode = "input";
|
||||
inputModeParams = {
|
||||
command: showInputCommand,
|
||||
placeholder: escaped,
|
||||
value: showInputCommand.defaultValue?.() ?? "",
|
||||
icon: showInputCommand.icon ?? "fa-chevron-right",
|
||||
};
|
||||
updateInput(inputModeParams.value as string);
|
||||
hideCommands();
|
||||
}
|
||||
}, 1);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -226,6 +226,19 @@ export const commands: CommandsSubgroup = {
|
|||
UpdateConfig.setCustomLayoutfluid(input);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "changeCustomPolyglot",
|
||||
display: "Polyglot languages...",
|
||||
defaultValue: (): string => {
|
||||
return Config.customPolyglot.join(" ");
|
||||
},
|
||||
input: true,
|
||||
icon: "fa-language",
|
||||
exec: ({ input }): void => {
|
||||
if (input === undefined) return;
|
||||
void UpdateConfig.setCustomPolyglot(input.split(" "));
|
||||
},
|
||||
},
|
||||
|
||||
//input
|
||||
...FreedomModeCommands,
|
||||
|
|
|
@ -1906,6 +1906,26 @@ export function setCustomLayoutfluid(
|
|||
return true;
|
||||
}
|
||||
|
||||
export function setCustomPolyglot(
|
||||
value: ConfigSchemas.CustomPolyglot,
|
||||
nosave?: boolean
|
||||
): boolean {
|
||||
if (
|
||||
!isConfigValueValid(
|
||||
"customPolyglot",
|
||||
value,
|
||||
ConfigSchemas.CustomPolyglotSchema
|
||||
)
|
||||
)
|
||||
return false;
|
||||
|
||||
config.customPolyglot = value;
|
||||
saveToLocalStorage("customPolyglot", nosave);
|
||||
ConfigEvent.dispatch("customPolyglot", config.customPolyglot);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function setCustomBackgroundSize(
|
||||
value: ConfigSchemas.CustomBackgroundSize,
|
||||
nosave?: boolean
|
||||
|
@ -2012,6 +2032,7 @@ export async function apply(
|
|||
true
|
||||
);
|
||||
setCustomLayoutfluid(configObj.customLayoutfluid, true);
|
||||
setCustomPolyglot(configObj.customPolyglot, true);
|
||||
setCustomBackground(configObj.customBackground, true);
|
||||
setCustomBackgroundSize(configObj.customBackgroundSize, true);
|
||||
setCustomBackgroundFilter(configObj.customBackgroundFilter, true);
|
||||
|
|
|
@ -93,6 +93,7 @@ const obj = {
|
|||
customBackgroundSize: "cover",
|
||||
customBackgroundFilter: [0, 1, 1, 1],
|
||||
customLayoutfluid: "qwerty#dvorak#colemak",
|
||||
customPolyglot: ["english", "spanish", "french", "german"],
|
||||
monkeyPowerLevel: "off",
|
||||
minBurst: "off",
|
||||
minBurstCustomSpeed: 100,
|
||||
|
|
|
@ -9,6 +9,7 @@ import { isAuthenticated } from "../firebase";
|
|||
import * as CustomTextState from "../states/custom-text-name";
|
||||
import { getLanguageDisplayString } from "../utils/strings";
|
||||
import Format from "../utils/format";
|
||||
import { getActiveFunboxNames } from "../test/funbox/list";
|
||||
|
||||
ConfigEvent.subscribe((eventKey) => {
|
||||
if (
|
||||
|
@ -25,6 +26,7 @@ ConfigEvent.subscribe((eventKey) => {
|
|||
"showAverage",
|
||||
"typingSpeedUnit",
|
||||
"quickRestart",
|
||||
"changeCustomPolyglot",
|
||||
].includes(eventKey)
|
||||
) {
|
||||
void update();
|
||||
|
@ -91,7 +93,9 @@ export async function update(): Promise<void> {
|
|||
);
|
||||
}
|
||||
|
||||
if (Config.mode !== "zen") {
|
||||
const usingPolyglot = getActiveFunboxNames().includes("polyglot");
|
||||
|
||||
if (Config.mode !== "zen" && !usingPolyglot) {
|
||||
$(".pageTest #testModesNotice").append(
|
||||
`<button class="textButton" commands="languages"><i class="fas fa-globe-americas"></i>${getLanguageDisplayString(
|
||||
Config.language,
|
||||
|
@ -100,6 +104,19 @@ export async function update(): Promise<void> {
|
|||
);
|
||||
}
|
||||
|
||||
if (usingPolyglot) {
|
||||
const languages = Config.customPolyglot
|
||||
.map((lang) => {
|
||||
const langDisplay = getLanguageDisplayString(lang, true);
|
||||
return langDisplay;
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
$(".pageTest #testModesNotice").append(
|
||||
`<button class="textButton" commandId="changeCustomPolyglot"><i class="fas fa-globe-americas"></i>${languages}</button>`
|
||||
);
|
||||
}
|
||||
|
||||
if (Config.difficulty === "expert") {
|
||||
$(".pageTest #testModesNotice").append(
|
||||
`<button class="textButton" commands="difficulty"><i class="fas fa-star-half-alt"></i>expert</button>`
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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<T extends ConfigValue> = Record<string, SettingsGroup<T>>;
|
||||
|
||||
let customLayoutFluidSelect: SlimSelect | undefined;
|
||||
let customPolyglotSelect: SlimSelect | undefined;
|
||||
|
||||
export const groups: SettingsGroups<ConfigValue> = {};
|
||||
|
||||
|
@ -476,21 +477,12 @@ async function fillSettingsPage(): Promise<void> {
|
|||
".pageSettings .section[data-config-name='language'] select"
|
||||
) as Element;
|
||||
|
||||
let html = "";
|
||||
if (languageGroups) {
|
||||
for (const group of languageGroups) {
|
||||
html += `<optgroup label="${group.name}">`;
|
||||
for (const language of group.languages) {
|
||||
const selected = language === Config.language ? "selected" : "";
|
||||
const text = Strings.getLanguageDisplayString(language);
|
||||
html += `<option value="${language}" ${selected}>${text}</option>`;
|
||||
}
|
||||
html += `</optgroup>`;
|
||||
}
|
||||
}
|
||||
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<void> {
|
|||
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<void> {
|
|||
});
|
||||
}
|
||||
|
||||
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<void> {
|
|||
) {
|
||||
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 [
|
||||
|
|
|
@ -645,6 +645,16 @@ const list: Partial<Record<FunboxName, FunboxFunctions>> = {
|
|||
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<FunboxName, FunboxFunctions> {
|
||||
|
|
|
@ -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<typeof CustomLayoutFluidSchema>;
|
||||
|
||||
export const CustomPolyglotSchema = z.array(LanguageSchema).min(1);
|
||||
export type CustomPolyglot = z.infer<typeof CustomPolyglotSchema>;
|
||||
|
||||
export const MonkeyPowerLevelSchema = z.enum(["off", "1", "2", "3", "4"]);
|
||||
export type MonkeyPowerLevel = z.infer<typeof MonkeyPowerLevelSchema>;
|
||||
|
||||
|
@ -383,6 +386,7 @@ export const ConfigSchema = z
|
|||
lazyMode: z.boolean(),
|
||||
showAverage: ShowAverageSchema,
|
||||
maxLineWidth: MaxLineWidthSchema,
|
||||
customPolyglot: CustomPolyglotSchema,
|
||||
} satisfies Record<string, ZodSchema>)
|
||||
.strict();
|
||||
|
||||
|
@ -496,6 +500,7 @@ export const ConfigGroupsLiteral = {
|
|||
lazyMode: "input",
|
||||
showAverage: "hideElements",
|
||||
maxLineWidth: "appearance",
|
||||
customPolyglot: "behavior",
|
||||
} as const satisfies Record<ConfigKey, ConfigGroupName>;
|
||||
|
||||
export type ConfigGroups = typeof ConfigGroupsLiteral;
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -444,6 +444,14 @@ const list: Record<FunboxName, FunboxMetadata> = {
|
|||
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
|
||||
|
|
|
@ -41,7 +41,8 @@ export type FunboxName =
|
|||
| "ddoouubblleedd"
|
||||
| "instant_messaging"
|
||||
| "underscore_spaces"
|
||||
| "ALL_CAPS";
|
||||
| "ALL_CAPS"
|
||||
| "polyglot";
|
||||
|
||||
export type FunboxForcedConfig = Record<string, string[] | boolean[]>;
|
||||
|
||||
|
|
|
@ -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 =
|
||||
|
|
Loading…
Add table
Reference in a new issue