diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3821707 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test diff --git a/README.md b/README.md index 8ee015e..bcfe9f5 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,105 @@ local_read/ - **ePub.js**: 用于解析和渲染 EPUB 文件 - **PDF.js**: 用于解析和渲染 PDF 文件 - **JSZip**: 用于处理压缩文件 +- **测试框架**: Vitest, Supertest + +## 🧪 测试用例 + +项目包含完整的单元测试和集成测试,使用 Vitest 作为测试框架。 + +### 📋 测试结构 + +``` +tests/ +├── frontend/ # 前端单元测试 +│ ├── config.test.js # 配置模块测试 +│ └── utils.test.js # 工具函数测试 +└── server/ # 服务端测试 + ├── api.test.js # API 集成测试 + └── utils.test.js # 服务端工具函数测试 +``` + +### 🔍 测试覆盖范围 + +#### **前端测试** (`tests/frontend/`) + +**配置模块测试** (`config.test.js`) +- ✅ `getFileKey` - 文件路径键生成函数 + - 路径前缀添加测试 + - 空字符串处理测试 + - Windows 路径处理测试 + - 中文路径处理测试 +- ✅ `CONFIG` - 配置常量验证 + - 字体大小限制常量 + - API 路径定义 + - 存储键定义 + - 主题定义 + - 侧边栏视图定义 + +**工具函数测试** (`utils.test.js`) +- ✅ `DEFAULT_READING_PREFS` - 默认阅读偏好设置 +- ✅ `clamp` - 数值范围限制函数(11个测试用例) +- ✅ `normalizePrefs` - 阅读偏好标准化(5个测试用例) +- ✅ `computeVerticalPadding` - 垂直边距计算(3个测试用例) +- ✅ `formatFileSize` - 文件大小格式化(4个测试用例) +- ✅ `formatDecimal` - 小数格式化(4个测试用例) +- ✅ `formatTimeAgo` - 时间相对显示(6个测试用例) +- ✅ `getFileExtension` - 文件扩展名提取(3个测试用例) +- ✅ `deriveBookNameFromPath` - 从路径提取书名(5个测试用例) +- ✅ `debounce` - 防抖函数(2个测试用例) +- ✅ `throttle` - 节流函数(3个测试用例) + +#### **服务端测试** (`tests/server/`) + +**API 集成测试** (`api.test.js`) +- ✅ **书籍管理功能** + - `GET /api/bookshelf` - 获取书籍列表 + - `POST /api/upload` - 上传书籍(支持的格式、格式验证) + - `GET /api/book` - 读取书籍内容(成功读取、404处理) + - `DELETE /api/book` - 删除书籍(成功删除、404处理) + - `GET /api/book-cover` - 获取书籍封面(不同格式处理) +- ✅ **配置管理功能** + - `POST /api/save-config` - 保存配置 + - `GET /api/config-list` - 获取配置列表 + - `GET /api/load-config/:filename` - 加载配置(成功加载、404处理) + - `DELETE /api/config/:filename` - 删除配置 +- ✅ **字体管理功能** + - `GET /api/fonts` - 获取字体列表 + +**服务端工具函数测试** (`utils.test.js`) +- ✅ `normalizePath` - 路径标准化(4个测试用例) +- ✅ `resolveBookPath` - 书籍路径解析(含安全性测试,5个测试用例) +- ✅ `resolveConfigPath` - 配置文件路径解析(含安全性测试,5个测试用例) +- ✅ `decodeFilename` - 文件名解码(2个测试用例) +- ✅ `isAllowedExtension` - 文件扩展名验证(7个测试用例) + +### 🚀 运行测试 + +```bash +# 运行所有测试 +npm test + +# 运行测试并显示覆盖率 +npm run test:coverage + +# 监听模式运行测试(开发时使用) +npm run test:watch + +# 仅运行前端测试 +npx vitest tests/frontend + +# 仅运行服务端测试 +npx vitest tests/server +``` + +### 📊 测试统计 + +- **总测试用例数**: 70+ 个 +- **前端测试**: 46+ 个用例 +- **服务端测试**: 24+ 个用例 +- **测试环境**: + - 前端测试使用 JSDOM 环境 + - 服务端测试使用 Node 环境 ## 🤝 贡献 diff --git a/_test_electron.js b/_test_electron.js new file mode 100644 index 0000000..077451e --- /dev/null +++ b/_test_electron.js @@ -0,0 +1,11 @@ +// 最小化测试脚本 +console.log('process.type:', process.type); +console.log('process.versions.electron:', process.versions.electron); +const electron = require('electron'); +console.log('typeof electron:', typeof electron); +if (typeof electron === 'string') { + console.log('electron path:', electron); +} else { + console.log('keys:', Object.keys(electron)); +} +process.exit(0); diff --git a/electron-main.js b/electron-main.js index 6f73921..bb51a1d 100644 --- a/electron-main.js +++ b/electron-main.js @@ -24,19 +24,16 @@ const getRuntimeDirs = () => { }; // ==================== 性能优化:启动加速 ==================== -// 禁用不必要的 Chromium 特性以加快启动速度 app.commandLine.appendSwitch('disable-gpu-sandbox'); app.commandLine.appendSwitch('disable-software-rasterizer'); app.commandLine.appendSwitch('disable-background-timer-throttling'); app.commandLine.appendSwitch('disable-renderer-backgrounding'); app.commandLine.appendSwitch('disable-backgrounding-occluded-windows'); -// 高 DPI 支持 app.commandLine.appendSwitch('high-dpi-support', '1'); app.commandLine.appendSwitch('force-color-profile', 'srgb'); -// 导入服务器模块 let server = null; -const PORT = 31337; // 使用非常见端口避免冲突 +const PORT = 31337; // 主窗口引用 let mainWindow = null; @@ -53,14 +50,13 @@ function createWindow() { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js'), - // 性能优化选项 backgroundThrottling: false, spellcheck: false, enableWebSQL: false }, show: false, backgroundColor: '#1a1a2e', - titleBarStyle: 'default', + frame: false, autoHideMenuBar: true }); @@ -72,604 +68,43 @@ function createWindow() { // 加载应用 mainWindow.loadURL(`http://localhost:${PORT}`); - // 开发环境下可按 F12 打开开发者工具(不再自动打开) - // if (process.argv.includes('--dev')) { - // mainWindow.webContents.openDevTools(); - // } - // 处理外部链接 mainWindow.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' }; }); + // 窗口最大化/还原事件 -> 通知渲染进程更新标题栏按钮图标 + mainWindow.on('maximize', () => { + mainWindow.webContents.send('maximize-change', true); + }); + mainWindow.on('unmaximize', () => { + mainWindow.webContents.send('maximize-change', false); + }); + // 窗口关闭事件 mainWindow.on('closed', () => { mainWindow = null; }); } -// 启动内嵌服务器 +// 启动内嵌服务器(复用共享核心模块) function startServer() { return new Promise((resolve, reject) => { try { - // 延迟加载模块以加速启动 const express = require('express'); - const multer = require('multer'); - const fs = require('fs'); - const AdmZip = require('adm-zip'); - - const expressApp = express(); + const { createApp } = require('./shared/server-core'); - // 目录配置 - 绿色版:始终使用程序所在目录 const DIRS = getRuntimeDirs(); + const { app: expressApp } = createApp(DIRS); - // 支持的文件扩展名 - const ALLOWED_EXTENSIONS = ['.epub', '.txt', '.pdf']; - const ALLOWED_FONT_EXTENSIONS = ['.ttf', '.otf', '.woff', '.woff2']; - - // MIME类型映射 - const FONT_MIME_TYPES = { - '.ttf': 'font/ttf', - '.otf': 'font/otf', - '.woff': 'font/woff', - '.woff2': 'font/woff2' - }; - - const IMAGE_MIME_TYPES = { - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.png': 'image/png', - '.gif': 'image/gif', - '.webp': 'image/webp' - }; - - // 封面缓存(限制大小,避免大书库占用过多内存) - const coverCache = new Map(); - const COVER_CACHE_LIMIT = 200; - - const setCoverCache = (key, mtime, data) => { - coverCache.set(key, { mtime, data }); - if (coverCache.size > COVER_CACHE_LIMIT) { - const oldestKey = coverCache.keys().next().value; - if (oldestKey) coverCache.delete(oldestKey); - } - }; - - // 确保目录存在 - Object.values(DIRS).forEach(dir => { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - }); - - // 中间件配置 - expressApp.use(express.json({ limit: '10mb' })); // 静态文件服务 - 支持打包后的路径 expressApp.use(express.static(__dirname)); // 书籍/配置目录可能在可执行文件目录之外,统一显式挂载 expressApp.use('/books', express.static(DIRS.books)); expressApp.use('/user-data', express.static(DIRS.config)); - // 工具函数 - const utils = { - normalizePath: (p = '') => p.split(path.sep).join('/'), - resolveBookPath: (relativePath = '') => { - const normalized = path.normalize(relativePath).replace(/^([\.\\/])+/, ''); - const resolved = path.resolve(DIRS.books, normalized); - const booksRoot = path.resolve(DIRS.books); - if (!resolved.toLowerCase().startsWith(booksRoot.toLowerCase())) { - throw new Error('Invalid book path'); - } - return resolved; - }, - resolveConfigPath: (filename = '') => { - const normalized = path.normalize(filename).replace(/^([\.\\/])+/, ''); - const resolved = path.resolve(DIRS.config, normalized); - const configRoot = path.resolve(DIRS.config); - if (!resolved.toLowerCase().startsWith(configRoot.toLowerCase())) { - throw new Error('Invalid config path'); - } - if (path.extname(resolved).toLowerCase() !== '.json') { - throw new Error('Invalid config file type'); - } - return resolved; - }, - cleanupEmptyFolders: (startPath) => { - let current = path.dirname(startPath); - const booksRoot = path.resolve(DIRS.books); - while (current.toLowerCase().startsWith(booksRoot.toLowerCase()) && current !== booksRoot) { - try { - if (fs.readdirSync(current).length === 0) { - fs.rmdirSync(current); - current = path.dirname(current); - } else { - break; - } - } catch { - break; - } - } - }, - decodeFilename: (filename) => Buffer.from(filename, 'latin1').toString('utf8'), - isAllowedExtension: (filename) => { - const ext = path.extname(filename).toLowerCase(); - return ALLOWED_EXTENSIONS.includes(ext); - } - }; - - // 文件上传配置 - const storage = multer.diskStorage({ - destination: (req, file, cb) => { - if (!fs.existsSync(DIRS.books)) { - fs.mkdirSync(DIRS.books, { recursive: true }); - } - cb(null, DIRS.books); - }, - filename: (req, file, cb) => { - const originalName = utils.decodeFilename(file.originalname); - let finalName = originalName; - let counter = 1; - while (fs.existsSync(path.join(DIRS.books, finalName))) { - const ext = path.extname(originalName); - const nameWithoutExt = path.basename(originalName, ext); - finalName = `${nameWithoutExt}(${counter})${ext}`; - counter++; - } - cb(null, finalName); - } - }); - - const upload = multer({ - storage, - fileFilter: (req, file, cb) => { - const originalName = utils.decodeFilename(file.originalname); - if (utils.isAllowedExtension(originalName)) { - cb(null, true); - } else { - cb(new Error('只支持 .epub, .txt, .pdf 文件格式')); - } - } - }); - - // 字体上传配置 - const fontStorage = multer.diskStorage({ - destination: (req, file, cb) => { - if (!fs.existsSync(DIRS.fonts)) { - fs.mkdirSync(DIRS.fonts, { recursive: true }); - } - cb(null, DIRS.fonts); - }, - filename: (req, file, cb) => { - const originalName = utils.decodeFilename(file.originalname); - let finalName = originalName; - let counter = 1; - while (fs.existsSync(path.join(DIRS.fonts, finalName))) { - const ext = path.extname(originalName); - const nameWithoutExt = path.basename(originalName, ext); - finalName = `${nameWithoutExt}(${counter})${ext}`; - counter++; - } - cb(null, finalName); - } - }); - - const fontUpload = multer({ - storage: fontStorage, - fileFilter: (req, file, cb) => { - const originalName = utils.decodeFilename(file.originalname); - const ext = path.extname(originalName).toLowerCase(); - if (ALLOWED_FONT_EXTENSIONS.includes(ext)) { - cb(null, true); - } else { - cb(new Error('只支持 .ttf, .otf, .woff, .woff2 字体格式')); - } - }, - limits: { fileSize: 20 * 1024 * 1024 } - }); - - // EPUB封面提取 - const extractEpubCover = (absolutePath) => { - try { - const stats = fs.statSync(absolutePath); - const cacheKey = absolutePath; - const cached = coverCache.get(cacheKey); - if (cached && cached.mtime === stats.mtimeMs) { - return cached.data; - } - - const zip = new AdmZip(absolutePath); - const entries = zip.getEntries(); - if (!entries || entries.length === 0) return null; - - const imageEntries = entries.filter(entry => { - if (entry.isDirectory) return false; - const ext = path.extname(entry.entryName).toLowerCase(); - return Object.prototype.hasOwnProperty.call(IMAGE_MIME_TYPES, ext); - }); - - if (imageEntries.length === 0) { - setCoverCache(cacheKey, stats.mtimeMs, null); - return null; - } - - let coverEntry = imageEntries.find(entry => /cover/i.test(path.basename(entry.entryName))); - if (!coverEntry) coverEntry = imageEntries[0]; - if (!coverEntry) { - setCoverCache(cacheKey, stats.mtimeMs, null); - return null; - } - - const data = coverEntry.getData(); - if (!data) { - setCoverCache(cacheKey, stats.mtimeMs, null); - return null; - } - - const ext = path.extname(coverEntry.entryName).toLowerCase(); - const mime = IMAGE_MIME_TYPES[ext] || 'image/jpeg'; - const base64 = data.toString('base64'); - const dataUrl = `data:${mime};base64,${base64}`; - - setCoverCache(cacheKey, stats.mtimeMs, dataUrl); - return dataUrl; - } catch (error) { - console.warn('Failed to extract EPUB cover:', error.message); - return null; - } - }; - - // 递归查找书籍 - const findBooks = (dir, fileList = [], parentDir = '') => { - const files = fs.readdirSync(dir); - files.forEach(file => { - const filePath = path.join(dir, file); - const fileStat = fs.statSync(filePath); - const relativePath = path.join(parentDir, file); - - if (fileStat.isDirectory()) { - findBooks(filePath, fileList, relativePath); - } else if (utils.isAllowedExtension(file)) { - const ext = path.extname(file).toLowerCase(); - fileList.push({ - name: file, - path: utils.normalizePath(relativePath), - extension: ext, - size: fileStat.size, - addedAt: fileStat.birthtimeMs || fileStat.ctimeMs, - modifiedAt: fileStat.mtimeMs, - coverAvailable: ext === '.epub' - }); - } - }); - return fileList; - }; - - // ==================== API 路由 ==================== - - // 获取书架列表 - expressApp.get('/api/bookshelf', (req, res) => { - try { - if (!fs.existsSync(DIRS.books)) { - fs.mkdirSync(DIRS.books); - } - const books = findBooks(DIRS.books); - res.json(books); - } catch (error) { - console.error('Error reading bookshelf:', error); - res.status(500).json({ error: 'Failed to read bookshelf directory.' }); - } - }); - - // 获取书籍封面 - expressApp.get('/api/book-cover', (req, res) => { - try { - const relPath = req.query.path; - if (!relPath) { - return res.status(400).json({ error: '缺少书籍路径' }); - } - const absolutePath = utils.resolveBookPath(relPath); - if (!fs.existsSync(absolutePath)) { - return res.status(404).json({ error: '书籍不存在' }); - } - const ext = path.extname(absolutePath).toLowerCase(); - if (ext !== '.epub') { - return res.json({ success: true, cover: null }); - } - const cover = extractEpubCover(absolutePath); - res.json({ success: true, cover }); - } catch (error) { - console.error('Error extracting cover:', error); - res.status(500).json({ error: '封面提取失败: ' + error.message }); - } - }); - - // 保存用户配置 - expressApp.post('/api/save-config', (req, res) => { - try { - const { config, filename } = req.body; - if (!config) { - return res.status(400).json({ error: '配置数据不能为空' }); - } - const configFilename = filename || `reader-config-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`; - let configPath; - try { - configPath = utils.resolveConfigPath(configFilename); - } catch { - return res.status(400).json({ error: '无效的配置文件名' }); - } - const configWithMeta = { - ...config, - metadata: { - ...config.metadata, - savedAt: new Date().toISOString(), - version: '1.0.0', - appName: 'Local E-Book Reader' - } - }; - if (!fs.existsSync(DIRS.config)) { - fs.mkdirSync(DIRS.config, { recursive: true }); - } - fs.writeFileSync(configPath, JSON.stringify(configWithMeta, null, 2)); - res.json({ success: true, message: '配置保存成功', filename: configFilename, path: configPath }); - } catch (error) { - console.error('Error saving config:', error); - res.status(500).json({ error: '保存配置失败: ' + error.message }); - } - }); - - // 加载用户配置 - expressApp.get('/api/load-config/:filename', (req, res) => { - try { - let configPath; - try { - configPath = utils.resolveConfigPath(req.params.filename); - } catch { - return res.status(400).json({ error: '无效的配置文件' }); - } - if (!fs.existsSync(configPath)) { - return res.status(404).json({ error: '配置文件不存在' }); - } - const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); - res.json({ success: true, config, filename: req.params.filename }); - } catch (error) { - console.error('Error loading config:', error); - res.status(500).json({ error: '加载配置失败: ' + error.message }); - } - }); - - // 获取配置文件列表 - expressApp.get('/api/config-list', (req, res) => { - try { - if (!fs.existsSync(DIRS.config)) { - return res.json({ success: true, configs: [] }); - } - const files = fs.readdirSync(DIRS.config) - .filter(file => file.endsWith('.json')) - .map(file => { - const filePath = path.join(DIRS.config, file); - const stats = fs.statSync(filePath); - let metadata = null; - try { - metadata = JSON.parse(fs.readFileSync(filePath, 'utf8')).metadata; - } catch { /* ignore */ } - return { - filename: file, - size: stats.size, - createdAt: stats.birthtime.toISOString(), - modifiedAt: stats.mtime.toISOString(), - metadata - }; - }) - .sort((a, b) => new Date(b.modifiedAt) - new Date(a.modifiedAt)); - res.json({ success: true, configs: files }); - } catch (error) { - console.error('Error listing configs:', error); - res.status(500).json({ error: '获取配置列表失败: ' + error.message }); - } - }); - - // 删除配置文件 - expressApp.delete('/api/config/:filename', (req, res) => { - try { - let configPath; - try { - configPath = utils.resolveConfigPath(req.params.filename); - } catch { - return res.status(400).json({ error: '无效的配置文件' }); - } - if (!fs.existsSync(configPath)) { - return res.status(404).json({ error: '配置文件不存在' }); - } - fs.unlinkSync(configPath); - res.json({ success: true, message: '配置文件删除成功' }); - } catch (error) { - console.error('Error deleting config:', error); - res.status(500).json({ error: '删除配置失败: ' + error.message }); - } - }); - - // 下载配置文件 - expressApp.get('/api/download-config/:filename', (req, res) => { - try { - let configPath; - try { - configPath = utils.resolveConfigPath(req.params.filename); - } catch { - return res.status(400).json({ error: '无效的配置文件' }); - } - if (!fs.existsSync(configPath)) { - return res.status(404).json({ error: '配置文件不存在' }); - } - res.download(configPath, req.params.filename); - } catch (error) { - console.error('Error downloading config:', error); - res.status(500).json({ error: '下载配置失败: ' + error.message }); - } - }); - - // 上传书籍文件 - expressApp.post('/api/upload', upload.array('books'), (req, res) => { - try { - if (!req.files || req.files.length === 0) { - return res.status(400).json({ error: '没有选择文件' }); - } - const uploadedFiles = req.files.map(file => ({ - originalName: utils.decodeFilename(file.originalname), - savedName: file.filename, - size: file.size - })); - res.json({ - success: true, - message: `成功上传 ${uploadedFiles.length} 个文件`, - files: uploadedFiles - }); - } catch (error) { - console.error('Error uploading files:', error); - res.status(500).json({ error: '文件上传失败: ' + error.message }); - } - }); - - // 获取书籍内容 - expressApp.get('/api/book', (req, res) => { - const bookPath = req.query.path; - if (!bookPath) { - return res.status(400).send('Book path is required.'); - } - try { - const safePath = utils.resolveBookPath(bookPath); - if (fs.existsSync(safePath)) { - res.sendFile(safePath); - } else { - res.status(404).send('Book not found.'); - } - } catch (error) { - return res.status(403).send('Forbidden.'); - } - }); - - // 删除书籍 - expressApp.delete('/api/book', (req, res) => { - try { - const relPath = req.query.path; - if (!relPath) { - return res.status(400).json({ error: '缺少书籍路径' }); - } - const absolutePath = utils.resolveBookPath(relPath); - if (!fs.existsSync(absolutePath)) { - return res.status(404).json({ error: '书籍不存在' }); - } - fs.unlinkSync(absolutePath); - utils.cleanupEmptyFolders(absolutePath); - coverCache.delete(absolutePath); - res.json({ success: true }); - } catch (error) { - console.error('Error deleting book:', error); - res.status(500).json({ error: '删除书籍失败: ' + error.message }); - } - }); - - // ==================== 字体管理 API ==================== - - // 获取字体列表 - expressApp.get('/api/fonts', (req, res) => { - try { - if (!fs.existsSync(DIRS.fonts)) { - return res.json([]); - } - const files = fs.readdirSync(DIRS.fonts); - const fonts = files - .filter(file => ALLOWED_FONT_EXTENSIONS.includes(path.extname(file).toLowerCase())) - .map(file => { - const ext = path.extname(file); - const nameWithoutExt = path.basename(file, ext); - const stats = fs.statSync(path.join(DIRS.fonts, file)); - return { - id: file, - name: nameWithoutExt, - fontFamily: `CustomFont_${nameWithoutExt.replace(/[^a-zA-Z0-9]/g, '_')}`, - filename: file, - size: stats.size, - addedAt: stats.birthtimeMs || stats.ctimeMs - }; - }); - res.json(fonts); - } catch (error) { - console.error('Error getting fonts:', error); - res.status(500).json({ error: '获取字体列表失败' }); - } - }); - - // 上传字体 - expressApp.post('/api/fonts/upload', fontUpload.single('font'), (req, res) => { - try { - if (!req.file) { - return res.status(400).json({ error: '没有上传文件' }); - } - const ext = path.extname(req.file.filename); - const nameWithoutExt = path.basename(req.file.filename, ext); - res.json({ - success: true, - font: { - id: req.file.filename, - name: nameWithoutExt, - fontFamily: `CustomFont_${nameWithoutExt.replace(/[^a-zA-Z0-9]/g, '_')}`, - filename: req.file.filename, - size: req.file.size - } - }); - } catch (error) { - console.error('Error uploading font:', error); - res.status(500).json({ error: '上传字体失败: ' + error.message }); - } - }); - - // 获取字体文件 - expressApp.get('/api/fonts/file/:fontId', (req, res) => { - try { - const fontId = req.params.fontId; - const fontPath = path.join(DIRS.fonts, fontId); - const resolved = path.resolve(fontPath); - if (!resolved.startsWith(path.resolve(DIRS.fonts))) { - return res.status(403).json({ error: '无效的字体路径' }); - } - if (!fs.existsSync(fontPath)) { - return res.status(404).json({ error: '字体不存在' }); - } - const ext = path.extname(fontId).toLowerCase(); - const mimeType = FONT_MIME_TYPES[ext] || 'application/octet-stream'; - res.set({ - 'Content-Type': mimeType, - 'Cache-Control': 'public, max-age=31536000' - }); - res.sendFile(fontPath); - } catch (error) { - console.error('Error serving font:', error); - res.status(500).json({ error: '获取字体失败' }); - } - }); - - // 删除字体 - expressApp.delete('/api/fonts/:fontId', (req, res) => { - try { - const fontId = req.params.fontId; - const fontPath = path.join(DIRS.fonts, fontId); - const resolved = path.resolve(fontPath); - if (!resolved.startsWith(path.resolve(DIRS.fonts))) { - return res.status(403).json({ error: '无效的字体路径' }); - } - if (!fs.existsSync(fontPath)) { - return res.status(404).json({ error: '字体不存在' }); - } - fs.unlinkSync(fontPath); - res.json({ success: true }); - } catch (error) { - console.error('Error deleting font:', error); - res.status(500).json({ error: '删除字体失败: ' + error.message }); - } - }); - - // 启动服务器 + // 启动服务器(仅绑定本地回环地址) server = expressApp.listen(PORT, '127.0.0.1', () => { console.log(`Electron embedded server running at http://localhost:${PORT}`); resolve(); @@ -694,9 +129,7 @@ function startServer() { // 应用准备完成 app.whenReady().then(async () => { try { - // 先启动服务器 await startServer(); - // 再创建窗口 createWindow(); } catch (error) { console.error('Failed to start server:', error); @@ -736,7 +169,8 @@ app.on('quit', () => { } }); -// IPC 通信处理 +// ==================== IPC 通信处理 ==================== + ipcMain.handle('get-app-version', () => { return app.getVersion(); }); @@ -753,3 +187,26 @@ ipcMain.handle('open-books-folder', () => { ipcMain.handle('open-external-link', (event, url) => { shell.openExternal(url); }); + +// 窗口控制 IPC(自定义标题栏) +ipcMain.handle('window-minimize', () => { + if (mainWindow) mainWindow.minimize(); +}); + +ipcMain.handle('window-maximize-toggle', () => { + if (!mainWindow) return false; + if (mainWindow.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow.maximize(); + } + return mainWindow.isMaximized(); +}); + +ipcMain.handle('window-close', () => { + if (mainWindow) mainWindow.close(); +}); + +ipcMain.handle('window-is-maximized', () => { + return mainWindow ? mainWindow.isMaximized() : false; +}); diff --git a/index.html b/index.html index 7b28672..cce970d 100644 --- a/index.html +++ b/index.html @@ -14,9 +14,45 @@ + + + +