mirror of
https://github.com/yeban8090/book-smith.git
synced 2026-07-22 05:46:23 +00:00
commit
14990fb6d3
26 changed files with 4105 additions and 580 deletions
|
|
@ -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",
|
||||
|
|
|
|||
37
package-lock.json
generated
37
package-lock.json
generated
|
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"docx": "^9.5.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -577,6 +578,24 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@pdf-lib/standard-fonts": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
|
||||
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pako": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@pdf-lib/upng": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
|
||||
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pako": "^1.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
|
|
@ -2126,6 +2145,24 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-lib": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
|
||||
"integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pdf-lib/standard-fonts": "^1.0.0",
|
||||
"@pdf-lib/upng": "^1.0.1",
|
||||
"pako": "^1.0.11",
|
||||
"tslib": "^1.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-lib/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "^3.3.0",
|
||||
"electron": "32.2.5",
|
||||
"esbuild": "^0.17.3",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
|
|
@ -29,6 +30,7 @@
|
|||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"docx": "^9.5.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"uuid": "^11.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
973
src/components/TypographyView.ts
Normal file
973
src/components/TypographyView.ts
Normal file
|
|
@ -0,0 +1,973 @@
|
|||
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 } from '../services/ThemeManager';
|
||||
import { CoverManager, CoverSettings } from '../services/CoverManager';
|
||||
import { CoverSettingModal } from '../modals/CoverSettingModal';
|
||||
import { ExportModal } from '../modals/ExportModal';
|
||||
|
||||
export 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) {
|
||||
// 创建一个包含封面和内容的容器
|
||||
const previewContainer = container.createDiv({ cls: 'typography-preview-container' });
|
||||
|
||||
// 添加封面预览(独立容器)
|
||||
const coverContainer = previewContainer.createDiv({ cls: 'typography-cover-container' });
|
||||
this.coverPreviewElement = coverContainer.createDiv({ cls: 'typography-cover-preview' });
|
||||
|
||||
// 内容预览(独立容器)
|
||||
const contentContainer = previewContainer.createDiv({ cls: 'typography-content-container' });
|
||||
this.previewElement = contentContainer.createDiv({ cls: 'typography-preview' });
|
||||
}
|
||||
|
||||
// 按照要求的顺序创建顶部选择器:书籍、模板、主题、字体、字号大小
|
||||
private createTopSelectors(container: HTMLElement) {
|
||||
// 创建第一行选择器容器
|
||||
const selectorsRow = container.createDiv({ cls: 'typography-selectors-row' });
|
||||
// 1. 创建书籍选择器
|
||||
this.createBookSelector(selectorsRow);
|
||||
// 2. 创建模板选择器
|
||||
// this.createTemplateSelector(selectorsRow);
|
||||
// 3. 创建主题选择器
|
||||
// this.createThemeSelector(selectorsRow);
|
||||
// 4. 创建字体选择器
|
||||
this.createFontSelector(selectorsRow);
|
||||
// 5. 创建字号大小控制
|
||||
this.createFontSizeControls(selectorsRow);
|
||||
// 6. 创建开本大小选择器
|
||||
this.createBookSizeSelector(selectorsRow);
|
||||
}
|
||||
|
||||
// 自定义选择器通用方法
|
||||
private createCustomSelect(parent: HTMLElement, className: string, options: { value: string, text: string }[]): HTMLElement {
|
||||
const container = parent.createDiv({ cls: `book-smith-select-container ${className}` });
|
||||
|
||||
const select = container.createDiv({ cls: 'book-smith-select' });
|
||||
const textSpan = select.createSpan({ cls: 'book-smith-text' });
|
||||
const arrowSpan = select.createSpan({ cls: 'book-smith-select-arrow' });
|
||||
setIcon(arrowSpan, 'chevron-down');
|
||||
|
||||
// 默认选择第一个选项
|
||||
if (options.length > 0) {
|
||||
textSpan.textContent = options[0].text;
|
||||
select.dataset.value = options[0].value;
|
||||
}
|
||||
|
||||
// 创建下拉菜单
|
||||
const dropdown = container.createDiv({ cls: 'book-smith-select-dropdown' });
|
||||
|
||||
// 添加选项
|
||||
for (const option of options) {
|
||||
const item = dropdown.createDiv({ cls: 'book-smith-select-item' });
|
||||
item.textContent = option.text;
|
||||
item.dataset.value = option.value;
|
||||
|
||||
// 默认选中第一个
|
||||
if (options.indexOf(option) === 0) {
|
||||
item.addClass('book-smith-selected');
|
||||
}
|
||||
|
||||
// 点击选项
|
||||
item.addEventListener('click', () => {
|
||||
// 更新显示文本和值
|
||||
textSpan.textContent = option.text;
|
||||
select.dataset.value = option.value;
|
||||
|
||||
// 更新选中状态
|
||||
dropdown.querySelectorAll('.book-select-item').forEach(el => {
|
||||
el.removeClass('book-smith-selected');
|
||||
});
|
||||
item.addClass('book-smith-selected');
|
||||
|
||||
// 关闭下拉菜单
|
||||
dropdown.removeClass('book-smith-show');
|
||||
arrowSpan.style.transform = '';
|
||||
|
||||
// 触发自定义事件
|
||||
select.dispatchEvent(new CustomEvent('change', {
|
||||
detail: { value: option.value, text: option.text }
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// 点击选择器显示/隐藏下拉菜单
|
||||
select.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
dropdown.toggleClass('book-smith-show', !dropdown.hasClass('book-smith-show'));
|
||||
arrowSpan.style.transform = dropdown.hasClass('book-smith-show') ? 'rotate(180deg)' : '';
|
||||
});
|
||||
|
||||
// 点击其他地方关闭下拉菜单
|
||||
document.addEventListener('click', () => {
|
||||
dropdown.removeClass('book-smith-show');
|
||||
arrowSpan.style.transform = '';
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
// 更新自定义选择器的选项
|
||||
private updateCustomSelectOptions(selectElement: HTMLElement, options: { value: string, text: string }[]) {
|
||||
const dropdown = selectElement.querySelector('.book-smith-select-dropdown');
|
||||
const select = selectElement.querySelector('.book-smith-select');
|
||||
const textSpan = selectElement.querySelector('.book-smith-text');
|
||||
|
||||
if (!dropdown || !select || !textSpan) return;
|
||||
|
||||
// 清空现有选项
|
||||
dropdown.innerHTML = '';
|
||||
|
||||
// 添加新选项
|
||||
for (const option of options) {
|
||||
const item = dropdown.createDiv({ cls: 'book-smith-select-item' });
|
||||
item.textContent = option.text;
|
||||
item.dataset.value = option.value;
|
||||
|
||||
// 默认选中第一个
|
||||
if (options.indexOf(option) === 0) {
|
||||
item.addClass('book-smith-selected');
|
||||
textSpan.textContent = option.text;
|
||||
(select as HTMLElement).setAttribute('data-value', option.value);
|
||||
}
|
||||
|
||||
// 点击选项
|
||||
item.addEventListener('click', () => {
|
||||
// 更新显示文本和值
|
||||
textSpan.textContent = option.text;
|
||||
(select as HTMLElement).setAttribute('data-value', option.value);
|
||||
|
||||
// 更新选中状态
|
||||
dropdown.querySelectorAll('.book-smith-select-item').forEach(el => {
|
||||
el.removeClass('book-smith-selected');
|
||||
});
|
||||
item.addClass('book-smith-selected');
|
||||
|
||||
// 关闭下拉菜单
|
||||
dropdown.removeClass('book-smith-show');
|
||||
|
||||
// 触发自定义事件
|
||||
select.dispatchEvent(new CustomEvent('change', {
|
||||
detail: { value: option.value, text: option.text }
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 书籍选择器
|
||||
private createBookSelector(parent: HTMLElement) {
|
||||
// 获取书籍选项
|
||||
const bookOptions = this.books.map(book => ({
|
||||
value: book.basic.uuid,
|
||||
text: book.basic.title
|
||||
}));
|
||||
|
||||
// 如果没有书籍,添加一个提示选项
|
||||
if (bookOptions.length === 0) {
|
||||
bookOptions.push({
|
||||
value: '',
|
||||
text: i18n.t('NO_BOOKS_AVAILABLE') || '没有可用的书籍'
|
||||
});
|
||||
}
|
||||
|
||||
// 创建自定义选择器
|
||||
this.customBookSelect = this.createCustomSelect(
|
||||
parent,
|
||||
'book-smith-book-select',
|
||||
bookOptions
|
||||
);
|
||||
this.customBookSelect.id = 'book-select';
|
||||
}
|
||||
|
||||
// 初始化书籍选择器
|
||||
private initializeBookSelect() {
|
||||
if (!this.customBookSelect) return;
|
||||
|
||||
// 更新书籍选项
|
||||
const bookOptions = this.books.map(book => ({
|
||||
value: book.basic.uuid,
|
||||
text: book.basic.title
|
||||
}));
|
||||
|
||||
// 更新选择器选项
|
||||
this.updateCustomSelectOptions(this.customBookSelect, bookOptions);
|
||||
|
||||
// 添加事件监听
|
||||
this.customBookSelect.querySelector('.book-smith-select')?.addEventListener('change', async (e: any) => {
|
||||
const value = e.detail.value;
|
||||
this.selectedBook = this.books.find(book => book.basic.uuid === value) || null;
|
||||
this.updatePreview();
|
||||
});
|
||||
|
||||
// 如果有书籍,选择第一本
|
||||
if (bookOptions.length > 0) {
|
||||
const select = this.customBookSelect.querySelector('.book-smith-select');
|
||||
if (select) {
|
||||
(select as HTMLElement).setAttribute('data-value', bookOptions[0].value);
|
||||
this.selectedBook = this.books[0];
|
||||
this.updatePreview();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 模板选择器
|
||||
private createTemplateSelector(parent: HTMLElement) {
|
||||
this.customTemplateSelect = this.createCustomSelect(
|
||||
parent,
|
||||
'book-smith-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('.book-smith-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('.book-smith-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,
|
||||
'book-smith-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('.book-smith-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('.book-smith-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: 'songti', text: i18n.t('SONGTI_FONT') || '宋体' },
|
||||
{ value: 'heiti', text: i18n.t('HEITI_FONT') || '黑体' },
|
||||
{ value: 'kaiti', text: i18n.t('KAITI_FONT') || '楷体' },
|
||||
{ value: 'fangsong', text: i18n.t('FANGSONG_FONT') || '仿宋' },
|
||||
{ value: 'serif', text: i18n.t('SERIF_FONT') || '衬线字体' },
|
||||
{ value: 'sans-serif', text: i18n.t('SANS_SERIF_FONT') || '无衬线字体' },
|
||||
{ value: 'monospace', text: i18n.t('MONOSPACE_FONT') || '等宽字体' },
|
||||
];
|
||||
|
||||
this.customFontSelect = this.createCustomSelect(
|
||||
parent,
|
||||
'book-smith-font-select',
|
||||
fontOptions
|
||||
);
|
||||
this.customFontSelect.id = 'font-select';
|
||||
}
|
||||
|
||||
// 初始化字体选择器
|
||||
private initializeFontSelect() {
|
||||
if (!this.customFontSelect) return;
|
||||
|
||||
// 添加事件监听
|
||||
this.customFontSelect.querySelector('.book-smith-select')?.addEventListener('change', () => {
|
||||
this.updatePreview();
|
||||
});
|
||||
}
|
||||
|
||||
// 5. 字号大小控制
|
||||
private createFontSizeControls(parent: HTMLElement) {
|
||||
const fontSizeGroup = parent.createEl('div', { cls: 'book-smith-font-size-group' });
|
||||
|
||||
// 添加减小按钮
|
||||
fontSizeGroup.createEl('button', {
|
||||
cls: 'book-smith-font-size-button book-smith-decrease-button',
|
||||
text: '-'
|
||||
});
|
||||
|
||||
this.fontSizeInput = fontSizeGroup.createEl('input', {
|
||||
cls: 'book-smith-font-size-input',
|
||||
type: 'text',
|
||||
value: '16',
|
||||
attr: {
|
||||
style: 'border: none; outline: none; background: transparent;',
|
||||
min: '12',
|
||||
max: '30'
|
||||
}
|
||||
}) as HTMLInputElement;
|
||||
|
||||
// 添加增大按钮
|
||||
fontSizeGroup.createEl('button', {
|
||||
cls: 'book-smith-font-size-button book-smith-increase-button',
|
||||
text: '+'
|
||||
});
|
||||
|
||||
// 添加单位标签
|
||||
fontSizeGroup.createEl('span', {
|
||||
cls: 'book-smith-font-size-unit'
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化字号控制
|
||||
private initializeFontSizeControls() {
|
||||
if (!this.fontSizeInput) return;
|
||||
|
||||
const updateFontSize = () => {
|
||||
if (!this.fontSizeInput) return;
|
||||
|
||||
// 获取当前值并确保在有效范围内
|
||||
let currentSize = parseInt(this.fontSizeInput.value) || 16;
|
||||
currentSize = Math.max(12, Math.min(30, currentSize));
|
||||
|
||||
// 更新输入框值
|
||||
this.fontSizeInput.value = currentSize.toString();
|
||||
|
||||
// 更新主题管理器中的字体大小
|
||||
this.themeManager.setFontSize(currentSize);
|
||||
|
||||
// 更新预览
|
||||
this.updatePreview();
|
||||
};
|
||||
|
||||
// 获取按钮元素
|
||||
const decreaseButton = this.fontSizeInput.parentElement?.querySelector('.book-smith-decrease-button');
|
||||
const increaseButton = this.fontSizeInput.parentElement?.querySelector('.book-smith-increase-button');
|
||||
|
||||
// 添加减小字号事件
|
||||
decreaseButton?.addEventListener('click', () => {
|
||||
if (!this.fontSizeInput) return;
|
||||
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: 'A3', text: 'A3' },
|
||||
{ value: 'Legal', text: 'Legal' },
|
||||
{ value: 'Letter', text: 'Letter' },
|
||||
{ value: 'Tabloid', text: 'Tabloid' }
|
||||
];
|
||||
|
||||
this.customBookSizeSelect = this.createCustomSelect(
|
||||
parent,
|
||||
'book-smith-book-size-select',
|
||||
bookSizeOptions
|
||||
);
|
||||
this.customBookSizeSelect.id = 'book-size-select';
|
||||
}
|
||||
|
||||
// 初始化开本大小选择器
|
||||
private initializeBookSizeSelect() {
|
||||
if (!this.customBookSizeSelect) return;
|
||||
|
||||
// 添加事件监听
|
||||
this.customBookSizeSelect.querySelector('.book-smith-select')?.addEventListener('change', () => {
|
||||
this.updatePreview();
|
||||
});
|
||||
}
|
||||
|
||||
// 获取排版设置
|
||||
private getTypographySettings(): TypographySettings {
|
||||
// 获取封面开关状态
|
||||
const coverToggle = this.parentEl.querySelector('.cover-toggle-input') as HTMLInputElement;
|
||||
const showCover = coverToggle ? coverToggle.checked : true;
|
||||
|
||||
// 获取字体系列
|
||||
const fontFamily = (this.customFontSelect?.querySelector('.book-smith-select') as HTMLElement)?.getAttribute('data-value') || 'default';
|
||||
|
||||
// 获取实际的字体CSS定义
|
||||
const fontFamilyCSS = this.getFontFamilyCSS(fontFamily);
|
||||
|
||||
return {
|
||||
fontFamily: fontFamilyCSS,
|
||||
fontSize: this.fontSizeInput?.value || '16',
|
||||
lineHeight: 'normal', // 保留默认值
|
||||
margin: '0', // 保留默认值
|
||||
templateId: (this.customTemplateSelect?.querySelector('.book-smith-select') as HTMLElement)?.getAttribute('data-value') || 'notes',
|
||||
themeId: (this.customThemeSelect?.querySelector('.book-smith-select') as HTMLElement)?.getAttribute('data-value') || 'default',
|
||||
coverSettings: this.coverSettings || undefined,
|
||||
showCover: showCover,
|
||||
bookSize: (this.customBookSizeSelect?.querySelector('.book-smith-select') as HTMLElement)?.getAttribute('data-value') || 'a4'
|
||||
};
|
||||
}
|
||||
|
||||
// 获取字体CSS定义
|
||||
private getFontFamilyCSS(fontFamily: string): string {
|
||||
switch (fontFamily) {
|
||||
case 'serif':
|
||||
return 'Georgia, Times, \'Times New Roman\', serif';
|
||||
case 'sans-serif':
|
||||
return '-apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, Helvetica, Arial, sans-serif';
|
||||
case 'monospace':
|
||||
return '\'SFMono-Regular\', Consolas, \'Liberation Mono\', Menlo, Courier, monospace';
|
||||
case 'songti':
|
||||
return 'SimSun, "宋体", STSong, "华文宋体", serif';
|
||||
case 'heiti':
|
||||
return 'SimHei, "黑体", STHeiti, "华文黑体", sans-serif';
|
||||
case 'kaiti':
|
||||
return 'KaiTi, "楷体", STKaiti, "华文楷体", serif';
|
||||
case 'fangsong':
|
||||
return 'FangSong, "仿宋", STFangsong, "华文仿宋", serif';
|
||||
case 'default':
|
||||
default:
|
||||
return 'var(--default-font)';
|
||||
}
|
||||
}
|
||||
|
||||
// 预览相关方法
|
||||
private async updatePreview() {
|
||||
if (!this.selectedBook) return;
|
||||
|
||||
// 获取封面开关状态
|
||||
const coverToggle = this.parentEl.querySelector('.cover-toggle-input') as HTMLInputElement;
|
||||
const showCover = coverToggle ? coverToggle.checked : true;
|
||||
|
||||
// 分离封面和内容的显示逻辑
|
||||
if (this.coverPreviewElement) {
|
||||
this.coverPreviewElement.style.display = showCover ? 'block' : 'none';
|
||||
if (showCover) {
|
||||
this.updateCoverPreview();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.previewElement) {
|
||||
this.previewElement.style.display = 'block';
|
||||
// 清空预览元素
|
||||
this.previewElement.empty();
|
||||
// 渲染内容
|
||||
await this.renderPaginatedContent();
|
||||
}
|
||||
}
|
||||
|
||||
private async renderPaginatedContent() {
|
||||
if (!this.previewElement) return;
|
||||
|
||||
// 清空预览区域
|
||||
this.previewElement.empty();
|
||||
|
||||
// 获取排版设置
|
||||
const settings = this.getTypographySettings();
|
||||
|
||||
// 更新预览样式
|
||||
this.updatePreviewStyle(settings);
|
||||
|
||||
// 渲染所有内容
|
||||
await this.renderAllContent(this.previewElement);
|
||||
|
||||
// 在渲染完成后应用主题和模板
|
||||
// if (this.currentTemplate) {
|
||||
// this.currentTemplate.render(this.previewElement);
|
||||
// }
|
||||
// this.themeManager.applyTheme(this.previewElement);
|
||||
}
|
||||
|
||||
// 渲染所有内容到指定容器
|
||||
private async renderAllContent(container: HTMLElement) {
|
||||
if (!this.selectedBook) return;
|
||||
|
||||
// 渲染章节内容
|
||||
if (this.selectedBook.structure && this.selectedBook.structure.tree) {
|
||||
await this.renderChapters(this.selectedBook.structure.tree, container);
|
||||
}
|
||||
}
|
||||
|
||||
// 递归渲染章节内容(不分页,只渲染到临时容器)
|
||||
private async renderChapters(chapters: ChapterNode[], container: HTMLElement, level: number = 1) {
|
||||
if (!chapters || chapters.length === 0) return;
|
||||
|
||||
for (const chapter of chapters) {
|
||||
// 跳过被排除的章节
|
||||
if (chapter.exclude) continue;
|
||||
// 创建标题元素
|
||||
const heading = document.createElement(`h${Math.min(level, 6)}`);
|
||||
heading.textContent = chapter.title;
|
||||
heading.dataset.actualLevel = level.toString(); // 添加自定义属性记录实际层级
|
||||
container.appendChild(heading);
|
||||
|
||||
// 如果是文件节点,渲染文件内容
|
||||
if (chapter.type === 'file' && chapter.path) {
|
||||
try {
|
||||
// 构建文件路径
|
||||
const filePath = `${this.rootPath}/${this.selectedBook?.basic.title}/${chapter.path}`;
|
||||
const file = this.app.vault.getAbstractFileByPath(filePath);
|
||||
|
||||
if (file instanceof TFile) {
|
||||
// 创建内容容器
|
||||
const contentEl = container.createDiv({ cls: 'chapter-content' });
|
||||
|
||||
// 使用 cachedRead 获取文件内容
|
||||
const content = await this.app.vault.cachedRead(file);
|
||||
|
||||
// 使用 MarkdownRenderer 渲染内容
|
||||
await MarkdownRenderer.render(
|
||||
this.app,
|
||||
content,
|
||||
contentEl,
|
||||
file.path,
|
||||
this.plugin
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`无法读取文件 ${chapter.path}:`, error);
|
||||
container.createEl('p', { text: `*无法读取此章节内容*`, cls: 'error-message' });
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理子章节
|
||||
if (chapter.children && chapter.children.length > 0) {
|
||||
await this.renderChapters(chapter.children, container, level + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updatePreviewStyle(settings: TypographySettings) {
|
||||
if (!this.previewElement) return;
|
||||
|
||||
// 重置样式
|
||||
this.previewElement.className = 'typography-preview';
|
||||
|
||||
// 应用字体
|
||||
this.previewElement.style.fontFamily = `${settings.fontFamily}`;
|
||||
|
||||
// 应用字号
|
||||
this.previewElement.style.fontSize = `${settings.fontSize}px`;
|
||||
|
||||
// 应用行间距
|
||||
this.previewElement.classList.add(`line-height-${settings.lineHeight}`);
|
||||
|
||||
// 应用页边距
|
||||
this.previewElement.classList.add(`margin-${settings.margin}`);
|
||||
|
||||
// 应用开本大小
|
||||
this.previewElement.classList.add(`book-size-${settings.bookSize}`);
|
||||
|
||||
// 如果封面预览元素存在,也应用开本大小
|
||||
if (this.coverPreviewElement) {
|
||||
this.coverPreviewElement.className = 'typography-cover-preview';
|
||||
this.coverPreviewElement.classList.add(`book-size-${settings.bookSize}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 底部按钮
|
||||
private createButtons(container: HTMLElement) {
|
||||
// 创建按钮容器
|
||||
const buttonsContainer = container.createDiv({ cls: 'typography-buttons-container' });
|
||||
|
||||
// 第一行按钮组 - 封面相关
|
||||
const buttonsGroup = buttonsContainer.createDiv({ cls: 'typography-buttons-group cover-buttons-group' });
|
||||
|
||||
// 添加封面设计开关
|
||||
this.createCoverToggle(buttonsGroup);
|
||||
|
||||
// 导出按钮
|
||||
const exportBtn = buttonsGroup.createEl('button', {
|
||||
text: i18n.t('EXPORT') || '导出',
|
||||
cls: 'typography-btn export-btn'
|
||||
});
|
||||
exportBtn.addEventListener('click', () => this.exportWithTypography());
|
||||
}
|
||||
|
||||
// 添加封面设计开关
|
||||
private createCoverToggle(parent: HTMLElement) {
|
||||
const coverToggleContainer = parent.createDiv({ cls: 'cover-toggle-container' });
|
||||
|
||||
coverToggleContainer.createEl('span', {
|
||||
text: i18n.t('SHOW_COVER') || '显示封面',
|
||||
cls: 'cover-toggle-label'
|
||||
});
|
||||
|
||||
const coverToggle = coverToggleContainer.createEl('input', {
|
||||
type: 'checkbox',
|
||||
cls: 'cover-toggle-input'
|
||||
}) as HTMLInputElement;
|
||||
coverToggle.checked = 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;');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 导出功能
|
||||
private async exportWithTypography() {
|
||||
try {
|
||||
if (!this.selectedBook) {
|
||||
throw new Error('未选择书籍');
|
||||
}
|
||||
|
||||
// 使用新的导出模态框
|
||||
const exportModal = new ExportModal(
|
||||
this.parentEl,
|
||||
this.exportService,
|
||||
(format) => this.executeExport(format)
|
||||
);
|
||||
|
||||
exportModal.open();
|
||||
} catch (error) {
|
||||
console.error('导出错误:', error);
|
||||
new Notice(`导出失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行导出
|
||||
private async executeExport(format: string) {
|
||||
try {
|
||||
if (!this.selectedBook) {
|
||||
throw new Error("未选择书籍");
|
||||
}
|
||||
|
||||
let useTypography = false;
|
||||
let htmlContent: string | undefined;
|
||||
let typographySettings: any | undefined;
|
||||
|
||||
if (format !== "txt") {
|
||||
useTypography = true;
|
||||
typographySettings = this.getTypographySettings();
|
||||
const tempContainer = document.createElement("div");
|
||||
if (this.previewElement) {
|
||||
tempContainer.innerHTML = this.previewElement.innerHTML;
|
||||
htmlContent = tempContainer.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
// 调用统一导出接口
|
||||
const result = await this.exportService.exportBook(format, this.selectedBook, {
|
||||
useTypography,
|
||||
htmlContent,
|
||||
typographySettings
|
||||
});
|
||||
|
||||
if (result.content === "cancelled") {
|
||||
new Notice("导出已取消");
|
||||
return;
|
||||
}
|
||||
new Notice(`${format} 已成功导出到: ${result.content}`);
|
||||
} catch (error) {
|
||||
console.error("导出错误:", error);
|
||||
new Notice(`导出失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 移除视图
|
||||
remove() {
|
||||
this.container.remove();
|
||||
}
|
||||
}
|
||||
|
|
@ -52,4 +52,37 @@ 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;
|
||||
SONGTI_FONT: string;
|
||||
HEITI_FONT: string;
|
||||
KAITI_FONT: string;
|
||||
FANGSONG_FONT: string;
|
||||
APPLY: string;
|
||||
TYPOGRAPHY_APPLIED: string;
|
||||
EXPORT: string;
|
||||
SELECT_BOOK_FIRST: string;
|
||||
EXPORT_SUCCESS: string;
|
||||
EXPORT_FAILED: string;
|
||||
FORMAT: string;
|
||||
CUSTOM_SIZE: string;
|
||||
SELECT_EXPORT_FORMAT: 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;
|
||||
}
|
||||
|
|
@ -373,7 +373,41 @@ 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',
|
||||
SONGTI_FONT: 'Songti font',
|
||||
HEITI_FONT: 'Heiti font',
|
||||
KAITI_FONT: 'Kaiti font',
|
||||
FANGSONG_FONT: 'Fangsong 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',
|
||||
SELECT_EXPORT_FORMAT: 'Select export format',
|
||||
|
||||
// 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 = {
|
||||
|
|
|
|||
|
|
@ -372,7 +372,41 @@ 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: '等宽字体',
|
||||
SONGTI_FONT: '宋体',
|
||||
HEITI_FONT: '黑体',
|
||||
KAITI_FONT: '楷体',
|
||||
FANGSONG_FONT: '仿宋',
|
||||
APPLY: '应用',
|
||||
TYPOGRAPHY_APPLIED: '排版设置已应用',
|
||||
EXPORT: '导出',
|
||||
SELECT_BOOK_FIRST: '请先选择一本书籍',
|
||||
EXPORT_SUCCESS: '导出成功',
|
||||
EXPORT_FAILED: '导出失败: ',
|
||||
FORMAT : '格式',
|
||||
CUSTOM_SIZE: '自定义大小',
|
||||
SELECT_EXPORT_FORMAT: '选择导出格式',
|
||||
|
||||
// CoverModal
|
||||
DESIGN_COVER: '设计封面',
|
||||
SHOW_COVER: '显示封面',
|
||||
COVER_DESIGN: '封面设计',
|
||||
COVER_IMAGE: '封面图片',
|
||||
CLEAR_IMAGE: '清除图片',
|
||||
BACKGROUND_COLOR: '背景颜色',
|
||||
OVERLAY: '覆盖层',
|
||||
OVERLAY_DESC: '添加半透明覆盖层以增强文字可读性',
|
||||
OVERLAY_OPACITY: '覆盖层透明度',
|
||||
SCALE: '缩放',
|
||||
};
|
||||
|
||||
const componentTranslation: ComponentTranslation = {
|
||||
|
|
|
|||
|
|
@ -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.themeManager = new ThemeManager(this.app, this.settings);
|
||||
this.imgTemplateManager = new ImgTemplateManager(this.app, this.themeManager);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
|
|
|
|||
240
src/modals/CoverSettingModal.ts
Normal file
240
src/modals/CoverSettingModal.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
import { App, Notice } from "obsidian";
|
||||
import { BaseModal } from "./BaseModal";
|
||||
import { BookManager } from "../services/BookManager";
|
||||
import { Book } from "../types/book";
|
||||
import { ExportService } from "../services/ExportService";
|
||||
import { i18n } from "../i18n/i18n";
|
||||
|
||||
export class EbookModal extends BaseModal {
|
||||
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);
|
||||
}
|
||||
|
||||
async open() {
|
||||
// 加载所有书籍
|
||||
this.books = await this.bookManager.getAllBooks();
|
||||
super.open();
|
||||
}
|
||||
|
||||
protected createContent() {
|
||||
const content = this.element.createDiv({ cls: 'ebook-content' });
|
||||
|
||||
// 电子书信息表单
|
||||
const form = content.createDiv({ cls: 'ebook-form' });
|
||||
|
||||
// 书籍选择
|
||||
this.createBookSelector(form);
|
||||
|
||||
// 格式选择
|
||||
const formatField = this.createFormField(form, '格式', 'format', 'select');
|
||||
const formatSelect = formatField.querySelector('select');
|
||||
if (formatSelect) {
|
||||
// 添加支持的格式
|
||||
this.createSelectOption(formatSelect, 'txt', 'TXT');
|
||||
this.createSelectOption(formatSelect, 'docx', 'DOCX');
|
||||
this.createSelectOption(formatSelect, 'pdf', 'PDF');
|
||||
this.createSelectOption(formatSelect, 'epub', 'EPUB');
|
||||
|
||||
formatSelect.addEventListener('change', (e) => {
|
||||
this.selectedFormat = (e.target as HTMLSelectElement).value;
|
||||
});
|
||||
}
|
||||
|
||||
// 生成按钮
|
||||
const generateBtn = content.createDiv({ cls: 'generate-btn', text: '生成电子书' });
|
||||
generateBtn.addEventListener('click', () => {
|
||||
this.exportBook();
|
||||
});
|
||||
}
|
||||
|
||||
private createBookSelector(container: HTMLElement) {
|
||||
const field = this.createFormField(container, '选择书籍', 'book', 'select');
|
||||
const select = field.querySelector('select');
|
||||
|
||||
if (select && this.books.length > 0) {
|
||||
// 添加书籍选项
|
||||
for (const book of this.books) {
|
||||
this.createSelectOption(select, 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) {
|
||||
// 没有书籍时显示提示
|
||||
this.createSelectOption(select, '', '没有可用的书籍');
|
||||
select.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async exportBook() {
|
||||
if (!this.selectedBook) {
|
||||
this.showNotice('请先选择一本书籍');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用导出服务生成内容
|
||||
const content = await this.exportService.exportBook(
|
||||
this.selectedFormat,
|
||||
this.selectedBook
|
||||
);
|
||||
|
||||
// 如果导出功能尚未实现,显示提示
|
||||
if (content.content.startsWith(`${this.selectedFormat.toUpperCase()}导出功能开发中`)) {
|
||||
this.showNotice(`${this.selectedFormat.toUpperCase()}导出功能尚在开发中,敬请期待!`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据格式处理下载
|
||||
if (this.selectedFormat === 'docx') {
|
||||
// 对于DOCX,内容是base64字符串
|
||||
const dataUrl = content.content;
|
||||
const a = document.createElement('a');
|
||||
a.href = dataUrl;
|
||||
a.download = content.fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
} else {
|
||||
// 对于其他格式(如TXT),使用Blob
|
||||
const blob = new Blob([content.content], { type: 'text/plain' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = content.fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
this.showNotice(`导出成功!`);
|
||||
this.close();
|
||||
} catch (error) {
|
||||
console.error('导出错误:', error);
|
||||
this.showNotice(`导出失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
|
||||
if (type === 'select') {
|
||||
field.createEl('select', { attr: { id } });
|
||||
} else {
|
||||
field.createEl('input', {
|
||||
attr: {
|
||||
type,
|
||||
id,
|
||||
placeholder: placeholder || null
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
private createSelectOption(select: HTMLSelectElement, value: string, text: string) {
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.text = text;
|
||||
select.appendChild(option);
|
||||
}
|
||||
}
|
||||
84
src/modals/ExportModal.ts
Normal file
84
src/modals/ExportModal.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { Notice } from "obsidian";
|
||||
import { BaseModal } from "./BaseModal";
|
||||
import { i18n } from "../i18n/i18n";
|
||||
import { ExportService } from "../services/ExportService";
|
||||
import { Book } from "../types/book";
|
||||
|
||||
export class ExportModal extends BaseModal {
|
||||
private formatButtons: HTMLButtonElement[] = [];
|
||||
private selectedFormat: string | null = null;
|
||||
private contentEl: HTMLElement;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
private exportService: ExportService,
|
||||
private executeExportCallback: (format: string) => Promise<void>
|
||||
) {
|
||||
super(container, i18n.t('SELECT_EXPORT_FORMAT') || '选择导出格式');
|
||||
}
|
||||
|
||||
protected createContent() {
|
||||
// 创建内容容器
|
||||
this.contentEl = this.element.createDiv({ cls: 'export-content' });
|
||||
|
||||
// 创建格式选择容器
|
||||
const formatsContainer = this.contentEl.createDiv({ cls: 'export-formats-container' });
|
||||
|
||||
// 获取支持的导出格式
|
||||
const formats = this.exportService.getSupportedFormats();
|
||||
|
||||
// 创建格式按钮
|
||||
formats.forEach(format => {
|
||||
const formatBtn = formatsContainer.createEl('button', {
|
||||
text: format.toUpperCase(),
|
||||
cls: 'export-format-btn'
|
||||
});
|
||||
|
||||
this.formatButtons.push(formatBtn);
|
||||
|
||||
formatBtn.addEventListener('click', () => {
|
||||
// 移除其他按钮的选中状态
|
||||
this.formatButtons.forEach(btn => btn.classList.remove('selected'));
|
||||
// 添加当前按钮的选中状态
|
||||
formatBtn.classList.add('selected');
|
||||
// 设置选中的格式
|
||||
this.selectedFormat = format;
|
||||
});
|
||||
});
|
||||
|
||||
// 添加分隔线
|
||||
this.contentEl.createEl('hr', { cls: 'export-divider' });
|
||||
|
||||
|
||||
// 创建按钮容器
|
||||
const buttonContainer = this.contentEl.createDiv({ cls: 'export-buttons-container' });
|
||||
|
||||
// 创建取消按钮
|
||||
const cancelBtn = buttonContainer.createEl('button', {
|
||||
text: i18n.t('CANCEL') || '取消',
|
||||
cls: 'export-cancel-btn'
|
||||
});
|
||||
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
// 创建确认按钮
|
||||
const confirmBtn = buttonContainer.createEl('button', {
|
||||
text: i18n.t('CONFIRM') || '确定',
|
||||
cls: 'export-confirm-btn'
|
||||
});
|
||||
|
||||
confirmBtn.addEventListener('click', async () => {
|
||||
if (this.selectedFormat) {
|
||||
// 关闭对话框
|
||||
this.close();
|
||||
|
||||
// 执行导出回调
|
||||
await this.executeExportCallback(this.selectedFormat);
|
||||
} else {
|
||||
this.showNotice(i18n.t('SELECT_EXPORT_FORMAT') || '请选择导出格式');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
95
src/services/CoverManager.ts
Normal file
95
src/services/CoverManager.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,50 @@
|
|||
import { App, TFile, TFolder } from 'obsidian';
|
||||
import { App, TFile } from 'obsidian';
|
||||
import { Book, ChapterNode } from '../types/book';
|
||||
import { i18n } from '../i18n/i18n';
|
||||
import { TypographySettings } from '../components/TypographyView';
|
||||
import * as fs from "fs/promises";
|
||||
|
||||
// 导出服务类
|
||||
export class ExportService {
|
||||
private rootPath: string;
|
||||
private strategies: Record<string, ExportStrategy>;
|
||||
|
||||
constructor(private app: App, settings: any) {
|
||||
this.rootPath = settings.defaultBookPath;
|
||||
this.strategies = {
|
||||
'txt': new TxtExportStrategy(this.rootPath),
|
||||
'pdf': new PdfExportStrategy(this.rootPath),
|
||||
};
|
||||
}
|
||||
|
||||
async exportBook(format: string, book: Book, options?: ExportOptions): Promise<{ content: string, fileName: string }> {
|
||||
const strategy = this.strategies[format];
|
||||
if (!strategy) {
|
||||
throw new Error(`不支持的导出格式: ${format}`);
|
||||
}
|
||||
|
||||
const content = await strategy.export(this.app, book, options);
|
||||
return {
|
||||
content: content,
|
||||
fileName: `${book.basic.title}.${format}`
|
||||
};
|
||||
}
|
||||
|
||||
getSupportedFormats(): string[] {
|
||||
return Object.keys(this.strategies);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出格式接口
|
||||
export interface ExportStrategy {
|
||||
export(app: App, book: Book, selectedChapters?: ChapterNode[]): Promise<string>;
|
||||
export(app: App, book: Book, options?: ExportOptions): Promise<string>;
|
||||
}
|
||||
|
||||
// 导出选项接口
|
||||
export interface ExportOptions {
|
||||
selectedChapters?: ChapterNode[];
|
||||
htmlContent?: string;
|
||||
useTypography?: boolean;
|
||||
typographySettings?: TypographySettings;
|
||||
}
|
||||
|
||||
// TXT导出策略 - 实际实现
|
||||
|
|
@ -13,10 +53,18 @@ export class TxtExportStrategy implements ExportStrategy {
|
|||
constructor(rootPath: string) {
|
||||
this.rootPath = rootPath;
|
||||
}
|
||||
async export(app: App, book: Book, selectedChapters?: ChapterNode[]): Promise<string> {
|
||||
async export(app: App, book: Book, options?: ExportOptions): Promise<string> {
|
||||
try {
|
||||
const content = await this.generateContent(app, book, selectedChapters);
|
||||
return content;
|
||||
const content = await this.generateContent(app, book, options?.selectedChapters);
|
||||
|
||||
// 获取保存路径
|
||||
const filePath = await this.getOutputFile(book.basic.title);
|
||||
if (!filePath) return "cancelled";
|
||||
|
||||
// 写入文件
|
||||
await fs.writeFile(filePath, content);
|
||||
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
console.error('TXT导出错误:', error);
|
||||
throw new Error(`TXT导出失败: ${error.message}`);
|
||||
|
|
@ -24,14 +72,14 @@ export class TxtExportStrategy implements ExportStrategy {
|
|||
}
|
||||
|
||||
private async generateContent(app: App, book: Book, selectedChapters?: ChapterNode[]): Promise<string> {
|
||||
let content = `${book.basic.title}\n`;
|
||||
let content = `${book.basic.title}`;
|
||||
if (book.basic.subtitle) {
|
||||
content += `${book.basic.subtitle}\n`;
|
||||
content += `${book.basic.subtitle}`;
|
||||
}
|
||||
content += `作者: ${book.basic.author.join(', ')}\n\n`;
|
||||
content += `作者: ${book.basic.author.join(', ')}`;
|
||||
|
||||
if (book.basic.desc) {
|
||||
content += `${book.basic.desc}\n\n`;
|
||||
content += `${book.basic.desc}`;
|
||||
}
|
||||
|
||||
const chapters = selectedChapters || book.structure.tree;
|
||||
|
|
@ -46,7 +94,7 @@ export class TxtExportStrategy implements ExportStrategy {
|
|||
if (chapter.exclude) continue;
|
||||
|
||||
// 添加章节标题
|
||||
content += `${chapter.title}\n\n`;
|
||||
content += `${chapter.title}`;
|
||||
if (chapter.type === 'file') {
|
||||
// 读取文件内容
|
||||
const filePath = `${this.rootPath}/${book?.basic.title}/${chapter.path}`;
|
||||
|
|
@ -55,7 +103,7 @@ export class TxtExportStrategy implements ExportStrategy {
|
|||
const fileContent = await app.vault.read(file);
|
||||
// 处理Markdown语法,转换为纯文本
|
||||
const plainTextContent = this.convertMarkdownToPlainText(fileContent);
|
||||
content += `${plainTextContent}\n\n`;
|
||||
content += `${plainTextContent}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,325 +158,210 @@ export class TxtExportStrategy implements ExportStrategy {
|
|||
plainText = plainText.replace(/<[^>]+>/g, '');
|
||||
|
||||
// 处理转义字符
|
||||
plainText = plainText.replace(/\\([\*_\[\]\(\)\`\#\+\-\.\!\\])/g, '$1');
|
||||
plainText = plainText.replace(/([\\`*_{}$begin:math:display$$end:math:display$()#+\-.!])/g, '\\$1');
|
||||
|
||||
// 移除多余的空行(连续两个以上的换行符替换为两个)
|
||||
plainText = plainText.replace(/\n{3,}/g, '\n\n');
|
||||
|
||||
return plainText;
|
||||
}
|
||||
}
|
||||
|
||||
// 其他导出策略 - 返回提示信息
|
||||
// DOCX导出策略 - 实际实现
|
||||
export class DocxExportStrategy implements ExportStrategy {
|
||||
private rootPath: string;
|
||||
constructor(rootPath: string) {
|
||||
this.rootPath = rootPath;
|
||||
}
|
||||
|
||||
async export(app: App, book: Book, selectedChapters?: ChapterNode[]): Promise<string> {
|
||||
try {
|
||||
const docxBlob = await this.generateDocx(app, book, selectedChapters);
|
||||
// 将Blob转换为base64字符串
|
||||
return await this.blobToBase64(docxBlob);
|
||||
} catch (error) {
|
||||
console.error('DOCX导出错误:', error);
|
||||
throw new Error(`DOCX导出失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async generateDocx(app: App, book: Book, selectedChapters?: ChapterNode[]): Promise<Blob> {
|
||||
// 导入docx库
|
||||
const { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType, BorderStyle } = await import('docx');
|
||||
|
||||
// 文档内容
|
||||
const children = [];
|
||||
|
||||
// 添加标题
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: book.basic.title,
|
||||
heading: HeadingLevel.TITLE,
|
||||
alignment: AlignmentType.CENTER,
|
||||
spacing: { after: 200 }
|
||||
})
|
||||
);
|
||||
|
||||
// 添加副标题
|
||||
if (book.basic.subtitle) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: book.basic.subtitle,
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
alignment: AlignmentType.CENTER,
|
||||
spacing: { after: 200 }
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 添加作者
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: `作者: ${book.basic.author.join(', ')}`,
|
||||
alignment: AlignmentType.CENTER,
|
||||
spacing: { after: 400 }
|
||||
})
|
||||
);
|
||||
|
||||
// 添加描述
|
||||
if (book.basic.desc) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: book.basic.desc,
|
||||
alignment: AlignmentType.JUSTIFIED,
|
||||
spacing: { after: 400 }
|
||||
})
|
||||
);
|
||||
}
|
||||
// 处理章节
|
||||
const chapters = selectedChapters || book.structure.tree;
|
||||
const chapterParagraphs = await this.processChapters(app, book, chapters);
|
||||
children.push(...chapterParagraphs);
|
||||
|
||||
// 创建文档并设置内容
|
||||
const doc = new Document({
|
||||
title: book.basic.title,
|
||||
description: book.basic.desc || '',
|
||||
creator: book.basic.author.join(', '),
|
||||
sections: [
|
||||
{
|
||||
children: children
|
||||
}
|
||||
]
|
||||
private async getOutputFile(filename: string): Promise<string | null> {
|
||||
//@ts-ignore
|
||||
const result = await electron.remote.dialog.showSaveDialog({
|
||||
title: "导出 TXT 文件",
|
||||
defaultPath: filename + ".txt",
|
||||
filters: [{ name: "TXT", extensions: ["txt"] }],
|
||||
properties: ["showOverwriteConfirmation", "createDirectory"]
|
||||
});
|
||||
|
||||
// 生成DOCX文件
|
||||
return await Packer.toBlob(doc);
|
||||
}
|
||||
|
||||
private async processChapters(app: App, book: Book, chapters: ChapterNode[], level: number = 1): Promise<any[]> {
|
||||
const { Paragraph, TextRun, HeadingLevel } = await import('docx');
|
||||
const paragraphs = [];
|
||||
|
||||
for (const chapter of chapters) {
|
||||
if (chapter.exclude) continue;
|
||||
|
||||
// 添加章节标题
|
||||
const headingLevel = level <= 6 ? level : 6; // 最多支持6级标题
|
||||
paragraphs.push(
|
||||
new Paragraph({
|
||||
text: chapter.title,
|
||||
heading: HeadingLevel[`HEADING_${headingLevel}` as keyof typeof HeadingLevel],
|
||||
spacing: { before: 300, after: 200 }
|
||||
})
|
||||
);
|
||||
|
||||
if (chapter.type === 'file') {
|
||||
// 读取文件内容
|
||||
const filePath = `${this.rootPath}/${book?.basic.title}/${chapter.path}`;
|
||||
const file = app.vault.getAbstractFileByPath(filePath);
|
||||
if (file instanceof TFile) {
|
||||
const fileContent = await app.vault.read(file);
|
||||
// 将Markdown转换为DOCX段落
|
||||
const contentParagraphs = this.convertMarkdownToDocx(fileContent);
|
||||
// 等待Promise解析后再添加段落
|
||||
const resolvedParagraphs = await Promise.resolve(contentParagraphs);
|
||||
paragraphs.push(...resolvedParagraphs);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理子章节
|
||||
if (chapter.children && chapter.children.length > 0) {
|
||||
const childParagraphs = await this.processChapters(app, book, chapter.children, level + 1);
|
||||
paragraphs.push(...childParagraphs);
|
||||
}
|
||||
}
|
||||
|
||||
return paragraphs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Markdown格式转换为DOCX段落
|
||||
* @param markdown Markdown格式的文本
|
||||
* @returns DOCX段落数组
|
||||
*/
|
||||
private async convertMarkdownToDocx(markdown: string): Promise<any[]> {
|
||||
const { Paragraph, TextRun, HeadingLevel, AlignmentType } = await import('docx');
|
||||
const paragraphs = [];
|
||||
|
||||
// 按行分割Markdown
|
||||
const lines = markdown.split('\n');
|
||||
let inCodeBlock = false;
|
||||
let codeBlockContent = '';
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// 处理代码块
|
||||
if (line.startsWith('```')) {
|
||||
if (inCodeBlock) {
|
||||
// 结束代码块
|
||||
paragraphs.push(
|
||||
new Paragraph({
|
||||
text: codeBlockContent,
|
||||
spacing: { before: 200, after: 200 },
|
||||
indent: { left: 720 }, // 缩进
|
||||
})
|
||||
);
|
||||
codeBlockContent = '';
|
||||
inCodeBlock = false;
|
||||
} else {
|
||||
// 开始代码块
|
||||
inCodeBlock = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
codeBlockContent += line + '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理标题
|
||||
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
||||
if (headingMatch) {
|
||||
const level = headingMatch[1].length;
|
||||
const headingText = headingMatch[2];
|
||||
paragraphs.push(
|
||||
new Paragraph({
|
||||
text: headingText,
|
||||
heading: HeadingLevel[`HEADING_${level}` as keyof typeof HeadingLevel],
|
||||
spacing: { before: 200, after: 120 }
|
||||
})
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理普通段落
|
||||
if (line.trim() !== '') {
|
||||
// 处理加粗、斜体等格式
|
||||
let processedLine = line;
|
||||
const textRuns = [];
|
||||
|
||||
// 简单处理加粗和斜体
|
||||
// 注意:这是一个简化的处理,实际上需要更复杂的解析
|
||||
const boldItalicRegex = /\*\*\*(.+?)\*\*\*/g;
|
||||
const boldRegex = /\*\*(.+?)\*\*/g;
|
||||
const italicRegex = /\*(.+?)\*/g;
|
||||
|
||||
// 替换加粗斜体
|
||||
processedLine = processedLine.replace(boldItalicRegex, (match, p1) => {
|
||||
textRuns.push(new TextRun({ text: p1, bold: true, italics: true }));
|
||||
return '';
|
||||
});
|
||||
|
||||
// 替换加粗
|
||||
processedLine = processedLine.replace(boldRegex, (match, p1) => {
|
||||
textRuns.push(new TextRun({ text: p1, bold: true }));
|
||||
return '';
|
||||
});
|
||||
|
||||
// 替换斜体
|
||||
processedLine = processedLine.replace(italicRegex, (match, p1) => {
|
||||
textRuns.push(new TextRun({ text: p1, italics: true }));
|
||||
return '';
|
||||
});
|
||||
|
||||
// 如果有剩余文本,添加为普通文本
|
||||
if (processedLine.trim() !== '') {
|
||||
textRuns.push(new TextRun({ text: processedLine }));
|
||||
}
|
||||
|
||||
// 如果没有特殊格式,直接添加为普通段落
|
||||
if (textRuns.length === 0) {
|
||||
paragraphs.push(new Paragraph({ text: line }));
|
||||
} else {
|
||||
paragraphs.push(new Paragraph({ children: textRuns }));
|
||||
}
|
||||
} else if (i > 0 && lines[i - 1].trim() !== '') {
|
||||
// 空行,但前一行不是空行,添加段落间距
|
||||
paragraphs.push(new Paragraph({ text: '', spacing: { after: 120 } }));
|
||||
}
|
||||
}
|
||||
|
||||
return paragraphs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Blob转换为base64字符串
|
||||
* @param blob Blob对象
|
||||
* @returns base64字符串
|
||||
*/
|
||||
private async blobToBase64(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
if (typeof reader.result === 'string') {
|
||||
resolve(reader.result);
|
||||
} else {
|
||||
reject(new Error('转换失败'));
|
||||
}
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
return result.canceled ? null : result.filePath;
|
||||
}
|
||||
}
|
||||
|
||||
// PDF导出策略
|
||||
export class PdfExportStrategy implements ExportStrategy {
|
||||
private rootPath: string;
|
||||
private bookSizeMap: Record<string, { width: number; height: number }> = {
|
||||
A3: { width: 841.89, height: 1190.55 }, // 297mm x 420mm
|
||||
A4: { width: 595.28, height: 841.89 }, // 210mm x 297mm
|
||||
A5: { width: 419.53, height: 595.28 }, // 148mm x 210mm
|
||||
Legal: { width: 612, height: 1008 }, // 8.5in x 14in
|
||||
Letter: { width: 612, height: 792 }, // 8.5in x 11in
|
||||
Tabloid: { width: 792, height: 1224 } // 11in x 17in
|
||||
};
|
||||
constructor(rootPath: string) {
|
||||
this.rootPath = rootPath;
|
||||
}
|
||||
|
||||
async export(app: App, book: Book, selectedChapters?: ChapterNode[]): Promise<string> {
|
||||
return `PDF导出功能开发中 - ${book.basic.title}`;
|
||||
}
|
||||
}
|
||||
async export(app: App, book: Book, options?: ExportOptions): Promise<string> {
|
||||
try {
|
||||
// 如果提供了排版后的 HTML 内容,使用 HTML 转 PDF 策略
|
||||
if (options?.useTypography && options?.htmlContent && options?.typographySettings) {
|
||||
return await this.exportHTML(book, options.htmlContent, options.typographySettings);
|
||||
}
|
||||
|
||||
export class EpubExportStrategy implements ExportStrategy {
|
||||
private rootPath: string;
|
||||
constructor(rootPath: string) {
|
||||
this.rootPath = rootPath;
|
||||
}
|
||||
|
||||
async export(app: App, book: Book, selectedChapters?: ChapterNode[]): Promise<string> {
|
||||
return `EPUB导出功能开发中 - ${book.basic.title}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出服务类
|
||||
export class ExportService {
|
||||
|
||||
private rootPath: string;
|
||||
|
||||
private strategies: Record<string, ExportStrategy>;
|
||||
|
||||
constructor(private app: App, settings: any) {
|
||||
this.rootPath = settings.defaultBookPath;
|
||||
this.strategies = {
|
||||
'txt': new TxtExportStrategy(this.rootPath),
|
||||
'docx': new DocxExportStrategy(this.rootPath),
|
||||
'pdf': new PdfExportStrategy(this.rootPath),
|
||||
'epub': new EpubExportStrategy(this.rootPath)
|
||||
};
|
||||
}
|
||||
|
||||
async exportBook(format: string, book: Book, selectedChapters?: ChapterNode[]): Promise<{ content: string, fileName: string }> {
|
||||
const strategy = this.strategies[format];
|
||||
if (!strategy) {
|
||||
throw new Error(`不支持的导出格式: ${format}`);
|
||||
// 否则返回一个提示信息
|
||||
return "PDF导出功能需要使用排版视图";
|
||||
} catch (error) {
|
||||
console.error('PDF导出错误:', error);
|
||||
throw new Error(`PDF导出失败: ${error.message}`);
|
||||
}
|
||||
|
||||
const content = await strategy.export(this.app, book, selectedChapters);
|
||||
return {
|
||||
content: content,
|
||||
fileName: `${book.basic.title}.${format}`
|
||||
};
|
||||
}
|
||||
|
||||
getSupportedFormats(): string[] {
|
||||
return Object.keys(this.strategies);
|
||||
async exportHTML(book: Book, htmlContent: string, typographySettings: TypographySettings): Promise<string> {
|
||||
try {
|
||||
// 1. 构建样式 CSS 字符串
|
||||
const style = `
|
||||
body {
|
||||
font-family: ${typographySettings.fontFamily || 'serif'};
|
||||
font-size: ${typographySettings.fontSize || '16px'};
|
||||
line-height: ${typographySettings.lineHeight || '1.75'};
|
||||
margin: ${typographySettings.margin || '2cm'};
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
.markdown-preview-view {
|
||||
max-width: 720px;
|
||||
margin: auto;
|
||||
}
|
||||
@media print {
|
||||
body {
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
.page-break {
|
||||
page-break-before: always;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// 2. 构建完整 HTML 页面(包含分页)
|
||||
const fullHtml = `
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>${book.basic.title}</title>
|
||||
<style>${style}</style>
|
||||
</head>
|
||||
<body>
|
||||
${htmlContent}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
// 3. 创建窗口加载 HTML 页面
|
||||
//@ts-ignore
|
||||
const win = new electron.remote.BrowserWindow({
|
||||
show: false, // 设置为 true 可调试
|
||||
width: 1024,
|
||||
height: 768,
|
||||
webPreferences: {
|
||||
sandbox: false,
|
||||
contextIsolation: false,
|
||||
nodeIntegration: true,
|
||||
}
|
||||
});
|
||||
|
||||
// 等待页面加载完成
|
||||
console.log('等待页面加载完成');
|
||||
const ready = new Promise<void>((resolve) => {
|
||||
win.webContents.once("did-finish-load", resolve);
|
||||
});
|
||||
await win.loadURL(`data:text/html;charset=UTF-8,${encodeURIComponent(fullHtml)}`);
|
||||
await ready;
|
||||
|
||||
// 稍等以确保样式生效
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
|
||||
// 4. 打印设置(自动分页)
|
||||
const printOptions = {
|
||||
marginsType: 1,
|
||||
pageSize: typographySettings.bookSize || "A4",
|
||||
printBackground: true,
|
||||
landscape: false,
|
||||
scale: 1.0,
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: `<div style="font-size:14px;text-align:center;width:100vw;"></div>`,
|
||||
footerTemplate: `<div style="font-size:14px;text-align:center;width:100vw;"><span class="pageNumber"></span> / <span class="totalPages"></span></div>`
|
||||
};
|
||||
|
||||
// 5. 生成 PDF Buffer
|
||||
const bodyPdfBuffer = await win.webContents.printToPDF(printOptions);
|
||||
win.close();
|
||||
|
||||
const PDFLib = await import('pdf-lib');
|
||||
|
||||
const coverDoc = await PDFLib.PDFDocument.create();
|
||||
const page = coverDoc.addPage();
|
||||
const size = this.bookSizeMap[typographySettings.bookSize || "A4"];
|
||||
page.setSize(size.width, size.height);
|
||||
|
||||
page.drawRectangle({ x: 0, y: 0, width: page.getWidth(), height: page.getHeight(), color: PDFLib.rgb(0, 0, 0) });
|
||||
page.drawText("Book", {
|
||||
x: (page.getWidth() - 10) / 2,
|
||||
y: page.getHeight() - 300, // 距离顶部100
|
||||
size: 30,
|
||||
color: PDFLib.rgb(1, 1, 1),
|
||||
});
|
||||
const coverBuffer = await coverDoc.save();
|
||||
|
||||
const finalPdf = await PDFLib.PDFDocument.create();
|
||||
const [coverPage] = await finalPdf.copyPages(await PDFLib.PDFDocument.load(coverBuffer), [0]);
|
||||
finalPdf.addPage(coverPage);
|
||||
|
||||
const bodyDoc = await PDFLib.PDFDocument.load(bodyPdfBuffer);
|
||||
const bodyPages = await finalPdf.copyPages(bodyDoc, bodyDoc.getPageIndices());
|
||||
bodyPages.forEach(p => finalPdf.addPage(p));
|
||||
|
||||
const finalBuffer = await finalPdf.save();
|
||||
|
||||
const filePath = await this.getOutputFile(book.basic.title);
|
||||
if (!filePath) return "cancelled";
|
||||
await fs.writeFile(filePath, finalBuffer);
|
||||
|
||||
return filePath;
|
||||
|
||||
} catch (err: any) {
|
||||
console.error("PDF导出错误:", err);
|
||||
throw new Error(`PDF导出失败: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private getHeadingTree(doc: Document) {
|
||||
const headings: any[] = [];
|
||||
const headingElements = doc.querySelectorAll("h1, h2, h3, h4, h5, h6");
|
||||
|
||||
headingElements.forEach((el) => {
|
||||
const level = parseInt(el.tagName.substring(1));
|
||||
const id = el.id || crypto.randomUUID();
|
||||
if (!el.id) el.id = id;
|
||||
|
||||
headings.push({
|
||||
level,
|
||||
text: el.textContent,
|
||||
id
|
||||
});
|
||||
});
|
||||
|
||||
return headings;
|
||||
}
|
||||
|
||||
private async editPDF(data: Buffer, options: any): Promise<Buffer> {
|
||||
return data;
|
||||
}
|
||||
|
||||
private async getOutputFile(filename: string): Promise<string | null> {
|
||||
//@ts-ignore
|
||||
const result = await electron.remote.dialog.showSaveDialog({
|
||||
title: "导出 PDF 文件",
|
||||
defaultPath: filename + ".pdf",
|
||||
filters: [{ name: "PDF", extensions: ["pdf"] }],
|
||||
properties: ["showOverwriteConfirmation", "createDirectory"]
|
||||
});
|
||||
|
||||
return result.canceled ? null : result.filePath;
|
||||
}
|
||||
}
|
||||
230
src/services/ImgTemplateManager.ts
Normal file
230
src/services/ImgTemplateManager.ts
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
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) {
|
||||
// 添加模板特定的类名
|
||||
element.classList.add('template-default');
|
||||
|
||||
// 设置基本布局
|
||||
const contentPages = element.querySelector('.typography-content-pages');
|
||||
if (contentPages) {
|
||||
contentPages.classList.add('default-content-layout');
|
||||
}
|
||||
|
||||
// 设置页面边距和布局
|
||||
const pages = element.querySelectorAll('.page');
|
||||
pages.forEach(page => {
|
||||
page.classList.add('default-page-layout');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 备忘录模板实现
|
||||
class NotesTemplate implements ImgTemplate {
|
||||
id = 'notes';
|
||||
name = '备忘录';
|
||||
sections = {
|
||||
cover: false,
|
||||
header: true,
|
||||
content: true as const,
|
||||
footer: false
|
||||
};
|
||||
|
||||
constructor() {}
|
||||
|
||||
render(element: HTMLElement, settings?: any) {
|
||||
// 添加模板特定的类名
|
||||
element.classList.add('template-notes');
|
||||
|
||||
// 设置备忘录风格的页眉
|
||||
const pages = element.querySelectorAll('.page');
|
||||
pages.forEach((page, index) => {
|
||||
// 添加页眉
|
||||
const header = document.createElement('div');
|
||||
header.className = 'notes-header';
|
||||
header.innerHTML = `<div class="notes-title">备忘录</div><div class="notes-date">${new Date().toLocaleDateString()}</div>`;
|
||||
|
||||
// 将页眉插入到页面内容之前
|
||||
if (page.firstChild) {
|
||||
page.insertBefore(header, page.firstChild);
|
||||
} else {
|
||||
page.appendChild(header);
|
||||
}
|
||||
|
||||
// 添加备忘录风格的线条背景
|
||||
page.classList.add('notes-page-style');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 书籍模板实现
|
||||
class BookTemplate implements ImgTemplate {
|
||||
id = 'book';
|
||||
name = '书籍';
|
||||
sections = {
|
||||
cover: true,
|
||||
header: false,
|
||||
content: true as const,
|
||||
footer: true
|
||||
};
|
||||
|
||||
constructor() {}
|
||||
|
||||
render(element: HTMLElement, settings?: any) {
|
||||
// 添加模板特定的类名
|
||||
element.classList.add('template-book');
|
||||
|
||||
// 设置书籍风格的页面
|
||||
const pages = element.querySelectorAll('.page');
|
||||
pages.forEach((page, index) => {
|
||||
// 添加书籍风格的页面样式
|
||||
page.classList.add('book-page-style');
|
||||
|
||||
// 添加页脚(页码)
|
||||
const footer = document.createElement('div');
|
||||
footer.className = 'book-footer';
|
||||
footer.innerHTML = `<div class="book-page-number">${index + 1}</div>`;
|
||||
page.appendChild(footer);
|
||||
});
|
||||
|
||||
// 设置封面样式
|
||||
const coverPage = element.querySelector('.page:first-child');
|
||||
if (coverPage) {
|
||||
coverPage.classList.add('book-cover');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 杂志模板实现
|
||||
class MagazineTemplate implements ImgTemplate {
|
||||
id = 'magazine';
|
||||
name = '杂志';
|
||||
sections = {
|
||||
cover: true,
|
||||
header: true,
|
||||
content: true as const,
|
||||
footer: true
|
||||
};
|
||||
|
||||
constructor() {}
|
||||
|
||||
render(element: HTMLElement, settings?: any) {
|
||||
// 添加模板特定的类名
|
||||
element.classList.add('template-magazine');
|
||||
|
||||
// 设置杂志风格的页面
|
||||
const pages = element.querySelectorAll('.page');
|
||||
pages.forEach((page, index) => {
|
||||
// 添加杂志风格的页面样式
|
||||
page.classList.add('magazine-page-style');
|
||||
|
||||
// 添加页眉
|
||||
const header = document.createElement('div');
|
||||
header.className = 'magazine-header';
|
||||
header.innerHTML = `<div class="magazine-section">专题</div>`;
|
||||
|
||||
// 将页眉插入到页面内容之前
|
||||
if (page.firstChild) {
|
||||
page.insertBefore(header, page.firstChild);
|
||||
} else {
|
||||
page.appendChild(header);
|
||||
}
|
||||
|
||||
// 添加页脚
|
||||
const footer = document.createElement('div');
|
||||
footer.className = 'magazine-footer';
|
||||
footer.innerHTML = `<div class="magazine-page-number">${index + 1}</div>`;
|
||||
page.appendChild(footer);
|
||||
});
|
||||
|
||||
// 设置封面样式
|
||||
const coverPage = element.querySelector('.page:first-child');
|
||||
if (coverPage) {
|
||||
coverPage.classList.add('magazine-cover');
|
||||
}
|
||||
}
|
||||
}
|
||||
246
src/services/ThemeManager.ts
Normal file
246
src/services/ThemeManager.ts
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
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 方法,使用 CSS 类而不是内联样式
|
||||
applyTheme(element: HTMLElement, themeId?: string): void {
|
||||
const theme = themeId ? this.getTheme(themeId) : this.getCurrentTheme();
|
||||
if (!theme) return;
|
||||
|
||||
// 移除所有主题相关的类
|
||||
const themeClasses = Array.from(element.classList).filter(cls => cls.startsWith('theme-'));
|
||||
themeClasses.forEach(cls => element.classList.remove(cls));
|
||||
|
||||
// 添加当前主题的类
|
||||
element.classList.add(`theme-${theme.id}`);
|
||||
|
||||
// 设置字体大小
|
||||
element.style.fontSize = `${this.currentFontSize}px`;
|
||||
}
|
||||
}
|
||||
123
src/styles/components/cover-design.css
Normal file
123
src/styles/components/cover-design.css
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
/* 封面预览样式 */
|
||||
.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: 0; /* 移除底部边距 */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
108
src/styles/components/ebook-view.css
Normal file
108
src/styles/components/ebook-view.css
Normal 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;
|
||||
}
|
||||
193
src/styles/components/paginated-engine.css
Normal file
193
src/styles/components/paginated-engine.css
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
/* 分页容器样式 */
|
||||
.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-a3 {
|
||||
width: 297mm;
|
||||
min-height: 420mm;
|
||||
max-height: 420mm;
|
||||
padding: 15mm 10mm 10mm 10mm;
|
||||
}
|
||||
|
||||
.book-size-a4 {
|
||||
width: 210mm;
|
||||
min-height: 297mm;
|
||||
max-height: 297mm;
|
||||
padding: 15mm 10mm 10mm 10mm;
|
||||
}
|
||||
|
||||
.book-size-a5 {
|
||||
width: 148mm;
|
||||
min-height: 210mm;
|
||||
max-height: 210mm;
|
||||
padding: 15mm 10mm 10mm 10mm;
|
||||
}
|
||||
|
||||
.book-size-legal {
|
||||
width: 216mm;
|
||||
min-height: 356mm;
|
||||
max-height: 356mm;
|
||||
padding: 15mm 10mm 10mm 10mm;
|
||||
}
|
||||
|
||||
.book-size-letter {
|
||||
width: 216mm;
|
||||
min-height: 279mm;
|
||||
max-height: 279mm;
|
||||
padding: 15mm 10mm 10mm 10mm;
|
||||
}
|
||||
|
||||
.book-size-tabloid {
|
||||
width: 279mm;
|
||||
min-height: 432mm;
|
||||
max-height: 432mm;
|
||||
padding: 15mm 10mm 10mm 10mm;
|
||||
}
|
||||
|
||||
.book-size-b5 {
|
||||
width: 176mm;
|
||||
min-height: 250mm;
|
||||
max-height: 250mm;
|
||||
padding: 15mm 10mm 10mm 10mm;
|
||||
}
|
||||
|
||||
.book-size-16k {
|
||||
width: 184mm;
|
||||
min-height: 260mm;
|
||||
max-height: 260mm;
|
||||
padding: 15mm 10mm 10mm 10mm;
|
||||
}
|
||||
|
||||
.book-size-custom {
|
||||
width: 180mm;
|
||||
min-height: 240mm;
|
||||
max-height: 240mm;
|
||||
padding: 15mm 10mm 10mm 10mm;
|
||||
}
|
||||
|
||||
/* 目录样式 */
|
||||
.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;
|
||||
font-size: 14px;
|
||||
font-weight: 200;
|
||||
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;
|
||||
}
|
||||
|
||||
/* 页码样式 */
|
||||
.page-number {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 新增:页码标记内容布局 */
|
||||
.marker-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
/* 页码标记样式 */
|
||||
.page-marker {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* 新增:BookSmith标识样式 */
|
||||
.booksmith-logo {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
}
|
||||
631
src/styles/components/typography-view.css
Normal file
631
src/styles/components/typography-view.css
Normal file
|
|
@ -0,0 +1,631 @@
|
|||
/* 排版视图基础样式 */
|
||||
.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: 0 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;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.typography-exit-btn {
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.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: 10px;
|
||||
max-width: 680px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
background-color: var(--background-secondary-alt);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 选择器行容器样式 */
|
||||
.typography-selectors-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap; /* 防止换行 */
|
||||
align-items: center;
|
||||
justify-content: center; /* 居中排列 */
|
||||
min-width: min-content; /* 确保内容不会被压缩 */
|
||||
max-width: 1200px; /* 设置最大宽度 */
|
||||
margin: 0 auto; /* 居中显示 */
|
||||
}
|
||||
|
||||
/* 自定义选择器容器样式 */
|
||||
.book-smith-select-container {
|
||||
position: relative;
|
||||
flex: 0 0 auto; /* 防止压缩 */
|
||||
min-width: 120px; /* 最小宽度 */
|
||||
max-width: 200px; /* 最大宽度 */
|
||||
}
|
||||
|
||||
/* 表单字段样式 */
|
||||
.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-preview-panel {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 预览容器样式 - 添加居中和缩放支持 */
|
||||
.typography-preview-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 封面和内容容器样式 */
|
||||
.typography-cover-container,
|
||||
.typography-content-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 预览内容区域 - 确保缩放时的行为正确 */
|
||||
.typography-preview {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
color: black;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: auto;
|
||||
margin: 0 auto;
|
||||
min-width: 650px;
|
||||
transform-origin: center top; /* 确保缩放以中心为基准 */
|
||||
}
|
||||
|
||||
/* 书页样式优化 */
|
||||
.book-page {
|
||||
margin: 0 auto; /* 确保居中 */
|
||||
max-width: 100%; /* 限制最大宽度 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 预览标题 */
|
||||
.typography-preview-panel h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
font-size: 18px;
|
||||
color: var(--text-normal);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 底部按钮面板样式 */
|
||||
.typography-bottom-panel {
|
||||
padding: 10px 30px 0 36px;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
background-color: var(--background-secondary-alt);
|
||||
}
|
||||
|
||||
/* 按钮容器样式 */
|
||||
.typography-buttons-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 按钮组样式 */
|
||||
.typography-buttons-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 封面按钮组样式 */
|
||||
.cover-buttons-group {
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
/* 操作按钮组样式 */
|
||||
.action-buttons-group {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
/* 原有的按钮样式保持不变 */
|
||||
.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;
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.export-btn:hover {
|
||||
background-color: var(--interactive-accent-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-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;
|
||||
}
|
||||
|
||||
/* 自定义选择器样式 */
|
||||
.book-smith-select {
|
||||
max-width: 112px;
|
||||
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;
|
||||
}
|
||||
.book-smith-book-size-select .book-smith-select{
|
||||
max-width: 76px;
|
||||
}
|
||||
.book-smith-select:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
box-shadow: 0 2px 6px var(--background-modifier-box-shadow);
|
||||
}
|
||||
|
||||
.book-smith-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.book-smith-select-arrow {
|
||||
margin-left: 8px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* 选择器下拉菜单样式 */
|
||||
.book-smith-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;
|
||||
}
|
||||
|
||||
.book-smith-select-dropdown.book-smith-show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.book-smith-select-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.book-smith-select-item:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.book-smith-select-item.book-smith-selected {
|
||||
background: var(--background-modifier-hover);
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
/* 字体大小控制组样式 */
|
||||
.book-smith-font-size-group {
|
||||
min-width: 36px;
|
||||
max-width: 120px;
|
||||
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;
|
||||
margin: 0 10px 0 1px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px var(--background-modifier-box-shadow);
|
||||
}
|
||||
|
||||
.book-smith-font-size-input {
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.book-smith-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;
|
||||
}
|
||||
|
||||
.book-smith-font-size-btn:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
/* 开本大小样式 */
|
||||
.book-size-A3 {
|
||||
width: 297mm;
|
||||
min-height: 420mm;
|
||||
max-height: 420mm;
|
||||
}
|
||||
|
||||
.book-size-A4 {
|
||||
width: 210mm;
|
||||
min-height: 297mm;
|
||||
max-height: 297mm;
|
||||
}
|
||||
|
||||
.book-size-A5 {
|
||||
width: 148mm;
|
||||
min-height: 210mm;
|
||||
max-height: 210mm;
|
||||
}
|
||||
|
||||
.book-size-Legal {
|
||||
width: 216mm;
|
||||
min-height: 356mm;
|
||||
max-height: 356mm;
|
||||
}
|
||||
|
||||
.book-size-Letter {
|
||||
width: 216mm;
|
||||
min-height: 279mm;
|
||||
max-height: 279mm;
|
||||
}
|
||||
|
||||
.book-size-Tabloid {
|
||||
width: 279mm;
|
||||
min-height: 432mm;
|
||||
max-height: 432mm;
|
||||
}
|
||||
|
||||
.book-size-B5 {
|
||||
width: 176mm;
|
||||
min-height: 250mm;
|
||||
max-height: 250mm;
|
||||
}
|
||||
|
||||
.book-size-16k {
|
||||
width: 184mm;
|
||||
min-height: 260mm;
|
||||
max-height: 260mm;
|
||||
}
|
||||
|
||||
.book-size-custom {
|
||||
width: 180mm;
|
||||
min-height: 240mm;
|
||||
max-height: 240mm;
|
||||
}
|
||||
|
||||
/* 简洁模板样式 */
|
||||
.simple-template {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
font-family: var(--font-text, sans-serif);
|
||||
max-width: 800px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.simple-template .header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.simple-template .title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.simple-template .meta {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.simple-template .abstract {
|
||||
font-style: italic;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
border-left: 3px solid #ddd;
|
||||
}
|
||||
|
||||
.simple-template .content {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.simple-template .footer {
|
||||
margin-top: 30px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #eee;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 文章模板样式 */
|
||||
.article-template {
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
font-family: var(--font-text, serif);
|
||||
max-width: 800px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.article-template .header {
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.article-template .title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.article-template .meta {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.article-template .abstract {
|
||||
font-style: italic;
|
||||
margin: 0 auto 30px;
|
||||
padding: 15px;
|
||||
background-color: #f9f9f9;
|
||||
border-left: 4px solid #ddd;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.article-template .content {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.article-template .footer {
|
||||
margin-top: 40px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.article-template blockquote {
|
||||
margin: 20px 0;
|
||||
padding: 10px 20px;
|
||||
background-color: #f9f9f9;
|
||||
border-left: 4px solid #ddd;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.article-template .copyright {
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 选择器高亮效果 */
|
||||
.selected-option {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
|
@ -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,7 @@
|
|||
@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';
|
||||
@import './settings/settings-tab.css';
|
||||
@import './modals/export-modal.css';
|
||||
|
|
@ -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);
|
||||
}
|
||||
135
src/styles/modals/export-modal.css
Normal file
135
src/styles/modals/export-modal.css
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
/* 导出模态框样式 */
|
||||
.book-smith-modal-content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.book-smith-modal-header h2 {
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding-bottom: 10px;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
/* 导出内容区域 */
|
||||
.export-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 导出标题 */
|
||||
.export-title {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
color: var(--text-normal);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* 分隔线 */
|
||||
.export-divider {
|
||||
border: none;
|
||||
height: 1px;
|
||||
background-color: var(--background-modifier-border);
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
/* 格式选择容器 */
|
||||
.export-formats-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
/* 格式按钮样式 */
|
||||
.export-format-btn {
|
||||
padding: 12px 20px;
|
||||
background-color: var(--background-secondary);
|
||||
color: var(--text-normal);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.export-format-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 8px var(--background-modifier-box-shadow);
|
||||
background-color: var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.export-format-btn.selected {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 8px var(--background-modifier-box-shadow);
|
||||
}
|
||||
|
||||
/* 按钮容器 */
|
||||
.export-buttons-container {
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 取消按钮 */
|
||||
.export-cancel-btn {
|
||||
padding: 10px 16px;
|
||||
background-color: var(--background-secondary);
|
||||
color: var(--text-normal);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.export-cancel-btn:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 确认按钮 */
|
||||
.export-confirm-btn {
|
||||
padding: 10px 16px;
|
||||
background-color: var(--interactive-accent-hover);
|
||||
color: var(--text-on-accent);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.export-confirm-btn:hover {
|
||||
background-color: var(--interactive-accent-hover);
|
||||
opacity: 0.9;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 导出模态框标题 */
|
||||
.book-smith-modal-header {
|
||||
position: relative;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* 选择导出格式标题 */
|
||||
.export-title {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
98
src/styles/templates/templates.css
Normal file
98
src/styles/templates/templates.css
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/* 默认模板样式 */
|
||||
.template-default .page {
|
||||
padding: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 备忘录模板样式 */
|
||||
.template-notes .page {
|
||||
background-color: #fffdf7;
|
||||
background-image: linear-gradient(#eee 1px, transparent 1px);
|
||||
background-size: 100% 24px;
|
||||
padding: 30px 20px;
|
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.notes-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.notes-title {
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.notes-date {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* 书籍模板样式 */
|
||||
.template-book .page {
|
||||
padding: 30px;
|
||||
background-color: #fffef5;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.book-page-style {
|
||||
position: relative;
|
||||
min-height: 800px;
|
||||
}
|
||||
|
||||
.book-footer {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.book-cover {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 杂志模板样式 */
|
||||
.template-magazine .page {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.magazine-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #333;
|
||||
}
|
||||
|
||||
.magazine-section {
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.magazine-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 20px;
|
||||
margin-top: 30px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.magazine-cover {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
98
src/styles/themes/themes.css
Normal file
98
src/styles/themes/themes.css
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/* 默认主题 */
|
||||
.theme-default {
|
||||
background-color: white;
|
||||
color: #333;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.theme-default h1 {
|
||||
font-size: 2em;
|
||||
margin-bottom: 0.5em;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.theme-default h2 {
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.theme-default h3 {
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 0.5em;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.theme-default p {
|
||||
margin-bottom: 1em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.theme-default blockquote {
|
||||
border-left: 4px solid #ddd;
|
||||
padding-left: 1em;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.theme-default ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 2em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.theme-default ol {
|
||||
list-style-type: decimal;
|
||||
padding-left: 2em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.theme-default li {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.theme-default strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.theme-default em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.theme-default del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.theme-default pre code {
|
||||
background-color: #f5f5f5;
|
||||
padding: 1em;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.theme-default code:not(pre code) {
|
||||
background-color: #f5f5f5;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* 亮色主题 */
|
||||
.theme-light {
|
||||
background-color: #f9f9f9;
|
||||
color: #333;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* 添加亮色主题的其他样式... */
|
||||
|
||||
/* 暗色主题 */
|
||||
.theme-dark {
|
||||
background-color: #222;
|
||||
color: #eee;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* 添加暗色主题的其他样式... */
|
||||
410
src/utils/PaginatedEngine.ts
Normal file
410
src/utils/PaginatedEngine.ts
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
/**
|
||||
* 基础块接口,定义分页所需的基本操作
|
||||
*/
|
||||
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) 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 + 20;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取 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();
|
||||
}
|
||||
}
|
||||
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');
|
||||
|
||||
// 将页码格式化为三位数
|
||||
const pageNum = (idx + 1).toString().padStart(3, '0');
|
||||
|
||||
// 创建包含BookSmith标识和页码的内容
|
||||
const markerContent = document.createElement('div');
|
||||
markerContent.classList.add('marker-content');
|
||||
|
||||
// 添加BookSmith标识
|
||||
const bookSmithLogo = document.createElement('span');
|
||||
bookSmithLogo.classList.add('booksmith-logo');
|
||||
bookSmithLogo.textContent = 'BookSmith';
|
||||
markerContent.appendChild(bookSmithLogo);
|
||||
|
||||
// 添加页码
|
||||
const pageMarker = document.createElement('span');
|
||||
pageMarker.textContent = format.replace("{page}", pageNum);
|
||||
markerContent.appendChild(pageMarker);
|
||||
|
||||
marker.appendChild(markerContent);
|
||||
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(contentContainer: HTMLElement, bookSize: string = 'a4'): HTMLElement[] {
|
||||
const pageHeightMap = {
|
||||
'a4': 1123, 'a5': 794, 'b5': 945, '16k': 983, 'custom': 907
|
||||
};
|
||||
const maxHeight = pageHeightMap[bookSize as keyof typeof pageHeightMap] || 1000;
|
||||
|
||||
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'));
|
||||
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}`;
|
||||
heading.id = id;
|
||||
headingsWithPages.push({
|
||||
level,
|
||||
text,
|
||||
id,
|
||||
pageNumber: pageIndex + 1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const tocPages: HTMLElement[] = [];
|
||||
|
||||
// 工具函数:创建一页目录结构
|
||||
const createTocPage = (): {
|
||||
page: HTMLElement;
|
||||
list: HTMLElement;
|
||||
} => {
|
||||
const tocPage = document.createElement('div');
|
||||
tocPage.classList.add('book-page', 'toc-page', `book-size-${bookSize}`);
|
||||
|
||||
const tocContainer = document.createElement('div');
|
||||
const tocTitle = document.createElement('h1');
|
||||
tocTitle.textContent = '目录';
|
||||
tocContainer.appendChild(tocTitle);
|
||||
|
||||
const tocList = document.createElement('div');
|
||||
tocList.classList.add('toc-list');
|
||||
tocContainer.appendChild(tocList);
|
||||
|
||||
tocPage.appendChild(tocContainer);
|
||||
document.body.appendChild(tocPage); // 必须临时挂载才能测量 scrollHeight
|
||||
|
||||
return { page: tocPage, list: tocList };
|
||||
};
|
||||
|
||||
let { page: currentPage, list: currentList } = createTocPage();
|
||||
tocPages.push(currentPage);
|
||||
|
||||
for (let i = 0; i < headingsWithPages.length; ) {
|
||||
const { level, text, id, pageNumber } = headingsWithPages[i];
|
||||
|
||||
// 构建目录项
|
||||
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().padStart(3, '0');
|
||||
itemContent.appendChild(pageRef);
|
||||
|
||||
listItem.appendChild(itemContent);
|
||||
currentList.appendChild(listItem);
|
||||
|
||||
// 判断是否溢出
|
||||
if (currentPage.scrollHeight > maxHeight) {
|
||||
currentList.removeChild(listItem); // 撤回当前项
|
||||
|
||||
// 新建一页
|
||||
const next = createTocPage();
|
||||
currentPage = next.page;
|
||||
currentList = next.list;
|
||||
tocPages.push(currentPage);
|
||||
|
||||
continue; // 不推进 i,下次重新尝试这一条
|
||||
}
|
||||
|
||||
i++; // 成功添加才推进
|
||||
}
|
||||
|
||||
// 清除挂载
|
||||
tocPages.forEach(p => document.body.contains(p) && p.remove());
|
||||
|
||||
return tocPages;
|
||||
}
|
||||
|
|
@ -2,11 +2,11 @@ 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';
|
||||
|
||||
interface ToolItem {
|
||||
icon: string;
|
||||
|
|
@ -19,6 +19,7 @@ interface ToolItem {
|
|||
export class ToolView extends ItemView {
|
||||
private normalView: HTMLElement | null = null;
|
||||
private focusView: FocusToolView | null = null;
|
||||
private typographyView: TypographyView | null = null;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, private plugin: BookSmithPlugin) {
|
||||
super(leaf);
|
||||
|
|
@ -46,13 +47,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) {
|
||||
|
|
@ -115,17 +109,10 @@ export class ToolView extends ItemView {
|
|||
// 导出发布工具组
|
||||
if (this.plugin.settings.tools.export) {
|
||||
this.createToolGroup(container, i18n.t('EXPORT_PUBLISH'), [
|
||||
{
|
||||
icon: 'edit-3',
|
||||
text: i18n.t('DESIGN_TYPOGRAPHY'),
|
||||
onClick: () => {
|
||||
new Notice(i18n.t('FEATURE_COMING_SOON', { feature: i18n.t('DESIGN_TYPOGRAPHY') }));
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'book',
|
||||
text: i18n.t('GENERATE_EBOOK'),
|
||||
onClick: () => new EbookModal(this.containerEl, this.app, this.plugin.settings).open()
|
||||
text: i18n.t('DESIGN_TYPOGRAPHY'),
|
||||
onClick: () => this.enterTypographyMode()
|
||||
},
|
||||
{
|
||||
icon: 'clock',
|
||||
|
|
@ -241,4 +228,40 @@ 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();
|
||||
}
|
||||
|
||||
// 修改 onClose 方法,确保所有视图都被正确关闭
|
||||
async onClose() {
|
||||
if (this.focusView) {
|
||||
this.focusView.remove();
|
||||
this.focusView = null;
|
||||
}
|
||||
|
||||
if (this.typographyView) {
|
||||
this.typographyView.remove();
|
||||
this.typographyView = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue