diff --git a/src/manifest.json b/src/manifest.json index 443aedc2e..d1607b598 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,7 +1,7 @@ { "name" : "__MSG_extension_name__", "default_locale" : "en", - "version" : "1.1.4.6016", + "version" : "1.1.4.6022", "short_name" : "SimpRead", "description" : "__MSG_extension_desc__", "homepage_url" : "http://ksria.com/simpread", diff --git a/src/module/authorize.jsx b/src/module/authorize.jsx index b8b6bdc82..15480608a 100644 --- a/src/module/authorize.jsx +++ b/src/module/authorize.jsx @@ -287,9 +287,15 @@ export default class Auth extends React.Component { save( state, value ) { state == "pocket" && ( storage.secret.pocket.tags = value.trim() ); state == "linnk" && ( storage.secret.linnk.group_name = value.trim() ); - state == "notion" && ( storage.secret.notion.folder_id = value.trim() ); - state == "notion" && ( storage.secret.notion.type = this.state.notion.filter( item => item.value == value.trim() )[0].type ); state == "youdao" && ( storage.secret.youdao.folder_id = value.trim() ); + state == "notion_save_image" && ( storage.secret.notion.save_image = value ); + if ( state == 'notion' ) { + const obj = this.state.notion.filter( item => item.value == value.trim() )[0]; + storage.secret.notion.folder_id = value.trim(); + storage.secret.notion.type = obj.type; + obj.schema && ( storage.secret.notion.schema = obj.schema ); + obj.type == "page" && delete storage.secret.notion.schema; + } storage.Safe( () => this.setState({ secret: storage.secret }), storage.secret ); } @@ -518,14 +524,20 @@ export default class Auth extends React.Component { onChange={ (s)=>this.onChange( "notion", s ) } /> { this.state.secret.notion.access_token && -
+
{ this.state.notion ? this.save( "notion", v ) } /> :
} + + this.save( "notion_save_image", s ) } /> +
}
sendResponse({ done: response })) .catch( error => sendResponse({ fail: error })); + } else if( request.value.type == 'download' ) { + axios( request.value ) + .then( response => sendResponse({ done: response })) + .catch( error => sendResponse({ fail: error })); } } return true; }); + +/** + * Listen runtime message, include: `notion` + */ +const downLoadCache = new Map(); + +browser.runtime.onMessage.addListener( async function ( request, sender, sendResponse ) { + if ( request.type == msg.MESSAGE_ACTION.notion_dl_img ) { + try { + const option = request.value, + { url, protocol } = option, + dlRes = await axios({ method: 'get', url: url.replace( /https?:/, protocol ), responseType: 'blob', }); + let blob = dlRes.data; + + if ( blob.type === 'image/webp' ) { + blob = blob.slice( 0, blob.size, 'image/jpeg' ); + } + downLoadCache.set( url, blob ); + sendResponse({ + done: { + type: blob.type, + size: blob.size, + url, + }, + }); + } catch ( err ) { + sendResponse({ fail: err }); + } + } else if ( request.type == msg.MESSAGE_ACTION.notion_up_img ) { + try { + const option = request.value, + { url, upUrl } = option, + blob = downLoadCache.get( url ); + + await axios.put( upUrl, blob, { + headers: { + 'Content-Type': blob.type, + }, + }); + + downLoadCache.delete( url ); + sendResponse({ done: true }); + } catch ( err ) { + sendResponse({ fail: err }); + } + } + return true; +}); \ No newline at end of file diff --git a/src/service/export.js b/src/service/export.js index 874b2e0af..d49744e2b 100644 --- a/src/service/export.js +++ b/src/service/export.js @@ -1248,6 +1248,17 @@ class Notion { return "https://www.notion.so/"; } + hasWriteRule(role){ + return role === 'read_and_write' || role === 'editor' + } + + getBlockName(titleArray){ + if (!titleArray) return 'Undefined' + if (Array.isArray(titleArray)) + return titleArray.map((t) => t[0]).join('') + return 'Undefined' + } + UUID() { var __extends=void 0&&(void 0).__extends||function(){var _extendStatics=function extendStatics(d,b){_extendStatics=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(d,b){d.__proto__=b}||function(d,b){for(var p in b)if(b.hasOwnProperty(p))d[p]=b[p]};return _extendStatics(d,b)};return function(d,b){_extendStatics(d,b);function __(){this.constructor=d}d.prototype=b===null?Object.create(b):(__.prototype=b.prototype,new __())}}();var ValueUUID=function(){function ValueUUID(_value){this._value=_value;this._value=_value}ValueUUID.prototype.asHex=function(){return this._value};return ValueUUID}();var V4UUID=function(_super){__extends(V4UUID,_super);function V4UUID(){return _super.call(this,[V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),'-',V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),'-','4',V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),'-',V4UUID._oneOf(V4UUID._timeHighBits),V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),'-',V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex(),V4UUID._randomHex()].join(''))||this}V4UUID._oneOf=function(array){return array[Math.floor(array.length*Math.random())]};V4UUID._randomHex=function(){return V4UUID._oneOf(V4UUID._chars)};V4UUID._chars=['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'];V4UUID._timeHighBits=['8','9','a','b'];return V4UUID}(ValueUUID);function generateUuid(){return new V4UUID().asHex()} return generateUuid(); @@ -1261,20 +1272,92 @@ class Notion { if ( result && status == "success" ) { this.access_token = Object.values( result.recordMap.notion_user )[0].value.id; this.blocks = []; - Object.values( result.recordMap.space ).forEach( item => { - item.value.pages.forEach( ( id, idx ) => { - const block = result.recordMap.block[id]; - idx == 0 && this.blocks.push({ name: item.value.name, value: block.value.id, type: block.value.type }); - if ( block.value.type == "page" ) { - this.blocks.push({ name: "  " + ( block.value.properties ? block.value.properties.title[0][0] : "Undefined" ), value: block.value.id, type: "page" }); - } else if ( block.value.type == "collection_view_page" ) { - Object.values( result.recordMap.collection ).forEach( collection => { - collection.value.parent_id == block.value.id && - this.blocks.push({ name: "  " + collection.value.name[0][0], value: collection.value.id, type: "collection" }); - }); + + /** + * 读取所有空间,并创建映射。 + */ + const spaceMaps = {} + Object.values( result.recordMap.space ).forEach(({ value: spaceValue, role }) => { + if (!this.hasWriteRule(role)) return; + spaceMaps[spaceValue.id] = { + name : spaceValue.name, + value : spaceValue.id, + type : 'space', + blocks: [], + } + }); + + /** + * 读取所有收藏空间,并创建映射。 + */ + const collectionMaps = {}; + Object.values( + result.recordMap.collection + ).forEach(({ value: collectionValue, role }) => { + if (!this.hasWriteRule(role)) return; + collectionMaps[ collectionValue.parent_id ] = collectionValue; + collectionMaps[ collectionValue.id ] = collectionValue; + }) + + /** + * 遍历所有当前用户能看到的空间。 + */ + const processCollection = ( id, spaceBlocks ) => { + const collection = collectionMaps[id]; + if ( !collection ) return; + + let schemaKey; + Object.keys( collection.schema ).some( key => { + const schema = collection.schema[key]; + if ( schema.type === 'url' && schema.name === 'URL' ) { + schemaKey = key; + return true; } - }); + return false; + }) + + const block = { + name : '  ' + this.getBlockName( collection.name ), + value : collection.id, + type : 'collection', + }; + schemaKey && ( block.schema = schemaKey ); + spaceBlocks.push( block ); + } + Object.values( result.recordMap.block ).forEach( ({ role, value: blockValue }) => { + if ( !this.hasWriteRule( role )) return; + const { + type, + space_id, + parent_id, + id, + collection_id, + } = blockValue; + + const _space = space_id ? spaceMaps[space_id] : spaceMaps[parent_id], + _spaceBlocks = _space ? _space.blocks : this.blocks; + + if (type == 'page') { + _spaceBlocks.push({ + name : '  ' + this.getBlockName( blockValue.properties.title ), + value: id, + type : 'page', + }); + } else if ( type == 'collection_view_page' ) { + processCollection( id, _spaceBlocks ); + } else if ( type == 'collection_view' ) { + processCollection( collection_id, _spaceBlocks ); + } + }); + + Object.values( spaceMaps ).forEach( space => { + const { blocks, ...spaceAttr } = space; + if ( blocks && blocks.length > 0 ) { + this.blocks.push({ name: spaceAttr.name, type: blocks[0].type, value: blocks[0].value }); + this.blocks.push( ...blocks ); + } }); + this.type = this.blocks[0].type; this.folder_id = this.blocks[0].value; callback( result, undefined ); @@ -1285,7 +1368,108 @@ class Notion { }); } - Add( title, content, callback ) { + MathImages( content ) { + const result = content.match( /!\[.*?\]\(http(.*?)\)/g ); + + if( !result ) { return []; } + + const images = result.map( o => { + const temp = /!\[.*?\]\((http.*?)\)/.exec( o ); + if ( temp ) { + return temp[1]; + } + return ''; + }).filter( o => o && !~o.indexOf( 'secure.notion-static.com/' )); + return images; + } + + DonwloadOriginImage( url ) { + return new Promise(( resolve, reject ) => { + browser.runtime.sendMessage( + msg.Add( msg.MESSAGE_ACTION.notion_dl_img, { + url, + protocol: window.location.protocol + }), res => { + if ( res.done ) resolve( res.done ); + else reject( res.fail ); + }); + }); + } + + UploadOriginImage({ type, size, url }) { + return new Promise(( resolve, reject ) => { + this.GetFileUrl( + this.UUID(), + urls => { + browser.runtime.sendMessage( + msg.Add( msg.MESSAGE_ACTION.notion_up_img, { + url: url, + upUrl: urls.signedPutUrl, + }), res => { + if ( res.done ) { + resolve({ from: url, to: urls.url }); + } else resolve(); + } + ) + }, + { + bucket: 'secure', + contentType: type, + } + ) + }) + } + + async UploadImages( content ) { + + let completeCount = 0; + const images = [ ...new Set( this.MathImages( content ))], + imagesCount = images.length, + replacements = [], + uploadImage = this.UploadOriginImage.bind( this ), + notify = new Notify().Render({ state: "loading", content: `正在上传图片到 Notion ,请稍等` }), + updateNotify = () => { + notify.update( `正在上传图片到 Notion ,当前进度 ${ completeCount }/${ imagesCount }` ); + }, + escapeRegExp = str => { + return str.replace( /[.*+\-?^${}()|[\]\\]/g, '\\$&' ); // $& means the whole matched string + }; + replacements.push( + await images.reduce(( prevPromise, imageUrl ) => { + if ( !imageUrl ) return; + return prevPromise.then(( replacement ) => { + if ( replacement ) { + replacements.push( replacement ); + } + completeCount ++; + updateNotify(); + return this.DonwloadOriginImage( imageUrl ).then( + uploadImage, + err => { + console.error(err) + } + ) + }); + }, Promise.resolve()) + ) + notify.complete(); + + replacements.forEach(( replacement ) => { + if ( replacement ) { + content = content.replace( + new RegExp(escapeRegExp( replacement.from ), 'g' ), + replacement.to + ) + } + }) + + return content; + } + + async Add( title, content, callback ) { + if ( this.save_image ) { + content = await this.UploadImages( content ); + } this.TempFile( this.folder_id, title, ( documentId, error ) => { console.log( 'TempFile: ', documentId ) if ( error ) { @@ -1409,14 +1593,13 @@ class Notion { }); } - GetFileUrl( name, callback ) { + GetFileUrl( name, callback, option = { bucket: 'temporary', contentType: 'text/markdown' } ) { browser.runtime.sendMessage( msg.Add( msg.MESSAGE_ACTION.AXIOS, { type: "post", url: this.url + "api/v3/getUploadFileUrl", data:{ - bucket: 'temporary', name: name, - contentType: 'text/markdown', + ...option } }), result => { if ( result && result.done ) { @@ -1457,7 +1640,151 @@ class Notion { }, } } - }), result => callback( result )); + }), result => { + if ( this.type == 'collection' ) { + /* result.done && this.CheckQueueTask(result.done.data.taskId, () => { + this.SetProperties(documentId, () => callback(result)) + }) */ + const taskId = result.done.data.taskId; + if ( result.done ) { + this.CheckQueueTask( taskId, () => { + if ( this.schema ) { + this.SetProperties( documentId, () => callback( result )); + } else { + this.GetCollectionData( collection => { + this.AddCollectionUrlSchema( collection, schemaKey => { + this.schema = schemaKey; + this.SetProperties( documentId, () => callback( result )); + }); + }); + } + }) + } + } else callback( result ); + }); + } + + GetCollectionData( callback ) { + browser.runtime.sendMessage( + msg.Add( msg.MESSAGE_ACTION.AXIOS, { + type: 'post', + url : this.url + 'api/v3/loadUserContent', + }), result => { + if (result.done) { + const { + recordMap: { collection }, + } = result.done.data, + currentCollection = collection[ this.folder_id ].value; + callback( currentCollection ); + } + } + ) + } + + AddCollectionUrlSchema( collection, callback ) { + const { schema } = collection, + schemaKeys = Object.keys( schema ), + genSchemaKey = ( len = 4 ) => { + let key = ''; + for ( ; key.length < len; ) { + key += String.fromCharCode( 33 + 94 * Math.random()); + } + return key; + }; + let schemaKey = null; + + schemaKeys.some( key => { + const schemaItem = schema[key]; + if ( schemaItem.type === 'url' && schemaItem.name === 'URL' ) { + schemaKey = key; + return true; + } + return false; + }); + + if ( schemaKey ) { + callback( schemaKey ); + return; + } + + const newSchema = { ...schema }; + let newSchemaKey = ''; + while ( !newSchemaKey && schemaKeys.indexOf(newSchemaKey) < 0 ) { + newSchemaKey = genSchemaKey(); + } + + newSchema[ newSchemaKey ] = { type: 'url', name: 'URL' }; + + browser.runtime.sendMessage( + msg.Add( msg.MESSAGE_ACTION.AXIOS, { + type: 'post', + url : this.url + 'api/v3/submitTransaction', + data: { + operations: [{ + args: { + schema: newSchema, + }, + command: 'update', + id: this.folder_id, + path: [], + table: 'collection', + }], + }, + }), result => { + if ( result.done ) { + callback( newSchemaKey ); + } + } + ) + } + + CheckQueueTask( taskId, callback ) { + if ( taskId ) { + browser.runtime.sendMessage( + msg.Add(msg.MESSAGE_ACTION.AXIOS, { + type: 'post', + url : this.url + 'api/v3/getTasks', + data: { + taskIds: [taskId], + }, + }), result => { + if ( result.done ) { + const { results } = result.done.data; + if ( results[0].state !== 'success' ) { + setTimeout( () => { + this.CheckQueueTask( taskId, callback ); + }, 1000 ); + } else callback(); + } + } + ) + } + } + + SetProperties( documentId, callback ) { + browser.runtime.sendMessage( + msg.Add( msg.MESSAGE_ACTION.AXIOS, { + type: 'post', + url : this.url + 'api/v3/submitTransaction', + data: { + operations: [{ + id: documentId, + table: 'block', + path: ['properties', this.schema], + command: 'set', + args: [[ window.location.origin, [[ 'a', window.location.href ]]]], + }], + }, + }), result => { + result.done && callback( documentId, undefined ); + } + ) + } + + Save( storage ) { + storage.Safe( () => { + // TO-DO + }, storage.secret ); } } diff --git a/src/service/message.js b/src/service/message.js index 57f81c72c..82668d8d4 100644 --- a/src/service/message.js +++ b/src/service/message.js @@ -53,6 +53,9 @@ const action = { // tips tips : "tips", tips_norepeat : "tips_norepeat", + // notion.so + notion_dl_img : "notion_dl_img", + notion_up_img : "notion_up_img", }; /** diff --git a/src/service/output.js b/src/service/output.js index 80d104d20..5bcb5978a 100644 --- a/src/service/output.js +++ b/src/service/output.js @@ -316,7 +316,15 @@ function action( type, title, desc, content ) { corbLoader( "load", () => { notion.access_token = storage.secret.notion.access_token; notion.folder_id = storage.secret.notion.folder_id; - notion.Add( title, result.replace( /.jpeg!720/ig, '.jpeg' ), ( result, error ) => { + notion.save_image = storage.secret.notion.save_image; + notion.schema = storage.secret.notion.schema; + notion.type = storage.secret.notion.type; + notion.Add( title, result.replace( /.(png|jpe?g)!\d+/ig, '.$1' ).replace( /, 原文地址 \S+\)/i, '\n' ), ( result, error ) => { + // hack code + if ( notion.type == "collection" && notion.schema != storage.secret.notion.schema ) { + storage.secret.notion.schema = notion.schema; + notion.Save( storage ); + } exp.svcCbWrapper( result, error, notion.name, type, new Notify() ) }); }, 500 ); diff --git a/src/service/storage.js b/src/service/storage.js index 133b775e5..fe4113d9a 100644 --- a/src/service/storage.js +++ b/src/service/storage.js @@ -253,6 +253,7 @@ let current = {}, "notion" : { access_token : "", type : "", + save_image : false, folder_id : "", }, "youdao" : { diff --git a/src/service/version.js b/src/service/version.js index 6034da9d3..e6c3d1af7 100644 --- a/src/service/version.js +++ b/src/service/version.js @@ -37,6 +37,7 @@ const version = browser.runtime.getManifest().version.replace( /.\d{2,}/, "" ), ]), patchs = new Map([ [ "1.1.4.6016", "修复 Notion 相关问题,并支持 Database 导出方案," ], + [ "1.1.4.6022", "修复 Notion 授权问题,并支持 图床 导出方案," ], ]), tips = { "root" : value => `.version-tips[data-hits='${value}']`, diff --git a/src/vender/notify/notify.js b/src/vender/notify/notify.js index 9fe49119a..e7e62e4af 100644 --- a/src/vender/notify/notify.js +++ b/src/vender/notify/notify.js @@ -58,7 +58,7 @@ * */ var Notify = ( function () { - var VERSION = "2.0.2.0105", + var VERSION = "2.0.2.0621", name = "notify", root = "notify-gp", roottmpl= "<" + root + ">", @@ -184,6 +184,11 @@ var Notify = ( function () { this.title ? $title.text( this.title ) : $title.hide(); this.content ? $content.html( this.content ) : $content.hide(); + this.update = function( content ) { + this.content = content; + this.content ? $content.html( this.content ) : $content.hide(); + } + if ( this.mode === MODE.modal ) { $target.addClass( "notify-modal" ); $content.addClass( "notify-modal-content" );