mirror of
https://github.com/jxpeng98/obsidian-to-NotionNext
synced 2026-07-30 08:58:36 +08:00
Pause for code
This commit is contained in:
@@ -85,6 +85,7 @@ export default class RibbonCommands {
|
||||
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);
|
||||
|
||||
526
src/upload/Upload2Notion.ts
Normal file
526
src/upload/Upload2Notion.ts
Normal file
@@ -0,0 +1,526 @@
|
||||
import {App, Notice, Platform, TFile, requestUrl} from "obsidian";
|
||||
import {markdownToBlocks} from "@jxpeng98/martian";
|
||||
import * as yamlFrontMatter from "yaml-front-matter";
|
||||
import MyPlugin from "src/main";
|
||||
import {DatabaseDetails, PluginSettings} from "../../ui/settingTabs";
|
||||
import {updateYamlInfo} from "../updateYaml";
|
||||
import {UploadBase} from "./UploadBase";
|
||||
import fetch from 'node-fetch';
|
||||
import {i18nConfig} from "../../lang/I18n";
|
||||
|
||||
interface CreatePageResponse {
|
||||
response: any;
|
||||
data: any;
|
||||
}
|
||||
|
||||
export class Upload2Notion extends UploadBase {
|
||||
settings: PluginSettings;
|
||||
dbDetails: DatabaseDetails;
|
||||
|
||||
constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) {
|
||||
super(plugin, dbDetails);
|
||||
this.dbDetails = dbDetails;
|
||||
}
|
||||
|
||||
async updatePage(
|
||||
notionID: string,
|
||||
cover: string,
|
||||
Values: Record<string, string>,
|
||||
childArr: any,
|
||||
) {
|
||||
await this.deletePage(notionID);
|
||||
|
||||
const {databaseID} = this.dbDetails;
|
||||
|
||||
const databaseCover = await this.getDataBase(databaseID);
|
||||
|
||||
if (cover == null) {
|
||||
cover = databaseCover;
|
||||
}
|
||||
|
||||
return await this.createPage(cover, Values, childArr);
|
||||
}
|
||||
|
||||
async createPage(
|
||||
cover: string,
|
||||
Values: Record<string, string>,
|
||||
childArr: any,
|
||||
): Promise<CreatePageResponse> {
|
||||
|
||||
// general
|
||||
const {
|
||||
format,
|
||||
databaseID,
|
||||
notionAPI
|
||||
} = this.dbDetails;
|
||||
|
||||
// next
|
||||
const {
|
||||
format,
|
||||
databaseID,
|
||||
notionAPI
|
||||
} = this.dbDetails;
|
||||
|
||||
// custom
|
||||
const {
|
||||
format,
|
||||
databaseID,
|
||||
customProperties,
|
||||
notionAPI
|
||||
} = this.dbDetails;
|
||||
|
||||
// remove the annotations from the childArr if type is code block
|
||||
childArr.forEach((block: any) => {
|
||||
if (block.type === "code") {
|
||||
block.code.rich_text.forEach((item: any) => {
|
||||
if (item.type === "text" && item.annotations) {
|
||||
delete item.annotations;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// check the length of the childArr and split it into chunks of 100
|
||||
const childArrLength = childArr.length;
|
||||
let extraArr: any[] = [];
|
||||
let firstArr: any;
|
||||
let pushCount = 0;
|
||||
|
||||
console.log(`Page includes ${childArrLength} blocks`)
|
||||
|
||||
if (childArrLength > 100) {
|
||||
for (let i = 0; i < childArr.length; i += 100) {
|
||||
if (i == 0) {
|
||||
firstArr = childArr.slice(0, 100);
|
||||
} else {
|
||||
const chunk = childArr.slice(i, i + 100);
|
||||
extraArr.push(chunk);
|
||||
pushCount++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
firstArr = childArr;
|
||||
}
|
||||
|
||||
// modify for all type of format
|
||||
const bodyString: any = this.buildBodyString(customProperties, Values, firstArr);
|
||||
|
||||
// general
|
||||
if (format === "general") {
|
||||
const bodyString: any = {
|
||||
parent: {
|
||||
database_id: databaseID,
|
||||
},
|
||||
properties: {
|
||||
[customTitleButton
|
||||
? customTitleName
|
||||
: "title"]: {
|
||||
title: [
|
||||
{
|
||||
text: {
|
||||
content: title,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
...(tagButton
|
||||
? {
|
||||
tags: {
|
||||
multi_select: tags && true ? tags.map((tag) => ({name: tag})) : [],
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
children: firstArr,
|
||||
};
|
||||
}
|
||||
|
||||
// next
|
||||
if (format === "next") {
|
||||
// parse the values to the bodyString
|
||||
}
|
||||
|
||||
|
||||
// apply for all type of format
|
||||
if (cover) {
|
||||
bodyString.cover = {
|
||||
type: "external",
|
||||
external: {
|
||||
url: cover,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!bodyString.cover && this.plugin.settings.bannerUrl) {
|
||||
bodyString.cover = {
|
||||
type: "external",
|
||||
external: {
|
||||
url: this.plugin.settings.bannerUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
console.log(bodyString)
|
||||
|
||||
console.log(Platform.isDesktopApp)
|
||||
|
||||
let response: any;
|
||||
let data: any;
|
||||
|
||||
if (Platform.isMobileApp) {
|
||||
if(childArrLength > 100) {
|
||||
new Notice(i18nConfig["reach-mobile-limit"], 5000);
|
||||
} else {
|
||||
const {response} = await this.fetchMobile(bodyString);
|
||||
}
|
||||
}
|
||||
|
||||
if (Platform.isDesktopApp) {
|
||||
|
||||
const {response, data} = await this.fetchDesktopFirst(bodyString);
|
||||
|
||||
// upload the rest of the blocks
|
||||
if (pushCount > 0) {
|
||||
for (let i = 0; i < pushCount; i++) {
|
||||
const extraBlocks = {children: extraArr[i]};
|
||||
|
||||
console.log(extraBlocks)
|
||||
|
||||
const {extraResponse, extraData} = await this.fetchDesktopExtra(extraBlocks, data);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
response, // for status code
|
||||
data // for id and url
|
||||
}
|
||||
}
|
||||
|
||||
async fetchMobile(bodyString: any) {
|
||||
return await requestUrl({
|
||||
url: `https://api.notion.com/v1/pages`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// 'User-Agent': 'obsidian.md',
|
||||
Authorization:
|
||||
"Bearer " + this.dbDetails.notionAPI,
|
||||
"Notion-Version": "2022-06-28",
|
||||
},
|
||||
body: JSON.stringify(bodyString),
|
||||
});
|
||||
}
|
||||
|
||||
async fetchDesktopFirst(bodyString: any) {
|
||||
response = await fetch("https://api.notion.com/v1/pages", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + notionAPI,
|
||||
"Notion-Version": "2022-06-28",
|
||||
},
|
||||
body: JSON.stringify(bodyString),
|
||||
});
|
||||
|
||||
data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
new Notice(`Error ${data.status}: ${data.code} \n ${i18nConfig["CheckConsole"]}`, 5000);
|
||||
console.log(`Error message: \n ${data.message}`);
|
||||
} else {
|
||||
console.log(`Page created: ${data.url}`);
|
||||
console.log(`Page ID: ${data.id}`);
|
||||
}
|
||||
|
||||
return {response, data};
|
||||
}
|
||||
|
||||
async fetchDesktopExtra(extraBlocks: any, data: any) {
|
||||
const extraResponse = await fetch(`https://api.notion.com/v1/blocks/${data.id}/children`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + notionAPI,
|
||||
"Notion-Version": "2022-06-28",
|
||||
},
|
||||
body: JSON.stringify(extraBlocks),
|
||||
});
|
||||
|
||||
const extraData: any = await extraResponse.json();
|
||||
|
||||
if (!extraResponse.ok) {
|
||||
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: ${data.id}`);
|
||||
if (i === pushCount - 1) {
|
||||
console.log(`${i18nConfig["BlockUploaded"]} to page: ${data.id}`);
|
||||
new Notice(`${i18nConfig["BlockUploaded"]} page: ${data.id}`, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
return {extraResponse, extraData};
|
||||
}
|
||||
|
||||
async syncMarkdownToNotion(
|
||||
cover: string,
|
||||
Values: Record<string, string>,
|
||||
markdown: string,
|
||||
nowFile: TFile,
|
||||
app: App,
|
||||
): Promise<any> {
|
||||
const options = {
|
||||
strictImageUrls: true,
|
||||
notionLimits: {
|
||||
truncate: false,
|
||||
}
|
||||
}
|
||||
let res: any;
|
||||
const yamlContent: any = yamlFrontMatter.loadFront(markdown);
|
||||
const __content = yamlContent.__content;
|
||||
const file2Block = markdownToBlocks(__content, options);
|
||||
const frontMatter = app.metadataCache.getFileCache(nowFile)?.frontmatter;
|
||||
const {abName} = this.dbDetails
|
||||
const notionIDKey = `NotionID-${abName}`;
|
||||
const notionID = frontMatter ? frontMatter[notionIDKey] : null;
|
||||
|
||||
if (notionID) {
|
||||
res = await this.updatePage(
|
||||
notionID,
|
||||
cover,
|
||||
Values,
|
||||
file2Block,
|
||||
);
|
||||
} else {
|
||||
res = await this.createPage(cover, Values, file2Block);
|
||||
}
|
||||
|
||||
let {response, data} = res;
|
||||
|
||||
// console.log(response)
|
||||
|
||||
if (Platform.isDesktopApp) {
|
||||
if (response && response.status === 200) {
|
||||
await updateYamlInfo(
|
||||
markdown,
|
||||
nowFile,
|
||||
data,
|
||||
app,
|
||||
this.plugin,
|
||||
this.dbDetails
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (Platform.isMobileApp) {
|
||||
if (response && response.status === 200) {
|
||||
await updateYamlInfo(
|
||||
markdown,
|
||||
nowFile,
|
||||
response,
|
||||
app,
|
||||
this.plugin,
|
||||
this.dbDetails,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private buildPropertyObjectCustom(customName: string, customType: string, Values: Record<string, any>) {
|
||||
const value = Values[customName] || '';
|
||||
|
||||
switch (customType) {
|
||||
case "title":
|
||||
return {
|
||||
title: [
|
||||
{
|
||||
text: {
|
||||
content: value,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
case "rich_text":
|
||||
return {
|
||||
rich_text: [
|
||||
{
|
||||
text: {
|
||||
content: value || '',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
case "date":
|
||||
return {
|
||||
date: {
|
||||
start: value || new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
case "number":
|
||||
return {
|
||||
number: Number(value),
|
||||
};
|
||||
case "phone_number":
|
||||
return {
|
||||
phone_number: value,
|
||||
};
|
||||
case "email":
|
||||
return {
|
||||
email: value,
|
||||
};
|
||||
case "url":
|
||||
return {
|
||||
url: value,
|
||||
};
|
||||
case "files":
|
||||
return {
|
||||
files: Array.isArray(value) ? value.map(url => ({
|
||||
name: url,
|
||||
type: "external",
|
||||
external: {
|
||||
url: url,
|
||||
},
|
||||
})) : [
|
||||
{
|
||||
name: value,
|
||||
type: "external",
|
||||
external: {
|
||||
url: value,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
case "checkbox":
|
||||
return {
|
||||
checkbox: Boolean(value) || false,
|
||||
};
|
||||
case "select":
|
||||
return {
|
||||
select: {
|
||||
name: value,
|
||||
},
|
||||
};
|
||||
case "multi_select":
|
||||
return {
|
||||
multi_select: Array.isArray(value) ? value.map(item => ({name: item})) : [{name: value}],
|
||||
};
|
||||
// add cases for next
|
||||
case "type":
|
||||
return {
|
||||
select: {
|
||||
name: value || 'Post',
|
||||
},
|
||||
};
|
||||
case "status":
|
||||
return {
|
||||
select: {
|
||||
name: value || 'Draft',
|
||||
},
|
||||
};
|
||||
case "category":
|
||||
return {
|
||||
select: {
|
||||
name: value || 'Obsidian',
|
||||
},
|
||||
};
|
||||
case "password":
|
||||
return {
|
||||
rich_text: [
|
||||
{
|
||||
text: {
|
||||
content: value || '',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
case "icon":
|
||||
return {
|
||||
rich_text: [
|
||||
{
|
||||
text: {
|
||||
content: value || '',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
case "date":
|
||||
return {
|
||||
date: {
|
||||
start: value || new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
case "slug":
|
||||
return {
|
||||
rich_text: [
|
||||
{
|
||||
text: {
|
||||
content: value || '',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
case "summary":
|
||||
return {
|
||||
rich_text: [
|
||||
{
|
||||
text: {
|
||||
content: value || '',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
// tags can be used for next and general
|
||||
case "tags":
|
||||
return {
|
||||
multi_select: Array.isArray(value) ? value.map(item => ({name: item})) : [{name: value}],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private buildBodyStringCustom(
|
||||
customProperties: { customName: string; customType: string }[],
|
||||
customValues: Record<string, string>,
|
||||
childArr: any,
|
||||
) {
|
||||
|
||||
const properties: { [key: string]: any } = {};
|
||||
|
||||
// Only include custom properties that have values
|
||||
customProperties.forEach(({customName, customType}) => {
|
||||
if (customValues[customName] !== undefined) {
|
||||
properties[customName] = this.buildPropertyObjectCustom(customName, customType, customValues);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// console.log(properties)
|
||||
|
||||
return {
|
||||
parent: {
|
||||
database_id: this.dbDetails.databaseID,
|
||||
},
|
||||
properties,
|
||||
children: childArr,
|
||||
};
|
||||
}
|
||||
|
||||
private buildBodyStringGeneral(
|
||||
Values: Record<string, string>,
|
||||
childArr: any,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
private buildBodyStringNext(
|
||||
Values: Record<string, string>,
|
||||
childArr: any,
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,11 @@
|
||||
import { App, Notice, requestUrl, TFile } from "obsidian";
|
||||
import { Client } from '@notionhq/client';
|
||||
import { markdownToBlocks } from "@jxpeng98/martian";
|
||||
import * as yamlFrontMatter from "yaml-front-matter";
|
||||
import MyPlugin from "src/main";
|
||||
import { DatabaseDetails } from "../../ui/settingTabs";
|
||||
|
||||
|
||||
export class UploadBase {
|
||||
plugin: MyPlugin;
|
||||
notion: Client;
|
||||
140
src/upload/getMarkdown.ts
Normal file
140
src/upload/getMarkdown.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { App, Notice } from "obsidian";
|
||||
import { i18nConfig } from "../../lang/I18n";
|
||||
import { DatabaseDetails } from "../../ui/settingTabs";
|
||||
|
||||
interface MarkdownData {
|
||||
markDownData: string;
|
||||
nowFile: TFile;
|
||||
cover: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface GeneralValues {
|
||||
title: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
interface NextValues {
|
||||
title: string;
|
||||
emoji: string;
|
||||
tags: string[];
|
||||
type: string;
|
||||
slug: string;
|
||||
stats: string;
|
||||
category: string;
|
||||
summary: string;
|
||||
paword: string;
|
||||
favicon: string;
|
||||
datetime: string;
|
||||
}
|
||||
|
||||
export async function getMarkdown(
|
||||
app: App,
|
||||
dbDetails: DatabaseDetails,
|
||||
): Promise<MarkdownData | undefined> {
|
||||
const nowFile = app.workspace.getActiveFile();
|
||||
|
||||
if (!nowFile) {
|
||||
new Notice(i18nConfig["open-file"]);
|
||||
return;
|
||||
}
|
||||
|
||||
const markDownData = await nowFile.vault.read(nowFile);
|
||||
|
||||
const { format } = dbDetails; // general, next or custom
|
||||
const cover = fileCache.frontmatter.coverurl || '';
|
||||
const FileCache = app.metadataCache.getFileCache(nowFile);
|
||||
let result: MarkdownData = { markDownData, cover }; // title will be saved in the values object
|
||||
|
||||
try {
|
||||
switch (format) {
|
||||
case "general":
|
||||
result = { ...result, ...getGeneralFormat(FileCache, dbDetails, nowFile) };
|
||||
break;
|
||||
case "next":
|
||||
result = { ...result, ...getNextFormat(FileCache, nowFile) };
|
||||
break;
|
||||
case "custom":
|
||||
result = { ...result, ...getCustomFormat(FileCache, dbDetails, nowFile) };
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported format: ${format}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing markdown:', error);
|
||||
new Notice(i18nConfig["set-tags-fail"]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: use the front matter as the title
|
||||
|
||||
function getGeneralFormat(fileCache: FileCache, nowFile: TFile): Record<string, any> {
|
||||
const { customTitleButton, customTitleName, tagButton } = dbDetails;
|
||||
const GeneralValues: GeneralValues = {
|
||||
title: '',
|
||||
tags: [],
|
||||
};
|
||||
|
||||
if (fileCache && fileCache.frontmatter) {
|
||||
GeneralValues.title = fileCache.frontmatter.title || nowFile.basename;
|
||||
GeneralValues.tags = fileCache.frontmatter.tags || [];
|
||||
}
|
||||
|
||||
return { GeneralValues };
|
||||
}
|
||||
|
||||
function getNextFormat(fileCache: FileCache, nowFile: TFile): Record<string, any> {
|
||||
const NextValues: NextValues = {
|
||||
title: '',
|
||||
emoji: '',
|
||||
tags: [],
|
||||
type: '',
|
||||
slug: '',
|
||||
stats: '',
|
||||
category: '',
|
||||
summary: '',
|
||||
paword: '',
|
||||
favicon: '',
|
||||
datetime: ''
|
||||
};
|
||||
|
||||
if (fileCache && fileCache.frontmatter) {
|
||||
NextValues.title = fileCache.frontmatter.title || nowFile.basename;
|
||||
NextValues.emoji = fileCache.frontmatter.titleicon || '';
|
||||
NextValues.tags = fileCache.frontmatter.tags || [];
|
||||
NextValues.type = fileCache.frontmatter.type || '';
|
||||
NextValues.slug = fileCache.frontmatter.slug || '';
|
||||
NextValues.stats = fileCache.frontmatter.stats || fileCache.frontmatter.status || '';
|
||||
NextValues.category = fileCache.frontmatter.category || '';
|
||||
NextValues.summary = fileCache.frontmatter.summary || '';
|
||||
NextValues.paword = fileCache.frontmatter.password || '';
|
||||
NextValues.favicon = fileCache.frontmatter.icon || '';
|
||||
NextValues.datetime = fileCache.frontmatter.date || '';
|
||||
}
|
||||
|
||||
return { NextValues };
|
||||
}
|
||||
|
||||
function getCustomFormat(fileCache: FileCache, dbDetails: DatabaseDetails, nowFile: TFile): Record<string, any> {
|
||||
const customValues: Record<string, any> = {};
|
||||
|
||||
dbDetails.customProperties
|
||||
.filter(property => property.customType !== 'title') // Exclude 'title' type property
|
||||
.forEach(({ customName }) => {
|
||||
if (fileCache.frontmatter[customName] !== undefined) {
|
||||
customValues[customName] = fileCache.frontmatter[customName];
|
||||
}
|
||||
});
|
||||
|
||||
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] = nowFile.basename; // Use 'basename' for the file name without extension
|
||||
}
|
||||
|
||||
return { customValues };
|
||||
}
|
||||
Reference in New Issue
Block a user