Compare commits

...

312 Commits

Author SHA1 Message Date
Jiaxin Peng
bb211543f5 fix: remove branch trigger for prerelease workflow 2025-11-01 00:38:25 +00:00
Jiaxin Peng
c52f84232d Merge pull request #67 from jxpeng98/automate-sync
Automate sync
2025-11-01 00:35:52 +00:00
Jiaxin Peng
3730b7645a chore: update changelog with detailed auto sync feature description and improvements 2025-11-01 00:34:23 +00:00
Jiaxin Peng
e2a965e3d6 fix: update autoSyncTimeout type and ensure window context for setTimeout calls 2025-11-01 00:32:35 +00:00
Jiaxin Peng
2738498dc2 fix: clean up formatting in auto sync logic for better readability 2025-11-01 00:30:39 +00:00
Jiaxin Peng
6df5e21f3d feat: implement enhanced frontmatter change detection and caching for auto sync 2025-11-01 00:30:09 +00:00
Jiaxin Peng
d660a088e3 feat: add GitHub Actions workflow for prerelease builds and asset uploads 2025-10-31 23:44:53 +00:00
Jiaxin Peng
83d4f1e48c feat: enhance auto sync functionality to support multiple databases and improved user notifications 2025-10-31 23:36:57 +00:00
Jiaxin Peng
7c841f3db3 feat: update UI text to use internationalization configuration for modals and notices 2025-10-31 23:36:49 +00:00
Jiaxin Peng
fab3acdc21 feat: add auto sync messages and database management labels in English, Japanese, and Chinese locales 2025-10-31 23:35:28 +00:00
Jiaxin Peng
59fa32b5e5 docs: enhance auto sync documentation with detailed setup, scenarios, and troubleshooting guidance 2025-10-31 23:35:16 +00:00
Jiaxin Peng
e5f906bba2 feat: migrate settings for auto sync feature and add user notification 2025-10-31 21:57:21 +00:00
Jiaxin Peng
c369c291a7 feat: add auto sync functionality with configurable delay in settings 2025-10-31 21:42:48 +00:00
Jiaxin Peng
33f0848b32 docs: add changelog and roadmap links to English and Chinese index files 2025-10-31 21:21:50 +00:00
Jiaxin Peng
a33e63a219 docs: add changelog and roadmap files in English and Chinese 2025-10-31 17:00:26 +00:00
Jiaxin Peng
99a40375f5 docs: update documentation website link in README files 2025-10-31 14:27:39 +00:00
Jiaxin Peng
7282fc6955 chore: Switch to pnpm and create individual package for docs 2025-10-31 13:46:31 +00:00
Jiaxin Peng
770061e826 chore: remove unused image files from documentation 2025-10-31 11:14:43 +00:00
Jiaxin Peng
de7b7dd28d Merge pull request #65 from jxpeng98/docs
add doc website
2025-10-31 11:03:19 +00:00
Jiaxin Peng
1c6c3fb0d9 chore: add vitepress package 2025-10-31 10:57:37 +00:00
Jiaxin Peng
ca0935bfe8 docs: update README to reflect new features and improved database support 2025-10-31 10:56:47 +00:00
Jiaxin Peng
e944721415 docs: add documentation for Obsidian to NotionNext plugin setup and usage 2025-10-31 10:47:34 +00:00
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
Jiaxin Peng
800e218dee Update version to 2.3.5 2024-08-18 13:08:04 +01:00
Jiaxin Peng
3b774552af update martian 2024-08-18 13:07:52 +01:00
b8a02c49f1 Update version to 2.3.4 2024-07-28 01:13:39 +01:00
Jiaxin Peng
046a67f22c Merge pull request #29 from jxpeng98/debug-general
General: use node-fetch to post notes
2024-07-28 01:08:08 +01:00
426f05dfbd General: use node-fetch to post notes 2024-07-28 01:07:35 +01:00
Jiaxin Peng
0addaa0d7f Merge pull request #28 from jxpeng98/debug-next
Next: use node-fetch to post notes for a better debug
2024-07-28 00:54:05 +01:00
1d5792c966 Next: use node-fetch to post notes for a better debug 2024-07-28 00:52:27 +01:00
Jiaxin Peng
193d3f6f87 Merge pull request #27 from jxpeng98/better-debug
custom: node-fetch replaces requestUrl
2024-07-27 23:37:27 +01:00
e9c2b4aada Custom: use node-fetch to post notes rather than requestUrl for better debug 2024-07-27 23:36:00 +01:00
b5ac42e6e3 use node-fetch to post notes for a better debug 2024-07-27 22:28:21 +01:00
60d002677b use json to parse the error information 2024-07-27 21:38:09 +01:00
3c3ec6881e Update version to 2.3.3 2024-07-15 14:19:11 +01:00
Jiaxin Peng
a80155fafc Merge pull request #26 from jxpeng98/notice-info
change the success notice colour
2024-07-15 14:17:01 +01:00
3a814b8390 feat: change the success notice colour 2024-07-15 14:15:10 +01:00
bb522db9a2 fix: readme gif link update 2024-07-15 14:13:09 +01:00
201f72ae0e update notice 2024-07-14 23:57:01 +01:00
be6f192dd8 Update readme file 2024-07-14 21:51:53 +01:00
4ce1019094 Update Readme links 2024-07-14 20:27:02 +01:00
095c608511 update readme with new instruction 2024-07-14 17:58:21 +01:00
2d8d7fb11a Update version to 2.3.2 2024-07-14 17:52:59 +01:00
36725cb699 fix: Text property in custom database 2024-07-14 17:52:43 +01:00
Jiaxin Peng
654e7e854d 更新 README.md 2024-07-10 00:32:00 +01:00
Jiaxin Peng
fa67f08512 Update version to 2.3.1 2024-07-05 09:50:31 +01:00
Jiaxin Peng
7e42ef49a7 Merge pull request #24 from jxpeng98/notion-link
feat: add notion link display
2024-07-05 09:49:27 +01:00
Jiaxin Peng
5a95aa0acc feat: add notion link display 2024-07-05 09:48:35 +01:00
Jiaxin Peng
ded01ce678 Update version to 2.3.0 2024-07-04 20:57:35 +01:00
Jiaxin Peng
3535d9fc70 Merge pull request #23 from jxpeng98/edit-modal
edit modal update for customised database
2024-07-04 20:55:41 +01:00
Jiaxin Peng
9c93e38aff reformat code and update readme file 2024-07-04 20:51:07 +01:00
Jiaxin Peng
bfde5dfe1a finish the edit modal 2024-07-04 20:28:28 +01:00
Jiaxin Peng
d8eb3c8984 feat: cancel function works well 2024-07-04 18:12:31 +01:00
Jiaxin Peng
1cee2b67a6 update edit modal with better display setting. 2024-07-04 17:06:23 +01:00
Jiaxin Peng
82529ce56a back up for edit modal 2024-07-04 10:00:46 +01:00
Jiaxin Peng
70f30f0713 Add support for notion link display in the front matter. 2024-07-03 22:35:47 +01:00
Jiaxin Peng
24ebb0bf8a Back up edit modal 2024-07-03 22:01:46 +01:00
Jiaxin Peng
d7372c7c55 Improve preview function for customised database 2024-07-03 16:05:24 +01:00
Jiaxin Peng
f7bcf71020 create retrieve function to get all created properties for customised database 2024-07-03 14:26:02 +01:00
Jiaxin Peng
72146afe48 update settingModal.ts refactor the customise database and improve the creating logic 2024-07-03 13:39:56 +01:00
Jiaxin Peng
9bcdb42edb settingmodal save mapping for reference 2024-07-03 11:06:54 +01:00
Jiaxin Peng
16e0827991 update the basic function of edit modal 2024-07-03 08:47:33 +01:00
Jiaxin Peng
863a6fb0dc more clear logic customised database setting code 2024-07-02 23:56:35 +01:00
Jiaxin Peng
5799ffe79b Update custom function and adjust the custom interface ui 2024-07-02 23:48:37 +01:00
Jiaxin Peng
a8ebab7fcb change tags titleicon and summary to optional function in next database 2024-03-17 14:36:55 +00:00
Jiaxin Peng
e0ec27ad07 update readme 2024-03-12 22:04:34 +00:00
Jiaxin Peng
85220105b0 update README 2024-03-08 15:23:17 +00:00
Jiaxin Peng
9afdb24a82 typo 2024-02-26 11:57:28 +00:00
Jiaxin Peng
3c7ab13995 update readme 2024-02-26 11:53:02 +00:00
Jiaxin Peng
31b72f3bd3 Update version to 2.2.4 2024-02-19 23:08:47 +00:00
Jiaxin Peng
ee5aada1b9 Update version to 2.2.3 2024-02-19 20:51:47 +00:00
Jiaxin Peng
9e15044f6a Update version to 2.2.2 2024-02-16 23:10:22 +00:00
Jiaxin Peng
9d891dae55 Update version to 2.2.1 2024-01-29 20:43:14 +00:00
Jiaxin Peng
bc665253ec Update version to 2.2.0 2024-01-29 19:03:09 +00:00
Jiaxin Peng
125c75f7c0 Merge pull request #13 from jxpeng98/custom
add the customisation function
2024-01-29 19:01:37 +00:00
Jiaxin Peng
8248640b5f fix the extra argument in the uploadcommand 2024-01-29 11:55:52 +00:00
Jiaxin Peng
0198898086 update the readme and changelog 2024-01-29 11:53:18 +00:00
Jiaxin Peng
8c6aea8250 Complete the basic function of customisation 2024-01-29 11:12:07 +00:00
Jiaxin Peng
a179d3da52 almost finish the structure 2024-01-29 00:55:06 +00:00
Jiaxin Peng
569d8eb699 fix the error of the display of title custom button 2024-01-28 00:26:58 +00:00
Jiaxin Peng
2a58dd3258 complete the custom ui design 2024-01-28 00:11:11 +00:00
Jiaxin Peng
8c65e471eb Update version to 2.1.2 2024-01-09 11:44:40 +00:00
Jiaxin Peng
388693d5b4 Update version to 2.1.1 2024-01-09 11:38:03 +00:00
Jiaxin Peng
dda6710fe9 remove redundant settingtabs 2024-01-09 11:37:47 +00:00
Jiaxin Peng
76ea9a4aee Update version to 2.1.0 2024-01-09 11:34:11 +00:00
Jiaxin Peng
856f83e8a1 Add confirmation delete button and modify edit modal interface. 2024-01-09 11:33:39 +00:00
Jiaxin Peng
f670dd972a Update version to 2.0.1 2024-01-03 00:37:47 +00:00
Jiaxin Peng
e6de2d036a add preview and edit function 2024-01-03 00:37:34 +00:00
Jiaxin Peng
dd131f5df1 readme update 2023-12-29 00:41:02 +00:00
Jiaxin Peng
a9fb81fc1b readme error 2023-12-29 00:39:18 +00:00
Jiaxin Peng
4f21e3ff91 Update version to 2.0.0 2023-12-29 00:36:48 +00:00
Jiaxin Peng
4abbbecd37 modify the notionapi variable 2023-12-29 00:36:29 +00:00
Jiaxin Peng
35746f1c51 Merge pull request #12 from jxpeng98/add-button
rebuild the plugin and add new button for multi database support
2023-12-29 00:26:16 +00:00
Jiaxin Peng
9ac64268fc Merge branch 'master' into add-button 2023-12-29 00:26:02 +00:00
Jiaxin Peng
3123d91e97 complete the new version for add button 2023-12-29 00:21:21 +00:00
Jiaxin Peng
cf08703fea complete the basic notionnext function 2023-12-28 00:55:06 +00:00
Jiaxin Peng
fb463ceaae modify language map 2023-12-27 00:48:39 +00:00
Jiaxin Peng
306573d81e merge 1.1.2 2023-12-27 00:37:54 +00:00
Jiaxin Peng
fb5f7b502d finish some command coding 2023-12-27 00:27:32 +00:00
Jiaxin Peng
59c15f1206 modify setting modal 2023-12-26 21:19:24 +00:00
Jiaxin Peng
370444fb94 basic display of modal 2023-12-26 09:03:29 +00:00
Jiaxin Peng
11aaf14c0b update general tabs 2023-12-25 22:47:24 +00:00
Jiaxin Peng
f3fb251cf7 set the class next tabs 2023-12-25 22:43:11 +00:00
Jiaxin Peng
913fa4dd03 Update version to 1.1.2 2023-12-06 15:22:53 +00:00
Jiaxin Peng
f638310f31 modify workflow 2023-12-06 15:22:09 +00:00
Jiaxin Peng
c2421f7a84 add the property 'status' for NotionNext 2023-12-06 15:19:57 +00:00
Jiaxin Peng
6c30d0c2b4 adjust the limitation of blocks 2023-12-06 14:40:15 +00:00
Jiaxin Peng
ccb5a20abe add custom frontmatter 2023-11-23 22:11:04 +00:00
Jiaxin Peng
630e3a20c2 Update version to 1.1.1 2023-11-23 11:54:50 +00:00
Jiaxin Peng
040b72b760 Update version to 1.1.0 2023-11-20 22:01:20 +00:00
Jiaxin Peng
7df4b5e9c8 Merge pull request #9 from jxpeng98/package-test
package update and add tag syncing button
2023-11-20 21:59:53 +00:00
Jiaxin Peng
3f35a56b6e rebuild the locale file and add tag button to control tag syncing function 2023-11-20 21:57:30 +00:00
Jiaxin Peng
3c7e266a35 setting tab bug fix 2023-11-20 17:07:04 +00:00
Jiaxin Peng
8201c706ea remove block limit and update packages 2023-11-16 21:09:31 +00:00
Jiaxin Peng
4433ea449d Update version to 1.0.1 2023-11-16 13:55:14 +00:00
Jiaxin Peng
96126d1334 Jump to version 1.0.0 2023-11-16 13:32:07 +00:00
Jiaxin Peng
b3702a9cc1 Update version to 1.0.0 2023-11-16 13:29:29 +00:00
Jiaxin Peng
ba9b477db7 Update version to 0.2.7 2023-11-16 13:27:48 +00:00
Jiaxin Peng
fb6951cfdf Update version to 0.3.5 2023-11-16 13:25:56 +00:00
Jiaxin Peng
6454c0417d Update version to 0.3.4 2023-11-16 13:10:53 +00:00
Jiaxin Peng
35f19a9f43 modify the action file 2023-11-16 13:10:27 +00:00
Jiaxin Peng
8e32e8f0f5 Update version to 0.3.3 2023-11-16 12:59:41 +00:00
Jiaxin Peng
880f256ae1 Update version to 0.3.2 2023-11-16 12:55:01 +00:00
Jiaxin Peng
bf9c3f2933 Update version to 0.3.1 2023-11-16 11:58:58 +00:00
Jiaxin Peng
2ac354299a update action file 2023-11-16 11:58:44 +00:00
Jiaxin Peng
8317abb667 Update version to 0.3.0 2023-11-16 11:50:36 +00:00
Jiaxin Peng
d74634aca6 Merge pull request #7 from jxpeng98/custom-title
Custom title
2023-11-16 11:48:14 +00:00
Jiaxin Peng
d400b92263 update readme 2023-11-16 11:44:45 +00:00
Jiaxin Peng
e7fa597e5a update translate 2023-11-16 11:14:19 +00:00
Jiaxin Peng
6ce689ba96 complete the basic customisation feature 2023-11-16 01:00:52 +00:00
Jiaxin Peng
b978cf7e01 complete the setting ui 2023-11-16 00:02:35 +00:00
Jiaxin Peng
4e3c88f129 reorganise the code for add new setting container 2023-11-15 12:25:30 +00:00
Jiaxin Peng
6550bca70f reorganise the setting coding 2023-11-15 09:34:22 +00:00
Jiaxin Peng
662edbb84c remove the collapse details 2023-11-15 00:16:19 +00:00
Jiaxin Peng
671936ee41 add some details for collapse 2023-11-15 00:15:52 +00:00
Jiaxin Peng
2b16ea76c6 update the setting ui 2023-11-14 23:27:38 +00:00
Jiaxin Peng
399a02187f update packages and complete the setting UI 2023-11-14 15:42:30 +00:00
Jiaxin Peng
eee349509a Update version to 0.2.6 2023-11-12 21:02:00 +00:00
Jiaxin Peng
57c0941fed Merge pull request #6 from jxpeng98/add-button
Add switch button
2023-11-12 21:00:43 +00:00
Jiaxin Peng
8734af6dbf add switch button 2023-11-12 20:59:39 +00:00
Jiaxin Peng
041a78fb52 add button to display the command in the command palette 2023-11-11 00:29:35 +00:00
Jiaxin Peng
afec5d8364 Update version to 0.2.5 2023-11-05 20:49:28 +00:00
Jiaxin Peng
75926a38d4 Update version to 0.2.4 2023-11-05 20:46:42 +00:00
Jiaxin Peng
793dd67e0a Update version to 0.2.3 2023-11-05 20:44:51 +00:00
Jiaxin Peng
f235c80d18 update readme 2023-11-05 20:44:42 +00:00
Jiaxin Peng
f33315a4b6 fix github action bug 2023-11-05 20:43:47 +00:00
Jiaxin Peng
b62bd4ba2e Update version to 0.2.2 2023-11-05 17:58:19 +00:00
Jiaxin Peng
402d15a38b Merge pull request #5 from jxpeng98/general-database
General database
2023-11-05 17:57:21 +00:00
Jiaxin Peng
87f1ce0146 update readme and settingtabs 2023-11-05 17:50:51 +00:00
Jiaxin Peng
398b785e03 update the script and support both notionnext and general database 2023-11-05 17:32:10 +00:00
Jiaxin Peng
8617878582 restructure the code for further feature 2023-11-04 22:37:02 +00:00
Jiaxin Peng
ae9e0c14fb modify the directory structure and rename the function 2023-11-02 14:32:25 +00:00
Jiaxin Peng
9be9b18efb Update version to 0.2.0 2023-11-01 23:24:34 +00:00
Jiaxin Peng
588f57205b Modified the template and rebuilt the interactive logic 2023-11-01 23:24:18 +00:00
Jiaxin Peng
cfbedea91d Update version to 0.1.10 2023-11-01 10:32:35 +00:00
Jiaxin Peng
4cd6557444 chinese support in settings 2023-11-01 10:32:22 +00:00
Jiaxin Peng
407688c303 Update version to 0.1.9 2023-11-01 09:53:29 +00:00
Jiaxin Peng
84b998aa93 initialise the NNon value and add i18n 2023-11-01 09:53:14 +00:00
Jiaxin Peng
4b6f1a1f1d Update version to 0.1.8 2023-10-31 23:56:50 +00:00
Jiaxin Peng
4a2c59d13b Update version to 0.1.7 2023-10-11 23:05:46 +01:00
Jiaxin Peng
8f116956ce Update version to 0.1.6 2023-09-28 12:00:40 +01:00
Jiaxin Peng
df138d9304 Update version to 0.1.5 2023-08-29 15:18:41 +01:00
Jiaxin Peng
49b8d470fb Update version to 0.1.4 2023-08-29 14:48:50 +01:00
Jiaxin Peng
c39323c3fa Update version to 0.1.3 2023-08-29 14:29:42 +01:00
Jiaxin Peng
05dff7a389 Update version to 0.1.2 2023-08-29 14:28:22 +01:00
Jiaxin Peng
24fcadbc8b modify the action 2023-08-29 14:27:10 +01:00
Jiaxin Peng
fdb81f76a7 Update version to 0.1.1 2023-08-29 14:21:12 +01:00
Jiaxin Peng
024b7f726e Update version to 0.1.0 2023-08-29 13:39:52 +01:00
Jiaxin Peng
722ca7bb56 update packages and modify upload2notion 2023-08-29 13:39:36 +01:00
Jiaxin Peng
fb7294d607 Update version to 0.0.9 2023-08-11 23:11:24 +01:00
Jiaxin Peng
4f06d81588 change colour and size for the icon 2023-08-11 23:11:02 +01:00
Jiaxin Peng
2434ddd532 fix the notification bug 2023-08-10 23:20:26 +01:00
Jiaxin Peng
55804e491a modify the cover image url setting 2023-08-10 22:50:48 +01:00
Jiaxin Peng
679e7ec4f1 add support for cover image and title icon 2023-08-10 22:04:57 +01:00
Jiaxin Peng
9be679f456 amend the template 2023-08-10 09:44:06 +01:00
Jiaxin Peng
a0f5d2d51b update the notion version 2023-08-10 09:30:27 +01:00
Jiaxin Peng
a68a3920ad minor update 2023-08-10 09:26:50 +01:00
Jiaxin Peng
76d3fc9872 Update version to 0.0.3 2023-08-10 09:15:26 +01:00
Jiaxin Peng
e3fb50b205 Update version to 0.0.2 2023-08-10 09:07:22 +01:00
Jiaxin Peng
5411b0bac6 Update version to 0.0.2 2023-08-10 09:02:51 +01:00
Jiaxin Peng
f974d6dc66 Update version to 0.0.1 2023-08-09 22:12:14 +01:00
Jiaxin Peng
de110334e6 Update version to 0.02 2023-08-09 22:11:44 +01:00
Jiaxin Peng
d8c8e78625 Update version to 2023-08-09 22:04:47 +01:00
Jiaxin Peng
feab049033 Update version to 2023-08-09 21:36:54 +01:00
Jiaxin Peng
04d73fdccc Merge pull request #4 from jxpeng98/test-date
complete the code
2023-08-09 20:42:17 +01:00
Jiaxin Peng
f4ab15c0d5 complete the code 2023-08-09 20:41:32 +01:00
Jiaxin Peng
0bd4427f7a Merge pull request #3 from jxpeng98/password-test
update the function of password and icon
2023-08-09 20:29:35 +01:00
Jiaxin Peng
221cf62fcc update the function of password and icon 2023-08-09 20:15:47 +01:00
Jiaxin Peng
7a2610c4b9 Merge pull request #2 from jxpeng98/test-status
Test status
2023-08-09 19:48:27 +01:00
Jiaxin Peng
f9f30df7ea successful category and summary 2023-08-09 19:48:03 +01:00
Jiaxin Peng
1d22f2e741 success status 2023-08-09 19:41:08 +01:00
Jiaxin Peng
935ecacd2b Merge pull request #1 from jxpeng98/test-slug
Test slug
2023-08-09 19:33:01 +01:00
Jiaxin Peng
6724399628 successful build for type and slug 2023-08-09 19:32:25 +01:00
Jiaxin Peng
8fd6f7c701 add type and slug 2023-08-09 16:50:03 +01:00
chris
f3e3ccdeb1 Update version to 0.7.6 2023-01-01 14:02:15 +08:00
chris
c65cb6a4df Fixed tags 2023-01-01 14:01:39 +08:00
chris
7c3028dd63 Update version to 0.7.5 2023-01-01 12:34:12 +08:00
chris
d2a40694e3 Fixed tags bug 2023-01-01 12:34:10 +08:00
chris
9f7298a880 Update version to 0.7.4 2023-01-01 11:53:53 +08:00
chris
b8860beb85 Fixed tags bug 2023-01-01 11:53:47 +08:00
chris
5e8c8f6f63 Update version to 0.7.3 2023-01-01 11:44:31 +08:00
chris
1887ad8f3d Fixed tags bug 2023-01-01 11:44:18 +08:00
chris
517426ea7a Update version to 0.7.2 2022-12-19 21:07:34 +08:00
chris
b6d5ff0093 Fixed error: when switch tags is null 2022-12-19 21:07:26 +08:00
chris
41ebff9067 Update version to 0.7.1 2022-12-18 11:37:11 +08:00
Easy
96b83b6da0 Merge pull request #35 from jannikbuscha/master
fix: error if no tags but option enabled
2022-12-18 11:36:29 +08:00
Jannik | Kuqs
63ded7d50e fix: error if no tags but option enabled 2022-12-17 17:10:22 +01:00
chris
8ef9a007e3 Merge custion tags usage 2022-12-17 14:29:34 +08:00
chris
2d57952c38 Added tags show 2022-12-17 14:26:04 +08:00
chris
e3a74d61cc Added tags usage and thanks for jannikbuscha 2022-12-17 12:26:42 +08:00
chris
47e902a6ee Update version to 0.7.0 2022-12-17 12:17:51 +08:00
Easy
098110d9ba Merge pull request #34 from jannikbuscha/master
feat: convert tags support
2022-12-17 12:16:27 +08:00
Jannik | Kuqs
82629c122f feat: convert tags support 2022-12-16 16:29:35 +01:00
chris
7be8f1c799 Added error delay to 3000 and add some readme to dev 2022-12-14 12:09:46 +08:00
chris
df58b7acb6 Fixed new integration 2022-09-15 16:43:05 +08:00
chris
0fedf4e83f Update version to 0.6.9 2022-08-22 09:50:36 +08:00
chris
444ccfcd0f Update version to 0.6.8 2022-08-22 09:36:17 +08:00
chris
05a8b789d9 Fixed delete old page 2022-08-22 09:35:52 +08:00
chris
d845dc7945 Added upload image readme 2022-07-23 09:45:34 +08:00
chris
26a326bb0d Update readme 2022-07-01 13:36:33 +08:00
chris
539a2d1731 Added market download 2022-06-28 21:44:44 +08:00
chris
3eb9b0305a Added market download 2022-06-28 21:42:59 +08:00
chris
a6df90dc56 Update version to 0.6.7 2022-06-22 21:36:55 +08:00
chris
46e5477509 Refactor password and notice 2022-06-22 21:36:42 +08:00
chris
2364a6c756 Update readme 2022-06-18 10:23:06 +08:00
chris
21133862ff Update readme 2022-06-18 09:50:17 +08:00
chris
68a0796e9b Update readme 2022-06-18 09:47:10 +08:00
chris
058d45e834 Update version to 0.6.6 2022-06-18 09:35:28 +08:00
chris
fdc0b3e756 Update version to 0.6.5 2022-06-18 09:09:40 +08:00
chris
27d88a9b94 Update support mobile 2022-06-18 09:09:27 +08:00
chris
089dca83bb Update version to 0.6.3 2022-06-18 00:16:32 +08:00
chris
fc0954540a Update version to 0.6.2 2022-06-18 00:12:40 +08:00
chris
f3fe8e7835 Added right share url link 2022-06-18 00:12:36 +08:00
chris
f60bc598a8 Update version to 0.6.1 2022-06-17 23:55:16 +08:00
chris
0752c5bbd9 Added right share url link 2022-06-17 23:55:07 +08:00
chris
22f12063e4 Update version to 0.6.0 2022-06-17 23:54:09 +08:00
chris
e2defbd248 Update readme 2022-06-17 23:53:58 +08:00
chris
0b7d5b3435 Added right share url link 2022-06-17 23:47:38 +08:00
chris
2a4e03e319 Update use file modify instead of fs write 2022-06-17 23:11:38 +08:00
chris
c0d9f41555 Update use file modify instead of fs write 2022-06-17 23:10:37 +08:00
chris
bc65eb2781 Update use file modify instead of fs write 2022-06-17 23:09:14 +08:00
chris
22d2f79612 Fixed pull requests and Added api error alert 2022-06-17 22:18:39 +08:00
chris
c62482d83b uodate readme add todo board 2022-06-01 21:00:12 +08:00
chris
bf512fe2a3 Update readme with banner option 2022-05-26 15:41:11 +08:00
chris
ae7a9ac6cf Update license 2022-05-26 09:56:33 +08:00
chris
595935ffae Added new lang config and remove no use clg 2022-05-25 15:21:29 +08:00
chris
cf3129b90b Update version to 0.5.6 2022-05-25 15:14:56 +08:00
chris
6d5ece76b6 Added auto copy share link to clipboard 2022-05-25 15:14:31 +08:00
chris
021584dac6 Update version to 0.5.5 2022-05-25 14:54:15 +08:00
chris
e6fc5e522b Remove styless.css 2022-05-25 14:54:06 +08:00
chris
e6099a94d5 Update version to 0.5.3 2022-05-25 14:50:13 +08:00
chris
04b8ba65de Added styles.css 2022-05-25 14:49:36 +08:00
chris
43a7165983 Update version to 0.5.2 2022-05-25 14:47:45 +08:00
chris
3740680bdd Update version to 0.5.1 2022-05-25 14:45:06 +08:00
chris
5ac25c3de9 Added style.css 2022-05-25 14:43:57 +08:00
chris
8bc84b8d61 Update version to 0.5.0 2022-05-25 14:41:12 +08:00
chris
7aae1b23e9 Added mult language show error and notice 2022-05-25 14:40:51 +08:00
chris
511a3e2289 Added update 2022-05-25 14:15:09 +08:00
chris
22bf322127 Remove no-used styles.css 2022-05-24 21:58:45 +08:00
chris
4497d9cd7f Added feature 2022-05-24 20:15:46 +08:00
chris
d4aeaaa0f9 Update version to 0.3.8 2022-05-24 18:03:07 +08:00
chris
bf6ef7af0c Added custom banner url 2022-05-24 18:02:52 +08:00
chris
27c8e0f610 Added custom banner url 2022-05-24 18:00:44 +08:00
chris
c9a5c72c6c Added desk only 2022-05-24 16:31:42 +08:00
chris
7bc0b43d4f Update chinese doc 2022-05-24 15:55:10 +08:00
chris
d5f908ed34 Remove main.js 2022-05-24 15:53:30 +08:00
chris
f87e024e77 Added README 2022-05-24 15:52:14 +08:00
chris
e667bec7e0 Added README 2022-05-24 15:51:46 +08:00
chris
845cb7bf66 Update version to 0.3.7 2022-05-24 15:42:12 +08:00
chris
7d096e1cfb Added Ci 2022-05-24 15:41:36 +08:00
chris
f3346c455e Added workflows CI 2022-05-24 15:40:51 +08:00
chris
208fb40f72 Added english readme 2022-05-24 15:30:45 +08:00
chris
e3669e7a83 Added auto shell 2022-05-24 15:23:43 +08:00
chris
617bb370dc FIxed BRAT download 2022-05-24 15:13:42 +08:00
66 changed files with 8968 additions and 16389 deletions

111
.github/workflows/prerelease.yml vendored Normal file
View 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: "18"
- name: Build
id: build
run: |
npm install
npm run build
mkdir ${{ env.PLUGIN_NAME }}
cp main.js manifest.json ${{ env.PLUGIN_NAME }}
zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
ls
echo "tag_name=$(git tag --sort version:refname | tail -n 1)" >> $GITHUB_OUTPUT
- name: Create Prerelease
id: create_prerelease
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
VERSION: ${{ github.ref }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }} (Prerelease)
body: |
## ⚠️ Prerelease Version - For Testing Only
This is a prerelease version intended for testing purposes.
**Important Notes:**
- This version may contain bugs or experimental features
- Not recommended for production use
- Please report any issues you encounter
### Installation
**Method 1: Using BRAT (Recommended)**
1. Install [BRAT](https://github.com/TfTHacker/obsidian42-brat) plugin if you haven't already
2. Open BRAT settings
3. Click "Add Beta plugin"
4. Enter: `jxpeng98/obsidian-to-NotionNext`
5. Enable "Share to NotionNext" in Community Plugins
**Method 2: Manual Installation**
1. Download the `${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip` file
2. Extract it to your `.obsidian/plugins/` folder
3. Reload Obsidian
### Changelog
See [CHANGELOG.md](${{ env.CHANGELOG_FILENAME }}) for details.
---
**Feedback:** Please report issues in the [GitHub Issues](https://github.com/${{ github.repository }}/issues) section.
draft: false
prerelease: true
- name: Upload zip file
id: upload-zip
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
with:
upload_url: ${{ steps.create_prerelease.outputs.upload_url }}
asset_path: ./${{ env.PLUGIN_NAME }}.zip
asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip
asset_content_type: application/zip
- name: Upload main.js
id: upload-main
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
with:
upload_url: ${{ steps.create_prerelease.outputs.upload_url }}
asset_path: ./main.js
asset_name: main.js
asset_content_type: text/javascript
- name: Upload manifest.json
id: upload-manifest
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
with:
upload_url: ${{ steps.create_prerelease.outputs.upload_url }}
asset_path: ./manifest.json
asset_name: manifest.json
asset_content_type: application/json

View File

@@ -1,87 +1,108 @@
name: Release Obsidian plugin name: Release
on: on:
push: push:
tags: tags:
- "*" - "*"
env: env:
PLUGIN_NAME: obsidian-to-notion # 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:
runs-on: ubuntu-latest name: release
runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: "14.x" node-version: "18"
- name: Build # - name: Generate changelog
id: build # id: changelog
run: | # uses: saadmk11/changelog-ci@v1.1.2
npm install # with:
npm run build # github_token: ${{ secrets.REPO_ACCESS_TOKEN }}
mkdir ${{ env.PLUGIN_NAME }} # release_version: ${{ github.ref }}
cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }} # changelog_filename: CHANGELOG.md
zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
ls
echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)"
- name: Create Release - name: Build
id: create_release id: build
uses: actions/create-release@v1 run: |
env: npm install
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} npm run build
VERSION: ${{ github.ref }} mkdir ${{ env.PLUGIN_NAME }}
with: cp main.js manifest.json ${{ env.PLUGIN_NAME }}
tag_name: ${{ github.ref }} zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
release_name: ${{ github.ref }} ls
draft: false echo "tag_name=$(git tag --sort version:refname | tail -n 1)" >> $GITHUB_OUTPUT
prerelease: false
- name: Upload zip file - name: Create Release
id: upload-zip id: create_release
uses: actions/upload-release-asset@v1 uses: actions/create-release@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
with: VERSION: ${{ github.ref }}
upload_url: ${{ steps.create_release.outputs.upload_url }} with:
asset_path: ./${{ env.PLUGIN_NAME }}.zip tag_name: ${{ github.ref }}
asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip release_name: ${{ github.ref }}
asset_content_type: application/zip body_path: ${{ env.CHANGELOG_FILENAME }}
draft: false
prerelease: false
- name: Upload main.js - name: Upload zip file
id: upload-main id: upload-zip
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./main.js asset_path: ./${{ env.PLUGIN_NAME }}.zip
asset_name: main.js asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip
asset_content_type: text/javascript asset_content_type: application/zip
- name: Upload manifest.json - name: Upload main.js
id: upload-manifest id: upload-main
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./manifest.json asset_path: ./main.js
asset_name: manifest.json asset_name: main.js
asset_content_type: application/json asset_content_type: text/javascript
- name: Upload styles.css - name: Upload manifest.json
id: upload-css id: upload-manifest
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./styles.css asset_path: ./manifest.json
asset_name: styles.css asset_name: manifest.json
asset_content_type: text/css asset_content_type: application/json
- name: Upload markdown template
id: upload-md
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./obsidian_template.md
asset_name: template.md
asset_content_type: text/markdown
# - name: Upload styles.css
# id: upload-css
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: ./styles.css
# asset_name: styles.css
# asset_content_type: text/css

32
.github/workflows/test.yml vendored Normal file
View 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: "18"
- name: Build
id: build
run: |
npm install
npm run build
mkdir ${{ env.PLUGIN_NAME }}
cp main.js manifest.json ${{ env.PLUGIN_NAME }}
zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
ls
echo "tag_name=$(git tag --sort version:refname | tail -n 1)" >> $GITHUB_OUTPUT

10
.gitignore vendored
View File

@@ -10,7 +10,7 @@ node_modules
# Don't include the compiled main.js file in the repo. # Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead. # They should be uploaded to GitHub releases instead.
# main.js main.js
# Exclude sourcemaps # Exclude sourcemaps
*.map *.map
@@ -20,3 +20,11 @@ data.json
# Exclude macOS Finder (System Explorer) View States # Exclude macOS Finder (System Explorer) View States
.DS_Store .DS_Store
.history
# local data
local-data
# vitepress
docs/.vitepress/dist
docs/.vitepress/cache

26
CHANGELOG.md Normal file
View File

@@ -0,0 +1,26 @@
# Changelog
## [Unreleased]
### Added
- **Auto Sync Feature**: Automatically sync notes to Notion when content or frontmatter changes
- Configurable delay (default: 5 seconds, minimum: 2 seconds)
- Support for multiple database syncing
- Smart detection to avoid sync loops when only NotionID is updated
- Content hash comparison to detect body text changes
- Works on both desktop and mobile platforms
- Added comprehensive i18n support for all UI elements and notifications
- Added prerelease workflow for beta testing via GitHub Actions and BRAT
### Changed
- Enhanced settings tab with auto-sync configuration options
- Improved debug logging for better troubleshooting
- Updated documentation with auto-sync usage guide and troubleshooting section
### Fixed
- Fixed mobile compatibility issues by using `window.setTimeout` instead of `NodeJS.Timeout`
- Fixed sync loop prevention logic to properly handle frontmatter and content changes
- Fixed cache update timing to ensure accurate change detection

20
README-zh.md Normal file
View File

@@ -0,0 +1,20 @@
# Obsidian to NotionNext
[![Test](https://github.com/jxpeng98/obsidian-to-NotionNext/actions/workflows/test.yml/badge.svg)](https://github.com/jxpeng98/obsidian-to-NotionNext/actions/workflows/test.yml)
[![Release](https://github.com/jxpeng98/obsidian-to-NotionNext/actions/workflows/release.yml/badge.svg)](https://github.com/jxpeng98/obsidian-to-NotionNext/actions/workflows/release.yml)
[![Obsidian Downloads](https://img.shields.io/badge/dynamic/json?logo=obsidian&color=%23483699&label=downloads&query=%24%5B%22share-to-notionnext%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json)](https://GitHub.com/jxpeng98/obsidian-to-NotionNext/releases/)
[![GitHub release (with filter)](https://img.shields.io/github/package-json/v/jxpeng98/obsidian-to-NotionNext)](https://github.com/jxpeng98/obsidian-to-NotionNext/releases/)
使用 Notion API 将文件从 Obsidian 分享到任何 Notion 数据库。此插件支持通用 Notion 数据库、NotionNext 博客模板和完全自定义的数据库模式。
**详细说明请访问[文档网站](https://obsidian-to-notionnext.pages.dev/)。**
## 功能特性
- **多种数据库类型**支持通用、NotionNext 和自定义数据库。
- **自定义属性**:在自定义数据库中,可将任何 frontmatter 键映射到任何 Notion 属性。
- **灵活同步**:即时选择要同步到哪个数据库。
## 致谢
此插件是 EasyChris 的原创作品 [obsidian-to-notion](https://github.com/EasyChris/obsidian-to-notion) 的一个分支,增加了新功能并支持更灵活的数据库配置。

View File

@@ -1,66 +1,21 @@
Obsidian share to Notion # Obsidian to NotionNext
将obsidian文件一键分享到Notion,并在obsidian中添加Notion分享链接 [![Test](https://github.com/jxpeng98/obsidian-to-NotionNext/actions/workflows/test.yml/badge.svg)](https://github.com/jxpeng98/obsidian-to-NotionNext/actions/workflows/test.yml)
[![Release](https://github.com/jxpeng98/obsidian-to-NotionNext/actions/workflows/release.yml/badge.svg)](https://github.com/jxpeng98/obsidian-to-NotionNext/actions/workflows/release.yml)
[![Obsidian Downloads](https://img.shields.io/badge/dynamic/json?logo=obsidian&color=%23483699&label=downloads&query=%24%5B%22share-to-notionnext%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json)](https://GitHub.com/jxpeng98/obsidian-to-NotionNext/releases/)
[![GitHub release (with filter)](https://img.shields.io/github/package-json/v/jxpeng98/obsidian-to-NotionNext)](https://github.com/jxpeng98/obsidian-to-NotionNext/releases/)
![](./doc/1.gif) [中文版](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.
# 使用方式 **For detailed instructions, please visit the [documentation website](https://obsidian-to-notionnext.pages.dev/).**
## 安装插件
### BRAT
插件中中心搜索 BRAT
添加 `EasyChris/obsidian-to-notion` 到 BRAT 插件安装列表中
返回插件中心启用即可
### 手动安装
```
cd YOUR_OBSIDIAN_FOLDER/.obsidian/plugins/
git clone https://github.com/EasyChris/obsidian-to-notion.git
```
### 市场下载
因为暂时还未上架所以请通过BRAT安装
## 申请 Notion API ## Features
官方参考文档:[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`
![](https://files.readme.io/2ec137d-093ad49-create-integration.gif)
### 第2步与你的集成共享一个数据库 - **Multiple Database Types**: Supports General, NotionNext, and Custom databases.
新建一个的page权限为公开 - **Custom Properties**: Map any frontmatter key to any Notion property in custom databases.
在page中新建一个数据库 -> 需要`full page database` - **Flexible Syncing**: Choose which database to sync to on-the-fly.
![](./doc/3.gif)
`integration`添加到你的新建的数据库中 ## Acknowledgment
![](https://files.readme.io/0a267dd-share-database-with-integration.gif) 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.
### 第三步复制database ID
```
https://www.notion.so/myworkspace/a8aec43384f447ed84390e8e42c2e089?v=...
|--------- Database ID --------|
```
## 打开插件配置
将得到的 `NOTION_API_KEY``DATABASE_ID`填入配置当中
![](./doc/2.png)
## 上传文件内容到notion
点击上传notion的按钮
![](./doc/4.png)
上传成功之后会自动生成一个分享链接
![](./doc/5.png)
# 请我喝杯咖啡
[顿顿饭](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)

View File

@@ -1,76 +0,0 @@
import { requestUrl } from "obsidian";
import { Client } from "@notionhq/client";
import { markdownToBlocks, } from "@tryfabric/martian";
import * as fs from "fs";
import * as yamlFrontMatter from "yaml-front-matter";
import * as yaml from "yaml"
import MyPlugin from "main";
export class Upload2Notion {
app: MyPlugin;
notion: Client;
agent: any;
constructor(app: MyPlugin) {
this.app = app;
}
async createPage(title:string, childArr: any) {
const bodyString = {
parent: {
database_id: this.app.settings.databaseID
},
properties: {
Name: {
title: [
{
text: {
content: title,
},
},
],
},
},
children: childArr,
}
const response = await requestUrl({
url: `https://api.notion.com/v1/pages`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
// 'User-Agent': 'obsidian.md',
'Authorization': 'Bearer ' + this.app.settings.notionAPI,
'Notion-Version': '2021-08-16',
},
body: JSON.stringify(bodyString),
})
return response;
}
async syncMarkdownToNotion(title:string, markdown: string, fullPath:string): Promise<any> {
const yamlObj:any = yamlFrontMatter.loadFront(markdown);
const __content = yamlObj.__content
const file2md = markdownToBlocks(__content);
const res = await this.createPage(title, file2md);
if (res.status === 200) {
this.updateYamlInfo(markdown, fullPath, res)
}
return res
}
async updateYamlInfo(yamlContent: string, fullPath: string, res: any) {
const yamlObj:any = yamlFrontMatter.loadFront(yamlContent);
const {url} = res.json
yamlObj.link = url;
const __content = yamlObj.__content;
delete yamlObj.__content
const yamlhead = yaml.stringify(yamlObj)
// 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;
//write content fo file
fs.writeFileSync(fullPath, content);
return res
}
}

BIN
doc/1.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 552 KiB

BIN
doc/2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

BIN
doc/3.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

BIN
doc/4.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

BIN
doc/5.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

69
docs/.vitepress/config.ts Normal file
View 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));

View 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`.
![new-integration](https://r2imga.jxpeng.dev/2025/10/3d12358494c000ab7035820e1e836934.png)
3. Fill in the required information:
![integration-info](https://r2imga.jxpeng.dev/2025/10/645b8182105e5e64e61bd263394534d6.png)
4. Integration successfully created.
![success](https://r2imga.jxpeng.dev/2025/10/57ea92c48efc15b95a69f3663424f5ae.png)
5. Enter the "Configure integration settings" page and **prepare to copy the API Token,** which you will need in the plugin settings.
![configure-integration](https://r2imga.jxpeng.dev/2025/10/4a50e55c35e47f905fb7fe63af5c8402.png)
::: 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.
![create-new-page](https://r2imga.jxpeng.dev/2025/10/1f092ef1d8c5a25aeb75694d654810a1.png)
2. create a new page and select the `Database - Full page` option.
![new-database](https://r2imga.jxpeng.dev/2025/10/73b700fc65a53f2ff518b58832bf9757.png)
3. Click the `...` menu in the top-right corner of the database page, go to `Connections`, and connect your newly created integration.
![connect-integration](https://r2imga.jxpeng.dev/2025/10/e8d9be4718f6f5c76fdc3bee7d694d23.png)
4. Click `Share` -> `Publish` to publish the database to the web.
![publish](https://r2imga.jxpeng.dev/2025/10/4b1d3f597d67508ee67d20a62151200f.png)
5. Click and copy the **Database ID**.
![database-id](https://r2imga.jxpeng.dev/2025/10/d8f22c1daf5063305256e5e50210adf9.png)
Next, you can proceed to install and configure the plugin in Obsidian by following the [Installation Guide](./02-installation.md).

View 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`.
![obsidian](https://r2imga.jxpeng.dev/2025/10/6f80d5126391162dd7cc98de87d3c1b2.png)
2. Navigate to `Community plugins`.
![community-plugins](https://r2imga.jxpeng.dev/2025/10/adf7dd7d292dcc331d7625b8f409f222.png)
::: 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`.
![browse](https://r2imga.jxpeng.dev/2025/10/e4f170f966902999fe2137196d8b6f9c.png)
4. Search for "NotionNext" and click `Install`.
![install](https://r2imga.jxpeng.dev/2025/10/1b0fcf15bc80aeb3714d9c97cbc6d5b0.png)
5. After installation, enable the plugin.
![enable](https://r2imga.jxpeng.dev/2025/10/6c7a7ee7d6e5861a3f0867c826e3f2c0.png)
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.

112
docs/en/03-configuration.md Normal file
View File

@@ -0,0 +1,112 @@
---
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.
![open-plugin-settings](https://r2imga.jxpeng.dev/2025/10/d2cfc05a86a8f014354fbd4e8456ccdc.png)
Click "Add new database" to start creating a new database configuration.
![add-database](https://r2img.jxpeng.dev/2024/07/115f47b4180d04fb362b931f8092b5fb.png)
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)
## 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`.
![notion-general](https://r2imga.jxpeng.dev/2025/10/3abc05f71a1bd81bdc00fd988dd71108.png)
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.
![general](https://r2imga.jxpeng.dev/2025/10/9697de930e530c55081e07835e224782.png)
## 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.
![notionnext](https://r2imga.jxpeng.dev/2025/10/5b1b3b3fb4ef9a76f4fed26fcc6c426b.png)
## 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.
![custom](https://r2imga.jxpeng.dev/2025/10/667335d7fd2cfa96e307be3df79e157e.png)
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.
![title-property](https://r2imga.jxpeng.dev/2025/10/2ad52037476d3f86f3c4cfbed507f74b.png)
::: 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.
![property-1](https://r2imga.jxpeng.dev/2025/10/d4c0bcc3abbc741dd34ce22e589d6223.png)
For now, the plugin supports the following Notion property types:
- Text
- Number
- Select
- Multi-select
- Date
- Files & Media
- Checkbox
- URL
- Email
- Phone Number
![types](https://r2imga.jxpeng.dev/2025/10/81d321693dfd046ad3386e4507d60792.png)
::: 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.

92
docs/en/04-sync.md Normal file
View File

@@ -0,0 +1,92 @@
---
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.
## Auto Sync
The plugin supports automatic syncing that monitors your notes for changes and automatically syncs them to Notion.
### Enabling Auto Sync
1. Open the plugin settings
2. Find the "Auto Sync" toggle under General Settings
3. Enable the toggle
4. Configure the "Auto Sync Delay" (default: 5 seconds, minimum: 2 seconds)
### How Auto Sync Works
When auto sync is enabled:
- The plugin monitors markdown files for changes
- After you stop editing for the configured delay period, auto sync is triggered
- Only files that have already been synced to Notion (have a NotionID in frontmatter) will be auto-synced
- If a file is linked to multiple databases, it will sync to all of them automatically
### Auto Sync Scenarios
#### Scenario A: New Document (Not Yet Synced)
```yaml
---
title: My New Article
tags: [blog, tech]
---
```
**Behavior:**
- ✅ Detects no NotionID present
- ✅ Shows notice: "⚠️ Auto sync skipped: This document has not been synced to Notion, please upload manually first"
- ✅ No sync operation performed
- 📝 **Action Required:** Manually sync the document first using the command palette
#### Scenario B: Synced to One Database
```yaml
---
title: My Article
NotionID-blog: abc123
---
```
**Behavior:**
- ✅ Detects 1 NotionID
- ✅ Automatically syncs to the Blog database
- ✅ Shows success/failure notification from the upload command
- 📝 **No Action Required:** Changes are automatically synced
#### Scenario C: Synced to Multiple Databases
```yaml
---
title: My Article
NotionID-blog: abc123
NotionID-portfolio: def456
NotionID-notes: ghi789
---
```
**Behavior:**
- ✅ Detects 3 NotionIDs
- ✅ Shows notice: "🔄 Auto sync: Syncing to 3 database(s)..."
- ✅ Syncs to all 3 databases sequentially
- ✅ Shows individual result notifications for each database
- 📝 **No Action Required:** Changes are automatically synced to all linked databases
### Auto Sync Best Practices
1. **First Sync Manually**: Always perform the first sync manually to establish the NotionID link
2. **Configure Delay Appropriately**: Set a longer delay (5-10 seconds) if you make frequent edits
3. **Monitor Sync Status**: Check the notifications to ensure syncs complete successfully
4. **Check Logs**: Open the developer console (Ctrl+Shift+I / Cmd+Option+I) to view detailed sync logs
### Troubleshooting
Having issues with auto sync? Check the [Troubleshooting Guide](05-troubleshooting.md) for detailed solutions to common problems.

View File

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

8
docs/en/06-changelog.md Normal file
View File

@@ -0,0 +1,8 @@
---
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.

10
docs/en/07-roadmap.md Normal file
View File

@@ -0,0 +1,10 @@
---
title: Roadmap
description: Planned features and improvements for Obsidian to NotionNext
---
# Roadmap
- [ ] Automatic Syncing: Implement automatic syncing of notes at regular intervals or upon changes.
- [ ] 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View 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`
![new-integration](https://r2imga.jxpeng.dev/2025/10/3d12358494c000ab7035820e1e836934.png)
3. 填写一些基本信息:
![integration-info](https://r2imga.jxpeng.dev/2025/10/645b8182105e5e64e61bd263394534d6.png)
4. 成功创建集成。
![success](https://r2imga.jxpeng.dev/2025/10/57ea92c48efc15b95a69f3663424f5ae.png)
5. 进入“配置集成设置”页面,**准备好复制 API 令牌**,你很快就会在插件设置中用到它。
![configure-integration](https://r2imga.jxpeng.dev/2025/10/4a50e55c35e47f905fb7fe63af5c8402.png)
::: warning
**⚠️ 注意:** 请妥善保管你的 API 令牌,不要与他人分享,因为它授予了访问你 Notion 数据的权限。
:::
## 创建 Notion 数据库并连接集成
接下来,你需要创建一个 Notion 数据库,用于存放从 Obsidian 同步过来的笔记。
1. 在你的 Notion 工作区中,点击 `Create a new page` 来创建一个新页面。
![create-new-page](https://r2imga.jxpeng.dev/2025/10/1f092ef1d8c5a25aeb75694d654810a1.png)
2. 创建一个新页面后,选择 `Database - Full page` 选项。
![new-database](https://r2imga.jxpeng.dev/2025/10/73b700fc65a53f2ff518b58832bf9757.png)
3. 点击数据库页面右上角的 `...` 菜单,进入 `Connections`,然后连接你刚刚创建的集成。
![connect-integration](https://r2imga.jxpeng.dev/2025/10/e8d9be4718f6f5c76fdc3bee7d694d23.png)
4. 点击 `Share` -> `Publish`,将数据库发布到网络。
![publish](https://r2imga.jxpeng.dev/2025/10/4b1d3f597d67508ee67d20a62151200f.png)
5. 点击并复制 **数据库 ID**
![database-id](https://r2imga.jxpeng.dev/2025/10/d8f22c1daf5063305256e5e50210adf9.png)
接下来,你可以根据[安装指南](./02-installation.md)在 Obsidian 中安装和配置插件了。

View File

@@ -0,0 +1,28 @@
---
title: 安装插件
description: 如何安装 Obsidian to NotionNext 插件
---
# 安装插件
当你在 Notion 端完成上一章节的设置后,就可以在 Obsidian 里安装和配置插件了。
1. 打开 Obsidian 的 `设置`
![obsidian](https://r2imga.jxpeng.dev/2025/10/6f80d5126391162dd7cc98de87d3c1b2.png)
2. 找到 `第三方插件` 菜单。
![community-plugins](https://r2imga.jxpeng.dev/2025/10/adf7dd7d292dcc331d7625b8f409f222.png)
::: tip
如果你之前没有启用过第三方插件,可能需要先关闭 `安全模式`(点击 `打开社区插件`)。
:::
3. 进入 `社区插件` -> `浏览`
![browse](https://r2imga.jxpeng.dev/2025/10/e4f170f966902999fe2137196d8b6f9c.png)
4. 搜索 "NotionNext" 并点击 `安装`
![install](https://r2imga.jxpeng.dev/2025/10/1b0fcf15bc80aeb3714d9c97cbc6d5b0.png)
5. 安装完成后,记得 `启用` 插件。
![enable](https://r2imga.jxpeng.dev/2025/10/6c7a7ee7d6e5861a3f0867c826e3f2c0.png)
启用后,你就可以在插件设置列表中找到 "Share to NotionNext",并在下一步开始[配置你的数据库](./03-configuration.md)了。

112
docs/zh/03-configuration.md Normal file
View File

@@ -0,0 +1,112 @@
---
title: 配置数据库
description: 在 Obsidian to NotionNext 插件中配置 Notion 数据库的指南
---
# 配置数据库
在安装好插件并获取了 Notion API 令牌和数据库 ID 之后,你就可以开始配置要同步的数据库了。
首先,在 Obsidian 中打开插件设置。
![open-plugin-settings](https://r2imga.jxpeng.dev/2025/10/d2cfc05a86a8f014354fbd4e8456ccdc.png)
点击 “添加新数据库” 来创建一个新的数据库配置。
![add-database](https://r2img.jxpeng.dev/2024/07/115f47b4180d04fb362b931f8092b5fb.png)
插件支持三种类型的数据库配置,你可以根据自己的需求选择。
- [数据库配置](#数据库配置)
- [1⃣ 通用数据库](#1⃣-通用数据库)
- [2⃣ NotionNext 数据库](#2⃣-notionnext-数据库)
- [3⃣ 自定义数据库](#3⃣-自定义数据库)
- [完成配置](#完成配置)
## 1⃣ 通用数据库
这是最基础的数据库类型,适合大多数用户。
**配置项:**
- **数据库全名**: 给这个数据库配置起一个容易识别的名字。
- **Notion API 令牌**: 你在第一步中获取的 API 令牌。
- **数据库 ID**: 你在第二步中获取的数据库 ID。
**Notion 数据库要求:**
- 必须包含一个名为 `title``Title` 属性。
- 必须包含一个名为 `tags``Multi-select` 属性。
![notion-general](https://r2imga.jxpeng.dev/2025/10/3abc05f71a1bd81bdc00fd988dd71108.png)
然后,像下面这样填写配置字段:
- **数据库全名 (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。
![general](https://r2imga.jxpeng.dev/2025/10/9697de930e530c55081e07835e224782.png)
## 2⃣ NotionNext 数据库
这个类型是为 [NotionNext](https://github.com/tangly1024/NotionNext) 博客系统的用户量身定制的,其属性已预设好以匹配 NotionNext 模板。
配置方式与通用数据库类似:
- **数据库全名**: 为这个数据库配置起一个名字。
- **数据库简称**: 一个简短的别名。
- **Notion API 令牌**: 你在第一步中获取的 API 令牌。
- **数据库 ID**: 你在第二步中获取的数据库 ID。
![notionnext](https://r2imga.jxpeng.dev/2025/10/5b1b3b3fb4ef9a76f4fed26fcc6c426b.png)
## 3⃣ 自定义数据库
这个类型为高级用户提供了极大的灵活性,允许你将 Obsidian 笔记中的任何 frontmatter 元数据映射到 Notion 数据库的任意属性。
首先,你需要像配置通用数据库一样填写基本信息:
- **数据库全名**: 为这个数据库配置起一个名字。
- **数据库简称**: 一个简短的别名。
- **Notion API 令牌**: 你在第一步中获取的 API 令牌。
- **数据库 ID**: 你在第二步中获取的数据库 ID。
![custom](https://r2imga.jxpeng.dev/2025/10/667335d7fd2cfa96e307be3df79e157e.png)
填写完基本配置后,你就可以开始添加新的属性映射了。
你可以点击 `Add New Property` 按钮来创建一个新的属性映射。
首先,你需要添加标题属性的映射,这是 Notion 数据库正常工作所必需的。
![title-property](https://r2imga.jxpeng.dev/2025/10/2ad52037476d3f86f3c4cfbed507f74b.png)
::: tip
请记住,"Notion 属性 (Notion Property)" 需要填写你在 Notion 数据库中的属性名称(区分大小写),而 "Frontmatter 键 (Frontmatter Key)" 则是你 Obsidian 笔记 frontmatter 中的键。
:::
添加完标题属性后,你可以根据需要继续添加更多属性。
![property-1](https://r2imga.jxpeng.dev/2025/10/d4c0bcc3abbc741dd34ce22e589d6223.png)
目前,插件支持以下 Notion 属性类型:
- Text
- Number
- Select
- Multi-select
- Date
- Files & Media
- Checkbox
- URL
- Email
- Phone Number
![types](https://r2imga.jxpeng.dev/2025/10/81d321693dfd046ad3386e4507d60792.png)
::: info
关联Relation和汇总Rollup类型暂时还不支持。
:::
## 完成配置
配置完数据库后,请确保保存你的设置。现在,你可以通过命令面板或笔记右键菜单中的 “Share to NotionNext” 命令,开始将你的 Obsidian 笔记同步到配置好的 Notion 数据库了。

96
docs/zh/04-sync.md Normal file
View File

@@ -0,0 +1,96 @@
---
title: 同步笔记
description: 如何使用 NotionNext 插件将你的 Obsidian 笔记同步到 Notion
---
# 同步笔记
在插件设置中配置好你的 Notion 数据库后,你就可以开始将 Obsidian 笔记同步到 Notion 了。
## 手动同步
要同步一篇笔记,只需打开你想要同步的笔记,然后从命令面板(`Ctrl/Cmd + P`)或笔记的右键菜单中选择 "Share to NotionNext" 命令。这会在你的 Notion 数据库中创建一个新页面,内容与你的 Obsidian 笔记完全一致。
## 自动同步
插件支持自动同步功能,可以监控你的笔记变化并自动同步到 Notion。
### 启用自动同步
1. 打开插件设置
2. 在通用设置下找到"自动同步"开关
3. 开启该开关
4. 配置"自动同步延迟时间"默认5秒最小2秒
### 自动同步工作原理
当自动同步启用后:
- 插件会监控 Markdown 文件的变化
- 在你停止编辑达到配置的延迟时间后,自动触发同步
- 只有已经同步过的文件frontmatter 中有 NotionID才会被自动同步
- 如果文件关联了多个数据库,会自动同步到所有数据库
### 自动同步场景示例
#### 场景 A新文档未同步
```yaml
---
title: 我的新文章
tags: [博客, 技术]
---
```
**行为:**
- ✅ 检测到没有 NotionID
- ✅ 显示提示:"⚠️ 自动同步跳过:此文档未同步到 Notion请先手动上传"
- ✅ 不执行同步操作
- 📝 **需要操作:** 先使用命令面板手动同步文档
#### 场景 B已同步到一个数据库
```yaml
---
title: 我的文章
NotionID-blog: abc123
---
```
**行为:**
- ✅ 检测到 1 个 NotionID
- ✅ 自动同步到 Blog 数据库
- ✅ 显示上传命令返回的成功/失败通知
- 📝 **无需操作:** 变更会自动同步
#### 场景 C同步到多个数据库
```yaml
---
title: 我的文章
NotionID-blog: abc123
NotionID-portfolio: def456
NotionID-notes: ghi789
---
```
**行为:**
- ✅ 检测到 3 个 NotionID
- ✅ 显示提示:"🔄 自动同步:正在同步到 3 个数据库..."
- ✅ 依次同步到所有 3 个数据库
- ✅ 为每个数据库显示独立的结果通知
- 📝 **无需操作:** 变更会自动同步到所有关联的数据库
### 自动同步最佳实践
1. **首次手动同步**:始终先手动执行第一次同步以建立 NotionID 链接
2. **合理配置延迟**如果你经常编辑设置较长的延迟时间5-10 秒)
3. **监控同步状态**:注意查看通知以确保同步成功完成
4. **查看日志**打开开发者控制台Ctrl+Shift+I / Cmd+Option+I查看详细的同步日志
### 故障排除
遇到自动同步问题?请查看[问题排查指南](05-troubleshooting.md)获取常见问题的详细解决方案。

View File

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

6
docs/zh/06-changelog.md Normal file
View File

@@ -0,0 +1,6 @@
---
title: 更新日志
description: Obsidian to NotionNext 的版本更新与变更记录
---
欢迎来到 Obsidian to NotionNext 的更新日志!这里会记录自 `2.7.0` 版本起的所有更新、改进以及问题修复,方便你快速了解插件的演进情况。

8
docs/zh/07-roadmap.md Normal file
View File

@@ -0,0 +1,8 @@
---
title: 路线图
description: Obsidian to NotionNext 的规划功能与未来改进
---
- [ ] 自动同步:支持按时间间隔或内容变更自动同步笔记。
- [ ] 附件支持:允许将 Obsidian 笔记中的附件一同上传到 Notion。
- [ ] 关联属性:实现将 Obsidian 链接映射到 Notion 的 relation 属性。

27
docs/zh/index.md Normal file
View 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)

View File

@@ -1,52 +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({
esbuild.build({
banner: { banner: {
js: banner, js: banner,
}, },
entryPoints: ['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', ],
watch: !prod, 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",
}).catch(() => process.exit(1)); });
if (!prod) {
await ctx.watch();
} else {
await ctx.rebuild();
ctx.dispose().then(
(r) => console.log("Build completed"),
(e) => console.error(e),
);
}

17
icon.ts
View File

@@ -1,17 +0,0 @@
import { addIcon } from 'obsidian';
const icons: Record<string, string> = {
'notion-logo': `
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100px" height="100px" viewBox="0 0 100 100" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:evenodd;fill:currentColor;fill-opacity:1;" d="M 18.738281 17.796875 C 21.832031 20.308594 22.996094 20.117188 28.804688 19.730469 L 83.582031 16.441406 C 84.746094 16.441406 83.78125 15.28125 83.390625 15.089844 L 74.292969 8.511719 C 72.550781 7.160156 70.230469 5.609375 65.777344 5.996094 L 12.734375 9.867188 C 10.800781 10.058594 10.414062 11.023438 11.183594 11.800781 Z M 22.027344 30.5625 L 22.027344 88.199219 C 22.027344 91.296875 23.574219 92.457031 27.058594 92.265625 L 87.261719 88.78125 C 90.746094 88.589844 91.136719 86.457031 91.136719 83.941406 L 91.136719 26.691406 C 91.136719 24.179688 90.167969 22.824219 88.035156 23.019531 L 25.125 26.691406 C 22.800781 26.886719 22.027344 28.046875 22.027344 30.5625 Z M 81.460938 33.652344 C 81.84375 35.398438 81.460938 37.136719 79.714844 37.332031 L 76.8125 37.910156 L 76.8125 80.460938 C 74.292969 81.816406 71.972656 82.589844 70.035156 82.589844 C 66.9375 82.589844 66.164062 81.621094 63.839844 78.722656 L 44.867188 48.9375 L 44.867188 77.753906 L 50.871094 79.109375 C 50.871094 79.109375 50.871094 82.589844 46.027344 82.589844 L 32.675781 83.363281 C 32.285156 82.589844 32.675781 80.65625 34.027344 80.269531 L 37.511719 79.304688 L 37.511719 41.199219 L 32.675781 40.8125 C 32.285156 39.070312 33.253906 36.558594 35.964844 36.363281 L 50.292969 35.398438 L 70.035156 65.570312 L 70.035156 38.878906 L 65.003906 38.300781 C 64.617188 36.171875 66.164062 34.625 68.097656 34.433594 Z M 8.28125 4.644531 L 63.453125 0.582031 C 70.230469 0 71.972656 0.390625 76.230469 3.484375 L 93.84375 15.859375 C 96.75 17.988281 97.71875 18.570312 97.71875 20.890625 L 97.71875 88.78125 C 97.71875 93.035156 96.167969 95.550781 90.75 95.9375 L 26.675781 99.804688 C 22.609375 100 20.671875 99.421875 18.542969 96.710938 L 5.574219 79.882812 C 3.25 76.785156 2.28125 74.46875 2.28125 71.757812 L 2.28125 11.410156 C 2.28125 7.933594 3.832031 5.03125 8.28125 4.644531 Z M 8.28125 4.644531 "/>
</g>
</svg>
`
};
export const addIcons = (): void => {
Object.keys(icons).forEach((key) => {
addIcon(key, icons[key]);
});
};

14931
main.js

File diff suppressed because one or more lines are too long

190
main.ts
View File

@@ -1,190 +0,0 @@
import { timingSafeEqual } from "crypto";
import {
App,
EditableFileView,
requestUrl,
Editor,
MarkdownView,
Modal,
Notice,
Plugin,
PluginSettingTab,
Setting,
normalizePath
} from "obsidian";
import { join } from "path";
import * as fs from "fs";
import {addIcons} from 'icon';
import { Upload2Notion } from "Upload2Notion";
// Remember to rename these classes and interfaces!
interface MyPluginSettings {
notionAPI: string;
databaseID: string;
proxy: string;
}
const DEFAULT_SETTINGS: MyPluginSettings = {
notionAPI: "",
databaseID: "",
proxy: "",
};
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
async onload() {
await this.loadSettings();
addIcons();
// This creates an icon in the left ribbon.
const ribbonIconEl = this.addRibbonIcon(
"notion-logo",
"Share to notion",
async (evt: MouseEvent) => {
// Called when the user clicks the icon.
this.upload();
}
);
// Perform additional things with the ribbon
ribbonIconEl.addClass("my-plugin-ribbon-class");
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
const statusBarItemEl = this.addStatusBarItem();
// statusBarItemEl.setText("share to notion");
this.addCommand({
id: "share-to-notion",
name: "share to notion",
editorCallback: async (editor: Editor, view: MarkdownView) => {
this.upload()
},
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new SampleSettingTab(this.app, this));
}
onunload() {}
async upload(){
const { notionAPI, databaseID } = this.settings;
if (notionAPI === "" || databaseID === "") {
new Notice(
"Please set up the notion API and database ID in the settings tab."
);
return;
}
const { nowFile, fileData,fullPath } =
await this.getNowFileMarkdwonContent(this.app);
const { basename } = nowFile;
if (fileData) {
const upload = new Upload2Notion(this);
const res = await upload.syncMarkdownToNotion(basename,fileData, fullPath)
console.log(res)
if(res.status === 200){
new Notice(`${basename} 同步成功`)
}else {
new Notice(`${basename} 同步失败`)
}
}
}
async getNowFileMarkdwonContent(app: App) {
const nowFile = app.workspace.getActiveFile();
if (nowFile) {
const filePath: string = nowFile.path;
// @ts-ignore
const basePath: string = nowFile.vault.adapter.basePath;
const fullPath = normalizePath(join(basePath, filePath));
console.log("fullpath", fullPath);
const fileData = fs.readFileSync(fullPath, "utf8");
return {
fileData,
nowFile,
fullPath,
};
} else {
new Notice("请打开需要同步的文件");
return;
}
}
async loadSettings() {
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class SampleModal extends Modal {
constructor(app: App) {
super(app);
}
onOpen() {
const { contentEl } = this;
contentEl.setText("Woah!");
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h2", {
text: "Settings for obsidian to notion plugin.",
});
new Setting(containerEl)
.setName("Notion API Token")
.setDesc("It's a secret")
.addText((text) =>
text
.setPlaceholder("Enter your Notion API Token")
.setValue(this.plugin.settings.notionAPI)
.onChange(async (value) => {
console.log("Secret: " + value);
this.plugin.settings.notionAPI = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Database ID")
.setDesc("It's a secret")
.addText((text) =>
text
.setPlaceholder("Enter your Database ID")
.setValue(this.plugin.settings.databaseID)
.onChange(async (value) => {
console.log("Secret: " + value);
this.plugin.settings.databaseID = value;
await this.plugin.saveSettings();
})
);
}
}

View File

@@ -1,10 +1,10 @@
{ {
"id": "obsidian-to-notion", "id": "share-to-notionnext",
"name": "Obsidian shared to Notion", "name": "Share to NotionNext",
"version": "0.3.0", "version": "2.7.0",
"minAppVersion": "0.0.1", "minAppVersion": "0.0.1",
"description": "This is a plugin for Obsidian. This plugin share obsidian md file to notion with notion api", "description": "Shares obsidian md file to notion with notion api for NotionNext web deploy, originally created by EasyChris/obsidian-to-notion.",
"author": "Easychris", "author": "EasyChris, jxpeng98",
"authorUrl": "https://github.com/EasyChris/obsidian-to-notion", "authorUrl": "https://github.com/jxpeng98",
"isDesktopOnly": false "isDesktopOnly": false
} }

26
obsidian_template.md Normal file
View File

@@ -0,0 +1,26 @@
---
# 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: 📎 # 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 默认是空
status: 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

View File

@@ -1,32 +1,33 @@
{ {
"name": "obsidian-to-notion", "name": "share-to-notionnext",
"version": "0.3.0", "version": "2.7.0",
"type": "module", "type": "module",
"description": "This is a plugin for Obsidian. This plugin share obsidian md file to notion with notion api", "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"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "Jiaxin PENG",
"license": "GNU GPLv3", "license": "GNU GPLv3",
"devDependencies": { "devDependencies": {
"@types/node": "^17.0.35", "@types/node": "^20.5.7",
"@types/yaml-front-matter": "^4.1.0", "@types/yaml-front-matter": "^4.1.3",
"@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/eslint-plugin": "^6.16.0",
"@typescript-eslint/parser": "^5.2.0", "@typescript-eslint/parser": "^6.16.0",
"builtin-modules": "^3.2.0", "builtin-modules": "^3.3.0",
"esbuild": "0.13.12", "esbuild": "0.19.5",
"obsidian": "latest", "obsidian": "^1.6.6",
"tslib": "2.3.1", "tslib": "2.6.2",
"typescript": "4.4.4" "typescript": "5.2.2"
}, },
"dependencies": { "dependencies": {
"@tryfabric/martian": "^1.2.0", "@jxpeng98/martian": "^1.3.1",
"https-proxy-agent": "^5.0.1", "https-proxy-agent": "^7.0.2",
"yaml": "^2.1.0", "remark-math": "^6.0.0",
"yaml-front-matter": "^4.1.1" "yaml": "^2.3.4",
} "yaml-front-matter": "^4.1.1"
}
} }

2213
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
release.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
node update-version.js $1
git add .
git commit -m "Update version to $1"
git tag -a $1 -m $1
git push origin $1

View File

@@ -0,0 +1,51 @@
import { FuzzySuggestModal, FuzzyMatch } from 'obsidian';
import MyPlugin from "../main";
import { i18nConfig } from "../lang/I18n";
/**
* Simple interface for what should be displayed and stored for suggester
*/
export interface DatabaseList {
name: string, // specific database name
match: any //
}
export class FuzzySuggester extends FuzzySuggestModal<DatabaseList> {
private plugin: MyPlugin;
private data: DatabaseList[];
private callback: any;
constructor(plugin: MyPlugin) {
super(plugin.app);
this.plugin = plugin;
this.setPlaceholder(i18nConfig.PlaceHolder);
}
setSuggesterData(suggesterData: Array<DatabaseList>): void { this.data = suggesterData }
async display(callBack: (item: DatabaseList, evt: MouseEvent | KeyboardEvent) => void): Promise<any> {
this.callback = callBack;
this.open();
}
// Store the data
getItems(): DatabaseList[] {
return this.data
}
getItemText(item: DatabaseList): string {
return item.name
}
onChooseItem(item: DatabaseList, evt: MouseEvent | KeyboardEvent): void { }
onChooseSuggestion(item: FuzzyMatch<DatabaseList>, evt: MouseEvent | KeyboardEvent): void {
this.callback(item.item, evt)
}
renderSuggestion(item: FuzzyMatch<DatabaseList>, el: HTMLElement): void {
el.createEl('div', { text: item.item.name })
}
}

View File

@@ -0,0 +1,107 @@
import { i18nConfig } from "src/lang/I18n";
import { Editor, MarkdownView, setTooltip } from "obsidian";
import { FuzzySuggester, DatabaseList } from "./FuzzySuggester";
import { uploadCommandCustom, uploadCommandGeneral, uploadCommandNext } from "../upload/uploadCommand";
import ObsidianSyncNotionPlugin from "src/main";
import { DatabaseDetails } from "../ui/settingTabs";
interface Command {
id: string;
name: string;
editorCallback: (editor: Editor, view: MarkdownView) => Promise<void>;
}
// create the commands list
export default class RibbonCommands {
plugin: ObsidianSyncNotionPlugin;
Ncommand: Command[] = [];
constructor(plugin: ObsidianSyncNotionPlugin) {
this.plugin = plugin;
// iterate through the database detail
for (let key in this.plugin.settings.databaseDetails) {
let dbDetails = this.plugin.settings.databaseDetails[key];
this.addCommandForDatabase(dbDetails);
}
// Register all the commands
this.Ncommand.forEach(command => {
this.plugin.addCommand(
{
id: command.id,
name: command.name,
editorCallback: command.editorCallback,
}
);
});
}
async ribbonDisplay() {
const NcommandList: DatabaseList[] = [];
this.Ncommand.map(command => NcommandList.push(
{
name: command.name,
match: command.editorCallback
}
)
);
const fusg = new FuzzySuggester(this.plugin);
fusg.setSuggesterData(NcommandList);
await fusg.display(async (results) => { await results.match() })
};
// if the setting has been changed, try to rebuild the command list
async updateCommand() {
this.Ncommand = [];
for (let key in this.plugin.settings.databaseDetails) {
let dbDetails = this.plugin.settings.databaseDetails[key];
this.addCommandForDatabase(dbDetails);
}
this.Ncommand.forEach(command => {
this.plugin.addCommand(
{
id: command.id,
name: command.name,
editorCallback: command.editorCallback,
}
);
});
}
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 });
}
}

49
src/lang/I18n.ts Normal file
View File

@@ -0,0 +1,49 @@
import {en} from "./locale/en";
import {zh} from "./locale/zh";
import {ja} from "./locale/ja";
export const I18n: {[key: string]:any} = {
en,
zh,
ja,
};
class I18nManager {
private currentLanguage: string;
constructor() {
this.currentLanguage = this.detectLanguage();
}
// 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();

111
src/lang/locale/en.ts Normal file
View File

@@ -0,0 +1,111 @@
export const en = {
databaseFormat: "Database Format",
databaseFormatDesc: "Select the database format you want to sync to NotionNext or General",
databaseNext: "NotionNext",
databaseGeneral: "General",
databaseCustom: "Custom",
databaseFullName: "Database Full Name",
databaseFullNameDesc: "Please give a full name for your database",
databaseFullNameText: "Enter your database full name",
databaseAbbreviateName: "Database Abbreviate Name",
databaseAbbreviateNameDesc: "Please give a nick name for your database",
databaseAbbreviateNameText: "Enter your database nick name",
ribbonIcon: "Share to NotionNext",
GeneralSetting: "General information Settings",
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",
DatabaseIDDesc: "Collect from the top-right Share --> Publish",
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://abc.com/b.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",
NotionLinkDisplay: "Notion Link Display",
NotionLinkDisplayDesc: "Default is ON, if you want to hide the link in the front matter, please turn it off",
AutoSync: "Auto Sync",
AutoSyncDesc: "Automatically sync to Notion when frontmatter or content is modified (requires existing NotionID)",
AutoSyncDelay: "Auto Sync Delay (seconds)",
AutoSyncDelayDesc: "How many seconds to wait after document modification before triggering auto sync (default: 5 seconds, minimum: 2 seconds)",
AutoSyncDelayText: "Enter delay in seconds",
NotionGeneralSettingHeader: "General Notion Database Settings",
NotionGeneralButton: "Notion General Sync",
NotionGeneralButtonDesc: "Open this option, Sync to Notion General Database command will be displayed in the command palette (default: ON)",
NotionTagButton: "Notion Tags Sync",
NotionTagButtonDesc: "Sync Tags to Notion General Database (default: ON)",
NotionCustomTitle: "Customise title property",
NotionCustomTitleDesc: "Modify the column name of the Notion database (default: OFF)",
NotionCustomTitleName: "Preferred title name",
NotionCustomTitleNameDesc: "Enter the preferred title name for the first column of the Notion database (default: title)",
NotionCustomTitleText: "Enter the name",
NotionCustomValues: "Customise values property",
NotionCustomValuesDesc: "Modify the column name of the Notion databaseone per line",
NotionCustomValuesText: "Enter all properties that you want to sync",
NotYetFinish: "Not finished. This function will be available in the next version",
PlaceHolder: "Enter database Name",
"notion-logo": "Share to NotionNext",
"sync-preffix": "Sync to ",
"sync-success": "success",
"sync-fail": "failed",
"open-notion": "Please open the file that needs to be synchronized",
"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",
CustomPropertyName: "Property Name",
CustomPropertyFirstColumn: "Title Column",
CustomPropertyFirstColumnDesc: "The title of the page, must be the first property",
CustomProperty: "Property",
AddCustomProperty: "Add Custom Property",
AddNewProperty: "Add New Property",
AddNewPropertyDesc: "Add new property match with your notion database",
CopyErrorMessage: "Auto copy failed, please copy it manually",
BlockUploaded: "All blocks uploaded",
ExtraBlockUploaded: "Extra blocks uploaded",
CheckConsole: "Check the console for more information \n opt+cmd+i/ctrl+shift+i",
SettingsMigrated: "✨ Plugin settings updated! Auto sync feature added, check plugin settings",
AutoSyncNoNotionID: "⚠️ Auto sync skipped: This document has not been synced to Notion, please upload manually first",
AutoSyncMultipleSync: "🔄 Auto sync: Syncing to {count} database(s)...",
AutoSyncFailed: "Auto sync to {database} failed: {error}",
AutoSyncError: "Auto sync failed for {filename}: {error}",
"reach-mobile-limit": "The number of blocks exceeds the limit of 100, please use the desktop plugin",
StartUpload: "Start upload {filename}",
AddNewDatabase: "Add New Database",
AddNewDatabaseDesc: "Add a new database configuration",
AddNewDatabaseTooltip: "Add New Database",
EditDatabase: "Edit Database",
Preview: "Preview",
DatabaseFormatLabel: "Database Format",
DatabaseFullNameLabel: "Database Full Name",
DatabaseAbbreviateNameLabel: "Database Abbreviate Name",
NotionAPILabel: "Notion API Key",
DatabaseIDLabel: "Database ID",
ToggleAPIKeyVisibility: "Toggle API Key Visibility",
CopyAPIKey: "Copy API Key",
APIKeyCopied: "API Key copied to clipboard",
ToggleDatabaseIDVisibility: "Toggle Database ID Visibility",
CopyDatabaseID: "Copy Database ID",
DatabaseIDCopied: "Database ID copied to clipboard",
AddNewDatabaseModal: "Add new database",
}

103
src/lang/locale/ja.ts Normal file
View File

@@ -0,0 +1,103 @@
export const ja = {
databaseFormat: "データベース形式",
databaseFormatDesc: "同期したいデータベース形式を選択してください",
databaseNext: "NotionNext",
databaseGeneral: "一般的なNotion",
databaseCustom: "カスタム",
databaseFullName: "データベースの全称",
databaseFullNameDesc: "データベースの全称を入力してください",
databaseFullNameText: "データベースの全称を入力",
databaseAbbreviateName: "データベースの略称",
databaseAbbreviateNameDesc: "データベースの略称を入力してください",
databaseAbbreviateNameText: "データベースの略称を入力",
ribbonIcon: "NotionNextで共有",
GeneralSetting: "一般設定",
CommandID: "share-to-notionnext",
CommandName: "NotionNextデータベースに共有",
CommandIDGeneral: "share-to-notion",
CommandNameGeneral: "一般的なNotionデータベースに共有",
NotionNextButton: "NotionNext同期",
NotionNextButtonDesc: "このオプションを開くと、NotionNext同期コマンドがコマンドパレットに表示されますデフォルトON",
NotionNextSettingHeader: "NotionNextデータベース設定",
NotionAPI: "Notion API トークン",
NotionAPIDesc: "https://www.notion.so/my-integrations から生成してください",
NotionAPIText: "Notion API トークンを入力",
DatabaseID: "データベースID",
DatabaseIDDesc: "右上の共有 --> 公開から取得してください",
DatabaseIDText: "データベースIDを入力",
BannerUrl: "バナーのURL任意",
BannerUrlDesc: "デフォルトは空白です。バナーを表示したい場合は、URLを入力してくださいhttps://abc.com/b.png",
BannerUrlText: "バナーのURLを入力",
NotionUser: "Notion IDユーザー名、任意",
NotionUserDesc: "共有リンクから取得https://username.notion.site。Notion IDは[username]です",
NotionUserText: "Notion IDを入力",
NotionLinkDisplay: "Notionリンク表示",
NotionLinkDisplayDesc: "デフォルトはONです。front matterにリンクを非表示にしたい場合は、オフにしてください",
AutoSync: "自動同期",
AutoSyncDesc: "frontmatter またはコンテンツが変更されたときに自動的に Notion に同期しますNotionID が必要)",
AutoSyncDelay: "自動同期遅延時間(秒)",
AutoSyncDelayDesc: "ドキュメントの変更後、自動同期をトリガーするまでの待機時間デフォルト5秒、最小2秒",
AutoSyncDelayText: "遅延秒数を入力",
NotionGeneralSettingHeader: "一般的なNotionデータベース設定",
NotionGeneralButton: "一般的なNotion同期",
NotionGeneralButtonDesc: "このオプションを開くと、一般的なNotionデータベース同期コマンドがコマンドパレットに表示されますデフォルトON",
NotionTagButton: "Notionタグ同期",
NotionTagButtonDesc: "タグを一般的なNotionデータベースに同期デフォルトON",
NotionCustomTitle: "タイトルのカスタマイズ",
NotionCustomTitleDesc: "Notionデータベースの列名を変更デフォルトOFF",
NotionCustomTitleName: "希望のタイトル名",
NotionCustomTitleNameDesc: "Notionデータベースの最初の列のための希望のタイトル名を入力デフォルトtitle",
NotionCustomTitleText: "名前を入力",
NotionCustomValues: "値のカスタマイズ",
NotionCustomValuesDesc: "Notionデータベースの列名を変更、1行に1つ",
NotionCustomValuesText: "同期したいすべてのプロパティを入力",
NotYetFinish: "未完了。この機能は次のバージョンで利用可能になります",
PlaceHolder: "データベース名を入力",
"notion-logo": "NotionNextで共有",
"sync-success": "NotionNextへの同期に成功\n",
"sync-fail": "NotionNextへの同期に失敗\n",
"open-notion": "同期が必要なファイルを開いてください",
"config-secrets-notion-api": "設定タブでNotion APIを設定してください",
"config-secrets-database-id": "設定タブでデータベースIDを設定してください",
"set-tags-fail": "タグの設定に失敗。ファイルのfrontmatterを確認するか、設定タブでタグのスイッチをオフにしてください",
NNonMissing: "設定に 'NNon' プロパティがありません。設定してください",
"set-api-id": "設定タブでNotion APIおよびデータベースIDを設定してください",
NotionCustomSettingHeader: "Notionカスタムデータベース設定",
NotionCustomButton: "Notionカスタマイズコマンドの切り替え",
NotionCustomButtonDesc: "このオプションを開くと、Notionカスタムデータベース同期コマンドがコマンドパレットに表示されます",
CustomPropertyName: "カスタムプロパティ名",
CustomPropertyFirstColumn: "最初の列のカスタムプロパティ名",
CustomPropertyFirstColumnDesc: "最初の列のカスタムプロパティ名を入力してください",
CustomProperty: "カスタムプロパティ",
AddCustomProperty: "カスタムプロパティを追加",
AddNewProperty: "新しいプロパティを追加",
AddNewPropertyDesc: "新しいプロパティを追加してください",
CopyErrorMessage: "自動コピーに失敗しました",
BlockUploaded: "ブロックがアップロードされました",
ExtraBlockUploaded: "追加ブロックがアップロードされました",
CheckConsole: "詳細情報を確認するには、コンソールを開いてください \n opt+cmd+i/ctrl+shift+i",
SettingsMigrated: "✨ プラグイン設定が更新されました!自動同期機能が追加されました。設定を確認してください",
AutoSyncNoNotionID: "⚠️ 自動同期をスキップ:このドキュメントは Notion に同期されていません。まず手動でアップロードしてください",
AutoSyncMultipleSync: "🔄 自動同期:{count} 個のデータベースに同期中...",
AutoSyncFailed: "{database} への自動同期に失敗しました:{error}",
AutoSyncError: "{filename} の自動同期に失敗しました:{error}",
"reach-mobile-limit": "ブロック数が100の制限を超えています。デスクトップ版プラグインを使用してください",
StartUpload: "アップロード開始 {filename}",
AddNewDatabase: "新しいデータベースを追加",
AddNewDatabaseDesc: "新しいデータベース構成を追加",
AddNewDatabaseTooltip: "新しいデータベースを追加",
EditDatabase: "データベースを編集",
Preview: "プレビュー",
DatabaseFormatLabel: "データベース形式",
DatabaseFullNameLabel: "データベースの全称",
DatabaseAbbreviateNameLabel: "データベースの略称",
NotionAPILabel: "Notion API キー",
DatabaseIDLabel: "データベース ID",
ToggleAPIKeyVisibility: "API キーの表示を切り替え",
CopyAPIKey: "API キーをコピー",
APIKeyCopied: "API キーをクリップボードにコピーしました",
ToggleDatabaseIDVisibility: "データベース ID の表示を切り替え",
CopyDatabaseID: "データベース ID をコピー",
DatabaseIDCopied: "データベース ID をクリップボードにコピーしました",
AddNewDatabaseModal: "新しいデータベースを追加",
};

105
src/lang/locale/zh.ts Normal file
View File

@@ -0,0 +1,105 @@
export const zh = {
databaseFormat: "数据库格式",
databaseFormatDesc: "选择你想要同步的数据库格式Next 或者 普通",
databaseNext: "NotionNext",
databaseGeneral: "普通",
databaseCustom: "自定义",
databaseFullName: "数据库全称",
databaseFullNameDesc: "给你的数据库起一个全称",
databaseFullNameText: "输入你的数据库全称",
databaseAbbreviateName: "数据库简称",
databaseAbbreviateNameDesc: "给你的数据库起一个简称",
databaseAbbreviateNameText: "输入你的数据库简称",
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",
DatabaseIDDesc: "从右上角的分享 --> 发布中获取",
DatabaseIDText: "输入你的数据库 ID",
BannerUrl: "封面图片地址(可选)",
BannerUrlDesc:
"默认为空如果你想显示封面图片请输入图片地址例如https://abc.com/b.png",
BannerUrlText: "输入你的封面图片地址",
NotionUser: "Notion ID用户名可选",
NotionUserDesc:
"数据库分享链接类似https://username.notion.site/。你的 Notion ID 是 [username]",
NotionUserText: "输入你的 Notion ID",
NotionLinkDisplay: "Notion 链接显示",
NotionLinkDisplayDesc: "默认开启如果你不想在front matter中显示链接请关闭",
AutoSync: "自动同步",
AutoSyncDesc: "当检测到文档的 frontmatter 或内容发生修改时,自动同步到 Notion需要文档已有 NotionID",
AutoSyncDelay: "自动同步延迟时间(秒)",
AutoSyncDelayDesc: "文档修改后等待多少秒才触发自动同步避免频繁同步默认5秒最小2秒",
AutoSyncDelayText: "输入延迟秒数",
NotionGeneralSettingHeader: "普通 Notion 数据库设置",
NotionGeneralButton: "普通数据库同步",
NotionGeneralButtonDesc: "打开此选项,同步到普通数据库命令将显示在命令面板中(默认:开)",
NotionTagButton: "标签同步开关",
NotionTagButtonDesc: "将标签同步到普通数据库(默认:开)",
NotionCustomTitle: "修改 Notion 数据库表头开关",
NotionCustomTitleDesc: "自定义Notion 数据库第一列表头名(默认:关)",
NotionCustomTitleName: "想要修改的表头名",
NotionCustomTitleNameDesc: "输入你想要修改的notion数据库的表头名默认title",
NotionCustomTitleText: "输入表头名",
NotionCustomValues: "自定义Notion 数据库表头",
NotionCustomValuesDesc: "自定义Notion 数据库表头,每行一个",
NotionCustomValuesText: "输入你想要同步的所有属性",
NotYetFinish: "未完成。此功能将在之后版本中提供",
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: "打开此选项,同步到自定义数据库命令将显示在命令面板中",
CustomPropertyName: "自定义属性名",
CustomPropertyFirstColumn: "第一列属性名",
CustomPropertyFirstColumnDesc: "第一列必须为标题属性名",
CustomProperty: "自定义属性",
AddCustomProperty: "添加自定义属性",
AddNewProperty: "添加新属性",
AddNewPropertyDesc: "添加一个和Notion数据库匹配的新属性",
CopyErrorMessage: "复制链接失败,请手动复制",
BlockUploaded: "所有内容已成功上传",
ExtraBlockUploaded: "额外内容已成功上传",
CheckConsole: "opt+cmd+i/ctrl+shift+i\n打开控制台查看更多信息",
SettingsMigrated: "✨ 插件设置已更新!新增自动同步功能,请在设置中查看",
AutoSyncNoNotionID: "⚠️ 自动同步跳过:此文档未同步到 Notion请先手动上传",
AutoSyncMultipleSync: "🔄 自动同步:正在同步到 {count} 个数据库...",
AutoSyncFailed: "自动同步到 {database} 失败:{error}",
AutoSyncError: "自动同步 {filename} 失败:{error}",
StartUpload: "开始上传 {filename}",
AddNewDatabase: "添加新数据库",
AddNewDatabaseDesc: "添加新的数据库配置",
AddNewDatabaseTooltip: "添加新数据库",
EditDatabase: "编辑数据库",
Preview: "预览",
DatabaseFormatLabel: "数据库格式",
DatabaseFullNameLabel: "数据库全称",
DatabaseAbbreviateNameLabel: "数据库简称",
NotionAPILabel: "Notion API 密钥",
DatabaseIDLabel: "数据库 ID",
ToggleAPIKeyVisibility: "切换 API 密钥可见性",
CopyAPIKey: "复制 API 密钥",
APIKeyCopied: "API 密钥已复制到剪贴板",
ToggleDatabaseIDVisibility: "切换数据库 ID 可见性",
CopyDatabaseID: "复制数据库 ID",
DatabaseIDCopied: "数据库 ID 已复制到剪贴板",
AddNewDatabaseModal: "添加新数据库",
}

406
src/main.ts Normal file
View File

@@ -0,0 +1,406 @@
import { App, Editor, MarkdownView, Notice, Plugin, PluginSettingTab, Setting, TFile, EventRef } from "obsidian";
import { addIcons } from 'src/ui/icon';
import { i18nConfig } from "src/lang/I18n";
import ribbonCommands from "src/commands/NotionCommands";
import { ObsidianSettingTab, PluginSettings, DEFAULT_SETTINGS, DatabaseDetails } from "src/ui/settingTabs";
import { uploadCommandNext, uploadCommandGeneral, uploadCommandCustom } from "src/upload/uploadCommand";
// Remember to rename these classes and interfaces!
export default class ObsidianSyncNotionPlugin extends Plugin {
settings: PluginSettings;
commands: ribbonCommands;
app: App;
modifyEventRef: EventRef | null = null;
autoSyncTimeout: number | null = null;
private syncingFiles: Set<string> = new Set();
private lastFrontmatterCache: Map<string, any> = new Map();
private lastContentHashCache: Map<string, string> = new Map();
async onload() {
await this.loadSettings();
// Log loaded settings for debugging
console.log('[Plugin] Loaded settings:', {
autoSync: this.settings.autoSync,
autoSyncDelay: this.settings.autoSyncDelay,
NotionLinkDisplay: this.settings.NotionLinkDisplay,
bannerUrl: this.settings.bannerUrl ? 'set' : 'empty',
notionUser: this.settings.notionUser ? 'set' : 'empty',
databaseCount: Object.keys(this.settings.databaseDetails || {}).length
});
this.commands = new ribbonCommands(this);
addIcons();
// This creates an icon in the left ribbon.
this.addRibbonIcon(
"notion-logo",
i18nConfig.ribbonIcon,
async (evt: MouseEvent) => {
// Called when the user clicks the icon.
// await this.uploadCommand();
await this.commands.ribbonDisplay();
}
);
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
// const statusBarItemEl = this.addStatusBarItem();
// // statusBarItemEl.setText("share to notion");
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new ObsidianSettingTab(this.app, this));
// Setup auto sync listener
this.setupAutoSync();
}
onunload() {
if (this.modifyEventRef) {
this.app.vault.offref(this.modifyEventRef);
}
if (this.autoSyncTimeout) {
clearTimeout(this.autoSyncTimeout);
}
this.lastFrontmatterCache.clear();
this.lastContentHashCache.clear();
}
async loadSettings() {
const loadedData = await this.loadData();
// Merge loaded data with defaults, ensuring all fields exist
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
loadedData || {}
);
// Ensure critical fields have valid values
if (typeof this.settings.autoSync !== 'boolean') {
this.settings.autoSync = DEFAULT_SETTINGS.autoSync;
}
if (typeof this.settings.autoSyncDelay !== 'number' || this.settings.autoSyncDelay < 2) {
this.settings.autoSyncDelay = DEFAULT_SETTINGS.autoSyncDelay;
}
if (typeof this.settings.NotionLinkDisplay !== 'boolean') {
this.settings.NotionLinkDisplay = DEFAULT_SETTINGS.NotionLinkDisplay;
}
// Ensure databaseDetails exists
if (!this.settings.databaseDetails || typeof this.settings.databaseDetails !== 'object') {
this.settings.databaseDetails = {};
}
// Save settings if any migration was needed
const needsSave = !loadedData ||
loadedData.autoSync === undefined ||
loadedData.autoSyncDelay === undefined ||
loadedData.NotionLinkDisplay === undefined;
if (needsSave) {
const migratedFields = [];
if (!loadedData) {
console.log('[Settings] First-time setup, creating default settings');
} else {
if (loadedData.autoSync === undefined) migratedFields.push('autoSync');
if (loadedData.autoSyncDelay === undefined) migratedFields.push('autoSyncDelay');
if (loadedData.NotionLinkDisplay === undefined) migratedFields.push('NotionLinkDisplay');
console.log('[Settings] Migrating settings, adding fields:', migratedFields.join(', '));
}
await this.saveSettings();
// Notify user about settings migration (only for existing users, not first-time setup)
if (loadedData && Object.keys(loadedData).length > 0 && migratedFields.length > 0) {
new Notice(i18nConfig.SettingsMigrated, 6000);
console.log('[Settings] Migration notice shown to user');
}
}
}
async saveSettings() {
// Validate settings before saving
this.validateSettings();
await this.saveData(this.settings);
console.log('[Settings] Settings saved successfully', {
autoSync: this.settings.autoSync,
autoSyncDelay: this.settings.autoSyncDelay,
NotionLinkDisplay: this.settings.NotionLinkDisplay,
databaseCount: Object.keys(this.settings.databaseDetails || {}).length
});
}
validateSettings() {
// Ensure all required fields have valid values
if (typeof this.settings.autoSync !== 'boolean') {
console.warn('[Settings] Invalid autoSync value, resetting to default');
this.settings.autoSync = DEFAULT_SETTINGS.autoSync;
}
if (typeof this.settings.autoSyncDelay !== 'number' || this.settings.autoSyncDelay < 2) {
console.warn('[Settings] Invalid autoSyncDelay value, resetting to default');
this.settings.autoSyncDelay = DEFAULT_SETTINGS.autoSyncDelay;
}
if (typeof this.settings.NotionLinkDisplay !== 'boolean') {
console.warn('[Settings] Invalid NotionLinkDisplay value, resetting to default');
this.settings.NotionLinkDisplay = DEFAULT_SETTINGS.NotionLinkDisplay;
}
if (!this.settings.databaseDetails || typeof this.settings.databaseDetails !== 'object') {
console.warn('[Settings] Invalid databaseDetails, resetting to empty object');
this.settings.databaseDetails = {};
}
}
async addDatabaseDetails(dbDetails: DatabaseDetails) {
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) {
console.log(`[AutoSync] No frontmatter found in ${file.path}`);
return;
}
// Get file content hash for comparison
const content = await this.app.vault.read(file);
const contentHash = this.simpleHash(content);
const lastContentHash = this.lastContentHashCache.get(file.path);
// Check if only NotionID fields changed (to avoid sync loops)
const lastFrontmatter = this.lastFrontmatterCache.get(file.path);
if (lastFrontmatter && lastContentHash) {
const frontmatterOnlyNotionIDChanged = this.onlyNotionIDChanged(lastFrontmatter, frontMatter);
const contentUnchanged = contentHash === lastContentHash;
console.log(`[AutoSync] Change analysis for ${file.basename}:`, {
frontmatterOnlyNotionIDChanged,
contentUnchanged,
frontmatterHasRealChanges: !frontmatterOnlyNotionIDChanged,
contentChanged: !contentUnchanged,
willSync: !(frontmatterOnlyNotionIDChanged && contentUnchanged)
});
// Only skip sync if BOTH conditions are true:
// 1. Frontmatter only has NotionID changes (no real user changes)
// 2. Content is completely unchanged
if (frontmatterOnlyNotionIDChanged && contentUnchanged) {
console.log(`[AutoSync] Only NotionID updated (from sync), content unchanged - skipping auto sync`);
// Update cache even when skipping, so next comparison uses the current state
this.lastFrontmatterCache.set(file.path, { ...frontMatter });
this.lastContentHashCache.set(file.path, contentHash);
return;
}
if (!contentUnchanged) {
console.log(`[AutoSync] Content changed - will sync`);
} else if (!frontmatterOnlyNotionIDChanged) {
console.log(`[AutoSync] Frontmatter changed - will sync`);
}
}
// Find all databases this file belongs to by checking for NotionID-{abName}
const foundDatabases: Array<{ dbDetails: DatabaseDetails, notionId: string }> = [];
for (const key in this.settings.databaseDetails) {
const dbDetails = this.settings.databaseDetails[key];
const notionIDKey = `NotionID-${dbDetails.abName}`;
if (frontMatter[notionIDKey]) {
foundDatabases.push({
dbDetails: dbDetails,
notionId: String(frontMatter[notionIDKey])
});
}
}
// If no NotionID found, notify user to upload manually first
if (foundDatabases.length === 0) {
console.log(`[AutoSync] No NotionID found in ${file.path}, skipping auto sync`);
new Notice(i18nConfig.AutoSyncNoNotionID, 4000);
return;
}
// Notify user about multiple syncs if applicable
if (foundDatabases.length > 1) {
const message = i18nConfig.AutoSyncMultipleSync.replace('{count}', String(foundDatabases.length));
new Notice(message, 3000);
console.log(`[AutoSync] Found ${foundDatabases.length} NotionIDs in ${file.path}`);
}
// Sync to all found databases
for (const { dbDetails, notionId } of foundDatabases) {
console.log(`[AutoSync] ${new Date().toISOString()} Auto syncing ${file.basename} to ${dbDetails.fullName} (${dbDetails.abName})`);
try {
// Trigger appropriate upload command based on database format
if (dbDetails.format === 'next') {
await uploadCommandNext(this, this.settings, dbDetails, this.app);
} else if (dbDetails.format === 'general') {
await uploadCommandGeneral(this, this.settings, dbDetails, this.app);
} else if (dbDetails.format === 'custom') {
await uploadCommandCustom(this, this.settings, dbDetails, this.app);
}
} catch (error) {
console.error(`[AutoSync] Error syncing to ${dbDetails.fullName}:`, error);
const message = i18nConfig.AutoSyncFailed
.replace('{database}', dbDetails.fullName)
.replace('{error}', error.message);
new Notice(message, 5000);
}
}
// After sync completes, update cache with the latest frontmatter (including updated NotionIDs)
// Wait a bit for metadata cache to update
window.setTimeout(async () => {
const updatedFrontmatter = this.app.metadataCache.getFileCache(file)?.frontmatter;
const updatedContent = await this.app.vault.read(file);
const updatedHash = this.simpleHash(updatedContent);
if (updatedFrontmatter) {
this.lastFrontmatterCache.set(file.path, { ...updatedFrontmatter });
this.lastContentHashCache.set(file.path, updatedHash);
console.log(`[AutoSync] Cached updated frontmatter and content hash for ${file.path}`);
}
}, 500);
} catch (error) {
console.error(`[AutoSync] Error syncing file ${file.path}:`, error);
const message = i18nConfig.AutoSyncError
.replace('{filename}', file.basename)
.replace('{error}', error.message);
new Notice(message);
} finally {
this.syncingFiles.delete(file.path);
}
}
}

64
src/ui/DeleteModal.ts Normal file
View 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
View 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
View 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
});
});
}
}

14
src/ui/icon.ts Normal file
View File

@@ -0,0 +1,14 @@
import { addIcon } from 'obsidian';
const icons: Record<string, string> = {
'notion-logo': `
<!-- License: Apache. Made by lawnchairlauncher: https://github.com/lawnchairlauncher/lawnicons -->
<svg width="110px" height="110px" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill="#817f7a" fill-rule="evenodd" d="m138.462 21.522 27.784 19.588.044.033.275.201c1.713 1.256 3.349 2.455 4.452 3.83 1.411 1.76 1.884 3.644 1.884 5.877v104.706c0 3.587-.635 7.178-3.058 9.934-2.451 2.789-6.145 4.067-10.732 4.394l-.018.001-98.629 5.971-.021.001c-3.242.154-6.094.035-8.669-.907-2.688-.984-4.719-2.727-6.604-5.129l-.01-.012-19.979-25.979-.012-.017c-3.81-5.086-5.723-9.348-5.723-14.509V34.509c0-3.12.688-6.394 2.745-9.033 2.124-2.727 5.356-4.328 9.503-4.686l.058-.005 84.854-4.344c5.192-.445 8.938-.576 12.286.185 3.459.787 6.208 2.452 9.57 4.896ZM56.43 157.336h.002v3.3c0 1.904.47 2.337.613 2.452.296.235 1.203.652 3.642.518l97.449-5.371c1.928-.106 2.256-.649 2.348-.801l.005-.008c.29-.476.486-1.407.486-3.357V60.001c0-1.635-.334-2.218-.421-2.327l-.005-.007-.002-.003-.006-.002a.117.117 0 0 1-.012-.004c-.053-.019-.263-.078-.724-.037l-.057.005-101.622 5.668c-.624.056-.973.163-1.152.242-.142.062-.173.104-.181.116l-.002.002c-.066.085-.36.586-.36 2.321v91.361Zm9.085-106.705 87.074-4.506-21.028-15.375-.039-.031c-1.259-.98-2.507-1.854-4.12-2.46-1.588-.597-3.695-.993-6.669-.734l-.05.005-87.009 4.898h-.01a6.453 6.453 0 0 0-.893.116L49.934 48.56c2.037 1.646 3.109 2.146 4.337 2.367 1.538.277 3.52.167 7.722-.115l3.522-.237v.056Zm-34.231-3.586v83.893c0 .538.175 1.061.498 1.49l13.174 17.464V61.224a2.47 2.47 0 0 0-.877-1.889l-.08-.068-12.715-12.222Zm109.871 35.062c.451 2.04 0 4.082-2.041 4.315l-3.393.673v49.881c-2.947 1.586-5.66 2.492-7.927 2.492-3.622 0-4.528-1.134-7.239-4.53l-.003-.003L98.36 100.02v33.78l7.02 1.59s0 4.082-5.664 4.082l-15.615.906c-.455-.91 0-3.176 1.582-3.627l4.078-1.131V90.955l-5.66-.459c-.454-2.04.677-4.987 3.85-5.216l16.754-1.128 23.09 35.367V88.231l-5.885-.677c-.455-2.499 1.356-4.315 3.618-4.536l15.627-.91v-.001Z" clip-rule="evenodd"/></svg>
`
};
export const addIcons = (): void => {
Object.keys(icons).forEach((key) => {
addIcon(key, icons[key]);
});
};

377
src/ui/settingModal.ts Normal file
View 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();
})
);
}
}
}

353
src/ui/settingTabs.ts Normal file
View File

@@ -0,0 +1,353 @@
import { App, ButtonComponent, PluginSettingTab, Setting } from "obsidian";
import { i18nConfig } from "../lang/I18n";
import ObsidianSyncNotionPlugin from "../main";
import { SettingModal } from "./settingModal";
import { PreviewModal } from "./PreviewModal";
import { EditModal } from "./EditModal";
import { DeleteModal } from "./DeleteModal";
export interface PluginSettings {
NextButton: boolean;
notionAPINext: string;
databaseIDNext: string;
bannerUrl: string;
notionUser: string;
NotionLinkDisplay: boolean;
autoSync: boolean;
autoSyncDelay: number;
proxy: string;
GeneralButton: boolean;
tagButton: boolean;
customTitleButton: boolean;
customTitleName: string;
notionAPIGeneral: string;
databaseIDGeneral: string;
CustomButton: boolean;
CustomValues: string;
notionAPICustom: string;
databaseIDCustom: string;
[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 = {
NextButton: true,
notionAPINext: "",
databaseIDNext: "",
bannerUrl: "",
notionUser: "",
NotionLinkDisplay: true,
autoSync: false,
autoSyncDelay: 5,
proxy: "",
GeneralButton: true,
tagButton: true,
customTitleButton: false,
customTitleName: "",
notionAPIGeneral: "",
databaseIDGeneral: "",
CustomButton: false,
CustomValues: "",
notionAPICustom: "",
databaseIDCustom: "",
databaseDetails: {},
};
export class ObsidianSettingTab extends PluginSettingTab {
plugin: ObsidianSyncNotionPlugin;
databaseEl: HTMLDivElement;
autoSyncDelayContainer: HTMLElement | null = null;
constructor(app: App, plugin: ObsidianSyncNotionPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
// General Settings
containerEl.createEl('h2', { text: i18nConfig.GeneralSetting });
this.createSettingEl(containerEl, i18nConfig.BannerUrl, i18nConfig.BannerUrlDesc, 'text', i18nConfig.BannerUrlText, this.plugin.settings.bannerUrl, 'bannerUrl')
this.createSettingEl(containerEl, i18nConfig.NotionUser, i18nConfig.NotionUserDesc, 'text', i18nConfig.NotionUserText, this.plugin.settings.notionUser, 'notionUser')
this.createSettingEl(containerEl, i18nConfig.NotionLinkDisplay, i18nConfig.NotionLinkDisplayDesc, 'toggle', i18nConfig.NotionLinkDisplay, this.plugin.settings.NotionLinkDisplay, 'NotionLinkDisplay')
this.createSettingEl(containerEl, i18nConfig.AutoSync, i18nConfig.AutoSyncDesc, 'toggle', i18nConfig.AutoSync, this.plugin.settings.autoSync, 'autoSync')
// Auto Sync Delay setting - only visible when autoSync is enabled
this.autoSyncDelayContainer = containerEl.createDiv();
const delaySetting = new Setting(this.autoSyncDelayContainer)
.setName(i18nConfig.AutoSyncDelay)
.setDesc(i18nConfig.AutoSyncDelayDesc)
.addText((text) =>
text
.setPlaceholder(i18nConfig.AutoSyncDelayText)
.setValue(String(this.plugin.settings.autoSyncDelay))
.onChange(async (value) => {
const delay = parseFloat(value);
if (!isNaN(delay) && delay >= 2) {
this.plugin.settings.autoSyncDelay = delay;
await this.plugin.saveSettings();
} else if (!isNaN(delay) && delay < 2) {
// If user enters less than 2 seconds, set it to 2
this.plugin.settings.autoSyncDelay = 2;
await this.plugin.saveSettings();
text.setValue('2');
}
})
);
// Set initial visibility
this.updateAutoSyncDelayVisibility();
// add new button
new Setting(containerEl)
.setName(i18nConfig.AddNewDatabase)
.setDesc(i18nConfig.AddNewDatabaseDesc)
.addButton((button: ButtonComponent): ButtonComponent => {
return button
.setTooltip(i18nConfig.AddNewDatabaseTooltip)
.setIcon("plus")
.onClick(async () => {
let modal = new SettingModal(this.app, this.plugin, this);
modal.onClose = () => {
if (modal.data.saved) {
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);
this.plugin.commands.updateCommand();
this.display()
}
}
modal.open();
});
});
// new section to display all created database
containerEl.createEl('h2', { text: "Database List" });
this.databaseEl = containerEl.createDiv('database-list');
// list all created database
this.showDatabase();
}
// create a function to create a div with a style for pop over elements
// public createStyleDiv(className: string, commandValue: boolean = false) {
// return this.containerEl.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";
}
// Update visibility of autoSyncDelay setting based on autoSync toggle
public updateAutoSyncDelayVisibility() {
if (this.autoSyncDelayContainer) {
this.autoSyncDelayContainer.style.display = this.plugin.settings.autoSync ? "block" : "none";
}
}
// function to add one setting element in the setting tab.
public createSettingEl(containerEl: HTMLElement, name: string, desc: string, type: string, placeholder: string, holderValue: any, settingsKey: string) {
if (type === 'password') {
return new Setting(containerEl)
.setName(name)
.setDesc(desc)
.addText((text) => {
text.inputEl.type = type;
return text
.setPlaceholder(placeholder)
.setValue(holderValue)
.onChange(async (value) => {
this.plugin.settings[settingsKey] = value; // Update the plugin settings directly
await this.plugin.saveSettings();
})
});
} else if (type === 'toggle') {
return new Setting(containerEl)
.setName(name)
.setDesc(desc)
.addToggle((toggle) =>
toggle
.setValue(holderValue)
.onChange(async (value) => {
this.plugin.settings[settingsKey] = value; // Update the plugin settings directly
await this.plugin.saveSettings();
await this.plugin.commands.updateCommand();
// If autoSync setting changed, update the listener and visibility
if (settingsKey === 'autoSync') {
this.plugin.setupAutoSync();
this.updateAutoSyncDelayVisibility();
}
})
);
} else if (type === 'text') {
return new Setting(containerEl)
.setName(name)
.setDesc(desc)
.addText((text) =>
text
.setPlaceholder(placeholder)
.setValue(holderValue)
.onChange(async (value) => {
this.plugin.settings[settingsKey] = value; // Update the plugin settings directly
await this.plugin.saveSettings();
await this.plugin.commands.updateCommand();
})
);
}
}
// 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();
});
});
}
}
}

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

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

View 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;
}
}

View File

@@ -0,0 +1,60 @@
import { App, Notice } from "obsidian";
import { i18nConfig } from "../../lang/I18n";
import { PluginSettings } from "../../ui/settingTabs";
export async function getNowFileMarkdownContentNext(
app: App,
settings: PluginSettings,
) {
const nowFile = app.workspace.getActiveFile();
let emoji = '';
let cover = '';
let tags = [];
let type = '';
let slug = '';
let stats = '';
let status = '';
let category = '';
let summary = '';
let paword = '';
let favicon = '';
let datetime = '';
const FileCache = app.metadataCache.getFileCache(nowFile);
try {
emoji = FileCache.frontmatter.titleicon;
cover = FileCache.frontmatter.coverurl;
tags = FileCache.frontmatter.tags;
type = FileCache.frontmatter.type;
slug = FileCache.frontmatter.slug;
stats = FileCache.frontmatter.stats || FileCache.frontmatter.status;
category = FileCache.frontmatter.category;
summary = FileCache.frontmatter.summary;
paword = FileCache.frontmatter.password;
favicon = FileCache.frontmatter.icon;
datetime = FileCache.frontmatter.date;
} catch (error) {
new Notice(i18nConfig["set-tags-fail"]);
}
if (nowFile) {
const markDownData = await nowFile.vault.read(nowFile);
return {
markDownData,
nowFile,
emoji,
cover,
tags,
type,
slug,
stats,
category,
summary,
paword,
favicon,
datetime,
};
} else {
new Notice(i18nConfig["open-file"]);
return;
}
}

44
src/upload/updateYaml.ts Normal file
View File

@@ -0,0 +1,44 @@
import { App, Notice, TFile } from "obsidian";
import ObsidianSyncNotionPlugin from "../main";
import { DatabaseDetails } from "../ui/settingTabs";
import { i18nConfig } from "src/lang/I18n";
export async function updateYamlInfo(
yamlContent: string,
nowFile: TFile,
res: any,
app: App,
plugin: ObsidianSyncNotionPlugin,
dbDetails: DatabaseDetails,
) {
let { url, id } = res;
// replace www to notionID
const { notionUser, NotionLinkDisplay } = plugin.settings;
const { abName } = dbDetails
const notionIDKey = `NotionID-${abName}`;
const linkKey = `link-${abName}`;
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[notionIDKey]) {
delete yamlContent[notionIDKey]
}
if (yamlContent[linkKey]) {
delete yamlContent[linkKey]
}
// add new notionID and link
yamlContent[notionIDKey] = id;
(NotionLinkDisplay) ? yamlContent[linkKey] = url : null;
});
try {
await navigator.clipboard.writeText(url)
} catch (error) {
console.log(error)
new Notice(`${i18nConfig.CopyErrorMessage}`);
}
}

295
src/upload/uploadCommand.ts Normal file
View File

@@ -0,0 +1,295 @@
import {i18nConfig} from "../lang/I18n";
import {App, Notice} from "obsidian";
import {Upload2Notion} from "./Upload2Notion";
import type {NotionPageResponse} from "./common/UploadBase";
import {DatabaseDetails, PluginSettings} from "../ui/settingTabs";
import ObsidianSyncNotionPlugin from "../main";
import {getNowFileMarkdownContentNext} from "./common/getMarkdownNext";
import {getNowFileMarkdownContentGeneral} from "./common/getMarkdownGeneral";
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(
plugin: ObsidianSyncNotionPlugin,
settings: PluginSettings,
dbDetails: DatabaseDetails,
app: App,
) {
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
if (notionAPI === "" || databaseID === "") {
const setAPIMessage = i18nConfig["set-api-id"];
new Notice(setAPIMessage);
return;
}
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) {
const {basename} = nowFile;
logCommandDebug("uploadCommandNext", "Preparing to upload", {
filename: basename,
filePath: nowFile.path,
});
const upload = new Upload2Notion(plugin, dbDetails);
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;
if (response.status === 200) {
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 {
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(
plugin: ObsidianSyncNotionPlugin,
settings: PluginSettings,
dbDetails: DatabaseDetails,
app: App,
) {
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
if (notionAPI === "" || databaseID === "") {
const setAPIMessage = i18nConfig["set-api-id"];
new Notice(setAPIMessage);
return;
}
const {markDownData, nowFile, cover, tags} = await getNowFileMarkdownContentGeneral(app, settings)
new Notice(i18nConfig.StartUpload.replace('{filename}', nowFile.basename));
console.log(`Start upload ${nowFile.basename}`);
if (markDownData) {
const {basename} = nowFile;
logCommandDebug("uploadCommandGeneral", "Preparing to upload", {
filename: basename,
markdownLength: markDownData.length,
tagCount: tags?.length ?? 0,
hasCover: !!cover,
});
const upload = new Upload2Notion(plugin, dbDetails);
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;
if (response.status === 200) {
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 {
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,
) {
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)
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",
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) {
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,
});
}
}
}

View File

@@ -1,4 +0,0 @@
/* Sets all the text color to red! */
/* body {
color: red;
} */

View File

@@ -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,6 +19,8 @@
] ]
}, },
"include": [ "include": [
"**/*.ts" "**/*.ts",
, "Upload2Notion.ts" ] "src/Upload2Notion.ts",
"src/*.ts"
]
} }

24
update-version.js Normal file
View File

@@ -0,0 +1,24 @@
// update package.json version
import fs from "fs";
const packageJson = JSON.parse(fs.readFileSync("./package.json", "utf8"));
console.log(packageJson.version);
// get node args
const args = process.argv.slice(2);
packageJson.version = args[0];
console.log(packageJson.version);
// write package.json
fs.writeFileSync("./package.json", JSON.stringify(packageJson, null, 2));
// read manifest.json
const manifestJson = JSON.parse(fs.readFileSync("./manifest.json", "utf8"));
manifestJson.version = args[0];
// write manifest.json
fs.writeFileSync("./manifest.json", JSON.stringify(manifestJson, null, 2));

958
yarn.lock
View File

@@ -1,958 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@codemirror/rangeset@^0.19.5":
"integrity" "sha512-V8YUuOvK+ew87Xem+71nKcqu1SXd5QROMRLMS/ljT5/3MCxtgrRie1Cvild0G/Z2f1fpWxzX78V0U4jjXBorBQ=="
"resolved" "https://registry.npmmirror.com/@codemirror/rangeset/-/rangeset-0.19.9.tgz"
"version" "0.19.9"
dependencies:
"@codemirror/state" "^0.19.0"
"@codemirror/state@^0.19.0", "@codemirror/state@^0.19.3", "@codemirror/state@^0.19.6":
"integrity" "sha512-psOzDolKTZkx4CgUqhBQ8T8gBc0xN5z4gzed109aF6x7D7umpDRoimacI/O6d9UGuyl4eYuDCZmDFr2Rq7aGOw=="
"resolved" "https://registry.npmmirror.com/@codemirror/state/-/state-0.19.9.tgz"
"version" "0.19.9"
dependencies:
"@codemirror/text" "^0.19.0"
"@codemirror/text@^0.19.0":
"integrity" "sha512-T9jnREMIygx+TPC1bOuepz18maGq/92q2a+n4qTqObKwvNMg+8cMTslb8yxeEDEq7S3kpgGWxgO1UWbQRij0dA=="
"resolved" "https://registry.npmmirror.com/@codemirror/text/-/text-0.19.6.tgz"
"version" "0.19.6"
"@codemirror/view@^0.19.31":
"integrity" "sha512-0eg7D2Nz4S8/caetCTz61rK0tkHI17V/d15Jy0kLOT8dTLGGNJUponDnW28h2B6bERmPlVHKh8MJIr5OCp1nGw=="
"resolved" "https://registry.npmmirror.com/@codemirror/view/-/view-0.19.48.tgz"
"version" "0.19.48"
dependencies:
"@codemirror/rangeset" "^0.19.5"
"@codemirror/state" "^0.19.3"
"@codemirror/text" "^0.19.0"
"style-mod" "^4.0.0"
"w3c-keyname" "^2.2.4"
"@nodelib/fs.scandir@2.1.5":
"integrity" "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="
"resolved" "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
"version" "2.1.5"
dependencies:
"@nodelib/fs.stat" "2.0.5"
"run-parallel" "^1.1.9"
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
"integrity" "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="
"resolved" "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
"version" "2.0.5"
"@nodelib/fs.walk@^1.2.3":
"integrity" "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="
"resolved" "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz"
"version" "1.2.8"
dependencies:
"@nodelib/fs.scandir" "2.1.5"
"fastq" "^1.6.0"
"@notionhq/client@^1.0.4":
"integrity" "sha512-m7zZ5l3RUktayf1lRBV1XMb8HSKsmWTv/LZPqP7UGC1NMzOlc+bbTOPNQ4CP/c1P4cP61VWLb/zBq7a3c0nMaw=="
"resolved" "https://registry.npmmirror.com/@notionhq/client/-/client-1.0.4.tgz"
"version" "1.0.4"
dependencies:
"@types/node-fetch" "^2.5.10"
"node-fetch" "^2.6.1"
"@tryfabric/martian@^1.2.0":
"integrity" "sha512-q3grnGgwfujNZelpK6uMswObYKSy1dY+yKypgjl7EPxpSvSHlJb1f0gIfghGUWf1gVxZfHfSZDe+k9KCzaLbwQ=="
"resolved" "https://registry.npmmirror.com/@tryfabric/martian/-/martian-1.2.0.tgz"
"version" "1.2.0"
dependencies:
"@notionhq/client" "^1.0.4"
"remark-gfm" "^1.0.0"
"remark-parse" "^9.0.0"
"unified" "^9.2.1"
"@types/codemirror@0.0.108":
"integrity" "sha512-3FGFcus0P7C2UOGCNUVENqObEb4SFk+S8Dnxq7K6aIsLVs/vDtlangl3PEO0ykaKXyK56swVF6Nho7VsA44uhw=="
"resolved" "https://registry.npmmirror.com/@types/codemirror/-/codemirror-0.0.108.tgz"
"version" "0.0.108"
dependencies:
"@types/tern" "*"
"@types/estree@*":
"integrity" "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ=="
"resolved" "https://registry.npmmirror.com/@types/estree/-/estree-0.0.51.tgz"
"version" "0.0.51"
"@types/js-yaml@*":
"integrity" "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA=="
"resolved" "https://registry.npmmirror.com/@types/js-yaml/-/js-yaml-4.0.5.tgz"
"version" "4.0.5"
"@types/json-schema@^7.0.9":
"integrity" "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
"resolved" "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.11.tgz"
"version" "7.0.11"
"@types/mdast@^3.0.0":
"integrity" "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA=="
"resolved" "https://registry.npmmirror.com/@types/mdast/-/mdast-3.0.10.tgz"
"version" "3.0.10"
dependencies:
"@types/unist" "*"
"@types/node-fetch@^2.5.10":
"integrity" "sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA=="
"resolved" "https://registry.npmmirror.com/@types/node-fetch/-/node-fetch-2.6.1.tgz"
"version" "2.6.1"
dependencies:
"@types/node" "*"
"form-data" "^3.0.0"
"@types/node@*", "@types/node@^17.0.35":
"integrity" "sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg=="
"resolved" "https://registry.npmmirror.com/@types/node/-/node-17.0.35.tgz"
"version" "17.0.35"
"@types/tern@*":
"integrity" "sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg=="
"resolved" "https://registry.npmmirror.com/@types/tern/-/tern-0.23.4.tgz"
"version" "0.23.4"
dependencies:
"@types/estree" "*"
"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
"integrity" "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
"resolved" "https://registry.npmmirror.com/@types/unist/-/unist-2.0.6.tgz"
"version" "2.0.6"
"@types/yaml-front-matter@^4.1.0":
"integrity" "sha512-d4YGe2onl1T4VA6QhT4agdxaStOBe/ig9RAFAT1OltytvO2z9Ro69DiwHD2FUwewcjeD9PWEfUeZhw7E9hApcQ=="
"resolved" "https://registry.npmmirror.com/@types/yaml-front-matter/-/yaml-front-matter-4.1.0.tgz"
"version" "4.1.0"
dependencies:
"@types/js-yaml" "*"
"@types/node" "*"
"@typescript-eslint/eslint-plugin@^5.2.0":
"integrity" "sha512-icYrFnUzvm+LhW0QeJNKkezBu6tJs9p/53dpPLFH8zoM9w1tfaKzVurkPotEpAqQ8Vf8uaFyL5jHd0Vs6Z0ZQg=="
"resolved" "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.25.0.tgz"
"version" "5.25.0"
dependencies:
"@typescript-eslint/scope-manager" "5.25.0"
"@typescript-eslint/type-utils" "5.25.0"
"@typescript-eslint/utils" "5.25.0"
"debug" "^4.3.4"
"functional-red-black-tree" "^1.0.1"
"ignore" "^5.2.0"
"regexpp" "^3.2.0"
"semver" "^7.3.7"
"tsutils" "^3.21.0"
"@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@^5.2.0":
"integrity" "sha512-r3hwrOWYbNKP1nTcIw/aZoH+8bBnh/Lh1iDHoFpyG4DnCpvEdctrSl6LOo19fZbzypjQMHdajolxs6VpYoChgA=="
"resolved" "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-5.25.0.tgz"
"version" "5.25.0"
dependencies:
"@typescript-eslint/scope-manager" "5.25.0"
"@typescript-eslint/types" "5.25.0"
"@typescript-eslint/typescript-estree" "5.25.0"
"debug" "^4.3.4"
"@typescript-eslint/scope-manager@5.25.0":
"integrity" "sha512-p4SKTFWj+2VpreUZ5xMQsBMDdQ9XdRvODKXN4EksyBjFp2YvQdLkyHqOffakYZPuWJUDNu3jVXtHALDyTv3cww=="
"resolved" "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-5.25.0.tgz"
"version" "5.25.0"
dependencies:
"@typescript-eslint/types" "5.25.0"
"@typescript-eslint/visitor-keys" "5.25.0"
"@typescript-eslint/type-utils@5.25.0":
"integrity" "sha512-B6nb3GK3Gv1Rsb2pqalebe/RyQoyG/WDy9yhj8EE0Ikds4Xa8RR28nHz+wlt4tMZk5bnAr0f3oC8TuDAd5CPrw=="
"resolved" "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-5.25.0.tgz"
"version" "5.25.0"
dependencies:
"@typescript-eslint/utils" "5.25.0"
"debug" "^4.3.4"
"tsutils" "^3.21.0"
"@typescript-eslint/types@5.25.0":
"integrity" "sha512-7fWqfxr0KNHj75PFqlGX24gWjdV/FDBABXL5dyvBOWHpACGyveok8Uj4ipPX/1fGU63fBkzSIycEje4XsOxUFA=="
"resolved" "https://registry.npmmirror.com/@typescript-eslint/types/-/types-5.25.0.tgz"
"version" "5.25.0"
"@typescript-eslint/typescript-estree@5.25.0":
"integrity" "sha512-MrPODKDych/oWs/71LCnuO7NyR681HuBly2uLnX3r5i4ME7q/yBqC4hW33kmxtuauLTM0OuBOhhkFaxCCOjEEw=="
"resolved" "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.25.0.tgz"
"version" "5.25.0"
dependencies:
"@typescript-eslint/types" "5.25.0"
"@typescript-eslint/visitor-keys" "5.25.0"
"debug" "^4.3.4"
"globby" "^11.1.0"
"is-glob" "^4.0.3"
"semver" "^7.3.7"
"tsutils" "^3.21.0"
"@typescript-eslint/utils@5.25.0":
"integrity" "sha512-qNC9bhnz/n9Kba3yI6HQgQdBLuxDoMgdjzdhSInZh6NaDnFpTUlwNGxplUFWfY260Ya0TRPvkg9dd57qxrJI9g=="
"resolved" "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-5.25.0.tgz"
"version" "5.25.0"
dependencies:
"@types/json-schema" "^7.0.9"
"@typescript-eslint/scope-manager" "5.25.0"
"@typescript-eslint/types" "5.25.0"
"@typescript-eslint/typescript-estree" "5.25.0"
"eslint-scope" "^5.1.1"
"eslint-utils" "^3.0.0"
"@typescript-eslint/visitor-keys@5.25.0":
"integrity" "sha512-yd26vFgMsC4h2dgX4+LR+GeicSKIfUvZREFLf3DDjZPtqgLx5AJZr6TetMNwFP9hcKreTTeztQYBTNbNoOycwA=="
"resolved" "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.25.0.tgz"
"version" "5.25.0"
dependencies:
"@typescript-eslint/types" "5.25.0"
"eslint-visitor-keys" "^3.3.0"
"agent-base@6":
"integrity" "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="
"resolved" "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz"
"version" "6.0.2"
dependencies:
"debug" "4"
"argparse@^1.0.7":
"integrity" "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="
"resolved" "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz"
"version" "1.0.10"
dependencies:
"sprintf-js" "~1.0.2"
"array-union@^2.1.0":
"integrity" "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="
"resolved" "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz"
"version" "2.1.0"
"asynckit@^0.4.0":
"integrity" "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
"resolved" "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz"
"version" "0.4.0"
"bail@^1.0.0":
"integrity" "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ=="
"resolved" "https://registry.npmmirror.com/bail/-/bail-1.0.5.tgz"
"version" "1.0.5"
"braces@^3.0.2":
"integrity" "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A=="
"resolved" "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz"
"version" "3.0.2"
dependencies:
"fill-range" "^7.0.1"
"builtin-modules@^3.2.0":
"integrity" "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw=="
"resolved" "https://registry.npmmirror.com/builtin-modules/-/builtin-modules-3.3.0.tgz"
"version" "3.3.0"
"ccount@^1.0.0":
"integrity" "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg=="
"resolved" "https://registry.npmmirror.com/ccount/-/ccount-1.1.0.tgz"
"version" "1.1.0"
"character-entities-legacy@^1.0.0":
"integrity" "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA=="
"resolved" "https://registry.npmmirror.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz"
"version" "1.1.4"
"character-entities@^1.0.0":
"integrity" "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw=="
"resolved" "https://registry.npmmirror.com/character-entities/-/character-entities-1.2.4.tgz"
"version" "1.2.4"
"character-reference-invalid@^1.0.0":
"integrity" "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="
"resolved" "https://registry.npmmirror.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz"
"version" "1.1.4"
"combined-stream@^1.0.8":
"integrity" "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="
"resolved" "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz"
"version" "1.0.8"
dependencies:
"delayed-stream" "~1.0.0"
"commander@^6.2.0":
"integrity" "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="
"resolved" "https://registry.npmmirror.com/commander/-/commander-6.2.1.tgz"
"version" "6.2.1"
"debug@^4.0.0", "debug@^4.3.4", "debug@4":
"integrity" "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="
"resolved" "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz"
"version" "4.3.4"
dependencies:
"ms" "2.1.2"
"delayed-stream@~1.0.0":
"integrity" "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
"resolved" "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz"
"version" "1.0.0"
"dir-glob@^3.0.1":
"integrity" "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="
"resolved" "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz"
"version" "3.0.1"
dependencies:
"path-type" "^4.0.0"
"encoding@^0.1.0":
"integrity" "sha512-bl1LAgiQc4ZWr++pNYUdRe/alecaHFeHxIJ/pNciqGdKXghaTCOwKkbKp6ye7pKZGu/GcaSXFk8PBVhgs+dJdA=="
"resolved" "https://registry.npmmirror.com/encoding/-/encoding-0.1.12.tgz"
"version" "0.1.12"
dependencies:
"iconv-lite" "~0.4.13"
"esbuild-darwin-64@0.13.12":
"integrity" "sha512-c51C+N+UHySoV2lgfWSwwmlnLnL0JWj/LzuZt9Ltk9ub1s2Y8cr6SQV5W3mqVH1egUceew6KZ8GyI4nwu+fhsw=="
"resolved" "https://registry.npmmirror.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.12.tgz"
"version" "0.13.12"
"esbuild@0.13.12":
"integrity" "sha512-vTKKUt+yoz61U/BbrnmlG9XIjwpdIxmHB8DlPR0AAW6OdS+nBQBci6LUHU2q9WbBobMEIQxxDpKbkmOGYvxsow=="
"resolved" "https://registry.npmmirror.com/esbuild/-/esbuild-0.13.12.tgz"
"version" "0.13.12"
optionalDependencies:
"esbuild-android-arm64" "0.13.12"
"esbuild-darwin-64" "0.13.12"
"esbuild-darwin-arm64" "0.13.12"
"esbuild-freebsd-64" "0.13.12"
"esbuild-freebsd-arm64" "0.13.12"
"esbuild-linux-32" "0.13.12"
"esbuild-linux-64" "0.13.12"
"esbuild-linux-arm" "0.13.12"
"esbuild-linux-arm64" "0.13.12"
"esbuild-linux-mips64le" "0.13.12"
"esbuild-linux-ppc64le" "0.13.12"
"esbuild-netbsd-64" "0.13.12"
"esbuild-openbsd-64" "0.13.12"
"esbuild-sunos-64" "0.13.12"
"esbuild-windows-32" "0.13.12"
"esbuild-windows-64" "0.13.12"
"esbuild-windows-arm64" "0.13.12"
"escape-string-regexp@^4.0.0":
"integrity" "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
"resolved" "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz"
"version" "4.0.0"
"eslint-scope@^5.1.1":
"integrity" "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="
"resolved" "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-5.1.1.tgz"
"version" "5.1.1"
dependencies:
"esrecurse" "^4.3.0"
"estraverse" "^4.1.1"
"eslint-utils@^3.0.0":
"integrity" "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA=="
"resolved" "https://registry.npmmirror.com/eslint-utils/-/eslint-utils-3.0.0.tgz"
"version" "3.0.0"
dependencies:
"eslint-visitor-keys" "^2.0.0"
"eslint-visitor-keys@^2.0.0":
"integrity" "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw=="
"resolved" "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz"
"version" "2.1.0"
"eslint-visitor-keys@^3.3.0":
"integrity" "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA=="
"resolved" "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz"
"version" "3.3.0"
"esprima@^4.0.0":
"integrity" "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
"resolved" "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz"
"version" "4.0.1"
"esrecurse@^4.3.0":
"integrity" "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="
"resolved" "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz"
"version" "4.3.0"
dependencies:
"estraverse" "^5.2.0"
"estraverse@^4.1.1":
"integrity" "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="
"resolved" "https://registry.npmmirror.com/estraverse/-/estraverse-4.3.0.tgz"
"version" "4.3.0"
"estraverse@^5.2.0":
"integrity" "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="
"resolved" "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz"
"version" "5.3.0"
"extend@^3.0.0":
"integrity" "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
"resolved" "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz"
"version" "3.0.2"
"fast-glob@^3.2.9":
"integrity" "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew=="
"resolved" "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.2.11.tgz"
"version" "3.2.11"
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
"glob-parent" "^5.1.2"
"merge2" "^1.3.0"
"micromatch" "^4.0.4"
"fastq@^1.6.0":
"integrity" "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw=="
"resolved" "https://registry.npmmirror.com/fastq/-/fastq-1.13.0.tgz"
"version" "1.13.0"
dependencies:
"reusify" "^1.0.4"
"fill-range@^7.0.1":
"integrity" "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ=="
"resolved" "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz"
"version" "7.0.1"
dependencies:
"to-regex-range" "^5.0.1"
"form-data@^3.0.0":
"integrity" "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg=="
"resolved" "https://registry.npmmirror.com/form-data/-/form-data-3.0.1.tgz"
"version" "3.0.1"
dependencies:
"asynckit" "^0.4.0"
"combined-stream" "^1.0.8"
"mime-types" "^2.1.12"
"functional-red-black-tree@^1.0.1":
"integrity" "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g=="
"resolved" "https://registry.npmmirror.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz"
"version" "1.0.1"
"glob-parent@^5.1.2":
"integrity" "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="
"resolved" "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz"
"version" "5.1.2"
dependencies:
"is-glob" "^4.0.1"
"globby@^11.1.0":
"integrity" "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="
"resolved" "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz"
"version" "11.1.0"
dependencies:
"array-union" "^2.1.0"
"dir-glob" "^3.0.1"
"fast-glob" "^3.2.9"
"ignore" "^5.2.0"
"merge2" "^1.4.1"
"slash" "^3.0.0"
"https-proxy-agent@^5.0.1":
"integrity" "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="
"resolved" "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz"
"version" "5.0.1"
dependencies:
"agent-base" "6"
"debug" "4"
"iconv-lite@~0.4.13":
"integrity" "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="
"resolved" "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz"
"version" "0.4.24"
dependencies:
"safer-buffer" ">= 2.1.2 < 3"
"ignore@^5.2.0":
"integrity" "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ=="
"resolved" "https://registry.npmmirror.com/ignore/-/ignore-5.2.0.tgz"
"version" "5.2.0"
"is-alphabetical@^1.0.0":
"integrity" "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="
"resolved" "https://registry.npmmirror.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz"
"version" "1.0.4"
"is-alphanumerical@^1.0.0":
"integrity" "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A=="
"resolved" "https://registry.npmmirror.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz"
"version" "1.0.4"
dependencies:
"is-alphabetical" "^1.0.0"
"is-decimal" "^1.0.0"
"is-buffer@^2.0.0":
"integrity" "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
"resolved" "https://registry.npmmirror.com/is-buffer/-/is-buffer-2.0.5.tgz"
"version" "2.0.5"
"is-decimal@^1.0.0":
"integrity" "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw=="
"resolved" "https://registry.npmmirror.com/is-decimal/-/is-decimal-1.0.4.tgz"
"version" "1.0.4"
"is-extglob@^2.1.1":
"integrity" "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
"resolved" "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz"
"version" "2.1.1"
"is-glob@^4.0.1", "is-glob@^4.0.3":
"integrity" "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="
"resolved" "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz"
"version" "4.0.3"
dependencies:
"is-extglob" "^2.1.1"
"is-hexadecimal@^1.0.0":
"integrity" "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="
"resolved" "https://registry.npmmirror.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz"
"version" "1.0.4"
"is-number@^7.0.0":
"integrity" "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
"resolved" "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz"
"version" "7.0.0"
"is-plain-obj@^2.0.0":
"integrity" "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="
"resolved" "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz"
"version" "2.1.0"
"js-yaml@^3.14.1":
"integrity" "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="
"resolved" "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.1.tgz"
"version" "3.14.1"
dependencies:
"argparse" "^1.0.7"
"esprima" "^4.0.0"
"longest-streak@^2.0.0":
"integrity" "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg=="
"resolved" "https://registry.npmmirror.com/longest-streak/-/longest-streak-2.0.4.tgz"
"version" "2.0.4"
"lru-cache@^6.0.0":
"integrity" "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="
"resolved" "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz"
"version" "6.0.0"
dependencies:
"yallist" "^4.0.0"
"markdown-table@^2.0.0":
"integrity" "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A=="
"resolved" "https://registry.npmmirror.com/markdown-table/-/markdown-table-2.0.0.tgz"
"version" "2.0.0"
dependencies:
"repeat-string" "^1.0.0"
"mdast-util-find-and-replace@^1.1.0":
"integrity" "sha512-9cKl33Y21lyckGzpSmEQnIDjEfeeWelN5s1kUW1LwdB0Fkuq2u+4GdqcGEygYxJE8GVqCl0741bYXHgamfWAZA=="
"resolved" "https://registry.npmmirror.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-1.1.1.tgz"
"version" "1.1.1"
dependencies:
"escape-string-regexp" "^4.0.0"
"unist-util-is" "^4.0.0"
"unist-util-visit-parents" "^3.0.0"
"mdast-util-from-markdown@^0.8.0":
"integrity" "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ=="
"resolved" "https://registry.npmmirror.com/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz"
"version" "0.8.5"
dependencies:
"@types/mdast" "^3.0.0"
"mdast-util-to-string" "^2.0.0"
"micromark" "~2.11.0"
"parse-entities" "^2.0.0"
"unist-util-stringify-position" "^2.0.0"
"mdast-util-gfm-autolink-literal@^0.1.0":
"integrity" "sha512-GjmLjWrXg1wqMIO9+ZsRik/s7PLwTaeCHVB7vRxUwLntZc8mzmTsLVr6HW1yLokcnhfURsn5zmSVdi3/xWWu1A=="
"resolved" "https://registry.npmmirror.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-0.1.3.tgz"
"version" "0.1.3"
dependencies:
"ccount" "^1.0.0"
"mdast-util-find-and-replace" "^1.1.0"
"micromark" "^2.11.3"
"mdast-util-gfm-strikethrough@^0.2.0":
"integrity" "sha512-5OQLXpt6qdbttcDG/UxYY7Yjj3e8P7X16LzvpX8pIQPYJ/C2Z1qFGMmcw+1PZMUM3Z8wt8NRfYTvCni93mgsgA=="
"resolved" "https://registry.npmmirror.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-0.2.3.tgz"
"version" "0.2.3"
dependencies:
"mdast-util-to-markdown" "^0.6.0"
"mdast-util-gfm-table@^0.1.0":
"integrity" "sha512-j4yDxQ66AJSBwGkbpFEp9uG/LS1tZV3P33fN1gkyRB2LoRL+RR3f76m0HPHaby6F4Z5xr9Fv1URmATlRRUIpRQ=="
"resolved" "https://registry.npmmirror.com/mdast-util-gfm-table/-/mdast-util-gfm-table-0.1.6.tgz"
"version" "0.1.6"
dependencies:
"markdown-table" "^2.0.0"
"mdast-util-to-markdown" "~0.6.0"
"mdast-util-gfm-task-list-item@^0.1.0":
"integrity" "sha512-/d51FFIfPsSmCIRNp7E6pozM9z1GYPIkSy1urQ8s/o4TC22BZ7DqfHFWiqBD23bc7J3vV1Fc9O4QIHBlfuit8A=="
"resolved" "https://registry.npmmirror.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-0.1.6.tgz"
"version" "0.1.6"
dependencies:
"mdast-util-to-markdown" "~0.6.0"
"mdast-util-gfm@^0.1.0":
"integrity" "sha512-NNkhDx/qYcuOWB7xHUGWZYVXvjPFFd6afg6/e2g+SV4r9q5XUcCbV4Wfa3DLYIiD+xAEZc6K4MGaE/m0KDcPwQ=="
"resolved" "https://registry.npmmirror.com/mdast-util-gfm/-/mdast-util-gfm-0.1.2.tgz"
"version" "0.1.2"
dependencies:
"mdast-util-gfm-autolink-literal" "^0.1.0"
"mdast-util-gfm-strikethrough" "^0.2.0"
"mdast-util-gfm-table" "^0.1.0"
"mdast-util-gfm-task-list-item" "^0.1.0"
"mdast-util-to-markdown" "^0.6.1"
"mdast-util-to-markdown@^0.6.0", "mdast-util-to-markdown@^0.6.1", "mdast-util-to-markdown@~0.6.0":
"integrity" "sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ=="
"resolved" "https://registry.npmmirror.com/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz"
"version" "0.6.5"
dependencies:
"@types/unist" "^2.0.0"
"longest-streak" "^2.0.0"
"mdast-util-to-string" "^2.0.0"
"parse-entities" "^2.0.0"
"repeat-string" "^1.0.0"
"zwitch" "^1.0.0"
"mdast-util-to-string@^2.0.0":
"integrity" "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w=="
"resolved" "https://registry.npmmirror.com/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz"
"version" "2.0.0"
"merge2@^1.3.0", "merge2@^1.4.1":
"integrity" "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="
"resolved" "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz"
"version" "1.4.1"
"micromark-extension-gfm-autolink-literal@~0.5.0":
"integrity" "sha512-ePiDGH0/lhcngCe8FtH4ARFoxKTUelMp4L7Gg2pujYD5CSMb9PbblnyL+AAMud/SNMyusbS2XDSiPIRcQoNFAw=="
"resolved" "https://registry.npmmirror.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-0.5.7.tgz"
"version" "0.5.7"
dependencies:
"micromark" "~2.11.3"
"micromark-extension-gfm-strikethrough@~0.6.5":
"integrity" "sha512-PpOKlgokpQRwUesRwWEp+fHjGGkZEejj83k9gU5iXCbDG+XBA92BqnRKYJdfqfkrRcZRgGuPuXb7DaK/DmxOhw=="
"resolved" "https://registry.npmmirror.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-0.6.5.tgz"
"version" "0.6.5"
dependencies:
"micromark" "~2.11.0"
"micromark-extension-gfm-table@~0.4.0":
"integrity" "sha512-hVGvESPq0fk6ALWtomcwmgLvH8ZSVpcPjzi0AjPclB9FsVRgMtGZkUcpE0zgjOCFAznKepF4z3hX8z6e3HODdA=="
"resolved" "https://registry.npmmirror.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-0.4.3.tgz"
"version" "0.4.3"
dependencies:
"micromark" "~2.11.0"
"micromark-extension-gfm-tagfilter@~0.3.0":
"integrity" "sha512-9GU0xBatryXifL//FJH+tAZ6i240xQuFrSL7mYi8f4oZSbc+NvXjkrHemeYP0+L4ZUT+Ptz3b95zhUZnMtoi/Q=="
"resolved" "https://registry.npmmirror.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-0.3.0.tgz"
"version" "0.3.0"
"micromark-extension-gfm-task-list-item@~0.3.0":
"integrity" "sha512-0zvM5iSLKrc/NQl84pZSjGo66aTGd57C1idmlWmE87lkMcXrTxg1uXa/nXomxJytoje9trP0NDLvw4bZ/Z/XCQ=="
"resolved" "https://registry.npmmirror.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-0.3.3.tgz"
"version" "0.3.3"
dependencies:
"micromark" "~2.11.0"
"micromark-extension-gfm@^0.3.0":
"integrity" "sha512-oVN4zv5/tAIA+l3GbMi7lWeYpJ14oQyJ3uEim20ktYFAcfX1x3LNlFGGlmrZHt7u9YlKExmyJdDGaTt6cMSR/A=="
"resolved" "https://registry.npmmirror.com/micromark-extension-gfm/-/micromark-extension-gfm-0.3.3.tgz"
"version" "0.3.3"
dependencies:
"micromark" "~2.11.0"
"micromark-extension-gfm-autolink-literal" "~0.5.0"
"micromark-extension-gfm-strikethrough" "~0.6.5"
"micromark-extension-gfm-table" "~0.4.0"
"micromark-extension-gfm-tagfilter" "~0.3.0"
"micromark-extension-gfm-task-list-item" "~0.3.0"
"micromark@^2.11.3", "micromark@~2.11.0", "micromark@~2.11.3":
"integrity" "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA=="
"resolved" "https://registry.npmmirror.com/micromark/-/micromark-2.11.4.tgz"
"version" "2.11.4"
dependencies:
"debug" "^4.0.0"
"parse-entities" "^2.0.0"
"micromatch@^4.0.4":
"integrity" "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA=="
"resolved" "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.5.tgz"
"version" "4.0.5"
dependencies:
"braces" "^3.0.2"
"picomatch" "^2.3.1"
"mime-db@1.52.0":
"integrity" "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
"resolved" "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz"
"version" "1.52.0"
"mime-types@^2.1.12":
"integrity" "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="
"resolved" "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz"
"version" "2.1.35"
dependencies:
"mime-db" "1.52.0"
"moment@2.29.2":
"integrity" "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg=="
"resolved" "https://registry.npmmirror.com/moment/-/moment-2.29.2.tgz"
"version" "2.29.2"
"ms@2.1.2":
"integrity" "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"resolved" "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz"
"version" "2.1.2"
"node-fetch@^2.6.1":
"integrity" "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="
"resolved" "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.6.7.tgz"
"version" "2.6.7"
dependencies:
"whatwg-url" "^5.0.0"
"obsidian@latest":
"integrity" "sha512-CQz+B2HSbhGVEBwZBL3rPl29ruOBmEhCbBmW7PIILnnRh6fFFvYy3kZLHVTUidzvRGZnEW/mQ7n9LXeJCp2a/Q=="
"resolved" "https://registry.npmmirror.com/obsidian/-/obsidian-0.14.8.tgz"
"version" "0.14.8"
dependencies:
"@codemirror/state" "^0.19.6"
"@codemirror/view" "^0.19.31"
"@types/codemirror" "0.0.108"
"moment" "2.29.2"
"parse-entities@^2.0.0":
"integrity" "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ=="
"resolved" "https://registry.npmmirror.com/parse-entities/-/parse-entities-2.0.0.tgz"
"version" "2.0.0"
dependencies:
"character-entities" "^1.0.0"
"character-entities-legacy" "^1.0.0"
"character-reference-invalid" "^1.0.0"
"is-alphanumerical" "^1.0.0"
"is-decimal" "^1.0.0"
"is-hexadecimal" "^1.0.0"
"path-type@^4.0.0":
"integrity" "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
"resolved" "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz"
"version" "4.0.0"
"picomatch@^2.3.1":
"integrity" "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
"resolved" "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz"
"version" "2.3.1"
"queue-microtask@^1.2.2":
"integrity" "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
"resolved" "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz"
"version" "1.2.3"
"regexpp@^3.2.0":
"integrity" "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg=="
"resolved" "https://registry.npmmirror.com/regexpp/-/regexpp-3.2.0.tgz"
"version" "3.2.0"
"remark-gfm@^1.0.0":
"integrity" "sha512-KfexHJCiqvrdBZVbQ6RopMZGwaXz6wFJEfByIuEwGf0arvITHjiKKZ1dpXujjH9KZdm1//XJQwgfnJ3lmXaDPA=="
"resolved" "https://registry.npmmirror.com/remark-gfm/-/remark-gfm-1.0.0.tgz"
"version" "1.0.0"
dependencies:
"mdast-util-gfm" "^0.1.0"
"micromark-extension-gfm" "^0.3.0"
"remark-parse@^9.0.0":
"integrity" "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw=="
"resolved" "https://registry.npmmirror.com/remark-parse/-/remark-parse-9.0.0.tgz"
"version" "9.0.0"
dependencies:
"mdast-util-from-markdown" "^0.8.0"
"repeat-string@^1.0.0":
"integrity" "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w=="
"resolved" "https://registry.npmmirror.com/repeat-string/-/repeat-string-1.6.1.tgz"
"version" "1.6.1"
"reusify@^1.0.4":
"integrity" "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="
"resolved" "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz"
"version" "1.0.4"
"run-parallel@^1.1.9":
"integrity" "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="
"resolved" "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz"
"version" "1.2.0"
dependencies:
"queue-microtask" "^1.2.2"
"safer-buffer@>= 2.1.2 < 3":
"integrity" "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
"resolved" "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz"
"version" "2.1.2"
"semver@^7.3.7":
"integrity" "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g=="
"resolved" "https://registry.npmmirror.com/semver/-/semver-7.3.7.tgz"
"version" "7.3.7"
dependencies:
"lru-cache" "^6.0.0"
"slash@^3.0.0":
"integrity" "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="
"resolved" "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz"
"version" "3.0.0"
"sprintf-js@~1.0.2":
"integrity" "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
"resolved" "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz"
"version" "1.0.3"
"style-mod@^4.0.0":
"integrity" "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw=="
"resolved" "https://registry.npmmirror.com/style-mod/-/style-mod-4.0.0.tgz"
"version" "4.0.0"
"to-regex-range@^5.0.1":
"integrity" "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="
"resolved" "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz"
"version" "5.0.1"
dependencies:
"is-number" "^7.0.0"
"tr46@~0.0.3":
"integrity" "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
"resolved" "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz"
"version" "0.0.3"
"trough@^1.0.0":
"integrity" "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA=="
"resolved" "https://registry.npmmirror.com/trough/-/trough-1.0.5.tgz"
"version" "1.0.5"
"tslib@^1.8.1":
"integrity" "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
"resolved" "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz"
"version" "1.14.1"
"tslib@2.3.1":
"integrity" "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
"resolved" "https://registry.npmmirror.com/tslib/-/tslib-2.3.1.tgz"
"version" "2.3.1"
"tsutils@^3.21.0":
"integrity" "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA=="
"resolved" "https://registry.npmmirror.com/tsutils/-/tsutils-3.21.0.tgz"
"version" "3.21.0"
dependencies:
"tslib" "^1.8.1"
"typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", "typescript@4.4.4":
"integrity" "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA=="
"resolved" "https://registry.npmmirror.com/typescript/-/typescript-4.4.4.tgz"
"version" "4.4.4"
"unified@^9.2.1":
"integrity" "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ=="
"resolved" "https://registry.npmmirror.com/unified/-/unified-9.2.2.tgz"
"version" "9.2.2"
dependencies:
"bail" "^1.0.0"
"extend" "^3.0.0"
"is-buffer" "^2.0.0"
"is-plain-obj" "^2.0.0"
"trough" "^1.0.0"
"vfile" "^4.0.0"
"unist-util-is@^4.0.0":
"integrity" "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg=="
"resolved" "https://registry.npmmirror.com/unist-util-is/-/unist-util-is-4.1.0.tgz"
"version" "4.1.0"
"unist-util-stringify-position@^2.0.0":
"integrity" "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g=="
"resolved" "https://registry.npmmirror.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz"
"version" "2.0.3"
dependencies:
"@types/unist" "^2.0.2"
"unist-util-visit-parents@^3.0.0":
"integrity" "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg=="
"resolved" "https://registry.npmmirror.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz"
"version" "3.1.1"
dependencies:
"@types/unist" "^2.0.0"
"unist-util-is" "^4.0.0"
"vfile-message@^2.0.0":
"integrity" "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ=="
"resolved" "https://registry.npmmirror.com/vfile-message/-/vfile-message-2.0.4.tgz"
"version" "2.0.4"
dependencies:
"@types/unist" "^2.0.0"
"unist-util-stringify-position" "^2.0.0"
"vfile@^4.0.0":
"integrity" "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA=="
"resolved" "https://registry.npmmirror.com/vfile/-/vfile-4.2.1.tgz"
"version" "4.2.1"
dependencies:
"@types/unist" "^2.0.0"
"is-buffer" "^2.0.0"
"unist-util-stringify-position" "^2.0.0"
"vfile-message" "^2.0.0"
"w3c-keyname@^2.2.4":
"integrity" "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw=="
"resolved" "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.4.tgz"
"version" "2.2.4"
"webidl-conversions@^3.0.0":
"integrity" "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
"resolved" "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz"
"version" "3.0.1"
"whatwg-url@^5.0.0":
"integrity" "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="
"resolved" "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz"
"version" "5.0.0"
dependencies:
"tr46" "~0.0.3"
"webidl-conversions" "^3.0.0"
"yallist@^4.0.0":
"integrity" "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
"resolved" "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz"
"version" "4.0.0"
"yaml-front-matter@^4.1.1":
"integrity" "sha512-ULGbghCLsN8Hs8vfExlqrJIe8Hl2TUjD7/zsIGMP8U+dgRXEsDXk4yydxeZJgdGiimP1XB7zhmhOB4/HyfqOyQ=="
"resolved" "https://registry.npmmirror.com/yaml-front-matter/-/yaml-front-matter-4.1.1.tgz"
"version" "4.1.1"
dependencies:
"commander" "^6.2.0"
"js-yaml" "^3.14.1"
"yaml@^2.1.0":
"integrity" "sha512-OuAINfTsoJrY5H7CBWnKZhX6nZciXBydrMtTHr1dC4nP40X5jyTIVlogZHxSlVZM8zSgXRfgZGsaHF4+pV+JRw=="
"resolved" "https://registry.npmmirror.com/yaml/-/yaml-2.1.0.tgz"
"version" "2.1.0"
"zwitch@^1.0.0":
"integrity" "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw=="
"resolved" "https://registry.npmmirror.com/zwitch/-/zwitch-1.0.5.tgz"
"version" "1.0.5"