mirror of
https://github.com/jxpeng98/obsidian-to-NotionNext
synced 2026-07-30 00:48:36 +08:00
Compare commits
193 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b08c46c233 | ||
|
|
8dfb82b4ce | ||
|
|
8502650033 | ||
|
|
30e34e3f8c | ||
|
|
ec3d3e85a2 | ||
|
|
fe04e8df7f | ||
|
|
d36deb7b30 | ||
|
|
e8295746e1 | ||
|
|
83df1111c4 | ||
|
|
6e8be42e3c | ||
|
|
6b02cb219e | ||
|
|
cd7eb78378 | ||
|
|
3e00f127e9 | ||
|
|
4fb3b99996 | ||
|
|
269d354734 | ||
|
|
a37bda1575 | ||
|
|
19d66917d2 | ||
|
|
a9acfbe956 | ||
|
|
879cd66aed | ||
|
|
202385ac21 | ||
|
|
9c6980d1c9 | ||
|
|
b61264a1f6 | ||
|
|
876b6233cb | ||
|
|
ccfe40c1f0 | ||
|
|
90dcc1aef0 | ||
|
|
7053a74e24 | ||
|
|
5144f10e77 | ||
|
|
5806a2831b | ||
|
|
eeaf7c036d | ||
|
|
3de92d3f54 | ||
|
|
e6b13e5eee | ||
|
|
f4def623bb | ||
|
|
3620505b56 | ||
|
|
e9355aaf92 | ||
|
|
943ec6af6d | ||
|
|
bb4b75c82e | ||
|
|
ae4488546c | ||
|
|
7661bc94c7 | ||
|
|
e8a9594ea1 | ||
|
|
bb211543f5 | ||
|
|
c52f84232d | ||
|
|
3730b7645a | ||
|
|
e2a965e3d6 | ||
|
|
2738498dc2 | ||
|
|
6df5e21f3d | ||
|
|
d660a088e3 | ||
|
|
83d4f1e48c | ||
|
|
7c841f3db3 | ||
|
|
fab3acdc21 | ||
|
|
59fa32b5e5 | ||
|
|
e5f906bba2 | ||
|
|
c369c291a7 | ||
|
|
33f0848b32 | ||
|
|
a33e63a219 | ||
|
|
99a40375f5 | ||
|
|
7282fc6955 | ||
|
|
770061e826 | ||
|
|
de7b7dd28d | ||
|
|
1c6c3fb0d9 | ||
|
|
ca0935bfe8 | ||
|
|
e944721415 | ||
|
|
db66ef137c | ||
|
|
a152201510 | ||
|
|
abb699fe4d | ||
|
|
cf6c80df0f | ||
|
|
cf884de554 | ||
|
|
8eb4694342 | ||
|
|
6c0f6cea30 | ||
|
|
8af4816f87 | ||
|
|
f6d24daa52 | ||
|
|
da72846903 | ||
|
|
8e08c4325d | ||
|
|
ebb7a290e8 | ||
|
|
24e68fc798 | ||
|
|
78d2a29c8a | ||
|
|
1156bd3809 | ||
|
|
b46e24f1ed | ||
|
|
a950e2e301 | ||
|
|
aac7bc154a | ||
|
|
a573ef728d | ||
|
|
1344c14933 | ||
|
|
c1f4ecd14b | ||
|
|
5942876c92 | ||
|
|
3218eb9fba | ||
|
|
48566eca20 | ||
|
|
8bd8346423 | ||
|
|
00411e9599 | ||
|
|
296125f6b4 | ||
|
|
1b385ccd0a | ||
|
|
a90e4871bb | ||
|
|
885ccba027 | ||
|
|
f282dde1b7 | ||
|
|
bc166d8db1 | ||
|
|
7612637951 | ||
|
|
e8b1617916 | ||
|
|
fd29ed820e | ||
|
|
1f58670906 | ||
|
|
fc47d36e8d | ||
|
|
cd4e610c46 | ||
|
|
b05c248c44 | ||
|
|
800e218dee | ||
|
|
3b774552af | ||
| b8a02c49f1 | |||
|
|
046a67f22c | ||
| 426f05dfbd | |||
|
|
0addaa0d7f | ||
| 1d5792c966 | |||
|
|
193d3f6f87 | ||
| e9c2b4aada | |||
| b5ac42e6e3 | |||
| 60d002677b | |||
| 3c3ec6881e | |||
|
|
a80155fafc | ||
| 3a814b8390 | |||
| bb522db9a2 | |||
| 201f72ae0e | |||
| be6f192dd8 | |||
| 4ce1019094 | |||
| 095c608511 | |||
| 2d8d7fb11a | |||
| 36725cb699 | |||
|
|
654e7e854d | ||
|
|
fa67f08512 | ||
|
|
7e42ef49a7 | ||
|
|
5a95aa0acc | ||
|
|
ded01ce678 | ||
|
|
3535d9fc70 | ||
|
|
9c93e38aff | ||
|
|
bfde5dfe1a | ||
|
|
d8eb3c8984 | ||
|
|
1cee2b67a6 | ||
|
|
82529ce56a | ||
|
|
70f30f0713 | ||
|
|
24ebb0bf8a | ||
|
|
d7372c7c55 | ||
|
|
f7bcf71020 | ||
|
|
72146afe48 | ||
|
|
9bcdb42edb | ||
|
|
16e0827991 | ||
|
|
863a6fb0dc | ||
|
|
5799ffe79b | ||
|
|
a8ebab7fcb | ||
|
|
e0ec27ad07 | ||
|
|
85220105b0 | ||
|
|
9afdb24a82 | ||
|
|
3c7ab13995 | ||
|
|
31b72f3bd3 | ||
|
|
ee5aada1b9 | ||
|
|
9e15044f6a | ||
|
|
9d891dae55 | ||
|
|
bc665253ec | ||
|
|
125c75f7c0 | ||
|
|
8248640b5f | ||
|
|
0198898086 | ||
|
|
8c6aea8250 | ||
|
|
a179d3da52 | ||
|
|
569d8eb699 | ||
|
|
2a58dd3258 | ||
|
|
8c65e471eb | ||
|
|
388693d5b4 | ||
|
|
dda6710fe9 | ||
|
|
76ea9a4aee | ||
|
|
856f83e8a1 | ||
|
|
f670dd972a | ||
|
|
e6de2d036a | ||
|
|
dd131f5df1 | ||
|
|
a9fb81fc1b | ||
|
|
4f21e3ff91 | ||
|
|
4abbbecd37 | ||
|
|
35746f1c51 | ||
|
|
9ac64268fc | ||
|
|
3123d91e97 | ||
|
|
cf08703fea | ||
|
|
fb463ceaae | ||
|
|
306573d81e | ||
|
|
fb5f7b502d | ||
|
|
59c15f1206 | ||
|
|
370444fb94 | ||
|
|
11aaf14c0b | ||
|
|
f3fb251cf7 | ||
|
|
913fa4dd03 | ||
|
|
f638310f31 | ||
|
|
c2421f7a84 | ||
|
|
6c30d0c2b4 | ||
|
|
ccb5a20abe | ||
|
|
630e3a20c2 | ||
|
|
040b72b760 | ||
|
|
7df4b5e9c8 | ||
|
|
3f35a56b6e | ||
|
|
3c7e266a35 | ||
|
|
8201c706ea | ||
|
|
4433ea449d | ||
|
|
96126d1334 |
111
.github/workflows/prerelease.yml
vendored
Normal file
111
.github/workflows/prerelease.yml
vendored
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
name: Prerelease
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
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: "22"
|
||||||
|
|
||||||
|
- 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
|
||||||
82
.github/workflows/release.yml
vendored
82
.github/workflows/release.yml
vendored
@@ -1,23 +1,41 @@
|
|||||||
name: Release Obsidian-to-NotionNext plugin
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
# Only trigger for release tags (e.g., 1.0.0, 2.3.4)
|
||||||
|
# Excludes prerelease tags (e.g., 2.8.0-beta.3, 1.0.0-rc.1)
|
||||||
|
- "[0-9]+.[0-9]+.[0-9]"
|
||||||
|
- "[0-9]+.[0-9]+.[0-9][0-9]"
|
||||||
|
- "[0-9]+.[0-9]+.[0-9][0-9][0-9]"
|
||||||
|
- "[0-9]+.[0-9][0-9].[0-9]"
|
||||||
|
- "[0-9]+.[0-9][0-9].[0-9][0-9]"
|
||||||
|
- "[0-9]+.[0-9][0-9].[0-9][0-9][0-9]"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PLUGIN_NAME: share-to-notionnext # Change this to match the id of your plugin.
|
PLUGIN_NAME: share-to-notionnext # Change this to match the id of your plugin.
|
||||||
|
CHANGELOG_FILENAME: CHANGELOG.md
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
name: release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: "16.x"
|
node-version: "22"
|
||||||
|
|
||||||
|
# - name: Generate changelog
|
||||||
|
# id: changelog
|
||||||
|
# uses: saadmk11/changelog-ci@v1.1.2
|
||||||
|
# with:
|
||||||
|
# github_token: ${{ secrets.REPO_ACCESS_TOKEN }}
|
||||||
|
# release_version: ${{ github.ref }}
|
||||||
|
# changelog_filename: CHANGELOG.md
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
id: build
|
id: build
|
||||||
@@ -30,20 +48,67 @@ jobs:
|
|||||||
ls
|
ls
|
||||||
echo "tag_name=$(git tag --sort version:refname | tail -n 1)" >> $GITHUB_OUTPUT
|
echo "tag_name=$(git tag --sort version:refname | tail -n 1)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Generate release notes
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
run: |
|
||||||
|
node - <<'NODE'
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const tag = process.env.GITHUB_REF_NAME;
|
||||||
|
if (!tag) {
|
||||||
|
throw new Error('GITHUB_REF_NAME is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
const changelog = fs.readFileSync('CHANGELOG.md', 'utf8');
|
||||||
|
const lines = changelog.split(/\r?\n/);
|
||||||
|
|
||||||
|
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const headerRe = new RegExp(`^##\\s+${escapeRegex(tag)}\\b`);
|
||||||
|
|
||||||
|
const start = lines.findIndex((line) => headerRe.test(line));
|
||||||
|
if (start === -1) {
|
||||||
|
throw new Error(`Could not find changelog section for tag "${tag}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let end = lines.length;
|
||||||
|
for (let i = start + 1; i < lines.length; i++) {
|
||||||
|
if (/^##\s+/.test(lines[i])) {
|
||||||
|
end = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let section = lines.slice(start, end);
|
||||||
|
while (section.length > 0 && section[section.length - 1].trim() === '') {
|
||||||
|
section.pop();
|
||||||
|
}
|
||||||
|
if (section.length > 0 && section[section.length - 1].trim() === '---') {
|
||||||
|
section.pop();
|
||||||
|
while (section.length > 0 && section[section.length - 1].trim() === '') {
|
||||||
|
section.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync('release-notes.md', `${section.join('\n')}\n`);
|
||||||
|
NODE
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
id: create_release
|
id: create_release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
uses: actions/create-release@v1
|
uses: actions/create-release@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
|
||||||
VERSION: ${{ github.ref }}
|
VERSION: ${{ github.ref }}
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref }}
|
tag_name: ${{ github.ref_name }}
|
||||||
release_name: ${{ github.ref }}
|
release_name: ${{ github.ref_name }}
|
||||||
|
body_path: release-notes.md
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|
||||||
- name: Upload zip file
|
- name: Upload zip file
|
||||||
id: upload-zip
|
id: upload-zip
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
|
||||||
@@ -55,6 +120,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload main.js
|
- name: Upload main.js
|
||||||
id: upload-main
|
id: upload-main
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
|
||||||
@@ -66,6 +132,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload manifest.json
|
- name: Upload manifest.json
|
||||||
id: upload-manifest
|
id: upload-manifest
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
|
||||||
@@ -77,6 +144,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload markdown template
|
- name: Upload markdown template
|
||||||
id: upload-md
|
id: upload-md
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
|
||||||
|
|||||||
32
.github/workflows/test.yml
vendored
Normal file
32
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Test and Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
env:
|
||||||
|
PLUGIN_NAME: share-to-notionnext # Change this to match the id of your plugin.
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- 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
|
||||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -21,3 +21,13 @@ data.json
|
|||||||
# Exclude macOS Finder (System Explorer) View States
|
# Exclude macOS Finder (System Explorer) View States
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.history
|
.history
|
||||||
|
|
||||||
|
# local data
|
||||||
|
local-data
|
||||||
|
|
||||||
|
# vitepress
|
||||||
|
docs/.vitepress/dist
|
||||||
|
docs/.vitepress/cache
|
||||||
|
|
||||||
|
# claude code
|
||||||
|
.claude
|
||||||
1
.npmrc
1
.npmrc
@@ -1 +1,2 @@
|
|||||||
tag-version-prefix=""
|
tag-version-prefix=""
|
||||||
|
message="chore: bump version to %s"
|
||||||
|
|||||||
141
CHANGELOG.md
Normal file
141
CHANGELOG.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
## v2.8.3 (2026-03-04)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
## 2.8.2 (2026-03-04)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed version bump with `tag-version-prefix="v"` in `.npmrc` causing incorrect version format in `package.json` (e.g. `v2.8.1` instead of `2.8.1`)
|
||||||
|
|
||||||
|
## 2.8.1 (2026-03-04)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Auto-sync success notice setting**: Toggle whether to show success notifications for auto-sync (defaults to off)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Auto-sync is quieter by default: success and "start upload" notices are suppressed unless explicitly enabled
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Auto-sync no longer shows "All blocks has been uploaded" (`BlockUploaded`) notice when success notices are disabled
|
||||||
|
|
||||||
|
## 2.8.0 (2026-01-29)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Auto Sync**: Automatically sync notes on content or frontmatter changes, with configurable delay and multi-database support
|
||||||
|
- **Attachment Upload**: Upload local images and PDFs to Notion via the File Upload API and insert them as `image`/`file` blocks
|
||||||
|
- **Auto-copy Notion Link**: Option to copy the Notion page link to clipboard after syncing
|
||||||
|
- **Auto-sync frontmatter key**: Customize the frontmatter key for auto-sync database lists (default: `autosync-database`)
|
||||||
|
- Comprehensive i18n support for UI and notifications
|
||||||
|
- Prerelease workflow for beta testing via GitHub Actions and BRAT
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved auto-sync behavior and notices for files without `autosync-database` or missing NotionID
|
||||||
|
- Limited attachment link parsing to **Wikilinks** and **standard Markdown links** (Obsidian/App URL formats are now TODO/disabled)
|
||||||
|
- Standardized Notion API request header `Notion-Version` to `2025-09-03`
|
||||||
|
- Reduced per-file upload limit to **5MB** to maximize compatibility across Notion plans
|
||||||
|
- Enhanced settings tab and documentation for auto-sync usage
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed mobile compatibility issues by using `window.setTimeout` instead of `NodeJS.Timeout`
|
||||||
|
- Prevented sync loops and improved change detection for frontmatter/body updates
|
||||||
|
- File placeholder tokens no longer break due to Markdown underscore parsing
|
||||||
|
- Better block ordering when attachments are on standalone lines in Markdown
|
||||||
|
- Preserve image captions when converting `external` images to `file_upload`
|
||||||
|
- Avoid duplicate filename display on uploaded `file` blocks
|
||||||
|
- Fixed `undefined` appearing in sync success notification by adding missing `sync-preffix` i18n key
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.8.0-beta.4 (2026-01-04)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Attachment Upload**: Upload local images and PDFs to Notion via the File Upload API and insert them as `image`/`file` blocks
|
||||||
|
- **Auto-sync safeguard**: Auto-sync is skipped for notes containing internal attachments (manual sync required)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Limited attachment link parsing to **Wikilinks** and **standard Markdown links** (Obsidian/App URL formats are now TODO/disabled)
|
||||||
|
- Standardized Notion API request header `Notion-Version` to `2025-09-03`
|
||||||
|
- Reduced per-file upload limit to **5MB** to maximize compatibility across Notion plans
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- File placeholder tokens no longer break due to Markdown underscore parsing
|
||||||
|
- Better block ordering when attachments are on standalone lines in Markdown
|
||||||
|
- Preserve image captions when converting `external` images to `file_upload`
|
||||||
|
- Avoid duplicate filename display on uploaded `file` blocks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
- 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
|
||||||
|
- Added setting to customise the frontmatter key used for auto sync database lists (defaults to `autosync-database`)
|
||||||
|
|
||||||
|
### 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
|
||||||
247
README-zh.md
247
README-zh.md
@@ -1,240 +1,21 @@
|
|||||||
# Obsidian share to NotionNext
|
# Obsidian to NotionNext
|
||||||
感谢[原作者](https://github.com/EasyChris/obsidian-to-notion)开发出了这么好用的插件,能够将obsidian同步到notion。 但是原仓库只能同步Name和Tags信息,如果像我一样通过[NotionNext](https://github.com/tangly1024/NotionNext)来搭建自己的网页,就有一些局限性。每次导入之后都需要进行大量的修改。
|
|
||||||
|
|
||||||
所以我在[原作者](https://github.com/EasyChris/obsidian-to-notion)的基础之上,增加了匹配[NotionNext](https://github.com/tangly1024/NotionNext)模板的功能。这样可以直接在Obsidian编辑,整理好之后一键发布。
|
[](https://github.com/jxpeng98/obsidian-to-NotionNext/actions/workflows/test.yml)
|
||||||
## 更新说明
|
[](https://github.com/jxpeng98/obsidian-to-NotionNext/actions/workflows/release.yml)
|
||||||
### 0.3.0 (大更新)
|
[](https://GitHub.com/jxpeng98/obsidian-to-NotionNext/releases/)
|
||||||
- 这个版本开始,你可以**任意修改第一列表头的名字 (标题列,默认:'title')**。 (**注意:你必须保证你的Notion数据库中有'tags‘这一列的存在,并且在markdown文件中把`tags:`添加到你的YAML表头中。否则会返回`400 错误`。但是你可以`tags:`设置为空。**)
|
[](https://github.com/jxpeng98/obsidian-to-NotionNext/releases/)
|
||||||
|
|
||||||

|
使用 Notion API 将文件从 Obsidian 分享到任何 Notion 数据库。此插件支持通用 Notion 数据库、NotionNext 博客模板和完全自定义的数据库模式。
|
||||||
例如这样:
|
|
||||||

|
|
||||||
- 增加了一个控制按钮来控制是否开启当前数据库同步命令的功能。如果你关闭了这个按钮,你在上传命令列表中就不会出现当前数据库的选项。
|
|
||||||
|
|
||||||

|
**详细说明请访问[文档网站](https://obsidian-to-notionnext.pages.dev/)。**
|
||||||
|
|
||||||
### 0.2.6
|
## 功能特性
|
||||||
- 增加一个按钮用来控制是否在名列列表中显示如下数据库。
|
|
||||||
|
|
||||||

|
- **多种数据库类型**:支持通用、NotionNext 和自定义数据库。
|
||||||
如果你关闭普通数据库的按钮,你在上传命令列表中就不会出现普通数据库的选项。如果你关闭NotionNext数据库的按钮,你在上传命令列表中就不会出现NotionNext数据库的选项。
|
- **自定义属性**:在自定义数据库中,可将任何 frontmatter 键映射到任何 Notion 属性。
|
||||||
|
- **灵活同步**:即时选择要同步到哪个数据库。
|
||||||
|
- **附件上传**:自动上传本地图片和 PDF 到 Notion,支持 Wikilink、Markdown 链接格式。
|
||||||
|
|
||||||

|
## 致谢
|
||||||
|
|
||||||
### 0.2.3
|
此插件是 EasyChris 的原创作品 [obsidian-to-notion](https://github.com/EasyChris/obsidian-to-notion) 的一个分支,增加了新功能并支持更灵活的数据库配置。
|
||||||
- 修复了一个bug,现在可以正常更新了。
|
|
||||||
### 0.2.2
|
|
||||||
- 支持NotionNext 和 普通Notion 数据库的同步。
|
|
||||||
- 你可以有一个NotionNext和一个普通Notion数据库。
|
|
||||||
- 普通Notion数据库的同步方式和原作者的一样,只同步`title`和`tags`。**如果使用普通数据库功能,你需要保证你的数据库中有`title`和`tags`这两个表头。必须使用小写。**
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
### 0.2.1
|
|
||||||
- 重构代码。
|
|
||||||
### 0.2.0
|
|
||||||
- 从这个版本开始,重构了交互逻辑。现在当你点击左侧的按钮,会弹出可同步的数据库选项。你可以选择你想要同步的数据库,然后点击同步按钮。**但是,现在仅支持同步单个数据库,如果你的数据库不是NotionNext,你可以不用更新。**
|
|
||||||
|
|
||||||
|
|
||||||
### 0.1.10
|
|
||||||
- 修正了设置中的中文显示。
|
|
||||||
### 0.1.8
|
|
||||||
- 重新整理了代码,增加了一个仓库转换按钮为之后的多数据库支持作准备。**但是现在还不支持多数据库,如果你的数据库不是NotionNext,你可以不用更新。**
|
|
||||||
### 0.1.7
|
|
||||||
- [x] 删除了设置中`convert tags`的选项。 你现在可以直接添加tags,而不需要考虑是否开启tags选项。如果你不需要tags,可以直接删除tags选项或者留白。
|
|
||||||
|
|
||||||
## 使用方式
|
|
||||||
### 注意事项
|
|
||||||
本插件现在暂时只能用于匹配[NotionNext](https://github.com/tangly1024/NotionNext)。如果你不是使用这个模板,你只会无限返回`error 400`.
|
|
||||||
|
|
||||||
所以没有NotionNext需求的,请使用原作者的[Obsidian-to-notion](https://github.com/EasyChris/obsidian-to-notion)
|
|
||||||
|
|
||||||
### 准备安装
|
|
||||||
在安装插件之前,你必须配置好了以下内容:
|
|
||||||
|
|
||||||
1. 你的NotionNext数据库。
|
|
||||||
2. 根据原作者的readme.md,配置好了Notion API,并且已经和你的NotionNext仓库关联。
|
|
||||||
3. NotionNext Database ID
|
|
||||||
4. 你的NotionNext数据库有如下内容:
|
|
||||||
- type
|
|
||||||
- title
|
|
||||||
- slug
|
|
||||||
- category
|
|
||||||
- tags
|
|
||||||
- date
|
|
||||||
- status
|
|
||||||
- summary
|
|
||||||
- password
|
|
||||||
- icon
|
|
||||||
|
|
||||||
在最新的0.0.6版本,我还增加了以下内容:
|
|
||||||
- titleicon: 标题的emoji图标,暂时只能支持emoji
|
|
||||||
- coverurl: 封面图片的url, url可以是任何一种图片的url, 例如jpg,png,svg等等。
|
|
||||||
|
|
||||||
**如果你是直接复制的NotionNext的模板,这些内容应该已经有了。我在原作者的基础之上对本插件的内容进行了更改,所以你只需要保证你的数据库有如上内容,并且所有的字母都是小写!!!**
|
|
||||||
|
|
||||||
|
|
||||||
**⚠️⚠️⚠️:表头全部小写!!!顺序无所谓!**
|
|
||||||
|
|
||||||
### 安装插件
|
|
||||||
#### 社区插件市场下载
|
|
||||||
打开`Obsidian -> 设置 -> 社区插件 -> 浏览 -> 搜索Share to NotionNext`, 然后点击安装。
|
|
||||||
|
|
||||||
#### 手动安装
|
|
||||||
1. 关闭Obsidian
|
|
||||||
2. 从Release下载插件文件,解压到你的obsidian插件目录下。
|
|
||||||
3. 重新打开Obsidian,进入设置,启用插件。
|
|
||||||
4. 在设置中,找到Obsidian to NotionNext,填入你的NotionNext Database ID 和 API token。
|
|
||||||
|
|
||||||
### 使用插件
|
|
||||||
在仓库中我上传了一个模板,你可以直接复制到你的模板文件夹中。然后使用Obsidian的模板功能一键生成新笔记。
|
|
||||||
|
|
||||||
|
|
||||||
如果你不想使用模板,你也可以直接在Obsidian中创建一个新的文件,然后复制下边的内容。然后保存。
|
|
||||||
```markdown
|
|
||||||
---
|
|
||||||
# default value has been set.
|
|
||||||
# for any unwanted value, you can delete it or set it to empty.
|
|
||||||
# for example, if you donot want to set password, you can delete password: "1234" or set it to password: ""
|
|
||||||
# 我已经在插件中设置了默认值,如果有不需要的选项,可以直接删除。
|
|
||||||
# 例如你不需要密码选项,你可以将password: "1234"删除,或者将它设置为空。
|
|
||||||
# !!!!!!!!!!!!
|
|
||||||
# 现在阶段一定不要修改表头的名字, please do not change the name of the header in YAML front matter
|
|
||||||
# !!!!!!!!!!!!
|
|
||||||
titleicon: 📎 # emoji icon, default is 📜, 默认是📜
|
|
||||||
date: 2023-07-23 # default is today, 默认是今天。 Format is YYYY-MM-DD, 格式是YYYY-MM-DD
|
|
||||||
coverurl: https://img.jxpeng.dev/2023/08/843e27a210847f05a0f7cfb121fec100.jpg # default is empty, 默认是空
|
|
||||||
type: Post # Post or Page, default is Post, 默认是Post
|
|
||||||
slug: test # slug for url, default is empty, 默认是空
|
|
||||||
stats: Draft # Draft, Invisible, Published, default is Draft, 默认是Draft
|
|
||||||
category: test # default is 'Obsidian', 默认是'Obsidian'
|
|
||||||
summary: this is a summary for test post # default is empty, 默认是空
|
|
||||||
icon: fa-solid fa-camera # you can ignore this, default is empty, 默认是空,可直接删除
|
|
||||||
password: "1234" # if you donot want to set password, you can delete this line, default is empty, 默认是空,可直接删除
|
|
||||||
# 现在必须开启tags选项,否则会报错
|
|
||||||
tags:
|
|
||||||
- test # tags for post
|
|
||||||
- web # add more tags if you want
|
|
||||||
---
|
|
||||||
|
|
||||||
Contents Below
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
模板使用如下:
|
|
||||||

|
|
||||||
|
|
||||||
**插件预览如下**
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**以下为原README.md**
|
|
||||||
|
|
||||||
非常感谢原作者的开发,我只是在原作者的基础之上进行了修改,增加了一些功能。如果你觉得这个插件对你有帮助,可以给[原作者](https://github.com/EasyChris/obsidian-to-notion)一个star。
|
|
||||||
|
|
||||||
# Obsidian to Notion
|
|
||||||
[](https://github.com/Easychris/obsidian-to-notion/actions/workflows/CI.yml)
|
|
||||||
[](https://github.com/Easychris/obsidian-to-notion/actions/workflows/release.yml)
|
|
||||||
[](https://raw.githubusercontent.com/EasyChris/obsidian-to-notion/master/LICENSE)
|
|
||||||
[](https://GitHub.com/Easychris/obsidian-to-notion/releases/)
|
|
||||||
[](https://github.com/Easychris/obsidian-to-notion/releases)
|
|
||||||
|
|
||||||
Obsidian share to Notion [English](README.md)
|
|
||||||
|
|
||||||
将obsidian文件一键分享到Notion,并在obsidian中添加Notion分享链接
|
|
||||||
|
|
||||||
如果能对你有所帮助,欢迎给一个star支持。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
# 使用方式
|
|
||||||
## 安装插件
|
|
||||||
|
|
||||||
### 市场下载
|
|
||||||
插件市场搜索 noiton 即可下载
|
|
||||||
|
|
||||||

|
|
||||||
### BRAT
|
|
||||||
插件中中心搜索 BRAT
|
|
||||||
添加 `EasyChris/obsidian-to-notion` 到 BRAT 插件安装列表中
|
|
||||||
返回插件中心启用即可
|
|
||||||
### 手动安装
|
|
||||||
```
|
|
||||||
cd YOUR_OBSIDIAN_FOLDER/.obsidian/plugins/
|
|
||||||
git clone https://github.com/EasyChris/obsidian-to-notion.git
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## 申请 Notion API
|
|
||||||
官方参考文档:[https://developers.notion.com/docs](https://developers.notion.com/docs)
|
|
||||||
### 第 1 步:创建integration。
|
|
||||||
转到 [https://www.notion.com/my-integrations](https://www.notion.com/my-integrations)
|
|
||||||
创建完成之后,复制`secrets toekn`
|
|
||||||

|
|
||||||
|
|
||||||
### 第2步:与你的集成共享一个数据库
|
|
||||||
新建一个的page(权限为公开)
|
|
||||||
在page中新建一个数据库 -> 需要`full page database`
|
|
||||||

|
|
||||||
|
|
||||||
将`integration`添加到你的新建的数据库中
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
#### 注意
|
|
||||||
|
|
||||||
数据库的第一个自定义名称必须是 "Name",否则同步会失败。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 第三步:复制database ID
|
|
||||||
|
|
||||||
```
|
|
||||||
https://www.notion.so/myworkspace/a8aec43384f447ed84390e8e42c2e089?v=...
|
|
||||||
|--------- Database ID --------|
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 打开插件配置
|
|
||||||
将得到的 `NOTION_API_KEY` 和 `DATABASE_ID`填入配置当中
|
|
||||||

|
|
||||||
|
|
||||||
## 上传文件内容到notion
|
|
||||||
点击上传notion的按钮
|
|
||||||

|
|
||||||
上传成功之后会自动生成一个分享链接
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
## 页面 Banner 链接[可选]
|
|
||||||
默认可以不填写
|
|
||||||
横幅URL必须是图像URL,例如:https://i.imgur.com/xxx.jpg
|
|
||||||
|
|
||||||
## Notion ID [可选]
|
|
||||||
Notion ID是你想分享文件的页面ID。
|
|
||||||
如果你不写它,notion将分享到默认的链接,如:https://www.notion.so/myworkspace/a8aec43384f447ed84390,访问这个页面将重定向到你的网站页面。
|
|
||||||
如果你写了Notion ID,它将分享到页面链接如:https://your_user_name.notion.site/myworkspace/a8aec43384f447ed84390。不需要重定向网址。
|
|
||||||
|
|
||||||
## 同步图片
|
|
||||||
|
|
||||||
使用 [Obsidian Image Auto Upload Plugin](https://github.com/renmu123/obsidian-image-auto-upload-plugin) 插件,配置你自己的 cos 或者 oss,将图片存储到你自己的云存储,然后在 obsidian 中使用图片链接即可。该插件会自动帮你上传图片,并替换成链接。
|
|
||||||
|
|
||||||
# 请我喝杯咖啡
|
|
||||||
|
|
||||||
[顿顿饭](https://dun.mianbaoduo.com/@easy)
|
|
||||||
|
|
||||||
# 感谢
|
|
||||||
[开发流程 | Obsidian 插件开发文档](https://luhaifeng666.github.io/obsidian-plugin-docs-zh/zh/getting-started/development-workflow.html)
|
|
||||||
|
|
||||||
[GitHub - devbean/obsidian-wordpress: An obsidian plugin for publishing docs to WordPress.](https://github.com/devbean/obsidian-wordpress)
|
|
||||||
|
|
||||||
[GitHub - obsidianmd/obsidian-api](https://github.com/obsidianmd/obsidian-api)
|
|
||||||
|
|
||||||
[GitHub - zhaohongxuan/obsidian-weread-plugin: Obsidian Weread Plugin is an plugin to sync Weread(微信读书) hightlights and annotations into your Obsidian Vault.](https://github.com/zhaohongxuan/obsidian-weread-plugin)
|
|
||||||
|
|||||||
335
README.md
335
README.md
@@ -1,329 +1,22 @@
|
|||||||
# Obsidian to NotionNext
|
# Obsidian to NotionNext
|
||||||
[中文文档](README-zh.md)
|
|
||||||
|
|
||||||
Thanks to the [original author](https://github.com/EasyChris/obsidian-to-notion) for developing such a useful plugin that can synchronize Obsidian to Notion. However, the original repository can only sync Name and Tags information. For those like me who use [NotionNext](https://github.com/tangly1024/NotionNext) to set up their website, this presents some limitations. Every time I import, I need to make a lot of modifications.
|
[](https://github.com/jxpeng98/obsidian-to-NotionNext/actions/workflows/test.yml)
|
||||||
|
[](https://github.com/jxpeng98/obsidian-to-NotionNext/actions/workflows/release.yml)
|
||||||
|
[](https://GitHub.com/jxpeng98/obsidian-to-NotionNext/releases/)
|
||||||
|
[](https://github.com/jxpeng98/obsidian-to-NotionNext/releases/)
|
||||||
|
|
||||||
Thus, based on the [original author's work](https://github.com/EasyChris/obsidian-to-notion), I've added a feature to match the [NotionNext](https://github.com/tangly1024/NotionNext) template. This way, you can edit directly in Obsidian and publish with a single click after organizing.
|
[中文版](README-zh.md)
|
||||||
|
Share files from Obsidian to any Notion database using the Notion API. This plugin supports general Notion databases, NotionNext blog templates, and fully custom database schemas.
|
||||||
|
|
||||||
**Now, support both NotionNext and General databases.**
|
**For detailed instructions, please visit the [documentation website](https://obsidian-to-notionnext.pages.dev/).**
|
||||||
|
|
||||||
## Update
|
## Features
|
||||||
### 0.3.0 (Big Update)
|
|
||||||
- From this version, you can **modify the first column name (title column, default: 'title')** as you want. (**Note: You need to have the 'tags' column in your Notion General database, and add `tags:` in your markdown frontmatter. If not, you will receive `network error 400`. But you can leave the `tags:` blank.**)
|
|
||||||
|
|
||||||

|
- **Multiple Database Types**: Supports General, NotionNext, and Custom databases.
|
||||||
Like this:
|
- **Custom Properties**: Map any frontmatter key to any Notion property in custom databases.
|
||||||

|
- **Flexible Syncing**: Choose which database to sync to on-the-fly.
|
||||||
- Add a switch button to control whether display the setting tabs in the plugin settings for both NotionNext and Notion General databases.
|
- **Attachment Upload**: Automatically uploads local images and PDFs to Notion, supporting Wikilinks and Markdown links.
|
||||||
|
|
||||||

|
## Acknowledgment
|
||||||
|
|
||||||
|
This plugin is a fork of the original [obsidian-to-notion](https://github.com/EasyChris/obsidian-to-notion) by EasyChris, with added features and support for more flexible database configurations.
|
||||||
|
|
||||||
### 0.2.6
|
|
||||||
- Add a switch button to control whether to show the upload command in the command palette.
|
|
||||||
|
|
||||||

|
|
||||||
If you turn off the button for the General database, you won't see the option for the General database in the upload command list.
|
|
||||||
|
|
||||||
If you turn off the button for the NotionNext database, you won't see the option for the NotionNext database in the upload command list.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 0.2.3
|
|
||||||
- Fix the bug, now you can update normally.
|
|
||||||
### 0.2.2
|
|
||||||
- Support both NotionNext and General Notion database.
|
|
||||||
- You can have one NotionNext and one General Notion database.
|
|
||||||
- General Notion database can only have `title` and `tags` columns, and `tags` columns should be the multi-selected property. **the name of the columns is case sensitive. You should use small letter.**
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 0.2.1
|
|
||||||
- Restructure the code
|
|
||||||
### 0.2.0
|
|
||||||
- From this version, the interactive logic has been rewritten. When you click the ribbon icon, it will display the sync command for all presetting NotionNext databases. You can choose the database you want to sync to. **However, only NotionNext database is supported for now.**
|
|
||||||
|
|
||||||
### 0.1.10
|
|
||||||
- Fix the Chinese support in the settings.
|
|
||||||
### 0.1.8
|
|
||||||
- Rebuild the uploadCommand function, and add one button to select the different databases. **However, only NotionNext database is supported for now.**
|
|
||||||
### 0.1.7
|
|
||||||
- [x] Removed the `convert tag` option. Now, you can directly add tags in the YAML front matter. If you don't want to add tags, you can delete the tags in the YAML front matter or leave the tags blank.
|
|
||||||
|
|
||||||
|
|
||||||
## How to Use
|
|
||||||
### Precautions
|
|
||||||
For now, this plugin is exclusively for [NotionNext](https://github.com/tangly1024/NotionNext). If you're not using this template, you'll keep receiving `error 400`.
|
|
||||||
|
|
||||||
For those without a NotionNext requirement, please use the original [Obsidian-to-notion](https://github.com/EasyChris/obsidian-to-notion).
|
|
||||||
|
|
||||||
### Pre-Installation Steps
|
|
||||||
Before installing the plugin, you must have set up the following:
|
|
||||||
1. Your NotionNext database.
|
|
||||||
2. According to the original author's readme.md, set up the Notion API, and it should already be associated with your NotionNext repository.
|
|
||||||
3. NotionNext Database ID
|
|
||||||
4. Your NotionNext database should have the following contents:
|
|
||||||
- type
|
|
||||||
- title
|
|
||||||
- slug
|
|
||||||
- category
|
|
||||||
- tags
|
|
||||||
- date
|
|
||||||
- status
|
|
||||||
- summary
|
|
||||||
- password
|
|
||||||
- icon
|
|
||||||
|
|
||||||
From version 0.0.6, I also add the following contents:
|
|
||||||
- titleicon: the unique icon for each post, it can only support emoji currently.
|
|
||||||
- cover url: the cover image for each post, it should be end with a type of image, such as .jpg, .png, .gif, etc.
|
|
||||||
|
|
||||||
**If you've directly copied the NotionNext template, these contents should already exist. I've made changes to this plugin based on the original author's work. All you need to ensure is that your database has the above content, and every letter is in lowercase!!!**
|
|
||||||
|
|
||||||
**⚠️⚠️⚠️: All headers are in lowercase!!! The order doesn't matter!**
|
|
||||||
|
|
||||||
### Plugin Installation
|
|
||||||
#### Install via Community Plugins
|
|
||||||
Open `Obsidian settings -> Community Plugins -> Browse -> NotionNext -> Share to NotionNext`
|
|
||||||
|
|
||||||
#### Mannually Install
|
|
||||||
1. Close Obsidian.
|
|
||||||
2. Download the plugin file from Release and unzip it into your Obsidian plugin directory.
|
|
||||||
3. Re-open Obsidian, go to settings, and enable the plugin.
|
|
||||||
4. In the settings, find Obsidian to NotionNext, and enter your NotionNext Database ID and API token.
|
|
||||||
|
|
||||||
### How to Use
|
|
||||||
### Using the Plugin
|
|
||||||
In the repository, I have uploaded a template which you can directly copy into your template folder. After that, use Obsidian's template feature to create a new note with one click.
|
|
||||||
|
|
||||||
If you don't want to use the template, you can also directly create a new file in Obsidian and then copy the content below. Then save it.
|
|
||||||
```markdown
|
|
||||||
---
|
|
||||||
# default value has been set.
|
|
||||||
# for any unwanted value, you can delete it or set it to empty.
|
|
||||||
# for example, if you donot want to set password, you can delete password: "1234" or set it to password: ""
|
|
||||||
# 我已经在插件中设置了默认值,如果有不需要的选项,可以直接删除。
|
|
||||||
# 例如你不需要密码选项,你可以将password: "1234"删除,或者将它设置为空。
|
|
||||||
# !!!!!!!!!!!!
|
|
||||||
# 现在阶段一定不要修改表头的名字, please do not change the name of the header in YAML front matter
|
|
||||||
# !!!!!!!!!!!!
|
|
||||||
titleicon: 📎 # emoji icon, default is 📜, 默认是📜
|
|
||||||
date: 2023-07-23 # default is today, 默认是今天。 Format is YYYY-MM-DD, 格式是YYYY-MM-DD
|
|
||||||
coverurl: https://img.jxpeng.dev/2023/08/843e27a210847f05a0f7cfb121fec100.jpg # default is empty, 默认是空
|
|
||||||
type: Post # Post or Page, default is Post, 默认是Post
|
|
||||||
slug: test # slug for url, default is empty, 默认是空
|
|
||||||
stats: Draft # Draft, Invisible, Published, default is Draft, 默认是Draft
|
|
||||||
category: test # default is 'Obsidian', 默认是'Obsidian'
|
|
||||||
summary: this is a summary for test post # default is empty, 默认是空
|
|
||||||
icon: fa-solid fa-camera # you can ignore this, default is empty, 默认是空,可直接删除
|
|
||||||
password: "1234" # if you donot want to set password, you can delete this line, default is empty, 默认是空,可直接删除
|
|
||||||
# 现在必须开启tags选项,否则会报错
|
|
||||||
tags:
|
|
||||||
- test # tags for post
|
|
||||||
- web # add more tags if you want
|
|
||||||
---
|
|
||||||
|
|
||||||
Contents Below
|
|
||||||
|
|
||||||
```
|
|
||||||
Usage of the template is as follows:
|
|
||||||

|
|
||||||
|
|
||||||
**Plugin preview is shown below**
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Original README.md**
|
|
||||||
|
|
||||||
Many Thanks for the original author's work. I've only made some changes to the original author's work. If you find this plugin useful, please give the [original author](https://github.com/EasyChris/obsidian-to-notion) a star.
|
|
||||||
|
|
||||||
# Obsidian to Notion
|
|
||||||
[](https://github.com/Easychris/obsidian-to-notion/actions/workflows/CI.yml)
|
|
||||||
[](https://github.com/Easychris/obsidian-to-notion/actions/workflows/release.yml)
|
|
||||||
[](https://raw.githubusercontent.com/EasyChris/obsidian-to-notion/master/LICENSE)
|
|
||||||
[](https://GitHub.com/Easychris/obsidian-to-notion/releases/)
|
|
||||||
[](https://github.com/Easychris/obsidian-to-notion/releases)
|
|
||||||
|
|
||||||
|
|
||||||
Share of obsidian to Notion [中文文档](README-zh.md)
|
|
||||||
|
|
||||||
Sharing files from Obsidian to Notion with a single click, and Obsidian will automatically add the Notion share link
|
|
||||||
|
|
||||||
You are welcome to offer it a star if it can benefit you.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
# TODO
|
|
||||||
### [TODO Board](https://github.com/users/EasyChris/projects/3/views/1)
|
|
||||||
- [x] support for custom page banner
|
|
||||||
- [x] update the exsit page
|
|
||||||
- [x] support for mult language
|
|
||||||
- [x] support for auto copy the share link to clipboard
|
|
||||||
- [x] support for mobile
|
|
||||||
- [x] support tags thank for [@jannikbuscha](https://github.com/jannikbuscha)
|
|
||||||
- [ ] transfer the bi-link format like [[]] into the format that Notion supports.
|
|
||||||
|
|
||||||
|
|
||||||
# How to use
|
|
||||||
## Install the plugin
|
|
||||||
|
|
||||||
### Marketplace download
|
|
||||||
Open obsidian setting -> Add plugin -> Search -> notion
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### BRAT
|
|
||||||
Enter `BRAT` into the plugin market center to find it.
|
|
||||||
Add `EasyChris/obsidian-to-notion` to the list of BRAT plugins that have been installed.
|
|
||||||
Return to the plugin center and turn it on.
|
|
||||||
### Manual installation
|
|
||||||
```
|
|
||||||
cd YOUR_OBSIDIAN_FOLDER/.obsidian/plugins/
|
|
||||||
git clone https://github.com/EasyChris/obsidian-to-notion.git
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Apply Notion API
|
|
||||||
Official reference documentation: [https://developers.notion.com/docs](https://developers.notion.com/docs)
|
|
||||||
### Step 1: Create integration.
|
|
||||||
Go to [https://www.notion.com/my-integrations](https://www.notion.com/my-integrations)
|
|
||||||
Once created, copy `secrets toekn`
|
|
||||||

|
|
||||||
|
|
||||||
#### Note:
|
|
||||||
database first custom name must be "Name", otherwise sync to notion will be failed
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Step 2: Share a database with your integration
|
|
||||||
Create a new page (with public permissions)
|
|
||||||
Create a new database in the page -> you need `full page database`
|
|
||||||

|
|
||||||
|
|
||||||
Add `integration` to your new database
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Step 3: Copy the database ID
|
|
||||||
|
|
||||||
```
|
|
||||||
https://www.notion.so/myworkspace/a8aec43384f447ed84390e8e42c2e089?v=...
|
|
||||||
| --------- Database ID --------|
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Open the plugin configuration
|
|
||||||
Fill the configuration with the `NOTION_API_KEY` and `DATABASE_ID` you got
|
|
||||||

|
|
||||||
|
|
||||||
## Upload file content to notion
|
|
||||||
Click the uploadCommand notion button
|
|
||||||

|
|
||||||
A share link will be automatically generated after successful uploadCommand
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
## Banner URL [option]
|
|
||||||
Banner url must be a image url like: https://i.imgur.com/xxx.jpg
|
|
||||||
If you don't want to use banner, leave it blank
|
|
||||||
|
|
||||||
|
|
||||||
## Convert Tags [option]
|
|
||||||
Transfer the Obsidian tags to the Notion table.
|
|
||||||
It requires the column with the name 'Tags'.
|
|
||||||

|
|
||||||
|
|
||||||
Add tags to your notion page
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
* open plugin convert tags
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
* add tags in the head
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
---
|
|
||||||
tags: [tag1,tag2]
|
|
||||||
---
|
|
||||||
|
|
||||||
this is test tags
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
---
|
|
||||||
tags:
|
|
||||||
- tag4
|
|
||||||
---
|
|
||||||
|
|
||||||
this is test tags
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Thanks for [@jannikbuscha](https://github.com/jannikbuscha) contribution
|
|
||||||
|
|
||||||
|
|
||||||
## Notion ID [option]
|
|
||||||
Notion ID is the your notion site ID that you want to share the file to.
|
|
||||||
if you don't write it, notion will share to the default link like:
|
|
||||||
https://www.notion.so/myworkspace/a8aec43384f447ed84390
|
|
||||||
that visit this page need to redirect to your site url
|
|
||||||
if you write the Notion ID, it will share to the page link like:
|
|
||||||
https://your_user_name.notion.site/myworkspace/a8aec43384f447ed84390.
|
|
||||||
The visiter don't need to redirect url.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Sync image to Notion
|
|
||||||
|
|
||||||
To sync images to your oss or cos bucket, use the [Obsidian Image Auto Upload Plugin](https://github.com/renmu123/obsidian-image-auto-upload-plugin).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Development
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/EasyChris/obsidian-to-notion.git
|
|
||||||
yarn install
|
|
||||||
yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Release
|
|
||||||
|
|
||||||
```
|
|
||||||
node update-version.js
|
|
||||||
./release.sh
|
|
||||||
```
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Thanks
|
|
||||||
[Development Process | Obsidian Plugin Development Documentation](https://luhaifeng666.github.io/obsidian-plugin-docs-zh/zh/getting-started/development-workflow.html)
|
|
||||||
|
|
||||||
[GitHub - devbean/obsidian-wordpress: An obsidian plugin for publishing docs to WordPress.](https://github.com/devbean/obsidian-wordpress)
|
|
||||||
|
|
||||||
[GitHub - obsidianmd/obsidian-api](https://github.com/obsidianmd/obsidian-api)
|
|
||||||
|
|
||||||
[GitHub - Easychris/obsidian-to-notion: Obsidian Weread Plugin is an plugin to sync Weread(微信读书) hightlights and annotations into your Obsidian Vault.](https://github.dev/zhaohongxuan/obsidian-weread-plugin)
|
|
||||||
|
|
||||||
[GitHub - Quorafind/Obsidian-Memos: A quick capture plugin for Obsidian, all data from your notes.](https://github.com/Quorafind/Obsidian-Memos)
|
|
||||||
|
|
||||||
[https://github.com/jannikbuscha/obsidian-to-notion](https://github.com/jannikbuscha)
|
|
||||||
|
|
||||||
# License
|
|
||||||
GNU GPLv3
|
|
||||||
|
|||||||
BIN
doc/10.png
BIN
doc/10.png
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
69
docs/.vitepress/config.ts
Normal file
69
docs/.vitepress/config.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { defineConfig } from 'vitepress'
|
||||||
|
import { withSidebar } from 'vitepress-sidebar';
|
||||||
|
|
||||||
|
// https://vitepress.dev/reference/site-config
|
||||||
|
const vitePressOptions = {
|
||||||
|
title: "Obsidian to NotionNext",
|
||||||
|
description: "A plugin to share files to any Notion database using the Notion API.",
|
||||||
|
|
||||||
|
themeConfig: {
|
||||||
|
socialLinks: [
|
||||||
|
{ icon: 'github', link: 'https://github.com/jxpeng98/obsidian-to-NotionNext' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
locales: {
|
||||||
|
root: {
|
||||||
|
label: 'English',
|
||||||
|
lang: 'en',
|
||||||
|
link: '/en/',
|
||||||
|
themeConfig: {
|
||||||
|
nav: [
|
||||||
|
{ text: 'Home', link: '/en/' },
|
||||||
|
{ text: 'Guide', link: '/en/01-getting-started' }
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
label: '简体中文',
|
||||||
|
lang: 'zh-CN',
|
||||||
|
link: '/zh/',
|
||||||
|
themeConfig: {
|
||||||
|
nav: [
|
||||||
|
{ text: '首页', link: '/zh/' },
|
||||||
|
{ text: '指南', link: '/zh/01-getting-started' }
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const commonSidebarOptions = {
|
||||||
|
documentRootPath: '/',
|
||||||
|
useTitleFromFrontmatter: true,
|
||||||
|
frontmatterTitleFieldName: 'title',
|
||||||
|
collapsed: true,
|
||||||
|
removePrefixAfterOrdering: true,
|
||||||
|
prefixSeparator: '-',
|
||||||
|
hyphenToSpace: true,
|
||||||
|
useFolderTitleFromIndexFile: true,
|
||||||
|
useFolderLinkFromIndexFile: true,
|
||||||
|
sortMenusByName: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const vitePressSidebarOptions = [
|
||||||
|
{
|
||||||
|
...commonSidebarOptions,
|
||||||
|
scanStartPath: '/en',
|
||||||
|
basePath: '/',
|
||||||
|
resolvePath: '/en/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...commonSidebarOptions,
|
||||||
|
scanStartPath: '/zh',
|
||||||
|
basePath: '/',
|
||||||
|
resolvePath: '/zh/',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default defineConfig(withSidebar(vitePressOptions, vitePressSidebarOptions));
|
||||||
45
docs/en/01-getting-started.md
Normal file
45
docs/en/01-getting-started.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
title: Getting Started
|
||||||
|
description: Guide to setting up the Obsidian to NotionNext plugin
|
||||||
|
---
|
||||||
|
|
||||||
|
# Getting Started
|
||||||
|
|
||||||
|
## Get Notion API Token
|
||||||
|
|
||||||
|
Before using the plugin, you need to obtain an API Token from Notion to allow the plugin to access your Notion account.
|
||||||
|
|
||||||
|
1. Go to the [Notion API](https://www.notion.so/profile/integrations) page to create a new integration. You need to be logged into your Notion account.
|
||||||
|
2. Click on `+ New integration`.
|
||||||
|

|
||||||
|
3. Fill in the required information:
|
||||||
|

|
||||||
|
4. Integration successfully created.
|
||||||
|

|
||||||
|
5. Enter the "Configure integration settings" page and **prepare to copy the API Token,** which you will need in the plugin settings.
|
||||||
|

|
||||||
|
|
||||||
|
::: warning
|
||||||
|
**⚠️ Note:** Keep your API Token secure and do not share it with others, as it grants access to your Notion data.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Create a Notion Database and Connect Integration
|
||||||
|
|
||||||
|
Next, you need to create a Notion database to store the notes synced from Obsidian.
|
||||||
|
|
||||||
|
1. In your Notion workspace, click on `Create a new page` to create a new page.
|
||||||
|

|
||||||
|
|
||||||
|
2. create a new page and select the `Database - Full page` option.
|
||||||
|

|
||||||
|
|
||||||
|
3. Click the `...` menu in the top-right corner of the database page, go to `Connections`, and connect your newly created integration.
|
||||||
|

|
||||||
|
|
||||||
|
4. Click `Share` -> `Publish` to publish the database to the web.
|
||||||
|

|
||||||
|
|
||||||
|
5. Click and copy the **Database ID**.
|
||||||
|

|
||||||
|
|
||||||
|
Next, you can proceed to install and configure the plugin in Obsidian by following the [Installation Guide](./02-installation.md).
|
||||||
28
docs/en/02-installation.md
Normal file
28
docs/en/02-installation.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
title: Installation
|
||||||
|
description: Guide to installing the Obsidian to NotionNext plugin
|
||||||
|
---
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
Once you have completed the previous section setup on the Notion side, you can install and configure the plugin in Obsidian.
|
||||||
|
|
||||||
|
1. Open Obsidian's `Settings`.
|
||||||
|

|
||||||
|
|
||||||
|
2. Navigate to `Community plugins`.
|
||||||
|

|
||||||
|
::: tip
|
||||||
|
If you haven't enabled community plugins before, you may need to turn off `Safe mode` first (`Turn on Community plugins`).
|
||||||
|
:::
|
||||||
|
|
||||||
|
3. Go to `Community plugins` -> `Browse`.
|
||||||
|

|
||||||
|
|
||||||
|
4. Search for "NotionNext" and click `Install`.
|
||||||
|

|
||||||
|
|
||||||
|
5. After installation, enable the plugin.
|
||||||
|

|
||||||
|
|
||||||
|
Now, once enabled, you can find the "Share to NotionNext" settings in the plugin settings tab to begin [configuring your databases](./03-configuration.md) in the next step.
|
||||||
127
docs/en/03-configuration.md
Normal file
127
docs/en/03-configuration.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
---
|
||||||
|
title: Configuration
|
||||||
|
description: Guide to configuring Notion databases in the Obsidian to NotionNext plugin
|
||||||
|
---
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
|
||||||
|
After installing the plugin and obtaining the Notion API token and Database ID, you can proceed to configure the databases you want to sync with.
|
||||||
|
|
||||||
|
First, open the plugin settings in Obsidian.
|
||||||
|

|
||||||
|
|
||||||
|
Click "Add new database" to start creating a new database configuration.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
In the plugin settings, you can add and configure the Notion databases you want to sync with. The plugin supports three types of database configurations.
|
||||||
|
|
||||||
|
- [Database Configuration](#database-configuration)
|
||||||
|
- [1️⃣ General Database](#1️⃣-general-database)
|
||||||
|
- [2️⃣ NotionNext Database](#2️⃣-notionnext-database)
|
||||||
|
- [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.
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
|
||||||
|
- **Full Name**: A name for this database configuration.
|
||||||
|
- **Notion API Token**: The API token you obtained in the first step.
|
||||||
|
- **Database ID**: The database ID you obtained in the second step.
|
||||||
|
|
||||||
|
**Notion Database Requirements:**
|
||||||
|
|
||||||
|
- Must contain a `Title` property named `title`.
|
||||||
|
- Must contain a `Multi-select` property named `tags`.
|
||||||
|

|
||||||
|
|
||||||
|
Then, fill in the configuration fields as shown below:
|
||||||
|
|
||||||
|
- **Database Full Name**: the name you want to give this database to recognise it in the database list (e.g. "Learning Database").
|
||||||
|
- **Database Abbreviate Name**: a short abbreviation for this database (e.g. "learning").
|
||||||
|
- **Notion Tags Sync**: enable this option if you want to sync tags columns from Obsidian to Notion (**if you enable this option, make sure you add tags in your notes**).
|
||||||
|
- **Customise title property**: the property in your Notion database that you want to use as the primary column (default is `title`).
|
||||||
|
- **Notion API Token**: the API token you obtained in the first step.
|
||||||
|
- **Database ID**: the database ID you obtained in the second step.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 2️⃣ NotionNext Database
|
||||||
|
|
||||||
|
This type is specifically designed for users of the [NotionNext](https://github.com/tangly1024/NotionNext) blog system, with properties preset to match the NotionNext template.
|
||||||
|
|
||||||
|
Config the database similarly to the General Database type:
|
||||||
|
|
||||||
|
- **Database Full Name**: A name for this database configuration.
|
||||||
|
- **Database Abbreviate Name**: A short abbreviation for this database.
|
||||||
|
- **Notion API Token**: The API token you obtained in the first step.
|
||||||
|
- **Database ID**: The database ID you obtained in the second step.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 3️⃣ Custom Database
|
||||||
|
|
||||||
|
This type offers maximum flexibility for advanced users. It allows you to map any frontmatter metadata from your Obsidian notes to any property in your Notion database.
|
||||||
|
|
||||||
|
You should first config the database similarly to the General Database type:
|
||||||
|
- **Database Full Name**: A name for this database configuration.
|
||||||
|
- **Database Abbreviate Name**: A short abbreviation for this database.
|
||||||
|
- **Notion API Token**: The API token you obtained in the first step.
|
||||||
|
- **Database ID**: The database ID you obtained in the second step.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Once you have filled in the basic configuration, you can start to add new properties.
|
||||||
|
|
||||||
|
You can click the `Add New Property` button to create a new property mapping.
|
||||||
|
|
||||||
|
First, you need to add the title property mapping, which is required for the Notion database to function correctly.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
::: tip
|
||||||
|
Remember to set the "Notion Property" to the property name in your Notion database (case-sensitive) and the "Frontmatter Key" to the key in your Obsidian note's frontmatter.
|
||||||
|
:::
|
||||||
|
|
||||||
|
After adding the title property, you can continue to add more properties as needed.
|
||||||
|

|
||||||
|
|
||||||
|
For now, the plugin supports the following Notion property types:
|
||||||
|
- Text
|
||||||
|
- Number
|
||||||
|
- Select
|
||||||
|
- Multi-select
|
||||||
|
- Date
|
||||||
|
- Files & Media
|
||||||
|
- Checkbox
|
||||||
|
- URL
|
||||||
|
- Email
|
||||||
|
- Phone Number
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
::: info
|
||||||
|
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.
|
||||||
200
docs/en/04-sync.md
Normal file
200
docs/en/04-sync.md
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
---
|
||||||
|
title: Syncing Notes
|
||||||
|
description: How to sync your Obsidian notes to Notion using the NotionNext plugin
|
||||||
|
---
|
||||||
|
|
||||||
|
# Syncing Notes
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
[document.pdf](document.pdf)
|
||||||
|
[document.pdf](folder/document.pdf)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TODO
|
||||||
|
|
||||||
|
- [ ] Obsidian URL Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] App URL Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### How Auto Sync Works
|
||||||
|
|
||||||
|
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
|
||||||
|
- **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: 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 B: Synced to One Database
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: My Article
|
||||||
|
NotionID-blog: abc123
|
||||||
|
autosync-database: [blog]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
**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
|
||||||
|
autosync-database: [blog, portfolio, notes]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
- ✅ 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
|
||||||
|
- 📝 **No Action Required:** Changes are automatically synced to all linked databases
|
||||||
|
|
||||||
|
#### Scenario D: 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. **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
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
Having issues with auto sync? Check the [Troubleshooting Guide](05-troubleshooting.md) for detailed solutions to common problems.
|
||||||
74
docs/en/05-troubleshooting.md
Normal file
74
docs/en/05-troubleshooting.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
title: Troubleshooting
|
||||||
|
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
|
||||||
69
docs/en/06-changelog.md
Normal file
69
docs/en/06-changelog.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
title: Changelog
|
||||||
|
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 (2026-01-29)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Auto Sync**: Automatically sync notes on content or frontmatter changes, with configurable delay and multi-database support
|
||||||
|
- **Attachment Upload**: Upload local images and PDFs to Notion via the File Upload API and insert them as `image`/`file` blocks
|
||||||
|
- **Auto-copy Notion Link**: Option to copy the Notion page link to clipboard after syncing
|
||||||
|
- **Auto-sync frontmatter key**: Customize the frontmatter key for auto-sync database lists (default: `autosync-database`)
|
||||||
|
- Comprehensive i18n support for UI and notifications
|
||||||
|
- Prerelease workflow for beta testing via GitHub Actions and BRAT
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved auto-sync behavior and notices for files without `autosync-database` or missing NotionID
|
||||||
|
- Limited attachment link parsing to **Wikilinks** and **standard Markdown links** (Obsidian/App URL formats are now TODO/disabled)
|
||||||
|
- Standardized Notion API request header `Notion-Version` to `2025-09-03`
|
||||||
|
- Reduced per-file upload limit to **5MB** to maximize compatibility across Notion plans
|
||||||
|
- Enhanced settings tab and documentation for auto-sync usage
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed mobile compatibility issues by using `window.setTimeout` instead of `NodeJS.Timeout`
|
||||||
|
- Prevented sync loops and improved change detection for frontmatter/body updates
|
||||||
|
- File placeholder tokens no longer break due to Markdown underscore parsing
|
||||||
|
- Better block ordering when attachments are on standalone lines in Markdown
|
||||||
|
- Preserve image captions when converting `external` images to `file_upload`
|
||||||
|
- Avoid duplicate filename display on uploaded `file` blocks
|
||||||
|
- Fixed `undefined` appearing in sync success notification by adding missing `sync-preffix` i18n key
|
||||||
|
|
||||||
|
## 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
|
||||||
10
docs/en/07-roadmap.md
Normal file
10
docs/en/07-roadmap.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
title: Roadmap
|
||||||
|
description: Planned features and improvements for Obsidian to NotionNext
|
||||||
|
---
|
||||||
|
|
||||||
|
# Roadmap
|
||||||
|
|
||||||
|
- [x] Automatic Syncing: Implement automatic syncing of notes at regular intervals or upon changes.
|
||||||
|
- [x] File Attachments: Support for syncing file attachments from Obsidian to Notion.
|
||||||
|
- [ ] Relation Property: Enable mapping of Obsidian links to Notion relation properties.
|
||||||
27
docs/en/index.md
Normal file
27
docs/en/index.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
title: Introduction
|
||||||
|
description: Share files to any Notion database using the Notion API
|
||||||
|
order: 0
|
||||||
|
---
|
||||||
|
|
||||||
|
# Obsidian to NotionNext
|
||||||
|
|
||||||
|
In this documentation, you will find guides and instructions on how to set up and use the Obsidian to NotionNext plugin. This plugin allows you to share your Obsidian notes directly to Notion databases using the Notion API.
|
||||||
|
|
||||||
|
This guide will walk you through the process of setting up the Obsidian to NotionNext plugin.
|
||||||
|
|
||||||
|
[Getting Started](./01-getting-started.md)
|
||||||
|
|
||||||
|
[Installation](./02-installation.md)
|
||||||
|
|
||||||
|
[Configuration](./03-configuration.md)
|
||||||
|
|
||||||
|
[Syncing Notes](./04-sync.md)
|
||||||
|
|
||||||
|
[Troubleshooting](./05-troubleshooting.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[Changelog](./06-changelog.md)
|
||||||
|
|
||||||
|
[Roadmap](./07-roadmap.md)
|
||||||
29
docs/index.md
Normal file
29
docs/index.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
# https://vitepress.dev/reference/default-theme-home-page
|
||||||
|
layout: home
|
||||||
|
|
||||||
|
hero:
|
||||||
|
name: "Obsidian to NotionNext"
|
||||||
|
text: "Share files to any Notion database using the Notion API"
|
||||||
|
tagline: Support All Notion databases, including NotionNext and General Notion databases, and customise the database list.
|
||||||
|
actions:
|
||||||
|
- theme: brand
|
||||||
|
text: Get Started
|
||||||
|
link: /en/
|
||||||
|
- theme: brand
|
||||||
|
text: 现在开始
|
||||||
|
link: /zh/
|
||||||
|
- theme: alt
|
||||||
|
text: View on GitHub
|
||||||
|
link: https://github.com/jxpeng98/obsidian-to-NotionNext
|
||||||
|
|
||||||
|
features:
|
||||||
|
- title: All Notion Databases
|
||||||
|
details: Support for both general and NotionNext-specific databases.
|
||||||
|
- title: Custom Properties
|
||||||
|
details: Customize database properties to fit your workflow.
|
||||||
|
- title: Long Note Sync
|
||||||
|
details: Sync long notes seamlessly.
|
||||||
|
- title: Callout Support
|
||||||
|
details: Obsidian callouts are converted to Notion callout blocks.
|
||||||
|
---
|
||||||
17
docs/package.json
Normal file
17
docs/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "obsidian-to-notionnext-docs",
|
||||||
|
"private": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vitepress dev",
|
||||||
|
"build": "vitepress build",
|
||||||
|
"preview": "vitepress preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitepress": "^2.0.0-alpha.12",
|
||||||
|
"vitepress-sidebar": "^1.33.0",
|
||||||
|
"vue": "^3.5.22"
|
||||||
|
}
|
||||||
|
}
|
||||||
1756
docs/pnpm-lock.yaml
generated
Normal file
1756
docs/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
docs/zh/01-getting-started.md
Normal file
45
docs/zh/01-getting-started.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
title: 开始使用
|
||||||
|
description: Obsidian to NotionNext 插件设置指南
|
||||||
|
---
|
||||||
|
|
||||||
|
# 开始使用
|
||||||
|
|
||||||
|
## 获取 Notion API 令牌
|
||||||
|
|
||||||
|
在使用本插件前,你需要从 Notion 获取一个 API 令牌(Token),以授权插件访问你的 Notion 账户。
|
||||||
|
|
||||||
|
1. 前往 [Notion API](https://www.notion.so/profile/integrations) 页面来创建一个新的集成(integration)。你需要先登录你的 Notion 账户。
|
||||||
|
2. 点击 `+ New integration`。
|
||||||
|

|
||||||
|
3. 填写一些基本信息:
|
||||||
|

|
||||||
|
4. 成功创建集成。
|
||||||
|

|
||||||
|
5. 进入“配置集成设置”页面,**准备好复制 API 令牌**,你很快就会在插件设置中用到它。
|
||||||
|

|
||||||
|
|
||||||
|
::: warning
|
||||||
|
**⚠️ 注意:** 请妥善保管你的 API 令牌,不要与他人分享,因为它授予了访问你 Notion 数据的权限。
|
||||||
|
:::
|
||||||
|
|
||||||
|
## 创建 Notion 数据库并连接集成
|
||||||
|
|
||||||
|
接下来,你需要创建一个 Notion 数据库,用于存放从 Obsidian 同步过来的笔记。
|
||||||
|
|
||||||
|
1. 在你的 Notion 工作区中,点击 `Create a new page` 来创建一个新页面。
|
||||||
|

|
||||||
|
|
||||||
|
2. 创建一个新页面后,选择 `Database - Full page` 选项。
|
||||||
|

|
||||||
|
|
||||||
|
3. 点击数据库页面右上角的 `...` 菜单,进入 `Connections`,然后连接你刚刚创建的集成。
|
||||||
|

|
||||||
|
|
||||||
|
4. 点击 `Share` -> `Publish`,将数据库发布到网络。
|
||||||
|

|
||||||
|
|
||||||
|
5. 点击并复制 **数据库 ID**。
|
||||||
|

|
||||||
|
|
||||||
|
接下来,你可以根据[安装指南](./02-installation.md)在 Obsidian 中安装和配置插件了。
|
||||||
28
docs/zh/02-installation.md
Normal file
28
docs/zh/02-installation.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
title: 安装插件
|
||||||
|
description: 如何安装 Obsidian to NotionNext 插件
|
||||||
|
---
|
||||||
|
|
||||||
|
# 安装插件
|
||||||
|
|
||||||
|
当你在 Notion 端完成上一章节的设置后,就可以在 Obsidian 里安装和配置插件了。
|
||||||
|
|
||||||
|
1. 打开 Obsidian 的 `设置`。
|
||||||
|

|
||||||
|
|
||||||
|
2. 找到 `第三方插件` 菜单。
|
||||||
|

|
||||||
|
::: tip
|
||||||
|
如果你之前没有启用过第三方插件,可能需要先关闭 `安全模式`(点击 `打开社区插件`)。
|
||||||
|
:::
|
||||||
|
|
||||||
|
3. 进入 `社区插件` -> `浏览`。
|
||||||
|

|
||||||
|
|
||||||
|
4. 搜索 "NotionNext" 并点击 `安装`。
|
||||||
|

|
||||||
|
|
||||||
|
5. 安装完成后,记得 `启用` 插件。
|
||||||
|

|
||||||
|
|
||||||
|
启用后,你就可以在插件设置列表中找到 "Share to NotionNext",并在下一步开始[配置你的数据库](./03-configuration.md)了。
|
||||||
112
docs/zh/03-configuration.md
Normal file
112
docs/zh/03-configuration.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
---
|
||||||
|
title: 配置数据库
|
||||||
|
description: 在 Obsidian to NotionNext 插件中配置 Notion 数据库的指南
|
||||||
|
---
|
||||||
|
|
||||||
|
# 配置数据库
|
||||||
|
|
||||||
|
在安装好插件并获取了 Notion API 令牌和数据库 ID 之后,你就可以开始配置要同步的数据库了。
|
||||||
|
|
||||||
|
首先,在 Obsidian 中打开插件设置。
|
||||||
|

|
||||||
|
|
||||||
|
点击 “添加新数据库” 来创建一个新的数据库配置。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
插件支持三种类型的数据库配置,你可以根据自己的需求选择。
|
||||||
|
|
||||||
|
- [数据库配置](#数据库配置)
|
||||||
|
- [1️⃣ 通用数据库](#1️⃣-通用数据库)
|
||||||
|
- [2️⃣ NotionNext 数据库](#2️⃣-notionnext-数据库)
|
||||||
|
- [3️⃣ 自定义数据库](#3️⃣-自定义数据库)
|
||||||
|
- [完成配置](#完成配置)
|
||||||
|
|
||||||
|
## 1️⃣ 通用数据库
|
||||||
|
|
||||||
|
这是最基础的数据库类型,适合大多数用户。
|
||||||
|
|
||||||
|
**配置项:**
|
||||||
|
|
||||||
|
- **数据库全名**: 给这个数据库配置起一个容易识别的名字。
|
||||||
|
- **Notion API 令牌**: 你在第一步中获取的 API 令牌。
|
||||||
|
- **数据库 ID**: 你在第二步中获取的数据库 ID。
|
||||||
|
|
||||||
|
**Notion 数据库要求:**
|
||||||
|
|
||||||
|
- 必须包含一个名为 `title` 的 `Title` 属性。
|
||||||
|
- 必须包含一个名为 `tags` 的 `Multi-select` 属性。
|
||||||
|

|
||||||
|
|
||||||
|
然后,像下面这样填写配置字段:
|
||||||
|
|
||||||
|
- **数据库全名 (Database Full Name)**: 你希望这个数据库在列表中显示的名字(例如:“学习资料库”)。
|
||||||
|
- **数据库简称 (Database Abbreviate Name)**: 一个简短的别名(例如:“learning”)。
|
||||||
|
- **同步 Notion 标签 (Notion Tags Sync)**: 如果希望将 Obsidian 的标签同步到 Notion,请启用此选项(**启用后,请确保你的笔记中包含 `tags`**)。
|
||||||
|
- **自定义标题属性 (Customise title property)**: 你想用作主列的 Notion 数据库属性(默认为 `title`)。
|
||||||
|
- **Notion API 令牌 (Notion API Token)**: 你在第一步中获取的 API 令牌。
|
||||||
|
- **数据库 ID (Database ID)**: 你在第二步中获取的数据库 ID。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 2️⃣ NotionNext 数据库
|
||||||
|
|
||||||
|
这个类型是为 [NotionNext](https://github.com/tangly1024/NotionNext) 博客系统的用户量身定制的,其属性已预设好以匹配 NotionNext 模板。
|
||||||
|
|
||||||
|
配置方式与通用数据库类似:
|
||||||
|
|
||||||
|
- **数据库全名**: 为这个数据库配置起一个名字。
|
||||||
|
- **数据库简称**: 一个简短的别名。
|
||||||
|
- **Notion API 令牌**: 你在第一步中获取的 API 令牌。
|
||||||
|
- **数据库 ID**: 你在第二步中获取的数据库 ID。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 3️⃣ 自定义数据库
|
||||||
|
|
||||||
|
这个类型为高级用户提供了极大的灵活性,允许你将 Obsidian 笔记中的任何 frontmatter 元数据映射到 Notion 数据库的任意属性。
|
||||||
|
|
||||||
|
首先,你需要像配置通用数据库一样填写基本信息:
|
||||||
|
- **数据库全名**: 为这个数据库配置起一个名字。
|
||||||
|
- **数据库简称**: 一个简短的别名。
|
||||||
|
- **Notion API 令牌**: 你在第一步中获取的 API 令牌。
|
||||||
|
- **数据库 ID**: 你在第二步中获取的数据库 ID。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
填写完基本配置后,你就可以开始添加新的属性映射了。
|
||||||
|
|
||||||
|
你可以点击 `Add New Property` 按钮来创建一个新的属性映射。
|
||||||
|
|
||||||
|
首先,你需要添加标题属性的映射,这是 Notion 数据库正常工作所必需的。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
::: tip
|
||||||
|
请记住,"Notion 属性 (Notion Property)" 需要填写你在 Notion 数据库中的属性名称(区分大小写),而 "Frontmatter 键 (Frontmatter Key)" 则是你 Obsidian 笔记 frontmatter 中的键。
|
||||||
|
:::
|
||||||
|
|
||||||
|
添加完标题属性后,你可以根据需要继续添加更多属性。
|
||||||
|

|
||||||
|
|
||||||
|
目前,插件支持以下 Notion 属性类型:
|
||||||
|
- Text
|
||||||
|
- Number
|
||||||
|
- Select
|
||||||
|
- Multi-select
|
||||||
|
- Date
|
||||||
|
- Files & Media
|
||||||
|
- Checkbox
|
||||||
|
- URL
|
||||||
|
- Email
|
||||||
|
- Phone Number
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
::: info
|
||||||
|
关联(Relation)和汇总(Rollup)类型暂时还不支持。
|
||||||
|
:::
|
||||||
|
|
||||||
|
## 完成配置
|
||||||
|
|
||||||
|
配置完数据库后,请确保保存你的设置。现在,你可以通过命令面板或笔记右键菜单中的 “Share to NotionNext” 命令,开始将你的 Obsidian 笔记同步到配置好的 Notion 数据库了。
|
||||||
189
docs/zh/04-sync.md
Normal file
189
docs/zh/04-sync.md
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
---
|
||||||
|
title: 同步笔记
|
||||||
|
description: 如何使用 NotionNext 插件将你的 Obsidian 笔记同步到 Notion
|
||||||
|
---
|
||||||
|
|
||||||
|
# 同步笔记
|
||||||
|
|
||||||
|
在插件设置中配置好你的 Notion 数据库后,你就可以开始将 Obsidian 笔记同步到 Notion 了。
|
||||||
|
|
||||||
|
## 手动同步
|
||||||
|
|
||||||
|
要同步一篇笔记,只需打开你想要同步的笔记,然后从命令面板(`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
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
[document.pdf](document.pdf)
|
||||||
|
[document.pdf](folder/document.pdf)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TODO
|
||||||
|
|
||||||
|
- [ ] Obsidian URL 格式
|
||||||
|
|
||||||
|
```markdown
|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] App URL 格式
|
||||||
|
|
||||||
|
```markdown
|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
|
### 附件上传工作原理
|
||||||
|
|
||||||
|
1. **自动检测**:同步时,插件会自动扫描笔记内容,识别所有本地附件引用
|
||||||
|
2. **上传到 Notion**:检测到的附件会通过 Notion File Upload API 上传
|
||||||
|
3. **链接替换**:上传成功后,笔记中的本地链接会被替换为 Notion 的文件引用
|
||||||
|
4. **图片显示**:图片会作为 Notion 的图片块显示
|
||||||
|
5. **文件嵌入**:PDF 等非图片文件会作为文件块嵌入
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 代码块中的附件引用不会被处理
|
||||||
|
- 外部 URL(`http://` 或 `https://`)不会被处理
|
||||||
|
- 单个文件大小限制为 5MB
|
||||||
|
- 确保附件文件存在于 Vault 中
|
||||||
|
|
||||||
|
## 自动同步
|
||||||
|
|
||||||
|
插件支持自动同步功能,可以监控你的笔记变化并自动同步到 Notion。
|
||||||
|
|
||||||
|
### 启用自动同步
|
||||||
|
|
||||||
|
1. 打开插件设置
|
||||||
|
2. 在通用设置下找到"自动同步"开关
|
||||||
|
3. 开启该开关
|
||||||
|
4. 配置"自动同步延迟时间"(默认:5秒,最小:2秒)
|
||||||
|
|
||||||
|
### 准备 Frontmatter
|
||||||
|
|
||||||
|
自动同步会读取你在 **设置 → 自动同步 Frontmatter 键名** 中配置的键名(默认为 `autosync-database`)来确定要同步的数据库。要让你的笔记能够自动同步:
|
||||||
|
|
||||||
|
- 在笔记的 frontmatter 中添加配置的键名
|
||||||
|
- 列出一个或多个你在插件设置中定义的数据库简称
|
||||||
|
- 如果修改了笔记要同步的数据库,记得更新列表
|
||||||
|
|
||||||
|
示例(使用默认键名):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: 我的文章
|
||||||
|
autosync-database: [blog, portfolio]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
如果你在设置中修改了键名,记得同时更新你的 frontmatter。
|
||||||
|
|
||||||
|
### 自动同步工作原理
|
||||||
|
|
||||||
|
当自动同步启用后:
|
||||||
|
|
||||||
|
- 插件会监控 Markdown 文件的变化
|
||||||
|
- **只有 frontmatter 中包含自动同步配置键的文件才会被处理**
|
||||||
|
- 没有配置自动同步键的文件会被静默跳过,不会触发任何同步操作或提示
|
||||||
|
- 含有本地附件(图片/PDF)的文件会跳过自动同步,请手动同步
|
||||||
|
- 在你停止编辑达到配置的延迟时间后,自动触发同步
|
||||||
|
- **支持首次自动上传**:无需先手动同步,只要添加 frontmatter 键名,插件会自动处理首次上传
|
||||||
|
- 如果文件关联了多个数据库,会自动同步到所有数据库
|
||||||
|
- 首次同步后,会自动在 frontmatter 中添加 `NotionID-{数据库}` 用于后续更新
|
||||||
|
|
||||||
|
### 自动同步场景示例
|
||||||
|
|
||||||
|
#### 场景 A:新文档(首次自动上传)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: 我的新文章
|
||||||
|
autosync-database: [blog]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
**行为:**
|
||||||
|
|
||||||
|
- ✅ 检测到没有 NotionID,但配置了 `autosync-database`
|
||||||
|
- ✅ 自动执行首次上传到 Blog 数据库
|
||||||
|
- ✅ 上传成功后自动添加 `NotionID-blog: xxx` 到 frontmatter
|
||||||
|
- ✅ 显示成功/失败通知
|
||||||
|
- 📝 **无需操作:** 插件会自动处理首次上传
|
||||||
|
|
||||||
|
#### 场景 B:已同步到一个数据库(更新)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: 我的文章
|
||||||
|
NotionID-blog: abc123
|
||||||
|
autosync-database: [blog]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
**行为:**
|
||||||
|
|
||||||
|
- ✅ 检测到 1 个 NotionID
|
||||||
|
- ✅ 自动同步更新到 Blog 数据库
|
||||||
|
- ✅ 显示上传命令返回的成功/失败通知
|
||||||
|
- 📝 **无需操作:** 变更会自动同步
|
||||||
|
|
||||||
|
#### 场景 C:同步到多个数据库
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: 我的文章
|
||||||
|
NotionID-blog: abc123
|
||||||
|
NotionID-portfolio: def456
|
||||||
|
NotionID-notes: ghi789
|
||||||
|
autosync-database: [blog, portfolio, notes]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
**行为:**
|
||||||
|
|
||||||
|
- ✅ 检测到 3 个数据库目标
|
||||||
|
- ✅ 显示提示:"🔄 自动同步:正在同步到 3 个数据库..."
|
||||||
|
- ✅ 依次同步到所有 3 个数据库
|
||||||
|
- ✅ 为每个数据库显示独立的结果通知
|
||||||
|
- 📝 **无需操作:** 变更会自动同步到所有关联的数据库
|
||||||
|
|
||||||
|
### 自动同步最佳实践
|
||||||
|
|
||||||
|
1. **添加 Frontmatter 配置**:只需添加 `autosync-database: [你的数据库]` 即可启用自动同步,无需手动上传
|
||||||
|
2. **合理配置延迟**:如果你经常编辑,设置较长的延迟时间(5-10 秒)
|
||||||
|
3. **监控同步状态**:注意查看通知以确保同步成功完成
|
||||||
|
4. **查看日志**:打开开发者控制台(Ctrl+Shift+I / Cmd+Option+I)查看详细的同步日志
|
||||||
|
|
||||||
|
### 故障排除
|
||||||
|
|
||||||
|
遇到自动同步问题?请查看[问题排查指南](05-troubleshooting.md)获取常见问题的详细解决方案。
|
||||||
74
docs/zh/05-troubleshooting.md
Normal file
74
docs/zh/05-troubleshooting.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
title: 问题排查
|
||||||
|
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. **设置信息**:注明是否启用了自动同步以及配置的延迟时间
|
||||||
35
docs/zh/06-changelog.md
Normal file
35
docs/zh/06-changelog.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
title: 更新日志
|
||||||
|
description: Obsidian to NotionNext 的版本更新与变更记录
|
||||||
|
---
|
||||||
|
|
||||||
|
欢迎来到 Obsidian to NotionNext 的更新日志!这里会记录自 `2.7.0` 版本起的所有更新、改进以及问题修复,方便你快速了解插件的演进情况。
|
||||||
|
|
||||||
|
## v2.8.0 (2026-01-29)
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- **自动同步**:当内容或 frontmatter 变化时自动同步,支持可配置延迟与多数据库
|
||||||
|
- **附件上传**:通过 File Upload API 上传本地图片与 PDF,并插入为 `image`/`file` 块
|
||||||
|
- **自动复制 Notion 链接**:同步后自动复制页面链接到剪贴板
|
||||||
|
- **自动同步 frontmatter key**:可自定义自动同步数据库列表的 frontmatter key(默认 `autosync-database`)
|
||||||
|
- 完整的 UI 与通知 i18n 支持
|
||||||
|
- 通过 GitHub Actions + BRAT 的预发布测试流程
|
||||||
|
|
||||||
|
### 变更
|
||||||
|
|
||||||
|
- 优化自动同步行为与提示:无 `autosync-database` 或缺少 NotionID 的文件处理更智能
|
||||||
|
- 限制附件链接解析为 **Wikilinks** 与 **标准 Markdown 链接**(Obsidian/App URL 暂停/待办)
|
||||||
|
- 统一 Notion API 请求头 `Notion-Version` 为 `2025-09-03`
|
||||||
|
- 单文件上传限制降为 **5MB**,提升兼容性
|
||||||
|
- 强化设置面板与自动同步相关文档
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- 移动端兼容性修复:使用 `window.setTimeout` 替代 `NodeJS.Timeout`
|
||||||
|
- 防止同步循环,改进 frontmatter/正文变更检测
|
||||||
|
- 修复 Markdown 下划线导致的占位符解析问题
|
||||||
|
- 修复附件独占行时的块顺序
|
||||||
|
- 保留 `external` 图片转为 `file_upload` 时的图片标题
|
||||||
|
- 避免上传 `file` 块时重复显示文件名
|
||||||
|
- 修复同步成功通知出现 `undefined`(补充 `sync-preffix` i18n key)
|
||||||
8
docs/zh/07-roadmap.md
Normal file
8
docs/zh/07-roadmap.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: 路线图
|
||||||
|
description: Obsidian to NotionNext 的规划功能与未来改进
|
||||||
|
---
|
||||||
|
|
||||||
|
- [x] 自动同步:支持按时间间隔或内容变更自动同步笔记。
|
||||||
|
- [x] 附件支持:允许将 Obsidian 笔记中的附件一同上传到 Notion。
|
||||||
|
- [ ] 关联属性:实现将 Obsidian 链接映射到 Notion 的 relation 属性。
|
||||||
27
docs/zh/index.md
Normal file
27
docs/zh/index.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
title: 介绍
|
||||||
|
description: 使用 Notion API 将文件分享到任何 Notion 数据库
|
||||||
|
order: 0
|
||||||
|
---
|
||||||
|
|
||||||
|
# Obsidian to NotionNext
|
||||||
|
|
||||||
|
在这份文档中,你将找到关于如何设置和使用 Obsidian to NotionNext 插件的指南和说明。这款插件可以让你使用 Notion API 将你的 Obsidian 笔记直接分享到 Notion 数据库。
|
||||||
|
|
||||||
|
本指南将引导你完成设置 Obsidian to NotionNext 插件的全过程。
|
||||||
|
|
||||||
|
[开始使用](./01-getting-started.md)
|
||||||
|
|
||||||
|
[安装插件](./02-installation.md)
|
||||||
|
|
||||||
|
[配置数据库](./03-configuration.md)
|
||||||
|
|
||||||
|
[同步笔记](./04-sync.md)
|
||||||
|
|
||||||
|
[问题排查](./05-troubleshooting.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[更新日志](./06-changelog.md)
|
||||||
|
|
||||||
|
[路线图](./07-roadmap.md)
|
||||||
@@ -1,58 +1,60 @@
|
|||||||
import esbuild from "esbuild";
|
import esbuild from "esbuild";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
import builtins from 'builtin-modules'
|
import builtins from "builtin-modules";
|
||||||
|
|
||||||
const banner =
|
const banner = `/*
|
||||||
`/*
|
|
||||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||||
if you want to view the source, please visit the github repository of this plugin
|
if you want to view the source, please visit the github repository of this plugin
|
||||||
*/
|
*/
|
||||||
`;
|
`;
|
||||||
const prod = (process.argv[2] === 'production');
|
const prod = process.argv[2] === "production";
|
||||||
|
|
||||||
const ctx = await esbuild.context({
|
const ctx = await esbuild.context({
|
||||||
banner: {
|
banner: {
|
||||||
js: banner,
|
js: banner,
|
||||||
},
|
},
|
||||||
entryPoints: ['src/main.ts'],
|
entryPoints: ["src/main.ts"],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
external: [
|
external: [
|
||||||
'obsidian',
|
"obsidian",
|
||||||
'electron',
|
"electron",
|
||||||
'@codemirror/autocomplete',
|
"@codemirror/autocomplete",
|
||||||
'@codemirror/closebrackets',
|
"@codemirror/closebrackets",
|
||||||
'@codemirror/collab',
|
"@codemirror/collab",
|
||||||
'@codemirror/commands',
|
"@codemirror/commands",
|
||||||
'@codemirror/comment',
|
"@codemirror/comment",
|
||||||
'@codemirror/fold',
|
"@codemirror/fold",
|
||||||
'@codemirror/gutter',
|
"@codemirror/gutter",
|
||||||
'@codemirror/highlight',
|
"@codemirror/highlight",
|
||||||
'@codemirror/history',
|
"@codemirror/history",
|
||||||
'@codemirror/language',
|
"@codemirror/language",
|
||||||
'@codemirror/lint',
|
"@codemirror/lint",
|
||||||
'@codemirror/matchbrackets',
|
"@codemirror/matchbrackets",
|
||||||
'@codemirror/panel',
|
"@codemirror/panel",
|
||||||
'@codemirror/rangeset',
|
"@codemirror/rangeset",
|
||||||
'@codemirror/rectangular-selection',
|
"@codemirror/rectangular-selection",
|
||||||
'@codemirror/search',
|
"@codemirror/search",
|
||||||
'@codemirror/state',
|
"@codemirror/state",
|
||||||
'@codemirror/stream-parser',
|
"@codemirror/stream-parser",
|
||||||
'@codemirror/text',
|
"@codemirror/text",
|
||||||
'@codemirror/tooltip',
|
"@codemirror/tooltip",
|
||||||
'@codemirror/view',
|
"@codemirror/view",
|
||||||
...builtins
|
...builtins,
|
||||||
],
|
],
|
||||||
format: 'cjs',
|
format: "cjs",
|
||||||
target: 'es2016',
|
target: "es2016",
|
||||||
logLevel: "info",
|
logLevel: "info",
|
||||||
sourcemap: prod ? false : 'inline',
|
sourcemap: prod ? false : "inline",
|
||||||
treeShaking: true,
|
treeShaking: true,
|
||||||
outfile: 'main.js',
|
outfile: "main.js",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!prod) {
|
if (!prod) {
|
||||||
await ctx.watch();
|
await ctx.watch();
|
||||||
} else {
|
} else {
|
||||||
await ctx.rebuild()
|
await ctx.rebuild();
|
||||||
ctx.dispose().then(r => console.log('Build completed'), e => console.error(e));
|
ctx.dispose().then(
|
||||||
|
(r) => console.log("Build completed"),
|
||||||
|
(e) => console.error(e),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "share-to-notionnext",
|
"id": "share-to-notionnext",
|
||||||
"name": "Share to NotionNext",
|
"name": "Share to NotionNext",
|
||||||
"version": "1.0.0",
|
"version": "2.8.3",
|
||||||
"minAppVersion": "0.0.1",
|
"minAppVersion": "0.0.1",
|
||||||
"description": "Shares obsidian md file to notion with notion api for NotionNext web deploy, originally created by EasyChris/obsidian-to-notion.",
|
"description": "Shares obsidian md file to notion with notion api for NotionNext web deploy, originally created by EasyChris/obsidian-to-notion.",
|
||||||
"author": "EasyChris, jxpeng98",
|
"author": "EasyChris, jxpeng98",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ date: 2023-07-23 # default is today, 默认是今天。 Format is YYYY-MM-DD
|
|||||||
coverurl: https://img.jxpeng.dev/2023/08/843e27a210847f05a0f7cfb121fec100.jpg # default is empty, 默认是空
|
coverurl: https://img.jxpeng.dev/2023/08/843e27a210847f05a0f7cfb121fec100.jpg # default is empty, 默认是空
|
||||||
type: Post # Post or Page, default is Post, 默认是Post
|
type: Post # Post or Page, default is Post, 默认是Post
|
||||||
slug: test # slug for url, default is empty, 默认是空
|
slug: test # slug for url, default is empty, 默认是空
|
||||||
stats: Draft # Draft, Invisible, Published, default is Draft, 默认是Draft
|
status: Draft # Draft, Invisible, Published, default is Draft, 默认是Draft
|
||||||
category: test # default is 'Obsidian', 默认是'Obsidian'
|
category: test # default is 'Obsidian', 默认是'Obsidian'
|
||||||
summary: this is a summary for test post # default is empty, 默认是空
|
summary: this is a summary for test post # default is empty, 默认是空
|
||||||
icon: fa-solid fa-camera # you can ignore this, default is empty, 默认是空,可直接删除
|
icon: fa-solid fa-camera # you can ignore this, default is empty, 默认是空,可直接删除
|
||||||
|
|||||||
23
package.json
23
package.json
@@ -1,32 +1,31 @@
|
|||||||
{
|
{
|
||||||
"name": "share-to-notionnext",
|
"name": "share-to-notionnext",
|
||||||
"version": "1.0.0",
|
"version": "2.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Shares obsidian md file to notion with notion api for NotionNext web deploy, originally created by EasyChris/obsidian-to-notion.",
|
"description": "Share files to any Notion database using the Notion API, originally created by EasyChris/obsidian-to-notion.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node esbuild.config.mjs",
|
"dev": "node esbuild.config.mjs",
|
||||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
"version": "node version-bump.mjs && git add manifest.json versions.json CHANGELOG.md"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "Jiaxin PENG",
|
||||||
"license": "GNU GPLv3",
|
"license": "GNU GPLv3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.5.7",
|
"@types/node": "^22.11.3",
|
||||||
"@types/yaml-front-matter": "^4.1.3",
|
"@types/yaml-front-matter": "^4.1.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||||
"@typescript-eslint/parser": "^6.11.0",
|
"@typescript-eslint/parser": "^6.16.0",
|
||||||
"builtin-modules": "^3.2.0",
|
"builtin-modules": "^3.3.0",
|
||||||
"esbuild": "0.19.2",
|
"esbuild": "0.19.5",
|
||||||
"obsidian": "latest",
|
"obsidian": "^1.6.6",
|
||||||
"tslib": "2.6.2",
|
"tslib": "2.6.2",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.2.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tryfabric/martian": "^1.2.4",
|
"@jxpeng98/martian": "^1.3.1",
|
||||||
"https-proxy-agent": "^7.0.2",
|
"https-proxy-agent": "^7.0.2",
|
||||||
"process": "^0.11.10",
|
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"yaml": "^2.3.4",
|
"yaml": "^2.3.4",
|
||||||
"yaml-front-matter": "^4.1.1"
|
"yaml-front-matter": "^4.1.1"
|
||||||
|
|||||||
2213
pnpm-lock.yaml
generated
Normal file
2213
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
|||||||
import { i18nConfig } from "src/lang/I18n";
|
import { i18nConfig } from "src/lang/I18n";
|
||||||
import { Editor, MarkdownView } from "obsidian";
|
import { Editor, MarkdownView, setTooltip } from "obsidian";
|
||||||
import { FuzzySuggester, DatabaseList } from "./FuzzySuggester";
|
import { FuzzySuggester, DatabaseList } from "./FuzzySuggester";
|
||||||
import { uploadCommandGeneral, uploadCommandNext } from "../upload/uploadCommand";
|
import { uploadCommandCustom, uploadCommandGeneral, uploadCommandNext } from "../upload/uploadCommand";
|
||||||
import ObsidianSyncNotionPlugin from "src/main";
|
import ObsidianSyncNotionPlugin from "src/main";
|
||||||
|
import { DatabaseDetails } from "../ui/settingTabs";
|
||||||
|
|
||||||
|
|
||||||
interface Command {
|
interface Command {
|
||||||
@@ -21,26 +22,11 @@ export default class RibbonCommands {
|
|||||||
constructor(plugin: ObsidianSyncNotionPlugin) {
|
constructor(plugin: ObsidianSyncNotionPlugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
|
|
||||||
// Check if NextButton is true, then include the corresponding command
|
// iterate through the database detail
|
||||||
if (this.plugin.settings.NextButton) {
|
|
||||||
this.Ncommand.push({
|
|
||||||
id: "share-to-notionnext",
|
|
||||||
name: i18nConfig.CommandName, // Use the translated text from i18nConfig
|
|
||||||
editorCallback: async (editor: Editor, view: MarkdownView) => {
|
|
||||||
await uploadCommandNext(this.plugin, this.plugin.settings, this.plugin.app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if GeneralButton is true, then include the corresponding command
|
for (let key in this.plugin.settings.databaseDetails) {
|
||||||
if (this.plugin.settings.GeneralButton) {
|
let dbDetails = this.plugin.settings.databaseDetails[key];
|
||||||
this.Ncommand.push({
|
this.addCommandForDatabase(dbDetails);
|
||||||
id: "share-to-notion",
|
|
||||||
name: i18nConfig.CommandNameGeneral, // Use the translated text from i18nConfig
|
|
||||||
editorCallback: async (editor: Editor, view: MarkdownView) => {
|
|
||||||
await uploadCommandGeneral(this.plugin, this.plugin.settings, this.plugin.app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register all the commands
|
// Register all the commands
|
||||||
@@ -77,24 +63,9 @@ export default class RibbonCommands {
|
|||||||
|
|
||||||
this.Ncommand = [];
|
this.Ncommand = [];
|
||||||
|
|
||||||
if (this.plugin.settings.NextButton) {
|
for (let key in this.plugin.settings.databaseDetails) {
|
||||||
this.Ncommand.push({
|
let dbDetails = this.plugin.settings.databaseDetails[key];
|
||||||
id: "share-to-notionnext",
|
this.addCommandForDatabase(dbDetails);
|
||||||
name: i18nConfig.CommandName, // Use the translated text from i18nConfig
|
|
||||||
editorCallback: async (editor: Editor, view: MarkdownView) => {
|
|
||||||
await uploadCommandNext(this.plugin, this.plugin.settings, this.plugin.app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.plugin.settings.GeneralButton) {
|
|
||||||
this.Ncommand.push({
|
|
||||||
id: "share-to-notion",
|
|
||||||
name: i18nConfig.CommandNameGeneral, // Use the translated text from i18nConfig
|
|
||||||
editorCallback: async (editor: Editor, view: MarkdownView) => {
|
|
||||||
await uploadCommandGeneral(this.plugin, this.plugin.settings, this.plugin.app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.Ncommand.forEach(command => {
|
this.Ncommand.forEach(command => {
|
||||||
@@ -107,4 +78,30 @@ export default class RibbonCommands {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addCommandForDatabase(dbDetails: DatabaseDetails) {
|
||||||
|
// Example logic - adjust based on your specific requirements
|
||||||
|
let commandId = `share-to-${dbDetails.abName}`;
|
||||||
|
let commandName = `Share to ${dbDetails.fullName} (${dbDetails.abName})`; // or use a translated name
|
||||||
|
|
||||||
|
let editorCallback: (editor: Editor, view: MarkdownView) => Promise<void>;
|
||||||
|
if (dbDetails.format === 'next') {
|
||||||
|
editorCallback = async (editor, view) => {
|
||||||
|
await uploadCommandNext(this.plugin, this.plugin.settings, dbDetails, this.plugin.app);
|
||||||
|
};
|
||||||
|
} else if (dbDetails.format === 'general') {
|
||||||
|
editorCallback = async (editor, view) => {
|
||||||
|
await uploadCommandGeneral(this.plugin, this.plugin.settings, dbDetails, this.plugin.app);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (dbDetails.format === 'custom') {
|
||||||
|
editorCallback = async (editor, view) => {
|
||||||
|
await uploadCommandCustom(this.plugin, this.plugin.settings, dbDetails, this.plugin.app);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Ncommand.push({ id: commandId, name: commandName, editorCallback });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
150
src/lang/I18n.ts
150
src/lang/I18n.ts
@@ -1,109 +1,49 @@
|
|||||||
|
import {en} from "./locale/en";
|
||||||
|
import {zh} from "./locale/zh";
|
||||||
|
import {ja} from "./locale/ja";
|
||||||
|
|
||||||
export const I18n: {[key: string]:any} = {
|
export const I18n: {[key: string]:any} = {
|
||||||
en: {
|
en,
|
||||||
ribbonIcon: "Share to NotionNext",
|
zh,
|
||||||
GeneralSetting: "General information Settings",
|
ja,
|
||||||
CommandID: "share-to-notionnext",
|
|
||||||
CommandName: "Share to NotionNext Database",
|
|
||||||
CommandIDGeneral: "share-to-notion",
|
|
||||||
CommandNameGeneral: "Share to Notion General Database",
|
|
||||||
NotionNextButton: "NotionNext Sync",
|
|
||||||
NotionNextButtonDesc: "Open this option, Sync to NotionNext command will be displayed in the command palette (default: ON)",
|
|
||||||
NotionNextSettingHeader: "NotionNext Database Settings",
|
|
||||||
NotionAPI: "Notion API Token",
|
|
||||||
NotionAPIDesc: "Generate from https://www.notion.so/my-integrations",
|
|
||||||
NotionAPIText: "Enter your Notion API Token",
|
|
||||||
DatabaseID: "Database ID",
|
|
||||||
DatabaseIDText: "Enter your Database ID",
|
|
||||||
BannerUrl: "Banner url (optional)",
|
|
||||||
BannerUrlDesc:
|
|
||||||
"Default is empty, if you want to show a banner, please enter the url (like: https://minioapi.pjx.ac.cn/img1/2023/11/b7b40a0724e93b7d7ab494bb3b8a2da8.png)",
|
|
||||||
BannerUrlText: "Enter your banner url",
|
|
||||||
NotionUser: "Notion ID (username, optional)",
|
|
||||||
NotionUserDesc:
|
|
||||||
"Collect from share link likes:https://username.notion.site. Your notion id is [username]",
|
|
||||||
NotionUserText: "Enter your notion ID",
|
|
||||||
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)",
|
|
||||||
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",
|
|
||||||
NotYetFinish:
|
|
||||||
"Not finished. This function will be available in the next version",
|
|
||||||
PlaceHolder: "Enter database Name",
|
|
||||||
"notion-logo": "Share to NotionNext",
|
|
||||||
"sync-success": "Sync to NotionNext success: \n",
|
|
||||||
"sync-fail": "Sync to NotionNext fail: \n",
|
|
||||||
"open-notion": "Please open the file that needs to be synchronized",
|
|
||||||
"config-secrets-notion-api":
|
|
||||||
"Please set up the notion API in the settings tab.",
|
|
||||||
"config-secrets-database-id":
|
|
||||||
"Please set up the database id in the settings tab.",
|
|
||||||
"set-tags-fail":
|
|
||||||
"Set tags fail,please check the frontmatter of the file or close the tag switch in the settings tab.",
|
|
||||||
NNonMissing:
|
|
||||||
"The 'NNon' property is missing in the settings. Please set it up.",
|
|
||||||
"set-api-id":
|
|
||||||
"Please set up the notion API and database ID in the settings tab.",
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
ribbonIcon: "分享到 NotionNext",
|
|
||||||
GeneralSetting: "通用设置",
|
|
||||||
CommandID: "share-to-notionnext",
|
|
||||||
CommandName: "分享到 NotionNext",
|
|
||||||
CommandIDGeneral: "share-to-notion",
|
|
||||||
CommandNameGeneral: "分享到 Notion 普通数据库",
|
|
||||||
NotionNextButton: "NotionNext 同步",
|
|
||||||
NotionNextButtonDesc: "打开此选项,NotionNext 同步将显示在命令面板中(默认:开)",
|
|
||||||
NotionNextSettingHeader: "NotionNext 数据库参数设置",
|
|
||||||
NotionAPI: "Notion API 令牌",
|
|
||||||
NotionAPIDesc: "从 https://www.notion.so/my-integrations 生成",
|
|
||||||
NotionAPIText: "输入你的 Notion API 令牌",
|
|
||||||
DatabaseID: "数据库 ID",
|
|
||||||
DatabaseIDText: "输入你的数据库 ID",
|
|
||||||
BannerUrl: "封面图片地址(可选)",
|
|
||||||
BannerUrlDesc:
|
|
||||||
"默认为空,如果你想显示封面图片,请输入图片地址(例如:https://minioapi.pjx.ac.cn/img1/2023/11/b7b40a0724e93b7d7ab494bb3b8a2da8.png)",
|
|
||||||
BannerUrlText: "输入你的封面图片地址",
|
|
||||||
NotionUser: "Notion ID(用户名,可选)",
|
|
||||||
NotionUserDesc:
|
|
||||||
"数据库分享链接类似:https://username.notion.site/。你的 Notion ID 是 [username]",
|
|
||||||
NotionUserText: "输入你的 Notion ID",
|
|
||||||
NotionGeneralSettingHeader: "普通 Notion 数据库设置",
|
|
||||||
NotionGeneralButton: "普通数据库同步",
|
|
||||||
NotionGeneralButtonDesc: "打开此选项,同步到普通数据库命令将显示在命令面板中(默认:开)",
|
|
||||||
NotionCustomTitle: "修改 Notion 数据库表头开关",
|
|
||||||
NotionCustomTitleDesc: "自定义Notion 数据库第一列表头名(默认:关)",
|
|
||||||
NotionCustomTitleName: "想要修改的表头名",
|
|
||||||
NotionCustomTitleNameDesc: "输入你想要修改的notion数据库的表头名(默认:title)",
|
|
||||||
NotionCustomTitleText: "输入表头名",
|
|
||||||
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",
|
|
||||||
"set-tags-fail":
|
|
||||||
"设置标签失败,请检查文件的frontmatter,或者在插件设置中关闭设置tags开关",
|
|
||||||
NNonMissing: "未设置'NNon'属性,请在插件设置中选择NotionNext数据库。",
|
|
||||||
"set-api-id": "请在插件设置中设置notion API和database ID",
|
|
||||||
NotionCustomSettingHeader: "Notion 自定义数据库设置",
|
|
||||||
NotionCustomButton: "Notion 自定义数据库同步命令开关",
|
|
||||||
NotionCustomButtonDesc: "打开此选项,同步到自定义数据库命令将显示在命令面板中",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const I18nConfig = (lang: any): any => {
|
class I18nManager {
|
||||||
return I18n[lang];
|
private currentLanguage: string;
|
||||||
};
|
|
||||||
|
|
||||||
export const i18nConfig = I18nConfig(
|
constructor() {
|
||||||
window.localStorage.getItem("language") || "en",
|
this.currentLanguage = this.detectLanguage();
|
||||||
); // Export i18nConfig
|
}
|
||||||
|
|
||||||
|
// return the language to use
|
||||||
|
private detectLanguage(): string {
|
||||||
|
const storedLanguage = window.localStorage.getItem("language");
|
||||||
|
if (storedLanguage && this.isLanguageSupported(storedLanguage)) {
|
||||||
|
console.log(`Using stored language: ${storedLanguage}`);
|
||||||
|
return storedLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
const browserLanguage = window.navigator.language.split("-")[0];
|
||||||
|
if (this.isLanguageSupported(browserLanguage)) {
|
||||||
|
console.log(`Using browser language: ${browserLanguage}`);
|
||||||
|
return browserLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to English if no match is found
|
||||||
|
console.log("Using default language: en");
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
|
||||||
|
private isLanguageSupported(lang: string): boolean {
|
||||||
|
return Object.prototype.hasOwnProperty.call(I18n, lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the i18n configuration for the current language
|
||||||
|
public getConfig(): any {
|
||||||
|
return I18n[this.currentLanguage];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const i18nManager = new I18nManager();
|
||||||
|
|
||||||
|
export const i18nConfig = i18nManager.getConfig();
|
||||||
|
|||||||
119
src/lang/locale/en.ts
Normal file
119
src/lang/locale/en.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
export const en = {
|
||||||
|
databaseFormat: "Database Format",
|
||||||
|
databaseFormatDesc: "Select the database format to sync to: NotionNext or General.",
|
||||||
|
databaseNext: "NotionNext",
|
||||||
|
databaseGeneral: "General",
|
||||||
|
databaseCustom: "Custom",
|
||||||
|
databaseFullName: "Database Full Name",
|
||||||
|
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: "Sync to NotionNext",
|
||||||
|
CommandIDGeneral: "share-to-notion",
|
||||||
|
CommandNameGeneral: "Sync to General Database",
|
||||||
|
NotionNextButton: "NotionNext Sync",
|
||||||
|
NotionNextButtonDesc: "Enables the 'Sync to NotionNext' command in the command palette (default: on).",
|
||||||
|
NotionNextSettingHeader: "NotionNext Database Settings",
|
||||||
|
NotionAPI: "Notion API Token",
|
||||||
|
NotionAPIDesc: "Get yours from notion.so/my-integrations.",
|
||||||
|
NotionAPIText: "Enter your Notion API Token",
|
||||||
|
DatabaseID: "Database ID",
|
||||||
|
DatabaseIDDesc: "Find this in your Notion page's top-right 'Share' menu.",
|
||||||
|
DatabaseIDText: "Enter your Database ID",
|
||||||
|
BannerUrl: "Banner URL (optional)",
|
||||||
|
BannerUrlDesc:
|
||||||
|
"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:
|
||||||
|
"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 syncs changes to Notion when the file's frontmatter or content is modified. Supports creating and updating pages.",
|
||||||
|
AutoSyncFrontmatterKey: "Auto Sync Frontmatter Key",
|
||||||
|
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: "Delay in seconds to wait before syncing after a change. Prevents excessive syncs (default: 5s, min: 2s).",
|
||||||
|
AutoSyncDelayText: "Enter delay in seconds",
|
||||||
|
AutoSyncSuccessNotice: "Auto Sync Success Notice",
|
||||||
|
AutoSyncSuccessNoticeDesc: "Show a notification when auto-sync succeeds (default: off; failures are still notified).",
|
||||||
|
NotionGeneralSettingHeader: "General Notion Database Settings",
|
||||||
|
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-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.",
|
||||||
|
"config-secrets-notion-api":
|
||||||
|
"Please configure your Notion API key in the plugin settings.",
|
||||||
|
"config-secrets-database-id":
|
||||||
|
"Please configure your Database ID in the plugin settings.",
|
||||||
|
"set-tags-fail":
|
||||||
|
"Failed to set tags. Check the frontmatter or disable tag sync in settings.",
|
||||||
|
NNonMissing:
|
||||||
|
"The 'NNon' property is not set. Please select a NotionNext database in settings.",
|
||||||
|
"set-api-id":
|
||||||
|
"Please configure your Notion API key and Database ID in the plugin settings.",
|
||||||
|
NotionCustomSettingHeader: "Notion Custom Database Settings",
|
||||||
|
NotionCustomButton: "Enable Custom Database Command",
|
||||||
|
NotionCustomButtonDesc: "If enabled, the 'Sync to Custom Database' command appears in the command palette.",
|
||||||
|
CustomPropertyName: "Property Name",
|
||||||
|
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 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.",
|
||||||
|
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}",
|
||||||
|
"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",
|
||||||
|
EditDatabase: "Edit Database",
|
||||||
|
Preview: "Preview",
|
||||||
|
DatabaseFormatLabel: "Database Format",
|
||||||
|
DatabaseFullNameLabel: "Database Full 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.",
|
||||||
|
ToggleDatabaseIDVisibility: "Toggle Database ID Visibility",
|
||||||
|
CopyDatabaseID: "Copy Database ID",
|
||||||
|
DatabaseIDCopied: "Database ID copied to clipboard.",
|
||||||
|
AddNewDatabaseModal: "Add New Database",
|
||||||
|
}
|
||||||
112
src/lang/locale/ja.ts
Normal file
112
src/lang/locale/ja.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
export const ja = {
|
||||||
|
databaseFormat: "データベース形式",
|
||||||
|
databaseFormatDesc: "同期先のデータベース形式を選択してください(NotionNext または 一般)。",
|
||||||
|
databaseNext: "NotionNext",
|
||||||
|
databaseGeneral: "一般",
|
||||||
|
databaseCustom: "カスタム",
|
||||||
|
databaseFullName: "データベースの全称",
|
||||||
|
databaseFullNameDesc: "データベースのフルネームを設定します。",
|
||||||
|
databaseFullNameText: "データベースの全称を入力",
|
||||||
|
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",
|
||||||
|
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ユーザー名を入力",
|
||||||
|
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: "遅延秒数を入力",
|
||||||
|
AutoSyncSuccessNotice: "自動同期成功通知",
|
||||||
|
AutoSyncSuccessNoticeDesc: "自動同期が成功したときに通知を表示します(デフォルト:オフ。失敗時は通知されます)。",
|
||||||
|
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: "ページのタイトル。これはリストの最初のプロパティである必要があります。",
|
||||||
|
CustomProperty: "プロパティ",
|
||||||
|
AddCustomProperty: "カスタムプロパティを追加",
|
||||||
|
AddNewProperty: "新しいプロパティを追加",
|
||||||
|
AddNewPropertyDesc: "Notionデータベースのプロパティと一致する新しいプロパティを追加します。",
|
||||||
|
CopyErrorMessage: "リンクの自動コピーに失敗しました。手動でコピーしてください。",
|
||||||
|
BlockUploaded: "すべてのブロックをアップロードしました",
|
||||||
|
ExtraBlockUploaded: "追加のブロックをアップロードしました",
|
||||||
|
CheckConsole: "詳細は、開発者コンソール(opt+cmd+i または ctrl+shift+i)で確認できます。",
|
||||||
|
SettingsMigrated: "✨ 設定が更新されました!自動同期が利用可能です。詳細は設定画面をご確認ください。",
|
||||||
|
AutoSyncNoNotionID: "🆕 自動同期:Notionへ初めてアップロードします",
|
||||||
|
AutoSyncMissingDatabaseList: "⚠️ 自動同期をスキップ:frontmatterに `{key}: [データベース名]` を追加して同期先を指定してください。",
|
||||||
|
AutoSyncSkippedAttachments: "⚠️ 自動同期をスキップ:{filename} に内部添付(画像/PDF)が含まれています。手動で同期してください。",
|
||||||
|
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: "新しいデータベースを追加",
|
||||||
|
};
|
||||||
114
src/lang/locale/zh.ts
Normal file
114
src/lang/locale/zh.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
export const zh = {
|
||||||
|
databaseFormat: "数据库格式",
|
||||||
|
databaseFormatDesc: "选择同步的目标数据库格式:NotionNext 或 通用",
|
||||||
|
databaseNext: "NotionNext",
|
||||||
|
databaseGeneral: "通用",
|
||||||
|
databaseCustom: "自定义",
|
||||||
|
databaseFullName: "数据库全名",
|
||||||
|
databaseFullNameDesc: "为数据库设置一个全名",
|
||||||
|
databaseFullNameText: "输入您的数据库全名",
|
||||||
|
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",
|
||||||
|
DatabaseIDDesc: "可从 Notion 页面右上角的“分享”菜单中获取",
|
||||||
|
DatabaseIDText: "输入您的数据库 ID",
|
||||||
|
BannerUrl: "封面图片地址(可选)",
|
||||||
|
BannerUrlDesc:
|
||||||
|
"留空则不显示。如需封面,请输入图片地址(例如:https://abc.com/b.png)",
|
||||||
|
BannerUrlText: "输入您的封面图片地址",
|
||||||
|
NotionUser: "Notion 用户名(可选)",
|
||||||
|
NotionUserDesc:
|
||||||
|
"若分享链接为 username.notion.site,你的 Notion 用户名即为 [username]",
|
||||||
|
NotionUserText: "输入您的 Notion 用户名",
|
||||||
|
NotionLinkDisplay: "显示 Notion 链接",
|
||||||
|
NotionLinkDisplayDesc: "默认开启。关闭后,同步成功时 frontmatter 中不会出现 Notion 链接",
|
||||||
|
AutoCopyNotionLink: "自动复制 Notion 链接",
|
||||||
|
AutoCopyNotionLinkDesc: "同步后自动将 Notion 链接复制到剪贴板(默认开启)",
|
||||||
|
AutoSync: "自动同步",
|
||||||
|
AutoSyncDesc: "当文档的 frontmatter 或内容修改时,将自动同步到 Notion(支持新建和更新)",
|
||||||
|
AutoSyncFrontmatterKey: "自动同步 Frontmatter 键名",
|
||||||
|
AutoSyncFrontmatterKeyDesc: "设置用于指定自动同步数据库列表的 frontmatter 键名(默认为 autosync-database)。",
|
||||||
|
AutoSyncDelay: "自动同步延迟(秒)",
|
||||||
|
AutoSyncDelayDesc: "文档修改后,等待指定秒数再触发自动同步,以避免频繁操作(默认 5 秒,最少 2 秒)",
|
||||||
|
AutoSyncDelayText: "输入延迟秒数",
|
||||||
|
AutoSyncSuccessNotice: "自动同步成功通知",
|
||||||
|
AutoSyncSuccessNoticeDesc: "是否在自动同步成功后弹出通知(默认关闭,仅在失败时通知)",
|
||||||
|
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-preffix": "📄",
|
||||||
|
"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 或在设置中关闭标签同步。",
|
||||||
|
NNonMissing: "未设置 'NNon' 属性,请在插件设置中选择一个 NotionNext 数据库。",
|
||||||
|
"set-api-id": "请在插件设置中配置 Notion API 和数据库 ID。",
|
||||||
|
NotionCustomSettingHeader: "Notion 自定义数据库设置",
|
||||||
|
NotionCustomButton: "启用自定义数据库同步命令",
|
||||||
|
NotionCustomButtonDesc: "启用后,“同步到自定义数据库”的命令将出现在命令面板中。",
|
||||||
|
CustomPropertyName: "自定义属性名",
|
||||||
|
CustomPropertyFirstColumn: "标题属性",
|
||||||
|
CustomPropertyFirstColumnDesc: "第一列必须为标题属性。",
|
||||||
|
CustomProperty: "自定义属性",
|
||||||
|
AddCustomProperty: "添加自定义属性",
|
||||||
|
AddNewProperty: "添加新属性",
|
||||||
|
AddNewPropertyDesc: "添加一个与您 Notion 数据库中的属性相匹配的新属性。",
|
||||||
|
CopyErrorMessage: "自动复制链接失败,请手动复制。",
|
||||||
|
BlockUploaded: "所有块已上传成功",
|
||||||
|
ExtraBlockUploaded: "额外块已上传成功",
|
||||||
|
CheckConsole: "按 opt+cmd+i / ctrl+shift+i 打开控制台查看详情。",
|
||||||
|
SettingsMigrated: "✨ 插件设置已更新!新增自动同步功能,详情请查看设置。",
|
||||||
|
AutoSyncNoNotionID: "🆕 自动同步:首次上传到 Notion",
|
||||||
|
AutoSyncMissingDatabaseList: "⚠️ 自动同步已跳过:请在 frontmatter 中添加 \"{key}\" 以指定目标数据库。",
|
||||||
|
AutoSyncSkippedAttachments: "⚠️ 自动同步已跳过:检测到 {filename} 含有本地附件(图片/PDF),请手动同步。",
|
||||||
|
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: "添加新数据库",
|
||||||
|
}
|
||||||
417
src/main.ts
417
src/main.ts
@@ -1,10 +1,11 @@
|
|||||||
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 { addIcons } from 'src/ui/icon';
|
||||||
import { Upload2NotionGeneral } from "src/upload/upload_general/Upload2NotionGeneral";
|
|
||||||
import { Upload2NotionNext } from "src/upload/upload_next/Upload2NotionNext";
|
|
||||||
import { i18nConfig } from "src/lang/I18n";
|
import { i18nConfig } from "src/lang/I18n";
|
||||||
import ribbonCommands from "src/commands/NotionCommands";
|
import ribbonCommands from "src/commands/NotionCommands";
|
||||||
import { ObsidianSettingTab, PluginSettings, DEFAULT_SETTINGS } from "src/ui/settingTabs";
|
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!
|
// Remember to rename these classes and interfaces!
|
||||||
|
|
||||||
@@ -13,14 +14,32 @@ export default class ObsidianSyncNotionPlugin extends Plugin {
|
|||||||
settings: PluginSettings;
|
settings: PluginSettings;
|
||||||
commands: ribbonCommands;
|
commands: ribbonCommands;
|
||||||
app: App;
|
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();
|
||||||
|
private autoSyncAttachmentBlocked: Set<string> = new Set();
|
||||||
|
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
await this.loadSettings();
|
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);
|
this.commands = new ribbonCommands(this);
|
||||||
|
|
||||||
addIcons();
|
addIcons();
|
||||||
// This creates an icon in the left ribbon.
|
// This creates an icon in the left ribbon.
|
||||||
const ribbonIconEl = this.addRibbonIcon(
|
this.addRibbonIcon(
|
||||||
"notion-logo",
|
"notion-logo",
|
||||||
i18nConfig.ribbonIcon,
|
i18nConfig.ribbonIcon,
|
||||||
async (evt: MouseEvent) => {
|
async (evt: MouseEvent) => {
|
||||||
@@ -38,25 +57,411 @@ export default class ObsidianSyncNotionPlugin extends Plugin {
|
|||||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||||
this.addSettingTab(new ObsidianSettingTab(this.app, this));
|
this.addSettingTab(new ObsidianSettingTab(this.app, this));
|
||||||
|
|
||||||
|
// Setup auto sync listener
|
||||||
|
this.setupAutoSync();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
|
if (this.modifyEventRef) {
|
||||||
|
this.app.vault.offref(this.modifyEventRef);
|
||||||
|
}
|
||||||
|
if (this.autoSyncTimeout) {
|
||||||
|
clearTimeout(this.autoSyncTimeout);
|
||||||
|
}
|
||||||
|
this.lastFrontmatterCache.clear();
|
||||||
|
this.lastContentHashCache.clear();
|
||||||
|
this.autoSyncAttachmentBlocked.clear();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSettings() {
|
async loadSettings() {
|
||||||
|
const loadedData = await this.loadData();
|
||||||
|
|
||||||
|
// Merge loaded data with defaults, ensuring all fields exist
|
||||||
this.settings = Object.assign(
|
this.settings = Object.assign(
|
||||||
{},
|
{},
|
||||||
DEFAULT_SETTINGS,
|
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.autoSyncSuccessNotice !== 'boolean') {
|
||||||
|
this.settings.autoSyncSuccessNotice = DEFAULT_SETTINGS.autoSyncSuccessNotice;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
this.settings.autoSyncFrontmatterKey = resolveAutoSyncKey(this.settings.autoSyncFrontmatterKey);
|
||||||
|
|
||||||
|
// 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.autoSyncSuccessNotice === undefined ||
|
||||||
|
loadedData.NotionLinkDisplay === undefined ||
|
||||||
|
loadedData.autoCopyNotionLink === undefined ||
|
||||||
|
loadedData.autoSyncFrontmatterKey === 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.autoSyncSuccessNotice === undefined) migratedFields.push('autoSyncSuccessNotice');
|
||||||
|
if (loadedData.NotionLinkDisplay === undefined) migratedFields.push('NotionLinkDisplay');
|
||||||
|
if (loadedData.autoCopyNotionLink === undefined) migratedFields.push('autoCopyNotionLink');
|
||||||
|
|
||||||
|
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() {
|
async saveSettings() {
|
||||||
|
// Validate settings before saving
|
||||||
|
this.validateSettings();
|
||||||
await this.saveData(this.settings);
|
await this.saveData(this.settings);
|
||||||
|
console.log('[Settings] Settings saved successfully', {
|
||||||
|
autoSync: this.settings.autoSync,
|
||||||
|
autoSyncDelay: this.settings.autoSyncDelay,
|
||||||
|
autoSyncSuccessNotice: this.settings.autoSyncSuccessNotice,
|
||||||
|
NotionLinkDisplay: this.settings.NotionLinkDisplay,
|
||||||
|
autoSyncFrontmatterKey: this.settings.autoSyncFrontmatterKey,
|
||||||
|
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.autoSyncSuccessNotice !== 'boolean') {
|
||||||
|
console.warn('[Settings] Invalid autoSyncSuccessNotice value, resetting to default');
|
||||||
|
this.settings.autoSyncSuccessNotice = DEFAULT_SETTINGS.autoSyncSuccessNotice;
|
||||||
|
}
|
||||||
|
if (typeof this.settings.NotionLinkDisplay !== 'boolean') {
|
||||||
|
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 = {};
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
getAutoSyncFrontmatterKey(): string {
|
||||||
|
return resolveAutoSyncKey(this.settings.autoSyncFrontmatterKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addDatabaseDetails(dbDetails: DatabaseDetails) {
|
||||||
|
this.settings.databaseDetails = {
|
||||||
|
...this.settings.databaseDetails,
|
||||||
|
[dbDetails.abName]: dbDetails,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteDatabaseDetails(dbDetails: DatabaseDetails) {
|
||||||
|
delete this.settings.databaseDetails[dbDetails.abName];
|
||||||
|
|
||||||
|
await this.saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDatabaseDetails(dbDetails: DatabaseDetails) {
|
||||||
|
// delete the old database details
|
||||||
|
delete this.settings.databaseDetails[dbDetails.abName];
|
||||||
|
|
||||||
|
this.settings.databaseDetails = {
|
||||||
|
...this.settings.databaseDetails,
|
||||||
|
[dbDetails.abName]: dbDetails,
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check autosync property first - only proceed if it exists
|
||||||
|
const autoSyncKey = this.getAutoSyncFrontmatterKey();
|
||||||
|
const autoSyncTargets = parseAutoSyncDatabaseList(frontMatter[autoSyncKey]);
|
||||||
|
|
||||||
|
if (autoSyncTargets.length === 0) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (frontmatterOnlyNotionIDChanged && contentUnchanged) {
|
||||||
|
this.lastFrontmatterCache.set(file.path, { ...frontMatter });
|
||||||
|
this.lastContentHashCache.set(file.path, contentHash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const dbByShortName = new Map<string, DatabaseDetails>();
|
||||||
|
for (const key in this.settings.databaseDetails) {
|
||||||
|
const dbDetails = this.settings.databaseDetails[key];
|
||||||
|
dbByShortName.set(dbDetails.abName.toLowerCase(), dbDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundDatabases: Array<{ dbDetails: DatabaseDetails, notionId: string | undefined }> = [];
|
||||||
|
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}`;
|
||||||
|
// 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 valid databases found in settings, skip
|
||||||
|
if (foundDatabases.length === 0) {
|
||||||
|
console.log(`[AutoSync] No matching databases found in settings for ${file.path}, skipping auto sync`);
|
||||||
|
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));
|
||||||
|
new Notice(message, 3000);
|
||||||
|
console.log(`[AutoSync] Found ${foundDatabases.length} database targets in ${file.path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync to all found databases
|
||||||
|
for (const { dbDetails, notionId } of foundDatabases) {
|
||||||
|
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
|
||||||
|
if (dbDetails.format === 'next') {
|
||||||
|
await uploadCommandNext(this, this.settings, dbDetails, this.app, { isAutoSync: true });
|
||||||
|
} else if (dbDetails.format === 'general') {
|
||||||
|
await uploadCommandGeneral(this, this.settings, dbDetails, this.app, { isAutoSync: true });
|
||||||
|
} else if (dbDetails.format === 'custom') {
|
||||||
|
await uploadCommandCustom(this, this.settings, dbDetails, this.app, { isAutoSync: true });
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
64
src/ui/DeleteModal.ts
Normal file
64
src/ui/DeleteModal.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { App, ButtonComponent, Modal, Setting } from "obsidian";
|
||||||
|
import { DatabaseDetails, ObsidianSettingTab } from "./settingTabs";
|
||||||
|
import ObsidianSyncNotionPlugin from "../main";
|
||||||
|
|
||||||
|
export class DeleteModal extends Modal {
|
||||||
|
data: Record<string, any> = {
|
||||||
|
deleted: false,
|
||||||
|
}
|
||||||
|
plugin: ObsidianSyncNotionPlugin;
|
||||||
|
settingTab: ObsidianSettingTab;
|
||||||
|
dbDetails: DatabaseDetails
|
||||||
|
|
||||||
|
constructor(app: App, plugin: ObsidianSyncNotionPlugin, settingTab: ObsidianSettingTab, dbDetails: DatabaseDetails) {
|
||||||
|
super(app);
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.settingTab = settingTab;
|
||||||
|
this.dbDetails = dbDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
this.containerEl.addClass("delete-modal");
|
||||||
|
this.titleEl.setText('Delete Database');
|
||||||
|
|
||||||
|
let { contentEl } = this;
|
||||||
|
contentEl.empty();
|
||||||
|
|
||||||
|
const deleteDiv = contentEl.createDiv('delete-div');
|
||||||
|
deleteDiv.createEl('h4', { text: 'Are you sure you want to delete the following database?' });
|
||||||
|
deleteDiv.createEl('h2', { text: this.dbDetails.fullName + ' (' + this.dbDetails.abName + ', ' + this.dbDetails.format + ')' });
|
||||||
|
|
||||||
|
|
||||||
|
// add delete button
|
||||||
|
let footerEl = contentEl.createDiv('save-button');
|
||||||
|
let deleteButton = new Setting(footerEl)
|
||||||
|
|
||||||
|
deleteButton
|
||||||
|
.addButton((button: ButtonComponent): ButtonComponent => {
|
||||||
|
return button
|
||||||
|
.setTooltip("Delete")
|
||||||
|
.setIcon("trash")
|
||||||
|
.onClick(async () => {
|
||||||
|
this.data.deleted = true;
|
||||||
|
this.close();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteButton.addExtraButton((button) => {
|
||||||
|
return button
|
||||||
|
.setTooltip('Cancel')
|
||||||
|
.setIcon('cross')
|
||||||
|
.onClick(() => {
|
||||||
|
this.data.deleted = false;
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onOpen() {
|
||||||
|
this.display();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
349
src/ui/EditModal.ts
Normal file
349
src/ui/EditModal.ts
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
import { App, ButtonComponent, Setting } from "obsidian";
|
||||||
|
import { customProperty, SettingModal } from "./settingModal";
|
||||||
|
import ObsidianSyncNotionPlugin from "../main";
|
||||||
|
import { DatabaseDetails, ObsidianSettingTab } from "./settingTabs";
|
||||||
|
import { i18nConfig } from "../lang/I18n";
|
||||||
|
|
||||||
|
export class EditModal extends SettingModal {
|
||||||
|
propertyLines: Setting[] = []; // Store all property line settings
|
||||||
|
[key: string]: any; // Index signature
|
||||||
|
dataTemp: Record<string, any> = {
|
||||||
|
databaseFormatTemp: '',
|
||||||
|
// databaseFormatTempInd: false,
|
||||||
|
databaseFullNameTemp: '',
|
||||||
|
// databaseFullNameTempInd: false,
|
||||||
|
databaseAbbreviateNameTemp: '',
|
||||||
|
// databaseAbbreviateNameTempInd: false,
|
||||||
|
notionAPITemp: '',
|
||||||
|
// notionAPITempInd: false,
|
||||||
|
databaseIDTemp: '',
|
||||||
|
// databaseIDTempInd: false,
|
||||||
|
tagButtonTemp: false,
|
||||||
|
// tagButtonTempInd: false,
|
||||||
|
customTitleButtonTemp: false,
|
||||||
|
// customTitleButtonTempInd: false,
|
||||||
|
customTitleNameTemp: '',
|
||||||
|
customPropertiesTemp: [],
|
||||||
|
// customTitleNameTempInd: false,
|
||||||
|
// customValues: '',
|
||||||
|
savedTemp: false,
|
||||||
|
savedTempInd: false,
|
||||||
|
};
|
||||||
|
dataPrev: Record<string, any> = {
|
||||||
|
databaseFormatPrev: '',
|
||||||
|
// databaseFormatPrevInd: false,
|
||||||
|
databaseFullNamePrev: '',
|
||||||
|
// databaseFullNamePrevInd: false,
|
||||||
|
databaseAbbreviateNamePrev: '',
|
||||||
|
// databaseAbbreviateNamePrevInd: false,
|
||||||
|
notionAPIPrev: '',
|
||||||
|
// notionAPIPrevInd: false,
|
||||||
|
databaseIDPrev: '',
|
||||||
|
// databaseIDPrevInd: false,
|
||||||
|
tagButtonPrev: false,
|
||||||
|
// tagButtonPrevInd: false,
|
||||||
|
customTitleButtonPrev: false,
|
||||||
|
// customTitleButtonPrevInd: false,
|
||||||
|
customTitleNamePrev: '',
|
||||||
|
customPropertiesPrev: [],
|
||||||
|
// customTitleNamePrevInd: false,
|
||||||
|
// customValues: '',
|
||||||
|
savedPrev: false,
|
||||||
|
savedPrevInd: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
plugin: ObsidianSyncNotionPlugin;
|
||||||
|
settingTab: ObsidianSettingTab;
|
||||||
|
dbDetails: DatabaseDetails;
|
||||||
|
|
||||||
|
constructor(app: App, plugin: ObsidianSyncNotionPlugin, settingTab: ObsidianSettingTab, dbDetails: DatabaseDetails) {
|
||||||
|
super(app, plugin, settingTab);
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.settingTab = settingTab;
|
||||||
|
if (dbDetails) {
|
||||||
|
// Temp details
|
||||||
|
this.dataTemp.databaseFormatTemp = dbDetails.format;
|
||||||
|
this.dataTemp.databaseFullNameTemp = dbDetails.fullName;
|
||||||
|
this.dataTemp.databaseAbbreviateNameTemp = dbDetails.abName;
|
||||||
|
this.dataTemp.notionAPITemp = dbDetails.notionAPI;
|
||||||
|
this.dataTemp.databaseIDTemp = dbDetails.databaseID;
|
||||||
|
this.dataTemp.tagButtonTemp = dbDetails.tagButton;
|
||||||
|
this.dataTemp.customTitleButtonTemp = dbDetails.customTitleButton;
|
||||||
|
this.dataTemp.customTitleNameTemp = dbDetails.customTitleName;
|
||||||
|
this.dataTemp.customPropertiesTemp = dbDetails.customProperties.map(prop => ({ ...prop })); // Ensure deep copy
|
||||||
|
// this.dataTemp.customValues = dbDetails.customValues;
|
||||||
|
this.dataTemp.savedTemp = dbDetails.saved;
|
||||||
|
|
||||||
|
// Prev details
|
||||||
|
this.dataPrev.databaseFormatPrev = dbDetails.format;
|
||||||
|
this.dataPrev.databaseFullNamePrev = dbDetails.fullName;
|
||||||
|
this.dataPrev.databaseAbbreviateNamePrev = dbDetails.abName;
|
||||||
|
this.dataPrev.notionAPIPrev = dbDetails.notionAPI;
|
||||||
|
this.dataPrev.databaseIDPrev = dbDetails.databaseID;
|
||||||
|
this.dataPrev.tagButtonPrev = dbDetails.tagButton;
|
||||||
|
this.dataPrev.customTitleButtonPrev = dbDetails.customTitleButton;
|
||||||
|
this.dataPrev.customTitleNamePrev = dbDetails.customTitleName;
|
||||||
|
this.dataPrev.customPropertiesPrev = dbDetails.customProperties.map(prop => ({ ...prop })); // Ensure deep copy
|
||||||
|
// this.dataTemp.customValues = dbDetails.customValues;
|
||||||
|
this.dataPrev.savedPrev = dbDetails.saved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
display(): void {
|
||||||
|
this.containerEl.addClass("edit-modal");
|
||||||
|
this.titleEl.setText(i18nConfig.EditDatabase);
|
||||||
|
|
||||||
|
let { contentEl } = this;
|
||||||
|
contentEl.empty();
|
||||||
|
|
||||||
|
const editDiv = contentEl.createDiv('edit-div');
|
||||||
|
const nextTabs = contentEl.createDiv('next-tabs');
|
||||||
|
|
||||||
|
|
||||||
|
new Setting(editDiv)
|
||||||
|
.setName(i18nConfig.databaseFormat)
|
||||||
|
.setDesc(i18nConfig.databaseFormatDesc)
|
||||||
|
.addDropdown((component) => {
|
||||||
|
component
|
||||||
|
.addOption('none', '')
|
||||||
|
.addOption('general', i18nConfig.databaseGeneral)
|
||||||
|
.addOption('next', i18nConfig.databaseNext)
|
||||||
|
.addOption('custom', i18nConfig.databaseCustom)
|
||||||
|
.setValue(this.dataTemp.databaseFormatTemp)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.dataTemp.databaseFormatTemp = value;
|
||||||
|
nextTabs.empty();
|
||||||
|
this.updateContentBasedOnSelection(value, nextTabs);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize content based on the current dropdown value
|
||||||
|
this.updateContentBasedOnSelection(this.dataTemp.databaseFormatTemp, nextTabs);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// add save button
|
||||||
|
let footerEl = contentEl.createDiv('save-button');
|
||||||
|
let saveButton = new Setting(footerEl)
|
||||||
|
saveButton.addButton((button: ButtonComponent) => {
|
||||||
|
return button
|
||||||
|
.setTooltip('Save')
|
||||||
|
.setIcon('checkmark')
|
||||||
|
.onClick(async () => {
|
||||||
|
this.dataTemp.savedTempInd = true;
|
||||||
|
this.dataTemp.savedTemp = true;
|
||||||
|
// console.log(this.dataTemp);
|
||||||
|
// console.log(this.dataPrev);
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
saveButton.addExtraButton((button) => {
|
||||||
|
return button
|
||||||
|
.setTooltip('Cancel')
|
||||||
|
.setIcon('cross')
|
||||||
|
.onClick(() => {
|
||||||
|
// console.log(this.dataTemp);
|
||||||
|
// console.log(this.dataPrev);
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpen(): void {
|
||||||
|
this.display()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContentBasedOnSelection(value: string, nextTabs: HTMLElement): void {
|
||||||
|
// Clear existing content
|
||||||
|
nextTabs.empty();
|
||||||
|
|
||||||
|
// Generate content based on the selected value
|
||||||
|
if (value === 'general') {
|
||||||
|
nextTabs.createEl('h3', { text: i18nConfig.NotionGeneralSettingHeader });
|
||||||
|
|
||||||
|
// add full name
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.databaseFullName, i18nConfig.databaseFullNameDesc, 'text', i18nConfig.databaseFullNameText, this.dataTemp.databaseFullNameTemp, 'dataTemp', 'databaseFullNameTemp')
|
||||||
|
|
||||||
|
// add abbreviate name
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.databaseAbbreviateName, i18nConfig.databaseAbbreviateNameDesc, 'text', i18nConfig.databaseAbbreviateNameText, this.dataTemp.databaseAbbreviateNameTemp, 'dataTemp', 'databaseAbbreviateNameTemp')
|
||||||
|
|
||||||
|
// tag button
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.NotionTagButton, i18nConfig.NotionTagButtonDesc, 'toggle', i18nConfig.NotionCustomTitleText, this.dataTemp.tagButtonTemp, 'dataTemp', 'tagButtonTemp')
|
||||||
|
|
||||||
|
// add custom title button
|
||||||
|
|
||||||
|
new Setting(nextTabs)
|
||||||
|
.setName(i18nConfig.NotionCustomTitle)
|
||||||
|
.setDesc(i18nConfig.NotionCustomTitleDesc)
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle
|
||||||
|
.setValue(this.dataTemp.customTitleButtonTemp)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.dataTemp.customTitleButtonTemp = value;
|
||||||
|
this.updateSettingEl(CustomNameEl, value)
|
||||||
|
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
// add custom title name
|
||||||
|
const CustomNameEl = this.createStyleDiv('custom-name', (this.dataTemp.customTitleButtonTemp), nextTabs);
|
||||||
|
this.createSettingEl(CustomNameEl, i18nConfig.NotionCustomTitleName, i18nConfig.NotionCustomTitleNameDesc, 'text', i18nConfig.NotionCustomTitleText, this.dataTemp.customTitleNameTemp, 'dataTemp', 'customTitleNameTemp')
|
||||||
|
|
||||||
|
|
||||||
|
// add api key
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.NotionAPI, i18nConfig.NotionAPIDesc, 'password', i18nConfig.NotionAPIText, this.dataTemp.notionAPITemp, 'dataTemp', 'notionAPITemp')
|
||||||
|
|
||||||
|
// add database id
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.DatabaseID, i18nConfig.DatabaseIDDesc, 'password', i18nConfig.DatabaseIDText, this.dataTemp.databaseIDTemp, 'dataTemp', 'databaseIDTemp')
|
||||||
|
|
||||||
|
|
||||||
|
} else if (value === 'next') {
|
||||||
|
|
||||||
|
nextTabs.createEl('h3', { text: i18nConfig.NotionNextSettingHeader });
|
||||||
|
|
||||||
|
// add full name
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.databaseFullName, i18nConfig.databaseFullNameDesc, 'text', i18nConfig.databaseFullNameText, this.dataTemp.databaseFullNameTemp, 'dataTemp', 'databaseFullNameTemp')
|
||||||
|
|
||||||
|
// add abbreviate name
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.databaseAbbreviateName, i18nConfig.databaseAbbreviateNameDesc, 'text', i18nConfig.databaseAbbreviateNameText, this.dataTemp.databaseAbbreviateNameTemp, 'dataTemp', 'databaseAbbreviateNameTemp')
|
||||||
|
|
||||||
|
// add api key
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.NotionAPI, i18nConfig.NotionAPIDesc, 'password', i18nConfig.NotionAPIText, this.dataTemp.notionAPITemp, 'dataTemp', 'notionAPITemp')
|
||||||
|
|
||||||
|
// add database id
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.DatabaseID, i18nConfig.DatabaseIDDesc, 'password', i18nConfig.DatabaseIDText, this.dataTemp.databaseIDTemp, 'dataTemp', 'databaseIDTemp')
|
||||||
|
|
||||||
|
} else if (value === 'custom') {
|
||||||
|
|
||||||
|
nextTabs.createEl('h3', { text: i18nConfig.NotionCustomSettingHeader });
|
||||||
|
|
||||||
|
// add full name
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.databaseFullName, i18nConfig.databaseFullNameDesc, 'text', i18nConfig.databaseFullNameText, this.dataTemp.databaseFullNameTemp, 'dataTemp', 'databaseFullNameTemp')
|
||||||
|
|
||||||
|
// add abbreviate name
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.databaseAbbreviateName, i18nConfig.databaseAbbreviateNameDesc, 'text', i18nConfig.databaseAbbreviateNameText, this.dataTemp.databaseAbbreviateNameTemp, 'dataTemp', 'databaseAbbreviateNameTemp')
|
||||||
|
|
||||||
|
// add api key
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.NotionAPI, i18nConfig.NotionAPIDesc, 'password', i18nConfig.NotionAPIText, this.dataTemp.notionAPITemp, 'dataTemp', 'notionAPITemp')
|
||||||
|
|
||||||
|
// add database id
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.DatabaseID, i18nConfig.DatabaseIDDesc, 'password', i18nConfig.DatabaseIDText, this.dataTemp.databaseIDTemp, 'dataTemp', 'databaseIDTemp')
|
||||||
|
|
||||||
|
// add custom properties
|
||||||
|
this.initializePropertyLines(nextTabs, this.dataTemp.customPropertiesTemp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializePropertyLines(containerEl: HTMLElement, properties: customProperty[]): void {
|
||||||
|
if (!containerEl) {
|
||||||
|
console.error('Failed to initialize property lines: containerEl is null');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName(i18nConfig.AddNewProperty)
|
||||||
|
.setDesc(i18nConfig.AddNewPropertyDesc)
|
||||||
|
.addButton(button => {
|
||||||
|
return button
|
||||||
|
.setButtonText('Add')
|
||||||
|
.setTooltip('Add one more property')
|
||||||
|
.onClick(() => {
|
||||||
|
this.createPropertyLine(containerEl, properties);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
properties.forEach(property => {
|
||||||
|
this.updatePropertyLine(containerEl, property, properties);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
updatePropertyLine(containerEl: HTMLElement, property: customProperty, properties: customProperty[]) {
|
||||||
|
let isExistingProperty = property !== null;
|
||||||
|
const propertyIndex = isExistingProperty ? property.index : properties.length;
|
||||||
|
|
||||||
|
const propertyLine = new Setting(containerEl)
|
||||||
|
.setName(propertyIndex === 0 ? i18nConfig.CustomPropertyFirstColumn : `${i18nConfig.CustomProperty} ${propertyIndex}`)
|
||||||
|
.setDesc(propertyIndex === 0 ? i18nConfig.CustomPropertyFirstColumnDesc : "");
|
||||||
|
|
||||||
|
propertyLine.addText(text => {
|
||||||
|
text.setPlaceholder(i18nConfig.CustomPropertyName)
|
||||||
|
.setValue(isExistingProperty ? property.customName : "")
|
||||||
|
.onChange(value => {
|
||||||
|
const actualIndex = properties.findIndex(p => p.index === propertyIndex);
|
||||||
|
if (actualIndex !== -1) {
|
||||||
|
properties[actualIndex].customName = value;
|
||||||
|
} else {
|
||||||
|
properties.push({ customName: value, customType: '', index: propertyIndex });
|
||||||
|
isExistingProperty = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
propertyLine.addDropdown((dropdown) => {
|
||||||
|
const options: Record<string, string> = {
|
||||||
|
'rich_text': 'Text',
|
||||||
|
'number': 'Number',
|
||||||
|
'select': 'Select',
|
||||||
|
'multi_select': 'Multi-Select',
|
||||||
|
'date': 'Date',
|
||||||
|
'files': 'Files & Media',
|
||||||
|
'checkbox': 'Checkbox',
|
||||||
|
'url': 'URL',
|
||||||
|
'email': 'Email',
|
||||||
|
'phone_number': 'Phone Number'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (propertyIndex === 0) {
|
||||||
|
dropdown.addOption('title', 'Title');
|
||||||
|
} else {
|
||||||
|
Object.keys(options).forEach(key => {
|
||||||
|
dropdown.addOption(key, options[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dropdown.setValue(isExistingProperty ? property.customType : "")
|
||||||
|
.onChange(value => {
|
||||||
|
const actualIndex = properties.findIndex(p => p.index === propertyIndex);
|
||||||
|
if (actualIndex !== -1) {
|
||||||
|
properties[actualIndex].customType = value;
|
||||||
|
} else if (!isExistingProperty) {
|
||||||
|
properties.push({ customName: '', customType: value, index: propertyIndex });
|
||||||
|
isExistingProperty = true; // Update the flag to prevent re-adding
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (propertyIndex > 0) {
|
||||||
|
propertyLine.addButton(button => {
|
||||||
|
return button
|
||||||
|
.setTooltip("Delete")
|
||||||
|
.setIcon("trash")
|
||||||
|
.onClick(() => {
|
||||||
|
console.log('Deleting property', properties[propertyIndex]);
|
||||||
|
this.deleteProperty(propertyIndex, properties);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.propertyLines.push(propertyLine);
|
||||||
|
this.updatePropertyLines();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
createStyleDiv(className: string, commandValue: boolean = false, parentEl: HTMLElement): HTMLDivElement {
|
||||||
|
return super.createStyleDiv(className, commandValue, parentEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSettingEl(element: HTMLElement, commandValue: boolean) {
|
||||||
|
super.updateSettingEl(element, commandValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
createSettingEl(contentEl: HTMLElement, name: string, desc: string, type: string, placeholder: string, holderValue: any, dataRecord: string, settingsKey: string): Setting {
|
||||||
|
return super.createSettingEl(contentEl, name, desc, type, placeholder, holderValue, dataRecord, settingsKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
178
src/ui/PreviewModal.ts
Normal file
178
src/ui/PreviewModal.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import { App, ExtraButtonComponent, Modal, Notice, Setting } from "obsidian";
|
||||||
|
import ObsidianSyncNotionPlugin from "../main";
|
||||||
|
import { DatabaseDetails, ObsidianSettingTab } from "./settingTabs";
|
||||||
|
import { customProperty } from "./settingModal";
|
||||||
|
import { i18nConfig } from "../lang/I18n";
|
||||||
|
|
||||||
|
export class PreviewModal extends Modal {
|
||||||
|
plugin: ObsidianSyncNotionPlugin;
|
||||||
|
settingTab: ObsidianSettingTab;
|
||||||
|
dbDetails: DatabaseDetails;
|
||||||
|
|
||||||
|
constructor(app: App, plugin: ObsidianSyncNotionPlugin, settingTab: ObsidianSettingTab, dbDetails: DatabaseDetails) {
|
||||||
|
super(app);
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.settingTab = settingTab;
|
||||||
|
this.dbDetails = dbDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
display(): void {
|
||||||
|
this.containerEl.addClass('preview-modal')
|
||||||
|
this.titleEl.setText(i18nConfig.Preview)
|
||||||
|
|
||||||
|
let { contentEl } = this;
|
||||||
|
|
||||||
|
const previewEl = contentEl.createDiv('preview-content')
|
||||||
|
|
||||||
|
const dbFormatEl = new Setting(previewEl)
|
||||||
|
dbFormatEl
|
||||||
|
.setName(i18nConfig.DatabaseFormatLabel)
|
||||||
|
.addText(text => text
|
||||||
|
.setValue(this.dbDetails.format)
|
||||||
|
.setDisabled(true));
|
||||||
|
|
||||||
|
const dbFullEl = new Setting(previewEl)
|
||||||
|
dbFullEl
|
||||||
|
.setName(i18nConfig.DatabaseFullNameLabel)
|
||||||
|
.addText(text => text
|
||||||
|
.setValue(this.dbDetails.fullName)
|
||||||
|
.setDisabled(true));
|
||||||
|
|
||||||
|
const dbAbbrEl = new Setting(previewEl)
|
||||||
|
dbAbbrEl
|
||||||
|
.setName(i18nConfig.DatabaseAbbreviateNameLabel)
|
||||||
|
.addText(text => text
|
||||||
|
.setValue(this.dbDetails.abName)
|
||||||
|
.setDisabled(true));
|
||||||
|
// .controlEl.createEl('p', { text: this.dbDetails.abName })
|
||||||
|
|
||||||
|
// Setting for toggle and copy buttons
|
||||||
|
new Setting(previewEl)
|
||||||
|
.setName(i18nConfig.NotionAPILabel)
|
||||||
|
.addExtraButton((button: ExtraButtonComponent) => {
|
||||||
|
let isApiKeyVisible = false;
|
||||||
|
|
||||||
|
return button
|
||||||
|
.setTooltip(i18nConfig.ToggleAPIKeyVisibility)
|
||||||
|
.setIcon('eye')
|
||||||
|
.onClick(() => {
|
||||||
|
isApiKeyVisible = !isApiKeyVisible;
|
||||||
|
button.setIcon(isApiKeyVisible ? 'eye-off' : 'eye');
|
||||||
|
apiKeySetting.settingEl.style.display = isApiKeyVisible ? '' : 'none'; // Toggle visibility
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Setting for displaying the API key
|
||||||
|
const apiKeySetting = new Setting(previewEl)
|
||||||
|
|
||||||
|
apiKeySetting.infoEl.createEl('p', { text: this.dbDetails.notionAPI })
|
||||||
|
|
||||||
|
|
||||||
|
apiKeySetting
|
||||||
|
.addExtraButton((button: ExtraButtonComponent) => {
|
||||||
|
return button
|
||||||
|
.setTooltip(i18nConfig.CopyAPIKey)
|
||||||
|
.setIcon('clipboard')
|
||||||
|
.onClick(() => {
|
||||||
|
navigator.clipboard.writeText(this.dbDetails.notionAPI)
|
||||||
|
new Notice(i18nConfig.APIKeyCopied);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
apiKeySetting.settingEl.style.display = 'none'; // Hide initially
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
new Setting(previewEl)
|
||||||
|
.setName(i18nConfig.DatabaseIDLabel)
|
||||||
|
.addExtraButton((button: ExtraButtonComponent) => {
|
||||||
|
let isDbIdVisible = false;
|
||||||
|
|
||||||
|
return button
|
||||||
|
.setTooltip(i18nConfig.ToggleDatabaseIDVisibility)
|
||||||
|
.setIcon('eye')
|
||||||
|
.onClick(() => {
|
||||||
|
|
||||||
|
isDbIdVisible = !isDbIdVisible;
|
||||||
|
button.setIcon(isDbIdVisible ? 'eye-off' : 'eye');
|
||||||
|
dbIdSetting.settingEl.style.display = isDbIdVisible ? '' : 'none'; // Toggle visibility
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
const dbIdSetting = new Setting(previewEl)
|
||||||
|
|
||||||
|
dbIdSetting.infoEl.createEl('p', { text: this.dbDetails.databaseID })
|
||||||
|
|
||||||
|
dbIdSetting
|
||||||
|
.addExtraButton((button: ExtraButtonComponent) => {
|
||||||
|
return button
|
||||||
|
.setTooltip(i18nConfig.CopyDatabaseID)
|
||||||
|
.setIcon('clipboard')
|
||||||
|
.onClick(() => {
|
||||||
|
navigator.clipboard.writeText(this.dbDetails.databaseID)
|
||||||
|
new Notice(i18nConfig.DatabaseIDCopied);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dbIdSetting.settingEl.style.display = 'none'; // Hide initially
|
||||||
|
|
||||||
|
// Preview the custom properties
|
||||||
|
|
||||||
|
if (this.dbDetails.format === 'custom') {
|
||||||
|
|
||||||
|
const customPrv = previewEl.createDiv("custom-tabs");
|
||||||
|
|
||||||
|
this.previewPropertyLine(previewEl, this.dbDetails.customProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onOpen() {
|
||||||
|
this.display()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
previewPropertyLine(containerEl: HTMLElement, properties: customProperty[]): void {
|
||||||
|
|
||||||
|
properties.forEach((property, index) => {
|
||||||
|
const propertyLine = new Setting(containerEl)
|
||||||
|
.setName(index === 0 ? i18nConfig.CustomPropertyFirstColumn : `${i18nConfig.CustomProperty} ${index}`)
|
||||||
|
.setDesc(index === 0 ? i18nConfig.CustomPropertyFirstColumnDesc : "");
|
||||||
|
|
||||||
|
propertyLine.addText(text => {
|
||||||
|
text.setPlaceholder(i18nConfig.CustomPropertyName)
|
||||||
|
.setValue(property.customName)
|
||||||
|
.setDisabled(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
propertyLine.addDropdown((dropdown) => {
|
||||||
|
const options: Record<string, string> = {
|
||||||
|
'title': 'Title',
|
||||||
|
'rich_text': 'Text',
|
||||||
|
'number': 'Number',
|
||||||
|
'select': 'Select',
|
||||||
|
'multi_select': 'Multi-Select',
|
||||||
|
'date': 'Date',
|
||||||
|
'files': 'Files & Media',
|
||||||
|
'checkbox': 'Checkbox',
|
||||||
|
'url': 'URL',
|
||||||
|
'email': 'Email',
|
||||||
|
'phone_number': 'Phone Number',
|
||||||
|
// Additional options can be added here
|
||||||
|
};
|
||||||
|
|
||||||
|
// Populate dropdown with options
|
||||||
|
Object.keys(options).forEach(key => {
|
||||||
|
dropdown.addOption(key, options[key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
dropdown.setValue(property.customType)
|
||||||
|
.setDisabled(true); // Disable dropdown to prevent changes
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
377
src/ui/settingModal.ts
Normal file
377
src/ui/settingModal.ts
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Setting,
|
||||||
|
ButtonComponent, App
|
||||||
|
} from 'obsidian';
|
||||||
|
|
||||||
|
import { i18nConfig } from "../lang/I18n";
|
||||||
|
import ObsidianSyncNotionPlugin from "../main";
|
||||||
|
import { DatabaseDetails, ObsidianSettingTab } from "./settingTabs";
|
||||||
|
|
||||||
|
export interface customProperty {
|
||||||
|
customName: string;
|
||||||
|
customType: string;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SettingModal extends Modal {
|
||||||
|
propertyLines: Setting[] = []; // Store all property line settings
|
||||||
|
properties: customProperty[] = []; // Array to store property values and types
|
||||||
|
[key: string]: any; // Index signature
|
||||||
|
data: Record<string, any> = {
|
||||||
|
databaseFormat: 'none',
|
||||||
|
databaseFullName: '',
|
||||||
|
databaseAbbreviateName: '',
|
||||||
|
notionAPI: '',
|
||||||
|
databaseID: '',
|
||||||
|
tagButton: true,
|
||||||
|
customTitleButton: false,
|
||||||
|
customTitleName: '',
|
||||||
|
customProperties: [],
|
||||||
|
// customValues: '',
|
||||||
|
saved: false,
|
||||||
|
};
|
||||||
|
plugin: ObsidianSyncNotionPlugin;
|
||||||
|
settingTab: ObsidianSettingTab;
|
||||||
|
|
||||||
|
constructor(app: App, plugin: ObsidianSyncNotionPlugin, settingTab: ObsidianSettingTab, dbDetails?: DatabaseDetails) {
|
||||||
|
super(app);
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.settingTab = settingTab;
|
||||||
|
this.properties = [];
|
||||||
|
if (dbDetails) {
|
||||||
|
this.data.databaseFormat = dbDetails.format;
|
||||||
|
this.data.databaseFullName = dbDetails.fullName;
|
||||||
|
this.data.databaseAbbreviateName = dbDetails.abName;
|
||||||
|
this.data.notionAPI = dbDetails.notionAPI;
|
||||||
|
this.data.databaseID = dbDetails.databaseID;
|
||||||
|
this.data.tagButton = dbDetails.tagButton;
|
||||||
|
this.data.customTitleButton = dbDetails.customTitleButton;
|
||||||
|
this.data.customTitleName = dbDetails.customTitleName;
|
||||||
|
this.data.customProperties = dbDetails.customProperties;
|
||||||
|
// this.data.customValues = dbDetails.customValues;
|
||||||
|
this.data.saved = dbDetails.saved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
display(): void {
|
||||||
|
this.containerEl.addClass("settings-modal");
|
||||||
|
this.titleEl.setText(i18nConfig.AddNewDatabaseModal);
|
||||||
|
|
||||||
|
// create the dropdown button to select the database format
|
||||||
|
let { contentEl } = this;
|
||||||
|
contentEl.empty();
|
||||||
|
|
||||||
|
const settingDiv = contentEl.createDiv('setting-div');
|
||||||
|
const nextTabs = contentEl.createDiv('next-tabs');
|
||||||
|
|
||||||
|
|
||||||
|
new Setting(settingDiv)
|
||||||
|
.setName(i18nConfig.databaseFormat)
|
||||||
|
.setDesc(i18nConfig.databaseFormatDesc)
|
||||||
|
.addDropdown((component) => {
|
||||||
|
component
|
||||||
|
.addOption('none', '')
|
||||||
|
.addOption('general', i18nConfig.databaseGeneral)
|
||||||
|
.addOption('next', i18nConfig.databaseNext)
|
||||||
|
.addOption('custom', i18nConfig.databaseCustom)
|
||||||
|
.setValue(this.data.databaseFormat)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.data.databaseFormat = value;
|
||||||
|
nextTabs.empty();
|
||||||
|
this.updateContentBasedOnSelection(value, nextTabs);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize content based on the current dropdown value
|
||||||
|
(this.data.saved) ? this.updateContentBasedOnSelection(this.data.databaseFormat, nextTabs) : this.updateContentBasedOnSelection(this.plugin.settings.databaseFormat, nextTabs);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// add save button
|
||||||
|
let footerEl = contentEl.createDiv('save-button');
|
||||||
|
let saveButton = new Setting(footerEl)
|
||||||
|
saveButton.addButton((button: ButtonComponent) => {
|
||||||
|
return button
|
||||||
|
.setTooltip('Save')
|
||||||
|
.setIcon('checkmark')
|
||||||
|
.onClick(async () => {
|
||||||
|
this.data.saved = true;
|
||||||
|
this.data.customProperties = this.properties;
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
saveButton.addExtraButton((button) => {
|
||||||
|
return button
|
||||||
|
.setTooltip('Cancel')
|
||||||
|
.setIcon('cross')
|
||||||
|
.onClick(() => {
|
||||||
|
this.data.saved = false;
|
||||||
|
// this.data.customProperties = this.properties;
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContentBasedOnSelection(value: string, nextTabs: HTMLElement): void {
|
||||||
|
// Clear existing content
|
||||||
|
nextTabs.empty();
|
||||||
|
|
||||||
|
// Generate content based on the selected value
|
||||||
|
if (value === 'general') {
|
||||||
|
nextTabs.createEl('h3', { text: i18nConfig.NotionGeneralSettingHeader });
|
||||||
|
|
||||||
|
// add full name
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.databaseFullName, i18nConfig.databaseFullNameDesc, 'text', i18nConfig.databaseFullNameText, this.data.databaseFullName, 'data', 'databaseFullName')
|
||||||
|
|
||||||
|
// add abbreviate name
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.databaseAbbreviateName, i18nConfig.databaseAbbreviateNameDesc, 'text', i18nConfig.databaseAbbreviateNameText, this.data.databaseAbbreviateName, 'data', 'databaseAbbreviateName')
|
||||||
|
|
||||||
|
// tag button
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.NotionTagButton, i18nConfig.NotionTagButtonDesc, 'toggle', i18nConfig.NotionCustomTitleText, this.data.tagButton, 'data', 'tagButton')
|
||||||
|
|
||||||
|
// add custom title button
|
||||||
|
|
||||||
|
new Setting(nextTabs)
|
||||||
|
.setName(i18nConfig.NotionCustomTitle)
|
||||||
|
.setDesc(i18nConfig.NotionCustomTitleDesc)
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle
|
||||||
|
.setValue(this.data.customTitleButton)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.data.customTitleButton = value;
|
||||||
|
this.updateSettingEl(CustomNameEl, value)
|
||||||
|
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
// add custom title name
|
||||||
|
const CustomNameEl = this.createStyleDiv('custom-name', (this.data.customTitleButton), nextTabs);
|
||||||
|
this.createSettingEl(CustomNameEl, i18nConfig.NotionCustomTitleName, i18nConfig.NotionCustomTitleNameDesc, 'text', i18nConfig.NotionCustomTitleText, this.data.customTitleName, 'data', 'customTitleName')
|
||||||
|
|
||||||
|
|
||||||
|
// add api key
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.NotionAPI, i18nConfig.NotionAPIDesc, 'password', i18nConfig.NotionAPIText, this.data.notionAPI, 'data', 'notionAPI')
|
||||||
|
|
||||||
|
// add database id
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.DatabaseID, i18nConfig.DatabaseIDDesc, 'password', i18nConfig.DatabaseIDText, this.data.databaseID, 'data', 'databaseID')
|
||||||
|
|
||||||
|
|
||||||
|
} else if (value === 'next') {
|
||||||
|
|
||||||
|
nextTabs.createEl('h3', { text: i18nConfig.NotionNextSettingHeader });
|
||||||
|
|
||||||
|
// add full name
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.databaseFullName, i18nConfig.databaseFullNameDesc, 'text', i18nConfig.databaseFullNameText, this.data.databaseFullName, 'data', 'databaseFullName')
|
||||||
|
|
||||||
|
// add abbreviate name
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.databaseAbbreviateName, i18nConfig.databaseAbbreviateNameDesc, 'text', i18nConfig.databaseAbbreviateNameText, this.data.databaseAbbreviateName, 'data', 'databaseAbbreviateName')
|
||||||
|
|
||||||
|
// add api key
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.NotionAPI, i18nConfig.NotionAPIDesc, 'password', i18nConfig.NotionAPIText, this.data.notionAPI, 'data', 'notionAPI')
|
||||||
|
|
||||||
|
|
||||||
|
// add database id
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.DatabaseID, i18nConfig.DatabaseIDDesc, 'password', i18nConfig.DatabaseIDText, this.data.databaseID, 'data', 'databaseID')
|
||||||
|
|
||||||
|
} else if (value === 'custom') {
|
||||||
|
|
||||||
|
nextTabs.createEl('h3', { text: i18nConfig.NotionCustomSettingHeader });
|
||||||
|
|
||||||
|
// add full name
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.databaseFullName, i18nConfig.databaseFullNameDesc, 'text', i18nConfig.databaseFullNameText, this.data.databaseFullName, 'data', 'databaseFullName')
|
||||||
|
|
||||||
|
// add abbreviate name
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.databaseAbbreviateName, i18nConfig.databaseAbbreviateNameDesc, 'text', i18nConfig.databaseAbbreviateNameText, this.data.databaseAbbreviateName, 'data', 'databaseAbbreviateName')
|
||||||
|
|
||||||
|
// add api key
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.NotionAPI, i18nConfig.NotionAPIDesc, 'password', i18nConfig.NotionAPIText, this.data.notionAPI, 'data', 'notionAPI')
|
||||||
|
|
||||||
|
// add database id
|
||||||
|
this.createSettingEl(nextTabs, i18nConfig.DatabaseID, i18nConfig.DatabaseIDDesc, 'password', i18nConfig.DatabaseIDText, this.data.databaseID, 'data', 'databaseID')
|
||||||
|
|
||||||
|
// add new property button
|
||||||
|
new Setting(nextTabs)
|
||||||
|
.setName(i18nConfig.NotionCustomValues)
|
||||||
|
.setDesc(i18nConfig.NotionCustomValuesDesc)
|
||||||
|
.addButton((button: ButtonComponent) => {
|
||||||
|
return button
|
||||||
|
.setTooltip('Add one more property')
|
||||||
|
.setButtonText('Add New Property')
|
||||||
|
.onClick(async () => {
|
||||||
|
const customTabs = nextTabs.createDiv("custom-tabs");
|
||||||
|
this.createPropertyLine(customTabs, this.properties);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onOpen() {
|
||||||
|
// add console log to check if the modal is opened
|
||||||
|
this.display()
|
||||||
|
}
|
||||||
|
|
||||||
|
createPropertyLine(containerEl: HTMLElement, properties: customProperty[]): void {
|
||||||
|
const propertyIndex = properties.length;
|
||||||
|
|
||||||
|
properties.push({ customName: "", customType: "", index: propertyIndex });
|
||||||
|
|
||||||
|
const propertyLine = new Setting(containerEl)
|
||||||
|
.setName(propertyIndex === 0 ? i18nConfig.CustomPropertyFirstColumn : `${i18nConfig.CustomProperty} ${propertyIndex}`)
|
||||||
|
.setDesc(propertyIndex === 0 ? i18nConfig.CustomPropertyFirstColumnDesc : "");
|
||||||
|
|
||||||
|
propertyLine.addText(text => {
|
||||||
|
text.setPlaceholder(i18nConfig.CustomPropertyName)
|
||||||
|
.setValue("")
|
||||||
|
.onChange(value => {
|
||||||
|
let actualIndex = properties.findIndex(p => p.index === propertyIndex);
|
||||||
|
if (actualIndex !== -1) {
|
||||||
|
properties[actualIndex].customName = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
propertyLine.addDropdown((dropdown) => {
|
||||||
|
const options: Record<string, string> = {
|
||||||
|
'rich_text': 'Text',
|
||||||
|
'number': 'Number',
|
||||||
|
'select': 'Select',
|
||||||
|
'multi_select': 'Multi-Select',
|
||||||
|
'date': 'Date',
|
||||||
|
'files': 'Files & Media',
|
||||||
|
'checkbox': 'Checkbox',
|
||||||
|
'url': 'URL',
|
||||||
|
'email': 'Email',
|
||||||
|
'phone_number': 'Phone Number',
|
||||||
|
// 'formula': 'Formula',
|
||||||
|
// 'relation': 'Relation',
|
||||||
|
// 'rollup': 'Rollup',
|
||||||
|
// 'created_time': 'Created time',
|
||||||
|
// 'created_by': 'Created by',
|
||||||
|
// 'last_edited_time': 'Last Edited Time',
|
||||||
|
// 'last_edited_by': 'Last Edited By',
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentProperty = properties[propertyIndex];
|
||||||
|
|
||||||
|
if (propertyIndex === 0) {
|
||||||
|
dropdown.addOption("title", "Title");
|
||||||
|
} else {
|
||||||
|
Object.keys(options).forEach(key => {
|
||||||
|
dropdown.addOption(key, options[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
dropdown.setValue("")
|
||||||
|
.onChange(value => {
|
||||||
|
if (currentProperty) {
|
||||||
|
currentProperty.customType = value;
|
||||||
|
// Retrieve the index of currentProperty from the properties array
|
||||||
|
const updatedIndex = properties.findIndex(p => p === currentProperty);
|
||||||
|
console.log(`Updated value at index ${updatedIndex}: ${value}`);
|
||||||
|
} else {
|
||||||
|
console.log("Property not found, may have been deleted.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (propertyIndex > 0) {
|
||||||
|
propertyLine.addButton(button => {
|
||||||
|
return button
|
||||||
|
.setTooltip("Delete")
|
||||||
|
.setIcon("trash")
|
||||||
|
.onClick(() => {
|
||||||
|
this.deleteProperty(propertyIndex, properties);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.propertyLines.push(propertyLine);
|
||||||
|
this.updatePropertyLines(); // Ensure property lines are updated after creation
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteProperty(propertyIndex: number, properties: customProperty[]): void {
|
||||||
|
let actualIndex = properties.findIndex(p => p.index === propertyIndex);
|
||||||
|
if (actualIndex !== -1) {
|
||||||
|
properties.splice(actualIndex, 1);
|
||||||
|
if (this.propertyLines[actualIndex]) {
|
||||||
|
this.propertyLines[actualIndex].settingEl.remove();
|
||||||
|
this.propertyLines.splice(actualIndex, 1);
|
||||||
|
}
|
||||||
|
// Update indices in the properties array
|
||||||
|
properties.forEach((prop, idx) => {
|
||||||
|
prop.index = idx;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updatePropertyLines();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePropertyLines() {
|
||||||
|
this.propertyLines.forEach((line, idx) => {
|
||||||
|
line.setName(idx === 0 ? i18nConfig.CustomPropertyFirstColumn : `${i18nConfig.CustomProperty} ${idx}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a function to create a div with a style for pop over elements
|
||||||
|
public createStyleDiv(className: string, commandValue: boolean = false, parentEl: HTMLElement) {
|
||||||
|
return parentEl.createDiv(className, (div) => {
|
||||||
|
this.updateSettingEl(div, commandValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the setting display style in the setting tab
|
||||||
|
public updateSettingEl(element: HTMLElement, commandValue: boolean) {
|
||||||
|
element.style.borderTop = commandValue ? "1px solid var(--background-modifier-border)" : "none";
|
||||||
|
element.style.paddingTop = commandValue ? "0.75em" : "0";
|
||||||
|
element.style.display = commandValue ? "block" : "none";
|
||||||
|
element.style.alignItems = "center";
|
||||||
|
}
|
||||||
|
|
||||||
|
// function to add one setting element in the setting tab.
|
||||||
|
public createSettingEl(contentEl: HTMLElement, name: string, desc: string, type: string, placeholder: string, holderValue: any, dataRecord: string, settingsKey: string) {
|
||||||
|
if (type === 'password') {
|
||||||
|
return new Setting(contentEl)
|
||||||
|
.setName(name)
|
||||||
|
.setDesc(desc)
|
||||||
|
.addText((text) => {
|
||||||
|
text.inputEl.type = type;
|
||||||
|
return text
|
||||||
|
.setPlaceholder(placeholder)
|
||||||
|
.setValue(holderValue)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this[dataRecord][settingsKey] = value; // Update the settings dictionary await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else if (type === 'toggle') {
|
||||||
|
return new Setting(contentEl)
|
||||||
|
.setName(name)
|
||||||
|
.setDesc(desc)
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle
|
||||||
|
.setValue(holderValue)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this[dataRecord][settingsKey] = value; // Update the settings dictionary await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (type === 'text') {
|
||||||
|
return new Setting(contentEl)
|
||||||
|
.setName(name)
|
||||||
|
.setDesc(desc)
|
||||||
|
.addText((text) =>
|
||||||
|
text
|
||||||
|
.setPlaceholder(placeholder)
|
||||||
|
.setValue(holderValue)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this[dataRecord][settingsKey] = value; // Update the settings dictionary await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
import { App, PluginSettingTab, Setting } from "obsidian";
|
import { App, ButtonComponent, PluginSettingTab, Setting } from "obsidian";
|
||||||
import { i18nConfig } from "../lang/I18n";
|
import { i18nConfig } from "../lang/I18n";
|
||||||
import ObsidianSyncNotionPlugin from "../main";
|
import ObsidianSyncNotionPlugin from "../main";
|
||||||
|
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 {
|
export interface PluginSettings {
|
||||||
NextButton: boolean;
|
NextButton: boolean;
|
||||||
@@ -8,16 +13,39 @@ export interface PluginSettings {
|
|||||||
databaseIDNext: string;
|
databaseIDNext: string;
|
||||||
bannerUrl: string;
|
bannerUrl: string;
|
||||||
notionUser: string;
|
notionUser: string;
|
||||||
|
NotionLinkDisplay: boolean;
|
||||||
|
autoCopyNotionLink: boolean;
|
||||||
|
autoSync: boolean;
|
||||||
|
autoSyncDelay: number;
|
||||||
|
autoSyncSuccessNotice: boolean;
|
||||||
|
autoSyncFrontmatterKey: string;
|
||||||
proxy: string;
|
proxy: string;
|
||||||
GeneralButton: boolean;
|
GeneralButton: boolean;
|
||||||
CustomTitleButton: boolean;
|
tagButton: boolean;
|
||||||
CustomTitleName: string;
|
customTitleButton: boolean;
|
||||||
|
customTitleName: string;
|
||||||
notionAPIGeneral: string;
|
notionAPIGeneral: string;
|
||||||
databaseIDGeneral: string;
|
databaseIDGeneral: string;
|
||||||
CustomButton: boolean;
|
CustomButton: boolean;
|
||||||
|
CustomValues: string;
|
||||||
notionAPICustom: string;
|
notionAPICustom: string;
|
||||||
databaseIDCustom: string;
|
databaseIDCustom: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
|
databaseDetails: Record<string, DatabaseDetails>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DatabaseDetails {
|
||||||
|
format: string;
|
||||||
|
fullName: string;
|
||||||
|
abName: string;
|
||||||
|
notionAPI: string;
|
||||||
|
databaseID: string;
|
||||||
|
tagButton: boolean;
|
||||||
|
customTitleButton: boolean;
|
||||||
|
customTitleName: string;
|
||||||
|
customProperties: { customName: string, customType: string, index: number }[];
|
||||||
|
// customValues: string;
|
||||||
|
saved: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: PluginSettings = {
|
export const DEFAULT_SETTINGS: PluginSettings = {
|
||||||
@@ -26,20 +54,31 @@ export const DEFAULT_SETTINGS: PluginSettings = {
|
|||||||
databaseIDNext: "",
|
databaseIDNext: "",
|
||||||
bannerUrl: "",
|
bannerUrl: "",
|
||||||
notionUser: "",
|
notionUser: "",
|
||||||
|
NotionLinkDisplay: true,
|
||||||
|
autoCopyNotionLink: true,
|
||||||
|
autoSync: false,
|
||||||
|
autoSyncDelay: 5,
|
||||||
|
autoSyncSuccessNotice: false,
|
||||||
|
autoSyncFrontmatterKey: DEFAULT_AUTO_SYNC_DATABASE_KEY,
|
||||||
proxy: "",
|
proxy: "",
|
||||||
GeneralButton: true,
|
GeneralButton: true,
|
||||||
CustomTitleButton: false,
|
tagButton: true,
|
||||||
CustomTitleName: "",
|
customTitleButton: false,
|
||||||
|
customTitleName: "",
|
||||||
notionAPIGeneral: "",
|
notionAPIGeneral: "",
|
||||||
databaseIDGeneral: "",
|
databaseIDGeneral: "",
|
||||||
CustomButton: false,
|
CustomButton: false,
|
||||||
|
CustomValues: "",
|
||||||
notionAPICustom: "",
|
notionAPICustom: "",
|
||||||
databaseIDCustom: "",
|
databaseIDCustom: "",
|
||||||
|
databaseDetails: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export class ObsidianSettingTab extends PluginSettingTab {
|
export class ObsidianSettingTab extends PluginSettingTab {
|
||||||
plugin: ObsidianSyncNotionPlugin;
|
plugin: ObsidianSyncNotionPlugin;
|
||||||
|
databaseEl: HTMLDivElement;
|
||||||
|
autoSyncDelayContainer: HTMLElement | null = null;
|
||||||
|
|
||||||
constructor(app: App, plugin: ObsidianSyncNotionPlugin) {
|
constructor(app: App, plugin: ObsidianSyncNotionPlugin) {
|
||||||
super(app, plugin);
|
super(app, plugin);
|
||||||
@@ -58,132 +97,133 @@ export class ObsidianSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
this.createSettingEl(containerEl, i18nConfig.NotionUser, i18nConfig.NotionUserDesc, 'text', i18nConfig.NotionUserText, this.plugin.settings.notionUser, 'notionUser')
|
this.createSettingEl(containerEl, i18nConfig.NotionUser, i18nConfig.NotionUserDesc, 'text', i18nConfig.NotionUserText, this.plugin.settings.notionUser, 'notionUser')
|
||||||
|
|
||||||
containerEl.createEl('h2', { text: i18nConfig.NotionNextSettingHeader })
|
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')
|
||||||
|
this.createSettingEl(
|
||||||
|
containerEl,
|
||||||
|
i18nConfig.AutoSyncSuccessNotice,
|
||||||
|
i18nConfig.AutoSyncSuccessNoticeDesc,
|
||||||
|
'toggle',
|
||||||
|
i18nConfig.AutoSyncSuccessNotice,
|
||||||
|
this.plugin.settings.autoSyncSuccessNotice,
|
||||||
|
'autoSyncSuccessNotice'
|
||||||
|
)
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(i18nConfig.NotionNextButton)
|
.setName(i18nConfig.AutoSyncFrontmatterKey)
|
||||||
.setDesc(i18nConfig.NotionNextButtonDesc)
|
.setDesc(i18nConfig.AutoSyncFrontmatterKeyDesc)
|
||||||
.addToggle((toggle) =>
|
.addText((text) =>
|
||||||
toggle
|
text
|
||||||
.setValue(this.plugin.settings.NextButton)
|
.setPlaceholder(DEFAULT_AUTO_SYNC_DATABASE_KEY)
|
||||||
|
.setValue(this.plugin.settings.autoSyncFrontmatterKey ?? "")
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.NextButton = value;
|
this.plugin.settings.autoSyncFrontmatterKey = value;
|
||||||
|
|
||||||
this.updateSettingEl(notionAPINextEl, value)
|
|
||||||
|
|
||||||
this.updateSettingEl(databaseIDNextEl, value)
|
|
||||||
|
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
await this.plugin.commands.updateCommand();
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Auto Sync Delay setting - only visible when autoSync is enabled
|
||||||
|
this.autoSyncDelayContainer = containerEl.createDiv();
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const notionAPINextEl = this.createStyleDiv('api-next', this.plugin.settings.NextButton)
|
// Set initial visibility
|
||||||
this.createSettingEl(notionAPINextEl, i18nConfig.NotionAPI, i18nConfig.NotionAPIDesc, 'password', i18nConfig.NotionAPIText, this.plugin.settings.notionAPINext, 'notionAPINext')
|
this.updateAutoSyncDelayVisibility();
|
||||||
|
|
||||||
const databaseIDNextEl = this.createStyleDiv('databaseID-next', this.plugin.settings.NextButton)
|
|
||||||
this.createSettingEl(databaseIDNextEl, i18nConfig.DatabaseID, i18nConfig.NotionAPIDesc, 'password', i18nConfig.DatabaseIDText, this.plugin.settings.databaseIDNext, 'databaseIDNext')
|
|
||||||
|
|
||||||
|
|
||||||
// General Database Settings
|
// add new button
|
||||||
containerEl.createEl('h2', { text: i18nConfig.NotionGeneralSettingHeader });
|
|
||||||
|
|
||||||
// new Setting(containerEl)
|
|
||||||
// .setName(i18nConfig.NotYetFinish)
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(i18nConfig.NotionGeneralButton)
|
.setName(i18nConfig.AddNewDatabase)
|
||||||
.setDesc(i18nConfig.NotionGeneralButtonDesc)
|
.setDesc(i18nConfig.AddNewDatabaseDesc)
|
||||||
.addToggle((toggle) =>
|
.addButton((button: ButtonComponent): ButtonComponent => {
|
||||||
toggle
|
return button
|
||||||
.setValue(this.plugin.settings.GeneralButton)
|
.setTooltip(i18nConfig.AddNewDatabaseTooltip)
|
||||||
.onChange(async (value) => {
|
.setIcon("plus")
|
||||||
this.plugin.settings.GeneralButton = value;
|
.onClick(async () => {
|
||||||
|
let modal = new SettingModal(this.app, this.plugin, this);
|
||||||
|
|
||||||
this.updateSettingEl(CustomTitleEl, value)
|
modal.onClose = () => {
|
||||||
this.updateSettingEl(notionAPIGeneralEl, value)
|
if (modal.data.saved) {
|
||||||
this.updateSettingEl(databaseIDGeneralEl, value)
|
const dbDetails = {
|
||||||
|
format: modal.data.databaseFormat,
|
||||||
|
fullName: modal.data.databaseFullName,
|
||||||
|
abName: modal.data.databaseAbbreviateName,
|
||||||
|
notionAPI: modal.data.notionAPI,
|
||||||
|
databaseID: modal.data.databaseID,
|
||||||
|
tagButton: modal.data.tagButton,
|
||||||
|
customTitleButton: modal.data.customTitleButton,
|
||||||
|
customTitleName: modal.data.customTitleName,
|
||||||
|
customProperties: modal.data.customProperties,
|
||||||
|
// customValues: modal.data.customValues,
|
||||||
|
saved: modal.data.saved,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.plugin.addDatabaseDetails(dbDetails);
|
||||||
|
|
||||||
await this.plugin.saveSettings();
|
this.plugin.commands.updateCommand();
|
||||||
await this.plugin.commands.updateCommand();
|
|
||||||
|
|
||||||
})
|
this.display()
|
||||||
);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const CustomTitleEl = this.createStyleDiv('custom-title', this.plugin.settings.GeneralButton);
|
modal.open();
|
||||||
new Setting(CustomTitleEl)
|
});
|
||||||
.setName(i18nConfig.NotionCustomTitle)
|
});
|
||||||
.setDesc(i18nConfig.NotionCustomTitleDesc)
|
|
||||||
.addToggle((toggle) =>
|
|
||||||
toggle
|
|
||||||
.setValue(this.plugin.settings.CustomTitleButton)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.CustomTitleButton = value;
|
|
||||||
|
|
||||||
this.updateSettingEl(CustomNameEl, value)
|
// new section to display all created database
|
||||||
|
containerEl.createEl('h2', { text: "Database List" });
|
||||||
|
|
||||||
await this.plugin.saveSettings();
|
this.databaseEl = containerEl.createDiv('database-list');
|
||||||
await this.plugin.commands.updateCommand();
|
// list all created database
|
||||||
})
|
this.showDatabase();
|
||||||
);
|
|
||||||
|
|
||||||
const CustomNameEl = this.createStyleDiv('custom-name', this.plugin.settings.CustomTitleButton);
|
|
||||||
this.createSettingEl(CustomNameEl, i18nConfig.NotionCustomTitleName, i18nConfig.NotionCustomTitleNameDesc, 'text', i18nConfig.NotionCustomTitleText, this.plugin.settings.CustomTitleName, 'CustomTitleName')
|
|
||||||
|
|
||||||
// new Setting(containerEl)
|
|
||||||
// .setName("Convert tags(optional)")
|
|
||||||
// .setDesc("Transfer the Obsidian tags to the Notion table. It requires the column with the name 'Tags'")
|
|
||||||
// .addToggle((toggle) =>
|
|
||||||
// toggle
|
|
||||||
// .setValue(this.plugin.settings.allowTags)
|
|
||||||
// .onChange(async (value) => {
|
|
||||||
// this.plugin.settings.allowTags = value;
|
|
||||||
// await this.plugin.saveSettings();
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
|
|
||||||
const notionAPIGeneralEl = this.createStyleDiv('api-general', this.plugin.settings.GeneralButton);
|
|
||||||
this.createSettingEl(notionAPIGeneralEl, i18nConfig.NotionAPI, i18nConfig.NotionAPIDesc, 'password', i18nConfig.NotionAPIText, this.plugin.settings.notionAPIGeneral, 'notionAPIGeneral')
|
|
||||||
|
|
||||||
|
|
||||||
const databaseIDGeneralEl = this.createStyleDiv('databaseID-general', this.plugin.settings.GeneralButton);
|
|
||||||
this.createSettingEl(databaseIDGeneralEl, i18nConfig.DatabaseID, i18nConfig.NotionAPIDesc, 'password', i18nConfig.DatabaseIDText, this.plugin.settings.databaseIDGeneral, 'databaseIDGeneral')
|
|
||||||
|
|
||||||
// Custom Database Settings
|
|
||||||
|
|
||||||
// containerEl.createEl('h2', {text: i18nConfig.NotionCustomSettingHeader});
|
|
||||||
//
|
|
||||||
// new Setting(containerEl)
|
|
||||||
// .setName(i18nConfig.NotionCustomButton)
|
|
||||||
// .setDesc(i18nConfig.NotionCustomButtonDesc)
|
|
||||||
// .addToggle((toggle) =>
|
|
||||||
// toggle
|
|
||||||
// .setValue(this.plugin.settings.CustomButton)
|
|
||||||
// .onChange(async (value) => {
|
|
||||||
// this.plugin.settings.CustomButton = value;
|
|
||||||
// await this.plugin.saveSettings();
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a function to create a div with a style for pop over elements
|
// create a function to create a div with a style for pop over elements
|
||||||
private createStyleDiv(className: string, commandValue: boolean = false) {
|
// public createStyleDiv(className: string, commandValue: boolean = false) {
|
||||||
return this.containerEl.createDiv(className, (div) => {
|
// return this.containerEl.createDiv(className, (div) => {
|
||||||
this.updateSettingEl(div, commandValue);
|
// this.updateSettingEl(div, commandValue);
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
// update the setting display style in the setting tab
|
// update the setting display style in the setting tab
|
||||||
private updateSettingEl(element: HTMLElement, commandValue: boolean) {
|
public updateSettingEl(element: HTMLElement, commandValue: boolean) {
|
||||||
element.style.borderTop = commandValue ? "1px solid var(--background-modifier-border)" : "none";
|
element.style.borderTop = commandValue ? "1px solid var(--background-modifier-border)" : "none";
|
||||||
element.style.paddingTop = commandValue ? "0.75em" : "0";
|
element.style.paddingTop = commandValue ? "0.75em" : "0";
|
||||||
element.style.display = commandValue ? "block" : "none";
|
element.style.display = commandValue ? "block" : "none";
|
||||||
element.style.alignItems = "center";
|
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.
|
// function to add one setting element in the setting tab.
|
||||||
private createSettingEl(containerEl: HTMLElement, name: string, desc: string, type: string, placeholder: string, holderValue: any, settingsKey: string) {
|
public createSettingEl(containerEl: HTMLElement, name: string, desc: string, type: string, placeholder: string, holderValue: any, settingsKey: string) {
|
||||||
if (type === 'password') {
|
if (type === 'password') {
|
||||||
return new Setting(containerEl)
|
return new Setting(containerEl)
|
||||||
.setName(name)
|
.setName(name)
|
||||||
@@ -209,6 +249,12 @@ export class ObsidianSettingTab extends PluginSettingTab {
|
|||||||
this.plugin.settings[settingsKey] = value; // Update the plugin settings directly
|
this.plugin.settings[settingsKey] = value; // Update the plugin settings directly
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
await this.plugin.commands.updateCommand();
|
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') {
|
} else if (type === 'text') {
|
||||||
@@ -228,4 +274,109 @@ export class ObsidianSettingTab extends PluginSettingTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// function to show all the database details
|
||||||
|
showDatabase() {
|
||||||
|
this.databaseEl.empty();
|
||||||
|
|
||||||
|
for (let key in this.plugin.settings.databaseDetails) {
|
||||||
|
let dbDetails = this.plugin.settings.databaseDetails[key];
|
||||||
|
|
||||||
|
const databaseDiv = this.databaseEl.createDiv('database-div');
|
||||||
|
|
||||||
|
let settingEl = new Setting(databaseDiv)
|
||||||
|
.setName(`${dbDetails.fullName} (${dbDetails.abName})`)
|
||||||
|
.setDesc(dbDetails.format)
|
||||||
|
|
||||||
|
|
||||||
|
// add a button for preview data
|
||||||
|
settingEl
|
||||||
|
.addButton((button: ButtonComponent): ButtonComponent => {
|
||||||
|
return button
|
||||||
|
.setTooltip("Preview Database")
|
||||||
|
.setIcon("eye")
|
||||||
|
.onClick(async () => {
|
||||||
|
let modal = new PreviewModal(this.app, this.plugin, this, dbDetails);
|
||||||
|
|
||||||
|
modal.open();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// add a button for edit data
|
||||||
|
settingEl
|
||||||
|
.addButton((button: ButtonComponent): ButtonComponent => {
|
||||||
|
return button
|
||||||
|
.setTooltip("Edit Database")
|
||||||
|
.setIcon("pencil")
|
||||||
|
.onClick(async () => {
|
||||||
|
let modal = new EditModal(this.app, this.plugin, this, dbDetails);
|
||||||
|
|
||||||
|
modal.onClose = () => {
|
||||||
|
if (modal.dataTemp.savedTempInd) {
|
||||||
|
const dbDetailsNew: DatabaseDetails = {
|
||||||
|
format: modal.dataTemp.databaseFormatTemp,
|
||||||
|
fullName: modal.dataTemp.databaseFullNameTemp,
|
||||||
|
abName: modal.dataTemp.databaseAbbreviateNameTemp,
|
||||||
|
notionAPI: modal.dataTemp.notionAPITemp,
|
||||||
|
databaseID: modal.dataTemp.databaseIDTemp,
|
||||||
|
tagButton: modal.dataTemp.tagButtonTemp,
|
||||||
|
customTitleButton: modal.dataTemp.customTitleButtonTemp,
|
||||||
|
customTitleName: modal.dataTemp.customTitleNameTemp,
|
||||||
|
customProperties: modal.dataTemp.customPropertiesTemp,
|
||||||
|
// customValues: modal.data.customValues,
|
||||||
|
saved: modal.dataTemp.savedTemp,
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbDetailsPrev: DatabaseDetails = {
|
||||||
|
format: modal.dataPrev.databaseFormatPrev,
|
||||||
|
fullName: modal.dataPrev.databaseFullNamePrev,
|
||||||
|
abName: modal.dataPrev.databaseAbbreviateNamePrev,
|
||||||
|
notionAPI: modal.dataPrev.notionAPIPrev,
|
||||||
|
databaseID: modal.dataPrev.databaseIDPrev,
|
||||||
|
tagButton: modal.dataPrev.tagButtonPrev,
|
||||||
|
customTitleButton: modal.dataPrev.customTitleButtonPrev,
|
||||||
|
customTitleName: modal.dataPrev.customTitleNamePrev,
|
||||||
|
customProperties: modal.dataPrev.customPropertiesPrev,
|
||||||
|
// customValues: modal.data.customValues,
|
||||||
|
saved: modal.dataPrev.savedPrev,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.plugin.deleteDatabaseDetails(dbDetailsPrev);
|
||||||
|
this.plugin.updateDatabaseDetails(dbDetailsNew);
|
||||||
|
|
||||||
|
this.plugin.commands.updateCommand();
|
||||||
|
|
||||||
|
this.display()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.open();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
settingEl
|
||||||
|
.addButton((button: ButtonComponent): ButtonComponent => {
|
||||||
|
return button
|
||||||
|
.setTooltip("Delete Database")
|
||||||
|
.setIcon("trash")
|
||||||
|
.onClick(async () => {
|
||||||
|
let modal = new DeleteModal(this.app, this.plugin, this, dbDetails);
|
||||||
|
|
||||||
|
modal.onClose = () => {
|
||||||
|
if (modal.data.deleted) {
|
||||||
|
this.plugin.deleteDatabaseDetails(dbDetails);
|
||||||
|
|
||||||
|
console.log(dbDetails.fullName + " deleted");
|
||||||
|
|
||||||
|
this.plugin.commands.updateCommand();
|
||||||
|
|
||||||
|
this.display()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.open();
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
645
src/upload/Upload2Notion.ts
Normal file
645
src/upload/Upload2Notion.ts
Normal file
@@ -0,0 +1,645 @@
|
|||||||
|
import { App, TFile } from "obsidian";
|
||||||
|
import { markdownToBlocks } from "@jxpeng98/martian";
|
||||||
|
import * as yamlFrontMatter from "yaml-front-matter";
|
||||||
|
import { LIMITS, paragraph } from "@jxpeng98/martian";
|
||||||
|
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";
|
||||||
|
|
||||||
|
interface BaseSyncRequest {
|
||||||
|
dataset: DatasetType;
|
||||||
|
markdown: string;
|
||||||
|
nowFile: TFile;
|
||||||
|
app: App;
|
||||||
|
isAutoSync?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeneralSyncRequest extends BaseSyncRequest {
|
||||||
|
dataset: "general";
|
||||||
|
title: string;
|
||||||
|
cover: string;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NextSyncRequest extends BaseSyncRequest {
|
||||||
|
dataset: "next";
|
||||||
|
title: string;
|
||||||
|
emoji: string;
|
||||||
|
cover: string;
|
||||||
|
tags: string[];
|
||||||
|
type: string;
|
||||||
|
slug: string;
|
||||||
|
stats: string;
|
||||||
|
category: string;
|
||||||
|
summary: string;
|
||||||
|
password: string;
|
||||||
|
favicon: string;
|
||||||
|
datetime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomSyncRequest extends BaseSyncRequest {
|
||||||
|
dataset: "custom";
|
||||||
|
cover: string;
|
||||||
|
customValues: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SyncRequest =
|
||||||
|
| GeneralSyncRequest
|
||||||
|
| NextSyncRequest
|
||||||
|
| CustomSyncRequest;
|
||||||
|
|
||||||
|
interface GeneralParams {
|
||||||
|
title: string;
|
||||||
|
cover?: string;
|
||||||
|
tags: string[];
|
||||||
|
childArr: any[];
|
||||||
|
notionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NextParams {
|
||||||
|
title: string;
|
||||||
|
emoji: string;
|
||||||
|
cover?: string;
|
||||||
|
tags: string[];
|
||||||
|
type: string;
|
||||||
|
slug: string;
|
||||||
|
stats: string;
|
||||||
|
category: string;
|
||||||
|
summary: string;
|
||||||
|
password: string;
|
||||||
|
favicon: string;
|
||||||
|
datetime: string;
|
||||||
|
childArr: any[];
|
||||||
|
notionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomParams {
|
||||||
|
cover?: string;
|
||||||
|
customValues: Record<string, string>;
|
||||||
|
childArr: any[];
|
||||||
|
notionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Upload2Notion extends UploadBase {
|
||||||
|
|
||||||
|
constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
|
||||||
|
super(plugin, dbDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sync(request: SyncRequest): Promise<NotionPageResponse> {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
this.isAutoSync = !!request.isAutoSync;
|
||||||
|
|
||||||
|
let response: NotionPageResponse;
|
||||||
|
|
||||||
|
switch (request.dataset) {
|
||||||
|
case "general":
|
||||||
|
response = await this.handleGeneral(request);
|
||||||
|
break;
|
||||||
|
case "next":
|
||||||
|
response = await this.handleNext(request);
|
||||||
|
break;
|
||||||
|
case "custom":
|
||||||
|
response = await this.handleCustom(request);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported dataset type: ${(request as any).dataset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.response && response.response.status === 200) {
|
||||||
|
await updateYamlInfo(
|
||||||
|
request.markdown,
|
||||||
|
request.nowFile,
|
||||||
|
response.data,
|
||||||
|
request.app,
|
||||||
|
this.plugin,
|
||||||
|
this.dbDetails,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(`[Upload2Notion] Sync failed`, response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleGeneral(request: GeneralSyncRequest): Promise<NotionPageResponse> {
|
||||||
|
console.log(`[Upload2Notion] Handling general dataset`, {
|
||||||
|
cover: request.cover,
|
||||||
|
tags: request.tags,
|
||||||
|
});
|
||||||
|
const blocks = await this.buildBlocks(request.markdown, request.nowFile, {
|
||||||
|
notionLimits: {truncate: false},
|
||||||
|
});
|
||||||
|
const notionId = this.getNotionId(request.app, request.nowFile);
|
||||||
|
this.debugLog("Upload2Notion", "General dataset payload prepared", {
|
||||||
|
blockCount: blocks.length,
|
||||||
|
notionId: notionId ?? null,
|
||||||
|
tagCount: request.tags?.length ?? 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.upsertGeneral({
|
||||||
|
title: request.title,
|
||||||
|
cover: request.cover,
|
||||||
|
tags: request.tags || [],
|
||||||
|
childArr: blocks,
|
||||||
|
notionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleNext(request: NextSyncRequest): Promise<NotionPageResponse> {
|
||||||
|
console.log(`[Upload2Notion] Handling next dataset`, {
|
||||||
|
type: request.type,
|
||||||
|
slug: request.slug,
|
||||||
|
category: request.category,
|
||||||
|
});
|
||||||
|
const blocks = await this.buildBlocks(request.markdown, request.nowFile, {
|
||||||
|
notionLimits: {truncate: false},
|
||||||
|
});
|
||||||
|
this.splitRichTextParagraphs(blocks);
|
||||||
|
const notionId = this.getNotionId(request.app, request.nowFile);
|
||||||
|
this.debugLog("Upload2Notion", "Next dataset payload prepared", {
|
||||||
|
blockCount: blocks.length,
|
||||||
|
notionId: notionId ?? null,
|
||||||
|
hasEmoji: !!request.emoji,
|
||||||
|
tags: request.tags || [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.upsertNext({
|
||||||
|
title: request.title,
|
||||||
|
emoji: request.emoji,
|
||||||
|
cover: request.cover,
|
||||||
|
tags: request.tags || [],
|
||||||
|
type: request.type,
|
||||||
|
slug: request.slug,
|
||||||
|
stats: request.stats,
|
||||||
|
category: request.category,
|
||||||
|
summary: request.summary,
|
||||||
|
password: request.password,
|
||||||
|
favicon: request.favicon,
|
||||||
|
datetime: request.datetime,
|
||||||
|
childArr: blocks,
|
||||||
|
notionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleCustom(request: CustomSyncRequest): Promise<NotionPageResponse> {
|
||||||
|
console.log(`[Upload2Notion] Handling custom dataset`, {
|
||||||
|
customKeys: Object.keys(request.customValues || {}),
|
||||||
|
});
|
||||||
|
const blocks = await this.buildBlocks(request.markdown, request.nowFile, {
|
||||||
|
strictImageUrls: true,
|
||||||
|
notionLimits: {truncate: false},
|
||||||
|
});
|
||||||
|
const notionId = this.getNotionId(request.app, request.nowFile);
|
||||||
|
this.debugLog("Upload2Notion", "Custom dataset payload prepared", {
|
||||||
|
blockCount: blocks.length,
|
||||||
|
notionId: notionId ?? null,
|
||||||
|
customValueKeys: Object.keys(request.customValues || {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.upsertCustom({
|
||||||
|
cover: request.cover,
|
||||||
|
customValues: request.customValues,
|
||||||
|
childArr: blocks,
|
||||||
|
notionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildBlocks(markdown: string, nowFile: TFile, options: Record<string, unknown>): Promise<any[]> {
|
||||||
|
const yamlContent: any = yamlFrontMatter.loadFront(markdown);
|
||||||
|
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),
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNotionId(app: App, nowFile: TFile): string | undefined {
|
||||||
|
const frontMatter = app.metadataCache.getFileCache(nowFile)?.frontmatter;
|
||||||
|
if (!frontMatter) {
|
||||||
|
this.debugLog("Upload2Notion", "No frontmatter found when resolving Notion ID", {
|
||||||
|
filePath: nowFile.path,
|
||||||
|
});
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const {abName} = this.dbDetails;
|
||||||
|
const notionIDKey = `NotionID-${abName}`;
|
||||||
|
const notionId = frontMatter[notionIDKey];
|
||||||
|
return notionId ? String(notionId) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private splitRichTextParagraphs(blocks: any[]): void {
|
||||||
|
blocks.forEach((block, index) => {
|
||||||
|
if (
|
||||||
|
block.type === "paragraph" &&
|
||||||
|
block.paragraph.rich_text.length > LIMITS.RICH_TEXT_ARRAYS
|
||||||
|
) {
|
||||||
|
console.log(`[Upload2Notion] Splitting rich text paragraph`, {
|
||||||
|
index,
|
||||||
|
length: block.paragraph.rich_text.length,
|
||||||
|
});
|
||||||
|
const paragraphChunks = this.chunkArray<any>(block.paragraph.rich_text, 100);
|
||||||
|
const newParagraphBlocks = paragraphChunks.map((chunk) => paragraph(chunk));
|
||||||
|
blocks.splice(index, 1, ...newParagraphBlocks);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private chunkArray<T>(items: T[], size: number): T[][] {
|
||||||
|
const result: T[][] = [];
|
||||||
|
for (let i = 0; i < items.length; i += size) {
|
||||||
|
result.push(items.slice(i, i + size));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async upsertGeneral(params: GeneralParams): Promise<NotionPageResponse> {
|
||||||
|
const {firstChunk, extraChunks} = this.prepareBlocks(params.childArr);
|
||||||
|
const cover = params.notionId
|
||||||
|
? await this.resolveCoverForUpdate(params.cover)
|
||||||
|
: params.cover;
|
||||||
|
this.debugLog("Upload2Notion", "Upserting general page", {
|
||||||
|
title: params.title,
|
||||||
|
blockCount: params.childArr.length,
|
||||||
|
firstChunkSize: firstChunk.length,
|
||||||
|
extraChunkCount: extraChunks.length,
|
||||||
|
cover: cover ?? null,
|
||||||
|
notionIdExisting: params.notionId ?? null,
|
||||||
|
tagList: params.tags,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (params.notionId) {
|
||||||
|
console.log(`[Upload2Notion] Deleting existing Notion page`, {
|
||||||
|
notionId: params.notionId,
|
||||||
|
});
|
||||||
|
await this.deletePage(params.notionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = this.buildGeneralBody({
|
||||||
|
title: params.title,
|
||||||
|
tags: params.tags,
|
||||||
|
firstChunk,
|
||||||
|
});
|
||||||
|
this.applyCover(body, cover);
|
||||||
|
console.log(body);
|
||||||
|
return this.submitPage(body, extraChunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async upsertNext(params: NextParams): Promise<NotionPageResponse> {
|
||||||
|
const {firstChunk, extraChunks} = this.prepareBlocks(params.childArr);
|
||||||
|
const cover = params.notionId
|
||||||
|
? await this.resolveCoverForUpdate(params.cover)
|
||||||
|
: params.cover;
|
||||||
|
this.debugLog("Upload2Notion", "Upserting NotionNext page", {
|
||||||
|
title: params.title,
|
||||||
|
type: params.type,
|
||||||
|
slug: params.slug,
|
||||||
|
blockCount: params.childArr.length,
|
||||||
|
firstChunkSize: firstChunk.length,
|
||||||
|
extraChunkCount: extraChunks.length,
|
||||||
|
cover: cover ?? null,
|
||||||
|
hasEmoji: !!params.emoji,
|
||||||
|
notionIdExisting: params.notionId ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (params.notionId) {
|
||||||
|
await this.deletePage(params.notionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = this.buildNextBody({
|
||||||
|
firstChunk,
|
||||||
|
title: params.title,
|
||||||
|
emoji: params.emoji,
|
||||||
|
tags: params.tags,
|
||||||
|
type: params.type,
|
||||||
|
slug: params.slug,
|
||||||
|
stats: params.stats,
|
||||||
|
category: params.category,
|
||||||
|
summary: params.summary,
|
||||||
|
password: params.password,
|
||||||
|
favicon: params.favicon,
|
||||||
|
datetime: params.datetime,
|
||||||
|
});
|
||||||
|
this.applyCover(body, cover);
|
||||||
|
console.log(body);
|
||||||
|
return this.submitPage(body, extraChunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async upsertCustom(params: CustomParams): Promise<NotionPageResponse> {
|
||||||
|
const {firstChunk, extraChunks} = this.prepareBlocks(params.childArr);
|
||||||
|
const cover = params.notionId
|
||||||
|
? await this.resolveCoverForUpdate(params.cover)
|
||||||
|
: params.cover;
|
||||||
|
this.debugLog("Upload2Notion", "Upserting custom page", {
|
||||||
|
blockCount: params.childArr.length,
|
||||||
|
firstChunkSize: firstChunk.length,
|
||||||
|
extraChunkCount: extraChunks.length,
|
||||||
|
cover: cover ?? null,
|
||||||
|
notionIdExisting: params.notionId ?? null,
|
||||||
|
customValueKeys: Object.keys(params.customValues || {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (params.notionId) {
|
||||||
|
await this.deletePage(params.notionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = this.buildCustomBody({
|
||||||
|
customValues: params.customValues,
|
||||||
|
firstChunk,
|
||||||
|
});
|
||||||
|
this.applyCover(body, cover);
|
||||||
|
console.log(body);
|
||||||
|
return this.submitPage(body, extraChunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildGeneralBody(params: {title: string; tags: string[]; firstChunk: any[]}): any {
|
||||||
|
const {
|
||||||
|
databaseID,
|
||||||
|
customTitleButton,
|
||||||
|
customTitleName,
|
||||||
|
tagButton,
|
||||||
|
} = this.dbDetails;
|
||||||
|
|
||||||
|
return {
|
||||||
|
parent: {
|
||||||
|
database_id: databaseID,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
[customTitleButton ? customTitleName : "title"]: {
|
||||||
|
title: [
|
||||||
|
{
|
||||||
|
text: {
|
||||||
|
content: params.title,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...(tagButton
|
||||||
|
? {
|
||||||
|
tags: {
|
||||||
|
multi_select:
|
||||||
|
params.tags && params.tags.length > 0
|
||||||
|
? params.tags.map((tag) => ({name: tag}))
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
children: params.firstChunk,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildNextBody(params: {
|
||||||
|
firstChunk: any[];
|
||||||
|
title: string;
|
||||||
|
emoji: string;
|
||||||
|
tags: string[];
|
||||||
|
type: string;
|
||||||
|
slug: string;
|
||||||
|
stats: string;
|
||||||
|
category: string;
|
||||||
|
summary: string;
|
||||||
|
password: string;
|
||||||
|
favicon: string;
|
||||||
|
datetime: string;
|
||||||
|
}): any {
|
||||||
|
const {databaseID} = this.dbDetails;
|
||||||
|
|
||||||
|
const pageProperties: any = {
|
||||||
|
parent: {
|
||||||
|
database_id: databaseID,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
title: [
|
||||||
|
{
|
||||||
|
text: {
|
||||||
|
content: params.title,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
select: {
|
||||||
|
name: params.type || "Post",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
select: {
|
||||||
|
name: params.stats || "Draft",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
select: {
|
||||||
|
name: params.category || "Obsidian",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
rich_text: [
|
||||||
|
{
|
||||||
|
text: {
|
||||||
|
content: params.password || "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
rich_text: [
|
||||||
|
{
|
||||||
|
text: {
|
||||||
|
content: params.favicon || "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
date: {
|
||||||
|
start: params.datetime || new Date().toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (params.tags && params.tags.length > 0) {
|
||||||
|
pageProperties.properties.tags = {
|
||||||
|
multi_select: params.tags.map((tag) => ({name: tag})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.emoji) {
|
||||||
|
pageProperties.icon = {
|
||||||
|
emoji: params.emoji,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.slug) {
|
||||||
|
pageProperties.properties.slug = {
|
||||||
|
rich_text: [
|
||||||
|
{
|
||||||
|
text: {
|
||||||
|
content: params.slug,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.summary) {
|
||||||
|
pageProperties.properties.summary = {
|
||||||
|
rich_text: [
|
||||||
|
{
|
||||||
|
text: {
|
||||||
|
content: params.summary,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...pageProperties,
|
||||||
|
children: params.firstChunk,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildCustomBody(params: {
|
||||||
|
customValues: Record<string, string>;
|
||||||
|
firstChunk: any[];
|
||||||
|
}): any {
|
||||||
|
const {customProperties, databaseID} = this.dbDetails;
|
||||||
|
const properties: { [key: string]: any } = {};
|
||||||
|
|
||||||
|
if (customProperties) {
|
||||||
|
customProperties.forEach(({customName, customType}) => {
|
||||||
|
if (params.customValues[customName] !== undefined) {
|
||||||
|
const propertyValue = this.buildCustomProperty(customName, customType, params.customValues);
|
||||||
|
if (propertyValue !== undefined) {
|
||||||
|
properties[customName] = propertyValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parent: {
|
||||||
|
database_id: databaseID,
|
||||||
|
},
|
||||||
|
properties,
|
||||||
|
children: params.firstChunk,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildCustomProperty(customName: string, customType: string, customValues: Record<string, any>) {
|
||||||
|
const value = customValues[customName] || "";
|
||||||
|
|
||||||
|
switch (customType) {
|
||||||
|
case "title":
|
||||||
|
return {
|
||||||
|
title: [
|
||||||
|
{
|
||||||
|
text: {
|
||||||
|
content: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
case "rich_text":
|
||||||
|
return {
|
||||||
|
rich_text: [
|
||||||
|
{
|
||||||
|
text: {
|
||||||
|
content: value || "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
case "date":
|
||||||
|
return {
|
||||||
|
date: {
|
||||||
|
start: value || new Date().toISOString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case "number":
|
||||||
|
return {
|
||||||
|
number: Number(value),
|
||||||
|
};
|
||||||
|
case "phone_number":
|
||||||
|
return {
|
||||||
|
phone_number: value,
|
||||||
|
};
|
||||||
|
case "email":
|
||||||
|
return {
|
||||||
|
email: value,
|
||||||
|
};
|
||||||
|
case "url":
|
||||||
|
return {
|
||||||
|
url: value,
|
||||||
|
};
|
||||||
|
case "files":
|
||||||
|
return {
|
||||||
|
files: Array.isArray(value)
|
||||||
|
? value.map((url: string) => ({
|
||||||
|
name: url,
|
||||||
|
type: "external",
|
||||||
|
external: {
|
||||||
|
url,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
name: value,
|
||||||
|
type: "external",
|
||||||
|
external: {
|
||||||
|
url: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
case "checkbox":
|
||||||
|
return {
|
||||||
|
checkbox: Boolean(value) || false,
|
||||||
|
};
|
||||||
|
case "select":
|
||||||
|
return {
|
||||||
|
select: {
|
||||||
|
name: value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case "multi_select":
|
||||||
|
return {
|
||||||
|
multi_select: Array.isArray(value)
|
||||||
|
? value.map((item: string) => ({name: item}))
|
||||||
|
: [{name: value}],
|
||||||
|
};
|
||||||
|
case "relation":
|
||||||
|
return {
|
||||||
|
relation: Array.isArray(value)
|
||||||
|
? value.map((item: string) => ({id: item}))
|
||||||
|
: [{id: value}],
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
697
src/upload/common/AttachmentProcessor.ts
Normal file
697
src/upload/common/AttachmentProcessor.ts
Normal file
@@ -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<string, string>;
|
||||||
|
filePlaceholderToUpload: Record<string, { id: string; name: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = /(?<!!)\[([^\]]+)\]\(([^)]+)\)/g;
|
||||||
|
const linkWikilinkRegex = /(?<!!)\[\[([^\]]+)\]\]/g;
|
||||||
|
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
|
while ((match = embedImageRegex.exec(contentWithoutCode)) !== null) {
|
||||||
|
const rawPath = this.parseDestination(match[2]);
|
||||||
|
if (this.shouldSkipLinkDestination(rawPath)) continue;
|
||||||
|
const file = this.resolveFile(app, sourceFile, rawPath, { log: false });
|
||||||
|
if (file && this.isSupported(file)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
while ((match = embedWikilinkRegex.exec(contentWithoutCode)) !== null) {
|
||||||
|
const linkPath = this.parseWikilink(match[1]);
|
||||||
|
if (this.shouldSkipLinkDestination(linkPath)) continue;
|
||||||
|
const file = this.resolveFile(app, sourceFile, linkPath, { log: false });
|
||||||
|
if (file && this.isSupported(file)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
while ((match = linkMarkdownRegex.exec(contentWithoutCode)) !== null) {
|
||||||
|
const rawPath = this.parseDestination(match[2]);
|
||||||
|
if (this.shouldSkipLinkDestination(rawPath)) continue;
|
||||||
|
const file = this.resolveFile(app, sourceFile, rawPath, { log: false });
|
||||||
|
if (file && this.isSupported(file)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
while ((match = linkWikilinkRegex.exec(contentWithoutCode)) !== null) {
|
||||||
|
const linkPath = this.parseWikilink(match[1]);
|
||||||
|
if (this.shouldSkipLinkDestination(linkPath)) continue;
|
||||||
|
const file = this.resolveFile(app, sourceFile, linkPath, { log: false });
|
||||||
|
if (file && this.isSupported(file)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async processContent(content: string, sourceFile: TFile): Promise<AttachmentPrepareResult> {
|
||||||
|
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<string, { id: string; file: TFile }>();
|
||||||
|
|
||||||
|
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<string>();
|
||||||
|
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 = /(?<!!)\[([^\]]+)\]\(([^)]+)\)/g;
|
||||||
|
const linkWikilinkRegex = /(?<!!)\[\[([^\]]+)\]\]/g;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
|
||||||
|
// Process embedded images: 
|
||||||
|
let embedCount = 0;
|
||||||
|
while ((match = embedImageRegex.exec(content)) !== null) {
|
||||||
|
embedCount++;
|
||||||
|
const rawPath = this.parseDestination(match[2]);
|
||||||
|
console.log(`[AttachmentProcessor] Found embedded image #${embedCount}: "${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 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<string, { id: string; file: TFile }>
|
||||||
|
): AttachmentPrepareResult {
|
||||||
|
const imageUrlToUploadId: Record<string, string> = {};
|
||||||
|
const filePlaceholderToUpload: Record<string, { id: string; name: string }> = {};
|
||||||
|
const app = this.plugin.app;
|
||||||
|
|
||||||
|
let rewritten = content;
|
||||||
|
|
||||||
|
// Rewrite embedded images: 
|
||||||
|
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 = ``;
|
||||||
|
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 = ``;
|
||||||
|
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(
|
||||||
|
/(?<!!)\[([^\]]+)\]\(([^)]+)\)/g,
|
||||||
|
(fullMatch, _linkText, rawDest) => {
|
||||||
|
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(
|
||||||
|
/(?<!!)\[\[([^\]]+)\]\]/g,
|
||||||
|
(fullMatch, inner) => {
|
||||||
|
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: <path>
|
||||||
|
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<AttachmentPrepareResult, "imageUrlToUploadId" | "filePlaceholderToUpload">
|
||||||
|
): void {
|
||||||
|
transformBlocksInPlace(blocks, rewrites);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformBlocksInPlace(
|
||||||
|
blocks: any[],
|
||||||
|
rewrites: Pick<AttachmentPrepareResult, "imageUrlToUploadId" | "filePlaceholderToUpload">
|
||||||
|
): 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 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
256
src/upload/common/AttachmentUploader.ts
Normal file
256
src/upload/common/AttachmentUploader.ts
Normal file
@@ -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<UploadResult> {
|
||||||
|
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<FileUploadSession> {
|
||||||
|
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<void> {
|
||||||
|
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<any> {
|
||||||
|
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<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getContentType(extension: string): string {
|
||||||
|
const ext = extension.toLowerCase();
|
||||||
|
const mimeTypes: Record<string, string> = {
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
260
src/upload/common/UploadBase.ts
Normal file
260
src/upload/common/UploadBase.ts
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
import { Notice, requestUrl } from "obsidian";
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PreparedBlocks {
|
||||||
|
firstChunk: any[];
|
||||||
|
extraChunks: any[][];
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class UploadBase {
|
||||||
|
protected plugin: MyPlugin;
|
||||||
|
protected dbDetails: DatabaseDetails;
|
||||||
|
protected isAutoSync = false;
|
||||||
|
|
||||||
|
protected constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.dbDetails = dbDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldShowSuccessNotices(): boolean {
|
||||||
|
if (!this.isAutoSync) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !!this.plugin.settings.autoSyncSuccessNotice;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deletePage(notionID: string) {
|
||||||
|
const {notionAPI} = this.dbDetails;
|
||||||
|
return requestUrl({
|
||||||
|
url: `https://api.notion.com/v1/blocks/${notionID}`,
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + notionAPI,
|
||||||
|
"Notion-Version": NOTION_API_VERSION,
|
||||||
|
},
|
||||||
|
body: "",
|
||||||
|
throw: false,
|
||||||
|
}).catch((error) =>
|
||||||
|
this.handleRequestError(error, `Deleting Notion page ${notionID}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected prepareBlocks(childArr: any[]): PreparedBlocks {
|
||||||
|
this.stripCodeAnnotations(childArr);
|
||||||
|
|
||||||
|
const childArrLength = childArr.length;
|
||||||
|
|
||||||
|
if (childArrLength <= 100) {
|
||||||
|
this.debugLog("UploadBase", "Blocks fit into a single request", {
|
||||||
|
totalBlocks: childArrLength,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
firstChunk: childArr,
|
||||||
|
extraChunks: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const extraChunks: any[][] = [];
|
||||||
|
for (let i = 100; i < childArr.length; i += 100) {
|
||||||
|
extraChunks.push(childArr.slice(i, i + 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.debugLog("UploadBase", "Blocks split into multiple chunks", {
|
||||||
|
totalBlocks: childArrLength,
|
||||||
|
firstChunkSize: 100,
|
||||||
|
extraChunkCount: extraChunks.length,
|
||||||
|
lastChunkSize: extraChunks[extraChunks.length - 1].length,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
firstChunk: childArr.slice(0, 100),
|
||||||
|
extraChunks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected applyCover(body: any, cover?: string) {
|
||||||
|
if (cover) {
|
||||||
|
body.cover = {
|
||||||
|
type: "external",
|
||||||
|
external: {
|
||||||
|
url: cover,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (!body.cover && this.plugin.settings.bannerUrl) {
|
||||||
|
body.cover = {
|
||||||
|
type: "external",
|
||||||
|
external: {
|
||||||
|
url: this.plugin.settings.bannerUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.debugLog("UploadBase", "Cover applied to payload", {
|
||||||
|
chosenCover: body.cover?.external?.url ?? null,
|
||||||
|
defaultBannerUsed: !cover && !!this.plugin.settings.bannerUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async resolveCoverForUpdate(cover?: string): Promise<string | undefined> {
|
||||||
|
if (cover) {
|
||||||
|
this.debugLog("UploadBase", "Existing cover retained for update", {
|
||||||
|
cover,
|
||||||
|
});
|
||||||
|
return cover;
|
||||||
|
}
|
||||||
|
const databaseCover = await this.fetchDatabaseCover();
|
||||||
|
return databaseCover ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async submitPage(body: any, extraChunks: any[][]): Promise<NotionPageResponse> {
|
||||||
|
const {notionAPI} = this.dbDetails;
|
||||||
|
const startedAt = Date.now();
|
||||||
|
|
||||||
|
const response = await requestUrl({
|
||||||
|
url: `https://api.notion.com/v1/pages`,
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + notionAPI,
|
||||||
|
"Notion-Version": NOTION_API_VERSION,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
throw: false,
|
||||||
|
}).catch((error) =>
|
||||||
|
this.handleRequestError(error, "Creating Notion page"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json;
|
||||||
|
this.debugLog("UploadBase", "Page creation response received", {
|
||||||
|
status: response.status,
|
||||||
|
durationMs: Date.now() - startedAt,
|
||||||
|
notionUrl: data?.url ?? null,
|
||||||
|
pageId: data?.id ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
new Notice(`Error ${data.status}: ${data.code} \n ${i18nConfig["CheckConsole"]}`, 5000);
|
||||||
|
console.log(`Error message: \n ${data.message}`);
|
||||||
|
} else {
|
||||||
|
if (extraChunks.length > 0) {
|
||||||
|
await this.appendExtraBlocks(data.id, extraChunks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {response, data};
|
||||||
|
}
|
||||||
|
|
||||||
|
private stripCodeAnnotations(childArr: any[]) {
|
||||||
|
childArr.forEach((block: any) => {
|
||||||
|
if (block.type === "code") {
|
||||||
|
block.code.rich_text.forEach((item: any) => {
|
||||||
|
if (item.type === "text" && item.annotations) {
|
||||||
|
delete item.annotations;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async appendExtraBlocks(pageId: string, extraChunks: any[][]) {
|
||||||
|
const {notionAPI} = this.dbDetails;
|
||||||
|
|
||||||
|
for (let i = 0; i < extraChunks.length; i++) {
|
||||||
|
const chunk = extraChunks[i];
|
||||||
|
const extraBlocks = {
|
||||||
|
children: chunk,
|
||||||
|
};
|
||||||
|
|
||||||
|
const extraResponse = await requestUrl({
|
||||||
|
url: `https://api.notion.com/v1/blocks/${pageId}/children`,
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + notionAPI,
|
||||||
|
"Notion-Version": NOTION_API_VERSION,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(extraBlocks),
|
||||||
|
throw: false,
|
||||||
|
}).catch((error) =>
|
||||||
|
this.handleRequestError(error, `Appending blocks to page ${pageId}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
const extraData: any = await extraResponse.json;
|
||||||
|
|
||||||
|
if (extraResponse.status !== 200) {
|
||||||
|
new Notice(`Error ${extraData.status}: ${extraData.code} \n ${i18nConfig["CheckConsole"]}`, 5000);
|
||||||
|
console.log(`Error message: \n ${extraData.message}`);
|
||||||
|
} else {
|
||||||
|
console.log(`${i18nConfig["ExtraBlockUploaded"]} to page: ${pageId}`);
|
||||||
|
if (i === extraChunks.length - 1) {
|
||||||
|
console.log(`${i18nConfig["BlockUploaded"]} to page: ${pageId}`);
|
||||||
|
if (this.shouldShowSuccessNotices()) {
|
||||||
|
new Notice(`${i18nConfig["BlockUploaded"]} page: ${pageId}`, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchDatabaseCover(): Promise<string | null> {
|
||||||
|
const {notionAPI, databaseID} = this.dbDetails;
|
||||||
|
const response = await requestUrl({
|
||||||
|
url: `https://api.notion.com/v1/databases/${databaseID}`,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + notionAPI,
|
||||||
|
"Notion-Version": NOTION_API_VERSION,
|
||||||
|
},
|
||||||
|
throw: false,
|
||||||
|
}).catch((error) =>
|
||||||
|
this.handleRequestError(error, `Fetching database ${databaseID}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.json.cover && response.json.cover.external) {
|
||||||
|
return response.json.cover.external.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected debugLog(context: string, message: string, payload?: Record<string, unknown>): void {
|
||||||
|
if (payload) {
|
||||||
|
console.log(`[${context}] ${message}`, payload);
|
||||||
|
} else {
|
||||||
|
console.log(`[${context}] ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected maskValue(value?: string, visibleChars = 4): string | undefined {
|
||||||
|
if (!value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value.length <= visibleChars * 2) {
|
||||||
|
return `${value.slice(0, visibleChars)}***`;
|
||||||
|
}
|
||||||
|
return `${value.slice(0, visibleChars)}***${value.slice(-visibleChars)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRequestError(error: unknown, context: string): never {
|
||||||
|
const message = error instanceof Error && error.message
|
||||||
|
? error.message
|
||||||
|
: String(error);
|
||||||
|
console.error(`[UploadBase] ${context} failed`, {
|
||||||
|
message,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
throw new Error(
|
||||||
|
`${context} failed: ${message}. Please check your network connection or proxy settings.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/upload/common/getMarkdownCustom.ts
Normal file
62
src/upload/common/getMarkdownCustom.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import {App, Notice} from "obsidian";
|
||||||
|
import {i18nConfig} from "../../lang/I18n";
|
||||||
|
import {DatabaseDetails} from "../../ui/settingTabs";
|
||||||
|
|
||||||
|
export async function getNowFileMarkdownContentCustom(
|
||||||
|
app: App,
|
||||||
|
dbDetails: DatabaseDetails,
|
||||||
|
) {
|
||||||
|
const nowFile = app.workspace.getActiveFile();
|
||||||
|
if (!nowFile) {
|
||||||
|
new Notice(i18nConfig["open-file"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cover = '';
|
||||||
|
let customValues: Record<string, any> = {};
|
||||||
|
|
||||||
|
const FileCache = app.metadataCache.getFileCache(nowFile);
|
||||||
|
try {
|
||||||
|
cover = FileCache.frontmatter.coverurl;
|
||||||
|
|
||||||
|
// Get custom property names from dbDetails excluding the title type property
|
||||||
|
const customPropertyNames = dbDetails.customProperties
|
||||||
|
.filter(property => property.customType !== 'title') // Exclude 'title' type property
|
||||||
|
.map(property => property.customName);
|
||||||
|
|
||||||
|
// Extract custom values from the front matter based on the names
|
||||||
|
// Only collect data 'Relation' should be handled separately in the function buildBodyString
|
||||||
|
customPropertyNames.forEach(propertyName => {
|
||||||
|
if (FileCache.frontmatter && FileCache.frontmatter[propertyName] !== undefined) {
|
||||||
|
customValues[propertyName] = FileCache.frontmatter[propertyName];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if any of the customProperties has a customType of 'title'
|
||||||
|
const titleProperty = dbDetails.customProperties
|
||||||
|
.find(property => property.customType === 'title');
|
||||||
|
|
||||||
|
// If a 'title' type property exists, use the file's basename as its value
|
||||||
|
if (titleProperty) {
|
||||||
|
customValues[titleProperty.customName] =
|
||||||
|
(FileCache.frontmatter && FileCache.frontmatter[titleProperty.customName]) ?
|
||||||
|
FileCache.frontmatter[titleProperty.customName] : // use the front matter value if it exists
|
||||||
|
nowFile.basename; // Use 'basename' for the file name without extension
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
new Notice(i18nConfig["set-tags-fail"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nowFile) {
|
||||||
|
const markDownData = await nowFile.vault.read(nowFile);
|
||||||
|
return {
|
||||||
|
markDownData,
|
||||||
|
nowFile,
|
||||||
|
cover,
|
||||||
|
customValues,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
new Notice(i18nConfig["open-file"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/upload/common/getMarkdownGeneral.ts
Normal file
33
src/upload/common/getMarkdownGeneral.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { App, Notice } from "obsidian";
|
||||||
|
import { i18nConfig } from "../../lang/I18n";
|
||||||
|
import { PluginSettings } from "../../ui/settingTabs";
|
||||||
|
|
||||||
|
export async function getNowFileMarkdownContentGeneral(
|
||||||
|
app: App,
|
||||||
|
settings: PluginSettings,
|
||||||
|
) {
|
||||||
|
const nowFile = app.workspace.getActiveFile();
|
||||||
|
let cover = '';
|
||||||
|
let tags = [];
|
||||||
|
|
||||||
|
const FileCache = app.metadataCache.getFileCache(nowFile);
|
||||||
|
try {
|
||||||
|
cover = FileCache.frontmatter.coverurl;
|
||||||
|
tags = FileCache.frontmatter.tags;
|
||||||
|
} catch (error) {
|
||||||
|
new Notice(i18nConfig["set-tags-fail"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nowFile) {
|
||||||
|
const markDownData = await nowFile.vault.read(nowFile);
|
||||||
|
return {
|
||||||
|
markDownData,
|
||||||
|
nowFile,
|
||||||
|
cover,
|
||||||
|
tags,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
new Notice(i18nConfig["open-file"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ export async function getNowFileMarkdownContentNext(
|
|||||||
let type = '';
|
let type = '';
|
||||||
let slug = '';
|
let slug = '';
|
||||||
let stats = '';
|
let stats = '';
|
||||||
|
let status = '';
|
||||||
let category = '';
|
let category = '';
|
||||||
let summary = '';
|
let summary = '';
|
||||||
let paword = '';
|
let paword = '';
|
||||||
@@ -26,7 +27,7 @@ export async function getNowFileMarkdownContentNext(
|
|||||||
tags = FileCache.frontmatter.tags;
|
tags = FileCache.frontmatter.tags;
|
||||||
type = FileCache.frontmatter.type;
|
type = FileCache.frontmatter.type;
|
||||||
slug = FileCache.frontmatter.slug;
|
slug = FileCache.frontmatter.slug;
|
||||||
stats = FileCache.frontmatter.stats;
|
stats = FileCache.frontmatter.stats || FileCache.frontmatter.status;
|
||||||
category = FileCache.frontmatter.category;
|
category = FileCache.frontmatter.category;
|
||||||
summary = FileCache.frontmatter.summary;
|
summary = FileCache.frontmatter.summary;
|
||||||
paword = FileCache.frontmatter.password;
|
paword = FileCache.frontmatter.password;
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { App, Notice, TFile } from "obsidian";
|
import { App, Notice, TFile } from "obsidian";
|
||||||
import ObsidianSyncNotionPlugin from "../main";
|
import ObsidianSyncNotionPlugin from "../main";
|
||||||
import { PluginSettings } from "../ui/settingTabs";
|
import { DatabaseDetails } from "../ui/settingTabs";
|
||||||
|
import { i18nConfig } from "src/lang/I18n";
|
||||||
|
import { ensureAutoSyncDatabaseEntry } from "src/utils/frontmatter";
|
||||||
|
|
||||||
export async function updateYamlInfo(
|
export async function updateYamlInfo(
|
||||||
yamlContent: string,
|
yamlContent: string,
|
||||||
@@ -8,10 +10,15 @@ export async function updateYamlInfo(
|
|||||||
res: any,
|
res: any,
|
||||||
app: App,
|
app: App,
|
||||||
plugin: ObsidianSyncNotionPlugin,
|
plugin: ObsidianSyncNotionPlugin,
|
||||||
|
dbDetails: DatabaseDetails,
|
||||||
) {
|
) {
|
||||||
let { url, id } = res.json
|
let { url, id } = res;
|
||||||
// replace www to notionID
|
// replace www to notionID
|
||||||
const { notionUser } = plugin.settings;
|
const { notionUser, NotionLinkDisplay } = plugin.settings;
|
||||||
|
const { abName } = dbDetails
|
||||||
|
const notionIDKey = `NotionID-${abName}`;
|
||||||
|
const linkKey = `link-${abName}`;
|
||||||
|
const autoSyncKey = plugin.getAutoSyncFrontmatterKey();
|
||||||
|
|
||||||
if (notionUser !== "") {
|
if (notionUser !== "") {
|
||||||
// replace url str "www" to notionID
|
// replace url str "www" to notionID
|
||||||
@@ -19,33 +26,30 @@ export async function updateYamlInfo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await app.fileManager.processFrontMatter(nowFile, yamlContent => {
|
await app.fileManager.processFrontMatter(nowFile, yamlContent => {
|
||||||
if (yamlContent['notionID']) {
|
if (yamlContent[notionIDKey]) {
|
||||||
delete yamlContent['notionID']
|
delete yamlContent[notionIDKey]
|
||||||
}
|
}
|
||||||
if (yamlContent['link']) {
|
if (yamlContent[linkKey]) {
|
||||||
delete yamlContent['link']
|
delete yamlContent[linkKey]
|
||||||
}
|
}
|
||||||
// add new notionID and link
|
// add new notionID and link
|
||||||
yamlContent.notionID = id;
|
yamlContent[notionIDKey] = id;
|
||||||
yamlContent.link = url;
|
(NotionLinkDisplay) ? yamlContent[linkKey] = url : null;
|
||||||
|
|
||||||
|
// ensure auto sync database list contains current short name
|
||||||
|
yamlContent[autoSyncKey] = ensureAutoSyncDatabaseEntry(
|
||||||
|
yamlContent[autoSyncKey],
|
||||||
|
abName
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// copy url to clipboard only if autoCopyNotionLink is enabled
|
||||||
|
if (plugin.settings.autoCopyNotionLink) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(url)
|
await navigator.clipboard.writeText(url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
new Notice(`复制链接失败,请手动复制${error}`)
|
console.log(error);
|
||||||
|
new Notice(`${i18nConfig.CopyErrorMessage}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// const __content = yamlContent.__content;
|
|
||||||
// delete yamlContent.__content
|
|
||||||
// const yamlhead = yaml.stringify(yamlContent)
|
|
||||||
// // if yamlhead hava last \n remove it
|
|
||||||
// const yamlhead_remove_n = yamlhead.replace(/\n$/, '')
|
|
||||||
// // if __content have start \n remove it
|
|
||||||
// const __content_remove_n = __content.replace(/^\n/, '')
|
|
||||||
// const content = '---\n' +yamlhead_remove_n +'\n---\n' + __content_remove_n;
|
|
||||||
// try {
|
|
||||||
// await nowFile.vault.modify(nowFile, content)
|
|
||||||
// } catch (error) {
|
|
||||||
// new Notice(`write file error ${error}`)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +1,177 @@
|
|||||||
import {i18nConfig} from "../lang/I18n";
|
import {i18nConfig} from "../lang/I18n";
|
||||||
import {App, Notice} from "obsidian";
|
import {App, Notice} from "obsidian";
|
||||||
import { Upload2NotionNext } from "./upload_next/Upload2NotionNext";
|
import {Upload2Notion} from "./Upload2Notion";
|
||||||
import { Upload2NotionGeneral } from "./upload_general/Upload2NotionGeneral";
|
import type {NotionPageResponse} from "./common/UploadBase";
|
||||||
import { PluginSettings } from "../ui/settingTabs";
|
import {DatabaseDetails, PluginSettings} from "../ui/settingTabs";
|
||||||
import ObsidianSyncNotionPlugin from "../main";
|
import ObsidianSyncNotionPlugin from "../main";
|
||||||
import { getNowFileMarkdownContentNext } from "./upload_next/getMarkdownNext";
|
import {getNowFileMarkdownContentNext} from "./common/getMarkdownNext";
|
||||||
import { getNowFileMarkdownContentGeneral } from "./upload_general/getMarkdownGeneral";
|
import {getNowFileMarkdownContentGeneral} from "./common/getMarkdownGeneral";
|
||||||
|
import {getNowFileMarkdownContentCustom} from "./common/getMarkdownCustom";
|
||||||
|
|
||||||
|
const SYNC_ERROR_NOTICE_DURATION = 8000;
|
||||||
|
|
||||||
|
interface UploadCommandOptions {
|
||||||
|
isAutoSync?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractErrorMessage(error: unknown): string {
|
||||||
|
if (error instanceof Error && error.message) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
return String(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShowAutoSyncSuccessNotice(
|
||||||
|
plugin: ObsidianSyncNotionPlugin,
|
||||||
|
options?: UploadCommandOptions,
|
||||||
|
): boolean {
|
||||||
|
if (!options?.isAutoSync) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !!plugin.settings.autoSyncSuccessNotice;
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifySyncError(prefix: string, basename: string, error: unknown): void {
|
||||||
|
const errorMessage = extractErrorMessage(error);
|
||||||
|
console.error(`${prefix} Sync failed`, error);
|
||||||
|
new Notice(
|
||||||
|
`${i18nConfig["sync-fail"]} ${basename}. ${errorMessage}\n${i18nConfig["CheckConsole"]}`,
|
||||||
|
SYNC_ERROR_NOTICE_DURATION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logCommandDebug(context: string, message: string, payload?: Record<string, unknown>): void {
|
||||||
|
if (payload) {
|
||||||
|
console.log(`[${context}] ${message}`, payload);
|
||||||
|
} else {
|
||||||
|
console.log(`[${context}] ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function uploadCommandNext(
|
export async function uploadCommandNext(
|
||||||
plugin: ObsidianSyncNotionPlugin,
|
plugin: ObsidianSyncNotionPlugin,
|
||||||
settings: PluginSettings,
|
settings: PluginSettings,
|
||||||
|
dbDetails: DatabaseDetails,
|
||||||
app: App,
|
app: App,
|
||||||
|
options?: UploadCommandOptions,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
const { notionAPINext, databaseIDNext } = settings;
|
const {notionAPI, databaseID} = dbDetails;
|
||||||
|
console.log(`[uploadCommandNext] ${new Date().toISOString()} Triggered for file`, app.workspace.getActiveFile()?.path);
|
||||||
// Check if NNon exists
|
|
||||||
// if (NNon === undefined) {
|
|
||||||
// const NNonmessage = i18nConfig.NNonMissing;
|
|
||||||
// new Notice(NNonmessage);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Check if the user has set up the Notion API and database ID
|
// Check if the user has set up the Notion API and database ID
|
||||||
if (notionAPINext === "" || databaseIDNext === "") {
|
if (notionAPI === "" || databaseID === "") {
|
||||||
const setAPIMessage = i18nConfig["set-api-id"];
|
const setAPIMessage = i18nConfig["set-api-id"];
|
||||||
new Notice(setAPIMessage);
|
new Notice(setAPIMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { markDownData, nowFile, emoji, cover, tags, type, slug, stats, category, summary, paword, favicon, datetime } = await getNowFileMarkdownContentNext(app, settings)
|
const {
|
||||||
|
markDownData,
|
||||||
|
nowFile,
|
||||||
|
emoji,
|
||||||
|
cover,
|
||||||
|
tags,
|
||||||
|
type,
|
||||||
|
slug,
|
||||||
|
stats,
|
||||||
|
category,
|
||||||
|
summary,
|
||||||
|
paword,
|
||||||
|
favicon,
|
||||||
|
datetime
|
||||||
|
} = await getNowFileMarkdownContentNext(app, settings);
|
||||||
|
|
||||||
|
logCommandDebug("uploadCommandNext", "Metadata extracted from markdown", {
|
||||||
|
hasMarkdown: !!markDownData,
|
||||||
|
markdownLength: markDownData?.length ?? 0,
|
||||||
|
tagCount: tags?.length ?? 0,
|
||||||
|
coverIncluded: !!cover,
|
||||||
|
hasEmoji: !!emoji,
|
||||||
|
type,
|
||||||
|
slug,
|
||||||
|
hasPassword: !!paword,
|
||||||
|
datetime,
|
||||||
|
});
|
||||||
|
|
||||||
if (markDownData) {
|
if (markDownData) {
|
||||||
const {basename} = nowFile;
|
const {basename} = nowFile;
|
||||||
const upload = new Upload2NotionNext(plugin);
|
logCommandDebug("uploadCommandNext", "Preparing to upload", {
|
||||||
const res = await upload.syncMarkdownToNotionNext(basename, emoji, cover, tags, type, slug, stats, category, summary, paword, favicon, datetime, markDownData, nowFile, this.app);
|
filename: basename,
|
||||||
|
filePath: nowFile.path,
|
||||||
|
});
|
||||||
|
|
||||||
if (res.status === 200) {
|
const upload = new Upload2Notion(plugin, dbDetails);
|
||||||
new Notice(`${i18nConfig["sync-success"]}${basename}`);
|
let res: NotionPageResponse;
|
||||||
|
try {
|
||||||
|
res = await upload.sync({
|
||||||
|
dataset: "next",
|
||||||
|
isAutoSync: options?.isAutoSync,
|
||||||
|
title: basename,
|
||||||
|
emoji: emoji || "",
|
||||||
|
cover: cover || "",
|
||||||
|
tags: tags || [],
|
||||||
|
type: type || "",
|
||||||
|
slug: slug || "",
|
||||||
|
stats: stats || "",
|
||||||
|
category: category || "",
|
||||||
|
summary: summary || "",
|
||||||
|
password: paword || "",
|
||||||
|
favicon: favicon || "",
|
||||||
|
datetime: datetime || "",
|
||||||
|
markdown: markDownData,
|
||||||
|
nowFile,
|
||||||
|
app,
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
notifySyncError("[uploadCommandNext]", basename, error);
|
||||||
|
logCommandDebug("uploadCommandNext", "Sync threw error", {
|
||||||
|
filename: basename,
|
||||||
|
error: extractErrorMessage(error),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {response} = res;
|
||||||
|
if (response.status === 200) {
|
||||||
|
if (shouldShowAutoSyncSuccessNotice(plugin, options)) {
|
||||||
|
new Notice(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`).noticeEl.style.color = "green";
|
||||||
|
}
|
||||||
|
|
||||||
|
logCommandDebug("uploadCommandNext", "Sync succeeded", {
|
||||||
|
filename: basename,
|
||||||
|
status: response.status,
|
||||||
|
pageId: res.data?.id ?? null,
|
||||||
|
pageUrl: res.data?.url ?? null,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
new Notice(`${i18nConfig["sync-fail"]} ${basename}`, 5000);
|
new Notice(`${i18nConfig["sync-fail"]} ${basename}`, 5000);
|
||||||
|
console.log(`${i18nConfig["sync-fail"]} ${basename}`);
|
||||||
|
logCommandDebug("uploadCommandNext", "Sync failed with status", {
|
||||||
|
filename: basename,
|
||||||
|
status: response.status,
|
||||||
|
errorCode: res.data?.code ?? null,
|
||||||
|
errorStatus: res.data?.status ?? null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function uploadCommandGeneral(
|
export async function uploadCommandGeneral(
|
||||||
plugin: ObsidianSyncNotionPlugin,
|
plugin: ObsidianSyncNotionPlugin,
|
||||||
settings: PluginSettings,
|
settings: PluginSettings,
|
||||||
|
dbDetails: DatabaseDetails,
|
||||||
app: App,
|
app: App,
|
||||||
|
options?: UploadCommandOptions,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
const { notionAPIGeneral, databaseIDGeneral } = settings;
|
const {notionAPI, databaseID} = dbDetails;
|
||||||
|
console.log(`[uploadCommandGeneral] ${new Date().toISOString()} Triggered for file`, app.workspace.getActiveFile()?.path);
|
||||||
|
|
||||||
// Check if the user has set up the Notion API and database ID
|
// Check if the user has set up the Notion API and database ID
|
||||||
if (notionAPIGeneral === "" || databaseIDGeneral === "") {
|
if (notionAPI === "" || databaseID === "") {
|
||||||
const setAPIMessage = i18nConfig["set-api-id"];
|
const setAPIMessage = i18nConfig["set-api-id"];
|
||||||
new Notice(setAPIMessage);
|
new Notice(setAPIMessage);
|
||||||
return;
|
return;
|
||||||
@@ -64,16 +179,146 @@ export async function uploadCommandGeneral(
|
|||||||
|
|
||||||
const {markDownData, nowFile, cover, tags} = await getNowFileMarkdownContentGeneral(app, settings)
|
const {markDownData, nowFile, cover, tags} = await getNowFileMarkdownContentGeneral(app, settings)
|
||||||
|
|
||||||
|
if (!options?.isAutoSync) {
|
||||||
|
new Notice(i18nConfig.StartUpload.replace('{filename}', nowFile.basename));
|
||||||
|
}
|
||||||
|
console.log(`Start upload ${nowFile.basename}`);
|
||||||
|
|
||||||
if (markDownData) {
|
if (markDownData) {
|
||||||
const {basename} = nowFile;
|
const {basename} = nowFile;
|
||||||
|
logCommandDebug("uploadCommandGeneral", "Preparing to upload", {
|
||||||
|
filename: basename,
|
||||||
|
markdownLength: markDownData.length,
|
||||||
|
tagCount: tags?.length ?? 0,
|
||||||
|
hasCover: !!cover,
|
||||||
|
});
|
||||||
|
|
||||||
const upload = new Upload2NotionGeneral(plugin);
|
const upload = new Upload2Notion(plugin, dbDetails);
|
||||||
const res = await upload.syncMarkdownToNotionGeneral(basename, cover, tags, markDownData, nowFile, this.app);
|
let res: NotionPageResponse;
|
||||||
|
try {
|
||||||
|
res = await upload.sync({
|
||||||
|
dataset: "general",
|
||||||
|
isAutoSync: options?.isAutoSync,
|
||||||
|
title: basename,
|
||||||
|
cover: cover || "",
|
||||||
|
tags: tags || [],
|
||||||
|
markdown: markDownData,
|
||||||
|
nowFile,
|
||||||
|
app,
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
notifySyncError("[uploadCommandGeneral]", basename, error);
|
||||||
|
logCommandDebug("uploadCommandGeneral", "Sync threw error", {
|
||||||
|
filename: basename,
|
||||||
|
error: extractErrorMessage(error),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (res.status === 200) {
|
const {response} = res;
|
||||||
new Notice(`${i18nConfig["sync-success"]}${basename}`);
|
if (response.status === 200) {
|
||||||
|
if (shouldShowAutoSyncSuccessNotice(plugin, options)) {
|
||||||
|
new Notice(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`).noticeEl.style.color = "green";
|
||||||
|
}
|
||||||
|
|
||||||
|
logCommandDebug("uploadCommandGeneral", "Sync succeeded", {
|
||||||
|
filename: basename,
|
||||||
|
status: response.status,
|
||||||
|
pageId: res.data?.id ?? null,
|
||||||
|
pageUrl: res.data?.url ?? null,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
new Notice(`${i18nConfig["sync-fail"]} ${basename}`, 5000);
|
new Notice(`${i18nConfig["sync-fail"]} ${basename}`, 5000);
|
||||||
|
console.log(`${i18nConfig["sync-fail"]} ${basename}`);
|
||||||
|
logCommandDebug("uploadCommandGeneral", "Sync failed with status", {
|
||||||
|
filename: basename,
|
||||||
|
status: response.status,
|
||||||
|
errorCode: res.data?.code ?? null,
|
||||||
|
errorStatus: res.data?.status ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function uploadCommandCustom(
|
||||||
|
plugin: ObsidianSyncNotionPlugin,
|
||||||
|
settings: PluginSettings,
|
||||||
|
dbDetails: DatabaseDetails,
|
||||||
|
app: App,
|
||||||
|
options?: UploadCommandOptions,
|
||||||
|
) {
|
||||||
|
|
||||||
|
const {notionAPI, databaseID} = dbDetails;
|
||||||
|
console.log(`[uploadCommandCustom] ${new Date().toISOString()} Triggered for file`, app.workspace.getActiveFile()?.path);
|
||||||
|
|
||||||
|
// Check if the user has set up the Notion API and database ID
|
||||||
|
if (notionAPI === "" || databaseID === "") {
|
||||||
|
const setAPIMessage = i18nConfig["set-api-id"];
|
||||||
|
new Notice(setAPIMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {markDownData, nowFile, cover, customValues} = await getNowFileMarkdownContentCustom(app, dbDetails)
|
||||||
|
|
||||||
|
if (!options?.isAutoSync) {
|
||||||
|
new Notice(i18nConfig.StartUpload.replace('{filename}', nowFile.basename));
|
||||||
|
}
|
||||||
|
console.log(`Start upload ${nowFile.basename}`);
|
||||||
|
|
||||||
|
if (markDownData) {
|
||||||
|
const {basename} = nowFile;
|
||||||
|
logCommandDebug("uploadCommandCustom", "Preparing to upload", {
|
||||||
|
filename: basename,
|
||||||
|
markdownLength: markDownData.length,
|
||||||
|
hasCover: !!cover,
|
||||||
|
customValueKeys: Object.keys(customValues || {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = new Upload2Notion(plugin, dbDetails);
|
||||||
|
let res: NotionPageResponse;
|
||||||
|
try {
|
||||||
|
res = await upload.sync({
|
||||||
|
dataset: "custom",
|
||||||
|
isAutoSync: options?.isAutoSync,
|
||||||
|
cover: cover || "",
|
||||||
|
customValues,
|
||||||
|
markdown: markDownData,
|
||||||
|
nowFile,
|
||||||
|
app,
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
notifySyncError("[uploadCommandCustom]", basename, error);
|
||||||
|
logCommandDebug("uploadCommandCustom", "Sync threw error", {
|
||||||
|
filename: basename,
|
||||||
|
error: extractErrorMessage(error),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {response} = res;
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
if (shouldShowAutoSyncSuccessNotice(plugin, options)) {
|
||||||
|
new Notice(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`).noticeEl.style.color = "green";
|
||||||
|
}
|
||||||
|
|
||||||
|
logCommandDebug("uploadCommandCustom", "Sync succeeded", {
|
||||||
|
filename: basename,
|
||||||
|
status: response.status,
|
||||||
|
pageId: res.data?.id ?? null,
|
||||||
|
pageUrl: res.data?.url ?? null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
new Notice(`${i18nConfig["sync-fail"]} ${basename}`, 5000);
|
||||||
|
console.log(`${i18nConfig["sync-fail"]} ${basename}`);
|
||||||
|
logCommandDebug("uploadCommandCustom", "Sync failed with status", {
|
||||||
|
filename: basename,
|
||||||
|
status: response.status,
|
||||||
|
errorCode: res.data?.code ?? null,
|
||||||
|
errorStatus: res.data?.status ?? null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
import {App, Notice, requestUrl, TFile} from "obsidian";
|
|
||||||
import {Client} from '@notionhq/client';
|
|
||||||
import {markdownToBlocks,} from "@tryfabric/martian";
|
|
||||||
import * as yamlFrontMatter from "yaml-front-matter";
|
|
||||||
// import * as yaml from "yaml"
|
|
||||||
import MyPlugin from "src/main";
|
|
||||||
|
|
||||||
export class UploadBaseGeneral {
|
|
||||||
plugin: MyPlugin;
|
|
||||||
notion: Client;
|
|
||||||
agent: any;
|
|
||||||
|
|
||||||
constructor(plugin: MyPlugin) {
|
|
||||||
this.plugin = plugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deletePage(notionID: string) {
|
|
||||||
return requestUrl({
|
|
||||||
url: `https://api.notion.com/v1/blocks/${notionID}`,
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'Bearer ' + this.plugin.settings.notionAPINext,
|
|
||||||
'Notion-Version': '2022-06-28',
|
|
||||||
},
|
|
||||||
body: ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDataBase(databaseID: string) {
|
|
||||||
const response = await requestUrl({
|
|
||||||
url: `https://api.notion.com/v1/databases/${databaseID}`,
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Bearer ' + this.plugin.settings.notionAPINext,
|
|
||||||
'Notion-Version': '2022-06-28',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check if cover is present in the JSON response and then get the URL
|
|
||||||
if (response.json.cover && response.json.cover.external) {
|
|
||||||
return response.json.cover.external.url;
|
|
||||||
} else {
|
|
||||||
return null; // or some other default value, if you prefer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// async updateYamlInfo(yamlContent: string, nowFile: TFile, res: any, app: App, settings: any) {
|
|
||||||
// let {url, id} = res.json
|
|
||||||
// // replace www to notionID
|
|
||||||
// const {notionUser} = this.plugin.settings;
|
|
||||||
//
|
|
||||||
// if (notionUser !== "") {
|
|
||||||
// // replace url str "www" to notionID
|
|
||||||
// url = url.replace("www.notion.so", `${notionUser}.notion.site`)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// await app.fileManager.processFrontMatter(nowFile, yamlContent => {
|
|
||||||
// if (yamlContent['notionID']) {
|
|
||||||
// delete yamlContent['notionID']
|
|
||||||
// }
|
|
||||||
// if (yamlContent['link']) {
|
|
||||||
// delete yamlContent['link']
|
|
||||||
// }
|
|
||||||
// // add new notionID and link
|
|
||||||
// yamlContent.notionID = id;
|
|
||||||
// yamlContent.link = url;
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// try {
|
|
||||||
// await navigator.clipboard.writeText(url)
|
|
||||||
// } catch (error) {
|
|
||||||
// new Notice(`复制链接失败,请手动复制${error}`)
|
|
||||||
// }
|
|
||||||
// // const __content = yamlContent.__content;
|
|
||||||
// // delete yamlContent.__content
|
|
||||||
// // const yamlhead = yaml.stringify(yamlContent)
|
|
||||||
// // // if yamlhead hava last \n remove it
|
|
||||||
// // const yamlhead_remove_n = yamlhead.replace(/\n$/, '')
|
|
||||||
// // // if __content have start \n remove it
|
|
||||||
// // const __content_remove_n = __content.replace(/^\n/, '')
|
|
||||||
// // const content = '---\n' +yamlhead_remove_n +'\n---\n' + __content_remove_n;
|
|
||||||
// // try {
|
|
||||||
// // await nowFile.vault.modify(nowFile, content)
|
|
||||||
// // } catch (error) {
|
|
||||||
// // new Notice(`write file error ${error}`)
|
|
||||||
// // }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
import { App, Notice, requestUrl, TFile } from "obsidian";
|
|
||||||
import { Client } from '@notionhq/client';
|
|
||||||
import { markdownToBlocks, } from "@tryfabric/martian";
|
|
||||||
import * as yamlFrontMatter from "yaml-front-matter";
|
|
||||||
// import * as yaml from "yaml"
|
|
||||||
import MyPlugin from "src/main";
|
|
||||||
import { PluginSettings } from "../../ui/settingTabs";
|
|
||||||
import { UploadBaseGeneral } from "./BaseUpload2NotionGeneral";
|
|
||||||
import { updateYamlInfo } from "../updateYaml";
|
|
||||||
|
|
||||||
export class Upload2NotionGeneral extends UploadBaseGeneral {
|
|
||||||
settings: PluginSettings;
|
|
||||||
|
|
||||||
constructor(plugin: MyPlugin) {
|
|
||||||
super(plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 因为需要解析notion的block进行对比,非常的麻烦,
|
|
||||||
// 暂时就直接删除,新建一个page
|
|
||||||
async updatePage(
|
|
||||||
notionID: string,
|
|
||||||
title: string,
|
|
||||||
cover: string,
|
|
||||||
tags: string[],
|
|
||||||
childArr: any,
|
|
||||||
) {
|
|
||||||
await this.deletePage(notionID)
|
|
||||||
|
|
||||||
const databasecover = await this.getDataBase(this.plugin.settings.databaseIDGeneral)
|
|
||||||
|
|
||||||
if (cover == null) {
|
|
||||||
cover = databasecover
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.createPage(
|
|
||||||
title,
|
|
||||||
cover,
|
|
||||||
tags,
|
|
||||||
childArr)
|
|
||||||
}
|
|
||||||
|
|
||||||
async createPage(
|
|
||||||
title: string,
|
|
||||||
cover: string,
|
|
||||||
tags: string[],
|
|
||||||
childArr: any,
|
|
||||||
) {
|
|
||||||
const bodyString: any = {
|
|
||||||
parent: {
|
|
||||||
database_id: this.plugin.settings.databaseIDGeneral,
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
[this.plugin.settings.CustomTitleButton ? this.plugin.settings.CustomTitleName : 'title']: {
|
|
||||||
title: [
|
|
||||||
{
|
|
||||||
text: {
|
|
||||||
content: title
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
tags: {
|
|
||||||
multi_select: tags && true ? tags.map(tag => {
|
|
||||||
return { "name": tag }
|
|
||||||
}) : [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
children: childArr,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cover) {
|
|
||||||
bodyString.cover = {
|
|
||||||
type: "external",
|
|
||||||
external: {
|
|
||||||
url: cover
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bodyString.cover && this.plugin.settings.bannerUrl) {
|
|
||||||
bodyString.cover = {
|
|
||||||
type: "external",
|
|
||||||
external: {
|
|
||||||
url: this.plugin.settings.bannerUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await requestUrl({
|
|
||||||
url: `https://api.notion.com/v1/pages`,
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
// 'User-Agent': 'obsidian.md',
|
|
||||||
'Authorization': 'Bearer ' + this.plugin.settings.notionAPIGeneral,
|
|
||||||
'Notion-Version': '2022-06-28',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(bodyString),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
new Notice(`network error ${error}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncMarkdownToNotionGeneral(
|
|
||||||
title: string,
|
|
||||||
cover: string,
|
|
||||||
tags: string[],
|
|
||||||
markdown: string,
|
|
||||||
nowFile: TFile,
|
|
||||||
app: App,
|
|
||||||
): Promise<any> {
|
|
||||||
let res: any
|
|
||||||
const yamlContent: any = yamlFrontMatter.loadFront(markdown);
|
|
||||||
const __content = yamlContent.__content
|
|
||||||
const file2Block = markdownToBlocks(__content);
|
|
||||||
const frontmasster = app.metadataCache.getFileCache(nowFile)?.frontmatter
|
|
||||||
const notionID = frontmasster ? frontmasster.notionID : null
|
|
||||||
|
|
||||||
if (notionID) {
|
|
||||||
res = await this.updatePage(
|
|
||||||
notionID,
|
|
||||||
title,
|
|
||||||
cover,
|
|
||||||
tags,
|
|
||||||
file2Block
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
res = await this.createPage(
|
|
||||||
title,
|
|
||||||
cover,
|
|
||||||
tags,
|
|
||||||
file2Block
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (res.status === 200) {
|
|
||||||
await updateYamlInfo(markdown, nowFile, res, app, this.plugin)
|
|
||||||
} else {
|
|
||||||
new Notice(`${res.text}`)
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import {App, Notice} from "obsidian";
|
|
||||||
import {i18nConfig} from "../../lang/I18n";
|
|
||||||
import {PluginSettings} from "../../ui/settingTabs";
|
|
||||||
|
|
||||||
export async function getNowFileMarkdownContentGeneral(
|
|
||||||
app: App,
|
|
||||||
settings: PluginSettings,
|
|
||||||
) {
|
|
||||||
const nowFile = app.workspace.getActiveFile();
|
|
||||||
let cover = '';
|
|
||||||
let tags = [];
|
|
||||||
|
|
||||||
const FileCache = app.metadataCache.getFileCache(nowFile);
|
|
||||||
try {
|
|
||||||
cover = FileCache.frontmatter.coverurl;
|
|
||||||
tags = FileCache.frontmatter.tags;
|
|
||||||
} catch (error) {
|
|
||||||
new Notice(i18nConfig["set-tags-fail"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nowFile) {
|
|
||||||
const markDownData = await nowFile.vault.read(nowFile);
|
|
||||||
return {
|
|
||||||
markDownData,
|
|
||||||
nowFile,
|
|
||||||
cover,
|
|
||||||
tags,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
new Notice(i18nConfig["open-file"]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import {App, Notice, requestUrl, TFile} from "obsidian";
|
|
||||||
import {Client} from '@notionhq/client';
|
|
||||||
import {markdownToBlocks,} from "@tryfabric/martian";
|
|
||||||
import * as yamlFrontMatter from "yaml-front-matter";
|
|
||||||
// import * as yaml from "yaml"
|
|
||||||
import MyPlugin from "src/main";
|
|
||||||
|
|
||||||
export class UploadBaseNext {
|
|
||||||
plugin: MyPlugin;
|
|
||||||
notion: Client;
|
|
||||||
agent: any;
|
|
||||||
|
|
||||||
constructor(plugin: MyPlugin) {
|
|
||||||
this.plugin = plugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deletePage(notionID: string) {
|
|
||||||
return requestUrl({
|
|
||||||
url: `https://api.notion.com/v1/blocks/${notionID}`,
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'Bearer ' + this.plugin.settings.notionAPINext,
|
|
||||||
'Notion-Version': '2022-06-28',
|
|
||||||
},
|
|
||||||
body: ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDataBase(databaseID: string) {
|
|
||||||
const response = await requestUrl({
|
|
||||||
url: `https://api.notion.com/v1/databases/${databaseID}`,
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Bearer ' + this.plugin.settings.notionAPINext,
|
|
||||||
'Notion-Version': '2022-06-28',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check if cover is present in the JSON response and then get the URL
|
|
||||||
if (response.json.cover && response.json.cover.external) {
|
|
||||||
return response.json.cover.external.url;
|
|
||||||
} else {
|
|
||||||
return null; // or some other default value, if you prefer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// async updateYamlInfo(yamlContent: string, nowFile: TFile, res: any, app: App, settings: any) {
|
|
||||||
// let {url, id} = res.json
|
|
||||||
// // replace www to notionID
|
|
||||||
// const { notionUser} = this.plugin.settings;
|
|
||||||
//
|
|
||||||
// if (notionUser !== "") {
|
|
||||||
// // replace url str "www" to notionID
|
|
||||||
// url = url.replace("www.notion.so", `${notionUser}.notion.site`)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// await app.fileManager.processFrontMatter(nowFile, yamlContent => {
|
|
||||||
// if (yamlContent['notionID']) {
|
|
||||||
// delete yamlContent['notionID']
|
|
||||||
// }
|
|
||||||
// if (yamlContent['link']) {
|
|
||||||
// delete yamlContent['link']
|
|
||||||
// }
|
|
||||||
// // add new notionID and link
|
|
||||||
// yamlContent.notionID = id;
|
|
||||||
// yamlContent.link = url;
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// try {
|
|
||||||
// await navigator.clipboard.writeText(url)
|
|
||||||
// } catch (error) {
|
|
||||||
// new Notice(`复制链接失败,请手动复制${error}`)
|
|
||||||
// }
|
|
||||||
// // const __content = yamlContent.__content;
|
|
||||||
// // delete yamlContent.__content
|
|
||||||
// // const yamlhead = yaml.stringify(yamlContent)
|
|
||||||
// // // if yamlhead hava last \n remove it
|
|
||||||
// // const yamlhead_remove_n = yamlhead.replace(/\n$/, '')
|
|
||||||
// // // if __content have start \n remove it
|
|
||||||
// // const __content_remove_n = __content.replace(/^\n/, '')
|
|
||||||
// // const content = '---\n' +yamlhead_remove_n +'\n---\n' + __content_remove_n;
|
|
||||||
// // try {
|
|
||||||
// // await nowFile.vault.modify(nowFile, content)
|
|
||||||
// // } catch (error) {
|
|
||||||
// // new Notice(`write file error ${error}`)
|
|
||||||
// // }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
import { UploadBaseNext } from "./BaseUpload2NotionNext";
|
|
||||||
import { App, Notice, requestUrl, TFile } from "obsidian";
|
|
||||||
import { Client } from '@notionhq/client';
|
|
||||||
import { markdownToBlocks, } from "@tryfabric/martian";
|
|
||||||
import * as yamlFrontMatter from "yaml-front-matter";
|
|
||||||
// import * as yaml from "yaml"
|
|
||||||
import MyPlugin from "src/main";
|
|
||||||
import {PluginSettings} from "../../ui/settingTabs";
|
|
||||||
import {updateYamlInfo} from "../updateYaml";
|
|
||||||
|
|
||||||
export class Upload2NotionNext extends UploadBaseNext {
|
|
||||||
settings: PluginSettings;
|
|
||||||
|
|
||||||
constructor(plugin: MyPlugin) {
|
|
||||||
super(plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 因为需要解析notion的block进行对比,非常的麻烦,
|
|
||||||
// 暂时就直接删除,新建一个page
|
|
||||||
async updatePage(
|
|
||||||
notionID: string,
|
|
||||||
title: string,
|
|
||||||
emoji: string,
|
|
||||||
cover: string,
|
|
||||||
tags: string[],
|
|
||||||
type: string,
|
|
||||||
slug: string,
|
|
||||||
stats: string,
|
|
||||||
category: string,
|
|
||||||
summary: string,
|
|
||||||
paword: string,
|
|
||||||
favicon: string,
|
|
||||||
datetime: string,
|
|
||||||
childArr: any
|
|
||||||
) {
|
|
||||||
await this.deletePage(notionID)
|
|
||||||
|
|
||||||
const databasecover = await this.getDataBase(this.plugin.settings.databaseIDNext)
|
|
||||||
|
|
||||||
if (cover == null) {
|
|
||||||
cover = databasecover
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.createPage(
|
|
||||||
title,
|
|
||||||
emoji,
|
|
||||||
cover,
|
|
||||||
tags,
|
|
||||||
type,
|
|
||||||
slug,
|
|
||||||
stats,
|
|
||||||
category,
|
|
||||||
summary,
|
|
||||||
paword,
|
|
||||||
favicon,
|
|
||||||
datetime,
|
|
||||||
childArr)
|
|
||||||
}
|
|
||||||
|
|
||||||
async createPage(
|
|
||||||
title: string,
|
|
||||||
emoji: string,
|
|
||||||
cover: string,
|
|
||||||
tags: string[],
|
|
||||||
type: string,
|
|
||||||
slug: string,
|
|
||||||
stats: string,
|
|
||||||
category: string,
|
|
||||||
summary: string,
|
|
||||||
pawrod: string,
|
|
||||||
favicon: string,
|
|
||||||
datetime: string,
|
|
||||||
childArr: any
|
|
||||||
) {
|
|
||||||
const bodyString: any = {
|
|
||||||
parent: {
|
|
||||||
database_id: this.plugin.settings.databaseIDNext
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
emoji: emoji || '📜'
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
title: {
|
|
||||||
title: [
|
|
||||||
{
|
|
||||||
text: {
|
|
||||||
content: title
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
tags: {
|
|
||||||
multi_select: tags && true ? tags.map(tag => {
|
|
||||||
return { "name": tag }
|
|
||||||
}) : [],
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
select: {
|
|
||||||
name: type || 'Post'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
slug: {
|
|
||||||
rich_text: [
|
|
||||||
{
|
|
||||||
text: {
|
|
||||||
content: slug || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
select: {
|
|
||||||
name: stats || 'Draft'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
category: {
|
|
||||||
select: {
|
|
||||||
name: category || 'Obsidian'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
rich_text: [
|
|
||||||
{
|
|
||||||
text: {
|
|
||||||
content: summary || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
rich_text: [
|
|
||||||
{
|
|
||||||
text: {
|
|
||||||
content: pawrod || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
rich_text: [
|
|
||||||
{
|
|
||||||
text: {
|
|
||||||
content: favicon || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
date: {
|
|
||||||
date: {
|
|
||||||
start: datetime || new Date().toISOString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
children: childArr,
|
|
||||||
}
|
|
||||||
if (cover) {
|
|
||||||
bodyString.cover = {
|
|
||||||
type: "external",
|
|
||||||
external: {
|
|
||||||
url: cover
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bodyString.cover && this.plugin.settings.bannerUrl) {
|
|
||||||
bodyString.cover = {
|
|
||||||
type: "external",
|
|
||||||
external: {
|
|
||||||
url: this.plugin.settings.bannerUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await requestUrl({
|
|
||||||
url: `https://api.notion.com/v1/pages`,
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
// 'User-Agent': 'obsidian.md',
|
|
||||||
'Authorization': 'Bearer ' + this.plugin.settings.notionAPINext,
|
|
||||||
'Notion-Version': '2022-06-28',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(bodyString),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
new Notice(`network error ${error}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncMarkdownToNotionNext(
|
|
||||||
title: string,
|
|
||||||
emoji: string,
|
|
||||||
cover: string,
|
|
||||||
tags: string[],
|
|
||||||
type: string,
|
|
||||||
slug: string,
|
|
||||||
stats: string,
|
|
||||||
category: string,
|
|
||||||
summary: string,
|
|
||||||
paword: string,
|
|
||||||
favicon: string,
|
|
||||||
datetime: string,
|
|
||||||
markdown: string,
|
|
||||||
nowFile: TFile,
|
|
||||||
app: App,
|
|
||||||
): Promise<any> {
|
|
||||||
let res: any
|
|
||||||
const yamlContent: any = yamlFrontMatter.loadFront(markdown);
|
|
||||||
const __content = yamlContent.__content
|
|
||||||
const file2Block = markdownToBlocks(__content);
|
|
||||||
const frontmasster = app.metadataCache.getFileCache(nowFile)?.frontmatter
|
|
||||||
const notionID = frontmasster ? frontmasster.notionID : null
|
|
||||||
|
|
||||||
if (notionID) {
|
|
||||||
res = await this.updatePage(
|
|
||||||
notionID,
|
|
||||||
title,
|
|
||||||
emoji,
|
|
||||||
cover,
|
|
||||||
tags,
|
|
||||||
type,
|
|
||||||
slug,
|
|
||||||
stats,
|
|
||||||
category,
|
|
||||||
summary,
|
|
||||||
paword,
|
|
||||||
favicon,
|
|
||||||
datetime,
|
|
||||||
file2Block
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
res = await this.createPage(
|
|
||||||
title,
|
|
||||||
emoji,
|
|
||||||
cover,
|
|
||||||
tags,
|
|
||||||
type,
|
|
||||||
slug,
|
|
||||||
stats,
|
|
||||||
category,
|
|
||||||
summary,
|
|
||||||
paword,
|
|
||||||
favicon,
|
|
||||||
datetime,
|
|
||||||
file2Block
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (res.status === 200) {
|
|
||||||
await updateYamlInfo(markdown, nowFile, res, app, this.plugin)
|
|
||||||
} else {
|
|
||||||
new Notice(`${res.text}`)
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
63
src/utils/frontmatter.ts
Normal file
63
src/utils/frontmatter.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
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)) {
|
||||||
|
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<string, string>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"inlineSourceMap": true,
|
"inlineSourceMap": true,
|
||||||
"inlineSources": true,
|
"inlineSources": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
@@ -17,8 +19,8 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"**/*.ts"
|
"**/*.ts",
|
||||||
,
|
"src/Upload2Notion.ts",
|
||||||
"src/Upload2Notion.ts"
|
"src/*.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
// update package.json version
|
// update package.json version
|
||||||
import fs from 'fs';
|
import fs from "fs";
|
||||||
|
|
||||||
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
const packageJson = JSON.parse(fs.readFileSync("./package.json", "utf8"));
|
||||||
|
|
||||||
console.log(packageJson.version);
|
console.log(packageJson.version);
|
||||||
|
|
||||||
|
|
||||||
// get node args
|
// get node args
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
@@ -13,14 +12,13 @@ packageJson.version = args[0];
|
|||||||
|
|
||||||
console.log(packageJson.version);
|
console.log(packageJson.version);
|
||||||
|
|
||||||
|
|
||||||
// write package.json
|
// write package.json
|
||||||
fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 2));
|
fs.writeFileSync("./package.json", JSON.stringify(packageJson, null, 2));
|
||||||
|
|
||||||
// read manifest.json
|
// read manifest.json
|
||||||
const manifestJson = JSON.parse(fs.readFileSync('./manifest.json', 'utf8'));
|
const manifestJson = JSON.parse(fs.readFileSync("./manifest.json", "utf8"));
|
||||||
|
|
||||||
manifestJson.version = args[0];
|
manifestJson.version = args[0];
|
||||||
|
|
||||||
// write manifest.json
|
// write manifest.json
|
||||||
fs.writeFileSync('./manifest.json', JSON.stringify(manifestJson, null, 2));
|
fs.writeFileSync("./manifest.json", JSON.stringify(manifestJson, null, 2));
|
||||||
|
|||||||
@@ -12,3 +12,40 @@ writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
|||||||
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
||||||
versions[targetVersion] = minAppVersion;
|
versions[targetVersion] = minAppVersion;
|
||||||
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
||||||
|
|
||||||
|
// tag the changelog by converting "Unreleased" to the new version section
|
||||||
|
function bumpChangelog(version) {
|
||||||
|
const changelogPath = "CHANGELOG.md";
|
||||||
|
const content = readFileSync(changelogPath, "utf8");
|
||||||
|
|
||||||
|
const unreleasedHeader = "## Unreleased";
|
||||||
|
if (!content.includes(unreleasedHeader)) {
|
||||||
|
console.warn(`[version-bump] ${unreleasedHeader} not found in ${changelogPath}, skipping changelog tagging`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
|
const releaseHeader = `## v${version} (${today})`;
|
||||||
|
const updated = content.replace(unreleasedHeader, releaseHeader);
|
||||||
|
|
||||||
|
const marker = "# Changelog\n\n";
|
||||||
|
if (!updated.startsWith(marker)) {
|
||||||
|
writeFileSync(changelogPath, updated);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUnreleased = [
|
||||||
|
"## Unreleased",
|
||||||
|
"",
|
||||||
|
"### Added",
|
||||||
|
"",
|
||||||
|
"### Changed",
|
||||||
|
"",
|
||||||
|
"### Fixed",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
writeFileSync(changelogPath, marker + newUnreleased + updated.slice(marker.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
bumpChangelog(targetVersion);
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
"1.0.0": "0.9.7",
|
"1.0.0": "0.9.7",
|
||||||
"1.0.1": "0.12.0"
|
"1.0.1": "0.12.0",
|
||||||
|
"2.8.0": "0.0.1",
|
||||||
|
"2.8.1": "0.0.1",
|
||||||
|
"2.8.2": "0.0.1",
|
||||||
|
"2.8.3": "0.0.1"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user