From 6698cd79c16851579cc0a9b185c16f3ae1c8bb4d Mon Sep 17 00:00:00 2001 From: Xheldon Date: Wed, 30 Jul 2025 16:18:01 +0800 Subject: [PATCH] [done]cos support --- i18n-simple.ts | 12 +- main.ts | 507 +++++++++++++++++++++++++++++-------------------- types.ts | 6 + 3 files changed, 316 insertions(+), 209 deletions(-) diff --git a/i18n-simple.ts b/i18n-simple.ts index 825af85..5862931 100644 --- a/i18n-simple.ts +++ b/i18n-simple.ts @@ -50,6 +50,8 @@ const translations = { // COS settings 'settings.image.section.title': '图片处理', + 'settings.image.enable.name': '启用图片处理功能', + 'settings.image.enable.desc': '开启后,粘贴图片时将自动上传到云存储并替换为链接', 'settings.cos.provider.section.title': '云服务商设置', 'settings.image.upload.section.title': '图片上传设置', 'settings.cos.provider.name': '云存储服务商', @@ -191,8 +193,9 @@ const translations = { // COS upload messages 'cos.upload.success': '图片上传成功', - 'cos.upload.failed': '图片上传失败:{error}', + 'cos.upload.failed': '图片上传失败: {error}', 'cos.upload.uploading': '正在上传图片...', + 'cos.upload.no.config': '图片处理功能已启用,但需要配置云存储或本地存储路径', 'cos.test.success': 'COS配置测试成功', 'cos.test.failed': 'COS配置测试失败:{error}', 'cos.error.unsupported.provider': '不支持的云存储服务商:{provider}', @@ -202,7 +205,7 @@ const translations = { 'cos.error.aws.upload.failed': 'AWS S3上传失败({status}):{error}', 'cos.error.cloudflare.upload.failed': 'Cloudflare R2上传失败({status}):{error}', 'cos.error.config.invalid': 'COS配置不完整,请检查配置项', - 'cos.paste.placeholder': '![上传中...]({filename})', + 'cos.paste.placeholder': '正在上传 {filename}...', }, en: { // Settings interface @@ -250,6 +253,8 @@ const translations = { // COS settings 'settings.image.section.title': 'Image Processing', + 'settings.image.enable.name': 'Enable Image Processing', + 'settings.image.enable.desc': 'When enabled, images pasted will be automatically uploaded to cloud storage and replaced with links', 'settings.cos.provider.section.title': 'Cloud Provider Settings', 'settings.image.upload.section.title': 'Image Upload Settings', 'settings.cos.provider.name': 'Cloud Storage Provider', @@ -393,6 +398,7 @@ const translations = { 'cos.upload.success': 'Image uploaded successfully', 'cos.upload.failed': 'Image upload failed: {error}', 'cos.upload.uploading': 'Uploading image...', + 'cos.upload.no.config': 'Image processing is enabled, but cloud storage or local storage path needs to be configured', 'cos.test.success': 'COS configuration test successful', 'cos.test.failed': 'COS configuration test failed: {error}', 'cos.error.unsupported.provider': 'Unsupported cloud storage provider: {provider}', @@ -402,7 +408,7 @@ const translations = { 'cos.error.aws.upload.failed': 'AWS S3 upload failed ({status}): {error}', 'cos.error.cloudflare.upload.failed': 'Cloudflare R2 upload failed ({status}): {error}', 'cos.error.config.invalid': 'COS configuration is incomplete, please check settings', - 'cos.paste.placeholder': '![Uploading...]({filename})', + 'cos.paste.placeholder': 'Uploading {filename}...', } }; diff --git a/main.ts b/main.ts index 22fe091..e95d506 100644 --- a/main.ts +++ b/main.ts @@ -13,6 +13,7 @@ export default class GitSyncPlugin extends Plugin { statusBarEl: HTMLElement | null = null; currentFile: TFile | null = null; private fileModifyTimeout: NodeJS.Timeout | null = null; + private isProcessingImagePaste: boolean = false; async onload() { await this.loadSettings(); @@ -60,7 +61,11 @@ export default class GitSyncPlugin extends Plugin { ); // Listen for paste events to handle image uploads - this.registerDomEvent(document, 'paste', this.handlePasteEvent.bind(this)); + // Use capture phase to intercept events before Obsidian's handlers + this.registerDomEvent(document, 'paste', this.handlePasteEvent.bind(this), { capture: true }); + + // Also listen on the workspace container to catch events that might bypass document listener + this.registerDomEvent(document.body, 'paste', this.handlePasteEvent.bind(this), { capture: true }); // Add sync button to note editing interface this.addCommand({ @@ -671,30 +676,58 @@ export default class GitSyncPlugin extends Plugin { return; } - // Check if COS is configured - if (!this.isCosConfigured()) { + // Check if there are any image files in clipboard + const imageFiles: File[] = []; + for (let i = 0; i < clipboardData.files.length; i++) { + const file = clipboardData.files[i]; + if (file.type.startsWith('image/')) { + imageFiles.push(file); + } + } + + // If no image files, let Obsidian handle it normally + if (imageFiles.length === 0) { return; } - // Process each file in clipboard - for (let i = 0; i < clipboardData.files.length; i++) { - const file = clipboardData.files[i]; - - // Only handle image files - if (!file.type.startsWith('image/')) { - continue; - } + // If image processing is disabled, let Obsidian handle it normally + if (!this.settings.enableImageProcessing) { + return; + } - // Prevent default paste behavior for images - event.preventDefault(); + // If image processing is enabled, we take control immediately + // Prevent default paste behavior as early as possible + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + // Set flag to indicate image paste is being processed + this.isProcessingImagePaste = true; + + // Check if COS is configured for upload + const cosConfigured = this.isCosConfigured(); + + // Process each image file + for (const file of imageFiles) { try { - await this.handleImageUpload(file, activeView.editor); + if (cosConfigured) { + // Upload to COS and insert CDN link + await this.handleImageUpload(file, activeView.editor); + } else if (this.settings.keepLocalImages && this.settings.localImagePath) { + // Only save locally and insert local link + await this.handleLocalImageOnly(file, activeView.editor); + } else { + // Show notice that configuration is needed + new Notice(t('cos.upload.no.config')); + } } catch (error) { - console.error('Error handling image upload:', error); + console.error('Error handling image:', error); new Notice(t('cos.upload.failed', { error: error.message })); } } + + // Reset flag after processing all images + this.isProcessingImagePaste = false; } /** @@ -820,6 +853,52 @@ export default class GitSyncPlugin extends Plugin { new Notice(t('cos.upload.failed', { error: error.message })); console.error('Image upload failed:', error); + throw error; // Re-throw to be handled by the caller + } + } + + /** + * Handle local image storage only (when COS is not configured) + */ + private async handleLocalImageOnly(file: File, editor: Editor): Promise { + // Generate unique filename + const timestamp = Date.now(); + const extension = file.name.split('.').pop() || 'png'; + const fileName = `image-${timestamp}.${extension}`; + + try { + // Save image locally using the configured local path + const localPath = parseImagePath(this.settings.localImagePath, { + currentFilePath: this.currentFile!.path, + fileName: fileName + }); + + // Create directory if it doesn't exist + const dirPath = localPath.substring(0, localPath.lastIndexOf('/')); + if (dirPath) { + try { + await this.app.vault.createFolder(dirPath); + } catch (error) { + // Folder might already exist, ignore error + } + } + + // Convert File to ArrayBuffer + const arrayBuffer = await file.arrayBuffer(); + + // Save file to vault + await this.app.vault.createBinary(localPath, arrayBuffer); + + // Insert image link at cursor position + const imageMarkdown = `![${fileName}](${localPath})`; + const cursorPos = editor.getCursor(); + editor.replaceRange(imageMarkdown, cursorPos); + + new Notice(t('cos.upload.success')); + console.log('Image saved locally:', localPath); + } catch (error) { + new Notice(t('cos.upload.failed', { error: error.message })); + console.error('Local image save failed:', error); } } @@ -959,225 +1038,241 @@ class GitSyncSettingTab extends PluginSettingTab { cls: 'git-sync-section-title' }); - // Cloud provider settings subsection - containerEl.createEl('h3', { - text: t('settings.cos.provider.section.title'), - cls: 'git-sync-subsection-title' - }); - - // Cloud storage provider + // Enable image processing toggle new Setting(containerEl) - .setName(t('settings.cos.provider.name')) - .setDesc(t('settings.cos.provider.desc')) - .addDropdown(dropdown => { - dropdown.addOption('aliyun', t('settings.cos.provider.aliyun')); - dropdown.addOption('tencent', t('settings.cos.provider.tencent')); - dropdown.addOption('aws', t('settings.cos.provider.aws')); - dropdown.addOption('cloudflare', t('settings.cos.provider.cloudflare')); - - dropdown.setValue(this.plugin.settings.cosProvider); - dropdown.onChange(async (value) => { - this.plugin.settings.cosProvider = value as 'aliyun' | 'tencent' | 'aws' | 'cloudflare'; + .setName(t('settings.image.enable.name')) + .setDesc(t('settings.image.enable.desc')) + .addToggle(toggle => toggle + .setValue(this.plugin.settings.enableImageProcessing) + .onChange(async (value) => { + this.plugin.settings.enableImageProcessing = value; await this.plugin.saveSettings(); - // Re-render settings to show/hide region field and load saved config + // Re-render to show/hide image processing settings this.display(); - }); + })); + + // Only show image processing settings if enabled + if (this.plugin.settings.enableImageProcessing) { + // Cloud provider settings subsection + containerEl.createEl('h3', { + text: t('settings.cos.provider.section.title'), + cls: 'git-sync-subsection-title' }); - // Get current config for the selected provider - const currentConfig = this.plugin.getCurrentCosConfig(); - - // Secret ID - new Setting(containerEl) - .setName(t('settings.cos.secret.id.name') + ' *') - .setDesc(t('settings.cos.secret.id.desc')) - .addText(text => text - .setPlaceholder(t('settings.cos.secret.id.placeholder')) - .setValue(currentConfig.secretId) - .onChange(async (value) => { - this.plugin.updateCurrentCosConfig({ secretId: value }); - await this.plugin.saveSettings(); - })); - - // Secret Key - new Setting(containerEl) - .setName(t('settings.cos.secret.key.name') + ' *') - .setDesc(t('settings.cos.secret.key.desc')) - .addText(text => text - .setPlaceholder(t('settings.cos.secret.key.placeholder')) - .setValue(currentConfig.secretKey) - .onChange(async (value) => { - this.plugin.updateCurrentCosConfig({ secretKey: value }); - await this.plugin.saveSettings(); - })); - - // Bucket - new Setting(containerEl) - .setName(t('settings.cos.bucket.name') + ' *') - .setDesc(t('settings.cos.bucket.desc')) - .addText(text => text - .setPlaceholder(t('settings.cos.bucket.placeholder')) - .setValue(currentConfig.bucket) - .onChange(async (value) => { - this.plugin.updateCurrentCosConfig({ bucket: value }); - await this.plugin.saveSettings(); - })); - - // Region (required for all providers) - new Setting(containerEl) - .setName(t('settings.cos.region.name') + ' *') - .setDesc(t('settings.cos.region.desc')) - .addText(text => text - .setPlaceholder(t('settings.cos.region.placeholder')) - .setValue(currentConfig.region) - .onChange(async (value) => { - this.plugin.updateCurrentCosConfig({ region: value }); - await this.plugin.saveSettings(); - })); - - // Endpoint (only for Cloudflare R2) - if (this.plugin.settings.cosProvider === 'cloudflare') { + // Cloud storage provider new Setting(containerEl) - .setName(t('settings.cos.endpoint.name') + ' *') - .setDesc(t('settings.cos.endpoint.desc')) + .setName(t('settings.cos.provider.name')) + .setDesc(t('settings.cos.provider.desc')) + .addDropdown(dropdown => { + dropdown.addOption('aliyun', t('settings.cos.provider.aliyun')); + dropdown.addOption('tencent', t('settings.cos.provider.tencent')); + dropdown.addOption('aws', t('settings.cos.provider.aws')); + dropdown.addOption('cloudflare', t('settings.cos.provider.cloudflare')); + + dropdown.setValue(this.plugin.settings.cosProvider); + dropdown.onChange(async (value) => { + this.plugin.settings.cosProvider = value as 'aliyun' | 'tencent' | 'aws' | 'cloudflare'; + await this.plugin.saveSettings(); + // Re-render settings to show/hide region field and load saved config + this.display(); + }); + }); + + // Get current config for the selected provider + const currentConfig = this.plugin.getCurrentCosConfig(); + + // Secret ID + new Setting(containerEl) + .setName(t('settings.cos.secret.id.name') + ' *') + .setDesc(t('settings.cos.secret.id.desc')) .addText(text => text - .setPlaceholder(t('settings.cos.endpoint.placeholder')) - .setValue(currentConfig.endpoint) + .setPlaceholder(t('settings.cos.secret.id.placeholder')) + .setValue(currentConfig.secretId) .onChange(async (value) => { - this.plugin.updateCurrentCosConfig({ endpoint: value }); + this.plugin.updateCurrentCosConfig({ secretId: value }); await this.plugin.saveSettings(); })); - } - // CDN URL - new Setting(containerEl) - .setName(t('settings.cos.cdn.name')) - .setDesc(t('settings.cos.cdn.desc')) - .addText(text => text - .setPlaceholder(t('settings.cos.cdn.placeholder')) - .setValue(currentConfig.cdnUrl) - .onChange(async (value) => { - this.plugin.updateCurrentCosConfig({ cdnUrl: value }); - await this.plugin.saveSettings(); - })); + // Secret Key + new Setting(containerEl) + .setName(t('settings.cos.secret.key.name') + ' *') + .setDesc(t('settings.cos.secret.key.desc')) + .addText(text => text + .setPlaceholder(t('settings.cos.secret.key.placeholder')) + .setValue(currentConfig.secretKey) + .onChange(async (value) => { + this.plugin.updateCurrentCosConfig({ secretKey: value }); + await this.plugin.saveSettings(); + })); - // Clear current configuration button - new Setting(containerEl) - .setName(t('settings.cos.clear.name')) - .setDesc(t('settings.cos.clear.desc')) - .addButton(button => button - .setButtonText(t('settings.cos.clear.button')) - .setWarning() - .onClick(async () => { - this.plugin.clearCurrentCosConfig(); - await this.plugin.saveSettings(); - new Notice(t('settings.cos.clear.success')); - // Re-render to show cleared fields - this.display(); - })); + // Bucket + new Setting(containerEl) + .setName(t('settings.cos.bucket.name') + ' *') + .setDesc(t('settings.cos.bucket.desc')) + .addText(text => text + .setPlaceholder(t('settings.cos.bucket.placeholder')) + .setValue(currentConfig.bucket) + .onChange(async (value) => { + this.plugin.updateCurrentCosConfig({ bucket: value }); + await this.plugin.saveSettings(); + })); - // Test COS configuration - new Setting(containerEl) - .setName(t('settings.cos.test.name')) - .setDesc(t('settings.cos.test.desc')) - .addButton(button => button - .setButtonText(t('settings.cos.test.button')) - .onClick(async () => { - // Validate required fields based on provider - const requiredFields = [ - currentConfig.secretId, - currentConfig.secretKey, - currentConfig.bucket, - currentConfig.region // Region is required for all providers - ]; + // Region (required for all providers) + new Setting(containerEl) + .setName(t('settings.cos.region.name') + ' *') + .setDesc(t('settings.cos.region.desc')) + .addText(text => text + .setPlaceholder(t('settings.cos.region.placeholder')) + .setValue(currentConfig.region) + .onChange(async (value) => { + this.plugin.updateCurrentCosConfig({ region: value }); + await this.plugin.saveSettings(); + })); - // Add provider-specific required fields - if (this.plugin.settings.cosProvider === 'cloudflare') { - requiredFields.push(currentConfig.endpoint); // Endpoint only required for Cloudflare R2 - } + // Endpoint (only for Cloudflare R2) + if (this.plugin.settings.cosProvider === 'cloudflare') { + new Setting(containerEl) + .setName(t('settings.cos.endpoint.name') + ' *') + .setDesc(t('settings.cos.endpoint.desc')) + .addText(text => text + .setPlaceholder(t('settings.cos.endpoint.placeholder')) + .setValue(currentConfig.endpoint) + .onChange(async (value) => { + this.plugin.updateCurrentCosConfig({ endpoint: value }); + await this.plugin.saveSettings(); + })); + } - if (requiredFields.some(field => !field.trim())) { - new Notice(t('cos.error.config.invalid')); - return; - } + // CDN URL + new Setting(containerEl) + .setName(t('settings.cos.cdn.name')) + .setDesc(t('settings.cos.cdn.desc')) + .addText(text => text + .setPlaceholder(t('settings.cos.cdn.placeholder')) + .setValue(currentConfig.cdnUrl) + .onChange(async (value) => { + this.plugin.updateCurrentCosConfig({ cdnUrl: value }); + await this.plugin.saveSettings(); + })); - // Set loading state - button.setButtonText(t('settings.cos.test.loading')); - button.setDisabled(true); + // Clear current configuration button + new Setting(containerEl) + .setName(t('settings.cos.clear.name')) + .setDesc(t('settings.cos.clear.desc')) + .addButton(button => button + .setButtonText(t('settings.cos.clear.button')) + .setWarning() + .onClick(async () => { + this.plugin.clearCurrentCosConfig(); + await this.plugin.saveSettings(); + new Notice(t('settings.cos.clear.success')); + // Re-render to show cleared fields + this.display(); + })); - try { - const cosConfig = { - provider: this.plugin.settings.cosProvider, - secretId: currentConfig.secretId, - secretKey: currentConfig.secretKey, - bucket: currentConfig.bucket, - endpoint: currentConfig.endpoint, - cdnUrl: currentConfig.cdnUrl, - region: currentConfig.region - }; + // Test COS configuration + new Setting(containerEl) + .setName(t('settings.cos.test.name')) + .setDesc(t('settings.cos.test.desc')) + .addButton(button => button + .setButtonText(t('settings.cos.test.button')) + .onClick(async () => { + // Validate required fields based on provider + const requiredFields = [ + currentConfig.secretId, + currentConfig.secretKey, + currentConfig.bucket, + currentConfig.region // Region is required for all providers + ]; - const cosService = new (await import('./cos-service')).CosService(cosConfig); - const result = await cosService.testConnection(); - - if (result.success) { - new Notice(t('cos.test.success')); - } else { - new Notice(t('cos.test.failed', { error: result.message })); + // Add provider-specific required fields + if (this.plugin.settings.cosProvider === 'cloudflare') { + requiredFields.push(currentConfig.endpoint); // Endpoint only required for Cloudflare R2 } - } catch (error) { - console.error('COS test error:', error); - new Notice(t('cos.test.failed', { error: error.message })); - } finally { - button.setButtonText(t('settings.cos.test.button')); - button.setDisabled(false); - } - })); - // Image upload settings subsection - containerEl.createEl('h3', { - text: t('settings.image.upload.section.title'), - cls: 'git-sync-subsection-title' - }); + if (requiredFields.some(field => !field.trim())) { + new Notice(t('cos.error.config.invalid')); + return; + } - // Keep images locally - new Setting(containerEl) - .setName(t('settings.cos.keep.local.name')) - .setDesc(t('settings.cos.keep.local.desc')) - .addToggle(toggle => toggle - .setValue(this.plugin.settings.keepLocalImages) - .onChange(async (value) => { - this.plugin.settings.keepLocalImages = value; - await this.plugin.saveSettings(); - // Re-render to show/hide local path setting - this.display(); - })); + // Set loading state + button.setButtonText(t('settings.cos.test.loading')); + button.setDisabled(true); - // Local storage path (only when keepLocalImages is true) - if (this.plugin.settings.keepLocalImages) { + try { + const cosConfig = { + provider: this.plugin.settings.cosProvider, + secretId: currentConfig.secretId, + secretKey: currentConfig.secretKey, + bucket: currentConfig.bucket, + endpoint: currentConfig.endpoint, + cdnUrl: currentConfig.cdnUrl, + region: currentConfig.region + }; + + const cosService = new (await import('./cos-service')).CosService(cosConfig); + const result = await cosService.testConnection(); + + if (result.success) { + new Notice(t('cos.test.success')); + } else { + new Notice(t('cos.test.failed', { error: result.message })); + } + } catch (error) { + console.error('COS test error:', error); + new Notice(t('cos.test.failed', { error: error.message })); + } finally { + button.setButtonText(t('settings.cos.test.button')); + button.setDisabled(false); + } + })); + + // Image upload settings subsection + containerEl.createEl('h3', { + text: t('settings.image.upload.section.title'), + cls: 'git-sync-subsection-title' + }); + + // Keep images locally new Setting(containerEl) - .setName(t('settings.cos.local.path.name')) - .setDesc(t('settings.cos.local.path.desc')) - .addText(text => text - .setPlaceholder(t('settings.cos.local.path.placeholder')) - .setValue(this.plugin.settings.localImagePath) + .setName(t('settings.cos.keep.local.name')) + .setDesc(t('settings.cos.keep.local.desc')) + .addToggle(toggle => toggle + .setValue(this.plugin.settings.keepLocalImages) .onChange(async (value) => { - this.plugin.settings.localImagePath = value; + this.plugin.settings.keepLocalImages = value; + await this.plugin.saveSettings(); + // Re-render to show/hide local path setting + this.display(); + })); + + // Local storage path (only when keepLocalImages is true) + if (this.plugin.settings.keepLocalImages) { + new Setting(containerEl) + .setName(t('settings.cos.local.path.name')) + .setDesc(t('settings.cos.local.path.desc')) + .addText(text => text + .setPlaceholder(t('settings.cos.local.path.placeholder')) + .setValue(this.plugin.settings.localImagePath) + .onChange(async (value) => { + this.plugin.settings.localImagePath = value; + await this.plugin.saveSettings(); + })); + } + + // Upload path template + new Setting(containerEl) + .setName(t('settings.cos.upload.path.name')) + .setDesc(t('settings.cos.upload.path.desc')) + .addText(text => text + .setPlaceholder(t('settings.cos.upload.path.placeholder')) + .setValue(this.plugin.settings.imageUploadPath) + .onChange(async (value) => { + this.plugin.settings.imageUploadPath = value; await this.plugin.saveSettings(); })); } - // Upload path template - new Setting(containerEl) - .setName(t('settings.cos.upload.path.name')) - .setDesc(t('settings.cos.upload.path.desc')) - .addText(text => text - .setPlaceholder(t('settings.cos.upload.path.placeholder')) - .setValue(this.plugin.settings.imageUploadPath) - .onChange(async (value) => { - this.plugin.settings.imageUploadPath = value; - await this.plugin.saveSettings(); - })); - // Danger zone title diff --git a/types.ts b/types.ts index fd44b0f..ed4bdf6 100644 --- a/types.ts +++ b/types.ts @@ -14,6 +14,9 @@ export interface GitSyncSettings { showRibbonIcon: boolean; // Whether to show sidebar button language: 'zh' | 'en' | 'auto'; // Language setting, auto means follow system + // Image processing settings + enableImageProcessing: boolean; // Whether to enable image processing and upload functionality + // COS (Cloud Object Storage) settings cosProvider: 'aliyun' | 'tencent' | 'aws' | 'cloudflare'; // Current selected provider cosConfigs: { @@ -38,6 +41,9 @@ export const DEFAULT_SETTINGS: GitSyncSettings = { showRibbonIcon: true, language: 'auto', // Follow Obsidian language setting + // Image processing defaults + enableImageProcessing: false, + // COS default settings cosProvider: 'aliyun', cosConfigs: {