From feb1477397fc6561ca433351009e931f2074c561 Mon Sep 17 00:00:00 2001 From: Yeban8090 Date: Mon, 7 Jul 2025 22:36:42 +0800 Subject: [PATCH 01/10] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modals/BookSelectionModal.ts | 169 ++++ src/modals/ExportModal.ts | 879 +++++++++++++++++++-- src/modals/PreviewModal.ts | 40 + src/services/BookRenderService.ts | 759 ++++++++++++++++++ src/styles/index.css | 1 + src/styles/modals/book-selection-modal.css | 76 ++ src/styles/modals/export-modal.css | 815 ++++++++++++++++--- src/views/ToolsView.ts | 22 +- 8 files changed, 2576 insertions(+), 185 deletions(-) create mode 100644 src/modals/BookSelectionModal.ts create mode 100644 src/modals/PreviewModal.ts create mode 100644 src/services/BookRenderService.ts create mode 100644 src/styles/modals/book-selection-modal.css diff --git a/src/modals/BookSelectionModal.ts b/src/modals/BookSelectionModal.ts new file mode 100644 index 0000000..709a92a --- /dev/null +++ b/src/modals/BookSelectionModal.ts @@ -0,0 +1,169 @@ +import { App, Modal, Notice, ButtonComponent } from 'obsidian'; +import { BookManager } from '../services/BookManager'; +import { ExportModal } from './ExportModal'; +import { BookRenderService } from '../services/BookRenderService'; +import { i18n } from '../i18n/i18n'; +import { Book } from '../types/book'; +import BookSmithPlugin from '../main'; + +export class BookSelectionModal extends Modal { + private selectedBook: Book | null = null; + private books: Book[] = []; + private bookListContainer: HTMLElement; + constructor( + app: App, + private plugin: BookSmithPlugin + ) { + super(app); + } + + async onOpen() { + const { contentEl } = this; + contentEl.empty(); + + // 设置模态框标题 + contentEl.createEl('h2', { text: i18n.t('SELECT_BOOK') || '选择要导出的书籍' }); + + // 加载书籍列表 + await this.loadBooks(); + + // 创建书籍列表容器 + this.bookListContainer = contentEl.createDiv({ cls: 'book-selection-list' }); + this.renderBookList(); + + // 创建按钮容器 + const buttonContainer = contentEl.createDiv({ cls: 'book-selection-buttons' }); + + // 取消按钮 + new ButtonComponent(buttonContainer) + .setButtonText(i18n.t('CANCEL') || '取消') + .onClick(() => { + this.close(); + }); + + // 导出按钮 + new ButtonComponent(buttonContainer) + .setButtonText(i18n.t('EXPORT') || '导出') + .setCta() + .onClick(() => { + this.handleExport(); + }); + } + + private async loadBooks() { + try { + this.books = await this.plugin.bookManager.getAllBooks(); + } catch (error) { + console.error('加载书籍列表失败:', error); + new Notice('加载书籍列表失败'); + this.books = []; + } + } + + private renderBookList() { + this.bookListContainer.empty(); + + if (this.books.length === 0) { + const emptyMessage = this.bookListContainer.createDiv({ cls: 'book-selection-empty' }); + emptyMessage.createEl('p', { text: '未找到任何书籍' }); + emptyMessage.createEl('p', { + text: '请先创建一本书籍再进行导出', + cls: 'book-selection-hint' + }); + return; + } + + this.books.forEach((book, index) => { + const bookItem = this.bookListContainer.createDiv({ cls: 'book-selection-item' }); + + // 单选框 + const radioContainer = bookItem.createDiv({ cls: 'book-selection-radio' }); + const radio = radioContainer.createEl('input', { + type: 'radio', + attr: { + name: 'selected-book', + value: book.basic.uuid + } + }); + + // 书籍信息 + const bookInfo = bookItem.createDiv({ cls: 'book-selection-info' }); + const title = bookInfo.createEl('h3', { + text: book.basic.title, + cls: 'book-selection-title' + }); + + const details = bookInfo.createDiv({ cls: 'book-selection-details' }); + details.createEl('span', { + text: `${i18n.t('AUTHOR') || '作者'}: ${book.basic.author.join(', ')}`, + cls: 'book-selection-author' + }); + + if (book.stats) { + details.createEl('span', { + text: `${i18n.t('WORD_COUNT') || '字数'}: ${book.stats.total_words || 0}`, + cls: 'book-selection-words' + }); + } + + const createdDate = new Date(book.basic.created_at).toLocaleDateString(); + details.createEl('span', { + text: `${'创建日期'}: ${createdDate}`, + cls: 'book-selection-date' + }); + + // 点击整个项目选中 + bookItem.addEventListener('click', () => { + // 清除其他选中状态 + this.bookListContainer.querySelectorAll('.book-selection-item').forEach(item => { + item.removeClass('selected'); + }); + this.bookListContainer.querySelectorAll('input[type="radio"]').forEach((input: HTMLInputElement) => { + input.checked = false; + }); + + // 设置当前选中 + bookItem.addClass('selected'); + radio.checked = true; + this.selectedBook = book; + }); + + // 默认选中第一本书 + if (index === 0) { + bookItem.addClass('selected'); + radio.checked = true; + this.selectedBook = book; + } + }); + } + + private async handleExport() { + if (!this.selectedBook) { + new Notice('请先选择一本书籍'); + return; + } + + try { + this.close(); + const bookRenderService = new BookRenderService(this.plugin); + // 创建并打开导出模态框 - 修正参数顺序 + const exportModal = new ExportModal( + this.app, + this.plugin, + bookRenderService, + this.selectedBook + ); + exportModal.open(); + } catch (error) { + console.error('打开导出模态框失败:', error); + new Notice('打开导出模态框失败'); + } + } + + // 移除 addStyles 方法,因为样式已经移到独立的CSS文件中 + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} \ No newline at end of file diff --git a/src/modals/ExportModal.ts b/src/modals/ExportModal.ts index 8fabe71..938a7f9 100644 --- a/src/modals/ExportModal.ts +++ b/src/modals/ExportModal.ts @@ -1,84 +1,855 @@ -import { Notice } from "obsidian"; -import { BaseModal } from "./BaseModal"; +import { Notice, App, Modal, ButtonComponent } from "obsidian"; import { i18n } from "../i18n/i18n"; -import { ExportService } from "../services/ExportService"; +import { BookRenderService, RenderConfig, RenderedBook } from "../services/BookRenderService"; import { Book } from "../types/book"; +import { HeaderFooterTocModal } from "./HeaderFooterTocModal"; +import { CoverSettingModal } from "./CoverSettingModal"; +import BookSmithPlugin from "../main"; +import * as fs from "fs/promises"; +import * as electron from "electron"; -export class ExportModal extends BaseModal { +export interface ExportSettings { + format: string; + bookSize: string; + cover?: any; + headerFooterToc?: any; + theme?: string; + showCover: boolean; + coverImageData?: string; +} + +export class ExportModal extends Modal { private formatButtons: HTMLButtonElement[] = []; private selectedFormat: string | null = null; - private contentEl: HTMLElement; + private settingsContainer: HTMLElement; + private previewContainer: HTMLElement; + private mainContent: HTMLElement; + private exportBtn: HTMLButtonElement; + private isRendering: boolean = false; + private abortController: AbortController | null = null; + private renderedBook: RenderedBook | null = null; + private webview: electron.WebviewTag | null = null; + private webviewReady: boolean = false; + + private exportSettings: ExportSettings = { + format: '', + bookSize: 'A4', + showCover: false + }; + + private renderSettings = { + showTitle: true, + scale: 100, + displayHeader: true, + displayFooter: true, + cssSnippet: '' + }; constructor( - container: HTMLElement, - private exportService: ExportService, - private executeExportCallback: (format: string) => Promise + app: App, + private plugin: BookSmithPlugin, + private bookRenderService: BookRenderService, + private selectedBook: Book, + private pluginSettings?: any ) { - super(container, i18n.t('SELECT_EXPORT_FORMAT') || '选择导出格式'); + super(app); + + if (pluginSettings) { + this.renderSettings = { + showTitle: pluginSettings.showTitle ?? true, + scale: pluginSettings.scale ?? 100, + displayHeader: pluginSettings.displayHeader ?? true, + displayFooter: pluginSettings.displayFooter ?? true, + cssSnippet: pluginSettings.cssSnippet ?? '' + }; + } } - protected createContent() { - // 创建内容容器 - this.contentEl = this.element.createDiv({ cls: 'export-content' }); + onOpen() { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass('export-modal'); - // 创建格式选择容器 - const formatsContainer = this.contentEl.createDiv({ cls: 'export-formats-container' }); + // 调整模态框尺寸 + this.containerEl.style.setProperty('--dialog-width', '50vw'); + this.containerEl.style.setProperty('--dialog-height', '70vh'); - // 获取支持的导出格式 - const formats = this.exportService.getSupportedFormats(); + this.createHeader(); + this.createMainContent(); + this.createFooter(); + } - // 创建格式按钮 - formats.forEach(format => { - const formatBtn = formatsContainer.createEl('button', { - text: format.toUpperCase(), - cls: 'export-format-btn' - }); + private createHeader() { + const { contentEl } = this; + const header = contentEl.createDiv({ cls: 'export-modal-header' }); - this.formatButtons.push(formatBtn); + header.createEl('h2', { + text: '导出书籍', + cls: 'export-modal-title' + }); + } - formatBtn.addEventListener('click', () => { - // 移除其他按钮的选中状态 - this.formatButtons.forEach(btn => btn.classList.remove('selected')); - // 添加当前按钮的选中状态 - formatBtn.classList.add('selected'); - // 设置选中的格式 - this.selectedFormat = format; + private createMainContent() { + const { contentEl } = this; + const mainContent = contentEl.createDiv({ cls: 'export-modal-main centered' }); + this.mainContent = mainContent; + + // 左侧预览区域 - 只在选择PDF格式时显示 + this.previewContainer = mainContent.createDiv({ cls: 'export-preview-panel' }); + this.updatePreviewVisibility(); + + // 右侧设置区域 + const settingsPanel = mainContent.createDiv({ cls: 'export-settings-panel' }); + this.createSettingsContent(settingsPanel); + } + + private updateLayoutMode() { + if (!this.mainContent) return; + + if (this.selectedFormat && this.selectedFormat == 'pdf') { + this.mainContent.removeClass('centered'); + this.mainContent.addClass('split-layout'); + } else { + this.mainContent.removeClass('split-layout'); + this.mainContent.addClass('centered'); + } + } + + private updatePreviewVisibility() { + if (!this.previewContainer) return; + + if (this.selectedFormat === 'pdf') { + this.previewContainer.style.display = 'flex'; + this.createPreviewArea(); + } else { + this.previewContainer.style.display = 'none'; + this.previewContainer.empty(); + this.cleanupWebview(); + } + } + + private createWebview(scale = 1.0): electron.WebviewTag { + const webview = document.createElement('webview') as electron.WebviewTag; + webview.src = 'app://obsidian.md/help.html'; + webview.setAttribute('style', ` + height: 100%; + width: 100%; + border: 1px solid #f2f2f2; + background: white; + `); + webview.nodeintegration = true; + return webview; + } + + private getAllStyles(): string[] { + const cssTexts: string[] = []; + + // 添加主题相关的样式注释 + cssTexts.push('/* ---------- Obsidian Theme Styles ---------- */'); + + Array.from(document.styleSheets).forEach((sheet) => { + // @ts-ignore + const id = sheet.ownerNode?.id; + // @ts-ignore + const href = sheet.ownerNode?.href; + + // 跳过Svelte样式,但保留所有主题相关样式 + if (id?.startsWith('svelte-')) { + return; + } + + const division = `/* ----------${id ? `id:${id}` : href ? `href:${href}` : 'inline'}---------- */`; + cssTexts.push(division); + + try { + Array.from(sheet?.cssRules ?? []).forEach((rule) => { + cssTexts.push(rule.cssText); + }); + } catch (error) { + console.error('Error reading CSS rules:', error); + // 对于跨域样式表,尝试获取基本信息 + if (href) { + cssTexts.push(`/* External stylesheet: ${href} */`); + } + } + }); + + // 获取当前主题的body类名并添加相关样式 + const bodyClasses = Array.from(document.body.classList); + const themeClasses = bodyClasses.filter(cls => + cls.includes('theme-') || + cls.includes('dark') || + cls.includes('light') + ); + + if (themeClasses.length > 0) { + cssTexts.push(`/* ---------- Current Theme Classes: ${themeClasses.join(', ')} ---------- */`); + } + + // 添加补丁样式 + cssTexts.push(...this.getPatchStyles()); + + return cssTexts; + } + + private getPatchStyles(): string[] { + const CSS_PATCH = ` + /* ---------- css patch ---------- */ + body { + overflow: auto !important; + } + @media print { + .print .markdown-preview-view { + height: auto !important; + } + .md-print-anchor, .blockid { + white-space: pre !important; + border: none !important; + display: inline-block !important; + position: absolute !important; + width: 1px !important; + height: 1px !important; + right: 0 !important; + outline: 0 !important; + background: 0 0 !important; + text-decoration: initial !important; + text-shadow: initial !important; + } + table { + break-inside: auto; + } + tr { + break-inside: avoid; + break-after: auto; + } + } + img.__canvas__ { + width: 100% !important; + height: 100% !important; + } + .book-chapter { + margin-bottom: 2em; + } + `; + + return [CSS_PATCH, ...this.getPrintStyles()]; + } + + private getPrintStyles(): string[] { + const cssTexts: string[] = []; + + Array.from(document.styleSheets).forEach((sheet) => { + try { + const cssRules = sheet?.cssRules ?? []; + Array.from(cssRules).forEach((rule) => { + if (rule.constructor.name === "CSSMediaRule") { + if ((rule as CSSMediaRule).conditionText === "print") { + const res = rule.cssText.replace(/@media print\s*\{(.+)\}/g, "$1"); + cssTexts.push(res); + } + } + }); + } catch (error) { + console.error('Error reading print styles:', error); + } + }); + + return cssTexts; + } + + private makeWebviewJs(doc: Document): string { + // 获取当前主题相关的类名 + const currentBodyClasses = Array.from(document.body.classList); + const currentHtmlClasses = Array.from(document.documentElement.classList); + + // 获取重要的data属性 + const themeAttr = document.documentElement.getAttribute('data-theme') || ''; + const modeAttr = document.documentElement.getAttribute('data-mode') || ''; + + return ` + // 设置基本内容 + document.body.innerHTML = decodeURIComponent(\`${encodeURIComponent(doc.body.innerHTML)}\`); + document.head.innerHTML = decodeURIComponent(\`${encodeURIComponent(doc.head.innerHTML)}\`); + + // 复制原始属性 + document.body.setAttribute("class", \`${doc.body.getAttribute("class") || ''}\`); + document.body.setAttribute("style", \`${doc.body.getAttribute("style") || ''}\`); + + // 动态应用当前主题的类名到body + ${currentBodyClasses.map(cls => + cls.includes('theme-') || cls.includes('dark') || cls.includes('light') || cls.includes('obsidian-app') + ? `document.body.classList.add('${cls}');` + : '' + ).filter(Boolean).join('\n ')} + + // 动态应用当前主题的类名到html + ${currentHtmlClasses.map(cls => + `document.documentElement.classList.add('${cls}');` + ).join('\n ')} + + // 设置重要的data属性 + ${themeAttr ? `document.documentElement.setAttribute('data-theme', '${themeAttr}');` : ''} + ${modeAttr ? `document.documentElement.setAttribute('data-mode', '${modeAttr}');` : ''} + + document.title = \`${doc.title}\`; + `; + } + + private async setupWebview(doc: Document) { + if (!this.webview) return; + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Webview setup timeout')); + }, 10000); + + this.webview!.addEventListener('dom-ready', async () => { + try { + clearTimeout(timeout); + + // 1. 先注入所有基础样式 + const styles = this.getAllStyles(); + for (const css of styles) { + await this.webview!.insertCSS(css); + } + + // 2. 处理自定义CSS片段(如果有的话) + if (this.renderSettings.cssSnippet && this.renderSettings.cssSnippet !== '0') { + try { + // 这里需要实现CSS片段读取逻辑 + await this.webview!.insertCSS(this.renderSettings.cssSnippet); + } catch (error) { + console.warn('Failed to load CSS snippet:', error); + } + } + + // 3. 注入内容 + await this.webview!.executeJavaScript(this.makeWebviewJs(doc)); + + // 4. 最后再次注入补丁样式,确保优先级 + const patchStyles = this.getPatchStyles(); + for (const css of patchStyles) { + await this.webview!.insertCSS(css); + } + + this.webviewReady = true; + this.updateExportButtonState(); + resolve(); + } catch (error) { + clearTimeout(timeout); + reject(error); + } }); }); + } - // 添加分隔线 - this.contentEl.createEl('hr', { cls: 'export-divider' }); + private createPreviewArea() { + if (this.selectedFormat !== 'pdf') { + return; + } + + this.previewContainer.empty(); + this.cleanupWebview(); + + const previewHeader = this.previewContainer.createDiv({ cls: 'preview-header' }); + previewHeader.createEl('h3', { text: 'PDF导出预览', cls: 'preview-title' }); + + const previewContent = this.previewContainer.createDiv({ cls: 'preview-content' }); + + if (this.isRendering) { + const loading = previewContent.createDiv({ cls: 'preview-loading' }); + loading.innerHTML = ` +
+
正在渲染预览...
+
+
+
+
准备中...
+
+ `; + } else if (this.renderedBook) { + this.displayRenderedContent(previewContent); + } else { + const error = previewContent.createDiv({ cls: 'preview-error' }); + error.innerHTML = ` +
+
渲染失败,请重试
+ `; + } + } + private cleanupWebview() { + if (this.webview) { + this.webview.remove(); + this.webview = null; + this.webviewReady = false; + } + } - // 创建按钮容器 - const buttonContainer = this.contentEl.createDiv({ cls: 'export-buttons-container' }); + private updateRenderProgress(current: number, total: number, fileName: string) { + const progressFill = this.previewContainer.querySelector('.progress-fill') as HTMLElement; + const progressText = this.previewContainer.querySelector('.progress-text') as HTMLElement; + const progressFile = this.previewContainer.querySelector('.progress-file') as HTMLElement; - // 创建取消按钮 - const cancelBtn = buttonContainer.createEl('button', { - text: i18n.t('CANCEL') || '取消', - cls: 'export-cancel-btn' + if (progressFill && progressText && progressFile) { + const percentage = total > 0 ? Math.round((current / total) * 100) : 0; + progressFill.style.width = `${percentage}%`; + progressText.textContent = `${current}/${total} (${percentage}%)`; + progressFile.textContent = fileName; + } + } + + // 移除重复的方法:getAllStyles, getPatchStyles, getPrintStyles, makeWebviewJs + + private async startRenderPreview() { + if (!this.selectedFormat || this.isRendering) return; + + this.stopRendering(); + + this.isRendering = true; + this.renderedBook = null; + this.abortController = new AbortController(); + this.updateExportButtonState(); + this.updateFormatButtonsState(); + this.createPreviewArea(); + + try { + const renderService = new BookRenderService(this.app); + const config: RenderConfig = { + showTitle: this.renderSettings.showTitle, + scale: this.renderSettings.scale, + displayHeader: this.renderSettings.displayHeader, + displayFooter: this.renderSettings.displayFooter, + cssSnippet: this.renderSettings.cssSnippet, + abortSignal: this.abortController.signal, + onProgress: (current: number, total: number, fileName: string) => { + this.updateRenderProgress(current, total, fileName); + } + }; + + this.renderedBook = await renderService.renderBook( + this.selectedBook, + this.plugin.settings.defaultBookPath, + config + ); + + if (this.abortController.signal.aborted) { + console.log('Rendering was aborted after completion'); + return; + } + + console.log('Render completed, updating preview...', this.renderedBook); + + } catch (error) { + if (this.abortController?.signal.aborted || error.message === 'Render aborted') { + console.log('Rendering was aborted'); + return; + } + + console.error('Render failed:', error); + if (this.isRendering) { + new Notice('渲染失败,请检查控制台错误信息'); + } + } finally { + if (!this.abortController?.signal.aborted) { + this.isRendering = false; + this.updateFormatButtonsState(); + this.createPreviewArea(); // 这会触发 displayRenderedContent + // 延迟更新按钮状态,确保webview已经设置完成 + setTimeout(() => { + this.updateExportButtonState(); + }, 100); + } + } + } + + private async displayRenderedContent(container: HTMLElement) { + if (!this.renderedBook?.doc) return; + + container.empty(); + + try { + this.webview = this.createWebview(); + container.appendChild(this.webview); + + // 使用BookRenderService的setupWebview方法 + await this.bookRenderService.setupWebview(this.webview, this.renderedBook.doc); + + this.webviewReady = true; + this.updateExportButtonState(); + } catch (error) { + console.error('Error setting up webview:', error); + const errorDiv = container.createDiv({ cls: 'preview-error' }); + errorDiv.innerHTML = ` +
+
设置预览时出错: ${error.message}
+ `; + } + } + + private createSettingsContent(container: HTMLElement) { + this.createBookInfo(container); + this.createFormatSelection(container); + this.createSettingsArea(container); + } + + private createFooter() { + const { contentEl } = this; + const footer = contentEl.createDiv({ cls: 'export-modal-footer' }); + + const buttonGroup = footer.createDiv({ cls: 'export-button-group' }); + + const cancelBtn = buttonGroup.createEl('button', { + text: '取消', + cls: 'export-btn export-btn-secondary' }); + cancelBtn.addEventListener('click', () => this.handleCancel()); - cancelBtn.addEventListener('click', () => { - this.close(); - }); + this.exportBtn = buttonGroup.createEl('button', { + text: '导出', + cls: 'export-btn export-btn-primary' + }) as HTMLButtonElement; + this.exportBtn.addEventListener('click', () => this.handleExport()); - // 创建确认按钮 - const confirmBtn = buttonContainer.createEl('button', { - text: i18n.t('CONFIRM') || '确定', - cls: 'export-confirm-btn' - }); + this.updateExportButtonState(); + } - confirmBtn.addEventListener('click', async () => { - if (this.selectedFormat) { - // 关闭对话框 - this.close(); - - // 执行导出回调 - await this.executeExportCallback(this.selectedFormat); + private updateExportButtonState() { + if (this.exportBtn) { + let canExport: boolean; + + if (this.selectedFormat === 'pdf') { + canExport = !!(this.selectedFormat && !this.isRendering && this.renderedBook && this.webviewReady); } else { - this.showNotice(i18n.t('SELECT_EXPORT_FORMAT') || '请选择导出格式'); + canExport = !!this.selectedFormat && !this.isRendering; + } + + this.exportBtn.disabled = !canExport; + this.exportBtn.textContent = this.isRendering ? '渲染中...' : '导出'; + } + } + + private updateFormatButtonsState() { + this.formatButtons.forEach(btn => { + if (this.isRendering) { + btn.classList.add('disabled'); + btn.style.pointerEvents = 'none'; + btn.style.opacity = '0.6'; + } else { + btn.classList.remove('disabled'); + btn.style.pointerEvents = 'auto'; + btn.style.opacity = '1'; } }); } + + private async handleCancel() { + this.stopRendering(); + this.cleanupWebview(); + this.close(); + } + + private stopRendering() { + if (this.abortController) { + this.abortController.abort(); + this.abortController = null; + } + this.isRendering = false; + this.updateExportButtonState(); + this.updateFormatButtonsState(); + } + + private createBookInfo(container: HTMLElement) { + const bookCard = container.createDiv({ cls: 'export-book-card' }); + + const bookIcon = bookCard.createDiv({ cls: 'export-book-icon' }); + bookIcon.innerHTML = '📖'; + + const bookDetails = bookCard.createDiv({ cls: 'export-book-details' }); + + bookDetails.createEl('h3', { + text: this.selectedBook.basic.title, + cls: 'export-book-title' + }); + + if (this.selectedBook.basic.author && this.selectedBook.basic.author.length > 0) { + bookDetails.createEl('p', { + text: `作者: ${this.selectedBook.basic.author.join(', ')}`, + cls: 'export-book-author' + }); + } + + if (this.selectedBook.basic.desc) { + bookDetails.createEl('p', { + text: this.selectedBook.basic.desc, + cls: 'export-book-desc' + }); + } + } + + private createFormatSelection(container: HTMLElement) { + const formatSection = container.createDiv({ cls: 'export-format-section' }); + + formatSection.createEl('h4', { + text: '选择导出格式', + cls: 'export-section-title' + }); + + const formatGrid = formatSection.createDiv({ cls: 'export-format-grid' }); + + const formats = [ + { key: 'pdf', label: 'PDF', icon: '📄', desc: '便携文档格式' }, + { key: 'txt', label: 'TXT', icon: '📝', desc: '纯文本格式' }, + { key: 'docx', label: 'DOCX', icon: '📋', desc: 'Word文档格式' } + ]; + + formats.forEach(format => { + const formatCard = formatGrid.createDiv({ cls: 'export-format-card' }); + + const formatIcon = formatCard.createDiv({ cls: 'export-format-icon' }); + formatIcon.innerHTML = format.icon; + + const formatInfo = formatCard.createDiv({ cls: 'export-format-info' }); + formatInfo.createEl('div', { + text: format.label, + cls: 'export-format-label' + }); + formatInfo.createEl('div', { + text: format.desc, + cls: 'export-format-desc' + }); + + this.formatButtons.push(formatCard as unknown as HTMLButtonElement); + + formatCard.addEventListener('click', () => { + if (this.isRendering) { + new Notice('渲染进行中,请稍候...'); + return; + } + + this.formatButtons.forEach(btn => btn.classList.remove('selected')); + formatCard.classList.add('selected'); + this.selectedFormat = format.key; + this.exportSettings.format = format.key; + + this.updateLayoutMode(); + this.updateSettingsArea(); + this.updatePreviewVisibility(); + + if (format.key === 'pdf') { + this.startRenderPreview(); + } else { + this.renderedBook = null; + this.cleanupWebview(); + } + + this.updateExportButtonState(); + }); + }); + } + + private createSettingsArea(container: HTMLElement) { + const settingsSection = container.createDiv({ cls: 'export-settings-section' }); + + settingsSection.createEl('h4', { + text: '导出设置', + cls: 'export-section-title' + }); + + this.settingsContainer = settingsSection.createDiv({ cls: 'export-settings-content' }); + + const placeholder = this.settingsContainer.createDiv({ cls: 'export-settings-placeholder' }); + placeholder.innerHTML = ` +
⚙️
+
请先选择导出格式
+ `; + } + + private updateSettingsArea() { + this.settingsContainer.empty(); + + if (!this.selectedFormat) { + const placeholder = this.settingsContainer.createDiv({ cls: 'export-settings-placeholder' }); + placeholder.innerHTML = ` +
⚙️
+
请先选择导出格式
+ `; + return; + } + + const settingsGrid = this.settingsContainer.createDiv({ cls: 'export-settings-grid' }); + + this.createCommonSettings(settingsGrid); + + if (this.selectedFormat === 'pdf') { + this.createPdfSettings(settingsGrid); + } else if (this.selectedFormat === 'html') { + this.createHtmlSettings(settingsGrid); + } else if (this.selectedFormat === 'docx') { + this.createDocxSettings(settingsGrid); + } + } + + private createCommonSettings(container: HTMLElement) { + if (['pdf', 'docx'].includes(this.selectedFormat!)) { + const sizeCard = container.createDiv({ cls: 'export-setting-card' }); + + const sizeHeader = sizeCard.createDiv({ cls: 'export-setting-header' }); + sizeHeader.innerHTML = ` + 📏 + 开本大小 + `; + + const sizeSelect = sizeCard.createEl('select', { cls: 'export-setting-select' }); + const sizes = ['A4', 'A5', 'A3', 'Letter', 'Legal', 'Tabloid']; + sizes.forEach(size => { + const option = sizeSelect.createEl('option', { value: size, text: size }); + if (size === 'A4') option.selected = true; + }); + + sizeSelect.addEventListener('change', () => { + this.exportSettings.bookSize = sizeSelect.value; + }); + } + } + + private createPdfSettings(container: HTMLElement) { + // PDF 特定设置 + const coverCard = container.createDiv({ cls: 'export-setting-card' }); + + const coverHeader = coverCard.createDiv({ cls: 'export-setting-header' }); + coverHeader.innerHTML = ` + 🎨 + 封面设置 + `; + + const coverToggle = coverCard.createDiv({ cls: 'export-setting-toggle' }); + const coverCheckbox = coverToggle.createEl('input', { type: 'checkbox' }); + coverToggle.createEl('label', { text: '包含封面' }); + + coverCheckbox.addEventListener('change', () => { + this.exportSettings.showCover = coverCheckbox.checked; + }); + } + + private createHtmlSettings(container: HTMLElement) { + // HTML 特定设置 + } + + private createDocxSettings(container: HTMLElement) { + // DOCX 特定设置 + } + + private async getOutputFile(filename: string): Promise { + try { + // @ts-ignore + const result = await electron.remote.dialog.showSaveDialog({ + title: '导出 PDF', + defaultPath: `${filename}.pdf`, + filters: [ + { name: 'PDF Files', extensions: ['pdf'] }, + { name: 'All Files', extensions: ['*'] } + ], + properties: ['showOverwriteConfirmation', 'createDirectory'] + }); + + if (result.canceled) { + return undefined; + } + return result.filePath; + } catch (error) { + console.error('Error showing save dialog:', error); + new Notice('无法打开保存对话框'); + return undefined; + } + } + + private async exportToPdf() { + if (!this.webview || !this.webviewReady || !this.renderedBook) { + new Notice('PDF 预览未准备就绪,请稍候'); + return; + } + + try { + const filename = this.selectedBook.basic.title || 'exported-book'; + const outputFile = await this.getOutputFile(filename); + + if (!outputFile) { + return; // 用户取消了保存 + } + + // PDF 导出选项 + const printOptions: electron.PrintToPDFOptions = { + pageSize: this.exportSettings.bookSize as any || 'A4', + printBackground: true, + landscape: false, + scale: this.renderSettings.scale / 100, + margins: { + marginType: 'default' + }, + displayHeaderFooter: this.renderSettings.displayHeader || this.renderSettings.displayFooter, + headerTemplate: this.renderSettings.displayHeader ? '' : '', + footerTemplate: this.renderSettings.displayFooter ? '' : '' + }; + + + // 使用 webview 生成 PDF + const pdfBuffer = await this.webview.printToPDF(printOptions); + + // 保存文件 + await fs.writeFile(outputFile, pdfBuffer); + + new Notice('PDF 导出成功!'); + + // 询问是否打开文件 + const shouldOpen = confirm('PDF 导出成功!是否打开文件?'); + if (shouldOpen) { + // @ts-ignore + electron.remote.shell.openPath(outputFile); + } + + this.close(); + } catch (error) { + console.error('PDF export failed:', error); + new Notice('PDF 导出失败: ' + error.message); + } + } + + private async exportToTxt() { + // TXT 导出逻辑 + new Notice('TXT 导出功能开发中...'); + } + + private async exportToDocx() { + // DOCX 导出逻辑 + new Notice('DOCX 导出功能开发中...'); + } + + private async handleExport() { + if (!this.selectedFormat) { + new Notice('请先选择导出格式'); + return; + } + + switch (this.selectedFormat) { + case 'pdf': + await this.exportToPdf(); + break; + case 'txt': + await this.exportToTxt(); + break; + case 'docx': + await this.exportToDocx(); + break; + default: + new Notice('不支持的导出格式'); + } + } + + onClose() { + this.stopRendering(); + this.cleanupWebview(); + const { contentEl } = this; + contentEl.empty(); + } } \ No newline at end of file diff --git a/src/modals/PreviewModal.ts b/src/modals/PreviewModal.ts new file mode 100644 index 0000000..c435e06 --- /dev/null +++ b/src/modals/PreviewModal.ts @@ -0,0 +1,40 @@ +import { App, Modal } from "obsidian"; +import { RenderedBook } from "../services/BookRenderService"; + +export class PreviewModal extends Modal { + constructor( + app: App, + private renderedBook: RenderedBook + ) { + super(app); + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass('preview-modal'); + + // 创建预览容器 + const previewContainer = contentEl.createDiv({ cls: 'preview-container' }); + + // 添加渲染的文档内容 + const docClone = this.renderedBook.doc.cloneNode(true) as Document; + const bodyContent = docClone.body; + + if (bodyContent) { + previewContainer.appendChild(bodyContent); + } + + // 添加关闭按钮 + const closeBtn = contentEl.createEl('button', { + text: '关闭预览', + cls: 'preview-close-btn' + }); + closeBtn.addEventListener('click', () => this.close()); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} \ No newline at end of file diff --git a/src/services/BookRenderService.ts b/src/services/BookRenderService.ts new file mode 100644 index 0000000..ab87150 --- /dev/null +++ b/src/services/BookRenderService.ts @@ -0,0 +1,759 @@ +import { App, Component, MarkdownRenderer, MarkdownView, TFile, Notice } from 'obsidian'; +import { Book, ChapterNode } from 'src/types/book'; +import * as electron from 'electron'; + +export interface RenderConfig { + showTitle: boolean; + scale: number; + displayHeader: boolean; + displayFooter: boolean; + cssSnippet?: string; + abortSignal?: AbortSignal; + onProgress?: (current: number, total: number, fileName: string) => void; +} + +export interface RenderedChapter { + file: TFile; + title: string; + content: HTMLElement; + frontMatter: any; +} + +export interface RenderedBook { + doc: Document; + title: string; + frontMatter: any; + chapters: RenderedChapter[]; +} + +export interface WebviewContent { + doc: Document; + styles: string[]; + webviewJs: string; +} + +export class BookRenderService { + constructor(private app: App) { } + + /** + * 渲染整本书籍 + */ + async renderBook(book: Book, rootPath: String, config: RenderConfig): Promise { + const startTime = new Date().getTime(); + + // 检查是否已被中断 + if (config.abortSignal?.aborted) { + throw new Error('Render aborted'); + } + + // 创建主文档 + const doc = document.implementation.createHTMLDocument(book.basic.title || 'Book'); + doc.title = book.basic.title || 'Book'; + + // 添加样式 + this.injectStyles(doc, config); + + // 渲染章节 + const chapters: RenderedChapter[] = []; + + // 递归收集所有文件类型的章节节点 + const fileNodes = this.collectFileNodes(book.structure.tree); + const totalFiles = fileNodes.length; + + // 报告开始进度 + config.onProgress?.(0, totalFiles, '开始渲染...'); + + for (let i = 0; i < fileNodes.length; i++) { + const chapterNode = fileNodes[i]; + + // 在每个章节渲染前检查中断信号 + if (config.abortSignal?.aborted) { + throw new Error('Render aborted'); + } + + // 报告当前进度 + config.onProgress?.(i + 1, totalFiles, chapterNode.title || chapterNode.path); + + try { + // 根据路径获取TFile对象 + const filePath = `${rootPath}/${book?.basic.title}/${chapterNode.path}`; + const file = this.app.vault.getAbstractFileByPath(filePath); + if (file instanceof TFile) { + const renderedChapter = await this.renderChapter(file, config); + chapters.push(renderedChapter); + } else { + console.warn(`File not found: ${chapterNode.path}`); + } + } catch (error) { + if (config.abortSignal?.aborted) { + throw new Error('Render aborted'); + } + console.error(`Failed to render chapter: ${chapterNode.path}`, error); + new Notice(`渲染章节失败: ${chapterNode.title}`); + } + } + + // 最后检查一次中断信号 + if (config.abortSignal?.aborted) { + throw new Error('Render aborted'); + } + + // 报告完成进度 + config.onProgress?.(totalFiles, totalFiles, '合并章节中...'); + + // 合并章节到主文档 + this.mergeChaptersToDocument(doc, chapters, config); + + console.log(`Book render time: ${new Date().getTime() - startTime}ms`); + + return { + doc, + title: book.basic.title || 'Book', + frontMatter: book.basic || {}, + chapters + }; + } + + /** + * 为webview准备渲染内容(新增方法) + */ + async prepareWebviewContent(book: Book, rootPath: string, config: RenderConfig): Promise { + // 渲染书籍 + const renderedBook = await this.renderBook(book, rootPath, config); + + // 准备webview所需的样式和脚本 + const styles = this.getAllStyles(); + const webviewJs = this.makeWebviewJs(renderedBook.doc); + + return { + doc: renderedBook.doc, + styles, + webviewJs + }; + } + + /** + * 设置webview内容(新增方法) + */ + async setupWebview(webview: electron.WebviewTag, doc: Document, config?: RenderConfig): Promise { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Webview setup timeout')); + }, 10000); + + webview.addEventListener('dom-ready', async () => { + try { + clearTimeout(timeout); + + // 1. 先注入所有基础样式 + const styles = this.getAllStyles(); + for (const css of styles) { + await webview.insertCSS(css); + } + + // 2. 处理自定义CSS片段(如果有的话) + if (config?.cssSnippet && config.cssSnippet !== '0') { + try { + await webview.insertCSS(config.cssSnippet); + } catch (error) { + console.warn('Failed to load CSS snippet:', error); + } + } + + // 3. 注入内容 + await webview.executeJavaScript(this.makeWebviewJs(doc)); + + // 4. 最后再次注入补丁样式,确保优先级 + const patchStyles = this.getPatchStyles(); + for (const css of patchStyles) { + await webview.insertCSS(css); + } + + resolve(); + } catch (error) { + clearTimeout(timeout); + reject(error); + } + }); + }); + } + + /** + * 生成webview注入脚本(从ExportModal移动过来) + */ + private makeWebviewJs(doc: Document): string { + // 获取当前主题相关的类名 + const currentBodyClasses = Array.from(document.body.classList); + const currentHtmlClasses = Array.from(document.documentElement.classList); + + // 获取重要的data属性 + const themeAttr = document.documentElement.getAttribute('data-theme') || ''; + const modeAttr = document.documentElement.getAttribute('data-mode') || ''; + + return ` + // 设置基本内容 + document.body.innerHTML = decodeURIComponent(\`${encodeURIComponent(doc.body.innerHTML)}\`); + document.head.innerHTML = decodeURIComponent(\`${encodeURIComponent(doc.head.innerHTML)}\`); + + // 复制原始属性 + document.body.setAttribute("class", \`${doc.body.getAttribute("class") || ''}\`); + document.body.setAttribute("style", \`${doc.body.getAttribute("style") || ''}\`); + + // 动态应用当前主题的类名到body + ${currentBodyClasses.map(cls => + cls.includes('theme-') || cls.includes('dark') || cls.includes('light') || cls.includes('obsidian-app') + ? `document.body.classList.add('${cls}');` + : '' + ).filter(Boolean).join('\n ')} + + // 动态应用当前主题的类名到html + ${currentHtmlClasses.map(cls => + `document.documentElement.classList.add('${cls}');` + ).join('\n ')} + + // 设置重要的data属性 + ${themeAttr ? `document.documentElement.setAttribute('data-theme', '${themeAttr}');` : ''} + ${modeAttr ? `document.documentElement.setAttribute('data-mode', '${modeAttr}');` : ''} + + document.title = \`${doc.title}\`; + `; + } + + /** + * 递归收集所有文件类型的章节节点 + */ + private collectFileNodes(nodes: ChapterNode[]): ChapterNode[] { + const fileNodes: ChapterNode[] = []; + + for (const node of nodes) { + // 跳过被排除的节点 + if (node.exclude) { + continue; + } + + if (node.type === 'file') { + fileNodes.push(node); + } else if (node.type === 'group' && node.children) { + // 递归处理子节点 + fileNodes.push(...this.collectFileNodes(node.children)); + } + } + + // 按order排序 + return fileNodes.sort((a, b) => a.order - b.order); + } + + /** + * 渲染单个章节 + */ + async renderChapter(file: TFile, config: RenderConfig): Promise { + // 检查中断信号 + if (config.abortSignal?.aborted) { + throw new Error('Render aborted'); + } + + const ws = this.app.workspace; + + // 创建临时叶子来渲染文件 + const leaf = ws.getLeaf(true); + await leaf.openFile(file); + const view = leaf.view as MarkdownView; + + // 获取文件内容 + const data: string = view?.data || await this.app.vault.cachedRead(file); + + // 获取前置元数据 + const frontMatter = this.getFrontMatter(file); + + // 获取CSS类 + const cssclasses = this.extractCssClasses(frontMatter); + + // 创建组件 + const comp = new Component(); + comp.load(); + + try { + // 再次检查中断信号 + if (config.abortSignal?.aborted) { + throw new Error('Render aborted'); + } + + // 在当前文档环境中完成渲染 + const printEl = document.body.createDiv('print'); + const viewEl = printEl.createDiv({ + cls: 'markdown-preview-view markdown-rendered ' + cssclasses.join(' ') + }); + + // 设置RTL和属性显示 + // @ts-ignore + viewEl.toggleClass('rtl', this.app.vault.getConfig('rightToLeft')); + // @ts-ignore + viewEl.toggleClass('show-properties', 'hidden' !== this.app.vault.getConfig('propertiesInDocument')); + + // 添加标题 + const title = frontMatter?.title || file.basename; + viewEl.createEl('h1', { text: title }, (e) => { + e.addClass('__title__'); + e.style.display = config.showTitle ? 'block' : 'none'; + }); + + // 处理块引用 - 如果数据为空,使用空字符串 + const processedData = this.processBlockReferences(data || '', file); + + // 渲染Markdown + await this.renderMarkdownContent(processedData, viewEl, file, comp); + + // 检查中断信号 + if (config.abortSignal?.aborted) { + throw new Error('Render aborted'); + } + + // 后处理 + await this.postProcessContent(viewEl, file, comp); + await this.fixRenderedContent(data || '', viewEl); + + // 在当前环境中完成所有渲染后,克隆内容 + const clonedContent = viewEl.cloneNode(true) as HTMLElement; + + // 清理当前 document 中的临时元素 + printEl.detach(); + printEl.remove(); + leaf.detach(); + + return { + file, + title, + content: clonedContent, + frontMatter + }; + + } finally { + comp.unload(); + } + } + + /** + * 获取前置元数据 + */ + private getFrontMatter(file: TFile) { + const cache = this.app.metadataCache.getFileCache(file); + return cache?.frontmatter || {}; + } + + /** + * 提取CSS类 + */ + private extractCssClasses(frontMatter: any): string[] { + const cssclasses: string[] = []; + for (const [key, val] of Object.entries(frontMatter)) { + if (key.toLowerCase() === 'cssclass' || key.toLowerCase() === 'cssclasses') { + if (Array.isArray(val)) { + cssclasses.push(...val); + } else { + cssclasses.push(val as string); + } + } + } + return cssclasses; + } + + /** + * 处理块引用 + */ + private processBlockReferences(data: string, file: TFile): string { + const cache = this.app.metadataCache.getFileCache(file); + const blocks = new Map(Object.entries(cache?.blocks || {})); + + const lines = data.split('\n').map((line, i) => { + for (const { id, position: { start, end } } of blocks.values()) { + const blockid = `^${id}`; + if (line.includes(blockid) && i >= start.line && i <= end.line) { + blocks.delete(id); + return line.replace(blockid, ` ${blockid}`); + } + } + return line; + }); + + // 添加剩余的块引用 + [...blocks.values()].forEach(({ id, position: { start } }) => { + const idx = start.line; + lines[idx] = `\n\n` + lines[idx]; + }); + + return lines.join('\n'); + } + + /** + * 渲染Markdown内容 + */ + private async renderMarkdownContent(data: string, viewEl: HTMLElement, file: TFile, comp: Component) { + const fragment = { + children: undefined, + appendChild(e: DocumentFragment) { + this.children = e?.children; + throw new Error('exit'); + } + } as unknown as HTMLElement; + + try { + await MarkdownRenderer.render(this.app, data, fragment, file.path, comp); + } catch (error) { + // 预期的错误,用于跳过postProcess + } + + const el = createFragment(); + Array.from(fragment.children).forEach((item) => { + el.createDiv({}, (t) => { + return t.appendChild(item); + }); + }); + + viewEl.appendChild(el); + } + + /** + * 后处理内容 + */ + private async postProcessContent(viewEl: HTMLElement, file: TFile, comp: Component) { + const promises: Array<() => Promise> = []; + //@ts-ignore + await MarkdownRenderer.postProcess(this.app, { + docId: this.generateDocId(16), + sourcePath: file.path, + frontmatter: {}, + promises, + addChild: function (e: Component) { + return comp.addChild(e); + }, + getSectionInfo: function () { + return null; + }, + containerEl: viewEl, + el: viewEl, + displayMode: true + }); + + await Promise.all(promises); + } + + /** + * 修复渲染内容 + */ + private async fixRenderedContent(data: string, viewEl: HTMLElement) { + // 修复内部链接 + viewEl.findAll('a.internal-link').forEach((el: HTMLAnchorElement) => { + const [title, anchor] = el.dataset.href?.split('#') || []; + if ((!title || title?.length === 0) && anchor?.startsWith('^')) { + return; + } + el.removeAttribute('href'); + }); + + // 等待动态内容渲染 + try { + await this.waitForDynamicContent(data, viewEl); + } catch (error) { + console.warn('Wait timeout for dynamic content'); + } + + // 修复Canvas为图片 + this.fixCanvasToImage(viewEl); + } + + /** + * 等待动态内容渲染 + */ + private async waitForDynamicContent(data: string, viewEl: HTMLElement) { + if (data.includes('```dataview') || data.includes('```gEvent') || data.includes('![[')) { + await this.sleep(2000); + } + + try { + await this.waitForDomChange(viewEl); + } catch (error) { + await this.sleep(1000); + } + } + + /** + * 修复Canvas为图片 + */ + private fixCanvasToImage(el: HTMLElement) { + for (const canvas of Array.from(el.querySelectorAll('canvas'))) { + const data = canvas.toDataURL(); + const img = document.createElement('img'); + img.src = data; + img.className = '__canvas__'; + + // 复制属性 + Array.from(canvas.attributes).forEach(attr => { + img.setAttribute(attr.name, attr.value); + }); + + canvas.replaceWith(img); + } + } + + /** + * 合并章节到文档 + */ + private mergeChaptersToDocument(doc: Document, chapters: RenderedChapter[], config: RenderConfig) { + const body = doc.body; + body.innerHTML = ''; + + // 添加打印容器,保持与当前文档相同的类名结构 + const printEl = body.appendChild(doc.createElement('div')); + printEl.className = 'print'; + + // 添加主容器,确保包含所有必要的类名 + const mainEl = printEl.appendChild(doc.createElement('div')); + mainEl.className = 'markdown-preview-view markdown-rendered'; + + // 复制当前文档中markdown-preview-view的相关类名 + const currentPreviewEl = document.querySelector('.markdown-preview-view'); + if (currentPreviewEl) { + Array.from(currentPreviewEl.classList).forEach(className => { + if (!mainEl.classList.contains(className)) { + mainEl.classList.add(className); + } + }); + } + + // 添加每个章节 + chapters.forEach((chapter, index) => { + const section = doc.createElement('section'); + section.className = 'book-chapter'; + section.setAttribute('data-chapter', index.toString()); + + // 导入章节内容 + Array.from(chapter.content.children).forEach(child => { + const importedNode = doc.importNode(child, true); + section.appendChild(importedNode); + }); + + mainEl.appendChild(section); + + // 添加分页符(除了最后一章) + // if (index < chapters.length - 1) { + // const pageBreak = doc.createElement('div'); + // pageBreak.style.pageBreakAfter = 'always'; + // pageBreak.style.breakAfter = 'page'; + // pageBreak.className = 'page-break'; + // mainEl.appendChild(pageBreak); + // } + }); + } + + /** + * 注入样式 + */ + private injectStyles(doc: Document, config?: RenderConfig) { + const head = doc.head; + const body = doc.body; + + // 复制当前文档的主题相关类名到目标文档 + const bodyClasses = Array.from(document.body.classList); + bodyClasses.forEach(className => { + // 复制所有主题相关的类名 + if (className.includes('theme-') || + className.includes('dark') || + className.includes('light') || + className.includes('obsidian-app')) { + body.classList.add(className); + } + }); + + // 复制html元素的类名和属性 + const htmlClasses = Array.from(document.documentElement.classList); + htmlClasses.forEach(className => { + doc.documentElement.classList.add(className); + }); + + // 复制重要的data属性 + const htmlElement = document.documentElement; + ['data-theme', 'data-mode'].forEach(attr => { + const value = htmlElement.getAttribute(attr); + if (value) { + doc.documentElement.setAttribute(attr, value); + } + }); + + // 获取所有样式 + const styles = this.getAllStyles(); + + // 创建样式元素 + const styleEl = doc.createElement('style'); + styleEl.textContent = styles.join('\n'); + head.appendChild(styleEl); + + // 处理自定义CSS片段 + if (config?.cssSnippet && config.cssSnippet !== '0') { + const customStyleEl = doc.createElement('style'); + customStyleEl.textContent = config.cssSnippet; + head.appendChild(customStyleEl); + } + } + + /** + * 获取所有样式 + */ + private getAllStyles(): string[] { + const cssTexts: string[] = []; + + // 添加主题相关的样式注释 + cssTexts.push('/* ---------- Obsidian Theme Styles ---------- */'); + + Array.from(document.styleSheets).forEach((sheet) => { + // @ts-ignore + const id = sheet.ownerNode?.id; + // @ts-ignore + const href = sheet.ownerNode?.href; + + // 跳过Svelte样式,但保留所有主题相关样式 + if (id?.startsWith('svelte-')) { + return; + } + + const division = `/* ----------${id ? `id:${id}` : href ? `href:${href}` : 'inline'}---------- */`; + cssTexts.push(division); + + try { + Array.from(sheet?.cssRules || []).forEach((rule) => { + cssTexts.push(rule.cssText); + }); + } catch (error) { + console.error('Error reading CSS rules:', error); + // 对于跨域样式表,尝试获取基本信息 + if (href) { + cssTexts.push(`/* External stylesheet: ${href} */`); + } + } + }); + + // 获取当前主题的body类名并添加相关样式 + const bodyClasses = Array.from(document.body.classList); + const themeClasses = bodyClasses.filter(cls => + cls.includes('theme-') || + cls.includes('dark') || + cls.includes('light') + ); + + if (themeClasses.length > 0) { + cssTexts.push(`/* ---------- Current Theme Classes: ${themeClasses.join(', ')} ---------- */`); + } + + // 添加补丁样式 + cssTexts.push(...this.getPatchStyles()); + + return cssTexts; + } + + /** + * 获取补丁样式 + */ + private getPatchStyles(): string[] { + const patchCSS = ` + /* ---------- css patch ---------- */ + body { + overflow: auto !important; + } + + @media print { + .print .markdown-preview-view { + height: auto !important; + } + + .md-print-anchor, .blockid { + white-space: pre !important; + border: none !important; + display: inline-block !important; + position: absolute !important; + width: 1px !important; + height: 1px !important; + right: 0 !important; + outline: 0 !important; + background: 0 0 !important; + text-decoration: initial !important; + text-shadow: initial !important; + } + + table { + break-inside: auto; + } + + tr { + break-inside: avoid; + break-after: auto; + } + } + + img.__canvas__ { + width: 100% !important; + height: 100% !important; + } + + .book-chapter { + margin-bottom: 2em; + } + `; + + return [patchCSS, ...this.getPrintStyles()]; + } + + private getPrintStyles(): string[] { + const printStyles: string[] = []; + + Array.from(document.styleSheets).forEach((sheet) => { + try { + Array.from(sheet?.cssRules || []).forEach((rule) => { + if (rule instanceof CSSMediaRule && rule.media.mediaText.includes('print')) { + Array.from(rule.cssRules).forEach((printRule) => { + printStyles.push(printRule.cssText); + }); + } + }); + } catch (error) { + console.error('Error reading print CSS rules:', error); + } + }); + + return printStyles; + } + + /** + * 工具方法 + */ + private generateDocId(n: number): string { + return Array.from({ length: n }, () => ((16 * Math.random()) | 0).toString(16)).join(''); + } + + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + private waitForDomChange(target: HTMLElement, timeout = 2000, interval = 200): Promise { + return new Promise((resolve, reject) => { + let timer: NodeJS.Timeout; + const observer = new MutationObserver(() => { + clearTimeout(timer); + timer = setTimeout(() => { + observer.disconnect(); + resolve(true); + }, interval); + }); + + observer.observe(target, { + childList: true, + subtree: true, + attributes: true, + characterData: true + }); + + setTimeout(() => { + observer.disconnect(); + reject(new Error(`timeout ${timeout}ms`)); + }, timeout); + }); + } +} \ No newline at end of file diff --git a/src/styles/index.css b/src/styles/index.css index 61f1a18..bf43446 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -11,6 +11,7 @@ @import './modals/manage-book-modal.css'; @import './modals/unimported-books-modal.css'; @import './modals/template-modal.css'; +@import './modals/book-selection-modal.css'; /* 新增 */ @import './views/tools-view.css'; @import './tools/focus-tool.css'; @import './modals/base-modal.css'; diff --git a/src/styles/modals/book-selection-modal.css b/src/styles/modals/book-selection-modal.css new file mode 100644 index 0000000..f02f8ce --- /dev/null +++ b/src/styles/modals/book-selection-modal.css @@ -0,0 +1,76 @@ +/* 书籍选择模态框样式 */ +.book-selection-list { + max-height: 400px; + overflow-y: auto; + margin: 20px 0; + border: 1px solid var(--background-modifier-border); + border-radius: 6px; +} + +.book-selection-item { + display: flex; + align-items: flex-start; + padding: 12px; + border-bottom: 1px solid var(--background-modifier-border); + cursor: pointer; + transition: background-color 0.2s; +} + +.book-selection-item:hover { + background-color: var(--background-modifier-hover); +} + +.book-selection-item.selected { + background-color: var(--background-modifier-active-hover); +} + +.book-selection-item:last-child { + border-bottom: none; +} + +.book-selection-radio { + margin-right: 12px; + margin-top: 2px; +} + +.book-selection-info { + flex: 1; +} + +.book-selection-title { + margin: 0 0 8px 0; + font-size: 16px; + font-weight: 600; + color: var(--text-normal); +} + +.book-selection-details { + display: flex; + flex-direction: column; + gap: 4px; +} + +.book-selection-details span { + font-size: 12px; + color: var(--text-muted); +} + +.book-selection-empty { + text-align: center; + padding: 40px 20px; + color: var(--text-muted); +} + +.book-selection-hint { + font-size: 12px; + margin-top: 8px; +} + +.book-selection-buttons { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid var(--background-modifier-border); +} \ No newline at end of file diff --git a/src/styles/modals/export-modal.css b/src/styles/modals/export-modal.css index a14f4c2..34d7015 100644 --- a/src/styles/modals/export-modal.css +++ b/src/styles/modals/export-modal.css @@ -1,135 +1,728 @@ -/* 导出模态框样式 */ -.book-smith-modal-content { - padding: 20px; +/* 导出模态框主体样式 */ +.export-modal { + --export-primary: var(--interactive-accent); + --export-primary-hover: var(--interactive-accent-hover); + --export-secondary: var(--background-secondary); + --export-border: var(--background-modifier-border); + --export-radius: 8px; + --export-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); +} + +.export-modal .modal { + max-width: 50vw; + width: 50vw; + height: 70vh; + max-height: 70vh; +} + +/* 主要内容区域 - 支持动态布局切换 */ +.export-modal-main { + display: flex; + gap: 20px; + height: calc(70vh - 120px); + transition: all 0.3s ease; +} + +/* 未选择格式时的居中布局 */ +.export-modal-main.centered { + flex-direction: column; + align-items: center; + justify-content: flex-start; + max-width: 600px; + margin: 0 auto; +} + +/* 选择格式后的左右分栏布局 */ +.export-modal-main.split-layout { + flex-direction: row; + align-items: stretch; +} + +/* 右侧设置面板 */ +.export-settings-panel { + flex: 0 0 400px; display: flex; flex-direction: column; gap: 20px; + overflow-y: auto; + padding-right: 8px; + transition: all 0.3s ease; } -/* 标题样式 */ -.book-smith-modal-header h2 { - font-size: 18px; - text-align: center; - margin: 0; - padding-bottom: 10px; - color: var(--text-normal); +/* 居中布局时的设置面板 */ +.export-modal-main.centered .export-settings-panel { + flex: none; + width: 100%; + max-width: 600px; + padding-right: 0; } -/* 导出内容区域 */ -.export-content { +/* 左侧预览面板 */ +.export-preview-panel { + flex: 1; display: flex; flex-direction: column; - gap: 15px; - padding: 0; + border: 1px solid var(--export-border); + border-radius: var(--export-radius); + background: var(--background-primary); + overflow: hidden; + transition: all 0.3s ease; } -/* 导出标题 */ -.export-title { +/* 居中布局时隐藏预览面板 */ +.export-modal-main.centered .export-preview-panel { + display: none; +} + +.preview-header { + padding: 16px 20px; + border-bottom: 1px solid var(--export-border); + background: var(--background-secondary); +} + +.preview-title { + margin: 0; + font-size: 16px; + font-weight: 600; + color: var(--text-normal); +} + +.preview-content { + flex: 1; + overflow: auto; + padding: 20px; + scroll-behavior: smooth; +} + +/* 预览状态样式 - 合并相似的状态样式 */ +.preview-placeholder, +.preview-loading, +.preview-error, +.preview-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: var(--text-muted); +} + +.preview-placeholder-icon, +.preview-error-icon, +.preview-empty-icon { + font-size: 48px; + margin-bottom: 16px; + opacity: 0.6; +} + +.preview-placeholder-text, +.preview-error-text, +.preview-empty-text { font-size: 16px; text-align: center; +} + +.preview-loading-spinner { + width: 32px; + height: 32px; + border: 3px solid var(--background-modifier-border); + border-top: 3px solid var(--export-primary); + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 16px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.preview-loading-text { + font-size: 16px; +} + +/* 渲染内容包装器 */ +.rendered-content-wrapper { + width: 100%; + height: 100%; + max-width: 100%; + overflow-x: auto; + position: relative; +} + +.rendered-content-wrapper .markdown-preview-view { + padding: 0; + margin: 0; +} + +/* 模态框头部 */ +.export-modal-header { + position: relative; + padding: 24px 24px 16px; + border-bottom: 1px solid var(--export-border); + background: linear-gradient(135deg, var(--background-primary) 0%, var(--background-secondary) 100%); +} + +.export-modal-title { + margin: 0; + font-size: 24px; + font-weight: 700; color: var(--text-normal); + text-align: center; +} + +/* 隐藏关闭按钮 */ +.export-modal-close { + display: none; +} + +/* 模态框内容 */ +.export-modal-content { + padding: 24px; + max-height: calc(70vh - 120px); + overflow-y: auto; +} + +/* 模态框底部 */ +.export-modal-footer { + padding: 16px 24px; + border-top: 1px solid var(--export-border); + background: var(--background-primary); + display: flex; + justify-content: flex-end; +} + +/* 书籍信息卡片 */ +.export-book-card { + display: flex; + align-items: center; + gap: 16px; + padding: 20px; + background: var(--export-secondary); + border-radius: var(--export-radius); + border: 1px solid var(--export-border); + margin-bottom: 24px; + transition: all 0.2s ease; +} + +.export-book-card:hover { + box-shadow: var(--export-shadow); + transform: translateY(-2px); +} + +.export-book-icon { + font-size: 32px; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: var(--export-primary); + border-radius: 50%; + color: white; +} + +.export-book-details { + flex: 1; +} + +.export-book-title { + margin: 0 0 8px 0; + font-size: 18px; + font-weight: 600; + color: var(--text-normal); +} + +.export-book-author { + margin: 0 0 4px 0; + font-size: 14px; + color: var(--text-muted); +} + +.export-book-desc { + margin: 0; + font-size: 13px; + color: var(--text-faint); + line-height: 1.4; +} + +/* 章节标题 */ +.export-section-title { + margin: 0 0 16px 0; + font-size: 16px; + font-weight: 600; + color: var(--text-normal); + display: flex; + align-items: center; + gap: 8px; +} + +.export-section-title::before { + content: ''; + width: 4px; + height: 16px; + background: var(--export-primary); + border-radius: 2px; +} + +/* 格式选择区域 */ +.export-format-section { + margin-bottom: 32px; +} + +.export-format-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 12px; +} + +.export-format-card { + padding: 16px; + border: 2px solid var(--export-border); + border-radius: var(--export-radius); + background: var(--background-primary); + cursor: pointer; + transition: all 0.3s ease; + text-align: center; + position: relative; + overflow: hidden; +} + +.export-format-card::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); + transition: left 0.5s ease; +} + +.export-format-card:hover { + border-color: var(--export-primary); + transform: translateY(-4px); + box-shadow: var(--export-shadow); +} + +.export-format-card:hover::before { + left: 100%; +} + +.export-format-card.selected { + border-color: var(--export-primary); + background: var(--export-primary); + color: white; + transform: translateY(-4px); + box-shadow: var(--export-shadow); +} + +.export-format-icon { + font-size: 24px; + margin-bottom: 8px; +} + +.export-format-label { + font-size: 14px; + font-weight: 600; + margin-bottom: 4px; +} + +.export-format-desc { + font-size: 12px; + opacity: 0.8; +} + +/* 设置区域 */ +.export-settings-section { + margin-bottom: 32px; +} + +.export-settings-content { + min-height: 120px; +} + +.export-settings-placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px; + color: var(--text-muted); + text-align: center; +} + +.export-placeholder-icon { + font-size: 32px; + margin-bottom: 12px; + opacity: 0.5; +} + +.export-placeholder-text { + font-size: 14px; +} + +.export-settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 16px; +} + +/* 设置卡片 */ +.export-setting-card { + padding: 20px; + border: 1px solid var(--export-border); + border-radius: var(--export-radius); + background: var(--background-primary); + transition: all 0.2s ease; +} + +.export-setting-card:hover { + border-color: var(--export-primary); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); +} + +.export-setting-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; +} + +.export-setting-icon { + font-size: 16px; +} + +.export-setting-title { + font-size: 14px; + font-weight: 600; + color: var(--text-normal); +} + +/* 优化后的选择框样式 */ +.export-setting-select { + width: 100%; + padding: 12px 16px; + border: 2px solid var(--export-border); + border-radius: 6px; + background: var(--background-primary); + color: var(--text-normal); + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + appearance: none; + background-image: url('data:image/svg+xml;charset=US-ASCII,'); + background-repeat: no-repeat; + background-position: right 12px center; + background-size: 12px; + padding-right: 40px; +} + +.export-setting-select:hover { + border-color: var(--export-primary); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + transform: translateY(-1px); +} + +.export-setting-select:focus { + outline: none; + border-color: var(--export-primary); + box-shadow: 0 0 0 3px rgba(var(--export-primary), 0.1); + transform: translateY(-1px); +} + +.export-setting-select option { + padding: 8px 12px; + background: var(--background-primary); + color: var(--text-normal); +} + +.export-setting-toggle { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; +} + +.export-toggle-input { + width: 16px; + height: 16px; + cursor: pointer; +} + +.export-toggle-label { + font-size: 14px; + color: var(--text-normal); + cursor: pointer; +} + +.export-setting-button { + width: 100%; + padding: 8px 16px; + border: 1px solid var(--export-border); + border-radius: 4px; + background: var(--export-secondary); + color: var(--text-normal); + font-size: 13px; + cursor: pointer; + transition: all 0.2s ease; +} + +.export-setting-button:hover { + background: var(--background-modifier-hover); + border-color: var(--export-primary); +} + +/* 信息卡片 */ +.export-info-card { + background: var(--background-secondary); + border-color: var(--text-accent); +} + +.export-info-text { + font-size: 13px; + color: var(--text-muted); + line-height: 1.4; +} + +/* 按钮组 */ +.export-button-group { + display: flex; + gap: 12px; +} + +.export-btn { + padding: 12px 24px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + min-width: 80px; + position: relative; + overflow: hidden; +} + +.export-btn::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + background: rgba(255, 255, 255, 0.2); + border-radius: 50%; + transform: translate(-50%, -50%); + transition: width 0.3s ease, height 0.3s ease; +} + +.export-btn:active::before { + width: 300px; + height: 300px; +} + +.export-btn-secondary { + background: var(--export-secondary); + color: var(--text-normal); + border: 1px solid var(--export-border); +} + +.export-btn-secondary:hover { + background: var(--background-modifier-hover); + border-color: var(--export-primary); + transform: translateY(-2px); +} + +.export-btn-primary { + background: var(--export-primary); + color: white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.export-btn-primary:hover { + background: var(--export-primary-hover); + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); +} + +.export-btn-primary:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +/* 导出按钮禁用状态 */ +.export-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + +/* 隐藏旧的action-section */ +.export-action-section { + display: none; +} + +/* 进度条样式 */ +.preview-progress { + margin-top: 20px; + width: 100%; +} + +.progress-bar { + width: 100%; + height: 8px; + background-color: var(--background-modifier-border); + border-radius: 4px; + overflow: hidden; + margin-bottom: 10px; +} + +.progress-fill { + height: 100%; + background-color: var(--interactive-accent); + transition: width 0.3s ease; + border-radius: 4px; +} + +.progress-text { + font-size: 14px; + color: var(--text-muted); + text-align: center; margin-bottom: 5px; } -/* 分隔线 */ -.export-divider { - border: none; - height: 1px; - background-color: var(--background-modifier-border); - margin: 0 0 10px 0; -} - -/* 格式选择容器 */ -.export-formats-container { - display: flex; - flex-wrap: wrap; - gap: 10px; - justify-content: center; - margin: 10px; -} - -/* 格式按钮样式 */ -.export-format-btn { - padding: 12px 20px; - background-color: var(--background-secondary); - color: var(--text-normal); - border: none; - border-radius: 8px; - cursor: pointer; - font-weight: 500; - transition: all 0.3s ease; - font-size: 0.95em; -} - -.export-format-btn:hover { - transform: translateY(-2px); - box-shadow: 0 2px 8px var(--background-modifier-box-shadow); - background-color: var(--background-modifier-border); -} - -.export-format-btn.selected { - background-color: var(--interactive-accent); - color: var(--text-on-accent); - transform: translateY(-2px); - box-shadow: 0 2px 8px var(--background-modifier-box-shadow); -} - -/* 按钮容器 */ -.export-buttons-container { - display: flex; - justify-content: right; - gap: 10px; -} - -/* 取消按钮 */ -.export-cancel-btn { - padding: 10px 16px; - background-color: var(--background-secondary); - color: var(--text-normal); - border: none; - border-radius: 6px; - cursor: pointer; - font-weight: 500; - transition: all 0.3s ease; +.progress-file { + font-size: 12px; + color: var(--text-faint); text-align: center; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.export-cancel-btn:hover { - background-color: var(--background-modifier-hover); - transform: translateY(-2px); +/* 预览文档样式 */ +.preview-document { + font-size: 14px; + max-height: none !important; + overflow-y: visible !important; + padding: 0 !important; } -/* 确认按钮 */ -.export-confirm-btn { - padding: 10px 16px; - background-color: var(--interactive-accent-hover); - color: var(--text-on-accent); - border: none; - border-radius: 6px; - cursor: pointer; - font-weight: 500; - transition: all 0.3s ease; - text-align: center; -} - -.export-confirm-btn:hover { - background-color: var(--interactive-accent-hover); - opacity: 0.9; - transform: translateY(-2px); -} - -/* 导出模态框标题 */ -.book-smith-modal-header { - position: relative; - padding-bottom: 10px; - border-bottom: 1px solid var(--background-modifier-border); - margin-bottom: 15px; -} - -/* 选择导出格式标题 */ -.export-title { - font-size: 16px; - text-align: center; - margin: 0 0 15px 0; +.preview-document h1, +.preview-document h2, +.preview-document h3, +.preview-document h4, +.preview-document h5, +.preview-document h6 { color: var(--text-normal); + margin-top: 1em; + margin-bottom: 0.5em; +} + +.preview-document p { + margin-bottom: 1em; +} + +.preview-document img { + max-width: 100%; + height: auto; +} + +/* 滚动条样式 */ +.export-modal-content::-webkit-scrollbar, +.preview-content::-webkit-scrollbar { + width: 6px; +} + +.export-modal-content::-webkit-scrollbar-track, +.preview-content::-webkit-scrollbar-track { + background: var(--background-secondary); + border-radius: 3px; +} + +.export-modal-content::-webkit-scrollbar-thumb, +.preview-content::-webkit-scrollbar-thumb { + background: var(--text-faint); + border-radius: 3px; + transition: background 0.2s ease; +} + +.export-modal-content::-webkit-scrollbar-thumb:hover, +.preview-content::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +/* 响应式设计 */ +@media (max-width: 1200px) { + .export-modal .modal { + width: 85vw; + height: 80vh; + } + + .export-settings-panel { + flex: 0 0 350px; + } +} + +@media (max-width: 900px) { + .export-modal-main { + flex-direction: column; + height: auto; + } + + .export-preview-panel { + height: 400px; + } + + .export-settings-panel { + flex: none; + } +} + +@media (max-width: 768px) { + .export-modal .modal { + width: 95vw; + margin: 10px; + } + + .export-modal-content { + padding: 16px; + } + + .export-modal-footer { + padding: 12px 16px; + } + + .export-format-grid { + grid-template-columns: repeat(2, 1fr); + } + + .export-settings-grid { + grid-template-columns: 1fr; + } + + .export-button-group { + flex-direction: column; + width: 100%; + } + + .export-btn { + width: 100%; + } +} + +@media (max-width: 480px) { + .export-format-grid { + grid-template-columns: 1fr; + } + + .export-book-card { + flex-direction: column; + text-align: center; + } } \ No newline at end of file diff --git a/src/views/ToolsView.ts b/src/views/ToolsView.ts index ea283dc..1ff6411 100644 --- a/src/views/ToolsView.ts +++ b/src/views/ToolsView.ts @@ -7,7 +7,7 @@ import { ContactModal } from '../modals/ContactModal'; import BookSmithPlugin from '../main'; import { i18n } from '../i18n/i18n'; import { TypographyView } from '../components/TypographyView'; - +import { BookSelectionModal } from '../modals/BookSelectionModal'; interface ToolItem { icon: string; text: string; @@ -231,25 +231,7 @@ 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(); + new BookSelectionModal(this.app, this.plugin).open(); } // 修改 onClose 方法,确保所有视图都被正确关闭 From 9430b5bdb140435d812999ddc8df3e493a8a3657 Mon Sep 17 00:00:00 2001 From: Yeban8090 Date: Mon, 7 Jul 2025 23:57:59 +0800 Subject: [PATCH 02/10] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=88=B0=E4=BA=86?= =?UTF-8?q?=E5=BE=88=E6=A3=92=E7=9A=84=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modals/BookSelectionModal.ts | 2 +- src/modals/ExportModal.ts | 270 +-------- src/services/BookRenderService.ts | 894 +++++++++++++----------------- 3 files changed, 416 insertions(+), 750 deletions(-) diff --git a/src/modals/BookSelectionModal.ts b/src/modals/BookSelectionModal.ts index 709a92a..5e7b7df 100644 --- a/src/modals/BookSelectionModal.ts +++ b/src/modals/BookSelectionModal.ts @@ -145,7 +145,7 @@ export class BookSelectionModal extends Modal { try { this.close(); - const bookRenderService = new BookRenderService(this.plugin); + const bookRenderService = new BookRenderService(this.app); // 创建并打开导出模态框 - 修正参数顺序 const exportModal = new ExportModal( this.app, diff --git a/src/modals/ExportModal.ts b/src/modals/ExportModal.ts index 938a7f9..7a08a49 100644 --- a/src/modals/ExportModal.ts +++ b/src/modals/ExportModal.ts @@ -1,6 +1,6 @@ import { Notice, App, Modal, ButtonComponent } from "obsidian"; import { i18n } from "../i18n/i18n"; -import { BookRenderService, RenderConfig, RenderedBook } from "../services/BookRenderService"; +import { BookRenderService, RenderConfig } from "../services/BookRenderService"; import { Book } from "../types/book"; import { HeaderFooterTocModal } from "./HeaderFooterTocModal"; import { CoverSettingModal } from "./CoverSettingModal"; @@ -27,7 +27,6 @@ export class ExportModal extends Modal { private exportBtn: HTMLButtonElement; private isRendering: boolean = false; private abortController: AbortController | null = null; - private renderedBook: RenderedBook | null = null; private webview: electron.WebviewTag | null = null; private webviewReady: boolean = false; @@ -141,208 +140,6 @@ export class ExportModal extends Modal { return webview; } - private getAllStyles(): string[] { - const cssTexts: string[] = []; - - // 添加主题相关的样式注释 - cssTexts.push('/* ---------- Obsidian Theme Styles ---------- */'); - - Array.from(document.styleSheets).forEach((sheet) => { - // @ts-ignore - const id = sheet.ownerNode?.id; - // @ts-ignore - const href = sheet.ownerNode?.href; - - // 跳过Svelte样式,但保留所有主题相关样式 - if (id?.startsWith('svelte-')) { - return; - } - - const division = `/* ----------${id ? `id:${id}` : href ? `href:${href}` : 'inline'}---------- */`; - cssTexts.push(division); - - try { - Array.from(sheet?.cssRules ?? []).forEach((rule) => { - cssTexts.push(rule.cssText); - }); - } catch (error) { - console.error('Error reading CSS rules:', error); - // 对于跨域样式表,尝试获取基本信息 - if (href) { - cssTexts.push(`/* External stylesheet: ${href} */`); - } - } - }); - - // 获取当前主题的body类名并添加相关样式 - const bodyClasses = Array.from(document.body.classList); - const themeClasses = bodyClasses.filter(cls => - cls.includes('theme-') || - cls.includes('dark') || - cls.includes('light') - ); - - if (themeClasses.length > 0) { - cssTexts.push(`/* ---------- Current Theme Classes: ${themeClasses.join(', ')} ---------- */`); - } - - // 添加补丁样式 - cssTexts.push(...this.getPatchStyles()); - - return cssTexts; - } - - private getPatchStyles(): string[] { - const CSS_PATCH = ` - /* ---------- css patch ---------- */ - body { - overflow: auto !important; - } - @media print { - .print .markdown-preview-view { - height: auto !important; - } - .md-print-anchor, .blockid { - white-space: pre !important; - border: none !important; - display: inline-block !important; - position: absolute !important; - width: 1px !important; - height: 1px !important; - right: 0 !important; - outline: 0 !important; - background: 0 0 !important; - text-decoration: initial !important; - text-shadow: initial !important; - } - table { - break-inside: auto; - } - tr { - break-inside: avoid; - break-after: auto; - } - } - img.__canvas__ { - width: 100% !important; - height: 100% !important; - } - .book-chapter { - margin-bottom: 2em; - } - `; - - return [CSS_PATCH, ...this.getPrintStyles()]; - } - - private getPrintStyles(): string[] { - const cssTexts: string[] = []; - - Array.from(document.styleSheets).forEach((sheet) => { - try { - const cssRules = sheet?.cssRules ?? []; - Array.from(cssRules).forEach((rule) => { - if (rule.constructor.name === "CSSMediaRule") { - if ((rule as CSSMediaRule).conditionText === "print") { - const res = rule.cssText.replace(/@media print\s*\{(.+)\}/g, "$1"); - cssTexts.push(res); - } - } - }); - } catch (error) { - console.error('Error reading print styles:', error); - } - }); - - return cssTexts; - } - - private makeWebviewJs(doc: Document): string { - // 获取当前主题相关的类名 - const currentBodyClasses = Array.from(document.body.classList); - const currentHtmlClasses = Array.from(document.documentElement.classList); - - // 获取重要的data属性 - const themeAttr = document.documentElement.getAttribute('data-theme') || ''; - const modeAttr = document.documentElement.getAttribute('data-mode') || ''; - - return ` - // 设置基本内容 - document.body.innerHTML = decodeURIComponent(\`${encodeURIComponent(doc.body.innerHTML)}\`); - document.head.innerHTML = decodeURIComponent(\`${encodeURIComponent(doc.head.innerHTML)}\`); - - // 复制原始属性 - document.body.setAttribute("class", \`${doc.body.getAttribute("class") || ''}\`); - document.body.setAttribute("style", \`${doc.body.getAttribute("style") || ''}\`); - - // 动态应用当前主题的类名到body - ${currentBodyClasses.map(cls => - cls.includes('theme-') || cls.includes('dark') || cls.includes('light') || cls.includes('obsidian-app') - ? `document.body.classList.add('${cls}');` - : '' - ).filter(Boolean).join('\n ')} - - // 动态应用当前主题的类名到html - ${currentHtmlClasses.map(cls => - `document.documentElement.classList.add('${cls}');` - ).join('\n ')} - - // 设置重要的data属性 - ${themeAttr ? `document.documentElement.setAttribute('data-theme', '${themeAttr}');` : ''} - ${modeAttr ? `document.documentElement.setAttribute('data-mode', '${modeAttr}');` : ''} - - document.title = \`${doc.title}\`; - `; - } - - private async setupWebview(doc: Document) { - if (!this.webview) return; - - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Webview setup timeout')); - }, 10000); - - this.webview!.addEventListener('dom-ready', async () => { - try { - clearTimeout(timeout); - - // 1. 先注入所有基础样式 - const styles = this.getAllStyles(); - for (const css of styles) { - await this.webview!.insertCSS(css); - } - - // 2. 处理自定义CSS片段(如果有的话) - if (this.renderSettings.cssSnippet && this.renderSettings.cssSnippet !== '0') { - try { - // 这里需要实现CSS片段读取逻辑 - await this.webview!.insertCSS(this.renderSettings.cssSnippet); - } catch (error) { - console.warn('Failed to load CSS snippet:', error); - } - } - - // 3. 注入内容 - await this.webview!.executeJavaScript(this.makeWebviewJs(doc)); - - // 4. 最后再次注入补丁样式,确保优先级 - const patchStyles = this.getPatchStyles(); - for (const css of patchStyles) { - await this.webview!.insertCSS(css); - } - - this.webviewReady = true; - this.updateExportButtonState(); - resolve(); - } catch (error) { - clearTimeout(timeout); - reject(error); - } - }); - }); - } - private createPreviewArea() { if (this.selectedFormat !== 'pdf') { return; @@ -367,8 +164,11 @@ export class ExportModal extends Modal {
准备中...
`; - } else if (this.renderedBook) { - this.displayRenderedContent(previewContent); + } else if (this.webviewReady) { + // 如果webview已经准备好,显示它 + if (this.webview) { + previewContent.appendChild(this.webview); + } } else { const error = previewContent.createDiv({ cls: 'preview-error' }); error.innerHTML = ` @@ -399,22 +199,28 @@ export class ExportModal extends Modal { } } - // 移除重复的方法:getAllStyles, getPatchStyles, getPrintStyles, makeWebviewJs - private async startRenderPreview() { if (!this.selectedFormat || this.isRendering) return; - + this.stopRendering(); this.isRendering = true; - this.renderedBook = null; + this.webviewReady = false; this.abortController = new AbortController(); this.updateExportButtonState(); this.updateFormatButtonsState(); this.createPreviewArea(); try { - const renderService = new BookRenderService(this.app); + // 创建webview + this.webview = this.createWebview(); + const previewContent = this.previewContainer.querySelector('.preview-content'); + if (previewContent) { + previewContent.empty(); + previewContent.appendChild(this.webview); + } + + // 使用BookRenderService的新方法直接渲染到webview const config: RenderConfig = { showTitle: this.renderSettings.showTitle, scale: this.renderSettings.scale, @@ -427,7 +233,9 @@ export class ExportModal extends Modal { } }; - this.renderedBook = await renderService.renderBook( + // 直接渲染到webview,无需中间对象 + await this.bookRenderService.renderToWebview( + this.webview, this.selectedBook, this.plugin.settings.defaultBookPath, config @@ -437,8 +245,9 @@ export class ExportModal extends Modal { console.log('Rendering was aborted after completion'); return; } - - console.log('Render completed, updating preview...', this.renderedBook); + + this.webviewReady = true; + console.log('Rendering completed successfully'); } catch (error) { if (this.abortController?.signal.aborted || error.message === 'Render aborted') { @@ -454,7 +263,7 @@ export class ExportModal extends Modal { if (!this.abortController?.signal.aborted) { this.isRendering = false; this.updateFormatButtonsState(); - this.createPreviewArea(); // 这会触发 displayRenderedContent + // 延迟更新按钮状态,确保webview已经设置完成 setTimeout(() => { this.updateExportButtonState(); @@ -463,30 +272,6 @@ export class ExportModal extends Modal { } } - private async displayRenderedContent(container: HTMLElement) { - if (!this.renderedBook?.doc) return; - - container.empty(); - - try { - this.webview = this.createWebview(); - container.appendChild(this.webview); - - // 使用BookRenderService的setupWebview方法 - await this.bookRenderService.setupWebview(this.webview, this.renderedBook.doc); - - this.webviewReady = true; - this.updateExportButtonState(); - } catch (error) { - console.error('Error setting up webview:', error); - const errorDiv = container.createDiv({ cls: 'preview-error' }); - errorDiv.innerHTML = ` -
-
设置预览时出错: ${error.message}
- `; - } - } - private createSettingsContent(container: HTMLElement) { this.createBookInfo(container); this.createFormatSelection(container); @@ -519,7 +304,7 @@ export class ExportModal extends Modal { let canExport: boolean; if (this.selectedFormat === 'pdf') { - canExport = !!(this.selectedFormat && !this.isRendering && this.renderedBook && this.webviewReady); + canExport = !!(this.selectedFormat && !this.isRendering && this.webviewReady); } else { canExport = !!this.selectedFormat && !this.isRendering; } @@ -639,7 +424,6 @@ export class ExportModal extends Modal { if (format.key === 'pdf') { this.startRenderPreview(); } else { - this.renderedBook = null; this.cleanupWebview(); } @@ -765,7 +549,7 @@ export class ExportModal extends Modal { } private async exportToPdf() { - if (!this.webview || !this.webviewReady || !this.renderedBook) { + if (!this.webview || !this.webviewReady) { new Notice('PDF 预览未准备就绪,请稍候'); return; } @@ -781,7 +565,7 @@ export class ExportModal extends Modal { // PDF 导出选项 const printOptions: electron.PrintToPDFOptions = { pageSize: this.exportSettings.bookSize as any || 'A4', - printBackground: true, + printBackground: false, landscape: false, scale: this.renderSettings.scale / 100, margins: { diff --git a/src/services/BookRenderService.ts b/src/services/BookRenderService.ts index ab87150..795d510 100644 --- a/src/services/BookRenderService.ts +++ b/src/services/BookRenderService.ts @@ -12,130 +12,40 @@ export interface RenderConfig { onProgress?: (current: number, total: number, fileName: string) => void; } -export interface RenderedChapter { +export interface DocType { + doc: Document; + frontMatter: any; file: TFile; title: string; - content: HTMLElement; - frontMatter: any; } -export interface RenderedBook { - doc: Document; - title: string; - frontMatter: any; - chapters: RenderedChapter[]; -} - -export interface WebviewContent { - doc: Document; - styles: string[]; - webviewJs: string; +export interface ParamType { + app: App; + file: TFile; + config: RenderConfig; + book: Book; + rootPath: string; + extra?: { + title?: string; + id?: string; + }; } export class BookRenderService { + private docs: DocType[] = []; + private scale = 1; + constructor(private app: App) { } /** - * 渲染整本书籍 + * 主要渲染方法 - 基于 obsidian-better-export-pdf 架构 */ - async renderBook(book: Book, rootPath: String, config: RenderConfig): Promise { - const startTime = new Date().getTime(); - - // 检查是否已被中断 - if (config.abortSignal?.aborted) { - throw new Error('Render aborted'); - } - - // 创建主文档 - const doc = document.implementation.createHTMLDocument(book.basic.title || 'Book'); - doc.title = book.basic.title || 'Book'; - - // 添加样式 - this.injectStyles(doc, config); - - // 渲染章节 - const chapters: RenderedChapter[] = []; - - // 递归收集所有文件类型的章节节点 - const fileNodes = this.collectFileNodes(book.structure.tree); - const totalFiles = fileNodes.length; - - // 报告开始进度 - config.onProgress?.(0, totalFiles, '开始渲染...'); - - for (let i = 0; i < fileNodes.length; i++) { - const chapterNode = fileNodes[i]; - - // 在每个章节渲染前检查中断信号 - if (config.abortSignal?.aborted) { - throw new Error('Render aborted'); - } - - // 报告当前进度 - config.onProgress?.(i + 1, totalFiles, chapterNode.title || chapterNode.path); - - try { - // 根据路径获取TFile对象 - const filePath = `${rootPath}/${book?.basic.title}/${chapterNode.path}`; - const file = this.app.vault.getAbstractFileByPath(filePath); - if (file instanceof TFile) { - const renderedChapter = await this.renderChapter(file, config); - chapters.push(renderedChapter); - } else { - console.warn(`File not found: ${chapterNode.path}`); - } - } catch (error) { - if (config.abortSignal?.aborted) { - throw new Error('Render aborted'); - } - console.error(`Failed to render chapter: ${chapterNode.path}`, error); - new Notice(`渲染章节失败: ${chapterNode.title}`); - } - } - - // 最后检查一次中断信号 - if (config.abortSignal?.aborted) { - throw new Error('Render aborted'); - } - - // 报告完成进度 - config.onProgress?.(totalFiles, totalFiles, '合并章节中...'); - - // 合并章节到主文档 - this.mergeChaptersToDocument(doc, chapters, config); - - console.log(`Book render time: ${new Date().getTime() - startTime}ms`); - - return { - doc, - title: book.basic.title || 'Book', - frontMatter: book.basic || {}, - chapters - }; - } - - /** - * 为webview准备渲染内容(新增方法) - */ - async prepareWebviewContent(book: Book, rootPath: string, config: RenderConfig): Promise { - // 渲染书籍 - const renderedBook = await this.renderBook(book, rootPath, config); - - // 准备webview所需的样式和脚本 - const styles = this.getAllStyles(); - const webviewJs = this.makeWebviewJs(renderedBook.doc); - - return { - doc: renderedBook.doc, - styles, - webviewJs - }; - } - - /** - * 设置webview内容(新增方法) - */ - async setupWebview(webview: electron.WebviewTag, doc: Document, config?: RenderConfig): Promise { + async renderToWebview( + webview: electron.WebviewTag, + book: Book, + rootPath: string, + config: RenderConfig + ): Promise { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Webview setup timeout')); @@ -145,13 +55,17 @@ export class BookRenderService { try { clearTimeout(timeout); - // 1. 先注入所有基础样式 + // 1. 收集所有文件并渲染为文档 + const { data, docs } = await this.getAllFiles(book, rootPath, config); + await this.renderFiles(data, docs, config); + + // 2. 注入所有样式 const styles = this.getAllStyles(); for (const css of styles) { await webview.insertCSS(css); } - // 2. 处理自定义CSS片段(如果有的话) + // 3. 处理自定义CSS片段 if (config?.cssSnippet && config.cssSnippet !== '0') { try { await webview.insertCSS(config.cssSnippet); @@ -160,10 +74,10 @@ export class BookRenderService { } } - // 3. 注入内容 - await webview.executeJavaScript(this.makeWebviewJs(doc)); + // 4. 将渲染好的文档注入到webview + await this.appendWebview(webview, this.docs[0]); - // 4. 最后再次注入补丁样式,确保优先级 + // 5. 注入补丁样式 const patchStyles = this.getPatchStyles(); for (const css of patchStyles) { await webview.insertCSS(css); @@ -179,46 +93,318 @@ export class BookRenderService { } /** - * 生成webview注入脚本(从ExportModal移动过来) + * 收集所有文件 - 对应 modal.ts 的 getAllFiles + */ + private async getAllFiles(book: Book, rootPath: string, config: RenderConfig): Promise<{ data: ParamType[], docs: DocType[] }> { + const fileNodes = this.collectFileNodes(book.structure.tree); + const data: ParamType[] = fileNodes.map(node => ({ + app: this.app, + file: this.getFileFromNode(node, book, rootPath), + config, + book, + rootPath, + extra: { + title: node.title, + id: node.id + } + })).filter(param => param.file !== null) as ParamType[]; + + return { data, docs: [] }; + } + + /** + * 渲染所有文件 - 对应 modal.ts 的 renderFiles + */ + private async renderFiles( + data: ParamType[], + docs: DocType[] = [], + config: RenderConfig + ): Promise { + const totalFiles = data.length; + + // 并发渲染所有文件 + const inputs = data.map((param, i) => + this.renderMarkdown(param).then(res => { + config.onProgress?.(i + 1, totalFiles, param.file.basename); + return res; + }) + ); + + let _docs = [...docs, ...(await Promise.all(inputs))]; + + // 合并所有文档为一个完整的书籍文档 + _docs = this.mergeDoc(_docs); + + // 修复文档 + this.docs = _docs.map(({ doc, ...rest }) => { + return { ...rest, doc: this.fixDoc(doc, doc.title) }; + }); + } + + /** + * 渲染单个Markdown文件 - 基于 render.ts 的 renderMarkdown + */ + private async renderMarkdown({ app, file, config, book, extra }: ParamType): Promise { + const startTime = new Date().getTime(); + + // 检查中断信号 + if (config.abortSignal?.aborted) { + throw new Error('Render aborted'); + } + + const leaf = app.workspace.getLeaf(true); + await leaf.openFile(file); + const view = leaf.view as MarkdownView; + const data: string = view?.data || await app.vault.cachedRead(file); + + if (!data) { + console.warn(`Data is empty for file: ${file.path}`); + } + + const frontMatter = this.getFrontMatter(file); + const cssclasses = this.extractCssClasses(frontMatter); + + const comp = new Component(); + comp.load(); + + try { + const printEl = document.body.createDiv('print'); + const viewEl = printEl.createDiv({ + cls: 'markdown-preview-view markdown-rendered ' + cssclasses.join(' '), + }); + + // 设置RTL和属性显示 + // @ts-ignore + viewEl.toggleClass('rtl', app.vault.getConfig('rightToLeft')); + // @ts-ignore + viewEl.toggleClass('show-properties', 'hidden' !== app.vault.getConfig('propertiesInDocument')); + + // 设置标题 + const title = extra?.title || frontMatter?.title || file.basename; + viewEl.createEl('h1', { text: title }, (e) => { + e.addClass('__title__'); + e.style.display = config.showTitle ? 'block' : 'none'; + e.id = extra?.id || ''; + }); + + // 处理块引用 + const processedData = this.processBlockReferences(data || '', file); + + // 渲染markdown - 使用 better-export-pdf 的方式 + const fragment = this.createRenderFragment(); + const promises: Array<() => Promise> = []; + + try { + await MarkdownRenderer.render(app, processedData, fragment, file.path, comp); + } catch (error) { + // 预期的错误,用于避免postProcess + } + + // 添加渲染内容到viewEl + const el = createFragment(); + Array.from(fragment.children).forEach((item) => { + el.createDiv({}, (t) => { + //@ts-ignore + return t.appendChild(item); + }); + }); + viewEl.appendChild(el); + + // 后处理 + // @ts-ignore + await MarkdownRenderer.postProcess(app, { + docId: this.generateDocId(16), + sourcePath: file.path, + frontmatter: {}, + promises, + addChild: function (e: Component) { + return comp.addChild(e); + }, + getSectionInfo: function () { + return null; + }, + containerEl: viewEl, + el: viewEl, + displayMode: true, + }); + + await Promise.all(promises); + + // 修复内部链接 + this.fixInternalLinks(printEl, file); + + // 等待动态内容渲染 + await this.fixWaitRender(data || '', viewEl); + + // 修复canvas为图片 + this.fixCanvasToImage(viewEl); + + // 创建文档 + const doc = document.implementation.createHTMLDocument('document'); + doc.body.appendChild(printEl.cloneNode(true)); + doc.title = title; + + // 清理 + printEl.detach(); + printEl.remove(); + leaf.detach(); + + console.log(`Markdown render time: ${new Date().getTime() - startTime}ms`); + return { doc, frontMatter, file, title }; + + } finally { + comp.unload(); + } + } + + /** + * 合并所有文档 - 对应 modal.ts 的 mergeDoc + */ + private mergeDoc(docs: DocType[]): DocType[] { + if (docs.length <= 1) return docs; + + const { doc: doc0, frontMatter, file, title } = docs[0]; + const sections = []; + + for (const { doc } of docs) { + const element = doc.querySelector('.markdown-preview-view'); + if (element) { + const section = doc0.createElement('section'); + section.className = 'book-chapter'; + Array.from(element.children).forEach((child) => { + section.appendChild(doc0.importNode(child, true)); + }); + sections.push(section); + } + } + + const root = doc0.querySelector('.markdown-preview-view'); + if (root) { + root.innerHTML = ''; + sections.forEach((section, index) => { + root.appendChild(section); + + // 添加分页符(除了最后一章) + // if (index < sections.length - 1) { + // const pageBreak = doc0.createElement('div'); + // pageBreak.style.pageBreakAfter = 'always'; + // pageBreak.style.breakAfter = 'page'; + // pageBreak.className = 'page-break'; + // root.appendChild(pageBreak); + // } + }); + } + + return [{ doc: doc0, frontMatter, file, title }]; + } + + /** + * 将文档注入到webview - 对应 modal.ts 的 appendWebview + */ + private async appendWebview(webview: electron.WebviewTag, docData: DocType): Promise { + const { doc } = docData; + + // 使用 better-export-pdf 的安全注入方式 + const webviewJs = this.makeWebviewJs(doc); + + try { + await webview.executeJavaScript(webviewJs); + } catch (error) { + console.error('Failed to inject content to webview:', error); + throw new Error(`Failed to render book: ${error.message}`); + } + } + + /** + * 生成webview注入脚本 - 对应 modal.ts 的 makeWebviewJs */ private makeWebviewJs(doc: Document): string { - // 获取当前主题相关的类名 - const currentBodyClasses = Array.from(document.body.classList); - const currentHtmlClasses = Array.from(document.documentElement.classList); - - // 获取重要的data属性 - const themeAttr = document.documentElement.getAttribute('data-theme') || ''; - const modeAttr = document.documentElement.getAttribute('data-mode') || ''; + // 使用更安全的方式传输HTML内容 + const bodyContent = doc.body.innerHTML; + const headContent = doc.head.innerHTML; + const docTitle = doc.title; return ` - // 设置基本内容 - document.body.innerHTML = decodeURIComponent(\`${encodeURIComponent(doc.body.innerHTML)}\`); - document.head.innerHTML = decodeURIComponent(\`${encodeURIComponent(doc.head.innerHTML)}\`); - - // 复制原始属性 - document.body.setAttribute("class", \`${doc.body.getAttribute("class") || ''}\`); - document.body.setAttribute("style", \`${doc.body.getAttribute("style") || ''}\`); - - // 动态应用当前主题的类名到body - ${currentBodyClasses.map(cls => - cls.includes('theme-') || cls.includes('dark') || cls.includes('light') || cls.includes('obsidian-app') - ? `document.body.classList.add('${cls}');` - : '' - ).filter(Boolean).join('\n ')} - - // 动态应用当前主题的类名到html - ${currentHtmlClasses.map(cls => - `document.documentElement.classList.add('${cls}');` - ).join('\n ')} - - // 设置重要的data属性 - ${themeAttr ? `document.documentElement.setAttribute('data-theme', '${themeAttr}');` : ''} - ${modeAttr ? `document.documentElement.setAttribute('data-mode', '${modeAttr}');` : ''} - - document.title = \`${doc.title}\`; + try { + // 安全地设置内容 + document.body.innerHTML = decodeURIComponent(\`${encodeURIComponent(bodyContent)}\`); + document.head.innerHTML = decodeURIComponent(\`${encodeURIComponent(headContent)}\`); + + // 递归解码嵌入内容 + function decodeAndReplaceEmbed(element) { + if (!element || !element.innerHTML) return; + try { + element.innerHTML = decodeURIComponent(element.innerHTML); + const newEmbeds = element.querySelectorAll("span.markdown-embed"); + newEmbeds.forEach(decodeAndReplaceEmbed); + } catch (e) { + console.warn('Failed to decode embed:', e); + } + } + + document.querySelectorAll("span.markdown-embed").forEach(decodeAndReplaceEmbed); + + // 应用主题类名 + const currentBodyClasses = "${Array.from(document.body.classList).join(' ')}"; + const currentHtmlClasses = "${Array.from(document.documentElement.classList).join(' ')}"; + + if (currentBodyClasses) { + document.body.className = currentBodyClasses; + } + if (currentHtmlClasses) { + document.documentElement.className = currentHtmlClasses; + } + + // 设置主题相关属性 + const themeAttr = "${document.documentElement.getAttribute('data-theme') || ''}"; + const modeAttr = "${document.documentElement.getAttribute('data-mode') || ''}"; + + if (themeAttr) document.documentElement.setAttribute('data-theme', themeAttr); + if (modeAttr) document.documentElement.setAttribute('data-mode', modeAttr); + + document.body.addClass("theme-light"); + document.body.removeClass("theme-dark"); + document.title = \`${docTitle.replace(/[`\\]/g, '\\$&')}\`; + + } catch (error) { + console.error('Error in webview script:', error); + throw error; + } `; } + /** + * 修复文档 - 对应 render.ts 的 fixDoc + */ + private fixDoc(doc: Document, title: string): Document { + this.encodeEmbeds(doc); + return doc; + } + + /** + * 编码嵌入内容 - 对应 render.ts 的 encodeEmbeds + */ + private encodeEmbeds(doc: Document): void { + const spans = Array.from(doc.querySelectorAll('span.markdown-embed')).reverse(); + spans.forEach((span: HTMLElement) => { + span.innerHTML = encodeURIComponent(span.innerHTML); + }); + } + + // ... existing code ... + // 保留所有现有的辅助方法:collectFileNodes, getFrontMatter, extractCssClasses, + // processBlockReferences, createRenderFragment, fixInternalLinks, fixWaitRender, + // fixCanvasToImage, getAllStyles, getPatchStyles, 等等 + + /** + * 根据章节节点获取文件对象 + */ + private getFileFromNode(node: ChapterNode, book: Book, rootPath: string): TFile | null { + const filePath = `${rootPath}/${book.basic.title}/${node.path}`; + const file = this.app.vault.getAbstractFileByPath(filePath); + return file instanceof TFile ? file : null; + } + /** * 递归收集所有文件类型的章节节点 */ @@ -226,112 +412,18 @@ export class BookRenderService { const fileNodes: ChapterNode[] = []; for (const node of nodes) { - // 跳过被排除的节点 - if (node.exclude) { - continue; - } + if (node.exclude) continue; if (node.type === 'file') { fileNodes.push(node); } else if (node.type === 'group' && node.children) { - // 递归处理子节点 fileNodes.push(...this.collectFileNodes(node.children)); } } - // 按order排序 return fileNodes.sort((a, b) => a.order - b.order); } - /** - * 渲染单个章节 - */ - async renderChapter(file: TFile, config: RenderConfig): Promise { - // 检查中断信号 - if (config.abortSignal?.aborted) { - throw new Error('Render aborted'); - } - - const ws = this.app.workspace; - - // 创建临时叶子来渲染文件 - const leaf = ws.getLeaf(true); - await leaf.openFile(file); - const view = leaf.view as MarkdownView; - - // 获取文件内容 - const data: string = view?.data || await this.app.vault.cachedRead(file); - - // 获取前置元数据 - const frontMatter = this.getFrontMatter(file); - - // 获取CSS类 - const cssclasses = this.extractCssClasses(frontMatter); - - // 创建组件 - const comp = new Component(); - comp.load(); - - try { - // 再次检查中断信号 - if (config.abortSignal?.aborted) { - throw new Error('Render aborted'); - } - - // 在当前文档环境中完成渲染 - const printEl = document.body.createDiv('print'); - const viewEl = printEl.createDiv({ - cls: 'markdown-preview-view markdown-rendered ' + cssclasses.join(' ') - }); - - // 设置RTL和属性显示 - // @ts-ignore - viewEl.toggleClass('rtl', this.app.vault.getConfig('rightToLeft')); - // @ts-ignore - viewEl.toggleClass('show-properties', 'hidden' !== this.app.vault.getConfig('propertiesInDocument')); - - // 添加标题 - const title = frontMatter?.title || file.basename; - viewEl.createEl('h1', { text: title }, (e) => { - e.addClass('__title__'); - e.style.display = config.showTitle ? 'block' : 'none'; - }); - - // 处理块引用 - 如果数据为空,使用空字符串 - const processedData = this.processBlockReferences(data || '', file); - - // 渲染Markdown - await this.renderMarkdownContent(processedData, viewEl, file, comp); - - // 检查中断信号 - if (config.abortSignal?.aborted) { - throw new Error('Render aborted'); - } - - // 后处理 - await this.postProcessContent(viewEl, file, comp); - await this.fixRenderedContent(data || '', viewEl); - - // 在当前环境中完成所有渲染后,克隆内容 - const clonedContent = viewEl.cloneNode(true) as HTMLElement; - - // 清理当前 document 中的临时元素 - printEl.detach(); - printEl.remove(); - leaf.detach(); - - return { - file, - title, - content: clonedContent, - frontMatter - }; - - } finally { - comp.unload(); - } - } - /** * 获取前置元数据 */ @@ -375,7 +467,6 @@ export class BookRenderService { return line; }); - // 添加剩余的块引用 [...blocks.values()].forEach(({ id, position: { start } }) => { const idx = start.line; lines[idx] = `\n\n` + lines[idx]; @@ -385,90 +476,38 @@ export class BookRenderService { } /** - * 渲染Markdown内容 + * 创建渲染片段 */ - private async renderMarkdownContent(data: string, viewEl: HTMLElement, file: TFile, comp: Component) { - const fragment = { + private createRenderFragment(): any { + return { children: undefined, appendChild(e: DocumentFragment) { this.children = e?.children; throw new Error('exit'); - } + }, } as unknown as HTMLElement; - - try { - await MarkdownRenderer.render(this.app, data, fragment, file.path, comp); - } catch (error) { - // 预期的错误,用于跳过postProcess - } - - const el = createFragment(); - Array.from(fragment.children).forEach((item) => { - el.createDiv({}, (t) => { - return t.appendChild(item); - }); - }); - - viewEl.appendChild(el); } /** - * 后处理内容 + * 修复内部链接 */ - private async postProcessContent(viewEl: HTMLElement, file: TFile, comp: Component) { - const promises: Array<() => Promise> = []; - //@ts-ignore - await MarkdownRenderer.postProcess(this.app, { - docId: this.generateDocId(16), - sourcePath: file.path, - frontmatter: {}, - promises, - addChild: function (e: Component) { - return comp.addChild(e); - }, - getSectionInfo: function () { - return null; - }, - containerEl: viewEl, - el: viewEl, - displayMode: true - }); - - await Promise.all(promises); - } - - /** - * 修复渲染内容 - */ - private async fixRenderedContent(data: string, viewEl: HTMLElement) { - // 修复内部链接 - viewEl.findAll('a.internal-link').forEach((el: HTMLAnchorElement) => { + private fixInternalLinks(printEl: HTMLElement, file: TFile): void { + printEl.findAll('a.internal-link').forEach((el: HTMLAnchorElement) => { const [title, anchor] = el.dataset.href?.split('#') || []; - if ((!title || title?.length === 0) && anchor?.startsWith('^')) { + if ((!title || title?.length === 0 || title === file.basename) && anchor?.startsWith('^')) { return; } el.removeAttribute('href'); }); - - // 等待动态内容渲染 - try { - await this.waitForDynamicContent(data, viewEl); - } catch (error) { - console.warn('Wait timeout for dynamic content'); - } - - // 修复Canvas为图片 - this.fixCanvasToImage(viewEl); } /** * 等待动态内容渲染 */ - private async waitForDynamicContent(data: string, viewEl: HTMLElement) { + private async fixWaitRender(data: string, viewEl: HTMLElement): Promise { if (data.includes('```dataview') || data.includes('```gEvent') || data.includes('![[')) { await this.sleep(2000); } - try { await this.waitForDomChange(viewEl); } catch (error) { @@ -479,173 +518,44 @@ export class BookRenderService { /** * 修复Canvas为图片 */ - private fixCanvasToImage(el: HTMLElement) { + private fixCanvasToImage(el: HTMLElement): void { for (const canvas of Array.from(el.querySelectorAll('canvas'))) { const data = canvas.toDataURL(); const img = document.createElement('img'); img.src = data; img.className = '__canvas__'; - - // 复制属性 Array.from(canvas.attributes).forEach(attr => { img.setAttribute(attr.name, attr.value); }); - canvas.replaceWith(img); } } - /** - * 合并章节到文档 - */ - private mergeChaptersToDocument(doc: Document, chapters: RenderedChapter[], config: RenderConfig) { - const body = doc.body; - body.innerHTML = ''; - - // 添加打印容器,保持与当前文档相同的类名结构 - const printEl = body.appendChild(doc.createElement('div')); - printEl.className = 'print'; - - // 添加主容器,确保包含所有必要的类名 - const mainEl = printEl.appendChild(doc.createElement('div')); - mainEl.className = 'markdown-preview-view markdown-rendered'; - - // 复制当前文档中markdown-preview-view的相关类名 - const currentPreviewEl = document.querySelector('.markdown-preview-view'); - if (currentPreviewEl) { - Array.from(currentPreviewEl.classList).forEach(className => { - if (!mainEl.classList.contains(className)) { - mainEl.classList.add(className); - } - }); - } - - // 添加每个章节 - chapters.forEach((chapter, index) => { - const section = doc.createElement('section'); - section.className = 'book-chapter'; - section.setAttribute('data-chapter', index.toString()); - - // 导入章节内容 - Array.from(chapter.content.children).forEach(child => { - const importedNode = doc.importNode(child, true); - section.appendChild(importedNode); - }); - - mainEl.appendChild(section); - - // 添加分页符(除了最后一章) - // if (index < chapters.length - 1) { - // const pageBreak = doc.createElement('div'); - // pageBreak.style.pageBreakAfter = 'always'; - // pageBreak.style.breakAfter = 'page'; - // pageBreak.className = 'page-break'; - // mainEl.appendChild(pageBreak); - // } - }); - } - - /** - * 注入样式 - */ - private injectStyles(doc: Document, config?: RenderConfig) { - const head = doc.head; - const body = doc.body; - - // 复制当前文档的主题相关类名到目标文档 - const bodyClasses = Array.from(document.body.classList); - bodyClasses.forEach(className => { - // 复制所有主题相关的类名 - if (className.includes('theme-') || - className.includes('dark') || - className.includes('light') || - className.includes('obsidian-app')) { - body.classList.add(className); - } - }); - - // 复制html元素的类名和属性 - const htmlClasses = Array.from(document.documentElement.classList); - htmlClasses.forEach(className => { - doc.documentElement.classList.add(className); - }); - - // 复制重要的data属性 - const htmlElement = document.documentElement; - ['data-theme', 'data-mode'].forEach(attr => { - const value = htmlElement.getAttribute(attr); - if (value) { - doc.documentElement.setAttribute(attr, value); - } - }); - - // 获取所有样式 - const styles = this.getAllStyles(); - - // 创建样式元素 - const styleEl = doc.createElement('style'); - styleEl.textContent = styles.join('\n'); - head.appendChild(styleEl); - - // 处理自定义CSS片段 - if (config?.cssSnippet && config.cssSnippet !== '0') { - const customStyleEl = doc.createElement('style'); - customStyleEl.textContent = config.cssSnippet; - head.appendChild(customStyleEl); - } - } - /** * 获取所有样式 */ private getAllStyles(): string[] { const cssTexts: string[] = []; - - // 添加主题相关的样式注释 - cssTexts.push('/* ---------- Obsidian Theme Styles ---------- */'); - + Array.from(document.styleSheets).forEach((sheet) => { // @ts-ignore const id = sheet.ownerNode?.id; + if (id?.startsWith('svelte-')) return; + // @ts-ignore const href = sheet.ownerNode?.href; - - // 跳过Svelte样式,但保留所有主题相关样式 - if (id?.startsWith('svelte-')) { - return; - } - const division = `/* ----------${id ? `id:${id}` : href ? `href:${href}` : 'inline'}---------- */`; cssTexts.push(division); - + try { Array.from(sheet?.cssRules || []).forEach((rule) => { cssTexts.push(rule.cssText); }); } catch (error) { console.error('Error reading CSS rules:', error); - // 对于跨域样式表,尝试获取基本信息 - if (href) { - cssTexts.push(`/* External stylesheet: ${href} */`); - } } }); - - // 获取当前主题的body类名并添加相关样式 - const bodyClasses = Array.from(document.body.classList); - const themeClasses = bodyClasses.filter(cls => - cls.includes('theme-') || - cls.includes('dark') || - cls.includes('light') - ); - if (themeClasses.length > 0) { - cssTexts.push(`/* ---------- Current Theme Classes: ${themeClasses.join(', ')} ---------- */`); - } - - // 添加补丁样式 - cssTexts.push(...this.getPatchStyles()); - return cssTexts; } @@ -654,71 +564,43 @@ export class BookRenderService { */ private getPatchStyles(): string[] { const patchCSS = ` - /* ---------- css patch ---------- */ - body { - overflow: auto !important; - } - - @media print { - .print .markdown-preview-view { - height: auto !important; - } - - .md-print-anchor, .blockid { - white-space: pre !important; - border: none !important; - display: inline-block !important; - position: absolute !important; - width: 1px !important; - height: 1px !important; - right: 0 !important; - outline: 0 !important; - background: 0 0 !important; - text-decoration: initial !important; - text-shadow: initial !important; - } - - table { - break-inside: auto; - } - - tr { - break-inside: avoid; - break-after: auto; - } - } - - img.__canvas__ { - width: 100% !important; - height: 100% !important; - } - - .book-chapter { - margin-bottom: 2em; - } - `; - - return [patchCSS, ...this.getPrintStyles()]; - } - - private getPrintStyles(): string[] { - const printStyles: string[] = []; - - Array.from(document.styleSheets).forEach((sheet) => { - try { - Array.from(sheet?.cssRules || []).forEach((rule) => { - if (rule instanceof CSSMediaRule && rule.media.mediaText.includes('print')) { - Array.from(rule.cssRules).forEach((printRule) => { - printStyles.push(printRule.cssText); - }); - } - }); - } catch (error) { - console.error('Error reading print CSS rules:', error); + /* ---------- css patch ---------- */ + body { + overflow: auto !important; } - }); - - return printStyles; + + @media print { + .print .markdown-preview-view { + height: auto !important; + } + + .md-print-anchor, .blockid { + white-space: pre !important; + border: none !important; + display: inline-block !important; + position: absolute !important; + width: 1px !important; + height: 1px !important; + right: 0 !important; + } + } + + img.__canvas__ { + width: 100% !important; + height: 100% !important; + } + + .book-chapter { + margin-bottom: 2em; + } + + .page-break { + page-break-after: always; + break-after: page; + } + `; + + return [patchCSS]; } /** From f1d8a004e09bc5562c4951e201fc6c993dfb3593 Mon Sep 17 00:00:00 2001 From: Yeban8090 Date: Tue, 8 Jul 2025 02:54:48 +0800 Subject: [PATCH 03/10] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modals/ExportModal.ts | 209 ++++++++++++++++++++--------- src/services/BookRenderService.ts | 4 - src/styles/modals/export-modal.css | 4 +- 3 files changed, 146 insertions(+), 71 deletions(-) diff --git a/src/modals/ExportModal.ts b/src/modals/ExportModal.ts index 7a08a49..65f3ea1 100644 --- a/src/modals/ExportModal.ts +++ b/src/modals/ExportModal.ts @@ -92,19 +92,19 @@ export class ExportModal extends Modal { const { contentEl } = this; const mainContent = contentEl.createDiv({ cls: 'export-modal-main centered' }); this.mainContent = mainContent; - + // 左侧预览区域 - 只在选择PDF格式时显示 this.previewContainer = mainContent.createDiv({ cls: 'export-preview-panel' }); this.updatePreviewVisibility(); - + // 右侧设置区域 const settingsPanel = mainContent.createDiv({ cls: 'export-settings-panel' }); this.createSettingsContent(settingsPanel); } - + private updateLayoutMode() { if (!this.mainContent) return; - + if (this.selectedFormat && this.selectedFormat == 'pdf') { this.mainContent.removeClass('centered'); this.mainContent.addClass('split-layout'); @@ -113,12 +113,13 @@ export class ExportModal extends Modal { this.mainContent.addClass('centered'); } } - + private updatePreviewVisibility() { if (!this.previewContainer) return; - + if (this.selectedFormat === 'pdf') { this.previewContainer.style.display = 'flex'; + // 只创建预览区域结构,不立即开始渲染 this.createPreviewArea(); } else { this.previewContainer.style.display = 'none'; @@ -144,7 +145,7 @@ export class ExportModal extends Modal { if (this.selectedFormat !== 'pdf') { return; } - + this.previewContainer.empty(); this.cleanupWebview(); @@ -152,29 +153,60 @@ export class ExportModal extends Modal { previewHeader.createEl('h3', { text: 'PDF导出预览', cls: 'preview-title' }); const previewContent = this.previewContainer.createDiv({ cls: 'preview-content' }); + + // 初始状态:显示等待开始渲染的提示 + this.showPreviewState('waiting', previewContent); + } - if (this.isRendering) { - const loading = previewContent.createDiv({ cls: 'preview-loading' }); - loading.innerHTML = ` -
-
正在渲染预览...
-
-
-
-
准备中...
-
- `; - } else if (this.webviewReady) { - // 如果webview已经准备好,显示它 - if (this.webview) { - previewContent.appendChild(this.webview); - } - } else { - const error = previewContent.createDiv({ cls: 'preview-error' }); - error.innerHTML = ` -
-
渲染失败,请重试
- `; + // 新增:统一的预览状态管理方法 + private showPreviewState(state: 'waiting' | 'loading' | 'ready' | 'error', container?: HTMLElement, errorMessage?: string) { + const previewContent = container || this.previewContainer.querySelector('.preview-content') as HTMLElement; + if (!previewContent) return; + + previewContent.empty(); + + switch (state) { + case 'waiting': + const waiting = previewContent.createDiv({ cls: 'preview-waiting' }); + waiting.innerHTML = ` +
📄
+
点击开始渲染预览
+ `; + break; + + case 'loading': + const loading = previewContent.createDiv({ cls: 'preview-loading' }); + loading.innerHTML = ` +
+
正在渲染预览...
+
+
+
+
准备中...
+
+ `; + break; + + case 'ready': + if (this.webview) { + previewContent.appendChild(this.webview); + } + break; + + case 'error': + const error = previewContent.createDiv({ cls: 'preview-error' }); + error.innerHTML = ` +
+
${errorMessage || '渲染失败,请重试'}
+ + `; + + // 添加重试按钮事件 + const retryBtn = error.querySelector('.preview-retry-btn') as HTMLButtonElement; + retryBtn?.addEventListener('click', () => { + this.startRenderPreview(); + }); + break; } } @@ -200,54 +232,78 @@ export class ExportModal extends Modal { } private async startRenderPreview() { - if (!this.selectedFormat || this.isRendering) return; - - this.stopRendering(); + if (this.isRendering) { + new Notice('渲染进行中,请稍候...'); + return; + } + // 重置状态 this.isRendering = true; this.webviewReady = false; - this.abortController = new AbortController(); + this.cleanupWebview(); + + // 显示加载状态 + const previewContent = this.previewContainer.querySelector('.preview-content') as HTMLElement; + if (!previewContent) { + this.createPreviewArea(); // 确保预览区域已创建 + } + + previewContent.empty(); + + // 添加加载指示器 + const loading = previewContent.createDiv({ cls: 'preview-loading' }); + loading.innerHTML = ` +
+
正在渲染预览...
+
+
+
+
准备中...
+
+ `; + this.updateExportButtonState(); this.updateFormatButtonsState(); - this.createPreviewArea(); + + // 创建中止控制器 + this.abortController = new AbortController(); try { - // 创建webview + // 创建 webview 并立即添加到 DOM(但设为隐藏) this.webview = this.createWebview(); - const previewContent = this.previewContainer.querySelector('.preview-content'); - if (previewContent) { - previewContent.empty(); - previewContent.appendChild(this.webview); - } - - // 使用BookRenderService的新方法直接渲染到webview - const config: RenderConfig = { + this.webview.style.opacity = '0'; + previewContent.appendChild(this.webview); + + // 渲染配置 + const renderConfig: RenderConfig = { showTitle: this.renderSettings.showTitle, - scale: this.renderSettings.scale, + scale: this.renderSettings.scale / 100, displayHeader: this.renderSettings.displayHeader, displayFooter: this.renderSettings.displayFooter, cssSnippet: this.renderSettings.cssSnippet, - abortSignal: this.abortController.signal, onProgress: (current: number, total: number, fileName: string) => { this.updateRenderProgress(current, total, fileName); } }; - // 直接渲染到webview,无需中间对象 + // 执行渲染 - 现在 webview 已在 DOM 中,dom-ready 事件会正常触发 await this.bookRenderService.renderToWebview( this.webview, this.selectedBook, this.plugin.settings.defaultBookPath, - config + renderConfig ); - if (this.abortController.signal.aborted) { - console.log('Rendering was aborted after completion'); - return; + // 渲染成功 + if (!this.abortController.signal.aborted) { + this.webviewReady = true; + + // 移除加载指示器并显示 webview + loading.remove(); + this.webview.style.opacity = '1'; + + console.log('Rendering completed successfully'); } - - this.webviewReady = true; - console.log('Rendering completed successfully'); } catch (error) { if (this.abortController?.signal.aborted || error.message === 'Render aborted') { @@ -256,7 +312,24 @@ export class ExportModal extends Modal { } console.error('Render failed:', error); + this.webviewReady = false; + if (this.isRendering) { + // 显示错误信息 + previewContent.empty(); + const errorEl = previewContent.createDiv({ cls: 'preview-error' }); + errorEl.innerHTML = ` +
+
${error.message || '渲染失败,请重试'}
+ + `; + + // 添加重试按钮事件 + const retryBtn = errorEl.querySelector('.preview-retry-btn') as HTMLButtonElement; + retryBtn?.addEventListener('click', () => { + this.startRenderPreview(); + }); + new Notice('渲染失败,请检查控制台错误信息'); } } finally { @@ -264,7 +337,6 @@ export class ExportModal extends Modal { this.isRendering = false; this.updateFormatButtonsState(); - // 延迟更新按钮状态,确保webview已经设置完成 setTimeout(() => { this.updateExportButtonState(); }, 100); @@ -302,13 +374,13 @@ export class ExportModal extends Modal { private updateExportButtonState() { if (this.exportBtn) { let canExport: boolean; - + if (this.selectedFormat === 'pdf') { canExport = !!(this.selectedFormat && !this.isRendering && this.webviewReady); } else { canExport = !!this.selectedFormat && !this.isRendering; } - + this.exportBtn.disabled = !canExport; this.exportBtn.textContent = this.isRendering ? '渲染中...' : '导出'; } @@ -421,8 +493,12 @@ export class ExportModal extends Modal { this.updateSettingsArea(); this.updatePreviewVisibility(); + // PDF 格式自动开始渲染 if (format.key === 'pdf') { - this.startRenderPreview(); + // 延迟一点时间,确保 UI 更新完成 + setTimeout(() => { + this.startRenderPreview(); + }, 50); } else { this.cleanupWebview(); } @@ -431,7 +507,10 @@ export class ExportModal extends Modal { }); }); } - + private updateLayoutAndPreview() { + this.updateLayoutMode(); + this.updatePreviewVisibility(); + } private createSettingsArea(container: HTMLElement) { const settingsSection = container.createDiv({ cls: 'export-settings-section' }); @@ -557,11 +636,11 @@ export class ExportModal extends Modal { try { const filename = this.selectedBook.basic.title || 'exported-book'; const outputFile = await this.getOutputFile(filename); - + if (!outputFile) { return; // 用户取消了保存 } - + // PDF 导出选项 const printOptions: electron.PrintToPDFOptions = { pageSize: this.exportSettings.bookSize as any || 'A4', @@ -575,23 +654,23 @@ export class ExportModal extends Modal { headerTemplate: this.renderSettings.displayHeader ? '' : '', footerTemplate: this.renderSettings.displayFooter ? '' : '' }; - + // 使用 webview 生成 PDF const pdfBuffer = await this.webview.printToPDF(printOptions); - + // 保存文件 await fs.writeFile(outputFile, pdfBuffer); - + new Notice('PDF 导出成功!'); - + // 询问是否打开文件 const shouldOpen = confirm('PDF 导出成功!是否打开文件?'); if (shouldOpen) { // @ts-ignore electron.remote.shell.openPath(outputFile); } - + this.close(); } catch (error) { console.error('PDF export failed:', error); diff --git a/src/services/BookRenderService.ts b/src/services/BookRenderService.ts index 795d510..c03fa81 100644 --- a/src/services/BookRenderService.ts +++ b/src/services/BookRenderService.ts @@ -157,10 +157,6 @@ export class BookRenderService { const view = leaf.view as MarkdownView; const data: string = view?.data || await app.vault.cachedRead(file); - if (!data) { - console.warn(`Data is empty for file: ${file.path}`); - } - const frontMatter = this.getFrontMatter(file); const cssclasses = this.extractCssClasses(frontMatter); diff --git a/src/styles/modals/export-modal.css b/src/styles/modals/export-modal.css index 34d7015..67da954 100644 --- a/src/styles/modals/export-modal.css +++ b/src/styles/modals/export-modal.css @@ -67,6 +67,7 @@ background: var(--background-primary); overflow: hidden; transition: all 0.3s ease; + margin: 24px 0 24px 0; } /* 居中布局时隐藏预览面板 */ @@ -90,7 +91,6 @@ .preview-content { flex: 1; overflow: auto; - padding: 20px; scroll-behavior: smooth; } @@ -201,7 +201,7 @@ background: var(--export-secondary); border-radius: var(--export-radius); border: 1px solid var(--export-border); - margin-bottom: 24px; + margin: 24px 0 24px 0; transition: all 0.2s ease; } From b9932d1ca6431e30fcb4ee0eec2baa4ee0fd55aa Mon Sep 17 00:00:00 2001 From: Yeban8090 Date: Tue, 8 Jul 2025 02:58:10 +0800 Subject: [PATCH 04/10] =?UTF-8?q?=E5=B0=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modals/BookSelectionModal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modals/BookSelectionModal.ts b/src/modals/BookSelectionModal.ts index 5e7b7df..d0d76af 100644 --- a/src/modals/BookSelectionModal.ts +++ b/src/modals/BookSelectionModal.ts @@ -22,7 +22,7 @@ export class BookSelectionModal extends Modal { contentEl.empty(); // 设置模态框标题 - contentEl.createEl('h2', { text: i18n.t('SELECT_BOOK') || '选择要导出的书籍' }); + contentEl.createEl('h2', { text: '选择要导出的书籍' }); // 加载书籍列表 await this.loadBooks(); @@ -43,7 +43,7 @@ export class BookSelectionModal extends Modal { // 导出按钮 new ButtonComponent(buttonContainer) - .setButtonText(i18n.t('EXPORT') || '导出') + .setButtonText('导出设置') .setCta() .onClick(() => { this.handleExport(); From ee32954819715b7610b4a3c68f207fc683a04aae Mon Sep 17 00:00:00 2001 From: Yeban8090 Date: Tue, 8 Jul 2025 11:19:34 +0800 Subject: [PATCH 05/10] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=B0=81=E9=9D=A2?= =?UTF-8?q?=E3=80=81=E9=A1=B5=E7=9C=89=E9=A1=B5=E8=84=9A=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- manifest.json | 2 +- src/modals/CoverSettingModal.ts | 6 +- src/modals/ExportModal.ts | 308 +++++++++++++++++++------ src/services/BookRenderService.ts | 355 +++++++++++++++++++++++++---- src/styles/modals/export-modal.css | 25 ++ 6 files changed, 579 insertions(+), 120 deletions(-) diff --git a/.gitignore b/.gitignore index 1c83a9c..98e6e49 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ styles.css data.json # Exclude macOS Finder (System Explorer) View States -.DS_Store \ No newline at end of file +.DS_Store +other \ No newline at end of file diff --git a/manifest.json b/manifest.json index d96c609..f4bb749 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "book-smith", "name": "Book Smith", - "version": "1.0.4", + "version": "1.0.5", "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/src/modals/CoverSettingModal.ts b/src/modals/CoverSettingModal.ts index eadb95e..7d989e9 100644 --- a/src/modals/CoverSettingModal.ts +++ b/src/modals/CoverSettingModal.ts @@ -1,4 +1,4 @@ -import { App, Modal, Setting } from 'obsidian'; +import { App, Modal, Setting, Notice } from 'obsidian'; import { CoverManager } from '../services/CoverManager'; import { i18n } from '../i18n/i18n'; import { CoverSettings, TextStyleConfig } from '../types/book'; @@ -146,6 +146,10 @@ export class CoverSettingModal extends Modal { authorStyleConfig: this.authorStyleConfig, subtitleStyleConfig: this.subtitleStyleConfig }); + + // 添加提示 + new Notice('封面设置已更新,请点击"重新渲染"按钮查看效果'); + this.close(); }); } diff --git a/src/modals/ExportModal.ts b/src/modals/ExportModal.ts index 65f3ea1..2299ed4 100644 --- a/src/modals/ExportModal.ts +++ b/src/modals/ExportModal.ts @@ -1,41 +1,79 @@ import { Notice, App, Modal, ButtonComponent } from "obsidian"; import { i18n } from "../i18n/i18n"; import { BookRenderService, RenderConfig } from "../services/BookRenderService"; -import { Book } from "../types/book"; -import { HeaderFooterTocModal } from "./HeaderFooterTocModal"; +import { Book, CoverSettings } from "../types/book"; +import { HeaderFooterTocModal, HeaderFooterTocSettings } from "./HeaderFooterTocModal"; import { CoverSettingModal } from "./CoverSettingModal"; import BookSmithPlugin from "../main"; import * as fs from "fs/promises"; import * as electron from "electron"; +import { CoverManager } from "src/services/CoverManager"; +// 导出设置接口,定义了导出过程中需要的各种配置项 export interface ExportSettings { - format: string; - bookSize: string; - cover?: any; - headerFooterToc?: any; - theme?: string; - showCover: boolean; - coverImageData?: string; + format: string; // 导出格式(pdf、txt、docx) + bookSize: string; // 开本大小(A4、A5等) + cover?: CoverSettings; // 封面设置(可选) + headerFooterToc?: HeaderFooterTocSettings; // 页眉页脚和目录设置(可选) + theme?: string; // 主题(可选) + showCover: boolean; // 是否显示封面 + coverImageData?: string; // 封面图片数据(可选) } export class ExportModal extends Modal { + // UI 元素引用 private formatButtons: HTMLButtonElement[] = []; private selectedFormat: string | null = null; private settingsContainer: HTMLElement; private previewContainer: HTMLElement; private mainContent: HTMLElement; private exportBtn: HTMLButtonElement; + + // 状态标志 private isRendering: boolean = false; private abortController: AbortController | null = null; private webview: electron.WebviewTag | null = null; private webviewReady: boolean = false; + // 导出设置,包含默认值 private exportSettings: ExportSettings = { format: '', bookSize: 'A4', - showCover: false + showCover: true, // 修改为默认打开封面 + headerFooterToc: { + // 默认页眉页脚设置 + headerEnabled: true, + headerLeft: '{{title}}', + headerCenter: '', + headerRight: '{{author}}', + headerFontSize: 12, + headerColor: '#000000', + headerHeight: 15, + + // 默认页脚设置 + footerEnabled: true, + footerLeft: '', + footerCenter: '', + footerRight: '{{pageNumber}}/{{totalPages}}', + footerFontSize: 12, + footerColor: '#000000', + footerHeight: 20, + + // 默认目录设置 + tocEnabled: true, + tocTitle: '目录', + tocMaxLevel: 3, + tocFontSize: 14, + tocFontFamily: 'serif', + tocColor: '#000000', + tocLineHeight: 1.5, + tocIndentSize: 20, + tocIndent: 20, + tocPageBreak: true + } }; + // 渲染设置,控制渲染过程中的一些参数 private renderSettings = { showTitle: true, scale: 100, @@ -44,26 +82,20 @@ export class ExportModal extends Modal { cssSnippet: '' }; + // 构造函数,接收必要的依赖项 constructor( app: App, private plugin: BookSmithPlugin, private bookRenderService: BookRenderService, - private selectedBook: Book, - private pluginSettings?: any + private selectedBook: Book ) { super(app); - - if (pluginSettings) { - this.renderSettings = { - showTitle: pluginSettings.showTitle ?? true, - scale: pluginSettings.scale ?? 100, - displayHeader: pluginSettings.displayHeader ?? true, - displayFooter: pluginSettings.displayFooter ?? true, - cssSnippet: pluginSettings.cssSnippet ?? '' - }; - } + // 初始化封面设置默认值 + const coverManager = new CoverManager(this.app); + this.exportSettings.cover = coverManager.getDefaultCoverSettings(this.selectedBook); } + // 模态框打开时初始化 UI onOpen() { const { contentEl } = this; contentEl.empty(); @@ -78,6 +110,7 @@ export class ExportModal extends Modal { this.createFooter(); } + // 创建模态框的标题头部 private createHeader() { const { contentEl } = this; const header = contentEl.createDiv({ cls: 'export-modal-header' }); @@ -88,6 +121,7 @@ export class ExportModal extends Modal { }); } + // 创建主内容区域,包括左侧预览区域和右侧设置区域 private createMainContent() { const { contentEl } = this; const mainContent = contentEl.createDiv({ cls: 'export-modal-main centered' }); @@ -102,6 +136,7 @@ export class ExportModal extends Modal { this.createSettingsContent(settingsPanel); } + // 根据选择的导出格式更新布局模式 private updateLayoutMode() { if (!this.mainContent) return; @@ -114,9 +149,10 @@ export class ExportModal extends Modal { } } + // 根据选择的导出格式更新预览区域的可见性 private updatePreviewVisibility() { if (!this.previewContainer) return; - + if (this.selectedFormat === 'pdf') { this.previewContainer.style.display = 'flex'; // 只创建预览区域结构,不立即开始渲染 @@ -128,6 +164,7 @@ export class ExportModal extends Modal { } } + // 创建用于 PDF 预览的 Electron Webview private createWebview(scale = 1.0): electron.WebviewTag { const webview = document.createElement('webview') as electron.WebviewTag; webview.src = 'app://obsidian.md/help.html'; @@ -141,30 +178,43 @@ export class ExportModal extends Modal { return webview; } + // 创建预览区域的 UI 结构 private createPreviewArea() { if (this.selectedFormat !== 'pdf') { return; } - + this.previewContainer.empty(); this.cleanupWebview(); - + const previewHeader = this.previewContainer.createDiv({ cls: 'preview-header' }); - previewHeader.createEl('h3', { text: 'PDF导出预览', cls: 'preview-title' }); - - const previewContent = this.previewContainer.createDiv({ cls: 'preview-content' }); + // 创建标题和按钮的容器,使用 flex 布局 + const headerContent = previewHeader.createDiv({ cls: 'preview-header-content' }); + headerContent.createEl('h3', { text: 'PDF导出预览', cls: 'preview-title' }); + + // 添加重新渲染按钮 + const renderButton = headerContent.createEl('button', { + text: '重新渲染', + cls: 'preview-render-btn' + }); + renderButton.addEventListener('click', () => { + this.startRenderPreview(); + }); + + const previewContent = this.previewContainer.createDiv({ cls: 'preview-content' }); + // 初始状态:显示等待开始渲染的提示 this.showPreviewState('waiting', previewContent); } - - // 新增:统一的预览状态管理方法 + + // 统一的预览状态管理方法,处理不同状态(等待、加载中、就绪、错误) private showPreviewState(state: 'waiting' | 'loading' | 'ready' | 'error', container?: HTMLElement, errorMessage?: string) { const previewContent = container || this.previewContainer.querySelector('.preview-content') as HTMLElement; if (!previewContent) return; - + previewContent.empty(); - + switch (state) { case 'waiting': const waiting = previewContent.createDiv({ cls: 'preview-waiting' }); @@ -173,7 +223,7 @@ export class ExportModal extends Modal {
点击开始渲染预览
`; break; - + case 'loading': const loading = previewContent.createDiv({ cls: 'preview-loading' }); loading.innerHTML = ` @@ -186,13 +236,13 @@ export class ExportModal extends Modal {
`; break; - + case 'ready': if (this.webview) { previewContent.appendChild(this.webview); } break; - + case 'error': const error = previewContent.createDiv({ cls: 'preview-error' }); error.innerHTML = ` @@ -200,7 +250,7 @@ export class ExportModal extends Modal {
${errorMessage || '渲染失败,请重试'}
`; - + // 添加重试按钮事件 const retryBtn = error.querySelector('.preview-retry-btn') as HTMLButtonElement; retryBtn?.addEventListener('click', () => { @@ -210,6 +260,7 @@ export class ExportModal extends Modal { } } + // 清理 Webview 资源 private cleanupWebview() { if (this.webview) { this.webview.remove(); @@ -218,6 +269,7 @@ export class ExportModal extends Modal { } } + // 更新渲染进度条和进度文本 private updateRenderProgress(current: number, total: number, fileName: string) { const progressFill = this.previewContainer.querySelector('.progress-fill') as HTMLElement; const progressText = this.previewContainer.querySelector('.progress-text') as HTMLElement; @@ -231,25 +283,26 @@ export class ExportModal extends Modal { } } + // 开始渲染预览,这是渲染预览的核心方法 private async startRenderPreview() { if (this.isRendering) { new Notice('渲染进行中,请稍候...'); return; } - + // 重置状态 this.isRendering = true; this.webviewReady = false; this.cleanupWebview(); - + // 显示加载状态 const previewContent = this.previewContainer.querySelector('.preview-content') as HTMLElement; if (!previewContent) { this.createPreviewArea(); // 确保预览区域已创建 } - + previewContent.empty(); - + // 添加加载指示器 const loading = previewContent.createDiv({ cls: 'preview-loading' }); loading.innerHTML = ` @@ -261,31 +314,35 @@ export class ExportModal extends Modal {
准备中...
`; - + this.updateExportButtonState(); this.updateFormatButtonsState(); - + // 创建中止控制器 this.abortController = new AbortController(); - + try { // 创建 webview 并立即添加到 DOM(但设为隐藏) this.webview = this.createWebview(); this.webview.style.opacity = '0'; previewContent.appendChild(this.webview); - + // 渲染配置 const renderConfig: RenderConfig = { showTitle: this.renderSettings.showTitle, scale: this.renderSettings.scale / 100, - displayHeader: this.renderSettings.displayHeader, - displayFooter: this.renderSettings.displayFooter, + displayHeader: !!(this.renderSettings.displayHeader && this.exportSettings.headerFooterToc?.headerEnabled), + displayFooter: !!(this.renderSettings.displayFooter && this.exportSettings.headerFooterToc?.footerEnabled), cssSnippet: this.renderSettings.cssSnippet, + headerFooterToc: this.exportSettings.headerFooterToc, + showCover: this.exportSettings.showCover, + coverSettings: this.exportSettings.cover, + abortSignal: this.abortController?.signal, onProgress: (current: number, total: number, fileName: string) => { this.updateRenderProgress(current, total, fileName); } }; - + // 执行渲染 - 现在 webview 已在 DOM 中,dom-ready 事件会正常触发 await this.bookRenderService.renderToWebview( this.webview, @@ -293,27 +350,27 @@ export class ExportModal extends Modal { this.plugin.settings.defaultBookPath, renderConfig ); - + // 渲染成功 if (!this.abortController.signal.aborted) { this.webviewReady = true; - + // 移除加载指示器并显示 webview loading.remove(); this.webview.style.opacity = '1'; - + this.webview.style.transition = 'opacity 0.3s ease-in-out'; console.log('Rendering completed successfully'); } - + } catch (error) { if (this.abortController?.signal.aborted || error.message === 'Render aborted') { console.log('Rendering was aborted'); return; } - + console.error('Render failed:', error); this.webviewReady = false; - + if (this.isRendering) { // 显示错误信息 previewContent.empty(); @@ -323,20 +380,20 @@ export class ExportModal extends Modal {
${error.message || '渲染失败,请重试'}
`; - + // 添加重试按钮事件 const retryBtn = errorEl.querySelector('.preview-retry-btn') as HTMLButtonElement; retryBtn?.addEventListener('click', () => { this.startRenderPreview(); }); - + new Notice('渲染失败,请检查控制台错误信息'); } } finally { if (!this.abortController?.signal.aborted) { this.isRendering = false; this.updateFormatButtonsState(); - + setTimeout(() => { this.updateExportButtonState(); }, 100); @@ -344,12 +401,14 @@ export class ExportModal extends Modal { } } + // 创建设置区域的内容,包括书籍信息、格式选择和设置区域 private createSettingsContent(container: HTMLElement) { this.createBookInfo(container); this.createFormatSelection(container); this.createSettingsArea(container); } + // 创建模态框底部的按钮区域 private createFooter() { const { contentEl } = this; const footer = contentEl.createDiv({ cls: 'export-modal-footer' }); @@ -371,6 +430,7 @@ export class ExportModal extends Modal { this.updateExportButtonState(); } + // 根据渲染状态更新导出按钮的可用性和文本 private updateExportButtonState() { if (this.exportBtn) { let canExport: boolean; @@ -386,6 +446,7 @@ export class ExportModal extends Modal { } } + // 根据渲染状态更新格式按钮的可用性和样式 private updateFormatButtonsState() { this.formatButtons.forEach(btn => { if (this.isRendering) { @@ -400,12 +461,14 @@ export class ExportModal extends Modal { }); } + // 处理取消按钮点击 private async handleCancel() { this.stopRendering(); this.cleanupWebview(); this.close(); } + // 中止正在进行的渲染 private stopRendering() { if (this.abortController) { this.abortController.abort(); @@ -416,6 +479,7 @@ export class ExportModal extends Modal { this.updateFormatButtonsState(); } + // 显示书籍基本信息(标题、作者、描述) private createBookInfo(container: HTMLElement) { const bookCard = container.createDiv({ cls: 'export-book-card' }); @@ -444,6 +508,7 @@ export class ExportModal extends Modal { } } + // 创建导出格式选择区域(PDF、TXT、DOCX) private createFormatSelection(container: HTMLElement) { const formatSection = container.createDiv({ cls: 'export-format-section' }); @@ -488,11 +553,11 @@ export class ExportModal extends Modal { formatCard.classList.add('selected'); this.selectedFormat = format.key; this.exportSettings.format = format.key; - + this.updateLayoutMode(); this.updateSettingsArea(); this.updatePreviewVisibility(); - + // PDF 格式自动开始渲染 if (format.key === 'pdf') { // 延迟一点时间,确保 UI 更新完成 @@ -502,15 +567,13 @@ export class ExportModal extends Modal { } else { this.cleanupWebview(); } - + this.updateExportButtonState(); }); }); } - private updateLayoutAndPreview() { - this.updateLayoutMode(); - this.updatePreviewVisibility(); - } + + // 创建设置区域的容器 private createSettingsArea(container: HTMLElement) { const settingsSection = container.createDiv({ cls: 'export-settings-section' }); @@ -528,6 +591,7 @@ export class ExportModal extends Modal { `; } + // 根据选择的格式更新设置区域 private updateSettingsArea() { this.settingsContainer.empty(); @@ -553,8 +617,10 @@ export class ExportModal extends Modal { } } + // 创建通用设置(如开本大小) private createCommonSettings(container: HTMLElement) { if (['pdf', 'docx'].includes(this.selectedFormat!)) { + // 开本大小设置 const sizeCard = container.createDiv({ cls: 'export-setting-card' }); const sizeHeader = sizeCard.createDiv({ cls: 'export-setting-header' }); @@ -567,7 +633,7 @@ export class ExportModal extends Modal { const sizes = ['A4', 'A5', 'A3', 'Letter', 'Legal', 'Tabloid']; sizes.forEach(size => { const option = sizeSelect.createEl('option', { value: size, text: size }); - if (size === 'A4') option.selected = true; + if (size === (this.exportSettings.bookSize || 'A4')) option.selected = true; }); sizeSelect.addEventListener('change', () => { @@ -576,6 +642,7 @@ export class ExportModal extends Modal { } } + // 创建 PDF 特定设置(封面设置、页眉页脚目录设置) private createPdfSettings(container: HTMLElement) { // PDF 特定设置 const coverCard = container.createDiv({ cls: 'export-setting-card' }); @@ -587,22 +654,84 @@ export class ExportModal extends Modal { `; const coverToggle = coverCard.createDiv({ cls: 'export-setting-toggle' }); - const coverCheckbox = coverToggle.createEl('input', { type: 'checkbox' }); - coverToggle.createEl('label', { text: '包含封面' }); + const coverCheckbox = coverToggle.createEl('input', { type: 'checkbox', attr: { id: 'cover-toggle' } }); + coverToggle.createEl('label', { text: '包含封面', attr: { for: 'cover-toggle' } }); + coverCheckbox.checked = this.exportSettings.showCover !== false; // 修改为默认选中 coverCheckbox.addEventListener('change', () => { this.exportSettings.showCover = coverCheckbox.checked; }); + + // 添加封面设置按钮 + const coverButtonContainer = coverCard.createDiv({ cls: 'export-setting-button-container' }); + const coverSettingButton = coverButtonContainer.createEl('button', { + cls: 'export-setting-button', + text: '自定义封面' + }); + + coverSettingButton.addEventListener('click', () => { + // 打开封面设置模态框 + const coverModal = new CoverSettingModal( + this.app, + (settings) => { + // 保存封面设置 + this.exportSettings.cover = settings; + }, + document.createElement('div'), // 临时元素作为预览容器 + new CoverManager(this.app), + this.exportSettings.cover, + this.selectedBook.basic.title, + this.selectedBook.basic.author, + this.selectedBook.basic.subtitle + ); + coverModal.open(); + }); + + // 页眉页脚目录设置 + const headerFooterTocCard = container.createDiv({ cls: 'export-setting-card' }); + + const headerFooterTocHeader = headerFooterTocCard.createDiv({ cls: 'export-setting-header' }); + headerFooterTocHeader.innerHTML = ` + 📑 + 页眉页脚目录设置 + `; + + // 添加页眉页脚目录设置按钮 + const headerFooterTocButtonContainer = headerFooterTocCard.createDiv({ cls: 'export-setting-button-container' }); + const headerFooterTocButton = headerFooterTocButtonContainer.createEl('button', { + cls: 'export-setting-button', + text: '自定义页眉页脚和目录' + }); + + headerFooterTocButton.addEventListener('click', () => { + // 打开页眉页脚目录设置模态框 + const headerFooterTocModal = new HeaderFooterTocModal( + this.plugin, + this.exportSettings.headerFooterToc || {}, + (settings) => { + // 保存页眉页脚目录设置 + this.exportSettings.headerFooterToc = settings; + + // 更新渲染设置 + this.renderSettings.displayHeader = settings.headerEnabled; + this.renderSettings.displayFooter = settings.footerEnabled; + } + ); + headerFooterTocModal.open(); + }); } + // 创建 HTML 特定设置(占位符) private createHtmlSettings(container: HTMLElement) { // HTML 特定设置 } + // 创建 DOCX 特定设置(占位符) private createDocxSettings(container: HTMLElement) { // DOCX 特定设置 } + // 显示保存文件对话框 private async getOutputFile(filename: string): Promise { try { // @ts-ignore @@ -627,6 +756,7 @@ export class ExportModal extends Modal { } } + // 导出为 PDF private async exportToPdf() { if (!this.webview || !this.webviewReady) { new Notice('PDF 预览未准备就绪,请稍候'); @@ -641,6 +771,33 @@ export class ExportModal extends Modal { return; // 用户取消了保存 } + // 获取页眉页脚设置 + // const headerFooterToc = this.exportSettings.headerFooterToc || {}; + + // 构建页眉模板 + let headerTemplate = ''; + if (this.renderSettings.displayHeader && this.exportSettings.headerFooterToc?.headerEnabled) { + headerTemplate = ` +
+
${this.processVariables(this.exportSettings.headerFooterToc.headerLeft || '')}
+
${this.processVariables(this.exportSettings.headerFooterToc.headerCenter || '')}
+
${this.processVariables(this.exportSettings.headerFooterToc.headerRight || '')}
+
+ `; + } + + // 构建页脚模板 + let footerTemplate = ''; + if (this.renderSettings.displayFooter && this.exportSettings.headerFooterToc?.footerEnabled) { + footerTemplate = ` +
+
${this.processVariables(this.exportSettings.headerFooterToc.footerLeft || '')}
+
${this.processVariables(this.exportSettings.headerFooterToc.footerCenter || '')}
+
${this.processVariables(this.exportSettings.headerFooterToc.footerRight || '').replace('{{pageNumber}}', '').replace('{{totalPages}}', '')}
+
+ `; + } + // PDF 导出选项 const printOptions: electron.PrintToPDFOptions = { pageSize: this.exportSettings.bookSize as any || 'A4', @@ -651,11 +808,10 @@ export class ExportModal extends Modal { marginType: 'default' }, displayHeaderFooter: this.renderSettings.displayHeader || this.renderSettings.displayFooter, - headerTemplate: this.renderSettings.displayHeader ? '' : '', - footerTemplate: this.renderSettings.displayFooter ? '' : '' + headerTemplate: headerTemplate, + footerTemplate: footerTemplate }; - // 使用 webview 生成 PDF const pdfBuffer = await this.webview.printToPDF(printOptions); @@ -678,16 +834,29 @@ export class ExportModal extends Modal { } } + // 处理页眉页脚模板中的变量(如 {{title}}、{{author}}、{{date}}) + private processVariables(text: string): string { + if (!text) return ''; + + return text + .replace('{{title}}', this.selectedBook.basic.title || '') + .replace('{{author}}', Array.isArray(this.selectedBook.basic.author) ? this.selectedBook.basic.author.join(', ') : (this.selectedBook.basic.author || '')) + .replace('{{date}}', new Date().toLocaleDateString()); + } + + // 导出为 TXT(开发中) private async exportToTxt() { // TXT 导出逻辑 new Notice('TXT 导出功能开发中...'); } + // 导出为 DOCX(开发中) private async exportToDocx() { // DOCX 导出逻辑 new Notice('DOCX 导出功能开发中...'); } + // 根据选择的格式调用相应的导出方法 private async handleExport() { if (!this.selectedFormat) { new Notice('请先选择导出格式'); @@ -709,6 +878,7 @@ export class ExportModal extends Modal { } } + // 在模态框关闭时执行清理操作 onClose() { this.stopRendering(); this.cleanupWebview(); diff --git a/src/services/BookRenderService.ts b/src/services/BookRenderService.ts index c03fa81..816930c 100644 --- a/src/services/BookRenderService.ts +++ b/src/services/BookRenderService.ts @@ -1,5 +1,6 @@ -import { App, Component, MarkdownRenderer, MarkdownView, TFile, Notice } from 'obsidian'; -import { Book, ChapterNode } from 'src/types/book'; +import { App, Component, MarkdownRenderer, MarkdownView, TFile } from 'obsidian'; +import { Book, ChapterNode, CoverSettings } from '../types/book'; +import { HeaderFooterTocSettings } from '../modals/HeaderFooterTocModal'; import * as electron from 'electron'; export interface RenderConfig { @@ -10,6 +11,9 @@ export interface RenderConfig { cssSnippet?: string; abortSignal?: AbortSignal; onProgress?: (current: number, total: number, fileName: string) => void; + headerFooterToc?: HeaderFooterTocSettings; // 添加目录设置 + showCover?: boolean; + coverSettings?: CoverSettings; } export interface DocType { @@ -41,9 +45,9 @@ export class BookRenderService { * 主要渲染方法 - 基于 obsidian-better-export-pdf 架构 */ async renderToWebview( - webview: electron.WebviewTag, - book: Book, - rootPath: string, + webview: electron.WebviewTag, + book: Book, + rootPath: string, config: RenderConfig ): Promise { return new Promise((resolve, reject) => { @@ -51,21 +55,48 @@ export class BookRenderService { reject(new Error('Webview setup timeout')); }, 10000); + webview.addEventListener('dom-ready', async () => { try { clearTimeout(timeout); - + // 1. 收集所有文件并渲染为文档 const { data, docs } = await this.getAllFiles(book, rootPath, config); await this.renderFiles(data, docs, config); - - // 2. 注入所有样式 + + // 处理封面 + if (config.showCover && config.coverSettings) { + + // 生成封面 HTML + const coverHtml = this.generateCover(config.coverSettings, book); + + // 将封面插入到文档的开头 + if (coverHtml) { + const doc = this.docs[0].doc; + const contentEl = doc.querySelector('.markdown-preview-view'); + if (contentEl) { + const coverContainer = doc.createElement('div'); + coverContainer.innerHTML = coverHtml; + const firstChild = coverContainer.firstChild; + if (firstChild) { + contentEl.insertBefore(firstChild, contentEl.firstChild); + } + } + } + } + // 2. 如果启用了目录,生成目录 HTML + let tocHtml = ''; + if (config.headerFooterToc?.tocEnabled) { + tocHtml = this.generateTOC(config.headerFooterToc, book); + } + + // 3. 注入所有样式 const styles = this.getAllStyles(); for (const css of styles) { await webview.insertCSS(css); } - - // 3. 处理自定义CSS片段 + + // 4. 处理自定义CSS片段 if (config?.cssSnippet && config.cssSnippet !== '0') { try { await webview.insertCSS(config.cssSnippet); @@ -73,16 +104,30 @@ export class BookRenderService { console.warn('Failed to load CSS snippet:', error); } } - - // 4. 将渲染好的文档注入到webview + + // 5. 将目录和渲染好的文档注入到webview + if (tocHtml) { + // 将目录插入到文档的开头 + const doc = this.docs[0].doc; + const contentEl = doc.querySelector('.markdown-preview-view'); + if (contentEl) { + const tocContainer = doc.createElement('div'); + tocContainer.innerHTML = tocHtml; + const firstChild = tocContainer.firstChild; + if (firstChild) { + contentEl.insertBefore(firstChild, contentEl.firstChild); + } + } + } + await this.appendWebview(webview, this.docs[0]); - - // 5. 注入补丁样式 + + // 6. 注入补丁样式 const patchStyles = this.getPatchStyles(); for (const css of patchStyles) { await webview.insertCSS(css); } - + resolve(); } catch (error) { clearTimeout(timeout); @@ -92,6 +137,133 @@ export class BookRenderService { }); } + /** + * 生成封面 HTML + * @param settings 封面设置 + * @param book 书籍信息 + * @returns 封面 HTML 字符串 + */ + private generateCover(settings: CoverSettings, book: Book): string { + if (!settings) return ''; + + // 创建临时容器元素 + const tempContainer = document.createElement('div'); + tempContainer.className = 'book-cover'; + // 添加分页符样式 + tempContainer.style.pageBreakAfter = 'always'; + tempContainer.style.breakAfter = 'page'; + // 设置开本大小样式 + this.applyBookSizeStyles(tempContainer, settings.bookSize || 'A4'); + + // 设置背景图片 + if (settings.imageUrl) { + tempContainer.style.backgroundImage = `url(${settings.imageUrl})`; + tempContainer.style.backgroundSize = `${settings.scale * 100}%`; + tempContainer.style.backgroundPosition = `${settings.position.x}px ${settings.position.y}px`; + tempContainer.style.backgroundRepeat = 'no-repeat'; + } + + // 创建内容容器 + const contentContainer = document.createElement('div'); + contentContainer.className = 'cover-content'; + contentContainer.style.position = 'relative'; + 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'; + tempContainer.appendChild(contentContainer); + + // 添加书籍信息 + // 使用自定义文本和位置 + const titleText = settings.customTitle || book.basic.title; + const subtitleText = settings.customSubtitle || book.basic.subtitle; + const authorText = settings.customAuthor || (book.basic.author ? book.basic.author.join(', ') : ''); + + // 添加书名 + if (titleText) { + const titleEl = document.createElement('div'); + titleEl.className = 'cover-title'; + titleEl.textContent = titleText; + + let titleStyle = ''; + if (settings.titleStyleConfig) { + titleStyle = this.buildStyleString(settings.titleStyleConfig); + } else { + titleStyle = settings.titleStyle || ''; + } + titleEl.setAttribute('style', titleStyle + `position: absolute; left: ${settings.titlePosition?.x || 50}%; top: ${settings.titlePosition?.y || 30}%; transform: translate(-50%, -50%); z-index: 10;`); + contentContainer.appendChild(titleEl); + } + + // 添加副标题 + if (subtitleText) { + const subtitleEl = document.createElement('div'); + subtitleEl.className = 'cover-subtitle'; + subtitleEl.textContent = subtitleText; + + let subtitleStyle = ''; + if (settings.subtitleStyleConfig) { + subtitleStyle = this.buildStyleString(settings.subtitleStyleConfig); + } else { + subtitleStyle = 'font-size: 18px; color: #ffffff; text-shadow: 0 1px 2px rgba(0,0,0,0.5);'; + } + subtitleEl.setAttribute('style', subtitleStyle + `position: absolute; left: ${settings.subtitlePosition?.x || 50}%; top: ${settings.subtitlePosition?.y || 50}%; transform: translate(-50%, -50%); z-index: 10;`); + contentContainer.appendChild(subtitleEl); + } + + // 添加作者信息 + if (authorText) { + const authorEl = document.createElement('div'); + authorEl.className = 'cover-author'; + authorEl.textContent = authorText; + + let authorStyle = ''; + if (settings.authorStyleConfig) { + authorStyle = this.buildStyleString(settings.authorStyleConfig); + } else { + authorStyle = settings.authorStyle || ''; + } + authorEl.setAttribute('style', authorStyle + `position: absolute; left: ${settings.authorPosition?.x || 50}%; top: ${settings.authorPosition?.y || 70}%; transform: translate(-50%, -50%); z-index: 10;`); + contentContainer.appendChild(authorEl); + } + + // 返回生成的 HTML + return tempContainer.outerHTML; + } + + /** + * 构建样式字符串 + * @param styleConfig 样式配置 + * @returns 样式字符串 + */ + private buildStyleString(styleConfig: any): string { + return `font-size: ${styleConfig.fontSize}px; color: ${styleConfig.color}; font-weight: ${styleConfig.fontWeight}; font-style: ${styleConfig.fontStyle}; text-shadow: ${styleConfig.textShadow || 'none'}; `; + } + + /** + * 应用开本大小样式 + * @param element 目标元素 + * @param bookSize 开本大小 + */ + private applyBookSizeStyles(element: HTMLElement, bookSize: string) { + const sizeMap: Record = { + 'A4': { aspectRatio: '210/297' }, + 'A5': { aspectRatio: '148/210' }, + 'A3': { aspectRatio: '297/420' }, + 'Legal': { aspectRatio: '8.5/14' }, + 'Letter': { aspectRatio: '8.5/11' }, + 'Tabloid': { aspectRatio: '11/17' } + }; + + const size = sizeMap[bookSize] || sizeMap['A4']; + element.style.aspectRatio = size.aspectRatio; + element.style.width = '100%'; + element.style.height = 'auto'; + } + /** * 收集所有文件 - 对应 modal.ts 的 getAllFiles */ @@ -108,7 +280,7 @@ export class BookRenderService { id: node.id } })).filter(param => param.file !== null) as ParamType[]; - + return { data, docs: [] }; } @@ -116,25 +288,25 @@ export class BookRenderService { * 渲染所有文件 - 对应 modal.ts 的 renderFiles */ private async renderFiles( - data: ParamType[], - docs: DocType[] = [], + data: ParamType[], + docs: DocType[] = [], config: RenderConfig ): Promise { const totalFiles = data.length; - + // 并发渲染所有文件 - const inputs = data.map((param, i) => + const inputs = data.map((param, i) => this.renderMarkdown(param).then(res => { config.onProgress?.(i + 1, totalFiles, param.file.basename); return res; }) ); - + let _docs = [...docs, ...(await Promise.all(inputs))]; - + // 合并所有文档为一个完整的书籍文档 _docs = this.mergeDoc(_docs); - + // 修复文档 this.docs = _docs.map(({ doc, ...rest }) => { return { ...rest, doc: this.fixDoc(doc, doc.title) }; @@ -146,7 +318,7 @@ export class BookRenderService { */ private async renderMarkdown({ app, file, config, book, extra }: ParamType): Promise { const startTime = new Date().getTime(); - + // 检查中断信号 if (config.abortSignal?.aborted) { throw new Error('Render aborted'); @@ -156,10 +328,10 @@ export class BookRenderService { await leaf.openFile(file); const view = leaf.view as MarkdownView; const data: string = view?.data || await app.vault.cachedRead(file); - + const frontMatter = this.getFrontMatter(file); const cssclasses = this.extractCssClasses(frontMatter); - + const comp = new Component(); comp.load(); @@ -189,7 +361,7 @@ export class BookRenderService { // 渲染markdown - 使用 better-export-pdf 的方式 const fragment = this.createRenderFragment(); const promises: Array<() => Promise> = []; - + try { await MarkdownRenderer.render(app, processedData, fragment, file.path, comp); } catch (error) { @@ -223,15 +395,15 @@ export class BookRenderService { el: viewEl, displayMode: true, }); - + await Promise.all(promises); // 修复内部链接 this.fixInternalLinks(printEl, file); - + // 等待动态内容渲染 await this.fixWaitRender(data || '', viewEl); - + // 修复canvas为图片 this.fixCanvasToImage(viewEl); @@ -245,9 +417,8 @@ export class BookRenderService { printEl.remove(); leaf.detach(); - console.log(`Markdown render time: ${new Date().getTime() - startTime}ms`); return { doc, frontMatter, file, title }; - + } finally { comp.unload(); } @@ -258,10 +429,10 @@ export class BookRenderService { */ private mergeDoc(docs: DocType[]): DocType[] { if (docs.length <= 1) return docs; - + const { doc: doc0, frontMatter, file, title } = docs[0]; const sections = []; - + for (const { doc } of docs) { const element = doc.querySelector('.markdown-preview-view'); if (element) { @@ -273,13 +444,13 @@ export class BookRenderService { sections.push(section); } } - + const root = doc0.querySelector('.markdown-preview-view'); if (root) { root.innerHTML = ''; sections.forEach((section, index) => { root.appendChild(section); - + // 添加分页符(除了最后一章) // if (index < sections.length - 1) { // const pageBreak = doc0.createElement('div'); @@ -290,7 +461,7 @@ export class BookRenderService { // } }); } - + return [{ doc: doc0, frontMatter, file, title }]; } @@ -299,10 +470,10 @@ export class BookRenderService { */ private async appendWebview(webview: electron.WebviewTag, docData: DocType): Promise { const { doc } = docData; - + // 使用 better-export-pdf 的安全注入方式 const webviewJs = this.makeWebviewJs(doc); - + try { await webview.executeJavaScript(webviewJs); } catch (error) { @@ -319,7 +490,7 @@ export class BookRenderService { const bodyContent = doc.body.innerHTML; const headContent = doc.head.innerHTML; const docTitle = doc.title; - + return ` try { // 安全地设置内容 @@ -387,11 +558,6 @@ export class BookRenderService { }); } - // ... existing code ... - // 保留所有现有的辅助方法:collectFileNodes, getFrontMatter, extractCssClasses, - // processBlockReferences, createRenderFragment, fixInternalLinks, fixWaitRender, - // fixCanvasToImage, getAllStyles, getPatchStyles, 等等 - /** * 根据章节节点获取文件对象 */ @@ -537,12 +703,12 @@ export class BookRenderService { // @ts-ignore const id = sheet.ownerNode?.id; if (id?.startsWith('svelte-')) return; - + // @ts-ignore const href = sheet.ownerNode?.href; const division = `/* ----------${id ? `id:${id}` : href ? `href:${href}` : 'inline'}---------- */`; cssTexts.push(division); - + try { Array.from(sheet?.cssRules || []).forEach((rule) => { cssTexts.push(rule.cssText); @@ -551,7 +717,7 @@ export class BookRenderService { console.error('Error reading CSS rules:', error); } }); - + return cssTexts; } @@ -634,4 +800,97 @@ export class BookRenderService { }, timeout); }); } + + /** + * 生成目录 HTML + * @param settings 目录设置 + * @param book 书籍信息 + * @returns 目录 HTML 字符串 + */ + public generateTOC(settings: any, book: Book): string { + if (!settings?.tocEnabled) return ''; + + // 收集所有标题 + const headings = this.collectHeadings(); + + if (headings.length === 0) return ''; + + let tocHtml = ` +
+

${settings.tocTitle}

+
+ `; + + headings.forEach(heading => { + if (heading.level <= settings.tocMaxLevel) { + const indent = (heading.level - 1) * (settings.tocIndent || settings.tocIndentSize || 20); + tocHtml += ` +
+ ${heading.text} + + ? +
+ `; + } + }); + + tocHtml += ` +
+
+ `; + + return tocHtml; + } + + /** + * 收集文档中的所有标题 + * @returns 标题数组 + */ + private collectHeadings(): Array<{ level: number, text: string, id: string }> { + interface Heading { + level: number; + text: string; + id: string; + } + const headings: Heading[] = []; + + // 遍历所有文档 + this.docs.forEach(docData => { + const { doc } = docData; + const headingElements = doc.querySelectorAll('h1, h2, h3, h4, h5, h6'); + + headingElements.forEach((el, index) => { + const level = parseInt(el.tagName.substring(1)); + const id = el.id || `heading-${index}`; + if (!el.id) el.id = id; + + headings.push({ + level, + text: el.textContent?.trim() || '', + id + }); + }); + }); + + return headings; + } } \ No newline at end of file diff --git a/src/styles/modals/export-modal.css b/src/styles/modals/export-modal.css index 67da954..f07c149 100644 --- a/src/styles/modals/export-modal.css +++ b/src/styles/modals/export-modal.css @@ -74,7 +74,32 @@ .export-modal-main.centered .export-preview-panel { display: none; } +.preview-header-content { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} +.preview-render-btn { + background-color: var(--interactive-accent); + color: var(--text-on-accent); + padding: 4px 12px; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + border: none; + transition: background-color 0.2s ease; +} + +.preview-render-btn:hover { + background-color: var(--interactive-accent-hover); +} + +.preview-render-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} .preview-header { padding: 16px 20px; border-bottom: 1px solid var(--export-border); From d4fc925712e6958e53e837f63b873c1b1fb00833 Mon Sep 17 00:00:00 2001 From: Yeban8090 Date: Tue, 8 Jul 2025 14:21:59 +0800 Subject: [PATCH 06/10] =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E5=AE=8C=E5=96=84?= =?UTF-8?q?=EF=BC=8C=E7=AD=89=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modals/ExportModal.ts | 585 +++++++++++++++++++++++++++-- src/services/BookRenderService.ts | 155 +------- src/styles/modals/export-modal.css | 234 +++++++----- 3 files changed, 708 insertions(+), 266 deletions(-) diff --git a/src/modals/ExportModal.ts b/src/modals/ExportModal.ts index 2299ed4..b3967b7 100644 --- a/src/modals/ExportModal.ts +++ b/src/modals/ExportModal.ts @@ -1,5 +1,4 @@ -import { Notice, App, Modal, ButtonComponent } from "obsidian"; -import { i18n } from "../i18n/i18n"; +import { Notice, App, Modal } from "obsidian"; import { BookRenderService, RenderConfig } from "../services/BookRenderService"; import { Book, CoverSettings } from "../types/book"; import { HeaderFooterTocModal, HeaderFooterTocSettings } from "./HeaderFooterTocModal"; @@ -8,6 +7,7 @@ import BookSmithPlugin from "../main"; import * as fs from "fs/promises"; import * as electron from "electron"; import { CoverManager } from "src/services/CoverManager"; +import { PDFDocument } from 'pdf-lib'; // 导出设置接口,定义了导出过程中需要的各种配置项 export interface ExportSettings { @@ -28,12 +28,13 @@ export class ExportModal extends Modal { private previewContainer: HTMLElement; private mainContent: HTMLElement; private exportBtn: HTMLButtonElement; - + // 状态标志 private isRendering: boolean = false; private abortController: AbortController | null = null; private webview: electron.WebviewTag | null = null; private webviewReady: boolean = false; + private coverPreviewElement: HTMLElement; // 导出设置,包含默认值 private exportSettings: ExportSettings = { @@ -188,26 +189,133 @@ export class ExportModal extends Modal { this.cleanupWebview(); const previewHeader = this.previewContainer.createDiv({ cls: 'preview-header' }); - + // 创建标题和按钮的容器,使用 flex 布局 const headerContent = previewHeader.createDiv({ cls: 'preview-header-content' }); headerContent.createEl('h3', { text: 'PDF导出预览', cls: 'preview-title' }); - - // 添加重新渲染按钮 - const renderButton = headerContent.createEl('button', { - text: '重新渲染', - cls: 'preview-render-btn' - }); - renderButton.addEventListener('click', () => { - this.startRenderPreview(); - }); + // 创建一个包含封面和内容的滚动容器 + const scrollContainer = this.previewContainer.createDiv({ cls: 'preview-scroll-container' }); - const previewContent = this.previewContainer.createDiv({ cls: 'preview-content' }); + // 添加封面预览区域(初始隐藏) + if (this.exportSettings.showCover && this.exportSettings.cover) { + const coverPreviewContainer = scrollContainer.createDiv({ cls: 'cover-preview-container' }); + coverPreviewContainer.style.display = 'none'; // 初始隐藏,等待渲染完成后显示 + coverPreviewContainer.style.marginBottom = '20px'; + coverPreviewContainer.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)'; + + // 创建封面预览标题 + const coverPreviewHeader = coverPreviewContainer.createDiv({ cls: 'cover-preview-header' }); + coverPreviewHeader.style.padding = '8px 12px'; + coverPreviewHeader.style.borderBottom = '1px solid #e0e0e0'; + coverPreviewHeader.style.display = 'flex'; + coverPreviewHeader.style.justifyContent = 'space-between'; + coverPreviewHeader.style.alignItems = 'center'; + + coverPreviewHeader.createEl('span', { text: '封面预览', cls: 'cover-preview-title' }); + + // 添加封面预览内容区域 + this.coverPreviewElement = coverPreviewContainer.createDiv({ cls: 'cover-preview-content' }); + this.coverPreviewElement.style.padding = '15px'; + this.coverPreviewElement.style.display = 'flex'; + this.coverPreviewElement.style.justifyContent = 'center'; + // 移除背景色设置 + // this.coverPreviewElement.style.backgroundColor = '#f5f5f5'; + + // 更新封面预览 + this.updateCoverPreview(); + } + + // 添加内容预览区域 + const previewContent = scrollContainer.createDiv({ cls: 'preview-content' }); // 初始状态:显示等待开始渲染的提示 this.showPreviewState('waiting', previewContent); } + /** + * 更新封面预览 + */ + private updateCoverPreview() { + if (!this.coverPreviewElement || !this.exportSettings.cover) return; + this.coverPreviewElement.empty(); + + // 创建封面预览内部容器 + const coverContainer = this.coverPreviewElement.createDiv({ cls: 'cover-container' }); + coverContainer.style.maxWidth = '250px'; + coverContainer.style.maxHeight = '350px'; + coverContainer.style.overflow = 'hidden'; + coverContainer.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)'; + + // 应用开本大小样式 + this.applyBookSizeStyles(coverContainer, this.exportSettings.cover.bookSize || this.exportSettings.bookSize || 'A4'); + + // 设置背景图片 + if (this.exportSettings.cover.imageUrl) { + coverContainer.style.backgroundImage = `url(${this.exportSettings.cover.imageUrl})`; + coverContainer.style.backgroundSize = `${this.exportSettings.cover.scale * 100}%`; + coverContainer.style.backgroundPosition = `${this.exportSettings.cover.position.x}px ${this.exportSettings.cover.position.y}px`; + coverContainer.style.backgroundRepeat = 'no-repeat'; + } + + // 创建内容容器 + const contentContainer = coverContainer.createDiv({ cls: 'cover-content' }); + contentContainer.style.position = 'relative'; + contentContainer.style.height = '100%'; + contentContainer.style.display = 'flex'; + contentContainer.style.flexDirection = 'column'; + contentContainer.style.justifyContent = 'center'; + contentContainer.style.alignItems = 'center'; + contentContainer.style.padding = '20px'; + contentContainer.style.textAlign = 'center'; + + // 添加书籍信息 + const settings = this.exportSettings.cover; + const book = this.selectedBook; + + // 使用自定义文本和位置 + const titleText = settings.customTitle || book.basic.title; + const subtitleText = settings.customSubtitle || book.basic.subtitle; + const authorText = settings.customAuthor || (book.basic.author ? book.basic.author.join(', ') : ''); + + // 添加书名 + if (titleText) { + const titleEl = contentContainer.createDiv({ cls: 'cover-title', text: titleText }); + + let titleStyle = ''; + if (settings.titleStyleConfig) { + titleStyle = this.buildStyleString(settings.titleStyleConfig); + } else { + titleStyle = settings.titleStyle || ''; + } + titleEl.setAttribute('style', titleStyle + `position: absolute; left: ${settings.titlePosition?.x || 50}%; top: ${settings.titlePosition?.y || 30}%; transform: translate(-50%, -50%); z-index: 10;`); + } + + // 添加副标题 + if (subtitleText) { + const subtitleEl = contentContainer.createDiv({ cls: 'cover-subtitle', text: subtitleText }); + + let subtitleStyle = ''; + if (settings.subtitleStyleConfig) { + subtitleStyle = this.buildStyleString(settings.subtitleStyleConfig); + } else { + subtitleStyle = 'font-size: 18px; color: #ffffff; text-shadow: 0 1px 2px rgba(0,0,0,0.5);'; + } + subtitleEl.setAttribute('style', subtitleStyle + `position: absolute; left: ${settings.subtitlePosition?.x || 50}%; top: ${settings.subtitlePosition?.y || 50}%; transform: translate(-50%, -50%); z-index: 10;`); + } + + // 添加作者信息 + if (authorText) { + const authorEl = contentContainer.createDiv({ cls: 'cover-author', text: authorText }); + + let authorStyle = ''; + if (settings.authorStyleConfig) { + authorStyle = this.buildStyleString(settings.authorStyleConfig); + } else { + authorStyle = settings.authorStyle || ''; + } + authorEl.setAttribute('style', authorStyle + `position: absolute; left: ${settings.authorPosition?.x || 50}%; top: ${settings.authorPosition?.y || 70}%; transform: translate(-50%, -50%); z-index: 10;`); + } + } // 统一的预览状态管理方法,处理不同状态(等待、加载中、就绪、错误) private showPreviewState(state: 'waiting' | 'loading' | 'ready' | 'error', container?: HTMLElement, errorMessage?: string) { const previewContent = container || this.previewContainer.querySelector('.preview-content') as HTMLElement; @@ -360,6 +468,18 @@ export class ExportModal extends Modal { this.webview.style.opacity = '1'; this.webview.style.transition = 'opacity 0.3s ease-in-out'; console.log('Rendering completed successfully'); + + // 渲染成功后,如果启用了封面,更新并显示封面预览 + if (this.exportSettings.showCover && this.exportSettings.cover) { + // 更新封面预览内容 + this.updateCoverPreview(); + + // 显示封面预览容器 + const coverPreviewContainer = this.previewContainer.querySelector('.cover-preview-container'); + if (coverPreviewContainer) { + (coverPreviewContainer as HTMLElement).style.display = 'block'; + } + } } } catch (error) { @@ -660,6 +780,12 @@ export class ExportModal extends Modal { coverCheckbox.addEventListener('change', () => { this.exportSettings.showCover = coverCheckbox.checked; + + // 更新封面预览区域的可见性,只有在已渲染完成时才显示 + const coverPreviewContainer = this.previewContainer.querySelector('.cover-preview-container'); + if (coverPreviewContainer) { + (coverPreviewContainer as HTMLElement).style.display = (this.exportSettings.showCover && this.webviewReady) ? 'block' : 'none'; + } }); // 添加封面设置按钮 @@ -676,6 +802,9 @@ export class ExportModal extends Modal { (settings) => { // 保存封面设置 this.exportSettings.cover = settings; + + // 更新封面预览 + this.updateCoverPreview(); }, document.createElement('div'), // 临时元素作为预览容器 new CoverManager(this.app), @@ -771,14 +900,11 @@ export class ExportModal extends Modal { return; // 用户取消了保存 } - // 获取页眉页脚设置 - // const headerFooterToc = this.exportSettings.headerFooterToc || {}; - // 构建页眉模板 let headerTemplate = ''; if (this.renderSettings.displayHeader && this.exportSettings.headerFooterToc?.headerEnabled) { headerTemplate = ` -
+
${this.processVariables(this.exportSettings.headerFooterToc.headerLeft || '')}
${this.processVariables(this.exportSettings.headerFooterToc.headerCenter || '')}
${this.processVariables(this.exportSettings.headerFooterToc.headerRight || '')}
@@ -790,14 +916,19 @@ export class ExportModal extends Modal { let footerTemplate = ''; if (this.renderSettings.displayFooter && this.exportSettings.headerFooterToc?.footerEnabled) { footerTemplate = ` -
-
${this.processVariables(this.exportSettings.headerFooterToc.footerLeft || '')}
-
${this.processVariables(this.exportSettings.headerFooterToc.footerCenter || '')}
-
${this.processVariables(this.exportSettings.headerFooterToc.footerRight || '').replace('{{pageNumber}}', '').replace('{{totalPages}}', '')}
+
+ ${this.processVariables(this.exportSettings.headerFooterToc.footerLeft || '')} + ${this.processVariables(this.exportSettings.headerFooterToc.footerCenter || '')} + ${this.processVariables(this.exportSettings.headerFooterToc.footerRight || '').replace('{{pageNumber}}', '').replace('{{totalPages}}', '')}
`; } + // 更新目录页码 + if (this.exportSettings.headerFooterToc?.tocEnabled) { + await this.updateTocPageNumbers(); + } + // PDF 导出选项 const printOptions: electron.PrintToPDFOptions = { pageSize: this.exportSettings.bookSize as any || 'A4', @@ -815,8 +946,31 @@ export class ExportModal extends Modal { // 使用 webview 生成 PDF const pdfBuffer = await this.webview.printToPDF(printOptions); + // 如果启用了封面,生成并合并封面 + let finalPdfBuffer: Buffer | Uint8Array = pdfBuffer; + if (this.exportSettings.showCover && this.exportSettings.cover && this.coverPreviewElement) { + // 直接使用预览区域的封面元素 + const coverContainer = this.coverPreviewElement.querySelector('.cover-container'); + if (!coverContainer) { + console.warn('未找到封面容器元素'); + return null; + } + const coverImageData = await this.convertCoverToImage(coverContainer as HTMLElement); + if (coverImageData) { + // 将封面图片数据保存到导出设置中 + this.exportSettings.coverImageData = coverImageData; + // 合并封面 + finalPdfBuffer = Buffer.from(await this.generateAndMergeCover(Buffer.from(pdfBuffer))); + } else { + // 如果无法从预览区域获取封面图片,使用原有方法 + finalPdfBuffer = await this.generateAndMergeCover(Buffer.from(pdfBuffer)); + } + } else if (this.exportSettings.showCover && this.exportSettings.cover) { + finalPdfBuffer = await this.generateAndMergeCover(Buffer.from(pdfBuffer)); + } + // 保存文件 - await fs.writeFile(outputFile, pdfBuffer); + await fs.writeFile(outputFile, finalPdfBuffer); new Notice('PDF 导出成功!'); @@ -834,6 +988,391 @@ export class ExportModal extends Modal { } } + /** + * 更新目录页码 + */ + private async updateTocPageNumbers(): Promise { + try { + // 注入脚本计算每个标题的页码 + const script = ` + (function() { + // 创建一个映射,存储每个标题ID对应的页码 + const headingPageMap = {}; + + // 获取所有标题元素 + const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); + + // 计算每个标题所在的页码 + headings.forEach(heading => { + if (!heading.id) return; + + // 获取元素的位置信息 + const rect = heading.getBoundingClientRect(); + + // 计算页码(基于A4纸张和默认边距) + // 这里的计算是近似的,实际页码可能会有差异 + const pageHeight = 1122; // A4纸张高度(点) + const pageNumber = Math.floor(rect.top / pageHeight) + 1; + + headingPageMap[heading.id] = pageNumber; + }); + + // 更新目录中的页码 + const tocPageElements = document.querySelectorAll('.toc-page'); + tocPageElements.forEach(pageEl => { + const headingId = pageEl.getAttribute('data-heading-id'); + if (headingId && headingPageMap[headingId]) { + pageEl.textContent = headingPageMap[headingId]; + } + }); + + return true; + })(); + `; + + if (this.webview) { + await this.webview.executeJavaScript(script); + } + } catch (error) { + console.error('Failed to update TOC page numbers:', error); + } + } + + /** + * 生成封面并与内容PDF合并 + */ + private async generateAndMergeCover(contentPdfBuffer: Buffer): Promise { + try { + // 如果已经有封面图片数据,直接使用 + if (this.exportSettings.coverImageData) { + // 创建一个新的PDF文档作为封面 + const coverPdfDoc = await PDFDocument.create(); + + // 根据选择的开本大小设置页面尺寸 + const pageSizes = { + 'A4': [595.28, 841.89], + 'A5': [419.53, 595.28], + 'A3': [841.89, 1190.55], + 'Letter': [612, 792], + 'Legal': [612, 1008], + 'Tabloid': [792, 1224] + }; + + const pageSize = pageSizes[this.exportSettings.bookSize as keyof typeof pageSizes] || pageSizes['A4']; + const coverPage = coverPdfDoc.addPage([pageSize[0], pageSize[1]]); + + // 将封面图片添加到PDF + const coverImage = await coverPdfDoc.embedPng(this.exportSettings.coverImageData); + const { width, height } = coverImage.size(); + + // 计算图片在页面上的位置和大小 + const scale = Math.min( + coverPage.getWidth() / width, + coverPage.getHeight() / height + ); + + coverPage.drawImage(coverImage, { + x: (coverPage.getWidth() - width * scale) / 2, + y: (coverPage.getHeight() - height * scale) / 2, + width: width * scale, + height: height * scale + }); + + // 加载内容PDF + const contentPdfDoc = await PDFDocument.load(contentPdfBuffer); + + // 创建最终的PDF文档 + const finalPdfDoc = await PDFDocument.create(); + + // 复制封面页到最终文档 + const [coverPageCopy] = await finalPdfDoc.copyPages(coverPdfDoc, [0]); + finalPdfDoc.addPage(coverPageCopy); + + // 复制内容页到最终文档 + const contentPages = await finalPdfDoc.copyPages( + contentPdfDoc, + contentPdfDoc.getPageIndices() + ); + contentPages.forEach(page => finalPdfDoc.addPage(page)); + + // 保存最终文档 + return Buffer.from(await finalPdfDoc.save()); + } else { + // 如果没有封面图片数据,使用原有方法生成封面 + // 创建一个临时的HTML元素来生成封面 + const coverContainer = document.createElement('div'); + coverContainer.style.position = 'fixed'; + coverContainer.style.top = '-9999px'; + coverContainer.style.left = '-9999px'; + document.body.appendChild(coverContainer); + + // 使用generateCoverHTML方法生成封面HTML + if (this.exportSettings.cover) { + const coverHTML = this.generateCoverHTML(this.exportSettings.cover, this.selectedBook); + coverContainer.innerHTML = coverHTML; + } + + // 等待图片加载完成 + await new Promise(resolve => setTimeout(resolve, 300)); + + // 将封面转换为图片 + let coverImageData = await this.convertCoverToImage(coverContainer); + + // 清理临时元素 + document.body.removeChild(coverContainer); + + // 如果无法生成封面图片,使用简单的文本封面 + if (!coverImageData) { + return this.createSimpleCoverAndMerge(contentPdfBuffer); + } + + // 保存封面图片数据 + this.exportSettings.coverImageData = coverImageData; + + // 递归调用自身,这次会走上面的分支 + return this.generateAndMergeCover(contentPdfBuffer); + } + } catch (error) { + console.error('Failed to generate and merge cover:', error); + // 如果封面生成失败,返回原始内容PDF + return contentPdfBuffer; + } + } + + /** + * 生成封面HTML + */ + private generateCoverHTML(settings: CoverSettings, book: Book): string { + if (!settings) return ''; + + // 创建临时容器元素 + const tempContainer = document.createElement('div'); + tempContainer.className = 'book-cover'; + // 添加分页符样式 + tempContainer.style.pageBreakAfter = 'always'; + tempContainer.style.breakAfter = 'page'; + // 设置开本大小样式 + this.applyBookSizeStyles(tempContainer, settings.bookSize || this.exportSettings.bookSize || 'A4'); + + // 设置背景图片 + if (settings.imageUrl) { + tempContainer.style.backgroundImage = `url(${settings.imageUrl})`; + tempContainer.style.backgroundSize = `${settings.scale * 100}%`; + tempContainer.style.backgroundPosition = `${settings.position.x}px ${settings.position.y}px`; + tempContainer.style.backgroundRepeat = 'no-repeat'; + } + + // 创建内容容器 + const contentContainer = document.createElement('div'); + contentContainer.className = 'cover-content'; + contentContainer.style.position = 'relative'; + 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'; + tempContainer.appendChild(contentContainer); + + // 添加书籍信息 + // 使用自定义文本和位置 + const titleText = settings.customTitle || book.basic.title; + const subtitleText = settings.customSubtitle || book.basic.subtitle; + const authorText = settings.customAuthor || (book.basic.author ? book.basic.author.join(', ') : ''); + + // 添加书名 + if (titleText) { + const titleEl = document.createElement('div'); + titleEl.className = 'cover-title'; + titleEl.textContent = titleText; + + let titleStyle = ''; + if (settings.titleStyleConfig) { + titleStyle = this.buildStyleString(settings.titleStyleConfig); + } else { + titleStyle = settings.titleStyle || ''; + } + titleEl.setAttribute('style', titleStyle + `position: absolute; left: ${settings.titlePosition?.x || 50}%; top: ${settings.titlePosition?.y || 30}%; transform: translate(-50%, -50%); z-index: 10;`); + contentContainer.appendChild(titleEl); + } + + // 添加副标题 + if (subtitleText) { + const subtitleEl = document.createElement('div'); + subtitleEl.className = 'cover-subtitle'; + subtitleEl.textContent = subtitleText; + + let subtitleStyle = ''; + if (settings.subtitleStyleConfig) { + subtitleStyle = this.buildStyleString(settings.subtitleStyleConfig); + } else { + subtitleStyle = 'font-size: 18px; color: #ffffff; text-shadow: 0 1px 2px rgba(0,0,0,0.5);'; + } + subtitleEl.setAttribute('style', subtitleStyle + `position: absolute; left: ${settings.subtitlePosition?.x || 50}%; top: ${settings.subtitlePosition?.y || 50}%; transform: translate(-50%, -50%); z-index: 10;`); + contentContainer.appendChild(subtitleEl); + } + + // 添加作者信息 + if (authorText) { + const authorEl = document.createElement('div'); + authorEl.className = 'cover-author'; + authorEl.textContent = authorText; + + let authorStyle = ''; + if (settings.authorStyleConfig) { + authorStyle = this.buildStyleString(settings.authorStyleConfig); + } else { + authorStyle = settings.authorStyle || ''; + } + authorEl.setAttribute('style', authorStyle + `position: absolute; left: ${settings.authorPosition?.x || 50}%; top: ${settings.authorPosition?.y || 70}%; transform: translate(-50%, -50%); z-index: 10;`); + contentContainer.appendChild(authorEl); + } + + // 返回生成的 HTML + return tempContainer.outerHTML; + } + + /** + * 构建样式字符串 + */ + private buildStyleString(styleConfig: any): string { + return `font-size: ${styleConfig.fontSize}px; color: ${styleConfig.color}; font-weight: ${styleConfig.fontWeight}; font-style: ${styleConfig.fontStyle}; text-shadow: ${styleConfig.textShadow || 'none'}; `; + } + + /** + * 应用开本大小样式 + */ + private applyBookSizeStyles(element: HTMLElement, bookSize: string) { + const sizeMap: Record = { + 'A4': { aspectRatio: '210/297' }, + 'A5': { aspectRatio: '148/210' }, + 'A3': { aspectRatio: '297/420' }, + 'Legal': { aspectRatio: '8.5/14' }, + 'Letter': { aspectRatio: '8.5/11' }, + 'Tabloid': { aspectRatio: '11/17' } + }; + + const size = sizeMap[bookSize] || sizeMap['A4']; + element.style.aspectRatio = size.aspectRatio; + element.style.width = '100%'; + element.style.height = 'auto'; + } + + /** + * 将封面HTML转换为图片 + */ + private async convertCoverToImage(coverElement: HTMLElement): Promise { + try { + // 确保浏览器完成重绘并等待资源加载 + await new Promise(resolve => setTimeout(resolve, 300)); + + // 导入 html-to-image 库 + const htmlToImage = require('html-to-image'); + + // 配置导出选项 + const exportConfig = { + quality: 1, + pixelRatio: 2, // 提高分辨率 + backgroundColor: '#333333', // 默认背景色 + style: { + transform: 'scale(1)', + transformOrigin: 'top left' + } + }; + + try { + // 首选方法:直接转换为 DataURL + const dataUrl = await htmlToImage.toPng(coverElement, exportConfig); + return dataUrl; + } catch (err) { + console.warn('toPng 失败,尝试备用方法', err); + + // 备用方法:使用 toCanvas 然后转换为 DataURL + const canvas = await htmlToImage.toCanvas(coverElement, exportConfig); + return canvas.toDataURL('image/png', 0.9); + } + } catch (error) { + console.error('封面转图片失败:', error); + return null; + } + } + + /** + * 创建简单的文本封面并合并 + */ + private async createSimpleCoverAndMerge(contentPdfBuffer: Buffer): Promise { + // 创建一个新的PDF文档作为封面 + const coverPdfDoc = await PDFDocument.create(); + + // 根据选择的开本大小设置页面尺寸 + const pageSizes = { + 'A4': [595.28, 841.89], + 'A5': [419.53, 595.28], + 'A3': [841.89, 1190.55], + 'Letter': [612, 792], + 'Legal': [612, 1008], + 'Tabloid': [792, 1224] + }; + + const pageSize = pageSizes[this.exportSettings.bookSize as keyof typeof pageSizes] || pageSizes['A4']; + const coverPage = coverPdfDoc.addPage([pageSize[0], pageSize[1]]); + + // 如果没有封面图片,创建一个简单的文本封面 + const { rgb } = require('pdf-lib'); + + // 添加标题 + coverPage.drawText(this.selectedBook.basic.title || 'Book Title', { + x: 50, + y: coverPage.getHeight() - 150, + size: 24, + color: rgb(0, 0, 0) + }); + + // 添加副标题(如果有) + if (this.selectedBook.basic.subtitle) { + coverPage.drawText(this.selectedBook.basic.subtitle, { + x: 50, + y: coverPage.getHeight() - 200, + size: 18, + color: rgb(0.3, 0.3, 0.3) + }); + } + + // 添加作者(如果有) + if (this.selectedBook.basic.author && this.selectedBook.basic.author.length > 0) { + coverPage.drawText(this.selectedBook.basic.author.join(', '), { + x: 50, + y: coverPage.getHeight() - 250, + size: 16, + color: rgb(0.5, 0.5, 0.5) + }); + } + + // 将封面保存为Buffer + const coverPdfBytes = await coverPdfDoc.save(); + + // 加载内容PDF + const contentPdfDoc = await PDFDocument.load(contentPdfBuffer); + + // 创建最终的PDF文档 + const finalPdfDoc = await PDFDocument.create(); + + // 复制封面页到最终文档 + const [coverPageCopy] = await finalPdfDoc.copyPages(coverPdfDoc, [0]); + finalPdfDoc.addPage(coverPageCopy); + + // 复制内容页到最终文档 + const contentPages = await finalPdfDoc.copyPages( + contentPdfDoc, + contentPdfDoc.getPageIndices() + ); + contentPages.forEach(page => finalPdfDoc.addPage(page)); + + // 保存最终文档 + return Buffer.from(await finalPdfDoc.save()); + } + // 处理页眉页脚模板中的变量(如 {{title}}、{{author}}、{{date}}) private processVariables(text: string): string { if (!text) return ''; diff --git a/src/services/BookRenderService.ts b/src/services/BookRenderService.ts index 816930c..97bdd44 100644 --- a/src/services/BookRenderService.ts +++ b/src/services/BookRenderService.ts @@ -64,26 +64,6 @@ export class BookRenderService { const { data, docs } = await this.getAllFiles(book, rootPath, config); await this.renderFiles(data, docs, config); - // 处理封面 - if (config.showCover && config.coverSettings) { - - // 生成封面 HTML - const coverHtml = this.generateCover(config.coverSettings, book); - - // 将封面插入到文档的开头 - if (coverHtml) { - const doc = this.docs[0].doc; - const contentEl = doc.querySelector('.markdown-preview-view'); - if (contentEl) { - const coverContainer = doc.createElement('div'); - coverContainer.innerHTML = coverHtml; - const firstChild = coverContainer.firstChild; - if (firstChild) { - contentEl.insertBefore(firstChild, contentEl.firstChild); - } - } - } - } // 2. 如果启用了目录,生成目录 HTML let tocHtml = ''; if (config.headerFooterToc?.tocEnabled) { @@ -106,22 +86,20 @@ export class BookRenderService { } // 5. 将目录和渲染好的文档注入到webview + // 先处理目录,将其插入到内容最前面 if (tocHtml) { - // 将目录插入到文档的开头 const doc = this.docs[0].doc; const contentEl = doc.querySelector('.markdown-preview-view'); if (contentEl) { const tocContainer = doc.createElement('div'); tocContainer.innerHTML = tocHtml; - const firstChild = tocContainer.firstChild; - if (firstChild) { - contentEl.insertBefore(firstChild, contentEl.firstChild); - } + contentEl.insertBefore(tocContainer, contentEl.firstChild); } } await this.appendWebview(webview, this.docs[0]); + // 6. 注入补丁样式 const patchStyles = this.getPatchStyles(); for (const css of patchStyles) { @@ -137,133 +115,6 @@ export class BookRenderService { }); } - /** - * 生成封面 HTML - * @param settings 封面设置 - * @param book 书籍信息 - * @returns 封面 HTML 字符串 - */ - private generateCover(settings: CoverSettings, book: Book): string { - if (!settings) return ''; - - // 创建临时容器元素 - const tempContainer = document.createElement('div'); - tempContainer.className = 'book-cover'; - // 添加分页符样式 - tempContainer.style.pageBreakAfter = 'always'; - tempContainer.style.breakAfter = 'page'; - // 设置开本大小样式 - this.applyBookSizeStyles(tempContainer, settings.bookSize || 'A4'); - - // 设置背景图片 - if (settings.imageUrl) { - tempContainer.style.backgroundImage = `url(${settings.imageUrl})`; - tempContainer.style.backgroundSize = `${settings.scale * 100}%`; - tempContainer.style.backgroundPosition = `${settings.position.x}px ${settings.position.y}px`; - tempContainer.style.backgroundRepeat = 'no-repeat'; - } - - // 创建内容容器 - const contentContainer = document.createElement('div'); - contentContainer.className = 'cover-content'; - contentContainer.style.position = 'relative'; - 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'; - tempContainer.appendChild(contentContainer); - - // 添加书籍信息 - // 使用自定义文本和位置 - const titleText = settings.customTitle || book.basic.title; - const subtitleText = settings.customSubtitle || book.basic.subtitle; - const authorText = settings.customAuthor || (book.basic.author ? book.basic.author.join(', ') : ''); - - // 添加书名 - if (titleText) { - const titleEl = document.createElement('div'); - titleEl.className = 'cover-title'; - titleEl.textContent = titleText; - - let titleStyle = ''; - if (settings.titleStyleConfig) { - titleStyle = this.buildStyleString(settings.titleStyleConfig); - } else { - titleStyle = settings.titleStyle || ''; - } - titleEl.setAttribute('style', titleStyle + `position: absolute; left: ${settings.titlePosition?.x || 50}%; top: ${settings.titlePosition?.y || 30}%; transform: translate(-50%, -50%); z-index: 10;`); - contentContainer.appendChild(titleEl); - } - - // 添加副标题 - if (subtitleText) { - const subtitleEl = document.createElement('div'); - subtitleEl.className = 'cover-subtitle'; - subtitleEl.textContent = subtitleText; - - let subtitleStyle = ''; - if (settings.subtitleStyleConfig) { - subtitleStyle = this.buildStyleString(settings.subtitleStyleConfig); - } else { - subtitleStyle = 'font-size: 18px; color: #ffffff; text-shadow: 0 1px 2px rgba(0,0,0,0.5);'; - } - subtitleEl.setAttribute('style', subtitleStyle + `position: absolute; left: ${settings.subtitlePosition?.x || 50}%; top: ${settings.subtitlePosition?.y || 50}%; transform: translate(-50%, -50%); z-index: 10;`); - contentContainer.appendChild(subtitleEl); - } - - // 添加作者信息 - if (authorText) { - const authorEl = document.createElement('div'); - authorEl.className = 'cover-author'; - authorEl.textContent = authorText; - - let authorStyle = ''; - if (settings.authorStyleConfig) { - authorStyle = this.buildStyleString(settings.authorStyleConfig); - } else { - authorStyle = settings.authorStyle || ''; - } - authorEl.setAttribute('style', authorStyle + `position: absolute; left: ${settings.authorPosition?.x || 50}%; top: ${settings.authorPosition?.y || 70}%; transform: translate(-50%, -50%); z-index: 10;`); - contentContainer.appendChild(authorEl); - } - - // 返回生成的 HTML - return tempContainer.outerHTML; - } - - /** - * 构建样式字符串 - * @param styleConfig 样式配置 - * @returns 样式字符串 - */ - private buildStyleString(styleConfig: any): string { - return `font-size: ${styleConfig.fontSize}px; color: ${styleConfig.color}; font-weight: ${styleConfig.fontWeight}; font-style: ${styleConfig.fontStyle}; text-shadow: ${styleConfig.textShadow || 'none'}; `; - } - - /** - * 应用开本大小样式 - * @param element 目标元素 - * @param bookSize 开本大小 - */ - private applyBookSizeStyles(element: HTMLElement, bookSize: string) { - const sizeMap: Record = { - 'A4': { aspectRatio: '210/297' }, - 'A5': { aspectRatio: '148/210' }, - 'A3': { aspectRatio: '297/420' }, - 'Legal': { aspectRatio: '8.5/14' }, - 'Letter': { aspectRatio: '8.5/11' }, - 'Tabloid': { aspectRatio: '11/17' } - }; - - const size = sizeMap[bookSize] || sizeMap['A4']; - element.style.aspectRatio = size.aspectRatio; - element.style.width = '100%'; - element.style.height = 'auto'; - } - /** * 收集所有文件 - 对应 modal.ts 的 getAllFiles */ diff --git a/src/styles/modals/export-modal.css b/src/styles/modals/export-modal.css index f07c149..1c509a4 100644 --- a/src/styles/modals/export-modal.css +++ b/src/styles/modals/export-modal.css @@ -15,6 +15,43 @@ max-height: 70vh; } +/* 模态框头部 */ +.export-modal-header { + position: relative; + padding: 24px 24px 16px; + border-bottom: 1px solid var(--export-border); + background: linear-gradient(135deg, var(--background-primary) 0%, var(--background-secondary) 100%); +} + +.export-modal-title { + margin: 0; + font-size: 24px; + font-weight: 700; + color: var(--text-normal); + text-align: center; +} + +/* 隐藏关闭按钮 */ +.export-modal-close { + display: none; +} + +/* 模态框内容 */ +.export-modal-content { + padding: 24px; + max-height: calc(70vh - 120px); + overflow-y: auto; +} + +/* 模态框底部 */ +.export-modal-footer { + padding: 16px 24px; + border-top: 1px solid var(--export-border); + background: var(--background-primary); + display: flex; + justify-content: flex-end; +} + /* 主要内容区域 - 支持动态布局切换 */ .export-modal-main { display: flex; @@ -67,13 +104,21 @@ background: var(--background-primary); overflow: hidden; transition: all 0.3s ease; - margin: 24px 0 24px 0; + margin: 24px 0; } /* 居中布局时隐藏预览面板 */ .export-modal-main.centered .export-preview-panel { display: none; } + +/* 预览头部 */ +.preview-header { + padding: 16px 20px; + border-bottom: 1px solid var(--export-border); + background: var(--background-secondary); +} + .preview-header-content { display: flex; justify-content: space-between; @@ -81,6 +126,14 @@ margin-bottom: 10px; } +.preview-title { + margin: 0; + font-size: 16px; + font-weight: 600; + color: var(--text-normal); +} + +/* 预览渲染按钮 */ .preview-render-btn { background-color: var(--interactive-accent); color: var(--text-on-accent); @@ -100,22 +153,54 @@ opacity: 0.6; cursor: not-allowed; } -.preview-header { - padding: 16px 20px; + +/* 预览滚动容器 - 包含封面和内容 */ +.preview-scroll-container { + flex: 1; + overflow: auto; + height: 100%; + display: flex; + flex-direction: column; + scroll-behavior: smooth; +} + +/* 封面预览容器 */ +.cover-preview-container { + margin-bottom: 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border-radius: var(--export-radius); + background: var(--background-primary); + overflow: hidden; +} + +/* 封面预览标题 */ +.cover-preview-header { + padding: 8px 12px; border-bottom: 1px solid var(--export-border); + display: flex; + justify-content: space-between; + align-items: center; background: var(--background-secondary); } -.preview-title { +.cover-preview-title { margin: 0; - font-size: 16px; + font-size: 14px; font-weight: 600; color: var(--text-normal); } +/* 封面预览内容 */ +.cover-preview-content { + padding: 15px; + display: flex; + justify-content: center; +} + +/* 预览内容区域 */ .preview-content { flex: 1; - overflow: auto; + overflow: visible; scroll-behavior: smooth; } @@ -180,43 +265,6 @@ margin: 0; } -/* 模态框头部 */ -.export-modal-header { - position: relative; - padding: 24px 24px 16px; - border-bottom: 1px solid var(--export-border); - background: linear-gradient(135deg, var(--background-primary) 0%, var(--background-secondary) 100%); -} - -.export-modal-title { - margin: 0; - font-size: 24px; - font-weight: 700; - color: var(--text-normal); - text-align: center; -} - -/* 隐藏关闭按钮 */ -.export-modal-close { - display: none; -} - -/* 模态框内容 */ -.export-modal-content { - padding: 24px; - max-height: calc(70vh - 120px); - overflow-y: auto; -} - -/* 模态框底部 */ -.export-modal-footer { - padding: 16px 24px; - border-top: 1px solid var(--export-border); - background: var(--background-primary); - display: flex; - justify-content: flex-end; -} - /* 书籍信息卡片 */ .export-book-card { display: flex; @@ -226,7 +274,7 @@ background: var(--export-secondary); border-radius: var(--export-radius); border: 1px solid var(--export-border); - margin: 24px 0 24px 0; + margin: 24px 0; transition: all 0.2s ease; } @@ -424,6 +472,43 @@ color: var(--text-normal); } +/* 设置控件样式 */ +.export-setting-toggle { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; +} + +.export-toggle-input { + width: 16px; + height: 16px; + cursor: pointer; +} + +.export-toggle-label { + font-size: 14px; + color: var(--text-normal); + cursor: pointer; +} + +.export-setting-button { + width: 100%; + padding: 8px 16px; + border: 1px solid var(--export-border); + border-radius: 4px; + background: var(--export-secondary); + color: var(--text-normal); + font-size: 13px; + cursor: pointer; + transition: all 0.2s ease; +} + +.export-setting-button:hover { + background: var(--background-modifier-hover); + border-color: var(--export-primary); +} + /* 优化后的选择框样式 */ .export-setting-select { width: 100%; @@ -464,42 +549,6 @@ color: var(--text-normal); } -.export-setting-toggle { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 12px; -} - -.export-toggle-input { - width: 16px; - height: 16px; - cursor: pointer; -} - -.export-toggle-label { - font-size: 14px; - color: var(--text-normal); - cursor: pointer; -} - -.export-setting-button { - width: 100%; - padding: 8px 16px; - border: 1px solid var(--export-border); - border-radius: 4px; - background: var(--export-secondary); - color: var(--text-normal); - font-size: 13px; - cursor: pointer; - transition: all 0.2s ease; -} - -.export-setting-button:hover { - background: var(--background-modifier-hover); - border-color: var(--export-primary); -} - /* 信息卡片 */ .export-info-card { background: var(--background-secondary); @@ -549,6 +598,12 @@ height: 300px; } +.export-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + .export-btn-secondary { background: var(--export-secondary); color: var(--text-normal); @@ -579,13 +634,6 @@ transform: none; } -/* 导出按钮禁用状态 */ -.export-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - pointer-events: none; -} - /* 隐藏旧的action-section */ .export-action-section { display: none; @@ -660,25 +708,25 @@ /* 滚动条样式 */ .export-modal-content::-webkit-scrollbar, -.preview-content::-webkit-scrollbar { +.preview-scroll-container::-webkit-scrollbar { width: 6px; } .export-modal-content::-webkit-scrollbar-track, -.preview-content::-webkit-scrollbar-track { +.preview-scroll-container::-webkit-scrollbar-track { background: var(--background-secondary); border-radius: 3px; } .export-modal-content::-webkit-scrollbar-thumb, -.preview-content::-webkit-scrollbar-thumb { +.preview-scroll-container::-webkit-scrollbar-thumb { background: var(--text-faint); border-radius: 3px; transition: background 0.2s ease; } .export-modal-content::-webkit-scrollbar-thumb:hover, -.preview-content::-webkit-scrollbar-thumb:hover { +.preview-scroll-container::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } @@ -707,6 +755,10 @@ .export-settings-panel { flex: none; } + + .preview-scroll-container { + max-height: 400px; + } } @media (max-width: 768px) { From 6dbdf7aa17e4c3ecc567749a82edb4b592dea91f Mon Sep 17 00:00:00 2001 From: Yeban8090 Date: Tue, 8 Jul 2025 18:41:20 +0800 Subject: [PATCH 07/10] =?UTF-8?q?=E5=AF=BC=E5=87=BAPDF=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- src/components/TypographyView.ts | 1063 ---------------------------- src/modals/ExportModal.ts | 129 ++-- src/modals/HeaderFooterTocModal.ts | 6 +- src/services/BookRenderService.ts | 2 +- src/services/ExportService.ts | 707 ------------------ src/styles/modals/export-modal.css | 206 ++++-- 7 files changed, 219 insertions(+), 1897 deletions(-) delete mode 100644 src/components/TypographyView.ts delete mode 100644 src/services/ExportService.ts diff --git a/.gitignore b/.gitignore index 98e6e49..1c83a9c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,4 @@ styles.css data.json # Exclude macOS Finder (System Explorer) View States -.DS_Store -other \ No newline at end of file +.DS_Store \ No newline at end of file diff --git a/src/components/TypographyView.ts b/src/components/TypographyView.ts deleted file mode 100644 index 1a882df..0000000 --- a/src/components/TypographyView.ts +++ /dev/null @@ -1,1063 +0,0 @@ -import { App, setIcon, Notice, TFile, MarkdownRenderer } from 'obsidian'; -import { BookManager } from "../services/BookManager"; -import * as htmlToImage from 'html-to-image'; -import { Book, CoverSettings, ChapterNode } from "../types/book"; -import { ExportService } from "../services/ExportService"; -import { i18n } from "../i18n/i18n"; -import BookSmithPlugin from '../main'; -import { CoverManager } from '../services/CoverManager'; -import { CoverSettingModal } from '../modals/CoverSettingModal'; -import { ExportModal } from '../modals/ExportModal'; -import { HeaderFooterTocSettings, HeaderFooterTocModal } from '../modals/HeaderFooterTocModal'; -export interface TypographySettings { - fontFamily: string; - fontSize: string; - lineHeight: string; - margin: string; - templateId: string; - themeId: string; - coverSettings?: CoverSettings; - showCover: boolean; - bookSize: string; - coverImageData?: string; // 新增:封面图片的 base64 数据 - headerFooterToc?: HeaderFooterTocSettings; -} - -export class TypographyView { - private container: HTMLElement; - private bookManager: BookManager; - private exportService: ExportService; - - private coverManager: CoverManager; - private books: Book[] = []; - private selectedBook: Book | null = null; - private previewElement: HTMLElement | null = null; - private coverPreviewElement: HTMLElement | null = null; - - private coverSettings: CoverSettings | null = null; - private headerFooterTocSettings: HeaderFooterTocSettings | 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.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.initializeDefaultHeaderFooterTocSettings(); - - // 初始化各个选择器 - this.initializeBookSelect(); - 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) { - // 创建一个包含封面和内容的容器 - 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 selectorsRow = container.createDiv({ cls: 'typography-selectors-row' }); - // 1. 创建书籍选择器 - this.createBookSelector(selectorsRow); - // 2. 创建模板选择器 - // this.createTemplateSelector(selectorsRow); - // 3. 创建主题选择器 - // this.createThemeSelector(selectorsRow); - // 4. 创建字体选择器 - this.createFontSelector(selectorsRow); - // 5. 创建字号大小控制 - this.createFontSizeControls(selectorsRow); - // 6. 创建开本大小选择器 - this.createBookSizeSelector(selectorsRow); - } - - // 自定义选择器通用方法 - private createCustomSelect(parent: HTMLElement, className: string, options: { value: string, text: string }[]): HTMLElement { - const container = parent.createDiv({ cls: `book-smith-select-container ${className}` }); - - const select = container.createDiv({ cls: 'book-smith-select' }); - const textSpan = select.createSpan({ cls: 'book-smith-text' }); - const arrowSpan = select.createSpan({ cls: 'book-smith-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: 'book-smith-select-dropdown' }); - - // 添加选项 - for (const option of options) { - const item = dropdown.createDiv({ cls: 'book-smith-select-item' }); - item.textContent = option.text; - item.dataset.value = option.value; - - // 默认选中第一个 - if (options.indexOf(option) === 0) { - item.addClass('book-smith-selected'); - } - - // 点击选项 - item.addEventListener('click', () => { - // 更新显示文本和值 - textSpan.textContent = option.text; - select.dataset.value = option.value; - - // 更新选中状态 - dropdown.querySelectorAll('.book-select-item').forEach(el => { - el.removeClass('book-smith-selected'); - }); - item.addClass('book-smith-selected'); - - // 关闭下拉菜单 - dropdown.removeClass('book-smith-show'); - arrowSpan.style.transform = ''; - - // 触发自定义事件 - select.dispatchEvent(new CustomEvent('change', { - detail: { value: option.value, text: option.text } - })); - }); - } - - // 点击选择器显示/隐藏下拉菜单 - select.addEventListener('click', (e) => { - e.stopPropagation(); - dropdown.toggleClass('book-smith-show', !dropdown.hasClass('book-smith-show')); - arrowSpan.style.transform = dropdown.hasClass('book-smith-show') ? 'rotate(180deg)' : ''; - }); - - // 点击其他地方关闭下拉菜单 - document.addEventListener('click', () => { - dropdown.removeClass('book-smith-show'); - arrowSpan.style.transform = ''; - }); - - return container; - } - - // 更新自定义选择器的选项 - private updateCustomSelectOptions(selectElement: HTMLElement, options: { value: string, text: string }[]) { - const dropdown = selectElement.querySelector('.book-smith-select-dropdown'); - const select = selectElement.querySelector('.book-smith-select'); - const textSpan = selectElement.querySelector('.book-smith-text'); - - if (!dropdown || !select || !textSpan) return; - - // 清空现有选项 - dropdown.innerHTML = ''; - - // 添加新选项 - for (const option of options) { - const item = dropdown.createDiv({ cls: 'book-smith-select-item' }); - item.textContent = option.text; - item.dataset.value = option.value; - - // 默认选中第一个 - if (options.indexOf(option) === 0) { - item.addClass('book-smith-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('.book-smith-select-item').forEach(el => { - el.removeClass('book-smith-selected'); - }); - item.addClass('book-smith-selected'); - - // 关闭下拉菜单 - dropdown.removeClass('book-smith-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, - 'book-smith-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('.book-smith-select')?.addEventListener('change', async (e: any) => { - const value = e.detail.value; - this.selectedBook = this.books.find(book => book.basic.uuid === value) || null; - - // 先清空封面预览元素 - if (this.coverPreviewElement) { - this.coverPreviewElement.empty(); - } - - // 使用CoverManager智能加载封面配置 - if (this.selectedBook) { - this.coverSettings = this.coverManager.getBookCoverSettings(this.selectedBook); - } else { - this.coverSettings = null; - } - - this.updatePreview(); - }); - - // 如果有书籍,选择第一本并加载其封面配置 - if (bookOptions.length > 0) { - const select = this.customBookSelect.querySelector('.book-smith-select'); - if (select) { - (select as HTMLElement).setAttribute('data-value', bookOptions[0].value); - this.selectedBook = this.books[0]; - - // 初始化时也清空封面预览元素 - if (this.coverPreviewElement) { - this.coverPreviewElement.empty(); - } - - // 使用CoverManager智能加载封面配置 - this.coverSettings = this.coverManager.getBookCoverSettings(this.selectedBook); - - this.updatePreview(); - } - } - } - - // 4. 字体选择器 - private createFontSelector(parent: HTMLElement) { - const fontOptions = [ - { value: 'default', text: i18n.t('DEFAULT_FONT') || '默认字体' }, - { value: 'songti', text: i18n.t('SONGTI_FONT') || '宋体' }, - { value: 'heiti', text: i18n.t('HEITI_FONT') || '黑体' }, - { value: 'kaiti', text: i18n.t('KAITI_FONT') || '楷体' }, - { value: 'fangsong', text: i18n.t('FANGSONG_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, - 'book-smith-font-select', - fontOptions - ); - this.customFontSelect.id = 'font-select'; - } - - // 初始化字体选择器 - private initializeFontSelect() { - if (!this.customFontSelect) return; - - // 添加事件监听 - this.customFontSelect.querySelector('.book-smith-select')?.addEventListener('change', () => { - this.updatePreview(); - }); - } - - // 5. 字号大小控制 - private createFontSizeControls(parent: HTMLElement) { - const fontSizeGroup = parent.createEl('div', { cls: 'book-smith-font-size-group' }); - - // 添加减小按钮 - fontSizeGroup.createEl('button', { - cls: 'book-smith-font-size-button book-smith-decrease-button', - text: '-' - }); - - this.fontSizeInput = fontSizeGroup.createEl('input', { - cls: 'book-smith-font-size-input', - type: 'text', - value: '16', - attr: { - style: 'border: none; outline: none; background: transparent;', - min: '12', - max: '30' - } - }) as HTMLInputElement; - - // 添加增大按钮 - fontSizeGroup.createEl('button', { - cls: 'book-smith-font-size-button book-smith-increase-button', - text: '+' - }); - - // 添加单位标签 - fontSizeGroup.createEl('span', { - cls: 'book-smith-font-size-unit' - }); - } - - // 初始化字号控制 - private initializeFontSizeControls() { - if (!this.fontSizeInput) return; - - const updateFontSize = () => { - if (!this.fontSizeInput) return; - - // 获取当前值并确保在有效范围内 - let currentSize = parseInt(this.fontSizeInput.value) || 16; - currentSize = Math.max(12, Math.min(30, currentSize)); - - // 更新输入框值 - this.fontSizeInput.value = currentSize.toString(); - - // 更新预览 - this.updatePreview(); - }; - - // 获取按钮元素 - const decreaseButton = this.fontSizeInput.parentElement?.querySelector('.book-smith-decrease-button'); - const increaseButton = this.fontSizeInput.parentElement?.querySelector('.book-smith-increase-button'); - - // 添加减小字号事件 - 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 createBookSizeSelector(parent: HTMLElement) { - const bookSizeOptions = [ - { value: 'A4', text: 'A4' }, - { value: 'A5', text: 'A5' }, - { value: 'A3', text: 'A3' }, - { value: 'Legal', text: 'Legal' }, - { value: 'Letter', text: 'Letter' }, - { value: 'Tabloid', text: 'Tabloid' } - ]; - - this.customBookSizeSelect = this.createCustomSelect( - parent, - 'book-smith-book-size-select', - bookSizeOptions - ); - this.customBookSizeSelect.id = 'book-size-select'; - } - - // 初始化开本大小选择器 - private initializeBookSizeSelect() { - if (!this.customBookSizeSelect) return; - - // 添加事件监听 - this.customBookSizeSelect.querySelector('.book-smith-select')?.addEventListener('change', () => { - this.updatePreview(); - // 如果显示封面,也更新封面预览 - const coverToggle = document.querySelector('.cover-toggle-input') as HTMLInputElement; - if (coverToggle?.checked) { - this.updateCoverPreview(); - } - }); - } - - // 获取排版设置 - private async getTypographySettings(): Promise { - // 获取封面开关状态 - const coverToggle = this.parentEl.querySelector('.cover-toggle-input') as HTMLInputElement; - const showCover = coverToggle ? coverToggle.checked : true; - - // 如果显示封面,转换封面为图片 - let coverImageData: string | undefined; - if (showCover && this.coverSettings) { - coverImageData = (await this.convertCoverToImage()) || undefined; - } - - // 获取字体系列 - const fontFamily = (this.customFontSelect?.querySelector('.book-smith-select') as HTMLElement)?.getAttribute('data-value') || 'default'; - - // 获取实际的字体CSS定义 - const fontFamilyCSS = this.getFontFamilyCSS(fontFamily); - - return { - fontFamily: fontFamilyCSS, - fontSize: this.fontSizeInput?.value || '16', - lineHeight: 'normal', // 保留默认值 - margin: '0', // 保留默认值 - templateId: (this.customTemplateSelect?.querySelector('.book-smith-select') as HTMLElement)?.getAttribute('data-value') || 'notes', - themeId: (this.customThemeSelect?.querySelector('.book-smith-select') as HTMLElement)?.getAttribute('data-value') || 'default', - coverSettings: this.coverSettings || undefined, - showCover: showCover, - bookSize: (this.customBookSizeSelect?.querySelector('.book-smith-select') as HTMLElement)?.getAttribute('data-value') || 'a4', - coverImageData: coverImageData || undefined, // 新增封面图片数据 - headerFooterToc: this.headerFooterTocSettings || undefined // 新增页眉页脚目录设置 - }; - } - - // 获取字体CSS定义 - private getFontFamilyCSS(fontFamily: string): string { - switch (fontFamily) { - case 'serif': - return 'Georgia, Times, \'Times New Roman\', serif'; - case 'sans-serif': - return '-apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, Helvetica, Arial, sans-serif'; - case 'monospace': - return '\'SFMono-Regular\', Consolas, \'Liberation Mono\', Menlo, Courier, monospace'; - case 'songti': - return 'SimSun, "宋体", STSong, "华文宋体", serif'; - case 'heiti': - return 'SimHei, "黑体", STHeiti, "华文黑体", sans-serif'; - case 'kaiti': - return 'KaiTi, "楷体", STKaiti, "华文楷体", serif'; - case 'fangsong': - return 'FangSong, "仿宋", STFangsong, "华文仿宋", serif'; - case 'default': - default: - return 'var(--default-font)'; - } - } - - // 预览相关方法 - private async updatePreview() { - if (!this.selectedBook) return; - - // 获取封面开关状态 - const coverToggle = this.parentEl.querySelector('.cover-toggle-input') as HTMLInputElement; - const showCover = coverToggle ? coverToggle.checked : true; - - // 分离封面和内容的显示逻辑 - 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(); - // 渲染内容 - await this.renderPaginatedContent(); - } - } - - private async renderPaginatedContent() { - if (!this.previewElement) return; - - // 清空预览区域 - this.previewElement.empty(); - - // 获取排版设置 - const settings = this.getTypographySettings(); - - // 更新预览样式 - // 等待 settings Promise 解析完成后再更新样式 - settings.then(resolvedSettings => { - this.updatePreviewStyle(resolvedSettings); - }); - - // 渲染所有内容 - await this.renderAllContent(this.previewElement); - - // 在渲染完成后应用主题和模板 - // if (this.currentTemplate) { - // this.currentTemplate.render(this.previewElement); - // } - // this.themeManager.applyTheme(this.previewElement); - } - - // 渲染所有内容到指定容器 - 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.style.fontFamily = `${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-container' }); - - // 第一行按钮组 - 封面相关 - const buttonsGroup = buttonsContainer.createDiv({ cls: 'typography-buttons-group cover-buttons-group' }); - - // 添加封面设计开关 - this.createCoverToggle(buttonsGroup); - - // 导出按钮 - const exportBtn = buttonsGroup.createEl('button', { - text: i18n.t('EXPORT') || '导出', - cls: 'typography-btn export-btn' - }); - exportBtn.addEventListener('click', () => this.exportWithTypography()); - } - - // 添加封面设计开关 - 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 = true; // 默认不显示封面 - - coverToggle.addEventListener('change', () => { - this.updatePreview(); - }); - - // 添加封面设计按钮 - const coverDesignBtn = coverToggleContainer.createEl('button', { - text: i18n.t('DESIGN_COVER') || '设计封面', - cls: 'cover-design-btn' - }); - - coverDesignBtn.addEventListener('click', () => this.openCoverDesigner()); - - // 页眉页脚和目录设置按钮 - const headerFooterTocButton = coverToggleContainer.createEl('button', { - text: '页眉页脚和目录', - cls: 'cover-design-btn' - }); - headerFooterTocButton.addEventListener('click', () => { - this.openHeaderFooterTocModal(); - }); - } - - // 打开封面设计器 - private openCoverDesigner() { - if (!this.selectedBook) { - new Notice(i18n.t('SELECT_BOOK_FIRST') || '请先选择一本书籍'); - return; - } - - // 使用CoverManager获取当前的封面配置 - const currentCoverSettings = this.coverSettings || this.coverManager.getBookCoverSettings(this.selectedBook); - - // 获取当前选择的开本大小并添加到封面配置中 - const currentBookSize = this.getSelectedBookSize(); - if (currentBookSize) { - currentCoverSettings.bookSize = currentBookSize; - } - - new CoverSettingModal( - this.app, - async (settings) => { - this.coverSettings = settings; - if (!this.selectedBook) { - new Notice('请先选择一本书籍'); - return; - } - - // 保存封面设计到书籍元数据 - try { - await this.bookManager.updateBook(this.selectedBook.basic.uuid, { - basic: { - ...this.selectedBook.basic, - coverSettings: settings, - // 如果设计中包含图片,也更新cover字段 - cover: settings.imageUrl || this.selectedBook.basic.cover - } - }); - - // 更新当前选中的书籍对象 - this.selectedBook.basic.coverSettings = settings; - if (settings.imageUrl) { - this.selectedBook.basic.cover = settings.imageUrl; - } - - this.updatePreview(); - new Notice('封面设计已保存'); - } catch (error) { - console.error('保存封面设计失败:', error); - new Notice('保存封面设计失败'); - } - }, - this.parentEl, - this.coverManager, - currentCoverSettings, - this.selectedBook.basic.title, - this.selectedBook.basic.author, - this.selectedBook.basic.subtitle - ).open(); - } - - // 新增方法:获取当前选择的开本大小 - private getSelectedBookSize(): string { - const bookSizeSelect = this.customBookSizeSelect?.querySelector('.book-smith-select') as HTMLElement; - return bookSizeSelect?.getAttribute('data-value') || 'A4'; - } - - // 新增方法:初始化默认的页眉页脚设置 - private initializeDefaultHeaderFooterTocSettings() { - this.headerFooterTocSettings = { - headerEnabled: true, - headerLeft: '{{title}}', - headerCenter: '', - headerRight: '{{author}}', - headerFontSize: 12, - headerColor: '#000000', - headerHeight: 15, - - footerEnabled: true, - footerLeft: '', - footerCenter: '', - footerRight: '{{pageNumber}}/{{totalPages}}', - footerFontSize: 12, - footerColor: '#000000', - footerHeight: 20, - - tocEnabled: true, - tocTitle: '目录', - tocMaxLevel: 3, - tocFontSize: 14, - tocFontFamily: 'serif', - tocColor: '#000000', - tocLineHeight: 1.5, - tocIndentSize: 20, - tocIndent: 20, - tocPageBreak: true - }; - } - - private async openHeaderFooterTocModal() { - const currentSettings = await this.getTypographySettings(); - const modal = new HeaderFooterTocModal( - this.plugin, - currentSettings.headerFooterToc || {}, - (settings) => { - // 保存设置并更新预览 - this.headerFooterTocSettings = settings; - this.updatePreview(); - } - ); - modal.open(); - } - - // 添加封面预览更新方法 - private updateCoverPreview() { - if (!this.coverPreviewElement || !this.selectedBook) return; - - // 清除现有内容(确保彻底清空) - this.coverPreviewElement.empty(); - // 清空背景图和所有样式 - this.coverPreviewElement.style.backgroundImage = ''; - this.coverPreviewElement.style.background = ''; - this.coverPreviewElement.removeAttribute('style'); - - // 获取封面配置(如果没有当前配置,从CoverManager获取) - const coverSettings = this.coverSettings || this.coverManager.getBookCoverSettings(this.selectedBook); - - // 获取当前选择的开本大小并应用到封面配置 - const currentBookSize = this.getSelectedBookSize(); - if (currentBookSize) { - coverSettings.bookSize = currentBookSize; - } - - // 应用开本大小样式到预览元素 - this.applyBookSizeToPreview(this.coverPreviewElement, coverSettings.bookSize || 'A4'); - - const contentContainer = this.coverManager.applyCoverStyles(this.coverPreviewElement, coverSettings); - - // 添加书籍信息 - if (contentContainer) { - // 使用自定义文本和位置 - const titleText = coverSettings.customTitle || this.selectedBook.basic.title; - const subtitleText = coverSettings.customSubtitle || this.selectedBook.basic.subtitle; - const authorText = coverSettings.customAuthor || (this.selectedBook.basic.author ? this.selectedBook.basic.author.join(', ') : ''); - - // 添加书名 - if (titleText) { - const titleEl = contentContainer.createEl('div', { - cls: 'cover-title', - text: titleText - }); - - let titleStyle = ''; - if (coverSettings.titleStyleConfig) { - titleStyle = this.buildStyleString(coverSettings.titleStyleConfig); - } else { - titleStyle = coverSettings.titleStyle || ''; - } - titleEl.setAttribute('style', titleStyle + `position: absolute; left: ${coverSettings.titlePosition?.x || 50}%; top: ${coverSettings.titlePosition?.y || 30}%; transform: translate(-50%, -50%); z-index: 10;`); - } - - // 添加副标题 - if (subtitleText) { - const subtitleEl = contentContainer.createEl('div', { - cls: 'cover-subtitle', - text: subtitleText - }); - - let subtitleStyle = ''; - if (coverSettings.subtitleStyleConfig) { - subtitleStyle = this.buildStyleString(coverSettings.subtitleStyleConfig); - } else { - subtitleStyle = 'font-size: 18px; color: #ffffff; text-shadow: 0 1px 2px rgba(0,0,0,0.5);'; - } - subtitleEl.setAttribute('style', subtitleStyle + `position: absolute; left: ${coverSettings.subtitlePosition?.x || 50}%; top: ${coverSettings.subtitlePosition?.y || 50}%; transform: translate(-50%, -50%); z-index: 10;`); - } - - // 添加作者信息 - if (authorText) { - const authorEl = contentContainer.createEl('div', { - cls: 'cover-author', - text: authorText - }); - - let authorStyle = ''; - if (coverSettings.authorStyleConfig) { - authorStyle = this.buildStyleString(coverSettings.authorStyleConfig); - } else { - authorStyle = coverSettings.authorStyle || ''; - } - authorEl.setAttribute('style', authorStyle + `position: absolute; left: ${coverSettings.authorPosition?.x || 50}%; top: ${coverSettings.authorPosition?.y || 70}%; transform: translate(-50%, -50%); z-index: 10;`); - } - } - } - - private buildStyleString(styleConfig: any): string { - return `font-size: ${styleConfig.fontSize}px; color: ${styleConfig.color}; font-weight: ${styleConfig.fontWeight}; font-style: ${styleConfig.fontStyle}; text-shadow: ${styleConfig.textShadow || 'none'}; `; - } - - // 新增方法:应用开本大小到预览 - private applyBookSizeToPreview(element: HTMLElement, bookSize: string) { - const sizeMap: Record = { - 'A4': { aspectRatio: '210/297' }, - 'A5': { aspectRatio: '148/210' }, - 'A3': { aspectRatio: '297/420' }, - 'Legal': { aspectRatio: '8.5/14' }, - 'Letter': { aspectRatio: '8.5/11' }, - 'Tabloid': { aspectRatio: '11/17' } - }; - - const size = sizeMap[bookSize] || sizeMap['A4']; - element.style.aspectRatio = size.aspectRatio; - element.style.width = '100%'; - element.style.height = 'auto'; - } - - // 新增方法:将封面转换为图片 - private async convertCoverToImage(): Promise { - if (!this.coverPreviewElement || !this.coverSettings) { - return null; - } - - try { - // 确保浏览器完成重绘并等待资源加载 - await new Promise(resolve => setTimeout(resolve, 300)); - - // 配置导出选项 - const exportConfig = { - quality: 1, - pixelRatio: 2, // 提高分辨率 - backgroundColor: '#333333', // 默认背景色 - style: { - transform: 'scale(1)', - transformOrigin: 'top left' - } - }; - - try { - // 首选方法:直接转换为 DataURL - const dataUrl = await htmlToImage.toPng(this.coverPreviewElement, exportConfig); - return dataUrl; - } catch (err) { - console.warn('toPng 失败,尝试备用方法', err); - - // 备用方法:使用 toCanvas 然后转换为 DataURL - const canvas = await htmlToImage.toCanvas(this.coverPreviewElement, exportConfig); - return canvas.toDataURL('image/png', 0.9); - } - } catch (error) { - console.error('封面转图片失败:', error); - return null; - } - } - - - // 导出功能 - private async exportWithTypography() { - try { - if (!this.selectedBook) { - throw new Error('未选择书籍'); - } - - // 使用新的导出模态框 - const exportModal = new ExportModal( - this.parentEl, - this.exportService, - (format) => this.executeExport(format) - ); - - exportModal.open(); - } catch (error) { - console.error('导出错误:', error); - new Notice(`导出失败: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 执行导出 - private async executeExport(format: string) { - try { - if (!this.selectedBook) { - throw new Error("未选择书籍"); - } - - let useTypography = false; - let htmlContent: HTMLElement | undefined; - let typographySettings: any | undefined; - - if (format !== "txt") { - useTypography = true; - typographySettings = await this.getTypographySettings(); // 改为 await - if (this.previewElement) { - htmlContent = this.previewElement.cloneNode(true) as HTMLElement; - } - } - - // 调用统一导出接口 - const result = await this.exportService.exportBook(format, this.selectedBook, { - useTypography, - htmlContent, - typographySettings - }); - - if (result.content === "cancelled") { - new Notice("导出已取消"); - return; - } - new Notice(`${format} 已成功导出到: ${result.content}`); - } catch (error) { - console.error("导出错误:", error); - new Notice(`导出失败: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // 移除视图 - remove() { - this.container.remove(); - } -} diff --git a/src/modals/ExportModal.ts b/src/modals/ExportModal.ts index b3967b7..15e74bc 100644 --- a/src/modals/ExportModal.ts +++ b/src/modals/ExportModal.ts @@ -200,26 +200,14 @@ export class ExportModal extends Modal { if (this.exportSettings.showCover && this.exportSettings.cover) { const coverPreviewContainer = scrollContainer.createDiv({ cls: 'cover-preview-container' }); coverPreviewContainer.style.display = 'none'; // 初始隐藏,等待渲染完成后显示 - coverPreviewContainer.style.marginBottom = '20px'; - coverPreviewContainer.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)'; - + // 创建封面预览标题 const coverPreviewHeader = coverPreviewContainer.createDiv({ cls: 'cover-preview-header' }); - coverPreviewHeader.style.padding = '8px 12px'; - coverPreviewHeader.style.borderBottom = '1px solid #e0e0e0'; - coverPreviewHeader.style.display = 'flex'; - coverPreviewHeader.style.justifyContent = 'space-between'; - coverPreviewHeader.style.alignItems = 'center'; coverPreviewHeader.createEl('span', { text: '封面预览', cls: 'cover-preview-title' }); // 添加封面预览内容区域 this.coverPreviewElement = coverPreviewContainer.createDiv({ cls: 'cover-preview-content' }); - this.coverPreviewElement.style.padding = '15px'; - this.coverPreviewElement.style.display = 'flex'; - this.coverPreviewElement.style.justifyContent = 'center'; - // 移除背景色设置 - // this.coverPreviewElement.style.backgroundColor = '#f5f5f5'; // 更新封面预览 this.updateCoverPreview(); @@ -241,32 +229,20 @@ export class ExportModal extends Modal { // 创建封面预览内部容器 const coverContainer = this.coverPreviewElement.createDiv({ cls: 'cover-container' }); - coverContainer.style.maxWidth = '250px'; - coverContainer.style.maxHeight = '350px'; - coverContainer.style.overflow = 'hidden'; - coverContainer.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)'; - + // 应用开本大小样式 - this.applyBookSizeStyles(coverContainer, this.exportSettings.cover.bookSize || this.exportSettings.bookSize || 'A4'); + this.applyBookSizeStyles(coverContainer, this.exportSettings.bookSize || 'A4'); // 设置背景图片 if (this.exportSettings.cover.imageUrl) { coverContainer.style.backgroundImage = `url(${this.exportSettings.cover.imageUrl})`; coverContainer.style.backgroundSize = `${this.exportSettings.cover.scale * 100}%`; coverContainer.style.backgroundPosition = `${this.exportSettings.cover.position.x}px ${this.exportSettings.cover.position.y}px`; - coverContainer.style.backgroundRepeat = 'no-repeat'; } // 创建内容容器 const contentContainer = coverContainer.createDiv({ cls: 'cover-content' }); - contentContainer.style.position = 'relative'; - contentContainer.style.height = '100%'; - contentContainer.style.display = 'flex'; - contentContainer.style.flexDirection = 'column'; - contentContainer.style.justifyContent = 'center'; - contentContainer.style.alignItems = 'center'; - contentContainer.style.padding = '20px'; - contentContainer.style.textAlign = 'center'; + // 添加书籍信息 const settings = this.exportSettings.cover; @@ -287,7 +263,12 @@ export class ExportModal extends Modal { } else { titleStyle = settings.titleStyle || ''; } - titleEl.setAttribute('style', titleStyle + `position: absolute; left: ${settings.titlePosition?.x || 50}%; top: ${settings.titlePosition?.y || 30}%; transform: translate(-50%, -50%); z-index: 10;`); + // 只保留动态位置设置 + titleEl.style.left = `${settings.titlePosition?.x || 50}%`; + titleEl.style.top = `${settings.titlePosition?.y || 30}%`; + if (titleStyle) { + titleEl.setAttribute('style', titleStyle + `left: ${settings.titlePosition?.x || 50}%; top: ${settings.titlePosition?.y || 30}%; position: absolute; transform: translate(-50%, -50%); z-index: 10;`); + } } // 添加副标题 @@ -298,9 +279,14 @@ export class ExportModal extends Modal { if (settings.subtitleStyleConfig) { subtitleStyle = this.buildStyleString(settings.subtitleStyleConfig); } else { - subtitleStyle = 'font-size: 18px; color: #ffffff; text-shadow: 0 1px 2px rgba(0,0,0,0.5);'; + subtitleStyle = ''; + } + // 只保留动态位置设置 + subtitleEl.style.left = `${settings.subtitlePosition?.x || 50}%`; + subtitleEl.style.top = `${settings.subtitlePosition?.y || 50}%`; + if (subtitleStyle) { + subtitleEl.setAttribute('style', subtitleStyle + `left: ${settings.subtitlePosition?.x || 50}%; top: ${settings.subtitlePosition?.y || 50}%; position: absolute; transform: translate(-50%, -50%); z-index: 10;`); } - subtitleEl.setAttribute('style', subtitleStyle + `position: absolute; left: ${settings.subtitlePosition?.x || 50}%; top: ${settings.subtitlePosition?.y || 50}%; transform: translate(-50%, -50%); z-index: 10;`); } // 添加作者信息 @@ -313,7 +299,12 @@ export class ExportModal extends Modal { } else { authorStyle = settings.authorStyle || ''; } - authorEl.setAttribute('style', authorStyle + `position: absolute; left: ${settings.authorPosition?.x || 50}%; top: ${settings.authorPosition?.y || 70}%; transform: translate(-50%, -50%); z-index: 10;`); + // 只保留动态位置设置 + authorEl.style.left = `${settings.authorPosition?.x || 50}%`; + authorEl.style.top = `${settings.authorPosition?.y || 70}%`; + if (authorStyle) { + authorEl.setAttribute('style', authorStyle + `left: ${settings.authorPosition?.x || 50}%; top: ${settings.authorPosition?.y || 70}%; position: absolute; transform: translate(-50%, -50%); z-index: 10;`); + } } } // 统一的预览状态管理方法,处理不同状态(等待、加载中、就绪、错误) @@ -641,7 +632,7 @@ export class ExportModal extends Modal { const formats = [ { key: 'pdf', label: 'PDF', icon: '📄', desc: '便携文档格式' }, - { key: 'txt', label: 'TXT', icon: '📝', desc: '纯文本格式' }, + // { key: 'txt', label: 'TXT', icon: '📝', desc: '纯文本格式' }, { key: 'docx', label: 'DOCX', icon: '📋', desc: 'Word文档格式' } ]; @@ -744,12 +735,17 @@ export class ExportModal extends Modal { const sizeCard = container.createDiv({ cls: 'export-setting-card' }); const sizeHeader = sizeCard.createDiv({ cls: 'export-setting-header' }); + sizeHeader.style.marginBottom = '16px'; sizeHeader.innerHTML = ` - 📏 - 开本大小 + 📏 + 开本大小 `; - const sizeSelect = sizeCard.createEl('select', { cls: 'export-setting-select' }); + const sizeSelectContainer = sizeCard.createDiv(); + + const sizeSelect = sizeSelectContainer.createEl('select', { cls: 'export-setting-select' }); + + const sizes = ['A4', 'A5', 'A3', 'Letter', 'Legal', 'Tabloid']; sizes.forEach(size => { const option = sizeSelect.createEl('option', { value: size, text: size }); @@ -758,26 +754,49 @@ export class ExportModal extends Modal { sizeSelect.addEventListener('change', () => { this.exportSettings.bookSize = sizeSelect.value; + + // 更新封面预览 + if (this.exportSettings.showCover && this.webviewReady) { + this.updateCoverPreview(); + } }); } } // 创建 PDF 特定设置(封面设置、页眉页脚目录设置) private createPdfSettings(container: HTMLElement) { - // PDF 特定设置 + // PDF 特定设置 - 封面卡片 const coverCard = container.createDiv({ cls: 'export-setting-card' }); const coverHeader = coverCard.createDiv({ cls: 'export-setting-header' }); coverHeader.innerHTML = ` - 🎨 - 封面设置 + 🎨 + 封面设置 `; - const coverToggle = coverCard.createDiv({ cls: 'export-setting-toggle' }); + // 创建一个包含复选框和按钮的容器,使用flex布局 + const coverControlsContainer = coverCard.createDiv({ cls: 'cover-controls-container' }); + + + // 创建包含封面选项 + const coverToggle = coverControlsContainer.createDiv({ cls: 'export-setting-toggle' }); + + const coverCheckbox = coverToggle.createEl('input', { type: 'checkbox', attr: { id: 'cover-toggle' } }); + + coverToggle.createEl('label', { text: '包含封面', attr: { for: 'cover-toggle' } }); + + coverCheckbox.checked = this.exportSettings.showCover !== false; // 修改为默认选中 + // 创建自定义封面按钮 + const coverSettingButton = coverControlsContainer.createEl('button', { + cls: 'export-setting-button', + text: '自定义封面' + }); + + coverCheckbox.addEventListener('change', () => { this.exportSettings.showCover = coverCheckbox.checked; @@ -788,13 +807,6 @@ export class ExportModal extends Modal { } }); - // 添加封面设置按钮 - const coverButtonContainer = coverCard.createDiv({ cls: 'export-setting-button-container' }); - const coverSettingButton = coverButtonContainer.createEl('button', { - cls: 'export-setting-button', - text: '自定义封面' - }); - coverSettingButton.addEventListener('click', () => { // 打开封面设置模态框 const coverModal = new CoverSettingModal( @@ -816,17 +828,20 @@ export class ExportModal extends Modal { coverModal.open(); }); - // 页眉页脚目录设置 + + // 页眉页脚目录设置卡片 const headerFooterTocCard = container.createDiv({ cls: 'export-setting-card' }); const headerFooterTocHeader = headerFooterTocCard.createDiv({ cls: 'export-setting-header' }); + headerFooterTocHeader.innerHTML = ` - 📑 - 页眉页脚目录设置 + 📑 + 页眉页脚目录设置 `; // 添加页眉页脚目录设置按钮 - const headerFooterTocButtonContainer = headerFooterTocCard.createDiv({ cls: 'export-setting-button-container' }); + const headerFooterTocButtonContainer = headerFooterTocCard.createDiv(); + const headerFooterTocButton = headerFooterTocButtonContainer.createEl('button', { cls: 'export-setting-button', text: '自定义页眉页脚和目录' @@ -904,7 +919,7 @@ export class ExportModal extends Modal { let headerTemplate = ''; if (this.renderSettings.displayHeader && this.exportSettings.headerFooterToc?.headerEnabled) { headerTemplate = ` -
+
${this.processVariables(this.exportSettings.headerFooterToc.headerLeft || '')}
${this.processVariables(this.exportSettings.headerFooterToc.headerCenter || '')}
${this.processVariables(this.exportSettings.headerFooterToc.headerRight || '')}
@@ -916,7 +931,7 @@ export class ExportModal extends Modal { let footerTemplate = ''; if (this.renderSettings.displayFooter && this.exportSettings.headerFooterToc?.footerEnabled) { footerTemplate = ` -
+
${this.processVariables(this.exportSettings.headerFooterToc.footerLeft || '')} ${this.processVariables(this.exportSettings.headerFooterToc.footerCenter || '')} ${this.processVariables(this.exportSettings.headerFooterToc.footerRight || '').replace('{{pageNumber}}', '').replace('{{totalPages}}', '')} @@ -936,11 +951,15 @@ export class ExportModal extends Modal { landscape: false, scale: this.renderSettings.scale / 100, margins: { - marginType: 'default' + top: 1, // 加大页眉区域空间 + bottom: 1, + left: 0.6, + right: 0 }, displayHeaderFooter: this.renderSettings.displayHeader || this.renderSettings.displayFooter, headerTemplate: headerTemplate, - footerTemplate: footerTemplate + footerTemplate: footerTemplate, + generateDocumentOutline: true }; // 使用 webview 生成 PDF diff --git a/src/modals/HeaderFooterTocModal.ts b/src/modals/HeaderFooterTocModal.ts index 868243e..3b0d9c6 100644 --- a/src/modals/HeaderFooterTocModal.ts +++ b/src/modals/HeaderFooterTocModal.ts @@ -52,7 +52,7 @@ export class HeaderFooterTocModal extends Modal { headerLeft: '{{title}}', headerCenter: '', headerRight: '{{author}}', - headerFontSize: 12, + headerFontSize: 15, headerColor: '#000000', headerHeight: 15, @@ -60,14 +60,14 @@ export class HeaderFooterTocModal extends Modal { footerLeft: '', footerCenter: '', footerRight: '{{pageNumber}}/{{totalPages}}', - footerFontSize: 12, + footerFontSize: 15, footerColor: '#000000', footerHeight: 20, tocEnabled: true, tocTitle: '目录', tocMaxLevel: 3, - tocFontSize: 14, + tocFontSize: 15, tocFontFamily: 'serif', // 新增默认值 tocColor: '#000000', // 新增默认值 tocLineHeight: 1.5, diff --git a/src/services/BookRenderService.ts b/src/services/BookRenderService.ts index 97bdd44..5a70592 100644 --- a/src/services/BookRenderService.ts +++ b/src/services/BookRenderService.ts @@ -672,7 +672,7 @@ export class BookRenderService { font-family: ${settings.tocFontFamily || 'serif'}; font-size: ${settings.tocFontSize}px; color: ${settings.tocColor || '#000000'}; - margin: 40px 0; + margin: 20px 0; line-height: ${settings.tocLineHeight}; ">

${settings.tocTitle}

diff --git a/src/services/ExportService.ts b/src/services/ExportService.ts deleted file mode 100644 index a8038c2..0000000 --- a/src/services/ExportService.ts +++ /dev/null @@ -1,707 +0,0 @@ -import { App, TFile } from 'obsidian'; -import { Book, ChapterNode } from '../types/book'; -import { TypographySettings } from '../components/TypographyView'; -import { HeaderFooterTocSettings } from '../modals/HeaderFooterTocModal'; -import * as fs from "fs/promises"; - -// 导出服务类 -export class ExportService { - private rootPath: string; - private strategies: Record; - - constructor(private app: App, settings: any) { - this.rootPath = settings.defaultBookPath; - this.strategies = { - 'txt': new TxtExportStrategy(this.rootPath), - 'pdf': new PdfExportStrategy(this.rootPath), - }; - } - - async exportBook(format: string, book: Book, options?: ExportOptions): Promise<{ content: string, fileName: string }> { - const strategy = this.strategies[format]; - if (!strategy) { - throw new Error(`不支持的导出格式: ${format}`); - } - - const content = await strategy.export(this.app, book, options); - return { - content: content, - fileName: `${book.basic.title}.${format}` - }; - } - - getSupportedFormats(): string[] { - return Object.keys(this.strategies); - } -} - -// 导出格式接口 -export interface ExportStrategy { - export(app: App, book: Book, options?: ExportOptions): Promise; -} - -// 导出选项接口 -export interface ExportOptions { - selectedChapters?: ChapterNode[]; - htmlContent?: HTMLElement; - useTypography?: boolean; - typographySettings?: TypographySettings; -} - -// TXT导出策略 - 实际实现 -export class TxtExportStrategy implements ExportStrategy { - private rootPath: string; - constructor(rootPath: string) { - this.rootPath = rootPath; - } - async export(app: App, book: Book, options?: ExportOptions): Promise { - try { - const content = await this.generateContent(app, book, options?.selectedChapters); - - // 获取保存路径 - const filePath = await this.getOutputFile(book.basic.title); - if (!filePath) return "cancelled"; - - // 写入文件 - await fs.writeFile(filePath, content); - - return filePath; - } catch (error) { - console.error('TXT导出错误:', error); - throw new Error(`TXT导出失败: ${error.message}`); - } - } - - private async generateContent(app: App, book: Book, selectedChapters?: ChapterNode[]): Promise { - let content = `${book.basic.title}`; - if (book.basic.subtitle) { - content += `${book.basic.subtitle}`; - } - content += `作者: ${book.basic.author.join(', ')}`; - - if (book.basic.desc) { - content += `${book.basic.desc}`; - } - - const chapters = selectedChapters || book.structure.tree; - content += await this.processChapters(app, book, chapters); - - return content; - } - - private async processChapters(app: App, book: Book, chapters: ChapterNode[], level: number = 0): Promise { - let content = ''; - for (const chapter of chapters) { - if (chapter.exclude) continue; - - // 添加章节标题 - content += `${chapter.title}`; - if (chapter.type === 'file') { - // 读取文件内容 - const filePath = `${this.rootPath}/${book?.basic.title}/${chapter.path}`; - const file = app.vault.getAbstractFileByPath(filePath); - if (file instanceof TFile) { - const fileContent = await app.vault.read(file); - // 处理Markdown语法,转换为纯文本 - const plainTextContent = this.convertMarkdownToPlainText(fileContent); - content += `${plainTextContent}`; - } - } - - // 处理子章节 - if (chapter.children && chapter.children.length > 0) { - content += await this.processChapters(app, book, chapter.children, level + 1); - } - } - return content; - } - - /** - * 将Markdown格式转换为纯文本 - * @param markdown Markdown格式的文本 - * @returns 转换后的纯文本 - */ - private convertMarkdownToPlainText(markdown: string): string { - let plainText = markdown; - - // 移除标题格式 (# 标题) - plainText = plainText.replace(/^#{1,6}\s+(.+)$/gm, '$1'); - - // 移除加粗和斜体 (**文本** 或 *文本*) - plainText = plainText.replace(/\*\*(.+?)\*\*/g, '$1'); - plainText = plainText.replace(/\*(.+?)\*/g, '$1'); - plainText = plainText.replace(/__(.+?)__/g, '$1'); - plainText = plainText.replace(/_(.+?)_/g, '$1'); - - // 移除双链接 [[链接]] 或 [[链接|显示文本]] - plainText = plainText.replace(/\[\[([^\|\]]+)\|([^\]]+)\]\]/g, '$2'); - plainText = plainText.replace(/\[\[([^\]]+)\]\]/g, '$1'); - - // 移除普通链接 [显示文本](链接) - plainText = plainText.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1'); - - // 移除代码块 - plainText = plainText.replace(/```[\s\S]*?```/g, ''); - - // 移除行内代码 - plainText = plainText.replace(/`([^`]+)`/g, '$1'); - - // 移除引用块 - plainText = plainText.replace(/^>\s+(.+)$/gm, '$1'); - - // 移除水平分割线 - plainText = plainText.replace(/^-{3,}|^\*{3,}|^_{3,}/gm, ''); - - // 移除任务列表标记 [x] 或 [ ] - plainText = plainText.replace(/^\s*- \[[x\s]\]\s+(.+)$/gm, '- $1'); - - // 处理HTML标签 - plainText = plainText.replace(/<[^>]+>/g, ''); - - // 处理转义字符 - plainText = plainText.replace(/([\\`*_{}$begin:math:display$$end:math:display$()#+\-.!])/g, '\\$1'); - - // 移除多余的空行(连续两个以上的换行符替换为两个) - plainText = plainText.replace(/\n{3,}/g, '\n\n'); - - return plainText; - } - - private async getOutputFile(filename: string): Promise { - //@ts-ignore - const result = await electron.remote.dialog.showSaveDialog({ - title: "导出 TXT 文件", - defaultPath: filename + ".txt", - filters: [{ name: "TXT", extensions: ["txt"] }], - properties: ["showOverwriteConfirmation", "createDirectory"] - }); - - return result.canceled ? null : result.filePath; - } -} - -// PDF导出策略 -export class PdfExportStrategy implements ExportStrategy { - private rootPath: string; - private bookSizeMap: Record = { - A3: { width: 841.89, height: 1190.55 }, // 297mm x 420mm - A4: { width: 595.28, height: 841.89 }, // 210mm x 297mm - A5: { width: 419.53, height: 595.28 }, // 148mm x 210mm - Legal: { width: 612, height: 1008 }, // 8.5in x 14in - Letter: { width: 612, height: 792 }, // 8.5in x 11in - Tabloid: { width: 792, height: 1224 } // 11in x 17in - }; - constructor(rootPath: string) { - this.rootPath = rootPath; - } - - async export(app: App, book: Book, options?: ExportOptions): Promise { - try { - // 如果提供了排版后的 HTML 内容,使用 HTML 转 PDF 策略 - if (options?.useTypography && options?.htmlContent && options?.typographySettings) { - return await this.exportHTML(book, options.htmlContent, options.typographySettings); - } - - // 否则返回一个提示信息 - return "PDF导出功能需要使用排版视图"; - } catch (error) { - console.error('PDF导出错误:', error); - throw new Error(`PDF导出失败: ${error.message}`); - } - } - - async exportHTML(book: Book, htmlContent: HTMLElement, typographySettings: TypographySettings): Promise { - try { - // 0. 处理htmlContent - await PdfExportStrategy.processImages(htmlContent); - - // 0.5 生成目录(如果启用)- 使用准确页码计算 - let tocHtml = ''; - if (typographySettings.headerFooterToc?.tocEnabled) { - tocHtml = await this.generateAccurateTOC(typographySettings, htmlContent, book); - } - - // 1. 构建样式 CSS 字符串 - const style = ` - body { - font-family: ${typographySettings.fontFamily || 'serif'}; - font-size: ${typographySettings.fontSize || '16px'}; - line-height: ${typographySettings.lineHeight || '1.75'}; - margin: ${typographySettings.margin || '2cm'}; - padding: 0; - box-sizing: border-box; - } - h1, h2, h3, h4, h5, h6 { - page-break-after: avoid; - } - .markdown-preview-view { - max-width: 720px; - margin: auto; - } - .table-of-contents { - page-break-after: always; - } - .toc-item { - page-break-inside: avoid; - } - @media print { - body { - -webkit-print-color-adjust: exact; - } - .page-break { - page-break-before: always; - } - } - `; - - // 2. 构建完整 HTML 页面(包含目录和分页) - const fullHtml = ` - - - - ${book.basic.title} - - - - ${tocHtml} - ${htmlContent.innerHTML} - - - `; - - // 3. 创建窗口加载 HTML 页面 - //@ts-ignore - const win = new electron.remote.BrowserWindow({ - show: false, // 设置为 true 可调试 - width: 1024, - height: 768, - webPreferences: { - sandbox: false, - contextIsolation: false, - nodeIntegration: true, - } - }); - - const ready = new Promise((resolve) => { - win.webContents.once("did-finish-load", resolve); - }); - await win.loadURL(`data:text/html;charset=UTF-8,${encodeURIComponent(fullHtml)}`); - await ready; - - // 稍等以确保样式生效 - await new Promise((r) => setTimeout(r, 300)); - - // 4. 打印设置(自动分页) - const printOptions = { - marginsType: 1, - pageSize: typographySettings.bookSize || "A4", - printBackground: true, - landscape: false, - scale: 1.0, - displayHeaderFooter: typographySettings.headerFooterToc?.headerEnabled || typographySettings.headerFooterToc?.footerEnabled, - headerTemplate: this.buildHeaderTemplate(typographySettings.headerFooterToc, book), - footerTemplate: this.buildFooterTemplate(typographySettings.headerFooterToc, book), - }; - - // 5. 生成 PDF Buffer - const bodyPdfBuffer = await win.webContents.printToPDF(printOptions); - win.close(); - - // 替换第295-325行的代码 - const PDFLib = await import('pdf-lib'); - - // 检查是否有封面图片数据 - if (typographySettings.showCover && typographySettings.coverImageData) { - // 创建封面页 - const coverDoc = await PDFLib.PDFDocument.create(); - const page = coverDoc.addPage(); - const size = this.bookSizeMap[typographySettings.bookSize || "A4"]; - page.setSize(size.width, size.height); - - try { - // 处理base64图片数据 - const base64Data = typographySettings.coverImageData.split(',')[1]; // 移除 "data:image/xxx;base64," 前缀 - const imageBytes = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0)); - - // 根据图片类型嵌入图片 - let coverImage; - if (typographySettings.coverImageData.includes('data:image/png')) { - coverImage = await coverDoc.embedPng(imageBytes); - } else if (typographySettings.coverImageData.includes('data:image/jpeg') || typographySettings.coverImageData.includes('data:image/jpg')) { - coverImage = await coverDoc.embedJpg(imageBytes); - } else { - // 默认尝试PNG格式 - coverImage = await coverDoc.embedPng(imageBytes); - } - - // 绘制封面图片 - page.drawImage(coverImage, { - x: 0, - y: 0, - width: page.getWidth(), - height: page.getHeight(), - }); - } catch (error) { - console.error('封面图片处理失败,使用默认封面:', error); - // 如果图片处理失败,绘制默认封面 - page.drawRectangle({ - x: 0, - y: 0, - width: page.getWidth(), - height: page.getHeight(), - color: PDFLib.rgb(0.2, 0.2, 0.2) - }); - page.drawText("Book", { - x: page.getWidth() / 2 - 50, - y: page.getHeight() / 2, - size: 30, - color: PDFLib.rgb(1, 1, 1), - }); - } - - const coverBuffer = await coverDoc.save(); - - // 合并封面和内容PDF - const finalPdf = await PDFLib.PDFDocument.create(); - const [coverPage] = await finalPdf.copyPages(await PDFLib.PDFDocument.load(coverBuffer), [0]); - finalPdf.addPage(coverPage); - - const bodyDoc = await PDFLib.PDFDocument.load(bodyPdfBuffer); - const bodyPages = await finalPdf.copyPages(bodyDoc, bodyDoc.getPageIndices()); - bodyPages.forEach(p => finalPdf.addPage(p)); - - const finalBuffer = await finalPdf.save(); - - const filePath = await this.getOutputFile(book.basic.title); - if (!filePath) return "cancelled"; - await fs.writeFile(filePath, finalBuffer); - - return filePath; - } else { - // 如果没有封面设置或不显示封面,创建简单的文本封面 - const coverDoc = await PDFLib.PDFDocument.create(); - const page = coverDoc.addPage(); - const size = this.bookSizeMap[typographySettings.bookSize || "A4"]; - page.setSize(size.width, size.height); - - // 绘制默认封面 - page.drawRectangle({ - x: 0, - y: 0, - width: page.getWidth(), - height: page.getHeight(), - color: PDFLib.rgb(0.1, 0.1, 0.1) - }); - - // 添加书名 - const titleText = "Book"; - page.drawText(titleText, { - x: page.getWidth() / 2 - (titleText.length * 10), - y: page.getHeight() / 2 + 50, - size: 24, - color: PDFLib.rgb(1, 1, 1), - }); - - // 添加作者信息 - if (book.basic.author && book.basic.author.length > 0) { - const authorText = book.basic.author.join(', '); - page.drawText(authorText, { - x: page.getWidth() / 2 - (authorText.length * 6), - y: page.getHeight() / 2 - 50, - size: 16, - color: PDFLib.rgb(0.8, 0.8, 0.8), - }); - } - - const coverBuffer = await coverDoc.save(); - - // 合并封面和内容PDF - const finalPdf = await PDFLib.PDFDocument.create(); - const [coverPage] = await finalPdf.copyPages(await PDFLib.PDFDocument.load(coverBuffer), [0]); - finalPdf.addPage(coverPage); - - const bodyDoc = await PDFLib.PDFDocument.load(bodyPdfBuffer); - const bodyPages = await finalPdf.copyPages(bodyDoc, bodyDoc.getPageIndices()); - bodyPages.forEach(p => finalPdf.addPage(p)); - - const finalBuffer = await finalPdf.save(); - - const filePath = await this.getOutputFile(book.basic.title); - if (!filePath) return "cancelled"; - await fs.writeFile(filePath, finalBuffer); - - return filePath; - } - } catch (err: any) { - console.error("PDF导出错误:", err); - throw new Error(`PDF导出失败: ${err.message}`); - } - } - - private static async processImages(container: HTMLElement): Promise { - const images = container.querySelectorAll('img'); - const imageArray = Array.from(images); - - for (const img of imageArray) { - try { - const response = await fetch(img.src); - const blob = await response.blob(); - const reader = new FileReader(); - await new Promise((resolve, reject) => { - reader.onload = () => { - img.src = reader.result as string; - resolve(null); - }; - reader.onerror = reject; - reader.readAsDataURL(blob); - }); - } catch (error) { - console.error('图片转换失败:', error); - } - } - } - - // 添加页眉模板构建方法 - private buildHeaderTemplate(settings?: HeaderFooterTocSettings, book?: Book): string { - if (!settings?.headerEnabled) return ''; - - return ` -
- ${this.replaceVariables(settings.headerLeft, book)} - ${this.replaceVariables(settings.headerCenter, book)} - ${this.replaceVariables(settings.headerRight, book)} -
- `; - } - - // 添加页脚模板构建方法 - private buildFooterTemplate(settings?: HeaderFooterTocSettings, book?: Book): string { - if (!settings?.footerEnabled) return ''; - - return ` -
- ${this.replaceVariables(settings.footerLeft)} - ${this.replaceVariables(settings.footerCenter)} - ${this.replaceVariables(settings.footerRight)} -
- `; - } - // 添加变量替换方法 - private replaceVariables(text: string, book?: Book, pageNumber?: number, totalPages?: number): string { - return text - .replace(/\{\{title\}\}/g, book?.basic.title || '书籍标题') - .replace(/\{\{author\}\}/g, book?.basic.author?.join(', ') || '作者') - .replace(/\{\{date\}\}/g, new Date().toLocaleDateString()) - .replace(/\{\{pageNumber\}\}/g, pageNumber?.toString() || '') - .replace(/\{\{totalPages\}\}/g, totalPages?.toString() || ''); - } - - // 新增:生成准确页码的目录 -private async generateAccurateTOC(typographySettings: TypographySettings, htmlContent: HTMLElement, book: Book): Promise { - const settings = typographySettings.headerFooterToc; - if (!settings?.tocEnabled) return ''; - - // 获取标题到页码的映射 - const headingPageMapping = await this.getHeadingPageMapping(typographySettings, htmlContent, book); - - if (headingPageMapping.length === 0) return ''; - - let tocHtml = ` -
-

${settings.tocTitle}

-
- `; - - headingPageMapping.forEach(heading => { - const indent = (heading.level - 1) * (settings.tocIndent || settings.tocIndentSize || 20); - tocHtml += ` -
- ${heading.text} - - ${heading.pageNumber} -
- `; - }); - - tocHtml += ` -
-
- `; - - return tocHtml; -} - -// 新增:获取标题页码映射 -private async getHeadingPageMapping(typographySettings: TypographySettings, htmlContent: HTMLElement, book: Book): Promise> { - const settings = typographySettings.headerFooterToc; - - // 1. 构建用于页码计算的完整HTML(不包含目录) - const style = ` - body { - font-family: ${typographySettings.fontFamily || 'serif'}; - font-size: ${typographySettings.fontSize || '16px'}; - line-height: ${typographySettings.lineHeight || '1.75'}; - margin: ${typographySettings.margin || '2cm'}; - padding: 0; - box-sizing: border-box; - } - h1, h2, h3, h4, h5, h6 { - page-break-after: avoid; - } - .markdown-preview-view { - max-width: 720px; - margin: auto; - } - @media print { - body { - -webkit-print-color-adjust: exact; - } - .page-break { - page-break-before: always; - } - } - `; - - const measureHtml = ` - - - - ${book.basic.title} - - - - ${htmlContent.innerHTML} - - - `; - - // 2. 创建临时窗口进行页码计算 - //@ts-ignore - const measureWin = new electron.remote.BrowserWindow({ - show: false, // 设置为 true 可调试 - width: 1024, - height: 768, - webPreferences: { - sandbox: false, - contextIsolation: false, - nodeIntegration: true, - } - }); - - try { - // 3. 加载HTML并等待完成 - const ready = new Promise((resolve) => { - measureWin.webContents.once("did-finish-load", resolve); - }); - await measureWin.loadURL(`data:text/html;charset=UTF-8,${encodeURIComponent(measureHtml)}`); - await ready; - await new Promise((r) => setTimeout(r, 300)); // 等待样式生效 - - // 4. 设置打印参数(与最终PDF相同) - const printOptions = { - marginsType: 1, - pageSize: typographySettings.bookSize || "A4", - printBackground: true, - landscape: false, - scale: 1.0, - displayHeaderFooter: settings?.headerEnabled || settings?.footerEnabled, - headerTemplate: this.buildHeaderTemplate(settings, book), - footerTemplate: this.buildFooterTemplate(settings, book), - }; - - // 5. 执行JavaScript获取标题页码信息 - const headingData = await measureWin.webContents.executeJavaScript(` - (async () => { - const headings = []; - const headingElements = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); - const maxLevel = ${settings?.tocMaxLevel || 3}; - - // 模拟打印环境的页面高度计算 - const printOptions = ${JSON.stringify(printOptions)}; - const pageHeight = window.innerHeight; - const margin = parseFloat('${typographySettings.margin || '2cm'}'.replace('cm', '')) * 37.8; // cm to px - const contentHeight = pageHeight - (margin * 2); - - headingElements.forEach((el, index) => { - const level = parseInt(el.tagName.substring(1)); - if (level <= maxLevel) { - const rect = el.getBoundingClientRect(); - const elementTop = rect.top + window.pageYOffset; - - // 计算页码(考虑页边距) - const pageNumber = Math.max(1, Math.ceil((elementTop - margin) / contentHeight) + 1); - - const id = el.id || \`heading-\${index}\`; - if (!el.id) el.id = id; - - headings.push({ - level, - text: el.textContent?.trim() || '', - id, - pageNumber - }); - } - }); - - return headings; - })() - `); - - return headingData; - } finally { - measureWin.close(); - } -} - - private async getOutputFile(filename: string): Promise { - //@ts-ignore - const result = await electron.remote.dialog.showSaveDialog({ - title: "导出 PDF 文件", - defaultPath: filename + ".pdf", - filters: [{ name: "PDF", extensions: ["pdf"] }], - properties: ["showOverwriteConfirmation", "createDirectory"] - }); - - return result.canceled ? null : result.filePath; - } -} diff --git a/src/styles/modals/export-modal.css b/src/styles/modals/export-modal.css index 1c509a4..5cba6d2 100644 --- a/src/styles/modals/export-modal.css +++ b/src/styles/modals/export-modal.css @@ -159,7 +159,6 @@ flex: 1; overflow: auto; height: 100%; - display: flex; flex-direction: column; scroll-behavior: smooth; } @@ -200,7 +199,8 @@ /* 预览内容区域 */ .preview-content { flex: 1; - overflow: visible; + height: inherit; + overflow: hidden; scroll-behavior: smooth; } @@ -477,7 +477,6 @@ display: flex; align-items: center; gap: 8px; - margin-bottom: 12px; } .export-toggle-input { @@ -493,7 +492,7 @@ } .export-setting-button { - width: 100%; + width: 50%; padding: 8px 16px; border: 1px solid var(--export-border); border-radius: 4px; @@ -509,46 +508,6 @@ border-color: var(--export-primary); } -/* 优化后的选择框样式 */ -.export-setting-select { - width: 100%; - padding: 12px 16px; - border: 2px solid var(--export-border); - border-radius: 6px; - background: var(--background-primary); - color: var(--text-normal); - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: all 0.3s ease; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); - appearance: none; - background-image: url('data:image/svg+xml;charset=US-ASCII,'); - background-repeat: no-repeat; - background-position: right 12px center; - background-size: 12px; - padding-right: 40px; -} - -.export-setting-select:hover { - border-color: var(--export-primary); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - transform: translateY(-1px); -} - -.export-setting-select:focus { - outline: none; - border-color: var(--export-primary); - box-shadow: 0 0 0 3px rgba(var(--export-primary), 0.1); - transform: translateY(-1px); -} - -.export-setting-select option { - padding: 8px 12px; - background: var(--background-primary); - color: var(--text-normal); -} - /* 信息卡片 */ .export-info-card { background: var(--background-secondary); @@ -706,29 +665,7 @@ height: auto; } -/* 滚动条样式 */ -.export-modal-content::-webkit-scrollbar, -.preview-scroll-container::-webkit-scrollbar { - width: 6px; -} -.export-modal-content::-webkit-scrollbar-track, -.preview-scroll-container::-webkit-scrollbar-track { - background: var(--background-secondary); - border-radius: 3px; -} - -.export-modal-content::-webkit-scrollbar-thumb, -.preview-scroll-container::-webkit-scrollbar-thumb { - background: var(--text-faint); - border-radius: 3px; - transition: background 0.2s ease; -} - -.export-modal-content::-webkit-scrollbar-thumb:hover, -.preview-scroll-container::-webkit-scrollbar-thumb:hover { - background: var(--text-muted); -} /* 响应式设计 */ @media (max-width: 1200px) { @@ -802,4 +739,141 @@ flex-direction: column; text-align: center; } +} +/* 封面容器背景图片样式 */ +.cover-container { + background-size: 100%; + background-position: 0px 0px; + background-repeat: no-repeat; +} + +/* 封面内容容器样式 */ +.cover-content { + position: relative; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 20px; + text-align: center; +} + +/* 封面标题样式 */ +.cover-title { + position: absolute; + left: 50%; + top: 30%; + transform: translate(-50%, -50%); + z-index: 10; +} + +/* 封面副标题默认样式 */ +.cover-subtitle { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + z-index: 10; + font-size: 18px; + color: #ffffff; + text-shadow: 0 1px 2px rgba(0,0,0,0.5); +} + +/* 封面作者样式 */ +.cover-author { + position: absolute; + left: 50%; + top: 70%; + transform: translate(-50%, -50%); + z-index: 10; +} + +/* PDF 设置部分样式 */ +.export-setting-icon { + font-size: 18px; +} + +.export-setting-title { + font-size: 15px; +} + +/* 尺寸选择容器 */ +.size-select-container { + display: flex; + align-items: center; +} + +/* 尺寸选择下拉框 */ +.export-setting-select { + width: 100%; + border-radius: 6px; + border: 1px solid var(--export-border); + background-color: var(--background-primary); + font-size: 14px; + cursor: pointer; +} + +/* PDF 设置卡片 */ +.export-setting-card { + padding: 16px 20px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +/* 封面控制容器 */ +.cover-controls-container { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +/* 封面切换开关 */ +.export-setting-toggle { + display: flex; + align-items: center; + gap: 8px; +} + +/* 封面复选框 */ +.export-toggle-input { + width: 16px; + height: 16px; + cursor: pointer; +} + +/* 封面标签 */ +.export-toggle-label { + font-size: 14px; + cursor: pointer; +} + +/* 设置按钮 */ +.export-setting-button { + width: auto; + padding: 6px 12px; + border-radius: 6px; + font-size: 13px; + background-color: var(--export-secondary); + border: 1px solid var(--export-border); + cursor: pointer; + transition: all 0.2s ease; +} + +/* 页眉页脚目录按钮容器 */ +.header-footer-toc-button-container { + display: flex; + justify-content: center; +} + +/* 页眉页脚目录按钮 */ +.header-footer-toc-button { + width: 100%; + padding: 8px 16px; + border-radius: 6px; + font-size: 13px; + background-color: var(--export-secondary); + border: 1px solid var(--export-border); + cursor: pointer; + transition: all 0.2s ease; } \ No newline at end of file From 455dbfed404d2aa29608f2e4ad14683c964a11a3 Mon Sep 17 00:00:00 2001 From: Yeban8090 Date: Tue, 8 Jul 2025 19:01:22 +0800 Subject: [PATCH 08/10] =?UTF-8?q?=E9=BB=98=E8=AE=A4=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E4=B9=A6=E7=B1=8D=E5=9B=BD=E9=99=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/i18n/interfaces/common.ts | 8 +++++++ src/i18n/locales/en.ts | 10 +++++++- src/i18n/locales/zh-CN.ts | 14 ++++++++--- src/services/BookManager.ts | 44 ++++++++++++++++++++++++++++++++++- 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/i18n/interfaces/common.ts b/src/i18n/interfaces/common.ts index a60b2cb..56744ec 100644 --- a/src/i18n/interfaces/common.ts +++ b/src/i18n/interfaces/common.ts @@ -11,4 +11,12 @@ export interface CommonTranslation { OPEN_BOOK_PANEL: string; OPEN_TOOL_PANEL: string; OPEN_ALL_PANELS: string; + + PREFACE: string; + OUTLINE: string; + VOLUME_1: string; + CHAPTER_1: string; + CHAPTER_2: string; + AFTERWORD: string; + TEMPLATE_OPTIONS_DESC: string; } \ No newline at end of file diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index fa6af25..af52a26 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -22,6 +22,14 @@ const commonTranslation: CommonTranslation = { OPEN_BOOK_PANEL: 'Open book panel', OPEN_TOOL_PANEL: 'Open tool panel', OPEN_ALL_PANELS: 'Open all panels', + + PREFACE: 'Preface', + OUTLINE: 'Outline', + VOLUME_1: 'Volume 1', + CHAPTER_1: 'Chapter 1', + CHAPTER_2: 'Chapter 2', + AFTERWORD: 'Afterword', + TEMPLATE_OPTIONS_DESC: 'Standard structure with preface, outline, chapters and afterword' }; // 书籍管理视图翻译 @@ -479,7 +487,7 @@ const translation: Translation = { ...modalTranslation, ...managerTranslation, ...toolbarModalTranslation, - ...componentTranslation + ...componentTranslation }; diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index c4db7f7..410d4e2 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -21,6 +21,14 @@ const commonTranslation: CommonTranslation = { OPEN_BOOK_PANEL: '打开书籍管理面板', OPEN_TOOL_PANEL: '打开工具面板', OPEN_ALL_PANELS: '打开所有面板', + + PREFACE: '前言', + OUTLINE: '大纲', + VOLUME_1: '第一卷', + CHAPTER_1: '第一章', + CHAPTER_2: '第二章', + AFTERWORD: '后记', + TEMPLATE_OPTIONS_DESC: '包含前言、大纲、正文卷章和后记的标准结构', }; // 书籍管理视图翻译 @@ -392,10 +400,10 @@ const toolbarModalTranslation: ToolbarModalTranslation = { SELECT_BOOK_FIRST: '请先选择一本书籍', EXPORT_SUCCESS: '导出成功', EXPORT_FAILED: '导出失败: ', - FORMAT : '格式', + FORMAT: '格式', CUSTOM_SIZE: '自定义大小', SELECT_EXPORT_FORMAT: '选择导出格式', - + // CoverModal DESIGN_COVER: '设计封面', SHOW_COVER: '显示封面', @@ -479,7 +487,7 @@ const translation: Translation = { ...modalTranslation, ...managerTranslation, ...toolbarModalTranslation, - ...componentTranslation + ...componentTranslation }; export default translation; diff --git a/src/services/BookManager.ts b/src/services/BookManager.ts index 04f8656..9113e42 100644 --- a/src/services/BookManager.ts +++ b/src/services/BookManager.ts @@ -221,8 +221,50 @@ export class BookManager { } private async getTemplateStructure(templateType: string): Promise { - return this.templateManager.getTemplate(templateType); + const template = this.templateManager.getTemplate(templateType); + + // 如果是默认模板,应用国际化 + if (templateType === 'default') { + // 深拷贝模板结构以避免修改原始模板 + const localizedTemplate = JSON.parse(JSON.stringify(template)); + + // 更新模板结构中的节点标题 + const updateNodeTitles = (nodes: ChapterNode[]) => { + for (const node of nodes) { + // 根据节点 id 或原始标题进行翻译 + if (node.id === 'preface') { + node.title = i18n.t('PREFACE'); + node.path = `${i18n.t('PREFACE')}.md`; + } else if (node.id === 'outline') { + node.title = i18n.t('OUTLINE'); + node.path = `${i18n.t('OUTLINE')}.md`; + } else if (node.id === 'volume1') { + node.title = i18n.t('VOLUME_1'); + node.path = i18n.t('VOLUME_1'); + } else if (node.id === 'chapter1') { + node.title = i18n.t('CHAPTER_1'); + node.path = `${i18n.t('VOLUME_1')}/${i18n.t('CHAPTER_1')}.md`; + } else if (node.id === 'chapter2') { + node.title = i18n.t('CHAPTER_2'); + node.path = `${i18n.t('VOLUME_1')}/${i18n.t('CHAPTER_2')}.md`; + } else if (node.id === 'afterword') { + node.title = i18n.t('AFTERWORD'); + node.path = `${i18n.t('AFTERWORD')}.md`; + } + + // 递归处理子节点 + if (node.children) { + updateNodeTitles(node.children); + } + } + }; + + updateNodeTitles(localizedTemplate.tree); + return localizedTemplate; } + + return template; +} private async createInitialStructure(folder: TFolder, structure: ChapterTree): Promise { const createNode = async (node: ChapterNode) => { From 422878d3d035d8a53195c4dd9a49375a18a9a335 Mon Sep 17 00:00:00 2001 From: Yeban8090 Date: Tue, 8 Jul 2025 19:04:26 +0800 Subject: [PATCH 09/10] =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/BookManager.ts | 140 ++++++++++++++++++------------------ 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/src/services/BookManager.ts b/src/services/BookManager.ts index 9113e42..6caec8c 100644 --- a/src/services/BookManager.ts +++ b/src/services/BookManager.ts @@ -15,7 +15,7 @@ export class BookManager { } async createBook( - basicInfo: Omit, + basicInfo: Omit, templateType: string = 'default', targetTotalWords: number = 0 ): Promise { @@ -144,16 +144,16 @@ export class BookManager { ...updates.basic }, structure: { - ...book.structure, - ...updates.structure + ...book.structure, + ...updates.structure }, stats: { - ...book.stats, - ...updates.stats + ...book.stats, + ...updates.stats }, export: { - ...book.export, - ...updates.export + ...book.export, + ...updates.export } }; @@ -206,7 +206,7 @@ export class BookManager { try { const configFile = this.app.vault.getAbstractFileByPath(configPath); const jsonContent = JSON.stringify(book, null, 2); - + if (configFile instanceof TFile) { // 如果文件已存在,使用 modify 方法 await this.app.vault.modify(configFile, jsonContent); @@ -221,50 +221,50 @@ export class BookManager { } private async getTemplateStructure(templateType: string): Promise { - const template = this.templateManager.getTemplate(templateType); - - // 如果是默认模板,应用国际化 - if (templateType === 'default') { - // 深拷贝模板结构以避免修改原始模板 - const localizedTemplate = JSON.parse(JSON.stringify(template)); - - // 更新模板结构中的节点标题 - const updateNodeTitles = (nodes: ChapterNode[]) => { - for (const node of nodes) { - // 根据节点 id 或原始标题进行翻译 - if (node.id === 'preface') { - node.title = i18n.t('PREFACE'); - node.path = `${i18n.t('PREFACE')}.md`; - } else if (node.id === 'outline') { - node.title = i18n.t('OUTLINE'); - node.path = `${i18n.t('OUTLINE')}.md`; - } else if (node.id === 'volume1') { - node.title = i18n.t('VOLUME_1'); - node.path = i18n.t('VOLUME_1'); - } else if (node.id === 'chapter1') { - node.title = i18n.t('CHAPTER_1'); - node.path = `${i18n.t('VOLUME_1')}/${i18n.t('CHAPTER_1')}.md`; - } else if (node.id === 'chapter2') { - node.title = i18n.t('CHAPTER_2'); - node.path = `${i18n.t('VOLUME_1')}/${i18n.t('CHAPTER_2')}.md`; - } else if (node.id === 'afterword') { - node.title = i18n.t('AFTERWORD'); - node.path = `${i18n.t('AFTERWORD')}.md`; + const template = this.templateManager.getTemplate(templateType); + + // 如果是默认模板,应用国际化 + if (templateType === 'default') { + // 深拷贝模板结构以避免修改原始模板 + const localizedTemplate = JSON.parse(JSON.stringify(template)); + + // 更新模板结构中的节点标题 + const updateNodeTitles = (nodes: ChapterNode[]) => { + for (const node of nodes) { + // 根据节点 id 或原始标题进行翻译 + if (node.id === 'preface') { + node.title = i18n.t('PREFACE'); + node.path = `${i18n.t('PREFACE')}.md`; + } else if (node.id === 'outline') { + node.title = i18n.t('OUTLINE'); + node.path = `${i18n.t('OUTLINE')}.md`; + } else if (node.id === 'volume1') { + node.title = i18n.t('VOLUME_1'); + node.path = i18n.t('VOLUME_1'); + } else if (node.id === 'chapter1') { + node.title = i18n.t('CHAPTER_1'); + node.path = `${i18n.t('VOLUME_1')}/${i18n.t('CHAPTER_1')}.md`; + } else if (node.id === 'chapter2') { + node.title = i18n.t('CHAPTER_2'); + node.path = `${i18n.t('VOLUME_1')}/${i18n.t('CHAPTER_2')}.md`; + } else if (node.id === 'afterword') { + node.title = i18n.t('AFTERWORD'); + node.path = `${i18n.t('AFTERWORD')}.md`; + } + + // 递归处理子节点 + if (node.children) { + updateNodeTitles(node.children); + } } - - // 递归处理子节点 - if (node.children) { - updateNodeTitles(node.children); - } - } - }; - - updateNodeTitles(localizedTemplate.tree); - return localizedTemplate; + }; + + updateNodeTitles(localizedTemplate.tree); + return localizedTemplate; + } + + return template; } - - return template; -} private async createInitialStructure(folder: TFolder, structure: ChapterTree): Promise { const createNode = async (node: ChapterNode) => { @@ -292,15 +292,15 @@ export class BookManager { } // 在 BookManager 类中添加这个方法 - + async importBookFromFolder(folderName: string): Promise { try { // 获取文件夹内的文件结构 const folderPath = `${this.settings.defaultBookPath}/${folderName}`; - + // 创建书籍结构 const structure = await this.buildFolderStructure(folderPath, ''); - + // 创建新书籍对象,确保符合 Book 接口定义 const newBook: Book = { basic: { @@ -329,13 +329,13 @@ export class BookManager { include_cover: true } }; - + // 保存书籍配置 const bookFolder = this.app.vault.getAbstractFileByPath(folderPath); if (!(bookFolder instanceof TFolder)) { throw new Error(i18n.t('BOOK_FOLDER_NOT_FOUND')); } - + await this.saveBookConfig(bookFolder, newBook); return newBook; } catch (error) { @@ -343,26 +343,26 @@ export class BookManager { throw new Error(i18n.t('IMPORT_BOOK_FAILED')); } } - + // 构建文件夹结构的辅助方法 private async buildFolderStructure(folderPath: string, parentPath: string = ''): Promise { const structure: ChapterNode[] = []; let order = 0; - + const folderContents = await this.app.vault.adapter.list(folderPath); - + // 处理文件 for (const filePath of folderContents.files) { // 只处理markdown文件 if (!filePath.endsWith('.md')) continue; - + const fileName = filePath.split('/').pop() || ''; // 过滤掉隐藏文件和配置文件 if (fileName.startsWith('.')) continue; - + const title = fileName.replace('.md', ''); const relativePath = parentPath ? `${parentPath}/${fileName}` : fileName; - + structure.push({ id: uuidv4(), title: title, @@ -374,24 +374,24 @@ export class BookManager { last_modified: new Date().toISOString() }); } - + // 处理文件夹 for (const subFolderPath of folderContents.folders) { const folderName = subFolderPath.split('/').pop() || ''; - + // 过滤掉隐藏文件夹和特殊系统文件夹 - if (folderName.startsWith('.') || - folderName === '__MACOSX' || + if (folderName.startsWith('.') || + folderName === '__MACOSX' || folderName === 'node_modules') continue; - + const relativePath = parentPath ? `${parentPath}/${folderName}` : folderName; - + // 递归获取子文件夹内容 const children = await this.buildFolderStructure( - subFolderPath, + subFolderPath, relativePath ); - + structure.push({ id: uuidv4(), title: folderName, @@ -405,7 +405,7 @@ export class BookManager { is_expanded: true }); } - + return structure; } } \ No newline at end of file From 7f102a9d46c52342dee68b9cfaf75a76eba95ca0 Mon Sep 17 00:00:00 2001 From: Yeban8090 Date: Tue, 8 Jul 2025 19:06:51 +0800 Subject: [PATCH 10/10] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modals/PreviewModal.ts | 40 -------------------------------------- src/views/ToolsView.ts | 8 +------- 2 files changed, 1 insertion(+), 47 deletions(-) delete mode 100644 src/modals/PreviewModal.ts diff --git a/src/modals/PreviewModal.ts b/src/modals/PreviewModal.ts deleted file mode 100644 index c435e06..0000000 --- a/src/modals/PreviewModal.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { App, Modal } from "obsidian"; -import { RenderedBook } from "../services/BookRenderService"; - -export class PreviewModal extends Modal { - constructor( - app: App, - private renderedBook: RenderedBook - ) { - super(app); - } - - onOpen() { - const { contentEl } = this; - contentEl.empty(); - contentEl.addClass('preview-modal'); - - // 创建预览容器 - const previewContainer = contentEl.createDiv({ cls: 'preview-container' }); - - // 添加渲染的文档内容 - const docClone = this.renderedBook.doc.cloneNode(true) as Document; - const bodyContent = docClone.body; - - if (bodyContent) { - previewContainer.appendChild(bodyContent); - } - - // 添加关闭按钮 - const closeBtn = contentEl.createEl('button', { - text: '关闭预览', - cls: 'preview-close-btn' - }); - closeBtn.addEventListener('click', () => this.close()); - } - - onClose() { - const { contentEl } = this; - contentEl.empty(); - } -} \ No newline at end of file diff --git a/src/views/ToolsView.ts b/src/views/ToolsView.ts index 1ff6411..1ec86b8 100644 --- a/src/views/ToolsView.ts +++ b/src/views/ToolsView.ts @@ -6,7 +6,6 @@ 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 { BookSelectionModal } from '../modals/BookSelectionModal'; interface ToolItem { icon: string; @@ -19,7 +18,6 @@ interface ToolItem { export class ToolView extends ItemView { private normalView: HTMLElement | null = null; private focusView: FocusToolView | null = null; - private typographyView: TypographyView | null = null; constructor(leaf: WorkspaceLeaf, private plugin: BookSmithPlugin) { super(leaf); @@ -240,10 +238,6 @@ export class ToolView extends ItemView { this.focusView.remove(); this.focusView = null; } - - if (this.typographyView) { - this.typographyView.remove(); - this.typographyView = null; - } + } } \ No newline at end of file