Pause for code

This commit is contained in:
Jiaxin Peng
2024-09-19 12:06:27 +01:00
parent 651592dfea
commit 6b9290d7b5
4 changed files with 675 additions and 0 deletions

View File

@@ -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
View 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,
) {
}
}

View File

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