mirror of
https://github.com/woodchen-ink/obsidian-publish-to-discourse.git
synced 2025-07-17 21:32:05 +08:00
Enhance DiscourseSyncPlugin with tag management features
- Updated styles for modal and form areas to improve layout and usability. - Added support for selecting and managing tags in the DiscourseSync settings. - Implemented fetching of available tags from the Discourse API. - Enhanced the category selection modal to include tag selection functionality. - Updated settings interface to store selected tags. - Improved error handling and user feedback during tag operations.
This commit is contained in:
parent
efbb0a5c9c
commit
ce2d944ee6
@ -6,6 +6,7 @@ export interface DiscourseSyncSettings {
|
||||
apiKey: string;
|
||||
disUser: string;
|
||||
category: number;
|
||||
selectedTags: string[];
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: DiscourseSyncSettings = {
|
||||
@ -13,6 +14,7 @@ export const DEFAULT_SETTINGS: DiscourseSyncSettings = {
|
||||
apiKey: "apikey",
|
||||
disUser: "DiscourseUsername",
|
||||
category: 1,
|
||||
selectedTags: []
|
||||
};
|
||||
|
||||
export class DiscourseSyncSettingsTab extends PluginSettingTab {
|
||||
|
241
src/main.ts
241
src/main.ts
@ -140,11 +140,13 @@ export default class DiscourseSyncPlugin extends Plugin {
|
||||
|
||||
const body = JSON.stringify(isUpdate ? {
|
||||
raw: content,
|
||||
edit_reason: "Updated from Obsidian"
|
||||
edit_reason: "Updated from Obsidian",
|
||||
tags: this.settings.selectedTags || []
|
||||
} : {
|
||||
title: this.activeFile.name,
|
||||
raw: content,
|
||||
category: this.settings.category
|
||||
category: this.settings.category,
|
||||
tags: this.settings.selectedTags || []
|
||||
});
|
||||
|
||||
try {
|
||||
@ -228,7 +230,6 @@ export default class DiscourseSyncPlugin extends Plugin {
|
||||
// 获取当前活动文件
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
if (!activeFile) {
|
||||
console.error('No active file found');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -247,9 +248,11 @@ export default class DiscourseSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
await this.app.vault.modify(activeFile, newContent);
|
||||
console.log('Successfully updated frontmatter with post ID:', postId);
|
||||
} catch (error) {
|
||||
console.error('Error updating frontmatter:', error);
|
||||
return {
|
||||
message: "Error",
|
||||
error: "更新失败"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -288,6 +291,66 @@ export default class DiscourseSyncPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchTags(): Promise<{ name: string; canCreate: boolean }[]> {
|
||||
const url = `${this.settings.baseUrl}/tags.json`;
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Api-Key": this.settings.apiKey,
|
||||
"Api-Username": this.settings.disUser,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await requestUrl({
|
||||
url: url,
|
||||
method: "GET",
|
||||
contentType: "application/json",
|
||||
headers,
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
const data = response.json;
|
||||
// 获取用户权限信息
|
||||
const canCreateTags = await this.checkCanCreateTags();
|
||||
// Discourse 返回的 tags 列表在 tags 数组中
|
||||
return data.tags.map((tag: any) => ({
|
||||
name: tag.name,
|
||||
canCreate: canCreateTags
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 检查用户是否有创建标签的权限
|
||||
private async checkCanCreateTags(): Promise<boolean> {
|
||||
try {
|
||||
const url = `${this.settings.baseUrl}/u/${this.settings.disUser}.json`;
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Api-Key": this.settings.apiKey,
|
||||
"Api-Username": this.settings.disUser,
|
||||
};
|
||||
|
||||
const response = await requestUrl({
|
||||
url: url,
|
||||
method: "GET",
|
||||
contentType: "application/json",
|
||||
headers,
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
const data = response.json;
|
||||
// 检查用户的 trust_level
|
||||
return data.user.trust_level >= 3;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
registerDirMenu(menu: Menu, file: TFile) {
|
||||
const syncDiscourse = (item: MenuItem) => {
|
||||
item.setTitle("发布到 Discourse");
|
||||
@ -306,9 +369,12 @@ export default class DiscourseSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
private async openCategoryModal() {
|
||||
const categories = await this.fetchCategories();
|
||||
const [categories, tags] = await Promise.all([
|
||||
this.fetchCategories(),
|
||||
this.fetchTags()
|
||||
]);
|
||||
if (categories.length > 0) {
|
||||
new SelectCategoryModal(this.app, this, categories).open();
|
||||
new SelectCategoryModal(this.app, this, categories, tags).open();
|
||||
}
|
||||
}
|
||||
|
||||
@ -363,10 +429,15 @@ export class NotifyUser extends Modal {
|
||||
export class SelectCategoryModal extends Modal {
|
||||
plugin: DiscourseSyncPlugin;
|
||||
categories: {id: number; name: string}[];
|
||||
constructor(app: App, plugin: DiscourseSyncPlugin, categories: {id: number; name: string }[]) {
|
||||
tags: { name: string; canCreate: boolean }[];
|
||||
canCreateTags = false;
|
||||
|
||||
constructor(app: App, plugin: DiscourseSyncPlugin, categories: {id: number; name: string }[], tags: { name: string; canCreate: boolean }[]) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.categories = categories;
|
||||
this.tags = tags;
|
||||
this.canCreateTags = tags.length > 0 ? tags[0].canCreate : false;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
@ -379,8 +450,11 @@ export class SelectCategoryModal extends Modal {
|
||||
const isUpdate = this.plugin.activeFile.postId !== undefined;
|
||||
contentEl.createEl("h1", { text: isUpdate ? '更新帖子' : '发布到 Discourse' });
|
||||
|
||||
// 创建表单区域容器
|
||||
const formArea = contentEl.createEl('div', { cls: 'form-area' });
|
||||
|
||||
// 创建选择器容器
|
||||
const selectContainer = contentEl.createEl('div', { cls: 'select-container' });
|
||||
const selectContainer = formArea.createEl('div', { cls: 'select-container' });
|
||||
if (!isUpdate) {
|
||||
// 只在新建帖子时显示分类选择
|
||||
selectContainer.createEl('label', { text: '分类' });
|
||||
@ -391,13 +465,154 @@ export class SelectCategoryModal extends Modal {
|
||||
});
|
||||
}
|
||||
|
||||
const submitButton = contentEl.createEl('button', {
|
||||
// 创建标签选择容器
|
||||
const tagContainer = formArea.createEl('div', { cls: 'tag-container' });
|
||||
tagContainer.createEl('label', { text: '标签' });
|
||||
|
||||
// 创建标签选择区域
|
||||
const tagSelectArea = tagContainer.createEl('div', { cls: 'tag-select-area' });
|
||||
|
||||
// 已选标签显示区域
|
||||
const selectedTagsContainer = tagSelectArea.createEl('div', { cls: 'selected-tags' });
|
||||
const selectedTags = new Set<string>();
|
||||
|
||||
// 更新已选标签显示
|
||||
const updateSelectedTags = () => {
|
||||
selectedTagsContainer.empty();
|
||||
selectedTags.forEach(tag => {
|
||||
const tagEl = selectedTagsContainer.createEl('span', {
|
||||
cls: 'tag',
|
||||
text: tag
|
||||
});
|
||||
const removeBtn = tagEl.createEl('span', {
|
||||
cls: 'remove-tag',
|
||||
text: '×'
|
||||
});
|
||||
removeBtn.onclick = () => {
|
||||
selectedTags.delete(tag);
|
||||
updateSelectedTags();
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 创建标签输入容器
|
||||
const tagInputContainer = tagSelectArea.createEl('div', { cls: 'tag-input-container' });
|
||||
|
||||
// 创建标签输入和建议
|
||||
const tagInput = tagInputContainer.createEl('input', {
|
||||
type: 'text',
|
||||
placeholder: this.canCreateTags ? '输入标签名称(回车添加)' : '输入标签名称(回车添加)'
|
||||
});
|
||||
|
||||
// 创建标签建议容器
|
||||
const tagSuggestions = tagInputContainer.createEl('div', { cls: 'tag-suggestions' });
|
||||
|
||||
// 处理输入事件,显示匹配的标签
|
||||
tagInput.oninput = () => {
|
||||
const value = tagInput.value.toLowerCase();
|
||||
tagSuggestions.empty();
|
||||
|
||||
if (value) {
|
||||
const matches = this.tags
|
||||
.filter(tag =>
|
||||
tag.name.toLowerCase().includes(value) &&
|
||||
!selectedTags.has(tag.name)
|
||||
)
|
||||
.slice(0, 10);
|
||||
|
||||
if (matches.length > 0) {
|
||||
// 获取输入框的位置和宽度
|
||||
const inputRect = tagInput.getBoundingClientRect();
|
||||
const modalRect = this.modalEl.getBoundingClientRect();
|
||||
|
||||
// 确保建议列表不会超出模态框宽度
|
||||
const maxWidth = modalRect.right - inputRect.left - 24; // 24px 是右侧padding
|
||||
|
||||
// 设置建议列表的位置和宽度
|
||||
tagSuggestions.style.top = `${inputRect.bottom + 4}px`;
|
||||
tagSuggestions.style.left = `${inputRect.left}px`;
|
||||
tagSuggestions.style.width = `${Math.min(inputRect.width, maxWidth)}px`;
|
||||
|
||||
matches.forEach(tag => {
|
||||
const suggestion = tagSuggestions.createEl('div', {
|
||||
cls: 'tag-suggestion',
|
||||
text: tag.name
|
||||
});
|
||||
suggestion.onclick = () => {
|
||||
selectedTags.add(tag.name);
|
||||
tagInput.value = '';
|
||||
tagSuggestions.empty();
|
||||
updateSelectedTags();
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理回车事件
|
||||
tagInput.onkeydown = (e) => {
|
||||
if (e.key === 'Enter' && tagInput.value) {
|
||||
e.preventDefault();
|
||||
const value = tagInput.value.trim();
|
||||
if (value && !selectedTags.has(value)) {
|
||||
const existingTag = this.tags.find(t => t.name.toLowerCase() === value.toLowerCase());
|
||||
if (existingTag) {
|
||||
selectedTags.add(existingTag.name);
|
||||
updateSelectedTags();
|
||||
} else if (this.canCreateTags) {
|
||||
selectedTags.add(value);
|
||||
updateSelectedTags();
|
||||
} else {
|
||||
// 显示权限提示
|
||||
const notice = contentEl.createEl('div', {
|
||||
cls: 'tag-notice',
|
||||
text: '权限不足,只能使用已有的标签'
|
||||
});
|
||||
setTimeout(() => {
|
||||
notice.remove();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
tagInput.value = '';
|
||||
tagSuggestions.empty();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理失焦事件,隐藏建议
|
||||
tagInput.onblur = () => {
|
||||
// 延迟隐藏,以便可以点击建议
|
||||
setTimeout(() => {
|
||||
tagSuggestions.empty();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
// 处理窗口滚动时更新建议列表位置
|
||||
const updateSuggestionsPosition = () => {
|
||||
if (tagSuggestions.childNodes.length > 0) {
|
||||
const inputRect = tagInput.getBoundingClientRect();
|
||||
tagSuggestions.style.top = `${inputRect.bottom + 4}px`;
|
||||
tagSuggestions.style.left = `${inputRect.left}px`;
|
||||
tagSuggestions.style.width = `${inputRect.width}px`;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听滚动事件
|
||||
this.modalEl.addEventListener('scroll', updateSuggestionsPosition);
|
||||
|
||||
// 在模态框关闭时移除事件监听
|
||||
this.modalEl.onclose = () => {
|
||||
this.modalEl.removeEventListener('scroll', updateSuggestionsPosition);
|
||||
};
|
||||
|
||||
// 创建按钮区域
|
||||
const buttonArea = contentEl.createEl('div', { cls: 'button-area' });
|
||||
const submitButton = buttonArea.createEl('button', {
|
||||
text: isUpdate ? '更新' : '发布',
|
||||
cls: 'submit-button'
|
||||
});
|
||||
|
||||
// 创建提示信息容器
|
||||
const noticeContainer = contentEl.createEl('div');
|
||||
const noticeContainer = buttonArea.createEl('div');
|
||||
|
||||
submitButton.onclick = async () => {
|
||||
if (!isUpdate) {
|
||||
@ -410,6 +625,10 @@ export class SelectCategoryModal extends Modal {
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
|
||||
// 保存选中的标签
|
||||
this.plugin.settings.selectedTags = Array.from(selectedTags);
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
// 禁用提交按钮,显示加载状态
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = isUpdate ? '更新中...' : '发布中...';
|
||||
|
207
styles.css
207
styles.css
@ -10,16 +10,35 @@ If your plugin does not need CSS, delete this file.
|
||||
/* 基础模态框样式覆盖 */
|
||||
.modal.mod-discourse-sync {
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
max-height: 90vh;
|
||||
background-color: var(--background-primary);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 内容区域样式 */
|
||||
.discourse-sync-modal {
|
||||
padding: 24px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 表单区域 */
|
||||
.discourse-sync-modal .form-area {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 按钮区域固定在底部 */
|
||||
.discourse-sync-modal .button-area {
|
||||
flex-shrink: 0;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.discourse-sync-modal h1 {
|
||||
@ -29,27 +48,37 @@ If your plugin does not need CSS, delete this file.
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.discourse-sync-modal .select-container {
|
||||
/* 表单容器通用样式 */
|
||||
.discourse-sync-modal .select-container,
|
||||
.discourse-sync-modal .tag-container {
|
||||
margin-bottom: 24px;
|
||||
padding: 0; /* 移除内边距 */
|
||||
}
|
||||
|
||||
.discourse-sync-modal .select-container label {
|
||||
.discourse-sync-modal .select-container label,
|
||||
.discourse-sync-modal .tag-container label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.discourse-sync-modal select {
|
||||
/* 输入框和选择器样式 */
|
||||
.discourse-sync-modal select,
|
||||
.discourse-sync-modal .tag-select-area {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
height: 42px;
|
||||
line-height: 1.5;
|
||||
border: 2px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
font-size: 14px;
|
||||
min-height: 42px;
|
||||
}
|
||||
|
||||
.discourse-sync-modal select {
|
||||
height: 42px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.discourse-sync-modal select:focus {
|
||||
@ -141,3 +170,169 @@ If your plugin does not need CSS, delete this file.
|
||||
background-color: rgb(255, 82, 82);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 标签选择区域样式 */
|
||||
.discourse-sync-modal .tag-select-area {
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.discourse-sync-modal .selected-tags {
|
||||
display: none; /* 默认隐藏 */
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.discourse-sync-modal .selected-tags:not(:empty) {
|
||||
display: flex; /* 当有内容时显示 */
|
||||
}
|
||||
|
||||
.discourse-sync-modal .tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.discourse-sync-modal .remove-tag {
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.discourse-sync-modal .remove-tag:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.discourse-sync-modal input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-normal);
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.discourse-sync-modal .suggestions-container {
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
/* 标签输入容器 */
|
||||
.discourse-sync-modal .tag-input-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 标签建议下拉框 */
|
||||
.discourse-sync-modal .tag-suggestions {
|
||||
position: fixed;
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1001;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
margin-top: 4px;
|
||||
display: none; /* 默认隐藏 */
|
||||
}
|
||||
|
||||
.discourse-sync-modal .tag-suggestions:not(:empty) {
|
||||
display: block; /* 当有内容时显示 */
|
||||
}
|
||||
|
||||
/* 标签建议项 */
|
||||
.discourse-sync-modal .tag-suggestion {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
color: var(--text-normal);
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background-color: var(--background-primary);
|
||||
}
|
||||
|
||||
.discourse-sync-modal .tag-suggestion:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
/* 美化滚动条 */
|
||||
.discourse-sync-modal .tag-suggestions::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.discourse-sync-modal .tag-suggestions::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.discourse-sync-modal .tag-suggestions::-webkit-scrollbar-thumb {
|
||||
background-color: var(--background-modifier-border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.discourse-sync-modal .tag-suggestions::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--background-modifier-border-hover);
|
||||
}
|
||||
|
||||
/* 移除 tag-container 的 z-index,避免干扰 */
|
||||
.discourse-sync-modal .tag-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.discourse-sync-modal .tag-notice {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 12px 20px;
|
||||
background-color: rgb(255, 235, 235);
|
||||
border: 1px solid rgba(255, 82, 82, 0.2);
|
||||
color: rgb(255, 82, 82);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
z-index: 1002;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
max-width: 80%;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
animation: fadeInOut 2s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInOut {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user