Skip to content

Commit 7aa7c0f

Browse files
committed
feat: support export png (screenshot)
1 parent fdc0109 commit 7aa7c0f

File tree

5 files changed

+115
-8
lines changed

5 files changed

+115
-8
lines changed

packages/userscript/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"lint:fix": "eslint . --fix"
1717
},
1818
"dependencies": {
19+
"html2canvas": "^1.4.1",
1920
"vite-plugin-monkey": "^2.7.3"
2021
},
2122
"devDependencies": {

packages/userscript/src/icons.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ export const fileLines = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3
44
// source: fontawesome: file-code
55
export const fileCode = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" class="w-4 h-4" fill="currentColor"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zM256 0V128H384L256 0zM153 289l-31 31 31 31c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0L71 337c-9.4-9.4-9.4-24.6 0-33.9l48-48c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9zM265 255l48 48c9.4 9.4 9.4 24.6 0 33.9l-48 48c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l31-31-31-31c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0z"/></svg>'
66

7+
// source: fontawesome: file-image
8+
export const iconCamera = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="w-4 h-4" fill="currentColor"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M149.1 64.8L138.7 96H64C28.7 96 0 124.7 0 160V416c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H373.3L362.9 64.8C356.4 45.2 338.1 32 317.4 32H194.6c-20.7 0-39 13.2-45.5 32.8zM256 384c-53 0-96-43-96-96s43-96 96-96s96 43 96 96s-43 96-96 96z"/></svg>'
9+
710
// source: ChatGPT generated
8-
export const fileGif = `
9-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" class="w-4 h-4" fill="currentColor">
10-
<rect x="10" y="10" width="80" height="80" fill="#EFEFEF" />
11-
<text x="50" y="60" font-size="40" text-anchor="middle">GIF</text>
12-
</svg>`
11+
// export const fileGif = `
12+
// <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" class="w-4 h-4" fill="currentColor">
13+
// <rect x="10" y="10" width="80" height="80" fill="#EFEFEF" />
14+
// <text x="50" y="60" font-size="40" text-anchor="middle">GIF</text>
15+
// </svg>`
1316

1417
// source: fontawesome: copy
1518
export const iconCopy = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="w-4 h-4" fill="currentColor"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M502.6 70.63l-61.25-61.25C435.4 3.371 427.2 0 418.7 0H255.1c-35.35 0-64 28.66-64 64l.0195 256C192 355.4 220.7 384 256 384h192c35.2 0 64-28.8 64-64V93.25C512 84.77 508.6 76.63 502.6 70.63zM464 320c0 8.836-7.164 16-16 16H255.1c-8.838 0-16-7.164-16-16L239.1 64.13c0-8.836 7.164-16 16-16h128L384 96c0 17.67 14.33 32 32 32h47.1V320zM272 448c0 8.836-7.164 16-16 16H63.1c-8.838 0-16-7.164-16-16L47.98 192.1c0-8.836 7.164-16 16-16H160V128H63.99c-35.35 0-64 28.65-64 64l.0098 256C.002 483.3 28.66 512 64 512h192c35.2 0 64-28.8 64-64v-32h-47.1L272 448z"/></svg>'

packages/userscript/src/main.ts

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { chatGPTAvatarSVG, fileCode, iconCopy } from './icons'
1+
import html2canvas from 'html2canvas'
2+
import { chatGPTAvatarSVG, fileCode, iconCamera, iconCopy } from './icons'
23
import './style.scss'
3-
import { copyToClipboard, downloadFile, getBase64FromImg, onloadSafe } from './utils'
4+
import { copyToClipboard, downloadFile, downloadUrl, getBase64FromImg, onloadSafe, sleep } from './utils'
45
import templateHtml from './template.html?raw'
56

67
type ConversationLine = |
@@ -56,9 +57,15 @@ function main() {
5657
})
5758
container.appendChild(copyButton)
5859

60+
const imageButton = <HTMLAnchorElement>firstItem.cloneNode(true)
61+
imageButton.removeAttribute('href')
62+
imageButton.innerHTML = `${iconCamera}Screenshot`
63+
imageButton.addEventListener('click', () => exportToPng())
64+
container.appendChild(imageButton)
65+
5966
const htmlButton = <HTMLAnchorElement>firstItem.cloneNode(true)
6067
htmlButton.removeAttribute('href')
61-
htmlButton.innerHTML = `${fileCode}Export to .html`
68+
htmlButton.innerHTML = `${fileCode}Export WebPage`
6269
htmlButton.addEventListener('click', () => exportToHtml())
6370
container.appendChild(htmlButton)
6471
})
@@ -120,6 +127,56 @@ ${linesHtml}
120127
downloadFile(fileName, 'text/html', html)
121128
}
122129

130+
async function exportToPng() {
131+
const thread = document.querySelector('[class^="ThreadLayout__NodeWrapper"]') as HTMLElement
132+
if (!thread) return
133+
134+
// hide irrelevant elements
135+
thread.style.height = 'auto'
136+
137+
const alertWrapper = document.querySelector('[class^="_app__AlertWrapper"]')
138+
if (alertWrapper) alertWrapper.remove()
139+
140+
const positionForm = document.querySelector('[class^="Thread__PositionForm"]') as HTMLElement
141+
if (positionForm) positionForm.style.display = 'none'
142+
143+
const bottomSpacer = document.querySelector('[class^="ThreadLayout__BottomSpacer"]') as HTMLElement
144+
if (bottomSpacer) bottomSpacer.style.display = 'none'
145+
146+
const threadWrapper = document.querySelector('[class^="Thread__Wrapper"]')
147+
if (threadWrapper && threadWrapper.children.length > 1) {
148+
const leftSidebar = threadWrapper.children[1] as HTMLElement
149+
const mainContent = threadWrapper.children[0] as HTMLElement
150+
leftSidebar.style.display = 'none'
151+
mainContent.style.paddingLeft = '0'
152+
}
153+
154+
await sleep(100)
155+
156+
const canvas = await html2canvas(thread, {
157+
scrollX: -window.scrollX,
158+
scrollY: -window.scrollY,
159+
windowWidth: thread.scrollWidth,
160+
windowHeight: thread.scrollHeight,
161+
})
162+
163+
// restore the layout
164+
if (threadWrapper && threadWrapper.children.length > 1) {
165+
const leftSidebar = threadWrapper.children[1] as HTMLElement
166+
const mainContent = threadWrapper.children[0] as HTMLElement
167+
leftSidebar.style.display = ''
168+
mainContent.style.paddingLeft = ''
169+
}
170+
if (positionForm) positionForm.style.display = ''
171+
if (bottomSpacer) bottomSpacer.style.display = ''
172+
thread.style.height = ''
173+
174+
const dataUrl = canvas.toDataURL('image/png', 1)
175+
.replace(/^data:image\/[^;]/, 'data:application/octet-stream')
176+
const fileName = `ChatGPT-${new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '')}.png`
177+
downloadUrl(fileName, dataUrl)
178+
}
179+
123180
function getConversation(): ConversationItem[] {
124181
const thread = document.querySelector('[class^="ThreadLayout__NodeWrapper"]')
125182
if (!thread) return []

packages/userscript/src/utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ export function downloadFile(filename: string, type: string, content: string) {
3333
document.body.removeChild(a)
3434
}
3535

36+
export function downloadUrl(filename: string, url: string) {
37+
const a = document.createElement('a')
38+
a.href = url
39+
a.download = filename
40+
document.body.appendChild(a)
41+
a.click()
42+
document.body.removeChild(a)
43+
}
44+
3645
export function getBase64FromImg(el: HTMLImageElement) {
3746
const canvas = document.createElement('canvas')
3847
canvas.width = el.naturalWidth
@@ -42,3 +51,7 @@ export function getBase64FromImg(el: HTMLImageElement) {
4251
ctx.drawImage(el, 0, 0)
4352
return canvas.toDataURL('image/png')
4453
}
54+
55+
export function sleep(ms: number) {
56+
return new Promise(resolve => setTimeout(resolve, ms))
57+
}

pnpm-lock.yaml

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)