mirror of
https://github.com/woodchen-ink/obsidian-publish-to-discourse.git
synced 2025-07-18 05:42:05 +08:00
Add internationalization (i18n) support
- Introduced i18n framework with translation support - Updated config, main, and styles files to use translation keys - Implemented dynamic locale setting based on Obsidian's language - Replaced hardcoded Chinese strings with translatable keys - Added locale change event listener to update translations dynamically
This commit is contained in:
parent
9d64d31947
commit
f27ada6a4e
@ -1,5 +1,6 @@
|
|||||||
import { PluginSettingTab, Setting, App } from 'obsidian';
|
import { PluginSettingTab, Setting, App } from 'obsidian';
|
||||||
import DiscourseSyncPlugin from './main';
|
import DiscourseSyncPlugin from './main';
|
||||||
|
import { t } from './i18n';
|
||||||
|
|
||||||
export interface DiscourseSyncSettings {
|
export interface DiscourseSyncSettings {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
@ -28,8 +29,8 @@ export class DiscourseSyncSettingsTab extends PluginSettingTab {
|
|||||||
containerEl.empty();
|
containerEl.empty();
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName("论坛地址")
|
.setName(t('FORUM_URL'))
|
||||||
.setDesc("Discourse 论坛的网址")
|
.setDesc(t('FORUM_URL_DESC'))
|
||||||
.addText((text) =>
|
.addText((text) =>
|
||||||
text
|
text
|
||||||
.setPlaceholder("https://forum.example.com")
|
.setPlaceholder("https://forum.example.com")
|
||||||
@ -41,8 +42,8 @@ export class DiscourseSyncSettingsTab extends PluginSettingTab {
|
|||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName("API 密钥")
|
.setName(t('API_KEY'))
|
||||||
.setDesc("在'/admin/api/keys'中创建的 API 密钥")
|
.setDesc(t('API_KEY_DESC'))
|
||||||
.addText((text) =>
|
.addText((text) =>
|
||||||
text
|
text
|
||||||
.setPlaceholder("api_key")
|
.setPlaceholder("api_key")
|
||||||
@ -54,8 +55,8 @@ export class DiscourseSyncSettingsTab extends PluginSettingTab {
|
|||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName("用户名")
|
.setName(t('USERNAME'))
|
||||||
.setDesc("Discourse 用户名")
|
.setDesc(t('USERNAME_DESC'))
|
||||||
.addText((text) =>
|
.addText((text) =>
|
||||||
text
|
text
|
||||||
.setPlaceholder("username")
|
.setPlaceholder("username")
|
||||||
|
37
src/i18n/en.ts
Normal file
37
src/i18n/en.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
export default {
|
||||||
|
// Settings page
|
||||||
|
'FORUM_URL': 'Forum URL',
|
||||||
|
'FORUM_URL_DESC': 'The URL of your Discourse forum',
|
||||||
|
'API_KEY': 'API Key',
|
||||||
|
'API_KEY_DESC': "API key created in '/admin/api/keys'",
|
||||||
|
'USERNAME': 'Username',
|
||||||
|
'USERNAME_DESC': 'Your Discourse username',
|
||||||
|
|
||||||
|
// Publish page
|
||||||
|
'PUBLISH_TO_DISCOURSE': 'Publish to Discourse',
|
||||||
|
'UPDATE_POST': 'Update Post',
|
||||||
|
'CATEGORY': 'Category',
|
||||||
|
'TAGS': 'Tags',
|
||||||
|
'ENTER_TAG': 'Enter tag name (press Enter to add)',
|
||||||
|
'ENTER_TAG_WITH_CREATE': 'Enter tag name (can create new tags)',
|
||||||
|
'PUBLISHING': 'Publishing...',
|
||||||
|
'UPDATING': 'Updating...',
|
||||||
|
'PUBLISH': 'Publish',
|
||||||
|
'UPDATE': 'Update',
|
||||||
|
'RETRY': 'Retry',
|
||||||
|
|
||||||
|
// Success messages
|
||||||
|
'PUBLISH_SUCCESS': '✓ Published successfully!',
|
||||||
|
'UPDATE_SUCCESS': '✓ Updated successfully!',
|
||||||
|
|
||||||
|
// Error messages
|
||||||
|
'PUBLISH_FAILED': 'Publish failed',
|
||||||
|
'UPDATE_FAILED': 'Update failed',
|
||||||
|
'PUBLISH_ERROR': 'Publish error',
|
||||||
|
'UPDATE_ERROR': 'Update error',
|
||||||
|
'PERMISSION_ERROR': 'Insufficient permissions, can only use existing tags',
|
||||||
|
'UNKNOWN_ERROR': 'Unknown error',
|
||||||
|
'TRY_AGAIN': 'Please try again',
|
||||||
|
'POST_ID_ERROR': 'Published successfully but failed to get post ID',
|
||||||
|
'SAVE_POST_ID_ERROR': 'Published successfully but failed to save post ID',
|
||||||
|
}
|
30
src/i18n/index.ts
Normal file
30
src/i18n/index.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { moment } from 'obsidian';
|
||||||
|
import zhCN from './zh-CN';
|
||||||
|
import en from './en';
|
||||||
|
|
||||||
|
const localeMap: { [key: string]: any } = {
|
||||||
|
'zh': zhCN,
|
||||||
|
'zh-cn': zhCN,
|
||||||
|
'en': en,
|
||||||
|
'en-us': en,
|
||||||
|
'en-gb': en,
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentLocale = 'en';
|
||||||
|
|
||||||
|
export function setLocale(locale: string) {
|
||||||
|
const normalizedLocale = locale.toLowerCase();
|
||||||
|
if (localeMap[normalizedLocale]) {
|
||||||
|
currentLocale = normalizedLocale;
|
||||||
|
moment.locale(normalizedLocale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentLocale(): string {
|
||||||
|
return currentLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function t(key: string): string {
|
||||||
|
const translations = localeMap[currentLocale] || localeMap['en'];
|
||||||
|
return translations[key] || localeMap['en'][key] || key;
|
||||||
|
}
|
37
src/i18n/zh-CN.ts
Normal file
37
src/i18n/zh-CN.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
export default {
|
||||||
|
// 设置页面
|
||||||
|
'FORUM_URL': '论坛地址',
|
||||||
|
'FORUM_URL_DESC': 'Discourse 论坛的网址',
|
||||||
|
'API_KEY': 'API 密钥',
|
||||||
|
'API_KEY_DESC': "在'/admin/api/keys'中创建的 API 密钥",
|
||||||
|
'USERNAME': '用户名',
|
||||||
|
'USERNAME_DESC': 'Discourse 用户名',
|
||||||
|
|
||||||
|
// 发布页面
|
||||||
|
'PUBLISH_TO_DISCOURSE': '发布到 Discourse',
|
||||||
|
'UPDATE_POST': '更新帖子',
|
||||||
|
'CATEGORY': '分类',
|
||||||
|
'TAGS': '标签',
|
||||||
|
'ENTER_TAG': '输入标签名称(回车添加)',
|
||||||
|
'ENTER_TAG_WITH_CREATE': '输入标签名称(可创建新标签)',
|
||||||
|
'PUBLISHING': '发布中...',
|
||||||
|
'UPDATING': '更新中...',
|
||||||
|
'PUBLISH': '发布',
|
||||||
|
'UPDATE': '更新',
|
||||||
|
'RETRY': '重试',
|
||||||
|
|
||||||
|
// 成功提示
|
||||||
|
'PUBLISH_SUCCESS': '✓ 发布成功!',
|
||||||
|
'UPDATE_SUCCESS': '✓ 更新成功!',
|
||||||
|
|
||||||
|
// 错误提示
|
||||||
|
'PUBLISH_FAILED': '发布失败',
|
||||||
|
'UPDATE_FAILED': '更新失败',
|
||||||
|
'PUBLISH_ERROR': '发布出错',
|
||||||
|
'UPDATE_ERROR': '更新出错',
|
||||||
|
'PERMISSION_ERROR': '权限不足,只能使用已有的标签',
|
||||||
|
'UNKNOWN_ERROR': '未知错误',
|
||||||
|
'TRY_AGAIN': '请重试',
|
||||||
|
'POST_ID_ERROR': '发布成功但无法获取帖子ID',
|
||||||
|
'SAVE_POST_ID_ERROR': '发布成功但无法保存帖子ID',
|
||||||
|
}
|
155
src/main.ts
155
src/main.ts
@ -1,15 +1,26 @@
|
|||||||
import { App, Menu, MenuItem, Plugin, Modal, requestUrl, TFile } from 'obsidian';
|
import { App, Menu, MenuItem, Plugin, Modal, requestUrl, TFile, moment } from 'obsidian';
|
||||||
import { DEFAULT_SETTINGS, DiscourseSyncSettings, DiscourseSyncSettingsTab } from './config';
|
import { DEFAULT_SETTINGS, DiscourseSyncSettings, DiscourseSyncSettingsTab } from './config';
|
||||||
import * as yaml from 'yaml';
|
import * as yaml from 'yaml';
|
||||||
|
import { t, setLocale } from './i18n';
|
||||||
|
|
||||||
export default class DiscourseSyncPlugin extends Plugin {
|
export default class DiscourseSyncPlugin extends Plugin {
|
||||||
settings: DiscourseSyncSettings;
|
settings: DiscourseSyncSettings;
|
||||||
activeFile: {
|
activeFile: {
|
||||||
name: string;
|
name: string;
|
||||||
content: string;
|
content: string;
|
||||||
postId?: number; // 添加帖子ID字段
|
postId?: number; // Post ID field
|
||||||
};
|
};
|
||||||
async onload() {
|
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();
|
await this.loadSettings();
|
||||||
this.addSettingTab(new DiscourseSyncSettingsTab(this.app, this));
|
this.addSettingTab(new DiscourseSyncSettingsTab(this.app, this));
|
||||||
this.registerEvent(
|
this.registerEvent(
|
||||||
@ -20,7 +31,7 @@ export default class DiscourseSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "category-modal",
|
id: "category-modal",
|
||||||
name: "发布到 Discourse",
|
name: t('PUBLISH_TO_DISCOURSE'),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
this.openCategoryModal();
|
this.openCategoryModal();
|
||||||
},
|
},
|
||||||
@ -118,11 +129,11 @@ export default class DiscourseSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
let content = this.activeFile.content;
|
let content = this.activeFile.content;
|
||||||
|
|
||||||
// 检查是否包含帖子ID的frontmatter
|
// Check if frontmatter contains post ID
|
||||||
const frontMatter = this.getFrontMatter(content);
|
const frontMatter = this.getFrontMatter(content);
|
||||||
const postId = frontMatter?.discourse_post_id;
|
const postId = frontMatter?.discourse_post_id;
|
||||||
|
|
||||||
// 过滤掉笔记属性部分
|
// Filter out note properties section
|
||||||
content = content.replace(/^---[\s\S]*?---\n/, '');
|
content = content.replace(/^---[\s\S]*?---\n/, '');
|
||||||
|
|
||||||
const imageReferences = this.extractImageReferences(content);
|
const imageReferences = this.extractImageReferences(content);
|
||||||
@ -162,20 +173,20 @@ export default class DiscourseSyncPlugin extends Plugin {
|
|||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
if (!isUpdate) {
|
if (!isUpdate) {
|
||||||
try {
|
try {
|
||||||
// 获取新帖子的ID
|
// Get new post ID
|
||||||
const responseData = response.json;
|
const responseData = response.json;
|
||||||
if (responseData && responseData.id) {
|
if (responseData && responseData.id) {
|
||||||
await this.updateFrontMatter(responseData.id);
|
await this.updateFrontMatter(responseData.id);
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
message: "Error",
|
message: "Error",
|
||||||
error: "发布成功但无法获取帖子ID"
|
error: t('POST_ID_ERROR')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
message: "Error",
|
message: "Error",
|
||||||
error: "发布成功但无法保存帖子ID"
|
error: t('SAVE_POST_ID_ERROR')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -198,20 +209,20 @@ export default class DiscourseSyncPlugin extends Plugin {
|
|||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
return {
|
return {
|
||||||
message: "Error",
|
message: "Error",
|
||||||
error: `${isUpdate ? '更新' : '发布'}失败 (${response.status})`
|
error: `${isUpdate ? t('UPDATE_FAILED') : t('PUBLISH_FAILED')} (${response.status})`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
message: "Error",
|
message: "Error",
|
||||||
error: `${isUpdate ? '更新' : '发布'}失败: ${error.message || '未知错误'}`
|
error: `${isUpdate ? t('UPDATE_FAILED') : t('PUBLISH_FAILED')}: ${error.message || t('UNKNOWN_ERROR')}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { message: "Error", error: `${isUpdate ? '更新' : '发布'}失败,请重试` };
|
return { message: "Error", error: `${isUpdate ? t('UPDATE_FAILED') : t('PUBLISH_FAILED')}, ${t('TRY_AGAIN')}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取frontmatter数据
|
// Get frontmatter data
|
||||||
private getFrontMatter(content: string): any {
|
private getFrontMatter(content: string): any {
|
||||||
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||||
if (fmMatch) {
|
if (fmMatch) {
|
||||||
@ -224,10 +235,10 @@ export default class DiscourseSyncPlugin extends Plugin {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新frontmatter,添加帖子ID
|
// Update frontmatter, add post ID
|
||||||
private async updateFrontMatter(postId: number) {
|
private async updateFrontMatter(postId: number) {
|
||||||
try {
|
try {
|
||||||
// 获取当前活动文件
|
// Get current active file
|
||||||
const activeFile = this.app.workspace.getActiveFile();
|
const activeFile = this.app.workspace.getActiveFile();
|
||||||
if (!activeFile) {
|
if (!activeFile) {
|
||||||
return;
|
return;
|
||||||
@ -238,11 +249,11 @@ export default class DiscourseSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
let newContent: string;
|
let newContent: string;
|
||||||
if (fm) {
|
if (fm) {
|
||||||
// 更新现有的frontmatter
|
// Update existing frontmatter
|
||||||
const updatedFm = { ...fm, discourse_post_id: postId };
|
const updatedFm = { ...fm, discourse_post_id: postId };
|
||||||
newContent = content.replace(/^---\n[\s\S]*?\n---\n/, `---\n${yaml.stringify(updatedFm)}---\n`);
|
newContent = content.replace(/^---\n[\s\S]*?\n---\n/, `---\n${yaml.stringify(updatedFm)}---\n`);
|
||||||
} else {
|
} else {
|
||||||
// 添加新的frontmatter
|
// Add new frontmatter
|
||||||
const newFm = { discourse_post_id: postId };
|
const newFm = { discourse_post_id: postId };
|
||||||
newContent = `---\n${yaml.stringify(newFm)}---\n${content}`;
|
newContent = `---\n${yaml.stringify(newFm)}---\n${content}`;
|
||||||
}
|
}
|
||||||
@ -251,7 +262,7 @@ export default class DiscourseSyncPlugin extends Plugin {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
message: "Error",
|
message: "Error",
|
||||||
error: "更新失败"
|
error: t('UPDATE_FAILED')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -309,9 +320,9 @@ export default class DiscourseSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
const data = response.json;
|
const data = response.json;
|
||||||
// 获取用户权限信息
|
// Get user permissions
|
||||||
const canCreateTags = await this.checkCanCreateTags();
|
const canCreateTags = await this.checkCanCreateTags();
|
||||||
// Discourse 返回的 tags 列表在 tags 数组中
|
// Tags list returned by Discourse is in the tags array
|
||||||
return data.tags.map((tag: any) => ({
|
return data.tags.map((tag: any) => ({
|
||||||
name: tag.name,
|
name: tag.name,
|
||||||
canCreate: canCreateTags
|
canCreate: canCreateTags
|
||||||
@ -323,7 +334,7 @@ export default class DiscourseSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查用户是否有创建标签的权限
|
// Check if user has permission to create tags
|
||||||
private async checkCanCreateTags(): Promise<boolean> {
|
private async checkCanCreateTags(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const url = `${this.settings.baseUrl}/u/${this.settings.disUser}.json`;
|
const url = `${this.settings.baseUrl}/u/${this.settings.disUser}.json`;
|
||||||
@ -342,7 +353,7 @@ export default class DiscourseSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
const data = response.json;
|
const data = response.json;
|
||||||
// 检查用户的 trust_level
|
// Check user's trust_level
|
||||||
return data.user.trust_level >= 3;
|
return data.user.trust_level >= 3;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -353,7 +364,7 @@ export default class DiscourseSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
registerDirMenu(menu: Menu, file: TFile) {
|
registerDirMenu(menu: Menu, file: TFile) {
|
||||||
const syncDiscourse = (item: MenuItem) => {
|
const syncDiscourse = (item: MenuItem) => {
|
||||||
item.setTitle("发布到 Discourse");
|
item.setTitle(t('PUBLISH_TO_DISCOURSE'));
|
||||||
item.onClick(async () => {
|
item.onClick(async () => {
|
||||||
const content = await this.app.vault.read(file);
|
const content = await this.app.vault.read(file);
|
||||||
const fm = this.getFrontMatter(content);
|
const fm = this.getFrontMatter(content);
|
||||||
@ -441,23 +452,23 @@ export class SelectCategoryModal extends Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
// 添加模态框基础样式
|
// Add modal base style
|
||||||
this.modalEl.addClass('mod-discourse-sync');
|
this.modalEl.addClass('mod-discourse-sync');
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
contentEl.addClass('discourse-sync-modal');
|
contentEl.addClass('discourse-sync-modal');
|
||||||
|
|
||||||
const isUpdate = this.plugin.activeFile.postId !== undefined;
|
const isUpdate = this.plugin.activeFile.postId !== undefined;
|
||||||
contentEl.createEl("h1", { text: isUpdate ? '更新帖子' : '发布到 Discourse' });
|
contentEl.createEl("h1", { text: isUpdate ? t('UPDATE_POST') : t('PUBLISH_TO_DISCOURSE') });
|
||||||
|
|
||||||
// 创建表单区域容器
|
// Create form area container
|
||||||
const formArea = contentEl.createEl('div', { cls: 'form-area' });
|
const formArea = contentEl.createEl('div', { cls: 'form-area' });
|
||||||
|
|
||||||
// 创建选择器容器
|
// Create selector container
|
||||||
const selectContainer = formArea.createEl('div', { cls: 'select-container' });
|
const selectContainer = formArea.createEl('div', { cls: 'select-container' });
|
||||||
if (!isUpdate) {
|
if (!isUpdate) {
|
||||||
// 只在新建帖子时显示分类选择
|
// Only show category selector when creating a new post
|
||||||
selectContainer.createEl('label', { text: '分类' });
|
selectContainer.createEl('label', { text: t('CATEGORY') });
|
||||||
const selectEl = selectContainer.createEl('select');
|
const selectEl = selectContainer.createEl('select');
|
||||||
this.categories.forEach(category => {
|
this.categories.forEach(category => {
|
||||||
const option = selectEl.createEl('option', { text: category.name });
|
const option = selectEl.createEl('option', { text: category.name });
|
||||||
@ -465,18 +476,18 @@ export class SelectCategoryModal extends Modal {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建标签选择容器
|
// Create tag selector container
|
||||||
const tagContainer = formArea.createEl('div', { cls: 'tag-container' });
|
const tagContainer = formArea.createEl('div', { cls: 'tag-container' });
|
||||||
tagContainer.createEl('label', { text: '标签' });
|
tagContainer.createEl('label', { text: t('TAGS') });
|
||||||
|
|
||||||
// 创建标签选择区域
|
// Create tag selector area
|
||||||
const tagSelectArea = tagContainer.createEl('div', { cls: 'tag-select-area' });
|
const tagSelectArea = tagContainer.createEl('div', { cls: 'tag-select-area' });
|
||||||
|
|
||||||
// 已选标签显示区域
|
// Selected tags display area
|
||||||
const selectedTagsContainer = tagSelectArea.createEl('div', { cls: 'selected-tags' });
|
const selectedTagsContainer = tagSelectArea.createEl('div', { cls: 'selected-tags' });
|
||||||
const selectedTags = new Set<string>();
|
const selectedTags = new Set<string>();
|
||||||
|
|
||||||
// 更新已选标签显示
|
// Update selected tags display
|
||||||
const updateSelectedTags = () => {
|
const updateSelectedTags = () => {
|
||||||
selectedTagsContainer.empty();
|
selectedTagsContainer.empty();
|
||||||
selectedTags.forEach(tag => {
|
selectedTags.forEach(tag => {
|
||||||
@ -495,19 +506,19 @@ export class SelectCategoryModal extends Modal {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建标签输入容器
|
// Create tag input container
|
||||||
const tagInputContainer = tagSelectArea.createEl('div', { cls: 'tag-input-container' });
|
const tagInputContainer = tagSelectArea.createEl('div', { cls: 'tag-input-container' });
|
||||||
|
|
||||||
// 创建标签输入和建议
|
// Create tag input and suggestions
|
||||||
const tagInput = tagInputContainer.createEl('input', {
|
const tagInput = tagInputContainer.createEl('input', {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
placeholder: this.canCreateTags ? '输入标签名称(回车添加)' : '输入标签名称(回车添加)'
|
placeholder: this.canCreateTags ? t('ENTER_TAG_WITH_CREATE') : t('ENTER_TAG')
|
||||||
});
|
});
|
||||||
|
|
||||||
// 创建标签建议容器
|
// Create tag suggestions container
|
||||||
const tagSuggestions = tagInputContainer.createEl('div', { cls: 'tag-suggestions' });
|
const tagSuggestions = tagInputContainer.createEl('div', { cls: 'tag-suggestions' });
|
||||||
|
|
||||||
// 处理输入事件,显示匹配的标签
|
// Handle input event, show matching tags
|
||||||
tagInput.oninput = () => {
|
tagInput.oninput = () => {
|
||||||
const value = tagInput.value.toLowerCase();
|
const value = tagInput.value.toLowerCase();
|
||||||
tagSuggestions.empty();
|
tagSuggestions.empty();
|
||||||
@ -521,14 +532,14 @@ export class SelectCategoryModal extends Modal {
|
|||||||
.slice(0, 10);
|
.slice(0, 10);
|
||||||
|
|
||||||
if (matches.length > 0) {
|
if (matches.length > 0) {
|
||||||
// 获取输入框的位置和宽度
|
// Get input box position and width
|
||||||
const inputRect = tagInput.getBoundingClientRect();
|
const inputRect = tagInput.getBoundingClientRect();
|
||||||
const modalRect = this.modalEl.getBoundingClientRect();
|
const modalRect = this.modalEl.getBoundingClientRect();
|
||||||
|
|
||||||
// 确保建议列表不会超出模态框宽度
|
// Ensure suggestions list doesn't exceed modal width
|
||||||
const maxWidth = modalRect.right - inputRect.left - 24; // 24px 是右侧padding
|
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.top = `${inputRect.bottom + 4}px`;
|
||||||
tagSuggestions.style.left = `${inputRect.left}px`;
|
tagSuggestions.style.left = `${inputRect.left}px`;
|
||||||
tagSuggestions.style.width = `${Math.min(inputRect.width, maxWidth)}px`;
|
tagSuggestions.style.width = `${Math.min(inputRect.width, maxWidth)}px`;
|
||||||
@ -549,7 +560,7 @@ export class SelectCategoryModal extends Modal {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理回车事件
|
// Handle enter event
|
||||||
tagInput.onkeydown = (e) => {
|
tagInput.onkeydown = (e) => {
|
||||||
if (e.key === 'Enter' && tagInput.value) {
|
if (e.key === 'Enter' && tagInput.value) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -563,10 +574,10 @@ export class SelectCategoryModal extends Modal {
|
|||||||
selectedTags.add(value);
|
selectedTags.add(value);
|
||||||
updateSelectedTags();
|
updateSelectedTags();
|
||||||
} else {
|
} else {
|
||||||
// 显示权限提示
|
// Show permission notice
|
||||||
const notice = contentEl.createEl('div', {
|
const notice = contentEl.createEl('div', {
|
||||||
cls: 'tag-notice',
|
cls: 'tag-notice',
|
||||||
text: '权限不足,只能使用已有的标签'
|
text: t('PERMISSION_ERROR')
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
notice.remove();
|
notice.remove();
|
||||||
@ -578,15 +589,15 @@ export class SelectCategoryModal extends Modal {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理失焦事件,隐藏建议
|
// Handle blur event, hide suggestions
|
||||||
tagInput.onblur = () => {
|
tagInput.onblur = () => {
|
||||||
// 延迟隐藏,以便可以点击建议
|
// Delay hide, so suggestions can be clicked
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
tagSuggestions.empty();
|
tagSuggestions.empty();
|
||||||
}, 200);
|
}, 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理窗口滚动时更新建议列表位置
|
// Handle window scroll, update suggestions list position
|
||||||
const updateSuggestionsPosition = () => {
|
const updateSuggestionsPosition = () => {
|
||||||
if (tagSuggestions.childNodes.length > 0) {
|
if (tagSuggestions.childNodes.length > 0) {
|
||||||
const inputRect = tagInput.getBoundingClientRect();
|
const inputRect = tagInput.getBoundingClientRect();
|
||||||
@ -596,22 +607,22 @@ export class SelectCategoryModal extends Modal {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听滚动事件
|
// Listen for scroll event
|
||||||
this.modalEl.addEventListener('scroll', updateSuggestionsPosition);
|
this.modalEl.addEventListener('scroll', updateSuggestionsPosition);
|
||||||
|
|
||||||
// 在模态框关闭时移除事件监听
|
// Remove event listeners when modal closes
|
||||||
this.modalEl.onclose = () => {
|
this.modalEl.onclose = () => {
|
||||||
this.modalEl.removeEventListener('scroll', updateSuggestionsPosition);
|
this.modalEl.removeEventListener('scroll', updateSuggestionsPosition);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建按钮区域
|
// Create button area
|
||||||
const buttonArea = contentEl.createEl('div', { cls: 'button-area' });
|
const buttonArea = contentEl.createEl('div', { cls: 'button-area' });
|
||||||
const submitButton = buttonArea.createEl('button', {
|
const submitButton = buttonArea.createEl('button', {
|
||||||
text: isUpdate ? '更新' : '发布',
|
text: isUpdate ? t('UPDATE') : t('PUBLISH'),
|
||||||
cls: 'submit-button'
|
cls: 'submit-button'
|
||||||
});
|
});
|
||||||
|
|
||||||
// 创建提示信息容器
|
// Create notice container
|
||||||
const noticeContainer = buttonArea.createEl('div');
|
const noticeContainer = buttonArea.createEl('div');
|
||||||
|
|
||||||
submitButton.onclick = async () => {
|
submitButton.onclick = async () => {
|
||||||
@ -625,25 +636,25 @@ export class SelectCategoryModal extends Modal {
|
|||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存选中的标签
|
// Save selected tags
|
||||||
this.plugin.settings.selectedTags = Array.from(selectedTags);
|
this.plugin.settings.selectedTags = Array.from(selectedTags);
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
|
|
||||||
// 禁用提交按钮,显示加载状态
|
// Disable submit button, show loading state
|
||||||
submitButton.disabled = true;
|
submitButton.disabled = true;
|
||||||
submitButton.textContent = isUpdate ? '更新中...' : '发布中...';
|
submitButton.textContent = isUpdate ? t('UPDATING') : t('PUBLISHING');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const reply = await this.plugin.postTopic();
|
const reply = await this.plugin.postTopic();
|
||||||
|
|
||||||
// 显示提示信息
|
// Show notice
|
||||||
noticeContainer.empty();
|
noticeContainer.empty();
|
||||||
if (reply.message === 'Success') {
|
if (reply.message === 'Success') {
|
||||||
noticeContainer.createEl('div', {
|
noticeContainer.createEl('div', {
|
||||||
cls: 'notice success',
|
cls: 'notice success',
|
||||||
text: isUpdate ? '✓ 更新成功!' : '✓ 发布成功!'
|
text: isUpdate ? t('UPDATE_SUCCESS') : t('PUBLISH_SUCCESS')
|
||||||
});
|
});
|
||||||
// 成功后延迟关闭
|
// Close after success
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.close();
|
this.close();
|
||||||
}, 1500);
|
}, 1500);
|
||||||
@ -651,24 +662,24 @@ export class SelectCategoryModal extends Modal {
|
|||||||
const errorContainer = noticeContainer.createEl('div', { cls: 'notice error' });
|
const errorContainer = noticeContainer.createEl('div', { cls: 'notice error' });
|
||||||
errorContainer.createEl('div', {
|
errorContainer.createEl('div', {
|
||||||
cls: 'error-title',
|
cls: 'error-title',
|
||||||
text: isUpdate ? '更新失败' : '发布失败'
|
text: isUpdate ? t('UPDATE_ERROR') : t('PUBLISH_ERROR')
|
||||||
});
|
});
|
||||||
|
|
||||||
// 显示 Discourse 返回的具体错误信息
|
// Show Discourse-specific error information
|
||||||
errorContainer.createEl('div', {
|
errorContainer.createEl('div', {
|
||||||
cls: 'error-message',
|
cls: 'error-message',
|
||||||
text: reply.error || (isUpdate ? '更新失败,请重试' : '发布失败,请重试')
|
text: reply.error || (isUpdate ? t('UPDATE_FAILED') + ', ' + t('TRY_AGAIN') : t('PUBLISH_FAILED') + ', ' + t('TRY_AGAIN'))
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加重试按钮
|
// Add retry button
|
||||||
const retryButton = errorContainer.createEl('button', {
|
const retryButton = errorContainer.createEl('button', {
|
||||||
cls: 'retry-button',
|
cls: 'retry-button',
|
||||||
text: '重试'
|
text: t('RETRY')
|
||||||
});
|
});
|
||||||
retryButton.onclick = () => {
|
retryButton.onclick = () => {
|
||||||
noticeContainer.empty();
|
noticeContainer.empty();
|
||||||
submitButton.disabled = false;
|
submitButton.disabled = false;
|
||||||
submitButton.textContent = isUpdate ? '更新' : '发布';
|
submitButton.textContent = isUpdate ? t('UPDATE') : t('PUBLISH');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -676,29 +687,29 @@ export class SelectCategoryModal extends Modal {
|
|||||||
const errorContainer = noticeContainer.createEl('div', { cls: 'notice error' });
|
const errorContainer = noticeContainer.createEl('div', { cls: 'notice error' });
|
||||||
errorContainer.createEl('div', {
|
errorContainer.createEl('div', {
|
||||||
cls: 'error-title',
|
cls: 'error-title',
|
||||||
text: isUpdate ? '更新出错' : '发布出错'
|
text: isUpdate ? t('UPDATE_ERROR') : t('PUBLISH_ERROR')
|
||||||
});
|
});
|
||||||
errorContainer.createEl('div', {
|
errorContainer.createEl('div', {
|
||||||
cls: 'error-message',
|
cls: 'error-message',
|
||||||
text: error.message || '未知错误'
|
text: error.message || t('UNKNOWN_ERROR')
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加重试按钮
|
// Add retry button
|
||||||
const retryButton = errorContainer.createEl('button', {
|
const retryButton = errorContainer.createEl('button', {
|
||||||
cls: 'retry-button',
|
cls: 'retry-button',
|
||||||
text: '重试'
|
text: t('RETRY')
|
||||||
});
|
});
|
||||||
retryButton.onclick = () => {
|
retryButton.onclick = () => {
|
||||||
noticeContainer.empty();
|
noticeContainer.empty();
|
||||||
submitButton.disabled = false;
|
submitButton.disabled = false;
|
||||||
submitButton.textContent = isUpdate ? '更新' : '发布';
|
submitButton.textContent = isUpdate ? t('UPDATE') : t('PUBLISH');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果发生错误,重置按钮状态
|
// If error occurs, reset button state
|
||||||
if (submitButton.disabled) {
|
if (submitButton.disabled) {
|
||||||
submitButton.disabled = false;
|
submitButton.disabled = false;
|
||||||
submitButton.textContent = isUpdate ? '更新' : '发布';
|
submitButton.textContent = isUpdate ? t('UPDATE') : t('PUBLISH');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
36
styles.css
36
styles.css
@ -7,7 +7,7 @@ If your plugin does not need CSS, delete this file.
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* 基础模态框样式覆盖 */
|
/* Basic Modal Style Override */
|
||||||
.modal.mod-discourse-sync {
|
.modal.mod-discourse-sync {
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
@ -18,7 +18,7 @@ If your plugin does not need CSS, delete this file.
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 内容区域样式 */
|
/* Content Area Style */
|
||||||
.discourse-sync-modal {
|
.discourse-sync-modal {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -28,14 +28,14 @@ If your plugin does not need CSS, delete this file.
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表单区域 */
|
/* Form Area */
|
||||||
.discourse-sync-modal .form-area {
|
.discourse-sync-modal .form-area {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 按钮区域固定在底部 */
|
/* Button Area Fixed at Bottom */
|
||||||
.discourse-sync-modal .button-area {
|
.discourse-sync-modal .button-area {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
@ -48,11 +48,11 @@ If your plugin does not need CSS, delete this file.
|
|||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表单容器通用样式 */
|
/* Common Form Container Style */
|
||||||
.discourse-sync-modal .select-container,
|
.discourse-sync-modal .select-container,
|
||||||
.discourse-sync-modal .tag-container {
|
.discourse-sync-modal .tag-container {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
padding: 0; /* 移除内边距 */
|
padding: 0; /* Remove padding */
|
||||||
}
|
}
|
||||||
|
|
||||||
.discourse-sync-modal .select-container label,
|
.discourse-sync-modal .select-container label,
|
||||||
@ -63,7 +63,7 @@ If your plugin does not need CSS, delete this file.
|
|||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 输入框和选择器样式 */
|
/* Input and Selector Style */
|
||||||
.discourse-sync-modal select,
|
.discourse-sync-modal select,
|
||||||
.discourse-sync-modal .tag-select-area {
|
.discourse-sync-modal .tag-select-area {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -108,7 +108,7 @@ If your plugin does not need CSS, delete this file.
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 提示信息样式 */
|
/* Notice Style */
|
||||||
.discourse-sync-modal .notice {
|
.discourse-sync-modal .notice {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
@ -171,7 +171,7 @@ If your plugin does not need CSS, delete this file.
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 标签选择区域样式 */
|
/* Tag Select Area Style */
|
||||||
.discourse-sync-modal .tag-select-area {
|
.discourse-sync-modal .tag-select-area {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -181,7 +181,7 @@ If your plugin does not need CSS, delete this file.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.discourse-sync-modal .selected-tags {
|
.discourse-sync-modal .selected-tags {
|
||||||
display: none; /* 默认隐藏 */
|
display: none; /* Hidden by default */
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
@ -189,7 +189,7 @@ If your plugin does not need CSS, delete this file.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.discourse-sync-modal .selected-tags:not(:empty) {
|
.discourse-sync-modal .selected-tags:not(:empty) {
|
||||||
display: flex; /* 当有内容时显示 */
|
display: flex; /* Show when has content */
|
||||||
}
|
}
|
||||||
|
|
||||||
.discourse-sync-modal .tag {
|
.discourse-sync-modal .tag {
|
||||||
@ -237,13 +237,13 @@ If your plugin does not need CSS, delete this file.
|
|||||||
border-bottom: 1px solid var(--background-modifier-border);
|
border-bottom: 1px solid var(--background-modifier-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 标签输入容器 */
|
/* Tag Input Container */
|
||||||
.discourse-sync-modal .tag-input-container {
|
.discourse-sync-modal .tag-input-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 标签建议下拉框 */
|
/* Tag Suggestion Dropdown */
|
||||||
.discourse-sync-modal .tag-suggestions {
|
.discourse-sync-modal .tag-suggestions {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background-color: var(--background-primary);
|
background-color: var(--background-primary);
|
||||||
@ -254,14 +254,14 @@ If your plugin does not need CSS, delete this file.
|
|||||||
max-height: 180px;
|
max-height: 180px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
display: none; /* 默认隐藏 */
|
display: none; /* Hidden by default */
|
||||||
}
|
}
|
||||||
|
|
||||||
.discourse-sync-modal .tag-suggestions:not(:empty) {
|
.discourse-sync-modal .tag-suggestions:not(:empty) {
|
||||||
display: block; /* 当有内容时显示 */
|
display: block; /* Show when has content */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 标签建议项 */
|
/* Tag Suggestion Item */
|
||||||
.discourse-sync-modal .tag-suggestion {
|
.discourse-sync-modal .tag-suggestion {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -279,7 +279,7 @@ If your plugin does not need CSS, delete this file.
|
|||||||
background-color: var(--background-modifier-hover);
|
background-color: var(--background-modifier-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 美化滚动条 */
|
/* Tag Suggestion Scrollbar Style */
|
||||||
.discourse-sync-modal .tag-suggestions::-webkit-scrollbar {
|
.discourse-sync-modal .tag-suggestions::-webkit-scrollbar {
|
||||||
width: 4px;
|
width: 4px;
|
||||||
}
|
}
|
||||||
@ -297,7 +297,7 @@ If your plugin does not need CSS, delete this file.
|
|||||||
background-color: var(--background-modifier-border-hover);
|
background-color: var(--background-modifier-border-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移除 tag-container 的 z-index,避免干扰 */
|
/* Remove tag-container z-index to avoid interference */
|
||||||
.discourse-sync-modal .tag-container {
|
.discourse-sync-modal .tag-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user