Google Drive realtime sync
This commit is contained in:
parent
e1ab1cbca8
commit
d1be6b1566
@ -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;
|
||||
|
BIN
img/icons.png
BIN
img/icons.png
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 5.4 KiB |
21
index.html
21
index.html
@ -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>":
|
||||
|
@ -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;
|
||||
|
||||
});
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -478,10 +478,10 @@ define([
|
||||
code: err.type
|
||||
}, task);
|
||||
});
|
||||
});
|
||||
task.onSuccess(function() {
|
||||
callback(undefined, doc);
|
||||
});
|
||||
});
|
||||
task.onError(function(error) {
|
||||
callback(error);
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
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);
|
||||
// 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);
|
||||
|
@ -46,6 +46,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);
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user