Merge pull request #58 from standardnotes/sync-provider-destruction
Sync refactor
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var Neeto = Neeto || {};
|
var Neeto = Neeto || {};
|
||||||
|
var SN = SN || {};
|
||||||
|
|
||||||
// detect IE8 and above, and edge.
|
// detect IE8 and above, and edge.
|
||||||
// IE and Edge do not support pbkdf2 in WebCrypto, therefore we need to use CryptoJS
|
// IE and Edge do not support pbkdf2 in WebCrypto, therefore we need to use CryptoJS
|
||||||
@@ -17,12 +18,9 @@ angular.module('app.frontend', [
|
|||||||
'restangular'
|
'restangular'
|
||||||
])
|
])
|
||||||
|
|
||||||
.config(function (RestangularProvider, apiControllerProvider) {
|
.config(function (RestangularProvider, authManagerProvider) {
|
||||||
RestangularProvider.setDefaultHeaders({"Content-Type": "application/json"});
|
RestangularProvider.setDefaultHeaders({"Content-Type": "application/json"});
|
||||||
|
|
||||||
var url = apiControllerProvider.defaultServerURL();
|
|
||||||
RestangularProvider.setBaseUrl(url + "/api");
|
|
||||||
|
|
||||||
RestangularProvider.setFullRequestInterceptor(function(element, operation, route, url, headers, params, httpConfig) {
|
RestangularProvider.setFullRequestInterceptor(function(element, operation, route, url, headers, params, httpConfig) {
|
||||||
var token = localStorage.getItem("jwt");
|
var token = localStorage.getItem("jwt");
|
||||||
if(token) {
|
if(token) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
class BaseCtrl {
|
class BaseCtrl {
|
||||||
constructor($rootScope, modelManager, apiController, dbManager) {
|
constructor(syncManager, dbManager) {
|
||||||
dbManager.openDatabase(null, function(){
|
dbManager.openDatabase(null, function(){
|
||||||
// new database, delete syncToken so that items can be refetched entirely from server
|
// new database, delete syncToken so that items can be refetched entirely from server
|
||||||
apiController.clearSyncToken();
|
syncManager.clearSyncToken();
|
||||||
apiController.sync();
|
syncManager.sync();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ angular.module('app.frontend')
|
|||||||
/**
|
/**
|
||||||
* Insert 4 spaces when a tab key is pressed,
|
* Insert 4 spaces when a tab key is pressed,
|
||||||
* only used when inside of the text editor.
|
* only used when inside of the text editor.
|
||||||
* If the shift key is pressed first, this event is
|
* If the shift key is pressed first, this event is
|
||||||
* not fired.
|
* not fired.
|
||||||
*/
|
*/
|
||||||
var handleTab = function (event) {
|
var handleTab = function (event) {
|
||||||
if (!event.shiftKey && event.which == 9) {
|
if (!event.shiftKey && event.which == 9) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -29,13 +29,13 @@ angular.module('app.frontend')
|
|||||||
var end = this.selectionEnd;
|
var end = this.selectionEnd;
|
||||||
var spaces = " ";
|
var spaces = " ";
|
||||||
|
|
||||||
// Insert 4 spaces
|
// Insert 4 spaces
|
||||||
this.value = this.value.substring(0, start)
|
this.value = this.value.substring(0, start)
|
||||||
+ spaces + this.value.substring(end);
|
+ spaces + this.value.substring(end);
|
||||||
|
|
||||||
// Place cursor 4 spaces away from where
|
// Place cursor 4 spaces away from where
|
||||||
// the tab key was pressed
|
// the tab key was pressed
|
||||||
this.selectionStart = this.selectionEnd = start + 4;
|
this.selectionStart = this.selectionEnd = start + 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.controller('EditorCtrl', function ($sce, $timeout, apiController, markdownRenderer, $rootScope, extensionManager) {
|
.controller('EditorCtrl', function ($sce, $timeout, authManager, markdownRenderer, $rootScope, extensionManager, syncManager) {
|
||||||
|
|
||||||
this.setNote = function(note, oldNote) {
|
this.setNote = function(note, oldNote) {
|
||||||
this.editorMode = 'edit';
|
this.editorMode = 'edit';
|
||||||
@@ -146,16 +146,20 @@ angular.module('app.frontend')
|
|||||||
note.dummy = false;
|
note.dummy = false;
|
||||||
this.save()(note, function(success){
|
this.save()(note, function(success){
|
||||||
if(success) {
|
if(success) {
|
||||||
apiController.clearDraft();
|
|
||||||
|
|
||||||
if(statusTimeout) $timeout.cancel(statusTimeout);
|
if(statusTimeout) $timeout.cancel(statusTimeout);
|
||||||
statusTimeout = $timeout(function(){
|
statusTimeout = $timeout(function(){
|
||||||
this.noteStatus = "All changes saved"
|
var status = "All changes saved"
|
||||||
|
if(authManager.offline()) {
|
||||||
|
status += " (offline)";
|
||||||
|
}
|
||||||
|
this.saveError = false;
|
||||||
|
this.noteStatus = status;
|
||||||
}.bind(this), 200)
|
}.bind(this), 200)
|
||||||
} else {
|
} else {
|
||||||
if(statusTimeout) $timeout.cancel(statusTimeout);
|
if(statusTimeout) $timeout.cancel(statusTimeout);
|
||||||
statusTimeout = $timeout(function(){
|
statusTimeout = $timeout(function(){
|
||||||
this.noteStatus = "(Offline) — All changes saved"
|
this.saveError = true;
|
||||||
|
this.noteStatus = "Error saving"
|
||||||
}.bind(this), 200)
|
}.bind(this), 200)
|
||||||
}
|
}
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
@@ -171,10 +175,6 @@ angular.module('app.frontend')
|
|||||||
this.changesMade = function() {
|
this.changesMade = function() {
|
||||||
this.note.hasChanges = true;
|
this.note.hasChanges = true;
|
||||||
this.note.dummy = false;
|
this.note.dummy = false;
|
||||||
if(apiController.isUserSignedIn()) {
|
|
||||||
// signed out users have local autosave, dont need draft saving
|
|
||||||
apiController.saveDraftToDisk(this.note);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(saveTimeout) $timeout.cancel(saveTimeout);
|
if(saveTimeout) $timeout.cancel(saveTimeout);
|
||||||
if(statusTimeout) $timeout.cancel(statusTimeout);
|
if(statusTimeout) $timeout.cancel(statusTimeout);
|
||||||
@@ -236,9 +236,10 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.deleteNote = function() {
|
this.deleteNote = function() {
|
||||||
apiController.clearDraft();
|
if(confirm("Are you sure you want to delete this note?")) {
|
||||||
this.remove()(this.note);
|
this.remove()(this.note);
|
||||||
this.showMenu = false;
|
this.showMenu = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.clickedEditNote = function() {
|
this.clickedEditNote = function() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
angular.module('app.frontend')
|
angular.module('app.frontend')
|
||||||
.directive("header", function(apiController, extensionManager){
|
.directive("header", function(authManager){
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
scope: {},
|
scope: {},
|
||||||
@@ -12,119 +12,54 @@ angular.module('app.frontend')
|
|||||||
link:function(scope, elem, attrs, ctrl) {
|
link:function(scope, elem, attrs, ctrl) {
|
||||||
scope.$on("sync:updated_token", function(){
|
scope.$on("sync:updated_token", function(){
|
||||||
ctrl.syncUpdated();
|
ctrl.syncUpdated();
|
||||||
|
ctrl.findErrors();
|
||||||
|
ctrl.updateOfflineStatus();
|
||||||
|
})
|
||||||
|
scope.$on("sync:error", function(){
|
||||||
|
ctrl.findErrors();
|
||||||
|
ctrl.updateOfflineStatus();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.controller('HeaderCtrl', function ($state, apiController, modelManager, $timeout, extensionManager, dbManager) {
|
.controller('HeaderCtrl', function (authManager, modelManager, $timeout, dbManager, syncManager) {
|
||||||
|
|
||||||
this.user = apiController.user;
|
this.user = authManager.user;
|
||||||
this.extensionManager = extensionManager;
|
|
||||||
this.loginData = {mergeLocal: true};
|
|
||||||
|
|
||||||
this.changePasswordPressed = function() {
|
this.updateOfflineStatus = function() {
|
||||||
this.showNewPasswordForm = !this.showNewPasswordForm;
|
this.offline = authManager.offline();
|
||||||
}
|
}
|
||||||
|
this.updateOfflineStatus();
|
||||||
|
|
||||||
|
this.findErrors = function() {
|
||||||
|
this.error = syncManager.syncStatus.error;
|
||||||
|
}
|
||||||
|
this.findErrors();
|
||||||
|
|
||||||
this.accountMenuPressed = function() {
|
this.accountMenuPressed = function() {
|
||||||
this.serverData = {url: apiController.getServer()};
|
this.serverData = {};
|
||||||
this.showAccountMenu = !this.showAccountMenu;
|
this.showAccountMenu = !this.showAccountMenu;
|
||||||
this.showFaq = false;
|
this.showFaq = false;
|
||||||
this.showNewPasswordForm = false;
|
this.showNewPasswordForm = false;
|
||||||
this.showExtensionsMenu = false;
|
this.showExtensionsMenu = false;
|
||||||
|
this.showIOMenu = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.toggleExtensions = function() {
|
this.toggleExtensions = function() {
|
||||||
this.showAccountMenu = false;
|
this.showAccountMenu = false;
|
||||||
|
this.showIOMenu = false;
|
||||||
this.showExtensionsMenu = !this.showExtensionsMenu;
|
this.showExtensionsMenu = !this.showExtensionsMenu;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.toggleExtensionForm = function() {
|
this.toggleIO = function() {
|
||||||
this.newExtensionData = {};
|
this.showIOMenu = !this.showIOMenu;
|
||||||
this.showNewExtensionForm = !this.showNewExtensionForm;
|
this.showExtensionsMenu = false;
|
||||||
}
|
|
||||||
|
|
||||||
this.submitNewExtensionForm = function() {
|
|
||||||
if(this.newExtensionData.url) {
|
|
||||||
extensionManager.addExtension(this.newExtensionData.url, function(response){
|
|
||||||
if(!response) {
|
|
||||||
alert("Unable to register this extension. Make sure the link is valid and try again.");
|
|
||||||
} else {
|
|
||||||
this.newExtensionData.url = "";
|
|
||||||
this.showNewExtensionForm = false;
|
|
||||||
}
|
|
||||||
}.bind(this))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedAction = function(action, extension) {
|
|
||||||
action.running = true;
|
|
||||||
extensionManager.executeAction(action, extension, null, function(response){
|
|
||||||
action.running = false;
|
|
||||||
if(response && response.error) {
|
|
||||||
action.error = true;
|
|
||||||
alert("There was an error performing this action. Please try again.");
|
|
||||||
} else {
|
|
||||||
action.error = false;
|
|
||||||
apiController.sync(null);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.deleteExtension = function(extension) {
|
|
||||||
if(confirm("Are you sure you want to delete this extension?")) {
|
|
||||||
extensionManager.deleteExtension(extension);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reloadExtensionsPressed = function() {
|
|
||||||
if(confirm("For your security, reloading extensions will disable any currently enabled repeat actions.")) {
|
|
||||||
extensionManager.refreshExtensionsFromServer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.changeServer = function() {
|
|
||||||
apiController.setServer(this.serverData.url, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.signOutPressed = function() {
|
|
||||||
this.showAccountMenu = false;
|
this.showAccountMenu = false;
|
||||||
apiController.signout(function(){
|
|
||||||
window.location.reload();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.submitPasswordChange = function() {
|
|
||||||
this.passwordChangeData.status = "Generating New Keys...";
|
|
||||||
|
|
||||||
$timeout(function(){
|
|
||||||
if(data.password != data.password_confirmation) {
|
|
||||||
alert("Your new password does not match its confirmation.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
apiController.changePassword(this.passwordChangeData.current_password, this.passwordChangeData.new_password, function(response){
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
}.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.localNotesCount = function() {
|
|
||||||
return modelManager.filteredNotes.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mergeLocalChanged = function() {
|
|
||||||
if(!this.loginData.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?")) {
|
|
||||||
this.loginData.mergeLocal = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refreshData = function() {
|
this.refreshData = function() {
|
||||||
this.isRefreshing = true;
|
this.isRefreshing = true;
|
||||||
apiController.sync(function(response){
|
syncManager.sync(function(response){
|
||||||
$timeout(function(){
|
$timeout(function(){
|
||||||
this.isRefreshing = false;
|
this.isRefreshing = false;
|
||||||
}.bind(this), 200)
|
}.bind(this), 200)
|
||||||
@@ -139,110 +74,4 @@ angular.module('app.frontend')
|
|||||||
this.syncUpdated = function() {
|
this.syncUpdated = function() {
|
||||||
this.lastSyncDate = new Date();
|
this.lastSyncDate = new Date();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
this.loginSubmitPressed = function() {
|
|
||||||
this.loginData.status = "Generating Login Keys...";
|
|
||||||
|
|
||||||
$timeout(function(){
|
|
||||||
apiController.login(this.loginData.email, this.loginData.user_password, function(response){
|
|
||||||
if(!response || response.error) {
|
|
||||||
var error = response ? response.error : {message: "An unknown error occured."}
|
|
||||||
this.loginData.status = null;
|
|
||||||
if(!response || (response && !response.didDisplayAlert)) {
|
|
||||||
alert(error.message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.onAuthSuccess(response.user);
|
|
||||||
}
|
|
||||||
}.bind(this));
|
|
||||||
}.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.submitRegistrationForm = function() {
|
|
||||||
this.loginData.status = "Generating Account Keys...";
|
|
||||||
|
|
||||||
$timeout(function(){
|
|
||||||
apiController.register(this.loginData.email, this.loginData.user_password, function(response){
|
|
||||||
if(!response || response.error) {
|
|
||||||
var error = response ? response.error : {message: "An unknown error occured."}
|
|
||||||
this.loginData.status = null;
|
|
||||||
alert(error.message);
|
|
||||||
} else {
|
|
||||||
this.onAuthSuccess(response.user);
|
|
||||||
}
|
|
||||||
}.bind(this));
|
|
||||||
}.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.encryptionStatusForNotes = function() {
|
|
||||||
var allNotes = modelManager.filteredNotes;
|
|
||||||
return allNotes.length + "/" + allNotes.length + " notes encrypted";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.archiveEncryptionFormat = {encrypted: true};
|
|
||||||
|
|
||||||
this.downloadDataArchive = function() {
|
|
||||||
var link = document.createElement('a');
|
|
||||||
link.setAttribute('download', 'notes.json');
|
|
||||||
link.href = apiController.itemsDataFile(this.archiveEncryptionFormat.encrypted);
|
|
||||||
link.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.performImport = function(data, password) {
|
|
||||||
this.importData.loading = true;
|
|
||||||
// allow loading indicator to come up with timeout
|
|
||||||
$timeout(function(){
|
|
||||||
apiController.importJSONData(data, password, function(success, response){
|
|
||||||
console.log("Import response:", success, response);
|
|
||||||
this.importData.loading = false;
|
|
||||||
if(success) {
|
|
||||||
this.importData = null;
|
|
||||||
} else {
|
|
||||||
alert("There was an error importing your data. Please try again.");
|
|
||||||
}
|
|
||||||
}.bind(this))
|
|
||||||
}.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.submitImportPassword = function() {
|
|
||||||
this.performImport(this.importData.data, this.importData.password);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.importFileSelected = function(files) {
|
|
||||||
this.importData = {};
|
|
||||||
|
|
||||||
var file = files[0];
|
|
||||||
var reader = new FileReader();
|
|
||||||
reader.onload = function(e) {
|
|
||||||
var data = JSON.parse(e.target.result);
|
|
||||||
$timeout(function(){
|
|
||||||
if(data.auth_params) {
|
|
||||||
// request password
|
|
||||||
this.importData.requestPassword = true;
|
|
||||||
this.importData.data = data;
|
|
||||||
} else {
|
|
||||||
this.performImport(data, null);
|
|
||||||
}
|
|
||||||
}.bind(this))
|
|
||||||
}.bind(this)
|
|
||||||
|
|
||||||
reader.readAsText(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onAuthSuccess = function(user) {
|
|
||||||
var block = function(){
|
|
||||||
window.location.reload();
|
|
||||||
this.showLogin = false;
|
|
||||||
this.showRegistration = false;
|
|
||||||
}.bind(this);
|
|
||||||
|
|
||||||
if(!this.loginData.mergeLocal) {
|
|
||||||
dbManager.clearAllItems(function(){
|
|
||||||
block();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
block();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
angular.module('app.frontend')
|
angular.module('app.frontend')
|
||||||
.controller('HomeCtrl', function ($scope, $rootScope, $timeout, apiController, modelManager) {
|
.controller('HomeCtrl', function ($scope, $rootScope, $timeout, modelManager, syncManager, authManager) {
|
||||||
$rootScope.bodyClass = "app-body-class";
|
$rootScope.bodyClass = "app-body-class";
|
||||||
|
|
||||||
apiController.loadLocalItems(function(items){
|
syncManager.loadLocalItems(function(items) {
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
|
|
||||||
apiController.sync(null);
|
syncManager.sync(null);
|
||||||
// refresh every 30s
|
// refresh every 30s
|
||||||
setInterval(function () {
|
// setInterval(function () {
|
||||||
apiController.sync(null);
|
// syncManager.sync(null);
|
||||||
}, 30000);
|
// }, 30000);
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.allTag = new Tag({all: true});
|
$scope.allTag = new Tag({all: true});
|
||||||
@@ -31,7 +31,7 @@ angular.module('app.frontend')
|
|||||||
modelManager.createRelationshipBetweenItems(note, tag);
|
modelManager.createRelationshipBetweenItems(note, tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
apiController.sync();
|
syncManager.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -56,8 +56,12 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
|
|
||||||
$scope.tagsSave = function(tag, callback) {
|
$scope.tagsSave = function(tag, callback) {
|
||||||
|
if(!tag.title || tag.title.length == 0) {
|
||||||
|
$scope.notesRemoveTag(tag);
|
||||||
|
return;
|
||||||
|
}
|
||||||
tag.setDirty(true);
|
tag.setDirty(true);
|
||||||
apiController.sync(callback);
|
syncManager.sync(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -69,12 +73,9 @@ angular.module('app.frontend')
|
|||||||
if(validNotes == 0) {
|
if(validNotes == 0) {
|
||||||
modelManager.setItemToBeDeleted(tag);
|
modelManager.setItemToBeDeleted(tag);
|
||||||
// if no more notes, delete tag
|
// if no more notes, delete tag
|
||||||
apiController.sync(function(){
|
syncManager.sync(function(){
|
||||||
// force scope tags to update on sub directives
|
// force scope tags to update on sub directives
|
||||||
$scope.tags = [];
|
$scope.safeApply();
|
||||||
$timeout(function(){
|
|
||||||
$scope.tags = modelManager.tags;
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
alert("To delete this tag, remove all its notes first.");
|
alert("To delete this tag, remove all its notes first.");
|
||||||
@@ -100,7 +101,7 @@ angular.module('app.frontend')
|
|||||||
$scope.saveNote = function(note, callback) {
|
$scope.saveNote = function(note, callback) {
|
||||||
note.setDirty(true);
|
note.setDirty(true);
|
||||||
|
|
||||||
apiController.sync(function(response){
|
syncManager.sync(function(response){
|
||||||
if(response && response.error) {
|
if(response && response.error) {
|
||||||
if(!$scope.didShowErrorAlert) {
|
if(!$scope.didShowErrorAlert) {
|
||||||
$scope.didShowErrorAlert = true;
|
$scope.didShowErrorAlert = true;
|
||||||
@@ -137,8 +138,8 @@ angular.module('app.frontend')
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
apiController.sync(function(){
|
syncManager.sync(function(){
|
||||||
if(!apiController.user) {
|
if(authManager.offline()) {
|
||||||
// when deleting items while ofline, we need to explictly tell angular to refresh UI
|
// when deleting items while ofline, we need to explictly tell angular to refresh UI
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
$scope.safeApply();
|
$scope.safeApply();
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.controller('NotesCtrl', function (apiController, $timeout, $rootScope, modelManager) {
|
.controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager) {
|
||||||
|
|
||||||
$rootScope.$on("editorFocused", function(){
|
$rootScope.$on("editorFocused", function(){
|
||||||
this.showMenu = false;
|
this.showMenu = false;
|
||||||
@@ -32,6 +32,11 @@ angular.module('app.frontend')
|
|||||||
|
|
||||||
var isFirstLoad = true;
|
var isFirstLoad = true;
|
||||||
|
|
||||||
|
this.notesToDisplay = 20;
|
||||||
|
this.paginate = function() {
|
||||||
|
this.notesToDisplay += 20
|
||||||
|
}
|
||||||
|
|
||||||
this.tagDidChange = function(tag, oldTag) {
|
this.tagDidChange = function(tag, oldTag) {
|
||||||
this.showMenu = false;
|
this.showMenu = false;
|
||||||
|
|
||||||
@@ -48,14 +53,8 @@ angular.module('app.frontend')
|
|||||||
|
|
||||||
if(isFirstLoad) {
|
if(isFirstLoad) {
|
||||||
$timeout(function(){
|
$timeout(function(){
|
||||||
var draft = apiController.getDraft();
|
this.createNewNote();
|
||||||
if(draft) {
|
isFirstLoad = false;
|
||||||
var note = draft;
|
|
||||||
this.selectNote(note);
|
|
||||||
} else {
|
|
||||||
this.createNewNote();
|
|
||||||
isFirstLoad = false;
|
|
||||||
}
|
|
||||||
}.bind(this))
|
}.bind(this))
|
||||||
} else if(tag.notes.length == 0) {
|
} else if(tag.notes.length == 0) {
|
||||||
this.createNewNote();
|
this.createNewNote();
|
||||||
|
|||||||
@@ -79,19 +79,20 @@ angular.module('app.frontend')
|
|||||||
|
|
||||||
this.saveTag = function($event, tag) {
|
this.saveTag = function($event, tag) {
|
||||||
this.editingTag = null;
|
this.editingTag = null;
|
||||||
if(tag.title.length == 0) {
|
$event.target.blur();
|
||||||
tag.title = originalTagName;
|
|
||||||
originalTagName = "";
|
if(!tag.title || tag.title.length == 0) {
|
||||||
|
if(originalTagName) {
|
||||||
|
tag.title = originalTagName;
|
||||||
|
originalTagName = null;
|
||||||
|
} else {
|
||||||
|
// newly created tag without content
|
||||||
|
modelManager.removeItemLocally(tag);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$event.target.blur();
|
|
||||||
if(!tag.title || tag.title.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.save()(tag, function(savedTag){
|
this.save()(tag, function(savedTag){
|
||||||
// _.merge(tag, savedTag);
|
|
||||||
this.selectTag(tag);
|
this.selectTag(tag);
|
||||||
this.newTag = null;
|
this.newTag = null;
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|||||||
@@ -50,10 +50,6 @@ class Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
alternateUUID() {
|
|
||||||
this.uuid = Neeto.crypto.generateUUID();
|
|
||||||
}
|
|
||||||
|
|
||||||
setDirty(dirty) {
|
setDirty(dirty) {
|
||||||
this.dirty = dirty;
|
this.dirty = dirty;
|
||||||
|
|
||||||
@@ -107,6 +103,10 @@ class Item {
|
|||||||
// must override
|
// must override
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isBeingRemovedLocally() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
removeAllRelationships() {
|
removeAllRelationships() {
|
||||||
// must override
|
// must override
|
||||||
this.setDirty(true);
|
this.setDirty(true);
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
class Action {
|
class Action {
|
||||||
constructor(json) {
|
constructor(json) {
|
||||||
_.merge(this, json);
|
_.merge(this, json);
|
||||||
this.running = false; // in case running=true was synced with server since model is uploaded nondiscriminatory
|
this.running = false; // in case running=true was synced with server since model is uploaded nondiscriminatory
|
||||||
this.error = false;
|
this.error = false;
|
||||||
if(this.lastExecuted) {
|
if(this.lastExecuted) {
|
||||||
// is string
|
// is string
|
||||||
this.lastExecuted = new Date(this.lastExecuted);
|
this.lastExecuted = new Date(this.lastExecuted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get permissionsString() {
|
permissionsString() {
|
||||||
|
console.log("permissions", this.permissions);
|
||||||
if(!this.permissions) {
|
if(!this.permissions) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
var permission = this.permissions.charAt(0).toUpperCase() + this.permissions.slice(1); // capitalize first letter
|
var permission = this.permissions.charAt(0).toUpperCase() + this.permissions.slice(1); // capitalize first letter
|
||||||
permission += ": ";
|
permission += ": ";
|
||||||
for(var contentType of this.content_types) {
|
for(var contentType of this.content_types) {
|
||||||
@@ -28,7 +30,7 @@ class Action {
|
|||||||
return permission;
|
return permission;
|
||||||
}
|
}
|
||||||
|
|
||||||
get encryptionModeString() {
|
encryptionModeString() {
|
||||||
if(this.verb != "post") {
|
if(this.verb != "post") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -54,6 +56,12 @@ class Extension extends Item {
|
|||||||
|
|
||||||
this.encrypted = true;
|
this.encrypted = true;
|
||||||
this.content_type = "Extension";
|
this.content_type = "Extension";
|
||||||
|
|
||||||
|
if(json.actions) {
|
||||||
|
this.actions = json.actions.map(function(action){
|
||||||
|
return new Action(action);
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actionsInGlobalContext() {
|
actionsInGlobalContext() {
|
||||||
|
|||||||
@@ -56,6 +56,13 @@ class Note extends Item {
|
|||||||
this.tags = [];
|
this.tags = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isBeingRemovedLocally() {
|
||||||
|
this.tags.forEach(function(tag){
|
||||||
|
_.pull(tag.notes, this);
|
||||||
|
}.bind(this))
|
||||||
|
super.isBeingRemovedLocally();
|
||||||
|
}
|
||||||
|
|
||||||
static filterDummyNotes(notes) {
|
static filterDummyNotes(notes) {
|
||||||
var filtered = notes.filter(function(note){return note.dummy == false || note.dummy == null});
|
var filtered = notes.filter(function(note){return note.dummy == false || note.dummy == null});
|
||||||
return filtered;
|
return filtered;
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ class Tag extends Item {
|
|||||||
this.notes = [];
|
this.notes = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isBeingRemovedLocally() {
|
||||||
|
this.notes.forEach(function(note){
|
||||||
|
_.pull(note.tags, this);
|
||||||
|
}.bind(this))
|
||||||
|
super.isBeingRemovedLocally();
|
||||||
|
}
|
||||||
|
|
||||||
get content_type() {
|
get content_type() {
|
||||||
return "Tag";
|
return "Tag";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
class ItemParams {
|
||||||
|
|
||||||
|
constructor(item, ek) {
|
||||||
|
this.item = item;
|
||||||
|
this.ek = ek;
|
||||||
|
}
|
||||||
|
|
||||||
|
paramsForExportFile() {
|
||||||
|
this.additionalFields = ["updated_at"];
|
||||||
|
this.forExportFile = true;
|
||||||
|
return _.omit(this.__params(), ["deleted"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
paramsForExtension() {
|
||||||
|
return this.paramsForExportFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
paramsForLocalStorage() {
|
||||||
|
this.additionalFields = ["updated_at", "dirty"];
|
||||||
|
this.forExportFile = true;
|
||||||
|
return this.__params();
|
||||||
|
}
|
||||||
|
|
||||||
|
paramsForSync() {
|
||||||
|
return this.__params(null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
__params() {
|
||||||
|
var itemCopy = _.cloneDeep(this.item);
|
||||||
|
|
||||||
|
console.assert(!this.item.dummy, "Item is dummy, should not have gotten here.", this.item.dummy)
|
||||||
|
|
||||||
|
var params = {uuid: this.item.uuid, content_type: this.item.content_type, deleted: this.item.deleted, created_at: this.item.created_at};
|
||||||
|
|
||||||
|
if(this.ek) {
|
||||||
|
EncryptionHelper.encryptItem(itemCopy, this.ek);
|
||||||
|
params.content = itemCopy.content;
|
||||||
|
params.enc_item_key = itemCopy.enc_item_key;
|
||||||
|
params.auth_hash = itemCopy.auth_hash;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
params.content = this.forExportFile ? itemCopy.createContentJSONFromProperties() : "000" + Neeto.crypto.base64(JSON.stringify(itemCopy.createContentJSONFromProperties()));
|
||||||
|
if(!this.forExportFile) {
|
||||||
|
params.enc_item_key = null;
|
||||||
|
params.auth_hash = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.additionalFields) {
|
||||||
|
_.merge(params, _.pick(this.item, this.additionalFields));
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,607 +0,0 @@
|
|||||||
angular.module('app.frontend')
|
|
||||||
.provider('apiController', function () {
|
|
||||||
|
|
||||||
function domainName() {
|
|
||||||
var domain_comps = location.hostname.split(".");
|
|
||||||
var domain = domain_comps[domain_comps.length - 2] + "." + domain_comps[domain_comps.length - 1];
|
|
||||||
return domain;
|
|
||||||
}
|
|
||||||
|
|
||||||
var url;
|
|
||||||
|
|
||||||
this.defaultServerURL = function() {
|
|
||||||
if(!url) {
|
|
||||||
url = localStorage.getItem("server");
|
|
||||||
if(!url) {
|
|
||||||
url = "https://n3.standardnotes.org";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
this.$get = function($rootScope, Restangular, modelManager, dbManager) {
|
|
||||||
return new ApiController($rootScope, Restangular, modelManager, dbManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ApiController($rootScope, Restangular, modelManager, dbManager) {
|
|
||||||
|
|
||||||
var userData = localStorage.getItem("user");
|
|
||||||
if(userData) {
|
|
||||||
this.user = JSON.parse(userData);
|
|
||||||
} else {
|
|
||||||
// legacy, check for uuid
|
|
||||||
var idData = localStorage.getItem("uuid");
|
|
||||||
if(idData) {
|
|
||||||
this.user = {uuid: idData};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.syncToken = localStorage.getItem("syncToken");
|
|
||||||
|
|
||||||
/*
|
|
||||||
Config
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.getServer = function() {
|
|
||||||
if(!url) {
|
|
||||||
url = localStorage.getItem("server");
|
|
||||||
if(!url) {
|
|
||||||
url = "https://n3.standardnotes.org";
|
|
||||||
this.setServer(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setServer = function(url, refresh) {
|
|
||||||
localStorage.setItem("server", url);
|
|
||||||
if(refresh) {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
Auth
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.getAuthParams = function() {
|
|
||||||
return JSON.parse(localStorage.getItem("auth_params"));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isUserSignedIn = function() {
|
|
||||||
return localStorage.getItem("jwt");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getAuthParamsForEmail = function(email, callback) {
|
|
||||||
var request = Restangular.one("auth", "params");
|
|
||||||
request.get({email: email}).then(function(response){
|
|
||||||
callback(response.plain());
|
|
||||||
})
|
|
||||||
.catch(function(response){
|
|
||||||
console.log("Error getting current user", response);
|
|
||||||
callback(response.data);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.supportsPasswordDerivationCost = function(cost) {
|
|
||||||
// some passwords are created on platforms with stronger pbkdf2 capabilities, like iOS,
|
|
||||||
// which accidentally used 60,000 iterations (now adjusted), which CryptoJS can't handle here (WebCrypto can however).
|
|
||||||
// if user has high password cost and is using browser that doesn't support WebCrypto,
|
|
||||||
// we want to tell them that they can't login with this browser.
|
|
||||||
if(cost > 5000) {
|
|
||||||
return Neeto.crypto instanceof SNCryptoWeb ? true : false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.login = function(email, password, callback) {
|
|
||||||
this.getAuthParamsForEmail(email, function(authParams){
|
|
||||||
if(!authParams) {
|
|
||||||
callback(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!this.supportsPasswordDerivationCost(authParams.pw_cost)) {
|
|
||||||
var string = "Your account was created on a platform with higher security capabilities than this browser supports. " +
|
|
||||||
"If we attempted to generate your login keys here, it would take hours. " +
|
|
||||||
"Please use a browser with more up to date security capabilities, like Google Chrome or Firefox, to login."
|
|
||||||
alert(string)
|
|
||||||
callback({didDisplayAlert: true});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, authParams), function(keys){
|
|
||||||
this.setMk(keys.mk);
|
|
||||||
var request = Restangular.one("auth/sign_in");
|
|
||||||
var params = {password: keys.pw, email: email};
|
|
||||||
_.merge(request, params);
|
|
||||||
request.post().then(function(response){
|
|
||||||
localStorage.setItem("jwt", response.token);
|
|
||||||
localStorage.setItem("user", JSON.stringify(response.user));
|
|
||||||
localStorage.setItem("auth_params", JSON.stringify(authParams));
|
|
||||||
callback(response);
|
|
||||||
})
|
|
||||||
.catch(function(response){
|
|
||||||
callback(response.data);
|
|
||||||
})
|
|
||||||
}.bind(this));
|
|
||||||
}.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.register = function(email, password, callback) {
|
|
||||||
Neeto.crypto.generateInitialEncryptionKeysForUser({password: password, email: email}, function(keys, authParams){
|
|
||||||
this.setMk(keys.mk);
|
|
||||||
keys.mk = null;
|
|
||||||
var request = Restangular.one("auth");
|
|
||||||
var params = _.merge({password: keys.pw, email: email}, authParams);
|
|
||||||
_.merge(request, params);
|
|
||||||
request.post().then(function(response){
|
|
||||||
localStorage.setItem("jwt", response.token);
|
|
||||||
localStorage.setItem("user", JSON.stringify(response.user));
|
|
||||||
localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"])));
|
|
||||||
callback(response);
|
|
||||||
})
|
|
||||||
.catch(function(response){
|
|
||||||
callback(response.data);
|
|
||||||
})
|
|
||||||
}.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._performPasswordChange = function(email, current_keys, new_keys, callback) {
|
|
||||||
var request = Restangular.one("auth");
|
|
||||||
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);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
Items
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.setSyncToken = function(syncToken) {
|
|
||||||
this.syncToken = syncToken;
|
|
||||||
localStorage.setItem("syncToken", this.syncToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncWithOptions = function(callback, options = {}) {
|
|
||||||
|
|
||||||
if(this.syncOpInProgress) {
|
|
||||||
// will perform anoter sync after current completes
|
|
||||||
this.repeatSync = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncOpInProgress = true;
|
|
||||||
|
|
||||||
var allDirtyItems = modelManager.getDirtyItems();
|
|
||||||
|
|
||||||
// we want to write all dirty items to disk only if the user is not signed in, or if the sync op fails
|
|
||||||
// if the sync op succeeds, these items will be written to disk by handling the "saved_items" response from the server
|
|
||||||
var writeAllDirtyItemsToDisk = function(completion) {
|
|
||||||
this.writeItemsToLocalStorage(allDirtyItems, function(responseItems){
|
|
||||||
if(completion) {
|
|
||||||
completion();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}.bind(this);
|
|
||||||
|
|
||||||
if(!this.isUserSignedIn()) {
|
|
||||||
writeAllDirtyItemsToDisk(function(){
|
|
||||||
// delete anything needing to be deleted
|
|
||||||
allDirtyItems.forEach(function(item){
|
|
||||||
if(item.deleted) {
|
|
||||||
modelManager.removeItemLocally(item);
|
|
||||||
}
|
|
||||||
}.bind(this))
|
|
||||||
|
|
||||||
modelManager.clearDirtyItems(allDirtyItems);
|
|
||||||
|
|
||||||
}.bind(this))
|
|
||||||
this.syncOpInProgress = false;
|
|
||||||
if(callback) {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let submitLimit = 100;
|
|
||||||
var dirtyItems = allDirtyItems.slice(0, submitLimit);
|
|
||||||
if(dirtyItems.length < allDirtyItems.length) {
|
|
||||||
// more items left to be synced, repeat
|
|
||||||
this.repeatSync = true;
|
|
||||||
} else {
|
|
||||||
this.repeatSync = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = Restangular.one("items/sync");
|
|
||||||
request.limit = 150;
|
|
||||||
request.sync_token = this.syncToken;
|
|
||||||
request.cursor_token = this.cursorToken;
|
|
||||||
request.items = _.map(dirtyItems, function(item){
|
|
||||||
return this.createRequestParamsForItem(item, options.additionalFields);
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
request.post().then(function(response) {
|
|
||||||
|
|
||||||
modelManager.clearDirtyItems(dirtyItems);
|
|
||||||
|
|
||||||
// handle sync token
|
|
||||||
this.setSyncToken(response.sync_token);
|
|
||||||
$rootScope.$broadcast("sync:updated_token", this.syncToken);
|
|
||||||
|
|
||||||
// handle cursor token (more results waiting, perform another sync)
|
|
||||||
this.cursorToken = response.cursor_token;
|
|
||||||
|
|
||||||
var retrieved = this.handleItemsResponse(response.retrieved_items, null);
|
|
||||||
// merge only metadata for saved items
|
|
||||||
var omitFields = ["content", "auth_hash"];
|
|
||||||
var saved = this.handleItemsResponse(response.saved_items, omitFields);
|
|
||||||
|
|
||||||
this.handleUnsavedItemsResponse(response.unsaved)
|
|
||||||
|
|
||||||
this.writeItemsToLocalStorage(saved, null);
|
|
||||||
this.writeItemsToLocalStorage(retrieved, null);
|
|
||||||
|
|
||||||
this.syncOpInProgress = false;
|
|
||||||
|
|
||||||
if(this.cursorToken || this.repeatSync == true) {
|
|
||||||
this.syncWithOptions(callback, options);
|
|
||||||
} else {
|
|
||||||
if(callback) {
|
|
||||||
callback(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function(response){
|
|
||||||
console.log("Sync error: ", response);
|
|
||||||
|
|
||||||
writeAllDirtyItemsToDisk();
|
|
||||||
this.syncOpInProgress = false;
|
|
||||||
|
|
||||||
if(callback) {
|
|
||||||
callback({error: "Sync error"});
|
|
||||||
}
|
|
||||||
}.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sync = function(callback) {
|
|
||||||
this.syncWithOptions(callback, undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handleUnsavedItemsResponse = function(unsaved) {
|
|
||||||
if(unsaved.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Handle unsaved", unsaved);
|
|
||||||
for(var mapping of unsaved) {
|
|
||||||
var itemResponse = mapping.item;
|
|
||||||
var item = modelManager.findItem(itemResponse.uuid);
|
|
||||||
var error = mapping.error;
|
|
||||||
if(error.tag == "uuid_conflict") {
|
|
||||||
item.alternateUUID();
|
|
||||||
item.setDirty(true);
|
|
||||||
item.markAllReferencesDirty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncWithOptions(null, {additionalFields: ["created_at", "updated_at"]});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handleItemsResponse = function(responseItems, omitFields) {
|
|
||||||
this.decryptItems(responseItems);
|
|
||||||
return modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.createRequestParamsForItem = function(item, additionalFields) {
|
|
||||||
return this.paramsForItem(item, true, additionalFields, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.paramsForExportFile = function(item, encrypted) {
|
|
||||||
return _.omit(this.paramsForItem(item, encrypted, ["created_at", "updated_at"], true), ["deleted"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.paramsForExtension = function(item, encrypted) {
|
|
||||||
return _.omit(this.paramsForItem(item, encrypted, ["created_at", "updated_at"], true), ["deleted"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.paramsForItem = function(item, encrypted, additionalFields, forExportFile) {
|
|
||||||
var itemCopy = _.cloneDeep(item);
|
|
||||||
|
|
||||||
console.assert(!item.dummy, "Item is dummy, should not have gotten here.", item.dummy)
|
|
||||||
|
|
||||||
var params = {uuid: item.uuid, content_type: item.content_type, deleted: item.deleted};
|
|
||||||
|
|
||||||
if(encrypted) {
|
|
||||||
this.encryptSingleItem(itemCopy, this.retrieveMk());
|
|
||||||
params.content = itemCopy.content;
|
|
||||||
params.enc_item_key = itemCopy.enc_item_key;
|
|
||||||
params.auth_hash = itemCopy.auth_hash;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
params.content = forExportFile ? itemCopy.createContentJSONFromProperties() : "000" + Neeto.crypto.base64(JSON.stringify(itemCopy.createContentJSONFromProperties()));
|
|
||||||
if(!forExportFile) {
|
|
||||||
params.enc_item_key = null;
|
|
||||||
params.auth_hash = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(additionalFields) {
|
|
||||||
_.merge(params, _.pick(item, additionalFields));
|
|
||||||
}
|
|
||||||
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Import
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.clearSyncToken = function() {
|
|
||||||
this.syncToken = null;
|
|
||||||
localStorage.removeItem("syncToken");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.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.markAllReferencesDirty();
|
|
||||||
})
|
|
||||||
this.syncWithOptions(callback, {additionalFields: ["created_at", "updated_at"]});
|
|
||||||
}.bind(this)
|
|
||||||
|
|
||||||
if(data.auth_params) {
|
|
||||||
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, data.auth_params), function(keys){
|
|
||||||
var mk = keys.mk;
|
|
||||||
try {
|
|
||||||
this.decryptItemsWithKey(data.items, mk);
|
|
||||||
// delete items enc_item_key since the user's actually key will do the encrypting once its passed off
|
|
||||||
data.items.forEach(function(item){
|
|
||||||
item.enc_item_key = null;
|
|
||||||
item.auth_hash = null;
|
|
||||||
})
|
|
||||||
onDataReady();
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.log("Error decrypting", e);
|
|
||||||
alert("There was an error decrypting your items. Make sure the password you entered is correct and try again.");
|
|
||||||
callback(false, null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}.bind(this));
|
|
||||||
} else {
|
|
||||||
onDataReady();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Export
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.itemsDataFile = function(encrypted) {
|
|
||||||
var textFile = null;
|
|
||||||
var makeTextFile = function (text) {
|
|
||||||
var data = new Blob([text], {type: 'text/json'});
|
|
||||||
|
|
||||||
// If we are replacing a previously generated file we need to
|
|
||||||
// manually revoke the object URL to avoid memory leaks.
|
|
||||||
if (textFile !== null) {
|
|
||||||
window.URL.revokeObjectURL(textFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
textFile = window.URL.createObjectURL(data);
|
|
||||||
|
|
||||||
// returns a URL you can use as a href
|
|
||||||
return textFile;
|
|
||||||
}.bind(this);
|
|
||||||
|
|
||||||
var items = _.map(modelManager.allItemsMatchingTypes(["Tag", "Note"]), function(item){
|
|
||||||
return this.paramsForExportFile(item, encrypted);
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
var data = {
|
|
||||||
items: items
|
|
||||||
}
|
|
||||||
|
|
||||||
if(encrypted) {
|
|
||||||
data["auth_params"] = this.getAuthParams();
|
|
||||||
}
|
|
||||||
|
|
||||||
return makeTextFile(JSON.stringify(data, null, 2 /* pretty print */));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.staticifyObject = function(object) {
|
|
||||||
return JSON.parse(JSON.stringify(object));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.writeItemsToLocalStorage = function(items, callback) {
|
|
||||||
var params = items.map(function(item) {
|
|
||||||
return this.paramsForItem(item, false, ["created_at", "updated_at", "dirty"], true)
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
dbManager.saveItems(params, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadLocalItems = function(callback) {
|
|
||||||
var params = dbManager.getAllItems(function(items){
|
|
||||||
var items = this.handleItemsResponse(items, null);
|
|
||||||
Item.sortItemsByDate(items);
|
|
||||||
callback(items);
|
|
||||||
}.bind(this))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Drafts
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.saveDraftToDisk = function(draft) {
|
|
||||||
localStorage.setItem("draft", JSON.stringify(draft));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clearDraft = function() {
|
|
||||||
localStorage.removeItem("draft");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getDraft = function() {
|
|
||||||
var draftString = localStorage.getItem("draft");
|
|
||||||
if(!draftString || draftString == 'undefined') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var jsonObj = _.merge({content_type: "Note"}, JSON.parse(draftString));
|
|
||||||
return modelManager.createItem(jsonObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
Encrpytion
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.retrieveMk = function() {
|
|
||||||
if(!this.mk) {
|
|
||||||
this.mk = localStorage.getItem("mk");
|
|
||||||
}
|
|
||||||
return this.mk;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setMk = function(mk) {
|
|
||||||
localStorage.setItem('mk', mk);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.signout = function(callback) {
|
|
||||||
dbManager.clearAllItems(function(){
|
|
||||||
localStorage.clear();
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.encryptSingleItem = function(item, masterKey) {
|
|
||||||
var item_key = null;
|
|
||||||
if(item.enc_item_key) {
|
|
||||||
item_key = Neeto.crypto.decryptText(item.enc_item_key, masterKey);
|
|
||||||
} else {
|
|
||||||
item_key = Neeto.crypto.generateRandomEncryptionKey();
|
|
||||||
item.enc_item_key = Neeto.crypto.encryptText(item_key, masterKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
var ek = Neeto.crypto.firstHalfOfKey(item_key);
|
|
||||||
var ak = Neeto.crypto.secondHalfOfKey(item_key);
|
|
||||||
var encryptedContent = "001" + Neeto.crypto.encryptText(JSON.stringify(item.createContentJSONFromProperties()), ek);
|
|
||||||
var authHash = Neeto.crypto.hmac256(encryptedContent, ak);
|
|
||||||
|
|
||||||
item.content = encryptedContent;
|
|
||||||
item.auth_hash = authHash;
|
|
||||||
item.local_encryption_scheme = "1.0";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.decryptSingleItem = function(item, masterKey) {
|
|
||||||
var item_key = Neeto.crypto.decryptText(item.enc_item_key, masterKey);
|
|
||||||
|
|
||||||
var ek = Neeto.crypto.firstHalfOfKey(item_key);
|
|
||||||
var ak = Neeto.crypto.secondHalfOfKey(item_key);
|
|
||||||
var authHash = Neeto.crypto.hmac256(item.content, ak);
|
|
||||||
if(authHash !== item.auth_hash || !item.auth_hash) {
|
|
||||||
console.log("Authentication hash does not match.")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var content = Neeto.crypto.decryptText(item.content.substring(3, item.content.length), ek);
|
|
||||||
item.content = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.decryptItems = function(items) {
|
|
||||||
var masterKey = this.retrieveMk();
|
|
||||||
this.decryptItemsWithKey(items, masterKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.decryptItemsWithKey = function(items, key) {
|
|
||||||
for (var item of items) {
|
|
||||||
if(item.deleted == true) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
var isString = typeof item.content === 'string' || item.content instanceof String;
|
|
||||||
if(isString) {
|
|
||||||
try {
|
|
||||||
if(item.content.substring(0, 3) == "001" && item.enc_item_key) {
|
|
||||||
// is encrypted
|
|
||||||
this.decryptSingleItem(item, key);
|
|
||||||
} else {
|
|
||||||
// is base64 encoded
|
|
||||||
item.content = Neeto.crypto.base64Decode(item.content.substring(3, item.content.length))
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Error decrypting item", item, e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reencryptAllItemsAndSave = function(user, newMasterKey, oldMasterKey, callback) {
|
|
||||||
var items = modelManager.allItems();
|
|
||||||
items.forEach(function(item){
|
|
||||||
if(item.content.substring(0, 3) == "001" && item.enc_item_key) {
|
|
||||||
// first decrypt item_key with old key
|
|
||||||
var item_key = Neeto.crypto.decryptText(item.enc_item_key, oldMasterKey);
|
|
||||||
// now encrypt item_key with new key
|
|
||||||
item.enc_item_key = Neeto.crypto.encryptText(item_key, newMasterKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.saveBatchItems(user, items, function(success) {
|
|
||||||
callback(success);
|
|
||||||
}.bind(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
178
app/assets/javascripts/app/services/authManager.js
Normal file
178
app/assets/javascripts/app/services/authManager.js
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
angular.module('app.frontend')
|
||||||
|
.provider('authManager', function () {
|
||||||
|
|
||||||
|
function domainName() {
|
||||||
|
var domain_comps = location.hostname.split(".");
|
||||||
|
var domain = domain_comps[domain_comps.length - 2] + "." + domain_comps[domain_comps.length - 1];
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$get = function($rootScope, Restangular, modelManager) {
|
||||||
|
return new AuthManager($rootScope, Restangular, modelManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthManager($rootScope, Restangular, modelManager) {
|
||||||
|
|
||||||
|
var userData = localStorage.getItem("user");
|
||||||
|
if(userData) {
|
||||||
|
this.user = JSON.parse(userData);
|
||||||
|
} else {
|
||||||
|
// legacy, check for uuid
|
||||||
|
var idData = localStorage.getItem("uuid");
|
||||||
|
if(idData) {
|
||||||
|
this.user = {uuid: idData};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.offline = function() {
|
||||||
|
return !this.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getAuthParams = function() {
|
||||||
|
return JSON.parse(localStorage.getItem("auth_params"));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getAuthParamsForEmail = function(url, email, callback) {
|
||||||
|
var requestUrl = url + "/auth/params";
|
||||||
|
var request = Restangular.oneUrl(requestUrl, requestUrl);
|
||||||
|
request.get({email: email}).then(function(response){
|
||||||
|
callback(response.plain());
|
||||||
|
})
|
||||||
|
.catch(function(response){
|
||||||
|
console.log("Error getting auth params", response);
|
||||||
|
callback(null);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.supportsPasswordDerivationCost = function(cost) {
|
||||||
|
// some passwords are created on platforms with stronger pbkdf2 capabilities, like iOS,
|
||||||
|
// which accidentally used 60,000 iterations (now adjusted), which CryptoJS can't handle here (WebCrypto can however).
|
||||||
|
// if user has high password cost and is using browser that doesn't support WebCrypto,
|
||||||
|
// we want to tell them that they can't login with this browser.
|
||||||
|
if(cost > 5000) {
|
||||||
|
return Neeto.crypto instanceof SNCryptoWeb ? true : false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.login = function(url, email, password, callback) {
|
||||||
|
this.getAuthParamsForEmail(url, email, function(authParams){
|
||||||
|
if(!authParams) {
|
||||||
|
callback({error : {message: "Unable to get authentication parameters."}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.supportsPasswordDerivationCost(authParams.pw_cost)) {
|
||||||
|
var string = "Your account was created on a platform with higher security capabilities than this browser supports. " +
|
||||||
|
"If we attempted to generate your login keys here, it would take hours. " +
|
||||||
|
"Please use a browser with more up to date security capabilities, like Google Chrome or Firefox, to login."
|
||||||
|
alert(string)
|
||||||
|
callback({didDisplayAlert: true});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("compute encryption keys", password, authParams);
|
||||||
|
|
||||||
|
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);
|
||||||
|
callback(response);
|
||||||
|
}.bind(this))
|
||||||
|
.catch(function(response){
|
||||||
|
console.log("Error logging in", response);
|
||||||
|
callback(response.data);
|
||||||
|
})
|
||||||
|
}.bind(this));
|
||||||
|
}.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleAuthResponse = function(response, email, url, authParams, mk, pw) {
|
||||||
|
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);
|
||||||
|
localStorage.setItem("pw", pw);
|
||||||
|
localStorage.setItem("jwt", response.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
callback(response);
|
||||||
|
}.bind(this))
|
||||||
|
.catch(function(response){
|
||||||
|
console.log("Registration error", response);
|
||||||
|
callback(null);
|
||||||
|
})
|
||||||
|
}.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._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);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.staticifyObject = function(object) {
|
||||||
|
return JSON.parse(JSON.stringify(object));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -98,11 +98,14 @@ class DBManager {
|
|||||||
}, null)
|
}, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteItem(item) {
|
deleteItem(item, callback) {
|
||||||
this.openDatabase((db) => {
|
this.openDatabase((db) => {
|
||||||
var request = db.transaction("items", "readwrite").objectStore("items").delete(item.uuid);
|
var request = db.transaction("items", "readwrite").objectStore("items").delete(item.uuid);
|
||||||
request.onsuccess = function(event) {
|
request.onsuccess = function(event) {
|
||||||
console.log("Successfully deleted item", item.uuid);
|
console.log("Successfully deleted item", item.uuid);
|
||||||
|
if(callback) {
|
||||||
|
callback(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, null)
|
}, null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
angular
|
||||||
|
.module('app.frontend')
|
||||||
|
.directive('delayHide', function($timeout) {
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
scope: {
|
||||||
|
show: '=',
|
||||||
|
delay: '@'
|
||||||
|
},
|
||||||
|
link: function(scope, elem, attrs) {
|
||||||
|
var showTimer;
|
||||||
|
|
||||||
|
showElement(false);
|
||||||
|
|
||||||
|
//This is where all the magic happens!
|
||||||
|
// Whenever the scope variable updates we simply
|
||||||
|
// show if it evaluates to 'true' and hide if 'false'
|
||||||
|
scope.$watch('show', function(newVal){
|
||||||
|
newVal ? showSpinner() : hideSpinner();
|
||||||
|
});
|
||||||
|
|
||||||
|
function showSpinner() {
|
||||||
|
showElement(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideSpinner() {
|
||||||
|
$timeout(showElement.bind(this, false), getDelay());
|
||||||
|
}
|
||||||
|
|
||||||
|
function showElement(show) {
|
||||||
|
show ? elem.css({display:''}) : elem.css({display:'none'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDelay() {
|
||||||
|
var delay = parseInt(scope.delay);
|
||||||
|
|
||||||
|
return angular.isNumber(delay) ? delay : 200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
angular.module('app.frontend').directive('infiniteScroll', [
|
||||||
|
'$rootScope', '$window', '$timeout', function($rootScope, $window, $timeout) {
|
||||||
|
return {
|
||||||
|
link: function(scope, elem, attrs) {
|
||||||
|
elem.css('overflow-x', 'hidden');
|
||||||
|
elem.css('height', 'inherit');
|
||||||
|
|
||||||
|
var offset = parseInt(attrs.threshold) || 0;
|
||||||
|
var e = elem[0]
|
||||||
|
|
||||||
|
elem.on('scroll', function(){
|
||||||
|
if(scope.$eval(attrs.canLoad) && e.scrollTop + e.offsetHeight >= e.scrollHeight - offset) {
|
||||||
|
scope.$apply(attrs.infiniteScroll);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]);
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
class AccountMenu {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.restrict = "E";
|
||||||
|
this.templateUrl = "frontend/directives/account-menu.html";
|
||||||
|
this.scope = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
controller($scope, authManager, modelManager, syncManager, $timeout) {
|
||||||
|
'ngInject';
|
||||||
|
|
||||||
|
$scope.formData = {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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.serverPassword = function() {
|
||||||
|
return syncManager.serverPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
authManager.changePassword($scope.passwordChangeData.current_password, $scope.passwordChangeData.new_password, function(response){
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$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) {
|
||||||
|
$scope.formData.status = null;
|
||||||
|
var error = response ? response.error : {message: "An unknown error occured."}
|
||||||
|
if(!response || (response && !response.didDisplayAlert)) {
|
||||||
|
alert(error.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$scope.onAuthSuccess();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.submitRegistrationForm = function() {
|
||||||
|
$scope.formData.status = "Generating Account Keys...";
|
||||||
|
|
||||||
|
$timeout(function(){
|
||||||
|
authManager.register($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){
|
||||||
|
if(!response || response.error) {
|
||||||
|
$scope.formData.status = null;
|
||||||
|
var error = response ? response.error : {message: "An unknown error occured."}
|
||||||
|
alert(error.message);
|
||||||
|
} else {
|
||||||
|
$scope.onAuthSuccess();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.onAuthSuccess = function() {
|
||||||
|
syncManager.markAllItemsDirtyAndSaveOffline(function(){
|
||||||
|
window.location.reload();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.destroyLocalData = function() {
|
||||||
|
if(!confirm("Are you sure you want to end your session? This will delete all local items and extensions.")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncManager.destroyLocalData(function(){
|
||||||
|
window.location.reload();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Import/Export */
|
||||||
|
|
||||||
|
$scope.archiveFormData = {encrypted: $scope.user ? true : false};
|
||||||
|
$scope.user = authManager.user;
|
||||||
|
|
||||||
|
$scope.downloadDataArchive = function() {
|
||||||
|
var link = document.createElement('a');
|
||||||
|
link.setAttribute('download', 'notes.json');
|
||||||
|
|
||||||
|
var ek = $scope.archiveFormData.encrypted ? syncManager.masterKey : null;
|
||||||
|
|
||||||
|
link.href = $scope.itemsDataFile(ek);
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.submitImportPassword = function() {
|
||||||
|
$scope.performImport($scope.importData.data, $scope.importData.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.performImport = function(data, password) {
|
||||||
|
$scope.importData.loading = true;
|
||||||
|
// allow loading indicator to come up with timeout
|
||||||
|
$timeout(function(){
|
||||||
|
$scope.importJSONData(data, password, function(response){
|
||||||
|
$timeout(function(){
|
||||||
|
$scope.importData.loading = false;
|
||||||
|
$scope.importData = null;
|
||||||
|
if(!response) {
|
||||||
|
alert("There was an error importing your data. Please try again.");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.importFileSelected = function(files) {
|
||||||
|
$scope.importData = {};
|
||||||
|
|
||||||
|
var file = files[0];
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
var data = JSON.parse(e.target.result);
|
||||||
|
$timeout(function(){
|
||||||
|
if(data.auth_params) {
|
||||||
|
// request password
|
||||||
|
$scope.importData.requestPassword = true;
|
||||||
|
$scope.importData.data = data;
|
||||||
|
} else {
|
||||||
|
$scope.performImport(data, null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.encryptionStatusForNotes = function() {
|
||||||
|
var allNotes = modelManager.filteredNotes;
|
||||||
|
return allNotes.length + "/" + allNotes.length + " notes encrypted";
|
||||||
|
}
|
||||||
|
|
||||||
|
$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.markAllReferencesDirty();
|
||||||
|
})
|
||||||
|
|
||||||
|
syncManager.sync(callback, {additionalFields: ["created_at", "updated_at"]});
|
||||||
|
}.bind(this)
|
||||||
|
|
||||||
|
if(data.auth_params) {
|
||||||
|
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, data.auth_params), function(keys){
|
||||||
|
var mk = keys.mk;
|
||||||
|
try {
|
||||||
|
EncryptionHelper.decryptMultipleItems(data.items, mk, true);
|
||||||
|
// delete items enc_item_key since the user's actually key will do the encrypting once its passed off
|
||||||
|
data.items.forEach(function(item){
|
||||||
|
item.enc_item_key = null;
|
||||||
|
item.auth_hash = null;
|
||||||
|
})
|
||||||
|
onDataReady();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log("Error decrypting", e);
|
||||||
|
alert("There was an error decrypting your items. Make sure the password you entered is correct and try again.");
|
||||||
|
callback(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
} else {
|
||||||
|
onDataReady();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Export
|
||||||
|
*/
|
||||||
|
|
||||||
|
$scope.itemsDataFile = function(ek) {
|
||||||
|
var textFile = null;
|
||||||
|
var makeTextFile = function (text) {
|
||||||
|
var data = new Blob([text], {type: 'text/json'});
|
||||||
|
|
||||||
|
// If we are replacing a previously generated file we need to
|
||||||
|
// manually revoke the object URL to avoid memory leaks.
|
||||||
|
if (textFile !== null) {
|
||||||
|
window.URL.revokeObjectURL(textFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
textFile = window.URL.createObjectURL(data);
|
||||||
|
|
||||||
|
// returns a URL you can use as a href
|
||||||
|
return textFile;
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
var items = _.map(modelManager.allItemsMatchingTypes(["Tag", "Note"]), function(item){
|
||||||
|
var itemParams = new ItemParams(item, ek);
|
||||||
|
return itemParams.paramsForExportFile();
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
items: items
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ek) {
|
||||||
|
// auth params are only needed when encrypted with a standard file key
|
||||||
|
data["auth_params"] = authManager.getAuthParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeTextFile(JSON.stringify(data, null, 2 /* pretty print */));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('app.frontend').directive('accountMenu', () => new AccountMenu);
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
class GlobalExtensionsMenu {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.restrict = "E";
|
||||||
|
this.templateUrl = "frontend/directives/global-extensions-menu.html";
|
||||||
|
this.scope = {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
controller($scope, extensionManager, syncManager) {
|
||||||
|
'ngInject';
|
||||||
|
|
||||||
|
$scope.extensionManager = extensionManager;
|
||||||
|
|
||||||
|
$scope.toggleExtensionForm = function() {
|
||||||
|
$scope.newExtensionData = {};
|
||||||
|
$scope.showNewExtensionForm = !$scope.showNewExtensionForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.submitNewExtensionForm = function() {
|
||||||
|
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.");
|
||||||
|
} else {
|
||||||
|
$scope.newExtensionData.url = "";
|
||||||
|
$scope.showNewExtensionForm = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.selectedAction = function(action, extension) {
|
||||||
|
extensionManager.executeAction(action, extension, null, function(response){
|
||||||
|
if(response && response.error) {
|
||||||
|
action.error = true;
|
||||||
|
alert("There was an error performing this action. Please try again.");
|
||||||
|
} else {
|
||||||
|
action.error = false;
|
||||||
|
syncManager.sync(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.changeExtensionEncryptionFormat = function(encrypted, extension) {
|
||||||
|
extensionManager.changeExtensionEncryptionFormat(encrypted, extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.deleteExtension = function(extension) {
|
||||||
|
if(confirm("Are you sure you want to delete this extension?")) {
|
||||||
|
extensionManager.deleteExtension(extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.reloadExtensionsPressed = function() {
|
||||||
|
if(confirm("For your security, reloading extensions will disable any currently enabled repeat actions.")) {
|
||||||
|
extensionManager.refreshExtensionsFromServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('app.frontend').directive('globalExtensionsMenu', () => new GlobalExtensionsMenu);
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
class ExtensionManager {
|
class ExtensionManager {
|
||||||
|
|
||||||
constructor(Restangular, modelManager, apiController) {
|
constructor(Restangular, modelManager, authManager, syncManager) {
|
||||||
this.Restangular = Restangular;
|
this.Restangular = Restangular;
|
||||||
this.modelManager = modelManager;
|
this.modelManager = modelManager;
|
||||||
this.apiController = apiController;
|
this.authManager = authManager;
|
||||||
this.enabledRepeatActionUrls = JSON.parse(localStorage.getItem("enabledRepeatActionUrls")) || [];
|
this.enabledRepeatActionUrls = JSON.parse(localStorage.getItem("enabledRepeatActionUrls")) || [];
|
||||||
this.decryptedExtensions = JSON.parse(localStorage.getItem("decryptedExtensions")) || [];
|
this.decryptedExtensions = JSON.parse(localStorage.getItem("decryptedExtensions")) || [];
|
||||||
|
this.syncManager = syncManager;
|
||||||
|
|
||||||
modelManager.addItemSyncObserver("extensionManager", "Extension", function(items){
|
modelManager.addItemSyncObserver("extensionManager", "Extension", function(items){
|
||||||
for (var ext of items) {
|
for (var ext of items) {
|
||||||
@@ -42,6 +43,7 @@ class ExtensionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
changeExtensionEncryptionFormat(encrypted, extension) {
|
changeExtensionEncryptionFormat(encrypted, extension) {
|
||||||
|
console.log("changing encryption status");
|
||||||
if(encrypted) {
|
if(encrypted) {
|
||||||
_.pull(this.decryptedExtensions, extension.url);
|
_.pull(this.decryptedExtensions, extension.url);
|
||||||
} else {
|
} else {
|
||||||
@@ -68,7 +70,7 @@ class ExtensionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.modelManager.setItemToBeDeleted(extension);
|
this.modelManager.setItemToBeDeleted(extension);
|
||||||
this.apiController.sync(null);
|
this.syncManager.sync(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -111,7 +113,7 @@ class ExtensionManager {
|
|||||||
extension.url = url;
|
extension.url = url;
|
||||||
extension.setDirty(true);
|
extension.setDirty(true);
|
||||||
this.modelManager.addItem(extension);
|
this.modelManager.addItem(extension);
|
||||||
this.apiController.sync(null);
|
this.syncManager.sync(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return extension;
|
return extension;
|
||||||
@@ -134,22 +136,30 @@ class ExtensionManager {
|
|||||||
|
|
||||||
executeAction(action, extension, item, callback) {
|
executeAction(action, extension, item, callback) {
|
||||||
|
|
||||||
if(this.extensionUsesEncryptedData(extension) && !this.apiController.isUserSignedIn()) {
|
if(this.extensionUsesEncryptedData(extension) && this.authManager.offline()) {
|
||||||
alert("To send data encrypted, you must have an encryption key, and must therefore be signed in.");
|
alert("To send data encrypted, you must have an encryption key, and must therefore be signed in.");
|
||||||
callback(null);
|
callback(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var customCallback = function(response) {
|
||||||
|
action.running = false;
|
||||||
|
callback(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
action.running = true;
|
||||||
|
|
||||||
switch (action.verb) {
|
switch (action.verb) {
|
||||||
case "get": {
|
case "get": {
|
||||||
this.Restangular.oneUrl(action.url, action.url).get().then(function(response){
|
this.Restangular.oneUrl(action.url, action.url).get().then(function(response){
|
||||||
action.error = false;
|
action.error = false;
|
||||||
var items = response.items;
|
var items = response.items;
|
||||||
this.modelManager.mapResponseItemsToLocalModels(items);
|
this.modelManager.mapResponseItemsToLocalModels(items);
|
||||||
callback(items);
|
customCallback(items);
|
||||||
}.bind(this))
|
}.bind(this))
|
||||||
.catch(function(response){
|
.catch(function(response){
|
||||||
action.error = true;
|
action.error = true;
|
||||||
|
customCallback(null);
|
||||||
})
|
})
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -158,7 +168,7 @@ class ExtensionManager {
|
|||||||
case "show": {
|
case "show": {
|
||||||
var win = window.open(action.url, '_blank');
|
var win = window.open(action.url, '_blank');
|
||||||
win.focus();
|
win.focus();
|
||||||
callback();
|
customCallback();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +187,7 @@ class ExtensionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.performPost(action, extension, params, function(response){
|
this.performPost(action, extension, params, function(response){
|
||||||
callback(response);
|
customCallback(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -261,20 +271,25 @@ class ExtensionManager {
|
|||||||
var params = this.outgoingParamsForItem(item, extension);
|
var params = this.outgoingParamsForItem(item, extension);
|
||||||
return params;
|
return params;
|
||||||
}.bind(this))
|
}.bind(this))
|
||||||
this.performPost(action, extension, params, null);
|
|
||||||
|
action.running = true;
|
||||||
|
this.performPost(action, extension, params, function(){
|
||||||
|
action.running = false;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// todo
|
// todo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outgoingParamsForItem(item, extension) {
|
outgoingParamsForItem(item, extension) {
|
||||||
return this.apiController.paramsForExtension(item, this.extensionUsesEncryptedData(extension));
|
var itemParams = new ItemParams(item, this.syncManager.masterKey);
|
||||||
|
return itemParams.paramsForExtension();
|
||||||
}
|
}
|
||||||
|
|
||||||
performPost(action, extension, params, callback) {
|
performPost(action, extension, params, callback) {
|
||||||
var request = this.Restangular.oneUrl(action.url, action.url);
|
var request = this.Restangular.oneUrl(action.url, action.url);
|
||||||
if(this.extensionUsesEncryptedData(extension)) {
|
if(this.extensionUsesEncryptedData(extension)) {
|
||||||
request.auth_params = this.apiController.getAuthParams();
|
request.auth_params = this.authManager.getAuthParams();
|
||||||
}
|
}
|
||||||
_.merge(request, params);
|
_.merge(request, params);
|
||||||
|
|
||||||
|
|||||||
6
app/assets/javascripts/app/services/filters/startFrom.js
Normal file
6
app/assets/javascripts/app/services/filters/startFrom.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// Start from filter
|
||||||
|
angular.module('app.frontend').filter('startFrom', function() {
|
||||||
|
return function(input, start) {
|
||||||
|
return input.slice(start);
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
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 ek = Neeto.crypto.firstHalfOfKey(item_key);
|
||||||
|
var ak = Neeto.crypto.secondHalfOfKey(item_key);
|
||||||
|
var encryptedContent = "001" + Neeto.crypto.encryptText(JSON.stringify(item.createContentJSONFromProperties()), ek);
|
||||||
|
var authHash = Neeto.crypto.hmac256(encryptedContent, ak);
|
||||||
|
|
||||||
|
item.content = encryptedContent;
|
||||||
|
item.auth_hash = authHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
static decryptItem(item, key) {
|
||||||
|
var item_key = Neeto.crypto.decryptText(item.enc_item_key, key);
|
||||||
|
|
||||||
|
var ek = Neeto.crypto.firstHalfOfKey(item_key);
|
||||||
|
var ak = Neeto.crypto.secondHalfOfKey(item_key);
|
||||||
|
var authHash = Neeto.crypto.hmac256(item.content, ak);
|
||||||
|
if(authHash !== item.auth_hash || !item.auth_hash) {
|
||||||
|
console.log("Authentication hash does not match.")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = Neeto.crypto.decryptText(item.content.substring(3, item.content.length), ek);
|
||||||
|
item.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
static decryptMultipleItems(items, key, throws) {
|
||||||
|
for (var item of items) {
|
||||||
|
if(item.deleted == true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isString = typeof item.content === 'string' || item.content instanceof String;
|
||||||
|
if(isString) {
|
||||||
|
try {
|
||||||
|
if(item.content.substring(0, 3) == "001" && item.enc_item_key) {
|
||||||
|
// is encrypted
|
||||||
|
this.decryptItem(item, key);
|
||||||
|
} else {
|
||||||
|
// is base64 encoded
|
||||||
|
item.content = Neeto.crypto.base64Decode(item.content.substring(3, item.content.length))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if(throws) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
console.log("Error decrypting item", item, e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -22,6 +22,18 @@ class ModelManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
alternateUUIDForItem(item, callback) {
|
||||||
|
// we need to clone this item and give it a new uuid, then delete item with old uuid from db (you can't mofidy uuid's in our indexeddb setup)
|
||||||
|
var newItem = this.createItem(item);
|
||||||
|
newItem.uuid = Neeto.crypto.generateUUID();
|
||||||
|
this.removeItemLocally(item, function(){
|
||||||
|
this.addItem(newItem);
|
||||||
|
newItem.setDirty(true);
|
||||||
|
newItem.markAllReferencesDirty();
|
||||||
|
callback();
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
allItemsMatchingTypes(contentTypes) {
|
allItemsMatchingTypes(contentTypes) {
|
||||||
return this.items.filter(function(item){
|
return this.items.filter(function(item){
|
||||||
return (_.includes(contentTypes, item.content_type) || _.includes(contentTypes, "*")) && !item.dummy;
|
return (_.includes(contentTypes, item.content_type) || _.includes(contentTypes, "*")) && !item.dummy;
|
||||||
@@ -76,7 +88,6 @@ class ModelManager {
|
|||||||
|
|
||||||
this.notifySyncObserversOfModels(models);
|
this.notifySyncObserversOfModels(models);
|
||||||
|
|
||||||
this.sortItems();
|
|
||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +144,9 @@ class ModelManager {
|
|||||||
}
|
}
|
||||||
} else if(item.content_type == "Note") {
|
} else if(item.content_type == "Note") {
|
||||||
if(!_.find(this.notes, {uuid: item.uuid})) {
|
if(!_.find(this.notes, {uuid: item.uuid})) {
|
||||||
this.notes.unshift(item);
|
this.notes.splice(_.sortedLastIndexBy(this.notes, item, function(item){
|
||||||
|
return -item.created_at;
|
||||||
|
}), 0, item);
|
||||||
}
|
}
|
||||||
} else if(item.content_type == "Extension") {
|
} else if(item.content_type == "Extension") {
|
||||||
if(!_.find(this._extensions, {uuid: item.uuid})) {
|
if(!_.find(this._extensions, {uuid: item.uuid})) {
|
||||||
@@ -170,14 +183,6 @@ class ModelManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sortItems() {
|
|
||||||
Item.sortItemsByDate(this.notes);
|
|
||||||
|
|
||||||
this.tags.forEach(function(tag){
|
|
||||||
Item.sortItemsByDate(tag.notes);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
addItemSyncObserver(id, type, callback) {
|
addItemSyncObserver(id, type, callback) {
|
||||||
this.itemSyncObservers.push({id: id, type: type, callback: callback});
|
this.itemSyncObservers.push({id: id, type: type, callback: callback});
|
||||||
}
|
}
|
||||||
@@ -220,18 +225,21 @@ class ModelManager {
|
|||||||
item.removeAllRelationships();
|
item.removeAllRelationships();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeItemLocally(item) {
|
removeItemLocally(item, callback) {
|
||||||
_.pull(this.items, item);
|
_.pull(this.items, item);
|
||||||
|
|
||||||
|
item.isBeingRemovedLocally();
|
||||||
|
|
||||||
if(item.content_type == "Tag") {
|
if(item.content_type == "Tag") {
|
||||||
_.pull(this.tags, item);
|
_.pull(this.tags, item);
|
||||||
} else if(item.content_type == "Note") {
|
} else if(item.content_type == "Note") {
|
||||||
_.pull(this.notes, item);
|
_.pull(this.notes, item);
|
||||||
|
|
||||||
} else if(item.content_type == "Extension") {
|
} else if(item.content_type == "Extension") {
|
||||||
_.pull(this._extensions, item);
|
_.pull(this._extensions, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dbManager.deleteItem(item);
|
this.dbManager.deleteItem(item, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
245
app/assets/javascripts/app/services/syncManager.js
Normal file
245
app/assets/javascripts/app/services/syncManager.js
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
class SyncManager {
|
||||||
|
|
||||||
|
constructor($rootScope, modelManager, authManager, dbManager, Restangular) {
|
||||||
|
this.$rootScope = $rootScope;
|
||||||
|
this.modelManager = modelManager;
|
||||||
|
this.authManager = authManager;
|
||||||
|
this.Restangular = Restangular;
|
||||||
|
this.dbManager = dbManager;
|
||||||
|
this.syncStatus = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
get serverURL() {
|
||||||
|
return localStorage.getItem("server") || "https://n3.standardnotes.org";
|
||||||
|
}
|
||||||
|
|
||||||
|
get masterKey() {
|
||||||
|
return localStorage.getItem("mk");
|
||||||
|
}
|
||||||
|
|
||||||
|
get serverPassword() {
|
||||||
|
return localStorage.getItem("pw");
|
||||||
|
}
|
||||||
|
|
||||||
|
writeItemsToLocalStorage(items, offlineOnly, callback) {
|
||||||
|
var params = items.map(function(item) {
|
||||||
|
var itemParams = new ItemParams(item, null);
|
||||||
|
itemParams = itemParams.paramsForLocalStorage();
|
||||||
|
if(offlineOnly) {
|
||||||
|
delete itemParams.dirty;
|
||||||
|
}
|
||||||
|
return itemParams;
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
this.dbManager.saveItems(params, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadLocalItems(callback) {
|
||||||
|
var params = this.dbManager.getAllItems(function(items){
|
||||||
|
var items = this.handleItemsResponse(items, null, null);
|
||||||
|
Item.sortItemsByDate(items);
|
||||||
|
callback(items);
|
||||||
|
}.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
syncOffline(items, callback) {
|
||||||
|
this.writeItemsToLocalStorage(items, true, function(responseItems){
|
||||||
|
// delete anything needing to be deleted
|
||||||
|
for(var item of items) {
|
||||||
|
if(item.deleted) {
|
||||||
|
this.modelManager.removeItemLocally(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(callback) {
|
||||||
|
callback({success: true});
|
||||||
|
}
|
||||||
|
}.bind(this))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
markAllItemsDirtyAndSaveOffline(callback) {
|
||||||
|
var items = this.modelManager.allItems;
|
||||||
|
for(var item of items) {
|
||||||
|
item.setDirty(true);
|
||||||
|
}
|
||||||
|
this.writeItemsToLocalStorage(items, false, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
get syncURL() {
|
||||||
|
return this.serverURL + "/items/sync";
|
||||||
|
}
|
||||||
|
|
||||||
|
set syncToken(token) {
|
||||||
|
this._syncToken = token;
|
||||||
|
localStorage.setItem("syncToken", token);
|
||||||
|
}
|
||||||
|
|
||||||
|
get syncToken() {
|
||||||
|
if(!this._syncToken) {
|
||||||
|
this._syncToken = localStorage.getItem("syncToken");
|
||||||
|
}
|
||||||
|
return this._syncToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
set cursorToken(token) {
|
||||||
|
this._cursorToken = token;
|
||||||
|
if(token) {
|
||||||
|
localStorage.setItem("cursorToken", token);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem("cursorToken");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get cursorToken() {
|
||||||
|
if(!this._cursorToken) {
|
||||||
|
this._cursorToken = localStorage.getItem("cursorToken");
|
||||||
|
}
|
||||||
|
return this._cursorToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
sync(callback, options = {}) {
|
||||||
|
|
||||||
|
if(this.syncStatus.syncOpInProgress) {
|
||||||
|
this.repeatOnCompletion = true;
|
||||||
|
console.log("Sync op in progress; returning.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var allDirtyItems = this.modelManager.getDirtyItems();
|
||||||
|
|
||||||
|
// we want to write all dirty items to disk only if the user is offline, or if the sync op fails
|
||||||
|
// if the sync op succeeds, these items will be written to disk by handling the "saved_items" response from the server
|
||||||
|
if(this.authManager.offline()) {
|
||||||
|
this.syncOffline(allDirtyItems, callback);
|
||||||
|
this.modelManager.clearDirtyItems(allDirtyItems);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isContinuationSync = this.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;
|
||||||
|
} else {
|
||||||
|
this.needsMoreSync = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isContinuationSync) {
|
||||||
|
this.syncStatus.total = allDirtyItems.length;
|
||||||
|
this.syncStatus.current = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = this.Restangular.oneUrl(this.syncURL, this.syncURL);
|
||||||
|
request.limit = 150;
|
||||||
|
request.items = _.map(subItems, function(item){
|
||||||
|
var itemParams = new ItemParams(item, localStorage.getItem("mk"));
|
||||||
|
itemParams.additionalFields = options.additionalFields;
|
||||||
|
return itemParams.paramsForSync();
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
request.sync_token = this.syncToken;
|
||||||
|
request.cursor_token = this.cursorToken;
|
||||||
|
|
||||||
|
request.post().then(function(response) {
|
||||||
|
this.modelManager.clearDirtyItems(subItems);
|
||||||
|
this.syncStatus.error = null;
|
||||||
|
|
||||||
|
this.$rootScope.$broadcast("sync:updated_token", this.syncToken);
|
||||||
|
|
||||||
|
var retrieved = this.handleItemsResponse(response.retrieved_items, null);
|
||||||
|
// merge only metadata for saved items
|
||||||
|
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);
|
||||||
|
|
||||||
|
this.syncStatus.syncOpInProgress = false;
|
||||||
|
this.syncStatus.current += subItems.length;
|
||||||
|
|
||||||
|
// set the sync token at the end, so that if any errors happen above, you can resync
|
||||||
|
this.syncToken = response.sync_token;
|
||||||
|
this.cursorToken = response.cursor_token;
|
||||||
|
|
||||||
|
if(this.cursorToken || this.repeatOnCompletion || this.needsMoreSync) {
|
||||||
|
setTimeout(function () {
|
||||||
|
this.sync(callback, options);
|
||||||
|
}.bind(this), 10); // wait 10ms to allow UI to update
|
||||||
|
} else {
|
||||||
|
if(callback) {
|
||||||
|
callback(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}.bind(this))
|
||||||
|
.catch(function(response){
|
||||||
|
console.log("Sync error: ", response);
|
||||||
|
var error = response.data ? response.data.error : {message: "Could not connect to server."};
|
||||||
|
|
||||||
|
this.syncStatus.syncOpInProgress = false;
|
||||||
|
this.syncStatus.error = error;
|
||||||
|
this.writeItemsToLocalStorage(allDirtyItems, false, null);
|
||||||
|
|
||||||
|
this.$rootScope.$broadcast("sync:error", error);
|
||||||
|
|
||||||
|
if(callback) {
|
||||||
|
callback({error: "Sync error"});
|
||||||
|
}
|
||||||
|
}.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUnsavedItemsResponse(unsaved) {
|
||||||
|
if(unsaved.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Handle unsaved", unsaved);
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
var handleNext = function() {
|
||||||
|
if (i < unsaved.length) {
|
||||||
|
var mapping = unsaved[i];
|
||||||
|
var itemResponse = mapping.item;
|
||||||
|
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
|
||||||
|
this.modelManager.alternateUUIDForItem(item, handleNext);
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
} else {
|
||||||
|
this.sync(null, {additionalFields: ["created_at", "updated_at"]});
|
||||||
|
}
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
handleNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleItemsResponse(responseItems, omitFields) {
|
||||||
|
EncryptionHelper.decryptMultipleItems(responseItems, localStorage.getItem("mk"));
|
||||||
|
return this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSyncToken() {
|
||||||
|
localStorage.removeItem("syncToken");
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyLocalData(callback) {
|
||||||
|
this.dbManager.clearAllItems(function(){
|
||||||
|
localStorage.clear();
|
||||||
|
if(callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('app.frontend').service('syncManager', SyncManager);
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.ext-header {
|
.ext-header {
|
||||||
background-color: #ededed;
|
background-color: #ededed;
|
||||||
border-bottom: 1px solid #d3d3d3;
|
border-bottom: 1px solid #d3d3d3;
|
||||||
@@ -64,9 +63,7 @@
|
|||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,155 @@
|
|||||||
.header {
|
.pull-left {
|
||||||
|
float: left !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pull-right {
|
||||||
|
float: right !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-1 {
|
||||||
|
margin-top: 1px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-2 {
|
||||||
|
margin-top: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-5 {
|
||||||
|
margin-top: 5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-10 {
|
||||||
|
margin-top: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-15 {
|
||||||
|
margin-top: 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-25 {
|
||||||
|
margin-top: 25px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-0 {
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-5 {
|
||||||
|
margin-bottom: 5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-10 {
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mr-5 {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faded {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-align {
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
margin-left: auto !important;
|
||||||
|
margin-right: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.one-line-overflow {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-v-space {
|
||||||
|
height: 6px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium-v-space {
|
||||||
|
height: 12px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-v-space {
|
||||||
|
height: 24px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-padding {
|
||||||
|
padding: 5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium-padding {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pb-4 {
|
||||||
|
padding-bottom: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pb-6 {
|
||||||
|
padding-bottom: 6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pb-10 {
|
||||||
|
padding-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-padding {
|
||||||
|
padding: 22px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red {
|
||||||
|
color: red !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue {
|
||||||
|
color: $blue-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold {
|
||||||
|
font-weight: bold !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.normal {
|
||||||
|
font-weight: normal !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
font-size: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fake-link {
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $blue-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bar {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 5px;
|
||||||
background-color: #d8d7d9;
|
background-color: #d8d7d9;
|
||||||
height: $header-height;
|
height: $header-height;
|
||||||
max-height: $header-height;
|
max-height: $header-height;
|
||||||
@@ -9,90 +158,125 @@
|
|||||||
color: $dark-gray;
|
color: $dark-gray;
|
||||||
border-bottom: 1px solid rgba(#979799, 0.4);
|
border-bottom: 1px solid rgba(#979799, 0.4);
|
||||||
|
|
||||||
|
.medium-text {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $dark-gray;
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $blue-color;
|
||||||
|
|
||||||
|
&.gray {
|
||||||
|
color: $dark-gray !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.block {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 2px 0px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 14px !important;
|
||||||
|
margin-top: 4px !important;
|
||||||
|
margin-bottom: 3px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-bottom: 4px !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
padding: 5px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
&.inline-h {
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-content {
|
.footer-bar-link {
|
||||||
margin-bottom: 0px;
|
|
||||||
padding-top: 0px;
|
|
||||||
border-radius: 0px;
|
|
||||||
|
|
||||||
left: 0px;
|
|
||||||
right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-status-text {
|
|
||||||
margin-top: 20px;
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
margin-left: 15px;
|
|
||||||
padding-top: 5px;
|
|
||||||
margin-top: 0px;
|
|
||||||
color: #515263;
|
|
||||||
z-index: 1000;
|
|
||||||
margin-bottom: 0px;
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #515263;
|
||||||
|
|
||||||
&.left {
|
z-index: 1000;
|
||||||
float: left;
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
> a {
|
||||||
|
color: #515263;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.right {
|
.footer-bar-link .panel {
|
||||||
float: right;
|
font-weight: normal;
|
||||||
margin-right: 10px;
|
font-size: 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.login-panel .login-input {
|
max-height: 85vh;
|
||||||
border-radius: 0px;
|
position: absolute;
|
||||||
}
|
right: 0px;
|
||||||
|
bottom: 20px;
|
||||||
|
min-width: 300px;
|
||||||
|
z-index: 1000;
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
.items {
|
box-shadow: 0px 0px 15px rgba(black, 0.2);
|
||||||
|
border: none;
|
||||||
|
cursor: default;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
.item {
|
button.light {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
font-size: 12px;
|
||||||
|
height: 30px;
|
||||||
|
padding-top: 3px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
background-color: white;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid rgba(gray, 0.15);
|
||||||
|
cursor: pointer;
|
||||||
|
color: $blue-color;
|
||||||
|
|
||||||
display: inline-block;
|
&:hover {
|
||||||
margin-right: 7px;
|
background-color: rgba(gray, 0.10);
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #515263;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
position: absolute;
|
|
||||||
right: 0px;
|
|
||||||
bottom: 20px;
|
|
||||||
min-width: 300px;
|
|
||||||
z-index: 1000;
|
|
||||||
margin-top: 15px;
|
|
||||||
box-shadow: 0px 0px 15px rgba(black, 0.2);
|
|
||||||
border: none;
|
|
||||||
cursor: default;
|
|
||||||
max-height: 85vh;
|
|
||||||
overflow: auto;
|
|
||||||
background-color: white;
|
|
||||||
font-weight: normal;
|
|
||||||
|
|
||||||
|
|
||||||
.storage-text {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: normal;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,162 +288,25 @@
|
|||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gray-bg {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
border: 1px solid #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.white-bg {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid rgba(gray, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.item.last-refreshed {
|
.item.last-refreshed {
|
||||||
font-weight: normal !important;
|
font-weight: normal !important;
|
||||||
cursor: default !important;
|
cursor: default !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item.account {
|
|
||||||
|
|
||||||
.email {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links {
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-item {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
a {
|
|
||||||
font-size: 12px;
|
|
||||||
color: $blue-color;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
border-radius: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-panel {
|
|
||||||
|
|
||||||
padding: 12px;
|
|
||||||
padding-bottom: 6px;
|
|
||||||
|
|
||||||
.account-items {
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-item {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 34px;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $blue-color;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .icon-container {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .meta-container {
|
|
||||||
display: block;
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .action-container {
|
|
||||||
font-size: 12px;
|
|
||||||
margin-top: 6px;
|
|
||||||
|
|
||||||
.status-title {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtext {
|
|
||||||
font-size: 10px;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: -10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.import-password {
|
|
||||||
margin-top: 14px;
|
|
||||||
|
|
||||||
> .field {
|
|
||||||
display: block;
|
|
||||||
margin: 5px 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.encryption-confirmation {
|
|
||||||
position: relative;
|
|
||||||
.buttons {
|
|
||||||
.cancel {
|
|
||||||
font-weight: normal;
|
|
||||||
margin-right: 3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .icon-container {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
.icon {
|
|
||||||
height: 35px;
|
|
||||||
&.archive {
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-container {
|
|
||||||
> .title {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .desc {
|
|
||||||
font-size: 12px;
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.membership-settings {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a.disabled {
|
a.disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-form {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.registration-login {
|
|
||||||
|
|
||||||
.login-forgot {
|
|
||||||
margin-top: 20px;
|
|
||||||
clear: both;
|
|
||||||
a {
|
|
||||||
display: block;
|
|
||||||
font-size: 13px !important;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
height: 10px;
|
height: 10px;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
@@ -267,138 +314,14 @@ a.disabled {
|
|||||||
border: 1px solid #515263;
|
border: 1px solid #515263;
|
||||||
border-right-color: transparent;
|
border-right-color: transparent;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&.blue {
|
||||||
|
border: 1px solid $blue-color;
|
||||||
|
border-right-color: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes rotate {
|
@keyframes rotate {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
Extensions
|
|
||||||
*/
|
|
||||||
|
|
||||||
.extensions-panel {
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
.extension-link {
|
|
||||||
margin-top: 8px;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $blue-color !important;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.extension-form {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.registered-extensions {
|
|
||||||
|
|
||||||
|
|
||||||
.extension {
|
|
||||||
margin-bottom: 18px;
|
|
||||||
background-color: #f6f6f6;
|
|
||||||
border: 1px solid #f2f2f2;
|
|
||||||
padding: 14px 6px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
color: black;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $blue-color !important;
|
|
||||||
font-size: 12px !important;
|
|
||||||
font-weight: bold !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .name {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.encryption-format {
|
|
||||||
margin-top: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
> .title {
|
|
||||||
font-size: 13px;
|
|
||||||
// font-weight: bold;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .subtitle {
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .actions {
|
|
||||||
margin-top: 15px;
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
.action {
|
|
||||||
padding: 13px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
background-color: rgba(white, 0.9);
|
|
||||||
border: 1px solid rgba(gray, 0.15);
|
|
||||||
|
|
||||||
> .name {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .permissions {
|
|
||||||
margin-top: 2px;
|
|
||||||
a {
|
|
||||||
font-weight: normal !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .execute {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 0px;
|
|
||||||
font-size: 12px;
|
|
||||||
height: 30px;
|
|
||||||
padding-top: 7px;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 6px;
|
|
||||||
border: 1px solid rgba(gray, 0.15);
|
|
||||||
cursor: pointer;
|
|
||||||
color: $blue-color;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(gray, 0.10);
|
|
||||||
}
|
|
||||||
|
|
||||||
.execution-spinner {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .execute-type {
|
|
||||||
font-size: 12px;
|
|
||||||
margin-bottom: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .error {
|
|
||||||
color: red;
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .last-run {
|
|
||||||
opacity: 0.5;
|
|
||||||
font-size: 11px;
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -41,12 +41,11 @@
|
|||||||
|
|
||||||
.note {
|
.note {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
// max-width: 100%;
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
// height: 70px;
|
|
||||||
border-bottom: 1px solid $bg-color;
|
border-bottom: 1px solid $bg-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|
||||||
> .name {
|
> .name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
.panel.panel-default.panel-right.account-data-menu
|
||||||
|
.panel-body.large-padding
|
||||||
|
%div{"ng-if" => "!user"}
|
||||||
|
%p Enter your <a href="https://standardnotes.org" target="_blank">Standard File</a> account information. You can also register for free using the default server address.
|
||||||
|
.small-v-space
|
||||||
|
|
||||||
|
%form.account-form.mt-5{'name' => "loginForm"}
|
||||||
|
%input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'formData.url'}
|
||||||
|
%input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'formData.email'}
|
||||||
|
%input.form-control{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'formData.user_password'}
|
||||||
|
|
||||||
|
%div{"ng-if" => "!formData.status"}
|
||||||
|
%button.btn.dark-button.half-button{"ng-click" => "loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
||||||
|
%span Sign In
|
||||||
|
%button.btn.dark-button.half-button{"ng-click" => "submitRegistrationForm()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
||||||
|
%span Register
|
||||||
|
%br
|
||||||
|
.block{"style" => "margin-top: 10px; font-size: 14px; font-weight: bold; text-align: center;"}
|
||||||
|
%a.btn{"ng-click" => "showResetForm = !showResetForm"} Passwords cannot be forgotten.
|
||||||
|
|
||||||
|
%em.block.center-align.mt-10{"ng-if" => "formData.status", "style" => "font-size: 14px;"} {{formData.status}}
|
||||||
|
|
||||||
|
%div{"ng-if" => "showResetForm"}
|
||||||
|
%p{"style" => "font-size: 13px; text-align: center;"}
|
||||||
|
Because notes are locally encrypted using a secret key derived from your password, there's no way to decrypt these notes if you forget your password.
|
||||||
|
For this reason, Standard Notes cannot offer a password reset option. You <strong>must</strong> make sure to store or remember your password.
|
||||||
|
|
||||||
|
%div{"ng-if" => "user"}
|
||||||
|
%h2 {{user.email}}
|
||||||
|
%p {{server}}
|
||||||
|
%a.block.mt-5{"ng-click" => "showCredentials = !showCredentials"} Show Credentials
|
||||||
|
%section.gray-bg.mt-10.medium-padding{"ng-if" => "showCredentials"}
|
||||||
|
%label.block
|
||||||
|
Encryption key:
|
||||||
|
.wrap.normal.mt-1 {{encryptionKey()}}
|
||||||
|
%label.block.mt-5.mb-0
|
||||||
|
Server password:
|
||||||
|
.wrap.normal.mt-1 {{serverPassword() ? serverPassword() : 'Not available. Sign out then sign back in to compute.'}}
|
||||||
|
|
||||||
|
%div.bold.mt-10.blue{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress", "delay" => "1000"}
|
||||||
|
.spinner.inline.mr-5.blue
|
||||||
|
Syncing
|
||||||
|
%span{"ng-if" => "syncStatus.total > 0"}: {{syncStatus.current}}/{{syncStatus.total}}
|
||||||
|
%p.bold.mt-10.red.block{"ng-if" => "syncStatus.error"} Error syncing: {{syncStatus.error.message}}
|
||||||
|
|
||||||
|
.medium-v-space
|
||||||
|
|
||||||
|
%h4 Local Encryption
|
||||||
|
%p Notes are encrypted locally before being sent to the server. Neither the server owner nor an intrusive entity can decrypt your locally encrypted notes.
|
||||||
|
%div.mt-5
|
||||||
|
%label Status:
|
||||||
|
{{encryptionStatusForNotes()}}
|
||||||
|
|
||||||
|
.mt-25{"ng-if" => "!importData.loading"}
|
||||||
|
%h4 Data Archives
|
||||||
|
.mt-5{"ng-if" => "user"}
|
||||||
|
%label.normal.inline{"ng-if" => "user"}
|
||||||
|
%input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "true", "ng-change" => "archiveFormData.encrypted = true"}
|
||||||
|
Encrypted
|
||||||
|
%label.normal.inline
|
||||||
|
%input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "false", "ng-change" => "archiveFormData.encrypted = false"}
|
||||||
|
Decrypted
|
||||||
|
|
||||||
|
%a.block{"ng-click" => "downloadDataArchive()"} Download Data Archive
|
||||||
|
|
||||||
|
%label.block.mt-5
|
||||||
|
%input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"}
|
||||||
|
.fake-link Import Data from Archive
|
||||||
|
|
||||||
|
%div{"ng-if" => "importData.requestPassword"}
|
||||||
|
Enter the account password associated with the import file.
|
||||||
|
%input{"type" => "text", "ng-model" => "importData.password"}
|
||||||
|
%button{"ng-click" => "submitImportPassword()"} Decrypt & Import
|
||||||
|
|
||||||
|
.spinner.mt-10{"ng-if" => "importData.loading"}
|
||||||
|
|
||||||
|
%a.block.mt-25.red{"ng-click" => "destroyLocalData()"} Destroy all local data
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
.panel.panel-default.account-panel.panel-right
|
||||||
|
.panel-body
|
||||||
|
%div{"style" => "font-size: 18px;", "ng-if" => "!extensionManager.extensions.length"} No extensions installed
|
||||||
|
%div{"ng-if" => "extensionManager.extensions.length"}
|
||||||
|
%section.gray-bg.inline-h.mb-10.medium-padding{"ng-repeat" => "extension in extensionManager.extensions", "ng-init" => "extension.formData = {}"}
|
||||||
|
%h3.center-align {{extension.name}}
|
||||||
|
.center-align.centered.mt-10
|
||||||
|
%label.block.normal Send data:
|
||||||
|
%label.normal
|
||||||
|
%input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "true", "ng-change" => "changeExtensionEncryptionFormat(true, extension)"}
|
||||||
|
Encrypted
|
||||||
|
%label.normal
|
||||||
|
%input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "false", "ng-change" => "changeExtensionEncryptionFormat(false, extension)"}
|
||||||
|
Decrypted
|
||||||
|
|
||||||
|
.small-v-space
|
||||||
|
|
||||||
|
%section.inline-h.white-bg.medium-padding.mb-10.pb-4{"ng-repeat" => "action in extension.actionsInGlobalContext()"}
|
||||||
|
%label.block {{action.label}}
|
||||||
|
%em{"style" => "font-style: italic;"} {{action.desc}}
|
||||||
|
%em{"ng-if" => "action.repeat_mode == 'watch'"}
|
||||||
|
Repeats when a change is made to your items.
|
||||||
|
%em{"ng-if" => "action.repeat_mode == 'loop'"}
|
||||||
|
Repeats at most once every {{action.repeat_timeout}} seconds
|
||||||
|
%div
|
||||||
|
%a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}}
|
||||||
|
%div{"ng-if" => "action.showPermissions"}
|
||||||
|
{{action.permissionsString()}}
|
||||||
|
%label.block.normal {{action.encryptionModeString()}}
|
||||||
|
|
||||||
|
%div
|
||||||
|
.mt-5{"ng-if" => "action.repeat_mode"}
|
||||||
|
%button.light{"ng-if" => "extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.disableRepeatAction(action, extension)"} Disable
|
||||||
|
%button.light{"ng-if" => "!extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.enableRepeatAction(action, extension)"} Enable
|
||||||
|
%button.light.mt-10{"ng-if" => "!action.running && !action.repeat_mode", "ng-click" => "selectedAction(action, extension)"}
|
||||||
|
Perform Action
|
||||||
|
.spinner.mb-5.centered.center-align.block{"ng-if" => "action.running"}
|
||||||
|
%p.mb-5.mt-5.small{"ng-if" => "!action.error && action.lastExecuted && !action.running"}
|
||||||
|
Last run {{action.lastExecuted | appDateTime}}
|
||||||
|
%label.red{"ng-if" => "action.error"}
|
||||||
|
Error performing action.
|
||||||
|
|
||||||
|
%a.block.center-align.mt-10{"ng-click" => "extension.showURL = !extension.showURL"} Show URL
|
||||||
|
%p.center-align.wrap{"ng-if" => "extension.showURL"} {{extension.url}}
|
||||||
|
%a.block.center-align.mt-5{"ng-click" => "deleteExtension(extension)"} Remove extension
|
||||||
|
|
||||||
|
.large-v-space
|
||||||
|
|
||||||
|
%a.block{"ng-click" => "toggleExtensionForm()"} Add new extension
|
||||||
|
|
||||||
|
%form.mt-10.mb-10{"ng-if" => "showNewExtensionForm"}
|
||||||
|
%input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Extension URL', :required => true, :type => 'url', 'ng-model' => 'newExtensionData.url'}
|
||||||
|
%button.btn.dark-button.btn-block{"ng-click" => "submitNewExtensionForm()", :type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
||||||
|
Add Extension
|
||||||
|
|
||||||
|
%a.block.mt-5{"ng-click" => "reloadExtensionsPressed()", "ng-if" => "extensionManager.extensions.length > 0"} Reload all extensions
|
||||||
|
%a.block.mt-5{"href" => "https://standardnotes.org/extensions", "target" => "_blank"} List of available extensions
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
%input.input#note-title-editor{"ng-model" => "ctrl.note.title", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveTitle($event)",
|
%input.input#note-title-editor{"ng-model" => "ctrl.note.title", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveTitle($event)",
|
||||||
"ng-change" => "ctrl.nameChanged()", "ng-focus" => "ctrl.onNameFocus()",
|
"ng-change" => "ctrl.nameChanged()", "ng-focus" => "ctrl.onNameFocus()",
|
||||||
"select-on-click" => "true"}
|
"select-on-click" => "true"}
|
||||||
.save-status {{ctrl.noteStatus}}
|
.save-status{"ng-class" => "{'red bold': ctrl.saveError}"} {{ctrl.noteStatus}}
|
||||||
.tags
|
.tags
|
||||||
%input.tags-input{"type" => "text", "ng-keyup" => "$event.keyCode == 13 && ctrl.updateTagsFromTagsString($event, ctrl.tagsString)",
|
%input.tags-input{"type" => "text", "ng-keyup" => "$event.keyCode == 13 && ctrl.updateTagsFromTagsString($event, ctrl.tagsString)",
|
||||||
"ng-model" => "ctrl.tagsString", "placeholder" => "#tags", "ng-blur" => "ctrl.updateTagsFromTagsString($event, ctrl.tagsString)"}
|
"ng-model" => "ctrl.tagsString", "placeholder" => "#tags", "ng-blur" => "ctrl.updateTagsFromTagsString($event, ctrl.tagsString)"}
|
||||||
|
|||||||
@@ -1,170 +1,25 @@
|
|||||||
.header
|
.footer-bar
|
||||||
.header-content
|
.pull-left
|
||||||
.menu.left
|
.footer-bar-link
|
||||||
.items
|
%a{"ng-click" => "ctrl.accountMenuPressed()", "ng-class" => "{red: ctrl.error}"} Account
|
||||||
.item.account
|
%account-menu{"ng-if" => "ctrl.showAccountMenu"}
|
||||||
%div{"ng-click" => "ctrl.accountMenuPressed()"}
|
|
||||||
%div{"ng-if" => "ctrl.user"} Account
|
|
||||||
%div{"ng-if" => "!ctrl.user"} Sign in or Register
|
|
||||||
.panel.panel-default.account-panel.panel-right{"ng-if" => "ctrl.showAccountMenu"}
|
|
||||||
.panel-body
|
|
||||||
.account-items
|
|
||||||
.account-item.registration-login{"ng-if" => "!ctrl.user"}
|
|
||||||
.account-item
|
|
||||||
.meta-container
|
|
||||||
.title Server
|
|
||||||
.desc Enter your <a href="https://standardfile.org" target="_blank">Standard File</a> server address, or use the default.
|
|
||||||
.action-container
|
|
||||||
%form.account-form{'ng-submit' => 'ctrl.changeServer()', 'name' => "serverChangeForm"}
|
|
||||||
.form-tag.has-feedback
|
|
||||||
%input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'ctrl.serverData.url'}
|
|
||||||
%button.btn.dark-button.btn-block{:type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
|
||||||
%span.ladda-label Set Server
|
|
||||||
.meta-container
|
|
||||||
.title Sign in or Register
|
|
||||||
.desc
|
|
||||||
%form.account-form{'name' => "loginForm"}
|
|
||||||
.form-tag.has-feedback
|
|
||||||
%input.form-control.login-input{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'ctrl.loginData.email'}
|
|
||||||
.form-tag.has-feedback
|
|
||||||
%input.form-control.login-input{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.loginData.user_password'}
|
|
||||||
.checkbox{"ng-if" => "ctrl.localNotesCount() > 0"}
|
|
||||||
%label
|
|
||||||
%input{"type" => "checkbox", "ng-model" => "ctrl.loginData.mergeLocal", "ng-bind" => "true", "ng-change" => "ctrl.mergeLocalChanged()"}
|
|
||||||
Merge local notes ({{ctrl.localNotesCount()}} notes)
|
|
||||||
%button.btn.dark-button.half-button{"ng-click" => "ctrl.loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
|
||||||
%span Sign In
|
|
||||||
%button.btn.dark-button.half-button{"ng-click" => "ctrl.submitRegistrationForm()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
|
||||||
%span Register
|
|
||||||
%br
|
|
||||||
.login-forgot{"style" => "padding-top: 4px;"}
|
|
||||||
%a.btn.btn-link{"ng-click" => "ctrl.showResetForm = !ctrl.showResetForm"} Passwords cannot be forgotten.
|
|
||||||
.panel-status-text{"ng-if" => "ctrl.loginData.status", "style" => "font-size: 14px;"} {{ctrl.loginData.status}}
|
|
||||||
|
|
||||||
%div{"ng-if" => "ctrl.showResetForm"}
|
.footer-bar-link
|
||||||
%p{"style" => "font-size: 13px; text-align: center;"}
|
%a{"ng-click" => "ctrl.toggleExtensions()"} Extensions
|
||||||
Because notes are locally encrypted using a secret key derived from your password, there's no way to decrypt these notes if you forget your password.
|
%global-extensions-menu{"ng-if" => "ctrl.showExtensionsMenu"}
|
||||||
For this reason, Standard Notes cannot offer a password reset option. You <strong>must</strong> make sure to store or remember your password.
|
|
||||||
|
|
||||||
.account-item{"ng-if" => "ctrl.user"}
|
.footer-bar-link
|
||||||
.email {{ctrl.user.email}}
|
%a{"href" => "https://standardnotes.org", "target" => "_blank"}
|
||||||
.server {{ctrl.serverData.url}}
|
Help
|
||||||
.links{"ng-if" => "ctrl.user"}
|
|
||||||
-# .link-item
|
|
||||||
-# %a{"ng-click" => "ctrl.changePasswordPressed()"} Change Password
|
|
||||||
-# %form.account-form{"ng-if" => "ctrl.showNewPasswordForm", 'ng-submit' => 'ctrl.submitPasswordChange()', 'name' => "passwordChangeForm"}
|
|
||||||
-# .form-tag.has-feedback
|
|
||||||
-# %input.form-control.login-input{:autofocus => 'autofocus', :name => 'current', :placeholder => 'Current password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.current_password'}
|
|
||||||
-# .form-tag.has-feedback
|
|
||||||
-# %input.form-control.login-input{:placeholder => 'New password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.new_password', "autocomplete" => "new-password"}
|
|
||||||
-# .form-tag.has-feedback
|
|
||||||
-# %input.form-control.login-input{:placeholder => 'Confirm password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.new_password_confirmation', "autocomplete" => "new-password"}
|
|
||||||
-# %button.btn.dark-button.btn-block{:type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
|
||||||
-# %span.ladda-label Change Password
|
|
||||||
-# .panel-status-text{"ng-if" => "ctrl.passwordChangeData.status", "style" => "font-size: 14px;"}
|
|
||||||
-# {{ctrl.passwordChangeData.status}}
|
|
||||||
.link-item
|
|
||||||
%a{"ng-click" => "ctrl.signOutPressed()"} Sign Out
|
|
||||||
.meta-container
|
|
||||||
.title Local Encryption
|
|
||||||
.desc Notes are encrypted locally before being sent to the server. Neither the server owner nor an intrusive entity can decrypt your locally encrypted notes.
|
|
||||||
.action-container
|
|
||||||
%span.status-title Status:
|
|
||||||
{{ctrl.encryptionStatusForNotes()}}
|
|
||||||
.account-item{"ng-if" => "ctrl.user"}
|
|
||||||
.meta-container
|
|
||||||
.title Data Archives
|
|
||||||
.options{"style" => "font-size: 12px; margin-top: 4px;"}
|
|
||||||
%label
|
|
||||||
%input{"type" => "radio", "ng-model" => "ctrl.archiveEncryptionFormat.encrypted", "ng-value" => "true", "ng-change" => "ctrl.archiveEncryptionFormat.encrypted = true"}
|
|
||||||
Encrypted
|
|
||||||
%label
|
|
||||||
%input{"type" => "radio", "ng-model" => "ctrl.archiveEncryptionFormat.encrypted", "ng-value" => "false", "ng-change" => "ctrl.archiveEncryptionFormat.encrypted = false"}
|
|
||||||
Decrypted
|
|
||||||
.action-container
|
|
||||||
%a{"ng-click" => "ctrl.downloadDataArchive()"} Download Data Archive
|
|
||||||
%br
|
|
||||||
%div{"ng-if" => "!ctrl.importData.loading"}
|
|
||||||
%label#import-archive
|
|
||||||
%input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "ctrl.importFileSelected(files)"}
|
|
||||||
%a.disabled
|
|
||||||
%span
|
|
||||||
Import Data from Archive
|
|
||||||
.import-password{"ng-if" => "ctrl.importData.requestPassword"}
|
|
||||||
Enter the account password associated with the import file.
|
|
||||||
%input.field{"type" => "text", "ng-model" => "ctrl.importData.password"}
|
|
||||||
%button{"ng-click" => "ctrl.submitImportPassword()"} Decrypt & Import
|
|
||||||
.spinner{"ng-if" => "ctrl.importData.loading"}
|
|
||||||
|
|
||||||
.item
|
.pull-right
|
||||||
%a{"ng-click" => "ctrl.toggleExtensions()"} Extensions
|
|
||||||
.panel.panel-default.account-panel.panel-right.extensions-panel{"ng-if" => "ctrl.showExtensionsMenu"}
|
|
||||||
.panel-body
|
|
||||||
%div{"style" => "font-size: 18px;", "ng-if" => "!ctrl.extensionManager.extensions.length"} No extensions installed
|
|
||||||
.registered-extensions{"ng-if" => "ctrl.extensionManager.extensions.length"}
|
|
||||||
.extension{"ng-repeat" => "extension in ctrl.extensionManager.extensions"}
|
|
||||||
.name {{extension.name}}
|
|
||||||
.encryption-format
|
|
||||||
.title Send data:
|
|
||||||
%label
|
|
||||||
%input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "true", "ng-change" => "ctrl.extensionManager.changeExtensionEncryptionFormat(true, extension)"}
|
|
||||||
Encrypted
|
|
||||||
%label
|
|
||||||
%input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "false", "ng-change" => "ctrl.extensionManager.changeExtensionEncryptionFormat(false, extension)"}
|
|
||||||
Decrypted
|
|
||||||
.actions
|
|
||||||
.action{"ng-repeat" => "action in extension.actionsInGlobalContext()"}
|
|
||||||
.name {{action.label}}
|
|
||||||
.desc{"style" => "font-style: italic;"} {{action.desc}}
|
|
||||||
.execute-type{"ng-if" => "action.repeat_mode == 'watch'"}
|
|
||||||
Repeats when a change is made to your items.
|
|
||||||
.execute-type{"ng-if" => "action.repeat_mode == 'loop'"}
|
|
||||||
Repeats at most once every {{action.repeat_timeout}} seconds
|
|
||||||
.permissions
|
|
||||||
%a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}}
|
|
||||||
%div{"ng-if" => "action.showPermissions"}
|
|
||||||
{{action.permissionsString}}
|
|
||||||
.encryption-type
|
|
||||||
%span {{action.encryptionModeString}}
|
|
||||||
.execute
|
|
||||||
%div{"ng-if" => "action.repeat_mode"}
|
|
||||||
%div{"ng-if" => "ctrl.extensionManager.isRepeatActionEnabled(action)", "ng-click" => "ctrl.extensionManager.disableRepeatAction(action, extension)"} Disable
|
|
||||||
%div{"ng-if" => "!ctrl.extensionManager.isRepeatActionEnabled(action)", "ng-click" => "ctrl.extensionManager.enableRepeatAction(action, extension)"} Enable
|
|
||||||
%div{"ng-if" => "!action.repeat_mode", "ng-click" => "ctrl.selectedAction(action, extension)"}
|
|
||||||
%div{"ng-if" => "!action.running"}
|
|
||||||
Perform Action
|
|
||||||
%div{"ng-if" => "action.running"}
|
|
||||||
.spinner.execution-spinner
|
|
||||||
.last-run{"ng-if" => "!action.error && action.lastExecuted && !action.running"}
|
|
||||||
Last run {{action.lastExecuted | appDateTime}}
|
|
||||||
.error{"ng-if" => "action.error"}
|
|
||||||
Error performing action.
|
|
||||||
%a{"ng-click" => "ctrl.deleteExtension(extension)", "style" => "margin-top: 22px; display: block; text-align: center;"} Remove extension
|
|
||||||
|
|
||||||
.extension-link
|
.footer-bar-link{"style" => "margin-right: 5px;"}
|
||||||
%a{"ng-click" => "ctrl.toggleExtensionForm()"} Add new extension
|
%div{"ng-if" => "ctrl.lastSyncDate", "style" => "float: left; font-weight: normal; margin-right: 8px;"}
|
||||||
|
%span{"ng-if" => "!ctrl.isRefreshing"}
|
||||||
|
Last refreshed {{ctrl.lastSyncDate | appDateTime}}
|
||||||
|
%span{"ng-if" => "ctrl.isRefreshing"}
|
||||||
|
.spinner{"style" => "margin-top: 2px;"}
|
||||||
|
|
||||||
%form.extension-form{"ng-if" => "ctrl.showNewExtensionForm"}
|
%strong{"ng-if" => "ctrl.offline"} Offline
|
||||||
.form-tag.has-feedback
|
%a{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"} Refresh
|
||||||
%input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Extension URL', :required => true, :type => 'url', 'ng-model' => 'ctrl.newExtensionData.url'}
|
|
||||||
%button.btn.dark-button.btn-block{"ng-click" => "ctrl.submitNewExtensionForm()", :type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
|
||||||
%span.ladda-label Add Extension
|
|
||||||
|
|
||||||
.extension-link
|
|
||||||
%a{"ng-click" => "ctrl.reloadExtensionsPressed()", "ng-if" => "ctrl.extensionManager.extensions.length > 0"} Reload all extensions
|
|
||||||
.extension-link
|
|
||||||
%a{"href" => "https://standardnotes.org/extensions", "target" => "_blank"} List of available extensions
|
|
||||||
|
|
||||||
.item
|
|
||||||
%a{"href" => "https://standardnotes.org", "target" => "_blank"}
|
|
||||||
Help
|
|
||||||
|
|
||||||
.menu.right
|
|
||||||
.items
|
|
||||||
.item.last-refreshed{"ng-if" => "ctrl.lastSyncDate"}
|
|
||||||
%span{"ng-if" => "!ctrl.isRefreshing"}
|
|
||||||
Last refreshed {{ctrl.lastSyncDate | appDateTime}}
|
|
||||||
%span{"ng-if" => "ctrl.isRefreshing"}
|
|
||||||
.spinner
|
|
||||||
.item{"ng-click" => "ctrl.refreshData()"}
|
|
||||||
Refresh
|
|
||||||
|
|||||||
@@ -8,4 +8,5 @@
|
|||||||
"tag" => "selectedTag", "remove" => "deleteNote"}
|
"tag" => "selectedTag", "remove" => "deleteNote"}
|
||||||
|
|
||||||
%editor-section{"ng-if" => "selectedNote", "note" => "selectedNote", "remove" => "deleteNote", "save" => "saveNote", "update-tags" => "updateTagsForNote"}
|
%editor-section{"ng-if" => "selectedNote", "note" => "selectedNote", "remove" => "deleteNote", "save" => "saveNote", "update-tags" => "updateTagsForNote"}
|
||||||
%header{"user" => "defaultUser"}
|
|
||||||
|
%header
|
||||||
|
|||||||
@@ -18,10 +18,11 @@
|
|||||||
%li
|
%li
|
||||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedTagDelete()"} Delete Tag
|
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedTagDelete()"} Delete Tag
|
||||||
|
|
||||||
.note{"ng-repeat" => "note in ctrl.tag.notes | filter: ctrl.filterNotes",
|
%div{"infinite-scroll" => "ctrl.paginate()", "can-load" => "true", "threshold" => "200"}
|
||||||
"ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}"}
|
.note{"ng-repeat" => "note in ctrl.tag.notes | limitTo:ctrl.notesToDisplay | filter: ctrl.filterNotes",
|
||||||
.name{"ng-if" => "note.title"}
|
"ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}"}
|
||||||
{{note.title}}
|
.name{"ng-if" => "note.title"}
|
||||||
.note-preview
|
{{note.title}}
|
||||||
{{note.text}}
|
.note-preview
|
||||||
.date {{(note.created_at | appDateTime) || 'Now'}}
|
{{note.text}}
|
||||||
|
.date {{(note.created_at | appDateTime) || 'Now'}}
|
||||||
|
|||||||
@@ -13,5 +13,5 @@
|
|||||||
|
|
||||||
%input.title{"ng-disabled" => "tag != ctrl.selectedTag", "ng-model" => "tag.title",
|
%input.title{"ng-disabled" => "tag != ctrl.selectedTag", "ng-model" => "tag.title",
|
||||||
"ng-keyup" => "$event.keyCode == 13 && ctrl.saveTag($event, tag)", "mb-autofocus" => "true", "should-focus" => "ctrl.newTag",
|
"ng-keyup" => "$event.keyCode == 13 && ctrl.saveTag($event, tag)", "mb-autofocus" => "true", "should-focus" => "ctrl.newTag",
|
||||||
"ng-change" => "ctrl.tagTitleDidChange(tag)", "ng-focus" => "ctrl.onTagTitleFocus(tag)"}
|
"ng-change" => "ctrl.tagTitleDidChange(tag)", "ng-focus" => "ctrl.onTagTitleFocus(tag)", "ng-blur" => "ctrl.saveTag($event, tag)"}
|
||||||
.count {{ctrl.noteCount(tag)}}
|
.count {{ctrl.noteCount(tag)}}
|
||||||
|
|||||||
Reference in New Issue
Block a user