initial commit
This commit is contained in:
0
app/assets/images/.keep
Normal file
0
app/assets/images/.keep
Normal file
BIN
app/assets/images/archive.png
Normal file
BIN
app/assets/images/archive.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/assets/images/encryption.png
Normal file
BIN
app/assets/images/encryption.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
app/assets/images/filter.png
Normal file
BIN
app/assets/images/filter.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
app/assets/images/logo.png
Normal file
BIN
app/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
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
|
||||
142
app/assets/stylesheets/app/_common.scss
Normal file
142
app/assets/stylesheets/app/_common.scss
Normal file
@@ -0,0 +1,142 @@
|
||||
.nt-dropdown-menu {
|
||||
border-radius: 0;
|
||||
padding: 0 0;
|
||||
margin-top: 15px;
|
||||
border: none;
|
||||
width: 280px;
|
||||
|
||||
li {
|
||||
height: 35px;
|
||||
overflow: hidden;
|
||||
// padding-top: 2px;
|
||||
|
||||
.text {
|
||||
padding: 10px;
|
||||
padding-top: 7px;
|
||||
height: 100%;
|
||||
height: 100%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
opacity: 0.5;
|
||||
margin-top: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nt-dropdown-menu.dark {
|
||||
background-color: $dark-gray;
|
||||
color: white;
|
||||
|
||||
li {
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: black;
|
||||
a {
|
||||
color: black !important;
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: white !important;
|
||||
height: 100%;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.text {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nt-dropdown-menu.light {
|
||||
background-color: white;
|
||||
color: black;
|
||||
|
||||
li {
|
||||
&:hover {
|
||||
background-color: $dark-gray;
|
||||
color: white;
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: black;
|
||||
}
|
||||
.text {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.menu-right-container {
|
||||
float: right;
|
||||
margin-top: 3px;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
width: 70%;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
|
||||
a {
|
||||
color: white;
|
||||
// font-size: 12px;
|
||||
}
|
||||
|
||||
.public-link {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
height: 20px;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
.url {
|
||||
text-align: right;
|
||||
|
||||
.icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 90%;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.edit-url {
|
||||
// float: left;
|
||||
border: none;
|
||||
outline: none;
|
||||
margin-top: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
> .icon {
|
||||
float: left;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
min-width: 255px;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
margin-right: 18px;
|
||||
margin-bottom: 18px;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
239
app/assets/stylesheets/app/_editor.scss
Normal file
239
app/assets/stylesheets/app/_editor.scss
Normal file
@@ -0,0 +1,239 @@
|
||||
.editor {
|
||||
width: 50%;
|
||||
|
||||
&.fullscreen {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
z-index: 200;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.section-title-bar {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
$heading-height: 100px;
|
||||
|
||||
.editor-heading {
|
||||
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
padding-top: 0px;
|
||||
background-color: white;
|
||||
|
||||
min-height: 100px;
|
||||
|
||||
width: 100%;
|
||||
padding-right: 10px;
|
||||
|
||||
> .title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
padding-top: 0px;
|
||||
width: 100%;
|
||||
|
||||
> .input {
|
||||
float: left;
|
||||
text-overflow:ellipsis;
|
||||
width: 90%;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
|
||||
&:disabled {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.save-status {
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
text-transform: none;
|
||||
font-weight: normal;
|
||||
margin-top: -18px;
|
||||
width: 120px;
|
||||
text-align: right;
|
||||
color: rgba(black, 0.5);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
max-height: 100%;
|
||||
|
||||
height: 100%;
|
||||
clear: both;
|
||||
min-width: 0;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
overflow: auto;
|
||||
|
||||
padding-top: $heading-height;
|
||||
|
||||
.sampler-container {
|
||||
margin-top: 10px;
|
||||
padding: 15px;
|
||||
padding-top: 17px;
|
||||
font-size: 17px;
|
||||
// opacity: 0.5;
|
||||
}
|
||||
|
||||
.sampler {
|
||||
// opacity: 0.5;
|
||||
color: rgba(black, 0.3);
|
||||
}
|
||||
|
||||
.editable {
|
||||
font-family: monospace;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 15px;
|
||||
padding-top: 17px;
|
||||
font-size: 17px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.preview {
|
||||
// font-family: monospace;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
line-height: 23px;
|
||||
overflow-y: scroll;
|
||||
padding: 0px 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markdown {
|
||||
margin-top: 6px;
|
||||
margin-left: 15px;
|
||||
float: right;
|
||||
text-align: right;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.full-screen-button {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
right: 20px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.editor-menu {
|
||||
padding-top: 0px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
padding-left: inherit;
|
||||
padding-right: inherit;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0px;
|
||||
background-color: $dark-gray;
|
||||
color: white;
|
||||
padding-top: 8px;
|
||||
height: 36px;
|
||||
cursor: default;
|
||||
|
||||
ul {
|
||||
li {
|
||||
text-align: left;
|
||||
a {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
padding: 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
list-style-position: inside;
|
||||
-webkit-margin-before: 1em;
|
||||
-webkit-margin-after: 1em;
|
||||
-webkit-margin-start: 0px;
|
||||
-webkit-margin-end: 0px;
|
||||
-webkit-padding-start: 0px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.nav-tabs {
|
||||
a {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
.nav {
|
||||
padding-left: 0;
|
||||
margin-bottom: 0;
|
||||
list-style: none;
|
||||
}
|
||||
ol, ul {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.nav-pills>li {
|
||||
float: left;
|
||||
}
|
||||
.nav>li {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover {
|
||||
color: #fff;
|
||||
background-color: #337ab7;
|
||||
}
|
||||
.nav-pills>li>a {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.nav>li>a {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.nav-tabs>li {
|
||||
float: left;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
.nav>li {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-tabs>li.active>a, .nav-tabs>li.active>a:focus, .nav-tabs>li.active>a:hover {
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
.nav-tabs>li>a {
|
||||
margin-right: 2px;
|
||||
line-height: 1.42857143;
|
||||
border: 1px solid transparent;
|
||||
// border-radius: 4px 4px 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.nav>li>a {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
92
app/assets/stylesheets/app/_fonts.scss
Normal file
92
app/assets/stylesheets/app/_fonts.scss
Normal file
@@ -0,0 +1,92 @@
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
src: font-url('ProximaNova/ProximaNova-Regular.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Regular.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Regular.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Regular.svg') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
font-style: italic;
|
||||
src: font-url('ProximaNova/ProximaNova-Regular-Italic.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Regular-Italic.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Regular-Italic.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Regular-Italic.svg') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
font-weight: 600;
|
||||
src: font-url('ProximaNova/ProximaNova-Semibold.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Semibold.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Semibold.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Semibold.svg') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
src: font-url('ProximaNova/ProximaNova-Semibold-Italic.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Semibold-Italic.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Semibold-Italic.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Semibold-Italic.svg') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
font-weight: bold;
|
||||
src: font-url('ProximaNova/ProximaNova-Bold.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Bold.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Bold.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Bold.svg') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
src: font-url('ProximaNova/ProximaNova-Bold-Italic.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Bold-Italic.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Bold-Italic.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Bold-Italic.svg') format('svg');
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
font-weight: 100;
|
||||
src: font-url('fonts/ProximaNova-Thin.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Thin.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Thin.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Thin.svg') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
font-weight: 100;
|
||||
font-style: italic;
|
||||
src: font-url('fonts/ProximaNova-Thin-Italic.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Thin-Italic.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Thin-Italic.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Thin-Italic.svg') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
font-weight: 900;
|
||||
src: font-url('ProximaNova/ProximaNova-Extrabold.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Extrabold.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Extrabold.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Extrabold.svg') format('svg');
|
||||
}
|
||||
68
app/assets/stylesheets/app/_groups.scss
Normal file
68
app/assets/stylesheets/app/_groups.scss
Normal file
@@ -0,0 +1,68 @@
|
||||
.groups {
|
||||
width: 22%;
|
||||
|
||||
.groups-title-bar {
|
||||
color: #0707ff;
|
||||
}
|
||||
|
||||
.group {
|
||||
height: 50px;
|
||||
border-bottom: 1px solid $bg-color;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
transition: height .1s ease-in-out;
|
||||
position: relative;
|
||||
|
||||
> .icon {
|
||||
float: left;
|
||||
padding-top: 6px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
> .title {
|
||||
width: 80%;
|
||||
background-color: transparent;
|
||||
font-weight: 600;
|
||||
float: left;
|
||||
color: $main-text-color;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-overflow: ellipsis;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
> .count {
|
||||
position: absolute;
|
||||
right: 17px;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: #5151ff;
|
||||
color: white;
|
||||
> .title {
|
||||
color: white;
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
|
||||
/* When a note is dragged over group */
|
||||
&.over {
|
||||
background-color: rgba(#5151ff, 0.8);
|
||||
color: white;
|
||||
border: 2px dashed white;
|
||||
> .title {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(.selected) {
|
||||
background-color: rgba(#5151ff, 0.8);
|
||||
color: white;
|
||||
> .title {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
292
app/assets/stylesheets/app/_header.scss
Normal file
292
app/assets/stylesheets/app/_header.scss
Normal file
@@ -0,0 +1,292 @@
|
||||
.header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: $bg-color;
|
||||
height: $header-height;
|
||||
max-height: $header-height;
|
||||
z-index: 100;
|
||||
font-size: 14px;
|
||||
color: $dark-gray;
|
||||
// padding-top: 5px;
|
||||
margin-top: 4px;
|
||||
|
||||
a {
|
||||
color: $dark-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.header-content {
|
||||
margin-bottom: 0px;
|
||||
padding-top: 0px;
|
||||
border-radius: 0px;
|
||||
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
|
||||
.header-name {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
padding-left: 11px;
|
||||
margin-left: 0px;
|
||||
height: 22px;
|
||||
margin-top: 2px;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.panel-status-text {
|
||||
margin-top: 20px;
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
|
||||
float: right;
|
||||
padding-top: 5px;
|
||||
margin-top: 10px;
|
||||
color: black;
|
||||
z-index: 1000;
|
||||
margin-bottom: 0px;
|
||||
font-size: 18px;
|
||||
|
||||
.login-panel .login-input {
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.items {
|
||||
|
||||
.advanced-brand {
|
||||
font-size: 18px;
|
||||
|
||||
&.btn {
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
font-weight: bold;
|
||||
.n {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.advanced {
|
||||
margin-left: -4px;
|
||||
text-transform: uppercase;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.beta {
|
||||
font-size: 7px;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
|
||||
display: inline-block;
|
||||
margin-right: 7px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
a {
|
||||
color: $dark-gray;
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
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: scroll;
|
||||
background-color: white;
|
||||
|
||||
|
||||
.storage-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.half-button {
|
||||
$spacing: 2px;
|
||||
width: calc(50% - #{$spacing});
|
||||
margin-left: $spacing/2.0;
|
||||
margin-right: $spacing/2.0;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.item.account {
|
||||
|
||||
|
||||
.link-item {
|
||||
margin-bottom: 8px;
|
||||
a {
|
||||
font-size: 14px;
|
||||
color: #00228f;
|
||||
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: #00228f;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
> .icon-container {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
> .meta-container {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> .action-container {
|
||||
font-size: 14px;
|
||||
margin-top: 6px;
|
||||
.status-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.subtext {
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.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: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> .desc {
|
||||
font-size: 14px;
|
||||
margin-top: 3px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.membership-settings {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.account-form {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.registration-login {
|
||||
|
||||
.login-forgot {
|
||||
margin-top: 20px;
|
||||
clear: both;
|
||||
a {
|
||||
display: block;
|
||||
font-size: 13px !important;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.account-menu {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.faq-panel {
|
||||
width: 350px;
|
||||
font-size: 16px;
|
||||
z-index: 1000;
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.faq-item {
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.question {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.answer {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
ul {
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
181
app/assets/stylesheets/app/_main.scss
Normal file
181
app/assets/stylesheets/app/_main.scss
Normal file
@@ -0,0 +1,181 @@
|
||||
$main-text-color: black;
|
||||
$secondary-text-color: rgba($main-text-color, 0.8);
|
||||
$bg-color: #e3e3e3;
|
||||
|
||||
@mixin MQ-Small() {
|
||||
@media (max-width: $screen-xs-max) {
|
||||
@content;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin MQ-Medium() {
|
||||
@media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mixin MQ-Large() {
|
||||
@media (min-width: $screen-lg-min) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
[ng\:cloak], [ng-cloak], .ng-cloak {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans",
|
||||
"Droid Sans", "Helvetica Neue", sans-serif;
|
||||
color: $main-text-color;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dark-button {
|
||||
background-color: #2e2e2e;
|
||||
border: 0;
|
||||
padding: 6px 18px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
border-radius: 2px;
|
||||
border: 1px solid transparent;
|
||||
-webkit-appearance: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
line-height: 1.45;
|
||||
background-color: #f7f7f7;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
code {
|
||||
word-wrap: break-word;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
p {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.main-ui-view {
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.app-body-class {
|
||||
height: 100%;
|
||||
background-color: $bg-color;
|
||||
min-width: 100px;
|
||||
overflow: auto;
|
||||
min-width: 900px;
|
||||
}
|
||||
|
||||
$header-height: 50px;
|
||||
|
||||
.app-container {
|
||||
display: table;
|
||||
background-color: $bg-color;
|
||||
width: 100%;
|
||||
height: calc(100% - #{$header-height});
|
||||
padding: 15px;
|
||||
padding-top: 0px;
|
||||
font-size: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
$section-header-height: 70px;
|
||||
|
||||
.app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: table-row;
|
||||
vertical-align: top;
|
||||
|
||||
.light-button {
|
||||
background-color: $bg-color;
|
||||
font-weight: bold;
|
||||
color: $main-text-color;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
height: 35px;
|
||||
border-radius: 4px;
|
||||
padding-top: 6px;
|
||||
|
||||
&:hover {
|
||||
background-color: #cdcdcd;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 8px;
|
||||
padding-bottom: 0px;
|
||||
|
||||
display: block;
|
||||
height: 100%;
|
||||
float: left;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
min-width: 0;
|
||||
|
||||
font-size: 17px;
|
||||
|
||||
> .content {
|
||||
height: 100%;
|
||||
background-color: white;
|
||||
position: relative;
|
||||
box-shadow: 0px 0px 2px rgba(gray, 0.3);
|
||||
}
|
||||
|
||||
.section-title-bar {
|
||||
padding: 20px;
|
||||
height: $section-header-height;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid $bg-color;
|
||||
|
||||
> .title {
|
||||
float: left;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 85%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> .add-button {
|
||||
float: right;
|
||||
font-size: 30px;
|
||||
margin-top: -10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
410
app/assets/stylesheets/app/_mostrap.scss
Normal file
410
app/assets/stylesheets/app/_mostrap.scss
Normal file
@@ -0,0 +1,410 @@
|
||||
//== Media queries breakpoints
|
||||
//
|
||||
//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
|
||||
|
||||
// Extra small screen / phone
|
||||
//** Deprecated `$screen-xs` as of v3.0.1
|
||||
$screen-xs: 480px !default;
|
||||
//** Deprecated `$screen-xs-min` as of v3.2.0
|
||||
$screen-xs-min: $screen-xs !default;
|
||||
//** Deprecated `$screen-phone` as of v3.0.1
|
||||
$screen-phone: $screen-xs-min !default;
|
||||
|
||||
// Small screen / tablet
|
||||
//** Deprecated `$screen-sm` as of v3.0.1
|
||||
$screen-sm: 768px !default;
|
||||
$screen-sm-min: $screen-sm !default;
|
||||
//** Deprecated `$screen-tablet` as of v3.0.1
|
||||
$screen-tablet: $screen-sm-min !default;
|
||||
|
||||
// Medium screen / desktop
|
||||
//** Deprecated `$screen-md` as of v3.0.1
|
||||
$screen-md: 992px !default;
|
||||
$screen-md-min: $screen-md !default;
|
||||
//** Deprecated `$screen-desktop` as of v3.0.1
|
||||
$screen-desktop: $screen-md-min !default;
|
||||
|
||||
// Large screen / wide desktop
|
||||
//** Deprecated `$screen-lg` as of v3.0.1
|
||||
$screen-lg: 1200px !default;
|
||||
$screen-lg-min: $screen-lg !default;
|
||||
//** Deprecated `$screen-lg-desktop` as of v3.0.1
|
||||
$screen-lg-desktop: $screen-lg-min !default;
|
||||
|
||||
// So media queries don't overlap when required, provide a maximum
|
||||
$screen-xs-max: ($screen-sm-min - 1) !default;
|
||||
$screen-sm-max: ($screen-md-min - 1) !default;
|
||||
$screen-md-max: ($screen-lg-min - 1) !default;
|
||||
|
||||
@mixin MQ-Xsmall() {
|
||||
@media (max-width: $screen-xs-max) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin MQ-Small() {
|
||||
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin MQ-Medium() {
|
||||
@media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mixin MQ-Large() {
|
||||
@media (min-width: $screen-lg-min) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*:focus {outline:0;}
|
||||
|
||||
.navbar {
|
||||
min-height: 0px !important;
|
||||
background-color: white;
|
||||
height: 80px;
|
||||
margin-bottom: 0px;
|
||||
padding-top: 10px;
|
||||
border-radius: 0px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.navbar {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
position: relative;
|
||||
min-height: 50px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.navbar-header {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.container > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-header, .container-fluid > .navbar-collapse {
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.navbar > .container .navbar-brand, .navbar > .container-fluid .navbar-brand {
|
||||
margin-left: -15px;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
float: left;
|
||||
padding: 15px 15px;
|
||||
font-size: 18px;
|
||||
line-height: 20px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-header, .container-fluid > .navbar-collapse {
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.container > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-header, .container-fluid > .navbar-collapse {
|
||||
margin-right: -15px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.navbar-collapse.collapse {
|
||||
display: block !important;
|
||||
height: auto !important;
|
||||
padding-bottom: 0;
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.navbar-collapse {
|
||||
width: auto;
|
||||
border-top: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
overflow-x: visible;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
border-top: 1px solid transparent;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.navbar-right {
|
||||
float: right !important;
|
||||
margin-right: -15px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.navbar-text {
|
||||
float: left;
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
.navbar-text {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.dropup, .dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
// display: none;
|
||||
float: left;
|
||||
min-width: 160px;
|
||||
padding: 5px 0;
|
||||
margin: 2px 0 0;
|
||||
list-style: none;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.dropdown-menu>li>a {
|
||||
display: block;
|
||||
padding: 3px 20px;
|
||||
clear: both;
|
||||
font-weight: 400;
|
||||
line-height: 1.42857143;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.caret {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin-left: 2px;
|
||||
vertical-align: middle;
|
||||
border-top: 4px dashed;
|
||||
border-top: 4px solid\9;
|
||||
border-right: 4px solid transparent;
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
|
||||
button:focus {outline:0;}
|
||||
|
||||
.dropdown-menu-right {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.open > .dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
touch-action: manipulation;
|
||||
cursor: pointer;
|
||||
background-image: none;
|
||||
border: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857;
|
||||
border-radius: 4px;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// ul, menu, dir {
|
||||
// display: block;
|
||||
// list-style-type: disc;
|
||||
// -webkit-margin-before: 1em;
|
||||
// -webkit-margin-after: 1em;
|
||||
// -webkit-margin-start: 0px;
|
||||
// -webkit-margin-end: 0px;
|
||||
// -webkit-padding-start: 40px;
|
||||
// }
|
||||
|
||||
.dropdown-menu .divider {
|
||||
height: 1px;
|
||||
margin: 9px 0;
|
||||
overflow: hidden;
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
min-width: 300px;
|
||||
z-index: 1000;
|
||||
margin-top: 10px;
|
||||
box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.2);
|
||||
border: none;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.panel-top {
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.panel-left {
|
||||
left: -50px;
|
||||
}
|
||||
|
||||
.panel-centered {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857;
|
||||
color: #555555;
|
||||
background-color: #fff;
|
||||
background-image: none;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
-webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
|
||||
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
|
||||
}
|
||||
|
||||
input, button, select, textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.has-feedback {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
background-color: transparent;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-link:hover, .btn-link:focus {
|
||||
color: #23527c;
|
||||
text-decoration: underline;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.animated {
|
||||
-webkit-animation-duration: 1s;
|
||||
animation-duration: 1s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.animated-fast {
|
||||
-webkit-animation-duration: 0.5s;
|
||||
animation-duration: 0.5s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.fadeInDown {
|
||||
-webkit-animation-name: fadeInDown;
|
||||
animation-name: fadeInDown;
|
||||
}
|
||||
|
||||
.fadeIn {
|
||||
-webkit-animation-name: fadeIn;
|
||||
animation-name: fadeIn;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translate3d(0,-100%,0);
|
||||
transform: translate3d(0,-100%,0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: none;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
78
app/assets/stylesheets/app/_notes.scss
Normal file
78
app/assets/stylesheets/app/_notes.scss
Normal file
@@ -0,0 +1,78 @@
|
||||
.notes {
|
||||
width: 28%;
|
||||
|
||||
.notes-title-bar {
|
||||
color: #ff6551;
|
||||
height: 136px !important;
|
||||
}
|
||||
|
||||
.group-menu-bar {
|
||||
position: relative;
|
||||
margin: 0 -20px;
|
||||
width: auto;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
clear: left;
|
||||
height: 32px;
|
||||
margin-top: 14px;
|
||||
|
||||
.filter-bar {
|
||||
background-color: $bg-color;
|
||||
border-radius: 4px;
|
||||
height: 100%;
|
||||
color: #909090;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
line-height: 35px;
|
||||
|
||||
border: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.notes-footer {
|
||||
border-top: 1px solid $bg-color;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
margin-top: 1px solid $bg-color;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
|
||||
> .new-button {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.note {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
height: 70px;
|
||||
border-bottom: 1px solid $bg-color;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
> .name {
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
> .date {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: #ff6551;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:hover:not(.selected) {
|
||||
background-color: rgba(#ff6551, 0.8);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
app/assets/stylesheets/frontend.css.scss
Normal file
63
app/assets/stylesheets/frontend.css.scss
Normal file
@@ -0,0 +1,63 @@
|
||||
$dark-gray: #2e2e2e;
|
||||
|
||||
@import "app/mostrap";
|
||||
@import "app/common";
|
||||
@import "app/main";
|
||||
@import "app/header";
|
||||
@import "app/groups";
|
||||
@import "app/notes";
|
||||
@import "app/editor";
|
||||
|
||||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src: url('icomoon/icomoon.eot');
|
||||
src: url('icomoon/icomoon.eot') format('embedded-opentype'),
|
||||
url('icomoon/icomoon.ttf') format('truetype'),
|
||||
url('icomoon/icomoon.woff') format('woff'),
|
||||
url('icomoon/icomoon.svg') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
[class^="icon-"], [class*=" icon-"] {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: 'icomoon' !important;
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
// line-height: 1;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
line-height: 10px;
|
||||
}
|
||||
|
||||
.inline-icon {
|
||||
display: inline-block;
|
||||
// margin-right: -5px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.icon-lock:before {
|
||||
content: "\e98f";
|
||||
}
|
||||
|
||||
.icon-rss:before {
|
||||
content: "\ea9c";
|
||||
}
|
||||
|
||||
.icon-markdown:before {
|
||||
content: "\e901";
|
||||
}
|
||||
|
||||
.icon-keyboard:before {
|
||||
content: "\e900";
|
||||
}
|
||||
|
||||
.icon-enlarge:before {
|
||||
content: "\e989";
|
||||
}
|
||||
16
app/assets/templates/frontend/auth/forgot.html.haml
Normal file
16
app/assets/templates/frontend/auth/forgot.html.haml
Normal file
@@ -0,0 +1,16 @@
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
%h3.panel-title Forgot Your Password?
|
||||
.panel-body
|
||||
%p We'll send reset instructions to your email.
|
||||
%form{'ng-submit' => 'requestPasswordReset(forgotData)', 'ng-init' => 'forgotData = {}'}
|
||||
.form-group.has-feedback
|
||||
%input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'forgotData.email'}/
|
||||
%span.glyphicon.glyphicon-envelope.form-control-feedback
|
||||
.row
|
||||
.col-sm-8.hidden-xs
|
||||
%a.btn.btn-link{'ui-sref' => 'auth.login'} Go back to the login page
|
||||
.col-sm-4
|
||||
%button.btn.btn-main.btn-block.btn-flat{:type => 'submit'} Reset Password
|
||||
.col-xs-12.visible-xs
|
||||
%a.btn.btn-link.btn-block{'ui-sref' => 'auth.login'} Go back to the login page
|
||||
18
app/assets/templates/frontend/auth/login.html.haml
Normal file
18
app/assets/templates/frontend/auth/login.html.haml
Normal file
@@ -0,0 +1,18 @@
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
%h3.panel-title Sign in to start your session
|
||||
.panel-body
|
||||
%form{'ng-submit' => 'submitLogin(loginData)', 'ng-init' => 'loginData = {}'}
|
||||
.form-group.has-feedback
|
||||
%input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Username', :required => true, :type => 'email', 'ng-model' => 'loginData.email'}/
|
||||
%span.glyphicon.glyphicon-user.form-control-feedback
|
||||
.form-group.has-feedback
|
||||
%input.form-control{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'loginData.password'}/
|
||||
%span.glyphicon.glyphicon-lock.form-control-feedback
|
||||
.row
|
||||
.col-sm-8.hidden-xs
|
||||
%a.btn.btn-link{'ui-sref' => 'auth.forgot'} I forgot my password
|
||||
.col-sm-4
|
||||
%button.btn.btn-main.btn-block.btn-flat{:type => 'submit'} Sign in
|
||||
.col-xs-12.visible-xs
|
||||
%a.btn.btn-link.btn-block{'ui-sref' => 'auth.forgot'} I forgot my password
|
||||
15
app/assets/templates/frontend/auth/reset.html.haml
Normal file
15
app/assets/templates/frontend/auth/reset.html.haml
Normal file
@@ -0,0 +1,15 @@
|
||||
.panel.panel-default.panel-centered
|
||||
.panel-heading
|
||||
%h3.panel-title Reset Password
|
||||
.panel-body
|
||||
%p Type your new password.
|
||||
%form{'ng-submit' => 'resetPasswordSubmit()'}
|
||||
.form-group.has-feedback
|
||||
%input.form-control{:placeholder => 'New password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'resetData.password'}/
|
||||
%span.glyphicon.glyphicon-lock.form-control-feedback
|
||||
.form-group.has-feedback
|
||||
%input.form-control{:placeholder => 'Password confirmation', :name => 'password_confirmation', :required => true, :type => 'password', 'ng-model' => 'resetData.password_confirmation'}/
|
||||
%span.glyphicon.glyphicon-lock.form-control-feedback
|
||||
.row
|
||||
.col-sm-4.col-sm-offset-8
|
||||
%button.btn.btn-main.btn-block.btn-flat{:type => 'submit'} Update Password
|
||||
6
app/assets/templates/frontend/auth/wrapper.html.haml
Normal file
6
app/assets/templates/frontend/auth/wrapper.html.haml
Normal file
@@ -0,0 +1,6 @@
|
||||
.login-box.margin-auto{"style" => "margin-top: 150px;"}
|
||||
|
||||
%uib-alert{:type => '{{data.authAlert.type}}', :close => 'data.authAlert = null', 'ng-if' => 'data.authAlert'}
|
||||
{{data.authAlert.msg}}
|
||||
|
||||
%ui-view
|
||||
61
app/assets/templates/frontend/editor.html.haml
Normal file
61
app/assets/templates/frontend/editor.html.haml
Normal file
@@ -0,0 +1,61 @@
|
||||
.section.editor{"ng-class" => "{'fullscreen' : ctrl.fullscreen}"}
|
||||
.content
|
||||
.section-title-bar.editor-heading{"ng-class" => "{'shared' : ctrl.note.isPublic() }"}
|
||||
.title
|
||||
%input.input#note-title-editor{"ng-model" => "ctrl.note.name", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveTitle($event)",
|
||||
"ng-disabled" => "ctrl.note.locked", "ng-change" => "ctrl.nameChanged()", "ng-focus" => "ctrl.onNameFocus()",
|
||||
"select-on-click" => "true"}
|
||||
.save-status {{ctrl.noteStatus}}
|
||||
.editor-menu
|
||||
%ul.nav.nav-pills
|
||||
%li.dropdown
|
||||
%a.dropdown-toggle{"ng-click" => "ctrl.clickedMenu()"}
|
||||
File
|
||||
%span.caret{"ng-if" => "!ctrl.note.locked"}
|
||||
%span{"ng-if" => " ctrl.note.locked"}
|
||||
.inline-icon.icon-lock
|
||||
%span.sr-only
|
||||
|
||||
%ul.dropdown-menu.dropdown-menu-left.nt-dropdown-menu.dark{"ng-if" => "ctrl.showMenu && !ctrl.note.locked"}
|
||||
-# %li
|
||||
-# %a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.saveNote($event, note)"} Save
|
||||
-# .shortcut Cmd + S
|
||||
%li
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.toggleFullScreen()"} Toggle Fullscreen
|
||||
.shortcut Cmd + O
|
||||
%li
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.toggleMarkdown()"} Toggle Markdown Preview
|
||||
.shortcut Cmd + M
|
||||
%li{"ng-if" => "!ctrl.note.hasEnabledPresentation()"}
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.shareNote()"} Share
|
||||
%li{"ng-if" => "ctrl.note.hasEnabledPresentation()"}
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.editUrlPressed()"} Edit URL
|
||||
%li{"ng-if" => "ctrl.note.hasEnabledPresentation()"}
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.unshareNote()"} Unshare
|
||||
%li
|
||||
%a.text{"ng-click" => "ctrl.deleteNote()"} Delete
|
||||
.markdown.icon{"ng-if" => "ctrl.editorMode == 'preview'", "ng-click" => "ctrl.showMarkdown = !ctrl.showMarkdown"}
|
||||
.icon-markdown
|
||||
.panel.panel-default.info-panel{"ng-if" => "ctrl.showMarkdown"}
|
||||
.panel-body{"style" => "text-align: center; color: black;"}
|
||||
This editor is Markdown enabled.
|
||||
.menu-right-container
|
||||
.public-link{"ng-if" => "ctrl.note.hasEnabledPresentation()"}
|
||||
%a.url{"ng-if" => "!ctrl.editingUrl", "href" => "{{ctrl.publicUrlForNote(ctrl.note)}}", "target" => "_blank"}
|
||||
%span.icon-rss.icon
|
||||
{{ctrl.publicUrlForNote(note)}}
|
||||
.edit-url{"ng-if" => "ctrl.editingUrl"}
|
||||
{{ctrl.url.base}}
|
||||
%input.input{"ng-model" => "ctrl.url.token", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveUrl($event)",
|
||||
"ng-disabled" => "ctrl.note.locked", "ng-change" => "ctrl.urlChanged()", "ng-focus" => "ctrl.onUrlFocus()",
|
||||
"select-on-click" => "true", "autofocus" => "true"}
|
||||
|
||||
.editor-content{"ng-class" => "{'shared' : ctrl.note.isPublic() }"}
|
||||
.sampler-container{"ng-if" => "ctrl.showSampler", "ng-click" => "ctrl.focusEditor()"}
|
||||
%strong.name-sampler.sampler{"typewrite" => "true", "text" => "ctrl.demoNoteNames", "type-delay" => "30", "initial-delay" => "1.5s",
|
||||
"iteration-callback" => "ctrl.callback", "prebegin-fn" => "ctrl.prebeginFn", "iteration-delay" => "2000", "cursor" => ""}
|
||||
%code{"ng-if" => "ctrl.currentDemoContent.text"}
|
||||
.content-sampler.sampler{"typewrite" => "true", "text" => "ctrl.currentDemoContent.text", "type-delay" => "10", "iteration-callback" => "ctrl.contentCallback"}
|
||||
%textarea.editable#note-text-editor{"ng-disabled" => "ctrl.note.locked", "ng-show" => "ctrl.editorMode == 'edit'", "ng-model" => "ctrl.note.content",
|
||||
"ng-change" => "ctrl.contentChanged()", "ng-click" => "ctrl.clickedTextArea()", "ng-focus" => "ctrl.onContentFocus()"}
|
||||
.preview{"ng-if" => "ctrl.editorMode == 'preview'", "ng-bind-html" => "ctrl.renderedContent()", "ng-dblclick" => "ctrl.onPreviewDoubleClick()"}
|
||||
16
app/assets/templates/frontend/groups.html.haml
Normal file
16
app/assets/templates/frontend/groups.html.haml
Normal file
@@ -0,0 +1,16 @@
|
||||
.section.groups
|
||||
.content
|
||||
.section-title-bar.groups-title-bar
|
||||
.title Groups
|
||||
.add-button{"ng-click" => "ctrl.clickedAddNewGroup()"} +
|
||||
.group{"ng-if" => "ctrl.allGroup", "ng-click" => "ctrl.selectGroup(ctrl.allGroup)", "ng-class" => "{'selected' : ctrl.selectedGroup == ctrl.allGroup}",
|
||||
"droppable" => true, "drop" => "ctrl.handleDrop", "group" => "ctrl.allGroup"}
|
||||
%input.title{"ng-disabled" => "true", "ng-model" => "ctrl.allGroup.name"}
|
||||
.count {{ctrl.noteCount(ctrl.allGroup)}}
|
||||
.group{"ng-repeat" => "group in ctrl.groups", "ng-click" => "ctrl.selectGroup(group)", "ng-class" => "{'selected' : ctrl.selectedGroup == group}",
|
||||
"droppable" => true, "drop" => "ctrl.handleDrop", "group" => "group"}
|
||||
.icon.icon-rss{"ng-if" => "group.presentation.enabled"}
|
||||
%input.title{"ng-disabled" => "group != ctrl.selectedGroup", "ng-model" => "group.name",
|
||||
"ng-keyup" => "$event.keyCode == 13 && ctrl.saveGroup($event, group)", "mb-autofocus" => "true", "should-focus" => "ctrl.newGroup",
|
||||
"ng-change" => "ctrl.groupTitleDidChange(group)", "ng-focus" => "ctrl.onGroupTitleFocus(group)"}
|
||||
.count {{ctrl.noteCount(group)}}
|
||||
137
app/assets/templates/frontend/header.html.haml
Normal file
137
app/assets/templates/frontend/header.html.haml
Normal file
@@ -0,0 +1,137 @@
|
||||
.header
|
||||
.header-content
|
||||
%nav.animated-fast.fadeInDown
|
||||
%a.navbar-brand{"ui-sref" => "home"}
|
||||
.header-name
|
||||
neeto
|
||||
%span.tagline{"ng-if" => "!ctrl.user.id", "ng-cloak" => "true"} secure code box for developers
|
||||
|
||||
.menu.navbar-text.navbar-right
|
||||
.items
|
||||
.item.account
|
||||
%button.btn.dark-button.advanced-brand{"ng-click" => "ctrl.accountMenuPressed()"}
|
||||
%div{"ng-if" => "ctrl.user.email"} {{ctrl.user.email}}
|
||||
%div{"ng-if" => "!ctrl.user.email"} Sign in or Register
|
||||
.panel.panel-default.account-panel{"ng-if" => "ctrl.showAccountMenu"}
|
||||
.panel-body
|
||||
.account-items
|
||||
.account-item.registration-login{"ng-if" => "!ctrl.user.email"}
|
||||
.meta-container
|
||||
.title Sign in or Register
|
||||
.desc
|
||||
%form.account-form{'name' => "loginForm"}
|
||||
.form-group.has-feedback
|
||||
%input.form-control.login-input{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'ctrl.loginData.email'}
|
||||
.form-group.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.hasLocalData()"}
|
||||
%label
|
||||
%input{"type" => "checkbox", "ng-model" => "ctrl.user.shouldMerge", "ng-bind" => "true", "ng-change" => "ctrl.mergeLocalChanged()"}
|
||||
Merge local 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
|
||||
%a.btn.btn-link{"ng-click" => "ctrl.showResetForm = !ctrl.showResetForm"} I forgot my password
|
||||
.panel-status-text{"ng-if" => "ctrl.loginData.status", "style" => "font-size: 14px;"} {{ctrl.loginData.status}}
|
||||
|
||||
%form{"style" => "margin-top: 20px;", "ng-if" => "ctrl.showResetForm", "ng-init" => "ctrl.resetData = {}", 'ng-submit' => 'ctrl.forgotPasswordSubmit()', 'name' => "resetForm"}
|
||||
.form-group.has-feedback
|
||||
%input.form-control.login-input{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'ctrl.resetData.email'}
|
||||
%button.btn.dark-button.btn-block{:type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
||||
%span Send Reset Email
|
||||
.panel-status-text{"ng-if" => "ctrl.resetData.response", "style" => "font-size: 14px;"}
|
||||
{{ctrl.resetData.response}}
|
||||
|
||||
.account-item{"ng-if" => "ctrl.user.email"}
|
||||
.icon-container
|
||||
%img.icon{"lazy-img" => "assets/encryption.png"}
|
||||
.meta-container
|
||||
.title Local Encryption
|
||||
.desc Encrypt notes locally before sending to server. Neither the server owner nor an intrusive government can decrypt your locally encrypted notes.
|
||||
.action-container
|
||||
%span.status-title Status:
|
||||
{{ctrl.user.local_encryption_enabled ? 'enabled' : 'disabled'}}
|
||||
{{" | "}}
|
||||
%a{"ng-click" => "ctrl.toggleEncryptionStatus()"}
|
||||
{{ctrl.user.local_encryption_enabled ? 'Disable' : 'Enable'}}
|
||||
.subtext{"ng-if" => "ctrl.user.local_encryption_enabled"}
|
||||
{{ctrl.encryptionStatusForNotes()}} (shared notes not encrypted)
|
||||
.encryption-confirmation{"ng-if" => "ctrl.encryptionConfirmation"}
|
||||
%div{"ng-if" => "ctrl.user.local_encryption_enabled"}
|
||||
%p Are you sure you want to disable local encryption? All currently encrypted notes will be decrypted locally, then sent back to Neeto servers over a secure connection.
|
||||
%div{"ng-if" => "!ctrl.user.local_encryption_enabled"}
|
||||
%p We're glad you're taking privacy and security into your own hands. There are a couple things you should note about moving to local encryption:
|
||||
%ul
|
||||
%li
|
||||
If you forget your password, there is no way to reset it or recover it. Your data will be forever lost without your password.
|
||||
(You can however still change your password, as long as you know your current password.)
|
||||
%li
|
||||
The strength of the encryption is tied to the strength of your password. If you take your security seriously, you should use a password of at least 32 characters long.
|
||||
%p Are you sure you want to enable local encryption?
|
||||
.buttons
|
||||
%a.cancel{"ng-click" => "ctrl.cancelEncryptionChange()"} Cancel
|
||||
%a.confirm{"ng-click" => "ctrl.confirmEncryptionChange()"} Confirm
|
||||
.account-item{"ng-if" => "ctrl.user.email"}
|
||||
.icon-container
|
||||
%img.icon.archive{"lazy-img" => "assets/archive.png"}
|
||||
.meta-container
|
||||
.title Data Archives
|
||||
.desc Note: data archives that you download using the link below are decrypted before save. You should take care to store them in a safe location.
|
||||
.action-container
|
||||
%a#download-archive{"ng-click" => "ctrl.downloadDataArchive()"} Download Latest Data Archive
|
||||
|
||||
.account-item
|
||||
.meta-container
|
||||
.title Server
|
||||
.desc Use a custom Neeto server to store and retrieve your account data.
|
||||
.action-container
|
||||
%form.account-form{'ng-submit' => 'ctrl.changeServer()', 'name' => "serverChangeForm"}
|
||||
.form-group.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 Change Server
|
||||
|
||||
.links{"ng-if" => "ctrl.user.email"}
|
||||
.link-item
|
||||
%a{"ng-click" => "ctrl.changePasswordPressed()"} Change Password
|
||||
%form.account-form{"ng-if" => "ctrl.showNewPasswordForm", 'ng-submit' => 'ctrl.submitPasswordChange()', 'name' => "passwordChangeForm"}
|
||||
.form-group.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-group.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-group.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
|
||||
|
||||
.item
|
||||
%a.menuItem{"ng-click" => "ctrl.showFaq = !ctrl.showFaq"}
|
||||
faq
|
||||
.panel.panel-default.faq-panel{"ng-if" => "ctrl.showFaq"}
|
||||
.panel-body
|
||||
.faq-item
|
||||
%strong.question What is Neeto?
|
||||
%p.answer Neeto is a secure code box for developers to store common commands and useful notes.
|
||||
.faq-item
|
||||
%strong.question What privacy features does Neeto have?
|
||||
%p.answer
|
||||
We don't want your data. We really don't. Storing your data in our database is a liability for us. But there's a tradeoff between privacy and convenience. We try to be convenient but also focus highly on privacy and security.
|
||||
%ul
|
||||
%li Neeto can be used locally, so that no data is sent to our servers whatsoever. This means however that your data is controlled by your browser, and can vanish without notice.
|
||||
%li When signed in, your data is always sent through a secure connection to Neeto servers.
|
||||
%li When the server receives your data, it is encrypted before being saved.
|
||||
%li Your password is never sent to Neeto servers. Instead, we derive a key from your password and send that instead. This way, Neeto can never know the original contents of your password.
|
||||
%li With Neeto Advanced, you can enable Local Encryption. This encrypts your notes locally on your machine <strong>before</strong> sending to Neeto servers. This means that technically even Neeto couldn't see the contents of your notes (and for that matter any government eavesdroppers.)
|
||||
%li Neeto does not use Google Analytics, a seemingly harmless tool that Google uses to track your web usage patterns and sells it to advertisers.
|
||||
.faq-item
|
||||
%strong.question How does local encryption work?
|
||||
%p.answer Users who opt into using local encryption can add an additional layer of security over their notes. These notes will be encrypted locally on your machine before being sent over the wire. This means that when Neeto receives your notes, we have no idea what the contents are. And if a government ever forces us to give up your data, we couldn't decrypt it for them even if we wanted to.
|
||||
%p This encryption is based on your password, which is also never sent over the air. The strength of this encryption is directly tied to the strength of your password.
|
||||
|
||||
12
app/assets/templates/frontend/home.html.haml
Normal file
12
app/assets/templates/frontend/home.html.haml
Normal file
@@ -0,0 +1,12 @@
|
||||
.main-ui-view
|
||||
%header{"user" => "defaultUser", "logout" => "headerLogout"}
|
||||
.app-container
|
||||
.app
|
||||
%groups-section{"save" => "groupsSave", "add-new" => "groupsAddNew", "will-select" => "groupsWillMakeSelection", "selection-made" => "groupsSelectionMade", "all-group" => "allGroup",
|
||||
"groups" => "groups", "user" => "defaultUser", "update-note-group" => "groupsUpdateNoteGroup"}
|
||||
|
||||
%notes-section{"remove-group" => "notesRemoveGroup", "user" => "defaultUser", "add-new" => "notesAddNew", "selection-made" => "notesSelectionMade",
|
||||
"group" => "selectedGroup", "user-id" => "defaultUser.id", "remove" => "deleteNote"}
|
||||
|
||||
%editor-section{"ng-if" => "selectedNote", "note" => "selectedNote", "remove" => "deleteNote",
|
||||
"user" => "defaultUser", "save" => "saveNote"}
|
||||
6
app/assets/templates/frontend/layouts/about.html.haml
Normal file
6
app/assets/templates/frontend/layouts/about.html.haml
Normal file
@@ -0,0 +1,6 @@
|
||||
.about.animated.fadeIn
|
||||
.title About
|
||||
.summary Namewhale helps you find a unique name for your startup. Using an intelligent, seed-based algorithm, names are generated based on the sound, style, and feel of the seed words you chose.
|
||||
.links
|
||||
%a{"href" => "https://itunes.apple.com/us/app/namewhale/id1028881375?ls=1&mt=8", "target" => "_blank"} Namewhale on the AppStore
|
||||
%a{"href" => "https://twitter.com/namewhale", "target" => "_blank"} @namewhale
|
||||
4
app/assets/templates/frontend/layouts/footer.html.haml
Normal file
4
app/assets/templates/frontend/layouts/footer.html.haml
Normal file
@@ -0,0 +1,4 @@
|
||||
%footer.footer{"ng-class" => "footerClass"}
|
||||
.container
|
||||
.row
|
||||
.footer-about-section
|
||||
2
app/assets/templates/frontend/modals/username.html.haml
Normal file
2
app/assets/templates/frontend/modals/username.html.haml
Normal file
@@ -0,0 +1,2 @@
|
||||
%strong Choose a public username for all your shared note groups.
|
||||
%input{"style" => "margin-top: 10px; padding-left: 8px;", "type" => "text", "ng-keyup" => "$event.keyCode == 13 && saveUsername($event)", "ng-model" => "formData.username"}
|
||||
38
app/assets/templates/frontend/notes.html.haml
Normal file
38
app/assets/templates/frontend/notes.html.haml
Normal file
@@ -0,0 +1,38 @@
|
||||
.section.notes
|
||||
.content
|
||||
.section-title-bar.notes-title-bar
|
||||
.title {{ctrl.group.name}} notes
|
||||
.add-button{"ng-click" => "ctrl.createNewNote()"} +
|
||||
%br
|
||||
.filter-section
|
||||
%input.filter-bar{"select-on-click" => "true", "ng-model" => "ctrl.noteFilter.text", "placeholder" => "Filter", "ng-change" => "ctrl.filterTextChanged()", "lowercase" => "true"}
|
||||
.editor-menu.group-menu-bar
|
||||
%ul.nav.nav-pills
|
||||
%li.dropdown
|
||||
%a.dropdown-toggle{"ng-click" => "ctrl.showMenu = !ctrl.showMenu"}
|
||||
File
|
||||
%span.caret
|
||||
%span.sr-only
|
||||
%ul.dropdown-menu.dropdown-menu-left.nt-dropdown-menu.dark{"ng-if" => "ctrl.showMenu"}
|
||||
%li{"ng-if" => "!ctrl.group.presentation.enabled"}
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedGroupShare($event)"} Share Group
|
||||
%li{"ng-if" => "ctrl.group.presentation.enabled"}
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedGroupUnshare()"} Unshare Group
|
||||
%li{"ng-if" => "!ctrl.group.all"}
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedGroupDelete()"} Delete Group
|
||||
.menu-right-container
|
||||
.public-link{"ng-if" => "ctrl.group.presentation.enabled"}
|
||||
%a.url{"ng-if" => "!ctrl.editingUrl", "href" => "{{ctrl.publicUrlForGroup(ctrl.group)}}", "target" => "_blank"}
|
||||
%span.icon-rss.icon
|
||||
{{ctrl.publicUrlForGroup()}}
|
||||
.edit-url{"ng-if" => "ctrl.editingUrl"}
|
||||
{{ctrl.url.base}}
|
||||
%input.input{"ng-model" => "ctrl.url.token", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveUrl($event)",
|
||||
"ng-change" => "ctrl.urlChanged()", "ng-focus" => "ctrl.onUrlFocus()",
|
||||
"select-on-click" => "true", "autofocus" => "true"}
|
||||
.note{"ng-repeat" => "note in ctrl.notes | filter: ctrl.filterNotes",
|
||||
"ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}",
|
||||
"ng-attr-draggable" => "{{note.dummy ? undefined : 'true'}}", "note" => "note"}
|
||||
.name
|
||||
{{note.name}}
|
||||
.date {{note.created_at || 'Now'}}
|
||||
0
app/assets/templates/services/.keep
Normal file
0
app/assets/templates/services/.keep
Normal file
27
app/controllers/application_controller.rb
Normal file
27
app/controllers/application_controller.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
include DeviseTokenAuth::Concerns::SetUserByToken
|
||||
# Prevent CSRF attacks by raising an exception.
|
||||
# For APIs, you may want to use :null_session instead.
|
||||
|
||||
protect_from_forgery with: :null_session
|
||||
after_action :set_csrf_cookie
|
||||
|
||||
layout :false
|
||||
|
||||
def frontend
|
||||
set_app_domain
|
||||
end
|
||||
|
||||
rescue_from ActionView::MissingTemplate do |exception|
|
||||
end
|
||||
protected
|
||||
|
||||
def set_app_domain
|
||||
@appDomain = request.domain
|
||||
@appDomain << ':' + request.port.to_s unless request.port.blank?
|
||||
end
|
||||
def set_csrf_cookie
|
||||
cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
|
||||
end
|
||||
|
||||
end
|
||||
0
app/controllers/concerns/.keep
Normal file
0
app/controllers/concerns/.keep
Normal file
2
app/helpers/application_helper.rb
Normal file
2
app/helpers/application_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module ApplicationHelper
|
||||
end
|
||||
0
app/mailers/.keep
Normal file
0
app/mailers/.keep
Normal file
0
app/models/.keep
Normal file
0
app/models/.keep
Normal file
61
app/views/application/frontend.html.erb
Normal file
61
app/views/application/frontend.html.erb
Normal file
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html ng-app="app.frontend" ng-controller="BaseCtrl">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport"/>
|
||||
|
||||
<link href="favicon/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180"></link>
|
||||
<link href="favicon/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"></link>
|
||||
<link href="favicon/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"></link>
|
||||
<link href="favicon/manifest.json" rel="manifest"></link>
|
||||
|
||||
<link color="#5bbad5" href="favicon/safari-pinned-tab.svg" rel="mask-icon"></link>
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<meta ng-bind="title" content="Neeto" name="apple-mobile-web-app-title"/>
|
||||
<meta ng-bind="title" content="Neeto" name="application-name"/>
|
||||
<base href="/"></base>
|
||||
|
||||
<title ng-bind="title">Neeto</title>
|
||||
<meta name="description" content="A private and secure personal notes/blogging system."/>
|
||||
|
||||
<meta name="twitter:title" content="Neeto, a private and secure notes app."/>
|
||||
<meta name="twitter:description" content="A private and secure personal notes/blogging system."/>
|
||||
<meta name="twitter:site" content="@neetoapp"/>
|
||||
<meta name="twitter:card" content="summary"/>
|
||||
|
||||
<meta name="og:title" content="Neeto, a private and secure notes app."/>
|
||||
<meta name="og:description" content="A private and secure personal notes/blogging system."/>
|
||||
|
||||
<% if Rails.env.development? %>
|
||||
<%= javascript_include_tag "compiled.js", debug: false %>
|
||||
<% else %>
|
||||
<%= javascript_include_tag "compiled.min.js", debug: false %>
|
||||
|
||||
<!-- Piwik -->
|
||||
<script type="text/javascript">
|
||||
var _paq = _paq || [];
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u="//dash.neeto.io/";
|
||||
_paq.push(['setTrackerUrl', u+'piwik.php']);
|
||||
_paq.push(['setSiteId', '1']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
<noscript><p><img src="//dash.neeto.io/piwik.php?idsite=1" style="border:0;" alt="" /></p></noscript>
|
||||
<!-- End Piwik Code -->
|
||||
|
||||
<% end %>
|
||||
<%= stylesheet_link_tag "app", media: "all", debug: false %>
|
||||
|
||||
</head>
|
||||
|
||||
<body ng-class="bodyClass">
|
||||
<div ui-view="content"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
14
app/views/layouts/application.html.erb
Normal file
14
app/views/layouts/application.html.erb
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Namewhale</title>
|
||||
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
|
||||
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
|
||||
<%= csrf_meta_tags %>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<%= yield %>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user