From af7250bd5a701c137951a09dfb0388b33d8a8181 Mon Sep 17 00:00:00 2001 From: Yeban8090 Date: Tue, 10 Jun 2025 03:46:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E6=8E=92=E7=89=88?= =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- manifest.json | 2 +- package-lock.json | 181 ++++ .../EbookModal.ts => components/EbookView.ts} | 121 ++- src/components/TypographyView.ts | 939 ++++++++++++++++++ src/i18n/interfaces/modals/toolbarModals.ts | 29 + src/i18n/locales/en.ts | 31 +- src/i18n/locales/zh-CN.ts | 31 +- src/main.ts | 8 + src/modals/CoverSettingModal.ts | 240 +++++ src/services/CoverManager.ts | 95 ++ src/services/ImgTemplateManager.ts | 145 +++ src/services/ThemeManager.ts | 322 ++++++ src/styles/components/cover-design.css | 122 +++ src/styles/components/ebook-view.css | 108 ++ src/styles/components/paginated-engine.css | 158 +++ src/styles/components/typography-view.css | 612 ++++++++++++ src/styles/index.css | 6 +- src/styles/modals/ebook-modal.css | 90 -- src/utils/PaginatedEngine.ts | 474 +++++++++ src/views/ToolsView.ts | 82 +- 20 files changed, 3654 insertions(+), 142 deletions(-) rename src/{modals/EbookModal.ts => components/EbookView.ts} (58%) create mode 100644 src/components/TypographyView.ts create mode 100644 src/modals/CoverSettingModal.ts create mode 100644 src/services/CoverManager.ts create mode 100644 src/services/ImgTemplateManager.ts create mode 100644 src/services/ThemeManager.ts create mode 100644 src/styles/components/cover-design.css create mode 100644 src/styles/components/ebook-view.css create mode 100644 src/styles/components/paginated-engine.css create mode 100644 src/styles/components/typography-view.css delete mode 100644 src/styles/modals/ebook-modal.css create mode 100644 src/utils/PaginatedEngine.ts diff --git a/manifest.json b/manifest.json index 14e7fd1..a3f13f0 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "book-smith", "name": "Book Smith", - "version": "1.0.2", + "version": "1.0.3", "minAppVersion": "0.15.0", "description": "Simplify long-form writing and book creation. Organize chapters, track progress, and export your manuscript in various formats for a seamless publishing workflow.", "author": "Yeban", diff --git a/package-lock.json b/package-lock.json index 8f7a2b4..028c550 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@popperjs/core": "^2.11.8", "docx": "^9.5.0", + "pagedjs": "^0.4.3", "uuid": "^11.1.0" }, "devDependencies": { @@ -25,6 +26,26 @@ "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", @@ -999,6 +1020,12 @@ "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", @@ -1029,6 +1056,14 @@ "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", @@ -1051,6 +1086,32 @@ "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", @@ -1130,6 +1191,46 @@ "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", @@ -1325,6 +1426,21 @@ "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", @@ -1413,6 +1529,25 @@ "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", @@ -1897,6 +2032,12 @@ "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", @@ -1984,6 +2125,12 @@ "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", @@ -2063,6 +2210,19 @@ "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", @@ -2203,6 +2363,12 @@ "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", @@ -2346,6 +2512,15 @@ "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", @@ -2456,6 +2631,12 @@ "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/src/modals/EbookModal.ts b/src/components/EbookView.ts similarity index 58% rename from src/modals/EbookModal.ts rename to src/components/EbookView.ts index d497c19..4e0efff 100644 --- a/src/modals/EbookModal.ts +++ b/src/components/EbookView.ts @@ -1,31 +1,58 @@ -import { App, Notice } from "obsidian"; -import { BaseModal } from "./BaseModal"; +import { App, setIcon, Notice } from 'obsidian'; import { BookManager } from "../services/BookManager"; import { Book } from "../types/book"; import { ExportService } from "../services/ExportService"; import { i18n } from "../i18n/i18n"; +import BookSmithPlugin from '../main'; -export class EbookModal extends BaseModal { +export class EbookView { + private container: HTMLElement; private bookManager: BookManager; private exportService: ExportService; private books: Book[] = []; private selectedBook: Book | null = null; private selectedFormat: string = 'txt'; - constructor(container: HTMLElement, private app: App, private settings: any) { - super(container, '生成电子书'); - this.bookManager = new BookManager(app, settings); - this.exportService = new ExportService(app, settings); + constructor( + private app: App, + private plugin: BookSmithPlugin, + private parentEl: HTMLElement, + private onExit: () => void + ) { + this.bookManager = new BookManager(app, plugin.settings); + this.exportService = new ExportService(app, plugin.settings); + this.createUI(); } - async open() { - // 加载所有书籍 - this.books = await this.bookManager.getAllBooks(); - super.open(); + async initialize() { + try { + // 加载所有书籍 + this.books = await this.bookManager.getAllBooks(); + this.updateBookSelector(); + } catch (error) { + console.error('加载书籍失败:', error); + } } - protected createContent() { - const content = this.element.createDiv({ cls: 'ebook-content' }); + private createUI() { + this.container = this.parentEl.createDiv({ cls: 'book-smith-ebook-view' }); + this.createHeader(); + this.createContent(); + } + + private createHeader() { + const header = this.container.createDiv({ cls: 'ebook-header' }); + setIcon(header.createSpan({ cls: 'ebook-header-icon' }), 'book'); + header.createSpan({ text: i18n.t('GENERATE_EBOOK'), cls: 'ebook-title' }); + + // 添加退出按钮 + const exitBtn = header.createDiv({ cls: 'ebook-exit-btn' }); + setIcon(exitBtn, 'x'); + exitBtn.addEventListener('click', () => this.onExit()); + } + + private createContent() { + const content = this.container.createDiv({ cls: 'ebook-content' }); // 电子书信息表单 const form = content.createDiv({ cls: 'ebook-form' }); @@ -34,7 +61,7 @@ export class EbookModal extends BaseModal { this.createBookSelector(form); // 格式选择 - const formatField = this.createFormField(form, '格式', 'format', 'select'); + const formatField = this.createFormField(form, i18n.t('FORMAT') || '格式', 'format', 'select'); const formatSelect = formatField.querySelector('select'); if (formatSelect) { // 添加支持的格式 @@ -49,40 +76,59 @@ export class EbookModal extends BaseModal { } // 生成按钮 - const generateBtn = content.createDiv({ cls: 'generate-btn', text: '生成电子书' }); + const generateBtn = content.createDiv({ cls: 'generate-btn', text: i18n.t('GENERATE_EBOOK') || '生成电子书' }); generateBtn.addEventListener('click', () => { this.exportBook(); }); } - - private createBookSelector(container: HTMLElement) { - const field = this.createFormField(container, '选择书籍', 'book', 'select'); - const select = field.querySelector('select'); + + private updateBookSelector() { + const select = this.container.querySelector('select'); + if (!select) return; - if (select && this.books.length > 0) { + // 清空现有选项 + select.innerHTML = ''; + + if (this.books.length > 0) { // 添加书籍选项 for (const book of this.books) { - this.createSelectOption(select, book.basic.uuid, book.basic.title); + this.createSelectOption(select as HTMLSelectElement, book.basic.uuid, book.basic.title); } // 默认选择第一本书 this.selectedBook = this.books[0]; - - // 监听选择变化 - select.addEventListener('change', (e) => { - const uuid = (e.target as HTMLSelectElement).value; - this.selectedBook = this.books.find(book => book.basic.uuid === uuid) || null; - }); - } else if (select) { + select.value = this.books[0].basic.uuid; + } else { // 没有书籍时显示提示 - this.createSelectOption(select, '', '没有可用的书籍'); + this.createSelectOption(select as HTMLSelectElement, '', i18n.t('NO_BOOKS_AVAILABLE') || '没有可用的书籍'); select.disabled = true; } } + private createBookSelector(container: HTMLElement) { + const field = this.createFormField(container, i18n.t('SELECT_BOOK') || '选择书籍', 'book', 'select'); + const select = field.querySelector('select'); + + if (select) { + // 监听选择变化 + select.addEventListener('change', (e) => { + const uuid = (e.target as HTMLSelectElement).value; + this.selectedBook = this.books.find(book => book.basic.uuid === uuid) || null; + + // 添加高亮效果 + select.classList.add('selected-option'); + setTimeout(() => select.classList.remove('selected-option'), 500); + }); + } + } + + // 其余方法从 EbookModal 迁移过来,保持相同的功能 + // ... 省略其他方法,与 EbookModal 中的相同 ... + + // 以下是必要的方法实现 private async exportBook() { if (!this.selectedBook) { - this.showNotice('请先选择一本书籍'); + new Notice(i18n.t('SELECT_BOOK_FIRST') || '请先选择一本书籍'); return; } @@ -95,7 +141,7 @@ export class EbookModal extends BaseModal { // 如果导出功能尚未实现,显示提示 if (content.content.startsWith(`${this.selectedFormat.toUpperCase()}导出功能开发中`)) { - this.showNotice(`${this.selectedFormat.toUpperCase()}导出功能尚在开发中,敬请期待!`); + new Notice(`${this.selectedFormat.toUpperCase()}导出功能尚在开发中,敬请期待!`); return; } @@ -122,14 +168,13 @@ export class EbookModal extends BaseModal { document.body.removeChild(a); } - this.showNotice(`导出成功!`); - this.close(); + new Notice(`导出成功!`); } catch (error) { console.error('导出错误:', error); - this.showNotice(`导出失败: ${error.message}`); + new Notice(`导出失败: ${error instanceof Error ? error.message : String(error)}`); } } - + private createFormField(container: HTMLElement, label: string, id: string, type: string, placeholder?: string) { const field = container.createDiv({ cls: 'form-field' }); field.createDiv({ cls: 'field-label', text: label }); @@ -141,7 +186,7 @@ export class EbookModal extends BaseModal { attr: { type, id, - placeholder: placeholder || null + placeholder: placeholder || '' } }); } @@ -155,4 +200,8 @@ export class EbookModal extends BaseModal { option.text = text; select.appendChild(option); } + + remove() { + this.container.remove(); + } } \ No newline at end of file diff --git a/src/components/TypographyView.ts b/src/components/TypographyView.ts new file mode 100644 index 0000000..9a71b95 --- /dev/null +++ b/src/components/TypographyView.ts @@ -0,0 +1,939 @@ +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 { i18n } from "../i18n/i18n"; +import BookSmithPlugin from '../main'; +import { ImgTemplateManager, ImgTemplate } from '../services/ImgTemplateManager'; +import { ThemeManager, Theme } from '../services/ThemeManager'; +import { CoverManager, CoverSettings } from '../services/CoverManager'; +import { CoverSettingModal } from '../modals/CoverSettingModal'; +import { PaginatedEngine, IBlock, extractBlocks, generatePaginatedTOC } from '../utils/PaginatedEngine'; +interface TypographySettings { + fontFamily: string; + fontSize: string; + lineHeight: string; + margin: string; + templateId: string; + themeId: string; + coverSettings?: CoverSettings; // 封面设置 + showCover: boolean; // 是否显示封面 + bookSize: string; // 新增:书籍开本大小 +} + +export class TypographyView { + private container: HTMLElement; + private bookManager: BookManager; + private exportService: ExportService; + private imgTemplateManager: ImgTemplateManager; + private themeManager: ThemeManager; + private coverManager: CoverManager; + private books: Book[] = []; + private selectedBook: Book | null = null; + private previewElement: HTMLElement | null = null; + private coverPreviewElement: HTMLElement | null = null; + private currentTemplate: ImgTemplate | null = null; + private coverSettings: CoverSettings | null = null; + + // 自定义选择器引用 + private customBookSelect: HTMLElement | null = null; + private customTemplateSelect: HTMLElement | null = null; + private customThemeSelect: HTMLElement | null = null; + private customFontSelect: HTMLElement | null = null; + private fontSizeInput: HTMLInputElement | null = null; + + // 添加开本大小选择器引用 + private customBookSizeSelect: HTMLElement | null = null; + private rootPath: string = ''; // 添加根路径属性 + private tempRenderContainer: HTMLElement | null = null; // 临时渲染容器 + + constructor( + private app: App, + private plugin: BookSmithPlugin, + private parentEl: HTMLElement, + private onExit: () => void + ) { + this.bookManager = new BookManager(app, plugin.settings); + this.exportService = new ExportService(app, plugin.settings); + this.themeManager = new ThemeManager(app, plugin.settings); + this.imgTemplateManager = new ImgTemplateManager(app, this.themeManager); + this.coverManager = new CoverManager(app); + this.rootPath = plugin.settings.defaultBookPath || ''; // 初始化根路径 + this.createUI(); + + // 创建临时渲染容器 + this.tempRenderContainer = document.createElement('div'); + this.tempRenderContainer.style.display = 'none'; + document.body.appendChild(this.tempRenderContainer); + } + + async initialize() { + try { + // 加载所有书籍 + this.books = await this.bookManager.getAllBooks(); + + // 初始化各个选择器 + this.initializeBookSelect(); + this.initializeTemplateSelect(); + this.initializeThemeSelect(); + this.initializeFontSelect(); + this.initializeFontSizeControls(); + this.initializeBookSizeSelect(); // 新增初始化开本大小选择器 + + // 更新预览 + this.updatePreview(); + } catch (error) { + console.error('加载书籍失败:', error); + } + } + + private createUI() { + this.container = this.parentEl.createDiv({ cls: 'book-smith-typography-view' }); + this.createHeader(); + this.createContent(); + } + + private createHeader() { + const header = this.container.createDiv({ cls: 'typography-header' }); + setIcon(header.createSpan({ cls: 'typography-header-icon' }), 'edit-3'); + header.createSpan({ text: i18n.t('DESIGN_TYPOGRAPHY'), cls: 'typography-title' }); + + // 添加退出按钮 + const exitBtn = header.createDiv({ cls: 'typography-exit-btn' }); + setIcon(exitBtn, 'x'); + exitBtn.addEventListener('click', () => this.onExit()); + } + + private createContent() { + // 创建主容器 + const content = this.container.createDiv({ cls: 'typography-content' }); + + // 创建顶部选择器面板 + const topPanel = content.createDiv({ cls: 'typography-top-panel' }); + + // 创建预览面板 + const previewPanel = content.createDiv({ cls: 'typography-preview-panel' }); + + // 创建底部按钮面板 + const bottomPanel = content.createDiv({ cls: 'typography-bottom-panel' }); + + // 创建顶部选择器 + this.createTopSelectors(topPanel); + + // 创建预览区域 + this.createPreviewArea(previewPanel); + + // 创建底部按钮 + this.createButtons(bottomPanel); + } + + private createPreviewArea(container: HTMLElement) { + container.createEl('h2', { text: i18n.t('PREVIEW') }); + + // 创建一个包含封面和内容的容器 + const previewContainer = container.createDiv({ cls: 'typography-preview-container' }); + + // 添加封面预览(独立容器) + const coverContainer = previewContainer.createDiv({ cls: 'typography-cover-container' }); + this.coverPreviewElement = coverContainer.createDiv({ cls: 'typography-cover-preview' }); + + // 内容预览(独立容器) + const contentContainer = previewContainer.createDiv({ cls: 'typography-content-container' }); + this.previewElement = contentContainer.createDiv({ cls: 'typography-preview' }); + } + + // 按照要求的顺序创建顶部选择器:书籍、模板、主题、字体、字号大小 + private createTopSelectors(container: HTMLElement) { + const selectorsContainer = container.createDiv({ cls: 'typography-selectors' }); + + // 1. 创建书籍选择器 + this.createBookSelector(selectorsContainer); + + // 2. 创建模板选择器 + this.createTemplateSelector(selectorsContainer); + + // 3. 创建主题选择器 + this.createThemeSelector(selectorsContainer); + + // 4. 创建字体选择器 + this.createFontSelector(selectorsContainer); + + // 5. 创建字号大小控制 + this.createFontSizeControls(selectorsContainer); + + // 6. 创建开本大小选择器 + this.createBookSizeSelector(selectorsContainer); + } + + // 自定义选择器通用方法 + private createCustomSelect(parent: HTMLElement, className: string, options: { value: string, text: string }[]): HTMLElement { + const container = parent.createDiv({ cls: `red-select-container ${className}` }); + + const select = container.createDiv({ cls: 'red-select' }); + const textSpan = select.createSpan({ cls: 'red-text' }); + const arrowSpan = select.createSpan({ cls: 'red-select-arrow' }); + setIcon(arrowSpan, 'chevron-down'); + + // 默认选择第一个选项 + if (options.length > 0) { + textSpan.textContent = options[0].text; + select.dataset.value = options[0].value; + } + + // 创建下拉菜单 + const dropdown = container.createDiv({ cls: 'red-select-dropdown' }); + + // 添加选项 + for (const option of options) { + const item = dropdown.createDiv({ cls: 'red-select-item' }); + item.textContent = option.text; + item.dataset.value = option.value; + + // 默认选中第一个 + if (options.indexOf(option) === 0) { + item.addClass('red-selected'); + } + + // 点击选项 + item.addEventListener('click', () => { + // 更新显示文本和值 + textSpan.textContent = option.text; + select.dataset.value = option.value; + + // 更新选中状态 + dropdown.querySelectorAll('.red-select-item').forEach(el => { + el.removeClass('red-selected'); + }); + item.addClass('red-selected'); + + // 关闭下拉菜单 + dropdown.removeClass('red-show'); + arrowSpan.style.transform = ''; + + // 触发自定义事件 + select.dispatchEvent(new CustomEvent('change', { + detail: { value: option.value, text: option.text } + })); + }); + } + + // 点击选择器显示/隐藏下拉菜单 + select.addEventListener('click', (e) => { + e.stopPropagation(); + dropdown.toggleClass('red-show', !dropdown.hasClass('red-show')); + arrowSpan.style.transform = dropdown.hasClass('red-show') ? 'rotate(180deg)' : ''; + }); + + // 点击其他地方关闭下拉菜单 + document.addEventListener('click', () => { + dropdown.removeClass('red-show'); + arrowSpan.style.transform = ''; + }); + + return container; + } + + // 更新自定义选择器的选项 + private updateCustomSelectOptions(selectElement: HTMLElement, options: { value: string, text: string }[]) { + const dropdown = selectElement.querySelector('.red-select-dropdown'); + const select = selectElement.querySelector('.red-select'); + const textSpan = selectElement.querySelector('.red-text'); + + if (!dropdown || !select || !textSpan) return; + + // 清空现有选项 + dropdown.innerHTML = ''; + + // 添加新选项 + for (const option of options) { + const item = dropdown.createDiv({ cls: 'red-select-item' }); + item.textContent = option.text; + item.dataset.value = option.value; + + // 默认选中第一个 + if (options.indexOf(option) === 0) { + item.addClass('red-selected'); + textSpan.textContent = option.text; + (select as HTMLElement).setAttribute('data-value', option.value); + } + + // 点击选项 + item.addEventListener('click', () => { + // 更新显示文本和值 + textSpan.textContent = option.text; + (select as HTMLElement).setAttribute('data-value', option.value); + + // 更新选中状态 + dropdown.querySelectorAll('.red-select-item').forEach(el => { + el.removeClass('red-selected'); + }); + item.addClass('red-selected'); + + // 关闭下拉菜单 + dropdown.removeClass('red-show'); + + // 触发自定义事件 + select.dispatchEvent(new CustomEvent('change', { + detail: { value: option.value, text: option.text } + })); + }); + } + } + + // 1. 书籍选择器 + private createBookSelector(parent: HTMLElement) { + // 获取书籍选项 + const bookOptions = this.books.map(book => ({ + value: book.basic.uuid, + text: book.basic.title + })); + + // 如果没有书籍,添加一个提示选项 + if (bookOptions.length === 0) { + bookOptions.push({ + value: '', + text: i18n.t('NO_BOOKS_AVAILABLE') || '没有可用的书籍' + }); + } + + // 创建自定义选择器 + this.customBookSelect = this.createCustomSelect( + parent, + 'red-book-select', + bookOptions + ); + this.customBookSelect.id = 'book-select'; + } + + // 初始化书籍选择器 + private initializeBookSelect() { + if (!this.customBookSelect) return; + + // 更新书籍选项 + const bookOptions = this.books.map(book => ({ + value: book.basic.uuid, + text: book.basic.title + })); + + // 更新选择器选项 + this.updateCustomSelectOptions(this.customBookSelect, bookOptions); + + // 添加事件监听 + this.customBookSelect.querySelector('.red-select')?.addEventListener('change', async (e: any) => { + const value = e.detail.value; + this.selectedBook = this.books.find(book => book.basic.uuid === value) || null; + this.updatePreview(); + }); + + // 如果有书籍,选择第一本 + if (bookOptions.length > 0) { + const select = this.customBookSelect.querySelector('.red-select'); + if (select) { + (select as HTMLElement).setAttribute('data-value', bookOptions[0].value); + this.selectedBook = this.books[0]; + this.updatePreview(); + } + } + } + + // 2. 模板选择器 + private createTemplateSelector(parent: HTMLElement) { + this.customTemplateSelect = this.createCustomSelect( + parent, + 'red-template-select', + [{ value: '', text: i18n.t('LOADING') || '加载中...' }] + ); + this.customTemplateSelect.id = 'template-select'; + } + + // 初始化模板选择器 + private async initializeTemplateSelect() { + if (!this.customTemplateSelect) return; + + // 获取模板选项 + const templateOptions = await this.getTemplateOptions(); + + // 更新选择器选项 + this.updateCustomSelectOptions(this.customTemplateSelect, templateOptions); + + // 添加事件监听 + this.customTemplateSelect.querySelector('.red-select')?.addEventListener('change', async (e: any) => { + const value = e.detail.value; + this.currentTemplate = this.imgTemplateManager.getTemplate(value) || null; + this.updatePreview(); + }); + + // 默认选择第一个模板 + if (templateOptions.length > 0) { + const select = this.customTemplateSelect.querySelector('.red-select'); + if (select) { + (select as HTMLElement).setAttribute('data-value', templateOptions[0].value); + this.currentTemplate = this.imgTemplateManager.getTemplate(templateOptions[0].value) || null; + } + } + } + + // 3. 主题选择器 + private createThemeSelector(parent: HTMLElement) { + this.customThemeSelect = this.createCustomSelect( + parent, + 'red-theme-select', + [{ value: '', text: i18n.t('LOADING') || '加载中...' }] + ); + this.customThemeSelect.id = 'theme-select'; + } + + // 初始化主题选择器 + private async initializeThemeSelect() { + if (!this.customThemeSelect) return; + + // 获取主题选项 + const themes = await this.getThemeOptions(); + + // 更新选择器选项 + this.updateCustomSelectOptions(this.customThemeSelect, themes); + + // 添加事件监听 + this.customThemeSelect.querySelector('.red-select')?.addEventListener('change', async (e: any) => { + const value = e.detail.value; + this.themeManager.setCurrentTheme(value); + if (this.previewElement) { + this.themeManager.applyTheme(this.previewElement); + } + }); + + // 默认选择第一个主题 + if (themes.length > 0) { + const select = this.customThemeSelect.querySelector('.red-select'); + if (select) { + (select as HTMLElement).setAttribute('data-value', themes[0].value); + this.themeManager.setCurrentTheme(themes[0].value); + } + } + } + + // 4. 字体选择器 + private createFontSelector(parent: HTMLElement) { + const fontOptions = [ + { value: 'default', text: i18n.t('DEFAULT_FONT') || '默认字体' }, + { value: 'serif', text: i18n.t('SERIF_FONT') || '衬线字体' }, + { value: 'sans-serif', text: i18n.t('SANS_SERIF_FONT') || '无衬线字体' }, + { value: 'monospace', text: i18n.t('MONOSPACE_FONT') || '等宽字体' } + ]; + + this.customFontSelect = this.createCustomSelect( + parent, + 'red-font-select', + fontOptions + ); + this.customFontSelect.id = 'font-select'; + } + + // 初始化字体选择器 + private initializeFontSelect() { + if (!this.customFontSelect) return; + + // 添加事件监听 + this.customFontSelect.querySelector('.red-select')?.addEventListener('change', () => { + this.updatePreview(); + }); + } + + // 5. 字号大小控制 + private createFontSizeControls(parent: HTMLElement) { + const fontSizeGroup = parent.createEl('div', { cls: 'red-font-size-group' }); + + + this.fontSizeInput = fontSizeGroup.createEl('input', { + cls: 'red-font-size-input', + type: 'text', + value: '16', + attr: { + style: 'border: none; outline: none; background: transparent;' + } + }) as HTMLInputElement; + + } + + // 初始化字号控制 + private initializeFontSizeControls() { + if (!this.fontSizeInput) return; + + const updateFontSize = () => { + if (!this.fontSizeInput) return; + this.updatePreview(); + }; + + // 获取按钮元素 + const decreaseButton = this.fontSizeInput.parentElement?.querySelector('button:first-child'); + const increaseButton = this.fontSizeInput.parentElement?.querySelector('button:last-child'); + + // 添加减小字号事件 + decreaseButton?.addEventListener('click', () => { + if (!this.fontSizeInput) return; + const currentSize = parseInt(this.fontSizeInput.value) || 16; + if (currentSize > 12) { + this.fontSizeInput.value = (currentSize - 1).toString(); + updateFontSize(); + } + }); + + // 添加增大字号事件 + increaseButton?.addEventListener('click', () => { + if (!this.fontSizeInput) return; + const currentSize = parseInt(this.fontSizeInput.value) || 16; + if (currentSize < 30) { + this.fontSizeInput.value = (currentSize + 1).toString(); + updateFontSize(); + } + }); + + // 添加输入框变化事件 + this.fontSizeInput.addEventListener('change', updateFontSize); + this.fontSizeInput.addEventListener('input', updateFontSize); + } + + // 获取选项方法 + private async getThemeOptions() { + const themes = this.themeManager.getVisibleThemes(); + return themes.map(theme => ({ + value: theme.id, + text: theme.name + })); + } + + private async getTemplateOptions() { + const templates = this.imgTemplateManager.getVisibleTemplates(); + return templates.map(template => ({ + value: template.id, + text: template.name + })); + } + + // 开本大小选择器 + private createBookSizeSelector(parent: HTMLElement) { + const bookSizeOptions = [ + { value: 'a4', text: 'A4' }, + { value: 'a5', text: 'A5' }, + { value: 'b5', text: 'B5' }, + { value: '16k', text: '16K' }, + { value: 'custom', text: i18n.t('CUSTOM_SIZE') || '自定义' } + ]; + + this.customBookSizeSelect = this.createCustomSelect( + parent, + 'red-book-size-select', + bookSizeOptions + ); + this.customBookSizeSelect.id = 'book-size-select'; + } + + // 初始化开本大小选择器 + private initializeBookSizeSelect() { + if (!this.customBookSizeSelect) return; + + // 添加事件监听 + this.customBookSizeSelect.querySelector('.red-select')?.addEventListener('change', () => { + this.updatePreview(); + }); + } + + // 获取排版设置 + private getTypographySettings(): TypographySettings { + // 获取封面开关状态 + const coverToggle = this.parentEl.querySelector('.cover-toggle-input') as HTMLInputElement; + const showCover = coverToggle ? coverToggle.checked : true; + + return { + fontFamily: (this.customFontSelect?.querySelector('.red-select') as HTMLElement)?.getAttribute('data-value') || 'default', + fontSize: this.fontSizeInput?.value || '16', + lineHeight: 'normal', // 保留默认值 + margin: '0', // 保留默认值 + templateId: (this.customTemplateSelect?.querySelector('.red-select') as HTMLElement)?.getAttribute('data-value') || 'notes', + themeId: (this.customThemeSelect?.querySelector('.red-select') as HTMLElement)?.getAttribute('data-value') || 'default', + coverSettings: this.coverSettings || undefined, + showCover: showCover, + bookSize: (this.customBookSizeSelect?.querySelector('.red-select') as HTMLElement)?.getAttribute('data-value') || 'a4' + }; + } + + // 预览相关方法 + // 在 updatePreview 方法中添加分页逻辑 + private async updatePreview() { + if (!this.selectedBook) return; + + // 获取封面开关状态 + const coverToggle = this.parentEl.querySelector('.cover-toggle-input') as HTMLInputElement; + const showCover = coverToggle ? coverToggle.checked : true; + + // 获取排版设置 + const settings = this.getTypographySettings(); + + // 更新预览样式 + this.updatePreviewStyle(settings); + + // 分离封面和内容的显示逻辑 + if (this.coverPreviewElement) { + this.coverPreviewElement.style.display = showCover ? 'block' : 'none'; + if (showCover) { + this.updateCoverPreview(); + } + } + + if (this.previewElement) { + this.previewElement.style.display = 'block'; + + // 清空预览元素 + this.previewElement.empty(); + + // 应用模板 + if (this.currentTemplate) { + this.currentTemplate.render(this.previewElement); + } + + // 应用主题 + this.themeManager.applyTheme(this.previewElement); + + // 渲染内容 + await this.renderPaginatedContent(settings); + + + } + } + + // 新增:渲染分页内容 + private async renderPaginatedContent(settings: TypographySettings) { + if (!this.previewElement || !this.tempRenderContainer) return; + + // 清空临时容器 + this.tempRenderContainer.empty(); + + // 1. 先渲染所有内容到临时容器 + await this.renderAllContent(this.tempRenderContainer); + + // 2. 提取内容块,转换为支持文字级分页的 IBlock 对象 + const blocks = extractBlocks(this.tempRenderContainer); + // 3. 创建分页内容容器 + const contentContainer = this.previewElement.createDiv({ cls: 'typography-content-pages' }); + + // 4. 创建分页引擎并应用分页 + const engine = new PaginatedEngine(contentContainer); + engine.setOptions({ + bookSize: settings.bookSize + }); + + // 执行分页 + const pageCount = engine.paginate(blocks); + + // 5. 生成分页目录 + const tocPages = generatePaginatedTOC(this.previewElement, contentContainer, settings.bookSize); + + // 6. 将目录页添加到内容前面 + const tocContainer = this.previewElement.createDiv({ cls: 'typography-toc-pages' }); + tocPages.forEach(tocPage => { + tocContainer.appendChild(tocPage); + }); + + // 将目录容器移到内容容器前面 + this.previewElement.insertBefore(tocContainer, contentContainer); + + // 7. 添加页码信息 + const pageInfo = this.previewElement.createDiv({ cls: 'page-info' }); + pageInfo.textContent = `共 ${pageCount + tocPages.length} 页(目录 ${tocPages.length} 页,正文 ${pageCount} 页)`; + } + + // 渲染所有内容到指定容器 + private async renderAllContent(container: HTMLElement) { + if (!this.selectedBook) return; + + // 渲染章节内容 + if (this.selectedBook.structure && this.selectedBook.structure.tree) { + await this.renderChapters(this.selectedBook.structure.tree, container); + } + } + + // 递归渲染章节内容(不分页,只渲染到临时容器) + private async renderChapters(chapters: ChapterNode[], container: HTMLElement, level: number = 1) { + if (!chapters || chapters.length === 0) return; + + for (const chapter of chapters) { + // 跳过被排除的章节 + if (chapter.exclude) continue; + // 创建标题元素 + const heading = document.createElement(`h${Math.min(level, 6)}`); + heading.textContent = chapter.title; + heading.dataset.actualLevel = level.toString(); // 添加自定义属性记录实际层级 + container.appendChild(heading); + + // 如果是文件节点,渲染文件内容 + if (chapter.type === 'file' && chapter.path) { + try { + // 构建文件路径 + const filePath = `${this.rootPath}/${this.selectedBook?.basic.title}/${chapter.path}`; + const file = this.app.vault.getAbstractFileByPath(filePath); + + if (file instanceof TFile) { + // 创建内容容器 + const contentEl = container.createDiv({ cls: 'chapter-content' }); + + // 使用 cachedRead 获取文件内容 + const content = await this.app.vault.cachedRead(file); + + // 使用 MarkdownRenderer 渲染内容 + await MarkdownRenderer.render( + this.app, + content, + contentEl, + file.path, + this.plugin + ); + } + } catch (error) { + console.error(`无法读取文件 ${chapter.path}:`, error); + container.createEl('p', { text: `*无法读取此章节内容*`, cls: 'error-message' }); + } + } + + // 递归处理子章节 + if (chapter.children && chapter.children.length > 0) { + await this.renderChapters(chapter.children, container, level + 1); + } + } + } + + private updatePreviewStyle(settings: TypographySettings) { + if (!this.previewElement) return; + + // 重置样式 + this.previewElement.className = 'typography-preview'; + + // 应用字体 + this.previewElement.classList.add(`font-${settings.fontFamily}`); + + // 应用字号 + this.previewElement.style.fontSize = `${settings.fontSize}px`; + + // 应用行间距 + this.previewElement.classList.add(`line-height-${settings.lineHeight}`); + + // 应用页边距 + this.previewElement.classList.add(`margin-${settings.margin}`); + + // 应用开本大小 + // this.previewElement.classList.add(`book-size-${settings.bookSize}`); + + // 如果封面预览元素存在,也应用开本大小 + if (this.coverPreviewElement) { + this.coverPreviewElement.className = 'typography-cover-preview'; + this.coverPreviewElement.classList.add(`book-size-${settings.bookSize}`); + } + } + + // 底部按钮 + private createButtons(container: HTMLElement) { + // 创建按钮容器 + const buttonsContainer = container.createDiv({ cls: 'typography-buttons' }); + + // 添加封面设计开关 + this.createCoverToggle(buttonsContainer); + + // 应用按钮 + const applyBtn = buttonsContainer.createEl('button', { + text: i18n.t('APPLY') || '应用', + cls: 'typography-btn apply-btn' + }); + applyBtn.addEventListener('click', () => { + // 应用排版设置 + new Notice(i18n.t('TYPOGRAPHY_APPLIED') || '排版设置已应用'); + }); + + // 导出按钮 + const exportBtn = buttonsContainer.createEl('button', { + text: i18n.t('EXPORT') || '导出', + cls: 'typography-btn export-btn' + }); + exportBtn.addEventListener('click', () => this.exportWithTypography()); + } + + // 导出功能 + 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 || '未知错误')); + // } + } catch (error) { + console.error('导出错误:', error); + new Notice(`导出失败: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // 添加封面设计开关 + private createCoverToggle(parent: HTMLElement) { + const coverToggleContainer = parent.createDiv({ cls: 'cover-toggle-container' }); + + coverToggleContainer.createEl('span', { + text: i18n.t('SHOW_COVER') || '显示封面', + cls: 'cover-toggle-label' + }); + + const coverToggle = coverToggleContainer.createEl('input', { + type: 'checkbox', + cls: 'cover-toggle-input' + }) as HTMLInputElement; + coverToggle.checked = false; // 默认显示封面 + + coverToggle.addEventListener('change', () => { + this.updatePreview(); + }); + + // 添加封面设计按钮 + const coverDesignBtn = coverToggleContainer.createEl('button', { + text: i18n.t('DESIGN_COVER') || '设计封面', + cls: 'cover-design-btn' + }); + + coverDesignBtn.addEventListener('click', () => this.openCoverDesigner()); + } + + // 打开封面设计器 + private openCoverDesigner() { + if (!this.selectedBook) { + new Notice(i18n.t('SELECT_BOOK_FIRST') || '请先选择一本书籍'); + return; + } + + new CoverSettingModal( + this.app, + (settings) => { + this.coverSettings = settings; + this.updatePreview(); + }, + this.parentEl, + this.coverManager, + this.coverSettings || undefined, + this.selectedBook.basic.title, + this.selectedBook.basic.author + ).open(); + } + + // 添加封面预览更新方法 + private updateCoverPreview() { + if (!this.coverPreviewElement || !this.selectedBook) return; + + // 清除现有内容 + this.coverPreviewElement.empty(); + + // 应用封面样式 + if (this.coverSettings) { + const contentContainer = this.coverManager.applyCoverStyles(this.coverPreviewElement, this.coverSettings); + + // 添加标题和作者 + if (contentContainer) { + // 添加书名 + const titleEl = contentContainer.createEl('div', { + cls: 'cover-title', + text: this.selectedBook.basic.title + }); + titleEl.setAttribute('style', this.coverSettings.titleStyle); + + // 添加副标题 + if (this.selectedBook.basic.subtitle) { + const subtitleEl = contentContainer.createEl('div', { + cls: 'cover-subtitle', + text: this.selectedBook.basic.subtitle + }); + subtitleEl.setAttribute('style', 'font-size: 18px; color: #ffffff; margin-top: 10px; text-shadow: 0 1px 2px rgba(0,0,0,0.5);'); + } + + // 添加作者信息 + if (this.selectedBook.basic.author && this.selectedBook.basic.author.length > 0) { + const authorEl = contentContainer.createEl('div', { + cls: 'cover-author', + text: this.selectedBook.basic.author.join(', ') + }); + authorEl.setAttribute('style', this.coverSettings.authorStyle); + } + + // 添加描述信息 + if (this.selectedBook.basic.desc) { + const descriptionEl = contentContainer.createEl('div', { + cls: 'cover-description', + text: this.selectedBook.basic.desc + }); + descriptionEl.setAttribute('style', 'font-size: 14px; color: #f0f0f0; margin-top: 20px; max-width: 80%; text-align: center; font-style: italic;'); + } + } + } else { + // 使用默认封面样式 + const defaultSettings = this.coverManager.getDefaultCoverSettings(); + const contentContainer = this.coverManager.applyCoverStyles(this.coverPreviewElement, defaultSettings); + + // 添加标题和作者 + if (contentContainer) { + const titleEl = contentContainer.createEl('div', { + cls: 'cover-title', + text: this.selectedBook.basic.title + }); + titleEl.setAttribute('style', defaultSettings.titleStyle); + + // 添加副标题 + if (this.selectedBook.basic.subtitle) { + const subtitleEl = contentContainer.createEl('div', { + cls: 'cover-subtitle', + text: this.selectedBook.basic.subtitle + }); + subtitleEl.setAttribute('style', 'font-size: 18px; color: #ffffff; margin-top: 10px; text-shadow: 0 1px 2px rgba(0,0,0,0.5);'); + } + + if (this.selectedBook.basic.author && this.selectedBook.basic.author.length > 0) { + const authorEl = contentContainer.createEl('div', { + cls: 'cover-author', + text: this.selectedBook.basic.author.join(', ') + }); + authorEl.setAttribute('style', defaultSettings.authorStyle); + } + + // 添加描述信息 + if (this.selectedBook.basic.desc) { + const descriptionEl = contentContainer.createEl('div', { + cls: 'cover-description', + text: this.selectedBook.basic.desc + }); + descriptionEl.setAttribute('style', 'font-size: 14px; color: #f0f0f0; margin-top: 20px; max-width: 80%; text-align: center; font-style: italic;'); + } + } + } + } + + // 移除视图 + remove() { + this.container.remove(); + } +} diff --git a/src/i18n/interfaces/modals/toolbarModals.ts b/src/i18n/interfaces/modals/toolbarModals.ts index 729539c..3ed68f1 100644 --- a/src/i18n/interfaces/modals/toolbarModals.ts +++ b/src/i18n/interfaces/modals/toolbarModals.ts @@ -52,4 +52,33 @@ export interface ToolbarModalTranslation { PAYMENT_ALIPAY: string; PAYMENT_KOFI: string; CURRENCY_UNIT: string; + + // TypographyModal + PREVIEW: string; + NO_BOOKS_AVAILABLE: string; + LOADING: string; + DEFAULT_FONT: string; + SERIF_FONT: string; + SANS_SERIF_FONT: string; + MONOSPACE_FONT: string; + APPLY: string; + TYPOGRAPHY_APPLIED: string; + EXPORT: string; + SELECT_BOOK_FIRST: string; + EXPORT_SUCCESS: string; + EXPORT_FAILED: string; + FORMAT: string; + CUSTOM_SIZE: string; + + // CoverModal + DESIGN_COVER: string; + SHOW_COVER: string; + COVER_DESIGN: string; + COVER_IMAGE: string; + CLEAR_IMAGE: string; + BACKGROUND_COLOR: string; + OVERLAY: string; + OVERLAY_DESC: string; + OVERLAY_OPACITY: string; + SCALE: string; } \ No newline at end of file diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 55e7a71..66403f5 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -373,7 +373,36 @@ const toolbarModalTranslation: ToolbarModalTranslation = { PAYMENT_WECHAT: 'WeChat pay', PAYMENT_ALIPAY: 'Alipay', PAYMENT_KOFI: 'Ko-fi', - CURRENCY_UNIT: 'CNY' + CURRENCY_UNIT: 'CNY', + + // TypographyModal + PREVIEW: 'Preview', + NO_BOOKS_AVAILABLE: 'No books available', + LOADING: 'Loading...', + DEFAULT_FONT: 'Default font', + SERIF_FONT: 'Serif font', + SANS_SERIF_FONT: 'Sans-serif font', + MONOSPACE_FONT: 'Monospace font', + APPLY: 'Apply', + TYPOGRAPHY_APPLIED: 'Typography applied', + EXPORT: 'Export', + SELECT_BOOK_FIRST: 'Please select a book first', + EXPORT_SUCCESS: 'Export successful', + EXPORT_FAILED: 'Export failed: ', + FORMAT: 'Format', + CUSTOM_SIZE: 'Custom size', + + // CoverModal + DESIGN_COVER: 'Design cover', + SHOW_COVER: 'Show cover', + COVER_DESIGN: 'Cover design', + COVER_IMAGE: 'Cover image', + CLEAR_IMAGE: 'Clear image', + BACKGROUND_COLOR: 'Background color', + OVERLAY: 'Overlay', + OVERLAY_DESC: 'Add an overlay to the cover', + OVERLAY_OPACITY: 'Overlay opacity', + SCALE: 'Scale', }; const componentTranslation: ComponentTranslation = { diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 133f1b9..aaf5cbc 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -372,7 +372,36 @@ const toolbarModalTranslation: ToolbarModalTranslation = { PAYMENT_WECHAT: '微信赞赏', PAYMENT_ALIPAY: '支付宝赞赏', PAYMENT_KOFI: 'Ko-fi 赞赏', - CURRENCY_UNIT: '元' + CURRENCY_UNIT: '元', + + // TypographyModal + PREVIEW: '预览', + NO_BOOKS_AVAILABLE: '没有可用的书籍', + LOADING: '加载中...', + DEFAULT_FONT: '默认字体', + SERIF_FONT: '衬线字体', + SANS_SERIF_FONT: '无衬线字体', + MONOSPACE_FONT: '等宽字体', + APPLY: '应用', + TYPOGRAPHY_APPLIED: '排版设置已应用', + EXPORT: '导出', + SELECT_BOOK_FIRST: '请先选择一本书籍', + EXPORT_SUCCESS: '导出成功', + EXPORT_FAILED: '导出失败: ', + FORMAT : '格式', + CUSTOM_SIZE: '自定义大小', + + // CoverModal + DESIGN_COVER: '设计封面', + SHOW_COVER: '显示封面', + COVER_DESIGN: '封面设计', + COVER_IMAGE: '封面图片', + CLEAR_IMAGE: '清除图片', + BACKGROUND_COLOR: '背景颜色', + OVERLAY: '覆盖层', + OVERLAY_DESC: '添加半透明覆盖层以增强文字可读性', + OVERLAY_OPACITY: '覆盖层透明度', + SCALE: '缩放', }; const componentTranslation: ComponentTranslation = { diff --git a/src/main.ts b/src/main.ts index 7c05f2b..5dd8b06 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,11 +8,15 @@ import { BookManager } from './services/BookManager'; import { TemplateManager } from './services/TemplateManager'; import { BookStatsManager } from './services/BookStatsManager'; import { i18n } from './i18n/i18n'; +import { ImgTemplateManager } from './services/ImgTemplateManager'; +import { ThemeManager } from './services/ThemeManager'; export default class BookSmithPlugin extends Plugin { settings: BookSmithSettings; bookManager: BookManager; templateManager: TemplateManager; + imgTemplateManager: ImgTemplateManager; + themeManager: ThemeManager; statsManager: BookStatsManager; async onload() { @@ -70,6 +74,10 @@ export default class BookSmithPlugin extends Plugin { async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + this.bookManager = new BookManager(this.app, this.settings); + this.templateManager = new TemplateManager(this.settings); + this.imgTemplateManager = new ImgTemplateManager(this.app, this.settings); + this.themeManager = new ThemeManager(this.app, this.settings); } async saveSettings() { diff --git a/src/modals/CoverSettingModal.ts b/src/modals/CoverSettingModal.ts new file mode 100644 index 0000000..53ffeee --- /dev/null +++ b/src/modals/CoverSettingModal.ts @@ -0,0 +1,240 @@ +import { App, Modal, Setting } from 'obsidian'; +import { CoverManager, CoverSettings } from '../services/CoverManager'; +import { i18n } from '../i18n/i18n'; + +export class CoverSettingModal extends Modal { + private imageUrl: string = ''; + private scale: number = 1; + private position: { x: number; y: number } = { x: 0, y: 0 }; + private titleStyle: string = ''; + private authorStyle: string = ''; + private backgroundColor: string = '#1a1a1a'; + private overlay: boolean = true; + private overlayOpacity: number = 0.4; + + private initialSettings?: CoverSettings; + private previewElement: HTMLElement | null = null; + private dragEventCleanup: (() => void) | null = null; + private coverManager: CoverManager; + + constructor( + app: App, + private onSubmit: (settings: CoverSettings) => void, + private targetPreviewEl: HTMLElement, + coverManager: CoverManager, + initialSettings?: CoverSettings, + private bookTitle?: string, + private bookAuthor?: string[] + ) { + super(app); + this.coverManager = coverManager; + this.initialSettings = initialSettings; + + if (initialSettings) { + this.imageUrl = initialSettings.imageUrl; + this.scale = initialSettings.scale; + this.position = { ...initialSettings.position }; + this.titleStyle = initialSettings.titleStyle; + this.authorStyle = initialSettings.authorStyle; + this.backgroundColor = initialSettings.backgroundColor; + this.overlay = initialSettings.overlay; + this.overlayOpacity = initialSettings.overlayOpacity; + } else { + const defaultSettings = coverManager.getDefaultCoverSettings(); + this.titleStyle = defaultSettings.titleStyle; + this.authorStyle = defaultSettings.authorStyle; + this.backgroundColor = defaultSettings.backgroundColor; + this.overlay = defaultSettings.overlay; + this.overlayOpacity = defaultSettings.overlayOpacity; + } + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass('book-smith-cover-modal'); + + const container = contentEl.createEl('div', { cls: 'cover-settings-container' }); + container.createEl('h3', { text: i18n.t('COVER_DESIGN') || '封面设计' }); + + // 预览区域 + const previewArea = container.createEl('div', { cls: 'cover-preview-area' }); + this.previewElement = previewArea.createEl('div', { cls: 'cover-preview' }); + + // 控制区域 + const controlsArea = container.createEl('div', { cls: 'cover-controls' }); + this.createControls(controlsArea); + + // 应用初始设置到预览 + this.updatePreview(); + + // 底部按钮 + const buttonContainer = contentEl.createEl('div', { cls: 'cover-button-container' }); + + const cancelButton = buttonContainer.createEl('button', { + text: i18n.t('CANCEL') || '取消', + cls: 'cover-cancel-button' + }); + cancelButton.addEventListener('click', () => this.close()); + + const applyButton = buttonContainer.createEl('button', { + text: i18n.t('APPLY') || '应用', + cls: 'cover-apply-button' + }); + applyButton.addEventListener('click', () => { + this.onSubmit({ + imageUrl: this.imageUrl, + scale: this.scale, + position: this.position, + titleStyle: this.titleStyle, + authorStyle: this.authorStyle, + backgroundColor: this.backgroundColor, + overlay: this.overlay, + overlayOpacity: this.overlayOpacity + }); + this.close(); + }); + } + + private createControls(container: HTMLElement) { + // 图片上传和清除按钮 + new Setting(container) + .setName(i18n.t('COVER_IMAGE') || '封面图片') + .addButton(button => button + .setButtonText(i18n.t('SELECT_IMAGE') || '选择图片') + .onClick(() => this.handleImageUpload())) + .addButton(button => button + .setButtonText(i18n.t('CLEAR_IMAGE') || '清除图片') + .onClick(() => this.handleClearImage())); + + // 背景颜色选择器 + new Setting(container) + .setName(i18n.t('BACKGROUND_COLOR') || '背景颜色') + .addColorPicker(color => color + .setValue(this.backgroundColor) + .onChange(value => { + this.backgroundColor = value; + this.updatePreview(); + })); + + // 覆盖层开关 + new Setting(container) + .setName(i18n.t('OVERLAY') || '覆盖层') + .setDesc(i18n.t('OVERLAY_DESC') || '添加半透明覆盖层以增强文字可读性') + .addToggle(toggle => toggle + .setValue(this.overlay) + .onChange(value => { + this.overlay = value; + this.updatePreview(); + })); + + // 覆盖层透明度 + if (this.overlay) { + new Setting(container) + .setName(i18n.t('OVERLAY_OPACITY') || '覆盖层透明度') + .addSlider(slider => slider + .setLimits(0, 1, 0.1) + .setValue(this.overlayOpacity) + .onChange(value => { + this.overlayOpacity = value; + this.updatePreview(); + })); + } + + // 缩放控制 + if (this.imageUrl) { + new Setting(container) + .setName(i18n.t('SCALE') || '缩放') + .addSlider(slider => slider + .setLimits(0.1, 2, 0.1) + .setValue(this.scale) + .onChange(value => { + this.scale = value; + this.updatePreview(); + })); + } + } + + private handleImageUpload() { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/*'; + + input.addEventListener('change', (e: Event) => { + const target = e.target as HTMLInputElement; + if (target.files && target.files[0]) { + const reader = new FileReader(); + reader.onload = (e) => { + this.imageUrl = e.target?.result as string; + this.updatePreview(); + // 重新创建控制区域以显示缩放控制 + const controlsArea = this.contentEl.querySelector('.cover-controls'); + if (controlsArea) { + controlsArea.empty(); + this.createControls(controlsArea as HTMLElement); + } + }; + reader.readAsDataURL(target.files[0]); + } + }); + + input.click(); + } + + private handleClearImage() { + this.imageUrl = ''; + this.scale = 1; + this.position = { x: 0, y: 0 }; + this.updatePreview(); + + // 重新创建控制区域以隐藏缩放控制 + const controlsArea = this.contentEl.querySelector('.cover-controls'); + if (controlsArea) { + controlsArea.empty(); + this.createControls(controlsArea as HTMLElement); + } + } + + private updatePreview() { + if (!this.previewElement) return; + + // 清除现有样式 + this.coverManager.clearCoverStyles(this.previewElement); + + // 应用新样式 + const contentContainer = this.coverManager.applyCoverStyles(this.previewElement, { + imageUrl: this.imageUrl, + scale: this.scale, + position: this.position, + titleStyle: this.titleStyle, + authorStyle: this.authorStyle, + backgroundColor: this.backgroundColor, + overlay: this.overlay, + overlayOpacity: this.overlayOpacity + }); + + // 添加标题和作者 + if (contentContainer) { + if (this.bookTitle) { + const titleEl = contentContainer.createEl('div', { cls: 'cover-title', text: this.bookTitle }); + titleEl.setAttribute('style', this.titleStyle); + } + + if (this.bookAuthor && this.bookAuthor.length > 0) { + const authorEl = contentContainer.createEl('div', { + cls: 'cover-author', + text: this.bookAuthor.join(', ') + }); + authorEl.setAttribute('style', this.authorStyle); + } + } + } + + onClose() { + if (this.dragEventCleanup) { + this.dragEventCleanup(); + this.dragEventCleanup = null; + } + this.contentEl.empty(); + } +} \ No newline at end of file diff --git a/src/services/CoverManager.ts b/src/services/CoverManager.ts new file mode 100644 index 0000000..46793c2 --- /dev/null +++ b/src/services/CoverManager.ts @@ -0,0 +1,95 @@ +import { App } from 'obsidian'; + +export interface CoverSettings { + imageUrl: string; + scale: number; + position: { x: number; y: number }; + titleStyle: string; + authorStyle: string; + backgroundColor: string; + overlay: boolean; + overlayOpacity: number; +} + +export class CoverManager { + constructor( + private app: App + ) {} + + public applyCoverStyles(element: HTMLElement, settings: CoverSettings) { + // 设置背景图片 + if (settings.imageUrl) { + const stylesArray = [ + `background-image: url(${settings.imageUrl})`, + `background-size: ${settings.scale * 100}%`, + `background-position: ${settings.position.x}px ${settings.position.y}px`, + `background-repeat: no-repeat` + ]; + + stylesArray.forEach(style => { + const match = style.match(/([^:]+):(.+)/); + if (match) { + const [, key, value] = match.map(item => item.trim()); + if (key && value) { + element.style[key as any] = value; + } + } + }); + } else { + // 如果没有背景图片,使用背景颜色 + element.style.backgroundColor = settings.backgroundColor || '#ffffff'; + } + + // 添加覆盖层以增强文字可读性 + if (settings.overlay) { + const overlay = element.createDiv({ cls: 'cover-overlay' }); + overlay.style.backgroundColor = `rgba(0, 0, 0, ${settings.overlayOpacity || 0.3})`; + overlay.style.position = 'absolute'; + overlay.style.top = '0'; + overlay.style.left = '0'; + overlay.style.right = '0'; + overlay.style.bottom = '0'; + overlay.style.zIndex = '1'; + } + + // 创建内容容器,确保在覆盖层之上 + const contentContainer = element.createDiv({ cls: 'cover-content' }); + contentContainer.style.position = 'relative'; + contentContainer.style.zIndex = '2'; + contentContainer.style.height = '100%'; + contentContainer.style.display = 'flex'; + contentContainer.style.flexDirection = 'column'; + contentContainer.style.justifyContent = 'center'; + contentContainer.style.alignItems = 'center'; + contentContainer.style.padding = '40px'; + contentContainer.style.textAlign = 'center'; + + return contentContainer; + } + + public clearCoverStyles(element: HTMLElement) { + const style = element.getAttribute('style') || ''; + const clearedStyle = style.replace(/background-image:[^;]+;|background-size:[^;]+;|background-position:[^;]+;|background-repeat:[^;]+;|background-color:[^;]+;/g, ''); + element.setAttribute('style', clearedStyle); + + // 移除覆盖层和内容容器 + const overlay = element.querySelector('.cover-overlay'); + const contentContainer = element.querySelector('.cover-content'); + + if (overlay) overlay.remove(); + if (contentContainer) contentContainer.remove(); + } + + public getDefaultCoverSettings(): CoverSettings { + return { + imageUrl: '', + scale: 1, + position: { x: 0, y: 0 }, + titleStyle: 'font-size: 32px; font-weight: bold; color: #ffffff; margin-bottom: 20px; text-shadow: 0 2px 4px rgba(0,0,0,0.5);', + authorStyle: 'font-size: 18px; color: #ffffff; font-style: italic; text-shadow: 0 1px 2px rgba(0,0,0,0.5);', + backgroundColor: '#1a1a1a', + overlay: true, + overlayOpacity: 0.4 + }; + } +} \ No newline at end of file diff --git a/src/services/ImgTemplateManager.ts b/src/services/ImgTemplateManager.ts new file mode 100644 index 0000000..dbb64c7 --- /dev/null +++ b/src/services/ImgTemplateManager.ts @@ -0,0 +1,145 @@ +import { App } from 'obsidian'; + +import { ThemeManager } from './ThemeManager'; + +export interface ImgTemplate { + id: string; + name: string; + sections: { + cover?: boolean; + header?: boolean; + content: true; + footer?: boolean; + }; + render: (element: HTMLElement, settings?: any) => void; +} + +export class ImgTemplateManager { + private templates: ImgTemplate[] = []; + private currentTemplate: ImgTemplate | null = null; + + constructor( + private app: App, + private themeManager?: ThemeManager + ) { + this.initializeTemplates(); + } + + private initializeTemplates() { + // 注册默认模板 + this.registerTemplate(new DefaultTemplate()); + + // 注册备忘录模板 + this.registerTemplate(new NotesTemplate()); + + // 注册书籍模板 + this.registerTemplate(new BookTemplate()); + + // 注册杂志模板 + this.registerTemplate(new MagazineTemplate()); + } + + registerTemplate(template: ImgTemplate) { + this.templates.push(template); + } + + getVisibleTemplates() { + return this.templates; + } + + getTemplate(id: string): ImgTemplate | null { + return this.templates.find(t => t.id === id) || null; + } + + setCurrentTemplate(id: string) { + const template = this.templates.find(t => t.id === id); + if (template) { + this.currentTemplate = template; + } + } + + applyTemplate(element: HTMLElement, settings?: any) { + if (!this.currentTemplate) { + this.currentTemplate = this.templates[0]; + } + + if (this.currentTemplate) { + this.currentTemplate.render(element, settings); + if (this.themeManager) { + this.themeManager.applyTheme(element); + } + } + } +} + +// 默认模板实现 +class DefaultTemplate implements ImgTemplate { + id = 'default'; + name = '默认模板'; + sections = { + cover: true, + header: true, + content: true as const, + footer: true + }; + + constructor() {} + + render(element: HTMLElement, settings?: any) { + // 实现默认模板的渲染逻辑 + } +} + +// 备忘录模板实现 +class NotesTemplate implements ImgTemplate { + id = 'notes'; + name = '备忘录'; + sections = { + cover: false, + header: true, + content: true as const, + footer: false + }; + + constructor() {} + + render(element: HTMLElement, settings?: any) { + // 实现备忘录模板的渲染逻辑 + } +} + +// 书籍模板实现 +class BookTemplate implements ImgTemplate { + id = 'book'; + name = '书籍'; + sections = { + cover: true, + header: false, + content: true as const, + footer: true + }; + + constructor() {} + + render(element: HTMLElement, settings?: any) { + // 实现书籍模板的渲染逻辑 + } +} + +// 杂志模板实现 +class MagazineTemplate implements ImgTemplate { + id = 'magazine'; + name = '杂志'; + sections = { + cover: true, + header: true, + content: true as const, + footer: true + }; + + constructor() {} + + render(element: HTMLElement, settings?: any) { + // 实现杂志模板的渲染逻辑 + } +} \ No newline at end of file diff --git a/src/services/ThemeManager.ts b/src/services/ThemeManager.ts new file mode 100644 index 0000000..67032fc --- /dev/null +++ b/src/services/ThemeManager.ts @@ -0,0 +1,322 @@ +import { App } from 'obsidian'; +import { BookSmithSettings } from '../settings/settings'; + +export interface Theme { + id: string; + name: string; + description: string; + isBuiltin?: boolean; + isVisible?: boolean; + styles: { + // 容器基础样式 + container: string; + // 标题样式 + title: { + h1: string; + h2: string; + h3: string; + }; + // 段落样式 + paragraph: string; + // 引用样式 + quote: string; + // 列表样式 + list: { + ul: string; + ol: string; + li: string; + }; + // 强调样式 + emphasis: { + strong: string; + em: string; + del: string; + }; + // 代码样式 + code: { + block: string; + inline: string; + }; + // 表格样式 + table: { + container: string; + header: string; + cell: string; + }; + // 其他元素样式 + hr: string; + image: string; + link: string; + }; +} + +export class ThemeManager { + private themes: Map = new Map(); + private currentThemeId: string = 'default'; + private currentFontSize: number = 16; + + constructor( + private app: App, + private settings: BookSmithSettings + ) { + // 注册默认主题 + this.registerTheme(this.createDefaultTheme()); + this.registerTheme(this.createLightTheme()); + this.registerTheme(this.createDarkTheme()); + } + + private createDefaultTheme(): Theme { + return { + id: 'default', + name: '默认主题', + description: '系统默认主题', + isBuiltin: true, + isVisible: true, + styles: { + container: 'background-color: white; color: #333; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;', + title: { + h1: 'font-size: 2em; margin-bottom: 0.5em; color: #222;', + h2: 'font-size: 1.5em; margin-bottom: 0.5em; color: #333;', + h3: 'font-size: 1.2em; margin-bottom: 0.5em; color: #444;' + }, + paragraph: 'margin-bottom: 1em; line-height: 1.6;', + quote: 'border-left: 4px solid #ddd; padding-left: 1em; color: #666; font-style: italic;', + list: { + ul: 'list-style-type: disc; padding-left: 2em; margin-bottom: 1em;', + ol: 'list-style-type: decimal; padding-left: 2em; margin-bottom: 1em;', + li: 'margin-bottom: 0.5em;' + }, + emphasis: { + strong: 'font-weight: bold;', + em: 'font-style: italic;', + del: 'text-decoration: line-through;' + }, + code: { + block: 'background-color: #f5f5f5; padding: 1em; border-radius: 4px; font-family: monospace; overflow-x: auto;', + inline: 'background-color: #f5f5f5; padding: 0.2em 0.4em; border-radius: 3px; font-family: monospace;' + }, + table: { + container: 'width: 100%; border-collapse: collapse; margin-bottom: 1em;', + header: 'background-color: #f5f5f5; font-weight: bold; text-align: left; padding: 0.5em;', + cell: 'border: 1px solid #ddd; padding: 0.5em;' + }, + hr: 'border: 0; border-top: 1px solid #ddd; margin: 1em 0;', + image: 'max-width: 100%; height: auto;', + link: 'color: #0366d6; text-decoration: none;' + } + }; + } + + private createLightTheme(): Theme { + return { + id: 'light', + name: '浅色主题', + description: '明亮清新的浅色主题', + isBuiltin: true, + isVisible: true, + styles: { + container: 'background-color: #f9f9f9; color: #333; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;', + title: { + h1: 'font-size: 2em; margin-bottom: 0.5em; color: #222; border-bottom: 1px solid #eee; padding-bottom: 0.3em;', + h2: 'font-size: 1.5em; margin-bottom: 0.5em; color: #333; border-bottom: 1px solid #eee; padding-bottom: 0.2em;', + h3: 'font-size: 1.2em; margin-bottom: 0.5em; color: #444;' + }, + paragraph: 'margin-bottom: 1em; line-height: 1.6; color: #444;', + quote: 'border-left: 4px solid #ddd; padding-left: 1em; color: #666; font-style: italic; background-color: #f5f5f5;', + list: { + ul: 'list-style-type: disc; padding-left: 2em; margin-bottom: 1em;', + ol: 'list-style-type: decimal; padding-left: 2em; margin-bottom: 1em;', + li: 'margin-bottom: 0.5em;' + }, + emphasis: { + strong: 'font-weight: bold; color: #222;', + em: 'font-style: italic; color: #444;', + del: 'text-decoration: line-through; color: #999;' + }, + code: { + block: 'background-color: #f0f0f0; padding: 1em; border-radius: 4px; font-family: monospace; overflow-x: auto; border: 1px solid #ddd;', + inline: 'background-color: #f0f0f0; padding: 0.2em 0.4em; border-radius: 3px; font-family: monospace; border: 1px solid #ddd;' + }, + table: { + container: 'width: 100%; border-collapse: collapse; margin-bottom: 1em;', + header: 'background-color: #f0f0f0; font-weight: bold; text-align: left; padding: 0.5em; border: 1px solid #ddd;', + cell: 'border: 1px solid #ddd; padding: 0.5em;' + }, + hr: 'border: 0; border-top: 1px solid #ddd; margin: 1em 0;', + image: 'max-width: 100%; height: auto; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);', + link: 'color: #0366d6; text-decoration: none;' + } + }; + } + + private createDarkTheme(): Theme { + return { + id: 'dark', + name: '深色主题', + description: '护眼舒适的深色主题', + isBuiltin: true, + isVisible: true, + styles: { + container: 'background-color: #222; color: #eee; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;', + title: { + h1: 'font-size: 2em; margin-bottom: 0.5em; color: #fff; border-bottom: 1px solid #444; padding-bottom: 0.3em;', + h2: 'font-size: 1.5em; margin-bottom: 0.5em; color: #ddd; border-bottom: 1px solid #444; padding-bottom: 0.2em;', + h3: 'font-size: 1.2em; margin-bottom: 0.5em; color: #ccc;' + }, + paragraph: 'margin-bottom: 1em; line-height: 1.6; color: #bbb;', + quote: 'border-left: 4px solid #444; padding-left: 1em; color: #999; font-style: italic; background-color: #333;', + list: { + ul: 'list-style-type: disc; padding-left: 2em; margin-bottom: 1em;', + ol: 'list-style-type: decimal; padding-left: 2em; margin-bottom: 1em;', + li: 'margin-bottom: 0.5em;' + }, + emphasis: { + strong: 'font-weight: bold; color: #fff;', + em: 'font-style: italic; color: #ddd;', + del: 'text-decoration: line-through; color: #666;' + }, + code: { + block: 'background-color: #333; padding: 1em; border-radius: 4px; font-family: monospace; overflow-x: auto; border: 1px solid #444;', + inline: 'background-color: #333; padding: 0.2em 0.4em; border-radius: 3px; font-family: monospace; border: 1px solid #444;' + }, + table: { + container: 'width: 100%; border-collapse: collapse; margin-bottom: 1em;', + header: 'background-color: #333; font-weight: bold; text-align: left; padding: 0.5em; border: 1px solid #444;', + cell: 'border: 1px solid #444; padding: 0.5em;' + }, + hr: 'border: 0; border-top: 1px solid #444; margin: 1em 0;', + image: 'max-width: 100%; height: auto; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.3);', + link: 'color: #58a6ff; text-decoration: none;' + } + }; + } + + registerTheme(theme: Theme): void { + this.themes.set(theme.id, theme); + } + + getTheme(id: string): Theme | undefined { + return this.themes.get(id); + } + + getAllThemes(): Theme[] { + return Array.from(this.themes.values()); + } + + getVisibleThemes(): Theme[] { + return this.getAllThemes().filter(theme => theme.isVisible !== false); + } + + getCurrentTheme(): Theme { + return this.themes.get(this.currentThemeId) || this.themes.get('default')!; + } + + setCurrentTheme(id: string): boolean { + if (this.themes.has(id)) { + this.currentThemeId = id; + return true; + } + return false; + } + + setFontSize(size: number): void { + this.currentFontSize = size; + } + + getFontSize(): number { + return this.currentFontSize; + } + + // 应用主题到元素 + applyTheme(element: HTMLElement, themeId?: string): void { + const theme = themeId ? this.getTheme(themeId) : this.getCurrentTheme(); + if (!theme) return; + + // 应用容器样式 + element.setAttribute('style', theme.styles.container); + + // 应用标题样式 + element.querySelectorAll('h1').forEach(el => { + el.setAttribute('style', theme.styles.title.h1); + }); + + element.querySelectorAll('h2').forEach(el => { + el.setAttribute('style', theme.styles.title.h2); + }); + + element.querySelectorAll('h3').forEach(el => { + el.setAttribute('style', theme.styles.title.h3); + }); + + // 应用段落样式 + element.querySelectorAll('p').forEach(el => { + el.setAttribute('style', theme.styles.paragraph); + }); + + // 应用引用样式 + element.querySelectorAll('blockquote').forEach(el => { + el.setAttribute('style', theme.styles.quote); + }); + + // 应用列表样式 + element.querySelectorAll('ul').forEach(el => { + el.setAttribute('style', theme.styles.list.ul); + }); + + element.querySelectorAll('ol').forEach(el => { + el.setAttribute('style', theme.styles.list.ol); + }); + + element.querySelectorAll('li').forEach(el => { + el.setAttribute('style', theme.styles.list.li); + }); + + // 应用强调样式 + element.querySelectorAll('strong').forEach(el => { + el.setAttribute('style', theme.styles.emphasis.strong); + }); + + element.querySelectorAll('em').forEach(el => { + el.setAttribute('style', theme.styles.emphasis.em); + }); + + element.querySelectorAll('del').forEach(el => { + el.setAttribute('style', theme.styles.emphasis.del); + }); + + // 应用代码样式 + element.querySelectorAll('pre code').forEach(el => { + el.setAttribute('style', theme.styles.code.block); + }); + + element.querySelectorAll('code:not(pre code)').forEach(el => { + el.setAttribute('style', theme.styles.code.inline); + }); + + // 应用表格样式 + element.querySelectorAll('table').forEach(el => { + el.setAttribute('style', theme.styles.table.container); + }); + + element.querySelectorAll('th').forEach(el => { + el.setAttribute('style', theme.styles.table.header); + }); + + element.querySelectorAll('td').forEach(el => { + el.setAttribute('style', theme.styles.table.cell); + }); + + // 应用其他元素样式 + element.querySelectorAll('hr').forEach(el => { + el.setAttribute('style', theme.styles.hr); + }); + + element.querySelectorAll('img').forEach(el => { + el.setAttribute('style', theme.styles.image); + }); + + element.querySelectorAll('a').forEach(el => { + el.setAttribute('style', theme.styles.link); + }); + } +} \ No newline at end of file diff --git a/src/styles/components/cover-design.css b/src/styles/components/cover-design.css new file mode 100644 index 0000000..e6fd334 --- /dev/null +++ b/src/styles/components/cover-design.css @@ -0,0 +1,122 @@ +/* 封面预览样式 */ +.typography-cover-preview { + width: 100%; + height: 400px; + margin-bottom: 30px; + position: relative; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); +} + +/* 封面内容样式 */ +.cover-content { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; + padding: 40px; + text-align: center; +} + +/* 封面标题默认样式 */ +.cover-title { + font-size: 32px; + font-weight: bold; + color: #ffffff; + margin-bottom: 20px; + text-shadow: 0 2px 4px rgba(0,0,0,0.5); +} + +/* 封面作者默认样式 */ +.cover-author { + font-size: 18px; + color: #ffffff; + font-style: italic; + text-shadow: 0 1px 2px rgba(0,0,0,0.5); +} + +/* 封面设计模态框样式 */ +.book-smith-cover-modal { + max-width: 90vw; +} + +.cover-settings-container { + display: flex; + flex-direction: column; + gap: 20px; +} + +.cover-preview-area { + width: 100%; + height: 300px; + margin-bottom: 20px; +} + +.cover-preview { + width: 100%; + height: 100%; + border-radius: 8px; + overflow: hidden; + position: relative; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.cover-controls { + display: flex; + flex-direction: column; + gap: 10px; +} + +.cover-button-container { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 20px; +} + +.cover-cancel-button, +.cover-apply-button { + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; +} + +.cover-cancel-button { + background-color: #f5f5f5; + border: 1px solid #ddd; +} + +.cover-apply-button { + background-color: var(--interactive-accent); + color: var(--text-on-accent); + border: none; +} + +/* 封面开关样式 */ +.cover-toggle-container { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; +} + +.cover-toggle-label { + font-size: 14px; +} + +.cover-toggle-input { + margin: 0; +} + +.cover-design-btn { + padding: 4px 8px; + border-radius: 4px; + background-color: var(--interactive-accent); + color: var(--text-on-accent); + font-size: 12px; + cursor: pointer; + border: none; + margin-left: 10px; +} \ No newline at end of file diff --git a/src/styles/components/ebook-view.css b/src/styles/components/ebook-view.css new file mode 100644 index 0000000..aab8e5c --- /dev/null +++ b/src/styles/components/ebook-view.css @@ -0,0 +1,108 @@ +/* 电子书视图基础样式 */ +.book-smith-ebook-view { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +/* 头部样式 */ +.ebook-header { + display: flex; + align-items: center; + padding: 12px 16px; + background-color: var(--background-secondary); + border-bottom: 1px solid var(--background-modifier-border); +} + +.ebook-header-icon { + margin-right: 8px; + color: var(--text-accent); +} + +.ebook-title { + flex: 1; + font-size: 16px; + font-weight: 600; +} + +.ebook-exit-btn { + cursor: pointer; + color: var(--text-muted); + padding: 4px; + border-radius: 4px; +} + +.ebook-exit-btn:hover { + color: var(--text-normal); + background-color: var(--background-modifier-hover); +} + +/* 内容区域样式 */ +.ebook-content { + display: flex; + flex-direction: column; + flex: 1; + overflow-y: auto; + padding: 20px; +} + +/* 表单样式 */ +.ebook-form { + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: 20px; + max-width: 500px; +} + +.form-field { + display: flex; + flex-direction: column; + gap: 6px; +} + +.field-label { + font-weight: 500; + color: var(--text-normal); +} + +/* 选择器样式 */ +.ebook-form select { + width: 100%; + padding: 8px 12px; + border-radius: 6px; + background-color: var(--background-primary); + border: 1px solid var(--background-modifier-border); + color: var(--text-normal); + font-size: 14px; +} + +.ebook-form select:focus { + border-color: var(--interactive-accent); + box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2); +} + +/* 生成按钮样式 */ +.generate-btn { + padding: 10px 20px; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + text-align: center; + transition: all 0.2s ease; + background-color: var(--interactive-accent); + color: var(--text-on-accent); + max-width: 200px; + margin-top: 10px; +} + +.generate-btn:hover { + background-color: var(--interactive-accent-hover); +} + +/* 选中高亮效果 */ +.selected-option { + background-color: rgba(var(--interactive-accent-rgb), 0.2) !important; + transition: background-color 0.5s ease; +} \ No newline at end of file diff --git a/src/styles/components/paginated-engine.css b/src/styles/components/paginated-engine.css new file mode 100644 index 0000000..58cb15e --- /dev/null +++ b/src/styles/components/paginated-engine.css @@ -0,0 +1,158 @@ +/* 分页容器样式 */ +.typography-content-pages { + display: flex; + flex-direction: column; + gap: 30px; + margin-top: 30px; +} + +/* 页面样式 */ +.book-page { + position: relative; + background-color: white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + overflow: hidden; + page-break-after: always; + padding: 20px 25px; + border: 1px solid #ddd; +} + +/* 不同开本大小的页面样式 */ +.book-size-a4 { + width: 210mm; + min-height: 297mm; + max-height: 297mm; + padding-bottom:10px +} + +.book-size-a5 { + width: 148mm; + min-height: 210mm; + max-height: 210mm; +} + +.book-size-b5 { + width: 176mm; + min-height: 250mm; + max-height: 250mm; +} + +.book-size-16k { + width: 184mm; + min-height: 260mm; + max-height: 260mm; + padding: 18mm; +} + +.book-size-custom { + width: 180mm; + min-height: 240mm; + max-height: 240mm; + padding: 15mm; +} + +/* 目录样式 */ +.typography-toc { + margin-bottom: 30px; + padding: 15px; + border: 1px solid #ddd; + background-color: #f9f9f9; +} + +.typography-toc-pages { + display: flex; + flex-direction: column; + gap: 30px; + margin-bottom: 30px; +} + +.toc-page { + position: relative; +} + +.toc-list { + list-style-type: none; + padding-left: 0; +} + +.toc-list li { + margin: 5px 0; + line-height: 1.5; +} + +.toc-item-content { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + border-bottom: 1px dotted #ccc; +} + +.toc-page-ref { + margin-left: 10px; + color: #666; +} + +.toc-list a { + text-decoration: none; + color: #333; + flex-grow: 1; +} + +.toc-list a:hover { + text-decoration: underline; +} + +/* 目录级别样式 */ +.toc-level-1 { font-weight: bold; font-size: 1.2em; } +.toc-level-2 { padding-left: 20px; font-weight: bold; } +.toc-level-3 { padding-left: 20px; } +.toc-level-4 { padding-left: 40px; } +.toc-level-5 { padding-left: 60px; } +.toc-level-6 { padding-left: 80px; } + +/* 页面信息样式 */ +.page-info { + text-align: center; + margin-top: 20px; + color: #666; + font-size: 14px; +} + +/* 封面和内容容器 */ +.typography-preview-container { + display: flex; + flex-direction: column; + gap: 30px; + width: 100%; +} + +.typography-cover-container, +.typography-content-container { + width: 100%; +} + +/* 页码样式 */ +.page-number { + position: absolute; + bottom: 10px; + left: 0; + right: 0; + text-align: center; + font-size: 12px; + color: #666; +} + +/* 页码标记样式 */ +.page-marker { + position: absolute; + bottom: 30px; + left: 0; + right: 0; + text-align: center; + font-size: 12px; + color: #999; + border-top: 1px solid #eee; + padding-top: 5px; + margin-top: 10px; +} \ No newline at end of file diff --git a/src/styles/components/typography-view.css b/src/styles/components/typography-view.css new file mode 100644 index 0000000..0bd1744 --- /dev/null +++ b/src/styles/components/typography-view.css @@ -0,0 +1,612 @@ +/* 排版视图基础样式 */ +.book-smith-typography-view { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + background-color: var(--background-primary); +} + +/* 头部样式 */ +.typography-header { + display: flex; + align-items: center; + padding: 12px 16px; + background-color: var(--background-secondary); + border-bottom: 1px solid var(--background-modifier-border); +} + +.typography-header-icon { + margin-right: 8px; + color: var(--text-accent); +} + +.typography-title { + flex: 1; + font-size: 16px; + font-weight: 600; +} + +.typography-exit-btn { + cursor: pointer; + color: var(--text-muted); + padding: 4px; + border-radius: 4px; +} + +.typography-exit-btn:hover { + color: var(--text-normal); + background-color: var(--background-modifier-hover); +} + +/* 内容区域样式 */ +.typography-content { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; +} + +/* 顶部选择器面板样式 */ +.typography-top-panel { + padding: 16px; + border-bottom: 1px solid var(--background-modifier-border); + background-color: var(--background-secondary-alt); +} + +/* 选择器容器样式 */ +.typography-selectors { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; +} + +/* 表单字段样式 */ +.form-field { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 150px; +} + +.field-label { + font-weight: 500; + color: var(--text-normal); + font-size: 12px; +} + +/* 选择器样式 */ +.typography-selectors select { + padding: 8px 12px; + border-radius: 6px; + background-color: var(--background-primary); + border: 1px solid var(--background-modifier-border); + color: var(--text-normal); + font-size: 14px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; + padding-right: 30px; +} + +.typography-selectors select:focus { + border-color: var(--interactive-accent); + outline: none; + box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2); +} + +.typography-selectors select:hover { + border-color: var(--interactive-hover); +} + +/* 选择器容器样式 */ +.typography-selectors { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + justify-content: center; +} + +/* 表单字段样式 - 移除垂直布局 */ +.form-field { + display: inline-block; + min-width: 150px; +} + +.field-label { + font-weight: 500; + color: var(--text-normal); + font-size: 12px; +} + +/* 预览面板样式 */ +.typography-preview-panel { + flex: 1; + overflow-y: auto; + padding: 20px; + display: flex; + flex-direction: column; +} + +/* 预览标题 */ +.typography-preview-panel h2 { + margin-top: 0; + margin-bottom: 16px; + font-size: 18px; + color: var(--text-normal); + text-align: center; +} + +/* 预览内容区域 */ +.typography-preview { + flex: 1; + padding: 20px; + background-color: white; + color: black; + border-radius: 4px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); + overflow-y: auto; + margin: 0 auto; + max-width: 800px; +} + +/* 底部按钮面板样式 */ +.typography-bottom-panel { + padding: 16px; + border-top: 1px solid var(--background-modifier-border); + background-color: var(--background-secondary-alt); +} + +/* 按钮样式 */ +.typography-buttons { + display: flex; + gap: 12px; + justify-content: center; +} + +.apply-btn, .export-btn { + padding: 10px 16px; + border-radius: 6px; + text-align: center; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease; + min-width: 120px; +} + +.apply-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); +} + +/* 选中效果 */ +.selected-option { + animation: highlight 0.5s ease-in-out; +} + +@keyframes highlight { + 0% { background-color: var(--background-primary); } + 50% { background-color: var(--interactive-accent-hover); } + 100% { background-color: var(--background-primary); } +} + +/* 保留原有的字体、字号、行高和页边距样式 */ +.font-default { + font-family: var(--default-font); +} + +.font-serif { + font-family: Georgia, Times, 'Times New Roman', serif; +} + +.font-sans-serif { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; +} + +.font-monospace { + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; +} + +/* 字号预览样式 */ +.font-size-small { + font-size: 14px; +} + +.font-size-medium { + font-size: 16px; +} + +.font-size-large { + font-size: 18px; +} + +/* 行高预览样式 */ +.line-height-compact { + line-height: 1.3; +} + +.line-height-normal { + line-height: 1.6; +} + +.line-height-relaxed { + line-height: 1.8; +} + +/* 页边距预览样式 */ +.margin-narrow { + padding: 10px; +} + +.margin-normal { + padding: 20px; +} + +.margin-wide { + padding: 30px; +} + + +/* 添加模板相关样式 */ + +/* 备忘录模板样式 */ +.notes-template { + background-color: #f9f9f9; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + padding: 20px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} + +.notes-header { + border-bottom: 1px solid #eee; + padding-bottom: 10px; + margin-bottom: 15px; +} + +.notes-title { + font-size: 18px; + font-weight: 600; + color: #333; + text-align: center; +} + +.notes-content { + line-height: 1.6; +} + +/* 简洁模板样式 */ +.simple-template { + background-color: white; + padding: 20px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} + +.simple-content { + max-width: 650px; + margin: 0 auto; + line-height: 1.6; +} + +/* 文章模板样式 */ +.article-template { + background-color: white; + padding: 30px; + font-family: Georgia, Times, "Times New Roman", serif; +} + +.article-header { + text-align: center; + margin-bottom: 30px; +} + +.article-meta { + color: #666; + font-size: 14px; + margin-top: 5px; +} + +.article-abstract { + background-color: #f5f5f5; + padding: 15px; + border-left: 4px solid #ddd; + margin-bottom: 20px; +} + +.article-content { + line-height: 1.8; +} + +.article-footer { + margin-top: 30px; + padding-top: 15px; + border-top: 1px solid #eee; + font-size: 14px; + color: #666; +} + +.article-references { + margin-bottom: 10px; +} + +.article-copyright { + font-style: italic; +} + +/* 选择器高亮效果 */ +.selected-option { + background-color: rgba(14, 101, 235, 0.1) !important; + transition: background-color 0.5s ease; +} + +/* ===== 下拉选择器 ===== */ +.red-select-container { + position: relative; + max-width: 200px; + flex: 1; +} + +.red-select { + height: 36px; + padding: 0 12px; + border: 1px solid var(--background-modifier-border); + border-radius: 8px; + background: var(--background-primary); + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + user-select: none; + transition: all 0.2s ease; + color: var(--text-normal); + box-shadow: 0 1px 3px var(--background-modifier-box-shadow); + white-space: nowrap; + overflow: hidden; +} + +.red-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + min-width: 0; +} + +.red-select:hover { + background: var(--background-modifier-hover); + box-shadow: 0 2px 6px var(--background-modifier-box-shadow); +} + +.red-select.disabled { + opacity: 0.5; + background: var(--background-secondary) !important; + color: var(--text-muted) !important; + border-color: var(--background-modifier-border) !important; + cursor: not-allowed; + box-shadow: none; +} + +.red-select-arrow { + color: var(--text-normal); + font-size: 12px; + transition: transform 0.2s ease; + margin-left: 4px; + flex-shrink: 0; +} + +.red-select-dropdown { + position: absolute; + top: calc(100% + 4px); + left: 0; + width: 100%; + background: var(--background-primary); + border: 1px solid var(--background-modifier-border); + border-radius: 8px; + box-shadow: 0 4px 12px var(--background-modifier-box-shadow); + display: none; + z-index: 1000; + overflow: hidden; +} + +.red-select-dropdown.red-show { + display: block; +} + +.red-select-item { + padding: 8px 12px; + cursor: pointer; + transition: all 0.2s ease; + color: var(--text-normal); +} + +.red-select-item:hover { + background: var(--background-modifier-hover); +} + +.red-select-item.red-selected { + background: var(--background-modifier-hover); + color: var(--text-accent); +} + +/* ===== 字号调整组件 ===== */ +.red-font-size-group { + height: 36px; + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; + background: var(--background-primary); + border: 1px solid var(--background-modifier-border); + border-radius: 8px; + padding: 0; + overflow: hidden; + box-shadow: 0 1px 3px var(--background-modifier-box-shadow); +} + +.red-font-size-input { + width: 30px; + text-align: center; +} + +.red-font-size-btn { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + cursor: pointer; + color: var(--text-normal); + font-size: 16px; + transition: background 0.2s ease; +} + +.red-font-size-btn:hover { + background: var(--background-modifier-hover); +} + + +/* 开本大小样式 */ +.book-size-a4 { + aspect-ratio: 1 / 1.414; /* A4 比例 */ + max-width: 595px; /* A4 宽度 */ +} + +.book-size-a5 { + aspect-ratio: 1 / 1.414; /* A5 比例 */ + max-width: 420px; /* A5 宽度 */ +} + +.book-size-b5 { + aspect-ratio: 1 / 1.414; /* B5 比例 */ + max-width: 498px; /* B5 宽度 */ +} + +.book-size-16k { + aspect-ratio: 1 / 1.5; /* 16K 比例 */ + max-width: 460px; /* 16K 宽度 */ +} + +.book-size-custom { + max-width: 800px; +} + + +/* 分页容器 */ +.typography-paged-content { + column-count: 2; + column-gap: 40px; + column-fill: auto; + height: 100%; + overflow: hidden; +} + +/* 页面样式 */ +.typography-page { + break-inside: avoid; + page-break-inside: avoid; + margin-bottom: 20px; + padding: 20px; + border: 1px solid #ddd; + background-color: white; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +/* 章节标题强制分页 */ +h1, h2 { + break-before: page; + page-break-before: always; +} + +/* 目录样式 */ +.typography-toc { + margin-bottom: 30px; +} + +.typography-toc-item { + margin: 5px 0; +} + +.typography-toc-level-1 { + font-weight: bold; + margin-left: 0; +} + +.typography-toc-level-2 { + margin-left: 20px; +} + +.typography-toc-level-3 { + margin-left: 40px; +} + +/* 添加到 styles/views/typography.css 或直接在 TypographyView 中添加样式 */ +.typography-content-pages { + display: flex; + flex-direction: column; + gap: 20px; +} + +.book-page { + border: 1px solid #ddd; + padding: 20px; + background-color: white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + min-height: 200px; + max-height: 800px; + overflow: hidden; + page-break-after: always; + position: relative; +} + +/* 目录样式 */ +.typography-toc { + margin-bottom: 30px; + padding: 15px; + border: 1px solid #ddd; + background-color: #f9f9f9; +} + +.toc-list { + list-style-type: none; + padding-left: 0; +} + +.toc-list ul { + padding-left: 20px; + list-style-type: none; +} + +.toc-list li { + margin: 5px 0; +} + +.toc-list a { + text-decoration: none; + color: #333; +} + +.toc-list a:hover { + text-decoration: underline; +} diff --git a/src/styles/index.css b/src/styles/index.css index aa3c16c..bfbb343 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -1,4 +1,9 @@ +/* 导入全屏模态框样式 */ @import './variables.css'; +@import './components/typography-view.css'; +@import './components/paginated-engine.css'; +@import './components/ebook-view.css'; +@import './components/cover-design.css'; @import './views/book-view.css'; @import './components/chapter-tree.css'; @import './modals/create-book-modal.css'; @@ -11,7 +16,6 @@ @import './modals/base-modal.css'; @import './modals/community-modal.css'; @import './modals/donate-modal.css'; -@import './modals/ebook-modal.css'; @import './modals/inspiration-modal.css'; @import './modals/contact-modal.css'; @import './settings/settings-tab.css'; \ No newline at end of file diff --git a/src/styles/modals/ebook-modal.css b/src/styles/modals/ebook-modal.css deleted file mode 100644 index d38f891..0000000 --- a/src/styles/modals/ebook-modal.css +++ /dev/null @@ -1,90 +0,0 @@ -/* 电子书内容区域 */ -.ebook-content { - display: flex; - flex-direction: column; - gap: 20px; -} - -/* 表单样式 */ -.ebook-form { - display: flex; - flex-direction: column; - gap: 15px; -} - -.form-field { - display: flex; - flex-direction: column; - gap: 6px; -} - -/* 文件输入框隐藏 */ -.form-field input[type="file"] { - display: none; -} - -.field-label { - font-size: 0.9em; - color: var(--text-normal); - font-weight: 500; -} - -.form-field input, .form-field select { - padding: 10px 12px; - border-radius: 6px; - border: 1px solid var(--background-modifier-border); - background-color: var(--background-modifier-form-field); - color: var(--text-normal); - font-size: 0.9em; - height: 40px; -} - -.form-field input:focus, .form-field select:focus { - border-color: var(--text-accent); - outline: none; -} - -.form-field input::placeholder { - color: var(--text-muted); -} - -/* 文件选择按钮样式 */ -.file-select-button { - display: inline-block; - padding: 6px 12px; - background-color: var(--background-modifier-border); - color: var(--text-normal); - border-radius: 4px; - cursor: pointer; - font-size: 0.85em; - margin-right: 10px; -} - -.file-select-button:hover { - background-color: var(--background-modifier-border-hover); -} - -.file-label { - display: inline-block; - color: var(--text-muted); - font-size: 0.85em; -} - -/* 生成按钮 */ -.generate-btn { - background: var(--text-accent); - color: var(--text-on-accent); - padding: 12px 0; - border-radius: 6px; - text-align: center; - cursor: pointer; - margin-top: 10px; - transition: all 0.3s ease; - font-weight: 500; -} - -.generate-btn:hover { - background: var(--text-accent-hover, var(--text-accent)); - opacity: 0.9; - transform: translateY(-2px); -} \ No newline at end of file diff --git a/src/utils/PaginatedEngine.ts b/src/utils/PaginatedEngine.ts new file mode 100644 index 0000000..8518bc1 --- /dev/null +++ b/src/utils/PaginatedEngine.ts @@ -0,0 +1,474 @@ +/** + * 基础块接口,定义分页所需的基本操作 + */ +export interface IBlock { + type: string; + isEmpty(): boolean; + splitTail(minLength?: number): IBlock | null; // 分页时只允许尾部分割 + mergeTail(other: IBlock): void; // 块合并仅支持尾部合并 + toHTMLElement(): HTMLElement; + clone(): IBlock; +} + +/** + * Paragraph/TextBlock:支持从后往前按标点拆分,无法拆分时等长强拆,保证极端容错 + */ +export class TextBlock implements IBlock { + type: string; + content: string; + + constructor(type: string, content: string) { + this.type = type; + this.content = content; + } + + isEmpty(): boolean { + return this.content.trim().length === 0; + } + + splitTail(minLength = 40): TextBlock | null { + if (this.content.length <= minLength * 2) return null; + + const punctuation = /[。!?;.?!;]/; + let splitPoint = -1; + + for (let i = this.content.length - minLength; i >= minLength; i--) { + if (punctuation.test(this.content[i])) { + splitPoint = i + 1; + break; + } + } + + // fallback: 没找到标点就强拆中间 + if (splitPoint === -1) { + splitPoint = Math.floor(this.content.length / 2); + } + + const head = this.content.slice(0, splitPoint).trim(); + const tail = this.content.slice(splitPoint).trim(); + + if (tail.length < minLength * 0.6) return null; + + this.content = head; + return new TextBlock(this.type, tail); + } + + mergeTail(other: IBlock): void { + if (other instanceof TextBlock) { + this.content += other.content; + } + } + + toHTMLElement(): HTMLElement { + const el = document.createElement(this.type); + el.innerHTML = this.content; + return el; + } + + clone(): TextBlock { + return new TextBlock(this.type, this.content); + } +} + +/** + * DOMBlock:不可分割的块,如列表/表格/引用块等 + */ +export class DOMBlock implements IBlock { + type: string; + element: HTMLElement; + + constructor(element: HTMLElement) { + this.type = element.tagName.toLowerCase(); + this.element = element.cloneNode(true) as HTMLElement; + } + + isEmpty(): boolean { + return !this.element.textContent?.trim(); + } + splitTail(): IBlock | null { return null; } // DOM块不可分割 + mergeTail(other: IBlock): void { } // DOM块不可合并 + toHTMLElement(): HTMLElement { + return this.element.cloneNode(true) as HTMLElement; + } + clone(): DOMBlock { + return new DOMBlock(this.element); + } +} + +/** + * ImageBlock:专门处理图片元素,亦不可分割合并 + */ +export class ImageBlock implements IBlock { + type: string; + element: HTMLElement; + + constructor(element: HTMLImageElement) { + this.type = 'img'; + this.element = element.cloneNode(true) as HTMLElement; + } + + isEmpty(): boolean { return false; } + splitTail(): IBlock | null { return null; } + mergeTail(other: IBlock): void { } + toHTMLElement(): HTMLElement { + return this.element.cloneNode(true) as HTMLElement; + } + clone(): ImageBlock { + return new ImageBlock(this.element as HTMLImageElement); + } +} + +/** + * 单页结构 + */ +export class Page { + blocks: IBlock[] = []; + element: HTMLElement; + bookSize: string; + + constructor(bookSize: string = 'a4') { + this.bookSize = bookSize; + this.element = this.createPageElement(); + } + + private createPageElement(): HTMLElement { + const page = document.createElement('div'); + page.classList.add('book-page', `book-size-${this.bookSize}`); + // 页码占位 + const pageNumber = document.createElement('div'); + pageNumber.classList.add('page-number'); + page.appendChild(pageNumber); + return page; + } + + addBlock(block: IBlock): void { + this.blocks.push(block.clone()); + this.element.appendChild(block.toHTMLElement()); + } + removeLastBlock(): IBlock | null { + if (!this.blocks.length) return null; + this.element.removeChild(this.element.lastChild as Node); + return this.blocks.pop() || null; + } + isOverflow(maxHeight: number): boolean { + return this.element.scrollHeight > maxHeight; + } + setPageNumber(number: number): void { + const pageNumberElement = this.element.querySelector('.page-number'); + if (pageNumberElement) { + pageNumberElement.textContent = number.toString(); + (pageNumberElement as HTMLElement).style.position = 'absolute'; + (pageNumberElement as HTMLElement).style.bottom = '10px'; + (pageNumberElement as HTMLElement).style.left = '0'; + (pageNumberElement as HTMLElement).style.right = '0'; + (pageNumberElement as HTMLElement).style.textAlign = 'center'; + } + } +} + +/** + * 提取 DOM 容器内的所有内容块 + */ +export function extractBlocks(container: HTMLElement): IBlock[] { + const blocks: IBlock[] = []; + const selectors = 'p, h1, h2, h3, h4, h5, h6, ul, ol, pre, blockquote, table, .callout, img'; + const elements = Array.from(container.querySelectorAll(selectors)); + for (const el of elements) { + if (el.tagName.toLowerCase() === 'img') { + blocks.push(new ImageBlock(el as HTMLImageElement)); + } else if (['p','h1','h2','h3','h4','h5','h6'].includes(el.tagName.toLowerCase())) { + const block = new TextBlock(el.tagName.toLowerCase(), el.innerHTML); + blocks.push(block); + } else { + blocks.push(new DOMBlock(el as HTMLElement)); + } + } + return blocks; +} + +/** + * 分页主引擎 + */ +export class PaginatedEngine { + private container: HTMLElement; + private pages: Page[] = []; + private bookSize: string = 'a4'; + private pageHeight: number = 800; + + constructor(container: HTMLElement) { + this.container = container; + } + + setOptions(options: { bookSize?: string; pageHeight?: number }) { + if (options.bookSize) this.bookSize = options.bookSize; + if (options.pageHeight) this.pageHeight = options.pageHeight; + } + + /** + * 主分页流程,极端容错防止死循环 + */ + paginate(blocks: IBlock[]): number { + this.container.innerHTML = ''; + this.pages = []; + const pageHeightMap = { + 'a4': 1123, 'a5': 794, 'b5': 945, '16k': 983, 'custom': 907 + }; + const actualPageHeight = pageHeightMap[this.bookSize as keyof typeof pageHeightMap] || this.pageHeight; + + let currentPage = this.createNewPage(); + let unpaginatedBlocks = blocks.map(b => b.clone()); + let safetyCounter = 0; + const maxIterations = blocks.length * 6; + + while (unpaginatedBlocks.length > 0 && safetyCounter < maxIterations) { + safetyCounter++; + const nextBlock = unpaginatedBlocks[0]; + currentPage.addBlock(nextBlock); + + if (currentPage.isOverflow(actualPageHeight)) { + const removed = currentPage.removeLastBlock(); + if (!removed) break; + + let splitBlock = removed; + let splitSuccess = false; + + while (true) { + const tail = splitBlock.splitTail?.() ?? null; + if (!tail) break; + + currentPage.addBlock(splitBlock); + if (!currentPage.isOverflow(actualPageHeight)) { + unpaginatedBlocks[0] = tail; + splitSuccess = true; + break; + } + + currentPage.removeLastBlock(); + splitBlock = tail; + } + + if (!splitSuccess) { + unpaginatedBlocks[0] = removed; + currentPage = this.createNewPage(); + + if (unpaginatedBlocks.length === 1 && unpaginatedBlocks[0] === removed) { + currentPage.addBlock(removed); + unpaginatedBlocks.shift(); + } + } + } else { + unpaginatedBlocks.shift(); + } + } + this.pages.forEach((page, idx) => page.setPageNumber(idx + 1)); + return this.pages.length; + } + + private createNewPage(): Page { + const page = new Page(this.bookSize); + this.pages.push(page); + this.container.appendChild(page.element); + return page; + } + getPages(): Page[] { return this.pages; } + addPageMarkers(format = "— 第 {page} 页 —") { + this.pages.forEach((page, idx) => { + const marker = document.createElement('div'); + marker.classList.add('page-marker'); + marker.textContent = format.replace("{page}", (idx + 1).toString()); + page.element.appendChild(marker); + }); + } +} +/** + * 创建一个分页容器,模拟一页纸的样式 + */ +export function createNewPage(bookSize: string = 'a4'): HTMLElement { + const page = document.createElement('div'); + page.classList.add('book-page'); + page.classList.add(`book-size-${bookSize}`); + + // 添加页码占位元素 + const pageNumber = document.createElement('div'); + pageNumber.classList.add('page-number'); + page.appendChild(pageNumber); + + return page; +} + +/** + * 生成分页目录 + */ +export function generatePaginatedTOC(container: HTMLElement, contentContainer: HTMLElement, bookSize: string = 'a4'): HTMLElement[] { + // 创建目录容器 + const tocContainer = document.createElement('div'); + + const tocTitle = document.createElement('h2'); + tocTitle.textContent = '目录'; + tocContainer.appendChild(tocTitle); + + const tocList = document.createElement('ul'); + tocList.classList.add('toc-list'); + tocContainer.appendChild(tocList); + + // 查找所有标题元素和它们所在的页面 + const headingsWithPages: Array<{ + level: number; + text: string; + id: string; + pageNumber: number; + }> = []; + const pages = Array.from(contentContainer.querySelectorAll('.book-page')); + + pages.forEach((page, pageIndex) => { + const headings = Array.from(page.querySelectorAll('h1, h2, h3, h4, h5, h6')); + headings.forEach((heading, index) => { + // 优先使用自定义属性中的实际层级,如果没有则使用标签名中的数字 + const level = (heading as HTMLElement).dataset.actualLevel + ? parseInt((heading as HTMLElement).dataset.actualLevel || '1') + : parseInt(heading.tagName.substring(1)); + const text = heading.textContent || ''; + const id = `heading-${pageIndex}-${index}`; + + // 设置标题ID,用于锚点链接 + heading.id = id; + + headingsWithPages.push({ + level, + text, + id, + pageNumber: pageIndex + 1 + }); + }); + }); + + // 为每个标题创建目录项 + headingsWithPages.forEach(({ level, text, id, pageNumber }) => { + const listItem = document.createElement('li'); + listItem.classList.add(`toc-level-${level}`); + + // 创建链接和页码容器 + const itemContent = document.createElement('div'); + itemContent.classList.add('toc-item-content'); + + // 添加链接 + const link = document.createElement('a'); + link.href = `#${id}`; + link.textContent = text; + itemContent.appendChild(link); + + // 添加页码 + const pageRef = document.createElement('span'); + pageRef.classList.add('toc-page-ref'); + pageRef.textContent = pageNumber.toString(); + itemContent.appendChild(pageRef); + + listItem.appendChild(itemContent); + tocList.appendChild(listItem); + }); + + // 对目录进行分页处理 + const tocPages = []; + const pageHeight = getPageHeightByBookSize(bookSize); + + // 创建第一个目录页 + let currentTocPage = document.createElement('div'); + currentTocPage.classList.add('book-page', 'toc-page', `book-size-${bookSize}`); + currentTocPage.appendChild(tocContainer.cloneNode(true)); + tocPages.push(currentTocPage); + + // 检查目录是否需要分页 + if (currentTocPage.scrollHeight > pageHeight) { + // 重新创建目录,按项目分页 + tocPages.length = 0; + currentTocPage = document.createElement('div'); + currentTocPage.classList.add('book-page', 'toc-page', `book-size-${bookSize}`); + + const newTocContainer = document.createElement('div'); + newTocContainer.classList.add('typography-toc'); + newTocContainer.classList.add(`book-size-${bookSize}`); + + const newTocTitle = document.createElement('h2'); + newTocTitle.textContent = '目录'; + newTocContainer.appendChild(newTocTitle); + + const newTocList = document.createElement('ul'); + newTocList.classList.add('toc-list'); + newTocContainer.appendChild(newTocList); + + currentTocPage.appendChild(newTocContainer); + tocPages.push(currentTocPage); + + // 逐项添加目录项,检查是否需要创建新页 + headingsWithPages.forEach(({ level, text, id, pageNumber }) => { + const listItem = document.createElement('li'); + listItem.classList.add(`toc-level-${level}`); + + // 创建链接和页码容器 + const itemContent = document.createElement('div'); + itemContent.classList.add('toc-item-content'); + + // 添加链接 + const link = document.createElement('a'); + link.href = `#${id}`; + link.textContent = text; + itemContent.appendChild(link); + + // 添加页码 + const pageRef = document.createElement('span'); + pageRef.classList.add('toc-page-ref'); + pageRef.textContent = pageNumber.toString(); + itemContent.appendChild(pageRef); + + listItem.appendChild(itemContent); + + // 添加到当前目录页 + const currentTocList = currentTocPage.querySelector('.toc-list'); + if (currentTocList) { + currentTocList.appendChild(listItem); + + // 检查是否溢出 + if (currentTocPage.scrollHeight > pageHeight) { + // 移除刚添加的项 + currentTocList.removeChild(listItem); + + // 创建新的目录页 + const nextTocPage = document.createElement('div'); + nextTocPage.classList.add('book-page', 'toc-page', `book-size-${bookSize}`); + + const nextTocContainer = document.createElement('div'); + nextTocContainer.classList.add('typography-toc'); + nextTocContainer.classList.add(`book-size-${bookSize}`); + + const nextTocTitle = document.createElement('h2'); + nextTocTitle.textContent = '目录'; + nextTocContainer.appendChild(nextTocTitle); + + const nextTocList = document.createElement('ul'); + nextTocList.classList.add('toc-list'); + nextTocContainer.appendChild(nextTocList); + + nextTocPage.appendChild(nextTocContainer); + nextTocList.appendChild(listItem); + + tocPages.push(nextTocPage); + currentTocPage = nextTocPage; + } + } + }); + } + + return tocPages; +} + +// 辅助函数:根据开本大小获取页面高度 +function getPageHeightByBookSize(bookSize: string): number { + switch (bookSize) { + case 'a4': return 1000; + case 'a5': return 800; + case 'b5': return 900; + case '16k': return 850; + case 'custom': return 800; + default: return 800; + } +} \ No newline at end of file diff --git a/src/views/ToolsView.ts b/src/views/ToolsView.ts index c67cbd2..8669b28 100644 --- a/src/views/ToolsView.ts +++ b/src/views/ToolsView.ts @@ -2,11 +2,12 @@ import { ItemView, WorkspaceLeaf, setIcon, Menu, Notice } from 'obsidian'; import { FocusToolView } from '../components/FocusToolView'; import { DonateModal } from '../modals/DonateModal'; import { InspirationModal } from '../modals/InspirationModal'; -import { EbookModal } from '../modals/EbookModal'; import { CommunityModal } from '../modals/CommunityModal'; import { ContactModal } from '../modals/ContactModal'; import BookSmithPlugin from '../main'; import { i18n } from '../i18n/i18n'; +import { TypographyView } from '../components/TypographyView'; +import { EbookView } from '../components/EbookView'; interface ToolItem { icon: string; @@ -19,6 +20,8 @@ interface ToolItem { export class ToolView extends ItemView { private normalView: HTMLElement | null = null; private focusView: FocusToolView | null = null; + private typographyView: TypographyView | null = null; + private ebookView: EbookView | null = null; constructor(leaf: WorkspaceLeaf, private plugin: BookSmithPlugin) { super(leaf); @@ -46,13 +49,6 @@ export class ToolView extends ItemView { this.createNormalView(container as HTMLElement); } - async onClose() { - if (this.focusView) { - this.focusView.remove(); - this.focusView = null; - } - } - // 视图刷新方法 refresh() { if (this.normalView) { @@ -118,14 +114,12 @@ export class ToolView extends ItemView { { icon: 'edit-3', text: i18n.t('DESIGN_TYPOGRAPHY'), - onClick: () => { - new Notice(i18n.t('FEATURE_COMING_SOON', { feature: i18n.t('DESIGN_TYPOGRAPHY') })); - } + onClick: () => this.enterTypographyMode() }, { icon: 'book', text: i18n.t('GENERATE_EBOOK'), - onClick: () => new EbookModal(this.containerEl, this.app, this.plugin.settings).open() + onClick: () => this.enterEbookMode() }, { icon: 'clock', @@ -241,4 +235,68 @@ export class ToolView extends ItemView { } ); } + + // 添加进入排版模式的方法 + private enterTypographyMode() { + if (!this.normalView) return; + this.normalView.empty(); + + this.typographyView = new TypographyView( + this.app, + this.plugin, + this.normalView, + () => { + this.typographyView?.remove(); + this.typographyView = null; + if (this.normalView) { + this.normalView.empty(); + this.createNormalView(this.normalView); + } + } + ); + + // 初始化视图(加载书籍等) + this.typographyView.initialize(); + } + + // 添加进入电子书模式的方法 + private enterEbookMode() { + if (!this.normalView) return; + this.normalView.empty(); + + this.ebookView = new EbookView( + this.app, + this.plugin, + this.normalView, + () => { + this.ebookView?.remove(); + this.ebookView = null; + if (this.normalView) { + this.normalView.empty(); + this.createNormalView(this.normalView); + } + } + ); + + // 初始化视图(加载书籍等) + this.ebookView.initialize(); + } + + // 修改 onClose 方法,确保所有视图都被正确关闭 + async onClose() { + if (this.focusView) { + this.focusView.remove(); + this.focusView = null; + } + + if (this.typographyView) { + this.typographyView.remove(); + this.typographyView = null; + } + + if (this.ebookView) { + this.ebookView.remove(); + this.ebookView = null; + } + } } \ No newline at end of file