diff --git a/packages/obsidian-plugin/src/api.ts b/packages/obsidian-plugin/src/api.ts index bfe0209..5514f77 100644 --- a/packages/obsidian-plugin/src/api.ts +++ b/packages/obsidian-plugin/src/api.ts @@ -12,6 +12,7 @@ export interface PostPayload { slug: string; content: string; category_slug?: string; + tags?: string[]; } export interface PostMeta { @@ -21,6 +22,7 @@ export interface PostMeta { published_at: string; category_slug?: string; category_name?: string; + tags?: string[]; } export interface PostDetail extends PostMeta { diff --git a/packages/obsidian-plugin/src/main.ts b/packages/obsidian-plugin/src/main.ts index a36ef12..045c080 100644 --- a/packages/obsidian-plugin/src/main.ts +++ b/packages/obsidian-plugin/src/main.ts @@ -1,4 +1,4 @@ -import { Notice, Plugin, MarkdownView } from 'obsidian'; +import { Notice, Plugin, MarkdownView, Menu, TFile } from 'obsidian'; import { BlogFlareSettings, DEFAULT_SETTINGS, @@ -97,64 +97,29 @@ export default class BlogFlarePlugin extends Plugin { this.addCommand({ id: 'publish-post', - name: '发布文章', + 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+/, ''); - } + void this.publishCurrentFile(); + } + return true; + } + return false; + } + }); - 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) => { - fm['blogflare_published_slug'] = res.slug; - }); - new Notice('文章发布成功!'); - } else { - new Notice(`发布失败: ${res.error || '未知错误'}`); - } - } - } catch (e) { - console.error(e); - new Notice('发布请求出错,请检查网络或配置。'); - } - })(); + this.addCommand({ + id: 'pull-current-post', + name: '下载更新当前文章', + checkCallback: (checking: boolean) => { + const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); + const file = activeView?.file; + if (activeView && file) { + if (!checking) { + void this.pullCurrentFile(); } return true; } @@ -285,6 +250,253 @@ export default class BlogFlarePlugin extends Plugin { return false; } }); + + // 统一的右下角状态栏入口 + const statusBarItem = this.addStatusBarItem(); + statusBarItem.setText('BlogFlare 同步'); + statusBarItem.addClass('mod-clickable'); + statusBarItem.addEventListener('click', (evt: MouseEvent) => { + this.showSyncMenu(evt); + }); + + // 增加左侧 Ribbon 图标 + this.addRibbonIcon('cloud-download', 'BlogFlare 同步选项', (evt: MouseEvent) => { + this.showSyncMenu(evt); + }); + } + + showSyncMenu(evt: MouseEvent) { + const menu = new Menu(); + + menu.addItem((item) => + item + .setTitle('上传当前文章') + .setIcon('upload-cloud') + .onClick(() => { + void this.publishCurrentFile(); + }) + ); + + menu.addItem((item) => + item + .setTitle('下载更新当前文章') + .setIcon('download-cloud') + .onClick(() => { + void this.pullCurrentFile(); + }) + ); + + menu.addSeparator(); + + menu.addItem((item) => + item + .setTitle('下载更新全部文章') + .setIcon('refresh-cw') + .onClick(() => { + void this.pullAllPosts(); + }) + ); + + menu.showAtMouseEvent(evt); + } + + async pullAllPosts() { + try { + new Notice('开始拉取全量文章列表...'); + const items = await this.api.getPosts(); + if (items.length === 0) { + new Notice('未找到文章,同步结束。'); + return; + } + + new Notice(`发现 ${items.length} 篇文章,开始同步...`); + let successCount = 0; + + // 收集本地已有的 blogflare_published_slug,用于匹配是新建还是覆盖 + const mdFiles = this.app.vault.getMarkdownFiles(); + const slugMap = new Map(); + for (const f of mdFiles) { + const cache = this.app.metadataCache.getFileCache(f); + const pubSlug = cache?.frontmatter?.blogflare_published_slug as string | undefined; + if (pubSlug) { + slugMap.set(pubSlug, f); + } + } + + 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}`); + } + if (detail.tags && detail.tags.length > 0) { + frontmatterLines.push(`tags:`); + for (const t of detail.tags) { + frontmatterLines.push(` - ${t}`); + } + } + frontmatterLines.push('---'); + + const fileContent = frontmatterLines.join('\n') + '\n\n' + (detail.content || ''); + + const existingFile = slugMap.get(item.slug); + if (existingFile) { + // 已存在,执行覆写 + await this.app.vault.modify(existingFile, fileContent); + } else { + // 不存在,执行新建 + 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); + } + } + // 确保文件名唯一 + let fileName = dirPath ? `${dirPath}/${baseName}.md` : `${baseName}.md`; + let counter = 1; + while (!existingFile && this.app.vault.getAbstractFileByPath(fileName)) { + fileName = dirPath ? `${dirPath}/${baseName}-${counter}.md` : `${baseName}-${counter}.md`; + counter++; + } + 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('全量同步请求出错,请检查网络或配置。'); + } + } + + async publishCurrentFile() { + const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); + const file = activeView?.file; + if (!file) { + new Notice('没有打开的 Markdown 文件'); + return; + } + + 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; + + let tags: string[] | undefined = undefined; + if (frontmatter?.tags) { + tags = Array.isArray(frontmatter.tags) ? frontmatter.tags : [frontmatter.tags as string]; + } + + const payload = { + title, + slug, + content, + category_slug, + tags + }; + + 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) => { + fm['blogflare_published_slug'] = res.slug; + }); + new Notice('文章发布成功!'); + } else { + new Notice(`发布失败: ${res.error || '未知错误'}`); + } + } + } catch (e) { + console.error(e); + new Notice('发布请求出错,请检查网络或配置。'); + } + } + + async pullCurrentFile() { + const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); + const file = activeView?.file; + if (!file) { + new Notice('没有打开的 Markdown 文件'); + return; + } + + const cache = this.app.metadataCache.getFileCache(file); + const publishedSlug = cache?.frontmatter?.blogflare_published_slug as string | undefined; + + if (!publishedSlug) { + new Notice('当前文件尚未发布到 blogflare,无法下载更新。如果想拉取其他文章,请使用"拉取文章"命令。'); + return; + } + + try { + new Notice('开始下载文章...'); + const detail = await this.api.getPost(publishedSlug); + + // 组装 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}`); + } + if (detail.tags && detail.tags.length > 0) { + frontmatterLines.push(`tags:`); + for (const t of detail.tags) { + frontmatterLines.push(` - ${t}`); + } + } + frontmatterLines.push('---'); + + const fileContent = frontmatterLines.join('\n') + '\n\n' + (detail.content || ''); + + await this.app.vault.modify(file, fileContent); + new Notice('文章下载并更新成功!'); + } catch (err) { + console.error('下载文章失败:', err); + new Notice('下载请求出错,请检查网络或配置。'); + } } onunload() {}