Files
note2any/src/preview-view.ts
2025-10-08 19:45:28 +08:00

350 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 文件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';
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);
// 不在未完成 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();
}
private async performInitialization(): Promise<void> {
try {
const start = performance.now();
this.showLoading();
console.time('[PreviewView] initializeSettings');
await this.initializeSettings();
console.timeEnd('[PreviewView] initializeSettings');
console.time('[PreviewView] createManager');
await this.createManager();
console.timeEnd('[PreviewView] createManager');
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));
uevent('open');
} catch (error) {
console.error('[PreviewView] 初始化失败:', error);
new Notice('预览视图初始化失败: ' + (error instanceof Error ? error.message : String(error)));
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: error instanceof Error ? error.message : String(error) });
errorDiv.createEl('p', { text: '请尝试重新加载插件或查看控制台获取更多信息' });
}
}
/**
* 视图关闭时的回调
*/
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();
}
}
/** 外部接口:切换平台 */
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; }
}