mirror of
https://github.com/yeban8090/book-smith.git
synced 2026-07-22 05:46:23 +00:00
初步提交保存
This commit is contained in:
parent
9b5af8a672
commit
9d4cbf0f68
11 changed files with 665 additions and 499 deletions
218
package-lock.json
generated
218
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"docx": "^9.5.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"uuid": "^11.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -394,7 +394,8 @@ const toolbarModalTranslation: ToolbarModalTranslation = {
|
|||
EXPORT_FAILED: '导出失败: ',
|
||||
FORMAT : '格式',
|
||||
CUSTOM_SIZE: '自定义大小',
|
||||
|
||||
SELECT_EXPORT_FORMAT: '选择导出格式',
|
||||
|
||||
// CoverModal
|
||||
DESIGN_COVER: '设计封面',
|
||||
SHOW_COVER: '显示封面',
|
||||
|
|
|
|||
84
src/modals/ExportModal.ts
Normal file
84
src/modals/ExportModal.ts
Normal file
|
|
@ -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<void>
|
||||
) {
|
||||
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') || '请选择导出格式');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string>;
|
||||
export(app: App, book: Book, options?: ExportOptions): Promise<string>;
|
||||
}
|
||||
|
||||
// 导出选项接口
|
||||
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<string> {
|
||||
async export(app: App, book: Book, options?: ExportOptions): Promise<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
async export(app: App, book: Book, options?: ExportOptions): Promise<string> {
|
||||
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<Blob> {
|
||||
// 导入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<any[]> {
|
||||
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<any[]> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
return `PDF导出功能开发中 - ${book.basic.title}`;
|
||||
async export(app: App, book: Book, options?: ExportOptions): Promise<string> {
|
||||
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<string> {
|
||||
return `EPUB导出功能开发中 - ${book.basic.title}`;
|
||||
async export(app: App, book: Book, options?: ExportOptions): Promise<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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: `<div style="font-size:10px;text-align:center;width:100vw;"><span class="title"></span></div>`,
|
||||
footerTemplate: `<div style="font-size:10px;text-align:center;width:100vw;"><span class="pageNumber"></span> / <span class="totalPages"></span></div>`,
|
||||
};
|
||||
|
||||
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 `
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>${book.basic.title}</title>
|
||||
<style>${styles.join("\n")}</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="book-title" class="__title__">${book.basic.title}</h1>
|
||||
${htmlContent}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
// 获取所有样式(页面内 + 插件插入)
|
||||
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<Buffer> {
|
||||
// 简版:原样返回(你可替换为插入目录页逻辑)
|
||||
return data;
|
||||
}
|
||||
|
||||
// 打开保存对话框
|
||||
private async getOutputFile(filename: string): Promise<string | null> {
|
||||
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<string> {
|
||||
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<string, ExportStrategy>;
|
||||
|
||||
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}`
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/* 选中效果 */
|
||||
|
|
|
|||
|
|
@ -18,4 +18,5 @@
|
|||
@import './modals/donate-modal.css';
|
||||
@import './modals/inspiration-modal.css';
|
||||
@import './modals/contact-modal.css';
|
||||
@import './settings/settings-tab.css';
|
||||
@import './settings/settings-tab.css';
|
||||
@import './modals/export-modal.css';
|
||||
135
src/styles/modals/export-modal.css
Normal file
135
src/styles/modals/export-modal.css
Normal file
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in a new issue