Explorar o código

feat: generic import/export dialog stuff

Sv443 hai 10 meses
pai
achega
362baa625a

+ 3 - 3
src/components/ImportExportDialog.ts

@@ -11,11 +11,11 @@ type ImportExportDialogOpts =
     /** Function that gets called when the user imports data */
     onImport: (data: string) => void;
     /** Translation key for the dialog title */
-    trKeyTitle: TrKey;
+    trKeyTitle: TrKey | (string & {});
     /** Translation key for the dialog description when importing */
-    trKeyDescImport: TrKey;
+    trKeyDescImport: TrKey | (string & {});
     /** Translation key for the dialog description when exporting */
-    trKeyDescExport: TrKey;
+    trKeyDescExport: TrKey | (string & {});
     /** Whether the data should be hidden by default when exporting and importing */
     dataHidden?: boolean;
   };

+ 47 - 6
src/dialogs/autoLike.ts

@@ -1,11 +1,14 @@
-import { debounce } from "@sv443-network/userutils";
-import { getDomain, log, onInteraction, parseChannelIdFromUrl, t } from "../utils/index.js";
+import { compress, debounce } from "@sv443-network/userutils";
+import { compressionSupported, error, getDomain, log, onInteraction, parseChannelIdFromUrl, t, tryToDecompressAndParse } from "../utils/index.js";
 import { BytmDialog, createCircularBtn, createToggleInput } from "../components/index.js";
-import { autoLikeStore, initAutoLikeStore } from "../features/index.js";
+import { autoLikeStore, initAutoLikeStore, type AutoLikeData } from "../features/index.js";
 import { siteEvents } from "../siteEvents.js";
+import { ImportExportDialog } from "../components/ImportExportDialog.js";
+import { compressionFormat } from "../constants.js";
 import "./autoLike.css";
 
 let autoLikeDialog: BytmDialog | null = null;
+let autoLikeImExDialog: ImportExportDialog | null = null;
 
 /** Creates and/or returns the import dialog */
 export async function getAutoLikeDialog() {
@@ -27,14 +30,52 @@ export async function getAutoLikeDialog() {
     });
 
     siteEvents.on("autoLikeChannelsUpdated", async () => {
+      if(autoLikeImExDialog?.isOpen())
+        autoLikeImExDialog.unmount();
       if(autoLikeDialog?.isOpen()) {
-        autoLikeDialog.close();
         autoLikeDialog.unmount();
         await autoLikeDialog.open();
         log("Auto-like channels updated, refreshed dialog");
       }
     });
   }
+  
+  if(!autoLikeImExDialog) {
+    autoLikeImExDialog = new ImportExportDialog({
+      id: "auto-like-channels-import-export",
+      width: 600,
+      height: 500,
+      // try to compress the data if possible
+      exportData: async () => await compressionSupported()
+        ? await compress(JSON.stringify(autoLikeStore.getData()), compressionFormat, "string")
+        : JSON.stringify(autoLikeStore.getData()),
+      // copy plain when shift-clicking the copy button
+      exportDataSpecial: () => JSON.stringify(autoLikeStore.getData()),
+      onImport: async (data) => {
+        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)
+            return alert(t("import_error_no_data"));
+
+          await autoLikeStore.setData(parsed);
+          siteEvents.emit("autoLikeChannelsUpdated");
+        }
+        catch(err) {
+          error("Couldn't import auto-like channels data:", err);
+        }
+      },
+      trKeyTitle: "auto_like_export_import_title",
+      trKeyDescImport: "auto_like_import_desc",
+      trKeyDescExport: "auto_like_export_desc",
+      dataHidden: false,
+    });
+  }
+
   return autoLikeDialog;
 }
 
@@ -191,8 +232,8 @@ function renderFooter() {
   return wrapperEl;
 }
 
-function openImportExportAutoLikeChannelsDialog() {
-  alert("TODO: ImportExportDialog stuff");
+async function openImportExportAutoLikeChannelsDialog() {
+  await autoLikeImExDialog?.open();
 }
 
 //#region add prompt

+ 5 - 20
src/dialogs/importCfg.ts

@@ -1,10 +1,10 @@
-import { decompress } from "@sv443-network/userutils";
-import { error, t, warn } from "../utils/index.js";
+import { error, tryToDecompressAndParse, t, warn } from "../utils/index.js";
 import { BytmDialog } from "../components/index.js";
-import { compressionFormat, scriptInfo } from "../constants.js";
+import { scriptInfo } from "../constants.js";
 import { emitSiteEvent } from "../siteEvents.js";
 import { formatVersion, getFeatures, migrations, setFeatures } from "../config.js";
 import { disableBeforeUnload } from "../features/index.js";
+import { FeatureConfig } from "src/types.js";
 
 let importDialog: BytmDialog | null = null;
 
@@ -68,23 +68,8 @@ async function renderFooter() {
     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")
+      const parsed = await tryToDecompressAndParse<{ data: FeatureConfig, formatVersion: number }>(textAreaElem.value.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"));

+ 1 - 1
src/features/input.ts

@@ -150,7 +150,7 @@ export async function initNumKeysSkip() {
 
 let canCompress = false;
 
-type AutoLikeData = {
+export type AutoLikeData = {
   channels: {
     /** 24-character channel ID or user ID including the @ prefix */
     id: string;

+ 17 - 1
src/utils/misc.ts

@@ -1,4 +1,4 @@
-import { compress, fetchAdvanced, openInNewTab, randomId } from "@sv443-network/userutils";
+import { compress, decompress, fetchAdvanced, openInNewTab, randomId } from "@sv443-network/userutils";
 import { marked } from "marked";
 import { branch, compressionFormat, repo } from "../constants.js";
 import { type Domain, type ResourceKey } from "../types.js";
@@ -139,6 +139,22 @@ export function openInTab(href: string, background = false) {
   }
 }
 
+/** Tries to parse an uncompressed or compressed input string as a JSON object */
+export async function tryToDecompressAndParse<TData = Record<string, unknown>>(input: string): Promise<TData | null> {
+  try {
+    return JSON.parse(input);
+  }
+  catch {
+    try {
+      return JSON.parse(await decompress(input, compressionFormat, "string"));
+    }
+    catch(err) {
+      error("Couldn't decompress and parse data due to an error:", err);
+      return null;
+    }
+  }
+}
+
 //#region resources
 
 /**