Explorar o código

feat: implement exim dialog in cfg menu

Sv443 hai 10 meses
pai
achega
32d95baa78

+ 51 - 23
assets/translations/README.md

@@ -16,15 +16,15 @@ To submit or edit a translation, please follow [this guide](../../contributing.m
 ### Translation progress:
 |   | Locale | Translated keys | Based on |
 | :----: | ------ | --------------- | :------: |
-| ─ | [`en_US`](./en_US.json) | 267 (default locale) |  |
-| ‼️ | [`de_DE`](./de_DE.json) | `209/267` (78.3%) | ─ |
-| ─ | [`en_UK`](./en_UK.json) | `267` (100%) | `en_US` |
-| ‼️ | [`es_ES`](./es_ES.json) | `209/267` (78.3%) | ─ |
-| ‼️ | [`fr_FR`](./fr_FR.json) | `209/267` (78.3%) | ─ |
-| ‼️ | [`hi_IN`](./hi_IN.json) | `209/267` (78.3%) | ─ |
-| ‼️ | [`ja_JA`](./ja_JA.json) | `209/267` (78.3%) | ─ |
-| ‼️ | [`pt_BR`](./pt_BR.json) | `209/267` (78.3%) | ─ |
-| ‼️ | [`zh_CN`](./zh_CN.json) | `209/267` (78.3%) | ─ |
+| ─ | [`en_US`](./en_US.json) | 271 (default locale) |  |
+| ‼️ | [`de_DE`](./de_DE.json) | `209/271` (77.1%) | ─ |
+| ─ | [`en_UK`](./en_UK.json) | `271` (100%) | `en_US` |
+| ‼️ | [`es_ES`](./es_ES.json) | `209/271` (77.1%) | ─ |
+| ‼️ | [`fr_FR`](./fr_FR.json) | `209/271` (77.1%) | ─ |
+| ‼️ | [`hi_IN`](./hi_IN.json) | `209/271` (77.1%) | ─ |
+| ‼️ | [`ja_JA`](./ja_JA.json) | `209/271` (77.1%) | ─ |
+| ‼️ | [`pt_BR`](./pt_BR.json) | `209/271` (77.1%) | ─ |
+| ‼️ | [`zh_CN`](./zh_CN.json) | `209/271` (77.1%) | ─ |
 
 <sub>
 ✅ - Fully translated
@@ -45,14 +45,18 @@ This means to figure out which keys are untranslated, you will need to manually
 
 ### Missing keys:
 
-<details><summary><code>de_DE</code> - 58 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>de_DE</code> - 62 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
+| `bytm_config_export_import_title` | `Export/Import Configuration` |
+| `bytm_config_import_desc` | `Paste the configuration you want to import into the field below, then click the import button:` |
+| `bytm_config_export_desc` | `Copy the following text to export your configuration. Warning: it may contain sensitive data.` |
 | `export_import` | `Export/Import` |
 | `click_to_reveal` | `(click to reveal)` |
 | `start_import_tooltip` | `Click to import the data you pasted above` |
 | `copy` | `Copy` |
+| `copied_to_clipboard` | `Copied to clipboard!` |
 | `close_tooltip` | `Click to close` |
 | `new_entry` | `New entry` |
 | `new_entry_tooltip` | `Click to create a new entry` |
@@ -75,7 +79,7 @@ This means to figure out which keys are untranslated, you will need to manually
 | `auto_liked_a_channels_song` | `Liked song by %1` |
 | `auto_liked_a_channels_video` | `Liked video by %1` |
 | `auto_like_export_or_import_tooltip` | `Export or import your auto-liked channels` |
-| `auto_like_export_import_title` | `Export/import auto-liked channels` |
+| `auto_like_export_import_title` | `Export/Import auto-liked channels` |
 | `auto_like_export_desc` | `Copy the following text to export your auto-liked channels.` |
 | `auto_like_import_desc` | `Paste the auto-liked channels you want to import into the field below, then click the import button:` |
 | `vote_label_likes` | `%1 likes` |
@@ -110,14 +114,18 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>es_ES</code> - 58 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>es_ES</code> - 62 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
+| `bytm_config_export_import_title` | `Export/Import Configuration` |
+| `bytm_config_import_desc` | `Paste the configuration you want to import into the field below, then click the import button:` |
+| `bytm_config_export_desc` | `Copy the following text to export your configuration. Warning: it may contain sensitive data.` |
 | `export_import` | `Export/Import` |
 | `click_to_reveal` | `(click to reveal)` |
 | `start_import_tooltip` | `Click to import the data you pasted above` |
 | `copy` | `Copy` |
+| `copied_to_clipboard` | `Copied to clipboard!` |
 | `close_tooltip` | `Click to close` |
 | `new_entry` | `New entry` |
 | `new_entry_tooltip` | `Click to create a new entry` |
@@ -140,7 +148,7 @@ This means to figure out which keys are untranslated, you will need to manually
 | `auto_liked_a_channels_song` | `Liked song by %1` |
 | `auto_liked_a_channels_video` | `Liked video by %1` |
 | `auto_like_export_or_import_tooltip` | `Export or import your auto-liked channels` |
-| `auto_like_export_import_title` | `Export/import auto-liked channels` |
+| `auto_like_export_import_title` | `Export/Import auto-liked channels` |
 | `auto_like_export_desc` | `Copy the following text to export your auto-liked channels.` |
 | `auto_like_import_desc` | `Paste the auto-liked channels you want to import into the field below, then click the import button:` |
 | `vote_label_likes` | `%1 likes` |
@@ -175,14 +183,18 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>fr_FR</code> - 58 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>fr_FR</code> - 62 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
+| `bytm_config_export_import_title` | `Export/Import Configuration` |
+| `bytm_config_import_desc` | `Paste the configuration you want to import into the field below, then click the import button:` |
+| `bytm_config_export_desc` | `Copy the following text to export your configuration. Warning: it may contain sensitive data.` |
 | `export_import` | `Export/Import` |
 | `click_to_reveal` | `(click to reveal)` |
 | `start_import_tooltip` | `Click to import the data you pasted above` |
 | `copy` | `Copy` |
+| `copied_to_clipboard` | `Copied to clipboard!` |
 | `close_tooltip` | `Click to close` |
 | `new_entry` | `New entry` |
 | `new_entry_tooltip` | `Click to create a new entry` |
@@ -205,7 +217,7 @@ This means to figure out which keys are untranslated, you will need to manually
 | `auto_liked_a_channels_song` | `Liked song by %1` |
 | `auto_liked_a_channels_video` | `Liked video by %1` |
 | `auto_like_export_or_import_tooltip` | `Export or import your auto-liked channels` |
-| `auto_like_export_import_title` | `Export/import auto-liked channels` |
+| `auto_like_export_import_title` | `Export/Import auto-liked channels` |
 | `auto_like_export_desc` | `Copy the following text to export your auto-liked channels.` |
 | `auto_like_import_desc` | `Paste the auto-liked channels you want to import into the field below, then click the import button:` |
 | `vote_label_likes` | `%1 likes` |
@@ -240,14 +252,18 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>hi_IN</code> - 58 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>hi_IN</code> - 62 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
+| `bytm_config_export_import_title` | `Export/Import Configuration` |
+| `bytm_config_import_desc` | `Paste the configuration you want to import into the field below, then click the import button:` |
+| `bytm_config_export_desc` | `Copy the following text to export your configuration. Warning: it may contain sensitive data.` |
 | `export_import` | `Export/Import` |
 | `click_to_reveal` | `(click to reveal)` |
 | `start_import_tooltip` | `Click to import the data you pasted above` |
 | `copy` | `Copy` |
+| `copied_to_clipboard` | `Copied to clipboard!` |
 | `close_tooltip` | `Click to close` |
 | `new_entry` | `New entry` |
 | `new_entry_tooltip` | `Click to create a new entry` |
@@ -270,7 +286,7 @@ This means to figure out which keys are untranslated, you will need to manually
 | `auto_liked_a_channels_song` | `Liked song by %1` |
 | `auto_liked_a_channels_video` | `Liked video by %1` |
 | `auto_like_export_or_import_tooltip` | `Export or import your auto-liked channels` |
-| `auto_like_export_import_title` | `Export/import auto-liked channels` |
+| `auto_like_export_import_title` | `Export/Import auto-liked channels` |
 | `auto_like_export_desc` | `Copy the following text to export your auto-liked channels.` |
 | `auto_like_import_desc` | `Paste the auto-liked channels you want to import into the field below, then click the import button:` |
 | `vote_label_likes` | `%1 likes` |
@@ -305,14 +321,18 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>ja_JA</code> - 58 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>ja_JA</code> - 62 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
+| `bytm_config_export_import_title` | `Export/Import Configuration` |
+| `bytm_config_import_desc` | `Paste the configuration you want to import into the field below, then click the import button:` |
+| `bytm_config_export_desc` | `Copy the following text to export your configuration. Warning: it may contain sensitive data.` |
 | `export_import` | `Export/Import` |
 | `click_to_reveal` | `(click to reveal)` |
 | `start_import_tooltip` | `Click to import the data you pasted above` |
 | `copy` | `Copy` |
+| `copied_to_clipboard` | `Copied to clipboard!` |
 | `close_tooltip` | `Click to close` |
 | `new_entry` | `New entry` |
 | `new_entry_tooltip` | `Click to create a new entry` |
@@ -335,7 +355,7 @@ This means to figure out which keys are untranslated, you will need to manually
 | `auto_liked_a_channels_song` | `Liked song by %1` |
 | `auto_liked_a_channels_video` | `Liked video by %1` |
 | `auto_like_export_or_import_tooltip` | `Export or import your auto-liked channels` |
-| `auto_like_export_import_title` | `Export/import auto-liked channels` |
+| `auto_like_export_import_title` | `Export/Import auto-liked channels` |
 | `auto_like_export_desc` | `Copy the following text to export your auto-liked channels.` |
 | `auto_like_import_desc` | `Paste the auto-liked channels you want to import into the field below, then click the import button:` |
 | `vote_label_likes` | `%1 likes` |
@@ -370,14 +390,18 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>pt_BR</code> - 58 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>pt_BR</code> - 62 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
+| `bytm_config_export_import_title` | `Export/Import Configuration` |
+| `bytm_config_import_desc` | `Paste the configuration you want to import into the field below, then click the import button:` |
+| `bytm_config_export_desc` | `Copy the following text to export your configuration. Warning: it may contain sensitive data.` |
 | `export_import` | `Export/Import` |
 | `click_to_reveal` | `(click to reveal)` |
 | `start_import_tooltip` | `Click to import the data you pasted above` |
 | `copy` | `Copy` |
+| `copied_to_clipboard` | `Copied to clipboard!` |
 | `close_tooltip` | `Click to close` |
 | `new_entry` | `New entry` |
 | `new_entry_tooltip` | `Click to create a new entry` |
@@ -400,7 +424,7 @@ This means to figure out which keys are untranslated, you will need to manually
 | `auto_liked_a_channels_song` | `Liked song by %1` |
 | `auto_liked_a_channels_video` | `Liked video by %1` |
 | `auto_like_export_or_import_tooltip` | `Export or import your auto-liked channels` |
-| `auto_like_export_import_title` | `Export/import auto-liked channels` |
+| `auto_like_export_import_title` | `Export/Import auto-liked channels` |
 | `auto_like_export_desc` | `Copy the following text to export your auto-liked channels.` |
 | `auto_like_import_desc` | `Paste the auto-liked channels you want to import into the field below, then click the import button:` |
 | `vote_label_likes` | `%1 likes` |
@@ -435,14 +459,18 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>zh_CN</code> - 58 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>zh_CN</code> - 62 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
+| `bytm_config_export_import_title` | `Export/Import Configuration` |
+| `bytm_config_import_desc` | `Paste the configuration you want to import into the field below, then click the import button:` |
+| `bytm_config_export_desc` | `Copy the following text to export your configuration. Warning: it may contain sensitive data.` |
 | `export_import` | `Export/Import` |
 | `click_to_reveal` | `(click to reveal)` |
 | `start_import_tooltip` | `Click to import the data you pasted above` |
 | `copy` | `Copy` |
+| `copied_to_clipboard` | `Copied to clipboard!` |
 | `close_tooltip` | `Click to close` |
 | `new_entry` | `New entry` |
 | `new_entry_tooltip` | `Click to create a new entry` |
@@ -465,7 +493,7 @@ This means to figure out which keys are untranslated, you will need to manually
 | `auto_liked_a_channels_song` | `Liked song by %1` |
 | `auto_liked_a_channels_video` | `Liked video by %1` |
 | `auto_like_export_or_import_tooltip` | `Export or import your auto-liked channels` |
-| `auto_like_export_import_title` | `Export/import auto-liked channels` |
+| `auto_like_export_import_title` | `Export/Import auto-liked channels` |
 | `auto_like_export_desc` | `Copy the following text to export your auto-liked channels.` |
 | `auto_like_import_desc` | `Paste the auto-liked channels you want to import into the field below, then click the import button:` |
 | `vote_label_likes` | `%1 likes` |

+ 6 - 1
assets/translations/en_US.json

@@ -12,6 +12,10 @@
     "reload_tooltip": "Reload the page",
     "feature_requires_reload": "Changing this feature requires a page reload",
     "version_tooltip": "Version %1 (build %2) - click to open the changelog",
+
+    "bytm_config_export_import_title": "Export/Import Configuration",
+    "bytm_config_import_desc": "Paste the configuration you want to import into the field below, then click the import button:",
+    "bytm_config_export_desc": "Copy the following text to export your configuration. Warning: it may contain sensitive data.",
     "export_import": "Export/Import",
     "export": "Export",
     "export_hint": "Copy the following text to export your configuration.\nWarning: it may contain sensitive data.",
@@ -33,6 +37,7 @@
     "copy_to_clipboard_error": "Couldn't copy the text to the clipboard. Please copy it from here manually:\n%1",
     "copy_config_tooltip": "Copy the configuration to your clipboard",
     "copied": "Copied!",
+    "copied_to_clipboard": "Copied to clipboard!",
     "copy_hidden_value": "Copy hidden value",
     "copy_hidden_tooltip": "Click to copy the hidden value - this is sensitive data ⚠️",
     "open_github": "Open %1 on GitHub",
@@ -152,7 +157,7 @@
     "auto_liked_a_channels_song": "Liked song by %1",
     "auto_liked_a_channels_video": "Liked video by %1",
     "auto_like_export_or_import_tooltip": "Export or import your auto-liked channels",
-    "auto_like_export_import_title": "Export/import auto-liked channels",
+    "auto_like_export_import_title": "Export/Import auto-liked channels",
     "auto_like_export_desc": "Copy the following text to export your auto-liked channels.",
     "auto_like_import_desc": "Paste the auto-liked channels you want to import into the field below, then click the import button:",
 

+ 10 - 15
src/components/ExportImportDialog.ts

@@ -3,7 +3,7 @@ import { t, type TrKey } from "../utils/translations.js";
 import { scriptInfo } from "../constants.js";
 import { onInteraction } from "../utils/input.js";
 import { copyToClipboard } from "../utils/dom.js";
-import { createLongBtn, showToast } from "./index.js";
+import { createLongBtn, createRipple, showToast } from "./index.js";
 import "./ExportImportDialog.css";
 
 type ExImDialogOpts =
@@ -93,24 +93,19 @@ export class ExImDialog extends BytmDialog {
       const exportCenterBtnCont = document.createElement("div");
       exportCenterBtnCont.classList.add("bytm-exim-dialog-center-btn-cont");
 
-      const copyBtn = await createLongBtn({
+      const copyBtn = createRipple(await createLongBtn({
         title: t("copy_hidden_value"),
         text: t("copy"),
         resourceName: "icon-experimental",
         async onClick({ shiftKey }) {
           const copyData = shiftKey && opts.exportDataSpecial ? opts.exportDataSpecial : opts.exportData;
-          try {
-            copyToClipboard(typeof copyData === "function" ? await copyData() : copyData);
-            await showToast({
-              message: t("copied"),
-              title: t("copied"),
-            });
-          }
-          catch {
-            alert(t("copy_to_clipboard_error", typeof copyData === "function" ? await copyData() : copyData));
-          }
+          copyToClipboard(typeof copyData === "function" ? await copyData() : copyData);
+          await showToast({
+            position: "bl",
+            message: t("copied_to_clipboard"),
+          });
         },
-      });
+      }));
 
       exportCenterBtnCont.appendChild(copyBtn);
       exportPane.append(descEl, dataEl, exportCenterBtnCont);
@@ -135,12 +130,12 @@ export class ExImDialog extends BytmDialog {
       const importCenterBtnCont = document.createElement("div");
       importCenterBtnCont.classList.add("bytm-exim-dialog-center-btn-cont");
 
-      const importBtn = await createLongBtn({
+      const importBtn = createRipple(await createLongBtn({
         title: t("start_import_tooltip"),
         text: t("import"),
         resourceName: "icon-experimental",
         onClick: () => opts.onImport(dataEl.value),
-      });
+      }));
 
       importCenterBtnCont.appendChild(importBtn);
       importPane.append(descEl, dataEl, importCenterBtnCont);

+ 3 - 2
src/components/toast.ts

@@ -7,6 +7,7 @@ import "./toast.css";
 type ToastPos = "tl" | "tr" | "bl" | "br";
 
 type ToastProps = {
+  /** Duration in seconds */
   duration?: number;
   position?: ToastPos;
 } & (
@@ -73,7 +74,7 @@ export async function showIconToast({
   });
 }
 
-/** Shows a toast message in the bottom left corner of the screen by default */
+/** Shows a toast message in the top right corner of the screen by default */
 export async function showToast({
   duration = getFeature("toastDuration"),
   position = "tr",
@@ -106,7 +107,7 @@ export async function showToast({
 
   toastElem.classList.add("visible", `pos-${position}`);
 
-  timeout = setTimeout(async () => await closeToast(), duration);
+  timeout = setTimeout(async () => await closeToast(), duration * 1000);
 }
 
 /** Closes the currently open toast */

+ 0 - 2
src/dialogs/autoLike.ts

@@ -56,8 +56,6 @@ export async function getAutoLikeDialog() {
         try {
           const parsed = await tryToDecompressAndParse<AutoLikeData>(data);
 
-          if(!parsed)
-            throw new Error("No valid data found in the imported string");
           if(!parsed || typeof parsed !== "object")
             return alert(t("import_error_invalid"));
           if(!parsed.channels || typeof parsed.channels !== "object" || Object.keys(parsed.channels).length === 0)

+ 79 - 6
src/menu/menu_old.ts

@@ -1,13 +1,13 @@
-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 { compress, debounce, isScrollable, type Stringifiable } from "@sv443-network/userutils";
+import { defaultData, formatVersion, getFeature, getFeatures, migrations, setFeatures } from "../config.js";
+import { buildNumber, compressionFormat, 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 { error, getResourceUrl, info, log, resourceToHTMLString, getLocale, hasKey, initTranslations, setLocale, t, arrayWithSeparators, tp, type TrKey, onInteraction, getDomain, copyToClipboard, warn, compressionSupported, tryToDecompressAndParse } from "../utils/index.js";
+import { emitSiteEvent, 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 { BytmDialog, ExImDialog, createHotkeyInput, createToggleInput, openDialogs, setCurrentDialogId } from "../components/index.js";
 import { emitInterface } from "../interface.js";
 import pkg from "../../package.json" with { type: "json" };
 
@@ -173,6 +173,77 @@ async function mountCfgMenu() {
   reloadFooterEl.appendChild(reloadTxtEl);
   reloadFooterCont.appendChild(reloadFooterEl);
 
+  const exImDlg = new ExImDialog({
+    id: "bytm-config-import-export",
+    width: 800,
+    height: 600,
+    // try to compress the data if possible
+    exportData: async () => await compressionSupported()
+      ? await compress(JSON.stringify(getFeatures()), compressionFormat, "string")
+      : JSON.stringify(getFeatures()),
+    // copy plain when shift-clicking the copy button
+    exportDataSpecial: () => JSON.stringify(getFeatures()),
+    onImport: async (data) => {
+      try {
+        const parsed = await tryToDecompressAndParse<{ data: FeatureConfig, formatVersion: number }>(data.trim());
+        if(!parsed || 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);
+        exImDlg.unmount();
+      }
+      catch(err) {
+        warn("Couldn't import configuration:", err);
+        alert(t("import_error_invalid"));
+      }
+    },
+    trKeyTitle: "bytm_config_export_import_title",
+    trKeyDescImport: "bytm_config_import_desc",
+    trKeyDescExport: "bytm_config_export_desc",
+    dataHidden: false,
+  });
+
+  const exportImportBtn = document.createElement("button");
+  exportImportBtn.classList.add("bytm-btn");
+  exportImportBtn.textContent = exportImportBtn.ariaLabel = exportImportBtn.title = t("export_import");
+  onInteraction(exportImportBtn, async () => await exImDlg.open());
+
   const exportElem = document.createElement("button");
   exportElem.classList.add("bytm-btn");
   exportElem.ariaLabel = exportElem.title = t("export_tooltip");
@@ -199,6 +270,8 @@ async function mountCfgMenu() {
 
   const buttonsCont = document.createElement("div");
   buttonsCont.classList.add("bytm-menu-footer-buttons-cont");
+
+  buttonsCont.appendChild(exportImportBtn);
   buttonsCont.appendChild(exportElem);
   buttonsCont.appendChild(importElem);