Merge 2.1
This commit is contained in:
9
.gitmodules
vendored
Normal file
9
.gitmodules
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
[submodule "vendor/extensions/extensions-manager"]
|
||||
path = vendor/extensions/extensions-manager
|
||||
url = https://github.com/sn-extensions/extensions-manager.git
|
||||
[submodule "app/extensions/extensions-manager"]
|
||||
path = app/extensions/extensions-manager
|
||||
url = https://github.com/sn-extensions/extensions-manager.git
|
||||
[submodule "public/extensions/extensions-manager"]
|
||||
path = public/extensions/extensions-manager
|
||||
url = https://github.com/sn-extensions/extensions-manager.git
|
||||
7
Capfile
7
Capfile
@@ -4,6 +4,12 @@ require "capistrano/setup"
|
||||
# Include default deployment tasks
|
||||
require "capistrano/deploy"
|
||||
|
||||
require "capistrano/scm/git"
|
||||
install_plugin Capistrano::SCM::Git
|
||||
|
||||
require "capistrano/scm/git-with-submodules"
|
||||
install_plugin Capistrano::SCM::Git::WithSubmodules
|
||||
|
||||
# Include tasks from other gems included in your Gemfile
|
||||
#
|
||||
# For documentation on these, see for example:
|
||||
@@ -23,7 +29,6 @@ require 'capistrano/rails/assets'
|
||||
# require 'capistrano/rails/migrations'
|
||||
require 'capistrano/passenger'
|
||||
# require 'capistrano/sidekiq'
|
||||
require 'capistrano/git-submodule-strategy'
|
||||
# require "whenever/capistrano" # Update crontab on deploy
|
||||
|
||||
|
||||
|
||||
2
Gemfile
2
Gemfile
@@ -44,5 +44,5 @@ group :development, :test do
|
||||
gem 'capistrano-rails'
|
||||
gem 'capistrano-rvm'
|
||||
gem 'capistrano-sidekiq'
|
||||
gem 'capistrano-git-submodule-strategy', '~> 0.1.22'
|
||||
gem 'capistrano-git-with-submodules', '~> 2.0'
|
||||
end
|
||||
|
||||
142
Gemfile.lock
142
Gemfile.lock
@@ -38,74 +38,75 @@ GEM
|
||||
i18n (~> 0.7)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
airbrussh (1.1.1)
|
||||
airbrussh (1.3.0)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
arel (7.1.4)
|
||||
binding_of_caller (0.7.2)
|
||||
binding_of_caller (0.8.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bower-rails (0.10.0)
|
||||
builder (3.2.2)
|
||||
byebug (9.0.6)
|
||||
capistrano (3.6.1)
|
||||
builder (3.2.3)
|
||||
byebug (10.0.0)
|
||||
capistrano (3.10.1)
|
||||
airbrussh (>= 1.0.0)
|
||||
capistrano-harrow
|
||||
i18n
|
||||
rake (>= 10.0.0)
|
||||
sshkit (>= 1.9.0)
|
||||
capistrano-bundler (1.2.0)
|
||||
capistrano-bundler (1.3.0)
|
||||
capistrano (~> 3.1)
|
||||
sshkit (~> 1.2)
|
||||
capistrano-git-submodule-strategy (0.1.22)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-harrow (0.5.3)
|
||||
capistrano-git-with-submodules (2.0.3)
|
||||
capistrano (~> 3.7)
|
||||
capistrano-passenger (0.2.0)
|
||||
capistrano (~> 3.0)
|
||||
capistrano-rails (1.2.0)
|
||||
capistrano-rails (1.3.1)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-bundler (~> 1.1)
|
||||
capistrano-rvm (0.1.2)
|
||||
capistrano (~> 3.0)
|
||||
sshkit (~> 1.2)
|
||||
capistrano-sidekiq (0.10.0)
|
||||
capistrano
|
||||
capistrano-sidekiq (1.0.0)
|
||||
capistrano (>= 3.9.0)
|
||||
sidekiq (>= 3.4)
|
||||
concurrent-ruby (1.0.2)
|
||||
concurrent-ruby (1.0.5)
|
||||
connection_pool (2.2.1)
|
||||
debug_inspector (0.0.2)
|
||||
dotenv (2.1.1)
|
||||
dotenv-rails (2.1.1)
|
||||
dotenv (= 2.1.1)
|
||||
railties (>= 4.0, < 5.1)
|
||||
crass (1.0.3)
|
||||
debug_inspector (0.0.3)
|
||||
dotenv (2.1.2)
|
||||
dotenv-rails (2.1.2)
|
||||
dotenv (= 2.1.2)
|
||||
railties (>= 3.2, < 5.1)
|
||||
erubis (2.7.0)
|
||||
execjs (2.7.0)
|
||||
globalid (0.3.7)
|
||||
activesupport (>= 4.1.0)
|
||||
haml (4.0.7)
|
||||
ffi (1.9.18)
|
||||
globalid (0.4.1)
|
||||
activesupport (>= 4.2.0)
|
||||
haml (5.0.4)
|
||||
temple (>= 0.8.0)
|
||||
tilt
|
||||
i18n (0.7.0)
|
||||
json (1.8.3)
|
||||
loofah (2.0.3)
|
||||
i18n (0.9.3)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (1.8.6)
|
||||
loofah (2.1.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.6.4)
|
||||
mime-types (>= 1.16, < 4)
|
||||
method_source (0.8.2)
|
||||
mime-types (3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2016.0521)
|
||||
mini_portile2 (2.1.0)
|
||||
minitest (5.9.1)
|
||||
mail (2.7.0)
|
||||
mini_mime (>= 0.1.1)
|
||||
method_source (0.9.0)
|
||||
mini_mime (1.0.0)
|
||||
mini_portile2 (2.3.0)
|
||||
minitest (5.11.3)
|
||||
net-scp (1.2.1)
|
||||
net-ssh (>= 2.6.5)
|
||||
net-ssh (3.2.0)
|
||||
net-ssh (4.2.0)
|
||||
nio4r (1.2.1)
|
||||
nokogiri (1.6.8.1)
|
||||
mini_portile2 (~> 2.1.0)
|
||||
nokogiri (1.8.2)
|
||||
mini_portile2 (~> 2.3.0)
|
||||
non-stupid-digest-assets (1.0.9)
|
||||
sprockets (>= 2.0)
|
||||
puma (3.6.2)
|
||||
rack (2.0.1)
|
||||
rack-cors (1.0.0)
|
||||
rack-protection (1.5.3)
|
||||
puma (3.11.2)
|
||||
rack (2.0.3)
|
||||
rack-cors (1.0.2)
|
||||
rack-protection (2.0.0)
|
||||
rack
|
||||
rack-test (0.6.3)
|
||||
rack (>= 1.0)
|
||||
@@ -121,9 +122,9 @@ GEM
|
||||
bundler (>= 1.3.0, < 2.0)
|
||||
railties (= 5.0.0.1)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.1)
|
||||
activesupport (>= 4.2.0, < 6.0)
|
||||
nokogiri (~> 1.6.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.0.3)
|
||||
loofah (~> 2.0)
|
||||
railties (5.0.0.1)
|
||||
@@ -132,50 +133,59 @@ GEM
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
rake (11.3.0)
|
||||
rake (12.3.0)
|
||||
rb-fsevent (0.10.2)
|
||||
rb-inotify (0.9.10)
|
||||
ffi (>= 0.5.0, < 2)
|
||||
rdoc (4.3.0)
|
||||
redis (3.3.2)
|
||||
responders (2.3.0)
|
||||
railties (>= 4.2.0, < 5.1)
|
||||
sass (3.4.22)
|
||||
redis (4.0.1)
|
||||
responders (2.4.0)
|
||||
actionpack (>= 4.2.0, < 5.3)
|
||||
railties (>= 4.2.0, < 5.3)
|
||||
sass (3.5.5)
|
||||
sass-listen (~> 4.0.0)
|
||||
sass-listen (4.0.0)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
sdoc (0.4.2)
|
||||
json (~> 1.7, >= 1.7.7)
|
||||
rdoc (~> 4.0)
|
||||
secure_headers (3.6.7)
|
||||
useragent
|
||||
sidekiq (4.2.7)
|
||||
secure_headers (5.0.4)
|
||||
useragent (>= 0.15.0)
|
||||
sidekiq (5.0.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
rack-protection (>= 1.5.0)
|
||||
redis (~> 3.2, >= 3.2.1)
|
||||
spring (2.0.0)
|
||||
redis (>= 3.3.4, < 5)
|
||||
spring (2.0.2)
|
||||
activesupport (>= 4.2)
|
||||
sprockets (3.7.0)
|
||||
sprockets (3.7.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.2.0)
|
||||
sprockets-rails (3.2.1)
|
||||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
sshkit (1.11.4)
|
||||
sshkit (1.15.1)
|
||||
net-scp (>= 1.1.2)
|
||||
net-ssh (>= 2.8.0)
|
||||
thor (0.19.4)
|
||||
thread_safe (0.3.5)
|
||||
tilt (2.0.5)
|
||||
tzinfo (1.2.2)
|
||||
temple (0.8.0)
|
||||
thor (0.20.0)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.8)
|
||||
tzinfo (1.2.4)
|
||||
thread_safe (~> 0.1)
|
||||
uglifier (3.0.3)
|
||||
uglifier (4.1.5)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
useragent (0.16.8)
|
||||
useragent (0.16.9)
|
||||
web-console (2.3.0)
|
||||
activemodel (>= 4.0)
|
||||
binding_of_caller (>= 0.7.2)
|
||||
railties (>= 4.0)
|
||||
sprockets-rails (>= 2.0, < 4.0)
|
||||
websocket-driver (0.6.4)
|
||||
websocket-driver (0.6.5)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.2)
|
||||
websocket-extensions (0.1.3)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
@@ -185,7 +195,7 @@ DEPENDENCIES
|
||||
byebug
|
||||
capistrano
|
||||
capistrano-bundler
|
||||
capistrano-git-submodule-strategy (~> 0.1.22)
|
||||
capistrano-git-with-submodules (~> 2.0)
|
||||
capistrano-passenger (>= 0.2.0)
|
||||
capistrano-rails
|
||||
capistrano-rvm
|
||||
@@ -205,4 +215,4 @@ DEPENDENCIES
|
||||
web-console (~> 2.0)
|
||||
|
||||
BUNDLED WITH
|
||||
1.15.1
|
||||
1.16.1
|
||||
|
||||
21
Gruntfile.js
21
Gruntfile.js
@@ -34,7 +34,7 @@ module.exports = function(grunt) {
|
||||
style: 'expanded'
|
||||
},
|
||||
files: {
|
||||
'vendor/assets/stylesheets/app.css': 'app/assets/stylesheets/frontend.css.scss'
|
||||
'vendor/assets/stylesheets/app.css': 'app/assets/stylesheets/main.css.scss'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -59,7 +59,7 @@ module.exports = function(grunt) {
|
||||
src: ['**/*.html'],
|
||||
dest: 'vendor/assets/javascripts/templates.js',
|
||||
options: {
|
||||
module: 'app.frontend'
|
||||
module: 'app'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -70,12 +70,13 @@ module.exports = function(grunt) {
|
||||
},
|
||||
app: {
|
||||
src: [
|
||||
'app/assets/javascripts/app/services/encryption/*.js',
|
||||
'app/assets/javascripts/app/services/encryption/*.js', // this should come first
|
||||
'app/assets/javascripts/app/*.js',
|
||||
'app/assets/javascripts/app/frontend/*.js',
|
||||
'app/assets/javascripts/app/frontend/controllers/*.js',
|
||||
'app/assets/javascripts/app/frontend/models/**/*.js',
|
||||
'app/assets/javascripts/app/services/**/*.js'
|
||||
'app/assets/javascripts/app/controllers/**/*.js',
|
||||
'app/assets/javascripts/app/models/**/*.js',
|
||||
'app/assets/javascripts/app/services/**/*.js',
|
||||
'app/assets/javascripts/app/filters/**/*.js',
|
||||
'app/assets/javascripts/app/directives/**/*.js',
|
||||
],
|
||||
dest: 'vendor/assets/javascripts/app.js',
|
||||
},
|
||||
@@ -95,8 +96,12 @@ module.exports = function(grunt) {
|
||||
},
|
||||
|
||||
css: {
|
||||
options: {
|
||||
separator: '',
|
||||
},
|
||||
src: [
|
||||
'vendor/assets/stylesheets/app.css'
|
||||
'vendor/assets/stylesheets/app.css',
|
||||
'node_modules/sn-stylekit/dist/stylekit.css'
|
||||
],
|
||||
dest: 'vendor/assets/stylesheets/app.css'
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.0 KiB |
@@ -13,7 +13,7 @@ if(!IEOrEdge && (window.crypto && window.crypto.subtle)) {
|
||||
Neeto.crypto = new SNCryptoJS();
|
||||
}
|
||||
|
||||
angular.module('app.frontend', [])
|
||||
angular.module('app', [])
|
||||
|
||||
function getParameterByName(name, url) {
|
||||
name = name.replace(/[\[\]]/g, "\\$&");
|
||||
@@ -36,3 +36,17 @@ function parametersFromURL(url) {
|
||||
function isDesktopApplication() {
|
||||
return window && window.process && window.process.type && window.process.versions["electron"];
|
||||
}
|
||||
|
||||
function isMacApplication() {
|
||||
return window && window.process && window.process.type && window.process.platform == "darwin";
|
||||
}
|
||||
|
||||
/* Use with numbers and strings, not objects */
|
||||
Array.prototype.containsPrimitiveSubset = function(array) {
|
||||
return !array.some(val => this.indexOf(val) === -1);
|
||||
}
|
||||
|
||||
/* Use with numbers and strings, not objects */
|
||||
Array.prototype.containsObjectSubset = function(array) {
|
||||
return !array.some(val => !_.find(this, val));
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('app.frontend')
|
||||
angular.module('app')
|
||||
.directive("editorSection", function($timeout, $sce){
|
||||
return {
|
||||
restrict: 'E',
|
||||
@@ -8,7 +8,7 @@ angular.module('app.frontend')
|
||||
note: "=",
|
||||
updateTags: "&"
|
||||
},
|
||||
templateUrl: 'frontend/editor.html',
|
||||
templateUrl: 'editor.html',
|
||||
replace: true,
|
||||
controller: 'EditorCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
@@ -23,10 +23,10 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, extensionManager, syncManager, modelManager, themeManager, componentManager, storageManager) {
|
||||
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, actionsManager, syncManager, modelManager, themeManager, componentManager, storageManager) {
|
||||
|
||||
this.spellcheck = true;
|
||||
this.componentManager = componentManager;
|
||||
this.componentStack = [];
|
||||
|
||||
$rootScope.$on("sync:taking-too-long", function(){
|
||||
this.syncTakingTooLong = true;
|
||||
@@ -50,31 +50,30 @@ angular.module('app.frontend')
|
||||
this.showMenu = false;
|
||||
this.loadTagsString();
|
||||
|
||||
let onReady = () => {
|
||||
this.noteReady = true;
|
||||
$timeout(() => {
|
||||
this.loadPreferences();
|
||||
})
|
||||
}
|
||||
|
||||
let associatedEditor = this.editorForNote(note);
|
||||
if(associatedEditor) {
|
||||
if(associatedEditor && associatedEditor != this.selectedEditor) {
|
||||
// setting note to not ready will remove the editor from view in a flash,
|
||||
// so we only want to do this if switching between external editors
|
||||
this.noteReady = false;
|
||||
} else {
|
||||
this.noteReady = true;
|
||||
}
|
||||
|
||||
if(this.editorComponent && this.editorComponent != associatedEditor) {
|
||||
// Deactivate old editor
|
||||
componentManager.deactivateComponent(this.editorComponent);
|
||||
this.editorComponent = null;
|
||||
}
|
||||
|
||||
// Activate new editor if it's different from the one currently activated
|
||||
if(associatedEditor && associatedEditor != this.editorComponent) {
|
||||
// switch after timeout, so that note data isnt posted to current editor
|
||||
// switch after timeout, so that note data isnt posted to current editor
|
||||
$timeout(() => {
|
||||
this.enableComponent(associatedEditor);
|
||||
this.editorComponent = associatedEditor;
|
||||
this.noteReady = true;
|
||||
this.selectedEditor = associatedEditor;
|
||||
onReady();
|
||||
})
|
||||
} else if(associatedEditor) {
|
||||
// Same editor as currently active
|
||||
onReady();
|
||||
} else {
|
||||
this.noteReady = true;
|
||||
// No editor
|
||||
this.selectedEditor = null;
|
||||
onReady();
|
||||
}
|
||||
|
||||
if(note.safeText().length == 0 && note.dummy) {
|
||||
@@ -93,7 +92,7 @@ angular.module('app.frontend')
|
||||
this.editorForNote = function(note) {
|
||||
let editors = componentManager.componentsForArea("editor-editor");
|
||||
for(var editor of editors) {
|
||||
if(editor.isActiveForItem(note)) {
|
||||
if(editor.isExplicitlyEnabledForItem(note)) {
|
||||
return editor;
|
||||
}
|
||||
}
|
||||
@@ -104,33 +103,56 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedEditor = function(editorComponent) {
|
||||
this.onEditorMenuClick = function() {
|
||||
// App bar menu item click
|
||||
this.showEditorMenu = !this.showEditorMenu;
|
||||
this.showMenu = false;
|
||||
this.showExtensions = false;
|
||||
}
|
||||
|
||||
this.closeAllMenus = function() {
|
||||
this.showEditorMenu = false;
|
||||
this.showMenu = false;
|
||||
this.showExtensions = false;
|
||||
}
|
||||
|
||||
if(this.editorComponent && this.editorComponent !== editorComponent) {
|
||||
// This disassociates the editor from the note, but the component itself still needs to be deactivated
|
||||
this.disableComponentForCurrentItem(this.editorComponent);
|
||||
// Now deactivate the component
|
||||
componentManager.deactivateComponent(this.editorComponent);
|
||||
this.editorMenuOnSelect = function(component) {
|
||||
if(!component || component.area == "editor-editor") {
|
||||
// if plain editor or other editor
|
||||
this.showEditorMenu = false;
|
||||
var editor = component;
|
||||
if(this.selectedEditor && editor !== this.selectedEditor) {
|
||||
this.disassociateComponentWithCurrentNote(this.selectedEditor);
|
||||
}
|
||||
if(editor) {
|
||||
if(this.note.getAppDataItem("prefersPlainEditor") == true) {
|
||||
this.note.setAppDataItem("prefersPlainEditor", false);
|
||||
this.note.setDirty(true);
|
||||
}
|
||||
this.associateComponentWithCurrentNote(editor);
|
||||
} else {
|
||||
// Note prefers plain editor
|
||||
if(!this.note.getAppDataItem("prefersPlainEditor")) {
|
||||
this.note.setAppDataItem("prefersPlainEditor", true);
|
||||
this.note.setDirty(true);
|
||||
}
|
||||
$timeout(() => {
|
||||
this.reloadFont();
|
||||
})
|
||||
}
|
||||
|
||||
this.selectedEditor = editor;
|
||||
} else if(component.area == "editor-stack") {
|
||||
// If component stack item
|
||||
this.toggleStackComponentForCurrentItem(component);
|
||||
}
|
||||
|
||||
if(editorComponent) {
|
||||
this.note.setAppDataItem("prefersPlainEditor", false);
|
||||
this.note.setDirty(true);
|
||||
this.enableComponent(editorComponent);
|
||||
this.associateComponentWithCurrentItem(editorComponent);
|
||||
} else {
|
||||
// Note prefers plain editor
|
||||
this.note.setAppDataItem("prefersPlainEditor", true);
|
||||
this.note.setDirty(true);
|
||||
syncManager.sync();
|
||||
}
|
||||
|
||||
this.editorComponent = editorComponent;
|
||||
// Lots of dirtying can happen above, so we'll sync
|
||||
syncManager.sync("editorMenuOnSelect");
|
||||
}.bind(this)
|
||||
|
||||
this.hasAvailableExtensions = function() {
|
||||
return extensionManager.extensionsInContextOfItem(this.note).length > 0;
|
||||
return actionsManager.extensionsInContextOfItem(this.note).length > 0;
|
||||
}
|
||||
|
||||
this.focusEditor = function(delay) {
|
||||
@@ -183,16 +205,25 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
var saveTimeout;
|
||||
this.changesMade = function() {
|
||||
this.note.hasChanges = true;
|
||||
this.changesMade = function(bypassDebouncer = false) {
|
||||
this.note.dummy = false;
|
||||
|
||||
/* In the case of keystrokes, saving should go through a debouncer to avoid frequent calls.
|
||||
In the case of deleting or archiving a note, it should happen immediately before the note is switched out
|
||||
*/
|
||||
let delay = bypassDebouncer ? 0 : 275;
|
||||
|
||||
// In the case of archiving a note, the note is saved immediately, then switched to another note.
|
||||
// Usually note.hasChanges is set back to false after the saving delay, but in this case, because there is no delay,
|
||||
// we set it to false immediately so that it is not saved twice: once now, and the other on setNote in oldNote.hasChanges.
|
||||
this.note.hasChanges = bypassDebouncer ? false : true;
|
||||
|
||||
if(saveTimeout) $timeout.cancel(saveTimeout);
|
||||
if(statusTimeout) $timeout.cancel(statusTimeout);
|
||||
saveTimeout = $timeout(function(){
|
||||
this.showSavingStatus();
|
||||
this.saveNote();
|
||||
}.bind(this), 275)
|
||||
}.bind(this), delay)
|
||||
}
|
||||
|
||||
this.showSavingStatus = function() {
|
||||
@@ -229,18 +260,13 @@ angular.module('app.frontend')
|
||||
|
||||
this.onNameBlur = function() {
|
||||
this.editingName = false;
|
||||
this.updateTagsFromTagsString()
|
||||
}
|
||||
|
||||
this.toggleFullScreen = function() {
|
||||
this.fullscreen = !this.fullscreen;
|
||||
if(this.fullscreen) {
|
||||
this.focusEditor(0);
|
||||
this.selectedMenuItem = function($event, hide) {
|
||||
if(hide) {
|
||||
this.showMenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedMenuItem = function($event) {
|
||||
this.showMenu = false;
|
||||
$event.stopPropagation();
|
||||
}
|
||||
|
||||
this.deleteNote = function() {
|
||||
@@ -260,7 +286,7 @@ angular.module('app.frontend')
|
||||
this.toggleArchiveNote = function() {
|
||||
this.note.setAppDataItem("archived", !this.note.archived);
|
||||
this.note.setDirty(true);
|
||||
this.changesMade();
|
||||
this.changesMade(true);
|
||||
$rootScope.$broadcast("noteArchived");
|
||||
}
|
||||
|
||||
@@ -280,11 +306,7 @@ angular.module('app.frontend')
|
||||
*/
|
||||
|
||||
this.loadTagsString = function() {
|
||||
var string = "";
|
||||
for(var tag of this.note.tags) {
|
||||
string += "#" + tag.title + " ";
|
||||
}
|
||||
this.tagsString = string;
|
||||
this.tagsString = this.note.tagsString();
|
||||
}
|
||||
|
||||
this.addTag = function(tag) {
|
||||
@@ -309,20 +331,100 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
this.updateTagsFromTagsString = function() {
|
||||
var tags = this.tagsString.split("#");
|
||||
tags = _.filter(tags, function(tag){
|
||||
return tag.length > 0;
|
||||
})
|
||||
tags = _.map(tags, function(tag){
|
||||
return tag.trim();
|
||||
if(this.tagsString == this.note.tagsString()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var strings = this.tagsString.split("#").filter((string) => {
|
||||
return string.length > 0;
|
||||
}).map((string) => {
|
||||
return string.trim();
|
||||
})
|
||||
|
||||
this.note.dummy = false;
|
||||
this.updateTags()(this.note, tags);
|
||||
this.updateTags()(this.note, strings);
|
||||
}
|
||||
|
||||
|
||||
/* Resizability */
|
||||
|
||||
this.resizeControl = {};
|
||||
|
||||
this.onPanelResizeFinish = function(width, left, isMaxWidth) {
|
||||
if(isMaxWidth) {
|
||||
authManager.setUserPrefValue("editorWidth", null);
|
||||
} else {
|
||||
if(width !== undefined && width !== null) {
|
||||
authManager.setUserPrefValue("editorWidth", width);
|
||||
}
|
||||
}
|
||||
|
||||
if(left !== undefined && left !== null) {
|
||||
authManager.setUserPrefValue("editorLeft", left);
|
||||
}
|
||||
authManager.syncUserPreferences();
|
||||
}
|
||||
|
||||
$rootScope.$on("user-preferences-changed", () => {
|
||||
this.loadPreferences();
|
||||
});
|
||||
|
||||
this.loadPreferences = function() {
|
||||
this.monospaceFont = authManager.getUserPrefValue("monospaceFont", "monospace");
|
||||
this.spellcheck = authManager.getUserPrefValue("spellcheck", true);
|
||||
|
||||
if(!document.getElementById("editor-content")) {
|
||||
// Elements have not yet loaded due to ng-if around wrapper
|
||||
return;
|
||||
}
|
||||
|
||||
this.reloadFont();
|
||||
|
||||
let width = authManager.getUserPrefValue("editorWidth", null);
|
||||
if(width !== null) {
|
||||
this.resizeControl.setWidth(width);
|
||||
}
|
||||
|
||||
let left = authManager.getUserPrefValue("editorLeft", null);
|
||||
if(left !== null) {
|
||||
this.resizeControl.setLeft(left);
|
||||
}
|
||||
}
|
||||
|
||||
this.reloadFont = function() {
|
||||
var editable = document.getElementById("note-text-editor");
|
||||
|
||||
if(!editable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.monospaceFont) {
|
||||
if(isMacApplication()) {
|
||||
editable.style.fontFamily = "Menlo, Consolas, 'DejaVu Sans Mono', monospace";
|
||||
} else {
|
||||
editable.style.fontFamily = "monospace";
|
||||
}
|
||||
} else {
|
||||
editable.style.fontFamily = "inherit";
|
||||
}
|
||||
}
|
||||
|
||||
this.toggleKey = function(key) {
|
||||
this[key] = !this[key];
|
||||
authManager.setUserPrefValue(key, this[key], true);
|
||||
this.reloadFont();
|
||||
|
||||
if(key == "spellcheck") {
|
||||
// Allows textarea to reload
|
||||
this.noteReady = false;
|
||||
$timeout(() => {
|
||||
this.noteReady = true;
|
||||
$timeout(() => {
|
||||
this.reloadFont();
|
||||
})
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -330,43 +432,27 @@ angular.module('app.frontend')
|
||||
Components
|
||||
*/
|
||||
|
||||
componentManager.registerHandler({identifier: "editor", areas: ["note-tags", "editor-stack", "editor-editor"], activationHandler: function(component){
|
||||
|
||||
componentManager.registerHandler({identifier: "editor", areas: ["note-tags", "editor-stack", "editor-editor"], activationHandler: (component) => {
|
||||
if(component.area === "note-tags") {
|
||||
// Autocomplete Tags
|
||||
this.tagsComponent = component.active ? component : null;
|
||||
} else if(component.area == "editor-stack") {
|
||||
// Stack
|
||||
if(component.active) {
|
||||
if(!_.find(this.componentStack, component)) {
|
||||
this.componentStack.push(component);
|
||||
}
|
||||
} else {
|
||||
_.pull(this.componentStack, component);
|
||||
}
|
||||
} else {
|
||||
} else if(component.area == "editor-editor") {
|
||||
// Editor
|
||||
if(component.active && this.note && component.isActiveForItem(this.note)) {
|
||||
this.editorComponent = component;
|
||||
if(component.active && this.note && (component.isExplicitlyEnabledForItem(this.note) || component.isDefaultEditor())) {
|
||||
this.selectedEditor = component;
|
||||
} else {
|
||||
this.editorComponent = null;
|
||||
this.selectedEditor = null;
|
||||
}
|
||||
} else if(component.area == "editor-stack") {
|
||||
this.reloadComponentContext();
|
||||
}
|
||||
|
||||
if(component.active) {
|
||||
$timeout(function(){
|
||||
var iframe = componentManager.iframeForComponent(component);
|
||||
if(iframe) {
|
||||
iframe.onload = function() {
|
||||
componentManager.registerComponentWindow(component, iframe.contentWindow);
|
||||
}.bind(this);
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
}.bind(this), contextRequestHandler: function(component){
|
||||
}, contextRequestHandler: (component) => {
|
||||
return this.note;
|
||||
}.bind(this), actionHandler: function(component, action, data){
|
||||
}, focusHandler: (component, focused) => {
|
||||
if(component.isEditor() && focused) {
|
||||
this.closeAllMenus();
|
||||
}
|
||||
}, actionHandler: (component, action, data) => {
|
||||
if(action === "set-size") {
|
||||
var setSize = function(element, size) {
|
||||
var widthString = typeof size.width === 'string' ? size.width : `${data.width}px`;
|
||||
@@ -374,21 +460,10 @@ angular.module('app.frontend')
|
||||
element.setAttribute("style", `width:${widthString}; height:${heightString}; `);
|
||||
}
|
||||
|
||||
if(data.type === "content") {
|
||||
var iframe = componentManager.iframeForComponent(component);
|
||||
var width = data.width;
|
||||
var height = data.height;
|
||||
iframe.width = width;
|
||||
iframe.height = height;
|
||||
|
||||
setSize(iframe, data);
|
||||
} else {
|
||||
if(data.type == "container") {
|
||||
if(component.area == "note-tags") {
|
||||
var container = document.getElementById("note-tags-component-container");
|
||||
setSize(container, data);
|
||||
} else {
|
||||
var container = document.getElementById("component-" + component.uuid);
|
||||
setSize(container, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -423,11 +498,15 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
}
|
||||
}.bind(this)});
|
||||
}});
|
||||
|
||||
this.reloadComponentContext = function() {
|
||||
// componentStack is used by the template to ng-repeat
|
||||
this.componentStack = componentManager.componentsForArea("editor-stack");
|
||||
for(var component of this.componentStack) {
|
||||
componentManager.setEventFlowForComponent(component, component.isActiveForItem(this.note));
|
||||
if(component.active) {
|
||||
component.hidden = !this.note || component.isExplicitlyDisabledForItem(this.note);
|
||||
}
|
||||
}
|
||||
|
||||
componentManager.contextItemDidChangeInArea("note-tags");
|
||||
@@ -435,51 +514,41 @@ angular.module('app.frontend')
|
||||
componentManager.contextItemDidChangeInArea("editor-editor");
|
||||
}
|
||||
|
||||
this.enableComponent = function(component) {
|
||||
componentManager.activateComponent(component);
|
||||
componentManager.setEventFlowForComponent(component, 1);
|
||||
}
|
||||
|
||||
this.associateComponentWithCurrentItem = function(component) {
|
||||
componentManager.associateComponentWithItem(component, this.note);
|
||||
}
|
||||
|
||||
let alertKey = "displayed-component-disable-alert";
|
||||
this.disableComponentForCurrentItem = function(component, showAlert) {
|
||||
componentManager.disassociateComponentWithItem(component, this.note);
|
||||
componentManager.setEventFlowForComponent(component, 0);
|
||||
if(showAlert && !storageManager.getItem(alertKey)) {
|
||||
alert("This component will be disabled for this note. You can re-enable this component in the 'Menu' of the editor pane.");
|
||||
storageManager.setItem(alertKey, true);
|
||||
}
|
||||
}
|
||||
|
||||
this.hasDisabledStackComponents = function() {
|
||||
for(var component of this.componentStack) {
|
||||
if(component.ignoreEvents) {
|
||||
return true;
|
||||
this.toggleStackComponentForCurrentItem = function(component) {
|
||||
if(component.hidden) {
|
||||
// Unhide, associate with current item
|
||||
component.hidden = false;
|
||||
if(!component.active) {
|
||||
componentManager.activateComponent(component);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
this.restoreDisabledStackComponents = function() {
|
||||
var relevantComponents = this.componentStack.filter(function(component){
|
||||
return component.ignoreEvents;
|
||||
})
|
||||
|
||||
componentManager.enableComponentsForItem(relevantComponents, this.note);
|
||||
|
||||
for(var component of relevantComponents) {
|
||||
componentManager.setEventFlowForComponent(component, true);
|
||||
this.associateComponentWithCurrentNote(component);
|
||||
componentManager.contextItemDidChangeInArea("editor-stack");
|
||||
} else {
|
||||
// not hidden, hide
|
||||
component.hidden = true;
|
||||
this.disassociateComponentWithCurrentNote(component);
|
||||
}
|
||||
}
|
||||
|
||||
this.disassociateComponentWithCurrentNote = function(component) {
|
||||
component.associatedItemIds = component.associatedItemIds.filter((id) => {return id !== this.note.uuid});
|
||||
|
||||
if(!component.disassociatedItemIds.includes(this.note.uuid)) {
|
||||
component.disassociatedItemIds.push(this.note.uuid);
|
||||
}
|
||||
|
||||
component.setDirty(true);
|
||||
}
|
||||
|
||||
this.associateComponentWithCurrentNote = function(component) {
|
||||
component.disassociatedItemIds = component.disassociatedItemIds.filter((id) => {return id !== this.note.uuid});
|
||||
|
||||
if(!component.associatedItemIds.includes(this.note.uuid)) {
|
||||
component.associatedItemIds.push(this.note.uuid);
|
||||
}
|
||||
|
||||
component.setDirty(true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
angular.module('app.frontend')
|
||||
angular.module('app')
|
||||
.directive("footer", function(authManager){
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {},
|
||||
templateUrl: 'frontend/footer.html',
|
||||
templateUrl: 'footer.html',
|
||||
replace: true,
|
||||
controller: 'FooterCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
@@ -22,9 +22,12 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('FooterCtrl', function ($rootScope, authManager, modelManager, $timeout, dbManager, syncManager, storageManager, passcodeManager) {
|
||||
.controller('FooterCtrl', function ($rootScope, authManager, modelManager, $timeout, dbManager,
|
||||
syncManager, storageManager, passcodeManager, componentManager, singletonManager) {
|
||||
|
||||
this.user = authManager.user;
|
||||
this.getUser = function() {
|
||||
return authManager.user;
|
||||
}
|
||||
|
||||
this.updateOfflineStatus = function() {
|
||||
this.offline = authManager.offline();
|
||||
@@ -45,23 +48,11 @@ angular.module('app.frontend')
|
||||
}.bind(this)
|
||||
|
||||
this.accountMenuPressed = function() {
|
||||
this.serverData = {};
|
||||
this.showAccountMenu = !this.showAccountMenu;
|
||||
this.showFaq = false;
|
||||
this.showNewPasswordForm = false;
|
||||
this.showExtensionsMenu = false;
|
||||
this.showIOMenu = false;
|
||||
this.closeAllRooms();
|
||||
}
|
||||
|
||||
this.toggleExtensions = function() {
|
||||
this.showAccountMenu = false;
|
||||
this.showIOMenu = false;
|
||||
this.showExtensionsMenu = !this.showExtensionsMenu;
|
||||
}
|
||||
|
||||
this.toggleIO = function() {
|
||||
this.showIOMenu = !this.showIOMenu;
|
||||
this.showExtensionsMenu = false;
|
||||
this.closeAccountMenu = () => {
|
||||
this.showAccountMenu = false;
|
||||
}
|
||||
|
||||
@@ -75,7 +66,7 @@ angular.module('app.frontend')
|
||||
|
||||
this.refreshData = function() {
|
||||
this.isRefreshing = true;
|
||||
syncManager.sync(function(response){
|
||||
syncManager.sync((response) => {
|
||||
$timeout(function(){
|
||||
this.isRefreshing = false;
|
||||
}.bind(this), 200)
|
||||
@@ -84,7 +75,7 @@ angular.module('app.frontend')
|
||||
} else {
|
||||
this.syncUpdated();
|
||||
}
|
||||
}.bind(this));
|
||||
}, null, "refreshData");
|
||||
}
|
||||
|
||||
this.syncUpdated = function() {
|
||||
@@ -104,6 +95,60 @@ angular.module('app.frontend')
|
||||
|
||||
this.clickedNewUpdateAnnouncement = function() {
|
||||
this.newUpdateAvailable = false;
|
||||
alert("A new update is ready to install. Updates address performance and security issues, as well as bug fixes and feature enhancements. Simply quit Standard Notes and re-open it for the update to be applied.")
|
||||
alert("A new update is ready to install. Simply quit Standard Notes and reopen it after a brief delay to apply the update.")
|
||||
}
|
||||
|
||||
|
||||
/* Rooms */
|
||||
|
||||
this.componentManager = componentManager;
|
||||
this.rooms = [];
|
||||
|
||||
modelManager.addItemSyncObserver("room-bar", "SN|Component", (allItems, validItems, deletedItems, source) => {
|
||||
var incomingRooms = allItems.filter((candidate) => {return candidate.area == "rooms"});
|
||||
this.rooms = _.uniq(this.rooms.concat(incomingRooms)).filter((candidate) => {return !candidate.deleted});
|
||||
});
|
||||
|
||||
componentManager.registerHandler({identifier: "roomBar", areas: ["rooms", "modal"], activationHandler: (component) => {
|
||||
if(component.active) {
|
||||
// Show room, if it was not activated manually (in the event of event from componentManager)
|
||||
if(component.area == "rooms" && !component.showRoom) {
|
||||
component.showRoom = true;
|
||||
}
|
||||
$timeout(() => {
|
||||
var lastSize = component.getLastSize();
|
||||
if(lastSize) {
|
||||
componentManager.handleSetSizeEvent(component, lastSize);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, actionHandler: (component, action, data) => {
|
||||
if(action == "set-size") {
|
||||
component.setLastSize(data);
|
||||
}
|
||||
}, focusHandler: (component, focused) => {
|
||||
if(component.isEditor() && focused) {
|
||||
this.closeAllRooms();
|
||||
this.closeAccountMenu();
|
||||
}
|
||||
}});
|
||||
|
||||
$rootScope.$on("editorFocused", () => {
|
||||
this.closeAllRooms();
|
||||
this.closeAccountMenu();
|
||||
})
|
||||
|
||||
this.onRoomDismiss = function(room) {
|
||||
room.showRoom = false;
|
||||
}
|
||||
|
||||
this.closeAllRooms = function() {
|
||||
for(var room of this.rooms) {
|
||||
room.showRoom = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.selectRoom = function(room) {
|
||||
room.showRoom = !room.showRoom;
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('app.frontend')
|
||||
angular.module('app')
|
||||
.controller('HomeCtrl', function ($scope, $location, $rootScope, $timeout, modelManager,
|
||||
dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager, migrationManager) {
|
||||
|
||||
@@ -8,6 +8,11 @@ angular.module('app.frontend')
|
||||
$rootScope.$broadcast('new-update-available', version);
|
||||
}
|
||||
|
||||
/* Used to avoid circular dependencies where syncManager cannot be imported but rootScope can */
|
||||
$rootScope.sync = function(source) {
|
||||
syncManager.sync("$rootScope.sync - " + source);
|
||||
}
|
||||
|
||||
$rootScope.lockApplication = function() {
|
||||
// Reloading wipes current objects from memory
|
||||
window.location.reload();
|
||||
@@ -44,7 +49,7 @@ angular.module('app.frontend')
|
||||
dbManager.openDatabase(null, function() {
|
||||
// new database, delete syncToken so that items can be refetched entirely from server
|
||||
syncManager.clearSyncToken();
|
||||
syncManager.sync();
|
||||
syncManager.sync("openDatabase");
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,15 +57,14 @@ angular.module('app.frontend')
|
||||
authManager.loadInitialData();
|
||||
syncManager.loadLocalItems(function(items) {
|
||||
$scope.allTag.didLoad = true;
|
||||
themeManager.activateInitialTheme();
|
||||
$scope.$apply();
|
||||
|
||||
$rootScope.$broadcast("initial-data-loaded");
|
||||
|
||||
syncManager.sync(null);
|
||||
syncManager.sync("initiateSync");
|
||||
// refresh every 30s
|
||||
setInterval(function () {
|
||||
syncManager.sync(null);
|
||||
syncManager.sync("timer");
|
||||
}, 30000);
|
||||
});
|
||||
}
|
||||
@@ -111,7 +115,7 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
note.setDirty(true);
|
||||
syncManager.sync();
|
||||
syncManager.sync("updateTagsForNote");
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -141,7 +145,7 @@ angular.module('app.frontend')
|
||||
return;
|
||||
}
|
||||
tag.setDirty(true);
|
||||
syncManager.sync(callback);
|
||||
syncManager.sync(callback, null, "tagsSave");
|
||||
$rootScope.$broadcast("tag-changed");
|
||||
modelManager.resortTag(tag);
|
||||
}
|
||||
@@ -157,7 +161,7 @@ angular.module('app.frontend')
|
||||
syncManager.sync(function(){
|
||||
// force scope tags to update on sub directives
|
||||
$scope.safeApply();
|
||||
});
|
||||
}, null, "removeTag");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +199,7 @@ angular.module('app.frontend')
|
||||
callback(true);
|
||||
}
|
||||
}
|
||||
})
|
||||
}, null, "saveNote")
|
||||
}
|
||||
|
||||
$scope.safeApply = function(fn) {
|
||||
@@ -236,7 +240,7 @@ angular.module('app.frontend')
|
||||
} else {
|
||||
$scope.notifyDelete();
|
||||
}
|
||||
});
|
||||
}, null, "deleteNote");
|
||||
}
|
||||
|
||||
|
||||
@@ -259,12 +263,13 @@ angular.module('app.frontend')
|
||||
return;
|
||||
} else {
|
||||
// sign out
|
||||
authManager.signOut();
|
||||
syncManager.destroyLocalData(function(){
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
} else {
|
||||
authManager.login(server, email, pw, false, function(response){
|
||||
authManager.login(server, email, pw, false, {}, function(response){
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
@@ -2,7 +2,7 @@ class LockScreen {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/lock-screen.html";
|
||||
this.templateUrl = "lock-screen.html";
|
||||
this.scope = {
|
||||
onSuccess: "&",
|
||||
};
|
||||
@@ -27,4 +27,4 @@ class LockScreen {
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('lockScreen', () => new LockScreen);
|
||||
angular.module('app').directive('lockScreen', () => new LockScreen);
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('app.frontend')
|
||||
angular.module('app')
|
||||
.directive("notesSection", function(){
|
||||
return {
|
||||
scope: {
|
||||
@@ -7,7 +7,7 @@ angular.module('app.frontend')
|
||||
tag: "="
|
||||
},
|
||||
|
||||
templateUrl: 'frontend/notes.html',
|
||||
templateUrl: 'notes.html',
|
||||
replace: true,
|
||||
controller: 'NotesCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
@@ -33,49 +33,109 @@ angular.module('app.frontend')
|
||||
})
|
||||
.controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager, storageManager) {
|
||||
|
||||
this.sortBy = storageManager.getItem("sortBy") || "created_at";
|
||||
this.showArchived = storageManager.getBooleanValue("showArchived") || false;
|
||||
this.sortDescending = this.sortBy != "title";
|
||||
this.panelController = {};
|
||||
|
||||
$rootScope.$on("user-preferences-changed", () => {
|
||||
this.loadPreferences();
|
||||
});
|
||||
|
||||
this.loadPreferences = function() {
|
||||
let prevSortValue = this.sortBy;
|
||||
this.sortBy = authManager.getUserPrefValue("sortBy", "created_at");
|
||||
if(prevSortValue && prevSortValue != this.sortBy) {
|
||||
$timeout(() => {
|
||||
this.selectFirstNote();
|
||||
})
|
||||
}
|
||||
this.sortDescending = this.sortBy != "title";
|
||||
|
||||
this.showArchived = authManager.getUserPrefValue("showArchived", false);
|
||||
this.hidePinned = authManager.getUserPrefValue("hidePinned", false);
|
||||
this.hideNotePreview = authManager.getUserPrefValue("hideNotePreview", false);
|
||||
this.hideDate = authManager.getUserPrefValue("hideDate", false);
|
||||
this.hideTags = authManager.getUserPrefValue("hideTags", false);
|
||||
|
||||
let width = authManager.getUserPrefValue("notesPanelWidth");
|
||||
if(width) {
|
||||
this.panelController.setWidth(width);
|
||||
}
|
||||
}
|
||||
|
||||
this.loadPreferences();
|
||||
|
||||
this.onPanelResize = function(newWidth) {
|
||||
authManager.setUserPrefValue("notesPanelWidth", newWidth);
|
||||
authManager.syncUserPreferences();
|
||||
}
|
||||
|
||||
angular.element(document).ready(() => {
|
||||
this.loadPreferences();
|
||||
});
|
||||
|
||||
$rootScope.$on("editorFocused", function(){
|
||||
this.showMenu = false;
|
||||
}.bind(this))
|
||||
|
||||
$rootScope.$on("noteDeleted", function() {
|
||||
this.selectFirstNote(false);
|
||||
$timeout(this.onNoteRemoval.bind(this));
|
||||
}.bind(this))
|
||||
|
||||
$rootScope.$on("noteArchived", function() {
|
||||
this.selectFirstNote(false);
|
||||
}.bind(this))
|
||||
$timeout(this.onNoteRemoval.bind(this));
|
||||
}.bind(this));
|
||||
|
||||
this.DefaultNotesToDisplayValue = 20;
|
||||
|
||||
// When a note is removed from the list
|
||||
this.onNoteRemoval = function() {
|
||||
let visibleNotes = this.visibleNotes();
|
||||
if(this.selectedIndex < visibleNotes.length) {
|
||||
this.selectNote(visibleNotes[this.selectedIndex]);
|
||||
} else {
|
||||
this.selectNote(visibleNotes[visibleNotes.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
let MinNoteHeight = 51.0; // This is the height of a note cell with nothing but the title, which *is* a display option
|
||||
this.DefaultNotesToDisplayValue = (document.documentElement.clientHeight / MinNoteHeight) || 20;
|
||||
|
||||
this.notesToDisplay = this.DefaultNotesToDisplayValue;
|
||||
this.paginate = function() {
|
||||
this.notesToDisplay += this.DefaultNotesToDisplayValue
|
||||
}
|
||||
|
||||
this.panelTitle = function() {
|
||||
if(this.noteFilter.text.length) {
|
||||
return `${this.tag.notes.filter((i) => {return i.visible;}).length} search results`;
|
||||
} else if(this.tag) {
|
||||
return `${this.tag.title} notes`;
|
||||
}
|
||||
}
|
||||
|
||||
this.optionsSubtitle = function() {
|
||||
var base = "Sorting by";
|
||||
var base = "";
|
||||
if(this.sortBy == "created_at") {
|
||||
base += " date added";
|
||||
base += " Date Added";
|
||||
} else if(this.sortBy == "updated_at") {
|
||||
base += " date modifed";
|
||||
base += " Date Modifed";
|
||||
} else if(this.sortBy == "title") {
|
||||
base += " title";
|
||||
base += " Title";
|
||||
}
|
||||
|
||||
if(this.showArchived && (!this.tag || !this.tag.archiveTag)) {
|
||||
base += " | Including archived"
|
||||
base += " | + Archived"
|
||||
}
|
||||
|
||||
if(this.hidePinned) {
|
||||
base += " | – Pinned"
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
this.toggleShowArchived = function() {
|
||||
this.showArchived = !this.showArchived;
|
||||
storageManager.setBooleanValue("showArchived", this.showArchived);
|
||||
this.toggleKey = function(key) {
|
||||
this[key] = !this[key];
|
||||
authManager.setUserPrefValue(key, this[key]);
|
||||
authManager.syncUserPreferences();
|
||||
}
|
||||
|
||||
this.tagDidChange = function(tag, oldTag) {
|
||||
@@ -108,10 +168,14 @@ angular.module('app.frontend')
|
||||
this.selectFirstNote(createNew);
|
||||
}
|
||||
|
||||
this.selectFirstNote = function(createNew) {
|
||||
var visibleNotes = this.sortedNotes.filter(function(note){
|
||||
this.visibleNotes = function() {
|
||||
return this.sortedNotes.filter(function(note){
|
||||
return note.visible;
|
||||
});
|
||||
}
|
||||
|
||||
this.selectFirstNote = function(createNew) {
|
||||
var visibleNotes = this.visibleNotes();
|
||||
|
||||
if(visibleNotes.length > 0) {
|
||||
this.selectNote(visibleNotes[0]);
|
||||
@@ -121,9 +185,11 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
this.selectNote = function(note) {
|
||||
if(!note) { return; }
|
||||
this.selectedNote = note;
|
||||
note.conflict_of = null; // clear conflict
|
||||
this.selectionMade()(note);
|
||||
this.selectedIndex = this.visibleNotes().indexOf(note);
|
||||
}
|
||||
|
||||
this.createNewNote = function() {
|
||||
@@ -137,12 +203,7 @@ angular.module('app.frontend')
|
||||
this.noteFilter = {text : ''};
|
||||
|
||||
this.filterNotes = function(note) {
|
||||
if(this.tag.archiveTag) {
|
||||
note.visible = note.archived;
|
||||
return note.visible;
|
||||
}
|
||||
|
||||
if(note.archived && !this.showArchived) {
|
||||
if((note.archived && !this.showArchived && !this.tag.archiveTag) || (note.pinned && this.hidePinned)) {
|
||||
note.visible = false;
|
||||
return note.visible;
|
||||
}
|
||||
@@ -156,6 +217,11 @@ angular.module('app.frontend')
|
||||
var matchesBody = words.every(function(word) { return note.safeText().toLowerCase().indexOf(word) >= 0; });
|
||||
note.visible = matchesTitle || matchesBody;
|
||||
}
|
||||
|
||||
if(this.tag.archiveTag) {
|
||||
note.visible = note.visible && note.archived;
|
||||
}
|
||||
|
||||
return note.visible;
|
||||
}.bind(this)
|
||||
|
||||
@@ -188,7 +254,21 @@ angular.module('app.frontend')
|
||||
|
||||
this.setSortBy = function(type) {
|
||||
this.sortBy = type;
|
||||
storageManager.setItem("sortBy", type);
|
||||
authManager.setUserPrefValue("sortBy", this.sortBy);
|
||||
authManager.syncUserPreferences();
|
||||
}
|
||||
|
||||
this.shouldShowTags = function(note) {
|
||||
if(this.hideTags) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(this.tag.all) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Inside a tag, only show tags string if note contains tags other than this.tag
|
||||
return note.tags && note.tags.length > 1;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('app.frontend')
|
||||
angular.module('app')
|
||||
.directive("tagsSection", function(){
|
||||
return {
|
||||
restrict: 'E',
|
||||
@@ -13,7 +13,7 @@ angular.module('app.frontend')
|
||||
updateNoteTag: "&",
|
||||
removeTag: "&"
|
||||
},
|
||||
templateUrl: 'frontend/tags.html',
|
||||
templateUrl: 'tags.html',
|
||||
replace: true,
|
||||
controller: 'TagsCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
@@ -34,26 +34,37 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('TagsCtrl', function ($rootScope, modelManager, $timeout, componentManager) {
|
||||
.controller('TagsCtrl', function ($rootScope, modelManager, $timeout, componentManager, authManager) {
|
||||
|
||||
var initialLoad = true;
|
||||
|
||||
this.panelController = {};
|
||||
|
||||
$rootScope.$on("user-preferences-changed", () => {
|
||||
this.loadPreferences();
|
||||
});
|
||||
|
||||
this.loadPreferences = function() {
|
||||
let width = authManager.getUserPrefValue("tagsPanelWidth");
|
||||
if(width) {
|
||||
this.panelController.setWidth(width);
|
||||
}
|
||||
}
|
||||
|
||||
this.loadPreferences();
|
||||
|
||||
this.onPanelResize = function(newWidth) {
|
||||
authManager.setUserPrefValue("tagsPanelWidth", newWidth);
|
||||
authManager.syncUserPreferences();
|
||||
}
|
||||
|
||||
this.componentManager = componentManager;
|
||||
|
||||
componentManager.registerHandler({identifier: "tags", areas: ["tags-list"], activationHandler: function(component){
|
||||
this.component = component;
|
||||
|
||||
if(component.active) {
|
||||
$timeout(function(){
|
||||
var iframe = document.getElementById("tags-list-iframe");
|
||||
iframe.onload = function() {
|
||||
componentManager.registerComponentWindow(this.component, iframe.contentWindow);
|
||||
}.bind(this);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
}.bind(this), contextRequestHandler: function(component){
|
||||
return null;
|
||||
}.bind(this), actionHandler: function(component, action, data){
|
||||
|
||||
if(action === "select-item") {
|
||||
var tag = modelManager.findItem(data.item.uuid);
|
||||
if(tag) {
|
||||
@@ -64,7 +75,6 @@ angular.module('app.frontend')
|
||||
else if(action === "clear-selection") {
|
||||
this.selectTag(this.allTag);
|
||||
}
|
||||
|
||||
}.bind(this)});
|
||||
|
||||
this.setAllTag = function(allTag) {
|
||||
@@ -1,6 +1,6 @@
|
||||
angular
|
||||
.module('app.frontend')
|
||||
.directive('mbAutofocus', ['$timeout', function($timeout) {
|
||||
.module('app')
|
||||
.directive('snAutofocus', ['$timeout', function($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
@@ -1,5 +1,5 @@
|
||||
angular
|
||||
.module('app.frontend')
|
||||
.module('app')
|
||||
.directive('clickOutside', ['$document', function($document) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
@@ -1,5 +1,5 @@
|
||||
angular
|
||||
.module('app.frontend')
|
||||
.module('app')
|
||||
.directive('delayHide', function($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
@@ -1,5 +1,5 @@
|
||||
angular
|
||||
.module('app.frontend')
|
||||
.module('app')
|
||||
.directive('fileChange', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('app.frontend').directive('infiniteScroll', [
|
||||
angular.module('app').directive('infiniteScroll', [
|
||||
'$rootScope', '$window', '$timeout', function($rootScope, $window, $timeout) {
|
||||
return {
|
||||
link: function(scope, elem, attrs) {
|
||||
@@ -1,5 +1,5 @@
|
||||
angular
|
||||
.module('app.frontend')
|
||||
.module('app')
|
||||
.directive('lowercase', function() {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
@@ -1,5 +1,5 @@
|
||||
angular
|
||||
.module('app.frontend')
|
||||
.module('app')
|
||||
.directive('selectOnClick', ['$window', function ($window) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
@@ -2,9 +2,10 @@ class AccountMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/account-menu.html";
|
||||
this.templateUrl = "directives/account-menu.html";
|
||||
this.scope = {
|
||||
"onSuccessfulAuth" : "&"
|
||||
"onSuccessfulAuth" : "&",
|
||||
"closeFunction" : "&"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,28 +16,17 @@ class AccountMenu {
|
||||
$scope.user = authManager.user;
|
||||
$scope.server = syncManager.serverURL;
|
||||
|
||||
$scope.close = function() {
|
||||
$timeout(() => {
|
||||
$scope.closeFunction()();
|
||||
})
|
||||
}
|
||||
|
||||
$scope.encryptedBackupsAvailable = function() {
|
||||
return authManager.user || passcodeManager.hasPasscode();
|
||||
}
|
||||
|
||||
$scope.syncStatus = syncManager.syncStatus;
|
||||
|
||||
$scope.encryptionKey = function() {
|
||||
return authManager.keys().mk;
|
||||
}
|
||||
|
||||
$scope.authKey = function() {
|
||||
return authManager.keys().ak;
|
||||
}
|
||||
|
||||
$scope.serverPassword = function() {
|
||||
return syncManager.serverPassword;
|
||||
}
|
||||
|
||||
$scope.dashboardURL = function() {
|
||||
return `${$scope.server}/dashboard/#server=${$scope.server}&id=${encodeURIComponent($scope.user.email)}&pw=${$scope.serverPassword()}`;
|
||||
}
|
||||
|
||||
$scope.newPasswordData = {};
|
||||
|
||||
$scope.showPasswordChangeForm = function() {
|
||||
@@ -45,7 +35,13 @@ class AccountMenu {
|
||||
|
||||
$scope.submitPasswordChange = function() {
|
||||
|
||||
if($scope.newPasswordData.newPassword != $scope.newPasswordData.newPasswordConfirmation) {
|
||||
let newPass = $scope.newPasswordData.newPassword;
|
||||
|
||||
if(!newPass || newPass.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(newPass != $scope.newPasswordData.newPasswordConfirmation) {
|
||||
alert("Your new password does not match its confirmation.");
|
||||
$scope.newPasswordData.status = null;
|
||||
return;
|
||||
@@ -63,7 +59,7 @@ class AccountMenu {
|
||||
|
||||
// perform a sync beforehand to pull in any last minutes changes before we change the encryption key (and thus cant decrypt new changes)
|
||||
syncManager.sync(function(response){
|
||||
authManager.changePassword(email, $scope.newPasswordData.newPassword, function(response){
|
||||
authManager.changePassword(email, newPass, function(response){
|
||||
if(response.error) {
|
||||
alert("There was an error changing your password. Please try again.");
|
||||
$scope.newPasswordData.status = null;
|
||||
@@ -86,10 +82,19 @@ class AccountMenu {
|
||||
}, 1000)
|
||||
});
|
||||
})
|
||||
})
|
||||
}, null, "submitPasswordChange")
|
||||
}
|
||||
|
||||
$scope.submitMfaForm = function() {
|
||||
var params = {};
|
||||
params[$scope.formData.mfa.payload.mfa_key] = $scope.formData.userMfaCode;
|
||||
$scope.login(params);
|
||||
}
|
||||
|
||||
$scope.submitAuthForm = function() {
|
||||
if(!$scope.formData.email || !$scope.formData.user_password) {
|
||||
return;
|
||||
}
|
||||
if($scope.formData.showLogin) {
|
||||
$scope.login();
|
||||
} else {
|
||||
@@ -97,19 +102,37 @@ class AccountMenu {
|
||||
}
|
||||
}
|
||||
|
||||
$scope.login = function() {
|
||||
$scope.login = function(extraParams) {
|
||||
$scope.formData.status = "Generating Login Keys...";
|
||||
$timeout(function(){
|
||||
authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, $scope.formData.ephemeral, function(response){
|
||||
if(!response || response.error) {
|
||||
$scope.formData.status = null;
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
if(!response || (response && !response.didDisplayAlert)) {
|
||||
alert(error.message);
|
||||
authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, $scope.formData.ephemeral, extraParams,
|
||||
(response) => {
|
||||
if(!response || response.error) {
|
||||
$scope.formData.status = null;
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
|
||||
// MFA Error
|
||||
if(error.tag == "mfa-required" || error.tag == "mfa-invalid") {
|
||||
$timeout(() => {
|
||||
$scope.formData.showLogin = false;
|
||||
$scope.formData.mfa = error;
|
||||
})
|
||||
}
|
||||
|
||||
// General Error
|
||||
else {
|
||||
$timeout(() => {
|
||||
$scope.formData.showLogin = true;
|
||||
$scope.formData.mfa = null;
|
||||
})
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Success
|
||||
else {
|
||||
$scope.onAuthSuccess();
|
||||
}
|
||||
} else {
|
||||
$scope.onAuthSuccess();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
@@ -150,7 +173,7 @@ class AccountMenu {
|
||||
$timeout(function(){
|
||||
$scope.onSuccessfulAuth()();
|
||||
syncManager.refreshErroredItems();
|
||||
syncManager.sync();
|
||||
syncManager.sync("onAuthSuccess");
|
||||
})
|
||||
}
|
||||
|
||||
@@ -159,7 +182,6 @@ class AccountMenu {
|
||||
$rootScope.$broadcast("major-data-change");
|
||||
$scope.clearDatabaseAndRewriteAllItems(true, block);
|
||||
}
|
||||
|
||||
else {
|
||||
modelManager.resetLocalMemory();
|
||||
storageManager.clearAllModels(function(){
|
||||
@@ -268,7 +290,7 @@ class AccountMenu {
|
||||
|
||||
syncManager.sync((response) => {
|
||||
callback(response, errorCount);
|
||||
}, {additionalFields: ["created_at", "updated_at"]});
|
||||
}, {additionalFields: ["created_at", "updated_at"]}, "importJSONData");
|
||||
}.bind(this)
|
||||
|
||||
if(data.auth_params) {
|
||||
@@ -431,7 +453,7 @@ class AccountMenu {
|
||||
alert("Your items have been successfully re-encrypted and synced. You must sign out of all other signed in applications (mobile, desktop, web) and sign in again, or else you may corrupt your data.")
|
||||
$scope.newPasswordData = {};
|
||||
}, 1000)
|
||||
});
|
||||
}, null, "reencryptPressed");
|
||||
|
||||
}
|
||||
|
||||
@@ -499,9 +521,9 @@ class AccountMenu {
|
||||
|
||||
$scope.encryptionStatusString = function() {
|
||||
if(!authManager.offline()) {
|
||||
return "End-to-end encryption is enabled. Your data is encrypted before being synced to your private account.";
|
||||
return "End-to-end encryption is enabled. Your data is encrypted before syncing to your private account.";
|
||||
} else if(passcodeManager.hasPasscode()) {
|
||||
return "Encryption is enabled. Your data is encrypted using your passcode before being stored on disk.";
|
||||
return "Encryption is enabled. Your data is encrypted using your passcode before saving to your device storage.";
|
||||
} else {
|
||||
return "Encryption is not enabled. Sign in, register, or add a passcode lock to enable encryption.";
|
||||
}
|
||||
@@ -511,11 +533,6 @@ class AccountMenu {
|
||||
Passcode Lock
|
||||
*/
|
||||
|
||||
$scope.passcodeOptionAvailable = function() {
|
||||
// If you're signed in with an ephemeral session, passcode lock is unavailable
|
||||
return authManager.offline() || !authManager.isEphemeralSession();
|
||||
}
|
||||
|
||||
$scope.hasPasscode = function() {
|
||||
return passcodeManager.hasPasscode();
|
||||
}
|
||||
@@ -531,18 +548,13 @@ class AccountMenu {
|
||||
return;
|
||||
}
|
||||
|
||||
passcodeManager.setPasscode(passcode, () => {
|
||||
let fn = $scope.formData.changingPasscode ? passcodeManager.changePasscode : passcodeManager.setPasscode;
|
||||
|
||||
fn(passcode, () => {
|
||||
$timeout(function(){
|
||||
$scope.formData.showPasscodeForm = false;
|
||||
var offline = authManager.offline();
|
||||
|
||||
// Allow UI to update before showing alert
|
||||
setTimeout(function () {
|
||||
var message = "You've succesfully set an app passcode.";
|
||||
if(offline) { message += " Your items will now be encrypted using this passcode."; }
|
||||
alert(message);
|
||||
}, 10);
|
||||
|
||||
if(offline) {
|
||||
// Allows desktop to make backup file
|
||||
$rootScope.$broadcast("major-data-change");
|
||||
@@ -552,6 +564,12 @@ class AccountMenu {
|
||||
})
|
||||
}
|
||||
|
||||
$scope.changePasscodePressed = function() {
|
||||
$scope.formData.changingPasscode = true;
|
||||
$scope.addPasscodeClicked();
|
||||
$scope.formData.changingPasscode = false;
|
||||
}
|
||||
|
||||
$scope.removePasscodePressed = function() {
|
||||
var signedIn = !authManager.offline();
|
||||
var message = "Are you sure you want to remove your local passcode?";
|
||||
@@ -577,4 +595,4 @@ class AccountMenu {
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('accountMenu', () => new AccountMenu);
|
||||
angular.module('app').directive('accountMenu', () => new AccountMenu);
|
||||
86
app/assets/javascripts/app/directives/views/actionsMenu.js
Normal file
86
app/assets/javascripts/app/directives/views/actionsMenu.js
Normal file
@@ -0,0 +1,86 @@
|
||||
class ActionsMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/actions-menu.html";
|
||||
this.scope = {
|
||||
item: "="
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, modelManager, actionsManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.renderData = {};
|
||||
|
||||
$scope.extensions = actionsManager.extensions.sort((a, b) => {return a.name.toLowerCase() > b.name.toLowerCase()});
|
||||
|
||||
for(let ext of $scope.extensions) {
|
||||
ext.loading = true;
|
||||
actionsManager.loadExtensionInContextOfItem(ext, $scope.item, function(scopedExtension) {
|
||||
ext.loading = false;
|
||||
})
|
||||
}
|
||||
|
||||
$scope.executeAction = function(action, extension, parentAction) {
|
||||
if(action.verb == "nested") {
|
||||
if(!action.subrows) {
|
||||
action.subrows = $scope.subRowsForAction(action, extension);
|
||||
} else {
|
||||
action.subrows = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
action.running = true;
|
||||
actionsManager.executeAction(action, extension, $scope.item, function(response){
|
||||
action.running = false;
|
||||
$scope.handleActionResponse(action, response);
|
||||
|
||||
// reload extension actions
|
||||
actionsManager.loadExtensionInContextOfItem(extension, $scope.item, function(ext){
|
||||
// keep nested state
|
||||
if(parentAction) {
|
||||
var matchingAction = _.find(ext.actions, {label: parentAction.label});
|
||||
matchingAction.subrows = $scope.subRowsForAction(parentAction, extension);
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
$scope.handleActionResponse = function(action, response) {
|
||||
switch (action.verb) {
|
||||
case "render": {
|
||||
var item = response.item;
|
||||
if(item.content_type == "Note") {
|
||||
$scope.renderData.title = item.title;
|
||||
$scope.renderData.text = item.text;
|
||||
$scope.renderData.showRenderModal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$scope.subRowsForAction = function(parentAction, extension) {
|
||||
if(!parentAction.subactions) {
|
||||
return null;
|
||||
}
|
||||
return parentAction.subactions.map((subaction) => {
|
||||
return {
|
||||
onClick: ($event) => {
|
||||
this.executeAction(subaction, extension, parentAction);
|
||||
$event.stopPropagation();
|
||||
},
|
||||
title: subaction.label,
|
||||
subtitle: subaction.desc,
|
||||
spinnerClass: subaction.running ? 'info' : null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('actionsMenu', () => new ActionsMenu);
|
||||
@@ -0,0 +1,31 @@
|
||||
class ComponentModal {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/component-modal.html";
|
||||
this.scope = {
|
||||
show: "=",
|
||||
component: "=",
|
||||
callback: "=",
|
||||
onDismiss: "&"
|
||||
};
|
||||
}
|
||||
|
||||
link($scope, el, attrs) {
|
||||
$scope.el = el;
|
||||
}
|
||||
|
||||
controller($scope, $timeout, componentManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.dismiss = function(callback) {
|
||||
$scope.el.remove();
|
||||
$scope.$destroy();
|
||||
$scope.onDismiss && $scope.onDismiss() && $scope.onDismiss()($scope.component);
|
||||
callback && callback();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('componentModal', () => new ComponentModal);
|
||||
130
app/assets/javascripts/app/directives/views/componentView.js
Normal file
130
app/assets/javascripts/app/directives/views/componentView.js
Normal file
@@ -0,0 +1,130 @@
|
||||
class ComponentView {
|
||||
|
||||
constructor(componentManager, desktopManager, $timeout) {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/component-view.html";
|
||||
this.scope = {
|
||||
component: "=",
|
||||
manualDealloc: "="
|
||||
};
|
||||
|
||||
this.componentManager = componentManager;
|
||||
this.desktopManager = desktopManager;
|
||||
this.timeout = $timeout;
|
||||
}
|
||||
|
||||
link($scope, el, attrs, ctrl) {
|
||||
$scope.el = el;
|
||||
|
||||
$scope.identifier = "component-view-" + Math.random();
|
||||
|
||||
// console.log("Registering handler", $scope.identifier, $scope.component.name);
|
||||
|
||||
this.componentManager.registerHandler({identifier: $scope.identifier, areas: [$scope.component.area], activationHandler: (component) => {
|
||||
if(component.active) {
|
||||
this.timeout(() => {
|
||||
var iframe = this.componentManager.iframeForComponent(component);
|
||||
if(iframe) {
|
||||
iframe.onload = function() {
|
||||
this.componentManager.registerComponentWindow(component, iframe.contentWindow);
|
||||
}.bind(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
actionHandler: (component, action, data) => {
|
||||
if(action == "set-size") {
|
||||
this.componentManager.handleSetSizeEvent(component, data);
|
||||
}
|
||||
}});
|
||||
|
||||
$scope.updateObserver = this.desktopManager.registerUpdateObserver((component) => {
|
||||
if(component == $scope.component && component.active) {
|
||||
$scope.reloadComponent();
|
||||
}
|
||||
})
|
||||
|
||||
$scope.$watch('component', function(component, prevComponent){
|
||||
ctrl.componentValueChanging(component, prevComponent);
|
||||
});
|
||||
}
|
||||
|
||||
controller($scope, $timeout, componentManager, desktopManager) {
|
||||
'ngInject';
|
||||
|
||||
this.componentValueChanging = (component, prevComponent) => {
|
||||
if(prevComponent && component !== prevComponent) {
|
||||
// Deactive old component
|
||||
componentManager.deactivateComponent(prevComponent);
|
||||
}
|
||||
|
||||
if(component) {
|
||||
componentManager.activateComponent(component);
|
||||
console.log("Loading", $scope.component.name, $scope.getUrl(), component.valid_until);
|
||||
|
||||
$scope.reloadStatus();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.reloadComponent = function() {
|
||||
console.log("Reloading component", $scope.component);
|
||||
componentManager.deactivateComponent($scope.component);
|
||||
$timeout(() => {
|
||||
componentManager.activateComponent($scope.component);
|
||||
})
|
||||
}
|
||||
|
||||
$scope.reloadStatus = function() {
|
||||
let component = $scope.component;
|
||||
$scope.reloading = true;
|
||||
let previouslyValid = $scope.componentValid;
|
||||
|
||||
var expired, offlineRestricted, urlError;
|
||||
|
||||
offlineRestricted = component.offlineOnly && !isDesktopApplication();
|
||||
|
||||
urlError =
|
||||
(!isDesktopApplication() && (!component.url && !component.hosted_url))
|
||||
||
|
||||
(isDesktopApplication() && (!component.local_url && !component.url && !component.hosted_url))
|
||||
|
||||
expired = component.valid_until && component.valid_until <= new Date();
|
||||
|
||||
$scope.componentValid = !offlineRestricted && !urlError && !expired;
|
||||
|
||||
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) {
|
||||
if($scope.componentValid) {
|
||||
componentManager.activateComponent(component);
|
||||
}
|
||||
}
|
||||
|
||||
$timeout(() => {
|
||||
$scope.reloading = false;
|
||||
}, 500)
|
||||
}
|
||||
|
||||
$scope.getUrl = function() {
|
||||
var url = componentManager.urlForComponent($scope.component);
|
||||
$scope.component.runningLocally = (url !== $scope.component.url) && url !== ($scope.component.hosted_url);
|
||||
return url;
|
||||
}
|
||||
|
||||
$scope.$on("$destroy", function() {
|
||||
// console.log("Deregistering handler", $scope.identifier, $scope.component.name);
|
||||
componentManager.deregisterHandler($scope.identifier);
|
||||
if($scope.component && !$scope.manualDealloc) {
|
||||
componentManager.deactivateComponent($scope.component);
|
||||
}
|
||||
|
||||
desktopManager.deregisterUpdateObserver($scope.updateObserver);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('componentView', (componentManager, desktopManager, $timeout) => new ComponentView(componentManager, desktopManager, $timeout));
|
||||
91
app/assets/javascripts/app/directives/views/editorMenu.js
Normal file
91
app/assets/javascripts/app/directives/views/editorMenu.js
Normal file
@@ -0,0 +1,91 @@
|
||||
class EditorMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/editor-menu.html";
|
||||
this.scope = {
|
||||
callback: "&",
|
||||
selectedEditor: "=",
|
||||
currentItem: "="
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, componentManager, syncManager, $timeout) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {};
|
||||
|
||||
$scope.editors = componentManager.componentsForArea("editor-editor").sort((a, b) => {return a.name.toLowerCase() > b.name.toLowerCase()});
|
||||
$scope.stack = componentManager.componentsForArea("editor-stack").sort((a, b) => {return a.name.toLowerCase() > b.name.toLowerCase()});
|
||||
|
||||
$scope.isDesktop = isDesktopApplication();
|
||||
|
||||
$scope.defaultEditor = $scope.editors.filter((e) => {return e.isDefaultEditor()})[0];
|
||||
|
||||
$scope.selectComponent = function($event, component) {
|
||||
$event.stopPropagation();
|
||||
if(component) {
|
||||
component.conflict_of = null; // clear conflict if applicable
|
||||
}
|
||||
$timeout(() => {
|
||||
$scope.callback()(component);
|
||||
})
|
||||
}
|
||||
|
||||
$scope.toggleDefaultForEditor = function(editor) {
|
||||
if($scope.defaultEditor == editor) {
|
||||
$scope.removeEditorDefault(editor);
|
||||
} else {
|
||||
$scope.makeEditorDefault(editor);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.offlineAvailableForComponent = function(component) {
|
||||
return component.local_url && isDesktopApplication();
|
||||
}
|
||||
|
||||
$scope.makeEditorDefault = function(component) {
|
||||
var currentDefault = componentManager.componentsForArea("editor-editor").filter((e) => {return e.isDefaultEditor()})[0];
|
||||
if(currentDefault) {
|
||||
currentDefault.setAppDataItem("defaultEditor", false);
|
||||
currentDefault.setDirty(true);
|
||||
}
|
||||
|
||||
component.setAppDataItem("defaultEditor", true);
|
||||
component.setDirty(true);
|
||||
syncManager.sync("makeEditorDefault");
|
||||
|
||||
$scope.defaultEditor = component;
|
||||
}
|
||||
|
||||
$scope.removeEditorDefault = function(component) {
|
||||
component.setAppDataItem("defaultEditor", false);
|
||||
component.setDirty(true);
|
||||
syncManager.sync("removeEditorDefault");
|
||||
|
||||
$scope.defaultEditor = null;
|
||||
}
|
||||
|
||||
$scope.shouldDisplayRunningLocallyLabel = function(component) {
|
||||
if(!component.runningLocally) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('editorMenu', () => new EditorMenu);
|
||||
32
app/assets/javascripts/app/directives/views/menuRow.js
Normal file
32
app/assets/javascripts/app/directives/views/menuRow.js
Normal file
@@ -0,0 +1,32 @@
|
||||
class MenuRow {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.transclude = true;
|
||||
this.templateUrl = "directives/menu-row.html";
|
||||
this.scope = {
|
||||
circle: "=",
|
||||
title: "=",
|
||||
subtite: "=",
|
||||
hasButton: "=",
|
||||
buttonText: "=",
|
||||
buttonClass: "=",
|
||||
buttonAction: "&",
|
||||
spinnerClass: "=",
|
||||
subRows: "=",
|
||||
faded: "="
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, componentManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.clickButton = function($event) {
|
||||
$event.stopPropagation();
|
||||
$scope.buttonAction();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app').directive('menuRow', () => new MenuRow);
|
||||
246
app/assets/javascripts/app/directives/views/panelResizer.js
Normal file
246
app/assets/javascripts/app/directives/views/panelResizer.js
Normal file
@@ -0,0 +1,246 @@
|
||||
class PanelResizer {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/panel-resizer.html";
|
||||
this.scope = {
|
||||
index: "=",
|
||||
panelId: "=",
|
||||
onResize: "&",
|
||||
onResizeFinish: "&",
|
||||
control: "=",
|
||||
alwaysVisible: "=",
|
||||
minWidth: "=",
|
||||
property: "=",
|
||||
hoverable: "=",
|
||||
collapsable: "="
|
||||
};
|
||||
}
|
||||
|
||||
link(scope, elem, attrs, ctrl) {
|
||||
scope.elem = elem;
|
||||
|
||||
scope.control.setWidth = function(value) {
|
||||
scope.setWidth(value, true);
|
||||
}
|
||||
|
||||
scope.control.setLeft = function(value) {
|
||||
scope.setLeft(value);
|
||||
}
|
||||
}
|
||||
|
||||
controller($scope, $element, modelManager, actionsManager, $timeout) {
|
||||
'ngInject';
|
||||
|
||||
let panel = document.getElementById($scope.panelId);
|
||||
if(!panel) {
|
||||
console.log("Panel not found for", $scope.panelId);
|
||||
}
|
||||
|
||||
let resizerColumn = $element[0];
|
||||
let resizerWidth = resizerColumn.offsetWidth;
|
||||
let minWidth = $scope.minWidth || resizerWidth;
|
||||
var pressed = false;
|
||||
var startWidth = panel.scrollWidth, startX = 0, lastDownX = 0, collapsed, lastWidth = startWidth, startLeft, lastLeft;
|
||||
var appFrame;
|
||||
|
||||
function getParentRect() {
|
||||
return panel.parentNode.getBoundingClientRect();
|
||||
}
|
||||
|
||||
if($scope.property == "right") {
|
||||
let handleReize = debounce((event) => {
|
||||
reloadDefaultValues();
|
||||
handleWidthEvent();
|
||||
$timeout(() => { $scope.finishSettingWidth(); })
|
||||
}, 250);
|
||||
|
||||
window.addEventListener('resize', handleReize);
|
||||
|
||||
$scope.$on("$destroy", function() {
|
||||
window.removeEventListener('resize', handleReize);
|
||||
});
|
||||
}
|
||||
|
||||
function reloadDefaultValues() {
|
||||
startWidth = panel.scrollWidth;
|
||||
appFrame = document.getElementById("app").getBoundingClientRect();
|
||||
}
|
||||
reloadDefaultValues();
|
||||
|
||||
if($scope.alwaysVisible) {
|
||||
resizerColumn.classList.add("always-visible");
|
||||
}
|
||||
|
||||
if($scope.hoverable) {
|
||||
resizerColumn.classList.add("hoverable");
|
||||
}
|
||||
|
||||
$scope.setWidth = function(width, finish) {
|
||||
if(width < minWidth) {
|
||||
width = minWidth;
|
||||
}
|
||||
|
||||
let parentRect = getParentRect();
|
||||
|
||||
if(width > parentRect.width) {
|
||||
width = parentRect.width;
|
||||
}
|
||||
|
||||
let maxWidth = appFrame.width - panel.getBoundingClientRect().x;
|
||||
if(width > maxWidth) {
|
||||
width = maxWidth;
|
||||
}
|
||||
|
||||
if(width == parentRect.width) {
|
||||
panel.style.width = "100%";
|
||||
panel.style.flexBasis = "100%";
|
||||
} else {
|
||||
panel.style.flexBasis = width + "px";
|
||||
panel.style.width = width + "px";
|
||||
}
|
||||
|
||||
lastWidth = width;
|
||||
|
||||
if(finish) {
|
||||
$scope.finishSettingWidth();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.setLeft = function(left) {
|
||||
panel.style.left = left + "px";
|
||||
lastLeft = left;
|
||||
}
|
||||
|
||||
$scope.finishSettingWidth = function() {
|
||||
if(!$scope.collapsable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(lastWidth <= minWidth) {
|
||||
collapsed = true;
|
||||
} else {
|
||||
collapsed = false;
|
||||
}
|
||||
if(collapsed) {
|
||||
resizerColumn.classList.add("collapsed");
|
||||
} else {
|
||||
resizerColumn.classList.remove("collapsed");
|
||||
}
|
||||
}
|
||||
|
||||
resizerColumn.addEventListener("mousedown", function(event){
|
||||
pressed = true;
|
||||
lastDownX = event.clientX;
|
||||
startWidth = panel.scrollWidth;
|
||||
startLeft = panel.offsetLeft;
|
||||
panel.classList.add("no-selection");
|
||||
|
||||
if($scope.hoverable) {
|
||||
resizerColumn.classList.add("dragging");
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener("mousemove", function(event){
|
||||
if(!pressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if($scope.property && $scope.property == 'left') {
|
||||
handleLeftEvent(event);
|
||||
} else {
|
||||
handleWidthEvent(event);
|
||||
}
|
||||
})
|
||||
|
||||
function handleWidthEvent(event) {
|
||||
var rect = panel.getBoundingClientRect();
|
||||
var panelMaxX = rect.left + (startWidth || panel.style.maxWidth);
|
||||
|
||||
var x;
|
||||
if(event) {
|
||||
x = event.clientX;
|
||||
} else {
|
||||
// coming from resize event
|
||||
x = 0;
|
||||
lastDownX = 0;
|
||||
}
|
||||
|
||||
let deltaX = x - lastDownX;
|
||||
var newWidth = startWidth + deltaX;
|
||||
|
||||
$scope.setWidth(newWidth, false);
|
||||
|
||||
if($scope.onResize()) {
|
||||
$scope.onResize()(lastWidth, panel);
|
||||
}
|
||||
}
|
||||
|
||||
function handleLeftEvent(event) {
|
||||
var panelRect = panel.getBoundingClientRect();
|
||||
var x = event.clientX || panelRect.x;
|
||||
let deltaX = x - lastDownX;
|
||||
var newLeft = startLeft + deltaX;
|
||||
if(newLeft < 0) {
|
||||
newLeft = 0;
|
||||
deltaX = -startLeft;
|
||||
}
|
||||
|
||||
let parentRect = getParentRect();
|
||||
|
||||
var newWidth = startWidth - deltaX;
|
||||
if(newWidth < minWidth) {
|
||||
newWidth = minWidth;
|
||||
}
|
||||
|
||||
if(newWidth > parentRect.width) {
|
||||
newWidth = parentRect.width;
|
||||
}
|
||||
|
||||
|
||||
if(newLeft + newWidth > parentRect.width) {
|
||||
newLeft = parentRect.width - newWidth;
|
||||
}
|
||||
|
||||
$scope.setLeft(newLeft, false);
|
||||
$scope.setWidth(newWidth, false);
|
||||
}
|
||||
|
||||
document.addEventListener("mouseup", function(event){
|
||||
if(pressed) {
|
||||
pressed = false;
|
||||
resizerColumn.classList.remove("dragging");
|
||||
panel.classList.remove("no-selection");
|
||||
|
||||
let isMaxWidth = lastWidth == getParentRect().width;
|
||||
|
||||
if($scope.onResizeFinish) {
|
||||
$scope.onResizeFinish()(lastWidth, lastLeft, isMaxWidth);
|
||||
}
|
||||
|
||||
$scope.finishSettingWidth();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('panelResizer', () => new PanelResizer);
|
||||
|
||||
/* via https://davidwalsh.name/javascript-debounce-function */
|
||||
function debounce(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this, args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
};
|
||||
100
app/assets/javascripts/app/directives/views/permissionsModal.js
Normal file
100
app/assets/javascripts/app/directives/views/permissionsModal.js
Normal file
@@ -0,0 +1,100 @@
|
||||
class PermissionsModal {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/permissions-modal.html";
|
||||
this.scope = {
|
||||
show: "=",
|
||||
component: "=",
|
||||
permissions: "=",
|
||||
callback: "="
|
||||
};
|
||||
}
|
||||
|
||||
link($scope, el, attrs) {
|
||||
|
||||
$scope.dismiss = function() {
|
||||
el.remove();
|
||||
}
|
||||
|
||||
$scope.accept = function() {
|
||||
$scope.callback(true);
|
||||
$scope.dismiss();
|
||||
}
|
||||
|
||||
$scope.deny = function() {
|
||||
$scope.callback(false);
|
||||
$scope.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
controller($scope, modelManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.permissionsString = function() {
|
||||
var finalString = "";
|
||||
let permissionsCount = $scope.permissions.length;
|
||||
|
||||
let addSeparator = (index, length) => {
|
||||
if(index > 0) {
|
||||
if(index == length - 1) {
|
||||
if(length == 2) {
|
||||
return " and ";
|
||||
} else {
|
||||
return ", and "
|
||||
}
|
||||
} else {
|
||||
return ", ";
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
$scope.permissions.forEach((permission, index) => {
|
||||
|
||||
if(permission.name === "stream-items") {
|
||||
var types = permission.content_types.map(function(type){
|
||||
var desc = modelManager.humanReadableDisplayForContentType(type);
|
||||
if(desc) {
|
||||
return desc + "s";
|
||||
} else {
|
||||
return "items of type " + type;
|
||||
}
|
||||
})
|
||||
var typesString = "";
|
||||
|
||||
for(var i = 0;i < types.length;i++) {
|
||||
var type = types[i];
|
||||
typesString += addSeparator(i, types.length + permissionsCount - index - 1);
|
||||
typesString += type;
|
||||
}
|
||||
|
||||
finalString += addSeparator(index, permissionsCount);
|
||||
|
||||
finalString += typesString;
|
||||
|
||||
if(types.length >= 2 && index < permissionsCount - 1) {
|
||||
// If you have a list of types, and still an additional root-level permission coming up, add a comma
|
||||
finalString += ", ";
|
||||
}
|
||||
} else if(permission.name === "stream-context-item") {
|
||||
var mapping = {
|
||||
"editor-stack" : "working note",
|
||||
"note-tags" : "working note",
|
||||
"editor-editor": "working note"
|
||||
}
|
||||
|
||||
finalString += addSeparator(index, permissionsCount, true);
|
||||
|
||||
finalString += mapping[$scope.component.area];
|
||||
}
|
||||
})
|
||||
|
||||
return finalString + ".";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('permissionsModal', () => new PermissionsModal);
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('app.frontend')
|
||||
angular.module('app')
|
||||
.filter('appDate', function ($filter) {
|
||||
return function (input) {
|
||||
return input ? $filter('date')(new Date(input), 'MM/dd/yyyy', 'UTC') : '';
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('app.frontend')
|
||||
angular.module('app')
|
||||
.filter('sortBy', function ($filter) {
|
||||
return function(items, sortBy) {
|
||||
let sortValueFn = (a, b, pinCheck = false) => {
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('app.frontend').filter('startFrom', function() {
|
||||
angular.module('app').filter('startFrom', function() {
|
||||
return function(input, start) {
|
||||
return input.slice(start);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('app.frontend').filter('trusted', ['$sce', function ($sce) {
|
||||
angular.module('app').filter('trusted', ['$sce', function ($sce) {
|
||||
return function(url) {
|
||||
return $sce.trustAsResourceUrl(url);
|
||||
};
|
||||
@@ -1,125 +0,0 @@
|
||||
class Action {
|
||||
constructor(json) {
|
||||
_.merge(this, json);
|
||||
this.running = false; // in case running=true was synced with server since model is uploaded nondiscriminatory
|
||||
this.error = false;
|
||||
if(this.lastExecuted) {
|
||||
// is string
|
||||
this.lastExecuted = new Date(this.lastExecuted);
|
||||
}
|
||||
}
|
||||
|
||||
permissionsString() {
|
||||
if(!this.permissions) {
|
||||
return "";
|
||||
}
|
||||
|
||||
var permission = this.permissions.charAt(0).toUpperCase() + this.permissions.slice(1); // capitalize first letter
|
||||
permission += ": ";
|
||||
for(var contentType of this.content_types) {
|
||||
if(contentType == "*") {
|
||||
permission += "All items";
|
||||
} else {
|
||||
permission += contentType;
|
||||
}
|
||||
|
||||
permission += " ";
|
||||
}
|
||||
|
||||
return permission;
|
||||
}
|
||||
|
||||
encryptionModeString() {
|
||||
if(this.verb != "post") {
|
||||
return null;
|
||||
}
|
||||
var encryptionMode = "This action accepts data ";
|
||||
if(this.accepts_encrypted && this.accepts_decrypted) {
|
||||
encryptionMode += "encrypted or decrypted.";
|
||||
} else {
|
||||
if(this.accepts_encrypted) {
|
||||
encryptionMode += "encrypted.";
|
||||
} else {
|
||||
encryptionMode += "decrypted.";
|
||||
}
|
||||
}
|
||||
return encryptionMode;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Extension extends Item {
|
||||
constructor(json) {
|
||||
super(json);
|
||||
|
||||
if(this.encrypted === null || this.encrypted === undefined) {
|
||||
// Default to encrypted on creation.
|
||||
this.encrypted = true;
|
||||
}
|
||||
|
||||
if(json.actions) {
|
||||
this.actions = json.actions.map(function(action){
|
||||
return new Action(action);
|
||||
})
|
||||
}
|
||||
|
||||
if(!this.actions) {
|
||||
this.actions = [];
|
||||
}
|
||||
}
|
||||
|
||||
actionsInGlobalContext() {
|
||||
return this.actions.filter(function(action){
|
||||
return action.context == "global";
|
||||
})
|
||||
}
|
||||
|
||||
actionsWithContextForItem(item) {
|
||||
return this.actions.filter(function(action){
|
||||
return action.context == item.content_type || action.context == "Item";
|
||||
})
|
||||
}
|
||||
|
||||
mapContentToLocalProperties(content) {
|
||||
super.mapContentToLocalProperties(content)
|
||||
this.name = content.name;
|
||||
this.description = content.description;
|
||||
this.url = content.url;
|
||||
|
||||
if(content.encrypted !== null && content.encrypted !== undefined) {
|
||||
this.encrypted = content.encrypted;
|
||||
} else {
|
||||
this.encrypted = true;
|
||||
}
|
||||
|
||||
this.supported_types = content.supported_types;
|
||||
if(content.actions) {
|
||||
this.actions = content.actions.map(function(action){
|
||||
return new Action(action);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
referenceParams() {
|
||||
return null;
|
||||
}
|
||||
|
||||
get content_type() {
|
||||
return "Extension";
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
var params = {
|
||||
name: this.name,
|
||||
url: this.url,
|
||||
description: this.description,
|
||||
actions: this.actions,
|
||||
supported_types: this.supported_types,
|
||||
encrypted: this.encrypted
|
||||
};
|
||||
|
||||
_.merge(params, super.structureParams());
|
||||
return params;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,9 +50,17 @@ class Item {
|
||||
|
||||
if(json.content) {
|
||||
this.mapContentToLocalProperties(this.contentObject);
|
||||
} else if(json.deleted == true) {
|
||||
this.handleDeletedContent();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Allows the item to handle the case where the item is deleted and the content is null */
|
||||
handleDeletedContent() {
|
||||
// Subclasses can override
|
||||
}
|
||||
|
||||
setDirty(dirty) {
|
||||
this.dirty = dirty;
|
||||
|
||||
@@ -84,10 +92,10 @@ class Item {
|
||||
}
|
||||
|
||||
mapContentToLocalProperties(contentObj) {
|
||||
this.appData = contentObj.appData;
|
||||
if(!this.appData) {
|
||||
this.appData = {};
|
||||
if(contentObj.appData) {
|
||||
this.appData = contentObj.appData;
|
||||
}
|
||||
if(!this.appData) { this.appData = {}; }
|
||||
}
|
||||
|
||||
createContentJSONFromProperties() {
|
||||
@@ -185,7 +193,27 @@ class Item {
|
||||
return this.getAppDataItem("archived");
|
||||
}
|
||||
|
||||
/*
|
||||
During sync conflicts, when determing whether to create a duplicate for an item, we can omit keys that have no
|
||||
meaningful weight and can be ignored. For example, if one component has active = true and another component has active = false,
|
||||
it would be silly to duplicate them, so instead we ignore this.
|
||||
*/
|
||||
keysToIgnoreWhenCheckingContentEquality() {
|
||||
return [];
|
||||
}
|
||||
|
||||
isItemContentEqualWith(otherItem) {
|
||||
let omit = (obj, keys) => {
|
||||
for(var key of keys) {
|
||||
delete obj[key];
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
var left = omit(this.structureParams(), this.keysToIgnoreWhenCheckingContentEquality());
|
||||
var right = omit(otherItem.structureParams(), otherItem.keysToIgnoreWhenCheckingContentEquality());
|
||||
|
||||
return JSON.stringify(left) === JSON.stringify(right)
|
||||
}
|
||||
|
||||
/*
|
||||
Dates
|
||||
@@ -1,4 +1,4 @@
|
||||
class Theme extends Item {
|
||||
class Mfa extends Item {
|
||||
|
||||
constructor(json_obj) {
|
||||
super(json_obj);
|
||||
@@ -6,18 +6,11 @@ class Theme extends Item {
|
||||
|
||||
mapContentToLocalProperties(content) {
|
||||
super.mapContentToLocalProperties(content)
|
||||
this.url = content.url;
|
||||
this.name = content.name;
|
||||
this.serverContent = content;
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
var params = {
|
||||
url: this.url,
|
||||
name: this.name,
|
||||
};
|
||||
|
||||
_.merge(params, super.structureParams());
|
||||
return params;
|
||||
return _.merge(this.serverContent, super.structureParams());
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
@@ -25,6 +18,11 @@ class Theme extends Item {
|
||||
}
|
||||
|
||||
get content_type() {
|
||||
return "SN|Theme";
|
||||
return "SF|MFA";
|
||||
}
|
||||
|
||||
doNotEncrypt() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
class SyncAdapter extends Item {
|
||||
class ServerExtension extends Item {
|
||||
|
||||
constructor(json_obj) {
|
||||
super(json_obj);
|
||||
@@ -18,13 +18,30 @@ class Component extends Item {
|
||||
|
||||
mapContentToLocalProperties(content) {
|
||||
super.mapContentToLocalProperties(content)
|
||||
this.url = content.url;
|
||||
/* Legacy */
|
||||
this.url = content.url || content.hosted_url;
|
||||
/* New */
|
||||
this.local_url = content.local_url;
|
||||
this.hosted_url = content.hosted_url || content.url;
|
||||
this.offlineOnly = content.offlineOnly;
|
||||
|
||||
if(content.valid_until) {
|
||||
this.valid_until = new Date(content.valid_until);
|
||||
}
|
||||
|
||||
this.name = content.name;
|
||||
this.autoupdateDisabled = content.autoupdateDisabled;
|
||||
|
||||
this.package_info = content.package_info;
|
||||
|
||||
// the location in the view this component is located in. Valid values are currently tags-list, note-tags, and editor-stack`
|
||||
this.area = content.area;
|
||||
|
||||
this.permissions = content.permissions;
|
||||
if(!this.permissions) {
|
||||
this.permissions = [];
|
||||
}
|
||||
|
||||
this.active = content.active;
|
||||
|
||||
// custom data that a component can store in itself
|
||||
@@ -37,13 +54,25 @@ class Component extends Item {
|
||||
this.associatedItemIds = content.associatedItemIds || [];
|
||||
}
|
||||
|
||||
handleDeletedContent() {
|
||||
super.handleDeletedContent();
|
||||
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
var params = {
|
||||
url: this.url,
|
||||
hosted_url: this.hosted_url,
|
||||
local_url: this.local_url,
|
||||
valid_until: this.valid_until,
|
||||
offlineOnly: this.offlineOnly,
|
||||
name: this.name,
|
||||
area: this.area,
|
||||
package_info: this.package_info,
|
||||
permissions: this.permissions,
|
||||
active: this.active,
|
||||
autoupdateDisabled: this.autoupdateDisabled,
|
||||
componentData: this.componentData,
|
||||
disassociatedItemIds: this.disassociatedItemIds,
|
||||
associatedItemIds: this.associatedItemIds,
|
||||
@@ -65,10 +94,26 @@ class Component extends Item {
|
||||
return this.area == "editor-editor";
|
||||
}
|
||||
|
||||
isTheme() {
|
||||
return this.content_type == "SN|Theme" || this.area == "themes";
|
||||
}
|
||||
|
||||
isDefaultEditor() {
|
||||
return this.getAppDataItem("defaultEditor") == true;
|
||||
}
|
||||
|
||||
setLastSize(size) {
|
||||
this.setAppDataItem("lastSize", size);
|
||||
}
|
||||
|
||||
getLastSize() {
|
||||
return this.getAppDataItem("lastSize");
|
||||
}
|
||||
|
||||
keysToIgnoreWhenCheckingContentEquality() {
|
||||
return ["active"].concat(super.keysToIgnoreWhenCheckingContentEquality());
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
An associative component depends on being explicitly activated for a given item, compared to a dissaciative component,
|
||||
@@ -86,11 +131,11 @@ class Component extends Item {
|
||||
this.associatedItemIds.push(item.uuid);
|
||||
}
|
||||
|
||||
isActiveForItem(item) {
|
||||
if(this.isAssociative()) {
|
||||
return this.associatedItemIds.indexOf(item.uuid) !== -1;
|
||||
} else {
|
||||
return this.disassociatedItemIds.indexOf(item.uuid) === -1;
|
||||
}
|
||||
isExplicitlyEnabledForItem(item) {
|
||||
return this.associatedItemIds.indexOf(item.uuid) !== -1;
|
||||
}
|
||||
|
||||
isExplicitlyDisabledForItem(item) {
|
||||
return this.disassociatedItemIds.indexOf(item.uuid) !== -1;
|
||||
}
|
||||
}
|
||||
61
app/assets/javascripts/app/models/app/extension.js
Normal file
61
app/assets/javascripts/app/models/app/extension.js
Normal file
@@ -0,0 +1,61 @@
|
||||
class Action {
|
||||
constructor(json) {
|
||||
_.merge(this, json);
|
||||
this.running = false; // in case running=true was synced with server since model is uploaded nondiscriminatory
|
||||
this.error = false;
|
||||
if(this.lastExecuted) {
|
||||
// is string
|
||||
this.lastExecuted = new Date(this.lastExecuted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Extension extends Component {
|
||||
constructor(json) {
|
||||
super(json);
|
||||
|
||||
if(json.actions) {
|
||||
this.actions = json.actions.map(function(action){
|
||||
return new Action(action);
|
||||
})
|
||||
}
|
||||
|
||||
if(!this.actions) {
|
||||
this.actions = [];
|
||||
}
|
||||
}
|
||||
|
||||
actionsWithContextForItem(item) {
|
||||
return this.actions.filter(function(action){
|
||||
return action.context == item.content_type || action.context == "Item";
|
||||
})
|
||||
}
|
||||
|
||||
mapContentToLocalProperties(content) {
|
||||
super.mapContentToLocalProperties(content)
|
||||
this.description = content.description;
|
||||
|
||||
this.supported_types = content.supported_types;
|
||||
if(content.actions) {
|
||||
this.actions = content.actions.map(function(action){
|
||||
return new Action(action);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get content_type() {
|
||||
return "Extension";
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
var params = {
|
||||
description: this.description,
|
||||
actions: this.actions.map((a) => {return _.omit(a, ["subrows", "subactions"])}),
|
||||
supported_types: this.supported_types
|
||||
};
|
||||
|
||||
_.merge(params, super.structureParams());
|
||||
return params;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -87,13 +87,9 @@ class Tag extends Item {
|
||||
return this.notes;
|
||||
}
|
||||
|
||||
static arrayToDisplayString(tags, includeComma) {
|
||||
return tags.map(function(tag, i){
|
||||
var text = "#" + tag.title;
|
||||
if(i != tags.length - 1) {
|
||||
text += includeComma ? ", " : " ";
|
||||
}
|
||||
return text;
|
||||
static arrayToDisplayString(tags) {
|
||||
return tags.sort((a, b) => {return a.title > b.title}).map(function(tag, i){
|
||||
return "#" + tag.title;
|
||||
}).join(" ");
|
||||
}
|
||||
}
|
||||
12
app/assets/javascripts/app/models/app/theme.js
Normal file
12
app/assets/javascripts/app/models/app/theme.js
Normal file
@@ -0,0 +1,12 @@
|
||||
class Theme extends Component {
|
||||
|
||||
constructor(json_obj) {
|
||||
super(json_obj);
|
||||
|
||||
this.area = "themes";
|
||||
}
|
||||
|
||||
get content_type() {
|
||||
return "SN|Theme";
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,14 @@ class ItemParams {
|
||||
this.version = version || "002";
|
||||
}
|
||||
|
||||
paramsForExportFile() {
|
||||
paramsForExportFile(includeDeleted) {
|
||||
this.additionalFields = ["updated_at"];
|
||||
this.forExportFile = true;
|
||||
return _.omit(this.__params(), ["deleted"]);
|
||||
if(includeDeleted) {
|
||||
return this.__params();
|
||||
} else {
|
||||
return _.omit(this.__params(), ["deleted"]);
|
||||
}
|
||||
}
|
||||
|
||||
paramsForExtension() {
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('app.frontend')
|
||||
angular.module('app')
|
||||
.config(function ($locationProvider) {
|
||||
|
||||
if(!isDesktopApplication()) {
|
||||
@@ -11,5 +11,4 @@ angular.module('app.frontend')
|
||||
} else {
|
||||
$locationProvider.html5Mode(false);
|
||||
}
|
||||
|
||||
});
|
||||
159
app/assets/javascripts/app/services/actionsManager.js
Normal file
159
app/assets/javascripts/app/services/actionsManager.js
Normal file
@@ -0,0 +1,159 @@
|
||||
class ActionsManager {
|
||||
|
||||
constructor(httpManager, modelManager, authManager, syncManager) {
|
||||
this.httpManager = httpManager;
|
||||
this.modelManager = modelManager;
|
||||
this.authManager = authManager;
|
||||
this.syncManager = syncManager;
|
||||
}
|
||||
|
||||
get extensions() {
|
||||
return this.modelManager.extensions;
|
||||
}
|
||||
|
||||
extensionsInContextOfItem(item) {
|
||||
return this.extensions.filter(function(ext){
|
||||
return _.includes(ext.supported_types, item.content_type) || ext.actionsWithContextForItem(item).length > 0;
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Loads an extension in the context of a certain item. The server then has the chance to respond with actions that are
|
||||
relevant just to this item. The response extension is not saved, just displayed as a one-time thing.
|
||||
*/
|
||||
loadExtensionInContextOfItem(extension, item, callback) {
|
||||
this.httpManager.getAbsolute(extension.url, {content_type: item.content_type, item_uuid: item.uuid}, function(response){
|
||||
this.updateExtensionFromRemoteResponse(extension, response);
|
||||
callback && callback(extension);
|
||||
}.bind(this), function(response){
|
||||
console.log("Error loading extension", response);
|
||||
if(callback) {
|
||||
callback(null);
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
updateExtensionFromRemoteResponse(extension, response) {
|
||||
if(response.description) { extension.description = response.description; }
|
||||
if(response.supported_types) { extension.supported_types = response.supported_types; }
|
||||
|
||||
if(response.actions) {
|
||||
extension.actions = response.actions.map(function(action){
|
||||
return new Action(action);
|
||||
})
|
||||
} else {
|
||||
extension.actions = [];
|
||||
}
|
||||
}
|
||||
|
||||
executeAction(action, extension, item, callback) {
|
||||
|
||||
var customCallback = function(response) {
|
||||
action.running = false;
|
||||
callback(response);
|
||||
}
|
||||
|
||||
action.running = true;
|
||||
|
||||
let decrypted = action.access_type == "decrypted";
|
||||
|
||||
switch (action.verb) {
|
||||
case "get": {
|
||||
|
||||
this.httpManager.getAbsolute(action.url, {}, function(response){
|
||||
action.error = false;
|
||||
var items = response.items || [response.item];
|
||||
EncryptionHelper.decryptMultipleItems(items, this.authManager.keys());
|
||||
items = this.modelManager.mapResponseItemsToLocalModels(items, ModelManager.MappingSourceRemoteActionRetrieved);
|
||||
for(var item of items) {
|
||||
item.setDirty(true);
|
||||
}
|
||||
this.syncManager.sync(null);
|
||||
customCallback({items: items});
|
||||
}.bind(this), function(response){
|
||||
action.error = true;
|
||||
customCallback(null);
|
||||
})
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "render": {
|
||||
|
||||
this.httpManager.getAbsolute(action.url, {}, function(response){
|
||||
action.error = false;
|
||||
EncryptionHelper.decryptItem(response.item, this.authManager.keys());
|
||||
var item = this.modelManager.createItem(response.item, true /* Dont notify observers */);
|
||||
customCallback({item: item});
|
||||
|
||||
}.bind(this), function(response){
|
||||
action.error = true;
|
||||
customCallback(null);
|
||||
})
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "show": {
|
||||
var win = window.open(action.url, '_blank');
|
||||
win.focus();
|
||||
customCallback();
|
||||
break;
|
||||
}
|
||||
|
||||
case "post": {
|
||||
var params = {};
|
||||
|
||||
if(action.all) {
|
||||
var items = this.modelManager.allItemsMatchingTypes(action.content_types);
|
||||
params.items = items.map(function(item){
|
||||
var params = this.outgoingParamsForItem(item, extension, decrypted);
|
||||
return params;
|
||||
}.bind(this))
|
||||
|
||||
} else {
|
||||
params.items = [this.outgoingParamsForItem(item, extension, decrypted)];
|
||||
}
|
||||
|
||||
this.performPost(action, extension, params, function(response){
|
||||
customCallback(response);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
action.lastExecuted = new Date();
|
||||
}
|
||||
|
||||
outgoingParamsForItem(item, extension, decrypted = false) {
|
||||
var keys = this.authManager.keys();
|
||||
if(decrypted) {
|
||||
keys = null;
|
||||
}
|
||||
var itemParams = new ItemParams(item, keys, this.authManager.protocolVersion());
|
||||
return itemParams.paramsForExtension();
|
||||
}
|
||||
|
||||
performPost(action, extension, params, callback) {
|
||||
this.httpManager.postAbsolute(action.url, params, function(response){
|
||||
action.error = false;
|
||||
if(callback) {
|
||||
callback(response);
|
||||
}
|
||||
}.bind(this), function(response){
|
||||
action.error = true;
|
||||
console.log("Action error response:", response);
|
||||
if(callback) {
|
||||
callback({error: "Request error"});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').service('actionsManager', ActionsManager);
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('app.frontend')
|
||||
angular.module('app')
|
||||
.provider('authManager', function () {
|
||||
|
||||
function domainName() {
|
||||
@@ -7,11 +7,11 @@ angular.module('app.frontend')
|
||||
return domain;
|
||||
}
|
||||
|
||||
this.$get = function($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager) {
|
||||
return new AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager);
|
||||
this.$get = function($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager) {
|
||||
return new AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager);
|
||||
}
|
||||
|
||||
function AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager) {
|
||||
function AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager) {
|
||||
|
||||
this.loadInitialData = function() {
|
||||
var userData = storageManager.getItem("user");
|
||||
@@ -43,11 +43,10 @@ angular.module('app.frontend')
|
||||
this.ephemeral = ephemeral;
|
||||
if(ephemeral) {
|
||||
storageManager.setModelStorageMode(StorageManager.Ephemeral);
|
||||
storageManager.setItemsMode(storageManager.hasPasscode() ? StorageManager.FixedEncrypted : StorageManager.Ephemeral);
|
||||
storageManager.setItemsMode(StorageManager.Ephemeral);
|
||||
} else {
|
||||
storageManager.setModelStorageMode(StorageManager.Fixed);
|
||||
storageManager.setItemsMode(storageManager.hasPasscode() ? StorageManager.FixedEncrypted : StorageManager.Fixed);
|
||||
|
||||
storageManager.setItem("ephemeral", JSON.stringify(false), StorageManager.Fixed);
|
||||
}
|
||||
}
|
||||
@@ -95,9 +94,9 @@ angular.module('app.frontend')
|
||||
return supportedVersions.includes(version);
|
||||
}
|
||||
|
||||
this.getAuthParamsForEmail = function(url, email, callback) {
|
||||
this.getAuthParamsForEmail = function(url, email, extraParams, callback) {
|
||||
var requestUrl = url + "/auth/params";
|
||||
httpManager.getAbsolute(requestUrl, {email: email}, function(response){
|
||||
httpManager.getAbsolute(requestUrl, _.merge({email: email}, extraParams), function(response){
|
||||
callback(response);
|
||||
}, function(response){
|
||||
console.error("Error getting auth params", response);
|
||||
@@ -120,8 +119,8 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
|
||||
this.login = function(url, email, password, ephemeral, callback) {
|
||||
this.getAuthParamsForEmail(url, email, function(authParams){
|
||||
this.login = function(url, email, password, ephemeral, extraParams, callback) {
|
||||
this.getAuthParamsForEmail(url, email, extraParams, function(authParams){
|
||||
|
||||
if(authParams.error) {
|
||||
callback(authParams);
|
||||
@@ -134,31 +133,30 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
if(!this.isProtocolVersionSupported(authParams.version)) {
|
||||
alert("The protocol version associated with your account is outdated and no longer supported by this application. Please visit standardnotes.org/help/security-update for more information.");
|
||||
callback({didDisplayAlert: true});
|
||||
let message = "The protocol version associated with your account is outdated and no longer supported by this application. Please visit standardnotes.org/help/security-update for more information.";
|
||||
callback({error: {message: message}});
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.supportsPasswordDerivationCost(authParams.pw_cost)) {
|
||||
var string = "Your account was created on a platform with higher security capabilities than this browser supports. " +
|
||||
let message = "Your account was created on a platform with higher security capabilities than this browser supports. " +
|
||||
"If we attempted to generate your login keys here, it would take hours. " +
|
||||
"Please use a browser with more up to date security capabilities, like Google Chrome or Firefox, to login."
|
||||
alert(string)
|
||||
callback({didDisplayAlert: true});
|
||||
"Please use a browser with more up to date security capabilities, like Google Chrome or Firefox, to log in."
|
||||
callback({error: {message: message}});
|
||||
return;
|
||||
}
|
||||
|
||||
var minimum = this.costMinimumForVersion(authParams.version);
|
||||
if(authParams.pw_cost < minimum) {
|
||||
alert("Unable to login due to insecure password parameters. Please visit standardnotes.org/help/password-upgrade for more information.");
|
||||
callback({didDisplayAlert: true});
|
||||
let message = "Unable to login due to insecure password parameters. Please visit standardnotes.org/help/password-upgrade for more information.";
|
||||
callback({error: {message: message}});
|
||||
return;
|
||||
}
|
||||
|
||||
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, authParams), function(keys){
|
||||
|
||||
var requestUrl = url + "/auth/sign_in";
|
||||
var params = {password: keys.pw, email: email};
|
||||
var params = _.merge({password: keys.pw, email: email}, extraParams);
|
||||
httpManager.postAbsolute(requestUrl, params, function(response){
|
||||
this.setEphemeral(ephemeral);
|
||||
this.handleAuthResponse(response, email, url, authParams, keys);
|
||||
@@ -291,5 +289,45 @@ angular.module('app.frontend')
|
||||
this._authParams = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* User Preferences */
|
||||
|
||||
let prefsContentType = "SN|UserPreferences";
|
||||
|
||||
singletonManager.registerSingleton({content_type: prefsContentType}, (resolvedSingleton) => {
|
||||
this.userPreferences = resolvedSingleton;
|
||||
this.userPreferencesDidChange();
|
||||
}, (valueCallback) => {
|
||||
// Safe to create. Create and return object.
|
||||
var prefs = new Item({content_type: prefsContentType});
|
||||
modelManager.addItem(prefs);
|
||||
prefs.setDirty(true);
|
||||
$rootScope.sync("authManager singletonCreate");
|
||||
valueCallback(prefs);
|
||||
});
|
||||
|
||||
this.userPreferencesDidChange = function() {
|
||||
$rootScope.$broadcast("user-preferences-changed");
|
||||
}
|
||||
|
||||
this.syncUserPreferences = function() {
|
||||
this.userPreferences.setDirty(true);
|
||||
$rootScope.sync("syncUserPreferences");
|
||||
}
|
||||
|
||||
this.getUserPrefValue = function(key, defaultValue) {
|
||||
if(!this.userPreferences) { return defaultValue; }
|
||||
var value = this.userPreferences.getAppDataItem(key);
|
||||
return (value !== undefined && value != null) ? value : defaultValue;
|
||||
}
|
||||
|
||||
this.setUserPrefValue = function(key, value, sync) {
|
||||
if(!this.userPreferences) { console.log("Prefs are null, not setting value", key); return; }
|
||||
this.userPreferences.setAppDataItem(key, value);
|
||||
if(sync) {
|
||||
this.syncUserPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,27 +3,45 @@ let ClientDataDomain = "org.standardnotes.sn.components";
|
||||
|
||||
class ComponentManager {
|
||||
|
||||
constructor($rootScope, modelManager, syncManager, themeManager, $timeout, $compile) {
|
||||
constructor($rootScope, modelManager, syncManager, desktopManager, nativeExtManager, $timeout, $compile) {
|
||||
this.$compile = $compile;
|
||||
this.$rootScope = $rootScope;
|
||||
this.modelManager = modelManager;
|
||||
this.syncManager = syncManager;
|
||||
this.themeManager = themeManager;
|
||||
this.desktopManager = desktopManager;
|
||||
this.nativeExtManager = nativeExtManager;
|
||||
this.timeout = $timeout;
|
||||
this.streamObservers = [];
|
||||
this.contextStreamObservers = [];
|
||||
this.activeComponents = [];
|
||||
|
||||
const detectFocusChange = (event) => {
|
||||
for(var component of this.activeComponents) {
|
||||
if(document.activeElement == this.iframeForComponent(component)) {
|
||||
this.timeout(() => {
|
||||
this.focusChangedForComponent(component);
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener ? window.addEventListener('focus', detectFocusChange, true) : window.attachEvent('onfocusout', detectFocusChange);
|
||||
window.addEventListener ? window.addEventListener('blur', detectFocusChange, true) : window.attachEvent('onblur', detectFocusChange);
|
||||
|
||||
desktopManager.registerUpdateObserver((component) => {
|
||||
// Reload theme if active
|
||||
if(component.active && component.isTheme()) {
|
||||
this.postActiveThemeToAllComponents();
|
||||
}
|
||||
})
|
||||
|
||||
// this.loggingEnabled = true;
|
||||
|
||||
this.permissionDialogs = [];
|
||||
|
||||
this.handlers = [];
|
||||
|
||||
$rootScope.$on("theme-changed", function(){
|
||||
this.postThemeToComponents();
|
||||
}.bind(this))
|
||||
|
||||
window.addEventListener("message", function(event){
|
||||
if(this.loggingEnabled) {
|
||||
console.log("Web app: received message", event);
|
||||
@@ -31,7 +49,7 @@ class ComponentManager {
|
||||
this.handleMessage(this.componentForSessionKey(event.data.sessionKey), event.data);
|
||||
}.bind(this), false);
|
||||
|
||||
this.modelManager.addItemSyncObserver("component-manager", "*", function(allItems, validItems, deletedItems, source) {
|
||||
this.modelManager.addItemSyncObserver("component-manager", "*", (allItems, validItems, deletedItems, source) => {
|
||||
|
||||
/* If the source of these new or updated items is from a Component itself saving items, we don't need to notify
|
||||
components again of the same item. Regarding notifying other components than the issuing component, other mapping sources
|
||||
@@ -41,7 +59,18 @@ class ComponentManager {
|
||||
return;
|
||||
}
|
||||
|
||||
var syncedComponents = allItems.filter(function(item){return item.content_type === "SN|Component" });
|
||||
var syncedComponents = allItems.filter(function(item) {
|
||||
return item.content_type === "SN|Component" || item.content_type == "SN|Theme"
|
||||
});
|
||||
|
||||
/* We only want to sync if the item source is Retrieved, not MappingSourceRemoteSaved to avoid
|
||||
recursion caused by the component being modified and saved after it is updated.
|
||||
*/
|
||||
if(syncedComponents.length > 0 && source != ModelManager.MappingSourceRemoteSaved) {
|
||||
// Ensure any component in our data is installed by the system
|
||||
this.desktopManager.syncComponentsInstallation(syncedComponents);
|
||||
}
|
||||
|
||||
for(var component of syncedComponents) {
|
||||
var activeComponent = _.find(this.activeComponents, {uuid: component.uuid});
|
||||
if(component.active && !component.deleted && !activeComponent) {
|
||||
@@ -56,6 +85,10 @@ class ComponentManager {
|
||||
return observer.contentTypes.indexOf(item.content_type) !== -1;
|
||||
})
|
||||
|
||||
if(relevantItems.length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var requiredPermissions = [
|
||||
{
|
||||
name: "stream-items",
|
||||
@@ -63,9 +96,9 @@ class ComponentManager {
|
||||
}
|
||||
];
|
||||
|
||||
this.runWithPermissions(observer.component, requiredPermissions, observer.originalMessage.permissions, function(){
|
||||
this.runWithPermissions(observer.component, requiredPermissions, () => {
|
||||
this.sendItemsInReply(observer.component, relevantItems, observer.originalMessage);
|
||||
}.bind(this))
|
||||
})
|
||||
}
|
||||
|
||||
var requiredContextPermissions = [
|
||||
@@ -75,36 +108,45 @@ class ComponentManager {
|
||||
];
|
||||
|
||||
for(let observer of this.contextStreamObservers) {
|
||||
this.runWithPermissions(observer.component, requiredContextPermissions, observer.originalMessage.permissions, function(){
|
||||
for(let handler of this.handlers) {
|
||||
if(!handler.areas.includes(observer.component.area)) {
|
||||
continue;
|
||||
}
|
||||
for(let handler of this.handlers) {
|
||||
if(!handler.areas.includes(observer.component.area) && !handler.areas.includes("*")) {
|
||||
continue;
|
||||
}
|
||||
if(handler.contextRequestHandler) {
|
||||
var itemInContext = handler.contextRequestHandler(observer.component);
|
||||
if(itemInContext) {
|
||||
var matchingItem = _.find(allItems, {uuid: itemInContext.uuid});
|
||||
if(matchingItem) {
|
||||
this.sendContextItemInReply(observer.component, matchingItem, observer.originalMessage, source);
|
||||
this.runWithPermissions(observer.component, requiredContextPermissions, () => {
|
||||
this.sendContextItemInReply(observer.component, matchingItem, observer.originalMessage, source);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
}
|
||||
}.bind(this))
|
||||
});
|
||||
}
|
||||
|
||||
postThemeToComponents() {
|
||||
postActiveThemeToAllComponents() {
|
||||
for(var component of this.components) {
|
||||
if(!component.active || !component.window) {
|
||||
// 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.postThemeToComponent(component);
|
||||
this.postActiveThemeToComponent(component);
|
||||
}
|
||||
}
|
||||
|
||||
postThemeToComponent(component) {
|
||||
getActiveTheme() {
|
||||
return this.componentsForArea("themes").find((theme) => {return theme.active});
|
||||
}
|
||||
|
||||
postActiveThemeToComponent(component) {
|
||||
var activeTheme = this.getActiveTheme();
|
||||
var data = {
|
||||
themes: [this.themeManager.currentTheme ? this.themeManager.currentTheme.url : null]
|
||||
themes: [activeTheme ? this.urlForComponent(activeTheme) : null]
|
||||
}
|
||||
|
||||
this.sendMessageToComponent(component, {action: "themes", data: data})
|
||||
@@ -112,7 +154,7 @@ class ComponentManager {
|
||||
|
||||
contextItemDidChangeInArea(area) {
|
||||
for(let handler of this.handlers) {
|
||||
if(handler.areas.includes(area) === false) {
|
||||
if(handler.areas.includes(area) === false && !handler.areas.includes("*")) {
|
||||
continue;
|
||||
}
|
||||
var observers = this.contextStreamObservers.filter(function(observer){
|
||||
@@ -120,8 +162,10 @@ class ComponentManager {
|
||||
})
|
||||
|
||||
for(let observer of observers) {
|
||||
var itemInContext = handler.contextRequestHandler(observer.component);
|
||||
this.sendContextItemInReply(observer.component, itemInContext, observer.originalMessage);
|
||||
if(handler.contextRequestHandler) {
|
||||
var itemInContext = handler.contextRequestHandler(observer.component);
|
||||
this.sendContextItemInReply(observer.component, itemInContext, observer.originalMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,7 +173,8 @@ class ComponentManager {
|
||||
jsonForItem(item, component, source) {
|
||||
var params = {uuid: item.uuid, content_type: item.content_type, created_at: item.created_at, updated_at: item.updated_at, deleted: item.deleted};
|
||||
params.content = item.createContentJSONFromProperties();
|
||||
params.clientData = item.getDomainDataItem(component.url, ClientDataDomain) || {};
|
||||
/* Legacy is using component.url key, so if it's present, use it, otherwise use uuid */
|
||||
params.clientData = item.getDomainDataItem(component.url || component.uuid, ClientDataDomain) || {};
|
||||
|
||||
/* This means the this function is being triggered through a remote Saving response, which should not update
|
||||
actual local content values. The reason is, Save responses may be delayed, and a user may have changed some values
|
||||
@@ -139,7 +184,7 @@ class ComponentManager {
|
||||
if(source && source == ModelManager.MappingSourceRemoteSaved) {
|
||||
params.isMetadataUpdate = true;
|
||||
}
|
||||
this.removePrivatePropertiesFromResponseItems([params]);
|
||||
this.removePrivatePropertiesFromResponseItems([params], component);
|
||||
return params;
|
||||
}
|
||||
|
||||
@@ -160,8 +205,33 @@ class ComponentManager {
|
||||
this.replyToMessage(component, originalMessage, response);
|
||||
}
|
||||
|
||||
replyToMessage(component, originalMessage, replyData) {
|
||||
var reply = {
|
||||
action: "reply",
|
||||
original: originalMessage,
|
||||
data: replyData
|
||||
}
|
||||
|
||||
this.sendMessageToComponent(component, reply);
|
||||
}
|
||||
|
||||
sendMessageToComponent(component, message) {
|
||||
let permissibleActionsWhileHidden = ["component-registered", "themes"];
|
||||
if(component.hidden && !permissibleActionsWhileHidden.includes(message.action)) {
|
||||
if(this.loggingEnabled) {
|
||||
console.log("Component disabled for current item, not sending any messages.", component.name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.loggingEnabled) {
|
||||
console.log("Web|sendMessageToComponent", component, message);
|
||||
}
|
||||
component.window.postMessage(message, "*");
|
||||
}
|
||||
|
||||
get components() {
|
||||
return this.modelManager.itemsForContentType("SN|Component");
|
||||
return this.modelManager.allItemsMatchingTypes(["SN|Component", "SN|Theme"]);
|
||||
}
|
||||
|
||||
componentsForArea(area) {
|
||||
@@ -170,9 +240,17 @@ class ComponentManager {
|
||||
})
|
||||
}
|
||||
|
||||
urlForComponent(component) {
|
||||
if(component.offlineOnly || (isDesktopApplication() && component.local_url)) {
|
||||
return component.local_url && component.local_url.replace("sn://", this.desktopManager.getApplicationDataPath() + "/");
|
||||
} else {
|
||||
return component.hosted_url || component.url;
|
||||
}
|
||||
}
|
||||
|
||||
componentForUrl(url) {
|
||||
return this.components.filter(function(component){
|
||||
return component.url === url;
|
||||
return component.url === url || component.hosted_url === url;
|
||||
})[0];
|
||||
}
|
||||
|
||||
@@ -191,91 +269,46 @@ class ComponentManager {
|
||||
|
||||
/**
|
||||
Possible Messages:
|
||||
set-size
|
||||
stream-items
|
||||
stream-context-item
|
||||
save-items
|
||||
select-item
|
||||
associate-item
|
||||
deassociate-item
|
||||
clear-selection
|
||||
create-item
|
||||
delete-items
|
||||
set-component-data
|
||||
save-context-client-data
|
||||
get-context-client-data
|
||||
set-size
|
||||
stream-items
|
||||
stream-context-item
|
||||
save-items
|
||||
select-item
|
||||
associate-item
|
||||
deassociate-item
|
||||
clear-selection
|
||||
create-item
|
||||
delete-items
|
||||
set-component-data
|
||||
install-local-component
|
||||
toggle-activate-component
|
||||
request-permissions
|
||||
*/
|
||||
|
||||
if(message.action === "stream-items") {
|
||||
this.handleStreamItemsMessage(component, message);
|
||||
}
|
||||
|
||||
else if(message.action === "stream-context-item") {
|
||||
} else if(message.action === "stream-context-item") {
|
||||
this.handleStreamContextItemMessage(component, message);
|
||||
} else if(message.action === "set-component-data") {
|
||||
this.handleSetComponentDataMessage(component, message);
|
||||
} else if(message.action === "delete-items") {
|
||||
this.handleDeleteItemsMessage(component, message);
|
||||
} else if(message.action === "create-item") {
|
||||
this.handleCreateItemMessage(component, message);
|
||||
} else if(message.action === "save-items") {
|
||||
this.handleSaveItemsMessage(component, message);
|
||||
} else if(message.action === "toggle-activate-component") {
|
||||
let componentToToggle = this.modelManager.findItem(message.data.uuid);
|
||||
this.handleToggleComponentMessage(component, componentToToggle, message);
|
||||
} else if(message.action === "request-permissions") {
|
||||
this.handleRequestPermissionsMessage(component, message);
|
||||
} else if(message.action === "install-local-component") {
|
||||
this.handleInstallLocalComponentMessage(component, message);
|
||||
}
|
||||
|
||||
else if(message.action === "set-component-data") {
|
||||
component.componentData = message.data.componentData;
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
else if(message.action === "delete-items") {
|
||||
var items = message.data.items;
|
||||
var noun = items.length == 1 ? "item" : "items";
|
||||
if(confirm(`Are you sure you want to delete ${items.length} ${noun}?`)) {
|
||||
for(var item of items) {
|
||||
var model = this.modelManager.findItem(item.uuid);
|
||||
this.modelManager.setItemToBeDeleted(model);
|
||||
}
|
||||
|
||||
this.syncManager.sync();
|
||||
}
|
||||
}
|
||||
|
||||
else if(message.action === "create-item") {
|
||||
var responseItem = message.data.item;
|
||||
this.removePrivatePropertiesFromResponseItems([responseItem]);
|
||||
var item = this.modelManager.createItem(responseItem);
|
||||
if(responseItem.clientData) {
|
||||
item.setDomainDataItem(component.url, responseItem.clientData, ClientDataDomain);
|
||||
}
|
||||
this.modelManager.addItem(item);
|
||||
this.modelManager.resolveReferencesForItem(item);
|
||||
item.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
this.replyToMessage(component, message, {item: this.jsonForItem(item, component)})
|
||||
}
|
||||
|
||||
else if(message.action === "save-items") {
|
||||
var responseItems = message.data.items;
|
||||
|
||||
this.removePrivatePropertiesFromResponseItems(responseItems);
|
||||
|
||||
/*
|
||||
We map the items here because modelManager is what updates the UI. If you were to instead get the items directly,
|
||||
this would update them server side via sync, but would never make its way back to the UI.
|
||||
*/
|
||||
var localItems = this.modelManager.mapResponseItemsToLocalModels(responseItems, ModelManager.MappingSourceComponentRetrieved);
|
||||
|
||||
for(var item of localItems) {
|
||||
var responseItem = _.find(responseItems, {uuid: item.uuid});
|
||||
_.merge(item.content, responseItem.content);
|
||||
if(responseItem.clientData) {
|
||||
item.setDomainDataItem(component.url, responseItem.clientData, ClientDataDomain);
|
||||
}
|
||||
item.setDirty(true);
|
||||
}
|
||||
this.syncManager.sync((response) => {
|
||||
// Allow handlers to be notified when a save begins and ends, to update the UI
|
||||
var saveMessage = Object.assign({}, message);
|
||||
saveMessage.action = response && response.error ? "save-error" : "save-success";
|
||||
this.handleMessage(component, saveMessage);
|
||||
});
|
||||
}
|
||||
|
||||
// Notify observers
|
||||
for(let handler of this.handlers) {
|
||||
if(handler.areas.includes(component.area)) {
|
||||
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
|
||||
this.timeout(function(){
|
||||
handler.actionHandler(component, message.action, message.data);
|
||||
})
|
||||
@@ -283,9 +316,18 @@ class ComponentManager {
|
||||
}
|
||||
}
|
||||
|
||||
removePrivatePropertiesFromResponseItems(responseItems) {
|
||||
removePrivatePropertiesFromResponseItems(responseItems, component, options = {}) {
|
||||
if(component) {
|
||||
// System extensions can bypass this step
|
||||
if(this.nativeExtManager.isSystemExtension(component)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Don't allow component to overwrite these properties.
|
||||
let privateProperties = ["appData"];
|
||||
var privateProperties = ["appData", "autoupdateDisabled", "permissions", "active"];
|
||||
if(options) {
|
||||
if(options.includeUrls) { privateProperties = privateProperties.concat(["url", "hosted_url", "local_url"])}
|
||||
}
|
||||
for(var responseItem of responseItems) {
|
||||
|
||||
// Do not pass in actual items here, otherwise that would be destructive.
|
||||
@@ -293,7 +335,7 @@ class ComponentManager {
|
||||
console.assert(typeof responseItem.setDirty !== 'function');
|
||||
|
||||
for(var prop of privateProperties) {
|
||||
delete responseItem[prop];
|
||||
delete responseItem.content[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,25 +348,24 @@ class ComponentManager {
|
||||
}
|
||||
];
|
||||
|
||||
this.runWithPermissions(component, requiredPermissions, message.permissions, function(){
|
||||
if(!_.find(this.streamObservers, {identifier: component.url})) {
|
||||
this.runWithPermissions(component, requiredPermissions, () => {
|
||||
if(!_.find(this.streamObservers, {identifier: component.uuid})) {
|
||||
// for pushing laster as changes come in
|
||||
this.streamObservers.push({
|
||||
identifier: component.url,
|
||||
identifier: component.uuid,
|
||||
component: component,
|
||||
originalMessage: message,
|
||||
contentTypes: message.data.content_types
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// push immediately now
|
||||
var items = [];
|
||||
for(var contentType of message.data.content_types) {
|
||||
items = items.concat(this.modelManager.itemsForContentType(contentType));
|
||||
}
|
||||
this.sendItemsInReply(component, items, message);
|
||||
}.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
handleStreamContextItemMessage(component, message) {
|
||||
@@ -335,55 +376,223 @@ class ComponentManager {
|
||||
}
|
||||
];
|
||||
|
||||
this.runWithPermissions(component, requiredPermissions, message.permissions, function(){
|
||||
if(!_.find(this.contextStreamObservers, {identifier: component.url})) {
|
||||
this.runWithPermissions(component, requiredPermissions, function(){
|
||||
if(!_.find(this.contextStreamObservers, {identifier: component.uuid})) {
|
||||
// for pushing laster as changes come in
|
||||
this.contextStreamObservers.push({
|
||||
identifier: component.url,
|
||||
identifier: component.uuid,
|
||||
component: component,
|
||||
originalMessage: message
|
||||
})
|
||||
}
|
||||
|
||||
// push immediately now
|
||||
for(let handler of this.handlers) {
|
||||
if(handler.areas.includes(component.area) === false) {
|
||||
continue;
|
||||
for(let handler of this.handlersForArea(component.area)) {
|
||||
if(handler.contextRequestHandler) {
|
||||
var itemInContext = handler.contextRequestHandler(component);
|
||||
this.sendContextItemInReply(component, itemInContext, message);
|
||||
}
|
||||
var itemInContext = handler.contextRequestHandler(component);
|
||||
this.sendContextItemInReply(component, itemInContext, message);
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
runWithPermissions(component, requiredPermissions, requestedPermissions, runFunction) {
|
||||
|
||||
var acquiredPermissions = component.permissions;
|
||||
|
||||
var requestedMatchesRequired = true;
|
||||
|
||||
for(var required of requiredPermissions) {
|
||||
var matching = _.find(requestedPermissions, required);
|
||||
if(!matching) {
|
||||
requestedMatchesRequired = false;
|
||||
break;
|
||||
isItemWithinComponentContextJurisdiction(item, component) {
|
||||
for(let handler of this.handlersForArea(component.area)) {
|
||||
if(handler.contextRequestHandler) {
|
||||
var itemInContext = handler.contextRequestHandler(component);
|
||||
if(itemInContext && itemInContext.uuid == item.uuid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!requestedMatchesRequired) {
|
||||
// Error with Component permissions request
|
||||
console.error("You are requesting permissions", requestedPermissions, "when you need to be requesting", requiredPermissions, ". Component:", component);
|
||||
handlersForArea(area) {
|
||||
return this.handlers.filter((candidate) => {return candidate.areas.includes(area)});
|
||||
}
|
||||
|
||||
handleSaveItemsMessage(component, message) {
|
||||
var responseItems = message.data.items;
|
||||
var requiredPermissions;
|
||||
|
||||
// Check if you're just trying to save the context item, which requires only stream-context-item permissions
|
||||
if(responseItems.length == 1 && this.isItemWithinComponentContextJurisdiction(responseItems[0], component)) {
|
||||
requiredPermissions = [
|
||||
{
|
||||
name: "stream-context-item"
|
||||
}
|
||||
];
|
||||
} else {
|
||||
var requiredContentTypes = _.uniq(responseItems.map((i) => {return i.content_type})).sort();
|
||||
requiredPermissions = [
|
||||
{
|
||||
name: "stream-items",
|
||||
content_types: requiredContentTypes
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
this.runWithPermissions(component, requiredPermissions, () => {
|
||||
|
||||
this.removePrivatePropertiesFromResponseItems(responseItems, component, {includeUrls: true});
|
||||
|
||||
/*
|
||||
We map the items here because modelManager is what updates the UI. If you were to instead get the items directly,
|
||||
this would update them server side via sync, but would never make its way back to the UI.
|
||||
*/
|
||||
var localItems = this.modelManager.mapResponseItemsToLocalModels(responseItems, ModelManager.MappingSourceComponentRetrieved);
|
||||
|
||||
for(var item of localItems) {
|
||||
var responseItem = _.find(responseItems, {uuid: item.uuid});
|
||||
_.merge(item.content, responseItem.content);
|
||||
if(responseItem.clientData) {
|
||||
item.setDomainDataItem(component.url || component.uuid, responseItem.clientData, ClientDataDomain);
|
||||
}
|
||||
item.setDirty(true);
|
||||
}
|
||||
|
||||
this.syncManager.sync((response) => {
|
||||
// Allow handlers to be notified when a save begins and ends, to update the UI
|
||||
var saveMessage = Object.assign({}, message);
|
||||
saveMessage.action = response && response.error ? "save-error" : "save-success";
|
||||
this.replyToMessage(component, message, {error: response.error})
|
||||
this.handleMessage(component, saveMessage);
|
||||
}, null, "handleSaveItemsMessage");
|
||||
});
|
||||
}
|
||||
|
||||
handleCreateItemMessage(component, message) {
|
||||
var requiredPermissions = [
|
||||
{
|
||||
name: "stream-items",
|
||||
content_types: [message.data.item.content_type]
|
||||
}
|
||||
];
|
||||
|
||||
this.runWithPermissions(component, requiredPermissions, () => {
|
||||
var responseItem = message.data.item;
|
||||
this.removePrivatePropertiesFromResponseItems([responseItem], component);
|
||||
var item = this.modelManager.createItem(responseItem);
|
||||
if(responseItem.clientData) {
|
||||
item.setDomainDataItem(component.url || component.uuid, responseItem.clientData, ClientDataDomain);
|
||||
}
|
||||
this.modelManager.addItem(item);
|
||||
this.modelManager.resolveReferencesForItem(item);
|
||||
item.setDirty(true);
|
||||
this.syncManager.sync("handleCreateItemMessage");
|
||||
this.replyToMessage(component, message, {item: this.jsonForItem(item, component)})
|
||||
});
|
||||
}
|
||||
|
||||
handleDeleteItemsMessage(component, message) {
|
||||
var requiredContentTypes = _.uniq(message.data.items.map((i) => {return i.content_type})).sort();
|
||||
var requiredPermissions = [
|
||||
{
|
||||
name: "stream-items",
|
||||
content_types: requiredContentTypes
|
||||
}
|
||||
];
|
||||
|
||||
this.runWithPermissions(component, requiredPermissions, () => {
|
||||
var itemsData = message.data.items;
|
||||
var noun = itemsData.length == 1 ? "item" : "items";
|
||||
if(confirm(`Are you sure you want to delete ${itemsData.length} ${noun}?`)) {
|
||||
// Filter for any components and deactivate before deleting
|
||||
for(var itemData of itemsData) {
|
||||
var model = this.modelManager.findItem(itemData.uuid);
|
||||
if(["SN|Component", "SN|Theme"].includes(model.content_type)) {
|
||||
this.deactivateComponent(model, true);
|
||||
}
|
||||
this.modelManager.setItemToBeDeleted(model);
|
||||
}
|
||||
|
||||
this.syncManager.sync("handleDeleteItemsMessage");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleRequestPermissionsMessage(component, message) {
|
||||
this.runWithPermissions(component, message.data.permissions, () => {
|
||||
this.replyToMessage(component, message, {approved: true});
|
||||
});
|
||||
}
|
||||
|
||||
handleSetComponentDataMessage(component, message) {
|
||||
// A component setting its own data does not require special permissions
|
||||
this.runWithPermissions(component, [], () => {
|
||||
component.componentData = message.data.componentData;
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync("handleSetComponentDataMessage");
|
||||
});
|
||||
}
|
||||
|
||||
handleToggleComponentMessage(sourceComponent, targetComponent, message) {
|
||||
if(targetComponent.area == "modal") {
|
||||
this.openModalComponent(targetComponent);
|
||||
} else {
|
||||
if(targetComponent.active) {
|
||||
this.deactivateComponent(targetComponent);
|
||||
} else {
|
||||
if(targetComponent.content_type == "SN|Theme") {
|
||||
// Deactive currently active theme
|
||||
var activeTheme = this.getActiveTheme();
|
||||
if(activeTheme) {
|
||||
this.deactivateComponent(activeTheme);
|
||||
}
|
||||
}
|
||||
this.activateComponent(targetComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleInstallLocalComponentMessage(sourceComponent, message) {
|
||||
// Only extensions manager has this permission
|
||||
if(!this.nativeExtManager.isSystemExtension(sourceComponent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let targetComponent = this.modelManager.findItem(message.data.uuid);
|
||||
this.desktopManager.installComponent(targetComponent);
|
||||
}
|
||||
|
||||
runWithPermissions(component, requiredPermissions, runFunction) {
|
||||
|
||||
if(!component.permissions) {
|
||||
component.permissions = [];
|
||||
}
|
||||
|
||||
var acquiredMatchesRequested = angular.toJson(component.permissions.sort()) === angular.toJson(requestedPermissions.sort());
|
||||
var acquiredPermissions = component.permissions;
|
||||
var acquiredMatchesRequired = true;
|
||||
|
||||
if(!acquiredMatchesRequested) {
|
||||
this.promptForPermissions(component, requestedPermissions, function(approved){
|
||||
for(var required of requiredPermissions) {
|
||||
var matching = acquiredPermissions.find((candidate) => {
|
||||
var matchesContentTypes = true;
|
||||
if(candidate.content_types && required.content_types) {
|
||||
matchesContentTypes = JSON.stringify(candidate.content_types.sort()) == JSON.stringify(required.content_types.sort());
|
||||
}
|
||||
return candidate.name == required.name && matchesContentTypes;
|
||||
});
|
||||
|
||||
if(!matching) {
|
||||
/* Required permissions can be 1 content type, and requestedPermisisons may send an array of content types.
|
||||
In the case of an array, we can just check to make sure that requiredPermissions content type is found in the array
|
||||
*/
|
||||
matching = acquiredPermissions.find((candidate) => {
|
||||
return Array.isArray(candidate.content_types)
|
||||
&& Array.isArray(required.content_types)
|
||||
&& candidate.content_types.containsPrimitiveSubset(required.content_types);
|
||||
});
|
||||
|
||||
if(!matching) {
|
||||
acquiredMatchesRequired = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!acquiredMatchesRequired) {
|
||||
this.promptForPermissions(component, requiredPermissions, function(approved){
|
||||
if(approved) {
|
||||
runFunction();
|
||||
}
|
||||
@@ -393,107 +602,86 @@ class ComponentManager {
|
||||
}
|
||||
}
|
||||
|
||||
promptForPermissions(component, requestedPermissions, callback) {
|
||||
// since these calls are asyncronous, multiple dialogs may be requested at the same time. We only want to present one and trigger all callbacks based on one modal result
|
||||
var existingDialog = _.find(this.permissionDialogs, {component: component});
|
||||
|
||||
component.trusted = component.url.startsWith("https://standardnotes.org") || component.url.startsWith("https://extensions.standardnotes.org");
|
||||
promptForPermissions(component, permissions, callback) {
|
||||
var scope = this.$rootScope.$new(true);
|
||||
scope.component = component;
|
||||
scope.permissions = requestedPermissions;
|
||||
scope.permissions = permissions;
|
||||
scope.actionBlock = callback;
|
||||
|
||||
scope.callback = function(approved) {
|
||||
if(approved) {
|
||||
component.permissions = requestedPermissions;
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
for(var existing of this.permissionDialogs) {
|
||||
if(existing.component === component && existing.actionBlock) {
|
||||
existing.actionBlock(approved);
|
||||
for(var permission of permissions) {
|
||||
if(!component.permissions.includes(permission)) {
|
||||
component.permissions.push(permission);
|
||||
}
|
||||
}
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync("promptForPermissions");
|
||||
}
|
||||
|
||||
this.permissionDialogs = this.permissionDialogs.filter(function(dialog){
|
||||
return dialog.component !== component;
|
||||
this.permissionDialogs = this.permissionDialogs.filter((pendingDialog) => {
|
||||
// Remove self
|
||||
if(pendingDialog == scope) {
|
||||
pendingDialog.actionBlock && pendingDialog.actionBlock(approved);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(pendingDialog.component == component) {
|
||||
// remove pending dialogs that are encapsulated by already approved permissions, and run its function
|
||||
if(pendingDialog.permissions == permissions || permissions.containsObjectSubset(pendingDialog.permissions)) {
|
||||
// If approved, run the action block. Otherwise, if canceled, cancel any pending ones as well, since the user was
|
||||
// explicit in their intentions
|
||||
if(approved) {
|
||||
pendingDialog.actionBlock && pendingDialog.actionBlock(approved);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
|
||||
if(this.permissionDialogs.length > 0) {
|
||||
this.presentDialog(this.permissionDialogs[0]);
|
||||
}
|
||||
|
||||
}.bind(this);
|
||||
|
||||
// since these calls are asyncronous, multiple dialogs may be requested at the same time. We only want to present one and trigger all callbacks based on one modal result
|
||||
var existingDialog = _.find(this.permissionDialogs, {component: component});
|
||||
|
||||
this.permissionDialogs.push(scope);
|
||||
|
||||
if(!existingDialog) {
|
||||
var el = this.$compile( "<permissions-modal component='component' permissions='permissions' callback='callback' class='permissions-modal'></permissions-modal>" )(scope);
|
||||
angular.element(document.body).append(el);
|
||||
this.presentDialog(scope);
|
||||
} else {
|
||||
console.log("Existing dialog, not presenting.");
|
||||
}
|
||||
}
|
||||
|
||||
replyToMessage(component, originalMessage, replyData) {
|
||||
var reply = {
|
||||
action: "reply",
|
||||
original: originalMessage,
|
||||
data: replyData
|
||||
}
|
||||
|
||||
this.sendMessageToComponent(component, reply);
|
||||
presentDialog(dialog) {
|
||||
var permissions = dialog.permissions;
|
||||
var component = dialog.component;
|
||||
var callback = dialog.callback;
|
||||
var el = this.$compile( "<permissions-modal component='component' permissions='permissions' callback='callback' class='modal'></permissions-modal>" )(dialog);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
|
||||
sendMessageToComponent(component, message) {
|
||||
if(component.ignoreEvents && message.action !== "component-registered") {
|
||||
if(this.loggingEnabled) {
|
||||
console.log("Component disabled for current item, not sending any messages.", component.name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(this.loggingEnabled) {
|
||||
console.log("Web|sendMessageToComponent", component, message);
|
||||
}
|
||||
component.window.postMessage(message, "*");
|
||||
}
|
||||
|
||||
installComponent(url) {
|
||||
var name = getParameterByName("name", url);
|
||||
var area = getParameterByName("area", url);
|
||||
var component = this.modelManager.createItem({
|
||||
content_type: "SN|Component",
|
||||
url: url,
|
||||
name: name,
|
||||
area: area
|
||||
})
|
||||
|
||||
this.modelManager.addItem(component);
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
activateComponent(component) {
|
||||
var didChange = component.active != true;
|
||||
|
||||
component.active = true;
|
||||
for(var handler of this.handlers) {
|
||||
if(handler.areas.includes(component.area)) {
|
||||
handler.activationHandler(component);
|
||||
}
|
||||
}
|
||||
|
||||
if(didChange) {
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
if(!this.activeComponents.includes(component)) {
|
||||
this.activeComponents.push(component);
|
||||
}
|
||||
openModalComponent(component) {
|
||||
var scope = this.$rootScope.$new(true);
|
||||
scope.component = component;
|
||||
var el = this.$compile( "<component-modal component='component' class='modal'></component-modal>" )(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
|
||||
registerHandler(handler) {
|
||||
this.handlers.push(handler);
|
||||
}
|
||||
|
||||
deregisterHandler(identifier) {
|
||||
var handler = _.find(this.handlers, {identifier: identifier});
|
||||
this.handlers.splice(this.handlers.indexOf(handler), 1);
|
||||
}
|
||||
|
||||
// Called by other views when the iframe is ready
|
||||
registerComponentWindow(component, componentWindow) {
|
||||
if(component.window === componentWindow) {
|
||||
@@ -507,24 +695,56 @@ class ComponentManager {
|
||||
}
|
||||
component.window = componentWindow;
|
||||
component.sessionKey = Neeto.crypto.generateUUID();
|
||||
this.sendMessageToComponent(component, {action: "component-registered", sessionKey: component.sessionKey, componentData: component.componentData});
|
||||
this.postThemeToComponent(component);
|
||||
this.sendMessageToComponent(component, {
|
||||
action: "component-registered",
|
||||
sessionKey: component.sessionKey,
|
||||
componentData: component.componentData,
|
||||
data: {
|
||||
uuid: component.uuid,
|
||||
environment: isDesktopApplication() ? "desktop" : "web"
|
||||
}
|
||||
});
|
||||
this.postActiveThemeToComponent(component);
|
||||
}
|
||||
|
||||
deactivateComponent(component) {
|
||||
activateComponent(component, dontSync = false) {
|
||||
var didChange = component.active != true;
|
||||
|
||||
component.active = true;
|
||||
for(var handler of this.handlers) {
|
||||
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
|
||||
handler.activationHandler(component);
|
||||
}
|
||||
}
|
||||
|
||||
if(didChange && !dontSync) {
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync("activateComponent");
|
||||
}
|
||||
|
||||
if(!this.activeComponents.includes(component)) {
|
||||
this.activeComponents.push(component);
|
||||
}
|
||||
|
||||
if(component.area == "themes") {
|
||||
this.postActiveThemeToAllComponents();
|
||||
}
|
||||
}
|
||||
|
||||
deactivateComponent(component, dontSync = false) {
|
||||
var didChange = component.active != false;
|
||||
component.active = false;
|
||||
component.sessionKey = null;
|
||||
|
||||
for(var handler of this.handlers) {
|
||||
if(handler.areas.includes(component.area)) {
|
||||
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
|
||||
handler.activationHandler(component);
|
||||
}
|
||||
}
|
||||
|
||||
if(didChange) {
|
||||
if(didChange && !dontSync) {
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
this.syncManager.sync("deactivateComponent");
|
||||
}
|
||||
|
||||
_.pull(this.activeComponents, component);
|
||||
@@ -536,57 +756,23 @@ class ComponentManager {
|
||||
this.contextStreamObservers = this.contextStreamObservers.filter(function(o){
|
||||
return o.component !== component;
|
||||
})
|
||||
|
||||
if(component.area == "themes") {
|
||||
this.postActiveThemeToAllComponents();
|
||||
}
|
||||
}
|
||||
|
||||
deleteComponent(component) {
|
||||
this.modelManager.setItemToBeDeleted(component);
|
||||
this.syncManager.sync();
|
||||
this.syncManager.sync("deleteComponent");
|
||||
}
|
||||
|
||||
isComponentActive(component) {
|
||||
return component.active;
|
||||
}
|
||||
|
||||
disassociateComponentWithItem(component, item) {
|
||||
_.pull(component.associatedItemIds, item.uuid);
|
||||
|
||||
if(component.disassociatedItemIds.indexOf(item.uuid) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
component.disassociatedItemIds.push(item.uuid);
|
||||
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
associateComponentWithItem(component, item) {
|
||||
_.pull(component.disassociatedItemIds, item.uuid);
|
||||
|
||||
if(component.associatedItemIds.includes(item.uuid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
component.associatedItemIds.push(item.uuid);
|
||||
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
enableComponentsForItem(components, item) {
|
||||
for(var component of components) {
|
||||
_.pull(component.disassociatedItemIds, item.uuid);
|
||||
component.setDirty(true);
|
||||
}
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
setEventFlowForComponent(component, on) {
|
||||
component.ignoreEvents = !on;
|
||||
}
|
||||
|
||||
iframeForComponent(component) {
|
||||
for(var frame of document.getElementsByTagName("iframe")) {
|
||||
for(var frame of Array.from(document.getElementsByTagName("iframe"))) {
|
||||
var componentId = frame.dataset.componentId;
|
||||
if(componentId === component.uuid) {
|
||||
return frame;
|
||||
@@ -594,7 +780,40 @@ class ComponentManager {
|
||||
}
|
||||
}
|
||||
|
||||
focusChangedForComponent(component) {
|
||||
let focused = document.activeElement == this.iframeForComponent(component);
|
||||
for(var handler of this.handlers) {
|
||||
// Notify all handlers, and not just ones that match this component type
|
||||
handler.focusHandler && handler.focusHandler(component, focused);
|
||||
}
|
||||
}
|
||||
|
||||
handleSetSizeEvent(component, data) {
|
||||
var setSize = function(element, size) {
|
||||
var widthString = typeof size.width === 'string' ? size.width : `${data.width}px`;
|
||||
var heightString = typeof size.height === 'string' ? size.height : `${data.height}px`;
|
||||
element.setAttribute("style", `width:${widthString}; height:${heightString}; `);
|
||||
}
|
||||
|
||||
if(data.type === "content") {
|
||||
var iframe = this.iframeForComponent(component);
|
||||
var width = data.width;
|
||||
var height = data.height;
|
||||
iframe.width = width;
|
||||
iframe.height = height;
|
||||
|
||||
setSize(iframe, data);
|
||||
} else {
|
||||
var container = document.getElementById("component-" + component.uuid);
|
||||
if(container) {
|
||||
// in the case of Modals, sometimes they may be "active" because they were so in another session,
|
||||
// but no longer actually visible. So check to make sure the container exists
|
||||
setSize(container, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('componentManager', ComponentManager);
|
||||
angular.module('app').service('componentManager', ComponentManager);
|
||||
|
||||
@@ -158,4 +158,4 @@ class DBManager {
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('dbManager', DBManager);
|
||||
angular.module('app').service('dbManager', DBManager);
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
|
||||
class DesktopManager {
|
||||
|
||||
constructor($rootScope, modelManager, authManager, passcodeManager) {
|
||||
constructor($rootScope, $timeout, modelManager, syncManager, authManager, passcodeManager) {
|
||||
this.passcodeManager = passcodeManager;
|
||||
this.modelManager = modelManager;
|
||||
this.authManager = authManager;
|
||||
this.syncManager = syncManager;
|
||||
this.$rootScope = $rootScope;
|
||||
this.timeout = $timeout;
|
||||
this.updateObservers = [];
|
||||
|
||||
this.isDesktop = isDesktopApplication();
|
||||
|
||||
$rootScope.$on("initial-data-loaded", () => {
|
||||
this.dataLoaded = true;
|
||||
@@ -22,6 +27,84 @@ class DesktopManager {
|
||||
})
|
||||
}
|
||||
|
||||
getApplicationDataPath() {
|
||||
console.assert(this.applicationDataPath, "applicationDataPath is null");
|
||||
return this.applicationDataPath;
|
||||
}
|
||||
|
||||
/* Sending a component in its raw state is really slow for the desktop app */
|
||||
convertComponentForTransmission(component) {
|
||||
return new ItemParams(component).paramsForExportFile(true);
|
||||
}
|
||||
|
||||
// All `components` should be installed
|
||||
syncComponentsInstallation(components) {
|
||||
if(!this.isDesktop) return;
|
||||
|
||||
var data = components.map((component) => {
|
||||
return this.convertComponentForTransmission(component);
|
||||
})
|
||||
this.installationSyncHandler(data);
|
||||
}
|
||||
|
||||
installComponent(component) {
|
||||
this.installComponentHandler(this.convertComponentForTransmission(component));
|
||||
}
|
||||
|
||||
registerUpdateObserver(callback) {
|
||||
var observer = {id: Math.random, callback: callback};
|
||||
this.updateObservers.push(observer);
|
||||
return observer;
|
||||
}
|
||||
|
||||
deregisterUpdateObserver(observer) {
|
||||
_.pull(this.updateObservers, observer);
|
||||
}
|
||||
|
||||
desktop_onComponentInstallationComplete(componentData, error) {
|
||||
console.log("Web|Component Installation/Update Complete", componentData, error);
|
||||
|
||||
// Desktop is only allowed to change these keys:
|
||||
let permissableKeys = ["package_info", "local_url"];
|
||||
var component = this.modelManager.findItem(componentData.uuid);
|
||||
|
||||
if(!component) {
|
||||
console.error("desktop_onComponentInstallationComplete component is null for uuid", componentData.uuid);
|
||||
return;
|
||||
}
|
||||
|
||||
if(error) {
|
||||
component.setAppDataItem("installError", error);
|
||||
} else {
|
||||
for(var key of permissableKeys) {
|
||||
component[key] = componentData.content[key];
|
||||
}
|
||||
this.modelManager.notifySyncObserversOfModels([component], ModelManager.MappingSourceDesktopInstalled);
|
||||
component.setAppDataItem("installError", null);
|
||||
}
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync("onComponentInstallationComplete");
|
||||
|
||||
this.timeout(() => {
|
||||
for(var observer of this.updateObservers) {
|
||||
observer.callback(component);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* Used to resolve "sn://" */
|
||||
desktop_setApplicationDataPath(path) {
|
||||
this.applicationDataPath = path;
|
||||
}
|
||||
|
||||
desktop_setComponentInstallationSyncHandler(handler) {
|
||||
this.installationSyncHandler = handler;
|
||||
}
|
||||
|
||||
desktop_setInstallComponentHandler(handler) {
|
||||
this.installComponentHandler = handler;
|
||||
}
|
||||
|
||||
desktop_setInitialDataLoadHandler(handler) {
|
||||
this.dataLoadHandler = handler;
|
||||
if(this.dataLoaded) {
|
||||
@@ -56,4 +139,4 @@ class DesktopManager {
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('desktopManager', DesktopManager);
|
||||
angular.module('app').service('desktopManager', DesktopManager);
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
class ContextualExtensionsMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/contextual-menu.html";
|
||||
this.scope = {
|
||||
item: "="
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, modelManager, extensionManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.renderData = {};
|
||||
|
||||
$scope.extensions = _.map(extensionManager.extensionsInContextOfItem($scope.item), function(ext){
|
||||
// why are we cloning deep? commenting out because we want original reference so that extension.hide is saved between menu opens
|
||||
// return _.cloneDeep(ext);
|
||||
return ext;
|
||||
});
|
||||
|
||||
for(let ext of $scope.extensions) {
|
||||
ext.loading = true;
|
||||
extensionManager.loadExtensionInContextOfItem(ext, $scope.item, function(scopedExtension) {
|
||||
ext.loading = false;
|
||||
})
|
||||
}
|
||||
|
||||
$scope.executeAction = function(action, extension, parentAction) {
|
||||
if(!$scope.isActionEnabled(action, extension)) {
|
||||
alert("This action requires " + action.access_type + " access to this note. You can change this setting in the Extensions menu on the bottom of the app.");
|
||||
return;
|
||||
}
|
||||
if(action.verb == "nested") {
|
||||
action.showNestedActions = !action.showNestedActions;
|
||||
return;
|
||||
}
|
||||
action.running = true;
|
||||
extensionManager.executeAction(action, extension, $scope.item, function(response){
|
||||
action.running = false;
|
||||
$scope.handleActionResponse(action, response);
|
||||
|
||||
// reload extension actions
|
||||
extensionManager.loadExtensionInContextOfItem(extension, $scope.item, function(ext){
|
||||
// keep nested state
|
||||
if(parentAction) {
|
||||
var matchingAction = _.find(ext.actions, {label: parentAction.label});
|
||||
matchingAction.showNestedActions = true;
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
$scope.handleActionResponse = function(action, response) {
|
||||
switch (action.verb) {
|
||||
case "render": {
|
||||
var item = response.item;
|
||||
if(item.content_type == "Note") {
|
||||
$scope.renderData.title = item.title;
|
||||
$scope.renderData.text = item.text;
|
||||
$scope.renderData.showRenderModal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.isActionEnabled = function(action, extension) {
|
||||
if(action.access_type) {
|
||||
var extEncryptedAccess = extension.encrypted;
|
||||
if(action.access_type == "decrypted" && extEncryptedAccess) {
|
||||
return false;
|
||||
} else if(action.access_type == "encrypted" && !extEncryptedAccess) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
$scope.accessTypeForExtension = function(extension) {
|
||||
return extension.encrypted ? "encrypted" : "decrypted";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('contextualExtensionsMenu', () => new ContextualExtensionsMenu);
|
||||
@@ -1,30 +0,0 @@
|
||||
class EditorMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/editor-menu.html";
|
||||
this.scope = {
|
||||
callback: "&",
|
||||
selectedEditor: "="
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, componentManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {};
|
||||
|
||||
$scope.editors = componentManager.componentsForArea("editor-editor");
|
||||
|
||||
$scope.selectEditor = function($event, editor) {
|
||||
if(editor) {
|
||||
editor.conflict_of = null; // clear conflict if applicable
|
||||
}
|
||||
$scope.callback()(editor);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('editorMenu', () => new EditorMenu);
|
||||
@@ -1,225 +0,0 @@
|
||||
class GlobalExtensionsMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/global-extensions-menu.html";
|
||||
this.scope = {
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, extensionManager, syncManager, modelManager, themeManager, componentManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {};
|
||||
|
||||
$scope.extensionManager = extensionManager;
|
||||
$scope.themeManager = themeManager;
|
||||
$scope.componentManager = componentManager;
|
||||
|
||||
$scope.serverExtensions = modelManager.itemsForContentType("SF|Extension");
|
||||
|
||||
$scope.selectedAction = function(action, extension) {
|
||||
extensionManager.executeAction(action, extension, null, function(response){
|
||||
if(response && response.error) {
|
||||
action.error = true;
|
||||
alert("There was an error performing this action. Please try again.");
|
||||
} else {
|
||||
action.error = false;
|
||||
syncManager.sync(null);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$scope.changeExtensionEncryptionFormat = function(encrypted, extension) {
|
||||
extension.encrypted = encrypted;
|
||||
extension.setDirty(true);
|
||||
syncManager.sync();
|
||||
}
|
||||
|
||||
$scope.deleteActionExtension = function(extension) {
|
||||
if(confirm("Are you sure you want to delete this extension?")) {
|
||||
extensionManager.deleteExtension(extension);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.reloadExtensionsPressed = function() {
|
||||
if(confirm("For your security, reloading extensions will disable any currently enabled repeat actions.")) {
|
||||
extensionManager.refreshExtensionsFromServer();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.deleteTheme = function(theme) {
|
||||
if(confirm("Are you sure you want to delete this theme?")) {
|
||||
themeManager.deactivateTheme(theme);
|
||||
modelManager.setItemToBeDeleted(theme);
|
||||
syncManager.sync();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.renameExtension = function(extension) {
|
||||
extension.tempName = extension.name;
|
||||
extension.rename = true;
|
||||
}
|
||||
|
||||
$scope.submitExtensionRename = function(extension) {
|
||||
extension.name = extension.tempName;
|
||||
extension.tempName = null;
|
||||
extension.setDirty(true);
|
||||
extension.rename = false;
|
||||
syncManager.sync();
|
||||
}
|
||||
|
||||
$scope.clickedExtension = function(extension) {
|
||||
if(extension.rename) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($scope.currentlyExpandedExtension && $scope.currentlyExpandedExtension !== extension) {
|
||||
$scope.currentlyExpandedExtension.showDetails = false;
|
||||
$scope.currentlyExpandedExtension.rename = false;
|
||||
}
|
||||
|
||||
extension.showDetails = !extension.showDetails;
|
||||
|
||||
if(extension.showDetails) {
|
||||
$scope.currentlyExpandedExtension = extension;
|
||||
}
|
||||
}
|
||||
|
||||
// Server extensions
|
||||
|
||||
$scope.deleteServerExt = function(ext) {
|
||||
if(confirm("Are you sure you want to delete and disable this extension?")) {
|
||||
_.remove($scope.serverExtensions, {uuid: ext.uuid});
|
||||
modelManager.setItemToBeDeleted(ext);
|
||||
syncManager.sync();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.nameForServerExtension = function(ext) {
|
||||
var url = ext.url;
|
||||
if(!url) {
|
||||
return "Invalid Extension";
|
||||
}
|
||||
if(url.includes("gdrive")) {
|
||||
return "Google Drive Sync";
|
||||
} else if(url.includes("file_attacher")) {
|
||||
return "File Attacher";
|
||||
} else if(url.includes("onedrive")) {
|
||||
return "OneDrive Sync";
|
||||
} else if(url.includes("backup.email_archive")) {
|
||||
return "Daily Email Backups";
|
||||
} else if(url.includes("dropbox")) {
|
||||
return "Dropbox Sync";
|
||||
} else if(url.includes("revisions")) {
|
||||
return "Revision History";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Components
|
||||
|
||||
$scope.revokePermissions = function(component) {
|
||||
component.permissions = [];
|
||||
component.setDirty(true);
|
||||
syncManager.sync();
|
||||
}
|
||||
|
||||
$scope.deleteComponent = function(component) {
|
||||
if(confirm("Are you sure you want to delete this component?")) {
|
||||
componentManager.deleteComponent(component);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.makeEditorDefault = function(component) {
|
||||
var currentDefault = componentManager.componentsForArea("editor-editor").filter((e) => {return e.isDefaultEditor()})[0];
|
||||
if(currentDefault) {
|
||||
currentDefault.setAppDataItem("defaultEditor", false);
|
||||
currentDefault.setDirty(true);
|
||||
}
|
||||
component.setAppDataItem("defaultEditor", true);
|
||||
component.setDirty(true);
|
||||
syncManager.sync();
|
||||
}
|
||||
|
||||
$scope.removeEditorDefault = function(component) {
|
||||
component.setAppDataItem("defaultEditor", false);
|
||||
component.setDirty(true);
|
||||
syncManager.sync();
|
||||
}
|
||||
|
||||
// Installation
|
||||
|
||||
$scope.submitInstallLink = function() {
|
||||
|
||||
var fullLink = $scope.formData.installLink;
|
||||
if(!fullLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
var completion = function() {
|
||||
$scope.formData.installLink = "";
|
||||
$scope.formData.successfullyInstalled = true;
|
||||
}
|
||||
|
||||
var links = fullLink.split(",");
|
||||
for(var link of links) {
|
||||
var type = getParameterByName("type", link);
|
||||
|
||||
if(type == "sf") {
|
||||
$scope.handleSyncAdapterLink(link, completion);
|
||||
} else if(type == "editor") {
|
||||
$scope.handleEditorLink(link, completion);
|
||||
} else if(link.indexOf(".css") != -1 || type == "theme") {
|
||||
$scope.handleThemeLink(link, completion);
|
||||
} else if(type == "component") {
|
||||
$scope.handleComponentLink(link, completion);
|
||||
}
|
||||
|
||||
else {
|
||||
$scope.handleActionLink(link, completion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.handleSyncAdapterLink = function(link, completion) {
|
||||
var params = parametersFromURL(link);
|
||||
params["url"] = link;
|
||||
var ext = new SyncAdapter({content: params});
|
||||
ext.setDirty(true);
|
||||
|
||||
modelManager.addItem(ext);
|
||||
syncManager.sync();
|
||||
$scope.serverExtensions.push(ext);
|
||||
completion();
|
||||
}
|
||||
|
||||
$scope.handleThemeLink = function(link, completion) {
|
||||
themeManager.submitTheme(link);
|
||||
completion();
|
||||
}
|
||||
|
||||
$scope.handleComponentLink = function(link, completion) {
|
||||
componentManager.installComponent(link);
|
||||
completion();
|
||||
}
|
||||
|
||||
$scope.handleActionLink = function(link, completion) {
|
||||
if(link) {
|
||||
extensionManager.addExtension(link, function(response){
|
||||
if(!response) {
|
||||
alert("Unable to register this extension. Make sure the link is valid and try again.");
|
||||
} else {
|
||||
completion();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('globalExtensionsMenu', () => new GlobalExtensionsMenu);
|
||||
@@ -1,70 +0,0 @@
|
||||
class PermissionsModal {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/permissions-modal.html";
|
||||
this.scope = {
|
||||
show: "=",
|
||||
component: "=",
|
||||
permissions: "=",
|
||||
callback: "="
|
||||
};
|
||||
}
|
||||
|
||||
link($scope, el, attrs) {
|
||||
|
||||
$scope.dismiss = function() {
|
||||
el.remove();
|
||||
}
|
||||
|
||||
$scope.accept = function() {
|
||||
$scope.callback(true);
|
||||
$scope.dismiss();
|
||||
}
|
||||
|
||||
$scope.deny = function() {
|
||||
$scope.callback(false);
|
||||
$scope.dismiss();
|
||||
}
|
||||
|
||||
$scope.formattedPermissions = $scope.permissions.map(function(permission){
|
||||
if(permission.name === "stream-items") {
|
||||
var title = "Access to ";
|
||||
var types = permission.content_types.map(function(type){
|
||||
return (type + "s").toLowerCase();
|
||||
})
|
||||
var typesString = "";
|
||||
var separator = ", ";
|
||||
|
||||
for(var i = 0;i < types.length;i++) {
|
||||
var type = types[i];
|
||||
if(i == 0) {
|
||||
// first element
|
||||
typesString = typesString + type;
|
||||
} else if(i == types.length - 1) {
|
||||
// last element
|
||||
if(types.length > 2) {
|
||||
typesString += separator + "and " + typesString;
|
||||
} else if(types.length == 2) {
|
||||
typesString = typesString + " and " + type;
|
||||
}
|
||||
} else {
|
||||
typesString += separator + type;
|
||||
}
|
||||
}
|
||||
|
||||
return title + typesString;
|
||||
} else if(permission.name === "stream-context-item") {
|
||||
var mapping = {
|
||||
"editor-stack" : "working note",
|
||||
"note-tags" : "working note",
|
||||
"editor-editor": "working note"
|
||||
}
|
||||
return "Access to " + mapping[$scope.component.area];
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('permissionsModal', () => new PermissionsModal);
|
||||
@@ -1,337 +0,0 @@
|
||||
class ExtensionManager {
|
||||
|
||||
constructor(httpManager, modelManager, authManager, syncManager, storageManager) {
|
||||
this.httpManager = httpManager;
|
||||
this.modelManager = modelManager;
|
||||
this.authManager = authManager;
|
||||
this.enabledRepeatActionUrls = JSON.parse(storageManager.getItem("enabledRepeatActionUrls")) || [];
|
||||
this.syncManager = syncManager;
|
||||
this.storageManager = storageManager;
|
||||
|
||||
modelManager.addItemSyncObserver("extensionManager", "Extension", function(allItems, validItems, deletedItems){
|
||||
for (var ext of validItems) {
|
||||
for (var action of ext.actions) {
|
||||
if(_.includes(this.enabledRepeatActionUrls, action.url)) {
|
||||
this.enableRepeatAction(action, ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
get extensions() {
|
||||
return this.modelManager.extensions;
|
||||
}
|
||||
|
||||
extensionsInContextOfItem(item) {
|
||||
return this.extensions.filter(function(ext){
|
||||
return _.includes(ext.supported_types, item.content_type) || ext.actionsWithContextForItem(item).length > 0;
|
||||
})
|
||||
}
|
||||
|
||||
actionWithURL(url) {
|
||||
for (var extension of this.extensions) {
|
||||
return _.find(extension.actions, {url: url})
|
||||
}
|
||||
}
|
||||
|
||||
addExtension(url, callback) {
|
||||
this.retrieveExtensionFromServer(url, callback);
|
||||
}
|
||||
|
||||
deleteExtension(extension) {
|
||||
for(var action of extension.actions) {
|
||||
if(action.repeat_mode) {
|
||||
if(this.isRepeatActionEnabled(action)) {
|
||||
this.disableRepeatAction(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.modelManager.setItemToBeDeleted(extension);
|
||||
this.syncManager.sync(null);
|
||||
}
|
||||
|
||||
/*
|
||||
Loads an extension in the context of a certain item. The server then has the chance to respond with actions that are
|
||||
relevant just to this item. The response extension is not saved, just displayed as a one-time thing.
|
||||
*/
|
||||
loadExtensionInContextOfItem(extension, item, callback) {
|
||||
|
||||
this.httpManager.getAbsolute(extension.url, {content_type: item.content_type, item_uuid: item.uuid}, function(response){
|
||||
this.updateExtensionFromRemoteResponse(extension, response);
|
||||
callback && callback(extension);
|
||||
}.bind(this), function(response){
|
||||
console.log("Error loading extension", response);
|
||||
if(callback) {
|
||||
callback(null);
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
/*
|
||||
Registers new extension and saves it to user's account
|
||||
*/
|
||||
retrieveExtensionFromServer(url, callback) {
|
||||
this.httpManager.getAbsolute(url, {}, function(response){
|
||||
if(typeof response !== 'object') {
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
var ext = this.handleExtensionLoadExternalResponseItem(url, response);
|
||||
if(callback) {
|
||||
callback(ext);
|
||||
}
|
||||
}.bind(this), function(response){
|
||||
console.error("Error registering extension", response);
|
||||
callback(null);
|
||||
})
|
||||
}
|
||||
|
||||
handleExtensionLoadExternalResponseItem(url, externalResponseItem) {
|
||||
// Don't allow remote response to set these flags
|
||||
delete externalResponseItem.encrypted;
|
||||
delete externalResponseItem.uuid;
|
||||
|
||||
var extension = _.find(this.extensions, {url: url});
|
||||
if(extension) {
|
||||
this.updateExtensionFromRemoteResponse(extension, externalResponseItem);
|
||||
} else {
|
||||
extension = new Extension(externalResponseItem);
|
||||
extension.url = url;
|
||||
extension.setDirty(true);
|
||||
this.modelManager.addItem(extension);
|
||||
this.syncManager.sync(null);
|
||||
}
|
||||
|
||||
return extension;
|
||||
}
|
||||
|
||||
updateExtensionFromRemoteResponse(extension, response) {
|
||||
if(response.description) {
|
||||
extension.description = response.description;
|
||||
}
|
||||
if(response.supported_types) {
|
||||
extension.supported_types = response.supported_types;
|
||||
}
|
||||
|
||||
if(response.actions) {
|
||||
extension.actions = response.actions.map(function(action){
|
||||
return new Action(action);
|
||||
})
|
||||
} else {
|
||||
extension.actions = [];
|
||||
}
|
||||
}
|
||||
|
||||
refreshExtensionsFromServer() {
|
||||
for (var url of this.enabledRepeatActionUrls) {
|
||||
var action = this.actionWithURL(url);
|
||||
if(action) {
|
||||
this.disableRepeatAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
for(var ext of this.extensions) {
|
||||
this.retrieveExtensionFromServer(ext.url, function(extension){
|
||||
extension.setDirty(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
executeAction(action, extension, item, callback) {
|
||||
|
||||
if(extension.encrypted && this.authManager.offline()) {
|
||||
alert("To send data encrypted, you must have an encryption key, and must therefore be signed in.");
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
|
||||
var customCallback = function(response) {
|
||||
action.running = false;
|
||||
callback(response);
|
||||
}
|
||||
|
||||
action.running = true;
|
||||
|
||||
switch (action.verb) {
|
||||
case "get": {
|
||||
|
||||
this.httpManager.getAbsolute(action.url, {}, function(response){
|
||||
action.error = false;
|
||||
var items = response.items || [response.item];
|
||||
EncryptionHelper.decryptMultipleItems(items, this.authManager.keys());
|
||||
items = this.modelManager.mapResponseItemsToLocalModels(items, ModelManager.MappingSourceRemoteActionRetrieved);
|
||||
for(var item of items) {
|
||||
item.setDirty(true);
|
||||
}
|
||||
this.syncManager.sync(null);
|
||||
customCallback({items: items});
|
||||
}.bind(this), function(response){
|
||||
action.error = true;
|
||||
customCallback(null);
|
||||
})
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "render": {
|
||||
|
||||
this.httpManager.getAbsolute(action.url, {}, function(response){
|
||||
action.error = false;
|
||||
EncryptionHelper.decryptItem(response.item, this.authManager.keys());
|
||||
var item = this.modelManager.createItem(response.item);
|
||||
customCallback({item: item});
|
||||
|
||||
}.bind(this), function(response){
|
||||
action.error = true;
|
||||
customCallback(null);
|
||||
})
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "show": {
|
||||
var win = window.open(action.url, '_blank');
|
||||
win.focus();
|
||||
customCallback();
|
||||
break;
|
||||
}
|
||||
|
||||
case "post": {
|
||||
var params = {};
|
||||
|
||||
if(action.all) {
|
||||
var items = this.modelManager.allItemsMatchingTypes(action.content_types);
|
||||
params.items = items.map(function(item){
|
||||
var params = this.outgoingParamsForItem(item, extension);
|
||||
return params;
|
||||
}.bind(this))
|
||||
|
||||
} else {
|
||||
params.items = [this.outgoingParamsForItem(item, extension)];
|
||||
}
|
||||
|
||||
this.performPost(action, extension, params, function(response){
|
||||
customCallback(response);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
action.lastExecuted = new Date();
|
||||
}
|
||||
|
||||
isRepeatActionEnabled(action) {
|
||||
return _.includes(this.enabledRepeatActionUrls, action.url);
|
||||
}
|
||||
|
||||
disableRepeatAction(action, extension) {
|
||||
_.pull(this.enabledRepeatActionUrls, action.url);
|
||||
this.storageManager.setItem("enabledRepeatActionUrls", JSON.stringify(this.enabledRepeatActionUrls));
|
||||
this.modelManager.removeItemChangeObserver(action.url);
|
||||
|
||||
console.assert(this.isRepeatActionEnabled(action) == false);
|
||||
}
|
||||
|
||||
enableRepeatAction(action, extension) {
|
||||
if(!_.find(this.enabledRepeatActionUrls, action.url)) {
|
||||
this.enabledRepeatActionUrls.push(action.url);
|
||||
this.storageManager.setItem("enabledRepeatActionUrls", JSON.stringify(this.enabledRepeatActionUrls));
|
||||
}
|
||||
|
||||
if(action.repeat_mode) {
|
||||
|
||||
if(action.repeat_mode == "watch") {
|
||||
this.modelManager.addItemChangeObserver(action.url, action.content_types, function(changedItems){
|
||||
this.triggerWatchAction(action, extension, changedItems);
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
if(action.repeat_mode == "loop") {
|
||||
// todo
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
queueAction(action, extension, delay, changedItems) {
|
||||
this.actionQueue = this.actionQueue || [];
|
||||
if(_.find(this.actionQueue, {url: action.url})) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionQueue.push(action);
|
||||
|
||||
setTimeout(function () {
|
||||
this.triggerWatchAction(action, extension, changedItems);
|
||||
_.pull(this.actionQueue, action);
|
||||
}.bind(this), delay * 1000);
|
||||
}
|
||||
|
||||
triggerWatchAction(action, extension, changedItems) {
|
||||
if(action.repeat_timeout > 0) {
|
||||
var lastExecuted = action.lastExecuted;
|
||||
var diffInSeconds = (new Date() - lastExecuted)/1000;
|
||||
if(diffInSeconds < action.repeat_timeout) {
|
||||
var delay = action.repeat_timeout - diffInSeconds;
|
||||
this.queueAction(action, extension, delay, changedItems);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
action.lastExecuted = new Date();
|
||||
|
||||
if(action.verb == "post") {
|
||||
var params = {};
|
||||
params.items = changedItems.map(function(item){
|
||||
var params = this.outgoingParamsForItem(item, extension);
|
||||
return params;
|
||||
}.bind(this))
|
||||
|
||||
action.running = true;
|
||||
this.performPost(action, extension, params, function(){
|
||||
action.running = false;
|
||||
});
|
||||
} else {
|
||||
// todo
|
||||
}
|
||||
}
|
||||
|
||||
outgoingParamsForItem(item, extension) {
|
||||
var keys = this.authManager.keys();
|
||||
if(!extension.encrypted) {
|
||||
keys = null;
|
||||
}
|
||||
var itemParams = new ItemParams(item, keys, this.authManager.protocolVersion());
|
||||
return itemParams.paramsForExtension();
|
||||
}
|
||||
|
||||
performPost(action, extension, params, callback) {
|
||||
|
||||
if(extension.encrypted) {
|
||||
params.auth_params = this.authManager.getAuthParams();
|
||||
}
|
||||
|
||||
this.httpManager.postAbsolute(action.url, params, function(response){
|
||||
action.error = false;
|
||||
if(callback) {
|
||||
callback(response);
|
||||
}
|
||||
}.bind(this), function(response){
|
||||
action.error = true;
|
||||
console.log("Action error response:", response);
|
||||
if(callback) {
|
||||
callback({error: "Request error"});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('extensionManager', ExtensionManager);
|
||||
@@ -77,4 +77,4 @@ class HttpManager {
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('httpManager', HttpManager);
|
||||
angular.module('app').service('httpManager', HttpManager);
|
||||
|
||||
@@ -50,7 +50,7 @@ class MigrationManager {
|
||||
this.modelManager.setItemToBeDeleted(editor);
|
||||
}
|
||||
|
||||
this.syncManager.sync();
|
||||
this.syncManager.sync("addEditorToComponentMigrator");
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -58,4 +58,4 @@ class MigrationManager {
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('migrationManager', MigrationManager);
|
||||
angular.module('app').service('migrationManager', MigrationManager);
|
||||
|
||||
@@ -3,8 +3,10 @@ class ModelManager {
|
||||
constructor(storageManager) {
|
||||
ModelManager.MappingSourceRemoteRetrieved = "MappingSourceRemoteRetrieved";
|
||||
ModelManager.MappingSourceRemoteSaved = "MappingSourceRemoteSaved";
|
||||
ModelManager.MappingSourceLocalSaved = "MappingSourceLocalSaved";
|
||||
ModelManager.MappingSourceLocalRetrieved = "MappingSourceLocalRetrieved";
|
||||
ModelManager.MappingSourceComponentRetrieved = "MappingSourceComponentRetrieved";
|
||||
ModelManager.MappingSourceDesktopInstalled = "MappingSourceDesktopInstalled"; // When a component is installed by the desktop and some of its values change
|
||||
ModelManager.MappingSourceRemoteActionRetrieved = "MappingSourceRemoteActionRetrieved"; /* aciton-based Extensions like note history */
|
||||
ModelManager.MappingSourceFileImport = "MappingSourceFileImport";
|
||||
|
||||
@@ -18,7 +20,7 @@ class ModelManager {
|
||||
this._extensions = [];
|
||||
this.acceptableContentTypes = [
|
||||
"Note", "Tag", "Extension", "SN|Editor", "SN|Theme",
|
||||
"SN|Component", "SF|Extension", "SN|UserPreferences"
|
||||
"SN|Component", "SF|Extension", "SN|UserPreferences", "SF|MFA"
|
||||
];
|
||||
}
|
||||
|
||||
@@ -52,6 +54,8 @@ class ModelManager {
|
||||
|
||||
this.informModelsOfUUIDChangeForItem(newItem, item.uuid, newItem.uuid);
|
||||
|
||||
console.log(item.uuid, "-->", newItem.uuid);
|
||||
|
||||
var block = () => {
|
||||
this.addItem(newItem);
|
||||
newItem.setDirty(true);
|
||||
@@ -60,9 +64,10 @@ class ModelManager {
|
||||
}
|
||||
|
||||
if(removeOriginal) {
|
||||
this.removeItemLocally(item, function(){
|
||||
block();
|
||||
});
|
||||
// Set to deleted, then run through mapping function so that observers can be notified
|
||||
item.deleted = true;
|
||||
this.mapResponseItemsToLocalModels([item], ModelManager.MappingSourceLocalSaved);
|
||||
block();
|
||||
} else {
|
||||
block();
|
||||
}
|
||||
@@ -79,13 +84,13 @@ class ModelManager {
|
||||
}
|
||||
|
||||
allItemsMatchingTypes(contentTypes) {
|
||||
return this.items.filter(function(item){
|
||||
return this.allItems.filter(function(item){
|
||||
return (_.includes(contentTypes, item.content_type) || _.includes(contentTypes, "*")) && !item.dummy;
|
||||
})
|
||||
}
|
||||
|
||||
itemsForContentType(contentType) {
|
||||
return this.items.filter(function(item){
|
||||
return this.allItems.filter(function(item){
|
||||
return item.content_type == contentType;
|
||||
});
|
||||
}
|
||||
@@ -103,6 +108,10 @@ class ModelManager {
|
||||
return tag;
|
||||
}
|
||||
|
||||
didSyncModelsOffline(items) {
|
||||
this.notifySyncObserversOfModels(items, ModelManager.MappingSourceLocalSaved);
|
||||
}
|
||||
|
||||
mapResponseItemsToLocalModels(items, source) {
|
||||
return this.mapResponseItemsToLocalModelsOmittingFields(items, null, source);
|
||||
}
|
||||
@@ -139,7 +148,8 @@ class ModelManager {
|
||||
continue;
|
||||
}
|
||||
|
||||
var unknownContentType = !_.includes(this.acceptableContentTypes, json_obj["content_type"]);
|
||||
let contentType = json_obj["content_type"] || (item && item.content_type);
|
||||
var unknownContentType = !_.includes(this.acceptableContentTypes, contentType);
|
||||
if(json_obj.deleted == true || unknownContentType) {
|
||||
if(item && !unknownContentType) {
|
||||
modelsToNotifyObserversOf.push(item);
|
||||
@@ -149,7 +159,7 @@ class ModelManager {
|
||||
}
|
||||
|
||||
if(!item) {
|
||||
item = this.createItem(json_obj);
|
||||
item = this.createItem(json_obj, true);
|
||||
}
|
||||
|
||||
this.addItem(item);
|
||||
@@ -172,6 +182,7 @@ class ModelManager {
|
||||
return models;
|
||||
}
|
||||
|
||||
/* Note that this function is public, and can also be called manually (desktopManager uses it) */
|
||||
notifySyncObserversOfModels(models, source) {
|
||||
for(var observer of this.itemSyncObservers) {
|
||||
var allRelevantItems = models.filter(function(item){return item.content_type == observer.type || observer.type == "*"});
|
||||
@@ -202,7 +213,7 @@ class ModelManager {
|
||||
}
|
||||
}
|
||||
|
||||
createItem(json_obj) {
|
||||
createItem(json_obj, dontNotifyObservers) {
|
||||
var item;
|
||||
if(json_obj.content_type == "Note") {
|
||||
item = new Note(json_obj);
|
||||
@@ -217,13 +228,24 @@ class ModelManager {
|
||||
} else if(json_obj.content_type == "SN|Component") {
|
||||
item = new Component(json_obj);
|
||||
} else if(json_obj.content_type == "SF|Extension") {
|
||||
item = new SyncAdapter(json_obj);
|
||||
item = new ServerExtension(json_obj);
|
||||
} else if(json_obj.content_type == "SF|MFA") {
|
||||
item = new Mfa(json_obj);
|
||||
}
|
||||
|
||||
else {
|
||||
item = new Item(json_obj);
|
||||
}
|
||||
|
||||
// Some observers would be interested to know when an an item is locally created
|
||||
// If we don't send this out, these observers would have to wait until MappingSourceRemoteSaved
|
||||
// to hear about it, but sometimes, RemoveSaved is explicitly ignored by the observer to avoid
|
||||
// recursive callbacks. See componentManager's syncObserver callback.
|
||||
// dontNotifyObservers is currently only set true by modelManagers mapResponseItemsToLocalModels
|
||||
if(!dontNotifyObservers) {
|
||||
this.notifySyncObserversOfModels([item], ModelManager.MappingSourceLocalSaved);
|
||||
}
|
||||
|
||||
item.addObserver(this, function(changedItem){
|
||||
this.notifyItemChangeObserversOfModels([changedItem]);
|
||||
}.bind(this));
|
||||
@@ -232,7 +254,7 @@ class ModelManager {
|
||||
}
|
||||
|
||||
createDuplicateItem(itemResponse, sourceItem) {
|
||||
var dup = this.createItem(itemResponse);
|
||||
var dup = this.createItem(itemResponse, true);
|
||||
this.resolveReferencesForItem(dup);
|
||||
return dup;
|
||||
}
|
||||
@@ -405,6 +427,25 @@ class ModelManager {
|
||||
|
||||
return JSON.stringify(data, null, 2 /* pretty print */);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Misc
|
||||
*/
|
||||
|
||||
humanReadableDisplayForContentType(contentType) {
|
||||
return {
|
||||
"Note" : "note",
|
||||
"Tag" : "tag",
|
||||
"Extension" : "action-based extension",
|
||||
"SN|Component" : "component",
|
||||
"SN|Editor" : "editor",
|
||||
"SN|Theme" : "theme",
|
||||
"SF|Extension" : "server extension",
|
||||
"SF|MFA" : "two-factor authentication setting"
|
||||
}[contentType];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('modelManager', ModelManager);
|
||||
angular.module('app').service('modelManager', ModelManager);
|
||||
|
||||
92
app/assets/javascripts/app/services/nativeExtManager.js
Normal file
92
app/assets/javascripts/app/services/nativeExtManager.js
Normal file
@@ -0,0 +1,92 @@
|
||||
/* A class for handling installation of system extensions */
|
||||
|
||||
class NativeExtManager {
|
||||
|
||||
constructor(modelManager, syncManager, singletonManager) {
|
||||
this.modelManager = modelManager;
|
||||
this.syncManager = syncManager;
|
||||
this.singletonManager = singletonManager;
|
||||
|
||||
this.extensionsIdentifier = "org.standardnotes.extensions-manager";
|
||||
this.systemExtensions = [];
|
||||
|
||||
this.resolveExtensionsManager();
|
||||
}
|
||||
|
||||
isSystemExtension(extension) {
|
||||
return this.systemExtensions.includes(extension.uuid);
|
||||
}
|
||||
|
||||
resolveExtensionsManager() {
|
||||
|
||||
this.singletonManager.registerSingleton({content_type: "SN|Component", package_info: {identifier: this.extensionsIdentifier}}, (resolvedSingleton) => {
|
||||
// Resolved Singleton
|
||||
this.systemExtensions.push(resolvedSingleton.uuid);
|
||||
|
||||
var needsSync = false;
|
||||
if(isDesktopApplication()) {
|
||||
if(!resolvedSingleton.local_url) {
|
||||
resolvedSingleton.local_url = window._extensions_manager_location;
|
||||
needsSync = true;
|
||||
}
|
||||
} else {
|
||||
if(!resolvedSingleton.hosted_url) {
|
||||
resolvedSingleton.hosted_url = window._extensions_manager_location;
|
||||
needsSync = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(needsSync) {
|
||||
resolvedSingleton.setDirty(true);
|
||||
this.syncManager.sync("resolveExtensionsManager");
|
||||
}
|
||||
}, (valueCallback) => {
|
||||
// Safe to create. Create and return object.
|
||||
let url = window._extensions_manager_location;
|
||||
console.log("Installing Extensions Manager from URL", url);
|
||||
if(!url) {
|
||||
console.error("window._extensions_manager_location must be set.");
|
||||
return;
|
||||
}
|
||||
|
||||
let packageInfo = {
|
||||
name: "Extensions",
|
||||
identifier: this.extensionsIdentifier
|
||||
}
|
||||
|
||||
var item = {
|
||||
content_type: "SN|Component",
|
||||
content: {
|
||||
name: packageInfo.name,
|
||||
area: "rooms",
|
||||
package_info: packageInfo,
|
||||
permissions: [
|
||||
{
|
||||
name: "stream-items",
|
||||
content_types: ["SN|Component", "SN|Theme", "SF|Extension", "Extension", "SF|MFA", "SN|Editor"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if(isDesktopApplication()) {
|
||||
item.content.local_url = window._extensions_manager_location;
|
||||
} else {
|
||||
item.content.hosted_url = window._extensions_manager_location;
|
||||
}
|
||||
|
||||
var component = this.modelManager.createItem(item);
|
||||
this.modelManager.addItem(component);
|
||||
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync("resolveExtensionsManager createNew");
|
||||
|
||||
this.systemExtensions.push(component.uuid);
|
||||
|
||||
valueCallback(component);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').service('nativeExtManager', NativeExtManager);
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('app.frontend')
|
||||
angular.module('app')
|
||||
.provider('passcodeManager', function () {
|
||||
|
||||
this.$get = function($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) {
|
||||
@@ -41,7 +41,7 @@ angular.module('app.frontend')
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.setPasscode = function(passcode, callback) {
|
||||
this.setPasscode = (passcode, callback) => {
|
||||
var cost = Neeto.crypto.defaultPasswordGenerationCost();
|
||||
var salt = Neeto.crypto.generateRandomKey(512);
|
||||
var defaultParams = {pw_cost: cost, pw_salt: salt, version: "002"};
|
||||
@@ -60,6 +60,10 @@ angular.module('app.frontend')
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -70,7 +74,8 @@ angular.module('app.frontend')
|
||||
this.encryptLocalStorage = function(keys) {
|
||||
storageManager.setKeys(keys);
|
||||
// Switch to Ephemeral storage, wiping Fixed storage
|
||||
storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted);
|
||||
// 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 = function(keys) {
|
||||
|
||||
174
app/assets/javascripts/app/services/singletonManager.js
Normal file
174
app/assets/javascripts/app/services/singletonManager.js
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
The SingletonManager allows controllers to register an item as a singleton, which means only one instance of that model
|
||||
should exist, both on the server and on the client. When the SingletonManager detects multiple items matching the singleton predicate,
|
||||
the oldest ones will be deleted, leaving the newest ones.
|
||||
|
||||
We will treat the model most recently arrived from the server as the most recent one. The reason for this is, if you're offline,
|
||||
a singleton can be created, as in the case of UserPreferneces. Then when you sign in, you'll retrieve your actual user preferences.
|
||||
In that case, even though the offline singleton has a more recent updated_at, the server retreived value is the one we care more about.
|
||||
*/
|
||||
|
||||
class SingletonManager {
|
||||
|
||||
constructor($rootScope, modelManager) {
|
||||
this.$rootScope = $rootScope;
|
||||
this.modelManager = modelManager;
|
||||
this.singletonHandlers = [];
|
||||
|
||||
$rootScope.$on("initial-data-loaded", (event, data) => {
|
||||
this.resolveSingletons(modelManager.allItems, null, true);
|
||||
})
|
||||
|
||||
$rootScope.$on("sync:completed", (event, data) => {
|
||||
// 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.
|
||||
|
||||
// HOWEVER, by considering savedItems, we are now ruining everything, especially during sign in. A singleton can be created
|
||||
// offline, and upon sign in, will sync all items to the server, and by combining retrievedItems & savedItems, and only choosing
|
||||
// the latest, you are now resolving to the most recent one, which is in the savedItems list and not retrieved items, defeating
|
||||
// the whole purpose of this thing.
|
||||
|
||||
// Updated solution: resolveSingletons will now evaluate both of these arrays separately.
|
||||
this.resolveSingletons(data.retrievedItems, data.savedItems);
|
||||
})
|
||||
}
|
||||
|
||||
registerSingleton(predicate, resolveCallback, createBlock) {
|
||||
/*
|
||||
predicate: a key/value pair that specifies properties that should match in order for an item to be considered a predicate
|
||||
resolveCallback: called when one or more items are deleted and a new item becomes the reigning singleton
|
||||
createBlock: called when a sync is complete and no items are found. The createBlock should create the item and return it.
|
||||
*/
|
||||
this.singletonHandlers.push({
|
||||
predicate: predicate,
|
||||
resolutionCallback: resolveCallback,
|
||||
createBlock: createBlock
|
||||
});
|
||||
}
|
||||
|
||||
resolveSingletons(retrievedItems, savedItems, initialLoad) {
|
||||
retrievedItems = retrievedItems || [];
|
||||
savedItems = savedItems || [];
|
||||
|
||||
for(let singletonHandler of this.singletonHandlers) {
|
||||
var predicate = singletonHandler.predicate;
|
||||
let retrievedSingletonItems = this.filterItemsWithPredicate(retrievedItems, predicate);
|
||||
|
||||
// We only want to consider saved items count to see if it's more than 0, and do nothing else with it.
|
||||
// This way we know there was some action and things need to be resolved. The saved items will come up
|
||||
// in filterItemsWithPredicate(this.modelManager.allItems) and be deleted anyway
|
||||
let savedSingletonItemsCount = this.filterItemsWithPredicate(savedItems, predicate).length;
|
||||
|
||||
if(retrievedSingletonItems.length > 0 || savedSingletonItemsCount > 0) {
|
||||
/*
|
||||
Check local inventory and make sure only 1 similar item exists. If more than 1, delete oldest
|
||||
Note that this local inventory will also contain whatever is in retrievedItems.
|
||||
However, as stated in the header comment, retrievedItems take precendence over existing items,
|
||||
even if they have a lower updated_at value
|
||||
*/
|
||||
var allExtantItemsMatchingPredicate = this.filterItemsWithPredicate(this.modelManager.allItems, predicate);
|
||||
|
||||
/*
|
||||
If there are more than 1 matches, delete everything not in `retrievedSingletonItems`,
|
||||
then delete all but the latest in `retrievedSingletonItems`
|
||||
*/
|
||||
if(allExtantItemsMatchingPredicate.length >= 2) {
|
||||
|
||||
// Items that will be deleted
|
||||
var toDelete = [];
|
||||
// The item that will be chosen to be kept
|
||||
var winningItem, sorted;
|
||||
|
||||
if(retrievedSingletonItems.length > 0) {
|
||||
for(let extantItem of allExtantItemsMatchingPredicate) {
|
||||
if(!retrievedSingletonItems.includes(extantItem)) {
|
||||
// Delete it
|
||||
toDelete.push(extantItem);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort incoming singleton items by most recently updated first, then delete all the rest
|
||||
sorted = retrievedSingletonItems.sort((a, b) => {
|
||||
return a.updated_at < b.updated_at;
|
||||
})
|
||||
|
||||
} else {
|
||||
// We're in here because of savedItems
|
||||
// This can be the case if retrievedSingletonItems/retrievedItems length is 0, but savedSingletonItemsCount is non zero.
|
||||
// In this case, we want to sort by date and delete all but the most recent one
|
||||
sorted = allExtantItemsMatchingPredicate.sort((a, b) => {
|
||||
return a.updated_at < b.updated_at;
|
||||
});
|
||||
}
|
||||
|
||||
winningItem = sorted[0];
|
||||
|
||||
// Delete everything but the first one
|
||||
toDelete = toDelete.concat(sorted.slice(1, sorted.length));
|
||||
|
||||
for(var d of toDelete) {
|
||||
this.modelManager.setItemToBeDeleted(d);
|
||||
}
|
||||
|
||||
this.$rootScope.sync("resolveSingletons");
|
||||
|
||||
// Send remaining item to callback
|
||||
singletonHandler.singleton = winningItem;
|
||||
singletonHandler.resolutionCallback(winningItem);
|
||||
|
||||
} else if(allExtantItemsMatchingPredicate.length == 1) {
|
||||
if(!singletonHandler.singleton) {
|
||||
// Not yet notified interested parties of object
|
||||
var singleton = allExtantItemsMatchingPredicate[0];
|
||||
singletonHandler.singleton = singleton;
|
||||
singletonHandler.resolutionCallback(singleton);
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Retrieved items does not include any items of interest. If we don't have a singleton registered to this handler,
|
||||
// we need to create one. Only do this on actual sync completetions and not on initial data load. Because we want
|
||||
// to get the latest from the server before making the decision to create a new item
|
||||
if(!singletonHandler.singleton && !initialLoad && !singletonHandler.pendingCreateBlockCallback) {
|
||||
singletonHandler.pendingCreateBlockCallback = true;
|
||||
singletonHandler.createBlock((created) => {
|
||||
singletonHandler.singleton = created;
|
||||
singletonHandler.pendingCreateBlockCallback = false;
|
||||
singletonHandler.resolutionCallback(created);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterItemsWithPredicate(items, predicate) {
|
||||
return items.filter((candidate) => {
|
||||
return this.itemSatisfiesPredicate(candidate, predicate);
|
||||
})
|
||||
}
|
||||
|
||||
itemSatisfiesPredicate(candidate, predicate) {
|
||||
for(var key in predicate) {
|
||||
var predicateValue = predicate[key];
|
||||
var candidateValue = candidate[key];
|
||||
if(typeof predicateValue == 'object') {
|
||||
// Check nested properties
|
||||
if(!candidateValue) {
|
||||
// predicateValue is 'object' but candidateValue is null
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!this.itemSatisfiesPredicate(candidateValue, predicateValue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if(candidateValue != predicateValue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').service('singletonManager', SingletonManager);
|
||||
@@ -62,9 +62,9 @@ class StorageManager {
|
||||
return this._memoryStorage;
|
||||
}
|
||||
|
||||
setItemsMode(mode) {
|
||||
setItemsMode(mode, force) {
|
||||
var newStorage = this.getVault(mode);
|
||||
if(newStorage !== this.storage) {
|
||||
if(newStorage !== this.storage || mode !== this.itemsStorageMode || force) {
|
||||
// transfer storages
|
||||
var length = this.storage.length;
|
||||
for(var i = 0; i < length; i++) {
|
||||
@@ -161,7 +161,6 @@ class StorageManager {
|
||||
for(var key of Object.keys(encryptedStorage.storage)) {
|
||||
this.setItem(key, encryptedStorage.storage[key]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
hasPasscode() {
|
||||
@@ -228,4 +227,4 @@ StorageManager.FixedEncrypted = "FixedEncrypted"; // encrypted memoryStorage + l
|
||||
StorageManager.Ephemeral = "Ephemeral"; // memoryStorage
|
||||
StorageManager.Fixed = "Fixed"; // localStorage
|
||||
|
||||
angular.module('app.frontend').service('storageManager', StorageManager);
|
||||
angular.module('app').service('storageManager', StorageManager);
|
||||
|
||||
@@ -21,10 +21,6 @@ class SyncManager {
|
||||
return this.storageManager.getItem("mk");
|
||||
}
|
||||
|
||||
get serverPassword() {
|
||||
return this.storageManager.getItem("pw");
|
||||
}
|
||||
|
||||
writeItemsToLocalStorage(items, offlineOnly, callback) {
|
||||
if(items.length == 0) {
|
||||
callback && callback();
|
||||
@@ -62,6 +58,11 @@ class SyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
this.$rootScope.$broadcast("sync:completed", {});
|
||||
|
||||
// Required in order for modelManager to notify sync observers
|
||||
this.modelManager.didSyncModelsOffline(items);
|
||||
|
||||
if(callback) {
|
||||
callback({success: true});
|
||||
}
|
||||
@@ -92,7 +93,7 @@ class SyncManager {
|
||||
|
||||
let alternateNextItem = () => {
|
||||
if(index >= originalItems.length) {
|
||||
// We don't use originalItems as altnerating UUID will have deleted them.
|
||||
// We don't use originalItems as alternating UUID will have deleted them.
|
||||
block();
|
||||
return;
|
||||
}
|
||||
@@ -188,7 +189,17 @@ class SyncManager {
|
||||
this.$interval.cancel(this.syncStatus.checker);
|
||||
}
|
||||
|
||||
sync(callback, options = {}) {
|
||||
sync(callback, options = {}, source) {
|
||||
|
||||
if(!options) options = {};
|
||||
|
||||
if(typeof callback == 'string') {
|
||||
// is source string, used to avoid filling parameters on call
|
||||
source = callback;
|
||||
callback = null;
|
||||
}
|
||||
|
||||
// console.log("Syncing from", source);
|
||||
|
||||
var allDirtyItems = this.modelManager.getDirtyItems();
|
||||
|
||||
@@ -241,6 +252,11 @@ class SyncManager {
|
||||
this.allRetreivedItems = [];
|
||||
}
|
||||
|
||||
// We also want to do this for savedItems
|
||||
if(!this.allSavedItems) {
|
||||
this.allSavedItems = [];
|
||||
}
|
||||
|
||||
var version = this.authManager.protocolVersion();
|
||||
var keys = this.authManager.keys();
|
||||
|
||||
@@ -265,7 +281,17 @@ class SyncManager {
|
||||
|
||||
this.$rootScope.$broadcast("sync:updated_token", this.syncToken);
|
||||
|
||||
// Filter retrieved_items to remove any items that may be in saved_items for this complete sync operation
|
||||
// When signing in, and a user requires many round trips to complete entire retrieval of data, an item may be saved
|
||||
// on the first trip, then on subsequent trips using cursor_token, this same item may be returned, since it's date is
|
||||
// greater than cursor_token. We keep track of all saved items in whole sync operation with this.allSavedItems
|
||||
// We need this because singletonManager looks at retrievedItems as higher precendence than savedItems, but if it comes in both
|
||||
// then that's problematic.
|
||||
let allSavedUUIDs = this.allSavedItems.map((item) => {return item.uuid});
|
||||
response.retrieved_items = response.retrieved_items.filter((candidate) => {return !allSavedUUIDs.includes(candidate.uuid)});
|
||||
|
||||
// Map retrieved items to local data
|
||||
// Note that deleted items will not be returned
|
||||
var retrieved
|
||||
= this.handleItemsResponse(response.retrieved_items, null, ModelManager.MappingSourceRemoteRetrieved);
|
||||
|
||||
@@ -281,6 +307,9 @@ class SyncManager {
|
||||
var saved =
|
||||
this.handleItemsResponse(response.saved_items, omitFields, ModelManager.MappingSourceRemoteSaved);
|
||||
|
||||
// Append items to master list of saved items for this ongoing sync operation
|
||||
this.allSavedItems = this.allSavedItems.concat(saved);
|
||||
|
||||
// Create copies of items or alternate their uuids if neccessary
|
||||
var unsaved = response.unsaved;
|
||||
this.handleUnsavedItemsResponse(unsaved)
|
||||
@@ -298,12 +327,12 @@ class SyncManager {
|
||||
|
||||
if(this.cursorToken || this.syncStatus.needsMoreSync) {
|
||||
setTimeout(function () {
|
||||
this.sync(callback, options);
|
||||
this.sync(callback, options, "onSyncSuccess cursorToken || needsMoreSync");
|
||||
}.bind(this), 10); // wait 10ms to allow UI to update
|
||||
} else if(this.repeatOnCompletion) {
|
||||
this.repeatOnCompletion = false;
|
||||
setTimeout(function () {
|
||||
this.sync(callback, options);
|
||||
this.sync(callback, options, "onSyncSuccess repeatOnCompletion");
|
||||
}.bind(this), 10); // wait 10ms to allow UI to update
|
||||
} else {
|
||||
this.writeItemsToLocalStorage(this.allRetreivedItems, false, null);
|
||||
@@ -319,10 +348,11 @@ class SyncManager {
|
||||
this.$rootScope.$broadcast("major-data-change");
|
||||
}
|
||||
|
||||
this.allRetreivedItems = [];
|
||||
|
||||
this.callQueuedCallbacksAndCurrent(callback, response);
|
||||
this.$rootScope.$broadcast("sync:completed");
|
||||
this.$rootScope.$broadcast("sync:completed", {retrievedItems: this.allRetreivedItems, savedItems: this.allSavedItems});
|
||||
|
||||
this.allRetreivedItems = [];
|
||||
this.allSavedItems = [];
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
@@ -391,14 +421,13 @@ class SyncManager {
|
||||
console.log("Handle unsaved", unsaved);
|
||||
|
||||
var i = 0;
|
||||
var handleNext = function() {
|
||||
var handleNext = () => {
|
||||
if(i >= unsaved.length) {
|
||||
// Handled all items
|
||||
this.sync(null, {additionalFields: ["created_at", "updated_at"]});
|
||||
return;
|
||||
}
|
||||
|
||||
var handled = false;
|
||||
var mapping = unsaved[i];
|
||||
var itemResponse = mapping.item;
|
||||
EncryptionHelper.decryptMultipleItems([itemResponse], this.authManager.keys());
|
||||
@@ -414,8 +443,10 @@ class SyncManager {
|
||||
if(error.tag === "uuid_conflict") {
|
||||
// UUID conflicts can occur if a user attempts to
|
||||
// import an old data archive with uuids from the old account into a new account
|
||||
handled = true;
|
||||
this.modelManager.alternateUUIDForItem(item, handleNext, true);
|
||||
this.modelManager.alternateUUIDForItem(item, () => {
|
||||
i++;
|
||||
handleNext();
|
||||
}, true);
|
||||
}
|
||||
|
||||
else if(error.tag === "sync_conflict") {
|
||||
@@ -425,20 +456,16 @@ class SyncManager {
|
||||
itemResponse.uuid = null;
|
||||
|
||||
var dup = this.modelManager.createDuplicateItem(itemResponse, item);
|
||||
if(!itemResponse.deleted && JSON.stringify(item.structureParams()) !== JSON.stringify(dup.structureParams())) {
|
||||
if(!itemResponse.deleted && !item.isItemContentEqualWith(dup)) {
|
||||
this.modelManager.addItem(dup);
|
||||
dup.conflict_of = item.uuid;
|
||||
dup.setDirty(true);
|
||||
}
|
||||
}
|
||||
|
||||
++i;
|
||||
|
||||
if(!handled) {
|
||||
i++;
|
||||
handleNext();
|
||||
}
|
||||
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
handleNext();
|
||||
}
|
||||
@@ -459,4 +486,4 @@ class SyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('syncManager', SyncManager);
|
||||
angular.module('app').service('syncManager', SyncManager);
|
||||
|
||||
@@ -1,99 +1,45 @@
|
||||
class ThemeManager {
|
||||
|
||||
constructor(modelManager, syncManager, $rootScope, storageManager) {
|
||||
this.syncManager = syncManager;
|
||||
this.modelManager = modelManager;
|
||||
this.$rootScope = $rootScope;
|
||||
this.storageManager = storageManager;
|
||||
}
|
||||
constructor(componentManager, desktopManager) {
|
||||
this.componentManager = componentManager;
|
||||
|
||||
get themes() {
|
||||
return this.modelManager.itemsForContentType("SN|Theme");
|
||||
}
|
||||
desktopManager.registerUpdateObserver((component) => {
|
||||
// Reload theme if active
|
||||
if(component.active && component.isTheme()) {
|
||||
this.deactivateTheme(component);
|
||||
setTimeout(() => {
|
||||
this.activateTheme(component);
|
||||
}, 10);
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
activeTheme: computed property that returns saved theme
|
||||
currentTheme: stored variable that allows other classes to watch changes
|
||||
*/
|
||||
|
||||
get activeTheme() {
|
||||
var activeThemeId = this.storageManager.getItem("activeTheme");
|
||||
if(!activeThemeId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var theme = _.find(this.themes, {uuid: activeThemeId});
|
||||
return theme;
|
||||
}
|
||||
|
||||
activateInitialTheme() {
|
||||
var theme = this.activeTheme;
|
||||
if(theme) {
|
||||
this.activateTheme(theme);
|
||||
}
|
||||
}
|
||||
|
||||
submitTheme(url) {
|
||||
var name = this.displayNameForThemeFile(this.fileNameFromPath(url));
|
||||
var theme = this.modelManager.createItem({content_type: "SN|Theme", url: url, name: name});
|
||||
this.modelManager.addItem(theme);
|
||||
theme.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
componentManager.registerHandler({identifier: "themeManager", areas: ["themes"], activationHandler: (component) => {
|
||||
if(component.active) {
|
||||
this.activateTheme(component);
|
||||
} else {
|
||||
this.deactivateTheme(component);
|
||||
}
|
||||
}});
|
||||
}
|
||||
|
||||
activateTheme(theme) {
|
||||
var activeTheme = this.activeTheme;
|
||||
if(activeTheme) {
|
||||
this.deactivateTheme(activeTheme);
|
||||
}
|
||||
|
||||
var url = this.componentManager.urlForComponent(theme);
|
||||
var link = document.createElement("link");
|
||||
link.href = theme.url;
|
||||
link.href = url;
|
||||
link.type = "text/css";
|
||||
link.rel = "stylesheet";
|
||||
link.media = "screen,print";
|
||||
link.id = theme.uuid;
|
||||
document.getElementsByTagName("head")[0].appendChild(link);
|
||||
this.storageManager.setItem("activeTheme", theme.uuid);
|
||||
|
||||
this.currentTheme = theme;
|
||||
this.$rootScope.$broadcast("theme-changed");
|
||||
}
|
||||
|
||||
deactivateTheme(theme) {
|
||||
this.storageManager.removeItem("activeTheme");
|
||||
var element = document.getElementById(theme.uuid);
|
||||
if(element) {
|
||||
element.disabled = true;
|
||||
element.parentNode.removeChild(element);
|
||||
}
|
||||
|
||||
this.currentTheme = null;
|
||||
this.$rootScope.$broadcast("theme-changed");
|
||||
}
|
||||
|
||||
isThemeActive(theme) {
|
||||
return this.storageManager.getItem("activeTheme") === theme.uuid;
|
||||
}
|
||||
|
||||
fileNameFromPath(filePath) {
|
||||
return filePath.replace(/^.*[\\\/]/, '');
|
||||
}
|
||||
|
||||
capitalizeString(string) {
|
||||
return string.replace(/(?:^|\s)\S/g, function(a) { return a.toUpperCase(); });
|
||||
}
|
||||
|
||||
displayNameForThemeFile(fileName) {
|
||||
let fromParam = getParameterByName("name", fileName);
|
||||
if(fromParam) {
|
||||
return fromParam;
|
||||
}
|
||||
let name = fileName.split(".")[0];
|
||||
let cleaned = name.split("-").join(" ");
|
||||
return this.capitalizeString(cleaned);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('themeManager', ThemeManager);
|
||||
angular.module('app').service('themeManager', ThemeManager);
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
//= require app/app.frontend.js
|
||||
//= require_tree ./app/services
|
||||
|
||||
//= require app/app.frontend.js
|
||||
//= require_tree ./app/frontend
|
||||
1
app/assets/javascripts/main.js
Normal file
1
app/assets/javascripts/main.js
Normal file
@@ -0,0 +1 @@
|
||||
//= require_tree ./app
|
||||
File diff suppressed because one or more lines are too long
@@ -1,25 +1,10 @@
|
||||
$heading-height: 75px;
|
||||
.editor {
|
||||
flex: 1 50%;
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: hidden;
|
||||
background-color: white;
|
||||
|
||||
&.fullscreen {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
z-index: 200;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.section-menu-bar {
|
||||
flex: 1 0 28px;
|
||||
max-height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
#editor-title-bar {
|
||||
@@ -37,10 +22,6 @@ $heading-height: 75px;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
|
||||
&.fullscreen {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
> .title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
@@ -87,7 +68,7 @@ $heading-height: 75px;
|
||||
|
||||
#note-tags-component-container {
|
||||
height: 50px;
|
||||
#note-tags-iframe {
|
||||
iframe {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
@@ -103,7 +84,7 @@ $heading-height: 75px;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
.editor-content, #editor-content {
|
||||
flex: 1;
|
||||
z-index: 10;
|
||||
overflow-y: hidden;
|
||||
@@ -111,9 +92,7 @@ $heading-height: 75px;
|
||||
display: flex;
|
||||
background-color: white;
|
||||
|
||||
&.fullscreen {
|
||||
padding-top: 0px;
|
||||
}
|
||||
position: relative;
|
||||
|
||||
#editor-iframe {
|
||||
flex: 1;
|
||||
@@ -122,7 +101,6 @@ $heading-height: 75px;
|
||||
|
||||
.editable {
|
||||
font-family: monospace;
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
width: 100%;
|
||||
|
||||
@@ -132,54 +110,16 @@ $heading-height: 75px;
|
||||
padding-top: 11px;
|
||||
font-size: 17px;
|
||||
resize: none;
|
||||
|
||||
&.fullscreen {
|
||||
padding: 85px 10%;
|
||||
max-width: 1200px;
|
||||
display: inline-block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#editor-pane-component-stack {
|
||||
width: 100%;
|
||||
|
||||
.component {
|
||||
height: 50px;
|
||||
.component-stack-item {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid $bg-color;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top: 1px solid $bg-color;
|
||||
}
|
||||
|
||||
.exit-button {
|
||||
width: 15px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(black, 0.7);
|
||||
text-align: center;
|
||||
padding-left: 2px;
|
||||
|
||||
.content {
|
||||
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(gray, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
border-top: 1px solid $bg-color;
|
||||
iframe {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
.extension-render-modal {
|
||||
position: fixed;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 10000;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(gray, 0.3);
|
||||
color: black;
|
||||
|
||||
.content {
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
background-color: white;
|
||||
width: 700px;
|
||||
height: 500px;
|
||||
margin: auto;
|
||||
padding: 25px;
|
||||
position: absolute;
|
||||
top: 0; left: 0; bottom: 0; right: 0;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
#global-ext-menu {
|
||||
color: black;
|
||||
.panel-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 13px 18px;
|
||||
|
||||
&.no-bottom {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
|
||||
&.small {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-group {
|
||||
a {
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-link {
|
||||
padding-top: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.section-margin {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
input {
|
||||
border: 1px solid $blue-color;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
ul {
|
||||
border-top: 1px solid $light-bg-color;
|
||||
border-bottom: 1px solid $light-bg-color;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
cursor: pointer;
|
||||
background-color: rgba($light-bg-color, 0.2);
|
||||
&:hover {
|
||||
background-color: rgba($light-bg-color, 0.4);
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid $light-bg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1,221 +1,48 @@
|
||||
.fake-link {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 0px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
#footer-bar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
background-color: #f1f1f1;
|
||||
border-top: 1px solid rgba(black, 0.04);
|
||||
height: $footer-height;
|
||||
max-height: $footer-height;
|
||||
z-index: 100;
|
||||
font-size: 10px;
|
||||
color: $dark-gray;
|
||||
|
||||
.medium-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
|
||||
&.gray {
|
||||
color: $dark-gray;
|
||||
}
|
||||
|
||||
&.block {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 2px 0px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 5px;
|
||||
padding-bottom: 2px;
|
||||
margin-top: 5px;
|
||||
|
||||
&.inline-h {
|
||||
padding-top: 5px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
input {
|
||||
// margin-bottom: 10px;
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
#footer-bar .footer-bar-link {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
margin-left: 8px;
|
||||
color: #515263;
|
||||
|
||||
#footer-bar .item {
|
||||
z-index: 1000;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
user-select: none;
|
||||
|
||||
> a {
|
||||
color: #515263;
|
||||
.panel {
|
||||
max-height: 85vh;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
left: 10px;
|
||||
bottom: 40px;
|
||||
min-width: 300px;
|
||||
z-index: 1000;
|
||||
margin-top: 15px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.footer-bar-link .panel {
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
#account-panel {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
max-height: 85vh;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 20px;
|
||||
min-width: 300px;
|
||||
z-index: 1000;
|
||||
margin-top: 15px;
|
||||
|
||||
box-shadow: 0px 0px 15px rgba(black, 0.2);
|
||||
border: none;
|
||||
.panel {
|
||||
cursor: default;
|
||||
overflow: auto;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
button.light {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0px;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
padding-top: 3px;
|
||||
text-align: center;
|
||||
margin-bottom: 6px;
|
||||
background-color: white;
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(gray, 0.15);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(gray, 0.10);
|
||||
}
|
||||
}
|
||||
|
||||
.half-button {
|
||||
$spacing: 2px;
|
||||
width: calc(50% - #{$spacing});
|
||||
margin-left: $spacing/2.0;
|
||||
margin-right: $spacing/2.0;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.gray-bg {
|
||||
background-color: #f6f6f6;
|
||||
border: 1px solid #f2f2f2;
|
||||
}
|
||||
|
||||
.white-bg {
|
||||
background-color: white;
|
||||
border: 1px solid rgba(gray, 0.2);
|
||||
}
|
||||
|
||||
a.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#lock-item {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.icon.ion-locked {
|
||||
margin-left: 5px;
|
||||
border-left: 1px solid gray;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* Global Ext Menu */
|
||||
|
||||
.ext-section {
|
||||
|
||||
min-height: 50px;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
&.opened {
|
||||
h1 {
|
||||
padding-top: 0px;
|
||||
// padding-bottom: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.spinner {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
animation: rotate 0.8s infinite linear;
|
||||
border: 1px solid #515263;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
|
||||
&.tinted {
|
||||
border: 1px solid $blue-color;
|
||||
border-right-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
24
app/assets/stylesheets/app/_ionicons.scss
Normal file
24
app/assets/stylesheets/app/_ionicons.scss
Normal file
@@ -0,0 +1,24 @@
|
||||
@charset "UTF-8";
|
||||
/*!
|
||||
Ionicons, v2.0.1
|
||||
Created by Ben Sperry for the Ionic Framework, http://ionicons.com/
|
||||
https://twitter.com/benjsperry https://twitter.com/ionicframework
|
||||
MIT License: https://github.com/driftyco/ionicons
|
||||
|
||||
Android-style icons originally built by Google’s
|
||||
Material Design Icons: https://github.com/google/material-design-icons
|
||||
used under CC BY http://creativecommons.org/licenses/by/4.0/
|
||||
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-ios-box:before { content: "\f3ec"; }
|
||||
|
||||
.ion-locked:before { content: "\f200"; }
|
||||
|
||||
.ion-bookmark:before {
|
||||
content: "\f26b";
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=ionicons.css.map */
|
||||
@@ -14,6 +14,7 @@
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.background {
|
||||
position: absolute;
|
||||
@@ -22,30 +23,12 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
// box-shadow: 0 3px 3px rgba(0, 0, 0, 0.175);
|
||||
border: 1px solid rgba(black, 0.1);
|
||||
background-color: white;
|
||||
width: 300px;
|
||||
// height: 500px;
|
||||
margin: auto;
|
||||
padding: 10px 30px;
|
||||
padding-bottom: 30px;
|
||||
// position: absolute;
|
||||
// top: 0; left: 0; bottom: 0; right: 0;
|
||||
overflow-y: scroll;
|
||||
.panel {
|
||||
width: 315px;
|
||||
flex-grow: 0;
|
||||
|
||||
p {
|
||||
margin-bottom: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 6px;
|
||||
.header {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,56 +6,6 @@ $selection-color: $bg-color;
|
||||
$selected-text-color: black;
|
||||
$blue-color: #086dd6;
|
||||
|
||||
@mixin MQ-Small() {
|
||||
@media (max-width: $screen-xs-max) {
|
||||
@content;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin MQ-Medium() {
|
||||
@media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mixin MQ-Large() {
|
||||
@media (min-width: $screen-lg-min) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
.tinted {
|
||||
color: $blue-color;
|
||||
}
|
||||
|
||||
.tinted-selected {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tinted-box {
|
||||
background-color: $blue-color;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
padding: 16px 20px;
|
||||
|
||||
button {
|
||||
background-color: white;
|
||||
color: $blue-color;
|
||||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
padding: 6px 20px;
|
||||
width: 100%;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont,
|
||||
@@ -66,34 +16,49 @@ body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
font-size: 20px;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
background-color: $bg-color;
|
||||
}
|
||||
|
||||
.dark-button {
|
||||
background-color: #2e2e2e;
|
||||
border: 0;
|
||||
padding: 6px 18px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
border-radius: 2px;
|
||||
border: 1px solid transparent;
|
||||
-webkit-appearance: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: black;
|
||||
}
|
||||
.tinted {
|
||||
color: $blue-color;
|
||||
}
|
||||
|
||||
.tinted-selected {
|
||||
color: white;
|
||||
}
|
||||
|
||||
*:focus {outline:0;}
|
||||
|
||||
button:focus {
|
||||
outline:0;
|
||||
}
|
||||
|
||||
input, button, select, textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue-color;
|
||||
text-decoration: none;
|
||||
|
||||
&.no-decoration {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;;
|
||||
text-decoration: underline;
|
||||
|
||||
&.no-decoration {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,12 +73,9 @@ p {
|
||||
background-color: $bg-color;
|
||||
}
|
||||
|
||||
$footer-height: 25px;
|
||||
|
||||
$section-header-height: 70px;
|
||||
$footer-height: 32px;
|
||||
|
||||
.app {
|
||||
// height: 100%;
|
||||
height: calc(100% - #{$footer-height});
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@@ -121,18 +83,38 @@ $section-header-height: 70px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.light-button {
|
||||
background-color: $bg-color;
|
||||
font-weight: bold;
|
||||
color: $main-text-color;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
height: 35px;
|
||||
border-radius: 4px;
|
||||
padding-top: 6px;
|
||||
panel-resizer {
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
width: 8px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
cursor: col-resize;
|
||||
background-color: rgb(224, 224, 224);
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: #cdcdcd;
|
||||
&.left {
|
||||
left: 0;
|
||||
right: none;
|
||||
}
|
||||
|
||||
&.always-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.hoverable {
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +123,8 @@ $section-header-height: 70px;
|
||||
height: 100%;
|
||||
max-height: calc(100vh - #{$footer-height});
|
||||
font-size: 17px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.scrollable {
|
||||
overflow-y: auto;
|
||||
@@ -155,34 +139,42 @@ $section-header-height: 70px;
|
||||
}
|
||||
|
||||
.section-title-bar {
|
||||
padding: 20px;
|
||||
height: $section-header-height;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
|
||||
> .title {
|
||||
float: left;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 80%;
|
||||
overflow: hidden;
|
||||
.padded {
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
> .add-button {
|
||||
float: right;
|
||||
font-size: 18px;
|
||||
width: 45px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
background-color: #e9e9e9;
|
||||
border-radius: 4px;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
.section-title-bar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(#e9e9e9, 0.8);
|
||||
> .title {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,243 +1,20 @@
|
||||
ul.section-menu-bar {
|
||||
width: 100%;
|
||||
padding-top: 0px;
|
||||
padding-left: 6px;
|
||||
padding-right: 21px;
|
||||
|
||||
user-select: none;
|
||||
|
||||
background-color: #f1f1f1;
|
||||
color: $selected-text-color;
|
||||
height: 28px;
|
||||
cursor: default;
|
||||
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0;
|
||||
list-style: none;
|
||||
font-weight: bold;
|
||||
font-size: 0; /* trick to remove gaps between li inline-block elements */
|
||||
|
||||
> li {
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
display: inline-block;
|
||||
.app-bar {
|
||||
.item {
|
||||
position: relative;
|
||||
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
|
||||
&.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.item-with-subtitle {
|
||||
label {
|
||||
// float: left;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-top: 1px;
|
||||
opacity: 0.5;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
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 */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.selected {
|
||||
background-color: $blue-color;
|
||||
border-radius: 1px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
float: left;
|
||||
min-width: 160px;
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
float: left;
|
||||
min-width: 160px;
|
||||
z-index: 100;
|
||||
|
||||
list-style: none;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
|
||||
padding: 0 0;
|
||||
border: none;
|
||||
width: 280px;
|
||||
|
||||
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
background-clip: padding-box;
|
||||
|
||||
background-color: white;
|
||||
color: $selected-text-color;
|
||||
|
||||
li:hover {
|
||||
background-color: $blue-color;
|
||||
color: white;
|
||||
}
|
||||
|
||||
> li {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding-top: 3px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
border-bottom: 1px solid rgba(black, 0.1);
|
||||
|
||||
color: $selected-text-color;
|
||||
float: left;
|
||||
|
||||
label {
|
||||
padding: 10px;
|
||||
padding-top: 8px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.shortcut {
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
opacity: 0.5;
|
||||
margin-top: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.dropdown-menu.sectioned-menu {
|
||||
overflow-y: scroll;
|
||||
margin-top: 5px;
|
||||
width: 280px;
|
||||
max-height: calc(85vh - 90px);
|
||||
|
||||
ul {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
padding-left:0;
|
||||
position: relative;
|
||||
|
||||
li {
|
||||
cursor: pointer;
|
||||
height: auto;
|
||||
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid rgba(black, 0.1);
|
||||
background-color: rgba(white, 0.9);
|
||||
height: auto;
|
||||
|
||||
.left-side {
|
||||
left: 0;
|
||||
width: 60%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.right-side {
|
||||
right: 12px;
|
||||
width: 30%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
text-align: right;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $blue-color;
|
||||
|
||||
|
||||
.tinted {
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.nested-hover {
|
||||
color: black;
|
||||
background-color: $light-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.nested-hover {
|
||||
color: black;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.menu-item-title {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.menu-item-subtitle {
|
||||
font-weight: normal;
|
||||
opacity: 0.5;
|
||||
margin-top: 1px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #ededed;
|
||||
border-bottom: 1px solid #d3d3d3;
|
||||
position: relative;
|
||||
padding-top: 12px;
|
||||
padding-left: 10px;
|
||||
padding-bottom: 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
|
||||
> .title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> .subtitle {
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
font-weight: normal;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
> .loading {
|
||||
position: absolute;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
right: 10px;
|
||||
top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: #ededed;
|
||||
border-top: 1px solid #d3d3d3;
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
}
|
||||
background-color: white;
|
||||
color: $selected-text-color;
|
||||
}
|
||||
|
||||
91
app/assets/stylesheets/app/_modals.scss
Normal file
91
app/assets/stylesheets/app/_modals.scss
Normal file
@@ -0,0 +1,91 @@
|
||||
#permissions-modal {
|
||||
width: 350px;
|
||||
.panel {
|
||||
border-radius: 0;
|
||||
background-color: white;
|
||||
}
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
.footer {
|
||||
padding-bottom: 1.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.header .subtitle {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 10000;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(gray, 0.3);
|
||||
color: black;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.sn-component {
|
||||
height: 100%;
|
||||
.panel {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
> .content {
|
||||
width: 700px;
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.background {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
> .content {
|
||||
overflow-y: auto;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
padding-bottom: 0;
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally use if .component-view container is not flex-based
|
||||
.component-view-container {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.component-view {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
|
||||
.sn-component {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
iframe {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,44 @@
|
||||
.notes {
|
||||
#notes-column, .notes {
|
||||
border-left: 1px solid #dddddd;
|
||||
border-right: 1px solid #dddddd;
|
||||
|
||||
flex: 1 20%;
|
||||
max-width: 350px;
|
||||
min-width: 170px;
|
||||
|
||||
width: 350px;
|
||||
flex-grow: 0;
|
||||
user-select: none;
|
||||
|
||||
$notes-title-bar-height: 148px;
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#notes-title-bar {
|
||||
color: rgba(black, 0.40);
|
||||
padding-top: 16px;
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
height: $notes-title-bar-height;
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
|
||||
.title {
|
||||
.section-title-bar-header .title {
|
||||
color: rgba(black, 0.40);
|
||||
width: calc(90% - 45px);
|
||||
}
|
||||
}
|
||||
|
||||
#notes-add-button {
|
||||
right: 14px;
|
||||
|
||||
}
|
||||
|
||||
#notes-menu-bar {
|
||||
position: relative;
|
||||
margin: 0 -14px;
|
||||
margin-top: 14px;
|
||||
height: 45px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
clear: left;
|
||||
height: 32px;
|
||||
margin-top: 20px;
|
||||
margin-top: 14px;
|
||||
position: relative;
|
||||
|
||||
.filter-bar {
|
||||
@@ -80,7 +79,7 @@
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
height: calc(100vh - (#{$notes-title-bar-height} + #{$footer-height}));
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.infinite-scroll {
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
.permissions-modal {
|
||||
position: fixed;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 10000;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(gray, 0.3);
|
||||
color: black;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.background {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
background-color: white;
|
||||
width: 700px;
|
||||
// height: 500px;
|
||||
margin: auto;
|
||||
padding: 10px 30px;
|
||||
padding-bottom: 30px;
|
||||
// position: absolute;
|
||||
// top: 0; left: 0; bottom: 0; right: 0;
|
||||
overflow-y: scroll;
|
||||
|
||||
p {
|
||||
margin-bottom: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.learn-more {
|
||||
margin-top: 20px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.status {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 35px;
|
||||
}
|
||||
|
||||
button.standard {
|
||||
border-radius: 1px;
|
||||
font-weight: bold;
|
||||
padding: 6px 20px;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.tinted {
|
||||
background-color: $blue-color;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.white {
|
||||
color: black;
|
||||
background-color: white;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
.selectable {
|
||||
user-select: text !important;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.pull-left {
|
||||
float: left !important;
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
float: right !important;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 1px !important;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 2px !important;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 3px !important;
|
||||
}
|
||||
|
||||
.mt-5 {
|
||||
margin-top: 5px !important;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10px !important;
|
||||
}
|
||||
|
||||
.mt-15 {
|
||||
margin-top: 15px !important;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20px !important;
|
||||
}
|
||||
|
||||
.mt-25 {
|
||||
margin-top: 25px !important;
|
||||
}
|
||||
|
||||
.mt-50 {
|
||||
margin-top: 50px !important;
|
||||
}
|
||||
|
||||
.mt-100 {
|
||||
margin-top: 100px !important;
|
||||
}
|
||||
|
||||
.mb-0 {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
|
||||
.mb-5 {
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
|
||||
.mb-10 {
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
|
||||
.mr-5 {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.mr-10 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mr-15 {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.mr-20 {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.pb-0 {
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
|
||||
.pt-5 {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.faded {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.center-align {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.center {
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.one-line-overflow {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.small-v-space {
|
||||
height: 6px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.medium-v-space {
|
||||
height: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.large-v-space {
|
||||
height: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.small-padding {
|
||||
padding: 5px !important;
|
||||
}
|
||||
|
||||
.medium-padding {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.pb-4 {
|
||||
padding-bottom: 4px !important;
|
||||
}
|
||||
|
||||
.pb-6 {
|
||||
padding-bottom: 6px !important;
|
||||
}
|
||||
|
||||
.pb-10 {
|
||||
padding-bottom: 10px !important;
|
||||
}
|
||||
|
||||
.large-padding {
|
||||
padding: 22px !important;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: red !important;
|
||||
}
|
||||
|
||||
.orange {
|
||||
color: orange !important;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.normal {
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.medium {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline-block !important;
|
||||
|
||||
&.top {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
&.middle {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input.form-control {
|
||||
margin-bottom: 10px;
|
||||
border-radius: 0px;
|
||||
min-height: 39px;
|
||||
font-size: 14px;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
|
||||
@mixin wide-button() {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
// min-width: 200px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&.black {
|
||||
@include wide-button();
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.white {
|
||||
@include wide-button();
|
||||
background-color: white;
|
||||
color: black;
|
||||
border: 1px solid rgba(gray, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.gray-bg {
|
||||
background-color: #f6f6f6;
|
||||
border: 1px solid #f2f2f2;
|
||||
}
|
||||
|
||||
.white-bg {
|
||||
background-color: white;
|
||||
border: 1px solid rgba(gray, 0.2);
|
||||
}
|
||||
|
||||
.col-container {
|
||||
// white-space: nowrap;
|
||||
}
|
||||
|
||||
@mixin col() {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.col-10 {
|
||||
width: 10%;
|
||||
@include col();
|
||||
}
|
||||
|
||||
.col-15 {
|
||||
width: 15%;
|
||||
@include col();
|
||||
}
|
||||
|
||||
.col-20 {
|
||||
width: 20%;
|
||||
@include col();
|
||||
}
|
||||
|
||||
.col-45 {
|
||||
width: 45%;
|
||||
@include col();
|
||||
}
|
||||
|
||||
.col-50 {
|
||||
width: 50%;
|
||||
@include col();
|
||||
}
|
||||
|
||||
.col-80 {
|
||||
width: 80%;
|
||||
@include col();
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.absolute {
|
||||
position: absolute !important;
|
||||
}
|
||||
31
app/assets/stylesheets/app/_stylekit-sub.scss
Normal file
31
app/assets/stylesheets/app/_stylekit-sub.scss
Normal file
@@ -0,0 +1,31 @@
|
||||
.sn-component {
|
||||
|
||||
}
|
||||
|
||||
.panel {
|
||||
color: black;
|
||||
|
||||
.header {
|
||||
.close-button {
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
min-height: 39px;
|
||||
}
|
||||
|
||||
|
||||
.button-group.stretch {
|
||||
.button:not(.featured) {
|
||||
// Default buttons that are not featured and stretched should have larger vertical padding
|
||||
padding: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue-color;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,28 @@
|
||||
.tags {
|
||||
flex: 1 10%;
|
||||
max-width: 180px;
|
||||
min-width: 100px;
|
||||
width: 180px;
|
||||
background-color: #f6f6f6;
|
||||
flex-grow: 0;
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
user-select: none;
|
||||
|
||||
$tags-title-bar-height: 55px;
|
||||
&, #tags-content {
|
||||
background-color: #f6f6f6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#tags-title-bar {
|
||||
color: black;
|
||||
height: $tags-title-bar-height;
|
||||
padding-top: 14px;
|
||||
padding-bottom: 16px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
font-size: 12px;
|
||||
color: rgba(black, 0.8);
|
||||
}
|
||||
|
||||
#tags-content {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
#tag-add-button {
|
||||
margin-top: -6px;
|
||||
background-color: #d7d7d7;
|
||||
float: right;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(#d7d7d7, 0.8);
|
||||
@@ -33,7 +30,12 @@
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
height: calc(100vh - (#{$tags-title-bar-height} + #{$footer-height}));
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.infinite-scroll {
|
||||
overflow-x: hidden;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.tag {
|
||||
|
||||
@@ -57,104 +57,56 @@ $screen-md-max: ($screen-lg-min - 1) !default;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
.selectable {
|
||||
user-select: text !important;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
*:focus {outline:0;}
|
||||
|
||||
.float-group {
|
||||
height: 15px;
|
||||
&.h10 {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
&.h20 {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&.h30 {
|
||||
height: 30px;
|
||||
}
|
||||
clear: both;
|
||||
.mt-5 {
|
||||
margin-top: 5px !important;
|
||||
}
|
||||
|
||||
button:focus {outline:0;}
|
||||
|
||||
|
||||
.button-group {
|
||||
clear: both;
|
||||
height: 36px;
|
||||
.mt-10 {
|
||||
margin-top: 10px !important;
|
||||
}
|
||||
|
||||
.ui-button {
|
||||
background-color: $blue-color;
|
||||
border: 0;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
min-height: 36px;
|
||||
font-size: 14px;
|
||||
|
||||
&.block {
|
||||
width: 100%;
|
||||
}
|
||||
.faded {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
min-width: 300px;
|
||||
z-index: 1000;
|
||||
margin-top: 10px;
|
||||
box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.2);
|
||||
border: none;
|
||||
background-color: white;
|
||||
.center-align {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.panel-right {
|
||||
left: 0px;
|
||||
.block {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 15px;
|
||||
.wrap {
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857;
|
||||
color: #555555;
|
||||
background-color: #fff;
|
||||
background-image: none;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0;
|
||||
.medium-padding {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
input, button, select, textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
.red {
|
||||
color: red !important;
|
||||
}
|
||||
|
||||
button {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
.bold {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 10px;
|
||||
|
||||
input {
|
||||
margin-left: 0px;
|
||||
}
|
||||
.normal {
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.medium {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
$dark-gray: #2e2e2e;
|
||||
|
||||
@import "app/standard";
|
||||
@import "app/main";
|
||||
@import "app/ui";
|
||||
@import "app/footer";
|
||||
@import "app/tags";
|
||||
@import "app/notes";
|
||||
@import "app/editor";
|
||||
@import "app/extensions";
|
||||
@import "app/menus";
|
||||
@import "app/permissions-modal";
|
||||
@import "app/modals";
|
||||
@import "app/lock-screen";
|
||||
|
||||
@import "ionicons";
|
||||
@import "app/stylekit-sub";
|
||||
@import "app/ionicons";
|
||||
189
app/assets/templates/directives/account-menu.html.haml
Normal file
189
app/assets/templates/directives/account-menu.html.haml
Normal file
@@ -0,0 +1,189 @@
|
||||
.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
|
||||
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
|
||||
{{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'}
|
||||
|
||||
%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.
|
||||
.advanced-options.panel-row{"ng-if" => "formData.showAdvanced"}
|
||||
.panel-column.stretch
|
||||
%label Sync Server Domain
|
||||
%input.form-control.mt-5{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'formData.url'}
|
||||
|
||||
.button-group.stretch.panel-row.form-submit
|
||||
%button.button.info.featured{"type" => "submit"}
|
||||
.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)
|
||||
|
||||
%em.block.center-align.mt-10{"ng-if" => "formData.status", "style" => "font-size: 14px;"}
|
||||
{{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
|
||||
|
||||
%div{"ng-if" => "!formData.showLogin && !formData.showRegister && !formData.mfa"}
|
||||
.panel-section{"ng-if" => "user"}
|
||||
.panel-row
|
||||
%h2.title.wrap {{user.email}}
|
||||
.horizontal-group{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress || syncStatus.needsMoreSync", "delay" => "1000"}
|
||||
.spinner.small.info
|
||||
.sublabel
|
||||
{{"Syncing" + (syncStatus.total > 0 ? ":" : "")}}
|
||||
%span{"ng-if" => "syncStatus.total > 0"} {{syncStatus.current}}/{{syncStatus.total}}
|
||||
|
||||
.subtitle.danger.panel-row{"ng-if" => "syncStatus.error"} Error syncing: {{syncStatus.error.message}}
|
||||
|
||||
.subtitle.subtle.normal {{server}}
|
||||
|
||||
.panel-row
|
||||
|
||||
%a.panel-row.condensed{"ng-click" => "newPasswordData.changePassword = !newPasswordData.changePassword"} Change Password
|
||||
.notification.warning{"ng-if" => "newPasswordData.changePassword"}
|
||||
%h1.title Change Password
|
||||
.text
|
||||
%p Since your encryption key is based on your password, changing your password requires all your notes and tags to be re-encrypted using your new key.
|
||||
%p If you have thousands of items, this can take several minutes — you must keep the application window open during this process.
|
||||
%p After changing your password, you must log out of all other applications currently signed in to your account.
|
||||
%p.bold It is highly recommended you download a backup of your data before proceeding.
|
||||
.panel-row{"ng-if" => "!newPasswordData.status"}
|
||||
.horizontal-group{"ng-if" => "!newPasswordData.showForm"}
|
||||
%a.red{"ng-click" => "showPasswordChangeForm()"} Continue
|
||||
%a{"ng-click" => "newPasswordData.changePassword = false; newPasswordData.showForm = false"} Cancel
|
||||
.panel-row{"ng-if" => "newPasswordData.showForm"}
|
||||
%form.panel-form.stretch
|
||||
%input{:type => 'password', "ng-model" => "newPasswordData.newPassword", "placeholder" => "Enter new password"}
|
||||
%input{:type => 'password', "ng-model" => "newPasswordData.newPasswordConfirmation", "placeholder" => "Confirm new password"}
|
||||
.button-group.stretch.panel-row.form-submit
|
||||
.button.info{"type" => "submit", "ng-click" => "submitPasswordChange()"}
|
||||
.label Submit
|
||||
%a{"ng-click" => "newPasswordData.changePassword = false; newPasswordData.showForm = false"} Cancel
|
||||
|
||||
%p.italic.mt-10{"ng-if" => "newPasswordData.status"} {{newPasswordData.status}}
|
||||
|
||||
|
||||
|
||||
%a.panel-row.condensed{"ng-click" => "showAdvanced = !showAdvanced"} Advanced
|
||||
%div{"ng-if" => "showAdvanced"}
|
||||
%a.panel-row{"ng-click" => "reencryptPressed()"} Resync All Items
|
||||
|
||||
|
||||
%a.panel-row.condensed{"ng-if" => "securityUpdateAvailable()", "ng-click" => "clickedSecurityUpdate()"} Security Update Available
|
||||
.notification.default{"ng-if" => "securityUpdateData.showForm"}
|
||||
%p
|
||||
%a{"href" => "https://standardnotes.org/help/security-update", "target" => "_blank"} Learn more.
|
||||
%form.panel-form.stretch{"ng-if" => "!securityUpdateData.processing", "ng-submit" => "submitSecurityUpdateForm()"}
|
||||
%p Enter your password to update:
|
||||
%input.panel-row{:type => 'password', "ng-model" => "securityUpdateData.password", "placeholder" => "Enter password"}
|
||||
.button-group.stretch.panel-row.form-submit
|
||||
%button.button.info{"ng-type" => "submit"}
|
||||
.label Update
|
||||
.panel-row{"ng-if" => "securityUpdateData.processing"}
|
||||
%p.info Processing...
|
||||
|
||||
|
||||
.panel-section
|
||||
%h3.title.panel-row Encryption
|
||||
%h5.subtitle.info.panel-row{"ng-if" => "encryptionEnabled()"}
|
||||
{{encryptionStatusForNotes()}}
|
||||
%p
|
||||
{{encryptionStatusString()}}
|
||||
|
||||
.panel-section
|
||||
%h3.title.panel-row Passcode Lock
|
||||
%div{"ng-if" => "!hasPasscode()"}
|
||||
.panel-row{"ng-if" => "!formData.showPasscodeForm"}
|
||||
.button.info{"ng-click" => "addPasscodeClicked(); $event.stopPropagation();"}
|
||||
.label Add Passcode
|
||||
|
||||
%p Add an app passcode to lock the app and encrypt on-device key storage.
|
||||
|
||||
%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
|
||||
|
||||
%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
|
||||
|
||||
|
||||
|
||||
.panel-section{"ng-if" => "!importData.loading"}
|
||||
%h3.title Data Backups
|
||||
%form.panel-form{"ng-if" => "encryptedBackupsAvailable()"}
|
||||
.input-group
|
||||
%label
|
||||
%input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "true", "ng-change" => "archiveFormData.encrypted = true"}
|
||||
Encrypted
|
||||
%label
|
||||
%input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "false", "ng-change" => "archiveFormData.encrypted = false"}
|
||||
Decrypted
|
||||
|
||||
.button-group
|
||||
.button.info{"ng-click" => "downloadDataArchive()", "ng-class" => "{'mt-5' : !user}"}
|
||||
.label Download Backup
|
||||
|
||||
%label.button.info
|
||||
%input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"}
|
||||
.label Import From Backup
|
||||
|
||||
%div{"ng-if" => "importData.requestPassword"}
|
||||
%form.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
|
||||
.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;"}
|
||||
Cancel
|
||||
%a.right{"ng-if" => "!formData.showLogin && !formData.showRegister", "ng-click" => "destroyLocalData()"}
|
||||
{{ user ? "Sign out and clear local data" : "Clear all local data" }}
|
||||
32
app/assets/templates/directives/actions-menu.html.haml
Normal file
32
app/assets/templates/directives/actions-menu.html.haml
Normal file
@@ -0,0 +1,32 @@
|
||||
.sn-component
|
||||
.menu-panel.dropdown-menu
|
||||
|
||||
%a.no-decoration{"ng-if" => "extensions.length == 0", "href" => "https://standardnotes.org/extensions", "target" => "blank"}
|
||||
%menu-row{"title" => "'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"}
|
||||
%div{"ng-if" => "extension.hide"} …
|
||||
|
||||
%menu-row{"ng-if" => "!extension.hide", "ng-repeat" => "action in extension.actionsWithContextForItem(item)",
|
||||
"ng-click" => "executeAction(action, extension); $event.stopPropagation();", "title" => "action.label", "subtitle" => "action.desc",
|
||||
"spinner-class" => "action.running ? 'info' : null", "sub-rows" => "action.subrows"}
|
||||
.sublabel{"ng-if" => "action.access_type"}
|
||||
Uses
|
||||
%strong {{action.access_type}}
|
||||
access to this note.
|
||||
|
||||
|
||||
.modal.medium{"ng-if" => "renderData.showRenderModal", "ng-click" => "$event.stopPropagation();"}
|
||||
.content
|
||||
.sn-component
|
||||
.panel
|
||||
.header
|
||||
%h1.title Preview
|
||||
%a.close-button.info{"ng-click" => "renderData.showRenderModal = false; $event.stopPropagation();"} Close
|
||||
.content.selectable
|
||||
%h2 {{renderData.title}}
|
||||
%p.normal{"style" => "white-space: pre-wrap; font-family: monospace; font-size: 16px;"} {{renderData.text}}
|
||||
10
app/assets/templates/directives/component-modal.html.haml
Normal file
10
app/assets/templates/directives/component-modal.html.haml
Normal file
@@ -0,0 +1,10 @@
|
||||
.background{"ng-click" => "dismiss()"}
|
||||
|
||||
.content
|
||||
.sn-component
|
||||
.panel{"ng-attr-id" => "component-{{component.uuid}}"}
|
||||
.header
|
||||
%h1.title
|
||||
{{component.name}}
|
||||
%a.close-button.info{"ng-click" => "dismiss()"} Close
|
||||
%component-view.component-view{"component" => "component"}
|
||||
70
app/assets/templates/directives/component-view.html.haml
Normal file
70
app/assets/templates/directives/component-view.html.haml
Normal file
@@ -0,0 +1,70 @@
|
||||
.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, then open the "Extensions" menu via the bottom menu of the app to refresh your account data.
|
||||
Afterwards, press the button below to attempt to reload this component.
|
||||
.panel-row
|
||||
.button.info{"ng-if" => "!reloading", "ng-click" => "reloadStatus()"}
|
||||
.label Reload
|
||||
.spinner.info.small{"ng-if" => "reloading"}
|
||||
|
||||
.panel-row
|
||||
.panel-row
|
||||
.panel-column
|
||||
%p <strong>Otherwise</strong>, please follow the steps below to disable any external editors, so you can edit your note using the plain text editor instead.
|
||||
|
||||
%p
|
||||
%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" => "error == 'offline-restricted'"}
|
||||
.panel.static
|
||||
.content
|
||||
.panel-section.stretch
|
||||
%h2.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
|
||||
%p You can either:
|
||||
%p
|
||||
%ul
|
||||
%li <strong>Enable the Hosted option</strong> 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 <strong>Use the Desktop application.</strong>
|
||||
.panel-row
|
||||
.button.info{"ng-if" => "!reloading", "ng-click" => "reloadStatus()"}
|
||||
.label Reload
|
||||
.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.
|
||||
%p Please uninstall {{component.name}}, then re-install it.
|
||||
|
||||
%p
|
||||
This issue can occur if you access Standard Notes using an older version of the app.
|
||||
Ensure you are running at least version 2.1 on all platforms.
|
||||
|
||||
|
||||
%iframe{"ng-if" => "component && componentValid",
|
||||
"ng-attr-id" => "component-{{component.uuid}}",
|
||||
"ng-src" => "{{getUrl() | trusted}}", "frameBorder" => "0",
|
||||
"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
|
||||
26
app/assets/templates/directives/editor-menu.html.haml
Normal file
26
app/assets/templates/directives/editor-menu.html.haml
Normal file
@@ -0,0 +1,26 @@
|
||||
.sn-component
|
||||
.menu-panel.dropdown-menu
|
||||
.section
|
||||
.header
|
||||
%h4.title Note Editor
|
||||
%menu-row{"title" => "'Plain Editor'", "circle" => "selectedEditor == null && 'success'", "ng-click" => "selectComponent($event, null)"}
|
||||
|
||||
%menu-row{"ng-repeat" => "editor in editors", "ng-click" => "selectComponent($event, editor)", "title" => "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{"ng-if" => "editor.conflict_of"} Conflicted copy
|
||||
.sublabel{"ng-if" => "shouldDisplayRunningLocallyLabel(editor)"} Running Locally
|
||||
|
||||
%a.no-decoration{"ng-if" => "editors.length == 0", "href" => "https://standardnotes.org/extensions", "target" => "blank"}
|
||||
%menu-row{"title" => "'Download More Editors'"}
|
||||
|
||||
.section{"ng-if" => "stack.length > 0"}
|
||||
.header
|
||||
%h4.title Editor Stack
|
||||
%menu-row{"ng-repeat" => "component in stack", "ng-click" => "selectComponent($event, component)", "title" => "component.name",
|
||||
"circle" => "stackComponentEnabled(component) ? 'success' : 'danger'"}
|
||||
.column{"ng-if" => "component.conflict_of || shouldDisplayRunningLocallyLabel(component)"}
|
||||
%strong.red.medium{"ng-if" => "component.conflict_of"} Conflicted copy
|
||||
.sublabel{"ng-if" => "shouldDisplayRunningLocallyLabel(component)"} Running Locally
|
||||
21
app/assets/templates/directives/menu-row.html.haml
Normal file
21
app/assets/templates/directives/menu-row.html.haml
Normal file
@@ -0,0 +1,21 @@
|
||||
.row
|
||||
.column
|
||||
.left
|
||||
.column{"ng-if" => "circle"}
|
||||
.circle.small{"ng-class" => "circle"}
|
||||
.column{"ng-class" => "{'faded' : faded}"}
|
||||
.label
|
||||
{{title}}
|
||||
.sublabel{"ng-if" => "subtitle"}
|
||||
{{subtitle}}
|
||||
%ng-transclude
|
||||
.subrows{"ng-if" => "subRows && subRows.length > 0"}
|
||||
%menu-row{"ng-repeat" => "row in subRows", "ng-click" => "row.onClick($event); $event.stopPropagation();",
|
||||
"title" => "row.title", "subtitle" => "row.subtitle", "spinner-class" => "row.spinnerClass"}
|
||||
|
||||
.column{"ng-if" => "hasButton"}
|
||||
.button.info{"ng-click" => "clickButton($event)", "ng-class" => "buttonClass"}
|
||||
{{buttonText}}
|
||||
|
||||
.column{"ng-if" => "spinnerClass"}
|
||||
.spinner.small{"ng-class" => "spinnerClass"}
|
||||
1
app/assets/templates/directives/panel-resizer.html.haml
Normal file
1
app/assets/templates/directives/panel-resizer.html.haml
Normal file
@@ -0,0 +1 @@
|
||||
.panel-resizer-column
|
||||
22
app/assets/templates/directives/permissions-modal.html.haml
Normal file
22
app/assets/templates/directives/permissions-modal.html.haml
Normal file
@@ -0,0 +1,22 @@
|
||||
.background{"ng-click" => "deny()"}
|
||||
|
||||
.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
|
||||
%strong {{component.name}}
|
||||
would like to interact with your
|
||||
{{permissionsString()}}
|
||||
|
||||
.panel-row
|
||||
%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
|
||||
59
app/assets/templates/editor.html.haml
Normal file
59
app/assets/templates/editor.html.haml
Normal file
@@ -0,0 +1,59 @@
|
||||
.section.editor#editor-column
|
||||
#editor-title-bar.section-title-bar{"ng-show" => "ctrl.note && !ctrl.note.errorDecrypting"}
|
||||
.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"}
|
||||
|
||||
#save-status{"ng-class" => "{'red bold': ctrl.saveError, 'warning bold': ctrl.syncTakingTooLong}", "ng-bind-html" => "ctrl.noteStatus"}
|
||||
|
||||
.editor-tags
|
||||
#note-tags-component-container{"ng-if" => "ctrl.tagsComponent"}
|
||||
%component-view.component-view{ "component" => "ctrl.tagsComponent"}
|
||||
%input.tags-input{"ng-if" => "!(ctrl.tagsComponent && ctrl.tagsComponent.active)", "type" => "text", "ng-keyup" => "$event.keyCode == 13 && $event.target.blur();",
|
||||
"ng-model" => "ctrl.tagsString", "placeholder" => "#tags", "ng-blur" => "ctrl.updateTagsFromTagsString($event, ctrl.tagsString)",
|
||||
"spellcheck" => "false"}
|
||||
|
||||
.sn-component{"ng-if" => "ctrl.note"}
|
||||
.app-bar.no-edges
|
||||
.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 Menu
|
||||
.menu-panel.dropdown-menu{"ng-if" => "ctrl.showMenu"}
|
||||
.section
|
||||
.header
|
||||
%h4.title Note Options
|
||||
%menu-row{"title" => "ctrl.note.pinned ? 'Unpin' : 'Pin'", "ng-click" => "ctrl.selectedMenuItem($event, true); ctrl.togglePin()"}
|
||||
%menu-row{"title" => "ctrl.note.archived ? 'Unarchive' : 'Archive'", "ng-click" => "ctrl.selectedMenuItem($event, true); ctrl.toggleArchiveNote()"}
|
||||
%menu-row{"title" => "'Delete'", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.deleteNote()"}
|
||||
|
||||
.section{"ng-if" => "!ctrl.selectedEditor"}
|
||||
.header
|
||||
%h4.title Global Display
|
||||
%menu-row{"title" => "'Monospace Font'", "circle" => "ctrl.monospaceFont ? 'success' : 'default'", "ng-click" => "ctrl.selectedMenuItem($event, true); ctrl.toggleKey('monospaceFont')"}
|
||||
%menu-row{"title" => "'Spellcheck'", "circle" => "ctrl.spellcheck ? 'success' : 'default'", "ng-click" => "ctrl.selectedMenuItem($event, true); ctrl.toggleKey('spellcheck')"}
|
||||
|
||||
.item{"ng-click" => "ctrl.onEditorMenuClick()", "ng-class" => "{'selected' : ctrl.showEditorMenu}", "click-outside" => "ctrl.showEditorMenu = false;", "is-open" => "ctrl.showEditorMenu"}
|
||||
.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
|
||||
%actions-menu{"ng-if" => "ctrl.showExtensions", "item" => "ctrl.note"}
|
||||
|
||||
.editor-content#editor-content{"ng-if" => "ctrl.noteReady && !ctrl.note.errorDecrypting"}
|
||||
%panel-resizer.left{"panel-id" => "'editor-content'", "on-resize-finish" => "ctrl.onPanelResizeFinish","control" => "ctrl.resizeControl", "min-width" => 300, "property" => "'left'", "hoverable" => "true"}
|
||||
%component-view.component-view{"ng-if" => "ctrl.selectedEditor", "component" => "ctrl.selectedEditor"}
|
||||
%textarea.editable#note-text-editor{"ng-if" => "!ctrl.selectedEditor", "ng-model" => "ctrl.note.text",
|
||||
"ng-change" => "ctrl.contentChanged()", "ng-trim" => "false", "ng-click" => "ctrl.clickedTextArea()",
|
||||
"ng-focus" => "ctrl.onContentFocus()", "dir" => "auto", "ng-attr-spellcheck" => "{{ctrl.spellcheck}}"}
|
||||
{{ctrl.onSystemEditorLoad()}}
|
||||
%panel-resizer{"panel-id" => "'editor-content'", "on-resize-finish" => "ctrl.onPanelResizeFinish","control" => "ctrl.resizeControl", "min-width" => 300, "hoverable" => "true", "property" => "'right'"}
|
||||
|
||||
%section.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
|
||||
.sn-component
|
||||
%component-view.component-view.component-stack-item.border-color{"ng-repeat" => "component in ctrl.componentStack",
|
||||
"ng-if" => "component.active", "ng-show" => "!component.hidden", "manual-dealloc" => "true", "component" => "component"}
|
||||
41
app/assets/templates/footer.html.haml
Normal file
41
app/assets/templates/footer.html.haml
Normal file
@@ -0,0 +1,41 @@
|
||||
.sn-component
|
||||
#footer-bar.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
|
||||
%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"}
|
||||
Help
|
||||
|
||||
.item.border
|
||||
|
||||
.item{"ng-repeat" => "room in ctrl.rooms track by room.uuid"}
|
||||
.column{"ng-click" => "ctrl.selectRoom(room)"}
|
||||
.label {{room.name}}
|
||||
%component-modal{"ng-if" => "room.showRoom", "component" => "room", "on-dismiss" => "ctrl.onRoomDismiss"}
|
||||
|
||||
|
||||
.right
|
||||
|
||||
.item{"ng-if" => "ctrl.newUpdateAvailable", "ng-click" => "ctrl.clickedNewUpdateAnnouncement()"}
|
||||
%span.info.label New update downloaded. Installs on app restart.
|
||||
|
||||
.item.no-pointer{"ng-if" => "ctrl.lastSyncDate && !ctrl.isRefreshing"}
|
||||
.label.subtle
|
||||
Last refreshed {{ctrl.lastSyncDate | appDateTime}}
|
||||
.item{"ng-if" => "ctrl.lastSyncDate && ctrl.isRefreshing"}
|
||||
.spinner.small
|
||||
|
||||
.item{"ng-if" => "ctrl.offline"}
|
||||
.label Offline
|
||||
.item{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"}
|
||||
.label Refresh
|
||||
|
||||
.item#lock-item{"ng-if" => "ctrl.hasPasscode()"}
|
||||
.label
|
||||
%i.icon.ion-locked{"ng-if" => "ctrl.hasPasscode()", "ng-click" => "ctrl.lockApp()"}
|
||||
@@ -1,33 +0,0 @@
|
||||
%ul.dropdown-menu.sectioned-menu
|
||||
.extension{"ng-repeat" => "extension in extensions"}
|
||||
.header{"ng-click" => "extension.hide = !extension.hide"}
|
||||
.title {{extension.name}}
|
||||
.subtitle
|
||||
Will submit your note
|
||||
%strong {{accessTypeForExtension(extension)}}
|
||||
.spinner.loading{"ng-if" => "extension.loading"}
|
||||
%div{"ng-if" => "extension.hide"} …
|
||||
%ul{"ng-if" => "!extension.hide"}
|
||||
%li.menu-item{"ng-repeat" => "action in extension.actionsWithContextForItem(item)", "ng-click" => "executeAction(action, extension);",
|
||||
"ng-class" => "{'faded' : !isActionEnabled(action, extension)}"}
|
||||
%label.menu-item-title {{action.label}}
|
||||
.menu-item-subtitle {{action.desc}}
|
||||
|
||||
.small.normal{"ng-if" => "!isActionEnabled(action, extension)"}
|
||||
Requires {{action.access_type}} access to this note.
|
||||
|
||||
%div{"ng-if" => "action.showNestedActions"}
|
||||
%ul.mt-10
|
||||
%li.menu-item.white-bg.nested-hover{"ng-repeat" => "subaction in action.subactions", "ng-click" => "executeAction(subaction, extension, action); $event.stopPropagation();", "style" => "margin-top: -1px;"}
|
||||
%label.menu-item-title {{subaction.label}}
|
||||
.menu-item-subtitle {{subaction.desc}}
|
||||
%span{"ng-if" => "subaction.running"}
|
||||
.spinner{"style" => "margin-top: 3px;"}
|
||||
|
||||
%span{"ng-if" => "action.running"}
|
||||
.spinner{"style" => "margin-top: 3px;"}
|
||||
|
||||
.extension-render-modal{"ng-if" => "renderData.showRenderModal", "ng-click" => "renderData.showRenderModal = false"}
|
||||
.content
|
||||
%h2 {{renderData.title}}
|
||||
%p.normal{"style" => "white-space: pre-wrap; font-family: monospace; font-size: 16px;"} {{renderData.text}}
|
||||
@@ -1,18 +0,0 @@
|
||||
%ul.dropdown-menu.sectioned-menu
|
||||
.header
|
||||
.title System Editor
|
||||
%ul
|
||||
%li.menu-item{"ng-click" => "selectEditor($event, null)"}
|
||||
%span.pull-left.mr-10{"ng-if" => "selectedEditor == null"} ✓
|
||||
%label.menu-item-title.pull-left Plain
|
||||
|
||||
%div{"ng-if" => "editors.length > 0"}
|
||||
.header
|
||||
.title External Editors
|
||||
.subtitle Can access your current note decrypted.
|
||||
%ul
|
||||
%li.menu-item{"ng-repeat" => "editor in editors", "ng-click" => "selectEditor($event, editor)"}
|
||||
%strong.red.medium{"ng-if" => "editor.conflict_of"} Conflicted copy
|
||||
%label.menu-item-title
|
||||
%span.inline.tinted.mr-10{"ng-if" => "selectedEditor === editor"} ✓
|
||||
{{editor.name}}
|
||||
@@ -1,138 +0,0 @@
|
||||
.panel.panel-default.account-panel.panel-right#global-ext-menu
|
||||
.panel-body
|
||||
.container
|
||||
.float-group.h20
|
||||
%h1.tinted.pull-left Extensions
|
||||
%a.block.pull-right.dashboard-link{"href" => "https://dashboard.standardnotes.org", "target" => "_blank"} Open Dashboard
|
||||
%div.clear{"ng-if" => "!extensionManager.extensions.length && !themeManager.themes.length && !componentManager.components.length"}
|
||||
%p Customize your experience with editors, themes, and actions.
|
||||
.tinted-box.mt-10
|
||||
%h3 Available as part of the Extended subscription.
|
||||
%p.mt-5 Note history
|
||||
%p.mt-5 Automated backups
|
||||
%p.mt-5 Editors, themes, and actions
|
||||
%a{"href" => "https://standardnotes.org/extensions", "target" => "_blank"}
|
||||
%button.mt-10
|
||||
%h3 Learn More
|
||||
|
||||
%div{"ng-if" => "themeManager.themes.length > 0"}
|
||||
.header.container.section-margin
|
||||
%h2 Themes
|
||||
%ul
|
||||
%li{"ng-repeat" => "theme in themeManager.themes | orderBy: 'name'", "ng-click" => "clickedExtension(theme)"}
|
||||
.container
|
||||
%h3
|
||||
%input.bold{"ng-if" => "theme.rename", "ng-model" => "theme.tempName", "ng-keyup" => "$event.keyCode == 13 && submitExtensionRename(theme);", "mb-autofocus" => "true", "should-focus" => "true"}
|
||||
%span{"ng-if" => "!theme.rename"} {{theme.name}}
|
||||
%a{"ng-if" => "!themeManager.isThemeActive(theme)", "ng-click" => "themeManager.activateTheme(theme); $event.stopPropagation();"} Activate
|
||||
%a{"ng-if" => "themeManager.isThemeActive(theme)", "ng-click" => "themeManager.deactivateTheme(theme); $event.stopPropagation();"} Deactivate
|
||||
.mt-3{"ng-if" => "theme.showDetails"}
|
||||
.link-group
|
||||
%a{"ng-click" => "renameExtension(theme); $event.stopPropagation();"} Rename
|
||||
%a{"ng-click" => "theme.showLink = !theme.showLink; $event.stopPropagation();"} Show Link
|
||||
%a.red{"ng-click" => "deleteTheme(theme); $event.stopPropagation();"} Delete
|
||||
%p.small.selectable.wrap{"ng-if" => "theme.showLink"}
|
||||
{{theme.url}}
|
||||
|
||||
|
||||
%div{"ng-if" => "extensionManager.extensions.length"}
|
||||
.header.container.section-margin
|
||||
%h2 Actions
|
||||
%p{"style" => "margin-top: 3px;"} Choose "Actions" in the note editor to use installed actions.
|
||||
|
||||
%ul
|
||||
%li{"ng-repeat" => "extension in extensionManager.extensions | orderBy: 'name'", "ng-init" => "extension.formData = {}", "ng-click" => "clickedExtension(extension)"}
|
||||
.container
|
||||
%h3
|
||||
%input.bold{"ng-if" => "extension.rename", "ng-model" => "extension.tempName", "ng-keyup" => "$event.keyCode == 13 && submitExtensionRename(extension);", "mb-autofocus" => "true", "should-focus" => "true"}
|
||||
%span{"ng-if" => "!extension.rename"} {{extension.name}}
|
||||
%p.small{"ng-if" => "extension.description"} {{extension.description}}
|
||||
%div{"ng-if" => "extension.showDetails"}
|
||||
.mt-10
|
||||
%label.block Access Type
|
||||
%label.normal.block{"ng-click" => " $event.stopPropagation();"}
|
||||
%input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "true", "ng-change" => "changeExtensionEncryptionFormat(true, extension);"}
|
||||
Encrypted
|
||||
%label.normal.block{"ng-click" => " $event.stopPropagation();"}
|
||||
%input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "false", "ng-change" => "changeExtensionEncryptionFormat(false, extension);"}
|
||||
Decrypted
|
||||
|
||||
.small-v-space
|
||||
|
||||
%ul{"ng-repeat" => "action in extension.actionsInGlobalContext()"}
|
||||
%li
|
||||
%label.block {{action.label}}
|
||||
%em{"style" => "font-style: italic;"} {{action.desc}}
|
||||
%em{"ng-if" => "action.repeat_mode == 'watch'"}
|
||||
Repeats when a change is made to your items.
|
||||
%em{"ng-if" => "action.repeat_mode == 'loop'"}
|
||||
Repeats at most once every {{action.repeat_timeout}} seconds
|
||||
%div
|
||||
%a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}}
|
||||
%div{"ng-if" => "action.showPermissions"}
|
||||
{{action.permissionsString()}}
|
||||
%label.block.normal {{action.encryptionModeString()}}
|
||||
|
||||
%div
|
||||
.mt-5{"ng-if" => "action.repeat_mode"}
|
||||
%button.light.tinted{"ng-if" => "extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.disableRepeatAction(action, extension); $event.stopPropagation();"} Disable
|
||||
%button.light.tinted{"ng-if" => "!extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.enableRepeatAction(action, extension); $event.stopPropagation();"} Enable
|
||||
%button.light.mt-10{"ng-if" => "!action.running && !action.repeat_mode", "ng-click" => "selectedAction(action, extension); $event.stopPropagation();"}
|
||||
Perform Action
|
||||
.spinner.mb-5.block{"ng-if" => "action.running"}
|
||||
%p.mb-5.mt-5.small{"ng-if" => "!action.error && action.lastExecuted && !action.running"}
|
||||
Last run {{action.lastExecuted | appDateTime}}
|
||||
%label.red{"ng-if" => "action.error"}
|
||||
Error performing action.
|
||||
|
||||
%a.block.mt-5{"ng-click" => "renameExtension(extension); $event.stopPropagation();"} Rename
|
||||
%a.block.mt-5{"ng-click" => "extension.showURL = !extension.showURL; $event.stopPropagation();"} Show Link
|
||||
%p.wrap.selectable.small{"ng-if" => "extension.showURL"} {{extension.url}}
|
||||
%a.block.mt-5{"ng-click" => "deleteActionExtension(extension); $event.stopPropagation();"} Delete
|
||||
|
||||
%div{"ng-if" => "componentManager.components.length > 0"}
|
||||
.header.container.section-margin
|
||||
%h2 Components
|
||||
%ul
|
||||
%li{"ng-repeat" => "component in componentManager.components | orderBy: 'name'", "ng-click" => "clickedExtension(component)"}
|
||||
.container
|
||||
%h3
|
||||
%input.bold{"ng-if" => "component.rename", "ng-model" => "component.tempName", "ng-keyup" => "$event.keyCode == 13 && submitExtensionRename(component);", "mb-autofocus" => "true", "should-focus" => "true"}
|
||||
%span{"ng-if" => "!component.rename"} {{component.name}}
|
||||
|
||||
%div{"ng-if" => "component.isEditor()"}
|
||||
%a{"ng-if" => "!component.isDefaultEditor()", "ng-click" => "makeEditorDefault(component); $event.stopPropagation();"} Make Default
|
||||
%a{"ng-if" => "component.isDefaultEditor()", "ng-click" => "removeEditorDefault(component); $event.stopPropagation();"} Remove Default
|
||||
%div{"ng-if" => "!component.isEditor()"}
|
||||
%a{"ng-if" => "!componentManager.isComponentActive(component)", "ng-click" => "componentManager.activateComponent(component); $event.stopPropagation();"} Activate
|
||||
%a{"ng-if" => "componentManager.isComponentActive(component)", "ng-click" => "componentManager.deactivateComponent(component); $event.stopPropagation();"} Deactivate
|
||||
.mt-3{"ng-if" => "component.showDetails"}
|
||||
.link-group
|
||||
%a{"ng-click" => "renameExtension(component); $event.stopPropagation();"} Rename
|
||||
%a{"ng-click" => "component.showLink = !component.showLink; $event.stopPropagation();"} Show Link
|
||||
%a{"ng-if" => "component.permissions.length", "ng-click" => "revokePermissions(component); $event.stopPropagation();"} Revoke Permissions
|
||||
%a.red{"ng-click" => "deleteComponent(component); $event.stopPropagation();"} Delete
|
||||
%p.small.selectable.wrap{"ng-if" => "component.showLink"}
|
||||
{{component.url}}
|
||||
|
||||
%div{"ng-if" => "serverExtensions.length > 0"}
|
||||
.header.container.section-margin
|
||||
%h2 Server Extensions
|
||||
%ul
|
||||
%li{"ng-repeat" => "ext in serverExtensions", "ng-click" => "ext.showDetails = !ext.showDetails"}
|
||||
.container
|
||||
%strong.red.medium{"ng-if" => "ext.conflict_of"} Conflicted copy
|
||||
%h3 {{nameForServerExtension(ext)}}
|
||||
%div.mt-3{"ng-if" => "ext.showDetails"}
|
||||
.link-group
|
||||
%a{"ng-click" => "ext.showUrl = !ext.showUrl; $event.stopPropagation();"} Show Link
|
||||
%a.red{ "ng-click" => "deleteServerExt(ext); $event.stopPropagation();"} Delete
|
||||
.wrap.mt-5.selectable{"ng-if" => "ext.showUrl"} {{ext.url}}
|
||||
|
||||
.container.section-margin
|
||||
%h2.tinted Install
|
||||
%p.faded Enter an install link
|
||||
%form.mt-10.mb-10
|
||||
%input.form-control{:autofocus => 'autofocus', :name => 'url', :required => true, :autocomplete => "off",
|
||||
:type => 'url', 'ng-model' => 'formData.installLink', "ng-keyup" => "$event.keyCode == 13 && submitInstallLink();"}
|
||||
%p.tinted{"ng-if" => "formData.successfullyInstalled"} Successfully installed extension.
|
||||
@@ -1,25 +0,0 @@
|
||||
.background{"ng-click" => "dismiss()"}
|
||||
|
||||
.content
|
||||
%h3 The following extension has requested these permissions:
|
||||
|
||||
%h4 Extension
|
||||
%p Name: {{component.name}}
|
||||
%p.wrap URL: {{component.url}}
|
||||
|
||||
%h4 Permissions
|
||||
.permission{"ng-repeat" => "permission in formattedPermissions"}
|
||||
%p {{permission}}
|
||||
|
||||
%h4 Status
|
||||
%p.status{"ng-class" => "{'trusted tinted' : component.trusted}"} {{component.trusted ? 'Trusted' : 'Untrusted'}}
|
||||
|
||||
.learn-more
|
||||
%h4 Details
|
||||
%p
|
||||
Extensions use an offline messaging system to communicate. With <i>Trusted</i> extensions, data is never sent remotely without your consent. Learn more about extension permissions at
|
||||
%a{"href" => "https://standardnotes.org/permissions", "target" => "_blank"} https://standardnotes.org/permissions.
|
||||
|
||||
.buttons
|
||||
%button.standard.white{"ng-click" => "deny()"} Deny
|
||||
%button.standard.tinted{"ng-click" => "accept()"} Accept
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user