V2
This commit is contained in:
@@ -70,7 +70,7 @@ module.exports = function(grunt) {
|
||||
},
|
||||
app: {
|
||||
src: [
|
||||
'app/assets/javascripts/app/services/helpers/*.js',
|
||||
'app/assets/javascripts/app/services/encryption/*.js',
|
||||
'app/assets/javascripts/app/*.js',
|
||||
'app/assets/javascripts/app/frontend/*.js',
|
||||
'app/assets/javascripts/app/frontend/controllers/*.js',
|
||||
@@ -142,7 +142,6 @@ module.exports = function(grunt) {
|
||||
dest: 'vendor/assets/javascripts/compiled.min.js'
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -14,3 +14,25 @@ if(!IEOrEdge && (window.crypto && window.crypto.subtle)) {
|
||||
}
|
||||
|
||||
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.componentStack = [];
|
||||
@@ -50,6 +50,10 @@ angular.module('app.frontend')
|
||||
this.syncTakingTooLong = true;
|
||||
}.bind(this));
|
||||
|
||||
$rootScope.$on("sync:completed", function(){
|
||||
this.syncTakingTooLong = false;
|
||||
}.bind(this));
|
||||
|
||||
$rootScope.$on("tag-changed", function(){
|
||||
this.loadTagsString();
|
||||
}.bind(this));
|
||||
@@ -291,7 +295,7 @@ angular.module('app.frontend')
|
||||
if(success) {
|
||||
if(statusTimeout) $timeout.cancel(statusTimeout);
|
||||
statusTimeout = $timeout(function(){
|
||||
var status = "All changes saved"
|
||||
var status = "All changes saved";
|
||||
if(authManager.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.editorMode = 'edit';
|
||||
this.focusEditor(100);
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Tags
|
||||
*/
|
||||
|
||||
this.loadTagsString = function() {
|
||||
var string = "";
|
||||
@@ -419,16 +445,23 @@ angular.module('app.frontend')
|
||||
this.updateTags()(this.note, tags);
|
||||
}
|
||||
|
||||
/* Components */
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Components
|
||||
*/
|
||||
|
||||
let alertKey = "displayed-component-disable-alert";
|
||||
|
||||
this.disableComponent = function(component) {
|
||||
componentManager.disableComponentForItem(component, this.note);
|
||||
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.");
|
||||
localStorage.setItem(alertKey, true);
|
||||
storageManager.setItem(alertKey, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,6 +488,19 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Editor Customization
|
||||
*/
|
||||
|
||||
this.onSystemEditorLoad = function() {
|
||||
if(this.loadedTabListener) {
|
||||
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;
|
||||
|
||||
@@ -31,7 +31,7 @@ angular.module('app.frontend')
|
||||
}
|
||||
this.updateOfflineStatus();
|
||||
|
||||
if(this.offline) {
|
||||
if(this.offline && !passcodeManager.hasPasscode()) {
|
||||
this.showAccountMenu = true;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ angular.module('app.frontend')
|
||||
}
|
||||
this.findErrors();
|
||||
|
||||
this.onAuthSuccess = function() {
|
||||
this.showAccountMenu = false;
|
||||
}.bind(this)
|
||||
|
||||
this.accountMenuPressed = function() {
|
||||
this.serverData = {};
|
||||
this.showAccountMenu = !this.showAccountMenu;
|
||||
@@ -61,6 +65,14 @@ angular.module('app.frontend')
|
||||
this.showAccountMenu = false;
|
||||
}
|
||||
|
||||
this.hasPasscode = function() {
|
||||
return passcodeManager.hasPasscode();
|
||||
}
|
||||
|
||||
this.lockApp = function() {
|
||||
$rootScope.lockApplication();
|
||||
}
|
||||
|
||||
this.refreshData = function() {
|
||||
this.isRefreshing = true;
|
||||
syncManager.sync(function(response){
|
||||
|
||||
@@ -1,56 +1,86 @@
|
||||
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) {
|
||||
return $location.search()[key];
|
||||
storageManager.initialize(passcodeManager.hasPasscode(), authManager.isEphemeralSession());
|
||||
|
||||
$scope.onUpdateAvailable = function(version) {
|
||||
$rootScope.$broadcast('new-update-available', version);
|
||||
}
|
||||
|
||||
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, function(response){
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
$rootScope.lockApplication = function() {
|
||||
// Render first to show lock screen immediately, then refresh
|
||||
$scope.needsUnlock = true;
|
||||
// Reloading wipes current objects from memory
|
||||
setTimeout(function () {
|
||||
window.location.reload();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
if(urlParam("server")) {
|
||||
autoSignInFromParams();
|
||||
function load() {
|
||||
// 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) {
|
||||
$scope.allTag.didLoad = true;
|
||||
themeManager.activateInitialTheme();
|
||||
$scope.$apply();
|
||||
if(passcodeManager.isLocked()) {
|
||||
$scope.needsUnlock = true;
|
||||
} else {
|
||||
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);
|
||||
}, 30000);
|
||||
});
|
||||
// refresh every 30s
|
||||
setInterval(function () {
|
||||
syncManager.sync(null);
|
||||
}, 30000);
|
||||
});
|
||||
}
|
||||
|
||||
var allTag = new Tag({all: true});
|
||||
allTag.needsLoad = true;
|
||||
$scope.allTag = allTag;
|
||||
$scope.allTag.title = "All";
|
||||
$scope.tags = modelManager.tags;
|
||||
$scope.allTag.notes = modelManager.notes;
|
||||
function loadAllTag() {
|
||||
var allTag = new Tag({all: true, title: "All"});
|
||||
allTag.needsLoad = true;
|
||||
$scope.allTag = allTag;
|
||||
$scope.tags = modelManager.tags;
|
||||
$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
|
||||
@@ -116,6 +146,7 @@ angular.module('app.frontend')
|
||||
tag.setDirty(true);
|
||||
syncManager.sync(callback);
|
||||
$rootScope.$broadcast("tag-changed");
|
||||
modelManager.resortTag(tag);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -140,7 +171,7 @@ angular.module('app.frontend')
|
||||
$scope.notesAddNew = function(note) {
|
||||
modelManager.addItem(note);
|
||||
|
||||
if(!$scope.selectedTag.all) {
|
||||
if(!$scope.selectedTag.all && !$scope.selectedTag.archiveTag) {
|
||||
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";
|
||||
|
||||
$rootScope.$on("editorFocused", function(){
|
||||
@@ -44,11 +44,26 @@ angular.module('app.frontend')
|
||||
this.selectFirstNote(false);
|
||||
}.bind(this))
|
||||
|
||||
$rootScope.$on("noteArchived", function() {
|
||||
this.selectFirstNote(false);
|
||||
}.bind(this))
|
||||
|
||||
this.notesToDisplay = 20;
|
||||
this.paginate = function() {
|
||||
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.showMenu = false;
|
||||
|
||||
@@ -98,6 +113,14 @@ angular.module('app.frontend')
|
||||
this.noteFilter = {text : ''};
|
||||
|
||||
this.filterNotes = function(note) {
|
||||
if(this.tag.archiveTag) {
|
||||
return note.archived;
|
||||
}
|
||||
|
||||
if(note.archived) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var filterText = this.noteFilter.text.toLowerCase();
|
||||
if(filterText.length == 0) {
|
||||
note.visible = true;
|
||||
@@ -139,7 +162,7 @@ angular.module('app.frontend')
|
||||
|
||||
this.setSortBy = function(type) {
|
||||
this.sortBy = type;
|
||||
localStorage.setItem("sortBy", type);
|
||||
storageManager.setItem("sortBy", type);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ angular.module('app.frontend')
|
||||
save: "&",
|
||||
tags: "=",
|
||||
allTag: "=",
|
||||
archiveTag: "=",
|
||||
updateNoteTag: "&",
|
||||
removeTag: "&"
|
||||
},
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
let AppDomain = "org.standardnotes.sn";
|
||||
var dateFormatter;
|
||||
|
||||
class Item {
|
||||
|
||||
constructor(json_obj) {
|
||||
|
||||
constructor(json_obj = {}) {
|
||||
this.appData = {};
|
||||
this.updateFromJSON(json_obj);
|
||||
|
||||
this.observers = [];
|
||||
|
||||
if(!this.uuid) {
|
||||
@@ -30,7 +32,7 @@ class Item {
|
||||
try {
|
||||
return JSON.parse(this.content);
|
||||
} catch (e) {
|
||||
console.log("Error parsing json", e);
|
||||
console.log("Error parsing json", e, this);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -81,7 +83,10 @@ class Item {
|
||||
}
|
||||
|
||||
mapContentToLocalProperties(contentObj) {
|
||||
|
||||
this.appData = contentObj.appData;
|
||||
if(!this.appData) {
|
||||
this.appData = {};
|
||||
}
|
||||
}
|
||||
|
||||
createContentJSONFromProperties() {
|
||||
@@ -93,7 +98,10 @@ class Item {
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
return {references: this.referenceParams()}
|
||||
return {
|
||||
references: this.referenceParams(),
|
||||
appData: this.appData
|
||||
}
|
||||
}
|
||||
|
||||
addItemAsRelationship(item) {
|
||||
@@ -137,4 +145,71 @@ class Item {
|
||||
doNotEncrypt() {
|
||||
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() {
|
||||
// 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 || {};
|
||||
_.merge(params, super.structureParams());
|
||||
return params;
|
||||
|
||||
@@ -106,4 +106,8 @@ class Note extends Item {
|
||||
get content_type() {
|
||||
return "Note";
|
||||
}
|
||||
|
||||
tagsString() {
|
||||
return Tag.arrayToDisplayString(this.tags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,4 +86,14 @@ class Tag extends Item {
|
||||
allReferencedObjects() {
|
||||
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) {
|
||||
this.item = item;
|
||||
this.keys = keys;
|
||||
this.version = version;
|
||||
this.version = version || "002";
|
||||
}
|
||||
|
||||
paramsForExportFile() {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
angular.module('app.frontend')
|
||||
.config(function ($locationProvider) {
|
||||
|
||||
var runningInElectron = window && window.process && window.process.type && window.process.versions["electron"];
|
||||
if(!runningInElectron) {
|
||||
if(!isDesktopApplication()) {
|
||||
if (window.history && window.history.pushState) {
|
||||
$locationProvider.html5Mode({
|
||||
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;
|
||||
}
|
||||
|
||||
this.$get = function($rootScope, $timeout, httpManager, modelManager, dbManager) {
|
||||
return new AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager);
|
||||
this.$get = function($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager) {
|
||||
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");
|
||||
if(userData) {
|
||||
this.user = JSON.parse(userData);
|
||||
} else {
|
||||
// legacy, check for uuid
|
||||
var idData = localStorage.getItem("uuid");
|
||||
if(idData) {
|
||||
this.user = {uuid: idData};
|
||||
this.loadInitialData = function() {
|
||||
var userData = storageManager.getItem("user");
|
||||
if(userData) {
|
||||
this.user = JSON.parse(userData);
|
||||
} else {
|
||||
// legacy, check for uuid
|
||||
var idData = storageManager.getItem("uuid");
|
||||
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() {
|
||||
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() {
|
||||
if(!this._authParams) {
|
||||
this._authParams = JSON.parse(localStorage.getItem("auth_params"));
|
||||
this._authParams = JSON.parse(storageManager.getItem("auth_params"));
|
||||
}
|
||||
return this._authParams;
|
||||
}
|
||||
|
||||
this.keys = function() {
|
||||
var mk = localStorage.getItem("mk");
|
||||
if(!mk) {
|
||||
return null;
|
||||
if(!this._keys) {
|
||||
var mk = storageManager.getItem("mk");
|
||||
if(!mk) {
|
||||
return null;
|
||||
}
|
||||
this._keys = {mk: mk, ak: storageManager.getItem("ak")};
|
||||
}
|
||||
var keys = {mk: mk, ak: localStorage.getItem("ak")};
|
||||
return keys;
|
||||
return this._keys;
|
||||
}
|
||||
|
||||
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){
|
||||
|
||||
if(!authParams || !authParams.pw_cost) {
|
||||
@@ -134,7 +147,11 @@ angular.module('app.frontend')
|
||||
var requestUrl = url + "/auth/sign_in";
|
||||
var params = {password: keys.pw, email: email};
|
||||
httpManager.postAbsolute(requestUrl, params, function(response){
|
||||
this.setEphemeral(ephemeral);
|
||||
|
||||
this.handleAuthResponse(response, email, url, authParams, keys);
|
||||
storageManager.setModelStorageMode(ephemeral ? StorageManager.Ephemeral : StorageManager.Fixed);
|
||||
|
||||
callback(response);
|
||||
}.bind(this), function(response){
|
||||
console.error("Error logging in", response);
|
||||
@@ -148,11 +165,16 @@ angular.module('app.frontend')
|
||||
this.handleAuthResponse = function(response, email, url, authParams, keys) {
|
||||
try {
|
||||
if(url) {
|
||||
localStorage.setItem("server", url);
|
||||
storageManager.setItem("server", url);
|
||||
}
|
||||
localStorage.setItem("user", JSON.stringify(response.user));
|
||||
localStorage.setItem("auth_params", JSON.stringify(authParams));
|
||||
localStorage.setItem("jwt", response.token);
|
||||
|
||||
this.user = response.user;
|
||||
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);
|
||||
} catch(e) {
|
||||
dbManager.displayOfflineAlert();
|
||||
@@ -160,18 +182,24 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
this.saveKeys = function(keys) {
|
||||
localStorage.setItem("pw", keys.pw);
|
||||
localStorage.setItem("mk", keys.mk);
|
||||
localStorage.setItem("ak", keys.ak);
|
||||
this._keys = keys;
|
||||
storageManager.setItem("pw", keys.pw);
|
||||
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){
|
||||
var requestUrl = url + "/auth";
|
||||
var params = _.merge({password: keys.pw, email: email}, authParams);
|
||||
|
||||
httpManager.postAbsolute(requestUrl, params, function(response){
|
||||
this.setEphemeral(ephemeral);
|
||||
|
||||
this.handleAuthResponse(response, email, url, authParams, keys);
|
||||
|
||||
storageManager.setModelStorageMode(ephemeral ? StorageManager.Ephemeral : StorageManager.Fixed);
|
||||
|
||||
callback(response);
|
||||
}.bind(this), function(response){
|
||||
console.error("Registration error", response);
|
||||
@@ -182,7 +210,7 @@ angular.module('app.frontend')
|
||||
|
||||
this.changePassword = function(email, new_password, callback) {
|
||||
Neeto.crypto.generateInitialEncryptionKeysForUser({password: new_password, email: email}, function(keys, authParams){
|
||||
var requestUrl = localStorage.getItem("server") + "/auth/change_pw";
|
||||
var requestUrl = storageManager.getItem("server") + "/auth/change_pw";
|
||||
var params = _.merge({new_password: keys.pw}, authParams);
|
||||
|
||||
httpManager.postAbsolute(requestUrl, params, function(response) {
|
||||
@@ -200,10 +228,10 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
this.updateAuthParams = function(authParams, callback) {
|
||||
var requestUrl = localStorage.getItem("server") + "/auth/update";
|
||||
var requestUrl = storageManager.getItem("server") + "/auth/update";
|
||||
var params = authParams;
|
||||
httpManager.postAbsolute(requestUrl, params, function(response) {
|
||||
localStorage.setItem("auth_params", JSON.stringify(authParams));
|
||||
storageManager.setItem("auth_params", JSON.stringify(authParams));
|
||||
if(callback) {
|
||||
callback(response);
|
||||
}
|
||||
@@ -246,6 +274,8 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
this.signOut = function() {
|
||||
this._keys = null;
|
||||
this.user = null;
|
||||
this._authParams = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
class DBManager {
|
||||
|
||||
constructor() {
|
||||
this.locked = true;
|
||||
}
|
||||
|
||||
displayOfflineAlert() {
|
||||
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.";
|
||||
@@ -7,7 +11,15 @@ class DBManager {
|
||||
alert(message);
|
||||
}
|
||||
|
||||
setLocked(locked) {
|
||||
this.locked = locked;
|
||||
}
|
||||
|
||||
openDatabase(callback, onUgradeNeeded) {
|
||||
if(this.locked) {
|
||||
return;
|
||||
}
|
||||
|
||||
var request = window.indexedDB.open("standardnotes", 1);
|
||||
|
||||
request.onerror = function(event) {
|
||||
@@ -57,7 +69,7 @@ class DBManager {
|
||||
};
|
||||
}
|
||||
|
||||
getAllItems(callback) {
|
||||
getAllModels(callback) {
|
||||
this.openDatabase((db) => {
|
||||
var objectStore = db.transaction("items").objectStore("items");
|
||||
var items = [];
|
||||
@@ -74,11 +86,11 @@ class DBManager {
|
||||
}, null)
|
||||
}
|
||||
|
||||
saveItem(item) {
|
||||
this.saveItems([item]);
|
||||
saveModel(item) {
|
||||
this.saveModels([item]);
|
||||
}
|
||||
|
||||
saveItems(items, callback) {
|
||||
saveModels(items, callback) {
|
||||
|
||||
if(items.length == 0) {
|
||||
if(callback) {
|
||||
@@ -115,7 +127,7 @@ class DBManager {
|
||||
}, null)
|
||||
}
|
||||
|
||||
deleteItem(item, callback) {
|
||||
deleteModel(item, callback) {
|
||||
this.openDatabase((db) => {
|
||||
var request = db.transaction("items", "readwrite").objectStore("items").delete(item.uuid);
|
||||
request.onsuccess = function(event) {
|
||||
@@ -126,26 +138,17 @@ class DBManager {
|
||||
}, null)
|
||||
}
|
||||
|
||||
getItemByUUID(uuid, callback) {
|
||||
this.openDatabase((db) => {
|
||||
var request = db.transaction("items", "readonly").objectStore("items").get(uuid);
|
||||
request.onsuccess = function(event) {
|
||||
callback(event.result);
|
||||
};
|
||||
}, null);
|
||||
}
|
||||
|
||||
clearAllItems(callback) {
|
||||
clearAllModels(callback) {
|
||||
var deleteRequest = window.indexedDB.deleteDatabase("standardnotes");
|
||||
|
||||
deleteRequest.onerror = function(event) {
|
||||
console.log("Error deleting database.");
|
||||
callback();
|
||||
callback && callback();
|
||||
};
|
||||
|
||||
deleteRequest.onsuccess = function(event) {
|
||||
console.log("Database deleted successfully");
|
||||
callback();
|
||||
callback && callback();
|
||||
};
|
||||
|
||||
deleteRequest.onblocked = function(event) {
|
||||
|
||||
@@ -3,18 +3,19 @@ class AccountMenu {
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
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';
|
||||
|
||||
$scope.formData = {mergeLocal: true, url: syncManager.serverURL};
|
||||
$scope.formData = {mergeLocal: true, url: syncManager.serverURL, ephemeral: false};
|
||||
$scope.user = authManager.user;
|
||||
$scope.server = syncManager.serverURL;
|
||||
|
||||
$scope.syncStatus = syncManager.syncStatus;
|
||||
$scope.analyticsManager = analyticsManager;
|
||||
|
||||
$scope.encryptionKey = function() {
|
||||
return authManager.keys().mk;
|
||||
@@ -29,7 +30,7 @@ class AccountMenu {
|
||||
}
|
||||
|
||||
$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 = {};
|
||||
@@ -95,7 +96,7 @@ class AccountMenu {
|
||||
$scope.login = function() {
|
||||
$scope.formData.status = "Generating Login Keys...";
|
||||
$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) {
|
||||
$scope.formData.status = null;
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
@@ -120,7 +121,7 @@ class AccountMenu {
|
||||
$scope.formData.status = "Generating Account Keys...";
|
||||
|
||||
$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) {
|
||||
$scope.formData.status = null;
|
||||
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() {
|
||||
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?")) {
|
||||
@@ -146,18 +143,20 @@ class AccountMenu {
|
||||
|
||||
$scope.onAuthSuccess = function() {
|
||||
var block = function() {
|
||||
window.location.reload();
|
||||
$timeout(function(){
|
||||
$scope.onSuccessfulAuth()();
|
||||
syncManager.sync();
|
||||
})
|
||||
}
|
||||
|
||||
if($scope.formData.mergeLocal) {
|
||||
syncManager.markAllItemsDirtyAndSaveOffline(function(){
|
||||
block();
|
||||
})
|
||||
}, true)
|
||||
} else {
|
||||
dbManager.clearAllItems(function(){
|
||||
$timeout(function(){
|
||||
block();
|
||||
})
|
||||
modelManager.resetLocalMemory();
|
||||
storageManager.clearAllModels(function(){
|
||||
block();
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -206,26 +205,25 @@ class AccountMenu {
|
||||
var file = files[0];
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
var data = JSON.parse(e.target.result);
|
||||
$timeout(function(){
|
||||
if(data.auth_params) {
|
||||
// request password
|
||||
$scope.importData.requestPassword = true;
|
||||
$scope.importData.data = data;
|
||||
} else {
|
||||
$scope.performImport(data, null);
|
||||
}
|
||||
})
|
||||
try {
|
||||
var data = JSON.parse(e.target.result);
|
||||
$timeout(function(){
|
||||
if(data.auth_params) {
|
||||
// request password
|
||||
$scope.importData.requestPassword = true;
|
||||
$scope.importData.data = data;
|
||||
} else {
|
||||
$scope.performImport(data, null);
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
alert("Unable to open file. Ensure it is a proper JSON file and try again.");
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
var onDataReady = function() {
|
||||
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.componentManager = componentManager;
|
||||
|
||||
$scope.serverExtensions = modelManager.itemsForContentType("SF|Extension");
|
||||
|
||||
$scope.selectedAction = function(action, extension) {
|
||||
extensionManager.executeAction(action, extension, null, function(response){
|
||||
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
|
||||
|
||||
@@ -127,6 +161,7 @@ class GlobalExtensionsMenu {
|
||||
|
||||
modelManager.addItem(ext);
|
||||
syncManager.sync();
|
||||
$scope.serverExtensions.push(ext);
|
||||
completion();
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class SNCrypto {
|
||||
var pw_cost = this.defaultPasswordGenerationCost();
|
||||
var pw_nonce = this.generateRandomKey(512);
|
||||
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"});
|
||||
}.bind(this));
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class EncryptionHelper {
|
||||
return fullCiphertext;
|
||||
}
|
||||
|
||||
static encryptItem(item, keys, version) {
|
||||
static encryptItem(item, keys, version = "002") {
|
||||
var params = {};
|
||||
// encrypt item key
|
||||
var item_key = Neeto.crypto.generateRandomEncryptionKey();
|
||||
@@ -73,7 +73,10 @@ class EncryptionHelper {
|
||||
// is encrypted, continue to below
|
||||
} else {
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
class ExtensionManager {
|
||||
|
||||
constructor(httpManager, modelManager, authManager, syncManager) {
|
||||
constructor(httpManager, modelManager, authManager, syncManager, storageManager) {
|
||||
this.httpManager = httpManager;
|
||||
this.modelManager = modelManager;
|
||||
this.authManager = authManager;
|
||||
this.enabledRepeatActionUrls = JSON.parse(localStorage.getItem("enabledRepeatActionUrls")) || [];
|
||||
this.decryptedExtensions = JSON.parse(localStorage.getItem("decryptedExtensions")) || [];
|
||||
this.enabledRepeatActionUrls = JSON.parse(storageManager.getItem("enabledRepeatActionUrls")) || [];
|
||||
this.decryptedExtensions = JSON.parse(storageManager.getItem("decryptedExtensions")) || [];
|
||||
this.syncManager = syncManager;
|
||||
this.storageManager = storageManager;
|
||||
|
||||
modelManager.addItemSyncObserver("extensionManager", "Extension", function(items){
|
||||
for (var ext of items) {
|
||||
@@ -49,7 +50,7 @@ class ExtensionManager {
|
||||
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);
|
||||
}
|
||||
@@ -240,7 +241,7 @@ class ExtensionManager {
|
||||
|
||||
disableRepeatAction(action, extension) {
|
||||
_.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);
|
||||
|
||||
console.assert(this.isRepeatActionEnabled(action) == false);
|
||||
@@ -249,7 +250,7 @@ class ExtensionManager {
|
||||
enableRepeatAction(action, extension) {
|
||||
if(!_.find(this.enabledRepeatActionUrls, 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) {
|
||||
|
||||
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 {
|
||||
|
||||
constructor($timeout) {
|
||||
constructor($timeout, storageManager) {
|
||||
// calling callbacks in a $timeout allows angular UI to update
|
||||
this.$timeout = $timeout;
|
||||
this.storageManager = storageManager;
|
||||
}
|
||||
|
||||
setAuthHeadersForRequest(request) {
|
||||
var token = localStorage.getItem("jwt");
|
||||
var token = this.storageManager.getItem("jwt");
|
||||
if(token) {
|
||||
request.setRequestHeader('Authorization', 'Bearer ' + localStorage.getItem("jwt"));
|
||||
request.setRequestHeader('Authorization', 'Bearer ' + token);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
class ModelManager {
|
||||
|
||||
constructor(dbManager) {
|
||||
this.dbManager = dbManager;
|
||||
constructor(storageManager) {
|
||||
this.storageManager = storageManager;
|
||||
this.notes = [];
|
||||
this.tags = [];
|
||||
this.itemSyncObservers = [];
|
||||
this.itemChangeObservers = [];
|
||||
this.items = [];
|
||||
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() {
|
||||
@@ -26,9 +33,14 @@ class ModelManager {
|
||||
alternateUUIDForItem(item, callback) {
|
||||
// we need to clone this item and give it a new uuid, then delete item with old uuid from db (you can't mofidy uuid's in our indexeddb setup)
|
||||
var newItem = this.createItem(item);
|
||||
|
||||
newItem.uuid = Neeto.crypto.generateUUID();
|
||||
|
||||
// Update uuids of relationships
|
||||
newItem.informReferencesOfUUIDChange(item.uuid, newItem.uuid);
|
||||
|
||||
this.informModelsOfUUIDChangeForItem(newItem, item.uuid, newItem.uuid);
|
||||
|
||||
this.removeItemLocally(item, function(){
|
||||
this.addItem(newItem);
|
||||
newItem.setDirty(true);
|
||||
@@ -53,6 +65,12 @@ class ModelManager {
|
||||
})
|
||||
}
|
||||
|
||||
itemsForContentType(contentType) {
|
||||
return this.items.filter(function(item){
|
||||
return item.content_type == contentType;
|
||||
});
|
||||
}
|
||||
|
||||
findItem(itemId) {
|
||||
return _.find(this.items, {uuid: itemId});
|
||||
}
|
||||
@@ -151,6 +169,8 @@ class ModelManager {
|
||||
item = new Theme(json_obj);
|
||||
} else if(json_obj.content_type == "SN|Component") {
|
||||
item = new Component(json_obj);
|
||||
} else if(json_obj.content_type == "SF|Extension") {
|
||||
item = new SyncAdapter(json_obj);
|
||||
}
|
||||
|
||||
else {
|
||||
@@ -189,14 +209,16 @@ class ModelManager {
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
addItem(item) {
|
||||
this.addItems([item]);
|
||||
resortTag(tag) {
|
||||
_.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) {
|
||||
return this.items.filter(function(item){
|
||||
return item.content_type == contentType;
|
||||
});
|
||||
addItem(item) {
|
||||
this.addItems([item]);
|
||||
}
|
||||
|
||||
resolveReferencesForItem(item) {
|
||||
@@ -288,7 +310,7 @@ class ModelManager {
|
||||
_.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 {
|
||||
|
||||
constructor($rootScope, modelManager, authManager, dbManager, httpManager, $interval, $timeout) {
|
||||
constructor($rootScope, modelManager, authManager, dbManager, httpManager, $interval, $timeout, storageManager, passcodeManager) {
|
||||
this.$rootScope = $rootScope;
|
||||
this.httpManager = httpManager;
|
||||
this.modelManager = modelManager;
|
||||
@@ -8,25 +8,33 @@ class SyncManager {
|
||||
this.dbManager = dbManager;
|
||||
this.$interval = $interval;
|
||||
this.$timeout = $timeout;
|
||||
this.storageManager = storageManager;
|
||||
this.passcodeManager = passcodeManager;
|
||||
this.syncStatus = {};
|
||||
}
|
||||
|
||||
get serverURL() {
|
||||
return localStorage.getItem("server") || window._default_sf_server;
|
||||
return this.storageManager.getItem("server") || window._default_sf_server;
|
||||
}
|
||||
|
||||
get masterKey() {
|
||||
return localStorage.getItem("mk");
|
||||
return this.storageManager.getItem("mk");
|
||||
}
|
||||
|
||||
get serverPassword() {
|
||||
return localStorage.getItem("pw");
|
||||
return this.storageManager.getItem("pw");
|
||||
}
|
||||
|
||||
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 itemParams = new ItemParams(item, null, version);
|
||||
var itemParams = new ItemParams(item, keys, version);
|
||||
itemParams = itemParams.paramsForLocalStorage();
|
||||
if(offlineOnly) {
|
||||
delete itemParams.dirty;
|
||||
@@ -34,12 +42,12 @@ class SyncManager {
|
||||
return itemParams;
|
||||
}.bind(this));
|
||||
|
||||
this.dbManager.saveItems(params, callback);
|
||||
this.storageManager.saveModels(params, callback);
|
||||
}
|
||||
|
||||
loadLocalItems(callback) {
|
||||
var params = this.dbManager.getAllItems(function(items){
|
||||
var items = this.handleItemsResponse(items, null, null);
|
||||
var params = this.storageManager.getAllModels(function(items){
|
||||
var items = this.handleItemsResponse(items, null);
|
||||
Item.sortItemsByDate(items);
|
||||
callback(items);
|
||||
}.bind(this))
|
||||
@@ -61,12 +69,40 @@ class SyncManager {
|
||||
|
||||
}
|
||||
|
||||
markAllItemsDirtyAndSaveOffline(callback) {
|
||||
var items = this.modelManager.allItems;
|
||||
for(var item of items) {
|
||||
item.setDirty(true);
|
||||
/*
|
||||
In the case of signing in and merging local data, we alternative UUIDs
|
||||
to avoid overwriting data a user may retrieve that has the same UUID.
|
||||
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() {
|
||||
@@ -75,12 +111,12 @@ class SyncManager {
|
||||
|
||||
set syncToken(token) {
|
||||
this._syncToken = token;
|
||||
localStorage.setItem("syncToken", token);
|
||||
this.storageManager.setItem("syncToken", token);
|
||||
}
|
||||
|
||||
get syncToken() {
|
||||
if(!this._syncToken) {
|
||||
this._syncToken = localStorage.getItem("syncToken");
|
||||
this._syncToken = this.storageManager.getItem("syncToken");
|
||||
}
|
||||
return this._syncToken;
|
||||
}
|
||||
@@ -88,15 +124,15 @@ class SyncManager {
|
||||
set cursorToken(token) {
|
||||
this._cursorToken = token;
|
||||
if(token) {
|
||||
localStorage.setItem("cursorToken", token);
|
||||
this.storageManager.setItem("cursorToken", token);
|
||||
} else {
|
||||
localStorage.removeItem("cursorToken");
|
||||
this.storageManager.removeItem("cursorToken");
|
||||
}
|
||||
}
|
||||
|
||||
get cursorToken() {
|
||||
if(!this._cursorToken) {
|
||||
this._cursorToken = localStorage.getItem("cursorToken");
|
||||
this._cursorToken = this.storageManager.getItem("cursorToken");
|
||||
}
|
||||
return this._cursorToken;
|
||||
}
|
||||
@@ -129,7 +165,7 @@ class SyncManager {
|
||||
this.syncStatus.checker = this.$interval(function(){
|
||||
// check to see if the ongoing sync is taking too long, alert the user
|
||||
var secondsPassed = (new Date() - this.syncStatus.syncStart) / 1000;
|
||||
var warningThreshold = 5; // seconds
|
||||
var warningThreshold = 5.0; // seconds
|
||||
if(secondsPassed > warningThreshold) {
|
||||
this.$rootScope.$broadcast("sync:taking-too-long");
|
||||
this.stopCheckingIfSyncIsTakingTooLong();
|
||||
@@ -159,7 +195,6 @@ class SyncManager {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// we want to write all dirty items to disk only if the user is offline, or if the sync op fails
|
||||
// if the sync op succeeds, these items will be written to disk by handling the "saved_items" response from the server
|
||||
if(this.authManager.offline()) {
|
||||
@@ -288,7 +323,8 @@ class SyncManager {
|
||||
}
|
||||
|
||||
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);
|
||||
return items;
|
||||
}
|
||||
@@ -302,45 +338,64 @@ class SyncManager {
|
||||
|
||||
var i = 0;
|
||||
var handleNext = function() {
|
||||
if (i < unsaved.length) {
|
||||
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
|
||||
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 {
|
||||
if(i >= unsaved.length) {
|
||||
// Handled all items
|
||||
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);
|
||||
|
||||
handleNext();
|
||||
}
|
||||
|
||||
clearSyncToken() {
|
||||
localStorage.removeItem("syncToken");
|
||||
this.storageManager.removeItem("syncToken");
|
||||
}
|
||||
|
||||
destroyLocalData(callback) {
|
||||
localStorage.clear();
|
||||
this.dbManager.clearAllItems(function(){
|
||||
this.storageManager.clear();
|
||||
this.storageManager.clearAllModels(function(){
|
||||
if(callback) {
|
||||
this.$timeout(function(){
|
||||
callback();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
class ThemeManager {
|
||||
|
||||
constructor(modelManager, syncManager, $rootScope) {
|
||||
constructor(modelManager, syncManager, $rootScope, storageManager) {
|
||||
this.syncManager = syncManager;
|
||||
this.modelManager = modelManager;
|
||||
this.$rootScope = $rootScope;
|
||||
this.storageManager = storageManager;
|
||||
}
|
||||
|
||||
get themes() {
|
||||
@@ -16,7 +17,7 @@ class ThemeManager {
|
||||
*/
|
||||
|
||||
get activeTheme() {
|
||||
var activeThemeId = localStorage.getItem("activeTheme");
|
||||
var activeThemeId = this.storageManager.getItem("activeTheme");
|
||||
if(!activeThemeId) {
|
||||
return null;
|
||||
}
|
||||
@@ -53,14 +54,14 @@ class ThemeManager {
|
||||
link.media = "screen,print";
|
||||
link.id = theme.uuid;
|
||||
document.getElementsByTagName("head")[0].appendChild(link);
|
||||
localStorage.setItem("activeTheme", theme.uuid);
|
||||
this.storageManager.setItem("activeTheme", theme.uuid);
|
||||
|
||||
this.currentTheme = theme;
|
||||
this.$rootScope.$broadcast("theme-changed");
|
||||
}
|
||||
|
||||
deactivateTheme(theme) {
|
||||
localStorage.removeItem("activeTheme");
|
||||
this.storageManager.removeItem("activeTheme");
|
||||
var element = document.getElementById(theme.uuid);
|
||||
if(element) {
|
||||
element.disabled = true;
|
||||
@@ -72,7 +73,7 @@ class ThemeManager {
|
||||
}
|
||||
|
||||
isThemeActive(theme) {
|
||||
return localStorage.getItem("activeTheme") === theme.uuid;
|
||||
return this.storageManager.getItem("activeTheme") === theme.uuid;
|
||||
}
|
||||
|
||||
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 {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
|
||||
padding-top: 14px;
|
||||
padding-left: 14px;
|
||||
padding-bottom: 10px;
|
||||
padding-right: 10px;
|
||||
|
||||
background-color: white;
|
||||
border-bottom: none;
|
||||
z-index: 100;
|
||||
|
||||
height: auto;
|
||||
padding-right: 10px;
|
||||
overflow: visible;
|
||||
|
||||
&.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 {
|
||||
padding-top: 12px;
|
||||
font-weight: normal;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.fake-link {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
color: $blue-color;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
@@ -31,7 +30,6 @@ h2 {
|
||||
a {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
color: $blue-color;
|
||||
|
||||
&.gray {
|
||||
color: $dark-gray;
|
||||
@@ -140,7 +138,6 @@ button.light {
|
||||
width: 100%;
|
||||
border: 1px solid rgba(gray, 0.15);
|
||||
cursor: pointer;
|
||||
color: $blue-color;
|
||||
|
||||
&:hover {
|
||||
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-radius: 50%;
|
||||
|
||||
&.blue {
|
||||
&.tinted {
|
||||
border: 1px solid $blue-color;
|
||||
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;
|
||||
}
|
||||
|
||||
.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,
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont,
|
||||
@@ -67,6 +90,7 @@ body {
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue-color;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;;
|
||||
@@ -164,3 +188,7 @@ $section-header-height: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
ul.section-menu-bar {
|
||||
width: 100%;
|
||||
padding-top: 0px;
|
||||
padding-left: 15px;
|
||||
padding-left: 6px;
|
||||
padding-right: 21px;
|
||||
|
||||
background-color: #f1f1f1;
|
||||
@@ -63,10 +63,13 @@ ul.section-menu-bar {
|
||||
|
||||
> li {
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
height: 40px;
|
||||
padding-top: 3px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
border-bottom: 1px solid rgba(black, 0.1);
|
||||
|
||||
color: $selected-text-color;
|
||||
float: left;
|
||||
|
||||
@@ -103,10 +106,6 @@ ul.section-menu-bar {
|
||||
overflow-y: scroll;
|
||||
max-height: calc(85vh - 90px);
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
@@ -143,7 +142,7 @@ ul.section-menu-bar {
|
||||
background-color: $blue-color;
|
||||
|
||||
|
||||
.blue {
|
||||
.tinted {
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#notes-title-bar {
|
||||
color: rgba(black, 0.40);
|
||||
padding-top: 16px;
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
height: $notes-title-bar-height;
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
@@ -21,14 +23,15 @@
|
||||
}
|
||||
|
||||
#notes-add-button {
|
||||
right: 20px;
|
||||
right: 14px;
|
||||
}
|
||||
|
||||
#tag-menu-bar {
|
||||
position: relative;
|
||||
margin: 0 -20px;
|
||||
width: auto;
|
||||
margin: 0 -14px;
|
||||
margin-top: 14px;
|
||||
// padding-left: 4px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
@@ -101,6 +104,20 @@
|
||||
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 {
|
||||
font-size: 15px;
|
||||
margin-top: 1px;
|
||||
@@ -117,6 +134,10 @@
|
||||
&.selected {
|
||||
background-color: $blue-color;
|
||||
color: white;
|
||||
|
||||
.pinned {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -55,9 +55,6 @@
|
||||
|
||||
.status {
|
||||
color: orange;
|
||||
&.trusted {
|
||||
color: $blue-color;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
@@ -74,7 +71,7 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.blue {
|
||||
&.tinted {
|
||||
background-color: $blue-color;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
margin-top: 2px !important;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 3px !important;
|
||||
}
|
||||
|
||||
.mt-5 {
|
||||
margin-top: 5px !important;
|
||||
}
|
||||
|
||||
@@ -10,3 +10,6 @@ $dark-gray: #2e2e2e;
|
||||
@import "app/extensions";
|
||||
@import "app/menus";
|
||||
@import "app/permissions-modal";
|
||||
@import "app/lock-screen";
|
||||
|
||||
@import "ionicons";
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
.mb-10
|
||||
|
||||
.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
|
||||
|
||||
.button-group.mt-5
|
||||
@@ -31,10 +31,14 @@
|
||||
%label.pull-left Sync Server Domain
|
||||
%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
|
||||
%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"}}
|
||||
|
||||
.mt-15{"ng-if" => "formData.showRegister"}
|
||||
@@ -48,8 +52,8 @@
|
||||
%div{"ng-if" => "user"}
|
||||
%h2 {{user.email}}
|
||||
%p {{server}}
|
||||
%div.bold.mt-10.blue{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress || syncStatus.needsMoreSync", "delay" => "1000"}
|
||||
.spinner.inline.mr-5.blue
|
||||
%div.bold.mt-10.tinted{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress || syncStatus.needsMoreSync", "delay" => "1000"}
|
||||
.spinner.inline.mr-5.tinted
|
||||
{{"Syncing" + (syncStatus.total > 0 ? ":" : "")}}
|
||||
%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}}
|
||||
@@ -104,20 +108,37 @@
|
||||
%input.form-control{:type => 'password', "ng-model" => "securityUpdateData.password", "placeholder" => "Enter password"}
|
||||
%button.ui-button.block{"ng-click" => "submitSecurityUpdateForm()"} Update
|
||||
%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"}
|
||||
%h4 Data Archives
|
||||
.mt-5{"ng-if" => "user"}
|
||||
@@ -132,25 +153,15 @@
|
||||
|
||||
%label.block.mt-5
|
||||
%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"}
|
||||
%p Enter the account password associated with the import file.
|
||||
%input.form-control.mt-5{:type => 'password', "ng-model" => "importData.password"}
|
||||
%button.standard.ui-button.block.blue.mt-5{"ng-click" => "submitImportPassword()"} Decrypt & Import
|
||||
%input.form-control.mt-5{:type => 'password', "ng-model" => "importData.password", "autofocus" => "true"}
|
||||
%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".
|
||||
|
||||
.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" }}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
%ul{"ng-if" => "!extension.hide"}
|
||||
%li.menu-item{"ng-repeat" => "action in extension.actionsWithContextForItem(item)", "ng-click" => "executeAction(action, extension);",
|
||||
"ng-class" => "{'faded' : !isActionEnabled(action, extension)}"}
|
||||
.menu-item-title {{action.label}}
|
||||
%label.menu-item-title {{action.label}}
|
||||
.menu-item-subtitle {{action.desc}}
|
||||
|
||||
.small.normal{"ng-if" => "!isActionEnabled(action, extension)"}
|
||||
@@ -19,7 +19,7 @@
|
||||
%div{"ng-if" => "action.showNestedActions"}
|
||||
%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;"}
|
||||
.menu-item-title {{subaction.label}}
|
||||
%label.menu-item-title {{subaction.label}}
|
||||
.menu-item-subtitle {{subaction.desc}}
|
||||
%span{"ng-if" => "subaction.running"}
|
||||
.spinner{"style" => "margin-top: 3px;"}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
%ul
|
||||
%li.menu-item{"ng-repeat" => "editor in editorManager.systemEditors", "ng-click" => "selectEditor($event, 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"}
|
||||
.header
|
||||
@@ -13,6 +13,6 @@
|
||||
%ul
|
||||
%li.menu-item{"ng-repeat" => "editor in editorManager.externalEditors", "ng-click" => "selectEditor($event, editor)"}
|
||||
%strong.red.medium{"ng-if" => "editor.conflict_of"} Conflicted copy
|
||||
.menu-item-title
|
||||
%label.menu-item-title
|
||||
{{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
|
||||
.container
|
||||
.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
|
||||
%div.clear{"ng-if" => "!extensionManager.extensions.length && !themeManager.themes.length && !editorManager.externalEditors.length"}
|
||||
%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.
|
||||
%p.mt-5 Note history
|
||||
%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"}
|
||||
%button.mt-10
|
||||
%h3 Learn More
|
||||
@@ -24,7 +24,7 @@
|
||||
%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.deactivateTheme(theme); $event.stopPropagation();"} Deactivate
|
||||
%div{"ng-if" => "theme.showDetails"}
|
||||
.mt-3{"ng-if" => "theme.showDetails"}
|
||||
.link-group
|
||||
%a.red{"ng-click" => "deleteTheme(theme); $event.stopPropagation();"} Delete
|
||||
%a{"ng-click" => "theme.showLink = !theme.showLink; $event.stopPropagation();"} Show Link
|
||||
@@ -43,7 +43,7 @@
|
||||
%div.mt-5{"ng-if" => "editor.showDetails"}
|
||||
.link-group
|
||||
%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.red{ "ng-click" => "deleteEditor(editor); $event.stopPropagation();"} Delete
|
||||
.wrap.mt-5.selectable{"ng-if" => "editor.showUrl"} {{editor.url}}
|
||||
@@ -86,8 +86,8 @@
|
||||
|
||||
%div
|
||||
.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{"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.disableRepeatAction(action, extension); $event.stopPropagation();"} Disable
|
||||
%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();"}
|
||||
Perform Action
|
||||
.spinner.mb-5.block{"ng-if" => "action.running"}
|
||||
@@ -109,7 +109,7 @@
|
||||
%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.deactivateComponent(component); $event.stopPropagation();"} Deactivate
|
||||
%div{"ng-if" => "component.showDetails"}
|
||||
.mt-3{"ng-if" => "component.showDetails"}
|
||||
.link-group
|
||||
%a.red{"ng-click" => "deleteComponent(component); $event.stopPropagation();"} Delete
|
||||
%a{"ng-click" => "component.showLink = !component.showLink; $event.stopPropagation();"} Show Link
|
||||
@@ -117,10 +117,24 @@
|
||||
%p.small.selectable.wrap{"ng-if" => "component.showLink"}
|
||||
{{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
|
||||
%h2.blue Install
|
||||
%h2.tinted Install
|
||||
%p.faded Enter an install link
|
||||
%form.mt-10.mb-10
|
||||
%input.form-control{:autofocus => 'autofocus', :name => 'url', :required => true, :autocomplete => "off",
|
||||
: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}}
|
||||
|
||||
%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
|
||||
%h4 Details
|
||||
@@ -22,4 +22,4 @@
|
||||
|
||||
.buttons
|
||||
%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"}
|
||||
%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
|
||||
%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
|
||||
%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()"}
|
||||
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.restoreDisabledComponents()"} Restore Disabled Components
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
.pull-left
|
||||
.footer-bar-link{"click-outside" => "ctrl.showAccountMenu = false;", "is-open" => "ctrl.showAccountMenu"}
|
||||
%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"}
|
||||
%a{"ng-click" => "ctrl.toggleExtensions()"} Extensions
|
||||
@@ -15,7 +15,7 @@
|
||||
.pull-right
|
||||
|
||||
.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;"}
|
||||
%div{"ng-if" => "ctrl.lastSyncDate", "style" => "float: left; font-weight: normal; margin-right: 8px;"}
|
||||
@@ -26,3 +26,6 @@
|
||||
|
||||
%strong{"ng-if" => "ctrl.offline"} Offline
|
||||
%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
|
||||
.app
|
||||
%tags-section{"save" => "tagsSave", "add-new" => "tagsAddNew", "will-select" => "tagsWillMakeSelection", "selection-made" => "tagsSelectionMade", "all-tag" => "allTag",
|
||||
"tags" => "tags", "remove-tag" => "removeTag"}
|
||||
%lock-screen{"ng-if" => "needsUnlock", "on-success" => "onSuccessfulUnlock"}
|
||||
.app{"ng-if" => "!needsUnlock"}
|
||||
%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"}
|
||||
|
||||
%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()"} ✕
|
||||
%ul.section-menu-bar#tag-menu-bar
|
||||
%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"}
|
||||
%li
|
||||
@@ -27,12 +27,22 @@
|
||||
|
||||
.scrollable
|
||||
.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}"}
|
||||
%strong.red.medium{"ng-if" => "note.conflict_of"} Conflicted copy
|
||||
%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"}
|
||||
{{note.title}}
|
||||
.note-preview
|
||||
{{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.saveTag($event, tag)", "ng-if" => "ctrl.editingTag"} Save
|
||||
%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>
|
||||
<html ng-app="app.frontend" ng-controller="BaseCtrl">
|
||||
<html ng-app="app.frontend">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
|
||||
|
||||
@@ -53,12 +53,12 @@ module Neeto
|
||||
font_src: %w('self'),
|
||||
form_action: %w('self'),
|
||||
frame_ancestors: %w('none'),
|
||||
img_src: %w('self' piwik.standardnotes.org data:),
|
||||
img_src: %w('self' data:),
|
||||
manifest_src: %w('self'),
|
||||
media_src: %w('self'),
|
||||
object_src: %w('self'),
|
||||
plugin_types: %w(),
|
||||
script_src: %w('self' 'unsafe-inline' piwik.standardnotes.org),
|
||||
script_src: %w('self' 'unsafe-inline'),
|
||||
style_src: %w(* 'unsafe-inline'),
|
||||
upgrade_insecure_requests: false, # see https://www.w3.org/TR/upgrade-insecure-requests/
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"grunt-babel": "^6.0.0",
|
||||
"grunt-browserify": "^5.0.0",
|
||||
"grunt-contrib-concat": "^1.0.1",
|
||||
"grunt-contrib-copy": "^1.0.0",
|
||||
"grunt-contrib-cssmin": "^1.0.2",
|
||||
"grunt-contrib-sass": "^1.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