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;
}
.icon-gdrive.realtime {
width: 18px;
background-position: -180px 0;
}
.icon-dropbox {
background-image: url("../img/icons.png") !important;
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>
</div>
<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>
<p>
Here, you can specify a <b>folder ID</b> (optional):
Please specify a <b>folder ID</b> (optional):
</p>
<div class="input-prepend">
<span class="add-on"><i class="icon-gdrive"></i></span><input
@ -302,6 +302,15 @@
placeholder="FolderID"></input>
</div>
<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>
<label class="checkbox"> <input
id="input-sync-export-gdrive-realtime" type="checkbox">
@ -311,12 +320,8 @@
<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>
<li>Real time collaborative documents can't be open outside
StackEdit</li>
StackEdit.</li>
<li>Real time collaborative documents can't have multiple
synchronized locations.</li>
</ul>
@ -336,7 +341,7 @@
<h3>Export to Dropbox</h3>
</div>
<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>
<p>
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);
};
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;
});

View File

@ -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();

View File

@ -47,7 +47,11 @@ define([
_.chain(attributesList).sortBy(function(attributes) {
return attributes.provider.providerId;
}).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(fileDesc.title);

View File

@ -26,7 +26,11 @@ define([
_.chain(attributesList).sortBy(function(attributes) {
return attributes.provider.providerId;
}).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(fileDesc.title);

View File

@ -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);

View File

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

View File

@ -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);

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
@ -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);
});
}