feat(halo-plugin): 新增文章导入、删除和标签分类管理功能
添加从本地 Markdown 文件导入创建文章的功能,支持文件预览和自动发布选项 新增文章删除命令,支持选择性删除 Halo 文章或本地文件 添加标签和分类管理功能,支持创建、编辑和删除操作 更新国际化文案,支持新功能的多种语言界面 扩展服务层以支持文章导入、删除和标签分类管理 API 调用 更新插件版本至 2.1.1 并更新作者信息
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
"version": "1.1.1",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Halo's Obsidian integration supports publishing content to Halo sites",
|
||||
"author": "Ryan Wang",
|
||||
"authorUrl": "https://github.com/ruibaby",
|
||||
"author": "刘航宇 (LHY)",
|
||||
"authorUrl": "https://github.com/LHY0125/obsidian-halo",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@halo-dev/obsidian-halo",
|
||||
"private": true,
|
||||
"version": "1.1.1",
|
||||
"version": "2.1.1",
|
||||
"description": "Halo's Obsidian integration supports publishing content to Halo sites",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@@ -10,12 +10,12 @@
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||
"check": "biome check --write src/"
|
||||
},
|
||||
"author": "@halo-dev",
|
||||
"author": "LHY",
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Ryan Wang",
|
||||
"email": "i@ryanc.cc",
|
||||
"url": "https://github.com/ruibaby"
|
||||
"name": "刘航宇",
|
||||
"email": "3364451258@qq.com",
|
||||
"url": "https://github.com/LHY0125/obsidian-halo"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import i18next from "i18next";
|
||||
import { Notice } from "obsidian";
|
||||
import HaloService from "../service";
|
||||
import { openSiteSelectionModal } from "../site-selection-modal";
|
||||
import { openDeleteConfirmModal } from "../modals/delete-confirm-modal";
|
||||
import type HaloPlugin from "../main";
|
||||
import type { HaloSite } from "../settings";
|
||||
|
||||
export interface DeleteOptions {
|
||||
deleteHalo: boolean;
|
||||
deleteLocal: boolean;
|
||||
}
|
||||
|
||||
export async function deletePost(plugin: HaloPlugin): Promise<void> {
|
||||
try {
|
||||
const { activeEditor } = plugin.app.workspace;
|
||||
|
||||
if (!activeEditor || !activeEditor.file) {
|
||||
new Notice(i18next.t("common.error_no_active_editor"));
|
||||
return;
|
||||
}
|
||||
|
||||
const matterData = plugin.app.metadataCache.getFileCache(activeEditor.file)?.frontmatter;
|
||||
|
||||
if (!matterData?.halo?.name) {
|
||||
new Notice(i18next.t("command.delete_post.error_not_published"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!matterData?.halo?.site) {
|
||||
new Notice(i18next.t("service.error_site_not_match"));
|
||||
return;
|
||||
}
|
||||
|
||||
const site = plugin.settings.sites.find((s) => s.url === matterData.halo.site);
|
||||
|
||||
if (!site) {
|
||||
new Notice(i18next.t("command.delete_post.error_no_matched_site"));
|
||||
return;
|
||||
}
|
||||
|
||||
const title = matterData.title || activeEditor.file.basename;
|
||||
|
||||
const result = await openDeleteConfirmModal(plugin, site, title, matterData.halo.name);
|
||||
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const service = new HaloService(plugin.app, plugin.settings, site);
|
||||
|
||||
if (result.options.deleteHalo) {
|
||||
const success = await service.deletePost(matterData.halo.name);
|
||||
if (!success) {
|
||||
new Notice(i18next.t("service.error_delete_failed"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.options.deleteLocal) {
|
||||
await plugin.app.vault.delete(activeEditor.file);
|
||||
}
|
||||
|
||||
new Notice(i18next.t("service.notice_delete_success"));
|
||||
} catch (error) {
|
||||
console.error("[HaloPlugin] Delete post failed:", error);
|
||||
new Notice(i18next.t("service.error_delete_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
export async function deletePostByName(plugin: HaloPlugin, site: HaloSite, postName: string, title: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await openDeleteConfirmModal(plugin, site, title, postName);
|
||||
|
||||
if (!result.success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const service = new HaloService(plugin.app, plugin.settings, site);
|
||||
|
||||
if (result.options.deleteHalo) {
|
||||
const success = await service.deletePost(postName);
|
||||
if (!success) {
|
||||
new Notice(i18next.t("service.error_delete_failed"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
new Notice(i18next.t("service.notice_delete_success"));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[HaloPlugin] Delete post by name failed:", error);
|
||||
new Notice(i18next.t("service.error_delete_failed"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import i18next from "i18next";
|
||||
import { Notice, TFile } from "obsidian";
|
||||
import { openSiteSelectionModal } from "../site-selection-modal";
|
||||
import { openFilePreviewModal, type ImportOptions } from "../modals/file-preview-modal";
|
||||
import HaloService from "../service";
|
||||
import type HaloPlugin from "../main";
|
||||
import type { HaloSite } from "../settings";
|
||||
|
||||
export async function importFromMarkdownFile(plugin: HaloPlugin): Promise<void> {
|
||||
try {
|
||||
if (plugin.settings.sites.length === 0) {
|
||||
new Notice(i18next.t("command.pull_post.error_no_sites"));
|
||||
return;
|
||||
}
|
||||
|
||||
let site: HaloSite = plugin.settings.sites[0];
|
||||
if (plugin.settings.sites.length > 1) {
|
||||
site = await openSiteSelectionModal(plugin);
|
||||
}
|
||||
|
||||
const options: ImportOptions = {
|
||||
publishImmediately: plugin.settings.publishByDefault,
|
||||
};
|
||||
|
||||
const file = await new Promise<TFile | null>((resolve) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = ".md";
|
||||
|
||||
input.onchange = async () => {
|
||||
const selectedFile = input.files?.[0];
|
||||
if (!selectedFile) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const allFiles = plugin.app.vault.getFiles();
|
||||
const matchedFile = allFiles.find(f => f.name === selectedFile.name);
|
||||
|
||||
if (matchedFile) {
|
||||
resolve(matchedFile);
|
||||
} else {
|
||||
new Notice(i18next.t("import_modal.error_file_not_in_vault"));
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
input.oncancel = () => resolve(null);
|
||||
input.click();
|
||||
});
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await openFilePreviewModal(plugin, site, file, options);
|
||||
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const service = new HaloService(plugin.app, plugin.settings, site);
|
||||
|
||||
const success = await service.importPost(file, result.options.publishImmediately);
|
||||
|
||||
if (success) {
|
||||
new Notice(i18next.t("service.notice_import_success"));
|
||||
} else {
|
||||
new Notice(i18next.t("service.error_import_failed"));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("[HaloPlugin] Import from markdown file failed:", error);
|
||||
new Notice(i18next.t("service.error_import_failed"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import i18next from "i18next";
|
||||
import { Notice } from "obsidian";
|
||||
import { openSiteSelectionModal } from "../site-selection-modal";
|
||||
import { openTagManagerModal } from "../modals/tag-manager-modal";
|
||||
import { openCategoryManagerModal } from "../modals/category-manager-modal";
|
||||
import type HaloPlugin from "../main";
|
||||
|
||||
export async function manageTags(plugin: HaloPlugin): Promise<void> {
|
||||
try {
|
||||
if (plugin.settings.sites.length === 0) {
|
||||
new Notice(i18next.t("command.pull_post.error_no_sites"));
|
||||
return;
|
||||
}
|
||||
|
||||
let site = plugin.settings.sites[0];
|
||||
if (plugin.settings.sites.length > 1) {
|
||||
site = await openSiteSelectionModal(plugin);
|
||||
}
|
||||
|
||||
openTagManagerModal(plugin, site);
|
||||
} catch (error) {
|
||||
console.error("[HaloPlugin] Manage tags failed:", error);
|
||||
new Notice(i18next.t("common.error_connection_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
export async function manageCategories(plugin: HaloPlugin): Promise<void> {
|
||||
try {
|
||||
if (plugin.settings.sites.length === 0) {
|
||||
new Notice(i18next.t("command.pull_post.error_no_sites"));
|
||||
return;
|
||||
}
|
||||
|
||||
let site = plugin.settings.sites[0];
|
||||
if (plugin.settings.sites.length > 1) {
|
||||
site = await openSiteSelectionModal(plugin);
|
||||
}
|
||||
|
||||
openCategoryManagerModal(plugin, site);
|
||||
} catch (error) {
|
||||
console.error("[HaloPlugin] Manage categories failed:", error);
|
||||
new Notice(i18next.t("common.error_connection_failed"));
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,22 @@
|
||||
"pull_post": {
|
||||
"name": "Pull posts from Halo",
|
||||
"error_no_sites": "Please configure sites first"
|
||||
},
|
||||
"list_posts": {
|
||||
"name": "View Halo posts list"
|
||||
},
|
||||
"delete_post": {
|
||||
"name": "Delete Halo post",
|
||||
"error_not_published": "This document is not published to Halo yet"
|
||||
},
|
||||
"import_markdown": {
|
||||
"name": "Import from Markdown file"
|
||||
},
|
||||
"manage_tags": {
|
||||
"name": "Manage tags"
|
||||
},
|
||||
"manage_categories": {
|
||||
"name": "Manage categories"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -53,7 +69,50 @@
|
||||
},
|
||||
"post_selection_modal": {
|
||||
"title": "Pull posts from Halo",
|
||||
"button_pull": "Pull"
|
||||
"button_pull": "Pull",
|
||||
"button_view": "View",
|
||||
"filter_all": "All",
|
||||
"filter_published": "Published",
|
||||
"filter_draft": "Draft",
|
||||
"status_published": "Published",
|
||||
"status_draft": "Draft",
|
||||
"total_items": "posts",
|
||||
"button_prev": "Previous",
|
||||
"button_next": "Next",
|
||||
"untitled": "Untitled",
|
||||
"empty": "No posts found"
|
||||
},
|
||||
"post_list_modal": {
|
||||
"title": "Halo Posts List",
|
||||
"button_pull": "Pull",
|
||||
"button_view": "View",
|
||||
"button_edit": "Edit",
|
||||
"button_delete": "Delete",
|
||||
"filter_all": "All",
|
||||
"filter_published": "Published",
|
||||
"filter_draft": "Draft",
|
||||
"status_published": "Published",
|
||||
"status_draft": "Draft",
|
||||
"total_items": "posts",
|
||||
"button_prev": "Previous",
|
||||
"button_next": "Next",
|
||||
"untitled": "Untitled",
|
||||
"empty": "No posts found",
|
||||
"confirm_delete": "Confirm Delete",
|
||||
"confirm_delete_message": "Are you sure you want to delete the post \"{title}\"? This action cannot be undone."
|
||||
},
|
||||
"post_delete_modal": {
|
||||
"title": "Delete Post",
|
||||
"message": "Are you sure you want to delete the post \"{title}\"?",
|
||||
"message_halo_only": "Only delete the post on Halo, keep local file",
|
||||
"message_local_only": "Only delete local file, keep Halo post",
|
||||
"message_both": "Delete both Halo post and local file",
|
||||
"button_delete_halo": "Delete Halo only",
|
||||
"button_delete_local": "Delete local only",
|
||||
"button_delete_both": "Delete both",
|
||||
"success": "Post deleted",
|
||||
"error_failed": "Delete failed",
|
||||
"error_no_option_selected": "Please select at least one delete option"
|
||||
},
|
||||
"site_editing_modal": {
|
||||
"title": "Halo site",
|
||||
@@ -105,10 +164,72 @@
|
||||
"error_post_not_found": "Post does not exist",
|
||||
"image_upload_success": "Image uploaded successfully",
|
||||
"image_upload_failed": "Image upload failed",
|
||||
"image_uploading": "Uploading image..."
|
||||
"image_uploading": "Uploading image...",
|
||||
"error_delete_failed": "Failed to delete post",
|
||||
"error_import_failed": "Failed to import post",
|
||||
"notice_import_success": "Imported successfully",
|
||||
"notice_delete_success": "Deleted successfully"
|
||||
},
|
||||
"file_preview_modal": {
|
||||
"title": "File Preview",
|
||||
"frontmatter": "Frontmatter Metadata",
|
||||
"content_preview": "Content Preview",
|
||||
"publish_immediately": "Publish immediately after import",
|
||||
"publish_immediately_desc": "If checked, the created post will be published directly",
|
||||
"button_import": "Import",
|
||||
"error_file_not_in_vault": "File is not in the vault"
|
||||
},
|
||||
"tag_manager_modal": {
|
||||
"title": "Tag Manager",
|
||||
"button_create": "Create Tag",
|
||||
"loading": "Loading...",
|
||||
"empty": "No tags",
|
||||
"column_name": "Name",
|
||||
"column_slug": "Slug",
|
||||
"column_color": "Color",
|
||||
"column_actions": "Actions",
|
||||
"button_edit": "Edit",
|
||||
"button_delete": "Delete",
|
||||
"prompt_name": "Enter tag name:",
|
||||
"prompt_slug": "Enter tag slug:",
|
||||
"prompt_color": "Enter tag color (hex):",
|
||||
"notice_create_success": "Tag created successfully",
|
||||
"error_create_failed": "Failed to create tag",
|
||||
"notice_update_success": "Tag updated successfully",
|
||||
"error_update_failed": "Failed to update tag",
|
||||
"confirm_delete": "Are you sure you want to delete tag \"{name}\"?",
|
||||
"notice_delete_success": "Tag deleted successfully",
|
||||
"error_delete_failed": "Failed to delete tag"
|
||||
},
|
||||
"category_manager_modal": {
|
||||
"title": "Category Manager",
|
||||
"button_create": "Create Category",
|
||||
"loading": "Loading...",
|
||||
"empty": "No categories",
|
||||
"column_name": "Name",
|
||||
"column_slug": "Slug",
|
||||
"column_priority": "Priority",
|
||||
"column_actions": "Actions",
|
||||
"button_edit": "Edit",
|
||||
"button_delete": "Delete",
|
||||
"prompt_name": "Enter category name:",
|
||||
"prompt_slug": "Enter category slug:",
|
||||
"prompt_priority": "Enter category priority:",
|
||||
"notice_create_success": "Category created successfully",
|
||||
"error_create_failed": "Failed to create category",
|
||||
"notice_update_success": "Category updated successfully",
|
||||
"error_update_failed": "Failed to update category",
|
||||
"confirm_delete": "Are you sure you want to delete category \"{name}\"?",
|
||||
"notice_delete_success": "Category deleted successfully",
|
||||
"error_delete_failed": "Failed to delete category"
|
||||
},
|
||||
"common": {
|
||||
"error_connection_failed": "Connection failed",
|
||||
"button_close": "Close"
|
||||
"button_close": "Close",
|
||||
"button_cancel": "Cancel",
|
||||
"button_confirm": "Confirm",
|
||||
"button_delete": "Delete",
|
||||
"button_import": "Import",
|
||||
"error_no_active_editor": "No file is open"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,22 @@
|
||||
"pull_post": {
|
||||
"name": "从 Halo 拉取文档",
|
||||
"error_no_sites": "请先配置站点"
|
||||
},
|
||||
"list_posts": {
|
||||
"name": "查看 Halo 文章列表"
|
||||
},
|
||||
"delete_post": {
|
||||
"name": "删除 Halo 文章",
|
||||
"error_not_published": "此文档还未发布到 Halo"
|
||||
},
|
||||
"import_markdown": {
|
||||
"name": "从 Markdown 文件导入"
|
||||
},
|
||||
"manage_tags": {
|
||||
"name": "管理标签"
|
||||
},
|
||||
"manage_categories": {
|
||||
"name": "管理分类"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -53,7 +69,50 @@
|
||||
},
|
||||
"post_selection_modal": {
|
||||
"title": "从 Halo 拉取文章",
|
||||
"button_pull": "拉取"
|
||||
"button_pull": "拉取",
|
||||
"button_view": "查看",
|
||||
"filter_all": "全部",
|
||||
"filter_published": "已发布",
|
||||
"filter_draft": "草稿",
|
||||
"status_published": "已发布",
|
||||
"status_draft": "草稿",
|
||||
"total_items": "篇",
|
||||
"button_prev": "上一页",
|
||||
"button_next": "下一页",
|
||||
"untitled": "无标题",
|
||||
"empty": "没有找到文章"
|
||||
},
|
||||
"post_list_modal": {
|
||||
"title": "Halo 文章列表",
|
||||
"button_pull": "拉取",
|
||||
"button_view": "查看",
|
||||
"button_edit": "编辑",
|
||||
"button_delete": "删除",
|
||||
"filter_all": "全部",
|
||||
"filter_published": "已发布",
|
||||
"filter_draft": "草稿",
|
||||
"status_published": "已发布",
|
||||
"status_draft": "草稿",
|
||||
"total_items": "篇",
|
||||
"button_prev": "上一页",
|
||||
"button_next": "下一页",
|
||||
"untitled": "无标题",
|
||||
"empty": "没有找到文章",
|
||||
"confirm_delete": "确认删除",
|
||||
"confirm_delete_message": "确定要删除文章「{title}」吗?此操作不可撤销。"
|
||||
},
|
||||
"post_delete_modal": {
|
||||
"title": "删除文章",
|
||||
"message": "确定要删除文章「{title}」吗?",
|
||||
"message_halo_only": "仅删除 Halo 上的文章,本地文件保留",
|
||||
"message_local_only": "仅删除本地文件,Halo 文章保留",
|
||||
"message_both": "同时删除 Halo 文章和本地文件",
|
||||
"button_delete_halo": "仅删除 Halo",
|
||||
"button_delete_local": "仅删除本地",
|
||||
"button_delete_both": "全部删除",
|
||||
"success": "文章已删除",
|
||||
"error_failed": "删除失败",
|
||||
"error_no_option_selected": "请至少选择一个删除选项"
|
||||
},
|
||||
"site_editing_modal": {
|
||||
"title": "Halo 站点",
|
||||
@@ -105,10 +164,72 @@
|
||||
"error_post_not_found": "文章不存在",
|
||||
"image_upload_success": "图片上传成功",
|
||||
"image_upload_failed": "图片上传失败",
|
||||
"image_uploading": "正在上传图片..."
|
||||
"image_uploading": "正在上传图片...",
|
||||
"error_delete_failed": "删除文章失败",
|
||||
"error_import_failed": "导入文章失败",
|
||||
"notice_import_success": "导入成功",
|
||||
"notice_delete_success": "删除成功"
|
||||
},
|
||||
"file_preview_modal": {
|
||||
"title": "文件预览",
|
||||
"frontmatter": "Frontmatter 元数据",
|
||||
"content_preview": "内容预览",
|
||||
"publish_immediately": "导入后立即发布",
|
||||
"publish_immediately_desc": "勾选后,创建的文章将直接发布",
|
||||
"button_import": "导入",
|
||||
"error_file_not_in_vault": "文件不在 Vault 中"
|
||||
},
|
||||
"tag_manager_modal": {
|
||||
"title": "标签管理",
|
||||
"button_create": "创建标签",
|
||||
"loading": "加载中...",
|
||||
"empty": "暂无标签",
|
||||
"column_name": "名称",
|
||||
"column_slug": "别名",
|
||||
"column_color": "颜色",
|
||||
"column_actions": "操作",
|
||||
"button_edit": "编辑",
|
||||
"button_delete": "删除",
|
||||
"prompt_name": "请输入标签名称:",
|
||||
"prompt_slug": "请输入标签别名:",
|
||||
"prompt_color": "请输入标签颜色(hex):",
|
||||
"notice_create_success": "标签创建成功",
|
||||
"error_create_failed": "标签创建失败",
|
||||
"notice_update_success": "标签更新成功",
|
||||
"error_update_failed": "标签更新失败",
|
||||
"confirm_delete": "确定要删除标签「{name}」吗?",
|
||||
"notice_delete_success": "标签删除成功",
|
||||
"error_delete_failed": "标签删除失败"
|
||||
},
|
||||
"category_manager_modal": {
|
||||
"title": "分类管理",
|
||||
"button_create": "创建分类",
|
||||
"loading": "加载中...",
|
||||
"empty": "暂无分类",
|
||||
"column_name": "名称",
|
||||
"column_slug": "别名",
|
||||
"column_priority": "优先级",
|
||||
"column_actions": "操作",
|
||||
"button_edit": "编辑",
|
||||
"button_delete": "删除",
|
||||
"prompt_name": "请输入分类名称:",
|
||||
"prompt_slug": "请输入分类别名:",
|
||||
"prompt_priority": "请输入分类优先级:",
|
||||
"notice_create_success": "分类创建成功",
|
||||
"error_create_failed": "分类创建失败",
|
||||
"notice_update_success": "分类更新成功",
|
||||
"error_update_failed": "分类更新失败",
|
||||
"confirm_delete": "确定要删除分类「{name}」吗?",
|
||||
"notice_delete_success": "分类删除成功",
|
||||
"error_delete_failed": "分类删除失败"
|
||||
},
|
||||
"common": {
|
||||
"error_connection_failed": "连接失败",
|
||||
"button_close": "关闭"
|
||||
"button_close": "关闭",
|
||||
"button_cancel": "取消",
|
||||
"button_confirm": "确认",
|
||||
"button_delete": "删除",
|
||||
"button_import": "导入",
|
||||
"error_no_active_editor": "没有打开的文件"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,22 @@
|
||||
"pull_post": {
|
||||
"name": "從 Halo 拉取文件",
|
||||
"error_no_sites": "請先配置網站"
|
||||
},
|
||||
"list_posts": {
|
||||
"name": "查看 Halo 文章列表"
|
||||
},
|
||||
"delete_post": {
|
||||
"name": "刪除 Halo 文章",
|
||||
"error_not_published": "此文件還未發佈到 Halo"
|
||||
},
|
||||
"import_markdown": {
|
||||
"name": "從 Markdown 文件導入"
|
||||
},
|
||||
"manage_tags": {
|
||||
"name": "管理標籤"
|
||||
},
|
||||
"manage_categories": {
|
||||
"name": "管理分類"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -53,7 +69,50 @@
|
||||
},
|
||||
"post_selection_modal": {
|
||||
"title": "從 Halo 拉取文章",
|
||||
"button_pull": "拉取"
|
||||
"button_pull": "拉取",
|
||||
"button_view": "查看",
|
||||
"filter_all": "全部",
|
||||
"filter_published": "已發布",
|
||||
"filter_draft": "草稿",
|
||||
"status_published": "已發布",
|
||||
"status_draft": "草稿",
|
||||
"total_items": "篇",
|
||||
"button_prev": "上一頁",
|
||||
"button_next": "下一頁",
|
||||
"untitled": "無標題",
|
||||
"empty": "沒有找到文章"
|
||||
},
|
||||
"post_list_modal": {
|
||||
"title": "Halo 文章列表",
|
||||
"button_pull": "拉取",
|
||||
"button_view": "查看",
|
||||
"button_edit": "編輯",
|
||||
"button_delete": "刪除",
|
||||
"filter_all": "全部",
|
||||
"filter_published": "已發布",
|
||||
"filter_draft": "草稿",
|
||||
"status_published": "已發布",
|
||||
"status_draft": "草稿",
|
||||
"total_items": "篇",
|
||||
"button_prev": "上一頁",
|
||||
"button_next": "下一頁",
|
||||
"untitled": "無標題",
|
||||
"empty": "沒有找到文章",
|
||||
"confirm_delete": "確認刪除",
|
||||
"confirm_delete_message": "確定要刪除文章「{title}」嗎?此操作不可撤銷。"
|
||||
},
|
||||
"post_delete_modal": {
|
||||
"title": "刪除文章",
|
||||
"message": "確定要刪除文章「{title}」嗎?",
|
||||
"message_halo_only": "僅刪除 Halo 上的文章,本地文件保留",
|
||||
"message_local_only": "僅刪除本地文件,Halo 文章保留",
|
||||
"message_both": "同時刪除 Halo 文章和本地文件",
|
||||
"button_delete_halo": "僅刪除 Halo",
|
||||
"button_delete_local": "僅刪除本地",
|
||||
"button_delete_both": "全部刪除",
|
||||
"success": "文章已刪除",
|
||||
"error_failed": "刪除失敗",
|
||||
"error_no_option_selected": "請至少選擇一個刪除選項"
|
||||
},
|
||||
"site_editing_modal": {
|
||||
"title": "Halo 網站",
|
||||
@@ -105,10 +164,72 @@
|
||||
"error_post_not_found": "文章不存在",
|
||||
"image_upload_success": "圖片上傳成功",
|
||||
"image_upload_failed": "圖片上傳失敗",
|
||||
"image_uploading": "正在上傳圖片..."
|
||||
"image_uploading": "正在上傳圖片...",
|
||||
"error_delete_failed": "刪除文章失敗",
|
||||
"error_import_failed": "導入文章失敗",
|
||||
"notice_import_success": "導入成功",
|
||||
"notice_delete_success": "刪除成功"
|
||||
},
|
||||
"file_preview_modal": {
|
||||
"title": "文件預覽",
|
||||
"frontmatter": "Frontmatter 元數據",
|
||||
"content_preview": "內容預覽",
|
||||
"publish_immediately": "導入後立即發布",
|
||||
"publish_immediately_desc": "勾選後,創建的文章將直接發布",
|
||||
"button_import": "導入",
|
||||
"error_file_not_in_vault": "文件不在 Vault 中"
|
||||
},
|
||||
"tag_manager_modal": {
|
||||
"title": "標籤管理",
|
||||
"button_create": "創建標籤",
|
||||
"loading": "加載中...",
|
||||
"empty": "暫無標籤",
|
||||
"column_name": "名稱",
|
||||
"column_slug": "別名",
|
||||
"column_color": "顏色",
|
||||
"column_actions": "操作",
|
||||
"button_edit": "編輯",
|
||||
"button_delete": "刪除",
|
||||
"prompt_name": "請輸入標籤名稱:",
|
||||
"prompt_slug": "請輸入標籤別名:",
|
||||
"prompt_color": "請輸入標籤顏色(hex):",
|
||||
"notice_create_success": "標籤創建成功",
|
||||
"error_create_failed": "標籤創建失敗",
|
||||
"notice_update_success": "標籤更新成功",
|
||||
"error_update_failed": "標籤更新失敗",
|
||||
"confirm_delete": "確定要刪除標籤「{name}」嗎?",
|
||||
"notice_delete_success": "標籤刪除成功",
|
||||
"error_delete_failed": "標籤刪除失敗"
|
||||
},
|
||||
"category_manager_modal": {
|
||||
"title": "分類管理",
|
||||
"button_create": "創建分類",
|
||||
"loading": "加載中...",
|
||||
"empty": "暫無分類",
|
||||
"column_name": "名稱",
|
||||
"column_slug": "別名",
|
||||
"column_priority": "優先級",
|
||||
"column_actions": "操作",
|
||||
"button_edit": "編輯",
|
||||
"button_delete": "刪除",
|
||||
"prompt_name": "請輸入分類名稱:",
|
||||
"prompt_slug": "請輸入分類別名:",
|
||||
"prompt_priority": "請輸入分類優先級:",
|
||||
"notice_create_success": "分類創建成功",
|
||||
"error_create_failed": "分類創建失敗",
|
||||
"notice_update_success": "分類更新成功",
|
||||
"error_update_failed": "分類更新失敗",
|
||||
"confirm_delete": "確定要刪除分類「{name}」嗎?",
|
||||
"notice_delete_success": "分類刪除成功",
|
||||
"error_delete_failed": "分類刪除失敗"
|
||||
},
|
||||
"common": {
|
||||
"error_connection_failed": "連接失敗",
|
||||
"button_close": "關閉"
|
||||
"button_close": "關閉",
|
||||
"button_cancel": "取消",
|
||||
"button_confirm": "確認",
|
||||
"button_delete": "刪除",
|
||||
"button_import": "導入",
|
||||
"error_no_active_editor": "沒有打開的文件"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,11 @@ import { Notice, Plugin, moment } from "obsidian";
|
||||
import { resources } from "./i18n";
|
||||
import { addHaloIcon } from "./icons";
|
||||
import { openPostSelectionModal } from "./post-selection-model";
|
||||
import HaloService from "./service";
|
||||
import { importFromMarkdownFile } from "./commands/import-markdown";
|
||||
import { deletePost } from "./commands/delete-post";
|
||||
import { manageTags, manageCategories } from "./commands/manage-taxonomy";
|
||||
import { DEFAULT_SETTINGS, type HaloSetting, HaloSettingTab, type HaloSite } from "./settings";
|
||||
import HaloService from "./service";
|
||||
import { openSiteSelectionModal } from "./site-selection-modal";
|
||||
|
||||
export default class HaloPlugin extends Plugin {
|
||||
@@ -106,6 +109,38 @@ export default class HaloPlugin extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "import-markdown",
|
||||
name: i18next.t("command.import_markdown.name"),
|
||||
callback: async () => {
|
||||
await importFromMarkdownFile(this);
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "delete-post",
|
||||
name: i18next.t("command.delete_post.name"),
|
||||
editorCallback: async () => {
|
||||
await deletePost(this);
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "manage-tags",
|
||||
name: i18next.t("command.manage_tags.name"),
|
||||
callback: async () => {
|
||||
await manageTags(this);
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "manage-categories",
|
||||
name: i18next.t("command.manage_categories.name"),
|
||||
callback: async () => {
|
||||
await manageCategories(this);
|
||||
},
|
||||
});
|
||||
|
||||
this.addSettingTab(new HaloSettingTab(this));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import i18next from "i18next";
|
||||
import { Modal, Notice, Setting } from "obsidian";
|
||||
import type HaloPlugin from "../main";
|
||||
import type { HaloSite } from "../settings";
|
||||
import HaloService from "../service";
|
||||
|
||||
export function openCategoryManagerModal(plugin: HaloPlugin, site: HaloSite): void {
|
||||
const modal = new CategoryManagerModal(plugin, site);
|
||||
modal.open();
|
||||
}
|
||||
|
||||
class CategoryManagerModal extends Modal {
|
||||
private categories: any[] = [];
|
||||
private loading = true;
|
||||
|
||||
constructor(
|
||||
private readonly plugin: HaloPlugin,
|
||||
private readonly site: HaloSite,
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
contentEl.createEl("h2", {
|
||||
text: i18next.t("category_manager_modal.title"),
|
||||
});
|
||||
|
||||
const actionsEl = contentEl.createDiv("category-manager-actions");
|
||||
|
||||
new Setting(actionsEl)
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("category_manager_modal.button_create"))
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.showCreateCategoryDialog();
|
||||
});
|
||||
})
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("common.button_close"))
|
||||
.onClick(() => this.close());
|
||||
});
|
||||
|
||||
const listEl = contentEl.createDiv("category-list");
|
||||
listEl.createEl("p", { text: i18next.t("category_manager_modal.loading") });
|
||||
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
this.categories = await service.getCategories();
|
||||
this.loading = false;
|
||||
this.renderCategoryList();
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("common.error_connection_failed"));
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
private renderCategoryList() {
|
||||
const { contentEl } = this;
|
||||
const listEl = contentEl.querySelector(".category-list") as HTMLElement;
|
||||
|
||||
if (!listEl) return;
|
||||
|
||||
listEl.empty();
|
||||
|
||||
if (this.categories.length === 0) {
|
||||
listEl.createEl("p", { text: i18next.t("category_manager_modal.empty") });
|
||||
return;
|
||||
}
|
||||
|
||||
const table = listEl.createEl("table", { cls: "category-table" });
|
||||
const header = table.createEl("tr");
|
||||
header.createEl("th", { text: i18next.t("category_manager_modal.column_name") });
|
||||
header.createEl("th", { text: i18next.t("category_manager_modal.column_slug") });
|
||||
header.createEl("th", { text: i18next.t("category_manager_modal.column_priority") });
|
||||
header.createEl("th", { text: i18next.t("category_manager_modal.column_actions") });
|
||||
|
||||
for (const category of this.categories) {
|
||||
const row = table.createEl("tr");
|
||||
|
||||
row.createEl("td", { text: category.spec.displayName });
|
||||
row.createEl("td", { text: category.spec.slug });
|
||||
row.createEl("td", { text: String(category.spec.priority || 0) });
|
||||
|
||||
const actionsCell = row.createEl("td");
|
||||
actionsCell.createEl("button", {
|
||||
text: i18next.t("category_manager_modal.button_edit"),
|
||||
cls: "category-action-btn",
|
||||
}).addEventListener("click", () => {
|
||||
this.showEditCategoryDialog(category);
|
||||
});
|
||||
|
||||
actionsCell.createEl("button", {
|
||||
text: i18next.t("category_manager_modal.button_delete"),
|
||||
cls: "category-action-btn danger",
|
||||
}).addEventListener("click", () => {
|
||||
this.showDeleteCategoryDialog(category);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async showCreateCategoryDialog() {
|
||||
const name = prompt(i18next.t("category_manager_modal.prompt_name"));
|
||||
if (!name) return;
|
||||
|
||||
const slug = prompt(i18next.t("category_manager_modal.prompt_slug"), this.generateSlug(name));
|
||||
if (!slug) return;
|
||||
|
||||
const priority = prompt(i18next.t("category_manager_modal.prompt_priority"), "0");
|
||||
if (priority === null) return;
|
||||
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
await service.createCategory(name, slug, parseInt(priority) || 0);
|
||||
new Notice(i18next.t("category_manager_modal.notice_create_success"));
|
||||
|
||||
this.categories = await service.getCategories();
|
||||
this.renderCategoryList();
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("category_manager_modal.error_create_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
private async showEditCategoryDialog(category: any) {
|
||||
const name = prompt(i18next.t("category_manager_modal.prompt_name"), category.spec.displayName);
|
||||
if (name === null) return;
|
||||
|
||||
const slug = prompt(i18next.t("category_manager_modal.prompt_slug"), category.spec.slug);
|
||||
if (slug === null) return;
|
||||
|
||||
const priority = prompt(i18next.t("category_manager_modal.prompt_priority"), String(category.spec.priority || 0));
|
||||
if (priority === null) return;
|
||||
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
await service.updateCategory(category.metadata.name, name, slug, parseInt(priority) || 0);
|
||||
new Notice(i18next.t("category_manager_modal.notice_update_success"));
|
||||
|
||||
this.categories = await service.getCategories();
|
||||
this.renderCategoryList();
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("category_manager_modal.error_update_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
private async showDeleteCategoryDialog(category: any) {
|
||||
if (!confirm(i18next.t("category_manager_modal.confirm_delete", { name: category.spec.displayName }))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
await service.deleteCategory(category.metadata.name);
|
||||
new Notice(i18next.t("category_manager_modal.notice_delete_success"));
|
||||
|
||||
this.categories = await service.getCategories();
|
||||
this.renderCategoryList();
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("category_manager_modal.error_delete_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
private generateSlug(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import i18next from "i18next";
|
||||
import { Modal, Notice, Setting } from "obsidian";
|
||||
import type HaloPlugin from "../main";
|
||||
import type { HaloSite } from "../settings";
|
||||
|
||||
export interface DeleteResult {
|
||||
success: boolean;
|
||||
options: {
|
||||
deleteHalo: boolean;
|
||||
deleteLocal: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export function openDeleteConfirmModal(
|
||||
plugin: HaloPlugin,
|
||||
site: HaloSite,
|
||||
title: string,
|
||||
postName: string,
|
||||
): Promise<DeleteResult> {
|
||||
return new Promise((resolve) => {
|
||||
const modal = new DeleteConfirmModal(plugin, site, title, postName, (result) => {
|
||||
resolve(result);
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
|
||||
class DeleteConfirmModal extends Modal {
|
||||
private deleteHalo = true;
|
||||
private deleteLocal = false;
|
||||
|
||||
constructor(
|
||||
private readonly plugin: HaloPlugin,
|
||||
private readonly site: HaloSite,
|
||||
private readonly title: string,
|
||||
private readonly postName: string,
|
||||
private readonly onResult: (result: DeleteResult) => void,
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
contentEl.createEl("h2", {
|
||||
text: i18next.t("post_delete_modal.title"),
|
||||
});
|
||||
|
||||
contentEl.createEl("p", {
|
||||
text: i18next.t("post_delete_modal.message", { title: this.title }),
|
||||
cls: "delete-message",
|
||||
});
|
||||
|
||||
const optionsEl = contentEl.createDiv("delete-options");
|
||||
|
||||
new Setting(optionsEl)
|
||||
.setName(i18next.t("post_delete_modal.message_halo_only").split(",")[0])
|
||||
.setDesc(i18next.t("post_delete_modal.message_halo_only").split(",").slice(1).join(","))
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(this.deleteHalo).onChange((value) => {
|
||||
this.deleteHalo = value;
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(optionsEl)
|
||||
.setName(i18next.t("post_delete_modal.message_local_only").split(",")[0])
|
||||
.setDesc(i18next.t("post_delete_modal.message_local_only").split(",").slice(1).join(","))
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(this.deleteLocal).onChange((value) => {
|
||||
this.deleteLocal = value;
|
||||
});
|
||||
});
|
||||
|
||||
const hasLocalFile = this.plugin.app.metadataCache.getFileCache(
|
||||
Array.from(this.plugin.app.vault.getFiles()).find(f => f.basename === this.title) || this.plugin.app.vault.getAbstractFileByPath(`/${this.title}.md`) as any
|
||||
);
|
||||
|
||||
if (!hasLocalFile) {
|
||||
const localFile = this.plugin.app.vault.getAbstractFileByPath(`/${this.title}.md`);
|
||||
if (!localFile) {
|
||||
const localToggle = optionsEl.querySelector('.setting-item:nth-child(2) input[type="checkbox"]') as HTMLInputElement;
|
||||
if (localToggle) {
|
||||
localToggle.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const actionsEl = contentEl.createDiv("delete-actions");
|
||||
|
||||
new Setting(actionsEl)
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("post_delete_modal.button_delete_both"))
|
||||
.setWarning()
|
||||
.onClick(() => {
|
||||
if (!this.deleteHalo && !this.deleteLocal) {
|
||||
new Notice(i18next.t("post_delete_modal.error_no_option_selected"));
|
||||
return;
|
||||
}
|
||||
this.onResult({
|
||||
success: true,
|
||||
options: {
|
||||
deleteHalo: this.deleteHalo,
|
||||
deleteLocal: this.deleteLocal,
|
||||
},
|
||||
});
|
||||
this.close();
|
||||
});
|
||||
})
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("common.button_cancel"))
|
||||
.onClick(() => {
|
||||
this.onResult({
|
||||
success: false,
|
||||
options: {
|
||||
deleteHalo: false,
|
||||
deleteLocal: false,
|
||||
},
|
||||
});
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import i18next from "i18next";
|
||||
import { Modal, Notice, Setting } from "obsidian";
|
||||
import type { TFile } from "obsidian";
|
||||
import type HaloPlugin from "../main";
|
||||
import type { HaloSite } from "../settings";
|
||||
|
||||
export interface ImportOptions {
|
||||
publishImmediately: boolean;
|
||||
}
|
||||
|
||||
export interface ImportResult {
|
||||
file: TFile;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export function openFilePreviewModal(
|
||||
plugin: HaloPlugin,
|
||||
site: HaloSite,
|
||||
file: TFile,
|
||||
options: ImportOptions,
|
||||
): Promise<ImportResult> {
|
||||
return new Promise((resolve) => {
|
||||
const modal = new FilePreviewModal(plugin, site, file, options, (result) => {
|
||||
resolve(result);
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
|
||||
class FilePreviewModal extends Modal {
|
||||
constructor(
|
||||
private readonly plugin: HaloPlugin,
|
||||
private readonly site: HaloSite,
|
||||
private readonly file: TFile,
|
||||
private readonly options: ImportOptions,
|
||||
private readonly onResult: (result: ImportResult) => void,
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
const headerEl = contentEl.createDiv("file-preview-header");
|
||||
headerEl.createEl("h2", {
|
||||
text: i18next.t("file_preview_modal.title"),
|
||||
});
|
||||
headerEl.createEl("p", {
|
||||
text: this.file.path,
|
||||
cls: "file-path",
|
||||
});
|
||||
|
||||
try {
|
||||
const content = await this.app.vault.read(this.file);
|
||||
const matterData = this.app.metadataCache.getFileCache(this.file)?.frontmatter;
|
||||
const frontmatterPosition = this.app.metadataCache.getFileCache(this.file)?.frontmatterPosition;
|
||||
|
||||
const previewContent = contentEl.createDiv("file-preview-content");
|
||||
|
||||
if (matterData) {
|
||||
const frontmatterEl = previewContent.createDiv("frontmatter-section");
|
||||
frontmatterEl.createEl("h3", {
|
||||
text: i18next.t("file_preview_modal.frontmatter"),
|
||||
});
|
||||
|
||||
const frontmatterList = frontmatterEl.createEl("ul");
|
||||
if (matterData.title) {
|
||||
frontmatterList.createEl("li", { text: `Title: ${matterData.title}` });
|
||||
}
|
||||
if (matterData.slug) {
|
||||
frontmatterList.createEl("li", { text: `Slug: ${matterData.slug}` });
|
||||
}
|
||||
if (matterData.tags) {
|
||||
frontmatterList.createEl("li", { text: `Tags: ${Array.isArray(matterData.tags) ? matterData.tags.join(", ") : matterData.tags}` });
|
||||
}
|
||||
if (matterData.categories) {
|
||||
frontmatterList.createEl("li", { text: `Categories: ${Array.isArray(matterData.categories) ? matterData.categories.join(", ") : matterData.categories}` });
|
||||
}
|
||||
}
|
||||
|
||||
const contentPreview = previewContent.createDiv("content-preview");
|
||||
contentPreview.createEl("h3", {
|
||||
text: i18next.t("file_preview_modal.content_preview"),
|
||||
});
|
||||
|
||||
const contentText = frontmatterPosition
|
||||
? content.slice(frontmatterPosition.end.offset, Math.min(content.length, 500))
|
||||
: content.slice(0, 500);
|
||||
|
||||
contentPreview.createEl("pre", {
|
||||
text: contentText + (content.length > 500 ? "..." : ""),
|
||||
cls: "content-text",
|
||||
});
|
||||
|
||||
const actionsEl = contentEl.createDiv("file-preview-actions");
|
||||
|
||||
const publishToggle = new Setting(actionsEl)
|
||||
.setName(i18next.t("file_preview_modal.publish_immediately"))
|
||||
.setDesc(i18next.t("file_preview_modal.publish_immediately_desc"))
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(this.options.publishImmediately).onChange((value) => {
|
||||
this.options.publishImmediately = value;
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(actionsEl)
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("file_preview_modal.button_import"))
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.onResult({ file: this.file, success: true });
|
||||
this.close();
|
||||
});
|
||||
})
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("common.button_cancel"))
|
||||
.onClick(() => {
|
||||
this.onResult({ file: this.file, success: false });
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("common.error_connection_failed"));
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import i18next from "i18next";
|
||||
import { Modal, Notice, Setting } from "obsidian";
|
||||
import type HaloPlugin from "../main";
|
||||
import type { HaloSite } from "../settings";
|
||||
import HaloService from "../service";
|
||||
|
||||
export function openTagManagerModal(plugin: HaloPlugin, site: HaloSite): void {
|
||||
const modal = new TagManagerModal(plugin, site);
|
||||
modal.open();
|
||||
}
|
||||
|
||||
class TagManagerModal extends Modal {
|
||||
private tags: any[] = [];
|
||||
private loading = true;
|
||||
|
||||
constructor(
|
||||
private readonly plugin: HaloPlugin,
|
||||
private readonly site: HaloSite,
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
contentEl.createEl("h2", {
|
||||
text: i18next.t("tag_manager_modal.title"),
|
||||
});
|
||||
|
||||
const actionsEl = contentEl.createDiv("tag-manager-actions");
|
||||
|
||||
new Setting(actionsEl)
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("tag_manager_modal.button_create"))
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.showCreateTagDialog();
|
||||
});
|
||||
})
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("common.button_close"))
|
||||
.onClick(() => this.close());
|
||||
});
|
||||
|
||||
const listEl = contentEl.createDiv("tag-list");
|
||||
listEl.createEl("p", { text: i18next.t("tag_manager_modal.loading") });
|
||||
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
this.tags = await service.getTags();
|
||||
this.loading = false;
|
||||
this.renderTagList();
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("common.error_connection_failed"));
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
private renderTagList() {
|
||||
const { contentEl } = this;
|
||||
const listEl = contentEl.querySelector(".tag-list") as HTMLElement;
|
||||
|
||||
if (!listEl) return;
|
||||
|
||||
listEl.empty();
|
||||
|
||||
if (this.tags.length === 0) {
|
||||
listEl.createEl("p", { text: i18next.t("tag_manager_modal.empty") });
|
||||
return;
|
||||
}
|
||||
|
||||
const table = listEl.createEl("table", { cls: "tag-table" });
|
||||
const header = table.createEl("tr");
|
||||
header.createEl("th", { text: i18next.t("tag_manager_modal.column_name") });
|
||||
header.createEl("th", { text: i18next.t("tag_manager_modal.column_slug") });
|
||||
header.createEl("th", { text: i18next.t("tag_manager_modal.column_color") });
|
||||
header.createEl("th", { text: i18next.t("tag_manager_modal.column_actions") });
|
||||
|
||||
for (const tag of this.tags) {
|
||||
const row = table.createEl("tr");
|
||||
|
||||
row.createEl("td", { text: tag.spec.displayName });
|
||||
row.createEl("td", { text: tag.spec.slug });
|
||||
|
||||
const colorCell = row.createEl("td");
|
||||
colorCell.createEl("span", {
|
||||
text: tag.spec.color || "#ffffff",
|
||||
cls: "tag-color-preview",
|
||||
attr: { style: `background-color: ${tag.spec.color || "#ffffff"}` }
|
||||
});
|
||||
|
||||
const actionsCell = row.createEl("td");
|
||||
actionsCell.createEl("button", {
|
||||
text: i18next.t("tag_manager_modal.button_edit"),
|
||||
cls: "tag-action-btn",
|
||||
}).addEventListener("click", () => {
|
||||
this.showEditTagDialog(tag);
|
||||
});
|
||||
|
||||
actionsCell.createEl("button", {
|
||||
text: i18next.t("tag_manager_modal.button_delete"),
|
||||
cls: "tag-action-btn danger",
|
||||
}).addEventListener("click", () => {
|
||||
this.showDeleteTagDialog(tag);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async showCreateTagDialog() {
|
||||
const name = prompt(i18next.t("tag_manager_modal.prompt_name"));
|
||||
if (!name) return;
|
||||
|
||||
const slug = prompt(i18next.t("tag_manager_modal.prompt_slug"), this.generateSlug(name));
|
||||
if (!slug) return;
|
||||
|
||||
const color = prompt(i18next.t("tag_manager_modal.prompt_color"), "#4A90E2");
|
||||
if (!color) return;
|
||||
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
await service.createTag(name, slug, color);
|
||||
new Notice(i18next.t("tag_manager_modal.notice_create_success"));
|
||||
|
||||
this.tags = await service.getTags();
|
||||
this.renderTagList();
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("tag_manager_modal.error_create_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
private async showEditTagDialog(tag: any) {
|
||||
const name = prompt(i18next.t("tag_manager_modal.prompt_name"), tag.spec.displayName);
|
||||
if (name === null) return;
|
||||
|
||||
const slug = prompt(i18next.t("tag_manager_modal.prompt_slug"), tag.spec.slug);
|
||||
if (slug === null) return;
|
||||
|
||||
const color = prompt(i18next.t("tag_manager_modal.prompt_color"), tag.spec.color || "#4A90E2");
|
||||
if (color === null) return;
|
||||
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
await service.updateTag(tag.metadata.name, name, slug, color);
|
||||
new Notice(i18next.t("tag_manager_modal.notice_update_success"));
|
||||
|
||||
this.tags = await service.getTags();
|
||||
this.renderTagList();
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("tag_manager_modal.error_update_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
private async showDeleteTagDialog(tag: any) {
|
||||
if (!confirm(i18next.t("tag_manager_modal.confirm_delete", { name: tag.spec.displayName }))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
await service.deleteTag(tag.metadata.name);
|
||||
new Notice(i18next.t("tag_manager_modal.notice_delete_success"));
|
||||
|
||||
this.tags = await service.getTags();
|
||||
this.renderTagList();
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("tag_manager_modal.error_delete_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
private generateSlug(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,12 @@ export function openPostSelectionModal(plugin: HaloPlugin, site: HaloSite): Prom
|
||||
}
|
||||
|
||||
class PostSelectionModal extends Modal {
|
||||
private currentPage = 1;
|
||||
private pageSize = 20;
|
||||
private totalItems = 0;
|
||||
private posts: ListedPost[] = [];
|
||||
private filter: "all" | "published" | "draft" = "all";
|
||||
|
||||
constructor(
|
||||
private readonly plugin: HaloPlugin,
|
||||
private readonly site: HaloSite,
|
||||
@@ -22,51 +28,173 @@ class PostSelectionModal extends Modal {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
async onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
const renderPostList = (): void => {
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h2", {
|
||||
text: i18next.t("post_selection_modal.title"),
|
||||
});
|
||||
|
||||
contentEl.createEl("h2", {
|
||||
text: i18next.t("post_selection_modal.title"),
|
||||
});
|
||||
await this.loadPosts();
|
||||
this.render();
|
||||
}
|
||||
|
||||
requestUrl({
|
||||
url: `${this.site.url}/apis/uc.api.content.halo.run/v1alpha1/posts?labelSelector=content.halo.run%2Fdeleted%3Dfalse`,
|
||||
private async loadPosts() {
|
||||
try {
|
||||
let labelSelector = "content.halo.run/deleted=false";
|
||||
|
||||
if (this.filter === "published") {
|
||||
labelSelector = "content.halo.run/deleted=false,content.halo.run/published=true";
|
||||
} else if (this.filter === "draft") {
|
||||
labelSelector = "content.halo.run/deleted=false,content.halo.run/published=false";
|
||||
}
|
||||
|
||||
const response = await requestUrl({
|
||||
url: `${this.site.url}/apis/uc.api.content.halo.run/v1alpha1/posts?labelSelector=${encodeURIComponent(labelSelector)}&page=${this.currentPage}&size=${this.pageSize}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.site.token}`,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
const posts: ListedPost[] = response.json.items;
|
||||
});
|
||||
|
||||
for (const post of posts) {
|
||||
const setting = new Setting(contentEl).setName(post.post.spec.title).setDesc(post.post.spec.slug);
|
||||
this.posts = response.json.items || [];
|
||||
this.totalItems = response.json.total || this.posts.length;
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("common.error_connection_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
setting.addButton((button) =>
|
||||
button.setButtonText(i18next.t("post_selection_modal.button_pull")).onClick(() => {
|
||||
this.onSelect(post);
|
||||
this.close();
|
||||
}),
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
new Notice(i18next.t("common.error_connection_failed"));
|
||||
})
|
||||
.finally(() => {
|
||||
new Setting(contentEl).addButton((button) =>
|
||||
button.setButtonText(i18next.t("common.button_close")).onClick(() => this.close()),
|
||||
);
|
||||
});
|
||||
};
|
||||
private render() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
renderPostList();
|
||||
contentEl.createEl("h2", {
|
||||
text: i18next.t("post_selection_modal.title"),
|
||||
});
|
||||
|
||||
this.renderFilter();
|
||||
|
||||
if (this.posts.length === 0) {
|
||||
contentEl.createEl("p", {
|
||||
text: i18next.t("post_selection_modal.empty"),
|
||||
});
|
||||
} else {
|
||||
for (const post of this.posts) {
|
||||
this.renderPostItem(post);
|
||||
}
|
||||
}
|
||||
|
||||
this.renderPagination();
|
||||
}
|
||||
|
||||
private renderFilter() {
|
||||
const filterEl = this.contentEl.createDiv("post-list-filter");
|
||||
|
||||
const allButton = filterEl.createEl("button", {
|
||||
text: i18next.t("post_selection_modal.filter_all"),
|
||||
cls: this.filter === "all" ? "active" : "",
|
||||
});
|
||||
allButton.addEventListener("click", async () => {
|
||||
this.filter = "all";
|
||||
this.currentPage = 1;
|
||||
await this.loadPosts();
|
||||
this.render();
|
||||
});
|
||||
|
||||
const publishedButton = filterEl.createEl("button", {
|
||||
text: i18next.t("post_selection_modal.filter_published"),
|
||||
cls: this.filter === "published" ? "active" : "",
|
||||
});
|
||||
publishedButton.addEventListener("click", async () => {
|
||||
this.filter = "published";
|
||||
this.currentPage = 1;
|
||||
await this.loadPosts();
|
||||
this.render();
|
||||
});
|
||||
|
||||
const draftButton = filterEl.createEl("button", {
|
||||
text: i18next.t("post_selection_modal.filter_draft"),
|
||||
cls: this.filter === "draft" ? "active" : "",
|
||||
});
|
||||
draftButton.addEventListener("click", async () => {
|
||||
this.filter = "draft";
|
||||
this.currentPage = 1;
|
||||
await this.loadPosts();
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
private renderPostItem(post: ListedPost) {
|
||||
const postEl = this.contentEl.createDiv("post-item");
|
||||
|
||||
const headerEl = postEl.createDiv("post-header");
|
||||
|
||||
const titleEl = headerEl.createEl("h3", {
|
||||
text: post.post.spec.title || i18next.t("post_selection_modal.untitled"),
|
||||
});
|
||||
|
||||
const statusEl = headerEl.createSpan({
|
||||
text: post.post.spec.publish ? i18next.t("post_selection_modal.status_published") : i18next.t("post_selection_modal.status_draft"),
|
||||
cls: post.post.spec.publish ? "status-published" : "status-draft",
|
||||
});
|
||||
|
||||
const metaEl = postEl.createDiv("post-meta");
|
||||
metaEl.createEl("span", { text: `Slug: ${post.post.spec.slug}` });
|
||||
metaEl.createEl("span", { text: ` | ${new Date(post.post.metadata.creationTimestamp).toLocaleDateString()}` });
|
||||
|
||||
const actionsEl = postEl.createDiv("post-actions");
|
||||
|
||||
actionsEl.createEl("button", {
|
||||
text: i18next.t("post_selection_modal.button_pull"),
|
||||
cls: "mod-cta",
|
||||
}).addEventListener("click", () => {
|
||||
this.onSelect(post);
|
||||
this.close();
|
||||
});
|
||||
|
||||
actionsEl.createEl("button", {
|
||||
text: i18next.t("post_selection_modal.button_view"),
|
||||
}).addEventListener("click", () => {
|
||||
window.open(`${this.site.url}/archives/${post.post.spec.slug}`, "_blank");
|
||||
});
|
||||
}
|
||||
|
||||
private renderPagination() {
|
||||
const paginationEl = this.contentEl.createDiv("pagination");
|
||||
|
||||
const totalPages = Math.ceil(this.totalItems / this.pageSize);
|
||||
|
||||
if (this.currentPage > 1) {
|
||||
paginationEl.createEl("button", {
|
||||
text: i18next.t("post_selection_modal.button_prev"),
|
||||
}).addEventListener("click", async () => {
|
||||
this.currentPage--;
|
||||
await this.loadPosts();
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
paginationEl.createEl("span", {
|
||||
text: `${this.currentPage} / ${totalPages} (${this.totalItems} ${i18next.t("post_selection_modal.total_items")})`,
|
||||
});
|
||||
|
||||
if (this.currentPage < totalPages) {
|
||||
paginationEl.createEl("button", {
|
||||
text: i18next.t("post_selection_modal.button_next"),
|
||||
}).addEventListener("click", async () => {
|
||||
this.currentPage++;
|
||||
await this.loadPosts();
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
new Setting(paginationEl).addButton((button) =>
|
||||
button.setButtonText(i18next.t("common.button_close")).onClick(() => this.close()),
|
||||
);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import ImageUploader from "./image-uploader";
|
||||
import i18next from "i18next";
|
||||
import { type App, Notice, requestUrl } from "obsidian";
|
||||
import { type App, Notice, requestUrl, type TFile } from "obsidian";
|
||||
import { randomUUID } from "src/utils/id";
|
||||
import markdownIt from "src/utils/markdown";
|
||||
import { slugify } from "transliteration";
|
||||
@@ -514,6 +514,245 @@ class HaloService {
|
||||
})
|
||||
.filter(Boolean) as string[];
|
||||
}
|
||||
|
||||
public async importPost(file: TFile, publishImmediately: boolean = false): Promise<boolean> {
|
||||
try {
|
||||
const imageUploader = new ImageUploader(this.site.url, this.site.token);
|
||||
|
||||
const md = await this.app.vault.read(file);
|
||||
const matterData = this.app.metadataCache.getFileCache(file)?.frontmatter;
|
||||
const frontmatterPosition = this.app.metadataCache.getFileCache(file)?.frontmatterPosition;
|
||||
|
||||
let raw = frontmatterPosition ? md.slice(frontmatterPosition.end.offset) : md;
|
||||
|
||||
// 检测并上传本地图片
|
||||
if (this.settings.imageUpload?.enabled) {
|
||||
const imageReferences = extractImageReferences(raw);
|
||||
if (imageReferences.length > 0) {
|
||||
const localImages = imageReferences.filter((ref) =>
|
||||
!ref.path.startsWith("http://") &&
|
||||
!ref.path.startsWith("https://") &&
|
||||
!ref.path.startsWith("data:")
|
||||
);
|
||||
|
||||
if (localImages.length > 0) {
|
||||
new Notice(`检测到 ${localImages.length} 个本地图片,正在上传...`);
|
||||
|
||||
const absolutePaths = localImages
|
||||
.map((ref) => ({
|
||||
original: ref.path,
|
||||
absolute: getAbsolutePath(this.app.vault, ref.path, file.path),
|
||||
}))
|
||||
.filter((item) => item.absolute !== null) as { original: string; absolute: string }[];
|
||||
|
||||
if (absolutePaths.length > 0) {
|
||||
const pathMapping = await imageUploader.uploadImages(absolutePaths.map((item) => item.absolute), this.app.vault);
|
||||
|
||||
if (pathMapping.size > 0) {
|
||||
const mapping = new Map<string, string>();
|
||||
absolutePaths.forEach((item) => {
|
||||
const remoteUrl = pathMapping.get(item.absolute);
|
||||
if (remoteUrl) {
|
||||
mapping.set(item.original, remoteUrl);
|
||||
}
|
||||
});
|
||||
|
||||
raw = replaceImagePaths(raw, mapping);
|
||||
|
||||
const successCount = mapping.size;
|
||||
const failCount = absolutePaths.length - successCount;
|
||||
|
||||
if (failCount === 0) {
|
||||
new Notice(`✓ 图片上传成功 (${successCount}/${absolutePaths.length})`);
|
||||
} else {
|
||||
new Notice(`⚠ 部分图片上传失败 (成功: ${successCount}, 失败: ${failCount})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const content: Content = {
|
||||
rawType: "markdown",
|
||||
raw: raw,
|
||||
content: markdownIt.render(raw),
|
||||
};
|
||||
|
||||
let params: Post = {
|
||||
apiVersion: "content.halo.run/v1alpha1",
|
||||
kind: "Post",
|
||||
metadata: {
|
||||
annotations: {
|
||||
"content.halo.run/content-json": JSON.stringify(content),
|
||||
},
|
||||
name: randomUUID(),
|
||||
},
|
||||
spec: {
|
||||
allowComment: true,
|
||||
baseSnapshot: "",
|
||||
categories: [],
|
||||
cover: matterData?.cover || "",
|
||||
deleted: false,
|
||||
excerpt: {
|
||||
autoGenerate: matterData?.excerpt ? false : true,
|
||||
raw: matterData?.excerpt || "",
|
||||
},
|
||||
headSnapshot: "",
|
||||
htmlMetas: [],
|
||||
owner: "",
|
||||
pinned: false,
|
||||
priority: 0,
|
||||
publish: publishImmediately,
|
||||
publishTime: "",
|
||||
releaseSnapshot: "",
|
||||
slug: matterData?.slug || slugify(matterData?.title || file.basename, { trim: true }),
|
||||
tags: [],
|
||||
template: "",
|
||||
title: matterData?.title || file.basename,
|
||||
visible: "PUBLIC",
|
||||
},
|
||||
};
|
||||
|
||||
// 处理分类
|
||||
if (matterData?.categories) {
|
||||
const categoryNames = await this.getCategoryNames(matterData.categories);
|
||||
params.spec.categories = categoryNames;
|
||||
}
|
||||
|
||||
// 处理标签
|
||||
if (matterData?.tags) {
|
||||
const tagNames = await this.getTagNames(matterData.tags);
|
||||
params.spec.tags = tagNames;
|
||||
}
|
||||
|
||||
// 创建文章
|
||||
const post = await requestUrl({
|
||||
url: `${this.site.url}/apis/uc.api.content.halo.run/v1alpha1/posts`,
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(params),
|
||||
}).json;
|
||||
|
||||
return !!post && !!post.metadata;
|
||||
} catch (error) {
|
||||
console.error("[HaloService] 导入文章失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async deletePost(name: string): Promise<boolean> {
|
||||
try {
|
||||
await requestUrl({
|
||||
url: `${this.site.url}/apis/uc.api.content.halo.run/v1alpha1/posts/${name}`,
|
||||
method: "DELETE",
|
||||
headers: this.headers,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[HaloService] 删除文章失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async createTag(displayName: string, slug: string, color: string): Promise<void> {
|
||||
await requestUrl({
|
||||
url: `${this.site.url}/apis/content.halo.run/v1alpha1/tags`,
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify({
|
||||
spec: {
|
||||
displayName,
|
||||
slug,
|
||||
color: color || "#ffffff",
|
||||
cover: "",
|
||||
},
|
||||
apiVersion: "content.halo.run/v1alpha1",
|
||||
kind: "Tag",
|
||||
metadata: { name: "", generateName: "tag-" },
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
public async updateTag(name: string, displayName: string, slug: string, color: string): Promise<void> {
|
||||
const tag = (await requestUrl({
|
||||
url: `${this.site.url}/apis/content.halo.run/v1alpha1/tags/${name}`,
|
||||
headers: this.headers,
|
||||
}).json) as any;
|
||||
|
||||
tag.spec.displayName = displayName;
|
||||
tag.spec.slug = slug;
|
||||
tag.spec.color = color || "#ffffff";
|
||||
|
||||
await requestUrl({
|
||||
url: `${this.site.url}/apis/content.halo.run/v1alpha1/tags/${name}`,
|
||||
method: "PUT",
|
||||
contentType: "application/json",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(tag),
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteTag(name: string): Promise<void> {
|
||||
await requestUrl({
|
||||
url: `${this.site.url}/apis/content.halo.run/v1alpha1/tags/${name}`,
|
||||
method: "DELETE",
|
||||
headers: this.headers,
|
||||
});
|
||||
}
|
||||
|
||||
public async createCategory(displayName: string, slug: string, priority: number): Promise<void> {
|
||||
await requestUrl({
|
||||
url: `${this.site.url}/apis/content.halo.run/v1alpha1/categories`,
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify({
|
||||
spec: {
|
||||
displayName,
|
||||
slug,
|
||||
description: "",
|
||||
cover: "",
|
||||
template: "",
|
||||
priority,
|
||||
children: [],
|
||||
},
|
||||
apiVersion: "content.halo.run/v1alpha1",
|
||||
kind: "Category",
|
||||
metadata: { name: "", generateName: "category-" },
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
public async updateCategory(name: string, displayName: string, slug: string, priority: number): Promise<void> {
|
||||
const category = (await requestUrl({
|
||||
url: `${this.site.url}/apis/content.halo.run/v1alpha1/categories/${name}`,
|
||||
headers: this.headers,
|
||||
}).json) as any;
|
||||
|
||||
category.spec.displayName = displayName;
|
||||
category.spec.slug = slug;
|
||||
category.spec.priority = priority;
|
||||
|
||||
await requestUrl({
|
||||
url: `${this.site.url}/apis/content.halo.run/v1alpha1/categories/${name}`,
|
||||
method: "PUT",
|
||||
contentType: "application/json",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(category),
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteCategory(name: string): Promise<void> {
|
||||
await requestUrl({
|
||||
url: `${this.site.url}/apis/content.halo.run/v1alpha1/categories/${name}`,
|
||||
method: "DELETE",
|
||||
headers: this.headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default HaloService;
|
||||
|
||||
Reference in New Issue
Block a user