Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions packages/electron-screenshots/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
dialog,
ipcMain,
nativeImage,
globalShortcut,
} from 'electron';
import Events from 'events';
import fs from 'fs-extra';
Expand All @@ -31,6 +32,7 @@ export interface Lang {
operation_arrow_title?: string;
operation_ellipse_title?: string;
operation_rectangle_title?: string;
operation_scroll_title?: string;
}

export interface ScreenshotsOpts {
Expand All @@ -57,6 +59,10 @@ export default class Screenshots extends Events {

private singleWindow: boolean;

private scrollDisplay: Display | null = null;

private isScrollCapturing = false;

private isReady = new Promise<void>((resolve) => {
ipcMain.once('SCREENSHOTS:ready', () => {
this.logger('SCREENSHOTS:ready');
Expand Down Expand Up @@ -84,6 +90,8 @@ export default class Screenshots extends Events {
public async startCapture(): Promise<void> {
this.logger('startCapture');

this.stopScrollCapture(false, false);

const display = getDisplay();

const [imageUrl] = await Promise.all([this.capture(display), this.isReady]);
Expand All @@ -98,6 +106,7 @@ export default class Screenshots extends Events {
*/
public async endCapture(): Promise<void> {
this.logger('endCapture');
this.stopScrollCapture(false, false);
await this.reset();

if (!this.$win) {
Expand Down Expand Up @@ -206,6 +215,7 @@ export default class Screenshots extends Events {
});

this.$win.on('closed', () => {
this.stopScrollCapture(false, false);
this.emit('windowClosed', this.$win);
this.$win = null;
});
Expand Down Expand Up @@ -396,5 +406,72 @@ export default class Screenshots extends Events {
this.endCapture();
},
);

ipcMain.on('SCREENSHOTS:scroll-start', (e, display: Display) => {
this.logger('SCREENSHOTS:scroll-start');

this.startScrollCapture(display);
});
}

private startScrollCapture(display: Display): void {
this.stopScrollCapture(false, false);
this.scrollDisplay = display;
this.isScrollCapturing = true;

this.$win?.hide();
this.$win?.setKiosk(false);
this.$win?.setAlwaysOnTop(false);

this.registerScrollCaptureShortcuts();
}

private registerScrollCaptureShortcuts(): void {
globalShortcut.unregister('Control+Alt+S');
globalShortcut.unregister('Control+Alt+X');

globalShortcut.register('Control+Alt+S', async () => {
this.logger('SCREENSHOTS:scroll-shortcut-capture');

if (!this.isScrollCapturing || !this.scrollDisplay) {
return;
}

try {
const imageUrl = await this.capture(this.scrollDisplay);
this.$view.webContents.send('SCREENSHOTS:scrollCapture', imageUrl);
} catch (err) {
this.logger('SCREENSHOTS:scroll-capture error %o', err);
}
});

globalShortcut.register('Control+Alt+X', () => {
this.logger('SCREENSHOTS:scroll-shortcut-end');

this.stopScrollCapture(true, true);
});
}

private stopScrollCapture(sendEvent = false, showWindow = false): void {
if (!this.isScrollCapturing) {
return;
}

globalShortcut.unregister('Control+Alt+S');
globalShortcut.unregister('Control+Alt+X');

this.isScrollCapturing = false;
this.scrollDisplay = null;

if (showWindow && this.$win) {
this.$win.show();
this.$win.focus();
this.$win.setKiosk(true);
this.$win.setAlwaysOnTop(true);
}

if (sendEvent) {
this.$view.webContents.send('SCREENSHOTS:scrollEnd');
}
}
}
5 changes: 5 additions & 0 deletions packages/electron-screenshots/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ contextBridge.exposeInMainWorld('screenshots', {

ipcRenderer.send('SCREENSHOTS:ok', Buffer.from(arrayBuffer), data);
},
startScroll: (display: Display) => {
console.log('contextBridge start scroll');

ipcRenderer.send('SCREENSHOTS:scroll-start', display);
},
on: (channel: string, fn: ScreenshotsListener) => {
console.log('contextBridge on', fn);

Expand Down
1 change: 1 addition & 0 deletions packages/react-screenshots/src/Screenshots/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface ScreenshotsProps {
height: number
lang?: Partial<Lang>
className?: string
onScrollCapture?: (bounds: Bounds, payload: { url: string, width: number, height: number }) => void
[key: string]: unknown
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { ReactElement, useCallback } from 'react'
import useStore from '../../hooks/useStore'
import useCall from '../../hooks/useCall'
import ScreenshotsButton from '../../ScreenshotsButton'

export default function Scroll (): ReactElement {
const { url, bounds, width, height, lang } = useStore()
const call = useCall()

const onClick = useCallback(() => {
if (!url || !bounds) {
return
}
call('onScrollCapture', bounds, { url, width, height })
}, [bounds, call, height, url, width])

return (
<ScreenshotsButton
title={lang.operation_scroll_title}
icon='icon-arrow'
disabled={!bounds}
onClick={onClick}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ import Brush from './Brush'
import Arrow from './Arrow'
import Ellipse from './Ellipse'
import Rectangle from './Rectangle'
import Scroll from './Scroll'

export default [Rectangle, Ellipse, Arrow, Brush, Text, Mosaic, '|', Undo, Redo, '|', Save, Cancel, Ok]
export default [Rectangle, Ellipse, Arrow, Brush, Text, Mosaic, '|', Undo, Redo, '|', Scroll, Save, Cancel, Ok]
4 changes: 3 additions & 1 deletion packages/react-screenshots/src/Screenshots/zh_CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface Lang {
operation_arrow_title: string
operation_ellipse_title: string
operation_rectangle_title: string
operation_scroll_title: string
}

const zhCN: Lang = {
Expand All @@ -25,7 +26,8 @@ const zhCN: Lang = {
operation_brush_title: '画笔',
operation_arrow_title: '箭头',
operation_ellipse_title: '椭圆',
operation_rectangle_title: '矩形'
operation_rectangle_title: '矩形',
operation_scroll_title: '长截图'
}

export default zhCN
85 changes: 84 additions & 1 deletion packages/react-screenshots/src/electron/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'
import Screenshots from '../Screenshots'
import { Bounds } from '../Screenshots/types'
import { Lang } from '../Screenshots/zh_CN'
import { cropImage, stitchImages } from './scrollCapture'
import './app.less'

export interface Display {
Expand All @@ -18,6 +19,9 @@ export default function App (): JSX.Element {
const [height, setHeight] = useState(window.innerHeight)
const [display, setDisplay] = useState<Display | undefined>(undefined)
const [lang, setLang] = useState<Lang | undefined>(undefined)
const [scrolling, setScrolling] = useState(false)
const scrollBoundsRef = React.useRef<Bounds | null>(null)
const scrollImagesRef = React.useRef<string[]>([])

const onSave = useCallback(
async (blob: Blob | null, bounds: Bounds) => {
Expand All @@ -43,6 +47,39 @@ export default function App (): JSX.Element {
[display]
)

const onScrollCapture = useCallback(
async (bounds: Bounds, payload?: { url: string, width: number, height: number }) => {
if (!display || !url || scrolling) {
return
}

const shouldStart = window.confirm(
`${lang?.operation_scroll_title ?? '滚动截图'}: ` +
'使用 Ctrl+Alt+S 追加截图,Ctrl+Alt+X 结束拼接'
)

if (!shouldStart) {
return
}

try {
const firstImage = await cropImage(
payload?.url ?? url,
bounds,
payload?.width ?? width,
payload?.height ?? height
)
scrollBoundsRef.current = bounds
scrollImagesRef.current = [firstImage]
setScrolling(true)
window.screenshots.startScroll(display)
} catch (err) {
console.error('start scroll capture fail', err)
}
},
[display, height, lang?.operation_scroll_title, scrolling, url, width]
)

useEffect(() => {
const onSetLang = (lang: Lang) => {
setLang(lang)
Expand All @@ -51,26 +88,71 @@ export default function App (): JSX.Element {
const onCapture = (display: Display, dataURL: string) => {
setDisplay(display)
setUrl(dataURL)
setScrolling(false)
scrollBoundsRef.current = null
scrollImagesRef.current = []
}

const onReset = () => {
setUrl(undefined)
setDisplay(undefined)
setScrolling(false)
scrollBoundsRef.current = null
scrollImagesRef.current = []
// 确保截图区域被重置
requestAnimationFrame(() => window.screenshots.reset())
}

const onScrollCaptureData = async (dataURL: string) => {
if (!scrollBoundsRef.current) {
return
}
try {
const nextImage = await cropImage(
dataURL,
scrollBoundsRef.current,
width,
height
)
scrollImagesRef.current.push(nextImage)
} catch (err) {
console.error('append scroll capture fail', err)
}
}

const onScrollEnd = async () => {
setScrolling(false)
if (!scrollImagesRef.current.length) {
scrollBoundsRef.current = null
return
}
try {
const stitched = await stitchImages(scrollImagesRef.current)
scrollImagesRef.current = []
scrollBoundsRef.current = null
setUrl(stitched)
} catch (err) {
scrollImagesRef.current = []
scrollBoundsRef.current = null
console.error('stitch scroll capture fail', err)
}
}

window.screenshots.on('setLang', onSetLang)
window.screenshots.on('capture', onCapture)
window.screenshots.on('reset', onReset)
window.screenshots.on('scrollCapture', onScrollCaptureData)
window.screenshots.on('scrollEnd', onScrollEnd)
// 告诉主进程页面准备完成
window.screenshots.ready()
return () => {
window.screenshots.off('capture', onCapture)
window.screenshots.off('setLang', onSetLang)
window.screenshots.off('reset', onReset)
window.screenshots.off('scrollCapture', onScrollCaptureData)
window.screenshots.off('scrollEnd', onScrollEnd)
}
}, [])
}, [height, width])

useEffect(() => {
const onResize = () => {
Expand All @@ -94,6 +176,7 @@ export default function App (): JSX.Element {
onSave={onSave}
onCancel={onCancel}
onOk={onOk}
onScrollCapture={onScrollCapture}
/>
</div>
)
Expand Down
Loading