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;
|
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;
|
||||||
|
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>
|
<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>":
|
||||||
|
@ -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;
|
||||||
|
|
||||||
});
|
});
|
@ -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();
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user