feat: support export image for notes (#10559)

* feat: support export image for notes

* feat: extract functions
This commit is contained in:
Tristan Zhang 2025-10-08 23:32:32 +08:00 committed by GitHub
parent 6a8544fb0e
commit 42849e4586
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 82 additions and 1 deletions

View File

@ -359,6 +359,23 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
[bases.length, t]
)
const handleImageAction = useCallback(
async (node: NotesTreeNode, platform: 'copyImage' | 'exportImage') => {
try {
if (activeNode?.id !== node.id) {
onSelectNode(node)
await new Promise((resolve) => setTimeout(resolve, 500))
}
await exportNote({ node, platform })
} catch (error) {
logger.error(`Failed to ${platform === 'copyImage' ? 'copy' : 'export'} as image:`, error as Error)
window.toast.error(t('common.copy_failed'))
}
},
[activeNode, onSelectNode, t]
)
const handleAutoRename = useCallback(
async (note: NotesTreeNode) => {
if (note.type !== 'file') return
@ -612,6 +629,16 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
key: 'export',
icon: <UploadIcon size={14} />,
children: [
exportMenuOptions.image && {
label: t('chat.topics.copy.image'),
key: 'copy-image',
onClick: () => handleImageAction(node, 'copyImage')
},
exportMenuOptions.image && {
label: t('chat.topics.export.image'),
key: 'export-image',
onClick: () => handleImageAction(node, 'exportImage')
},
exportMenuOptions.markdown && {
label: t('chat.topics.export.md.label'),
key: 'markdown',
@ -671,6 +698,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
handleStartEdit,
onToggleStar,
handleExportKnowledge,
handleImageAction,
handleDeleteNode,
renamingNodeIds,
handleAutoRename,

View File

@ -9,6 +9,7 @@ import { setExportState } from '@renderer/store/runtime'
import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { removeSpecialCharactersForFileName } from '@renderer/utils/file'
import { captureScrollableAsBlob, captureScrollableAsDataURL } from '@renderer/utils/image'
import { convertMathFormula, markdownToPlainText } from '@renderer/utils/markdown'
import { getCitationContent, getMainTextContent, getThinkingContent } from '@renderer/utils/messageUtils/find'
import { markdownToBlocks } from '@tryfabric/martian'
@ -1092,9 +1093,57 @@ const exportNoteAsMarkdown = async (noteName: string, content: string): Promise<
}
}
const getScrollableElement = (): HTMLElement | null => {
const notesPage = document.querySelector('#notes-page')
if (!notesPage) return null
const allDivs = notesPage.querySelectorAll('div')
for (const div of Array.from(allDivs)) {
const style = window.getComputedStyle(div)
if (style.overflowY === 'auto' || style.overflowY === 'scroll') {
if (div.querySelector('.ProseMirror')) {
return div as HTMLElement
}
}
}
return null
}
const getScrollableRef = (): { current: HTMLElement } | null => {
const element = getScrollableElement()
if (!element) {
window.toast.warning(i18n.t('notes.no_content_to_copy'))
return null
}
return { current: element }
}
const exportNoteAsImageToClipboard = async (): Promise<void> => {
const scrollableRef = getScrollableRef()
if (!scrollableRef) return
await captureScrollableAsBlob(scrollableRef, async (blob) => {
if (blob) {
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })])
window.toast.success(i18n.t('common.copied'))
}
})
}
const exportNoteAsImageFile = async (noteName: string): Promise<void> => {
const scrollableRef = getScrollableRef()
if (!scrollableRef) return
const dataUrl = await captureScrollableAsDataURL(scrollableRef)
if (dataUrl) {
const fileName = removeSpecialCharactersForFileName(noteName)
await window.api.file.saveImage(fileName, dataUrl)
}
}
interface NoteExportOptions {
node: { name: string; externalPath: string }
platform: 'markdown' | 'docx' | 'notion' | 'yuque' | 'obsidian' | 'joplin' | 'siyuan'
platform: 'markdown' | 'docx' | 'notion' | 'yuque' | 'obsidian' | 'joplin' | 'siyuan' | 'copyImage' | 'exportImage'
}
export const exportNote = async ({ node, platform }: NoteExportOptions): Promise<void> => {
@ -1102,6 +1151,10 @@ export const exportNote = async ({ node, platform }: NoteExportOptions): Promise
const content = await window.api.file.readExternal(node.externalPath)
switch (platform) {
case 'copyImage':
return await exportNoteAsImageToClipboard()
case 'exportImage':
return await exportNoteAsImageFile(node.name)
case 'markdown':
return await exportNoteAsMarkdown(node.name, content)
case 'docx':