123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858 |
- import { debounce, isScrollable, type Stringifiable } from "@sv443-network/userutils";
- import { defaultData, getFeature, getFeatures, setFeatures } from "../config.js";
- import { buildNumber, host, mode, scriptInfo } from "../constants.js";
- import { featInfo, disableBeforeUnload } from "../features/index.js";
- import { error, getResourceUrl, info, log, resourceToHTMLString, getLocale, hasKey, initTranslations, setLocale, t, arrayWithSeparators, tp, type TrKey, onInteraction, getDomain, copyToClipboard, warn } from "../utils/index.js";
- import { siteEvents } from "../siteEvents.js";
- import { getChangelogDialog, getExportDialog, getFeatHelpDialog, getImportDialog } from "../dialogs/index.js";
- import type { FeatureCategory, FeatureKey, FeatureConfig, HotkeyObj, FeatureInfo } from "../types.js";
- import "./menu_old.css";
- import { BytmDialog, createHotkeyInput, createToggleInput, openDialogs, setCurrentDialogId } from "../components/index.js";
- import { emitInterface } from "../interface.js";
- import pkg from "../../package.json" with { type: "json" };
- //#region create menu
- let isCfgMenuMounted = false;
- export let isCfgMenuOpen = false;
- /** Threshold in pixels from the top of the options container that dictates for how long the scroll indicator is shown */
- const scrollIndicatorOffsetThreshold = 30;
- let scrollIndicatorEnabled = true;
- /** Locale at the point of initializing the config menu */
- let initLocale: string | undefined;
- /** Stringified config at the point of initializing the config menu */
- let initConfig: FeatureConfig | undefined;
- /** Timeout id for the "copied" text in the hidden value copy button */
- let hiddenCopiedTxtTimeout: ReturnType<typeof setTimeout> | undefined;
- /**
- * Adds an element to open the BetterYTM menu
- * @deprecated to be replaced with new menu - see https://github.com/Sv443/BetterYTM/issues/23
- */
- async function mountCfgMenu() {
- if(isCfgMenuMounted)
- return;
- isCfgMenuMounted = true;
- initLocale = getFeature("locale");
- initConfig = getFeatures();
- const initLangReloadText = t("lang_changed_prompt_reload");
- //#region bg & container
- const backgroundElem = document.createElement("div");
- backgroundElem.id = "bytm-cfg-menu-bg";
- backgroundElem.classList.add("bytm-menu-bg");
- backgroundElem.ariaLabel = backgroundElem.title = t("close_menu_tooltip");
- backgroundElem.style.visibility = "hidden";
- backgroundElem.style.display = "none";
- backgroundElem.addEventListener("click", (e) => {
- if(isCfgMenuOpen && (e.target as HTMLElement)?.id === "bytm-cfg-menu-bg")
- closeCfgMenu(e);
- });
- document.body.addEventListener("keydown", (e) => {
- if(isCfgMenuOpen && e.key === "Escape" && BytmDialog.getCurrentDialogId() === "cfg-menu")
- closeCfgMenu(e);
- });
- const menuContainer = document.createElement("div");
- menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
- menuContainer.classList.add("bytm-menu");
- menuContainer.id = "bytm-cfg-menu";
- //#region title bar
- const headerElem = document.createElement("div");
- headerElem.classList.add("bytm-menu-header");
- const titleCont = document.createElement("div");
- titleCont.classList.add("bytm-menu-titlecont");
- titleCont.role = "heading";
- titleCont.ariaLevel = "1";
- const titleElem = document.createElement("h2");
- titleElem.classList.add("bytm-menu-title");
- const titleTextElem = document.createElement("div");
- titleTextElem.textContent = t("config_menu_title", scriptInfo.name);
- titleElem.appendChild(titleTextElem);
- const linksCont = document.createElement("div");
- linksCont.id = "bytm-menu-linkscont";
- linksCont.role = "navigation";
- const linkTitlesShort = {
- github: "GitHub",
- greasyfork: "GreasyFork",
- openuserjs: "OpenUserJS",
- discord: "Discord",
- };
- const addLink = (imgSrc: string, href: string, title: string, titleKey: keyof typeof linkTitlesShort) => {
- const anchorElem = document.createElement("a");
- anchorElem.classList.add("bytm-menu-link", "bytm-no-select");
- anchorElem.rel = "noopener noreferrer";
- anchorElem.href = href;
- anchorElem.target = "_blank";
- anchorElem.tabIndex = 0;
- anchorElem.role = "button";
- anchorElem.ariaLabel = anchorElem.title = title;
- const extendedAnchorEl = document.createElement("a");
- extendedAnchorEl.classList.add("bytm-menu-link", "extended-link", "bytm-no-select");
- extendedAnchorEl.rel = "noopener noreferrer";
- extendedAnchorEl.href = href;
- extendedAnchorEl.target = "_blank";
- extendedAnchorEl.tabIndex = -1;
- extendedAnchorEl.textContent = linkTitlesShort[titleKey];
- extendedAnchorEl.ariaLabel = extendedAnchorEl.title = title;
- const imgElem = document.createElement("img");
- imgElem.classList.add("bytm-menu-img");
- imgElem.src = imgSrc;
- anchorElem.appendChild(imgElem);
- anchorElem.appendChild(extendedAnchorEl);
- linksCont.appendChild(anchorElem);
- };
- const links: [name: string, ...Parameters<typeof addLink>][] = [
- ["github", await getResourceUrl("img-github"), scriptInfo.namespace, t("open_github", scriptInfo.name), "github"],
- ["greasyfork", await getResourceUrl("img-greasyfork"), pkg.hosts.greasyfork, t("open_greasyfork", scriptInfo.name), "greasyfork"],
- ["openuserjs", await getResourceUrl("img-openuserjs"), pkg.hosts.openuserjs, t("open_openuserjs", scriptInfo.name), "openuserjs"],
- ];
- const hostLink = links.find(([name]) => name === host);
- const otherLinks = links.filter(([name]) => name !== host);
- const reorderedLinks = hostLink ? [hostLink, ...otherLinks] : links;
- for(const [, ...args] of reorderedLinks)
- addLink(...args);
- addLink(await getResourceUrl("img-discord"), "https://dc.sv443.net/", t("open_discord"), "discord");
- const closeElem = document.createElement("img");
- closeElem.classList.add("bytm-menu-close");
- closeElem.role = "button";
- closeElem.tabIndex = 0;
- closeElem.src = await getResourceUrl("img-close");
- closeElem.ariaLabel = closeElem.title = t("close_menu_tooltip");
- onInteraction(closeElem, closeCfgMenu);
- titleCont.appendChild(titleElem);
- titleCont.appendChild(linksCont);
- headerElem.appendChild(titleCont);
- headerElem.appendChild(closeElem);
- //#region footer
- const footerCont = document.createElement("div");
- footerCont.classList.add("bytm-menu-footer-cont");
- const reloadFooterCont = document.createElement("div");
- const reloadFooterEl = document.createElement("div");
- reloadFooterEl.classList.add("bytm-menu-footer", "hidden");
- reloadFooterEl.setAttribute("aria-hidden", "true");
- reloadFooterEl.textContent = t("reload_hint");
- reloadFooterEl.role = "alert";
- reloadFooterEl.ariaLive = "polite";
- const reloadTxtEl = document.createElement("button");
- reloadTxtEl.classList.add("bytm-btn");
- reloadTxtEl.style.marginLeft = "10px";
- reloadTxtEl.textContent = t("reload_now");
- reloadTxtEl.ariaLabel = reloadTxtEl.title = t("reload_tooltip");
- reloadTxtEl.addEventListener("click", () => {
- closeCfgMenu();
- disableBeforeUnload();
- location.reload();
- });
- reloadFooterEl.appendChild(reloadTxtEl);
- reloadFooterCont.appendChild(reloadFooterEl);
- const exportElem = document.createElement("button");
- exportElem.classList.add("bytm-btn");
- exportElem.ariaLabel = exportElem.title = t("export_tooltip");
- exportElem.textContent = t("export");
- onInteraction(exportElem, async () => {
- const dlg = await getExportDialog();
- dlg.on("close", openCfgMenu);
- await dlg.mount();
- closeCfgMenu(undefined, false);
- await dlg.open();
- });
- const importElem = document.createElement("button");
- importElem.classList.add("bytm-btn");
- importElem.ariaLabel = importElem.title = t("import_tooltip");
- importElem.textContent = t("import");
- onInteraction(importElem, async () => {
- const dlg = await getImportDialog();
- dlg.on("close", openCfgMenu);
- await dlg.mount();
- closeCfgMenu(undefined, false);
- await dlg.open();
- });
- const buttonsCont = document.createElement("div");
- buttonsCont.classList.add("bytm-menu-footer-buttons-cont");
- buttonsCont.appendChild(exportElem);
- buttonsCont.appendChild(importElem);
- footerCont.appendChild(reloadFooterCont);
- footerCont.appendChild(buttonsCont);
- //#region feature list
- const featuresCont = document.createElement("div");
- featuresCont.id = "bytm-menu-opts";
- const onCfgChange = async (
- key: keyof typeof defaultData,
- initialVal: string | number | boolean | HotkeyObj,
- newVal: string | number | boolean | HotkeyObj,
- ) => {
- const fmt = (val: unknown) => typeof val === "object" ? JSON.stringify(val) : String(val);
- info(`Feature config changed at key '${key}', from value '${fmt(initialVal)}' to '${fmt(newVal)}'`);
- const featConf = JSON.parse(JSON.stringify(getFeatures())) as FeatureConfig;
- featConf[key] = newVal as never;
- const changedKeys = initConfig ? Object.keys(featConf).filter((k) =>
- typeof featConf[k as FeatureKey] !== "object"
- && featConf[k as FeatureKey] !== initConfig![k as FeatureKey]
- ) : [];
- const requiresReload =
- // @ts-ignore
- changedKeys.some((k) => featInfo[k as keyof typeof featInfo]?.reloadRequired !== false);
- await setFeatures(featConf);
- // @ts-ignore
- featInfo[key]?.change?.(key, initialVal, newVal);
- if(requiresReload) {
- reloadFooterEl.classList.remove("hidden");
- reloadFooterEl.setAttribute("aria-hidden", "false");
- }
- else if(!requiresReload) {
- reloadFooterEl.classList.add("hidden");
- reloadFooterEl.setAttribute("aria-hidden", "true");
- }
- if(initLocale !== featConf.locale) {
- await initTranslations(featConf.locale);
- setLocale(featConf.locale);
- const newText = t("lang_changed_prompt_reload");
- const confirmText = newText !== initLangReloadText ? `${newText}\n\n────────────────────────────────\n\n${initLangReloadText}` : newText;
- if(confirm(confirmText)) {
- closeCfgMenu();
- disableBeforeUnload();
- location.reload();
- }
- }
- else if(getLocale() !== featConf.locale)
- setLocale(featConf.locale);
- siteEvents.emit("configOptionChanged", key, initialVal, newVal);
- };
- /** Call whenever the feature config is changed */
- const confChanged = debounce(onCfgChange, 333, "falling");
- const featureCfg = getFeatures();
- const featureCfgWithCategories = Object.entries(featInfo)
- .reduce(
- (acc, [key, { category }]) => {
- if(!acc[category])
- acc[category] = {} as Record<FeatureKey, unknown>;
- acc[category][key as FeatureKey] = featureCfg[key as FeatureKey];
- return acc;
- },
- {} as Record<FeatureCategory, Record<FeatureKey, unknown>>,
- );
- const fmtVal = (v: unknown, key: FeatureKey) => {
- try {
- // @ts-ignore
- const renderValue = typeof featInfo?.[key]?.renderValue === "function" ? featInfo[key].renderValue : undefined;
- const retVal = (typeof v === "object" ? JSON.stringify(v) : String(v)).trim();
- return renderValue ? renderValue(retVal) : retVal;
- }
- catch {
- // because stringify throws on circular refs
- return String(v).trim();
- }
- };
- for(const category in featureCfgWithCategories) {
- const featObj = featureCfgWithCategories[category as FeatureCategory];
- const catHeaderElem = document.createElement("h3");
- catHeaderElem.classList.add("bytm-ftconf-category-header");
- catHeaderElem.role = "heading";
- catHeaderElem.ariaLevel = "2";
- catHeaderElem.tabIndex = 0;
- catHeaderElem.textContent = `${t(`feature_category_${category}`)}:`;
- featuresCont.appendChild(catHeaderElem);
- for(const featKey in featObj) {
- const ftInfo = featInfo[featKey as FeatureKey] as FeatureInfo[keyof typeof featureCfg];
- if(!ftInfo || ("hidden" in ftInfo && ftInfo.hidden === true))
- continue;
- if(ftInfo.advanced && !featureCfg.advancedMode)
- continue;
- const { type, default: ftDefault } = ftInfo;
- const step = "step" in ftInfo ? ftInfo.step : undefined;
- const val = featureCfg[featKey as FeatureKey];
- const initialVal = val ?? ftDefault ?? undefined;
- const ftConfElem = document.createElement("div");
- ftConfElem.classList.add("bytm-ftitem");
- {
- const featLeftSideElem = document.createElement("div");
- featLeftSideElem.classList.add("bytm-ftitem-leftside");
- if(getFeature("advancedMode")) {
- const defVal = fmtVal(ftDefault, featKey as FeatureKey);
- const extraTxts = [
- `default: ${defVal.length === 0 ? "(undefined)" : defVal}`,
- ];
- "min" in ftInfo && extraTxts.push(`min: ${ftInfo.min}`);
- "max" in ftInfo && extraTxts.push(`max: ${ftInfo.max}`);
- "step" in ftInfo && extraTxts.push(`step: ${ftInfo.step}`);
- const rel = "reloadRequired" in ftInfo && ftInfo.reloadRequired !== false ? " (reload required)" : "";
- const adv = ftInfo.advanced ? " (advanced feature)" : "";
- featLeftSideElem.title = `${featKey}${rel}${adv}${extraTxts.length > 0 ? `\n${extraTxts.join(" - ")}` : ""}`;
- }
- const textElem = document.createElement("span");
- textElem.tabIndex = 0;
- textElem.textContent = t(`feature_desc_${featKey}`);
- let adornmentElem: undefined | HTMLElement;
- const adornContent = ftInfo.textAdornment?.();
- const adornContentAw = adornContent instanceof Promise ? await adornContent : adornContent;
- if((typeof adornContent === "string" || adornContent instanceof Promise) && typeof adornContentAw !== "undefined") {
- adornmentElem = document.createElement("span");
- adornmentElem.id = `bytm-ftitem-${featKey}-adornment`;
- adornmentElem.classList.add("bytm-ftitem-adornment");
- adornmentElem.innerHTML = adornContentAw;
- }
- let helpElem: undefined | HTMLDivElement;
- // @ts-ignore
- const hasHelpTextFunc = typeof featInfo[featKey as keyof typeof featInfo]?.helpText === "function";
- // @ts-ignore
- const helpTextVal: string | undefined = hasHelpTextFunc && featInfo[featKey as keyof typeof featInfo]!.helpText();
- if(hasKey(`feature_helptext_${featKey}`) || (helpTextVal && hasKey(helpTextVal))) {
- const helpElemImgHtml = await resourceToHTMLString("icon-help");
- if(helpElemImgHtml) {
- helpElem = document.createElement("div");
- helpElem.classList.add("bytm-ftitem-help-btn", "bytm-generic-btn");
- helpElem.ariaLabel = helpElem.title = t("feature_help_button_tooltip");
- helpElem.role = "button";
- helpElem.tabIndex = 0;
- helpElem.innerHTML = helpElemImgHtml;
- onInteraction(helpElem, async (e: MouseEvent | KeyboardEvent) => {
- e.preventDefault();
- e.stopPropagation();
- await (await getFeatHelpDialog({ featKey: featKey as FeatureKey })).open();
- });
- }
- else {
- error(`Couldn't create help button SVG element for feature '${featKey}'`);
- }
- }
- adornmentElem && featLeftSideElem.appendChild(adornmentElem);
- featLeftSideElem.appendChild(textElem);
- helpElem && featLeftSideElem.appendChild(helpElem);
- ftConfElem.appendChild(featLeftSideElem);
- }
- {
- let inputType: string | undefined = "text";
- let inputTag: string | undefined = "input";
- switch(type)
- {
- case "toggle":
- inputTag = undefined;
- inputType = undefined;
- break;
- case "slider":
- inputType = "range";
- break;
- case "number":
- inputType = "number";
- break;
- case "text":
- inputType = "text";
- break;
- case "select":
- inputTag = "select";
- inputType = undefined;
- break;
- case "hotkey":
- inputTag = undefined;
- inputType = undefined;
- break;
- case "button":
- inputTag = undefined;
- inputType = undefined;
- break;
- }
- const inputElemId = `bytm-ftconf-${featKey}-input`;
- const ctrlElem = document.createElement("span");
- ctrlElem.classList.add("bytm-ftconf-ctrl");
- let advCopyHiddenCont: HTMLElement | undefined;
- if((getFeature("advancedMode") || mode === "development") && ftInfo.valueHidden) {
- const advCopyHintElem = document.createElement("span");
- advCopyHintElem.classList.add("bytm-ftconf-adv-copy-hint");
- advCopyHintElem.textContent = t("copied");
- advCopyHintElem.role = "status";
- advCopyHintElem.style.display = "none";
- const advCopyHiddenBtn = document.createElement("button");
- advCopyHiddenBtn.classList.add("bytm-ftconf-adv-copy-btn", "bytm-btn");
- advCopyHiddenBtn.tabIndex = 0;
- advCopyHiddenBtn.textContent = t("copy_hidden_value");
- advCopyHiddenBtn.ariaLabel = advCopyHiddenBtn.title = t("copy_hidden_tooltip");
- const copyHiddenInteraction = (e: MouseEvent | KeyboardEvent) => {
- e.preventDefault();
- e.stopPropagation();
- copyToClipboard(getFeatures()[featKey as keyof FeatureConfig] as Stringifiable);
- advCopyHintElem.style.display = "inline";
- if(typeof hiddenCopiedTxtTimeout === "undefined") {
- hiddenCopiedTxtTimeout = setTimeout(() => {
- advCopyHintElem.style.display = "none";
- hiddenCopiedTxtTimeout = undefined;
- }, 3000);
- }
- };
- onInteraction(advCopyHiddenBtn, copyHiddenInteraction);
- advCopyHiddenCont = document.createElement("span");
- advCopyHiddenCont.appendChild(advCopyHintElem);
- advCopyHiddenCont.appendChild(advCopyHiddenBtn);
- }
- advCopyHiddenCont && ctrlElem.appendChild(advCopyHiddenCont);
- if(inputTag) {
- // standard input element:
- const inputElem = document.createElement(inputTag) as HTMLInputElement;
- inputElem.classList.add("bytm-ftconf-input");
- inputElem.id = inputElemId;
- if(inputType)
- inputElem.type = inputType;
- if("min" in ftInfo && typeof ftInfo.min !== "undefined")
- inputElem.min = String(ftInfo.min);
- if("max" in ftInfo && typeof ftInfo.max !== "undefined")
- inputElem.max = String(ftInfo.max);
- if(typeof initialVal !== "undefined")
- inputElem.value = String(initialVal);
- if(type === "text" && ftInfo.valueHidden) {
- inputElem.type = "password";
- inputElem.autocomplete = "off";
- }
- if(type === "number" || type === "slider" && step)
- inputElem.step = String(step);
- if(type === "toggle" && typeof initialVal !== "undefined")
- inputElem.checked = Boolean(initialVal);
- const unitTxt = (
- "unit" in ftInfo && typeof ftInfo.unit === "string"
- ? ftInfo.unit
- : (
- "unit" in ftInfo && typeof ftInfo.unit === "function"
- ? ftInfo.unit(Number(inputElem.value))
- : ""
- )
- );
- let labelElem: HTMLLabelElement | undefined;
- let lastDisplayedVal: string | undefined;
- if(type === "slider") {
- labelElem = document.createElement("label");
- labelElem.classList.add("bytm-ftconf-label", "bytm-slider-label");
- labelElem.textContent = `${fmtVal(initialVal, featKey as FeatureKey)}${unitTxt}`;
- inputElem.addEventListener("input", () => {
- if(labelElem && lastDisplayedVal !== inputElem.value) {
- labelElem.textContent = `${fmtVal(inputElem.value, featKey as FeatureKey)}${unitTxt}`;
- lastDisplayedVal = inputElem.value;
- }
- });
- }
- else if(type === "select") {
- const ftOpts = typeof ftInfo.options === "function"
- ? ftInfo.options()
- : ftInfo.options;
- for(const { value, label } of ftOpts) {
- const optionElem = document.createElement("option");
- optionElem.value = String(value);
- optionElem.textContent = label;
- if(value === initialVal)
- optionElem.selected = true;
- inputElem.appendChild(optionElem);
- }
- }
- if(type === "text") {
- let lastValue: string | undefined = inputElem.value && inputElem.value.length > 0 ? inputElem.value : ftInfo.default;
- const textInputUpdate = () => {
- let v: string | number = String(inputElem.value).trim();
- if(type === "text" && ftInfo.normalize)
- v = inputElem.value = ftInfo.normalize(String(v));
- if(v === lastValue)
- return;
- lastValue = v;
- if(v === "")
- v = ftInfo.default;
- if(typeof initialVal !== "undefined")
- confChanged(featKey as keyof FeatureConfig, initialVal, v);
- };
- const unsub = siteEvents.on("cfgMenuClosed", () => {
- unsub();
- textInputUpdate();
- });
- inputElem.addEventListener("blur", () => textInputUpdate());
- inputElem.addEventListener("keydown", (e) => e.key === "Tab" && textInputUpdate());
- }
- else {
- inputElem.addEventListener("input", () => {
- let v: string | number = String(inputElem.value).trim();
- if(["number", "slider"].includes(type) || v.match(/^-?\d+$/))
- v = Number(v);
- if(typeof initialVal !== "undefined")
- confChanged(featKey as keyof FeatureConfig, initialVal, (type !== "toggle" ? v : inputElem.checked));
- });
- }
- if(labelElem) {
- labelElem.id = `bytm-ftconf-${featKey}-label`;
- labelElem.htmlFor = inputElemId;
- ctrlElem.appendChild(labelElem);
- }
- ctrlElem.appendChild(inputElem);
- }
- else {
- // custom input element:
- let wrapperElem: HTMLElement | undefined;
- switch(type) {
- case "hotkey":
- wrapperElem = createHotkeyInput({
- initialValue: typeof initialVal === "object" ? initialVal as HotkeyObj : undefined,
- onChange: (hotkey) => confChanged(featKey as keyof FeatureConfig, initialVal, hotkey),
- });
- break;
- case "toggle":
- wrapperElem = await createToggleInput({
- initialValue: Boolean(initialVal),
- onChange: (checked) => confChanged(featKey as keyof FeatureConfig, initialVal, checked),
- id: `ftconf-${featKey}`,
- labelPos: "left",
- });
- break;
- case "button":
- wrapperElem = document.createElement("button");
- wrapperElem.classList.add("bytm-btn");
- wrapperElem.tabIndex = 0;
- wrapperElem.textContent = wrapperElem.ariaLabel = wrapperElem.title = hasKey(`feature_btn_${featKey}`) ? t(`feature_btn_${featKey}`) : t("trigger_btn_action");
- onInteraction(wrapperElem, async () => {
- if((wrapperElem as HTMLButtonElement).disabled)
- return;
- const startTs = Date.now();
- const res = ftInfo.click();
- (wrapperElem as HTMLButtonElement).disabled = true;
- wrapperElem!.classList.add("bytm-busy");
- wrapperElem!.textContent = wrapperElem!.ariaLabel = wrapperElem!.title = hasKey(`feature_btn_${featKey}_running`) ? t(`feature_btn_${featKey}_running`) : t("trigger_btn_action_running");
- if(res instanceof Promise)
- await res;
- const finalize = () => {
- (wrapperElem as HTMLButtonElement).disabled = false;
- wrapperElem!.classList.remove("bytm-busy");
- wrapperElem!.textContent = wrapperElem!.ariaLabel = wrapperElem!.title = hasKey(`feature_btn_${featKey}`) ? t(`feature_btn_${featKey}`) : t("trigger_btn_action");
- };
- // artificial timeout ftw
- if(Date.now() - startTs < 350)
- setTimeout(finalize, 350 - (Date.now() - startTs));
- else
- finalize();
- });
- break;
- }
- ctrlElem.appendChild(wrapperElem!);
- }
- ftConfElem.appendChild(ctrlElem);
- }
- featuresCont.appendChild(ftConfElem);
- }
- }
- //#region reset inputs on external change
- siteEvents.on("rebuildCfgMenu", (newConfig) => {
- for(const ftKey in featInfo) {
- const ftElem = document.querySelector<HTMLInputElement>(`#bytm-ftconf-${ftKey}-input`);
- const labelElem = document.querySelector<HTMLLabelElement>(`#bytm-ftconf-${ftKey}-label`);
- if(!ftElem)
- continue;
- const ftInfo = featInfo[ftKey as keyof typeof featInfo];
- const value = newConfig[ftKey as keyof FeatureConfig];
- if(ftInfo.type === "toggle")
- ftElem.checked = Boolean(value);
- else
- ftElem.value = String(value);
- if(!labelElem)
- continue;
- const unitTxt = (
- "unit" in ftInfo && typeof ftInfo.unit === "string"
- ? ftInfo.unit
- : (
- "unit" in ftInfo && typeof ftInfo.unit === "function"
- ? ftInfo.unit(Number(ftElem.value))
- : ""
- )
- );
- if(ftInfo.type === "slider")
- labelElem.textContent = `${fmtVal(Number(value), ftKey as FeatureKey)}${unitTxt}`;
- }
- info("Rebuilt config menu");
- });
- //#region scroll indicator
- const scrollIndicator = document.createElement("img");
- scrollIndicator.id = "bytm-menu-scroll-indicator";
- scrollIndicator.src = await getResourceUrl("icon-arrow_down");
- scrollIndicator.role = "button";
- scrollIndicator.ariaLabel = scrollIndicator.title = t("scroll_to_bottom");
- featuresCont.appendChild(scrollIndicator);
- scrollIndicator.addEventListener("click", () => {
- const bottomAnchor = document.querySelector("#bytm-menu-bottom-anchor");
- bottomAnchor?.scrollIntoView({
- behavior: "smooth",
- });
- });
- featuresCont.addEventListener("scroll", (evt: Event) => {
- const scrollPos = (evt.target as HTMLDivElement)?.scrollTop ?? 0;
- const scrollIndicator = document.querySelector<HTMLImageElement>("#bytm-menu-scroll-indicator");
- if(!scrollIndicator)
- return;
- if(scrollIndicatorEnabled && scrollPos > scrollIndicatorOffsetThreshold && !scrollIndicator.classList.contains("bytm-hidden")) {
- scrollIndicator.classList.add("bytm-hidden");
- }
- else if(scrollIndicatorEnabled && scrollPos <= scrollIndicatorOffsetThreshold && scrollIndicator.classList.contains("bytm-hidden")) {
- scrollIndicator.classList.remove("bytm-hidden");
- }
- });
- const bottomAnchor = document.createElement("div");
- bottomAnchor.id = "bytm-menu-bottom-anchor";
- featuresCont.appendChild(bottomAnchor);
- //#region finalize
- menuContainer.appendChild(headerElem);
- menuContainer.appendChild(featuresCont);
- const subtitleElemCont = document.createElement("div");
- subtitleElemCont.id = "bytm-menu-subtitle-cont";
- const versionEl = document.createElement("a");
- versionEl.id = "bytm-menu-version-anchor";
- versionEl.classList.add("bytm-link");
- versionEl.role = "button";
- versionEl.tabIndex = 0;
- versionEl.ariaLabel = versionEl.title = t("version_tooltip", scriptInfo.version, buildNumber);
- versionEl.textContent = `v${scriptInfo.version} (#${buildNumber})`;
- onInteraction(versionEl, async (e: MouseEvent | KeyboardEvent) => {
- e.preventDefault();
- e.stopPropagation();
- const dlg = await getChangelogDialog();
- dlg.on("close", openCfgMenu);
- await dlg.mount();
- closeCfgMenu(undefined, false);
- await dlg.open();
- });
- subtitleElemCont.appendChild(versionEl);
- titleElem.appendChild(subtitleElemCont);
- const modeItems = [] as TrKey[];
- mode === "development" && modeItems.push("dev_mode");
- getFeature("advancedMode") && modeItems.push("advanced_mode");
- if(modeItems.length > 0) {
- const modeDisplayEl = document.createElement("span");
- modeDisplayEl.id = "bytm-menu-mode-display";
- modeDisplayEl.textContent = `[${t("active_mode_display", arrayWithSeparators(modeItems.map(v => t(`${v}_short`)), ", ", " & "))}]`;
- modeDisplayEl.ariaLabel = modeDisplayEl.title = tp("active_mode_tooltip", modeItems, arrayWithSeparators(modeItems.map(t), ", ", " & "));
- subtitleElemCont.appendChild(modeDisplayEl);
- }
- menuContainer.appendChild(footerCont);
- backgroundElem.appendChild(menuContainer);
- document.body.appendChild(backgroundElem);
- window.addEventListener("resize", debounce(checkToggleScrollIndicator, 250, "rising"));
- log("Added menu element");
- // ensure stuff is reset if menu was opened before being added
- isCfgMenuOpen = false;
- document.body.classList.remove("bytm-disable-scroll");
- document.querySelector(getDomain() === "ytm" ? "ytmusic-app" : "ytd-app")?.removeAttribute("inert");
- backgroundElem.style.visibility = "hidden";
- backgroundElem.style.display = "none";
- siteEvents.on("recreateCfgMenu", async () => {
- const bgElem = document.querySelector("#bytm-cfg-menu-bg");
- if(!bgElem)
- return;
- closeCfgMenu();
- bgElem.remove();
- isCfgMenuMounted = false;
- await mountCfgMenu();
- await openCfgMenu();
- });
- }
- //#region open & close
- /** Closes the config menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
- export function closeCfgMenu(evt?: MouseEvent | KeyboardEvent, enableScroll = true) {
- if(!isCfgMenuOpen)
- return;
- isCfgMenuOpen = false;
- evt?.bubbles && evt.stopPropagation();
- if(enableScroll) {
- document.body.classList.remove("bytm-disable-scroll");
- document.querySelector(getDomain() === "ytm" ? "ytmusic-app" : "ytd-app")?.removeAttribute("inert");
- }
- const menuBg = document.querySelector<HTMLElement>("#bytm-cfg-menu-bg");
- clearTimeout(hiddenCopiedTxtTimeout);
-
- openDialogs.splice(openDialogs.indexOf("cfg-menu"), 1);
- setCurrentDialogId(openDialogs?.[0] ?? null);
- emitInterface("bytm:dialogClosed", this as BytmDialog);
- emitInterface("bytm:dialogClosed:cfg-menu" as "bytm:dialogClosed:id", this as BytmDialog);
- if(!menuBg)
- return warn("Couldn't close config menu because background element couldn't be found. The config menu is considered closed but might still be open. In this case please reload the page. If the issue persists, please create an issue on GitHub.");
- menuBg.querySelectorAll<HTMLElement>(".bytm-ftconf-adv-copy-hint")?.forEach((el) => el.style.display = "none");
- menuBg.style.visibility = "hidden";
- menuBg.style.display = "none";
- }
- /** Opens the config menu if it is closed */
- export async function openCfgMenu() {
- if(!isCfgMenuMounted)
- await mountCfgMenu();
- if(isCfgMenuOpen)
- return;
- isCfgMenuOpen = true;
- document.body.classList.add("bytm-disable-scroll");
- document.querySelector(getDomain() === "ytm" ? "ytmusic-app" : "ytd-app")?.setAttribute("inert", "true");
- const menuBg = document.querySelector<HTMLElement>("#bytm-cfg-menu-bg");
- setCurrentDialogId("cfg-menu");
- openDialogs.unshift("cfg-menu");
- emitInterface("bytm:dialogOpened", this as BytmDialog);
- emitInterface("bytm:dialogOpened:cfg-menu" as "bytm:dialogOpened:id", this as BytmDialog);
- checkToggleScrollIndicator();
- if(!menuBg)
- return warn("Couldn't open config menu because background element couldn't be found. The config menu is considered open but might still be closed. In this case please reload the page. If the issue persists, please create an issue on GitHub.");
- menuBg.style.visibility = "visible";
- menuBg.style.display = "block";
- }
- //#region chk scroll indicator
- /** Checks if the features container is scrollable and toggles the scroll indicator accordingly */
- function checkToggleScrollIndicator() {
- const featuresCont = document.querySelector<HTMLElement>("#bytm-menu-opts");
- const scrollIndicator = document.querySelector<HTMLElement>("#bytm-menu-scroll-indicator");
-
- // disable scroll indicator if container doesn't scroll
- if(featuresCont && scrollIndicator) {
- const verticalScroll = isScrollable(featuresCont).vertical;
- /** If true, the indicator's threshold is under the available scrollable space and so it should be disabled */
- const underThreshold = featuresCont.scrollHeight - featuresCont.clientHeight <= scrollIndicatorOffsetThreshold;
- if(!underThreshold && verticalScroll && !scrollIndicatorEnabled) {
- scrollIndicatorEnabled = true;
- scrollIndicator.classList.remove("bytm-hidden");
- }
- if((!verticalScroll && scrollIndicatorEnabled) || underThreshold) {
- scrollIndicatorEnabled = false;
- scrollIndicator.classList.add("bytm-hidden");
- }
- }
- }
|