mirror of
https://github.com/woodchen-ink/obsidian-publish-to-discourse.git
synced 2025-07-18 05:42:05 +08:00
763 lines
21 KiB
TypeScript
763 lines
21 KiB
TypeScript
import { App, Menu, MenuItem, Plugin, Modal, requestUrl, TFile, moment } from 'obsidian';
|
||
import { DEFAULT_SETTINGS, DiscourseSyncSettings, DiscourseSyncSettingsTab } from './config';
|
||
import * as yaml from 'yaml';
|
||
import { t, setLocale } from './i18n';
|
||
import { expandEmbeds } from './expand-embeds';
|
||
|
||
export default class DiscourseSyncPlugin extends Plugin {
|
||
settings: DiscourseSyncSettings;
|
||
activeFile: {
|
||
name: string;
|
||
content: string;
|
||
postId?: number; // Post ID field
|
||
};
|
||
async onload() {
|
||
// Set locale based on Obsidian's language setting
|
||
setLocale(moment.locale());
|
||
|
||
// Update locale when Obsidian's language changes
|
||
this.registerEvent(
|
||
this.app.workspace.on('window-open', () => {
|
||
setLocale(moment.locale());
|
||
})
|
||
);
|
||
|
||
await this.loadSettings();
|
||
this.addSettingTab(new DiscourseSyncSettingsTab(this.app, this));
|
||
this.registerEvent(
|
||
this.app.workspace.on("file-menu", (menu, file: TFile) => {
|
||
this.registerDirMenu(menu, file);
|
||
}),
|
||
);
|
||
|
||
this.addCommand({
|
||
id: "category-modal",
|
||
name: t('PUBLISH_TO_DISCOURSE'),
|
||
callback: () => {
|
||
this.openCategoryModal();
|
||
},
|
||
});
|
||
|
||
// Add command to open post in browser
|
||
this.addCommand({
|
||
id: "open-in-discourse",
|
||
name: t('OPEN_IN_DISCOURSE'),
|
||
callback: () => {
|
||
this.openInDiscourse();
|
||
},
|
||
});
|
||
|
||
}
|
||
|
||
async loadSettings() {
|
||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||
}
|
||
|
||
async saveSettings() {
|
||
await this.saveData(this.settings);
|
||
}
|
||
|
||
extractImageReferences(content: string): string[] {
|
||
const regex = /!\[\[(.*?)\]\]/g;
|
||
const matches = [];
|
||
let match;
|
||
while ((match = regex.exec(content)) !== null) {
|
||
matches.push(match[1]);
|
||
}
|
||
return matches;
|
||
}
|
||
|
||
async uploadImages(imageReferences: string[]): Promise<string[]> {
|
||
const imageUrls: string[] = [];
|
||
for (const ref of imageReferences) {
|
||
const filePath = this.app.metadataCache.getFirstLinkpathDest(ref, this.activeFile.name)?.path;
|
||
if (filePath) {
|
||
const abstractFile = this.app.vault.getAbstractFileByPath(filePath);
|
||
if (abstractFile instanceof TFile) {
|
||
try {
|
||
const imgfile = await this.app.vault.readBinary(abstractFile);
|
||
const boundary = genBoundary();
|
||
const sBoundary = '--' + boundary + '\r\n';
|
||
const imgForm = `${sBoundary}Content-Disposition: form-data; name="file"; filename="${abstractFile.name}"\r\nContent-Type: image/${abstractFile.extension}\r\n\r\n`;
|
||
|
||
|
||
let body = '';
|
||
body += `\r\n${sBoundary}Content-Disposition: form-data; name="type"\r\n\r\ncomposer\r\n`;
|
||
body += `${sBoundary}Content-Disposition: form-data; name="synchronous"\r\n\r\ntrue\r\n`;
|
||
|
||
const eBoundary = '\r\n--' + boundary + '--\r\n';
|
||
const imgFormArray = new TextEncoder().encode(imgForm);
|
||
const bodyArray = new TextEncoder().encode(body);
|
||
const endBoundaryArray = new TextEncoder().encode(eBoundary);
|
||
|
||
const formDataArray = new Uint8Array(imgFormArray.length + imgfile.byteLength + bodyArray.length + endBoundaryArray.length);
|
||
formDataArray.set(imgFormArray, 0);
|
||
formDataArray.set(new Uint8Array(imgfile), imgFormArray.length);
|
||
formDataArray.set(bodyArray, imgFormArray.length + imgfile.byteLength);
|
||
formDataArray.set(endBoundaryArray, imgFormArray.length + bodyArray.length + imgfile.byteLength);
|
||
|
||
const url = `${this.settings.baseUrl}/uploads.json`;
|
||
const headers = {
|
||
"Api-Key": this.settings.apiKey,
|
||
"Api-Username": this.settings.disUser,
|
||
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
||
};
|
||
|
||
const response = await requestUrl({
|
||
url: url,
|
||
method: "POST",
|
||
body: formDataArray.buffer,
|
||
throw: false,
|
||
headers: headers,
|
||
});
|
||
|
||
if (response.status == 200) {
|
||
const jsonResponse = response.json;
|
||
imageUrls.push(jsonResponse.short_url);
|
||
} else {
|
||
new NotifyUser(this.app, `Error uploading image: ${response.status}`).open();
|
||
}
|
||
} catch (error) {
|
||
new NotifyUser(this.app, `Exception while uploading image: ${error}`).open();
|
||
}
|
||
} else {
|
||
new NotifyUser(this.app, `File not found in vault: ${ref}`).open();
|
||
}
|
||
} else {
|
||
new NotifyUser(this.app, `Unable to resolve file path for: ${ref}`).open();
|
||
}
|
||
}
|
||
return imageUrls;
|
||
}
|
||
|
||
async postTopic(): Promise<{ message: string; error?: string }> {
|
||
const url = `${this.settings.baseUrl}/posts.json`;
|
||
const headers = {
|
||
"Content-Type": "application/json",
|
||
"Api-Key": this.settings.apiKey,
|
||
"Api-Username": this.settings.disUser,
|
||
}
|
||
let content = this.activeFile.content;
|
||
|
||
// Check if frontmatter contains post ID
|
||
const frontMatter = this.getFrontMatter(content);
|
||
const postId = frontMatter?.discourse_post_id;
|
||
|
||
// Filter out note properties section
|
||
content = content.replace(/^---[\s\S]*?---\n/, '');
|
||
|
||
const imageReferences = this.extractImageReferences(content);
|
||
const imageUrls = await this.uploadImages(imageReferences);
|
||
|
||
imageReferences.forEach((ref, index) => {
|
||
const obsRef = `![[${ref}]]`;
|
||
const discoRef = ``;
|
||
content = content.replace(obsRef, discoRef);
|
||
});
|
||
|
||
const isUpdate = postId !== undefined;
|
||
const endpoint = isUpdate ? `${this.settings.baseUrl}/posts/${postId}` : url;
|
||
const method = isUpdate ? "PUT" : "POST";
|
||
|
||
const body = JSON.stringify(isUpdate ? {
|
||
raw: content,
|
||
edit_reason: "Updated from Obsidian",
|
||
tags: this.settings.selectedTags || []
|
||
} : {
|
||
title: (frontMatter?.title ? frontMatter?.title : this.activeFile.name),
|
||
raw: content,
|
||
category: this.settings.category,
|
||
tags: this.settings.selectedTags || []
|
||
});
|
||
|
||
try {
|
||
const response = await requestUrl({
|
||
url: endpoint,
|
||
method: method,
|
||
contentType: "application/json",
|
||
body,
|
||
headers,
|
||
throw: false
|
||
});
|
||
|
||
if (response.status === 200) {
|
||
if (!isUpdate) {
|
||
try {
|
||
// Get new post ID and topic ID
|
||
const responseData = response.json;
|
||
if (responseData && responseData.id) {
|
||
await this.updateFrontMatter(responseData.id, responseData.topic_id);
|
||
} else {
|
||
return {
|
||
message: "Error",
|
||
error: t('POST_ID_ERROR')
|
||
};
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
message: "Error",
|
||
error: t('SAVE_POST_ID_ERROR')
|
||
};
|
||
}
|
||
}
|
||
return { message: "Success" };
|
||
} else {
|
||
try {
|
||
const errorResponse = response.json;
|
||
if (errorResponse.errors && errorResponse.errors.length > 0) {
|
||
return {
|
||
message: "Error",
|
||
error: errorResponse.errors.join('\n')
|
||
};
|
||
}
|
||
if (errorResponse.error) {
|
||
return {
|
||
message: "Error",
|
||
error: errorResponse.error
|
||
};
|
||
}
|
||
} catch (parseError) {
|
||
return {
|
||
message: "Error",
|
||
error: `${isUpdate ? t('UPDATE_FAILED') : t('PUBLISH_FAILED')} (${response.status})`
|
||
};
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
message: "Error",
|
||
error: `${isUpdate ? t('UPDATE_FAILED') : t('PUBLISH_FAILED')}: ${error.message || t('UNKNOWN_ERROR')}`
|
||
};
|
||
}
|
||
return { message: "Error", error: `${isUpdate ? t('UPDATE_FAILED') : t('PUBLISH_FAILED')}, ${t('TRY_AGAIN')}` };
|
||
}
|
||
|
||
// Get frontmatter data
|
||
private getFrontMatter(content: string): any {
|
||
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||
if (fmMatch) {
|
||
try {
|
||
return yaml.parse(fmMatch[1]);
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Update frontmatter, add post ID
|
||
private async updateFrontMatter(postId: number, topicId: number) {
|
||
try {
|
||
// Get current active file
|
||
const activeFile = this.app.workspace.getActiveFile();
|
||
if (!activeFile) {
|
||
return;
|
||
}
|
||
|
||
const content = await this.app.vault.read(activeFile);
|
||
const fm = this.getFrontMatter(content);
|
||
const discourseUrl = `${this.settings.baseUrl}/t/${topicId}`;
|
||
|
||
let newContent: string;
|
||
if (fm) {
|
||
// Update existing frontmatter
|
||
const updatedFm = {
|
||
...fm,
|
||
discourse_post_id: postId,
|
||
discourse_topic_id: topicId,
|
||
discourse_url: discourseUrl
|
||
};
|
||
newContent = content.replace(/^---\n[\s\S]*?\n---\n/, `---\n${yaml.stringify(updatedFm)}---\n`);
|
||
} else {
|
||
// Add new frontmatter
|
||
const newFm = {
|
||
discourse_post_id: postId,
|
||
discourse_topic_id: topicId,
|
||
discourse_url: discourseUrl
|
||
};
|
||
newContent = `---\n${yaml.stringify(newFm)}---\n${content}`;
|
||
}
|
||
|
||
await this.app.vault.modify(activeFile, newContent);
|
||
} catch (error) {
|
||
return {
|
||
message: "Error",
|
||
error: t('UPDATE_FAILED')
|
||
};
|
||
}
|
||
}
|
||
|
||
private async fetchCategories() {
|
||
const url = `${this.settings.baseUrl}/categories.json?include_subcategories=true`;
|
||
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,
|
||
});
|
||
|
||
|
||
const data = await response.json;
|
||
const categories = data.category_list.categories;
|
||
const allCategories = categories.flatMap((category: Category) => {
|
||
const subcategories: { id: number; name: string }[] = category.subcategory_list?.map((sub: Subcategory) => ({
|
||
id: sub.id,
|
||
name: sub.name,
|
||
})) || [];
|
||
return [
|
||
{ id: category.id, name: category.name },
|
||
...subcategories,
|
||
];
|
||
});
|
||
return allCategories;
|
||
} catch (error) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
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;
|
||
// Get user permissions
|
||
const canCreateTags = await this.checkCanCreateTags();
|
||
// Tags list returned by Discourse is in the tags array
|
||
return data.tags.map((tag: any) => ({
|
||
name: tag.name,
|
||
canCreate: canCreateTags
|
||
}));
|
||
}
|
||
return [];
|
||
} catch (error) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// Check if user has permission to create tags
|
||
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;
|
||
// Check user's 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(t('PUBLISH_TO_DISCOURSE'));
|
||
item.onClick(async () => {
|
||
const content = await expandEmbeds(this.app, file);
|
||
const fm = this.getFrontMatter(content);
|
||
this.activeFile = {
|
||
name: file.basename,
|
||
content: content,
|
||
postId: fm?.discourse_post_id
|
||
};
|
||
await this.syncToDiscourse();
|
||
});
|
||
}
|
||
menu.addItem(syncDiscourse)
|
||
}
|
||
|
||
private async openCategoryModal() {
|
||
const [categories, tags] = await Promise.all([
|
||
this.fetchCategories(),
|
||
this.fetchTags()
|
||
]);
|
||
if (categories.length > 0) {
|
||
new SelectCategoryModal(this.app, this, categories, tags).open();
|
||
}
|
||
}
|
||
|
||
private async syncToDiscourse() {
|
||
await this.openCategoryModal();
|
||
}
|
||
|
||
private async openInDiscourse() {
|
||
const activeFile = this.app.workspace.getActiveFile();
|
||
if (!activeFile) {
|
||
new NotifyUser(this.app, t('NO_ACTIVE_FILE')).open();
|
||
return;
|
||
}
|
||
|
||
const content = await this.app.vault.read(activeFile);
|
||
const fm = this.getFrontMatter(content);
|
||
const discourseUrl = fm?.discourse_url;
|
||
const topicId = fm?.discourse_topic_id;
|
||
|
||
if (!discourseUrl && !topicId) {
|
||
new NotifyUser(this.app, t('NO_TOPIC_ID')).open();
|
||
return;
|
||
}
|
||
|
||
const url = discourseUrl || `${this.settings.baseUrl}/t/${topicId}`;
|
||
window.open(url, '_blank');
|
||
}
|
||
|
||
onunload() {}
|
||
|
||
}
|
||
|
||
interface Subcategory {
|
||
id: number;
|
||
name: string;
|
||
}
|
||
|
||
interface Category {
|
||
id: number;
|
||
name: string;
|
||
subcategory_list?: Subcategory[];
|
||
}
|
||
|
||
const genBoundary = (): string => {
|
||
return '----WebKitFormBoundary' + Math.random().toString(36).substring(2, 15);
|
||
}
|
||
|
||
|
||
export class NotifyUser extends Modal {
|
||
message: string;
|
||
constructor(app: App, message: string) {
|
||
super(app);
|
||
this.message = message;
|
||
}
|
||
|
||
onOpen() {
|
||
const { contentEl } = this;
|
||
contentEl.createEl("h1", { text: 'An error has occurred.' });
|
||
contentEl.createEl("h4", { text: this.message });
|
||
const okButton = contentEl.createEl('button', { text: 'Ok' });
|
||
okButton.onclick = () => {
|
||
this.close();
|
||
}
|
||
}
|
||
|
||
onClose() {
|
||
const { contentEl } = this;
|
||
contentEl.empty();
|
||
}
|
||
|
||
}
|
||
|
||
export class SelectCategoryModal extends Modal {
|
||
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() {
|
||
// Add modal base style
|
||
this.modalEl.addClass('mod-discourse-sync');
|
||
const { contentEl } = this;
|
||
contentEl.empty();
|
||
contentEl.addClass('discourse-sync-modal');
|
||
|
||
const isUpdate = this.plugin.activeFile.postId !== undefined;
|
||
contentEl.createEl("h1", { text: isUpdate ? t('UPDATE_POST') : t('PUBLISH_TO_DISCOURSE') });
|
||
|
||
// Create form area container
|
||
const formArea = contentEl.createEl('div', { cls: 'form-area' });
|
||
|
||
// Create selector container
|
||
const selectContainer = formArea.createEl('div', { cls: 'select-container' });
|
||
if (!isUpdate) {
|
||
// Only show category selector when creating a new post
|
||
selectContainer.createEl('label', { text: t('CATEGORY') });
|
||
const selectEl = selectContainer.createEl('select');
|
||
this.categories.forEach(category => {
|
||
const option = selectEl.createEl('option', { text: category.name });
|
||
option.value = category.id.toString();
|
||
});
|
||
}
|
||
|
||
// Create tag selector container
|
||
const tagContainer = formArea.createEl('div', { cls: 'tag-container' });
|
||
tagContainer.createEl('label', { text: t('TAGS') });
|
||
|
||
// Create tag selector area
|
||
const tagSelectArea = tagContainer.createEl('div', { cls: 'tag-select-area' });
|
||
|
||
// Selected tags display area
|
||
const selectedTagsContainer = tagSelectArea.createEl('div', { cls: 'selected-tags' });
|
||
const selectedTags = new Set<string>();
|
||
|
||
// Update selected tags display
|
||
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();
|
||
};
|
||
});
|
||
};
|
||
|
||
// Create tag input container
|
||
const tagInputContainer = tagSelectArea.createEl('div', { cls: 'tag-input-container' });
|
||
|
||
// Create tag input and suggestions
|
||
const tagInput = tagInputContainer.createEl('input', {
|
||
type: 'text',
|
||
placeholder: this.canCreateTags ? t('ENTER_TAG_WITH_CREATE') : t('ENTER_TAG')
|
||
});
|
||
|
||
// Create tag suggestions container
|
||
const tagSuggestions = tagInputContainer.createEl('div', { cls: 'tag-suggestions' });
|
||
|
||
// Handle input event, show matching tags
|
||
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) {
|
||
// Get input box position and width
|
||
const inputRect = tagInput.getBoundingClientRect();
|
||
const modalRect = this.modalEl.getBoundingClientRect();
|
||
|
||
// Ensure suggestions list doesn't exceed modal width
|
||
const maxWidth = modalRect.right - inputRect.left - 24; // 24px is right padding
|
||
|
||
// Set suggestions list position and width
|
||
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();
|
||
};
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
// Handle enter event
|
||
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 {
|
||
// Show permission notice
|
||
const notice = contentEl.createEl('div', {
|
||
cls: 'tag-notice',
|
||
text: t('PERMISSION_ERROR')
|
||
});
|
||
setTimeout(() => {
|
||
notice.remove();
|
||
}, 2000);
|
||
}
|
||
}
|
||
tagInput.value = '';
|
||
tagSuggestions.empty();
|
||
}
|
||
};
|
||
|
||
// Handle blur event, hide suggestions
|
||
tagInput.onblur = () => {
|
||
// Delay hide, so suggestions can be clicked
|
||
setTimeout(() => {
|
||
tagSuggestions.empty();
|
||
}, 200);
|
||
};
|
||
|
||
// Handle window scroll, update suggestions list position
|
||
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`;
|
||
}
|
||
};
|
||
|
||
// Listen for scroll event
|
||
this.modalEl.addEventListener('scroll', updateSuggestionsPosition);
|
||
|
||
// Remove event listeners when modal closes
|
||
this.modalEl.onclose = () => {
|
||
this.modalEl.removeEventListener('scroll', updateSuggestionsPosition);
|
||
};
|
||
|
||
// Create button area
|
||
const buttonArea = contentEl.createEl('div', { cls: 'button-area' });
|
||
const submitButton = buttonArea.createEl('button', {
|
||
text: isUpdate ? t('UPDATE') : t('PUBLISH'),
|
||
cls: 'submit-button'
|
||
});
|
||
|
||
// Create notice container
|
||
const noticeContainer = buttonArea.createEl('div');
|
||
|
||
submitButton.onclick = async () => {
|
||
if (!isUpdate) {
|
||
const selectEl = contentEl.querySelector('select') as HTMLSelectElement;
|
||
if (!selectEl) {
|
||
return;
|
||
}
|
||
const selectedCategoryId = selectEl.value;
|
||
this.plugin.settings.category = parseInt(selectedCategoryId);
|
||
await this.plugin.saveSettings();
|
||
}
|
||
|
||
// Save selected tags
|
||
this.plugin.settings.selectedTags = Array.from(selectedTags);
|
||
await this.plugin.saveSettings();
|
||
|
||
// Disable submit button, show loading state
|
||
submitButton.disabled = true;
|
||
submitButton.textContent = isUpdate ? t('UPDATING') : t('PUBLISHING');
|
||
|
||
try {
|
||
const reply = await this.plugin.postTopic();
|
||
|
||
// Show notice
|
||
noticeContainer.empty();
|
||
if (reply.message === 'Success') {
|
||
noticeContainer.createEl('div', {
|
||
cls: 'notice success',
|
||
text: isUpdate ? t('UPDATE_SUCCESS') : t('PUBLISH_SUCCESS')
|
||
});
|
||
// Close after success
|
||
setTimeout(() => {
|
||
this.close();
|
||
}, 1500);
|
||
} else {
|
||
const errorContainer = noticeContainer.createEl('div', { cls: 'notice error' });
|
||
errorContainer.createEl('div', {
|
||
cls: 'error-title',
|
||
text: isUpdate ? t('UPDATE_ERROR') : t('PUBLISH_ERROR')
|
||
});
|
||
|
||
// Show Discourse-specific error information
|
||
errorContainer.createEl('div', {
|
||
cls: 'error-message',
|
||
text: reply.error || (isUpdate ? t('UPDATE_FAILED') + ', ' + t('TRY_AGAIN') : t('PUBLISH_FAILED') + ', ' + t('TRY_AGAIN'))
|
||
});
|
||
|
||
// Add retry button
|
||
const retryButton = errorContainer.createEl('button', {
|
||
cls: 'retry-button',
|
||
text: t('RETRY')
|
||
});
|
||
retryButton.onclick = () => {
|
||
noticeContainer.empty();
|
||
submitButton.disabled = false;
|
||
submitButton.textContent = isUpdate ? t('UPDATE') : t('PUBLISH');
|
||
};
|
||
}
|
||
} catch (error) {
|
||
noticeContainer.empty();
|
||
const errorContainer = noticeContainer.createEl('div', { cls: 'notice error' });
|
||
errorContainer.createEl('div', {
|
||
cls: 'error-title',
|
||
text: isUpdate ? t('UPDATE_ERROR') : t('PUBLISH_ERROR')
|
||
});
|
||
errorContainer.createEl('div', {
|
||
cls: 'error-message',
|
||
text: error.message || t('UNKNOWN_ERROR')
|
||
});
|
||
|
||
// Add retry button
|
||
const retryButton = errorContainer.createEl('button', {
|
||
cls: 'retry-button',
|
||
text: t('RETRY')
|
||
});
|
||
retryButton.onclick = () => {
|
||
noticeContainer.empty();
|
||
submitButton.disabled = false;
|
||
submitButton.textContent = isUpdate ? t('UPDATE') : t('PUBLISH');
|
||
};
|
||
}
|
||
|
||
// If error occurs, reset button state
|
||
if (submitButton.disabled) {
|
||
submitButton.disabled = false;
|
||
submitButton.textContent = isUpdate ? t('UPDATE') : t('PUBLISH');
|
||
}
|
||
};
|
||
}
|
||
|
||
onClose() {
|
||
const { contentEl } = this;
|
||
contentEl.empty();
|
||
}
|
||
}
|