Google Drive realtime sync

This commit is contained in:
benweet 2013-07-21 15:38:53 +01:00
parent e1ab1cbca8
commit d1be6b1566
11 changed files with 121 additions and 57 deletions

View File

@ -478,6 +478,11 @@ div.dropdown-menu textarea {
background-position: -19px 0; background-position: -19px 0;
} }
.icon-gdrive.realtime {
width: 18px;
background-position: -180px 0;
}
.icon-dropbox { .icon-dropbox {
background-image: url("../img/icons.png") !important; background-image: url("../img/icons.png") !important;
width: 16px; width: 16px;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -291,10 +291,10 @@
<h3>Export to Google Drive</h3> <h3>Export to Google Drive</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<blockquote class="muted">This will upload the current <blockquote class="muted">This will save the current
document to your Google Drive account and keep it synchronized.</blockquote> document to your Google Drive account and keep it synchronized.</blockquote>
<p> <p>
Here, you can specify a <b>folder ID</b> (optional): Please specify a <b>folder ID</b> (optional):
</p> </p>
<div class="input-prepend"> <div class="input-prepend">
<span class="add-on"><i class="icon-gdrive"></i></span><input <span class="add-on"><i class="icon-gdrive"></i></span><input
@ -302,6 +302,15 @@
placeholder="FolderID"></input> placeholder="FolderID"></input>
</div> </div>
<br /> <br /> <br /> <br />
<blockquote class="muted">
<b>NOTE:</b>
<ul>
<li>If no folder ID is supplied, the file will be created in
your root folder.</li>
<li>You can move or rename the file afterwards within Google
Drive.</li>
</ul>
</blockquote>
<p> <p>
<label class="checkbox"> <input <label class="checkbox"> <input
id="input-sync-export-gdrive-realtime" type="checkbox"> id="input-sync-export-gdrive-realtime" type="checkbox">
@ -311,12 +320,8 @@
<blockquote class="muted"> <blockquote class="muted">
<b>NOTE:</b> <b>NOTE:</b>
<ul> <ul>
<li>If no folder ID is supplied, the file will be created in
your root folder.</li>
<li>You can move or rename the file afterwards within Google
Drive.</li>
<li>Real time collaborative documents can't be open outside <li>Real time collaborative documents can't be open outside
StackEdit</li> StackEdit.</li>
<li>Real time collaborative documents can't have multiple <li>Real time collaborative documents can't have multiple
synchronized locations.</li> synchronized locations.</li>
</ul> </ul>
@ -336,7 +341,7 @@
<h3>Export to Dropbox</h3> <h3>Export to Dropbox</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<blockquote class="muted">This will upload the current <blockquote class="muted">This will save the current
document to your Dropbox account and keep it synchronized.</blockquote> document to your Dropbox account and keep it synchronized.</blockquote>
<p> <p>
Please specify a <b>file path</b> for "<span class="file-title"></span>": Please specify a <b>file path</b> for "<span class="file-title"></span>":

View File

@ -21,16 +21,20 @@ define([
newConfig.syncPeriod = utils.getInputIntValue("#input-sync-period", event, 0); newConfig.syncPeriod = utils.getInputIntValue("#input-sync-period", event, 0);
}; };
var synchronizer = undefined;
buttonSync.onSynchronizerCreated = function(synchronizerParameter) {
synchronizer = synchronizerParameter;
};
var button = undefined; var button = undefined;
var syncRunning = false; var syncRunning = false;
var uploadPending = false;
var isOffline = false; var isOffline = false;
// Enable/disable the button // Enable/disable the button
var updateButtonState = function() { var updateButtonState = function() {
if(button === undefined) { if(button === undefined) {
return; return;
} }
if(syncRunning === true || uploadPending === false || isOffline) { if(syncRunning === true || synchronizer.hasSync() === false || isOffline) {
button.addClass("disabled"); button.addClass("disabled");
} }
else { else {
@ -38,11 +42,6 @@ define([
} }
}; };
var synchronizer = undefined;
buttonSync.onSynchronizerCreated = function(synchronizerParameter) {
synchronizer = synchronizerParameter;
};
// Run sync periodically // Run sync periodically
var lastSync = 0; var lastSync = 0;
buttonSync.onPeriodicRun = function() { buttonSync.onPeriodicRun = function() {
@ -64,15 +63,14 @@ define([
}; };
buttonSync.onReady = updateButtonState; buttonSync.onReady = updateButtonState;
buttonSync.onFileCreated = updateButtonState;
buttonSync.onFileDeleted = updateButtonState;
buttonSync.onSyncImportSuccess = updateButtonState;
buttonSync.onSyncExportSuccess = updateButtonState;
buttonSync.onSyncRemoved = updateButtonState;
buttonSync.onSyncRunning = function(isRunning) { buttonSync.onSyncRunning = function(isRunning) {
syncRunning = isRunning; syncRunning = isRunning;
uploadPending = true;
updateButtonState();
};
buttonSync.onSyncSuccess = function() {
uploadPending = false;
updateButtonState(); updateButtonState();
}; };
@ -81,19 +79,6 @@ define([
updateButtonState(); 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; return buttonSync;
}); });

View File

@ -38,7 +38,8 @@ define([
var syncDesc = syncAttributes.id || syncAttributes.path; var syncDesc = syncAttributes.id || syncAttributes.path;
var lineElement = $(_.template(dialogManageSynchronizationLocationHTML, { var lineElement = $(_.template(dialogManageSynchronizationLocationHTML, {
provider: syncAttributes.provider, provider: syncAttributes.provider,
syncDesc: syncDesc syncDesc: syncDesc,
isRealtime: syncAttributes.isRealtime
})); }));
lineElement.append($(removeButtonTemplate).click(function() { lineElement.append($(removeButtonTemplate).click(function() {
synchronizer.tryStopRealtimeSync(); synchronizer.tryStopRealtimeSync();

View File

@ -47,7 +47,11 @@ define([
_.chain(attributesList).sortBy(function(attributes) { _.chain(attributesList).sortBy(function(attributes) {
return attributes.provider.providerId; return attributes.provider.providerId;
}).each(function(attributes) { }).each(function(attributes) {
result.push('<i class="icon-' + attributes.provider.providerId + '"></i>'); var classes = 'icon-' + attributes.provider.providerId;
if(attributes.isRealtime === true) {
classes += " realtime";
}
result.push('<i class="' + classes + '"></i>');
}); });
result.push(" "); result.push(" ");
result.push(fileDesc.title); result.push(fileDesc.title);

View File

@ -26,7 +26,11 @@ define([
_.chain(attributesList).sortBy(function(attributes) { _.chain(attributesList).sortBy(function(attributes) {
return attributes.provider.providerId; return attributes.provider.providerId;
}).each(function(attributes) { }).each(function(attributes) {
result.push('<i class="icon-' + attributes.provider.providerId + '"></i>'); var classes = 'icon-' + attributes.provider.providerId;
if(attributes.isRealtime === true) {
classes += " realtime";
}
result.push('<i class="' + classes + '"></i>');
}); });
result.push(" "); result.push(" ");
result.push(fileDesc.title); result.push(fileDesc.title);

View File

@ -478,9 +478,9 @@ define([
code: err.type code: err.type
}, task); }, task);
}); });
task.onSuccess(function() { });
callback(undefined, doc); task.onSuccess(function() {
}); callback(undefined, doc);
}); });
task.onError(function(error) { task.onError(function(error) {
callback(error); callback(error);

View File

@ -1,5 +1,5 @@
<div class="input-prepend input-append"> <div class="input-prepend input-append">
<span class="add-on" title="<%= provider.providerName %>"> <i <span class="add-on" title="<%= provider.providerName %><%= isRealtime ? ' (real time)' : '' %>"> <i
class="icon-<%= provider.providerId %>"></i> class="icon-<%= provider.providerId %><%= isRealtime ? ' realtime' : '' %>"></i>
</span> <input class="span5" type="text" value="<%= syncDesc %>" disabled /> </span> <input class="span5" type="text" value="<%= syncDesc %>" disabled />
</div> </div>

View File

@ -156,9 +156,7 @@ define([
_.each(changes, function(change) { _.each(changes, function(change) {
var syncIndex = createSyncIndex(change.fileId); var syncIndex = createSyncIndex(change.fileId);
var syncAttributes = fileMgr.getSyncAttributes(syncIndex); var syncAttributes = fileMgr.getSyncAttributes(syncIndex);
// If file is not synchronized or it's a real time synchronized location if(syncAttributes === undefined) {
if(syncAttributes === undefined || syncAttributes.isRealtime === true) {
// Skip it
return; return;
} }
// Store syncAttributes to avoid 2 times searching // Store syncAttributes to avoid 2 times searching
@ -193,6 +191,9 @@ define([
extensionMgr.onError('"' + localTitle + '" has been removed from Google Drive.'); extensionMgr.onError('"' + localTitle + '" has been removed from Google Drive.');
fileDesc.removeSyncLocation(syncAttributes); fileDesc.removeSyncLocation(syncAttributes);
extensionMgr.onSyncRemoved(fileDesc, syncAttributes); extensionMgr.onSyncRemoved(fileDesc, syncAttributes);
if(syncAttributes.isRealtime === true && fileMgr.currentFile === fileDesc) {
gdriveProvider.stopRealtimeSync();
}
return; return;
} }
var localTitleChanged = syncAttributes.titleCRC != utils.crc32(localTitle); var localTitleChanged = syncAttributes.titleCRC != utils.crc32(localTitle);
@ -206,7 +207,7 @@ define([
var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC; var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC;
var fileContentChanged = localContent != file.content; var fileContentChanged = localContent != file.content;
// Conflict detection // 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); fileMgr.createFile(localTitle + " (backup)", localContent);
extensionMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.'); 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.'); extensionMgr.onMessage('"' + localTitle + '" has been renamed to "' + file.title + '" on Google Drive.');
} }
// If file content changed // If file content changed
if(fileContentChanged && remoteContentChanged === true) { if(!syncAttributes.isRealtime && fileContentChanged && remoteContentChanged === true) {
fileDesc.content = file.content; fileDesc.content = file.content;
extensionMgr.onContentChanged(fileDesc); extensionMgr.onContentChanged(fileDesc);
extensionMgr.onMessage('"' + file.title + '" has been updated from Google Drive.'); extensionMgr.onMessage('"' + file.title + '" has been updated from Google Drive.');
@ -227,7 +228,9 @@ define([
} }
// Update syncAttributes // Update syncAttributes
syncAttributes.etag = file.etag; syncAttributes.etag = file.etag;
syncAttributes.contentCRC = remoteContentCRC; if(!syncAttributes.isRealtime) {
syncAttributes.contentCRC = remoteContentCRC;
}
syncAttributes.titleCRC = remoteTitleCRC; syncAttributes.titleCRC = remoteTitleCRC;
utils.storeAttributes(syncAttributes); utils.storeAttributes(syncAttributes);
}); });
@ -269,9 +272,9 @@ define([
var realtimeBinding = undefined; var realtimeBinding = undefined;
var undoExecute = undefined; var undoExecute = undefined;
var redoExecute = undefined; var redoExecute = undefined;
gdriveProvider.startRealtimeSync = function(content, syncAttributes, callback) { gdriveProvider.startRealtimeSync = function(localTitle, localContent, syncAttributes, callback) {
logger.log("Starting Google Drive realtime synchronization"); 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) { if(err || !doc) {
callback(err); callback(err);
return; return;
@ -279,13 +282,63 @@ define([
realtimeDocument = doc; realtimeDocument = doc;
var model = realtimeDocument.getModel(); var model = realtimeDocument.getModel();
var string = model.getRoot().get('content'); 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]); realtimeBinding = gapi.drive.realtime.databinding.bindString(string, $("#wmd-input")[0]);
// Listen to text changed events // Update content state according to collaborators changes
var debouncedRefreshPreview = _.debounce(editor.refreshPreview, 100); if(remoteContentChanged === true) {
string.addEventListener(gapi.drive.realtime.EventType.TEXT_INSERTED, debouncedRefreshPreview); logger.log("Google Drive realtime document updated from server");
string.addEventListener(gapi.drive.realtime.EventType.TEXT_DELETED, debouncedRefreshPreview); updateContentState();
debouncedRefreshPreview(); debouncedRefreshPreview();
}
// Save undo/redo buttons actions // Save undo/redo buttons actions
undoExecute = editor.uiManager.buttons.undo.execute; undoExecute = editor.uiManager.buttons.undo.execute;
@ -299,7 +352,7 @@ define([
model.canRedo && model.redo(); model.canRedo && model.redo();
}; };
// Add event handler for UndoRedoStateChanged events. // Add event handler for model's UndoRedoStateChanged events
function setUndoRedoState() { function setUndoRedoState() {
editor.uiManager.setButtonState(editor.uiManager.buttons.undo, model.canUndo); editor.uiManager.setButtonState(editor.uiManager.buttons.undo, model.canUndo);
editor.uiManager.setButtonState(editor.uiManager.buttons.redo, model.canRedo); editor.uiManager.setButtonState(editor.uiManager.buttons.redo, model.canRedo);

View File

@ -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 * Standard synchronization
@ -234,7 +241,7 @@ define([
function tryStartRealtimeSync() { function tryStartRealtimeSync() {
if(realtimeFileDesc !== undefined && isOnline === true) { if(realtimeFileDesc !== undefined && isOnline === true) {
core.lockUI(true); core.lockUI(true);
realtimeSyncAttributes.provider.startRealtimeSync(realtimeFileDesc.content, realtimeSyncAttributes, function() { realtimeSyncAttributes.provider.startRealtimeSync(realtimeFileDesc.title, realtimeFileDesc.content, realtimeSyncAttributes, function() {
core.lockUI(false); core.lockUI(false);
}); });
} }