Merge 2.1
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user