V2
This commit is contained in:
@@ -70,7 +70,7 @@ module.exports = function(grunt) {
|
|||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
src: [
|
src: [
|
||||||
'app/assets/javascripts/app/services/helpers/*.js',
|
'app/assets/javascripts/app/services/encryption/*.js',
|
||||||
'app/assets/javascripts/app/*.js',
|
'app/assets/javascripts/app/*.js',
|
||||||
'app/assets/javascripts/app/frontend/*.js',
|
'app/assets/javascripts/app/frontend/*.js',
|
||||||
'app/assets/javascripts/app/frontend/controllers/*.js',
|
'app/assets/javascripts/app/frontend/controllers/*.js',
|
||||||
@@ -142,7 +142,6 @@ module.exports = function(grunt) {
|
|||||||
dest: 'vendor/assets/javascripts/compiled.min.js'
|
dest: 'vendor/assets/javascripts/compiled.min.js'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
grunt.loadNpmTasks('grunt-newer');
|
grunt.loadNpmTasks('grunt-newer');
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ For more information on Standard Notes, see https://standardnotes.org.
|
|||||||
|
|
||||||
Open your browser to http://localhost:3000
|
Open your browser to http://localhost:3000
|
||||||
|
|
||||||
In the bottom left, click on "Sign in or Register" or "Account" (if signed in), and make sure you're using the correct server. You can use a production server here as usual (like https://n3.standardnotes.org).
|
In the bottom left, click on "Sign in or Register" or "Account" (if signed in), and make sure you're using the correct server. You can use a production server here as usual (like https://sync.standardnotes.org).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -14,3 +14,25 @@ if(!IEOrEdge && (window.crypto && window.crypto.subtle)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
angular.module('app.frontend', [])
|
angular.module('app.frontend', [])
|
||||||
|
|
||||||
|
function getParameterByName(name, url) {
|
||||||
|
name = name.replace(/[\[\]]/g, "\\$&");
|
||||||
|
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
|
||||||
|
results = regex.exec(url);
|
||||||
|
if (!results) return null;
|
||||||
|
if (!results[2]) return '';
|
||||||
|
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parametersFromURL(url) {
|
||||||
|
url = url.split("?").slice(-1)[0];
|
||||||
|
var obj = {};
|
||||||
|
url.replace(/([^=&]+)=([^&]*)/g, function(m, key, value) {
|
||||||
|
obj[decodeURIComponent(key)] = decodeURIComponent(value);
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDesktopApplication() {
|
||||||
|
return window && window.process && window.process.type && window.process.versions["electron"];
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
class BaseCtrl {
|
|
||||||
constructor($rootScope, $scope, syncManager, dbManager, analyticsManager, componentManager) {
|
|
||||||
dbManager.openDatabase(null, function(){
|
|
||||||
// new database, delete syncToken so that items can be refetched entirely from server
|
|
||||||
syncManager.clearSyncToken();
|
|
||||||
syncManager.sync();
|
|
||||||
})
|
|
||||||
|
|
||||||
$scope.onUpdateAvailable = function(version) {
|
|
||||||
$rootScope.$broadcast('new-update-available', version);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getParameterByName(name, url) {
|
|
||||||
name = name.replace(/[\[\]]/g, "\\$&");
|
|
||||||
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
|
|
||||||
results = regex.exec(url);
|
|
||||||
if (!results) return null;
|
|
||||||
if (!results[2]) return '';
|
|
||||||
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
|
||||||
}
|
|
||||||
|
|
||||||
function parametersFromURL(url) {
|
|
||||||
url = url.split("?").slice(-1)[0];
|
|
||||||
var obj = {};
|
|
||||||
url.replace(/([^=&]+)=([^&]*)/g, function(m, key, value) {
|
|
||||||
obj[decodeURIComponent(key)] = decodeURIComponent(value);
|
|
||||||
});
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
angular.module('app.frontend').controller('BaseCtrl', BaseCtrl);
|
|
||||||
@@ -37,7 +37,7 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, extensionManager, syncManager, modelManager, editorManager, themeManager, componentManager) {
|
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, extensionManager, syncManager, modelManager, editorManager, themeManager, componentManager, storageManager) {
|
||||||
|
|
||||||
this.componentManager = componentManager;
|
this.componentManager = componentManager;
|
||||||
this.componentStack = [];
|
this.componentStack = [];
|
||||||
@@ -50,6 +50,10 @@ angular.module('app.frontend')
|
|||||||
this.syncTakingTooLong = true;
|
this.syncTakingTooLong = true;
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
|
$rootScope.$on("sync:completed", function(){
|
||||||
|
this.syncTakingTooLong = false;
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
$rootScope.$on("tag-changed", function(){
|
$rootScope.$on("tag-changed", function(){
|
||||||
this.loadTagsString();
|
this.loadTagsString();
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
@@ -291,7 +295,7 @@ angular.module('app.frontend')
|
|||||||
if(success) {
|
if(success) {
|
||||||
if(statusTimeout) $timeout.cancel(statusTimeout);
|
if(statusTimeout) $timeout.cancel(statusTimeout);
|
||||||
statusTimeout = $timeout(function(){
|
statusTimeout = $timeout(function(){
|
||||||
var status = "All changes saved"
|
var status = "All changes saved";
|
||||||
if(authManager.offline()) {
|
if(authManager.offline()) {
|
||||||
status += " (offline)";
|
status += " (offline)";
|
||||||
}
|
}
|
||||||
@@ -368,12 +372,34 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.togglePin = function() {
|
||||||
|
this.note.setAppDataItem("pinned", !this.note.pinned);
|
||||||
|
this.note.setDirty(true);
|
||||||
|
this.changesMade();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toggleArchiveNote = function() {
|
||||||
|
this.note.setAppDataItem("archived", !this.note.archived);
|
||||||
|
this.note.setDirty(true);
|
||||||
|
this.changesMade();
|
||||||
|
$rootScope.$broadcast("noteArchived");
|
||||||
|
}
|
||||||
|
|
||||||
this.clickedEditNote = function() {
|
this.clickedEditNote = function() {
|
||||||
this.editorMode = 'edit';
|
this.editorMode = 'edit';
|
||||||
this.focusEditor(100);
|
this.focusEditor(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tags */
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Tags
|
||||||
|
*/
|
||||||
|
|
||||||
this.loadTagsString = function() {
|
this.loadTagsString = function() {
|
||||||
var string = "";
|
var string = "";
|
||||||
@@ -419,16 +445,23 @@ angular.module('app.frontend')
|
|||||||
this.updateTags()(this.note, tags);
|
this.updateTags()(this.note, tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Components */
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Components
|
||||||
|
*/
|
||||||
|
|
||||||
let alertKey = "displayed-component-disable-alert";
|
let alertKey = "displayed-component-disable-alert";
|
||||||
|
|
||||||
this.disableComponent = function(component) {
|
this.disableComponent = function(component) {
|
||||||
componentManager.disableComponentForItem(component, this.note);
|
componentManager.disableComponentForItem(component, this.note);
|
||||||
componentManager.setEventFlowForComponent(component, false);
|
componentManager.setEventFlowForComponent(component, false);
|
||||||
if(!localStorage.getItem(alertKey)) {
|
if(!storageManager.getItem(alertKey)) {
|
||||||
alert("This component will be disabled for this note. You can re-enable this component in the 'Menu' of the editor pane.");
|
alert("This component will be disabled for this note. You can re-enable this component in the 'Menu' of the editor pane.");
|
||||||
localStorage.setItem(alertKey, true);
|
storageManager.setItem(alertKey, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,6 +488,19 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Editor Customization
|
||||||
|
*/
|
||||||
|
|
||||||
this.onSystemEditorLoad = function() {
|
this.onSystemEditorLoad = function() {
|
||||||
if(this.loadedTabListener) {
|
if(this.loadedTabListener) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.controller('FooterCtrl', function ($rootScope, authManager, modelManager, $timeout, dbManager, syncManager) {
|
.controller('FooterCtrl', function ($rootScope, authManager, modelManager, $timeout, dbManager, syncManager, storageManager, passcodeManager) {
|
||||||
|
|
||||||
this.user = authManager.user;
|
this.user = authManager.user;
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
this.updateOfflineStatus();
|
this.updateOfflineStatus();
|
||||||
|
|
||||||
if(this.offline) {
|
if(this.offline && !passcodeManager.hasPasscode()) {
|
||||||
this.showAccountMenu = true;
|
this.showAccountMenu = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +40,10 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
this.findErrors();
|
this.findErrors();
|
||||||
|
|
||||||
|
this.onAuthSuccess = function() {
|
||||||
|
this.showAccountMenu = false;
|
||||||
|
}.bind(this)
|
||||||
|
|
||||||
this.accountMenuPressed = function() {
|
this.accountMenuPressed = function() {
|
||||||
this.serverData = {};
|
this.serverData = {};
|
||||||
this.showAccountMenu = !this.showAccountMenu;
|
this.showAccountMenu = !this.showAccountMenu;
|
||||||
@@ -61,6 +65,14 @@ angular.module('app.frontend')
|
|||||||
this.showAccountMenu = false;
|
this.showAccountMenu = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.hasPasscode = function() {
|
||||||
|
return passcodeManager.hasPasscode();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lockApp = function() {
|
||||||
|
$rootScope.lockApplication();
|
||||||
|
}
|
||||||
|
|
||||||
this.refreshData = function() {
|
this.refreshData = function() {
|
||||||
this.isRefreshing = true;
|
this.isRefreshing = true;
|
||||||
syncManager.sync(function(response){
|
syncManager.sync(function(response){
|
||||||
|
|||||||
@@ -1,56 +1,86 @@
|
|||||||
angular.module('app.frontend')
|
angular.module('app.frontend')
|
||||||
.controller('HomeCtrl', function ($scope, $location, $rootScope, $timeout, modelManager, syncManager, authManager, themeManager) {
|
.controller('HomeCtrl', function ($scope, $location, $rootScope, $timeout, modelManager,
|
||||||
|
dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager) {
|
||||||
|
|
||||||
function urlParam(key) {
|
storageManager.initialize(passcodeManager.hasPasscode(), authManager.isEphemeralSession());
|
||||||
return $location.search()[key];
|
|
||||||
|
$scope.onUpdateAvailable = function(version) {
|
||||||
|
$rootScope.$broadcast('new-update-available', version);
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoSignInFromParams() {
|
$rootScope.lockApplication = function() {
|
||||||
var server = urlParam("server");
|
// Render first to show lock screen immediately, then refresh
|
||||||
var email = urlParam("email");
|
$scope.needsUnlock = true;
|
||||||
var pw = urlParam("pw");
|
// Reloading wipes current objects from memory
|
||||||
|
setTimeout(function () {
|
||||||
if(!authManager.offline()) {
|
window.location.reload();
|
||||||
// check if current account
|
}, 100);
|
||||||
if(syncManager.serverURL === server && authManager.user.email === email) {
|
|
||||||
// already signed in, return
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// sign out
|
|
||||||
syncManager.destroyLocalData(function(){
|
|
||||||
window.location.reload();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
authManager.login(server, email, pw, function(response){
|
|
||||||
window.location.reload();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(urlParam("server")) {
|
function load() {
|
||||||
autoSignInFromParams();
|
// pass keys to storageManager to decrypt storage
|
||||||
|
storageManager.setKeys(passcodeManager.keys());
|
||||||
|
|
||||||
|
openDatabase();
|
||||||
|
// Retrieve local data and begin sycing timer
|
||||||
|
initiateSync();
|
||||||
|
// Configure "All" psuedo-tag
|
||||||
|
loadAllTag();
|
||||||
|
// Configure "Archived" psuedo-tag
|
||||||
|
loadArchivedTag();
|
||||||
}
|
}
|
||||||
|
|
||||||
syncManager.loadLocalItems(function(items) {
|
if(passcodeManager.isLocked()) {
|
||||||
$scope.allTag.didLoad = true;
|
$scope.needsUnlock = true;
|
||||||
themeManager.activateInitialTheme();
|
} else {
|
||||||
$scope.$apply();
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.onSuccessfulUnlock = function() {
|
||||||
|
$timeout(() => {
|
||||||
|
$scope.needsUnlock = false;
|
||||||
|
load();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDatabase() {
|
||||||
|
dbManager.setLocked(false);
|
||||||
|
dbManager.openDatabase(null, function() {
|
||||||
|
// new database, delete syncToken so that items can be refetched entirely from server
|
||||||
|
syncManager.clearSyncToken();
|
||||||
|
syncManager.sync();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function initiateSync() {
|
||||||
|
authManager.loadInitialData();
|
||||||
|
syncManager.loadLocalItems(function(items) {
|
||||||
|
$scope.allTag.didLoad = true;
|
||||||
|
themeManager.activateInitialTheme();
|
||||||
|
$scope.$apply();
|
||||||
|
|
||||||
|
|
||||||
syncManager.sync(null);
|
|
||||||
// refresh every 30s
|
|
||||||
setInterval(function () {
|
|
||||||
syncManager.sync(null);
|
syncManager.sync(null);
|
||||||
}, 30000);
|
// refresh every 30s
|
||||||
});
|
setInterval(function () {
|
||||||
|
syncManager.sync(null);
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var allTag = new Tag({all: true});
|
function loadAllTag() {
|
||||||
allTag.needsLoad = true;
|
var allTag = new Tag({all: true, title: "All"});
|
||||||
$scope.allTag = allTag;
|
allTag.needsLoad = true;
|
||||||
$scope.allTag.title = "All";
|
$scope.allTag = allTag;
|
||||||
$scope.tags = modelManager.tags;
|
$scope.tags = modelManager.tags;
|
||||||
$scope.allTag.notes = modelManager.notes;
|
$scope.allTag.notes = modelManager.notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadArchivedTag() {
|
||||||
|
var archiveTag = new Tag({archiveTag: true, title: "Archived"});
|
||||||
|
$scope.archiveTag = archiveTag;
|
||||||
|
$scope.archiveTag.notes = modelManager.notes;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Editor Callbacks
|
Editor Callbacks
|
||||||
@@ -116,6 +146,7 @@ angular.module('app.frontend')
|
|||||||
tag.setDirty(true);
|
tag.setDirty(true);
|
||||||
syncManager.sync(callback);
|
syncManager.sync(callback);
|
||||||
$rootScope.$broadcast("tag-changed");
|
$rootScope.$broadcast("tag-changed");
|
||||||
|
modelManager.resortTag(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -140,7 +171,7 @@ angular.module('app.frontend')
|
|||||||
$scope.notesAddNew = function(note) {
|
$scope.notesAddNew = function(note) {
|
||||||
modelManager.addItem(note);
|
modelManager.addItem(note);
|
||||||
|
|
||||||
if(!$scope.selectedTag.all) {
|
if(!$scope.selectedTag.all && !$scope.selectedTag.archiveTag) {
|
||||||
modelManager.createRelationshipBetweenItems($scope.selectedTag, note);
|
modelManager.createRelationshipBetweenItems($scope.selectedTag, note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,4 +241,39 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Handle Auto Sign In From URL
|
||||||
|
|
||||||
|
function urlParam(key) {
|
||||||
|
return $location.search()[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoSignInFromParams() {
|
||||||
|
var server = urlParam("server");
|
||||||
|
var email = urlParam("email");
|
||||||
|
var pw = urlParam("pw");
|
||||||
|
|
||||||
|
if(!authManager.offline()) {
|
||||||
|
// check if current account
|
||||||
|
if(syncManager.serverURL === server && authManager.user.email === email) {
|
||||||
|
// already signed in, return
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// sign out
|
||||||
|
syncManager.destroyLocalData(function(){
|
||||||
|
window.location.reload();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
authManager.login(server, email, pw, false, function(response){
|
||||||
|
window.location.reload();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(urlParam("server")) {
|
||||||
|
autoSignInFromParams();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
class LockScreen {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.restrict = "E";
|
||||||
|
this.templateUrl = "frontend/lock-screen.html";
|
||||||
|
this.scope = {
|
||||||
|
onSuccess: "&",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
controller($scope, passcodeManager) {
|
||||||
|
'ngInject';
|
||||||
|
|
||||||
|
$scope.formData = {};
|
||||||
|
|
||||||
|
$scope.submitPasscodeForm = function() {
|
||||||
|
passcodeManager.unlock($scope.formData.passcode, (success) => {
|
||||||
|
if(!success) {
|
||||||
|
alert("Invalid passcode. Please try again.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.onSuccess()();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('app.frontend').directive('lockScreen', () => new LockScreen);
|
||||||
@@ -31,9 +31,9 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager) {
|
.controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager, storageManager) {
|
||||||
|
|
||||||
this.sortBy = localStorage.getItem("sortBy") || "created_at";
|
this.sortBy = storageManager.getItem("sortBy") || "created_at";
|
||||||
this.sortDescending = this.sortBy != "title";
|
this.sortDescending = this.sortBy != "title";
|
||||||
|
|
||||||
$rootScope.$on("editorFocused", function(){
|
$rootScope.$on("editorFocused", function(){
|
||||||
@@ -44,11 +44,26 @@ angular.module('app.frontend')
|
|||||||
this.selectFirstNote(false);
|
this.selectFirstNote(false);
|
||||||
}.bind(this))
|
}.bind(this))
|
||||||
|
|
||||||
|
$rootScope.$on("noteArchived", function() {
|
||||||
|
this.selectFirstNote(false);
|
||||||
|
}.bind(this))
|
||||||
|
|
||||||
this.notesToDisplay = 20;
|
this.notesToDisplay = 20;
|
||||||
this.paginate = function() {
|
this.paginate = function() {
|
||||||
this.notesToDisplay += 20
|
this.notesToDisplay += 20
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.sortByTitle = function() {
|
||||||
|
var base = "Sort |";
|
||||||
|
if(this.sortBy == "created_at") {
|
||||||
|
return base + " Date added";
|
||||||
|
} else if(this.sortBy == "updated_at") {
|
||||||
|
return base + " Date modifed";
|
||||||
|
} else if(this.sortBy == "title") {
|
||||||
|
return base + " Title";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.tagDidChange = function(tag, oldTag) {
|
this.tagDidChange = function(tag, oldTag) {
|
||||||
this.showMenu = false;
|
this.showMenu = false;
|
||||||
|
|
||||||
@@ -98,6 +113,14 @@ angular.module('app.frontend')
|
|||||||
this.noteFilter = {text : ''};
|
this.noteFilter = {text : ''};
|
||||||
|
|
||||||
this.filterNotes = function(note) {
|
this.filterNotes = function(note) {
|
||||||
|
if(this.tag.archiveTag) {
|
||||||
|
return note.archived;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(note.archived) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var filterText = this.noteFilter.text.toLowerCase();
|
var filterText = this.noteFilter.text.toLowerCase();
|
||||||
if(filterText.length == 0) {
|
if(filterText.length == 0) {
|
||||||
note.visible = true;
|
note.visible = true;
|
||||||
@@ -139,7 +162,7 @@ angular.module('app.frontend')
|
|||||||
|
|
||||||
this.setSortBy = function(type) {
|
this.setSortBy = function(type) {
|
||||||
this.sortBy = type;
|
this.sortBy = type;
|
||||||
localStorage.setItem("sortBy", type);
|
storageManager.setItem("sortBy", type);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ angular.module('app.frontend')
|
|||||||
save: "&",
|
save: "&",
|
||||||
tags: "=",
|
tags: "=",
|
||||||
allTag: "=",
|
allTag: "=",
|
||||||
|
archiveTag: "=",
|
||||||
updateNoteTag: "&",
|
updateNoteTag: "&",
|
||||||
removeTag: "&"
|
removeTag: "&"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
let AppDomain = "org.standardnotes.sn";
|
||||||
|
var dateFormatter;
|
||||||
|
|
||||||
class Item {
|
class Item {
|
||||||
|
|
||||||
constructor(json_obj) {
|
constructor(json_obj = {}) {
|
||||||
|
this.appData = {};
|
||||||
this.updateFromJSON(json_obj);
|
this.updateFromJSON(json_obj);
|
||||||
|
|
||||||
this.observers = [];
|
this.observers = [];
|
||||||
|
|
||||||
if(!this.uuid) {
|
if(!this.uuid) {
|
||||||
@@ -30,7 +32,7 @@ class Item {
|
|||||||
try {
|
try {
|
||||||
return JSON.parse(this.content);
|
return JSON.parse(this.content);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Error parsing json", e);
|
console.log("Error parsing json", e, this);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +83,10 @@ class Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mapContentToLocalProperties(contentObj) {
|
mapContentToLocalProperties(contentObj) {
|
||||||
|
this.appData = contentObj.appData;
|
||||||
|
if(!this.appData) {
|
||||||
|
this.appData = {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createContentJSONFromProperties() {
|
createContentJSONFromProperties() {
|
||||||
@@ -93,7 +98,10 @@ class Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
structureParams() {
|
structureParams() {
|
||||||
return {references: this.referenceParams()}
|
return {
|
||||||
|
references: this.referenceParams(),
|
||||||
|
appData: this.appData
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addItemAsRelationship(item) {
|
addItemAsRelationship(item) {
|
||||||
@@ -137,4 +145,71 @@ class Item {
|
|||||||
doNotEncrypt() {
|
doNotEncrypt() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
App Data
|
||||||
|
*/
|
||||||
|
|
||||||
|
setAppDataItem(key, value) {
|
||||||
|
var data = this.appData[AppDomain];
|
||||||
|
if(!data) {
|
||||||
|
data = {}
|
||||||
|
}
|
||||||
|
data[key] = value;
|
||||||
|
this.appData[AppDomain] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAppDataItem(key) {
|
||||||
|
var data = this.appData[AppDomain];
|
||||||
|
if(data) {
|
||||||
|
return data[key];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get pinned() {
|
||||||
|
return this.getAppDataItem("pinned");
|
||||||
|
}
|
||||||
|
|
||||||
|
get archived() {
|
||||||
|
return this.getAppDataItem("archived");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Dates
|
||||||
|
*/
|
||||||
|
|
||||||
|
createdAtString() {
|
||||||
|
return this.dateToLocalizedString(this.created_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedAtString() {
|
||||||
|
return this.dateToLocalizedString(this.updated_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
dateToLocalizedString(date) {
|
||||||
|
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
|
||||||
|
if (!dateFormatter) {
|
||||||
|
var locale = (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.language;
|
||||||
|
dateFormatter = new Intl.DateTimeFormat(locale, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: '2-digit',
|
||||||
|
weekday: 'long',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return dateFormatter.format(date);
|
||||||
|
} else {
|
||||||
|
// IE < 11, Safari <= 9.0.
|
||||||
|
// In English, this generates the string most similar to
|
||||||
|
// the toLocaleDateString() result above.
|
||||||
|
return date.toDateString() + ' ' + date.toLocaleTimeString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ class SyncAdapter extends Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
structureParams() {
|
structureParams() {
|
||||||
|
// There was a bug with the way Base64 content was parsed in previous releases related to this item.
|
||||||
|
// The bug would not parse the JSON behind the base64 string and thus saved data in an invalid format.
|
||||||
|
// This is the line: https://github.com/standardnotes/web/commit/1ad0bf73d8e995b7588854f1b1e4e4a02303a42f#diff-15753bac364782a3a5876032bcdbf99aR76
|
||||||
|
// We'll remedy this for affected users by trying to parse the content string
|
||||||
|
if(typeof this.content !== 'object') {
|
||||||
|
try {
|
||||||
|
this.content = JSON.parse(this.content);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
var params = this.content || {};
|
var params = this.content || {};
|
||||||
_.merge(params, super.structureParams());
|
_.merge(params, super.structureParams());
|
||||||
return params;
|
return params;
|
||||||
|
|||||||
@@ -106,4 +106,8 @@ class Note extends Item {
|
|||||||
get content_type() {
|
get content_type() {
|
||||||
return "Note";
|
return "Note";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tagsString() {
|
||||||
|
return Tag.arrayToDisplayString(this.tags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,4 +86,14 @@ class Tag extends Item {
|
|||||||
allReferencedObjects() {
|
allReferencedObjects() {
|
||||||
return this.notes;
|
return this.notes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static arrayToDisplayString(tags, includeComma) {
|
||||||
|
return tags.map(function(tag, i){
|
||||||
|
var text = "#" + tag.title;
|
||||||
|
if(i != tags.length - 1) {
|
||||||
|
text += includeComma ? ", " : " ";
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}).join(" ");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
class EncryptedStorage extends Item {
|
||||||
|
|
||||||
|
constructor(json_obj) {
|
||||||
|
super(json_obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
mapContentToLocalProperties(contentObject) {
|
||||||
|
super.mapContentToLocalProperties(contentObject)
|
||||||
|
this.storage = contentObject.storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
structureParams() {
|
||||||
|
var params = {
|
||||||
|
storage: this.storage,
|
||||||
|
};
|
||||||
|
|
||||||
|
_.merge(params, super.structureParams());
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {uuid: this.uuid}
|
||||||
|
}
|
||||||
|
|
||||||
|
get content_type() {
|
||||||
|
return "SN|EncryptedStorage";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ class ItemParams {
|
|||||||
constructor(item, keys, version) {
|
constructor(item, keys, version) {
|
||||||
this.item = item;
|
this.item = item;
|
||||||
this.keys = keys;
|
this.keys = keys;
|
||||||
this.version = version;
|
this.version = version || "002";
|
||||||
}
|
}
|
||||||
|
|
||||||
paramsForExportFile() {
|
paramsForExportFile() {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
angular.module('app.frontend')
|
angular.module('app.frontend')
|
||||||
.config(function ($locationProvider) {
|
.config(function ($locationProvider) {
|
||||||
|
|
||||||
var runningInElectron = window && window.process && window.process.type && window.process.versions["electron"];
|
if(!isDesktopApplication()) {
|
||||||
if(!runningInElectron) {
|
|
||||||
if (window.history && window.history.pushState) {
|
if (window.history && window.history.pushState) {
|
||||||
$locationProvider.html5Mode({
|
$locationProvider.html5Mode({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
class AnalyticsManager {
|
|
||||||
|
|
||||||
constructor(authManager) {
|
|
||||||
this.authManager = authManager;
|
|
||||||
|
|
||||||
var status = localStorage.getItem("analyticsEnabled");
|
|
||||||
if(status === null) {
|
|
||||||
this.enabled = false;
|
|
||||||
} else {
|
|
||||||
this.enabled = JSON.parse(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.enabled === true) {
|
|
||||||
this.initialize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus(enabled) {
|
|
||||||
this.enabled = enabled;
|
|
||||||
localStorage.setItem("analyticsEnabled", JSON.stringify(enabled));
|
|
||||||
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleStatus() {
|
|
||||||
this.setStatus(!this.enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize() {
|
|
||||||
// load analytics
|
|
||||||
window._paq = window._paq || [];
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
var u="https://piwik.standardnotes.org/";
|
|
||||||
window._paq.push(['setTrackerUrl', u+'piwik.php']);
|
|
||||||
window._paq.push(['setSiteId', '2']);
|
|
||||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
|
||||||
g.type='text/javascript'; g.id="piwik", g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
|
|
||||||
})();
|
|
||||||
|
|
||||||
var analyticsId = this.authManager.getUserAnalyticsId();
|
|
||||||
if(analyticsId) {
|
|
||||||
window._paq.push(['setUserId', analyticsId]);
|
|
||||||
}
|
|
||||||
window._paq.push(['trackPageView', "AppInterface"]);
|
|
||||||
window._paq.push(['enableLinkTracking']);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
angular.module('app.frontend').service('analyticsManager', AnalyticsManager);
|
|
||||||
@@ -7,49 +7,62 @@ angular.module('app.frontend')
|
|||||||
return domain;
|
return domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$get = function($rootScope, $timeout, httpManager, modelManager, dbManager) {
|
this.$get = function($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager) {
|
||||||
return new AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager);
|
return new AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager) {
|
function AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager) {
|
||||||
|
|
||||||
var userData = localStorage.getItem("user");
|
this.loadInitialData = function() {
|
||||||
if(userData) {
|
var userData = storageManager.getItem("user");
|
||||||
this.user = JSON.parse(userData);
|
if(userData) {
|
||||||
} else {
|
this.user = JSON.parse(userData);
|
||||||
// legacy, check for uuid
|
} else {
|
||||||
var idData = localStorage.getItem("uuid");
|
// legacy, check for uuid
|
||||||
if(idData) {
|
var idData = storageManager.getItem("uuid");
|
||||||
this.user = {uuid: idData};
|
if(idData) {
|
||||||
|
this.user = {uuid: idData};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.getUserAnalyticsId = function() {
|
|
||||||
if(!this.user || !this.user.uuid) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// anonymize user id irreversably
|
|
||||||
return Neeto.crypto.hmac256(this.user.uuid, Neeto.crypto.sha256(localStorage.getItem("pw")));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.offline = function() {
|
this.offline = function() {
|
||||||
return !this.user;
|
return !this.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isEphemeralSession = function() {
|
||||||
|
if(this.ephemeral == null || this.ephemeral == undefined) {
|
||||||
|
this.ephemeral = JSON.parse(storageManager.getItem("ephemeral", StorageManager.Fixed));
|
||||||
|
}
|
||||||
|
return this.ephemeral;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setEphemeral = function(ephemeral) {
|
||||||
|
this.ephemeral = ephemeral;
|
||||||
|
if(ephemeral) {
|
||||||
|
storageManager.setModelStorageMode(StorageManager.Ephemeral);
|
||||||
|
storageManager.setItemsMode(storageManager.hasPasscode() ? StorageManager.FixedEncrypted : StorageManager.Ephemeral);
|
||||||
|
} else {
|
||||||
|
storageManager.setItem("ephemeral", JSON.stringify(false), StorageManager.Fixed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.getAuthParams = function() {
|
this.getAuthParams = function() {
|
||||||
if(!this._authParams) {
|
if(!this._authParams) {
|
||||||
this._authParams = JSON.parse(localStorage.getItem("auth_params"));
|
this._authParams = JSON.parse(storageManager.getItem("auth_params"));
|
||||||
}
|
}
|
||||||
return this._authParams;
|
return this._authParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.keys = function() {
|
this.keys = function() {
|
||||||
var mk = localStorage.getItem("mk");
|
if(!this._keys) {
|
||||||
if(!mk) {
|
var mk = storageManager.getItem("mk");
|
||||||
return null;
|
if(!mk) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
this._keys = {mk: mk, ak: storageManager.getItem("ak")};
|
||||||
}
|
}
|
||||||
var keys = {mk: mk, ak: localStorage.getItem("ak")};
|
return this._keys;
|
||||||
return keys;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.protocolVersion = function() {
|
this.protocolVersion = function() {
|
||||||
@@ -99,7 +112,7 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.login = function(url, email, password, callback) {
|
this.login = function(url, email, password, ephemeral, callback) {
|
||||||
this.getAuthParamsForEmail(url, email, function(authParams){
|
this.getAuthParamsForEmail(url, email, function(authParams){
|
||||||
|
|
||||||
if(!authParams || !authParams.pw_cost) {
|
if(!authParams || !authParams.pw_cost) {
|
||||||
@@ -134,7 +147,11 @@ angular.module('app.frontend')
|
|||||||
var requestUrl = url + "/auth/sign_in";
|
var requestUrl = url + "/auth/sign_in";
|
||||||
var params = {password: keys.pw, email: email};
|
var params = {password: keys.pw, email: email};
|
||||||
httpManager.postAbsolute(requestUrl, params, function(response){
|
httpManager.postAbsolute(requestUrl, params, function(response){
|
||||||
|
this.setEphemeral(ephemeral);
|
||||||
|
|
||||||
this.handleAuthResponse(response, email, url, authParams, keys);
|
this.handleAuthResponse(response, email, url, authParams, keys);
|
||||||
|
storageManager.setModelStorageMode(ephemeral ? StorageManager.Ephemeral : StorageManager.Fixed);
|
||||||
|
|
||||||
callback(response);
|
callback(response);
|
||||||
}.bind(this), function(response){
|
}.bind(this), function(response){
|
||||||
console.error("Error logging in", response);
|
console.error("Error logging in", response);
|
||||||
@@ -148,11 +165,16 @@ angular.module('app.frontend')
|
|||||||
this.handleAuthResponse = function(response, email, url, authParams, keys) {
|
this.handleAuthResponse = function(response, email, url, authParams, keys) {
|
||||||
try {
|
try {
|
||||||
if(url) {
|
if(url) {
|
||||||
localStorage.setItem("server", url);
|
storageManager.setItem("server", url);
|
||||||
}
|
}
|
||||||
localStorage.setItem("user", JSON.stringify(response.user));
|
|
||||||
localStorage.setItem("auth_params", JSON.stringify(authParams));
|
this.user = response.user;
|
||||||
localStorage.setItem("jwt", response.token);
|
storageManager.setItem("user", JSON.stringify(response.user));
|
||||||
|
|
||||||
|
this._authParams = authParams;
|
||||||
|
storageManager.setItem("auth_params", JSON.stringify(authParams));
|
||||||
|
|
||||||
|
storageManager.setItem("jwt", response.token);
|
||||||
this.saveKeys(keys);
|
this.saveKeys(keys);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
dbManager.displayOfflineAlert();
|
dbManager.displayOfflineAlert();
|
||||||
@@ -160,18 +182,24 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.saveKeys = function(keys) {
|
this.saveKeys = function(keys) {
|
||||||
localStorage.setItem("pw", keys.pw);
|
this._keys = keys;
|
||||||
localStorage.setItem("mk", keys.mk);
|
storageManager.setItem("pw", keys.pw);
|
||||||
localStorage.setItem("ak", keys.ak);
|
storageManager.setItem("mk", keys.mk);
|
||||||
|
storageManager.setItem("ak", keys.ak);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.register = function(url, email, password, callback) {
|
this.register = function(url, email, password, ephemeral, callback) {
|
||||||
Neeto.crypto.generateInitialEncryptionKeysForUser({password: password, email: email}, function(keys, authParams){
|
Neeto.crypto.generateInitialEncryptionKeysForUser({password: password, email: email}, function(keys, authParams){
|
||||||
var requestUrl = url + "/auth";
|
var requestUrl = url + "/auth";
|
||||||
var params = _.merge({password: keys.pw, email: email}, authParams);
|
var params = _.merge({password: keys.pw, email: email}, authParams);
|
||||||
|
|
||||||
httpManager.postAbsolute(requestUrl, params, function(response){
|
httpManager.postAbsolute(requestUrl, params, function(response){
|
||||||
|
this.setEphemeral(ephemeral);
|
||||||
|
|
||||||
this.handleAuthResponse(response, email, url, authParams, keys);
|
this.handleAuthResponse(response, email, url, authParams, keys);
|
||||||
|
|
||||||
|
storageManager.setModelStorageMode(ephemeral ? StorageManager.Ephemeral : StorageManager.Fixed);
|
||||||
|
|
||||||
callback(response);
|
callback(response);
|
||||||
}.bind(this), function(response){
|
}.bind(this), function(response){
|
||||||
console.error("Registration error", response);
|
console.error("Registration error", response);
|
||||||
@@ -182,7 +210,7 @@ angular.module('app.frontend')
|
|||||||
|
|
||||||
this.changePassword = function(email, new_password, callback) {
|
this.changePassword = function(email, new_password, callback) {
|
||||||
Neeto.crypto.generateInitialEncryptionKeysForUser({password: new_password, email: email}, function(keys, authParams){
|
Neeto.crypto.generateInitialEncryptionKeysForUser({password: new_password, email: email}, function(keys, authParams){
|
||||||
var requestUrl = localStorage.getItem("server") + "/auth/change_pw";
|
var requestUrl = storageManager.getItem("server") + "/auth/change_pw";
|
||||||
var params = _.merge({new_password: keys.pw}, authParams);
|
var params = _.merge({new_password: keys.pw}, authParams);
|
||||||
|
|
||||||
httpManager.postAbsolute(requestUrl, params, function(response) {
|
httpManager.postAbsolute(requestUrl, params, function(response) {
|
||||||
@@ -200,10 +228,10 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.updateAuthParams = function(authParams, callback) {
|
this.updateAuthParams = function(authParams, callback) {
|
||||||
var requestUrl = localStorage.getItem("server") + "/auth/update";
|
var requestUrl = storageManager.getItem("server") + "/auth/update";
|
||||||
var params = authParams;
|
var params = authParams;
|
||||||
httpManager.postAbsolute(requestUrl, params, function(response) {
|
httpManager.postAbsolute(requestUrl, params, function(response) {
|
||||||
localStorage.setItem("auth_params", JSON.stringify(authParams));
|
storageManager.setItem("auth_params", JSON.stringify(authParams));
|
||||||
if(callback) {
|
if(callback) {
|
||||||
callback(response);
|
callback(response);
|
||||||
}
|
}
|
||||||
@@ -246,6 +274,8 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.signOut = function() {
|
this.signOut = function() {
|
||||||
|
this._keys = null;
|
||||||
|
this.user = null;
|
||||||
this._authParams = null;
|
this._authParams = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
class DBManager {
|
class DBManager {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.locked = true;
|
||||||
|
}
|
||||||
|
|
||||||
displayOfflineAlert() {
|
displayOfflineAlert() {
|
||||||
var message = "There was an issue loading your offline database. This could happen for two reasons:";
|
var message = "There was an issue loading your offline database. This could happen for two reasons:";
|
||||||
message += "\n\n1. You're in a private window in your browser. We can't save your data without access to the local database. Please use a non-private window.";
|
message += "\n\n1. You're in a private window in your browser. We can't save your data without access to the local database. Please use a non-private window.";
|
||||||
@@ -7,7 +11,15 @@ class DBManager {
|
|||||||
alert(message);
|
alert(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLocked(locked) {
|
||||||
|
this.locked = locked;
|
||||||
|
}
|
||||||
|
|
||||||
openDatabase(callback, onUgradeNeeded) {
|
openDatabase(callback, onUgradeNeeded) {
|
||||||
|
if(this.locked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var request = window.indexedDB.open("standardnotes", 1);
|
var request = window.indexedDB.open("standardnotes", 1);
|
||||||
|
|
||||||
request.onerror = function(event) {
|
request.onerror = function(event) {
|
||||||
@@ -57,7 +69,7 @@ class DBManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllItems(callback) {
|
getAllModels(callback) {
|
||||||
this.openDatabase((db) => {
|
this.openDatabase((db) => {
|
||||||
var objectStore = db.transaction("items").objectStore("items");
|
var objectStore = db.transaction("items").objectStore("items");
|
||||||
var items = [];
|
var items = [];
|
||||||
@@ -74,11 +86,11 @@ class DBManager {
|
|||||||
}, null)
|
}, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
saveItem(item) {
|
saveModel(item) {
|
||||||
this.saveItems([item]);
|
this.saveModels([item]);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveItems(items, callback) {
|
saveModels(items, callback) {
|
||||||
|
|
||||||
if(items.length == 0) {
|
if(items.length == 0) {
|
||||||
if(callback) {
|
if(callback) {
|
||||||
@@ -115,7 +127,7 @@ class DBManager {
|
|||||||
}, null)
|
}, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteItem(item, callback) {
|
deleteModel(item, callback) {
|
||||||
this.openDatabase((db) => {
|
this.openDatabase((db) => {
|
||||||
var request = db.transaction("items", "readwrite").objectStore("items").delete(item.uuid);
|
var request = db.transaction("items", "readwrite").objectStore("items").delete(item.uuid);
|
||||||
request.onsuccess = function(event) {
|
request.onsuccess = function(event) {
|
||||||
@@ -126,26 +138,17 @@ class DBManager {
|
|||||||
}, null)
|
}, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemByUUID(uuid, callback) {
|
clearAllModels(callback) {
|
||||||
this.openDatabase((db) => {
|
|
||||||
var request = db.transaction("items", "readonly").objectStore("items").get(uuid);
|
|
||||||
request.onsuccess = function(event) {
|
|
||||||
callback(event.result);
|
|
||||||
};
|
|
||||||
}, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearAllItems(callback) {
|
|
||||||
var deleteRequest = window.indexedDB.deleteDatabase("standardnotes");
|
var deleteRequest = window.indexedDB.deleteDatabase("standardnotes");
|
||||||
|
|
||||||
deleteRequest.onerror = function(event) {
|
deleteRequest.onerror = function(event) {
|
||||||
console.log("Error deleting database.");
|
console.log("Error deleting database.");
|
||||||
callback();
|
callback && callback();
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteRequest.onsuccess = function(event) {
|
deleteRequest.onsuccess = function(event) {
|
||||||
console.log("Database deleted successfully");
|
console.log("Database deleted successfully");
|
||||||
callback();
|
callback && callback();
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteRequest.onblocked = function(event) {
|
deleteRequest.onblocked = function(event) {
|
||||||
|
|||||||
@@ -3,18 +3,19 @@ class AccountMenu {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.restrict = "E";
|
this.restrict = "E";
|
||||||
this.templateUrl = "frontend/directives/account-menu.html";
|
this.templateUrl = "frontend/directives/account-menu.html";
|
||||||
this.scope = {};
|
this.scope = {
|
||||||
|
"onSuccessfulAuth" : "&"
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
controller($scope, authManager, modelManager, syncManager, dbManager, analyticsManager, $timeout) {
|
controller($scope, authManager, modelManager, syncManager, dbManager, passcodeManager, $timeout, storageManager) {
|
||||||
'ngInject';
|
'ngInject';
|
||||||
|
|
||||||
$scope.formData = {mergeLocal: true, url: syncManager.serverURL};
|
$scope.formData = {mergeLocal: true, url: syncManager.serverURL, ephemeral: false};
|
||||||
$scope.user = authManager.user;
|
$scope.user = authManager.user;
|
||||||
$scope.server = syncManager.serverURL;
|
$scope.server = syncManager.serverURL;
|
||||||
|
|
||||||
$scope.syncStatus = syncManager.syncStatus;
|
$scope.syncStatus = syncManager.syncStatus;
|
||||||
$scope.analyticsManager = analyticsManager;
|
|
||||||
|
|
||||||
$scope.encryptionKey = function() {
|
$scope.encryptionKey = function() {
|
||||||
return authManager.keys().mk;
|
return authManager.keys().mk;
|
||||||
@@ -29,7 +30,7 @@ class AccountMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$scope.dashboardURL = function() {
|
$scope.dashboardURL = function() {
|
||||||
return `${$scope.server}/dashboard/?server=${$scope.server}&id=${encodeURIComponent($scope.user.email)}&pw=${$scope.serverPassword()}`;
|
return `${$scope.server}/dashboard/#server=${$scope.server}&id=${encodeURIComponent($scope.user.email)}&pw=${$scope.serverPassword()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.newPasswordData = {};
|
$scope.newPasswordData = {};
|
||||||
@@ -95,7 +96,7 @@ class AccountMenu {
|
|||||||
$scope.login = function() {
|
$scope.login = function() {
|
||||||
$scope.formData.status = "Generating Login Keys...";
|
$scope.formData.status = "Generating Login Keys...";
|
||||||
$timeout(function(){
|
$timeout(function(){
|
||||||
authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){
|
authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, $scope.formData.ephemeral, function(response){
|
||||||
if(!response || response.error) {
|
if(!response || response.error) {
|
||||||
$scope.formData.status = null;
|
$scope.formData.status = null;
|
||||||
var error = response ? response.error : {message: "An unknown error occured."}
|
var error = response ? response.error : {message: "An unknown error occured."}
|
||||||
@@ -120,7 +121,7 @@ class AccountMenu {
|
|||||||
$scope.formData.status = "Generating Account Keys...";
|
$scope.formData.status = "Generating Account Keys...";
|
||||||
|
|
||||||
$timeout(function(){
|
$timeout(function(){
|
||||||
authManager.register($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){
|
authManager.register($scope.formData.url, $scope.formData.email, $scope.formData.user_password, $scope.formData.ephemeral ,function(response){
|
||||||
if(!response || response.error) {
|
if(!response || response.error) {
|
||||||
$scope.formData.status = null;
|
$scope.formData.status = null;
|
||||||
var error = response ? response.error : {message: "An unknown error occured."}
|
var error = response ? response.error : {message: "An unknown error occured."}
|
||||||
@@ -132,10 +133,6 @@ class AccountMenu {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.localNotesCount = function() {
|
|
||||||
return modelManager.filteredNotes.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.mergeLocalChanged = function() {
|
$scope.mergeLocalChanged = function() {
|
||||||
if(!$scope.formData.mergeLocal) {
|
if(!$scope.formData.mergeLocal) {
|
||||||
if(!confirm("Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?")) {
|
if(!confirm("Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?")) {
|
||||||
@@ -146,18 +143,20 @@ class AccountMenu {
|
|||||||
|
|
||||||
$scope.onAuthSuccess = function() {
|
$scope.onAuthSuccess = function() {
|
||||||
var block = function() {
|
var block = function() {
|
||||||
window.location.reload();
|
$timeout(function(){
|
||||||
|
$scope.onSuccessfulAuth()();
|
||||||
|
syncManager.sync();
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if($scope.formData.mergeLocal) {
|
if($scope.formData.mergeLocal) {
|
||||||
syncManager.markAllItemsDirtyAndSaveOffline(function(){
|
syncManager.markAllItemsDirtyAndSaveOffline(function(){
|
||||||
block();
|
block();
|
||||||
})
|
}, true)
|
||||||
} else {
|
} else {
|
||||||
dbManager.clearAllItems(function(){
|
modelManager.resetLocalMemory();
|
||||||
$timeout(function(){
|
storageManager.clearAllModels(function(){
|
||||||
block();
|
block();
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,26 +205,25 @@ class AccountMenu {
|
|||||||
var file = files[0];
|
var file = files[0];
|
||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
reader.onload = function(e) {
|
reader.onload = function(e) {
|
||||||
var data = JSON.parse(e.target.result);
|
try {
|
||||||
$timeout(function(){
|
var data = JSON.parse(e.target.result);
|
||||||
if(data.auth_params) {
|
$timeout(function(){
|
||||||
// request password
|
if(data.auth_params) {
|
||||||
$scope.importData.requestPassword = true;
|
// request password
|
||||||
$scope.importData.data = data;
|
$scope.importData.requestPassword = true;
|
||||||
} else {
|
$scope.importData.data = data;
|
||||||
$scope.performImport(data, null);
|
} else {
|
||||||
}
|
$scope.performImport(data, null);
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
alert("Unable to open file. Ensure it is a proper JSON file and try again.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.encryptionStatusForNotes = function() {
|
|
||||||
var items = modelManager.allItemsMatchingTypes(["Note", "Tag"]);
|
|
||||||
return items.length + "/" + items.length + " notes and tags encrypted";
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.importJSONData = function(data, password, callback) {
|
$scope.importJSONData = function(data, password, callback) {
|
||||||
var onDataReady = function() {
|
var onDataReady = function() {
|
||||||
var items = modelManager.mapResponseItemsToLocalModels(data.items);
|
var items = modelManager.mapResponseItemsToLocalModels(data.items);
|
||||||
@@ -424,6 +422,103 @@ class AccountMenu {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Encryption Status
|
||||||
|
*/
|
||||||
|
|
||||||
|
$scope.notesAndTagsCount = function() {
|
||||||
|
var items = modelManager.allItemsMatchingTypes(["Note", "Tag"]);
|
||||||
|
return items.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.encryptionStatusForNotes = function() {
|
||||||
|
var length = $scope.notesAndTagsCount();
|
||||||
|
return length + "/" + length + " notes and tags encrypted";
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.encryptionEnabled = function() {
|
||||||
|
return passcodeManager.hasPasscode() || !authManager.offline();
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.encryptionSource = function() {
|
||||||
|
if(!authManager.offline()) {
|
||||||
|
return "Account keys";
|
||||||
|
} else if(passcodeManager.hasPasscode()) {
|
||||||
|
return "Local Passcode";
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.encryptionStatusString = function() {
|
||||||
|
if(!authManager.offline()) {
|
||||||
|
return "End-to-end encryption is enabled. Your data is encrypted before being synced to your private account.";
|
||||||
|
} else if(passcodeManager.hasPasscode()) {
|
||||||
|
return "Encryption is enabled. Your data is encrypted using your passcode before being stored on disk.";
|
||||||
|
} else {
|
||||||
|
return "Encryption is not enabled. Sign in, register, or add a passcode lock to enable encryption.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Passcode Lock
|
||||||
|
*/
|
||||||
|
|
||||||
|
$scope.passcodeOptionAvailable = function() {
|
||||||
|
// If you're signed in with an ephemeral session, passcode lock is unavailable
|
||||||
|
return authManager.offline() || !authManager.isEphemeralSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.hasPasscode = function() {
|
||||||
|
return passcodeManager.hasPasscode();
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.addPasscodeClicked = function() {
|
||||||
|
$scope.formData.showPasscodeForm = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.submitPasscodeForm = function() {
|
||||||
|
var passcode = $scope.formData.passcode;
|
||||||
|
if(passcode !== $scope.formData.confirmPasscode) {
|
||||||
|
alert("The two passcodes you entered do not match. Please try again.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
passcodeManager.setPasscode(passcode, () => {
|
||||||
|
$timeout(function(){
|
||||||
|
$scope.formData.showPasscodeForm = false;
|
||||||
|
var offline = authManager.offline();
|
||||||
|
|
||||||
|
var message = "You've succesfully set an app passcode.";
|
||||||
|
if(offline) { message += " Your items will now be encrypted using this passcode."; }
|
||||||
|
alert(message);
|
||||||
|
|
||||||
|
if(offline) {
|
||||||
|
syncManager.markAllItemsDirtyAndSaveOffline();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.removePasscodePressed = function() {
|
||||||
|
var signedIn = !authManager.offline();
|
||||||
|
var message = "Are you sure you want to remove your local passcode?";
|
||||||
|
if(!signedIn) {
|
||||||
|
message += " This will remove encryption from your local data.";
|
||||||
|
}
|
||||||
|
if(confirm(message)) {
|
||||||
|
passcodeManager.clearPasscode();
|
||||||
|
if(authManager.offline()) {
|
||||||
|
syncManager.markAllItemsDirtyAndSaveOffline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.isDesktopApplication = function() {
|
||||||
|
return isDesktopApplication();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ class GlobalExtensionsMenu {
|
|||||||
$scope.editorManager = editorManager;
|
$scope.editorManager = editorManager;
|
||||||
$scope.componentManager = componentManager;
|
$scope.componentManager = componentManager;
|
||||||
|
|
||||||
|
$scope.serverExtensions = modelManager.itemsForContentType("SF|Extension");
|
||||||
|
|
||||||
$scope.selectedAction = function(action, extension) {
|
$scope.selectedAction = function(action, extension) {
|
||||||
extensionManager.executeAction(action, extension, null, function(response){
|
extensionManager.executeAction(action, extension, null, function(response){
|
||||||
if(response && response.error) {
|
if(response && response.error) {
|
||||||
@@ -53,6 +55,38 @@ class GlobalExtensionsMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server extensions
|
||||||
|
|
||||||
|
$scope.deleteServerExt = function(ext) {
|
||||||
|
if(confirm("Are you sure you want to delete and disable this extension?")) {
|
||||||
|
_.remove($scope.serverExtensions, {uuid: ext.uuid});
|
||||||
|
modelManager.setItemToBeDeleted(ext);
|
||||||
|
syncManager.sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.nameForServerExtension = function(ext) {
|
||||||
|
var url = ext.url;
|
||||||
|
if(!url) {
|
||||||
|
return "Invalid Extension";
|
||||||
|
}
|
||||||
|
if(url.includes("gdrive")) {
|
||||||
|
return "Google Drive Sync";
|
||||||
|
} else if(url.includes("file_attacher")) {
|
||||||
|
return "File Attacher";
|
||||||
|
} else if(url.includes("onedrive")) {
|
||||||
|
return "OneDrive Sync";
|
||||||
|
} else if(url.includes("backup.email_archive")) {
|
||||||
|
return "Daily Email Backups";
|
||||||
|
} else if(url.includes("dropbox")) {
|
||||||
|
return "Dropbox Sync";
|
||||||
|
} else if(url.includes("revisions")) {
|
||||||
|
return "Revision History";
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Editors
|
// Editors
|
||||||
|
|
||||||
@@ -127,6 +161,7 @@ class GlobalExtensionsMenu {
|
|||||||
|
|
||||||
modelManager.addItem(ext);
|
modelManager.addItem(ext);
|
||||||
syncManager.sync();
|
syncManager.sync();
|
||||||
|
$scope.serverExtensions.push(ext);
|
||||||
completion();
|
completion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class SNCrypto {
|
|||||||
var pw_cost = this.defaultPasswordGenerationCost();
|
var pw_cost = this.defaultPasswordGenerationCost();
|
||||||
var pw_nonce = this.generateRandomKey(512);
|
var pw_nonce = this.generateRandomKey(512);
|
||||||
var pw_salt = this.sha256([email, pw_nonce].join(":"));
|
var pw_salt = this.sha256([email, pw_nonce].join(":"));
|
||||||
this.generateSymmetricKeyPair({email: email, password: password, pw_salt: pw_salt, pw_cost: pw_cost}, function(keys){
|
this.generateSymmetricKeyPair({password: password, pw_salt: pw_salt, pw_cost: pw_cost}, function(keys){
|
||||||
callback({pw: keys[0], mk: keys[1], ak: keys[2]}, {pw_salt: pw_salt, pw_cost: pw_cost, version: "002"});
|
callback({pw: keys[0], mk: keys[1], ak: keys[2]}, {pw_salt: pw_salt, pw_cost: pw_cost, version: "002"});
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@ class EncryptionHelper {
|
|||||||
return fullCiphertext;
|
return fullCiphertext;
|
||||||
}
|
}
|
||||||
|
|
||||||
static encryptItem(item, keys, version) {
|
static encryptItem(item, keys, version = "002") {
|
||||||
var params = {};
|
var params = {};
|
||||||
// encrypt item key
|
// encrypt item key
|
||||||
var item_key = Neeto.crypto.generateRandomEncryptionKey();
|
var item_key = Neeto.crypto.generateRandomEncryptionKey();
|
||||||
@@ -73,7 +73,10 @@ class EncryptionHelper {
|
|||||||
// is encrypted, continue to below
|
// is encrypted, continue to below
|
||||||
} else {
|
} else {
|
||||||
// is base64 encoded
|
// is base64 encoded
|
||||||
item.content = Neeto.crypto.base64Decode(item.content.substring(3, item.content.length))
|
try {
|
||||||
|
item.content = JSON.parse(Neeto.crypto.base64Decode(item.content.substring(3, item.content.length)));
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
class ExtensionManager {
|
class ExtensionManager {
|
||||||
|
|
||||||
constructor(httpManager, modelManager, authManager, syncManager) {
|
constructor(httpManager, modelManager, authManager, syncManager, storageManager) {
|
||||||
this.httpManager = httpManager;
|
this.httpManager = httpManager;
|
||||||
this.modelManager = modelManager;
|
this.modelManager = modelManager;
|
||||||
this.authManager = authManager;
|
this.authManager = authManager;
|
||||||
this.enabledRepeatActionUrls = JSON.parse(localStorage.getItem("enabledRepeatActionUrls")) || [];
|
this.enabledRepeatActionUrls = JSON.parse(storageManager.getItem("enabledRepeatActionUrls")) || [];
|
||||||
this.decryptedExtensions = JSON.parse(localStorage.getItem("decryptedExtensions")) || [];
|
this.decryptedExtensions = JSON.parse(storageManager.getItem("decryptedExtensions")) || [];
|
||||||
this.syncManager = syncManager;
|
this.syncManager = syncManager;
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
|
||||||
modelManager.addItemSyncObserver("extensionManager", "Extension", function(items){
|
modelManager.addItemSyncObserver("extensionManager", "Extension", function(items){
|
||||||
for (var ext of items) {
|
for (var ext of items) {
|
||||||
@@ -49,7 +50,7 @@ class ExtensionManager {
|
|||||||
this.decryptedExtensions.push(extension.url);
|
this.decryptedExtensions.push(extension.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem("decryptedExtensions", JSON.stringify(this.decryptedExtensions))
|
this.storageManager.setItem("decryptedExtensions", JSON.stringify(this.decryptedExtensions))
|
||||||
|
|
||||||
extension.encrypted = this.extensionUsesEncryptedData(extension);
|
extension.encrypted = this.extensionUsesEncryptedData(extension);
|
||||||
}
|
}
|
||||||
@@ -240,7 +241,7 @@ class ExtensionManager {
|
|||||||
|
|
||||||
disableRepeatAction(action, extension) {
|
disableRepeatAction(action, extension) {
|
||||||
_.pull(this.enabledRepeatActionUrls, action.url);
|
_.pull(this.enabledRepeatActionUrls, action.url);
|
||||||
localStorage.setItem("enabledRepeatActionUrls", JSON.stringify(this.enabledRepeatActionUrls));
|
this.storageManager.setItem("enabledRepeatActionUrls", JSON.stringify(this.enabledRepeatActionUrls));
|
||||||
this.modelManager.removeItemChangeObserver(action.url);
|
this.modelManager.removeItemChangeObserver(action.url);
|
||||||
|
|
||||||
console.assert(this.isRepeatActionEnabled(action) == false);
|
console.assert(this.isRepeatActionEnabled(action) == false);
|
||||||
@@ -249,7 +250,7 @@ class ExtensionManager {
|
|||||||
enableRepeatAction(action, extension) {
|
enableRepeatAction(action, extension) {
|
||||||
if(!_.find(this.enabledRepeatActionUrls, action.url)) {
|
if(!_.find(this.enabledRepeatActionUrls, action.url)) {
|
||||||
this.enabledRepeatActionUrls.push(action.url);
|
this.enabledRepeatActionUrls.push(action.url);
|
||||||
localStorage.setItem("enabledRepeatActionUrls", JSON.stringify(this.enabledRepeatActionUrls));
|
this.storageManager.setItem("enabledRepeatActionUrls", JSON.stringify(this.enabledRepeatActionUrls));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(action.repeat_mode) {
|
if(action.repeat_mode) {
|
||||||
|
|||||||
42
app/assets/javascripts/app/services/filters/sortBy.js
Normal file
42
app/assets/javascripts/app/services/filters/sortBy.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
angular.module('app.frontend')
|
||||||
|
.filter('sortBy', function ($filter) {
|
||||||
|
return function(items, sortBy) {
|
||||||
|
let sortValueFn = (a, b, pinCheck = false) => {
|
||||||
|
if(!pinCheck) {
|
||||||
|
if(a.pinned && b.pinned) {
|
||||||
|
return sortValueFn(a, b, true);
|
||||||
|
}
|
||||||
|
if(a.pinned) { return -1; }
|
||||||
|
if(b.pinned) { return 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
var aValue = a[sortBy] || "";
|
||||||
|
var bValue = b[sortBy] || "";
|
||||||
|
|
||||||
|
let vector = 1;
|
||||||
|
if(sortBy == "title") {
|
||||||
|
aValue = aValue.toLowerCase();
|
||||||
|
bValue = bValue.toLowerCase();
|
||||||
|
|
||||||
|
if(aValue.length == 0 && bValue.length == 0) {
|
||||||
|
return 0;
|
||||||
|
} else if(aValue.length == 0 && bValue.length != 0) {
|
||||||
|
return 1;
|
||||||
|
} else if(aValue.length != 0 && bValue.length == 0) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
vector = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(aValue > bValue) { return -1 * vector;}
|
||||||
|
else if(aValue < bValue) { return 1 * vector;}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
items = items || [];
|
||||||
|
return items.sort(function(a, b){
|
||||||
|
return sortValueFn(a, b);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
class HttpManager {
|
class HttpManager {
|
||||||
|
|
||||||
constructor($timeout) {
|
constructor($timeout, storageManager) {
|
||||||
// calling callbacks in a $timeout allows angular UI to update
|
// calling callbacks in a $timeout allows angular UI to update
|
||||||
this.$timeout = $timeout;
|
this.$timeout = $timeout;
|
||||||
|
this.storageManager = storageManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
setAuthHeadersForRequest(request) {
|
setAuthHeadersForRequest(request) {
|
||||||
var token = localStorage.getItem("jwt");
|
var token = this.storageManager.getItem("jwt");
|
||||||
if(token) {
|
if(token) {
|
||||||
request.setRequestHeader('Authorization', 'Bearer ' + localStorage.getItem("jwt"));
|
request.setRequestHeader('Authorization', 'Bearer ' + token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
class ModelManager {
|
class ModelManager {
|
||||||
|
|
||||||
constructor(dbManager) {
|
constructor(storageManager) {
|
||||||
this.dbManager = dbManager;
|
this.storageManager = storageManager;
|
||||||
this.notes = [];
|
this.notes = [];
|
||||||
this.tags = [];
|
this.tags = [];
|
||||||
this.itemSyncObservers = [];
|
this.itemSyncObservers = [];
|
||||||
this.itemChangeObservers = [];
|
this.itemChangeObservers = [];
|
||||||
this.items = [];
|
this.items = [];
|
||||||
this._extensions = [];
|
this._extensions = [];
|
||||||
this.acceptableContentTypes = ["Note", "Tag", "Extension", "SN|Editor", "SN|Theme", "SN|Component"];
|
this.acceptableContentTypes = ["Note", "Tag", "Extension", "SN|Editor", "SN|Theme", "SN|Component", "SF|Extension"];
|
||||||
|
}
|
||||||
|
|
||||||
|
resetLocalMemory() {
|
||||||
|
this.notes.length = 0;
|
||||||
|
this.tags.length = 0;
|
||||||
|
this.items.length = 0;
|
||||||
|
this._extensions.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get allItems() {
|
get allItems() {
|
||||||
@@ -26,9 +33,14 @@ class ModelManager {
|
|||||||
alternateUUIDForItem(item, callback) {
|
alternateUUIDForItem(item, callback) {
|
||||||
// we need to clone this item and give it a new uuid, then delete item with old uuid from db (you can't mofidy uuid's in our indexeddb setup)
|
// we need to clone this item and give it a new uuid, then delete item with old uuid from db (you can't mofidy uuid's in our indexeddb setup)
|
||||||
var newItem = this.createItem(item);
|
var newItem = this.createItem(item);
|
||||||
|
|
||||||
newItem.uuid = Neeto.crypto.generateUUID();
|
newItem.uuid = Neeto.crypto.generateUUID();
|
||||||
|
|
||||||
|
// Update uuids of relationships
|
||||||
newItem.informReferencesOfUUIDChange(item.uuid, newItem.uuid);
|
newItem.informReferencesOfUUIDChange(item.uuid, newItem.uuid);
|
||||||
|
|
||||||
this.informModelsOfUUIDChangeForItem(newItem, item.uuid, newItem.uuid);
|
this.informModelsOfUUIDChangeForItem(newItem, item.uuid, newItem.uuid);
|
||||||
|
|
||||||
this.removeItemLocally(item, function(){
|
this.removeItemLocally(item, function(){
|
||||||
this.addItem(newItem);
|
this.addItem(newItem);
|
||||||
newItem.setDirty(true);
|
newItem.setDirty(true);
|
||||||
@@ -53,6 +65,12 @@ class ModelManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
itemsForContentType(contentType) {
|
||||||
|
return this.items.filter(function(item){
|
||||||
|
return item.content_type == contentType;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
findItem(itemId) {
|
findItem(itemId) {
|
||||||
return _.find(this.items, {uuid: itemId});
|
return _.find(this.items, {uuid: itemId});
|
||||||
}
|
}
|
||||||
@@ -151,6 +169,8 @@ class ModelManager {
|
|||||||
item = new Theme(json_obj);
|
item = new Theme(json_obj);
|
||||||
} else if(json_obj.content_type == "SN|Component") {
|
} else if(json_obj.content_type == "SN|Component") {
|
||||||
item = new Component(json_obj);
|
item = new Component(json_obj);
|
||||||
|
} else if(json_obj.content_type == "SF|Extension") {
|
||||||
|
item = new SyncAdapter(json_obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
@@ -189,14 +209,16 @@ class ModelManager {
|
|||||||
}.bind(this));
|
}.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
addItem(item) {
|
resortTag(tag) {
|
||||||
this.addItems([item]);
|
_.pull(this.tags, tag);
|
||||||
|
this.tags.splice(_.sortedIndexBy(this.tags, tag, function(tag){
|
||||||
|
if (tag.title) return tag.title.toLowerCase();
|
||||||
|
else return ''
|
||||||
|
}), 0, tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
itemsForContentType(contentType) {
|
addItem(item) {
|
||||||
return this.items.filter(function(item){
|
this.addItems([item]);
|
||||||
return item.content_type == contentType;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveReferencesForItem(item) {
|
resolveReferencesForItem(item) {
|
||||||
@@ -288,7 +310,7 @@ class ModelManager {
|
|||||||
_.pull(this._extensions, item);
|
_.pull(this._extensions, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dbManager.deleteItem(item, callback);
|
this.storageManager.deleteModel(item, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
77
app/assets/javascripts/app/services/passcodeManager.js
Normal file
77
app/assets/javascripts/app/services/passcodeManager.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
angular.module('app.frontend')
|
||||||
|
.provider('passcodeManager', function () {
|
||||||
|
|
||||||
|
this.$get = function($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) {
|
||||||
|
return new PasscodeManager($rootScope, $timeout, modelManager, dbManager, authManager, storageManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PasscodeManager($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) {
|
||||||
|
|
||||||
|
this._hasPasscode = storageManager.getItem("offlineParams", StorageManager.Fixed) != null;
|
||||||
|
this._locked = this._hasPasscode;
|
||||||
|
|
||||||
|
this.isLocked = function() {
|
||||||
|
return this._locked;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hasPasscode = function() {
|
||||||
|
return this._hasPasscode;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.keys = function() {
|
||||||
|
return this._keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.unlock = function(passcode, callback) {
|
||||||
|
var params = JSON.parse(storageManager.getItem("offlineParams", StorageManager.Fixed));
|
||||||
|
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: passcode}, params), function(keys){
|
||||||
|
if(keys.pw !== params.hash) {
|
||||||
|
callback(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._keys = keys;
|
||||||
|
this.decryptLocalStorage(keys);
|
||||||
|
this._locked = false;
|
||||||
|
callback(true);
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setPasscode = function(passcode, callback) {
|
||||||
|
var cost = Neeto.crypto.defaultPasswordGenerationCost();
|
||||||
|
var salt = Neeto.crypto.generateRandomKey(512);
|
||||||
|
var defaultParams = {pw_cost: cost, pw_salt: salt};
|
||||||
|
|
||||||
|
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: passcode}, defaultParams), function(keys) {
|
||||||
|
defaultParams.hash = keys.pw;
|
||||||
|
this._keys = keys;
|
||||||
|
this._hasPasscode = true;
|
||||||
|
|
||||||
|
// Encrypting will initially clear localStorage
|
||||||
|
this.encryptLocalStorage(keys);
|
||||||
|
|
||||||
|
// After it's cleared, it's safe to write to it
|
||||||
|
storageManager.setItem("offlineParams", JSON.stringify(defaultParams), StorageManager.Fixed);
|
||||||
|
callback(true);
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearPasscode = function() {
|
||||||
|
storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.Fixed); // Transfer from Ephemeral
|
||||||
|
storageManager.removeItem("offlineParams", StorageManager.Fixed);
|
||||||
|
this._keys = null;
|
||||||
|
this._hasPasscode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.encryptLocalStorage = function(keys) {
|
||||||
|
storageManager.setKeys(keys);
|
||||||
|
// Switch to Ephemeral storage, wiping Fixed storage
|
||||||
|
storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.decryptLocalStorage = function(keys) {
|
||||||
|
storageManager.setKeys(keys);
|
||||||
|
storageManager.decryptStorage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
221
app/assets/javascripts/app/services/storageManager.js
Normal file
221
app/assets/javascripts/app/services/storageManager.js
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
class MemoryStorage {
|
||||||
|
constructor() {
|
||||||
|
this.memory = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
getItem(key) {
|
||||||
|
return this.memory[key] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get length() {
|
||||||
|
return Object.keys(this.memory).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
setItem(key, value) {
|
||||||
|
this.memory[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(key) {
|
||||||
|
delete this.memory[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.memory = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
keys() {
|
||||||
|
return Object.keys(this.memory);
|
||||||
|
}
|
||||||
|
|
||||||
|
key(index) {
|
||||||
|
return Object.keys(this.memory)[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StorageManager {
|
||||||
|
|
||||||
|
constructor(dbManager) {
|
||||||
|
this.dbManager = dbManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(hasPasscode, ephemeral) {
|
||||||
|
if(hasPasscode) {
|
||||||
|
// We don't want to save anything in fixed storage except for actual item data (in IndexedDB)
|
||||||
|
this.storage = this.memoryStorage;
|
||||||
|
} else if(ephemeral) {
|
||||||
|
// We don't want to save anything in fixed storage as well as IndexedDB
|
||||||
|
this.storage = this.memoryStorage;
|
||||||
|
} else {
|
||||||
|
this.storage = localStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modelStorageMode = ephemeral ? StorageManager.Ephemeral : StorageManager.Fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
get memoryStorage() {
|
||||||
|
if(!this._memoryStorage) {
|
||||||
|
this._memoryStorage = new MemoryStorage();
|
||||||
|
}
|
||||||
|
return this._memoryStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
setItemsMode(mode) {
|
||||||
|
var newStorage = this.getVault(mode);
|
||||||
|
if(newStorage !== this.storage) {
|
||||||
|
// transfer storages
|
||||||
|
var length = this.storage.length;
|
||||||
|
for(var i = 0; i < length; i++) {
|
||||||
|
var key = this.storage.key(i);
|
||||||
|
newStorage.setItem(key, this.storage.getItem(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.storage.clear();
|
||||||
|
this.storage = newStorage;
|
||||||
|
|
||||||
|
if(mode == StorageManager.FixedEncrypted) {
|
||||||
|
this.writeEncryptedStorageToDisk();
|
||||||
|
} else if(mode == StorageManager.Fixed) {
|
||||||
|
// Remove encrypted storage
|
||||||
|
this.removeItem("encryptedStorage", StorageManager.Fixed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getVault(vaultKey) {
|
||||||
|
if(vaultKey) {
|
||||||
|
return this.storageForVault(vaultKey);
|
||||||
|
} else {
|
||||||
|
return this.storage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
storageForVault(vault) {
|
||||||
|
if(vault == StorageManager.Ephemeral || vault == StorageManager.FixedEncrypted) {
|
||||||
|
return this.memoryStorage;
|
||||||
|
} else {
|
||||||
|
return localStorage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setItem(key, value, vault) {
|
||||||
|
var storage = this.getVault(vault);
|
||||||
|
storage.setItem(key, value);
|
||||||
|
|
||||||
|
if(vault === StorageManager.FixedEncrypted) {
|
||||||
|
this.writeEncryptedStorageToDisk();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getItem(key, vault) {
|
||||||
|
var storage = this.getVault(vault);
|
||||||
|
return storage.getItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(key, vault) {
|
||||||
|
var storage = this.getVault(vault);
|
||||||
|
storage.removeItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.memoryStorage.clear();
|
||||||
|
localStorage.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
storageAsHash() {
|
||||||
|
var hash = {};
|
||||||
|
var length = this.storage.length;
|
||||||
|
for(var i = 0; i < length; i++) {
|
||||||
|
var key = this.storage.key(i);
|
||||||
|
hash[key] = this.storage.getItem(key)
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
setKeys(keys) {
|
||||||
|
this.encryptedStorageKeys = keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeEncryptedStorageToDisk() {
|
||||||
|
var encryptedStorage = new EncryptedStorage();
|
||||||
|
// Copy over totality of current storage
|
||||||
|
encryptedStorage.storage = this.storageAsHash();
|
||||||
|
// Save new encrypted storage in Fixed storage
|
||||||
|
var params = new ItemParams(encryptedStorage, this.encryptedStorageKeys);
|
||||||
|
this.setItem("encryptedStorage", JSON.stringify(params.paramsForSync()), StorageManager.Fixed);
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptStorage() {
|
||||||
|
var stored = JSON.parse(this.getItem("encryptedStorage", StorageManager.Fixed));
|
||||||
|
EncryptionHelper.decryptItem(stored, this.encryptedStorageKeys);
|
||||||
|
var encryptedStorage = new EncryptedStorage(stored);
|
||||||
|
|
||||||
|
for(var key of Object.keys(encryptedStorage.storage)) {
|
||||||
|
this.setItem(key, encryptedStorage.storage[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPasscode() {
|
||||||
|
return this.getItem("encryptedStorage", StorageManager.Fixed) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Model Storage
|
||||||
|
|
||||||
|
If using ephemeral storage, we don't need to write it to anything as references will be held already by controllers
|
||||||
|
and the global modelManager service.
|
||||||
|
*/
|
||||||
|
|
||||||
|
setModelStorageMode(mode) {
|
||||||
|
if(mode == this.modelStorageMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mode == StorageManager.Ephemeral) {
|
||||||
|
// Clear IndexedDB
|
||||||
|
this.dbManager.clearAllModels(null);
|
||||||
|
} else {
|
||||||
|
// Fixed
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modelStorageMode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllModels(callback) {
|
||||||
|
if(this.modelStorageMode == StorageManager.Fixed) {
|
||||||
|
this.dbManager.getAllModels(callback);
|
||||||
|
} else {
|
||||||
|
callback && callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveModel(item) {
|
||||||
|
this.saveModels([item]);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveModels(items, callback) {
|
||||||
|
if(this.modelStorageMode == StorageManager.Fixed) {
|
||||||
|
this.dbManager.saveModels(items, callback);
|
||||||
|
} else {
|
||||||
|
callback && callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteModel(item, callback) {
|
||||||
|
if(this.modelStorageMode == StorageManager.Fixed) {
|
||||||
|
this.dbManager.deleteModel(item, callback);
|
||||||
|
} else {
|
||||||
|
callback && callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAllModels(callback) {
|
||||||
|
this.dbManager.clearAllModels(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StorageManager.FixedEncrypted = "FixedEncrypted"; // encrypted memoryStorage + localStorage persistence
|
||||||
|
StorageManager.Ephemeral = "Ephemeral"; // memoryStorage
|
||||||
|
StorageManager.Fixed = "Fixed"; // localStorage
|
||||||
|
|
||||||
|
angular.module('app.frontend').service('storageManager', StorageManager);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
class SyncManager {
|
class SyncManager {
|
||||||
|
|
||||||
constructor($rootScope, modelManager, authManager, dbManager, httpManager, $interval, $timeout) {
|
constructor($rootScope, modelManager, authManager, dbManager, httpManager, $interval, $timeout, storageManager, passcodeManager) {
|
||||||
this.$rootScope = $rootScope;
|
this.$rootScope = $rootScope;
|
||||||
this.httpManager = httpManager;
|
this.httpManager = httpManager;
|
||||||
this.modelManager = modelManager;
|
this.modelManager = modelManager;
|
||||||
@@ -8,25 +8,33 @@ class SyncManager {
|
|||||||
this.dbManager = dbManager;
|
this.dbManager = dbManager;
|
||||||
this.$interval = $interval;
|
this.$interval = $interval;
|
||||||
this.$timeout = $timeout;
|
this.$timeout = $timeout;
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
this.passcodeManager = passcodeManager;
|
||||||
this.syncStatus = {};
|
this.syncStatus = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
get serverURL() {
|
get serverURL() {
|
||||||
return localStorage.getItem("server") || window._default_sf_server;
|
return this.storageManager.getItem("server") || window._default_sf_server;
|
||||||
}
|
}
|
||||||
|
|
||||||
get masterKey() {
|
get masterKey() {
|
||||||
return localStorage.getItem("mk");
|
return this.storageManager.getItem("mk");
|
||||||
}
|
}
|
||||||
|
|
||||||
get serverPassword() {
|
get serverPassword() {
|
||||||
return localStorage.getItem("pw");
|
return this.storageManager.getItem("pw");
|
||||||
}
|
}
|
||||||
|
|
||||||
writeItemsToLocalStorage(items, offlineOnly, callback) {
|
writeItemsToLocalStorage(items, offlineOnly, callback) {
|
||||||
var version = this.authManager.protocolVersion();
|
if(items.length == 0) {
|
||||||
|
callback && callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Use null to use the latest protocol version if offline
|
||||||
|
var version = this.authManager.offline() ? null : this.authManager.protocolVersion();
|
||||||
|
var keys = this.authManager.offline() ? this.passcodeManager.keys() : this.authManager.keys();
|
||||||
var params = items.map(function(item) {
|
var params = items.map(function(item) {
|
||||||
var itemParams = new ItemParams(item, null, version);
|
var itemParams = new ItemParams(item, keys, version);
|
||||||
itemParams = itemParams.paramsForLocalStorage();
|
itemParams = itemParams.paramsForLocalStorage();
|
||||||
if(offlineOnly) {
|
if(offlineOnly) {
|
||||||
delete itemParams.dirty;
|
delete itemParams.dirty;
|
||||||
@@ -34,12 +42,12 @@ class SyncManager {
|
|||||||
return itemParams;
|
return itemParams;
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
this.dbManager.saveItems(params, callback);
|
this.storageManager.saveModels(params, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadLocalItems(callback) {
|
loadLocalItems(callback) {
|
||||||
var params = this.dbManager.getAllItems(function(items){
|
var params = this.storageManager.getAllModels(function(items){
|
||||||
var items = this.handleItemsResponse(items, null, null);
|
var items = this.handleItemsResponse(items, null);
|
||||||
Item.sortItemsByDate(items);
|
Item.sortItemsByDate(items);
|
||||||
callback(items);
|
callback(items);
|
||||||
}.bind(this))
|
}.bind(this))
|
||||||
@@ -61,12 +69,40 @@ class SyncManager {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
markAllItemsDirtyAndSaveOffline(callback) {
|
/*
|
||||||
var items = this.modelManager.allItems;
|
In the case of signing in and merging local data, we alternative UUIDs
|
||||||
for(var item of items) {
|
to avoid overwriting data a user may retrieve that has the same UUID.
|
||||||
item.setDirty(true);
|
Alternating here forces us to to create duplicates of the items instead.
|
||||||
|
*/
|
||||||
|
markAllItemsDirtyAndSaveOffline(callback, alternateUUIDs) {
|
||||||
|
var originalItems = this.modelManager.allItems;
|
||||||
|
|
||||||
|
var block = (items) => {
|
||||||
|
for(var item of items) {
|
||||||
|
item.setDirty(true);
|
||||||
|
}
|
||||||
|
this.writeItemsToLocalStorage(items, false, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(alternateUUIDs) {
|
||||||
|
var index = 0;
|
||||||
|
|
||||||
|
let alternateNextItem = () => {
|
||||||
|
if(index >= originalItems.length) {
|
||||||
|
// We don't use originalItems as altnerating UUID will have deleted them.
|
||||||
|
block(this.modelManager.allItems);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = originalItems[index];
|
||||||
|
this.modelManager.alternateUUIDForItem(item, alternateNextItem);
|
||||||
|
++index;
|
||||||
|
}
|
||||||
|
|
||||||
|
alternateNextItem();
|
||||||
|
} else {
|
||||||
|
block(originalItems);
|
||||||
}
|
}
|
||||||
this.writeItemsToLocalStorage(items, false, callback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get syncURL() {
|
get syncURL() {
|
||||||
@@ -75,12 +111,12 @@ class SyncManager {
|
|||||||
|
|
||||||
set syncToken(token) {
|
set syncToken(token) {
|
||||||
this._syncToken = token;
|
this._syncToken = token;
|
||||||
localStorage.setItem("syncToken", token);
|
this.storageManager.setItem("syncToken", token);
|
||||||
}
|
}
|
||||||
|
|
||||||
get syncToken() {
|
get syncToken() {
|
||||||
if(!this._syncToken) {
|
if(!this._syncToken) {
|
||||||
this._syncToken = localStorage.getItem("syncToken");
|
this._syncToken = this.storageManager.getItem("syncToken");
|
||||||
}
|
}
|
||||||
return this._syncToken;
|
return this._syncToken;
|
||||||
}
|
}
|
||||||
@@ -88,15 +124,15 @@ class SyncManager {
|
|||||||
set cursorToken(token) {
|
set cursorToken(token) {
|
||||||
this._cursorToken = token;
|
this._cursorToken = token;
|
||||||
if(token) {
|
if(token) {
|
||||||
localStorage.setItem("cursorToken", token);
|
this.storageManager.setItem("cursorToken", token);
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem("cursorToken");
|
this.storageManager.removeItem("cursorToken");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get cursorToken() {
|
get cursorToken() {
|
||||||
if(!this._cursorToken) {
|
if(!this._cursorToken) {
|
||||||
this._cursorToken = localStorage.getItem("cursorToken");
|
this._cursorToken = this.storageManager.getItem("cursorToken");
|
||||||
}
|
}
|
||||||
return this._cursorToken;
|
return this._cursorToken;
|
||||||
}
|
}
|
||||||
@@ -129,7 +165,7 @@ class SyncManager {
|
|||||||
this.syncStatus.checker = this.$interval(function(){
|
this.syncStatus.checker = this.$interval(function(){
|
||||||
// check to see if the ongoing sync is taking too long, alert the user
|
// check to see if the ongoing sync is taking too long, alert the user
|
||||||
var secondsPassed = (new Date() - this.syncStatus.syncStart) / 1000;
|
var secondsPassed = (new Date() - this.syncStatus.syncStart) / 1000;
|
||||||
var warningThreshold = 5; // seconds
|
var warningThreshold = 5.0; // seconds
|
||||||
if(secondsPassed > warningThreshold) {
|
if(secondsPassed > warningThreshold) {
|
||||||
this.$rootScope.$broadcast("sync:taking-too-long");
|
this.$rootScope.$broadcast("sync:taking-too-long");
|
||||||
this.stopCheckingIfSyncIsTakingTooLong();
|
this.stopCheckingIfSyncIsTakingTooLong();
|
||||||
@@ -159,7 +195,6 @@ class SyncManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// we want to write all dirty items to disk only if the user is offline, or if the sync op fails
|
// we want to write all dirty items to disk only if the user is offline, or if the sync op fails
|
||||||
// if the sync op succeeds, these items will be written to disk by handling the "saved_items" response from the server
|
// if the sync op succeeds, these items will be written to disk by handling the "saved_items" response from the server
|
||||||
if(this.authManager.offline()) {
|
if(this.authManager.offline()) {
|
||||||
@@ -288,7 +323,8 @@ class SyncManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleItemsResponse(responseItems, omitFields) {
|
handleItemsResponse(responseItems, omitFields) {
|
||||||
EncryptionHelper.decryptMultipleItems(responseItems, this.authManager.keys());
|
var keys = this.authManager.keys() || this.passcodeManager.keys();
|
||||||
|
EncryptionHelper.decryptMultipleItems(responseItems, keys);
|
||||||
var items = this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields);
|
var items = this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields);
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -302,45 +338,64 @@ class SyncManager {
|
|||||||
|
|
||||||
var i = 0;
|
var i = 0;
|
||||||
var handleNext = function() {
|
var handleNext = function() {
|
||||||
if (i < unsaved.length) {
|
if(i >= unsaved.length) {
|
||||||
var mapping = unsaved[i];
|
// Handled all items
|
||||||
var itemResponse = mapping.item;
|
|
||||||
EncryptionHelper.decryptMultipleItems([itemResponse], this.authManager.keys());
|
|
||||||
var item = this.modelManager.findItem(itemResponse.uuid);
|
|
||||||
if(!item) {
|
|
||||||
// could be deleted
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var error = mapping.error;
|
|
||||||
if(error.tag == "uuid_conflict") {
|
|
||||||
// uuid conflicts can occur if a user attempts to import an old data archive with uuids from the old account into a new account
|
|
||||||
this.modelManager.alternateUUIDForItem(item, handleNext);
|
|
||||||
} else if(error.tag === "sync_conflict") {
|
|
||||||
// create a new item with the same contents of this item if the contents differ
|
|
||||||
itemResponse.uuid = null; // we want a new uuid for the new item
|
|
||||||
var dup = this.modelManager.createItem(itemResponse);
|
|
||||||
if(!itemResponse.deleted && JSON.stringify(item.structureParams()) !== JSON.stringify(dup.structureParams())) {
|
|
||||||
this.modelManager.addItem(dup);
|
|
||||||
dup.conflict_of = item.uuid;
|
|
||||||
dup.setDirty(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
++i;
|
|
||||||
} else {
|
|
||||||
this.sync(null, {additionalFields: ["created_at", "updated_at"]});
|
this.sync(null, {additionalFields: ["created_at", "updated_at"]});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var handled = false;
|
||||||
|
var mapping = unsaved[i];
|
||||||
|
var itemResponse = mapping.item;
|
||||||
|
EncryptionHelper.decryptMultipleItems([itemResponse], this.authManager.keys());
|
||||||
|
var item = this.modelManager.findItem(itemResponse.uuid);
|
||||||
|
|
||||||
|
if(!item) {
|
||||||
|
// Could be deleted
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var error = mapping.error;
|
||||||
|
|
||||||
|
if(error.tag === "uuid_conflict") {
|
||||||
|
// UUID conflicts can occur if a user attempts to
|
||||||
|
// import an old data archive with uuids from the old account into a new account
|
||||||
|
handled = true;
|
||||||
|
this.modelManager.alternateUUIDForItem(item, handleNext);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if(error.tag === "sync_conflict") {
|
||||||
|
// Create a new item with the same contents of this item if the contents differ
|
||||||
|
|
||||||
|
// We want a new uuid for the new item. Note that this won't neccessarily adjust references.
|
||||||
|
itemResponse.uuid = null;
|
||||||
|
|
||||||
|
var dup = this.modelManager.createItem(itemResponse);
|
||||||
|
if(!itemResponse.deleted && JSON.stringify(item.structureParams()) !== JSON.stringify(dup.structureParams())) {
|
||||||
|
this.modelManager.addItem(dup);
|
||||||
|
dup.conflict_of = item.uuid;
|
||||||
|
dup.setDirty(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
++i;
|
||||||
|
|
||||||
|
if(!handled) {
|
||||||
|
handleNext();
|
||||||
|
}
|
||||||
|
|
||||||
}.bind(this);
|
}.bind(this);
|
||||||
|
|
||||||
handleNext();
|
handleNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSyncToken() {
|
clearSyncToken() {
|
||||||
localStorage.removeItem("syncToken");
|
this.storageManager.removeItem("syncToken");
|
||||||
}
|
}
|
||||||
|
|
||||||
destroyLocalData(callback) {
|
destroyLocalData(callback) {
|
||||||
localStorage.clear();
|
this.storageManager.clear();
|
||||||
this.dbManager.clearAllItems(function(){
|
this.storageManager.clearAllModels(function(){
|
||||||
if(callback) {
|
if(callback) {
|
||||||
this.$timeout(function(){
|
this.$timeout(function(){
|
||||||
callback();
|
callback();
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
class ThemeManager {
|
class ThemeManager {
|
||||||
|
|
||||||
constructor(modelManager, syncManager, $rootScope) {
|
constructor(modelManager, syncManager, $rootScope, storageManager) {
|
||||||
this.syncManager = syncManager;
|
this.syncManager = syncManager;
|
||||||
this.modelManager = modelManager;
|
this.modelManager = modelManager;
|
||||||
this.$rootScope = $rootScope;
|
this.$rootScope = $rootScope;
|
||||||
|
this.storageManager = storageManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
get themes() {
|
get themes() {
|
||||||
@@ -16,7 +17,7 @@ class ThemeManager {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
get activeTheme() {
|
get activeTheme() {
|
||||||
var activeThemeId = localStorage.getItem("activeTheme");
|
var activeThemeId = this.storageManager.getItem("activeTheme");
|
||||||
if(!activeThemeId) {
|
if(!activeThemeId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -53,14 +54,14 @@ class ThemeManager {
|
|||||||
link.media = "screen,print";
|
link.media = "screen,print";
|
||||||
link.id = theme.uuid;
|
link.id = theme.uuid;
|
||||||
document.getElementsByTagName("head")[0].appendChild(link);
|
document.getElementsByTagName("head")[0].appendChild(link);
|
||||||
localStorage.setItem("activeTheme", theme.uuid);
|
this.storageManager.setItem("activeTheme", theme.uuid);
|
||||||
|
|
||||||
this.currentTheme = theme;
|
this.currentTheme = theme;
|
||||||
this.$rootScope.$broadcast("theme-changed");
|
this.$rootScope.$broadcast("theme-changed");
|
||||||
}
|
}
|
||||||
|
|
||||||
deactivateTheme(theme) {
|
deactivateTheme(theme) {
|
||||||
localStorage.removeItem("activeTheme");
|
this.storageManager.removeItem("activeTheme");
|
||||||
var element = document.getElementById(theme.uuid);
|
var element = document.getElementById(theme.uuid);
|
||||||
if(element) {
|
if(element) {
|
||||||
element.disabled = true;
|
element.disabled = true;
|
||||||
@@ -72,7 +73,7 @@ class ThemeManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isThemeActive(theme) {
|
isThemeActive(theme) {
|
||||||
return localStorage.getItem("activeTheme") === theme.uuid;
|
return this.storageManager.getItem("activeTheme") === theme.uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
fileNameFromPath(filePath) {
|
fileNameFromPath(filePath) {
|
||||||
|
|||||||
2966
app/assets/stylesheets/_ionicons.scss
Normal file
2966
app/assets/stylesheets/_ionicons.scss
Normal file
File diff suppressed because one or more lines are too long
@@ -24,14 +24,17 @@ $heading-height: 75px;
|
|||||||
|
|
||||||
#editor-title-bar {
|
#editor-title-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 20px;
|
|
||||||
|
padding-top: 14px;
|
||||||
|
padding-left: 14px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
||||||
height: auto;
|
height: auto;
|
||||||
padding-right: 10px;
|
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|
||||||
&.fullscreen {
|
&.fullscreen {
|
||||||
|
|||||||
@@ -53,25 +53,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.blue-box {
|
|
||||||
background-color: $blue-color;
|
|
||||||
color: white;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 16px 20px;
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: white;
|
|
||||||
color: $blue-color;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 6px 20px;
|
|
||||||
width: 100%;
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-link {
|
.dashboard-link {
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
.fake-link {
|
.fake-link {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: $blue-color;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
@@ -31,7 +30,6 @@ h2 {
|
|||||||
a {
|
a {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: $blue-color;
|
|
||||||
|
|
||||||
&.gray {
|
&.gray {
|
||||||
color: $dark-gray;
|
color: $dark-gray;
|
||||||
@@ -140,7 +138,6 @@ button.light {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid rgba(gray, 0.15);
|
border: 1px solid rgba(gray, 0.15);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: $blue-color;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(gray, 0.10);
|
background-color: rgba(gray, 0.10);
|
||||||
@@ -170,7 +167,11 @@ a.disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.icon.ion-locked {
|
||||||
|
margin-left: 5px;
|
||||||
|
border-left: 1px solid gray;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -211,7 +212,7 @@ a.disabled {
|
|||||||
border-right-color: transparent;
|
border-right-color: transparent;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
&.blue {
|
&.tinted {
|
||||||
border: 1px solid $blue-color;
|
border: 1px solid $blue-color;
|
||||||
border-right-color: transparent;
|
border-right-color: transparent;
|
||||||
}
|
}
|
||||||
|
|||||||
51
app/assets/stylesheets/app/_lock-screen.scss
Normal file
51
app/assets/stylesheets/app/_lock-screen.scss
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#lock-screen {
|
||||||
|
position: fixed;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: rgba(white, 0.5);
|
||||||
|
color: black;
|
||||||
|
font-size: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.background {
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
// box-shadow: 0 3px 3px rgba(0, 0, 0, 0.175);
|
||||||
|
border: 1px solid rgba(black, 0.1);
|
||||||
|
background-color: white;
|
||||||
|
width: 300px;
|
||||||
|
// height: 500px;
|
||||||
|
margin: auto;
|
||||||
|
padding: 10px 30px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
// position: absolute;
|
||||||
|
// top: 0; left: 0; bottom: 0; right: 0;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,10 +29,33 @@ $blue-color: #086dd6;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.blue {
|
.tinted {
|
||||||
color: $blue-color;
|
color: $blue-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tinted-selected {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tinted-box {
|
||||||
|
background-color: $blue-color;
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: white;
|
||||||
|
color: $blue-color;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 6px 20px;
|
||||||
|
width: 100%;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont,
|
font-family: -apple-system, BlinkMacSystemFont,
|
||||||
@@ -67,6 +90,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
color: $blue-color;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;;
|
text-decoration: underline;;
|
||||||
@@ -164,3 +188,7 @@ $section-header-height: 70px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
ul.section-menu-bar {
|
ul.section-menu-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
padding-left: 15px;
|
padding-left: 6px;
|
||||||
padding-right: 21px;
|
padding-right: 21px;
|
||||||
|
|
||||||
background-color: #f1f1f1;
|
background-color: #f1f1f1;
|
||||||
@@ -63,10 +63,13 @@ ul.section-menu-bar {
|
|||||||
|
|
||||||
> li {
|
> li {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 35px;
|
height: 40px;
|
||||||
|
padding-top: 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
border-bottom: 1px solid rgba(black, 0.1);
|
||||||
|
|
||||||
color: $selected-text-color;
|
color: $selected-text-color;
|
||||||
float: left;
|
float: left;
|
||||||
|
|
||||||
@@ -103,10 +106,6 @@ ul.section-menu-bar {
|
|||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
max-height: calc(85vh - 90px);
|
max-height: calc(85vh - 90px);
|
||||||
|
|
||||||
&:not(:first-child) {
|
|
||||||
margin-top: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
@@ -143,7 +142,7 @@ ul.section-menu-bar {
|
|||||||
background-color: $blue-color;
|
background-color: $blue-color;
|
||||||
|
|
||||||
|
|
||||||
.blue {
|
.tinted {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
#notes-title-bar {
|
#notes-title-bar {
|
||||||
color: rgba(black, 0.40);
|
color: rgba(black, 0.40);
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
|
padding-left: 14px;
|
||||||
|
padding-right: 14px;
|
||||||
height: $notes-title-bar-height;
|
height: $notes-title-bar-height;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@@ -21,14 +23,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#notes-add-button {
|
#notes-add-button {
|
||||||
right: 20px;
|
right: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tag-menu-bar {
|
#tag-menu-bar {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 -20px;
|
margin: 0 -14px;
|
||||||
width: auto;
|
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
|
// padding-left: 4px;
|
||||||
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-section {
|
.filter-section {
|
||||||
@@ -101,6 +104,20 @@
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags-string {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pinned {
|
||||||
|
.icon {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.note-preview {
|
.note-preview {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
@@ -117,6 +134,10 @@
|
|||||||
&.selected {
|
&.selected {
|
||||||
background-color: $blue-color;
|
background-color: $blue-color;
|
||||||
color: white;
|
color: white;
|
||||||
|
|
||||||
|
.pinned {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,9 +55,6 @@
|
|||||||
|
|
||||||
.status {
|
.status {
|
||||||
color: orange;
|
color: orange;
|
||||||
&.trusted {
|
|
||||||
color: $blue-color;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
@@ -74,7 +71,7 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.blue {
|
&.tinted {
|
||||||
background-color: $blue-color;
|
background-color: $blue-color;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
margin-top: 2px !important;
|
margin-top: 2px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mt-3 {
|
||||||
|
margin-top: 3px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.mt-5 {
|
.mt-5 {
|
||||||
margin-top: 5px !important;
|
margin-top: 5px !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,3 +10,6 @@ $dark-gray: #2e2e2e;
|
|||||||
@import "app/extensions";
|
@import "app/extensions";
|
||||||
@import "app/menus";
|
@import "app/menus";
|
||||||
@import "app/permissions-modal";
|
@import "app/permissions-modal";
|
||||||
|
@import "app/lock-screen";
|
||||||
|
|
||||||
|
@import "ionicons";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
.mb-10
|
.mb-10
|
||||||
|
|
||||||
.step-one{"ng-if" => "!formData.showLogin && !formData.showRegister"}
|
.step-one{"ng-if" => "!formData.showLogin && !formData.showRegister"}
|
||||||
%h3 Sign in or register to enable sync and encryption.
|
%h3 Sign in or register to enable sync and end-to-end encryption.
|
||||||
.small-v-space
|
.small-v-space
|
||||||
|
|
||||||
.button-group.mt-5
|
.button-group.mt-5
|
||||||
@@ -31,10 +31,14 @@
|
|||||||
%label.pull-left Sync Server Domain
|
%label.pull-left Sync Server Domain
|
||||||
%input.form-control.mt-5{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'formData.url'}
|
%input.form-control.mt-5{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'formData.url'}
|
||||||
|
|
||||||
.checkbox.mt-10{"ng-if" => "localNotesCount() > 0"}
|
.checkbox.mt-10
|
||||||
|
%p
|
||||||
|
%input{"type" => "checkbox", "ng-model" => "formData.ephemeral", "ng-true-value" => "false", "ng-false-value" => "true"}
|
||||||
|
Stay signed in
|
||||||
|
.checkbox.mt-10{"ng-if" => "notesAndTagsCount() > 0"}
|
||||||
%p
|
%p
|
||||||
%input{"type" => "checkbox", "ng-model" => "formData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"}
|
%input{"type" => "checkbox", "ng-model" => "formData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"}
|
||||||
Merge local notes ({{localNotesCount()}} notes)
|
Merge local data ({{notesAndTagsCount()}} notes and tags)
|
||||||
%button.ui-button.block.mt-10{"ng-click" => "submitAuthForm()"} {{formData.showLogin ? "Sign In" : "Register"}}
|
%button.ui-button.block.mt-10{"ng-click" => "submitAuthForm()"} {{formData.showLogin ? "Sign In" : "Register"}}
|
||||||
|
|
||||||
.mt-15{"ng-if" => "formData.showRegister"}
|
.mt-15{"ng-if" => "formData.showRegister"}
|
||||||
@@ -48,8 +52,8 @@
|
|||||||
%div{"ng-if" => "user"}
|
%div{"ng-if" => "user"}
|
||||||
%h2 {{user.email}}
|
%h2 {{user.email}}
|
||||||
%p {{server}}
|
%p {{server}}
|
||||||
%div.bold.mt-10.blue{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress || syncStatus.needsMoreSync", "delay" => "1000"}
|
%div.bold.mt-10.tinted{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress || syncStatus.needsMoreSync", "delay" => "1000"}
|
||||||
.spinner.inline.mr-5.blue
|
.spinner.inline.mr-5.tinted
|
||||||
{{"Syncing" + (syncStatus.total > 0 ? ":" : "")}}
|
{{"Syncing" + (syncStatus.total > 0 ? ":" : "")}}
|
||||||
%span{"ng-if" => "syncStatus.total > 0"} {{syncStatus.current}}/{{syncStatus.total}}
|
%span{"ng-if" => "syncStatus.total > 0"} {{syncStatus.current}}/{{syncStatus.total}}
|
||||||
%p.bold.mt-10.red.block{"ng-if" => "syncStatus.error"} Error syncing: {{syncStatus.error.message}}
|
%p.bold.mt-10.red.block{"ng-if" => "syncStatus.error"} Error syncing: {{syncStatus.error.message}}
|
||||||
@@ -104,20 +108,37 @@
|
|||||||
%input.form-control{:type => 'password', "ng-model" => "securityUpdateData.password", "placeholder" => "Enter password"}
|
%input.form-control{:type => 'password', "ng-model" => "securityUpdateData.password", "placeholder" => "Enter password"}
|
||||||
%button.ui-button.block{"ng-click" => "submitSecurityUpdateForm()"} Update
|
%button.ui-button.block{"ng-click" => "submitSecurityUpdateForm()"} Update
|
||||||
%div.mt-5{"ng-if" => "securityUpdateData.processing"}
|
%div.mt-5{"ng-if" => "securityUpdateData.processing"}
|
||||||
%p.blue Processing...
|
%p.tinted Processing...
|
||||||
|
|
||||||
|
|
||||||
|
.mt-25
|
||||||
|
%h4 Encryption Status
|
||||||
|
%p
|
||||||
|
{{encryptionStatusString()}}
|
||||||
|
%div.mt-5{"ng-if" => "encryptionEnabled()"}
|
||||||
|
%i {{encryptionStatusForNotes()}}
|
||||||
|
|
||||||
|
.mt-25
|
||||||
|
%h4 Passcode Lock
|
||||||
|
%div{"ng-if" => "!hasPasscode() && passcodeOptionAvailable()"}
|
||||||
|
%p Add an app passcode to lock the app and encrypt on-device key storage.
|
||||||
|
%a.block.mt-5{"ng-click" => "addPasscodeClicked()", "ng-if" => "!formData.showPasscodeForm"} Add Passcode
|
||||||
|
|
||||||
|
.mt-5{"ng-if" => "formData.showPasscodeForm"}
|
||||||
|
%p.bold Choose a passcode:
|
||||||
|
%input.form-control.mt-10{:type => 'password', "ng-model" => "formData.passcode", "placeholder" => "Passcode", "autofocus" => "true"}
|
||||||
|
%input.form-control.mt-10{:type => 'password', "ng-model" => "formData.confirmPasscode", "placeholder" => "Confirm Passcode"}
|
||||||
|
%button.standard.ui-button.block.tinted.mt-5{"ng-click" => "submitPasscodeForm()"} Set Passcode
|
||||||
|
%div{"ng-if" => "hasPasscode()"}
|
||||||
|
%p
|
||||||
|
Passcode lock is enabled.
|
||||||
|
%span{"ng-if" => "isDesktopApplication()"} Your passcode will be required on new sessions after app quit.
|
||||||
|
%a.block.mt-5{"ng-click" => "removePasscodePressed()"} Remove Passcode
|
||||||
|
%div{"ng-if" => "!passcodeOptionAvailable()"}
|
||||||
|
%p Passcode lock is only available to permanent sessions. (You chose not to stay signed in.)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.medium-v-space
|
|
||||||
|
|
||||||
%h4 Local Encryption
|
|
||||||
%p Notes are encrypted locally before being sent to the server. Neither the server owner nor an intrusive entity can decrypt your locally encrypted notes.
|
|
||||||
%div.mt-5
|
|
||||||
%label Status:
|
|
||||||
{{encryptionStatusForNotes()}}
|
|
||||||
|
|
||||||
.mt-25{"ng-if" => "!importData.loading"}
|
.mt-25{"ng-if" => "!importData.loading"}
|
||||||
%h4 Data Archives
|
%h4 Data Archives
|
||||||
.mt-5{"ng-if" => "user"}
|
.mt-5{"ng-if" => "user"}
|
||||||
@@ -132,25 +153,15 @@
|
|||||||
|
|
||||||
%label.block.mt-5
|
%label.block.mt-5
|
||||||
%input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"}
|
%input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"}
|
||||||
.fake-link Import Data from Archive
|
.fake-link.tinted Import Data from Archive
|
||||||
|
|
||||||
%div{"ng-if" => "importData.requestPassword"}
|
%div{"ng-if" => "importData.requestPassword"}
|
||||||
%p Enter the account password associated with the import file.
|
%p Enter the account password associated with the import file.
|
||||||
%input.form-control.mt-5{:type => 'password', "ng-model" => "importData.password"}
|
%input.form-control.mt-5{:type => 'password', "ng-model" => "importData.password", "autofocus" => "true"}
|
||||||
%button.standard.ui-button.block.blue.mt-5{"ng-click" => "submitImportPassword()"} Decrypt & Import
|
%button.standard.ui-button.block.tinted.mt-5{"ng-click" => "submitImportPassword()"} Decrypt & Import
|
||||||
|
|
||||||
%p.mt-5{"ng-if" => "user"} Notes are downloaded in the Standard File format, which allows you to re-import back into this app easily. To download as plain text files, choose "Decrypted".
|
%p.mt-5{"ng-if" => "user"} Notes are downloaded in the Standard File format, which allows you to re-import back into this app easily. To download as plain text files, choose "Decrypted".
|
||||||
|
|
||||||
.spinner.mt-10{"ng-if" => "importData.loading"}
|
.spinner.mt-10{"ng-if" => "importData.loading"}
|
||||||
|
|
||||||
.mt-25
|
|
||||||
%h4 Analytics
|
|
||||||
%p
|
|
||||||
Help Standard Notes improve by sending anonymous data on general usage.
|
|
||||||
%a{"href" => "https://standardnotes.org/privacy", "target" => "_blank"} Learn more.
|
|
||||||
%div.mt-5
|
|
||||||
%label Status:
|
|
||||||
{{analyticsManager.enabled ? "Enabled" : "Disabled"}}
|
|
||||||
%a{"ng-click" => "analyticsManager.toggleStatus()"} {{analyticsManager.enabled ? "Disable" : "Enable"}}
|
|
||||||
|
|
||||||
%a.block.mt-25.red{"ng-click" => "destroyLocalData()"} {{ user ? "Sign out and clear local data" : "Clear all local data" }}
|
%a.block.mt-25.red{"ng-click" => "destroyLocalData()"} {{ user ? "Sign out and clear local data" : "Clear all local data" }}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
%ul{"ng-if" => "!extension.hide"}
|
%ul{"ng-if" => "!extension.hide"}
|
||||||
%li.menu-item{"ng-repeat" => "action in extension.actionsWithContextForItem(item)", "ng-click" => "executeAction(action, extension);",
|
%li.menu-item{"ng-repeat" => "action in extension.actionsWithContextForItem(item)", "ng-click" => "executeAction(action, extension);",
|
||||||
"ng-class" => "{'faded' : !isActionEnabled(action, extension)}"}
|
"ng-class" => "{'faded' : !isActionEnabled(action, extension)}"}
|
||||||
.menu-item-title {{action.label}}
|
%label.menu-item-title {{action.label}}
|
||||||
.menu-item-subtitle {{action.desc}}
|
.menu-item-subtitle {{action.desc}}
|
||||||
|
|
||||||
.small.normal{"ng-if" => "!isActionEnabled(action, extension)"}
|
.small.normal{"ng-if" => "!isActionEnabled(action, extension)"}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
%div{"ng-if" => "action.showNestedActions"}
|
%div{"ng-if" => "action.showNestedActions"}
|
||||||
%ul.mt-10
|
%ul.mt-10
|
||||||
%li.menu-item.white-bg.nested-hover{"ng-repeat" => "subaction in action.subactions", "ng-click" => "executeAction(subaction, extension, action); $event.stopPropagation();", "style" => "margin-top: -1px;"}
|
%li.menu-item.white-bg.nested-hover{"ng-repeat" => "subaction in action.subactions", "ng-click" => "executeAction(subaction, extension, action); $event.stopPropagation();", "style" => "margin-top: -1px;"}
|
||||||
.menu-item-title {{subaction.label}}
|
%label.menu-item-title {{subaction.label}}
|
||||||
.menu-item-subtitle {{subaction.desc}}
|
.menu-item-subtitle {{subaction.desc}}
|
||||||
%span{"ng-if" => "subaction.running"}
|
%span{"ng-if" => "subaction.running"}
|
||||||
.spinner{"style" => "margin-top: 3px;"}
|
.spinner{"style" => "margin-top: 3px;"}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
%ul
|
%ul
|
||||||
%li.menu-item{"ng-repeat" => "editor in editorManager.systemEditors", "ng-click" => "selectEditor($event, editor)"}
|
%li.menu-item{"ng-repeat" => "editor in editorManager.systemEditors", "ng-click" => "selectEditor($event, editor)"}
|
||||||
%span.pull-left.mr-10{"ng-if" => "selectedEditor === editor"} ✓
|
%span.pull-left.mr-10{"ng-if" => "selectedEditor === editor"} ✓
|
||||||
.menu-item-title.pull-left {{editor.name}}
|
%label.menu-item-title.pull-left {{editor.name}}
|
||||||
|
|
||||||
%div{"ng-if" => "editorManager.externalEditors.length > 0"}
|
%div{"ng-if" => "editorManager.externalEditors.length > 0"}
|
||||||
.header
|
.header
|
||||||
@@ -13,6 +13,6 @@
|
|||||||
%ul
|
%ul
|
||||||
%li.menu-item{"ng-repeat" => "editor in editorManager.externalEditors", "ng-click" => "selectEditor($event, editor)"}
|
%li.menu-item{"ng-repeat" => "editor in editorManager.externalEditors", "ng-click" => "selectEditor($event, editor)"}
|
||||||
%strong.red.medium{"ng-if" => "editor.conflict_of"} Conflicted copy
|
%strong.red.medium{"ng-if" => "editor.conflict_of"} Conflicted copy
|
||||||
.menu-item-title
|
%label.menu-item-title
|
||||||
{{editor.name}}
|
{{editor.name}}
|
||||||
%span.inline.blue{"style" => "margin-left: 8px;", "ng-if" => "selectedEditor === editor"} ✓
|
%span.inline.tinted{"style" => "margin-left: 8px;", "ng-if" => "selectedEditor === editor"} ✓
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
.panel-body
|
.panel-body
|
||||||
.container
|
.container
|
||||||
.float-group.h20
|
.float-group.h20
|
||||||
%h1.blue.pull-left Extensions
|
%h1.tinted.pull-left Extensions
|
||||||
%a.block.pull-right.dashboard-link{"href" => "https://dashboard.standardnotes.org", "target" => "_blank"} Open Dashboard
|
%a.block.pull-right.dashboard-link{"href" => "https://dashboard.standardnotes.org", "target" => "_blank"} Open Dashboard
|
||||||
%div.clear{"ng-if" => "!extensionManager.extensions.length && !themeManager.themes.length && !editorManager.externalEditors.length"}
|
%div.clear{"ng-if" => "!extensionManager.extensions.length && !themeManager.themes.length && !editorManager.externalEditors.length"}
|
||||||
%p Customize your experience with editors, themes, and actions.
|
%p Customize your experience with editors, themes, and actions.
|
||||||
.blue-box.mt-10
|
.tinted-box.mt-10
|
||||||
%h3 Available as part of the Extended subscription.
|
%h3 Available as part of the Extended subscription.
|
||||||
%p.mt-5 Note history
|
%p.mt-5 Note history
|
||||||
%p.mt-5 Automated backups
|
%p.mt-5 Automated backups
|
||||||
%p.mt-5 All editors, themes, and actions
|
%p.mt-5 Editors, themes, and actions
|
||||||
%a{"href" => "https://standardnotes.org/extensions", "target" => "_blank"}
|
%a{"href" => "https://standardnotes.org/extensions", "target" => "_blank"}
|
||||||
%button.mt-10
|
%button.mt-10
|
||||||
%h3 Learn More
|
%h3 Learn More
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
%h3 {{theme.name}}
|
%h3 {{theme.name}}
|
||||||
%a{"ng-if" => "!themeManager.isThemeActive(theme)", "ng-click" => "themeManager.activateTheme(theme); $event.stopPropagation();"} Activate
|
%a{"ng-if" => "!themeManager.isThemeActive(theme)", "ng-click" => "themeManager.activateTheme(theme); $event.stopPropagation();"} Activate
|
||||||
%a{"ng-if" => "themeManager.isThemeActive(theme)", "ng-click" => "themeManager.deactivateTheme(theme); $event.stopPropagation();"} Deactivate
|
%a{"ng-if" => "themeManager.isThemeActive(theme)", "ng-click" => "themeManager.deactivateTheme(theme); $event.stopPropagation();"} Deactivate
|
||||||
%div{"ng-if" => "theme.showDetails"}
|
.mt-3{"ng-if" => "theme.showDetails"}
|
||||||
.link-group
|
.link-group
|
||||||
%a.red{"ng-click" => "deleteTheme(theme); $event.stopPropagation();"} Delete
|
%a.red{"ng-click" => "deleteTheme(theme); $event.stopPropagation();"} Delete
|
||||||
%a{"ng-click" => "theme.showLink = !theme.showLink; $event.stopPropagation();"} Show Link
|
%a{"ng-click" => "theme.showLink = !theme.showLink; $event.stopPropagation();"} Show Link
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
%div.mt-5{"ng-if" => "editor.showDetails"}
|
%div.mt-5{"ng-if" => "editor.showDetails"}
|
||||||
.link-group
|
.link-group
|
||||||
%a{"ng-if" => "!editor.default", "ng-click" => "setDefaultEditor(editor); $event.stopPropagation();"} Make Default
|
%a{"ng-if" => "!editor.default", "ng-click" => "setDefaultEditor(editor); $event.stopPropagation();"} Make Default
|
||||||
%a.blue{"ng-if" => "editor.default", "ng-click" => "removeDefaultEditor(editor); $event.stopPropagation();"} Remove as Default
|
%a.tinted{"ng-if" => "editor.default", "ng-click" => "removeDefaultEditor(editor); $event.stopPropagation();"} Remove as Default
|
||||||
%a{"ng-click" => "editor.showUrl = !editor.showUrl; $event.stopPropagation();"} Show Link
|
%a{"ng-click" => "editor.showUrl = !editor.showUrl; $event.stopPropagation();"} Show Link
|
||||||
%a.red{ "ng-click" => "deleteEditor(editor); $event.stopPropagation();"} Delete
|
%a.red{ "ng-click" => "deleteEditor(editor); $event.stopPropagation();"} Delete
|
||||||
.wrap.mt-5.selectable{"ng-if" => "editor.showUrl"} {{editor.url}}
|
.wrap.mt-5.selectable{"ng-if" => "editor.showUrl"} {{editor.url}}
|
||||||
@@ -86,8 +86,8 @@
|
|||||||
|
|
||||||
%div
|
%div
|
||||||
.mt-5{"ng-if" => "action.repeat_mode"}
|
.mt-5{"ng-if" => "action.repeat_mode"}
|
||||||
%button.light{"ng-if" => "extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.disableRepeatAction(action, extension); $event.stopPropagation();"} Disable
|
%button.light.tinted{"ng-if" => "extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.disableRepeatAction(action, extension); $event.stopPropagation();"} Disable
|
||||||
%button.light{"ng-if" => "!extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.enableRepeatAction(action, extension); $event.stopPropagation();"} Enable
|
%button.light.tinted{"ng-if" => "!extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.enableRepeatAction(action, extension); $event.stopPropagation();"} Enable
|
||||||
%button.light.mt-10{"ng-if" => "!action.running && !action.repeat_mode", "ng-click" => "selectedAction(action, extension); $event.stopPropagation();"}
|
%button.light.mt-10{"ng-if" => "!action.running && !action.repeat_mode", "ng-click" => "selectedAction(action, extension); $event.stopPropagation();"}
|
||||||
Perform Action
|
Perform Action
|
||||||
.spinner.mb-5.block{"ng-if" => "action.running"}
|
.spinner.mb-5.block{"ng-if" => "action.running"}
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
%h3 {{component.name}}
|
%h3 {{component.name}}
|
||||||
%a{"ng-if" => "!componentManager.isComponentActive(component)", "ng-click" => "componentManager.activateComponent(component); $event.stopPropagation();"} Activate
|
%a{"ng-if" => "!componentManager.isComponentActive(component)", "ng-click" => "componentManager.activateComponent(component); $event.stopPropagation();"} Activate
|
||||||
%a{"ng-if" => "componentManager.isComponentActive(component)", "ng-click" => "componentManager.deactivateComponent(component); $event.stopPropagation();"} Deactivate
|
%a{"ng-if" => "componentManager.isComponentActive(component)", "ng-click" => "componentManager.deactivateComponent(component); $event.stopPropagation();"} Deactivate
|
||||||
%div{"ng-if" => "component.showDetails"}
|
.mt-3{"ng-if" => "component.showDetails"}
|
||||||
.link-group
|
.link-group
|
||||||
%a.red{"ng-click" => "deleteComponent(component); $event.stopPropagation();"} Delete
|
%a.red{"ng-click" => "deleteComponent(component); $event.stopPropagation();"} Delete
|
||||||
%a{"ng-click" => "component.showLink = !component.showLink; $event.stopPropagation();"} Show Link
|
%a{"ng-click" => "component.showLink = !component.showLink; $event.stopPropagation();"} Show Link
|
||||||
@@ -117,10 +117,24 @@
|
|||||||
%p.small.selectable.wrap{"ng-if" => "component.showLink"}
|
%p.small.selectable.wrap{"ng-if" => "component.showLink"}
|
||||||
{{component.url}}
|
{{component.url}}
|
||||||
|
|
||||||
|
%div{"ng-if" => "serverExtensions.length > 0"}
|
||||||
|
.container.no-bottom.section-margin
|
||||||
|
%h2 Server Extensions
|
||||||
|
%ul
|
||||||
|
%li{"ng-repeat" => "ext in serverExtensions", "ng-click" => "ext.showDetails = !ext.showDetails"}
|
||||||
|
.container
|
||||||
|
%strong.red.medium{"ng-if" => "ext.conflict_of"} Conflicted copy
|
||||||
|
%h3 {{nameForServerExtension(ext)}}
|
||||||
|
%div.mt-3{"ng-if" => "ext.showDetails"}
|
||||||
|
.link-group
|
||||||
|
%a{"ng-click" => "ext.showUrl = !ext.showUrl; $event.stopPropagation();"} Show Link
|
||||||
|
%a.red{ "ng-click" => "deleteServerExt(ext); $event.stopPropagation();"} Delete
|
||||||
|
.wrap.mt-5.selectable{"ng-if" => "ext.showUrl"} {{ext.url}}
|
||||||
|
|
||||||
.container.section-margin
|
.container.section-margin
|
||||||
%h2.blue Install
|
%h2.tinted Install
|
||||||
%p.faded Enter an install link
|
%p.faded Enter an install link
|
||||||
%form.mt-10.mb-10
|
%form.mt-10.mb-10
|
||||||
%input.form-control{:autofocus => 'autofocus', :name => 'url', :required => true, :autocomplete => "off",
|
%input.form-control{:autofocus => 'autofocus', :name => 'url', :required => true, :autocomplete => "off",
|
||||||
:type => 'url', 'ng-model' => 'formData.installLink', "ng-keyup" => "$event.keyCode == 13 && submitInstallLink();"}
|
:type => 'url', 'ng-model' => 'formData.installLink', "ng-keyup" => "$event.keyCode == 13 && submitInstallLink();"}
|
||||||
%p.blue{"ng-if" => "formData.successfullyInstalled"} Successfully installed extension.
|
%p.tinted{"ng-if" => "formData.successfullyInstalled"} Successfully installed extension.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
%p {{permission}}
|
%p {{permission}}
|
||||||
|
|
||||||
%h4 Status
|
%h4 Status
|
||||||
%p.status{"ng-class" => "{'trusted' : component.trusted}"} {{component.trusted ? 'Trusted' : 'Untrusted'}}
|
%p.status{"ng-class" => "{'trusted tinted' : component.trusted}"} {{component.trusted ? 'Trusted' : 'Untrusted'}}
|
||||||
|
|
||||||
.learn-more
|
.learn-more
|
||||||
%h4 Details
|
%h4 Details
|
||||||
@@ -22,4 +22,4 @@
|
|||||||
|
|
||||||
.buttons
|
.buttons
|
||||||
%button.standard.white{"ng-click" => "deny()"} Deny
|
%button.standard.white{"ng-click" => "deny()"} Deny
|
||||||
%button.standard.blue{"ng-click" => "accept()"} Accept
|
%button.standard.tinted{"ng-click" => "accept()"} Accept
|
||||||
|
|||||||
@@ -16,11 +16,24 @@
|
|||||||
%li{"ng-class" => "{'selected' : ctrl.showMenu}", "click-outside" => "ctrl.showMenu = false;", "is-open" => "ctrl.showMenu"}
|
%li{"ng-class" => "{'selected' : ctrl.showMenu}", "click-outside" => "ctrl.showMenu = false;", "is-open" => "ctrl.showMenu"}
|
||||||
%label{"ng-click" => "ctrl.showMenu = !ctrl.showMenu; ctrl.showExtensions = false; ctrl.showEditorMenu = false;"} Menu
|
%label{"ng-click" => "ctrl.showMenu = !ctrl.showMenu; ctrl.showExtensions = false; ctrl.showEditorMenu = false;"} Menu
|
||||||
|
|
||||||
%ul.dropdown-menu{"ng-if" => "ctrl.showMenu"}
|
%ul.dropdown-menu.sectioned-menu{"ng-if" => "ctrl.showMenu"}
|
||||||
%li
|
%li
|
||||||
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleFullScreen()"} Toggle Fullscreen
|
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.togglePin()"}
|
||||||
|
%i.icon.ion-ios-flag
|
||||||
|
{{ctrl.note.pinned ? "Unpin" : "Pin"}}
|
||||||
%li
|
%li
|
||||||
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.deleteNote()"} Delete Note
|
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleArchiveNote()"}
|
||||||
|
%i.icon.ion-ios-box
|
||||||
|
{{ctrl.note.archived ? "Unarcnive" : "Archive"}}
|
||||||
|
%li
|
||||||
|
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.deleteNote()"}
|
||||||
|
%i.icon.ion-trash-b
|
||||||
|
Delete
|
||||||
|
%li
|
||||||
|
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleFullScreen()"}
|
||||||
|
%i.icon.ion-arrow-expand
|
||||||
|
Toggle Fullscreen
|
||||||
|
|
||||||
%li{"ng-if" => "ctrl.hasDisabledComponents()"}
|
%li{"ng-if" => "ctrl.hasDisabledComponents()"}
|
||||||
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.restoreDisabledComponents()"} Restore Disabled Components
|
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.restoreDisabledComponents()"} Restore Disabled Components
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
.pull-left
|
.pull-left
|
||||||
.footer-bar-link{"click-outside" => "ctrl.showAccountMenu = false;", "is-open" => "ctrl.showAccountMenu"}
|
.footer-bar-link{"click-outside" => "ctrl.showAccountMenu = false;", "is-open" => "ctrl.showAccountMenu"}
|
||||||
%a{"ng-click" => "ctrl.accountMenuPressed()", "ng-class" => "{red: ctrl.error}"} Account
|
%a{"ng-click" => "ctrl.accountMenuPressed()", "ng-class" => "{red: ctrl.error}"} Account
|
||||||
%account-menu{"ng-if" => "ctrl.showAccountMenu"}
|
%account-menu{"ng-if" => "ctrl.showAccountMenu", "on-successful-auth" => "ctrl.onAuthSuccess"}
|
||||||
|
|
||||||
.footer-bar-link{"click-outside" => "ctrl.showExtensionsMenu = false;", "is-open" => "ctrl.showExtensionsMenu"}
|
.footer-bar-link{"click-outside" => "ctrl.showExtensionsMenu = false;", "is-open" => "ctrl.showExtensionsMenu"}
|
||||||
%a{"ng-click" => "ctrl.toggleExtensions()"} Extensions
|
%a{"ng-click" => "ctrl.toggleExtensions()"} Extensions
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
.pull-right
|
.pull-right
|
||||||
|
|
||||||
.footer-bar-link{"ng-if" => "ctrl.newUpdateAvailable", "ng-click" => "ctrl.clickedNewUpdateAnnouncement()"}
|
.footer-bar-link{"ng-if" => "ctrl.newUpdateAvailable", "ng-click" => "ctrl.clickedNewUpdateAnnouncement()"}
|
||||||
%span.blue.normal New update downloaded. Installs on app restart.
|
%span.tinted.normal New update downloaded. Installs on app restart.
|
||||||
|
|
||||||
.footer-bar-link{"style" => "margin-right: 5px;"}
|
.footer-bar-link{"style" => "margin-right: 5px;"}
|
||||||
%div{"ng-if" => "ctrl.lastSyncDate", "style" => "float: left; font-weight: normal; margin-right: 8px;"}
|
%div{"ng-if" => "ctrl.lastSyncDate", "style" => "float: left; font-weight: normal; margin-right: 8px;"}
|
||||||
@@ -26,3 +26,6 @@
|
|||||||
|
|
||||||
%strong{"ng-if" => "ctrl.offline"} Offline
|
%strong{"ng-if" => "ctrl.offline"} Offline
|
||||||
%a{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"} Refresh
|
%a{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"} Refresh
|
||||||
|
|
||||||
|
%span{"ng-if" => "ctrl.hasPasscode()"}
|
||||||
|
%i.icon.ion-locked{"ng-click" => "ctrl.lockApp()"}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
.main-ui-view
|
.main-ui-view
|
||||||
.app
|
%lock-screen{"ng-if" => "needsUnlock", "on-success" => "onSuccessfulUnlock"}
|
||||||
%tags-section{"save" => "tagsSave", "add-new" => "tagsAddNew", "will-select" => "tagsWillMakeSelection", "selection-made" => "tagsSelectionMade", "all-tag" => "allTag",
|
.app{"ng-if" => "!needsUnlock"}
|
||||||
"tags" => "tags", "remove-tag" => "removeTag"}
|
%tags-section{"save" => "tagsSave", "add-new" => "tagsAddNew", "will-select" => "tagsWillMakeSelection", "selection-made" => "tagsSelectionMade",
|
||||||
|
"all-tag" => "allTag", "archive-tag" => "archiveTag", "tags" => "tags", "remove-tag" => "removeTag"}
|
||||||
|
|
||||||
%notes-section{"add-new" => "notesAddNew", "selection-made" => "notesSelectionMade", "tag" => "selectedTag"}
|
%notes-section{"add-new" => "notesAddNew", "selection-made" => "notesSelectionMade", "tag" => "selectedTag"}
|
||||||
|
|
||||||
%editor-section{"note" => "selectedNote", "remove" => "deleteNote", "save" => "saveNote", "update-tags" => "updateTagsForNote"}
|
%editor-section{"note" => "selectedNote", "remove" => "deleteNote", "save" => "saveNote", "update-tags" => "updateTagsForNote"}
|
||||||
|
|
||||||
%footer
|
%footer{"ng-if" => "!needsUnlock"}
|
||||||
|
|||||||
9
app/assets/templates/frontend/lock-screen.html.haml
Normal file
9
app/assets/templates/frontend/lock-screen.html.haml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#lock-screen
|
||||||
|
.content
|
||||||
|
%h3.center-align Passcode Required
|
||||||
|
|
||||||
|
%form.mt-20{"ng-submit" => "submitPasscodeForm()"}
|
||||||
|
%input.form-control.mt-10{:type => 'password',
|
||||||
|
"ng-model" => "formData.passcode", "autofocus" => "true",
|
||||||
|
"placeholder" => "Enter Passcode", "autocomplete" => "new-password"}
|
||||||
|
%button.standard.ui-button.block.tinted.mt-5{"type" => "submit"} Unlock
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
#search-clear-button{"ng-if" => "ctrl.noteFilter.text", "ng-click" => "ctrl.noteFilter.text = ''; ctrl.filterTextChanged()"} ✕
|
#search-clear-button{"ng-if" => "ctrl.noteFilter.text", "ng-click" => "ctrl.noteFilter.text = ''; ctrl.filterTextChanged()"} ✕
|
||||||
%ul.section-menu-bar#tag-menu-bar
|
%ul.section-menu-bar#tag-menu-bar
|
||||||
%li{"ng-class" => "{'selected' : ctrl.showMenu}"}
|
%li{"ng-class" => "{'selected' : ctrl.showMenu}"}
|
||||||
%label{"ng-click" => "ctrl.showMenu = !ctrl.showMenu"} Sort
|
%label{"ng-click" => "ctrl.showMenu = !ctrl.showMenu"} {{ctrl.sortByTitle()}}
|
||||||
|
|
||||||
%ul.dropdown-menu{"ng-if" => "ctrl.showMenu"}
|
%ul.dropdown-menu{"ng-if" => "ctrl.showMenu"}
|
||||||
%li
|
%li
|
||||||
@@ -27,12 +27,22 @@
|
|||||||
|
|
||||||
.scrollable
|
.scrollable
|
||||||
.infinite-scroll{"infinite-scroll" => "ctrl.paginate()", "can-load" => "true", "threshold" => "200"}
|
.infinite-scroll{"infinite-scroll" => "ctrl.paginate()", "can-load" => "true", "threshold" => "200"}
|
||||||
.note{"ng-repeat" => "note in (ctrl.sortedNotes = (ctrl.tag.notes | filter: ctrl.filterNotes | orderBy: ctrl.sortBy:ctrl.sortDescending | limitTo:ctrl.notesToDisplay))",
|
.note{"ng-repeat" => "note in (ctrl.sortedNotes = (ctrl.tag.notes | filter: ctrl.filterNotes | sortBy: ctrl.sortBy| limitTo:ctrl.notesToDisplay))",
|
||||||
"ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}"}
|
"ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}"}
|
||||||
%strong.red.medium{"ng-if" => "note.conflict_of"} Conflicted copy
|
%strong.red.medium{"ng-if" => "note.conflict_of"} Conflicted copy
|
||||||
%strong.red.medium{"ng-if" => "note.errorDecrypting"} Error decrypting
|
%strong.red.medium{"ng-if" => "note.errorDecrypting"} Error decrypting
|
||||||
|
|
||||||
|
.pinned.tinted{"ng-if" => "note.pinned", "ng-class" => "{'tinted-selected' : ctrl.selectedNote == note}"}
|
||||||
|
%i.icon.ion-ios-flag
|
||||||
|
%strong.medium Pinned
|
||||||
|
|
||||||
|
.tags-string{"ng-if" => "ctrl.tag.all"}
|
||||||
|
.faded {{note.tagsString()}}
|
||||||
|
|
||||||
.name{"ng-if" => "note.title"}
|
.name{"ng-if" => "note.title"}
|
||||||
{{note.title}}
|
{{note.title}}
|
||||||
.note-preview
|
.note-preview
|
||||||
{{note.text}}
|
{{note.text}}
|
||||||
.date {{(note.created_at | appDateTime) || 'Now'}}
|
.date.faded
|
||||||
|
%span{"ng-if" => "ctrl.sortBy == 'updated_at'"} Modified {{note.updatedAtString() || 'Now'}}
|
||||||
|
%span{"ng-if" => "ctrl.sortBy != 'updated_at'"} {{note.createdAtString() || 'Now'}}
|
||||||
|
|||||||
@@ -21,3 +21,6 @@
|
|||||||
%a.item{"ng-click" => "ctrl.selectedRenameTag($event, tag)", "ng-if" => "!ctrl.editingTag"} Rename
|
%a.item{"ng-click" => "ctrl.selectedRenameTag($event, tag)", "ng-if" => "!ctrl.editingTag"} Rename
|
||||||
%a.item{"ng-click" => "ctrl.saveTag($event, tag)", "ng-if" => "ctrl.editingTag"} Save
|
%a.item{"ng-click" => "ctrl.saveTag($event, tag)", "ng-if" => "ctrl.editingTag"} Save
|
||||||
%a.item{"ng-click" => "ctrl.selectedDeleteTag(tag)"} Delete
|
%a.item{"ng-click" => "ctrl.selectedDeleteTag(tag)"} Delete
|
||||||
|
.tag.faded{"ng-if" => "ctrl.archiveTag", "ng-click" => "ctrl.selectTag(ctrl.archiveTag)", "ng-class" => "{'selected' : ctrl.selectedTag == ctrl.archiveTag}"}
|
||||||
|
.info
|
||||||
|
%input.title{"ng-disabled" => "true", "ng-model" => "ctrl.archiveTag.title"}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html ng-app="app.frontend" ng-controller="BaseCtrl">
|
<html ng-app="app.frontend">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
|
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
|
||||||
|
|||||||
@@ -53,12 +53,12 @@ module Neeto
|
|||||||
font_src: %w('self'),
|
font_src: %w('self'),
|
||||||
form_action: %w('self'),
|
form_action: %w('self'),
|
||||||
frame_ancestors: %w('none'),
|
frame_ancestors: %w('none'),
|
||||||
img_src: %w('self' piwik.standardnotes.org data:),
|
img_src: %w('self' data:),
|
||||||
manifest_src: %w('self'),
|
manifest_src: %w('self'),
|
||||||
media_src: %w('self'),
|
media_src: %w('self'),
|
||||||
object_src: %w('self'),
|
object_src: %w('self'),
|
||||||
plugin_types: %w(),
|
plugin_types: %w(),
|
||||||
script_src: %w('self' 'unsafe-inline' piwik.standardnotes.org),
|
script_src: %w('self' 'unsafe-inline'),
|
||||||
style_src: %w(* 'unsafe-inline'),
|
style_src: %w(* 'unsafe-inline'),
|
||||||
upgrade_insecure_requests: false, # see https://www.w3.org/TR/upgrade-insecure-requests/
|
upgrade_insecure_requests: false, # see https://www.w3.org/TR/upgrade-insecure-requests/
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"grunt-babel": "^6.0.0",
|
"grunt-babel": "^6.0.0",
|
||||||
"grunt-browserify": "^5.0.0",
|
"grunt-browserify": "^5.0.0",
|
||||||
"grunt-contrib-concat": "^1.0.1",
|
"grunt-contrib-concat": "^1.0.1",
|
||||||
|
"grunt-contrib-copy": "^1.0.0",
|
||||||
"grunt-contrib-cssmin": "^1.0.2",
|
"grunt-contrib-cssmin": "^1.0.2",
|
||||||
"grunt-contrib-sass": "^1.0.0",
|
"grunt-contrib-sass": "^1.0.0",
|
||||||
"grunt-contrib-uglify": "^2.0.0",
|
"grunt-contrib-uglify": "^2.0.0",
|
||||||
|
|||||||
BIN
vendor/assets/fonts/ionicons.eot
vendored
Normal file
BIN
vendor/assets/fonts/ionicons.eot
vendored
Normal file
Binary file not shown.
2230
vendor/assets/fonts/ionicons.svg
vendored
Normal file
2230
vendor/assets/fonts/ionicons.svg
vendored
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 326 KiB |
BIN
vendor/assets/fonts/ionicons.ttf
vendored
Normal file
BIN
vendor/assets/fonts/ionicons.ttf
vendored
Normal file
Binary file not shown.
BIN
vendor/assets/fonts/ionicons.woff
vendored
Normal file
BIN
vendor/assets/fonts/ionicons.woff
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user