Compare commits

...

39 Commits
2.3.5 ... 2.7.0

Author SHA1 Message Date
Jiaxin Peng
db66ef137c Update version to 2.7.0 2025-10-27 23:21:30 +00:00
Jiaxin Peng
a152201510 Merge pull request #64 from jxpeng98/callout-support
feat: callout support
2025-10-27 23:20:27 +00:00
Jiaxin Peng
abb699fe4d docs: add support for converting Obsidian callouts to Notion callout blocks 2025-10-27 23:17:55 +00:00
Jiaxin Peng
cf6c80df0f update yarn.lock 2025-10-27 23:14:43 +00:00
Jiaxin Peng
cf884de554 refactor: remove redundant debug logs from upload commands and base class 2025-10-27 23:12:25 +00:00
Jiaxin Peng
8eb4694342 refactor: removing redundant debug logs 2025-10-27 22:57:28 +00:00
Jiaxin Peng
6c0f6cea30 feat: enhance logging and error handling in upload commands and base class 2025-10-27 22:34:46 +00:00
Jiaxin Peng
8af4816f87 chore: update martian dependency version to 1.3.1 2025-10-27 20:44:23 +00:00
Jiaxin Peng
f6d24daa52 chore: bump martian dependency version to 1.3.0 2025-10-27 13:40:42 +00:00
Jiaxin Peng
da72846903 Update version to 2.6.0 2025-10-06 23:25:29 +01:00
Jiaxin Peng
8e08c4325d Update version to 2.6.0 2025-10-06 23:22:00 +01:00
Jiaxin Peng
ebb7a290e8 Merge pull request #61 from jxpeng98/unify-upload-functions
unify three databases with single upload file
2025-10-06 23:19:55 +01:00
Jiaxin Peng
24e68fc798 unify three databases with single upload file 2025-10-06 23:18:01 +01:00
Jiaxin Peng
78d2a29c8a Update README.md img links 2025-07-20 12:19:10 +01:00
Jiaxin Peng
1156bd3809 Merge pull request #58 from jxpeng98/notion-relation
update img links
2025-07-20 12:17:56 +01:00
Jiaxin Peng
b46e24f1ed update img links 2025-07-20 12:16:10 +01:00
Jiaxin Peng
a950e2e301 Update version to 2.5.0 2024-11-20 22:19:38 +00:00
Jiaxin Peng
aac7bc154a feat: consistent sync function (requestUrl) for desktop and mobile 2024-11-20 22:19:03 +00:00
Jiaxin Peng
a573ef728d chore: remove node-fetch and change the platform back to browser. 2024-11-20 22:17:49 +00:00
Jiaxin Peng
1344c14933 Update version to 2.4.3 2024-10-22 10:29:09 +01:00
Jiaxin Peng
c1f4ecd14b Update version to 2.4.3 2024-10-22 10:27:49 +01:00
Jiaxin Peng
5942876c92 fix: cannot parse title from front matter 2024-10-22 10:27:33 +01:00
Jiaxin Peng
3218eb9fba Update version to 2.4.2 2024-09-19 13:50:51 +01:00
Jiaxin Peng
48566eca20 Merge pull request #37 from jxpeng98/pluggin-does-not-appear-on-mac
fix: obsidian language not set in English, Chinese and Japanese
2024-09-19 13:49:08 +01:00
Jiaxin Peng
8bd8346423 fix: obsidian language not set in English, Chinese and Japanese 2024-09-19 13:47:57 +01:00
Jiaxin Peng
00411e9599 Merge pull request #35 from jxpeng98/mobile-fail-to-start
feat: separate mobile sync from desktop version
2024-09-03 12:19:23 +01:00
Jiaxin Peng
296125f6b4 chore: remove process from dependencies 2024-09-03 12:15:36 +01:00
Jiaxin Peng
1b385ccd0a solve: the fail to load of mobile caused by the node version 2024-09-01 23:26:22 +01:00
Jiaxin Peng
a90e4871bb Backup mobile requesturl after version 2.4.1 2024-09-01 22:57:03 +01:00
Jiaxin Peng
885ccba027 fix: test the desktop function for next and general 2024-08-31 23:43:19 +01:00
Jiaxin Peng
f282dde1b7 fix: test the desktop function of custom database
with platform function
2024-08-31 00:00:09 +01:00
Jiaxin Peng
bc166d8db1 Update version to 2.4.1 2024-08-28 20:31:28 +01:00
Jiaxin Peng
7612637951 Merge pull request #34 from jxpeng98/code-highlight-issue
fix code highlight issue
2024-08-28 20:30:49 +01:00
Jiaxin Peng
e8b1617916 fix item type error for iteration 2024-08-28 20:30:05 +01:00
Jiaxin Peng
fd29ed820e fix code highlight issue 2024-08-28 20:26:06 +01:00
Jiaxin Peng
1f58670906 Update version to 2.4.0 2024-08-19 11:24:23 +01:00
Jiaxin Peng
fc47d36e8d Merge pull request #32 from jxpeng98/long-notes-support
Long notes support
2024-08-18 22:39:00 +01:00
Jiaxin Peng
cd4e610c46 modify type of pageProperties 2024-08-18 18:42:59 +01:00
Jiaxin Peng
b05c248c44 feat: support long notes syncing 2024-08-18 17:16:08 +01:00
25 changed files with 1557 additions and 1720 deletions

7
.gitignore vendored
View File

@@ -21,3 +21,10 @@ 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
doc/.vitepress/dist
doc/.vitepress/cache

View File

@@ -1,4 +1,6 @@
## Improvements ## Feature
- update the Markdown parser to ensure that no extra `\\` is added to the end of the line when having equations in the Markdown file - Better debugging information for upload failures.
- 输入公式的时候,不会在公式后面加上多余的`\\`符号 - Support synchronising Obsidian callouts as Notion callout blocks.
- 优化控制台输出的调试信息。
- 支持将 Obsidian Callout 同步为 Notion Callout 区块。

View File

@@ -44,21 +44,21 @@ tags:
- 这个版本开始,你可以**任意修改第一列表头的名字 (标题列,默认:'title')**。 **注意你必须保证你的Notion数据库中有'tags这一列的存在并且在markdown文件中把`tags:`添加到你的YAML表头中。否则会返回`400 错误`。但是你可以`tags:`设置为空。** - 这个版本开始,你可以**任意修改第一列表头的名字 (标题列,默认:'title')**。 **注意你必须保证你的Notion数据库中有'tags这一列的存在并且在markdown文件中把`tags:`添加到你的YAML表头中。否则会返回`400 错误`。但是你可以`tags:`设置为空。**
![](https://minioapi.pjx.ac.cn/img1/2023/11/4a298b9be3990e9d2201bf2f50ca5a0a.png) ![](https://r2img.jxpeng.dev/2023/11/4a298b9be3990e9d2201bf2f50ca5a0a.png)
例如这样: 例如这样:
![](https://minioapi.pjx.ac.cn/img1/2023/11/4cd8d79cd9dd9dde299e39c666cb3473.gif) ![](https://r2img.jxpeng.dev/2023/11/4cd8d79cd9dd9dde299e39c666cb3473.gif)
- 增加了一个控制按钮来控制是否开启当前数据库同步命令的功能。如果你关闭了这个按钮,你在上传命令列表中就不会出现当前数据库的选项。 - 增加了一个控制按钮来控制是否开启当前数据库同步命令的功能。如果你关闭了这个按钮,你在上传命令列表中就不会出现当前数据库的选项。
![](https://minioapi.pjx.ac.cn/img1/2023/11/becb60fc44783842da4b3cf4c322f363.gif) ![](https://r2img.jxpeng.dev/2023/11/becb60fc44783842da4b3cf4c322f363.gif)
### 0.2.6 ### 0.2.6
- 增加一个按钮用来控制是否在名列列表中显示如下数据库。 - 增加一个按钮用来控制是否在名列列表中显示如下数据库。
![](https://minioapi.pjx.ac.cn/img1/2023/11/147c6a4eaa34da41a6f102558ed77106.png) ![](https://r2img.jxpeng.dev/2023/11/147c6a4eaa34da41a6f102558ed77106.png)
如果你关闭普通数据库的按钮你在上传命令列表中就不会出现普通数据库的选项。如果你关闭NotionNext数据库的按钮你在上传命令列表中就不会出现NotionNext数据库的选项。 如果你关闭普通数据库的按钮你在上传命令列表中就不会出现普通数据库的选项。如果你关闭NotionNext数据库的按钮你在上传命令列表中就不会出现NotionNext数据库的选项。
![](https://minioapi.pjx.ac.cn/img1/2023/11/70b8e4fc2148688ccbd6cfc53ce339a2.png) ![](https://r2img.jxpeng.dev/2023/11/70b8e4fc2148688ccbd6cfc53ce339a2.png)
### 0.2.3 ### 0.2.3
@@ -70,9 +70,9 @@ tags:
- 你可以有一个NotionNext和一个普通Notion数据库。 - 你可以有一个NotionNext和一个普通Notion数据库。
- 普通Notion数据库的同步方式和原作者的一样只同步`title``tags`。**如果使用普通数据库功能,你需要保证你的数据库中有`title``tags`这两个表头。必须使用小写。** - 普通Notion数据库的同步方式和原作者的一样只同步`title``tags`。**如果使用普通数据库功能,你需要保证你的数据库中有`title``tags`这两个表头。必须使用小写。**
![](https://minioapi.pjx.ac.cn/img1/2023/11/712a12081d855aa60f82a7b46913ab7e.gif) ![](https://r2img.jxpeng.dev/2023/11/712a12081d855aa60f82a7b46913ab7e.gif)
![](https://minioapi.pjx.ac.cn/img1/2023/11/9de76cecceef74c78884ddfc1c221659.gif) ![](https://r2img.jxpeng.dev/2023/11/9de76cecceef74c78884ddfc1c221659.gif)
### 0.2.1 ### 0.2.1
@@ -181,10 +181,10 @@ Contents Below
``` ```
模板使用如下: 模板使用如下:
![](https://minioapi.pjx.ac.cn/img1/2023/09/354b950e5777b48832c2475e4a31f2cc.gif) ![](https://r2img.jxpeng.dev/2023/09/354b950e5777b48832c2475e4a31f2cc.gif)
**插件预览如下** **插件预览如下**
![](https://minioapi.pjx.ac.cn/img1/2023/09/d6199619b68fab218fca8ae0cebece78.gif) ![](https://r2img.jxpeng.dev/2023/09/d6199619b68fab218fca8ae0cebece78.gif)
--- ---

View File

@@ -9,27 +9,40 @@
[//]: # ([中文文档](README-zh.md)) [//]: # ([中文文档](README-zh.md))
**Now, support both NotionNext and General databases with customised properties.** **Now, support All Notion databases, including NotionNext and General Notion databases, and customise the database list.**
**现在支持NotionNext和普通Notion数据库自定义数据库列表。** **现在支持所有Notion数据库包括NotionNext和一般Notion数据库以及自定义数据库列表。**
## Precautions ## Precautions
### For customised database users ### For customised database users before version 2.3.0
**⚠️⚠️⚠️: The existing customised database should be recreated if you want to update to version 2.3.0. The new version has a new database structure, and the old database structure is not compatible with the new version to build the index properly.** **⚠️⚠️⚠️: The existing customised database should be recreated if you want to update to version 2.3.0. The new version has a new database structure, and the old database structure is not compatible with the new version to build the index properly.**
### 自定义数据库用户 ### 在2.3.0版本之前的自定义数据库用户
**⚠️⚠️⚠️: 如果你想要更新到2.3.0版本,你需要重新创建自定义数据库。新版本有一个新的数据库结构,旧的数据库结构无法构建索引。** **⚠️⚠️⚠️: 如果你想要更新到2.3.0版本,你需要重新创建自定义数据库。新版本有一个新的数据库结构,旧的数据库结构无法构建索引。**
## TODO List ## TODO List
- [x] ~~Modify the Edit function for the custom properties. 改进自定义属性的编辑功能~~ - [x] Modify the Edit function for the custom properties. 改进自定义属性的编辑功能
- [x] Support sync with long notes (From `v2.4.0`). 从`v2.4.0`开始支持长笔记同步
- [x] Support custom properties for Notion General database. 支持自定义属性
- [x] Support preview for database details in plugin settings. 支持预览数据库详情
- [x] Support edit for database details in plugin settings. 支持编辑数据库详情
- [x] Convert Obsidian callouts into Notion callout blocks so your `[!info]` style notes stay consistent after syncing.
- [ ] Support group upload with one click 支持一键多数据库上传 - [ ] Support group upload with one click 支持一键多数据库上传
- [x] ~~Support custom properties for Notion General database. 支持自定义属性~~
- [x] ~~Support preview for database details in plugin settings. 支持预览数据库详情~~ ## Callout support
- [x] ~~Support edit for database details in plugin settings. 支持编辑数据库详情~~
Obsidian callouts written with the standard syntax (for example `> [!warning]` or `> [!quote]`) are automatically rendered as Notion callout blocks during upload. The plugin keeps the chosen callout type, title, and body content so your callout styling survives the trip from Obsidian to Notion without extra configuration.
```markdown
> [!info] Tips
> Remember to update your front matter before syncing.
```
After syncing, the note will contain an equivalent Notion callout block with the same icon and text.
## How to use ## How to use
@@ -39,38 +52,38 @@ If you want to use this plugin, you need to follow the following steps to set up
1). Go to [Notion API](https://www.notion.com/my-integrations) to create a new integration, and **copy the token**. 1). Go to [Notion API](https://www.notion.com/my-integrations) to create a new integration, and **copy the token**.
![create-api](https://minioapi.pjx.ac.cn/img1/2024/07/28526e416571f8b1fe70d90bd9975b81.gif) ![create-api](https://r2img.jxpeng.dev/2024/07/28526e416571f8b1fe70d90bd9975b81.gif)
2). Create a database in your Notion workspace. 2). Create a database in your Notion workspace.
Open Notion, click top-left `Create` a new page` -> You can give a title for the page and press "/" Select 'Database - Full page'. Open Notion, click top-left `Create` a new page` -> You can give a title for the page and press "/" Select 'Database - Full page'.
![create-database](https://minioapi.pjx.ac.cn/img1/2024/07/f1bd993ed667bcc980a45194eb647c34.gif) ![create-database](https://r2img.jxpeng.dev/2024/07/f1bd993ed667bcc980a45194eb647c34.gif)
Rename the database properties to `title` and `tags`. Rename the database properties to `title` and `tags`.
**⚠️⚠️`title` is the title column! `tags` is lower case⚠** **⚠️⚠️`title` is the title column! `tags` is lower case⚠**
![rename-title-tags](https://minioapi.pjx.ac.cn/img1/2024/07/20c7b81761195706dde6e7dae69edbc4.gif) ![rename-title-tags](https://r2img.jxpeng.dev/2024/07/20c7b81761195706dde6e7dae69edbc4.gif)
3). Add the Notion API integration. 3). Add the Notion API integration.
Click the right-top '...' -> connections -> connect to -> find the integration you created and connect it. Click the right-top '...' -> connections -> connect to -> find the integration you created and connect it.
![add-integration](https://minioapi.pjx.ac.cn/img1/2024/07/bf59397146d96727c96dac06515c6c22.gif) ![add-integration](https://r2img.jxpeng.dev/2024/07/bf59397146d96727c96dac06515c6c22.gif)
4). Publish the database to the public. 4). Publish the database to the public.
Click the 'Share' -> 'Publish' -> 'Publish' -> **copy the database id from the URL**. Click the 'Share' -> 'Publish' -> 'Publish' -> **copy the database id from the URL**.
![share-publish](https://minioapi.pjx.ac.cn/img1/2024/07/5e5b9585cfa4a9086ef6704c3b48eacf.gif) ![share-publish](https://r2img.jxpeng.dev/2024/07/5e5b9585cfa4a9086ef6704c3b48eacf.gif)
### 2. Setting up the plugin in Obsidian ### 2. Setting up the plugin in Obsidian
1). Go to Obsidian settings -> community plugins -> search 'NotionNext' -> install it. 1). Go to Obsidian settings -> community plugins -> search 'NotionNext' -> install it.
![plugin](https://minioapi.pjx.ac.cn/img1/2024/07/1bf82618e943ec5eb68d89d4556bc3fc.png) ![plugin](https://r2img.jxpeng.dev/2024/07/1bf82618e943ec5eb68d89d4556bc3fc.png)
2). Open the plugin settings and create a new database. 2). Open the plugin settings and create a new database.
![add-database](https://minioapi.pjx.ac.cn/img1/2024/07/115f47b4180d04fb362b931f8092b5fb.png) ![add-database](https://r2img.jxpeng.dev/2024/07/115f47b4180d04fb362b931f8092b5fb.png)
### 3. Create a database ### 3. Create a database
@@ -80,7 +93,7 @@ Click the 'Share' -> 'Publish' -> 'Publish' -> **copy the database id from the U
General database is the default database, which only has the `title` and `tags` columns. **⚠️⚠️`title` and `tags` are lowercase⚠** General database is the default database, which only has the `title` and `tags` columns. **⚠️⚠️`title` and `tags` are lowercase⚠**
![general](https://minioapi.pjx.ac.cn/img1/2024/07/b2d0d0259afae7a542bd9f62256cd2c2.png) ![general](https://r2img.jxpeng.dev/2024/07/b2d0d0259afae7a542bd9f62256cd2c2.png)
The general is as follows: The general is as follows:
@@ -93,17 +106,17 @@ The general is as follows:
After setting, it should look like this: After setting, it should look like this:
![general-setting](https://minioapi.pjx.ac.cn/img1/2024/07/3fc630171f6e8ae1011271b2b37d5d48.png) ![general-setting](https://r2img.jxpeng.dev/2024/07/3fc630171f6e8ae1011271b2b37d5d48.png)
Once you click `save`, the new database will be created in the plugin settings and display in the detabase list. Once you click `save`, the new database will be created in the plugin settings and display in the detabase list.
![database-list](https://minioapi.pjx.ac.cn/img1/2024/07/3dff23b1c13a31f7696dc794fb78eba5.png) ![database-list](https://r2img.jxpeng.dev/2024/07/3dff23b1c13a31f7696dc794fb78eba5.png)
#### 2⃣ NotionNext database #### 2⃣ NotionNext database
If you use [NotionNext](https://github.com/tangly1024/NotionNext) to set up your website, you can use the NotionNext database. (**All the properties are in lowercase, and you do not need to change the properties**) If you use [NotionNext](https://github.com/tangly1024/NotionNext) to set up your website, you can use the NotionNext database. (**All the properties are in lowercase, and you do not need to change the properties**)
![notionnext](https://minioapi.pjx.ac.cn/img1/2024/07/b164160ccfaf4fb23aa7ac1ce467e235.png) ![notionnext](https://r2img.jxpeng.dev/2024/07/b164160ccfaf4fb23aa7ac1ce467e235.png)
Like the general database, you need to fill in the following information: Like the general database, you need to fill in the following information:
@@ -116,7 +129,7 @@ Like the general database, you need to fill in the following information:
The custom database is purely customised, and you can customise the properties you want to sync. (**All the properties are in lowercase, and you do not need to change the properties**) The custom database is purely customised, and you can customise the properties you want to sync. (**All the properties are in lowercase, and you do not need to change the properties**)
![customise](https://minioapi.pjx.ac.cn/img1/2024/07/612b1377ec94d452abec7dc3fbe6cefb.png) ![customise](https://r2img.jxpeng.dev/2024/07/612b1377ec94d452abec7dc3fbe6cefb.png)
The basic information is identical to the general database and NotionNext database. You need to fill in the following information: The basic information is identical to the general database and NotionNext database. You need to fill in the following information:
@@ -127,15 +140,15 @@ The basic information is identical to the general database and NotionNext databa
After adding the information, You can click `Add New Property` to add the properties you want to sync. There is no limit to the number of properties you can add. After adding the information, You can click `Add New Property` to add the properties you want to sync. There is no limit to the number of properties you can add.
![customise-add-property](https://minioapi.pjx.ac.cn/img1/2024/07/17cfbfa93d92404ff70d920302f15981.png) ![customise-add-property](https://r2img.jxpeng.dev/2024/07/17cfbfa93d92404ff70d920302f15981.png)
**⚠️⚠️⚠️**: **`Title column` is the main property in the Notion database, which has the open button to open the underlying page.** You can select the type of all other properties. **⚠️⚠️⚠️** **⚠️⚠️⚠️**: **`Title column` is the main property in the Notion database, which has the open button to open the underlying page.** You can select the type of all other properties. **⚠️⚠️⚠️**
![title-column](https://minioapi.pjx.ac.cn/img1/2024/07/0521b0aa6cedbe67bca4966bf79041ea.png) ![title-column](https://r2img.jxpeng.dev/2024/07/0521b0aa6cedbe67bca4966bf79041ea.png)
According to the previously shown database, you can use `title` as the `Title column`, and `Tags` as the property 1. According to the previously shown database, you can use `title` as the `Title column`, and `Tags` as the property 1.
![title-and-tags](https://minioapi.pjx.ac.cn/img1/2024/07/87529214008147d3aa1bbe5868849f31.png) ![title-and-tags](https://r2img.jxpeng.dev/2024/07/87529214008147d3aa1bbe5868849f31.png)
**Until now, you have set up the plugin in Obsidian. You can start syncing your notes to Notion.** **Until now, you have set up the plugin in Obsidian. You can start syncing your notes to Notion.**
@@ -159,7 +172,7 @@ This is a test file.
After creating the note, you can click the `Share to NotionNext` button on the left sidebar to sync the note to Notion. After creating the note, you can click the `Share to NotionNext` button on the left sidebar to sync the note to Notion.
![general-sync](https://minioapi.pjx.ac.cn/img1/2024/07/6a690dc4b04d6b74134e9d3d76636c1b.gif) ![general-sync](https://r2img.jxpeng.dev/2024/07/6a690dc4b04d6b74134e9d3d76636c1b.gif)
#### 2⃣ note for NotionNext database #### 2⃣ note for NotionNext database
@@ -187,7 +200,7 @@ NotionID-pengjiaxin: 8ba573de-8fdf-4681-b063-c39d26e7860e
this is a test file this is a test file
``` ```
![notionnext-sync](https://minioapi.pjx.ac.cn/img1/2024/07/5539ec534e7a855ad87aa7949e5e836f.gif) ![notionnext-sync](https://r2img.jxpeng.dev/2024/07/5539ec534e7a855ad87aa7949e5e836f.gif)
#### 3⃣ note for custom database #### 3⃣ note for custom database
@@ -195,7 +208,7 @@ this is a test file
Use the previously created general database and rename `title` to `Name` and `tags` to `Tags` in Notion. Use the previously created general database and rename `title` to `Name` and `tags` to `Tags` in Notion.
![rename-notion](https://minioapi.pjx.ac.cn/img1/2024/07/e4d1c988be22353537998db897c7a471.gif) ![rename-notion](https://r2img.jxpeng.dev/2024/07/e4d1c988be22353537998db897c7a471.gif)
The front matter for the custom database is as follows: The front matter for the custom database is as follows:
@@ -210,23 +223,23 @@ Tags:
This is a test file. This is a test file.
``` ```
![sync-custom](https://minioapi.pjx.ac.cn/img1/2024/07/973f17955cd921cca6b5007f0990eea4.gif) ![sync-custom](https://r2img.jxpeng.dev/2024/07/973f17955cd921cca6b5007f0990eea4.gif)
##### sync more properties ##### sync more properties
Add more property to the custom database. For example, we can add a `summary` and `date` property to the custom database. Add more property to the custom database. For example, we can add a `summary` and `date` property to the custom database.
1). Click `Edit` in the plugin settings. 1). Click `Edit` in the plugin settings.
![edit](https://minioapi.pjx.ac.cn/img1/2024/07/f50639d8118b1f106d7bb29be5d3527b.png) ![edit](https://r2img.jxpeng.dev/2024/07/f50639d8118b1f106d7bb29be5d3527b.png)
2). Click `Add` and add the `summary` and `date` property. 2). Click `Add` and add the `summary` and `date` property.
![add-property](https://minioapi.pjx.ac.cn/img1/2024/07/49716657adf0255a37fa214abf4716dc.gif) ![add-property](https://r2img.jxpeng.dev/2024/07/49716657adf0255a37fa214abf4716dc.gif)
3). After adding the properties, you can see the properties in the plugin settings. 3). After adding the properties, you can see the properties in the plugin settings.
![four properties](https://minioapi.pjx.ac.cn/img1/2024/07/63a0a0e09d537b1cf55cfdc49de4db1a.png) ![four properties](https://r2img.jxpeng.dev/2024/07/63a0a0e09d537b1cf55cfdc49de4db1a.png)
4). Add the `summary` and `date` in Notion 4). Add the `summary` and `date` in Notion
![add-in-notion](https://minioapi.pjx.ac.cn/img1/2024/07/d193fe72ce3676bf643b0d1eb92dd4b7.gif) ![add-in-notion](https://r2img.jxpeng.dev/2024/07/d193fe72ce3676bf643b0d1eb92dd4b7.gif)
5). Create a new note with the `summary` and `date` property. 5). Create a new note with the `summary` and `date` property.
@@ -245,7 +258,7 @@ Add more property to the custom database. For example, we can add a `summary` an
6). Sync the note to Notion. 6). Sync the note to Notion.
![sync-custom](https://minioapi.pjx.ac.cn/img1/2024/07/0e4cfbd61acdaf813fd81ee6d046e067.gif) ![sync-custom](https://r2img.jxpeng.dev/2024/07/0e4cfbd61acdaf813fd81ee6d046e067.gif)
--- ---

View File

@@ -41,7 +41,6 @@ const ctx = await esbuild.context({
"@codemirror/view", "@codemirror/view",
...builtins, ...builtins,
], ],
platform: "node",
format: "cjs", format: "cjs",
target: "es2016", target: "es2016",
logLevel: "info", logLevel: "info",

View File

@@ -1,7 +1,7 @@
{ {
"id": "share-to-notionnext", "id": "share-to-notionnext",
"name": "Share to NotionNext", "name": "Share to NotionNext",
"version": "2.3.5", "version": "2.7.0",
"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",

View File

@@ -1,8 +1,8 @@
{ {
"name": "share-to-notionnext", "name": "share-to-notionnext",
"version": "2.3.5", "version": "2.7.0",
"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",
@@ -10,7 +10,7 @@
"version": "node version-bump.mjs && git add manifest.json versions.json" "version": "node version-bump.mjs && git add manifest.json versions.json"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "Jiaxin PENG",
"license": "GNU GPLv3", "license": "GNU GPLv3",
"devDependencies": { "devDependencies": {
"@types/node": "^20.5.7", "@types/node": "^20.5.7",
@@ -24,10 +24,8 @@
"typescript": "5.2.2" "typescript": "5.2.2"
}, },
"dependencies": { "dependencies": {
"@jxpeng98/martian": "^1.2.7", "@jxpeng98/martian": "^1.3.1",
"https-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.2",
"node-fetch": "^3.3.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"

View File

@@ -8,10 +8,42 @@ export const I18n: {[key: string]:any} = {
ja, ja,
}; };
export const I18nConfig = (lang: string): any => { class I18nManager {
return I18n[lang]; private currentLanguage: string;
};
constructor() {
this.currentLanguage = this.detectLanguage();
}
// return the language to use
private detectLanguage(): string {
const storedLanguage = window.localStorage.getItem("language"); const storedLanguage = window.localStorage.getItem("language");
if (storedLanguage && this.isLanguageSupported(storedLanguage)) {
console.log(`Using stored language: ${storedLanguage}`);
return storedLanguage;
}
export const i18nConfig = I18n[storedLanguage || window.navigator.language.split('-')[0] || "en"]; 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();

View File

@@ -76,4 +76,8 @@ export const en = {
AddNewProperty: "Add New Property", AddNewProperty: "Add New Property",
AddNewPropertyDesc: "Add new property match with your notion database", AddNewPropertyDesc: "Add new property match with your notion database",
CopyErrorMessage: "Auto copy failed, please copy it manually", CopyErrorMessage: "Auto copy failed, please copy it manually",
BlockUploaded: "All blocks uploaded",
ExtraBlockUploaded: "Extra blocks uploaded",
CheckConsole: "Check the console for more information \n opt+cmd+i/ctrl+shift+i",
"reach-mobile-limit": "The number of blocks exceeds the limit of 100, please use the desktop plugin",
} }

View File

@@ -68,4 +68,7 @@ export const ja = {
AddNewProperty: "新しいプロパティを追加", AddNewProperty: "新しいプロパティを追加",
AddNewPropertyDesc: "新しいプロパティを追加してください", AddNewPropertyDesc: "新しいプロパティを追加してください",
CopyErrorMessage: "自動コピーに失敗しました", CopyErrorMessage: "自動コピーに失敗しました",
BlockUploaded: "ブロックがアップロードされました",
ExtraBlockUploaded: "追加ブロックがアップロードされました",
CheckConsole: "詳細情報を確認するには、コンソールを開いてください \n opt+cmd+i/ctrl+shift+i",
}; };

View File

@@ -71,4 +71,7 @@ export const zh = {
AddNewProperty: "添加新属性", AddNewProperty: "添加新属性",
AddNewPropertyDesc: "添加一个和Notion数据库匹配的新属性", AddNewPropertyDesc: "添加一个和Notion数据库匹配的新属性",
CopyErrorMessage: "复制链接失败,请手动复制", CopyErrorMessage: "复制链接失败,请手动复制",
BlockUploaded: "所有内容已成功上传",
ExtraBlockUploaded: "额外内容已成功上传",
CheckConsole: "opt+cmd+i/ctrl+shift+i\n打开控制台查看更多信息",
} }

View File

@@ -1,7 +1,5 @@
import { App, Editor, MarkdownView, Notice, Plugin, PluginSettingTab, Setting } from "obsidian"; import { App, Editor, MarkdownView, Notice, Plugin, PluginSettingTab, Setting } 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, DatabaseDetails } from "src/ui/settingTabs"; import { ObsidianSettingTab, PluginSettings, DEFAULT_SETTINGS, DatabaseDetails } from "src/ui/settingTabs";
@@ -20,7 +18,7 @@ export default class ObsidianSyncNotionPlugin extends Plugin {
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) => {
@@ -86,4 +84,3 @@ export default class ObsidianSyncNotionPlugin extends Plugin {

628
src/upload/Upload2Notion.ts Normal file
View File

@@ -0,0 +1,628 @@
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";
export type DatasetType = "general" | "next" | "custom";
interface BaseSyncRequest {
dataset: DatasetType;
markdown: string;
nowFile: TFile;
app: App;
}
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();
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 = this.buildBlocks(request.markdown, {
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 = this.buildBlocks(request.markdown, {
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 = this.buildBlocks(request.markdown, {
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 buildBlocks(markdown: string, options: Record<string, unknown>): any[] {
const yamlContent: any = yamlFrontMatter.loadFront(markdown);
const content = yamlContent.__content;
const blocks = markdownToBlocks(content, options);
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;
}
}
}

View File

@@ -0,0 +1,248 @@
import { Notice, requestUrl } from "obsidian";
import MyPlugin from "src/main";
import { DatabaseDetails } from "../../ui/settingTabs";
import { i18nConfig } from "../../lang/I18n";
export interface NotionPageResponse {
response: any;
data: any;
}
interface PreparedBlocks {
firstChunk: any[];
extraChunks: any[][];
}
export abstract class UploadBase {
protected plugin: MyPlugin;
protected dbDetails: DatabaseDetails;
protected constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
this.plugin = plugin;
this.dbDetails = dbDetails;
}
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": "2022-06-28",
},
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": "2022-06-28",
},
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": "2022-06-28",
},
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}`);
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": "2022-06-28",
},
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.`,
);
}
}

View File

@@ -24,6 +24,7 @@ export async function getNowFileMarkdownContentCustom(
.map(property => property.customName); .map(property => property.customName);
// Extract custom values from the front matter based on the names // 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 => { customPropertyNames.forEach(propertyName => {
if (FileCache.frontmatter && FileCache.frontmatter[propertyName] !== undefined) { if (FileCache.frontmatter && FileCache.frontmatter[propertyName] !== undefined) {
customValues[propertyName] = FileCache.frontmatter[propertyName]; customValues[propertyName] = FileCache.frontmatter[propertyName];
@@ -31,11 +32,15 @@ export async function getNowFileMarkdownContentCustom(
}); });
// Check if any of the customProperties has a customType of 'title' // Check if any of the customProperties has a customType of 'title'
const titleProperty = dbDetails.customProperties.find(property => property.customType === '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 a 'title' type property exists, use the file's basename as its value
if (titleProperty) { if (titleProperty) {
customValues[titleProperty.customName] = nowFile.basename; // Use 'basename' for the file name without extension 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) { } catch (error) {

View File

@@ -1,13 +1,38 @@
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 { Upload2NotionCustom } from "./upoload_custom/Upload2NotionCustom";
import {DatabaseDetails, 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 "./upoload_custom/getMarkdownCustom"; import {getNowFileMarkdownContentCustom} from "./common/getMarkdownCustom";
const SYNC_ERROR_NOTICE_DURATION = 8000;
function extractErrorMessage(error: unknown): string {
if (error instanceof Error && error.message) {
return error.message;
}
return String(error);
}
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,
@@ -17,6 +42,7 @@ export async function uploadCommandNext(
) { ) {
const {notionAPI, databaseID} = dbDetails; const {notionAPI, databaseID} = dbDetails;
console.log(`[uploadCommandNext] ${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 (notionAPI === "" || databaseID === "") { if (notionAPI === "" || databaseID === "") {
@@ -39,19 +65,76 @@ export async function uploadCommandNext(
paword, paword,
favicon, favicon,
datetime datetime
} = await getNowFileMarkdownContentNext(app, settings) } = 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;
logCommandDebug("uploadCommandNext", "Preparing to upload", {
filename: basename,
filePath: nowFile.path,
});
const upload = new Upload2NotionNext(plugin, dbDetails); const upload = new Upload2Notion(plugin, dbDetails);
const res = await upload.syncMarkdownToNotionNext(basename, emoji, cover, tags, type, slug, stats, category, summary, paword, favicon, datetime, markDownData, nowFile, this.app); let res: NotionPageResponse;
try {
res = await upload.sync({
dataset: "next",
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; const {response} = res;
if (response.status === 200) { if (response.status === 200) {
new Notice(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`).noticeEl.style.color = "green"; 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,
});
} }
} }
@@ -66,6 +149,7 @@ export async function uploadCommandGeneral(
) { ) {
const {notionAPI, databaseID} = dbDetails; 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 (notionAPI === "" || databaseID === "") { if (notionAPI === "" || databaseID === "") {
@@ -76,17 +160,58 @@ export async function uploadCommandGeneral(
const {markDownData, nowFile, cover, tags} = await getNowFileMarkdownContentGeneral(app, settings) const {markDownData, nowFile, cover, tags} = await getNowFileMarkdownContentGeneral(app, settings)
new Notice(`Start upload ${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, dbDetails); 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",
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;
}
const {response} = res; const {response} = res;
if (response.status === 200) { if (response.status === 200) {
new Notice(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`).noticeEl.style.color = "green"; 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,
});
} }
} }
@@ -100,7 +225,8 @@ export async function uploadCommandCustom(
app: App, app: App,
) { ) {
const { notionAPI, databaseID } = settings; 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 // Check if the user has set up the Notion API and database ID
if (notionAPI === "" || databaseID === "") { if (notionAPI === "" || databaseID === "") {
@@ -111,17 +237,58 @@ export async function uploadCommandCustom(
const {markDownData, nowFile, cover, customValues} = await getNowFileMarkdownContentCustom(app, dbDetails) const {markDownData, nowFile, cover, customValues} = await getNowFileMarkdownContentCustom(app, dbDetails)
new Notice(`Start upload ${nowFile.basename}`);
console.log(`Start upload ${nowFile.basename}`);
if (markDownData) { if (markDownData) {
const {basename} = nowFile; const {basename} = nowFile;
logCommandDebug("uploadCommandCustom", "Preparing to upload", {
filename: basename,
markdownLength: markDownData.length,
hasCover: !!cover,
customValueKeys: Object.keys(customValues || {}),
});
const upload = new Upload2NotionCustom(plugin, dbDetails); const upload = new Upload2Notion(plugin, dbDetails);
const res = await upload.syncMarkdownToNotionCustom(cover, customValues, markDownData, nowFile, this.app); let res: NotionPageResponse;
try {
res = await upload.sync({
dataset: "custom",
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; const {response} = res;
if (response.status === 200) { if (response.status === 200) {
new Notice(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`).noticeEl.style.color = "green"; 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 { } else {
new Notice(`${i18nConfig["sync-fail"]} ${basename}`, 5000); 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,
});
} }
} }

View File

@@ -1,96 +0,0 @@
import { App, Notice, requestUrl, TFile } from "obsidian";
import { Client } from '@notionhq/client';
import { markdownToBlocks, } from "@jxpeng98/martian";
import * as yamlFrontMatter from "yaml-front-matter";
// import * as yaml from "yaml"
import MyPlugin from "src/main";
import { DatabaseDetails } from "../../ui/settingTabs";
export class UploadBaseGeneral {
plugin: MyPlugin;
notion: Client;
agent: any;
dbDetails: DatabaseDetails
constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
this.plugin = plugin;
this.dbDetails = dbDetails
}
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': '2022-06-28',
},
body: ''
});
}
async getDataBase(databaseID: string) {
const { notionAPI } = this.dbDetails
const response = await requestUrl({
url: `https://api.notion.com/v1/databases/${databaseID}`,
method: 'GET',
headers: {
'Authorization': 'Bearer ' + notionAPI,
'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}`)
// // }
// }
}

View File

@@ -1,178 +0,0 @@
import { App, Notice, TFile } from "obsidian";
import { Client } from "@notionhq/client";
import { markdownToBlocks } from "@jxpeng98/martian";
import * as yamlFrontMatter from "yaml-front-matter";
// import * as yaml from "yaml"
import MyPlugin from "src/main";
import { DatabaseDetails, PluginSettings } from "../../ui/settingTabs";
import { UploadBaseGeneral } from "./BaseUpload2NotionGeneral";
import { updateYamlInfo } from "../updateYaml";
import fetch from 'node-fetch';
export class Upload2NotionGeneral extends UploadBaseGeneral {
settings: PluginSettings;
dbDetails: DatabaseDetails;
constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
super(plugin, dbDetails);
this.dbDetails = dbDetails;
}
// 因为需要解析notion的block进行对比非常的麻烦
// 暂时就直接删除新建一个page
async updatePage(
notionID: string,
title: string,
cover: string,
tags: string[],
childArr: any,
) {
await this.deletePage(notionID);
const { databaseID } = this.dbDetails;
const databaseCover = await this.getDataBase(
databaseID,
);
if (cover == null) {
cover = databaseCover;
}
return await this.createPage(title, cover, tags, childArr);
}
async createPage(
title: string,
cover: string,
tags: string[],
childArr: any,
) {
const {
databaseID,
customTitleButton,
customTitleName,
tagButton,
notionAPI
} = this.dbDetails;
const bodyString: any = {
parent: {
database_id: databaseID,
},
properties: {
[customTitleButton
? customTitleName
: "title"]: {
title: [
{
text: {
content: title,
},
},
],
},
...(tagButton
? {
tags: {
multi_select: tags && true ? tags.map((tag) => ({ 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,
},
};
}
console.log(bodyString)
const response = await fetch("https://api.notion.com/v1/pages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + notionAPI,
"Notion-Version": "2022-06-28",
},
body: JSON.stringify(bodyString),
});
const data: any = await response.json();
if (!response.ok) {
new Notice(`Error ${data.status}: ${data.code} \n Check the console for more information \n opt+cmd+i/ctrl+shift+i`, 5000);
console.log(`Error message: \n ${data.message}`);
} else {
console.log(`Page created: ${data.url}`);
console.log(`Page ID: ${data.id}`);
}
return {
response, // for status code
data // for id and url
}
}
async syncMarkdownToNotionGeneral(
title: string,
cover: string,
tags: string[],
markdown: string,
nowFile: TFile,
app: App,
): Promise<any> {
const options = {
notionLimits: {
truncate: false,
}
}
let res: any;
const yamlContent: any = yamlFrontMatter.loadFront(markdown);
const __content = yamlContent.__content;
const file2Block = markdownToBlocks(__content, options);
const frontmasster =
app.metadataCache.getFileCache(nowFile)?.frontmatter;
const { abName } = this.dbDetails
const notionIDKey = `NotionID-${abName}`;
const notionID = frontmasster ? frontmasster[notionIDKey] : null;
if (notionID) {
res = await this.updatePage(
notionID,
title,
cover,
tags,
file2Block,
);
} else {
res = await this.createPage(title, cover, tags, file2Block);
}
let {response, data} = res;
// console.log(response)
if (response && response.status === 200) {
await updateYamlInfo(markdown, nowFile, data, app, this.plugin, this.dbDetails);
}
return res;
}
}

View File

@@ -1,96 +0,0 @@
import { App, Notice, requestUrl, TFile } from "obsidian";
import { Client } from '@notionhq/client';
import { markdownToBlocks, } from "@jxpeng98/martian";
import * as yamlFrontMatter from "yaml-front-matter";
// import * as yaml from "yaml"
import MyPlugin from "src/main";
import { DatabaseDetails } from "../../ui/settingTabs";
export class UploadBaseNext {
plugin: MyPlugin;
notion: Client;
agent: any;
dbDetails: DatabaseDetails
constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
this.plugin = plugin;
this.dbDetails = dbDetails
}
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': '2022-06-28',
},
body: ''
});
}
async getDataBase(databaseID: string) {
const { notionAPI } = this.dbDetails
const response = await requestUrl({
url: `https://api.notion.com/v1/databases/${databaseID}`,
method: 'GET',
headers: {
'Authorization': 'Bearer ' + notionAPI,
'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}`)
// // }
// }
}

View File

@@ -1,334 +0,0 @@
import { UploadBaseNext } from "./BaseUpload2NotionNext";
import { App, Notice, TFile } from "obsidian";
import { Client } from '@notionhq/client';
import { markdownToBlocks, } from "@jxpeng98/martian";
import * as yamlFrontMatter from "yaml-front-matter";
// import * as yaml from "yaml"
import MyPlugin from "src/main";
import { DatabaseDetails, PluginSettings } from "../../ui/settingTabs";
import { updateYamlInfo } from "../updateYaml";
import { LIMITS, paragraph } from "@jxpeng98/martian/src/notion";
import fetch from 'node-fetch';
export class Upload2NotionNext extends UploadBaseNext {
settings: PluginSettings;
dbDetails: DatabaseDetails
constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
super(plugin, dbDetails);
this.dbDetails = dbDetails
}
// 因为需要解析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 { databaseID } = this.dbDetails
const databaseCover = await this.getDataBase(databaseID)
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 {
databaseID,
notionAPI
} = this.dbDetails
const bodyString: any = {
parent: {
database_id: databaseID,
},
properties: {
title: {
title: [
{
text: {
content: title
},
},
],
},
type: {
select: {
name: type || 'Post'
}
},
status: {
select: {
name: stats || 'Draft'
}
},
category: {
select: {
name: category || 'Obsidian'
}
},
password: {
rich_text: [
{
text: {
content: pawrod || ''
}
}
]
},
icon: {
rich_text: [
{
text: {
content: favicon || ''
}
}
]
},
date: {
date: {
start: datetime || new Date().toISOString()
}
}
},
children: childArr,
}
// add tags
if (tags) {
bodyString.properties.tags = {
multi_select: tags.map(tag => {
return { "name": tag }
})
}
}
// add title icon
if (emoji) {
bodyString.icon = {
emoji: emoji
}
}
// add slug
if (slug) {
bodyString.properties.slug = {
rich_text: [
{
text: {
content: slug
}
}
]
}
}
// check if summary is available
if (summary) {
bodyString.properties.summary = {
rich_text: [
{
text: {
content: summary
}
}
]
}
}
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
}
}
}
console.log(bodyString)
const response = await fetch("https://api.notion.com/v1/pages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + notionAPI,
"Notion-Version": "2022-06-28",
},
body: JSON.stringify(bodyString),
});
const data: any = await response.json();
// can use response.ok or response.status === 200
if (!response.ok) {
new Notice(`Error ${data.status}: ${data.code} \n Check the console for more information \n opt+cmd+i/ctrl+shift+i`, 5000);
console.log(`Error message: \n ${data.message}`);
} else {
console.log(`Page created: ${data.url}`);
console.log(`Page ID: ${data.id}`);
}
return {
response, // for status code
data // for id and url
}
}
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> {
const options = {
notionLimits: {
truncate: false,
}
}
let res: any
const yamlContent: any = yamlFrontMatter.loadFront(markdown);
const __content = yamlContent.__content
const file2Block = markdownToBlocks(__content, options);
const frontmasster = app.metadataCache.getFileCache(nowFile)?.frontmatter
const { abName } = this.dbDetails
const notionIDKey = `NotionID-${abName}`;
const notionID = frontmasster ? frontmasster[notionIDKey] : null;
// increase the limits
// Motivated by https://github.com/tryfabric/martian/issues/51
file2Block.forEach((block, index) => {
if (
block.type === 'paragraph' &&
block.paragraph.rich_text.length > LIMITS.RICH_TEXT_ARRAYS
) {
const newParagraphBlocks: any[] = []
const chunk: any = []
const richTextChunks = chunk(block.paragraph.rich_text, 100)
richTextChunks.forEach((chunk: any) => {
newParagraphBlocks.push(paragraph(chunk))
})
file2Block.splice(index, 1, ...newParagraphBlocks)
}
})
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
);
}
let {response, data} = res;
if (response && response.status === 200) {
await updateYamlInfo(markdown, nowFile, data, app, this.plugin, this.dbDetails);
}
return res;
}
}

View File

@@ -1,53 +0,0 @@
import { App, Notice, requestUrl, TFile } from "obsidian";
import { Client } from '@notionhq/client';
import { markdownToBlocks, } from "@jxpeng98/martian";
import * as yamlFrontMatter from "yaml-front-matter";
// import * as yaml from "yaml"
import MyPlugin from "src/main";
import { DatabaseDetails } from "../../ui/settingTabs";
export class UploadBaseCustom {
plugin: MyPlugin;
notion: Client;
agent: any;
dbDetails: DatabaseDetails
constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
this.plugin = plugin;
this.dbDetails = dbDetails
}
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': '2022-06-28',
},
body: ''
});
}
async getDataBase(databaseID: string) {
const { notionAPI } = this.dbDetails
const response = await requestUrl({
url: `https://api.notion.com/v1/databases/${databaseID}`,
method: 'GET',
headers: {
'Authorization': 'Bearer ' + notionAPI,
'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
}
}
}

View File

@@ -1,256 +0,0 @@
import {App, Notice, TFile} from "obsidian";
import {markdownToBlocks} from "@jxpeng98/martian";
import * as yamlFrontMatter from "yaml-front-matter";
// import * as yaml from "yaml"
import MyPlugin from "src/main";
import {DatabaseDetails, PluginSettings} from "../../ui/settingTabs";
import {updateYamlInfo} from "../updateYaml";
import {UploadBaseCustom} from "./BaseUpload2NotionCustom";
import fetch from 'node-fetch';
export class Upload2NotionCustom extends UploadBaseCustom {
settings: PluginSettings;
dbDetails: DatabaseDetails;
constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
super(plugin, dbDetails);
this.dbDetails = dbDetails;
}
// 因为需要解析notion的block进行对比非常的麻烦
// 暂时就直接删除新建一个page
async updatePage(
notionID: string,
cover: string,
customValues: Record<string, string>,
childArr: any,
) {
await this.deletePage(notionID);
const {databaseID} = this.dbDetails;
const databaseCover = await this.getDataBase(
databaseID
);
if (cover == null) {
cover = databaseCover;
}
return await this.createPage(cover, customValues, childArr);
}
async createPage(
cover: string,
customValues: Record<string, string>,
childArr: any,
) {
const {
databaseID,
customProperties,
notionAPI
} = this.dbDetails;
const bodyString: any = this.buildBodyString(customProperties, customValues, 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,
},
};
}
console.log(bodyString)
const response = await fetch("https://api.notion.com/v1/pages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + notionAPI,
"Notion-Version": "2022-06-28",
},
body: JSON.stringify(bodyString),
});
const data: any = await response.json();
if (!response.ok) {
new Notice(`Error ${data.status}: ${data.code} \n Check the console for more information \n opt+cmd+i/ctrl+shift+i`, 5000);
console.log(`Error message: \n ${data.message}`);
} else {
console.log(`Page created: ${data.url}`);
console.log(`Page ID: ${data.id}`);
}
return {
response, // for status code
data // for id and url
}
}
async syncMarkdownToNotionCustom(
cover: string,
customValues: Record<string, string>,
markdown: string,
nowFile: TFile,
app: App,
): Promise<any> {
const options = {
strictImageUrls: true,
notionLimits: {
truncate: false,
}
}
let res: any;
const yamlContent: any = yamlFrontMatter.loadFront(markdown);
const __content = yamlContent.__content;
const file2Block = markdownToBlocks(__content, options);
const frontmasster =
app.metadataCache.getFileCache(nowFile)?.frontmatter;
const {abName} = this.dbDetails
const notionIDKey = `NotionID-${abName}`;
const notionID = frontmasster ? frontmasster[notionIDKey] : null;
if (notionID) {
res = await this.updatePage(
notionID,
cover,
customValues,
file2Block,
);
} else {
res = await this.createPage(cover, customValues, file2Block);
}
let {response, data} = res;
// console.log(response)
if (response && response.status === 200) {
await updateYamlInfo(markdown, nowFile, data, app, this.plugin, this.dbDetails);
}
return res;
}
private buildPropertyObject(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 => ({
name: url,
type: "external",
external: {
url: 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 => ({name: item})) : [{name: value}],
};
}
}
private buildBodyString(
customProperties: { customName: string; customType: string }[],
customValues: Record<string, string>,
childArr: any,
) {
const properties: { [key: string]: any } = {};
// Only include custom properties that have values
customProperties.forEach(({customName, customType}) => {
if (customValues[customName] !== undefined) {
properties[customName] = this.buildPropertyObject(customName, customType, customValues);
}
}
);
// console.log(properties)
return {
parent: {
database_id: this.dbDetails.databaseID,
},
properties,
children: childArr,
};
}
}

970
yarn.lock

File diff suppressed because it is too large Load Diff