Merge pull request #67 from jxpeng98/automate-sync

Automate sync
This commit is contained in:
Jiaxin Peng
2025-11-01 00:35:52 +00:00
committed by GitHub
15 changed files with 909 additions and 33 deletions

113
.github/workflows/prerelease.yml vendored Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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)获取常见问题的详细解决方案。

View File

@@ -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. **设置信息**:注明是否启用了自动同步以及配置的延迟时间

View File

@@ -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)",
@@ -79,5 +84,28 @@ 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",
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",
}

View File

@@ -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",
@@ -71,4 +76,28 @@ export const ja = {
BlockUploaded: "ブロックがアップロードされました",
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: "新しいデータベースを追加",
};

View File

@@ -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: "打开此选项,同步到普通数据库命令将显示在命令面板中(默认:开)",
@@ -74,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: "添加新数据库",
}

View File

@@ -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,9 +12,25 @@ export default class ObsidianSyncNotionPlugin extends Plugin {
settings: PluginSettings;
commands: ribbonCommands;
app: App;
modifyEventRef: EventRef | null = null;
autoSyncTimeout: number | null = null;
private syncingFiles: Set<string> = new Set();
private lastFrontmatterCache: Map<string, any> = new Map();
private lastContentHashCache: Map<string, string> = new Map();
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();
@@ -36,21 +53,106 @@ 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);
}
this.lastFrontmatterCache.clear();
this.lastContentHashCache.clear();
}
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) {
@@ -80,7 +182,225 @@ 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 = window.setTimeout(async () => {
await this.autoSyncFile(file);
}, delayMs);
});
}
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)) {
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;
}
// 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 }> = [];
for (const key in this.settings.databaseDetails) {
const dbDetails = this.settings.databaseDetails[key];
const notionIDKey = `NotionID-${dbDetails.abName}`;
if (frontMatter[notionIDKey]) {
foundDatabases.push({
dbDetails: dbDetails,
notionId: String(frontMatter[notionIDKey])
});
}
}
// 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;
}
// 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}`);
}
// 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);
}
}
// After sync completes, update cache with the latest frontmatter (including updated NotionIDs)
// Wait a bit for metadata cache to update
window.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
.replace('{filename}', file.basename)
.replace('{error}', error.message);
new Notice(message);
} finally {
this.syncingFiles.delete(file.path);
}
}
}

View File

@@ -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')

View File

@@ -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);
});
});

View File

@@ -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;

View File

@@ -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,14 +92,42 @@ 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)
.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);
@@ -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') {

View File

@@ -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) {