update at 2025-10-08 19:45:28

This commit is contained in:
douboer
2025-10-08 19:45:28 +08:00
parent 5d32c0f5e7
commit 3460669602
20 changed files with 3325 additions and 101 deletions

349
src/preview-view.ts Normal file
View File

@@ -0,0 +1,349 @@
/**
* 文件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; }
}