Merge branch 'master' into after-delete-select-first
This commit is contained in:
@@ -15,87 +15,64 @@ angular.module('app.frontend')
|
||||
bindToController: true,
|
||||
|
||||
link:function(scope, elem, attrs, ctrl) {
|
||||
|
||||
/**
|
||||
* Insert 4 spaces when a tab key is pressed,
|
||||
* only used when inside of the text editor.
|
||||
* If the shift key is pressed first, this event is
|
||||
* not fired.
|
||||
*/
|
||||
var handleTab = function (event) {
|
||||
if (!event.shiftKey && event.which == 9) {
|
||||
event.preventDefault();
|
||||
var start = this.selectionStart;
|
||||
var end = this.selectionEnd;
|
||||
var spaces = " ";
|
||||
|
||||
// Insert 4 spaces
|
||||
this.value = this.value.substring(0, start)
|
||||
+ spaces + this.value.substring(end);
|
||||
|
||||
// Place cursor 4 spaces away from where
|
||||
// the tab key was pressed
|
||||
this.selectionStart = this.selectionEnd = start + 4;
|
||||
}
|
||||
}
|
||||
|
||||
var handler = function(event) {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
switch (String.fromCharCode(event.which).toLowerCase()) {
|
||||
case 's':
|
||||
event.preventDefault();
|
||||
$timeout(function(){
|
||||
ctrl.saveNote(event);
|
||||
});
|
||||
break;
|
||||
case 'e':
|
||||
event.preventDefault();
|
||||
$timeout(function(){
|
||||
ctrl.clickedEditNote();
|
||||
})
|
||||
break;
|
||||
case 'm':
|
||||
event.preventDefault();
|
||||
$timeout(function(){
|
||||
ctrl.toggleMarkdown();
|
||||
})
|
||||
break;
|
||||
case 'o':
|
||||
event.preventDefault();
|
||||
$timeout(function(){
|
||||
ctrl.toggleFullScreen();
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handler);
|
||||
var element = document.getElementById("note-text-editor");
|
||||
element.addEventListener('keydown', handleTab);
|
||||
|
||||
scope.$on('$destroy', function(){
|
||||
window.removeEventListener('keydown', handler);
|
||||
})
|
||||
|
||||
scope.$watch('ctrl.note', function(note, oldNote){
|
||||
if(note) {
|
||||
ctrl.setNote(note, oldNote);
|
||||
} else {
|
||||
ctrl.note = {};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('EditorCtrl', function ($sce, $timeout, authManager, markdownRenderer, $rootScope, extensionManager, syncManager) {
|
||||
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, extensionManager, syncManager, modelManager) {
|
||||
|
||||
window.addEventListener("message", function(event){
|
||||
if(event.data.status) {
|
||||
this.postNoteToExternalEditor();
|
||||
} else {
|
||||
var id = event.data.id;
|
||||
var text = event.data.text;
|
||||
var data = event.data.data;
|
||||
|
||||
if(this.note.uuid === id) {
|
||||
this.note.text = text;
|
||||
if(data) {
|
||||
var changesMade = this.customEditor.setData(id, data);
|
||||
if(changesMade) {
|
||||
this.customEditor.setDirty(true);
|
||||
}
|
||||
}
|
||||
this.changesMade();
|
||||
}
|
||||
}
|
||||
}.bind(this), false);
|
||||
|
||||
this.setNote = function(note, oldNote) {
|
||||
this.editorMode = 'edit';
|
||||
var currentEditor = this.customEditor;
|
||||
this.customEditor = null;
|
||||
this.showExtensions = false;
|
||||
this.showMenu = false;
|
||||
this.loadTagsString();
|
||||
|
||||
var setEditor = function(editor) {
|
||||
this.customEditor = editor;
|
||||
this.postNoteToExternalEditor();
|
||||
}.bind(this)
|
||||
|
||||
var editor = this.editorForNote(note);
|
||||
if(editor) {
|
||||
if(currentEditor !== editor) {
|
||||
// switch after timeout, so that note data isnt posted to current editor
|
||||
$timeout(function(){
|
||||
setEditor(editor);
|
||||
}.bind(this));
|
||||
} else {
|
||||
// switch immediately
|
||||
setEditor(editor);
|
||||
}
|
||||
} else {
|
||||
this.customEditor = null;
|
||||
}
|
||||
|
||||
if(note.safeText().length == 0 && note.dummy) {
|
||||
this.focusTitle(100);
|
||||
}
|
||||
@@ -109,19 +86,50 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
|
||||
this.hasAvailableExtensions = function() {
|
||||
return extensionManager.extensionsInContextOfItem(this.note).length > 0;
|
||||
this.selectedEditor = function(editor) {
|
||||
this.showEditorMenu = false;
|
||||
|
||||
if(this.customEditor && editor !== this.customEditor) {
|
||||
this.customEditor.removeItemAsRelationship(this.note);
|
||||
this.customEditor.setDirty(true);
|
||||
}
|
||||
|
||||
if(editor.default) {
|
||||
this.customEditor = null;
|
||||
} else {
|
||||
this.customEditor = editor;
|
||||
this.customEditor.addItemAsRelationship(this.note);
|
||||
this.customEditor.setDirty(true);
|
||||
}
|
||||
}.bind(this)
|
||||
|
||||
this.editorForNote = function(note) {
|
||||
var editors = modelManager.itemsForContentType("SN|Editor");
|
||||
for(var editor of editors) {
|
||||
if(_.includes(editor.notes, note)) {
|
||||
return editor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
this.onPreviewDoubleClick = function() {
|
||||
this.editorMode = 'edit';
|
||||
this.focusEditor(100);
|
||||
this.postNoteToExternalEditor = function() {
|
||||
var externalEditorElement = document.getElementById("editor-iframe");
|
||||
if(externalEditorElement) {
|
||||
externalEditorElement.contentWindow.postMessage({text: this.note.text, data: this.customEditor.dataForKey(this.note.uuid), id: this.note.uuid}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
this.hasAvailableExtensions = function() {
|
||||
return extensionManager.extensionsInContextOfItem(this.note).length > 0;
|
||||
}
|
||||
|
||||
this.focusEditor = function(delay) {
|
||||
setTimeout(function(){
|
||||
var element = document.getElementById("note-text-editor");
|
||||
element.focus();
|
||||
if(element) {
|
||||
element.focus();
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
|
||||
@@ -135,10 +143,6 @@ angular.module('app.frontend')
|
||||
this.showMenu = false;
|
||||
}
|
||||
|
||||
this.renderedContent = function() {
|
||||
return markdownRenderer.renderHtml(markdownRenderer.renderedContentForText(this.note.safeText()));
|
||||
}
|
||||
|
||||
var statusTimeout;
|
||||
|
||||
this.saveNote = function($event) {
|
||||
@@ -198,7 +202,6 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
this.onContentFocus = function() {
|
||||
this.showSampler = false;
|
||||
$rootScope.$broadcast("editorFocused");
|
||||
}
|
||||
|
||||
@@ -209,12 +212,7 @@ angular.module('app.frontend')
|
||||
this.toggleFullScreen = function() {
|
||||
this.fullscreen = !this.fullscreen;
|
||||
if(this.fullscreen) {
|
||||
if(this.editorMode == 'edit') {
|
||||
// refocus
|
||||
this.focusEditor(0);
|
||||
}
|
||||
} else {
|
||||
|
||||
this.focusEditor(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,19 +220,6 @@ angular.module('app.frontend')
|
||||
this.showMenu = false;
|
||||
}
|
||||
|
||||
this.toggleMarkdown = function() {
|
||||
if(this.editorMode == 'preview') {
|
||||
this.editorMode = 'edit';
|
||||
this.focusEditor(0);
|
||||
} else {
|
||||
this.editorMode = 'preview';
|
||||
}
|
||||
}
|
||||
|
||||
this.clickedMenu = function() {
|
||||
this.showMenu = !this.showMenu;
|
||||
}
|
||||
|
||||
this.deleteNote = function() {
|
||||
if(confirm("Are you sure you want to delete this note?")) {
|
||||
this.remove()(this.note);
|
||||
|
||||
@@ -1,8 +1,31 @@
|
||||
angular.module('app.frontend')
|
||||
.controller('HomeCtrl', function ($scope, $rootScope, $timeout, modelManager, syncManager, authManager) {
|
||||
$rootScope.bodyClass = "app-body-class";
|
||||
.controller('HomeCtrl', function ($scope, $stateParams, $rootScope, $timeout, modelManager, syncManager, authManager) {
|
||||
|
||||
function autoSignInFromParams() {
|
||||
if(!authManager.offline()) {
|
||||
// check if current account
|
||||
if(syncManager.serverURL == $stateParams.server && authManager.user.email == $stateParams.email) {
|
||||
// already signed in, return
|
||||
return;
|
||||
} else {
|
||||
// sign out
|
||||
syncManager.destroyLocalData(function(){
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
} else {
|
||||
authManager.login($stateParams.server, $stateParams.email, $stateParams.pw, function(response){
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if($stateParams.server && $stateParams.email) {
|
||||
autoSignInFromParams();
|
||||
}
|
||||
|
||||
syncManager.loadLocalItems(function(items) {
|
||||
$scope.allTag.didLoad = true;
|
||||
$scope.$apply();
|
||||
|
||||
syncManager.sync(null);
|
||||
@@ -12,7 +35,9 @@ angular.module('app.frontend')
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
$scope.allTag = new Tag({all: true});
|
||||
var allTag = new Tag({all: true});
|
||||
allTag.needsLoad = true;
|
||||
$scope.allTag = allTag;
|
||||
$scope.allTag.title = "All";
|
||||
$scope.tags = modelManager.tags;
|
||||
$scope.allTag.notes = modelManager.notes;
|
||||
@@ -31,6 +56,7 @@ angular.module('app.frontend')
|
||||
modelManager.createRelationshipBetweenItems(note, tag);
|
||||
}
|
||||
|
||||
note.setDirty(true);
|
||||
syncManager.sync();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,16 @@ angular.module('app.frontend')
|
||||
link:function(scope, elem, attrs, ctrl) {
|
||||
scope.$watch('ctrl.tag', function(tag, oldTag){
|
||||
if(tag) {
|
||||
ctrl.tagDidChange(tag, oldTag);
|
||||
if(tag.needsLoad) {
|
||||
scope.$watch('ctrl.tag.didLoad', function(didLoad){
|
||||
if(didLoad) {
|
||||
tag.needsLoad = false;
|
||||
ctrl.tagDidChange(tag, oldTag);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ctrl.tagDidChange(tag, oldTag);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -26,6 +35,8 @@ angular.module('app.frontend')
|
||||
})
|
||||
.controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager) {
|
||||
|
||||
this.sortBy = localStorage.getItem("sortBy") || "created_at";
|
||||
|
||||
$rootScope.$on("editorFocused", function(){
|
||||
this.showMenu = false;
|
||||
}.bind(this))
|
||||
@@ -34,8 +45,6 @@ angular.module('app.frontend')
|
||||
this.selectFirstNote(false);
|
||||
}.bind(this))
|
||||
|
||||
var isFirstLoad = true;
|
||||
|
||||
this.notesToDisplay = 20;
|
||||
this.paginate = function() {
|
||||
this.notesToDisplay += 20
|
||||
@@ -49,20 +58,16 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
this.noteFilter.text = "";
|
||||
this.setNotes(tag.notes);
|
||||
}
|
||||
|
||||
tag.notes.forEach(function(note){
|
||||
this.setNotes = function(notes) {
|
||||
notes.forEach(function(note){
|
||||
note.visible = true;
|
||||
})
|
||||
this.selectFirstNote(false);
|
||||
|
||||
if(isFirstLoad) {
|
||||
$timeout(function(){
|
||||
this.createNewNote();
|
||||
isFirstLoad = false;
|
||||
}.bind(this))
|
||||
} else if(tag.notes.length == 0) {
|
||||
this.createNewNote();
|
||||
}
|
||||
var createNew = notes.length == 0;
|
||||
this.selectFirstNote(createNew);
|
||||
}
|
||||
|
||||
this.selectedTagDelete = function() {
|
||||
@@ -71,7 +76,7 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
this.selectFirstNote = function(createNew) {
|
||||
var visibleNotes = this.tag.notes.filter(function(note){
|
||||
var visibleNotes = this.sortedNotes.filter(function(note){
|
||||
return note.visible;
|
||||
});
|
||||
|
||||
@@ -114,4 +119,22 @@ angular.module('app.frontend')
|
||||
}
|
||||
}.bind(this), 100)
|
||||
}
|
||||
|
||||
this.selectedMenuItem = function() {
|
||||
this.showMenu = false;
|
||||
}
|
||||
|
||||
this.selectedSortByCreated = function() {
|
||||
this.setSortBy("created_at");
|
||||
}
|
||||
|
||||
this.selectedSortByUpdated = function() {
|
||||
this.setSortBy("updated_at");
|
||||
}
|
||||
|
||||
this.setSortBy = function(type) {
|
||||
this.sortBy = type;
|
||||
localStorage.setItem("sortBy", type);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -37,6 +37,7 @@ class Item {
|
||||
|
||||
updateFromJSON(json) {
|
||||
_.merge(this, json);
|
||||
|
||||
if(this.created_at) {
|
||||
this.created_at = new Date(this.created_at);
|
||||
this.updated_at = new Date(this.updated_at);
|
||||
@@ -112,6 +113,10 @@ class Item {
|
||||
this.setDirty(true);
|
||||
}
|
||||
|
||||
locallyClearAllReferences() {
|
||||
|
||||
}
|
||||
|
||||
mergeMetadataFromItem(item) {
|
||||
_.merge(this, _.omit(item, ["content"]));
|
||||
}
|
||||
|
||||
89
app/assets/javascripts/app/frontend/models/app/editor.js
Normal file
89
app/assets/javascripts/app/frontend/models/app/editor.js
Normal file
@@ -0,0 +1,89 @@
|
||||
class Editor extends Item {
|
||||
|
||||
constructor(json_obj) {
|
||||
super(json_obj);
|
||||
if(!this.notes) {
|
||||
this.notes = [];
|
||||
}
|
||||
if(!this.data) {
|
||||
this.data = {};
|
||||
}
|
||||
}
|
||||
|
||||
mapContentToLocalProperties(contentObject) {
|
||||
super.mapContentToLocalProperties(contentObject)
|
||||
this.url = contentObject.url;
|
||||
this.name = contentObject.name;
|
||||
this.data = contentObject.data || {};
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
var params = {
|
||||
url: this.url,
|
||||
name: this.name,
|
||||
data: this.data
|
||||
};
|
||||
|
||||
_.merge(params, super.structureParams());
|
||||
return params;
|
||||
}
|
||||
|
||||
referenceParams() {
|
||||
var references = _.map(this.notes, function(note){
|
||||
return {uuid: note.uuid, content_type: note.content_type};
|
||||
})
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
addItemAsRelationship(item) {
|
||||
if(item.content_type == "Note") {
|
||||
if(!_.find(this.notes, item)) {
|
||||
this.notes.push(item);
|
||||
}
|
||||
}
|
||||
super.addItemAsRelationship(item);
|
||||
}
|
||||
|
||||
removeItemAsRelationship(item) {
|
||||
if(item.content_type == "Note") {
|
||||
_.pull(this.notes, item);
|
||||
}
|
||||
super.removeItemAsRelationship(item);
|
||||
}
|
||||
|
||||
removeAllRelationships() {
|
||||
super.removeAllRelationships();
|
||||
this.notes = [];
|
||||
}
|
||||
|
||||
locallyClearAllReferences() {
|
||||
super.locallyClearAllReferences();
|
||||
this.notes = [];
|
||||
}
|
||||
|
||||
allReferencedObjects() {
|
||||
return this.notes;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {uuid: this.uuid}
|
||||
}
|
||||
|
||||
get content_type() {
|
||||
return "SN|Editor";
|
||||
}
|
||||
|
||||
setData(key, value) {
|
||||
var dataHasChanged = JSON.stringify(this.data[key]) !== JSON.stringify(value);
|
||||
if(dataHasChanged) {
|
||||
this.data[key] = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
dataForKey(key) {
|
||||
return this.data[key] || {};
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,7 @@ class Extension extends Item {
|
||||
super.mapContentToLocalProperties(contentObject)
|
||||
this.name = contentObject.name;
|
||||
this.url = contentObject.url;
|
||||
this.supported_types = contentObject.supported_types;
|
||||
this.actions = contentObject.actions.map(function(action){
|
||||
return new Action(action);
|
||||
})
|
||||
@@ -99,7 +100,8 @@ class Extension extends Item {
|
||||
var params = {
|
||||
name: this.name,
|
||||
url: this.url,
|
||||
actions: this.actions
|
||||
actions: this.actions,
|
||||
supported_types: this.supported_types
|
||||
};
|
||||
|
||||
_.merge(params, super.structureParams());
|
||||
|
||||
@@ -56,6 +56,14 @@ class Note extends Item {
|
||||
this.tags = [];
|
||||
}
|
||||
|
||||
locallyClearAllReferences() {
|
||||
super.locallyClearAllReferences();
|
||||
this.tags.forEach(function(tag){
|
||||
_.pull(tag.notes, this);
|
||||
}.bind(this))
|
||||
this.tags = [];
|
||||
}
|
||||
|
||||
isBeingRemovedLocally() {
|
||||
this.tags.forEach(function(tag){
|
||||
_.pull(tag.notes, this);
|
||||
|
||||
@@ -55,6 +55,15 @@ class Tag extends Item {
|
||||
this.notes = [];
|
||||
}
|
||||
|
||||
locallyClearAllReferences() {
|
||||
super.locallyClearAllReferences();
|
||||
this.notes.forEach(function(note){
|
||||
_.pull(note.tags, this);
|
||||
}.bind(this))
|
||||
|
||||
this.notes = [];
|
||||
}
|
||||
|
||||
isBeingRemovedLocally() {
|
||||
this.notes.forEach(function(note){
|
||||
_.pull(note.tags, this);
|
||||
|
||||
@@ -7,7 +7,7 @@ angular.module('app.frontend')
|
||||
})
|
||||
|
||||
.state('home', {
|
||||
url: '/',
|
||||
url: '/?server&email&pw',
|
||||
parent: 'base',
|
||||
views: {
|
||||
'content@' : {
|
||||
|
||||
@@ -73,13 +73,12 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, authParams), function(keys){
|
||||
var mk = keys.mk;
|
||||
var requestUrl = url + "/auth/sign_in";
|
||||
var request = Restangular.oneUrl(requestUrl, requestUrl);
|
||||
var params = {password: keys.pw, email: email};
|
||||
_.merge(request, params);
|
||||
request.post().then(function(response){
|
||||
this.handleAuthResponse(response, email, url, authParams, mk, keys.pw);
|
||||
this.handleAuthResponse(response, email, url, authParams, keys.mk, keys.pw);
|
||||
callback(response);
|
||||
}.bind(this))
|
||||
.catch(function(response){
|
||||
@@ -91,7 +90,9 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
this.handleAuthResponse = function(response, email, url, authParams, mk, pw) {
|
||||
localStorage.setItem("server", url);
|
||||
if(url) {
|
||||
localStorage.setItem("server", url);
|
||||
}
|
||||
localStorage.setItem("user", JSON.stringify(response.plain().user));
|
||||
localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"])));
|
||||
localStorage.setItem("mk", mk);
|
||||
@@ -101,13 +102,12 @@ angular.module('app.frontend')
|
||||
|
||||
this.register = function(url, email, password, callback) {
|
||||
Neeto.crypto.generateInitialEncryptionKeysForUser({password: password, email: email}, function(keys, authParams){
|
||||
var mk = keys.mk;
|
||||
var requestUrl = url + "/auth";
|
||||
var request = Restangular.oneUrl(requestUrl, requestUrl);
|
||||
var params = _.merge({password: keys.pw, email: email}, authParams);
|
||||
_.merge(request, params);
|
||||
request.post().then(function(response){
|
||||
this.handleAuthResponse(response, email, url, authParams, mk, keys.pw);
|
||||
this.handleAuthResponse(response, email, url, authParams, keys.mk, keys.pw);
|
||||
callback(response);
|
||||
}.bind(this))
|
||||
.catch(function(response){
|
||||
@@ -117,55 +117,26 @@ angular.module('app.frontend')
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
// this.changePassword = function(current_password, new_password) {
|
||||
// this.getAuthParamsForEmail(email, function(authParams){
|
||||
// if(!authParams) {
|
||||
// callback(null);
|
||||
// return;
|
||||
// }
|
||||
// Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: current_password, email: user.email}, authParams), function(currentKeys) {
|
||||
// Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: new_password, email: user.email}, authParams), function(newKeys){
|
||||
// var data = {};
|
||||
// data.current_password = currentKeys.pw;
|
||||
// data.password = newKeys.pw;
|
||||
// data.password_confirmation = newKeys.pw;
|
||||
//
|
||||
// var user = this.user;
|
||||
//
|
||||
// this._performPasswordChange(currentKeys, newKeys, function(response){
|
||||
// if(response && !response.error) {
|
||||
// // this.showNewPasswordForm = false;
|
||||
// // reencrypt data with new mk
|
||||
// this.reencryptAllItemsAndSave(user, newKeys.mk, currentKeys.mk, function(success){
|
||||
// if(success) {
|
||||
// this.setMk(newKeys.mk);
|
||||
// alert("Your password has been changed and your data re-encrypted.");
|
||||
// } else {
|
||||
// // rollback password
|
||||
// this._performPasswordChange(newKeys, currentKeys, function(response){
|
||||
// alert("There was an error changing your password. Your password has been rolled back.");
|
||||
// window.location.reload();
|
||||
// })
|
||||
// }
|
||||
// }.bind(this));
|
||||
// } else {
|
||||
// // this.showNewPasswordForm = false;
|
||||
// alert("There was an error changing your password. Please try again.");
|
||||
// }
|
||||
// }.bind(this))
|
||||
// }.bind(this));
|
||||
// }.bind(this));
|
||||
// }.bind(this));
|
||||
// }
|
||||
this.changePassword = function(email, new_password, callback) {
|
||||
Neeto.crypto.generateInitialEncryptionKeysForUser({password: new_password, email: email}, function(keys, authParams){
|
||||
var requestUrl = localStorage.getItem("server") + "/auth/change_pw";
|
||||
var request = Restangular.oneUrl(requestUrl, requestUrl);
|
||||
var params = _.merge({new_password: keys.pw}, authParams);
|
||||
_.merge(request, params);
|
||||
|
||||
this._performPasswordChange = function(url, email, current_keys, new_keys, callback) {
|
||||
var requestUrl = url + "/auth";
|
||||
var request = Restangular.oneUrl(requestUrl, requestUrl);
|
||||
var params = {password: new_keys.pw, password_confirmation: new_keys.pw, current_password: current_keys.pw, email: email};
|
||||
_.merge(request, params);
|
||||
request.patch().then(function(response){
|
||||
callback(response);
|
||||
})
|
||||
request.post().then(function(response){
|
||||
this.handleAuthResponse(response, email, null, authParams, keys.mk, keys.pw);
|
||||
callback(response.plain());
|
||||
}.bind(this))
|
||||
.catch(function(response){
|
||||
var error = response.data;
|
||||
if(!error) {
|
||||
error = {message: "Something went wrong while changing your password. Your password was not changed. Please try again."}
|
||||
}
|
||||
console.log("Change pw error", response);
|
||||
callback({error: error});
|
||||
})
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.staticifyObject = function(object) {
|
||||
|
||||
@@ -102,7 +102,6 @@ class DBManager {
|
||||
this.openDatabase((db) => {
|
||||
var request = db.transaction("items", "readwrite").objectStore("items").delete(item.uuid);
|
||||
request.onsuccess = function(event) {
|
||||
console.log("Successfully deleted item", item.uuid);
|
||||
if(callback) {
|
||||
callback(true);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
angular
|
||||
.module('app.frontend')
|
||||
.directive('clickOutside', ['$document', function($document) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
replace: false,
|
||||
link : function($scope, $element, attrs) {
|
||||
|
||||
var didApplyClickOutside = false;
|
||||
|
||||
$element.bind('click', function(e) {
|
||||
didApplyClickOutside = false;
|
||||
if (attrs.isOpen) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
$document.bind('click', function() {
|
||||
if(!didApplyClickOutside) {
|
||||
$scope.$apply(attrs.clickOutside);
|
||||
didApplyClickOutside = true;
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
}]);
|
||||
@@ -20,11 +20,15 @@ angular
|
||||
});
|
||||
|
||||
function showSpinner() {
|
||||
if(scope.hidePromise) {
|
||||
$timeout.cancel(scope.hidePromise);
|
||||
scope.hidePromise = null;
|
||||
}
|
||||
showElement(true);
|
||||
}
|
||||
|
||||
function hideSpinner() {
|
||||
$timeout(showElement.bind(this, false), getDelay());
|
||||
scope.hidePromise = $timeout(showElement.bind(this, false), getDelay());
|
||||
}
|
||||
|
||||
function showElement(show) {
|
||||
|
||||
@@ -6,19 +6,15 @@ class AccountMenu {
|
||||
this.scope = {};
|
||||
}
|
||||
|
||||
controller($scope, authManager, modelManager, syncManager, $timeout) {
|
||||
controller($scope, authManager, modelManager, syncManager, dbManager, $timeout) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {url: syncManager.serverURL};
|
||||
$scope.formData = {mergeLocal: true, url: syncManager.serverURL};
|
||||
$scope.user = authManager.user;
|
||||
$scope.server = syncManager.serverURL;
|
||||
|
||||
$scope.syncStatus = syncManager.syncStatus;
|
||||
|
||||
$scope.changePasswordPressed = function() {
|
||||
$scope.showNewPasswordForm = !$scope.showNewPasswordForm;
|
||||
}
|
||||
|
||||
$scope.encryptionKey = function() {
|
||||
return syncManager.masterKey;
|
||||
}
|
||||
@@ -31,25 +27,61 @@ class AccountMenu {
|
||||
return `${$scope.server}/dashboard/?server=${$scope.server}&id=${$scope.user.email}&pw=${$scope.serverPassword()}`;
|
||||
}
|
||||
|
||||
$scope.newPasswordData = {};
|
||||
|
||||
$scope.showPasswordChangeForm = function() {
|
||||
$scope.newPasswordData.showForm = true;
|
||||
}
|
||||
|
||||
$scope.submitPasswordChange = function() {
|
||||
$scope.passwordChangeData.status = "Generating New Keys...";
|
||||
|
||||
$timeout(function(){
|
||||
if(data.password != data.password_confirmation) {
|
||||
alert("Your new password does not match its confirmation.");
|
||||
return;
|
||||
}
|
||||
if($scope.newPasswordData.newPassword != $scope.newPasswordData.newPasswordConfirmation) {
|
||||
alert("Your new password does not match its confirmation.");
|
||||
$scope.newPasswordData.status = null;
|
||||
return;
|
||||
}
|
||||
|
||||
authManager.changePassword($scope.passwordChangeData.current_password, $scope.passwordChangeData.new_password, function(response){
|
||||
var email = $scope.user.email;
|
||||
if(!email) {
|
||||
alert("We don't have your email stored. Please log out then log back in to fix this issue.");
|
||||
$scope.newPasswordData.status = null;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.newPasswordData.status = "Generating New Keys...";
|
||||
$scope.newPasswordData.showForm = false;
|
||||
|
||||
// perform a sync beforehand to pull in any last minutes changes before we change the encryption key (and thus cant decrypt new changes)
|
||||
syncManager.sync(function(response){
|
||||
authManager.changePassword(email, $scope.newPasswordData.newPassword, function(response){
|
||||
if(response.error) {
|
||||
alert("There was an error changing your password. Please try again.");
|
||||
$scope.newPasswordData.status = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// re-encrypt all items
|
||||
$scope.newPasswordData.status = "Re-encrypting all items with your new key...";
|
||||
|
||||
modelManager.setAllItemsDirty();
|
||||
syncManager.sync(function(response){
|
||||
if(response.error) {
|
||||
alert("There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.")
|
||||
return;
|
||||
}
|
||||
$scope.newPasswordData.status = "Successfully changed password and re-encrypted all items.";
|
||||
$timeout(function(){
|
||||
alert("Your password has been changed, and your items successfully re-encrypted and synced. You must sign out of all other signed in applications and sign in again, or else you may corrupt your data.")
|
||||
$scope.newPasswordData = {};
|
||||
}, 1000)
|
||||
});
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
$scope.loginSubmitPressed = function() {
|
||||
$scope.formData.status = "Generating Login Keys...";
|
||||
console.log("logging in with url", $scope.formData.url);
|
||||
$timeout(function(){
|
||||
authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){
|
||||
if(!response || response.error) {
|
||||
@@ -66,6 +98,11 @@ class AccountMenu {
|
||||
}
|
||||
|
||||
$scope.submitRegistrationForm = function() {
|
||||
var confirmation = prompt("Please confirm your password. Note that because your notes are encrypted using your password, Standard Notes does not have a password reset option. You cannot forget your password.")
|
||||
if(confirmation !== $scope.formData.user_password) {
|
||||
alert("The two passwords you entered do not match. Please try again.");
|
||||
return;
|
||||
}
|
||||
$scope.formData.status = "Generating Account Keys...";
|
||||
|
||||
$timeout(function(){
|
||||
@@ -81,10 +118,32 @@ class AccountMenu {
|
||||
})
|
||||
}
|
||||
|
||||
$scope.localNotesCount = function() {
|
||||
return modelManager.filteredNotes.length;
|
||||
}
|
||||
|
||||
$scope.mergeLocalChanged = function() {
|
||||
if(!$scope.formData.mergeLocal) {
|
||||
if(!confirm("Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?")) {
|
||||
$scope.formData.mergeLocal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.onAuthSuccess = function() {
|
||||
syncManager.markAllItemsDirtyAndSaveOffline(function(){
|
||||
var block = function() {
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
|
||||
if($scope.formData.mergeLocal) {
|
||||
syncManager.markAllItemsDirtyAndSaveOffline(function(){
|
||||
block();
|
||||
})
|
||||
} else {
|
||||
dbManager.clearAllItems(function(){
|
||||
block();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$scope.destroyLocalData = function() {
|
||||
@@ -116,6 +175,8 @@ class AccountMenu {
|
||||
$scope.importData = null;
|
||||
if(!response) {
|
||||
alert("There was an error importing your data. Please try again.");
|
||||
} else {
|
||||
alert("Your data was successfully imported.")
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -149,12 +210,11 @@ class AccountMenu {
|
||||
}
|
||||
|
||||
$scope.importJSONData = function(data, password, callback) {
|
||||
console.log("Importing data", data);
|
||||
|
||||
var onDataReady = function() {
|
||||
var items = modelManager.mapResponseItemsToLocalModels(data.items);
|
||||
items.forEach(function(item){
|
||||
item.setDirty(true);
|
||||
item.deleted = false;
|
||||
item.markAllReferencesDirty();
|
||||
})
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ class ContextualExtensionsMenu {
|
||||
controller($scope, modelManager, extensionManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.renderData = {};
|
||||
|
||||
$scope.extensions = _.map(extensionManager.extensionsInContextOfItem($scope.item), function(ext){
|
||||
return _.cloneDeep(ext);
|
||||
});
|
||||
@@ -27,12 +29,30 @@ class ContextualExtensionsMenu {
|
||||
}
|
||||
|
||||
$scope.executeAction = function(action, extension) {
|
||||
if(action.verb == "nested") {
|
||||
action.showNestedActions = !action.showNestedActions;
|
||||
return;
|
||||
}
|
||||
action.running = true;
|
||||
extensionManager.executeAction(action, extension, $scope.item, function(response){
|
||||
action.running = false;
|
||||
$scope.handleActionResponse(action, response);
|
||||
})
|
||||
}
|
||||
|
||||
$scope.handleActionResponse = function(action, response) {
|
||||
switch (action.verb) {
|
||||
case "render": {
|
||||
var item = response.item;
|
||||
if(item.content_type == "Note") {
|
||||
$scope.renderData.title = item.title;
|
||||
$scope.renderData.text = item.text;
|
||||
$scope.renderData.showRenderModal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.accessTypeForExtension = function(extension) {
|
||||
return extensionManager.extensionUsesEncryptedData(extension) ? "encrypted" : "decrypted";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
class EditorMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/editor-menu.html";
|
||||
this.scope = {
|
||||
callback: "&",
|
||||
selectedEditor: "="
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, modelManager, extensionManager, syncManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {};
|
||||
|
||||
let editorContentType = "SN|Editor";
|
||||
|
||||
let defaultEditor = {
|
||||
default: true,
|
||||
name: "Plain"
|
||||
}
|
||||
|
||||
$scope.sysEditors = [defaultEditor];
|
||||
$scope.editors = modelManager.itemsForContentType(editorContentType);
|
||||
|
||||
$scope.editorForUrl = function(url) {
|
||||
return $scope.editors.filter(function(editor){return editor.url == url})[0];
|
||||
}
|
||||
|
||||
$scope.selectEditor = function(editor) {
|
||||
$scope.callback()(editor);
|
||||
}
|
||||
|
||||
$scope.deleteEditor = function(editor) {
|
||||
if(confirm("Are you sure you want to delete this editor?")) {
|
||||
modelManager.setItemToBeDeleted(editor);
|
||||
syncManager.sync();
|
||||
_.pull($scope.editors, editor);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.submitNewEditorRequest = function() {
|
||||
var editor = createEditor($scope.formData.url);
|
||||
modelManager.addItem(editor);
|
||||
editor.setDirty(true);
|
||||
syncManager.sync();
|
||||
$scope.editors.push(editor);
|
||||
$scope.formData = {};
|
||||
}
|
||||
|
||||
function createEditor(url) {
|
||||
var name = getParameterByName("name", url);
|
||||
return modelManager.createItem({
|
||||
content_type: editorContentType,
|
||||
url: url,
|
||||
name: name
|
||||
})
|
||||
}
|
||||
|
||||
function getParameterByName(name, url) {
|
||||
name = name.replace(/[\[\]]/g, "\\$&");
|
||||
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
|
||||
results = regex.exec(url);
|
||||
if (!results) return null;
|
||||
if (!results[2]) return '';
|
||||
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('editorMenu', () => new EditorMenu);
|
||||
@@ -21,7 +21,14 @@ class GlobalExtensionsMenu {
|
||||
if($scope.newExtensionData.url) {
|
||||
extensionManager.addExtension($scope.newExtensionData.url, function(response){
|
||||
if(!response) {
|
||||
alert("Unable to register this extension. Make sure the link is valid and try again.");
|
||||
if($scope.newExtensionData.url.indexOf("type=sf") != -1) {
|
||||
alert("Unable to register this extension. You are attempting to register a Standard File extension in Standard Notes. You should instead open your Standard File Dashboard and register this extension there.");
|
||||
} else if($scope.newExtensionData.url.indexOf("name=") != -1) {
|
||||
// user is mistakenly trying to register editor extension, most likely
|
||||
alert("Unable to register this extension. It looks like you may be trying to install an editor extension. To do that, click 'Editor' under the current note's title.");
|
||||
} else {
|
||||
alert("Unable to register this extension. Make sure the link is valid and try again.");
|
||||
}
|
||||
} else {
|
||||
$scope.newExtensionData.url = "";
|
||||
$scope.showNewExtensionForm = false;
|
||||
|
||||
@@ -28,7 +28,7 @@ class ExtensionManager {
|
||||
|
||||
extensionsInContextOfItem(item) {
|
||||
return this.extensions.filter(function(ext){
|
||||
return ext.actionsWithContextForItem(item).length > 0;
|
||||
return _.includes(ext.supported_types, item.content_type) || ext.actionsWithContextForItem(item).length > 0;
|
||||
})
|
||||
}
|
||||
|
||||
@@ -152,9 +152,29 @@ class ExtensionManager {
|
||||
case "get": {
|
||||
this.Restangular.oneUrl(action.url, action.url).get().then(function(response){
|
||||
action.error = false;
|
||||
var items = response.items;
|
||||
this.modelManager.mapResponseItemsToLocalModels(items);
|
||||
customCallback(items);
|
||||
var items = response.items || [response.item];
|
||||
EncryptionHelper.decryptMultipleItems(items, localStorage.getItem("mk"));
|
||||
items = this.modelManager.mapResponseItemsToLocalModels(items);
|
||||
for(var item of items) {
|
||||
item.setDirty(true);
|
||||
}
|
||||
this.syncManager.sync(null);
|
||||
customCallback({items: items});
|
||||
}.bind(this))
|
||||
.catch(function(response){
|
||||
action.error = true;
|
||||
customCallback(null);
|
||||
})
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "render": {
|
||||
this.Restangular.oneUrl(action.url, action.url).get().then(function(response){
|
||||
action.error = false;
|
||||
EncryptionHelper.decryptItem(response.item, localStorage.getItem("mk"));
|
||||
var item = this.modelManager.createItem(response.item);
|
||||
customCallback({item: item});
|
||||
}.bind(this))
|
||||
.catch(function(response){
|
||||
action.error = true;
|
||||
@@ -281,7 +301,11 @@ class ExtensionManager {
|
||||
}
|
||||
|
||||
outgoingParamsForItem(item, extension) {
|
||||
var itemParams = new ItemParams(item, this.syncManager.masterKey);
|
||||
var ek = this.syncManager.masterKey;
|
||||
if(!this.extensionUsesEncryptedData(extension)) {
|
||||
ek = null;
|
||||
}
|
||||
var itemParams = new ItemParams(item, ek);
|
||||
return itemParams.paramsForExtension();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Start from filter
|
||||
angular.module('app.frontend').filter('startFrom', function() {
|
||||
return function(input, start) {
|
||||
return input.slice(start);
|
||||
|
||||
5
app/assets/javascripts/app/services/filters/trusted.js
Normal file
5
app/assets/javascripts/app/services/filters/trusted.js
Normal file
@@ -0,0 +1,5 @@
|
||||
angular.module('app.frontend').filter('trusted', ['$sce', function ($sce) {
|
||||
return function(url) {
|
||||
return $sce.trustAsResourceUrl(url);
|
||||
};
|
||||
}]);
|
||||
@@ -60,11 +60,13 @@ class SNCrypto {
|
||||
}
|
||||
|
||||
base64(text) {
|
||||
return CryptoJS.enc.Utf8.parse(text).toString(CryptoJS.enc.Base64)
|
||||
// return CryptoJS.enc.Utf8.parse(text).toString(CryptoJS.enc.Base64)
|
||||
return window.btoa(text);
|
||||
}
|
||||
|
||||
base64Decode(base64String) {
|
||||
return CryptoJS.enc.Base64.parse(base64String).toString(CryptoJS.enc.Utf8)
|
||||
// return CryptoJS.enc.Base64.parse(base64String).toString(CryptoJS.enc.Utf8)
|
||||
return window.atob(base64String);
|
||||
}
|
||||
|
||||
sha256(text) {
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
class EncryptionHelper {
|
||||
|
||||
static encryptItem(item, key) {
|
||||
var item_key = null;
|
||||
if(item.enc_item_key) {
|
||||
// we reuse the key, but this is optional
|
||||
item_key = Neeto.crypto.decryptText(item.enc_item_key, key);
|
||||
} else {
|
||||
item_key = Neeto.crypto.generateRandomEncryptionKey();
|
||||
item.enc_item_key = Neeto.crypto.encryptText(item_key, key);
|
||||
}
|
||||
var item_key = Neeto.crypto.generateRandomEncryptionKey();
|
||||
item.enc_item_key = Neeto.crypto.encryptText(item_key, key);
|
||||
|
||||
var ek = Neeto.crypto.firstHalfOfKey(item_key);
|
||||
var ak = Neeto.crypto.secondHalfOfKey(item_key);
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
angular.module('app.frontend')
|
||||
.service('markdownRenderer', function ($sce) {
|
||||
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
sanitize: true
|
||||
});
|
||||
|
||||
this.renderedContentForText = function(text) {
|
||||
if(!text || text.length == 0) {
|
||||
return "";
|
||||
}
|
||||
return marked(text);
|
||||
}
|
||||
|
||||
this.renderHtml = function(html_code) {
|
||||
return $sce.trustAsHtml(html_code);
|
||||
};
|
||||
|
||||
|
||||
});
|
||||
@@ -8,6 +8,7 @@ class ModelManager {
|
||||
this.itemChangeObservers = [];
|
||||
this.items = [];
|
||||
this._extensions = [];
|
||||
this.acceptableContentTypes = ["Note", "Tag", "Extension", "SN|Editor"];
|
||||
}
|
||||
|
||||
get allItems() {
|
||||
@@ -58,15 +59,17 @@ class ModelManager {
|
||||
}
|
||||
|
||||
mapResponseItemsToLocalModelsOmittingFields(items, omitFields) {
|
||||
var models = [];
|
||||
var models = [], processedObjects = [];
|
||||
|
||||
// first loop should add and process items
|
||||
for (var json_obj of items) {
|
||||
json_obj = _.omit(json_obj, omitFields || [])
|
||||
var item = this.findItem(json_obj["uuid"]);
|
||||
if(json_obj["deleted"] == true) {
|
||||
if(item) {
|
||||
this.removeItemLocally(item)
|
||||
}
|
||||
continue;
|
||||
if(json_obj["deleted"] == true || !_.includes(this.acceptableContentTypes, json_obj["content_type"])) {
|
||||
if(item) {
|
||||
this.removeItemLocally(item)
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
_.omit(json_obj, omitFields);
|
||||
@@ -79,11 +82,16 @@ class ModelManager {
|
||||
|
||||
this.addItem(item);
|
||||
|
||||
if(json_obj.content) {
|
||||
this.resolveReferencesForItem(item);
|
||||
}
|
||||
|
||||
models.push(item);
|
||||
processedObjects.push(json_obj);
|
||||
}
|
||||
|
||||
// second loop should process references
|
||||
for (var index in processedObjects) {
|
||||
var json_obj = processedObjects[index];
|
||||
if(json_obj.content) {
|
||||
this.resolveReferencesForItem(models[index]);
|
||||
}
|
||||
}
|
||||
|
||||
this.notifySyncObserversOfModels(models);
|
||||
@@ -120,6 +128,8 @@ class ModelManager {
|
||||
item = new Tag(json_obj);
|
||||
} else if(json_obj.content_type == "Extension") {
|
||||
item = new Extension(json_obj);
|
||||
} else if(json_obj.content_type == "SN|Editor") {
|
||||
item = new Editor(json_obj);
|
||||
} else {
|
||||
item = new Item(json_obj);
|
||||
}
|
||||
@@ -144,9 +154,7 @@ class ModelManager {
|
||||
}
|
||||
} else if(item.content_type == "Note") {
|
||||
if(!_.find(this.notes, {uuid: item.uuid})) {
|
||||
this.notes.splice(_.sortedLastIndexBy(this.notes, item, function(item){
|
||||
return -item.created_at;
|
||||
}), 0, item);
|
||||
this.notes.unshift(item);
|
||||
}
|
||||
} else if(item.content_type == "Extension") {
|
||||
if(!_.find(this._extensions, {uuid: item.uuid})) {
|
||||
@@ -167,11 +175,13 @@ class ModelManager {
|
||||
}
|
||||
|
||||
resolveReferencesForItem(item) {
|
||||
item.locallyClearAllReferences();
|
||||
var contentObject = item.contentObject;
|
||||
if(!contentObject.references) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for(var reference of contentObject.references) {
|
||||
var referencedItem = this.findItem(reference.uuid);
|
||||
if(referencedItem) {
|
||||
@@ -225,6 +235,17 @@ class ModelManager {
|
||||
item.removeAllRelationships();
|
||||
}
|
||||
|
||||
/* Used when changing encryption key */
|
||||
setAllItemsDirty() {
|
||||
var relevantItems = this.allItems.filter(function(item){
|
||||
return _.includes(this.acceptableContentTypes, item.content_type);
|
||||
}.bind(this));
|
||||
|
||||
for(var item of relevantItems) {
|
||||
item.setDirty(true);
|
||||
}
|
||||
}
|
||||
|
||||
removeItemLocally(item, callback) {
|
||||
_.pull(this.items, item);
|
||||
|
||||
|
||||
@@ -98,10 +98,37 @@ class SyncManager {
|
||||
return this._cursorToken;
|
||||
}
|
||||
|
||||
get queuedCallbacks() {
|
||||
if(!this._queuedCallbacks) {
|
||||
this._queuedCallbacks = [];
|
||||
}
|
||||
return this._queuedCallbacks;
|
||||
}
|
||||
|
||||
clearQueuedCallbacks() {
|
||||
this._queuedCallbacks = [];
|
||||
}
|
||||
|
||||
callQueuedCallbacksAndCurrent(currentCallback, response) {
|
||||
var allCallbacks = this.queuedCallbacks;
|
||||
if(currentCallback) {
|
||||
allCallbacks.push(currentCallback);
|
||||
}
|
||||
if(allCallbacks.length) {
|
||||
for(var eachCallback of allCallbacks) {
|
||||
eachCallback(response);
|
||||
}
|
||||
this.clearQueuedCallbacks();
|
||||
}
|
||||
}
|
||||
|
||||
sync(callback, options = {}) {
|
||||
|
||||
if(this.syncStatus.syncOpInProgress) {
|
||||
this.repeatOnCompletion = true;
|
||||
if(callback) {
|
||||
this.queuedCallbacks.push(callback);
|
||||
}
|
||||
console.log("Sync op in progress; returning.");
|
||||
return;
|
||||
}
|
||||
@@ -116,18 +143,17 @@ class SyncManager {
|
||||
return;
|
||||
}
|
||||
|
||||
var isContinuationSync = this.needsMoreSync;
|
||||
var isContinuationSync = this.syncStatus.needsMoreSync;
|
||||
|
||||
this.repeatOnCompletion = false;
|
||||
this.syncStatus.syncOpInProgress = true;
|
||||
|
||||
let submitLimit = 100;
|
||||
var subItems = allDirtyItems.slice(0, submitLimit);
|
||||
if(subItems.length < allDirtyItems.length) {
|
||||
// more items left to be synced, repeat
|
||||
this.needsMoreSync = true;
|
||||
this.syncStatus.needsMoreSync = true;
|
||||
} else {
|
||||
this.needsMoreSync = false;
|
||||
this.syncStatus.needsMoreSync = false;
|
||||
}
|
||||
|
||||
if(!isContinuationSync) {
|
||||
@@ -153,12 +179,14 @@ class SyncManager {
|
||||
this.$rootScope.$broadcast("sync:updated_token", this.syncToken);
|
||||
|
||||
var retrieved = this.handleItemsResponse(response.retrieved_items, null);
|
||||
|
||||
// merge only metadata for saved items
|
||||
// we write saved items to disk now because it clears their dirty status then saves
|
||||
// if we saved items before completion, we had have to save them as dirty and save them again on success as clean
|
||||
var omitFields = ["content", "auth_hash"];
|
||||
var saved = this.handleItemsResponse(response.saved_items, omitFields);
|
||||
|
||||
this.handleUnsavedItemsResponse(response.unsaved)
|
||||
|
||||
this.writeItemsToLocalStorage(saved, false, null);
|
||||
this.writeItemsToLocalStorage(retrieved, false, null);
|
||||
|
||||
@@ -169,14 +197,17 @@ class SyncManager {
|
||||
this.syncToken = response.sync_token;
|
||||
this.cursorToken = response.cursor_token;
|
||||
|
||||
if(this.cursorToken || this.repeatOnCompletion || this.needsMoreSync) {
|
||||
if(this.cursorToken || this.syncStatus.needsMoreSync) {
|
||||
setTimeout(function () {
|
||||
this.sync(callback, options);
|
||||
}.bind(this), 10); // wait 10ms to allow UI to update
|
||||
} else if(this.repeatOnCompletion) {
|
||||
this.repeatOnCompletion = false;
|
||||
setTimeout(function () {
|
||||
this.sync(callback, options);
|
||||
}.bind(this), 10); // wait 10ms to allow UI to update
|
||||
} else {
|
||||
if(callback) {
|
||||
callback(response);
|
||||
}
|
||||
this.callQueuedCallbacksAndCurrent(callback, response);
|
||||
}
|
||||
|
||||
}.bind(this))
|
||||
@@ -190,9 +221,7 @@ class SyncManager {
|
||||
|
||||
this.$rootScope.$broadcast("sync:error", error);
|
||||
|
||||
if(callback) {
|
||||
callback({error: "Sync error"});
|
||||
}
|
||||
this.callQueuedCallbacksAndCurrent(callback, {error: "Sync error"});
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
@@ -211,7 +240,7 @@ class SyncManager {
|
||||
var item = this.modelManager.findItem(itemResponse.uuid);
|
||||
var error = mapping.error;
|
||||
if(error.tag == "uuid_conflict") {
|
||||
// uuid conflicts can occur if a user attempts to import an old data archive with uuids form the old account into a new account
|
||||
// uuid conflicts can occur if a user attempts to import an old data archive with uuids from the old account into a new account
|
||||
this.modelManager.alternateUUIDForItem(item, handleNext);
|
||||
}
|
||||
++i;
|
||||
|
||||
Reference in New Issue
Block a user