继续优化

This commit is contained in:
Yeban8090 2025-06-12 12:38:53 +08:00
parent 9d4cbf0f68
commit 65d82b838d
2 changed files with 191 additions and 195 deletions

View file

@ -1,7 +1,7 @@
import { App, setIcon, Notice, TFile, MarkdownRenderer } from 'obsidian';
import { BookManager } from "../services/BookManager";
import { Book, ChapterNode } from "../types/book";
import { ExportService} from "../services/ExportService";
import { ExportService } from "../services/ExportService";
import { i18n } from "../i18n/i18n";
import BookSmithPlugin from '../main';
import { ImgTemplateManager, ImgTemplate } from '../services/ImgTemplateManager';
@ -157,7 +157,7 @@ export class TypographyView {
// 5. 创建字号大小控制
this.createFontSizeControls(selectorsRow);
// 6. 创建开本大小选择器
this.createBookSizeSelector(selectorsRow);
}
@ -449,7 +449,7 @@ export class TypographyView {
cls: 'book-smith-font-size-button book-smith-decrease-button',
text: '-'
});
this.fontSizeInput = fontSizeGroup.createEl('input', {
cls: 'book-smith-font-size-input',
type: 'text',
@ -460,44 +460,44 @@ export class TypographyView {
max: '30'
}
}) as HTMLInputElement;
// 添加增大按钮
const increaseButton = 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.themeManager.setFontSize(currentSize);
// 更新预览
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;
@ -507,7 +507,7 @@ export class TypographyView {
updateFontSize();
}
});
// 添加增大字号事件
increaseButton?.addEventListener('click', () => {
if (!this.fontSizeInput) return;
@ -517,7 +517,7 @@ export class TypographyView {
updateFontSize();
}
});
// 添加输入框变化事件
this.fontSizeInput.addEventListener('change', updateFontSize);
this.fontSizeInput.addEventListener('input', updateFontSize);
@ -624,43 +624,43 @@ export class TypographyView {
// 修改 renderPaginatedContent 方法,确保一致的应用顺序
private async renderPaginatedContent(settings: TypographySettings) {
if (!this.previewElement || !this.tempRenderContainer) return;
// 清空临时容器
this.tempRenderContainer.empty();
// 1. 先渲染所有内容到临时容器
await this.renderAllContent(this.tempRenderContainer);
// 2. 提取内容块,转换为支持文字级分页的 IBlock 对象
const blocks = extractBlocks(this.tempRenderContainer);
// 3. 创建分页内容容器
const contentContainer = this.previewElement.createDiv({ cls: 'typography-content-pages' });
// 4. 创建分页引擎并应用分页
const engine = new PaginatedEngine(contentContainer);
engine.setOptions({
bookSize: settings.bookSize
});
// 执行分页
engine.paginate(blocks);
// 添加页码标记,修改格式为三位数
engine.addPageMarkers(" {page} ");
// 5. 生成分页目录
const tocPages = generatePaginatedTOC(contentContainer, settings.bookSize);
// 6. 将目录页添加到内容前面
const tocContainer = this.previewElement.createDiv({ cls: 'typography-toc-pages' });
tocPages.forEach(tocPage => {
tocContainer.appendChild(tocPage);
});
// 7.将目录容器移到内容容器前面
this.previewElement.insertBefore(tocContainer, contentContainer);
// 8. 在分页完成后应用主题和模板
if (this.currentTemplate) {
this.currentTemplate.render(this.previewElement);
@ -759,13 +759,13 @@ export class TypographyView {
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') || '导出',
@ -780,102 +780,77 @@ export class TypographyView {
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('未选择书籍');
throw new Error("未选择书籍");
}
let htmlContent: string | undefined;
let useTypography = false;
// 对于非 txt 格式,获取排版后的 HTML 内容
if (format !== 'txt') {
if (format !== "txt") {
useTypography = true;
// 获取当前排版设置
const settings = this.getTypographySettings();
// 创建临时容器,用于获取完整的 HTML 内容
const tempContainer = document.createElement('div');
tempContainer.className = 'typography-export-container';
// 复制预览元素的内容到临时容器
const tempContainer = document.createElement("div");
tempContainer.className = "typography-export-container";
if (this.previewElement) {
tempContainer.innerHTML = this.previewElement.innerHTML;
// 应用样式
tempContainer.style.fontSize = `${settings.fontSize}px`;
tempContainer.classList.add(`font-${settings.fontFamily}`);
// 获取 HTML 内容(只获取内部 HTML不包括外层容器
htmlContent = tempContainer.innerHTML;
}
}
// 使用统一的 exportBook 方法进行导出
const content = await this.exportService.exportBook(format, this.selectedBook, {
// 调用统一导出接口
const result = await this.exportService.exportBook(format, this.selectedBook, {
useTypography,
htmlContent
htmlContent,
});
// 处理下载或打印
if (format === 'pdf' && content.content === 'print-success') {
// PDF 已通过 Printd 直接打印,不需要下载
new Notice(`PDF 打印成功!`);
} else {
// 其他格式需要下载
const blob = format === 'txt'
? new Blob([content.content], { type: 'text/plain' })
: this.dataURLToBlob(content.content);
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = content.fileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
// 特殊处理 PDF 格式的返回值
if (format === "pdf") {
if (result.content === "cancelled") {
new Notice("导出已取消");
return;
}
new Notice(`导出成功!`);
// 如果返回的是文件路径(不是 data URL
if (!result.content.startsWith("data:")) {
new Notice(`PDF 已成功导出到: ${result.content}`);
return;
}
}
// 处理其他格式或返回 data URL 的情况
if (result?.content === "print-success") {
new Notice("PDF 已成功打印");
} else {
new Notice("导出完成!");
}
} catch (error) {
console.error('导出错误:', error);
console.error("导出错误:", error);
new Notice(`导出失败: ${error instanceof Error ? error.message : String(error)}`);
}
}
// 将 Data URL 转换为 Blob
private dataURLToBlob(dataURL: string): Blob {
const parts = dataURL.split(';base64,');
const contentType = parts[0].split(':')[1];
const raw = window.atob(parts[1]);
const rawLength = raw.length;
const uInt8Array = new Uint8Array(rawLength);
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], { type: contentType });
}
// 添加封面设计开关
private createCoverToggle(parent: HTMLElement) {

View file

@ -1,6 +1,6 @@
import { App, TFile, TFolder } from 'obsidian';
import { Book, ChapterNode } from '../types/book';
import * as fs from "fs/promises";
// 导出格式接口
export interface ExportStrategy {
@ -284,126 +284,147 @@ export class HTMLToDocxExport {
}
}
// HTMLToPdfExport.ts
import * as fs from "fs/promises";
// @ts-ignore
import { BrowserWindow, dialog } from "@electron/remote";
// HTML 转 PDF 导出策略 - 使用 better-export-pdf 的方法
export class HTMLToPdfExport {
private rootPath: string;
private rootPath: string;
constructor(rootPath: string) {
this.rootPath = rootPath;
}
async exportHTML(app: App, book: Book, htmlContent: string): Promise<string> {
try {
const styles = this.getAllStyles();
const fullHtml = this.buildFullHTML(book, htmlContent, styles);
console.log(fullHtml);
const win = new BrowserWindow({ show: false });
await win.loadURL(`data:text/html;charset=UTF-8,${encodeURIComponent(fullHtml)}`);
const printOptions = {
marginsType: 1,
pageSize: "A4",
printBackground: true,
landscape: false,
scale: 1.0,
displayHeaderFooter: true,
headerTemplate: `<div style="font-size:10px;text-align:center;width:100vw;"><span class="title"></span></div>`,
footerTemplate: `<div style="font-size:10px;text-align:center;width:100vw;"><span class="pageNumber"></span> / <span class="totalPages"></span></div>`,
};
const pdfData = await win.webContents.printToPDF(printOptions);
// 构造一个 DOM 对象用于目录提取
const tempDoc = new DOMParser().parseFromString(fullHtml, "text/html");
const headings = this.getHeadingTree(tempDoc);
const finalPDF = await this.editPDF(pdfData, { headings });
const filePath = await this.getOutputFile(book.basic.title);
if (!filePath) return "cancelled";
await fs.writeFile(filePath, finalPDF);
return filePath;
} catch (err: any) {
console.error("PDF导出错误:", err);
throw new Error(`PDF导出失败: ${err.message}`);
constructor(rootPath: string) {
this.rootPath = rootPath;
}
}
// 拼接完整 HTML含样式
private buildFullHTML(book: Book, htmlContent: string, styles: string[]): string {
return `
<html>
<head>
<meta charset="utf-8">
<title>${book.basic.title}</title>
<style>${styles.join("\n")}</style>
</head>
<body>
<h1 id="book-title" class="__title__">${book.basic.title}</h1>
${htmlContent}
</body>
</html>
`;
}
async exportHTML(app: App, book: Book, htmlContent: string): Promise<string> {
try {
// 获取所有样式表
const styles = this.getAllStyles();
// 获取所有样式(页面内 + 插件插入)
private getAllStyles(): string[] {
const cssTexts: string[] = [];
// 构建完整 HTML 页面(包含分页)
const fullHtml = `
<html>
<head>
<meta charset="utf-8">
<title>${book.basic.title}</title>
<style>${styles.join("\n")}</style>
</head>
<body>
<div class="markdown-preview-view markdown-rendered">
<h1 id="book-title" class="__title__">${book.basic.title}</h1>
${htmlContent}
</div>
</body>
</html>
`;
Array.from(document.styleSheets).forEach((sheet: any) => {
try {
if (sheet.cssRules) {
for (const rule of sheet.cssRules) {
cssTexts.push(rule.cssText);
}
//@ts-ignore
// 修改BrowserWindow配置
const win = new electron.remote.BrowserWindow({
show: true, // 可以设为true进行调试
width: 1024, // 增加窗口大小
height: 768,
webPreferences: {
sandbox: false,
nodeIntegration: true, // 启用Node集成
contextIsolation: false // 禁用上下文隔离
}
});
await win.loadURL(`data:text/html;charset=UTF-8,${encodeURIComponent(fullHtml)}`);
// 打印设置(自动分页)
const printOptions = {
marginsType: 1,
pageSize: "A4",
printBackground: true,
landscape: false,
scale: 1.0,
displayHeaderFooter: true,
headerTemplate: `<div style="font-size:10px;text-align:center;width:100vw;"><span class="title"></span></div>`,
footerTemplate: `<div style="font-size:10px;text-align:center;width:100vw;"><span class="pageNumber"></span> / <span class="totalPages"></span></div>`
};
// 生成 PDF Buffer
const pdfBuffer = await win.webContents.printToPDF(printOptions);
console.log("PDF Buffer 大小:", pdfBuffer.length);
// 用 DOMParser 重建 DOM 供目录抽取(用于 editPDF
const tempDoc = new DOMParser().parseFromString(fullHtml, "text/html");
const headings = this.getHeadingTree(tempDoc);
const finalPdf = await this.editPDF(pdfBuffer, {
headings,
displayMetadata: true,
maxLevel: 3
});
// 获取保存路径
const filePath = await this.getOutputFile(book.basic.title);
if (!filePath) return "cancelled";
// 写入 PDF 文件
await fs.writeFile(filePath, finalPdf);
// 关闭窗口
win.close();
return filePath;
} catch (err: any) {
console.error("PDF导出错误:", err);
throw new Error(`PDF导出失败: ${err.message}`);
}
} catch (e) {
console.warn("样式读取失败:", e);
}
});
}
return cssTexts;
}
private getAllStyles(): string[] {
const cssTexts: string[] = [];
// 提取标题结构(用于 TOC 或编辑 PDF
private getHeadingTree(doc: Document) {
const headings: any[] = [];
const headingElements = doc.querySelectorAll("h1, h2, h3, h4, h5, h6");
Array.from(document.styleSheets).forEach((sheet: any) => {
try {
if (sheet.cssRules) {
for (const rule of sheet.cssRules) {
cssTexts.push(rule.cssText);
}
}
} catch (e) {
console.warn("样式读取失败:", e);
}
});
headingElements.forEach((el) => {
const level = parseInt(el.tagName.substring(1));
headings.push({
level,
text: el.textContent,
id: el.id || crypto.randomUUID(),
});
return cssTexts;
}
if (!el.id) el.id = headings[headings.length - 1].id;
});
private getHeadingTree(doc: Document) {
const headings: any[] = [];
const headingElements = doc.querySelectorAll("h1, h2, h3, h4, h5, h6");
return headings;
}
headingElements.forEach((el) => {
const level = parseInt(el.tagName.substring(1));
const id = el.id || crypto.randomUUID();
if (!el.id) el.id = id;
// 可选:添加目录或元信息(你可以引入 pdf-lib、HummusJS 等)
private async editPDF(data: Buffer, options: any): Promise<Buffer> {
// 简版:原样返回(你可替换为插入目录页逻辑)
return data;
}
headings.push({
level,
text: el.textContent,
id
});
});
// 打开保存对话框
private async getOutputFile(filename: string): Promise<string | null> {
const result = await dialog.showSaveDialog({
title: "导出 PDF 文件",
defaultPath: filename + ".pdf",
filters: [{ name: "PDF", extensions: ["pdf"] }],
properties: ["showOverwriteConfirmation", "createDirectory"],
});
return headings;
}
return result.canceled ? null : result.filePath;
}
private async editPDF(data: Buffer, options: any): Promise<Buffer> {
// 你可以在这里用 pdf-lib 添加封面页/目录页
return data;
}
private async getOutputFile(filename: string): Promise<string | null> {
//@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;
}
}
// HTML 转 EPUB 导出策略