mirror of
https://github.com/yeban8090/book-smith.git
synced 2026-07-22 05:46:23 +00:00
继续优化
This commit is contained in:
parent
9d4cbf0f68
commit
65d82b838d
2 changed files with 191 additions and 195 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 导出策略
|
||||
|
|
|
|||
Loading…
Reference in a new issue