浏览代码

feat: migrate import & export menu to bytmdialog class

Sv443 1 年之前
父节点
当前提交
4260e39542

+ 0 - 3
src/components/BytmDialog.css

@@ -162,9 +162,6 @@
 }
 
 .bytm-dialog-footer-cont {
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
   margin-top: 6px;
   padding: 15px 20px;
   background: var(--bytm-dialog-bg);

+ 6 - 6
src/components/BytmDialog.ts

@@ -8,10 +8,10 @@ import "./BytmDialog.css";
 export interface BytmDialogOptions {
   /** ID that gets added to child element IDs - has to be unique and conform to HTML ID naming rules! */
   id: string;
-  /** Maximum width of the dialog in pixels */
-  maxWidth: number;
-  /** Maximum height of the dialog in pixels */
-  maxHeight: number;
+  /** Target and max width of the dialog in pixels */
+  width: number;
+  /** Target and max height of the dialog in pixels */
+  height: number;
   /** Whether the dialog should close when the background is clicked - defaults to true */
   closeOnBgClick?: boolean;
   /** Whether the dialog should close when the escape key is pressed - defaults to true */
@@ -94,8 +94,8 @@ export class BytmDialog extends NanoEmitter<{
 
     addStyle(`\
 #bytm-${this.id}-dialog-bg {
-  --bytm-dialog-width-max: ${this.options.maxWidth}px;
-  --bytm-dialog-height-max: ${this.options.maxHeight}px;
+  --bytm-dialog-width-max: ${this.options.width}px;
+  --bytm-dialog-height-max: ${this.options.height}px;
 }`, `dialog-${this.id}`);
 
     this.events.emit("render");

+ 4 - 3
src/dialogs/changelog.ts

@@ -9,8 +9,8 @@ export async function getChangelogDialog() {
   if(!changelogDialog) {
     changelogDialog = new BytmDialog({
       id: "changelog",
-      maxWidth: 900,
-      maxHeight: 800,
+      width: 900,
+      height: 800,
       closeBtnEnabled: true,
       closeOnBgClick: true,
       closeOnEscPress: true,
@@ -39,7 +39,8 @@ export async function getChangelogDialog() {
 }
 
 async function renderHeader() {
-  const headerEl = document.createElement("h1");
+  const headerEl = document.createElement("h2");
+  headerEl.classList.add("bytm-dialog-title");
   headerEl.role = "heading";
   headerEl.ariaLevel = "1";
   headerEl.textContent = t("changelog_menu_title", scriptInfo.name);

+ 0 - 9
src/dialogs/dialogs.css

@@ -15,15 +15,6 @@
   --bytm-menu-width-max: 1150px;
 }
 
-#bytm-export-dialog-bg, #bytm-import-dialog-bg,
-#bytm-export-menu-bg, #bytm-import-menu-bg
-{
-  --bytm-dialog-height-max: 500px;
-  --bytm-dialog-width-max: 600px;
-  --bytm-menu-height-max: 500px;
-  --bytm-menu-width-max: 600px;
-}
-
 .bytm-dialog-body p {
   overflow-wrap: break-word;
 }

+ 133 - 0
src/dialogs/export.ts

@@ -0,0 +1,133 @@
+import { compress } from "@sv443-network/userutils";
+import { compressionSupported, onInteraction, t } from "../utils";
+import { BytmDialog } from "../components";
+import { compressionFormat, scriptInfo } from "../constants";
+import { formatVersion, getFeatures } from "../config";
+import { siteEvents } from "src/siteEvents";
+
+let exportDialog: BytmDialog | null = null;
+let copiedTxtTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
+let lastUncompressedCfgString: string | undefined;
+
+/** Creates and/or returns the export dialog */
+export async function getExportDialog() {
+  if(!exportDialog) {
+    exportDialog = new BytmDialog({
+      id: "export",
+      width: 600,
+      height: 500,
+      closeBtnEnabled: true,
+      closeOnBgClick: true,
+      closeOnEscPress: true,
+      small: true,
+      renderHeader,
+      renderBody,
+      renderFooter,
+    });
+
+    exportDialog.on("close", () => {
+      const textAreaElem = document.querySelector<HTMLTextAreaElement>("#bytm-export-dialog-bg #bytm-export-menu-textarea");
+      if(textAreaElem) {
+        textAreaElem.value = t("click_to_reveal_sensitive_info");
+        textAreaElem.setAttribute("revealed", "false");
+      }
+
+      const copiedTxtElem = document.querySelector<HTMLElement>("#bytm-export-menu-copied-txt");
+      if(copiedTxtElem) {
+        copiedTxtElem.style.display = "none";
+        if(typeof copiedTxtTimeout === "number") {
+          clearTimeout(copiedTxtTimeout);
+          copiedTxtTimeout = undefined;
+        }
+      }
+    });
+  }
+  return exportDialog;
+}
+
+async function renderHeader() {
+  const headerEl = document.createElement("h2");
+  headerEl.classList.add("bytm-menu-title");
+  headerEl.role = "heading";
+  headerEl.ariaLevel = "1";
+  headerEl.textContent = t("export_menu_title", scriptInfo.name);
+
+  return headerEl;
+}
+
+async function renderBody() {
+  const canCompress = await compressionSupported();
+
+  const contElem = document.createElement("div");
+
+  const textElem = document.createElement("div");
+  textElem.id = "bytm-export-menu-text";
+  textElem.textContent = t("export_hint");
+
+  const textAreaElem = document.createElement("textarea");
+  textAreaElem.id = "bytm-export-menu-textarea";
+  textAreaElem.readOnly = true;
+  lastUncompressedCfgString = JSON.stringify({ formatVersion, data: getFeatures() }, undefined, 2);
+  textAreaElem.value = t("click_to_reveal_sensitive_info");
+  textAreaElem.setAttribute("revealed", "false");
+
+  const textAreaInteraction = async () => {
+    const cfgString = JSON.stringify({ formatVersion, data: getFeatures() });
+    lastUncompressedCfgString = JSON.stringify({ formatVersion, data: getFeatures() }, undefined, 2);
+    textAreaElem.value = canCompress ? await compress(cfgString, compressionFormat, "string") : cfgString;
+    textAreaElem.setAttribute("revealed", "true");
+  };
+
+  onInteraction(textAreaElem, textAreaInteraction);
+
+  siteEvents.on("configChanged", async (data) => {
+    const textAreaElem = document.querySelector<HTMLTextAreaElement>("#bytm-export-menu-textarea");
+    const cfgString = JSON.stringify({ formatVersion, data });
+    lastUncompressedCfgString = JSON.stringify({ formatVersion, data }, undefined, 2);
+    if(textAreaElem) {
+      if(textAreaElem.getAttribute("revealed") !== "true")
+        return;
+      textAreaElem.value = canCompress ? await compress(cfgString, compressionFormat, "string") : cfgString;
+    }
+  });
+
+  contElem.appendChild(textElem);
+  contElem.appendChild(textAreaElem);
+
+  return contElem;
+}
+
+async function renderFooter() {
+  const footerElem = document.createElement("div");
+  footerElem.classList.add("bytm-menu-footer-right");
+
+  const copyBtnElem = document.createElement("button");
+  copyBtnElem.classList.add("bytm-btn");
+  copyBtnElem.textContent = t("copy_to_clipboard");
+  copyBtnElem.ariaLabel = copyBtnElem.title = t("copy_config_tooltip");
+
+  const copiedTextElem = document.createElement("span");
+  copiedTextElem.id = "bytm-export-menu-copied-txt";
+  copiedTextElem.role = "status";
+  copiedTextElem.classList.add("bytm-menu-footer-copied");
+  copiedTextElem.textContent = t("copied");
+  copiedTextElem.style.display = "none";
+
+  onInteraction(copyBtnElem, async (evt: MouseEvent | KeyboardEvent) => {
+    evt?.bubbles && evt.stopPropagation();
+    GM.setClipboard(String(evt?.shiftKey || evt?.ctrlKey ? lastUncompressedCfgString : await compress(JSON.stringify({ formatVersion, data: getFeatures() }), compressionFormat, "string")));
+    copiedTextElem.style.display = "inline-block";
+    if(typeof copiedTxtTimeout === "undefined") {
+      copiedTxtTimeout = setTimeout(() => {
+        copiedTextElem.style.display = "none";
+        copiedTxtTimeout = undefined;
+      }, 3000);
+    }
+  });
+
+  // flex-direction is row-reverse
+  footerElem.appendChild(copyBtnElem);
+  footerElem.appendChild(copiedTextElem);
+
+  return footerElem;
+}

+ 2 - 2
src/dialogs/featHelp.ts

@@ -18,8 +18,8 @@ export async function getFeatHelpDialog({
   if(!featHelpDialog) {
     featHelpDialog = new BytmDialog({
       id: "feat-help",
-      maxWidth: 600,
-      maxHeight: 400,
+      width: 600,
+      height: 400,
       closeBtnEnabled: true,
       closeOnBgClick: true,
       closeOnEscPress: true,

+ 139 - 0
src/dialogs/import.ts

@@ -0,0 +1,139 @@
+import { decompress } from "@sv443-network/userutils";
+import { error, t, warn } from "../utils";
+import { BytmDialog } from "../components";
+import { compressionFormat, scriptInfo } from "../constants";
+import { emitSiteEvent } from "../siteEvents";
+import { formatVersion, getFeatures, migrations, setFeatures } from "../config";
+import { disableBeforeUnload } from "../features";
+
+let importDialog: BytmDialog | null = null;
+
+/** Creates and/or returns the import dialog */
+export async function getImportDialog() {
+  if(!importDialog) {
+    importDialog = new BytmDialog({
+      id: "import",
+      width: 600,
+      height: 500,
+      closeBtnEnabled: true,
+      closeOnBgClick: true,
+      closeOnEscPress: true,
+      small: true,
+      renderHeader,
+      renderBody,
+      renderFooter,
+    });
+  }
+  return importDialog;
+}
+
+async function renderHeader() {
+  const headerEl = document.createElement("h2");
+  headerEl.classList.add("bytm-dialog-title");
+  headerEl.role = "heading";
+  headerEl.ariaLevel = "1";
+  headerEl.textContent = t("import_menu_title", scriptInfo.name);
+
+  return headerEl;
+}
+
+async function renderBody() {
+  const contElem = document.createElement("div");
+
+  const textElem = document.createElement("div");
+  textElem.id = "bytm-import-menu-text";
+  textElem.textContent = t("import_hint");
+
+  const textAreaElem = document.createElement("textarea");
+  textAreaElem.id = "bytm-import-menu-textarea";
+
+  contElem.appendChild(textElem);
+  contElem.appendChild(textAreaElem);
+
+  return contElem;
+}
+
+async function renderFooter() {
+  const footerElem = document.createElement("div");
+  footerElem.classList.add("bytm-menu-footer-right");
+
+  const importBtnElem = document.createElement("button");
+  importBtnElem.classList.add("bytm-btn");
+  importBtnElem.textContent = t("import");
+  importBtnElem.ariaLabel = importBtnElem.title = t("start_import_tooltip");
+
+  importBtnElem.addEventListener("click", async (evt) => {
+    evt?.bubbles && evt.stopPropagation();
+    const textAreaElem = document.querySelector<HTMLTextAreaElement>("#bytm-import-menu-textarea");
+    if(!textAreaElem)
+      return warn("Couldn't find import menu textarea element");
+    try {
+      /** Tries to parse an uncompressed or compressed input string as a JSON object */
+      const decode = async (input: string) => {
+        try {
+          return JSON.parse(input);
+        }
+        catch {
+          try {
+            return JSON.parse(await decompress(input, compressionFormat, "string"));
+          }
+          catch(err) {
+            warn("Couldn't import configuration:", err);
+            return null;
+          }
+        }
+      };
+      const parsed = await decode(textAreaElem.value.trim());
+      if(typeof parsed !== "object")
+        return alert(t("import_error_invalid"));
+      if(typeof parsed.formatVersion !== "number")
+        return alert(t("import_error_no_format_version"));
+      if(typeof parsed.data !== "object" || parsed.data === null || Object.keys(parsed.data).length === 0)
+        return alert(t("import_error_no_data"));
+      if(parsed.formatVersion < formatVersion) {
+        let newData = JSON.parse(JSON.stringify(parsed.data));
+        const sortedMigrations = Object.entries(migrations)
+          .sort(([a], [b]) => Number(a) - Number(b));
+
+        let curFmtVer = Number(parsed.formatVersion);
+
+        for(const [fmtVer, migrationFunc] of sortedMigrations) {
+          const ver = Number(fmtVer);
+          if(curFmtVer < formatVersion && curFmtVer < ver) {
+            try {
+              const migRes = JSON.parse(JSON.stringify(migrationFunc(newData)));
+              newData = migRes instanceof Promise ? await migRes : migRes;
+              curFmtVer = ver;
+            }
+            catch(err) {
+              error(`Error while running migration function for format version ${fmtVer}:`, err);
+            }
+          }
+        }
+        parsed.formatVersion = curFmtVer;
+        parsed.data = newData;
+      }
+      else if(parsed.formatVersion !== formatVersion)
+        return alert(t("import_error_wrong_format_version", formatVersion, parsed.formatVersion));
+
+      await setFeatures({ ...getFeatures(), ...parsed.data });
+
+      if(confirm(t("import_success_confirm_reload"))) {
+        disableBeforeUnload();
+        return location.reload();
+      }
+
+      emitSiteEvent("rebuildCfgMenu", parsed.data);
+
+      importDialog?.close();
+    }
+    catch(err) {
+      warn("Couldn't import configuration:", err);
+      alert(t("import_error_invalid"));
+    }
+  });
+
+  footerElem.appendChild(importBtnElem);
+
+  return footerElem;
+}

+ 2 - 0
src/dialogs/index.ts

@@ -1,5 +1,7 @@
 import "./dialogs.css";
 
 export * from "./changelog";
+export * from "./export";
 export * from "./featHelp";
+export * from "./import";
 export * from "./versionNotif";

+ 2 - 2
src/dialogs/versionNotif.ts

@@ -21,8 +21,8 @@ export async function getVersionNotifDialog({
 
     verNotifDialog = new BytmDialog({
       id: "version-notif",
-      maxWidth: 600,
-      maxHeight: 800,
+      width: 600,
+      height: 800,
       closeBtnEnabled: false,
       closeOnBgClick: false,
       closeOnEscPress: true,

+ 2 - 3
src/menu/menu_old.css

@@ -217,7 +217,6 @@
   display: flex;
   flex-direction: row-reverse;
   align-items: center;
-  margin-top: 15px;
 }
 
 #bytm-menu-footer-left-buttons-cont button:not(:last-of-type) {
@@ -310,12 +309,12 @@
 
 #bytm-export-menu-text, #bytm-import-menu-text {
   white-space: pre-wrap;
-  font-size: 1.6em;
+  font-size: 1.6rem;
   margin-bottom: 15px;
 }
 
 .bytm-menu-footer-copied {
-  font-size: 1.6em;
+  font-size: 1.6rem;
   margin-right: 15px;
 }
 

+ 14 - 417
src/menu/menu_old.ts

@@ -1,11 +1,10 @@
-import { compress, decompress, debounce, isScrollable } from "@sv443-network/userutils";
-import { defaultData, getFeatures, migrations, setFeatures, setDefaultFeatures } from "../config";
-import { buildNumber, compressionFormat, host, mode, scriptInfo } from "../constants";
+import { debounce, isScrollable } from "@sv443-network/userutils";
+import { defaultData, getFeatures, setFeatures, setDefaultFeatures } from "../config";
+import { buildNumber, host, mode, scriptInfo } from "../constants";
 import { featInfo, disableBeforeUnload } from "../features/index";
-import { error, getResourceUrl, info, log, resourceToHTMLString, warn, getLocale, hasKey, initTranslations, setLocale, t, compressionSupported, arrayWithSeparators, tp, type TrKey, onInteraction } from "../utils";
-import { formatVersion } from "../config";
-import { emitSiteEvent, siteEvents } from "../siteEvents";
-import { getChangelogDialog, getFeatHelpDialog } from "../dialogs";
+import { error, getResourceUrl, info, log, resourceToHTMLString, getLocale, hasKey, initTranslations, setLocale, t, arrayWithSeparators, tp, type TrKey, onInteraction } from "../utils";
+import { siteEvents } from "../siteEvents";
+import { getChangelogDialog, getExportDialog, getFeatHelpDialog, getImportDialog } from "../dialogs";
 import type { FeatureCategory, FeatureKey, FeatureConfig, HotkeyObj, FeatureInfo } from "../types";
 import "./menu_old.css";
 import { createHotkeyInput, createToggleInput } from "../components";
@@ -188,16 +187,22 @@ async function addCfgMenu() {
   exportElem.ariaLabel = exportElem.title = t("export_tooltip");
   exportElem.textContent = t("export");
   exportElem.addEventListener("click", async () => {
-    await openExportMenu();
+    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");
   importElem.addEventListener("click", async () => {
-    await openImportMenu();
+    const dlg = await getImportDialog();
+    dlg.on("close", openCfgMenu);
+    await dlg.mount();
     closeCfgMenu(undefined, false);
+    await dlg.open();
   });
 
   const buttonsCont = document.createElement("div");
@@ -797,411 +802,3 @@ function checkToggleScrollIndicator() {
     }
   }
 }
-
-//#MARKER export menu
-
-let isExportMenuAdded = false;
-let isExportMenuOpen = false;
-let copiedTxtTimeout: number | undefined = undefined;
-let lastUncompressedCfgString: string | undefined;
-
-/** Adds a menu to copy the current configuration as compressed (if supported) or uncompressed JSON (hidden by default) */
-async function addExportMenu() {
-  const canCompress = await compressionSupported();
-
-  const menuBgElem = document.createElement("div");
-  menuBgElem.id = "bytm-export-menu-bg";
-  menuBgElem.classList.add("bytm-menu-bg");
-  menuBgElem.ariaLabel = menuBgElem.title = t("close_menu_tooltip");
-  menuBgElem.style.visibility = "hidden";
-  menuBgElem.style.display = "none";
-  menuBgElem.addEventListener("click", (e) => {
-    if(isExportMenuOpen && (e.target as HTMLElement)?.id === "bytm-export-menu-bg") {
-      closeExportMenu(e);
-      openCfgMenu();
-    }
-  });
-  document.body.addEventListener("keydown", (e) => {
-    if(isExportMenuOpen && e.key === "Escape") {
-      closeExportMenu(e);
-      openCfgMenu();
-    }
-  });
-
-  const menuContainer = document.createElement("div");
-  menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
-  menuContainer.classList.add("bytm-menu");
-  menuContainer.id = "bytm-export-menu";
-
-  //#SECTION title bar
-  const headerElem = document.createElement("div");
-  headerElem.classList.add("bytm-menu-header");
-
-  const titleCont = document.createElement("div");
-  titleCont.className = "bytm-menu-titlecont";
-  titleCont.role = "heading";
-  titleCont.ariaLevel = "1";
-
-  const titleElem = document.createElement("h2");
-  titleElem.className = "bytm-menu-title";
-  titleElem.textContent = t("export_menu_title", scriptInfo.name);
-
-  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, (e: MouseEvent | KeyboardEvent) => {
-    closeExportMenu(e);
-    openCfgMenu();
-  });
-
-  titleCont.appendChild(titleElem);
-
-  headerElem.appendChild(titleCont);
-  headerElem.appendChild(closeElem);
-
-  //#SECTION body
-
-  const menuBodyElem = document.createElement("div");
-  menuBodyElem.classList.add("bytm-menu-body");
-
-  const textElem = document.createElement("div");
-  textElem.id = "bytm-export-menu-text";
-  textElem.textContent = t("export_hint");
-
-  const textAreaElem = document.createElement("textarea");
-  textAreaElem.id = "bytm-export-menu-textarea";
-  textAreaElem.readOnly = true;
-  lastUncompressedCfgString = JSON.stringify({ formatVersion, data: getFeatures() }, undefined, 2);
-  textAreaElem.value = t("click_to_reveal_sensitive_info");
-  textAreaElem.setAttribute("revealed", "false");
-
-  const textAreaInteraction = async () => {
-    const cfgString = JSON.stringify({ formatVersion, data: getFeatures() });
-    lastUncompressedCfgString = JSON.stringify({ formatVersion, data: getFeatures() }, undefined, 2);
-    textAreaElem.value = canCompress ? await compress(cfgString, compressionFormat, "string") : cfgString;
-    textAreaElem.setAttribute("revealed", "true");
-  };
-
-  onInteraction(textAreaElem, textAreaInteraction);
-
-  siteEvents.on("configChanged", async (data) => {
-    const textAreaElem = document.querySelector<HTMLTextAreaElement>("#bytm-export-menu-textarea");
-    const cfgString = JSON.stringify({ formatVersion, data });
-    lastUncompressedCfgString = JSON.stringify({ formatVersion, data }, undefined, 2);
-    if(textAreaElem) {
-      if(textAreaElem.getAttribute("revealed") !== "true")
-        return;
-      textAreaElem.value = canCompress ? await compress(cfgString, compressionFormat, "string") : cfgString;
-    }
-  });
-
-  //#SECTION footer
-  const footerElem = document.createElement("div");
-  footerElem.classList.add("bytm-menu-footer-right");
-
-  const copyBtnElem = document.createElement("button");
-  copyBtnElem.classList.add("bytm-btn");
-  copyBtnElem.textContent = t("copy_to_clipboard");
-  copyBtnElem.ariaLabel = copyBtnElem.title = t("copy_config_tooltip");
-
-  const copiedTextElem = document.createElement("span");
-  copiedTextElem.id = "bytm-export-menu-copied-txt";
-  copiedTextElem.role = "status";
-  copiedTextElem.classList.add("bytm-menu-footer-copied");
-  copiedTextElem.textContent = t("copied");
-  copiedTextElem.style.display = "none";
-
-  onInteraction(copyBtnElem, async (evt: MouseEvent | KeyboardEvent) => {
-    evt?.bubbles && evt.stopPropagation();
-    GM.setClipboard(String(evt?.shiftKey || evt?.ctrlKey ? lastUncompressedCfgString : await compress(JSON.stringify({ formatVersion, data: getFeatures() }), compressionFormat, "string")));
-    copiedTextElem.style.display = "inline-block";
-    if(typeof copiedTxtTimeout === "undefined") {
-      copiedTxtTimeout = setTimeout(() => {
-        copiedTextElem.style.display = "none";
-        copiedTxtTimeout = undefined;
-      }, 3000) as unknown as number;
-    }
-  });
-
-  // flex-direction is row-reverse
-  footerElem.appendChild(copyBtnElem);
-  footerElem.appendChild(copiedTextElem);
-
-  //#SECTION finalize
-
-  menuBodyElem.appendChild(textElem);
-  menuBodyElem.appendChild(textAreaElem);
-  menuBodyElem.appendChild(footerElem);
-
-  menuContainer.appendChild(headerElem);
-  menuContainer.appendChild(menuBodyElem);
-  
-  menuBgElem.appendChild(menuContainer);
-
-  document.body.appendChild(menuBgElem);
-}
-
-/** Closes the export menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
-function closeExportMenu(evt: MouseEvent | KeyboardEvent) {
-  if(!isExportMenuOpen)
-    return;
-  isExportMenuOpen = false;
-  evt?.bubbles && evt.stopPropagation();
-
-  const menuBg = document.querySelector<HTMLElement>("#bytm-export-menu-bg");
-
-  if(!menuBg)
-    return warn("Couldn't find export menu background element");
-
-  menuBg.style.visibility = "hidden";
-  menuBg.style.display = "none";
-
-  const textAreaElem = menuBg.querySelector<HTMLTextAreaElement>("#bytm-export-menu-textarea");
-  if(textAreaElem) {
-    textAreaElem.value = t("click_to_reveal_sensitive_info");
-    textAreaElem.setAttribute("revealed", "false");
-  }
-
-  const copiedTxtElem = document.querySelector<HTMLElement>("#bytm-export-menu-copied-txt");
-  if(copiedTxtElem) {
-    copiedTxtElem.style.display = "none";
-    if(typeof copiedTxtTimeout === "number") {
-      clearTimeout(copiedTxtTimeout);
-      copiedTxtTimeout = undefined;
-    }
-  }
-}
-
-/** Opens the export menu if it is closed */
-async function openExportMenu() {
-  if(!isExportMenuAdded)
-    await addExportMenu();
-  isExportMenuAdded = true;
-
-  if(isExportMenuOpen)
-    return;
-  isExportMenuOpen = true;
-
-  document.body.classList.add("bytm-disable-scroll");
-  document.querySelector("ytmusic-app")?.setAttribute("inert", "true");
-  const menuBg = document.querySelector<HTMLElement>("#bytm-export-menu-bg");
-
-  if(!menuBg)
-    return warn("Couldn't find export menu background element");
-
-  menuBg.style.visibility = "visible";
-  menuBg.style.display = "block";
-}
-
-//#MARKER import menu
-
-let isImportMenuAdded = false;
-let isImportMenuOpen = false;
-
-/** Adds a menu to import a configuration from compressed or uncompressed JSON (hidden by default) */
-async function addImportMenu() {
-  const menuBgElem = document.createElement("div");
-  menuBgElem.id = "bytm-import-menu-bg";
-  menuBgElem.classList.add("bytm-menu-bg");
-  menuBgElem.ariaLabel = menuBgElem.title = t("close_menu_tooltip");
-  menuBgElem.style.visibility = "hidden";
-  menuBgElem.style.display = "none";
-  menuBgElem.addEventListener("click", (e) => {
-    if(isImportMenuOpen && (e.target as HTMLElement)?.id === "bytm-import-menu-bg") {
-      closeImportMenu(e);
-      openCfgMenu();
-    }
-  });
-  document.body.addEventListener("keydown", (e) => {
-    if(isImportMenuOpen && e.key === "Escape") {
-      closeImportMenu(e);
-      openCfgMenu();
-    }
-  });
-
-  const menuContainer = document.createElement("div");
-  menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
-  menuContainer.classList.add("bytm-menu");
-  menuContainer.id = "bytm-import-menu";
-
-  //#SECTION title bar
-  const headerElem = document.createElement("div");
-  headerElem.classList.add("bytm-menu-header");
-
-  const titleCont = document.createElement("div");
-  titleCont.className = "bytm-menu-titlecont";
-  titleCont.role = "heading";
-  titleCont.ariaLevel = "1";
-
-  const titleElem = document.createElement("h2");
-  titleElem.className = "bytm-menu-title";
-  titleElem.textContent = t("import_menu_title", scriptInfo.name);
-
-  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, (e: MouseEvent | KeyboardEvent) => {
-    closeImportMenu(e);
-    openCfgMenu();
-  });
-
-  titleCont.appendChild(titleElem);
-
-  headerElem.appendChild(titleCont);
-  headerElem.appendChild(closeElem);
-
-  //#SECTION body
-
-  const menuBodyElem = document.createElement("div");
-  menuBodyElem.classList.add("bytm-menu-body");
-
-  const textElem = document.createElement("div");
-  textElem.id = "bytm-import-menu-text";
-  textElem.textContent = t("import_hint");
-
-  const textAreaElem = document.createElement("textarea");
-  textAreaElem.id = "bytm-import-menu-textarea";
-
-  //#SECTION footer
-  const footerElem = document.createElement("div");
-  footerElem.classList.add("bytm-menu-footer-right");
-
-  const importBtnElem = document.createElement("button");
-  importBtnElem.classList.add("bytm-btn");
-  importBtnElem.textContent = t("import");
-  importBtnElem.ariaLabel = importBtnElem.title = t("start_import_tooltip");
-
-  importBtnElem.addEventListener("click", async (evt) => {
-    evt?.bubbles && evt.stopPropagation();
-    const textAreaElem = document.querySelector<HTMLTextAreaElement>("#bytm-import-menu-textarea");
-    if(!textAreaElem)
-      return warn("Couldn't find import menu textarea element");
-    try {
-      /** Tries to parse an uncompressed or compressed input string as a JSON object */
-      const decode = async (input: string) => {
-        try {
-          return JSON.parse(input);
-        }
-        catch {
-          try {
-            return JSON.parse(await decompress(input, compressionFormat, "string"));
-          }
-          catch(err) {
-            warn("Couldn't import configuration:", err);
-            return null;
-          }
-        }
-      };
-      const parsed = await decode(textAreaElem.value.trim());
-      if(typeof parsed !== "object")
-        return alert(t("import_error_invalid"));
-      if(typeof parsed.formatVersion !== "number")
-        return alert(t("import_error_no_format_version"));
-      if(typeof parsed.data !== "object" || parsed.data === null || Object.keys(parsed.data).length === 0)
-        return alert(t("import_error_no_data"));
-      if(parsed.formatVersion < formatVersion) {
-        let newData = JSON.parse(JSON.stringify(parsed.data));
-        const sortedMigrations = Object.entries(migrations)
-          .sort(([a], [b]) => Number(a) - Number(b));
-
-        let curFmtVer = Number(parsed.formatVersion);
-
-        for(const [fmtVer, migrationFunc] of sortedMigrations) {
-          const ver = Number(fmtVer);
-          if(curFmtVer < formatVersion && curFmtVer < ver) {
-            try {
-              const migRes = JSON.parse(JSON.stringify(migrationFunc(newData)));
-              newData = migRes instanceof Promise ? await migRes : migRes;
-              curFmtVer = ver;
-            }
-            catch(err) {
-              error(`Error while running migration function for format version ${fmtVer}:`, err);
-            }
-          }
-        }
-        parsed.formatVersion = curFmtVer;
-        parsed.data = newData;
-      }
-      else if(parsed.formatVersion !== formatVersion)
-        return alert(t("import_error_wrong_format_version", formatVersion, parsed.formatVersion));
-
-      await setFeatures({ ...getFeatures(), ...parsed.data });
-
-      if(confirm(t("import_success_confirm_reload"))) {
-        disableBeforeUnload();
-        return location.reload();
-      }
-
-      emitSiteEvent("rebuildCfgMenu", parsed.data);
-
-      closeImportMenu();
-      openCfgMenu();
-    }
-    catch(err) {
-      warn("Couldn't import configuration:", err);
-      alert(t("import_error_invalid"));
-    }
-  });
-
-  footerElem.appendChild(importBtnElem);
-
-  //#SECTION finalize
-
-  menuBodyElem.appendChild(textElem);
-  menuBodyElem.appendChild(textAreaElem);
-  menuBodyElem.appendChild(footerElem);
-
-  menuContainer.appendChild(headerElem);
-  menuContainer.appendChild(menuBodyElem);
-  
-  menuBgElem.appendChild(menuContainer);
-
-  document.body.appendChild(menuBgElem);
-}
-
-/** Closes the import menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
-function closeImportMenu(evt?: MouseEvent | KeyboardEvent) {
-  if(!isImportMenuOpen)
-    return;
-  isImportMenuOpen = false;
-  evt?.bubbles && evt.stopPropagation();
-
-  const menuBg = document.querySelector<HTMLElement>("#bytm-import-menu-bg");
-
-  const textAreaElem = document.querySelector<HTMLTextAreaElement>("#bytm-import-menu-textarea");
-  if(textAreaElem)
-    textAreaElem.value = "";
-
-  if(!menuBg)
-    return warn("Couldn't find import menu background element");
-
-  menuBg.style.visibility = "hidden";
-  menuBg.style.display = "none";
-}
-
-/** Opens the import menu if it is closed */
-async function openImportMenu() {
-  if(!isImportMenuAdded)
-    await addImportMenu();
-  isImportMenuAdded = true;
-
-  if(isImportMenuOpen)
-    return;
-  isImportMenuOpen = true;
-
-  document.body.classList.add("bytm-disable-scroll");
-  document.querySelector("ytmusic-app")?.setAttribute("inert", "true");
-  const menuBg = document.querySelector<HTMLElement>("#bytm-import-menu-bg");
-
-  if(!menuBg)
-    return warn("Couldn't find import menu background element");
-
-  menuBg.style.visibility = "visible";
-  menuBg.style.display = "block";
-}