+
+
+
diff --git a/public/res/providers/gdriveProvider.js b/public/res/providers/gdriveProvider.js
index c77068ef..c6bc6c07 100644
--- a/public/res/providers/gdriveProvider.js
+++ b/public/res/providers/gdriveProvider.js
@@ -1,5 +1,5 @@
define([
"providers/gdriveProviderBuilder"
], function(gdriveProviderBuilder) {
- return gdriveProviderBuilder("gdrive", "Google Drive", 'google.0')
+ return gdriveProviderBuilder("gdrive", "Google Drive", 0);
});
\ No newline at end of file
diff --git a/public/res/providers/gdriveProviderBuilder.js b/public/res/providers/gdriveProviderBuilder.js
new file mode 100644
index 00000000..ddc76298
--- /dev/null
+++ b/public/res/providers/gdriveProviderBuilder.js
@@ -0,0 +1,546 @@
+/*global gapi */
+define([
+ "jquery",
+ "underscore",
+ "constants",
+ "utils",
+ "storage",
+ "logger",
+ "classes/Provider",
+ "settings",
+ "eventMgr",
+ "fileMgr",
+ "helpers/googleHelper",
+ "text!html/dialogExportGdrive.html"
+], function($, _, constants, utils, storage, logger, Provider, settings, eventMgr, fileMgr, googleHelper, dialogExportGdriveHTML) {
+
+ return function(providerId, providerName, accountIndex) {
+ var accountId = 'google.gdrive' + accountIndex;
+
+ var gdriveProvider = new Provider(providerId, providerName);
+ gdriveProvider.defaultPublishFormat = "template";
+ gdriveProvider.exportPreferencesInputIds = [
+ providerId + "-parentid",
+ providerId + "-realtime",
+ ];
+
+ function createSyncIndex(id) {
+ return "sync." + providerId + "." + id;
+ }
+
+ function createSyncAttributes(id, etag, content, title) {
+ var syncAttributes = {};
+ syncAttributes.provider = gdriveProvider;
+ syncAttributes.id = id;
+ syncAttributes.etag = etag;
+ syncAttributes.contentCRC = utils.crc32(content);
+ syncAttributes.titleCRC = utils.crc32(title);
+ syncAttributes.syncIndex = createSyncIndex(id);
+ return syncAttributes;
+ }
+
+ function importFilesFromIds(ids) {
+ googleHelper.downloadMetadata(ids, accountId, function(error, result) {
+ if(error) {
+ return;
+ }
+ googleHelper.downloadContent(result, accountId, function(error, result) {
+ if(error) {
+ return;
+ }
+ var fileDescList = [];
+ var fileDesc;
+ _.each(result, function(file) {
+ var syncAttributes = createSyncAttributes(file.id, file.etag, file.content, file.title);
+ syncAttributes.isRealtime = file.isRealtime;
+ var syncLocations = {};
+ syncLocations[syncAttributes.syncIndex] = syncAttributes;
+ fileDesc = fileMgr.createFile(file.title, file.content, syncLocations);
+ fileDescList.push(fileDesc);
+ });
+ if(fileDesc !== undefined) {
+ eventMgr.onSyncImportSuccess(fileDescList, gdriveProvider);
+ fileMgr.selectFile(fileDesc);
+ }
+ });
+ });
+ }
+
+ gdriveProvider.importFiles = function() {
+ googleHelper.picker(function(error, docs) {
+ if(error || docs.length === 0) {
+ return;
+ }
+ var importIds = [];
+ _.each(docs, function(doc) {
+ var syncIndex = createSyncIndex(doc.id);
+ var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
+ if(fileDesc !== undefined) {
+ eventMgr.onError('"' + fileDesc.title + '" was already imported.');
+ return;
+ }
+ importIds.push(doc.id);
+ });
+ importFilesFromIds(importIds);
+ }, 'doc', accountId);
+ };
+
+ gdriveProvider.exportFile = function(event, title, content, callback) {
+ var fileId = utils.getInputTextValue('#input-sync-export-' + providerId + '-fileid');
+ if(fileId) {
+ // Check that file is not synchronized with another an existing
+ // document
+ var syncIndex = createSyncIndex(fileId);
+ var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
+ if(fileDesc !== undefined) {
+ eventMgr.onError('File ID is already synchronized with "' + fileDesc.title + '".');
+ callback(true);
+ return;
+ }
+ }
+ var parentId = utils.getInputTextValue('#input-sync-export-' + providerId + '-parentid');
+ googleHelper.upload(fileId, parentId, title, content, undefined, undefined, accountId, function(error, result) {
+ if(error) {
+ callback(error);
+ return;
+ }
+ var syncAttributes = createSyncAttributes(result.id, result.etag, content, title);
+ callback(undefined, syncAttributes);
+ });
+ };
+
+ gdriveProvider.exportRealtimeFile = function(event, title, content, callback) {
+ var parentId = utils.getInputTextValue('#input-sync-export-' + providerId + '-parentid');
+ googleHelper.createRealtimeFile(parentId, title, accountId, function(error, result) {
+ if(error) {
+ callback(error);
+ return;
+ }
+ var syncAttributes = createSyncAttributes(result.id, result.etag, content, title);
+ callback(undefined, syncAttributes);
+ });
+ };
+
+ gdriveProvider.syncUp = function(uploadContent, uploadContentCRC, uploadTitle, uploadTitleCRC, syncAttributes, callback) {
+ // Skip if CRC has not changed
+ if(uploadContentCRC == syncAttributes.contentCRC && uploadTitleCRC == syncAttributes.titleCRC) {
+ callback(undefined, false);
+ return;
+ }
+ googleHelper.upload(syncAttributes.id, undefined, uploadTitle, uploadContent, undefined, syncAttributes.etag, accountId, function(error, result) {
+ if(error) {
+ callback(error, true);
+ return;
+ }
+ syncAttributes.etag = result.etag;
+ syncAttributes.contentCRC = uploadContentCRC;
+ syncAttributes.titleCRC = uploadTitleCRC;
+ callback(undefined, true);
+ });
+ };
+
+ gdriveProvider.syncUpRealtime = function(uploadContent, uploadContentCRC, uploadTitle, uploadTitleCRC, syncAttributes, callback) {
+ // Skip if title CRC has not changed
+ if(uploadTitleCRC == syncAttributes.titleCRC) {
+ callback(undefined, false);
+ return;
+ }
+ googleHelper.rename(syncAttributes.id, uploadTitle, accountId, function(error, result) {
+ if(error) {
+ callback(error, true);
+ return;
+ }
+ syncAttributes.etag = result.etag;
+ syncAttributes.titleCRC = uploadTitleCRC;
+ callback(undefined, true);
+ });
+ };
+
+ gdriveProvider.syncDown = function(callback) {
+ var lastChangeId = parseInt(storage[accountId + ".gdrive.lastChangeId"], 10);
+ googleHelper.checkChanges(lastChangeId, accountId, function(error, changes, newChangeId) {
+ if(error) {
+ callback(error);
+ return;
+ }
+ var interestingChanges = [];
+ _.each(changes, function(change) {
+ var syncIndex = createSyncIndex(change.fileId);
+ var syncAttributes = fileMgr.getSyncAttributes(syncIndex);
+ if(syncAttributes === undefined) {
+ return;
+ }
+ // Store syncAttributes to avoid 2 times searching
+ change.syncAttributes = syncAttributes;
+ // Delete
+ if(change.deleted === true) {
+ interestingChanges.push(change);
+ return;
+ }
+ // Modify
+ if(syncAttributes.etag != change.file.etag) {
+ interestingChanges.push(change);
+ }
+ });
+ googleHelper.downloadContent(interestingChanges, accountId, function(error, changes) {
+ if(error) {
+ callback(error);
+ return;
+ }
+ _.each(changes, function(change) {
+ var syncAttributes = change.syncAttributes;
+ var syncIndex = syncAttributes.syncIndex;
+ var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
+ // No file corresponding (file may have been deleted
+ // locally)
+ if(fileDesc === undefined) {
+ return;
+ }
+ var localTitle = fileDesc.title;
+ // File deleted
+ if(change.deleted === true) {
+ eventMgr.onError('"' + localTitle + '" has been removed from Google Drive.');
+ fileDesc.removeSyncLocation(syncAttributes);
+ eventMgr.onSyncRemoved(fileDesc, syncAttributes);
+ if(syncAttributes.isRealtime === true && fileMgr.currentFile === fileDesc) {
+ gdriveProvider.stopRealtimeSync();
+ }
+ return;
+ }
+ var localTitleChanged = syncAttributes.titleCRC != utils.crc32(localTitle);
+ var localContent = fileDesc.content;
+ var localContentChanged = syncAttributes.contentCRC != utils.crc32(localContent);
+ var file = change.file;
+ var remoteTitleCRC = utils.crc32(file.title);
+ var remoteTitleChanged = syncAttributes.titleCRC != remoteTitleCRC;
+ var fileTitleChanged = localTitle != file.title;
+ var remoteContentCRC = utils.crc32(file.content);
+ var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC;
+ var fileContentChanged = localContent != file.content;
+ // Conflict detection
+ if((fileTitleChanged === true && localTitleChanged === true && remoteTitleChanged === true) || (!syncAttributes.isRealtime && fileContentChanged === true && localContentChanged === true && remoteContentChanged === true)) {
+ fileMgr.createFile(localTitle + " (backup)", localContent);
+ eventMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.');
+ }
+ // If file title changed
+ if(fileTitleChanged && remoteTitleChanged === true) {
+ fileDesc.title = file.title;
+ eventMgr.onTitleChanged(fileDesc);
+ eventMgr.onMessage('"' + localTitle + '" has been renamed to "' + file.title + '" on Google Drive.');
+ }
+ // If file content changed
+ if(!syncAttributes.isRealtime && fileContentChanged && remoteContentChanged === true) {
+ fileDesc.content = file.content;
+ eventMgr.onContentChanged(fileDesc);
+ eventMgr.onMessage('"' + file.title + '" has been updated from Google Drive.');
+ if(fileMgr.currentFile === fileDesc) {
+ fileMgr.selectFile(); // Refresh editor
+ }
+ }
+ // Update syncAttributes
+ syncAttributes.etag = file.etag;
+ if(!syncAttributes.isRealtime) {
+ syncAttributes.contentCRC = remoteContentCRC;
+ }
+ syncAttributes.titleCRC = remoteTitleCRC;
+ utils.storeAttributes(syncAttributes);
+ });
+ storage[accountId + ".gdrive.lastChangeId"] = newChangeId;
+ callback();
+ });
+ });
+ };
+
+ gdriveProvider.publish = function(publishAttributes, frontMatter, title, content, callback) {
+ var contentType = publishAttributes.format != "markdown" ? 'text/html' : undefined;
+ googleHelper.upload(publishAttributes.id, undefined, publishAttributes.fileName || title, content, contentType, undefined, accountId, function(error, result) {
+ if(error) {
+ callback(error);
+ return;
+ }
+ publishAttributes.id = result.id;
+ callback();
+ });
+ };
+
+ gdriveProvider.newPublishAttributes = function(event) {
+ var publishAttributes = {};
+ publishAttributes.id = utils.getInputTextValue('#input-publish-' + providerId + '-fileid');
+ publishAttributes.fileName = utils.getInputTextValue('#input-publish-' + providerId + '-filename');
+ if(event.isPropagationStopped()) {
+ return undefined;
+ }
+ return publishAttributes;
+ };
+
+ // Keep a link to the Pagedown editor
+ var pagedownEditor;
+ var undoExecute;
+ var redoExecute;
+ var setUndoRedoButtonStates;
+ eventMgr.addListener("onPagedownConfigure", function(pagedownEditorParam) {
+ pagedownEditor = pagedownEditorParam;
+ });
+
+ // Keep a link to the ACE editor
+ var realtimeContext;
+ var aceEditor;
+ var isAceUpToDate = true;
+ eventMgr.addListener('onAceCreated', function(aceEditorParam) {
+ aceEditor = aceEditorParam;
+ // Listen to editor's changes
+ aceEditor.session.on('change', function() {
+ // Update the real time model if any
+ realtimeContext && realtimeContext.string && realtimeContext.string.setText(aceEditor.getValue());
+ });
+ });
+
+ // Start realtime synchronization
+ var Range = require('ace/range').Range;
+ gdriveProvider.startRealtimeSync = function(fileDesc, syncAttributes) {
+ var localContext = {};
+ realtimeContext = localContext;
+ googleHelper.loadRealtime(syncAttributes.id, fileDesc.content, accountId, function(err, doc) {
+ if(err || !doc) {
+ return;
+ }
+
+ // If user just switched to another document or file has just been
+ // reselected
+ if(localContext.isStopped === true) {
+ doc.close();
+ return;
+ }
+
+ logger.log("Starting Google Drive realtime synchronization");
+ localContext.document = doc;
+ var model = doc.getModel();
+ var realtimeString = model.getRoot().get('content');
+
+ // Saves model content checksum
+ function updateContentState() {
+ syncAttributes.contentCRC = utils.crc32(realtimeString.getText());
+ utils.storeAttributes(syncAttributes);
+ }
+
+ var debouncedRefreshPreview = _.debounce(pagedownEditor.refreshPreview, 100);
+
+ // Listen to insert text events
+ realtimeString.addEventListener(gapi.drive.realtime.EventType.TEXT_INSERTED, function(e) {
+ if(aceEditor !== undefined && (isAceUpToDate === false || e.isLocal === false)) {
+ // Update ACE editor
+ var position = aceEditor.session.doc.indexToPosition(e.index);
+ aceEditor.session.insert(position, e.text);
+ isAceUpToDate = true;
+ }
+ // If modifications come down from a collaborator
+ if(e.isLocal === false) {
+ logger.log("Google Drive realtime document updated from server");
+ updateContentState();
+ aceEditor === undefined && debouncedRefreshPreview();
+ }
+ });
+ // Listen to delete text events
+ realtimeString.addEventListener(gapi.drive.realtime.EventType.TEXT_DELETED, function(e) {
+ if(aceEditor !== undefined && (isAceUpToDate === false || e.isLocal === false)) {
+ // Update ACE editor
+ var range = (function(posStart, posEnd) {
+ return new Range(posStart.row, posStart.column, posEnd.row, posEnd.column);
+ })(aceEditor.session.doc.indexToPosition(e.index), aceEditor.session.doc.indexToPosition(e.index + e.text.length));
+ aceEditor.session.remove(range);
+ isAceUpToDate = true;
+ }
+ // If modifications come down from a collaborator
+ if(e.isLocal === false) {
+ logger.log("Google Drive realtime document updated from server");
+ updateContentState();
+ aceEditor === undefined && debouncedRefreshPreview();
+ }
+ });
+ doc.addEventListener(gapi.drive.realtime.EventType.DOCUMENT_SAVE_STATE_CHANGED, function(e) {
+ // Save success event
+ if(e.isPending === false && e.isSaving === false) {
+ logger.log("Google Drive realtime document successfully saved on server");
+ updateContentState();
+ }
+ });
+
+ // Try to merge offline modifications
+ var localContent = fileDesc.content;
+ var localContentChanged = syncAttributes.contentCRC != utils.crc32(localContent);
+ var remoteContent = realtimeString.getText();
+ var remoteContentCRC = utils.crc32(remoteContent);
+ var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC;
+ var fileContentChanged = localContent != remoteContent;
+ if(fileContentChanged === true && localContentChanged === true) {
+ if(remoteContentChanged === true) {
+ // Conflict detected
+ fileMgr.createFile(fileDesc.title + " (backup)", localContent);
+ eventMgr.onMessage('Conflict detected on "' + fileDesc.title + '". A backup has been created locally.');
+ }
+ else {
+ // Add local modifications if no collaborators change
+ realtimeString.setText(localContent);
+ }
+ }
+
+ if(aceEditor === undefined) {
+ // Binds model with textarea
+ localContext.binding = gapi.drive.realtime.databinding.bindString(realtimeString, document.getElementById("wmd-input"));
+ }
+
+ // Update content state according to collaborators changes
+ if(remoteContentChanged === true) {
+ logger.log("Google Drive realtime document updated from server");
+ aceEditor !== undefined && aceEditor.setValue(remoteContent, -1);
+ updateContentState();
+ aceEditor === undefined && debouncedRefreshPreview();
+ }
+
+ if(aceEditor !== undefined) {
+ // Tell ACE to update realtime string on each change
+ localContext.string = realtimeString;
+ }
+
+ // Save undo/redo buttons default actions
+ undoExecute = pagedownEditor.uiManager.buttons.undo.execute;
+ redoExecute = pagedownEditor.uiManager.buttons.redo.execute;
+ setUndoRedoButtonStates = pagedownEditor.uiManager.setUndoRedoButtonStates;
+
+ // Set temporary actions for undo/redo buttons
+ pagedownEditor.uiManager.buttons.undo.execute = function() {
+ if(model.canUndo) {
+ // This flag is used to avoid replaying editor's own
+ // modifications (assuming it's synchronous)
+ isAceUpToDate = false;
+ model.undo();
+ }
+ };
+ pagedownEditor.uiManager.buttons.redo.execute = function() {
+ if(model.canRedo) {
+ // This flag is used to avoid replaying editor's own
+ // modifications (assuming it's synchronous)
+ isAceUpToDate = false;
+ model.redo();
+ }
+ };
+
+ // Add event handler for model's UndoRedoStateChanged events
+ pagedownEditor.uiManager.setUndoRedoButtonStates = function() {
+ setTimeout(function() {
+ pagedownEditor.uiManager.setButtonState(pagedownEditor.uiManager.buttons.undo, model.canUndo);
+ pagedownEditor.uiManager.setButtonState(pagedownEditor.uiManager.buttons.redo, model.canRedo);
+ }, 50);
+ };
+ pagedownEditor.uiManager.setUndoRedoButtonStates();
+ model.addEventListener(gapi.drive.realtime.EventType.UNDO_REDO_STATE_CHANGED, function() {
+ pagedownEditor.uiManager.setUndoRedoButtonStates();
+ });
+
+ }, function(err) {
+ console.error(err);
+ if(err.type == "token_refresh_required") {
+ googleHelper.refreshGdriveToken(accountId);
+ }
+ else if(err.type == "not_found") {
+ eventMgr.onError('"' + fileDesc.title + '" has been removed from Google Drive.');
+ fileDesc.removeSyncLocation(syncAttributes);
+ eventMgr.onSyncRemoved(fileDesc, syncAttributes);
+ gdriveProvider.stopRealtimeSync();
+ }
+ else if(err.isFatal) {
+ eventMgr.onError('An error has forced real time synchronization to stop.');
+ gdriveProvider.stopRealtimeSync();
+ }
+ });
+ };
+
+ // Stop realtime synchronization
+ gdriveProvider.stopRealtimeSync = function() {
+ logger.log("Stopping Google Drive realtime synchronization");
+ if(realtimeContext !== undefined) {
+ realtimeContext.isStopped = true;
+ realtimeContext.binding && realtimeContext.binding.unbind();
+ realtimeContext.document && realtimeContext.document.close();
+ realtimeContext = undefined;
+ }
+
+ if(setUndoRedoButtonStates !== undefined) {
+ // Set back original undo/redo actions
+ pagedownEditor.uiManager.buttons.undo.execute = undoExecute;
+ pagedownEditor.uiManager.buttons.redo.execute = redoExecute;
+ pagedownEditor.uiManager.setUndoRedoButtonStates = setUndoRedoButtonStates;
+ pagedownEditor.uiManager.setUndoRedoButtonStates();
+ }
+ };
+
+ // Disable publish on optional multi-account
+ gdriveProvider.isPublishEnabled = settings.gdriveMultiAccount > accountIndex;
+
+ eventMgr.addListener("onReady", function() {
+ // Hide optional multi-account sub-menus
+ $('.submenu-sync-' + providerId).toggle(settings.gdriveMultiAccount > accountIndex);
+
+ // Create export dialog
+ document.querySelector('.modal-upload-' + providerId).innerHTML = _.template(dialogExportGdriveHTML, {
+ providerId: providerId,
+ providerName: providerName
+ });
+
+ // Choose folder button in export modal
+ $('.export-' + providerId + '-choose-folder').click(function() {
+ googleHelper.picker(function(error, docs) {
+ if(error || docs.length === 0) {
+ return;
+ }
+ // Open export dialog
+ $(".modal-upload-" + providerId).modal();
+ // Set parent ID
+ utils.setInputValue('#input-sync-export-' + providerId + '-parentid', docs[0].id);
+ }, 'folder', accountId);
+ });
+
+ // On export, disable file ID input if realtime is checked
+ var $realtimeCheckboxElt = $('#input-sync-export-' + providerId + '-realtime');
+ var $fileIdInputElt = $('#input-sync-export-' + providerId + '-fileid');
+ $('#input-sync-export-' + providerId + '-realtime').change(function() {
+ $fileIdInputElt.prop('disabled', $realtimeCheckboxElt.prop('checked'));
+ });
+
+ var state = utils.retrieveIgnoreError(providerId + ".state");
+ if(state === undefined) {
+ return;
+ }
+ storage.removeItem(providerId + ".state");
+ if(state.action == "create") {
+ googleHelper.upload(undefined, state.folderId, constants.GDRIVE_DEFAULT_FILE_TITLE, settings.defaultContent, undefined, undefined, accountId, function(error, file) {
+ if(error) {
+ return;
+ }
+ var syncAttributes = createSyncAttributes(file.id, file.etag, file.content, file.title);
+ var syncLocations = {};
+ syncLocations[syncAttributes.syncIndex] = syncAttributes;
+ var fileDesc = fileMgr.createFile(file.title, file.content, syncLocations);
+ fileMgr.selectFile(fileDesc);
+ eventMgr.onMessage('"' + file.title + '" created successfully on Google Drive.');
+ });
+ }
+ else if(state.action == "open") {
+ var importIds = [];
+ _.each(state.ids, function(id) {
+ var syncIndex = createSyncIndex(id);
+ var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
+ if(fileDesc !== undefined) {
+ fileDesc !== fileMgr.currentFile && fileMgr.selectFile(fileDesc);
+ }
+ else {
+ importIds.push(id);
+ }
+ });
+ importFilesFromIds(importIds);
+ }
+ });
+
+ return gdriveProvider;
+ };
+});
\ No newline at end of file
diff --git a/public/res/providers/gdrivesecProvider.js b/public/res/providers/gdrivesecProvider.js
new file mode 100644
index 00000000..04ed43d6
--- /dev/null
+++ b/public/res/providers/gdrivesecProvider.js
@@ -0,0 +1,5 @@
+define([
+ "providers/gdriveProviderBuilder"
+], function(gdriveProviderBuilder) {
+ return gdriveProviderBuilder("gdrivesec", "Google Drive (2nd account)", 1);
+});
\ No newline at end of file
diff --git a/public/res/providers/gdriveterProvider.js b/public/res/providers/gdriveterProvider.js
new file mode 100644
index 00000000..d208d290
--- /dev/null
+++ b/public/res/providers/gdriveterProvider.js
@@ -0,0 +1,5 @@
+define([
+ "providers/gdriveProviderBuilder"
+], function(gdriveProviderBuilder) {
+ return gdriveProviderBuilder("gdriveter", "Google Drive (3rd account)", 2);
+});
\ No newline at end of file