CouchDB sync part 1
This commit is contained in:
		
							parent
							
								
									2be4919853
								
							
						
					
					
						commit
						8f5da7998f
					
				| @ -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
									
								
							
							
						
						
									
										144
									
								
								couchdb/setup-db.js
									
									
									
									
									
										Normal 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'); | ||||||
|  | 	} | ||||||
|  | }); | ||||||
| @ -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; | ||||||
|  | |||||||
| @ -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/"; | ||||||
|  | |||||||
| @ -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; | ||||||
|  | |||||||
| @ -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; | ||||||
|  | |||||||
| @ -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; | ||||||
|  | |||||||
| @ -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)); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										248
									
								
								public/res/helpers/couchdbHelper.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										248
									
								
								public/res/helpers/couchdbHelper.js
									
									
									
									
									
										Normal 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; | ||||||
|  | }); | ||||||
| @ -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">×</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">×</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"> | ||||||
|  | |||||||
| @ -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 | 
							
								
								
									
										297
									
								
								public/res/providers/couchdbProvider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								public/res/providers/couchdbProvider.js
									
									
									
									
									
										Normal 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; | ||||||
|  | }); | ||||||
| @ -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
 | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ define([ | |||||||
| 	"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) { | ||||||
| @ -29,6 +29,7 @@ define([ | |||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		var merge = settings.conflictMode == 'merge'; | 		var merge = settings.conflictMode == 'merge'; | ||||||
|  | 
 | ||||||
| 		function createSyncAttributes(id, etag, content, title, discussionListJSON) { | 		function createSyncAttributes(id, etag, content, title, discussionListJSON) { | ||||||
| 			discussionListJSON = discussionListJSON || '{}'; | 			discussionListJSON = discussionListJSON || '{}'; | ||||||
| 			var syncAttributes = {}; | 			var syncAttributes = {}; | ||||||
| @ -99,7 +100,7 @@ define([ | |||||||
| 			}, 'doc', accountId); | 			}, 'doc', accountId); | ||||||
| 		}; | 		}; | ||||||
| 
 | 
 | ||||||
|         gdriveProvider.exportFile = function(event, title, content, discussionListJSON, callback) { | 		gdriveProvider.exportFile = function(event, title, content, discussionListJSON, frontMatter, callback) { | ||||||
| 			var fileId = utils.getInputTextValue('#input-sync-export-' + providerId + '-fileid'); | 			var fileId = utils.getInputTextValue('#input-sync-export-' + providerId + '-fileid'); | ||||||
| 			if(fileId) { | 			if(fileId) { | ||||||
| 				// Check that file is not synchronized with another an existing
 | 				// Check that file is not synchronized with another an existing
 | ||||||
| @ -122,7 +123,7 @@ define([ | |||||||
| 			}); | 			}); | ||||||
| 		}; | 		}; | ||||||
| 
 | 
 | ||||||
|         gdriveProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, syncAttributes, callback) { | 		gdriveProvider.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.titleCRC == titleCRC) && // Title CRC hasn't changed
 | 				(syncAttributes.titleCRC == titleCRC) && // Title CRC hasn't changed
 | ||||||
| @ -227,6 +228,7 @@ define([ | |||||||
| 						utils.storeAttributes(syncAttributes); | 						utils.storeAttributes(syncAttributes); | ||||||
| 						setTimeout(mergeChange, 5); | 						setTimeout(mergeChange, 5); | ||||||
| 					} | 					} | ||||||
|  | 
 | ||||||
| 					setTimeout(mergeChange, 5); | 					setTimeout(mergeChange, 5); | ||||||
| 				}); | 				}); | ||||||
| 			}); | 			}); | ||||||
| @ -357,6 +359,7 @@ define([ | |||||||
| 
 | 
 | ||||||
| 			storage.removeItem('gdrive.state'); | 			storage.removeItem('gdrive.state'); | ||||||
| 			if(state.action == "create") { | 			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) { | 				googleHelper.upload(undefined, state.folderId, constants.GDRIVE_DEFAULT_FILE_TITLE, settings.defaultContent, undefined, undefined, accountId, function(error, file) { | ||||||
| 					if(error) { | 					if(error) { | ||||||
| 						return; | 						return; | ||||||
| @ -381,6 +384,7 @@ define([ | |||||||
| 						importIds.push(id); | 						importIds.push(id); | ||||||
| 					} | 					} | ||||||
| 				}); | 				}); | ||||||
|  | 				eventMgr.onMessage('Please wait while loading your document from ' + providerName); | ||||||
| 				importFilesFromIds(importIds); | 				importFilesFromIds(importIds); | ||||||
| 			} | 			} | ||||||
| 		}); | 		}); | ||||||
|  | |||||||
| @ -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); | ||||||
|  | |||||||
| @ -455,6 +455,10 @@ kbd { | |||||||
| 	background-position: -144px 0; | 	background-position: -144px 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .icon-provider-couchdb { | ||||||
|  | 	background-position: -162px 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /******************* | /******************* | ||||||
|  * RTL |  * RTL | ||||||
|  *******************/ |  *******************/ | ||||||
|  | |||||||
| @ -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 { | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ define([ | |||||||
| 	"fileMgr", | 	"fileMgr", | ||||||
| 	"classes/Provider", | 	"classes/Provider", | ||||||
| 	"providers/dropboxProvider", | 	"providers/dropboxProvider", | ||||||
|  | 	"providers/couchdbProvider", | ||||||
| 	"providers/gdriveProvider", | 	"providers/gdriveProvider", | ||||||
| 	"providers/gdrivesecProvider", | 	"providers/gdrivesecProvider", | ||||||
| 	"providers/gdriveterProvider" | 	"providers/gdriveterProvider" | ||||||
| @ -79,6 +80,7 @@ define([ | |||||||
| 
 | 
 | ||||||
| 	// Entry point for up synchronization (upload changes)
 | 	// Entry point for up synchronization (upload changes)
 | ||||||
| 	var uploadCycle = false; | 	var uploadCycle = false; | ||||||
|  | 
 | ||||||
| 	function syncUp(callback) { | 	function syncUp(callback) { | ||||||
| 		var uploadFileList = []; | 		var uploadFileList = []; | ||||||
| 
 | 
 | ||||||
| @ -96,12 +98,14 @@ define([ | |||||||
| 				return fileUp(); | 				return fileUp(); | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  | 			// Here we are freezing the data to make sure it's uploaded consistently
 | ||||||
| 			var uploadContent = fileDesc.content; | 			var uploadContent = fileDesc.content; | ||||||
| 			var uploadContentCRC = utils.crc32(uploadContent); | 			var uploadContentCRC = utils.crc32(uploadContent); | ||||||
| 			var uploadTitle = fileDesc.title; | 			var uploadTitle = fileDesc.title; | ||||||
| 			var uploadTitleCRC = utils.crc32(uploadTitle); | 			var uploadTitleCRC = utils.crc32(uploadTitle); | ||||||
| 			var uploadDiscussionList = fileDesc.discussionListJSON; | 			var uploadDiscussionList = fileDesc.discussionListJSON; | ||||||
| 			var uploadDiscussionListCRC = utils.crc32(uploadDiscussionList); | 			var uploadDiscussionListCRC = utils.crc32(uploadDiscussionList); | ||||||
|  | 			var uploadFrontMatter = fileDesc.frontMatter; | ||||||
| 
 | 
 | ||||||
| 			// Recursive function to upload a single file on multiple locations
 | 			// Recursive function to upload a single file on multiple locations
 | ||||||
| 			function locationUp() { | 			function locationUp() { | ||||||
| @ -121,6 +125,7 @@ define([ | |||||||
| 					uploadTitleCRC, | 					uploadTitleCRC, | ||||||
| 					uploadDiscussionList, | 					uploadDiscussionList, | ||||||
| 					uploadDiscussionListCRC, | 					uploadDiscussionListCRC, | ||||||
|  | 					uploadFrontMatter, | ||||||
| 					syncAttributes, | 					syncAttributes, | ||||||
| 					function(error, uploadFlag) { | 					function(error, uploadFlag) { | ||||||
| 						if(uploadFlag === true) { | 						if(uploadFlag === true) { | ||||||
| @ -138,6 +143,7 @@ define([ | |||||||
| 					} | 					} | ||||||
| 				); | 				); | ||||||
| 			} | 			} | ||||||
|  | 
 | ||||||
| 			locationUp(); | 			locationUp(); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| @ -176,6 +182,7 @@ define([ | |||||||
| 				providerDown(); | 				providerDown(); | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
| 		providerDown(); | 		providerDown(); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -196,6 +203,7 @@ define([ | |||||||
| 			var providerList = _.filter(providerMap, function(provider) { | 			var providerList = _.filter(providerMap, function(provider) { | ||||||
| 				return provider.autosyncConfig.mode == 'all'; | 				return provider.autosyncConfig.mode == 'all'; | ||||||
| 			}); | 			}); | ||||||
|  | 
 | ||||||
| 			function providerAutosync() { | 			function providerAutosync() { | ||||||
| 				// No more provider
 | 				// No more provider
 | ||||||
| 				if(providerList.length === 0) { | 				if(providerList.length === 0) { | ||||||
| @ -270,27 +278,30 @@ define([ | |||||||
| 	 * Initialize module | 	 * Initialize module | ||||||
| 	 **************************************************************************/ | 	 **************************************************************************/ | ||||||
| 
 | 
 | ||||||
|     // Initialize the export dialog
 | 	function loadPreferences(provider, action) { | ||||||
|     function initExportDialog(provider) { |  | ||||||
| 
 |  | ||||||
|         // Reset fields
 |  | ||||||
| 		utils.resetModalInputs(); | 		utils.resetModalInputs(); | ||||||
| 
 | 		var preferences = utils.retrieveIgnoreError(provider.providerId + '.' + action + 'Preferences'); | ||||||
|         // Load preferences
 | 		if(preferences) { | ||||||
|         var exportPreferences = utils.retrieveIgnoreError(provider.providerId + ".exportPreferences"); |  | ||||||
|         if(exportPreferences) { |  | ||||||
| 			_.each(provider.exportPreferencesInputIds, function(inputId) { | 			_.each(provider.exportPreferencesInputIds, function(inputId) { | ||||||
|                 var exportPreferenceValue = exportPreferences[inputId]; | 				var exportPreferenceValue = preferences[inputId]; | ||||||
|  | 				var setValue = utils.setInputValue; | ||||||
| 				if(_.isBoolean(exportPreferenceValue)) { | 				if(_.isBoolean(exportPreferenceValue)) { | ||||||
|                     utils.setInputChecked("#input-sync-export-" + inputId, exportPreferenceValue); | 					setValue = utils.setInputChecked; | ||||||
|                 } |  | ||||||
|                 else { |  | ||||||
|                     utils.setInputValue("#input-sync-export-" + inputId, exportPreferenceValue); |  | ||||||
| 				} | 				} | ||||||
|  | 				setValue('#input-sync-' + action + '-' + inputId, exportPreferenceValue); | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
|         // Open dialog
 | 	// Initialize the import dialog
 | ||||||
|  | 	function initImportDialog(provider) { | ||||||
|  | 		loadPreferences(provider, 'import'); | ||||||
|  | 		$(".modal-download-" + provider.providerId).modal(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Initialize the export dialog
 | ||||||
|  | 	function initExportDialog(provider) { | ||||||
|  | 		loadPreferences(provider, 'export'); | ||||||
| 		$(".modal-upload-" + provider.providerId).modal(); | 		$(".modal-upload-" + provider.providerId).modal(); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -318,7 +329,11 @@ define([ | |||||||
| 			$(".action-sync-import-" + provider.providerId).click(function(event) { | 			$(".action-sync-import-" + provider.providerId).click(function(event) { | ||||||
| 				provider.importFiles(event); | 				provider.importFiles(event); | ||||||
| 			}); | 			}); | ||||||
|             // Provider's export action
 | 			// 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() { | 			$(".action-sync-export-dialog-" + provider.providerId).click(function() { | ||||||
| 				initExportDialog(provider); | 				initExportDialog(provider); | ||||||
| 			}); | 			}); | ||||||
| @ -334,7 +349,7 @@ define([ | |||||||
| 			$(".action-sync-export-" + provider.providerId).click(function(event) { | 			$(".action-sync-export-" + provider.providerId).click(function(event) { | ||||||
| 				var fileDesc = fileMgr.currentFile; | 				var fileDesc = fileMgr.currentFile; | ||||||
| 
 | 
 | ||||||
|                 provider.exportFile(event, fileDesc.title, fileDesc.content, fileDesc.discussionListJSON, function(error, syncAttributes) { | 				provider.exportFile(event, fileDesc.title, fileDesc.content, fileDesc.discussionListJSON, fileDesc.frontMatter, function(error, syncAttributes) { | ||||||
| 					if(error) { | 					if(error) { | ||||||
| 						return; | 						return; | ||||||
| 					} | 					} | ||||||
|  | |||||||
| @ -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, | ||||||
|  | |||||||
| @ -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> | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 benweet
						benweet