diff --git a/css/default.css b/css/default.css index ca58473e..f648e252 100644 --- a/css/default.css +++ b/css/default.css @@ -478,6 +478,11 @@ div.dropdown-menu textarea { background-position: -19px 0; } +.icon-gdrive.realtime { + width: 18px; + background-position: -180px 0; +} + .icon-dropbox { background-image: url("../img/icons.png") !important; width: 16px; diff --git a/img/icons.png b/img/icons.png index d7fe732b..a656096d 100644 Binary files a/img/icons.png and b/img/icons.png differ diff --git a/index.html b/index.html index 3d1b45ad..a59c584a 100644 --- a/index.html +++ b/index.html @@ -291,10 +291,10 @@
This will upload the current +This will save the current document to your Google Drive account and keep it synchronized.- Here, you can specify a folder ID (optional): + Please specify a folder ID (optional):
++ NOTE: ++
+- If no folder ID is supplied, the file will be created in + your root folder.
+- You can move or rename the file afterwards within Google + Drive.
+
This will upload the current +This will save the current document to your Dropbox account and keep it synchronized.Please specify a file path for "": diff --git a/js/extensions/buttonSync.js b/js/extensions/buttonSync.js index 77b9c8ab..db642e80 100644 --- a/js/extensions/buttonSync.js +++ b/js/extensions/buttonSync.js @@ -21,16 +21,20 @@ define([ newConfig.syncPeriod = utils.getInputIntValue("#input-sync-period", event, 0); }; + var synchronizer = undefined; + buttonSync.onSynchronizerCreated = function(synchronizerParameter) { + synchronizer = synchronizerParameter; + }; + var button = undefined; var syncRunning = false; - var uploadPending = false; var isOffline = false; // Enable/disable the button var updateButtonState = function() { if(button === undefined) { return; } - if(syncRunning === true || uploadPending === false || isOffline) { + if(syncRunning === true || synchronizer.hasSync() === false || isOffline) { button.addClass("disabled"); } else { @@ -38,11 +42,6 @@ define([ } }; - var synchronizer = undefined; - buttonSync.onSynchronizerCreated = function(synchronizerParameter) { - synchronizer = synchronizerParameter; - }; - // Run sync periodically var lastSync = 0; buttonSync.onPeriodicRun = function() { @@ -64,15 +63,14 @@ define([ }; buttonSync.onReady = updateButtonState; + buttonSync.onFileCreated = updateButtonState; + buttonSync.onFileDeleted = updateButtonState; + buttonSync.onSyncImportSuccess = updateButtonState; + buttonSync.onSyncExportSuccess = updateButtonState; + buttonSync.onSyncRemoved = updateButtonState; buttonSync.onSyncRunning = function(isRunning) { syncRunning = isRunning; - uploadPending = true; - updateButtonState(); - }; - - buttonSync.onSyncSuccess = function() { - uploadPending = false; updateButtonState(); }; @@ -81,19 +79,6 @@ define([ updateButtonState(); }; - // Check that a file has synchronized locations and no real time synchronized location - var checkSynchronization = function(fileDesc) { - if(_.size(fileDesc.syncLocations) !== 0 && !_.some(fileDesc.syncLocations, function(syncAttributes) { - return syncAttributes.isRealtime; - })) { - uploadPending = true; - updateButtonState(); - } - }; - - buttonSync.onContentChanged = checkSynchronization; - buttonSync.onTitleChanged = checkSynchronization; - return buttonSync; }); \ No newline at end of file diff --git a/js/extensions/dialogManageSynchronization.js b/js/extensions/dialogManageSynchronization.js index 025b9baa..ff31a9a0 100644 --- a/js/extensions/dialogManageSynchronization.js +++ b/js/extensions/dialogManageSynchronization.js @@ -38,7 +38,8 @@ define([ var syncDesc = syncAttributes.id || syncAttributes.path; var lineElement = $(_.template(dialogManageSynchronizationLocationHTML, { provider: syncAttributes.provider, - syncDesc: syncDesc + syncDesc: syncDesc, + isRealtime: syncAttributes.isRealtime })); lineElement.append($(removeButtonTemplate).click(function() { synchronizer.tryStopRealtimeSync(); diff --git a/js/extensions/documentSelector.js b/js/extensions/documentSelector.js index 1bccad5b..aa4e21d1 100644 --- a/js/extensions/documentSelector.js +++ b/js/extensions/documentSelector.js @@ -47,7 +47,11 @@ define([ _.chain(attributesList).sortBy(function(attributes) { return attributes.provider.providerId; }).each(function(attributes) { - result.push(''); + var classes = 'icon-' + attributes.provider.providerId; + if(attributes.isRealtime === true) { + classes += " realtime"; + } + result.push(''); }); result.push(" "); result.push(fileDesc.title); diff --git a/js/extensions/documentTitle.js b/js/extensions/documentTitle.js index c742dc29..d20aeeb5 100644 --- a/js/extensions/documentTitle.js +++ b/js/extensions/documentTitle.js @@ -26,7 +26,11 @@ define([ _.chain(attributesList).sortBy(function(attributes) { return attributes.provider.providerId; }).each(function(attributes) { - result.push(''); + var classes = 'icon-' + attributes.provider.providerId; + if(attributes.isRealtime === true) { + classes += " realtime"; + } + result.push(''); }); result.push(" "); result.push(fileDesc.title); diff --git a/js/helpers/googleHelper.js b/js/helpers/googleHelper.js index 36ec2bd2..4c870941 100644 --- a/js/helpers/googleHelper.js +++ b/js/helpers/googleHelper.js @@ -478,9 +478,9 @@ define([ code: err.type }, task); }); - task.onSuccess(function() { - callback(undefined, doc); - }); + }); + task.onSuccess(function() { + callback(undefined, doc); }); task.onError(function(error) { callback(error); diff --git a/js/html/dialogManageSynchronizationLocation.html b/js/html/dialogManageSynchronizationLocation.html index 281b25b2..2fe592db 100644 --- a/js/html/dialogManageSynchronizationLocation.html +++ b/js/html/dialogManageSynchronizationLocation.html @@ -1,5 +1,5 @@
- +diff --git a/js/providers/gdriveProvider.js b/js/providers/gdriveProvider.js index c0c236c7..eb422296 100644 --- a/js/providers/gdriveProvider.js +++ b/js/providers/gdriveProvider.js @@ -156,9 +156,7 @@ define([ _.each(changes, function(change) { var syncIndex = createSyncIndex(change.fileId); var syncAttributes = fileMgr.getSyncAttributes(syncIndex); - // If file is not synchronized or it's a real time synchronized location - if(syncAttributes === undefined || syncAttributes.isRealtime === true) { - // Skip it + if(syncAttributes === undefined) { return; } // Store syncAttributes to avoid 2 times searching @@ -193,6 +191,9 @@ define([ extensionMgr.onError('"' + localTitle + '" has been removed from Google Drive.'); fileDesc.removeSyncLocation(syncAttributes); extensionMgr.onSyncRemoved(fileDesc, syncAttributes); + if(syncAttributes.isRealtime === true && fileMgr.currentFile === fileDesc) { + gdriveProvider.stopRealtimeSync(); + } return; } var localTitleChanged = syncAttributes.titleCRC != utils.crc32(localTitle); @@ -206,7 +207,7 @@ define([ var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC; var fileContentChanged = localContent != file.content; // Conflict detection - if((fileTitleChanged === true && localTitleChanged === true && remoteTitleChanged === true) || (fileContentChanged === true && localContentChanged === true && remoteContentChanged === true)) { + if((fileTitleChanged === true && localTitleChanged === true && remoteTitleChanged === true) || (!syncAttributes.isRealtime && fileContentChanged === true && localContentChanged === true && remoteContentChanged === true)) { fileMgr.createFile(localTitle + " (backup)", localContent); extensionMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.'); } @@ -217,7 +218,7 @@ define([ extensionMgr.onMessage('"' + localTitle + '" has been renamed to "' + file.title + '" on Google Drive.'); } // If file content changed - if(fileContentChanged && remoteContentChanged === true) { + if(!syncAttributes.isRealtime && fileContentChanged && remoteContentChanged === true) { fileDesc.content = file.content; extensionMgr.onContentChanged(fileDesc); extensionMgr.onMessage('"' + file.title + '" has been updated from Google Drive.'); @@ -227,7 +228,9 @@ define([ } // Update syncAttributes syncAttributes.etag = file.etag; - syncAttributes.contentCRC = remoteContentCRC; + if(!syncAttributes.isRealtime) { + syncAttributes.contentCRC = remoteContentCRC; + } syncAttributes.titleCRC = remoteTitleCRC; utils.storeAttributes(syncAttributes); }); @@ -269,9 +272,9 @@ define([ var realtimeBinding = undefined; var undoExecute = undefined; var redoExecute = undefined; - gdriveProvider.startRealtimeSync = function(content, syncAttributes, callback) { + gdriveProvider.startRealtimeSync = function(localTitle, localContent, syncAttributes, callback) { logger.log("Starting Google Drive realtime synchronization"); - googleHelper.loadRealtime(syncAttributes.id, content, function(err, doc) { + googleHelper.loadRealtime(syncAttributes.id, localContent, function(err, doc) { if(err || !doc) { callback(err); return; @@ -279,13 +282,63 @@ define([ realtimeDocument = doc; var model = realtimeDocument.getModel(); var string = model.getRoot().get('content'); + + // Saves model content checksum + function updateContentState() { + syncAttributes.contentCRC = utils.crc32(string.getText()); + utils.storeAttributes(syncAttributes); + } + + var debouncedRefreshPreview = _.debounce(editor.refreshPreview, 100); + // Called when a modification has been detected + function contentChangeListener(e) { + console.log(e); + // If modification comes down from a collaborator + if(e.isLocal === false) { + logger.log("Google Drive realtime document updated from server"); + updateContentState(); + debouncedRefreshPreview(); + } + } + // Listen to text changed events + string.addEventListener(gapi.drive.realtime.EventType.TEXT_INSERTED, contentChangeListener); + string.addEventListener(gapi.drive.realtime.EventType.TEXT_DELETED, contentChangeListener); + realtimeDocument.addEventListener(gapi.drive.realtime.EventType.DOCUMENT_SAVE_STATE_CHANGED, function(e) { + console.log(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 localContentChanged = syncAttributes.contentCRC != utils.crc32(localContent); + var remoteContent = string.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(localTitle + " (backup)", localContent); + extensionMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.'); + } + else { + // Add local modifications if no collaborators change + string.setText(localContent); + } + } + + // Binds model with textarea realtimeBinding = gapi.drive.realtime.databinding.bindString(string, $("#wmd-input")[0]); - // Listen to text changed events - var debouncedRefreshPreview = _.debounce(editor.refreshPreview, 100); - string.addEventListener(gapi.drive.realtime.EventType.TEXT_INSERTED, debouncedRefreshPreview); - string.addEventListener(gapi.drive.realtime.EventType.TEXT_DELETED, debouncedRefreshPreview); - debouncedRefreshPreview(); + // Update content state according to collaborators changes + if(remoteContentChanged === true) { + logger.log("Google Drive realtime document updated from server"); + updateContentState(); + debouncedRefreshPreview(); + } // Save undo/redo buttons actions undoExecute = editor.uiManager.buttons.undo.execute; @@ -299,7 +352,7 @@ define([ model.canRedo && model.redo(); }; - // Add event handler for UndoRedoStateChanged events. + // Add event handler for model's UndoRedoStateChanged events function setUndoRedoState() { editor.uiManager.setButtonState(editor.uiManager.buttons.undo, model.canUndo); editor.uiManager.setButtonState(editor.uiManager.buttons.redo, model.canRedo); diff --git a/js/synchronizer.js b/js/synchronizer.js index e16628ab..64f1fa65 100644 --- a/js/synchronizer.js +++ b/js/synchronizer.js @@ -45,6 +45,13 @@ define([ } }); }); + + // Returns true if at least one file has synchronized location + synchronizer.hasSync = function() { + return _.some(providerMap, function(provider) { + return fileMgr.hasSync(provider); + }); + }; /*************************************************************************** * Standard synchronization @@ -234,7 +241,7 @@ define([ function tryStartRealtimeSync() { if(realtimeFileDesc !== undefined && isOnline === true) { core.lockUI(true); - realtimeSyncAttributes.provider.startRealtimeSync(realtimeFileDesc.content, realtimeSyncAttributes, function() { + realtimeSyncAttributes.provider.startRealtimeSync(realtimeFileDesc.title, realtimeFileDesc.content, realtimeSyncAttributes, function() { core.lockUI(false); }); }