306 lines
8.9 KiB
TypeScript
306 lines
8.9 KiB
TypeScript
import { Notice, Plugin, MarkdownView } from 'obsidian';
|
|
import {
|
|
BlogFlareSettings,
|
|
DEFAULT_SETTINGS,
|
|
BlogFlareSettingTab,
|
|
} from './settings';
|
|
import { BlogFlareAPI } from './api';
|
|
import { MetaSuggestModal } from './suggest';
|
|
import { PullSuggestModal } from './pull_modal';
|
|
|
|
export default class BlogFlarePlugin extends Plugin {
|
|
settings!: BlogFlareSettings;
|
|
api!: BlogFlareAPI;
|
|
|
|
async onload() {
|
|
await this.loadSettings();
|
|
this.api = new BlogFlareAPI(this.settings);
|
|
|
|
// Add settings tab
|
|
this.addSettingTab(new BlogFlareSettingTab(this.app, this));
|
|
|
|
this.addCommand({
|
|
id: 'insert-category',
|
|
name: '插入分类',
|
|
checkCallback: (checking: boolean) => {
|
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
const file = activeView?.file;
|
|
if (activeView && file) {
|
|
if (!checking) {
|
|
this.api.getCategories().then(items => {
|
|
if (items.length === 0) {
|
|
new Notice('未找到分类或尚未配置 API。');
|
|
return;
|
|
}
|
|
new MetaSuggestModal(this.app, items, 'category_slug', file, true).open();
|
|
}).catch(e => {
|
|
console.error(e);
|
|
new Notice('获取分类失败。');
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'insert-tag',
|
|
name: '插入标签',
|
|
checkCallback: (checking: boolean) => {
|
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
const file = activeView?.file;
|
|
if (activeView && file) {
|
|
if (!checking) {
|
|
this.api.getTags().then(items => {
|
|
if (items.length === 0) {
|
|
new Notice('未找到标签或尚未配置 API。');
|
|
return;
|
|
}
|
|
// Tags only use name, so useSlug = false
|
|
new MetaSuggestModal(this.app, items, 'tags', file, false).open();
|
|
}).catch(e => {
|
|
console.error(e);
|
|
new Notice('获取标签失败。');
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'insert-series',
|
|
name: '插入系列',
|
|
checkCallback: (checking: boolean) => {
|
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
const file = activeView?.file;
|
|
if (activeView && file) {
|
|
if (!checking) {
|
|
this.api.getSeries().then(items => {
|
|
if (items.length === 0) {
|
|
new Notice('未找到系列或尚未配置 API。');
|
|
return;
|
|
}
|
|
new MetaSuggestModal(this.app, items, 'series_slug', file, true).open();
|
|
}).catch(e => {
|
|
console.error(e);
|
|
new Notice('获取系列失败。');
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'publish-post',
|
|
name: '发布文章',
|
|
checkCallback: (checking: boolean) => {
|
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
const file = activeView?.file;
|
|
if (activeView && file) {
|
|
if (!checking) {
|
|
void (async () => {
|
|
try {
|
|
new Notice('开始发布文章...');
|
|
const rawContent = await this.app.vault.read(file);
|
|
const cache = this.app.metadataCache.getFileCache(file);
|
|
const frontmatter = cache?.frontmatter;
|
|
|
|
let content = rawContent;
|
|
const frontmatterPosition = cache?.frontmatterPosition;
|
|
// 移除前置 YAML
|
|
if (frontmatterPosition) {
|
|
content = rawContent.substring(frontmatterPosition.end.offset).replace(/^\n+/, '');
|
|
}
|
|
|
|
const title = frontmatter?.title as string || file.basename;
|
|
const slug = frontmatter?.slug as string || file.basename.toLowerCase().replace(/\s+/g, '-');
|
|
const category_slug = frontmatter?.category_slug as string | undefined;
|
|
const publishedSlug = frontmatter?.blogflare_published_slug as string | undefined;
|
|
|
|
const payload = {
|
|
title,
|
|
slug,
|
|
content,
|
|
category_slug
|
|
};
|
|
|
|
if (publishedSlug) {
|
|
// 已发布,进行更新
|
|
const res = await this.api.updatePost(publishedSlug, payload);
|
|
if (res.success) {
|
|
new Notice('文章更新成功!');
|
|
} else {
|
|
new Notice(`更新失败: ${res.error || '未知错误'}`);
|
|
}
|
|
} else {
|
|
// 未发布,进行创建
|
|
const res = await this.api.createPost(payload);
|
|
if (res.success && res.slug) {
|
|
// 写入 blogflare_published_slug
|
|
await this.app.fileManager.processFrontMatter(file, (fm: Record<string, unknown>) => {
|
|
fm['blogflare_published_slug'] = res.slug;
|
|
});
|
|
new Notice('文章发布成功!');
|
|
} else {
|
|
new Notice(`发布失败: ${res.error || '未知错误'}`);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
new Notice('发布请求出错,请检查网络或配置。');
|
|
}
|
|
})();
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'pull-post',
|
|
name: '拉取文章',
|
|
callback: () => {
|
|
this.api.getPosts().then(items => {
|
|
if (items.length === 0) {
|
|
new Notice('未找到文章或 API 尚未配置。');
|
|
return;
|
|
}
|
|
new PullSuggestModal(this.app, items, this.api).open();
|
|
}).catch(e => {
|
|
console.error(e);
|
|
new Notice('获取文章列表失败。');
|
|
});
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'init-sync',
|
|
name: '全量拉取初始化',
|
|
callback: () => {
|
|
const mdFiles = this.app.vault.getMarkdownFiles();
|
|
if (mdFiles.length > 0) {
|
|
new Notice('全量同步失败:本地必需是空的 vault!', 5000);
|
|
return;
|
|
}
|
|
|
|
void (async () => {
|
|
try {
|
|
new Notice('开始拉取全量文章列表...');
|
|
const items = await this.api.getPosts();
|
|
if (items.length === 0) {
|
|
new Notice('未找到文章,同步结束。');
|
|
return;
|
|
}
|
|
|
|
new Notice(`发现 ${items.length} 篇文章,开始同步...`);
|
|
let successCount = 0;
|
|
|
|
for (const item of items) {
|
|
try {
|
|
const detail = await this.api.getPost(item.slug);
|
|
|
|
// 组装 Frontmatter
|
|
const frontmatterLines = [
|
|
'---',
|
|
`title: "${detail.title.replace(/"/g, '\\"')}"`,
|
|
`slug: ${detail.slug}`,
|
|
`blogflare_published_slug: ${detail.slug}`
|
|
];
|
|
if (detail.category_slug) {
|
|
frontmatterLines.push(`category_slug: ${detail.category_slug}`);
|
|
}
|
|
frontmatterLines.push('---');
|
|
|
|
const fileContent = frontmatterLines.join('\n') + '\n\n' + (detail.content || '');
|
|
|
|
const baseName = detail.title.replace(/[/\\:]/g, '-');
|
|
let dirPath = '';
|
|
const categoryDir = detail.category_name || detail.category_slug;
|
|
if (categoryDir) {
|
|
dirPath = categoryDir.replace(/[/\\:]/g, '-');
|
|
const folder = this.app.vault.getAbstractFileByPath(dirPath);
|
|
if (!folder) {
|
|
await this.app.vault.createFolder(dirPath);
|
|
}
|
|
}
|
|
const fileName = dirPath ? `${dirPath}/${baseName}.md` : `${baseName}.md`;
|
|
await this.app.vault.create(fileName, fileContent);
|
|
successCount++;
|
|
} catch (err) {
|
|
console.error(`拉取文章 ${item.slug} 失败:`, err);
|
|
}
|
|
}
|
|
|
|
new Notice(`全量同步完成!成功拉取 ${successCount}/${items.length} 篇文章。`, 5000);
|
|
} catch (e) {
|
|
console.error(e);
|
|
new Notice('全量同步请求出错,请检查网络或配置。');
|
|
}
|
|
})();
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'delete-post',
|
|
name: '下线云端文章',
|
|
checkCallback: (checking: boolean) => {
|
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
const file = activeView?.file;
|
|
if (activeView && file) {
|
|
if (!checking) {
|
|
void (async () => {
|
|
try {
|
|
const cache = this.app.metadataCache.getFileCache(file);
|
|
const publishedSlug = cache?.frontmatter?.blogflare_published_slug as string | undefined;
|
|
|
|
if (!publishedSlug) {
|
|
new Notice('本文尚未发布到 blogflare。');
|
|
return;
|
|
}
|
|
|
|
new Notice('正在尝试下线云端文章...');
|
|
const res = await this.api.deletePost(publishedSlug);
|
|
if (res.success) {
|
|
await this.app.fileManager.processFrontMatter(file, (fm: Record<string, unknown>) => {
|
|
delete fm['blogflare_published_slug'];
|
|
});
|
|
new Notice('文章已成功从云端删除!');
|
|
} else {
|
|
new Notice(`下线失败: ${res.error || '未知错误'}`);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
new Notice('删除请求出错,请检查网络或配置。');
|
|
}
|
|
})();
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
onunload() {}
|
|
|
|
async loadSettings() {
|
|
this.settings = Object.assign(
|
|
{},
|
|
DEFAULT_SETTINGS,
|
|
(await this.loadData()) as Partial<BlogFlareSettings>,
|
|
);
|
|
}
|
|
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
// re-init API with new settings
|
|
this.api = new BlogFlareAPI(this.settings);
|
|
}
|
|
}
|