Google Realtime synchronization

This commit is contained in:
benweet 2013-07-20 02:08:17 +01:00
parent d1cb3db557
commit 7eaeb45806
12 changed files with 205 additions and 72 deletions

View File

@ -134,6 +134,7 @@ input::-webkit-input-placeholder,textarea::-webkit-input-placeholder {
}
.navbar-inner .btn.disabled,
.navbar-inner .btn.blocked,
.navbar-inner .btn[disabled] {
color: #333333;
background-color: #ddd;

View File

@ -33,7 +33,7 @@
<script src="js/libs/require.js"></script>
</head>
<body>
<div id="navbar" class="navbar navbar-fixed-top ui-layout-north">
<div class="navbar navbar-fixed-top ui-layout-north">
<div class="navbar-inner">
<ul class="nav">
@ -303,7 +303,8 @@
</div>
<br /> <br />
<p>
<label class="checkbox"> <input id="input-sync-export-gdrive-realtime" type="checkbox">
<label class="checkbox"> <input
id="input-sync-export-gdrive-realtime" type="checkbox">
Create a real time collaborative document
</label>
</p>
@ -314,7 +315,9 @@
your root folder.</li>
<li>You can move or rename the file afterwards within Google
Drive.</li>
<li>Real time collaborative document can not have multiple
<li>Real time collaborative documents can't be open outside
StackEdit</li>
<li>Real time collaborative documents can't have multiple
synchronized locations.</li>
</ul>
</blockquote>

View File

@ -197,7 +197,7 @@ define([
south__minSize: 200
}));
}
$("#navbar").click(function() {
$(".navbar").click(function() {
layout.allowOverflow('north');
});
$(".ui-layout-toggler-north").addClass("btn").append($("<b>").addClass("caret"));
@ -212,7 +212,6 @@ define([
var editor = undefined;
var fileDesc = undefined;
var documentContent = undefined;
var undoManager = undefined;
core.initEditor = function(fileDescParam) {
if(fileDesc !== undefined) {
extensionMgr.onFileClosed(fileDesc);
@ -224,7 +223,7 @@ define([
editorElt.val(initDocumentContent);
if(editor !== undefined) {
// If the editor is already created
undoManager.reinit(initDocumentContent, fileDesc.editorStart, fileDesc.editorEnd, fileDesc.editorScrollTop);
editor.undoManager.reinit(initDocumentContent, fileDesc.editorStart, fileDesc.editorEnd, fileDesc.editorScrollTop);
editor.refreshPreview();
extensionMgr.onFileOpen(fileDesc);
return;
@ -310,9 +309,8 @@ define([
}
extensionMgr.onEditorConfigure(editor);
editor.hooks.chain("onPreviewRefresh", extensionMgr.onAsyncPreview);
undoManager = editor.run(previewWrapper);
undoManager.reinit(initDocumentContent, fileDesc.editorStart, fileDesc.editorEnd, fileDesc.editorScrollTop);
extensionMgr.onFileOpen(fileDesc);
editor.run(previewWrapper);
editor.undoManager.reinit(initDocumentContent, fileDesc.editorStart, fileDesc.editorEnd, fileDesc.editorScrollTop);
// Hide default buttons
$(".wmd-button-row").addClass("btn-group").find("li:not(.wmd-spacer)").addClass("btn").css("left", 0).find("span").hide();
@ -328,8 +326,15 @@ define([
$("#wmd-ulist-button").append($("<i>").addClass("icon-list"));
$("#wmd-heading-button").append($("<i>").addClass("icon-text-height"));
$("#wmd-hr-button").append($("<i>").addClass("icon-hr"));
$("#wmd-undo-button").append($("<i>").addClass("icon-undo"));
$("#wmd-redo-button").append($("<i>").addClass("icon-share-alt"));
// Create additional undo/redo button for real time synchronization
var realtimeUndoButton = $('<li class="btn hide" id="wmd-undo-button-realtime" title="Undo - Ctrl+Z" style="left: 0px;">');
realtimeUndoButton.append($("<i>").addClass("icon-undo"));
$("#wmd-undo-button").append($("<i>").addClass("icon-undo")).after(realtimeUndoButton);
var realtimeRedoButton = $('<li class="btn hide" id="wmd-redo-button-realtime" title="Redo - Ctrl+Shift+Z" style="left: 0px;">');
realtimeRedoButton.append($("<i>").addClass("icon-share-alt"));
$("#wmd-redo-button").append($("<i>").addClass("icon-share-alt")).after(realtimeRedoButton);
extensionMgr.onFileOpen(fileDesc);
};
// Used to lock the editor from the user interaction during asynchronous tasks
@ -337,14 +342,7 @@ define([
core.lockUI = function(param) {
uiLocked = param;
$("#wmd-input").prop("disabled", uiLocked);
$(".btn").each(function() {
var classes = $(this).attr("class");
if(uiLocked) {
$(this).attr("class", classes + " disabled");
} else {
$(this).attr("class", classes.replace(" disabled", ""));
}
});
$(".navbar-inner .btn").toggleClass("blocked", uiLocked);
if(uiLocked) {
$(".lock-ui").removeClass("hide");
}

View File

@ -65,15 +65,17 @@ define([
});
};
}
extensionMgr.addHookCallback = function(hookName, callback) {
hookCallbackList[hookName].push(callback);
};
// Add a Hook to the extensionMgr
function addHook(hookName, noLog) {
extensionMgr[hookName] = createHook(hookName, noLog);
}
// Used by external modules to listen to extension events
extensionMgr.addHookCallback = function(hookName, callback) {
hookCallbackList[hookName].push(callback);
};
// Set extension config
extensionSettings = settings.extensionSettings || {};
_.each(extensionList, function(extension) {

View File

@ -12,6 +12,11 @@ define([
dialogManageSynchronization.onExtensionMgrCreated = function(extensionMgrParameter) {
extensionMgr = extensionMgrParameter;
};
var synchronizer = undefined;
dialogManageSynchronization.onSynchronizerCreated = function(synchronizerParameter) {
synchronizer = synchronizerParameter;
};
var fileDesc = undefined;
var removeButtonTemplate = '<a class="btn" title="Remove this location"><i class="icon-trash"></i></a>';
@ -36,6 +41,7 @@ define([
syncDesc: syncDesc
}));
lineElement.append($(removeButtonTemplate).click(function() {
synchronizer.tryStopRealtimeSync();
fileDesc.removeSyncLocation(syncAttributes);
extensionMgr.onSyncRemoved(fileDesc, syncAttributes);
}));

View File

@ -180,7 +180,7 @@ define([
});
task.enqueue();
};
googleHelper.createRealtimeFile = function(parentId, title, callback) {
var result = undefined;
var task = new AsyncTask();
@ -189,7 +189,7 @@ define([
task.onRun(function() {
var metadata = {
title: title,
mimeType : 'application/vnd.google-apps.drive-sdk',
mimeType: 'application/vnd.google-apps.drive-sdk',
};
if(parentId !== undefined) {
// Specify the directory
@ -201,7 +201,7 @@ define([
];
}
var request = gapi.client.drive.files.insert({
'resource' : metadata
'resource': metadata
});
request.execute(function(response) {
if(response && response.id) {
@ -413,6 +413,14 @@ define([
task.chain(recursiveDownloadContent);
return;
}
// if file is a real time document
if(file.mimeType.indexOf("application/vnd.google-apps.drive-sdk") === 0) {
file.content = "";
file.isRealtime = true;
objects.shift();
task.chain(recursiveDownloadContent);
return;
}
var headers = {};
var token = gapi.auth.getToken();
if(token) {
@ -449,7 +457,7 @@ define([
});
task.enqueue();
};
googleHelper.loadRealtime = function(fileId, content, callback) {
var doc = undefined;
var task = new AsyncTask();
@ -469,9 +477,9 @@ define([
handleError({
code: err.type
}, task);
});
task.onSuccess(function() {
callback(undefined, doc);
});
task.onSuccess(function() {
callback(undefined, doc);
});
});
task.onError(function(error) {
@ -560,7 +568,12 @@ define([
pickerBuilder.setAppId(GOOGLE_DRIVE_APP_ID);
if(!isImagePicker) {
var view = new google.picker.View(google.picker.ViewId.DOCS);
view.setMimeTypes("text/x-markdown,text/plain,application/octet-stream");
view.setMimeTypes([
"text/x-markdown",
"text/plain",
"application/octet-stream",
"application/vnd.google-apps.drive-sdk." + GOOGLE_DRIVE_APP_ID
].join(","));
pickerBuilder.enableFeature(google.picker.Feature.NAV_HIDDEN);
pickerBuilder.enableFeature(google.picker.Feature.MULTISELECT_ENABLED);
pickerBuilder.addView(view);

View File

@ -148,7 +148,8 @@
//Not necessary
//forceRefresh();
return undoManager;
that.undoManager = undoManager;
that.uiManager = uiManager;
};
}
@ -663,7 +664,7 @@
}
};
util.addEvent(panels.input, "keydown", handleCtrlYZ);
//util.addEvent(panels.input, "keydown", handleCtrlYZ);
util.addEvent(panels.input, "keydown", handleModeChange);
util.addEvent(panels.input, "mousedown", function () {
setMode("moving");
@ -694,6 +695,7 @@
inputStateObj.setInputAreaSelection();
saveState();
};
this.setMode = setMode;
init();
}
@ -1251,7 +1253,7 @@
util.addEvent(inputBox, keyEvent, function (key) {
// Check to see if we have a button key and, if so execute the callback.
if ((key.ctrlKey || key.metaKey) && !key.altKey && !key.shiftKey) {
if ((key.ctrlKey || key.metaKey) && !key.altKey) {
var keyCode = key.charCode || key.keyCode;
var keyCodeStr = String.fromCharCode(keyCode).toLowerCase();
@ -1298,6 +1300,12 @@
doClick(buttons.undo);
}
break;
case "v":
undoManager.setMode("typing");
return;
case "x":
undoManager.setMode("deleting");
return;
default:
return;
}
@ -1547,6 +1555,8 @@
};
this.setUndoRedoButtonStates = setUndoRedoButtonStates;
this.buttons = buttons;
this.setButtonState = setupButton;
}

View File

@ -42,16 +42,18 @@ define([
return;
}
var fileDescList = [];
var fileDesc = undefined;
_.each(result, function(file) {
var syncAttributes = createSyncAttributes(file.id, file.etag, file.content, file.title);
syncAttributes.isRealtime = file.isRealtime;
var syncLocations = {};
syncLocations[syncAttributes.syncIndex] = syncAttributes;
var fileDesc = fileMgr.createFile(file.title, file.content, syncLocations);
fileMgr.selectFile(fileDesc);
fileDesc = fileMgr.createFile(file.title, file.content, syncLocations);
fileDescList.push(fileDesc);
});
if(fileDescList.length !== 0) {
if(fileDesc !== undefined) {
extensionMgr.onSyncImportSuccess(fileDescList, gdriveProvider);
fileMgr.selectFile(fileDesc);
}
});
});
@ -154,7 +156,9 @@ define([
_.each(changes, function(change) {
var syncIndex = createSyncIndex(change.fileId);
var syncAttributes = fileMgr.getSyncAttributes(syncIndex);
if(syncAttributes === undefined) {
// If file is not synchronized or it's a real time synchronized location
if(syncAttributes === undefined || syncAttributes.isRealtime === true) {
// Skip it
return;
}
// Store syncAttributes to avoid 2 times searching
@ -261,31 +265,75 @@ define([
});
// Start realtime synchronization
var binding = undefined;
gdriveProvider.startSync = function(content, syncAttributes, callback) {
var realtimeDocument = undefined;
var realtimeBinding = undefined;
var undoExecute = undefined;
var redoExecute = undefined;
gdriveProvider.startRealtimeSync = function(content, syncAttributes, callback) {
logger.log("Starting Google Drive realtime synchronization");
googleHelper.loadRealtime(syncAttributes.id, content, function(err, doc) {
if(err || !doc) {
callback(err);
return;
}
var string = doc.getModel().getRoot().get('content');
binding = gapi.drive.realtime.databinding.bindString(string, $("#wmd-input")[0]);
// Listen to
realtimeDocument = doc;
var model = realtimeDocument.getModel();
var string = model.getRoot().get('content');
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();
// Add event handler for UndoRedoStateChanged events.
undoExecute = editor.uiManager.buttons.undo.execute;
redoExecute = editor.uiManager.buttons.redo.execute;
// var undoButton = $('#wmd-undo-button-realtime').removeClass("hide");
// var redoButton = $('#wmd-redo-button-realtime').removeClass("hide");
// $('#wmd-undo-button').addClass("hide");
// $('#wmd-redo-button').addClass("hide");
editor.uiManager.buttons.undo.execute = function() {
model.canUndo && model.undo();
};
editor.uiManager.buttons.redo.execute = function() {
model.canRedo && model.redo();
};
function setUndoRedoState() {
editor.uiManager.setButtonState(editor.uiManager.buttons.undo, model.canUndo);
editor.uiManager.setButtonState(editor.uiManager.buttons.redo, model.canRedo);
}
model.addEventListener(
gapi.drive.realtime.EventType.UNDO_REDO_STATE_CHANGED,
setUndoRedoState);
setUndoRedoState();
callback();
});
};
// Stop realtime synchronization
gdriveProvider.stopSync = function(syncAttributes) {
gdriveProvider.stopRealtimeSync = function() {
logger.log("Stopping Google Drive realtime synchronization");
if(binding !== undefined) {
binding.unbind();
binding = undefined;
if(realtimeBinding !== undefined) {
realtimeBinding.unbind();
realtimeBinding = undefined;
}
if(realtimeDocument !== undefined) {
realtimeDocument.close();
realtimeDocument = undefined;
}
editor.uiManager.buttons.undo.execute = undoExecute;
editor.uiManager.buttons.redo.execute = redoExecute;
editor.uiManager.setUndoRedoButtonStates();
// $('#wmd-undo-button-realtime').off('click').addClass("hide");
// $('#wmd-redo-button-realtime').off('click').addClass("hide");
// $('#wmd-undo-button').removeClass("hide");
// $('#wmd-redo-button').removeClass("hide");
};
core.onReady(function() {

View File

@ -46,6 +46,10 @@ define([
});
});
/***************************************************************************
* Standard synchronization
**************************************************************************/
// Recursive function to upload a single file on multiple locations
var uploadSyncAttributesList = [];
var uploadContent = undefined;
@ -63,6 +67,12 @@ define([
// Dequeue a synchronized location
var syncAttributes = uploadSyncAttributesList.pop();
// Skip real time synchronized location
if(syncAttributes.isRealtime === true) {
locationUp(callback);
return;
}
// Use the specified provider to perform the upload
syncAttributes.provider.syncUp(uploadContent, uploadContentCRC, uploadTitle, uploadTitleCRC, syncAttributes, function(error, uploadFlag) {
if(uploadFlag === true) {
@ -188,30 +198,65 @@ define([
return true;
};
// Used for realtime synchronization
/***************************************************************************
* Realtime synchronization
**************************************************************************/
var realtimeFileDesc = undefined;
var realtimeSyncAttributes = undefined;
var isOnline = true;
// Determines if open file has real time sync location and tries to start
// real time sync
function onFileOpen(fileDesc) {
_.each(fileDesc.syncLocations, function(syncAttributes) {
if(syncAttributes.isRealtime) {
core.lockUI(true);
syncAttributes.provider.startSync(fileDesc.content, syncAttributes, function() {
core.lockUI(false);
});
}
});
}
function onFileClosed(fileDesc) {
_.each(fileDesc.syncLocations, function(syncAttributes) {
if(syncAttributes.isRealtime) {
syncAttributes.provider.stopSync(syncAttributes);
}
});
}
// Enable realtime synchronization
if(viewerMode === false) {
extensionMgr.addHookCallback("onFileOpen", onFileOpen);
extensionMgr.addHookCallback("onFileClosed", onFileClosed);
realtimeFileDesc = _.some(fileDesc.syncLocations, function(syncAttributes) {
realtimeSyncAttributes = syncAttributes;
return syncAttributes.isRealtime;
}) ? fileDesc : undefined;
tryStartRealtimeSync();
}
// Tries to start/stop real time sync on online/offline event
function onOfflineChanged(isOfflineParam) {
if(isOfflineParam === false) {
isOnline = true;
tryStartRealtimeSync();
}
else {
synchronizer.tryStopRealtimeSync();
isOnline = false;
}
}
// Starts real time synchronization if:
// 1. current file has real time sync location
// 2. we are online
function tryStartRealtimeSync() {
if(realtimeFileDesc !== undefined && isOnline === true) {
core.lockUI(true);
realtimeSyncAttributes.provider.startRealtimeSync(realtimeFileDesc.content, realtimeSyncAttributes, function() {
core.lockUI(false);
});
}
}
// Stops previously started synchronization if any
synchronizer.tryStopRealtimeSync = function() {
if(realtimeFileDesc !== undefined && isOnline === true) {
realtimeSyncAttributes.provider.stopRealtimeSync();
}
};
// Triggers realtime synchronization from extensionMgr events
if(viewerMode === false) {
extensionMgr.addHookCallback("onFileOpen", onFileOpen);
extensionMgr.addHookCallback("onFileClosed", synchronizer.tryStopRealtimeSync);
extensionMgr.addHookCallback("onOfflineChanged", onOfflineChanged);
}
/***************************************************************************
* Initialize module
**************************************************************************/
// Initialize the export dialog
function initExportDialog(provider) {
@ -250,7 +295,7 @@ define([
if(_.size(fileDesc.syncLocations) > 0) {
extensionMgr.onError("Realtime collaboration document can't be synchronized with multiple locations");
return;
}
}
// Perform the provider's real time export
provider.exportRealtimeFile(event, fileDesc.title, fileDesc.content, function(error, syncAttributes) {
if(error) {
@ -259,13 +304,18 @@ define([
syncAttributes.isRealtime = true;
fileDesc.addSyncLocation(syncAttributes);
extensionMgr.onSyncExportSuccess(fileDesc, syncAttributes);
// Start the real time sync
realtimeFileDesc = fileDesc;
realtimeSyncAttributes = syncAttributes;
tryStartRealtimeSync();
});
}
else {
if(_.size(fileDesc.syncLocations) > 0 && _.first(_.values(obj)).isRealtime) {
extensionMgr.onError("Realtime collaboration document can't be synchronized with multiple locations");
return;
}
}
// Perform the provider's standard export
provider.exportFile(event, fileDesc.title, fileDesc.content, function(error, syncAttributes) {
if(error) {
@ -290,7 +340,7 @@ define([
if(_.size(fileDesc.syncLocations) > 0 && _.first(_.values(obj)).isRealtime) {
extensionMgr.onError("Realtime collaboration document can't be synchronized with multiple locations");
return;
}
}
provider.exportManual(event, fileDesc.title, fileDesc.content, function(error, syncAttributes) {
if(error) {
return;

View File

@ -44,6 +44,7 @@ textarea[disabled],
}
.navbar-inner .btn.disabled,
.navbar-inner .btn.blocked,
.navbar-inner .btn[disabled] {
background-color: #ced5de;
}

View File

@ -111,6 +111,7 @@ blockquote {
}
.navbar-inner .btn.disabled,
.navbar-inner .btn.blocked,
.navbar-inner .btn[disabled] {
background-color: #444;
}

View File

@ -28,7 +28,7 @@
<script src="js/libs/require.js"></script>
</head>
<body class="viewer">
<div id="navbar" class="navbar navbar-fixed-top ui-layout-north">
<div class="navbar navbar-fixed-top ui-layout-north">
<div class="navbar-inner">
<ul class="nav pull-right hide" id="menu-bar">