Support export to and import from Google Drive
This commit is contained in:
parent
c183002c14
commit
845ef9bad6
8 changed files with 528 additions and 5 deletions
|
@ -329,6 +329,8 @@ function actionDownload(req, res, noteId) {
|
||||||
filename = encodeURIComponent(filename);
|
filename = encodeURIComponent(filename);
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Access-Control-Allow-Origin': '*', //allow CORS as API
|
'Access-Control-Allow-Origin': '*', //allow CORS as API
|
||||||
|
'Access-Control-Allow-Headers': 'Range',
|
||||||
|
'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
|
||||||
'Content-Type': 'text/markdown; charset=UTF-8',
|
'Content-Type': 'text/markdown; charset=UTF-8',
|
||||||
'Cache-Control': 'private',
|
'Cache-Control': 'private',
|
||||||
'Content-disposition': 'attachment; filename=' + filename + '.md',
|
'Content-disposition': 'attachment; filename=' + filename + '.md',
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
var domain = 'change this'; // domain name
|
var domain = 'change this'; // domain name
|
||||||
var urlpath = ''; // sub url path, like: www.example.com/<urlpath>
|
var urlpath = ''; // sub url path, like: www.example.com/<urlpath>
|
||||||
|
|
||||||
|
var GOOGLE_API_KEY = 'change this';
|
||||||
|
var GOOGLE_CLIENT_ID = 'change this';
|
||||||
|
|
||||||
var port = window.location.port;
|
var port = window.location.port;
|
||||||
var serverurl = window.location.protocol + '//' + domain + (port ? ':' + port : '') + (urlpath ? '/' + urlpath : '');
|
var serverurl = window.location.protocol + '//' + domain + (port ? ':' + port : '') + (urlpath ? '/' + urlpath : '');
|
||||||
var noteid = urlpath ? window.location.pathname.slice(urlpath.length + 1, window.location.pathname.length).split('/')[1] : window.location.pathname.split('/')[1];
|
var noteid = urlpath ? window.location.pathname.slice(urlpath.length + 1, window.location.pathname.length).split('/')[1] : window.location.pathname.split('/')[1];
|
||||||
|
|
118
public/js/google-drive-picker.js
Normal file
118
public/js/google-drive-picker.js
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
/**!
|
||||||
|
* Google Drive File Picker Example
|
||||||
|
* By Daniel Lo Nigro (http://dan.cx/)
|
||||||
|
*/
|
||||||
|
(function() {
|
||||||
|
/**
|
||||||
|
* Initialise a Google Driver file picker
|
||||||
|
*/
|
||||||
|
var FilePicker = window.FilePicker = function(options) {
|
||||||
|
// Config
|
||||||
|
this.apiKey = options.apiKey;
|
||||||
|
this.clientId = options.clientId;
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
this.buttonEl = options.buttonEl;
|
||||||
|
|
||||||
|
// Events
|
||||||
|
this.onSelect = options.onSelect;
|
||||||
|
this.buttonEl.on('click', this.open.bind(this));
|
||||||
|
|
||||||
|
// Disable the button until the API loads, as it won't work properly until then.
|
||||||
|
this.buttonEl.prop('disabled', true);
|
||||||
|
|
||||||
|
// Load the drive API
|
||||||
|
gapi.client.setApiKey(this.apiKey);
|
||||||
|
gapi.client.load('drive', 'v2', this._driveApiLoaded.bind(this));
|
||||||
|
google.load('picker', '1', { callback: this._pickerApiLoaded.bind(this) });
|
||||||
|
}
|
||||||
|
|
||||||
|
FilePicker.prototype = {
|
||||||
|
/**
|
||||||
|
* Open the file picker.
|
||||||
|
*/
|
||||||
|
open: function() {
|
||||||
|
// Check if the user has already authenticated
|
||||||
|
var token = gapi.auth.getToken();
|
||||||
|
if (token) {
|
||||||
|
this._showPicker();
|
||||||
|
} else {
|
||||||
|
// The user has not yet authenticated with Google
|
||||||
|
// We need to do the authentication before displaying the Drive picker.
|
||||||
|
this._doAuth(false, function() { this._showPicker(); }.bind(this));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the file picker once authentication has been done.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_showPicker: function() {
|
||||||
|
var accessToken = gapi.auth.getToken().access_token;
|
||||||
|
var view = new google.picker.DocsView();
|
||||||
|
view.setMimeTypes("text/markdown,text/html");
|
||||||
|
view.setIncludeFolders(true);
|
||||||
|
this.picker = new google.picker.PickerBuilder().
|
||||||
|
enableFeature(google.picker.Feature.NAV_HIDDEN).
|
||||||
|
addView(view).
|
||||||
|
setAppId(this.clientId).
|
||||||
|
setOAuthToken(accessToken).
|
||||||
|
setCallback(this._pickerCallback.bind(this)).
|
||||||
|
build().
|
||||||
|
setVisible(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a file has been selected in the Google Drive file picker.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_pickerCallback: function(data) {
|
||||||
|
if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
|
||||||
|
var file = data[google.picker.Response.DOCUMENTS][0],
|
||||||
|
id = file[google.picker.Document.ID],
|
||||||
|
request = gapi.client.drive.files.get({
|
||||||
|
fileId: id
|
||||||
|
});
|
||||||
|
|
||||||
|
request.execute(this._fileGetCallback.bind(this));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Called when file details have been retrieved from Google Drive.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_fileGetCallback: function(file) {
|
||||||
|
if (this.onSelect) {
|
||||||
|
this.onSelect(file);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the Google Drive file picker API has finished loading.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_pickerApiLoaded: function() {
|
||||||
|
this.buttonEl.prop('disabled', false);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the Google Drive API has finished loading.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_driveApiLoaded: function() {
|
||||||
|
this._doAuth(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate with Google Drive via the Google JavaScript API.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_doAuth: function(immediate, callback) {
|
||||||
|
gapi.auth.authorize({
|
||||||
|
client_id: this.clientId,
|
||||||
|
scope: 'https://www.googleapis.com/auth/drive.readonly',
|
||||||
|
immediate: immediate
|
||||||
|
}, callback ? callback : function() {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}());
|
269
public/js/google-drive-upload.js
Normal file
269
public/js/google-drive-upload.js
Normal file
|
@ -0,0 +1,269 @@
|
||||||
|
/**
|
||||||
|
* Helper for implementing retries with backoff. Initial retry
|
||||||
|
* delay is 1 second, increasing by 2x (+jitter) for subsequent retries
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
var RetryHandler = function() {
|
||||||
|
this.interval = 1000; // Start at one second
|
||||||
|
this.maxInterval = 60 * 1000; // Don't wait longer than a minute
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoke the function after waiting
|
||||||
|
*
|
||||||
|
* @param {function} fn Function to invoke
|
||||||
|
*/
|
||||||
|
RetryHandler.prototype.retry = function(fn) {
|
||||||
|
setTimeout(fn, this.interval);
|
||||||
|
this.interval = this.nextInterval_();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the counter (e.g. after successful request.)
|
||||||
|
*/
|
||||||
|
RetryHandler.prototype.reset = function() {
|
||||||
|
this.interval = 1000;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the next wait time.
|
||||||
|
* @return {number} Next wait interval, in milliseconds
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
RetryHandler.prototype.nextInterval_ = function() {
|
||||||
|
var interval = this.interval * 2 + this.getRandomInt_(0, 1000);
|
||||||
|
return Math.min(interval, this.maxInterval);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a random int in the range of min to max. Used to add jitter to wait times.
|
||||||
|
*
|
||||||
|
* @param {number} min Lower bounds
|
||||||
|
* @param {number} max Upper bounds
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
RetryHandler.prototype.getRandomInt_ = function(min, max) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for resumable uploads using XHR/CORS. Can upload any Blob-like item, whether
|
||||||
|
* files or in-memory constructs.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* var content = new Blob(["Hello world"], {"type": "text/plain"});
|
||||||
|
* var uploader = new MediaUploader({
|
||||||
|
* file: content,
|
||||||
|
* token: accessToken,
|
||||||
|
* onComplete: function(data) { ... }
|
||||||
|
* onError: function(data) { ... }
|
||||||
|
* });
|
||||||
|
* uploader.upload();
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param {object} options Hash of options
|
||||||
|
* @param {string} options.token Access token
|
||||||
|
* @param {blob} options.file Blob-like item to upload
|
||||||
|
* @param {string} [options.fileId] ID of file if replacing
|
||||||
|
* @param {object} [options.params] Additional query parameters
|
||||||
|
* @param {string} [options.contentType] Content-type, if overriding the type of the blob.
|
||||||
|
* @param {object} [options.metadata] File metadata
|
||||||
|
* @param {function} [options.onComplete] Callback for when upload is complete
|
||||||
|
* @param {function} [options.onProgress] Callback for status for the in-progress upload
|
||||||
|
* @param {function} [options.onError] Callback if upload fails
|
||||||
|
*/
|
||||||
|
var MediaUploader = function(options) {
|
||||||
|
var noop = function() {};
|
||||||
|
this.file = options.file;
|
||||||
|
this.contentType = options.contentType || this.file.type || 'application/octet-stream';
|
||||||
|
this.metadata = options.metadata || {
|
||||||
|
'title': this.file.name,
|
||||||
|
'mimeType': this.contentType
|
||||||
|
};
|
||||||
|
this.token = options.token;
|
||||||
|
this.onComplete = options.onComplete || noop;
|
||||||
|
this.onProgress = options.onProgress || noop;
|
||||||
|
this.onError = options.onError || noop;
|
||||||
|
this.offset = options.offset || 0;
|
||||||
|
this.chunkSize = options.chunkSize || 0;
|
||||||
|
this.retryHandler = new RetryHandler();
|
||||||
|
|
||||||
|
this.url = options.url;
|
||||||
|
if (!this.url) {
|
||||||
|
var params = options.params || {};
|
||||||
|
params.uploadType = 'resumable';
|
||||||
|
this.url = this.buildUrl_(options.fileId, params, options.baseUrl);
|
||||||
|
}
|
||||||
|
this.httpMethod = options.fileId ? 'PUT' : 'POST';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate the upload.
|
||||||
|
*/
|
||||||
|
MediaUploader.prototype.upload = function() {
|
||||||
|
var self = this;
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
xhr.open(this.httpMethod, this.url, true);
|
||||||
|
xhr.setRequestHeader('Authorization', 'Bearer ' + this.token);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
|
xhr.setRequestHeader('X-Upload-Content-Length', this.file.size);
|
||||||
|
xhr.setRequestHeader('X-Upload-Content-Type', this.contentType);
|
||||||
|
|
||||||
|
xhr.onload = function(e) {
|
||||||
|
if (e.target.status < 400) {
|
||||||
|
var location = e.target.getResponseHeader('Location');
|
||||||
|
this.url = location;
|
||||||
|
this.sendFile_();
|
||||||
|
} else {
|
||||||
|
this.onUploadError_(e);
|
||||||
|
}
|
||||||
|
}.bind(this);
|
||||||
|
xhr.onerror = this.onUploadError_.bind(this);
|
||||||
|
xhr.send(JSON.stringify(this.metadata));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the actual file content.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
MediaUploader.prototype.sendFile_ = function() {
|
||||||
|
var content = this.file;
|
||||||
|
var end = this.file.size;
|
||||||
|
|
||||||
|
if (this.offset || this.chunkSize) {
|
||||||
|
// Only bother to slice the file if we're either resuming or uploading in chunks
|
||||||
|
if (this.chunkSize) {
|
||||||
|
end = Math.min(this.offset + this.chunkSize, this.file.size);
|
||||||
|
}
|
||||||
|
content = content.slice(this.offset, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('PUT', this.url, true);
|
||||||
|
xhr.setRequestHeader('Content-Type', this.contentType);
|
||||||
|
xhr.setRequestHeader('Content-Range', "bytes " + this.offset + "-" + (end - 1) + "/" + this.file.size);
|
||||||
|
xhr.setRequestHeader('X-Upload-Content-Type', this.file.type);
|
||||||
|
if (xhr.upload) {
|
||||||
|
xhr.upload.addEventListener('progress', this.onProgress);
|
||||||
|
}
|
||||||
|
xhr.onload = this.onContentUploadSuccess_.bind(this);
|
||||||
|
xhr.onerror = this.onContentUploadError_.bind(this);
|
||||||
|
xhr.send(content);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query for the state of the file for resumption.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
MediaUploader.prototype.resume_ = function() {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('PUT', this.url, true);
|
||||||
|
xhr.setRequestHeader('Content-Range', "bytes */" + this.file.size);
|
||||||
|
xhr.setRequestHeader('X-Upload-Content-Type', this.file.type);
|
||||||
|
if (xhr.upload) {
|
||||||
|
xhr.upload.addEventListener('progress', this.onProgress);
|
||||||
|
}
|
||||||
|
xhr.onload = this.onContentUploadSuccess_.bind(this);
|
||||||
|
xhr.onerror = this.onContentUploadError_.bind(this);
|
||||||
|
xhr.send();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the last saved range if available in the request.
|
||||||
|
*
|
||||||
|
* @param {XMLHttpRequest} xhr Request object
|
||||||
|
*/
|
||||||
|
MediaUploader.prototype.extractRange_ = function(xhr) {
|
||||||
|
var range = xhr.getResponseHeader('Range');
|
||||||
|
if (range) {
|
||||||
|
this.offset = parseInt(range.match(/\d+/g).pop(), 10) + 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle successful responses for uploads. Depending on the context,
|
||||||
|
* may continue with uploading the next chunk of the file or, if complete,
|
||||||
|
* invokes the caller's callback.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {object} e XHR event
|
||||||
|
*/
|
||||||
|
MediaUploader.prototype.onContentUploadSuccess_ = function(e) {
|
||||||
|
if (e.target.status == 200 || e.target.status == 201) {
|
||||||
|
this.onComplete(e.target.response);
|
||||||
|
} else if (e.target.status == 308) {
|
||||||
|
this.extractRange_(e.target);
|
||||||
|
this.retryHandler.reset();
|
||||||
|
this.sendFile_();
|
||||||
|
} else {
|
||||||
|
this.onContentUploadError_(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles errors for uploads. Either retries or aborts depending
|
||||||
|
* on the error.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {object} e XHR event
|
||||||
|
*/
|
||||||
|
MediaUploader.prototype.onContentUploadError_ = function(e) {
|
||||||
|
if (e.target.status && e.target.status < 500) {
|
||||||
|
this.onError(e.target.response);
|
||||||
|
} else {
|
||||||
|
this.retryHandler.retry(this.resume_.bind(this));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles errors for the initial request.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {object} e XHR event
|
||||||
|
*/
|
||||||
|
MediaUploader.prototype.onUploadError_ = function(e) {
|
||||||
|
this.onError(e.target.response); // TODO - Retries for initial upload
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a query string from a hash/object
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {object} [params] Key/value pairs for query string
|
||||||
|
* @return {string} query string
|
||||||
|
*/
|
||||||
|
MediaUploader.prototype.buildQuery_ = function(params) {
|
||||||
|
params = params || {};
|
||||||
|
return Object.keys(params).map(function(key) {
|
||||||
|
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
|
||||||
|
}).join('&');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the drive upload URL
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {string} [id] File ID if replacing
|
||||||
|
* @param {object} [params] Query parameters
|
||||||
|
* @return {string} URL
|
||||||
|
*/
|
||||||
|
MediaUploader.prototype.buildUrl_ = function(id, params, baseUrl) {
|
||||||
|
var url = baseUrl || 'https://www.googleapis.com/upload/drive/v2/files/';
|
||||||
|
if (id) {
|
||||||
|
url += id;
|
||||||
|
}
|
||||||
|
var query = this.buildQuery_(params);
|
||||||
|
if (query) {
|
||||||
|
url += '?' + query;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -486,10 +486,12 @@ var ui = {
|
||||||
},
|
},
|
||||||
export: {
|
export: {
|
||||||
dropbox: $(".ui-save-dropbox"),
|
dropbox: $(".ui-save-dropbox"),
|
||||||
|
googleDrive: $(".ui-save-google-drive"),
|
||||||
gist: $(".ui-save-gist")
|
gist: $(".ui-save-gist")
|
||||||
},
|
},
|
||||||
import: {
|
import: {
|
||||||
dropbox: $(".ui-import-dropbox"),
|
dropbox: $(".ui-import-dropbox"),
|
||||||
|
googleDrive: $(".ui-import-google-drive"),
|
||||||
clipboard: $(".ui-import-clipboard")
|
clipboard: $(".ui-import-clipboard")
|
||||||
},
|
},
|
||||||
beta: {
|
beta: {
|
||||||
|
@ -994,6 +996,22 @@ function closestIndex(arr, closestTo) {
|
||||||
return index; // return the value
|
return index; // return the value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showMessageModal(title, header, href, text, success) {
|
||||||
|
var modal = $('.message-modal');
|
||||||
|
modal.find('.modal-title').html(title);
|
||||||
|
modal.find('.modal-body h5').html(header);
|
||||||
|
if (href)
|
||||||
|
modal.find('.modal-body a').attr('href', href).text(text);
|
||||||
|
else
|
||||||
|
modal.find('.modal-body a').removeAttr('href').text(text);
|
||||||
|
modal.find('.modal-footer button').removeClass('btn-default btn-success btn-danger')
|
||||||
|
if (success)
|
||||||
|
modal.find('.modal-footer button').addClass('btn-success');
|
||||||
|
else
|
||||||
|
modal.find('.modal-footer button').addClass('btn-danger');
|
||||||
|
modal.modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
//button actions
|
//button actions
|
||||||
//share
|
//share
|
||||||
ui.toolbar.publish.attr("href", noteurl + "/publish");
|
ui.toolbar.publish.attr("href", noteurl + "/publish");
|
||||||
|
@ -1031,6 +1049,53 @@ ui.toolbar.export.dropbox.click(function () {
|
||||||
};
|
};
|
||||||
Dropbox.save(options);
|
Dropbox.save(options);
|
||||||
});
|
});
|
||||||
|
function uploadToGoogleDrive(accessToken) {
|
||||||
|
ui.spinner.show();
|
||||||
|
var filename = renderFilename(ui.area.markdown) + '.md';
|
||||||
|
var markdown = editor.getValue();
|
||||||
|
var blob = new Blob([markdown], {
|
||||||
|
type: "text/markdown;charset=utf-8"
|
||||||
|
});
|
||||||
|
blob.name = filename;
|
||||||
|
var uploader = new MediaUploader({
|
||||||
|
file: blob,
|
||||||
|
token: accessToken,
|
||||||
|
onComplete: function(data) {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
showMessageModal('<i class="fa fa-cloud-upload"></i> Export to Google Drive', 'Export Complete!', data.alternateLink, 'Click here to view your file', true);
|
||||||
|
ui.spinner.hide();
|
||||||
|
},
|
||||||
|
onError: function(data) {
|
||||||
|
var modal = $('.export-modal');
|
||||||
|
showMessageModal('<i class="fa fa-cloud-upload"></i> Export to Google Drive', 'Export Error :(', '', data, false);
|
||||||
|
ui.spinner.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
uploader.upload();
|
||||||
|
}
|
||||||
|
function googleApiAuth(immediate, callback) {
|
||||||
|
gapi.auth.authorize(
|
||||||
|
{
|
||||||
|
'client_id': GOOGLE_CLIENT_ID,
|
||||||
|
'scope': 'https://www.googleapis.com/auth/drive.file',
|
||||||
|
'immediate': immediate
|
||||||
|
}, callback ? callback : function() {});
|
||||||
|
}
|
||||||
|
function onGoogleClientLoaded() {
|
||||||
|
googleApiAuth(true);
|
||||||
|
buildImportFromGoogleDrive();
|
||||||
|
}
|
||||||
|
// export to google drive
|
||||||
|
ui.toolbar.export.googleDrive.click(function (e) {
|
||||||
|
var token = gapi.auth.getToken();
|
||||||
|
if (token) {
|
||||||
|
uploadToGoogleDrive(token.access_token);
|
||||||
|
} else {
|
||||||
|
googleApiAuth(false, function(result) {
|
||||||
|
uploadToGoogleDrive(result.access_token);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
//export to gist
|
//export to gist
|
||||||
ui.toolbar.export.gist.attr("href", noteurl + "/gist");
|
ui.toolbar.export.gist.attr("href", noteurl + "/gist");
|
||||||
//import from dropbox
|
//import from dropbox
|
||||||
|
@ -1047,6 +1112,41 @@ ui.toolbar.import.dropbox.click(function () {
|
||||||
};
|
};
|
||||||
Dropbox.choose(options);
|
Dropbox.choose(options);
|
||||||
});
|
});
|
||||||
|
// import from google drive
|
||||||
|
var picker = null;
|
||||||
|
function buildImportFromGoogleDrive() {
|
||||||
|
picker = new FilePicker({
|
||||||
|
apiKey: GOOGLE_API_KEY,
|
||||||
|
clientId: GOOGLE_CLIENT_ID,
|
||||||
|
buttonEl: ui.toolbar.import.googleDrive,
|
||||||
|
onSelect: function(file) {
|
||||||
|
if (file.downloadUrl) {
|
||||||
|
ui.spinner.show();
|
||||||
|
var accessToken = gapi.auth.getToken().access_token;
|
||||||
|
$.ajax({
|
||||||
|
type: 'GET',
|
||||||
|
beforeSend: function (request)
|
||||||
|
{
|
||||||
|
request.setRequestHeader('Authorization', 'Bearer ' + accessToken);
|
||||||
|
},
|
||||||
|
url: file.downloadUrl,
|
||||||
|
success: function(data) {
|
||||||
|
if (file.fileExtension == 'html')
|
||||||
|
parseToEditor(data);
|
||||||
|
else
|
||||||
|
replaceAll(data);
|
||||||
|
},
|
||||||
|
error: function (data) {
|
||||||
|
showMessageModal('<i class="fa fa-cloud-download"></i> Import from Google Drive', 'Import failed :(', '', data, false);
|
||||||
|
},
|
||||||
|
complete: function () {
|
||||||
|
ui.spinner.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
//import from clipboard
|
//import from clipboard
|
||||||
ui.toolbar.import.clipboard.click(function () {
|
ui.toolbar.import.clipboard.click(function () {
|
||||||
//na
|
//na
|
||||||
|
@ -1188,7 +1288,7 @@ function importFromUrl(url) {
|
||||||
//console.log(url);
|
//console.log(url);
|
||||||
if (url == null) return;
|
if (url == null) return;
|
||||||
if (!isValidURL(url)) {
|
if (!isValidURL(url)) {
|
||||||
alert('Not valid URL :(');
|
showMessageModal('<i class="fa fa-cloud-download"></i> Import from URL', 'Not valid URL :(', '', '', false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$.ajax({
|
$.ajax({
|
||||||
|
@ -1201,8 +1301,8 @@ function importFromUrl(url) {
|
||||||
else
|
else
|
||||||
replaceAll(data);
|
replaceAll(data);
|
||||||
},
|
},
|
||||||
error: function () {
|
error: function (data) {
|
||||||
alert('Import failed :(');
|
showMessageModal('<i class="fa fa-cloud-download"></i> Import from URL', 'Import failed :(', '', data, false);
|
||||||
},
|
},
|
||||||
complete: function () {
|
complete: function () {
|
||||||
ui.spinner.hide();
|
ui.spinner.hide();
|
||||||
|
|
|
@ -135,4 +135,23 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- message modal -->
|
||||||
|
<div class="modal fade message-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-sm">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
<h4 class="modal-title" id="myModalLabel"></h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="color:black;">
|
||||||
|
<h5></h5>
|
||||||
|
<a target="_blank"></a>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">OK</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -61,7 +61,10 @@
|
||||||
<script src="<%- url %>/vendor/md-toc.js" defer></script>
|
<script src="<%- url %>/vendor/md-toc.js" defer></script>
|
||||||
<script src="<%- url %>/vendor/showup/showup.js" defer></script>
|
<script src="<%- url %>/vendor/showup/showup.js" defer></script>
|
||||||
<script src="<%- url %>/vendor/randomColor.js" defer></script>
|
<script src="<%- url %>/vendor/randomColor.js" defer></script>
|
||||||
<script type="text/javascript" src="https://www.dropbox.com/static/api/2/dropins.js" id="dropboxjs" data-app-key="rdoizrlnkuha23r" async defer></script>
|
<script type="text/javascript" src="https://www.dropbox.com/static/api/2/dropins.js" id="dropboxjs" data-app-key="change this" async defer></script>
|
||||||
|
<script src="https://www.google.com/jsapi" defer></script>
|
||||||
|
<script src="<%- url %>/js/google-drive-upload.js" defer></script>
|
||||||
|
<script src="<%- url %>/js/google-drive-picker.js" defer></script>
|
||||||
<script type="text/x-mathjax-config">
|
<script type="text/x-mathjax-config">
|
||||||
MathJax.Hub.Config({ messageStyle: "none", skipStartupTypeset: true ,tex2jax: {inlineMath: [['$','$'], ['\\(','\\)']], processEscapes: true }});
|
MathJax.Hub.Config({ messageStyle: "none", skipStartupTypeset: true ,tex2jax: {inlineMath: [['$','$'], ['\\(','\\)']], processEscapes: true }});
|
||||||
</script>
|
</script>
|
||||||
|
@ -71,4 +74,5 @@
|
||||||
<script src="<%- url %>/js/render.js" defer></script>
|
<script src="<%- url %>/js/render.js" defer></script>
|
||||||
<script src="<%- url %>/js/history.js" defer></script>
|
<script src="<%- url %>/js/history.js" defer></script>
|
||||||
<script src="<%- url %>/js/index.js" defer></script>
|
<script src="<%- url %>/js/index.js" defer></script>
|
||||||
<script src="<%- url %>/js/syncscroll.js" defer></script>
|
<script src="<%- url %>/js/syncscroll.js" defer></script>
|
||||||
|
<script src="https://apis.google.com/js/client:plusone.js?onload=onGoogleClientLoaded" defer></script>
|
|
@ -36,12 +36,16 @@
|
||||||
<li class="dropdown-header">Export</li>
|
<li class="dropdown-header">Export</li>
|
||||||
<li role="presentation"><a role="menuitem" class="ui-save-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a>
|
<li role="presentation"><a role="menuitem" class="ui-save-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li role="presentation"><a role="menuitem" class="ui-save-google-drive" tabindex="-1" href="#" target="_self"><i class="fa fa-cloud-upload fa-fw"></i> Google Drive</a>
|
||||||
|
</li>
|
||||||
<li role="presentation"><a role="menuitem" class="ui-save-gist" tabindex="-1" href="#" target="_blank"><i class="fa fa-github fa-fw"></i> Gist</a>
|
<li role="presentation"><a role="menuitem" class="ui-save-gist" tabindex="-1" href="#" target="_blank"><i class="fa fa-github fa-fw"></i> Gist</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li class="dropdown-header">Import</li>
|
<li class="dropdown-header">Import</li>
|
||||||
<li role="presentation"><a role="menuitem" class="ui-import-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a>
|
<li role="presentation"><a role="menuitem" class="ui-import-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li role="presentation"><a role="menuitem" class="ui-import-google-drive" tabindex="-1" href="#" target="_self"><i class="fa fa-cloud-download fa-fw"></i> Google Drive</a>
|
||||||
|
</li>
|
||||||
<li role="presentation"><a role="menuitem" class="ui-import-clipboard" href="#" data-toggle="modal" data-target="#clipboardModal"><i class="fa fa-clipboard fa-fw"></i> Clipboard</a>
|
<li role="presentation"><a role="menuitem" class="ui-import-clipboard" href="#" data-toggle="modal" data-target="#clipboardModal"><i class="fa fa-clipboard fa-fw"></i> Clipboard</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
|
@ -113,12 +117,16 @@
|
||||||
<li class="dropdown-header">Export</li>
|
<li class="dropdown-header">Export</li>
|
||||||
<li role="presentation"><a role="menuitem" class="ui-save-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a>
|
<li role="presentation"><a role="menuitem" class="ui-save-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li role="presentation"><a role="menuitem" class="ui-save-google-drive" tabindex="-1" href="#" target="_self"><i class="fa fa-cloud-upload fa-fw"></i> Google Drive</a>
|
||||||
|
</li>
|
||||||
<li role="presentation"><a role="menuitem" class="ui-save-gist" tabindex="-1" href="#" target="_blank"><i class="fa fa-github fa-fw"></i> Gist</a>
|
<li role="presentation"><a role="menuitem" class="ui-save-gist" tabindex="-1" href="#" target="_blank"><i class="fa fa-github fa-fw"></i> Gist</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li class="dropdown-header">Import</li>
|
<li class="dropdown-header">Import</li>
|
||||||
<li role="presentation"><a role="menuitem" class="ui-import-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a>
|
<li role="presentation"><a role="menuitem" class="ui-import-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li role="presentation"><a role="menuitem" class="ui-import-google-drive" tabindex="-1" href="#" target="_self"><i class="fa fa-cloud-download fa-fw"></i> Google Drive</a>
|
||||||
|
</li>
|
||||||
<li role="presentation"><a role="menuitem" class="ui-import-clipboard" href="#" data-toggle="modal" data-target="#clipboardModal"><i class="fa fa-clipboard fa-fw"></i> Clipboard</a>
|
<li role="presentation"><a role="menuitem" class="ui-import-clipboard" href="#" data-toggle="modal" data-target="#clipboardModal"><i class="fa fa-clipboard fa-fw"></i> Clipboard</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
|
|
Loading…
Reference in a new issue