diff --git a/Gemfile b/Gemfile
index af629c959..1fc8424e9 100644
--- a/Gemfile
+++ b/Gemfile
@@ -23,6 +23,8 @@ gem 'sdoc', '~> 0.4.0', group: :doc
# Used for 'respond_to' feature
gem 'responders', '~> 2.0'
+gem 'tzinfo-data'
+
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug'
diff --git a/Gemfile.lock b/Gemfile.lock
index 1056e5c0b..117dd64aa 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -76,6 +76,7 @@ GEM
erubi (1.7.1)
execjs (2.7.0)
ffi (1.9.23)
+ ffi (1.9.23-x64-mingw32)
globalid (0.4.1)
activesupport (>= 4.2.0)
haml (5.0.4)
@@ -99,6 +100,8 @@ GEM
nio4r (2.3.0)
nokogiri (1.8.2)
mini_portile2 (~> 2.3.0)
+ nokogiri (1.8.2-x64-mingw32)
+ mini_portile2 (~> 2.3.0)
non-stupid-digest-assets (1.0.9)
sprockets (>= 2.0)
puma (3.11.4)
@@ -172,6 +175,8 @@ GEM
tilt (2.0.8)
tzinfo (1.2.5)
thread_safe (~> 0.1)
+ tzinfo-data (1.2018.7)
+ tzinfo (>= 1.0.0)
uglifier (4.1.10)
execjs (>= 0.3.0, < 3)
web-console (3.5.1)
@@ -185,6 +190,7 @@ GEM
PLATFORMS
ruby
+ x64-mingw32
DEPENDENCIES
bower-rails (~> 0.10.0)
@@ -207,8 +213,9 @@ DEPENDENCIES
sdoc (~> 0.4.0)
secure_headers
spring
+ tzinfo-data
uglifier
web-console (= 3.5.1)
BUNDLED WITH
- 1.16.3
+ 1.17.1
diff --git a/Gruntfile.js b/Gruntfile.js
index f35fa3316..a4c4cbf0f 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -5,7 +5,7 @@ module.exports = function(grunt) {
watch: {
haml: {
files: ['app/assets/templates/**/*.haml'],
- tasks: ['newer:haml', 'ngtemplates', 'concat:app', 'babel', 'browserify', 'concat:dist'],
+ tasks: ['newer:haml', 'ngtemplates', 'concat:app', 'babel', 'browserify', 'concat:dist', 'ngAnnotate'],
options: {
spawn: false,
},
@@ -166,6 +166,6 @@ module.exports = function(grunt) {
grunt.registerTask('default', ['haml', 'ngtemplates', 'sass', 'concat:app', 'babel', 'browserify',
'concat:lib', 'concat:dist', 'ngAnnotate', 'concat:css', 'uglify']);
- grunt.registerTask('vendor', ['concat:app', 'babel', 'browserify',
+ grunt.registerTask('vendor', ['concat:app', 'sass', 'babel', 'browserify',
'concat:lib', 'concat:dist', 'ngAnnotate', 'concat:css', 'uglify']);
};
diff --git a/app/assets/javascripts/app/controllers/editor.js b/app/assets/javascripts/app/controllers/editor.js
index 1edba4c77..b1b196bb6 100644
--- a/app/assets/javascripts/app/controllers/editor.js
+++ b/app/assets/javascripts/app/controllers/editor.js
@@ -22,10 +22,12 @@ angular.module('app')
}
}
})
- .controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, actionsManager, syncManager, modelManager, themeManager, componentManager, storageManager, sessionHistory) {
+ .controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, actionsManager,
+ syncManager, modelManager, themeManager, componentManager, storageManager, sessionHistory, privilegesManager) {
this.spellcheck = true;
this.componentManager = componentManager;
+ this.componentStack = [];
$rootScope.$on("sync:taking-too-long", function(){
this.syncTakingTooLong = true;
@@ -87,11 +89,14 @@ angular.module('app')
}
});
- // Observe editor changes to see if the current note should update its editor
modelManager.addItemSyncObserver("editor-component-observer", "SN|Component", (allItems, validItems, deletedItems, source) => {
if(!this.note) { return; }
+ // Reload componentStack in case new ones were added or removed
+ this.reloadComponentStackArray();
+
+ // Observe editor changes to see if the current note should update its editor
var editors = allItems.filter(function(item) {
return item.isEditor();
});
@@ -157,24 +162,7 @@ angular.module('app')
}
this.editorForNote = function(note) {
- let editors = componentManager.componentsForArea("editor-editor");
- for(var editor of editors) {
- if(editor.isExplicitlyEnabledForItem(note)) {
- return editor;
- }
- }
-
- // No editor found for note. Use default editor, if note does not prefer system editor
- if(!note.getAppDataItem("prefersPlainEditor")) {
- return editors.filter((e) => {return e.isDefaultEditor()})[0];
- }
- }
-
- this.onEditorMenuClick = function() {
- // App bar menu item click
- this.showEditorMenu = !this.showEditorMenu;
- this.showMenu = false;
- this.showExtensions = false;
+ return componentManager.editorForNote(note);
}
this.closeAllMenus = function() {
@@ -183,6 +171,17 @@ angular.module('app')
this.showExtensions = false;
}
+ this.toggleMenu = function(menu) {
+ this[menu] = !this[menu];
+
+ let allMenus = ['showMenu', 'showEditorMenu', 'showExtensions', 'showSessionHistory'];
+ for(var candidate of allMenus) {
+ if(candidate != menu) {
+ this[candidate] = false;
+ }
+ }
+ }
+
this.editorMenuOnSelect = function(component) {
if(!component || component.area == "editor-editor") {
// if plain editor or other editor
@@ -329,7 +328,7 @@ angular.module('app')
}
this.showSavingStatus = function() {
- this.noteStatus = $sce.trustAsHtml("Saving...");
+ this.noteStatus = {message: "Saving..."};
}
this.showAllChangesSavedStatus = function() {
@@ -340,7 +339,7 @@ angular.module('app')
if(authManager.offline()) {
status += " (offline)";
}
- this.noteStatus = $sce.trustAsHtml(status);
+ this.noteStatus = {message: status};
}
this.showErrorStatus = function(error) {
@@ -352,7 +351,7 @@ angular.module('app')
}
this.saveError = true;
this.syncTakingTooLong = false;
- this.noteStatus = $sce.trustAsHtml(`${error.message}
${error.desc}`)
+ this.noteStatus = error;
}
this.contentChanged = function() {
@@ -382,16 +381,28 @@ angular.module('app')
}
}
- this.deleteNote = function() {
- if(this.note.locked) {
- alert("This note is locked. If you'd like to delete it, unlock it, and try again.");
- return;
+ this.deleteNote = async function() {
+ let run = () => {
+ $timeout(() => {
+ if(this.note.locked) {
+ alert("This note is locked. If you'd like to delete it, unlock it, and try again.");
+ return;
+ }
+
+ let title = this.note.safeTitle().length ? `'${this.note.title}'` : "this note";
+ if(confirm(`Are you sure you want to delete ${title}?`)) {
+ this.remove()(this.note);
+ this.showMenu = false;
+ }
+ });
}
- let title = this.note.safeTitle().length ? `'${this.note.title}'` : "this note";
- if(confirm(`Are you sure you want to delete ${title}?`)) {
- this.remove()(this.note);
- this.showMenu = false;
+ if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionDeleteNote)) {
+ privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionDeleteNote, () => {
+ run();
+ });
+ } else {
+ run();
}
}
@@ -405,6 +416,18 @@ angular.module('app')
this.changesMade({dontUpdateClientModified: true, dontUpdatePreviews: true});
}
+ this.toggleProtectNote = function() {
+ this.note.content.protected = !this.note.content.protected;
+ this.changesMade({dontUpdateClientModified: true, dontUpdatePreviews: true});
+
+ // Show privilegesManager if Protection is not yet set up
+ privilegesManager.actionHasPrivilegesConfigured(PrivilegesManager.ActionViewProtectedNotes).then((configured) => {
+ if(!configured) {
+ privilegesManager.presentPrivilegesManagementModal();
+ }
+ })
+ }
+
this.toggleNotePreview = function() {
this.note.content.hidePreview = !this.note.content.hidePreview;
this.changesMade({dontUpdateClientModified: true, dontUpdatePreviews: true});
@@ -499,7 +522,7 @@ angular.module('app')
this.loadPreferences = function() {
this.monospaceFont = authManager.getUserPrefValue("monospaceFont", "monospace");
this.spellcheck = authManager.getUserPrefValue("spellcheck", true);
- this.marginResizersEnabled = authManager.getUserPrefValue("marginResizersEnabled", false);
+ this.marginResizersEnabled = authManager.getUserPrefValue("marginResizersEnabled", true);
if(!document.getElementById("editor-content")) {
// Elements have not yet loaded due to ng-if around wrapper
@@ -601,7 +624,9 @@ angular.module('app')
this.reloadComponentContext();
}
}, contextRequestHandler: (component) => {
- return this.note;
+ if(component == this.selectedEditor || this.componentStack.includes(component)) {
+ return this.note;
+ }
}, focusHandler: (component, focused) => {
if(component.isEditor() && focused) {
this.closeAllMenus();
@@ -662,9 +687,18 @@ angular.module('app')
}
}});
+ this.reloadComponentStackArray = function() {
+ this.componentStack = componentManager.componentsForArea("editor-stack").sort((a, b) => {
+ // Careful here. For some reason (probably because re-assigning array everytime quickly destroys componentView elements, causing deallocs),
+ // sorting by updated_at (or any other property that may always be changing)
+ // causes weird problems with ext communication when changing notes or activating/deactivating in quick succession
+ return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
+ });
+ }
+
this.reloadComponentContext = function() {
// componentStack is used by the template to ng-repeat
- this.componentStack = componentManager.componentsForArea("editor-stack");
+ this.reloadComponentStackArray();
/*
In the past, we were doing this looping code even if the note wasn't currently defined.
The problem is if an editor stack item loaded first, requested to stream items, and the note was undefined,
@@ -679,7 +713,7 @@ angular.module('app')
if(this.note) {
for(var component of this.componentStack) {
if(component.active) {
- component.hidden = component.isExplicitlyDisabledForItem(this.note);
+ component.hidden = !component.isExplicitlyEnabledForItem(this.note);
}
}
}
@@ -690,13 +724,15 @@ angular.module('app')
}
this.toggleStackComponentForCurrentItem = function(component) {
- if(component.hidden) {
+ // If it's hidden, we want to show it
+ // If it's not active, then hidden won't be set, and we mean to activate and show it.
+ if(component.hidden || !component.active) {
// Unhide, associate with current item
component.hidden = false;
+ this.associateComponentWithCurrentNote(component);
if(!component.active) {
componentManager.activateComponent(component);
}
- this.associateComponentWithCurrentNote(component);
componentManager.contextItemDidChangeInArea("editor-stack");
} else {
// not hidden, hide
diff --git a/app/assets/javascripts/app/controllers/footer.js b/app/assets/javascripts/app/controllers/footer.js
index d408d1be4..15fcd07f2 100644
--- a/app/assets/javascripts/app/controllers/footer.js
+++ b/app/assets/javascripts/app/controllers/footer.js
@@ -23,7 +23,8 @@ angular.module('app')
}
})
.controller('FooterCtrl', function ($rootScope, authManager, modelManager, $timeout, dbManager,
- syncManager, storageManager, passcodeManager, componentManager, singletonManager, nativeExtManager) {
+ syncManager, storageManager, passcodeManager, componentManager, singletonManager, nativeExtManager,
+ privilegesManager) {
authManager.checkForSecurityUpdate().then((available) => {
this.securityUpdateAvailable = available;
@@ -137,11 +138,70 @@ angular.module('app')
this.componentManager = componentManager;
this.rooms = [];
+ this.themesWithIcons = [];
modelManager.addItemSyncObserver("room-bar", "SN|Component", (allItems, validItems, deletedItems, source) => {
this.rooms = modelManager.components.filter((candidate) => {return candidate.area == "rooms" && !candidate.deleted});
});
+ modelManager.addItemSyncObserver("footer-bar-themes", "SN|Theme", (allItems, validItems, deletedItems, source) => {
+ let themes = modelManager.validItemsForContentType("SN|Theme").filter((candidate) => {
+ return !candidate.deleted && candidate.content.package_info.dock_icon;
+ }).sort((a, b) => {
+ return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
+ });
+
+ let differ = themes.length != this.themesWithIcons.length;
+
+ this.themesWithIcons = themes;
+
+ if(differ) {
+ this.reloadDockShortcuts();
+ }
+ });
+
+ this.reloadDockShortcuts = function() {
+ let shortcuts = [];
+ for(var theme of this.themesWithIcons) {
+ var icon = theme.content.package_info.dock_icon;
+ if(!icon) {
+ continue;
+ }
+ shortcuts.push({
+ component: theme,
+ icon: icon
+ })
+ }
+
+ this.dockShortcuts = shortcuts.sort((a, b) => {
+ // circles first, then images
+
+ var aType = a.icon.type;
+ var bType = b.icon.type;
+
+ if(aType == bType) {
+ return 0;
+ } else if(aType == "circle" && bType == "svg") {
+ return -1;
+ } else if(bType == "circle" && aType == "svg") {
+ return 1;
+ }
+ });
+ }
+
+ this.initSvgForShortcut = function(shortcut) {
+ var id = "dock-svg-" + shortcut.component.uuid;
+ var element = document.getElementById(id);
+ var parser = new DOMParser();
+ var svg = shortcut.component.content.package_info.dock_icon.source;
+ var doc = parser.parseFromString(svg, "image/svg+xml");
+ element.appendChild(doc.documentElement);
+ }
+
+ this.selectShortcut = function(shortcut) {
+ componentManager.toggleComponent(shortcut.component);
+ }
+
componentManager.registerHandler({identifier: "roomBar", areas: ["rooms", "modal"], activationHandler: (component) => {
// RIP: There used to be code here that checked if component.active was true, and if so, displayed the component.
// However, we no longer want to persist active state for footer extensions. If you open Extensions on one computer,
@@ -172,7 +232,31 @@ angular.module('app')
}
}
- this.selectRoom = function(room) {
- room.showRoom = !room.showRoom;
+ this.selectRoom = async function(room) {
+ let run = () => {
+ $timeout(() => {
+ room.showRoom = !room.showRoom;
+ })
+ }
+
+ if(!room.showRoom) {
+ // About to show, check if has privileges
+ if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageExtensions)) {
+ privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageExtensions, () => {
+ run();
+ });
+ } else {
+ run();
+ }
+ } else {
+ run();
+ }
+ }
+
+ this.clickOutsideAccountMenu = function() {
+ if(privilegesManager.authenticationInProgress()) {
+ return;
+ }
+ this.showAccountMenu = false;
}
});
diff --git a/app/assets/javascripts/app/controllers/home.js b/app/assets/javascripts/app/controllers/home.js
index 6d8805094..0c155635f 100644
--- a/app/assets/javascripts/app/controllers/home.js
+++ b/app/assets/javascripts/app/controllers/home.js
@@ -1,6 +1,7 @@
angular.module('app')
.controller('HomeCtrl', function ($scope, $location, $rootScope, $timeout, modelManager,
- dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager, migrationManager) {
+ dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager, migrationManager,
+ privilegesManager) {
storageManager.initialize(passcodeManager.hasPasscode(), authManager.isEphemeralSession());
@@ -10,6 +11,17 @@ angular.module('app')
$rootScope.$broadcast('new-update-available', version);
}
+ $rootScope.$on("panel-resized", (event, info) => {
+ if(info.panel == "notes") { this.notesCollapsed = info.collapsed; }
+ if(info.panel == "tags") { this.tagsCollapsed = info.collapsed; }
+
+ let appClass = "";
+ if(this.notesCollapsed) { appClass += "collapsed-notes"; }
+ if(this.tagsCollapsed) { appClass += " collapsed-tags"; }
+
+ $scope.appClass = appClass;
+ })
+
/* Used to avoid circular dependencies where syncManager cannot be imported but rootScope can */
$rootScope.sync = function(source) {
syncManager.sync();
@@ -83,14 +95,14 @@ angular.module('app')
syncManager.loadLocalItems().then(() => {
$timeout(() => {
$scope.allTag.didLoad = true;
- $rootScope.$broadcast("initial-data-loaded");
+ $rootScope.$broadcast("initial-data-loaded"); // This needs to be processed first before sync is called so that singletonManager observers function properly.
+ syncManager.sync();
+ // refresh every 30s
+ setInterval(function () {
+ syncManager.sync();
+ }, 30000);
})
- syncManager.sync();
- // refresh every 30s
- setInterval(function () {
- syncManager.sync();
- }, 30000);
});
authManager.addEventHandler((event) => {
@@ -196,7 +208,7 @@ angular.module('app')
modelManager.setItemToBeDeleted(tag);
syncManager.sync().then(() => {
// force scope tags to update on sub directives
- $scope.safeApply();
+ $rootScope.safeApply();
});
}
}
@@ -218,7 +230,7 @@ angular.module('app')
Shared Callbacks
*/
- $scope.safeApply = function(fn) {
+ $rootScope.safeApply = function(fn) {
var phase = this.$root.$$phase;
if(phase == '$apply' || phase == '$digest')
this.$eval(fn);
@@ -250,7 +262,7 @@ angular.module('app')
// when deleting items while ofline, we need to explictly tell angular to refresh UI
setTimeout(function () {
$rootScope.notifyDelete();
- $scope.safeApply();
+ $rootScope.safeApply();
}, 50);
} else {
$timeout(() => {
diff --git a/app/assets/javascripts/app/controllers/lockScreen.js b/app/assets/javascripts/app/controllers/lockScreen.js
index e38eff9f6..8f3a9efd0 100644
--- a/app/assets/javascripts/app/controllers/lockScreen.js
+++ b/app/assets/javascripts/app/controllers/lockScreen.js
@@ -41,7 +41,6 @@ class LockScreen {
})
}
}
-
}
angular.module('app').directive('lockScreen', () => new LockScreen);
diff --git a/app/assets/javascripts/app/controllers/notes.js b/app/assets/javascripts/app/controllers/notes.js
index 8fc2ca944..374560039 100644
--- a/app/assets/javascripts/app/controllers/notes.js
+++ b/app/assets/javascripts/app/controllers/notes.js
@@ -31,7 +31,8 @@ angular.module('app')
}
}
})
- .controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager, storageManager, desktopManager) {
+ .controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager,
+ storageManager, desktopManager, privilegesManager) {
this.panelController = {};
this.searchSubmitted = false;
@@ -44,6 +45,7 @@ angular.module('app')
let prevSortValue = this.sortBy;
this.sortBy = authManager.getUserPrefValue("sortBy", "created_at");
+ this.sortReverse = authManager.getUserPrefValue("sortReverse", false);
if(this.sortBy == "updated_at") {
// use client_updated_at instead
@@ -55,7 +57,6 @@ angular.module('app')
this.selectFirstNote();
})
}
- this.sortDescending = this.sortBy != "title";
this.showArchived = authManager.getUserPrefValue("showArchived", false);
this.hidePinned = authManager.getUserPrefValue("hidePinned", false);
@@ -66,14 +67,18 @@ angular.module('app')
let width = authManager.getUserPrefValue("notesPanelWidth");
if(width) {
this.panelController.setWidth(width);
+ if(this.panelController.isCollapsed()) {
+ $rootScope.$broadcast("panel-resized", {panel: "notes", collapsed: this.panelController.isCollapsed()})
+ }
}
}
this.loadPreferences();
- this.onPanelResize = function(newWidth) {
+ this.onPanelResize = function(newWidth, lastLeft, isAtMaxWidth, isCollapsed) {
authManager.setUserPrefValue("notesPanelWidth", newWidth);
authManager.syncUserPreferences();
+ $rootScope.$broadcast("panel-resized", {panel: "notes", collapsed: isCollapsed})
}
angular.element(document).ready(() => {
@@ -203,19 +208,31 @@ angular.module('app')
}
}
- this.selectNote = function(note, viaClick = false) {
+ this.selectNote = async function(note, viaClick = false) {
if(!note) {
this.createNewNote();
return;
}
- this.selectedNote = note;
- note.conflict_of = null; // clear conflict
- this.selectionMade()(note);
- this.selectedIndex = Math.max(this.visibleNotes().indexOf(note), 0);
+ let run = () => {
+ $timeout(() => {
+ this.selectedNote = note;
+ note.conflict_of = null; // clear conflict
+ this.selectionMade()(note);
+ this.selectedIndex = Math.max(this.visibleNotes().indexOf(note), 0);
- if(viaClick && this.isFiltering()) {
- desktopManager.searchText(this.noteFilter.text);
+ if(viaClick && this.isFiltering()) {
+ desktopManager.searchText(this.noteFilter.text);
+ }
+ })
+ }
+
+ if(note.content.protected && await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionViewProtectedNotes)) {
+ privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionViewProtectedNotes, () => {
+ run();
+ });
+ } else {
+ run();
}
}
@@ -298,17 +315,21 @@ angular.module('app')
this.selectedSortByCreated = function() {
this.setSortBy("created_at");
- this.sortDescending = true;
}
this.selectedSortByUpdated = function() {
this.setSortBy("client_updated_at");
- this.sortDescending = true;
}
this.selectedSortByTitle = function() {
this.setSortBy("title");
- this.sortDescending = false;
+ }
+
+ this.toggleReverseSort = function() {
+ this.selectedMenuItem();
+ this.sortReverse = !this.sortReverse;
+ authManager.setUserPrefValue("sortReverse", this.sortReverse);
+ authManager.syncUserPreferences();
}
this.setSortBy = function(type) {
diff --git a/app/assets/javascripts/app/controllers/tags.js b/app/assets/javascripts/app/controllers/tags.js
index 52d15de84..4d6daa3a7 100644
--- a/app/assets/javascripts/app/controllers/tags.js
+++ b/app/assets/javascripts/app/controllers/tags.js
@@ -47,13 +47,17 @@ angular.module('app')
let width = authManager.getUserPrefValue("tagsPanelWidth");
if(width) {
this.panelController.setWidth(width);
+ if(this.panelController.isCollapsed()) {
+ $rootScope.$broadcast("panel-resized", {panel: "tags", collapsed: this.panelController.isCollapsed()})
+ }
}
}
this.loadPreferences();
- this.onPanelResize = function(newWidth) {
+ this.onPanelResize = function(newWidth, lastLeft, isAtMaxWidth, isCollapsed) {
authManager.setUserPrefValue("tagsPanelWidth", newWidth, true);
+ $rootScope.$broadcast("panel-resized", {panel: "tags", collapsed: isCollapsed})
}
this.componentManager = componentManager;
diff --git a/app/assets/javascripts/app/directives/functional/elemReady.js b/app/assets/javascripts/app/directives/functional/elemReady.js
new file mode 100644
index 000000000..7eef56fa7
--- /dev/null
+++ b/app/assets/javascripts/app/directives/functional/elemReady.js
@@ -0,0 +1,15 @@
+angular
+.module('app')
+.directive( 'elemReady', function( $parse ) {
+ return {
+ restrict: 'A',
+ link: function( $scope, elem, attrs ) {
+ elem.ready(function(){
+ $scope.$apply(function(){
+ var func = $parse(attrs.elemReady);
+ func($scope);
+ })
+ })
+ }
+ }
+})
diff --git a/app/assets/javascripts/app/directives/functional/infiniteScroll.js b/app/assets/javascripts/app/directives/functional/infiniteScroll.js
index 3e1e633ef..21bacd325 100644
--- a/app/assets/javascripts/app/directives/functional/infiniteScroll.js
+++ b/app/assets/javascripts/app/directives/functional/infiniteScroll.js
@@ -2,9 +2,6 @@ angular.module('app').directive('infiniteScroll', [
'$rootScope', '$window', '$timeout', function($rootScope, $window, $timeout) {
return {
link: function(scope, elem, attrs) {
- // elem.css('overflow-x', 'hidden');
- // elem.css('height', 'inherit');
-
var offset = parseInt(attrs.threshold) || 0;
var e = elem[0]
diff --git a/app/assets/javascripts/app/directives/functional/snEnter.js b/app/assets/javascripts/app/directives/functional/snEnter.js
new file mode 100644
index 000000000..fe12e0983
--- /dev/null
+++ b/app/assets/javascripts/app/directives/functional/snEnter.js
@@ -0,0 +1,15 @@
+angular
+.module('app')
+.directive('snEnter', function() {
+ return function(scope, element, attrs) {
+ element.bind("keydown keypress", function(event) {
+ if(event.which === 13) {
+ scope.$apply(function(){
+ scope.$eval(attrs.snEnter, {'event': event});
+ });
+
+ event.preventDefault();
+ }
+ });
+ };
+});
diff --git a/app/assets/javascripts/app/directives/views/accountMenu.js b/app/assets/javascripts/app/directives/views/accountMenu.js
index d24684515..6407cc2f3 100644
--- a/app/assets/javascripts/app/directives/views/accountMenu.js
+++ b/app/assets/javascripts/app/directives/views/accountMenu.js
@@ -10,10 +10,11 @@ class AccountMenu {
}
controller($scope, $rootScope, authManager, modelManager, syncManager, storageManager, dbManager, passcodeManager,
- $timeout, $compile, archiveManager) {
+ $timeout, $compile, archiveManager, privilegesManager) {
'ngInject';
$scope.formData = {mergeLocal: true, ephemeral: false};
+
$scope.user = authManager.user;
syncManager.getServerURL().then((url) => {
@@ -38,7 +39,6 @@ class AccountMenu {
}
$scope.canAddPasscode = !authManager.isEphemeralSession();
-
$scope.syncStatus = syncManager.syncStatus;
$scope.submitMfaForm = function() {
@@ -167,10 +167,27 @@ class AccountMenu {
$scope.openPasswordWizard = function(type) {
// Close the account menu
$scope.close();
-
authManager.presentPasswordWizard(type);
}
+ $scope.openPrivilegesModal = async function() {
+ $scope.close();
+
+ let run = () => {
+ $timeout(() => {
+ privilegesManager.presentPrivilegesManagementModal();
+ })
+ }
+
+ if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePrivileges)) {
+ privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePrivileges, () => {
+ run();
+ });
+ } else {
+ run();
+ }
+ }
+
// Allows indexeddb unencrypted logs to be deleted
// clearAllModels will remove data from backing store, but not from working memory
// See: https://github.com/standardnotes/desktop/issues/131
@@ -229,36 +246,49 @@ class AccountMenu {
})
}
- $scope.importFileSelected = function(files) {
- $scope.importData = {};
+ $scope.importFileSelected = async function(files) {
- var file = files[0];
- var reader = new FileReader();
- reader.onload = function(e) {
- try {
- var data = JSON.parse(e.target.result);
- $timeout(function(){
- if(data.auth_params) {
- // request password
- $scope.importData.requestPassword = true;
- $scope.importData.data = data;
+ let run = () => {
+ $timeout(() => {
+ $scope.importData = {};
- $timeout(() => {
- var element = document.getElementById("import-password-request");
- if(element) {
- element.scrollIntoView(false);
+ var file = files[0];
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ try {
+ var data = JSON.parse(e.target.result);
+ $timeout(function(){
+ if(data.auth_params) {
+ // request password
+ $scope.importData.requestPassword = true;
+ $scope.importData.data = data;
+
+ $timeout(() => {
+ var element = document.getElementById("import-password-request");
+ if(element) {
+ element.scrollIntoView(false);
+ }
+ })
+ } else {
+ $scope.performImport(data, null);
}
})
- } else {
- $scope.performImport(data, null);
+ } catch (e) {
+ alert("Unable to open file. Ensure it is a proper JSON file and try again.");
}
- })
- } catch (e) {
- alert("Unable to open file. Ensure it is a proper JSON file and try again.");
- }
+ }
+
+ reader.readAsText(file);
+ })
}
- reader.readAsText(file);
+ if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageBackups)) {
+ privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => {
+ run();
+ });
+ } else {
+ run();
+ }
}
$scope.importJSONData = function(data, password, callback) {
@@ -316,7 +346,7 @@ class AccountMenu {
Export
*/
- $scope.downloadDataArchive = function() {
+ $scope.downloadDataArchive = async function() {
archiveManager.downloadBackup($scope.archiveFormData.encrypted);
}
@@ -362,6 +392,35 @@ class AccountMenu {
Passcode Lock
*/
+ $scope.passcodeAutoLockOptions = passcodeManager.getAutoLockIntervalOptions();
+
+ $scope.reloadAutoLockInterval = function() {
+ passcodeManager.getAutoLockInterval().then((interval) => {
+ $timeout(() => {
+ $scope.selectedAutoLockInterval = interval;
+ })
+ })
+ }
+
+ $scope.reloadAutoLockInterval();
+
+ $scope.selectAutoLockInterval = async function(interval) {
+ let run = async () => {
+ await passcodeManager.setAutoLockInterval(interval);
+ $timeout(() => {
+ $scope.reloadAutoLockInterval();
+ });
+ }
+
+ if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePasscode)) {
+ privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePasscode, () => {
+ run();
+ });
+ } else {
+ run();
+ }
+ }
+
$scope.hasPasscode = function() {
return passcodeManager.hasPasscode();
}
@@ -377,10 +436,12 @@ class AccountMenu {
return;
}
- let fn = $scope.formData.changingPasscode ? passcodeManager.changePasscode : passcodeManager.setPasscode;
+ let fn = $scope.formData.changingPasscode ? passcodeManager.changePasscode.bind(passcodeManager) : passcodeManager.setPasscode.bind(passcodeManager);
fn(passcode, () => {
- $timeout(function(){
+ $timeout(() => {
+ $scope.formData.passcode = null;
+ $scope.formData.confirmPasscode = null;
$scope.formData.showPasscodeForm = false;
var offline = authManager.offline();
@@ -393,27 +454,50 @@ class AccountMenu {
})
}
- $scope.changePasscodePressed = function() {
- $scope.formData.changingPasscode = true;
- $scope.addPasscodeClicked();
- $scope.formData.changingPasscode = false;
+ $scope.changePasscodePressed = async function() {
+ let run = () => {
+ $timeout(() => {
+ $scope.formData.changingPasscode = true;
+ $scope.addPasscodeClicked();
+ })
+ }
+
+ if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePasscode)) {
+ privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePasscode, () => {
+ run();
+ });
+ } else {
+ run();
+ }
}
- $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();
+ $scope.removePasscodePressed = async function() {
+ let run = () => {
+ $timeout(() => {
+ 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();
- // Don't create backup here, as if the user is temporarily removing the passcode to change it,
- // we don't want to write unencrypted data to disk.
- // $rootScope.$broadcast("major-data-change");
- }
+ if(authManager.offline()) {
+ syncManager.markAllItemsDirtyAndSaveOffline();
+ // Don't create backup here, as if the user is temporarily removing the passcode to change it,
+ // we don't want to write unencrypted data to disk.
+ // $rootScope.$broadcast("major-data-change");
+ }
+ }
+ })
+ }
+
+ if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePasscode)) {
+ privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePasscode, () => {
+ run();
+ });
+ } else {
+ run();
}
}
diff --git a/app/assets/javascripts/app/directives/views/componentView.js b/app/assets/javascripts/app/directives/views/componentView.js
index a68ea4be5..03a06b6e9 100644
--- a/app/assets/javascripts/app/directives/views/componentView.js
+++ b/app/assets/javascripts/app/directives/views/componentView.js
@@ -30,6 +30,16 @@ class ComponentView {
controller($scope, $rootScope, $timeout, componentManager, desktopManager, themeManager) {
'ngInject';
+ $scope.onVisibilityChange = function() {
+ if(document.visibilityState == "hidden") {
+ return;
+ }
+
+ if($scope.issueLoading) {
+ $scope.reloadComponent();
+ }
+ }
+
$scope.themeHandlerIdentifier = "component-view-" + Math.random();
componentManager.registerHandler({identifier: $scope.themeHandlerIdentifier, areas: ["themes"], activationHandler: (component) => {
$scope.reloadThemeStatus();
@@ -45,34 +55,58 @@ class ComponentView {
return;
}
- // activationHandlers may be called multiple times, design below to be idempotent
- if(component.active) {
- $scope.loading = true;
- let iframe = componentManager.iframeForComponent(component);
- if(iframe) {
- // begin loading error handler. If onload isn't called in x seconds, display an error
- if($scope.loadTimeout) { $timeout.cancel($scope.loadTimeout);}
- $scope.loadTimeout = $timeout(() => {
- if($scope.loading) {
- $scope.issueLoading = true;
- }
- }, 3500);
- iframe.onload = function(event) {
- // console.log("iframe loaded for component", component.name, "cancelling load timeout", $scope.loadTimeout);
- $timeout.cancel($scope.loadTimeout);
- $scope.loading = false;
- $scope.issueLoading = false;
- componentManager.registerComponentWindow(component, iframe.contentWindow);
- }.bind(this);
- }
- }
- },
+ $timeout(() => {
+ $scope.handleActivation();
+ })
+ },
actionHandler: (component, action, data) => {
if(action == "set-size") {
componentManager.handleSetSizeEvent(component, data);
}
- }}
- );
+ }
+ });
+
+ $scope.handleActivation = function() {
+ // activationHandlers may be called multiple times, design below to be idempotent
+ let component = $scope.component;
+ if(!component.active) {
+ return;
+ }
+
+ $scope.loading = true;
+
+ let iframe = componentManager.iframeForComponent(component);
+ if(iframe) {
+ // begin loading error handler. If onload isn't called in x seconds, display an error
+ if($scope.loadTimeout) { $timeout.cancel($scope.loadTimeout);}
+ $scope.loadTimeout = $timeout(() => {
+ if($scope.loading) {
+ $scope.loading = false;
+ $scope.issueLoading = true;
+
+ if(!$scope.didAttemptReload) {
+ $scope.didAttemptReload = true;
+ $scope.reloadComponent();
+ } else {
+ // We'll attempt to reload when the tab gains focus
+ document.addEventListener("visibilitychange", $scope.onVisibilityChange);
+ }
+ }
+ }, 3500);
+ iframe.onload = (event) => {
+ $timeout.cancel($scope.loadTimeout);
+ componentManager.registerComponentWindow(component, iframe.contentWindow);
+
+ // Add small timeout to, as $scope.loading controls loading overlay,
+ // which is used to avoid flicker when enabling extensions while having an enabled theme
+ // we don't use ng-show because it causes problems with rendering iframes after timeout, for some reason.
+ $timeout(() => {
+ $scope.loading = false;
+ $scope.issueLoading = false;
+ }, 7)
+ };
+ }
+ }
/*
@@ -106,7 +140,11 @@ class ComponentView {
$scope.reloadComponent = function() {
console.log("Reloading component", $scope.component);
- componentManager.reloadComponent($scope.component);
+ // force iFrame to deinit, allows new one to be created
+ $scope.componentValid = false;
+ componentManager.reloadComponent($scope.component).then(() => {
+ $scope.reloadStatus();
+ });
}
$scope.reloadStatus = function(doManualReload = true) {
@@ -114,7 +152,7 @@ class ComponentView {
$scope.reloading = true;
let previouslyValid = $scope.componentValid;
- var expired, offlineRestricted, urlError;
+ var offlineRestricted, urlError;
offlineRestricted = component.offlineOnly && !isDesktopApplication();
@@ -123,13 +161,23 @@ class ComponentView {
||
(isDesktopApplication() && (!component.local_url && !component.hasValidHostedUrl()))
- expired = component.valid_until && component.valid_until <= new Date();
+ $scope.expired = component.valid_until && component.valid_until <= new Date();
- $scope.componentValid = !offlineRestricted && !urlError && !expired;
+ // Here we choose our own readonly state based on custom logic. However, if a parent
+ // wants to implement their own readonly logic, they can lock it.
+ if(!component.lockReadonly) {
+ component.readonly = $scope.expired;
+ }
+
+ $scope.componentValid = !offlineRestricted && !urlError;
+
+ if(!$scope.componentValid) {
+ // required to disable overlay
+ $scope.loading = false;
+ }
if(offlineRestricted) $scope.error = 'offline-restricted';
else if(urlError) $scope.error = 'url-missing';
- else if(expired) $scope.error = 'expired';
else $scope.error = null;
if($scope.componentValid !== previouslyValid) {
@@ -139,7 +187,7 @@ class ComponentView {
}
}
- if(expired && doManualReload) {
+ if($scope.expired && doManualReload) {
// Try reloading, handled by footer, which will open Extensions window momentarily to pull in latest data
// Upon completion, this method, reloadStatus, will be called, upon where doManualReload will be false to prevent recursion.
$rootScope.$broadcast("reload-ext-data");
@@ -189,10 +237,10 @@ class ComponentView {
}
desktopManager.deregisterUpdateObserver($scope.updateObserver);
+ document.removeEventListener("visibilitychange", $scope.onVisibilityChange);
}
$scope.$on("$destroy", function() {
- // console.log("Deregistering handler", $scope.identifier, $scope.component.name);
$scope.destroy();
});
}
diff --git a/app/assets/javascripts/app/directives/views/editorMenu.js b/app/assets/javascripts/app/directives/views/editorMenu.js
index 8736f484b..47bde5dd2 100644
--- a/app/assets/javascripts/app/directives/views/editorMenu.js
+++ b/app/assets/javascripts/app/directives/views/editorMenu.js
@@ -19,10 +19,6 @@ class EditorMenu {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
- $scope.stack = componentManager.componentsForArea("editor-stack").sort((a, b) => {
- return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
- });
-
$scope.isDesktop = isDesktopApplication();
$scope.defaultEditor = $scope.editors.filter((e) => {return e.isDefaultEditor()})[0];
@@ -77,17 +73,10 @@ class EditorMenu {
if(component == $scope.selectedEditor) {
return true;
- } else if(component.area == "editor-stack") {
- return $scope.stackComponentEnabled(component);
} else {
return false;
}
}
-
- $scope.stackComponentEnabled = function(component) {
- return component.active && !component.isExplicitlyDisabledForItem($scope.currentItem);
- }
-
}
}
diff --git a/app/assets/javascripts/app/directives/views/menuRow.js b/app/assets/javascripts/app/directives/views/menuRow.js
index 999e64f9f..49fbb5e0a 100644
--- a/app/assets/javascripts/app/directives/views/menuRow.js
+++ b/app/assets/javascripts/app/directives/views/menuRow.js
@@ -7,6 +7,7 @@ class MenuRow {
this.scope = {
action: "&",
circle: "=",
+ circleAlign: "=",
label: "=",
subtitle: "=",
hasButton: "=",
diff --git a/app/assets/javascripts/app/directives/views/panelResizer.js b/app/assets/javascripts/app/directives/views/panelResizer.js
index fc31c0ca6..0be60fffa 100644
--- a/app/assets/javascripts/app/directives/views/panelResizer.js
+++ b/app/assets/javascripts/app/directives/views/panelResizer.js
@@ -32,6 +32,10 @@ class PanelResizer {
scope.control.flash = function() {
scope.flash();
}
+
+ scope.control.isCollapsed = function() {
+ return scope.isCollapsed();
+ }
}
controller($scope, $element, modelManager, actionsManager, $timeout, $compile) {
@@ -52,16 +56,19 @@ class PanelResizer {
// Handle Double Click Event
var widthBeforeLastDblClick = 0;
resizerColumn.ondblclick = () => {
- var collapsed = $scope.isCollapsed();
$timeout(() => {
- if(collapsed) {
+ var preClickCollapseState = $scope.isCollapsed();
+ if(preClickCollapseState) {
$scope.setWidth(widthBeforeLastDblClick || $scope.defaultWidth);
} else {
widthBeforeLastDblClick = lastWidth;
$scope.setWidth(minWidth);
}
+
$scope.finishSettingWidth();
- $scope.onResizeFinish()(lastWidth, lastLeft, $scope.isAtMaxWidth());
+
+ var newCollapseState = !preClickCollapseState;
+ $scope.onResizeFinish()(lastWidth, lastLeft, $scope.isAtMaxWidth(), newCollapseState);
})
}
@@ -274,7 +281,7 @@ class PanelResizer {
let isMaxWidth = $scope.isAtMaxWidth();
if($scope.onResizeFinish) {
- $scope.onResizeFinish()(lastWidth, lastLeft, isMaxWidth);
+ $scope.onResizeFinish()(lastWidth, lastLeft, isMaxWidth, $scope.isCollapsed());
}
$scope.finishSettingWidth();
diff --git a/app/assets/javascripts/app/directives/views/passwordWizard.js b/app/assets/javascripts/app/directives/views/passwordWizard.js
index f93bed6d5..7c78e2cbb 100644
--- a/app/assets/javascripts/app/directives/views/passwordWizard.js
+++ b/app/assets/javascripts/app/directives/views/passwordWizard.js
@@ -102,6 +102,9 @@ class PasswordWizard {
if(preprocessor) {
preprocessor(() => {
next();
+ }, () => {
+ // on fail
+ $scope.isContinuing = false;
})
} else {
next();
@@ -114,7 +117,7 @@ class PasswordWizard {
$scope.preprocessorForStep = function(step) {
if(step == PasswordStep) {
- return (callback) => {
+ return (onSuccess, onFail) => {
$scope.showSpinner = true;
$scope.continueTitle = "Generating Keys...";
$timeout(() => {
@@ -122,7 +125,9 @@ class PasswordWizard {
$scope.showSpinner = false;
$scope.continueTitle = DefaultContinueTitle;
if(success) {
- callback();
+ onSuccess();
+ } else {
+ onFail && onFail();
}
});
})
diff --git a/app/assets/javascripts/app/directives/views/privilegesAuthModal.js b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js
new file mode 100644
index 000000000..8440fd849
--- /dev/null
+++ b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js
@@ -0,0 +1,92 @@
+class PrivilegesAuthModal {
+
+ constructor() {
+ this.restrict = "E";
+ this.templateUrl = "directives/privileges-auth-modal.html";
+ this.scope = {
+ action: "=",
+ onSuccess: "=",
+ onCancel: "=",
+ };
+ }
+
+ link($scope, el, attrs) {
+ $scope.dismiss = function() {
+ el.remove();
+ }
+ }
+
+ controller($scope, privilegesManager, passcodeManager, authManager, $timeout) {
+ 'ngInject';
+
+ $scope.authenticationParameters = {};
+ $scope.sessionLengthOptions = privilegesManager.getSessionLengthOptions();
+
+ privilegesManager.getSelectedSessionLength().then((length) => {
+ $timeout(() => {
+ $scope.selectedSessionLength = length;
+ })
+ })
+
+ $scope.selectSessionLength = function(length) {
+ $scope.selectedSessionLength = length;
+ }
+
+ privilegesManager.netCredentialsForAction($scope.action).then((credentials) => {
+ $timeout(() => {
+ $scope.requiredCredentials = credentials.sort();
+ });
+ });
+
+ $scope.promptForCredential = function(credential) {
+ return privilegesManager.displayInfoForCredential(credential).prompt;
+ }
+
+ $scope.cancel = function() {
+ $scope.dismiss();
+ $scope.onCancel && $scope.onCancel();
+ }
+
+ $scope.isCredentialInFailureState = function(credential) {
+ if(!$scope.failedCredentials) {
+ return false;
+ }
+ return $scope.failedCredentials.find((candidate) => {
+ return candidate == credential;
+ }) != null;
+ }
+
+ $scope.validate = function() {
+ var failed = [];
+ for(var cred of $scope.requiredCredentials) {
+ var value = $scope.authenticationParameters[cred];
+ if(!value || value.length == 0) {
+ failed.push(cred);
+ }
+ }
+
+ $scope.failedCredentials = failed;
+ return failed.length == 0;
+ }
+
+ $scope.submit = function() {
+ if(!$scope.validate()) {
+ return;
+ }
+ privilegesManager.authenticateAction($scope.action, $scope.authenticationParameters).then((result) => {
+ $timeout(() => {
+ if(result.success) {
+ privilegesManager.setSessionLength($scope.selectedSessionLength);
+ $scope.onSuccess();
+ $scope.dismiss();
+ } else {
+ $scope.failedCredentials = result.failedCredentials;
+ }
+ })
+ })
+ }
+
+ }
+}
+
+angular.module('app').directive('privilegesAuthModal', () => new PrivilegesAuthModal);
diff --git a/app/assets/javascripts/app/directives/views/privilegesManagementModal.js b/app/assets/javascripts/app/directives/views/privilegesManagementModal.js
new file mode 100644
index 000000000..bec8691d4
--- /dev/null
+++ b/app/assets/javascripts/app/directives/views/privilegesManagementModal.js
@@ -0,0 +1,88 @@
+class PrivilegesManagementModal {
+
+ constructor() {
+ this.restrict = "E";
+ this.templateUrl = "directives/privileges-management-modal.html";
+ this.scope = {
+
+ };
+ }
+
+ link($scope, el, attrs) {
+ $scope.dismiss = function() {
+ el.remove();
+ }
+ }
+
+ controller($scope, privilegesManager, passcodeManager, authManager, $timeout) {
+ 'ngInject';
+
+ $scope.dummy = {};
+
+ $scope.hasPasscode = passcodeManager.hasPasscode();
+ $scope.hasAccount = !authManager.offline();
+
+ $scope.displayInfoForCredential = function(credential) {
+ let info = privilegesManager.displayInfoForCredential(credential);
+ if(credential == PrivilegesManager.CredentialLocalPasscode) {
+ info["availability"] = $scope.hasPasscode;
+ } else if(credential == PrivilegesManager.CredentialAccountPassword) {
+ info["availability"] = $scope.hasAccount;
+ } else {
+ info["availability"] = true;
+ }
+
+ return info;
+ }
+
+ $scope.displayInfoForAction = function(action) {
+ return privilegesManager.displayInfoForAction(action).label;
+ }
+
+ $scope.isCredentialRequiredForAction = function(action, credential) {
+ if(!$scope.privileges) {
+ return false;
+ }
+ return $scope.privileges.isCredentialRequiredForAction(action, credential);
+ }
+
+ $scope.clearSession = function() {
+ privilegesManager.clearSession().then(() => {
+ $scope.reloadPrivileges();
+ })
+ }
+
+ $scope.reloadPrivileges = async function() {
+ $scope.availableActions = privilegesManager.getAvailableActions();
+ $scope.availableCredentials = privilegesManager.getAvailableCredentials();
+ let sessionEndDate = await privilegesManager.getSessionExpirey();
+ $scope.sessionExpirey = sessionEndDate.toLocaleString();
+ $scope.sessionExpired = new Date() >= sessionEndDate;
+
+ $scope.credentialDisplayInfo = {};
+ for(let cred of $scope.availableCredentials) {
+ $scope.credentialDisplayInfo[cred] = $scope.displayInfoForCredential(cred);
+ }
+
+ privilegesManager.getPrivileges().then((privs) => {
+ $timeout(() => {
+ $scope.privileges = privs;
+ })
+ })
+ }
+
+ $scope.checkboxValueChanged = function(action, credential) {
+ $scope.privileges.toggleCredentialForAction(action, credential);
+ privilegesManager.savePrivileges();
+ }
+
+ $scope.reloadPrivileges();
+
+ $scope.cancel = function() {
+ $scope.dismiss();
+ $scope.onCancel && $scope.onCancel();
+ }
+ }
+}
+
+angular.module('app').directive('privilegesManagementModal', () => new PrivilegesManagementModal);
diff --git a/app/assets/javascripts/app/directives/views/revisionPreviewModal.js b/app/assets/javascripts/app/directives/views/revisionPreviewModal.js
index d5d6313d3..70891f354 100644
--- a/app/assets/javascripts/app/directives/views/revisionPreviewModal.js
+++ b/app/assets/javascripts/app/directives/views/revisionPreviewModal.js
@@ -10,15 +10,56 @@ class RevisionPreviewModal {
}
link($scope, el, attrs) {
-
- $scope.dismiss = function() {
- el.remove();
- }
+ $scope.el = el;
}
- controller($scope, modelManager, syncManager) {
+ controller($scope, modelManager, syncManager, componentManager, $timeout) {
'ngInject';
+ $scope.dismiss = function() {
+ $scope.el.remove();
+ $scope.$destroy();
+ }
+
+ $scope.$on("$destroy", function() {
+ if($scope.identifier) {
+ componentManager.deregisterHandler($scope.identifier);
+ }
+ });
+
+ $scope.note = new SFItem({content: $scope.content, content_type: "Note"});
+ // Set UUID to editoForNote can find proper editor,
+ // but then generate new uuid for note as not to save changes to original, if editor makes changes.
+ $scope.note.uuid = $scope.uuid;
+ let editorForNote = componentManager.editorForNote($scope.note);
+ $scope.note.uuid = SFJS.crypto.generateUUIDSync();
+
+ if(editorForNote) {
+ // Create temporary copy, as a lot of componentManager is uuid based,
+ // so might interfere with active editor. Be sure to copy only the content, as the
+ // top level editor object has non-copyable properties like .window, which cannot be transfered
+ let editorCopy = new SNComponent({content: editorForNote.content});
+ editorCopy.readonly = true;
+ editorCopy.lockReadonly = true;
+ $scope.identifier = editorCopy.uuid;
+
+ componentManager.registerHandler({identifier: $scope.identifier, areas: ["editor-editor"],
+ contextRequestHandler: (component) => {
+ if(component == $scope.editor) {
+ return $scope.note;
+ }
+ },
+ componentForSessionKeyHandler: (key) => {
+ if(key == $scope.editor.sessionKey) {
+ return $scope.editor;
+ }
+ }
+ });
+
+ $scope.editor = editorCopy;
+ }
+
+
$scope.restore = function(asCopy) {
if(!asCopy && !confirm("Are you sure you want to replace the current note's contents with what you see in this preview?")) {
return;
diff --git a/app/assets/javascripts/app/filters/sortBy.js b/app/assets/javascripts/app/filters/sortBy.js
index e182b19a5..c1fa496a8 100644
--- a/app/assets/javascripts/app/filters/sortBy.js
+++ b/app/assets/javascripts/app/filters/sortBy.js
@@ -1,6 +1,6 @@
angular.module('app')
.filter('sortBy', function ($filter) {
- return function(items, sortBy) {
+ return function(items, sortBy, reverse) {
let sortValueFn = (a, b, pinCheck = false) => {
if(!pinCheck) {
if(a.pinned && b.pinned) {
@@ -14,6 +14,11 @@ angular.module('app')
var bValue = b[sortBy] || "";
let vector = 1;
+
+ if(reverse) {
+ vector *= -1;
+ }
+
if(sortBy == "title") {
aValue = aValue.toLowerCase();
bValue = bValue.toLowerCase();
@@ -21,11 +26,11 @@ angular.module('app')
if(aValue.length == 0 && bValue.length == 0) {
return 0;
} else if(aValue.length == 0 && bValue.length != 0) {
- return 1;
+ return 1 * vector;
} else if(aValue.length != 0 && bValue.length == 0) {
- return -1;
+ return -1 * vector;
} else {
- vector = -1;
+ vector *= -1;
}
}
diff --git a/app/assets/javascripts/app/models/privileges.js b/app/assets/javascripts/app/models/privileges.js
new file mode 100644
index 000000000..3fb5f96fa
--- /dev/null
+++ b/app/assets/javascripts/app/models/privileges.js
@@ -0,0 +1,34 @@
+class SNPrivileges extends SFItem {
+
+ setCredentialsForAction(action, credentials) {
+ this.content.desktopPrivileges[action] = credentials;
+ }
+
+ getCredentialsForAction(action) {
+ return this.content.desktopPrivileges[action] || [];
+ }
+
+ toggleCredentialForAction(action, credential) {
+ if(this.isCredentialRequiredForAction(action, credential)) {
+ this.removeCredentialForAction(action, credential);
+ } else {
+ this.addCredentialForAction(action, credential);
+ }
+ }
+
+ removeCredentialForAction(action, credential) {
+ _.pull(this.content.desktopPrivileges[action], credential);
+ }
+
+ addCredentialForAction(action, credential) {
+ var credentials = this.getCredentialsForAction(action);
+ credentials.push(credential);
+ this.setCredentialsForAction(action, credentials);
+ }
+
+ isCredentialRequiredForAction(action, credential) {
+ var credentialsRequired = this.getCredentialsForAction(action);
+ return credentialsRequired.includes(credential);
+ }
+
+}
diff --git a/app/assets/javascripts/app/services/actionsManager.js b/app/assets/javascripts/app/services/actionsManager.js
index 1953907c3..f23dfd379 100644
--- a/app/assets/javascripts/app/services/actionsManager.js
+++ b/app/assets/javascripts/app/services/actionsManager.js
@@ -202,7 +202,7 @@ class ActionsManager {
var scope = this.$rootScope.$new(true);
scope.uuid = uuid;
scope.content = content;
- var el = this.$compile( "" )(scope);
+ var el = this.$compile( "" )(scope);
angular.element(document.body).append(el);
}
diff --git a/app/assets/javascripts/app/services/archiveManager.js b/app/assets/javascripts/app/services/archiveManager.js
index 4c60c6b13..6517cf204 100644
--- a/app/assets/javascripts/app/services/archiveManager.js
+++ b/app/assets/javascripts/app/services/archiveManager.js
@@ -1,9 +1,10 @@
class ArchiveManager {
- constructor(passcodeManager, authManager, modelManager) {
+ constructor(passcodeManager, authManager, modelManager, privilegesManager) {
this.passcodeManager = passcodeManager;
this.authManager = authManager;
this.modelManager = modelManager;
+ this.privilegesManager = privilegesManager;
}
/*
@@ -15,26 +16,36 @@ class ArchiveManager {
}
async downloadBackupOfItems(items, encrypted) {
- // download in Standard File format
- var keys, authParams;
- if(encrypted) {
- if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
- keys = this.passcodeManager.keys();
- authParams = this.passcodeManager.passcodeAuthParams();
- } else {
- keys = await this.authManager.keys();
- authParams = await this.authManager.getAuthParams();
+ let run = async () => {
+ // download in Standard File format
+ var keys, authParams;
+ if(encrypted) {
+ if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
+ keys = this.passcodeManager.keys();
+ authParams = this.passcodeManager.passcodeAuthParams();
+ } else {
+ keys = await this.authManager.keys();
+ authParams = await this.authManager.getAuthParams();
+ }
}
- }
- this.__itemsData(items, keys, authParams).then((data) => {
- let modifier = encrypted ? "Encrypted" : "Decrypted";
- this.__downloadData(data, `Standard Notes ${modifier} Backup - ${this.__formattedDate()}.txt`);
+ this.__itemsData(items, keys, authParams).then((data) => {
+ let modifier = encrypted ? "Encrypted" : "Decrypted";
+ this.__downloadData(data, `Standard Notes ${modifier} Backup - ${this.__formattedDate()}.txt`);
- // download as zipped plain text files
- if(!keys) {
- this.__downloadZippedItems(items);
- }
- })
+ // download as zipped plain text files
+ if(!keys) {
+ this.__downloadZippedItems(items);
+ }
+ })
+ }
+
+ if(await this.privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageBackups)) {
+ this.privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => {
+ run();
+ });
+ } else {
+ run();
+ }
}
/*
diff --git a/app/assets/javascripts/app/services/authManager.js b/app/assets/javascripts/app/services/authManager.js
index 6787d3709..379617ca1 100644
--- a/app/assets/javascripts/app/services/authManager.js
+++ b/app/assets/javascripts/app/services/authManager.js
@@ -44,7 +44,7 @@ class AuthManager extends SFAuthManager {
this.storageManager.setItemsMode(StorageManager.Ephemeral);
} else {
this.storageManager.setModelStorageMode(StorageManager.Fixed);
- this.storageManager.setItemsMode(this.storageManager.hasPasscode() ? StorageManager.FixedEncrypted : StorageManager.Fixed);
+ this.storageManager.setItemsMode(this.storageManager.bestStorageMode());
this.storageManager.setItem("ephemeral", JSON.stringify(false), StorageManager.Fixed);
}
}
@@ -92,6 +92,13 @@ class AuthManager extends SFAuthManager {
}
}
+ async verifyAccountPassword(password) {
+ let authParams = await this.getAuthParams();
+ let keys = await SFJS.crypto.computeEncryptionKeysForUser(password, authParams);
+ let success = keys.mk === (await this.keys()).mk;
+ return success;
+ }
+
async checkForSecurityUpdate() {
if(this.offline()) {
return false;
diff --git a/app/assets/javascripts/app/services/componentManager.js b/app/assets/javascripts/app/services/componentManager.js
index 5d8fb15aa..045ceb93a 100644
--- a/app/assets/javascripts/app/services/componentManager.js
+++ b/app/assets/javascripts/app/services/componentManager.js
@@ -33,7 +33,7 @@ class ComponentManager {
desktopManager.registerUpdateObserver((component) => {
// Reload theme if active
if(component.active && component.isTheme()) {
- this.postActiveThemeToAllComponents();
+ this.postActiveThemesToAllComponents();
}
})
@@ -147,14 +147,15 @@ class ComponentManager {
});
}
- postActiveThemeToAllComponents() {
+ postActiveThemesToAllComponents() {
for(var component of this.components) {
// Skip over components that are themes themselves,
// or components that are not active, or components that don't have a window
if(component.isTheme() || !component.active || !component.window) {
continue;
}
- this.postActiveThemeToComponent(component);
+
+ this.postActiveThemesToComponent(component);
}
}
@@ -162,14 +163,16 @@ class ComponentManager {
return this.componentsForArea("themes").filter((theme) => {return theme.active});
}
- postActiveThemeToComponent(component) {
- var themes = this.getActiveThemes();
- var urls = themes.map((theme) => {
+ urlsForActiveThemes() {
+ let themes = this.getActiveThemes();
+ return themes.map((theme) => {
return this.urlForComponent(theme);
})
- var data = {
- themes: urls
- }
+ }
+
+ postActiveThemesToComponent(component) {
+ let urls = this.urlsForActiveThemes();
+ let data = { themes: urls }
this.sendMessageToComponent(component, {action: "themes", data: data})
}
@@ -291,7 +294,18 @@ class ComponentManager {
}
componentForSessionKey(key) {
- return _.find(this.components, {sessionKey: key});
+ let component = _.find(this.components, {sessionKey: key});
+ if(!component) {
+ for(let handler of this.handlers) {
+ if(handler.componentForSessionKeyHandler) {
+ component = handler.componentForSessionKeyHandler(key);
+ if(component) {
+ break;
+ }
+ }
+ }
+ }
+ return component;
}
handleMessage(component, message) {
@@ -302,6 +316,24 @@ class ComponentManager {
return;
}
+ // Actions that won't succeeed with readonly mode
+ let readwriteActions = [
+ "save-items",
+ "associate-item",
+ "deassociate-item",
+ "create-item",
+ "create-items",
+ "delete-items",
+ "set-component-data"
+ ];
+
+ if(component.readonly && readwriteActions.includes(message.action)) {
+ // A component can be marked readonly if changes should not be saved.
+ // Particullary used for revision preview windows where the notes should not be savable.
+ alert(`The extension ${component.name} is trying to save, but it is in a locked state and cannot accept changes.`);
+ return;
+ }
+
/**
Possible Messages:
set-size
@@ -343,11 +375,13 @@ class ComponentManager {
this.handleInstallLocalComponentMessage(component, message);
} else if(message.action === "present-conflict-resolution") {
this.handlePresentConflictResolutionMessage(component, message);
+ } else if(message.action === "duplicate-item") {
+ this.handleDuplicateItemMessage(component, message);
}
// Notify observers
for(let handler of this.handlers) {
- if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
+ if(handler.actionHandler && (handler.areas.includes(component.area) || handler.areas.includes("*"))) {
this.timeout(function(){
handler.actionHandler(component, message.action, message.data);
})
@@ -539,6 +573,24 @@ class ComponentManager {
});
}
+ handleDuplicateItemMessage(component, message) {
+ var itemParams = message.data.item;
+ var item = this.modelManager.findItem(itemParams.uuid);
+ var requiredPermissions = [
+ {
+ name: "stream-items",
+ content_types: [item.content_type]
+ }
+ ];
+
+ this.runWithPermissions(component, requiredPermissions, () => {
+ var duplicate = this.modelManager.duplicateItem(item);
+ this.syncManager.sync();
+
+ this.replyToMessage(component, message, {item: this.jsonForItem(duplicate, component)});
+ });
+ }
+
handleCreateItemsMessage(component, message) {
var responseItems = message.data.item ? [message.data.item] : message.data.items;
let uniqueContentTypes = _.uniq(responseItems.map((item) => {return item.content_type}));
@@ -629,22 +681,35 @@ class ComponentManager {
}
handleToggleComponentMessage(sourceComponent, targetComponent, message) {
- if(targetComponent.area == "modal") {
- this.openModalComponent(targetComponent);
+ this.toggleComponent(targetComponent);
+ }
+
+ toggleComponent(component) {
+ if(component.area == "modal") {
+ this.openModalComponent(component);
} else {
- if(targetComponent.active) {
- this.deactivateComponent(targetComponent);
+ if(component.active) {
+ this.deactivateComponent(component);
} else {
- if(targetComponent.content_type == "SN|Theme" && !targetComponent.isLayerable()) {
+ if(component.content_type == "SN|Theme") {
// Deactive currently active theme if new theme is not layerable
var activeThemes = this.getActiveThemes();
- for(var theme of activeThemes) {
- if(theme && !theme.isLayerable()) {
- this.deactivateComponent(theme);
- }
+
+ // Activate current before deactivating others, so as not to flicker
+ this.activateComponent(component);
+
+ if(!component.isLayerable()) {
+ setTimeout(() => {
+ for(var theme of activeThemes) {
+ if(theme && !theme.isLayerable()) {
+ this.deactivateComponent(theme);
+ }
+ }
+ }, 10);
}
+ } else {
+ this.activateComponent(component);
}
- this.activateComponent(targetComponent);
}
}
}
@@ -765,14 +830,14 @@ class ComponentManager {
var permissions = dialog.permissions;
var component = dialog.component;
var callback = dialog.callback;
- var el = this.$compile( "" )(dialog);
+ var el = this.$compile( "" )(dialog);
angular.element(document.body).append(el);
}
openModalComponent(component) {
var scope = this.$rootScope.$new(true);
scope.component = component;
- var el = this.$compile( "" )(scope);
+ var el = this.$compile( "" )(scope);
angular.element(document.body).append(el);
}
@@ -782,6 +847,10 @@ class ComponentManager {
deregisterHandler(identifier) {
var handler = _.find(this.handlers, {identifier: identifier});
+ if(!handler) {
+ console.log("Attempting to deregister non-existing handler");
+ return;
+ }
this.handlers.splice(this.handlers.indexOf(handler), 1);
}
@@ -805,34 +874,39 @@ class ComponentManager {
data: {
uuid: component.uuid,
environment: isDesktopApplication() ? "desktop" : "web",
- platform: getPlatformString()
+ platform: getPlatformString(),
+ activeThemeUrls: this.urlsForActiveThemes()
}
});
- this.postActiveThemeToComponent(component);
+ this.postActiveThemesToComponent(component);
this.desktopManager.notifyComponentActivation(component);
}
/* Performs func in timeout, but syncronously, if used `await waitTimeout` */
- async waitTimeout(func) {
- return new Promise((resolve, reject) => {
- this.timeout(() => {
- func();
- resolve();
- });
- })
- }
+ // No longer used. See comment in activateComponent.
+ // async waitTimeout(func) {
+ // return new Promise((resolve, reject) => {
+ // this.timeout(() => {
+ // func();
+ // resolve();
+ // });
+ // })
+ // }
async activateComponent(component, dontSync = false) {
var didChange = component.active != true;
component.active = true;
- for(var handler of this.handlers) {
+ for(let handler of this.handlers) {
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
// We want to run the handler in a $timeout so the UI updates, but we also don't want it to run asyncronously
// so that the steps below this one are run before the handler. So we run in a waitTimeout.
- await this.waitTimeout(() => {
+ // Update 12/18: We were using this.waitTimeout previously, however, that caused the iframe.onload callback to never be called
+ // for some reason for iframes on desktop inside the revision-preview-modal. So we'll use safeApply instead. I'm not quite sure
+ // where the original "so the UI updates" comment applies to, but we'll have to keep an eye out to see if this causes problems somewhere else.
+ this.$rootScope.safeApply(() => {
handler.activationHandler && handler.activationHandler(component);
})
}
@@ -848,7 +922,7 @@ class ComponentManager {
}
if(component.area == "themes") {
- this.postActiveThemeToAllComponents();
+ this.postActiveThemesToAllComponents();
}
}
@@ -859,7 +933,8 @@ class ComponentManager {
for(let handler of this.handlers) {
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
- await this.waitTimeout(() => {
+ // See comment in activateComponent regarding safeApply and awaitTimeout
+ this.$rootScope.safeApply(() => {
handler.activationHandler && handler.activationHandler(component);
})
}
@@ -881,7 +956,7 @@ class ComponentManager {
})
if(component.area == "themes") {
- this.postActiveThemeToAllComponents();
+ this.postActiveThemesToAllComponents();
}
}
@@ -893,7 +968,8 @@ class ComponentManager {
for(let handler of this.handlers) {
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
- await this.waitTimeout(() => {
+ // See comment in activateComponent regarding safeApply and awaitTimeout
+ this.$rootScope.safeApply(() => {
handler.activationHandler && handler.activationHandler(component);
})
}
@@ -908,7 +984,7 @@ class ComponentManager {
})
if(component.area == "themes") {
- this.postActiveThemeToAllComponents();
+ this.postActiveThemesToAllComponents();
}
//
@@ -919,7 +995,8 @@ class ComponentManager {
component.active = true;
for(var handler of this.handlers) {
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
- await this.waitTimeout(() => {
+ // See comment in activateComponent regarding safeApply and awaitTimeout
+ this.$rootScope.safeApply(() => {
handler.activationHandler && handler.activationHandler(component);
})
}
@@ -930,7 +1007,7 @@ class ComponentManager {
}
if(component.area == "themes") {
- this.postActiveThemeToAllComponents();
+ this.postActiveThemesToAllComponents();
}
})
}
@@ -981,10 +1058,7 @@ class ComponentManager {
if(!iframe) {
return;
}
- var width = data.width;
- var height = data.height;
- iframe.width = width;
- iframe.height = height;
+
setSize(iframe, data);
// On Firefox, resizing a component iframe does not seem to have an effect with editor-stack extensions.
@@ -1007,6 +1081,20 @@ class ComponentManager {
}
}
+ editorForNote(note) {
+ let editors = this.componentsForArea("editor-editor");
+ for(var editor of editors) {
+ if(editor.isExplicitlyEnabledForItem(note)) {
+ return editor;
+ }
+ }
+
+ // No editor found for note. Use default editor, if note does not prefer system editor
+ if(!note.getAppDataItem("prefersPlainEditor")) {
+ return editors.filter((e) => {return e.isDefaultEditor()})[0];
+ }
+ }
+
}
diff --git a/app/assets/javascripts/app/services/desktopManager.js b/app/assets/javascripts/app/services/desktopManager.js
index d437fa296..58ce492d9 100644
--- a/app/assets/javascripts/app/services/desktopManager.js
+++ b/app/assets/javascripts/app/services/desktopManager.js
@@ -79,6 +79,14 @@ class DesktopManager {
this.searchHandler = handler;
}
+ desktop_windowGainedFocus() {
+ this.$rootScope.$broadcast("window-gained-focus");
+ }
+
+ desktop_windowLostFocus() {
+ this.$rootScope.$broadcast("window-lost-focus");
+ }
+
desktop_onComponentInstallationComplete(componentData, error) {
console.log("Web|Component Installation/Update Complete", componentData, error);
diff --git a/app/assets/javascripts/app/services/httpManager.js b/app/assets/javascripts/app/services/httpManager.js
index 637db6041..a3415b4b5 100644
--- a/app/assets/javascripts/app/services/httpManager.js
+++ b/app/assets/javascripts/app/services/httpManager.js
@@ -5,7 +5,7 @@ class HttpManager extends SFHttpManager {
super($timeout);
this.setJWTRequestHandler(async () => {
- return storageManager.getItem("jwt");;
+ return storageManager.getItem("jwt");
})
}
}
diff --git a/app/assets/javascripts/app/services/modelManager.js b/app/assets/javascripts/app/services/modelManager.js
index c3f30c207..9af2df562 100644
--- a/app/assets/javascripts/app/services/modelManager.js
+++ b/app/assets/javascripts/app/services/modelManager.js
@@ -7,7 +7,8 @@ SFModelManager.ContentTypeClassMapping = {
"SN|Theme" : SNTheme,
"SN|Component" : SNComponent,
"SF|Extension" : SNServerExtension,
- "SF|MFA" : SNMfa
+ "SF|MFA" : SNMfa,
+ "SN|Privileges" : SNPrivileges
};
SFItem.AppDomain = "org.standardnotes.sn";
diff --git a/app/assets/javascripts/app/services/passcodeManager.js b/app/assets/javascripts/app/services/passcodeManager.js
index 47c9e763b..79ed39f02 100644
--- a/app/assets/javascripts/app/services/passcodeManager.js
+++ b/app/assets/javascripts/app/services/passcodeManager.js
@@ -1,100 +1,247 @@
-angular.module('app')
- .provider('passcodeManager', function () {
+const MillisecondsPerSecond = 1000;
- this.$get = function($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) {
- return new PasscodeManager($rootScope, $timeout, modelManager, dbManager, authManager, storageManager);
- }
+class PasscodeManager {
- function PasscodeManager($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) {
+ constructor($rootScope, authManager, storageManager, syncManager) {
+ this.authManager = authManager;
+ this.storageManager = storageManager;
+ this.syncManager = syncManager;
+ this.$rootScope = $rootScope;
- this._hasPasscode = storageManager.getItemSync("offlineParams", StorageManager.Fixed) != null;
+ this._hasPasscode = this.storageManager.getItemSync("offlineParams", StorageManager.Fixed) != null;
this._locked = this._hasPasscode;
- this.isLocked = function() {
- return this._locked;
- }
+ this.passcodeChangeObservers = [];
- this.hasPasscode = function() {
- return this._hasPasscode;
- }
+ this.configureAutoLock();
+ }
- this.keys = function() {
- return this._keys;
- }
+ addPasscodeChangeObserver(callback) {
+ this.passcodeChangeObservers.push(callback);
+ }
- this.passcodeAuthParams = function() {
- var authParams = JSON.parse(storageManager.getItemSync("offlineParams", StorageManager.Fixed));
- if(authParams && !authParams.version) {
- var keys = this.keys();
- if(keys && keys.ak) {
- // If there's no version stored, and there's an ak, it has to be 002. Newer versions would have thier version stored in authParams.
- authParams.version = "002";
- } else {
- authParams.version = "001";
- }
- }
- return authParams;
- }
+ lockApplication() {
+ window.location.reload();
+ this.cancelAutoLockTimer();
+ }
- this.unlock = function(passcode, callback) {
- var params = this.passcodeAuthParams();
- SFJS.crypto.computeEncryptionKeysForUser(passcode, params).then((keys) => {
- if(keys.pw !== params.hash) {
- callback(false);
- return;
- }
+ isLocked() {
+ return this._locked;
+ }
- this._keys = keys;
- this._authParams = params;
- this.decryptLocalStorage(keys, params).then(() => {
- this._locked = false;
- callback(true);
- })
- });
- }
+ hasPasscode() {
+ return this._hasPasscode;
+ }
- this.setPasscode = (passcode, callback) => {
- var uuid = SFJS.crypto.generateUUIDSync();
+ keys() {
+ return this._keys;
+ }
- SFJS.crypto.generateInitialKeysAndAuthParamsForUser(uuid, passcode).then((results) => {
- let keys = results.keys;
- let authParams = results.authParams;
+ async setAutoLockInterval(interval) {
+ return this.storageManager.setItem(PasscodeManager.AutoLockIntervalKey, JSON.stringify(interval), StorageManager.FixedEncrypted);
+ }
- authParams.hash = keys.pw;
- this._keys = keys;
- this._hasPasscode = true;
- this._authParams = authParams;
-
- // Encrypting will initially clear localStorage
- this.encryptLocalStorage(keys, authParams);
-
- // After it's cleared, it's safe to write to it
- storageManager.setItem("offlineParams", JSON.stringify(authParams), StorageManager.Fixed);
- callback(true);
- });
- }
-
- this.changePasscode = (newPasscode, callback) => {
- this.setPasscode(newPasscode, callback);
- }
-
- 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, authParams) {
- storageManager.setKeys(keys, authParams);
- // Switch to Ephemeral storage, wiping Fixed storage
- // Last argument is `force`, which we set to true because in the case of changing passcode
- storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted, true);
- }
-
- this.decryptLocalStorage = async function(keys, authParams) {
- storageManager.setKeys(keys, authParams);
- return storageManager.decryptStorage();
+ async getAutoLockInterval() {
+ let interval = await this.storageManager.getItem(PasscodeManager.AutoLockIntervalKey, StorageManager.FixedEncrypted);
+ if(interval) {
+ return JSON.parse(interval);
+ } else {
+ return PasscodeManager.AutoLockIntervalNone;
}
}
-});
+
+ passcodeAuthParams() {
+ var authParams = JSON.parse(this.storageManager.getItemSync("offlineParams", StorageManager.Fixed));
+ if(authParams && !authParams.version) {
+ var keys = this.keys();
+ if(keys && keys.ak) {
+ // If there's no version stored, and there's an ak, it has to be 002. Newer versions would have their version stored in authParams.
+ authParams.version = "002";
+ } else {
+ authParams.version = "001";
+ }
+ }
+ return authParams;
+ }
+
+ async verifyPasscode(passcode) {
+ return new Promise(async (resolve, reject) => {
+ var params = this.passcodeAuthParams();
+ let keys = await SFJS.crypto.computeEncryptionKeysForUser(passcode, params);
+ if(keys.pw !== params.hash) {
+ resolve(false);
+ } else {
+ resolve(true);
+ }
+ })
+ }
+
+ unlock(passcode, callback) {
+ var params = this.passcodeAuthParams();
+ SFJS.crypto.computeEncryptionKeysForUser(passcode, params).then((keys) => {
+ if(keys.pw !== params.hash) {
+ callback(false);
+ return;
+ }
+
+ this._keys = keys;
+ this._authParams = params;
+ this.decryptLocalStorage(keys, params).then(() => {
+ this._locked = false;
+ callback(true);
+ })
+ });
+ }
+
+ setPasscode(passcode, callback) {
+ var uuid = SFJS.crypto.generateUUIDSync();
+
+ SFJS.crypto.generateInitialKeysAndAuthParamsForUser(uuid, passcode).then((results) => {
+ let keys = results.keys;
+ let authParams = results.authParams;
+
+ authParams.hash = keys.pw;
+ this._keys = keys;
+ this._hasPasscode = true;
+ this._authParams = authParams;
+
+ // Encrypting will initially clear localStorage
+ this.encryptLocalStorage(keys, authParams);
+
+ // After it's cleared, it's safe to write to it
+ this.storageManager.setItem("offlineParams", JSON.stringify(authParams), StorageManager.Fixed);
+ callback(true);
+
+ this.notifyObserversOfPasscodeChange();
+ });
+ }
+
+ changePasscode(newPasscode, callback) {
+ this.setPasscode(newPasscode, callback);
+ }
+
+ clearPasscode() {
+ this.storageManager.setItemsMode(this.authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.Fixed); // Transfer from Ephemeral
+ this.storageManager.removeItem("offlineParams", StorageManager.Fixed);
+ this._keys = null;
+ this._hasPasscode = false;
+
+ this.notifyObserversOfPasscodeChange();
+ }
+
+ notifyObserversOfPasscodeChange() {
+ for(var observer of this.passcodeChangeObservers) {
+ observer();
+ }
+ }
+
+ encryptLocalStorage(keys, authParams) {
+ this.storageManager.setKeys(keys, authParams);
+ // Switch to Ephemeral storage, wiping Fixed storage
+ // Last argument is `force`, which we set to true because in the case of changing passcode
+ this.storageManager.setItemsMode(this.authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted, true);
+ }
+
+ async decryptLocalStorage(keys, authParams) {
+ this.storageManager.setKeys(keys, authParams);
+ return this.storageManager.decryptStorage();
+ }
+
+ configureAutoLock() {
+ if(isDesktopApplication()) {
+ // desktop only
+ this.$rootScope.$on("window-lost-focus", () => {
+ this.documentVisibilityChanged(false);
+ })
+ this.$rootScope.$on("window-gained-focus", () => {
+ this.documentVisibilityChanged(true);
+ })
+ } else {
+ // tab visibility listender, web only
+ document.addEventListener('visibilitychange', (e) => {
+ let visible = document.visibilityState == "visible";
+ this.documentVisibilityChanged(visible);
+ });
+ }
+
+ PasscodeManager.AutoLockIntervalNone = 0;
+ PasscodeManager.AutoLockIntervalImmediate = 1;
+ PasscodeManager.AutoLockIntervalOneMinute = 60 * MillisecondsPerSecond;
+ PasscodeManager.AutoLockIntervalFiveMinutes = 300 * MillisecondsPerSecond;
+ PasscodeManager.AutoLockIntervalOneHour = 3600 * MillisecondsPerSecond;
+
+ PasscodeManager.AutoLockIntervalKey = "AutoLockIntervalKey";
+ }
+
+ getAutoLockIntervalOptions() {
+ return [
+ {
+ value: PasscodeManager.AutoLockIntervalNone,
+ label: "Off"
+ },
+ {
+ value: PasscodeManager.AutoLockIntervalImmediate,
+ label: "Immediately"
+ },
+ {
+ value: PasscodeManager.AutoLockIntervalOneMinute,
+ label: "1m"
+ },
+ {
+ value: PasscodeManager.AutoLockIntervalFiveMinutes,
+ label: "5m"
+ },
+ {
+ value: PasscodeManager.AutoLockIntervalOneHour,
+ label: "1h"
+ }
+ ]
+ }
+
+ documentVisibilityChanged(visible) {
+ if(visible) {
+ // check to see if lockAfterDate is not null, and if the application isn't locked.
+ // if that's the case, it needs to be locked immediately.
+ if(this.lockAfterDate && new Date() > this.lockAfterDate && !this.isLocked()) {
+ this.lockApplication();
+ } else {
+ if(!this.isLocked()) {
+ this.syncManager.sync();
+ }
+ }
+ this.cancelAutoLockTimer();
+ } else {
+ this.beginAutoLockTimer();
+ }
+ }
+
+ async beginAutoLockTimer() {
+ var interval = await this.getAutoLockInterval();
+ if(interval == PasscodeManager.AutoLockIntervalNone) {
+ return;
+ }
+
+ // Use a timeout if possible, but if the computer is put to sleep, timeouts won't work.
+ // Need to set a date as backup. this.lockAfterDate does not need to be persisted, as
+ // living in memory seems sufficient. If memory is cleared, then the application will lock anyway.
+ let addToNow = (seconds) => {
+ let date = new Date();
+ date.setSeconds(date.getSeconds() + seconds);
+ return date;
+ }
+
+ this.lockAfterDate = addToNow(interval / MillisecondsPerSecond);
+ this.lockTimeout = setTimeout(() => {
+ this.lockApplication();
+ // We don't need to look at this anymore since we've succeeded with timeout lock
+ this.lockAfterDate = null;
+ }, interval);
+ }
+
+ cancelAutoLockTimer() {
+ clearTimeout(this.lockTimeout);
+ this.lockAfterDate = null;
+ }
+}
+
+angular.module('app').service('passcodeManager', PasscodeManager);
diff --git a/app/assets/javascripts/app/services/privilegesManager.js b/app/assets/javascripts/app/services/privilegesManager.js
new file mode 100644
index 000000000..a1c83f077
--- /dev/null
+++ b/app/assets/javascripts/app/services/privilegesManager.js
@@ -0,0 +1,309 @@
+class PrivilegesManager {
+
+ constructor(passcodeManager, authManager, singletonManager, modelManager, storageManager, $rootScope, $compile) {
+ this.passcodeManager = passcodeManager;
+ this.authManager = authManager;
+ this.singletonManager = singletonManager;
+ this.modelManager = modelManager;
+ this.storageManager = storageManager;
+ this.$rootScope = $rootScope;
+ this.$compile = $compile;
+
+ this.loadPrivileges();
+
+ PrivilegesManager.CredentialAccountPassword = "CredentialAccountPassword";
+ PrivilegesManager.CredentialLocalPasscode = "CredentialLocalPasscode";
+
+ PrivilegesManager.ActionManageExtensions = "ActionManageExtensions";
+ PrivilegesManager.ActionManageBackups = "ActionManageBackups";
+ PrivilegesManager.ActionViewProtectedNotes = "ActionViewProtectedNotes";
+ PrivilegesManager.ActionManagePrivileges = "ActionManagePrivileges";
+ PrivilegesManager.ActionManagePasscode = "ActionManagePasscode";
+ PrivilegesManager.ActionDeleteNote = "ActionDeleteNote";
+
+ PrivilegesManager.SessionExpiresAtKey = "SessionExpiresAtKey";
+ PrivilegesManager.SessionLengthKey = "SessionLengthKey";
+
+ PrivilegesManager.SessionLengthNone = 0;
+ PrivilegesManager.SessionLengthFiveMinutes = 300;
+ PrivilegesManager.SessionLengthOneHour = 3600;
+ PrivilegesManager.SessionLengthOneWeek = 604800;
+
+ this.availableActions = [
+ PrivilegesManager.ActionViewProtectedNotes,
+ PrivilegesManager.ActionDeleteNote,
+ PrivilegesManager.ActionManagePasscode,
+ PrivilegesManager.ActionManageBackups,
+ PrivilegesManager.ActionManageExtensions,
+ PrivilegesManager.ActionManagePrivileges,
+ ]
+
+ this.availableCredentials = [
+ PrivilegesManager.CredentialAccountPassword,
+ PrivilegesManager.CredentialLocalPasscode
+ ];
+
+ this.sessionLengths = [
+ PrivilegesManager.SessionLengthNone,
+ PrivilegesManager.SessionLengthFiveMinutes,
+ PrivilegesManager.SessionLengthOneHour,
+ PrivilegesManager.SessionLengthOneWeek,
+ PrivilegesManager.SessionLengthIndefinite
+ ]
+ }
+
+ getAvailableActions() {
+ return this.availableActions;
+ }
+
+ getAvailableCredentials() {
+ return this.availableCredentials;
+ }
+
+ presentPrivilegesModal(action, onSuccess, onCancel) {
+ if(this.authenticationInProgress()) {
+ onCancel && onCancel();
+ return;
+ }
+
+ let customSuccess = () => {
+ onSuccess && onSuccess();
+ this.currentAuthenticationElement = null;
+ }
+
+ let customCancel = () => {
+ onCancel && onCancel();
+ this.currentAuthenticationElement = null;
+ }
+
+ var scope = this.$rootScope.$new(true);
+ scope.action = action;
+ scope.onSuccess = customSuccess;
+ scope.onCancel = customCancel;
+ var el = this.$compile( "" )(scope);
+ angular.element(document.body).append(el);
+
+ this.currentAuthenticationElement = el;
+ }
+
+ async netCredentialsForAction(action) {
+ let credentials = (await this.getPrivileges()).getCredentialsForAction(action);
+ let netCredentials = [];
+
+ for(var cred of credentials) {
+ if(cred == PrivilegesManager.CredentialAccountPassword) {
+ if(!this.authManager.offline()) {
+ netCredentials.push(cred);
+ }
+ } else if(cred == PrivilegesManager.CredentialLocalPasscode) {
+ if(this.passcodeManager.hasPasscode()) {
+ netCredentials.push(cred);
+ }
+ }
+ }
+
+ return netCredentials;
+ }
+
+ presentPrivilegesManagementModal() {
+ var scope = this.$rootScope.$new(true);
+ var el = this.$compile( "")(scope);
+ angular.element(document.body).append(el);
+ }
+
+ authenticationInProgress() {
+ return this.currentAuthenticationElement != null;
+ }
+
+ async loadPrivileges() {
+ return new Promise((resolve, reject) => {
+ let prefsContentType = "SN|Privileges";
+ let contentTypePredicate = new SFPredicate("content_type", "=", prefsContentType);
+ this.singletonManager.registerSingleton([contentTypePredicate], (resolvedSingleton) => {
+ this.privileges = resolvedSingleton;
+ if(!this.privileges.content.desktopPrivileges) {
+ this.privileges.content.desktopPrivileges = {};
+ }
+ resolve(resolvedSingleton);
+ }, (valueCallback) => {
+ // Safe to create. Create and return object.
+ var privs = new SNPrivileges({content_type: prefsContentType});
+ this.modelManager.addItem(privs);
+ privs.setDirty(true);
+ this.$rootScope.sync();
+ valueCallback(privs);
+ resolve(privs);
+ });
+ });
+ }
+
+ async getPrivileges() {
+ if(this.privileges) {
+ return this.privileges;
+ } else {
+ return this.loadPrivileges();
+ }
+ }
+
+ displayInfoForCredential(credential) {
+ let metadata = {}
+
+ metadata[PrivilegesManager.CredentialAccountPassword] = {
+ label: "Account Password",
+ prompt: "Please enter your account password."
+ }
+
+ metadata[PrivilegesManager.CredentialLocalPasscode] = {
+ label: "Local Passcode",
+ prompt: "Please enter your local passcode."
+ }
+
+ return metadata[credential];
+ }
+
+ displayInfoForAction(action) {
+ let metadata = {};
+
+ metadata[PrivilegesManager.ActionManageExtensions] = {
+ label: "Manage Extensions"
+ };
+
+ metadata[PrivilegesManager.ActionManageBackups] = {
+ label: "Download/Import Backups"
+ };
+
+ metadata[PrivilegesManager.ActionViewProtectedNotes] = {
+ label: "View Protected Notes"
+ };
+
+ metadata[PrivilegesManager.ActionManagePrivileges] = {
+ label: "Manage Privileges"
+ };
+
+ metadata[PrivilegesManager.ActionManagePasscode] = {
+ label: "Manage Passcode"
+ }
+
+ metadata[PrivilegesManager.ActionDeleteNote] = {
+ label: "Delete Notes"
+ }
+
+ return metadata[action];
+ }
+
+ getSessionLengthOptions() {
+ return [
+ {
+ value: PrivilegesManager.SessionLengthNone,
+ label: "Don't Remember"
+ },
+ {
+ value: PrivilegesManager.SessionLengthFiveMinutes,
+ label: "5 Minutes"
+ },
+ {
+ value: PrivilegesManager.SessionLengthOneHour,
+ label: "1 Hour"
+ },
+ {
+ value: PrivilegesManager.SessionLengthOneWeek,
+ label: "1 Week"
+ }
+ ]
+ }
+
+ async setSessionLength(length) {
+ let addToNow = (seconds) => {
+ let date = new Date();
+ date.setSeconds(date.getSeconds() + seconds);
+ return date;
+ }
+
+ let expiresAt = addToNow(length);
+
+ return Promise.all([
+ this.storageManager.setItem(PrivilegesManager.SessionExpiresAtKey, JSON.stringify(expiresAt), this.storageManager.bestStorageMode()),
+ this.storageManager.setItem(PrivilegesManager.SessionLengthKey, JSON.stringify(length), this.storageManager.bestStorageMode()),
+ ])
+ }
+
+ async clearSession() {
+ return this.setSessionLength(PrivilegesManager.SessionLengthNone);
+ }
+
+ async getSelectedSessionLength() {
+ let length = await this.storageManager.getItem(PrivilegesManager.SessionLengthKey, this.storageManager.bestStorageMode());
+ if(length) {
+ return JSON.parse(length);
+ } else {
+ return PrivilegesManager.SessionLengthNone;
+ }
+ }
+
+ async getSessionExpirey() {
+ let expiresAt = await this.storageManager.getItem(PrivilegesManager.SessionExpiresAtKey, this.storageManager.bestStorageMode());
+ if(expiresAt) {
+ return new Date(JSON.parse(expiresAt));
+ } else {
+ return new Date();
+ }
+ }
+
+ async actionHasPrivilegesConfigured(action) {
+ return (await this.netCredentialsForAction(action)).length > 0;
+ }
+
+ async actionRequiresPrivilege(action) {
+ let expiresAt = await this.getSessionExpirey();
+ if(expiresAt > new Date()) {
+ return false;
+ }
+ return (await this.netCredentialsForAction(action)).length > 0;
+ }
+
+ async savePrivileges() {
+ let privs = await this.getPrivileges();
+ privs.setDirty(true);
+ this.$rootScope.sync();
+ }
+
+ async authenticateAction(action, credentialAuthMapping) {
+ var requiredCredentials = (await this.netCredentialsForAction(action));
+ var successfulCredentials = [], failedCredentials = [];
+
+ for(let requiredCredential of requiredCredentials) {
+ var passesAuth = await this._verifyAuthenticationParameters(requiredCredential, credentialAuthMapping[requiredCredential]);
+ if(passesAuth) {
+ successfulCredentials.push(requiredCredential);
+ } else {
+ failedCredentials.push(requiredCredential);
+ }
+ }
+
+ return {
+ success: failedCredentials.length == 0,
+ successfulCredentials: successfulCredentials,
+ failedCredentials: failedCredentials
+ }
+ }
+
+ async _verifyAuthenticationParameters(credential, value) {
+
+ let verifyAccountPassword = async (password) => {
+ return this.authManager.verifyAccountPassword(password);
+ }
+
+ let verifyLocalPasscode = async (passcode) => {
+ return this.passcodeManager.verifyPasscode(passcode);
+ }
+
+ if(credential == PrivilegesManager.CredentialAccountPassword) {
+ return verifyAccountPassword(value);
+ } else if(credential == PrivilegesManager.CredentialLocalPasscode) {
+ return verifyLocalPasscode(value);
+ }
+ }
+
+}
+
+angular.module('app').service('privilegesManager', PrivilegesManager);
diff --git a/app/assets/javascripts/app/services/sessionHistory.js b/app/assets/javascripts/app/services/sessionHistory.js
index d64dec37f..7b5ac9f04 100644
--- a/app/assets/javascripts/app/services/sessionHistory.js
+++ b/app/assets/javascripts/app/services/sessionHistory.js
@@ -6,10 +6,17 @@ class SessionHistory extends SFSessionHistoryManager {
"Note" : NoteHistoryEntry
}
+ // Session History can be encrypted with passcode keys. If it changes, we need to resave session
+ // history with the new keys.
+ passcodeManager.addPasscodeChangeObserver(() => {
+ this.saveToDisk();
+ })
+
var keyRequestHandler = async () => {
let offline = authManager.offline();
let auth_params = offline ? passcodeManager.passcodeAuthParams() : await authManager.getAuthParams();
let keys = offline ? passcodeManager.keys() : await authManager.keys();
+
return {
keys: keys,
offline: offline,
diff --git a/app/assets/javascripts/app/services/singletonManager.js b/app/assets/javascripts/app/services/singletonManager.js
index 8638cf36f..4d7969eb7 100644
--- a/app/assets/javascripts/app/services/singletonManager.js
+++ b/app/assets/javascripts/app/services/singletonManager.js
@@ -24,9 +24,29 @@ class SingletonManager {
$rootScope.$on("initial-data-loaded", (event, data) => {
this.resolveSingletons(modelManager.allItems, null, true);
+ this.initialDataLoaded = true;
+ })
+
+ /*
+ If an item alternates its uuid on registration, singletonHandlers might need to update
+ their local reference to the object, since the object reference will change on uuid alternation
+ */
+ modelManager.addModelUuidChangeObserver("singleton-manager", (oldModel, newModel) => {
+ for(var handler of this.singletonHandlers) {
+ if(handler.singleton && SFPredicate.ItemSatisfiesPredicates(newModel, handler.predicates)) {
+ // Reference is now invalid, calling resolveSingleton should update it
+ handler.singleton = null;
+ this.resolveSingletons([newModel]);
+ }
+ }
})
$rootScope.$on("sync:completed", (event, data) => {
+ // Wait for initial data load before handling any sync. If we don't want for initial data load,
+ // then the singleton resolver won't have the proper items to work with to determine whether to resolve or create.
+ if(!this.initialDataLoaded) {
+ return;
+ }
// The reason we also need to consider savedItems in consolidating singletons is in case of sync conflicts,
// a new item can be created, but is never processed through "retrievedItems" since it is only created locally then saved.
@@ -108,7 +128,6 @@ class SingletonManager {
var singleton = allExtantItemsMatchingPredicate[0];
singletonHandler.singleton = singleton;
singletonHandler.resolutionCallback(singleton);
-
}
}
} else {
diff --git a/app/assets/javascripts/app/services/storageManager.js b/app/assets/javascripts/app/services/storageManager.js
index 908490747..87b46eb56 100644
--- a/app/assets/javascripts/app/services/storageManager.js
+++ b/app/assets/javascripts/app/services/storageManager.js
@@ -174,6 +174,10 @@ class StorageManager extends SFStorageManager {
return this.getItemSync("encryptedStorage", StorageManager.Fixed) !== null;
}
+ bestStorageMode() {
+ return this.hasPasscode() ? StorageManager.FixedEncrypted : StorageManager.Fixed;
+ }
+
/*
Model Storage
diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js
index 291eaf50c..02596cf7f 100644
--- a/app/assets/javascripts/app/services/syncManager.js
+++ b/app/assets/javascripts/app/services/syncManager.js
@@ -11,7 +11,7 @@ class SyncManager extends SFSyncManager {
scope.item1 = items[0];
scope.item2 = items[1];
scope.callback = callback;
- var el = this.$compile( "" )(scope);
+ var el = this.$compile( "" )(scope);
angular.element(document.body).append(el);
}
diff --git a/app/assets/javascripts/app/services/themeManager.js b/app/assets/javascripts/app/services/themeManager.js
index d5bec58e3..84921e12e 100644
--- a/app/assets/javascripts/app/services/themeManager.js
+++ b/app/assets/javascripts/app/services/themeManager.js
@@ -1,9 +1,39 @@
class ThemeManager {
- constructor(componentManager, desktopManager) {
+ constructor(componentManager, desktopManager, storageManager, passcodeManager) {
this.componentManager = componentManager;
+ this.storageManager = storageManager;
+ this.desktopManager = desktopManager;
+ this.activeThemes = [];
- desktopManager.registerUpdateObserver((component) => {
+ ThemeManager.CachedThemesKey = "cachedThemes";
+
+ this.registerObservers();
+
+ // When a passcode is added, all local storage will be encrypted (it doesn't know what was
+ // originally saved as Fixed or FixedEncrypted). We want to rewrite cached themes here to Fixed
+ // so that it's readable without authentication.
+ passcodeManager.addPasscodeChangeObserver(() => {
+ this.cacheThemes();
+ })
+
+ // The desktop application won't have its applicationDataPath until the angular document is ready,
+ // so it wont be able to resolve local theme urls until thats ready
+ angular.element(document).ready(() => {
+ this.activateCachedThemes();
+ });
+ }
+
+ activateCachedThemes() {
+ let cachedThemes = this.getCachedThemes();
+ let writeToCache = false;
+ for(var theme of cachedThemes) {
+ this.activateTheme(theme, writeToCache);
+ }
+ }
+
+ registerObservers() {
+ this.desktopManager.registerUpdateObserver((component) => {
// Reload theme if active
if(component.active && component.isTheme()) {
this.deactivateTheme(component);
@@ -13,7 +43,7 @@ class ThemeManager {
}
})
- componentManager.registerHandler({identifier: "themeManager", areas: ["themes"], activationHandler: (component) => {
+ this.componentManager.registerHandler({identifier: "themeManager", areas: ["themes"], activationHandler: (component) => {
if(component.active) {
this.activateTheme(component);
} else {
@@ -33,9 +63,17 @@ class ThemeManager {
this.componentManager.deactivateComponent(theme);
}
}
+
+ this.decacheThemes();
}
- activateTheme(theme) {
+ activateTheme(theme, writeToCache = true) {
+ if(_.find(this.activeThemes, {uuid: theme.uuid})) {
+ return;
+ }
+
+ this.activeThemes.push(theme);
+
var url = this.componentManager.urlForComponent(theme);
var link = document.createElement("link");
link.href = url;
@@ -44,6 +82,10 @@ class ThemeManager {
link.media = "screen,print";
link.id = theme.uuid;
document.getElementsByTagName("head")[0].appendChild(link);
+
+ if(writeToCache) {
+ this.cacheThemes();
+ }
}
deactivateTheme(theme) {
@@ -52,6 +94,36 @@ class ThemeManager {
element.disabled = true;
element.parentNode.removeChild(element);
}
+
+ _.remove(this.activeThemes, {uuid: theme.uuid});
+
+ this.cacheThemes();
+ }
+
+ async cacheThemes() {
+ let mapped = await Promise.all(this.activeThemes.map(async (theme) => {
+ let transformer = new SFItemParams(theme);
+ let params = await transformer.paramsForLocalStorage();
+ return params;
+ }));
+ let data = JSON.stringify(mapped);
+ return this.storageManager.setItem(ThemeManager.CachedThemesKey, data, StorageManager.Fixed);
+ }
+
+ async decacheThemes() {
+ return this.storageManager.removeItem(ThemeManager.CachedThemesKey, StorageManager.Fixed);
+ }
+
+ getCachedThemes() {
+ let cachedThemes = this.storageManager.getItemSync(ThemeManager.CachedThemesKey, StorageManager.Fixed);
+ if(cachedThemes) {
+ let parsed = JSON.parse(cachedThemes);
+ return parsed.map((theme) => {
+ return new SNTheme(theme);
+ });
+ } else {
+ return [];
+ }
}
}
diff --git a/app/assets/stylesheets/app/_editor.scss b/app/assets/stylesheets/app/_editor.scss
index 0d4f3b225..47d3f2c66 100644
--- a/app/assets/stylesheets/app/_editor.scss
+++ b/app/assets/stylesheets/app/_editor.scss
@@ -1,14 +1,17 @@
$heading-height: 75px;
+
+#editor-column {
+ .locked {
+ opacity: 0.8;
+ }
+}
+
.editor {
flex: 1 50%;
display: flex;
flex-direction: column;
overflow-y: hidden;
- background-color: white;
-}
-
-.locked {
- opacity: 0.8;
+ background-color: var(--sn-stylekit-background-color);
}
#editor-title-bar {
@@ -19,7 +22,6 @@ $heading-height: 75px;
padding-bottom: 10px;
padding-right: 10px;
- background-color: white;
border-bottom: none;
z-index: $z-index-editor-title-bar;
@@ -27,7 +29,7 @@ $heading-height: 75px;
overflow: visible;
> .title {
- font-size: 18px;
+ font-size: var(--sn-stylekit-font-size-h1);
font-weight: bold;
padding-top: 0px;
width: 100%;
@@ -41,9 +43,10 @@ $heading-height: 75px;
border: none;
outline: none;
background-color: transparent;
+ color: var(--sn-stylekit-foreground-color);
&:disabled {
- color: black;
+ color: var(--sn-stylekit-foreground-color);
}
}
@@ -55,22 +58,21 @@ $heading-height: 75px;
position: absolute;
right: 20px;
- font-size: 12px;
+ font-size: calc(var(--sn-stylekit-base-font-size) - 2px);
text-transform: none;
font-weight: normal;
margin-top: 4px;
text-align: right;
- color: rgba(black, 0.23);
- &.error, .error {
- color: #f6a200;
+ .desc, .message:not(.warning):not(.danger) {
+ // color: var(--sn-stylekit-editor-foreground-color);
+ opacity: 0.35;
}
}
.editor-tags {
clear: left;
width: 100%;
- // height: 25px;
overflow: visible;
position: relative;
@@ -78,6 +80,11 @@ $heading-height: 75px;
height: 50px;
overflow: auto; // Required for expired sub to not overflow
+ .component-view {
+ // see comment under main .component-view css defintion
+ position: inherit;
+ }
+
iframe {
height: 50px;
width: 100%;
@@ -88,6 +95,7 @@ $heading-height: 75px;
.tags-input {
background-color: transparent;
+ color: var(--sn-stylekit-foreground-color);
width: 100%;
border: none;
}
@@ -100,7 +108,7 @@ $heading-height: 75px;
overflow-y: hidden;
height: 100%;
display: flex;
- background-color: white;
+ background-color: var(--sn-stylekit-background-color);
position: relative;
@@ -113,12 +121,14 @@ $heading-height: 75px;
font-family: monospace;
overflow-y: scroll;
width: 100%;
+ background-color: var(--sn-stylekit-background-color);
+ color: var(--sn-stylekit-foreground-color);
border: none;
outline: none;
padding: 15px;
padding-top: 11px;
- font-size: 17px;
+ font-size: var(--sn-stylekit-font-size-editor);
resize: none;
}
}
@@ -126,12 +136,25 @@ $heading-height: 75px;
#editor-pane-component-stack {
width: 100%;
+ // When two component stack items are expired and eat up full screen, this is required to scroll them.
+ // overflow: auto;
+ // When expired components, without this, requires scroll
+ overflow: visible;
+
+ #component-stack-menu-bar {
+ border-bottom: none;
+ }
+
.component-stack-item {
width: 100%;
position: relative;
- border-top: 1px solid $bg-color;
+
iframe {
width: 100%;
+ background-color: var(--sn-stylekit-background-color);
+ // we moved the border top from the .component-stack-item to the .iframe, as on parent,
+ // it increases its height and caused unneccessary scrollbars on windows.
+ border-top: 1px solid var(--sn-stylekit-border-color);
}
}
}
diff --git a/app/assets/stylesheets/app/_footer.scss b/app/assets/stylesheets/app/_footer.scss
index d2d1519ce..e5d46fe1f 100644
--- a/app/assets/stylesheets/app/_footer.scss
+++ b/app/assets/stylesheets/app/_footer.scss
@@ -6,12 +6,12 @@
z-index: $z-index-footer-bar;
}
-#footer-bar .item {
+#footer-bar .sk-app-bar-item {
z-index: $z-index-footer-bar-item;
position: relative;
user-select: none;
- .panel {
+ .sk-panel {
max-height: 85vh;
position: absolute;
right: 0px;
@@ -20,16 +20,28 @@
min-width: 300px;
z-index: $z-index-footer-bar-item-panel;
margin-top: 15px;
- background-color: white;
}
+ &.dock-shortcut:hover .sk-app-bar-item-column {
+ border-bottom: 2px solid var(--sn-stylekit-info-color);
+ }
+
+ svg {
+ width: 12px;
+ height: 12px;
+ fill: var(--sn-stylekit-foreground-color);
+
+ &:hover {
+ fill: var(--sn-stylekit-info-color);
+ }
+ }
}
#account-panel {
width: 400px;
}
-.panel {
+.sk-panel {
cursor: default;
}
@@ -43,6 +55,5 @@ a.disabled {
#footer-lock-icon {
margin-left: 5px;
- padding-left: 8px;
- border-left: 1px solid gray;
+ padding-left: 5px;
}
diff --git a/app/assets/stylesheets/app/_ionicons.scss b/app/assets/stylesheets/app/_ionicons.scss
index 884e1f781..6390e6f48 100644
--- a/app/assets/stylesheets/app/_ionicons.scss
+++ b/app/assets/stylesheets/app/_ionicons.scss
@@ -11,14 +11,34 @@
Modified icons to fit ionicon’s grid from original.
*/
@font-face { font-family: "Ionicons"; src: url("../assets/ionicons.eot?v=2.0.0"); src: url("../assets/ionicons.eot?v=2.0.1#iefix") format("embedded-opentype"), url("../assets/ionicons.ttf?v=2.0.1") format("truetype"), url("../assets/ionicons.woff?v=2.0.1") format("woff"), url("../assets/ionicons.svg?v=2.0.1#Ionicons") format("svg"); font-weight: normal; font-style: normal; }
-.ion, .ionicons, .ion-ios-box:before, .ion-bookmark:before, .ion-locked:before { display: inline-block; font-family: "Ionicons"; speak: none; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; text-rendering: auto; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
+
+.ion, .ionicons,
+.ion-ios-box:before,
+.ion-bookmark:before,
+.ion-locked:before,
+.ion-arrow-return-left:before,
+.ion-arrow-return-right:before,
+.ion-key:before,
+.ion-lock-combination:before,
+.ion-eye-disabled:before
+{
+ display: inline-block; font-family: "Ionicons"; speak: none; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; text-rendering: auto; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
+}
.ion-ios-box:before { content: "\f3ec"; }
.ion-locked:before { content: "\f200"; }
-.ion-bookmark:before {
- content: "\f26b";
-}
+.ion-bookmark:before { content: "\f26b"; }
+
+.ion-arrow-return-left:before { content: "\f265"; }
+
+.ion-arrow-return-right:before { content: "\f266"; }
+
+.ion-key:before { content: "\f296"; }
+
+.ion-lock-combination:before { content: "\f4d4"; }
+
+.ion-eye-disabled:before { content: "\f306"; }
/*# sourceMappingURL=ionicons.css.map */
diff --git a/app/assets/stylesheets/app/_lock-screen.scss b/app/assets/stylesheets/app/_lock-screen.scss
index 7dad8285a..89ce4dfb4 100644
--- a/app/assets/stylesheets/app/_lock-screen.scss
+++ b/app/assets/stylesheets/app/_lock-screen.scss
@@ -12,33 +12,24 @@
bottom: 0;
z-index: $z-index-lock-screen;
- background-color: rgba(white, 0.5);
- color: black;
- font-size: 16px;
+ background-color: var(--sn-stylekit-background-color);
+ color: var(--sn-stylekit-foreground-color);
display: flex;
+ flex-direction: column;
align-items: center;
justify-content: center;
- .background {
- position: absolute;
- z-index: -1;
- width: 100%;
- height: 100%;
- }
-
- .panel {
+ .sk-panel {
width: 315px;
flex-grow: 0;
+ // border-radius: 0;
- .header {
+ .sk-panel-header {
justify-content: center;
}
}
#passcode-reset {
- margin-top: 18px;
text-align: center;
- width: 100%;
- font-size: 13px;
}
}
diff --git a/app/assets/stylesheets/app/_main.scss b/app/assets/stylesheets/app/_main.scss
index c4f412b3b..a63f25de2 100644
--- a/app/assets/stylesheets/app/_main.scss
+++ b/app/assets/stylesheets/app/_main.scss
@@ -1,11 +1,3 @@
-$main-text-color: black;
-$secondary-text-color: rgba($main-text-color, 0.8);
-$bg-color: #e3e3e3;
-$light-bg-color: #e9e9e9;
-$selection-color: $bg-color;
-$selected-text-color: black;
-$blue-color: #086dd6;
-
$z-index-editor-content: 10;
$z-index-editor-title-bar: 100;
@@ -27,13 +19,13 @@ body {
"Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
- color: $main-text-color;
-webkit-font-smoothing: antialiased;
min-height: 100%;
height: 100%;
- font-size: 14px;
+ font-size: var(--sn-stylekit-base-font-size);
margin: 0;
- background-color: $bg-color;
+ color: var(--sn-stylekit-foreground-color);
+ background-color: var(--sn-stylekit-background-color);
}
* {
@@ -41,11 +33,11 @@ body {
}
.tinted {
- color: $blue-color;
+ color: var(--sn-stylekit-info-color);
}
.tinted-selected {
- color: white;
+ color: var(--sn-stylekit-info-contrast-color);
}
*:focus {outline:0;}
@@ -61,7 +53,6 @@ input, button, select, textarea {
}
a {
- color: $blue-color;
text-decoration: none;
&.no-decoration {
@@ -77,6 +68,16 @@ a {
}
}
+::selection {
+ background: var(--sn-stylekit-info-color) !important; /* WebKit/Blink Browsers */
+ color: var(--sn-stylekit-info-contrast-color);
+}
+
+::-moz-selection {
+ background: var(--sn-stylekit-info-color) !important;
+ color: var(--sn-stylekit-info-contrast-color);
+}
+
p {
overflow: auto;
}
@@ -85,7 +86,7 @@ p {
min-height: 100vh;
height: 100vh;
overflow: auto;
- background-color: $bg-color;
+ background-color: var(--sn-stylekit-background-color);
}
$footer-height: 32px;
@@ -115,7 +116,8 @@ $footer-height: 32px;
height: 100%;
position: absolute;
cursor: col-resize;
- background-color: rgba(black, 0.1);
+ // needs to be a color that works on main bg and contrast bg
+ background-color: var(--sn-stylekit-secondary-contrast-background-color);
opacity: 0;
border-top: none;
border-bottom: none;
@@ -145,7 +147,8 @@ $footer-height: 32px;
&.collapsed {
opacity: 1;
- background-color: #DDDDDD;
+ // so it blends in with editor a bit more
+ background-color: var(--sn-stylekit-editor-background-color);
}
&.dragging {
@@ -169,7 +172,6 @@ $footer-height: 32px;
padding-bottom: 0px;
height: 100%;
max-height: calc(100vh - #{$footer-height});
- font-size: 17px;
position: relative;
overflow: hidden;
@@ -181,13 +183,12 @@ $footer-height: 32px;
> .content {
height: 100%;
max-height: 100%;
- background-color: white;
+ background-color: var(--sn-stylekit-background-color);
position: relative;
}
.section-title-bar {
font-weight: bold;
- font-size: 14px;
.padded {
padding: 0 14px;
@@ -197,7 +198,8 @@ $footer-height: 32px;
display: flex;
justify-content: space-between;
align-items: center;
- overflow: hidden;
+ // This was causing problems with tags + button cutting off on right when the panel is a certain size
+ // overflow: hidden;
> .title {
white-space: nowrap;
@@ -205,24 +207,6 @@ $footer-height: 32px;
width: 80%;
overflow: hidden;
}
-
- > .add-button {
- $button-bg: #e9e9e9;
- color: lighten($main-text-color, 40%);
- font-size: 18px;
- width: 45px;
- height: 24px;
- cursor: pointer;
- background-color: $button-bg;
- border-radius: 4px;
- font-weight: normal;
- text-align: center;
-
- &:hover {
- background-color: darken($button-bg, 5%);
- color: lighten($main-text-color, 40%);
- }
- }
}
}
}
diff --git a/app/assets/stylesheets/app/_menus.scss b/app/assets/stylesheets/app/_menus.scss
index 77de3a5ff..e8f62bc0c 100644
--- a/app/assets/stylesheets/app/_menus.scss
+++ b/app/assets/stylesheets/app/_menus.scss
@@ -1,5 +1,5 @@
-.app-bar {
- .item {
+.sk-app-bar {
+ .sk-app-bar-item {
position: relative;
}
}
@@ -15,6 +15,4 @@
margin-top: 5px;
width: 280px;
max-height: calc(85vh - 90px);
- background-color: white;
- color: $selected-text-color;
}
diff --git a/app/assets/stylesheets/app/_modals.scss b/app/assets/stylesheets/app/_modals.scss
index ca1dac561..e277f2dcc 100644
--- a/app/assets/stylesheets/app/_modals.scss
+++ b/app/assets/stylesheets/app/_modals.scss
@@ -1,23 +1,58 @@
#permissions-modal {
width: 350px;
- .panel {
+ .sk-panel {
border-radius: 0;
- background-color: white;
}
- .content {
+ .sk-panel-content {
padding-top: 1.1rem;
}
- .footer {
+ .sk-panel-footer {
padding-bottom: 1.4rem;
}
}
+#privileges-modal {
+ width: 700px;
+
+ table {
+ margin-bottom: 12px;
+ width: 100%;
+ overflow: auto;
+ border-collapse: collapse;
+ border-spacing: 0px;
+ border-color: var(--sn-stylekit-contrast-border-color);
+ background-color: var(--sn-stylekit-background-color);
+ color: var(--sn-stylekit-contrast-foreground-color);
+
+ th, td {
+ padding: 6px 13px;
+ border: 1px solid var(--sn-stylekit-contrast-border-color);
+ }
+
+ tr:nth-child(2n) {
+ background-color: var(--sn-stylekit-contrast-background-color);
+ }
+ }
+
+ th {
+ text-align: center;
+ font-weight: normal;
+ }
+
+ .priv-header {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ }
+}
+
#password-wizard {
font-size: 16px;
}
#item-preview-modal {
- > .content {
+ > .sk-modal-content {
width: 800px;
height: 500px;
}
@@ -37,21 +72,17 @@
.border {
height: 100%;
- background-color: rgba(black, 0.1);
+ background-color: var(--sn-stylekit-border-color);
width: 1px;
margin: 0 15px;
}
}
-.panel {
- background-color: white;
-}
-
.header .subtitle {
font-size: 1.1rem;
}
-.modal {
+.sk-modal {
position: fixed;
margin-left: auto;
margin-right: auto;
@@ -62,54 +93,57 @@
z-index: $z-index-modal;
width: 100vw;
height: 100vh;
- background-color: rgba(gray, 0.3);
- color: black;
+
+ background-color: transparent;
+ color: var(--sn-stylekit-contrast-foreground-color);
display: flex;
align-items: center;
justify-content: center;
.sn-component {
height: 100%;
- .panel {
+ .sk-panel {
height: 100%;
}
}
&.auto-height {
- > .content {
+ > .sk-modal-content {
height: auto !important;
}
}
&.large {
- > .content {
+ > .sk-modal-content {
width: 900px;
height: 600px;
}
}
&.medium {
- > .content {
+ > .sk-modal-content {
width: 700px;
height: 500px;
}
}
&.small {
- > .content {
+ > .sk-modal-content {
width: 700px;
height: 344px;
}
}
- .background {
+ .sk-modal-background {
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
+ background-color: var(--sn-stylekit-contrast-background-color);
+ opacity: 0.7;
}
- > .content {
+ > .sk-modal-content {
overflow-y: auto;
width: auto;
padding: 0;
@@ -135,6 +169,10 @@
display: flex;
flex-direction: column;
+ // required so that .loading-overlay absolute works properly wrt to modal components. However, seems to break #note-tags-component-container.
+ // I couldn't find any solution to this other than to customize .component-view position back to inherit for note-tags-component-container.
+ position: relative;
+
// not sure why we need this. Removed because it creates unncessary scroll bars. Tested on folders extension, creates horizontal scrollbar at bottom on windows
// overflow: auto;
// Update: we needed that because when we display the expired Extended view, it allows it to scroll vertically.
@@ -144,8 +182,24 @@
min-width: 100%;
}
- iframe {
- flex: 1;
+ .loading-overlay {
+ position: absolute;
+ background-color: var(--sn-stylekit-editor-background-color);
width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+ }
+
+ iframe {
+ // We're disabling flex: 1; because on Firefox, it causes weird sizing issues with component stack items.
+ // Not sure yet if totally required.
+ // Update: The extensions manager doesn't display correctly without it
+ // flex-grow: 1 should fix that.
+
+ flex-grow: 1;
+ width: 100%;
+ height: 100%;
+ background-color: transparent;
}
}
diff --git a/app/assets/stylesheets/app/_notes.scss b/app/assets/stylesheets/app/_notes.scss
index e3c4a4647..5e6a549cc 100644
--- a/app/assets/stylesheets/app/_notes.scss
+++ b/app/assets/stylesheets/app/_notes.scss
@@ -1,6 +1,8 @@
#notes-column, .notes {
- border-left: 1px solid #dddddd;
- border-right: 1px solid #dddddd;
+ border-left: 1px solid var(--sn-stylekit-border-color);
+ border-right: 1px solid var(--sn-stylekit-border-color);
+
+ font-size: var(--sn-stylekit-font-size-h2);
width: 350px;
flex-grow: 0;
@@ -21,15 +23,11 @@
font-size: 18px;
.section-title-bar-header .title {
- color: rgba(black, 0.40);
+ color: var(--sn-stylekit-neutral-color);
width: calc(90% - 45px);
}
}
- #notes-add-button {
-
- }
-
#notes-menu-bar {
position: relative;
margin-top: 14px;
@@ -42,7 +40,7 @@
position: relative;
.filter-bar {
- background-color: $light-bg-color;
+ background-color: var(--sn-stylekit-contrast-background-color);
border-radius: 4px;
height: 100%;
color: #909090;
@@ -60,9 +58,9 @@
border-radius: 50%;
width: 17px;
height: 17px;
- color: white;
cursor: default;
- background-color: gray;
+ background-color: var(--sn-stylekit-neutral-color);
+ color: var(--sn-stylekit-neutral-contrast-color);
font-size: 10px;
line-height: 17px;
text-align: center;
@@ -73,7 +71,7 @@
transition: background-color 0.15s linear;
&:hover {
- background-color: $blue-color;
+ background-color: var(--sn-stylekit-info-color);
}
}
}
@@ -102,9 +100,8 @@
.note {
width: 100%;
padding: 15px;
- border-bottom: 1px solid $bg-color;
+ border-bottom: 1px solid var(--sn-stylekit-border-color);
cursor: pointer;
- background-color: white;
> .name {
font-weight: 600;
@@ -137,11 +134,11 @@
overflow: hidden;
text-overflow: ellipsis;
- $line-height: 18px;
.default-preview, .plain-preview {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1; /* number of lines to show */
+ $line-height: 18px;
line-height: $line-height; /* fallback */
max-height: calc(#{$line-height} * 1); /* fallback */
}
@@ -158,14 +155,53 @@
opacity: 0.6;
}
- &.selected {
- background-color: $blue-color;
- color: white;
+ .note-flag {
+ color: var(--sn-stylekit-info-color);
+ }
- .pinned {
- color: white;
+ .note-flags {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ .note-flag {
+ margin-right: 10px;
}
}
+ progress {
+ background-color: var(--sn-stylekit-contrast-background-color);
+ color: var(--sn-stylekit-info-color);
+ }
+
+ progress::-webkit-progress-bar {
+ background-color: var(--sn-stylekit-contrast-background-color);
+ }
+
+ progress::-webkit-progress-value {
+ background-color: var(--sn-stylekit-info-color);
+ }
+
+ &.selected {
+ background-color: var(--sn-stylekit-info-color);
+ color: var(--sn-stylekit-info-contrast-color);
+
+ .note-flag {
+ color: var(--sn-stylekit-info-contrast-color);
+ }
+
+ progress {
+ background-color: var(--sn-stylekit-secondary-foreground-color);
+ color: var(--sn-stylekit-secondary-background-color);
+ }
+
+ progress::-webkit-progress-bar {
+ background-color: var(--sn-stylekit-secondary-foreground-color);
+ }
+
+ progress::-webkit-progress-value {
+ background-color: var(--sn-stylekit-secondary-background-color);
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/app/_scrollbars.scss b/app/assets/stylesheets/app/_scrollbars.scss
deleted file mode 100644
index 415aa0ac9..000000000
--- a/app/assets/stylesheets/app/_scrollbars.scss
+++ /dev/null
@@ -1,31 +0,0 @@
-.windows-web, .windows-desktop, .linux-web, .linux-desktop {
-
- $thumb-color: #dfdfdf;
- $track-border-color: #E7E7E7;
- $thumb-width: 4px;
-
- ::-webkit-scrollbar {
- width: 17px;
- height: 18px;
- border-left: 0.5px solid $track-border-color;
- }
-
- ::-webkit-scrollbar-thumb {
- height: 6px;
- border: $thumb-width solid rgba(0, 0, 0, 0);
- background-clip: padding-box;
- -webkit-border-radius: 10px;
- background-color: $thumb-color;
- -webkit-box-shadow: inset -1px -1px 0px rgba(0, 0, 0, 0.05), inset 1px 1px 0px rgba(0, 0, 0, 0.05);
- }
-
- ::-webkit-scrollbar-button {
- width: 0;
- height: 0;
- display: none;
- }
-
- ::-webkit-scrollbar-corner {
- background-color: transparent;
- }
-}
diff --git a/app/assets/stylesheets/app/_stylekit-sub.scss b/app/assets/stylesheets/app/_stylekit-sub.scss
index b0a9285e1..3405cedaa 100644
--- a/app/assets/stylesheets/app/_stylekit-sub.scss
+++ b/app/assets/stylesheets/app/_stylekit-sub.scss
@@ -1,6 +1,6 @@
.sn-component {
- .notification {
+ .sk-notification {
&.unpadded {
padding: 0;
padding-bottom: 0 !important;
@@ -12,13 +12,13 @@
}
.bordered-row {
- border-bottom: 1px solid rgba(black, 0.1);
- border-top: 1px solid rgba(black, 0.1);
+ border-bottom: 1px solid var(--sn-stylekit-border-color);
+ border-top: 1px solid var(--sn-stylekit-border-color);
}
}
- .app-bar {
+ .sk-app-bar {
&.no-top-edge {
border-top: 0;
}
@@ -26,10 +26,8 @@
}
-.panel {
- color: black;
-
- .header {
+.sk-panel {
+ .sk-panel-header {
.close-button {
&:hover {
text-decoration: none;
@@ -37,25 +35,20 @@
}
}
- input {
- min-height: 39px;
- }
-
-
- .button-group.stretch {
- .button:not(.featured) {
+ .sk-button-group.stretch {
+ .sk-button:not(.featured) {
// Default buttons that are not featured and stretched should have larger vertical padding
padding: 9px;
}
}
-
- a {
- color: $blue-color;
- }
}
#session-history-menu {
- .menu-panel .row .sublabel.opaque {
+ .sk-menu-panel .sk-menu-panel-row .sk-sublabel.opaque {
opacity: 1.0
}
}
+
+button.sk-button {
+ border: none;
+}
diff --git a/app/assets/stylesheets/app/_tags.scss b/app/assets/stylesheets/app/_tags.scss
index 45a849be6..8049e4b88 100644
--- a/app/assets/stylesheets/app/_tags.scss
+++ b/app/assets/stylesheets/app/_tags.scss
@@ -6,27 +6,18 @@
-webkit-user-select: none;
&, #tags-content {
- background-color: #f6f6f6;
+ background-color: var(--sn-stylekit-secondary-background-color);
display: flex;
flex-direction: column;
}
#tags-title-bar {
- color: black;
- padding-top: 14px;
- padding-bottom: 16px;
+ color: var(--sn-stylekit-secondary-foreground-color);
+ padding-top: 15px;
+ padding-bottom: 8px;
padding-left: 12px;
padding-right: 12px;
font-size: 12px;
- color: rgba(black, 0.8);
- }
-
- #tag-add-button {
- background-color: #d7d7d7;
-
- &:hover {
- background-color: rgba(#d7d7d7, 0.8);
- }
}
.scrollable {
@@ -52,7 +43,6 @@
cursor: pointer;
transition: height .1s ease-in-out;
position: relative;
- font-size: 14px;
> .info {
height: 20px;
@@ -62,7 +52,7 @@
background-color: transparent;
font-weight: 600;
float: left;
- color: $main-text-color;
+ color: var(--sn-stylekit-secondary-foreground-color);
border: none;
cursor: pointer;
text-overflow: ellipsis;
@@ -76,6 +66,7 @@
right: 17px;
padding-top: 1px;
font-weight: bold;
+ color: var(--sn-stylekit-neutral-color);
}
}
@@ -97,32 +88,17 @@
}
}
- $tags-selected-color: #dbdbdb;
-
&.selected {
- background-color: $tags-selected-color;
- color: $selected-text-color;
> .title {
- color: $selected-text-color;
cursor: text;
}
}
- /* When a note is dragged over tag */
- &.over {
- background-color: $tags-selected-color;
- color: $selected-text-color;
- border: 2px dashed white;
+ &:hover:not(.selected), &.selected {
+ background-color: var(--sn-stylekit-secondary-contrast-background-color);
+ color: var(--sn-stylekit-secondary-contrast-foreground-color);
> .title {
- color: $selected-text-color;
- }
- }
-
- &:hover:not(.selected) {
- background-color: $tags-selected-color;
- color: $selected-text-color;
- > .title {
- color: $selected-text-color;
+ color: var(--sn-stylekit-secondary-contrast-foreground-color);
}
}
}
diff --git a/app/assets/stylesheets/main.css.scss b/app/assets/stylesheets/main.css.scss
index d59551f78..9e28dc002 100644
--- a/app/assets/stylesheets/main.css.scss
+++ b/app/assets/stylesheets/main.css.scss
@@ -1,6 +1,5 @@
@import "app/main";
@import "app/ui";
-@import "app/scrollbars";
@import "app/footer";
@import "app/tags";
@import "app/notes";
diff --git a/app/assets/templates/directives/account-menu.html.haml b/app/assets/templates/directives/account-menu.html.haml
index f0fb6736c..60d0d357c 100644
--- a/app/assets/templates/directives/account-menu.html.haml
+++ b/app/assets/templates/directives/account-menu.html.haml
@@ -1,139 +1,169 @@
.sn-component
- .panel#account-panel
- .header
- %h1.title Account
- %a.close-button{"ng-click" => "close()"} Close
- .content
-
- .panel-section.hero{"ng-if" => "!user && !formData.showLogin && !formData.showRegister && !formData.mfa"}
- %h1.title Sign in or register to enable sync and end-to-end encryption.
- .panel-row
- .panel-row
- .button-group.stretch
- .button.info.featured{"ng-click" => "formData.showLogin = true"}
- .label Sign In
- .button.info.featured{"ng-click" => "formData.showRegister = true"}
- .label Register
- %p
+ .sk-panel#account-panel
+ .sk-panel-header
+ .sk-panel-header-title Account
+ %a.sk-a.info.close-button{"ng-click" => "close()"} Close
+ .sk-panel-content
+ .sk-panel-section.sk-panel-hero{"ng-if" => "!user && !formData.showLogin && !formData.showRegister && !formData.mfa"}
+ .sk-panel-row
+ .sk-h1 Sign in or register to enable sync and end-to-end encryption.
+ .sk-panel-row
+ .sk-button-group.stretch
+ .sk-button.info.featured{"ng-click" => "formData.showLogin = true"}
+ .sk-label Sign In
+ .sk-button.info.featured{"ng-click" => "formData.showRegister = true"}
+ .sk-label Register
+ .sk-panel-row.sk-p
Standard Notes is free on every platform, and comes standard with sync and encryption.
- .panel-section{"ng-if" => "formData.showLogin || formData.showRegister"}
- %h3.title.panel-row
+ .sk-panel-section{"ng-if" => "formData.showLogin || formData.showRegister"}
+ .sk-panel-section-title
{{formData.showLogin ? "Sign In" : "Register"}}
- %form.panel-form{"ng-submit" => "submitAuthForm()"}
- %input{:placeholder => 'Email', "sn-autofocus" => 'true', "should-focus" => "true", :name => 'email', :required => true, :type => 'email', 'ng-model' => 'formData.email'}
- %input{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'formData.user_password'}
- %input{:placeholder => 'Confirm Password', "ng-if" => "formData.showRegister", :name => 'password', :required => true, :type => 'password', 'ng-model' => 'formData.password_conf'}
+ %form.sk-panel-form{"ng-submit" => "submitAuthForm()"}
+ .sk-panel-section
+ %input.sk-input.contrast{:placeholder => 'Email', "sn-autofocus" => 'true', "should-focus" => "true", :name => 'email', :required => true, :type => 'email', 'ng-model' => 'formData.email'}
+ %input.sk-input.contrast{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'formData.user_password', 'sn-enter' => 'submitAuthForm()'}
+ %input.sk-input.contrast{:placeholder => 'Confirm Password', "ng-if" => "formData.showRegister", :name => 'password', :required => true, :type => 'password', 'ng-model' => 'formData.password_conf', 'sn-enter' => 'submitAuthForm()'}
+ .sk-panel-row
+ %a.sk-panel-row.sk-bold{"ng-click" => "formData.showAdvanced = !formData.showAdvanced"}
+ Advanced Options
- %a.panel-row{"ng-click" => "formData.showAdvanced = !formData.showAdvanced"}
- Advanced Options
- .notification.info{"ng-if" => "formData.showRegister"}
- %h2.title No Password Reset.
- .text Because your notes are encrypted using your password, Standard Notes does not have a password reset option. You cannot forget your password.
-
- .notification.unpadded.default.advanced-options.panel-row{"ng-if" => "formData.showAdvanced"}
- .panel-column.stretch
- %h4.title.panel-row.padded-row Advanced Options
+ .sk-notification.unpadded.contrast.advanced-options.sk-panel-row{"ng-if" => "formData.showAdvanced"}
+ .sk-panel-column.stretch
+ .sk-notification-title.sk-panel-row.padded-row Advanced Options
%div.bordered-row.padded-row
- %label Sync Server Domain
- %input.form-control.mt-5{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'formData.url'}
- %label.padded-row{"ng-if" => "formData.showLogin"}
- %input{"type" => "checkbox", "ng-model" => "formData.strictSignin"}
+ %label.sk-label Sync Server Domain
+ %input.sk-input.mt-5.sk-base{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'formData.url'}
+ %label.sk-label.padded-row{"ng-if" => "formData.showLogin"}
+ %input.sk-input{"type" => "checkbox", "ng-model" => "formData.strictSignin"}
Use strict sign in
%span
%a{"href" => "https://standardnotes.org/help/security", "target" => "_blank"} (Learn more)
- .button-group.stretch.panel-row.form-submit
- %button.button.info.featured{"type" => "submit", "ng-disabled" => "formData.authenticating"}
- .label {{formData.showLogin ? "Sign In" : "Register"}}
+ .sk-panel-section.form-submit{"ng-if" => "!formData.authenticating"}
+ .sk-button-group.stretch
+ .sk-button.info.featured{'ng-click' => 'submitAuthForm()', "ng-disabled" => "formData.authenticating"}
+ .sk-label {{formData.showLogin ? "Sign In" : "Register"}}
- %label
- %input{"type" => "checkbox", "ng-model" => "formData.ephemeral", "ng-true-value" => "false", "ng-false-value" => "true"}
- Stay signed in
- %label{"ng-if" => "notesAndTagsCount() > 0"}
- %input{"type" => "checkbox", "ng-model" => "formData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"}
- Merge local data ({{notesAndTagsCount()}} notes and tags)
+ .sk-notification.neutral{"ng-if" => "formData.showRegister"}
+ .sk-notification-title No Password Reset.
+ .sk-notification-text Because your notes are encrypted using your password, Standard Notes does not have a password reset option. You cannot forget your password.
- %em.block.center-align.mt-10{"ng-if" => "formData.status", "style" => "font-size: 14px;"}
- {{formData.status}}
+ .sk-panel-section.no-bottom-pad{"ng-if" => "formData.status"}
+ .sk-horizontal-group
+ .sk-spinner.small.neutral
+ .sk-label {{formData.status}}
- .panel-section{"ng-if" => "formData.mfa"}
- %form{"ng-submit" => "submitMfaForm()"}
- %p {{formData.mfa.message}}
- %input.form-control.mt-10{:placeholder => "Enter Code", "sn-autofocus" => "true", "should-focus" => "true", :autofocus => "true", :name => 'mfa', :required => true, 'ng-model' => 'formData.userMfaCode'}
- .button-group.stretch.panel-row.form-submit
- %button.button.info.featured{"type" => "submit"}
- .label Sign In
+ .sk-panel-section.no-bottom-pad{"ng-if" => "!formData.authenticating"}
+ %label.sk-panel-row.justify-left
+ .sk-horizontal-group
+ %input{"type" => "checkbox", "ng-model" => "formData.ephemeral", "ng-true-value" => "false", "ng-false-value" => "true"}
+ Stay signed in
+ %label.sk-panel-row.justify-left{"ng-if" => "notesAndTagsCount() > 0"}
+ .sk-panel-row
+ %input{"type" => "checkbox", "ng-model" => "formData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"}
+ Merge local data ({{notesAndTagsCount()}} notes and tags)
+
+ .sk-panel-section{"ng-if" => "formData.mfa"}
+ %form.sk-panel-form{"ng-submit" => "submitMfaForm()"}
+ .sk-p.sk-panel-row {{formData.mfa.message}}
+ .sk-panel-row
+ %input.sk-input.contrast{:placeholder => "Enter Code", "sn-autofocus" => "true", "should-focus" => "true", :autofocus => "true", :name => 'mfa', :required => true, 'ng-model' => 'formData.userMfaCode'}
+ .sk-button-group.stretch.sk-panel-row.form-submit{"ng-if" => "!formData.status"}
+ %button.sk-button.info.featured{"type" => "submit"}
+ .sk-label Sign In
+ .sk-panel-section.no-bottom-pad{"ng-if" => "formData.status"}
+ .sk-panel-row
+ .sk-panel-row
+ .sk-horizontal-group
+ .sk-spinner.small.neutral
+ .sk-label {{formData.status}}
%div{"ng-if" => "!formData.showLogin && !formData.showRegister && !formData.mfa"}
- .panel-section{"ng-if" => "user"}
- .notification.danger{"ng-if" => "syncStatus.error"}
- %h2.title Sync Unreachable
- .text Hmm...we can't seem to sync your account. The reason: {{syncStatus.error.message}}
- %p
- %a{"href" => "https://standardnotes.org/help", "target" => "_blank"} Need help?
- .panel-row
- %h2.title.wrap {{user.email}}
- .horizontal-group{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress || syncStatus.needsMoreSync", "delay" => "1000"}
- .spinner.small.info
- .sublabel
+ .sk-panel-section{"ng-if" => "user"}
+ .sk-notification.danger{"ng-if" => "syncStatus.error"}
+ .sk-notification-title Sync Unreachable
+ .sk-notification-text Hmm...we can't seem to sync your account. The reason: {{syncStatus.error.message}}
+ %a.sk-a.info-contrast.sk-bold.sk-panel-row{"href" => "https://standardnotes.org/help", "target" => "_blank"} Need help?
+
+ .sk-panel-row
+ .sk-panel-column
+ .sk-h1.sk-bold.wrap {{user.email}}
+ .sk-subtitle.subtle.normal {{server}}
+ .sk-horizontal-group{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress || syncStatus.needsMoreSync", "delay" => "1000"}
+ .sk-spinner.small.info
+ .sk-sublabel
{{"Syncing" + (syncStatus.total > 0 ? ":" : "")}}
%span{"ng-if" => "syncStatus.total > 0"} {{syncStatus.current}}/{{syncStatus.total}}
- .subtitle.subtle.normal {{server}}
- .panel-row
+ .sk-panel-row
- %a.panel-row.condensed{"ng-click" => "openPasswordWizard('change-pw')"} Change Password
- %a.panel-row.justify-left.condensed.success{"ng-if" => "securityUpdateAvailable", "ng-click" => "openPasswordWizard('upgrade-security')"}
- .inline.circle.small.success.mr-8
+ %a.sk-a.info.sk-panel-row.condensed{"ng-click" => "openPasswordWizard('change-pw')"}
+ Change Password
+ %a.sk-a.info.sk-panel-row.condensed{"ng-show" => "user", "ng-click" => "openPrivilegesModal('')"}
+ Manage Privileges
+ %a.sk-panel-row.justify-left.condensed.success{"ng-if" => "securityUpdateAvailable", "ng-click" => "openPasswordWizard('upgrade-security')"}
+ .inline.sk-circle.small.success.mr-8
.inline Security Update Available
- .panel-section
- %h3.title.panel-row Encryption
- %h5.subtitle.info.panel-row{"ng-if" => "encryptionEnabled()"}
+ .sk-panel-section
+ .sk-panel-section-title Encryption
+ .sk-panel-section-subtitle.info{"ng-if" => "encryptionEnabled()"}
{{encryptionStatusForNotes()}}
- %p
+ %p.sk-p
{{encryptionStatusString()}}
- .panel-section
- %h3.title.panel-row Passcode Lock
+ .sk-panel-section
+ .sk-panel-section-title Passcode Lock
%div{"ng-if" => "!hasPasscode()"}
%div{"ng-if" => "canAddPasscode"}
- .panel-row{"ng-if" => "!formData.showPasscodeForm"}
- .button.info{"ng-click" => "addPasscodeClicked(); $event.stopPropagation();"}
- .label Add Passcode
+ .sk-panel-row{"ng-if" => "!formData.showPasscodeForm"}
+ .sk-button.info{"ng-click" => "addPasscodeClicked(); $event.stopPropagation();"}
+ .sk-label Add Passcode
- %p Add an app passcode to lock the app and encrypt on-device key storage.
+ %p.sk-p Add an app passcode to lock the app and encrypt on-device key storage.
%div{"ng-if" => "!canAddPasscode"}
- %p Adding a passcode is not supported in temporary sessions. Please sign out, then sign back in with the "Stay signed in" option checked.
+ %p.sk-p Adding a passcode is not supported in temporary sessions. Please sign out, then sign back in with the "Stay signed in" option checked.
- %form{"ng-if" => "formData.showPasscodeForm", "ng-submit" => "submitPasscodeForm()"}
- %input.form-control{:type => 'password', "ng-model" => "formData.passcode", "placeholder" => "Passcode", "sn-autofocus" => "true", "should-focus" => "true"}
- %input.form-control{:type => 'password', "ng-model" => "formData.confirmPasscode", "placeholder" => "Confirm Passcode"}
- .button-group.stretch.panel-row.form-submit
- %button.button.info{"type" => "submit"}
- .label Set Passcode
- %a.panel-row{"ng-click" => "formData.showPasscodeForm = false"} Cancel
+ %form.sk-panel-form{"ng-if" => "formData.showPasscodeForm", "ng-submit" => "submitPasscodeForm()"}
+ .sk-panel-row
+ %input.sk-input.contrast{:type => 'password', "ng-model" => "formData.passcode", "placeholder" => "Passcode", "sn-autofocus" => "true", "should-focus" => "true"}
+ %input.sk-input.contrast{:type => 'password', "ng-model" => "formData.confirmPasscode", "placeholder" => "Confirm Passcode"}
+ .sk-button-group.stretch.sk-panel-row.form-submit
+ %button.sk-button.info{"type" => "submit"}
+ .sk-label Set Passcode
+ %a.neutral.sk-a.sk-panel-row{"ng-click" => "formData.showPasscodeForm = false"} Cancel
%div{"ng-if" => "hasPasscode() && !formData.showPasscodeForm"}
- .panel-row
- %p
- Passcode lock is enabled.
- %span{"ng-if" => "isDesktopApplication()"} Your passcode will be required on new sessions after app quit.
- .panel-row.justify-left
- .horizontal-group
- %a.info{"ng-click" => "changePasscodePressed()"} Change Passcode
- %a.danger{"ng-click" => "removePasscodePressed()"} Remove Passcode
+ .sk-p
+ Passcode lock is enabled.
+ .sk-notification.contrast
+ .sk-notification-title Options
+ .sk-notification-text
+ .sk-panel-row
+ .sk-horizontal-group
+ .sk-h4.sk-bold Autolock
+ %a.sk-a.info{"ng-repeat" => "option in passcodeAutoLockOptions", "ng-click" => "selectAutoLockInterval(option.value)",
+ "ng-class" => "{'boxed' : option.value == selectedAutoLockInterval}"}
+ {{option.label}}
+ .sk-p The autolock timer begins when the window or tab loses focus.
+ .sk-panel-row
+ %a.sk-a.info.sk-panel-row.condensed{"ng-show" => "!user", "ng-click" => "openPrivilegesModal('')"} Manage Privileges
+ %a.sk-a.info.sk-panel-row.condensed{"ng-click" => "changePasscodePressed()"} Change Passcode
+ %a.sk-a.danger.sk-panel-row.condensed{"ng-click" => "removePasscodePressed()"} Remove Passcode
- .panel-section{"ng-if" => "!importData.loading"}
- %h3.title Data Backups
- %form.panel-form{"ng-if" => "encryptedBackupsAvailable()"}
- .input-group
+ .sk-panel-section{"ng-if" => "!importData.loading"}
+ .sk-panel-section-title Data Backups
+ .sk-p
+ Download a backup of all your data.
+ .sk-panel-row
+ %form.sk-panel-form.sk-panel-row{"ng-if" => "encryptedBackupsAvailable()"}
+ .sk-input-group
%label
%input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "true", "ng-change" => "archiveFormData.encrypted = true"}
Encrypted
@@ -141,30 +171,30 @@
%input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "false", "ng-change" => "archiveFormData.encrypted = false"}
Decrypted
- .button-group
- .button.info{"ng-click" => "downloadDataArchive()"}
- .label Download Backup
+ .sk-button-group.sk-panel-row.justify-left
+ .sk-button.info{"ng-click" => "downloadDataArchive()"}
+ .sk-label Download Backup
- %label.button.info
+ %label.sk-button.info
%input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"}
- .label Import From Backup
+ .sk-label Import Backup
%span{"ng-if" => "isDesktopApplication()"} Backups are automatically created on desktop and can be managed via the "Backups" top-level menu.
#import-password-request{"ng-if" => "importData.requestPassword"}
- %form.panel-form.stretch{"ng-submit" => "submitImportPassword()"}
+ %form.sk-panel-form.stretch{"ng-submit" => "submitImportPassword()"}
%p Enter the account password associated with the import file.
- %input.form-control.mt-5{:type => 'password', "placeholder" => "Enter File Account Password", "ng-model" => "importData.password", "autofocus" => "true"}
- .button-group.stretch.panel-row.form-submit
- %button.button.info{"type" => "submit"}
- .label Decrypt & Import
+ %input.sk-input.mt-5{:type => 'password', "placeholder" => "Enter File Account Password", "ng-model" => "importData.password", "autofocus" => "true"}
+ .sk-button-group.stretch.sk-panel-row.form-submit
+ %button.sk-button.info{"type" => "submit"}
+ .sk-label Decrypt & Import
%p
Importing from backup will not overwrite existing data, but instead create a duplicate of any differing data.
%p If you'd like to import only a selection of items instead of the whole file, please use the Batch Manager extension.
- .panel-row
- .spinner.small.info{"ng-if" => "importData.loading"}
- .footer
- %a.right{"ng-if" => "formData.showLogin || formData.showRegister", "ng-click" => "formData.showLogin = false; formData.showRegister = false;"}
+ .sk-panel-row
+ .sk-spinner.small.info{"ng-if" => "importData.loading"}
+ .sk-panel-footer
+ %a.sk-a.right{"ng-if" => "formData.showLogin || formData.showRegister", "ng-click" => "formData.showLogin = false; formData.showRegister = false;"}
Cancel
- %a.right{"ng-if" => "!formData.showLogin && !formData.showRegister", "ng-click" => "destroyLocalData()"}
+ %a.sk-a.right.danger{"ng-if" => "!formData.showLogin && !formData.showRegister", "ng-click" => "destroyLocalData()"}
{{ user ? "Sign out and clear local data" : "Clear all local data" }}
diff --git a/app/assets/templates/directives/actions-menu.html.haml b/app/assets/templates/directives/actions-menu.html.haml
index cf0b7fffe..47ce2b5d6 100644
--- a/app/assets/templates/directives/actions-menu.html.haml
+++ b/app/assets/templates/directives/actions-menu.html.haml
@@ -1,20 +1,20 @@
.sn-component
- .menu-panel.dropdown-menu
+ .sk-menu-panel.dropdown-menu
%a.no-decoration{"ng-if" => "extensions.length == 0", "href" => "https://standardnotes.org/extensions", "target" => "blank"}
%menu-row{"label" => "'Download Actions'"}
%div{"ng-repeat" => "extension in extensions"}
- .header{"ng-click" => "extension.hide = !extension.hide; $event.stopPropagation();"}
- .column
- %h4.title {{extension.name}}
- .spinner.small.loading{"ng-if" => "extension.loading"}
+ .sk-menu-panel-header{"ng-click" => "extension.hide = !extension.hide; $event.stopPropagation();"}
+ .sk-menu-panel-column
+ .sk-menu-panel-header-title {{extension.name}}
+ .sk-spinner.small.loading{"ng-if" => "extension.loading"}
%div{"ng-if" => "extension.hide"} …
%menu-row{"ng-if" => "!extension.hide", "ng-repeat" => "action in extension.actionsWithContextForItem(item)",
"action" => "executeAction(action, extension);", "label" => "action.label", "subtitle" => "action.desc",
"spinner-class" => "action.running ? 'info' : null", "sub-rows" => "action.subrows"}
- .sublabel{"ng-if" => "action.access_type"}
+ .sk-sublabel{"ng-if" => "action.access_type"}
Uses
%strong {{action.access_type}}
access to this note.
diff --git a/app/assets/templates/directives/component-modal.html.haml b/app/assets/templates/directives/component-modal.html.haml
index 6692df4d6..4501b11aa 100644
--- a/app/assets/templates/directives/component-modal.html.haml
+++ b/app/assets/templates/directives/component-modal.html.haml
@@ -1,10 +1,10 @@
-.background{"ng-click" => "dismiss()"}
+.sk-modal-background{"ng-click" => "dismiss()"}
-.content{"ng-attr-id" => "component-content-outer-{{component.uuid}}"}
+.sk-modal-content{"ng-attr-id" => "component-content-outer-{{component.uuid}}"}
.sn-component
- .panel{"ng-attr-id" => "component-content-inner-{{component.uuid}}"}
- .header
- %h1.title
+ .sk-panel{"ng-attr-id" => "component-content-inner-{{component.uuid}}"}
+ .sk-panel-header
+ .sk-panel-header-title
{{component.name}}
- %a.close-button.info{"ng-click" => "dismiss()"} Close
+ %a.sk-a.info.close-button{"ng-click" => "dismiss()"} Close
%component-view.component-view{"component" => "component"}
diff --git a/app/assets/templates/directives/component-view.html.haml b/app/assets/templates/directives/component-view.html.haml
index 70b6be9d8..e68436225 100644
--- a/app/assets/templates/directives/component-view.html.haml
+++ b/app/assets/templates/directives/component-view.html.haml
@@ -1,103 +1,61 @@
.sn-component{"ng-if" => "issueLoading"}
- .app-bar.no-edges.no-top-edge
+ .sk-app-bar.no-edges.no-top-edge
.left
- .item
- .label.warning There was an issue loading {{component.name}}.
+ .sk-app-bar-item
+ .sk-label.warning There was an issue loading {{component.name}}.
.right
- .item{"ng-click" => "reloadComponent()"}
- .label Reload
+ .sk-app-bar-item{"ng-click" => "reloadComponent()"}
+ .sk-label Reload
.sn-component{"ng-if" => "showNoThemesMessage"}
- .app-bar.no-edges.no-top-edge
+ .sk-app-bar.no-edges.no-top-edge
.left
- .item
- .label.warning This extension does not support themes.
+ .sk-app-bar-item
+ .sk-label.warning This extension does not support themes.
.right
- .item{"ng-click" => "noThemesMessageDismiss()"}
- .label Dismiss
- .item{"ng-click" => "disableActiveTheme()"}
- .label Disable Active Theme
+ .sk-app-bar-item{"ng-click" => "noThemesMessageDismiss()"}
+ .sk-label Dismiss
+ .sk-app-bar-item{"ng-click" => "disableActiveTheme()"}
+ .sk-label Disable Active Theme
-.sn-component{"ng-if" => "error == 'expired'"}
- .panel.static
- .content
- .panel-section.stretch
- %h2.title Unable to load Standard Notes Extended
- %p Your Extended subscription expired on {{component.dateToLocalizedString(component.valid_until)}}.
- %p
- Please visit
- %a{"href" => "https://dashboard.standardnotes.org", "target" => "_blank"} dashboard.standardnotes.org
- to renew your subscription.
- .panel-row
- .panel-column
- %p
- %strong To reload your account status:
- %p
- %ol
- %li
- Open the
- %strong Extensions
- menu located in the lower left corner of the app to refresh your account status.
- %li Click Reload below.
-
- .panel-row
- .button.info{"ng-if" => "!reloading", "ng-click" => "reloadStatus()"}
- .label Reload
- .spinner.info.small{"ng-if" => "reloading"}
-
- .panel-row
- .panel-section
- %p{"ng-if" => "component.isEditor()"}
- Otherwise, please follow the steps below to disable any external editors,
- so you can edit your note using the plain text editor instead.
-
- %p To temporarily disable this extension:
-
- .panel-row
- .button.info{"ng-click" => "destroy()"}
- .label Disable Extension
- .spinner.info.small{"ng-if" => "reloading"}
-
- .panel-row
-
- %div{"ng-if" => "component.isEditor()"}
- %p To disassociate this note from this editor:
-
- %ol
- %li Click the "Editor" menu item above (under the note title).
- %li Select "Plain Editor".
- %li Repeat this for every note you'd like to access. You can also delete the editor completely to disable it for all notes. To do so, click "Extensions" in the lower left corner of the app, then, for every editor, click "Uninstall".
-
- %p
- Need help? Please email us at
- %a{"href" => "mailto:hello@standardnotes.org", "target" => "_blank"} hello@standardnotes.org
- or check out the
- %a{"href" => "https://standardnotes.org/help", "target" => "_blank"} Help
- page.
+.sn-component{"ng-if" => "expired"}
+ .sk-app-bar
+ .left
+ .sk-app-bar-item
+ .sk-app-bar-item-column
+ .sk-circle.danger.small
+ .sk-app-bar-item-column
+ %a.sk-label.sk-base{"href" => "https://dashboard.standardnotes.org", "target" => "_blank"}
+ Your Extended subscription expired on {{component.dateToLocalizedString(component.valid_until)}}.
+ Extensions are in a read-only state.
+ .right
+ .sk-app-bar-item
+ .sk-app-bar-item-column
+ %a.sk-label{"href" => "https://standardnotes.org/help", "target" => "_blank"} Help
.sn-component{"ng-if" => "error == 'offline-restricted'"}
- .panel.static
- .content
- .panel-section.stretch
- %h2.title You have restricted this extension to be used offline only.
+ .sk-panel.static
+ .sk-panel-content
+ .sk-panel-section.stretch
+ .sk-panel-section-title You have restricted this extension to be used offline only.
%p Offline extensions are not available in the Web app.
- .panel-row
- .panel-column
+ .sk-panel-row
+ .sk-panel-column
%p You can either:
%p
%ul
%li Enable the Hosted option for this extension by opening the 'Extensions' menu and toggling 'Use hosted when local is unavailable' under this extension's options. Then press Reload below.
%li Use the Desktop application.
- .panel-row
- .button.info{"ng-if" => "!reloading", "ng-click" => "reloadStatus()"}
- .label Reload
- .spinner.info.small{"ng-if" => "reloading"}
+ .sk-panel-row
+ .sk-button.info{"ng-if" => "!reloading", "ng-click" => "reloadStatus()"}
+ .sk-label Reload
+ .sk-spinner.info.small{"ng-if" => "reloading"}
.sn-component{"ng-if" => "error == 'url-missing'"}
- .panel.static
- .content
- .panel-section.stretch
- %h2.title This extension is not installed correctly.
+ .sk-panel.static
+ .sk-panel-content
+ .sk-panel-section.stretch
+ .sk-panel-section-title This extension is not installed correctly.
%p Please uninstall {{component.name}}, then re-install it.
%p
@@ -111,3 +69,5 @@
"sandbox" => "allow-scripts allow-top-navigation-by-user-activation allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-modals allow-forms",
"data-component-id" => "{{component.uuid}}"}
Loading
+
+.loading-overlay{"ng-if" => "loading"}
diff --git a/app/assets/templates/directives/conflict-resolution-modal.html.haml b/app/assets/templates/directives/conflict-resolution-modal.html.haml
index 87bb97e28..c59b7ea03 100644
--- a/app/assets/templates/directives/conflict-resolution-modal.html.haml
+++ b/app/assets/templates/directives/conflict-resolution-modal.html.haml
@@ -1,26 +1,27 @@
-.modal.large#conflict-resolution-modal
- .content
- .sn-component
- .panel
- .header
- %h1.title Conflicted items — choose which version to keep
- .horizontal-group
- %a.close-button.info{"ng-click" => "keepItem1()"} Keep left
- %a.close-button.info{"ng-click" => "keepItem2()"} Keep right
- %a.close-button.info{"ng-click" => "keepBoth()"} Keep both
- %a.close-button.info{"ng-click" => "export()"} Export
- %a.close-button.info{"ng-click" => "dismiss(); $event.stopPropagation()"} Close
- .content.selectable
- .panel-section
+.sn-component
+ .sk-modal.large#conflict-resolution-modal
+ .sk-modal-background
+ .sk-modal-content
+ .sk-panel
+ .sk-panel-header
+ %h1.sk-panel-header-title Conflicted items — choose which version to keep
+ .sk-horizontal-group
+ %a.sk-a.info.close-button{"ng-click" => "keepItem1()"} Keep left
+ %a.sk-a.info.close-button{"ng-click" => "keepItem2()"} Keep right
+ %a.sk-a.info.close-button{"ng-click" => "keepBoth()"} Keep both
+ %a.sk-a.info.close-button{"ng-click" => "export()"} Export
+ %a.sk-a.info.close-button{"ng-click" => "dismiss(); $event.stopPropagation()"} Close
+ .sk-panel-content.selectable
+ .sk-panel-section
%h3
%strong Content type:
{{contentType}}
%p You may wish to look at the "created_at" and "updated_at" fields of the items to gain better context in deciding which to keep.
#items
- .panel.static#item1.item.border-color
+ .sk-panel.static#item1.item
%p.normal{"style" => "white-space: pre-wrap; font-size: 16px;"} {{item1Content}}
.border
- .panel.static#item2.item
+ .sk-panel.static#item2.item
%p.normal{"style" => "white-space: pre-wrap; font-size: 16px;"} {{item2Content}}
diff --git a/app/assets/templates/directives/editor-menu.html.haml b/app/assets/templates/directives/editor-menu.html.haml
index 9d6ce6df9..cc0ff2dcd 100644
--- a/app/assets/templates/directives/editor-menu.html.haml
+++ b/app/assets/templates/directives/editor-menu.html.haml
@@ -1,26 +1,17 @@
.sn-component
- .menu-panel.dropdown-menu
- .section
- .header
- %h4.title Note Editor
+ .sk-menu-panel.dropdown-menu
+ .sk-menu-panel-section
+ .sk-menu-panel-header
+ .sk-menu-panel-header-title Note Editor
%menu-row{"label" => "'Plain Editor'", "circle" => "selectedEditor == null && 'success'", "action" => "selectComponent(null)"}
%menu-row{"ng-repeat" => "editor in editors", "action" => "selectComponent(editor)", "label" => "editor.name",
"circle" => "selectedEditor === editor && 'success'",
"has-button" => "selectedEditor == editor || defaultEditor == editor", "button-text" => "defaultEditor == editor ? 'Undefault' : 'Set Default'",
"button-action" => "toggleDefaultForEditor(editor)", "button-class" => "defaultEditor == editor ? 'warning' : 'info'"}
- .column{"ng-if" => "component.conflict_of || shouldDisplayRunningLocallyLabel(editor)"}
- %strong.red.medium-text{"ng-if" => "editor.conflict_of"} Conflicted copy
- .sublabel{"ng-if" => "shouldDisplayRunningLocallyLabel(editor)"} Running Locally
+ .sk-menu-panel-column{"ng-if" => "component.conflict_of || shouldDisplayRunningLocallyLabel(editor)"}
+ %strong.danger.medium-text{"ng-if" => "editor.conflict_of"} Conflicted copy
+ .sk-sublabel{"ng-if" => "shouldDisplayRunningLocallyLabel(editor)"} Running Locally
%a.no-decoration{"ng-if" => "editors.length == 0", "href" => "https://standardnotes.org/extensions", "target" => "blank"}
%menu-row{"label" => "'Download More Editors'"}
-
- .section{"ng-if" => "stack.length > 0"}
- .header
- %h4.title Editor Stack
- %menu-row{"ng-repeat" => "component in stack", "action" => "selectComponent(component)", "label" => "component.name",
- "circle" => "stackComponentEnabled(component) ? 'success' : 'danger'"}
- .column{"ng-if" => "component.conflict_of || shouldDisplayRunningLocallyLabel(component)"}
- %strong.red.medium-text{"ng-if" => "component.conflict_of"} Conflicted copy
- .sublabel{"ng-if" => "shouldDisplayRunningLocallyLabel(component)"} Running Locally
diff --git a/app/assets/templates/directives/input-modal.html.haml b/app/assets/templates/directives/input-modal.html.haml
index 706e6ac97..66d36f10e 100644
--- a/app/assets/templates/directives/input-modal.html.haml
+++ b/app/assets/templates/directives/input-modal.html.haml
@@ -1,18 +1,20 @@
-.modal.small.auto-height
- .content
- .sn-component
- .panel
- .header
- %h1.title {{title}}
- %a.close-button{"ng-click" => "dismiss()"} Close
- .content
- .panel-section
- %p.panel-row {{message}}
- .panel-row
- .panel-column.stretch
- %form{"ng-submit" => "submit()"}
- %input.form-control{:type => '{{type}}', "ng-model" => "formData.input", "placeholder" => "{{placeholder}}", "sn-autofocus" => "true", "should-focus" => "true"}
+.sn-component
+ .sk-modal.small.auto-height
+ .sk-modal-background
+ .sk-modal-content
+ .sn-component
+ .sk-panel
+ .sk-panel-header
+ .sk-h1.sk-panel-header-title {{title}}
+ %a.sk-a.info.close-button{"ng-click" => "dismiss()"} Close
+ .sk-panel-content
+ .sk-panel-section
+ .sk-p.sk-panel-row {{message}}
+ .sk-panel-row
+ .sk-panel-column.stretch
+ %form{"ng-submit" => "submit()"}
+ %input.sk-input.contrast{:type => '{{type}}', "ng-model" => "formData.input", "placeholder" => "{{placeholder}}", "sn-autofocus" => "true", "should-focus" => "true"}
- .footer
- %a.right{"ng-click" => "submit()"}
- Submit
+ .sk-panel-footer
+ %a.sk-a.info.right{"ng-click" => "submit()"}
+ Submit
diff --git a/app/assets/templates/directives/menu-row.html.haml b/app/assets/templates/directives/menu-row.html.haml
index 0cb3cf604..ca3ffc399 100644
--- a/app/assets/templates/directives/menu-row.html.haml
+++ b/app/assets/templates/directives/menu-row.html.haml
@@ -1,21 +1,24 @@
-.row{"ng-attr-title" => "{{desc}}", "ng-click" => "onClick($event)"}
- .column
+.sk-menu-panel-row.row{"ng-attr-title" => "{{desc}}", "ng-click" => "onClick($event)"}
+ .sk-menu-panel-column
.left
- .column{"ng-if" => "circle"}
- .circle.small{"ng-class" => "circle"}
- .column{"ng-class" => "{'faded' : faded || disabled}"}
- .label
+ .sk-menu-panel-column{"ng-if" => "circle && (!circleAlign || circleAlign == 'left')"}
+ .sk-circle.small{"ng-class" => "circle"}
+ .sk-menu-panel-column{"ng-class" => "{'faded' : faded || disabled}"}
+ .sk-label
{{label}}
- .sublabel{"ng-if" => "subtitle"}
+ .sk-sublabel{"ng-if" => "subtitle"}
{{subtitle}}
%ng-transclude
- .subrows{"ng-if" => "subRows && subRows.length > 0"}
+ .sk-menu-panel-subrows{"ng-if" => "subRows && subRows.length > 0"}
%menu-row{"ng-repeat" => "row in subRows", "action" => "row.onClick()",
"label" => "row.label", "subtitle" => "row.subtitle", "spinner-class" => "row.spinnerClass"}
- .column{"ng-if" => "hasButton"}
- .button.info{"ng-click" => "clickButton($event)", "ng-class" => "buttonClass"}
- {{buttonText}}
+ .sk-menu-panel-column{"ng-if" => "circle && circleAlign == 'right'"}
+ .sk-circle.small{"ng-class" => "circle"}
- .column{"ng-if" => "spinnerClass"}
- .spinner.small{"ng-class" => "spinnerClass"}
+ .sk-menu-panel-column{"ng-if" => "hasButton"}
+ .sk-button{"ng-click" => "clickButton($event)", "ng-class" => "buttonClass"}
+ .sk-label {{buttonText}}
+
+ .sk-menu-panel-column{"ng-if" => "spinnerClass"}
+ .sk-spinner.small{"ng-class" => "spinnerClass"}
diff --git a/app/assets/templates/directives/password-wizard.html.haml b/app/assets/templates/directives/password-wizard.html.haml
index cbcdf6f54..b51f3cff3 100644
--- a/app/assets/templates/directives/password-wizard.html.haml
+++ b/app/assets/templates/directives/password-wizard.html.haml
@@ -1,108 +1,104 @@
-#password-wizard.modal.small.auto-height
- .content
- .sn-component
- .panel
- .header
- %h1.title {{title}}
- %a.close-button{"ng-click" => "dismiss()"} Close
- .content
+.sn-component
+ #password-wizard.sk-modal.small.auto-height
+ .sk-modal-background
+ .sk-modal-content
+ .sn-component
+ .sk-panel
+ .sk-panel-header
+ .sk-panel-header-title {{title}}
+ %a.sk-a.info.close-button{"ng-click" => "dismiss()"} Close
+ .sk-panel-content
- %div{"ng-if" => "step == 0"}
- %div{"ng-if" => "changePassword"}
- %h2.title.panel-row Change your password
- %p
- Changing your password involves changing your encryption key, which requires your data to be re-encrypted and synced.
- If you have many items, syncing your data can take several minutes.
- %p.panel-row
- %strong You must keep the application window open during this process.
- %div{"ng-if" => "securityUpdate"}
- %h2.title.panel-row Perform security update
- %p
- A new update is available for your account. Updates address improvements and enhancements to our security specification.
- This process will guide you through the update, and perform the steps necessary with your supervision.
- %p
- For more information about security updates, please visit
- %a{"href" => "https://standardnotes.org/help/security", "target" => "_blank"} standardnotes.org/help/security.
-
- %p.panel-row
- .info Press Continue to proceed.
-
- .panel-row
- .panel-row
-
- .panel-section{"ng-if" => "step > 0"}
-
- %h3.title.panel-row Step {{step}} — {{titleForStep(step)}}
-
- %div{"ng-if" => "step == 1"}
- %p.panel-row
- As a result of this process, the entirety of your data will be re-encrypted and synced to your account. This is a generally safe process,
- but unforeseen factors like poor network connectivity or a sudden shutdown of your computer may cause this process to fail.
- It's best to be on the safe side before large operations such as this one.
- .panel-row
- .panel-row
- .button-group
- .button.info{"ng-click" => "downloadBackup(true)"}
- .label Download Encrypted Backup
- .button.info{"ng-click" => "downloadBackup(false)"}
- .label Download Decrypted Backup
-
- %div{"ng-if" => "step == 2"}
- %p.panel-row
- As a result of this process, your encryption keys will change.
- Any device on which you use Standard Notes will need to end its session. After this process completes, you will be asked to sign back in.
-
- %p.bold.panel-row.info-i Please sign out of all applications (excluding this one), including:
- %ul
- %li Desktop
- %li Web (Chrome, Firefox, Safari)
- %li Mobile (iOS and Android)
- %p.panel-row
- If you do not currently have access to a device you're signed in on, you may proceed,
- but must make signing out and back in the first step upon gaining access to that device.
- %p.panel-row Press Continue only when you have completed signing out of all your devices.
-
-
- %div{"ng-if" => "step == 3"}
+ %div{"ng-if" => "step == 0"}
%div{"ng-if" => "changePassword"}
+ %p.sk-p.sk-panel-row
+ Changing your password involves changing your encryption key, which requires your data to be re-encrypted and synced.
+ If you have many items, syncing your data can take several minutes.
+ %p.sk-p.sk-panel-row You must keep the application window open during this process.
%div{"ng-if" => "securityUpdate"}
- %p.panel-row Enter your current password. We'll run this through our encryption scheme to generate strong new encryption keys.
- .panel-row
- .panel-row
- .panel-column.stretch
- %form
- %input.form-control{:type => 'password', "ng-model" => "formData.currentPassword", "placeholder" => "Current Password", "sn-autofocus" => "true", "should-focus" => "true"}
- %input.form-control{"ng-if" => "changePassword", :type => 'password', "ng-model" => "formData.newPassword", "placeholder" => "New Password"}
- %input.form-control{"ng-if" => "changePassword", :type => 'password', "ng-model" => "formData.newPasswordConfirmation", "placeholder" => "Confirm New Password"}
+ %p.sk-p.sk-panel-row
+ A new update is available for your account. Updates address improvements and enhancements to our security specification.
+ This process will guide you through the update, and perform the steps necessary with your supervision.
+ %p.sk-p.sk-panel-row
+ For more information about security updates, please visit
+ %a.sk-a.info{"href" => "https://standardnotes.org/help/security", "target" => "_blank"} standardnotes.org/help/security.
- %div{"ng-if" => "step == 4"}
- %p.panel-row
- Your data is being re-encrypted with your new keys and synced to your account.
- %p.panel-row.danger
- Do not close this window until this process completes.
+ %p.sk-panel-row.sk-p
+ .info Press Continue to proceed.
- .panel-row
- .panel-column
- .spinner.small.inline.info.mr-5{"ng-if" => "formData.processing"}
- .inline.bold{"ng-class" => "{'info' : !formData.statusError, 'error' : formData.statusError}"}
- {{formData.status}}
- .panel-column{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress || syncStatus.needsMoreSync", "delay" => "1000"}
- %p.info
- Syncing {{syncStatus.current}}/{{syncStatus.total}}
+ .sk-panel-section{"ng-if" => "step > 0"}
- %div{"ng-if" => "step == 5"}
- %div{"ng-if" => "changePassword"}
- %p.panel-row Your password has been successfully changed.
- %div{"ng-if" => "securityUpdate"}
- %p.panel-row
- The security update has been successfully applied to your account.
- %p.panel-row
- %strong Please ensure you are running the latest version of Standard Notes on all platforms to ensure maximum compatibility.
+ .sk-panel-section-title Step {{step}} — {{titleForStep(step)}}
- %p.panel-row You may now sign back in on all your devices and close this window.
+ %div{"ng-if" => "step == 1"}
+ %p.sk-panel-row.sk-p
+ As a result of this process, the entirety of your data will be re-encrypted and synced to your account. This is a generally safe process,
+ but unforeseen factors like poor network connectivity or a sudden shutdown of your computer may cause this process to fail.
+ It's best to be on the safe side before large operations such as this one.
+ .sk-panel-row
+ .sk-panel-row
+ .sk-button-group
+ .sk-button.info{"ng-click" => "downloadBackup(true)"}
+ .sk-label Download Encrypted Backup
+ .sk-button.info{"ng-click" => "downloadBackup(false)"}
+ .sk-label Download Decrypted Backup
- .footer
- .empty
- %a.right{"ng-click" => "continue()", "ng-disabled" => "lockContinue", "ng-class" => "{'disabled' : lockContinue}"}
- .spinner.small.inline.info.mr-5{"ng-if" => "showSpinner"}
- {{continueTitle}}
+ %div{"ng-if" => "step == 2"}
+ %p.sk-p.sk-panel-row
+ As a result of this process, your encryption keys will change.
+ Any device on which you use Standard Notes will need to end its session. After this process completes, you will be asked to sign back in.
+
+ %p.sk-p.bold.sk-panel-row.info-i Please sign out of all applications (excluding this one), including:
+ %ul
+ %li.sk-p Desktop
+ %li.sk-p Web (Chrome, Firefox, Safari)
+ %li.sk-p Mobile (iOS and Android)
+ %p.sk-p.sk-panel-row
+ If you do not currently have access to a device you're signed in on, you may proceed,
+ but must make signing out and back in the first step upon gaining access to that device.
+ %p.sk-p.sk-panel-row Press Continue only when you have completed signing out of all your devices.
+
+
+ %div{"ng-if" => "step == 3"}
+ %div{"ng-if" => "changePassword"}
+ %div{"ng-if" => "securityUpdate"}
+ %p.sk-panel-row Enter your current password. We'll run this through our encryption scheme to generate strong new encryption keys.
+ .sk-panel-row
+ .sk-panel-row
+ .sk-panel-column.stretch
+ %form.sk-panel-form
+ %input.sk-input.contrast{:type => 'password', "ng-model" => "formData.currentPassword", "placeholder" => "Current Password", "sn-autofocus" => "true", "should-focus" => "true"}
+ %input.sk-input.contrast{"ng-if" => "changePassword", :type => 'password', "ng-model" => "formData.newPassword", "placeholder" => "New Password"}
+ %input.sk-input.contrast{"ng-if" => "changePassword", :type => 'password', "ng-model" => "formData.newPasswordConfirmation", "placeholder" => "Confirm New Password"}
+
+ %div{"ng-if" => "step == 4"}
+ %p.sk-panel-row
+ Your data is being re-encrypted with your new keys and synced to your account.
+ %p.sk-panel-row.danger{"ng-if" => "lockContinue"}
+ Do not close this window until this process completes.
+
+ .sk-panel-row
+ .sk-panel-column
+ .sk-spinner.small.inline.info.mr-5{"ng-if" => "formData.processing"}
+ .inline.bold{"ng-class" => "{'info' : !formData.statusError, 'error' : formData.statusError}"}
+ {{formData.status}}
+ .sk-panel-column{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress || syncStatus.needsMoreSync", "delay" => "1000"}
+ %p.info
+ Syncing {{syncStatus.current}}/{{syncStatus.total}}
+
+ %div{"ng-if" => "step == 5"}
+ %div{"ng-if" => "changePassword"}
+ %p.sk-p.sk-panel-row.info-i Your password has been successfully changed.
+ %div{"ng-if" => "securityUpdate"}
+ %p.sk-p.sk-panel-row.info-i
+ The security update has been successfully applied to your account.
+ %p.sk-p.sk-panel-row
+ Please ensure you are running the latest version of Standard Notes on all platforms to ensure maximum compatibility.
+
+ %p.sk-p.sk-panel-row You may now sign back in on all your devices and close this window.
+
+ .sk-panel-footer
+ .empty
+ %a.sk-a.info.right{"ng-click" => "continue()", "ng-disabled" => "lockContinue", "ng-class" => "{'disabled' : lockContinue}"}
+ .sk-spinner.small.inline.info.mr-5{"ng-if" => "showSpinner"}
+ {{continueTitle}}
diff --git a/app/assets/templates/directives/permissions-modal.html.haml b/app/assets/templates/directives/permissions-modal.html.haml
index 63a5dc38f..9628eb9ad 100644
--- a/app/assets/templates/directives/permissions-modal.html.haml
+++ b/app/assets/templates/directives/permissions-modal.html.haml
@@ -1,22 +1,23 @@
-.background{"ng-click" => "deny()"}
+.sk-modal-background{"ng-click" => "deny()"}
-.content#permissions-modal
+.sk-modal-content#permissions-modal
.sn-component
- .panel
- .header
- %h1.title Activate Extension
- %a.close-button.info{"ng-click" => "deny()"} Cancel
- .content
- .panel-section
- .panel-row
- %h3
+ .sk-panel
+ .sk-panel-header
+ .sk-panel-header-title Activate Extension
+ %a.sk-a.info.close-button{"ng-click" => "deny()"} Cancel
+ .sk-panel-content
+ .sk-panel-section
+ .sk-panel-row
+ .sk-h2
%strong {{component.name}}
would like to interact with your
{{permissionsString()}}
- .panel-row
- %p
+ .sk-panel-row
+ %p.sk-p
Extensions use an offline messaging system to communicate. Learn more at
- %a{"href" => "https://standardnotes.org/permissions", "target" => "_blank"} https://standardnotes.org/permissions.
- .footer
- .button.info.big.block.bold{"ng-click" => "accept()"} Continue
+ %a.sk-a.info{"href" => "https://standardnotes.org/permissions", "target" => "_blank"} https://standardnotes.org/permissions.
+ .sk-panel-footer
+ .sk-button.info.big.block.bold{"ng-click" => "accept()"}
+ .sk-label Continue
diff --git a/app/assets/templates/directives/privileges-auth-modal.html.haml b/app/assets/templates/directives/privileges-auth-modal.html.haml
new file mode 100644
index 000000000..95d41c9a5
--- /dev/null
+++ b/app/assets/templates/directives/privileges-auth-modal.html.haml
@@ -0,0 +1,29 @@
+.sk-modal-background{"ng-click" => "cancel()"}
+
+.sk-modal-content#privileges-modal
+ .sn-component
+ .sk-panel
+ .sk-panel-header
+ .sk-panel-header-title Authentication Required
+ %a.close-button.info{"ng-click" => "cancel()"} Cancel
+ .sk-panel-content
+ .sk-panel-section
+ %div{"ng-repeat" => "credential in requiredCredentials"}
+ .sk-p.sk-bold.sk-panel-row
+ %strong {{promptForCredential(credential)}}
+ .sk-panel-row
+ %input.sk-input.contrast{"type" => "password", "ng-model" => "authenticationParameters[credential]",
+ "sn-autofocus" => "true", "should-focus" => "$index == 0", "sn-enter" => "submit()"}
+ .sk-panel-row
+ %label.sk-label.danger{"ng-if" => "isCredentialInFailureState(credential)"} Invalid authentication. Please try again.
+ .sk-panel-row
+ .sk-panel-row
+ .sk-horizontal-group
+ .sk-p.sk-bold Remember For
+ %a.sk-a.info{"ng-repeat" => "option in sessionLengthOptions", "ng-click" => "selectSessionLength(option.value)",
+ "ng-class" => "{'boxed' : option.value == selectedSessionLength}"}
+ {{option.label}}
+
+ .sk-panel-footer.extra-padding
+ .sk-button.info.big.block.bold{"ng-click" => "submit()"}
+ .sk-label Submit
diff --git a/app/assets/templates/directives/privileges-management-modal.html.haml b/app/assets/templates/directives/privileges-management-modal.html.haml
new file mode 100644
index 000000000..fb9654a05
--- /dev/null
+++ b/app/assets/templates/directives/privileges-management-modal.html.haml
@@ -0,0 +1,41 @@
+.sk-modal-background{"ng-click" => "cancel()"}
+
+.sk-modal-content#privileges-modal
+ .sn-component
+ .sk-panel
+ .sk-panel-header
+ .sk-panel-header-title Manage Privileges
+ %a.sk-a.close-button.info{"ng-click" => "cancel()"} Done
+ .sk-panel-content
+ .sk-panel-section
+ %table.sk-table
+ %thead
+ %tr
+ %th
+ %th{"ng-repeat" => "cred in availableCredentials"}
+ .priv-header
+ %strong {{credentialDisplayInfo[cred].label}}
+ .sk-p.font-small{"style" => "margin-top: 2px", "ng-show" => "!credentialDisplayInfo[cred].availability"} Not Configured
+ %tbody
+ %tr{"ng-repeat" => "action in availableActions"}
+ %td
+ .sk-p {{displayInfoForAction(action)}}
+ %th{"ng-repeat" => "credential in availableCredentials"}
+ %input{"type" => "checkbox", "ng-disabled" => "!credentialDisplayInfo[credential].availability", "ng-checked" => "isCredentialRequiredForAction(action, credential)", "ng-click" => "checkboxValueChanged(action, credential)"}
+
+ .sk-panel-section{"ng-if" => "sessionExpirey && !sessionExpired"}
+ .sk-p.sk-panel-row You will not be asked to authenticate until {{sessionExpirey}}.
+ %a.sk-a.sk-panel-row.info{"ng-click" => "clearSession()"} Clear Session
+ .sk-panel-footer
+ .sk-h2.sk-bold About Privileges
+ .sk-panel-section.no-bottom-pad
+ .sk-panel-row
+ .text-content
+ .sk-p
+ Privileges represent interface level authentication for accessing certain items and features.
+ Note that when your application is unlocked, your data exists in temporary memory in an unencrypted state.
+ Privileges are meant to protect against unwanted access in the event of an unlocked application, but do not affect data encryption state.
+ %p.sk-p
+ Privileges sync across your other devices (not including mobile); however, note that if you require
+ a "Local Passcode" privilege, and another device does not have a local passcode set up, the local passcode
+ requirement will be ignored on that device.
diff --git a/app/assets/templates/directives/revision-preview-modal.html.haml b/app/assets/templates/directives/revision-preview-modal.html.haml
index df37fb182..bd0a5158a 100644
--- a/app/assets/templates/directives/revision-preview-modal.html.haml
+++ b/app/assets/templates/directives/revision-preview-modal.html.haml
@@ -1,13 +1,17 @@
-.modal.medium#item-preview-modal
- .content
- .sn-component
- .panel
- .header
- %h1.title Preview
- .horizontal-group
- %a.close-button.info{"ng-click" => "restore(false)"} Restore
- %a.close-button.info{"ng-click" => "restore(true)"} Restore as copy
- %a.close-button.info{"ng-click" => "dismiss(); $event.stopPropagation()"} Close
- .content.selectable
- %h2 {{content.title}}
- %p.normal{"style" => "white-space: pre-wrap; font-size: 16px;"} {{content.text}}
+.sn-component
+ .sk-modal.medium#item-preview-modal
+ .sk-modal-background
+ .sk-modal-content
+ .sn-component
+ .sk-panel
+ .sk-panel-header
+ .sk-panel-header-title Preview
+ .sk-horizontal-group
+ %a.sk-a.info.close-button{"ng-click" => "restore(false)"} Restore
+ %a.sk-a.info.close-button{"ng-click" => "restore(true)"} Restore as copy
+ %a.sk-a.info.close-button{"ng-click" => "dismiss(); $event.stopPropagation()"} Close
+ .sk-panel-content.selectable{"ng-if" => "!editor"}
+ .sk-h2 {{content.title}}
+ %p.normal.sk-p{"style" => "white-space: pre-wrap; font-size: 16px;"} {{content.text}}
+
+ %component-view.component-view{"ng-if" => "editor", "component" => "editor"}
diff --git a/app/assets/templates/directives/session-history-menu.html.haml b/app/assets/templates/directives/session-history-menu.html.haml
index fd994a9a6..72f87e640 100644
--- a/app/assets/templates/directives/session-history-menu.html.haml
+++ b/app/assets/templates/directives/session-history-menu.html.haml
@@ -1,23 +1,23 @@
.sn-component#session-history-menu
- .menu-panel.dropdown-menu
- .header
- .column
- %h4.title {{history.entries.length || 'No'}} revisions
- %h4{"ng-click" => "showOptions = !showOptions; $event.stopPropagation();"}
- %a Options
+ .sk-menu-panel.dropdown-menu
+ .sk-menu-panel-header
+ .sk-menu-panel-column
+ .sk-menu-panel-header-title {{history.entries.length || 'No'}} revisions
+ .sk-menu-panel-column{"ng-click" => "showOptions = !showOptions; $event.stopPropagation();"}
+ %a.sk-a.info.sk-menu-panel-header-title Options
%div{"ng-if" => "showOptions"}
%menu-row{"label" => "'Clear note local history'", "action" => "clearItemHistory()"}
%menu-row{"label" => "'Clear all local history'", "action" => "clearAllHistory()"}
%menu-row{"label" => "(autoOptimize ? 'Disable' : 'Enable') + ' auto cleanup'", "action" => "toggleAutoOptimize()"}
- .sublabel
+ .sk-sublabel
Automatically cleans up small revisions to conserve space.
%menu-row{"label" => "(diskEnabled ? 'Disable' : 'Enable') + ' saving history to disk'", "action" => "toggleDiskSaving()"}
- .sublabel
+ .sk-sublabel
Saving to disk may increase app loading time and memory footprint.
%menu-row{"ng-repeat" => "revision in entries",
"action" => "openRevision(revision);",
"label" => "revision.previewTitle()"}
- .sublabel.opaque{"ng-class" => "classForRevision(revision)"}
+ .sk-sublabel.opaque{"ng-class" => "classForRevision(revision)"}
{{revision.previewSubTitle()}}
diff --git a/app/assets/templates/editor.html.haml b/app/assets/templates/editor.html.haml
index d78f5eca9..25386c77a 100644
--- a/app/assets/templates/editor.html.haml
+++ b/app/assets/templates/editor.html.haml
@@ -1,19 +1,21 @@
-.section.editor#editor-column{"aria-label" => "Note"}
+.section.editor#editor-column.sn-component{"aria-label" => "Note"}
.sn-component
- .app-bar.no-edges{"ng-if" => "ctrl.note.locked", "ng-init" => "ctrl.lockText = 'Note Locked'", "ng-mouseover" => "ctrl.lockText = 'Unlock'", "ng-mouseleave" => "ctrl.lockText = 'Note Locked'"}
+ .sk-app-bar.no-edges{"ng-if" => "ctrl.note.locked", "ng-init" => "ctrl.lockText = 'Note Locked'", "ng-mouseover" => "ctrl.lockText = 'Unlock'", "ng-mouseleave" => "ctrl.lockText = 'Note Locked'"}
.left
- .item{"ng-click" => "ctrl.toggleLockNote()"}
- .label.warning
+ .sk-app-bar-item{"ng-click" => "ctrl.toggleLockNote()"}
+ .sk-label.warning
%i.icon.ion-locked
{{ctrl.lockText}}
#editor-title-bar.section-title-bar{"ng-show" => "ctrl.note && !ctrl.note.errorDecrypting", "ng-class" => "{'locked' : ctrl.note.locked }"}
.title
%input.input#note-title-editor{"ng-model" => "ctrl.note.title", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveTitle($event)",
- "ng-change" => "ctrl.nameChanged()", "ng-focus" => "ctrl.onNameFocus()", "ng-blur" => "ctrl.onNameBlur()",
- "select-on-click" => "true", "ng-disabled" => "ctrl.note.locked"}
+ "ng-change" => "ctrl.nameChanged()", "ng-focus" => "ctrl.onNameFocus()", "ng-blur" => "ctrl.onNameBlur()",
+ "select-on-click" => "true", "ng-disabled" => "ctrl.note.locked"}
- #save-status{"ng-class" => "{'error bold': ctrl.syncTakingTooLong}", "ng-bind-html" => "ctrl.noteStatus"}
+ #save-status
+ .message{"ng-class" => "{'warning sk-bold': ctrl.syncTakingTooLong, 'danger sk-bold': ctrl.saveError}"} {{ctrl.noteStatus.message}}
+ .desc{"ng-show" => "ctrl.noteStatus.desc"} {{ctrl.noteStatus.desc}}
.editor-tags
#note-tags-component-container{"ng-if" => "ctrl.tagsComponent"}
@@ -23,43 +25,44 @@
"spellcheck" => "false", "ng-disabled" => "ctrl.note.locked"}
.sn-component{"ng-if" => "ctrl.note"}
- .app-bar.no-edges
+ .sk-app-bar.no-edges#editor-menu-bar
.left
- .item{"ng-click" => "ctrl.showMenu = !ctrl.showMenu; ctrl.showExtensions = false; ctrl.showEditorMenu = false;", "ng-class" => "{'selected' : ctrl.showMenu}", "click-outside" => "ctrl.showMenu = false;", "is-open" => "ctrl.showMenu"}
- .label Options
- .menu-panel.dropdown-menu{"ng-if" => "ctrl.showMenu"}
- .section
- .header
- %h4.title Note Options
+ .sk-app-bar-item{"ng-click" => "ctrl.toggleMenu('showMenu')", "ng-class" => "{'selected' : ctrl.showMenu}", "click-outside" => "ctrl.showMenu = false;", "is-open" => "ctrl.showMenu"}
+ .sk-label Options
+ .sk-menu-panel.dropdown-menu{"ng-if" => "ctrl.showMenu"}
+ .sk-menu-panel-section
+ .sk-menu-panel-header
+ .sk-menu-panel-header-title Note Options
%menu-row{"label" => "ctrl.note.pinned ? 'Unpin' : 'Pin'", "action" => "ctrl.selectedMenuItem(true); ctrl.togglePin()", "desc" => "'Pin or unpin a note from the top of your list'"}
%menu-row{"label" => "ctrl.note.archived ? 'Unarchive' : 'Archive'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleArchiveNote()", "desc" => "'Archive or unarchive a note from your Archived system tag'"}
%menu-row{"label" => "ctrl.note.locked ? 'Unlock' : 'Lock'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleLockNote()", "desc" => "'Locking notes prevents unintentional editing'"}
- %menu-row{"label" => "ctrl.note.content.hidePreview ? 'Unhide Preview' : 'Hide Preview'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleNotePreview()", "desc" => "'Hide or unhide the note preview from the list of notes'"}
+ %menu-row{"label" => "ctrl.note.content.protected ? 'Unprotect' : 'Protect'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleProtectNote()", "desc" => "'Protecting a note will require credentials to view it (Manage Privileges via Account menu)'"}
+ %menu-row{"label" => "'Preview'", "circle" => "ctrl.note.content.hidePreview ? 'danger' : 'success'", "circle-align" => "'right'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleNotePreview()", "desc" => "'Hide or unhide the note preview from the list of notes'"}
%menu-row{"label" => "'Delete'", "action" => "ctrl.selectedMenuItem(); ctrl.deleteNote()", "desc" => "'Delete this note permanently from all your devices'"}
- .section
- .header
- %h4.title Global Display
+ .sk-menu-panel-section
+ .sk-menu-panel-header
+ .sk-menu-panel-header-title Global Display
- %menu-row{"label" => "'Monospace Font'", "circle" => "ctrl.monospaceFont ? 'success' : 'default'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleKey('monospaceFont')",
+ %menu-row{"label" => "'Monospace Font'", "circle" => "ctrl.monospaceFont ? 'success' : 'neutral'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleKey('monospaceFont')",
"desc" => "'Toggles the font style for the default editor'", "subtitle" => "ctrl.selectedEditor ? 'Not available with editor extensions' : null", "disabled" => "ctrl.selectedEditor"}
- %menu-row{"label" => "'Spellcheck'", "circle" => "ctrl.spellcheck ? 'success' : 'default'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleKey('spellcheck')",
+ %menu-row{"label" => "'Spellcheck'", "circle" => "ctrl.spellcheck ? 'success' : 'neutral'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleKey('spellcheck')",
"desc" => "'Toggles spellcheck for the default editor'", "subtitle" => "ctrl.selectedEditor ? 'Not available with editor extensions' : null", "disabled" => "ctrl.selectedEditor"}
- %menu-row{"label" => "'Margin Resizers'", "circle" => "ctrl.marginResizersEnabled ? 'success' : 'default'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleKey('marginResizersEnabled')",
+ %menu-row{"label" => "'Margin Resizers'", "circle" => "ctrl.marginResizersEnabled ? 'success' : 'neutral'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleKey('marginResizersEnabled')",
"desc" => "'Allows for editor left and right margins to be resized'", "faded" => "!ctrl.marginResizersEnabled"}
- .item{"ng-click" => "ctrl.onEditorMenuClick()", "ng-class" => "{'selected' : ctrl.showEditorMenu}", "click-outside" => "ctrl.showEditorMenu = false;", "is-open" => "ctrl.showEditorMenu"}
- .label Editor
+ .sk-app-bar-item{"ng-click" => "ctrl.toggleMenu('showEditorMenu')", "ng-class" => "{'selected' : ctrl.showEditorMenu}", "click-outside" => "ctrl.showEditorMenu = false;", "is-open" => "ctrl.showEditorMenu"}
+ .sk-label Editor
%editor-menu{"ng-if" => "ctrl.showEditorMenu", "callback" => "ctrl.editorMenuOnSelect", "selected-editor" => "ctrl.selectedEditor", "current-item" => "ctrl.note"}
- .item{"ng-click" => "ctrl.showExtensions = !ctrl.showExtensions; ctrl.showMenu = false; ctrl.showEditorMenu = false;", "ng-class" => "{'selected' : ctrl.showExtensions}", "click-outside" => "ctrl.showExtensions = false;", "is-open" => "ctrl.showExtensions"}
- .label Actions
+ .sk-app-bar-item{"ng-click" => "ctrl.toggleMenu('showExtensions')", "ng-class" => "{'selected' : ctrl.showExtensions}", "click-outside" => "ctrl.showExtensions = false;", "is-open" => "ctrl.showExtensions"}
+ .sk-label Actions
%actions-menu{"ng-if" => "ctrl.showExtensions", "item" => "ctrl.note"}
- .item{"ng-click" => "ctrl.showSessionHistory = !ctrl.showSessionHistory; ctrl.showMenu = false; ctrl.showEditorMenu = false;", "click-outside" => "ctrl.showSessionHistory = false;", "is-open" => "ctrl.showSessionHistory"}
- .label Session History
+ .sk-app-bar-item{"ng-click" => "ctrl.toggleMenu('showSessionHistory')", "click-outside" => "ctrl.showSessionHistory = false;", "is-open" => "ctrl.showSessionHistory"}
+ .sk-label Session History
%session-history-menu{"ng-if" => "ctrl.showSessionHistory", "item" => "ctrl.note"}
.editor-content#editor-content{"ng-if" => "ctrl.noteReady && !ctrl.note.errorDecrypting"}
@@ -78,7 +81,15 @@
.section{"ng-if" => "ctrl.note.errorDecrypting"}
%p.medium-padding{"style" => "padding-top: 0 !important;"} There was an error decrypting this item. Ensure you are running the latest version of this app, then sign out and sign back in to try again.
- #editor-pane-component-stack
+ #editor-pane-component-stack{"ng-show" => "ctrl.note"}
+ #component-stack-menu-bar.sk-app-bar.no-edges{"ng-if" => "ctrl.componentStack.length"}
+ .left
+ .sk-app-bar-item{"ng-repeat" => "component in ctrl.componentStack", "ng-click" => "ctrl.toggleStackComponentForCurrentItem(component)"}
+ .sk-app-bar-item-column
+ .sk-circle.small{"ng-class" => "{'info' : !component.hidden && component.active, 'neutral' : component.hidden || !component.active}"}
+ .sk-app-bar-item-column
+ .sk-label {{component.name}}
+
.sn-component
- %component-view.component-view.component-stack-item.border-color{"ng-repeat" => "component in ctrl.componentStack",
+ %component-view.component-view.component-stack-item{"ng-repeat" => "component in ctrl.componentStack",
"ng-if" => "component.active", "ng-show" => "!component.hidden", "manual-dealloc" => "true", "component" => "component"}
diff --git a/app/assets/templates/footer.html.haml b/app/assets/templates/footer.html.haml
index c4dfd0171..31192f7eb 100644
--- a/app/assets/templates/footer.html.haml
+++ b/app/assets/templates/footer.html.haml
@@ -1,44 +1,55 @@
.sn-component
- #footer-bar.app-bar.no-edges
+ #footer-bar.sk-app-bar.no-edges
.left
- .item{"ng-click" => "ctrl.accountMenuPressed()", "click-outside" => "ctrl.showAccountMenu = false;", "is-open" => "ctrl.showAccountMenu"}
- .column
- .circle.small{"ng-class" => "ctrl.error ? 'danger' : (ctrl.getUser() ? 'info' : 'default')"}
- .column
- .label.title{"ng-class" => "{red: ctrl.error}"} Account
+
+ .sk-app-bar-item{"ng-click" => "ctrl.accountMenuPressed()", "click-outside" => "ctrl.clickOutsideAccountMenu()", "is-open" => "ctrl.showAccountMenu"}
+ .sk-app-bar-item-column
+ .sk-circle.small{"ng-class" => "ctrl.error ? 'danger' : (ctrl.getUser() ? 'info' : 'neutral')"}
+ .sk-app-bar-item-column
+ .sk-label.title{"ng-class" => "{red: ctrl.error}"} Account
%account-menu{"ng-click" => "$event.stopPropagation()", "ng-if" => "ctrl.showAccountMenu", "on-successful-auth" => "ctrl.onAuthSuccess", "close-function" => "ctrl.closeAccountMenu"}
- .item
- %a.no-decoration.label.title{"href" => "https://standardnotes.org/help", "target" => "_blank"}
+ .sk-app-bar-item
+ %a.no-decoration.sk-label.title{"href" => "https://standardnotes.org/help", "target" => "_blank"}
Help
- .item.border
+ .sk-app-bar-item.border
- .item{"ng-repeat" => "room in ctrl.rooms track by room.uuid"}
- .column{"ng-click" => "ctrl.selectRoom(room)"}
- .label {{room.name}}
+ .sk-app-bar-item{"ng-repeat" => "room in ctrl.rooms track by room.uuid"}
+ .sk-app-bar-item-column{"ng-click" => "ctrl.selectRoom(room)"}
+ .sk-label {{room.name}}
%component-modal{"ng-if" => "room.showRoom", "component" => "room", "on-dismiss" => "ctrl.onRoomDismiss"}
.right
- .item{"ng-if" => "ctrl.securityUpdateAvailable", "ng-click" => "ctrl.openSecurityUpdate()"}
- %span.success.label Security update available.
+ .sk-app-bar-item{"ng-if" => "ctrl.securityUpdateAvailable", "ng-click" => "ctrl.openSecurityUpdate()"}
+ %span.success.sk-label Security update available.
- .item{"ng-if" => "ctrl.newUpdateAvailable == true", "ng-click" => "ctrl.clickedNewUpdateAnnouncement()"}
- %span.info.label New update available.
+ .sk-app-bar-item{"ng-if" => "ctrl.newUpdateAvailable == true", "ng-click" => "ctrl.clickedNewUpdateAnnouncement()"}
+ %span.info.sk-label New update available.
- .item.no-pointer{"ng-if" => "ctrl.lastSyncDate && !ctrl.isRefreshing"}
- .label.subtle
+ .sk-app-bar-item.no-pointer{"ng-if" => "ctrl.lastSyncDate && !ctrl.isRefreshing"}
+ .sk-label.subtle
Last refreshed {{ctrl.lastSyncDate | appDateTime}}
- .item{"ng-if" => "ctrl.lastSyncDate && ctrl.isRefreshing"}
- .spinner.small
+ .sk-app-bar-item{"ng-if" => "ctrl.lastSyncDate && ctrl.isRefreshing"}
+ .sk-spinner.small
- .item{"ng-if" => "ctrl.offline"}
- .label Offline
- .item{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"}
- .label Refresh
+ .sk-app-bar-item{"ng-if" => "ctrl.offline"}
+ .sk-label Offline
+ .sk-app-bar-item{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"}
+ .sk-label Refresh
- .item#lock-item{"ng-if" => "ctrl.hasPasscode()"}
- .label
+ .sk-app-bar-item.border{"ng-if" => "ctrl.dockShortcuts.length > 0"}
+
+ .sk-app-bar-item.dock-shortcut{"ng-repeat" => "shortcut in ctrl.dockShortcuts"}
+ .sk-app-bar-item-column{"ng-click" => "ctrl.selectShortcut(shortcut)", "ng-class" => "{'underline': shortcut.component.active}"}
+ .div{"ng-if" => "shortcut.icon.type == 'circle'"}
+ .sk-circle.small{"ng-style" => "{'background-color': shortcut.icon.background_color, 'border-color': shortcut.icon.border_color}"}
+ .div{"ng-if" => "shortcut.icon.type == 'svg'"}
+ .svg-item{"ng-attr-id" => "dock-svg-{{shortcut.component.uuid}}", "elem-ready" => "ctrl.initSvgForShortcut(shortcut)"}
+
+ .sk-app-bar-item.border{"ng-if" => "ctrl.hasPasscode()"}
+ .sk-app-bar-item#lock-item{"ng-if" => "ctrl.hasPasscode()", "title" => "Locks application and wipes unencrypted data from memory."}
+ .sk-label
%i.icon.ion-locked#footer-lock-icon{"ng-if" => "ctrl.hasPasscode()", "ng-click" => "ctrl.lockApp()"}
diff --git a/app/assets/templates/home.html.haml b/app/assets/templates/home.html.haml
index b8d03b4d3..4ca4c5695 100644
--- a/app/assets/templates/home.html.haml
+++ b/app/assets/templates/home.html.haml
@@ -1,11 +1,11 @@
.main-ui-view{"ng-class" => "platform"}
+
%lock-screen{"ng-if" => "needsUnlock", "on-success" => "onSuccessfulUnlock"}
- .app#app{"ng-if" => "!needsUnlock"}
+
+ .app#app{"ng-if" => "!needsUnlock", "ng-class" => "appClass"}
%tags-section{"save" => "tagsSave", "add-new" => "tagsAddNew", "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", "update-tags" => "updateTagsForNote"}
%footer{"ng-if" => "!needsUnlock"}
diff --git a/app/assets/templates/lock-screen.html.haml b/app/assets/templates/lock-screen.html.haml
index e79559517..72ebdd7d7 100644
--- a/app/assets/templates/lock-screen.html.haml
+++ b/app/assets/templates/lock-screen.html.haml
@@ -1,23 +1,25 @@
#lock-screen.sn-component
- .panel
- .header
- %h1.title Passcode Required
- .content
- .panel-section
- %form.panel-form.panel-row{"ng-submit" => "submitPasscodeForm()"}
- .panel-column.stretch
- %input.panel-row{:type => 'password',
+ .sk-panel
+ .sk-panel-header
+ .sk-panel-header-title Passcode Required
+ .sk-panel-content
+ .sk-panel-section
+ %form.sk-panel-form.sk-panel-row{"ng-submit" => "submitPasscodeForm()"}
+ .sk-panel-column.stretch
+ %input.center-text.sk-input.contrast{:type => 'password',
"ng-model" => "formData.passcode", "autofocus" => "true", "sn-autofocus" => "true", "should-focus" => "true",
"placeholder" => "Enter Passcode", "autocomplete" => "new-password"}
- .button-group.stretch.panel-row.form-submit
- %button.button.info{"type" => "submit"}
- .label Unlock
+ .sk-button-group.stretch.sk-panel-row.form-submit
+ %button.sk-button.info{"type" => "submit"}
+ .sk-label Unlock
- #passcode-reset
- %a.default{"ng-if" => "!formData.showRecovery", "ng-click" => "forgotPasscode()"} Forgot Passcode?
+ .sk-panel-footer
+ #passcode-reset
+ %a.sk-a.neutral{"ng-if" => "!formData.showRecovery", "ng-click" => "forgotPasscode()"} Forgot?
- %div{"ng-if" => "formData.showRecovery"}
- %p
- If you forgot your local passcode, your only option is to clear all your local data from this device
- and sign back in to your account.
- %a.danger{"ng-click" => "beginDeleteData()"} Delete Local Data
+ %div{"ng-if" => "formData.showRecovery"}
+ .sk-p
+ If you forgot your local passcode, your only option is to clear your local data from this device
+ and sign back in to your account.
+ .sk-panel-row
+ %a.sk-a.danger.center-text{"ng-click" => "beginDeleteData()"} Delete Local Data
diff --git a/app/assets/templates/notes.html.haml b/app/assets/templates/notes.html.haml
index a1e48dd1b..475d35711 100644
--- a/app/assets/templates/notes.html.haml
+++ b/app/assets/templates/notes.html.haml
@@ -1,38 +1,40 @@
-.section.notes#notes-column{"aria-label" => "Notes"}
+.sn-component.section.notes#notes-column{"aria-label" => "Notes"}
.content
.section-title-bar#notes-title-bar
.padded
.section-title-bar-header
.title {{ctrl.panelTitle()}}
- .add-button#notes-add-button{"ng-click" => "ctrl.createNewNote()", "title" => "Create a new note in the selected tag"} +
+ .sk-button.contrast.wide{"ng-click" => "ctrl.createNewNote()", "title" => "Create a new note in the selected tag"}
+ .sk-label +
.filter-section{"role" => "search"}
%input.filter-bar#search-bar.mousetrap{"select-on-click" => "true", "ng-model" => "ctrl.noteFilter.text", "placeholder" => "Search",
"ng-change" => "ctrl.filterTextChanged()", "lowercase" => "true", "ng-blur" => "ctrl.onFilterEnter()", "ng-keyup" => "$event.keyCode == 13 && ctrl.onFilterEnter();",
"title" => "Searches notes in the currently selected tag"}
#search-clear-button{"ng-if" => "ctrl.noteFilter.text", "ng-click" => "ctrl.clearFilterText();"} ✕
.sn-component#notes-menu-bar
- .app-bar.no-edges
+ .sk-app-bar.no-edges
.left
- .item{"ng-click" => "ctrl.showMenu = !ctrl.showMenu", "ng-class" => "{'selected' : ctrl.showMenu}"}
- .column
- .label
+ .sk-app-bar-item{"ng-click" => "ctrl.showMenu = !ctrl.showMenu", "ng-class" => "{'selected' : ctrl.showMenu}"}
+ .sk-app-bar-item-column
+ .sk-label
Options
- .column
- .sublabel {{ctrl.optionsSubtitle()}}
+ .sk-app-bar-item-column
+ .sk-sublabel {{ctrl.optionsSubtitle()}}
- .sn-component{"ng-if" => "ctrl.showMenu"}
- .menu-panel.dropdown-menu
- .section
- .header
- %h4.title Sort By
+ .sk-menu-panel.dropdown-menu{"ng-if" => "ctrl.showMenu"}
+ .sk-menu-panel-header
+ .sk-menu-panel-header-title Sort By
+ .sk-button.sk-secondary-contrast{"ng-click" => "ctrl.toggleReverseSort()"}
+ .sk-label
+ %i.icon{"ng-class" => "{'ion-arrow-return-left' : ctrl.sortReverse == false, 'ion-arrow-return-right' : ctrl.sortReverse == true }"}
- %menu-row{"label" => "'Date Added'", "circle" => "ctrl.sortBy == 'created_at' && 'success'", "action" => "ctrl.selectedMenuItem(); ctrl.selectedSortByCreated()", "desc" => "'Sort notes by newest first'"}
- %menu-row{"label" => "'Date Modified'", "circle" => "ctrl.sortBy == 'client_updated_at' && 'success'", "action" => "ctrl.selectedMenuItem(); ctrl.selectedSortByUpdated()", "desc" => "'Sort notes with the most recently updated first'"}
- %menu-row{"label" => "'Title'", "circle" => "ctrl.sortBy == 'title' && 'success'", "action" => "ctrl.selectedMenuItem(); ctrl.selectedSortByTitle()", "desc" => "'Sort notes alphabetically by their title'"}
+ %menu-row{"label" => "'Date Added'", "circle" => "ctrl.sortBy == 'created_at' && 'success'", "action" => "ctrl.selectedMenuItem(); ctrl.selectedSortByCreated()", "desc" => "'Sort notes by newest first'"}
+ %menu-row{"label" => "'Date Modified'", "circle" => "ctrl.sortBy == 'client_updated_at' && 'success'", "action" => "ctrl.selectedMenuItem(); ctrl.selectedSortByUpdated()", "desc" => "'Sort notes with the most recently updated first'"}
+ %menu-row{"label" => "'Title'", "circle" => "ctrl.sortBy == 'title' && 'success'", "action" => "ctrl.selectedMenuItem(); ctrl.selectedSortByTitle()", "desc" => "'Sort notes alphabetically by their title'"}
- .section{"ng-if" => "!ctrl.tag.isSmartTag()"}
- .header
- %h4.title Display
+ .sk-menu-panel-section{"ng-if" => "!ctrl.tag.isSmartTag()"}
+ .sk-menu-panel-header
+ .sk-menu-panel-header-title Display
%menu-row{"label" => "'Archived Notes'", "circle" => "ctrl.showArchived ? 'success' : 'danger'", "faded" => "!ctrl.showArchived", "action" => "ctrl.selectedMenuItem(); ctrl.toggleKey('showArchived')", "desc" => "'Archived notes are usually hidden. You can explicitly show them with this option.'"}
%menu-row{"label" => "'Pinned Notes'", "circle" => "ctrl.hidePinned ? 'danger' : 'success'", "faded" => "ctrl.hidePinned", "action" => "ctrl.selectedMenuItem(); ctrl.toggleKey('hidePinned')", "desc" => "'Pinned notes always appear on top. You can hide them temporarily with this option so you can focus on other notes in the list.'"}
@@ -42,26 +44,31 @@
.scrollable
.infinite-scroll#notes-scrollable{"infinite-scroll" => "ctrl.paginate()", "can-load" => "true", "threshold" => "200"}
- .note{"ng-repeat" => "note in (ctrl.sortedNotes = (ctrl.tag.notes | filter: ctrl.filterNotes | sortBy: ctrl.sortBy | limitTo:ctrl.notesToDisplay)) track by note.uuid",
+ .note{"ng-repeat" => "note in (ctrl.sortedNotes = (ctrl.tag.notes | filter: ctrl.filterNotes | sortBy: ctrl.sortBy:ctrl.sortReverse | limitTo:ctrl.notesToDisplay)) track by note.uuid",
"ng-click" => "ctrl.selectNote(note, true)", "ng-class" => "{'selected' : ctrl.selectedNote == note}"}
%strong.red.medium-text{"ng-if" => "note.conflict_of"} Conflicted copy
%strong.red.medium-text{"ng-if" => "note.errorDecrypting"} Unable to Decrypt
- .pinned.tinted{"ng-if" => "note.pinned", "ng-class" => "{'tinted-selected' : ctrl.selectedNote == note}"}
- %i.icon.ion-bookmark
- %strong.medium-text Pinned
+ .note-flags
+ .pinned.note-flag{"ng-if" => "note.pinned"}
+ %i.icon.ion-bookmark
+ %strong.medium-text Pinned
- .archived.tinted{"ng-if" => "note.archived && !ctrl.tag.isSmartTag()", "ng-class" => "{'tinted-selected' : ctrl.selectedNote == note}"}
- %i.icon.ion-ios-box
- %strong.medium-text Archived
+ .archived.note-flag{"ng-if" => "note.archived && !ctrl.tag.isSmartTag()"}
+ %i.icon.ion-ios-box
+ %strong.medium-text Archived
.tags-string{"ng-if" => "ctrl.shouldShowTags(note)"}
.faded {{note.savedTagsString || note.tagsString()}}
.name{"ng-if" => "note.title"}
+ %span.note-flag{"ng-show" => "note.locked"}
+ %i.icon.ion-locked.medium-text
+ %span.note-flag{"ng-show" => "note.content.protected"}
+ %i.icon.ion-eye-disabled
{{note.title}}
- .note-preview{"ng-if" => "!ctrl.hideNotePreview && !note.content.hidePreview"}
+ .note-preview{"ng-if" => "!ctrl.hideNotePreview && !note.content.hidePreview && !note.content.protected"}
.html-preview{"ng-if" => "note.content.preview_html", "ng-bind-html" => "note.content.preview_html"}
.plain-preview{"ng-if" => "!note.content.preview_html && note.content.preview_plain"} {{note.content.preview_plain}}
.default-preview{"ng-if" => "!note.content.preview_html && !note.content.preview_plain"} {{note.text}}
diff --git a/app/assets/templates/tags.html.haml b/app/assets/templates/tags.html.haml
index 66c401a33..0af5c93b6 100644
--- a/app/assets/templates/tags.html.haml
+++ b/app/assets/templates/tags.html.haml
@@ -1,4 +1,4 @@
-.section.tags#tags-column{"aria-label" => "Tags"}
+.sn-component.section.tags#tags-column{"aria-label" => "Tags"}
.component-view-container{"ng-if" => "ctrl.component.active"}
%component-view.component-view{"component" => "ctrl.component"}
@@ -6,8 +6,10 @@
#tags-content.content{"ng-if" => "!(ctrl.component && ctrl.component.active)"}
#tags-title-bar.section-title-bar
.section-title-bar-header
- .title Tags
- .add-button#tag-add-button{"ng-click" => "ctrl.clickedAddNewTag()", "title" => "Create a new tag"} +
+ .sk-h3.title
+ %span.sk-bold Tags
+ .sk-button.sk-secondary-contrast.wide{"ng-click" => "ctrl.clickedAddNewTag()", "title" => "Create a new tag"}
+ .sk-label +
.scrollable
.infinite-scroll
diff --git a/dist/assets/ionicons.eot b/dist/assets/ionicons.eot
index 9edec3907..b2e176a7b 100644
Binary files a/dist/assets/ionicons.eot and b/dist/assets/ionicons.eot differ
diff --git a/dist/assets/ionicons.svg b/dist/assets/ionicons.svg
old mode 100644
new mode 100755
index 5188c5687..41ab5a832
--- a/dist/assets/ionicons.svg
+++ b/dist/assets/ionicons.svg
@@ -1,16 +1,16 @@