Merge pull request #14 from Yeban8090/1.0.5

1.0.5
This commit is contained in:
Yeban8090 2025-07-08 19:09:12 +08:00 committed by GitHub
commit c5c80a2e17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 3380 additions and 2005 deletions

View file

@ -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",

File diff suppressed because it is too large Load diff

View file

@ -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;
}

View file

@ -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
};

View file

@ -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;

View file

@ -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();
}
}

View file

@ -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();
});
}

File diff suppressed because it is too large Load diff

View file

@ -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,

View file

@ -15,7 +15,7 @@ export class BookManager {
}
async createBook(
basicInfo: Omit<BookBasicInfo, 'uuid' | 'created_at'>,
basicInfo: Omit<BookBasicInfo, 'uuid' | 'created_at'>,
templateType: string = 'default',
targetTotalWords: number = 0
): Promise<Book> {
@ -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<ChapterTree> {
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<void> {
@ -250,15 +292,15 @@ export class BookManager {
}
// 在 BookManager 类中添加这个方法
async importBookFromFolder(folderName: string): Promise<Book> {
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<ChapterNode[]> {
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;
}
}

View file

@ -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<void> {
return new Promise<void>((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<void> {
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<DocType> {
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<unknown>> = [];
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<void> {
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, `<span id="${blockid}" class="blockid"></span> ${blockid}`);
}
}
return line;
});
[...blocks.values()].forEach(({ id, position: { start } }) => {
const idx = start.line;
lines[idx] = `<span id="^${id}" class="blockid"></span>\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<void> {
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<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
private waitForDomChange(target: HTMLElement, timeout = 2000, interval = 200): Promise<boolean> {
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 = `
<div class="table-of-contents" style="
page-break-after: ${settings.tocPageBreak ? 'always' : 'auto'};
font-family: ${settings.tocFontFamily || 'serif'};
font-size: ${settings.tocFontSize}px;
color: ${settings.tocColor || '#000000'};
margin: 20px 0;
line-height: ${settings.tocLineHeight};
">
<h1 style="text-align: center; margin-bottom: 30px;">${settings.tocTitle}</h1>
<div class="toc-content">
`;
headings.forEach(heading => {
if (heading.level <= settings.tocMaxLevel) {
const indent = (heading.level - 1) * (settings.tocIndent || settings.tocIndentSize || 20);
tocHtml += `
<div class="toc-item" style="
margin-left: ${indent}px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: baseline;
">
<span class="toc-text">${heading.text}</span>
<span class="toc-dots" style="
flex: 1;
border-bottom: 1px dotted #ccc;
margin: 0 10px;
height: 1px;
align-self: center;
"></span>
<span class="toc-page" data-heading-id="${heading.id}">?</span>
</div>
`;
}
});
tocHtml += `
</div>
</div>
`;
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;
}
}

View file

@ -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<string, ExportStrategy>;
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<string>;
}
// 导出选项接口
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<string> {
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<string> {
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<string> {
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<string | null> {
//@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<string, { width: number; height: number }> = {
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<string> {
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<string> {
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 = `
<html>
<head>
<meta charset="utf-8">
<title>${book.basic.title}</title>
<style>${style}</style>
</head>
<body>
${tocHtml}
${htmlContent.innerHTML}
</body>
</html>
`;
// 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<void>((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<void> {
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 `
<div style="
width: 100%;
height: ${settings.headerHeight}px;
font-size: ${settings.headerFontSize}px;
color: ${settings.headerColor};
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
box-sizing: border-box;
border-bottom: 1px solid #ddd;
">
<span style="flex: 1; text-align: left;">${this.replaceVariables(settings.headerLeft, book)}</span>
<span style="flex: 1; text-align: center;">${this.replaceVariables(settings.headerCenter, book)}</span>
<span style="flex: 1; text-align: right;">${this.replaceVariables(settings.headerRight, book)}</span>
</div>
`;
}
// 添加页脚模板构建方法
private buildFooterTemplate(settings?: HeaderFooterTocSettings, book?: Book): string {
if (!settings?.footerEnabled) return '';
return `
<div style="
width: 100%;
height: ${settings.footerHeight}px;
font-size: ${settings.footerFontSize}px;
color: ${settings.footerColor};
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
box-sizing: border-box;
border-top: 1px solid #ddd;
">
<span style="flex: 1; text-align: left;">${this.replaceVariables(settings.footerLeft)}</span>
<span style="flex: 1; text-align: center;">${this.replaceVariables(settings.footerCenter)}</span>
<span style="flex: 1; text-align: right;">${this.replaceVariables(settings.footerRight)}</span>
</div>
`;
}
// 添加变量替换方法
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() || '<span class="pageNumber"></span>')
.replace(/\{\{totalPages\}\}/g, totalPages?.toString() || '<span class="totalPages"></span>');
}
// 新增:生成准确页码的目录
private async generateAccurateTOC(typographySettings: TypographySettings, htmlContent: HTMLElement, book: Book): Promise<string> {
const settings = typographySettings.headerFooterToc;
if (!settings?.tocEnabled) return '';
// 获取标题到页码的映射
const headingPageMapping = await this.getHeadingPageMapping(typographySettings, htmlContent, book);
if (headingPageMapping.length === 0) return '';
let tocHtml = `
<div class="table-of-contents" style="
page-break-after: always;
font-family: ${settings.tocFontFamily || 'serif'};
font-size: ${settings.tocFontSize}px;
color: ${settings.tocColor || '#000000'};
margin: 40px 0;
">
<h1 style="text-align: center; margin-bottom: 30px;">${settings.tocTitle}</h1>
<div class="toc-content">
`;
headingPageMapping.forEach(heading => {
const indent = (heading.level - 1) * (settings.tocIndent || settings.tocIndentSize || 20);
tocHtml += `
<div class="toc-item" style="
margin-left: ${indent}px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: baseline;
">
<span class="toc-text">${heading.text}</span>
<span class="toc-dots" style="
flex: 1;
border-bottom: 1px dotted #ccc;
margin: 0 10px;
height: 1px;
align-self: center;
"></span>
<span class="toc-page">${heading.pageNumber}</span>
</div>
`;
});
tocHtml += `
</div>
</div>
`;
return tocHtml;
}
// 新增:获取标题页码映射
private async getHeadingPageMapping(typographySettings: TypographySettings, htmlContent: HTMLElement, book: Book): Promise<Array<{ level: number, text: string, id: string, pageNumber: number }>> {
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 = `
<html>
<head>
<meta charset="utf-8">
<title>${book.basic.title}</title>
<style>${style}</style>
</head>
<body>
${htmlContent.innerHTML}
</body>
</html>
`;
// 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<void>((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<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;
}
}

View file

@ -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';

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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;
}
}
}