Browse Source

Client App Fix Issue [Bug] 'export' button does not work #2884

[+] fix(exporter.tsx): add async keyword to download function
[+] feat(exporter.tsx): add support for saving image file using window.__TAURI__ API
[+] feat(global.d.ts): add types for window.__TAURI__ API methods
[+] feat(locales): add translations for download success and failure messages
[+] feat(sync.ts): add support for generating backup file name with date and time
[+] fix(utils.ts): add async keyword to downloadAs function and add support for saving file using window.__TAURI__ API
H0llyW00dzZ 1 year ago
parent
commit
d2ad01a9ff
7 changed files with 106 additions and 22 deletions
  1. 44 14
      app/components/exporter.tsx
  2. 7 0
      app/global.d.ts
  3. 4 0
      app/locales/cn.ts
  4. 4 0
      app/locales/en.ts
  5. 4 0
      app/locales/id.ts
  6. 7 1
      app/store/sync.ts
  7. 36 7
      app/utils.ts

+ 44 - 14
app/components/exporter.tsx

@@ -433,25 +433,55 @@ export function ImagePreviewer(props: {
 
   const isMobile = useMobileScreen();
 
-  const download = () => {
+  const download = async () => {
     showToast(Locale.Export.Image.Toast);
     const dom = previewRef.current;
     if (!dom) return;
-    toPng(dom)
-      .then((blob) => {
-        if (!blob) return;
-
-        if (isMobile || getClientConfig()?.isApp) {
-          showImageModal(blob);
+  
+    const isApp = getClientConfig()?.isApp;
+  
+    try {
+      const blob = await toPng(dom);
+      if (!blob) return;
+  
+      if (isMobile || (isApp && window.__TAURI__)) {
+        if (isApp && window.__TAURI__) {
+          const result = await window.__TAURI__.dialog.save({
+            defaultPath: `${props.topic}.png`,
+            filters: [
+              {
+                name: "PNG Files",
+                extensions: ["png"],
+              },
+              {
+                name: "All Files",
+                extensions: ["*"],
+              },
+            ],
+          });
+  
+          if (result !== null) {
+            const response = await fetch(blob);
+            const buffer = await response.arrayBuffer();
+            const uint8Array = new Uint8Array(buffer);
+            await window.__TAURI__.fs.writeBinaryFile(result, uint8Array);
+            showToast(Locale.Download.Success);
+          } else {
+            showToast(Locale.Download.Failed);
+          }
         } else {
-          const link = document.createElement("a");
-          link.download = `${props.topic}.png`;
-          link.href = blob;
-          link.click();
-          refreshPreview();
+          showImageModal(blob);
         }
-      })
-      .catch((e) => console.log("[Export Image] ", e));
+      } else {
+        const link = document.createElement("a");
+        link.download = `${props.topic}.png`;
+        link.href = blob;
+        link.click();
+        refreshPreview();
+      }
+    } catch (error) {
+      showToast(Locale.Download.Failed);
+    }
   };
 
   const refreshPreview = () => {

+ 7 - 0
app/global.d.ts

@@ -13,6 +13,13 @@ declare module "*.svg";
 declare interface Window {
   __TAURI__?: {
     writeText(text: string): Promise<void>;
+    invoke(command: string, payload?: Record<string, unknown>): Promise<any>;
+    dialog: {
+      save(options?: Record<string, unknown>): Promise<string | null>;
+    };
+    fs: {
+      writeBinaryFile(path: string, data: Uint8Array): Promise<void>;
+    };
     notification:{
       requestPermission(): Promise<Permission>;
       isPermissionGranted(): Promise<boolean>;

+ 4 - 0
app/locales/cn.ts

@@ -323,6 +323,10 @@ const cn = {
     Success: "已写入剪切板",
     Failed: "复制失败,请赋予剪切板权限",
   },
+  Download: {
+    Success: "内容已下载到您的目录。",
+    Failed: "下载失败。",
+  },
   Context: {
     Toast: (x: any) => `包含 ${x} 条预设提示词`,
     Edit: "当前对话设置",

+ 4 - 0
app/locales/en.ts

@@ -329,6 +329,10 @@ const en: LocaleType = {
     Success: "Copied to clipboard",
     Failed: "Copy failed, please grant permission to access clipboard",
   },
+  Download: {
+    Success: "Content downloaded to your directory.",
+    Failed: "Download failed.",
+  },
   Context: {
     Toast: (x: any) => `With ${x} contextual prompts`,
     Edit: "Current Chat Settings",

+ 4 - 0
app/locales/id.ts

@@ -301,6 +301,10 @@ const id: PartialLocaleType = {
     Failed:
       "Gagal menyalin, mohon berikan izin untuk mengakses clipboard atau Clipboard API tidak didukung (Tauri)",
   },
+  Download: {
+    Success: "Konten berhasil diunduh ke direktori Anda.",
+    Failed: "Unduhan gagal.",
+  },
   Context: {
     Toast: (x: any) => `Dengan ${x} promp kontekstual`,
     Edit: "Pengaturan Obrolan Saat Ini",

+ 7 - 1
app/store/sync.ts

@@ -1,3 +1,4 @@
+import { getClientConfig } from "../config/client";
 import { Updater } from "../typing";
 import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
 import { createPersistStore } from "../utils/store";
@@ -20,6 +21,7 @@ export interface WebDavConfig {
   password: string;
 }
 
+const isApp = !!getClientConfig()?.isApp;
 export type SyncStore = GetStoreState<typeof useSyncStore>;
 
 const DEFAULT_SYNC_STATE = {
@@ -57,7 +59,11 @@ export const useSyncStore = createPersistStore(
 
     export() {
       const state = getLocalAppState();
-      const fileName = `Backup-${new Date().toLocaleString()}.json`;
+      const datePart = isApp
+      ? `${new Date().toLocaleDateString().replace(/\//g, '_')} ${new Date().toLocaleTimeString().replace(/:/g, '_')}`
+      : new Date().toLocaleString();
+
+      const fileName = `Backup-${datePart}.json`;
       downloadAs(JSON.stringify(state), fileName);
     },
 

+ 36 - 7
app/utils.ts

@@ -31,12 +31,41 @@ export async function copyToClipboard(text: string) {
   }
 }
 
-export function downloadAs(text: string, filename: string) {
-  const element = document.createElement("a");
-  element.setAttribute(
-    "href",
-    "data:text/plain;charset=utf-8," + encodeURIComponent(text),
-  );
+export async function downloadAs(text: string, filename: string) {
+  if (window.__TAURI__) {
+    const result = await window.__TAURI__.dialog.save({
+      defaultPath: `${filename}`,
+      filters: [
+        {
+          name: `${filename.split('.').pop()} files`,
+          extensions: [`${filename.split('.').pop()}`],
+        },
+        {
+          name: "All Files",
+          extensions: ["*"],
+        },
+      ],
+    });
+
+    if (result !== null) {
+      try {
+        await window.__TAURI__.fs.writeBinaryFile(
+          result,
+          new Uint8Array([...text].map((c) => c.charCodeAt(0)))
+        );
+        showToast(Locale.Download.Success);
+      } catch (error) {
+        showToast(Locale.Download.Failed);
+      }
+    } else {
+      showToast(Locale.Download.Failed);
+    }
+  } else {
+    const element = document.createElement("a");
+    element.setAttribute(
+      "href",
+      "data:text/plain;charset=utf-8," + encodeURIComponent(text),
+    );
   element.setAttribute("download", filename);
 
   element.style.display = "none";
@@ -46,7 +75,7 @@ export function downloadAs(text: string, filename: string) {
 
   document.body.removeChild(element);
 }
-
+}
 export function readFromFile() {
   return new Promise<string>((res, rej) => {
     const fileInput = document.createElement("input");