feat: download image in electron
Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
d007d90392
commit
041bd526b9
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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<number>().action(async ({ input }) => {
|
||||
app.setBadgeCount(input)
|
||||
}),
|
||||
|
||||
saveImage: t.procedure.input<string>().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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}, [])
|
||||
|
|
|
|||
|
|
@ -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<HTMLImageElement>) => {
|
||||
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 (
|
||||
<Wrapper src={src}>
|
||||
<img className="size-full object-contain" alt="cover" src={src} />
|
||||
<img
|
||||
className="size-full object-contain"
|
||||
alt="cover"
|
||||
src={src}
|
||||
onContextMenu={(e) => handleContextMenu(src, e)}
|
||||
/>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
|
@ -81,6 +120,7 @@ export const PreviewImageContent: FC<{
|
|||
{images.map((image, index) => (
|
||||
<SwiperSlide key={image} virtualIndex={index}>
|
||||
<img
|
||||
onContextMenu={(e) => handleContextMenu(image, e)}
|
||||
className="size-full object-contain"
|
||||
alt="cover"
|
||||
src={image}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export const EntryContent = ({ entryId }: { entryId: ActiveEntryId }) => {
|
|||
if (!entryId) {
|
||||
return (
|
||||
<m.div
|
||||
onContextMenu={stopPropagation}
|
||||
className="-mt-2 flex size-full min-w-0 flex-col items-center justify-center gap-1 text-lg font-medium text-zinc-400"
|
||||
initial={{ opacity: 0.01, y: 300 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,39 @@
|
|||
import type { BrowserWindow } from "electron"
|
||||
import type { toast } from "sonner"
|
||||
|
||||
const PREFIX = "__follow"
|
||||
interface RenderGlobalContext {
|
||||
showSetting: () => void
|
||||
|
||||
toast: typeof toast
|
||||
}
|
||||
|
||||
export const registerGlobalContext = (context: RenderGlobalContext) => {
|
||||
globalThis[PREFIX] = context
|
||||
}
|
||||
export const callGlobalContextMethod = (
|
||||
|
||||
export function callGlobalContextMethod(
|
||||
window: BrowserWindow,
|
||||
method: keyof RenderGlobalContext,
|
||||
) => window.webContents.executeJavaScript(`globalThis.${PREFIX}.${method}()`)
|
||||
method: string,
|
||||
args?: any[]
|
||||
): void
|
||||
|
||||
export function callGlobalContextMethod<T extends keyof RenderGlobalContext>(
|
||||
window: BrowserWindow,
|
||||
method: T,
|
||||
|
||||
// @ts-expect-error
|
||||
args: Parameters<RenderGlobalContext[T]> = [] as any
|
||||
): void
|
||||
export function callGlobalContextMethod<T extends keyof RenderGlobalContext>(
|
||||
window: BrowserWindow,
|
||||
method: T,
|
||||
|
||||
args: Parameters<RenderGlobalContext[T]> = [] as any,
|
||||
) {
|
||||
return window.webContents.executeJavaScript(
|
||||
`globalThis.${PREFIX}.${method}(${args
|
||||
.map((arg) => JSON.stringify(arg))
|
||||
.join(",")})`,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue