initial commit
This commit is contained in:
16
app/assets/javascripts/app/app.frontend.js
Normal file
16
app/assets/javascripts/app/app.frontend.js
Normal file
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
var Neeto = Neeto || {};
|
||||
|
||||
angular
|
||||
.module('app.frontend', [
|
||||
'app.services',
|
||||
'ui.router',
|
||||
'ng-token-auth',
|
||||
'restangular',
|
||||
'ipCookie',
|
||||
'oc.lazyLoad',
|
||||
'angularLazyImg',
|
||||
'ngDialog',
|
||||
])
|
||||
.config(configureAuth);
|
||||
23
app/assets/javascripts/app/app.services.js
Normal file
23
app/assets/javascripts/app/app.services.js
Normal file
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('app.services', [
|
||||
'restangular'
|
||||
])
|
||||
|
||||
// Configure path to API
|
||||
.config(function (RestangularProvider, apiControllerProvider) {
|
||||
var url = apiControllerProvider.defaultServerURL();
|
||||
RestangularProvider.setBaseUrl(url);
|
||||
})
|
||||
|
||||
// Shared function for configure auth service. Can be overwritten.
|
||||
function configureAuth ($authProvider, apiControllerProvider) {
|
||||
var url = apiControllerProvider.defaultServerURL();
|
||||
$authProvider.configure([{
|
||||
default: {
|
||||
apiUrl: url,
|
||||
passwordResetSuccessUrl: window.location.protocol + '//' + window.location.host + '/auth/reset',
|
||||
}
|
||||
}]);
|
||||
}
|
||||
20
app/assets/javascripts/app/frontend/controllers/_base.js
Normal file
20
app/assets/javascripts/app/frontend/controllers/_base.js
Normal file
@@ -0,0 +1,20 @@
|
||||
angular.module('app.frontend')
|
||||
.controller('BaseCtrl', function ($rootScope, $scope, $state, $auth, apiController) {
|
||||
$rootScope.$on('auth:password-change-success', function(ev) {
|
||||
$state.go("home");
|
||||
});
|
||||
|
||||
$rootScope.$on('auth:password-change-error', function(ev, reason) {
|
||||
alert("Error: " + reason);
|
||||
});
|
||||
|
||||
$rootScope.resetPasswordSubmit = function() {
|
||||
var new_keys = Neeto.crypto.generateEncryptionKeysForUser($rootScope.resetData.password, $rootScope.resetData.email);
|
||||
var data = _.clone($rootScope.resetData);
|
||||
data.password = new_keys.pw;
|
||||
data.password_confirmation = new_keys.pw;
|
||||
$auth.updatePassword(data);
|
||||
apiController.setGk(new_keys.gk);
|
||||
}
|
||||
|
||||
});
|
||||
296
app/assets/javascripts/app/frontend/controllers/editor.js
Normal file
296
app/assets/javascripts/app/frontend/controllers/editor.js
Normal file
@@ -0,0 +1,296 @@
|
||||
angular.module('app.frontend')
|
||||
.directive("editorSection", function($timeout){
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
save: "&",
|
||||
remove: "&",
|
||||
note: "=",
|
||||
user: "="
|
||||
},
|
||||
templateUrl: 'frontend/editor.html',
|
||||
replace: true,
|
||||
controller: 'EditorCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: true,
|
||||
|
||||
link:function(scope, elem, attrs, ctrl) {
|
||||
|
||||
var handler = function(event) {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
switch (String.fromCharCode(event.which).toLowerCase()) {
|
||||
case 's':
|
||||
event.preventDefault();
|
||||
$timeout(function(){
|
||||
ctrl.saveNote(event);
|
||||
});
|
||||
break;
|
||||
case 'e':
|
||||
event.preventDefault();
|
||||
$timeout(function(){
|
||||
ctrl.clickedEditNote();
|
||||
})
|
||||
break;
|
||||
case 'm':
|
||||
event.preventDefault();
|
||||
$timeout(function(){
|
||||
ctrl.toggleMarkdown();
|
||||
})
|
||||
break;
|
||||
case 'o':
|
||||
event.preventDefault();
|
||||
$timeout(function(){
|
||||
ctrl.toggleFullScreen();
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handler);
|
||||
|
||||
scope.$on('$destroy', function(){
|
||||
window.removeEventListener('keydown', handler);
|
||||
})
|
||||
|
||||
scope.$watch('ctrl.note', function(note, oldNote){
|
||||
if(note) {
|
||||
ctrl.setNote(note, oldNote);
|
||||
} else {
|
||||
ctrl.note = {};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('EditorCtrl', function ($sce, $timeout, apiController, markdownRenderer, $rootScope) {
|
||||
|
||||
this.demoNotes = [
|
||||
{name: "Live print a file with tail", content: "tail -f log/production.log"},
|
||||
{name: "Create SSH tunnel", content: "ssh -i .ssh/key.pem -N -L 3306:example.com:3306 ec2-user@example.com"},
|
||||
{name: "List of processes running on port", content: "lsof -i:8080"},
|
||||
{name: "Set ENV from file", content: "export $(cat .envfile | xargs)"},
|
||||
{name: "Find process by name", content: "ps -ax | grep <application name>"},
|
||||
{name: "NPM install without sudo", content: "sudo chown -R $(whoami) ~/.npm"},
|
||||
{name: "Email validation regex", content: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"},
|
||||
{name: "Ruby generate 256 bit key", content: "Digest::SHA256.hexdigest(SecureRandom.random_bytes(32))"},
|
||||
{name: "Mac add user to user group", content: "sudo dseditgroup -o edit -a USERNAME -t user GROUPNAME"},
|
||||
{name: "Kill Mac OS System Apache", content: "sudo launchctl unload -w /System/Library/LaunchDaemons/org.apache.httpd.plist"},
|
||||
{name: "Docker run with mount binding and port", content: "docker run -v /home/vagrant/www/app:/var/www/app -p 8080:80 -d kpi/s3"},
|
||||
{name: "MySQL grant privileges", content: "GRANT [type of permission] ON [database name].[table name] TO ‘[username]’@'%’;"},
|
||||
{name: "MySQL list users", content: "SELECT User FROM mysql.user;"},
|
||||
];
|
||||
|
||||
this.showSampler = !this.user.id && this.user.filteredNotes().length == 0;
|
||||
|
||||
this.demoNoteNames = _.map(this.demoNotes, function(note){
|
||||
return note.name;
|
||||
});
|
||||
|
||||
this.currentDemoContent = {text: null};
|
||||
|
||||
this.prebeginFn = function() {
|
||||
this.currentDemoContent.text = null;
|
||||
}.bind(this)
|
||||
|
||||
this.callback = function(index) {
|
||||
this.currentDemoContent.text = this.demoNotes[index].content;
|
||||
}.bind(this)
|
||||
|
||||
this.contentCallback = function(index) {
|
||||
}
|
||||
|
||||
this.setNote = function(note, oldNote) {
|
||||
this.editorMode = 'edit';
|
||||
if(note.content.length == 0) {
|
||||
this.focusTitle(100);
|
||||
}
|
||||
|
||||
if(oldNote && oldNote != note) {
|
||||
if(oldNote.hasChanges) {
|
||||
this.save()(oldNote, null);
|
||||
} else if(oldNote.dummy) {
|
||||
this.remove()(oldNote);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.onPreviewDoubleClick = function() {
|
||||
this.editorMode = 'edit';
|
||||
this.focusEditor(100);
|
||||
}
|
||||
|
||||
this.focusEditor = function(delay) {
|
||||
setTimeout(function(){
|
||||
var element = document.getElementById("note-text-editor");
|
||||
element.focus();
|
||||
}, delay)
|
||||
}
|
||||
|
||||
this.focusTitle = function(delay) {
|
||||
setTimeout(function(){
|
||||
document.getElementById("note-title-editor").focus();
|
||||
}, delay)
|
||||
}
|
||||
|
||||
this.clickedTextArea = function() {
|
||||
this.showMenu = false;
|
||||
}
|
||||
|
||||
this.renderedContent = function() {
|
||||
return markdownRenderer.renderHtml(markdownRenderer.renderedContentForText(this.note.content));
|
||||
}
|
||||
|
||||
var statusTimeout;
|
||||
|
||||
this.saveNote = function($event) {
|
||||
var note = this.note;
|
||||
note.dummy = false;
|
||||
this.save()(note, function(success){
|
||||
if(success) {
|
||||
apiController.clearDraft();
|
||||
|
||||
if(statusTimeout) $timeout.cancel(statusTimeout);
|
||||
statusTimeout = $timeout(function(){
|
||||
this.noteStatus = "All changes saved"
|
||||
}.bind(this), 200)
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.saveTitle = function($event) {
|
||||
$event.target.blur();
|
||||
this.saveNote($event);
|
||||
this.focusEditor();
|
||||
}
|
||||
|
||||
var saveTimeout;
|
||||
this.changesMade = function() {
|
||||
this.note.hasChanges = true;
|
||||
this.note.dummy = false;
|
||||
apiController.saveDraftToDisk(this.note);
|
||||
|
||||
if(saveTimeout) $timeout.cancel(saveTimeout);
|
||||
if(statusTimeout) $timeout.cancel(statusTimeout);
|
||||
saveTimeout = $timeout(function(){
|
||||
this.noteStatus = "Saving...";
|
||||
this.saveNote();
|
||||
}.bind(this), 150)
|
||||
}
|
||||
|
||||
|
||||
this.contentChanged = function() {
|
||||
this.changesMade();
|
||||
}
|
||||
|
||||
this.nameChanged = function() {
|
||||
this.changesMade();
|
||||
}
|
||||
|
||||
this.onNameFocus = function() {
|
||||
this.editingName = true;
|
||||
}
|
||||
|
||||
this.onContentFocus = function() {
|
||||
this.showSampler = false;
|
||||
$rootScope.$broadcast("editorFocused");
|
||||
this.editingUrl = false;
|
||||
}
|
||||
|
||||
this.onNameBlur = function() {
|
||||
this.editingName = false;
|
||||
}
|
||||
|
||||
this.toggleFullScreen = function() {
|
||||
this.fullscreen = !this.fullscreen;
|
||||
if(this.fullscreen) {
|
||||
if(this.editorMode == 'edit') {
|
||||
// refocus
|
||||
this.focusEditor(0);
|
||||
}
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedMenuItem = function() {
|
||||
this.showMenu = false;
|
||||
}
|
||||
|
||||
this.toggleMarkdown = function() {
|
||||
if(this.editorMode == 'preview') {
|
||||
this.editorMode = 'edit';
|
||||
} else {
|
||||
this.editorMode = 'preview';
|
||||
}
|
||||
}
|
||||
|
||||
this.editUrlPressed = function() {
|
||||
this.showMenu = false;
|
||||
var url = this.publicUrlForNote(this.note);
|
||||
url = url.replace(this.note.presentation.root_path, "");
|
||||
this.url = {base: url, token : this.note.presentation.root_path};
|
||||
this.editingUrl = true;
|
||||
}
|
||||
|
||||
this.saveUrl = function($event) {
|
||||
$event.target.blur();
|
||||
var original = this.note.presentation.root_path;
|
||||
this.note.presentation.root_path = this.url.token;
|
||||
apiController.saveNote(this.user, this.note, function(note){
|
||||
if(!note) {
|
||||
this.note.token = original;
|
||||
this.url.token = original;
|
||||
alert("This URL is not available.");
|
||||
} else {
|
||||
this.editingUrl = false;
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
this.shareNote = function() {
|
||||
|
||||
function openInNewTab(url) {
|
||||
var a = document.createElement("a");
|
||||
a.target = "_blank";
|
||||
a.href = url;
|
||||
a.click();
|
||||
}
|
||||
|
||||
apiController.shareNote(this.user, this.note, function(note){
|
||||
openInNewTab(this.publicUrlForNote(note));
|
||||
}.bind(this))
|
||||
this.showMenu = false;
|
||||
}
|
||||
|
||||
this.unshareNote = function() {
|
||||
apiController.unshareNote(this.user, this.note, function(note){
|
||||
|
||||
})
|
||||
this.showMenu = false;
|
||||
}
|
||||
|
||||
this.publicUrlForNote = function() {
|
||||
return this.note.presentationURL();
|
||||
}
|
||||
|
||||
this.clickedMenu = function() {
|
||||
if(this.note.locked) {
|
||||
alert("This note has been shared without an account, and can therefore not be changed.")
|
||||
} else {
|
||||
this.showMenu = !this.showMenu;
|
||||
}
|
||||
}
|
||||
|
||||
this.deleteNote = function() {
|
||||
apiController.clearDraft();
|
||||
this.remove()(this.note);
|
||||
this.showMenu = false;
|
||||
}
|
||||
|
||||
this.clickedEditNote = function() {
|
||||
this.editorMode = 'edit';
|
||||
this.focusEditor(100);
|
||||
}
|
||||
|
||||
});
|
||||
116
app/assets/javascripts/app/frontend/controllers/groups.js
Normal file
116
app/assets/javascripts/app/frontend/controllers/groups.js
Normal file
@@ -0,0 +1,116 @@
|
||||
angular.module('app.frontend')
|
||||
.directive("groupsSection", function(){
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
addNew: "&",
|
||||
selectionMade: "&",
|
||||
willSelect: "&",
|
||||
save: "&",
|
||||
groups: "=",
|
||||
allGroup: "=",
|
||||
user: "=",
|
||||
updateNoteGroup: "&"
|
||||
},
|
||||
templateUrl: 'frontend/groups.html',
|
||||
replace: true,
|
||||
controller: 'GroupsCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: true,
|
||||
|
||||
link:function(scope, elem, attrs, ctrl) {
|
||||
scope.$watch('ctrl.groups', function(newGroups){
|
||||
if(newGroups) {
|
||||
ctrl.setGroups(newGroups);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('GroupsCtrl', function (apiController) {
|
||||
|
||||
var initialLoad = true;
|
||||
|
||||
this.setGroups = function(groups) {
|
||||
if(initialLoad) {
|
||||
initialLoad = false;
|
||||
this.selectGroup(this.allGroup);
|
||||
} else {
|
||||
if(groups && groups.length > 0) {
|
||||
this.selectGroup(groups[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.selectGroup = function(group) {
|
||||
this.willSelect()(group);
|
||||
this.selectedGroup = group;
|
||||
this.selectionMade()(group);
|
||||
}
|
||||
|
||||
this.clickedAddNewGroup = function() {
|
||||
if(this.editingGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.newGroup = {notes : []};
|
||||
if(!this.user.id) {
|
||||
this.newGroup.id = Neeto.crypto.generateRandomKey()
|
||||
}
|
||||
this.selectedGroup = this.newGroup;
|
||||
this.editingGroup = this.newGroup;
|
||||
this.addNew()(this.newGroup);
|
||||
}
|
||||
|
||||
var originalGroupName = "";
|
||||
this.onGroupTitleFocus = function(group) {
|
||||
originalGroupName = group.name;
|
||||
}
|
||||
|
||||
this.groupTitleDidChange = function(group) {
|
||||
this.editingGroup = group;
|
||||
}
|
||||
|
||||
this.saveGroup = function($event, group) {
|
||||
this.editingGroup = null;
|
||||
if(group.name.length == 0) {
|
||||
group.name = originalGroupName;
|
||||
originalGroupName = "";
|
||||
return;
|
||||
}
|
||||
|
||||
$event.target.blur();
|
||||
if(!group.name || group.name.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.save()(group, function(savedGroup){
|
||||
_.merge(group, savedGroup);
|
||||
this.selectGroup(group);
|
||||
this.newGroup = null;
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.noteCount = function(group) {
|
||||
var validNotes = apiController.filterDummyNotes(group.notes);
|
||||
return validNotes.length;
|
||||
}
|
||||
|
||||
this.handleDrop = function(e, newGroup, note) {
|
||||
if(this.selectedGroup.all) {
|
||||
// coming from all, remove from original group if applicable
|
||||
if(note.group_id) {
|
||||
var originalGroup = this.groups.filter(function(group){
|
||||
return group.id == note.group_id;
|
||||
})[0];
|
||||
_.remove(originalGroup.notes, note);
|
||||
}
|
||||
} else {
|
||||
_.remove(this.selectedGroup.notes, note);
|
||||
}
|
||||
|
||||
this.updateNoteGroup()(note, newGroup, this.selectedGroup);
|
||||
}.bind(this)
|
||||
|
||||
|
||||
});
|
||||
232
app/assets/javascripts/app/frontend/controllers/header.js
Normal file
232
app/assets/javascripts/app/frontend/controllers/header.js
Normal file
@@ -0,0 +1,232 @@
|
||||
angular.module('app.frontend')
|
||||
.directive("header", function(){
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
user: "=",
|
||||
logout: "&"
|
||||
},
|
||||
templateUrl: 'frontend/header.html',
|
||||
replace: true,
|
||||
controller: 'HeaderCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: true,
|
||||
|
||||
link:function(scope, elem, attrs, ctrl) {
|
||||
scope.$on('auth:login-success', function(event, user) {
|
||||
ctrl.onAuthSuccess(user);
|
||||
});
|
||||
|
||||
scope.$on('auth:validation-success', function(ev) {
|
||||
setTimeout(function(){
|
||||
ctrl.onValidationSuccess();
|
||||
})
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('HeaderCtrl', function ($auth, $state, apiController, serverSideValidation, $timeout) {
|
||||
|
||||
this.changePasswordPressed = function() {
|
||||
this.showNewPasswordForm = !this.showNewPasswordForm;
|
||||
}
|
||||
|
||||
this.accountMenuPressed = function() {
|
||||
this.serverData = {url: apiController.getServer()};
|
||||
this.showAccountMenu = !this.showAccountMenu;
|
||||
this.showFaq = false;
|
||||
this.showNewPasswordForm = false;
|
||||
}
|
||||
|
||||
this.changeServer = function() {
|
||||
apiController.setServer(this.serverData.url, true);
|
||||
}
|
||||
|
||||
this.signOutPressed = function() {
|
||||
this.showAccountMenu = false;
|
||||
$auth.signOut();
|
||||
this.logout()();
|
||||
apiController.clearGk();
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
this.submitPasswordChange = function() {
|
||||
this.passwordChangeData.status = "Generating New Keys...";
|
||||
|
||||
$timeout(function(){
|
||||
var current_keys = Neeto.crypto.generateEncryptionKeysForUser(this.passwordChangeData.current_password, this.user.email);
|
||||
var new_keys = Neeto.crypto.generateEncryptionKeysForUser(this.passwordChangeData.new_password, this.user.email);
|
||||
// var new_pw_conf_keys = Neeto.crypto.generateEncryptionKeysForUser(this.passwordChangeData.new_password_confirmation, this.user.email);
|
||||
|
||||
var data = {};
|
||||
data.current_password = current_keys.pw;
|
||||
data.password = new_keys.pw;
|
||||
data.password_confirmation = new_keys.pw;
|
||||
|
||||
var user = this.user;
|
||||
|
||||
if(data.password == data.password_confirmation) {
|
||||
$auth.updatePassword(data)
|
||||
.then(function(response) {
|
||||
this.showNewPasswordForm = false;
|
||||
if(user.local_encryption_enabled) {
|
||||
// reencrypt data with new gk
|
||||
apiController.reencryptAllNotesAndSave(user, new_keys.gk, current_keys.gk, function(success){
|
||||
if(success) {
|
||||
apiController.setGk(new_keys.gk);
|
||||
alert("Your password has been changed and your data re-encrypted.");
|
||||
} else {
|
||||
// rollback password
|
||||
$auth.updatePassword({current_password: new_keys.pw, password: current_keys.pw, password_confirmation: current_keys.pw })
|
||||
.then(function(response){
|
||||
alert("There was an error changing your password. Your password has been rolled back.");
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alert("Your password has been changed.");
|
||||
}
|
||||
}.bind(this))
|
||||
.catch(function(response){
|
||||
this.showNewPasswordForm = false;
|
||||
alert("There was an error changing your password. Please try again.");
|
||||
}.bind(this))
|
||||
|
||||
} else {
|
||||
alert("Your new password does not match its confirmation.");
|
||||
}
|
||||
}.bind(this))
|
||||
|
||||
}
|
||||
|
||||
this.hasLocalData = function() {
|
||||
return this.user.filteredNotes().length > 0;
|
||||
}
|
||||
|
||||
this.mergeLocalChanged = function() {
|
||||
if(!this.user.shouldMerge) {
|
||||
if(!confirm("Unchecking this option means any locally stored groups and notes you have now will be deleted. Are you sure you want to continue?")) {
|
||||
this.user.shouldMerge = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.loginSubmitPressed = function() {
|
||||
this.loginData.status = "Generating Login Keys...";
|
||||
$timeout(function(){
|
||||
var keys = Neeto.crypto.generateEncryptionKeysForUser(this.loginData.user_password, this.loginData.email);
|
||||
var data = {password: keys.pw, email: this.loginData.email};
|
||||
|
||||
apiController.setGk(keys.gk);
|
||||
$auth.submitLogin(data)
|
||||
.then(function(response){
|
||||
|
||||
})
|
||||
.catch(function(response){
|
||||
this.loginData.status = response.errors[0];
|
||||
}.bind(this))
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
this.submitRegistrationForm = function() {
|
||||
this.loginData.status = "Generating Account Keys...";
|
||||
|
||||
$timeout(function(){
|
||||
var keys = Neeto.crypto.generateEncryptionKeysForUser(this.loginData.user_password, this.loginData.email);
|
||||
var data = {password: keys.pw, email: this.loginData.email};
|
||||
|
||||
apiController.setGk(keys.gk);
|
||||
|
||||
$auth.submitRegistration(data)
|
||||
.then(function(response) {
|
||||
$auth.user.id = response.data.data.id;
|
||||
this.onAuthSuccess($auth.user);
|
||||
}.bind(this))
|
||||
.catch(function(response) {
|
||||
this.loginData.status = response.data.errors.full_messages[0];
|
||||
}.bind(this));
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
this.forgotPasswordSubmit = function() {
|
||||
$auth.requestPasswordReset(this.resetData)
|
||||
.then(function(resp) {
|
||||
this.resetData.response = "Success";
|
||||
// handle success response
|
||||
}.bind(this))
|
||||
.catch(function(resp) {
|
||||
// handle error response
|
||||
this.resetData.response = "Error";
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.onValidationSuccess = function() {
|
||||
if(this.user.local_encryption_enabled) {
|
||||
apiController.verifyEncryptionStatusOfAllNotes(this.user, function(success){
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.encryptionStatusForNotes = function() {
|
||||
var allNotes = this.user.filteredNotes();
|
||||
var countEncrypted = 0;
|
||||
allNotes.forEach(function(note){
|
||||
if(note.isEncrypted()) {
|
||||
countEncrypted++;
|
||||
}
|
||||
}.bind(this))
|
||||
|
||||
return countEncrypted + "/" + allNotes.length + " notes encrypted";
|
||||
}
|
||||
|
||||
this.toggleEncryptionStatus = function() {
|
||||
this.encryptionConfirmation = true;
|
||||
}
|
||||
|
||||
this.cancelEncryptionChange = function() {
|
||||
this.encryptionConfirmation = false;
|
||||
}
|
||||
|
||||
this.confirmEncryptionChange = function() {
|
||||
|
||||
var callback = function(success, enabled) {
|
||||
if(success) {
|
||||
this.encryptionConfirmation = false;
|
||||
this.user.local_encryption_enabled = enabled;
|
||||
}
|
||||
}.bind(this)
|
||||
|
||||
if(this.user.local_encryption_enabled) {
|
||||
apiController.disableEncryptionForUser(this.user, callback);
|
||||
} else {
|
||||
apiController.enableEncryptionForUser(this.user, callback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.downloadDataArchive = function() {
|
||||
var link = document.createElement('a');
|
||||
link.setAttribute('download', 'neeto.json');
|
||||
link.href = apiController.notesDataFile(this.user);
|
||||
link.click();
|
||||
}
|
||||
|
||||
this.onAuthSuccess = function(user) {
|
||||
this.user.id = user.id;
|
||||
|
||||
if(this.user.shouldMerge && this.hasLocalData()) {
|
||||
apiController.mergeLocalDataRemotely(this.user, function(){
|
||||
window.location.reload();
|
||||
});
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
this.showLogin = false;
|
||||
this.showRegistration = false;
|
||||
}
|
||||
|
||||
});
|
||||
188
app/assets/javascripts/app/frontend/controllers/home.js
Normal file
188
app/assets/javascripts/app/frontend/controllers/home.js
Normal file
@@ -0,0 +1,188 @@
|
||||
angular.module('app.frontend')
|
||||
.controller('HomeCtrl', function ($scope, $rootScope, Restangular, $timeout, $state, $sce, $auth, apiController) {
|
||||
$rootScope.bodyClass = "app-body-class";
|
||||
$rootScope.title = "Notes — Neeto, a secure code box for developers";
|
||||
$rootScope.description = "A secure code box for developers to store common commands and useful notes.";
|
||||
|
||||
var onUserSet = function() {
|
||||
|
||||
$scope.defaultUser.notes = _.map($scope.defaultUser.notes, function(json_obj) {
|
||||
return new Note(json_obj);
|
||||
});
|
||||
|
||||
$scope.defaultUser.filteredNotes = function() {
|
||||
return apiController.filterDummyNotes($scope.defaultUser.notes);
|
||||
}
|
||||
var groups = $scope.defaultUser.groups;
|
||||
var allNotes = $scope.defaultUser.notes;
|
||||
groups.forEach(function(group){
|
||||
var notes = allNotes.filter(function(note){return note.group_id && note.group_id == group.id});
|
||||
group.notes = notes;
|
||||
})
|
||||
$scope.allGroup = {name: "All", all: true};
|
||||
$scope.groups = groups;
|
||||
}
|
||||
|
||||
$auth.validateUser()
|
||||
.then(function () {
|
||||
$scope.defaultUser = new User($auth.user);
|
||||
$rootScope.title = "Notes — Neeto";
|
||||
onUserSet();
|
||||
|
||||
})
|
||||
.catch(function () {
|
||||
$scope.defaultUser = new User(apiController.localUser());
|
||||
onUserSet();
|
||||
});
|
||||
|
||||
/*
|
||||
Groups Ctrl Callbacks
|
||||
*/
|
||||
|
||||
$scope.updateAllGroup = function() {
|
||||
var allNotes = apiController.filterDummyNotes($scope.defaultUser.notes);
|
||||
$scope.defaultUser.notes = allNotes;
|
||||
$scope.allGroup.notes = allNotes;
|
||||
}
|
||||
|
||||
$scope.groupsWillMakeSelection = function(group) {
|
||||
if(group.all) {
|
||||
$scope.updateAllGroup();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.groupsSelectionMade = function(group) {
|
||||
if(!group.notes) {
|
||||
group.notes = [];
|
||||
}
|
||||
$scope.selectedGroup = group;
|
||||
}
|
||||
|
||||
$scope.groupsAddNew = function(group) {
|
||||
$scope.defaultUser.groups.unshift(group);
|
||||
}
|
||||
|
||||
$scope.groupsSave = function(group, callback) {
|
||||
apiController.saveGroup($scope.defaultUser, group, callback);
|
||||
}
|
||||
|
||||
/*
|
||||
Called to update the group of a note after drag and drop change
|
||||
The note object is a copy of the original
|
||||
*/
|
||||
$scope.groupsUpdateNoteGroup = function(noteCopy, newGroup, oldGroup) {
|
||||
|
||||
var originalNote = _.find($scope.defaultUser.notes, {id: noteCopy.id});
|
||||
|
||||
if(newGroup.all) {
|
||||
// going to new group, nil out group_id
|
||||
originalNote.group_id = null;
|
||||
} else {
|
||||
originalNote.group_id = newGroup.id
|
||||
newGroup.notes.unshift(originalNote);
|
||||
newGroup.notes.sort(function(a,b){
|
||||
//subtract to get a value that is either negative, positive, or zero.
|
||||
return new Date(b.created_at) - new Date(a.created_at);
|
||||
});
|
||||
}
|
||||
|
||||
originalNote.shared_via_group = newGroup.presentation && newGroup.presentation.enabled;
|
||||
|
||||
apiController.saveNote($scope.defaultUser, originalNote, function(note){
|
||||
_.merge(originalNote, note);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
Notes Ctrl Callbacks
|
||||
*/
|
||||
|
||||
$scope.notesRemoveGroup = function(group) {
|
||||
var validNotes = apiController.filterDummyNotes(group.notes);
|
||||
if(validNotes == 0) {
|
||||
// if no more notes, delete group
|
||||
apiController.deleteGroup($scope.defaultUser, group, function(){
|
||||
// force scope groups to update on sub directives
|
||||
$scope.groups = [];
|
||||
$timeout(function(){
|
||||
$scope.groups = $scope.defaultUser.groups;
|
||||
})
|
||||
});
|
||||
} else {
|
||||
alert("To delete this group, remove all its notes first.");
|
||||
}
|
||||
}
|
||||
|
||||
$scope.notesSelectionMade = function(note) {
|
||||
$scope.selectedNote = note;
|
||||
}
|
||||
|
||||
$scope.notesAddNew = function(note) {
|
||||
if(!$scope.defaultUser.id) {
|
||||
// generate local id for note
|
||||
note.id = Neeto.crypto.generateRandomKey();
|
||||
}
|
||||
|
||||
$scope.defaultUser.notes.unshift(note);
|
||||
|
||||
if(!$scope.selectedGroup.all) {
|
||||
$scope.selectedGroup.notes.unshift(note);
|
||||
note.group_id = $scope.selectedGroup.id;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Shared Callbacks
|
||||
*/
|
||||
|
||||
$scope.saveNote = function(note, callback) {
|
||||
apiController.saveNote($scope.defaultUser, note, function(){
|
||||
// add to All notes if it doesnt exist
|
||||
if(!_.find($scope.defaultUser.notes, {id: note.id})) {
|
||||
$scope.defaultUser.notes.unshift(note);
|
||||
}
|
||||
note.hasChanges = false;
|
||||
|
||||
if(callback) {
|
||||
callback(true);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$scope.deleteNote = function(note) {
|
||||
_.remove($scope.defaultUser.notes, note);
|
||||
if($scope.selectedGroup.all && note.group_id) {
|
||||
var originalGroup = _.find($scope.groups, {id: note.group_id});
|
||||
if(originalGroup) {
|
||||
_.remove(originalGroup.notes, note);
|
||||
}
|
||||
} else {
|
||||
_.remove($scope.selectedGroup.notes, note);
|
||||
}
|
||||
|
||||
if(note == $scope.selectedNote) {
|
||||
$scope.selectedNote = null;
|
||||
}
|
||||
|
||||
note.onDelete();
|
||||
|
||||
if(note.dummy) {
|
||||
return;
|
||||
}
|
||||
|
||||
apiController.deleteNote($scope.defaultUser, note, function(success){
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Header Ctrl Callbacks
|
||||
*/
|
||||
|
||||
$scope.headerLogout = function() {
|
||||
$scope.defaultUser = apiController.localUser();
|
||||
$scope.groups = $scope.defaultUser.groups;
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
173
app/assets/javascripts/app/frontend/controllers/notes.js
Normal file
173
app/assets/javascripts/app/frontend/controllers/notes.js
Normal file
@@ -0,0 +1,173 @@
|
||||
angular.module('app.frontend')
|
||||
.directive("notesSection", function(){
|
||||
return {
|
||||
scope: {
|
||||
addNew: "&",
|
||||
selectionMade: "&",
|
||||
remove: "&",
|
||||
group: "=",
|
||||
user: "=",
|
||||
removeGroup: "&"
|
||||
},
|
||||
templateUrl: 'frontend/notes.html',
|
||||
replace: true,
|
||||
controller: 'NotesCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: true,
|
||||
|
||||
link:function(scope, elem, attrs, ctrl) {
|
||||
scope.$watch('ctrl.group', function(group, oldGroup){
|
||||
if(group) {
|
||||
ctrl.groupDidChange(group, oldGroup);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('NotesCtrl', function (apiController, $timeout, ngDialog, $rootScope) {
|
||||
|
||||
$rootScope.$on("editorFocused", function(){
|
||||
this.showMenu = false;
|
||||
}.bind(this))
|
||||
|
||||
var isFirstLoad = true;
|
||||
|
||||
this.groupDidChange = function(group, oldGroup) {
|
||||
this.showMenu = false;
|
||||
|
||||
if(this.selectedNote && this.selectedNote.dummy) {
|
||||
_.remove(oldGroup.notes, this.selectedNote);
|
||||
}
|
||||
|
||||
this.noteFilter.text = "";
|
||||
this.setNotes(group.notes, false);
|
||||
|
||||
if(isFirstLoad) {
|
||||
$timeout(function(){
|
||||
var draft = apiController.getDraft();
|
||||
if(draft) {
|
||||
var note = draft;
|
||||
this.selectNote(note);
|
||||
} else {
|
||||
this.createNewNote();
|
||||
isFirstLoad = false;
|
||||
}
|
||||
}.bind(this))
|
||||
} else if(group.notes.length == 0) {
|
||||
this.createNewNote();
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedGroupDelete = function() {
|
||||
this.showMenu = false;
|
||||
this.removeGroup()(this.group);
|
||||
}
|
||||
|
||||
this.selectedGroupShare = function() {
|
||||
this.showMenu = false;
|
||||
|
||||
if(!this.user.id) {
|
||||
alert("You must be signed in to share a group.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.group.all) {
|
||||
alert("You cannot share the 'All' group.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.user.local_encryption_enabled) {
|
||||
if(!confirm("Sharing this group will disable local encryption on all group notes.")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var callback = function(username) {
|
||||
apiController.shareGroup(this.user, this.group, function(response){
|
||||
})
|
||||
}.bind(this);
|
||||
|
||||
if(!this.user.getUsername()) {
|
||||
ngDialog.open({
|
||||
template: 'frontend/modals/username.html',
|
||||
controller: 'UsernameModalCtrl',
|
||||
resolve: {
|
||||
user: function() {return this.user}.bind(this),
|
||||
callback: function() {return callback}
|
||||
},
|
||||
className: 'ngdialog-theme-default',
|
||||
disableAnimation: true
|
||||
});
|
||||
} else {
|
||||
callback(this.user.getUsername());
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedGroupUnshare = function() {
|
||||
this.showMenu = false;
|
||||
apiController.unshareGroup(this.user, this.group, function(response){
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
this.publicUrlForGroup = function() {
|
||||
return this.group.presentation.url;
|
||||
}
|
||||
|
||||
this.setNotes = function(notes, createNew) {
|
||||
this.notes = notes;
|
||||
notes.forEach(function(note){
|
||||
note.visible = true;
|
||||
})
|
||||
apiController.decryptNotesWithLocalKey(notes);
|
||||
this.selectFirstNote(createNew);
|
||||
}
|
||||
|
||||
this.selectFirstNote = function(createNew) {
|
||||
var visibleNotes = this.notes.filter(function(note){
|
||||
return note.visible;
|
||||
});
|
||||
|
||||
if(visibleNotes.length > 0) {
|
||||
this.selectNote(visibleNotes[0]);
|
||||
} else if(createNew) {
|
||||
this.createNewNote();
|
||||
}
|
||||
}
|
||||
|
||||
this.selectNote = function(note) {
|
||||
this.selectedNote = note;
|
||||
this.selectionMade()(note);
|
||||
|
||||
note.onDelete = function() {
|
||||
this.setNotes(this.group.notes, false);
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
this.createNewNote = function() {
|
||||
var name = "New Note" + (this.notes ? (" " + (this.notes.length + 1)) : "");
|
||||
this.newNote = new Note({name: name, content: '', dummy: true});
|
||||
this.newNote.shared_via_group = this.group.presentation && this.group.presentation.enabled;
|
||||
this.selectNote(this.newNote);
|
||||
this.addNew()(this.newNote);
|
||||
}
|
||||
|
||||
this.noteFilter = {text : ''};
|
||||
|
||||
this.filterNotes = function(note) {
|
||||
if(this.noteFilter.text.length == 0) {
|
||||
note.visible = true;
|
||||
} else {
|
||||
note.visible = note.name.toLowerCase().includes(this.noteFilter.text) || note.content.toLowerCase().includes(this.noteFilter.text);
|
||||
}
|
||||
return note.visible;
|
||||
}.bind(this)
|
||||
|
||||
this.filterTextChanged = function() {
|
||||
$timeout(function(){
|
||||
if(!this.selectedNote.visible) {
|
||||
this.selectFirstNote(false);
|
||||
}
|
||||
}.bind(this), 100)
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
angular.module('app.frontend')
|
||||
.controller('UsernameModalCtrl', function ($scope, apiController, Restangular, user, callback, $timeout) {
|
||||
$scope.formData = {};
|
||||
|
||||
$scope.saveUsername = function() {
|
||||
apiController.setUsername(user, $scope.formData.username, function(response){
|
||||
var username = response.root_path;
|
||||
user.presentation = response;
|
||||
callback(username);
|
||||
$scope.closeThisDialog();
|
||||
})
|
||||
}
|
||||
});
|
||||
20
app/assets/javascripts/app/frontend/models/note.js
Normal file
20
app/assets/javascripts/app/frontend/models/note.js
Normal file
@@ -0,0 +1,20 @@
|
||||
var Note = function (json_obj) {
|
||||
_.merge(this, json_obj);
|
||||
};
|
||||
|
||||
/* Returns true if note is shared individually or via group */
|
||||
Note.prototype.isPublic = function() {
|
||||
return this.hasEnabledPresentation() || this.shared_via_group;
|
||||
};
|
||||
|
||||
Note.prototype.isEncrypted = function() {
|
||||
return this.local_eek ? true : false;
|
||||
}
|
||||
|
||||
Note.prototype.hasEnabledPresentation = function() {
|
||||
return this.presentation && this.presentation.enabled;
|
||||
}
|
||||
|
||||
Note.prototype.presentationURL = function() {
|
||||
return this.presentation.url;
|
||||
}
|
||||
10
app/assets/javascripts/app/frontend/models/user.js
Normal file
10
app/assets/javascripts/app/frontend/models/user.js
Normal file
@@ -0,0 +1,10 @@
|
||||
var User = function (json_obj) {
|
||||
_.merge(this, json_obj);
|
||||
};
|
||||
|
||||
User.prototype.getUsername = function() {
|
||||
if(!this.presentation) {
|
||||
return null;
|
||||
}
|
||||
return this.presentation.root_path;
|
||||
};
|
||||
116
app/assets/javascripts/app/frontend/routes.js
Normal file
116
app/assets/javascripts/app/frontend/routes.js
Normal file
@@ -0,0 +1,116 @@
|
||||
angular.module('app.frontend')
|
||||
.config(function ($stateProvider, $urlRouterProvider, $locationProvider) {
|
||||
|
||||
$stateProvider
|
||||
.state('base', {
|
||||
abstract: true,
|
||||
})
|
||||
|
||||
// Homepage
|
||||
.state('home', {
|
||||
url: '/',
|
||||
parent: 'base',
|
||||
views: {
|
||||
'content@' : {
|
||||
templateUrl: 'frontend/home.html',
|
||||
controller: 'HomeCtrl'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
.state('presentation', {
|
||||
url: '/:root_path',
|
||||
parent: 'base',
|
||||
views: {
|
||||
'content@' : {
|
||||
templateUrl: 'frontend/presentation.html',
|
||||
controller: "PresentationCtrl"
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
presentation: getPresentation
|
||||
}
|
||||
})
|
||||
|
||||
.state('group', {
|
||||
url: '/:root_path/:secondary_path',
|
||||
parent: 'base',
|
||||
views: {
|
||||
'content@' : {
|
||||
templateUrl: 'frontend/presentation.html',
|
||||
controller: "PresentationCtrl"
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
presentation: getPresentation
|
||||
}
|
||||
})
|
||||
|
||||
// Auth routes
|
||||
.state('auth', {
|
||||
abstract: true,
|
||||
url: '/auth',
|
||||
parent: 'base',
|
||||
views: {
|
||||
'content@' : {
|
||||
templateUrl: 'frontend/auth/wrapper.html',
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('auth.login', {
|
||||
url: '/login',
|
||||
templateUrl: 'frontend/auth/login.html',
|
||||
})
|
||||
.state('auth.forgot', {
|
||||
url: '/forgot',
|
||||
templateUrl: 'frontend/auth/forgot.html',
|
||||
})
|
||||
.state('auth.reset', {
|
||||
url: '/reset?reset_password_token&email',
|
||||
templateUrl: 'frontend/auth/reset.html',
|
||||
controller: function($rootScope, $stateParams) {
|
||||
$rootScope.resetData = {reset_password_token: $stateParams.reset_password_token, email: $stateParams.email};
|
||||
|
||||
// Clear reset_password_token on change state
|
||||
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams, options) {
|
||||
$rootScope.reset_password_token = null;
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
// 404 Error
|
||||
.state('404', {
|
||||
parent: 'base',
|
||||
views: {
|
||||
'content@' : {
|
||||
templateUrl: 'frontend/errors/404.html'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function getPresentation ($q, $state, $stateParams, Restangular) {
|
||||
var deferred = $q.defer();
|
||||
var restangularQuery = Restangular.one('presentations', 'show_by_path');
|
||||
restangularQuery.get({root_path: $stateParams.root_path, secondary_path: $stateParams.secondary_path})
|
||||
.then(function(response) {
|
||||
deferred.resolve(response);
|
||||
})
|
||||
.catch(function(response) {
|
||||
$state.go('404');
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Default fall back route
|
||||
$urlRouterProvider.otherwise(function($injector, $location){
|
||||
var state = $injector.get('$state');
|
||||
state.go('404');
|
||||
return $location.path();
|
||||
});
|
||||
|
||||
// enable HTML5 Mode for SEO
|
||||
$locationProvider.html5Mode(true);
|
||||
|
||||
});
|
||||
0
app/assets/javascripts/app/services/.keep
Normal file
0
app/assets/javascripts/app/services/.keep
Normal file
528
app/assets/javascripts/app/services/apiController.js
Normal file
528
app/assets/javascripts/app/services/apiController.js
Normal file
@@ -0,0 +1,528 @@
|
||||
angular.module('app.services')
|
||||
.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 = location.protocol + "//" + domainName() + (location.port ? ':' + location.port: '');
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
|
||||
this.$get = function(Restangular) {
|
||||
return new ApiController(Restangular);
|
||||
}
|
||||
|
||||
function ApiController(Restangular) {
|
||||
|
||||
/*
|
||||
Config
|
||||
*/
|
||||
|
||||
this.getServer = function() {
|
||||
if(!url) {
|
||||
url = localStorage.getItem("server");
|
||||
if(!url) {
|
||||
url = location.protocol + "//" + domainName() + (location.port ? ':' + location.port: '');
|
||||
this.setServer(url);
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
this.setServer = function(url, refresh) {
|
||||
localStorage.setItem("server", url);
|
||||
if(refresh) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
User
|
||||
*/
|
||||
|
||||
this.setUsername = function(user, username, callback) {
|
||||
var request = Restangular.one("users", user.id).one("set_username");
|
||||
request.username = username;
|
||||
request.post().then(function(response){
|
||||
callback(response.plain());
|
||||
})
|
||||
}
|
||||
|
||||
this.enableEncryptionForUser = function(user, callback) {
|
||||
Restangular.one("users", user.id).one('enable_encryption').post().then(function(response){
|
||||
|
||||
var enabled = response.plain().local_encryption_enabled;
|
||||
if(!enabled) {
|
||||
callback(false, enabled);
|
||||
return;
|
||||
}
|
||||
this.handleEncryptionStatusChange(user, enabled, callback);
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
this.disableEncryptionForUser = function(user, callback) {
|
||||
|
||||
Restangular.one("users", user.id).one('disable_encryption').post().then(function(response){
|
||||
var enabled = response.plain().local_encryption_enabled;
|
||||
|
||||
if(enabled) {
|
||||
// something went wrong
|
||||
callback(false, enabled);
|
||||
return;
|
||||
}
|
||||
this.handleEncryptionStatusChange(user, enabled, callback);
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
this.handleEncryptionStatusChange = function(user, encryptionEnabled, callback) {
|
||||
var allNotes = user.filteredNotes();
|
||||
if(encryptionEnabled) {
|
||||
allNotes = allNotes.filter(function(note){return note.isPublic() == false});
|
||||
this.encryptNotes(allNotes, this.retrieveGk());
|
||||
} else {
|
||||
this.decryptNotes(allNotes, this.retrieveGk());
|
||||
}
|
||||
|
||||
this.saveBatchNotes(user, allNotes, encryptionEnabled, function(success) {
|
||||
callback(success, encryptionEnabled);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
/*
|
||||
Ensures that if encryption is disabled, all local notes are uncrypted,
|
||||
and that if it's enabled, that all local notes are encrypted
|
||||
*/
|
||||
this.verifyEncryptionStatusOfAllNotes = function(user, callback) {
|
||||
var allNotes = user.filteredNotes();
|
||||
var notesNeedingUpdate = [];
|
||||
var key = this.retrieveGk();
|
||||
allNotes.forEach(function(note){
|
||||
if(user.local_encryption_enabled && !note.isPublic()) {
|
||||
if(!note.isEncrypted()) {
|
||||
// needs encryption
|
||||
this.encryptSingleNote(note, key);
|
||||
notesNeedingUpdate.push(note);
|
||||
}
|
||||
} else {
|
||||
if(note.isEncrypted()) {
|
||||
// needs decrypting
|
||||
this.decryptSingleNote(note, key);
|
||||
notesNeedingUpdate.push(note);
|
||||
}
|
||||
}
|
||||
}.bind(this))
|
||||
|
||||
if(notesNeedingUpdate.length > 0) {
|
||||
this.saveBatchNotes(user, notesNeedingUpdate, user.local_encryption_enabled, callback)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Groups
|
||||
*/
|
||||
|
||||
this.restangularizeGroup = function(group, user) {
|
||||
var request = Restangular.one("users", user.id).one("groups", group.id);
|
||||
_.merge(request, group);
|
||||
return request;
|
||||
}
|
||||
|
||||
this.saveGroup = function(user, group, callback) {
|
||||
if(user.id) {
|
||||
if(!group.route) {
|
||||
group = this.restangularizeGroup(group, user);
|
||||
}
|
||||
group.customOperation(group.id ? "put" : "post").then(function(response) {
|
||||
callback(response.plain());
|
||||
})
|
||||
} else {
|
||||
this.writeUserToLocalStorage(user);
|
||||
callback(group);
|
||||
}
|
||||
}
|
||||
|
||||
this.deleteGroup = function(user, group, callback) {
|
||||
if(!user.id) {
|
||||
_.remove(user.groups, group);
|
||||
this.writeUserToLocalStorage(user);
|
||||
callback(true);
|
||||
} else {
|
||||
Restangular.one("users", user.id).one("groups", group.id).remove()
|
||||
.then(function(response) {
|
||||
_.remove(user.groups, group);
|
||||
callback(true);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.shareGroup = function(user, group, callback) {
|
||||
var shareFn = function() {
|
||||
Restangular.one("users", user.id).one("groups", group.id).one("share").post()
|
||||
.then(function(response){
|
||||
var obj = response.plain();
|
||||
group.notes.forEach(function(note){
|
||||
note.shared_via_group = true;
|
||||
});
|
||||
_.merge(group, {presentation: obj.presentation});
|
||||
callback(obj);
|
||||
})
|
||||
}
|
||||
|
||||
if(user.local_encryption_enabled && group.notes.length > 0) {
|
||||
// decrypt group notes first
|
||||
var notes = group.notes;
|
||||
this.decryptNotesWithLocalKey(notes);
|
||||
this.saveBatchNotes(user, notes, false, function(success){
|
||||
shareFn();
|
||||
})
|
||||
} else {
|
||||
shareFn();
|
||||
}
|
||||
}
|
||||
|
||||
this.unshareGroup = function(user, group, callback) {
|
||||
Restangular.one("users", user.id).one("groups", group.id).one("unshare").post()
|
||||
.then(function(response){
|
||||
var obj = response.plain();
|
||||
_.merge(group, {presentation: obj.presentation});
|
||||
callback(obj);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Notes
|
||||
*/
|
||||
|
||||
this.saveBatchNotes = function(user, notes, encryptionEnabled, callback) {
|
||||
notes = _.cloneDeep(notes);
|
||||
notes.forEach(function(note){
|
||||
if(encryptionEnabled && !note.isPublic()) {
|
||||
note.content = null;
|
||||
note.name = null;
|
||||
} else {
|
||||
note.local_encrypted_content = null;
|
||||
note.local_eek = null;
|
||||
}
|
||||
})
|
||||
|
||||
var request = Restangular.one("users", user.id).one("notes/batch_update");
|
||||
request.notes = notes;
|
||||
request.put().then(function(response){
|
||||
var success = response.plain().success;
|
||||
callback(success);
|
||||
})
|
||||
}
|
||||
|
||||
this.preprocessNoteForSaving = function(note, user) {
|
||||
if(user.local_encryption_enabled && !note.pending_share && !note.isPublic()) {
|
||||
// encrypt
|
||||
this.encryptSingleNote(note, this.retrieveGk());
|
||||
note.content = null; // dont send unencrypted content to server
|
||||
note.name = null;
|
||||
}
|
||||
else {
|
||||
// decrypt
|
||||
note.local_encrypted_content = null;
|
||||
note.local_eek = null;
|
||||
note.local_encryption_scheme = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.saveNote = function(user, note, callback) {
|
||||
if(!user.id) {
|
||||
this.writeUserToLocalStorage(user);
|
||||
callback(note);
|
||||
return;
|
||||
}
|
||||
|
||||
var snipCopy = _.cloneDeep(note);
|
||||
|
||||
this.preprocessNoteForSaving(snipCopy, user);
|
||||
|
||||
var request = Restangular.one("users", user.id).one("notes", note.id);
|
||||
_.merge(request, snipCopy);
|
||||
|
||||
request.customOperation(request.id ? "put" : "post")
|
||||
.then(function(response) {
|
||||
var responseObject = response.plain();
|
||||
responseObject.content = note.content;
|
||||
responseObject.name = note.name;
|
||||
_.merge(note, responseObject);
|
||||
callback(note);
|
||||
})
|
||||
.catch(function(response){
|
||||
callback(null);
|
||||
})
|
||||
}
|
||||
|
||||
this.deleteNote = function(user, note, callback) {
|
||||
if(!user.id) {
|
||||
this.writeUserToLocalStorage(user);
|
||||
callback(true);
|
||||
} else {
|
||||
Restangular.one("users", user.id).one("notes", note.id).remove()
|
||||
.then(function(response) {
|
||||
callback(true);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.shareNote = function(user, note, callback) {
|
||||
if(!user.id) {
|
||||
if(confirm("Note: You are not signed in. Any note you share cannot be edited or unshared.")) {
|
||||
var request = Restangular.one("notes").one("share");
|
||||
_.merge(request, {name: note.name, content: note.content});
|
||||
request.post().then(function(response){
|
||||
var obj = response.plain();
|
||||
_.merge(note, {presentation: obj.presentation});
|
||||
note.locked = true;
|
||||
this.writeUserToLocalStorage(user);
|
||||
callback(note);
|
||||
}.bind(this))
|
||||
}
|
||||
} else {
|
||||
var shareFn = function(note, callback) {
|
||||
Restangular.one("users", user.id).one("notes", note.id).one("share").post()
|
||||
.then(function(response){
|
||||
var obj = response.plain();
|
||||
_.merge(note, {presentation: obj.presentation});
|
||||
callback(note);
|
||||
})
|
||||
}
|
||||
|
||||
if(user.local_encryption_enabled) {
|
||||
if(confirm("Note: Sharing this note will remove its local encryption.")) {
|
||||
note.pending_share = true;
|
||||
this.saveNote(user, note, function(saved_note){
|
||||
shareFn(saved_note, callback);
|
||||
})
|
||||
}
|
||||
} else {
|
||||
shareFn(note, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.unshareNote = function(user, note, callback) {
|
||||
Restangular.one("users", user.id).one("notes", note.id).one("unshare").post()
|
||||
.then(function(response){
|
||||
var obj = response.plain();
|
||||
_.merge(note, {presentation: obj.presentation});
|
||||
callback(note);
|
||||
})
|
||||
}
|
||||
|
||||
this.notesDataFile = function(user) {
|
||||
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);
|
||||
|
||||
// remove irrelevant keys
|
||||
var notes = _.map(user.filteredNotes(), function(note){
|
||||
return {
|
||||
id: note.id,
|
||||
name: note.name,
|
||||
content: note.content,
|
||||
created_at: note.created_at,
|
||||
modified_at: note.modified_at,
|
||||
group_id: note.group_id
|
||||
}
|
||||
});
|
||||
return makeTextFile(JSON.stringify(notes, null, 2 /* pretty print */));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Merging
|
||||
*/
|
||||
this.mergeLocalDataRemotely = function(user, callback) {
|
||||
var request = Restangular.one("users", user.id).one("merge");
|
||||
var groups = user.groups;
|
||||
request.notes = user.notes;
|
||||
request.notes.forEach(function(note){
|
||||
if(note.group_id) {
|
||||
var group = groups.filter(function(group){return group.id == note.group_id})[0];
|
||||
note.group_name = group.name;
|
||||
}
|
||||
})
|
||||
request.post().then(function(response){
|
||||
callback();
|
||||
localStorage.removeItem('user');
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
this.filterDummyNotes = function(notes) {
|
||||
var filtered = notes.filter(function(note){return note.dummy == false || note.dummy == null});
|
||||
return filtered;
|
||||
}
|
||||
|
||||
this.staticifyObject = function(object) {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
}
|
||||
|
||||
this.writeUserToLocalStorage = function(user) {
|
||||
var saveUser = _.cloneDeep(user);
|
||||
saveUser.notes = this.filterDummyNotes(saveUser.notes);
|
||||
saveUser.groups.forEach(function(group){
|
||||
group.notes = null;
|
||||
}.bind(this))
|
||||
this.writeToLocalStorage('user', saveUser);
|
||||
}
|
||||
|
||||
this.writeToLocalStorage = function(key, value) {
|
||||
localStorage.setItem(key, angular.toJson(value));
|
||||
}
|
||||
|
||||
this.localUser = function() {
|
||||
var user = JSON.parse(localStorage.getItem('user'));
|
||||
if(!user) {
|
||||
user = {notes: [], groups: []};
|
||||
}
|
||||
user.shouldMerge = true;
|
||||
return user;
|
||||
}
|
||||
|
||||
/*
|
||||
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;
|
||||
}
|
||||
return new Note(JSON.parse(draftString));
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Encrpytion
|
||||
*/
|
||||
|
||||
this.retrieveGk = function() {
|
||||
if(!this.gk) {
|
||||
this.gk = localStorage.getItem("gk");
|
||||
}
|
||||
return this.gk;
|
||||
}
|
||||
|
||||
this.setGk = function(gk) {
|
||||
localStorage.setItem('gk', gk);
|
||||
}
|
||||
|
||||
this.clearGk = function() {
|
||||
localStorage.removeItem("gk");
|
||||
}
|
||||
|
||||
this.encryptSingleNote = function(note, key) {
|
||||
var ek = null;
|
||||
if(note.isEncrypted()) {
|
||||
ek = Neeto.crypto.decryptText(note.local_eek, key);
|
||||
} else {
|
||||
ek = Neeto.crypto.generateRandomEncryptionKey();
|
||||
note.local_eek = Neeto.crypto.encryptText(ek, key);
|
||||
}
|
||||
var text = JSON.stringify({name: note.name, content: note.content});
|
||||
note.local_encrypted_content = Neeto.crypto.encryptText(text, ek);
|
||||
note.local_encryption_scheme = "1.0";
|
||||
}
|
||||
|
||||
this.encryptNotes = function(notes, key) {
|
||||
notes.forEach(function(note){
|
||||
this.encryptSingleNote(note, key);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.encryptSingleNoteWithLocalKey = function(note) {
|
||||
this.encryptSingleNote(note, this.retrieveGk());
|
||||
}
|
||||
|
||||
this.encryptNotesWithLocalKey = function(notes) {
|
||||
this.encryptNotes(notes, this.retrieveGk());
|
||||
}
|
||||
|
||||
this.decryptSingleNoteWithLocalKey = function(note) {
|
||||
this.decryptSingleNote(note, this.retrieveGk());
|
||||
}
|
||||
|
||||
this.decryptSingleNote = function(note, key) {
|
||||
var ek = Neeto.crypto.decryptText(note.local_eek, key);
|
||||
var obj = JSON.parse(Neeto.crypto.decryptText(note.local_encrypted_content, ek));
|
||||
note.name = obj.name;
|
||||
note.content = obj.content;
|
||||
}
|
||||
|
||||
this.decryptNotes = function(notes, key) {
|
||||
notes.forEach(function(note){
|
||||
if(note.isEncrypted()) {
|
||||
this.decryptSingleNote(note, key);
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.decryptNotesWithLocalKey = function(notes) {
|
||||
this.decryptNotes(notes, this.retrieveGk());
|
||||
}
|
||||
|
||||
this.reencryptAllNotesAndSave = function(user, newKey, oldKey, callback) {
|
||||
var notes = user.filteredNotes();
|
||||
notes.forEach(function(note){
|
||||
if(note.isEncrypted()) {
|
||||
// first decrypt eek with old key
|
||||
var ek = Neeto.crypto.decryptText(note.local_eek, oldKey);
|
||||
// now encrypt ek with new key
|
||||
note.local_eek = Neeto.crypto.encryptText(ek, newKey);
|
||||
}
|
||||
});
|
||||
|
||||
this.saveBatchNotes(user, notes, true, function(success) {
|
||||
callback(success);
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
});
|
||||
17
app/assets/javascripts/app/services/directives/autofocus.js
Normal file
17
app/assets/javascripts/app/services/directives/autofocus.js
Normal file
@@ -0,0 +1,17 @@
|
||||
angular
|
||||
.module('app.services')
|
||||
.directive('mbAutofocus', ['$timeout', function($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
shouldFocus: "="
|
||||
},
|
||||
link : function($scope, $element) {
|
||||
$timeout(function() {
|
||||
if($scope.shouldFocus) {
|
||||
$element[0].focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}]);
|
||||
109
app/assets/javascripts/app/services/directives/draggable.js
Normal file
109
app/assets/javascripts/app/services/directives/draggable.js
Normal file
@@ -0,0 +1,109 @@
|
||||
angular
|
||||
.module('app.services')
|
||||
.directive('draggable', function() {
|
||||
return {
|
||||
scope: {
|
||||
note: "="
|
||||
},
|
||||
link: function(scope, element) {
|
||||
// this gives us the native JS object
|
||||
var el = element[0];
|
||||
|
||||
el.draggable = true;
|
||||
|
||||
el.addEventListener(
|
||||
'dragstart',
|
||||
function(e) {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('Note', JSON.stringify(scope.note));
|
||||
this.classList.add('drag');
|
||||
return false;
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
el.addEventListener(
|
||||
'dragend',
|
||||
function(e) {
|
||||
this.classList.remove('drag');
|
||||
return false;
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
angular
|
||||
.module('app.services')
|
||||
.directive('droppable', function() {
|
||||
return {
|
||||
scope: {
|
||||
drop: '&',
|
||||
bin: '=',
|
||||
group: "="
|
||||
},
|
||||
link: function(scope, element) {
|
||||
// again we need the native object
|
||||
var el = element[0];
|
||||
|
||||
el.addEventListener(
|
||||
'dragover',
|
||||
function(e) {
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
// allows us to drop
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
this.classList.add('over');
|
||||
return false;
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
var counter = 0;
|
||||
|
||||
el.addEventListener(
|
||||
'dragenter',
|
||||
function(e) {
|
||||
counter++;
|
||||
this.classList.add('over');
|
||||
return false;
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
el.addEventListener(
|
||||
'dragleave',
|
||||
function(e) {
|
||||
counter--;
|
||||
if (counter === 0) {
|
||||
this.classList.remove('over');
|
||||
}
|
||||
return false;
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
el.addEventListener(
|
||||
'drop',
|
||||
function(e) {
|
||||
// Stops some browsers from redirecting.
|
||||
if (e.stopPropagation) e.stopPropagation();
|
||||
|
||||
this.classList.remove('over');
|
||||
|
||||
var binId = this.id;
|
||||
var note = new Note(JSON.parse(e.dataTransfer.getData('Note')));
|
||||
scope.$apply(function(scope) {
|
||||
var fn = scope.drop();
|
||||
if ('undefined' !== typeof fn) {
|
||||
fn(e, scope.group, note);
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
20
app/assets/javascripts/app/services/directives/lowercase.js
Normal file
20
app/assets/javascripts/app/services/directives/lowercase.js
Normal file
@@ -0,0 +1,20 @@
|
||||
angular
|
||||
.module('app.services')
|
||||
.directive('lowercase', function() {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, element, attrs, modelCtrl) {
|
||||
var lowercase = function(inputValue) {
|
||||
if (inputValue == undefined) inputValue = '';
|
||||
var lowercased = inputValue.toLowerCase();
|
||||
if (lowercased !== inputValue) {
|
||||
modelCtrl.$setViewValue(lowercased);
|
||||
modelCtrl.$render();
|
||||
}
|
||||
return lowercased;
|
||||
}
|
||||
modelCtrl.$parsers.push(lowercase);
|
||||
lowercase(scope[attrs.ngModel]);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
angular
|
||||
.module('app.services')
|
||||
.directive('selectOnClick', ['$window', function ($window) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attrs) {
|
||||
element.on('focus', function () {
|
||||
if (!$window.getSelection().toString()) {
|
||||
// Required for mobile Safari
|
||||
this.setSelectionRange(0, this.value.length)
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
17
app/assets/javascripts/app/services/directives/snippet.js
Normal file
17
app/assets/javascripts/app/services/directives/snippet.js
Normal file
@@ -0,0 +1,17 @@
|
||||
angular
|
||||
.module('app.services')
|
||||
.directive('note', function($timeout) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
controller: 'SingleNoteCtrl',
|
||||
templateUrl: "frontend/directives/note.html",
|
||||
scope: {
|
||||
note: "="
|
||||
},
|
||||
}
|
||||
})
|
||||
.controller('SingleNoteCtrl', function ($rootScope, $scope, $state, markdownRenderer) {
|
||||
$scope.renderedContent = function() {
|
||||
return markdownRenderer.renderHtml(markdownRenderer.renderedContentForText($scope.note.content));
|
||||
}
|
||||
});
|
||||
192
app/assets/javascripts/app/services/directives/typewrite.js
Normal file
192
app/assets/javascripts/app/services/directives/typewrite.js
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* AngularJS directive that simulates the effect of typing on a text editor - with a blinking cursor.
|
||||
* This directive works as an attribute to any HTML element, and it changes the speed/delay of its animation.
|
||||
*
|
||||
* There's also a simple less file included for basic styling of the dialog, which can be overridden.
|
||||
* The config object also lets the user define custom CSS classes for the modal.
|
||||
*
|
||||
* How to use:
|
||||
*
|
||||
* Just add the desired text to the 'text' attribute of the element and the directive takes care of the rest.
|
||||
* The 'text' attribute can be a single string or an array of string. In case an array is passed, the string
|
||||
* on each index is erased so the next item can be printed. When the last index is reached, that string stays
|
||||
* on the screen. (So if you want to erase the last string, just push an empty string to the end of the array)
|
||||
*
|
||||
* These are the optional preferences:
|
||||
* - initial delay: set an 'initial-delay' attribute for the element
|
||||
* - type delay: set a 'type-delay' attribute for the element
|
||||
* - erase delay: set a 'erase-delay' attribute for the element
|
||||
* - specify cursor : set a 'cursor' attribute for the element, specifying which cursor to use
|
||||
* - turn off cursor blinking: set the 'blink-cursor' attribute to "false"
|
||||
* - cursor blinking speed: set a 'blink-delay' attribute for the element
|
||||
* - scope callback: pass the desired scope callback as the 'callback-fn' attribute of the element
|
||||
*
|
||||
* Note:
|
||||
* Each time/delay value should be set either on seconds (1s) or milliseconds (1000)
|
||||
*
|
||||
* Dependencies:
|
||||
* The directive needs the css file provided in order to replicate the cursor blinking effect.
|
||||
*/
|
||||
|
||||
|
||||
angular
|
||||
.module('app.services').directive('typewrite', ['$timeout', function ($timeout) {
|
||||
function linkFunction($scope, $element, $attrs) {
|
||||
var timer = null,
|
||||
initialDelay = $attrs.initialDelay ? getTypeDelay($attrs.initialDelay) : 200,
|
||||
typeDelay = $attrs.typeDelay || 200,
|
||||
eraseDelay = $attrs.eraseDelay || typeDelay / 2,
|
||||
blinkDelay = $attrs.blinkDelay ? getAnimationDelay($attrs.blinkDelay) : false,
|
||||
cursor = $attrs.cursor || '|',
|
||||
blinkCursor = typeof $attrs.blinkCursor !== 'undefined' ? $attrs.blinkCursor === 'true' : true,
|
||||
currentText,
|
||||
textArray,
|
||||
running,
|
||||
auxStyle;
|
||||
|
||||
|
||||
|
||||
if ($scope.text) {
|
||||
if ($scope.text instanceof Array) {
|
||||
textArray = $scope.text;
|
||||
currentText = textArray[0];
|
||||
} else {
|
||||
currentText = $scope.text;
|
||||
}
|
||||
}
|
||||
if (typeof $scope.start === 'undefined' || $scope.start) {
|
||||
typewrite();
|
||||
}
|
||||
|
||||
function typewrite() {
|
||||
timer = $timeout(function () {
|
||||
updateIt($element, 0, 0, currentText);
|
||||
}, initialDelay);
|
||||
}
|
||||
|
||||
function updateIt(element, charIndex, arrIndex, text) {
|
||||
if (charIndex <= text.length) {
|
||||
updateValue(element, text.substring(0, charIndex) + cursor);
|
||||
charIndex++;
|
||||
timer = $timeout(function () {
|
||||
updateIt(element, charIndex, arrIndex, text);
|
||||
}, typeDelay);
|
||||
return;
|
||||
} else {
|
||||
charIndex--;
|
||||
|
||||
if($scope.iterationCallback) {
|
||||
$scope.iterationCallback()(arrIndex);
|
||||
}
|
||||
|
||||
// check if it's an array
|
||||
if (textArray && arrIndex < textArray.length - 1) {
|
||||
timer = $timeout(function () {
|
||||
cleanAndRestart(element, charIndex, arrIndex, textArray[arrIndex]);
|
||||
}, $scope.iterationDelay);
|
||||
} else {
|
||||
if ($scope.callbackFn) {
|
||||
$scope.callbackFn();
|
||||
}
|
||||
blinkIt(element, charIndex, currentText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function blinkIt(element, charIndex) {
|
||||
var text = element.text().substring(0, element.text().length - 1);
|
||||
if (blinkCursor) {
|
||||
if (blinkDelay) {
|
||||
auxStyle = '-webkit-animation:blink-it steps(1) ' + blinkDelay + ' infinite;-moz-animation:blink-it steps(1) ' + blinkDelay + ' infinite ' +
|
||||
'-ms-animation:blink-it steps(1) ' + blinkDelay + ' infinite;-o-animation:blink-it steps(1) ' + blinkDelay + ' infinite; ' +
|
||||
'animation:blink-it steps(1) ' + blinkDelay + ' infinite;';
|
||||
updateValue(element, text.substring(0, charIndex) + '<span class="blink" style="' + auxStyle + '">' + cursor + '</span>');
|
||||
} else {
|
||||
updateValue(element, text.substring(0, charIndex) + '<span class="blink">' + cursor + '</span>');
|
||||
}
|
||||
} else {
|
||||
updateValue(element, text.substring(0, charIndex));
|
||||
}
|
||||
}
|
||||
|
||||
function cleanAndRestart(element, charIndex, arrIndex, currentText) {
|
||||
if(charIndex == 0) {
|
||||
if($scope.prebeginFn) {
|
||||
$scope.prebeginFn()();
|
||||
}
|
||||
}
|
||||
if (charIndex > 0) {
|
||||
currentText = currentText.slice(0, -1);
|
||||
// element.html(currentText.substring(0, currentText.length - 1) + cursor);
|
||||
updateValue(element, currentText + cursor);
|
||||
charIndex--;
|
||||
timer = $timeout(function () {
|
||||
cleanAndRestart(element, charIndex, arrIndex, currentText);
|
||||
}, eraseDelay);
|
||||
return;
|
||||
} else {
|
||||
arrIndex++;
|
||||
currentText = textArray[arrIndex];
|
||||
timer = $timeout(function () {
|
||||
updateIt(element, 0, arrIndex, currentText);
|
||||
}, typeDelay);
|
||||
}
|
||||
}
|
||||
|
||||
function getTypeDelay(delay) {
|
||||
if (typeof delay === 'string') {
|
||||
return delay.charAt(delay.length - 1) === 's' ? parseInt(delay.substring(0, delay.length - 1), 10) * 1000 : +delay;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getAnimationDelay(delay) {
|
||||
if (typeof delay === 'string') {
|
||||
return delay.charAt(delay.length - 1) === 's' ? delay : parseInt(delay.substring(0, delay.length - 1), 10) / 1000;
|
||||
}
|
||||
}
|
||||
|
||||
function updateValue(element, value) {
|
||||
if (element.prop('nodeName').toUpperCase() === 'INPUT') {
|
||||
return element.val(value);
|
||||
}
|
||||
return element.html(value);
|
||||
}
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
if (timer) {
|
||||
$timeout.cancel(timer);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('start', function (newVal) {
|
||||
if (!running && newVal) {
|
||||
running = !running;
|
||||
typewrite();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('text', function (newVal, oldVal) {
|
||||
if(!(newVal instanceof Array)) {
|
||||
currentText = newVal;
|
||||
typewrite();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: linkFunction,
|
||||
replace: true,
|
||||
scope: {
|
||||
text: '=',
|
||||
callbackFn: '&',
|
||||
iterationCallback: '&',
|
||||
iterationDelay: '=',
|
||||
prebeginFn: '&',
|
||||
start: '='
|
||||
}
|
||||
};
|
||||
|
||||
}]);
|
||||
48
app/assets/javascripts/app/services/helpers/crypto.js
Normal file
48
app/assets/javascripts/app/services/helpers/crypto.js
Normal file
@@ -0,0 +1,48 @@
|
||||
var Neeto = Neeto || {};
|
||||
|
||||
Neeto.crypto = {
|
||||
|
||||
generateRandomKey: function() {
|
||||
return CryptoJS.lib.WordArray.random(256/8).toString();
|
||||
},
|
||||
|
||||
decryptText: function(encrypted_content, key) {
|
||||
return CryptoJS.AES.decrypt(encrypted_content, key).toString(CryptoJS.enc.Utf8);
|
||||
},
|
||||
|
||||
encryptText: function(text, key) {
|
||||
return CryptoJS.AES.encrypt(text, key).toString();
|
||||
},
|
||||
|
||||
generateRandomEncryptionKey: function() {
|
||||
var salt = Neeto.crypto.generateRandomKey();
|
||||
var passphrase = Neeto.crypto.generateRandomKey();
|
||||
return CryptoJS.PBKDF2(passphrase, salt, { keySize: 256/32 }).toString();
|
||||
},
|
||||
|
||||
sha256: function(text) {
|
||||
return CryptoJS.SHA256(text).toString();
|
||||
},
|
||||
|
||||
/** Generates two deterministic 256 bit keys based on one input */
|
||||
generateAsymmetricKeyPair: function(input, salt) {
|
||||
var output = CryptoJS.PBKDF2(input, salt, { keySize: 512/32, hasher: CryptoJS.algo.SHA512, iterations: 3000 });
|
||||
var firstHalf = _.clone(output);
|
||||
var secondHalf = _.clone(output);
|
||||
var sigBytes = output.sigBytes/2;
|
||||
var outputLength = output.words.length;
|
||||
firstHalf.words = output.words.slice(0, outputLength/2);
|
||||
secondHalf.words = output.words.slice(outputLength/2, outputLength);
|
||||
firstHalf.sigBytes = sigBytes;
|
||||
secondHalf.sigBytes = sigBytes;
|
||||
return [firstHalf.toString(), secondHalf.toString()];
|
||||
},
|
||||
|
||||
generateEncryptionKeysForUser: function(password, email) {
|
||||
var keys = Neeto.crypto.generateAsymmetricKeyPair(password, email);
|
||||
var pw = keys[0];
|
||||
var gk = keys[1];
|
||||
|
||||
return {pw: pw, gk: gk};
|
||||
}
|
||||
};
|
||||
21
app/assets/javascripts/app/services/markdownRenderer.js
Normal file
21
app/assets/javascripts/app/services/markdownRenderer.js
Normal file
@@ -0,0 +1,21 @@
|
||||
angular.module('app.services')
|
||||
.service('markdownRenderer', function ($sce) {
|
||||
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
sanitize: true
|
||||
});
|
||||
|
||||
this.renderedContentForText = function(text) {
|
||||
if(!text || text.length == 0) {
|
||||
return "";
|
||||
}
|
||||
return marked(text);
|
||||
}
|
||||
|
||||
this.renderHtml = function(html_code) {
|
||||
return $sce.trustAsHtml(html_code);
|
||||
};
|
||||
|
||||
|
||||
});
|
||||
20
app/assets/javascripts/app/services/serverSideValidation.js
Normal file
20
app/assets/javascripts/app/services/serverSideValidation.js
Normal file
@@ -0,0 +1,20 @@
|
||||
angular.module('app.services')
|
||||
.service('serverSideValidation', function ($sce) {
|
||||
// Show validation errors in form.
|
||||
this.showErrors = function (formErrors, form) {
|
||||
angular.forEach(formErrors, function (errors, key) {
|
||||
if (typeof form[key] !== 'undefined') {
|
||||
form[key].$setDirty();
|
||||
form[key].$setValidity('server', false);
|
||||
form[key].$error.server = $sce.trustAsHtml(errors.join(', '));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Get validation errors from server response and show them in form.
|
||||
this.parseErrors = function (response, form) {
|
||||
if (response.status === 422) {
|
||||
this.showErrors(response.data, form);
|
||||
}
|
||||
};
|
||||
});
|
||||
5
app/assets/javascripts/frontend.js
Normal file
5
app/assets/javascripts/frontend.js
Normal file
@@ -0,0 +1,5 @@
|
||||
//= require app/app.services.js
|
||||
//= require_tree ./app/services
|
||||
|
||||
//= require app/app.frontend.js
|
||||
//= require_tree ./app/frontend
|
||||
Reference in New Issue
Block a user