375 lines
12 KiB
TypeScript
375 lines
12 KiB
TypeScript
/**
|
||
* 文件:preview-view.ts
|
||
* 作用:Obsidian 视图容器,负责与 Obsidian 框架交互
|
||
*
|
||
* 职责:
|
||
* 1. 实现 ItemView 接口,集成 Obsidian 视图系统
|
||
* 2. 管理视图生命周期(onOpen/onClose)
|
||
* 3. 监听文件变化事件
|
||
* 4. 将实际业务逻辑委托给 PreviewManager
|
||
*
|
||
* 设计原则:
|
||
* - 极简化:只保留 Obsidian 视图必需的代码
|
||
* - 单一职责:只负责视图层包装
|
||
* - 委托模式:所有业务逻辑委托给 PreviewManager
|
||
*/
|
||
|
||
import { EventRef, ItemView, WorkspaceLeaf, Plugin, TFile, Notice } from 'obsidian';
|
||
import { PreviewManager } from './preview-manager';
|
||
import { ArticleRender } from './article-render';
|
||
import { NMPSettings } from './settings';
|
||
import AssetsManager from './assets';
|
||
import { waitForLayoutReady, uevent } from './utils';
|
||
import { LocalFile } from './markdown/local-file';
|
||
import { ErrorHandler } from './core/error-handler';
|
||
import { ProgressIndicator } from './core/progress-indicator';
|
||
import { ConfigManager } from './core/config-manager';
|
||
import { ContentProcessor } from './core/content-processor';
|
||
|
||
export const VIEW_TYPE_NOTE_PREVIEW = 'note-preview';
|
||
|
||
/**
|
||
* 笔记预览视图类
|
||
*
|
||
* 这是一个简化的视图容器,实际的预览逻辑由 PreviewManager 处理
|
||
*/
|
||
export class PreviewView extends ItemView {
|
||
private plugin: Plugin;
|
||
private manager: PreviewManager | null = null;
|
||
private settings: NMPSettings;
|
||
private assetsManager: AssetsManager;
|
||
private listeners: EventRef[] = [];
|
||
|
||
// ArticleRender 相关
|
||
private styleEl: HTMLElement | null = null;
|
||
private articleDiv: HTMLDivElement | null = null;
|
||
private _articleRender: ArticleRender | null = null;
|
||
|
||
constructor(leaf: WorkspaceLeaf, plugin: Plugin) {
|
||
super(leaf);
|
||
this.plugin = plugin;
|
||
this.settings = NMPSettings.getInstance();
|
||
this.assetsManager = AssetsManager.getInstance();
|
||
}
|
||
|
||
/**
|
||
* 获取视图类型
|
||
*/
|
||
getViewType(): string {
|
||
return VIEW_TYPE_NOTE_PREVIEW;
|
||
}
|
||
|
||
/**
|
||
* 获取显示名称
|
||
*/
|
||
getDisplayText(): string {
|
||
return '笔记预览';
|
||
}
|
||
|
||
/**
|
||
* 获取图标
|
||
*/
|
||
getIcon(): string {
|
||
return 'book-open';
|
||
}
|
||
|
||
/**
|
||
* 获取 ArticleRender 实例
|
||
*/
|
||
get render(): ArticleRender {
|
||
if (!this._articleRender) {
|
||
// 创建临时容器用于 ArticleRender
|
||
if (!this.styleEl) {
|
||
this.styleEl = document.createElement('style');
|
||
}
|
||
if (!this.articleDiv) {
|
||
this.articleDiv = document.createElement('div');
|
||
}
|
||
|
||
this._articleRender = new ArticleRender(
|
||
this.app,
|
||
this,
|
||
this.styleEl,
|
||
this.articleDiv
|
||
);
|
||
|
||
// 设置默认主题和高亮
|
||
this._articleRender.currentTheme = this.settings.defaultStyle;
|
||
this._articleRender.currentHighlight = this.settings.defaultHighlight;
|
||
}
|
||
return this._articleRender;
|
||
}
|
||
|
||
/**
|
||
* 视图打开时的回调
|
||
*/
|
||
async onOpen(): Promise<void> {
|
||
console.log('[PreviewView] 视图打开 layoutReady=', this.app.workspace.layoutReady);
|
||
|
||
try {
|
||
// 不在未完成 layoutReady 时做重初始化,改为延迟
|
||
if (!this.app.workspace.layoutReady) {
|
||
this.showLoading();
|
||
console.log('[PreviewView] defer initialization until layoutReady');
|
||
this.app.workspace.onLayoutReady(() => {
|
||
// 使用微任务再推进,确保其它插件也完成
|
||
setTimeout(() => this.performInitialization(), 0);
|
||
});
|
||
return;
|
||
}
|
||
await this.performInitialization();
|
||
} catch (error) {
|
||
ErrorHandler.handle(error as Error, 'PreviewView.onOpen');
|
||
}
|
||
}
|
||
|
||
private async performInitialization(): Promise<void> {
|
||
const progress = new ProgressIndicator();
|
||
progress.start('初始化预览视图');
|
||
|
||
try {
|
||
const start = performance.now();
|
||
this.showLoading();
|
||
|
||
progress.update('初始化设置');
|
||
console.time('[PreviewView] initializeSettings');
|
||
await this.initializeSettings();
|
||
console.timeEnd('[PreviewView] initializeSettings');
|
||
|
||
progress.update('创建管理器');
|
||
console.time('[PreviewView] createManager');
|
||
await this.createManager();
|
||
console.timeEnd('[PreviewView] createManager');
|
||
|
||
progress.update('注册事件监听器');
|
||
console.time('[PreviewView] registerEventListeners');
|
||
this.registerEventListeners();
|
||
console.timeEnd('[PreviewView] registerEventListeners');
|
||
|
||
// 初始不渲染正文,等用户真正激活 / 文件切换时再渲染(懒加载)
|
||
const activeFile = this.app.workspace.getActiveFile();
|
||
if (activeFile) {
|
||
// 轻量延迟,避免首屏阻塞
|
||
setTimeout(() => {
|
||
if (this.manager) {
|
||
this.manager.setFile(activeFile);
|
||
}
|
||
}, 200);
|
||
}
|
||
|
||
console.log('[PreviewView] 初始化耗时(ms):', (performance.now() - start).toFixed(1));
|
||
progress.finish('预览视图初始化完成');
|
||
uevent('open');
|
||
} catch (error) {
|
||
progress.error('预览视图初始化失败');
|
||
ErrorHandler.handle(error as Error, 'PreviewView.performInitialization');
|
||
console.error('[PreviewView] 初始化失败', error);
|
||
this.showError('预览视图初始化失败,请检查插件设置');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 视图关闭时的回调
|
||
*/
|
||
async onClose(): Promise<void> {
|
||
console.log('[PreviewView] 视图关闭');
|
||
|
||
// 清理事件监听
|
||
this.listeners.forEach(listener => {
|
||
this.app.workspace.offref(listener);
|
||
});
|
||
this.listeners = [];
|
||
|
||
// 清理管理器
|
||
if (this.manager) {
|
||
this.manager.destroy();
|
||
this.manager = null;
|
||
}
|
||
|
||
// 清理缓存
|
||
LocalFile.fileCache.clear();
|
||
|
||
uevent('close');
|
||
}
|
||
|
||
/**
|
||
* 显示加载动画
|
||
*/
|
||
private showLoading(): void {
|
||
const container = this.containerEl.children[1];
|
||
container.empty();
|
||
const loading = container.createDiv({ cls: 'loading-wrapper' });
|
||
loading.createDiv({ cls: 'loading-spinner' });
|
||
}
|
||
|
||
/**
|
||
* 初始化设置和资源
|
||
*/
|
||
private async initializeSettings(): Promise<void> {
|
||
console.log('[PreviewView]initSettings:start');
|
||
const t0 = performance.now();
|
||
try {
|
||
// 等待布局就绪
|
||
console.log('[PreviewView]initSettings:waitForLayoutReady');
|
||
await waitForLayoutReady(this.app);
|
||
console.log('[PreviewView]initSettings:layoutReady');
|
||
|
||
// 加载设置
|
||
if (!this.settings.isLoaded) {
|
||
console.log('[PreviewView]initSettings:loadData:start');
|
||
const data = await this.plugin.loadData();
|
||
NMPSettings.loadSettings(data);
|
||
console.log('[PreviewView]initSettings:loadData:done');
|
||
} else {
|
||
console.log('[PreviewView]initSettings:settingsAlreadyLoaded');
|
||
}
|
||
|
||
// 加载资源(加超时降级)
|
||
if (!this.assetsManager.isLoaded) {
|
||
console.log('[PreviewView]initSettings:assets:load:start');
|
||
const assetPromise = this.assetsManager.loadAssets();
|
||
const timeoutMs = 8000; // 8 秒防护
|
||
let timedOut = false;
|
||
let timer: number | null = null;
|
||
const timeout = new Promise<void>((resolve) => {
|
||
timer = window.setTimeout(() => {
|
||
timedOut = true;
|
||
console.warn('[PreviewView]initSettings:assets:timeout, fallback to minimal defaults');
|
||
resolve();
|
||
}, timeoutMs);
|
||
});
|
||
await Promise.race([assetPromise.then(()=>{ /* 成功加载 */ }), timeout]);
|
||
if (!timedOut && timer !== null) {
|
||
clearTimeout(timer);
|
||
}
|
||
console.log('[PreviewView]initSettings:assets:load:end', { timedOut });
|
||
} else {
|
||
console.log('[PreviewView]initSettings:assetsAlreadyLoaded');
|
||
}
|
||
} catch (e) {
|
||
console.error('[PreviewView]initSettings:error', e);
|
||
throw e;
|
||
} finally {
|
||
console.log('[PreviewView]initSettings:done in', (performance.now() - t0).toFixed(1), 'ms');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建预览管理器
|
||
*/
|
||
private async createManager(): Promise<void> {
|
||
// 获取容器
|
||
const container = this.containerEl.children[1] as HTMLElement;
|
||
container.empty();
|
||
|
||
// 创建预览管理器
|
||
this.manager = new PreviewManager(
|
||
container,
|
||
this.app,
|
||
this.render
|
||
);
|
||
|
||
// 构建界面
|
||
await this.manager.build();
|
||
}
|
||
|
||
/**
|
||
* 注册事件监听
|
||
*/
|
||
private registerEventListeners(): void {
|
||
// 监听文件切换
|
||
this.listeners.push(
|
||
this.app.workspace.on('file-open', async (file: TFile | null) => {
|
||
await this.handleFileOpen(file);
|
||
})
|
||
);
|
||
|
||
// 监听文件修改
|
||
this.listeners.push(
|
||
this.app.vault.on('modify', async (file) => {
|
||
if (file instanceof TFile) {
|
||
await this.handleFileModify(file);
|
||
}
|
||
})
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 处理文件打开事件
|
||
*/
|
||
private async handleFileOpen(file: TFile | null): Promise<void> {
|
||
if (this.manager) {
|
||
await this.manager.setFile(file);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理文件修改事件
|
||
*/
|
||
private async handleFileModify(file: TFile): Promise<void> {
|
||
if (!this.manager) return;
|
||
|
||
const currentFile = this.manager.getCurrentFile();
|
||
if (currentFile && currentFile.path === file.path) {
|
||
// 当前文件被修改,刷新预览
|
||
await this.manager.refresh();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 渲染当前打开的文件
|
||
*/
|
||
private async renderCurrentFile(): Promise<void> {
|
||
const activeFile = this.app.workspace.getActiveFile();
|
||
if (activeFile && this.manager) {
|
||
await this.manager.setFile(activeFile);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 对外接口:设置要预览的文件
|
||
*/
|
||
async setFile(file: TFile): Promise<void> {
|
||
if (this.manager) {
|
||
await this.manager.setFile(file);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 对外接口:刷新预览
|
||
*/
|
||
async refresh(): Promise<void> {
|
||
if (this.manager) {
|
||
await this.manager.refresh();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示错误信息
|
||
*/
|
||
private showError(message: string): void {
|
||
const container = this.containerEl.children[1] as HTMLElement;
|
||
container.empty();
|
||
const errorDiv = container.createDiv({ cls: 'preview-error' });
|
||
errorDiv.createEl('h3', { text: '预览视图错误' });
|
||
errorDiv.createEl('p', { text: message });
|
||
errorDiv.createEl('p', { text: '请尝试重新加载插件或查看控制台获取更多信息' });
|
||
}
|
||
|
||
/** 外部接口:切换平台 */
|
||
async changePlatform(platform: 'wechat' | 'xiaohongshu') {
|
||
await this.manager?.switchPlatform(platform as any);
|
||
}
|
||
|
||
/** 外部接口:设置当前文件并发布到微信草稿 */
|
||
async postWechatDraft(file: TFile) {
|
||
await this.setFile(file);
|
||
await this.changePlatform('wechat');
|
||
const wechat = this.manager?.getWechatPreview();
|
||
if (!wechat) throw new Error('微信预览未初始化');
|
||
await wechat.postDraft();
|
||
}
|
||
|
||
getManager() { return this.manager; }
|
||
}
|