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/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/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/modals/BookSelectionModal.ts b/src/modals/BookSelectionModal.ts new file mode 100644 index 0000000..d0d76af --- /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: '选择要导出的书籍' }); + + // 加载书籍列表 + 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('导出设置') + .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.app); + // 创建并打开导出模态框 - 修正参数顺序 + 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/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 8fabe71..15e74bc 100644 --- a/src/modals/ExportModal.ts +++ b/src/modals/ExportModal.ts @@ -1,84 +1,1446 @@ -import { Notice } from "obsidian"; -import { BaseModal } from "./BaseModal"; -import { i18n } from "../i18n/i18n"; -import { ExportService } from "../services/ExportService"; -import { Book } from "../types/book"; +import { Notice, App, Modal } from "obsidian"; +import { BookRenderService, RenderConfig } from "../services/BookRenderService"; +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"; +import { PDFDocument } from 'pdf-lib'; -export class ExportModal extends BaseModal { +// 导出设置接口,定义了导出过程中需要的各种配置项 +export interface ExportSettings { + 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 contentEl: HTMLElement; + 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 coverPreviewElement: HTMLElement; + + // 导出设置,包含默认值 + private exportSettings: ExportSettings = { + format: '', + bookSize: 'A4', + 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, + 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 ) { - super(container, i18n.t('SELECT_EXPORT_FORMAT') || '选择导出格式'); + super(app); + // 初始化封面设置默认值 + const coverManager = new CoverManager(this.app); + this.exportSettings.cover = coverManager.getDefaultCoverSettings(this.selectedBook); } - protected createContent() { + // 模态框打开时初始化 UI + onOpen() { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass('export-modal'); + + // 调整模态框尺寸 + this.containerEl.style.setProperty('--dialog-width', '50vw'); + this.containerEl.style.setProperty('--dialog-height', '70vh'); + + this.createHeader(); + this.createMainContent(); + this.createFooter(); + } + + // 创建模态框的标题头部 + private createHeader() { + const { contentEl } = this; + const header = contentEl.createDiv({ cls: 'export-modal-header' }); + + header.createEl('h2', { + text: '导出书籍', + cls: 'export-modal-title' + }); + } + + // 创建主内容区域,包括左侧预览区域和右侧设置区域 + 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(); + } + } + + // 创建用于 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'; + webview.setAttribute('style', ` + height: 100%; + width: 100%; + border: 1px solid #f2f2f2; + background: white; + `); + webview.nodeintegration = true; + return webview; + } + + // 创建预览区域的 UI 结构 + private createPreviewArea() { + if (this.selectedFormat !== 'pdf') { + return; + } + + this.previewContainer.empty(); + 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 scrollContainer = this.previewContainer.createDiv({ cls: 'preview-scroll-container' }); + + // 添加封面预览区域(初始隐藏) + if (this.exportSettings.showCover && this.exportSettings.cover) { + const coverPreviewContainer = scrollContainer.createDiv({ cls: 'cover-preview-container' }); + coverPreviewContainer.style.display = 'none'; // 初始隐藏,等待渲染完成后显示 + + // 创建封面预览标题 + const coverPreviewHeader = coverPreviewContainer.createDiv({ cls: 'cover-preview-header' }); + + coverPreviewHeader.createEl('span', { text: '封面预览', cls: 'cover-preview-title' }); + + // 添加封面预览内容区域 + this.coverPreviewElement = coverPreviewContainer.createDiv({ cls: 'cover-preview-content' }); + + // 更新封面预览 + 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' }); + + // 应用开本大小样式 + 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`; + } + // 创建内容容器 - this.contentEl = this.element.createDiv({ cls: 'export-content' }); - - // 创建格式选择容器 - const formatsContainer = this.contentEl.createDiv({ cls: 'export-formats-container' }); - - // 获取支持的导出格式 - const formats = this.exportService.getSupportedFormats(); - - // 创建格式按钮 - formats.forEach(format => { - const formatBtn = formatsContainer.createEl('button', { - text: format.toUpperCase(), - cls: 'export-format-btn' - }); - - this.formatButtons.push(formatBtn); - - formatBtn.addEventListener('click', () => { - // 移除其他按钮的选中状态 - this.formatButtons.forEach(btn => btn.classList.remove('selected')); - // 添加当前按钮的选中状态 - formatBtn.classList.add('selected'); - // 设置选中的格式 - this.selectedFormat = format; - }); - }); - - // 添加分隔线 - this.contentEl.createEl('hr', { cls: 'export-divider' }); + const contentContainer = coverContainer.createDiv({ cls: 'cover-content' }); - // 创建按钮容器 - const buttonContainer = this.contentEl.createDiv({ cls: 'export-buttons-container' }); + // 添加书籍信息 + const settings = this.exportSettings.cover; + const book = this.selectedBook; - // 创建取消按钮 - const cancelBtn = buttonContainer.createEl('button', { - text: i18n.t('CANCEL') || '取消', - cls: 'export-cancel-btn' - }); + // 使用自定义文本和位置 + 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(', ') : ''); - cancelBtn.addEventListener('click', () => { - this.close(); - }); + // 添加书名 + if (titleText) { + const titleEl = contentContainer.createDiv({ cls: 'cover-title', text: titleText }); - // 创建确认按钮 - const confirmBtn = buttonContainer.createEl('button', { - text: i18n.t('CONFIRM') || '确定', - cls: 'export-confirm-btn' - }); - - confirmBtn.addEventListener('click', async () => { - if (this.selectedFormat) { - // 关闭对话框 - this.close(); - - // 执行导出回调 - await this.executeExportCallback(this.selectedFormat); + let titleStyle = ''; + if (settings.titleStyleConfig) { + titleStyle = this.buildStyleString(settings.titleStyleConfig); } else { - this.showNotice(i18n.t('SELECT_EXPORT_FORMAT') || '请选择导出格式'); + titleStyle = settings.titleStyle || ''; + } + // 只保留动态位置设置 + 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;`); + } + } + + // 添加副标题 + if (subtitleText) { + const subtitleEl = contentContainer.createDiv({ cls: 'cover-subtitle', text: subtitleText }); + + let subtitleStyle = ''; + if (settings.subtitleStyleConfig) { + subtitleStyle = this.buildStyleString(settings.subtitleStyleConfig); + } else { + 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;`); + } + } + + // 添加作者信息 + 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.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;`); + } + } + } + // 统一的预览状态管理方法,处理不同状态(等待、加载中、就绪、错误) + 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; + } + } + + // 清理 Webview 资源 + private cleanupWebview() { + if (this.webview) { + this.webview.remove(); + this.webview = null; + this.webviewReady = false; + } + } + + // 更新渲染进度条和进度文本 + 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; + + 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; + } + } + + // 开始渲染预览,这是渲染预览的核心方法 + 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 = ` +
+
正在渲染预览...
+
+
+
+
准备中...
+
+ `; + + 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 && 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, + this.selectedBook, + 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'); + + // 渲染成功后,如果启用了封面,更新并显示封面预览 + 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) { + 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(); + 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 { + if (!this.abortController?.signal.aborted) { + this.isRendering = false; + this.updateFormatButtonsState(); + + setTimeout(() => { + this.updateExportButtonState(); + }, 100); + } + } + } + + // 创建设置区域的内容,包括书籍信息、格式选择和设置区域 + 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()); + + this.exportBtn = buttonGroup.createEl('button', { + text: '导出', + cls: 'export-btn export-btn-primary' + }) as HTMLButtonElement; + this.exportBtn.addEventListener('click', () => this.handleExport()); + + this.updateExportButtonState(); + } + + // 根据渲染状态更新导出按钮的可用性和文本 + 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 ? '渲染中...' : '导出'; + } + } + + // 根据渲染状态更新格式按钮的可用性和样式 + 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' + }); + } + } + + // 创建导出格式选择区域(PDF、TXT、DOCX) + 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(); + + // PDF 格式自动开始渲染 + if (format.key === 'pdf') { + // 延迟一点时间,确保 UI 更新完成 + setTimeout(() => { + this.startRenderPreview(); + }, 50); + } else { + 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.style.marginBottom = '16px'; + sizeHeader.innerHTML = ` + 📏 + 开本大小 + `; + + 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 }); + if (size === (this.exportSettings.bookSize || 'A4')) option.selected = true; + }); + + sizeSelect.addEventListener('change', () => { + this.exportSettings.bookSize = sizeSelect.value; + + // 更新封面预览 + if (this.exportSettings.showCover && this.webviewReady) { + this.updateCoverPreview(); + } + }); + } + } + + // 创建 PDF 特定设置(封面设置、页眉页脚目录设置) + private createPdfSettings(container: HTMLElement) { + // PDF 特定设置 - 封面卡片 + const coverCard = container.createDiv({ cls: 'export-setting-card' }); + + const coverHeader = coverCard.createDiv({ cls: 'export-setting-header' }); + coverHeader.innerHTML = ` + 🎨 + 封面设置 + `; + + // 创建一个包含复选框和按钮的容器,使用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; + + // 更新封面预览区域的可见性,只有在已渲染完成时才显示 + const coverPreviewContainer = this.previewContainer.querySelector('.cover-preview-container'); + if (coverPreviewContainer) { + (coverPreviewContainer as HTMLElement).style.display = (this.exportSettings.showCover && this.webviewReady) ? 'block' : 'none'; + } + }); + + coverSettingButton.addEventListener('click', () => { + // 打开封面设置模态框 + const coverModal = new CoverSettingModal( + this.app, + (settings) => { + // 保存封面设置 + this.exportSettings.cover = settings; + + // 更新封面预览 + this.updateCoverPreview(); + }, + 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(); + + 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 + 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; + } + } + + // 导出为 PDF + private async exportToPdf() { + if (!this.webview || !this.webviewReady) { + new Notice('PDF 预览未准备就绪,请稍候'); + return; + } + + try { + const filename = this.selectedBook.basic.title || 'exported-book'; + const outputFile = await this.getOutputFile(filename); + + if (!outputFile) { + return; // 用户取消了保存 + } + + // 构建页眉模板 + 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}}', '')} +
+ `; + } + + // 更新目录页码 + if (this.exportSettings.headerFooterToc?.tocEnabled) { + await this.updateTocPageNumbers(); + } + + // PDF 导出选项 + const printOptions: electron.PrintToPDFOptions = { + pageSize: this.exportSettings.bookSize as any || 'A4', + printBackground: false, + landscape: false, + scale: this.renderSettings.scale / 100, + margins: { + top: 1, // 加大页眉区域空间 + bottom: 1, + left: 0.6, + right: 0 + }, + displayHeaderFooter: this.renderSettings.displayHeader || this.renderSettings.displayFooter, + headerTemplate: headerTemplate, + footerTemplate: footerTemplate, + generateDocumentOutline: true + }; + + // 使用 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, finalPdfBuffer); + + 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 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 ''; + + 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('请先选择导出格式'); + 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/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/BookManager.ts b/src/services/BookManager.ts index 04f8656..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,7 +221,49 @@ 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 { @@ -250,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: { @@ -287,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) { @@ -301,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, @@ -332,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, @@ -363,7 +405,7 @@ export class BookManager { is_expanded: true }); } - + return structure; } } \ No newline at end of file diff --git a/src/services/BookRenderService.ts b/src/services/BookRenderService.ts new file mode 100644 index 0000000..5a70592 --- /dev/null +++ b/src/services/BookRenderService.ts @@ -0,0 +1,747 @@ +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 { + showTitle: boolean; + scale: number; + displayHeader: boolean; + displayFooter: boolean; + cssSnippet?: string; + abortSignal?: AbortSignal; + onProgress?: (current: number, total: number, fileName: string) => void; + headerFooterToc?: HeaderFooterTocSettings; // 添加目录设置 + showCover?: boolean; + coverSettings?: CoverSettings; +} + +export interface DocType { + doc: Document; + frontMatter: any; + file: TFile; + title: 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 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')); + }, 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. 如果启用了目录,生成目录 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); + } + + // 4. 处理自定义CSS片段 + if (config?.cssSnippet && config.cssSnippet !== '0') { + try { + await webview.insertCSS(config.cssSnippet); + } catch (error) { + console.warn('Failed to load CSS snippet:', error); + } + } + + // 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; + contentEl.insertBefore(tocContainer, contentEl.firstChild); + } + } + + await this.appendWebview(webview, this.docs[0]); + + + // 6. 注入补丁样式 + const patchStyles = this.getPatchStyles(); + for (const css of patchStyles) { + await webview.insertCSS(css); + } + + resolve(); + } catch (error) { + clearTimeout(timeout); + reject(error); + } + }); + }); + } + + /** + * 收集所有文件 - 对应 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); + + 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(); + + 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 { + // 使用更安全的方式传输HTML内容 + const bodyContent = doc.body.innerHTML; + const headContent = doc.head.innerHTML; + const docTitle = doc.title; + + return ` + 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); + }); + } + + /** + * 根据章节节点获取文件对象 + */ + 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; + } + + /** + * 递归收集所有文件类型的章节节点 + */ + 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)); + } + } + + return fileNodes.sort((a, b) => a.order - b.order); + } + + /** + * 获取前置元数据 + */ + 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'); + } + + /** + * 创建渲染片段 + */ + private createRenderFragment(): any { + return { + children: undefined, + appendChild(e: DocumentFragment) { + this.children = e?.children; + throw new Error('exit'); + }, + } as unknown as HTMLElement; + } + + /** + * 修复内部链接 + */ + 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 || title === file.basename) && anchor?.startsWith('^')) { + return; + } + el.removeAttribute('href'); + }); + } + + /** + * 等待动态内容渲染 + */ + 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) { + await this.sleep(1000); + } + } + + /** + * 修复Canvas为图片 + */ + 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 getAllStyles(): string[] { + const cssTexts: string[] = []; + + 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; + 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); + } + }); + + 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; + } + } + + img.__canvas__ { + width: 100% !important; + height: 100% !important; + } + + .book-chapter { + margin-bottom: 2em; + } + + .page-break { + page-break-after: always; + break-after: page; + } + `; + + return [patchCSS]; + } + + /** + * 工具方法 + */ + 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); + }); + } + + /** + * 生成目录 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/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/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..5cba6d2 100644 --- a/src/styles/modals/export-modal.css +++ b/src/styles/modals/export-modal.css @@ -1,135 +1,879 @@ -/* 导出模态框样式 */ -.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-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; + 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; + margin: 24px 0; } -/* 导出标题 */ -.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-header-content { + display: flex; + justify-content: space-between; + align-items: center; + 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); + 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-scroll-container { + flex: 1; + overflow: auto; + height: 100%; + 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); +} + +.cover-preview-title { + margin: 0; + font-size: 14px; + font-weight: 600; + color: var(--text-normal); +} + +/* 封面预览内容 */ +.cover-preview-content { + padding: 15px; + display: flex; + justify-content: center; +} + +/* 预览内容区域 */ +.preview-content { + flex: 1; + height: inherit; + overflow: hidden; + 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-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: 24px 0; + 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-toggle { + display: flex; + align-items: center; + gap: 8px; +} + +.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: 50%; + 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:disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + +.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; +} + +/* 隐藏旧的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; +.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; } -.export-confirm-btn:hover { - background-color: var(--interactive-accent-hover); - opacity: 0.9; - transform: translateY(-2px); +.preview-document p { + margin-bottom: 1em; } -/* 导出模态框标题 */ -.book-smith-modal-header { +.preview-document img { + max-width: 100%; + height: auto; +} + + + +/* 响应式设计 */ +@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; + } + + .preview-scroll-container { + max-height: 400px; + } +} + +@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; + } +} +/* 封面容器背景图片样式 */ +.cover-container { + background-size: 100%; + background-position: 0px 0px; + background-repeat: no-repeat; +} + +/* 封面内容容器样式 */ +.cover-content { position: relative; - padding-bottom: 10px; - border-bottom: 1px solid var(--background-modifier-border); - margin-bottom: 15px; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 20px; + text-align: center; } -/* 选择导出格式标题 */ -.export-title { - font-size: 16px; - text-align: center; - margin: 0 0 15px 0; - color: var(--text-normal); +/* 封面标题样式 */ +.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 diff --git a/src/views/ToolsView.ts b/src/views/ToolsView.ts index ea283dc..1ec86b8 100644 --- a/src/views/ToolsView.ts +++ b/src/views/ToolsView.ts @@ -6,8 +6,7 @@ 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; text: 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); @@ -231,25 +229,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 方法,确保所有视图都被正确关闭 @@ -258,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