319 lines
10 KiB
TypeScript
319 lines
10 KiB
TypeScript
/**
|
||
* 文件:main.ts
|
||
* 入口:Obsidian 插件主类,负责:
|
||
* - 视图注册 / 右键菜单扩展
|
||
* - 微信公众号与小红书发布入口调度
|
||
* - 设置加载与保存
|
||
* - 与 NotePreview / 批量发布 / 小红书登录流程衔接
|
||
*/
|
||
|
||
import { Plugin, WorkspaceLeaf, App, PluginManifest, Menu, Notice, TAbstractFile, TFile, TFolder } from 'obsidian';
|
||
import { PreviewView, VIEW_TYPE_NOTE_PREVIEW } from './preview-view';
|
||
import { NMPSettings } from './settings';
|
||
import { NoteToMpSettingTab } from './setting-tab';
|
||
import AssetsManager from './assets';
|
||
import { setVersion, uevent } from './utils';
|
||
import { WidgetsModal } from './widgets-modal';
|
||
import { BatchPublishModal } from './batch-publish-modal';
|
||
import { XiaohongshuLoginModal } from './xiaohongshu/login-modal';
|
||
import { XiaohongshuContentAdapter } from './xiaohongshu/adapter';
|
||
import { XiaohongshuAPIManager } from './xiaohongshu/api';
|
||
|
||
/**
|
||
* NoteToMpPlugin
|
||
*
|
||
* 中文说明:
|
||
* 这是插件的入口类,负责:
|
||
* - 插件生命周期管理(onload/onunload)
|
||
* - 注册自定义视图 NotePreview 用于渲染与发布文章
|
||
* - 提供多种命令:单篇发布、批量发布、插入样式组件等
|
||
* - 提供文件右键菜单扩展,支持对单文件或文件夹进行发布操作
|
||
*
|
||
* 设计决策(简要):
|
||
* - 将批量发布的 UI 放在 `BatchPublishModal` 中,命令 `note-to-mp-batch-publish` 会打开该模态框
|
||
* - 单篇发布/文件夹批量发布仍复用 `NotePreview` 的发布逻辑,避免重复实现上传流程
|
||
*/
|
||
|
||
|
||
export default class NoteToMpPlugin extends Plugin {
|
||
settings: NMPSettings;
|
||
assetsManager: AssetsManager;
|
||
ribbonIconEl: HTMLElement | null = null;
|
||
constructor(app: App, manifest: PluginManifest) {
|
||
super(app, manifest);
|
||
AssetsManager.setup(app, manifest);
|
||
this.assetsManager = AssetsManager.getInstance();
|
||
}
|
||
|
||
async loadResource() {
|
||
await this.loadSettings();
|
||
await this.assetsManager.loadAssets();
|
||
}
|
||
|
||
async onload() {
|
||
console.log('Loading NoteToMP (plugin onload start)');
|
||
setVersion(this.manifest.version);
|
||
uevent('load');
|
||
console.log('[NoteToMpPlugin] workspace.layoutReady at onload =', this.app.workspace.layoutReady);
|
||
|
||
// 先注册 view 之前,防止旧 snapshot 立即恢复创建大量视图:先临时卸载残留叶子(如果类型匹配)
|
||
try {
|
||
const legacyLeaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_NOTE_PREVIEW);
|
||
if (legacyLeaves.length > 0) {
|
||
console.log('[NoteToMpPlugin] detach legacy leaves early count=', legacyLeaves.length);
|
||
this.app.workspace.detachLeavesOfType(VIEW_TYPE_NOTE_PREVIEW);
|
||
}
|
||
} catch (e) {
|
||
console.warn('[NoteToMpPlugin] early detach failed', e);
|
||
}
|
||
this.app.workspace.onLayoutReady(async () => {
|
||
console.log('[NoteToMpPlugin] onLayoutReady callback entered');
|
||
console.time('[NoteToMpPlugin] startup:onLayoutReady→loadResource');
|
||
try {
|
||
await this.loadResource(); // 确保资源完全加载完再继续,避免后续视图初始化反复等待
|
||
} catch (e) {
|
||
console.error('[NoteToMpPlugin] loadResource 失败', e);
|
||
} finally {
|
||
console.timeEnd('[NoteToMpPlugin] startup:onLayoutReady→loadResource');
|
||
}
|
||
// 清理旧视图
|
||
this.cleanupLegacyViews();
|
||
// 取消自动打开预览视图(用于排查启动卡顿)。用户可通过图标或命令手动打开。
|
||
// console.log('[NoteToMpPlugin] 已跳过自动打开预览视图调试模式');
|
||
});
|
||
|
||
this.registerView(
|
||
VIEW_TYPE_NOTE_PREVIEW,
|
||
(leaf) => new PreviewView(leaf, this)
|
||
);
|
||
|
||
this.ribbonIconEl = this.addRibbonIcon('clipboard-paste', '复制到公众号', (evt: MouseEvent) => {
|
||
this.activateView();
|
||
});
|
||
this.ribbonIconEl.addClass('note-to-mp-plugin-ribbon-class');
|
||
|
||
this.addCommand({
|
||
id: 'note-to-mp-preview',
|
||
name: '复制到公众号',
|
||
callback: () => {
|
||
this.activateView();
|
||
}
|
||
});
|
||
|
||
this.addSettingTab(new NoteToMpSettingTab(this.app, this));
|
||
|
||
this.addCommand({
|
||
id: 'note-to-mp-widget',
|
||
name: '插入样式小部件',
|
||
callback: () => {
|
||
new WidgetsModal(this.app).open();
|
||
}
|
||
});
|
||
|
||
this.addCommand({
|
||
id: 'note-to-mp-batch-publish',
|
||
name: '批量发布文章',
|
||
callback: () => {
|
||
new BatchPublishModal(this.app, this).open();
|
||
}
|
||
});
|
||
|
||
// TODO: 重构后需要重新实现批量发布功能
|
||
// this.addCommand({
|
||
// id: 'note-to-mp-pub',
|
||
// name: '发布公众号文章',
|
||
// callback: async () => {
|
||
// await this.activateView();
|
||
// this.getNotePreview()?.postArticle();
|
||
// }
|
||
// });
|
||
|
||
// 命令:当前文件发布到微信草稿
|
||
this.addCommand({
|
||
id: 'note-to-mp-post-current',
|
||
name: '发布当前文件到公众号草稿',
|
||
callback: async () => {
|
||
const file = this.app.workspace.getActiveFile();
|
||
if (!file) { new Notice('没有活动文件'); return; }
|
||
if (file.extension.toLowerCase() !== 'md') { new Notice('只能发布 Markdown 文件'); return; }
|
||
await this.activateView();
|
||
await this.getNotePreview()?.postWechatDraft(file);
|
||
}
|
||
});
|
||
|
||
// 监听右键菜单(文件浏览器)
|
||
this.registerEvent(
|
||
this.app.workspace.on('file-menu', (menu, file) => {
|
||
// 发布到公众号草稿
|
||
menu.addItem((item) => {
|
||
item
|
||
.setTitle('发布公众号')
|
||
.setIcon('lucide-send')
|
||
.onClick(async () => {
|
||
if (file instanceof TFile) {
|
||
if (file.extension.toLowerCase() !== 'md') {
|
||
new Notice('只能发布 Markdown 文件');
|
||
return;
|
||
}
|
||
await this.activateView();
|
||
await this.getNotePreview()?.postWechatDraft(file);
|
||
}
|
||
});
|
||
});
|
||
|
||
// 发布到小红书
|
||
menu.addItem((item) => {
|
||
item
|
||
.setTitle('发布小红书')
|
||
.setIcon('lucide-heart')
|
||
.onClick(async () => {
|
||
if (file instanceof TFile) {
|
||
if (file.extension.toLowerCase() !== 'md') {
|
||
new Notice('只能发布 Markdown 文件');
|
||
return;
|
||
}
|
||
await this.publishToXiaohongshu(file);
|
||
}
|
||
});
|
||
});
|
||
})
|
||
);
|
||
}
|
||
|
||
onunload() {
|
||
console.log('Unloading NoteToMP');
|
||
// 移除 ribbon icon,避免重载插件时重复创建
|
||
if (this.ribbonIconEl) {
|
||
this.ribbonIconEl.remove();
|
||
this.ribbonIconEl = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理历史失效视图:
|
||
* 某些用户可能曾使用过旧插件构建(例如 note-mp-preview-manager),升级后残留的标签页会提示“插件不再活动”。
|
||
* 这里做一次性清理,避免用户手动关标签造成困扰。
|
||
*/
|
||
private cleanupLegacyViews() {
|
||
try {
|
||
const legacyIds = ['note-mp-preview-manager']; // 可扩展
|
||
const { workspace } = this.app;
|
||
// 遍历所有叶子,关闭可能的失效 view(无法直接匹配 id 时,仅检测报错视图类型)
|
||
workspace.getLeavesOfType(VIEW_TYPE_NOTE_PREVIEW).forEach(l => {
|
||
// 如果 view 的 plugin 不存在或 manifest id 不匹配我们当前的 id,则关闭
|
||
const anyView: any = l.view;
|
||
const vid = (anyView?.plugin?.manifest?.id) || '';
|
||
if (vid && vid !== this.manifest.id && legacyIds.includes(vid)) {
|
||
workspace.detachLeavesOfType(VIEW_TYPE_NOTE_PREVIEW);
|
||
}
|
||
});
|
||
} catch (e) {
|
||
console.warn('[NoteToMp] cleanupLegacyViews 失败', e);
|
||
}
|
||
}
|
||
|
||
async loadSettings() {
|
||
NMPSettings.loadSettings(await this.loadData());
|
||
}
|
||
|
||
async saveSettings() {
|
||
await this.saveData(NMPSettings.allSettings());
|
||
}
|
||
|
||
async activateView() {
|
||
const { workspace } = this.app;
|
||
|
||
let leaf: WorkspaceLeaf | null = null;
|
||
const leaves = workspace.getLeavesOfType(VIEW_TYPE_NOTE_PREVIEW);
|
||
|
||
if (leaves.length > 0) {
|
||
leaf = leaves[0];
|
||
} else {
|
||
leaf = workspace.getRightLeaf(false);
|
||
await leaf?.setViewState({ type: VIEW_TYPE_NOTE_PREVIEW, active: false });
|
||
}
|
||
|
||
if (leaf) workspace.revealLeaf(leaf);
|
||
}
|
||
|
||
getNotePreview(): PreviewView | null {
|
||
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_NOTE_PREVIEW);
|
||
if (leaves.length > 0) {
|
||
const leaf = leaves[0];
|
||
return leaf.view as PreviewView;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 发布到小红书
|
||
*/
|
||
async publishToXiaohongshu(file: TFile) {
|
||
try {
|
||
console.log('开始发布到小红书...', file.name);
|
||
new Notice('开始发布到小红书...');
|
||
|
||
// 获取API实例
|
||
const api = XiaohongshuAPIManager.getInstance(true);
|
||
|
||
// 检查登录状态,如果未登录则显示登录对话框
|
||
console.log('检查登录状态...');
|
||
// 暂时总是显示登录对话框进行测试
|
||
const isLoggedIn = false; // await api.checkLoginStatus();
|
||
console.log('登录状态:', isLoggedIn);
|
||
|
||
if (!isLoggedIn) {
|
||
console.log('用户未登录,显示登录对话框...');
|
||
new Notice('需要登录小红书账户');
|
||
|
||
let loginSuccess = false;
|
||
|
||
const loginModal = new XiaohongshuLoginModal(this.app, () => {
|
||
console.log('登录成功回调被调用');
|
||
loginSuccess = true;
|
||
});
|
||
|
||
console.log('打开登录模态窗口...');
|
||
await new Promise<void>((resolve) => {
|
||
const originalClose = loginModal.close;
|
||
loginModal.close = () => {
|
||
console.log('登录窗口关闭');
|
||
originalClose.call(loginModal);
|
||
resolve();
|
||
};
|
||
loginModal.open();
|
||
});
|
||
|
||
console.log('登录结果:', loginSuccess);
|
||
if (!loginSuccess) {
|
||
new Notice('登录失败,无法发布到小红书');
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 读取文件内容
|
||
const content = await this.app.vault.read(file);
|
||
|
||
// 转换内容格式
|
||
const adapter = new XiaohongshuContentAdapter();
|
||
const xiaohongshuPost = adapter.adaptMarkdownToXiaohongshu(content, {
|
||
generateTitle: true,
|
||
addStyle: true
|
||
});
|
||
|
||
// 发布文章
|
||
const result = await api.createPost(xiaohongshuPost);
|
||
|
||
if (result.success) {
|
||
new Notice('文章已成功发布到小红书!');
|
||
} else {
|
||
new Notice('发布失败: ' + result.message);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('发布到小红书失败:', error);
|
||
new Notice('发布失败: ' + (error instanceof Error ? error.message : String(error)));
|
||
}
|
||
}
|
||
}
|