Переглянути джерело

feat: lots of work on the new ex-& import dialog

Sv443 10 місяців тому
батько
коміт
c257a036da

+ 58 - 16
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) | 262 (default locale) |  |
-| ‼️ | [`de_DE`](./de_DE.json) | `210/262` (80.2%) | ─ |
-| ─ | [`en_UK`](./en_UK.json) | `262` (100%) | `en_US` |
-| ‼️ | [`es_ES`](./es_ES.json) | `210/262` (80.2%) | ─ |
-| ‼️ | [`fr_FR`](./fr_FR.json) | `210/262` (80.2%) | ─ |
-| ‼️ | [`hi_IN`](./hi_IN.json) | `210/262` (80.2%) | ─ |
-| ‼️ | [`ja_JA`](./ja_JA.json) | `210/262` (80.2%) | ─ |
-| ‼️ | [`pt_BR`](./pt_BR.json) | `210/262` (80.2%) | ─ |
-| ‼️ | [`zh_CN`](./zh_CN.json) | `210/262` (80.2%) | ─ |
+| ─ | [`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%) | ─ |
 
 <sub>
 ✅ - Fully translated
@@ -45,11 +45,14 @@ This means to figure out which keys are untranslated, you will need to manually
 
 ### Missing keys:
 
-<details><summary><code>de_DE</code> - 52 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>de_DE</code> - 58 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
 | `export_import` | `Export/Import` |
+| `click_to_reveal` | `(click to reveal)` |
+| `start_import_tooltip` | `Click to import the data you pasted above` |
+| `copy` | `Copy` |
 | `close_tooltip` | `Click to close` |
 | `new_entry` | `New entry` |
 | `new_entry_tooltip` | `Click to create a new entry` |
@@ -72,6 +75,9 @@ 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_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` |
 | `vote_label_dislikes` | `%1 dislikes` |
 | `vote_ratio_disabled` | `Disabled` |
@@ -104,11 +110,14 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>es_ES</code> - 52 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>es_ES</code> - 58 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
 | `export_import` | `Export/Import` |
+| `click_to_reveal` | `(click to reveal)` |
+| `start_import_tooltip` | `Click to import the data you pasted above` |
+| `copy` | `Copy` |
 | `close_tooltip` | `Click to close` |
 | `new_entry` | `New entry` |
 | `new_entry_tooltip` | `Click to create a new entry` |
@@ -131,6 +140,9 @@ 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_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` |
 | `vote_label_dislikes` | `%1 dislikes` |
 | `vote_ratio_disabled` | `Disabled` |
@@ -163,11 +175,14 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>fr_FR</code> - 52 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>fr_FR</code> - 58 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
 | `export_import` | `Export/Import` |
+| `click_to_reveal` | `(click to reveal)` |
+| `start_import_tooltip` | `Click to import the data you pasted above` |
+| `copy` | `Copy` |
 | `close_tooltip` | `Click to close` |
 | `new_entry` | `New entry` |
 | `new_entry_tooltip` | `Click to create a new entry` |
@@ -190,6 +205,9 @@ 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_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` |
 | `vote_label_dislikes` | `%1 dislikes` |
 | `vote_ratio_disabled` | `Disabled` |
@@ -222,11 +240,14 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>hi_IN</code> - 52 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>hi_IN</code> - 58 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
 | `export_import` | `Export/Import` |
+| `click_to_reveal` | `(click to reveal)` |
+| `start_import_tooltip` | `Click to import the data you pasted above` |
+| `copy` | `Copy` |
 | `close_tooltip` | `Click to close` |
 | `new_entry` | `New entry` |
 | `new_entry_tooltip` | `Click to create a new entry` |
@@ -249,6 +270,9 @@ 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_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` |
 | `vote_label_dislikes` | `%1 dislikes` |
 | `vote_ratio_disabled` | `Disabled` |
@@ -281,11 +305,14 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>ja_JA</code> - 52 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>ja_JA</code> - 58 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
 | `export_import` | `Export/Import` |
+| `click_to_reveal` | `(click to reveal)` |
+| `start_import_tooltip` | `Click to import the data you pasted above` |
+| `copy` | `Copy` |
 | `close_tooltip` | `Click to close` |
 | `new_entry` | `New entry` |
 | `new_entry_tooltip` | `Click to create a new entry` |
@@ -308,6 +335,9 @@ 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_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` |
 | `vote_label_dislikes` | `%1 dislikes` |
 | `vote_ratio_disabled` | `Disabled` |
@@ -340,11 +370,14 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>pt_BR</code> - 52 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>pt_BR</code> - 58 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
 | `export_import` | `Export/Import` |
+| `click_to_reveal` | `(click to reveal)` |
+| `start_import_tooltip` | `Click to import the data you pasted above` |
+| `copy` | `Copy` |
 | `close_tooltip` | `Click to close` |
 | `new_entry` | `New entry` |
 | `new_entry_tooltip` | `Click to create a new entry` |
@@ -367,6 +400,9 @@ 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_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` |
 | `vote_label_dislikes` | `%1 dislikes` |
 | `vote_ratio_disabled` | `Disabled` |
@@ -399,11 +435,14 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>zh_CN</code> - 52 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>zh_CN</code> - 58 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
 | `export_import` | `Export/Import` |
+| `click_to_reveal` | `(click to reveal)` |
+| `start_import_tooltip` | `Click to import the data you pasted above` |
+| `copy` | `Copy` |
 | `close_tooltip` | `Click to close` |
 | `new_entry` | `New entry` |
 | `new_entry_tooltip` | `Click to create a new entry` |
@@ -426,6 +465,9 @@ 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_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` |
 | `vote_label_dislikes` | `%1 dislikes` |
 | `vote_ratio_disabled` | `Disabled` |

+ 0 - 1
assets/translations/de_DE.json

@@ -19,7 +19,6 @@
     "import": "Importieren",
     "import_hint": "Füge die Einstellungen, die du importieren möchtest, in das Feld unten ein und klicke dann auf Importieren:",
     "import_tooltip": "Importiere Einstellungen, die du zuvor exportiert hast",
-    "start_import_tooltip": "Klicke um die Einstellungen, die du oben eingefügt hast, zu importieren",
     "import_error_invalid": "Die importierten Daten sind ungültig",
     "import_error_no_format_version": "Die importierten Daten enthalten keine Format-Version",
     "import_error_no_data": "Das importierte Objekt enthält keine Daten",

+ 6 - 1
assets/translations/en_US.json

@@ -15,18 +15,20 @@
     "export_import": "Export/Import",
     "export": "Export",
     "export_hint": "Copy the following text to export your configuration.\nWarning: it may contain sensitive data.",
+    "click_to_reveal": "(click to reveal)",
     "click_to_reveal_sensitive_info": "(click to reveal sensitive information)",
     "export_tooltip": "Export your current configuration",
     "import": "Import",
     "import_hint": "Paste the configuration you want to import into the field below, then click the import button:",
     "import_tooltip": "Import a configuration you have previously exported",
-    "start_import_tooltip": "Click to import the configuration you pasted above",
+    "start_import_tooltip": "Click to import the data you pasted above",
     "import_error_invalid": "The imported data is invalid",
     "import_error_no_format_version": "The imported data does not contain a format version",
     "import_error_no_data": "The imported object does not contain any data",
     "import_error_wrong_format_version": "The imported data is in an unsupported format version (expected %1 or lower but got %2)",
     "import_success_confirm_reload": "Successfully imported the configuration.\nDo you want to reload the page now to apply changes?",
     "reset_config_confirm": "Do you really want to reset all settings to their default values?\nThe page will be automatically reloaded.",
+    "copy": "Copy",
     "copy_to_clipboard": "Copy to clipboard",
     "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",
@@ -150,6 +152,9 @@
     "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_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",
     "vote_label_dislikes": "%1 dislikes",

+ 0 - 1
assets/translations/es_ES.json

@@ -19,7 +19,6 @@
     "import": "Importar",
     "import_hint": "Pegue la configuración que desea importar en el campo a continuación, luego haga clic en el botón de importación:",
     "import_tooltip": "Importe una configuración que haya exportado anteriormente",
-    "start_import_tooltip": "Haga clic para importar la configuración que pegó arriba",
     "import_error_invalid": "Los datos importados no son válidos",
     "import_error_no_format_version": "Los datos importados no contienen una versión de formato",
     "import_error_no_data": "El objeto importado no contiene ningún dato",

+ 0 - 1
assets/translations/fr_FR.json

@@ -19,7 +19,6 @@
     "import": "Importer",
     "import_hint": "Collez la configuration que vous souhaitez importer dans le champ ci-dessous, puis cliquez sur le bouton d'importation:",
     "import_tooltip": "Importer une configuration que vous avez précédemment exportée",
-    "start_import_tooltip": "Cliquez pour importer la configuration que vous avez collée ci-dessus",
     "import_error_invalid": "Les données importées ne sont pas valides",
     "import_error_no_format_version": "Les données importées ne contiennent pas de version de format",
     "import_error_no_data": "L'objet importé ne contient aucune donnée",

+ 0 - 1
assets/translations/hi_IN.json

@@ -19,7 +19,6 @@
     "import": "आयात",
     "import_hint": "आप जो कॉन्फ़िगरेशन आयात करना चाहते हैं, उसे नीचे दिए गए फ़ील्ड में पेस्ट करें, फिर आयात बटन पर क्लिक करें:",
     "import_tooltip": "आपने पहले से निर्यात की गई कॉन्फ़िगरेशन आयात करें",
-    "start_import_tooltip": "आपने ऊपर पेस्ट की गई कॉन्फ़िगरेशन को आयात करने के लिए क्लिक करें",
     "import_error_invalid": "आयात की गई डेटा अमान्य है",
     "import_error_no_format_version": "आयात की गई डेटा में कोई फ़ॉर्मेट संस्करण नहीं है",
     "import_error_no_data": "आयात किया गया ऑब्जेक्ट किसी भी डेटा को नहीं शामिल करता है",

+ 0 - 1
assets/translations/ja_JA.json

@@ -19,7 +19,6 @@
     "import": "インポート",
     "import_hint": "インポートしたい構成を以下のフィールドに貼り付け、インポートボタンをクリックしてください。",
     "import_tooltip": "以前にエクスポートした構成をインポートする",
-    "start_import_tooltip": "上記に貼り付けた構成をインポートするにはクリックしてください",
     "import_error_invalid": "インポートされたデータが無効です",
     "import_error_no_format_version": "インポートされたデータにフォーマットバージョンが含まれていません",
     "import_error_no_data": "インポートされたオブジェクトにデータが含まれていません",

+ 0 - 1
assets/translations/pt_BR.json

@@ -19,7 +19,6 @@
     "import": "Importar",
     "import_hint": "Cole a configuração que você deseja importar no campo abaixo e clique no botão de importação:",
     "import_tooltip": "Importe uma configuração que você exportou anteriormente",
-    "start_import_tooltip": "Clique para importar a configuração que você colou acima",
     "import_error_invalid": "Os dados importados são inválidos",
     "import_error_no_format_version": "Os dados importados não contêm uma versão de formato",
     "import_error_no_data": "O objeto importado não contém nenhum dado",

+ 0 - 1
assets/translations/zh_CN.json

@@ -19,7 +19,6 @@
     "import": "导入",
     "import_hint": "将要导入的配置粘贴到下面的字段中,然后点击导入按钮:",
     "import_tooltip": "导入您先前导出的配置",
-    "start_import_tooltip": "点击导入您刚刚粘贴的配置",
     "import_error_invalid": "导入的数据无效",
     "import_error_no_format_version": "导入的数据不包含格式版本",
     "import_error_no_data": "导入的对象不包含任何数据",

+ 26 - 0
dist/BetterYTM.css

@@ -630,6 +630,32 @@ body .bytm-ripple.slower {
   }
 }
 
+.bytm-exim-dialog-panes-cont {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  flex-wrap: nowrap;
+  height: 100%;
+  overflow: hidden;
+}
+
+.bytm-exim-dialog-pane {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  width: 48%;
+}
+
+.bytm-exim-dialog-center-btn-cont {
+  display: flex;
+  justify-content: center;
+}
+
+.bytm-exim-dialog-pane textarea {
+  height: 200px;
+  resize: none;
+}
+
 .bytm-hotkey-wrapper {
   display: flex;
   flex-direction: row;

+ 25 - 0
src/components/ExportImportDialog.css

@@ -0,0 +1,25 @@
+.bytm-exim-dialog-panes-cont {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  flex-wrap: nowrap;
+  height: 100%;
+  overflow: hidden;
+}
+
+.bytm-exim-dialog-pane {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  width: 48%;
+}
+
+.bytm-exim-dialog-center-btn-cont {
+  display: flex;
+  justify-content: center;
+}
+
+.bytm-exim-dialog-pane textarea {
+  height: 200px;
+  resize: none;
+}

+ 161 - 0
src/components/ExportImportDialog.ts

@@ -0,0 +1,161 @@
+import { BytmDialog, type BytmDialogOptions } from "./BytmDialog.js";
+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 "./ExportImportDialog.css";
+
+type ExImDialogOpts =
+  Omit<BytmDialogOptions, "renderHeader" | "renderBody" | "renderFooter">
+  & {
+    /** The data to export (or a function that returns the data as string, sync or async) */
+    exportData: string | (() => string | Promise<string>);
+    /** Optional variant of the data, used for special cases like when shift-clicking the copy button */
+    exportDataSpecial?: string | (() => string | Promise<string>);
+    /** Function that gets called when the user imports data */
+    onImport: (data: string) => void;
+    /** Translation key for the dialog title */
+    trKeyTitle: TrKey | (string & {});
+    /** Translation key for the dialog description when importing */
+    trKeyDescImport: TrKey | (string & {});
+    /** Translation key for the dialog description when exporting */
+    trKeyDescExport: TrKey | (string & {});
+    /** Whether the data should be hidden by default when exporting and importing */
+    dataHidden?: boolean;
+  };
+
+type ExImMode = "export" | "import";
+
+/** Generic dialog for exporting and importing any string of data */
+export class ExImDialog extends BytmDialog {
+  public mode: ExImMode = "export";
+
+  constructor(options: ExImDialogOpts) {
+    super({
+      renderHeader: () => ExImDialog.renderHeader(options),
+      renderBody: () => ExImDialog.renderBody(options),
+      closeOnBgClick: true,
+      closeOnEscPress: true,
+      closeBtnEnabled: true,
+      destroyOnClose: true,
+      small: true,
+      ...options,
+    });
+  }
+
+  static async renderHeader(opts: ExImDialogOpts): Promise<HTMLElement> {
+    const headerEl = document.createElement("h2");
+    headerEl.classList.add("bytm-menu-title");
+    headerEl.role = "heading";
+    headerEl.ariaLevel = "1";
+    headerEl.tabIndex = 0;
+    headerEl.textContent = headerEl.ariaLabel = t(opts.trKeyTitle as "_", scriptInfo.name);
+
+    return headerEl;
+  }
+
+  static async renderBody(opts: ExImDialogOpts): Promise<HTMLElement> {
+    // TODO: body
+    // two horizontal panes:
+    // - export:
+    //   - description element with trKeyDescExport
+    //   - textarea with data, if dataHidden is true, show a button to reveal it
+    //   - button to copy the data to clipboard
+    // - import:
+    //   - description element with trKeyDescImport
+    //   - textarea for user to paste data, if dataHidden is true, use password masking
+
+    const panesCont = document.createElement("div");
+    panesCont.classList.add("bytm-exim-dialog-panes-cont");
+
+    //#region export
+
+    const exportPane = document.createElement("div");
+    exportPane.classList.add("bytm-exim-dialog-pane", "export");
+
+    {
+      const descEl = document.createElement("p");
+      descEl.classList.add("bytm-exim-dialog-desc");
+      descEl.role = "note";
+      descEl.tabIndex = 0;
+      descEl.textContent = descEl.ariaLabel = t(opts.trKeyDescExport);
+
+      const dataEl = document.createElement("textarea");
+      dataEl.classList.add("bytm-exim-dialog-data");
+      dataEl.readOnly = true;
+      dataEl.tabIndex = 0;
+      dataEl.value = t("click_to_reveal");
+      onInteraction(dataEl, async () => {
+        dataEl.value = typeof opts.exportData === "function" ? await opts.exportData() : opts.exportData;
+      });
+
+      const exportCenterBtnCont = document.createElement("div");
+      exportCenterBtnCont.classList.add("bytm-exim-dialog-center-btn-cont");
+
+      const copyBtn = 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));
+          }
+        },
+      });
+
+      exportCenterBtnCont.appendChild(copyBtn);
+      exportPane.append(descEl, dataEl, exportCenterBtnCont);
+    }
+
+    //#region import
+
+    const importPane = document.createElement("div");
+    importPane.classList.add("bytm-exim-dialog-pane", "import");
+
+    {
+      const descEl = document.createElement("p");
+      descEl.classList.add("bytm-exim-dialog-desc");
+      descEl.role = "note";
+      descEl.tabIndex = 0;
+      descEl.textContent = descEl.ariaLabel = t(opts.trKeyDescImport);
+
+      const dataEl = document.createElement("textarea");
+      dataEl.classList.add("bytm-exim-dialog-data");
+      dataEl.tabIndex = 0;
+
+      const importCenterBtnCont = document.createElement("div");
+      importCenterBtnCont.classList.add("bytm-exim-dialog-center-btn-cont");
+
+      const importBtn = 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);
+    }
+
+    panesCont.append(exportPane, importPane);
+
+    // TODO: footer
+    // - when export:
+    //   - copy button
+    //     - on click, copy exportData to clipboard
+    //     - on shift-click, copy exportDataSpecial to clipboard, fall back to exportData
+    // - when import:
+    //   - import button
+
+    return panesCont;
+  }
+}

+ 0 - 71
src/components/ImportExportDialog.ts

@@ -1,71 +0,0 @@
-import { BytmDialog, type BytmDialogOptions } from "./BytmDialog.js";
-import type { TrKey } from "../utils/translations.js";
-
-type ImportExportDialogOpts =
-  Omit<BytmDialogOptions, "renderHeader" | "renderBody" | "renderFooter">
-  & {
-    /** The data to export (or a function that returns the data as string, sync or async) */
-    exportData: string | (() => string | Promise<string>);
-    /** Optional variant of the data, used for special cases like when shift-clicking the copy button */
-    exportDataSpecial?: string | (() => string | Promise<string>);
-    /** Function that gets called when the user imports data */
-    onImport: (data: string) => void;
-    /** Translation key for the dialog title */
-    trKeyTitle: TrKey | (string & {});
-    /** Translation key for the dialog description when importing */
-    trKeyDescImport: TrKey | (string & {});
-    /** Translation key for the dialog description when exporting */
-    trKeyDescExport: TrKey | (string & {});
-    /** Whether the data should be hidden by default when exporting and importing */
-    dataHidden?: boolean;
-  };
-
-/** Generic dialog for importing and exporting any string of data */
-export class ImportExportDialog extends BytmDialog {
-  constructor(options: ImportExportDialogOpts) {
-    super({
-      renderHeader: () => ImportExportDialog.renderHeader(options),
-      renderBody: () => ImportExportDialog.renderBody(options),
-      renderFooter: () => ImportExportDialog.renderFooter(options),
-      closeOnBgClick: true,
-      closeOnEscPress: true,
-      closeBtnEnabled: true,
-      destroyOnClose: true,
-      small: true,
-      ...options,
-    });
-  }
-
-  static async renderHeader(_options: ImportExportDialogOpts): Promise<HTMLElement> {
-    // TODO:
-    // render header with trKeyTitle
-
-    return document.createElement("div");
-  }
-
-  static async renderBody(_opts: ImportExportDialogOpts): Promise<HTMLElement> {
-    // TODO:
-    // two horizontal tabs:
-    // - export:
-    //   - description element with trKeyDescExport
-    //   - textarea with data, if dataHidden is true, show a button to reveal it
-    //   - button to copy the data to clipboard
-    // - import:
-    //   - description element with trKeyDescImport
-    //   - textarea for user to paste data, if dataHidden is true, use password masking
-
-    return document.createElement("div");
-  }
-
-  static async renderFooter(_options: ImportExportDialogOpts): Promise<HTMLElement> {
-    // TODO:
-    // - when export:
-    //   - copy button
-    //     - on click, copy exportData to clipboard
-    //     - on shift-click, copy exportDataSpecial to clipboard, fall back to exportData
-    // - when import:
-    //   - import button
-
-    return document.createElement("div");
-  }
-}

+ 1 - 0
src/components/index.ts

@@ -1,5 +1,6 @@
 export * from "./BytmDialog.js";
 export * from "./circularButton.js";
+export * from "./ExportImportDialog.js";
 export * from "./hotkeyInput.js";
 export * from "./longButton.js";
 export * from "./ripple.js";

+ 5 - 5
src/dialogs/autoLike.ts

@@ -3,13 +3,13 @@ import { compressionSupported, error, getDomain, log, onInteraction, parseChanne
 import { BytmDialog, createCircularBtn, createToggleInput } from "../components/index.js";
 import { autoLikeStore, initAutoLikeStore } from "../features/index.js";
 import { siteEvents } from "../siteEvents.js";
-import { ImportExportDialog } from "../components/ImportExportDialog.js";
+import { ExImDialog } from "../components/ExportImportDialog.js";
 import { compressionFormat } from "../constants.js";
 import type { AutoLikeData } from "../types.js";
 import "./autoLike.css";
 
 let autoLikeDialog: BytmDialog | null = null;
-let autoLikeImExDialog: ImportExportDialog | null = null;
+let autoLikeImExDialog: ExImDialog | null = null;
 
 /** Creates and/or returns the import dialog */
 export async function getAutoLikeDialog() {
@@ -42,10 +42,10 @@ export async function getAutoLikeDialog() {
   }
   
   if(!autoLikeImExDialog) {
-    autoLikeImExDialog = new ImportExportDialog({
+    autoLikeImExDialog = new ExImDialog({
       id: "auto-like-channels-import-export",
-      width: 600,
-      height: 500,
+      width: 800,
+      height: 600,
       // try to compress the data if possible
       exportData: async () => await compressionSupported()
         ? await compress(JSON.stringify(autoLikeStore.getData()), compressionFormat, "string")