From 9687bc82195519707044ea0959263542eaae13a4 Mon Sep 17 00:00:00 2001 From: Satori-Bot Date: Wed, 11 Jun 2025 15:51:05 -0700 Subject: [PATCH 1/2] feat: add long screenshot support --- packages/electron-screenshots/src/index.ts | 19 ++++ packages/electron-screenshots/src/preload.ts | 9 ++ .../src/Screenshots/operations/Long/index.tsx | 100 ++++++++++++++++++ .../src/Screenshots/operations/index.ts | 3 +- .../src/Screenshots/zh_CN.ts | 4 +- 5 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 packages/react-screenshots/src/Screenshots/operations/Long/index.tsx diff --git a/packages/electron-screenshots/src/index.ts b/packages/electron-screenshots/src/index.ts index 2d30de93..807c440c 100644 --- a/packages/electron-screenshots/src/index.ts +++ b/packages/electron-screenshots/src/index.ts @@ -31,6 +31,7 @@ export interface Lang { operation_arrow_title?: string; operation_ellipse_title?: string; operation_rectangle_title?: string; + operation_long_title?: string; } export interface ScreenshotsOpts { @@ -57,6 +58,8 @@ export default class Screenshots extends Events { private singleWindow: boolean; + private display: Display | null = null; + private isReady = new Promise((resolve) => { ipcMain.once('SCREENSHOTS:ready', () => { this.logger('SCREENSHOTS:ready'); @@ -85,6 +88,7 @@ export default class Screenshots extends Events { this.logger('startCapture'); const display = getDisplay(); + this.display = display; const [imageUrl] = await Promise.all([this.capture(display), this.isReady]); @@ -396,5 +400,20 @@ export default class Screenshots extends Events { this.endCapture(); }, ); + + ipcMain.handle('SCREENSHOTS:startLong', () => { + this.$win?.setIgnoreMouseEvents(true, { forward: true }); + }); + + ipcMain.handle('SCREENSHOTS:stopLong', () => { + this.$win?.setIgnoreMouseEvents(false); + }); + + ipcMain.handle('SCREENSHOTS:captureLong', async () => { + if (!this.display) { + return null; + } + return this.capture(this.display); + }); } } diff --git a/packages/electron-screenshots/src/preload.ts b/packages/electron-screenshots/src/preload.ts index 1407d529..afc44717 100644 --- a/packages/electron-screenshots/src/preload.ts +++ b/packages/electron-screenshots/src/preload.ts @@ -43,6 +43,15 @@ contextBridge.exposeInMainWorld('screenshots', { ipcRenderer.send('SCREENSHOTS:cancel'); }, + startLong: () => { + ipcRenderer.invoke('SCREENSHOTS:startLong'); + }, + stopLong: () => { + ipcRenderer.invoke('SCREENSHOTS:stopLong'); + }, + captureLong: () => { + return ipcRenderer.invoke('SCREENSHOTS:captureLong'); + }, ok: (arrayBuffer: ArrayBuffer, data: ScreenshotsData) => { console.log('contextBridge ok', arrayBuffer, data); diff --git a/packages/react-screenshots/src/Screenshots/operations/Long/index.tsx b/packages/react-screenshots/src/Screenshots/operations/Long/index.tsx new file mode 100644 index 00000000..03b3f0b4 --- /dev/null +++ b/packages/react-screenshots/src/Screenshots/operations/Long/index.tsx @@ -0,0 +1,100 @@ +import React, { ReactElement, useCallback, useEffect, useState } from 'react' +import ScreenshotsButton from '../../ScreenshotsButton' +import useStore from '../../hooks/useStore' +import useCall from '../../hooks/useCall' +import useReset from '../../hooks/useReset' + +export default function Long (): ReactElement { + const { bounds, width, height, lang } = useStore() + const call = useCall() + const reset = useReset() + const [capturing, setCapturing] = useState(false) + const [segments, setSegments] = useState([]) + + const capture = useCallback(async () => { + const url = (await window.screenshots.captureLong()) as string | null + if (url) { + setSegments((arr) => [...arr, url]) + } + }, []) + + useEffect(() => { + if (!capturing) return + const id = window.setInterval(capture, 500) + return () => window.clearInterval(id) + }, [capturing, capture]) + + const start = useCallback(async () => { + setSegments([]) + await window.screenshots.startLong() + await capture() + setCapturing(true) + }, [capture]) + + const stop = useCallback(async () => { + await capture() + await window.screenshots.stopLong() + if (!bounds) { + setCapturing(false) + setSegments([]) + return + } + const images = await Promise.all( + segments.map( + (u) => + new Promise((resolve, reject) => { + const img = new Image() + img.onload = () => resolve(img) + img.onerror = reject + img.src = u + }) + ) + ) + const canvas = document.createElement('canvas') + canvas.width = bounds.width + canvas.height = bounds.height * images.length + const ctx = canvas.getContext('2d') + if (ctx) { + images.forEach((img, i) => { + ctx.drawImage( + img, + bounds.x, + bounds.y, + bounds.width, + bounds.height, + 0, + i * bounds.height, + bounds.width, + bounds.height + ) + }) + } + const blob: Blob | null = await new Promise((res) => + canvas.toBlob((b) => res(b), 'image/png') + ) + if (blob) { + const totalBounds = { ...bounds, height: bounds.height * images.length } + call('onOk', blob, totalBounds) + reset() + } + setCapturing(false) + setSegments([]) + }, [bounds, capture, call, reset, segments]) + + const onClick = useCallback(() => { + if (capturing) { + stop() + } else { + start() + } + }, [capturing, start, stop]) + + return ( + + ) +} diff --git a/packages/react-screenshots/src/Screenshots/operations/index.ts b/packages/react-screenshots/src/Screenshots/operations/index.ts index d478c59b..07b1a132 100644 --- a/packages/react-screenshots/src/Screenshots/operations/index.ts +++ b/packages/react-screenshots/src/Screenshots/operations/index.ts @@ -9,5 +9,6 @@ import Brush from './Brush' import Arrow from './Arrow' import Ellipse from './Ellipse' import Rectangle from './Rectangle' +import Long from './Long' -export default [Rectangle, Ellipse, Arrow, Brush, Text, Mosaic, '|', Undo, Redo, '|', Save, Cancel, Ok] +export default [Rectangle, Ellipse, Arrow, Brush, Text, Mosaic, '|', Undo, Redo, '|', Long, Save, Cancel, Ok] diff --git a/packages/react-screenshots/src/Screenshots/zh_CN.ts b/packages/react-screenshots/src/Screenshots/zh_CN.ts index 8488f1bc..c7c163c8 100644 --- a/packages/react-screenshots/src/Screenshots/zh_CN.ts +++ b/packages/react-screenshots/src/Screenshots/zh_CN.ts @@ -11,6 +11,7 @@ export interface Lang { operation_arrow_title: string operation_ellipse_title: string operation_rectangle_title: string + operation_long_title: string } const zhCN: Lang = { @@ -25,7 +26,8 @@ const zhCN: Lang = { operation_brush_title: '画笔', operation_arrow_title: '箭头', operation_ellipse_title: '椭圆', - operation_rectangle_title: '矩形' + operation_rectangle_title: '矩形', + operation_long_title: '长截图' } export default zhCN From 218502385ab87266c3121c83b7e93909c60870db Mon Sep 17 00:00:00 2001 From: Satori-Bot Date: Wed, 11 Jun 2025 17:36:31 -0700 Subject: [PATCH 2/2] chore: cleanup long screenshot button --- .../react-screenshots/src/Screenshots/operations/Long/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-screenshots/src/Screenshots/operations/Long/index.tsx b/packages/react-screenshots/src/Screenshots/operations/Long/index.tsx index 03b3f0b4..36567893 100644 --- a/packages/react-screenshots/src/Screenshots/operations/Long/index.tsx +++ b/packages/react-screenshots/src/Screenshots/operations/Long/index.tsx @@ -5,7 +5,7 @@ import useCall from '../../hooks/useCall' import useReset from '../../hooks/useReset' export default function Long (): ReactElement { - const { bounds, width, height, lang } = useStore() + const { bounds, lang } = useStore() const call = useCall() const reset = useReset() const [capturing, setCapturing] = useState(false)