初始化排版完成

This commit is contained in:
Yeban8090 2025-06-10 03:46:51 +08:00
parent a47fbcf013
commit af7250bd5a
20 changed files with 3654 additions and 142 deletions

View file

@ -1,7 +1,7 @@
{
"id": "book-smith",
"name": "Book Smith",
"version": "1.0.2",
"version": "1.0.3",
"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",

181
package-lock.json generated
View file

@ -11,6 +11,7 @@
"dependencies": {
"@popperjs/core": "^2.11.8",
"docx": "^9.5.0",
"pagedjs": "^0.4.3",
"uuid": "^11.1.0"
},
"devDependencies": {
@ -25,6 +26,26 @@
"typescript": "4.7.4"
}
},
"node_modules/@babel/polyfill": {
"version": "7.12.1",
"resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.12.1.tgz",
"integrity": "sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==",
"deprecated": "🚨 This package has been deprecated in favor of separate inclusion of a polyfill and regenerator-runtime (when needed). See the @babel/polyfill docs (https://babeljs.io/docs/en/babel-polyfill) for more information.",
"license": "MIT",
"dependencies": {
"core-js": "^2.6.5",
"regenerator-runtime": "^0.13.4"
}
},
"node_modules/@babel/runtime": {
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
@ -999,6 +1020,12 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/clear-cut": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/clear-cut/-/clear-cut-2.0.2.tgz",
"integrity": "sha512-WVgn/gSejQ+0aoR8ucbKIdo6icduPZW6AbWwyUmAUgxy63rUYjwa5rj/HeoNPhf0/XPrl82X8bO/hwBkSmsFtg==",
"license": "MIT"
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -1029,6 +1056,14 @@
"license": "MIT",
"peer": true
},
"node_modules/core-js": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
"integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
"deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
"hasInstallScript": true,
"license": "MIT"
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@ -1051,6 +1086,32 @@
"node": ">= 8"
}
},
"node_modules/css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
"license": "MIT",
"dependencies": {
"mdn-data": "2.0.14",
"source-map": "^0.6.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/d": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
"license": "ISC",
"dependencies": {
"es5-ext": "^0.10.64",
"type": "^2.7.2"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@ -1130,6 +1191,46 @@
"undici-types": "~6.21.0"
}
},
"node_modules/es5-ext": {
"version": "0.10.64",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"es6-iterator": "^2.0.3",
"es6-symbol": "^3.1.3",
"esniff": "^2.0.1",
"next-tick": "^1.1.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
"license": "MIT",
"dependencies": {
"d": "1",
"es5-ext": "^0.10.35",
"es6-symbol": "^3.1.1"
}
},
"node_modules/es6-symbol": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
"license": "ISC",
"dependencies": {
"d": "^1.0.2",
"ext": "^1.7.0"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/esbuild": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.3.tgz",
@ -1325,6 +1426,21 @@
"node": ">=4.0"
}
},
"node_modules/esniff": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
"license": "ISC",
"dependencies": {
"d": "^1.0.1",
"es5-ext": "^0.10.62",
"event-emitter": "^0.3.5",
"type": "^2.7.2"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
@ -1413,6 +1529,25 @@
"node": ">=0.10.0"
}
},
"node_modules/event-emitter": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
"license": "MIT",
"dependencies": {
"d": "1",
"es5-ext": "~0.10.14"
}
},
"node_modules/ext": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
"license": "ISC",
"dependencies": {
"type": "^2.7.2"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -1897,6 +2032,12 @@
"license": "MIT",
"peer": true
},
"node_modules/mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"license": "CC0-1.0"
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -1984,6 +2125,12 @@
"license": "MIT",
"peer": true
},
"node_modules/next-tick": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
"license": "ISC"
},
"node_modules/obsidian": {
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz",
@ -2063,6 +2210,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pagedjs": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/pagedjs/-/pagedjs-0.4.3.tgz",
"integrity": "sha512-YtAN9JAjsQw1142gxEjEAwXvOF5nYQuDwnQ67RW2HZDkMLI+b4RsBE37lULZa9gAr6kDAOGBOhXI4wGMoY3raw==",
"license": "MIT",
"dependencies": {
"@babel/polyfill": "^7.10.1",
"@babel/runtime": "^7.21.0",
"clear-cut": "^2.0.2",
"css-tree": "^1.1.3",
"event-emitter": "^0.3.5"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@ -2203,6 +2363,12 @@
"util-deprecate": "~1.0.1"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT"
},
"node_modules/regexpp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
@ -2346,6 +2512,15 @@
"node": ">=8"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@ -2456,6 +2631,12 @@
"dev": true,
"license": "0BSD"
},
"node_modules/type": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
"license": "ISC"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View file

@ -1,31 +1,58 @@
import { App, Notice } from "obsidian";
import { BaseModal } from "./BaseModal";
import { App, setIcon, Notice } from 'obsidian';
import { BookManager } from "../services/BookManager";
import { Book } from "../types/book";
import { ExportService } from "../services/ExportService";
import { i18n } from "../i18n/i18n";
import BookSmithPlugin from '../main';
export class EbookModal extends BaseModal {
export class EbookView {
private container: HTMLElement;
private bookManager: BookManager;
private exportService: ExportService;
private books: Book[] = [];
private selectedBook: Book | null = null;
private selectedFormat: string = 'txt';
constructor(container: HTMLElement, private app: App, private settings: any) {
super(container, '生成电子书');
this.bookManager = new BookManager(app, settings);
this.exportService = new ExportService(app, settings);
constructor(
private app: App,
private plugin: BookSmithPlugin,
private parentEl: HTMLElement,
private onExit: () => void
) {
this.bookManager = new BookManager(app, plugin.settings);
this.exportService = new ExportService(app, plugin.settings);
this.createUI();
}
async open() {
// 加载所有书籍
this.books = await this.bookManager.getAllBooks();
super.open();
async initialize() {
try {
// 加载所有书籍
this.books = await this.bookManager.getAllBooks();
this.updateBookSelector();
} catch (error) {
console.error('加载书籍失败:', error);
}
}
protected createContent() {
const content = this.element.createDiv({ cls: 'ebook-content' });
private createUI() {
this.container = this.parentEl.createDiv({ cls: 'book-smith-ebook-view' });
this.createHeader();
this.createContent();
}
private createHeader() {
const header = this.container.createDiv({ cls: 'ebook-header' });
setIcon(header.createSpan({ cls: 'ebook-header-icon' }), 'book');
header.createSpan({ text: i18n.t('GENERATE_EBOOK'), cls: 'ebook-title' });
// 添加退出按钮
const exitBtn = header.createDiv({ cls: 'ebook-exit-btn' });
setIcon(exitBtn, 'x');
exitBtn.addEventListener('click', () => this.onExit());
}
private createContent() {
const content = this.container.createDiv({ cls: 'ebook-content' });
// 电子书信息表单
const form = content.createDiv({ cls: 'ebook-form' });
@ -34,7 +61,7 @@ export class EbookModal extends BaseModal {
this.createBookSelector(form);
// 格式选择
const formatField = this.createFormField(form, '格式', 'format', 'select');
const formatField = this.createFormField(form, i18n.t('FORMAT') || '格式', 'format', 'select');
const formatSelect = formatField.querySelector('select');
if (formatSelect) {
// 添加支持的格式
@ -49,40 +76,59 @@ export class EbookModal extends BaseModal {
}
// 生成按钮
const generateBtn = content.createDiv({ cls: 'generate-btn', text: '生成电子书' });
const generateBtn = content.createDiv({ cls: 'generate-btn', text: i18n.t('GENERATE_EBOOK') || '生成电子书' });
generateBtn.addEventListener('click', () => {
this.exportBook();
});
}
private createBookSelector(container: HTMLElement) {
const field = this.createFormField(container, '选择书籍', 'book', 'select');
const select = field.querySelector('select');
private updateBookSelector() {
const select = this.container.querySelector('select');
if (!select) return;
if (select && this.books.length > 0) {
// 清空现有选项
select.innerHTML = '';
if (this.books.length > 0) {
// 添加书籍选项
for (const book of this.books) {
this.createSelectOption(select, book.basic.uuid, book.basic.title);
this.createSelectOption(select as HTMLSelectElement, book.basic.uuid, book.basic.title);
}
// 默认选择第一本书
this.selectedBook = this.books[0];
// 监听选择变化
select.addEventListener('change', (e) => {
const uuid = (e.target as HTMLSelectElement).value;
this.selectedBook = this.books.find(book => book.basic.uuid === uuid) || null;
});
} else if (select) {
select.value = this.books[0].basic.uuid;
} else {
// 没有书籍时显示提示
this.createSelectOption(select, '', '没有可用的书籍');
this.createSelectOption(select as HTMLSelectElement, '', i18n.t('NO_BOOKS_AVAILABLE') || '没有可用的书籍');
select.disabled = true;
}
}
private createBookSelector(container: HTMLElement) {
const field = this.createFormField(container, i18n.t('SELECT_BOOK') || '选择书籍', 'book', 'select');
const select = field.querySelector('select');
if (select) {
// 监听选择变化
select.addEventListener('change', (e) => {
const uuid = (e.target as HTMLSelectElement).value;
this.selectedBook = this.books.find(book => book.basic.uuid === uuid) || null;
// 添加高亮效果
select.classList.add('selected-option');
setTimeout(() => select.classList.remove('selected-option'), 500);
});
}
}
// 其余方法从 EbookModal 迁移过来,保持相同的功能
// ... 省略其他方法,与 EbookModal 中的相同 ...
// 以下是必要的方法实现
private async exportBook() {
if (!this.selectedBook) {
this.showNotice('请先选择一本书籍');
new Notice(i18n.t('SELECT_BOOK_FIRST') || '请先选择一本书籍');
return;
}
@ -95,7 +141,7 @@ export class EbookModal extends BaseModal {
// 如果导出功能尚未实现,显示提示
if (content.content.startsWith(`${this.selectedFormat.toUpperCase()}导出功能开发中`)) {
this.showNotice(`${this.selectedFormat.toUpperCase()}导出功能尚在开发中,敬请期待!`);
new Notice(`${this.selectedFormat.toUpperCase()}导出功能尚在开发中,敬请期待!`);
return;
}
@ -122,14 +168,13 @@ export class EbookModal extends BaseModal {
document.body.removeChild(a);
}
this.showNotice(`导出成功!`);
this.close();
new Notice(`导出成功!`);
} catch (error) {
console.error('导出错误:', error);
this.showNotice(`导出失败: ${error.message}`);
new Notice(`导出失败: ${error instanceof Error ? error.message : String(error)}`);
}
}
private createFormField(container: HTMLElement, label: string, id: string, type: string, placeholder?: string) {
const field = container.createDiv({ cls: 'form-field' });
field.createDiv({ cls: 'field-label', text: label });
@ -141,7 +186,7 @@ export class EbookModal extends BaseModal {
attr: {
type,
id,
placeholder: placeholder || null
placeholder: placeholder || ''
}
});
}
@ -155,4 +200,8 @@ export class EbookModal extends BaseModal {
option.text = text;
select.appendChild(option);
}
remove() {
this.container.remove();
}
}

View file

@ -0,0 +1,939 @@
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 { i18n } from "../i18n/i18n";
import BookSmithPlugin from '../main';
import { ImgTemplateManager, ImgTemplate } from '../services/ImgTemplateManager';
import { ThemeManager, Theme } from '../services/ThemeManager';
import { CoverManager, CoverSettings } from '../services/CoverManager';
import { CoverSettingModal } from '../modals/CoverSettingModal';
import { PaginatedEngine, IBlock, extractBlocks, generatePaginatedTOC } from '../utils/PaginatedEngine';
interface TypographySettings {
fontFamily: string;
fontSize: string;
lineHeight: string;
margin: string;
templateId: string;
themeId: string;
coverSettings?: CoverSettings; // 封面设置
showCover: boolean; // 是否显示封面
bookSize: string; // 新增:书籍开本大小
}
export class TypographyView {
private container: HTMLElement;
private bookManager: BookManager;
private exportService: ExportService;
private imgTemplateManager: ImgTemplateManager;
private themeManager: ThemeManager;
private coverManager: CoverManager;
private books: Book[] = [];
private selectedBook: Book | null = null;
private previewElement: HTMLElement | null = null;
private coverPreviewElement: HTMLElement | null = null;
private currentTemplate: ImgTemplate | null = null;
private coverSettings: CoverSettings | null = null;
// 自定义选择器引用
private customBookSelect: HTMLElement | null = null;
private customTemplateSelect: HTMLElement | null = null;
private customThemeSelect: HTMLElement | null = null;
private customFontSelect: HTMLElement | null = null;
private fontSizeInput: HTMLInputElement | null = null;
// 添加开本大小选择器引用
private customBookSizeSelect: HTMLElement | null = null;
private rootPath: string = ''; // 添加根路径属性
private tempRenderContainer: HTMLElement | null = null; // 临时渲染容器
constructor(
private app: App,
private plugin: BookSmithPlugin,
private parentEl: HTMLElement,
private onExit: () => void
) {
this.bookManager = new BookManager(app, plugin.settings);
this.exportService = new ExportService(app, plugin.settings);
this.themeManager = new ThemeManager(app, plugin.settings);
this.imgTemplateManager = new ImgTemplateManager(app, this.themeManager);
this.coverManager = new CoverManager(app);
this.rootPath = plugin.settings.defaultBookPath || ''; // 初始化根路径
this.createUI();
// 创建临时渲染容器
this.tempRenderContainer = document.createElement('div');
this.tempRenderContainer.style.display = 'none';
document.body.appendChild(this.tempRenderContainer);
}
async initialize() {
try {
// 加载所有书籍
this.books = await this.bookManager.getAllBooks();
// 初始化各个选择器
this.initializeBookSelect();
this.initializeTemplateSelect();
this.initializeThemeSelect();
this.initializeFontSelect();
this.initializeFontSizeControls();
this.initializeBookSizeSelect(); // 新增初始化开本大小选择器
// 更新预览
this.updatePreview();
} catch (error) {
console.error('加载书籍失败:', error);
}
}
private createUI() {
this.container = this.parentEl.createDiv({ cls: 'book-smith-typography-view' });
this.createHeader();
this.createContent();
}
private createHeader() {
const header = this.container.createDiv({ cls: 'typography-header' });
setIcon(header.createSpan({ cls: 'typography-header-icon' }), 'edit-3');
header.createSpan({ text: i18n.t('DESIGN_TYPOGRAPHY'), cls: 'typography-title' });
// 添加退出按钮
const exitBtn = header.createDiv({ cls: 'typography-exit-btn' });
setIcon(exitBtn, 'x');
exitBtn.addEventListener('click', () => this.onExit());
}
private createContent() {
// 创建主容器
const content = this.container.createDiv({ cls: 'typography-content' });
// 创建顶部选择器面板
const topPanel = content.createDiv({ cls: 'typography-top-panel' });
// 创建预览面板
const previewPanel = content.createDiv({ cls: 'typography-preview-panel' });
// 创建底部按钮面板
const bottomPanel = content.createDiv({ cls: 'typography-bottom-panel' });
// 创建顶部选择器
this.createTopSelectors(topPanel);
// 创建预览区域
this.createPreviewArea(previewPanel);
// 创建底部按钮
this.createButtons(bottomPanel);
}
private createPreviewArea(container: HTMLElement) {
container.createEl('h2', { text: i18n.t('PREVIEW') });
// 创建一个包含封面和内容的容器
const previewContainer = container.createDiv({ cls: 'typography-preview-container' });
// 添加封面预览(独立容器)
const coverContainer = previewContainer.createDiv({ cls: 'typography-cover-container' });
this.coverPreviewElement = coverContainer.createDiv({ cls: 'typography-cover-preview' });
// 内容预览(独立容器)
const contentContainer = previewContainer.createDiv({ cls: 'typography-content-container' });
this.previewElement = contentContainer.createDiv({ cls: 'typography-preview' });
}
// 按照要求的顺序创建顶部选择器:书籍、模板、主题、字体、字号大小
private createTopSelectors(container: HTMLElement) {
const selectorsContainer = container.createDiv({ cls: 'typography-selectors' });
// 1. 创建书籍选择器
this.createBookSelector(selectorsContainer);
// 2. 创建模板选择器
this.createTemplateSelector(selectorsContainer);
// 3. 创建主题选择器
this.createThemeSelector(selectorsContainer);
// 4. 创建字体选择器
this.createFontSelector(selectorsContainer);
// 5. 创建字号大小控制
this.createFontSizeControls(selectorsContainer);
// 6. 创建开本大小选择器
this.createBookSizeSelector(selectorsContainer);
}
// 自定义选择器通用方法
private createCustomSelect(parent: HTMLElement, className: string, options: { value: string, text: string }[]): HTMLElement {
const container = parent.createDiv({ cls: `red-select-container ${className}` });
const select = container.createDiv({ cls: 'red-select' });
const textSpan = select.createSpan({ cls: 'red-text' });
const arrowSpan = select.createSpan({ cls: 'red-select-arrow' });
setIcon(arrowSpan, 'chevron-down');
// 默认选择第一个选项
if (options.length > 0) {
textSpan.textContent = options[0].text;
select.dataset.value = options[0].value;
}
// 创建下拉菜单
const dropdown = container.createDiv({ cls: 'red-select-dropdown' });
// 添加选项
for (const option of options) {
const item = dropdown.createDiv({ cls: 'red-select-item' });
item.textContent = option.text;
item.dataset.value = option.value;
// 默认选中第一个
if (options.indexOf(option) === 0) {
item.addClass('red-selected');
}
// 点击选项
item.addEventListener('click', () => {
// 更新显示文本和值
textSpan.textContent = option.text;
select.dataset.value = option.value;
// 更新选中状态
dropdown.querySelectorAll('.red-select-item').forEach(el => {
el.removeClass('red-selected');
});
item.addClass('red-selected');
// 关闭下拉菜单
dropdown.removeClass('red-show');
arrowSpan.style.transform = '';
// 触发自定义事件
select.dispatchEvent(new CustomEvent('change', {
detail: { value: option.value, text: option.text }
}));
});
}
// 点击选择器显示/隐藏下拉菜单
select.addEventListener('click', (e) => {
e.stopPropagation();
dropdown.toggleClass('red-show', !dropdown.hasClass('red-show'));
arrowSpan.style.transform = dropdown.hasClass('red-show') ? 'rotate(180deg)' : '';
});
// 点击其他地方关闭下拉菜单
document.addEventListener('click', () => {
dropdown.removeClass('red-show');
arrowSpan.style.transform = '';
});
return container;
}
// 更新自定义选择器的选项
private updateCustomSelectOptions(selectElement: HTMLElement, options: { value: string, text: string }[]) {
const dropdown = selectElement.querySelector('.red-select-dropdown');
const select = selectElement.querySelector('.red-select');
const textSpan = selectElement.querySelector('.red-text');
if (!dropdown || !select || !textSpan) return;
// 清空现有选项
dropdown.innerHTML = '';
// 添加新选项
for (const option of options) {
const item = dropdown.createDiv({ cls: 'red-select-item' });
item.textContent = option.text;
item.dataset.value = option.value;
// 默认选中第一个
if (options.indexOf(option) === 0) {
item.addClass('red-selected');
textSpan.textContent = option.text;
(select as HTMLElement).setAttribute('data-value', option.value);
}
// 点击选项
item.addEventListener('click', () => {
// 更新显示文本和值
textSpan.textContent = option.text;
(select as HTMLElement).setAttribute('data-value', option.value);
// 更新选中状态
dropdown.querySelectorAll('.red-select-item').forEach(el => {
el.removeClass('red-selected');
});
item.addClass('red-selected');
// 关闭下拉菜单
dropdown.removeClass('red-show');
// 触发自定义事件
select.dispatchEvent(new CustomEvent('change', {
detail: { value: option.value, text: option.text }
}));
});
}
}
// 1. 书籍选择器
private createBookSelector(parent: HTMLElement) {
// 获取书籍选项
const bookOptions = this.books.map(book => ({
value: book.basic.uuid,
text: book.basic.title
}));
// 如果没有书籍,添加一个提示选项
if (bookOptions.length === 0) {
bookOptions.push({
value: '',
text: i18n.t('NO_BOOKS_AVAILABLE') || '没有可用的书籍'
});
}
// 创建自定义选择器
this.customBookSelect = this.createCustomSelect(
parent,
'red-book-select',
bookOptions
);
this.customBookSelect.id = 'book-select';
}
// 初始化书籍选择器
private initializeBookSelect() {
if (!this.customBookSelect) return;
// 更新书籍选项
const bookOptions = this.books.map(book => ({
value: book.basic.uuid,
text: book.basic.title
}));
// 更新选择器选项
this.updateCustomSelectOptions(this.customBookSelect, bookOptions);
// 添加事件监听
this.customBookSelect.querySelector('.red-select')?.addEventListener('change', async (e: any) => {
const value = e.detail.value;
this.selectedBook = this.books.find(book => book.basic.uuid === value) || null;
this.updatePreview();
});
// 如果有书籍,选择第一本
if (bookOptions.length > 0) {
const select = this.customBookSelect.querySelector('.red-select');
if (select) {
(select as HTMLElement).setAttribute('data-value', bookOptions[0].value);
this.selectedBook = this.books[0];
this.updatePreview();
}
}
}
// 2. 模板选择器
private createTemplateSelector(parent: HTMLElement) {
this.customTemplateSelect = this.createCustomSelect(
parent,
'red-template-select',
[{ value: '', text: i18n.t('LOADING') || '加载中...' }]
);
this.customTemplateSelect.id = 'template-select';
}
// 初始化模板选择器
private async initializeTemplateSelect() {
if (!this.customTemplateSelect) return;
// 获取模板选项
const templateOptions = await this.getTemplateOptions();
// 更新选择器选项
this.updateCustomSelectOptions(this.customTemplateSelect, templateOptions);
// 添加事件监听
this.customTemplateSelect.querySelector('.red-select')?.addEventListener('change', async (e: any) => {
const value = e.detail.value;
this.currentTemplate = this.imgTemplateManager.getTemplate(value) || null;
this.updatePreview();
});
// 默认选择第一个模板
if (templateOptions.length > 0) {
const select = this.customTemplateSelect.querySelector('.red-select');
if (select) {
(select as HTMLElement).setAttribute('data-value', templateOptions[0].value);
this.currentTemplate = this.imgTemplateManager.getTemplate(templateOptions[0].value) || null;
}
}
}
// 3. 主题选择器
private createThemeSelector(parent: HTMLElement) {
this.customThemeSelect = this.createCustomSelect(
parent,
'red-theme-select',
[{ value: '', text: i18n.t('LOADING') || '加载中...' }]
);
this.customThemeSelect.id = 'theme-select';
}
// 初始化主题选择器
private async initializeThemeSelect() {
if (!this.customThemeSelect) return;
// 获取主题选项
const themes = await this.getThemeOptions();
// 更新选择器选项
this.updateCustomSelectOptions(this.customThemeSelect, themes);
// 添加事件监听
this.customThemeSelect.querySelector('.red-select')?.addEventListener('change', async (e: any) => {
const value = e.detail.value;
this.themeManager.setCurrentTheme(value);
if (this.previewElement) {
this.themeManager.applyTheme(this.previewElement);
}
});
// 默认选择第一个主题
if (themes.length > 0) {
const select = this.customThemeSelect.querySelector('.red-select');
if (select) {
(select as HTMLElement).setAttribute('data-value', themes[0].value);
this.themeManager.setCurrentTheme(themes[0].value);
}
}
}
// 4. 字体选择器
private createFontSelector(parent: HTMLElement) {
const fontOptions = [
{ value: 'default', text: i18n.t('DEFAULT_FONT') || '默认字体' },
{ value: 'serif', text: i18n.t('SERIF_FONT') || '衬线字体' },
{ value: 'sans-serif', text: i18n.t('SANS_SERIF_FONT') || '无衬线字体' },
{ value: 'monospace', text: i18n.t('MONOSPACE_FONT') || '等宽字体' }
];
this.customFontSelect = this.createCustomSelect(
parent,
'red-font-select',
fontOptions
);
this.customFontSelect.id = 'font-select';
}
// 初始化字体选择器
private initializeFontSelect() {
if (!this.customFontSelect) return;
// 添加事件监听
this.customFontSelect.querySelector('.red-select')?.addEventListener('change', () => {
this.updatePreview();
});
}
// 5. 字号大小控制
private createFontSizeControls(parent: HTMLElement) {
const fontSizeGroup = parent.createEl('div', { cls: 'red-font-size-group' });
this.fontSizeInput = fontSizeGroup.createEl('input', {
cls: 'red-font-size-input',
type: 'text',
value: '16',
attr: {
style: 'border: none; outline: none; background: transparent;'
}
}) as HTMLInputElement;
}
// 初始化字号控制
private initializeFontSizeControls() {
if (!this.fontSizeInput) return;
const updateFontSize = () => {
if (!this.fontSizeInput) return;
this.updatePreview();
};
// 获取按钮元素
const decreaseButton = this.fontSizeInput.parentElement?.querySelector('button:first-child');
const increaseButton = this.fontSizeInput.parentElement?.querySelector('button:last-child');
// 添加减小字号事件
decreaseButton?.addEventListener('click', () => {
if (!this.fontSizeInput) return;
const currentSize = parseInt(this.fontSizeInput.value) || 16;
if (currentSize > 12) {
this.fontSizeInput.value = (currentSize - 1).toString();
updateFontSize();
}
});
// 添加增大字号事件
increaseButton?.addEventListener('click', () => {
if (!this.fontSizeInput) return;
const currentSize = parseInt(this.fontSizeInput.value) || 16;
if (currentSize < 30) {
this.fontSizeInput.value = (currentSize + 1).toString();
updateFontSize();
}
});
// 添加输入框变化事件
this.fontSizeInput.addEventListener('change', updateFontSize);
this.fontSizeInput.addEventListener('input', updateFontSize);
}
// 获取选项方法
private async getThemeOptions() {
const themes = this.themeManager.getVisibleThemes();
return themes.map(theme => ({
value: theme.id,
text: theme.name
}));
}
private async getTemplateOptions() {
const templates = this.imgTemplateManager.getVisibleTemplates();
return templates.map(template => ({
value: template.id,
text: template.name
}));
}
// 开本大小选择器
private createBookSizeSelector(parent: HTMLElement) {
const bookSizeOptions = [
{ value: 'a4', text: 'A4' },
{ value: 'a5', text: 'A5' },
{ value: 'b5', text: 'B5' },
{ value: '16k', text: '16K' },
{ value: 'custom', text: i18n.t('CUSTOM_SIZE') || '自定义' }
];
this.customBookSizeSelect = this.createCustomSelect(
parent,
'red-book-size-select',
bookSizeOptions
);
this.customBookSizeSelect.id = 'book-size-select';
}
// 初始化开本大小选择器
private initializeBookSizeSelect() {
if (!this.customBookSizeSelect) return;
// 添加事件监听
this.customBookSizeSelect.querySelector('.red-select')?.addEventListener('change', () => {
this.updatePreview();
});
}
// 获取排版设置
private getTypographySettings(): TypographySettings {
// 获取封面开关状态
const coverToggle = this.parentEl.querySelector('.cover-toggle-input') as HTMLInputElement;
const showCover = coverToggle ? coverToggle.checked : true;
return {
fontFamily: (this.customFontSelect?.querySelector('.red-select') as HTMLElement)?.getAttribute('data-value') || 'default',
fontSize: this.fontSizeInput?.value || '16',
lineHeight: 'normal', // 保留默认值
margin: '0', // 保留默认值
templateId: (this.customTemplateSelect?.querySelector('.red-select') as HTMLElement)?.getAttribute('data-value') || 'notes',
themeId: (this.customThemeSelect?.querySelector('.red-select') as HTMLElement)?.getAttribute('data-value') || 'default',
coverSettings: this.coverSettings || undefined,
showCover: showCover,
bookSize: (this.customBookSizeSelect?.querySelector('.red-select') as HTMLElement)?.getAttribute('data-value') || 'a4'
};
}
// 预览相关方法
// 在 updatePreview 方法中添加分页逻辑
private async updatePreview() {
if (!this.selectedBook) return;
// 获取封面开关状态
const coverToggle = this.parentEl.querySelector('.cover-toggle-input') as HTMLInputElement;
const showCover = coverToggle ? coverToggle.checked : true;
// 获取排版设置
const settings = this.getTypographySettings();
// 更新预览样式
this.updatePreviewStyle(settings);
// 分离封面和内容的显示逻辑
if (this.coverPreviewElement) {
this.coverPreviewElement.style.display = showCover ? 'block' : 'none';
if (showCover) {
this.updateCoverPreview();
}
}
if (this.previewElement) {
this.previewElement.style.display = 'block';
// 清空预览元素
this.previewElement.empty();
// 应用模板
if (this.currentTemplate) {
this.currentTemplate.render(this.previewElement);
}
// 应用主题
this.themeManager.applyTheme(this.previewElement);
// 渲染内容
await this.renderPaginatedContent(settings);
}
}
// 新增:渲染分页内容
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
});
// 执行分页
const pageCount = engine.paginate(blocks);
// 5. 生成分页目录
const tocPages = generatePaginatedTOC(this.previewElement, contentContainer, settings.bookSize);
// 6. 将目录页添加到内容前面
const tocContainer = this.previewElement.createDiv({ cls: 'typography-toc-pages' });
tocPages.forEach(tocPage => {
tocContainer.appendChild(tocPage);
});
// 将目录容器移到内容容器前面
this.previewElement.insertBefore(tocContainer, contentContainer);
// 7. 添加页码信息
const pageInfo = this.previewElement.createDiv({ cls: 'page-info' });
pageInfo.textContent = `${pageCount + tocPages.length} 页(目录 ${tocPages.length} 页,正文 ${pageCount} 页)`;
}
// 渲染所有内容到指定容器
private async renderAllContent(container: HTMLElement) {
if (!this.selectedBook) return;
// 渲染章节内容
if (this.selectedBook.structure && this.selectedBook.structure.tree) {
await this.renderChapters(this.selectedBook.structure.tree, container);
}
}
// 递归渲染章节内容(不分页,只渲染到临时容器)
private async renderChapters(chapters: ChapterNode[], container: HTMLElement, level: number = 1) {
if (!chapters || chapters.length === 0) return;
for (const chapter of chapters) {
// 跳过被排除的章节
if (chapter.exclude) continue;
// 创建标题元素
const heading = document.createElement(`h${Math.min(level, 6)}`);
heading.textContent = chapter.title;
heading.dataset.actualLevel = level.toString(); // 添加自定义属性记录实际层级
container.appendChild(heading);
// 如果是文件节点,渲染文件内容
if (chapter.type === 'file' && chapter.path) {
try {
// 构建文件路径
const filePath = `${this.rootPath}/${this.selectedBook?.basic.title}/${chapter.path}`;
const file = this.app.vault.getAbstractFileByPath(filePath);
if (file instanceof TFile) {
// 创建内容容器
const contentEl = container.createDiv({ cls: 'chapter-content' });
// 使用 cachedRead 获取文件内容
const content = await this.app.vault.cachedRead(file);
// 使用 MarkdownRenderer 渲染内容
await MarkdownRenderer.render(
this.app,
content,
contentEl,
file.path,
this.plugin
);
}
} catch (error) {
console.error(`无法读取文件 ${chapter.path}:`, error);
container.createEl('p', { text: `*无法读取此章节内容*`, cls: 'error-message' });
}
}
// 递归处理子章节
if (chapter.children && chapter.children.length > 0) {
await this.renderChapters(chapter.children, container, level + 1);
}
}
}
private updatePreviewStyle(settings: TypographySettings) {
if (!this.previewElement) return;
// 重置样式
this.previewElement.className = 'typography-preview';
// 应用字体
this.previewElement.classList.add(`font-${settings.fontFamily}`);
// 应用字号
this.previewElement.style.fontSize = `${settings.fontSize}px`;
// 应用行间距
this.previewElement.classList.add(`line-height-${settings.lineHeight}`);
// 应用页边距
this.previewElement.classList.add(`margin-${settings.margin}`);
// 应用开本大小
// this.previewElement.classList.add(`book-size-${settings.bookSize}`);
// 如果封面预览元素存在,也应用开本大小
if (this.coverPreviewElement) {
this.coverPreviewElement.className = 'typography-cover-preview';
this.coverPreviewElement.classList.add(`book-size-${settings.bookSize}`);
}
}
// 底部按钮
private createButtons(container: HTMLElement) {
// 创建按钮容器
const buttonsContainer = container.createDiv({ cls: 'typography-buttons' });
// 添加封面设计开关
this.createCoverToggle(buttonsContainer);
// 应用按钮
const applyBtn = buttonsContainer.createEl('button', {
text: i18n.t('APPLY') || '应用',
cls: 'typography-btn apply-btn'
});
applyBtn.addEventListener('click', () => {
// 应用排版设置
new Notice(i18n.t('TYPOGRAPHY_APPLIED') || '排版设置已应用');
});
// 导出按钮
const exportBtn = buttonsContainer.createEl('button', {
text: i18n.t('EXPORT') || '导出',
cls: 'typography-btn export-btn'
});
exportBtn.addEventListener('click', () => this.exportWithTypography());
}
// 导出功能
private async exportWithTypography() {
if (!this.selectedBook) {
new Notice(i18n.t('SELECT_BOOK_FIRST') || '请先选择一本书籍');
return;
}
try {
// 获取排版设置
// 调用导出服务,传入排版设置和封面设置
// const result = await this.exportService.exportBook(
// 'pdf',
// this.selectedBook,
// settings
// );
// 处理导出结果
// if (result.success) {
// new Notice(i18n.t('EXPORT_SUCCESS') || '导出成功');
// // 如果有下载链接,触发下载
// if (result.downloadUrl) {
// const a = document.createElement('a');
// a.href = result.downloadUrl;
// a.download = result.fileName || 'export.html';
// document.body.appendChild(a);
// a.click();
// document.body.removeChild(a);
// }
// } else {
// new Notice(i18n.t('EXPORT_FAILED') || '导出失败: ' + (result.error || '未知错误'));
// }
} catch (error) {
console.error('导出错误:', error);
new Notice(`导出失败: ${error instanceof Error ? error.message : String(error)}`);
}
}
// 添加封面设计开关
private createCoverToggle(parent: HTMLElement) {
const coverToggleContainer = parent.createDiv({ cls: 'cover-toggle-container' });
coverToggleContainer.createEl('span', {
text: i18n.t('SHOW_COVER') || '显示封面',
cls: 'cover-toggle-label'
});
const coverToggle = coverToggleContainer.createEl('input', {
type: 'checkbox',
cls: 'cover-toggle-input'
}) as HTMLInputElement;
coverToggle.checked = false; // 默认显示封面
coverToggle.addEventListener('change', () => {
this.updatePreview();
});
// 添加封面设计按钮
const coverDesignBtn = coverToggleContainer.createEl('button', {
text: i18n.t('DESIGN_COVER') || '设计封面',
cls: 'cover-design-btn'
});
coverDesignBtn.addEventListener('click', () => this.openCoverDesigner());
}
// 打开封面设计器
private openCoverDesigner() {
if (!this.selectedBook) {
new Notice(i18n.t('SELECT_BOOK_FIRST') || '请先选择一本书籍');
return;
}
new CoverSettingModal(
this.app,
(settings) => {
this.coverSettings = settings;
this.updatePreview();
},
this.parentEl,
this.coverManager,
this.coverSettings || undefined,
this.selectedBook.basic.title,
this.selectedBook.basic.author
).open();
}
// 添加封面预览更新方法
private updateCoverPreview() {
if (!this.coverPreviewElement || !this.selectedBook) return;
// 清除现有内容
this.coverPreviewElement.empty();
// 应用封面样式
if (this.coverSettings) {
const contentContainer = this.coverManager.applyCoverStyles(this.coverPreviewElement, this.coverSettings);
// 添加标题和作者
if (contentContainer) {
// 添加书名
const titleEl = contentContainer.createEl('div', {
cls: 'cover-title',
text: this.selectedBook.basic.title
});
titleEl.setAttribute('style', this.coverSettings.titleStyle);
// 添加副标题
if (this.selectedBook.basic.subtitle) {
const subtitleEl = contentContainer.createEl('div', {
cls: 'cover-subtitle',
text: this.selectedBook.basic.subtitle
});
subtitleEl.setAttribute('style', 'font-size: 18px; color: #ffffff; margin-top: 10px; text-shadow: 0 1px 2px rgba(0,0,0,0.5);');
}
// 添加作者信息
if (this.selectedBook.basic.author && this.selectedBook.basic.author.length > 0) {
const authorEl = contentContainer.createEl('div', {
cls: 'cover-author',
text: this.selectedBook.basic.author.join(', ')
});
authorEl.setAttribute('style', this.coverSettings.authorStyle);
}
// 添加描述信息
if (this.selectedBook.basic.desc) {
const descriptionEl = contentContainer.createEl('div', {
cls: 'cover-description',
text: this.selectedBook.basic.desc
});
descriptionEl.setAttribute('style', 'font-size: 14px; color: #f0f0f0; margin-top: 20px; max-width: 80%; text-align: center; font-style: italic;');
}
}
} else {
// 使用默认封面样式
const defaultSettings = this.coverManager.getDefaultCoverSettings();
const contentContainer = this.coverManager.applyCoverStyles(this.coverPreviewElement, defaultSettings);
// 添加标题和作者
if (contentContainer) {
const titleEl = contentContainer.createEl('div', {
cls: 'cover-title',
text: this.selectedBook.basic.title
});
titleEl.setAttribute('style', defaultSettings.titleStyle);
// 添加副标题
if (this.selectedBook.basic.subtitle) {
const subtitleEl = contentContainer.createEl('div', {
cls: 'cover-subtitle',
text: this.selectedBook.basic.subtitle
});
subtitleEl.setAttribute('style', 'font-size: 18px; color: #ffffff; margin-top: 10px; text-shadow: 0 1px 2px rgba(0,0,0,0.5);');
}
if (this.selectedBook.basic.author && this.selectedBook.basic.author.length > 0) {
const authorEl = contentContainer.createEl('div', {
cls: 'cover-author',
text: this.selectedBook.basic.author.join(', ')
});
authorEl.setAttribute('style', defaultSettings.authorStyle);
}
// 添加描述信息
if (this.selectedBook.basic.desc) {
const descriptionEl = contentContainer.createEl('div', {
cls: 'cover-description',
text: this.selectedBook.basic.desc
});
descriptionEl.setAttribute('style', 'font-size: 14px; color: #f0f0f0; margin-top: 20px; max-width: 80%; text-align: center; font-style: italic;');
}
}
}
}
// 移除视图
remove() {
this.container.remove();
}
}

View file

@ -52,4 +52,33 @@ export interface ToolbarModalTranslation {
PAYMENT_ALIPAY: string;
PAYMENT_KOFI: string;
CURRENCY_UNIT: string;
// TypographyModal
PREVIEW: string;
NO_BOOKS_AVAILABLE: string;
LOADING: string;
DEFAULT_FONT: string;
SERIF_FONT: string;
SANS_SERIF_FONT: string;
MONOSPACE_FONT: string;
APPLY: string;
TYPOGRAPHY_APPLIED: string;
EXPORT: string;
SELECT_BOOK_FIRST: string;
EXPORT_SUCCESS: string;
EXPORT_FAILED: string;
FORMAT: string;
CUSTOM_SIZE: string;
// CoverModal
DESIGN_COVER: string;
SHOW_COVER: string;
COVER_DESIGN: string;
COVER_IMAGE: string;
CLEAR_IMAGE: string;
BACKGROUND_COLOR: string;
OVERLAY: string;
OVERLAY_DESC: string;
OVERLAY_OPACITY: string;
SCALE: string;
}

View file

@ -373,7 +373,36 @@ const toolbarModalTranslation: ToolbarModalTranslation = {
PAYMENT_WECHAT: 'WeChat pay',
PAYMENT_ALIPAY: 'Alipay',
PAYMENT_KOFI: 'Ko-fi',
CURRENCY_UNIT: 'CNY'
CURRENCY_UNIT: 'CNY',
// TypographyModal
PREVIEW: 'Preview',
NO_BOOKS_AVAILABLE: 'No books available',
LOADING: 'Loading...',
DEFAULT_FONT: 'Default font',
SERIF_FONT: 'Serif font',
SANS_SERIF_FONT: 'Sans-serif font',
MONOSPACE_FONT: 'Monospace font',
APPLY: 'Apply',
TYPOGRAPHY_APPLIED: 'Typography applied',
EXPORT: 'Export',
SELECT_BOOK_FIRST: 'Please select a book first',
EXPORT_SUCCESS: 'Export successful',
EXPORT_FAILED: 'Export failed: ',
FORMAT: 'Format',
CUSTOM_SIZE: 'Custom size',
// CoverModal
DESIGN_COVER: 'Design cover',
SHOW_COVER: 'Show cover',
COVER_DESIGN: 'Cover design',
COVER_IMAGE: 'Cover image',
CLEAR_IMAGE: 'Clear image',
BACKGROUND_COLOR: 'Background color',
OVERLAY: 'Overlay',
OVERLAY_DESC: 'Add an overlay to the cover',
OVERLAY_OPACITY: 'Overlay opacity',
SCALE: 'Scale',
};
const componentTranslation: ComponentTranslation = {

View file

@ -372,7 +372,36 @@ const toolbarModalTranslation: ToolbarModalTranslation = {
PAYMENT_WECHAT: '微信赞赏',
PAYMENT_ALIPAY: '支付宝赞赏',
PAYMENT_KOFI: 'Ko-fi 赞赏',
CURRENCY_UNIT: '元'
CURRENCY_UNIT: '元',
// TypographyModal
PREVIEW: '预览',
NO_BOOKS_AVAILABLE: '没有可用的书籍',
LOADING: '加载中...',
DEFAULT_FONT: '默认字体',
SERIF_FONT: '衬线字体',
SANS_SERIF_FONT: '无衬线字体',
MONOSPACE_FONT: '等宽字体',
APPLY: '应用',
TYPOGRAPHY_APPLIED: '排版设置已应用',
EXPORT: '导出',
SELECT_BOOK_FIRST: '请先选择一本书籍',
EXPORT_SUCCESS: '导出成功',
EXPORT_FAILED: '导出失败: ',
FORMAT : '格式',
CUSTOM_SIZE: '自定义大小',
// CoverModal
DESIGN_COVER: '设计封面',
SHOW_COVER: '显示封面',
COVER_DESIGN: '封面设计',
COVER_IMAGE: '封面图片',
CLEAR_IMAGE: '清除图片',
BACKGROUND_COLOR: '背景颜色',
OVERLAY: '覆盖层',
OVERLAY_DESC: '添加半透明覆盖层以增强文字可读性',
OVERLAY_OPACITY: '覆盖层透明度',
SCALE: '缩放',
};
const componentTranslation: ComponentTranslation = {

View file

@ -8,11 +8,15 @@ import { BookManager } from './services/BookManager';
import { TemplateManager } from './services/TemplateManager';
import { BookStatsManager } from './services/BookStatsManager';
import { i18n } from './i18n/i18n';
import { ImgTemplateManager } from './services/ImgTemplateManager';
import { ThemeManager } from './services/ThemeManager';
export default class BookSmithPlugin extends Plugin {
settings: BookSmithSettings;
bookManager: BookManager;
templateManager: TemplateManager;
imgTemplateManager: ImgTemplateManager;
themeManager: ThemeManager;
statsManager: BookStatsManager;
async onload() {
@ -70,6 +74,10 @@ export default class BookSmithPlugin extends Plugin {
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
this.bookManager = new BookManager(this.app, this.settings);
this.templateManager = new TemplateManager(this.settings);
this.imgTemplateManager = new ImgTemplateManager(this.app, this.settings);
this.themeManager = new ThemeManager(this.app, this.settings);
}
async saveSettings() {

View file

@ -0,0 +1,240 @@
import { App, Modal, Setting } from 'obsidian';
import { CoverManager, CoverSettings } from '../services/CoverManager';
import { i18n } from '../i18n/i18n';
export class CoverSettingModal extends Modal {
private imageUrl: string = '';
private scale: number = 1;
private position: { x: number; y: number } = { x: 0, y: 0 };
private titleStyle: string = '';
private authorStyle: string = '';
private backgroundColor: string = '#1a1a1a';
private overlay: boolean = true;
private overlayOpacity: number = 0.4;
private initialSettings?: CoverSettings;
private previewElement: HTMLElement | null = null;
private dragEventCleanup: (() => void) | null = null;
private coverManager: CoverManager;
constructor(
app: App,
private onSubmit: (settings: CoverSettings) => void,
private targetPreviewEl: HTMLElement,
coverManager: CoverManager,
initialSettings?: CoverSettings,
private bookTitle?: string,
private bookAuthor?: string[]
) {
super(app);
this.coverManager = coverManager;
this.initialSettings = initialSettings;
if (initialSettings) {
this.imageUrl = initialSettings.imageUrl;
this.scale = initialSettings.scale;
this.position = { ...initialSettings.position };
this.titleStyle = initialSettings.titleStyle;
this.authorStyle = initialSettings.authorStyle;
this.backgroundColor = initialSettings.backgroundColor;
this.overlay = initialSettings.overlay;
this.overlayOpacity = initialSettings.overlayOpacity;
} else {
const defaultSettings = coverManager.getDefaultCoverSettings();
this.titleStyle = defaultSettings.titleStyle;
this.authorStyle = defaultSettings.authorStyle;
this.backgroundColor = defaultSettings.backgroundColor;
this.overlay = defaultSettings.overlay;
this.overlayOpacity = defaultSettings.overlayOpacity;
}
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.addClass('book-smith-cover-modal');
const container = contentEl.createEl('div', { cls: 'cover-settings-container' });
container.createEl('h3', { text: i18n.t('COVER_DESIGN') || '封面设计' });
// 预览区域
const previewArea = container.createEl('div', { cls: 'cover-preview-area' });
this.previewElement = previewArea.createEl('div', { cls: 'cover-preview' });
// 控制区域
const controlsArea = container.createEl('div', { cls: 'cover-controls' });
this.createControls(controlsArea);
// 应用初始设置到预览
this.updatePreview();
// 底部按钮
const buttonContainer = contentEl.createEl('div', { cls: 'cover-button-container' });
const cancelButton = buttonContainer.createEl('button', {
text: i18n.t('CANCEL') || '取消',
cls: 'cover-cancel-button'
});
cancelButton.addEventListener('click', () => this.close());
const applyButton = buttonContainer.createEl('button', {
text: i18n.t('APPLY') || '应用',
cls: 'cover-apply-button'
});
applyButton.addEventListener('click', () => {
this.onSubmit({
imageUrl: this.imageUrl,
scale: this.scale,
position: this.position,
titleStyle: this.titleStyle,
authorStyle: this.authorStyle,
backgroundColor: this.backgroundColor,
overlay: this.overlay,
overlayOpacity: this.overlayOpacity
});
this.close();
});
}
private createControls(container: HTMLElement) {
// 图片上传和清除按钮
new Setting(container)
.setName(i18n.t('COVER_IMAGE') || '封面图片')
.addButton(button => button
.setButtonText(i18n.t('SELECT_IMAGE') || '选择图片')
.onClick(() => this.handleImageUpload()))
.addButton(button => button
.setButtonText(i18n.t('CLEAR_IMAGE') || '清除图片')
.onClick(() => this.handleClearImage()));
// 背景颜色选择器
new Setting(container)
.setName(i18n.t('BACKGROUND_COLOR') || '背景颜色')
.addColorPicker(color => color
.setValue(this.backgroundColor)
.onChange(value => {
this.backgroundColor = value;
this.updatePreview();
}));
// 覆盖层开关
new Setting(container)
.setName(i18n.t('OVERLAY') || '覆盖层')
.setDesc(i18n.t('OVERLAY_DESC') || '添加半透明覆盖层以增强文字可读性')
.addToggle(toggle => toggle
.setValue(this.overlay)
.onChange(value => {
this.overlay = value;
this.updatePreview();
}));
// 覆盖层透明度
if (this.overlay) {
new Setting(container)
.setName(i18n.t('OVERLAY_OPACITY') || '覆盖层透明度')
.addSlider(slider => slider
.setLimits(0, 1, 0.1)
.setValue(this.overlayOpacity)
.onChange(value => {
this.overlayOpacity = value;
this.updatePreview();
}));
}
// 缩放控制
if (this.imageUrl) {
new Setting(container)
.setName(i18n.t('SCALE') || '缩放')
.addSlider(slider => slider
.setLimits(0.1, 2, 0.1)
.setValue(this.scale)
.onChange(value => {
this.scale = value;
this.updatePreview();
}));
}
}
private handleImageUpload() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.addEventListener('change', (e: Event) => {
const target = e.target as HTMLInputElement;
if (target.files && target.files[0]) {
const reader = new FileReader();
reader.onload = (e) => {
this.imageUrl = e.target?.result as string;
this.updatePreview();
// 重新创建控制区域以显示缩放控制
const controlsArea = this.contentEl.querySelector('.cover-controls');
if (controlsArea) {
controlsArea.empty();
this.createControls(controlsArea as HTMLElement);
}
};
reader.readAsDataURL(target.files[0]);
}
});
input.click();
}
private handleClearImage() {
this.imageUrl = '';
this.scale = 1;
this.position = { x: 0, y: 0 };
this.updatePreview();
// 重新创建控制区域以隐藏缩放控制
const controlsArea = this.contentEl.querySelector('.cover-controls');
if (controlsArea) {
controlsArea.empty();
this.createControls(controlsArea as HTMLElement);
}
}
private updatePreview() {
if (!this.previewElement) return;
// 清除现有样式
this.coverManager.clearCoverStyles(this.previewElement);
// 应用新样式
const contentContainer = this.coverManager.applyCoverStyles(this.previewElement, {
imageUrl: this.imageUrl,
scale: this.scale,
position: this.position,
titleStyle: this.titleStyle,
authorStyle: this.authorStyle,
backgroundColor: this.backgroundColor,
overlay: this.overlay,
overlayOpacity: this.overlayOpacity
});
// 添加标题和作者
if (contentContainer) {
if (this.bookTitle) {
const titleEl = contentContainer.createEl('div', { cls: 'cover-title', text: this.bookTitle });
titleEl.setAttribute('style', this.titleStyle);
}
if (this.bookAuthor && this.bookAuthor.length > 0) {
const authorEl = contentContainer.createEl('div', {
cls: 'cover-author',
text: this.bookAuthor.join(', ')
});
authorEl.setAttribute('style', this.authorStyle);
}
}
}
onClose() {
if (this.dragEventCleanup) {
this.dragEventCleanup();
this.dragEventCleanup = null;
}
this.contentEl.empty();
}
}

View file

@ -0,0 +1,95 @@
import { App } from 'obsidian';
export interface CoverSettings {
imageUrl: string;
scale: number;
position: { x: number; y: number };
titleStyle: string;
authorStyle: string;
backgroundColor: string;
overlay: boolean;
overlayOpacity: number;
}
export class CoverManager {
constructor(
private app: App
) {}
public applyCoverStyles(element: HTMLElement, settings: CoverSettings) {
// 设置背景图片
if (settings.imageUrl) {
const stylesArray = [
`background-image: url(${settings.imageUrl})`,
`background-size: ${settings.scale * 100}%`,
`background-position: ${settings.position.x}px ${settings.position.y}px`,
`background-repeat: no-repeat`
];
stylesArray.forEach(style => {
const match = style.match(/([^:]+):(.+)/);
if (match) {
const [, key, value] = match.map(item => item.trim());
if (key && value) {
element.style[key as any] = value;
}
}
});
} else {
// 如果没有背景图片,使用背景颜色
element.style.backgroundColor = settings.backgroundColor || '#ffffff';
}
// 添加覆盖层以增强文字可读性
if (settings.overlay) {
const overlay = element.createDiv({ cls: 'cover-overlay' });
overlay.style.backgroundColor = `rgba(0, 0, 0, ${settings.overlayOpacity || 0.3})`;
overlay.style.position = 'absolute';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.right = '0';
overlay.style.bottom = '0';
overlay.style.zIndex = '1';
}
// 创建内容容器,确保在覆盖层之上
const contentContainer = element.createDiv({ cls: 'cover-content' });
contentContainer.style.position = 'relative';
contentContainer.style.zIndex = '2';
contentContainer.style.height = '100%';
contentContainer.style.display = 'flex';
contentContainer.style.flexDirection = 'column';
contentContainer.style.justifyContent = 'center';
contentContainer.style.alignItems = 'center';
contentContainer.style.padding = '40px';
contentContainer.style.textAlign = 'center';
return contentContainer;
}
public clearCoverStyles(element: HTMLElement) {
const style = element.getAttribute('style') || '';
const clearedStyle = style.replace(/background-image:[^;]+;|background-size:[^;]+;|background-position:[^;]+;|background-repeat:[^;]+;|background-color:[^;]+;/g, '');
element.setAttribute('style', clearedStyle);
// 移除覆盖层和内容容器
const overlay = element.querySelector('.cover-overlay');
const contentContainer = element.querySelector('.cover-content');
if (overlay) overlay.remove();
if (contentContainer) contentContainer.remove();
}
public getDefaultCoverSettings(): CoverSettings {
return {
imageUrl: '',
scale: 1,
position: { x: 0, y: 0 },
titleStyle: 'font-size: 32px; font-weight: bold; color: #ffffff; margin-bottom: 20px; text-shadow: 0 2px 4px rgba(0,0,0,0.5);',
authorStyle: 'font-size: 18px; color: #ffffff; font-style: italic; text-shadow: 0 1px 2px rgba(0,0,0,0.5);',
backgroundColor: '#1a1a1a',
overlay: true,
overlayOpacity: 0.4
};
}
}

View file

@ -0,0 +1,145 @@
import { App } from 'obsidian';
import { ThemeManager } from './ThemeManager';
export interface ImgTemplate {
id: string;
name: string;
sections: {
cover?: boolean;
header?: boolean;
content: true;
footer?: boolean;
};
render: (element: HTMLElement, settings?: any) => void;
}
export class ImgTemplateManager {
private templates: ImgTemplate[] = [];
private currentTemplate: ImgTemplate | null = null;
constructor(
private app: App,
private themeManager?: ThemeManager
) {
this.initializeTemplates();
}
private initializeTemplates() {
// 注册默认模板
this.registerTemplate(new DefaultTemplate());
// 注册备忘录模板
this.registerTemplate(new NotesTemplate());
// 注册书籍模板
this.registerTemplate(new BookTemplate());
// 注册杂志模板
this.registerTemplate(new MagazineTemplate());
}
registerTemplate(template: ImgTemplate) {
this.templates.push(template);
}
getVisibleTemplates() {
return this.templates;
}
getTemplate(id: string): ImgTemplate | null {
return this.templates.find(t => t.id === id) || null;
}
setCurrentTemplate(id: string) {
const template = this.templates.find(t => t.id === id);
if (template) {
this.currentTemplate = template;
}
}
applyTemplate(element: HTMLElement, settings?: any) {
if (!this.currentTemplate) {
this.currentTemplate = this.templates[0];
}
if (this.currentTemplate) {
this.currentTemplate.render(element, settings);
if (this.themeManager) {
this.themeManager.applyTheme(element);
}
}
}
}
// 默认模板实现
class DefaultTemplate implements ImgTemplate {
id = 'default';
name = '默认模板';
sections = {
cover: true,
header: true,
content: true as const,
footer: true
};
constructor() {}
render(element: HTMLElement, settings?: any) {
// 实现默认模板的渲染逻辑
}
}
// 备忘录模板实现
class NotesTemplate implements ImgTemplate {
id = 'notes';
name = '备忘录';
sections = {
cover: false,
header: true,
content: true as const,
footer: false
};
constructor() {}
render(element: HTMLElement, settings?: any) {
// 实现备忘录模板的渲染逻辑
}
}
// 书籍模板实现
class BookTemplate implements ImgTemplate {
id = 'book';
name = '书籍';
sections = {
cover: true,
header: false,
content: true as const,
footer: true
};
constructor() {}
render(element: HTMLElement, settings?: any) {
// 实现书籍模板的渲染逻辑
}
}
// 杂志模板实现
class MagazineTemplate implements ImgTemplate {
id = 'magazine';
name = '杂志';
sections = {
cover: true,
header: true,
content: true as const,
footer: true
};
constructor() {}
render(element: HTMLElement, settings?: any) {
// 实现杂志模板的渲染逻辑
}
}

View file

@ -0,0 +1,322 @@
import { App } from 'obsidian';
import { BookSmithSettings } from '../settings/settings';
export interface Theme {
id: string;
name: string;
description: string;
isBuiltin?: boolean;
isVisible?: boolean;
styles: {
// 容器基础样式
container: string;
// 标题样式
title: {
h1: string;
h2: string;
h3: string;
};
// 段落样式
paragraph: string;
// 引用样式
quote: string;
// 列表样式
list: {
ul: string;
ol: string;
li: string;
};
// 强调样式
emphasis: {
strong: string;
em: string;
del: string;
};
// 代码样式
code: {
block: string;
inline: string;
};
// 表格样式
table: {
container: string;
header: string;
cell: string;
};
// 其他元素样式
hr: string;
image: string;
link: string;
};
}
export class ThemeManager {
private themes: Map<string, Theme> = new Map();
private currentThemeId: string = 'default';
private currentFontSize: number = 16;
constructor(
private app: App,
private settings: BookSmithSettings
) {
// 注册默认主题
this.registerTheme(this.createDefaultTheme());
this.registerTheme(this.createLightTheme());
this.registerTheme(this.createDarkTheme());
}
private createDefaultTheme(): Theme {
return {
id: 'default',
name: '默认主题',
description: '系统默认主题',
isBuiltin: true,
isVisible: true,
styles: {
container: 'background-color: white; color: #333; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;',
title: {
h1: 'font-size: 2em; margin-bottom: 0.5em; color: #222;',
h2: 'font-size: 1.5em; margin-bottom: 0.5em; color: #333;',
h3: 'font-size: 1.2em; margin-bottom: 0.5em; color: #444;'
},
paragraph: 'margin-bottom: 1em; line-height: 1.6;',
quote: 'border-left: 4px solid #ddd; padding-left: 1em; color: #666; font-style: italic;',
list: {
ul: 'list-style-type: disc; padding-left: 2em; margin-bottom: 1em;',
ol: 'list-style-type: decimal; padding-left: 2em; margin-bottom: 1em;',
li: 'margin-bottom: 0.5em;'
},
emphasis: {
strong: 'font-weight: bold;',
em: 'font-style: italic;',
del: 'text-decoration: line-through;'
},
code: {
block: 'background-color: #f5f5f5; padding: 1em; border-radius: 4px; font-family: monospace; overflow-x: auto;',
inline: 'background-color: #f5f5f5; padding: 0.2em 0.4em; border-radius: 3px; font-family: monospace;'
},
table: {
container: 'width: 100%; border-collapse: collapse; margin-bottom: 1em;',
header: 'background-color: #f5f5f5; font-weight: bold; text-align: left; padding: 0.5em;',
cell: 'border: 1px solid #ddd; padding: 0.5em;'
},
hr: 'border: 0; border-top: 1px solid #ddd; margin: 1em 0;',
image: 'max-width: 100%; height: auto;',
link: 'color: #0366d6; text-decoration: none;'
}
};
}
private createLightTheme(): Theme {
return {
id: 'light',
name: '浅色主题',
description: '明亮清新的浅色主题',
isBuiltin: true,
isVisible: true,
styles: {
container: 'background-color: #f9f9f9; color: #333; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;',
title: {
h1: 'font-size: 2em; margin-bottom: 0.5em; color: #222; border-bottom: 1px solid #eee; padding-bottom: 0.3em;',
h2: 'font-size: 1.5em; margin-bottom: 0.5em; color: #333; border-bottom: 1px solid #eee; padding-bottom: 0.2em;',
h3: 'font-size: 1.2em; margin-bottom: 0.5em; color: #444;'
},
paragraph: 'margin-bottom: 1em; line-height: 1.6; color: #444;',
quote: 'border-left: 4px solid #ddd; padding-left: 1em; color: #666; font-style: italic; background-color: #f5f5f5;',
list: {
ul: 'list-style-type: disc; padding-left: 2em; margin-bottom: 1em;',
ol: 'list-style-type: decimal; padding-left: 2em; margin-bottom: 1em;',
li: 'margin-bottom: 0.5em;'
},
emphasis: {
strong: 'font-weight: bold; color: #222;',
em: 'font-style: italic; color: #444;',
del: 'text-decoration: line-through; color: #999;'
},
code: {
block: 'background-color: #f0f0f0; padding: 1em; border-radius: 4px; font-family: monospace; overflow-x: auto; border: 1px solid #ddd;',
inline: 'background-color: #f0f0f0; padding: 0.2em 0.4em; border-radius: 3px; font-family: monospace; border: 1px solid #ddd;'
},
table: {
container: 'width: 100%; border-collapse: collapse; margin-bottom: 1em;',
header: 'background-color: #f0f0f0; font-weight: bold; text-align: left; padding: 0.5em; border: 1px solid #ddd;',
cell: 'border: 1px solid #ddd; padding: 0.5em;'
},
hr: 'border: 0; border-top: 1px solid #ddd; margin: 1em 0;',
image: 'max-width: 100%; height: auto; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);',
link: 'color: #0366d6; text-decoration: none;'
}
};
}
private createDarkTheme(): Theme {
return {
id: 'dark',
name: '深色主题',
description: '护眼舒适的深色主题',
isBuiltin: true,
isVisible: true,
styles: {
container: 'background-color: #222; color: #eee; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;',
title: {
h1: 'font-size: 2em; margin-bottom: 0.5em; color: #fff; border-bottom: 1px solid #444; padding-bottom: 0.3em;',
h2: 'font-size: 1.5em; margin-bottom: 0.5em; color: #ddd; border-bottom: 1px solid #444; padding-bottom: 0.2em;',
h3: 'font-size: 1.2em; margin-bottom: 0.5em; color: #ccc;'
},
paragraph: 'margin-bottom: 1em; line-height: 1.6; color: #bbb;',
quote: 'border-left: 4px solid #444; padding-left: 1em; color: #999; font-style: italic; background-color: #333;',
list: {
ul: 'list-style-type: disc; padding-left: 2em; margin-bottom: 1em;',
ol: 'list-style-type: decimal; padding-left: 2em; margin-bottom: 1em;',
li: 'margin-bottom: 0.5em;'
},
emphasis: {
strong: 'font-weight: bold; color: #fff;',
em: 'font-style: italic; color: #ddd;',
del: 'text-decoration: line-through; color: #666;'
},
code: {
block: 'background-color: #333; padding: 1em; border-radius: 4px; font-family: monospace; overflow-x: auto; border: 1px solid #444;',
inline: 'background-color: #333; padding: 0.2em 0.4em; border-radius: 3px; font-family: monospace; border: 1px solid #444;'
},
table: {
container: 'width: 100%; border-collapse: collapse; margin-bottom: 1em;',
header: 'background-color: #333; font-weight: bold; text-align: left; padding: 0.5em; border: 1px solid #444;',
cell: 'border: 1px solid #444; padding: 0.5em;'
},
hr: 'border: 0; border-top: 1px solid #444; margin: 1em 0;',
image: 'max-width: 100%; height: auto; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.3);',
link: 'color: #58a6ff; text-decoration: none;'
}
};
}
registerTheme(theme: Theme): void {
this.themes.set(theme.id, theme);
}
getTheme(id: string): Theme | undefined {
return this.themes.get(id);
}
getAllThemes(): Theme[] {
return Array.from(this.themes.values());
}
getVisibleThemes(): Theme[] {
return this.getAllThemes().filter(theme => theme.isVisible !== false);
}
getCurrentTheme(): Theme {
return this.themes.get(this.currentThemeId) || this.themes.get('default')!;
}
setCurrentTheme(id: string): boolean {
if (this.themes.has(id)) {
this.currentThemeId = id;
return true;
}
return false;
}
setFontSize(size: number): void {
this.currentFontSize = size;
}
getFontSize(): number {
return this.currentFontSize;
}
// 应用主题到元素
applyTheme(element: HTMLElement, themeId?: string): void {
const theme = themeId ? this.getTheme(themeId) : this.getCurrentTheme();
if (!theme) return;
// 应用容器样式
element.setAttribute('style', theme.styles.container);
// 应用标题样式
element.querySelectorAll('h1').forEach(el => {
el.setAttribute('style', theme.styles.title.h1);
});
element.querySelectorAll('h2').forEach(el => {
el.setAttribute('style', theme.styles.title.h2);
});
element.querySelectorAll('h3').forEach(el => {
el.setAttribute('style', theme.styles.title.h3);
});
// 应用段落样式
element.querySelectorAll('p').forEach(el => {
el.setAttribute('style', theme.styles.paragraph);
});
// 应用引用样式
element.querySelectorAll('blockquote').forEach(el => {
el.setAttribute('style', theme.styles.quote);
});
// 应用列表样式
element.querySelectorAll('ul').forEach(el => {
el.setAttribute('style', theme.styles.list.ul);
});
element.querySelectorAll('ol').forEach(el => {
el.setAttribute('style', theme.styles.list.ol);
});
element.querySelectorAll('li').forEach(el => {
el.setAttribute('style', theme.styles.list.li);
});
// 应用强调样式
element.querySelectorAll('strong').forEach(el => {
el.setAttribute('style', theme.styles.emphasis.strong);
});
element.querySelectorAll('em').forEach(el => {
el.setAttribute('style', theme.styles.emphasis.em);
});
element.querySelectorAll('del').forEach(el => {
el.setAttribute('style', theme.styles.emphasis.del);
});
// 应用代码样式
element.querySelectorAll('pre code').forEach(el => {
el.setAttribute('style', theme.styles.code.block);
});
element.querySelectorAll('code:not(pre code)').forEach(el => {
el.setAttribute('style', theme.styles.code.inline);
});
// 应用表格样式
element.querySelectorAll('table').forEach(el => {
el.setAttribute('style', theme.styles.table.container);
});
element.querySelectorAll('th').forEach(el => {
el.setAttribute('style', theme.styles.table.header);
});
element.querySelectorAll('td').forEach(el => {
el.setAttribute('style', theme.styles.table.cell);
});
// 应用其他元素样式
element.querySelectorAll('hr').forEach(el => {
el.setAttribute('style', theme.styles.hr);
});
element.querySelectorAll('img').forEach(el => {
el.setAttribute('style', theme.styles.image);
});
element.querySelectorAll('a').forEach(el => {
el.setAttribute('style', theme.styles.link);
});
}
}

View file

@ -0,0 +1,122 @@
/* 封面预览样式 */
.typography-cover-preview {
width: 100%;
height: 400px;
margin-bottom: 30px;
position: relative;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
/* 封面内容样式 */
.cover-content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
padding: 40px;
text-align: center;
}
/* 封面标题默认样式 */
.cover-title {
font-size: 32px;
font-weight: bold;
color: #ffffff;
margin-bottom: 20px;
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
}
/* 封面作者默认样式 */
.cover-author {
font-size: 18px;
color: #ffffff;
font-style: italic;
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
}
/* 封面设计模态框样式 */
.book-smith-cover-modal {
max-width: 90vw;
}
.cover-settings-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.cover-preview-area {
width: 100%;
height: 300px;
margin-bottom: 20px;
}
.cover-preview {
width: 100%;
height: 100%;
border-radius: 8px;
overflow: hidden;
position: relative;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.cover-controls {
display: flex;
flex-direction: column;
gap: 10px;
}
.cover-button-container {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
.cover-cancel-button,
.cover-apply-button {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.cover-cancel-button {
background-color: #f5f5f5;
border: 1px solid #ddd;
}
.cover-apply-button {
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border: none;
}
/* 封面开关样式 */
.cover-toggle-container {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.cover-toggle-label {
font-size: 14px;
}
.cover-toggle-input {
margin: 0;
}
.cover-design-btn {
padding: 4px 8px;
border-radius: 4px;
background-color: var(--interactive-accent);
color: var(--text-on-accent);
font-size: 12px;
cursor: pointer;
border: none;
margin-left: 10px;
}

View file

@ -0,0 +1,108 @@
/* 电子书视图基础样式 */
.book-smith-ebook-view {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
/* 头部样式 */
.ebook-header {
display: flex;
align-items: center;
padding: 12px 16px;
background-color: var(--background-secondary);
border-bottom: 1px solid var(--background-modifier-border);
}
.ebook-header-icon {
margin-right: 8px;
color: var(--text-accent);
}
.ebook-title {
flex: 1;
font-size: 16px;
font-weight: 600;
}
.ebook-exit-btn {
cursor: pointer;
color: var(--text-muted);
padding: 4px;
border-radius: 4px;
}
.ebook-exit-btn:hover {
color: var(--text-normal);
background-color: var(--background-modifier-hover);
}
/* 内容区域样式 */
.ebook-content {
display: flex;
flex-direction: column;
flex: 1;
overflow-y: auto;
padding: 20px;
}
/* 表单样式 */
.ebook-form {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 20px;
max-width: 500px;
}
.form-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.field-label {
font-weight: 500;
color: var(--text-normal);
}
/* 选择器样式 */
.ebook-form select {
width: 100%;
padding: 8px 12px;
border-radius: 6px;
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border);
color: var(--text-normal);
font-size: 14px;
}
.ebook-form select:focus {
border-color: var(--interactive-accent);
box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2);
}
/* 生成按钮样式 */
.generate-btn {
padding: 10px 20px;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
text-align: center;
transition: all 0.2s ease;
background-color: var(--interactive-accent);
color: var(--text-on-accent);
max-width: 200px;
margin-top: 10px;
}
.generate-btn:hover {
background-color: var(--interactive-accent-hover);
}
/* 选中高亮效果 */
.selected-option {
background-color: rgba(var(--interactive-accent-rgb), 0.2) !important;
transition: background-color 0.5s ease;
}

View file

@ -0,0 +1,158 @@
/* 分页容器样式 */
.typography-content-pages {
display: flex;
flex-direction: column;
gap: 30px;
margin-top: 30px;
}
/* 页面样式 */
.book-page {
position: relative;
background-color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
page-break-after: always;
padding: 20px 25px;
border: 1px solid #ddd;
}
/* 不同开本大小的页面样式 */
.book-size-a4 {
width: 210mm;
min-height: 297mm;
max-height: 297mm;
padding-bottom:10px
}
.book-size-a5 {
width: 148mm;
min-height: 210mm;
max-height: 210mm;
}
.book-size-b5 {
width: 176mm;
min-height: 250mm;
max-height: 250mm;
}
.book-size-16k {
width: 184mm;
min-height: 260mm;
max-height: 260mm;
padding: 18mm;
}
.book-size-custom {
width: 180mm;
min-height: 240mm;
max-height: 240mm;
padding: 15mm;
}
/* 目录样式 */
.typography-toc {
margin-bottom: 30px;
padding: 15px;
border: 1px solid #ddd;
background-color: #f9f9f9;
}
.typography-toc-pages {
display: flex;
flex-direction: column;
gap: 30px;
margin-bottom: 30px;
}
.toc-page {
position: relative;
}
.toc-list {
list-style-type: none;
padding-left: 0;
}
.toc-list li {
margin: 5px 0;
line-height: 1.5;
}
.toc-item-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
border-bottom: 1px dotted #ccc;
}
.toc-page-ref {
margin-left: 10px;
color: #666;
}
.toc-list a {
text-decoration: none;
color: #333;
flex-grow: 1;
}
.toc-list a:hover {
text-decoration: underline;
}
/* 目录级别样式 */
.toc-level-1 { font-weight: bold; font-size: 1.2em; }
.toc-level-2 { padding-left: 20px; font-weight: bold; }
.toc-level-3 { padding-left: 20px; }
.toc-level-4 { padding-left: 40px; }
.toc-level-5 { padding-left: 60px; }
.toc-level-6 { padding-left: 80px; }
/* 页面信息样式 */
.page-info {
text-align: center;
margin-top: 20px;
color: #666;
font-size: 14px;
}
/* 封面和内容容器 */
.typography-preview-container {
display: flex;
flex-direction: column;
gap: 30px;
width: 100%;
}
.typography-cover-container,
.typography-content-container {
width: 100%;
}
/* 页码样式 */
.page-number {
position: absolute;
bottom: 10px;
left: 0;
right: 0;
text-align: center;
font-size: 12px;
color: #666;
}
/* 页码标记样式 */
.page-marker {
position: absolute;
bottom: 30px;
left: 0;
right: 0;
text-align: center;
font-size: 12px;
color: #999;
border-top: 1px solid #eee;
padding-top: 5px;
margin-top: 10px;
}

View file

@ -0,0 +1,612 @@
/* 排版视图基础样式 */
.book-smith-typography-view {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
background-color: var(--background-primary);
}
/* 头部样式 */
.typography-header {
display: flex;
align-items: center;
padding: 12px 16px;
background-color: var(--background-secondary);
border-bottom: 1px solid var(--background-modifier-border);
}
.typography-header-icon {
margin-right: 8px;
color: var(--text-accent);
}
.typography-title {
flex: 1;
font-size: 16px;
font-weight: 600;
}
.typography-exit-btn {
cursor: pointer;
color: var(--text-muted);
padding: 4px;
border-radius: 4px;
}
.typography-exit-btn:hover {
color: var(--text-normal);
background-color: var(--background-modifier-hover);
}
/* 内容区域样式 */
.typography-content {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
/* 顶部选择器面板样式 */
.typography-top-panel {
padding: 16px;
border-bottom: 1px solid var(--background-modifier-border);
background-color: var(--background-secondary-alt);
}
/* 选择器容器样式 */
.typography-selectors {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
/* 表单字段样式 */
.form-field {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 150px;
}
.field-label {
font-weight: 500;
color: var(--text-normal);
font-size: 12px;
}
/* 选择器样式 */
.typography-selectors select {
padding: 8px 12px;
border-radius: 6px;
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border);
color: var(--text-normal);
font-size: 14px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
padding-right: 30px;
}
.typography-selectors select:focus {
border-color: var(--interactive-accent);
outline: none;
box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2);
}
.typography-selectors select:hover {
border-color: var(--interactive-hover);
}
/* 选择器容器样式 */
.typography-selectors {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
justify-content: center;
}
/* 表单字段样式 - 移除垂直布局 */
.form-field {
display: inline-block;
min-width: 150px;
}
.field-label {
font-weight: 500;
color: var(--text-normal);
font-size: 12px;
}
/* 预览面板样式 */
.typography-preview-panel {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
}
/* 预览标题 */
.typography-preview-panel h2 {
margin-top: 0;
margin-bottom: 16px;
font-size: 18px;
color: var(--text-normal);
text-align: center;
}
/* 预览内容区域 */
.typography-preview {
flex: 1;
padding: 20px;
background-color: white;
color: black;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
overflow-y: auto;
margin: 0 auto;
max-width: 800px;
}
/* 底部按钮面板样式 */
.typography-bottom-panel {
padding: 16px;
border-top: 1px solid var(--background-modifier-border);
background-color: var(--background-secondary-alt);
}
/* 按钮样式 */
.typography-buttons {
display: flex;
gap: 12px;
justify-content: center;
}
.apply-btn, .export-btn {
padding: 10px 16px;
border-radius: 6px;
text-align: center;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
min-width: 120px;
}
.apply-btn {
background-color: var(--interactive-accent);
color: var(--text-on-accent);
}
.apply-btn:hover {
background-color: var(--interactive-accent-hover);
}
.export-btn {
background-color: var(--background-modifier-border);
color: var(--text-normal);
}
.export-btn:hover {
background-color: var(--background-modifier-hover);
}
/* 选中效果 */
.selected-option {
animation: highlight 0.5s ease-in-out;
}
@keyframes highlight {
0% { background-color: var(--background-primary); }
50% { background-color: var(--interactive-accent-hover); }
100% { background-color: var(--background-primary); }
}
/* 保留原有的字体、字号、行高和页边距样式 */
.font-default {
font-family: var(--default-font);
}
.font-serif {
font-family: Georgia, Times, 'Times New Roman', serif;
}
.font-sans-serif {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
.font-monospace {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
}
/* 字号预览样式 */
.font-size-small {
font-size: 14px;
}
.font-size-medium {
font-size: 16px;
}
.font-size-large {
font-size: 18px;
}
/* 行高预览样式 */
.line-height-compact {
line-height: 1.3;
}
.line-height-normal {
line-height: 1.6;
}
.line-height-relaxed {
line-height: 1.8;
}
/* 页边距预览样式 */
.margin-narrow {
padding: 10px;
}
.margin-normal {
padding: 20px;
}
.margin-wide {
padding: 30px;
}
/* 添加模板相关样式 */
/* 备忘录模板样式 */
.notes-template {
background-color: #f9f9f9;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.notes-header {
border-bottom: 1px solid #eee;
padding-bottom: 10px;
margin-bottom: 15px;
}
.notes-title {
font-size: 18px;
font-weight: 600;
color: #333;
text-align: center;
}
.notes-content {
line-height: 1.6;
}
/* 简洁模板样式 */
.simple-template {
background-color: white;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.simple-content {
max-width: 650px;
margin: 0 auto;
line-height: 1.6;
}
/* 文章模板样式 */
.article-template {
background-color: white;
padding: 30px;
font-family: Georgia, Times, "Times New Roman", serif;
}
.article-header {
text-align: center;
margin-bottom: 30px;
}
.article-meta {
color: #666;
font-size: 14px;
margin-top: 5px;
}
.article-abstract {
background-color: #f5f5f5;
padding: 15px;
border-left: 4px solid #ddd;
margin-bottom: 20px;
}
.article-content {
line-height: 1.8;
}
.article-footer {
margin-top: 30px;
padding-top: 15px;
border-top: 1px solid #eee;
font-size: 14px;
color: #666;
}
.article-references {
margin-bottom: 10px;
}
.article-copyright {
font-style: italic;
}
/* 选择器高亮效果 */
.selected-option {
background-color: rgba(14, 101, 235, 0.1) !important;
transition: background-color 0.5s ease;
}
/* ===== 下拉选择器 ===== */
.red-select-container {
position: relative;
max-width: 200px;
flex: 1;
}
.red-select {
height: 36px;
padding: 0 12px;
border: 1px solid var(--background-modifier-border);
border-radius: 8px;
background: var(--background-primary);
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
color: var(--text-normal);
box-shadow: 0 1px 3px var(--background-modifier-box-shadow);
white-space: nowrap;
overflow: hidden;
}
.red-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
}
.red-select:hover {
background: var(--background-modifier-hover);
box-shadow: 0 2px 6px var(--background-modifier-box-shadow);
}
.red-select.disabled {
opacity: 0.5;
background: var(--background-secondary) !important;
color: var(--text-muted) !important;
border-color: var(--background-modifier-border) !important;
cursor: not-allowed;
box-shadow: none;
}
.red-select-arrow {
color: var(--text-normal);
font-size: 12px;
transition: transform 0.2s ease;
margin-left: 4px;
flex-shrink: 0;
}
.red-select-dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
width: 100%;
background: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 8px;
box-shadow: 0 4px 12px var(--background-modifier-box-shadow);
display: none;
z-index: 1000;
overflow: hidden;
}
.red-select-dropdown.red-show {
display: block;
}
.red-select-item {
padding: 8px 12px;
cursor: pointer;
transition: all 0.2s ease;
color: var(--text-normal);
}
.red-select-item:hover {
background: var(--background-modifier-hover);
}
.red-select-item.red-selected {
background: var(--background-modifier-hover);
color: var(--text-accent);
}
/* ===== 字号调整组件 ===== */
.red-font-size-group {
height: 36px;
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
background: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 8px;
padding: 0;
overflow: hidden;
box-shadow: 0 1px 3px var(--background-modifier-box-shadow);
}
.red-font-size-input {
width: 30px;
text-align: center;
}
.red-font-size-btn {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
cursor: pointer;
color: var(--text-normal);
font-size: 16px;
transition: background 0.2s ease;
}
.red-font-size-btn:hover {
background: var(--background-modifier-hover);
}
/* 开本大小样式 */
.book-size-a4 {
aspect-ratio: 1 / 1.414; /* A4 比例 */
max-width: 595px; /* A4 宽度 */
}
.book-size-a5 {
aspect-ratio: 1 / 1.414; /* A5 比例 */
max-width: 420px; /* A5 宽度 */
}
.book-size-b5 {
aspect-ratio: 1 / 1.414; /* B5 比例 */
max-width: 498px; /* B5 宽度 */
}
.book-size-16k {
aspect-ratio: 1 / 1.5; /* 16K 比例 */
max-width: 460px; /* 16K 宽度 */
}
.book-size-custom {
max-width: 800px;
}
/* 分页容器 */
.typography-paged-content {
column-count: 2;
column-gap: 40px;
column-fill: auto;
height: 100%;
overflow: hidden;
}
/* 页面样式 */
.typography-page {
break-inside: avoid;
page-break-inside: avoid;
margin-bottom: 20px;
padding: 20px;
border: 1px solid #ddd;
background-color: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* 章节标题强制分页 */
h1, h2 {
break-before: page;
page-break-before: always;
}
/* 目录样式 */
.typography-toc {
margin-bottom: 30px;
}
.typography-toc-item {
margin: 5px 0;
}
.typography-toc-level-1 {
font-weight: bold;
margin-left: 0;
}
.typography-toc-level-2 {
margin-left: 20px;
}
.typography-toc-level-3 {
margin-left: 40px;
}
/* 添加到 styles/views/typography.css 或直接在 TypographyView 中添加样式 */
.typography-content-pages {
display: flex;
flex-direction: column;
gap: 20px;
}
.book-page {
border: 1px solid #ddd;
padding: 20px;
background-color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
min-height: 200px;
max-height: 800px;
overflow: hidden;
page-break-after: always;
position: relative;
}
/* 目录样式 */
.typography-toc {
margin-bottom: 30px;
padding: 15px;
border: 1px solid #ddd;
background-color: #f9f9f9;
}
.toc-list {
list-style-type: none;
padding-left: 0;
}
.toc-list ul {
padding-left: 20px;
list-style-type: none;
}
.toc-list li {
margin: 5px 0;
}
.toc-list a {
text-decoration: none;
color: #333;
}
.toc-list a:hover {
text-decoration: underline;
}

View file

@ -1,4 +1,9 @@
/* 导入全屏模态框样式 */
@import './variables.css';
@import './components/typography-view.css';
@import './components/paginated-engine.css';
@import './components/ebook-view.css';
@import './components/cover-design.css';
@import './views/book-view.css';
@import './components/chapter-tree.css';
@import './modals/create-book-modal.css';
@ -11,7 +16,6 @@
@import './modals/base-modal.css';
@import './modals/community-modal.css';
@import './modals/donate-modal.css';
@import './modals/ebook-modal.css';
@import './modals/inspiration-modal.css';
@import './modals/contact-modal.css';
@import './settings/settings-tab.css';

View file

@ -1,90 +0,0 @@
/* 电子书内容区域 */
.ebook-content {
display: flex;
flex-direction: column;
gap: 20px;
}
/* 表单样式 */
.ebook-form {
display: flex;
flex-direction: column;
gap: 15px;
}
.form-field {
display: flex;
flex-direction: column;
gap: 6px;
}
/* 文件输入框隐藏 */
.form-field input[type="file"] {
display: none;
}
.field-label {
font-size: 0.9em;
color: var(--text-normal);
font-weight: 500;
}
.form-field input, .form-field select {
padding: 10px 12px;
border-radius: 6px;
border: 1px solid var(--background-modifier-border);
background-color: var(--background-modifier-form-field);
color: var(--text-normal);
font-size: 0.9em;
height: 40px;
}
.form-field input:focus, .form-field select:focus {
border-color: var(--text-accent);
outline: none;
}
.form-field input::placeholder {
color: var(--text-muted);
}
/* 文件选择按钮样式 */
.file-select-button {
display: inline-block;
padding: 6px 12px;
background-color: var(--background-modifier-border);
color: var(--text-normal);
border-radius: 4px;
cursor: pointer;
font-size: 0.85em;
margin-right: 10px;
}
.file-select-button:hover {
background-color: var(--background-modifier-border-hover);
}
.file-label {
display: inline-block;
color: var(--text-muted);
font-size: 0.85em;
}
/* 生成按钮 */
.generate-btn {
background: var(--text-accent);
color: var(--text-on-accent);
padding: 12px 0;
border-radius: 6px;
text-align: center;
cursor: pointer;
margin-top: 10px;
transition: all 0.3s ease;
font-weight: 500;
}
.generate-btn:hover {
background: var(--text-accent-hover, var(--text-accent));
opacity: 0.9;
transform: translateY(-2px);
}

View file

@ -0,0 +1,474 @@
/**
*
*/
export interface IBlock {
type: string;
isEmpty(): boolean;
splitTail(minLength?: number): IBlock | null; // 分页时只允许尾部分割
mergeTail(other: IBlock): void; // 块合并仅支持尾部合并
toHTMLElement(): HTMLElement;
clone(): IBlock;
}
/**
* Paragraph/TextBlock
*/
export class TextBlock implements IBlock {
type: string;
content: string;
constructor(type: string, content: string) {
this.type = type;
this.content = content;
}
isEmpty(): boolean {
return this.content.trim().length === 0;
}
splitTail(minLength = 40): TextBlock | null {
if (this.content.length <= minLength * 2) return null;
const punctuation = /[。!?;.?!;]/;
let splitPoint = -1;
for (let i = this.content.length - minLength; i >= minLength; i--) {
if (punctuation.test(this.content[i])) {
splitPoint = i + 1;
break;
}
}
// fallback: 没找到标点就强拆中间
if (splitPoint === -1) {
splitPoint = Math.floor(this.content.length / 2);
}
const head = this.content.slice(0, splitPoint).trim();
const tail = this.content.slice(splitPoint).trim();
if (tail.length < minLength * 0.6) return null;
this.content = head;
return new TextBlock(this.type, tail);
}
mergeTail(other: IBlock): void {
if (other instanceof TextBlock) {
this.content += other.content;
}
}
toHTMLElement(): HTMLElement {
const el = document.createElement(this.type);
el.innerHTML = this.content;
return el;
}
clone(): TextBlock {
return new TextBlock(this.type, this.content);
}
}
/**
* DOMBlock//
*/
export class DOMBlock implements IBlock {
type: string;
element: HTMLElement;
constructor(element: HTMLElement) {
this.type = element.tagName.toLowerCase();
this.element = element.cloneNode(true) as HTMLElement;
}
isEmpty(): boolean {
return !this.element.textContent?.trim();
}
splitTail(): IBlock | null { return null; } // DOM块不可分割
mergeTail(other: IBlock): void { } // DOM块不可合并
toHTMLElement(): HTMLElement {
return this.element.cloneNode(true) as HTMLElement;
}
clone(): DOMBlock {
return new DOMBlock(this.element);
}
}
/**
* ImageBlock
*/
export class ImageBlock implements IBlock {
type: string;
element: HTMLElement;
constructor(element: HTMLImageElement) {
this.type = 'img';
this.element = element.cloneNode(true) as HTMLElement;
}
isEmpty(): boolean { return false; }
splitTail(): IBlock | null { return null; }
mergeTail(other: IBlock): void { }
toHTMLElement(): HTMLElement {
return this.element.cloneNode(true) as HTMLElement;
}
clone(): ImageBlock {
return new ImageBlock(this.element as HTMLImageElement);
}
}
/**
*
*/
export class Page {
blocks: IBlock[] = [];
element: HTMLElement;
bookSize: string;
constructor(bookSize: string = 'a4') {
this.bookSize = bookSize;
this.element = this.createPageElement();
}
private createPageElement(): HTMLElement {
const page = document.createElement('div');
page.classList.add('book-page', `book-size-${this.bookSize}`);
// 页码占位
const pageNumber = document.createElement('div');
pageNumber.classList.add('page-number');
page.appendChild(pageNumber);
return page;
}
addBlock(block: IBlock): void {
this.blocks.push(block.clone());
this.element.appendChild(block.toHTMLElement());
}
removeLastBlock(): IBlock | null {
if (!this.blocks.length) return null;
this.element.removeChild(this.element.lastChild as Node);
return this.blocks.pop() || null;
}
isOverflow(maxHeight: number): boolean {
return this.element.scrollHeight > maxHeight;
}
setPageNumber(number: number): void {
const pageNumberElement = this.element.querySelector('.page-number');
if (pageNumberElement) {
pageNumberElement.textContent = number.toString();
(pageNumberElement as HTMLElement).style.position = 'absolute';
(pageNumberElement as HTMLElement).style.bottom = '10px';
(pageNumberElement as HTMLElement).style.left = '0';
(pageNumberElement as HTMLElement).style.right = '0';
(pageNumberElement as HTMLElement).style.textAlign = 'center';
}
}
}
/**
* DOM
*/
export function extractBlocks(container: HTMLElement): IBlock[] {
const blocks: IBlock[] = [];
const selectors = 'p, h1, h2, h3, h4, h5, h6, ul, ol, pre, blockquote, table, .callout, img';
const elements = Array.from(container.querySelectorAll(selectors));
for (const el of elements) {
if (el.tagName.toLowerCase() === 'img') {
blocks.push(new ImageBlock(el as HTMLImageElement));
} else if (['p','h1','h2','h3','h4','h5','h6'].includes(el.tagName.toLowerCase())) {
const block = new TextBlock(el.tagName.toLowerCase(), el.innerHTML);
blocks.push(block);
} else {
blocks.push(new DOMBlock(el as HTMLElement));
}
}
return blocks;
}
/**
*
*/
export class PaginatedEngine {
private container: HTMLElement;
private pages: Page[] = [];
private bookSize: string = 'a4';
private pageHeight: number = 800;
constructor(container: HTMLElement) {
this.container = container;
}
setOptions(options: { bookSize?: string; pageHeight?: number }) {
if (options.bookSize) this.bookSize = options.bookSize;
if (options.pageHeight) this.pageHeight = options.pageHeight;
}
/**
*
*/
paginate(blocks: IBlock[]): number {
this.container.innerHTML = '';
this.pages = [];
const pageHeightMap = {
'a4': 1123, 'a5': 794, 'b5': 945, '16k': 983, 'custom': 907
};
const actualPageHeight = pageHeightMap[this.bookSize as keyof typeof pageHeightMap] || this.pageHeight;
let currentPage = this.createNewPage();
let unpaginatedBlocks = blocks.map(b => b.clone());
let safetyCounter = 0;
const maxIterations = blocks.length * 6;
while (unpaginatedBlocks.length > 0 && safetyCounter < maxIterations) {
safetyCounter++;
const nextBlock = unpaginatedBlocks[0];
currentPage.addBlock(nextBlock);
if (currentPage.isOverflow(actualPageHeight)) {
const removed = currentPage.removeLastBlock();
if (!removed) break;
let splitBlock = removed;
let splitSuccess = false;
while (true) {
const tail = splitBlock.splitTail?.() ?? null;
if (!tail) break;
currentPage.addBlock(splitBlock);
if (!currentPage.isOverflow(actualPageHeight)) {
unpaginatedBlocks[0] = tail;
splitSuccess = true;
break;
}
currentPage.removeLastBlock();
splitBlock = tail;
}
if (!splitSuccess) {
unpaginatedBlocks[0] = removed;
currentPage = this.createNewPage();
if (unpaginatedBlocks.length === 1 && unpaginatedBlocks[0] === removed) {
currentPage.addBlock(removed);
unpaginatedBlocks.shift();
}
}
} else {
unpaginatedBlocks.shift();
}
}
this.pages.forEach((page, idx) => page.setPageNumber(idx + 1));
return this.pages.length;
}
private createNewPage(): Page {
const page = new Page(this.bookSize);
this.pages.push(page);
this.container.appendChild(page.element);
return page;
}
getPages(): Page[] { return this.pages; }
addPageMarkers(format = "— 第 {page} 页 —") {
this.pages.forEach((page, idx) => {
const marker = document.createElement('div');
marker.classList.add('page-marker');
marker.textContent = format.replace("{page}", (idx + 1).toString());
page.element.appendChild(marker);
});
}
}
/**
*
*/
export function createNewPage(bookSize: string = 'a4'): HTMLElement {
const page = document.createElement('div');
page.classList.add('book-page');
page.classList.add(`book-size-${bookSize}`);
// 添加页码占位元素
const pageNumber = document.createElement('div');
pageNumber.classList.add('page-number');
page.appendChild(pageNumber);
return page;
}
/**
*
*/
export function generatePaginatedTOC(container: HTMLElement, contentContainer: HTMLElement, bookSize: string = 'a4'): HTMLElement[] {
// 创建目录容器
const tocContainer = document.createElement('div');
const tocTitle = document.createElement('h2');
tocTitle.textContent = '目录';
tocContainer.appendChild(tocTitle);
const tocList = document.createElement('ul');
tocList.classList.add('toc-list');
tocContainer.appendChild(tocList);
// 查找所有标题元素和它们所在的页面
const headingsWithPages: Array<{
level: number;
text: string;
id: string;
pageNumber: number;
}> = [];
const pages = Array.from(contentContainer.querySelectorAll('.book-page'));
pages.forEach((page, pageIndex) => {
const headings = Array.from(page.querySelectorAll('h1, h2, h3, h4, h5, h6'));
headings.forEach((heading, index) => {
// 优先使用自定义属性中的实际层级,如果没有则使用标签名中的数字
const level = (heading as HTMLElement).dataset.actualLevel
? parseInt((heading as HTMLElement).dataset.actualLevel || '1')
: parseInt(heading.tagName.substring(1));
const text = heading.textContent || '';
const id = `heading-${pageIndex}-${index}`;
// 设置标题ID用于锚点链接
heading.id = id;
headingsWithPages.push({
level,
text,
id,
pageNumber: pageIndex + 1
});
});
});
// 为每个标题创建目录项
headingsWithPages.forEach(({ level, text, id, pageNumber }) => {
const listItem = document.createElement('li');
listItem.classList.add(`toc-level-${level}`);
// 创建链接和页码容器
const itemContent = document.createElement('div');
itemContent.classList.add('toc-item-content');
// 添加链接
const link = document.createElement('a');
link.href = `#${id}`;
link.textContent = text;
itemContent.appendChild(link);
// 添加页码
const pageRef = document.createElement('span');
pageRef.classList.add('toc-page-ref');
pageRef.textContent = pageNumber.toString();
itemContent.appendChild(pageRef);
listItem.appendChild(itemContent);
tocList.appendChild(listItem);
});
// 对目录进行分页处理
const tocPages = [];
const pageHeight = getPageHeightByBookSize(bookSize);
// 创建第一个目录页
let currentTocPage = document.createElement('div');
currentTocPage.classList.add('book-page', 'toc-page', `book-size-${bookSize}`);
currentTocPage.appendChild(tocContainer.cloneNode(true));
tocPages.push(currentTocPage);
// 检查目录是否需要分页
if (currentTocPage.scrollHeight > pageHeight) {
// 重新创建目录,按项目分页
tocPages.length = 0;
currentTocPage = document.createElement('div');
currentTocPage.classList.add('book-page', 'toc-page', `book-size-${bookSize}`);
const newTocContainer = document.createElement('div');
newTocContainer.classList.add('typography-toc');
newTocContainer.classList.add(`book-size-${bookSize}`);
const newTocTitle = document.createElement('h2');
newTocTitle.textContent = '目录';
newTocContainer.appendChild(newTocTitle);
const newTocList = document.createElement('ul');
newTocList.classList.add('toc-list');
newTocContainer.appendChild(newTocList);
currentTocPage.appendChild(newTocContainer);
tocPages.push(currentTocPage);
// 逐项添加目录项,检查是否需要创建新页
headingsWithPages.forEach(({ level, text, id, pageNumber }) => {
const listItem = document.createElement('li');
listItem.classList.add(`toc-level-${level}`);
// 创建链接和页码容器
const itemContent = document.createElement('div');
itemContent.classList.add('toc-item-content');
// 添加链接
const link = document.createElement('a');
link.href = `#${id}`;
link.textContent = text;
itemContent.appendChild(link);
// 添加页码
const pageRef = document.createElement('span');
pageRef.classList.add('toc-page-ref');
pageRef.textContent = pageNumber.toString();
itemContent.appendChild(pageRef);
listItem.appendChild(itemContent);
// 添加到当前目录页
const currentTocList = currentTocPage.querySelector('.toc-list');
if (currentTocList) {
currentTocList.appendChild(listItem);
// 检查是否溢出
if (currentTocPage.scrollHeight > pageHeight) {
// 移除刚添加的项
currentTocList.removeChild(listItem);
// 创建新的目录页
const nextTocPage = document.createElement('div');
nextTocPage.classList.add('book-page', 'toc-page', `book-size-${bookSize}`);
const nextTocContainer = document.createElement('div');
nextTocContainer.classList.add('typography-toc');
nextTocContainer.classList.add(`book-size-${bookSize}`);
const nextTocTitle = document.createElement('h2');
nextTocTitle.textContent = '目录';
nextTocContainer.appendChild(nextTocTitle);
const nextTocList = document.createElement('ul');
nextTocList.classList.add('toc-list');
nextTocContainer.appendChild(nextTocList);
nextTocPage.appendChild(nextTocContainer);
nextTocList.appendChild(listItem);
tocPages.push(nextTocPage);
currentTocPage = nextTocPage;
}
}
});
}
return tocPages;
}
// 辅助函数:根据开本大小获取页面高度
function getPageHeightByBookSize(bookSize: string): number {
switch (bookSize) {
case 'a4': return 1000;
case 'a5': return 800;
case 'b5': return 900;
case '16k': return 850;
case 'custom': return 800;
default: return 800;
}
}

View file

@ -2,11 +2,12 @@ import { ItemView, WorkspaceLeaf, setIcon, Menu, Notice } from 'obsidian';
import { FocusToolView } from '../components/FocusToolView';
import { DonateModal } from '../modals/DonateModal';
import { InspirationModal } from '../modals/InspirationModal';
import { EbookModal } from '../modals/EbookModal';
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 { EbookView } from '../components/EbookView';
interface ToolItem {
icon: string;
@ -19,6 +20,8 @@ interface ToolItem {
export class ToolView extends ItemView {
private normalView: HTMLElement | null = null;
private focusView: FocusToolView | null = null;
private typographyView: TypographyView | null = null;
private ebookView: EbookView | null = null;
constructor(leaf: WorkspaceLeaf, private plugin: BookSmithPlugin) {
super(leaf);
@ -46,13 +49,6 @@ export class ToolView extends ItemView {
this.createNormalView(container as HTMLElement);
}
async onClose() {
if (this.focusView) {
this.focusView.remove();
this.focusView = null;
}
}
// 视图刷新方法
refresh() {
if (this.normalView) {
@ -118,14 +114,12 @@ export class ToolView extends ItemView {
{
icon: 'edit-3',
text: i18n.t('DESIGN_TYPOGRAPHY'),
onClick: () => {
new Notice(i18n.t('FEATURE_COMING_SOON', { feature: i18n.t('DESIGN_TYPOGRAPHY') }));
}
onClick: () => this.enterTypographyMode()
},
{
icon: 'book',
text: i18n.t('GENERATE_EBOOK'),
onClick: () => new EbookModal(this.containerEl, this.app, this.plugin.settings).open()
onClick: () => this.enterEbookMode()
},
{
icon: 'clock',
@ -241,4 +235,68 @@ 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();
}
// 添加进入电子书模式的方法
private enterEbookMode() {
if (!this.normalView) return;
this.normalView.empty();
this.ebookView = new EbookView(
this.app,
this.plugin,
this.normalView,
() => {
this.ebookView?.remove();
this.ebookView = null;
if (this.normalView) {
this.normalView.empty();
this.createNormalView(this.normalView);
}
}
);
// 初始化视图(加载书籍等)
this.ebookView.initialize();
}
// 修改 onClose 方法,确保所有视图都被正确关闭
async onClose() {
if (this.focusView) {
this.focusView.remove();
this.focusView = null;
}
if (this.typographyView) {
this.typographyView.remove();
this.typographyView = null;
}
if (this.ebookView) {
this.ebookView.remove();
this.ebookView = null;
}
}
}