Files
note2any/src/main.ts
2025-10-08 20:05:39 +08:00

319 lines
10 KiB
TypeScript
Raw 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.

/**
* 文件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)));
}
}
}