123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847 |
- import { debounce, isScrollable, type Stringifiable } from "@sv443-network/userutils";
- import { defaultData, 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 } 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 pkg from "../../package.json" with { type: "json" };
- //#region create menu
- let isCfgMenuAdded = 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 addCfgMenu() {
- if(isCfgMenuAdded)
- return;
- isCfgMenuAdded = true;
- initLocale = getFeatures().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())
- 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.id = "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(getFeatures().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((getFeatures().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");
- getFeatures().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();
- isCfgMenuAdded = false;
- await addCfgMenu();
- 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");
- siteEvents.emit("cfgMenuClosed");
- if(!menuBg)
- return;
- menuBg.querySelectorAll<HTMLElement>(".bytm-ftconf-adv-copy-hint")?.forEach((el) => el.style.display = "none");
- clearTimeout(hiddenCopiedTxtTimeout);
- menuBg.style.visibility = "hidden";
- menuBg.style.display = "none";
- openDialogs.splice(openDialogs.indexOf("cfg-menu"), 1);
- setCurrentDialogId(openDialogs?.[0] ?? null);
- }
- /** Opens the config menu if it is closed */
- export async function openCfgMenu() {
- if(!isCfgMenuAdded)
- await addCfgMenu();
- 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");
- if(!menuBg)
- return;
- menuBg.style.visibility = "visible";
- menuBg.style.display = "block";
- setCurrentDialogId("cfg-menu");
- openDialogs.unshift("cfg-menu");
- checkToggleScrollIndicator();
- }
- //#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");
- }
- }
- }
|