CouchDB sync part 1

This commit is contained in:
benweet 2014-09-10 00:37:21 +01:00
parent 2be4919853
commit 8f5da7998f
22 changed files with 1686 additions and 745 deletions

View File

@ -7,7 +7,7 @@
"jquery": "2.0.3", "jquery": "2.0.3",
"underscore": "1.5.1", "underscore": "1.5.1",
"requirejs": "~2.1.11", "requirejs": "~2.1.11",
"require-css": "0.1.2", "require-css": "0.1.5",
"require-less": "0.1.2", "require-less": "0.1.2",
"mousetrap": "~1.4.4", "mousetrap": "~1.4.4",
"jgrowl": "~1.2.10", "jgrowl": "~1.2.10",

144
couchdb/setup-db.js Normal file
View File

@ -0,0 +1,144 @@
var request = require('request');
var async = require('async');
var validate = function(newDoc) {
Object.keys(newDoc).forEach(function(key) {
if(key[0] !== '_' && [
'updated',
'tags',
'title'
].indexOf(key) === -1) {
throw({forbidden: 'Unknown document attribute: ' + key});
}
});
var toString = Object.prototype.toString;
if(toString.call(newDoc._id) !== '[object String]') {
throw({forbidden: 'ID must be a string.'});
}
if(!newDoc._id.match(/[a-zA-Z0-9]{24}/)) {
throw({forbidden: 'Invalid ID format.'});
}
if(toString.call(newDoc.updated) !== '[object Number]') {
throw({forbidden: 'Update time must be an integer.'});
}
if(newDoc.updated > Date.now() + 60000) {
throw({forbidden: 'Update time is in the future, please check your clock!'});
}
if(toString.call(newDoc.title) !== '[object String]') {
throw({forbidden: 'Title must be a string.'});
}
if(!newDoc.title) {
throw({forbidden: 'Title is empty.'});
}
if(newDoc.title.length >= 256) {
throw({forbidden: 'Title too long.'});
}
if(newDoc.tags !== undefined) {
if(toString.call(newDoc.tags) !== '[object Array]') {
throw({forbidden: 'Tags must be an array.'});
}
if(newDoc.tags.length >= 16) {
throw({forbidden: 'Too many tags.'});
}
newDoc.tags.forEach(function(tag) {
if(toString.call(tag) !== '[object String]') {
throw({forbidden: 'Tags must contain strings only.'});
}
if(!tag) {
throw({forbidden: 'Tag is empty.'});
}
if(tag.length > 32) {
throw({forbidden: 'Tag is too long.'});
}
});
}
var attachment = (newDoc._attachments || {}).content;
if(!attachment) {
throw({forbidden: 'Missing attached content.'});
}
if(attachment.content_type != 'text/plain') {
throw({forbidden: 'Invalid content type.'});
}
if(Object.keys(newDoc._attachments).length > 1) {
throw({forbidden: 'Too many attachments.'});
}
};
var byUpdate = function(doc) {
emit(doc.updated, null);
};
var byTagAndUpdate = function(doc) {
doc.tags && doc.tags.forEach(function(tag) {
emit([
tag,
doc.updated
], null);
});
};
if(process.argv.length < 3) {
console.error('Missing URL parameter');
process.exit(-1);
}
var url = process.argv[2];
var ddocs = [
{
path: '/_design/validate',
body: {
validate_doc_update: validate.toString()
}
},
{
path: '/_design/by_update',
body: {
views: {
default: {
map: byUpdate.toString()
}
}
}
},
{
path: '/_design/by_tag_and_update',
body: {
views: {
default: {
map: byTagAndUpdate.toString()
}
}
}
}
];
async.each(ddocs, function(ddoc, cb) {
request.get(url + ddoc.path, function(err, res) {
if(res && res.body) {
ddoc.body._rev = JSON.parse(res.body)._rev;
}
request.put({
url: url + ddoc.path,
json: true,
body: ddoc.body
}, function(err, res) {
if(err) {
return cb(res);
}
if(res.statusCode >= 300) {
return cb(res.body);
}
cb();
});
});
}, function(err) {
if(err) {
console.error(err);
} else {
console.log('All design documents updated successfully');
}
});

View File

@ -314,7 +314,7 @@ define([
// Create discussion index // Create discussion index
var discussionIndex; var discussionIndex;
do { do {
discussionIndex = utils.randomString() + utils.randomString(); // Increased size to prevent collision discussionIndex = utils.id();
} while(_.has(newDiscussionList, discussionIndex)); } while(_.has(newDiscussionList, discussionIndex));
conflict.discussionIndex = discussionIndex; conflict.discussionIndex = discussionIndex;
newDiscussionList[discussionIndex] = conflict; newDiscussionList[discussionIndex] = conflict;

View File

@ -27,6 +27,7 @@ define([], function() {
constants.PICASA_IMPORT_IMG_URL = "/picasaImportImg"; constants.PICASA_IMPORT_IMG_URL = "/picasaImportImg";
constants.SSH_PUBLISH_URL = '/sshPublish'; constants.SSH_PUBLISH_URL = '/sshPublish';
constants.PDF_EXPORT_URL = "/pdfExport"; constants.PDF_EXPORT_URL = "/pdfExport";
constants.COUCHDB_URL = 'http://localhost:5984/documents';
// Site dependent // Site dependent
constants.BASE_URL = "http://localhost/"; constants.BASE_URL = "http://localhost/";

View File

@ -55,7 +55,7 @@ define([
return; return;
} }
if(windowId === undefined) { if(windowId === undefined) {
windowId = utils.randomString(); windowId = utils.id();
storage.frontWindowId = windowId; storage.frontWindowId = windowId;
} }
var frontWindowId = storage.frontWindowId; var frontWindowId = storage.frontWindowId;

View File

@ -381,7 +381,7 @@ define([
// Create discussion index // Create discussion index
var discussionIndex; var discussionIndex;
do { do {
discussionIndex = utils.randomString(); discussionIndex = utils.id();
} while(_.has(discussionList, discussionIndex)); } while(_.has(discussionList, discussionIndex));
discussion.discussionIndex = discussionIndex; discussion.discussionIndex = discussionIndex;
discussionList[discussionIndex] = discussion; discussionList[discussionIndex] = discussion;

View File

@ -7,7 +7,7 @@ define([
"classes/Extension", "classes/Extension",
"classes/FolderDescriptor", "classes/FolderDescriptor",
"folderList", "folderList",
"fileSystem", "fileSystem"
], function($, _, constants, utils, storage, Extension, FolderDescriptor, folderList, fileSystem) { ], function($, _, constants, utils, storage, Extension, FolderDescriptor, folderList, fileSystem) {
var documentManager = new Extension("documentManager", 'Document Manager', false, true); var documentManager = new Extension("documentManager", 'Document Manager', false, true);
@ -39,17 +39,17 @@ define([
'<button class="btn btn-default button-delete" title="Delete"><i class="icon-trash"></i></button>', '<button class="btn btn-default button-delete" title="Delete"><i class="icon-trash"></i></button>',
'<button class="btn btn-default button-rename" title="Rename"><i class="icon-pencil"></i></button>', '<button class="btn btn-default button-rename" title="Rename"><i class="icon-pencil"></i></button>',
'<div class="name"><%= fileDesc.composeTitle() %></div>', '<div class="name"><%= fileDesc.composeTitle() %></div>',
'<input type="text" class="input-rename form-control hide"></li>', '<input type="text" class="input-rename form-control hide"></li>'
].join(''); ].join('');
var selectFolderEltTmpl = [ var selectFolderEltTmpl = [
'<a href="#" class="list-group-item folder clearfix" data-folder-index="<%= folderDesc.folderIndex %>">', '<a href="#" class="list-group-item folder clearfix" data-folder-index="<%= folderDesc.folderIndex %>">',
'<div class="pull-right file-count"><%= _.size(folderDesc.fileList) %></div>', '<div class="pull-right file-count"><%= _.size(folderDesc.fileList) %></div>',
'<div class="name"><i class="icon-forward"></i> ', '<div class="name"><i class="icon-forward"></i> ',
'<%= folderDesc.name %></div></a>', '<%= folderDesc.name %></div></a>'
].join(''); ].join('');
var selectedDocumentEltTmpl = [ var selectedDocumentEltTmpl = [
'<li class="list-group-item file clearfix">', '<li class="list-group-item file clearfix">',
'<div class="name"><%= fileDesc.composeTitle() %></div></li>', '<div class="name"><%= fileDesc.composeTitle() %></div></li>'
].join(''); ].join('');
var isVisible; var isVisible;
@ -86,7 +86,7 @@ define([
return fileDesc.title.toLowerCase(); return fileDesc.title.toLowerCase();
}).reduce(function(result, fileDesc) { }).reduce(function(result, fileDesc) {
return result + _.template(selectedDocumentEltTmpl, { return result + _.template(selectedDocumentEltTmpl, {
fileDesc: fileDesc, fileDesc: fileDesc
}); });
}, '').value(); }, '').value();
selectedDocumentListElt.innerHTML = '<ul class="file-list nav">' + selectedDocumentListHtml + '</ul>'; selectedDocumentListElt.innerHTML = '<ul class="file-list nav">' + selectedDocumentListHtml + '</ul>';
@ -147,7 +147,7 @@ define([
_.size(orphanDocumentList), _.size(orphanDocumentList),
'</div>', '</div>',
'<div class="name"><i class="icon-folder"></i> ', '<div class="name"><i class="icon-folder"></i> ',
'ROOT folder</div></a>', 'ROOT folder</div></a>'
].join(''); ].join('');
// Add orphan documents // Add orphan documents
@ -155,7 +155,7 @@ define([
return fileDesc.title.toLowerCase(); return fileDesc.title.toLowerCase();
}).reduce(function(result, fileDesc) { }).reduce(function(result, fileDesc) {
return result + _.template(documentEltTmpl, { return result + _.template(documentEltTmpl, {
fileDesc: fileDesc, fileDesc: fileDesc
}); });
}, '').value(); }, '').value();
orphanListHtml = orphanListHtml && '<ul class="nav">' + orphanListHtml + '</ul>'; orphanListHtml = orphanListHtml && '<ul class="nav">' + orphanListHtml + '</ul>';
@ -169,7 +169,7 @@ define([
return fileDesc.title.toLowerCase(); return fileDesc.title.toLowerCase();
}).reduce(function(result, fileDesc) { }).reduce(function(result, fileDesc) {
return result + _.template(documentEltTmpl, { return result + _.template(documentEltTmpl, {
fileDesc: fileDesc, fileDesc: fileDesc
}); });
}, '').value(); }, '').value();
fileListHtml = fileListHtml && '<ul class="nav">' + fileListHtml + '</ul>'; fileListHtml = fileListHtml && '<ul class="nav">' + fileListHtml + '</ul>';
@ -224,7 +224,7 @@ define([
$(modalElt.querySelectorAll('.action-create-folder')).click(function() { $(modalElt.querySelectorAll('.action-create-folder')).click(function() {
var folderIndex; var folderIndex;
do { do {
folderIndex = "folder." + utils.randomString(); folderIndex = "folder." + utils.id();
} while (_.has(folderList, folderIndex)); } while (_.has(folderList, folderIndex));
storage[folderIndex + ".name"] = constants.DEFAULT_FOLDER_NAME; storage[folderIndex + ".name"] = constants.DEFAULT_FOLDER_NAME;
@ -286,13 +286,13 @@ define([
_.size(orphanDocumentList), _.size(orphanDocumentList),
'</div>', '</div>',
'<div class="name"><i class="icon-forward"></i> ', '<div class="name"><i class="icon-forward"></i> ',
'ROOT folder</div></a>', 'ROOT folder</div></a>'
].join(''); ].join('');
selectFolderListHtml += _.chain(folderList).sortBy(function(folderDesc) { selectFolderListHtml += _.chain(folderList).sortBy(function(folderDesc) {
return folderDesc.name.toLowerCase(); return folderDesc.name.toLowerCase();
}).reduce(function(result, folderDesc) { }).reduce(function(result, folderDesc) {
return result + _.template(selectFolderEltTmpl, { return result + _.template(selectFolderEltTmpl, {
folderDesc: folderDesc, folderDesc: folderDesc
}); });
}, '').value(); }, '').value();
selectFolderListElt.innerHTML = selectFolderListHtml; selectFolderListElt.innerHTML = selectFolderListHtml;

View File

@ -68,7 +68,7 @@ define([
var fileIndex = constants.TEMPORARY_FILE_INDEX; var fileIndex = constants.TEMPORARY_FILE_INDEX;
if(!isTemporary) { if(!isTemporary) {
do { do {
fileIndex = "file." + utils.randomString(); fileIndex = "file." + utils.id();
} while(_.has(fileSystem, fileIndex)); } while(_.has(fileSystem, fileIndex));
} }

View File

@ -0,0 +1,248 @@
define([
"jquery",
"underscore",
"constants",
"core",
"utils",
"storage",
"logger",
"settings",
"eventMgr",
"classes/AsyncTask"
], function($, _, constants, core, utils, storage, logger, settings, eventMgr, AsyncTask) {
var couchdbHelper = {};
// Listen to offline status changes
var isOffline = false;
eventMgr.addListener("onOfflineChanged", function(isOfflineParam) {
isOffline = isOfflineParam;
});
couchdbHelper.uploadDocument = function(documentId, title, content, tags, rev, callback) {
var result;
var task = new AsyncTask();
task.onRun(function() {
if(tags) {
// Has to be an array
if(!_.isArray(tags)) {
tags = _.chain(('' + tags).split(/\s+/))
.compact()
.unique()
.value();
}
// Remove invalid tags
tags = tags.filter(function(tag) {
return _.isString(tag) && tag.length < 32;
});
// Limit the number of tags
tags = tags.slice(0, 16);
}
else {
tags = undefined;
}
$.ajax({
type: 'POST',
url: constants.COUCHDB_URL,
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify({
_id: documentId || utils.id(),
title: title,
tags: tags,
updated: Date.now(),
_rev: rev,
_attachments: {
content: {
content_type: 'text\/plain',
data: utils.encodeBase64(content)
}
}
})
}).done(function(data) {
result = data;
task.chain();
}).fail(function(jqXHR) {
handleError(jqXHR, task);
});
});
task.onSuccess(function() {
callback(undefined, result);
});
task.onError(function(error) {
callback(error);
});
task.enqueue();
};
couchdbHelper.checkChanges = function(lastChangeId, syncLocations, callback) {
var changes;
var newChangeId = lastChangeId || 0;
var task = new AsyncTask();
task.onRun(function() {
$.ajax({
type: 'POST',
url: constants.COUCHDB_URL + '/_changes?' + $.param({
filter: '_doc_ids',
since: newChangeId,
include_docs: true,
attachments: true
}),
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify({
doc_ids: Object.keys(syncLocations)
})
}).done(function(data) {
newChangeId = data.last_seq;
changes = _.map(data.results, function(result) {
return result.deleted ? {
_id: result.id,
deleted: true
} : result.doc;
});
task.chain();
}).fail(function(jqXHR) {
handleError(jqXHR, task);
});
});
task.onSuccess(function() {
callback(undefined, changes, newChangeId);
});
task.onError(function(error) {
callback(error);
});
task.enqueue();
};
couchdbHelper.downloadContent = function(documents, callback) {
var result = [];
var task = new AsyncTask();
task.onRun(function() {
function recursiveDownloadContent() {
if(documents.length === 0) {
return task.chain();
}
var document = documents[0];
result.push(document);
if(document.deleted || ((document._attachments || {}).content || {}).data !== undefined) {
documents.shift();
return task.chain(recursiveDownloadContent);
}
$.ajax({
url: constants.COUCHDB_URL + '/' + encodeURIComponent(document._id),
headers: {
Accept: 'application/json'
},
contentType: 'application/json',
dataType: 'json',
data: {
attachments: true
}
}).done(function(doc) {
documents.shift();
_.extend(document, doc);
task.chain(recursiveDownloadContent);
}).fail(function(jqXHR) {
handleError(jqXHR, task);
});
}
task.chain(recursiveDownloadContent);
});
task.onSuccess(function() {
callback(undefined, result);
});
task.onError(function(error) {
callback(error);
});
task.enqueue();
};
couchdbHelper.listDocuments = function(tag, updated, callback) {
var result;
var task = new AsyncTask();
task.onRun(function() {
var ddoc = '/_design/by_' + (tag ? 'tag_and_' : '') + 'update/_view/default';
var startKey = tag ? JSON.stringify([
tag,
updated || []
]) : updated;
var endKey = tag && JSON.stringify([
tag
]);
$.ajax({
url: constants.COUCHDB_URL + ddoc,
data: {
start_key: startKey,
end_key: endKey,
descending: true,
include_docs: true,
limit: 3,
reduce: false
},
dataType: 'json'
}).done(function(data) {
result = _.pluck(data.rows, 'doc');
task.chain();
}).fail(function(jqXHR) {
handleError(jqXHR, task);
});
});
task.onSuccess(function() {
callback(undefined, result);
});
task.onError(function(error) {
callback(error);
});
task.enqueue();
};
couchdbHelper.deleteDocuments = function(docs) {
var task = new AsyncTask();
task.onRun(function() {
$.ajax({
type: 'POST',
url: constants.COUCHDB_URL + '/_bulk_docs',
data: JSON.stringify({
docs: docs.map(function(doc) {
return {
_id: doc._id,
_rev: doc._rev,
_deleted: true
};
})
}),
contentType: 'application/json',
dataType: 'json'
}).done(function() {
task.chain();
}).fail(function(jqXHR) {
handleError(jqXHR, task);
});
});
task.enqueue();
};
function handleError(jqXHR, task) {
var error = {
code: jqXHR.status,
message: jqXHR.statusText,
reason: (jqXHR.responseJSON || {}).reason
};
var errorMsg;
if(error) {
logger.error(error);
// Try to analyze the error
if(typeof error === "string") {
errorMsg = error;
}
else {
errorMsg = "Error " + error.code + ": " + (error.reason || error.message);
}
}
task.error(new Error(errorMsg));
}
return couchdbHelper;
});

View File

@ -1,6 +1,6 @@
<div class="layout-wrapper-l1"> <div class="layout-wrapper-l1">
<div class="layout-wrapper-l2"> <div class="layout-wrapper-l2">
<div class="navbar navbar-default ui-layout-north"> <div class="navbar navbar-default">
<div class="navbar-inner"> <div class="navbar-inner">
<div class="nav left-space"></div> <div class="nav left-space"></div>
<div class="nav right-space pull-right"></div> <div class="nav right-space pull-right"></div>
@ -50,7 +50,7 @@
</div> </div>
</div> </div>
<div class="layout-wrapper-l3"> <div class="layout-wrapper-l3">
<pre id="wmd-input" class="ui-layout-center form-control"><div class="editor-content" contenteditable=true></div><div class="editor-margin"></div></pre> <pre id="wmd-input" class="form-control"><div class="editor-content" contenteditable=true></div><div class="editor-margin"></div></pre>
<div class="preview-panel"> <div class="preview-panel">
<div class="layout-resizer layout-resizer-preview"></div> <div class="layout-resizer layout-resizer-preview"></div>
<div class="layout-toggler layout-toggler-navbar btn btn-info" title="Toggle navigation bar"><i class="icon-th"></i></div> <div class="layout-toggler layout-toggler-navbar btn btn-info" title="Toggle navigation bar"><i class="icon-th"></i></div>
@ -89,7 +89,7 @@
<a href="#" data-toggle="collapse" data-target=".collapse-synchronize" <a href="#" data-toggle="collapse" data-target=".collapse-synchronize"
class="list-group-item"> class="list-group-item">
<div><i class="icon-refresh"></i> Synchronize</div> <div><i class="icon-refresh"></i> Synchronize</div>
<small>Backup, collaborate...</small> <small>Open/save in the Cloud</small>
</a> </a>
<div class="sub-menu collapse collapse-synchronize clearfix"> <div class="sub-menu collapse collapse-synchronize clearfix">
<ul class="nav alert alert-danger show-already-synchronized"> <ul class="nav alert alert-danger show-already-synchronized">
@ -101,6 +101,10 @@
class="icon-refresh"></i> Manage synchronization</a></li> class="icon-refresh"></i> Manage synchronization</a></li>
</ul> </ul>
<ul class="nav"> <ul class="nav">
<li><a href="#" class="action-sync-import-dialog-couchdb"><i
class="icon-provider-couchdb"></i> Open from CouchDB <sup class="text-danger">beta</sup></a></li>
<li><a href="#" class="action-sync-export-dialog-couchdb"><i
class="icon-provider-couchdb"></i> Save on CouchDB <sup class="text-danger">beta</sup></a></li>
<li><a href="#" class="action-sync-import-dropbox"><i <li><a href="#" class="action-sync-import-dropbox"><i
class="icon-provider-dropbox"></i> Open from Dropbox</a></li> class="icon-provider-dropbox"></i> Open from Dropbox</a></li>
<li><a href="#" class="action-sync-export-dialog-dropbox"><i <li><a href="#" class="action-sync-export-dialog-dropbox"><i
@ -544,6 +548,110 @@
</div> </div>
<div class="modal fade modal-download-couchdb">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-hidden="true">&times;</button>
<h2 class="modal-title">Open from CouchDB</h2>
<br>
<div class="form-horizontal">
<div class="form-group">
<label for="select-sync-import-couchdb-tag" class="col-sm-3 control-label">Filter by tag</label>
<div class="col-sm-5">
<select id="select-sync-import-couchdb-tag" class="form-control">
<option value="">None</option>
</select>
</div>
<div class="col-sm-4">
<button class="btn btn-link"><i class="icon-tags"></i> Manage tags</button>
</div>
</div>
</div>
</div>
<div class="modal-body">
<div class="form-horizontal hide">
<div class="form-group">
<label for="input-sync-import-couchdb-documentid" class="col-sm-3 control-label">Document
ID</label>
<div class="col-sm-9">
<input id="input-sync-import-couchdb-documentid" class="form-control"
placeholder="DocumentID">
<span class="help-block">Multiple IDs can be provided (space separated)</span>
</div>
</div>
</div>
<ul class="document-list nav nav-pills">
<li class="pull-right dropdown"><a href="#"
data-toggle="dropdown"><i class="icon-check"></i> Selection
<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#" class="action-unselect-all"><i
class="icon-check-empty"></i> Unselect all</a></li>
<li class="divider"></li>
<li><a href="#" class="action-delete-items"><i
class="icon-trash"></i> Delete</a></li>
</ul>
</li>
</ul>
<p class="document-list">
</p>
<div class="list-group document-list"></div>
<div class="document-list">
<div class="please-wait">Please wait...</div>
<button class="more-documents btn btn-link">More documents!</button>
</div>
<p class="confirm-delete hide">The following documents will be
removed from the database:</p>
<div class="confirm-delete list-group selected-document-list hide"></div>
</div>
<div class="modal-footer">
<a href="#"
class="btn btn-default confirm-delete action-cancel hide">Cancel</a>
<a href="#"
class="btn btn-primary confirm-delete action-delete-items-confirm hide">Delete</a>
<a href="#" class="btn btn-default document-list" data-dismiss="modal">Cancel</a>
<a href="#" data-dismiss="modal"
class="btn btn-primary action-sync-import-couchdb document-list">Open</a>
</div>
</div>
</div>
</div>
<div class="modal fade modal-upload-couchdb">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-hidden="true">&times;</button>
<h2 class="modal-title">Save on CouchDB</h2>
</div>
<div class="modal-body">
<p>
This will save "<span class="file-title"></span>" to CouchDB and keep it synchronized.
</p>
<blockquote>
<b>Tip:</b> You can use a
<a href="http://jekyllrb.com/docs/frontmatter/"
target="_blank">YAML front matter</a> to specify tags for your document.
</blockquote>
</div>
<div class="modal-footer">
<a href="#" class="btn btn-default" data-dismiss="modal">Cancel</a>
<a href="#" data-dismiss="modal"
class="btn btn-primary action-sync-export-couchdb">OK</a>
</div>
</div>
</div>
</div>
<div class="modal fade modal-manage-sync"> <div class="modal fade modal-manage-sync">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">

View File

@ -1,6 +1,6 @@
<div class="layout-wrapper-l1"> <div class="layout-wrapper-l1">
<div class="layout-wrapper-l2"> <div class="layout-wrapper-l2">
<div class="navbar navbar-default ui-layout-north"> <div class="navbar navbar-default">
<div class="navbar-inner"> <div class="navbar-inner">
<div class="nav left-space"></div> <div class="nav left-space"></div>
<div class="nav right-space pull-right"></div> <div class="nav right-space pull-right"></div>
@ -43,7 +43,7 @@
</div> </div>
</div> </div>
<div class="layout-wrapper-l3"> <div class="layout-wrapper-l3">
<pre id="wmd-input" class="ui-layout-center form-control"><div class="editor-content"></div><div class="editor-margin"></div></pre> <pre id="wmd-input" class="form-control"><div class="editor-content"></div><div class="editor-margin"></div></pre>
<div class="preview-panel"> <div class="preview-panel">
<div class="preview-container"> <div class="preview-container">
<div id="preview-contents"> <div id="preview-contents">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,297 @@
define([
"jquery",
"underscore",
"constants",
"utils",
"storage",
"logger",
"classes/Provider",
"settings",
"eventMgr",
"fileMgr",
"fileSystem",
"editor",
"helpers/couchdbHelper"
], function($, _, constants, utils, storage, logger, Provider, settings, eventMgr, fileMgr, fileSystem, editor, couchdbHelper) {
var PROVIDER_COUCHDB = "couchdb";
var couchdbProvider = new Provider(PROVIDER_COUCHDB, "CouchDB");
function createSyncIndex(id) {
return "sync." + PROVIDER_COUCHDB + "." + id;
}
var merge = settings.conflictMode == 'merge';
function createSyncAttributes(id, rev, content, title, discussionListJSON) {
discussionListJSON = discussionListJSON || '{}';
var syncAttributes = {};
syncAttributes.provider = couchdbProvider;
syncAttributes.id = id;
syncAttributes.rev = rev;
syncAttributes.contentCRC = utils.crc32(content);
syncAttributes.titleCRC = utils.crc32(title);
syncAttributes.discussionListCRC = utils.crc32(discussionListJSON);
syncAttributes.syncIndex = createSyncIndex(id);
if(merge === true) {
// Need to store the whole content for merge
syncAttributes.content = content;
syncAttributes.title = title;
syncAttributes.discussionList = discussionListJSON;
}
return syncAttributes;
}
function importFilesFromIds(ids) {
couchdbHelper.downloadContent(ids.map(function(id) {
return {
_id: id
};
}), function(error, result) {
if(error) {
return;
}
var fileDescList = [];
var fileDesc;
_.each(result, function(file) {
var content = utils.decodeBase64(file._attachments.content.data);
var parsedContent = couchdbProvider.parseContent(content);
var syncLocations;
var syncAttributes = createSyncAttributes(file._id, file._rev, parsedContent.content, file.title, parsedContent.discussionListJSON);
syncLocations = {};
syncLocations[syncAttributes.syncIndex] = syncAttributes;
fileDesc = fileMgr.createFile(file.title, parsedContent.content, parsedContent.discussionListJSON, syncLocations);
fileDescList.push(fileDesc);
});
if(fileDesc !== undefined) {
eventMgr.onSyncImportSuccess(fileDescList, couchdbProvider);
fileMgr.selectFile(fileDesc);
}
});
}
couchdbProvider.importFiles = function() {
var tag = $('#select-sync-import-couchdb-tag').val();
if(!tag) {
var ids = _.chain(($('#input-sync-import-couchdb-documentid').val() || '').split(/\s+/))
.compact()
.unique()
.value();
var importIds = [];
_.each(ids, function(id) {
var syncIndex = createSyncIndex(id);
var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
if(fileDesc !== undefined) {
return eventMgr.onError('"' + fileDesc.title + '" is already in your local documents.');
}
importIds.push(id);
});
importFilesFromIds(importIds);
}
};
couchdbProvider.exportFile = function(event, title, content, discussionListJSON, frontMatter, callback) {
var data = couchdbProvider.serializeContent(content, discussionListJSON);
var tags = frontMatter && frontMatter.tags;
couchdbHelper.uploadDocument(undefined, title, data, tags, undefined, function(error, result) {
if(error) {
return callback(error);
}
var syncAttributes = createSyncAttributes(result.id, result.rev, content, title, discussionListJSON);
callback(undefined, syncAttributes);
});
};
couchdbProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, frontMatter, syncAttributes, callback) {
if(
(syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed
(syncAttributes.titleCRC == titleCRC) && // Title CRC hasn't changed
(syncAttributes.discussionListCRC == discussionListCRC) // Discussion list CRC hasn't changed
) {
return callback(undefined, false);
}
var data = couchdbProvider.serializeContent(content, discussionList);
var tags = frontMatter && frontMatter.tags;
couchdbHelper.uploadDocument(syncAttributes.id, title, data, tags, syncAttributes.rev, function(error, result) {
if(error) {
return callback(error, true);
}
syncAttributes.rev = result.rev;
if(merge === true) {
// Need to store the whole content for merge
syncAttributes.content = content;
syncAttributes.title = title;
syncAttributes.discussionList = discussionList;
}
syncAttributes.contentCRC = contentCRC;
syncAttributes.titleCRC = titleCRC;
syncAttributes.discussionListCRC = discussionListCRC;
callback(undefined, true);
});
};
couchdbProvider.syncDown = function(callback) {
var lastChangeId = parseInt(storage[PROVIDER_COUCHDB + ".lastChangeId"], 10);
var syncLocations = {};
_.each(fileSystem, function(fileDesc) {
_.each(fileDesc.syncLocations, function(syncAttributes) {
syncAttributes.provider === couchdbProvider && (syncLocations[syncAttributes.id] = syncAttributes);
});
});
couchdbHelper.checkChanges(lastChangeId, syncLocations, function(error, changes, newChangeId) {
if(error) {
return callback(error);
}
var interestingChanges = [];
_.each(changes, function(change) {
var syncIndex = createSyncIndex(change._id);
var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
var syncAttributes = fileDesc && fileDesc.syncLocations[syncIndex];
if(!syncAttributes) {
return;
}
// Store fileDesc and syncAttributes references to avoid 2 times search
change.fileDesc = fileDesc;
change.syncAttributes = syncAttributes;
interestingChanges.push(change);
});
couchdbHelper.downloadContent(interestingChanges, function(error, changes) {
if(error) {
return callback(error);
}
function mergeChange() {
if(changes.length === 0) {
storage[PROVIDER_COUCHDB + ".lastChangeId"] = newChangeId;
return callback();
}
var change = changes.pop();
var fileDesc = change.fileDesc;
var syncAttributes = change.syncAttributes;
// File deleted
if(change.deleted === true) {
eventMgr.onError('"' + fileDesc.title + '" has been removed from CouchDB.');
fileDesc.removeSyncLocation(syncAttributes);
return eventMgr.onSyncRemoved(fileDesc, syncAttributes);
}
var file = change;
var content = utils.decodeBase64(file._attachments.content.data);
var parsedContent = couchdbProvider.parseContent(content);
var remoteContent = parsedContent.content;
var remoteTitle = file.title;
var remoteDiscussionListJSON = parsedContent.discussionListJSON;
var remoteDiscussionList = parsedContent.discussionList;
var remoteCRC = couchdbProvider.syncMerge(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList, remoteDiscussionListJSON);
// Update syncAttributes
syncAttributes.rev = file._rev;
if(merge === true) {
// Need to store the whole content for merge
syncAttributes.content = remoteContent;
syncAttributes.title = remoteTitle;
syncAttributes.discussionList = remoteDiscussionListJSON;
}
syncAttributes.contentCRC = remoteCRC.contentCRC;
syncAttributes.titleCRC = remoteCRC.titleCRC;
syncAttributes.discussionListCRC = remoteCRC.discussionListCRC;
utils.storeAttributes(syncAttributes);
setTimeout(mergeChange, 5);
}
setTimeout(mergeChange, 5);
});
});
};
var documentEltTmpl = [
'<a href="#" class="list-group-item document clearfix" data-document-id="<%= document._id %>">',
'<div class="date pull-right"><%= date %></div></div>',
'<div class="name"><i class="icon-provider-couchdb"></i> ',
'<%= document.title %></div>',
'</a>'
].join('');
eventMgr.addListener("onReady", function() {
var modalElt = document.querySelector('.modal-download-couchdb');
var $documentListElt = $(modalElt.querySelector('.list-group.document-list'));
var $selectedDocumentListElt = $(modalElt.querySelector('.selected-document-list'));
var $pleaseWaitElt = $(modalElt.querySelector('.please-wait'));
var $moreDocumentsElt = $(modalElt.querySelector('.more-documents'));
var documentMap, lastDocument;
var selectedDocuments, $selectedElts;
function doSelect() {
$selectedElts = $documentListElt.children('.active').clone();
selectedDocuments = [];
$selectedElts.each(function() {
selectedDocuments.push(documentMap[$(this).data('documentId')]);
});
$selectedDocumentListElt.empty().append($selectedElts);
$(modalElt.querySelectorAll('.action-delete-items')).parent().toggleClass('disabled', selectedDocuments.length === 0);
}
function clear() {
documentMap = {};
lastDocument = undefined;
$documentListElt.empty();
doSelect();
}
clear();
function deleteMode(enabled) {
$(modalElt.querySelectorAll('.confirm-delete')).toggleClass('hide', !enabled);
$(modalElt.querySelectorAll('.document-list')).toggleClass('hide', enabled);
}
function updateDocumentList() {
$pleaseWaitElt.removeClass('hide');
$moreDocumentsElt.addClass('hide');
couchdbHelper.listDocuments(undefined, lastDocument && lastDocument.updated, function(err, result) {
if(err) {
return;
}
$pleaseWaitElt.addClass('hide');
if(result.length === 3) {
$moreDocumentsElt.removeClass('hide');
lastDocument = result.pop();
}
var documentListHtml = _.reduce(result, function(result, document) {
documentMap[document._id] = document;
return result + _.template(documentEltTmpl, {
document: document,
date: utils.formatDate(document.updated)
});
}, '');
$documentListElt.append(documentListHtml);
});
deleteMode(false);
}
$(modalElt)
.on('show.bs.modal', updateDocumentList)
.on('hidden.bs.modal', clear)
.on('click', '.document-list .document', function() {
$(this).toggleClass('active');
doSelect();
})
.on('click', '.more-documents', updateDocumentList)
.on('click', '.action-unselect-all', function() {
$documentListElt.children().removeClass('active');
doSelect();
})
.on('click', '.action-delete-items', function() {
doSelect();
if($selectedElts.length) {
deleteMode(true);
}
})
.on('click', '.action-delete-items-confirm', function() {
couchdbHelper.deleteDocuments(selectedDocuments);
clear();
updateDocumentList();
})
.on('click', '.action-cancel', function() {
deleteMode(false);
});
});
return couchdbProvider;
});

View File

@ -94,7 +94,7 @@ define([
}); });
}; };
dropboxProvider.exportFile = function(event, title, content, discussionListJSON, callback) { dropboxProvider.exportFile = function(event, title, content, discussionListJSON, frontMatter, callback) {
var path = utils.getInputTextValue("#input-sync-export-dropbox-path", event); var path = utils.getInputTextValue("#input-sync-export-dropbox-path", event);
path = checkPath(path); path = checkPath(path);
if(path === undefined) { if(path === undefined) {
@ -118,7 +118,7 @@ define([
}); });
}; };
dropboxProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, syncAttributes, callback) { dropboxProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, frontMatter, syncAttributes, callback) {
if( if(
(syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed (syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed
(syncAttributes.discussionListCRC == discussionListCRC) // Discussion list CRC hasn't changed (syncAttributes.discussionListCRC == discussionListCRC) // Discussion list CRC hasn't changed

View File

@ -1,390 +1,394 @@
define([ define([
"jquery", "jquery",
"underscore", "underscore",
"constants", "constants",
"utils", "utils",
"storage", "storage",
"logger", "logger",
"classes/Provider", "classes/Provider",
"settings", "settings",
"eventMgr", "eventMgr",
"fileMgr", "fileMgr",
"editor", "editor",
"helpers/googleHelper", "helpers/googleHelper",
"text!html/dialogExportGdrive.html", "text!html/dialogExportGdrive.html",
"text!html/dialogAutoSyncGdrive.html", "text!html/dialogAutoSyncGdrive.html"
], function($, _, constants, utils, storage, logger, Provider, settings, eventMgr, fileMgr, editor, googleHelper, dialogExportGdriveHTML, dialogAutoSyncGdriveHTML) { ], function($, _, constants, utils, storage, logger, Provider, settings, eventMgr, fileMgr, editor, googleHelper, dialogExportGdriveHTML, dialogAutoSyncGdriveHTML) {
return function(providerId, providerName, accountIndex) { return function(providerId, providerName, accountIndex) {
var accountId = 'google.gdrive' + accountIndex; var accountId = 'google.gdrive' + accountIndex;
var gdriveProvider = new Provider(providerId, providerName); var gdriveProvider = new Provider(providerId, providerName);
gdriveProvider.defaultPublishFormat = "template"; gdriveProvider.defaultPublishFormat = "template";
gdriveProvider.exportPreferencesInputIds = [ gdriveProvider.exportPreferencesInputIds = [
providerId + "-parentid", providerId + "-parentid",
]; ];
function createSyncIndex(id) { function createSyncIndex(id) {
return "sync." + providerId + "." + id; return "sync." + providerId + "." + id;
} }
var merge = settings.conflictMode == 'merge'; var merge = settings.conflictMode == 'merge';
function createSyncAttributes(id, etag, content, title, discussionListJSON) {
discussionListJSON = discussionListJSON || '{}';
var syncAttributes = {};
syncAttributes.provider = gdriveProvider;
syncAttributes.id = id;
syncAttributes.etag = etag;
syncAttributes.contentCRC = utils.crc32(content);
syncAttributes.titleCRC = utils.crc32(title);
syncAttributes.discussionListCRC = utils.crc32(discussionListJSON);
syncAttributes.syncIndex = createSyncIndex(id);
if(merge === true) {
// Need to store the whole content for merge
syncAttributes.content = content;
syncAttributes.title = title;
syncAttributes.discussionList = discussionListJSON;
}
return syncAttributes;
}
function importFilesFromIds(ids) { function createSyncAttributes(id, etag, content, title, discussionListJSON) {
googleHelper.downloadMetadata(ids, accountId, function(error, result) { discussionListJSON = discussionListJSON || '{}';
if(error) { var syncAttributes = {};
return; syncAttributes.provider = gdriveProvider;
} syncAttributes.id = id;
googleHelper.downloadContent(result, accountId, function(error, result) { syncAttributes.etag = etag;
if(error) { syncAttributes.contentCRC = utils.crc32(content);
return; syncAttributes.titleCRC = utils.crc32(title);
} syncAttributes.discussionListCRC = utils.crc32(discussionListJSON);
var fileDescList = []; syncAttributes.syncIndex = createSyncIndex(id);
var fileDesc; if(merge === true) {
_.each(result, function(file) { // Need to store the whole content for merge
var parsedContent = gdriveProvider.parseContent(file.content); syncAttributes.content = content;
var syncLocations; syncAttributes.title = title;
if(file.isRealtime) { syncAttributes.discussionList = discussionListJSON;
eventMgr.onError('Real time synchronization is not supported anymore. Please use standard synchronization.'); }
} return syncAttributes;
else { }
var syncAttributes = createSyncAttributes(file.id, file.etag, parsedContent.content, file.title, parsedContent.discussionListJSON);
syncLocations = {};
syncLocations[syncAttributes.syncIndex] = syncAttributes;
}
fileDesc = fileMgr.createFile(file.title, parsedContent.content, parsedContent.discussionListJSON, syncLocations);
fileDescList.push(fileDesc);
});
if(fileDesc !== undefined) {
eventMgr.onSyncImportSuccess(fileDescList, gdriveProvider);
fileMgr.selectFile(fileDesc);
}
});
});
}
gdriveProvider.importFiles = function() { function importFilesFromIds(ids) {
googleHelper.picker(function(error, docs) { googleHelper.downloadMetadata(ids, accountId, function(error, result) {
if(error || docs.length === 0) { if(error) {
return; return;
} }
var importIds = []; googleHelper.downloadContent(result, accountId, function(error, result) {
_.each(docs, function(doc) { if(error) {
var syncIndex = createSyncIndex(doc.id); return;
var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); }
if(fileDesc !== undefined) { var fileDescList = [];
return eventMgr.onError('"' + fileDesc.title + '" is already in your local documents.'); var fileDesc;
} _.each(result, function(file) {
importIds.push(doc.id); var parsedContent = gdriveProvider.parseContent(file.content);
}); var syncLocations;
importFilesFromIds(importIds); if(file.isRealtime) {
}, 'doc', accountId); eventMgr.onError('Real time synchronization is not supported anymore. Please use standard synchronization.');
}; }
else {
var syncAttributes = createSyncAttributes(file.id, file.etag, parsedContent.content, file.title, parsedContent.discussionListJSON);
syncLocations = {};
syncLocations[syncAttributes.syncIndex] = syncAttributes;
}
fileDesc = fileMgr.createFile(file.title, parsedContent.content, parsedContent.discussionListJSON, syncLocations);
fileDescList.push(fileDesc);
});
if(fileDesc !== undefined) {
eventMgr.onSyncImportSuccess(fileDescList, gdriveProvider);
fileMgr.selectFile(fileDesc);
}
});
});
}
gdriveProvider.exportFile = function(event, title, content, discussionListJSON, callback) { gdriveProvider.importFiles = function() {
var fileId = utils.getInputTextValue('#input-sync-export-' + providerId + '-fileid'); googleHelper.picker(function(error, docs) {
if(fileId) { if(error || docs.length === 0) {
// Check that file is not synchronized with another an existing return;
// document }
var syncIndex = createSyncIndex(fileId); var importIds = [];
var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); _.each(docs, function(doc) {
if(fileDesc !== undefined) { var syncIndex = createSyncIndex(doc.id);
eventMgr.onError('File ID is already synchronized with "' + fileDesc.title + '".'); var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
return callback(true); if(fileDesc !== undefined) {
} return eventMgr.onError('"' + fileDesc.title + '" is already in your local documents.');
} }
var parentId = utils.getInputTextValue('#input-sync-export-' + providerId + '-parentid'); importIds.push(doc.id);
var data = gdriveProvider.serializeContent(content, discussionListJSON); });
googleHelper.upload(fileId, parentId, title, data, undefined, undefined, accountId, function(error, result) { importFilesFromIds(importIds);
if(error) { }, 'doc', accountId);
return callback(error); };
}
var syncAttributes = createSyncAttributes(result.id, result.etag, content, title, discussionListJSON);
callback(undefined, syncAttributes);
});
};
gdriveProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, syncAttributes, callback) { gdriveProvider.exportFile = function(event, title, content, discussionListJSON, frontMatter, callback) {
if( var fileId = utils.getInputTextValue('#input-sync-export-' + providerId + '-fileid');
(syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed if(fileId) {
(syncAttributes.titleCRC == titleCRC) && // Title CRC hasn't changed // Check that file is not synchronized with another an existing
(syncAttributes.discussionListCRC == discussionListCRC) // Discussion list CRC hasn't changed // document
) { var syncIndex = createSyncIndex(fileId);
return callback(undefined, false); var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
} if(fileDesc !== undefined) {
eventMgr.onError('File ID is already synchronized with "' + fileDesc.title + '".');
return callback(true);
}
}
var parentId = utils.getInputTextValue('#input-sync-export-' + providerId + '-parentid');
var data = gdriveProvider.serializeContent(content, discussionListJSON);
googleHelper.upload(fileId, parentId, title, data, undefined, undefined, accountId, function(error, result) {
if(error) {
return callback(error);
}
var syncAttributes = createSyncAttributes(result.id, result.etag, content, title, discussionListJSON);
callback(undefined, syncAttributes);
});
};
if(syncAttributes.isRealtime) { gdriveProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, frontMatter, syncAttributes, callback) {
var fileDesc = fileMgr.getFileFromSyncIndex(syncAttributes.syncIndex); if(
fileDesc.removeSyncLocation(syncAttributes); (syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed
eventMgr.onSyncRemoved(fileDesc, syncAttributes); (syncAttributes.titleCRC == titleCRC) && // Title CRC hasn't changed
return eventMgr.onError('Real time synchronization is not supported anymore. Please use standard synchronization.'); (syncAttributes.discussionListCRC == discussionListCRC) // Discussion list CRC hasn't changed
} ) {
return callback(undefined, false);
}
var data = gdriveProvider.serializeContent(content, discussionList); if(syncAttributes.isRealtime) {
googleHelper.upload(syncAttributes.id, undefined, title, data, undefined, syncAttributes.etag, accountId, function(error, result) { var fileDesc = fileMgr.getFileFromSyncIndex(syncAttributes.syncIndex);
if(error) { fileDesc.removeSyncLocation(syncAttributes);
return callback(error, true); eventMgr.onSyncRemoved(fileDesc, syncAttributes);
} return eventMgr.onError('Real time synchronization is not supported anymore. Please use standard synchronization.');
syncAttributes.etag = result.etag; }
// Remove this deprecated flag if any
delete syncAttributes.isRealtime;
if(merge === true) {
// Need to store the whole content for merge
syncAttributes.content = content;
syncAttributes.title = title;
syncAttributes.discussionList = discussionList;
}
syncAttributes.contentCRC = contentCRC;
syncAttributes.titleCRC = titleCRC;
syncAttributes.discussionListCRC = discussionListCRC;
callback(undefined, true);
});
};
gdriveProvider.syncDown = function(callback) { var data = gdriveProvider.serializeContent(content, discussionList);
var lastChangeId = parseInt(storage[accountId + ".gdrive.lastChangeId"], 10); googleHelper.upload(syncAttributes.id, undefined, title, data, undefined, syncAttributes.etag, accountId, function(error, result) {
googleHelper.checkChanges(lastChangeId, accountId, function(error, changes, newChangeId) { if(error) {
if(error) { return callback(error, true);
return callback(error); }
} syncAttributes.etag = result.etag;
var interestingChanges = []; // Remove this deprecated flag if any
_.each(changes, function(change) { delete syncAttributes.isRealtime;
var syncIndex = createSyncIndex(change.fileId); if(merge === true) {
var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); // Need to store the whole content for merge
var syncAttributes = fileDesc && fileDesc.syncLocations[syncIndex]; syncAttributes.content = content;
if(!syncAttributes) { syncAttributes.title = title;
return; syncAttributes.discussionList = discussionList;
} }
// Store fileDesc and syncAttributes references to avoid 2 times search syncAttributes.contentCRC = contentCRC;
change.fileDesc = fileDesc; syncAttributes.titleCRC = titleCRC;
change.syncAttributes = syncAttributes; syncAttributes.discussionListCRC = discussionListCRC;
// Delete callback(undefined, true);
if(change.deleted === true) { });
interestingChanges.push(change); };
return;
}
// Modify
if(syncAttributes.etag != change.file.etag) {
interestingChanges.push(change);
}
});
googleHelper.downloadContent(interestingChanges, accountId, function(error, changes) {
if(error) {
callback(error);
return;
}
function mergeChange() {
if(changes.length === 0) {
storage[accountId + ".gdrive.lastChangeId"] = newChangeId;
return callback();
}
var change = changes.pop();
var fileDesc = change.fileDesc;
var syncAttributes = change.syncAttributes;
// File deleted
if(change.deleted === true) {
eventMgr.onError('"' + fileDesc.title + '" has been removed from ' + providerName + '.');
fileDesc.removeSyncLocation(syncAttributes);
return eventMgr.onSyncRemoved(fileDesc, syncAttributes);
}
var file = change.file;
var parsedContent = gdriveProvider.parseContent(file.content);
var remoteContent = parsedContent.content;
var remoteTitle = file.title;
var remoteDiscussionListJSON = parsedContent.discussionListJSON;
var remoteDiscussionList = parsedContent.discussionList;
var remoteCRC = gdriveProvider.syncMerge(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList, remoteDiscussionListJSON);
// Update syncAttributes gdriveProvider.syncDown = function(callback) {
syncAttributes.etag = file.etag; var lastChangeId = parseInt(storage[accountId + ".gdrive.lastChangeId"], 10);
if(merge === true) { googleHelper.checkChanges(lastChangeId, accountId, function(error, changes, newChangeId) {
// Need to store the whole content for merge if(error) {
syncAttributes.content = remoteContent; return callback(error);
syncAttributes.title = remoteTitle; }
syncAttributes.discussionList = remoteDiscussionListJSON; var interestingChanges = [];
} _.each(changes, function(change) {
syncAttributes.contentCRC = remoteCRC.contentCRC; var syncIndex = createSyncIndex(change.fileId);
syncAttributes.titleCRC = remoteCRC.titleCRC; var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
syncAttributes.discussionListCRC = remoteCRC.discussionListCRC; var syncAttributes = fileDesc && fileDesc.syncLocations[syncIndex];
utils.storeAttributes(syncAttributes); if(!syncAttributes) {
setTimeout(mergeChange, 5); return;
} }
setTimeout(mergeChange, 5); // Store fileDesc and syncAttributes references to avoid 2 times search
}); change.fileDesc = fileDesc;
}); change.syncAttributes = syncAttributes;
}; // Delete
if(change.deleted === true) {
interestingChanges.push(change);
return;
}
// Modify
if(syncAttributes.etag != change.file.etag) {
interestingChanges.push(change);
}
});
googleHelper.downloadContent(interestingChanges, accountId, function(error, changes) {
if(error) {
callback(error);
return;
}
function mergeChange() {
if(changes.length === 0) {
storage[accountId + ".gdrive.lastChangeId"] = newChangeId;
return callback();
}
var change = changes.pop();
var fileDesc = change.fileDesc;
var syncAttributes = change.syncAttributes;
// File deleted
if(change.deleted === true) {
eventMgr.onError('"' + fileDesc.title + '" has been removed from ' + providerName + '.');
fileDesc.removeSyncLocation(syncAttributes);
return eventMgr.onSyncRemoved(fileDesc, syncAttributes);
}
var file = change.file;
var parsedContent = gdriveProvider.parseContent(file.content);
var remoteContent = parsedContent.content;
var remoteTitle = file.title;
var remoteDiscussionListJSON = parsedContent.discussionListJSON;
var remoteDiscussionList = parsedContent.discussionList;
var remoteCRC = gdriveProvider.syncMerge(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList, remoteDiscussionListJSON);
gdriveProvider.publish = function(publishAttributes, frontMatter, title, content, callback) { // Update syncAttributes
var contentType = publishAttributes.format != "markdown" ? 'text/html' : undefined; syncAttributes.etag = file.etag;
googleHelper.upload(publishAttributes.id, undefined, publishAttributes.fileName || title, content, contentType, undefined, accountId, function(error, result) { if(merge === true) {
if(error) { // Need to store the whole content for merge
callback(error); syncAttributes.content = remoteContent;
return; syncAttributes.title = remoteTitle;
} syncAttributes.discussionList = remoteDiscussionListJSON;
publishAttributes.id = result.id; }
callback(); syncAttributes.contentCRC = remoteCRC.contentCRC;
}); syncAttributes.titleCRC = remoteCRC.titleCRC;
}; syncAttributes.discussionListCRC = remoteCRC.discussionListCRC;
utils.storeAttributes(syncAttributes);
setTimeout(mergeChange, 5);
}
gdriveProvider.newPublishAttributes = function(event) { setTimeout(mergeChange, 5);
var publishAttributes = {}; });
publishAttributes.id = utils.getInputTextValue('#input-publish-' + providerId + '-fileid'); });
publishAttributes.fileName = utils.getInputTextValue('#input-publish-' + providerId + '-filename'); };
if(event.isPropagationStopped()) {
return undefined;
}
return publishAttributes;
};
// Initialize the AutoSync dialog fields gdriveProvider.publish = function(publishAttributes, frontMatter, title, content, callback) {
gdriveProvider.setAutosyncDialogConfig = function() { var contentType = publishAttributes.format != "markdown" ? 'text/html' : undefined;
var config = gdriveProvider.autosyncConfig; googleHelper.upload(publishAttributes.id, undefined, publishAttributes.fileName || title, content, contentType, undefined, accountId, function(error, result) {
utils.setInputRadio('radio-autosync-' + providerId + '-mode', config.mode || 'off'); if(error) {
utils.setInputValue('#input-autosync-' + providerId + '-parentid', config.parentId); callback(error);
}; return;
}
publishAttributes.id = result.id;
callback();
});
};
// Retrieve the AutoSync dialog fields gdriveProvider.newPublishAttributes = function(event) {
gdriveProvider.getAutosyncDialogConfig = function() { var publishAttributes = {};
var config = {}; publishAttributes.id = utils.getInputTextValue('#input-publish-' + providerId + '-fileid');
config.mode = utils.getInputRadio('radio-autosync-' + providerId + '-mode'); publishAttributes.fileName = utils.getInputTextValue('#input-publish-' + providerId + '-filename');
config.parentId = utils.getInputTextValue('#input-autosync-' + providerId + '-parentid'); if(event.isPropagationStopped()) {
return config; return undefined;
}; }
return publishAttributes;
};
// Perform AutoSync // Initialize the AutoSync dialog fields
gdriveProvider.autosyncFile = function(title, content, discussionListJSON, config, callback) { gdriveProvider.setAutosyncDialogConfig = function() {
var parentId = config.parentId; var config = gdriveProvider.autosyncConfig;
googleHelper.upload(undefined, parentId, title, content, undefined, undefined, accountId, function(error, result) { utils.setInputRadio('radio-autosync-' + providerId + '-mode', config.mode || 'off');
if(error) { utils.setInputValue('#input-autosync-' + providerId + '-parentid', config.parentId);
callback(error); };
return;
}
var syncAttributes = createSyncAttributes(result.id, result.etag, content, title, discussionListJSON);
callback(undefined, syncAttributes);
});
};
// Disable publish on optional multi-account // Retrieve the AutoSync dialog fields
gdriveProvider.isPublishEnabled = settings.gdriveMultiAccount > accountIndex; gdriveProvider.getAutosyncDialogConfig = function() {
var config = {};
config.mode = utils.getInputRadio('radio-autosync-' + providerId + '-mode');
config.parentId = utils.getInputTextValue('#input-autosync-' + providerId + '-parentid');
return config;
};
eventMgr.addListener("onReady", function() { // Perform AutoSync
// Hide optional multi-account sub-menus gdriveProvider.autosyncFile = function(title, content, discussionListJSON, config, callback) {
$('.submenu-sync-' + providerId).toggle(settings.gdriveMultiAccount > accountIndex); var parentId = config.parentId;
googleHelper.upload(undefined, parentId, title, content, undefined, undefined, accountId, function(error, result) {
if(error) {
callback(error);
return;
}
var syncAttributes = createSyncAttributes(result.id, result.etag, content, title, discussionListJSON);
callback(undefined, syncAttributes);
});
};
// Create export dialog // Disable publish on optional multi-account
var modalUploadElt = document.querySelector('.modal-upload-' + providerId); gdriveProvider.isPublishEnabled = settings.gdriveMultiAccount > accountIndex;
modalUploadElt && (modalUploadElt.innerHTML = _.template(dialogExportGdriveHTML, {
providerId: providerId,
providerName: providerName
}));
// Create autosync dialog eventMgr.addListener("onReady", function() {
var modalAutosyncElt = document.querySelector('.modal-autosync-' + providerId); // Hide optional multi-account sub-menus
modalAutosyncElt && (modalAutosyncElt.innerHTML = _.template(dialogAutoSyncGdriveHTML, { $('.submenu-sync-' + providerId).toggle(settings.gdriveMultiAccount > accountIndex);
providerId: providerId,
providerName: providerName
}));
// Choose folder button in export modal // Create export dialog
$('.action-export-' + providerId + '-choose-folder').click(function() { var modalUploadElt = document.querySelector('.modal-upload-' + providerId);
googleHelper.picker(function(error, docs) { modalUploadElt && (modalUploadElt.innerHTML = _.template(dialogExportGdriveHTML, {
if(error || docs.length === 0) { providerId: providerId,
return; providerName: providerName
} }));
// Open export dialog
$(".modal-upload-" + providerId).modal();
// Set parent ID
utils.setInputValue('#input-sync-export-' + providerId + '-parentid', docs[0].id);
}, 'folder', accountId);
});
// Choose folder button in autosync modal // Create autosync dialog
$('.action-autosync-' + providerId + '-choose-folder').click(function() { var modalAutosyncElt = document.querySelector('.modal-autosync-' + providerId);
googleHelper.picker(function(error, docs) { modalAutosyncElt && (modalAutosyncElt.innerHTML = _.template(dialogAutoSyncGdriveHTML, {
if(error || docs.length === 0) { providerId: providerId,
return; providerName: providerName
} }));
// Open export dialog
$(".modal-autosync-" + providerId).modal();
// Set parent ID
utils.setInputValue('#input-autosync-' + providerId + '-parentid', docs[0].id);
}, 'folder', accountId);
});
$('.action-remove-google-drive-state').click(function() { // Choose folder button in export modal
storage.removeItem('gdrive.state'); $('.action-export-' + providerId + '-choose-folder').click(function() {
}); googleHelper.picker(function(error, docs) {
if(error || docs.length === 0) {
return;
}
// Open export dialog
$(".modal-upload-" + providerId).modal();
// Set parent ID
utils.setInputValue('#input-sync-export-' + providerId + '-parentid', docs[0].id);
}, 'folder', accountId);
});
// Skip gdrive action if provider is not enabled in the settings // Choose folder button in autosync modal
if(accountIndex >= settings.gdriveMultiAccount) { $('.action-autosync-' + providerId + '-choose-folder').click(function() {
return; googleHelper.picker(function(error, docs) {
} if(error || docs.length === 0) {
var state = utils.retrieveIgnoreError('gdrive.state'); return;
var userId = storage[accountId + '.userId']; }
if(state === undefined) { // Open export dialog
return; $(".modal-autosync-" + providerId).modal();
} // Set parent ID
if(userId && state.userId != userId) { utils.setInputValue('#input-autosync-' + providerId + '-parentid', docs[0].id);
if(accountIndex === settings.gdriveMultiAccount - 1) { }, 'folder', accountId);
if(settings.gdriveMultiAccount === 3) { });
eventMgr.onError('None of your 3 Google Drive accounts is able to perform this request.');
storage.removeItem('gdrive.state');
}
else {
$(".modal-add-google-drive-account").modal();
}
}
return;
}
storage.removeItem('gdrive.state'); $('.action-remove-google-drive-state').click(function() {
if(state.action == "create") { storage.removeItem('gdrive.state');
googleHelper.upload(undefined, state.folderId, constants.GDRIVE_DEFAULT_FILE_TITLE, settings.defaultContent, undefined, undefined, accountId, function(error, file) { });
if(error) {
return;
}
var syncAttributes = createSyncAttributes(file.id, file.etag, file.content, file.title);
var syncLocations = {};
syncLocations[syncAttributes.syncIndex] = syncAttributes;
var fileDesc = fileMgr.createFile(file.title, file.content, undefined, syncLocations);
fileMgr.selectFile(fileDesc);
eventMgr.onMessage('"' + file.title + '" created successfully on ' + providerName + '.');
});
}
else if(state.action == "open") {
var importIds = [];
_.each(state.ids, function(id) {
var syncIndex = createSyncIndex(id);
var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
if(fileDesc !== undefined) {
fileDesc !== fileMgr.currentFile && fileMgr.selectFile(fileDesc);
}
else {
importIds.push(id);
}
});
importFilesFromIds(importIds);
}
});
return gdriveProvider; // Skip gdrive action if provider is not enabled in the settings
}; if(accountIndex >= settings.gdriveMultiAccount) {
return;
}
var state = utils.retrieveIgnoreError('gdrive.state');
var userId = storage[accountId + '.userId'];
if(state === undefined) {
return;
}
if(userId && state.userId != userId) {
if(accountIndex === settings.gdriveMultiAccount - 1) {
if(settings.gdriveMultiAccount === 3) {
eventMgr.onError('None of your 3 Google Drive accounts is able to perform this request.');
storage.removeItem('gdrive.state');
}
else {
$(".modal-add-google-drive-account").modal();
}
}
return;
}
storage.removeItem('gdrive.state');
if(state.action == "create") {
eventMgr.onMessage('Please wait while creating your document on ' + providerName);
googleHelper.upload(undefined, state.folderId, constants.GDRIVE_DEFAULT_FILE_TITLE, settings.defaultContent, undefined, undefined, accountId, function(error, file) {
if(error) {
return;
}
var syncAttributes = createSyncAttributes(file.id, file.etag, file.content, file.title);
var syncLocations = {};
syncLocations[syncAttributes.syncIndex] = syncAttributes;
var fileDesc = fileMgr.createFile(file.title, file.content, undefined, syncLocations);
fileMgr.selectFile(fileDesc);
eventMgr.onMessage('"' + file.title + '" created successfully on ' + providerName + '.');
});
}
else if(state.action == "open") {
var importIds = [];
_.each(state.ids, function(id) {
var syncIndex = createSyncIndex(id);
var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
if(fileDesc !== undefined) {
fileDesc !== fileMgr.currentFile && fileMgr.selectFile(fileDesc);
}
else {
importIds.push(id);
}
});
eventMgr.onMessage('Please wait while loading your document from ' + providerName);
importFilesFromIds(importIds);
}
});
return gdriveProvider;
};
}); });

View File

@ -188,7 +188,7 @@ define([
function createPublishIndex(fileDesc, publishAttributes) { function createPublishIndex(fileDesc, publishAttributes) {
var publishIndex; var publishIndex;
do { do {
publishIndex = "publish." + utils.randomString(); publishIndex = "publish." + utils.id();
} while(_.has(storage, publishIndex)); } while(_.has(storage, publishIndex));
publishAttributes.publishIndex = publishIndex; publishAttributes.publishIndex = publishIndex;
fileDesc.addPublishLocation(publishAttributes); fileDesc.addPublishLocation(publishAttributes);

View File

@ -455,6 +455,10 @@ kbd {
background-position: -144px 0; background-position: -144px 0;
} }
.icon-provider-couchdb {
background-position: -162px 0;
}
/******************* /*******************
* RTL * RTL
*******************/ *******************/

View File

@ -758,6 +758,22 @@ a {
} }
} }
} }
.file-list .list-group-item {
background-color: @transparent;
padding: 0 3px;
}
margin-bottom: 0;
.input-rename {
width: 220px;
height: @input-height-slim;
}
}
.modal {
.list-group .list-group-item {
border-radius: @border-radius-base;
}
.list-group-item { .list-group-item {
padding: 3px; padding: 3px;
margin: 0; margin: 0;
@ -778,28 +794,18 @@ a {
} }
} }
} }
.file-list .list-group-item { .folder, .document {
background-color: @transparent;
padding: 0 3px;
}
margin-bottom: 0;
.list-group .list-group-item {
border-radius: @border-radius-base;
}
.folder {
font-weight: bold; font-weight: bold;
color: @folder-color; color: @folder-color;
font-size: 15px; font-size: 15px;
background-color: @transparent; background-color: @transparent;
} }
.input-rename { .name, .date, .file-count {
width: 220px;
height: @input-height-slim;
}
.name, .file-count {
padding: 9px 20px 9px 15px; padding: 9px 20px 9px 15px;
} }
.date {
font-weight: normal;
}
} }
@ -1323,6 +1329,9 @@ a {
.layout-animate & { .layout-animate & {
.transition(350ms ease-in-out all); .transition(350ms ease-in-out all);
} }
.layout-vertical & {
.box-shadow(inset 0 1px fade(@secondary, 6%));
}
} }
#preview-contents { #preview-contents {

View File

@ -1,371 +1,386 @@
define([ define([
"jquery", "jquery",
"underscore", "underscore",
"utils", "utils",
"storage", "storage",
"eventMgr", "eventMgr",
"fileSystem", "fileSystem",
"fileMgr", "fileMgr",
"classes/Provider", "classes/Provider",
"providers/dropboxProvider", "providers/dropboxProvider",
"providers/gdriveProvider", "providers/couchdbProvider",
"providers/gdrivesecProvider", "providers/gdriveProvider",
"providers/gdriveterProvider" "providers/gdrivesecProvider",
"providers/gdriveterProvider"
], function($, _, utils, storage, eventMgr, fileSystem, fileMgr, Provider) { ], function($, _, utils, storage, eventMgr, fileSystem, fileMgr, Provider) {
var synchronizer = {}; var synchronizer = {};
// Create a map with providerId: providerModule // Create a map with providerId: providerModule
var providerMap = _.chain(arguments).map(function(argument) { var providerMap = _.chain(arguments).map(function(argument) {
return argument instanceof Provider && [ return argument instanceof Provider && [
argument.providerId, argument.providerId,
argument argument
]; ];
}).compact().object().value(); }).compact().object().value();
// Retrieve sync locations from storage // Retrieve sync locations from storage
(function() { (function() {
var syncIndexMap = {}; var syncIndexMap = {};
_.each(fileSystem, function(fileDesc) { _.each(fileSystem, function(fileDesc) {
utils.retrieveIndexArray(fileDesc.fileIndex + ".sync").forEach(function(syncIndex) { utils.retrieveIndexArray(fileDesc.fileIndex + ".sync").forEach(function(syncIndex) {
try { try {
var syncAttributes = JSON.parse(storage[syncIndex]); var syncAttributes = JSON.parse(storage[syncIndex]);
// Store syncIndex // Store syncIndex
syncAttributes.syncIndex = syncIndex; syncAttributes.syncIndex = syncIndex;
// Replace provider ID by provider module in attributes // Replace provider ID by provider module in attributes
var provider = providerMap[syncAttributes.provider]; var provider = providerMap[syncAttributes.provider];
if(!provider) { if(!provider) {
throw new Error("Invalid provider ID: " + syncAttributes.provider); throw new Error("Invalid provider ID: " + syncAttributes.provider);
} }
syncAttributes.provider = provider; syncAttributes.provider = provider;
fileDesc.syncLocations[syncIndex] = syncAttributes; fileDesc.syncLocations[syncIndex] = syncAttributes;
syncIndexMap[syncIndex] = syncAttributes; syncIndexMap[syncIndex] = syncAttributes;
} }
catch(e) { catch(e) {
// storage can be corrupted // storage can be corrupted
eventMgr.onError(e); eventMgr.onError(e);
// Remove sync location // Remove sync location
utils.removeIndexFromArray(fileDesc.fileIndex + ".sync", syncIndex); utils.removeIndexFromArray(fileDesc.fileIndex + ".sync", syncIndex);
} }
}); });
}); });
// Clean fields from deleted files in local storage // Clean fields from deleted files in local storage
Object.keys(storage).forEach(function(key) { Object.keys(storage).forEach(function(key) {
var match = key.match(/sync\.\S+/); var match = key.match(/sync\.\S+/);
if(match && !syncIndexMap.hasOwnProperty(match[0])) { if(match && !syncIndexMap.hasOwnProperty(match[0])) {
storage.removeItem(key); storage.removeItem(key);
} }
}); });
})(); })();
// AutoSync configuration // AutoSync configuration
_.each(providerMap, function(provider) { _.each(providerMap, function(provider) {
provider.autosyncConfig = utils.retrieveIgnoreError(provider.providerId + ".autosyncConfig") || {}; provider.autosyncConfig = utils.retrieveIgnoreError(provider.providerId + ".autosyncConfig") || {};
}); });
// Returns true if at least one file has synchronized location // Returns true if at least one file has synchronized location
synchronizer.hasSync = function(provider) { synchronizer.hasSync = function(provider) {
return _.some(fileSystem, function(fileDesc) { return _.some(fileSystem, function(fileDesc) {
return _.some(fileDesc.syncLocations, function(syncAttributes) { return _.some(fileDesc.syncLocations, function(syncAttributes) {
return provider === undefined || syncAttributes.provider === provider; return provider === undefined || syncAttributes.provider === provider;
}); });
}); });
}; };
/*************************************************************************** /***************************************************************************
* Synchronization * Synchronization
**************************************************************************/ **************************************************************************/
// Entry point for up synchronization (upload changes) // Entry point for up synchronization (upload changes)
var uploadCycle = false; var uploadCycle = false;
function syncUp(callback) {
var uploadFileList = [];
// Recursive function to upload multiple files function syncUp(callback) {
function fileUp() { var uploadFileList = [];
// No more fileDesc to synchronize
if(uploadFileList.length === 0) {
return syncUp(callback);
}
// Dequeue a fileDesc to synchronize // Recursive function to upload multiple files
var fileDesc = uploadFileList.pop(); function fileUp() {
var uploadSyncAttributesList = _.values(fileDesc.syncLocations); // No more fileDesc to synchronize
if(uploadSyncAttributesList.length === 0) { if(uploadFileList.length === 0) {
return fileUp(); return syncUp(callback);
} }
var uploadContent = fileDesc.content; // Dequeue a fileDesc to synchronize
var uploadContentCRC = utils.crc32(uploadContent); var fileDesc = uploadFileList.pop();
var uploadTitle = fileDesc.title; var uploadSyncAttributesList = _.values(fileDesc.syncLocations);
var uploadTitleCRC = utils.crc32(uploadTitle); if(uploadSyncAttributesList.length === 0) {
var uploadDiscussionList = fileDesc.discussionListJSON; return fileUp();
var uploadDiscussionListCRC = utils.crc32(uploadDiscussionList); }
// Recursive function to upload a single file on multiple locations // Here we are freezing the data to make sure it's uploaded consistently
function locationUp() { var uploadContent = fileDesc.content;
var uploadContentCRC = utils.crc32(uploadContent);
var uploadTitle = fileDesc.title;
var uploadTitleCRC = utils.crc32(uploadTitle);
var uploadDiscussionList = fileDesc.discussionListJSON;
var uploadDiscussionListCRC = utils.crc32(uploadDiscussionList);
var uploadFrontMatter = fileDesc.frontMatter;
// No more synchronized location for this document // Recursive function to upload a single file on multiple locations
if(uploadSyncAttributesList.length === 0) { function locationUp() {
return fileUp();
}
// Dequeue a synchronized location // No more synchronized location for this document
var syncAttributes = uploadSyncAttributesList.pop(); if(uploadSyncAttributesList.length === 0) {
return fileUp();
}
syncAttributes.provider.syncUp( // Dequeue a synchronized location
uploadContent, var syncAttributes = uploadSyncAttributesList.pop();
uploadContentCRC,
uploadTitle,
uploadTitleCRC,
uploadDiscussionList,
uploadDiscussionListCRC,
syncAttributes,
function(error, uploadFlag) {
if(uploadFlag === true) {
// If uploadFlag is true, request another upload cycle
uploadCycle = true;
}
if(error) {
return callback(error);
}
if(uploadFlag) {
// Update syncAttributes in storage
utils.storeAttributes(syncAttributes);
}
locationUp();
}
);
}
locationUp();
}
if(uploadCycle === true) { syncAttributes.provider.syncUp(
// New upload cycle uploadContent,
uploadCycle = false; uploadContentCRC,
uploadFileList = _.values(fileSystem); uploadTitle,
fileUp(); uploadTitleCRC,
} uploadDiscussionList,
else { uploadDiscussionListCRC,
callback(); uploadFrontMatter,
} syncAttributes,
} function(error, uploadFlag) {
if(uploadFlag === true) {
// If uploadFlag is true, request another upload cycle
uploadCycle = true;
}
if(error) {
return callback(error);
}
if(uploadFlag) {
// Update syncAttributes in storage
utils.storeAttributes(syncAttributes);
}
locationUp();
}
);
}
// Entry point for down synchronization (download changes) locationUp();
function syncDown(callback) { }
var providerList = _.values(providerMap);
// Recursive function to download changes from multiple providers if(uploadCycle === true) {
function providerDown() { // New upload cycle
if(providerList.length === 0) { uploadCycle = false;
return callback(); uploadFileList = _.values(fileSystem);
} fileUp();
var provider = providerList.pop(); }
else {
callback();
}
}
// Check that provider has files to sync // Entry point for down synchronization (download changes)
if(!synchronizer.hasSync(provider)) { function syncDown(callback) {
return providerDown(); var providerList = _.values(providerMap);
}
// Perform provider's syncDown // Recursive function to download changes from multiple providers
provider.syncDown(function(error) { function providerDown() {
if(error) { if(providerList.length === 0) {
return callback(error); return callback();
} }
providerDown(); var provider = providerList.pop();
});
}
providerDown();
}
// Entry point for the autosync feature // Check that provider has files to sync
function autosyncAll(callback) { if(!synchronizer.hasSync(provider)) {
var autosyncFileList = _.filter(fileSystem, function(fileDesc) { return providerDown();
return _.size(fileDesc.syncLocations) === 0; }
});
// Recursive function to autosync multiple files // Perform provider's syncDown
function fileAutosync() { provider.syncDown(function(error) {
// No more fileDesc to synchronize if(error) {
if(autosyncFileList.length === 0) { return callback(error);
return callback(); }
} providerDown();
var fileDesc = autosyncFileList.pop(); });
}
var providerList = _.filter(providerMap, function(provider) { providerDown();
return provider.autosyncConfig.mode == 'all'; }
});
function providerAutosync() {
// No more provider
if(providerList.length === 0) {
return fileAutosync();
}
var provider = providerList.pop();
provider.autosyncFile(fileDesc.title, fileDesc.content, fileDesc.discussionListJSON, provider.autosyncConfig, function(error, syncAttributes) { // Entry point for the autosync feature
if(error) { function autosyncAll(callback) {
return callback(error); var autosyncFileList = _.filter(fileSystem, function(fileDesc) {
} return _.size(fileDesc.syncLocations) === 0;
fileDesc.addSyncLocation(syncAttributes); });
eventMgr.onSyncExportSuccess(fileDesc, syncAttributes);
providerAutosync();
});
}
providerAutosync(); // Recursive function to autosync multiple files
} function fileAutosync() {
// No more fileDesc to synchronize
if(autosyncFileList.length === 0) {
return callback();
}
var fileDesc = autosyncFileList.pop();
fileAutosync(); var providerList = _.filter(providerMap, function(provider) {
} return provider.autosyncConfig.mode == 'all';
});
// Listen to offline status changes function providerAutosync() {
var isOffline = false; // No more provider
eventMgr.addListener("onOfflineChanged", function(isOfflineParam) { if(providerList.length === 0) {
isOffline = isOfflineParam; return fileAutosync();
}); }
var provider = providerList.pop();
// Main entry point for synchronization provider.autosyncFile(fileDesc.title, fileDesc.content, fileDesc.discussionListJSON, provider.autosyncConfig, function(error, syncAttributes) {
var syncRunning = false; if(error) {
synchronizer.sync = function() { return callback(error);
// If sync is already running or offline }
if(syncRunning === true || isOffline === true) { fileDesc.addSyncLocation(syncAttributes);
return false; eventMgr.onSyncExportSuccess(fileDesc, syncAttributes);
} providerAutosync();
syncRunning = true; });
eventMgr.onSyncRunning(true); }
uploadCycle = true;
function isError(error) { providerAutosync();
if(error !== undefined) { }
syncRunning = false;
eventMgr.onSyncRunning(false);
return true;
}
return false;
}
autosyncAll(function(error) { fileAutosync();
if(isError(error)) { }
return;
}
syncDown(function(error) {
if(isError(error)) {
return;
}
syncUp(function(error) {
if(isError(error)) {
return;
}
syncRunning = false;
eventMgr.onSyncRunning(false);
eventMgr.onSyncSuccess();
});
});
});
return true;
};
/*************************************************************************** // Listen to offline status changes
* Initialize module var isOffline = false;
**************************************************************************/ eventMgr.addListener("onOfflineChanged", function(isOfflineParam) {
isOffline = isOfflineParam;
});
// Initialize the export dialog // Main entry point for synchronization
function initExportDialog(provider) { var syncRunning = false;
synchronizer.sync = function() {
// If sync is already running or offline
if(syncRunning === true || isOffline === true) {
return false;
}
syncRunning = true;
eventMgr.onSyncRunning(true);
uploadCycle = true;
// Reset fields function isError(error) {
utils.resetModalInputs(); if(error !== undefined) {
syncRunning = false;
eventMgr.onSyncRunning(false);
return true;
}
return false;
}
// Load preferences autosyncAll(function(error) {
var exportPreferences = utils.retrieveIgnoreError(provider.providerId + ".exportPreferences"); if(isError(error)) {
if(exportPreferences) { return;
_.each(provider.exportPreferencesInputIds, function(inputId) { }
var exportPreferenceValue = exportPreferences[inputId]; syncDown(function(error) {
if(_.isBoolean(exportPreferenceValue)) { if(isError(error)) {
utils.setInputChecked("#input-sync-export-" + inputId, exportPreferenceValue); return;
} }
else { syncUp(function(error) {
utils.setInputValue("#input-sync-export-" + inputId, exportPreferenceValue); if(isError(error)) {
} return;
}); }
} syncRunning = false;
eventMgr.onSyncRunning(false);
eventMgr.onSyncSuccess();
});
});
});
return true;
};
// Open dialog /***************************************************************************
$(".modal-upload-" + provider.providerId).modal(); * Initialize module
} **************************************************************************/
eventMgr.addListener("onFileCreated", function(fileDesc) { function loadPreferences(provider, action) {
if(_.size(fileDesc.syncLocations) === 0) { utils.resetModalInputs();
_.each(providerMap, function(provider) { var preferences = utils.retrieveIgnoreError(provider.providerId + '.' + action + 'Preferences');
if(provider.autosyncConfig.mode != 'new') { if(preferences) {
return; _.each(provider.exportPreferencesInputIds, function(inputId) {
} var exportPreferenceValue = preferences[inputId];
provider.autosyncFile(fileDesc.title, fileDesc.content, fileDesc.discussionListJSON, provider.autosyncConfig, function(error, syncAttributes) { var setValue = utils.setInputValue;
if(error) { if(_.isBoolean(exportPreferenceValue)) {
return; setValue = utils.setInputChecked;
} }
fileDesc.addSyncLocation(syncAttributes); setValue('#input-sync-' + action + '-' + inputId, exportPreferenceValue);
eventMgr.onSyncExportSuccess(fileDesc, syncAttributes); });
}); }
}); }
}
});
eventMgr.addListener("onReady", function() { // Initialize the import dialog
// Init each provider function initImportDialog(provider) {
_.each(providerMap, function(provider) { loadPreferences(provider, 'import');
// Provider's import button $(".modal-download-" + provider.providerId).modal();
$(".action-sync-import-" + provider.providerId).click(function(event) { }
provider.importFiles(event);
});
// Provider's export action
$(".action-sync-export-dialog-" + provider.providerId).click(function() {
initExportDialog(provider);
});
// Provider's autosync action
$(".action-autosync-dialog-" + provider.providerId).click(function() {
// Reset fields
utils.resetModalInputs();
// Load config
provider.setAutosyncDialogConfig(provider);
// Open dialog
$(".modal-autosync-" + provider.providerId).modal();
});
$(".action-sync-export-" + provider.providerId).click(function(event) {
var fileDesc = fileMgr.currentFile;
provider.exportFile(event, fileDesc.title, fileDesc.content, fileDesc.discussionListJSON, function(error, syncAttributes) { // Initialize the export dialog
if(error) { function initExportDialog(provider) {
return; loadPreferences(provider, 'export');
} $(".modal-upload-" + provider.providerId).modal();
fileDesc.addSyncLocation(syncAttributes); }
eventMgr.onSyncExportSuccess(fileDesc, syncAttributes);
});
// Store input values as preferences for next time we open the eventMgr.addListener("onFileCreated", function(fileDesc) {
// export dialog if(_.size(fileDesc.syncLocations) === 0) {
var exportPreferences = {}; _.each(providerMap, function(provider) {
_.each(provider.exportPreferencesInputIds, function(inputId) { if(provider.autosyncConfig.mode != 'new') {
var inputElt = document.getElementById("input-sync-export-" + inputId); return;
if(inputElt.type == 'checkbox') { }
exportPreferences[inputId] = inputElt.checked; provider.autosyncFile(fileDesc.title, fileDesc.content, fileDesc.discussionListJSON, provider.autosyncConfig, function(error, syncAttributes) {
} if(error) {
else { return;
exportPreferences[inputId] = inputElt.value; }
} fileDesc.addSyncLocation(syncAttributes);
}); eventMgr.onSyncExportSuccess(fileDesc, syncAttributes);
storage[provider.providerId + ".exportPreferences"] = JSON.stringify(exportPreferences); });
}); });
$(".action-autosync-" + provider.providerId).click(function(event) { }
var config = provider.getAutosyncDialogConfig(event); });
if(config !== undefined) {
storage[provider.providerId + ".autosyncConfig"] = JSON.stringify(config);
provider.autosyncConfig = config;
}
});
});
});
eventMgr.onSynchronizerCreated(synchronizer); eventMgr.addListener("onReady", function() {
return synchronizer; // Init each provider
_.each(providerMap, function(provider) {
// Provider's import button
$(".action-sync-import-" + provider.providerId).click(function(event) {
provider.importFiles(event);
});
// Provider's import dialog action
$(".action-sync-import-dialog-" + provider.providerId).click(function() {
initImportDialog(provider);
});
// Provider's export dialog action
$(".action-sync-export-dialog-" + provider.providerId).click(function() {
initExportDialog(provider);
});
// Provider's autosync action
$(".action-autosync-dialog-" + provider.providerId).click(function() {
// Reset fields
utils.resetModalInputs();
// Load config
provider.setAutosyncDialogConfig(provider);
// Open dialog
$(".modal-autosync-" + provider.providerId).modal();
});
$(".action-sync-export-" + provider.providerId).click(function(event) {
var fileDesc = fileMgr.currentFile;
provider.exportFile(event, fileDesc.title, fileDesc.content, fileDesc.discussionListJSON, fileDesc.frontMatter, function(error, syncAttributes) {
if(error) {
return;
}
fileDesc.addSyncLocation(syncAttributes);
eventMgr.onSyncExportSuccess(fileDesc, syncAttributes);
});
// Store input values as preferences for next time we open the
// export dialog
var exportPreferences = {};
_.each(provider.exportPreferencesInputIds, function(inputId) {
var inputElt = document.getElementById("input-sync-export-" + inputId);
if(inputElt.type == 'checkbox') {
exportPreferences[inputId] = inputElt.checked;
}
else {
exportPreferences[inputId] = inputElt.value;
}
});
storage[provider.providerId + ".exportPreferences"] = JSON.stringify(exportPreferences);
});
$(".action-autosync-" + provider.providerId).click(function(event) {
var config = provider.getAutosyncDialogConfig(event);
if(config !== undefined) {
storage[provider.providerId + ".autosyncConfig"] = JSON.stringify(config);
provider.autosyncConfig = config;
}
});
});
});
eventMgr.onSynchronizerCreated(synchronizer);
return synchronizer;
}); });

View File

@ -87,24 +87,15 @@ define([
}; };
}; };
// Generates a 24 chars length random string (should be enough to prevent collisions) // Generates a 24 char length random id
utils.randomString = (function() { var idAlphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
var max = Math.pow(36, 6); utils.id = function() {
var result = [];
function s6() { for(var i = 0; i < 24; i++) {
// Linear [0-9a-z]{6} random string result.push(idAlphabet[Math.random() * idAlphabet.length | 0]);
return ('000000' + (Math.random() * max | 0).toString(36)).slice(-6);
} }
return result.join('');
return function() { };
return [
s6(),
s6(),
s6(),
s6()
].join('');
};
})();
// Return a parameter from the URL // Return a parameter from the URL
utils.getURLParameter = function(name) { utils.getURLParameter = function(name) {
@ -549,6 +540,122 @@ define([
return result.join(""); return result.join("");
}; };
function padNumber(num, digits, trim) {
var neg = '';
if (num < 0) {
neg = '-';
num = -num;
}
num = '' + num;
while(num.length < digits) {
num = '0' + num;
}
if (trim) {
num = num.substr(num.length - digits);
}
return neg + num;
}
function dateGetter(name, size, offset, trim) {
offset = offset || 0;
return function(date) {
var value = date['get' + name]();
if (offset > 0 || value > -offset) {
value += offset;
}
if (value === 0 && offset == -12 ) {
value = 12;
}
return padNumber(value, size, trim);
};
}
function dateStrGetter(name, shortForm) {
return function(date, formats) {
var value = date['get' + name]();
var get = (shortForm ? ('SHORT' + name) : name).toUpperCase();
return formats[get][value];
};
}
var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/;
var DATE_FORMATS = {
yyyy: dateGetter('FullYear', 4),
yy: dateGetter('FullYear', 2, 0, true),
y: dateGetter('FullYear', 1),
MMMM: dateStrGetter('Month'),
MMM: dateStrGetter('Month', true),
MM: dateGetter('Month', 2, 1),
M: dateGetter('Month', 1, 1),
dd: dateGetter('Date', 2),
d: dateGetter('Date', 1),
HH: dateGetter('Hours', 2),
H: dateGetter('Hours', 1),
hh: dateGetter('Hours', 2, -12),
h: dateGetter('Hours', 1, -12),
mm: dateGetter('Minutes', 2),
m: dateGetter('Minutes', 1),
ss: dateGetter('Seconds', 2),
s: dateGetter('Seconds', 1),
// while ISO 8601 requires fractions to be prefixed with `.` or `,`
// we can be just safely rely on using `sss` since we currently don't support single or two digit fractions
sss: dateGetter('Milliseconds', 3),
EEEE: dateStrGetter('Day'),
EEE: dateStrGetter('Day', true)
};
var DATETIME_FORMATS = {
MONTH: 'January,February,March,April,May,June,July,August,September,October,November,December'.split(','),
SHORTMONTH: 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','),
DAY: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(','),
SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','),
AMPMS: ['AM','PM'],
medium: 'MMM d, y h:mm:ss a',
short: 'M/d/yy h:mm a',
fullDate: 'EEEE, MMMM d, y',
longDate: 'MMMM d, y',
mediumDate: 'MMM d, y',
shortDate: 'M/d/yy',
mediumTime: 'h:mm:ss a',
shortTime: 'h:mm a'
};
utils.formatDate = function(date) {
var text = '',
parts = [],
fn, match;
var interval = Date.now() - date;
var format = 'HH:mm';
if(interval > 31556940000) {
format = 'y';
}
else if(interval > 86400000) {
format = 'MMM d';
}
date = new Date(date);
while(format) {
match = DATE_FORMATS_SPLIT.exec(format);
if (match) {
parts = parts.concat(match.slice(1));
format = parts.pop();
} else {
parts.push(format);
format = null;
}
}
parts.forEach(function(value){
fn = DATE_FORMATS[value];
text += fn ? fn(date, DATETIME_FORMATS)
: value.replace(/(^'|'$)/g, '').replace(/''/g, "'");
});
return text;
};
// Base64 conversion // Base64 conversion
utils.encodeBase64 = function(str) { utils.encodeBase64 = function(str) {
if(str.length === 0) { if(str.length === 0) {
@ -604,6 +711,10 @@ define([
return x.join(''); return x.join('');
}; };
utils.decodeBase64 = function(str) {
return window.unescape(decodeURIComponent(window.atob(str)));
};
// CRC32 algorithm // CRC32 algorithm
var mHash = [ var mHash = [
0, 0,

View File

@ -307,7 +307,7 @@
<div class="col-md-4 col-md-offset-2"> <div class="col-md-4 col-md-offset-2">
<h2 id="fully-customizable">Fully customizable</h2> <h2 id="fully-customizable">Fully customizable</h2>
<p>StackEdit has an infinite combinations of settings. Theme, layout, shortcuts can be <p>StackEdit offers infinite combinations of settings. Theme, layout, shortcuts can be
personalized. For the rest, StackEdit gives you the freedom to make your own extension…</p> personalized. For the rest, StackEdit gives you the freedom to make your own extension…</p>
</div> </div>
</div> </div>