From 93a61ec239780b7a332b908c5733e15bdb2cf0b5 Mon Sep 17 00:00:00 2001 From: qq_27963509 Date: Sat, 16 Aug 2025 22:04:26 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=8F=8C?= =?UTF-8?q?=E8=AF=AD=E5=9B=BD=E9=99=85=E5=8C=96=E6=94=AF=E6=8C=81\n\n-=20?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E5=AE=8C=E6=95=B4=E7=9A=84i18n=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E6=9E=B6=E6=9E=84=EF=BC=8C=E6=94=AF=E6=8C=81=E8=8B=B1?= =?UTF-8?q?=E6=96=87=E5=92=8C=E4=B8=AD=E6=96=87\n-=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=89=80=E6=9C=89=E7=94=A8=E6=88=B7=E7=95=8C=E9=9D=A2=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E4=BB=A5=E6=94=AF=E6=8C=81=E5=9B=BD=E9=99=85=E5=8C=96?= =?UTF-8?q?\n-=20=E6=B7=BB=E5=8A=A0=E8=AF=AD=E8=A8=80=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=88=B0=E8=AE=BE=E7=BD=AE=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?\n-=20=E4=BF=AE=E5=A4=8DTypeScript=E9=85=8D=E7=BD=AE=E4=BB=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=8E=B0=E4=BB=A3JavaScript=E7=89=B9?= =?UTF-8?q?=E6=80=A7\n-=20=E5=AE=89=E8=A3=85@types/node=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E8=A7=A3=E5=86=B3=E7=B1=BB=E5=9E=8B=E5=AE=9A=E4=B9=89=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 11 +- package.json | 2 +- src/i18n/index.ts | 115 +++++++++++++++++++ src/i18n/loader.ts | 44 ++++++++ src/i18n/locales/en.ts | 189 +++++++++++++++++++++++++++++++ src/i18n/locales/zh-cn.ts | 196 +++++++++++++++++++++++++++++++++ src/main.ts | 28 +++-- src/model/confirm.ts | 11 +- src/model/extensionOverride.ts | 29 +++-- src/model/override.ts | 43 ++++---- src/settings/settings.ts | 102 ++++++++++------- src/utils.ts | 11 +- tsconfig.json | 7 +- 13 files changed, 678 insertions(+), 110 deletions(-) create mode 100644 src/i18n/index.ts create mode 100644 src/i18n/loader.ts create mode 100644 src/i18n/locales/en.ts create mode 100644 src/i18n/locales/zh-cn.ts diff --git a/package-lock.json b/package-lock.json index 908d5c1..6faf876 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "ts-md5": "^1.3.1" }, "devDependencies": { - "@types/node": "^16.11.6", + "@types/node": "^16.18.126", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "builtin-modules": "3.3.0", @@ -708,10 +708,11 @@ "dev": true }, "node_modules/@types/node": { - "version": "16.18.101", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-16.18.101.tgz", - "integrity": "sha512-AAsx9Rgz2IzG8KJ6tXd6ndNkVcu+GYB6U/SnFAaokSPNx2N7dcIIfnighYUNumvj6YS2q39Dejz5tT0NCV7CWA==", - "dev": true + "version": "16.18.126", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-16.18.126.tgz", + "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", diff --git a/package.json b/package.json index 350186f..ad6de26 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "author": "trganda", "license": "MIT", "devDependencies": { - "@types/node": "^16.11.6", + "@types/node": "^16.18.126", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "builtin-modules": "3.3.0", diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..b231281 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,115 @@ +import { moment } from 'obsidian'; + +// 支持的语言类型 +export type SupportedLanguage = 'en' | 'zh-cn'; + +// 翻译键值对接口 +export interface TranslationMap { + [key: string]: string | TranslationMap; +} + +// 当前语言设置 +let currentLanguage: SupportedLanguage = 'en'; + +// 语言包存储 +const translations: Record = { + 'en': {}, + 'zh-cn': {} +}; + +/** + * 设置当前语言 + * @param language 语言代码 + */ +export function setLanguage(language: SupportedLanguage): void { + currentLanguage = language; +} + +/** + * 获取当前语言 + * @returns 当前语言代码 + */ +export function getCurrentLanguage(): SupportedLanguage { + return currentLanguage; +} + +/** + * 注册语言包 + * @param language 语言代码 + * @param translationMap 翻译映射 + */ +export function registerTranslations(language: SupportedLanguage, translationMap: TranslationMap): void { + translations[language] = { ...translations[language], ...translationMap }; +} + +/** + * 获取翻译文本 + * @param key 翻译键,支持点分隔的嵌套键 + * @param params 可选的参数对象,用于字符串插值 + * @returns 翻译后的文本 + */ +export function t(key: string, params?: Record): string { + const keys = key.split('.'); + let value: any = translations[currentLanguage]; + + // 遍历嵌套键 + for (const k of keys) { + if (value && typeof value === 'object' && k in value) { + value = value[k]; + } else { + // 如果当前语言没有找到,尝试使用英文作为后备 + if (currentLanguage !== 'en') { + let fallbackValue: any = translations['en']; + for (const fk of keys) { + if (fallbackValue && typeof fallbackValue === 'object' && fk in fallbackValue) { + fallbackValue = fallbackValue[fk]; + } else { + fallbackValue = key; // 最终后备:返回键本身 + break; + } + } + value = fallbackValue; + } else { + value = key; // 返回键本身作为后备 + } + break; + } + } + + // 确保返回字符串 + let result = typeof value === 'string' ? value : key; + + // 处理参数插值 + if (params) { + Object.entries(params).forEach(([paramKey, paramValue]) => { + result = result.replace(new RegExp(`\\{${paramKey}\\}`, 'g'), String(paramValue)); + }); + } + + return result; +} + +/** + * 根据系统语言自动检测语言设置 + * @returns 检测到的语言代码 + */ +export function detectLanguage(): SupportedLanguage { + const locale = moment.locale(); + + // 检测中文 + if (locale.startsWith('zh')) { + return 'zh-cn'; + } + + // 默认返回英文 + return 'en'; +} + +/** + * 初始化i18n系统 + * @param language 可选的初始语言,如果不提供则自动检测 + */ +export function initI18n(language?: SupportedLanguage): void { + const initialLanguage = language || detectLanguage(); + setLanguage(initialLanguage); +} \ No newline at end of file diff --git a/src/i18n/loader.ts b/src/i18n/loader.ts new file mode 100644 index 0000000..0be988e --- /dev/null +++ b/src/i18n/loader.ts @@ -0,0 +1,44 @@ +import { registerTranslations, SupportedLanguage } from './index'; +import { en } from './locales/en'; +import { zhCn } from './locales/zh-cn'; + +/** + * 加载所有语言包 + */ +export function loadAllTranslations(): void { + // 注册英文语言包 + registerTranslations('en', en); + + // 注册中文语言包 + registerTranslations('zh-cn', zhCn); +} + +/** + * 获取支持的语言列表 + * @returns 支持的语言列表,包含代码和显示名称 + */ +export function getSupportedLanguages(): Array<{ code: SupportedLanguage; name: string; nativeName: string }> { + return [ + { + code: 'en', + name: 'English', + nativeName: 'English' + }, + { + code: 'zh-cn', + name: 'Chinese (Simplified)', + nativeName: '简体中文' + } + ]; +} + +/** + * 根据语言代码获取语言显示名称 + * @param code 语言代码 + * @returns 语言显示名称 + */ +export function getLanguageName(code: SupportedLanguage): string { + const languages = getSupportedLanguages(); + const language = languages.find(lang => lang.code === code); + return language ? language.nativeName : code; +} \ No newline at end of file diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts new file mode 100644 index 0000000..b8dd156 --- /dev/null +++ b/src/i18n/locales/en.ts @@ -0,0 +1,189 @@ +import { TranslationMap } from '../index'; + +export const en: TranslationMap = { + // 通用 + common: { + save: 'Save', + cancel: 'Cancel', + delete: 'Delete', + edit: 'Edit', + add: 'Add', + remove: 'Remove', + confirm: 'Confirm', + close: 'Close' + }, + + // 设置页面 + settings: { + title: 'Attachment Management Settings', + language: { + name: 'Language', + desc: 'Select the interface language' + }, + rootPath: { + name: 'Root path to save attachment', + desc: 'Select root path of attachment', + options: { + obsidian: 'Copy Obsidian settings', + inFolder: 'In the folder specified below', + nextToNote: 'Next to note in folder specified below' + } + }, + rootFolder: { + name: 'Root folder', + desc: 'Root folder of new attachment' + }, + attachmentPath: { + name: 'Attachment path', + desc: 'Path of attachment in root folder, available variables {{notepath}}, {{notename}}, {{parent}}' + }, + attachmentFormat: { + name: 'Attachment format', + desc: 'Define how to name the attachment file, available variables {{dates}}, {{notename}}, {{md5}} and {{originalname}}.' + }, + dateFormat: { + name: 'Date format', + desc: 'Moment date format to use', + linkText: 'Moment format options' + }, + autoRename: { + name: 'Automatically rename attachment', + desc: 'Automatically rename the attachment folder/filename when you rename the folder/filename where the corresponding md/canvas file be placed.' + }, + extensionOverride: { + name: 'Extension override', + desc: 'Using the extension override if you want to autorename the attachment with a specific extension (e.g. pdf or zip).', + addButton: 'Add extension overrides', + extension: { + name: 'Extension', + desc: 'Extension to override', + placeholder: 'pdf|docx?' + }, + tooltips: { + remove: 'Remove extension override', + edit: 'Edit extension override', + save: 'Save extension override' + }, + saved: 'Saved extension override' + }, + excludeExtension: { + name: 'Exclude extension pattern', + desc: 'Regex pattern to exclude certain extensions from being handled.', + placeholder: 'pdf|docx?|xlsx?|pptx?|zip|rar' + }, + excludedPaths: { + name: 'Excluded paths', + desc: 'Provide the full path of the folder names (case sensitive and without leading slash \'/\') divided by semicolon (;) to be excluded from renaming.' + }, + excludeSubpaths: { + name: 'Exclude subpaths', + desc: 'Turn on this option if you want to also exclude all subfolders of the folder paths provided above.' + } + }, + + // 覆盖设置模态框 + override: { + title: 'Overriding Settings', + menuTitle: 'Overriding attachment setting', + addExtensionOverrides: 'Add extension overrides', + extension: { + name: 'Extension', + desc: 'Extension to override', + placeholder: 'pdf' + }, + buttons: { + reset: 'Reset', + submit: 'Submit' + }, + notifications: { + reset: 'Reset attachment setting of {path}', + overridden: 'Overridden attachment setting of {path}' + } + }, + + // 扩展覆盖模态框 + extensionOverride: { + title: 'Extension Override Settings', + extension: { + name: 'Extension', + desc: 'Extension pattern to override (e.g., pdf, docx, jpg)', + placeholder: 'pdf|docx?' + }, + rootPath: { + name: 'Root path to save attachment', + desc: 'Select root path of attachment for this extension' + }, + rootFolder: { + name: 'Root folder', + desc: 'Root folder for this extension' + }, + attachmentPath: { + name: 'Attachment path', + desc: 'Path of attachment in root folder for this extension' + }, + attachmentFormat: { + name: 'Attachment format', + desc: 'Define how to name the attachment file for this extension' + }, + buttons: { + save: 'Save' + }, + notice: { + extensionEmpty: 'Extension cannot be empty', + extensionExists: 'Extension already exists', + saved: 'Extension override saved successfully' + } + }, + + // 确认对话框 + confirm: { + title: 'Tips', + message: 'This operation is irreversible and experimental. Please backup your vault first!', + continue: 'Continue', + deleteOverride: 'Are you sure you want to delete this override setting?', + deleteExtensionOverride: 'Are you sure you want to delete this extension override?' + }, + + // 通知消息 + notices: { + settingsSaved: 'Settings saved successfully', + overrideSaved: 'Override setting saved successfully', + overrideDeleted: 'Override setting deleted successfully', + extensionOverrideSaved: 'Extension override saved successfully', + extensionOverrideDeleted: 'Extension override deleted successfully', + attachmentRenamed: 'Attachment renamed successfully', + attachmentMoved: 'Attachment moved successfully', + arrangeCompleted: 'Arrange completed', + fileExcluded: '{path} was excluded', + resetAttachmentSetting: 'Reset attachment setting of {path}', + error: { + invalidPath: 'Invalid path specified', + fileNotFound: 'File not found', + permissionDenied: 'Permission denied', + unknownError: 'An unknown error occurred' + } + }, + + // 命令 + commands: { + rearrangeActiveFile: 'Rearrange attachments for active file', + rearrangeAllFiles: 'Rearrange attachments for all files', + openSettings: 'Open Attachment Management settings', + overrideAttachmentSetting: 'Override attachment setting', + rearrangeAllLinks: 'Rearrange all linked attachments', + rearrangeActiveLinks: 'Rearrange linked attachments', + resetOverrideSetting: 'Reset override setting', + clearUnusedStorage: 'Clear unused original name storage' + }, + + + + // 错误消息 + errors: { + canvasNotSupported: 'Canvas is not supported as an extension override.', + markdownNotSupported: 'Markdown is not supported as an extension override.', + extensionEmpty: 'Extension override cannot be empty.', + duplicateExtension: 'Duplicate extension override.', + excludedExtension: 'Extension override cannot be an excluded extension.' + } +}; \ No newline at end of file diff --git a/src/i18n/locales/zh-cn.ts b/src/i18n/locales/zh-cn.ts new file mode 100644 index 0000000..2d1d10c --- /dev/null +++ b/src/i18n/locales/zh-cn.ts @@ -0,0 +1,196 @@ +import { TranslationMap } from '../index'; + +export const zhCn: TranslationMap = { + // 通用 + common: { + save: '保存', + cancel: '取消', + delete: '删除', + edit: '编辑', + add: '添加', + remove: '移除', + confirm: '确认', + close: '关闭' + }, + + // 设置页面 + settings: { + title: '附件管理设置', + language: { + name: '语言', + desc: '选择界面语言' + }, + rootPath: { + name: '附件保存根路径', + desc: '选择附件的根路径', + options: { + obsidian: '复制 Obsidian 设置', + inFolder: '在下方指定的文件夹中', + nextToNote: '在笔记旁边的指定文件夹中' + } + }, + rootFolder: { + name: '根文件夹', + desc: '新附件的根文件夹' + }, + attachmentPath: { + name: '附件路径', + desc: '附件在根文件夹中的路径,可用变量 {{notepath}}、{{notename}}、{{parent}}' + }, + attachmentFormat: { + name: '附件格式', + desc: '定义如何命名附件文件,可用变量 {{dates}}、{{notename}}、{{md5}} 和 {{originalname}}。' + }, + dateFormat: { + name: '日期格式', + desc: '使用的 Moment 日期格式', + linkText: 'Moment 格式选项' + }, + autoRename: { + name: '自动重命名附件', + desc: '当您重命名对应 md/canvas 文件所在的文件夹/文件名时,自动重命名附件文件夹/文件名。' + }, + extensionOverride: { + name: '扩展名覆盖', + desc: '如果您想要对特定扩展名的附件进行自动重命名(例如 pdf 或 zip),请使用扩展名覆盖。', + addButton: '添加扩展名覆盖', + extension: { + name: '扩展名', + desc: '要覆盖的扩展名', + placeholder: 'pdf|docx?' + }, + tooltips: { + remove: '移除扩展名覆盖', + edit: '编辑扩展名覆盖', + save: '保存扩展名覆盖' + }, + saved: '已保存扩展名覆盖' + }, + excludeExtension: { + name: '排除扩展名模式', + desc: '用于排除某些扩展名不被处理的正则表达式模式。', + placeholder: 'pdf|docx?|xlsx?|pptx?|zip|rar' + }, + excludedPaths: { + name: '排除路径', + desc: '提供要从重命名中排除的文件夹名称的完整路径(区分大小写且不带前导斜杠 "/"),用分号(;)分隔。' + }, + excludeSubpaths: { + name: '排除子路径', + desc: '如果您还想排除上面提供的文件夹路径的所有子文件夹,请打开此选项。' + } + }, + + // 覆盖设置模态框 + override: { + title: '覆盖设置', + menuTitle: '覆盖附件设置', + addExtensionOverrides: '添加扩展名覆盖', + extension: { + name: '扩展名', + desc: '要覆盖的扩展名', + placeholder: 'pdf' + }, + buttons: { + reset: '重置', + submit: '提交' + }, + notifications: { + reset: '已重置 {path} 的附件设置', + overridden: '已覆盖 {path} 的附件设置' + } + }, + + // 扩展覆盖模态框 + extensionOverride: { + title: '扩展名覆盖设置', + extension: { + name: '扩展名', + desc: '要覆盖的扩展名模式(例如:pdf、docx、jpg)', + placeholder: 'pdf|docx?' + }, + rootPath: { + name: '附件保存根路径', + desc: '选择此扩展名的附件根路径' + }, + rootFolder: { + name: '根文件夹', + desc: '此扩展名的根文件夹' + }, + attachmentPath: { + name: '附件路径', + desc: '此扩展名在根文件夹中的附件路径' + }, + attachmentFormat: { + name: '附件格式', + desc: '定义此扩展名的附件文件命名方式' + }, + buttons: { + save: '保存' + }, + notice: { + extensionEmpty: '扩展名不能为空', + extensionExists: '扩展名已存在', + saved: '扩展名覆盖保存成功' + } + }, + + // 确认对话框 + confirm: { + title: '提示', + message: '此操作不可逆且为实验性功能,请先备份您的库!', + continue: '继续', + deleteOverride: '您确定要删除此覆盖设置吗?', + deleteExtensionOverride: '您确定要删除此扩展名覆盖吗?' + }, + + // 通知消息 + notices: { + settingsSaved: '设置保存成功', + overrideSaved: '覆盖设置保存成功', + overrideDeleted: '覆盖设置删除成功', + extensionOverrideSaved: '扩展名覆盖保存成功', + extensionOverrideDeleted: '扩展名覆盖删除成功', + attachmentRenamed: '附件重命名成功', + attachmentMoved: '附件移动成功', + error: { + invalidPath: '指定的路径无效', + fileNotFound: '文件未找到', + permissionDenied: '权限被拒绝', + unknownError: '发生未知错误' + } + }, + + // 命令 + commands: { + rearrangeActiveFile: '重新整理当前文件的附件', + rearrangeAllFiles: '重新整理所有文件的附件', + openSettings: '打开附件管理设置', + overrideAttachmentSetting: '覆盖附件设置', + rearrangeAllLinks: '重新整理所有链接的附件', + rearrangeActiveLinks: '重新整理链接的附件', + resetOverrideSetting: '重置覆盖设置', + clearUnusedStorage: '清理未使用的原始名称存储' + }, + + // 通知消息 + notifications: { + success: '操作成功完成', + error: '发生错误', + warning: '警告', + arrangeCompleted: '整理完成', + fileExcluded: '{path} 已被排除', + resetAttachmentSetting: '已重置 {path} 的附件设置' + }, + + + + // 错误消息 + errors: { + canvasNotSupported: '不支持将 Canvas 作为扩展覆盖。', + markdownNotSupported: '不支持将 Markdown 作为扩展覆盖。', + extensionEmpty: '扩展覆盖不能为空。', + duplicateExtension: '重复的扩展覆盖。', + excludedExtension: '扩展覆盖不能是被排除的扩展。' + } +}; \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 46d792d..d6e14db 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,10 +5,12 @@ import { DEFAULT_SETTINGS, OriginalNameStorage, SETTINGS_TYPES, - SettingTab, + AttachmentManagementSettingTab, } from "./settings/settings"; import { debugLog } from "./lib/log"; import { OverrideModal } from "./model/override"; +import { initI18n, setLanguage, detectLanguage, t, SupportedLanguage } from "./i18n/index"; +import { loadAllTranslations } from "./i18n/loader"; import { ConfirmModal } from "./model/confirm"; import { checkEmptyFolder, getActiveFile } from "./commons"; import { deleteOverrideSetting, getOverrideSetting, getRenameOverrideSetting, updateOverrideSetting } from "./override"; @@ -26,6 +28,12 @@ export default class AttachmentManagementPlugin extends Plugin { async onload() { await this.loadSettings(); + // 初始化国际化系统 + loadAllTranslations(); + const savedLanguage = this.settings.language as SupportedLanguage || detectLanguage(); + setLanguage(savedLanguage); + initI18n(); + console.log(`Plugin loading: ${this.manifest.name} v.${this.manifest.version}`); this.app.workspace.onLayoutReady(() => { @@ -38,7 +46,7 @@ export default class AttachmentManagementPlugin extends Plugin { } menu.addItem((item) => { item - .setTitle("Overriding attachment setting") + .setTitle(t('override.menuTitle')) .setIcon("image-plus") .onClick(async () => { const { setting } = getOverrideSetting(this.settings, file); @@ -134,7 +142,7 @@ export default class AttachmentManagementPlugin extends Plugin { if (file instanceof TFile) { if (file.parent && isExcluded(file.parent.path, this.settings)) { debugLog("rename - exclude path:", file.parent.path); - new Notice(`${file.path} was excluded`); + new Notice(t('notifications.fileExcluded', { path: file.path })); return; } @@ -201,7 +209,7 @@ export default class AttachmentManagementPlugin extends Plugin { ); // This adds a settings tab so the user can configure various aspects of the plugin - this.addSettingTab(new SettingTab(this.app, this)); + this.addSettingTab(new AttachmentManagementSettingTab(this.app, this)); }); } @@ -223,7 +231,7 @@ export default class AttachmentManagementPlugin extends Plugin { initCommands() { this.addCommand({ id: "attachment-management-rearrange-all-links", - name: "Rearrange all linked attachments", + name: t('commands.rearrangeAllLinks'), callback: async () => { new ConfirmModal(this).open(); }, @@ -231,10 +239,10 @@ export default class AttachmentManagementPlugin extends Plugin { this.addCommand({ id: "attachment-management-rearrange-active-links", - name: "Rearrange linked attachments", + name: t('commands.rearrangeActiveLinks'), callback: async () => { new ArrangeHandler(this.settings, this.app, this).rearrangeAttachment(RearrangeType.ACTIVE).finally(() => { - new Notice("Arrange completed"); + new Notice(t('notifications.arrangeCompleted')); }); }, }); @@ -267,7 +275,7 @@ export default class AttachmentManagementPlugin extends Plugin { this.addCommand({ id: "attachment-management-reset-override-setting", - name: "Reset override setting", + name: t('commands.resetOverrideSetting'), checkCallback: (checking: boolean) => { const file = getActiveFile(this.app); if (file) { @@ -282,7 +290,7 @@ export default class AttachmentManagementPlugin extends Plugin { } delete this.settings.overridePath[file.path]; this.saveSettings().finally(() => { - new Notice(`Reset attachment setting of ${file.path}`); + new Notice(t('notifications.resetAttachmentSetting', { path: file.path })); }); } return true; @@ -293,7 +301,7 @@ export default class AttachmentManagementPlugin extends Plugin { this.addCommand({ id: "attachment-management-clear-unused-originalname-storage", - name: "Clear unused original name storage", + name: t('commands.clearUnusedStorage'), callback: async () => { const attachments = await new ArrangeHandler(this.settings, this.app, this).getAttachmentsInVault( this.settings, diff --git a/src/model/confirm.ts b/src/model/confirm.ts index 81c06f6..e198f46 100644 --- a/src/model/confirm.ts +++ b/src/model/confirm.ts @@ -1,6 +1,7 @@ import { Modal, Notice, Setting } from "obsidian"; import AttachmentManagementPlugin from "../main"; import { ArrangeHandler, RearrangeType } from "src/arrange"; +import { t } from "../i18n/index"; export class ConfirmModal extends Modal { plugin: AttachmentManagementPlugin; @@ -15,27 +16,27 @@ export class ConfirmModal extends Modal { contentEl.empty(); contentEl.createEl("h3", { - text: "Tips", + text: t('confirm.title'), }); contentEl.createSpan("", (el) => { - el.innerText = "This operation is irreversible and experimental. Please backup your vault first!"; + el.innerText = t('confirm.message'); }); new Setting(contentEl) .addButton((btn) => { btn - .setButtonText("Cancel") + .setButtonText(t('common.cancel')) .setCta() .onClick(() => { this.close(); }); }) .addButton((btn) => - btn.setButtonText("Continue").onClick(async () => { + btn.setButtonText(t('confirm.continue')).onClick(async () => { new ArrangeHandler(this.plugin.settings, this.plugin.app, this.plugin) .rearrangeAttachment(RearrangeType.LINKS) .finally(() => { - new Notice("Arrange completed"); + new Notice(t('notifications.arrangeCompleted')); this.close(); }); }) diff --git a/src/model/extensionOverride.ts b/src/model/extensionOverride.ts index f7548a5..553c174 100644 --- a/src/model/extensionOverride.ts +++ b/src/model/extensionOverride.ts @@ -15,6 +15,7 @@ import AttachmentManagementPlugin from "../main"; import { AttachmentPathSettings, DEFAULT_SETTINGS, ExtensionOverrideSettings } from "../settings/settings"; import { matchExtension } from "src/utils"; import { debugLog } from "src/lib/log"; +import { t } from "../i18n/index"; /** * Retrieves the override setting for a specific extension. @@ -78,17 +79,17 @@ export class OverrideExtensionModal extends Modal { contentEl.empty(); contentEl.createEl("h3", { - text: `Extension settings for ${this.settings.extension}`, + text: t('extensionOverride.title'), }); new Setting(contentEl) - .setName("Root path to save attachment") - .setDesc("Select root path of attachment") + .setName(t('extensionOverride.rootPath.name')) + .setDesc(t('extensionOverride.rootPath.desc')) .addDropdown((text) => text - .addOption(`${SETTINGS_ROOT_OBSFOLDER}`, "Copy Obsidian settings") - .addOption(`${SETTINGS_ROOT_INFOLDER}`, "In the folder specified below") - .addOption(`${SETTINGS_ROOT_NEXTTONOTE}`, "Next to note in folder specified below") + .addOption(`${SETTINGS_ROOT_OBSFOLDER}`, t('settings.rootPath.options.obsidian')) + .addOption(`${SETTINGS_ROOT_INFOLDER}`, t('settings.rootPath.options.inFolder')) + .addOption(`${SETTINGS_ROOT_NEXTTONOTE}`, t('settings.rootPath.options.nextToNote')) .setValue(this.settings.saveAttE) .onChange(async (value) => { this.settings.saveAttE = value; @@ -98,7 +99,7 @@ export class OverrideExtensionModal extends Modal { ); if (this.settings.saveAttE !== "obsFolder") { new Setting(contentEl) - .setName("Root folder") + .setName(t('extensionOverride.rootFolder.name')) .setClass("override_root_folder_set") .addText((text) => text @@ -110,10 +111,8 @@ export class OverrideExtensionModal extends Modal { ); } new Setting(contentEl) - .setName("Attachment path") - .setDesc( - `Path of attachment in root folder, available variables ${SETTINGS_VARIABLES_NOTEPATH}, ${SETTINGS_VARIABLES_NOTENAME} and ${SETTINGS_VARIABLES_NOTEPARENT}` - ) + .setName(t('extensionOverride.attachmentPath.name')) + .setDesc(t('extensionOverride.attachmentPath.desc')) .addText((text) => text .setPlaceholder(DEFAULT_SETTINGS.attachPath.attachmentPath) @@ -124,10 +123,8 @@ export class OverrideExtensionModal extends Modal { ); new Setting(contentEl) - .setName("Attachment format") - .setDesc( - `Define how to name the attachment file, available variables ${SETTINGS_VARIABLES_DATES}, ${SETTINGS_VARIABLES_NOTENAME}, ${SETTINGS_VARIABLES_MD5} and ${SETTINGS_VARIABLES_ORIGINALNAME}.` - ) + .setName(t('extensionOverride.attachmentFormat.name')) + .setDesc(t('extensionOverride.attachmentFormat.desc')) .addText((text) => text .setPlaceholder(DEFAULT_SETTINGS.attachPath.attachFormat) @@ -138,7 +135,7 @@ export class OverrideExtensionModal extends Modal { ); new Setting(contentEl).addButton((button) => - button.setButtonText("Save").onClick(async () => { + button.setButtonText(t('extensionOverride.buttons.save')).onClick(async () => { this.onSubmit(this.settings); this.close(); }) diff --git a/src/model/override.ts b/src/model/override.ts index 12bf438..ad5f57e 100644 --- a/src/model/override.ts +++ b/src/model/override.ts @@ -14,6 +14,7 @@ import { import AttachmentManagementPlugin from "../main"; import { OverrideExtensionModal } from "./extensionOverride"; import { debugLog } from "src/lib/log"; +import { t } from "../i18n/index"; export class OverrideModal extends Modal { plugin: AttachmentManagementPlugin; @@ -44,17 +45,17 @@ export class OverrideModal extends Modal { contentEl.empty(); contentEl.createEl("h3", { - text: "Overriding Settings", + text: t('override.title'), }); new Setting(contentEl) - .setName("Root path to save attachment") - .setDesc("Select root path of attachment") + .setName(t('settings.rootPath.name')) + .setDesc(t('settings.rootPath.desc')) .addDropdown((text) => text - .addOption(`${SETTINGS_ROOT_OBSFOLDER}`, "Copy Obsidian settings") - .addOption(`${SETTINGS_ROOT_INFOLDER}`, "In the folder specified below") - .addOption(`${SETTINGS_ROOT_NEXTTONOTE}`, "Next to note in folder specified below") + .addOption(`${SETTINGS_ROOT_OBSFOLDER}`, t('settings.rootPath.options.obsidian')) + .addOption(`${SETTINGS_ROOT_INFOLDER}`, t('settings.rootPath.options.inFolder')) + .addOption(`${SETTINGS_ROOT_NEXTTONOTE}`, t('settings.rootPath.options.nextToNote')) .setValue(this.setting.saveAttE) .onChange(async (value) => { this.setting.saveAttE = value; @@ -63,7 +64,7 @@ export class OverrideModal extends Modal { ); new Setting(contentEl) - .setName("Root folder") + .setName(t('settings.rootFolder.name')) .setClass("override_root_folder_set") .addText((text) => text @@ -76,10 +77,8 @@ export class OverrideModal extends Modal { ); new Setting(contentEl) - .setName("Attachment path") - .setDesc( - `Path of attachment in root folder, available variables ${SETTINGS_VARIABLES_NOTEPATH}, ${SETTINGS_VARIABLES_NOTENAME} and ${SETTINGS_VARIABLES_NOTEPARENT}` - ) + .setName(t('settings.attachmentPath.name')) + .setDesc(t('settings.attachmentPath.desc')) .addText((text) => text .setPlaceholder(DEFAULT_SETTINGS.attachPath.attachmentPath) @@ -91,10 +90,8 @@ export class OverrideModal extends Modal { ); new Setting(contentEl) - .setName("Attachment format") - .setDesc( - `Define how to name the attachment file, available variables ${SETTINGS_VARIABLES_DATES}, ${SETTINGS_VARIABLES_NOTENAME}, ${SETTINGS_VARIABLES_MD5} and ${SETTINGS_VARIABLES_ORIGINALNAME}.` - ) + .setName(t('settings.attachmentFormat.name')) + .setDesc(t('settings.attachmentFormat.desc')) .addText((text) => text .setPlaceholder(DEFAULT_SETTINGS.attachPath.attachFormat) @@ -106,7 +103,7 @@ export class OverrideModal extends Modal { ); new Setting(contentEl).addButton((btn) => { - btn.setButtonText("Add extension overrides").onClick(async () => { + btn.setButtonText(t('override.addExtensionOverrides')).onClick(async () => { if (this.setting.extensionOverride === undefined) { this.setting.extensionOverride = []; } @@ -124,12 +121,12 @@ export class OverrideModal extends Modal { if (this.setting.extensionOverride !== undefined) { this.setting.extensionOverride.forEach((ext) => { new Setting(contentEl) - .setName("Extension") - .setDesc("Extension to override") + .setName(t('override.extension.name')) + .setDesc(t('override.extension.desc')) .setClass("override_extension_set") .addText((text) => text - .setPlaceholder("pdf") + .setPlaceholder(t('override.extension.placeholder')) .setValue(ext.extension) .onChange(async (value) => { ext.extension = value; @@ -156,18 +153,18 @@ export class OverrideModal extends Modal { new Setting(contentEl) .addButton((btn) => { - btn.setButtonText("Reset").onClick(async () => { + btn.setButtonText(t('override.buttons.reset')).onClick(async () => { this.setting = this.plugin.settings.attachPath; delete this.plugin.settings.overridePath[this.file.path]; await this.plugin.saveSettings(); await this.plugin.loadSettings(); - new Notice(`Reset attachment setting of ${this.file.path}`); + new Notice(t('override.notifications.reset', { path: this.file.path })); this.close(); }); }) .addButton((btn) => btn - .setButtonText("Submit") + .setButtonText(t('override.buttons.submit')) .setCta() .onClick(async () => { if (this.file instanceof TFile) { @@ -178,7 +175,7 @@ export class OverrideModal extends Modal { this.plugin.settings.overridePath[this.file.path] = this.setting; await this.plugin.saveSettings(); debugLog("override - overriding settings:", this.file.path, this.setting); - new Notice(`Overridden attachment setting of ${this.file.path}`); + new Notice(t('override.notifications.overridden', { path: this.file.path })); this.close(); }) ); diff --git a/src/settings/settings.ts b/src/settings/settings.ts index ffcf96d..4980b4c 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -14,6 +14,8 @@ import { import { OverrideExtensionModal } from "src/model/extensionOverride"; import { validateExtensionEntry, generateErrorExtensionMessage } from "src/utils"; import { debugLog } from "src/lib/log"; +import { t, setLanguage, getCurrentLanguage } from "../i18n/index"; +import { getSupportedLanguages } from "../i18n/loader"; export enum SETTINGS_TYPES { GLOBAL = "GLOBAL", @@ -57,6 +59,8 @@ export interface ExtensionOverrideSettings { } export interface AttachmentManagementPluginSettings { + // Language setting + language: string; // Disable notification disableNotification: boolean; // Path @@ -80,6 +84,7 @@ export interface AttachmentManagementPluginSettings { } export const DEFAULT_SETTINGS: AttachmentManagementPluginSettings = { + language: "en", attachPath: { attachmentRoot: "", saveAttE: `${SETTINGS_ROOT_OBSFOLDER}`, @@ -98,7 +103,7 @@ export const DEFAULT_SETTINGS: AttachmentManagementPluginSettings = { disableNotification: false, }; -export class SettingTab extends PluginSettingTab { +export class AttachmentManagementSettingTab extends PluginSettingTab { plugin: AttachmentManagementPlugin; constructor(app: App, plugin: AttachmentManagementPlugin) { @@ -132,6 +137,27 @@ export class SettingTab extends PluginSettingTab { containerEl.empty(); + containerEl.createEl("h2", { text: t('settings.title') }); + + // 语言设置 + new Setting(containerEl) + .setName(t('settings.language.name')) + .setDesc(t('settings.language.desc')) + .addDropdown(dropdown => { + const languages = getSupportedLanguages(); + languages.forEach(lang => { + dropdown.addOption(lang.code, lang.nativeName); + }); + dropdown.setValue(getCurrentLanguage()); + dropdown.onChange(async (value) => { + setLanguage(value as any); + this.plugin.settings.language = value as any; + await this.plugin.saveSettings(); + // 重新显示设置页面以应用新语言 + this.display(); + }); + }); + // new Setting(containerEl).setName("Disable notification").addToggle((toggle) => { // toggle.setValue(this.plugin.settings.disableNotification).onChange(async (value) => { // this.plugin.settings.disableNotification = value; @@ -140,13 +166,13 @@ export class SettingTab extends PluginSettingTab { // }); new Setting(containerEl) - .setName("Root path to save attachment") - .setDesc("Select root path of attachment") + .setName(t('settings.rootPath.name')) + .setDesc(t('settings.rootPath.desc')) .addDropdown((text) => text - .addOption(`${SETTINGS_ROOT_OBSFOLDER}`, "Copy Obsidian settings") - .addOption(`${SETTINGS_ROOT_INFOLDER}`, "In the folder specified below") - .addOption(`${SETTINGS_ROOT_NEXTTONOTE}`, "Next to note in folder specified below") + .addOption(`${SETTINGS_ROOT_OBSFOLDER}`, t('settings.rootPath.options.obsidian')) + .addOption(`${SETTINGS_ROOT_INFOLDER}`, t('settings.rootPath.options.inFolder')) + .addOption(`${SETTINGS_ROOT_NEXTTONOTE}`, t('settings.rootPath.options.nextToNote')) .setValue(this.plugin.settings.attachPath.saveAttE) .onChange(async (value) => { this.plugin.settings.attachPath.saveAttE = value; @@ -156,8 +182,8 @@ export class SettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName("Root folder") - .setDesc("Root folder of new attachment") + .setName(t('settings.rootFolder.name')) + .setDesc(t('settings.rootFolder.desc')) .setClass("root_folder_set") .addText((text) => text @@ -171,10 +197,8 @@ export class SettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName("Attachment path") - .setDesc( - `Path of attachment in root folder, available variables ${SETTINGS_VARIABLES_NOTEPATH}, ${SETTINGS_VARIABLES_NOTENAME}, ${SETTINGS_VARIABLES_NOTEPARENT}` - ) + .setName(t('settings.attachmentPath.name')) + .setDesc(t('settings.attachmentPath.desc')) .addText((text) => text .setPlaceholder(DEFAULT_SETTINGS.attachPath.attachmentPath) @@ -187,10 +211,8 @@ export class SettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName("Attachment format") - .setDesc( - `Define how to name the attachment file, available variables ${SETTINGS_VARIABLES_DATES}, ${SETTINGS_VARIABLES_NOTENAME}, ${SETTINGS_VARIABLES_MD5} and ${SETTINGS_VARIABLES_ORIGINALNAME}.` - ) + .setName(t('settings.attachmentFormat.name')) + .setDesc(t('settings.attachmentFormat.desc')) .addText((text) => text .setPlaceholder(DEFAULT_SETTINGS.attachPath.attachFormat) @@ -203,13 +225,13 @@ export class SettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName("Date format") + .setName(t('settings.dateFormat.name')) .setDesc( createFragment((frag) => { - frag.appendText("Moment date format to use "); + frag.appendText(t('settings.dateFormat.desc') + " "); frag.createEl("a", { href: "https://momentjscom.readthedocs.io/en/latest/moment/04-displaying/01-format", - text: "Moment format options", + text: t('settings.dateFormat.linkText'), }); }) ) @@ -225,10 +247,8 @@ export class SettingTab extends PluginSettingTab { }); new Setting(containerEl) - .setName("Automatically rename attachment") - .setDesc( - "Automatically rename the attachment folder/filename when you rename the folder/filename where the corresponding md/canvas file be placed." - ) + .setName(t('settings.autoRename.name')) + .setDesc(t('settings.autoRename.desc')) .addToggle((toggle) => toggle.setValue(this.plugin.settings.autoRenameAttachment).onChange(async (value) => { debugLog("setting - automatically rename attachment folder:" + value); @@ -238,10 +258,10 @@ export class SettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName("Extension override") - .setDesc("Using the extension override if you want to autorename the attachment with a specific extension (e.g. pdf or zip).") + .setName(t('settings.extensionOverride.name')) + .setDesc(t('settings.extensionOverride.desc')) .addButton((btn) => { - btn.setButtonText("Add extension overrides").onClick(async () => { + btn.setButtonText(t('settings.extensionOverride.addButton')).onClick(async () => { if (this.plugin.settings.attachPath.extensionOverride === undefined) { this.plugin.settings.attachPath.extensionOverride = []; } @@ -260,12 +280,12 @@ export class SettingTab extends PluginSettingTab { if (this.plugin.settings.attachPath.extensionOverride !== undefined) { this.plugin.settings.attachPath.extensionOverride.forEach((ext) => { new Setting(containerEl) - .setName("Extension") - .setDesc("Extension to override") + .setName(t('settings.extensionOverride.extension.name')) + .setDesc(t('settings.extensionOverride.extension.desc')) .setClass("override_extension_set") .addText((text) => text - .setPlaceholder("pdf|docx?") + .setPlaceholder(t('settings.extensionOverride.extension.placeholder')) .setValue(ext.extension) .onChange(async (value) => { ext.extension = value; @@ -274,7 +294,7 @@ export class SettingTab extends PluginSettingTab { .addButton((btn) => { btn .setIcon("trash") - .setTooltip("Remove extension override") + .setTooltip(t('settings.extensionOverride.tooltips.remove')) .onClick(async () => { //get index of extension const index = this.plugin.settings.attachPath.extensionOverride?.indexOf(ext) ?? -1; @@ -287,7 +307,7 @@ export class SettingTab extends PluginSettingTab { .addButton((btn) => { btn .setIcon("pencil") - .setTooltip("Edit extension override") + .setTooltip(t('settings.extensionOverride.tooltips.edit')) .onClick(async () => { new OverrideExtensionModal(this.plugin, ext, (result) => { ext = result; @@ -297,7 +317,7 @@ export class SettingTab extends PluginSettingTab { .addButton((btn) => { btn .setIcon("check") - .setTooltip("Save extension override") + .setTooltip(t('settings.extensionOverride.tooltips.save')) .onClick(async () => { const wrongIndex = validateExtensionEntry(this.plugin.settings.attachPath, this.plugin.settings); if (wrongIndex.length > 0) { @@ -311,18 +331,18 @@ export class SettingTab extends PluginSettingTab { } await this.plugin.saveSettings(); this.display(); - new Notice("Saved extension override"); + new Notice(t('settings.extensionOverride.saveNotice')); }); }); }); } new Setting(containerEl) - .setName("Exclude extension pattern") - .setDesc(`Regex pattern to exclude certain extensions from being handled.`) + .setName(t('settings.excludeExtension.name')) + .setDesc(t('settings.excludeExtension.desc')) .addText((text) => text - .setPlaceholder("pdf|docx?|xlsx?|pptx?|zip|rar") + .setPlaceholder(t('settings.excludeExtension.placeholder')) .setValue(this.plugin.settings.excludeExtensionPattern) .onChange(async (value) => { this.plugin.settings.excludeExtensionPattern = value; @@ -331,10 +351,8 @@ export class SettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName("Excluded paths") - .setDesc( - `Provide the full path of the folder names (case sensitive and without leading slash '/') divided by semicolon (;) to be excluded from renaming.` - ) + .setName(t('settings.excludedPaths.name')) + .setDesc(t('settings.excludedPaths.desc')) .addTextArea((component: TextAreaComponent) => { component.setValue(this.plugin.settings.excludedPaths).onChange(async (value) => { this.plugin.settings.excludedPaths = value; @@ -346,8 +364,8 @@ export class SettingTab extends PluginSettingTab { }); new Setting(containerEl) - .setName("Exclude subpaths") - .setDesc("Turn on this option if you want to also exclude all subfolders of the folder paths provided above.") + .setName(t('settings.excludeSubpaths.name')) + .setDesc(t('settings.excludeSubpaths.desc')) .addToggle((toggle) => toggle.setValue(this.plugin.settings.excludeSubpaths).onChange(async (value) => { debugLog("setting - excluded subpaths:" + value); diff --git a/src/utils.ts b/src/utils.ts index b8aeb97..ec19233 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,6 @@ import { DataAdapter, Notice, TAbstractFile, TFile } from "obsidian"; import { AttachmentManagementPluginSettings, AttachmentPathSettings } from "./settings/settings"; +import { t } from "./i18n/index"; import { Md5 } from "ts-md5"; @@ -182,14 +183,14 @@ export function validateExtensionEntry(setting: AttachmentPathSettings, plugin: export function generateErrorExtensionMessage(type: "md" | "canvas" | "empty" | "duplicate" | "excluded") { if (type === "canvas") { - new Notice("Canvas is not supported as an extension override."); + new Notice(t('errors.canvasNotSupported')); } else if (type === "md") { - new Notice("Markdown is not supported as an extension override."); + new Notice(t('errors.markdownNotSupported')); } else if (type === "empty") { - new Notice("Extension override cannot be empty."); + new Notice(t('errors.extensionEmpty')); } else if (type === "duplicate") { - new Notice("Duplicate extension override."); + new Notice(t('errors.duplicateExtension')); } else if (type === "excluded") { - new Notice("Extension override cannot be an excluded extension."); + new Notice(t('errors.excludedExtension')); } } diff --git a/tsconfig.json b/tsconfig.json index 99baff5..e706762 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,5 @@ { + "types": ["node"], "compilerOptions": { "baseUrl": ".", "inlineSourceMap": true, @@ -13,9 +14,9 @@ "strictNullChecks": true, "lib": [ "DOM", - "ES5", - "ES6", - "ES7" + "ES2018", + "ES2019", + "ES2020" ] }, "resolveJsonModule": true, From 476627368e2e4a2a9966eea821d66f3e464a2132 Mon Sep 17 00:00:00 2001 From: qq_27963509 Date: Sat, 16 Aug 2025 22:50:46 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=88=AA=E5=9B=BE?= =?UTF-8?q?=E7=B2=98=E8=B4=B4=E5=90=8E=E9=93=BE=E6=8E=A5=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E5=9C=B0=E5=9D=80=E6=9C=AA=E6=9B=B4=E6=96=B0=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复linkDetector中markdown图片链接检测的正则表达式 - 添加LinkUpdater类处理文件重命名后的链接更新 - 在create.ts中添加手动链接更新逻辑 - 支持URL编码路径的正确解码和比较 Fixes: 截图粘贴到笔记后图片引用地址没有更新的问题 --- src/arrange.ts | 18 +++- src/create.ts | 16 +++- src/lib/linkDetector.ts | 4 +- src/lib/linkUpdater.ts | 186 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 src/lib/linkUpdater.ts diff --git a/src/arrange.ts b/src/arrange.ts index d336ab9..173d603 100644 --- a/src/arrange.ts +++ b/src/arrange.ts @@ -11,6 +11,7 @@ import { getMetadata } from "./settings/metadata"; import { getActiveFile } from "./commons"; import { isExcluded } from "./exclude"; import { containOriginalNameVariable, loadOriginalName } from "./lib/originalStorage"; +import { LinkUpdater } from "./lib/linkUpdater"; const bannerRegex = /!\[\[(.*?)\]\]/i; @@ -24,11 +25,13 @@ export class ArrangeHandler { settings: AttachmentManagementPluginSettings; app: App; plugin: Plugin; + linkUpdater: LinkUpdater; constructor(settings: AttachmentManagementPluginSettings, app: App, plugin: Plugin) { this.settings = settings; this.app = app; this.plugin = plugin; + this.linkUpdater = new LinkUpdater(app); } /** @@ -125,7 +128,20 @@ export class ArrangeHandler { const { name } = await deduplicateNewName(attachName + "." + path.extname(link), attachPathFile); debugLog("rearrangeAttachment - deduplicated name:", name); - await this.app.fileManager.renameFile(linkFile, path.join(attachPath, name)); + const oldPath = linkFile.path; + const newPath = path.join(attachPath, name); + + // 重命名文件 + await this.app.fileManager.renameFile(linkFile, newPath); + + // 手动更新链接引用,因为fileManager.renameFile在某些情况下不会自动更新链接 + // 参考: https://github.com/trganda/obsidian-attachment-management/issues/46 + try { + await this.linkUpdater.updateLinksForRenamedFile(oldPath, newPath); + debugLog(`Successfully updated links for renamed file: ${oldPath} -> ${newPath}`); + } catch (error) { + console.error(`Failed to update links for renamed file: ${oldPath} -> ${newPath}`, error); + } } } } diff --git a/src/create.ts b/src/create.ts index 3645745..a5b2784 100644 --- a/src/create.ts +++ b/src/create.ts @@ -9,16 +9,19 @@ import { isExcluded } from "./exclude"; import { getExtensionOverrideSetting } from "./model/extensionOverride"; import { md5sum, isImage, isPastedImage } from "./utils"; import { saveOriginalName } from "./lib/originalStorage"; +import { LinkUpdater } from "./lib/linkUpdater"; export class CreateHandler { readonly plugin: Plugin; readonly app: App; readonly settings: AttachmentManagementPluginSettings; + readonly linkUpdater: LinkUpdater; constructor(plugin: Plugin, settings: AttachmentManagementPluginSettings) { this.plugin = plugin; - this.app = this.plugin.app; + this.app = plugin.app; this.settings = settings; + this.linkUpdater = new LinkUpdater(plugin.app); } /** @@ -85,13 +88,22 @@ export class CreateHandler { const original = attach.basename; const name = attach.name; + const oldPath = attach.path; // this api will not update the link in markdown file automatically on `create` event // forgive using to rename, refer: https://github.com/trganda/obsidian-attachment-management/issues/46 this.app.fileManager .renameFile(attach, dst) - .then(() => { + .then(async () => { new Notice(`Renamed ${name} to ${attachName}.`); + + // 手动更新链接引用,因为fileManager.renameFile在create事件中不会自动更新链接 + try { + await this.linkUpdater.updateLinksForRenamedFile(oldPath, dst); + debugLog(`Successfully updated links for created file: ${oldPath} -> ${dst}`); + } catch (error) { + console.error(`Failed to update links for created file: ${oldPath} -> ${dst}`, error); + } }) .finally(() => { // save origianl name in setting diff --git a/src/lib/linkDetector.ts b/src/lib/linkDetector.ts index 918ac00..e30e8ba 100644 --- a/src/lib/linkDetector.ts +++ b/src/lib/linkDetector.ts @@ -64,8 +64,8 @@ export const getAllLinkMatchesInFile = async (mdFile: TFile, app: App, fileText? } } - // --> Get All Markdown Links - const markdownRegex = /\[(^$|.*?)\]\((.*?)\)/g; + // --> Get All Markdown Links (including image links) + const markdownRegex = /!?\[(^$|.*?)\]\((.*?)\)/g; const markdownMatches = fileText.match(markdownRegex); if (markdownMatches) { const fileRegex = /(?<=\().*(?=\))/; diff --git a/src/lib/linkUpdater.ts b/src/lib/linkUpdater.ts new file mode 100644 index 0000000..04673bf --- /dev/null +++ b/src/lib/linkUpdater.ts @@ -0,0 +1,186 @@ +import { App, TFile, normalizePath } from "obsidian"; +import { getAllLinkMatchesInFile, LinkMatch } from "./linkDetector"; +import { debugLog } from "./log"; + +/** + * 手动更新MD文件中的链接引用 + * 当Obsidian的fileManager.renameFile无法自动更新链接时使用 + */ +export class LinkUpdater { + private app: App; + + constructor(app: App) { + this.app = app; + } + + /** + * 解码URL编码的路径 + * @param path 可能包含URL编码的路径 + * @returns 解码后的路径 + */ + private decodeUrlPath(path: string): string { + try { + return decodeURIComponent(path); + } catch (error) { + // 如果解码失败,返回原始路径 + return path; + } + } + + /** + * 更新所有引用了指定文件的MD文件中的链接 + * @param oldPath 旧文件路径 + * @param newPath 新文件路径 + */ + async updateLinksForRenamedFile(oldPath: string, newPath: string): Promise { + debugLog(`LinkUpdater: Updating links from ${oldPath} to ${newPath}`); + + // 获取所有MD文件 + const markdownFiles = this.app.vault.getMarkdownFiles(); + + for (const mdFile of markdownFiles) { + try { + await this.updateLinksInFile(mdFile, oldPath, newPath); + } catch (error) { + console.error(`Failed to update links in ${mdFile.path}:`, error); + } + } + } + + /** + * 更新单个MD文件中的链接 + * @param mdFile MD文件 + * @param oldPath 旧文件路径 + * @param newPath 新文件路径 + */ + private async updateLinksInFile(mdFile: TFile, oldPath: string, newPath: string): Promise { + const content = await this.app.vault.read(mdFile); + const linkMatches = await getAllLinkMatchesInFile(mdFile, this.app, content); + + // 检查是否有需要更新的链接 + const linksToUpdate = linkMatches.filter(link => { + const normalizedLinkPath = normalizePath(this.decodeUrlPath(link.linkText)); + const normalizedOldPath = normalizePath(this.decodeUrlPath(oldPath)); + return normalizedLinkPath === normalizedOldPath; + }); + + if (linksToUpdate.length === 0) { + return; // 没有需要更新的链接 + } + + debugLog(`LinkUpdater: Found ${linksToUpdate.length} links to update in ${mdFile.path}`); + + let updatedContent = content; + + // 更新每个匹配的链接 + for (const linkMatch of linksToUpdate) { + updatedContent = this.updateLinkInContent(updatedContent, linkMatch, newPath); + } + + // 如果内容有变化,保存文件 + if (updatedContent !== content) { + await this.app.vault.modify(mdFile, updatedContent); + debugLog(`LinkUpdater: Updated links in ${mdFile.path}`); + } + } + + /** + * 在文件内容中更新特定的链接 + * @param content 文件内容 + * @param linkMatch 要更新的链接匹配 + * @param newPath 新路径 + * @returns 更新后的内容 + */ + private updateLinkInContent(content: string, linkMatch: LinkMatch, newPath: string): string { + const { match, type } = linkMatch; + let newMatch: string; + + switch (type) { + case "wiki": + // [[oldPath]] -> [[newPath]] + // [[oldPath|alias]] -> [[newPath|alias]] + if (match.includes("|")) { + const alias = match.split("|")[1].replace("]]", ""); + newMatch = `[[${newPath}|${alias}]]`; + } else { + newMatch = `[[${newPath}]]`; + } + break; + + case "markdown": + // [text](oldPath) -> [text](newPath) + const textPart = match.match(/\[(.*?)\]/)?.[1] || ""; + newMatch = `[${textPart}](${newPath})`; + break; + + case "wikiTransclusion": + // ![[oldPath]] -> ![[newPath]] + newMatch = `![[${newPath}]]`; + break; + + case "mdTransclusion": + // ![text](oldPath) -> ![text](newPath) + const altText = match.match(/!\[(.*?)\]/)?.[1] || ""; + newMatch = `![${altText}](${newPath})`; + break; + + default: + return content; // 未知类型,不更新 + } + + // 替换原始匹配 + return content.replace(match, newMatch); + } + + /** + * 批量更新多个文件的链接引用 + * @param renamedFiles 重命名的文件映射 {oldPath: newPath} + */ + async updateLinksForMultipleFiles(renamedFiles: Record): Promise { + debugLog(`LinkUpdater: Updating links for ${Object.keys(renamedFiles).length} renamed files`); + + const markdownFiles = this.app.vault.getMarkdownFiles(); + + for (const mdFile of markdownFiles) { + try { + await this.updateLinksInFileForMultiple(mdFile, renamedFiles); + } catch (error) { + console.error(`Failed to update links in ${mdFile.path}:`, error); + } + } + } + + /** + * 在单个MD文件中更新多个文件的链接 + * @param mdFile MD文件 + * @param renamedFiles 重命名的文件映射 + */ + private async updateLinksInFileForMultiple(mdFile: TFile, renamedFiles: Record): Promise { + const content = await this.app.vault.read(mdFile); + const linkMatches = await getAllLinkMatchesInFile(mdFile, this.app, content); + + let updatedContent = content; + let hasChanges = false; + + // 检查每个链接是否需要更新 + for (const linkMatch of linkMatches) { + const normalizedLinkPath = normalizePath(this.decodeUrlPath(linkMatch.linkText)); + + for (const [oldPath, newPath] of Object.entries(renamedFiles)) { + const normalizedOldPath = normalizePath(this.decodeUrlPath(oldPath)); + + if (normalizedLinkPath === normalizedOldPath) { + updatedContent = this.updateLinkInContent(updatedContent, linkMatch, newPath); + hasChanges = true; + break; // 找到匹配后跳出内层循环 + } + } + } + + // 如果有变化,保存文件 + if (hasChanges) { + await this.app.vault.modify(mdFile, updatedContent); + debugLog(`LinkUpdater: Updated multiple links in ${mdFile.path}`); + } + } +} \ No newline at end of file