New asyncRunner

This commit is contained in:
benweet 2013-04-20 01:14:20 +01:00
parent 51ba5930af
commit 2177785ca5
13 changed files with 1251 additions and 1176 deletions

View File

@ -64,7 +64,7 @@
<li class="dropdown-submenu"><a href="#"><i <li class="dropdown-submenu"><a href="#"><i
class="icon-gdrive"></i> Google Drive</a> class="icon-gdrive"></i> Google Drive</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="#" class="action-download-gdrive">Import <li><a href="#" class="action-sync-import-gdrive">Import
from Google Drive</a></li> from Google Drive</a></li>
<li><a href="#" data-toggle="modal" <li><a href="#" data-toggle="modal"
data-target="#modal-upload-gdrive" class="action-reset-input">Export data-target="#modal-upload-gdrive" class="action-reset-input">Export
@ -73,7 +73,7 @@
<li class="dropdown-submenu"><a href="#"><i <li class="dropdown-submenu"><a href="#"><i
class="icon-dropbox"></i> Dropbox</a> class="icon-dropbox"></i> Dropbox</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="action-download-dropbox" href="#">Import <li><a class="action-sync-import-dropbox" href="#">Import
from Dropbox</a></li> from Dropbox</a></li>
<li><a href="#" data-toggle="modal" <li><a href="#" data-toggle="modal"
data-target="#modal-upload-dropbox" class="action-reset-input">Export data-target="#modal-upload-dropbox" class="action-reset-input">Export
@ -92,6 +92,8 @@
class="icon-dropbox"></i> Dropbox</a></li> class="icon-dropbox"></i> Dropbox</a></li>
<li><a href="#" class="action-publish-github"><i <li><a href="#" class="action-publish-github"><i
class="icon-github"></i> GitHub</a></li> class="icon-github"></i> GitHub</a></li>
<li><a href="#" class="action-publish-gdrive"><i
class="icon-gdrive"></i> Google Drive</a></li>
</ul></li> </ul></li>
<li><a href="#" data-toggle="modal" <li><a href="#" data-toggle="modal"
data-target="#modal-manage-publish" class="action-reset-input"><i data-target="#modal-manage-publish" class="action-reset-input"><i
@ -182,7 +184,7 @@
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" <button type="button" class="close" data-dismiss="modal"
aria-hidden="true">&times;</button> aria-hidden="true">&times;</button>
<h3>Export</h3> <h3>Google Drive export</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>This will upload the current document into your Google Drive <p>This will upload the current document into your Google Drive
@ -193,7 +195,7 @@
<div class="modal-footer"> <div class="modal-footer">
<a href="#" class="btn" data-dismiss="modal">Cancel</a> <a href="#" <a href="#" class="btn" data-dismiss="modal">Cancel</a> <a href="#"
data-dismiss="modal" data-dismiss="modal"
class="btn btn-primary action-upload-gdrive-root">OK</a> class="btn btn-primary action-sync-export-gdrive">OK</a>
</div> </div>
</div> </div>
@ -201,7 +203,7 @@
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" <button type="button" class="close" data-dismiss="modal"
aria-hidden="true">&times;</button> aria-hidden="true">&times;</button>
<h3>Export</h3> <h3>Dropbox export</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>This will upload the current document to your Dropbox account <p>This will upload the current document to your Dropbox account
@ -210,7 +212,7 @@
</p> </p>
<div class="input-prepend"> <div class="input-prepend">
<span class="add-on"><i class="icon-dropbox"></i></span><input <span class="add-on"><i class="icon-dropbox"></i></span><input
id="upload-dropbox-path" type="text" class="span5" id="input-sync-export-dropbox-path" type="text" class="span5"
placeholder="/path/to/My Document.md"></input> placeholder="/path/to/My Document.md"></input>
</div> </div>
<br /> <br /> <b class="muted">NOTE:</b> <br /> <br /> <b class="muted">NOTE:</b>
@ -223,7 +225,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a href="#" class="btn" data-dismiss="modal">Cancel</a> <a href="#" <a href="#" class="btn" data-dismiss="modal">Cancel</a> <a href="#"
data-dismiss="modal" class="btn btn-primary action-upload-dropbox">OK</a> data-dismiss="modal" class="btn btn-primary action-sync-export-dropbox">OK</a>
</div> </div>
</div> </div>
@ -246,16 +248,16 @@
<p>Add a synchronized location manually:</p> <p>Add a synchronized location manually:</p>
<div class="input-prepend input-append"> <div class="input-prepend input-append">
<span class="add-on" title="Google Drive"><i <span class="add-on" title="Google Drive"><i
class="icon-gdrive"></i></span><input id="manual-gdrive-fileid" class="icon-gdrive"></i></span><input id="input-sync-manual-gdrive-id"
type="text" class="span5" placeholder="Google Drive file ID"></input> type="text" class="span5" placeholder="Google Drive file ID"></input>
<a class="btn action-manual-gdrive" title="Add location" <a class="btn action-sync-manual-gdrive" title="Add location"
data-dismiss="modal"><i class="icon-ok"></i></a> data-dismiss="modal"><i class="icon-ok"></i></a>
</div> </div>
<div class="input-prepend input-append"> <div class="input-prepend input-append">
<span class="add-on" title="Dropbox"><i class="icon-dropbox"></i></span><input <span class="add-on" title="Dropbox"><i class="icon-dropbox"></i></span><input
id="manual-dropbox-path" type="text" class="span5" id="input-sync-manual-dropbox-path" type="text" class="span5"
placeholder="Dropbox file path"></input> <a placeholder="Dropbox file path"></input> <a
class="btn action-manual-dropbox" title="Add location" class="btn action-sync-manual-dropbox" title="Add location"
data-dismiss="modal"><i class="icon-ok"></i></a> data-dismiss="modal"><i class="icon-ok"></i></a>
</div> </div>
<p class="muted"><b>NOTE:</b> Adding a synchronized location will <p class="muted"><b>NOTE:</b> Adding a synchronized location will
@ -271,7 +273,9 @@
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" <button type="button" class="close" data-dismiss="modal"
aria-hidden="true">&times;</button> aria-hidden="true">&times;</button>
<h3>Publish on <span class="publish-provider-name"></span></h3> <h3>
Publish on <span class="publish-provider-name"></span>
</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-horizontal"> <div class="form-horizontal">
@ -314,12 +318,29 @@
</div> </div>
</div> </div>
<div class="control-group modal-publish-dropbox"> <div class="control-group modal-publish-dropbox">
<label class="control-label" for="input-publish-dropbox-path">File path</label> <label class="control-label" for="input-publish-dropbox-path">File
path</label>
<div class="controls"> <div class="controls">
<input type="text" id="input-publish-dropbox-path" <input type="text" id="input-publish-dropbox-path"
placeholder="/path/to/My Document.html"> placeholder="/path/to/My Document.html">
</div> </div>
</div> </div>
<div class="control-group modal-publish-gdrive">
<label class="control-label" for="input-publish-gdrive-fileid">File
ID (optional)</label>
<div class="controls">
<input type="text" id="input-publish-gdrive-fileid"
placeholder="File ID">
</div>
</div>
<div class="control-group modal-publish-gdrive">
<div class="controls muted">
<b>NOTE:</b> If no file ID
is supplied, the file will be created into your Google Drive root
folder. You can move the file within Google Drive afterwards.
</div>
</div>
<div class="control-group"> <div class="control-group">
<div class="control-label">Format</div> <div class="control-label">Format</div>
<div class="controls"> <div class="controls">

View File

@ -1,121 +1,185 @@
/** /**
* Used to run asynchronous tasks sequentially (ajax mainly) * Used to run asynchronous tasks sequentially (ajax mainly) An asynchronous
* An asynchronous task must be created with: * task is composed of different callback types: onRun, onSuccess, onError
* - a required run() function that may call success(), error() or retry()
* - an optional onSuccess() function
* - an optional onError() function
* - an optional timeout field (default is 30000)
*/ */
define(["underscore"], function() { define([ "underscore" ], function() {
var asyncTaskRunner = {}; var asyncRunner = {};
// Dependencies // Dependencies
var core = undefined; var core = undefined;
var asyncTaskQueue = []; var taskQueue = [];
var currentTask = undefined; var currentTask = undefined;
var currentTaskRunning = false; var currentTaskRunning = false;
var currentTaskStartTime = 0; var currentTaskStartTime = 0;
// Run the next task in the queue if any and no other is running asyncRunner.createTask = function() {
var task = {};
task.finished = false;
task.timeout = ASYNC_TASK_DEFAULT_TIMEOUT;
task.retryCounter = 0;
/**
* onRun callbacks are called by chain(). These callbacks have to call
* chain() themselves to chain with next callback or to finish the task
* and call onSuccess callbacks.
*/
// Run callbacks
task.runCallbacks = [];
task.onRun = function(callback) {
task.runCallbacks.push(callback);
};
/**
* onSuccess callbacks are called when every onRun callbacks have
* succeed.
*/
task.successCallbacks = [];
task.onSuccess = function(callback) {
task.successCallbacks.push(callback);
};
/**
* onError callbacks are called when error() is called in a onRun
* callback.
*/
task.errorCallbacks = [];
task.onError = function(callback) {
task.errorCallbacks.push(callback);
};
/**
* chain() calls the next onRun callback or the onSuccess callbacks when
* finished. The optional callback parameter can be used to add a onRun
* callback during execution.
*/
task.chain = function(callback) {
if (task.finished === true) {
return;
}
// If first execution
if (task.queue === undefined) {
// Create a copy of the onRun callbacks
task.queue = task.runCallbacks.slice();
}
// If a callback is passed as a parameter
if(callback !== undefined) {
callback();
return;
}
// If all callbacks have been run
if (task.queue.length === 0) {
// Run the onSuccess callbacks
runSafe(task, task.successCallbacks);
return;
}
// Run the next callback
var runCallback = task.queue.shift();
runCallback();
};
/**
* error() calls the onError callbacks.
*/
task.error = function(error) {
if (task.finished === true) {
return;
}
error = error || "Unknown error";
runSafe(task, task.errorCallbacks, error);
// Exit the current call stack
throw error;
};
/**
* retry() can be called in an onRun callback to restart the task
*/
task.retry = function() {
if (task.finished === true) {
return;
}
task.queue = undefined;
if (task.retryCounter === 5) {
task.error(new Error("Maximum retry number reached"));
return;
}
// Implement an exponential backoff
var delay = Math.pow(2, task.retryCounter++) * 1000;
currentTaskStartTime = core.currentTime + delay;
currentTaskRunning = false;
asyncRunner.runTask();
};
return task;
};
// Run the next task in the queue if any and no other running
function runTask() { function runTask() {
// If there is a task currently running // If there is a task currently running
if(currentTaskRunning === true) { if (currentTaskRunning === true) {
// If the current task takes too long // If the current task takes too long
var timeout = currentTask.timeout || ASYNC_TASK_DEFAULT_TIMEOUT; if (currentTaskStartTime + currentTask.timeout < core.currentTime) {
if(currentTaskStartTime + timeout < core.currentTime) { var errorMsg = "A timeout occurred.";
currentTask.error(); core.showError(errorMsg);
currentTask.error(new Error(errorMsg));
} }
return; return;
} }
if(currentTask === undefined) { if (currentTask === undefined) {
// If no task in the queue // If no task in the queue
if(asyncTaskQueue.length === 0) { if (taskQueue.length === 0) {
return; return;
} }
// Dequeue an enqueued task // Dequeue an enqueued task
currentTask = asyncTaskQueue.shift(); currentTask = taskQueue.shift();
currentTaskStartTime = core.currentTime; currentTaskStartTime = core.currentTime;
core.showWorkingIndicator(true); core.showWorkingIndicator(true);
// Set task attributes and functions
currentTask.finished = false;
currentTask.retryCounter = 0;
currentTask.success = function() {
runSafe(this.onSuccess);
};
currentTask.error = function() {
runSafe(this.onError);
};
currentTask.retry = function() {
if(this.finished === true) {
return;
}
if(currentTask.retryCounter === 5) {
this.error();
return;
}
// Implement an exponential backoff
var delay = Math.pow(2, currentTask.retryCounter++) * 1000;
currentTaskStartTime = core.currentTime + delay;
currentTaskRunning = false;
asyncTaskRunner.runTask();
};
} }
// Run the task // Run the task
if(currentTaskStartTime <= core.currentTime) { if (currentTaskStartTime <= core.currentTime) {
currentTaskRunning = true; currentTaskRunning = true;
currentTask.run(); currentTask.chain();
} }
} }
asyncTaskRunner.runTask = function() { asyncRunner.runTask = function() {
// Use defer to avoid stack overflow // Use defer to avoid stack overflow
_.defer(runTask); _.defer(runTask);
}; };
function runSafe(func) { function runSafe(task, callbacks, param) {
if(currentTask === undefined || currentTask.finished === true) {
return;
}
try { try {
if(func) { _.each(callbacks, function(callback) {
func(); callback(param);
} });
} finally { } finally {
currentTask.finished = true; task.finished = true;
if (currentTask === task) {
currentTask = undefined; currentTask = undefined;
currentTaskRunning = false; currentTaskRunning = false;
if(asyncTaskQueue.length === 0) {
core.showWorkingIndicator(false);
} }
else { if (taskQueue.length === 0) {
asyncTaskRunner.runTask(); core.showWorkingIndicator(false);
} else {
asyncRunner.runTask();
} }
} }
} }
// Add a task into the queue // Add a task to the queue
asyncTaskRunner.addTask = function(asyncTask) { asyncRunner.addTask = function(task) {
asyncTaskQueue.push(asyncTask); taskQueue.push(task);
asyncTaskRunner.runTask(); asyncRunner.runTask();
}; };
// Change current task timeout // Change current task timeout
asyncTaskRunner.setCurrentTaskTimeout = function(timeout) { asyncRunner.setCurrentTaskTimeout = function(timeout) {
if(currentTask !== undefined) { if (currentTask !== undefined) {
currentTask.timeout = timeout; currentTask.timeout = timeout;
} }
}; };
asyncTaskRunner.init = function(coreModule) { asyncRunner.init = function(coreModule) {
core = coreModule; core = coreModule;
}; };
return asyncTaskRunner; return asyncRunner;
}); });

View File

@ -13,9 +13,9 @@ define(["jquery", "google-helper"], function($, googleHelper) {
bloggerProvider.publish = function(publishAttributes, title, content, callback) { bloggerProvider.publish = function(publishAttributes, title, content, callback) {
googleHelper.uploadBlogger(publishAttributes.blogUrl, googleHelper.uploadBlogger(publishAttributes.blogUrl,
publishAttributes.blogId, publishAttributes.postId, title, content, publishAttributes.blogId, publishAttributes.postId, title, content,
function(blogId, postId) { function(error, blogId, postId) {
if(blogId === undefined || postId === undefined) { if(error) {
callback(true); callback(error);
return; return;
} }
publishAttributes.blogId = blogId; publishAttributes.blogId = blogId;
@ -27,10 +27,7 @@ define(["jquery", "google-helper"], function($, googleHelper) {
bloggerProvider.newPublishAttributes = function(event) { bloggerProvider.newPublishAttributes = function(event) {
var publishAttributes = {}; var publishAttributes = {};
publishAttributes.blogUrl = core.getInputValue($("#input-publish-blogger-url"), event); publishAttributes.blogUrl = core.getInputValue($("#input-publish-blogger-url"), event);
var postId = $("#input-publish-blogger-postid").val(); publishAttributes.postId = $("#input-publish-blogger-postid").val() || undefined;
if(postId) {
publishAttributes.postId = postId;
}
if(event.isPropagationStopped()) { if(event.isPropagationStopped()) {
return undefined; return undefined;
} }

View File

@ -9,14 +9,16 @@ var GDRIVE_DEFAULT_FILE_TITLE = "New Markdown document";
var CHECK_ONLINE_PERIOD = 60000; var CHECK_ONLINE_PERIOD = 60000;
var AJAX_TIMEOUT = 10000; var AJAX_TIMEOUT = 10000;
var ASYNC_TASK_DEFAULT_TIMEOUT = 30000; var ASYNC_TASK_DEFAULT_TIMEOUT = 30000;
var AUTH_POPUP_TIMEOUT = 90000; var ASYNC_TASK_LONG_TIMEOUT = 90000;
var SYNC_PERIOD = 180000; var SYNC_PERIOD = 180000;
var USER_IDLE_THRESHOLD = 300000; var USER_IDLE_THRESHOLD = 300000;
var SYNC_PROVIDER_GDRIVE = "sync.gdrive."; var SYNC_PROVIDER_GDRIVE = "sync.gdrive.";
var SYNC_PROVIDER_DROPBOX = "sync.dropbox."; var SYNC_PROVIDER_DROPBOX = "sync.dropbox.";
var PROVIDER_TYPE_PUBLISH_FLAG = 1; var PROVIDER_TYPE_SYNC_FLAG = 1;
var PROVIDER_TYPE_PUBLISH_FLAG = 2;
var PROVIDER_BLOGGER = "blogger"; var PROVIDER_BLOGGER = "blogger";
var PROVIDER_DROPBOX = "dropbox"; var PROVIDER_DROPBOX = "dropbox";
var PROVIDER_GDRIVE = "gdrive";
var PROVIDER_GITHUB = "github"; var PROVIDER_GITHUB = "github";
// Use by Google's client.js // Use by Google's client.js

View File

@ -4,7 +4,7 @@ define(
"bootstrap", "jgrowl", "layout", "Markdown.Editor", "config", "bootstrap", "jgrowl", "layout", "Markdown.Editor", "config",
"underscore" ], "underscore" ],
function($, fileManager, googleHelper, dropboxHelper, githubHelper, function($, fileManager, googleHelper, dropboxHelper, githubHelper,
synchronizer, publisher, asyncTaskRunner) { synchronizer, publisher, asyncRunner) {
var core = {}; var core = {};
@ -102,7 +102,7 @@ define(
$(".modal input[type=text]:not([disabled])").val(""); $(".modal input[type=text]:not([disabled])").val("");
}; };
// Used by asyncTaskRunner // Used by asyncRunner
core.showWorkingIndicator = function(show) { core.showWorkingIndicator = function(show) {
if (show === false) { if (show === false) {
$(".working-indicator").addClass("hide"); $(".working-indicator").addClass("hide");
@ -115,6 +115,9 @@ define(
// Used to show a notification message // Used to show a notification message
core.showMessage = function(msg, iconClass, options) { core.showMessage = function(msg, iconClass, options) {
if(!msg) {
return;
}
options = options || {}; options = options || {};
iconClass = iconClass || "icon-info-sign"; iconClass = iconClass || "icon-info-sign";
$.jGrowl("<i class='icon-white " + iconClass + "'></i> " + _.escape(msg), options); $.jGrowl("<i class='icon-white " + iconClass + "'></i> " + _.escape(msg), options);
@ -466,7 +469,7 @@ define(
function setupLocalStorage() { function setupLocalStorage() {
if (localStorage["file.list"] === undefined) { if (localStorage["file.list"] === undefined) {
localStorage["file.list"] = ";"; localStorage["file.list"] = ";";
localStorage["version"] = "v1"; localStorage["version"] = "v2";
} }
} }
@ -482,22 +485,53 @@ define(
localStorage.removeItem("sync.current"); localStorage.removeItem("sync.current");
localStorage.removeItem("file.counter"); localStorage.removeItem("file.counter");
var fileIndexList = localStorage["file.list"].split(";"); var fileIndexList = _.compact(localStorage["file.list"].split(";"));
for ( var i = 1; i < fileIndexList.length - 1; i++) { _.each(fileIndexList, function(fileIndex) {
var fileIndex = fileIndexList[i];
localStorage[fileIndex + ".publish"] = ";"; localStorage[fileIndex + ".publish"] = ";";
var fileSyncIndexList = localStorage[fileIndex + ".sync"].split(";"); var fileSyncIndexList = _.compact(localStorage[fileIndex + ".sync"].split(";"));
for ( var j = 1; j < fileSyncIndexList.length - 1; j++) { _.each(fileSyncIndexList, function(fileSyncIndex) {
var fileSyncIndex = fileSyncIndexList[j];
localStorage[fileSyncIndex + ".contentCRC"] = "0"; localStorage[fileSyncIndex + ".contentCRC"] = "0";
// We store title CRC only for Google Drive synchronization // We store title CRC only for Google Drive synchronization
if(localStorage[fileSyncIndex + ".etag"] !== undefined) { if(localStorage[fileSyncIndex + ".etag"] !== undefined) {
localStorage[fileSyncIndex + ".titleCRC"] = "0"; localStorage[fileSyncIndex + ".titleCRC"] = "0";
} }
} });
} });
version = "v1"; version = "v1";
} }
// from v1 to v2
if(version == "v1") {
var SYNC_PROVIDER_GDRIVE = "sync." + PROVIDER_GDRIVE + ".";
var SYNC_PROVIDER_DROPBOX = "sync." + PROVIDER_DROPBOX + ".";
var fileIndexList = _.compact(localStorage["file.list"].split(";"));
_.each(fileIndexList, function(fileIndex) {
var fileSyncIndexList = _.compact(localStorage[fileIndex + ".sync"].split(";"));
_.each(fileSyncIndexList, function(fileSyncIndex) {
var syncAttributes = {};
if (fileSyncIndex.indexOf(SYNC_PROVIDER_GDRIVE) === 0) {
syncAttributes.provider = PROVIDER_GDRIVE;
syncAttributes.id = fileSyncIndex.substring(SYNC_PROVIDER_GDRIVE.length);
syncAttributes.etag = localStorage[fileSyncIndex + ".etag"];
syncAttributes.contentCRC = localStorage[fileSyncIndex + ".contentCRC"];
syncAttributes.titleCRC = localStorage[fileSyncIndex + ".titleCRC"];
}
else if (fileSyncIndex.indexOf(SYNC_PROVIDER_DROPBOX) === 0) {
syncAttributes.provider = PROVIDER_DROPBOX;
syncAttributes.path = decodeURIComponent(fileSyncIndex.substring(SYNC_PROVIDER_DROPBOX.length));
syncAttributes.version = localStorage[fileSyncIndex + ".version"];
syncAttributes.contentCRC = localStorage[fileSyncIndex + ".contentCRC"];
}
localStorage[fileSyncIndex] = JSON.stringify(syncAttributes);
localStorage.remoteItem(fileSyncIndex + ".etag");
localStorage.remoteItem(fileSyncIndex + ".version");
localStorage.remoteItem(fileSyncIndex + ".contentCRC");
localStorage.remoteItem(fileSyncIndex + ".titleCRC");
});
});
version = "v2";
}
localStorage["version"] = version; localStorage["version"] = version;
} }
@ -586,13 +620,13 @@ define(
core.resetModalInputs(); core.resetModalInputs();
}); });
// Init asyncTaskRunner // Init asyncRunner
asyncTaskRunner.init(core); asyncRunner.init(core);
// Init helpers // Init helpers
googleHelper.init(core, fileManager); googleHelper.init(core);
dropboxHelper.init(core, fileManager); dropboxHelper.init(core);
githubHelper.init(core, fileManager); githubHelper.init(core);
// Init synchronizer // Init synchronizer
synchronizer.init(core, fileManager); synchronizer.init(core, fileManager);
@ -614,7 +648,7 @@ define(
return; return;
} }
synchronizer.sync(); synchronizer.sync();
asyncTaskRunner.runTask(); asyncRunner.runTask();
checkOnline(); checkOnline();
}, 1000); }, 1000);
}; };

View File

@ -1,8 +1,7 @@
define(["jquery", "async-runner"], function($, asyncTaskRunner) { define(["jquery", "async-runner"], function($, asyncRunner) {
// Dependencies // Dependencies
var core = undefined; var core = undefined;
var fileManager = undefined;
var client = undefined; var client = undefined;
var authenticated = false; var authenticated = false;
@ -10,16 +9,16 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
var dropboxHelper = {}; var dropboxHelper = {};
// Try to connect dropbox by downloading client.js // Try to connect dropbox by downloading client.js
function connect(callback) { function connect(task) {
callback = callback || core.doNothing; task.onRun(function() {
if(core.isOffline === true) { if(core.isOffline === true) {
client = undefined; client = undefined;
core.showMessage("Operation not available in offline mode."); core.showMessage("Operation not available in offline mode.");
callback(true); task.error();
return; return;
} }
if (client !== undefined) { if (client !== undefined) {
callback(); task.chain();
return; return;
} }
$.ajax({ $.ajax({
@ -34,106 +33,95 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
receiverUrl: BASE_URL + "dropbox-oauth-receiver.html", receiverUrl: BASE_URL + "dropbox-oauth-receiver.html",
rememberUser: true rememberUser: true
})); }));
callback(); task.chain();
}).fail(function() { }).fail(function() {
core.setOffline(); core.setOffline();
callback(true); task.error(new Error("Network timeout"));
});
}); });
} }
// Try to authenticate with Oauth // Try to authenticate with Oauth
function authenticate(callback, immediate) { function authenticate(task) {
callback = callback || core.doNothing; task.onRun(function() {
if (immediate === undefined) {
immediate = true;
}
connect(function(error) {
if (error) {
callback(error);
return;
}
if (authenticated === true) { if (authenticated === true) {
callback(); task.chain();
return; return;
} }
var immediate = true;
function localAuthenticate() {
if (immediate === false) { if (immediate === false) {
core.showMessage("Please make sure the Dropbox authorization popup is not blocked by your browser."); core.showMessage("Please make sure the Dropbox authorization popup is not blocked by your browser.");
asyncTaskRunner.setCurrentTaskTimeout(AUTH_POPUP_TIMEOUT); // If not immediate we add time for user to enter his credentials
task.timeout = ASYNC_TASK_LONG_TIMEOUT;
} }
client.reset();
client.authenticate({interactive: !immediate}, function(error, client) { client.authenticate({interactive: !immediate}, function(error, client) {
// Success
if (client.authState === Dropbox.Client.DONE) { if (client.authState === Dropbox.Client.DONE) {
callback(); authenticated = true;
task.chain();
return; return;
} }
// If immediate did not work retry without immediate flag // If immediate did not work retry without immediate flag
if (client !== undefined && immediate === true) { if (immediate === true) {
authenticate(callback, false); immediate = false;
task.chain(localAuthenticate);
return; return;
} }
// Notify error // Error
callback(true); var errorMsg = "Access to Dropbox account is not authorized.";
core.showError(errorMsg);
task.error(new Error(errorMsg));
}); });
}
task.chain(localAuthenticate);
}); });
} }
dropboxHelper.upload = function(path, content, callback) { dropboxHelper.upload = function(path, content, callback) {
callback = callback || core.doNothing; callback = callback || core.doNothing;
var syncIndex = undefined; var result = undefined;
var asyncTask = {}; var task = asyncRunner.createTask();
asyncTask.run = function() { connect(task);
authenticate(function(error) { authenticate(task);
if (error) { task.onRun(function() {
handleError(error, asyncTask, callback);
return;
}
client.writeFile(path, content, function(error, stat) { client.writeFile(path, content, function(error, stat) {
if (!error) { if (!error) {
syncIndex = SYNC_PROVIDER_DROPBOX + encodeURIComponent(stat.path.toLowerCase()); result = stat;
localStorage[syncIndex + ".version"] = stat.versionTag; task.chain();
asyncTask.success();
return; return;
} }
// Handle error // Handle error
if(error.status === Dropbox.ApiError.INVALID_PARAM) { if(error.status === Dropbox.ApiError.INVALID_PARAM) {
error = 'Could not upload document into path "' + path + '".'; error = 'Could not upload document into path "' + path + '".';
} }
handleError(error, asyncTask, callback); handleError(error, task, callback);
}); });
}); });
}; task.onSuccess(function() {
asyncTask.onSuccess = function() { callback(undefined, result);
callback(undefined, syncIndex); });
}; task.onError(function(error) {
asyncTask.onError = function() { callback(error);
callback(true); });
}; asyncRunner.addTask(task);
asyncTaskRunner.addTask(asyncTask);
}; };
dropboxHelper.checkUpdates = function(lastChangeId, callback) { dropboxHelper.checkUpdates = function(lastChangeId, callback) {
callback = callback || core.doNothing; callback = callback || core.doNothing;
var changes = []; var changes = [];
var newChangeId = lastChangeId || 0; var newChangeId = lastChangeId || 0;
var asyncTask = {}; var task = asyncRunner.createTask();
asyncTask.run = function() { connect(task);
function retrievePageOfChanges(changeId) { authenticate(task);
if(asyncTask.finished === true) { task.onRun(function() {
return; function retrievePageOfChanges() {
} client.pullChanges(newChangeId, function(error, pullChanges) {
authenticate(function(error) {
if (error) { if (error) {
handleError(error, asyncTask, callback); handleError(error, task, callback);
return; return;
} }
client.pullChanges(changeId, function(error, pullChanges) {
if (error) {
handleError(error, asyncTask, callback);
return;
}
// Retrieve success // Retrieve success
newChangeId = pullChanges.cursor(); newChangeId = pullChanges.cursor();
if(pullChanges.changes !== undefined) { if(pullChanges.changes !== undefined) {
@ -147,82 +135,68 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
} }
} }
if (pullChanges.shouldPullAgain) { if (pullChanges.shouldPullAgain) {
retrievePageOfChanges(newChangeId); task.chain(retrievePageOfChanges);
} else { } else {
asyncTask.success(); task.chain();
} }
}); });
});
} }
retrievePageOfChanges(newChangeId); task.chain(retrievePageOfChanges);
}; });
asyncTask.onSuccess = function() { task.onSuccess(function() {
callback(undefined, changes, newChangeId); callback(undefined, changes, newChangeId);
}; });
asyncTask.onError = function() { task.onError(function(error) {
callback(true); callback(error);
}; });
asyncTaskRunner.addTask(asyncTask); asyncRunner.addTask(task);
}; };
dropboxHelper.downloadMetadata = function(paths, callback) { dropboxHelper.downloadMetadata = function(paths, callback) {
callback = callback || core.doNothing; callback = callback || core.doNothing;
result = result || []; var result = [];
var task = asyncRunner.createTask();
var path = paths.pop(); connect(task);
var asyncTask = {}; authenticate(task);
asyncTask.run = function() { task.onRun(function() {
function recursiveDownloadMetadata() { function recursiveDownloadMetadata() {
if(asyncTask.finished === true) {
return;
}
if(paths.length === 0) { if(paths.length === 0) {
asyncTask.success(); task.chain();
return; return;
} }
authenticate(function(error) { var path = paths.pop();
if (error) {
handleError(error, asyncTask, callback);
return;
}
client.stat(path, function(error, stat) { client.stat(path, function(error, stat) {
if(stat) { if(stat) {
result.push(stat); result.push(stat);
recursiveDownloadMetadata(); task.chain(recursiveDownloadMetadata);
return; return;
} }
handleError(error, asyncTask, callback); handleError(error, task, callback);
});
}); });
} }
recursiveDownloadMetadata(); task.chain(recursiveDownloadMetadata);
}; });
asyncTask.onSuccess = function() { task.onSuccess(function() {
callback(undefined, result); callback(undefined, result);
}; });
asyncTask.onError = function() { task.onError(function(error) {
callback(true); callback(error);
}; });
asyncTaskRunner.addTask(asyncTask); asyncRunner.addTask(task);
}; };
dropboxHelper.downloadContent = function(objects, callback, result) { dropboxHelper.downloadContent = function(objects, callback) {
callback = callback || core.doNothing; callback = callback || core.doNothing;
result = result || []; var result = [];
var task = asyncRunner.createTask();
var asyncTask = {}; connect(task);
asyncTask.run = function() { authenticate(task);
task.onRun(function() {
function recursiveDownloadContent() { function recursiveDownloadContent() {
if(asyncTask.finished === true) {
return;
}
if(objects.length === 0) { if(objects.length === 0) {
asyncTask.success(); task.chain();
return; return;
} }
var object = objects.pop(); var object = objects.pop();
result.push(object); result.push(object);
var file = undefined; var file = undefined;
@ -235,44 +209,31 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
file = object.stat; file = object.stat;
} }
if(!file) { if(!file) {
recursiveDownloadContent(); task.chain(recursiveDownloadContent);
return; return;
} }
authenticate(function(error) {
if (error) {
handleError(error, asyncTask, callback);
return;
}
client.readFile(file.path, function(error, data) { client.readFile(file.path, function(error, data) {
if(data) { if(data) {
file.content = data; file.content = data;
recursiveDownloadContent(); recursiveDownloadContent();
return; return;
} }
handleError(error, asyncTask, callback); handleError(error, task, callback);
});
}); });
} }
recursiveDownloadContent(); task.chain(recursiveDownloadContent);
}; });
asyncTask.onSuccess = function() { task.onSuccess(function() {
callback(undefined, result); callback(undefined, result);
}; });
asyncTask.onError = function() { task.onError(function(error) {
callback(true); callback(error);
}; });
asyncTaskRunner.addTask(asyncTask); asyncRunner.addTask(task);
}; };
function handleError(error, asyncTask, callback) { function handleError(error, task, callback) {
var errorMsg = undefined; var errorMsg = true;
asyncTask.onError = function() {
if (errorMsg !== undefined) {
core.showError(errorMsg);
}
callback(errorMsg);
};
if (error) { if (error) {
console.error(error); console.error(error);
// Try to analyze the error // Try to analyze the error
@ -281,7 +242,20 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
} else if (error.status === Dropbox.ApiError.INVALID_TOKEN } else if (error.status === Dropbox.ApiError.INVALID_TOKEN
|| error.status === Dropbox.ApiError.OAUTH_ERROR) { || error.status === Dropbox.ApiError.OAUTH_ERROR) {
authenticated = false; authenticated = false;
errorMsg = "Access to Dropbox is not authorized."; task.retry();
return;
} else if(error.status === Dropbox.ApiError.INVALID_PARAM && error.responseText
.indexOf("oauth_nonce") !== -1) {
// A bug I guess...
_.each(_.keys(localStorage), function(key) {
// We have to remove the Oauth cache from the localStorage
if(key.indexOf("dropbox-auth") === 0) {
localStorage.removeItem(key);
}
});
authenticated = false;
task.retry();
return;
} else if (error.status === Dropbox.ApiError.NETWORK_ERROR) { } else if (error.status === Dropbox.ApiError.NETWORK_ERROR) {
client = undefined; client = undefined;
authenticated = false; authenticated = false;
@ -290,31 +264,27 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
errorMsg = "Dropbox error (" errorMsg = "Dropbox error ("
+ error.status + ")."; + error.status + ").";
} }
core.showError(errorMsg);
} }
asyncTask.error(); task.error(new Error(errorMsg));
} }
var pickerLoaded = false; var pickerLoaded = false;
function loadPicker(callback) { function loadPicker(task) {
task.onRun(function() {
if (pickerLoaded === true) { if (pickerLoaded === true) {
callback(); task.chain();
return; return;
} }
connect(function(error) {
if (error) {
pickerLoaded = false;
callback(error);
return;
}
$.ajax({ $.ajax({
url : "https://www.dropbox.com/static/api/1/dropbox.js", url : "https://www.dropbox.com/static/api/1/dropbox.js",
dataType : "script", timeout : AJAX_TIMEOUT dataType : "script", timeout : AJAX_TIMEOUT
}).done(function() { }).done(function() {
pickerLoaded = true; pickerLoaded = true;
callback(); task.chain();
}).fail(function() { }).fail(function() {
callback(true); core.setOffline();
task.error();
}); });
}); });
} }
@ -322,14 +292,12 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
dropboxHelper.picker = function(callback) { dropboxHelper.picker = function(callback) {
callback = callback || core.doNothing; callback = callback || core.doNothing;
var paths = []; var paths = [];
var task = asyncRunner.createTask();
var asyncTask = {}; // Add some time for user to choose his files
asyncTask.run = function() { task.timeout = ASYNC_TASK_LONG_TIMEOUT;
loadPicker(function(error) { connect(task);
if (error) { loadPicker(task);
handleError(error, asyncTask, callback); task.onRun(function() {
return;
}
var options = {}; var options = {};
options.multiselect = true; options.multiselect = true;
options.linkType = "direct"; options.linkType = "direct";
@ -339,61 +307,25 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
path = path.replace(/.*\/view\/[^\/]*/, ""); path = path.replace(/.*\/view\/[^\/]*/, "");
paths.push(decodeURI(path)); paths.push(decodeURI(path));
} }
asyncTask.success(); task.chain();
}; };
options.cancel = function() { options.cancel = function() {
asyncTask.error(); task.chain();
}; };
Dropbox.choose(options); Dropbox.choose(options);
core.showMessage("Please make sure the Dropbox chooser popup is not blocked by your browser."); core.showMessage("Please make sure the Dropbox chooser popup is not blocked by your browser.");
}); });
}; task.onSuccess(function() {
asyncTask.onSuccess = function() {
callback(undefined, paths); callback(undefined, paths);
}; });
asyncTask.onError = function() { task.onError(function(error) {
callback(true); callback(error);
}; });
asyncTaskRunner.addTask(asyncTask); asyncRunner.addTask(task);
}; };
dropboxHelper.importFiles = function(paths) { dropboxHelper.init = function(coreModule) {
dropboxHelper.downloadMetadata(paths, function(error, result) {
if(error) {
return;
}
dropboxHelper.downloadContent(result, function(error, result) {
if(error) {
return;
}
for(var i=0; i<result.length; i++) {
var file = result[i];
syncIndex = SYNC_PROVIDER_DROPBOX + encodeURIComponent(file.path.toLowerCase());
localStorage[syncIndex + ".version"] = file.versionTag;
var contentCRC = core.crc32(file.content);
localStorage[syncIndex + ".contentCRC"] = contentCRC;
var fileIndex = fileManager.createFile(file.name, file.content, [syncIndex]);
fileManager.selectFile(fileIndex);
core.showMessage('"' + file.name + '" imported successfully from Dropbox.');
}
});
});
};
dropboxHelper.init = function(coreModule, fileManagerModule) {
core = coreModule; core = coreModule;
fileManager = fileManagerModule;
};
dropboxHelper.checkPath = function(path) {
if(!path.match(/^[^\\<>:"\|?\*]+$/)) {
core.showError('"' + path + '" contains invalid characters.');
return undefined;
}
if(path.indexOf("/") !== 0) {
return "/" + path;
}
return path;
}; };
return dropboxHelper; return dropboxHelper;

View File

@ -2,16 +2,116 @@ define(["jquery", "dropbox-helper"], function($, dropboxHelper) {
// Dependencies // Dependencies
var core = undefined; var core = undefined;
var fileManager = undefined;
var dropboxProvider = { var dropboxProvider = {
providerType: PROVIDER_TYPE_PUBLISH_FLAG, providerType: PROVIDER_TYPE_SYNC_FLAG | PROVIDER_TYPE_PUBLISH_FLAG,
providerId: PROVIDER_DROPBOX, providerId: PROVIDER_DROPBOX,
providerName: "Dropbox", providerName: "Dropbox",
defaultPublishFormat: "template" defaultPublishFormat: "template"
}; };
function checkPath(path) {
if(path === undefined) {
return undefined;
}
if(!path.match(/^[^\\<>:"\|?\*]+$/)) {
core.showError('"' + path + '" contains invalid characters.');
return undefined;
}
if(path.indexOf("/") !== 0) {
return "/" + path;
}
return path;
}
function createSyncAttributes(path, versionTag, content) {
var syncAttributes = {};
syncAttributes.provider = PROVIDER_DROPBOX;
syncAttributes.path = path;
syncAttributes.version = versionTag;
syncAttributes.contentCRC = core.crc32(content);
var syncIndex = "sync." + PROVIDER_DROPBOX + "." + encodeURIComponent(file.path.toLowerCase());
localStorage[syncIndex] = JSON.stringify(syncAttributes);
return syncIndex;
}
function importFilesFromPaths(paths) {
dropboxHelper.downloadMetadata(paths, function(error, result) {
if(error) {
return;
}
dropboxHelper.downloadContent(result, function(error, result) {
if(error) {
return;
}
_.each(result, function(file) {
var syncIndex = createSyncAttributes(file.path, file.versionTag, file.content);
var fileIndex = fileManager.createFile(file.name, file.content, [syncIndex]);
fileManager.selectFile(fileIndex);
core.showMessage('"' + file.name + '" imported successfully from Dropbox.');
});
});
});
}
dropboxProvider.importFiles = function() {
dropboxHelper.picker(function(error, paths) {
if(error) {
return;
}
var importPaths = [];
_.each(paths, function(path) {
var syncIndex = "sync." + PROVIDER_DROPBOX + "." + encodeURIComponent(path.toLowerCase());
var fileIndex = fileManager.getFileIndexFromSync(syncIndex);
if(fileIndex !== undefined) {
var title = localStorage[fileIndex + ".title"];
core.showError('"' + title + '" was already imported');
return;
}
importPaths.push(path);
});
dropboxHelper.importFiles(importPaths);
});
};
function exportFileToPath(path, title, content, callback) {
path = dropboxHelper.checkPath(path);
if(path === undefined) {
callback(true);
return;
}
// Check that file is not synchronized with an other one
var syncIndex = "sync." + PROVIDER_DROPBOX + "." + encodeURIComponent(path.toLowerCase());
var fileIndex = fileManager.getFileIndexFromSync(syncIndex);
if(fileIndex !== undefined) {
var existingTitle = localStorage[fileIndex + ".title"];
core.showError('File path is already synchronized with "' + existingTitle + '"');
callback(true);
return;
}
dropboxHelper.upload(path, content, function(error, result) {
if (error) {
callback(error);
return;
}
syncIndex = createSyncAttributes(result.path, result.versionTag, content);
callback(undefined, syncIndex);
});
}
dropboxProvider.exportFile = function(event, title, content, callback) {
var path = core.getInputValue($("#input-sync-export-dropbox-path"), event);
exportFileToPath(path);
};
dropboxProvider.exportManual = function(event, title, content, callback) {
var path = core.getInputValue($("#input-sync-manual-dropbox-path"), event);
exportFileToPath(path);
};
dropboxProvider.publish = function(publishAttributes, title, content, callback) { dropboxProvider.publish = function(publishAttributes, title, content, callback) {
var path = dropboxHelper.checkPath(publishAttributes.path); var path = checkPath(publishAttributes.path);
if(path === undefined) { if(path === undefined) {
callback(true); callback(true);
return; return;
@ -28,8 +128,9 @@ define(["jquery", "dropbox-helper"], function($, dropboxHelper) {
return publishAttributes; return publishAttributes;
}; };
dropboxProvider.init = function(coreModule) { dropboxProvider.init = function(coreModule, fileManagerModule) {
core = coreModule; core = coreModule;
fileManager = fileManagerModule;
}; };
return dropboxProvider; return dropboxProvider;

View File

@ -40,7 +40,7 @@ define(["jquery", "google-helper", "dropbox-helper", "github-helper", "synchroni
// Update the file titles // Update the file titles
fileManager.updateFileTitles(); fileManager.updateFileTitles();
refreshManageSync(); synchronizer.refreshManageSync();
publisher.notifyPublish(); publisher.notifyPublish();
// Recreate the editor // Recreate the editor
@ -186,14 +186,11 @@ define(["jquery", "google-helper", "dropbox-helper", "github-helper", "synchroni
localStorage[fileIndex + ".sync"] = localStorage[fileIndex + ".sync"].replace(";" localStorage[fileIndex + ".sync"] = localStorage[fileIndex + ".sync"].replace(";"
+ syncIndex + ";", ";"); + syncIndex + ";", ";");
if(fileManager.isCurrentFileIndex(fileIndex)) { if(fileManager.isCurrentFileIndex(fileIndex)) {
refreshManageSync(); synchronizer.refreshManageSync();
} }
} }
// Remove ETAG, version, CRCs (if any) // Remove sync attributes
localStorage.removeItem(syncIndex + ".etag"); localStorage.removeItem(syncIndex);
localStorage.removeItem(syncIndex + ".version");
localStorage.removeItem(syncIndex + ".contentCRC");
localStorage.removeItem(syncIndex + ".titleCRC");
}; };
// Get the fileIndex associated to a syncIndex // Get the fileIndex associated to a syncIndex
@ -215,7 +212,7 @@ define(["jquery", "google-helper", "dropbox-helper", "github-helper", "synchroni
publisher.notifyPublish(); publisher.notifyPublish();
} }
} }
// Remove publish object // Remove publish attributes
localStorage.removeItem(publishIndex); localStorage.removeItem(publishIndex);
}; };
@ -228,148 +225,6 @@ define(["jquery", "google-helper", "dropbox-helper", "github-helper", "synchroni
}).value(); }).value();
}; };
function uploadGdrive(fileId, folderId) {
var fileIndex = fileManager.getCurrentFileIndex();
var content = localStorage[fileIndex + ".content"];
var title = localStorage[fileIndex + ".title"];
googleHelper.upload(fileId, folderId, title, content, function(syncIndex) {
if (syncIndex === undefined) {
return;
}
var contentCRC = core.crc32(content);
localStorage[syncIndex + ".contentCRC"] = contentCRC;
var titleCRC = core.crc32(title);
localStorage[syncIndex + ".titleCRC"] = titleCRC;
localStorage[fileIndex + ".sync"] += syncIndex + ";";
refreshManageSync();
fileManager.updateFileTitles();
core.showMessage('"' + title
+ '" will now be synchronized on Google Drive.');
});
}
function manualGdrive(fileId) {
if(!fileId) {
return;
}
// Check that file is not synchronized with an other one
var syncIndex = SYNC_PROVIDER_GDRIVE + fileId;
var fileIndex = fileManager.getFileIndexFromSync(syncIndex);
if(fileIndex !== undefined) {
var title = localStorage[fileIndex + ".title"];
core.showError('File ID is already synchronized with "' + title + '"');
return;
}
uploadGdrive(fileId);
}
function importGdrive(ids) {
if(ids === undefined) {
return;
}
var importIds = [];
for(var i=0; i<ids.length; i++) {
var fileId = ids[i];
var syncIndex = SYNC_PROVIDER_GDRIVE + fileId;
var fileIndex = fileManager.getFileIndexFromSync(syncIndex);
if(fileIndex !== undefined) {
var title = localStorage[fileIndex + ".title"];
core.showError('"' + title + '" was already imported');
continue;
}
importIds.push(fileId);
}
googleHelper.importFiles(importIds);
}
function manualDropbox(path) {
if(!path) {
return;
}
path = dropboxHelper.checkPath(path);
if(path === undefined) {
return;
}
// Check that file is not synchronized with an other one
var syncIndex = SYNC_PROVIDER_DROPBOX + encodeURIComponent(path.toLowerCase());
var fileIndex = fileManager.getFileIndexFromSync(syncIndex);
if(fileIndex !== undefined) {
var title = localStorage[fileIndex + ".title"];
core.showError('Path "' + path + '" is already synchronized with "' + title + '"');
return;
}
var fileIndex = fileManager.getCurrentFileIndex();
var content = localStorage[fileIndex + ".content"];
var title = localStorage[fileIndex + ".title"];
dropboxHelper.upload(path, content, function(error, syncIndex) {
if (error) {
return;
}
var contentCRC = core.crc32(content);
localStorage[syncIndex + ".contentCRC"] = contentCRC;
localStorage[fileIndex + ".sync"] += syncIndex + ";";
refreshManageSync();
fileManager.updateFileTitles();
core.showMessage('"' + title
+ '" will now be synchronized on Dropbox.');
});
}
function importDropbox(error, paths) {
if(error) {
return;
}
var importPaths = [];
for(var i=0; i<paths.length; i++) {
var filePath = paths[i];
var syncIndex = SYNC_PROVIDER_DROPBOX + encodeURIComponent(filePath.toLowerCase());
var fileIndex = fileManager.getFileIndexFromSync(syncIndex);
if(fileIndex !== undefined) {
var title = localStorage[fileIndex + ".title"];
core.showError('"' + title + '" was already imported');
continue;
}
importPaths.push(filePath);
}
dropboxHelper.importFiles(importPaths);
}
function refreshManageSync() {
var fileIndex = fileManager.getCurrentFileIndex();
var syncIndexList = _.compact(localStorage[fileIndex + ".sync"].split(";"));
$(".msg-no-sync, .msg-sync-list").addClass("hide");
$("#manage-sync-list .input-append").remove();
if (syncIndexList.length > 0) {
$(".msg-sync-list").removeClass("hide");
} else {
$(".msg-no-sync").removeClass("hide");
}
_.each(syncIndexList, function(syncIndex) {
var line = $("<div>").addClass("input-prepend input-append");
if (syncIndex.indexOf(SYNC_PROVIDER_GDRIVE) === 0) {
line.append($("<span>").addClass("add-on").prop("title", "Google Drive").html(
'<i class="icon-gdrive"></i>'));
line.append($("<input>").prop("type", "text").prop(
"disabled", true).addClass("span5").val(
syncIndex.substring(SYNC_PROVIDER_GDRIVE.length)));
}
else if (syncIndex.indexOf(SYNC_PROVIDER_DROPBOX) === 0) {
line.append($("<span>").addClass("add-on").prop("title", "Dropbox").html(
'<i class="icon-dropbox"></i>'));
line.append($("<input>").prop("type", "text").prop(
"disabled", true).addClass("span5").val(
decodeURIComponent(syncIndex.substring(SYNC_PROVIDER_DROPBOX.length))));
}
line.append($("<a>").addClass("btn").html(
'<i class="icon-trash"></i>').prop("title",
"Remove this location").click(function() {
fileManager.removeSync(syncIndex);
fileManager.updateFileTitles();
}));
$("#manage-sync-list").append(line);
});
}
fileManager.init = function(coreModule) { fileManager.init = function(coreModule) {
core = coreModule; core = coreModule;
@ -388,18 +243,29 @@ define(["jquery", "google-helper", "dropbox-helper", "github-helper", "synchroni
$(this).hide(); $(this).hide();
$("#file-title-input").show().focus(); $("#file-title-input").show().focus();
}); });
$("#file-title-input").blur(function() { function applyTitle(input) {
var title = $.trim($(this).val()); var title = $.trim(input.val());
if (title) {
var fileIndexTitle = fileManager.getCurrentFileIndex() + ".title"; var fileIndexTitle = fileManager.getCurrentFileIndex() + ".title";
if (title) {
if (title != localStorage[fileIndexTitle]) { if (title != localStorage[fileIndexTitle]) {
localStorage[fileIndexTitle] = title; localStorage[fileIndexTitle] = title;
fileManager.updateFileTitles(); fileManager.updateFileTitles();
fileManager.saveFile(); fileManager.saveFile();
} }
} }
$(this).hide(); input.hide().val(localStorage[fileIndexTitle]);
$("#file-title").show(); $("#file-title").show();
}
$("#file-title-input").blur(function() {
applyTitle($(this));
}).keyup(function(e) {
if (e.keyCode == 13) {
applyTitle($(this));
}
if (e.keyCode == 27) {
$(this).val("");
applyTitle($(this));
}
}); });
$(".action-download-md").click( $(".action-download-md").click(
function() { function() {
@ -422,38 +288,6 @@ define(["jquery", "google-helper", "dropbox-helper", "github-helper", "synchroni
+ core.encodeBase64(content); + core.encodeBase64(content);
window.open(uriContent, 'file'); window.open(uriContent, 'file');
}); });
// Synchronize actions
$(".action-upload-gdrive-root").click(function() {
uploadGdrive();
});
$(".action-upload-gdrive-select").click(function() {
// This action is not available because picker does not support
// folder selection
googleHelper.picker(function(ids) {
if(ids !== undefined && ids.length !== 0) {
uploadGdrive(undefined, ids[0]);
}
}, true);
});
$(".action-download-gdrive").click(function() {
googleHelper.picker(importGdrive);
});
$(".action-manual-gdrive").click(function(event) {
var fileId = core.getInputValue($("#manual-gdrive-fileid"), event);
manualGdrive(fileId);
});
$(".action-download-dropbox").click(function() {
dropboxHelper.picker(importDropbox);
});
$(".action-upload-dropbox").click(function(event) {
var path = core.getInputValue($("#upload-dropbox-path"), event);
manualDropbox(path);
});
$(".action-manual-dropbox").click(function(event) {
var path = core.getInputValue($("#manual-dropbox-path"), event);
manualDropbox(path);
});
}; };
return fileManager; return fileManager;

152
js/gdrive-provider.js Normal file
View File

@ -0,0 +1,152 @@
define(["jquery", "google-helper", "underscore"], function($, googleHelper) {
// Dependencies
var core = undefined;
var fileManager = undefined;
var gdriveProvider = {
providerType: PROVIDER_TYPE_SYNC_FLAG | PROVIDER_TYPE_PUBLISH_FLAG,
providerId: PROVIDER_GDRIVE,
providerName: "Gdrive",
defaultPublishFormat: "template"
};
function createSyncAttributes(id, etag, content, title) {
var syncAttributes = {};
syncAttributes.provider = PROVIDER_GDRIVE;
syncAttributes.id = id;
syncAttributes.etag = etag;
syncAttributes.contentCRC = core.crc32(content);
syncAttributes.titleCRC = core.crc32(title);
var syncIndex = "sync." + PROVIDER_GDRIVE + "." + file.id;
localStorage[syncIndex] = JSON.stringify(syncAttributes);
return syncIndex;
}
function importFilesFromIds(ids) {
googleHelper.downloadMetadata(ids, function(error, result) {
if(error) {
return;
}
googleHelper.downloadContent(result, function(error, result) {
if(error) {
return;
}
_.each(result, function(file) {
var syncIndex = createSyncAttributes(file.id, file.etag, file.content, file.title);
var fileIndex = fileManager.createFile(file.title, file.content, [syncIndex]);
fileManager.selectFile(fileIndex);
core.showMessage('"' + file.title + '" imported successfully from Google Drive.');
});
});
});
};
gdriveProvider.importFiles = function() {
googleHelper.picker(function(error, ids) {
if(ids === undefined) {
return;
}
var importIds = [];
_.each(ids, function(id) {
var syncIndex = "sync." + PROVIDER_GDRIVE + "." + id;
var fileIndex = fileManager.getFileIndexFromSync(syncIndex);
if(fileIndex !== undefined) {
var title = localStorage[fileIndex + ".title"];
core.showError('"' + title + '" was already imported');
return;
}
importIds.push(id);
});
importFilesFromIds(importIds);
});
};
gdriveProvider.exportFile = function(event, title, content, callback) {
googleHelper.upload(undefined, undefined, title, content, function(error, result) {
if (error) {
callback(error);
return;
}
var syncIndex = createSyncAttributes(result.id, result.etag, content, title);
callback(undefined, syncIndex);
});
};
gdriveProvider.exportManual = function(event, title, content, callback) {
var id = core.getInputValue($("#input-sync-manual-gdrive-id"), event);
if(!id) {
return;
}
// Check that file is not synchronized with an other one
var syncIndex = "sync." + PROVIDER_GDRIVE + "." + id;
var fileIndex = fileManager.getFileIndexFromSync(syncIndex);
if(fileIndex !== undefined) {
var existingTitle = localStorage[fileIndex + ".title"];
core.showError('File ID is already synchronized with "' + existingTitle + '"');
callback(true);
return;
}
googleHelper.upload(id, undefined, title, content, function(error, result) {
if (error) {
callback(error);
return;
}
var syncIndex = createSyncAttributes(result.id, result.etag, content, title);
callback(undefined, syncIndex);
});
};
gdriveProvider.publish = function(publishAttributes, title, content, callback) {
googleHelper.upload(publishAttributes.fileId, undefined, title, content, callback);
};
gdriveProvider.newPublishAttributes = function(event) {
var publishAttributes = {};
publishAttributes.fileId = $("#input-publish-gdrive-fileid").val() || undefined;
if(event.isPropagationStopped()) {
return undefined;
}
return publishAttributes;
};
gdriveProvider.init = function(coreModule, fileManagerModule) {
core = coreModule;
fileManager = fileManagerModule;
var state = localStorage["sync.gdrive.state"];
if(state === undefined) {
return;
}
localStorage.removeItem("sync.gdrive.state");
state = JSON.parse(state);
if (state.action == "create") {
googleHelper.upload(undefined, state.folderId, GDRIVE_DEFAULT_FILE_TITLE,
"", function(error, file) {
if(error) {
return;
}
var syncIndex = createSyncAttributes(file.id, file.etag, file.content, file.title);
var fileIndex = fileManager.createFile(file.title, file.content, [syncIndex]);
fileManager.selectFile(fileIndex);
core.showMessage('"' + file.title + '" created successfully on Google Drive.');
});
}
else if (state.action == "open") {
var importIds = [];
_.each(state.ids, function(id) {
var syncIndex = "sync." + PROVIDER_GDRIVE + "." + id;
var fileIndex = fileManager.getFileIndexFromSync(syncIndex);
if(fileIndex !== undefined) {
fileManager.selectFile(fileIndex);
}
else {
importIds.push(id);
}
});
importFilesFromIds(importIds);
}
};
return gdriveProvider;
});

View File

@ -1,4 +1,4 @@
define(["jquery", "async-runner"], function($, asyncTaskRunner) { define(["jquery", "async-runner"], function($, asyncRunner) {
// Dependencies // Dependencies
var core = undefined; var core = undefined;
@ -9,18 +9,17 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
var githubHelper = {}; var githubHelper = {};
// Try to connect github by downloading js file // Try to connect github by downloading js file
function connect(callback) { function connect(task) {
callback = callback || core.doNothing; callback = callback || core.doNothing;
var asyncTask = {}; task.onRun(function() {
asyncTask.run = function() {
if(core.isOffline === true) { if(core.isOffline === true) {
connected = false; connected = false;
core.showMessage("Operation not available in offline mode."); core.showMessage("Operation not available in offline mode.");
asyncTask.error(); task.error();
return; return;
} }
if (connected === true) { if (connected === true) {
asyncTask.success(); task.chain();
return; return;
} }
$.ajax({ $.ajax({
@ -28,159 +27,138 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
dataType : "script", timeout : AJAX_TIMEOUT dataType : "script", timeout : AJAX_TIMEOUT
}).done(function() { }).done(function() {
connected = true; connected = true;
asyncTask.success(); task.chain();
}).fail(function() { }).fail(function() {
asyncTask.error();
});
};
asyncTask.onSuccess = function() {
callback();
};
asyncTask.onError = function() {
core.setOffline(); core.setOffline();
callback(); task.error(new Error("Network timeout"));
}; });
asyncTaskRunner.addTask(asyncTask); });
} }
// Try to authenticate with Oauth // Try to authenticate with Oauth
function authenticate(callback, immediate) { function authenticate(task) {
callback = callback || core.doNothing;
connect(function() {
if (connected === false) {
callback();
return;
}
var intervalId = undefined;
var authWindow = undefined; var authWindow = undefined;
var token = localStorage["githubToken"]; var intervalId = undefined;
var errorMsg = "Access to GitHub is not authorized."; task.onRun(function() {
var asyncTask = {};
asyncTask.run = function() {
if (github !== undefined) { if (github !== undefined) {
asyncTask.success(); task.chain();
return; return;
} }
var token = localStorage["githubToken"];
if (token !== undefined) { if(token !== undefined) {
github = new Github({ github = new Github({
token: token, token: token,
auth: "oauth" auth: "oauth"
}); });
asyncTask.success(); task.chain();
return; return;
} }
if(immediate === true) {
core.showError();
asyncTask.error();
return;
}
// We add time for user to enter his credentials
asyncTask.timeout = AUTH_POPUP_TIMEOUT;
core.showMessage("Please make sure the Github authorization popup is not blocked by your browser."); core.showMessage("Please make sure the Github authorization popup is not blocked by your browser.");
// We add time for user to enter his credentials
task.timeout = ASYNC_TASK_LONG_TIMEOUT;
var code = undefined;
function getCode() {
localStorage.removeItem("githubCode"); localStorage.removeItem("githubCode");
authWindow = core.popupWindow( authWindow = core.popupWindow(
'github-oauth-client.html?client_id=' + GITHUB_CLIENT_ID, 'github-oauth-client.html?client_id=' + GITHUB_CLIENT_ID,
'stackedit-github-oauth', 960, 600); 'stackedit-github-oauth', 960, 600);
authWindow.focus(); authWindow.focus();
intervalId = setInterval(function() { intervalId = setInterval(function() {
var code = localStorage["githubCode"]; if(authWindow.closed === true) {
if(authWindow.closed === true || code !== undefined) { clearInterval(intervalId);
localStorage.removeItem("githubCode"); authWindow = undefined;
intervalId = undefined;
code = localStorage["githubCode"];
if(code === undefined) { if(code === undefined) {
asyncTask.error(); task.error();
return; return;
} }
$.getJSON(GATEKEEPER_URL + "authenticate/" + code, function(data) { localStorage.removeItem("githubCode");
if(data.token !== undefined) { task.chain(getToken);
localStorage["githubToken"] = data.token;
asyncTask.success();
}
else {
asyncTask.error();
} }
}); });
} }
}, 500); function getToken() {
}; $.getJSON(GATEKEEPER_URL + "authenticate/" + code, function(data) {
asyncTask.onSuccess = function() { if(data.token !== undefined) {
if(intervalId !== undefined) { token = data.token;
clearInterval(intervalId); localStorage["githubToken"] = token;
github = new Github({
token: token,
auth: "oauth"
});
task.chain();
} }
if (github !== undefined) { else {
callback(); task.error();
return;
} }
authenticate(callback, true); });
}; }
asyncTask.onError = function() { task.chain(getCode);
});
task.onError(function() {
if(intervalId !== undefined) { if(intervalId !== undefined) {
clearInterval(intervalId); clearInterval(intervalId);
} }
if(authWindow !== undefined) { if(authWindow !== undefined) {
authWindow.close(); authWindow.close();
} }
core.showError(errorMsg);
callback();
};
asyncTaskRunner.addTask(asyncTask);
}); });
} }
githubHelper.upload = function(reponame, branch, path, content, commitMsg, callback) { githubHelper.upload = function(reponame, branch, path, content, commitMsg, callback) {
callback = callback || core.doNothing; callback = callback || core.doNothing;
authenticate(function() { var task = asyncRunner.createTask();
if (github === undefined) { connect(task);
callback("error"); authenticate(task);
return; task.onRun(function() {
} var userLogin = undefined;
function getUserLogin() {
var error = undefined;
var asyncTask = {};
asyncTask.run = function() {
var user = github.getUser(); var user = github.getUser();
user.show(undefined, function(err, result) { user.show(undefined, function(err, result) {
if(err) { if(err) {
error = err.error; task.error(err);
asyncTask.error();
return; return;
} }
var repo = github.getRepo(result.login, reponame); userLogin = result.login;
task.chain(write);
});
}
function write() {
var repo = github.getRepo(userLogin, reponame);
repo.write(branch, path, content, commitMsg, function(err) { repo.write(branch, path, content, commitMsg, function(err) {
if(err) { if(err) {
error = err.error; task.error(err);
asyncTask.error();
return; return;
} }
asyncTask.success(); task.chain();
}); });
});
};
asyncTask.onSuccess = function() {
callback(error);
};
asyncTask.onError = function() {
if(error !== undefined) {
console.error(error);
} }
task.chain(getUserLogin);
});
task.onSuccess(function() {
callback();
});
task.onError(function(err) {
var errorMsg = "Could not publish on GitHub."; var errorMsg = "Could not publish on GitHub.";
if(error === 401 || error === 403) { if(err !== undefined) {
console.error(err);
if(err.error === 401 || err.error === 403) {
github = undefined; github = undefined;
// Token must be renewed // Token must be renewed
localStorage.removeItem("githubToken"); localStorage.removeItem("githubToken");
errorMsg = "Access to GitHub is not authorized."; errorMsg = "Access to GitHub is not authorized.";
} }
else if(error === 0) { else if(err.error === 0) {
connected = false; connected = false;
github = undefined; github = undefined;
core.setOffline(); core.setOffline();
} }
}
core.showError(errorMsg); core.showError(errorMsg);
callback(error); callback(errorMsg);
};
asyncTaskRunner.addTask(asyncTask);
}); });
asyncRunner.addTask(task);
}; };
githubHelper.init = function(coreModule) { githubHelper.init = function(coreModule) {

View File

@ -1,8 +1,7 @@
define(["jquery", "async-runner"], function($, asyncTaskRunner) { define(["jquery", "async-runner"], function($, asyncRunner) {
// Dependencies // Dependencies
var core = undefined; var core = undefined;
var fileManager = undefined;
var connected = false; var connected = false;
var authenticated = false; var authenticated = false;
@ -10,109 +9,83 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
var googleHelper = {}; var googleHelper = {};
// Try to connect Gdrive by downloading client.js // Try to connect Gdrive by downloading client.js
function connect(callback) { function connect(task) {
callback = callback || core.doNothing; task.onRun(function() {
var asyncTask = {};
asyncTask.run = function() {
if(core.isOffline === true) { if(core.isOffline === true) {
connected = false; connected = false;
core.showMessage("Operation not available in offline mode."); core.showMessage("Operation not available in offline mode.");
asyncTask.error(); task.error();
return; return;
} }
if (connected === true) { if (connected === true) {
asyncTask.success(); task.chain();
return; return;
} }
delayedFunction = function() { delayedFunction = function() {
asyncTask.success(); connected = true;
task.chain();
}; };
$.ajax({ $.ajax({
url : "https://apis.google.com/js/client.js?onload=runDelayedFunction", url : "https://apis.google.com/js/client.js?onload=runDelayedFunction",
dataType : "script", timeout : AJAX_TIMEOUT dataType : "script", timeout : AJAX_TIMEOUT
}).fail(function() { }).fail(function() {
asyncTask.error();
});
};
asyncTask.onSuccess = function() {
connected = true;
callback();
};
asyncTask.onError = function() {
core.setOffline(); core.setOffline();
callback(); task.error(new Error("Network timeout"));
}; });
asyncTaskRunner.addTask(asyncTask); });
} }
// Try to authenticate with Oauth // Try to authenticate with Oauth
function authenticate(callback, immediate) { function authenticate(task) {
callback = callback || core.doNothing; task.onRun(function() {
if (immediate === undefined) {
immediate = true;
}
connect(function() {
if (connected === false) {
callback();
return;
}
var asyncTask = {};
// If not immediate we add time for user to enter his credentials
if (immediate === false) {
asyncTask.timeout = AUTH_POPUP_TIMEOUT;
}
asyncTask.run = function() {
if (authenticated === true) { if (authenticated === true) {
asyncTask.success(); task.chain();
return; return;
} }
var immediate = true;
function localAuthenticate() {
if (immediate === false) { if (immediate === false) {
core.showMessage("Please make sure the Google authorization popup is not blocked by your browser."); core.showMessage("Please make sure the Google authorization popup is not blocked by your browser.");
// If not immediate we add time for user to enter his credentials
task.timeout = ASYNC_TASK_LONG_TIMEOUT;
} }
gapi.auth.authorize({ 'client_id' : GOOGLE_CLIENT_ID, gapi.auth.authorize({ 'client_id' : GOOGLE_CLIENT_ID,
'scope' : GOOGLE_SCOPES, 'immediate' : immediate }, function( 'scope' : GOOGLE_SCOPES, 'immediate' : immediate }, function(
authResult) { authResult) {
gapi.client.load('drive', 'v2', function() { gapi.client.load('drive', 'v2', function() {
if (!authResult || authResult.error) { if (!authResult || authResult.error) {
asyncTask.error();
return;
}
authenticated = true;
asyncTask.success();
});
});
};
asyncTask.onSuccess = function() {
callback();
};
asyncTask.onError = function() {
// If immediate did not work retry without immediate flag // If immediate did not work retry without immediate flag
if (connected === true && immediate === true) { if (connected === true && immediate === true) {
authenticate(callback, false); immediate = false;
task.chain(localAuthenticate);
return; return;
} }
callback(); // Error
}; var errorMsg = "Access to Google account is not authorized.";
asyncTaskRunner.addTask(asyncTask); core.showError(errorMsg);
task.error(new Error(errorMsg));
return;
}
// Success
authenticated = true;
task.chain();
});
});
}
task.chain(localAuthenticate);
}); });
} }
googleHelper.upload = function(fileId, parentId, title, content, callback) { googleHelper.upload = function(fileId, parentId, title, content, callback) {
callback = callback || core.doNothing; callback = callback || core.doNothing;
authenticate(function() { var result = undefined;
if (connected === false) { var task = asyncRunner.createTask();
callback(); connect(task);
return; authenticate(task);
} task.onRun(function() {
var fileSyncIndex = undefined;
var asyncTask = {};
asyncTask.run = function() {
var boundary = '-------314159265358979323846'; var boundary = '-------314159265358979323846';
var delimiter = "\r\n--" + boundary + "\r\n"; var delimiter = "\r\n--" + boundary + "\r\n";
var close_delim = "\r\n--" + boundary + "--"; var close_delim = "\r\n--" + boundary + "--";
var contentType = 'text/x-markdown'; var contentType = 'text/x-markdown';
var metadata = { title : title, mimeType : contentType }; var metadata = { title : title, mimeType : contentType };
if (parentId !== undefined) { if (parentId !== undefined) {
@ -154,9 +127,8 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
request.execute(function(response) { request.execute(function(response) {
if (response && response.id) { if (response && response.id) {
// Upload success // Upload success
fileSyncIndex = SYNC_PROVIDER_GDRIVE + response.id; result = response;
localStorage[fileSyncIndex + ".etag"] = response.etag; task.chain();
asyncTask.success();
return; return;
} }
var error = response.error; var error = response.error;
@ -171,35 +143,44 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
error = 'Conflict on file ID "' + fileId + '". Please restart the synchronization.'; error = 'Conflict on file ID "' + fileId + '". Please restart the synchronization.';
} }
} }
handleError(error, asyncTask, callback); handleError(error, task, callback);
}); });
};
asyncTask.onSuccess = function() {
callback(fileSyncIndex);
};
asyncTask.onError = function() {
callback();
};
asyncTaskRunner.addTask(asyncTask);
}); });
task.onSuccess(function() {
callback(undefined, result);
});
task.onError(function(error) {
callback(error);
});
asyncRunner.addTask(task);
}; };
googleHelper.checkUpdates = function(lastChangeId, callback) { googleHelper.checkUpdates = function(lastChangeId, callback) {
callback = callback || core.doNothing; callback = callback || core.doNothing;
authenticate(function() {
if (connected === false) {
callback();
return;
}
var changes = []; var changes = [];
var newChangeId = lastChangeId || 0; var newChangeId = lastChangeId || 0;
function retrievePageOfChanges(request) { var task = asyncRunner.createTask();
connect(task);
authenticate(task);
task.onRun(function() {
var nextPageToken = undefined; var nextPageToken = undefined;
var asyncTask = {}; function retrievePageOfChanges() {
asyncTask.run = function() { var request = undefined;
if(nextPageToken === undefined) {
request = gapi.client.drive.changes
.list({ 'startChangeId' : newChangeId + 1 });
}
else {
request = gapi.client.drive.changes
.list({ 'pageToken' : nextPageToken });
}
request.execute(function(response) { request.execute(function(response) {
if (response && response.largestChangeId) { if (!response || !response.largestChangeId) {
// Handle error
handleError(response.error, task, callback);
return;
}
// Retrieve success // Retrieve success
newChangeId = response.largestChangeId; newChangeId = response.largestChangeId;
nextPageToken = response.nextPageToken; nextPageToken = response.nextPageToken;
@ -214,50 +195,38 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
} }
} }
} }
asyncTask.success();
return;
}
// Handle error
handleError(response.error, asyncTask, callback);
});
};
asyncTask.onSuccess = function() {
if (nextPageToken !== undefined) { if (nextPageToken !== undefined) {
request = gapi.client.drive.changes task.chain(retrievePageOfChanges);
.list({ 'pageToken' : nextPageToken });
retrievePageOfChanges(request);
} else {
callback(changes, newChangeId);
} }
}; else {
asyncTask.onError = function() { task.chain();
callback();
};
asyncTaskRunner.addTask(asyncTask);
} }
var initialRequest = gapi.client.drive.changes
.list({ 'startChangeId' : newChangeId + 1 });
retrievePageOfChanges(initialRequest);
}); });
}
task.chain(retrievePageOfChanges);
});
task.onSuccess(function() {
callback(undefined, changes, newChangeId);
});
task.onError(function(error) {
callback(error);
});
asyncRunner.addTask(task);
}; };
googleHelper.downloadMetadata = function(ids, callback, result) { googleHelper.downloadMetadata = function(ids, callback) {
callback = callback || core.doNothing; callback = callback || core.doNothing;
result = result || []; var result = [];
var task = asyncRunner.createTask();
connect(task);
authenticate(task);
task.onRun(function() {
function recursiveDownloadMetadata() {
if(ids.length === 0) { if(ids.length === 0) {
callback(result); task.chain();
return; return;
} }
authenticate(function() {
if (connected === false) {
callback();
return;
}
var id = ids.pop(); var id = ids.pop();
var asyncTask = {};
asyncTask.run = function() {
var token = gapi.auth.getToken(); var token = gapi.auth.getToken();
var headers = { var headers = {
Authorization : token ? "Bearer " + token.access_token: null Authorization : token ? "Bearer " + token.access_token: null
@ -269,7 +238,7 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
timeout : AJAX_TIMEOUT timeout : AJAX_TIMEOUT
}).done(function(data, textStatus, jqXHR) { }).done(function(data, textStatus, jqXHR) {
result.push(data); result.push(data);
asyncTask.success(); task.chain(recursiveDownloadMetadata);
}).fail(function(jqXHR) { }).fail(function(jqXHR) {
var error = { var error = {
code: jqXHR.status, code: jqXHR.status,
@ -279,27 +248,34 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
if(error.code === 404) { if(error.code === 404) {
error = 'File ID "' + id + '" does not exist on Google Drive.'; error = 'File ID "' + id + '" does not exist on Google Drive.';
} }
handleError(error, asyncTask, callback); handleError(error, task, callback);
}); });
}; }
asyncTask.onSuccess = function() { task.chain(recursiveDownloadMetadata);
googleHelper.downloadMetadata(ids, callback, result);
};
asyncTask.onError = function() {
callback();
};
asyncTaskRunner.addTask(asyncTask);
}); });
task.onSuccess(function() {
callback(undefined, result);
});
task.onError(function(error) {
callback(error);
});
asyncRunner.addTask(task);
}; };
googleHelper.downloadContent = function(objects, callback, result) { googleHelper.downloadContent = function(objects, callback) {
callback = callback || core.doNothing; callback = callback || core.doNothing;
result = result || []; var result = [];
var task = asyncRunner.createTask();
// Add some time for user to choose his files
task.timeout = ASYNC_TASK_LONG_TIMEOUT;
connect(task);
authenticate(task);
task.onRun(function() {
function recursiveDownloadContent() {
if(objects.length === 0) { if(objects.length === 0) {
callback(result); task.chain();
return; return;
} }
var object = objects.pop(); var object = objects.pop();
result.push(object); result.push(object);
var file = undefined; var file = undefined;
@ -312,18 +288,9 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
file = object.file; file = object.file;
} }
if(!file) { if(!file) {
this.downloadContent(objects, callback, result); task.chain(recursiveDownloadContent);
return; return;
} }
authenticate(function() {
if (connected === false) {
callback();
return;
}
var asyncTask = {};
asyncTask.run = function() {
var token = gapi.auth.getToken(); var token = gapi.auth.getToken();
var headers = { var headers = {
Authorization : token ? "Bearer " + token.access_token: null Authorization : token ? "Bearer " + token.access_token: null
@ -335,35 +302,29 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
timeout : AJAX_TIMEOUT timeout : AJAX_TIMEOUT
}).done(function(data, textStatus, jqXHR) { }).done(function(data, textStatus, jqXHR) {
file.content = data; file.content = data;
asyncTask.success(); task.chain(recursiveDownloadContent);
}).fail(function(jqXHR) { }).fail(function(jqXHR) {
var error = { var error = {
code: jqXHR.status, code: jqXHR.status,
message: jqXHR.statusText message: jqXHR.statusText
}; };
// Handle error // Handle error
handleError(error, asyncTask, callback); handleError(error, task, callback);
}); });
}; }
asyncTask.onSuccess = function() { task.chain(recursiveDownloadContent);
googleHelper.downloadContent(objects, callback, result);
};
asyncTask.onError = function() {
callback();
};
asyncTaskRunner.addTask(asyncTask);
}); });
task.onSuccess(function() {
callback(undefined, result);
});
task.onError(function(error) {
callback(error);
});
asyncRunner.addTask(task);
}; };
function handleError(error, asyncTask, callback, serviceName) { function handleError(error, task, callback) {
serviceName = serviceName || "Google Drive";
var errorMsg = undefined; var errorMsg = undefined;
asyncTask.onError = function() {
if (errorMsg !== undefined) {
core.showError(errorMsg);
}
callback();
};
if (error) { if (error) {
console.error(error); console.error(error);
// Try to analyze the error // Try to analyze the error
@ -371,38 +332,31 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
errorMsg = error; errorMsg = error;
} }
else if (error.code >= 500 && error.code < 600) { else if (error.code >= 500 && error.code < 600) {
errorMsg = serviceName + " is not accessible.";
// Retry as described in Google's best practices // Retry as described in Google's best practices
asyncTask.retry(); task.retry();
return; return;
} else if (error.code === 401 || error.code === 403) { } else if (error.code === 401 || error.code === 403) {
authenticated = false; authenticated = false;
errorMsg = "Access to " + serviceName + " is not authorized."; task.retry();
return;
} else if (error.code <= 0) { } else if (error.code <= 0) {
connected = false; connected = false;
authenticated = false; authenticated = false;
core.setOffline(); core.setOffline();
} else { } else {
errorMsg = serviceName + " error (" + error.code + ": " errorMsg = "Google error (" + error.code + ": "
+ error.message + ")."; + error.message + ").";
} }
core.showError(errorMsg);
} }
asyncTask.error(); task.error(new Error(errorMsg));
} }
var pickerLoaded = false; var pickerLoaded = false;
function loadPicker(callback) { function loadPicker(task) {
connect(function() { task.onRun(function() {
if (connected === false) {
pickerLoaded = false;
callback();
return;
}
var asyncTask = {};
asyncTask.run = function() {
if (pickerLoaded === true) { if (pickerLoaded === true) {
asyncTask.success(); task.chain();
return; return;
} }
$.ajax({ $.ajax({
@ -410,30 +364,23 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
data : {key: GOOGLE_KEY}, data : {key: GOOGLE_KEY},
dataType : "script", timeout : AJAX_TIMEOUT dataType : "script", timeout : AJAX_TIMEOUT
}).done(function() { }).done(function() {
asyncTask.success(); google.load('picker', '1', {callback: task.chain});
}).fail(function() {
asyncTask.error();
});
};
asyncTask.onSuccess = function() {
google.load('picker', '1', {callback: callback});
pickerLoaded = true; pickerLoaded = true;
}; }).fail(function() {
asyncTask.onError = function() {
core.setOffline(); core.setOffline();
callback(); task.error(new Error("Network timeout"));
}; });
asyncTaskRunner.addTask(asyncTask);
}); });
} }
googleHelper.picker = function(callback) { googleHelper.picker = function(callback) {
callback = callback || core.doNothing; callback = callback || core.doNothing;
loadPicker(function() { var ids = [];
if (pickerLoaded === false) { var picker = undefined;
callback(); var task = asyncRunner.createTask();
return; connect(task);
} loadPicker(task);
task.onRun(function() {
var view = new google.picker.View(google.picker.ViewId.DOCS); var view = new google.picker.View(google.picker.ViewId.DOCS);
view.setMimeTypes("text/x-markdown,text/plain"); view.setMimeTypes("text/x-markdown,text/plain");
var pickerBuilder = new google.picker.PickerBuilder(); var pickerBuilder = new google.picker.PickerBuilder();
@ -449,88 +396,46 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
pickerBuilder.setCallback(function(data) { pickerBuilder.setCallback(function(data) {
if (data.action == google.picker.Action.PICKED || if (data.action == google.picker.Action.PICKED ||
data.action == google.picker.Action.CANCEL) { data.action == google.picker.Action.CANCEL) {
var ids = [];
if(data.action == google.picker.Action.PICKED) { if(data.action == google.picker.Action.PICKED) {
for(var i=0; i<data.docs.length; i++) { for(var i=0; i<data.docs.length; i++) {
ids.push(data.docs[i].id); ids.push(data.docs[i].id);
} }
} }
$(".modal-backdrop, .picker").remove(); $(".modal-backdrop, .picker").remove();
callback(ids); task.chain();
} }
}); });
var picker = pickerBuilder.build(); picker = pickerBuilder.build();
$("body").append($("<div>").addClass("modal-backdrop").click(function() { $("body").append($("<div>").addClass("modal-backdrop").click(function() {
picker.setVisible(false); picker.setVisible(false);
$(".modal-backdrop, .picker").remove(); $(".modal-backdrop, .picker").remove();
callback(); task.chain();
})); }));
picker.setVisible(true); picker.setVisible(true);
}); });
}; task.onSuccess(function() {
callback(undefined, ids);
googleHelper.importFiles = function(ids) {
googleHelper.downloadMetadata(ids, function(result) {
if(result === undefined) {
return;
}
googleHelper.downloadContent(result, function(result) {
if(result === undefined) {
return;
}
for(var i=0; i<result.length; i++) {
var file = result[i];
fileSyncIndex = SYNC_PROVIDER_GDRIVE + file.id;
localStorage[fileSyncIndex + ".etag"] = file.etag;
var contentCRC = core.crc32(file.content);
localStorage[fileSyncIndex + ".contentCRC"] = contentCRC;
var titleCRC = core.crc32(file.title);
localStorage[fileSyncIndex + ".titleCRC"] = titleCRC;
var fileIndex = fileManager.createFile(file.title, file.content, [fileSyncIndex]);
fileManager.selectFile(fileIndex);
core.showMessage('"' + file.title + '" imported successfully from Google Drive.');
}
}); });
task.onError(function(error) {
if(picker !== undefined) {
picker.setVisible(false);
$(".modal-backdrop, .picker").remove();
task.chain();
}
callback(error);
}); });
asyncRunner.addTask(task);
}; };
googleHelper.uploadBlogger = function(blogUrl, blogId, postId, title, content, callback) { googleHelper.uploadBlogger = function(blogUrl, blogId, postId, title, content, callback) {
authenticate(function() { var task = asyncRunner.createTask();
if (connected === false) { connect(task);
callback(); authenticate(task);
return; task.onRun(function() {
}
var asyncTask = {};
asyncTask.run = function() {
var token = gapi.auth.getToken(); var token = gapi.auth.getToken();
var headers = { var headers = {
Authorization : token ? "Bearer " + token.access_token: null Authorization : token ? "Bearer " + token.access_token: null
}; };
function getBlogId(localCallback) {
$.ajax({
url : "https://www.googleapis.com/blogger/v3/blogs/byurl",
data: { url: blogUrl },
headers : headers,
dataType : "json",
timeout : AJAX_TIMEOUT
}).done(function(blog, textStatus, jqXHR) {
blogId = blog.id;
localCallback();
}).fail(function(jqXHR) {
var error = {
code: jqXHR.status,
message: jqXHR.statusText
};
// Handle error
if(error.code === 404) {
error = 'Blog "' + blogUrl + '" not found on Blogger.';
}
handleError(error, asyncTask, callback, "Blogger");
});
}
function publish() { function publish() {
var url = "https://www.googleapis.com/blogger/v3/blogs/" + blogId + "/posts/"; var url = "https://www.googleapis.com/blogger/v3/blogs/" + blogId + "/posts/";
var data = { var data = {
@ -556,7 +461,7 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
timeout : AJAX_TIMEOUT timeout : AJAX_TIMEOUT
}).done(function(post, textStatus, jqXHR) { }).done(function(post, textStatus, jqXHR) {
postId = post.id; postId = post.id;
asyncTask.success(); task.chain();
}).fail(function(jqXHR) { }).fail(function(jqXHR) {
var error = { var error = {
code: jqXHR.status, code: jqXHR.status,
@ -566,65 +471,47 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
if(error.code === 404 && postId !== undefined) { if(error.code === 404 && postId !== undefined) {
error = 'Post ' + postId + ' not found on Blogger.'; error = 'Post ' + postId + ' not found on Blogger.';
} }
handleError(error, asyncTask, callback, "Blogger"); handleError(error, task, callback);
}); });
} }
function getBlogId() {
if(blogId === undefined) { if(blogId !== undefined) {
getBlogId(publish); task.chain(publish);
} }
else { $.ajax({
publish(); url : "https://www.googleapis.com/blogger/v3/blogs/byurl",
data: { url: blogUrl },
headers : headers,
dataType : "json",
timeout : AJAX_TIMEOUT
}).done(function(blog, textStatus, jqXHR) {
blogId = blog.id;
task.chain(publish);
}).fail(function(jqXHR) {
var error = {
code: jqXHR.status,
message: jqXHR.statusText
};
// Handle error
if(error.code === 404) {
error = 'Blog "' + blogUrl + '" not found on Blogger.';
} }
}; handleError(error, task, callback);
asyncTask.onSuccess = function() {
callback(blogId, postId);
};
asyncTask.onError = function() {
callback();
};
asyncTaskRunner.addTask(asyncTask);
}); });
}
task.chain(getBlogId);
});
task.onSuccess = function() {
callback(undefined, blogId, postId);
};
task.onError = function(error) {
callback(error);
};
asyncRunner.addTask(task);
}; };
googleHelper.init = function(coreModule, fileManagerModule) { googleHelper.init = function(coreModule) {
core = coreModule; core = coreModule;
fileManager = fileManagerModule;
var state = localStorage["sync.gdrive.state"];
if(state === undefined) {
return;
}
localStorage.removeItem("sync.gdrive.state");
state = JSON.parse(state);
if (state.action == "create") {
googleHelper.upload(undefined, state.folderId, GDRIVE_DEFAULT_FILE_TITLE,
"", function(fileSyncIndex) {
if(fileSyncIndex === undefined) {
return;
}
var contentCRC = core.crc32("");
localStorage[fileSyncIndex + ".contentCRC"] = contentCRC;
var titleCRC = core.crc32(GDRIVE_DEFAULT_FILE_TITLE);
localStorage[fileSyncIndex + ".titleCRC"] = titleCRC;
var fileIndex = fileManager.createFile(GDRIVE_DEFAULT_FILE_TITLE, "", [fileSyncIndex]);
fileManager.selectFile(fileIndex);
core.showMessage('"' + GDRIVE_DEFAULT_FILE_TITLE + '" created successfully on Google Drive.');
});
}
else if (state.action == "open") {
var ids = [];
for(var i=0; i<state.ids.length; i++) {
var id = state.ids[i];
var fileSyncIndex = SYNC_PROVIDER_GDRIVE + id;
var fileIndex = fileManager.getFileIndexFromSync(fileSyncIndex);
if(fileIndex !== undefined) {
fileManager.selectFile(fileIndex);
} else {
ids.push(id);
}
}
googleHelper.importFiles(ids);
}
}; };
return googleHelper; return googleHelper;

View File

@ -1,4 +1,4 @@
define(["jquery", "github-provider", "blogger-provider", "dropbox-provider", "underscore"], function($) { define(["jquery", "github-provider", "blogger-provider", "dropbox-provider", "gdrive-provider", "underscore"], function($) {
// Dependencies // Dependencies
var core = undefined; var core = undefined;
@ -183,8 +183,7 @@ define(["jquery", "github-provider", "blogger-provider", "dropbox-provider", "un
$(".msg-no-publish").removeClass("hide"); $(".msg-no-publish").removeClass("hide");
} }
_.each(publishIndexList, function(publishIndex) { _.each(publishIndexList, function(publishIndex) {
var serializedObject = localStorage[publishIndex]; var publishAttributes = JSON.parse(localStorage[publishIndex]);
var publishAttributes = JSON.parse(serializedObject);
var publishDesc = JSON.stringify(publishAttributes).replace(/{|}|"/g, ""); var publishDesc = JSON.stringify(publishAttributes).replace(/{|}|"/g, "");
lineElement = $(_.template(lineTemplate, { lineElement = $(_.template(lineTemplate, {
provider: providerMap[publishAttributes.provider], provider: providerMap[publishAttributes.provider],
@ -203,7 +202,7 @@ define(["jquery", "github-provider", "blogger-provider", "dropbox-provider", "un
// Init each provider // Init each provider
_.each(providerMap, function(provider) { _.each(providerMap, function(provider) {
provider.init(core); provider.init(core, fileManager);
// Publish provider button // Publish provider button
$(".action-publish-" + provider.providerId).click(function() { $(".action-publish-" + provider.providerId).click(function() {
initNewLocation(provider); initNewLocation(provider);

View File

@ -1,10 +1,16 @@
define(["jquery", "google-helper", "dropbox-helper"], function($, googleHelper, dropboxHelper) { define(["jquery", "google-helper", "dropbox-helper", "dropbox-provider", "gdrive-provider"], function($, googleHelper, dropboxHelper) {
var synchronizer = {}; var synchronizer = {};
// Dependencies // Dependencies
var core = undefined; var core = undefined;
var fileManager = undefined; var fileManager = undefined;
// Create a map with providerName: providerObject
var providerMap = _.chain(arguments)
.map(function(argument) {
return argument && argument.providerType & PROVIDER_TYPE_SYNC_FLAG && [argument.providerId, argument];
}).compact().object().value();
// Used to know the providers we are connected to // Used to know the providers we are connected to
synchronizer.useGoogleDrive = false; synchronizer.useGoogleDrive = false;
synchronizer.useDropbox = false; synchronizer.useDropbox = false;
@ -52,15 +58,12 @@ define(["jquery", "google-helper", "dropbox-helper"], function($, googleHelper,
} }
// Dequeue a synchronized location // Dequeue a synchronized location
var fileSyncIndex = uploadFileSyncIndexList.pop(); var syncIndex = uploadFileSyncIndexList.pop();
if(!fileSyncIndex) { var syncAttributes = JSON.parse(localStorage[fileSyncIndex]);
locationUp(callback); =
return;
}
// Skip if CRC has not changed
var syncContentCRC = localStorage[fileSyncIndex + ".contentCRC"]; var syncContentCRC = localStorage[fileSyncIndex + ".contentCRC"];
var syncTitleCRC = localStorage[fileSyncIndex + ".titleCRC"]; var syncTitleCRC = localStorage[fileSyncIndex + ".titleCRC"];
// Skip if CRC has not changed
if(uploadContentCRC == syncContentCRC && (syncTitleCRC === undefined || uploadTitleCRC == syncTitleCRC)) { if(uploadContentCRC == syncContentCRC && (syncTitleCRC === undefined || uploadTitleCRC == syncTitleCRC)) {
locationUp(callback); locationUp(callback);
return; return;
@ -74,17 +77,15 @@ define(["jquery", "google-helper", "dropbox-helper"], function($, googleHelper,
// Try to find the provider // Try to find the provider
if (fileSyncIndex.indexOf(SYNC_PROVIDER_GDRIVE) === 0) { if (fileSyncIndex.indexOf(SYNC_PROVIDER_GDRIVE) === 0) {
var id = fileSyncIndex.substring(SYNC_PROVIDER_GDRIVE.length); var id = fileSyncIndex.substring(SYNC_PROVIDER_GDRIVE.length);
googleHelper.upload(id, undefined, uploadTitle, uploadContent, function(result) { googleHelper.upload(id, undefined, uploadTitle, uploadContent, function(error, result) {
if (result !== undefined) { if(error) {
// If error we abort the synchronization (retry later)
callback(error);
return;
}
localStorage[fileSyncIndex + ".contentCRC"] = uploadContentCRC; localStorage[fileSyncIndex + ".contentCRC"] = uploadContentCRC;
localStorage[fileSyncIndex + ".titleCRC"] = uploadTitleCRC; localStorage[fileSyncIndex + ".titleCRC"] = uploadTitleCRC;
locationUp(callback); locationUp(callback);
return;
}
// If error we abort the synchronization (retry later)
callback("abort");
return;
}); });
} else if (fileSyncIndex.indexOf(SYNC_PROVIDER_DROPBOX) === 0) { } else if (fileSyncIndex.indexOf(SYNC_PROVIDER_DROPBOX) === 0) {
var path = fileSyncIndex.substring(SYNC_PROVIDER_DROPBOX.length); var path = fileSyncIndex.substring(SYNC_PROVIDER_DROPBOX.length);
@ -130,7 +131,7 @@ define(["jquery", "google-helper", "dropbox-helper"], function($, googleHelper,
uploadTitleCRC = core.crc32(uploadTitle); uploadTitleCRC = core.crc32(uploadTitle);
// Parse the list of synchronized locations associated to the document // Parse the list of synchronized locations associated to the document
uploadFileSyncIndexList = fileSyncIndexes.split(";"); uploadFileSyncIndexList = _.compact(fileSyncIndexes.split(";"));
locationUp(callback); locationUp(callback);
} }
@ -156,14 +157,14 @@ define(["jquery", "google-helper", "dropbox-helper"], function($, googleHelper,
} }
var lastChangeId = parseInt(localStorage[SYNC_PROVIDER_GDRIVE var lastChangeId = parseInt(localStorage[SYNC_PROVIDER_GDRIVE
+ "lastChangeId"]); + "lastChangeId"]);
googleHelper.checkUpdates(lastChangeId, function(changes, newChangeId) { googleHelper.checkUpdates(lastChangeId, function(error, changes, newChangeId) {
if (changes === undefined) { if (error) {
callback("error"); callback(error);
return; return;
} }
googleHelper.downloadContent(changes, function(changes) { googleHelper.downloadContent(changes, function(error, changes) {
if (changes === undefined) { if (error) {
callback("error"); callback(error);
return; return;
} }
var updateFileTitles = false; var updateFileTitles = false;
@ -336,10 +337,83 @@ define(["jquery", "google-helper", "dropbox-helper"], function($, googleHelper,
}); });
}; };
// Used to populate the "Manage synchronization" dialog
var lineTemplate = ['<div class="input-prepend input-append">',
'<span class="add-on" title="<%= provider.providerName %>">',
'<i class="icon-<%= provider.providerId %>"></i></span>',
'<input class="span5" type="text" value="<%= syncDesc %>" disabled />',
'</div>'].join("");
var removeButtonTemplate = '<a class="btn" title="Remove this location"><i class="icon-trash"></i></a>';
synchronizer.refreshManageSync = function() {
var fileIndex = fileManager.getCurrentFileIndex();
var syncIndexList = _.compact(localStorage[fileIndex + ".sync"].split(";"));
$(".msg-no-sync, .msg-sync-list").addClass("hide");
$("#manage-sync-list .input-append").remove();
if (syncIndexList.length > 0) {
$(".msg-sync-list").removeClass("hide");
} else {
$(".msg-no-sync").removeClass("hide");
}
_.each(syncIndexList, function(syncIndex) {
var syncAttributes = JSON.parse(localStorage[syncIndex]);
var syncDesc = syncAttributes.id || syncAttributes.path;
lineElement = $(_.template(lineTemplate, {
provider: providerMap[syncAttributes.provider],
syncDesc: syncDesc
}));
lineElement.append($(removeButtonTemplate).click(function() {
fileManager.removeSync(syncIndex);
fileManager.updateFileTitles();
}));
$("#manage-sync-list").append(lineElement);
});
};
synchronizer.init = function(coreModule, fileManagerModule) { synchronizer.init = function(coreModule, fileManagerModule) {
core = coreModule; core = coreModule;
fileManager = fileManagerModule; fileManager = fileManagerModule;
// Init each provider
_.each(providerMap, function(provider) {
provider.init(core, fileManager);
// Provider's import button
$(".action-sync-import-" + provider.providerId).click(function(event) {
provider.importFiles(event);
});
// Provider's export button
$(".action-sync-export-" + provider.providerId).click(function(event) {
var fileIndex = fileManager.getCurrentFileIndex();
var title = localStorage[fileIndex + ".title"];
var content = localStorage[fileIndex + ".content"];
provider.exportFile(event, title, content, function(error, syncIndex) {
if(error) {
return;
}
localStorage[fileIndex + ".sync"] += syncIndex + ";";
synchronizer.refreshManageSync();
fileManager.updateFileTitles();
core.showMessage('"' + title
+ '" will now be synchronized on ' + provider.providerName + '.');
});
});
// Provider's manual sync button
$(".action-sync-manual-" + provider.providerId).click(function(event) {
var fileIndex = fileManager.getCurrentFileIndex();
var title = localStorage[fileIndex + ".title"];
var content = localStorage[fileIndex + ".content"];
provider.exportManual(event, title, content, function(error, syncIndex) {
if(error) {
return;
}
localStorage[fileIndex + ".sync"] += syncIndex + ";";
synchronizer.refreshManageSync();
fileManager.updateFileTitles();
core.showMessage('"' + title
+ '" will now be synchronized on ' + provider.providerName + '.');
});
});
});
synchronizer.updateSyncButton(); synchronizer.updateSyncButton();
$(".action-force-sync").click(function() { $(".action-force-sync").click(function() {
if(!$(this).hasClass("disabled")) { if(!$(this).hasClass("disabled")) {