diff --git a/src/renderer/src/pages/notes/NotesSidebar.tsx b/src/renderer/src/pages/notes/NotesSidebar.tsx index e7f3db9494..a2917f6316 100644 --- a/src/renderer/src/pages/notes/NotesSidebar.tsx +++ b/src/renderer/src/pages/notes/NotesSidebar.tsx @@ -359,6 +359,23 @@ const NotesSidebar: FC = ({ [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 = ({ key: 'export', icon: , 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 = ({ handleStartEdit, onToggleStar, handleExportKnowledge, + handleImageAction, handleDeleteNode, renamingNodeIds, handleAutoRename, diff --git a/src/renderer/src/utils/export.ts b/src/renderer/src/utils/export.ts index d50b5b0e5d..728fb2d89b 100644 --- a/src/renderer/src/utils/export.ts +++ b/src/renderer/src/utils/export.ts @@ -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 => { + 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 => { + 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 => { @@ -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':