From 041bd526b919d99505d24764be741d82fba12080 Mon Sep 17 00:00:00 2001 From: Innei Date: Thu, 4 Jul 2024 12:29:43 +0800 Subject: [PATCH] feat: download image in electron Signed-off-by: Innei --- src/main/lib/download.ts | 18 +++++++ src/main/tipc.ts | 22 ++++++++ src/renderer/src/App.tsx | 2 + .../src/components/ui/image/preview-image.tsx | 50 +++++++++++++++++-- .../src/modules/entry-content/index.tsx | 1 + src/shared/src/bridge.ts | 31 ++++++++++-- 6 files changed, 116 insertions(+), 8 deletions(-) create mode 100644 src/main/lib/download.ts diff --git a/src/main/lib/download.ts b/src/main/lib/download.ts new file mode 100644 index 0000000000..a30d6aeffd --- /dev/null +++ b/src/main/lib/download.ts @@ -0,0 +1,18 @@ +import { createWriteStream } from "node:fs" +import { pipeline } from "node:stream" +import { promisify } from "node:util" + +const streamPipeline = promisify(pipeline) + +export async function downloadFile(url: string, dest: string) { + const res = await fetch(url) + + // 检查响应是否成功 + if (!res.ok) { + throw new Error(`Failed to fetch ${url}: ${res.statusText}`) + } + if (!res.body) { + throw new Error(`Failed to get response body`) + } + await streamPipeline(res.body as any, createWriteStream(dest)) +} diff --git a/src/main/tipc.ts b/src/main/tipc.ts index 807655f6e4..f6e1c0a605 100644 --- a/src/main/tipc.ts +++ b/src/main/tipc.ts @@ -1,7 +1,9 @@ import { getRendererHandlers, tipc } from "@egoist/tipc/main" +import { callGlobalContextMethod } from "@shared/bridge" import type { MessageBoxOptions } from "electron" import { app, dialog, Menu, ShareMenu } from "electron" +import { downloadFile } from "./lib/download" import type { RendererHandlers } from "./renderer-handlers" import { createSettingWindow, createWindow, getMainWindow } from "./window" @@ -171,6 +173,26 @@ export const router = { setMacOSBadge: t.procedure.input().action(async ({ input }) => { app.setBadgeCount(input) }), + + saveImage: t.procedure.input().action(async ({ input }) => { + const result = await dialog.showSaveDialog({ + defaultPath: input.split("/").pop(), + }) + if (result.canceled) return + + // return result.filePath; + await downloadFile(input, result.filePath) + .catch((err) => { + const mainWindow = getMainWindow() + if (!mainWindow) return + callGlobalContextMethod(mainWindow, "toast.error", ["Download failed!"]) + throw err + }) + + const mainWindow = getMainWindow() + if (!mainWindow) return + callGlobalContextMethod(mainWindow, "toast.success", ["Download success!"]) + }), } export type Router = typeof router diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 91c397ed72..e24ad90f5f 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -2,6 +2,7 @@ import { queryClient } from "@renderer/lib/query-client" import { registerGlobalContext } from "@shared/bridge" import { useEffect } from "react" import { Outlet } from "react-router-dom" +import { toast } from "sonner" import { useAppIsReady, useUISettingKey } from "./atoms" import { useDark } from "./hooks/common/useDark" @@ -20,6 +21,7 @@ function App() { registerGlobalContext({ showSetting: window.router.showSettings, + toast, }) return cleanup }, []) diff --git a/src/renderer/src/components/ui/image/preview-image.tsx b/src/renderer/src/components/ui/image/preview-image.tsx index 7aceb6f552..8c410d3df4 100644 --- a/src/renderer/src/components/ui/image/preview-image.tsx +++ b/src/renderer/src/components/ui/image/preview-image.tsx @@ -1,14 +1,14 @@ import { m } from "@renderer/components/common/Motion" +import { tipcClient } from "@renderer/lib/client" import { stopPropagation } from "@renderer/lib/dom" +import { showNativeMenu } from "@renderer/lib/native-menu" import type { FC } from "react" -import { useState } from "react" +import { useCallback, useState } from "react" import { Mousewheel, Scrollbar, Virtual } from "swiper/modules" import { Swiper, SwiperSlide } from "swiper/react" import { ActionButton } from "../button" -import { - microReboundPreset, -} from "../constants/spring" +import { microReboundPreset } from "../constants/spring" import { useCurrentModal } from "../modal" const Wrapper: Component<{ @@ -52,12 +52,51 @@ export const PreviewImageContent: FC<{ }> = ({ images, initialIndex = 0 }) => { const [currentSrc, setCurrentSrc] = useState(images[initialIndex]) + const handleContextMenu = useCallback( + (image: string, e: React.MouseEvent) => { + if (!window.electron) return + + showNativeMenu( + [ + { + label: "Open in browser", + type: "text", + click: () => { + window.open(image) + }, + }, + { + label: "Copy image address", + type: "text", + click: () => { + navigator.clipboard.writeText(image) + }, + }, + { + label: "Save image as...", + type: "text", + click: () => { + // window.electron.ipcRenderer.invoke("save-image", image); + tipcClient?.saveImage(image) + }, + }, + ], + e, + ) + }, + [], + ) if (images.length === 0) return null if (images.length === 1) { const src = images[0] return ( - cover + cover handleContextMenu(src, e)} + /> ) } @@ -81,6 +120,7 @@ export const PreviewImageContent: FC<{ {images.map((image, index) => ( handleContextMenu(image, e)} className="size-full object-contain" alt="cover" src={image} diff --git a/src/renderer/src/modules/entry-content/index.tsx b/src/renderer/src/modules/entry-content/index.tsx index 6ae90fe18e..3ffbca8dab 100644 --- a/src/renderer/src/modules/entry-content/index.tsx +++ b/src/renderer/src/modules/entry-content/index.tsx @@ -24,6 +24,7 @@ export const EntryContent = ({ entryId }: { entryId: ActiveEntryId }) => { if (!entryId) { return ( void + + toast: typeof toast } export const registerGlobalContext = (context: RenderGlobalContext) => { globalThis[PREFIX] = context } -export const callGlobalContextMethod = ( + +export function callGlobalContextMethod( + window: BrowserWindow, + method: string, + args?: any[] +): void + +export function callGlobalContextMethod( window: BrowserWindow, - method: keyof RenderGlobalContext, -) => window.webContents.executeJavaScript(`globalThis.${PREFIX}.${method}()`) + method: T, + + // @ts-expect-error + args: Parameters = [] as any +): void +export function callGlobalContextMethod( + window: BrowserWindow, + method: T, + + args: Parameters = [] as any, +) { + return window.webContents.executeJavaScript( + `globalThis.${PREFIX}.${method}(${args + .map((arg) => JSON.stringify(arg)) + .join(",")})`, + ) +}