From c369c291a701611be6398675931b9cf8f42a0b92 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Fri, 31 Oct 2025 21:42:48 +0000 Subject: [PATCH 01/32] feat: add auto sync functionality with configurable delay in settings --- src/lang/locale/en.ts | 5 ++ src/lang/locale/ja.ts | 5 ++ src/lang/locale/zh.ts | 5 ++ src/main.ts | 103 +++++++++++++++++++++++++++++++++++++++++- src/ui/settingTabs.ts | 46 +++++++++++++++++++ 5 files changed, 163 insertions(+), 1 deletion(-) diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 23eb1b8..ea43964 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -35,6 +35,11 @@ export const en = { NotionUserText: "Enter your notion ID", NotionLinkDisplay: "Notion Link Display", NotionLinkDisplayDesc: "Default is ON, if you want to hide the link in the front matter, please turn it off", + AutoSync: "Auto Sync", + AutoSyncDesc: "Automatically sync to Notion when frontmatter or content is modified (requires existing NotionID)", + AutoSyncDelay: "Auto Sync Delay (seconds)", + AutoSyncDelayDesc: "How many seconds to wait after document modification before triggering auto sync (default: 5 seconds, minimum: 2 seconds)", + AutoSyncDelayText: "Enter delay in seconds", NotionGeneralSettingHeader: "General Notion Database Settings", NotionGeneralButton: "Notion General Sync", NotionGeneralButtonDesc: "Open this option, Sync to Notion General Database command will be displayed in the command palette (default: ON)", diff --git a/src/lang/locale/ja.ts b/src/lang/locale/ja.ts index df6e5ed..38c5bab 100644 --- a/src/lang/locale/ja.ts +++ b/src/lang/locale/ja.ts @@ -33,6 +33,11 @@ export const ja = { NotionUserText: "Notion IDを入力", NotionLinkDisplay: "Notionリンク表示", NotionLinkDisplayDesc: "デフォルトはONです。front matterにリンクを非表示にしたい場合は、オフにしてください", + AutoSync: "自動同期", + AutoSyncDesc: "frontmatter またはコンテンツが変更されたときに自動的に Notion に同期します(NotionID が必要)", + AutoSyncDelay: "自動同期遅延時間(秒)", + AutoSyncDelayDesc: "ドキュメントの変更後、自動同期をトリガーするまでの待機時間(デフォルト:5秒、最小:2秒)", + AutoSyncDelayText: "遅延秒数を入力", NotionGeneralSettingHeader: "一般的なNotionデータベース設定", NotionGeneralButton: "一般的なNotion同期", NotionGeneralButtonDesc: "このオプションを開くと、一般的なNotionデータベース同期コマンドがコマンドパレットに表示されます(デフォルト:ON)", diff --git a/src/lang/locale/zh.ts b/src/lang/locale/zh.ts index f982b45..b03296a 100644 --- a/src/lang/locale/zh.ts +++ b/src/lang/locale/zh.ts @@ -35,6 +35,11 @@ export const zh = { NotionUserText: "输入你的 Notion ID", NotionLinkDisplay: "Notion 链接显示", NotionLinkDisplayDesc: "默认开启,如果你不想在front matter中显示链接,请关闭", + AutoSync: "自动同步", + AutoSyncDesc: "当检测到文档的 frontmatter 或内容发生修改时,自动同步到 Notion(需要文档已有 NotionID)", + AutoSyncDelay: "自动同步延迟时间(秒)", + AutoSyncDelayDesc: "文档修改后等待多少秒才触发自动同步,避免频繁同步(默认:5秒,最小:2秒)", + AutoSyncDelayText: "输入延迟秒数", NotionGeneralSettingHeader: "普通 Notion 数据库设置", NotionGeneralButton: "普通数据库同步", NotionGeneralButtonDesc: "打开此选项,同步到普通数据库命令将显示在命令面板中(默认:开)", diff --git a/src/main.ts b/src/main.ts index 6d0b517..d856772 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,9 @@ -import { App, Editor, MarkdownView, Notice, Plugin, PluginSettingTab, Setting } from "obsidian"; +import { App, Editor, MarkdownView, Notice, Plugin, PluginSettingTab, Setting, TFile, EventRef } from "obsidian"; import { addIcons } from 'src/ui/icon'; import { i18nConfig } from "src/lang/I18n"; import ribbonCommands from "src/commands/NotionCommands"; import { ObsidianSettingTab, PluginSettings, DEFAULT_SETTINGS, DatabaseDetails } from "src/ui/settingTabs"; +import { uploadCommandNext, uploadCommandGeneral, uploadCommandCustom } from "src/upload/uploadCommand"; // Remember to rename these classes and interfaces! @@ -11,6 +12,9 @@ export default class ObsidianSyncNotionPlugin extends Plugin { settings: PluginSettings; commands: ribbonCommands; app: App; + modifyEventRef: EventRef | null = null; + autoSyncTimeout: NodeJS.Timeout | null = null; + private syncingFiles: Set = new Set(); async onload() { await this.loadSettings(); @@ -36,9 +40,18 @@ export default class ObsidianSyncNotionPlugin extends Plugin { // This adds a settings tab so the user can configure various aspects of the plugin this.addSettingTab(new ObsidianSettingTab(this.app, this)); + // Setup auto sync listener + this.setupAutoSync(); + } onunload() { + if (this.modifyEventRef) { + this.app.vault.offref(this.modifyEventRef); + } + if (this.autoSyncTimeout) { + clearTimeout(this.autoSyncTimeout); + } } async loadSettings() { @@ -80,6 +93,94 @@ export default class ObsidianSyncNotionPlugin extends Plugin { await this.saveSettings(); } + setupAutoSync() { + // Remove existing listener if any + if (this.modifyEventRef) { + this.app.vault.offref(this.modifyEventRef); + } + + // Only setup if autoSync is enabled + if (!this.settings.autoSync) { + return; + } + + // Listen for file modifications + this.modifyEventRef = this.app.vault.on('modify', async (file: TFile) => { + // Only process markdown files + if (!(file instanceof TFile) || file.extension !== 'md') { + return; + } + + // Debounce: clear existing timeout + if (this.autoSyncTimeout) { + clearTimeout(this.autoSyncTimeout); + } + + // Set a new timeout to trigger sync after user-configured delay (in seconds) + const delayMs = (this.settings.autoSyncDelay || 2) * 1000; + this.autoSyncTimeout = setTimeout(async () => { + await this.autoSyncFile(file); + }, delayMs); + }); + } + + async autoSyncFile(file: TFile) { + // Check if file is already being synced + if (this.syncingFiles.has(file.path)) { + console.log(`[AutoSync] File ${file.path} is already being synced, skipping`); + return; + } + + try { + this.syncingFiles.add(file.path); + + // Get file's frontmatter + const frontMatter = this.app.metadataCache.getFileCache(file)?.frontmatter; + if (!frontMatter) { + console.log(`[AutoSync] No frontmatter found in ${file.path}`); + return; + } + + // Find which database this file belongs to by checking for NotionID-{abName} + let foundDbDetails: DatabaseDetails | null = null; + let notionId: string | null = null; + + for (const key in this.settings.databaseDetails) { + const dbDetails = this.settings.databaseDetails[key]; + const notionIDKey = `NotionID-${dbDetails.abName}`; + + if (frontMatter[notionIDKey]) { + foundDbDetails = dbDetails; + notionId = String(frontMatter[notionIDKey]); + break; + } + } + + // If no NotionID found, skip auto sync + if (!foundDbDetails || !notionId) { + console.log(`[AutoSync] No NotionID found in ${file.path}, skipping auto sync`); + return; + } + + console.log(`[AutoSync] ${new Date().toISOString()} Auto syncing ${file.basename} to ${foundDbDetails.fullName}`); + + // Trigger appropriate upload command based on database format + if (foundDbDetails.format === 'next') { + await uploadCommandNext(this, this.settings, foundDbDetails, this.app); + } else if (foundDbDetails.format === 'general') { + await uploadCommandGeneral(this, this.settings, foundDbDetails, this.app); + } else if (foundDbDetails.format === 'custom') { + await uploadCommandCustom(this, this.settings, foundDbDetails, this.app); + } + + } catch (error) { + console.error(`[AutoSync] Error syncing file ${file.path}:`, error); + new Notice(`Auto sync failed for ${file.basename}: ${error.message}`); + } finally { + this.syncingFiles.delete(file.path); + } + } + } diff --git a/src/ui/settingTabs.ts b/src/ui/settingTabs.ts index 543d6f4..023479f 100644 --- a/src/ui/settingTabs.ts +++ b/src/ui/settingTabs.ts @@ -13,6 +13,8 @@ export interface PluginSettings { bannerUrl: string; notionUser: string; NotionLinkDisplay: boolean; + autoSync: boolean; + autoSyncDelay: number; proxy: string; GeneralButton: boolean; tagButton: boolean; @@ -49,6 +51,8 @@ export const DEFAULT_SETTINGS: PluginSettings = { bannerUrl: "", notionUser: "", NotionLinkDisplay: true, + autoSync: false, + autoSyncDelay: 5, proxy: "", GeneralButton: true, tagButton: true, @@ -67,6 +71,7 @@ export const DEFAULT_SETTINGS: PluginSettings = { export class ObsidianSettingTab extends PluginSettingTab { plugin: ObsidianSyncNotionPlugin; databaseEl: HTMLDivElement; + autoSyncDelayContainer: HTMLElement | null = null; constructor(app: App, plugin: ObsidianSyncNotionPlugin) { super(app, plugin); @@ -87,6 +92,34 @@ export class ObsidianSettingTab extends PluginSettingTab { this.createSettingEl(containerEl, i18nConfig.NotionLinkDisplay, i18nConfig.NotionLinkDisplayDesc, 'toggle', i18nConfig.NotionLinkDisplay, this.plugin.settings.NotionLinkDisplay, 'NotionLinkDisplay') + this.createSettingEl(containerEl, i18nConfig.AutoSync, i18nConfig.AutoSyncDesc, 'toggle', i18nConfig.AutoSync, this.plugin.settings.autoSync, 'autoSync') + + // Auto Sync Delay setting - only visible when autoSync is enabled + this.autoSyncDelayContainer = containerEl.createDiv(); + const delaySetting = new Setting(this.autoSyncDelayContainer) + .setName(i18nConfig.AutoSyncDelay) + .setDesc(i18nConfig.AutoSyncDelayDesc) + .addText((text) => + text + .setPlaceholder(i18nConfig.AutoSyncDelayText) + .setValue(String(this.plugin.settings.autoSyncDelay)) + .onChange(async (value) => { + const delay = parseFloat(value); + if (!isNaN(delay) && delay >= 2) { + this.plugin.settings.autoSyncDelay = delay; + await this.plugin.saveSettings(); + } else if (!isNaN(delay) && delay < 2) { + // If user enters less than 2 seconds, set it to 2 + this.plugin.settings.autoSyncDelay = 2; + await this.plugin.saveSettings(); + text.setValue('2'); + } + }) + ); + + // Set initial visibility + this.updateAutoSyncDelayVisibility(); + // add new button new Setting(containerEl) @@ -151,6 +184,13 @@ export class ObsidianSettingTab extends PluginSettingTab { element.style.alignItems = "center"; } + // Update visibility of autoSyncDelay setting based on autoSync toggle + public updateAutoSyncDelayVisibility() { + if (this.autoSyncDelayContainer) { + this.autoSyncDelayContainer.style.display = this.plugin.settings.autoSync ? "block" : "none"; + } + } + // function to add one setting element in the setting tab. public createSettingEl(containerEl: HTMLElement, name: string, desc: string, type: string, placeholder: string, holderValue: any, settingsKey: string) { if (type === 'password') { @@ -178,6 +218,12 @@ export class ObsidianSettingTab extends PluginSettingTab { this.plugin.settings[settingsKey] = value; // Update the plugin settings directly await this.plugin.saveSettings(); await this.plugin.commands.updateCommand(); + + // If autoSync setting changed, update the listener and visibility + if (settingsKey === 'autoSync') { + this.plugin.setupAutoSync(); + this.updateAutoSyncDelayVisibility(); + } }) ); } else if (type === 'text') { From e5f906bba20bf7b69bdeece7fd579414e0b673b8 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Fri, 31 Oct 2025 21:57:21 +0000 Subject: [PATCH 02/32] feat: migrate settings for auto sync feature and add user notification --- src/lang/locale/en.ts | 1 + src/lang/locale/ja.ts | 2 + src/main.ts | 94 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index ea43964..544ae03 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -84,5 +84,6 @@ export const en = { BlockUploaded: "All blocks uploaded", ExtraBlockUploaded: "Extra blocks uploaded", CheckConsole: "Check the console for more information \n opt+cmd+i/ctrl+shift+i", + SettingsMigrated: "✨ Plugin settings updated! Auto sync feature added, check plugin settings", "reach-mobile-limit": "The number of blocks exceeds the limit of 100, please use the desktop plugin", } diff --git a/src/lang/locale/ja.ts b/src/lang/locale/ja.ts index 38c5bab..215b6bf 100644 --- a/src/lang/locale/ja.ts +++ b/src/lang/locale/ja.ts @@ -76,4 +76,6 @@ export const ja = { BlockUploaded: "ブロックがアップロードされました", ExtraBlockUploaded: "追加ブロックがアップロードされました", CheckConsole: "詳細情報を確認するには、コンソールを開いてください \n opt+cmd+i/ctrl+shift+i", + SettingsMigrated: "✨ プラグイン設定が更新されました!自動同期機能が追加されました。設定を確認してください", + "reach-mobile-limit": "ブロック数が100の制限を超えています。デスクトップ版プラグインを使用してください", }; diff --git a/src/main.ts b/src/main.ts index d856772..ad8b61e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,6 +18,17 @@ export default class ObsidianSyncNotionPlugin extends Plugin { async onload() { await this.loadSettings(); + + // Log loaded settings for debugging + console.log('[Plugin] Loaded settings:', { + autoSync: this.settings.autoSync, + autoSyncDelay: this.settings.autoSyncDelay, + NotionLinkDisplay: this.settings.NotionLinkDisplay, + bannerUrl: this.settings.bannerUrl ? 'set' : 'empty', + notionUser: this.settings.notionUser ? 'set' : 'empty', + databaseCount: Object.keys(this.settings.databaseDetails || {}).length + }); + this.commands = new ribbonCommands(this); addIcons(); @@ -55,15 +66,89 @@ export default class ObsidianSyncNotionPlugin extends Plugin { } async loadSettings() { + const loadedData = await this.loadData(); + + // Merge loaded data with defaults, ensuring all fields exist this.settings = Object.assign( {}, DEFAULT_SETTINGS, - await this.loadData() + loadedData || {} ); + + // Ensure critical fields have valid values + if (typeof this.settings.autoSync !== 'boolean') { + this.settings.autoSync = DEFAULT_SETTINGS.autoSync; + } + if (typeof this.settings.autoSyncDelay !== 'number' || this.settings.autoSyncDelay < 2) { + this.settings.autoSyncDelay = DEFAULT_SETTINGS.autoSyncDelay; + } + if (typeof this.settings.NotionLinkDisplay !== 'boolean') { + this.settings.NotionLinkDisplay = DEFAULT_SETTINGS.NotionLinkDisplay; + } + + // Ensure databaseDetails exists + if (!this.settings.databaseDetails || typeof this.settings.databaseDetails !== 'object') { + this.settings.databaseDetails = {}; + } + + // Save settings if any migration was needed + const needsSave = !loadedData || + loadedData.autoSync === undefined || + loadedData.autoSyncDelay === undefined || + loadedData.NotionLinkDisplay === undefined; + + if (needsSave) { + const migratedFields = []; + if (!loadedData) { + console.log('[Settings] First-time setup, creating default settings'); + } else { + if (loadedData.autoSync === undefined) migratedFields.push('autoSync'); + if (loadedData.autoSyncDelay === undefined) migratedFields.push('autoSyncDelay'); + if (loadedData.NotionLinkDisplay === undefined) migratedFields.push('NotionLinkDisplay'); + + console.log('[Settings] Migrating settings, adding fields:', migratedFields.join(', ')); + } + + await this.saveSettings(); + + // Notify user about settings migration (only for existing users, not first-time setup) + if (loadedData && Object.keys(loadedData).length > 0 && migratedFields.length > 0) { + new Notice(i18nConfig.SettingsMigrated, 6000); + console.log('[Settings] Migration notice shown to user'); + } + } } async saveSettings() { + // Validate settings before saving + this.validateSettings(); await this.saveData(this.settings); + console.log('[Settings] Settings saved successfully', { + autoSync: this.settings.autoSync, + autoSyncDelay: this.settings.autoSyncDelay, + NotionLinkDisplay: this.settings.NotionLinkDisplay, + databaseCount: Object.keys(this.settings.databaseDetails || {}).length + }); + } + + validateSettings() { + // Ensure all required fields have valid values + if (typeof this.settings.autoSync !== 'boolean') { + console.warn('[Settings] Invalid autoSync value, resetting to default'); + this.settings.autoSync = DEFAULT_SETTINGS.autoSync; + } + if (typeof this.settings.autoSyncDelay !== 'number' || this.settings.autoSyncDelay < 2) { + console.warn('[Settings] Invalid autoSyncDelay value, resetting to default'); + this.settings.autoSyncDelay = DEFAULT_SETTINGS.autoSyncDelay; + } + if (typeof this.settings.NotionLinkDisplay !== 'boolean') { + console.warn('[Settings] Invalid NotionLinkDisplay value, resetting to default'); + this.settings.NotionLinkDisplay = DEFAULT_SETTINGS.NotionLinkDisplay; + } + if (!this.settings.databaseDetails || typeof this.settings.databaseDetails !== 'object') { + console.warn('[Settings] Invalid databaseDetails, resetting to empty object'); + this.settings.databaseDetails = {}; + } } async addDatabaseDetails(dbDetails: DatabaseDetails) { @@ -133,7 +218,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { try { this.syncingFiles.add(file.path); - + // Get file's frontmatter const frontMatter = this.app.metadataCache.getFileCache(file)?.frontmatter; if (!frontMatter) { @@ -148,7 +233,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { for (const key in this.settings.databaseDetails) { const dbDetails = this.settings.databaseDetails[key]; const notionIDKey = `NotionID-${dbDetails.abName}`; - + if (frontMatter[notionIDKey]) { foundDbDetails = dbDetails; notionId = String(frontMatter[notionIDKey]); @@ -180,8 +265,5 @@ export default class ObsidianSyncNotionPlugin extends Plugin { this.syncingFiles.delete(file.path); } } - } - - From 59fa32b5e5753ce09fe9c8626465dd82c2e426e9 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Fri, 31 Oct 2025 23:35:16 +0000 Subject: [PATCH 03/32] docs: enhance auto sync documentation with detailed setup, scenarios, and troubleshooting guidance --- docs/en/04-sync.md | 82 +++++++++++++++++++++++++++++++- docs/en/05-troubleshooting.md | 64 +++++++++++++++++++++++++ docs/zh/04-sync.md | 88 ++++++++++++++++++++++++++++++++++- docs/zh/05-troubleshooting.md | 64 +++++++++++++++++++++++++ 4 files changed, 295 insertions(+), 3 deletions(-) diff --git a/docs/en/04-sync.md b/docs/en/04-sync.md index e925697..cbca3f5 100644 --- a/docs/en/04-sync.md +++ b/docs/en/04-sync.md @@ -7,6 +7,86 @@ description: How to sync your Obsidian notes to Notion using the NotionNext plug After configuring your Notion database in the plugin settings, you can start syncing your Obsidian notes to Notion. +## Manual Sync + To sync a note, open the note you want to sync and use the "Share to NotionNext" command from the command palette or the note context menu. This will create a new page in your Notion database with the content of your Obsidian note. -You can also set up automatic syncing for specific notes or folders by configuring the plugin settings. This way, any changes you make to your Obsidian notes will be automatically reflected in Notion. +## Auto Sync + +The plugin supports automatic syncing that monitors your notes for changes and automatically syncs them to Notion. + +### Enabling Auto Sync + +1. Open the plugin settings +2. Find the "Auto Sync" toggle under General Settings +3. Enable the toggle +4. Configure the "Auto Sync Delay" (default: 5 seconds, minimum: 2 seconds) + +### How Auto Sync Works + +When auto sync is enabled: +- The plugin monitors markdown files for changes +- After you stop editing for the configured delay period, auto sync is triggered +- Only files that have already been synced to Notion (have a NotionID in frontmatter) will be auto-synced +- If a file is linked to multiple databases, it will sync to all of them automatically + +### Auto Sync Scenarios + +#### Scenario A: New Document (Not Yet Synced) + +```yaml +--- +title: My New Article +tags: [blog, tech] +--- +``` + +**Behavior:** +- ✅ Detects no NotionID present +- ✅ Shows notice: "⚠️ Auto sync skipped: This document has not been synced to Notion, please upload manually first" +- ✅ No sync operation performed +- 📝 **Action Required:** Manually sync the document first using the command palette + +#### Scenario B: Synced to One Database + +```yaml +--- +title: My Article +NotionID-blog: abc123 +--- +``` + +**Behavior:** +- ✅ Detects 1 NotionID +- ✅ Automatically syncs to the Blog database +- ✅ Shows success/failure notification from the upload command +- 📝 **No Action Required:** Changes are automatically synced + +#### Scenario C: Synced to Multiple Databases + +```yaml +--- +title: My Article +NotionID-blog: abc123 +NotionID-portfolio: def456 +NotionID-notes: ghi789 +--- +``` + +**Behavior:** +- ✅ Detects 3 NotionIDs +- ✅ Shows notice: "🔄 Auto sync: Syncing to 3 database(s)..." +- ✅ Syncs to all 3 databases sequentially +- ✅ Shows individual result notifications for each database +- 📝 **No Action Required:** Changes are automatically synced to all linked databases + +### Auto Sync Best Practices + +1. **First Sync Manually**: Always perform the first sync manually to establish the NotionID link +2. **Configure Delay Appropriately**: Set a longer delay (5-10 seconds) if you make frequent edits +3. **Monitor Sync Status**: Check the notifications to ensure syncs complete successfully +4. **Check Logs**: Open the developer console (Ctrl+Shift+I / Cmd+Option+I) to view detailed sync logs + +### Troubleshooting + +Having issues with auto sync? Check the [Troubleshooting Guide](05-troubleshooting.md) for detailed solutions to common problems. diff --git a/docs/en/05-troubleshooting.md b/docs/en/05-troubleshooting.md index 7647bad..6e4682d 100644 --- a/docs/en/05-troubleshooting.md +++ b/docs/en/05-troubleshooting.md @@ -5,6 +5,70 @@ description: Common issues and solutions for the Obsidian to NotionNext plugin # Troubleshooting +## Auto Sync Issues + +### Auto sync not working? + +**Possible causes and solutions:** + +- **Auto sync not enabled**: Ensure auto sync is enabled in plugin settings under General Settings +- **No NotionID in frontmatter**: Verify the document has a NotionID field (e.g., `NotionID-blog: abc123`) in its frontmatter. Auto sync only works for documents that have been manually synced at least once +- **Invalid database configuration**: Check that your database configuration is valid and the API credentials are correct +- **Console errors**: Look for errors in the developer console (`Ctrl+Shift+I` / `Cmd+Option+I`) + +### Sync too frequent? + +If auto sync is triggering too often while you're editing: + +- **Increase the delay**: Go to plugin settings and increase the "Auto Sync Delay" value (default is 5 seconds) +- **Understanding the delay**: The delay timer resets each time you make an edit, so sync only triggers after you stop editing for the configured duration + +### Missing notifications? + +If you're not seeing sync notifications: + +- **Notification duration**: Notifications appear for 3-6 seconds and then automatically disappear +- **Check console logs**: Open the developer console (`Ctrl+Shift+I` / `Cmd+Option+I`) to view detailed sync information +- **Multiple syncs**: When syncing to multiple databases, you'll see a notification for the multi-database sync plus individual result notifications + +### Auto sync skipped for new documents + +If you see the message "⚠️ Auto sync skipped: This document has not been synced to Notion, please upload manually first": + +- **First sync required**: This is expected behavior. Auto sync only works for documents that already have a NotionID +- **Solution**: Use the command palette (`Ctrl/Cmd + P`) and select "Share to NotionNext" to perform the first manual sync +- **After manual sync**: Once the document has a NotionID in its frontmatter, auto sync will work automatically + +## General Sync Issues + +### Sync failed with error message + +If you see error messages during sync: + +1. **Check API credentials**: Verify your Notion API token and Database ID are correct +2. **Check permissions**: Ensure the integration has access to the target database +3. **Network issues**: Check your internet connection +4. **Rate limiting**: Notion has API rate limits; wait a moment and try again +5. **Check console**: Open developer tools to see detailed error information + +### Multiple database sync issues + +When syncing to multiple databases: + +- **Partial failures**: If one database fails, others will still continue syncing +- **Individual notifications**: Each database sync shows its own result notification +- **Check frontmatter**: Verify all NotionID fields are present and correct (e.g., `NotionID-blog`, `NotionID-portfolio`) + +## Getting Help + If the problem persists, you can [open an issue on GitHub](https://github.com/jxpeng98/obsidian-to-NotionNext/issues) with detailed error information and the steps you took. You can find the error logs in Obsidian by going to developer tools (`Ctrl+Shift+I` or `Cmd+Option+I`) and checking the console for any error messages related to the NotionNext plugin. + +### What to include in bug reports: + +1. **Error messages**: Copy the exact error message from notifications or console +2. **Console logs**: Include relevant logs from the developer console (look for `[AutoSync]`, `[Settings]`, or `[Plugin]` prefixes) +3. **Steps to reproduce**: Describe what you were doing when the issue occurred +4. **Configuration**: Mention which database format you're using (NotionNext, General, or Custom) +5. **Settings**: Note if auto sync is enabled and what delay is configured diff --git a/docs/zh/04-sync.md b/docs/zh/04-sync.md index bfbe76b..4f1b0f5 100644 --- a/docs/zh/04-sync.md +++ b/docs/zh/04-sync.md @@ -7,6 +7,90 @@ description: 如何使用 NotionNext 插件将你的 Obsidian 笔记同步到 No 在插件设置中配置好你的 Notion 数据库后,你就可以开始将 Obsidian 笔记同步到 Notion 了。 -要同步一篇笔记,只需打开你想要同步的笔记,然后从命令面板(`Ctrl/Cmd + P`)或笔记的右键菜单中选择 “Share to NotionNext” 命令。这会在你的 Notion 数据库中创建一个新页面,内容与你的 Obsidian 笔记完全一致。 +## 手动同步 -你还可以为特定的笔记或文件夹设置自动同步。这样,你在 Obsidian 中对这些笔记所做的任何更改,都会自动反映到 Notion 中,非常方便。 +要同步一篇笔记,只需打开你想要同步的笔记,然后从命令面板(`Ctrl/Cmd + P`)或笔记的右键菜单中选择 "Share to NotionNext" 命令。这会在你的 Notion 数据库中创建一个新页面,内容与你的 Obsidian 笔记完全一致。 + +## 自动同步 + +插件支持自动同步功能,可以监控你的笔记变化并自动同步到 Notion。 + +### 启用自动同步 + +1. 打开插件设置 +2. 在通用设置下找到"自动同步"开关 +3. 开启该开关 +4. 配置"自动同步延迟时间"(默认:5秒,最小:2秒) + +### 自动同步工作原理 + +当自动同步启用后: + +- 插件会监控 Markdown 文件的变化 +- 在你停止编辑达到配置的延迟时间后,自动触发同步 +- 只有已经同步过的文件(frontmatter 中有 NotionID)才会被自动同步 +- 如果文件关联了多个数据库,会自动同步到所有数据库 + +### 自动同步场景示例 + +#### 场景 A:新文档(未同步) + +```yaml +--- +title: 我的新文章 +tags: [博客, 技术] +--- +``` + +**行为:** + +- ✅ 检测到没有 NotionID +- ✅ 显示提示:"⚠️ 自动同步跳过:此文档未同步到 Notion,请先手动上传" +- ✅ 不执行同步操作 +- 📝 **需要操作:** 先使用命令面板手动同步文档 + +#### 场景 B:已同步到一个数据库 + +```yaml +--- +title: 我的文章 +NotionID-blog: abc123 +--- +``` + +**行为:** + +- ✅ 检测到 1 个 NotionID +- ✅ 自动同步到 Blog 数据库 +- ✅ 显示上传命令返回的成功/失败通知 +- 📝 **无需操作:** 变更会自动同步 + +#### 场景 C:同步到多个数据库 + +```yaml +--- +title: 我的文章 +NotionID-blog: abc123 +NotionID-portfolio: def456 +NotionID-notes: ghi789 +--- +``` + +**行为:** + +- ✅ 检测到 3 个 NotionID +- ✅ 显示提示:"🔄 自动同步:正在同步到 3 个数据库..." +- ✅ 依次同步到所有 3 个数据库 +- ✅ 为每个数据库显示独立的结果通知 +- 📝 **无需操作:** 变更会自动同步到所有关联的数据库 + +### 自动同步最佳实践 + +1. **首次手动同步**:始终先手动执行第一次同步以建立 NotionID 链接 +2. **合理配置延迟**:如果你经常编辑,设置较长的延迟时间(5-10 秒) +3. **监控同步状态**:注意查看通知以确保同步成功完成 +4. **查看日志**:打开开发者控制台(Ctrl+Shift+I / Cmd+Option+I)查看详细的同步日志 + +### 故障排除 + +遇到自动同步问题?请查看[问题排查指南](05-troubleshooting.md)获取常见问题的详细解决方案。 diff --git a/docs/zh/05-troubleshooting.md b/docs/zh/05-troubleshooting.md index 69a93e3..ff6be2f 100644 --- a/docs/zh/05-troubleshooting.md +++ b/docs/zh/05-troubleshooting.md @@ -5,6 +5,70 @@ description: Obsidian to NotionNext 插件的常见问题与解决方案 # 问题排查 +## 自动同步问题 + +### 自动同步不工作? + +**可能的原因及解决方案:** + +- **未启用自动同步**:确保在插件设置的通用设置中启用了自动同步功能 +- **frontmatter 中没有 NotionID**:验证文档的 frontmatter 中有 NotionID 字段(如 `NotionID-blog: abc123`)。自动同步仅适用于至少手动同步过一次的文档 +- **数据库配置无效**:检查数据库配置是否有效,API 凭证是否正确 +- **控制台错误**:查看开发者控制台(`Ctrl+Shift+I` / `Cmd+Option+I`)中的错误信息 + +### 同步太频繁? + +如果在编辑时自动同步触发太频繁: + +- **增加延迟时间**:前往插件设置,增加"自动同步延迟时间"的值(默认为 5 秒) +- **理解延迟机制**:每次编辑都会重置延迟计时器,只有在停止编辑达到配置的时长后才会触发同步 + +### 看不到通知? + +如果没有看到同步通知: + +- **通知显示时长**:通知会显示 3-6 秒然后自动消失 +- **查看控制台日志**:打开开发者控制台(`Ctrl+Shift+I` / `Cmd+Option+I`)查看详细的同步信息 +- **多数据库同步**:同步到多个数据库时,你会看到一个多数据库同步通知以及各个数据库的结果通知 + +### 新文档跳过自动同步 + +如果看到消息"⚠️ 自动同步跳过:此文档未同步到 Notion,请先手动上传": + +- **需要首次同步**:这是预期行为。自动同步仅适用于已有 NotionID 的文档 +- **解决方案**:使用命令面板(`Ctrl/Cmd + P`)选择"Share to NotionNext"执行首次手动同步 +- **手动同步后**:一旦文档的 frontmatter 中有了 NotionID,自动同步就会自动工作 + +## 常规同步问题 + +### 同步失败并显示错误消息 + +如果在同步时看到错误消息: + +1. **检查 API 凭证**:验证 Notion API 令牌和数据库 ID 是否正确 +2. **检查权限**:确保集成有权限访问目标数据库 +3. **网络问题**:检查网络连接 +4. **速率限制**:Notion 有 API 速率限制,等待片刻后重试 +5. **查看控制台**:打开开发者工具查看详细的错误信息 + +### 多数据库同步问题 + +同步到多个数据库时: + +- **部分失败**:如果一个数据库失败,其他数据库仍会继续同步 +- **独立通知**:每个数据库同步都会显示自己的结果通知 +- **检查 frontmatter**:验证所有 NotionID 字段都存在且正确(如 `NotionID-blog`、`NotionID-portfolio`) + +## 获取帮助 + 如果问题依然存在,你可以在 GitHub 上[提交一个 Issue](https://github.com/jxpeng98/obsidian-to-NotionNext/issues),并附上详细的错误信息和你的操作步骤,我会尽快帮助你。 你也可以通过 `Ctrl+Shift+I` (Windows/Linux) 或 `Cmd+Option+I` (Mac) 打开 Obsidian 的开发者工具,在控制台(Console)中查看是否有与 NotionNext 插件相关的错误日志,这对于定位问题非常有帮助。 + +### 提交 Bug 报告时应包含的信息 + +1. **错误消息**:复制通知或控制台中的确切错误消息 +2. **控制台日志**:包含开发者控制台中的相关日志(查找 `[AutoSync]`、`[Settings]` 或 `[Plugin]` 前缀) +3. **重现步骤**:描述问题发生时你正在做什么 +4. **配置信息**:说明你使用的数据库格式(NotionNext、普通或自定义) +5. **设置信息**:注明是否启用了自动同步以及配置的延迟时间 From fab3acdc21c172cc668800566ea3d68c9abaea36 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Fri, 31 Oct 2025 23:35:28 +0000 Subject: [PATCH 04/32] feat: add auto sync messages and database management labels in English, Japanese, and Chinese locales --- src/lang/locale/en.ts | 22 ++++++++++++++++++++++ src/lang/locale/ja.ts | 22 ++++++++++++++++++++++ src/lang/locale/zh.ts | 23 +++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 544ae03..6552294 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -85,5 +85,27 @@ export const en = { ExtraBlockUploaded: "Extra blocks uploaded", CheckConsole: "Check the console for more information \n opt+cmd+i/ctrl+shift+i", SettingsMigrated: "✨ Plugin settings updated! Auto sync feature added, check plugin settings", + AutoSyncNoNotionID: "⚠️ Auto sync skipped: This document has not been synced to Notion, please upload manually first", + AutoSyncMultipleSync: "🔄 Auto sync: Syncing to {count} database(s)...", + AutoSyncFailed: "Auto sync to {database} failed: {error}", + AutoSyncError: "Auto sync failed for {filename}: {error}", "reach-mobile-limit": "The number of blocks exceeds the limit of 100, please use the desktop plugin", + StartUpload: "Start upload {filename}", + AddNewDatabase: "Add New Database", + AddNewDatabaseDesc: "Add a new database configuration", + AddNewDatabaseTooltip: "Add New Database", + EditDatabase: "Edit Database", + Preview: "Preview", + DatabaseFormatLabel: "Database Format", + DatabaseFullNameLabel: "Database Full Name", + DatabaseAbbreviateNameLabel: "Database Abbreviate Name", + NotionAPILabel: "Notion API Key", + DatabaseIDLabel: "Database ID", + ToggleAPIKeyVisibility: "Toggle API Key Visibility", + CopyAPIKey: "Copy API Key", + APIKeyCopied: "API Key copied to clipboard", + ToggleDatabaseIDVisibility: "Toggle Database ID Visibility", + CopyDatabaseID: "Copy Database ID", + DatabaseIDCopied: "Database ID copied to clipboard", + AddNewDatabaseModal: "Add new database", } diff --git a/src/lang/locale/ja.ts b/src/lang/locale/ja.ts index 215b6bf..a5cec6a 100644 --- a/src/lang/locale/ja.ts +++ b/src/lang/locale/ja.ts @@ -77,5 +77,27 @@ export const ja = { ExtraBlockUploaded: "追加ブロックがアップロードされました", CheckConsole: "詳細情報を確認するには、コンソールを開いてください \n opt+cmd+i/ctrl+shift+i", SettingsMigrated: "✨ プラグイン設定が更新されました!自動同期機能が追加されました。設定を確認してください", + AutoSyncNoNotionID: "⚠️ 自動同期をスキップ:このドキュメントは Notion に同期されていません。まず手動でアップロードしてください", + AutoSyncMultipleSync: "🔄 自動同期:{count} 個のデータベースに同期中...", + AutoSyncFailed: "{database} への自動同期に失敗しました:{error}", + AutoSyncError: "{filename} の自動同期に失敗しました:{error}", "reach-mobile-limit": "ブロック数が100の制限を超えています。デスクトップ版プラグインを使用してください", + StartUpload: "アップロード開始 {filename}", + AddNewDatabase: "新しいデータベースを追加", + AddNewDatabaseDesc: "新しいデータベース構成を追加", + AddNewDatabaseTooltip: "新しいデータベースを追加", + EditDatabase: "データベースを編集", + Preview: "プレビュー", + DatabaseFormatLabel: "データベース形式", + DatabaseFullNameLabel: "データベースの全称", + DatabaseAbbreviateNameLabel: "データベースの略称", + NotionAPILabel: "Notion API キー", + DatabaseIDLabel: "データベース ID", + ToggleAPIKeyVisibility: "API キーの表示を切り替え", + CopyAPIKey: "API キーをコピー", + APIKeyCopied: "API キーをクリップボードにコピーしました", + ToggleDatabaseIDVisibility: "データベース ID の表示を切り替え", + CopyDatabaseID: "データベース ID をコピー", + DatabaseIDCopied: "データベース ID をクリップボードにコピーしました", + AddNewDatabaseModal: "新しいデータベースを追加", }; diff --git a/src/lang/locale/zh.ts b/src/lang/locale/zh.ts index b03296a..2cf418a 100644 --- a/src/lang/locale/zh.ts +++ b/src/lang/locale/zh.ts @@ -79,4 +79,27 @@ export const zh = { BlockUploaded: "所有内容已成功上传", ExtraBlockUploaded: "额外内容已成功上传", CheckConsole: "opt+cmd+i/ctrl+shift+i,\n打开控制台查看更多信息", + SettingsMigrated: "✨ 插件设置已更新!新增自动同步功能,请在设置中查看", + AutoSyncNoNotionID: "⚠️ 自动同步跳过:此文档未同步到 Notion,请先手动上传", + AutoSyncMultipleSync: "🔄 自动同步:正在同步到 {count} 个数据库...", + AutoSyncFailed: "自动同步到 {database} 失败:{error}", + AutoSyncError: "自动同步 {filename} 失败:{error}", + StartUpload: "开始上传 {filename}", + AddNewDatabase: "添加新数据库", + AddNewDatabaseDesc: "添加新的数据库配置", + AddNewDatabaseTooltip: "添加新数据库", + EditDatabase: "编辑数据库", + Preview: "预览", + DatabaseFormatLabel: "数据库格式", + DatabaseFullNameLabel: "数据库全称", + DatabaseAbbreviateNameLabel: "数据库简称", + NotionAPILabel: "Notion API 密钥", + DatabaseIDLabel: "数据库 ID", + ToggleAPIKeyVisibility: "切换 API 密钥可见性", + CopyAPIKey: "复制 API 密钥", + APIKeyCopied: "API 密钥已复制到剪贴板", + ToggleDatabaseIDVisibility: "切换数据库 ID 可见性", + CopyDatabaseID: "复制数据库 ID", + DatabaseIDCopied: "数据库 ID 已复制到剪贴板", + AddNewDatabaseModal: "添加新数据库", } From 7c841f3db3f0e3c7db01d038ab7387b85581a8fa Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Fri, 31 Oct 2025 23:36:49 +0000 Subject: [PATCH 05/32] feat: update UI text to use internationalization configuration for modals and notices --- src/ui/EditModal.ts | 6 +++--- src/ui/PreviewModal.ts | 24 ++++++++++++------------ src/ui/settingModal.ts | 2 +- src/ui/settingTabs.ts | 6 +++--- src/upload/uploadCommand.ts | 4 ++-- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/ui/EditModal.ts b/src/ui/EditModal.ts index 80f4a18..d26d520 100644 --- a/src/ui/EditModal.ts +++ b/src/ui/EditModal.ts @@ -93,7 +93,7 @@ export class EditModal extends SettingModal { display(): void { this.containerEl.addClass("edit-modal"); - this.titleEl.setText('Edit Database'); + this.titleEl.setText(i18nConfig.EditDatabase); let { contentEl } = this; contentEl.empty(); @@ -245,8 +245,8 @@ export class EditModal extends SettingModal { } new Setting(containerEl) - .setName("Add New Property") - .setDesc("Click to add a new property") + .setName(i18nConfig.AddNewProperty) + .setDesc(i18nConfig.AddNewPropertyDesc) .addButton(button => { return button .setButtonText('Add') diff --git a/src/ui/PreviewModal.ts b/src/ui/PreviewModal.ts index 9c2ad2c..5db99b1 100644 --- a/src/ui/PreviewModal.ts +++ b/src/ui/PreviewModal.ts @@ -19,7 +19,7 @@ export class PreviewModal extends Modal { display(): void { this.containerEl.addClass('preview-modal') - this.titleEl.setText('Preview') + this.titleEl.setText(i18nConfig.Preview) let { contentEl } = this; @@ -27,21 +27,21 @@ export class PreviewModal extends Modal { const dbFormatEl = new Setting(previewEl) dbFormatEl - .setName('Database Format') + .setName(i18nConfig.DatabaseFormatLabel) .addText(text => text .setValue(this.dbDetails.format) .setDisabled(true)); const dbFullEl = new Setting(previewEl) dbFullEl - .setName('Database Full Name') + .setName(i18nConfig.DatabaseFullNameLabel) .addText(text => text .setValue(this.dbDetails.fullName) .setDisabled(true)); const dbAbbrEl = new Setting(previewEl) dbAbbrEl - .setName('Database Abbreviate Name') + .setName(i18nConfig.DatabaseAbbreviateNameLabel) .addText(text => text .setValue(this.dbDetails.abName) .setDisabled(true)); @@ -49,12 +49,12 @@ export class PreviewModal extends Modal { // Setting for toggle and copy buttons new Setting(previewEl) - .setName('Notion API Key') + .setName(i18nConfig.NotionAPILabel) .addExtraButton((button: ExtraButtonComponent) => { let isApiKeyVisible = false; return button - .setTooltip('Toggle API Key Visibility') + .setTooltip(i18nConfig.ToggleAPIKeyVisibility) .setIcon('eye') .onClick(() => { isApiKeyVisible = !isApiKeyVisible; @@ -73,11 +73,11 @@ export class PreviewModal extends Modal { apiKeySetting .addExtraButton((button: ExtraButtonComponent) => { return button - .setTooltip('Copy API Key') + .setTooltip(i18nConfig.CopyAPIKey) .setIcon('clipboard') .onClick(() => { navigator.clipboard.writeText(this.dbDetails.notionAPI) - new Notice('API Key copied to clipboard'); + new Notice(i18nConfig.APIKeyCopied); }); }); @@ -86,12 +86,12 @@ export class PreviewModal extends Modal { new Setting(previewEl) - .setName('Database ID') + .setName(i18nConfig.DatabaseIDLabel) .addExtraButton((button: ExtraButtonComponent) => { let isDbIdVisible = false; return button - .setTooltip('Toggle Database ID Visibility') + .setTooltip(i18nConfig.ToggleDatabaseIDVisibility) .setIcon('eye') .onClick(() => { @@ -108,11 +108,11 @@ export class PreviewModal extends Modal { dbIdSetting .addExtraButton((button: ExtraButtonComponent) => { return button - .setTooltip('Copy Database ID') + .setTooltip(i18nConfig.CopyDatabaseID) .setIcon('clipboard') .onClick(() => { navigator.clipboard.writeText(this.dbDetails.databaseID) - new Notice('Database ID copied to clipboard'); + new Notice(i18nConfig.DatabaseIDCopied); }); }); diff --git a/src/ui/settingModal.ts b/src/ui/settingModal.ts index b34c73d..017bf6b 100644 --- a/src/ui/settingModal.ts +++ b/src/ui/settingModal.ts @@ -56,7 +56,7 @@ export class SettingModal extends Modal { display(): void { this.containerEl.addClass("settings-modal"); - this.titleEl.setText('Add new database'); + this.titleEl.setText(i18nConfig.AddNewDatabaseModal); // create the dropdown button to select the database format let { contentEl } = this; diff --git a/src/ui/settingTabs.ts b/src/ui/settingTabs.ts index 023479f..b97f28d 100644 --- a/src/ui/settingTabs.ts +++ b/src/ui/settingTabs.ts @@ -123,11 +123,11 @@ export class ObsidianSettingTab extends PluginSettingTab { // add new button new Setting(containerEl) - .setName("Add New Database") - .setDesc("Add New Database") + .setName(i18nConfig.AddNewDatabase) + .setDesc(i18nConfig.AddNewDatabaseDesc) .addButton((button: ButtonComponent): ButtonComponent => { return button - .setTooltip("Add New Database") + .setTooltip(i18nConfig.AddNewDatabaseTooltip) .setIcon("plus") .onClick(async () => { let modal = new SettingModal(this.app, this.plugin, this); diff --git a/src/upload/uploadCommand.ts b/src/upload/uploadCommand.ts index a4a1080..890672f 100644 --- a/src/upload/uploadCommand.ts +++ b/src/upload/uploadCommand.ts @@ -160,7 +160,7 @@ export async function uploadCommandGeneral( const {markDownData, nowFile, cover, tags} = await getNowFileMarkdownContentGeneral(app, settings) - new Notice(`Start upload ${nowFile.basename}`); + new Notice(i18nConfig.StartUpload.replace('{filename}', nowFile.basename)); console.log(`Start upload ${nowFile.basename}`); if (markDownData) { @@ -237,7 +237,7 @@ export async function uploadCommandCustom( const {markDownData, nowFile, cover, customValues} = await getNowFileMarkdownContentCustom(app, dbDetails) - new Notice(`Start upload ${nowFile.basename}`); + new Notice(i18nConfig.StartUpload.replace('{filename}', nowFile.basename)); console.log(`Start upload ${nowFile.basename}`); if (markDownData) { From 83d4f1e48cedbfcecf769ca06f5b8467a8e7df63 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Fri, 31 Oct 2025 23:36:57 +0000 Subject: [PATCH 06/32] feat: enhance auto sync functionality to support multiple databases and improved user notifications --- src/main.ts | 56 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/src/main.ts b/src/main.ts index ad8b61e..f0392f8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -226,41 +226,63 @@ export default class ObsidianSyncNotionPlugin extends Plugin { return; } - // Find which database this file belongs to by checking for NotionID-{abName} - let foundDbDetails: DatabaseDetails | null = null; - let notionId: string | null = null; + // Find all databases this file belongs to by checking for NotionID-{abName} + const foundDatabases: Array<{ dbDetails: DatabaseDetails, notionId: string }> = []; for (const key in this.settings.databaseDetails) { const dbDetails = this.settings.databaseDetails[key]; const notionIDKey = `NotionID-${dbDetails.abName}`; if (frontMatter[notionIDKey]) { - foundDbDetails = dbDetails; - notionId = String(frontMatter[notionIDKey]); - break; + foundDatabases.push({ + dbDetails: dbDetails, + notionId: String(frontMatter[notionIDKey]) + }); } } - // If no NotionID found, skip auto sync - if (!foundDbDetails || !notionId) { + // If no NotionID found, notify user to upload manually first + if (foundDatabases.length === 0) { console.log(`[AutoSync] No NotionID found in ${file.path}, skipping auto sync`); + new Notice(i18nConfig.AutoSyncNoNotionID, 4000); return; } - console.log(`[AutoSync] ${new Date().toISOString()} Auto syncing ${file.basename} to ${foundDbDetails.fullName}`); + // Notify user about multiple syncs if applicable + if (foundDatabases.length > 1) { + const message = i18nConfig.AutoSyncMultipleSync.replace('{count}', String(foundDatabases.length)); + new Notice(message, 3000); + console.log(`[AutoSync] Found ${foundDatabases.length} NotionIDs in ${file.path}`); + } - // Trigger appropriate upload command based on database format - if (foundDbDetails.format === 'next') { - await uploadCommandNext(this, this.settings, foundDbDetails, this.app); - } else if (foundDbDetails.format === 'general') { - await uploadCommandGeneral(this, this.settings, foundDbDetails, this.app); - } else if (foundDbDetails.format === 'custom') { - await uploadCommandCustom(this, this.settings, foundDbDetails, this.app); + // Sync to all found databases + for (const { dbDetails, notionId } of foundDatabases) { + console.log(`[AutoSync] ${new Date().toISOString()} Auto syncing ${file.basename} to ${dbDetails.fullName} (${dbDetails.abName})`); + + try { + // Trigger appropriate upload command based on database format + if (dbDetails.format === 'next') { + await uploadCommandNext(this, this.settings, dbDetails, this.app); + } else if (dbDetails.format === 'general') { + await uploadCommandGeneral(this, this.settings, dbDetails, this.app); + } else if (dbDetails.format === 'custom') { + await uploadCommandCustom(this, this.settings, dbDetails, this.app); + } + } catch (error) { + console.error(`[AutoSync] Error syncing to ${dbDetails.fullName}:`, error); + const message = i18nConfig.AutoSyncFailed + .replace('{database}', dbDetails.fullName) + .replace('{error}', error.message); + new Notice(message, 5000); + } } } catch (error) { console.error(`[AutoSync] Error syncing file ${file.path}:`, error); - new Notice(`Auto sync failed for ${file.basename}: ${error.message}`); + const message = i18nConfig.AutoSyncError + .replace('{filename}', file.basename) + .replace('{error}', error.message); + new Notice(message); } finally { this.syncingFiles.delete(file.path); } From d660a088e308405b1ee5d461e8aef927bfb9b976 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Fri, 31 Oct 2025 23:44:53 +0000 Subject: [PATCH 07/32] feat: add GitHub Actions workflow for prerelease builds and asset uploads --- .github/workflows/prerelease.yml | 113 +++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 .github/workflows/prerelease.yml diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml new file mode 100644 index 0000000..7fcb73e --- /dev/null +++ b/.github/workflows/prerelease.yml @@ -0,0 +1,113 @@ +name: Prerelease + +on: + push: + branches: + - dev + tags: + - "*-beta*" + - "*-rc*" + - "*-alpha*" + - "*-test*" + +env: + PLUGIN_NAME: share-to-notionnext + CHANGELOG_FILENAME: CHANGELOG.md + +jobs: + build: + name: prerelease + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: "18" + + - name: Build + id: build + run: | + npm install + npm run build + mkdir ${{ env.PLUGIN_NAME }} + cp main.js manifest.json ${{ env.PLUGIN_NAME }} + zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} + ls + echo "tag_name=$(git tag --sort version:refname | tail -n 1)" >> $GITHUB_OUTPUT + + - name: Create Prerelease + id: create_prerelease + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }} + VERSION: ${{ github.ref }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} (Prerelease) + body: | + ## ⚠️ Prerelease Version - For Testing Only + + This is a prerelease version intended for testing purposes. + + **Important Notes:** + - This version may contain bugs or experimental features + - Not recommended for production use + - Please report any issues you encounter + + ### Installation + + **Method 1: Using BRAT (Recommended)** + 1. Install [BRAT](https://github.com/TfTHacker/obsidian42-brat) plugin if you haven't already + 2. Open BRAT settings + 3. Click "Add Beta plugin" + 4. Enter: `jxpeng98/obsidian-to-NotionNext` + 5. Enable "Share to NotionNext" in Community Plugins + + **Method 2: Manual Installation** + 1. Download the `${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip` file + 2. Extract it to your `.obsidian/plugins/` folder + 3. Reload Obsidian + + ### Changelog + See [CHANGELOG.md](${{ env.CHANGELOG_FILENAME }}) for details. + + --- + **Feedback:** Please report issues in the [GitHub Issues](https://github.com/${{ github.repository }}/issues) section. + draft: false + prerelease: true + + - name: Upload zip file + id: upload-zip + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }} + with: + upload_url: ${{ steps.create_prerelease.outputs.upload_url }} + asset_path: ./${{ env.PLUGIN_NAME }}.zip + asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip + asset_content_type: application/zip + + - name: Upload main.js + id: upload-main + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }} + with: + upload_url: ${{ steps.create_prerelease.outputs.upload_url }} + asset_path: ./main.js + asset_name: main.js + asset_content_type: text/javascript + + - name: Upload manifest.json + id: upload-manifest + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }} + with: + upload_url: ${{ steps.create_prerelease.outputs.upload_url }} + asset_path: ./manifest.json + asset_name: manifest.json + asset_content_type: application/json From 6df5e21f3d35ade95f8cbe8e8e0c123694565c66 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Sat, 1 Nov 2025 00:30:09 +0000 Subject: [PATCH 08/32] feat: implement enhanced frontmatter change detection and caching for auto sync --- src/main.ts | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/src/main.ts b/src/main.ts index f0392f8..a49f5a7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,6 +15,8 @@ export default class ObsidianSyncNotionPlugin extends Plugin { modifyEventRef: EventRef | null = null; autoSyncTimeout: NodeJS.Timeout | null = null; private syncingFiles: Set = new Set(); + private lastFrontmatterCache: Map = new Map(); + private lastContentHashCache: Map = new Map(); async onload() { await this.loadSettings(); @@ -63,6 +65,8 @@ export default class ObsidianSyncNotionPlugin extends Plugin { if (this.autoSyncTimeout) { clearTimeout(this.autoSyncTimeout); } + this.lastFrontmatterCache.clear(); + this.lastContentHashCache.clear(); } async loadSettings() { @@ -209,6 +213,65 @@ export default class ObsidianSyncNotionPlugin extends Plugin { }); } + onlyNotionIDChanged(oldFrontmatter: any, newFrontmatter: any): boolean { + // Get all keys from both frontmatters + const oldKeys = Object.keys(oldFrontmatter || {}); + const newKeys = Object.keys(newFrontmatter || {}); + + // Filter out NotionID-related keys and Obsidian internal keys + const isIgnoredKey = (key: string) => { + return key.startsWith('NotionID-') || + key === 'NotionID' || + key === 'position'; // Obsidian's internal metadata + }; + const oldNonNotionKeys = oldKeys.filter(k => !isIgnoredKey(k)).sort(); + const newNonNotionKeys = newKeys.filter(k => !isIgnoredKey(k)).sort(); + + // If number of non-NotionID keys changed, something else changed + if (oldNonNotionKeys.length !== newNonNotionKeys.length) { + console.log('[AutoSync] Frontmatter: Key count changed:', oldNonNotionKeys.length, '->', newNonNotionKeys.length); + return false; + } + + // Check if any non-NotionID key values changed + for (const key of oldNonNotionKeys) { + if (!newNonNotionKeys.includes(key)) { + console.log('[AutoSync] Frontmatter: Key removed or added:', key); + return false; // Key was removed or added + } + // Deep comparison for the value + const oldValue = JSON.stringify(oldFrontmatter[key]); + const newValue = JSON.stringify(newFrontmatter[key]); + if (oldValue !== newValue) { + console.log('[AutoSync] Frontmatter: Value changed for key "' + key + '"'); + console.log(' Old:', oldValue.substring(0, 100)); + console.log(' New:', newValue.substring(0, 100)); + return false; // Value changed + } + } + + // Check if any new non-NotionID keys were added + for (const key of newNonNotionKeys) { + if (!oldNonNotionKeys.includes(key)) { + console.log('[AutoSync] Frontmatter: New key added:', key); + return false; // New key was added + } + } + + // Only NotionID fields changed (or nothing in frontmatter changed) + return true; + } + + simpleHash(str: string): string { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32bit integer + } + return hash.toString(); + } + async autoSyncFile(file: TFile) { // Check if file is already being synced if (this.syncingFiles.has(file.path)) { @@ -226,6 +289,44 @@ export default class ObsidianSyncNotionPlugin extends Plugin { return; } + // Get file content hash for comparison + const content = await this.app.vault.read(file); + const contentHash = this.simpleHash(content); + const lastContentHash = this.lastContentHashCache.get(file.path); + + // Check if only NotionID fields changed (to avoid sync loops) + const lastFrontmatter = this.lastFrontmatterCache.get(file.path); + + if (lastFrontmatter && lastContentHash) { + const frontmatterOnlyNotionIDChanged = this.onlyNotionIDChanged(lastFrontmatter, frontMatter); + const contentUnchanged = contentHash === lastContentHash; + + console.log(`[AutoSync] Change analysis for ${file.basename}:`, { + frontmatterOnlyNotionIDChanged, + contentUnchanged, + frontmatterHasRealChanges: !frontmatterOnlyNotionIDChanged, + contentChanged: !contentUnchanged, + willSync: !(frontmatterOnlyNotionIDChanged && contentUnchanged) + }); + + // Only skip sync if BOTH conditions are true: + // 1. Frontmatter only has NotionID changes (no real user changes) + // 2. Content is completely unchanged + if (frontmatterOnlyNotionIDChanged && contentUnchanged) { + console.log(`[AutoSync] Only NotionID updated (from sync), content unchanged - skipping auto sync`); + // Update cache even when skipping, so next comparison uses the current state + this.lastFrontmatterCache.set(file.path, { ...frontMatter }); + this.lastContentHashCache.set(file.path, contentHash); + return; + } + + if (!contentUnchanged) { + console.log(`[AutoSync] Content changed - will sync`); + } else if (!frontmatterOnlyNotionIDChanged) { + console.log(`[AutoSync] Frontmatter changed - will sync`); + } + } + // Find all databases this file belongs to by checking for NotionID-{abName} const foundDatabases: Array<{ dbDetails: DatabaseDetails, notionId: string }> = []; @@ -277,6 +378,20 @@ export default class ObsidianSyncNotionPlugin extends Plugin { } } + // After sync completes, update cache with the latest frontmatter (including updated NotionIDs) + // Wait a bit for metadata cache to update + setTimeout(async () => { + const updatedFrontmatter = this.app.metadataCache.getFileCache(file)?.frontmatter; + const updatedContent = await this.app.vault.read(file); + const updatedHash = this.simpleHash(updatedContent); + + if (updatedFrontmatter) { + this.lastFrontmatterCache.set(file.path, { ...updatedFrontmatter }); + this.lastContentHashCache.set(file.path, updatedHash); + console.log(`[AutoSync] Cached updated frontmatter and content hash for ${file.path}`); + } + }, 500); + } catch (error) { console.error(`[AutoSync] Error syncing file ${file.path}:`, error); const message = i18nConfig.AutoSyncError From 2738498dc2243ee1e7f6e973b7dc8feffe4444b9 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Sat, 1 Nov 2025 00:30:39 +0000 Subject: [PATCH 09/32] fix: clean up formatting in auto sync logic for better readability --- src/main.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main.ts b/src/main.ts index a49f5a7..134e2d7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -220,9 +220,9 @@ export default class ObsidianSyncNotionPlugin extends Plugin { // Filter out NotionID-related keys and Obsidian internal keys const isIgnoredKey = (key: string) => { - return key.startsWith('NotionID-') || - key === 'NotionID' || - key === 'position'; // Obsidian's internal metadata + return key.startsWith('NotionID-') || + key === 'NotionID' || + key === 'position'; // Obsidian's internal metadata }; const oldNonNotionKeys = oldKeys.filter(k => !isIgnoredKey(k)).sort(); const newNonNotionKeys = newKeys.filter(k => !isIgnoredKey(k)).sort(); @@ -296,11 +296,11 @@ export default class ObsidianSyncNotionPlugin extends Plugin { // Check if only NotionID fields changed (to avoid sync loops) const lastFrontmatter = this.lastFrontmatterCache.get(file.path); - + if (lastFrontmatter && lastContentHash) { const frontmatterOnlyNotionIDChanged = this.onlyNotionIDChanged(lastFrontmatter, frontMatter); const contentUnchanged = contentHash === lastContentHash; - + console.log(`[AutoSync] Change analysis for ${file.basename}:`, { frontmatterOnlyNotionIDChanged, contentUnchanged, @@ -308,7 +308,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { contentChanged: !contentUnchanged, willSync: !(frontmatterOnlyNotionIDChanged && contentUnchanged) }); - + // Only skip sync if BOTH conditions are true: // 1. Frontmatter only has NotionID changes (no real user changes) // 2. Content is completely unchanged @@ -319,7 +319,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { this.lastContentHashCache.set(file.path, contentHash); return; } - + if (!contentUnchanged) { console.log(`[AutoSync] Content changed - will sync`); } else if (!frontmatterOnlyNotionIDChanged) { @@ -384,7 +384,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { const updatedFrontmatter = this.app.metadataCache.getFileCache(file)?.frontmatter; const updatedContent = await this.app.vault.read(file); const updatedHash = this.simpleHash(updatedContent); - + if (updatedFrontmatter) { this.lastFrontmatterCache.set(file.path, { ...updatedFrontmatter }); this.lastContentHashCache.set(file.path, updatedHash); From e2a965e3d6c29e4b8bcf460995c45ff1444c16fe Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Sat, 1 Nov 2025 00:32:35 +0000 Subject: [PATCH 10/32] fix: update autoSyncTimeout type and ensure window context for setTimeout calls --- src/main.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.ts b/src/main.ts index 134e2d7..3d34a81 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,7 +13,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { commands: ribbonCommands; app: App; modifyEventRef: EventRef | null = null; - autoSyncTimeout: NodeJS.Timeout | null = null; + autoSyncTimeout: number | null = null; private syncingFiles: Set = new Set(); private lastFrontmatterCache: Map = new Map(); private lastContentHashCache: Map = new Map(); @@ -207,7 +207,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { // Set a new timeout to trigger sync after user-configured delay (in seconds) const delayMs = (this.settings.autoSyncDelay || 2) * 1000; - this.autoSyncTimeout = setTimeout(async () => { + this.autoSyncTimeout = window.setTimeout(async () => { await this.autoSyncFile(file); }, delayMs); }); @@ -380,7 +380,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { // After sync completes, update cache with the latest frontmatter (including updated NotionIDs) // Wait a bit for metadata cache to update - setTimeout(async () => { + window.setTimeout(async () => { const updatedFrontmatter = this.app.metadataCache.getFileCache(file)?.frontmatter; const updatedContent = await this.app.vault.read(file); const updatedHash = this.simpleHash(updatedContent); From 3730b7645ae8a9d9befd917c1ef001b6d339679e Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Sat, 1 Nov 2025 00:34:23 +0000 Subject: [PATCH 11/32] chore: update changelog with detailed auto sync feature description and improvements --- CHANGELOG.md | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e73bff..63058fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,26 @@ -## Feature +# Changelog -- Better debugging information for upload failures. -- Support synchronising Obsidian callouts as Notion callout blocks. -- 优化控制台输出的调试信息。 -- 支持将 Obsidian Callout 同步为 Notion Callout 区块。 +## [Unreleased] + +### Added + +- **Auto Sync Feature**: Automatically sync notes to Notion when content or frontmatter changes + - Configurable delay (default: 5 seconds, minimum: 2 seconds) + - Support for multiple database syncing + - Smart detection to avoid sync loops when only NotionID is updated + - Content hash comparison to detect body text changes + - Works on both desktop and mobile platforms +- Added comprehensive i18n support for all UI elements and notifications +- Added prerelease workflow for beta testing via GitHub Actions and BRAT + +### Changed + +- Enhanced settings tab with auto-sync configuration options +- Improved debug logging for better troubleshooting +- Updated documentation with auto-sync usage guide and troubleshooting section + +### Fixed + +- Fixed mobile compatibility issues by using `window.setTimeout` instead of `NodeJS.Timeout` +- Fixed sync loop prevention logic to properly handle frontmatter and content changes +- Fixed cache update timing to ensure accurate change detection From bb211543f5953ed2e8a80b98368cba4388ad97a2 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Sat, 1 Nov 2025 00:38:25 +0000 Subject: [PATCH 12/32] fix: remove branch trigger for prerelease workflow --- .github/workflows/prerelease.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 7fcb73e..9fbac84 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -2,8 +2,6 @@ name: Prerelease on: push: - branches: - - dev tags: - "*-beta*" - "*-rc*" From e8a9594ea14cc55fe283b397725710adc66101eb Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Sat, 1 Nov 2025 00:42:01 +0000 Subject: [PATCH 13/32] fix: refine tag patterns for release triggers in workflow --- .github/workflows/release.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 975604f..5132112 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,8 +2,11 @@ name: Release on: push: + branches: + - main tags: - - "*" + - "v*.*.*" + - "[0-9]+.[0-9]+.[0-9]+" env: PLUGIN_NAME: share-to-notionnext # Change this to match the id of your plugin. From 7661bc94c743fad83c1cbd5e553cfc84eb7b6ce1 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Sat, 1 Nov 2025 00:44:35 +0000 Subject: [PATCH 14/32] fix: update changelog to reflect version v2.8.0-beta.1 release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63058fc..cfd48fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## v2.8.0-beta.1 (2025-10-31) ### Added From ae4488546c0892ad5e8820060a8129c194cf6462 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Thu, 6 Nov 2025 16:27:24 +0000 Subject: [PATCH 15/32] feat: add auto sync database list handling and notifications for missing entries --- src/lang/locale/en.ts | 1 + src/lang/locale/ja.ts | 1 + src/lang/locale/zh.ts | 1 + src/main.ts | 41 +++++++++++++++++++++++++++---- src/upload/updateYaml.ts | 7 ++++++ src/utils/frontmatter.ts | 52 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 src/utils/frontmatter.ts diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 6552294..a8fc216 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -86,6 +86,7 @@ export const en = { CheckConsole: "Check the console for more information \n opt+cmd+i/ctrl+shift+i", SettingsMigrated: "✨ Plugin settings updated! Auto sync feature added, check plugin settings", AutoSyncNoNotionID: "⚠️ Auto sync skipped: This document has not been synced to Notion, please upload manually first", + AutoSyncMissingDatabaseList: "⚠️ Auto sync skipped: Add an \"aytosync-database\" list in the frontmatter to choose databases", AutoSyncMultipleSync: "🔄 Auto sync: Syncing to {count} database(s)...", AutoSyncFailed: "Auto sync to {database} failed: {error}", AutoSyncError: "Auto sync failed for {filename}: {error}", diff --git a/src/lang/locale/ja.ts b/src/lang/locale/ja.ts index a5cec6a..c0594da 100644 --- a/src/lang/locale/ja.ts +++ b/src/lang/locale/ja.ts @@ -78,6 +78,7 @@ export const ja = { CheckConsole: "詳細情報を確認するには、コンソールを開いてください \n opt+cmd+i/ctrl+shift+i", SettingsMigrated: "✨ プラグイン設定が更新されました!自動同期機能が追加されました。設定を確認してください", AutoSyncNoNotionID: "⚠️ 自動同期をスキップ:このドキュメントは Notion に同期されていません。まず手動でアップロードしてください", + AutoSyncMissingDatabaseList: "⚠️ 自動同期をスキップ:frontmatter に aytosync-database リストを設定してください", AutoSyncMultipleSync: "🔄 自動同期:{count} 個のデータベースに同期中...", AutoSyncFailed: "{database} への自動同期に失敗しました:{error}", AutoSyncError: "{filename} の自動同期に失敗しました:{error}", diff --git a/src/lang/locale/zh.ts b/src/lang/locale/zh.ts index 2cf418a..1bb21dc 100644 --- a/src/lang/locale/zh.ts +++ b/src/lang/locale/zh.ts @@ -81,6 +81,7 @@ export const zh = { CheckConsole: "opt+cmd+i/ctrl+shift+i,\n打开控制台查看更多信息", SettingsMigrated: "✨ 插件设置已更新!新增自动同步功能,请在设置中查看", AutoSyncNoNotionID: "⚠️ 自动同步跳过:此文档未同步到 Notion,请先手动上传", + AutoSyncMissingDatabaseList: "⚠️ 自动同步跳过:请在 frontmatter 中设置 aytosync-database 列表", AutoSyncMultipleSync: "🔄 自动同步:正在同步到 {count} 个数据库...", AutoSyncFailed: "自动同步到 {database} 失败:{error}", AutoSyncError: "自动同步 {filename} 失败:{error}", diff --git a/src/main.ts b/src/main.ts index 3d34a81..83dfc8a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import { i18nConfig } from "src/lang/I18n"; import ribbonCommands from "src/commands/NotionCommands"; import { ObsidianSettingTab, PluginSettings, DEFAULT_SETTINGS, DatabaseDetails } from "src/ui/settingTabs"; import { uploadCommandNext, uploadCommandGeneral, uploadCommandCustom } from "src/upload/uploadCommand"; +import { AUTO_SYNC_DATABASE_KEY, parseAutoSyncDatabaseList } from "src/utils/frontmatter"; // Remember to rename these classes and interfaces! @@ -17,6 +18,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { private syncingFiles: Set = new Set(); private lastFrontmatterCache: Map = new Map(); private lastContentHashCache: Map = new Map(); + private missingAutoSyncNoticeShown: Set = new Set(); async onload() { await this.loadSettings(); @@ -67,6 +69,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { } this.lastFrontmatterCache.clear(); this.lastContentHashCache.clear(); + this.missingAutoSyncNoticeShown.clear(); } async loadSettings() { @@ -327,21 +330,50 @@ export default class ObsidianSyncNotionPlugin extends Plugin { } } - // Find all databases this file belongs to by checking for NotionID-{abName} - const foundDatabases: Array<{ dbDetails: DatabaseDetails, notionId: string }> = []; + const autoSyncTargets = parseAutoSyncDatabaseList(frontMatter[AUTO_SYNC_DATABASE_KEY]); + if (autoSyncTargets.length === 0) { + if (!this.missingAutoSyncNoticeShown.has(file.path)) { + new Notice(i18nConfig.AutoSyncMissingDatabaseList, 10000); + this.missingAutoSyncNoticeShown.add(file.path); + } + console.log(`[AutoSync] No auto sync database list found in ${file.path}`); + return; + } + + this.missingAutoSyncNoticeShown.delete(file.path); + + const dbByShortName = new Map(); for (const key in this.settings.databaseDetails) { const dbDetails = this.settings.databaseDetails[key]; - const notionIDKey = `NotionID-${dbDetails.abName}`; + dbByShortName.set(dbDetails.abName.toLowerCase(), dbDetails); + } + const foundDatabases: Array<{ dbDetails: DatabaseDetails, notionId: string }> = []; + const unresolvedTargets: string[] = []; + + for (const target of autoSyncTargets) { + const lookupKey = target.toLowerCase(); + const dbDetails = dbByShortName.get(lookupKey); + + if (!dbDetails) { + unresolvedTargets.push(target); + continue; + } + + const notionIDKey = `NotionID-${dbDetails.abName}`; if (frontMatter[notionIDKey]) { foundDatabases.push({ - dbDetails: dbDetails, + dbDetails, notionId: String(frontMatter[notionIDKey]) }); } } + if (unresolvedTargets.length > 0) { + console.log(`[AutoSync] Frontmatter auto sync targets not found in settings: ${unresolvedTargets.join(", ")}`); + } + // If no NotionID found, notify user to upload manually first if (foundDatabases.length === 0) { console.log(`[AutoSync] No NotionID found in ${file.path}, skipping auto sync`); @@ -403,4 +435,3 @@ export default class ObsidianSyncNotionPlugin extends Plugin { } } } - diff --git a/src/upload/updateYaml.ts b/src/upload/updateYaml.ts index be5ab01..3bdafb3 100644 --- a/src/upload/updateYaml.ts +++ b/src/upload/updateYaml.ts @@ -2,6 +2,7 @@ import { App, Notice, TFile } from "obsidian"; import ObsidianSyncNotionPlugin from "../main"; import { DatabaseDetails } from "../ui/settingTabs"; import { i18nConfig } from "src/lang/I18n"; +import { AUTO_SYNC_DATABASE_KEY, ensureAutoSyncDatabaseEntry } from "src/utils/frontmatter"; export async function updateYamlInfo( yamlContent: string, @@ -33,6 +34,12 @@ export async function updateYamlInfo( // add new notionID and link yamlContent[notionIDKey] = id; (NotionLinkDisplay) ? yamlContent[linkKey] = url : null; + + // ensure auto sync database list contains current short name + yamlContent[AUTO_SYNC_DATABASE_KEY] = ensureAutoSyncDatabaseEntry( + yamlContent[AUTO_SYNC_DATABASE_KEY], + abName + ); }); try { diff --git a/src/utils/frontmatter.ts b/src/utils/frontmatter.ts new file mode 100644 index 0000000..bb8b2b7 --- /dev/null +++ b/src/utils/frontmatter.ts @@ -0,0 +1,52 @@ +export const AUTO_SYNC_DATABASE_KEY = "aytosync-database"; + +function toCandidateList(value: unknown): string[] { + if (Array.isArray(value)) { + return value.map(item => String(item ?? "").trim()); + } + + if (typeof value === "string") { + if (value.includes(",")) { + return value.split(",").map(item => item.trim()); + } + return [value.trim()]; + } + + return []; +} + +export function parseAutoSyncDatabaseList(value: unknown): string[] { + const candidates = toCandidateList(value) + .map(name => name.replace(/^\[|\]$/g, "").trim()) // strip stray brackets + .filter(Boolean); + + const seen = new Map(); + for (const name of candidates) { + const key = name.toLowerCase(); + if (!seen.has(key)) { + seen.set(key, name); + } + } + + return Array.from(seen.values()); +} + +export function ensureAutoSyncDatabaseEntry(value: unknown, abName: string): string[] { + const current = parseAutoSyncDatabaseList(value); + const lower = abName.toLowerCase(); + + let contains = false; + const updated = current.map(name => { + if (name.toLowerCase() === lower) { + contains = true; + return abName; + } + return name; + }); + + if (!contains) { + updated.push(abName); + } + + return updated; +} From bb4b75c82e254b7790a9e388d430be62449e4503 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Thu, 6 Nov 2025 16:43:24 +0000 Subject: [PATCH 16/32] feat: add auto sync frontmatter key configuration and update related messages --- src/lang/locale/en.ts | 4 +++- src/lang/locale/ja.ts | 26 ++++++++++++++------------ src/lang/locale/zh.ts | 4 +++- src/main.ts | 32 +++++++++++++++++++++++++++----- src/ui/settingTabs.ts | 18 +++++++++++++++++- src/upload/updateYaml.ts | 7 ++++--- src/utils/frontmatter.ts | 13 ++++++++++++- 7 files changed, 80 insertions(+), 24 deletions(-) diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index a8fc216..b3b1e50 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -37,6 +37,8 @@ export const en = { NotionLinkDisplayDesc: "Default is ON, if you want to hide the link in the front matter, please turn it off", AutoSync: "Auto Sync", AutoSyncDesc: "Automatically sync to Notion when frontmatter or content is modified (requires existing NotionID)", + AutoSyncFrontmatterKey: "Auto Sync Frontmatter Key", + AutoSyncFrontmatterKeyDesc: "Set the frontmatter property name that lists which databases should auto sync (defaults to autosync-database).", AutoSyncDelay: "Auto Sync Delay (seconds)", AutoSyncDelayDesc: "How many seconds to wait after document modification before triggering auto sync (default: 5 seconds, minimum: 2 seconds)", AutoSyncDelayText: "Enter delay in seconds", @@ -86,7 +88,7 @@ export const en = { CheckConsole: "Check the console for more information \n opt+cmd+i/ctrl+shift+i", SettingsMigrated: "✨ Plugin settings updated! Auto sync feature added, check plugin settings", AutoSyncNoNotionID: "⚠️ Auto sync skipped: This document has not been synced to Notion, please upload manually first", - AutoSyncMissingDatabaseList: "⚠️ Auto sync skipped: Add an \"aytosync-database\" list in the frontmatter to choose databases", + AutoSyncMissingDatabaseList: "⚠️ Auto sync skipped: Add a \"{key}\" entry in the frontmatter to choose databases", AutoSyncMultipleSync: "🔄 Auto sync: Syncing to {count} database(s)...", AutoSyncFailed: "Auto sync to {database} failed: {error}", AutoSyncError: "Auto sync failed for {filename}: {error}", diff --git a/src/lang/locale/ja.ts b/src/lang/locale/ja.ts index c0594da..c25a2f4 100644 --- a/src/lang/locale/ja.ts +++ b/src/lang/locale/ja.ts @@ -31,13 +31,15 @@ export const ja = { NotionUser: "Notion ID(ユーザー名、任意)", NotionUserDesc: "共有リンクから取得(例:https://username.notion.site)。Notion IDは[username]です", NotionUserText: "Notion IDを入力", - NotionLinkDisplay: "Notionリンク表示", - NotionLinkDisplayDesc: "デフォルトはONです。front matterにリンクを非表示にしたい場合は、オフにしてください", - AutoSync: "自動同期", - AutoSyncDesc: "frontmatter またはコンテンツが変更されたときに自動的に Notion に同期します(NotionID が必要)", - AutoSyncDelay: "自動同期遅延時間(秒)", - AutoSyncDelayDesc: "ドキュメントの変更後、自動同期をトリガーするまでの待機時間(デフォルト:5秒、最小:2秒)", - AutoSyncDelayText: "遅延秒数を入力", + NotionLinkDisplay: "Notionリンク表示", + NotionLinkDisplayDesc: "デフォルトはONです。front matterにリンクを非表示にしたい場合は、オフにしてください", + AutoSync: "自動同期", + AutoSyncDesc: "frontmatter またはコンテンツが変更されたときに自動的に Notion に同期します(NotionID が必要)", + AutoSyncFrontmatterKey: "自動同期 frontmatter キー", + AutoSyncFrontmatterKeyDesc: "自動同期するデータベースを列挙する frontmatter のプロパティ名を設定します(デフォルトは autosync-database)。", + AutoSyncDelay: "自動同期遅延時間(秒)", + AutoSyncDelayDesc: "ドキュメントの変更後、自動同期をトリガーするまでの待機時間(デフォルト:5秒、最小:2秒)", + AutoSyncDelayText: "遅延秒数を入力", NotionGeneralSettingHeader: "一般的なNotionデータベース設定", NotionGeneralButton: "一般的なNotion同期", NotionGeneralButtonDesc: "このオプションを開くと、一般的なNotionデータベース同期コマンドがコマンドパレットに表示されます(デフォルト:ON)", @@ -75,11 +77,11 @@ export const ja = { CopyErrorMessage: "自動コピーに失敗しました", BlockUploaded: "ブロックがアップロードされました", ExtraBlockUploaded: "追加ブロックがアップロードされました", - CheckConsole: "詳細情報を確認するには、コンソールを開いてください \n opt+cmd+i/ctrl+shift+i", - SettingsMigrated: "✨ プラグイン設定が更新されました!自動同期機能が追加されました。設定を確認してください", - AutoSyncNoNotionID: "⚠️ 自動同期をスキップ:このドキュメントは Notion に同期されていません。まず手動でアップロードしてください", - AutoSyncMissingDatabaseList: "⚠️ 自動同期をスキップ:frontmatter に aytosync-database リストを設定してください", - AutoSyncMultipleSync: "🔄 自動同期:{count} 個のデータベースに同期中...", + CheckConsole: "詳細情報を確認するには、コンソールを開いてください \n opt+cmd+i/ctrl+shift+i", + SettingsMigrated: "✨ プラグイン設定が更新されました!自動同期機能が追加されました。設定を確認してください", + AutoSyncNoNotionID: "⚠️ 自動同期をスキップ:このドキュメントは Notion に同期されていません。まず手動でアップロードしてください", + AutoSyncMissingDatabaseList: "⚠️ 自動同期をスキップ:frontmatter に \"{key}\" 項目を追加してください", + AutoSyncMultipleSync: "🔄 自動同期:{count} 個のデータベースに同期中...", AutoSyncFailed: "{database} への自動同期に失敗しました:{error}", AutoSyncError: "{filename} の自動同期に失敗しました:{error}", "reach-mobile-limit": "ブロック数が100の制限を超えています。デスクトップ版プラグインを使用してください", diff --git a/src/lang/locale/zh.ts b/src/lang/locale/zh.ts index 1bb21dc..e3caa5c 100644 --- a/src/lang/locale/zh.ts +++ b/src/lang/locale/zh.ts @@ -37,6 +37,8 @@ export const zh = { NotionLinkDisplayDesc: "默认开启,如果你不想在front matter中显示链接,请关闭", AutoSync: "自动同步", AutoSyncDesc: "当检测到文档的 frontmatter 或内容发生修改时,自动同步到 Notion(需要文档已有 NotionID)", + AutoSyncFrontmatterKey: "自动同步 Frontmatter 键名", + AutoSyncFrontmatterKeyDesc: "设置用于列出自动同步数据库的 frontmatter 属性名称(默认 autosync-database)。", AutoSyncDelay: "自动同步延迟时间(秒)", AutoSyncDelayDesc: "文档修改后等待多少秒才触发自动同步,避免频繁同步(默认:5秒,最小:2秒)", AutoSyncDelayText: "输入延迟秒数", @@ -81,7 +83,7 @@ export const zh = { CheckConsole: "opt+cmd+i/ctrl+shift+i,\n打开控制台查看更多信息", SettingsMigrated: "✨ 插件设置已更新!新增自动同步功能,请在设置中查看", AutoSyncNoNotionID: "⚠️ 自动同步跳过:此文档未同步到 Notion,请先手动上传", - AutoSyncMissingDatabaseList: "⚠️ 自动同步跳过:请在 frontmatter 中设置 aytosync-database 列表", + AutoSyncMissingDatabaseList: "⚠️ 自动同步跳过:请在 frontmatter 中添加 \"{key}\" 项来指定数据库", AutoSyncMultipleSync: "🔄 自动同步:正在同步到 {count} 个数据库...", AutoSyncFailed: "自动同步到 {database} 失败:{error}", AutoSyncError: "自动同步 {filename} 失败:{error}", diff --git a/src/main.ts b/src/main.ts index 83dfc8a..4d66e07 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,7 @@ import { i18nConfig } from "src/lang/I18n"; import ribbonCommands from "src/commands/NotionCommands"; import { ObsidianSettingTab, PluginSettings, DEFAULT_SETTINGS, DatabaseDetails } from "src/ui/settingTabs"; import { uploadCommandNext, uploadCommandGeneral, uploadCommandCustom } from "src/upload/uploadCommand"; -import { AUTO_SYNC_DATABASE_KEY, parseAutoSyncDatabaseList } from "src/utils/frontmatter"; +import { DEFAULT_AUTO_SYNC_DATABASE_KEY, parseAutoSyncDatabaseList, resolveAutoSyncKey } from "src/utils/frontmatter"; // Remember to rename these classes and interfaces! @@ -92,6 +92,10 @@ export default class ObsidianSyncNotionPlugin extends Plugin { if (typeof this.settings.NotionLinkDisplay !== 'boolean') { this.settings.NotionLinkDisplay = DEFAULT_SETTINGS.NotionLinkDisplay; } + if (typeof this.settings.autoSyncFrontmatterKey !== 'string') { + this.settings.autoSyncFrontmatterKey = DEFAULT_AUTO_SYNC_DATABASE_KEY; + } + this.settings.autoSyncFrontmatterKey = resolveAutoSyncKey(this.settings.autoSyncFrontmatterKey); // Ensure databaseDetails exists if (!this.settings.databaseDetails || typeof this.settings.databaseDetails !== 'object') { @@ -102,7 +106,8 @@ export default class ObsidianSyncNotionPlugin extends Plugin { const needsSave = !loadedData || loadedData.autoSync === undefined || loadedData.autoSyncDelay === undefined || - loadedData.NotionLinkDisplay === undefined; + loadedData.NotionLinkDisplay === undefined || + loadedData.autoSyncFrontmatterKey === undefined; if (needsSave) { const migratedFields = []; @@ -134,6 +139,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { autoSync: this.settings.autoSync, autoSyncDelay: this.settings.autoSyncDelay, NotionLinkDisplay: this.settings.NotionLinkDisplay, + autoSyncFrontmatterKey: this.settings.autoSyncFrontmatterKey, databaseCount: Object.keys(this.settings.databaseDetails || {}).length }); } @@ -156,6 +162,20 @@ export default class ObsidianSyncNotionPlugin extends Plugin { console.warn('[Settings] Invalid databaseDetails, resetting to empty object'); this.settings.databaseDetails = {}; } + if (typeof this.settings.autoSyncFrontmatterKey !== 'string' || this.settings.autoSyncFrontmatterKey.trim().length === 0) { + console.warn('[Settings] Invalid autoSyncFrontmatterKey, resetting to default'); + this.settings.autoSyncFrontmatterKey = DEFAULT_AUTO_SYNC_DATABASE_KEY; + } else { + this.settings.autoSyncFrontmatterKey = resolveAutoSyncKey(this.settings.autoSyncFrontmatterKey); + } + } + + resetAutoSyncNoticeCache() { + this.missingAutoSyncNoticeShown.clear(); + } + + getAutoSyncFrontmatterKey(): string { + return resolveAutoSyncKey(this.settings.autoSyncFrontmatterKey); } async addDatabaseDetails(dbDetails: DatabaseDetails) { @@ -330,14 +350,16 @@ export default class ObsidianSyncNotionPlugin extends Plugin { } } - const autoSyncTargets = parseAutoSyncDatabaseList(frontMatter[AUTO_SYNC_DATABASE_KEY]); + const autoSyncKey = this.getAutoSyncFrontmatterKey(); + const autoSyncTargets = parseAutoSyncDatabaseList(frontMatter[autoSyncKey]); if (autoSyncTargets.length === 0) { if (!this.missingAutoSyncNoticeShown.has(file.path)) { - new Notice(i18nConfig.AutoSyncMissingDatabaseList, 10000); + const message = i18nConfig.AutoSyncMissingDatabaseList.replace('{key}', autoSyncKey); + new Notice(message, 10000); this.missingAutoSyncNoticeShown.add(file.path); } - console.log(`[AutoSync] No auto sync database list found in ${file.path}`); + console.log(`[AutoSync] No auto sync database list found in ${file.path} using key "${autoSyncKey}"`); return; } diff --git a/src/ui/settingTabs.ts b/src/ui/settingTabs.ts index b97f28d..0cb5975 100644 --- a/src/ui/settingTabs.ts +++ b/src/ui/settingTabs.ts @@ -5,6 +5,7 @@ import { SettingModal } from "./settingModal"; import { PreviewModal } from "./PreviewModal"; import { EditModal } from "./EditModal"; import { DeleteModal } from "./DeleteModal"; +import { DEFAULT_AUTO_SYNC_DATABASE_KEY } from "src/utils/frontmatter"; export interface PluginSettings { NextButton: boolean; @@ -15,6 +16,7 @@ export interface PluginSettings { NotionLinkDisplay: boolean; autoSync: boolean; autoSyncDelay: number; + autoSyncFrontmatterKey: string; proxy: string; GeneralButton: boolean; tagButton: boolean; @@ -53,6 +55,7 @@ export const DEFAULT_SETTINGS: PluginSettings = { NotionLinkDisplay: true, autoSync: false, autoSyncDelay: 5, + autoSyncFrontmatterKey: DEFAULT_AUTO_SYNC_DATABASE_KEY, proxy: "", GeneralButton: true, tagButton: true, @@ -94,6 +97,20 @@ export class ObsidianSettingTab extends PluginSettingTab { this.createSettingEl(containerEl, i18nConfig.AutoSync, i18nConfig.AutoSyncDesc, 'toggle', i18nConfig.AutoSync, this.plugin.settings.autoSync, 'autoSync') + new Setting(containerEl) + .setName(i18nConfig.AutoSyncFrontmatterKey) + .setDesc(i18nConfig.AutoSyncFrontmatterKeyDesc) + .addText((text) => + text + .setPlaceholder(DEFAULT_AUTO_SYNC_DATABASE_KEY) + .setValue(this.plugin.settings.autoSyncFrontmatterKey ?? "") + .onChange(async (value) => { + this.plugin.settings.autoSyncFrontmatterKey = value; + await this.plugin.saveSettings(); + this.plugin.resetAutoSyncNoticeCache(); + }) + ); + // Auto Sync Delay setting - only visible when autoSync is enabled this.autoSyncDelayContainer = containerEl.createDiv(); const delaySetting = new Setting(this.autoSyncDelayContainer) @@ -350,4 +367,3 @@ export class ObsidianSettingTab extends PluginSettingTab { } } - diff --git a/src/upload/updateYaml.ts b/src/upload/updateYaml.ts index 3bdafb3..4545c3e 100644 --- a/src/upload/updateYaml.ts +++ b/src/upload/updateYaml.ts @@ -2,7 +2,7 @@ import { App, Notice, TFile } from "obsidian"; import ObsidianSyncNotionPlugin from "../main"; import { DatabaseDetails } from "../ui/settingTabs"; import { i18nConfig } from "src/lang/I18n"; -import { AUTO_SYNC_DATABASE_KEY, ensureAutoSyncDatabaseEntry } from "src/utils/frontmatter"; +import { ensureAutoSyncDatabaseEntry } from "src/utils/frontmatter"; export async function updateYamlInfo( yamlContent: string, @@ -18,6 +18,7 @@ export async function updateYamlInfo( const { abName } = dbDetails const notionIDKey = `NotionID-${abName}`; const linkKey = `link-${abName}`; + const autoSyncKey = plugin.getAutoSyncFrontmatterKey(); if (notionUser !== "") { // replace url str "www" to notionID @@ -36,8 +37,8 @@ export async function updateYamlInfo( (NotionLinkDisplay) ? yamlContent[linkKey] = url : null; // ensure auto sync database list contains current short name - yamlContent[AUTO_SYNC_DATABASE_KEY] = ensureAutoSyncDatabaseEntry( - yamlContent[AUTO_SYNC_DATABASE_KEY], + yamlContent[autoSyncKey] = ensureAutoSyncDatabaseEntry( + yamlContent[autoSyncKey], abName ); }); diff --git a/src/utils/frontmatter.ts b/src/utils/frontmatter.ts index bb8b2b7..1a9d2a5 100644 --- a/src/utils/frontmatter.ts +++ b/src/utils/frontmatter.ts @@ -1,4 +1,15 @@ -export const AUTO_SYNC_DATABASE_KEY = "aytosync-database"; +export const DEFAULT_AUTO_SYNC_DATABASE_KEY = "autosync-database"; + +export function resolveAutoSyncKey(rawKey: unknown): string { + if (typeof rawKey === "string") { + const trimmed = rawKey.trim(); + if (trimmed.length > 0) { + return trimmed; + } + } + + return DEFAULT_AUTO_SYNC_DATABASE_KEY; +} function toCandidateList(value: unknown): string[] { if (Array.isArray(value)) { From e9355aaf92637bd907cd0c0240935e067bc36fe3 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Thu, 6 Nov 2025 16:50:50 +0000 Subject: [PATCH 17/32] docs: enhance auto sync documentation with frontmatter key configuration and examples --- docs/en/03-configuration.md | 17 ++++++++++- docs/en/04-sync.md | 56 +++++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/docs/en/03-configuration.md b/docs/en/03-configuration.md index 7a95714..f79643f 100644 --- a/docs/en/03-configuration.md +++ b/docs/en/03-configuration.md @@ -22,6 +22,21 @@ In the plugin settings, you can add and configure the Notion databases you want - [3️⃣ Custom Database](#3️⃣-custom-database) - [Finalizing Configuration](#finalizing-configuration) +## Auto Sync Frontmatter Entry + +If you enable auto sync, the plugin needs a frontmatter entry that lists which configured databases should receive updates. You can customise the name of this entry in **Settings → Auto Sync Frontmatter Key** (default: `autosync-database`). Use any text you like—letters, numbers, emojis, or other scripts are all supported. + +In your note's frontmatter, add the configured key and list the database abbreviations you created in the settings: + +```yaml +--- +title: My Article +autosync-database: [blog, ideas] +--- +``` + +The entry can be a YAML list or comma-separated string, and manual uploads will automatically add the current database abbreviation if it is missing. If you change the key name in settings, update your frontmatter to match the new value. + ## 1️⃣ General Database This is the most basic database type and is suitable for most users. @@ -109,4 +124,4 @@ Relation and Rollup types are not supported yet. ## Finalizing Configuration -After configuring your database, make sure to save your settings. You can now start syncing your Obsidian notes to the configured Notion database by using the "Share to NotionNext" command from the command palette or the note context menu. \ No newline at end of file +After configuring your database, make sure to save your settings. You can now start syncing your Obsidian notes to the configured Notion database by using the "Share to NotionNext" command from the command palette or the note context menu. diff --git a/docs/en/04-sync.md b/docs/en/04-sync.md index cbca3f5..36e521f 100644 --- a/docs/en/04-sync.md +++ b/docs/en/04-sync.md @@ -22,6 +22,25 @@ The plugin supports automatic syncing that monitors your notes for changes and a 3. Enable the toggle 4. Configure the "Auto Sync Delay" (default: 5 seconds, minimum: 2 seconds) +### Prepare the Frontmatter + +Auto sync reads the database list from the frontmatter key you configured in **Settings → Auto Sync Frontmatter Key** (default: `autosync-database`). To make sure your notes can sync automatically: + +- Add the configured key to your note's frontmatter +- List one or more database abbreviations that you defined in the plugin settings +- Keep the list updated if you change the databases a note should sync to + +Example with the default key: + +```yaml +--- +title: My Article +autosync-database: [blog, portfolio] +--- +``` + +If you change the key name in the settings, update your frontmatter to match. The plugin will prompt you when the entry is missing. + ### How Auto Sync Works When auto sync is enabled: @@ -32,7 +51,7 @@ When auto sync is enabled: ### Auto Sync Scenarios -#### Scenario A: New Document (Not Yet Synced) +#### Scenario A: Missing Auto Sync Entry ```yaml --- @@ -41,18 +60,34 @@ tags: [blog, tech] --- ``` +**Behavior:** +- ✅ Detects that the configured auto sync key is missing +- ✅ Shows notice: `⚠️ Auto sync skipped: Add a "{key}" entry in the frontmatter to choose databases` +- ✅ No sync operation performed +- 📝 **Action Required:** Add the configured key (default `autosync-database`) to the frontmatter with the database abbreviations + +#### Scenario B: New Document (Not Yet Synced) + +```yaml +--- +title: My New Article +aytosync-database: [blog] +--- +``` + **Behavior:** - ✅ Detects no NotionID present - ✅ Shows notice: "⚠️ Auto sync skipped: This document has not been synced to Notion, please upload manually first" - ✅ No sync operation performed - 📝 **Action Required:** Manually sync the document first using the command palette -#### Scenario B: Synced to One Database +#### Scenario C: Synced to One Database ```yaml --- title: My Article NotionID-blog: abc123 +autosync-database: [blog] --- ``` @@ -62,7 +97,7 @@ NotionID-blog: abc123 - ✅ Shows success/failure notification from the upload command - 📝 **No Action Required:** Changes are automatically synced -#### Scenario C: Synced to Multiple Databases +#### Scenario D: Synced to Multiple Databases ```yaml --- @@ -80,6 +115,21 @@ NotionID-notes: ghi789 - ✅ Shows individual result notifications for each database - 📝 **No Action Required:** Changes are automatically synced to all linked databases +#### Scenario E: Custom Frontmatter Key + +```yaml +--- +title: My Article +NotionID-blog: abc123 +NotionID-portfolio: def456 +🚀-sync-targets: [blog, portfolio] +--- +``` + +**Behavior:** +- ✅ Uses your custom key (for example `🚀-sync-targets`) configured in settings +- ✅ Syncs to the listed databases when NotionIDs are present +- 📝 **Remember:** Update both the setting and your frontmatter if you rename the key ### Auto Sync Best Practices 1. **First Sync Manually**: Always perform the first sync manually to establish the NotionID link From 3620505b56b14b1e761cbcf421ffc51a35d62d15 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Thu, 6 Nov 2025 16:51:42 +0000 Subject: [PATCH 18/32] update beta version 2.8.0-beta.2 --- CHANGELOG.md | 7 +++++++ docs/en/06-changelog.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfd48fc..fd0bae6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v2.8.0-beta.2 (2025-11-05) + +### Featured + +- Added setting to customise the frontmatter key used for auto sync database lists (defaults to `autosync-database`) + ## v2.8.0-beta.1 (2025-10-31) ### Added @@ -12,6 +18,7 @@ - Works on both desktop and mobile platforms - Added comprehensive i18n support for all UI elements and notifications - Added prerelease workflow for beta testing via GitHub Actions and BRAT +- Added setting to customise the frontmatter key used for auto sync database lists (defaults to `autosync-database`) ### Changed diff --git a/docs/en/06-changelog.md b/docs/en/06-changelog.md index a9817ab..9a82f2d 100644 --- a/docs/en/06-changelog.md +++ b/docs/en/06-changelog.md @@ -6,3 +6,35 @@ description: Release notes and updates for Obsidian to NotionNext # Changelog Welcome to the Changelog for Obsidian to NotionNext! Here you'll find a detailed list of all the updates, improvements, and bug fixes made to the plugin over time from the version `2.7.0` onwards. + +## v2.8.0-beta.2 (2025-11-05) + +### Featured + +- Added setting to customise the frontmatter key used for auto sync database lists (defaults to `autosync-database`) + + +## v2.8.0-beta.1 (2025-10-31) + +### Added + +- **Auto Sync Feature**: Automatically sync notes to Notion when content or frontmatter changes + - Configurable delay (default: 5 seconds, minimum: 2 seconds) + - Support for multiple database syncing + - Smart detection to avoid sync loops when only NotionID is updated + - Content hash comparison to detect body text changes + - Works on both desktop and mobile platforms +- Added comprehensive i18n support for all UI elements and notifications +- Added prerelease workflow for beta testing via GitHub Actions and BRAT + +### Changed + +- Enhanced settings tab with auto-sync configuration options +- Improved debug logging for better troubleshooting +- Updated documentation with auto-sync usage guide and troubleshooting section + +### Fixed + +- Fixed mobile compatibility issues by using `window.setTimeout` instead of `NodeJS.Timeout` +- Fixed sync loop prevention logic to properly handle frontmatter and content changes +- Fixed cache update timing to ensure accurate change detection From f4def623bbb90c26ade2de54f57c91d09b02d853 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Wed, 10 Dec 2025 21:30:49 +0000 Subject: [PATCH 19/32] styles: improve the translation --- src/lang/locale/en.ts | 139 +++++++++++++++++++++--------------------- src/lang/locale/ja.ts | 136 +++++++++++++++++++++-------------------- src/lang/locale/zh.ts | 138 ++++++++++++++++++++--------------------- 3 files changed, 209 insertions(+), 204 deletions(-) diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index b3b1e50..64e1597 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -1,99 +1,100 @@ export const en = { databaseFormat: "Database Format", - databaseFormatDesc: "Select the database format you want to sync to NotionNext or General", + databaseFormatDesc: "Select the database format to sync to: NotionNext or General.", databaseNext: "NotionNext", databaseGeneral: "General", databaseCustom: "Custom", databaseFullName: "Database Full Name", - databaseFullNameDesc: "Please give a full name for your database", - databaseFullNameText: "Enter your database full name", - databaseAbbreviateName: "Database Abbreviate Name", - databaseAbbreviateNameDesc: "Please give a nick name for your database", - databaseAbbreviateNameText: "Enter your database nick name", - ribbonIcon: "Share to NotionNext", - GeneralSetting: "General information Settings", + databaseFullNameDesc: "Set a full name for your database.", + databaseFullNameText: "Enter your database's full name", + databaseAbbreviateName: "Abbreviated Name", + databaseAbbreviateNameDesc: "Set a shorter, abbreviated name for your database.", + databaseAbbreviateNameText: "Enter your database's abbreviated name", + ribbonIcon: "Sync to NotionNext", + GeneralSetting: "General Settings", CommandID: "share-to-notionnext", - CommandName: "Share to NotionNext Database", + CommandName: "Sync to NotionNext", CommandIDGeneral: "share-to-notion", - CommandNameGeneral: "Share to Notion General Database", + CommandNameGeneral: "Sync to General Database", NotionNextButton: "NotionNext Sync", - NotionNextButtonDesc: "Open this option, Sync to NotionNext command will be displayed in the command palette (default: ON)", + NotionNextButtonDesc: "Enables the 'Sync to NotionNext' command in the command palette (default: on).", NotionNextSettingHeader: "NotionNext Database Settings", NotionAPI: "Notion API Token", - NotionAPIDesc: "Generate from https://www.notion.so/my-integrations", + NotionAPIDesc: "Get yours from notion.so/my-integrations.", NotionAPIText: "Enter your Notion API Token", DatabaseID: "Database ID", - DatabaseIDDesc: "Collect from the top-right Share --> Publish", + DatabaseIDDesc: "Find this in your Notion page's top-right 'Share' menu.", DatabaseIDText: "Enter your Database ID", - BannerUrl: "Banner url (optional)", + BannerUrl: "Banner URL (optional)", BannerUrlDesc: - "Default is empty, if you want to show a banner, please enter the url (like: https://abc.com/b.png)", - BannerUrlText: "Enter your banner url", - NotionUser: "Notion ID (username, optional)", + "Leave empty for no banner. If you want a banner, enter an image URL (e.g., https://abc.com/b.png).", + BannerUrlText: "Enter your banner URL", + NotionUser: "Notion Username (optional)", NotionUserDesc: - "Collect from share link likes:https://username.notion.site. Your notion id is [username]", - NotionUserText: "Enter your notion ID", - NotionLinkDisplay: "Notion Link Display", - NotionLinkDisplayDesc: "Default is ON, if you want to hide the link in the front matter, please turn it off", + "If your site is username.notion.site, your username is [username].", + NotionUserText: "Enter your Notion username", + NotionLinkDisplay: "Display Notion Link", + NotionLinkDisplayDesc: "If disabled, the Notion link won't be added to the front matter after syncing (default: on).", + AutoCopyNotionLink: "Auto-copy Notion Link", + AutoCopyNotionLinkDesc: "Automatically copy the Notion page link to the clipboard after syncing (default: on).", AutoSync: "Auto Sync", - AutoSyncDesc: "Automatically sync to Notion when frontmatter or content is modified (requires existing NotionID)", + AutoSyncDesc: "Automatically syncs changes to Notion when the file's frontmatter or content is modified. Supports creating and updating pages.", AutoSyncFrontmatterKey: "Auto Sync Frontmatter Key", - AutoSyncFrontmatterKeyDesc: "Set the frontmatter property name that lists which databases should auto sync (defaults to autosync-database).", + AutoSyncFrontmatterKeyDesc: "Specify the frontmatter key used to list the databases this file should auto-sync to (defaults to 'autosync-database').", AutoSyncDelay: "Auto Sync Delay (seconds)", - AutoSyncDelayDesc: "How many seconds to wait after document modification before triggering auto sync (default: 5 seconds, minimum: 2 seconds)", + AutoSyncDelayDesc: "Delay in seconds to wait before syncing after a change. Prevents excessive syncs (default: 5s, min: 2s).", AutoSyncDelayText: "Enter delay in seconds", NotionGeneralSettingHeader: "General Notion Database Settings", - NotionGeneralButton: "Notion General Sync", - NotionGeneralButtonDesc: "Open this option, Sync to Notion General Database command will be displayed in the command palette (default: ON)", - NotionTagButton: "Notion Tags Sync", - NotionTagButtonDesc: "Sync Tags to Notion General Database (default: ON)", - NotionCustomTitle: "Customise title property", - NotionCustomTitleDesc: "Modify the column name of the Notion database (default: OFF)", - NotionCustomTitleName: "Preferred title name", - NotionCustomTitleNameDesc: "Enter the preferred title name for the first column of the Notion database (default: title)", - NotionCustomTitleText: "Enter the name", - NotionCustomValues: "Customise values property", - NotionCustomValuesDesc: "Modify the column name of the Notion database,one per line", - NotionCustomValuesText: "Enter all properties that you want to sync", - NotYetFinish: "Not finished. This function will be available in the next version", - PlaceHolder: "Enter database Name", - "notion-logo": "Share to NotionNext", - "sync-preffix": "Sync to ", - "sync-success": "success", - "sync-fail": "failed", - "open-notion": "Please open the file that needs to be synchronized", + NotionGeneralButton: "General Database Sync", + NotionGeneralButtonDesc: "Enables the 'Sync to General Database' command in the command palette (default: on).", + NotionTagButton: "Sync Tags", + NotionTagButtonDesc: "Sync Obsidian tags to the Notion database (default: on).", + NotionCustomTitle: "Custom Title Property", + NotionCustomTitleDesc: "Customize the title property's name in your Notion database (default: off).", + NotionCustomTitleName: "Custom Title Property Name", + NotionCustomTitleNameDesc: "Enter the custom name for the title property of your Notion database (default: 'title').", + NotionCustomTitleText: "Enter the property name", + NotionCustomValues: "Custom Properties", + NotionCustomValuesDesc: "Define custom properties to sync to your Notion database, one per line.", + NotionCustomValuesText: "Enter all properties you want to sync", + NotYetFinish: "This feature will be available in a future version.", + PlaceHolder: "Enter database name", + "notion-logo": "Sync to NotionNext", + "sync-success": "Successfully synced to NotionNext:\n", + "sync-fail": "Failed to sync to NotionNext:\n", + "open-notion": "Please open a file to sync first.", "config-secrets-notion-api": - "Please set up the notion API in the settings tab.", + "Please configure your Notion API key in the plugin settings.", "config-secrets-database-id": - "Please set up the database id in the settings tab.", + "Please configure your Database ID in the plugin settings.", "set-tags-fail": - "Set tags fail,please check the frontmatter of the file or close the tag switch in the settings tab.", + "Failed to set tags. Check the frontmatter or disable tag sync in settings.", NNonMissing: - "The 'NNon' property is missing in the settings. Please set it up.", + "The 'NNon' property is not set. Please select a NotionNext database in settings.", "set-api-id": - "Please set up the notion API and database ID in the settings tab.", + "Please configure your Notion API key and Database ID in the plugin settings.", NotionCustomSettingHeader: "Notion Custom Database Settings", - NotionCustomButton: "Notion Customised command switch", - NotionCustomButtonDesc: "Open this option, Sync to Notion Customised Database command will be displayed in the command palette", + NotionCustomButton: "Enable Custom Database Command", + NotionCustomButtonDesc: "If enabled, the 'Sync to Custom Database' command appears in the command palette.", CustomPropertyName: "Property Name", - CustomPropertyFirstColumn: "Title Column", - CustomPropertyFirstColumnDesc: "The title of the page, must be the first property", + CustomPropertyFirstColumn: "Title Property Name", + CustomPropertyFirstColumnDesc: "The page title. This must be the first property in the list.", CustomProperty: "Property", AddCustomProperty: "Add Custom Property", AddNewProperty: "Add New Property", - AddNewPropertyDesc: "Add new property match with your notion database", - CopyErrorMessage: "Auto copy failed, please copy it manually", - BlockUploaded: "All blocks uploaded", - ExtraBlockUploaded: "Extra blocks uploaded", - CheckConsole: "Check the console for more information \n opt+cmd+i/ctrl+shift+i", - SettingsMigrated: "✨ Plugin settings updated! Auto sync feature added, check plugin settings", - AutoSyncNoNotionID: "⚠️ Auto sync skipped: This document has not been synced to Notion, please upload manually first", - AutoSyncMissingDatabaseList: "⚠️ Auto sync skipped: Add a \"{key}\" entry in the frontmatter to choose databases", - AutoSyncMultipleSync: "🔄 Auto sync: Syncing to {count} database(s)...", - AutoSyncFailed: "Auto sync to {database} failed: {error}", - AutoSyncError: "Auto sync failed for {filename}: {error}", - "reach-mobile-limit": "The number of blocks exceeds the limit of 100, please use the desktop plugin", - StartUpload: "Start upload {filename}", + AddNewPropertyDesc: "Add a new property that matches a property in your Notion database.", + CopyErrorMessage: "Auto-copy failed. Please copy the link manually.", + BlockUploaded: "All content blocks uploaded successfully.", + ExtraBlockUploaded: "Additional blocks uploaded successfully.", + CheckConsole: "For more details, open the developer console (opt+cmd+i or ctrl+shift+i).", + SettingsMigrated: "✨ Settings updated! Auto-Sync is now available. Check the settings to learn more.", + AutoSyncNoNotionID: "🆕 Auto-sync: First upload to Notion", + AutoSyncMissingDatabaseList: "⚠️ Auto-sync skipped: Add `{key}: [database_name]` to your frontmatter to specify target databases.", + AutoSyncMultipleSync: "🔄 Auto-sync: Syncing to {count} database(s)...", + AutoSyncFailed: "Auto-sync to {database} failed: {error}", + AutoSyncError: "Auto-sync for {filename} failed: {error}", + "reach-mobile-limit": "Block limit (100) reached. For unlimited blocks, please use the desktop version.", + StartUpload: "Starting upload for {filename}...", AddNewDatabase: "Add New Database", AddNewDatabaseDesc: "Add a new database configuration", AddNewDatabaseTooltip: "Add New Database", @@ -101,14 +102,14 @@ export const en = { Preview: "Preview", DatabaseFormatLabel: "Database Format", DatabaseFullNameLabel: "Database Full Name", - DatabaseAbbreviateNameLabel: "Database Abbreviate Name", + DatabaseAbbreviateNameLabel: "Abbreviated Name", NotionAPILabel: "Notion API Key", DatabaseIDLabel: "Database ID", ToggleAPIKeyVisibility: "Toggle API Key Visibility", CopyAPIKey: "Copy API Key", - APIKeyCopied: "API Key copied to clipboard", + APIKeyCopied: "API key copied to clipboard.", ToggleDatabaseIDVisibility: "Toggle Database ID Visibility", CopyDatabaseID: "Copy Database ID", - DatabaseIDCopied: "Database ID copied to clipboard", - AddNewDatabaseModal: "Add new database", + DatabaseIDCopied: "Database ID copied to clipboard.", + AddNewDatabaseModal: "Add New Database", } diff --git a/src/lang/locale/ja.ts b/src/lang/locale/ja.ts index c25a2f4..f57a0f5 100644 --- a/src/lang/locale/ja.ts +++ b/src/lang/locale/ja.ts @@ -1,91 +1,93 @@ export const ja = { databaseFormat: "データベース形式", - databaseFormatDesc: "同期したいデータベース形式を選択してください", + databaseFormatDesc: "同期先のデータベース形式を選択してください(NotionNext または 一般)。", databaseNext: "NotionNext", - databaseGeneral: "一般的なNotion", + databaseGeneral: "一般", databaseCustom: "カスタム", databaseFullName: "データベースの全称", - databaseFullNameDesc: "データベースの全称を入力してください", + databaseFullNameDesc: "データベースのフルネームを設定します。", databaseFullNameText: "データベースの全称を入力", databaseAbbreviateName: "データベースの略称", - databaseAbbreviateNameDesc: "データベースの略称を入力してください", + databaseAbbreviateNameDesc: "データベースの略称を設定します。", databaseAbbreviateNameText: "データベースの略称を入力", - ribbonIcon: "NotionNextで共有", + ribbonIcon: "NotionNextへ同期", GeneralSetting: "一般設定", CommandID: "share-to-notionnext", - CommandName: "NotionNextデータベースに共有", + CommandName: "NotionNextへ同期", CommandIDGeneral: "share-to-notion", - CommandNameGeneral: "一般的なNotionデータベースに共有", + CommandNameGeneral: "一般データベースへ同期", NotionNextButton: "NotionNext同期", - NotionNextButtonDesc: "このオプションを開くと、NotionNext同期コマンドがコマンドパレットに表示されます(デフォルト:ON)", + NotionNextButtonDesc: "有効にすると、コマンドパレットに「NotionNextへ同期」が表示されます(デフォルト:オン)。", NotionNextSettingHeader: "NotionNextデータベース設定", NotionAPI: "Notion API トークン", - NotionAPIDesc: "https://www.notion.so/my-integrations から生成してください", + NotionAPIDesc: "notion.so/my-integrations から取得します。", NotionAPIText: "Notion API トークンを入力", DatabaseID: "データベースID", - DatabaseIDDesc: "右上の共有 --> 公開から取得してください", + DatabaseIDDesc: "Notionページの右上「共有」メニューから取得します。", DatabaseIDText: "データベースIDを入力", - BannerUrl: "バナーのURL(任意)", - BannerUrlDesc: "デフォルトは空白です。バナーを表示したい場合は、URLを入力してください(例:https://abc.com/b.png)", + BannerUrl: "バナーURL(任意)", + BannerUrlDesc: "空のままにするとバナーは表示されません。表示するには画像のURLを入力してください(例:https://abc.com/b.png)。", BannerUrlText: "バナーのURLを入力", - NotionUser: "Notion ID(ユーザー名、任意)", - NotionUserDesc: "共有リンクから取得(例:https://username.notion.site)。Notion IDは[username]です", - NotionUserText: "Notion IDを入力", - NotionLinkDisplay: "Notionリンク表示", - NotionLinkDisplayDesc: "デフォルトはONです。front matterにリンクを非表示にしたい場合は、オフにしてください", - AutoSync: "自動同期", - AutoSyncDesc: "frontmatter またはコンテンツが変更されたときに自動的に Notion に同期します(NotionID が必要)", - AutoSyncFrontmatterKey: "自動同期 frontmatter キー", - AutoSyncFrontmatterKeyDesc: "自動同期するデータベースを列挙する frontmatter のプロパティ名を設定します(デフォルトは autosync-database)。", - AutoSyncDelay: "自動同期遅延時間(秒)", - AutoSyncDelayDesc: "ドキュメントの変更後、自動同期をトリガーするまでの待機時間(デフォルト:5秒、最小:2秒)", - AutoSyncDelayText: "遅延秒数を入力", - NotionGeneralSettingHeader: "一般的なNotionデータベース設定", - NotionGeneralButton: "一般的なNotion同期", - NotionGeneralButtonDesc: "このオプションを開くと、一般的なNotionデータベース同期コマンドがコマンドパレットに表示されます(デフォルト:ON)", - NotionTagButton: "Notionタグ同期", - NotionTagButtonDesc: "タグを一般的なNotionデータベースに同期(デフォルト:ON)", - NotionCustomTitle: "タイトルのカスタマイズ", - NotionCustomTitleDesc: "Notionデータベースの列名を変更(デフォルト:OFF)", - NotionCustomTitleName: "希望のタイトル名", - NotionCustomTitleNameDesc: "Notionデータベースの最初の列のための希望のタイトル名を入力(デフォルト:title)", - NotionCustomTitleText: "名前を入力", - NotionCustomValues: "値のカスタマイズ", - NotionCustomValuesDesc: "Notionデータベースの列名を変更、1行に1つ", + NotionUser: "Notionユーザー名(任意)", + NotionUserDesc: "共有リンクが `username.notion.site` の場合、Notionユーザー名は `[username]` です。", + NotionUserText: "Notionユーザー名を入力", + NotionLinkDisplay: "Notionリンク表示", + NotionLinkDisplayDesc: "デフォルトで有効。無効にすると、同期後にfront matterへNotionリンクが追加されません。", + AutoCopyNotionLink: "Notionリンクを自動コピー", + AutoCopyNotionLinkDesc: "同期完了後、Notionページのリンクをクリップボードに自動コピーします(デフォルト:オン)。", + AutoSync: "自動同期", + AutoSyncDesc: "ファイルの内容(frontmatterまたは本文)が変更されると、自動でNotionに同期します。新規作成と更新の両方に対応。", + AutoSyncFrontmatterKey: "自動同期 frontmatter キー", + AutoSyncFrontmatterKeyDesc: "自動同期の対象となるデータベースをリストアップするための frontmatterキーを設定します(デフォルト:autosync-database)。", + AutoSyncDelay: "自動同期の遅延(秒)", + AutoSyncDelayDesc: "変更が検知されてから同期を開始するまでの遅延時間(秒)。同期の頻発を防ぎます(デフォルト:5秒、最小:2秒)。", + AutoSyncDelayText: "遅延秒数を入力", + NotionGeneralSettingHeader: "一般Notionデータベース設定", + NotionGeneralButton: "一般データベース同期", + NotionGeneralButtonDesc: "有効にすると、コマンドパレットに「一般データベースへ同期」が表示されます(デフォルト:オン)。", + NotionTagButton: "タグを同期", + NotionTagButtonDesc: "ObsidianのタグをNotionデータベースに同期します(デフォルト:オン)。", + NotionCustomTitle: "タイトルプロパティをカスタム", + NotionCustomTitleDesc: "Notionデータベースのタイトル列の名前をカスタマイズします(デフォルト:オフ)。", + NotionCustomTitleName: "カスタムタイトル名", + NotionCustomTitleNameDesc: "Notionデータベースのタイトル列に使用するカスタム名を入力してください(デフォルト:「title」)。", + NotionCustomTitleText: "プロパティ名を入力", + NotionCustomValues: "カスタムプロパティ", + NotionCustomValuesDesc: "Notionデータベースに同期するカスタムプロパティを1行に1つずつ定義します。", NotionCustomValuesText: "同期したいすべてのプロパティを入力", - NotYetFinish: "未完了。この機能は次のバージョンで利用可能になります", + NotYetFinish: "この機能は将来のバージョンで利用可能になります。", PlaceHolder: "データベース名を入力", - "notion-logo": "NotionNextで共有", - "sync-success": "NotionNextへの同期に成功:\n", - "sync-fail": "NotionNextへの同期に失敗:\n", - "open-notion": "同期が必要なファイルを開いてください", - "config-secrets-notion-api": "設定タブでNotion APIを設定してください", - "config-secrets-database-id": "設定タブでデータベースIDを設定してください", - "set-tags-fail": "タグの設定に失敗。ファイルのfrontmatterを確認するか、設定タブでタグのスイッチをオフにしてください", - NNonMissing: "設定に 'NNon' プロパティがありません。設定してください", - "set-api-id": "設定タブでNotion APIおよびデータベースIDを設定してください", + "notion-logo": "NotionNextへ同期", + "sync-success": "NotionNextへの同期が成功しました。\n", + "sync-fail": "NotionNextへの同期に失敗しました。\n", + "open-notion": "同期するファイルを先に開いてください。", + "config-secrets-notion-api": "プラグイン設定でNotion APIキーを設定してください。", + "config-secrets-database-id": "プラグイン設定でデータベースIDを設定してください。", + "set-tags-fail": "タグの設定に失敗しました。frontmatterを確認するか、設定でタグ同期を無効にしてください。", + NNonMissing: "'NNon'プロパティが設定されていません。設定でNotionNextデータベースを選択してください。", + "set-api-id": "プラグイン設定でNotion APIキーとデータベースIDを設定してください。", NotionCustomSettingHeader: "Notionカスタムデータベース設定", - NotionCustomButton: "Notionカスタマイズコマンドの切り替え", - NotionCustomButtonDesc: "このオプションを開くと、Notionカスタムデータベース同期コマンドがコマンドパレットに表示されます", - CustomPropertyName: "カスタムプロパティ名", - CustomPropertyFirstColumn: "最初の列のカスタムプロパティ名", - CustomPropertyFirstColumnDesc: "最初の列のカスタムプロパティ名を入力してください", - CustomProperty: "カスタムプロパティ", + NotionCustomButton: "カスタムデータベースコマンドを有効化", + NotionCustomButtonDesc: "有効にすると、「カスタムデータベースへ同期」コマンドがコマンドパレットに表示されます。", + CustomPropertyName: "プロパティ名", + CustomPropertyFirstColumn: "タイトルプロパティ名", + CustomPropertyFirstColumnDesc: "ページのタイトル。これはリストの最初のプロパティである必要があります。", + CustomProperty: "プロパティ", AddCustomProperty: "カスタムプロパティを追加", AddNewProperty: "新しいプロパティを追加", - AddNewPropertyDesc: "新しいプロパティを追加してください", - CopyErrorMessage: "自動コピーに失敗しました", - BlockUploaded: "ブロックがアップロードされました", - ExtraBlockUploaded: "追加ブロックがアップロードされました", - CheckConsole: "詳細情報を確認するには、コンソールを開いてください \n opt+cmd+i/ctrl+shift+i", - SettingsMigrated: "✨ プラグイン設定が更新されました!自動同期機能が追加されました。設定を確認してください", - AutoSyncNoNotionID: "⚠️ 自動同期をスキップ:このドキュメントは Notion に同期されていません。まず手動でアップロードしてください", - AutoSyncMissingDatabaseList: "⚠️ 自動同期をスキップ:frontmatter に \"{key}\" 項目を追加してください", - AutoSyncMultipleSync: "🔄 自動同期:{count} 個のデータベースに同期中...", - AutoSyncFailed: "{database} への自動同期に失敗しました:{error}", - AutoSyncError: "{filename} の自動同期に失敗しました:{error}", - "reach-mobile-limit": "ブロック数が100の制限を超えています。デスクトップ版プラグインを使用してください", - StartUpload: "アップロード開始 {filename}", + AddNewPropertyDesc: "Notionデータベースのプロパティと一致する新しいプロパティを追加します。", + CopyErrorMessage: "リンクの自動コピーに失敗しました。手動でコピーしてください。", + BlockUploaded: "すべてのブロックをアップロードしました", + ExtraBlockUploaded: "追加のブロックをアップロードしました", + CheckConsole: "詳細は、開発者コンソール(opt+cmd+i または ctrl+shift+i)で確認できます。", + SettingsMigrated: "✨ 設定が更新されました!自動同期が利用可能です。詳細は設定画面をご確認ください。", + AutoSyncNoNotionID: "🆕 自動同期:Notionへ初めてアップロードします", + AutoSyncMissingDatabaseList: "⚠️ 自動同期をスキップ:frontmatterに `{key}: [データベース名]` を追加して同期先を指定してください。", + AutoSyncMultipleSync: "🔄 自動同期:{count}個のデータベースに同期しています...", + AutoSyncFailed: "{database}への自動同期に失敗しました:{error}", + AutoSyncError: "{filename}の自動同期に失敗しました:{error}", + "reach-mobile-limit": "ブロック上限(100)に達しました。ブロック数に制限のないデスクトップ版をご利用ください。", + StartUpload: "{filename} のアップロードを開始します...", AddNewDatabase: "新しいデータベースを追加", AddNewDatabaseDesc: "新しいデータベース構成を追加", AddNewDatabaseTooltip: "新しいデータベースを追加", @@ -98,9 +100,9 @@ export const ja = { DatabaseIDLabel: "データベース ID", ToggleAPIKeyVisibility: "API キーの表示を切り替え", CopyAPIKey: "API キーをコピー", - APIKeyCopied: "API キーをクリップボードにコピーしました", + APIKeyCopied: "APIキーをクリップボードにコピーしました。", ToggleDatabaseIDVisibility: "データベース ID の表示を切り替え", CopyDatabaseID: "データベース ID をコピー", - DatabaseIDCopied: "データベース ID をクリップボードにコピーしました", + DatabaseIDCopied: "データベースIDをクリップボードにコピーしました。", AddNewDatabaseModal: "新しいデータベースを追加", }; diff --git a/src/lang/locale/zh.ts b/src/lang/locale/zh.ts index e3caa5c..402a482 100644 --- a/src/lang/locale/zh.ts +++ b/src/lang/locale/zh.ts @@ -1,108 +1,110 @@ export const zh = { databaseFormat: "数据库格式", - databaseFormatDesc: "选择你想要同步的数据库格式Next 或者 普通", + databaseFormatDesc: "选择同步的目标数据库格式:NotionNext 或 通用", databaseNext: "NotionNext", - databaseGeneral: "普通", + databaseGeneral: "通用", databaseCustom: "自定义", - databaseFullName: "数据库全称", - databaseFullNameDesc: "给你的数据库起一个全称", - databaseFullNameText: "输入你的数据库全称", + databaseFullName: "数据库全名", + databaseFullNameDesc: "为数据库设置一个全名", + databaseFullNameText: "输入您的数据库全名", databaseAbbreviateName: "数据库简称", - databaseAbbreviateNameDesc: "给你的数据库起一个简称", - databaseAbbreviateNameText: "输入你的数据库简称", - ribbonIcon: "分享到 NotionNext", + databaseAbbreviateNameDesc: "为数据库设置一个简称", + databaseAbbreviateNameText: "输入您的数据库简称", + ribbonIcon: "同步到 NotionNext", GeneralSetting: "通用设置", CommandID: "share-to-notionnext", - CommandName: "分享到 NotionNext", + CommandName: "同步到 NotionNext", CommandIDGeneral: "share-to-notion", - CommandNameGeneral: "分享到 Notion 普通数据库", + CommandNameGeneral: "同步到通用数据库", NotionNextButton: "NotionNext 同步", - NotionNextButtonDesc: "打开此选项,NotionNext 同步将显示在命令面板中(默认:开)", - NotionNextSettingHeader: "NotionNext 数据库参数设置", + NotionNextButtonDesc: "启用后,命令面板中将显示“同步到 NotionNext”命令(默认开启)", + NotionNextSettingHeader: "NotionNext 数据库设置", NotionAPI: "Notion API 令牌", - NotionAPIDesc: "从 https://www.notion.so/my-integrations 生成", - NotionAPIText: "输入你的 Notion API 令牌", + NotionAPIDesc: "从 notion.so/my-integrations 获取", + NotionAPIText: "输入您的 Notion API 令牌", DatabaseID: "数据库 ID", - DatabaseIDDesc: "从右上角的分享 --> 发布中获取", - DatabaseIDText: "输入你的数据库 ID", + DatabaseIDDesc: "可从 Notion 页面右上角的“分享”菜单中获取", + DatabaseIDText: "输入您的数据库 ID", BannerUrl: "封面图片地址(可选)", BannerUrlDesc: - "默认为空,如果你想显示封面图片,请输入图片地址(例如:https://abc.com/b.png)", - BannerUrlText: "输入你的封面图片地址", - NotionUser: "Notion ID(用户名,可选)", + "留空则不显示。如需封面,请输入图片地址(例如:https://abc.com/b.png)", + BannerUrlText: "输入您的封面图片地址", + NotionUser: "Notion 用户名(可选)", NotionUserDesc: - "数据库分享链接类似:https://username.notion.site/。你的 Notion ID 是 [username]", - NotionUserText: "输入你的 Notion ID", - NotionLinkDisplay: "Notion 链接显示", - NotionLinkDisplayDesc: "默认开启,如果你不想在front matter中显示链接,请关闭", + "若分享链接为 username.notion.site,你的 Notion 用户名即为 [username]", + NotionUserText: "输入您的 Notion 用户名", + NotionLinkDisplay: "显示 Notion 链接", + NotionLinkDisplayDesc: "默认开启。关闭后,同步成功时 frontmatter 中不会出现 Notion 链接", + AutoCopyNotionLink: "自动复制 Notion 链接", + AutoCopyNotionLinkDesc: "同步后自动将 Notion 链接复制到剪贴板(默认开启)", AutoSync: "自动同步", - AutoSyncDesc: "当检测到文档的 frontmatter 或内容发生修改时,自动同步到 Notion(需要文档已有 NotionID)", + AutoSyncDesc: "当文档的 frontmatter 或内容修改时,将自动同步到 Notion(支持新建和更新)", AutoSyncFrontmatterKey: "自动同步 Frontmatter 键名", - AutoSyncFrontmatterKeyDesc: "设置用于列出自动同步数据库的 frontmatter 属性名称(默认 autosync-database)。", - AutoSyncDelay: "自动同步延迟时间(秒)", - AutoSyncDelayDesc: "文档修改后等待多少秒才触发自动同步,避免频繁同步(默认:5秒,最小:2秒)", + AutoSyncFrontmatterKeyDesc: "设置用于指定自动同步数据库列表的 frontmatter 键名(默认为 autosync-database)。", + AutoSyncDelay: "自动同步延迟(秒)", + AutoSyncDelayDesc: "文档修改后,等待指定秒数再触发自动同步,以避免频繁操作(默认 5 秒,最少 2 秒)", AutoSyncDelayText: "输入延迟秒数", - NotionGeneralSettingHeader: "普通 Notion 数据库设置", - NotionGeneralButton: "普通数据库同步", - NotionGeneralButtonDesc: "打开此选项,同步到普通数据库命令将显示在命令面板中(默认:开)", - NotionTagButton: "标签同步开关", - NotionTagButtonDesc: "将标签同步到普通数据库(默认:开)", - NotionCustomTitle: "修改 Notion 数据库表头开关", - NotionCustomTitleDesc: "自定义Notion 数据库第一列表头名(默认:关)", - NotionCustomTitleName: "想要修改的表头名", - NotionCustomTitleNameDesc: "输入你想要修改的notion数据库的表头名(默认:title)", - NotionCustomTitleText: "输入表头名", - NotionCustomValues: "自定义Notion 数据库表头", - NotionCustomValuesDesc: "自定义Notion 数据库表头,每行一个", - NotionCustomValuesText: "输入你想要同步的所有属性", - NotYetFinish: "未完成。此功能将在之后版本中提供", + NotionGeneralSettingHeader: "通用 Notion 数据库设置", + NotionGeneralButton: "通用数据库同步", + NotionGeneralButtonDesc: "启用后,命令面板中将显示“同步到通用数据库”命令(默认开启)", + NotionTagButton: "标签同步", + NotionTagButtonDesc: "将 Obsidian 标签同步到 Notion 数据库(默认开启)", + NotionCustomTitle: "自定义标题属性", + NotionCustomTitleDesc: "自定义 Notion 数据库中标题列的名称(默认关闭)", + NotionCustomTitleName: "自定义标题名称", + NotionCustomTitleNameDesc: "为 Notion 数据库的标题列设置一个自定义名称(默认为 title)", + NotionCustomTitleText: "输入标题名称", + NotionCustomValues: "自定义属性", + NotionCustomValuesDesc: "自定义同步到 Notion 数据库的属性,每行一个。", + NotionCustomValuesText: "输入所有你希望同步的属性", + NotYetFinish: "此功能将在未来版本中提供", PlaceHolder: "输入数据库名称", - "notion-logo": "分享到NotionNext", - "sync-success": "同步到NotionNext成功:\n", - "sync-fail": "同步到NotionNext失败: \n", - "open-file": "请打开需要同步的文件", - "config-secrets-notion-api": "请在插件设置中添加notion API", - "config-secrets-database-id": "请在插件设置中添加database id", + "notion-logo": "同步到 NotionNext", + "sync-success": "成功同步到 NotionNext:\n", + "sync-fail": "同步到 NotionNext 失败:\n", + "open-file": "请先打开要同步的文件。", + "config-secrets-notion-api": "请在插件设置中配置 Notion API 密钥。", + "config-secrets-database-id": "请在插件设置中配置数据库 ID。", "set-tags-fail": - "设置标签失败,请检查文件的frontmatter,或者在插件设置中关闭设置tags开关", - NNonMissing: "未设置'NNon'属性,请在插件设置中选择NotionNext数据库。", - "set-api-id": "请在插件设置中设置notion API和database ID", + "标签设置失败,请检查 frontmatter 或在设置中关闭标签同步。", + NNonMissing: "未设置 'NNon' 属性,请在插件设置中选择一个 NotionNext 数据库。", + "set-api-id": "请在插件设置中配置 Notion API 和数据库 ID。", NotionCustomSettingHeader: "Notion 自定义数据库设置", - NotionCustomButton: "Notion 自定义数据库同步命令开关", - NotionCustomButtonDesc: "打开此选项,同步到自定义数据库命令将显示在命令面板中", + NotionCustomButton: "启用自定义数据库同步命令", + NotionCustomButtonDesc: "启用后,“同步到自定义数据库”的命令将出现在命令面板中。", CustomPropertyName: "自定义属性名", - CustomPropertyFirstColumn: "第一列属性名", - CustomPropertyFirstColumnDesc: "第一列必须为标题属性名", + CustomPropertyFirstColumn: "标题属性", + CustomPropertyFirstColumnDesc: "第一列必须为标题属性。", CustomProperty: "自定义属性", AddCustomProperty: "添加自定义属性", AddNewProperty: "添加新属性", - AddNewPropertyDesc: "添加一个和Notion数据库匹配的新属性", - CopyErrorMessage: "复制链接失败,请手动复制", - BlockUploaded: "所有内容已成功上传", - ExtraBlockUploaded: "额外内容已成功上传", - CheckConsole: "opt+cmd+i/ctrl+shift+i,\n打开控制台查看更多信息", - SettingsMigrated: "✨ 插件设置已更新!新增自动同步功能,请在设置中查看", - AutoSyncNoNotionID: "⚠️ 自动同步跳过:此文档未同步到 Notion,请先手动上传", - AutoSyncMissingDatabaseList: "⚠️ 自动同步跳过:请在 frontmatter 中添加 \"{key}\" 项来指定数据库", + AddNewPropertyDesc: "添加一个与您 Notion 数据库中的属性相匹配的新属性。", + CopyErrorMessage: "自动复制链接失败,请手动复制。", + BlockUploaded: "所有块已上传成功", + ExtraBlockUploaded: "额外块已上传成功", + CheckConsole: "按 opt+cmd+i / ctrl+shift+i 打开控制台查看详情。", + SettingsMigrated: "✨ 插件设置已更新!新增自动同步功能,详情请查看设置。", + AutoSyncNoNotionID: "🆕 自动同步:首次上传到 Notion", + AutoSyncMissingDatabaseList: "⚠️ 自动同步已跳过:请在 frontmatter 中添加 \"{key}\" 以指定目标数据库。", AutoSyncMultipleSync: "🔄 自动同步:正在同步到 {count} 个数据库...", - AutoSyncFailed: "自动同步到 {database} 失败:{error}", - AutoSyncError: "自动同步 {filename} 失败:{error}", - StartUpload: "开始上传 {filename}", + AutoSyncFailed: "同步到 {database} 失败:{error}", + AutoSyncError: "同步 {filename} 失败:{error}", + StartUpload: "开始上传 {filename}...", AddNewDatabase: "添加新数据库", AddNewDatabaseDesc: "添加新的数据库配置", AddNewDatabaseTooltip: "添加新数据库", EditDatabase: "编辑数据库", Preview: "预览", DatabaseFormatLabel: "数据库格式", - DatabaseFullNameLabel: "数据库全称", + DatabaseFullNameLabel: "数据库全名", DatabaseAbbreviateNameLabel: "数据库简称", NotionAPILabel: "Notion API 密钥", DatabaseIDLabel: "数据库 ID", ToggleAPIKeyVisibility: "切换 API 密钥可见性", CopyAPIKey: "复制 API 密钥", - APIKeyCopied: "API 密钥已复制到剪贴板", + APIKeyCopied: "API 密钥已复制到剪贴板。", ToggleDatabaseIDVisibility: "切换数据库 ID 可见性", CopyDatabaseID: "复制数据库 ID", - DatabaseIDCopied: "数据库 ID 已复制到剪贴板", + DatabaseIDCopied: "数据库 ID 已复制到剪贴板。", AddNewDatabaseModal: "添加新数据库", } From e6b13e5eeeb453218dd389d9c599b6707c679943 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Wed, 10 Dec 2025 21:32:06 +0000 Subject: [PATCH 20/32] feat: auto copy after upload --- src/ui/settingTabs.ts | 4 ++++ src/upload/updateYaml.ts | 13 ++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/ui/settingTabs.ts b/src/ui/settingTabs.ts index 0cb5975..7b7147f 100644 --- a/src/ui/settingTabs.ts +++ b/src/ui/settingTabs.ts @@ -14,6 +14,7 @@ export interface PluginSettings { bannerUrl: string; notionUser: string; NotionLinkDisplay: boolean; + autoCopyNotionLink: boolean; autoSync: boolean; autoSyncDelay: number; autoSyncFrontmatterKey: string; @@ -53,6 +54,7 @@ export const DEFAULT_SETTINGS: PluginSettings = { bannerUrl: "", notionUser: "", NotionLinkDisplay: true, + autoCopyNotionLink: true, autoSync: false, autoSyncDelay: 5, autoSyncFrontmatterKey: DEFAULT_AUTO_SYNC_DATABASE_KEY, @@ -95,6 +97,8 @@ export class ObsidianSettingTab extends PluginSettingTab { this.createSettingEl(containerEl, i18nConfig.NotionLinkDisplay, i18nConfig.NotionLinkDisplayDesc, 'toggle', i18nConfig.NotionLinkDisplay, this.plugin.settings.NotionLinkDisplay, 'NotionLinkDisplay') + this.createSettingEl(containerEl, i18nConfig.AutoCopyNotionLink, i18nConfig.AutoCopyNotionLinkDesc, 'toggle', i18nConfig.AutoCopyNotionLink, this.plugin.settings.autoCopyNotionLink, 'autoCopyNotionLink') + this.createSettingEl(containerEl, i18nConfig.AutoSync, i18nConfig.AutoSyncDesc, 'toggle', i18nConfig.AutoSync, this.plugin.settings.autoSync, 'autoSync') new Setting(containerEl) diff --git a/src/upload/updateYaml.ts b/src/upload/updateYaml.ts index 4545c3e..85cf579 100644 --- a/src/upload/updateYaml.ts +++ b/src/upload/updateYaml.ts @@ -43,10 +43,13 @@ export async function updateYamlInfo( ); }); - try { - await navigator.clipboard.writeText(url) - } catch (error) { - console.log(error) - new Notice(`${i18nConfig.CopyErrorMessage}`); + // copy url to clipboard only if autoCopyNotionLink is enabled + if (plugin.settings.autoCopyNotionLink) { + try { + await navigator.clipboard.writeText(url); + } catch (error) { + console.log(error); + new Notice(`${i18nConfig.CopyErrorMessage}`); + } } } From 3de92d3f548bb52b1b7ce6385be431214db8eccb Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Wed, 10 Dec 2025 21:32:25 +0000 Subject: [PATCH 21/32] feat: Enable first-time auto-upload for new documents and update documentation. --- src/main.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main.ts b/src/main.ts index 4d66e07..32ae6ea 100644 --- a/src/main.ts +++ b/src/main.ts @@ -371,7 +371,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { dbByShortName.set(dbDetails.abName.toLowerCase(), dbDetails); } - const foundDatabases: Array<{ dbDetails: DatabaseDetails, notionId: string }> = []; + const foundDatabases: Array<{ dbDetails: DatabaseDetails, notionId: string | undefined }> = []; const unresolvedTargets: string[] = []; for (const target of autoSyncTargets) { @@ -384,22 +384,20 @@ export default class ObsidianSyncNotionPlugin extends Plugin { } const notionIDKey = `NotionID-${dbDetails.abName}`; - if (frontMatter[notionIDKey]) { - foundDatabases.push({ - dbDetails, - notionId: String(frontMatter[notionIDKey]) - }); - } + // Include database even if no NotionID exists (for first-time upload) + foundDatabases.push({ + dbDetails, + notionId: frontMatter[notionIDKey] ? String(frontMatter[notionIDKey]) : undefined + }); } if (unresolvedTargets.length > 0) { console.log(`[AutoSync] Frontmatter auto sync targets not found in settings: ${unresolvedTargets.join(", ")}`); } - // If no NotionID found, notify user to upload manually first + // If no valid databases found in settings, skip if (foundDatabases.length === 0) { - console.log(`[AutoSync] No NotionID found in ${file.path}, skipping auto sync`); - new Notice(i18nConfig.AutoSyncNoNotionID, 4000); + console.log(`[AutoSync] No matching databases found in settings for ${file.path}, skipping auto sync`); return; } @@ -407,12 +405,13 @@ export default class ObsidianSyncNotionPlugin extends Plugin { if (foundDatabases.length > 1) { const message = i18nConfig.AutoSyncMultipleSync.replace('{count}', String(foundDatabases.length)); new Notice(message, 3000); - console.log(`[AutoSync] Found ${foundDatabases.length} NotionIDs in ${file.path}`); + console.log(`[AutoSync] Found ${foundDatabases.length} database targets in ${file.path}`); } // Sync to all found databases for (const { dbDetails, notionId } of foundDatabases) { - console.log(`[AutoSync] ${new Date().toISOString()} Auto syncing ${file.basename} to ${dbDetails.fullName} (${dbDetails.abName})`); + const isFirstSync = !notionId; + console.log(`[AutoSync] ${new Date().toISOString()} Auto syncing ${file.basename} to ${dbDetails.fullName} (${dbDetails.abName})${isFirstSync ? ' [First Upload]' : ''}`); try { // Trigger appropriate upload command based on database format From eeaf7c036d3a0e285e2df585234fbb5c9d4c4950 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Wed, 10 Dec 2025 21:51:04 +0000 Subject: [PATCH 22/32] feat: Enable first-time auto-upload for new documents with `autosync-database` key and refine auto-sync notice logic. --- docs/en/04-sync.md | 50 ++++++++++++++++++++--------- docs/zh/04-sync.md | 78 +++++++++++++++++++++++++++++++++++++++------- src/main.ts | 25 +++++++++------ 3 files changed, 117 insertions(+), 36 deletions(-) diff --git a/docs/en/04-sync.md b/docs/en/04-sync.md index 36e521f..a7a67a8 100644 --- a/docs/en/04-sync.md +++ b/docs/en/04-sync.md @@ -39,19 +39,21 @@ autosync-database: [blog, portfolio] --- ``` -If you change the key name in the settings, update your frontmatter to match. The plugin will prompt you when the entry is missing. +If you change the key name in the settings, update your frontmatter to match. ### How Auto Sync Works When auto sync is enabled: - The plugin monitors markdown files for changes - After you stop editing for the configured delay period, auto sync is triggered -- Only files that have already been synced to Notion (have a NotionID in frontmatter) will be auto-synced +- Files with the `autosync-database` key in frontmatter will be automatically synced +- **First-time upload is supported**: No need to manually sync first - just add the frontmatter key and the plugin will handle the initial upload - If a file is linked to multiple databases, it will sync to all of them automatically +- After the first sync, a `NotionID-{database}` will be added to the frontmatter for future updates ### Auto Sync Scenarios -#### Scenario A: Missing Auto Sync Entry +#### Scenario A-1: New File Missing Auto Sync Entry ```yaml --- @@ -61,25 +63,42 @@ tags: [blog, tech] ``` **Behavior:** -- ✅ Detects that the configured auto sync key is missing -- ✅ Shows notice: `⚠️ Auto sync skipped: Add a "{key}" entry in the frontmatter to choose databases` -- ✅ No sync operation performed -- 📝 **Action Required:** Add the configured key (default `autosync-database`) to the frontmatter with the database abbreviations +- ✅ Detects that the auto sync key is missing +- ✅ Detects no NotionID present (new file) +- ✅ Silently skips - no notice shown +- 📝 **To enable auto sync:** Add `autosync-database: [your-db-abbreviation]` to the frontmatter -#### Scenario B: New Document (Not Yet Synced) +#### Scenario A-2: Synced File Missing Auto Sync Entry ```yaml --- -title: My New Article -aytosync-database: [blog] +title: My Article +NotionID-blog: abc123 --- ``` **Behavior:** -- ✅ Detects no NotionID present -- ✅ Shows notice: "⚠️ Auto sync skipped: This document has not been synced to Notion, please upload manually first" +- ✅ Detects that the auto sync key is missing +- ✅ Detects existing NotionID (file was synced before) +- ✅ Shows notice: "⚠️ Auto-sync skipped: Add autosync-database to your frontmatter to specify target databases" - ✅ No sync operation performed -- 📝 **Action Required:** Manually sync the document first using the command palette +- 📝 **Action Required:** Add `autosync-database: [blog]` to the frontmatter + +#### Scenario B: New Document (First-Time Auto Upload) + +```yaml +--- +title: My New Article +autosync-database: [blog] +--- +``` + +**Behavior:** +- ✅ Detects no NotionID present but `autosync-database` is configured +- ✅ Automatically performs first-time upload to the Blog database +- ✅ Adds `NotionID-blog: xxx` to the frontmatter after successful upload +- ✅ Shows success/failure notification +- 📝 **No Action Required:** The plugin handles the initial upload automatically #### Scenario C: Synced to One Database @@ -105,11 +124,12 @@ title: My Article NotionID-blog: abc123 NotionID-portfolio: def456 NotionID-notes: ghi789 +autosync-database: [blog, portfolio, notes] --- ``` **Behavior:** -- ✅ Detects 3 NotionIDs +- ✅ Detects 3 database targets - ✅ Shows notice: "🔄 Auto sync: Syncing to 3 database(s)..." - ✅ Syncs to all 3 databases sequentially - ✅ Shows individual result notifications for each database @@ -132,7 +152,7 @@ NotionID-portfolio: def456 - 📝 **Remember:** Update both the setting and your frontmatter if you rename the key ### Auto Sync Best Practices -1. **First Sync Manually**: Always perform the first sync manually to establish the NotionID link +1. **Add Frontmatter Key**: Just add `autosync-database: [your-db]` to enable auto sync - no manual upload needed 2. **Configure Delay Appropriately**: Set a longer delay (5-10 seconds) if you make frequent edits 3. **Monitor Sync Status**: Check the notifications to ensure syncs complete successfully 4. **Check Logs**: Open the developer console (Ctrl+Shift+I / Cmd+Option+I) to view detailed sync logs diff --git a/docs/zh/04-sync.md b/docs/zh/04-sync.md index 4f1b0f5..b8f59fc 100644 --- a/docs/zh/04-sync.md +++ b/docs/zh/04-sync.md @@ -22,18 +22,38 @@ description: 如何使用 NotionNext 插件将你的 Obsidian 笔记同步到 No 3. 开启该开关 4. 配置"自动同步延迟时间"(默认:5秒,最小:2秒) +### 准备 Frontmatter + +自动同步会读取你在 **设置 → 自动同步 Frontmatter 键名** 中配置的键名(默认为 `autosync-database`)来确定要同步的数据库。要让你的笔记能够自动同步: + +- 在笔记的 frontmatter 中添加配置的键名 +- 列出一个或多个你在插件设置中定义的数据库简称 +- 如果修改了笔记要同步的数据库,记得更新列表 + +示例(使用默认键名): + +```yaml +--- +title: 我的文章 +autosync-database: [blog, portfolio] +--- +``` + +如果你在设置中修改了键名,记得同时更新你的 frontmatter。 + ### 自动同步工作原理 当自动同步启用后: - 插件会监控 Markdown 文件的变化 - 在你停止编辑达到配置的延迟时间后,自动触发同步 -- 只有已经同步过的文件(frontmatter 中有 NotionID)才会被自动同步 +- **支持首次自动上传**:无需先手动同步,只要添加 frontmatter 键名,插件会自动处理首次上传 - 如果文件关联了多个数据库,会自动同步到所有数据库 +- 首次同步后,会自动在 frontmatter 中添加 `NotionID-{数据库}` 用于后续更新 ### 自动同步场景示例 -#### 场景 A:新文档(未同步) +#### 场景 A-1:新文件缺少自动同步配置 ```yaml --- @@ -44,12 +64,12 @@ tags: [博客, 技术] **行为:** -- ✅ 检测到没有 NotionID -- ✅ 显示提示:"⚠️ 自动同步跳过:此文档未同步到 Notion,请先手动上传" -- ✅ 不执行同步操作 -- 📝 **需要操作:** 先使用命令面板手动同步文档 +- ✅ 检测到缺少自动同步配置键 +- ✅ 检测到没有 NotionID(新文件) +- ✅ 静默跳过,不显示任何提示 +- 📝 **如需启用自动同步:** 在 frontmatter 中添加 `autosync-database: [你的数据库简称]` -#### 场景 B:已同步到一个数据库 +#### 场景 A-2:已同步文件缺少自动同步配置 ```yaml --- @@ -60,12 +80,47 @@ NotionID-blog: abc123 **行为:** +- ✅ 检测到缺少自动同步配置键 +- ✅ 检测到已存在 NotionID(说明之前同步过) +- ✅ 显示提示:"⚠️ 自动同步已跳过:请在 frontmatter 中添加 autosync-database 以指定目标数据库" +- ✅ 不执行同步操作 +- 📝 **需要操作:** 在 frontmatter 中添加 `autosync-database: [blog]` + +#### 场景 B:新文档(首次自动上传) + +```yaml +--- +title: 我的新文章 +autosync-database: [blog] +--- +``` + +**行为:** + +- ✅ 检测到没有 NotionID,但配置了 `autosync-database` +- ✅ 自动执行首次上传到 Blog 数据库 +- ✅ 上传成功后自动添加 `NotionID-blog: xxx` 到 frontmatter +- ✅ 显示成功/失败通知 +- 📝 **无需操作:** 插件会自动处理首次上传 + +#### 场景 C:已同步到一个数据库(更新) + +```yaml +--- +title: 我的文章 +NotionID-blog: abc123 +autosync-database: [blog] +--- +``` + +**行为:** + - ✅ 检测到 1 个 NotionID -- ✅ 自动同步到 Blog 数据库 +- ✅ 自动同步更新到 Blog 数据库 - ✅ 显示上传命令返回的成功/失败通知 - 📝 **无需操作:** 变更会自动同步 -#### 场景 C:同步到多个数据库 +#### 场景 D:同步到多个数据库 ```yaml --- @@ -73,12 +128,13 @@ title: 我的文章 NotionID-blog: abc123 NotionID-portfolio: def456 NotionID-notes: ghi789 +autosync-database: [blog, portfolio, notes] --- ``` **行为:** -- ✅ 检测到 3 个 NotionID +- ✅ 检测到 3 个数据库目标 - ✅ 显示提示:"🔄 自动同步:正在同步到 3 个数据库..." - ✅ 依次同步到所有 3 个数据库 - ✅ 为每个数据库显示独立的结果通知 @@ -86,7 +142,7 @@ NotionID-notes: ghi789 ### 自动同步最佳实践 -1. **首次手动同步**:始终先手动执行第一次同步以建立 NotionID 链接 +1. **添加 Frontmatter 配置**:只需添加 `autosync-database: [你的数据库]` 即可启用自动同步,无需手动上传 2. **合理配置延迟**:如果你经常编辑,设置较长的延迟时间(5-10 秒) 3. **监控同步状态**:注意查看通知以确保同步成功完成 4. **查看日志**:打开开发者控制台(Ctrl+Shift+I / Cmd+Option+I)查看详细的同步日志 diff --git a/src/main.ts b/src/main.ts index 32ae6ea..4b01978 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,7 +18,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { private syncingFiles: Set = new Set(); private lastFrontmatterCache: Map = new Map(); private lastContentHashCache: Map = new Map(); - private missingAutoSyncNoticeShown: Set = new Set(); + async onload() { await this.loadSettings(); @@ -69,7 +69,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { } this.lastFrontmatterCache.clear(); this.lastContentHashCache.clear(); - this.missingAutoSyncNoticeShown.clear(); + } async loadSettings() { @@ -170,9 +170,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { } } - resetAutoSyncNoticeCache() { - this.missingAutoSyncNoticeShown.clear(); - } + getAutoSyncFrontmatterKey(): string { return resolveAutoSyncKey(this.settings.autoSyncFrontmatterKey); @@ -353,17 +351,24 @@ export default class ObsidianSyncNotionPlugin extends Plugin { const autoSyncKey = this.getAutoSyncFrontmatterKey(); const autoSyncTargets = parseAutoSyncDatabaseList(frontMatter[autoSyncKey]); + // If no autosync-database field specified if (autoSyncTargets.length === 0) { - if (!this.missingAutoSyncNoticeShown.has(file.path)) { + // Check if file has any existing NotionID (meaning it was synced before) + const hasExistingNotionID = Object.keys(frontMatter).some(key => + key.startsWith('NotionID-') || key === 'NotionID' + ); + + if (hasExistingNotionID) { + // User likely forgot to add autosync-database - show a notice const message = i18nConfig.AutoSyncMissingDatabaseList.replace('{key}', autoSyncKey); - new Notice(message, 10000); - this.missingAutoSyncNoticeShown.add(file.path); + new Notice(message, 8000); + console.log(`[AutoSync] File ${file.path} has NotionID but missing autosync-database - showing notice`); } - console.log(`[AutoSync] No auto sync database list found in ${file.path} using key "${autoSyncKey}"`); + // Silently skip files without NotionID (new files that don't need auto-sync) return; } - this.missingAutoSyncNoticeShown.delete(file.path); + const dbByShortName = new Map(); for (const key in this.settings.databaseDetails) { From 5806a2831b7866833e79a47bdadeca5947e91023 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Wed, 10 Dec 2025 22:51:08 +0000 Subject: [PATCH 23/32] fix: remove unnecessary `resetAutoSyncNoticeCache` call when `autoSyncFrontmatterKey` changes --- src/ui/settingTabs.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ui/settingTabs.ts b/src/ui/settingTabs.ts index 7b7147f..b4557cf 100644 --- a/src/ui/settingTabs.ts +++ b/src/ui/settingTabs.ts @@ -111,7 +111,6 @@ export class ObsidianSettingTab extends PluginSettingTab { .onChange(async (value) => { this.plugin.settings.autoSyncFrontmatterKey = value; await this.plugin.saveSettings(); - this.plugin.resetAutoSyncNoticeCache(); }) ); @@ -137,7 +136,7 @@ export class ObsidianSettingTab extends PluginSettingTab { } }) ); - + // Set initial visibility this.updateAutoSyncDelayVisibility(); @@ -239,7 +238,7 @@ export class ObsidianSettingTab extends PluginSettingTab { this.plugin.settings[settingsKey] = value; // Update the plugin settings directly await this.plugin.saveSettings(); await this.plugin.commands.updateCommand(); - + // If autoSync setting changed, update the listener and visibility if (settingsKey === 'autoSync') { this.plugin.setupAutoSync(); From 5144f10e779e565d5bba886e430c838dc575ca9f Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Wed, 10 Dec 2025 23:04:58 +0000 Subject: [PATCH 24/32] feat: Add auto-copy Notion link setting --- src/main.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main.ts b/src/main.ts index 4b01978..9a0943a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -92,6 +92,9 @@ export default class ObsidianSyncNotionPlugin extends Plugin { if (typeof this.settings.NotionLinkDisplay !== 'boolean') { this.settings.NotionLinkDisplay = DEFAULT_SETTINGS.NotionLinkDisplay; } + if (typeof this.settings.autoCopyNotionLink !== 'boolean') { + this.settings.autoCopyNotionLink = DEFAULT_SETTINGS.autoCopyNotionLink; + } if (typeof this.settings.autoSyncFrontmatterKey !== 'string') { this.settings.autoSyncFrontmatterKey = DEFAULT_AUTO_SYNC_DATABASE_KEY; } @@ -107,6 +110,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { loadedData.autoSync === undefined || loadedData.autoSyncDelay === undefined || loadedData.NotionLinkDisplay === undefined || + loadedData.autoCopyNotionLink === undefined || loadedData.autoSyncFrontmatterKey === undefined; if (needsSave) { @@ -117,6 +121,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { if (loadedData.autoSync === undefined) migratedFields.push('autoSync'); if (loadedData.autoSyncDelay === undefined) migratedFields.push('autoSyncDelay'); if (loadedData.NotionLinkDisplay === undefined) migratedFields.push('NotionLinkDisplay'); + if (loadedData.autoCopyNotionLink === undefined) migratedFields.push('autoCopyNotionLink'); console.log('[Settings] Migrating settings, adding fields:', migratedFields.join(', ')); } @@ -158,6 +163,10 @@ export default class ObsidianSyncNotionPlugin extends Plugin { console.warn('[Settings] Invalid NotionLinkDisplay value, resetting to default'); this.settings.NotionLinkDisplay = DEFAULT_SETTINGS.NotionLinkDisplay; } + if (typeof this.settings.autoCopyNotionLink !== 'boolean') { + console.warn('[Settings] Invalid autoCopyNotionLink value, resetting to default'); + this.settings.autoCopyNotionLink = DEFAULT_SETTINGS.autoCopyNotionLink; + } if (!this.settings.databaseDetails || typeof this.settings.databaseDetails !== 'object') { console.warn('[Settings] Invalid databaseDetails, resetting to empty object'); this.settings.databaseDetails = {}; From 7053a74e242881d2cccabd6aa04ecc71f497e951 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Wed, 10 Dec 2025 23:05:07 +0000 Subject: [PATCH 25/32] feat: Add 'sync-preffix' translation key to multiple locales. --- src/lang/locale/en.ts | 1 + src/lang/locale/ja.ts | 97 ++++++++++++++++++++++--------------------- src/lang/locale/zh.ts | 1 + 3 files changed, 51 insertions(+), 48 deletions(-) diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 64e1597..ba2bd32 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -60,6 +60,7 @@ export const en = { NotYetFinish: "This feature will be available in a future version.", PlaceHolder: "Enter database name", "notion-logo": "Sync to NotionNext", + "sync-preffix": "📄", "sync-success": "Successfully synced to NotionNext:\n", "sync-fail": "Failed to sync to NotionNext:\n", "open-notion": "Please open a file to sync first.", diff --git a/src/lang/locale/ja.ts b/src/lang/locale/ja.ts index f57a0f5..44ca4ef 100644 --- a/src/lang/locale/ja.ts +++ b/src/lang/locale/ja.ts @@ -10,27 +10,27 @@ export const ja = { databaseAbbreviateName: "データベースの略称", databaseAbbreviateNameDesc: "データベースの略称を設定します。", databaseAbbreviateNameText: "データベースの略称を入力", - ribbonIcon: "NotionNextへ同期", - GeneralSetting: "一般設定", - CommandID: "share-to-notionnext", - CommandName: "NotionNextへ同期", - CommandIDGeneral: "share-to-notion", - CommandNameGeneral: "一般データベースへ同期", - NotionNextButton: "NotionNext同期", - NotionNextButtonDesc: "有効にすると、コマンドパレットに「NotionNextへ同期」が表示されます(デフォルト:オン)。", - NotionNextSettingHeader: "NotionNextデータベース設定", - NotionAPI: "Notion API トークン", - NotionAPIDesc: "notion.so/my-integrations から取得します。", - NotionAPIText: "Notion API トークンを入力", - DatabaseID: "データベースID", + ribbonIcon: "NotionNextへ同期", + GeneralSetting: "一般設定", + CommandID: "share-to-notionnext", + CommandName: "NotionNextへ同期", + CommandIDGeneral: "share-to-notion", + CommandNameGeneral: "一般データベースへ同期", + NotionNextButton: "NotionNext同期", + NotionNextButtonDesc: "有効にすると、コマンドパレットに「NotionNextへ同期」が表示されます(デフォルト:オン)。", + NotionNextSettingHeader: "NotionNextデータベース設定", + NotionAPI: "Notion API トークン", + NotionAPIDesc: "notion.so/my-integrations から取得します。", + NotionAPIText: "Notion API トークンを入力", + DatabaseID: "データベースID", DatabaseIDDesc: "Notionページの右上「共有」メニューから取得します。", - DatabaseIDText: "データベースIDを入力", - BannerUrl: "バナーURL(任意)", - BannerUrlDesc: "空のままにするとバナーは表示されません。表示するには画像のURLを入力してください(例:https://abc.com/b.png)。", - BannerUrlText: "バナーのURLを入力", - NotionUser: "Notionユーザー名(任意)", - NotionUserDesc: "共有リンクが `username.notion.site` の場合、Notionユーザー名は `[username]` です。", - NotionUserText: "Notionユーザー名を入力", + DatabaseIDText: "データベースIDを入力", + BannerUrl: "バナーURL(任意)", + BannerUrlDesc: "空のままにするとバナーは表示されません。表示するには画像のURLを入力してください(例:https://abc.com/b.png)。", + BannerUrlText: "バナーのURLを入力", + NotionUser: "Notionユーザー名(任意)", + NotionUserDesc: "共有リンクが `username.notion.site` の場合、Notionユーザー名は `[username]` です。", + NotionUserText: "Notionユーザー名を入力", NotionLinkDisplay: "Notionリンク表示", NotionLinkDisplayDesc: "デフォルトで有効。無効にすると、同期後にfront matterへNotionリンクが追加されません。", AutoCopyNotionLink: "Notionリンクを自動コピー", @@ -42,33 +42,34 @@ export const ja = { AutoSyncDelay: "自動同期の遅延(秒)", AutoSyncDelayDesc: "変更が検知されてから同期を開始するまでの遅延時間(秒)。同期の頻発を防ぎます(デフォルト:5秒、最小:2秒)。", AutoSyncDelayText: "遅延秒数を入力", - NotionGeneralSettingHeader: "一般Notionデータベース設定", - NotionGeneralButton: "一般データベース同期", - NotionGeneralButtonDesc: "有効にすると、コマンドパレットに「一般データベースへ同期」が表示されます(デフォルト:オン)。", - NotionTagButton: "タグを同期", - NotionTagButtonDesc: "ObsidianのタグをNotionデータベースに同期します(デフォルト:オン)。", - NotionCustomTitle: "タイトルプロパティをカスタム", - NotionCustomTitleDesc: "Notionデータベースのタイトル列の名前をカスタマイズします(デフォルト:オフ)。", - NotionCustomTitleName: "カスタムタイトル名", - NotionCustomTitleNameDesc: "Notionデータベースのタイトル列に使用するカスタム名を入力してください(デフォルト:「title」)。", - NotionCustomTitleText: "プロパティ名を入力", - NotionCustomValues: "カスタムプロパティ", - NotionCustomValuesDesc: "Notionデータベースに同期するカスタムプロパティを1行に1つずつ定義します。", - NotionCustomValuesText: "同期したいすべてのプロパティを入力", - NotYetFinish: "この機能は将来のバージョンで利用可能になります。", - PlaceHolder: "データベース名を入力", - "notion-logo": "NotionNextへ同期", - "sync-success": "NotionNextへの同期が成功しました。\n", - "sync-fail": "NotionNextへの同期に失敗しました。\n", - "open-notion": "同期するファイルを先に開いてください。", - "config-secrets-notion-api": "プラグイン設定でNotion APIキーを設定してください。", - "config-secrets-database-id": "プラグイン設定でデータベースIDを設定してください。", - "set-tags-fail": "タグの設定に失敗しました。frontmatterを確認するか、設定でタグ同期を無効にしてください。", - NNonMissing: "'NNon'プロパティが設定されていません。設定でNotionNextデータベースを選択してください。", - "set-api-id": "プラグイン設定でNotion APIキーとデータベースIDを設定してください。", - NotionCustomSettingHeader: "Notionカスタムデータベース設定", - NotionCustomButton: "カスタムデータベースコマンドを有効化", - NotionCustomButtonDesc: "有効にすると、「カスタムデータベースへ同期」コマンドがコマンドパレットに表示されます。", + NotionGeneralSettingHeader: "一般Notionデータベース設定", + NotionGeneralButton: "一般データベース同期", + NotionGeneralButtonDesc: "有効にすると、コマンドパレットに「一般データベースへ同期」が表示されます(デフォルト:オン)。", + NotionTagButton: "タグを同期", + NotionTagButtonDesc: "ObsidianのタグをNotionデータベースに同期します(デフォルト:オン)。", + NotionCustomTitle: "タイトルプロパティをカスタム", + NotionCustomTitleDesc: "Notionデータベースのタイトル列の名前をカスタマイズします(デフォルト:オフ)。", + NotionCustomTitleName: "カスタムタイトル名", + NotionCustomTitleNameDesc: "Notionデータベースのタイトル列に使用するカスタム名を入力してください(デフォルト:「title」)。", + NotionCustomTitleText: "プロパティ名を入力", + NotionCustomValues: "カスタムプロパティ", + NotionCustomValuesDesc: "Notionデータベースに同期するカスタムプロパティを1行に1つずつ定義します。", + NotionCustomValuesText: "同期したいすべてのプロパティを入力", + NotYetFinish: "この機能は将来のバージョンで利用可能になります。", + PlaceHolder: "データベース名を入力", + "notion-logo": "NotionNextへ同期", + "sync-preffix": "📄", + "sync-success": "NotionNextへの同期が成功しました。\n", + "sync-fail": "NotionNextへの同期に失敗しました。\n", + "open-notion": "同期するファイルを先に開いてください。", + "config-secrets-notion-api": "プラグイン設定でNotion APIキーを設定してください。", + "config-secrets-database-id": "プラグイン設定でデータベースIDを設定してください。", + "set-tags-fail": "タグの設定に失敗しました。frontmatterを確認するか、設定でタグ同期を無効にしてください。", + NNonMissing: "'NNon'プロパティが設定されていません。設定でNotionNextデータベースを選択してください。", + "set-api-id": "プラグイン設定でNotion APIキーとデータベースIDを設定してください。", + NotionCustomSettingHeader: "Notionカスタムデータベース設定", + NotionCustomButton: "カスタムデータベースコマンドを有効化", + NotionCustomButtonDesc: "有効にすると、「カスタムデータベースへ同期」コマンドがコマンドパレットに表示されます。", CustomPropertyName: "プロパティ名", CustomPropertyFirstColumn: "タイトルプロパティ名", CustomPropertyFirstColumnDesc: "ページのタイトル。これはリストの最初のプロパティである必要があります。", @@ -76,7 +77,7 @@ export const ja = { AddCustomProperty: "カスタムプロパティを追加", AddNewProperty: "新しいプロパティを追加", AddNewPropertyDesc: "Notionデータベースのプロパティと一致する新しいプロパティを追加します。", - CopyErrorMessage: "リンクの自動コピーに失敗しました。手動でコピーしてください。", + CopyErrorMessage: "リンクの自動コピーに失敗しました。手動でコピーしてください。", BlockUploaded: "すべてのブロックをアップロードしました", ExtraBlockUploaded: "追加のブロックをアップロードしました", CheckConsole: "詳細は、開発者コンソール(opt+cmd+i または ctrl+shift+i)で確認できます。", diff --git a/src/lang/locale/zh.ts b/src/lang/locale/zh.ts index 402a482..072cf82 100644 --- a/src/lang/locale/zh.ts +++ b/src/lang/locale/zh.ts @@ -60,6 +60,7 @@ export const zh = { NotYetFinish: "此功能将在未来版本中提供", PlaceHolder: "输入数据库名称", "notion-logo": "同步到 NotionNext", + "sync-preffix": "📄", "sync-success": "成功同步到 NotionNext:\n", "sync-fail": "同步到 NotionNext 失败:\n", "open-file": "请先打开要同步的文件。", From 90dcc1aef01d282416087dbe806890baa284edf7 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Wed, 10 Dec 2025 23:15:18 +0000 Subject: [PATCH 26/32] chore: release v2.8.0-beta.3 --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd0bae6..d91e95c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## v2.8.0-beta.3 (2025-12-10) + +### Added + +- **Auto-copy Notion Link setting**: New toggle to automatically copy the Notion page link to clipboard after syncing (defaults to on) +- **Smart auto-sync notice**: Show notice only for files that were previously synced but missing `autosync-database` field; new files are silently skipped + +### Fixed + +- Fixed `undefined` appearing in sync success notification by adding missing `sync-preffix` i18n key +- Fixed build error caused by removed `resetAutoSyncNoticeCache()` method reference +- Added `autoCopyNotionLink` to settings migration logic for seamless upgrades + +### Changed + +- Improved auto-sync behavior: files without `autosync-database` are now silently ignored unless they have an existing NotionID +- Updated documentation with new auto-sync scenarios (A-1 and A-2) + +--- + ## v2.8.0-beta.2 (2025-11-05) ### Featured From ccfe40c1f0d5ad48858cf867a7f31e97b2269b5f Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Wed, 10 Dec 2025 23:18:03 +0000 Subject: [PATCH 27/32] fix: Refine release workflow tag patterns to exclude prerelease versions. --- .github/workflows/release.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5132112..e39c7f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,8 +5,14 @@ on: branches: - main tags: - - "v*.*.*" - - "[0-9]+.[0-9]+.[0-9]+" + # Only trigger for release tags (e.g., v1.0.0, v2.3.4) + # Excludes prerelease tags (e.g., v2.8.0-beta.3, v1.0.0-rc.1) + - "v[0-9]+.[0-9]+.[0-9]" + - "v[0-9]+.[0-9]+.[0-9][0-9]" + - "v[0-9]+.[0-9]+.[0-9][0-9][0-9]" + - "v[0-9]+.[0-9][0-9].[0-9]" + - "v[0-9]+.[0-9][0-9].[0-9][0-9]" + - "v[0-9]+.[0-9][0-9].[0-9][0-9][0-9]" env: PLUGIN_NAME: share-to-notionnext # Change this to match the id of your plugin. From 876b6233cb49efb4503addfdf423334018ccfdf0 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Sun, 4 Jan 2026 19:10:59 +0000 Subject: [PATCH 28/32] docs: Enhance attachment upload documentation with supported formats and auto sync details --- docs/en/04-sync.md | 110 ++++++++++++++++++++++++++++++--------------- docs/zh/04-sync.md | 109 +++++++++++++++++++++++++++++--------------- 2 files changed, 147 insertions(+), 72 deletions(-) diff --git a/docs/en/04-sync.md b/docs/en/04-sync.md index a7a67a8..c18b653 100644 --- a/docs/en/04-sync.md +++ b/docs/en/04-sync.md @@ -11,6 +11,73 @@ After configuring your Notion database in the plugin settings, you can start syn To sync a note, open the note you want to sync and use the "Share to NotionNext" command from the command palette or the note context menu. This will create a new page in your Notion database with the content of your Obsidian note. +## Attachment Upload + +The plugin automatically detects and uploads local attachments (images, PDFs, etc.) from your notes to Notion. + +### Supported Attachment Formats + +**Image formats:** +- PNG, JPG, JPEG, GIF, WebP, SVG, HEIC, TIF, TIFF, BMP + +**Other formats:** +- PDF + +### Supported Link Formats + +Currently supported: + +#### Wikilink Format (Recommended) + +```markdown +![[image.png]] +![[folder/image.png]] +![[image.png|alt text]] + +[[document.pdf]] +[[folder/document.pdf]] +``` + +#### Standard Markdown Format + +```markdown +![alt text](image.png) +![alt text](folder/image.png) +![](./relative/path/image.png) + +[document.pdf](document.pdf) +[document.pdf](folder/document.pdf) +``` + +#### TODO + +- [ ] Obsidian URL Format + + ```markdown + ![](obsidian://open?vault=MyVault&file=path/to/image.png) + ``` + +- [ ] App URL Format + + ```markdown + ![](app://local/path/to/image.png) + ``` + +### How Attachment Upload Works + +1. **Auto Detection**: During sync, the plugin scans your note content and identifies all local attachment references +2. **Upload to Notion**: Detected attachments are uploaded via the Notion File Upload API +3. **Link Replacement**: After successful upload, local links are replaced with Notion file references +4. **Image Display**: Images are displayed as Notion image blocks +5. **File Embedding**: Non-image files like PDFs are embedded as file blocks + +### Notes + +- Attachment references inside code blocks are not processed +- External URLs (`http://` or `https://`) are not processed +- Single file size limit is 5MB +- Ensure attachment files exist in your Vault + ## Auto Sync The plugin supports automatic syncing that monitors your notes for changes and automatically syncs them to Notion. @@ -45,46 +112,17 @@ If you change the key name in the settings, update your frontmatter to match. When auto sync is enabled: - The plugin monitors markdown files for changes +- **Only files with the auto sync key in frontmatter will be processed** +- Files without the auto sync key are silently skipped - no sync operations or notices +- Files containing internal attachments (local images/PDFs) are skipped - sync them manually - After you stop editing for the configured delay period, auto sync is triggered -- Files with the `autosync-database` key in frontmatter will be automatically synced - **First-time upload is supported**: No need to manually sync first - just add the frontmatter key and the plugin will handle the initial upload - If a file is linked to multiple databases, it will sync to all of them automatically - After the first sync, a `NotionID-{database}` will be added to the frontmatter for future updates ### Auto Sync Scenarios -#### Scenario A-1: New File Missing Auto Sync Entry - -```yaml ---- -title: My New Article -tags: [blog, tech] ---- -``` - -**Behavior:** -- ✅ Detects that the auto sync key is missing -- ✅ Detects no NotionID present (new file) -- ✅ Silently skips - no notice shown -- 📝 **To enable auto sync:** Add `autosync-database: [your-db-abbreviation]` to the frontmatter - -#### Scenario A-2: Synced File Missing Auto Sync Entry - -```yaml ---- -title: My Article -NotionID-blog: abc123 ---- -``` - -**Behavior:** -- ✅ Detects that the auto sync key is missing -- ✅ Detects existing NotionID (file was synced before) -- ✅ Shows notice: "⚠️ Auto-sync skipped: Add autosync-database to your frontmatter to specify target databases" -- ✅ No sync operation performed -- 📝 **Action Required:** Add `autosync-database: [blog]` to the frontmatter - -#### Scenario B: New Document (First-Time Auto Upload) +#### Scenario A: New Document (First-Time Auto Upload) ```yaml --- @@ -100,7 +138,7 @@ autosync-database: [blog] - ✅ Shows success/failure notification - 📝 **No Action Required:** The plugin handles the initial upload automatically -#### Scenario C: Synced to One Database +#### Scenario B: Synced to One Database ```yaml --- @@ -116,7 +154,7 @@ autosync-database: [blog] - ✅ Shows success/failure notification from the upload command - 📝 **No Action Required:** Changes are automatically synced -#### Scenario D: Synced to Multiple Databases +#### Scenario C: Synced to Multiple Databases ```yaml --- @@ -135,7 +173,7 @@ autosync-database: [blog, portfolio, notes] - ✅ Shows individual result notifications for each database - 📝 **No Action Required:** Changes are automatically synced to all linked databases -#### Scenario E: Custom Frontmatter Key +#### Scenario D: Custom Frontmatter Key ```yaml --- diff --git a/docs/zh/04-sync.md b/docs/zh/04-sync.md index b8f59fc..ed188a9 100644 --- a/docs/zh/04-sync.md +++ b/docs/zh/04-sync.md @@ -11,6 +11,73 @@ description: 如何使用 NotionNext 插件将你的 Obsidian 笔记同步到 No 要同步一篇笔记,只需打开你想要同步的笔记,然后从命令面板(`Ctrl/Cmd + P`)或笔记的右键菜单中选择 "Share to NotionNext" 命令。这会在你的 Notion 数据库中创建一个新页面,内容与你的 Obsidian 笔记完全一致。 +## 附件上传 + +插件支持自动检测并上传笔记中的本地附件(图片、PDF 等)到 Notion。 + +### 支持的附件格式 + +**图片格式:** +- PNG, JPG, JPEG, GIF, WebP, SVG, HEIC, TIF, TIFF, BMP + +**其他格式:** +- PDF + +### 支持的链接格式 + +当前支持: + +#### Wikilink 格式(推荐) + +```markdown +![[image.png]] +![[folder/image.png]] +![[image.png|alt text]] + +[[document.pdf]] +[[folder/document.pdf]] +``` + +#### 标准 Markdown 格式 + +```markdown +![alt text](image.png) +![alt text](folder/image.png) +![](./relative/path/image.png) + +[document.pdf](document.pdf) +[document.pdf](folder/document.pdf) +``` + +#### TODO + +- [ ] Obsidian URL 格式 + + ```markdown + ![](obsidian://open?vault=MyVault&file=path/to/image.png) + ``` + +- [ ] App URL 格式 + + ```markdown + ![](app://local/path/to/image.png) + ``` + +### 附件上传工作原理 + +1. **自动检测**:同步时,插件会自动扫描笔记内容,识别所有本地附件引用 +2. **上传到 Notion**:检测到的附件会通过 Notion File Upload API 上传 +3. **链接替换**:上传成功后,笔记中的本地链接会被替换为 Notion 的文件引用 +4. **图片显示**:图片会作为 Notion 的图片块显示 +5. **文件嵌入**:PDF 等非图片文件会作为文件块嵌入 + +### 注意事项 + +- 代码块中的附件引用不会被处理 +- 外部 URL(`http://` 或 `https://`)不会被处理 +- 单个文件大小限制为 5MB +- 确保附件文件存在于 Vault 中 + ## 自动同步 插件支持自动同步功能,可以监控你的笔记变化并自动同步到 Notion。 @@ -46,6 +113,9 @@ autosync-database: [blog, portfolio] 当自动同步启用后: - 插件会监控 Markdown 文件的变化 +- **只有 frontmatter 中包含自动同步配置键的文件才会被处理** +- 没有配置自动同步键的文件会被静默跳过,不会触发任何同步操作或提示 +- 含有本地附件(图片/PDF)的文件会跳过自动同步,请手动同步 - 在你停止编辑达到配置的延迟时间后,自动触发同步 - **支持首次自动上传**:无需先手动同步,只要添加 frontmatter 键名,插件会自动处理首次上传 - 如果文件关联了多个数据库,会自动同步到所有数据库 @@ -53,40 +123,7 @@ autosync-database: [blog, portfolio] ### 自动同步场景示例 -#### 场景 A-1:新文件缺少自动同步配置 - -```yaml ---- -title: 我的新文章 -tags: [博客, 技术] ---- -``` - -**行为:** - -- ✅ 检测到缺少自动同步配置键 -- ✅ 检测到没有 NotionID(新文件) -- ✅ 静默跳过,不显示任何提示 -- 📝 **如需启用自动同步:** 在 frontmatter 中添加 `autosync-database: [你的数据库简称]` - -#### 场景 A-2:已同步文件缺少自动同步配置 - -```yaml ---- -title: 我的文章 -NotionID-blog: abc123 ---- -``` - -**行为:** - -- ✅ 检测到缺少自动同步配置键 -- ✅ 检测到已存在 NotionID(说明之前同步过) -- ✅ 显示提示:"⚠️ 自动同步已跳过:请在 frontmatter 中添加 autosync-database 以指定目标数据库" -- ✅ 不执行同步操作 -- 📝 **需要操作:** 在 frontmatter 中添加 `autosync-database: [blog]` - -#### 场景 B:新文档(首次自动上传) +#### 场景 A:新文档(首次自动上传) ```yaml --- @@ -103,7 +140,7 @@ autosync-database: [blog] - ✅ 显示成功/失败通知 - 📝 **无需操作:** 插件会自动处理首次上传 -#### 场景 C:已同步到一个数据库(更新) +#### 场景 B:已同步到一个数据库(更新) ```yaml --- @@ -120,7 +157,7 @@ autosync-database: [blog] - ✅ 显示上传命令返回的成功/失败通知 - 📝 **无需操作:** 变更会自动同步 -#### 场景 D:同步到多个数据库 +#### 场景 C:同步到多个数据库 ```yaml --- From b61264a1f643237daa38719d11f1113ebdf7e6e1 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Sun, 4 Jan 2026 19:11:07 +0000 Subject: [PATCH 29/32] feat: Add message for skipped attachments during auto-sync in multiple languages --- src/lang/locale/en.ts | 1 + src/lang/locale/ja.ts | 1 + src/lang/locale/zh.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index ba2bd32..fc79ffa 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -91,6 +91,7 @@ export const en = { SettingsMigrated: "✨ Settings updated! Auto-Sync is now available. Check the settings to learn more.", AutoSyncNoNotionID: "🆕 Auto-sync: First upload to Notion", AutoSyncMissingDatabaseList: "⚠️ Auto-sync skipped: Add `{key}: [database_name]` to your frontmatter to specify target databases.", + AutoSyncSkippedAttachments: "⚠️ Auto-sync skipped: {filename} contains internal attachments (images/PDFs). Please sync manually.", AutoSyncMultipleSync: "🔄 Auto-sync: Syncing to {count} database(s)...", AutoSyncFailed: "Auto-sync to {database} failed: {error}", AutoSyncError: "Auto-sync for {filename} failed: {error}", diff --git a/src/lang/locale/ja.ts b/src/lang/locale/ja.ts index 44ca4ef..bc8ddc4 100644 --- a/src/lang/locale/ja.ts +++ b/src/lang/locale/ja.ts @@ -84,6 +84,7 @@ export const ja = { SettingsMigrated: "✨ 設定が更新されました!自動同期が利用可能です。詳細は設定画面をご確認ください。", AutoSyncNoNotionID: "🆕 自動同期:Notionへ初めてアップロードします", AutoSyncMissingDatabaseList: "⚠️ 自動同期をスキップ:frontmatterに `{key}: [データベース名]` を追加して同期先を指定してください。", + AutoSyncSkippedAttachments: "⚠️ 自動同期をスキップ:{filename} に内部添付(画像/PDF)が含まれています。手動で同期してください。", AutoSyncMultipleSync: "🔄 自動同期:{count}個のデータベースに同期しています...", AutoSyncFailed: "{database}への自動同期に失敗しました:{error}", AutoSyncError: "{filename}の自動同期に失敗しました:{error}", diff --git a/src/lang/locale/zh.ts b/src/lang/locale/zh.ts index 072cf82..7e972c2 100644 --- a/src/lang/locale/zh.ts +++ b/src/lang/locale/zh.ts @@ -87,6 +87,7 @@ export const zh = { SettingsMigrated: "✨ 插件设置已更新!新增自动同步功能,详情请查看设置。", AutoSyncNoNotionID: "🆕 自动同步:首次上传到 Notion", AutoSyncMissingDatabaseList: "⚠️ 自动同步已跳过:请在 frontmatter 中添加 \"{key}\" 以指定目标数据库。", + AutoSyncSkippedAttachments: "⚠️ 自动同步已跳过:检测到 {filename} 含有本地附件(图片/PDF),请手动同步。", AutoSyncMultipleSync: "🔄 自动同步:正在同步到 {count} 个数据库...", AutoSyncFailed: "同步到 {database} 失败:{error}", AutoSyncError: "同步 {filename} 失败:{error}", From 9c6980d1c9eba7dbe12953547e08a9d28786d30e Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Sun, 4 Jan 2026 19:11:23 +0000 Subject: [PATCH 30/32] refactor: Simplify auto sync delay setting initialization --- src/ui/settingTabs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/settingTabs.ts b/src/ui/settingTabs.ts index b4557cf..ce758b2 100644 --- a/src/ui/settingTabs.ts +++ b/src/ui/settingTabs.ts @@ -116,7 +116,7 @@ export class ObsidianSettingTab extends PluginSettingTab { // Auto Sync Delay setting - only visible when autoSync is enabled this.autoSyncDelayContainer = containerEl.createDiv(); - const delaySetting = new Setting(this.autoSyncDelayContainer) + new Setting(this.autoSyncDelayContainer) .setName(i18nConfig.AutoSyncDelay) .setDesc(i18nConfig.AutoSyncDelayDesc) .addText((text) => From 202385ac21043249ff0c711c64926924cf6d8cf6 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Sun, 4 Jan 2026 19:11:37 +0000 Subject: [PATCH 31/32] feat: Implement attachment processing and uploading for Notion integration - Added AttachmentProcessor to handle local attachments in markdown content. - Introduced AttachmentUploader for managing file uploads to Notion. - Updated Upload2Notion to utilize new attachment processing and uploading logic. - Enhanced buildBlocks method to process attachments and apply block rewrites. - Updated Notion API version to 2025-09-03 for compatibility with new features. --- src/main.ts | 65 +-- src/upload/Upload2Notion.ts | 25 +- src/upload/common/AttachmentProcessor.ts | 697 +++++++++++++++++++++++ src/upload/common/AttachmentUploader.ts | 256 +++++++++ src/upload/common/UploadBase.ts | 10 +- 5 files changed, 1004 insertions(+), 49 deletions(-) create mode 100644 src/upload/common/AttachmentProcessor.ts create mode 100644 src/upload/common/AttachmentUploader.ts diff --git a/src/main.ts b/src/main.ts index 9a0943a..bc57e3f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import { i18nConfig } from "src/lang/I18n"; import ribbonCommands from "src/commands/NotionCommands"; import { ObsidianSettingTab, PluginSettings, DEFAULT_SETTINGS, DatabaseDetails } from "src/ui/settingTabs"; import { uploadCommandNext, uploadCommandGeneral, uploadCommandCustom } from "src/upload/uploadCommand"; +import { AttachmentProcessor } from "src/upload/common/AttachmentProcessor"; import { DEFAULT_AUTO_SYNC_DATABASE_KEY, parseAutoSyncDatabaseList, resolveAutoSyncKey } from "src/utils/frontmatter"; // Remember to rename these classes and interfaces! @@ -18,6 +19,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { private syncingFiles: Set = new Set(); private lastFrontmatterCache: Map = new Map(); private lastContentHashCache: Map = new Map(); + private autoSyncAttachmentBlocked: Set = new Set(); async onload() { @@ -69,6 +71,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin { } this.lastFrontmatterCache.clear(); this.lastContentHashCache.clear(); + this.autoSyncAttachmentBlocked.clear(); } @@ -315,7 +318,14 @@ export default class ObsidianSyncNotionPlugin extends Plugin { // Get file's frontmatter const frontMatter = this.app.metadataCache.getFileCache(file)?.frontmatter; if (!frontMatter) { - console.log(`[AutoSync] No frontmatter found in ${file.path}`); + return; + } + + // Check autosync property first - only proceed if it exists + const autoSyncKey = this.getAutoSyncFrontmatterKey(); + const autoSyncTargets = parseAutoSyncDatabaseList(frontMatter[autoSyncKey]); + + if (autoSyncTargets.length === 0) { return; } @@ -331,50 +341,11 @@ export default class ObsidianSyncNotionPlugin extends Plugin { const frontmatterOnlyNotionIDChanged = this.onlyNotionIDChanged(lastFrontmatter, frontMatter); const contentUnchanged = contentHash === lastContentHash; - console.log(`[AutoSync] Change analysis for ${file.basename}:`, { - frontmatterOnlyNotionIDChanged, - contentUnchanged, - frontmatterHasRealChanges: !frontmatterOnlyNotionIDChanged, - contentChanged: !contentUnchanged, - willSync: !(frontmatterOnlyNotionIDChanged && contentUnchanged) - }); - - // Only skip sync if BOTH conditions are true: - // 1. Frontmatter only has NotionID changes (no real user changes) - // 2. Content is completely unchanged if (frontmatterOnlyNotionIDChanged && contentUnchanged) { - console.log(`[AutoSync] Only NotionID updated (from sync), content unchanged - skipping auto sync`); - // Update cache even when skipping, so next comparison uses the current state this.lastFrontmatterCache.set(file.path, { ...frontMatter }); this.lastContentHashCache.set(file.path, contentHash); return; } - - if (!contentUnchanged) { - console.log(`[AutoSync] Content changed - will sync`); - } else if (!frontmatterOnlyNotionIDChanged) { - console.log(`[AutoSync] Frontmatter changed - will sync`); - } - } - - const autoSyncKey = this.getAutoSyncFrontmatterKey(); - const autoSyncTargets = parseAutoSyncDatabaseList(frontMatter[autoSyncKey]); - - // If no autosync-database field specified - if (autoSyncTargets.length === 0) { - // Check if file has any existing NotionID (meaning it was synced before) - const hasExistingNotionID = Object.keys(frontMatter).some(key => - key.startsWith('NotionID-') || key === 'NotionID' - ); - - if (hasExistingNotionID) { - // User likely forgot to add autosync-database - show a notice - const message = i18nConfig.AutoSyncMissingDatabaseList.replace('{key}', autoSyncKey); - new Notice(message, 8000); - console.log(`[AutoSync] File ${file.path} has NotionID but missing autosync-database - showing notice`); - } - // Silently skip files without NotionID (new files that don't need auto-sync) - return; } @@ -415,6 +386,20 @@ export default class ObsidianSyncNotionPlugin extends Plugin { return; } + // Temporarily disable auto-sync for files containing internal attachments (local images/PDFs) + const attachmentProcessor = new AttachmentProcessor(this, foundDatabases[0].dbDetails); + if (attachmentProcessor.hasInternalAttachments(content, file)) { + if (!this.autoSyncAttachmentBlocked.has(file.path)) { + const message = i18nConfig.AutoSyncSkippedAttachments + .replace('{filename}', file.basename); + new Notice(message, 6000); + this.autoSyncAttachmentBlocked.add(file.path); + } + console.log(`[AutoSync] Internal attachments detected in ${file.path}, auto-sync skipped`); + return; + } + this.autoSyncAttachmentBlocked.delete(file.path); + // Notify user about multiple syncs if applicable if (foundDatabases.length > 1) { const message = i18nConfig.AutoSyncMultipleSync.replace('{count}', String(foundDatabases.length)); diff --git a/src/upload/Upload2Notion.ts b/src/upload/Upload2Notion.ts index fe2e985..be208c4 100644 --- a/src/upload/Upload2Notion.ts +++ b/src/upload/Upload2Notion.ts @@ -6,6 +6,7 @@ import MyPlugin from "src/main"; import { DatabaseDetails } from "../ui/settingTabs"; import { updateYamlInfo } from "./updateYaml"; import { UploadBase, NotionPageResponse } from "./common/UploadBase"; +import { AttachmentProcessor, applyBlockRewrites } from "./common/AttachmentProcessor"; export type DatasetType = "general" | "next" | "custom"; @@ -128,7 +129,7 @@ export class Upload2Notion extends UploadBase { cover: request.cover, tags: request.tags, }); - const blocks = this.buildBlocks(request.markdown, { + const blocks = await this.buildBlocks(request.markdown, request.nowFile, { notionLimits: {truncate: false}, }); const notionId = this.getNotionId(request.app, request.nowFile); @@ -153,7 +154,7 @@ export class Upload2Notion extends UploadBase { slug: request.slug, category: request.category, }); - const blocks = this.buildBlocks(request.markdown, { + const blocks = await this.buildBlocks(request.markdown, request.nowFile, { notionLimits: {truncate: false}, }); this.splitRichTextParagraphs(blocks); @@ -187,7 +188,7 @@ export class Upload2Notion extends UploadBase { console.log(`[Upload2Notion] Handling custom dataset`, { customKeys: Object.keys(request.customValues || {}), }); - const blocks = this.buildBlocks(request.markdown, { + const blocks = await this.buildBlocks(request.markdown, request.nowFile, { strictImageUrls: true, notionLimits: {truncate: false}, }); @@ -206,10 +207,24 @@ export class Upload2Notion extends UploadBase { }); } - private buildBlocks(markdown: string, options: Record): any[] { + private async buildBlocks(markdown: string, nowFile: TFile, options: Record): Promise { const yamlContent: any = yamlFrontMatter.loadFront(markdown); - const content = yamlContent.__content; + let content: string = yamlContent.__content ?? ""; + + // Process local attachments + const processor = new AttachmentProcessor(this.plugin, this.dbDetails); + const result = await processor.processContent(content, nowFile); + content = result.content; + const imageUrlToUploadId = result.imageUrlToUploadId; + const filePlaceholderToUpload = result.filePlaceholderToUpload; + const blocks = markdownToBlocks(content, options); + + // Apply block rewrites for uploaded files + if (Object.keys(imageUrlToUploadId).length > 0 || Object.keys(filePlaceholderToUpload).length > 0) { + applyBlockRewrites(blocks, { imageUrlToUploadId, filePlaceholderToUpload }); + } + this.debugLog("Upload2Notion", "Converted markdown to blocks", { blockCount: blocks.length, firstBlockTypes: blocks.slice(0, 5).map((block: any) => block?.type), diff --git a/src/upload/common/AttachmentProcessor.ts b/src/upload/common/AttachmentProcessor.ts new file mode 100644 index 0000000..abf1363 --- /dev/null +++ b/src/upload/common/AttachmentProcessor.ts @@ -0,0 +1,697 @@ +import { App, TFile, normalizePath } from "obsidian"; +import { AttachmentUploader } from "./AttachmentUploader"; +import type MyPlugin from "src/main"; +import type { DatabaseDetails } from "../../ui/settingTabs"; + +export interface AttachmentPrepareResult { + content: string; + imageUrlToUploadId: Record; + filePlaceholderToUpload: Record; +} + +interface LocalAttachment { + file: TFile; + originalRef: string; +} + +const IMAGE_EXTENSIONS = new Set([ + "png", "jpg", "jpeg", "gif", "webp", "svg", "heic", "tif", "tiff", "bmp" +]); + +const SUPPORTED_EXTENSIONS = new Set([ + ...IMAGE_EXTENSIONS, "pdf" +]); + +export class AttachmentProcessor { + private plugin: MyPlugin; + private dbDetails: DatabaseDetails; + private uploader: AttachmentUploader; + + constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) { + this.plugin = plugin; + this.dbDetails = dbDetails; + this.uploader = new AttachmentUploader(plugin, dbDetails); + } + + private isStandaloneOnLine(input: string, offset: number, match: string): boolean { + const lineStart = input.lastIndexOf("\n", Math.max(0, offset - 1)) + 1; + const lineEndIdx = input.indexOf("\n", offset + match.length); + const lineEnd = lineEndIdx === -1 ? input.length : lineEndIdx; + const before = input.slice(lineStart, offset).trim(); + const after = input.slice(offset + match.length, lineEnd).trim(); + return before.length === 0 && after.length === 0; + } + + hasInternalAttachments(content: string, sourceFile: TFile): boolean { + const app = this.plugin.app; + const contentWithoutCode = content.replace(/```[\s\S]*?```|`[^`\n]+`/g, ""); + + const embedImageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; + const embedWikilinkRegex = /!\[\[([^\]]+)\]\]/g; + const linkMarkdownRegex = /(? { + console.log(`[AttachmentProcessor] Starting attachment processing for file: ${sourceFile.path}`); + + // Strip code blocks before processing to avoid matching inside them + const codeBlockPlaceholders: string[] = []; + const contentWithoutCode = content.replace(/```[\s\S]*?```|`[^`\n]+`/g, (match) => { + const placeholder = `__CODE_BLOCK_${codeBlockPlaceholders.length}__`; + codeBlockPlaceholders.push(match); + return placeholder; + }); + console.log(`[AttachmentProcessor] Stripped ${codeBlockPlaceholders.length} code blocks`); + + const { internal, external } = this.collectAttachments(contentWithoutCode, sourceFile); + + if (external.length > 0) { + console.log(`[AttachmentProcessor] Found ${external.length} external reference(s) (will be skipped):`); + external.forEach((ref, idx) => { + console.log(` ${idx + 1}. [EXTERNAL] ${ref}`); + }); + } + + if (internal.length === 0) { + console.log(`[AttachmentProcessor] No internal attachments found in ${sourceFile.path}`); + return { + content, + imageUrlToUploadId: {}, + filePlaceholderToUpload: {}, + }; + } + + console.log(`[AttachmentProcessor] Found ${internal.length} internal attachment(s) to upload:`); + internal.forEach((attachment, idx) => { + const typeLabel = this.isImage(attachment.file) ? 'IMAGE' : 'FILE'; + const sizeKB = (attachment.file.stat.size / 1024).toFixed(2); + console.log(` ${idx + 1}. [${typeLabel}] ${attachment.file.path} (${sizeKB} KB) - Ref: "${attachment.originalRef}"`); + }); + + const uploadedMap = new Map(); + + for (const attachment of internal) { + try { + console.log(`[AttachmentProcessor] Uploading: ${attachment.file.path} (${(attachment.file.stat.size / 1024).toFixed(2)} KB)`); + const result = await this.uploader.uploadFile(attachment.file); + uploadedMap.set(attachment.file.path, { id: result.id, file: attachment.file }); + console.log(`[AttachmentProcessor] ✓ Uploaded successfully: ${attachment.file.name} -> ${result.id}`); + } catch (error) { + console.error(`[AttachmentProcessor] ✗ Failed to upload ${attachment.file.path}:`, error); + } + } + + console.log(`[AttachmentProcessor] Upload complete: ${uploadedMap.size}/${internal.length} successful`); + + const rewriteResult = this.rewriteContent(contentWithoutCode, sourceFile, uploadedMap); + + // Restore code blocks + let restoredContent = rewriteResult.content; + codeBlockPlaceholders.forEach((code, idx) => { + restoredContent = restoredContent.replace(`__CODE_BLOCK_${idx}__`, code); + }); + + console.log(`[AttachmentProcessor] Content rewrite complete:`, { + imageReplacements: Object.keys(rewriteResult.imageUrlToUploadId).length, + fileReplacements: Object.keys(rewriteResult.filePlaceholderToUpload).length, + }); + + return { + content: restoredContent, + imageUrlToUploadId: rewriteResult.imageUrlToUploadId, + filePlaceholderToUpload: rewriteResult.filePlaceholderToUpload, + }; + } + + private collectAttachments(content: string, sourceFile: TFile): { internal: LocalAttachment[]; external: string[] } { + const internal: LocalAttachment[] = []; + const external: string[] = []; + const seen = new Set(); + const app = this.plugin.app; + + console.log(`[AttachmentProcessor] Scanning for attachments in content (${content.length} chars)`); + + // Match all types of references: ![...](...) ![[...]] [...](...) [[...]] + const embedImageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; + const embedWikilinkRegex = /!\[\[([^\]]+)\]\]/g; + const linkMarkdownRegex = /(? parsed path: "${rawPath}"`); + + if (this.shouldSkipLinkDestination(rawPath)) { + if (this.isTodoUrl(rawPath)) { + console.log(`[AttachmentProcessor] ⊘ Unsupported URL scheme (TODO, skipped): ${rawPath}`); + } else { + console.log(`[AttachmentProcessor] ⊘ External URL (skipped): ${rawPath}`); + } + external.push(match[0]); + continue; + } + + const file = this.resolveFile(app, sourceFile, rawPath); + if (!file) { + console.log(`[AttachmentProcessor] ✗ Could not resolve file for path: "${rawPath}"`); + } else if (!this.isSupported(file)) { + console.log(`[AttachmentProcessor] ✗ Unsupported file type: ${file.extension} (${file.path})`); + } else if (seen.has(file.path)) { + console.log(`[AttachmentProcessor] ⊚ Duplicate (already added): ${file.path}`); + } else { + seen.add(file.path); + internal.push({ file, originalRef: match[0] }); + console.log(`[AttachmentProcessor] ✓ Added: ${file.path}`); + } + } + + // Process embedded wikilinks: ![[path]] + let wikiEmbedCount = 0; + while ((match = embedWikilinkRegex.exec(content)) !== null) { + wikiEmbedCount++; + const linkPath = this.parseWikilink(match[1]); + console.log(`[AttachmentProcessor] Found embedded wikilink #${wikiEmbedCount}: "${match[0]}" -> parsed path: "${linkPath}"`); + + if (this.shouldSkipLinkDestination(linkPath)) { + if (this.isTodoUrl(linkPath)) { + console.log(`[AttachmentProcessor] ⊘ Unsupported URL scheme (TODO, skipped): ${linkPath}`); + } else { + console.log(`[AttachmentProcessor] ⊘ External URL (skipped): ${linkPath}`); + } + external.push(match[0]); + continue; + } + + const file = this.resolveFile(app, sourceFile, linkPath); + if (!file) { + console.log(`[AttachmentProcessor] ✗ Could not resolve file for path: "${linkPath}"`); + } else if (!this.isSupported(file)) { + console.log(`[AttachmentProcessor] ✗ Unsupported file type: ${file.extension} (${file.path})`); + } else if (seen.has(file.path)) { + console.log(`[AttachmentProcessor] ⊚ Duplicate (already added): ${file.path}`); + } else { + seen.add(file.path); + internal.push({ file, originalRef: match[0] }); + console.log(`[AttachmentProcessor] ✓ Added: ${file.path}`); + } + } + + // Process markdown links: [text](path) + let linkCount = 0; + while ((match = linkMarkdownRegex.exec(content)) !== null) { + linkCount++; + const rawPath = this.parseDestination(match[2]); + console.log(`[AttachmentProcessor] Found markdown link #${linkCount}: "${match[0]}" -> parsed path: "${rawPath}"`); + + if (this.shouldSkipLinkDestination(rawPath)) { + if (this.isTodoUrl(rawPath)) { + console.log(`[AttachmentProcessor] ⊘ Unsupported URL scheme (TODO, skipped): ${rawPath}`); + } else { + console.log(`[AttachmentProcessor] ⊘ External URL (skipped): ${rawPath}`); + } + external.push(match[0]); + continue; + } + + const file = this.resolveFile(app, sourceFile, rawPath); + if (!file) { + console.log(`[AttachmentProcessor] ✗ Could not resolve file for path: "${rawPath}"`); + } else if (!this.isSupported(file)) { + console.log(`[AttachmentProcessor] ✗ Unsupported file type: ${file.extension} (${file.path})`); + } else if (seen.has(file.path)) { + console.log(`[AttachmentProcessor] ⊚ Duplicate (already added): ${file.path}`); + } else { + seen.add(file.path); + internal.push({ file, originalRef: match[0] }); + console.log(`[AttachmentProcessor] ✓ Added: ${file.path}`); + } + } + + // Process wikilink references: [[path]] + let wikilinkCount = 0; + while ((match = linkWikilinkRegex.exec(content)) !== null) { + wikilinkCount++; + const linkPath = this.parseWikilink(match[1]); + console.log(`[AttachmentProcessor] Found wikilink reference #${wikilinkCount}: "${match[0]}" -> parsed path: "${linkPath}"`); + + if (this.shouldSkipLinkDestination(linkPath)) { + if (this.isTodoUrl(linkPath)) { + console.log(`[AttachmentProcessor] ⊘ Unsupported URL scheme (TODO, skipped): ${linkPath}`); + } else { + console.log(`[AttachmentProcessor] ⊘ External URL (skipped): ${linkPath}`); + } + external.push(match[0]); + continue; + } + + const file = this.resolveFile(app, sourceFile, linkPath); + if (!file) { + console.log(`[AttachmentProcessor] ✗ Could not resolve file for path: "${linkPath}"`); + } else if (!this.isSupported(file)) { + console.log(`[AttachmentProcessor] ✗ Unsupported file type: ${file.extension} (${file.path})`); + } else if (seen.has(file.path)) { + console.log(`[AttachmentProcessor] ⊚ Duplicate (already added): ${file.path}`); + } else { + seen.add(file.path); + internal.push({ file, originalRef: match[0] }); + console.log(`[AttachmentProcessor] ✓ Added: ${file.path}`); + } + } + + console.log(`[AttachmentProcessor] Scan complete: ${embedCount} embeds, ${wikiEmbedCount} wiki-embeds, ${linkCount} links, ${wikilinkCount} wikilinks -> ${internal.length} internal attachments, ${external.length} external references`); + return { internal, external }; + } + + private rewriteContent( + content: string, + sourceFile: TFile, + uploadedMap: Map + ): AttachmentPrepareResult { + const imageUrlToUploadId: Record = {}; + const filePlaceholderToUpload: Record = {}; + const app = this.plugin.app; + + let rewritten = content; + + // Rewrite embedded images: ![alt](path) + rewritten = rewritten.replace( + /!\[([^\]]*)\]\(([^)]+)\)/g, + (fullMatch, altText, rawDest, offset, input) => { + const path = this.parseDestination(rawDest); + if (this.shouldSkipLinkDestination(path)) return fullMatch; + + const file = this.resolveFile(app, sourceFile, path); + if (!file) return fullMatch; + + const uploaded = uploadedMap.get(file.path); + if (!uploaded) return fullMatch; + + if (this.isImage(file)) { + const sentinelUrl = this.buildSentinelUrl(uploaded.id, file, altText); + imageUrlToUploadId[sentinelUrl] = uploaded.id; + const markdown = `![${altText}](${sentinelUrl})`; + return typeof offset === "number" && typeof input === "string" && this.isStandaloneOnLine(input, offset, fullMatch) + ? `\n\n${markdown}\n\n` + : markdown; + } else { + const token = `__NOTION_FILE_UPLOAD__:${uploaded.id}`; + filePlaceholderToUpload[token] = { id: uploaded.id, name: file.name }; + return `\n\n\`${token}\`\n\n`; + } + } + ); + + // Rewrite embedded wikilinks: ![[path]] + rewritten = rewritten.replace( + /!\[\[([^\]]+)\]\]/g, + (fullMatch, inner, offset, input) => { + const linkPath = this.parseWikilink(inner); + if (this.shouldSkipLinkDestination(linkPath)) return fullMatch; + + const file = this.resolveFile(app, sourceFile, linkPath); + if (!file) return fullMatch; + + const uploaded = uploadedMap.get(file.path); + if (!uploaded) return fullMatch; + + if (this.isImage(file)) { + const sentinelUrl = this.buildSentinelUrl(uploaded.id, file); + imageUrlToUploadId[sentinelUrl] = uploaded.id; + const markdown = `![](${sentinelUrl})`; + return typeof offset === "number" && typeof input === "string" && this.isStandaloneOnLine(input, offset, fullMatch) + ? `\n\n${markdown}\n\n` + : markdown; + } else { + const token = `__NOTION_FILE_UPLOAD__:${uploaded.id}`; + filePlaceholderToUpload[token] = { id: uploaded.id, name: file.name }; + return `\n\n\`${token}\`\n\n`; + } + } + ); + + // Rewrite markdown links: [text](path) + rewritten = rewritten.replace( + /(? { + const path = this.parseDestination(rawDest); + if (this.shouldSkipLinkDestination(path)) return fullMatch; + + const file = this.resolveFile(app, sourceFile, path); + if (!file) return fullMatch; + + const uploaded = uploadedMap.get(file.path); + if (!uploaded) return fullMatch; + + // For markdown links, always use file placeholder (non-image treatment) + const token = `__NOTION_FILE_UPLOAD__:${uploaded.id}`; + filePlaceholderToUpload[token] = { id: uploaded.id, name: file.name }; + return `\n\n\`${token}\`\n\n`; + } + ); + + // Rewrite wikilink references: [[path]] + rewritten = rewritten.replace( + /(? { + const linkPath = this.parseWikilink(inner); + if (this.shouldSkipLinkDestination(linkPath)) return fullMatch; + + const file = this.resolveFile(app, sourceFile, linkPath); + if (!file) return fullMatch; + + const uploaded = uploadedMap.get(file.path); + if (!uploaded) return fullMatch; + + // For wikilink references, always use file placeholder (non-image treatment) + const token = `__NOTION_FILE_UPLOAD__:${uploaded.id}`; + filePlaceholderToUpload[token] = { id: uploaded.id, name: file.name }; + return `\n\n\`${token}\`\n\n`; + } + ); + + return { content: rewritten, imageUrlToUploadId, filePlaceholderToUpload }; + } + + private parseDestination(rawDest: string): string { + const trimmed = rawDest.trim(); + // Handle angle-bracket wrapped URLs: + if (trimmed.startsWith("<") && trimmed.includes(">")) { + const end = trimmed.indexOf(">"); + return this.decodePathOrUrl(trimmed.slice(1, end)); + } + // Take first non-space segment + const match = trimmed.match(/^(\S+)/); + return this.decodePathOrUrl(match ? match[1] : trimmed); + } + + private parseWikilink(inner: string): string { + const trimmed = inner.trim(); + // Remove alias: [[path|alias]] + const beforeAlias = trimmed.split("|")[0]?.trim() ?? trimmed; + // Remove heading: [[path#heading]] + const beforeHeading = beforeAlias.split("#")[0]?.trim() ?? beforeAlias; + return this.decodePathOrUrl(beforeHeading); + } + + private decodePathOrUrl(value: string): string { + /* + // TODO: Support `obsidian://` and `app://` URL destinations. + // For now we only support wikilink + standard markdown formats with vault paths. + if (value.startsWith("obsidian://") || value.startsWith("app://")) { + try { + return decodeURIComponent(value); + } catch { + return value; + } + } + */ + // For regular paths, strip query/hash and decode + const stripped = value.split(/[?#]/)[0] ?? value; + try { + return decodeURIComponent(stripped); + } catch { + return stripped; + } + } + + private isExternalUrl(link: string): boolean { + return link.startsWith("http://") || link.startsWith("https://"); + } + + private isTodoUrl(link: string): boolean { + return link.startsWith("obsidian://") || link.startsWith("app://"); + } + + private shouldSkipLinkDestination(link: string): boolean { + return this.isExternalUrl(link) || this.isTodoUrl(link); + } + + private resolveFile(app: App, sourceFile: TFile, link: string, options?: { log?: boolean }): TFile | null { + const shouldLog = options?.log !== false; + const log = (...args: any[]) => { + if (shouldLog) console.log(...args); + }; + + if (!link.trim()) { + log(`[AttachmentProcessor] resolveFile: empty link`); + return null; + } + + // TODO: Support `obsidian://` and `app://` URL destinations. + if (this.isTodoUrl(link)) { + return null; + } + + /* + // Handle obsidian:// URLs + if (link.startsWith("obsidian://")) { + const filePath = this.parseObsidianUrl(link); + log(`[AttachmentProcessor] resolveFile: obsidian:// URL -> extracted path: "${filePath}"`); + if (!filePath) return null; + const file = app.vault.getAbstractFileByPath(normalizePath(filePath)); + if (file instanceof TFile) { + log(`[AttachmentProcessor] resolveFile: ✓ Resolved obsidian:// to: ${file.path}`); + return file; + } + log(`[AttachmentProcessor] resolveFile: ✗ Failed to resolve obsidian:// path: ${filePath}`); + return null; + } + + // Handle app://local/ URLs (Obsidian internal) + if (link.startsWith("app://")) { + const filePath = this.parseAppUrl(link); + log(`[AttachmentProcessor] resolveFile: app:// URL -> extracted path: "${filePath}"`); + if (!filePath) return null; + + const vaultCandidate = filePath.startsWith("/") ? filePath.slice(1) : filePath; + let file = app.vault.getAbstractFileByPath(normalizePath(vaultCandidate)); + if (file instanceof TFile) { + log(`[AttachmentProcessor] resolveFile: ✓ Resolved app:// to: ${file.path}`); + return file; + } + + const mapped = this.mapAbsolutePathToVault(app, filePath); + if (mapped) { + file = app.vault.getAbstractFileByPath(normalizePath(mapped)); + if (file instanceof TFile) { + log(`[AttachmentProcessor] resolveFile: ✓ Resolved app:// absolute path to: ${file.path}`); + return file; + } + } + + log(`[AttachmentProcessor] resolveFile: ✗ Failed to resolve app:// path: ${filePath}`); + return null; + } + */ + + // Try metadata cache first + const cached = app.metadataCache.getFirstLinkpathDest(link, sourceFile.path); + if (cached instanceof TFile) { + log(`[AttachmentProcessor] resolveFile: ✓ Resolved via metadata cache: ${link} -> ${cached.path}`); + return cached; + } + + // Try absolute path + const byPath = app.vault.getAbstractFileByPath(normalizePath(link)); + if (byPath instanceof TFile) { + log(`[AttachmentProcessor] resolveFile: ✓ Resolved via absolute path: ${link} -> ${byPath.path}`); + return byPath; + } + + // Try relative to source file + const sourceDir = sourceFile.path.includes("/") + ? sourceFile.path.slice(0, sourceFile.path.lastIndexOf("/")) + : ""; + const relPath = normalizePath(sourceDir ? `${sourceDir}/${link}` : link); + const byRel = app.vault.getAbstractFileByPath(relPath); + if (byRel instanceof TFile) { + log(`[AttachmentProcessor] resolveFile: ✓ Resolved via relative path: ${link} -> ${byRel.path}`); + return byRel; + } + + log(`[AttachmentProcessor] resolveFile: ✗ Failed to resolve: ${link} (tried cache, absolute, relative)`); + return null; + } + + /* + // TODO: Support `obsidian://` URL destinations. + private parseObsidianUrl(url: string): string | null { + try { + const urlObj = new URL(url); + // obsidian://open?vault=VaultName&file=path/to/file.png + const filePath = urlObj.searchParams.get("file"); + if (filePath) { + return decodeURIComponent(filePath); + } + return null; + } catch { + return null; + } + } + + // TODO: Support `app://` URL destinations. + private parseAppUrl(url: string): string | null { + try { + const urlObj = new URL(url); + const pathname = urlObj.pathname; + if (!pathname || pathname === "/") return null; + return decodeURIComponent(pathname); + } catch { + return null; + } + } + + // TODO: Support mapping absolute paths to vault paths for `app://local/...`. + private mapAbsolutePathToVault(app: App, absolutePath: string): string | null { + const adapter: any = app.vault.adapter; + if (typeof adapter?.getBasePath !== "function") { + return null; + } + + let normalizedAbsolute = absolutePath.replace(/\\/g, "/"); + if (/^\/[A-Za-z]:\//.test(normalizedAbsolute)) { + normalizedAbsolute = normalizedAbsolute.slice(1); + } + + let basePath = String(adapter.getBasePath()).replace(/\\/g, "/"); + if (basePath.endsWith("/")) { + basePath = basePath.slice(0, -1); + } + if (/^\/[A-Za-z]:\//.test(basePath)) { + basePath = basePath.slice(1); + } + + const windowsStyle = /^[A-Za-z]:\//.test(basePath); + const compareAbsolute = windowsStyle ? normalizedAbsolute.toLowerCase() : normalizedAbsolute; + const compareBase = windowsStyle ? basePath.toLowerCase() : basePath; + + if (!compareAbsolute.startsWith(compareBase)) { + return null; + } + + let relative = normalizedAbsolute.slice(basePath.length); + if (relative.startsWith("/")) { + relative = relative.slice(1); + } + return relative || null; + } + */ + + private isSupported(file: TFile): boolean { + const ext = file.extension?.toLowerCase() ?? ""; + return SUPPORTED_EXTENSIONS.has(ext); + } + + private isImage(file: TFile): boolean { + const ext = file.extension?.toLowerCase() ?? ""; + return IMAGE_EXTENSIONS.has(ext); + } + + private buildSentinelUrl(uploadId: string, file: TFile, _altText?: string): string { + const ext = file.extension?.toLowerCase() ?? ""; + const suffix = ext ? `.${ext}` : ""; + return `https://notion-file-upload.local/${uploadId}${suffix}`; + } +} + +export function applyBlockRewrites( + blocks: any[], + rewrites: Pick +): void { + transformBlocksInPlace(blocks, rewrites); +} + +function transformBlocksInPlace( + blocks: any[], + rewrites: Pick +): void { + for (let i = 0; i < blocks.length; i++) { + const block = blocks[i]; + + // Transform image blocks with sentinel URLs + if (block?.type === "image" && block?.image?.type === "external") { + const url = block.image?.external?.url; + if (url && rewrites.imageUrlToUploadId[url]) { + const caption = block.image?.caption; + block.image = { + type: "file_upload", + file_upload: { id: rewrites.imageUrlToUploadId[url] }, + ...(caption ? { caption } : {}), + }; + } + } + + // Transform paragraph placeholders to file blocks + if (block?.type === "paragraph") { + const token = extractParagraphText(block); + if (token && rewrites.filePlaceholderToUpload[token]) { + const { id, name } = rewrites.filePlaceholderToUpload[token]; + blocks[i] = buildFileBlock(id, name); + } + } + + // Recurse into children + const inner = block?.[block?.type]; + if (inner?.children && Array.isArray(inner.children)) { + transformBlocksInPlace(inner.children, rewrites); + } + } +} + +function extractParagraphText(block: any): string | undefined { + const richText = block?.paragraph?.rich_text; + if (!Array.isArray(richText) || richText.length === 0) return undefined; + return richText + .map((item: any) => item?.plain_text ?? item?.text?.content ?? "") + .join("") + .trim() || undefined; +} + +function buildFileBlock(uploadId: string, name: string): any { + return { + object: "block", + type: "file", + file: { + type: "file_upload", + file_upload: { id: uploadId }, + }, + }; +} diff --git a/src/upload/common/AttachmentUploader.ts b/src/upload/common/AttachmentUploader.ts new file mode 100644 index 0000000..d6ee34e --- /dev/null +++ b/src/upload/common/AttachmentUploader.ts @@ -0,0 +1,256 @@ +import { TFile, requestUrl } from "obsidian"; +import type MyPlugin from "src/main"; +import type { DatabaseDetails } from "../../ui/settingTabs"; + +const NOTION_API_VERSION = "2025-09-03"; +const MAX_UPLOAD_BYTES = 5 * 1024 * 1024; + +interface FileUploadSession { + id: string; + status: string; + upload_url?: string; +} + +interface UploadResult { + id: string; + filename: string; +} + +export class AttachmentUploader { + private plugin: MyPlugin; + private dbDetails: DatabaseDetails; + private textEncoder = new TextEncoder(); + + constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) { + this.plugin = plugin; + this.dbDetails = dbDetails; + } + + async uploadFile(file: TFile): Promise { + const { notionAPI } = this.dbDetails; + const fileSizeBytes = file.stat?.size ?? 0; + const contentType = this.getContentType(file.extension); + + if (fileSizeBytes > MAX_UPLOAD_BYTES) { + throw new Error( + `File too large for Notion upload (max 5MB): ${file.path} (${(fileSizeBytes / 1024 / 1024).toFixed(2)} MB)`, + ); + } + const mode = "single_part"; + + console.log(`[AttachmentUploader] uploadFile: ${file.name}`, { + path: file.path, + size: `${(fileSizeBytes / 1024).toFixed(2)} KB`, + contentType, + mode, + }); + + const session = await this.createUploadSession({ + mode, + notionAPI, + }); + + console.log(`[AttachmentUploader] Upload session created:`, { + sessionId: session.id, + status: session.status, + }); + + const binary = await this.plugin.app.vault.readBinary(file); + console.log(`[AttachmentUploader] Read binary data: ${binary.byteLength} bytes`); + + if (binary.byteLength > MAX_UPLOAD_BYTES) { + throw new Error( + `File too large for Notion upload (max 5MB): ${file.path} (${(binary.byteLength / 1024 / 1024).toFixed(2)} MB)`, + ); + } + + const uploadUrl = + session.upload_url ?? + `https://api.notion.com/v1/file_uploads/${encodeURIComponent(session.id)}/send`; + await this.sendFileData(session.id, uploadUrl, binary, notionAPI, file.name, contentType); + + console.log(`[AttachmentUploader] Upload complete: ${file.name} -> ${session.id}`); + return { id: session.id, filename: file.name }; + } + + private async createUploadSession(params: { + mode: string; + notionAPI: string; + }): Promise { + console.log(`[AttachmentUploader] Creating upload session:`, { + mode: params.mode, + }); + + const response = await this.requestWithRetry({ + url: "https://api.notion.com/v1/file_uploads", + method: "POST", + headers: { + accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${params.notionAPI}`, + "Notion-Version": NOTION_API_VERSION, + }, + body: JSON.stringify({ + mode: params.mode, + }), + throw: false, + }); + + const data = response.json; + if (response.status < 200 || response.status >= 300) { + console.error(`[AttachmentUploader] Failed to create upload session:`, { + status: response.status, + message: data?.message, + response: data, + }); + throw new Error(`Failed to create upload session: ${data?.message ?? response.status}`); + } + + const id = data?.id ?? data?.file_upload?.id; + if (!id) { + throw new Error("Upload session response missing id"); + } + + return { id, status: data.status, upload_url: data.upload_url }; + } + + private async sendFileData( + fileUploadId: string, + uploadUrl: string, + binary: ArrayBuffer, + notionAPI: string, + filename: string, + contentType: string, + ): Promise { + console.log(`[AttachmentUploader] Sending file data for session: ${fileUploadId} (${binary.byteLength} bytes)`); + + const { body, boundary } = this.buildMultipartBody({ + fieldName: "file", + filename, + contentType: contentType || "application/octet-stream", + binary, + }); + + const response = await this.requestWithRetry({ + url: uploadUrl, + method: "POST", + headers: { + accept: "application/json", + "Content-Type": `multipart/form-data; boundary=${boundary}`, + Authorization: `Bearer ${notionAPI}`, + "Notion-Version": NOTION_API_VERSION, + }, + body, + throw: false, + }); + + const data = response.json; + if (response.status < 200 || response.status >= 300) { + console.error(`[AttachmentUploader] Failed to send file data:`, { + sessionId: fileUploadId, + status: response.status, + message: data?.message, + response: data, + }); + throw new Error(`Failed to send file data: ${data?.message ?? response.status}`); + } + + console.log(`[AttachmentUploader] File data sent successfully for session: ${fileUploadId}`); + } + + private buildMultipartBody(params: { + fieldName: string; + filename: string; + contentType: string; + binary: ArrayBuffer; + }): { body: ArrayBuffer; boundary: string } { + const boundary = `----NotionFormBoundary${Math.random().toString(16).slice(2)}${Math.random().toString(16).slice(2)}`; + + const safeFilename = params.filename.replace(/"/g, '\\"'); + const prefix = [ + `--${boundary}\r\n`, + `Content-Disposition: form-data; name="${params.fieldName}"; filename="${safeFilename}"\r\n`, + `Content-Type: ${params.contentType}\r\n`, + `\r\n`, + ].join(""); + const suffix = `\r\n--${boundary}--\r\n`; + + const prefixBytes = this.textEncoder.encode(prefix); + const fileBytes = new Uint8Array(params.binary); + const suffixBytes = this.textEncoder.encode(suffix); + + const out = new Uint8Array(prefixBytes.length + fileBytes.length + suffixBytes.length); + out.set(prefixBytes, 0); + out.set(fileBytes, prefixBytes.length); + out.set(suffixBytes, prefixBytes.length + fileBytes.length); + return { body: out.buffer, boundary }; + } + + private async requestWithRetry(params: any, maxAttempts = 4): Promise { + let attempt = 0; + let lastError: unknown; + + while (attempt < maxAttempts) { + attempt++; + try { + const response = await requestUrl(params); + if (this.shouldRetry(response.status) && attempt < maxAttempts) { + const delayMs = this.getRetryDelay(response, attempt); + console.warn(`[AttachmentUploader] Retryable status ${response.status}, attempt ${attempt}/${maxAttempts}, retrying in ${delayMs}ms`); + await this.sleep(delayMs); + continue; + } + return response; + } catch (error: unknown) { + lastError = error; + console.error(`[AttachmentUploader] Request failed, attempt ${attempt}/${maxAttempts}:`, error); + if (attempt >= maxAttempts) break; + const delayMs = this.getRetryDelay(undefined, attempt); + console.warn(`[AttachmentUploader] Retrying in ${delayMs}ms`); + await this.sleep(delayMs); + } + } + + console.error(`[AttachmentUploader] Request failed after ${maxAttempts} attempts`); + throw lastError ?? new Error("Request failed after retries"); + } + + private shouldRetry(status: number): boolean { + return status === 429 || status === 500 || status === 502 || status === 503 || status === 504; + } + + private getRetryDelay(response: any, attempt: number): number { + const retryAfter = response?.headers?.["retry-after"] ?? response?.headers?.["Retry-After"]; + if (retryAfter) { + const seconds = parseInt(retryAfter, 10); + if (!isNaN(seconds)) return seconds * 1000; + } + const base = 500; + const max = 8000; + const expo = Math.min(max, base * Math.pow(2, attempt - 1)); + const jitter = Math.floor(Math.random() * 250); + return expo + jitter; + } + + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + private getContentType(extension: string): string { + const ext = extension.toLowerCase(); + const mimeTypes: Record = { + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + webp: "image/webp", + svg: "image/svg+xml", + heic: "image/heic", + tif: "image/tiff", + tiff: "image/tiff", + bmp: "image/bmp", + pdf: "application/pdf", + }; + return mimeTypes[ext] ?? "application/octet-stream"; + } +} diff --git a/src/upload/common/UploadBase.ts b/src/upload/common/UploadBase.ts index 4799791..019c685 100644 --- a/src/upload/common/UploadBase.ts +++ b/src/upload/common/UploadBase.ts @@ -3,6 +3,8 @@ import MyPlugin from "src/main"; import { DatabaseDetails } from "../../ui/settingTabs"; import { i18nConfig } from "../../lang/I18n"; +const NOTION_API_VERSION = "2025-09-03"; + export interface NotionPageResponse { response: any; data: any; @@ -30,7 +32,7 @@ export abstract class UploadBase { headers: { "Content-Type": "application/json", Authorization: "Bearer " + notionAPI, - "Notion-Version": "2022-06-28", + "Notion-Version": NOTION_API_VERSION, }, body: "", throw: false, @@ -116,7 +118,7 @@ export abstract class UploadBase { headers: { "Content-Type": "application/json", Authorization: "Bearer " + notionAPI, - "Notion-Version": "2022-06-28", + "Notion-Version": NOTION_API_VERSION, }, body: JSON.stringify(body), throw: false, @@ -171,7 +173,7 @@ export abstract class UploadBase { headers: { "Content-Type": "application/json", Authorization: "Bearer " + notionAPI, - "Notion-Version": "2022-06-28", + "Notion-Version": NOTION_API_VERSION, }, body: JSON.stringify(extraBlocks), throw: false, @@ -201,7 +203,7 @@ export abstract class UploadBase { method: "GET", headers: { Authorization: "Bearer " + notionAPI, - "Notion-Version": "2022-06-28", + "Notion-Version": NOTION_API_VERSION, }, throw: false, }).catch((error) => From 879cd66aedc30b56347cc1fd96578db4c9ebf0f8 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Sun, 4 Jan 2026 19:11:49 +0000 Subject: [PATCH 32/32] docs: Add attachment upload feature to README documentation --- README-zh.md | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README-zh.md b/README-zh.md index a430668..67f6c78 100644 --- a/README-zh.md +++ b/README-zh.md @@ -14,6 +14,7 @@ - **多种数据库类型**:支持通用、NotionNext 和自定义数据库。 - **自定义属性**:在自定义数据库中,可将任何 frontmatter 键映射到任何 Notion 属性。 - **灵活同步**:即时选择要同步到哪个数据库。 +- **附件上传**:自动上传本地图片和 PDF 到 Notion,支持 Wikilink、Markdown 链接格式。 ## 致谢 diff --git a/README.md b/README.md index 4ea3851..84beb77 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Share files from Obsidian to any Notion database using the Notion API. This plug - **Multiple Database Types**: Supports General, NotionNext, and Custom databases. - **Custom Properties**: Map any frontmatter key to any Notion property in custom databases. - **Flexible Syncing**: Choose which database to sync to on-the-fly. +- **Attachment Upload**: Automatically uploads local images and PDFs to Notion, supporting Wikilinks and Markdown links. ## Acknowledgment