From 49bf8a97d27b6796c22632e419b38a9a59cefb55 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Mon, 9 Mar 2026 17:27:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(remote):=20=E6=B7=BB=E5=8A=A0=20CORS=20?= =?UTF-8?q?=E4=B8=AD=E9=97=B4=E4=BB=B6=E6=94=AF=E6=8C=81=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=B7=A8=E5=9F=9F=E8=AE=BF=E9=97=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 8 +- api/config/index.ts | 2 +- api/core/files/routes.ts | 104 +++++++++ .../time-tracking/sessionPersistence.ts | 40 +++- api/modules/time-tracking/timeService.ts | 28 ++- dist-electron/main.js | 2 +- dist-electron/main.js.map | 2 +- electron/main.ts | 2 +- index.html | 2 +- package.json | 10 +- remote/frp/frpc-runtime.toml | 2 +- remote/package-lock.json | 27 +++ remote/package.json | 1 + remote/src/core/App.js | 16 ++ shared/types/tab.ts | 2 +- shared/utils/tabType.ts | 10 + .../file-system/Sidebar/Sidebar.tsx | 3 +- .../file-transfer/LocalFilePanel.tsx | 46 +++- .../time-tracking/TimeTrackingPage.tsx | 208 ++++++++++++++++-- src/modules/time-tracking/types.ts | 10 + tools/mineru/config.py | 2 +- tools/tongyi/main.py | 2 +- 22 files changed, 467 insertions(+), 62 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c5c275f..4f7acba 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # 贡献指南 -感谢你对 XCNote 项目的兴趣!我们欢迎任何形式的贡献,包括但不限于: +感谢你对 XCDesktop 项目的兴趣!我们欢迎任何形式的贡献,包括但不限于: - 🐛 报告 Bug - 💡 提出新功能建议 @@ -19,8 +19,8 @@ ### 克隆项目 ```bash -git clone https://github.com/your-repo/XCNote.git -cd XCNote +git clone https://github.com/your-repo/XCDesktop.git +cd XCDesktop ``` ### 安装依赖 @@ -162,7 +162,7 @@ fix(editor): 修复保存内容丢失问题 ## 项目结构概览 ``` -XCNote/ +XCDesktop/ ├── src/ # 前端源码 │ ├── components/ # UI 组件 │ ├── contexts/ # React Context diff --git a/api/config/index.ts b/api/config/index.ts index e12c347..78b27b4 100644 --- a/api/config/index.ts +++ b/api/config/index.ts @@ -20,7 +20,7 @@ export const config = { }, get tempRoot(): string { - return path.join(os.tmpdir(), 'xcnote_uploads') + return path.join(os.tmpdir(), 'xcdesktop_uploads') }, get serverPort(): number { diff --git a/api/core/files/routes.ts b/api/core/files/routes.ts index ec7157f..733caa0 100644 --- a/api/core/files/routes.ts +++ b/api/core/files/routes.ts @@ -32,6 +32,110 @@ import { logger } from '../../utils/logger.js' const router = express.Router() +router.get( + '/drives', + asyncHandler(async (_req: Request, res: Response) => { + const drives: FileItemDTO[] = [] + const letters = 'CDEFGHIJKLMNOPQRSTUVWXYZ'.split('') + + for (const letter of letters) { + const drivePath = `${letter}:\\` + try { + await fs.access(drivePath) + drives.push({ + name: `${letter}:`, + type: 'dir', + size: 0, + modified: new Date().toISOString(), + path: drivePath, + }) + } catch { + // 驱动器不存在,跳过 + } + } + + successResponse(res, { items: drives }) + }), +) + +router.get( + '/system', + validateQuery(listFilesQuerySchema), + asyncHandler(async (req: Request, res: Response) => { + const systemPath = req.query.path as string + if (!systemPath) { + throw new BadRequestError('路径不能为空') + } + + const fullPath = path.resolve(systemPath) + + try { + await fs.access(fullPath) + } catch { + throw new NotFoundError('路径不存在') + } + + const stats = await fs.stat(fullPath) + if (!stats.isDirectory()) { + throw new NotADirectoryError() + } + + const files = await fs.readdir(fullPath) + const items = await Promise.all( + files.map(async (name): Promise => { + const filePath = path.join(fullPath, name) + try { + const fileStats = await fs.stat(filePath) + return { + name, + type: fileStats.isDirectory() ? 'dir' : 'file', + size: fileStats.size, + modified: fileStats.mtime.toISOString(), + path: filePath, + } + } catch { + return null + } + }), + ) + + const visibleItems = items.filter((i): i is FileItemDTO => i !== null && !i.name.startsWith('.')) + visibleItems.sort((a, b) => { + if (a.type === b.type) return a.name.localeCompare(b.name) + return a.type === 'dir' ? -1 : 1 + }) + + successResponse(res, { items: visibleItems }) + }), +) + +router.get( + '/system/content', + validateQuery(contentQuerySchema), + asyncHandler(async (req: Request, res: Response) => { + const systemPath = req.query.path as string + if (!systemPath) { + throw new BadRequestError('路径不能为空') + } + + const fullPath = path.resolve(systemPath) + const stats = await fs.stat(fullPath).catch(() => { + throw new NotFoundError('文件不存在') + }) + + if (!stats.isFile()) throw new BadRequestError('不是文件') + + const content = await fs.readFile(fullPath, 'utf-8') + successResponse(res, { + content, + metadata: { + size: stats.size, + modified: stats.mtime.toISOString(), + }, + }) + }), +) + router.get( '/', validateQuery(listFilesQuerySchema), diff --git a/api/modules/time-tracking/sessionPersistence.ts b/api/modules/time-tracking/sessionPersistence.ts index 562cddd..f0fc40b 100644 --- a/api/modules/time-tracking/sessionPersistence.ts +++ b/api/modules/time-tracking/sessionPersistence.ts @@ -182,7 +182,9 @@ class SessionPersistenceService implements SessionPersistence { const filePath = getMonthFilePath(year, month) try { const content = await fs.readFile(filePath, 'utf-8') - return JSON.parse(content) + const data = JSON.parse(content) + data.activeDays = Object.values(data.days).filter(d => d.totalDuration > 0).length + return data } catch (err) { return createEmptyMonthData(year, month) } @@ -192,7 +194,9 @@ class SessionPersistenceService implements SessionPersistence { const filePath = getYearFilePath(year) try { const content = await fs.readFile(filePath, 'utf-8') - return JSON.parse(content) + const data = JSON.parse(content) + data.totalActiveDays = Object.values(data.months).filter(m => m.totalDuration > 0).length + return data } catch (err) { return createEmptyYearData(year) } @@ -226,7 +230,7 @@ class SessionPersistenceService implements SessionPersistence { if (existingSessionIndex >= 0) { const oldDuration = dayData.sessions[existingSessionIndex].duration dayData.sessions[existingSessionIndex] = realtimeSession - dayData.totalDuration += currentSessionDuration - oldDuration + dayData.totalDuration = dayData.totalDuration - oldDuration + currentSessionDuration } else { dayData.sessions.push(realtimeSession) dayData.totalDuration += currentSessionDuration @@ -269,8 +273,10 @@ class SessionPersistenceService implements SessionPersistence { monthData.days[dayStr].totalDuration += duration monthData.days[dayStr].sessions += 1 monthData.monthlyTotal += duration - monthData.activeDays = Object.keys(monthData.days).length - monthData.averageDaily = Math.floor(monthData.monthlyTotal / monthData.activeDays) + monthData.activeDays = Object.values(monthData.days).filter(d => d.totalDuration > 0).length + monthData.averageDaily = monthData.activeDays > 0 + ? Math.floor(monthData.monthlyTotal / monthData.activeDays) + : 0 monthData.lastUpdated = new Date().toISOString() await fs.writeFile(filePath, JSON.stringify(monthData, null, 2), 'utf-8') @@ -289,10 +295,15 @@ class SessionPersistenceService implements SessionPersistence { yearData.months[monthStr].totalDuration += duration yearData.yearlyTotal += duration - yearData.totalActiveDays = Object.values(yearData.months).reduce((sum, m) => sum + m.activeDays, 0) + yearData.totalActiveDays = Object.values(yearData.months).reduce((sum, m) => { + const hasActiveDays = m.totalDuration > 0 ? 1 : 0 + return sum + hasActiveDays + }, 0) - const monthCount = Object.keys(yearData.months).length - yearData.averageMonthly = Math.floor(yearData.yearlyTotal / monthCount) + const activeMonthCount = Object.values(yearData.months).filter(m => m.totalDuration > 0).length + yearData.averageMonthly = activeMonthCount > 0 + ? Math.floor(yearData.yearlyTotal / activeMonthCount) + : 0 yearData.averageDaily = yearData.totalActiveDays > 0 ? Math.floor(yearData.yearlyTotal / yearData.totalActiveDays) : 0 @@ -315,7 +326,7 @@ class SessionPersistenceService implements SessionPersistence { const oldDayDuration = monthData.days[dayStr].totalDuration monthData.days[dayStr].totalDuration = todayDuration monthData.monthlyTotal = monthData.monthlyTotal - oldDayDuration + todayDuration - monthData.activeDays = Object.keys(monthData.days).length + monthData.activeDays = Object.values(monthData.days).filter(d => d.totalDuration > 0).length monthData.averageDaily = monthData.activeDays > 0 ? Math.floor(monthData.monthlyTotal / monthData.activeDays) : 0 @@ -345,10 +356,15 @@ class SessionPersistenceService implements SessionPersistence { yearData.months[monthStr].totalDuration = monthData.monthlyTotal yearData.months[monthStr].activeDays = monthData.activeDays yearData.yearlyTotal = yearData.yearlyTotal - oldMonthTotal + monthData.monthlyTotal - yearData.totalActiveDays = Object.values(yearData.months).reduce((sum, m) => sum + m.activeDays, 0) + yearData.totalActiveDays = Object.values(yearData.months).reduce((sum, m) => { + const hasActiveDays = m.totalDuration > 0 ? 1 : 0 + return sum + hasActiveDays + }, 0) - const monthCount = Object.keys(yearData.months).length - yearData.averageMonthly = monthCount > 0 ? Math.floor(yearData.yearlyTotal / monthCount) : 0 + const activeMonthCount = Object.values(yearData.months).filter(m => m.totalDuration > 0).length + yearData.averageMonthly = activeMonthCount > 0 + ? Math.floor(yearData.yearlyTotal / activeMonthCount) + : 0 yearData.averageDaily = yearData.totalActiveDays > 0 ? Math.floor(yearData.yearlyTotal / yearData.totalActiveDays) : 0 diff --git a/api/modules/time-tracking/timeService.ts b/api/modules/time-tracking/timeService.ts index 4c9f31f..112e1d5 100644 --- a/api/modules/time-tracking/timeService.ts +++ b/api/modules/time-tracking/timeService.ts @@ -366,23 +366,47 @@ class TimeTrackerService { if (targetMonth) { const monthData = await this.persistence.getMonthData(targetYear, targetMonth) totalDuration = monthData.monthlyTotal - activeDays = monthData.activeDays + activeDays = Object.values(monthData.days).filter(d => d.totalDuration > 0).length for (const [day, summary] of Object.entries(monthData.days)) { if (!longestDay || summary.totalDuration > longestDay.duration) { longestDay = { date: `${targetYear}-${targetMonth.toString().padStart(2, '0')}-${day}`, duration: summary.totalDuration } } + for (const tab of summary.topTabs || []) { + tabDurations[tab.fileName] = (tabDurations[tab.fileName] || 0) + tab.duration + } + } + + const dayData = await this.persistence.getDayData(targetYear, targetMonth, 1) + for (const session of dayData.sessions) { + for (const record of session.tabRecords) { + const key = record.filePath || record.fileName + tabDurations[key] = (tabDurations[key] || 0) + record.duration + tabTypeDurations[record.tabType] = (tabTypeDurations[record.tabType] || 0) + record.duration + } } } else { const yearData = await this.persistence.getYearData(targetYear) totalDuration = yearData.yearlyTotal - activeDays = yearData.totalActiveDays + activeDays = Object.values(yearData.months).reduce((sum, m) => { + return sum + Object.entries(m).filter(([_, d]) => (d as { totalDuration: number }).totalDuration > 0).length + }, 0) for (const [month, summary] of Object.entries(yearData.months)) { if (!longestDay || summary.totalDuration > longestDay.duration) { longestDay = { date: `${targetYear}-${month}`, duration: summary.totalDuration } } } + + for (let m = 1; m <= 12; m++) { + const monthStr = m.toString().padStart(2, '0') + const monthData = await this.persistence.getMonthData(targetYear, m) + for (const dayData of Object.values(monthData.days)) { + for (const tab of dayData.topTabs || []) { + tabDurations[tab.fileName] = (tabDurations[tab.fileName] || 0) + tab.duration + } + } + } } return { diff --git a/dist-electron/main.js b/dist-electron/main.js index efd708d..0032d00 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -140,7 +140,7 @@ var electronState = new ElectronState(); log2.initialize(); var __filename = fileURLToPath(import.meta.url); var __dirname = path2.dirname(__filename); -process.env.NOTEBOOK_ROOT = path2.join(app.getPath("documents"), "XCNote"); +process.env.NOTEBOOK_ROOT = path2.join(app.getPath("documents"), "XCDesktop"); if (!fs2.existsSync(process.env.NOTEBOOK_ROOT)) { try { fs2.mkdirSync(process.env.NOTEBOOK_ROOT, { recursive: true }); diff --git a/dist-electron/main.js.map b/dist-electron/main.js.map index a13385b..fefaef4 100644 --- a/dist-electron/main.js.map +++ b/dist-electron/main.js.map @@ -1 +1 @@ -{"version":3,"sources":["../electron/main.ts","../electron/services/pdfGenerator.ts","../electron/services/htmlImport.ts","../electron/state.ts"],"sourcesContent":["import { app, BrowserWindow, shell, ipcMain, dialog, nativeTheme, globalShortcut, clipboard } from 'electron';\r\nimport path from 'path';\r\nimport { fileURLToPath, pathToFileURL } from 'url';\r\nimport fs from 'fs';\r\nimport log from 'electron-log';\r\nimport { generatePdf } from './services/pdfGenerator';\r\nimport { selectHtmlFile } from './services/htmlImport';\r\nimport { electronState } from './state';\r\n\r\nlog.initialize();\r\n\r\nconst __filename = fileURLToPath(import.meta.url);\r\nconst __dirname = path.dirname(__filename);\r\n\r\nprocess.env.NOTEBOOK_ROOT = path.join(app.getPath('documents'), 'XCNote');\r\n\r\nif (!fs.existsSync(process.env.NOTEBOOK_ROOT)) {\r\n try {\r\n fs.mkdirSync(process.env.NOTEBOOK_ROOT, { recursive: true });\r\n } catch (err) {\r\n log.error('Failed to create notebook directory:', err);\r\n }\r\n}\r\n\r\nelectronState.setDevelopment(!app.isPackaged);\r\n\r\nlet lastClipboardText = '';\r\n\r\nfunction startClipboardWatcher() {\r\n lastClipboardText = clipboard.readText();\r\n \r\n setInterval(() => {\r\n try {\r\n const currentText = clipboard.readText();\r\n if (currentText && currentText !== lastClipboardText) {\r\n lastClipboardText = currentText;\r\n log.info('Clipboard changed, syncing to remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-auto-sync', currentText);\r\n }\r\n }\r\n } catch (e) {\r\n // ignore\r\n }\r\n }, 1000);\r\n}\r\n\r\nasync function createWindow() {\r\n const initialSymbolColor = nativeTheme.shouldUseDarkColors ? '#ffffff' : '#000000';\r\n \r\n const mainWindow = new BrowserWindow({\r\n width: 1280,\r\n height: 800,\r\n minWidth: 1600,\r\n minHeight: 900,\r\n autoHideMenuBar: true,\r\n titleBarStyle: 'hidden',\r\n titleBarOverlay: {\r\n color: '#00000000',\r\n symbolColor: initialSymbolColor,\r\n height: 32,\r\n },\r\n webPreferences: {\r\n nodeIntegration: false,\r\n contextIsolation: true,\r\n sandbox: false,\r\n webviewTag: true,\r\n preload: path.join(__dirname, 'preload.cjs'),\r\n },\r\n });\r\n\r\n electronState.setMainWindow(mainWindow);\r\n mainWindow.setMenu(null);\r\n\r\n mainWindow.webContents.setWindowOpenHandler(({ url }) => {\r\n if (url.startsWith('http:') || url.startsWith('https:')) {\r\n shell.openExternal(url);\r\n return { action: 'deny' };\r\n }\r\n return { action: 'allow' };\r\n });\r\n\r\n if (electronState.isDevelopment()) {\r\n log.info('Loading development URL...');\r\n try {\r\n await mainWindow.loadURL('http://localhost:5173');\r\n } catch (e) {\r\n log.error('Failed to load dev URL. Make sure npm run electron:dev is used.', e);\r\n }\r\n mainWindow.webContents.openDevTools();\r\n } else {\r\n log.info(`Loading production URL with port ${electronState.getServerPort()}...`);\r\n await mainWindow.loadURL(`http://localhost:${electronState.getServerPort()}`);\r\n }\r\n}\r\n\r\nipcMain.handle('export-pdf', async (event, title, htmlContent) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n if (!win) return { success: false, error: 'No window found' };\r\n\r\n try {\r\n const { filePath } = await dialog.showSaveDialog(win, {\r\n title: '导出 PDF',\r\n defaultPath: `${title}.pdf`,\r\n filters: [{ name: 'PDF Files', extensions: ['pdf'] }]\r\n });\r\n\r\n if (!filePath) return { success: false, canceled: true };\r\n\r\n if (!htmlContent) {\r\n throw new Error('No HTML content provided for PDF export');\r\n }\r\n\r\n const pdfData = await generatePdf(htmlContent);\r\n fs.writeFileSync(filePath, pdfData);\r\n\r\n return { success: true, filePath };\r\n } catch (error: any) {\r\n log.error('Export PDF failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('select-html-file', async (event) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n return selectHtmlFile(win);\r\n});\r\n\r\nipcMain.handle('update-titlebar-buttons', async (event, symbolColor: string) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n if (win) {\r\n win.setTitleBarOverlay({ symbolColor });\r\n return { success: true };\r\n }\r\n return { success: false };\r\n});\r\n\r\nipcMain.handle('clipboard-read-text', async () => {\r\n try {\r\n const text = clipboard.readText();\r\n return { success: true, text };\r\n } catch (error: any) {\r\n log.error('Clipboard read failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('clipboard-write-text', async (event, text: string) => {\r\n try {\r\n clipboard.writeText(text);\r\n return { success: true };\r\n } catch (error: any) {\r\n log.error('Clipboard write failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nasync function startServer() {\r\n if (electronState.isDevelopment()) {\r\n log.info('In dev mode, assuming external servers are running.');\r\n return;\r\n }\r\n\r\n const serverPath = path.join(__dirname, '../dist-api/server.js');\r\n const serverUrl = pathToFileURL(serverPath).href;\r\n\r\n log.info(`Starting internal server from: ${serverPath}`);\r\n try {\r\n const serverModule = await import(serverUrl);\r\n if (serverModule.startServer) {\r\n const port = await serverModule.startServer();\r\n electronState.setServerPort(port);\r\n log.info(`Internal server started successfully on port ${port}`);\r\n } else {\r\n log.warn('startServer function not found in server module, using default port 3001');\r\n }\r\n } catch (e) {\r\n log.error('Failed to start internal server:', e);\r\n }\r\n}\r\n\r\napp.whenReady().then(async () => {\r\n await startServer();\r\n await createWindow();\r\n\r\n startClipboardWatcher();\r\n\r\n globalShortcut.register('CommandOrControl+Shift+C', () => {\r\n log.info('Global shortcut: sync clipboard to remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-sync-to-remote');\r\n }\r\n });\r\n\r\n globalShortcut.register('CommandOrControl+Shift+V', () => {\r\n log.info('Global shortcut: sync clipboard from remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-sync-from-remote');\r\n }\r\n });\r\n\r\n app.on('activate', () => {\r\n if (BrowserWindow.getAllWindows().length === 0) {\r\n createWindow();\r\n }\r\n });\r\n});\r\n\r\napp.on('window-all-closed', () => {\r\n globalShortcut.unregisterAll();\r\n if (process.platform !== 'darwin') {\r\n app.quit();\r\n }\r\n});\r\n","import { BrowserWindow } from 'electron';\n\n/**\n * 生成 PDF 的服务\n * @param htmlContent 完整的 HTML 字符串\n * @returns PDF 文件的二进制数据\n */\nexport async function generatePdf(htmlContent: string): Promise {\n const printWin = new BrowserWindow({\n show: false,\n webPreferences: {\n nodeIntegration: false,\n contextIsolation: true,\n sandbox: false, // 与 main.ts 保持一致,确保脚本执行权限\n }\n });\n\n try {\n await printWin.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`);\n \n // 等待资源加载完成 (由 generatePrintHtml 注入的脚本控制)\n await printWin.webContents.executeJavaScript(`\n new Promise(resolve => {\n const check = () => {\n if (window.__PRINT_READY__) {\n resolve();\n } else {\n setTimeout(check, 100);\n }\n }\n check();\n })\n `);\n\n const pdfData = await printWin.webContents.printToPDF({\n printBackground: true,\n pageSize: 'A4',\n margins: { top: 0, bottom: 0, left: 0, right: 0 }\n });\n\n return pdfData;\n } finally {\n // 确保窗口被关闭,防止内存泄漏\n printWin.close();\n }\n}\n","import { dialog, BrowserWindow } from 'electron'\nimport path from 'path'\nimport fs from 'fs'\nimport log from 'electron-log'\n\nexport interface HtmlImportResult {\n success: boolean\n canceled?: boolean\n error?: string\n htmlPath?: string\n htmlDir?: string\n htmlFileName?: string\n assetsDirName?: string\n assetsFiles?: string[]\n}\n\nexport const selectHtmlFile = async (win: BrowserWindow | null): Promise => {\n if (!win) return { success: false, error: 'No window found' }\n\n try {\n const { filePaths, canceled } = await dialog.showOpenDialog(win, {\n title: '选择 HTML 文件',\n filters: [\n { name: 'HTML Files', extensions: ['html', 'htm'] }\n ],\n properties: ['openFile']\n })\n\n if (canceled || filePaths.length === 0) {\n return { success: false, canceled: true }\n }\n\n const htmlPath = filePaths[0]\n const htmlDir = path.dirname(htmlPath)\n const htmlFileName = path.basename(htmlPath, path.extname(htmlPath))\n \n const assetsDirName = `${htmlFileName}_files`\n const assetsDirPath = path.join(htmlDir, assetsDirName)\n \n const assetsFiles: string[] = []\n if (fs.existsSync(assetsDirPath)) {\n const collectFiles = (dir: string, baseDir: string) => {\n const entries = fs.readdirSync(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n collectFiles(fullPath, baseDir)\n } else {\n const relPath = path.relative(baseDir, fullPath)\n assetsFiles.push(relPath)\n }\n }\n }\n collectFiles(assetsDirPath, assetsDirPath)\n }\n\n return { \n success: true, \n htmlPath,\n htmlDir,\n htmlFileName,\n assetsDirName,\n assetsFiles\n }\n } catch (error: any) {\n log.error('Select HTML file failed:', error)\n return { success: false, error: error.message }\n }\n}\n","import { BrowserWindow } from 'electron'\n\ninterface ElectronAppState {\n mainWindow: BrowserWindow | null\n serverPort: number\n isDev: boolean\n}\n\nclass ElectronState {\n private state: ElectronAppState = {\n mainWindow: null,\n serverPort: 3001,\n isDev: false,\n }\n\n getMainWindow(): BrowserWindow | null {\n return this.state.mainWindow\n }\n\n setMainWindow(window: BrowserWindow | null): void {\n this.state.mainWindow = window\n }\n\n getServerPort(): number {\n return this.state.serverPort\n }\n\n setServerPort(port: number): void {\n this.state.serverPort = port\n }\n\n isDevelopment(): boolean {\n return this.state.isDev\n }\n\n setDevelopment(isDev: boolean): void {\n this.state.isDev = isDev\n }\n\n reset(): void {\n this.state = {\n mainWindow: null,\n serverPort: 3001,\n isDev: false,\n }\n }\n}\n\nexport const electronState = new ElectronState()\n"],"mappings":"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","names":["BrowserWindow","dialog","path","fs","log","log","path","fs","BrowserWindow","dialog"]} \ No newline at end of file +{"version":3,"sources":["../electron/main.ts","../electron/services/pdfGenerator.ts","../electron/services/htmlImport.ts","../electron/state.ts"],"sourcesContent":["import { app, BrowserWindow, shell, ipcMain, dialog, nativeTheme, globalShortcut, clipboard } from 'electron';\r\nimport path from 'path';\r\nimport { fileURLToPath, pathToFileURL } from 'url';\r\nimport fs from 'fs';\r\nimport log from 'electron-log';\r\nimport { generatePdf } from './services/pdfGenerator';\r\nimport { selectHtmlFile } from './services/htmlImport';\r\nimport { electronState } from './state';\r\n\r\nlog.initialize();\r\n\r\nconst __filename = fileURLToPath(import.meta.url);\r\nconst __dirname = path.dirname(__filename);\r\n\r\nprocess.env.NOTEBOOK_ROOT = path.join(app.getPath('documents'), 'XCDesktop');\r\n\r\nif (!fs.existsSync(process.env.NOTEBOOK_ROOT)) {\r\n try {\r\n fs.mkdirSync(process.env.NOTEBOOK_ROOT, { recursive: true });\r\n } catch (err) {\r\n log.error('Failed to create notebook directory:', err);\r\n }\r\n}\r\n\r\nelectronState.setDevelopment(!app.isPackaged);\r\n\r\nlet lastClipboardText = '';\r\n\r\nfunction startClipboardWatcher() {\r\n lastClipboardText = clipboard.readText();\r\n \r\n setInterval(() => {\r\n try {\r\n const currentText = clipboard.readText();\r\n if (currentText && currentText !== lastClipboardText) {\r\n lastClipboardText = currentText;\r\n log.info('Clipboard changed, syncing to remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-auto-sync', currentText);\r\n }\r\n }\r\n } catch (e) {\r\n // ignore\r\n }\r\n }, 1000);\r\n}\r\n\r\nasync function createWindow() {\r\n const initialSymbolColor = nativeTheme.shouldUseDarkColors ? '#ffffff' : '#000000';\r\n \r\n const mainWindow = new BrowserWindow({\r\n width: 1280,\r\n height: 800,\r\n minWidth: 1600,\r\n minHeight: 900,\r\n autoHideMenuBar: true,\r\n titleBarStyle: 'hidden',\r\n titleBarOverlay: {\r\n color: '#00000000',\r\n symbolColor: initialSymbolColor,\r\n height: 32,\r\n },\r\n webPreferences: {\r\n nodeIntegration: false,\r\n contextIsolation: true,\r\n sandbox: false,\r\n webviewTag: true,\r\n preload: path.join(__dirname, 'preload.cjs'),\r\n },\r\n });\r\n\r\n electronState.setMainWindow(mainWindow);\r\n mainWindow.setMenu(null);\r\n\r\n mainWindow.webContents.setWindowOpenHandler(({ url }) => {\r\n if (url.startsWith('http:') || url.startsWith('https:')) {\r\n shell.openExternal(url);\r\n return { action: 'deny' };\r\n }\r\n return { action: 'allow' };\r\n });\r\n\r\n if (electronState.isDevelopment()) {\r\n log.info('Loading development URL...');\r\n try {\r\n await mainWindow.loadURL('http://localhost:5173');\r\n } catch (e) {\r\n log.error('Failed to load dev URL. Make sure npm run electron:dev is used.', e);\r\n }\r\n mainWindow.webContents.openDevTools();\r\n } else {\r\n log.info(`Loading production URL with port ${electronState.getServerPort()}...`);\r\n await mainWindow.loadURL(`http://localhost:${electronState.getServerPort()}`);\r\n }\r\n}\r\n\r\nipcMain.handle('export-pdf', async (event, title, htmlContent) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n if (!win) return { success: false, error: 'No window found' };\r\n\r\n try {\r\n const { filePath } = await dialog.showSaveDialog(win, {\r\n title: '导出 PDF',\r\n defaultPath: `${title}.pdf`,\r\n filters: [{ name: 'PDF Files', extensions: ['pdf'] }]\r\n });\r\n\r\n if (!filePath) return { success: false, canceled: true };\r\n\r\n if (!htmlContent) {\r\n throw new Error('No HTML content provided for PDF export');\r\n }\r\n\r\n const pdfData = await generatePdf(htmlContent);\r\n fs.writeFileSync(filePath, pdfData);\r\n\r\n return { success: true, filePath };\r\n } catch (error: any) {\r\n log.error('Export PDF failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('select-html-file', async (event) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n return selectHtmlFile(win);\r\n});\r\n\r\nipcMain.handle('update-titlebar-buttons', async (event, symbolColor: string) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n if (win) {\r\n win.setTitleBarOverlay({ symbolColor });\r\n return { success: true };\r\n }\r\n return { success: false };\r\n});\r\n\r\nipcMain.handle('clipboard-read-text', async () => {\r\n try {\r\n const text = clipboard.readText();\r\n return { success: true, text };\r\n } catch (error: any) {\r\n log.error('Clipboard read failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('clipboard-write-text', async (event, text: string) => {\r\n try {\r\n clipboard.writeText(text);\r\n return { success: true };\r\n } catch (error: any) {\r\n log.error('Clipboard write failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nasync function startServer() {\r\n if (electronState.isDevelopment()) {\r\n log.info('In dev mode, assuming external servers are running.');\r\n return;\r\n }\r\n\r\n const serverPath = path.join(__dirname, '../dist-api/server.js');\r\n const serverUrl = pathToFileURL(serverPath).href;\r\n\r\n log.info(`Starting internal server from: ${serverPath}`);\r\n try {\r\n const serverModule = await import(serverUrl);\r\n if (serverModule.startServer) {\r\n const port = await serverModule.startServer();\r\n electronState.setServerPort(port);\r\n log.info(`Internal server started successfully on port ${port}`);\r\n } else {\r\n log.warn('startServer function not found in server module, using default port 3001');\r\n }\r\n } catch (e) {\r\n log.error('Failed to start internal server:', e);\r\n }\r\n}\r\n\r\napp.whenReady().then(async () => {\r\n await startServer();\r\n await createWindow();\r\n\r\n startClipboardWatcher();\r\n\r\n globalShortcut.register('CommandOrControl+Shift+C', () => {\r\n log.info('Global shortcut: sync clipboard to remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-sync-to-remote');\r\n }\r\n });\r\n\r\n globalShortcut.register('CommandOrControl+Shift+V', () => {\r\n log.info('Global shortcut: sync clipboard from remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-sync-from-remote');\r\n }\r\n });\r\n\r\n app.on('activate', () => {\r\n if (BrowserWindow.getAllWindows().length === 0) {\r\n createWindow();\r\n }\r\n });\r\n});\r\n\r\napp.on('window-all-closed', () => {\r\n globalShortcut.unregisterAll();\r\n if (process.platform !== 'darwin') {\r\n app.quit();\r\n }\r\n});\r\n","import { BrowserWindow } from 'electron';\n\n/**\n * 生成 PDF 的服务\n * @param htmlContent 完整的 HTML 字符串\n * @returns PDF 文件的二进制数据\n */\nexport async function generatePdf(htmlContent: string): Promise {\n const printWin = new BrowserWindow({\n show: false,\n webPreferences: {\n nodeIntegration: false,\n contextIsolation: true,\n sandbox: false, // 与 main.ts 保持一致,确保脚本执行权限\n }\n });\n\n try {\n await printWin.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`);\n \n // 等待资源加载完成 (由 generatePrintHtml 注入的脚本控制)\n await printWin.webContents.executeJavaScript(`\n new Promise(resolve => {\n const check = () => {\n if (window.__PRINT_READY__) {\n resolve();\n } else {\n setTimeout(check, 100);\n }\n }\n check();\n })\n `);\n\n const pdfData = await printWin.webContents.printToPDF({\n printBackground: true,\n pageSize: 'A4',\n margins: { top: 0, bottom: 0, left: 0, right: 0 }\n });\n\n return pdfData;\n } finally {\n // 确保窗口被关闭,防止内存泄漏\n printWin.close();\n }\n}\n","import { dialog, BrowserWindow } from 'electron'\nimport path from 'path'\nimport fs from 'fs'\nimport log from 'electron-log'\n\nexport interface HtmlImportResult {\n success: boolean\n canceled?: boolean\n error?: string\n htmlPath?: string\n htmlDir?: string\n htmlFileName?: string\n assetsDirName?: string\n assetsFiles?: string[]\n}\n\nexport const selectHtmlFile = async (win: BrowserWindow | null): Promise => {\n if (!win) return { success: false, error: 'No window found' }\n\n try {\n const { filePaths, canceled } = await dialog.showOpenDialog(win, {\n title: '选择 HTML 文件',\n filters: [\n { name: 'HTML Files', extensions: ['html', 'htm'] }\n ],\n properties: ['openFile']\n })\n\n if (canceled || filePaths.length === 0) {\n return { success: false, canceled: true }\n }\n\n const htmlPath = filePaths[0]\n const htmlDir = path.dirname(htmlPath)\n const htmlFileName = path.basename(htmlPath, path.extname(htmlPath))\n \n const assetsDirName = `${htmlFileName}_files`\n const assetsDirPath = path.join(htmlDir, assetsDirName)\n \n const assetsFiles: string[] = []\n if (fs.existsSync(assetsDirPath)) {\n const collectFiles = (dir: string, baseDir: string) => {\n const entries = fs.readdirSync(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n collectFiles(fullPath, baseDir)\n } else {\n const relPath = path.relative(baseDir, fullPath)\n assetsFiles.push(relPath)\n }\n }\n }\n collectFiles(assetsDirPath, assetsDirPath)\n }\n\n return { \n success: true, \n htmlPath,\n htmlDir,\n htmlFileName,\n assetsDirName,\n assetsFiles\n }\n } catch (error: any) {\n log.error('Select HTML file failed:', error)\n return { success: false, error: error.message }\n }\n}\n","import { BrowserWindow } from 'electron'\n\ninterface ElectronAppState {\n mainWindow: BrowserWindow | null\n serverPort: number\n isDev: boolean\n}\n\nclass ElectronState {\n private state: ElectronAppState = {\n mainWindow: null,\n serverPort: 3001,\n isDev: false,\n }\n\n getMainWindow(): BrowserWindow | null {\n return this.state.mainWindow\n }\n\n setMainWindow(window: BrowserWindow | null): void {\n this.state.mainWindow = window\n }\n\n getServerPort(): number {\n return this.state.serverPort\n }\n\n setServerPort(port: number): void {\n this.state.serverPort = port\n }\n\n isDevelopment(): boolean {\n return this.state.isDev\n }\n\n setDevelopment(isDev: boolean): void {\n this.state.isDev = isDev\n }\n\n reset(): void {\n this.state = {\n mainWindow: null,\n serverPort: 3001,\n isDev: false,\n }\n }\n}\n\nexport const electronState = new ElectronState()\n"],"mappings":"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","names":["BrowserWindow","dialog","path","fs","log","log","path","fs","BrowserWindow","dialog"]} \ No newline at end of file diff --git a/electron/main.ts b/electron/main.ts index cbc6534..3efae7d 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -12,7 +12,7 @@ log.initialize(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -process.env.NOTEBOOK_ROOT = path.join(app.getPath('documents'), 'XCNote'); +process.env.NOTEBOOK_ROOT = path.join(app.getPath('documents'), 'XCDesktop'); if (!fs.existsSync(process.env.NOTEBOOK_ROOT)) { try { diff --git a/index.html b/index.html index 956eba6..8f6b8b5 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - XCNote + XCDesktop