Skip to content

Commit

Permalink
feat: download image in electron
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <i@innei.in>
  • Loading branch information
Innei committed Jul 4, 2024
1 parent d007d90 commit 041bd52
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 8 deletions.
18 changes: 18 additions & 0 deletions src/main/lib/download.ts
Original file line number Diff line number Diff line change
@@ -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))

Check warning on line 17 in src/main/lib/download.ts

View workflow job for this annotation

GitHub Actions / Lint and Typecheck (18.x)

Unexpected any. Specify a different type
}
22 changes: 22 additions & 0 deletions src/main/tipc.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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 changes: 2 additions & 0 deletions src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -20,6 +21,7 @@ function App() {

registerGlobalContext({
showSetting: window.router.showSettings,
toast,
})
return cleanup
}, [])
Expand Down
50 changes: 45 additions & 5 deletions src/renderer/src/components/ui/image/preview-image.tsx
Original file line number Diff line number Diff line change
@@ -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<{
Expand Down Expand Up @@ -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>
)
}
Expand All @@ -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}
Expand Down
1 change: 1 addition & 0 deletions src/renderer/src/modules/entry-content/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
31 changes: 28 additions & 3 deletions src/shared/src/bridge.ts
Original file line number Diff line number Diff line change
@@ -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: string,
args?: any[]
): void

export function callGlobalContextMethod<T extends keyof RenderGlobalContext>(
window: BrowserWindow,
method: keyof RenderGlobalContext,
) => window.webContents.executeJavaScript(`globalThis.${PREFIX}.${method}()`)
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(",")})`,
)
}

0 comments on commit 041bd52

Please sign in to comment.