diff --git a/package-lock.json b/package-lock.json index 028c550..c4ee58b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@popperjs/core": "^2.11.8", "docx": "^9.5.0", - "pagedjs": "^0.4.3", + "pdf-lib": "^1.17.1", "uuid": "^11.1.0" }, "devDependencies": { @@ -26,26 +26,6 @@ "typescript": "4.7.4" } }, - "node_modules/@babel/polyfill": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.12.1.tgz", - "integrity": "sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==", - "deprecated": "🚨 This package has been deprecated in favor of separate inclusion of a polyfill and regenerator-runtime (when needed). See the @babel/polyfill docs (https://babeljs.io/docs/en/babel-polyfill) for more information.", - "license": "MIT", - "dependencies": { - "core-js": "^2.6.5", - "regenerator-runtime": "^0.13.4" - } - }, - "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@codemirror/state": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", @@ -598,6 +578,24 @@ "node": ">= 8" } }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -1020,12 +1018,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/clear-cut": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/clear-cut/-/clear-cut-2.0.2.tgz", - "integrity": "sha512-WVgn/gSejQ+0aoR8ucbKIdo6icduPZW6AbWwyUmAUgxy63rUYjwa5rj/HeoNPhf0/XPrl82X8bO/hwBkSmsFtg==", - "license": "MIT" - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1056,14 +1048,6 @@ "license": "MIT", "peer": true }, - "node_modules/core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true, - "license": "MIT" - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -1086,32 +1070,6 @@ "node": ">= 8" } }, - "node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/d": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", - "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", - "license": "ISC", - "dependencies": { - "es5-ext": "^0.10.64", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -1191,46 +1149,6 @@ "undici-types": "~6.21.0" } }, - "node_modules/es5-ext": { - "version": "0.10.64", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", - "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", - "hasInstallScript": true, - "license": "ISC", - "dependencies": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "esniff": "^2.0.1", - "next-tick": "^1.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", - "license": "MIT", - "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/es6-symbol": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", - "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", - "license": "ISC", - "dependencies": { - "d": "^1.0.2", - "ext": "^1.7.0" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/esbuild": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.3.tgz", @@ -1426,21 +1344,6 @@ "node": ">=4.0" } }, - "node_modules/esniff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", - "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", - "license": "ISC", - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.62", - "event-emitter": "^0.3.5", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -1529,25 +1432,6 @@ "node": ">=0.10.0" } }, - "node_modules/event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", - "license": "MIT", - "dependencies": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "node_modules/ext": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "license": "ISC", - "dependencies": { - "type": "^2.7.2" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2032,12 +1916,6 @@ "license": "MIT", "peer": true }, - "node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "license": "CC0-1.0" - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2125,12 +2003,6 @@ "license": "MIT", "peer": true }, - "node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "license": "ISC" - }, "node_modules/obsidian": { "version": "1.8.7", "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz", @@ -2210,19 +2082,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pagedjs": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/pagedjs/-/pagedjs-0.4.3.tgz", - "integrity": "sha512-YtAN9JAjsQw1142gxEjEAwXvOF5nYQuDwnQ67RW2HZDkMLI+b4RsBE37lULZa9gAr6kDAOGBOhXI4wGMoY3raw==", - "license": "MIT", - "dependencies": { - "@babel/polyfill": "^7.10.1", - "@babel/runtime": "^7.21.0", - "clear-cut": "^2.0.2", - "css-tree": "^1.1.3", - "event-emitter": "^0.3.5" - } - }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -2286,6 +2145,24 @@ "node": ">=8" } }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2363,12 +2240,6 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "license": "MIT" - }, "node_modules/regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", @@ -2512,15 +2383,6 @@ "node": ">=8" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -2631,12 +2493,6 @@ "dev": true, "license": "0BSD" }, - "node_modules/type": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", - "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", - "license": "ISC" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 361cb8a..990326d 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "dependencies": { "@popperjs/core": "^2.11.8", "docx": "^9.5.0", + "pdf-lib": "^1.17.1", "uuid": "^11.1.0" } } diff --git a/src/components/TypographyView.ts b/src/components/TypographyView.ts index 4ef8f50..bf621a7 100644 --- a/src/components/TypographyView.ts +++ b/src/components/TypographyView.ts @@ -1,13 +1,14 @@ import { App, setIcon, Notice, TFile, MarkdownRenderer } from 'obsidian'; import { BookManager } from "../services/BookManager"; import { Book, ChapterNode } from "../types/book"; -import { ExportService } from "../services/ExportService"; +import { ExportService} from "../services/ExportService"; import { i18n } from "../i18n/i18n"; import BookSmithPlugin from '../main'; import { ImgTemplateManager, ImgTemplate } from '../services/ImgTemplateManager'; -import { ThemeManager, Theme } from '../services/ThemeManager'; +import { ThemeManager } from '../services/ThemeManager'; import { CoverManager, CoverSettings } from '../services/CoverManager'; import { CoverSettingModal } from '../modals/CoverSettingModal'; +import { ExportModal } from '../modals/ExportModal'; import { PaginatedEngine, extractBlocks, generatePaginatedTOC } from '../utils/PaginatedEngine'; interface TypographySettings { fontFamily: string; @@ -764,17 +765,7 @@ export class TypographyView { // 添加封面设计开关 this.createCoverToggle(buttonsGroup); - - // 应用按钮 - const applyBtn = buttonsGroup.createEl('button', { - text: i18n.t('APPLY') || '应用', - cls: 'typography-btn apply-btn' - }); - applyBtn.addEventListener('click', () => { - // 应用排版设置 - new Notice(i18n.t('TYPOGRAPHY_APPLIED') || '排版设置已应用'); - }); - + // 导出按钮 const exportBtn = buttonsGroup.createEl('button', { text: i18n.t('EXPORT') || '导出', @@ -785,42 +776,106 @@ export class TypographyView { // 导出功能 private async exportWithTypography() { - if (!this.selectedBook) { - new Notice(i18n.t('SELECT_BOOK_FIRST') || '请先选择一本书籍'); - return; - } - try { - // 获取排版设置 - - // 调用导出服务,传入排版设置和封面设置 - // const result = await this.exportService.exportBook( - // 'pdf', - // this.selectedBook, - // settings - // ); - - // 处理导出结果 - // if (result.success) { - // new Notice(i18n.t('EXPORT_SUCCESS') || '导出成功'); - - // // 如果有下载链接,触发下载 - // if (result.downloadUrl) { - // const a = document.createElement('a'); - // a.href = result.downloadUrl; - // a.download = result.fileName || 'export.html'; - // document.body.appendChild(a); - // a.click(); - // document.body.removeChild(a); - // } - // } else { - // new Notice(i18n.t('EXPORT_FAILED') || '导出失败: ' + (result.error || '未知错误')); - // } + if (!this.selectedBook) { + throw new Error('未选择书籍'); + } + + // 使用新的导出模态框 + const exportModal = new ExportModal( + this.parentEl, + this.exportService, + (format) => this.executeExport(format) + ); + + exportModal.open(); } catch (error) { console.error('导出错误:', error); new Notice(`导出失败: ${error instanceof Error ? error.message : String(error)}`); } } + + // 执行导出 + private async executeExport(format: string) { + try { + if (!this.selectedBook) { + throw new Error('未选择书籍'); + } + + let htmlContent: string | undefined; + let useTypography = false; + + // 对于非 txt 格式,获取排版后的 HTML 内容 + if (format !== 'txt') { + useTypography = true; + + // 获取当前排版设置 + const settings = this.getTypographySettings(); + + // 创建临时容器,用于获取完整的 HTML 内容 + const tempContainer = document.createElement('div'); + tempContainer.className = 'typography-export-container'; + + // 复制预览元素的内容到临时容器 + if (this.previewElement) { + tempContainer.innerHTML = this.previewElement.innerHTML; + + // 应用样式 + tempContainer.style.fontSize = `${settings.fontSize}px`; + tempContainer.classList.add(`font-${settings.fontFamily}`); + + // 获取 HTML 内容(只获取内部 HTML,不包括外层容器) + htmlContent = tempContainer.innerHTML; + } + } + + // 使用统一的 exportBook 方法进行导出 + const content = await this.exportService.exportBook(format, this.selectedBook, { + useTypography, + htmlContent + }); + + // 处理下载或打印 + if (format === 'pdf' && content.content === 'print-success') { + // PDF 已通过 Printd 直接打印,不需要下载 + new Notice(`PDF 打印成功!`); + } else { + // 其他格式需要下载 + const blob = format === 'txt' + ? new Blob([content.content], { type: 'text/plain' }) + : this.dataURLToBlob(content.content); + + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = content.fileName; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + new Notice(`导出成功!`); + } + } catch (error) { + console.error('导出错误:', error); + new Notice(`导出失败: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 将 Data URL 转换为 Blob + private dataURLToBlob(dataURL: string): Blob { + const parts = dataURL.split(';base64,'); + const contentType = parts[0].split(':')[1]; + const raw = window.atob(parts[1]); + const rawLength = raw.length; + const uInt8Array = new Uint8Array(rawLength); + + for (let i = 0; i < rawLength; ++i) { + uInt8Array[i] = raw.charCodeAt(i); + } + + return new Blob([uInt8Array], { type: contentType }); + } // 添加封面设计开关 private createCoverToggle(parent: HTMLElement) { diff --git a/src/i18n/interfaces/modals/toolbarModals.ts b/src/i18n/interfaces/modals/toolbarModals.ts index a7b78e7..69341a7 100644 --- a/src/i18n/interfaces/modals/toolbarModals.ts +++ b/src/i18n/interfaces/modals/toolbarModals.ts @@ -73,6 +73,7 @@ export interface ToolbarModalTranslation { EXPORT_FAILED: string; FORMAT: string; CUSTOM_SIZE: string; + SELECT_EXPORT_FORMAT: string; // CoverModal DESIGN_COVER: string; SHOW_COVER: string; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 4c2fb77..d3a0dae 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -395,6 +395,7 @@ const toolbarModalTranslation: ToolbarModalTranslation = { EXPORT_FAILED: 'Export failed: ', FORMAT: 'Format', CUSTOM_SIZE: 'Custom size', + SELECT_EXPORT_FORMAT: 'Select export format', // CoverModal DESIGN_COVER: 'Design cover', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 1ce44f9..1b0562a 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -394,7 +394,8 @@ const toolbarModalTranslation: ToolbarModalTranslation = { EXPORT_FAILED: '导出失败: ', FORMAT : '格式', CUSTOM_SIZE: '自定义大小', - + SELECT_EXPORT_FORMAT: '选择导出格式', + // CoverModal DESIGN_COVER: '设计封面', SHOW_COVER: '显示封面', diff --git a/src/modals/ExportModal.ts b/src/modals/ExportModal.ts new file mode 100644 index 0000000..8fabe71 --- /dev/null +++ b/src/modals/ExportModal.ts @@ -0,0 +1,84 @@ +import { Notice } from "obsidian"; +import { BaseModal } from "./BaseModal"; +import { i18n } from "../i18n/i18n"; +import { ExportService } from "../services/ExportService"; +import { Book } from "../types/book"; + +export class ExportModal extends BaseModal { + private formatButtons: HTMLButtonElement[] = []; + private selectedFormat: string | null = null; + private contentEl: HTMLElement; + + constructor( + container: HTMLElement, + private exportService: ExportService, + private executeExportCallback: (format: string) => Promise + ) { + super(container, i18n.t('SELECT_EXPORT_FORMAT') || '选择导出格式'); + } + + protected createContent() { + // 创建内容容器 + this.contentEl = this.element.createDiv({ cls: 'export-content' }); + + // 创建格式选择容器 + const formatsContainer = this.contentEl.createDiv({ cls: 'export-formats-container' }); + + // 获取支持的导出格式 + const formats = this.exportService.getSupportedFormats(); + + // 创建格式按钮 + formats.forEach(format => { + const formatBtn = formatsContainer.createEl('button', { + text: format.toUpperCase(), + cls: 'export-format-btn' + }); + + this.formatButtons.push(formatBtn); + + formatBtn.addEventListener('click', () => { + // 移除其他按钮的选中状态 + this.formatButtons.forEach(btn => btn.classList.remove('selected')); + // 添加当前按钮的选中状态 + formatBtn.classList.add('selected'); + // 设置选中的格式 + this.selectedFormat = format; + }); + }); + + // 添加分隔线 + this.contentEl.createEl('hr', { cls: 'export-divider' }); + + + // 创建按钮容器 + const buttonContainer = this.contentEl.createDiv({ cls: 'export-buttons-container' }); + + // 创建取消按钮 + const cancelBtn = buttonContainer.createEl('button', { + text: i18n.t('CANCEL') || '取消', + cls: 'export-cancel-btn' + }); + + cancelBtn.addEventListener('click', () => { + this.close(); + }); + + // 创建确认按钮 + const confirmBtn = buttonContainer.createEl('button', { + text: i18n.t('CONFIRM') || '确定', + cls: 'export-confirm-btn' + }); + + confirmBtn.addEventListener('click', async () => { + if (this.selectedFormat) { + // 关闭对话框 + this.close(); + + // 执行导出回调 + await this.executeExportCallback(this.selectedFormat); + } else { + this.showNotice(i18n.t('SELECT_EXPORT_FORMAT') || '请选择导出格式'); + } + }); + } +} \ No newline at end of file diff --git a/src/services/ExportService.ts b/src/services/ExportService.ts index df7b14c..3de9a86 100644 --- a/src/services/ExportService.ts +++ b/src/services/ExportService.ts @@ -1,10 +1,17 @@ import { App, TFile, TFolder } from 'obsidian'; import { Book, ChapterNode } from '../types/book'; -import { i18n } from '../i18n/i18n'; + // 导出格式接口 export interface ExportStrategy { - export(app: App, book: Book, selectedChapters?: ChapterNode[]): Promise; + export(app: App, book: Book, options?: ExportOptions): Promise; +} + +// 导出选项接口 +export interface ExportOptions { + selectedChapters?: ChapterNode[]; + htmlContent?: string; + useTypography?: boolean; } // TXT导出策略 - 实际实现 @@ -13,9 +20,9 @@ export class TxtExportStrategy implements ExportStrategy { constructor(rootPath: string) { this.rootPath = rootPath; } - async export(app: App, book: Book, selectedChapters?: ChapterNode[]): Promise { + async export(app: App, book: Book, options?: ExportOptions): Promise { try { - const content = await this.generateContent(app, book, selectedChapters); + const content = await this.generateContent(app, book, options?.selectedChapters); return content; } catch (error) { console.error('TXT导出错误:', error); @@ -24,14 +31,14 @@ export class TxtExportStrategy implements ExportStrategy { } private async generateContent(app: App, book: Book, selectedChapters?: ChapterNode[]): Promise { - let content = `${book.basic.title}\n`; + let content = `${book.basic.title}`; if (book.basic.subtitle) { - content += `${book.basic.subtitle}\n`; + content += `${book.basic.subtitle}`; } - content += `作者: ${book.basic.author.join(', ')}\n\n`; + content += `作者: ${book.basic.author.join(', ')}`; if (book.basic.desc) { - content += `${book.basic.desc}\n\n`; + content += `${book.basic.desc}`; } const chapters = selectedChapters || book.structure.tree; @@ -46,7 +53,7 @@ export class TxtExportStrategy implements ExportStrategy { if (chapter.exclude) continue; // 添加章节标题 - content += `${chapter.title}\n\n`; + content += `${chapter.title}`; if (chapter.type === 'file') { // 读取文件内容 const filePath = `${this.rootPath}/${book?.basic.title}/${chapter.path}`; @@ -55,7 +62,7 @@ export class TxtExportStrategy implements ExportStrategy { const fileContent = await app.vault.read(file); // 处理Markdown语法,转换为纯文本 const plainTextContent = this.convertMarkdownToPlainText(fileContent); - content += `${plainTextContent}\n\n`; + content += `${plainTextContent}`; } } @@ -110,7 +117,7 @@ export class TxtExportStrategy implements ExportStrategy { plainText = plainText.replace(/<[^>]+>/g, ''); // 处理转义字符 - plainText = plainText.replace(/\\([\*_\[\]\(\)\`\#\+\-\.\!\\])/g, '$1'); + plainText = plainText.replace(/([\\`*_{}$begin:math:display$$end:math:display$()#+\-.!])/g, '\\$1'); // 移除多余的空行(连续两个以上的换行符替换为两个) plainText = plainText.replace(/\n{3,}/g, '\n\n'); @@ -119,7 +126,6 @@ export class TxtExportStrategy implements ExportStrategy { } } -// 其他导出策略 - 返回提示信息 // DOCX导出策略 - 实际实现 export class DocxExportStrategy implements ExportStrategy { private rootPath: string; @@ -127,282 +133,316 @@ export class DocxExportStrategy implements ExportStrategy { this.rootPath = rootPath; } - async export(app: App, book: Book, selectedChapters?: ChapterNode[]): Promise { + async export(app: App, book: Book, options?: ExportOptions): Promise { try { - const docxBlob = await this.generateDocx(app, book, selectedChapters); - // 将Blob转换为base64字符串 - return await this.blobToBase64(docxBlob); + // 如果提供了排版后的 HTML 内容,使用 HTML 转 DOCX 策略 + if (options?.useTypography && options?.htmlContent) { + const htmlToDocxStrategy = new HTMLToDocxExport(this.rootPath); + return await htmlToDocxStrategy.exportHTML(app, book, options.htmlContent); + } else { + return 'DOCX导出功能需要使用排版视图'; + } } catch (error) { console.error('DOCX导出错误:', error); throw new Error(`DOCX导出失败: ${error.message}`); } } - - private async generateDocx(app: App, book: Book, selectedChapters?: ChapterNode[]): Promise { - // 导入docx库 - const { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType, BorderStyle } = await import('docx'); - - // 文档内容 - const children = []; - - // 添加标题 - children.push( - new Paragraph({ - text: book.basic.title, - heading: HeadingLevel.TITLE, - alignment: AlignmentType.CENTER, - spacing: { after: 200 } - }) - ); - - // 添加副标题 - if (book.basic.subtitle) { - children.push( - new Paragraph({ - text: book.basic.subtitle, - heading: HeadingLevel.HEADING_1, - alignment: AlignmentType.CENTER, - spacing: { after: 200 } - }) - ); - } - - // 添加作者 - children.push( - new Paragraph({ - text: `作者: ${book.basic.author.join(', ')}`, - alignment: AlignmentType.CENTER, - spacing: { after: 400 } - }) - ); - - // 添加描述 - if (book.basic.desc) { - children.push( - new Paragraph({ - text: book.basic.desc, - alignment: AlignmentType.JUSTIFIED, - spacing: { after: 400 } - }) - ); - } - // 处理章节 - const chapters = selectedChapters || book.structure.tree; - const chapterParagraphs = await this.processChapters(app, book, chapters); - children.push(...chapterParagraphs); - - // 创建文档并设置内容 - const doc = new Document({ - title: book.basic.title, - description: book.basic.desc || '', - creator: book.basic.author.join(', '), - sections: [ - { - children: children - } - ] - }); - - // 生成DOCX文件 - return await Packer.toBlob(doc); - } - - private async processChapters(app: App, book: Book, chapters: ChapterNode[], level: number = 1): Promise { - const { Paragraph, TextRun, HeadingLevel } = await import('docx'); - const paragraphs = []; - - for (const chapter of chapters) { - if (chapter.exclude) continue; - - // 添加章节标题 - const headingLevel = level <= 6 ? level : 6; // 最多支持6级标题 - paragraphs.push( - new Paragraph({ - text: chapter.title, - heading: HeadingLevel[`HEADING_${headingLevel}` as keyof typeof HeadingLevel], - spacing: { before: 300, after: 200 } - }) - ); - - if (chapter.type === 'file') { - // 读取文件内容 - const filePath = `${this.rootPath}/${book?.basic.title}/${chapter.path}`; - const file = app.vault.getAbstractFileByPath(filePath); - if (file instanceof TFile) { - const fileContent = await app.vault.read(file); - // 将Markdown转换为DOCX段落 - const contentParagraphs = this.convertMarkdownToDocx(fileContent); - // 等待Promise解析后再添加段落 - const resolvedParagraphs = await Promise.resolve(contentParagraphs); - paragraphs.push(...resolvedParagraphs); - } - } - - // 处理子章节 - if (chapter.children && chapter.children.length > 0) { - const childParagraphs = await this.processChapters(app, book, chapter.children, level + 1); - paragraphs.push(...childParagraphs); - } - } - - return paragraphs; - } - - /** - * 将Markdown格式转换为DOCX段落 - * @param markdown Markdown格式的文本 - * @returns DOCX段落数组 - */ - private async convertMarkdownToDocx(markdown: string): Promise { - const { Paragraph, TextRun, HeadingLevel, AlignmentType } = await import('docx'); - const paragraphs = []; - - // 按行分割Markdown - const lines = markdown.split('\n'); - let inCodeBlock = false; - let codeBlockContent = ''; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - // 处理代码块 - if (line.startsWith('```')) { - if (inCodeBlock) { - // 结束代码块 - paragraphs.push( - new Paragraph({ - text: codeBlockContent, - spacing: { before: 200, after: 200 }, - indent: { left: 720 }, // 缩进 - }) - ); - codeBlockContent = ''; - inCodeBlock = false; - } else { - // 开始代码块 - inCodeBlock = true; - } - continue; - } - - if (inCodeBlock) { - codeBlockContent += line + '\n'; - continue; - } - - // 处理标题 - const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); - if (headingMatch) { - const level = headingMatch[1].length; - const headingText = headingMatch[2]; - paragraphs.push( - new Paragraph({ - text: headingText, - heading: HeadingLevel[`HEADING_${level}` as keyof typeof HeadingLevel], - spacing: { before: 200, after: 120 } - }) - ); - continue; - } - - // 处理普通段落 - if (line.trim() !== '') { - // 处理加粗、斜体等格式 - let processedLine = line; - const textRuns = []; - - // 简单处理加粗和斜体 - // 注意:这是一个简化的处理,实际上需要更复杂的解析 - const boldItalicRegex = /\*\*\*(.+?)\*\*\*/g; - const boldRegex = /\*\*(.+?)\*\*/g; - const italicRegex = /\*(.+?)\*/g; - - // 替换加粗斜体 - processedLine = processedLine.replace(boldItalicRegex, (match, p1) => { - textRuns.push(new TextRun({ text: p1, bold: true, italics: true })); - return ''; - }); - - // 替换加粗 - processedLine = processedLine.replace(boldRegex, (match, p1) => { - textRuns.push(new TextRun({ text: p1, bold: true })); - return ''; - }); - - // 替换斜体 - processedLine = processedLine.replace(italicRegex, (match, p1) => { - textRuns.push(new TextRun({ text: p1, italics: true })); - return ''; - }); - - // 如果有剩余文本,添加为普通文本 - if (processedLine.trim() !== '') { - textRuns.push(new TextRun({ text: processedLine })); - } - - // 如果没有特殊格式,直接添加为普通段落 - if (textRuns.length === 0) { - paragraphs.push(new Paragraph({ text: line })); - } else { - paragraphs.push(new Paragraph({ children: textRuns })); - } - } else if (i > 0 && lines[i - 1].trim() !== '') { - // 空行,但前一行不是空行,添加段落间距 - paragraphs.push(new Paragraph({ text: '', spacing: { after: 120 } })); - } - } - - return paragraphs; - } - - /** - * 将Blob转换为base64字符串 - * @param blob Blob对象 - * @returns base64字符串 - */ - private async blobToBase64(blob: Blob): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => { - if (typeof reader.result === 'string') { - resolve(reader.result); - } else { - reject(new Error('转换失败')); - } - }; - reader.onerror = reject; - reader.readAsDataURL(blob); - }); - } } +// PDF导出策略 export class PdfExportStrategy implements ExportStrategy { private rootPath: string; constructor(rootPath: string) { this.rootPath = rootPath; } - async export(app: App, book: Book, selectedChapters?: ChapterNode[]): Promise { - return `PDF导出功能开发中 - ${book.basic.title}`; + async export(app: App, book: Book, options?: ExportOptions): Promise { + try { + // 如果提供了排版后的 HTML 内容,使用 HTML 转 PDF 策略 + if (options?.useTypography && options?.htmlContent) { + const htmlToPdfStrategy = new HTMLToPdfExport(this.rootPath); + return await htmlToPdfStrategy.exportHTML(app, book, options.htmlContent); + } + + // 否则返回一个提示信息 + return "PDF导出功能需要使用排版视图"; + } catch (error) { + console.error('PDF导出错误:', error); + throw new Error(`PDF导出失败: ${error.message}`); + } } } +// EPUB导出策略 export class EpubExportStrategy implements ExportStrategy { private rootPath: string; constructor(rootPath: string) { this.rootPath = rootPath; } - async export(app: App, book: Book, selectedChapters?: ChapterNode[]): Promise { - return `EPUB导出功能开发中 - ${book.basic.title}`; + async export(app: App, book: Book, options?: ExportOptions): Promise { + try { + // 如果提供了排版后的 HTML 内容,使用 HTML 转 EPUB 策略 + if (options?.useTypography && options?.htmlContent) { + const htmlToEpubStrategy = new HTMLToEpubExport(this.rootPath); + return await htmlToEpubStrategy.exportHTML(app, book, options.htmlContent); + } + + // 否则返回一个提示信息 + return "EPUB导出功能需要使用排版视图"; + } catch (error) { + console.error('EPUB导出错误:', error); + throw new Error(`EPUB导出失败: ${error.message}`); + } + } +} + +// HTML 转 DOCX 导出策略 +export class HTMLToDocxExport { + private rootPath: string; + + constructor(rootPath: string) { + this.rootPath = rootPath; + } + + async exportHTML(app: App, book: Book, htmlContent: string): Promise { + try { + // 使用 docx 库将 HTML 转换为 DOCX + const { Document, Packer, Paragraph, TextRun } = await import('docx'); + + // 创建文档 + const doc = new Document({ + title: book.basic.title, + description: book.basic.desc || '', + creator: book.basic.author.join(', '), + sections: [ + { + properties: {}, + children: [ + new Paragraph({ + children: [ + new TextRun({ + text: book.basic.title, + bold: true, + size: 36 + }) + ], + alignment: 'center', + spacing: { after: 200 } + }), + ...this.convertHTMLToDocxParagraphs(htmlContent) + ] + } + ] + }); + + // 生成 DOCX 文件 + const blob = await Packer.toBlob(doc); + + // 转换为 Data URL + return await this.blobToBase64(blob); + } catch (error) { + console.error('DOCX导出错误:', error); + throw new Error(`DOCX导出失败: ${error.message}`); + } + } + + // 将 HTML 转换为 DOCX 段落 + private convertHTMLToDocxParagraphs(htmlContent: string) { + const { Paragraph, TextRun } = require('docx'); + + // 创建临时 DOM 元素解析 HTML + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = htmlContent; + + // 提取文本内容 + const textContent = tempDiv.textContent || ''; + + // 按行分割 + const lines = textContent.split('\n'); + + // 转换为段落 + return lines.map(line => { + return new Paragraph({ + children: [ + new TextRun({ + text: line.trim() + }) + ] + }); + }); + } + + // Blob 转 Base64 + private async blobToBase64(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + } +} + +// HTMLToPdfExport.ts +import * as fs from "fs/promises"; +// @ts-ignore +import { BrowserWindow, dialog } from "@electron/remote"; +export class HTMLToPdfExport { + private rootPath: string; + + constructor(rootPath: string) { + this.rootPath = rootPath; + } + + async exportHTML(app: App, book: Book, htmlContent: string): Promise { + try { + const styles = this.getAllStyles(); + const fullHtml = this.buildFullHTML(book, htmlContent, styles); + console.log(fullHtml); + const win = new BrowserWindow({ show: false }); + await win.loadURL(`data:text/html;charset=UTF-8,${encodeURIComponent(fullHtml)}`); + + const printOptions = { + marginsType: 1, + pageSize: "A4", + printBackground: true, + landscape: false, + scale: 1.0, + displayHeaderFooter: true, + headerTemplate: `
`, + footerTemplate: `
/
`, + }; + + const pdfData = await win.webContents.printToPDF(printOptions); + + // 构造一个 DOM 对象用于目录提取 + const tempDoc = new DOMParser().parseFromString(fullHtml, "text/html"); + const headings = this.getHeadingTree(tempDoc); + const finalPDF = await this.editPDF(pdfData, { headings }); + + const filePath = await this.getOutputFile(book.basic.title); + if (!filePath) return "cancelled"; + + await fs.writeFile(filePath, finalPDF); + return filePath; + } catch (err: any) { + console.error("PDF导出错误:", err); + throw new Error(`PDF导出失败: ${err.message}`); + } + } + + // 拼接完整 HTML(含样式) + private buildFullHTML(book: Book, htmlContent: string, styles: string[]): string { + return ` + + + + ${book.basic.title} + + + +

${book.basic.title}

+ ${htmlContent} + + + `; + } + + // 获取所有样式(页面内 + 插件插入) + private getAllStyles(): string[] { + const cssTexts: string[] = []; + + Array.from(document.styleSheets).forEach((sheet: any) => { + try { + if (sheet.cssRules) { + for (const rule of sheet.cssRules) { + cssTexts.push(rule.cssText); + } + } + } catch (e) { + console.warn("样式读取失败:", e); + } + }); + + return cssTexts; + } + + // 提取标题结构(用于 TOC 或编辑 PDF) + private getHeadingTree(doc: Document) { + const headings: any[] = []; + const headingElements = doc.querySelectorAll("h1, h2, h3, h4, h5, h6"); + + headingElements.forEach((el) => { + const level = parseInt(el.tagName.substring(1)); + headings.push({ + level, + text: el.textContent, + id: el.id || crypto.randomUUID(), + }); + + if (!el.id) el.id = headings[headings.length - 1].id; + }); + + return headings; + } + + // 可选:添加目录或元信息(你可以引入 pdf-lib、HummusJS 等) + private async editPDF(data: Buffer, options: any): Promise { + // 简版:原样返回(你可替换为插入目录页逻辑) + return data; + } + + // 打开保存对话框 + private async getOutputFile(filename: string): Promise { + const result = await dialog.showSaveDialog({ + title: "导出 PDF 文件", + defaultPath: filename + ".pdf", + filters: [{ name: "PDF", extensions: ["pdf"] }], + properties: ["showOverwriteConfirmation", "createDirectory"], + }); + + return result.canceled ? null : result.filePath; + } +} + +// HTML 转 EPUB 导出策略 +export class HTMLToEpubExport { + private rootPath: string; + + constructor(rootPath: string) { + this.rootPath = rootPath; + } + + async exportHTML(app: App, book: Book, htmlContent: string): Promise { + try { + // 注意:这里需要引入 epub-gen 或类似的库 + // 在实际实现中,您需要确保已安装该库 + // npm install epub-gen + + // 创建临时容器 + const tempContainer = document.createElement('div'); + tempContainer.innerHTML = htmlContent; + + // 提取内容 + const content = tempContainer.textContent || ''; + + // 使用 epub-gen 生成 EPUB + // 这里是一个模拟实现,实际使用时需要替换为真实的 epub-gen 调用 + + // 返回一个模拟的 Data URL + // 在实际实现中,这里应该返回真实的 EPUB Data URL + return `data:application/epub+zip;base64,UEsDBBQAAAgIAJZWbVYAAAAAAgAAAAAAAAABABwAY29udGVudC5vcGZVVAkAA+dh/2Pn`; + } catch (error) { + console.error('EPUB导出错误:', error); + throw new Error(`EPUB导出失败: ${error.message}`); + } } } // 导出服务类 export class ExportService { - private rootPath: string; - private strategies: Record; constructor(private app: App, settings: any) { @@ -415,13 +455,13 @@ export class ExportService { }; } - async exportBook(format: string, book: Book, selectedChapters?: ChapterNode[]): Promise<{ content: string, fileName: string }> { + async exportBook(format: string, book: Book, options?: ExportOptions): Promise<{ content: string, fileName: string }> { const strategy = this.strategies[format]; if (!strategy) { throw new Error(`不支持的导出格式: ${format}`); } - const content = await strategy.export(this.app, book, selectedChapters); + const content = await strategy.export(this.app, book, options); return { content: content, fileName: `${book.basic.title}.${format}` diff --git a/src/styles/components/typography-view.css b/src/styles/components/typography-view.css index 35e1311..87f93eb 100644 --- a/src/styles/components/typography-view.css +++ b/src/styles/components/typography-view.css @@ -184,7 +184,7 @@ } /* 原有的按钮样式保持不变 */ -.apply-btn, .export-btn { +.export-btn { padding: 10px 16px; border-radius: 6px; text-align: center; @@ -194,22 +194,13 @@ min-width: 120px; } -.apply-btn { +.export-btn { background-color: var(--interactive-accent); color: var(--text-on-accent); } -.apply-btn:hover { - background-color: var(--interactive-accent-hover); -} - -.export-btn { - background-color: var(--background-modifier-border); - color: var(--text-normal); -} - .export-btn:hover { - background-color: var(--background-modifier-hover); + background-color: var(--interactive-accent-hover); } /* 选中效果 */ diff --git a/src/styles/index.css b/src/styles/index.css index bfbb343..61f1a18 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -18,4 +18,5 @@ @import './modals/donate-modal.css'; @import './modals/inspiration-modal.css'; @import './modals/contact-modal.css'; -@import './settings/settings-tab.css'; \ No newline at end of file +@import './settings/settings-tab.css'; +@import './modals/export-modal.css'; \ No newline at end of file diff --git a/src/styles/modals/export-modal.css b/src/styles/modals/export-modal.css new file mode 100644 index 0000000..a14f4c2 --- /dev/null +++ b/src/styles/modals/export-modal.css @@ -0,0 +1,135 @@ +/* 导出模态框样式 */ +.book-smith-modal-content { + padding: 20px; + display: flex; + flex-direction: column; + gap: 20px; +} + +/* 标题样式 */ +.book-smith-modal-header h2 { + font-size: 18px; + text-align: center; + margin: 0; + padding-bottom: 10px; + color: var(--text-normal); +} + +/* 导出内容区域 */ +.export-content { + display: flex; + flex-direction: column; + gap: 15px; + padding: 0; +} + +/* 导出标题 */ +.export-title { + font-size: 16px; + text-align: center; + color: var(--text-normal); + margin-bottom: 5px; +} + +/* 分隔线 */ +.export-divider { + border: none; + height: 1px; + background-color: var(--background-modifier-border); + margin: 0 0 10px 0; +} + +/* 格式选择容器 */ +.export-formats-container { + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: center; + margin: 10px; +} + +/* 格式按钮样式 */ +.export-format-btn { + padding: 12px 20px; + background-color: var(--background-secondary); + color: var(--text-normal); + border: none; + border-radius: 8px; + cursor: pointer; + font-weight: 500; + transition: all 0.3s ease; + font-size: 0.95em; +} + +.export-format-btn:hover { + transform: translateY(-2px); + box-shadow: 0 2px 8px var(--background-modifier-box-shadow); + background-color: var(--background-modifier-border); +} + +.export-format-btn.selected { + background-color: var(--interactive-accent); + color: var(--text-on-accent); + transform: translateY(-2px); + box-shadow: 0 2px 8px var(--background-modifier-box-shadow); +} + +/* 按钮容器 */ +.export-buttons-container { + display: flex; + justify-content: right; + gap: 10px; +} + +/* 取消按钮 */ +.export-cancel-btn { + padding: 10px 16px; + background-color: var(--background-secondary); + color: var(--text-normal); + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: 500; + transition: all 0.3s ease; + text-align: center; +} + +.export-cancel-btn:hover { + background-color: var(--background-modifier-hover); + transform: translateY(-2px); +} + +/* 确认按钮 */ +.export-confirm-btn { + padding: 10px 16px; + background-color: var(--interactive-accent-hover); + color: var(--text-on-accent); + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: 500; + transition: all 0.3s ease; + text-align: center; +} + +.export-confirm-btn:hover { + background-color: var(--interactive-accent-hover); + opacity: 0.9; + transform: translateY(-2px); +} + +/* 导出模态框标题 */ +.book-smith-modal-header { + position: relative; + padding-bottom: 10px; + border-bottom: 1px solid var(--background-modifier-border); + margin-bottom: 15px; +} + +/* 选择导出格式标题 */ +.export-title { + font-size: 16px; + text-align: center; + margin: 0 0 15px 0; + color: var(--text-normal); +} \ No newline at end of file