mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-10 15:49:29 +08:00
feat: add PDF export functionality and related enhancements
This commit is contained in:
parent
e093ae72da
commit
009e0d0877
@ -3,6 +3,8 @@ import { CodeInspectorPlugin } from 'code-inspector-plugin'
|
|||||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
import { visualizer } from 'rollup-plugin-visualizer'
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
|
import { normalizePath } from 'vite'
|
||||||
|
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||||
|
|
||||||
import pkg from './package.json' assert { type: 'json' }
|
import pkg from './package.json' assert { type: 'json' }
|
||||||
|
|
||||||
@ -75,6 +77,22 @@ export default defineConfig({
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
|
viteStaticCopy({
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
src: normalizePath(resolve(__dirname, 'src/renderer/src/assets/styles/color.css')),
|
||||||
|
dest: normalizePath(resolve(__dirname, 'resources/styles'))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: normalizePath(resolve(__dirname, 'src/renderer/src/assets/styles/font.css')),
|
||||||
|
dest: normalizePath(resolve(__dirname, 'resources/styles'))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: normalizePath(resolve(__dirname, 'src/renderer/src/assets/styles/richtext.css')),
|
||||||
|
dest: normalizePath(resolve(__dirname, 'resources/styles'))
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
...(isDev ? [CodeInspectorPlugin({ bundler: 'vite' })] : []), // 只在开发环境下启用 CodeInspectorPlugin
|
...(isDev ? [CodeInspectorPlugin({ bundler: 'vite' })] : []), // 只在开发环境下启用 CodeInspectorPlugin
|
||||||
...visualizerPlugin('renderer')
|
...visualizerPlugin('renderer')
|
||||||
],
|
],
|
||||||
|
|||||||
@ -335,6 +335,7 @@
|
|||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"vite": "npm:rolldown-vite@latest",
|
"vite": "npm:rolldown-vite@latest",
|
||||||
|
"vite-plugin-static-copy": "^3.1.2",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
"webdav": "^5.8.0",
|
"webdav": "^5.8.0",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
|
|||||||
@ -189,6 +189,7 @@ export enum IpcChannel {
|
|||||||
FileService_Retrieve = 'file-service:retrieve',
|
FileService_Retrieve = 'file-service:retrieve',
|
||||||
|
|
||||||
Export_Word = 'export:word',
|
Export_Word = 'export:word',
|
||||||
|
Export_PDF = 'export:pdf',
|
||||||
|
|
||||||
Shortcuts_Update = 'shortcuts:update',
|
Shortcuts_Update = 'shortcuts:update',
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
export const defaultAppHeaders = () => {
|
export const defaultAppHeaders = () => {
|
||||||
return {
|
return {
|
||||||
'HTTP-Referer': 'https://cherry-ai.com',
|
'HTTP-Referer': 'https://cherry-ai.com',
|
||||||
Referer: 'https://cherry-ai.com',
|
|
||||||
'X-Title': 'Cherry Studio'
|
'X-Title': 'Cherry Studio'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,4 +7,17 @@ exports.default = async function (context) {
|
|||||||
fs.rmSync(path.join(context.appOutDir, 'LICENSE.electron.txt'), { force: true })
|
fs.rmSync(path.join(context.appOutDir, 'LICENSE.electron.txt'), { force: true })
|
||||||
fs.rmSync(path.join(context.appOutDir, 'LICENSES.chromium.html'), { force: true })
|
fs.rmSync(path.join(context.appOutDir, 'LICENSES.chromium.html'), { force: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up resources/styles directory from project root after packing
|
||||||
|
const projectRoot = path.resolve(__dirname, '..')
|
||||||
|
const resourcesStylesPath = path.join(projectRoot, 'resources', 'styles')
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(resourcesStylesPath)) {
|
||||||
|
fs.rmSync(resourcesStylesPath, { recursive: true, force: true })
|
||||||
|
console.log('✓ Cleaned up resources/styles directory from project root')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Warning: Could not clean up resources/styles directory:', error.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,3 +3,36 @@ export const isWin = process.platform === 'win32'
|
|||||||
export const isLinux = process.platform === 'linux'
|
export const isLinux = process.platform === 'linux'
|
||||||
export const isDev = process.env.NODE_ENV === 'development'
|
export const isDev = process.env.NODE_ENV === 'development'
|
||||||
export const isPortable = isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env
|
export const isPortable = isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env
|
||||||
|
|
||||||
|
export const PRINT_HTML_TEMPLATE = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{filename}}</title>
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
margin: 1cm;
|
||||||
|
size: A4;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Color variables */
|
||||||
|
{{colorCss}}
|
||||||
|
/* Font variables */
|
||||||
|
{{fontCss}}
|
||||||
|
/* Richtext styles */
|
||||||
|
{{richtextCss}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root" theme-mode="light" os=${isMac ? 'mac' : isWin ? 'windows' : 'linux'}>
|
||||||
|
<div class="tiptap">{{content}}</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
|||||||
@ -89,6 +89,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
// Initialize Python service with main window
|
// Initialize Python service with main window
|
||||||
pythonService.setMainWindow(mainWindow)
|
pythonService.setMainWindow(mainWindow)
|
||||||
|
|
||||||
|
// Initialize export service with main window
|
||||||
|
exportService.setMainWindow(mainWindow)
|
||||||
|
|
||||||
const checkMainWindow = () => {
|
const checkMainWindow = () => {
|
||||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||||
throw new Error('Main window does not exist or has been destroyed')
|
throw new Error('Main window does not exist or has been destroyed')
|
||||||
@ -512,6 +515,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
// export
|
// export
|
||||||
ipcMain.handle(IpcChannel.Export_Word, exportService.exportToWord.bind(exportService))
|
ipcMain.handle(IpcChannel.Export_Word, exportService.exportToWord.bind(exportService))
|
||||||
|
|
||||||
|
// PDF export
|
||||||
|
ipcMain.handle(IpcChannel.Export_PDF, exportService.exportToPDF.bind(exportService))
|
||||||
|
|
||||||
// open path
|
// open path
|
||||||
ipcMain.handle(IpcChannel.Open_Path, async (_, path: string) => {
|
ipcMain.handle(IpcChannel.Open_Path, async (_, path: string) => {
|
||||||
await shell.openPath(path)
|
await shell.openPath(path)
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
/* eslint-disable no-case-declarations */
|
/* eslint-disable no-case-declarations */
|
||||||
// ExportService
|
// ExportService
|
||||||
|
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import os from 'node:os'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
|
import { PRINT_HTML_TEMPLATE } from '@main/constant'
|
||||||
import {
|
import {
|
||||||
AlignmentType,
|
AlignmentType,
|
||||||
BorderStyle,
|
BorderStyle,
|
||||||
@ -18,7 +23,7 @@ import {
|
|||||||
VerticalAlign,
|
VerticalAlign,
|
||||||
WidthType
|
WidthType
|
||||||
} from 'docx'
|
} from 'docx'
|
||||||
import { dialog } from 'electron'
|
import { app, BrowserWindow, dialog } from 'electron'
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
|
|
||||||
import { fileStorage } from './FileStorage'
|
import { fileStorage } from './FileStorage'
|
||||||
@ -26,11 +31,16 @@ import { fileStorage } from './FileStorage'
|
|||||||
const logger = loggerService.withContext('ExportService')
|
const logger = loggerService.withContext('ExportService')
|
||||||
export class ExportService {
|
export class ExportService {
|
||||||
private md: MarkdownIt
|
private md: MarkdownIt
|
||||||
|
private mainWindow: BrowserWindow | null = null
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.md = new MarkdownIt()
|
this.md = new MarkdownIt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setMainWindow(window: BrowserWindow) {
|
||||||
|
this.mainWindow = window
|
||||||
|
}
|
||||||
|
|
||||||
private convertMarkdownToDocxElements(markdown: string) {
|
private convertMarkdownToDocxElements(markdown: string) {
|
||||||
const tokens = this.md.parse(markdown, {})
|
const tokens = this.md.parse(markdown, {})
|
||||||
const elements: any[] = []
|
const elements: any[] = []
|
||||||
@ -405,4 +415,91 @@ export class ExportService {
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public exportToPDF = async (_: Electron.IpcMainInvokeEvent, content: string, filename: string): Promise<any> => {
|
||||||
|
if (!this.mainWindow) {
|
||||||
|
throw new Error('Main window not set')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const loadCssFile = async (filename: string): Promise<string> => {
|
||||||
|
try {
|
||||||
|
let cssPath: string
|
||||||
|
if (app.isPackaged) {
|
||||||
|
cssPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'resources', 'styles', filename)
|
||||||
|
} else {
|
||||||
|
cssPath = path.join(app.getAppPath(), 'src', 'renderer', 'src', 'assets', 'styles', filename)
|
||||||
|
}
|
||||||
|
return await fs.promises.readFile(cssPath, 'utf-8')
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Could not load ${filename}, using fallback:`, error as Error)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorCss = await loadCssFile('color.css')
|
||||||
|
const fontCss = await loadCssFile('font.css')
|
||||||
|
const richtextCss = await loadCssFile('richtext.css')
|
||||||
|
|
||||||
|
const tempHtmlPath = path.join(os.tmpdir(), `temp_${Date.now()}.html`)
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
tempHtmlPath,
|
||||||
|
PRINT_HTML_TEMPLATE.replace('{{filename}}', filename.replace('.pdf', ''))
|
||||||
|
.replace('{{colorCss}}', colorCss)
|
||||||
|
.replace('{{richtextCss}}', richtextCss)
|
||||||
|
.replace('{{fontCss}}', fontCss)
|
||||||
|
.replace('{{content}}', content)
|
||||||
|
)
|
||||||
|
|
||||||
|
const printWindow = new BrowserWindow({
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
show: false,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await printWindow.loadFile(tempHtmlPath)
|
||||||
|
|
||||||
|
// Show save dialog for PDF
|
||||||
|
const result = await dialog.showSaveDialog(this.mainWindow, {
|
||||||
|
defaultPath: filename,
|
||||||
|
filters: [{ name: 'PDF Files', extensions: ['pdf'] }]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.canceled || !result.filePath) {
|
||||||
|
printWindow.close()
|
||||||
|
await fs.promises.unlink(tempHtmlPath)
|
||||||
|
return { success: false, message: 'Export cancelled' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate PDF using printToPDF for vector output
|
||||||
|
const pdfData = await printWindow.webContents.printToPDF({
|
||||||
|
margins: {
|
||||||
|
top: 0.5,
|
||||||
|
bottom: 0.5,
|
||||||
|
left: 0.5,
|
||||||
|
right: 0.5
|
||||||
|
},
|
||||||
|
pageSize: 'A4',
|
||||||
|
printBackground: true,
|
||||||
|
scale: 1.0,
|
||||||
|
preferCSSPageSize: false,
|
||||||
|
landscape: false
|
||||||
|
})
|
||||||
|
|
||||||
|
await fs.promises.writeFile(result.filePath, pdfData)
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
printWindow.close()
|
||||||
|
await fs.promises.unlink(tempHtmlPath)
|
||||||
|
|
||||||
|
return { success: true, filePath: result.filePath }
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Failed to export PDF:', error)
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -203,7 +203,8 @@ const api = {
|
|||||||
readText: (pathOrUrl: string): Promise<string> => ipcRenderer.invoke(IpcChannel.Fs_ReadText, pathOrUrl)
|
readText: (pathOrUrl: string): Promise<string> => ipcRenderer.invoke(IpcChannel.Fs_ReadText, pathOrUrl)
|
||||||
},
|
},
|
||||||
export: {
|
export: {
|
||||||
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke(IpcChannel.Export_Word, markdown, fileName)
|
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke(IpcChannel.Export_Word, markdown, fileName),
|
||||||
|
toPDF: (content: string, fileName: string) => ipcRenderer.invoke(IpcChannel.Export_PDF, content, fileName)
|
||||||
},
|
},
|
||||||
obsidian: {
|
obsidian: {
|
||||||
getVaults: () => ipcRenderer.invoke(IpcChannel.Obsidian_GetVaults),
|
getVaults: () => ipcRenderer.invoke(IpcChannel.Obsidian_GetVaults),
|
||||||
|
|||||||
@ -1711,6 +1711,7 @@
|
|||||||
"drop_markdown_hint": "Drop markdown files or folders here to import",
|
"drop_markdown_hint": "Drop markdown files or folders here to import",
|
||||||
"empty": "No notes available yet",
|
"empty": "No notes available yet",
|
||||||
"expand": "unfold",
|
"expand": "unfold",
|
||||||
|
"exportPDF": "Export to PDF",
|
||||||
"export_failed": "Failed to export to knowledge base",
|
"export_failed": "Failed to export to knowledge base",
|
||||||
"export_knowledge": "Export notes to knowledge base",
|
"export_knowledge": "Export notes to knowledge base",
|
||||||
"export_success": "Successfully exported to the knowledge base",
|
"export_success": "Successfully exported to the knowledge base",
|
||||||
|
|||||||
@ -1712,6 +1712,7 @@
|
|||||||
"drop_markdown_hint": "拖拽 Markdown 文件或目录到此处导入",
|
"drop_markdown_hint": "拖拽 Markdown 文件或目录到此处导入",
|
||||||
"empty": "暂无笔记",
|
"empty": "暂无笔记",
|
||||||
"expand": "展开",
|
"expand": "展开",
|
||||||
|
"exportPDF": "导出为PDF",
|
||||||
"export_failed": "导出到知识库失败",
|
"export_failed": "导出到知识库失败",
|
||||||
"export_knowledge": "导出笔记到知识库",
|
"export_knowledge": "导出笔记到知识库",
|
||||||
"export_success": "成功导出到知识库",
|
"export_success": "成功导出到知识库",
|
||||||
|
|||||||
@ -1711,6 +1711,7 @@
|
|||||||
"drop_markdown_hint": "拖拽 Markdown 文件或資料夾到此處導入",
|
"drop_markdown_hint": "拖拽 Markdown 文件或資料夾到此處導入",
|
||||||
"empty": "暫無筆記",
|
"empty": "暫無筆記",
|
||||||
"expand": "展開",
|
"expand": "展開",
|
||||||
|
"exportPDF": "匯出為PDF",
|
||||||
"export_failed": "匯出至知識庫失敗",
|
"export_failed": "匯出至知識庫失敗",
|
||||||
"export_knowledge": "匯出筆記至知識庫",
|
"export_knowledge": "匯出筆記至知識庫",
|
||||||
"export_success": "成功匯出至知識庫",
|
"export_success": "成功匯出至知識庫",
|
||||||
|
|||||||
@ -11,11 +11,11 @@ import { MoreHorizontal, PanelLeftClose, PanelRightClose, Star } from 'lucide-re
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { menuItems } from './MenuConfig'
|
import { handleExportPDF, MenuContext, menuItems } from './MenuConfig'
|
||||||
|
|
||||||
const logger = loggerService.withContext('HeaderNavbar')
|
const logger = loggerService.withContext('HeaderNavbar')
|
||||||
|
|
||||||
const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar }) => {
|
const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, editorRef, currentContent }) => {
|
||||||
const { showWorkspace, toggleShowWorkspace } = useShowWorkspace()
|
const { showWorkspace, toggleShowWorkspace } = useShowWorkspace()
|
||||||
const { activeNode } = useActiveNode(notesTree)
|
const { activeNode } = useActiveNode(notesTree)
|
||||||
const [breadcrumbItems, setBreadcrumbItems] = useState<Required<BreadcrumbProps>['items']>([])
|
const [breadcrumbItems, setBreadcrumbItems] = useState<Required<BreadcrumbProps>['items']>([])
|
||||||
@ -47,6 +47,15 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar }) => {
|
|||||||
}
|
}
|
||||||
}, [getCurrentNoteContent])
|
}, [getCurrentNoteContent])
|
||||||
|
|
||||||
|
const handleExportPDFAction = useCallback(async () => {
|
||||||
|
const menuContext: MenuContext = {
|
||||||
|
editorRef,
|
||||||
|
currentContent,
|
||||||
|
fileName: activeNode?.name || t('notes.title')
|
||||||
|
}
|
||||||
|
await handleExportPDF(menuContext)
|
||||||
|
}, [editorRef, currentContent, activeNode])
|
||||||
|
|
||||||
const buildMenuItem = (item: any) => {
|
const buildMenuItem = (item: any) => {
|
||||||
if (item.type === 'divider') {
|
if (item.type === 'divider') {
|
||||||
return { type: 'divider' as const, key: item.key }
|
return { type: 'divider' as const, key: item.key }
|
||||||
@ -88,6 +97,8 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar }) => {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (item.copyAction) {
|
if (item.copyAction) {
|
||||||
handleCopyContent()
|
handleCopyContent()
|
||||||
|
} else if (item.exportPdfAction) {
|
||||||
|
handleExportPDFAction()
|
||||||
} else if (item.action) {
|
} else if (item.action) {
|
||||||
item.action(settings, updateSettings)
|
item.action(settings, updateSettings)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,52 @@
|
|||||||
|
import { loggerService } from '@logger'
|
||||||
|
import { RichEditorRef } from '@renderer/components/RichEditor/types'
|
||||||
import { NotesSettings } from '@renderer/store/note'
|
import { NotesSettings } from '@renderer/store/note'
|
||||||
import { Copy, MonitorSpeaker, Type } from 'lucide-react'
|
import { Copy, Download, MonitorSpeaker, Type } from 'lucide-react'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode, RefObject } from 'react'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('MenuConfig')
|
||||||
|
export interface MenuContext {
|
||||||
|
editorRef: RefObject<RichEditorRef>
|
||||||
|
currentContent: string
|
||||||
|
fileName: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface MenuItem {
|
export interface MenuItem {
|
||||||
key: string
|
key: string
|
||||||
type?: 'divider' | 'component'
|
type?: 'divider' | 'component'
|
||||||
labelKey: string
|
labelKey: string
|
||||||
icon?: React.ComponentType<any>
|
icon?: React.ComponentType<any>
|
||||||
action?: (settings: NotesSettings, updateSettings: (newSettings: Partial<NotesSettings>) => void) => void
|
action?: (
|
||||||
|
settings: NotesSettings,
|
||||||
|
updateSettings: (newSettings: Partial<NotesSettings>) => void,
|
||||||
|
context?: MenuContext
|
||||||
|
) => void
|
||||||
children?: MenuItem[]
|
children?: MenuItem[]
|
||||||
isActive?: (settings: NotesSettings) => boolean
|
isActive?: (settings: NotesSettings) => boolean
|
||||||
component?: (settings: NotesSettings, updateSettings: (newSettings: Partial<NotesSettings>) => void) => ReactNode
|
component?: (settings: NotesSettings, updateSettings: (newSettings: Partial<NotesSettings>) => void) => ReactNode
|
||||||
copyAction?: boolean
|
copyAction?: boolean
|
||||||
|
exportPdfAction?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleExportPDF = async (context: MenuContext) => {
|
||||||
|
if (!context.editorRef?.current || !context.currentContent?.trim()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use Tiptap's getHTML API to get HTML content
|
||||||
|
const htmlContent = context.editorRef.current.getHtml()
|
||||||
|
const filename = context.fileName ? `${context.fileName}.pdf` : 'note.pdf'
|
||||||
|
const result = await window.api.export.toPDF(htmlContent, filename)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
logger.info('PDF exported successfully to:', result.filePath)
|
||||||
|
} else {
|
||||||
|
logger.error('PDF export failed:', result.message)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('PDF export error:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const menuItems: MenuItem[] = [
|
export const menuItems: MenuItem[] = [
|
||||||
@ -21,6 +56,12 @@ export const menuItems: MenuItem[] = [
|
|||||||
icon: Copy,
|
icon: Copy,
|
||||||
copyAction: true
|
copyAction: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'export-pdf',
|
||||||
|
labelKey: 'notes.exportPDF',
|
||||||
|
icon: Download,
|
||||||
|
exportPdfAction: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'divider0',
|
key: 'divider0',
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
|
|||||||
@ -631,6 +631,8 @@ const NotesPage: FC = () => {
|
|||||||
notesTree={notesTree}
|
notesTree={notesTree}
|
||||||
getCurrentNoteContent={getCurrentNoteContent}
|
getCurrentNoteContent={getCurrentNoteContent}
|
||||||
onToggleStar={handleToggleStar}
|
onToggleStar={handleToggleStar}
|
||||||
|
editorRef={editorRef}
|
||||||
|
currentContent={currentContent}
|
||||||
/>
|
/>
|
||||||
<NotesEditor
|
<NotesEditor
|
||||||
activeNodeId={activeNode?.id}
|
activeNodeId={activeNode?.id}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { Dropdown, Input, InputRef, MenuProps } from 'antd'
|
|||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
Download,
|
||||||
Edit3,
|
Edit3,
|
||||||
File,
|
File,
|
||||||
FilePlus,
|
FilePlus,
|
||||||
@ -37,6 +38,7 @@ interface NotesSidebarProps {
|
|||||||
onMoveNode: (sourceNodeId: string, targetNodeId: string, position: 'before' | 'after' | 'inside') => void
|
onMoveNode: (sourceNodeId: string, targetNodeId: string, position: 'before' | 'after' | 'inside') => void
|
||||||
onSortNodes: (sortType: NotesSortType) => void
|
onSortNodes: (sortType: NotesSortType) => void
|
||||||
onUploadFiles: (files: File[]) => void
|
onUploadFiles: (files: File[]) => void
|
||||||
|
onExportToPDF?: (nodeId: string) => void
|
||||||
notesTree: NotesTreeNode[]
|
notesTree: NotesTreeNode[]
|
||||||
selectedFolderId?: string | null
|
selectedFolderId?: string | null
|
||||||
}
|
}
|
||||||
@ -54,6 +56,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
|||||||
onMoveNode,
|
onMoveNode,
|
||||||
onSortNodes,
|
onSortNodes,
|
||||||
onUploadFiles,
|
onUploadFiles,
|
||||||
|
onExportToPDF,
|
||||||
notesTree,
|
notesTree,
|
||||||
selectedFolderId
|
selectedFolderId
|
||||||
}) => {
|
}) => {
|
||||||
@ -184,6 +187,15 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
|||||||
[bases.length, t]
|
[bases.length, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleExportToPDF = useCallback(
|
||||||
|
(note: NotesTreeNode) => {
|
||||||
|
if (onExportToPDF && note.type === 'file') {
|
||||||
|
onExportToPDF(note.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onExportToPDF]
|
||||||
|
)
|
||||||
|
|
||||||
const handleDragStart = useCallback((e: React.DragEvent, node: NotesTreeNode) => {
|
const handleDragStart = useCallback((e: React.DragEvent, node: NotesTreeNode) => {
|
||||||
setDraggedNodeId(node.id)
|
setDraggedNodeId(node.id)
|
||||||
e.dataTransfer.effectAllowed = 'move'
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
@ -330,6 +342,14 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
handleExportKnowledge(node)
|
handleExportKnowledge(node)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('notes.exportPDF'),
|
||||||
|
key: 'export_pdf',
|
||||||
|
icon: <Download size={14} />,
|
||||||
|
onClick: () => {
|
||||||
|
handleExportToPDF(node)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -348,7 +368,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
|||||||
|
|
||||||
return baseMenuItems
|
return baseMenuItems
|
||||||
},
|
},
|
||||||
[t, handleStartEdit, onToggleStar, handleExportKnowledge, handleDeleteNode]
|
[t, handleStartEdit, onToggleStar, handleExportKnowledge, handleExportToPDF, handleDeleteNode]
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderTreeNode = useCallback(
|
const renderTreeNode = useCallback(
|
||||||
|
|||||||
92
yarn.lock
92
yarn.lock
@ -13278,6 +13278,7 @@ __metadata:
|
|||||||
unified: "npm:^11.0.5"
|
unified: "npm:^11.0.5"
|
||||||
uuid: "npm:^10.0.0"
|
uuid: "npm:^10.0.0"
|
||||||
vite: "npm:rolldown-vite@latest"
|
vite: "npm:rolldown-vite@latest"
|
||||||
|
vite-plugin-static-copy: "npm:^3.1.2"
|
||||||
vitest: "npm:^3.2.4"
|
vitest: "npm:^3.2.4"
|
||||||
webdav: "npm:^5.8.0"
|
webdav: "npm:^5.8.0"
|
||||||
winston: "npm:^3.17.0"
|
winston: "npm:^3.17.0"
|
||||||
@ -13642,6 +13643,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"anymatch@npm:~3.1.2":
|
||||||
|
version: 3.1.3
|
||||||
|
resolution: "anymatch@npm:3.1.3"
|
||||||
|
dependencies:
|
||||||
|
normalize-path: "npm:^3.0.0"
|
||||||
|
picomatch: "npm:^2.0.4"
|
||||||
|
checksum: 10c0/57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"app-builder-bin@npm:5.0.0-alpha.12":
|
"app-builder-bin@npm:5.0.0-alpha.12":
|
||||||
version: 5.0.0-alpha.12
|
version: 5.0.0-alpha.12
|
||||||
resolution: "app-builder-bin@npm:5.0.0-alpha.12"
|
resolution: "app-builder-bin@npm:5.0.0-alpha.12"
|
||||||
@ -14041,7 +14052,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"binary-extensions@npm:^2.2.0":
|
"binary-extensions@npm:^2.0.0, binary-extensions@npm:^2.2.0":
|
||||||
version: 2.3.0
|
version: 2.3.0
|
||||||
resolution: "binary-extensions@npm:2.3.0"
|
resolution: "binary-extensions@npm:2.3.0"
|
||||||
checksum: 10c0/75a59cafc10fb12a11d510e77110c6c7ae3f4ca22463d52487709ca7f18f69d886aa387557cc9864fbdb10153d0bdb4caacabf11541f55e89ed6e18d12ece2b5
|
checksum: 10c0/75a59cafc10fb12a11d510e77110c6c7ae3f4ca22463d52487709ca7f18f69d886aa387557cc9864fbdb10153d0bdb4caacabf11541f55e89ed6e18d12ece2b5
|
||||||
@ -14163,7 +14174,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"braces@npm:^3.0.3":
|
"braces@npm:^3.0.3, braces@npm:~3.0.2":
|
||||||
version: 3.0.3
|
version: 3.0.3
|
||||||
resolution: "braces@npm:3.0.3"
|
resolution: "braces@npm:3.0.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -14685,6 +14696,25 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"chokidar@npm:^3.6.0":
|
||||||
|
version: 3.6.0
|
||||||
|
resolution: "chokidar@npm:3.6.0"
|
||||||
|
dependencies:
|
||||||
|
anymatch: "npm:~3.1.2"
|
||||||
|
braces: "npm:~3.0.2"
|
||||||
|
fsevents: "npm:~2.3.2"
|
||||||
|
glob-parent: "npm:~5.1.2"
|
||||||
|
is-binary-path: "npm:~2.1.0"
|
||||||
|
is-glob: "npm:~4.0.1"
|
||||||
|
normalize-path: "npm:~3.0.0"
|
||||||
|
readdirp: "npm:~3.6.0"
|
||||||
|
dependenciesMeta:
|
||||||
|
fsevents:
|
||||||
|
optional: true
|
||||||
|
checksum: 10c0/8361dcd013f2ddbe260eacb1f3cb2f2c6f2b0ad118708a343a5ed8158941a39cb8fb1d272e0f389712e74ee90ce8ba864eece9e0e62b9705cb468a2f6d917462
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"chokidar@npm:^4.0.3":
|
"chokidar@npm:^4.0.3":
|
||||||
version: 4.0.3
|
version: 4.0.3
|
||||||
resolution: "chokidar@npm:4.0.3"
|
resolution: "chokidar@npm:4.0.3"
|
||||||
@ -18182,6 +18212,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"fs-extra@npm:^11.3.0":
|
||||||
|
version: 11.3.1
|
||||||
|
resolution: "fs-extra@npm:11.3.1"
|
||||||
|
dependencies:
|
||||||
|
graceful-fs: "npm:^4.2.0"
|
||||||
|
jsonfile: "npm:^6.0.1"
|
||||||
|
universalify: "npm:^2.0.0"
|
||||||
|
checksum: 10c0/61e5b7285b1ca72c68dfe1058b2514294a922683afac2a80aa90540f9bd85370763d675e3b408ef500077d355956fece3bd24b546790e261c3d3015967e2b2d9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"fs-extra@npm:^8.1.0":
|
"fs-extra@npm:^8.1.0":
|
||||||
version: 8.1.0
|
version: 8.1.0
|
||||||
resolution: "fs-extra@npm:8.1.0"
|
resolution: "fs-extra@npm:8.1.0"
|
||||||
@ -18240,7 +18281,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"fsevents@npm:~2.3.3":
|
"fsevents@npm:~2.3.2, fsevents@npm:~2.3.3":
|
||||||
version: 2.3.3
|
version: 2.3.3
|
||||||
resolution: "fsevents@npm:2.3.3"
|
resolution: "fsevents@npm:2.3.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -18259,7 +18300,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin<compat/fsevents>":
|
"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin<compat/fsevents>":
|
||||||
version: 2.3.3
|
version: 2.3.3
|
||||||
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
|
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -18424,7 +18465,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"glob-parent@npm:^5.1.2":
|
"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2":
|
||||||
version: 5.1.2
|
version: 5.1.2
|
||||||
resolution: "glob-parent@npm:5.1.2"
|
resolution: "glob-parent@npm:5.1.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -19382,6 +19423,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"is-binary-path@npm:~2.1.0":
|
||||||
|
version: 2.1.0
|
||||||
|
resolution: "is-binary-path@npm:2.1.0"
|
||||||
|
dependencies:
|
||||||
|
binary-extensions: "npm:^2.0.0"
|
||||||
|
checksum: 10c0/a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"is-buffer@npm:^2.0.0":
|
"is-buffer@npm:^2.0.0":
|
||||||
version: 2.0.5
|
version: 2.0.5
|
||||||
resolution: "is-buffer@npm:2.0.5"
|
resolution: "is-buffer@npm:2.0.5"
|
||||||
@ -19469,7 +19519,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3":
|
"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1":
|
||||||
version: 4.0.3
|
version: 4.0.3
|
||||||
resolution: "is-glob@npm:4.0.3"
|
resolution: "is-glob@npm:4.0.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -22656,7 +22706,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"normalize-path@npm:^3.0.0":
|
"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0":
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
resolution: "normalize-path@npm:3.0.0"
|
resolution: "normalize-path@npm:3.0.0"
|
||||||
checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046
|
checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046
|
||||||
@ -23072,7 +23122,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"p-map@npm:^7.0.2":
|
"p-map@npm:^7.0.2, p-map@npm:^7.0.3":
|
||||||
version: 7.0.3
|
version: 7.0.3
|
||||||
resolution: "p-map@npm:7.0.3"
|
resolution: "p-map@npm:7.0.3"
|
||||||
checksum: 10c0/46091610da2b38ce47bcd1d8b4835a6fa4e832848a6682cf1652bc93915770f4617afc844c10a77d1b3e56d2472bb2d5622353fa3ead01a7f42b04fc8e744a5c
|
checksum: 10c0/46091610da2b38ce47bcd1d8b4835a6fa4e832848a6682cf1652bc93915770f4617afc844c10a77d1b3e56d2472bb2d5622353fa3ead01a7f42b04fc8e744a5c
|
||||||
@ -23439,7 +23489,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"picomatch@npm:^2.3.1":
|
"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1":
|
||||||
version: 2.3.1
|
version: 2.3.1
|
||||||
resolution: "picomatch@npm:2.3.1"
|
resolution: "picomatch@npm:2.3.1"
|
||||||
checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be
|
checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be
|
||||||
@ -25152,6 +25202,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"readdirp@npm:~3.6.0":
|
||||||
|
version: 3.6.0
|
||||||
|
resolution: "readdirp@npm:3.6.0"
|
||||||
|
dependencies:
|
||||||
|
picomatch: "npm:^2.2.1"
|
||||||
|
checksum: 10c0/6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"redent@npm:^3.0.0":
|
"redent@npm:^3.0.0":
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
resolution: "redent@npm:3.0.0"
|
resolution: "redent@npm:3.0.0"
|
||||||
@ -28287,6 +28346,21 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"vite-plugin-static-copy@npm:^3.1.2":
|
||||||
|
version: 3.1.2
|
||||||
|
resolution: "vite-plugin-static-copy@npm:3.1.2"
|
||||||
|
dependencies:
|
||||||
|
chokidar: "npm:^3.6.0"
|
||||||
|
fs-extra: "npm:^11.3.0"
|
||||||
|
p-map: "npm:^7.0.3"
|
||||||
|
picocolors: "npm:^1.1.1"
|
||||||
|
tinyglobby: "npm:^0.2.14"
|
||||||
|
peerDependencies:
|
||||||
|
vite: ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||||
|
checksum: 10c0/1a65f4c9d291cc27483a5b225b1ac5610edc3aa2f13fa3a76a77327874c83bbee52e1011ee0bf5b0168b9b7b974213d49fe800e44af398cfbcb6607814b45c5b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"vite@npm:rolldown-vite@latest":
|
"vite@npm:rolldown-vite@latest":
|
||||||
version: 7.1.5
|
version: 7.1.5
|
||||||
resolution: "rolldown-vite@npm:7.1.5"
|
resolution: "rolldown-vite@npm:7.1.5"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user