Added GitHub publishing

This commit is contained in:
benweet 2013-04-11 23:38:41 +01:00
commit 4ec862928b
13 changed files with 517 additions and 309 deletions

View File

@ -1,11 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<script src="lib/dropbox.js"></script> <script src="lib/dropbox.js"></script>
<script type="text/javascript"> <script type="text/javascript">
Dropbox.Drivers.Popup.oauthReceiver(); Dropbox.Drivers.Popup.oauthReceiver();
</script> </script>
</head> </head>
<body> <body>
</body> </body>
</html> </html>

24
github-oauth-client.html Normal file
View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script type="text/javascript">
function getParameter(name) {
var regex = new RegExp(name + "=(.+?)(&|$)");
try {
return decodeURI(regex.exec(location.search)[1]);
} catch (e) {
return undefined;
}
}
var client_id = getParameter("client_id");
var code = getParameter("code");
if (code) {
localStorage["githubCode"] = code;
window.close();
} else if (client_id) {
window.location.href = "https://github.com/login/oauth/authorize?client_id="
+ client_id + "&scope=repo";
}
</script>
</head>
</html>

View File

@ -79,17 +79,14 @@
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<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-blogger"><i
class="icon-blogger"></i> Blogger</a></li>
</ul></li> </ul></li>
<li><a href="#" data-toggle="modal" <li><a href="#" data-toggle="modal"
data-target="#modal-manage-publication" data-target="#modal-manage-publish" class="action-reset-input"><i
class="action-reset-input"><i class="icon-share"></i> Manage class="icon-share"></i> Manage publishing</a></li>
publishing</a></li>
<li class="divider"></li> <li class="divider"></li>
<li><a href="#" data-toggle="modal" <li><a href="#" data-toggle="modal"
data-target="#modal-settings" data-target="#modal-settings"
class="action-load-settings action-reset-input"><i class="action-load-settings"><i
class="icon-cog"></i> Settings</a></li> class="icon-cog"></i> Settings</a></li>
<li><a href="#" data-toggle="modal" <li><a href="#" data-toggle="modal"
data-target="#modal-about"><i class="icon-question-sign"></i> data-target="#modal-about"><i class="icon-question-sign"></i>
@ -236,17 +233,17 @@
</p> </p>
<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"><i class="icon-gdrive"></i></span><input <span class="add-on" title="Google Drive"><i
id="manual-gdrive-fileid" type="text" class="span5" class="icon-gdrive"></i></span><input id="manual-gdrive-fileid"
placeholder="Google Drive file ID"></input> <a type="text" class="span5" placeholder="Google Drive file ID"></input>
class="btn action-manual-gdrive" title="Add this location" <a class="btn action-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"><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="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 this location" class="btn action-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
@ -266,6 +263,36 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-horizontal"> <div class="form-horizontal">
<div class="control-group control-publish-github">
<label class="control-label" for="input-publish-github-username">GitHub
user</label>
<div class="controls">
<input type="text" id="input-publish-github-username"
placeholder="User name">
</div>
</div>
<div class="control-group control-publish-github">
<label class="control-label" for="input-publish-github-reponame">Repository</label>
<div class="controls">
<input type="text" id="input-publish-github-reponame"
placeholder="Repository name">
</div>
</div>
<div class="control-group control-publish-github">
<label class="control-label" for="input-publish-github-branch">Branch</label>
<div class="controls">
<input type="text" id="input-publish-github-branch"
placeholder="Branch name">
</div>
</div>
<div class="control-group control-publish-github">
<label class="control-label" for="input-publish-github-path">File
path</label>
<div class="controls">
<input type="text" id="input-publish-github-path"
placeholder="path/to/file.md">
</div>
</div>
<div class="control-group control-publish-blogger"> <div class="control-group control-publish-blogger">
<label class="control-label" for="input-publish-blogger-url">Blog <label class="control-label" for="input-publish-blogger-url">Blog
URL</label> URL</label>
@ -301,6 +328,28 @@
</div> </div>
</div> </div>
<div id="modal-manage-publish" class="modal hide">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-hidden="true">&times;</button>
<h3>Publishing</h3>
</div>
<div class="modal-body">
<p class="msg-publish-list hide">"<span class="file-title"></span>"
is published on the following location(s):
</p>
<div id="manage-publish-list"></div>
<p class="msg-no-publish hide">"<span class="file-title"></span>"
is not published.
</p>
<p class="muted"><b>NOTE:</b> You can add locations using
sub-menu "Publish on".</p>
</div>
<div class="modal-footer">
<a href="#" class="btn btn-primary" data-dismiss="modal">Close</a>
</div>
</div>
<div id="modal-settings" class="modal hide"> <div id="modal-settings" class="modal hide">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" <button type="button" class="close" data-dismiss="modal"
@ -308,28 +357,50 @@
<h3>Settings</h3> <h3>Settings</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-horizontal"> <ul class="nav nav-tabs">
<div class="control-group"> <li class="active"><a class="action-load-settings" href="#tabpane-settings-editor"
<div class="control-label">Layout orientation</div> data-toggle="tab">Editor</a></li>
<div class="controls"> <li><a class="action-load-settings" href="#tabpane-settings-publish" data-toggle="tab">Publishing</a></li>
<label class="radio"> <input type="radio" </ul>
name="radio-layout-orientation" value="horizontal">
Horizontal <div class="tab-content">
</label> <label class="radio"> <input type="radio" <div class="tab-pane active" id="tabpane-settings-editor">
name="radio-layout-orientation" value="vertical"> <div class="form-horizontal">
Vertical <div class="control-group">
</label> <div class="control-label">Layout orientation</div>
<div class="controls">
<label class="radio"> <input type="radio"
name="radio-layout-orientation" value="horizontal">
Horizontal
</label> <label class="radio"> <input type="radio"
name="radio-layout-orientation" value="vertical">
Vertical
</label>
</div>
</div>
<div class="control-group">
<label class="control-label"
for="input-settings-editor-font-size">Editor font size</label>
<div class="controls">
<input type="text" id="input-settings-editor-font-size"
class="input-mini"><span class="help-inline">px</span>
</div>
</div>
</div> </div>
</div> </div>
<div class="control-group"> <div class="tab-pane" id="tabpane-settings-publish">
<label class="control-label" for="input-editor-font-size">Editor <div class="form-horizontal">
font size</label> <div class="control-group">
<div class="controls"> <label class="control-label"
<input type="text" id="input-editor-font-size" class="input-mini"><span for="input-settings-publish-commit-msg">Commit message</label>
class="help-inline">px</span> <div class="controls">
<input type="text" id="input-settings-publish-commit-msg">
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</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="#"
@ -381,6 +452,13 @@
<dd> <dd>
<a target="_blank" href="http://twitter.github.com/bootstrap/">Bootstrap</a> <a target="_blank" href="http://twitter.github.com/bootstrap/">Bootstrap</a>
</dd> </dd>
<dd>
<a target="_blank" href="https://github.com/dropbox/dropbox-js">Dropbox-js</a>
</dd>
<dd>
<a target="_blank" href="https://github.com/michael/github">Github.js</a>
/ <a target="_blank" href="https://github.com/prose/gatekeeper">Gatekeeper</a>
</dd>
<dd> <dd>
<a target="_blank" href="http://glyphicons.com/">Glyphicons</a> <a target="_blank" href="http://glyphicons.com/">Glyphicons</a>
</dd> </dd>

View File

@ -14,6 +14,8 @@ 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 PUBLISH_PROVIDER_GITHUB = "github";
var PUBLISH_PROVIDER_BLOGGER = "blogger";
// Use by Google's client.js // Use by Google's client.js
var delayedFunction = undefined; var delayedFunction = undefined;

View File

@ -7,6 +7,9 @@ define(
var core = {}; var core = {};
// The interval Id for periodic tasks
var intervalId = undefined;
// Usage: callback = callback || core.doNothing; // Usage: callback = callback || core.doNothing;
core.doNothing = function() {}; core.doNothing = function() {};
@ -41,12 +44,15 @@ define(
return; return;
} }
if(windowId === undefined) { if(windowId === undefined) {
windowId = Math.random().toString(36); windowId = core.randomString();
localStorage["frontWindowId"] = windowId; localStorage["frontWindowId"] = windowId;
} }
var frontWindowId = localStorage["frontWindowId"]; var frontWindowId = localStorage["frontWindowId"];
if(frontWindowId != windowId) { if(frontWindowId != windowId) {
windowUnique = false; windowUnique = false;
if(intervalId !== undefined) {
clearInterval(intervalId);
}
$(".modal").modal("hide"); $(".modal").modal("hide");
$('#modal-non-unique').modal({ $('#modal-non-unique').modal({
backdrop: "static", backdrop: "static",
@ -166,22 +172,26 @@ define(
} }
// Setting management // Setting management
var settings = { core.settings = {
layoutOrientation : "horizontal", layoutOrientation : "horizontal",
editorFontSize : 14 editorFontSize : 14,
commitMsg : "Published by StackEdit."
}; };
core.loadSettings = function() { core.loadSettings = function() {
if (localStorage.settings) { if (localStorage.settings) {
$.extend(settings, JSON.parse(localStorage.settings)); $.extend(core.settings, JSON.parse(localStorage.settings));
} }
// Layout orientation // Layout orientation
$("input:radio[name=radio-layout-orientation][value=" $("input:radio[name=radio-layout-orientation][value="
+ settings.layoutOrientation + "]").prop("checked", true); + core.settings.layoutOrientation + "]").prop("checked", true);
// Editor font size // Editor font size
$("#input-editor-font-size").val(settings.editorFontSize); $("#input-settings-editor-font-size").val(core.settings.editorFontSize);
// Commit message
$("#input-settings-publish-commit-msg").val(core.settings.commitMsg);
}; };
core.saveSettings = function(event) { core.saveSettings = function(event) {
@ -192,10 +202,13 @@ define(
"input:radio[name=radio-layout-orientation]:checked").prop("value"); "input:radio[name=radio-layout-orientation]:checked").prop("value");
// Editor font size // Editor font size
newSettings.editorFontSize = core.getInputIntValue($("#input-editor-font-size"), event, 1, 99); newSettings.editorFontSize = core.getInputIntValue($("#input-settings-editor-font-size"), event, 1, 99);
// Commit message
newSettings.commitMsg = core.getInputValue($("#input-settings-publish-commit-msg"), event);
if(!event.isPropagationStopped()) { if(!event.isPropagationStopped()) {
settings = newSettings; core.settings = newSettings;
localStorage.settings = JSON.stringify(newSettings); localStorage.settings = JSON.stringify(newSettings);
} }
}; };
@ -215,7 +228,7 @@ define(
togglerLength_closed : 90, togglerLength_closed : 90,
stateManagement__enabled : false stateManagement__enabled : false
}; };
if (settings.layoutOrientation == "horizontal") { if (core.settings.layoutOrientation == "horizontal") {
$(".ui-layout-south").remove(); $(".ui-layout-south").remove();
$(".ui-layout-east").addClass("well").prop("id", "wmd-preview"); $(".ui-layout-east").addClass("well").prop("id", "wmd-preview");
layout = $('body').layout( layout = $('body').layout(
@ -225,7 +238,7 @@ define(
east__minSize : 200 east__minSize : 200
}) })
); );
} else if (settings.layoutOrientation == "vertical") { } else if (core.settings.layoutOrientation == "vertical") {
$(".ui-layout-east").remove(); $(".ui-layout-east").remove();
$(".ui-layout-south").addClass("well").prop("id", "wmd-preview"); $(".ui-layout-south").addClass("well").prop("id", "wmd-preview");
layout = $('body').layout( layout = $('body').layout(
@ -413,10 +426,14 @@ define(
return crc.toString(16); return crc.toString(16);
}; };
// Generates a random string
core.randomString = function() {
return Math.ceil(Math.random() * 4294967296).toString(36);
};
// Used to setup an empty localStorage // Used to setup an empty localStorage
function setupLocalStorage() { function setupLocalStorage() {
if (localStorage["file.counter"] === undefined) { if (localStorage["file.list"] === undefined) {
localStorage["file.counter"] = "0";
localStorage["file.list"] = ";"; localStorage["file.list"] = ";";
localStorage["version"] = "v1"; localStorage["version"] = "v1";
} }
@ -429,9 +446,10 @@ define(
// from v0 to v1 // from v0 to v1
if(version === undefined) { if(version === undefined) {
// Synchronization queue not used anymore // Not used anymore
localStorage.removeItem("sync.queue"); localStorage.removeItem("sync.queue");
localStorage.removeItem("sync.current"); localStorage.removeItem("sync.current");
localStorage.removeItem("file.counter");
var fileIndexList = localStorage["file.list"].split(";"); var fileIndexList = localStorage["file.list"].split(";");
for ( var i = 1; i < fileIndexList.length - 1; i++) { for ( var i = 1; i < fileIndexList.length - 1; i++) {
@ -452,7 +470,7 @@ define(
localStorage["version"] = version; localStorage["version"] = version;
} }
// Create an centered popup window
core.popupWindow = function(url, title, w, h) { core.popupWindow = function(url, title, w, h) {
var left = (screen.width / 2) - (w / 2); var left = (screen.width / 2) - (w / 2);
var top = (screen.height / 2) - (h / 2); var top = (screen.height / 2) - (h / 2);
@ -513,13 +531,13 @@ define(
}); });
$("#menu-bar, .ui-layout-center, .ui-layout-east, .ui-layout-south").removeClass("hide"); $("#menu-bar, .ui-layout-center, .ui-layout-east, .ui-layout-south").removeClass("hide");
this.loadSettings(); core.loadSettings();
this.createLayout(); core.createLayout();
// Apply editor font size // Apply editor font size
$("#wmd-input").css({ $("#wmd-input").css({
"font-size": settings.editorFontSize + "px", "font-size": core.settings.editorFontSize + "px",
"line-height": Math.round(settings.editorFontSize * (20/14)) + "px" "line-height": Math.round(core.settings.editorFontSize * (20/14)) + "px"
}); });
$(".action-load-settings").click(function() { $(".action-load-settings").click(function() {
@ -558,7 +576,7 @@ define(
fileManager.init(core); fileManager.init(core);
// Do periodic tasks // Do periodic tasks
window.setInterval(function() { intervalId = window.setInterval(function() {
updateCurrentTime(); updateCurrentTime();
core.checkWindowUnique(); core.checkWindowUnique();
if(isUserActive() === false) { if(isUserActive() === false) {

View File

@ -1,2 +1,5 @@
var BASE_URL = "http://benweet.github.io/stackedit/";
var GOOGLE_KEY = "AIzaSyB1Bc1wI_YUWkkOR-5Gri5BFuypgZl0Sxc"; var GOOGLE_KEY = "AIzaSyB1Bc1wI_YUWkkOR-5Gri5BFuypgZl0Sxc";
var GOOGLE_CLIENT_ID = '241271498917-jpto9lls9fqnem1e4h6ppds9uob8rpvu.apps.googleusercontent.com'; var GOOGLE_CLIENT_ID = '241271498917-jpto9lls9fqnem1e4h6ppds9uob8rpvu.apps.googleusercontent.com';
var GITHUB_CLIENT_ID = 'fa0d09514da8377ee32e';
var GATEKEEPER_URL = "http://stackedit-gatekeeper.herokuapp.com/";

5
js/custo.js Normal file
View File

@ -0,0 +1,5 @@
var BASE_URL = "http://localhost/";
var GOOGLE_KEY = "AIzaSyAeCU8CGcSkn0z9js6iocHuPBX4f_mMWkw";
var GOOGLE_CLIENT_ID = '241271498917-lev37kef013q85avc91am1gccg5g8lrb.apps.googleusercontent.com';
var GITHUB_CLIENT_ID = 'e47fef6055344579799d';
var GATEKEEPER_URL = "http://stackedit-gatekeeper-localhost.herokuapp.com/";

View File

@ -39,7 +39,7 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
secret: DROPBOX_APP_SECRET secret: DROPBOX_APP_SECRET
}); });
client.authDriver(new Dropbox.Drivers.Popup({ client.authDriver(new Dropbox.Drivers.Popup({
receiverUrl: "http://localhost/dropbox-oauth-receiver.html", receiverUrl: BASE_URL + "dropbox-oauth-receiver.html",
rememberUser: true rememberUser: true
})); }));
callback(); callback();
@ -125,6 +125,9 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
asyncTask.onSuccess = function() { asyncTask.onSuccess = function() {
callback(fileSyncIndex); callback(fileSyncIndex);
}; };
asyncTask.onError = function() {
callback();
};
asyncTaskRunner.addTask(asyncTask); asyncTaskRunner.addTask(asyncTask);
}); });
}; };
@ -172,6 +175,9 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
callback(changes, newChangeId); callback(changes, newChangeId);
} }
}; };
asyncTask.onError = function() {
callback();
};
asyncTaskRunner.addTask(asyncTask); asyncTaskRunner.addTask(asyncTask);
} }
retrievePageOfChanges(newChangeId); retrievePageOfChanges(newChangeId);
@ -207,6 +213,9 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
asyncTask.onSuccess = function() { asyncTask.onSuccess = function() {
dropboxHelper.downloadMetadata(paths, callback, result); dropboxHelper.downloadMetadata(paths, callback, result);
}; };
asyncTask.onError = function() {
callback();
};
asyncTaskRunner.addTask(asyncTask); asyncTaskRunner.addTask(asyncTask);
}); });
}; };
@ -255,6 +264,9 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
asyncTask.onSuccess = function() { asyncTask.onSuccess = function() {
dropboxHelper.downloadContent(objects, callback, result); dropboxHelper.downloadContent(objects, callback, result);
}; };
asyncTask.onError = function() {
callback();
};
asyncTaskRunner.addTask(asyncTask); asyncTaskRunner.addTask(asyncTask);
}); });
}; };

View File

@ -1,5 +1,5 @@
define(["jquery", "google-helper", "dropbox-helper", "synchronizer", "publisher"], define(["jquery", "google-helper", "dropbox-helper", "github-helper", "synchronizer", "publisher"],
function($, googleHelper, dropboxHelper, synchronizer, publisher) { function($, googleHelper, dropboxHelper, githubHelper, synchronizer, publisher) {
var fileManager = {}; var fileManager = {};
@ -23,6 +23,7 @@ define(["jquery", "google-helper", "dropbox-helper", "synchronizer", "publisher"
// Update the file titles // Update the file titles
fileManager.updateFileTitles(); fileManager.updateFileTitles();
refreshManageSync(); refreshManageSync();
refreshManagePublish();
publisher.notifyCurrentFile(localStorage["file.current"]); publisher.notifyCurrentFile(localStorage["file.current"]);
// Recreate the editor // Recreate the editor
@ -51,9 +52,13 @@ define(["jquery", "google-helper", "dropbox-helper", "synchronizer", "publisher"
title = DEFAULT_FILE_TITLE + indicator++; title = DEFAULT_FILE_TITLE + indicator++;
} }
} }
// Create the fileIndex
var fileCounter = parseInt(localStorage["file.counter"]); // Generate a unique fileIndex
var fileIndex = "file." + fileCounter; var fileIndex = undefined;
do {
fileIndex = "file." + core.randomString();
} while(localStorage[fileIndex + ".title"] !== undefined);
// Create the file in the localStorage // Create the file in the localStorage
localStorage[fileIndex + ".content"] = content; localStorage[fileIndex + ".content"] = content;
localStorage[fileIndex + ".title"] = title; localStorage[fileIndex + ".title"] = title;
@ -63,7 +68,6 @@ define(["jquery", "google-helper", "dropbox-helper", "synchronizer", "publisher"
} }
localStorage[fileIndex + ".sync"] = sync; localStorage[fileIndex + ".sync"] = sync;
localStorage[fileIndex + ".publish"] = ";"; localStorage[fileIndex + ".publish"] = ";";
localStorage["file.counter"] = fileCounter + 1;
localStorage["file.list"] += fileIndex + ";"; localStorage["file.list"] += fileIndex + ";";
return fileIndex; return fileIndex;
}; };
@ -85,6 +89,14 @@ define(["jquery", "google-helper", "dropbox-helper", "synchronizer", "publisher"
} }
localStorage.removeItem(fileIndex + ".sync"); localStorage.removeItem(fileIndex + ".sync");
// Remove publish locations
var publishIndexList = localStorage[fileIndex + ".publish"].split(";");
for ( var i = 1; i < publishIndexList.length - 1; i++) {
var publishIndex = publishIndexList[i];
fileManager.removePublish(publishIndex);
}
localStorage.removeItem(fileIndex + ".sync");
localStorage["file.list"] = localStorage["file.list"].replace(";" localStorage["file.list"] = localStorage["file.list"].replace(";"
+ fileIndex + ";", ";"); + fileIndex + ";", ";");
localStorage.removeItem(fileIndex + ".title"); localStorage.removeItem(fileIndex + ".title");
@ -191,17 +203,44 @@ define(["jquery", "google-helper", "dropbox-helper", "synchronizer", "publisher"
// Look for local file associated to a synchronized location // Look for local file associated to a synchronized location
fileManager.getFileIndexFromSync = function(fileSyncIndex) { fileManager.getFileIndexFromSync = function(fileSyncIndex) {
var fileIndex = undefined;
var fileIndexList = localStorage["file.list"].split(";"); var fileIndexList = localStorage["file.list"].split(";");
for ( var i = 1; i < fileIndexList.length - 1; i++) { for ( var i = 1; i < fileIndexList.length - 1; i++) {
var tempFileIndex = fileIndexList[i]; var fileIndex = fileIndexList[i];
var sync = localStorage[tempFileIndex + ".sync"]; var sync = localStorage[fileIndex + ".sync"];
if (sync.indexOf(";" + fileSyncIndex + ";") !== -1) { if (sync.indexOf(";" + fileSyncIndex + ";") !== -1) {
fileIndex = tempFileIndex; return fileIndex;
break;
} }
} }
return fileIndex; return undefined;
};
// Remove a publish location
fileManager.removePublish = function(publishIndex) {
var fileIndexCurrent = localStorage["file.current"];
var fileIndex = this.getFileIndexFromPublish(publishIndex);
if(fileIndex !== undefined) {
localStorage[fileIndex + ".publish"] = localStorage[fileIndex + ".publish"].replace(";"
+ publishIndex + ";", ";");
if(fileIndex == fileIndexCurrent) {
refreshManagePublish();
}
}
// Remove publish object
localStorage.removeItem(publishIndex);
publisher.notifyCurrentFile(localStorage["file.current"]);
};
// Look for local file associated to a publish location
fileManager.getFileIndexFromPublish = function(publishIndex) {
var fileIndexList = localStorage["file.list"].split(";");
for ( var i = 1; i < fileIndexList.length - 1; i++) {
var fileIndex = fileIndexList[i];
var publish = localStorage[fileIndex + ".publish"];
if (publish.indexOf(";" + publishIndex + ";") !== -1) {
return fileIndex;
}
}
return undefined;
}; };
function uploadGdrive(fileId, folderId) { function uploadGdrive(fileId, folderId) {
@ -325,14 +364,14 @@ define(["jquery", "google-helper", "dropbox-helper", "synchronizer", "publisher"
(function(fileSyncIndex) { (function(fileSyncIndex) {
var line = $("<div>").addClass("input-prepend input-append"); var line = $("<div>").addClass("input-prepend input-append");
if (fileSyncIndex.indexOf(SYNC_PROVIDER_GDRIVE) === 0) { if (fileSyncIndex.indexOf(SYNC_PROVIDER_GDRIVE) === 0) {
line.append($("<span>").addClass("add-on").html( line.append($("<span>").addClass("add-on").prop("title", "Google Drive").html(
'<i class="icon-gdrive"></i>')); '<i class="icon-gdrive"></i>'));
line.append($("<input>").prop("type", "text").prop( line.append($("<input>").prop("type", "text").prop(
"disabled", true).addClass("span5").val( "disabled", true).addClass("span5").val(
fileSyncIndex.substring(SYNC_PROVIDER_GDRIVE.length))); fileSyncIndex.substring(SYNC_PROVIDER_GDRIVE.length)));
} }
if (fileSyncIndex.indexOf(SYNC_PROVIDER_DROPBOX) === 0) { else if (fileSyncIndex.indexOf(SYNC_PROVIDER_DROPBOX) === 0) {
line.append($("<span>").addClass("add-on").html( line.append($("<span>").addClass("add-on").prop("title", "Dropbox").html(
'<i class="icon-dropbox"></i>')); '<i class="icon-dropbox"></i>'));
line.append($("<input>").prop("type", "text").prop( line.append($("<input>").prop("type", "text").prop(
"disabled", true).addClass("span5").val( "disabled", true).addClass("span5").val(
@ -349,6 +388,44 @@ define(["jquery", "google-helper", "dropbox-helper", "synchronizer", "publisher"
} }
} }
function refreshManagePublish() {
var fileIndex = localStorage["file.current"];
var publishIndexList = localStorage[fileIndex + ".publish"].split(";");
$(".msg-no-publish, .msg-publish-list").addClass("hide");
$("#manage-publish-list .input-append").remove();
if (publishIndexList.length > 2) {
$(".msg-publish-list").removeClass("hide");
} else {
$(".msg-no-publish").removeClass("hide");
}
for ( var i = 1; i < publishIndexList.length - 1; i++) {
var publishIndex = publishIndexList[i];
var serializedObject = localStorage[publishIndex];
(function(publishIndex, publishObject, serializedObject) {
var line = $("<div>").addClass("input-prepend input-append");
if (publishObject.provider == PUBLISH_PROVIDER_GITHUB) {
line.append($("<span>").addClass("add-on").prop("title", "GitHub").html(
'<i class="icon-github"></i>'));
line.append($("<input>").prop("type", "text").prop(
"disabled", true).addClass("span5").val(
serializedObject));
}
else if (publishObject.provider == PUBLISH_PROVIDER_BLOGGER) {
line.append($("<span>").addClass("add-on").prop("title", "Blogger").html(
'<i class="icon-blogger"></i>'));
line.append($("<input>").prop("type", "text").prop(
"disabled", true).addClass("span5").val(
serializedObject));
}
line.append($("<a>").addClass("btn").html(
'<i class="icon-trash"></i>').prop("title",
"Remove this location").click(function() {
fileManager.removePublish(publishIndex);
}));
$("#manage-publish-list").append(line);
})(publishIndex, JSON.parse(serializedObject), serializedObject.replace(/{|}|"/g, ""));
}
}
// Initialize the "New publication" dialog // Initialize the "New publication" dialog
var newPublishProvider = undefined; var newPublishProvider = undefined;
@ -367,7 +444,46 @@ define(["jquery", "google-helper", "dropbox-helper", "synchronizer", "publisher"
$("#modal-publish").modal(); $("#modal-publish").modal();
} }
// Create a new publication // Generate a publishIndex, store a publishObject and associate it to a fileIndex
function createPublishIndex(publishObject, fileIndex) {
var publishIndex = undefined;
do {
publishIndex = "publish." + core.randomString();
} while(localStorage[publishIndex] !== undefined);
localStorage[publishIndex] = JSON.stringify(publishObject);
localStorage[fileIndex + ".publish"] += publishIndex + ";";
}
// Create a new publication on GitHub
function newPublishGithub(event) {
var publishObject = {};
publishObject.username = core.getInputValue($("#input-publish-github-username"), event);
publishObject.repository = core.getInputValue($("#input-publish-github-reponame"), event);
publishObject.branch = core.getInputValue($("#input-publish-github-branch"), event);
publishObject.path = core.getInputValue($("#input-publish-github-path"), event);
publishObject.provider = newPublishProvider;
if(event.isPropagationStopped()) {
return;
}
var fileIndex = localStorage["file.current"];
var title = localStorage[fileIndex + ".title"];
var content = publisher.getPublishContent(publishObject);
var commitMsg = core.settings.commitMsg;
githubHelper.upload(publishObject.username, publishObject.repository,
publishObject.branch, publishObject.path, content, commitMsg,
function(error) {
if(error === undefined) {
createPublishIndex(publishObject, fileIndex);
refreshManagePublish();
publisher.notifyCurrentFile(localStorage["file.current"]);
core.showMessage('"' + title
+ '" will now be published on GitHub.');
}
});
}
// Create a new publication on Blogger
function newPublishBlogger(event) { function newPublishBlogger(event) {
var blogUrl = core.getInputValue($("#input-publish-blogger-url"), event); var blogUrl = core.getInputValue($("#input-publish-blogger-url"), event);
if(event.isPropagationStopped()) { if(event.isPropagationStopped()) {
@ -460,13 +576,16 @@ define(["jquery", "google-helper", "dropbox-helper", "synchronizer", "publisher"
// Publish actions // Publish actions
$(".action-publish-github").click(function() { $(".action-publish-github").click(function() {
initNewPublish("github"); initNewPublish(PUBLISH_PROVIDER_GITHUB);
}); });
$(".action-publish-blogger").click(function() { $(".action-publish-blogger").click(function() {
initNewPublish("blogger", "html"); initNewPublish(PUBLISH_PROVIDER_BLOGGER, "html");
}); });
$(".action-process-publish").click(function(e) { $(".action-process-publish").click(function(e) {
if(newPublishProvider == "blogger") { if(newPublishProvider == PUBLISH_PROVIDER_GITHUB) {
newPublishGithub(e);
}
else if(newPublishProvider == PUBLISH_PROVIDER_BLOGGER) {
newPublishBlogger(e); newPublishBlogger(e);
} }
}); });

View File

@ -2,10 +2,9 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
// Dependencies // Dependencies
var core = undefined; var core = undefined;
var fileManager = undefined;
var connected = undefined; var connected = undefined;
var client = undefined; var github = undefined;
var githubHelper = {}; var githubHelper = {};
@ -47,236 +46,129 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
// Try to authenticate with Oauth // Try to authenticate with Oauth
function authenticate(callback, immediate) { function authenticate(callback, immediate) {
callback = callback || core.doNothing; callback = callback || core.doNothing;
if (immediate === undefined) {
immediate = true;
}
connect(function() { connect(function() {
if (connected === false) { if (connected === false) {
callback(); callback();
return; return;
} }
var intervalId = undefined;
var authWindow = undefined;
var token = localStorage["githubToken"];
var asyncTask = {}; var asyncTask = {};
asyncTask.run = function() { asyncTask.run = function() {
if (client !== undefined) { if (github !== undefined) {
asyncTask.success(); asyncTask.success();
return; return;
} }
if (immediate !== false) {
if (token !== undefined) {
github = new Github({
token: token,
auth: "oauth"
});
asyncTask.success();
return;
} }
core.showMessage("Please make sure the Github authorization popup is not blocked by your browser."); if(immediate === true) {
myWindow=core.popupWindow('github-oauth-client.html?client_id=test','stackedit-github-oauth',500,400); core.showError("Unable to perform GitHub authenticate.");
myWindow.focus(); asyncTask.error();
};
asyncTask.onSuccess = function() {
callback();
};
asyncTask.onError = function() {
// If immediate did not work retry without immediate flag
if (connected === true && immediate === true) {
authenticate(callback, false);
return; return;
} }
callback(); // We add time for user to enter his credentials
}; asyncTask.timeout = AUTH_POPUP_TIMEOUT;
asyncTaskRunner.addTask(asyncTask); core.showMessage("Please make sure the Github authorization popup is not blocked by your browser.");
}); localStorage.removeItem("githubCode");
} authWindow = core.popupWindow(
'github-oauth-client.html?client_id=' + GITHUB_CLIENT_ID,
githubHelper.upload = function(path, content, callback) { 'stackedit-github-oauth', 960, 600);
callback = callback || core.doNothing; authWindow.focus();
authenticate(function() { intervalId = setInterval(function() {
if (client === undefined) { var code = localStorage["githubCode"];
callback(); if(code !== undefined) {
return; localStorage.removeItem("githubCode");
} $.getJSON(GATEKEEPER_URL + "authenticate/" + code, function(data) {
if(data.token !== undefined) {
var fileSyncIndex = undefined; localStorage["githubToken"] = data.token;
var asyncTask = {}; asyncTask.success();
asyncTask.run = function() {
client.writeFile(path, content, function(error, stat) {
if (!error) {
fileSyncIndex = SYNC_PROVIDER_GITHUB + encodeURIComponent(stat.path.toLowerCase());
localStorage[fileSyncIndex + ".version"] = stat.versionTag;
asyncTask.success();
return;
}
// Handle error
if(error.status === Github.ApiError.INVALID_PARAM) {
error = 'Could not upload document into path "' + path + '".';
}
handleError(error, asyncTask, callback);
});
};
asyncTask.onSuccess = function() {
callback(fileSyncIndex);
};
asyncTaskRunner.addTask(asyncTask);
});
};
githubHelper.checkUpdates = function(lastChangeId, callback) {
callback = callback || core.doNothing;
authenticate(function() {
if (client === undefined) {
callback();
return;
}
var changes = [];
var newChangeId = lastChangeId || 0;
function retrievePageOfChanges(changeId) {
var shouldPullAgain = false;
var asyncTask = {};
asyncTask.run = function() {
client.pullChanges(changeId, function(error, pullChanges) {
if (pullChanges && pullChanges.cursorTag) {
// Retrieve success
newChangeId = pullChanges.cursor();
shouldPullAgain = pullChanges.shouldPullAgain;
if(pullChanges.changes !== undefined) {
for(var i=0; i<pullChanges.changes.length; i++) {
var item = pullChanges.changes[i];
var version = localStorage[SYNC_PROVIDER_GITHUB
+ encodeURIComponent(item.path.toLowerCase()) + ".version"];
if(version && (item.wasRemoved || item.stat.versionTag != version)) {
changes.push(item);
}
}
} }
asyncTask.success(); else {
return; core.showError("Error retrieving GitHub Oauth token.");
} asyncTask.error();
// Handle error }
handleError(error, asyncTask, callback); });
});
};
asyncTask.onSuccess = function() {
if (shouldPullAgain === true) {
retrievePageOfChanges(newChangeId);
} else {
callback(changes, newChangeId);
} }
}; }, 500);
asyncTaskRunner.addTask(asyncTask);
}
retrievePageOfChanges(newChangeId);
});
};
githubHelper.downloadMetadata = function(paths, callback, result) {
callback = callback || core.doNothing;
result = result || [];
if(paths.length === 0) {
callback(result);
return;
}
authenticate(function() {
if (client === undefined) {
callback();
return;
}
var path = paths.pop();
var asyncTask = {};
asyncTask.run = function() {
client.stat(path, function(error, stat) {
if(stat) {
result.push(stat);
asyncTask.success();
return;
}
handleError(error, asyncTask, callback);
});
}; };
asyncTask.onSuccess = function() { asyncTask.onSuccess = function() {
githubHelper.downloadMetadata(paths, callback, result); if(intervalId !== undefined) {
clearInterval(intervalId);
}
if (github !== undefined) {
callback();
return;
}
authenticate(callback, true);
};
asyncTask.onError = function() {
if(intervalId !== undefined) {
clearInterval(intervalId);
}
if(authWindow !== undefined) {
authWindow.close();
}
callback();
}; };
asyncTaskRunner.addTask(asyncTask); asyncTaskRunner.addTask(asyncTask);
}); });
};
githubHelper.downloadContent = function(objects, callback, result) {
callback = callback || core.doNothing;
result = result || [];
if(objects.length === 0) {
callback(result);
return;
}
var object = objects.pop();
result.push(object);
var file = undefined;
// object may be a file
if(object.isFile === true) {
file = object;
}
// object may be a change
else if(object.wasRemoved !== undefined) {
file = object.stat;
}
if(!file) {
this.downloadContent(objects, callback, result);
return;
}
authenticate(function() {
if (client === undefined) {
callback();
return;
}
var asyncTask = {};
asyncTask.run = function() {
client.readFile(file.path, function(error, data) {
if(data) {
file.content = data;
asyncTask.success();
return;
}
handleError(error, asyncTask, callback);
});
};
asyncTask.onSuccess = function() {
githubHelper.downloadContent(objects, callback, result);
};
asyncTaskRunner.addTask(asyncTask);
});
};
function handleError(error, asyncTask, callback) {
var errorMsg = undefined;
asyncTask.onError = function() {
if (errorMsg !== undefined) {
core.showError(errorMsg);
}
callback();
};
if (error) {
// Try to analyze the error
if (typeof error === "string") {
errorMsg = error;
} else if (error.status === Github.ApiError.INVALID_TOKEN
|| error.status === Github.ApiError.OAUTH_ERROR) {
client = undefined;
errorMsg = "Access to Github is not authorized.";
} else if (error.status === Github.ApiError.NETWORK_ERROR) {
connected = false;
client = undefined;
core.setOffline();
} else {
errorMsg = "Github error ("
+ error.status + ").";
}
}
asyncTask.error();
} }
githubHelper.init = function(coreModule, fileManagerModule) { githubHelper.upload = function(username, reponame, branch, path, content, commitMsg, callback) {
callback = callback || core.doNothing;
authenticate(function() {
if (github === undefined) {
callback();
return;
}
var error = undefined;
var asyncTask = {};
asyncTask.run = function() {
var repo = github.getRepo(username, reponame);
repo.write(branch, path, content, commitMsg, function(err) {
if(!err) {
asyncTask.success();
return;
}
error = err.error;
asyncTask.error();
});
};
asyncTask.onSuccess = function() {
callback(error);
};
asyncTask.onError = function() {
if(error === 401) {
github = undefined;
// Token must be renewed
localStorage.removeItem("githubToken");
githubHelper.upload(username, reponame, branch, path, content, commitMsg, callback);
return;
}
if(error === 0) {
connected = false;
github = undefined;
core.setOffline();
}
core.showError("Could not publish on GitHub");
callback(error);
};
asyncTaskRunner.addTask(asyncTask);
});
};
githubHelper.init = function(coreModule) {
core = coreModule; core = coreModule;
fileManager = fileManagerModule;
authenticate();
}; };
return githubHelper; return githubHelper;

View File

@ -122,10 +122,18 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
} }
var path = '/upload/drive/v2/files'; var path = '/upload/drive/v2/files';
var method = 'POST'; var method = 'POST';
var etag = undefined;
if (fileId !== undefined) { if (fileId !== undefined) {
// If it's an update // If it's an update
path += "/" + fileId; path += "/" + fileId;
method = 'PUT'; method = 'PUT';
etag = localStorage[SYNC_PROVIDER_GDRIVE
+ fileId + ".etag"];
}
var headers = { 'Content-Type' : 'multipart/mixed; boundary="'
+ boundary + '"', };
if(etag !== undefined) {
headers["If-Match"] = etag;
} }
var base64Data = core.encodeBase64(content); var base64Data = core.encodeBase64(content);
@ -141,8 +149,8 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
'path' : path, 'path' : path,
'method' : method, 'method' : method,
'params' : { 'uploadType' : 'multipart', }, 'params' : { 'uploadType' : 'multipart', },
'headers' : { 'Content-Type' : 'multipart/mixed; boundary="' 'headers' : headers,
+ boundary + '"', }, 'body' : multipartRequestBody, }); 'body' : multipartRequestBody, });
request.execute(function(response) { request.execute(function(response) {
if (response && response.id) { if (response && response.id) {
// Upload success // Upload success
@ -154,7 +162,12 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
var error = response.error; var error = response.error;
// Handle error // Handle error
if(error !== undefined && fileId !== undefined && error.code === 404) { if(error !== undefined && fileId !== undefined && error.code === 404) {
error = 'File ID "' + fileId + '" does not exist on Google Drive.'; if(error.code === 404) {
error = 'File ID "' + fileId + '" does not exist on Google Drive.';
}
else if(error.code === 412) {
error = 'Conflict on file ID "' + fileId + '". Please restart the synchronization.';
}
} }
handleError(error, asyncTask, callback); handleError(error, asyncTask, callback);
}); });
@ -162,6 +175,9 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
asyncTask.onSuccess = function() { asyncTask.onSuccess = function() {
callback(fileSyncIndex); callback(fileSyncIndex);
}; };
asyncTask.onError = function() {
callback();
};
asyncTaskRunner.addTask(asyncTask); asyncTaskRunner.addTask(asyncTask);
}); });
}; };
@ -212,6 +228,9 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
callback(changes, newChangeId); callback(changes, newChangeId);
} }
}; };
asyncTask.onError = function() {
callback();
};
asyncTaskRunner.addTask(asyncTask); asyncTaskRunner.addTask(asyncTask);
} }
var initialRequest = gapi.client.drive.changes var initialRequest = gapi.client.drive.changes
@ -264,6 +283,9 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
asyncTask.onSuccess = function() { asyncTask.onSuccess = function() {
googleHelper.downloadMetadata(ids, callback, result); googleHelper.downloadMetadata(ids, callback, result);
}; };
asyncTask.onError = function() {
callback();
};
asyncTaskRunner.addTask(asyncTask); asyncTaskRunner.addTask(asyncTask);
}); });
}; };
@ -324,6 +346,9 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
asyncTask.onSuccess = function() { asyncTask.onSuccess = function() {
googleHelper.downloadContent(objects, callback, result); googleHelper.downloadContent(objects, callback, result);
}; };
asyncTask.onError = function() {
callback();
};
asyncTaskRunner.addTask(asyncTask); asyncTaskRunner.addTask(asyncTask);
}); });
}; };
@ -500,44 +525,8 @@ define(["jquery", "async-runner"], function($, asyncTaskRunner) {
asyncTask.onSuccess = function() { asyncTask.onSuccess = function() {
callback(result); callback(result);
}; };
asyncTaskRunner.addTask(asyncTask); asyncTask.onError = function() {
});
};
googleHelper.getBlogByUrl = function(url, callback) {
authenticate(function() {
if (connected === false) {
callback(); callback();
return;
}
var result = undefined;
var asyncTask = {};
asyncTask.run = function() {
var token = gapi.auth.getToken();
var headers = {
Authorization : token ? "Bearer " + token.access_token: null
};
$.ajax({
url : "https://www.googleapis.com/blogger/v3/blogs/byurl",
data: { url: url },
headers : headers,
dataType : "json",
timeout : AJAX_TIMEOUT
}).done(function(blog, textStatus, jqXHR) {
result = blog;
asyncTask.success();
}).fail(function(jqXHR) {
var error = {
code: jqXHR.status,
message: jqXHR.statusText
};
// Handle error
handleError(error, asyncTask, callback);
});
};
asyncTask.onSuccess = function() {
callback(result);
}; };
asyncTaskRunner.addTask(asyncTask); asyncTaskRunner.addTask(asyncTask);
}); });

View File

@ -32,11 +32,77 @@ define(["jquery", "google-helper", "github-helper"], function($, googleHelper, g
} }
}; };
// Used to get content to publish
publisher.getPublishContent = function(publishObject) {
if(publishObject.format === undefined) {
publishObject.format = $("input:radio[name=radio-publish-format]:checked").prop("value");
}
if(publishObject.format == "markdown") {
return $("#wmd-input").val();
}
return $("#wmd-preview").html();
};
// Recursive function to publish a file on multiple locations
var publishIndexList = [];
function publishLocation(callback, error) {
// No more publish location for this document
if (publishIndexList.length === 0) {
callback(error);
return;
}
// Dequeue a synchronized location
var publishIndex = publishIndexList.pop();
if(!publishIndex) {
publishLocation(callback, error);
return;
}
var publishObject = JSON.parse(localStorage[publishIndex]);
var content = publisher.getPublishContent(publishObject);
var commitMsg = core.settings.commitMsg;
// Try to find the provider
if(publishObject.provider == PUBLISH_PROVIDER_GITHUB) {
githubHelper.upload(publishObject.username, publishObject.repository,
publishObject.branch, publishObject.path, content, commitMsg,
function(error) {
publishLocation(callback, error);
});
}
}
var publishRunning = false; var publishRunning = false;
publisher.publish = function() {
// If publish is running or offline
if(publishRunning === true || core.isOffline) {
return;
}
publishRunning = true;
publisher.updatePublishButton();
var fileIndex = localStorage["file.current"];
var title = localStorage[fileIndex + ".title"];
publishIndexList = localStorage[fileIndex + ".publish"].split(";");;
publishLocation(function(error) {
publishRunning = false;
publisher.updatePublishButton();
if(error === undefined) {
core.showMessage('"' + title + '" successfully published.');
}
});
};
publisher.init = function(coreModule, fileManagerModule) { publisher.init = function(coreModule, fileManagerModule) {
core = coreModule; core = coreModule;
fileManager = fileManagerModule; fileManager = fileManagerModule;
$(".action-force-publish").click(function() {
if(!$(this).hasClass("disabled")) {
publisher.publish();
}
});
}; };
return publisher; return publisher;

View File

@ -187,9 +187,9 @@ define(["jquery", "google-helper", "dropbox-helper"], function($, googleHelper,
core.showMessage('"' + localTitle + '" has been removed from Google Drive.'); core.showMessage('"' + localTitle + '" has been removed from Google Drive.');
continue; continue;
} }
var localTitleChanged = localStorage[fileSyncIndex + ".titleCRC"] == core.crc32(localTitle); var localTitleChanged = localStorage[fileSyncIndex + ".titleCRC"] != core.crc32(localTitle);
var localContent = localStorage[fileIndex + ".content"]; var localContent = localStorage[fileIndex + ".content"];
var localContentChanged = localStorage[fileSyncIndex + ".contentCRC"] == core.crc32(localContent); var localContentChanged = localStorage[fileSyncIndex + ".contentCRC"] != core.crc32(localContent);
var file = change.file; var file = change.file;
var fileTitleChanged = localTitle != file.title; var fileTitleChanged = localTitle != file.title;
var fileContentChanged = localContent != file.content; var fileContentChanged = localContent != file.content;
@ -270,7 +270,7 @@ define(["jquery", "google-helper", "dropbox-helper"], function($, googleHelper,
continue; continue;
} }
var localContent = localStorage[fileIndex + ".content"]; var localContent = localStorage[fileIndex + ".content"];
var localContentChanged = localStorage[fileSyncIndex + ".contentCRC"] == core.crc32(localContent); var localContentChanged = localStorage[fileSyncIndex + ".contentCRC"] != core.crc32(localContent);
var file = change.stat; var file = change.stat;
var fileContentChanged = localContent != file.content; var fileContentChanged = localContent != file.content;
// Conflict detection // Conflict detection