Refactors most controllers and directives into classes for more organized and maintainable code
This commit is contained in:
1
.babelrc
1
.babelrc
@@ -9,6 +9,7 @@
|
|||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@babel/plugin-transform-runtime",
|
"@babel/plugin-transform-runtime",
|
||||||
|
"@babel/plugin-proposal-class-properties",
|
||||||
"angularjs-annotate"
|
"angularjs-annotate"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"extends": ["semistandard", "prettier"],
|
"extends": ["eslint:recommended", "semistandard", "prettier"],
|
||||||
|
"parser": "babel-eslint",
|
||||||
"rules": {
|
"rules": {
|
||||||
"standard/no-callback-literal": 0 // Disable this as we have too many callbacks relying on literals
|
"standard/no-callback-literal": 0, // Disable this as we have too many callbacks relying on literals
|
||||||
|
"no-throw-literal": 0,
|
||||||
|
"no-console": "error",
|
||||||
|
"semi": 1
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true
|
"browser": true
|
||||||
|
|||||||
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Chrome against localhost",
|
||||||
|
"url": "http://localhost:3001",
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -4,7 +4,11 @@ import angular from 'angular';
|
|||||||
import { configRoutes } from './routes';
|
import { configRoutes } from './routes';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Home,
|
AppState
|
||||||
|
} from './state';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Root,
|
||||||
TagsPanel,
|
TagsPanel,
|
||||||
NotesPanel,
|
NotesPanel,
|
||||||
EditorPanel,
|
EditorPanel,
|
||||||
@@ -65,7 +69,8 @@ import {
|
|||||||
StorageManager,
|
StorageManager,
|
||||||
SyncManager,
|
SyncManager,
|
||||||
ThemeManager,
|
ThemeManager,
|
||||||
AlertManager
|
AlertManager,
|
||||||
|
PreferencesManager
|
||||||
} from './services';
|
} from './services';
|
||||||
|
|
||||||
angular.module('app', ['ngSanitize']);
|
angular.module('app', ['ngSanitize']);
|
||||||
@@ -79,7 +84,7 @@ angular
|
|||||||
// Controllers
|
// Controllers
|
||||||
angular
|
angular
|
||||||
.module('app')
|
.module('app')
|
||||||
.directive('home', () => new Home())
|
.directive('root', () => new Root())
|
||||||
.directive('tagsPanel', () => new TagsPanel())
|
.directive('tagsPanel', () => new TagsPanel())
|
||||||
.directive('notesPanel', () => new NotesPanel())
|
.directive('notesPanel', () => new NotesPanel())
|
||||||
.directive('editorPanel', () => new EditorPanel())
|
.directive('editorPanel', () => new EditorPanel())
|
||||||
@@ -138,6 +143,8 @@ angular
|
|||||||
// Services
|
// Services
|
||||||
angular
|
angular
|
||||||
.module('app')
|
.module('app')
|
||||||
|
.service('appState', AppState)
|
||||||
|
.service('preferencesManager', PreferencesManager)
|
||||||
.service('actionsManager', ActionsManager)
|
.service('actionsManager', ActionsManager)
|
||||||
.service('archiveManager', ArchiveManager)
|
.service('archiveManager', ArchiveManager)
|
||||||
.service('authManager', AuthManager)
|
.service('authManager', AuthManager)
|
||||||
@@ -1,928 +0,0 @@
|
|||||||
|
|
||||||
import angular from 'angular';
|
|
||||||
import { SFModelManager } from 'snjs';
|
|
||||||
import { isDesktopApplication } from '@/utils';
|
|
||||||
import { KeyboardManager } from '@/services/keyboardManager';
|
|
||||||
import { PrivilegesManager } from '@/services/privilegesManager';
|
|
||||||
import template from '%/editor.pug';
|
|
||||||
|
|
||||||
export class EditorPanel {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.scope = {
|
|
||||||
remove: '&',
|
|
||||||
note: '=',
|
|
||||||
updateTags: '&'
|
|
||||||
};
|
|
||||||
|
|
||||||
this.template = template;
|
|
||||||
this.replace = true;
|
|
||||||
this.controllerAs = 'ctrl';
|
|
||||||
this.bindToController = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
link(scope, elem, attrs, ctrl) {
|
|
||||||
scope.$watch('ctrl.note', (note, oldNote) => {
|
|
||||||
if (note) {
|
|
||||||
ctrl.noteDidChange(note, oldNote);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller(
|
|
||||||
$timeout,
|
|
||||||
authManager,
|
|
||||||
$rootScope,
|
|
||||||
actionsManager,
|
|
||||||
syncManager,
|
|
||||||
modelManager,
|
|
||||||
themeManager,
|
|
||||||
componentManager,
|
|
||||||
storageManager,
|
|
||||||
sessionHistory,
|
|
||||||
privilegesManager,
|
|
||||||
keyboardManager,
|
|
||||||
desktopManager,
|
|
||||||
alertManager
|
|
||||||
) {
|
|
||||||
this.spellcheck = true;
|
|
||||||
this.componentManager = componentManager;
|
|
||||||
this.componentStack = [];
|
|
||||||
this.isDesktop = isDesktopApplication();
|
|
||||||
|
|
||||||
const MinimumStatusDurationMs = 400;
|
|
||||||
|
|
||||||
syncManager.addEventHandler((eventName, data) => {
|
|
||||||
if(!this.note) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(eventName == "sync:taking-too-long") {
|
|
||||||
this.syncTakingTooLong = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
else if(eventName == "sync:completed") {
|
|
||||||
this.syncTakingTooLong = false;
|
|
||||||
if(this.note.dirty) {
|
|
||||||
// if we're still dirty, don't change status, a sync is likely upcoming.
|
|
||||||
} else {
|
|
||||||
let savedItem = data.savedItems.find((item) => item.uuid == this.note.uuid);
|
|
||||||
let isInErrorState = this.saveError;
|
|
||||||
if(isInErrorState || savedItem) {
|
|
||||||
this.showAllChangesSavedStatus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if(eventName == "sync:error") {
|
|
||||||
// only show error status in editor if the note is dirty. Otherwise, it means the originating sync
|
|
||||||
// came from somewhere else and we don't want to display an error here.
|
|
||||||
if(this.note.dirty){
|
|
||||||
this.showErrorStatus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Right now this only handles offline saving status changes.
|
|
||||||
this.syncStatusObserver = syncManager.registerSyncStatusObserver((status) => {
|
|
||||||
if(status.localError) {
|
|
||||||
$timeout(() => {
|
|
||||||
this.showErrorStatus({
|
|
||||||
message: "Offline Saving Issue",
|
|
||||||
desc: "Changes not saved"
|
|
||||||
});
|
|
||||||
}, 500)
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
modelManager.addItemSyncObserver("editor-note-observer", "Note", (allItems, validItems, deletedItems, source) => {
|
|
||||||
if(!this.note) { return; }
|
|
||||||
|
|
||||||
// Before checking if isMappingSourceRetrieved, we check if this item was deleted via a local source,
|
|
||||||
// such as alternating uuids during sign in. Otherwise, we only want to make interface updates if it's a
|
|
||||||
// remote retrieved source.
|
|
||||||
if(this.note.deleted || this.note.content.trashed) {
|
|
||||||
$rootScope.notifyDelete();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!SFModelManager.isMappingSourceRetrieved(source)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var matchingNote = allItems.find((item) => {
|
|
||||||
return item.uuid == this.note.uuid;
|
|
||||||
});
|
|
||||||
|
|
||||||
if(!matchingNote) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update tags
|
|
||||||
this.loadTagsString();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelManager.addItemSyncObserver("editor-tag-observer", "Tag", (allItems, validItems, deletedItems, source) => {
|
|
||||||
if(!this.note) { return; }
|
|
||||||
|
|
||||||
for(var tag of allItems) {
|
|
||||||
// If a tag is deleted then we'll have lost references to notes. Reload anyway.
|
|
||||||
if(this.note.savedTagsString == null || tag.deleted || tag.hasRelationshipWithItem(this.note)) {
|
|
||||||
this.loadTagsString();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
modelManager.addItemSyncObserver("editor-component-observer", "SN|Component", (allItems, validItems, deletedItems, source) => {
|
|
||||||
if(!this.note) { return; }
|
|
||||||
|
|
||||||
// Reload componentStack in case new ones were added or removed
|
|
||||||
this.reloadComponentStackArray();
|
|
||||||
|
|
||||||
// Observe editor changes to see if the current note should update its editor
|
|
||||||
var editors = allItems.filter(function(item) {
|
|
||||||
return item.isEditor();
|
|
||||||
});
|
|
||||||
|
|
||||||
// If no editors have changed
|
|
||||||
if(editors.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look through editors again and find the most proper one
|
|
||||||
var editor = this.editorForNote(this.note);
|
|
||||||
this.selectedEditor = editor;
|
|
||||||
if(!editor) {
|
|
||||||
this.reloadFont();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.noteDidChange = function(note, oldNote) {
|
|
||||||
this.setNote(note, oldNote);
|
|
||||||
this.reloadComponentContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setNote = function(note, oldNote) {
|
|
||||||
this.showExtensions = false;
|
|
||||||
this.showMenu = false;
|
|
||||||
this.noteStatus = null;
|
|
||||||
// When setting alt key down and deleting note, an alert will come up and block the key up event when alt is released.
|
|
||||||
// We reset it on set note so that the alt menu restores to default.
|
|
||||||
this.altKeyDown = false;
|
|
||||||
this.loadTagsString();
|
|
||||||
|
|
||||||
let onReady = () => {
|
|
||||||
this.noteReady = true;
|
|
||||||
$timeout(() => {
|
|
||||||
this.loadPreferences();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let associatedEditor = this.editorForNote(note);
|
|
||||||
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;
|
|
||||||
// switch after timeout, so that note data isnt posted to current editor
|
|
||||||
$timeout(() => {
|
|
||||||
this.selectedEditor = associatedEditor;
|
|
||||||
onReady();
|
|
||||||
})
|
|
||||||
} else if(associatedEditor) {
|
|
||||||
// Same editor as currently active
|
|
||||||
onReady();
|
|
||||||
} else {
|
|
||||||
// No editor
|
|
||||||
this.selectedEditor = null;
|
|
||||||
onReady();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(note.safeText().length == 0 && note.dummy) {
|
|
||||||
this.focusTitle(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(oldNote && oldNote != note) {
|
|
||||||
if(oldNote.dummy) {
|
|
||||||
this.remove()(oldNote);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.editorForNote = function(note) {
|
|
||||||
return componentManager.editorForNote(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.closeAllMenus = function() {
|
|
||||||
this.showEditorMenu = false;
|
|
||||||
this.showMenu = false;
|
|
||||||
this.showExtensions = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.toggleMenu = function(menu) {
|
|
||||||
this[menu] = !this[menu];
|
|
||||||
|
|
||||||
let allMenus = ['showMenu', 'showEditorMenu', 'showExtensions', 'showSessionHistory'];
|
|
||||||
for(var candidate of allMenus) {
|
|
||||||
if(candidate != menu) {
|
|
||||||
this[candidate] = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.editorMenuOnSelect = function(component) {
|
|
||||||
if(!component || component.area == "editor-editor") {
|
|
||||||
// if plain editor or other editor
|
|
||||||
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);
|
|
||||||
modelManager.setItemDirty(this.note, true);
|
|
||||||
}
|
|
||||||
this.associateComponentWithCurrentNote(editor);
|
|
||||||
} else {
|
|
||||||
// Note prefers plain editor
|
|
||||||
if(!this.note.getAppDataItem("prefersPlainEditor")) {
|
|
||||||
this.note.setAppDataItem("prefersPlainEditor", true);
|
|
||||||
modelManager.setItemDirty(this.note, true);
|
|
||||||
}
|
|
||||||
$timeout(() => {
|
|
||||||
this.reloadFont();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedEditor = editor;
|
|
||||||
} else if(component.area == "editor-stack") {
|
|
||||||
// If component stack item
|
|
||||||
this.toggleStackComponentForCurrentItem(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lots of dirtying can happen above, so we'll sync
|
|
||||||
syncManager.sync();
|
|
||||||
}.bind(this)
|
|
||||||
|
|
||||||
this.hasAvailableExtensions = function() {
|
|
||||||
return actionsManager.extensionsInContextOfItem(this.note).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.focusEditor = function(delay) {
|
|
||||||
setTimeout(function(){
|
|
||||||
var element = document.getElementById("note-text-editor");
|
|
||||||
if(element) {
|
|
||||||
element.focus();
|
|
||||||
}
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.focusTitle = function(delay) {
|
|
||||||
setTimeout(function(){
|
|
||||||
document.getElementById("note-title-editor").focus();
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clickedTextArea = function() {
|
|
||||||
this.showMenu = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.EditorNgDebounce = 200;
|
|
||||||
const SyncDebouce = 350;
|
|
||||||
const SyncNoDebounce = 100;
|
|
||||||
|
|
||||||
this.saveNote = function({bypassDebouncer, updateClientModified, dontUpdatePreviews}) {
|
|
||||||
let note = this.note;
|
|
||||||
note.dummy = false;
|
|
||||||
|
|
||||||
if(note.deleted) {
|
|
||||||
alertManager.alert({text: "The note you are attempting to edit has been deleted, and is awaiting sync. Changes you make will be disregarded."});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!modelManager.findItem(note.uuid)) {
|
|
||||||
alertManager.alert({text: "The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note."});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showSavingStatus();
|
|
||||||
|
|
||||||
if(!dontUpdatePreviews) {
|
|
||||||
let limit = 80;
|
|
||||||
var text = note.text || "";
|
|
||||||
var truncate = text.length > limit;
|
|
||||||
note.content.preview_plain = text.substring(0, limit) + (truncate ? "..." : "");
|
|
||||||
// Clear dynamic previews if using plain editor
|
|
||||||
note.content.preview_html = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
modelManager.setItemDirty(note, true, updateClientModified);
|
|
||||||
|
|
||||||
if(this.saveTimeout) {
|
|
||||||
$timeout.cancel(this.saveTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
let syncDebouceMs;
|
|
||||||
if(authManager.offline() || bypassDebouncer) {
|
|
||||||
syncDebouceMs = SyncNoDebounce;
|
|
||||||
} else {
|
|
||||||
syncDebouceMs = SyncDebouce;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveTimeout = $timeout(() => {
|
|
||||||
syncManager.sync().then((response) => {
|
|
||||||
if(response && response.error && !this.didShowErrorAlert) {
|
|
||||||
this.didShowErrorAlert = true;
|
|
||||||
alertManager.alert({text: "There was an error saving your note. Please try again."});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, syncDebouceMs)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showSavingStatus = function() {
|
|
||||||
this.setStatus({message: "Saving..."}, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showAllChangesSavedStatus = function() {
|
|
||||||
this.saveError = false;
|
|
||||||
this.syncTakingTooLong = false;
|
|
||||||
|
|
||||||
var status = "All changes saved";
|
|
||||||
if(authManager.offline()) {
|
|
||||||
status += " (offline)";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setStatus({message: status});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showErrorStatus = function(error) {
|
|
||||||
if(!error) {
|
|
||||||
error = {
|
|
||||||
message: "Sync Unreachable",
|
|
||||||
desc: "Changes saved offline"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.saveError = true;
|
|
||||||
this.syncTakingTooLong = false;
|
|
||||||
this.setStatus(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setStatus = function(status, wait = true) {
|
|
||||||
// Keep every status up for a minimum duration so it doesnt flash crazily.
|
|
||||||
let waitForMs;
|
|
||||||
if(!this.noteStatus || !this.noteStatus.date) {
|
|
||||||
waitForMs = 0;
|
|
||||||
} else {
|
|
||||||
waitForMs = MinimumStatusDurationMs - (new Date() - this.noteStatus.date);
|
|
||||||
}
|
|
||||||
if(!wait || waitForMs < 0) {waitForMs = 0;}
|
|
||||||
if(this.statusTimeout) $timeout.cancel(this.statusTimeout);
|
|
||||||
this.statusTimeout = $timeout(() => {
|
|
||||||
status.date = new Date();
|
|
||||||
this.noteStatus = status;
|
|
||||||
}, waitForMs)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.contentChanged = function() {
|
|
||||||
this.saveNote({updateClientModified: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onTitleEnter = function($event) {
|
|
||||||
$event.target.blur();
|
|
||||||
this.onTitleChange();
|
|
||||||
this.focusEditor();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onTitleChange = function() {
|
|
||||||
this.saveNote({dontUpdatePreviews: true, updateClientModified: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onNameFocus = function() {
|
|
||||||
this.editingName = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onContentFocus = function() {
|
|
||||||
$rootScope.$broadcast("editorFocused");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onNameBlur = function() {
|
|
||||||
this.editingName = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedMenuItem = function(hide) {
|
|
||||||
if(hide) {
|
|
||||||
this.showMenu = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.deleteNote = async function(permanently) {
|
|
||||||
if(this.note.dummy) {
|
|
||||||
alertManager.alert({text: "This note is a placeholder and cannot be deleted. To remove from your list, simply navigate to a different note."});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let run = () => {
|
|
||||||
$timeout(() => {
|
|
||||||
if(this.note.locked) {
|
|
||||||
alertManager.alert({text: "This note is locked. If you'd like to delete it, unlock it, and try again."});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = this.note.safeTitle().length ? `'${this.note.title}'` : "this note";
|
|
||||||
let text = permanently ? `Are you sure you want to permanently delete ${title}?`
|
|
||||||
: `Are you sure you want to move ${title} to the trash?`
|
|
||||||
|
|
||||||
alertManager.confirm({text, destructive: true, onConfirm: () => {
|
|
||||||
if(permanently) {
|
|
||||||
this.remove()(this.note);
|
|
||||||
} else {
|
|
||||||
this.note.content.trashed = true;
|
|
||||||
this.saveNote({bypassDebouncer: true, dontUpdatePreviews: true});
|
|
||||||
}
|
|
||||||
this.showMenu = false;
|
|
||||||
}})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionDeleteNote)) {
|
|
||||||
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionDeleteNote, () => {
|
|
||||||
run();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.restoreTrashedNote = function() {
|
|
||||||
this.note.content.trashed = false;
|
|
||||||
this.saveNote({bypassDebouncer: true, dontUpdatePreviews: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.deleteNotePermanantely = function() {
|
|
||||||
this.deleteNote(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getTrashCount = function() {
|
|
||||||
return modelManager.trashedItems().length;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emptyTrash = function() {
|
|
||||||
let count = this.getTrashCount();
|
|
||||||
alertManager.confirm({text: `Are you sure you want to permanently delete ${count} note(s)?`, destructive: true, onConfirm: () => {
|
|
||||||
modelManager.emptyTrash();
|
|
||||||
syncManager.sync();
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.togglePin = function() {
|
|
||||||
this.note.setAppDataItem("pinned", !this.note.pinned);
|
|
||||||
this.saveNote({bypassDebouncer: true, dontUpdatePreviews: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.toggleLockNote = function() {
|
|
||||||
this.note.setAppDataItem("locked", !this.note.locked);
|
|
||||||
this.saveNote({bypassDebouncer: true, dontUpdatePreviews: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.toggleProtectNote = function() {
|
|
||||||
this.note.content.protected = !this.note.content.protected;
|
|
||||||
this.saveNote({bypassDebouncer: true, dontUpdatePreviews: true});
|
|
||||||
|
|
||||||
// Show privilegesManager if Protection is not yet set up
|
|
||||||
privilegesManager.actionHasPrivilegesConfigured(PrivilegesManager.ActionViewProtectedNotes).then((configured) => {
|
|
||||||
if(!configured) {
|
|
||||||
privilegesManager.presentPrivilegesManagementModal();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.toggleNotePreview = function() {
|
|
||||||
this.note.content.hidePreview = !this.note.content.hidePreview;
|
|
||||||
this.saveNote({bypassDebouncer: true, dontUpdatePreviews: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.toggleArchiveNote = function() {
|
|
||||||
this.note.setAppDataItem("archived", !this.note.archived);
|
|
||||||
this.saveNote({bypassDebouncer: true, dontUpdatePreviews: true});
|
|
||||||
$rootScope.$broadcast("noteArchived");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clickedEditNote = function() {
|
|
||||||
this.focusEditor(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
Tags
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.loadTagsString = function() {
|
|
||||||
this.tagsString = this.note.tagsString();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addTag = function(tag) {
|
|
||||||
var tags = this.note.tags;
|
|
||||||
var strings = tags.map(function(_tag){
|
|
||||||
return _tag.title;
|
|
||||||
})
|
|
||||||
strings.push(tag.title);
|
|
||||||
this.updateTags()(this.note, strings);
|
|
||||||
this.loadTagsString();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.removeTag = function(tag) {
|
|
||||||
var tags = this.note.tags;
|
|
||||||
var strings = tags.map(function(_tag){
|
|
||||||
return _tag.title;
|
|
||||||
}).filter(function(_tag){
|
|
||||||
return _tag !== tag.title;
|
|
||||||
})
|
|
||||||
this.updateTags()(this.note, strings);
|
|
||||||
this.loadTagsString();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateTagsFromTagsString = function() {
|
|
||||||
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, strings);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Resizability */
|
|
||||||
|
|
||||||
this.leftResizeControl = {};
|
|
||||||
this.rightResizeControl = {};
|
|
||||||
|
|
||||||
this.onPanelResizeFinish = (width, left, isMaxWidth) => {
|
|
||||||
if(isMaxWidth) {
|
|
||||||
authManager.setUserPrefValue("editorWidth", null);
|
|
||||||
} else {
|
|
||||||
if(width !== undefined && width !== null) {
|
|
||||||
authManager.setUserPrefValue("editorWidth", width);
|
|
||||||
this.leftResizeControl.setWidth(width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(left !== undefined && left !== null) {
|
|
||||||
authManager.setUserPrefValue("editorLeft", left);
|
|
||||||
this.rightResizeControl.setLeft(left);
|
|
||||||
}
|
|
||||||
authManager.syncUserPreferences();
|
|
||||||
}
|
|
||||||
|
|
||||||
$rootScope.$on("user-preferences-changed", () => {
|
|
||||||
this.loadPreferences();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loadPreferences = function() {
|
|
||||||
this.monospaceFont = authManager.getUserPrefValue("monospaceFont", "monospace");
|
|
||||||
|
|
||||||
// On desktop application, disable spellcheck by default, as it is not performant.
|
|
||||||
let defaultSpellcheckStatus = isDesktopApplication() ? false : true;
|
|
||||||
this.spellcheck = authManager.getUserPrefValue("spellcheck", defaultSpellcheckStatus);
|
|
||||||
|
|
||||||
this.marginResizersEnabled = authManager.getUserPrefValue("marginResizersEnabled", true);
|
|
||||||
|
|
||||||
if(!document.getElementById("editor-content")) {
|
|
||||||
// Elements have not yet loaded due to ng-if around wrapper
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reloadFont();
|
|
||||||
|
|
||||||
if(this.marginResizersEnabled) {
|
|
||||||
let width = authManager.getUserPrefValue("editorWidth", null);
|
|
||||||
if(width !== null) {
|
|
||||||
this.leftResizeControl.setWidth(width);
|
|
||||||
this.rightResizeControl.setWidth(width);
|
|
||||||
}
|
|
||||||
|
|
||||||
let left = authManager.getUserPrefValue("editorLeft", null);
|
|
||||||
if(left !== null) {
|
|
||||||
this.leftResizeControl.setLeft(left);
|
|
||||||
this.rightResizeControl.setLeft(left);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reloadFont = function() {
|
|
||||||
var editable = document.getElementById("note-text-editor");
|
|
||||||
|
|
||||||
if(!editable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.monospaceFont) {
|
|
||||||
if(isDesktopApplication()) {
|
|
||||||
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)
|
|
||||||
} else if(key == "marginResizersEnabled" && this[key] == true) {
|
|
||||||
$timeout(() => {
|
|
||||||
this.leftResizeControl.flash();
|
|
||||||
this.rightResizeControl.flash();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
Components
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.onEditorLoad = function(editor) {
|
|
||||||
desktopManager.redoSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
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-editor") {
|
|
||||||
// An editor is already active, ensure the potential replacement is explicitely enabled for this item
|
|
||||||
// We also check if the selectedEditor is active. If it's inactive, we want to treat it as an external reference wishing to deactivate this editor (i.e componentView)
|
|
||||||
if(this.selectedEditor && this.selectedEditor == component && component.active == false) {
|
|
||||||
this.selectedEditor = null;
|
|
||||||
}
|
|
||||||
else if(this.selectedEditor) {
|
|
||||||
if(this.selectedEditor.active) {
|
|
||||||
// In the case where an editor is duplicated, then you'll have two editors who are explicitely enabled for the same note.
|
|
||||||
// This will cause an infinite loop, where as soon as the first is enabled, the second will come in, pass the `isExplicitlyEnabledForItem` check,
|
|
||||||
// and replace the previous one. So we now check to make the current editor isn't also explicitely enabled, and if it is, then we'll just keep that one active.
|
|
||||||
if(component.isExplicitlyEnabledForItem(this.note) && !this.selectedEditor.isExplicitlyEnabledForItem(this.note)) {
|
|
||||||
this.selectedEditor = component;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// If no selected editor, let's see if the incoming one is a candidate
|
|
||||||
if(component.active && this.note && (component.isExplicitlyEnabledForItem(this.note) || component.isDefaultEditor())) {
|
|
||||||
this.selectedEditor = component;
|
|
||||||
} else {
|
|
||||||
// Not a candidate, and no selected editor. Disable the current editor by setting selectedEditor to null
|
|
||||||
this.selectedEditor = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if(component.area == "editor-stack") {
|
|
||||||
this.reloadComponentContext();
|
|
||||||
}
|
|
||||||
}, contextRequestHandler: (component) => {
|
|
||||||
if(component == this.selectedEditor || component == this.tagsComponent || this.componentStack.includes(component)) {
|
|
||||||
return this.note;
|
|
||||||
}
|
|
||||||
}, 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`;
|
|
||||||
var heightString = typeof size.height === 'string' ? size.height : `${data.height}px`;
|
|
||||||
element.setAttribute("style", `width:${widthString}; height:${heightString}; `);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(data.type == "container") {
|
|
||||||
if(component.area == "note-tags") {
|
|
||||||
var container = document.getElementById("note-tags-component-container");
|
|
||||||
setSize(container, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if(action === "associate-item") {
|
|
||||||
if(data.item.content_type == "Tag") {
|
|
||||||
var tag = modelManager.findItem(data.item.uuid);
|
|
||||||
this.addTag(tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if(action === "deassociate-item") {
|
|
||||||
var tag = modelManager.findItem(data.item.uuid);
|
|
||||||
this.removeTag(tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if(action === "save-items") {
|
|
||||||
if(data.items.map((item) => {return item.uuid}).includes(this.note.uuid)) {
|
|
||||||
this.showSavingStatus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}});
|
|
||||||
|
|
||||||
this.reloadComponentStackArray = function() {
|
|
||||||
this.componentStack = componentManager.componentsForArea("editor-stack").sort((a, b) => {
|
|
||||||
// Careful here. For some reason (probably because re-assigning array everytime quickly destroys componentView elements, causing deallocs),
|
|
||||||
// sorting by updated_at (or any other property that may always be changing)
|
|
||||||
// causes weird problems with ext communication when changing notes or activating/deactivating in quick succession
|
|
||||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reloadComponentContext = function() {
|
|
||||||
// componentStack is used by the template to ng-repeat
|
|
||||||
this.reloadComponentStackArray();
|
|
||||||
/*
|
|
||||||
In the past, we were doing this looping code even if the note wasn't currently defined.
|
|
||||||
The problem is if an editor stack item loaded first, requested to stream items, and the note was undefined,
|
|
||||||
we would set component.hidden = true. Which means messages would not be sent to the component.
|
|
||||||
Theoretically, upon the note loading, we would run this code again, and unhide the extension.
|
|
||||||
However, if you had requested to stream items when it was hidden, and then it unhid, it would never
|
|
||||||
resend those items upon unhiding.
|
|
||||||
|
|
||||||
Our solution here is to check that the note is defined before setting hidden. The question remains, when
|
|
||||||
would note really ever be undefined? Maybe temprarily when you're deleting a note?
|
|
||||||
*/
|
|
||||||
if(this.note) {
|
|
||||||
for(var component of this.componentStack) {
|
|
||||||
if(component.active) {
|
|
||||||
componentManager.setComponentHidden(component, !component.isExplicitlyEnabledForItem(this.note));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentManager.contextItemDidChangeInArea("note-tags");
|
|
||||||
componentManager.contextItemDidChangeInArea("editor-stack");
|
|
||||||
componentManager.contextItemDidChangeInArea("editor-editor");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.toggleStackComponentForCurrentItem = function(component) {
|
|
||||||
// If it's hidden, we want to show it
|
|
||||||
// If it's not active, then hidden won't be set, and we mean to activate and show it.
|
|
||||||
if(component.hidden || !component.active) {
|
|
||||||
// Unhide, associate with current item
|
|
||||||
componentManager.setComponentHidden(component, false);
|
|
||||||
this.associateComponentWithCurrentNote(component);
|
|
||||||
if(!component.active) {
|
|
||||||
componentManager.activateComponent(component);
|
|
||||||
}
|
|
||||||
componentManager.contextItemDidChangeInArea("editor-stack");
|
|
||||||
} else {
|
|
||||||
// not hidden, hide
|
|
||||||
componentManager.setComponentHidden(component, 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
modelManager.setItemDirty(component, true);
|
|
||||||
syncManager.sync();
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
modelManager.setItemDirty(component, true);
|
|
||||||
syncManager.sync();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.altKeyObserver = keyboardManager.addKeyObserver({
|
|
||||||
modifiers: [KeyboardManager.KeyModifierAlt],
|
|
||||||
onKeyDown: () => {
|
|
||||||
$timeout(() => {
|
|
||||||
this.altKeyDown = true;
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onKeyUp: () => {
|
|
||||||
$timeout(() => {
|
|
||||||
this.altKeyDown = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.trashKeyObserver = keyboardManager.addKeyObserver({
|
|
||||||
key: KeyboardManager.KeyBackspace,
|
|
||||||
notElementIds: ["note-text-editor", "note-title-editor"],
|
|
||||||
modifiers: [KeyboardManager.KeyModifierMeta],
|
|
||||||
onKeyDown: () => {
|
|
||||||
$timeout(() => {
|
|
||||||
this.deleteNote();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
this.deleteKeyObserver = keyboardManager.addKeyObserver({
|
|
||||||
key: KeyboardManager.KeyBackspace,
|
|
||||||
modifiers: [KeyboardManager.KeyModifierMeta, KeyboardManager.KeyModifierShift, KeyboardManager.KeyModifierAlt],
|
|
||||||
onKeyDown: (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
$timeout(() => {
|
|
||||||
this.deleteNote(true);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
/*
|
|
||||||
Editor Customization
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.onSystemEditorLoad = function() {
|
|
||||||
if(this.loadedTabListener) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.loadedTabListener = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert 4 spaces when a tab key is pressed,
|
|
||||||
* only used when inside of the text editor.
|
|
||||||
* If the shift key is pressed first, this event is
|
|
||||||
* not fired.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const editor = document.getElementById("note-text-editor");
|
|
||||||
this.tabObserver = keyboardManager.addKeyObserver({
|
|
||||||
element: editor,
|
|
||||||
key: KeyboardManager.KeyTab,
|
|
||||||
onKeyDown: (event) => {
|
|
||||||
if(event.shiftKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.note.locked) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
// Using document.execCommand gives us undo support
|
|
||||||
let insertSuccessful = document.execCommand("insertText", false, "\t");
|
|
||||||
if(!insertSuccessful) {
|
|
||||||
// document.execCommand works great on Chrome/Safari but not Firefox
|
|
||||||
var start = editor.selectionStart;
|
|
||||||
var end = editor.selectionEnd;
|
|
||||||
var spaces = " ";
|
|
||||||
|
|
||||||
// Insert 4 spaces
|
|
||||||
editor.value = editor.value.substring(0, start)
|
|
||||||
+ spaces + editor.value.substring(end);
|
|
||||||
|
|
||||||
// Place cursor 4 spaces away from where
|
|
||||||
// the tab key was pressed
|
|
||||||
editor.selectionStart = editor.selectionEnd = start + 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
$timeout(() => {
|
|
||||||
this.note.text = editor.value;
|
|
||||||
this.saveNote({bypassDebouncer: true});
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// This handles when the editor itself is destroyed, and not when our controller is destroyed.
|
|
||||||
angular.element(editor).on('$destroy', () => {
|
|
||||||
if(this.tabObserver) {
|
|
||||||
keyboardManager.removeKeyObserver(this.tabObserver);
|
|
||||||
this.loadedTabListener = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,328 +0,0 @@
|
|||||||
import { PrivilegesManager } from '@/services/privilegesManager';
|
|
||||||
import template from '%/footer.pug';
|
|
||||||
|
|
||||||
export class Footer {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.scope = {};
|
|
||||||
this.template = template;
|
|
||||||
this.replace = true;
|
|
||||||
this.controllerAs = 'ctrl';
|
|
||||||
this.bindToController = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
link(scope, elem, attrs, ctrl) {
|
|
||||||
scope.$on('sync:completed', function() {
|
|
||||||
ctrl.syncUpdated();
|
|
||||||
ctrl.findErrors();
|
|
||||||
ctrl.updateOfflineStatus();
|
|
||||||
});
|
|
||||||
scope.$on('sync:error', function() {
|
|
||||||
ctrl.findErrors();
|
|
||||||
ctrl.updateOfflineStatus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller(
|
|
||||||
$rootScope,
|
|
||||||
authManager,
|
|
||||||
modelManager,
|
|
||||||
$timeout,
|
|
||||||
dbManager,
|
|
||||||
syncManager,
|
|
||||||
storageManager,
|
|
||||||
passcodeManager,
|
|
||||||
componentManager,
|
|
||||||
singletonManager,
|
|
||||||
nativeExtManager,
|
|
||||||
privilegesManager,
|
|
||||||
statusManager,
|
|
||||||
alertManager
|
|
||||||
) {
|
|
||||||
authManager.checkForSecurityUpdate().then((available) => {
|
|
||||||
this.securityUpdateAvailable = available;
|
|
||||||
})
|
|
||||||
|
|
||||||
$rootScope.$on("security-update-status-changed", () => {
|
|
||||||
this.securityUpdateAvailable = authManager.securityUpdateAvailable;
|
|
||||||
})
|
|
||||||
|
|
||||||
statusManager.addStatusObserver((string) => {
|
|
||||||
$timeout(() => {
|
|
||||||
this.arbitraryStatusMessage = string;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
$rootScope.$on("did-begin-local-backup", () => {
|
|
||||||
$timeout(() => {
|
|
||||||
this.backupStatus = statusManager.addStatusFromString("Saving local backup...");
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
$rootScope.$on("did-finish-local-backup", (event, data) => {
|
|
||||||
$timeout(() => {
|
|
||||||
if(data.success) {
|
|
||||||
this.backupStatus = statusManager.replaceStatusWithString(this.backupStatus, "Successfully saved backup.");
|
|
||||||
} else {
|
|
||||||
this.backupStatus = statusManager.replaceStatusWithString(this.backupStatus, "Unable to save local backup.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$timeout(() => {
|
|
||||||
this.backupStatus = statusManager.removeStatus(this.backupStatus);
|
|
||||||
}, 2000)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
this.openSecurityUpdate = function() {
|
|
||||||
authManager.presentPasswordWizard("upgrade-security");
|
|
||||||
}
|
|
||||||
|
|
||||||
$rootScope.$on("reload-ext-data", () => {
|
|
||||||
this.reloadExtendedData();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.reloadExtendedData = () => {
|
|
||||||
if(this.reloadInProgress) { return; }
|
|
||||||
this.reloadInProgress = true;
|
|
||||||
|
|
||||||
// A reload occurs when the extensions manager window is opened. We can close it after a delay
|
|
||||||
let extWindow = this.rooms.find((room) => {return room.package_info.identifier == nativeExtManager.extensionsManagerIdentifier});
|
|
||||||
if(!extWindow) {
|
|
||||||
this.queueExtReload = true; // try again when the ext is available
|
|
||||||
this.reloadInProgress = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectRoom(extWindow);
|
|
||||||
|
|
||||||
$timeout(() => {
|
|
||||||
this.selectRoom(extWindow);
|
|
||||||
this.reloadInProgress = false;
|
|
||||||
$rootScope.$broadcast("ext-reload-complete");
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getUser = function() {
|
|
||||||
return authManager.user;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateOfflineStatus = function() {
|
|
||||||
this.offline = authManager.offline();
|
|
||||||
}
|
|
||||||
this.updateOfflineStatus();
|
|
||||||
|
|
||||||
|
|
||||||
syncManager.addEventHandler((syncEvent, data) => {
|
|
||||||
$timeout(() => {
|
|
||||||
if(syncEvent == "local-data-loaded") {
|
|
||||||
// If the user has no notes and is offline, show Account menu
|
|
||||||
if(this.offline && modelManager.noteCount() == 0) {
|
|
||||||
this.showAccountMenu = true;
|
|
||||||
}
|
|
||||||
} else if(syncEvent == "enter-out-of-sync") {
|
|
||||||
this.outOfSync = true;
|
|
||||||
} else if(syncEvent == "exit-out-of-sync") {
|
|
||||||
this.outOfSync = false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
this.findErrors = function() {
|
|
||||||
this.error = syncManager.syncStatus.error;
|
|
||||||
}
|
|
||||||
this.findErrors();
|
|
||||||
|
|
||||||
this.onAuthSuccess = function() {
|
|
||||||
this.showAccountMenu = false;
|
|
||||||
}.bind(this)
|
|
||||||
|
|
||||||
this.accountMenuPressed = function() {
|
|
||||||
this.showAccountMenu = !this.showAccountMenu;
|
|
||||||
this.closeAllRooms();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.toggleSyncResolutionMenu = function() {
|
|
||||||
this.showSyncResolution = !this.showSyncResolution;
|
|
||||||
}.bind(this);
|
|
||||||
|
|
||||||
this.closeAccountMenu = () => {
|
|
||||||
this.showAccountMenu = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hasPasscode = function() {
|
|
||||||
return passcodeManager.hasPasscode();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lockApp = function() {
|
|
||||||
$rootScope.lockApplication();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refreshData = function() {
|
|
||||||
this.isRefreshing = true;
|
|
||||||
// Enable integrity checking for this force request
|
|
||||||
syncManager.sync({force: true, performIntegrityCheck: true}).then((response) => {
|
|
||||||
$timeout(function(){
|
|
||||||
this.isRefreshing = false;
|
|
||||||
}.bind(this), 200)
|
|
||||||
if(response && response.error) {
|
|
||||||
alertManager.alert({text: "There was an error syncing. Please try again. If all else fails, try signing out and signing back in."});
|
|
||||||
} else {
|
|
||||||
this.syncUpdated();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncUpdated = function() {
|
|
||||||
this.lastSyncDate = new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
$rootScope.$on("new-update-available", () => {
|
|
||||||
$timeout(() => {
|
|
||||||
this.onNewUpdateAvailable();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.onNewUpdateAvailable = function() {
|
|
||||||
this.newUpdateAvailable = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clickedNewUpdateAnnouncement = function() {
|
|
||||||
this.newUpdateAvailable = false;
|
|
||||||
alertManager.alert({text: "A new update is ready to install. Please use the top-level 'Updates' menu to manage installation."})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Rooms */
|
|
||||||
|
|
||||||
this.componentManager = componentManager;
|
|
||||||
this.rooms = [];
|
|
||||||
this.themesWithIcons = [];
|
|
||||||
|
|
||||||
modelManager.addItemSyncObserver("room-bar", "SN|Component", (allItems, validItems, deletedItems, source) => {
|
|
||||||
this.rooms = modelManager.components.filter((candidate) => {return candidate.area == "rooms" && !candidate.deleted});
|
|
||||||
if(this.queueExtReload) {
|
|
||||||
this.queueExtReload = false;
|
|
||||||
this.reloadExtendedData();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
modelManager.addItemSyncObserver("footer-bar-themes", "SN|Theme", (allItems, validItems, deletedItems, source) => {
|
|
||||||
let themes = modelManager.validItemsForContentType("SN|Theme").filter((candidate) => {
|
|
||||||
return !candidate.deleted && candidate.content.package_info && candidate.content.package_info.dock_icon;
|
|
||||||
}).sort((a, b) => {
|
|
||||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
let differ = themes.length != this.themesWithIcons.length;
|
|
||||||
|
|
||||||
this.themesWithIcons = themes;
|
|
||||||
|
|
||||||
if(differ) {
|
|
||||||
this.reloadDockShortcuts();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.reloadDockShortcuts = function() {
|
|
||||||
let shortcuts = [];
|
|
||||||
for(var theme of this.themesWithIcons) {
|
|
||||||
var name = theme.content.package_info.name;
|
|
||||||
var icon = theme.content.package_info.dock_icon;
|
|
||||||
if(!icon) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
shortcuts.push({
|
|
||||||
name: name,
|
|
||||||
component: theme,
|
|
||||||
icon: icon
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dockShortcuts = shortcuts.sort((a, b) => {
|
|
||||||
// circles first, then images
|
|
||||||
|
|
||||||
var aType = a.icon.type;
|
|
||||||
var bType = b.icon.type;
|
|
||||||
|
|
||||||
if(aType == bType) {
|
|
||||||
return 0;
|
|
||||||
} else if(aType == "circle" && bType == "svg") {
|
|
||||||
return -1;
|
|
||||||
} else if(bType == "circle" && aType == "svg") {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.initSvgForShortcut = function(shortcut) {
|
|
||||||
var id = "dock-svg-" + shortcut.component.uuid;
|
|
||||||
var element = document.getElementById(id);
|
|
||||||
var parser = new DOMParser();
|
|
||||||
var svg = shortcut.component.content.package_info.dock_icon.source;
|
|
||||||
var doc = parser.parseFromString(svg, "image/svg+xml");
|
|
||||||
element.appendChild(doc.documentElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectShortcut = function(shortcut) {
|
|
||||||
componentManager.toggleComponent(shortcut.component);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentManager.registerHandler({identifier: "roomBar", areas: ["rooms", "modal"], activationHandler: (component) => {
|
|
||||||
// RIP: There used to be code here that checked if component.active was true, and if so, displayed the component.
|
|
||||||
// However, we no longer want to persist active state for footer extensions. If you open Extensions on one computer,
|
|
||||||
// it shouldn't open on another computer. Active state should only be persisted for persistent extensions, like Folders.
|
|
||||||
}, 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 = async function(room) {
|
|
||||||
let run = () => {
|
|
||||||
$timeout(() => {
|
|
||||||
room.showRoom = !room.showRoom;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!room.showRoom) {
|
|
||||||
// About to show, check if has privileges
|
|
||||||
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageExtensions)) {
|
|
||||||
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageExtensions, () => {
|
|
||||||
run();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
run();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clickOutsideAccountMenu = function() {
|
|
||||||
if(privilegesManager.authenticationInProgress()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.showAccountMenu = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,370 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { SFAuthManager } from 'snjs';
|
|
||||||
import { getPlatformString } from '@/utils';
|
|
||||||
import template from '%/home.pug';
|
|
||||||
|
|
||||||
export class Home {
|
|
||||||
constructor() {
|
|
||||||
this.template = template;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller(
|
|
||||||
$scope,
|
|
||||||
$location,
|
|
||||||
$rootScope,
|
|
||||||
$timeout,
|
|
||||||
modelManager,
|
|
||||||
dbManager,
|
|
||||||
syncManager,
|
|
||||||
authManager,
|
|
||||||
themeManager,
|
|
||||||
passcodeManager,
|
|
||||||
storageManager,
|
|
||||||
migrationManager,
|
|
||||||
privilegesManager,
|
|
||||||
statusManager,
|
|
||||||
alertManager
|
|
||||||
) {
|
|
||||||
storageManager.initialize(passcodeManager.hasPasscode(), authManager.isEphemeralSession());
|
|
||||||
|
|
||||||
$scope.platform = getPlatformString();
|
|
||||||
|
|
||||||
$scope.onUpdateAvailable = function() {
|
|
||||||
$rootScope.$broadcast('new-update-available');
|
|
||||||
}
|
|
||||||
|
|
||||||
$rootScope.$on("panel-resized", (event, info) => {
|
|
||||||
if(info.panel == "notes") { this.notesCollapsed = info.collapsed; }
|
|
||||||
if(info.panel == "tags") { this.tagsCollapsed = info.collapsed; }
|
|
||||||
|
|
||||||
let appClass = "";
|
|
||||||
if(this.notesCollapsed) { appClass += "collapsed-notes"; }
|
|
||||||
if(this.tagsCollapsed) { appClass += " collapsed-tags"; }
|
|
||||||
|
|
||||||
$scope.appClass = appClass;
|
|
||||||
})
|
|
||||||
|
|
||||||
/* Used to avoid circular dependencies where syncManager cannot be imported but rootScope can */
|
|
||||||
$rootScope.sync = function(source) {
|
|
||||||
syncManager.sync();
|
|
||||||
}
|
|
||||||
|
|
||||||
$rootScope.lockApplication = function() {
|
|
||||||
// Reloading wipes current objects from memory
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
const initiateSync = () => {
|
|
||||||
authManager.loadInitialData();
|
|
||||||
|
|
||||||
this.syncStatusObserver = syncManager.registerSyncStatusObserver((status) => {
|
|
||||||
if(status.retrievedCount > 20) {
|
|
||||||
var text = `Downloading ${status.retrievedCount} items. Keep app open.`
|
|
||||||
this.syncStatus = statusManager.replaceStatusWithString(this.syncStatus, text);
|
|
||||||
this.showingDownloadStatus = true;
|
|
||||||
} else if(this.showingDownloadStatus) {
|
|
||||||
this.showingDownloadStatus = false;
|
|
||||||
var text = "Download Complete.";
|
|
||||||
this.syncStatus = statusManager.replaceStatusWithString(this.syncStatus, text);
|
|
||||||
setTimeout(() => {
|
|
||||||
this.syncStatus = statusManager.removeStatus(this.syncStatus);
|
|
||||||
}, 2000);
|
|
||||||
} else if(status.total > 20) {
|
|
||||||
this.uploadSyncStatus = statusManager.replaceStatusWithString(this.uploadSyncStatus, `Syncing ${status.current}/${status.total} items...`)
|
|
||||||
} else if(this.uploadSyncStatus) {
|
|
||||||
this.uploadSyncStatus = statusManager.removeStatus(this.uploadSyncStatus);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
syncManager.setKeyRequestHandler(async () => {
|
|
||||||
let offline = authManager.offline();
|
|
||||||
let auth_params = offline ? passcodeManager.passcodeAuthParams() : await authManager.getAuthParams();
|
|
||||||
let keys = offline ? passcodeManager.keys() : await authManager.keys();
|
|
||||||
return {
|
|
||||||
keys: keys,
|
|
||||||
offline: offline,
|
|
||||||
auth_params: auth_params
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let lastSessionInvalidAlert;
|
|
||||||
|
|
||||||
syncManager.addEventHandler((syncEvent, data) => {
|
|
||||||
$rootScope.$broadcast(syncEvent, data || {});
|
|
||||||
if(syncEvent == "sync-session-invalid") {
|
|
||||||
// On Windows, some users experience issues where this message keeps appearing. It might be that on focus, the app syncs, and this message triggers again.
|
|
||||||
// We'll only show it once every X seconds
|
|
||||||
let showInterval = 30; // At most 30 seconds in between
|
|
||||||
if(!lastSessionInvalidAlert || (new Date() - lastSessionInvalidAlert)/1000 > showInterval) {
|
|
||||||
lastSessionInvalidAlert = new Date();
|
|
||||||
setTimeout(function () {
|
|
||||||
// If this alert is displayed on launch, it may sometimes dismiss automatically really quicky for some reason. So we wrap in timeout
|
|
||||||
alertManager.alert({text: "Your session has expired. New changes will not be pulled in. Please sign out and sign back in to refresh your session."});
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
} else if(syncEvent == "sync-exception") {
|
|
||||||
alertManager.alert({text: `There was an error while trying to save your items. Please contact support and share this message: ${data}`});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let encryptionEnabled = authManager.user || passcodeManager.hasPasscode();
|
|
||||||
this.syncStatus = statusManager.addStatusFromString(encryptionEnabled ? "Decrypting items..." : "Loading items...");
|
|
||||||
|
|
||||||
let incrementalCallback = (current, total) => {
|
|
||||||
let notesString = `${current}/${total} items...`
|
|
||||||
this.syncStatus = statusManager.replaceStatusWithString(this.syncStatus, encryptionEnabled ? `Decrypting ${notesString}` : `Loading ${notesString}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
syncManager.loadLocalItems({incrementalCallback}).then(() => {
|
|
||||||
$timeout(() => {
|
|
||||||
$rootScope.$broadcast("initial-data-loaded"); // This needs to be processed first before sync is called so that singletonManager observers function properly.
|
|
||||||
// Perform integrity check on first sync
|
|
||||||
this.syncStatus = statusManager.replaceStatusWithString(this.syncStatus, "Syncing...");
|
|
||||||
syncManager.sync({performIntegrityCheck: true}).then(() => {
|
|
||||||
this.syncStatus = statusManager.removeStatus(this.syncStatus);
|
|
||||||
})
|
|
||||||
// refresh every 30s
|
|
||||||
setInterval(function () {
|
|
||||||
syncManager.sync();
|
|
||||||
}, 30000);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
authManager.addEventHandler((event) => {
|
|
||||||
if(event == SFAuthManager.DidSignOutEvent) {
|
|
||||||
modelManager.handleSignout();
|
|
||||||
syncManager.handleSignout();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function load() {
|
|
||||||
// pass keys to storageManager to decrypt storage
|
|
||||||
// Update: Wait, why? passcodeManager already handles this.
|
|
||||||
// storageManager.setKeys(passcodeManager.keys());
|
|
||||||
|
|
||||||
openDatabase();
|
|
||||||
// Retrieve local data and begin sycing timer
|
|
||||||
initiateSync();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(passcodeManager.isLocked()) {
|
|
||||||
$scope.needsUnlock = true;
|
|
||||||
} else {
|
|
||||||
load();
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.onSuccessfulUnlock = function() {
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.needsUnlock = false;
|
|
||||||
load();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDatabase() {
|
|
||||||
dbManager.setLocked(false);
|
|
||||||
dbManager.openDatabase({
|
|
||||||
onUpgradeNeeded: () => {
|
|
||||||
// new database, delete syncToken so that items can be refetched entirely from server
|
|
||||||
syncManager.clearSyncToken();
|
|
||||||
syncManager.sync();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Editor Callbacks
|
|
||||||
*/
|
|
||||||
|
|
||||||
$scope.updateTagsForNote = function(note, stringTags) {
|
|
||||||
var toRemove = [];
|
|
||||||
for(var tag of note.tags) {
|
|
||||||
if(stringTags.indexOf(tag.title) === -1) {
|
|
||||||
// remove this tag
|
|
||||||
toRemove.push(tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for(var tagToRemove of toRemove) {
|
|
||||||
tagToRemove.removeItemAsRelationship(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
modelManager.setItemsDirty(toRemove, true);
|
|
||||||
|
|
||||||
var tags = [];
|
|
||||||
for(var tagString of stringTags) {
|
|
||||||
var existingRelationship = _.find(note.tags, {title: tagString});
|
|
||||||
if(!existingRelationship) {
|
|
||||||
tags.push(modelManager.findOrCreateTagByTitle(tagString));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for(var tag of tags) {
|
|
||||||
tag.addItemAsRelationship(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
modelManager.setItemsDirty(tags, true);
|
|
||||||
|
|
||||||
syncManager.sync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Tags Ctrl Callbacks
|
|
||||||
*/
|
|
||||||
|
|
||||||
$scope.tagsSelectionMade = function(tag) {
|
|
||||||
// If a tag is selected twice, then the needed dummy note is removed.
|
|
||||||
// So we perform this check.
|
|
||||||
if($scope.selectedTag && tag && $scope.selectedTag.uuid == tag.uuid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if($scope.selectedNote && $scope.selectedNote.dummy) {
|
|
||||||
modelManager.removeItemLocally($scope.selectedNote);
|
|
||||||
$scope.selectedNote = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.selectedTag = tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.tagsAddNew = function(tag) {
|
|
||||||
modelManager.addItem(tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.tagsSave = function(tag, callback) {
|
|
||||||
if(!tag.title || tag.title.length == 0) {
|
|
||||||
$scope.removeTag(tag);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
modelManager.setItemDirty(tag, true);
|
|
||||||
syncManager.sync().then(callback);
|
|
||||||
modelManager.resortTag(tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Notes Ctrl Callbacks
|
|
||||||
*/
|
|
||||||
|
|
||||||
$scope.removeTag = function(tag) {
|
|
||||||
alertManager.confirm({text: "Are you sure you want to delete this tag? Note: deleting a tag will not delete its notes.", destructive: true, onConfirm: () => {
|
|
||||||
modelManager.setItemToBeDeleted(tag);
|
|
||||||
syncManager.sync().then(() => {
|
|
||||||
// force scope tags to update on sub directives
|
|
||||||
$rootScope.safeApply();
|
|
||||||
});
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.notesSelectionMade = function(note) {
|
|
||||||
$scope.selectedNote = note;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.notesAddNew = function(note) {
|
|
||||||
modelManager.addItem(note);
|
|
||||||
modelManager.setItemDirty(note);
|
|
||||||
|
|
||||||
if(!$scope.selectedTag.isSmartTag()) {
|
|
||||||
$scope.selectedTag.addItemAsRelationship(note);
|
|
||||||
modelManager.setItemDirty($scope.selectedTag, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Shared Callbacks
|
|
||||||
*/
|
|
||||||
|
|
||||||
$rootScope.safeApply = function(fn) {
|
|
||||||
var phase = this.$root.$$phase;
|
|
||||||
if(phase == '$apply' || phase == '$digest')
|
|
||||||
this.$eval(fn);
|
|
||||||
else
|
|
||||||
this.$apply(fn);
|
|
||||||
};
|
|
||||||
|
|
||||||
$rootScope.notifyDelete = function() {
|
|
||||||
$timeout(function() {
|
|
||||||
$rootScope.$broadcast("noteDeleted");
|
|
||||||
}.bind(this), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.deleteNote = function(note) {
|
|
||||||
modelManager.setItemToBeDeleted(note);
|
|
||||||
|
|
||||||
if(note == $scope.selectedNote) {
|
|
||||||
$scope.selectedNote = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(note.dummy) {
|
|
||||||
modelManager.removeItemLocally(note);
|
|
||||||
$rootScope.notifyDelete();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
syncManager.sync().then(() => {
|
|
||||||
if(authManager.offline()) {
|
|
||||||
// when deleting items while ofline, we need to explictly tell angular to refresh UI
|
|
||||||
setTimeout(function () {
|
|
||||||
$rootScope.notifyDelete();
|
|
||||||
$rootScope.safeApply();
|
|
||||||
}, 50);
|
|
||||||
} else {
|
|
||||||
$timeout(() => {
|
|
||||||
$rootScope.notifyDelete();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Disable dragging and dropping of files into main SN interface.
|
|
||||||
both 'dragover' and 'drop' are required to prevent dropping of files.
|
|
||||||
This will not prevent extensions from receiving drop events.
|
|
||||||
*/
|
|
||||||
window.addEventListener('dragover', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
}, false)
|
|
||||||
|
|
||||||
window.addEventListener('drop', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
alertManager.alert({text: "Please use FileSafe or the Bold Editor to attach images and files. Learn more at standardnotes.org/filesafe."})
|
|
||||||
}, false)
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
Handle Auto Sign In From URL
|
|
||||||
*/
|
|
||||||
|
|
||||||
function urlParam(key) {
|
|
||||||
return $location.search()[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function autoSignInFromParams() {
|
|
||||||
var server = urlParam("server");
|
|
||||||
var email = urlParam("email");
|
|
||||||
var pw = urlParam("pw");
|
|
||||||
|
|
||||||
if(!authManager.offline()) {
|
|
||||||
// check if current account
|
|
||||||
if(await syncManager.getServerURL() === server && authManager.user.email === email) {
|
|
||||||
// already signed in, return
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// sign out
|
|
||||||
authManager.signout(true).then(() => {
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
authManager.login(server, email, pw, false, false, {}).then((response) => {
|
|
||||||
window.location.reload();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(urlParam("server")) {
|
|
||||||
autoSignInFromParams();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export { EditorPanel } from './editor';
|
|
||||||
export { Footer } from './footer';
|
|
||||||
export { NotesPanel } from './notes';
|
|
||||||
export { TagsPanel } from './tags';
|
|
||||||
export { Home } from './home';
|
|
||||||
export { LockScreen } from './lockScreen';
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import template from '%/lock-screen.pug';
|
|
||||||
|
|
||||||
export class LockScreen {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = "E";
|
|
||||||
this.template = template;
|
|
||||||
this.scope = {
|
|
||||||
onSuccess: "&",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller($scope, passcodeManager, authManager, syncManager, storageManager, alertManager) {
|
|
||||||
$scope.formData = {};
|
|
||||||
|
|
||||||
this.visibilityObserver = passcodeManager.addVisibilityObserver((visible) => {
|
|
||||||
if(visible) {
|
|
||||||
let input = document.getElementById("passcode-input");
|
|
||||||
if(input) {
|
|
||||||
input.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
$scope.$on("$destroy", () => {
|
|
||||||
passcodeManager.removeVisibilityObserver(this.visibilityObserver);
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.submitPasscodeForm = function() {
|
|
||||||
if(!$scope.formData.passcode || $scope.formData.passcode.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
passcodeManager.unlock($scope.formData.passcode, (success) => {
|
|
||||||
if(!success) {
|
|
||||||
alertManager.alert({text: "Invalid passcode. Please try again."});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.onSuccess()();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.forgotPasscode = function() {
|
|
||||||
$scope.formData.showRecovery = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.beginDeleteData = function() {
|
|
||||||
alertManager.confirm({text: "Are you sure you want to clear all local data?", destructive: true, onConfirm: () => {
|
|
||||||
authManager.signout(true).then(() => {
|
|
||||||
window.location.reload();
|
|
||||||
})
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,692 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import angular from 'angular';
|
|
||||||
import { SFAuthManager } from 'snjs';
|
|
||||||
import { PrivilegesManager } from '@/services/privilegesManager';
|
|
||||||
import { KeyboardManager } from '@/services/keyboardManager';
|
|
||||||
import template from '%/notes.pug';
|
|
||||||
|
|
||||||
export class NotesPanel {
|
|
||||||
constructor() {
|
|
||||||
this.scope = {
|
|
||||||
addNew: '&',
|
|
||||||
selectionMade: '&',
|
|
||||||
tag: '='
|
|
||||||
};
|
|
||||||
|
|
||||||
this.template = template;
|
|
||||||
this.replace = true;
|
|
||||||
|
|
||||||
this.controllerAs = 'ctrl';
|
|
||||||
this.bindToController = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
link(scope, elem, attrs, ctrl) {
|
|
||||||
scope.$watch('ctrl.tag', (tag, oldTag) => {
|
|
||||||
if (tag) {
|
|
||||||
ctrl.tagDidChange(tag, oldTag);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller(
|
|
||||||
authManager,
|
|
||||||
$timeout,
|
|
||||||
$rootScope,
|
|
||||||
modelManager,
|
|
||||||
syncManager,
|
|
||||||
storageManager,
|
|
||||||
desktopManager,
|
|
||||||
privilegesManager,
|
|
||||||
keyboardManager
|
|
||||||
) {
|
|
||||||
this.panelController = {};
|
|
||||||
this.searchSubmitted = false;
|
|
||||||
|
|
||||||
$rootScope.$on("user-preferences-changed", () => {
|
|
||||||
this.loadPreferences();
|
|
||||||
this.reloadNotes();
|
|
||||||
});
|
|
||||||
|
|
||||||
authManager.addEventHandler((event) => {
|
|
||||||
if(event == SFAuthManager.DidSignInEvent) {
|
|
||||||
// Delete dummy note if applicable
|
|
||||||
if(this.selectedNote && this.selectedNote.dummy) {
|
|
||||||
modelManager.removeItemLocally(this.selectedNote);
|
|
||||||
_.pull(this.notes, this.selectedNote);
|
|
||||||
this.selectedNote = null;
|
|
||||||
this.selectNote(null);
|
|
||||||
|
|
||||||
// We now want to see if the user will download any items from the server.
|
|
||||||
// If the next sync completes and our notes are still 0, we need to create a dummy.
|
|
||||||
this.createDummyOnSynCompletionIfNoNotes = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
syncManager.addEventHandler((syncEvent, data) => {
|
|
||||||
if(syncEvent == "local-data-loaded") {
|
|
||||||
if(this.notes.length == 0) {
|
|
||||||
this.createNewNote();
|
|
||||||
}
|
|
||||||
} else if(syncEvent == "sync:completed") {
|
|
||||||
// Pad with a timeout just to be extra patient
|
|
||||||
$timeout(() => {
|
|
||||||
if(this.createDummyOnSynCompletionIfNoNotes && this.notes.length == 0) {
|
|
||||||
this.createDummyOnSynCompletionIfNoNotes = false;
|
|
||||||
this.createNewNote();
|
|
||||||
}
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
modelManager.addItemSyncObserver("note-list", "*", (allItems, validItems, deletedItems, source, sourceKey) => {
|
|
||||||
// reload our notes
|
|
||||||
this.reloadNotes();
|
|
||||||
|
|
||||||
// Note has changed values, reset its flags
|
|
||||||
let notes = allItems.filter((item) => item.content_type == "Note");
|
|
||||||
for(let note of notes) {
|
|
||||||
this.loadFlagsForNote(note);
|
|
||||||
note.cachedCreatedAtString = note.createdAtString();
|
|
||||||
note.cachedUpdatedAtString = note.updatedAtString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// select first note if none is selected
|
|
||||||
if(!this.selectedNote) {
|
|
||||||
$timeout(() => {
|
|
||||||
// required to be in timeout since selecting notes depends on rendered notes
|
|
||||||
this.selectFirstNote();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setNotes = function(notes) {
|
|
||||||
notes = this.filterNotes(notes);
|
|
||||||
notes = this.sortNotes(notes, this.sortBy, this.sortReverse);
|
|
||||||
for(let note of notes) {
|
|
||||||
note.shouldShowTags = this.shouldShowTagsForNote(note);
|
|
||||||
}
|
|
||||||
this.notes = notes;
|
|
||||||
|
|
||||||
this.reloadPanelTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reloadNotes = function() {
|
|
||||||
let notes = this.tag.notes;
|
|
||||||
|
|
||||||
// Typically we reload flags via modelManager.addItemSyncObserver,
|
|
||||||
// but sync observers are not notified of errored items, so we'll do it here instead
|
|
||||||
for(let note of notes) {
|
|
||||||
if(note.errorDecrypting) {
|
|
||||||
this.loadFlagsForNote(note);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setNotes(notes);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reorderNotes = function() {
|
|
||||||
this.setNotes(this.notes);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadPreferences = function() {
|
|
||||||
let prevSortValue = this.sortBy;
|
|
||||||
|
|
||||||
this.sortBy = authManager.getUserPrefValue("sortBy", "created_at");
|
|
||||||
this.sortReverse = authManager.getUserPrefValue("sortReverse", false);
|
|
||||||
|
|
||||||
if(this.sortBy == "updated_at") {
|
|
||||||
// use client_updated_at instead
|
|
||||||
this.sortBy = "client_updated_at";
|
|
||||||
}
|
|
||||||
|
|
||||||
if(prevSortValue && prevSortValue != this.sortBy) {
|
|
||||||
$timeout(() => {
|
|
||||||
this.selectFirstNote();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
if(this.panelController.isCollapsed()) {
|
|
||||||
$rootScope.$broadcast("panel-resized", {panel: "notes", collapsed: this.panelController.isCollapsed()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadPreferences();
|
|
||||||
|
|
||||||
this.onPanelResize = function(newWidth, lastLeft, isAtMaxWidth, isCollapsed) {
|
|
||||||
authManager.setUserPrefValue("notesPanelWidth", newWidth);
|
|
||||||
authManager.syncUserPreferences();
|
|
||||||
$rootScope.$broadcast("panel-resized", {panel: "notes", collapsed: isCollapsed})
|
|
||||||
}
|
|
||||||
|
|
||||||
angular.element(document).ready(() => {
|
|
||||||
this.loadPreferences();
|
|
||||||
});
|
|
||||||
|
|
||||||
$rootScope.$on("editorFocused", function(){
|
|
||||||
this.showMenu = false;
|
|
||||||
}.bind(this))
|
|
||||||
|
|
||||||
$rootScope.$on("noteDeleted", function() {
|
|
||||||
$timeout(this.onNoteRemoval.bind(this));
|
|
||||||
}.bind(this))
|
|
||||||
|
|
||||||
$rootScope.$on("noteArchived", function() {
|
|
||||||
$timeout(this.onNoteRemoval.bind(this));
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
|
|
||||||
// When a note is removed from the list
|
|
||||||
this.onNoteRemoval = function() {
|
|
||||||
let visibleNotes = this.visibleNotes();
|
|
||||||
let index;
|
|
||||||
if(this.selectedIndex < visibleNotes.length) {
|
|
||||||
index = Math.max(this.selectedIndex, 0);
|
|
||||||
} else {
|
|
||||||
index = visibleNotes.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let note = visibleNotes[index];
|
|
||||||
if(note) {
|
|
||||||
this.selectNote(note);
|
|
||||||
} else {
|
|
||||||
this.createNewNote();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onresize = (event) => {
|
|
||||||
this.resetPagination({keepCurrentIfLarger: true});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.paginate = function() {
|
|
||||||
this.notesToDisplay += this.DefaultNotesToDisplayValue
|
|
||||||
|
|
||||||
if (this.searchSubmitted) {
|
|
||||||
desktopManager.searchText(this.noteFilter.text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.resetPagination = function({keepCurrentIfLarger} = {}) {
|
|
||||||
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;
|
|
||||||
if(keepCurrentIfLarger && this.notesToDisplay > this.DefaultNotesToDisplayValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.notesToDisplay = this.DefaultNotesToDisplayValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.resetPagination();
|
|
||||||
|
|
||||||
this.reloadPanelTitle = function() {
|
|
||||||
if(this.isFiltering()) {
|
|
||||||
this.panelTitle = `${this.notes.filter((i) => {return i.visible;}).length} search results`;
|
|
||||||
} else if(this.tag) {
|
|
||||||
this.panelTitle = `${this.tag.title}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.optionsSubtitle = function() {
|
|
||||||
var base = "";
|
|
||||||
if(this.sortBy == "created_at") {
|
|
||||||
base += " Date Added";
|
|
||||||
} else if(this.sortBy == "client_updated_at") {
|
|
||||||
base += " Date Modified";
|
|
||||||
} else if(this.sortBy == "title") {
|
|
||||||
base += " Title";
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.showArchived) {
|
|
||||||
base += " | + Archived"
|
|
||||||
}
|
|
||||||
if(this.hidePinned) {
|
|
||||||
base += " | – Pinned"
|
|
||||||
}
|
|
||||||
if(this.sortReverse) {
|
|
||||||
base += " | Reversed"
|
|
||||||
}
|
|
||||||
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadFlagsForNote = (note) => {
|
|
||||||
let flags = [];
|
|
||||||
|
|
||||||
if(note.pinned) {
|
|
||||||
flags.push({
|
|
||||||
text: "Pinned",
|
|
||||||
class: "info"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(note.archived) {
|
|
||||||
flags.push({
|
|
||||||
text: "Archived",
|
|
||||||
class: "warning"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(note.content.protected) {
|
|
||||||
flags.push({
|
|
||||||
text: "Protected",
|
|
||||||
class: "success"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(note.locked) {
|
|
||||||
flags.push({
|
|
||||||
text: "Locked",
|
|
||||||
class: "neutral"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(note.content.trashed) {
|
|
||||||
flags.push({
|
|
||||||
text: "Deleted",
|
|
||||||
class: "danger"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(note.content.conflict_of) {
|
|
||||||
flags.push({
|
|
||||||
text: "Conflicted Copy",
|
|
||||||
class: "danger"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(note.errorDecrypting) {
|
|
||||||
flags.push({
|
|
||||||
text: "Missing Keys",
|
|
||||||
class: "danger"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(note.deleted) {
|
|
||||||
flags.push({
|
|
||||||
text: "Deletion Pending Sync",
|
|
||||||
class: "danger"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
note.flags = flags;
|
|
||||||
|
|
||||||
return flags;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tagDidChange = function(tag, oldTag) {
|
|
||||||
var scrollable = document.getElementById("notes-scrollable");
|
|
||||||
if(scrollable) {
|
|
||||||
scrollable.scrollTop = 0;
|
|
||||||
scrollable.scrollLeft = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.resetPagination();
|
|
||||||
|
|
||||||
this.showMenu = false;
|
|
||||||
|
|
||||||
if(this.selectedNote) {
|
|
||||||
if(this.selectedNote.dummy && oldTag) {
|
|
||||||
_.remove(oldTag.notes, this.selectedNote);
|
|
||||||
}
|
|
||||||
this.selectNote(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.noteFilter.text = "";
|
|
||||||
desktopManager.searchText();
|
|
||||||
|
|
||||||
this.setNotes(tag.notes);
|
|
||||||
|
|
||||||
// perform in timeout since visibleNotes relies on renderedNotes which relies on render to complete
|
|
||||||
$timeout(() => {
|
|
||||||
if(this.notes.length > 0) {
|
|
||||||
this.notes.forEach((note) => { note.visible = true; })
|
|
||||||
this.selectFirstNote();
|
|
||||||
} else if(syncManager.initialDataLoaded()) {
|
|
||||||
if(!tag.isSmartTag() || tag.content.isAllTag) {
|
|
||||||
this.createNewNote();
|
|
||||||
} else {
|
|
||||||
if(this.selectedNote && !this.notes.includes(this.selectedNote)) {
|
|
||||||
this.selectNote(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.visibleNotes = function() {
|
|
||||||
return this.renderedNotes.filter(function(note){
|
|
||||||
return note.visible;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectFirstNote = function() {
|
|
||||||
var visibleNotes = this.visibleNotes();
|
|
||||||
if(visibleNotes.length > 0) {
|
|
||||||
this.selectNote(visibleNotes[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectNextNote = function() {
|
|
||||||
var visibleNotes = this.visibleNotes();
|
|
||||||
let currentIndex = visibleNotes.indexOf(this.selectedNote);
|
|
||||||
if(currentIndex + 1 < visibleNotes.length) {
|
|
||||||
this.selectNote(visibleNotes[currentIndex + 1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectPreviousNote = function() {
|
|
||||||
var visibleNotes = this.visibleNotes();
|
|
||||||
let currentIndex = visibleNotes.indexOf(this.selectedNote);
|
|
||||||
if(currentIndex - 1 >= 0) {
|
|
||||||
this.selectNote(visibleNotes[currentIndex - 1]);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectNote = async function(note, viaClick = false) {
|
|
||||||
if(this.selectedNote === note) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!note) {
|
|
||||||
this.selectedNote = null;
|
|
||||||
this.selectionMade()(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let run = () => {
|
|
||||||
$timeout(() => {
|
|
||||||
let dummyNote;
|
|
||||||
if(this.selectedNote && this.selectedNote != note && this.selectedNote.dummy == true) {
|
|
||||||
// remove dummy
|
|
||||||
dummyNote = this.selectedNote;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedNote = note;
|
|
||||||
if(note.content.conflict_of) {
|
|
||||||
note.content.conflict_of = null; // clear conflict
|
|
||||||
modelManager.setItemDirty(note, true);
|
|
||||||
syncManager.sync();
|
|
||||||
}
|
|
||||||
this.selectionMade()(note);
|
|
||||||
this.selectedIndex = Math.max(this.visibleNotes().indexOf(note), 0);
|
|
||||||
|
|
||||||
// There needs to be a long timeout after setting selection before removing the dummy
|
|
||||||
// Otherwise, you'll click a note, remove this one, and strangely, the click event registers for a lower cell
|
|
||||||
if(dummyNote && dummyNote.dummy == true) {
|
|
||||||
$timeout(() => {
|
|
||||||
modelManager.removeItemLocally(dummyNote);
|
|
||||||
_.pull(this.notes, dummyNote);
|
|
||||||
}, 250)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(viaClick && this.isFiltering()) {
|
|
||||||
desktopManager.searchText(this.noteFilter.text);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(note.content.protected && await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionViewProtectedNotes)) {
|
|
||||||
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionViewProtectedNotes, () => {
|
|
||||||
run();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isFiltering = function() {
|
|
||||||
return this.noteFilter.text && this.noteFilter.text.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.createNewNote = function() {
|
|
||||||
if(this.selectedNote && this.selectedNote.dummy) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// The "Note X" counter is based off this.notes.length, but sometimes, what you see in the list is only a subset.
|
|
||||||
// We can use this.visibleNotes().length, but that only accounts for non-paginated results, so first 15 or so.
|
|
||||||
let title = "Note" + (this.notes ? (" " + (this.notes.length + 1)) : "");
|
|
||||||
let newNote = modelManager.createItem({content_type: "Note", content: {text: "", title: title}});
|
|
||||||
newNote.client_updated_at = new Date();
|
|
||||||
newNote.dummy = true;
|
|
||||||
this.selectNote(newNote);
|
|
||||||
this.addNew()(newNote);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.noteFilter = {text : ''};
|
|
||||||
|
|
||||||
this.onFilterEnter = function() {
|
|
||||||
// For Desktop, performing a search right away causes input to lose focus.
|
|
||||||
// We wait until user explicity hits enter before highlighting desktop search results.
|
|
||||||
this.searchSubmitted = true;
|
|
||||||
desktopManager.searchText(this.noteFilter.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clearFilterText = function() {
|
|
||||||
this.noteFilter.text = '';
|
|
||||||
this.onFilterEnter();
|
|
||||||
this.filterTextChanged();
|
|
||||||
|
|
||||||
// Reset loaded notes
|
|
||||||
this.resetPagination();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.filterTextChanged = function() {
|
|
||||||
if(this.searchSubmitted) {
|
|
||||||
this.searchSubmitted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reloadNotes();
|
|
||||||
|
|
||||||
$timeout(() => {
|
|
||||||
if(!this.selectedNote.visible) {
|
|
||||||
this.selectFirstNote();
|
|
||||||
}
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedMenuItem = function() {
|
|
||||||
this.showMenu = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.togglePrefKey = function(key) {
|
|
||||||
this[key] = !this[key];
|
|
||||||
authManager.setUserPrefValue(key, this[key]);
|
|
||||||
authManager.syncUserPreferences();
|
|
||||||
this.reloadNotes();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedSortByCreated = function() {
|
|
||||||
this.setSortBy("created_at");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedSortByUpdated = function() {
|
|
||||||
this.setSortBy("client_updated_at");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedSortByTitle = function() {
|
|
||||||
this.setSortBy("title");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.toggleReverseSort = function() {
|
|
||||||
this.selectedMenuItem();
|
|
||||||
this.sortReverse = !this.sortReverse;
|
|
||||||
this.reorderNotes();
|
|
||||||
authManager.setUserPrefValue("sortReverse", this.sortReverse);
|
|
||||||
authManager.syncUserPreferences();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setSortBy = function(type) {
|
|
||||||
this.sortBy = type;
|
|
||||||
this.reorderNotes();
|
|
||||||
authManager.setUserPrefValue("sortBy", this.sortBy);
|
|
||||||
authManager.syncUserPreferences();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.shouldShowTagsForNote = function(note) {
|
|
||||||
if(this.hideTags || note.content.protected) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.tag.content.isAllTag) {
|
|
||||||
return note.tags && note.tags.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.tag.isSmartTag()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inside a tag, only show tags string if note contains tags other than this.tag
|
|
||||||
return note.tags && note.tags.length > 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.filterNotes = function(notes) {
|
|
||||||
return notes.filter((note) => {
|
|
||||||
let canShowArchived = this.showArchived, canShowPinned = !this.hidePinned;
|
|
||||||
let isTrash = this.tag.content.isTrashTag;
|
|
||||||
|
|
||||||
if(!isTrash && note.content.trashed) {
|
|
||||||
note.visible = false;
|
|
||||||
return note.visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
var isSmartTag = this.tag.isSmartTag();
|
|
||||||
if(isSmartTag) {
|
|
||||||
canShowArchived = canShowArchived || this.tag.content.isArchiveTag || isTrash;
|
|
||||||
}
|
|
||||||
|
|
||||||
if((note.archived && !canShowArchived) || (note.pinned && !canShowPinned)) {
|
|
||||||
note.visible = false;
|
|
||||||
return note.visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
var filterText = this.noteFilter.text.toLowerCase();
|
|
||||||
if(filterText.length == 0) {
|
|
||||||
note.visible = true;
|
|
||||||
} else {
|
|
||||||
var words = filterText.split(" ");
|
|
||||||
var matchesTitle = words.every(function(word) { return note.safeTitle().toLowerCase().indexOf(word) >= 0; });
|
|
||||||
var matchesBody = words.every(function(word) { return note.safeText().toLowerCase().indexOf(word) >= 0; });
|
|
||||||
note.visible = matchesTitle || matchesBody;
|
|
||||||
}
|
|
||||||
|
|
||||||
return note.visible;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sortNotes = function(items, sortBy, reverse) {
|
|
||||||
let sortValueFn = (a, b, pinCheck = false) => {
|
|
||||||
if(a.dummy) { return -1; }
|
|
||||||
if(b.dummy) { return 1; }
|
|
||||||
if(!pinCheck) {
|
|
||||||
if(a.pinned && b.pinned) {
|
|
||||||
return sortValueFn(a, b, true);
|
|
||||||
}
|
|
||||||
if(a.pinned) { return -1; }
|
|
||||||
if(b.pinned) { return 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
var aValue = a[sortBy] || "";
|
|
||||||
var bValue = b[sortBy] || "";
|
|
||||||
|
|
||||||
let vector = 1;
|
|
||||||
|
|
||||||
if(reverse) {
|
|
||||||
vector *= -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(sortBy == "title") {
|
|
||||||
aValue = aValue.toLowerCase();
|
|
||||||
bValue = bValue.toLowerCase();
|
|
||||||
|
|
||||||
if(aValue.length == 0 && bValue.length == 0) {
|
|
||||||
return 0;
|
|
||||||
} else if(aValue.length == 0 && bValue.length != 0) {
|
|
||||||
return 1 * vector;
|
|
||||||
} else if(aValue.length != 0 && bValue.length == 0) {
|
|
||||||
return -1 * vector;
|
|
||||||
} else {
|
|
||||||
vector *= -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(aValue > bValue) { return -1 * vector;}
|
|
||||||
else if(aValue < bValue) { return 1 * vector;}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
items = items || [];
|
|
||||||
var result = items.sort(function(a, b){
|
|
||||||
return sortValueFn(a, b);
|
|
||||||
})
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
Keyboard Shortcuts
|
|
||||||
*/
|
|
||||||
|
|
||||||
// In the browser we're not allowed to override cmd/ctrl + n, so we have to use Control modifier as well.
|
|
||||||
// These rules don't apply to desktop, but probably better to be consistent.
|
|
||||||
this.newNoteKeyObserver = keyboardManager.addKeyObserver({
|
|
||||||
key: "n",
|
|
||||||
modifiers: [KeyboardManager.KeyModifierMeta, KeyboardManager.KeyModifierCtrl],
|
|
||||||
onKeyDown: (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
$timeout(() => {
|
|
||||||
this.createNewNote();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.getSearchBar = function() {
|
|
||||||
return document.getElementById("search-bar");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.nextNoteKeyObserver = keyboardManager.addKeyObserver({
|
|
||||||
key: KeyboardManager.KeyDown,
|
|
||||||
elements: [document.body, this.getSearchBar()],
|
|
||||||
onKeyDown: (event) => {
|
|
||||||
let searchBar = this.getSearchBar();
|
|
||||||
if(searchBar == document.activeElement) {
|
|
||||||
searchBar.blur()
|
|
||||||
}
|
|
||||||
$timeout(() => {
|
|
||||||
this.selectNextNote();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.nextNoteKeyObserver = keyboardManager.addKeyObserver({
|
|
||||||
key: KeyboardManager.KeyUp,
|
|
||||||
element: document.body,
|
|
||||||
onKeyDown: (event) => {
|
|
||||||
$timeout(() => {
|
|
||||||
this.selectPreviousNote();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.searchKeyObserver = keyboardManager.addKeyObserver({
|
|
||||||
key: "f",
|
|
||||||
modifiers: [KeyboardManager.KeyModifierMeta, KeyboardManager.KeyModifierShift],
|
|
||||||
onKeyDown: (event) => {
|
|
||||||
let searchBar = this.getSearchBar();
|
|
||||||
if(searchBar) {searchBar.focus()};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
import { SNNote, SNSmartTag } from 'snjs';
|
|
||||||
import template from '%/tags.pug';
|
|
||||||
|
|
||||||
export class TagsPanel {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.scope = {
|
|
||||||
addNew: '&',
|
|
||||||
selectionMade: '&',
|
|
||||||
save: '&',
|
|
||||||
removeTag: '&'
|
|
||||||
};
|
|
||||||
this.template = template;
|
|
||||||
this.replace = true;
|
|
||||||
this.controllerAs = 'ctrl';
|
|
||||||
this.bindToController = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller(
|
|
||||||
$rootScope,
|
|
||||||
modelManager,
|
|
||||||
syncManager,
|
|
||||||
$timeout,
|
|
||||||
componentManager,
|
|
||||||
authManager
|
|
||||||
) {
|
|
||||||
// Wrap in timeout so that selectTag is defined
|
|
||||||
$timeout(() => {
|
|
||||||
this.smartTags = modelManager.getSmartTags();
|
|
||||||
this.selectTag(this.smartTags[0]);
|
|
||||||
})
|
|
||||||
|
|
||||||
syncManager.addEventHandler((syncEvent, data) => {
|
|
||||||
if(syncEvent == 'local-data-loaded'
|
|
||||||
|| syncEvent == 'sync:completed'
|
|
||||||
|| syncEvent == 'local-data-incremental-load') {
|
|
||||||
this.tags = modelManager.tags;
|
|
||||||
this.smartTags = modelManager.getSmartTags();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
modelManager.addItemSyncObserver(
|
|
||||||
'tags-list',
|
|
||||||
'*',
|
|
||||||
(allItems, validItems, deletedItems, source, sourceKey) => {
|
|
||||||
this.reloadNoteCounts();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
modelManager.addItemSyncObserver(
|
|
||||||
'tags-list-tags',
|
|
||||||
'Tag',
|
|
||||||
(allItems, validItems, deletedItems, source, sourceKey) => {
|
|
||||||
if(!this.selectedTag) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
/** If the selected tag has been deleted, revert to All view. */
|
|
||||||
const selectedTag = allItems.find((tag) => tag.uuid === this.selectedTag.uuid);
|
|
||||||
if(selectedTag && selectedTag.deleted) {
|
|
||||||
this.selectTag(this.smartTags[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.reloadNoteCounts = function() {
|
|
||||||
let allTags = [];
|
|
||||||
if(this.tags) { allTags = allTags.concat(this.tags);}
|
|
||||||
if(this.smartTags) { allTags = allTags.concat(this.smartTags);}
|
|
||||||
|
|
||||||
for(let tag of allTags) {
|
|
||||||
var validNotes = SNNote.filterDummyNotes(tag.notes).filter((note) => {
|
|
||||||
return !note.archived && !note.content.trashed;
|
|
||||||
});
|
|
||||||
|
|
||||||
tag.cachedNoteCount = validNotes.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.panelController = {};
|
|
||||||
|
|
||||||
$rootScope.$on("user-preferences-changed", () => {
|
|
||||||
this.loadPreferences();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loadPreferences = function() {
|
|
||||||
let width = authManager.getUserPrefValue("tagsPanelWidth");
|
|
||||||
if(width) {
|
|
||||||
this.panelController.setWidth(width);
|
|
||||||
if(this.panelController.isCollapsed()) {
|
|
||||||
$rootScope.$broadcast("panel-resized", {panel: "tags", collapsed: this.panelController.isCollapsed()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadPreferences();
|
|
||||||
|
|
||||||
this.onPanelResize = function(newWidth, lastLeft, isAtMaxWidth, isCollapsed) {
|
|
||||||
authManager.setUserPrefValue("tagsPanelWidth", newWidth, true);
|
|
||||||
$rootScope.$broadcast("panel-resized", {panel: "tags", collapsed: isCollapsed})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.componentManager = componentManager;
|
|
||||||
|
|
||||||
componentManager.registerHandler({identifier: "tags", areas: ["tags-list"], activationHandler: function(component){
|
|
||||||
this.component = component;
|
|
||||||
}.bind(this), contextRequestHandler: function(component){
|
|
||||||
return null;
|
|
||||||
}.bind(this), actionHandler: function(component, action, data){
|
|
||||||
if(action === "select-item") {
|
|
||||||
if(data.item.content_type == "Tag") {
|
|
||||||
let tag = modelManager.findItem(data.item.uuid);
|
|
||||||
if(tag) {
|
|
||||||
this.selectTag(tag);
|
|
||||||
}
|
|
||||||
} else if(data.item.content_type == "SN|SmartTag") {
|
|
||||||
let smartTag = new SNSmartTag(data.item);
|
|
||||||
this.selectTag(smartTag);
|
|
||||||
}
|
|
||||||
} else if(action === "clear-selection") {
|
|
||||||
this.selectTag(this.smartTags[0]);
|
|
||||||
}
|
|
||||||
}.bind(this)});
|
|
||||||
|
|
||||||
this.selectTag = function(tag) {
|
|
||||||
if(tag.isSmartTag()) {
|
|
||||||
Object.defineProperty(tag, "notes", {
|
|
||||||
get: () => {
|
|
||||||
return modelManager.notesMatchingSmartTag(tag);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.selectedTag = tag;
|
|
||||||
if(tag.content.conflict_of) {
|
|
||||||
tag.content.conflict_of = null;
|
|
||||||
modelManager.setItemDirty(tag, true);
|
|
||||||
syncManager.sync();
|
|
||||||
}
|
|
||||||
this.selectionMade()(tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clickedAddNewTag = function() {
|
|
||||||
if(this.editingTag) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.newTag = modelManager.createItem({content_type: "Tag"});
|
|
||||||
this.selectedTag = this.newTag;
|
|
||||||
this.editingTag = this.newTag;
|
|
||||||
this.addNew()(this.newTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tagTitleDidChange = function(tag) {
|
|
||||||
this.editingTag = tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveTag = function($event, tag) {
|
|
||||||
this.editingTag = null;
|
|
||||||
$event.target.blur();
|
|
||||||
|
|
||||||
if(!tag.title || tag.title.length == 0) {
|
|
||||||
if(originalTagName) {
|
|
||||||
tag.title = originalTagName;
|
|
||||||
originalTagName = null;
|
|
||||||
} else {
|
|
||||||
// newly created tag without content
|
|
||||||
modelManager.removeItemLocally(tag);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.save()(tag, (savedTag) => {
|
|
||||||
$timeout(() => {
|
|
||||||
this.selectTag(tag);
|
|
||||||
this.newTag = null;
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function inputElementForTag(tag) {
|
|
||||||
return document.getElementById("tag-" + tag.uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
var originalTagName = "";
|
|
||||||
this.selectedRenameTag = function($event, tag) {
|
|
||||||
originalTagName = tag.title;
|
|
||||||
this.editingTag = tag;
|
|
||||||
$timeout(function(){
|
|
||||||
inputElementForTag(tag).focus();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedDeleteTag = function(tag) {
|
|
||||||
this.removeTag()(tag);
|
|
||||||
this.selectTag(this.smartTags[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,528 +0,0 @@
|
|||||||
import { isDesktopApplication } from '@/utils';
|
|
||||||
import { PrivilegesManager } from '@/services/privilegesManager';
|
|
||||||
import template from '%/directives/account-menu.pug';
|
|
||||||
import { protocolManager } from 'snjs';
|
|
||||||
|
|
||||||
export class AccountMenu {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.template = template;
|
|
||||||
this.scope = {
|
|
||||||
onSuccessfulAuth: '&',
|
|
||||||
closeFunction: '&'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller(
|
|
||||||
$scope,
|
|
||||||
$rootScope,
|
|
||||||
authManager,
|
|
||||||
modelManager,
|
|
||||||
syncManager,
|
|
||||||
storageManager,
|
|
||||||
dbManager,
|
|
||||||
passcodeManager,
|
|
||||||
$timeout,
|
|
||||||
$compile,
|
|
||||||
archiveManager,
|
|
||||||
privilegesManager,
|
|
||||||
appVersion,
|
|
||||||
alertManager) {
|
|
||||||
'ngInject';
|
|
||||||
|
|
||||||
$scope.appVersion = "v" + (window.electronAppVersion || appVersion);
|
|
||||||
$scope.formData = {mergeLocal: true, ephemeral: false};
|
|
||||||
|
|
||||||
$scope.user = authManager.user;
|
|
||||||
|
|
||||||
syncManager.getServerURL().then((url) => {
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.server = url;
|
|
||||||
$scope.formData.url = url;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
authManager.checkForSecurityUpdate().then((available) => {
|
|
||||||
$scope.securityUpdateAvailable = available;
|
|
||||||
})
|
|
||||||
|
|
||||||
$scope.close = function() {
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.closeFunction()();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.encryptedBackupsAvailable = function() {
|
|
||||||
return authManager.user || passcodeManager.hasPasscode();
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.canAddPasscode = !authManager.isEphemeralSession();
|
|
||||||
$scope.syncStatus = syncManager.syncStatus;
|
|
||||||
|
|
||||||
$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 {
|
|
||||||
$scope.register();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.login = function(extraParams) {
|
|
||||||
// Prevent a timed sync from occuring while signing in. There may be a race condition where when
|
|
||||||
// calling `markAllItemsDirtyAndSaveOffline` during sign in, if an authenticated sync happens to occur
|
|
||||||
// right before that's called, items retreived from that sync will be marked as dirty, then resynced, causing mass duplication.
|
|
||||||
// Unlock sync after all sign in processes are complete.
|
|
||||||
syncManager.lockSyncing();
|
|
||||||
|
|
||||||
$scope.formData.status = "Generating Login Keys...";
|
|
||||||
$scope.formData.authenticating = true;
|
|
||||||
$timeout(function(){
|
|
||||||
authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password,
|
|
||||||
$scope.formData.ephemeral, $scope.formData.strictSignin, extraParams).then((response) => {
|
|
||||||
$timeout(() => {
|
|
||||||
if(!response || response.error) {
|
|
||||||
|
|
||||||
syncManager.unlockSyncing();
|
|
||||||
|
|
||||||
$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") {
|
|
||||||
$scope.formData.showLogin = false;
|
|
||||||
$scope.formData.mfa = error;
|
|
||||||
}
|
|
||||||
// General Error
|
|
||||||
else {
|
|
||||||
$scope.formData.showLogin = true;
|
|
||||||
$scope.formData.mfa = null;
|
|
||||||
if(error.message) {
|
|
||||||
alertManager.alert({text: error.message});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.formData.authenticating = false;
|
|
||||||
}
|
|
||||||
// Success
|
|
||||||
else {
|
|
||||||
$scope.onAuthSuccess(() => {
|
|
||||||
syncManager.unlockSyncing();
|
|
||||||
syncManager.sync({performIntegrityCheck: true});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.register = function() {
|
|
||||||
let confirmation = $scope.formData.password_conf;
|
|
||||||
if(confirmation !== $scope.formData.user_password) {
|
|
||||||
alertManager.alert({text: "The two passwords you entered do not match. Please try again."});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.formData.confirmPassword = false;
|
|
||||||
$scope.formData.status = "Generating Account Keys...";
|
|
||||||
$scope.formData.authenticating = true;
|
|
||||||
|
|
||||||
$timeout(function(){
|
|
||||||
authManager.register($scope.formData.url, $scope.formData.email, $scope.formData.user_password, $scope.formData.ephemeral).then((response) => {
|
|
||||||
$timeout(() => {
|
|
||||||
if(!response || response.error) {
|
|
||||||
$scope.formData.status = null;
|
|
||||||
var error = response ? response.error : {message: "An unknown error occured."}
|
|
||||||
$scope.formData.authenticating = false;
|
|
||||||
alertManager.alert({text: error.message});
|
|
||||||
} else {
|
|
||||||
$scope.onAuthSuccess(() => {
|
|
||||||
syncManager.sync();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.mergeLocalChanged = function() {
|
|
||||||
if(!$scope.formData.mergeLocal) {
|
|
||||||
alertManager.confirm({text: "Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?", destructive: true, onCancel: () => {
|
|
||||||
$scope.formData.mergeLocal = true;
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.onAuthSuccess = function(callback) {
|
|
||||||
var block = function() {
|
|
||||||
$timeout(function(){
|
|
||||||
$scope.formData.authenticating = false;
|
|
||||||
$scope.onSuccessfulAuth()();
|
|
||||||
syncManager.refreshErroredItems();
|
|
||||||
callback && callback();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if($scope.formData.mergeLocal) {
|
|
||||||
// Allows desktop to make backup file
|
|
||||||
$rootScope.$broadcast("major-data-change");
|
|
||||||
$scope.clearDatabaseAndRewriteAllItems(true, block);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
modelManager.removeAllItemsFromMemory();
|
|
||||||
storageManager.clearAllModels().then(() => {
|
|
||||||
block();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.openPasswordWizard = function(type) {
|
|
||||||
// Close the account menu
|
|
||||||
$scope.close();
|
|
||||||
authManager.presentPasswordWizard(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.openPrivilegesModal = async function() {
|
|
||||||
$scope.close();
|
|
||||||
|
|
||||||
let run = () => {
|
|
||||||
$timeout(() => {
|
|
||||||
privilegesManager.presentPrivilegesManagementModal();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePrivileges)) {
|
|
||||||
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePrivileges, () => {
|
|
||||||
run();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allows indexeddb unencrypted logs to be deleted
|
|
||||||
// clearAllModels will remove data from backing store, but not from working memory
|
|
||||||
// See: https://github.com/standardnotes/desktop/issues/131
|
|
||||||
$scope.clearDatabaseAndRewriteAllItems = function(alternateUuids, callback) {
|
|
||||||
storageManager.clearAllModels().then(() => {
|
|
||||||
syncManager.markAllItemsDirtyAndSaveOffline(alternateUuids).then(() => {
|
|
||||||
callback && callback();
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.destroyLocalData = function() {
|
|
||||||
alertManager.confirm({text: "Are you sure you want to end your session? This will delete all local items and extensions.", destructive: true, onConfirm: () => {
|
|
||||||
authManager.signout(true).then(() => {
|
|
||||||
window.location.reload();
|
|
||||||
})
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Import/Export */
|
|
||||||
|
|
||||||
$scope.archiveFormData = {encrypted: $scope.encryptedBackupsAvailable() ? true : false};
|
|
||||||
$scope.user = authManager.user;
|
|
||||||
|
|
||||||
$scope.submitImportPassword = function() {
|
|
||||||
$scope.performImport($scope.importData.data, $scope.importData.password);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.performImport = function(data, password) {
|
|
||||||
$scope.importData.loading = true;
|
|
||||||
// allow loading indicator to come up with timeout
|
|
||||||
$timeout(function(){
|
|
||||||
$scope.importJSONData(data, password, function(response, errorCount){
|
|
||||||
$timeout(function(){
|
|
||||||
$scope.importData.loading = false;
|
|
||||||
$scope.importData = null;
|
|
||||||
|
|
||||||
// Update UI before showing alert
|
|
||||||
setTimeout(function () {
|
|
||||||
// Response can be null if syncing offline
|
|
||||||
if(response && response.error) {
|
|
||||||
alertManager.alert({text: "There was an error importing your data. Please try again."});
|
|
||||||
} else {
|
|
||||||
if(errorCount > 0) {
|
|
||||||
var message = `Import complete. ${errorCount} items were not imported because there was an error decrypting them. Make sure the password is correct and try again.`;
|
|
||||||
alertManager.alert({text: message});
|
|
||||||
} else {
|
|
||||||
alertManager.alert({text: "Your data has been successfully imported."})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.importFileSelected = async function(files) {
|
|
||||||
|
|
||||||
let run = () => {
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.importData = {};
|
|
||||||
|
|
||||||
var file = files[0];
|
|
||||||
var reader = new FileReader();
|
|
||||||
reader.onload = function(e) {
|
|
||||||
try {
|
|
||||||
var data = JSON.parse(e.target.result);
|
|
||||||
$timeout(function(){
|
|
||||||
if(data.auth_params) {
|
|
||||||
// request password
|
|
||||||
$scope.importData.requestPassword = true;
|
|
||||||
$scope.importData.data = data;
|
|
||||||
|
|
||||||
$timeout(() => {
|
|
||||||
var element = document.getElementById("import-password-request");
|
|
||||||
if(element) {
|
|
||||||
element.scrollIntoView(false);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
$scope.performImport(data, null);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
alertManager.alert({text: "Unable to open file. Ensure it is a proper JSON file and try again."});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.readAsText(file);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageBackups)) {
|
|
||||||
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => {
|
|
||||||
run();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.importJSONData = function(data, password, callback) {
|
|
||||||
var onDataReady = async (errorCount) => {
|
|
||||||
var items = await modelManager.importItems(data.items);
|
|
||||||
for(var item of items) {
|
|
||||||
// We don't want to activate any components during import process in case of exceptions
|
|
||||||
// breaking up the import proccess
|
|
||||||
if(item.content_type == "SN|Component") { item.active = false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
syncManager.sync().then((response) => {
|
|
||||||
// Response can be null if syncing offline
|
|
||||||
callback(response, errorCount);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if(data.auth_params) {
|
|
||||||
protocolManager.computeEncryptionKeysForUser(password, data.auth_params).then((keys) => {
|
|
||||||
try {
|
|
||||||
protocolManager.decryptMultipleItems(data.items, keys, false) /* throws = false as we don't want to interrupt all decryption if just one fails */
|
|
||||||
.then(() => {
|
|
||||||
// delete items enc_item_key since the user's actually key will do the encrypting once its passed off
|
|
||||||
data.items.forEach(function(item){
|
|
||||||
item.enc_item_key = null;
|
|
||||||
item.auth_hash = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
var errorCount = 0;
|
|
||||||
// Don't import items that didn't decrypt properly
|
|
||||||
data.items = data.items.filter(function(item){
|
|
||||||
if(item.errorDecrypting) {
|
|
||||||
errorCount++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
|
|
||||||
onDataReady(errorCount);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.log("Error decrypting", e);
|
|
||||||
alertManager.alert({text: "There was an error decrypting your items. Make sure the password you entered is correct and try again."});
|
|
||||||
callback(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
onDataReady();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Export
|
|
||||||
*/
|
|
||||||
|
|
||||||
$scope.downloadDataArchive = async function() {
|
|
||||||
archiveManager.downloadBackup($scope.archiveFormData.encrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Encryption Status
|
|
||||||
*/
|
|
||||||
|
|
||||||
$scope.notesAndTagsCount = function() {
|
|
||||||
var items = modelManager.allItemsMatchingTypes(["Note", "Tag"]);
|
|
||||||
return items.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.encryptionStatusForNotes = function() {
|
|
||||||
var length = $scope.notesAndTagsCount();
|
|
||||||
return length + "/" + length + " notes and tags encrypted";
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.encryptionEnabled = function() {
|
|
||||||
return passcodeManager.hasPasscode() || !authManager.offline();
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.encryptionSource = function() {
|
|
||||||
if(!authManager.offline()) {
|
|
||||||
return "Account keys";
|
|
||||||
} else if(passcodeManager.hasPasscode()) {
|
|
||||||
return "Local Passcode";
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.encryptionStatusString = function() {
|
|
||||||
if(!authManager.offline()) {
|
|
||||||
return "End-to-end encryption is enabled. Your data is encrypted on your device first, then synced to your private cloud.";
|
|
||||||
} else if(passcodeManager.hasPasscode()) {
|
|
||||||
return "Encryption is enabled. Your data is encrypted using your passcode before it is saved to your device storage.";
|
|
||||||
} else {
|
|
||||||
return "Encryption is not enabled. Sign in, register, or add a passcode lock to enable encryption.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Passcode Lock
|
|
||||||
*/
|
|
||||||
|
|
||||||
$scope.passcodeAutoLockOptions = passcodeManager.getAutoLockIntervalOptions();
|
|
||||||
|
|
||||||
$scope.reloadAutoLockInterval = function() {
|
|
||||||
passcodeManager.getAutoLockInterval().then((interval) => {
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.selectedAutoLockInterval = interval;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.reloadAutoLockInterval();
|
|
||||||
|
|
||||||
$scope.selectAutoLockInterval = async function(interval) {
|
|
||||||
let run = async () => {
|
|
||||||
await passcodeManager.setAutoLockInterval(interval);
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.reloadAutoLockInterval();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePasscode)) {
|
|
||||||
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePasscode, () => {
|
|
||||||
run();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.hasPasscode = function() {
|
|
||||||
return passcodeManager.hasPasscode();
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.addPasscodeClicked = function() {
|
|
||||||
$scope.formData.showPasscodeForm = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.submitPasscodeForm = function() {
|
|
||||||
var passcode = $scope.formData.passcode;
|
|
||||||
if(passcode !== $scope.formData.confirmPasscode) {
|
|
||||||
alertManager.alert({text: "The two passcodes you entered do not match. Please try again."});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let fn = $scope.formData.changingPasscode ? passcodeManager.changePasscode.bind(passcodeManager) : passcodeManager.setPasscode.bind(passcodeManager);
|
|
||||||
|
|
||||||
fn(passcode, () => {
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.formData.passcode = null;
|
|
||||||
$scope.formData.confirmPasscode = null;
|
|
||||||
$scope.formData.showPasscodeForm = false;
|
|
||||||
var offline = authManager.offline();
|
|
||||||
|
|
||||||
if(offline) {
|
|
||||||
// Allows desktop to make backup file
|
|
||||||
$rootScope.$broadcast("major-data-change");
|
|
||||||
$scope.clearDatabaseAndRewriteAllItems(false);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.changePasscodePressed = async function() {
|
|
||||||
let run = () => {
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.formData.changingPasscode = true;
|
|
||||||
$scope.addPasscodeClicked();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePasscode)) {
|
|
||||||
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePasscode, () => {
|
|
||||||
run();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.removePasscodePressed = async function() {
|
|
||||||
let run = () => {
|
|
||||||
$timeout(() => {
|
|
||||||
var signedIn = !authManager.offline();
|
|
||||||
var message = "Are you sure you want to remove your local passcode?";
|
|
||||||
if(!signedIn) {
|
|
||||||
message += " This will remove encryption from your local data.";
|
|
||||||
}
|
|
||||||
|
|
||||||
alertManager.confirm({text: message, destructive: true, onConfirm: () => {
|
|
||||||
passcodeManager.clearPasscode();
|
|
||||||
|
|
||||||
if(authManager.offline()) {
|
|
||||||
syncManager.markAllItemsDirtyAndSaveOffline();
|
|
||||||
// Don't create backup here, as if the user is temporarily removing the passcode to change it,
|
|
||||||
// we don't want to write unencrypted data to disk.
|
|
||||||
// $rootScope.$broadcast("major-data-change");
|
|
||||||
}
|
|
||||||
}})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePasscode)) {
|
|
||||||
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePasscode, () => {
|
|
||||||
run();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.isDesktopApplication = function() {
|
|
||||||
return isDesktopApplication();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import template from '%/directives/actions-menu.pug';
|
|
||||||
|
|
||||||
export class ActionsMenu {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.template = template;
|
|
||||||
this.scope = {
|
|
||||||
item: '='
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller($scope, modelManager, actionsManager) {
|
|
||||||
$scope.extensions = actionsManager.extensions.sort((a, b) => {
|
|
||||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
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, (response, error) => {
|
|
||||||
if(error) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
action.running = false;
|
|
||||||
$scope.handleActionResponse(action, response);
|
|
||||||
|
|
||||||
// reload extension actions
|
|
||||||
actionsManager.loadExtensionInContextOfItem(extension, $scope.item, function(ext){
|
|
||||||
// keep nested state
|
|
||||||
// 4/1/2019: We're not going to do this anymore because we're no longer using nested actions for version history,
|
|
||||||
// and also because finding the parentAction based on only label is not good enough. Two actions can have same label.
|
|
||||||
// We'd need a way to track actions after they are reloaded, but there's no good way to do this.
|
|
||||||
// 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;
|
|
||||||
actionsManager.presentRevisionPreviewModal(item.uuid, item.content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$scope.subRowsForAction = function(parentAction, extension) {
|
|
||||||
if(!parentAction.subactions) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return parentAction.subactions.map((subaction) => {
|
|
||||||
return {
|
|
||||||
onClick: () => {
|
|
||||||
this.executeAction(subaction, extension, parentAction);
|
|
||||||
},
|
|
||||||
label: subaction.label,
|
|
||||||
subtitle: subaction.desc,
|
|
||||||
spinnerClass: subaction.running ? 'info' : null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import template from '%/directives/component-modal.pug';
|
|
||||||
|
|
||||||
export class ComponentModal {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.template = template;
|
|
||||||
this.scope = {
|
|
||||||
show: '=',
|
|
||||||
component: '=',
|
|
||||||
callback: '=',
|
|
||||||
onDismiss: '&'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
link($scope, el, attrs) {
|
|
||||||
$scope.el = el;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller($scope, $timeout, componentManager) {
|
|
||||||
$scope.dismiss = function(callback) {
|
|
||||||
$scope.el.remove();
|
|
||||||
$scope.$destroy();
|
|
||||||
$scope.onDismiss && $scope.onDismiss() && $scope.onDismiss()($scope.component);
|
|
||||||
callback && callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
import template from '%/directives/component-view.pug';
|
|
||||||
|
|
||||||
import { isDesktopApplication } from '../../utils';
|
|
||||||
|
|
||||||
export class ComponentView {
|
|
||||||
constructor(
|
|
||||||
$rootScope,
|
|
||||||
componentManager,
|
|
||||||
desktopManager,
|
|
||||||
$timeout,
|
|
||||||
themeManager
|
|
||||||
) {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.template = template;
|
|
||||||
this.scope = {
|
|
||||||
component: '=',
|
|
||||||
onLoad: '=?',
|
|
||||||
manualDealloc: '=?'
|
|
||||||
};
|
|
||||||
|
|
||||||
this.desktopManager = desktopManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
link($scope, el, attrs, ctrl) {
|
|
||||||
$scope.el = el;
|
|
||||||
|
|
||||||
$scope.componentValid = true;
|
|
||||||
|
|
||||||
$scope.updateObserver = this.desktopManager.registerUpdateObserver((component) => {
|
|
||||||
if(component == $scope.component && component.active) {
|
|
||||||
$scope.reloadComponent();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
$scope.$watch('component', function(component, prevComponent){
|
|
||||||
ctrl.componentValueChanging(component, prevComponent);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller(
|
|
||||||
$scope,
|
|
||||||
$rootScope,
|
|
||||||
$timeout,
|
|
||||||
componentManager,
|
|
||||||
desktopManager,
|
|
||||||
themeManager
|
|
||||||
) {
|
|
||||||
$scope.onVisibilityChange = function() {
|
|
||||||
if(document.visibilityState == "hidden") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if($scope.issueLoading) {
|
|
||||||
$scope.reloadComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.themeHandlerIdentifier = "component-view-" + Math.random();
|
|
||||||
componentManager.registerHandler({identifier: $scope.themeHandlerIdentifier, areas: ["themes"], activationHandler: (component) => {
|
|
||||||
$scope.reloadThemeStatus();
|
|
||||||
}});
|
|
||||||
|
|
||||||
$scope.identifier = "component-view-" + Math.random();
|
|
||||||
|
|
||||||
componentManager.registerHandler({
|
|
||||||
identifier: $scope.identifier,
|
|
||||||
areas: [$scope.component.area],
|
|
||||||
activationHandler: (component) => {
|
|
||||||
if(component !== $scope.component) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.handleActivation();
|
|
||||||
})
|
|
||||||
},
|
|
||||||
actionHandler: (component, action, data) => {
|
|
||||||
if(action == "set-size") {
|
|
||||||
componentManager.handleSetSizeEvent(component, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.handleActivation = function() {
|
|
||||||
// activationHandlers may be called multiple times, design below to be idempotent
|
|
||||||
let component = $scope.component;
|
|
||||||
if(!component.active) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let iframe = componentManager.iframeForComponent(component);
|
|
||||||
if(iframe) {
|
|
||||||
$scope.loading = true;
|
|
||||||
// begin loading error handler. If onload isn't called in x seconds, display an error
|
|
||||||
if($scope.loadTimeout) { $timeout.cancel($scope.loadTimeout);}
|
|
||||||
$scope.loadTimeout = $timeout(() => {
|
|
||||||
if($scope.loading) {
|
|
||||||
$scope.loading = false;
|
|
||||||
$scope.issueLoading = true;
|
|
||||||
|
|
||||||
if(!$scope.didAttemptReload) {
|
|
||||||
$scope.didAttemptReload = true;
|
|
||||||
$scope.reloadComponent();
|
|
||||||
} else {
|
|
||||||
// We'll attempt to reload when the tab gains focus
|
|
||||||
document.addEventListener("visibilitychange", $scope.onVisibilityChange);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 3500);
|
|
||||||
iframe.onload = (event) => {
|
|
||||||
let desktopError = false;
|
|
||||||
try {
|
|
||||||
// Accessing iframe.contentWindow.origin will throw an exception if we are in the web app, or if the iframe content
|
|
||||||
// is remote content. The only reason it works in this case is because we're accessing a local extension.
|
|
||||||
// In the future when the desktop app falls back to the web location if local fail loads, we won't be able to access this property anymore.
|
|
||||||
if(isDesktopApplication() && (iframe.contentWindow.origin == null || iframe.contentWindow.origin == 'null')) {
|
|
||||||
/*
|
|
||||||
Don't attempt reload in this case, as it results in infinite loop, since a reload will deactivate the extension and then reactivate.
|
|
||||||
This can cause this componentView to be dealloced and a new one to be instantiated. This happens in editor.js, which we'll need to look into.
|
|
||||||
Don't return from this clause either, since we don't want to cancel loadTimeout (that will trigger reload). Instead, handle custom fail logic here.
|
|
||||||
*/
|
|
||||||
desktopError = true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
$timeout.cancel($scope.loadTimeout);
|
|
||||||
componentManager.registerComponentWindow(component, iframe.contentWindow).then(() => {
|
|
||||||
// Add small timeout to, as $scope.loading controls loading overlay,
|
|
||||||
// which is used to avoid flicker when enabling extensions while having an enabled theme
|
|
||||||
// we don't use ng-show because it causes problems with rendering iframes after timeout, for some reason.
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.loading = false;
|
|
||||||
$scope.issueLoading = desktopError; /* Typically we'd just set this to false at this point, but we now account for desktopError */
|
|
||||||
$scope.onLoad && $scope.onLoad($scope.component);
|
|
||||||
}, 7)
|
|
||||||
})
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
General note regarding activation/deactivation of components:
|
|
||||||
We pass `true` to componentManager.ac/detivateComponent for the `dontSync` parameter.
|
|
||||||
The activation we do in here is not global, but just local, so we don't need to sync the state.
|
|
||||||
For example, if we activate an editor, we just need to do that for display purposes, but dont
|
|
||||||
need to perform a sync to propagate that .active flag.
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.componentValueChanging = (component, prevComponent) => {
|
|
||||||
//
|
|
||||||
// See comment above about passing true to componentManager.ac/detivateComponent
|
|
||||||
//
|
|
||||||
if(prevComponent && component !== prevComponent) {
|
|
||||||
// Deactive old component
|
|
||||||
componentManager.deactivateComponent(prevComponent, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(component) {
|
|
||||||
componentManager.activateComponent(component, true);
|
|
||||||
// console.log("Loading", $scope.component.name, $scope.getUrl(), component.valid_until);
|
|
||||||
|
|
||||||
$scope.reloadStatus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.$on("ext-reload-complete", () => {
|
|
||||||
$scope.reloadStatus(false);
|
|
||||||
})
|
|
||||||
|
|
||||||
$scope.reloadComponent = function() {
|
|
||||||
// console.log("Reloading component", $scope.component);
|
|
||||||
// force iFrame to deinit, allows new one to be created
|
|
||||||
$scope.componentValid = false;
|
|
||||||
componentManager.reloadComponent($scope.component).then(() => {
|
|
||||||
$scope.reloadStatus();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.reloadStatus = function(doManualReload = true) {
|
|
||||||
let component = $scope.component;
|
|
||||||
$scope.reloading = true;
|
|
||||||
let previouslyValid = $scope.componentValid;
|
|
||||||
|
|
||||||
let offlineRestricted = component.offlineOnly && !isDesktopApplication();
|
|
||||||
|
|
||||||
let urlError =
|
|
||||||
(!isDesktopApplication() && !component.hasValidHostedUrl())
|
|
||||||
||
|
|
||||||
(isDesktopApplication() && (!component.local_url && !component.hasValidHostedUrl()))
|
|
||||||
|
|
||||||
$scope.expired = component.valid_until && component.valid_until <= new Date();
|
|
||||||
|
|
||||||
// Here we choose our own readonly state based on custom logic. However, if a parent
|
|
||||||
// wants to implement their own readonly logic, they can lock it.
|
|
||||||
if(!component.lockReadonly) {
|
|
||||||
component.readonly = $scope.expired;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.componentValid = !offlineRestricted && !urlError;
|
|
||||||
|
|
||||||
if(!$scope.componentValid) {
|
|
||||||
// required to disable overlay
|
|
||||||
$scope.loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(offlineRestricted) $scope.error = 'offline-restricted';
|
|
||||||
else if(urlError) $scope.error = 'url-missing';
|
|
||||||
else $scope.error = null;
|
|
||||||
|
|
||||||
if($scope.componentValid !== previouslyValid) {
|
|
||||||
if($scope.componentValid) {
|
|
||||||
// We want to reload here, rather than `activateComponent`, because the component will already have attempted to been activated.
|
|
||||||
componentManager.reloadComponent(component, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if($scope.expired && doManualReload) {
|
|
||||||
// Try reloading, handled by footer, which will open Extensions window momentarily to pull in latest data
|
|
||||||
// Upon completion, this method, reloadStatus, will be called, upon where doManualReload will be false to prevent recursion.
|
|
||||||
$rootScope.$broadcast("reload-ext-data");
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.reloadThemeStatus();
|
|
||||||
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.reloading = false;
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.reloadThemeStatus = function() {
|
|
||||||
if(!$scope.component.acceptsThemes()) {
|
|
||||||
if(themeManager.hasActiveTheme()) {
|
|
||||||
if(!$scope.dismissedNoThemesMessage) {
|
|
||||||
$scope.showNoThemesMessage = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Can be the case if we've just deactivated a theme
|
|
||||||
$scope.showNoThemesMessage = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.noThemesMessageDismiss = function() {
|
|
||||||
$scope.showNoThemesMessage = false;
|
|
||||||
$scope.dismissedNoThemesMessage = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.disableActiveTheme = function() {
|
|
||||||
themeManager.deactivateAllThemes();
|
|
||||||
$scope.noThemesMessageDismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.getUrl = function() {
|
|
||||||
var url = componentManager.urlForComponent($scope.component);
|
|
||||||
$scope.component.runningLocally = (url == $scope.component.local_url);
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.destroy = function() {
|
|
||||||
componentManager.deregisterHandler($scope.themeHandlerIdentifier);
|
|
||||||
componentManager.deregisterHandler($scope.identifier);
|
|
||||||
if($scope.component && !$scope.manualDealloc) {
|
|
||||||
componentManager.deactivateComponent($scope.component, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
desktopManager.deregisterUpdateObserver($scope.updateObserver);
|
|
||||||
document.removeEventListener("visibilitychange", $scope.onVisibilityChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.$on("$destroy", function() {
|
|
||||||
$scope.destroy();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
/*
|
|
||||||
The purpose of the conflict resoltion modal is to present two versions of a conflicted item,
|
|
||||||
and allow the user to choose which to keep (or to keep both.)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import template from '%/directives/conflict-resolution-modal.pug';
|
|
||||||
|
|
||||||
export class ConflictResolutionModal {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.template = template;
|
|
||||||
this.scope = {
|
|
||||||
item1: '=',
|
|
||||||
item2: '=',
|
|
||||||
callback: '='
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
link($scope, el, attrs) {
|
|
||||||
|
|
||||||
$scope.dismiss = function() {
|
|
||||||
el.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller($scope, modelManager, syncManager, archiveManager, alertManager) {
|
|
||||||
$scope.createContentString = function(item) {
|
|
||||||
return JSON.stringify(
|
|
||||||
Object.assign({created_at: item.created_at, updated_at: item.updated_at}, item.content), null, 2
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.contentType = $scope.item1.content_type;
|
|
||||||
|
|
||||||
$scope.item1Content = $scope.createContentString($scope.item1);
|
|
||||||
$scope.item2Content = $scope.createContentString($scope.item2);
|
|
||||||
|
|
||||||
$scope.keepItem1 = function() {
|
|
||||||
alertManager.confirm({text: `Are you sure you want to delete the item on the right?`, destructive: true, onConfirm: () => {
|
|
||||||
modelManager.setItemToBeDeleted($scope.item2);
|
|
||||||
syncManager.sync().then(() => {
|
|
||||||
$scope.applyCallback();
|
|
||||||
})
|
|
||||||
|
|
||||||
$scope.dismiss();
|
|
||||||
}});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.keepItem2 = function() {
|
|
||||||
alertManager.confirm({text: `Are you sure you want to delete the item on the left?`, destructive: true, onConfirm: () => {
|
|
||||||
modelManager.setItemToBeDeleted($scope.item1);
|
|
||||||
syncManager.sync().then(() => {
|
|
||||||
$scope.applyCallback();
|
|
||||||
})
|
|
||||||
|
|
||||||
$scope.dismiss();
|
|
||||||
}});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.keepBoth = function() {
|
|
||||||
$scope.applyCallback();
|
|
||||||
$scope.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.export = function() {
|
|
||||||
archiveManager.downloadBackupOfItems([$scope.item1, $scope.item2], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.applyCallback = function() {
|
|
||||||
$scope.callback && $scope.callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import { isDesktopApplication } from '@/utils';
|
|
||||||
import template from '%/directives/editor-menu.pug';
|
|
||||||
|
|
||||||
export class EditorMenu {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.template = template;
|
|
||||||
this.scope = {
|
|
||||||
callback: '&',
|
|
||||||
selectedEditor: '=',
|
|
||||||
currentItem: '='
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller($scope, componentManager, syncManager, modelManager, $timeout) {
|
|
||||||
$scope.formData = {};
|
|
||||||
|
|
||||||
$scope.editors = componentManager.componentsForArea("editor-editor").sort((a, b) => {
|
|
||||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.isDesktop = isDesktopApplication();
|
|
||||||
|
|
||||||
$scope.defaultEditor = $scope.editors.filter((e) => {return e.isDefaultEditor()})[0];
|
|
||||||
|
|
||||||
$scope.selectComponent = function(component) {
|
|
||||||
if(component) {
|
|
||||||
if(component.content.conflict_of) {
|
|
||||||
component.content.conflict_of = null; // clear conflict if applicable
|
|
||||||
modelManager.setItemDirty(component, true);
|
|
||||||
syncManager.sync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$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);
|
|
||||||
modelManager.setItemDirty(currentDefault, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
component.setAppDataItem("defaultEditor", true);
|
|
||||||
modelManager.setItemDirty(component, true);
|
|
||||||
syncManager.sync();
|
|
||||||
|
|
||||||
$scope.defaultEditor = component;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.removeEditorDefault = function(component) {
|
|
||||||
component.setAppDataItem("defaultEditor", false);
|
|
||||||
modelManager.setItemDirty(component, true);
|
|
||||||
syncManager.sync();
|
|
||||||
|
|
||||||
$scope.defaultEditor = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.shouldDisplayRunningLocallyLabel = function(component) {
|
|
||||||
if(!component.runningLocally) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(component == $scope.selectedEditor) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import template from '%/directives/input-modal.pug';
|
|
||||||
|
|
||||||
export class InputModal {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.template = template;
|
|
||||||
this.scope = {
|
|
||||||
type: '=',
|
|
||||||
title: '=',
|
|
||||||
message: '=',
|
|
||||||
placeholder: '=',
|
|
||||||
callback: '&'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
link($scope, el, attrs) {
|
|
||||||
$scope.el = el;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller($scope, modelManager, archiveManager, authManager, syncManager, $timeout) {
|
|
||||||
$scope.formData = {};
|
|
||||||
|
|
||||||
$scope.dismiss = function() {
|
|
||||||
$scope.el.remove();
|
|
||||||
$scope.$destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.submit = function() {
|
|
||||||
$scope.callback()($scope.formData.input);
|
|
||||||
$scope.dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
import angular from 'angular';
|
|
||||||
import template from '%/directives/panel-resizer.pug';
|
|
||||||
|
|
||||||
export class PanelResizer {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.template = template;
|
|
||||||
this.scope = {
|
|
||||||
index: '=',
|
|
||||||
panelId: '=',
|
|
||||||
onResize: '&',
|
|
||||||
defaultWidth: '=',
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.control.flash = function() {
|
|
||||||
scope.flash();
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.control.isCollapsed = function() {
|
|
||||||
return scope.isCollapsed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller($scope, $element, modelManager, actionsManager, $timeout, $compile) {
|
|
||||||
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 = panel.offsetLeft, lastLeft = startLeft;
|
|
||||||
var appFrame;
|
|
||||||
|
|
||||||
$scope.isAtMaxWidth = function() {
|
|
||||||
return Math.round((lastWidth + lastLeft)) == Math.round(getParentRect().width);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.isCollapsed = function() {
|
|
||||||
return lastWidth <= minWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Double Click Event
|
|
||||||
var widthBeforeLastDblClick = 0;
|
|
||||||
resizerColumn.ondblclick = () => {
|
|
||||||
$timeout(() => {
|
|
||||||
var preClickCollapseState = $scope.isCollapsed();
|
|
||||||
if(preClickCollapseState) {
|
|
||||||
$scope.setWidth(widthBeforeLastDblClick || $scope.defaultWidth);
|
|
||||||
} else {
|
|
||||||
widthBeforeLastDblClick = lastWidth;
|
|
||||||
$scope.setWidth(minWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.finishSettingWidth();
|
|
||||||
|
|
||||||
var newCollapseState = !preClickCollapseState;
|
|
||||||
$scope.onResizeFinish()(lastWidth, lastLeft, $scope.isAtMaxWidth(), newCollapseState);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = $scope.isAtMaxWidth() ? getParentRect().width : panel.scrollWidth;
|
|
||||||
lastWidth = startWidth;
|
|
||||||
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((Math.round(width + lastLeft)) == Math.round(parentRect.width)) {
|
|
||||||
panel.style.width = `calc(100% - ${lastLeft}px)`;
|
|
||||||
panel.style.flexBasis = `calc(100% - ${lastLeft}px)`;
|
|
||||||
} 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
collapsed = $scope.isCollapsed();
|
|
||||||
if(collapsed) {
|
|
||||||
resizerColumn.classList.add("collapsed");
|
|
||||||
} else {
|
|
||||||
resizerColumn.classList.remove("collapsed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
If an iframe is displayed adjacent to our panel, and your mouse exits over the iframe,
|
|
||||||
document[onmouseup] is not triggered because the document is no longer the same over the iframe.
|
|
||||||
We add an invisible overlay while resizing so that the mouse context remains in our main document.
|
|
||||||
*/
|
|
||||||
$scope.addInvisibleOverlay = function() {
|
|
||||||
if($scope.overlay) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.overlay = $compile("<div id='resizer-overlay'></div>")($scope);
|
|
||||||
angular.element(document.body).prepend($scope.overlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.removeInvisibleOverlay = function() {
|
|
||||||
if($scope.overlay) {
|
|
||||||
$scope.overlay.remove();
|
|
||||||
$scope.overlay = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.flash = function() {
|
|
||||||
resizerColumn.classList.add("animate-opacity");
|
|
||||||
$timeout(() => {
|
|
||||||
resizerColumn.classList.remove("animate-opacity");
|
|
||||||
}, 3000)
|
|
||||||
}
|
|
||||||
|
|
||||||
resizerColumn.addEventListener("mousedown", function(event){
|
|
||||||
$scope.addInvisibleOverlay();
|
|
||||||
|
|
||||||
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) {
|
|
||||||
let 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", (event) => {
|
|
||||||
$scope.removeInvisibleOverlay();
|
|
||||||
|
|
||||||
if(pressed) {
|
|
||||||
pressed = false;
|
|
||||||
resizerColumn.classList.remove("dragging");
|
|
||||||
panel.classList.remove("no-selection");
|
|
||||||
|
|
||||||
let isMaxWidth = $scope.isAtMaxWidth();
|
|
||||||
|
|
||||||
if($scope.onResizeFinish) {
|
|
||||||
$scope.onResizeFinish()(lastWidth, lastLeft, isMaxWidth, $scope.isCollapsed());
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.finishSettingWidth();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
import { protocolManager } from 'snjs';
|
|
||||||
import template from '%/directives/password-wizard.pug';
|
|
||||||
|
|
||||||
export class PasswordWizard {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.template = template;
|
|
||||||
this.scope = {
|
|
||||||
type: '='
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
link($scope, el, attrs) {
|
|
||||||
$scope.el = el;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller($scope, modelManager, archiveManager, authManager, syncManager, $timeout, alertManager) {
|
|
||||||
|
|
||||||
window.onbeforeunload = (e) => {
|
|
||||||
// Confirms with user to close tab before closing
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.$on("$destroy", function() {
|
|
||||||
window.onbeforeunload = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.dismiss = function() {
|
|
||||||
if($scope.lockContinue) {
|
|
||||||
alertManager.alert({text: "Cannot close window until pending tasks are complete."});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$scope.el.remove();
|
|
||||||
$scope.$destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.syncStatus = syncManager.syncStatus;
|
|
||||||
$scope.formData = {};
|
|
||||||
|
|
||||||
const IntroStep = 0;
|
|
||||||
const BackupStep = 1;
|
|
||||||
const SignoutStep = 2;
|
|
||||||
const PasswordStep = 3;
|
|
||||||
const SyncStep = 4;
|
|
||||||
const FinishStep = 5;
|
|
||||||
|
|
||||||
let DefaultContinueTitle = "Continue";
|
|
||||||
$scope.continueTitle = DefaultContinueTitle;
|
|
||||||
|
|
||||||
$scope.step = IntroStep;
|
|
||||||
|
|
||||||
$scope.titleForStep = function(step) {
|
|
||||||
switch (step) {
|
|
||||||
case BackupStep:
|
|
||||||
return "Download a backup of your data";
|
|
||||||
case SignoutStep:
|
|
||||||
return "Sign out of all your devices";
|
|
||||||
case PasswordStep:
|
|
||||||
return $scope.changePassword ? "Password information" : "Enter your current password";
|
|
||||||
case SyncStep:
|
|
||||||
return "Encrypt and sync data with new keys";
|
|
||||||
case FinishStep:
|
|
||||||
return "Sign back in to your devices";
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.configure = function() {
|
|
||||||
if($scope.type == "change-pw") {
|
|
||||||
$scope.title = "Change Password";
|
|
||||||
$scope.changePassword = true;
|
|
||||||
} else if($scope.type == "upgrade-security") {
|
|
||||||
$scope.title = "Security Update";
|
|
||||||
$scope.securityUpdate = true;
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
|
|
||||||
$scope.continue = function() {
|
|
||||||
|
|
||||||
if($scope.lockContinue || $scope.isContinuing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// isContinuing is a way to lock the continue function separate from lockContinue
|
|
||||||
// lockContinue can be locked by other places, but isContinuing is only lockable from within this function.
|
|
||||||
|
|
||||||
$scope.isContinuing = true;
|
|
||||||
|
|
||||||
if($scope.step == FinishStep) {
|
|
||||||
$scope.dismiss();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let next = () => {
|
|
||||||
$scope.step += 1;
|
|
||||||
$scope.initializeStep($scope.step);
|
|
||||||
|
|
||||||
$scope.isContinuing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var preprocessor = $scope.preprocessorForStep($scope.step);
|
|
||||||
if(preprocessor) {
|
|
||||||
preprocessor(() => {
|
|
||||||
next();
|
|
||||||
}, () => {
|
|
||||||
// on fail
|
|
||||||
$scope.isContinuing = false;
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.downloadBackup = function(encrypted) {
|
|
||||||
archiveManager.downloadBackup(encrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.preprocessorForStep = function(step) {
|
|
||||||
if(step == PasswordStep) {
|
|
||||||
return (onSuccess, onFail) => {
|
|
||||||
$scope.showSpinner = true;
|
|
||||||
$scope.continueTitle = "Generating Keys...";
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.validateCurrentPassword((success) => {
|
|
||||||
$scope.showSpinner = false;
|
|
||||||
$scope.continueTitle = DefaultContinueTitle;
|
|
||||||
if(success) {
|
|
||||||
onSuccess();
|
|
||||||
} else {
|
|
||||||
onFail && onFail();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let FailedSyncMessage = "There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.";
|
|
||||||
|
|
||||||
$scope.initializeStep = function(step) {
|
|
||||||
if(step == SyncStep) {
|
|
||||||
$scope.lockContinue = true;
|
|
||||||
$scope.formData.status = "Processing encryption keys...";
|
|
||||||
$scope.formData.processing = true;
|
|
||||||
|
|
||||||
$scope.processPasswordChange((passwordSuccess) => {
|
|
||||||
$scope.formData.statusError = !passwordSuccess;
|
|
||||||
$scope.formData.processing = passwordSuccess;
|
|
||||||
|
|
||||||
if(passwordSuccess) {
|
|
||||||
$scope.formData.status = "Encrypting and syncing data with new keys...";
|
|
||||||
|
|
||||||
$scope.resyncData((syncSuccess) => {
|
|
||||||
$scope.formData.statusError = !syncSuccess;
|
|
||||||
$scope.formData.processing = !syncSuccess;
|
|
||||||
if(syncSuccess) {
|
|
||||||
$scope.lockContinue = false;
|
|
||||||
|
|
||||||
if($scope.changePassword) {
|
|
||||||
$scope.formData.status = "Successfully changed password and synced all items.";
|
|
||||||
} else if($scope.securityUpdate) {
|
|
||||||
$scope.formData.status = "Successfully performed security update and synced all items.";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$scope.formData.status = FailedSyncMessage;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
$scope.formData.status = "Unable to process your password. Please try again.";
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
else if(step == FinishStep) {
|
|
||||||
$scope.continueTitle = "Finish";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.validateCurrentPassword = async function(callback) {
|
|
||||||
let currentPassword = $scope.formData.currentPassword;
|
|
||||||
let newPass = $scope.securityUpdate ? currentPassword : $scope.formData.newPassword;
|
|
||||||
|
|
||||||
if(!currentPassword || currentPassword.length == 0) {
|
|
||||||
alertManager.alert({text: "Please enter your current password."});
|
|
||||||
callback(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if($scope.changePassword) {
|
|
||||||
if(!newPass || newPass.length == 0) {
|
|
||||||
alertManager.alert({text: "Please enter a new password."});
|
|
||||||
callback(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(newPass != $scope.formData.newPasswordConfirmation) {
|
|
||||||
alertManager.alert({text: "Your new password does not match its confirmation."});
|
|
||||||
$scope.formData.status = null;
|
|
||||||
callback(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!authManager.user.email) {
|
|
||||||
alertManager.alert({text: "We don't have your email stored. Please log out then log back in to fix this issue."});
|
|
||||||
$scope.formData.status = null;
|
|
||||||
callback(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure value for current password matches what's saved
|
|
||||||
let authParams = await authManager.getAuthParams();
|
|
||||||
let password = $scope.formData.currentPassword;
|
|
||||||
protocolManager.computeEncryptionKeysForUser(password, authParams).then(async (keys) => {
|
|
||||||
let success = keys.mk === (await authManager.keys()).mk;
|
|
||||||
if(success) {
|
|
||||||
this.currentServerPw = keys.pw;
|
|
||||||
} else {
|
|
||||||
alertManager.alert({text: "The current password you entered is not correct. Please try again."});
|
|
||||||
}
|
|
||||||
$timeout(() => callback(success));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.resyncData = function(callback) {
|
|
||||||
modelManager.setAllItemsDirty();
|
|
||||||
syncManager.sync().then((response) => {
|
|
||||||
if(!response || response.error) {
|
|
||||||
alertManager.alert({text: FailedSyncMessage})
|
|
||||||
$timeout(() => callback(false));
|
|
||||||
} else {
|
|
||||||
$timeout(() => callback(true));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.processPasswordChange = async function(callback) {
|
|
||||||
let newUserPassword = $scope.securityUpdate ? $scope.formData.currentPassword : $scope.formData.newPassword;
|
|
||||||
|
|
||||||
let currentServerPw = this.currentServerPw;
|
|
||||||
|
|
||||||
let results = await protocolManager.generateInitialKeysAndAuthParamsForUser(authManager.user.email, newUserPassword);
|
|
||||||
let newKeys = results.keys;
|
|
||||||
let newAuthParams = results.authParams;
|
|
||||||
|
|
||||||
// perform a sync beforehand to pull in any last minutes changes before we change the encryption key (and thus cant decrypt new changes)
|
|
||||||
let syncResponse = await syncManager.sync();
|
|
||||||
authManager.changePassword(await syncManager.getServerURL(), authManager.user.email, currentServerPw, newKeys, newAuthParams).then((response) => {
|
|
||||||
if(response.error) {
|
|
||||||
alertManager.alert({text: response.error.message ? response.error.message : "There was an error changing your password. Please try again."});
|
|
||||||
$timeout(() => callback(false));
|
|
||||||
} else {
|
|
||||||
$timeout(() => callback(true));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import template from '%/directives/permissions-modal.pug';
|
|
||||||
|
|
||||||
export class PermissionsModal {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.template = template;
|
|
||||||
this.scope = {
|
|
||||||
show: '=',
|
|
||||||
component: '=',
|
|
||||||
permissionsString: '=',
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller($scope, modelManager) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import template from '%/directives/privileges-auth-modal.pug';
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
export class PrivilegesAuthModal {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.template = template;
|
|
||||||
this.scope = {
|
|
||||||
action: '=',
|
|
||||||
onSuccess: '=',
|
|
||||||
onCancel: '='
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
link($scope, el, attrs) {
|
|
||||||
$scope.dismiss = function() {
|
|
||||||
el.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
controller($scope, privilegesManager, passcodeManager, authManager, $timeout) {
|
|
||||||
'ngInject';
|
|
||||||
|
|
||||||
$scope.authenticationParameters = {};
|
|
||||||
$scope.sessionLengthOptions = privilegesManager.getSessionLengthOptions();
|
|
||||||
|
|
||||||
privilegesManager.getSelectedSessionLength().then((length) => {
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.selectedSessionLength = length;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
$scope.selectSessionLength = function(length) {
|
|
||||||
$scope.selectedSessionLength = length;
|
|
||||||
}
|
|
||||||
|
|
||||||
privilegesManager.netCredentialsForAction($scope.action).then((credentials) => {
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.requiredCredentials = credentials.sort();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.promptForCredential = function(credential) {
|
|
||||||
return privilegesManager.displayInfoForCredential(credential).prompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.cancel = function() {
|
|
||||||
$scope.dismiss();
|
|
||||||
$scope.onCancel && $scope.onCancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.isCredentialInFailureState = function(credential) {
|
|
||||||
if(!$scope.failedCredentials) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return $scope.failedCredentials.find((candidate) => {
|
|
||||||
return candidate == credential;
|
|
||||||
}) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.validate = function() {
|
|
||||||
var failed = [];
|
|
||||||
for(var cred of $scope.requiredCredentials) {
|
|
||||||
var value = $scope.authenticationParameters[cred];
|
|
||||||
if(!value || value.length == 0) {
|
|
||||||
failed.push(cred);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.failedCredentials = failed;
|
|
||||||
return failed.length == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.submit = function() {
|
|
||||||
if(!$scope.validate()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
privilegesManager.authenticateAction($scope.action, $scope.authenticationParameters).then((result) => {
|
|
||||||
$timeout(() => {
|
|
||||||
if(result.success) {
|
|
||||||
privilegesManager.setSessionLength($scope.selectedSessionLength);
|
|
||||||
$scope.onSuccess();
|
|
||||||
$scope.dismiss();
|
|
||||||
} else {
|
|
||||||
$scope.failedCredentials = result.failedCredentials;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import { PrivilegesManager } from '@/services/privilegesManager';
|
|
||||||
import template from '%/directives/privileges-management-modal.pug';
|
|
||||||
|
|
||||||
export class PrivilegesManagementModal {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.template = template;
|
|
||||||
this.scope = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
link($scope, el, attrs) {
|
|
||||||
$scope.dismiss = function() {
|
|
||||||
el.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller($scope, privilegesManager, passcodeManager, authManager, $timeout) {
|
|
||||||
$scope.dummy = {};
|
|
||||||
|
|
||||||
$scope.hasPasscode = passcodeManager.hasPasscode();
|
|
||||||
$scope.hasAccount = !authManager.offline();
|
|
||||||
|
|
||||||
$scope.displayInfoForCredential = function(credential) {
|
|
||||||
let info = privilegesManager.displayInfoForCredential(credential);
|
|
||||||
if(credential == PrivilegesManager.CredentialLocalPasscode) {
|
|
||||||
info["availability"] = $scope.hasPasscode;
|
|
||||||
} else if(credential == PrivilegesManager.CredentialAccountPassword) {
|
|
||||||
info["availability"] = $scope.hasAccount;
|
|
||||||
} else {
|
|
||||||
info["availability"] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.displayInfoForAction = function(action) {
|
|
||||||
return privilegesManager.displayInfoForAction(action).label;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.isCredentialRequiredForAction = function(action, credential) {
|
|
||||||
if(!$scope.privileges) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return $scope.privileges.isCredentialRequiredForAction(action, credential);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.clearSession = function() {
|
|
||||||
privilegesManager.clearSession().then(() => {
|
|
||||||
$scope.reloadPrivileges();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.reloadPrivileges = async function() {
|
|
||||||
$scope.availableActions = privilegesManager.getAvailableActions();
|
|
||||||
$scope.availableCredentials = privilegesManager.getAvailableCredentials();
|
|
||||||
let sessionEndDate = await privilegesManager.getSessionExpirey();
|
|
||||||
$scope.sessionExpirey = sessionEndDate.toLocaleString();
|
|
||||||
$scope.sessionExpired = new Date() >= sessionEndDate;
|
|
||||||
|
|
||||||
$scope.credentialDisplayInfo = {};
|
|
||||||
for(let cred of $scope.availableCredentials) {
|
|
||||||
$scope.credentialDisplayInfo[cred] = $scope.displayInfoForCredential(cred);
|
|
||||||
}
|
|
||||||
|
|
||||||
privilegesManager.getPrivileges().then((privs) => {
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.privileges = privs;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.checkboxValueChanged = function(action, credential) {
|
|
||||||
$scope.privileges.toggleCredentialForAction(action, credential);
|
|
||||||
privilegesManager.savePrivileges();
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.reloadPrivileges();
|
|
||||||
|
|
||||||
$scope.cancel = function() {
|
|
||||||
$scope.dismiss();
|
|
||||||
$scope.onCancel && $scope.onCancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import { protocolManager, SNComponent, SFItem, SFModelManager } from 'snjs';
|
|
||||||
import template from '%/directives/revision-preview-modal.pug';
|
|
||||||
|
|
||||||
export class RevisionPreviewModal {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.template = template;
|
|
||||||
this.scope = {
|
|
||||||
uuid: '=',
|
|
||||||
content: '='
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
link($scope, el, attrs) {
|
|
||||||
$scope.el = el;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller($scope, modelManager, syncManager, componentManager, $timeout, alertManager) {
|
|
||||||
$scope.dismiss = function() {
|
|
||||||
$scope.el.remove();
|
|
||||||
$scope.$destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.$on("$destroy", function() {
|
|
||||||
if($scope.identifier) {
|
|
||||||
componentManager.deregisterHandler($scope.identifier);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.note = new SFItem({content: $scope.content, content_type: "Note"});
|
|
||||||
// Set UUID to editoForNote can find proper editor,
|
|
||||||
// but then generate new uuid for note as not to save changes to original, if editor makes changes.
|
|
||||||
$scope.note.uuid = $scope.uuid;
|
|
||||||
let editorForNote = componentManager.editorForNote($scope.note);
|
|
||||||
$scope.note.uuid = protocolManager.crypto.generateUUIDSync();
|
|
||||||
|
|
||||||
if(editorForNote) {
|
|
||||||
// Create temporary copy, as a lot of componentManager is uuid based,
|
|
||||||
// so might interfere with active editor. Be sure to copy only the content, as the
|
|
||||||
// top level editor object has non-copyable properties like .window, which cannot be transfered
|
|
||||||
let editorCopy = new SNComponent({content: editorForNote.content});
|
|
||||||
editorCopy.readonly = true;
|
|
||||||
editorCopy.lockReadonly = true;
|
|
||||||
$scope.identifier = editorCopy.uuid;
|
|
||||||
|
|
||||||
componentManager.registerHandler({identifier: $scope.identifier, areas: ["editor-editor"],
|
|
||||||
contextRequestHandler: (component) => {
|
|
||||||
if(component == $scope.editor) {
|
|
||||||
return $scope.note;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentForSessionKeyHandler: (key) => {
|
|
||||||
if(key == $scope.editor.sessionKey) {
|
|
||||||
return $scope.editor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.editor = editorCopy;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.restore = function(asCopy) {
|
|
||||||
const run = () => {
|
|
||||||
let item;
|
|
||||||
if(asCopy) {
|
|
||||||
let contentCopy = Object.assign({}, $scope.content);
|
|
||||||
if(contentCopy.title) { contentCopy.title += " (copy)"; }
|
|
||||||
item = modelManager.createItem({content_type: "Note", content: contentCopy});
|
|
||||||
modelManager.addItem(item);
|
|
||||||
} else {
|
|
||||||
let uuid = $scope.uuid;
|
|
||||||
item = modelManager.findItem(uuid);
|
|
||||||
item.content = Object.assign({}, $scope.content);
|
|
||||||
// mapResponseItemsToLocalModels is async, but we don't need to wait here.
|
|
||||||
modelManager.mapResponseItemsToLocalModels([item], SFModelManager.MappingSourceRemoteActionRetrieved);
|
|
||||||
}
|
|
||||||
|
|
||||||
modelManager.setItemDirty(item, true);
|
|
||||||
syncManager.sync();
|
|
||||||
$scope.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!asCopy) {
|
|
||||||
alertManager.confirm({text: "Are you sure you want to replace the current note's contents with what you see in this preview?", destructive: true, onConfirm: () => {
|
|
||||||
run();
|
|
||||||
}})
|
|
||||||
} else {
|
|
||||||
run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import template from '%/directives/session-history-menu.pug';
|
|
||||||
|
|
||||||
export class SessionHistoryMenu {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.template = template;
|
|
||||||
this.scope = {
|
|
||||||
item: '='
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller($scope, modelManager, sessionHistory, actionsManager, $timeout, alertManager) {
|
|
||||||
$scope.diskEnabled = sessionHistory.diskEnabled;
|
|
||||||
$scope.autoOptimize = sessionHistory.autoOptimize;
|
|
||||||
|
|
||||||
$scope.reloadHistory = function() {
|
|
||||||
let history = sessionHistory.historyForItem($scope.item);
|
|
||||||
// make copy as not to sort inline
|
|
||||||
$scope.entries = history.entries.slice(0).sort((a, b) => {
|
|
||||||
return a.item.updated_at < b.item.updated_at ? 1 : -1;
|
|
||||||
})
|
|
||||||
$scope.history = history;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.reloadHistory();
|
|
||||||
|
|
||||||
$scope.openRevision = function(revision) {
|
|
||||||
actionsManager.presentRevisionPreviewModal(revision.item.uuid, revision.item.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.classForRevision = function(revision) {
|
|
||||||
var vector = revision.operationVector();
|
|
||||||
if(vector == 0) {
|
|
||||||
return "default";
|
|
||||||
} else if(vector == 1) {
|
|
||||||
return "success";
|
|
||||||
} else if(vector == -1) {
|
|
||||||
return "danger";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.clearItemHistory = function() {
|
|
||||||
alertManager.confirm({text: "Are you sure you want to delete the local session history for this note?", destructive: true, onConfirm: () => {
|
|
||||||
sessionHistory.clearHistoryForItem($scope.item).then(() => {
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.reloadHistory();
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.clearAllHistory = function() {
|
|
||||||
alertManager.confirm({text: "Are you sure you want to delete the local session history for all notes?", destructive: true, onConfirm: () => {
|
|
||||||
sessionHistory.clearAllHistory().then(() => {
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.reloadHistory();
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.toggleDiskSaving = function() {
|
|
||||||
const run = () => {
|
|
||||||
sessionHistory.toggleDiskSaving().then(() => {
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.diskEnabled = sessionHistory.diskEnabled;
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!sessionHistory.diskEnabled) {
|
|
||||||
alertManager.confirm({text: "Are you sure you want to save history to disk? This will decrease general performance, especially as you type. You are advised to disable this feature if you experience any lagging.", destructive: true, onConfirm: () => {
|
|
||||||
run();
|
|
||||||
}})
|
|
||||||
} else {
|
|
||||||
run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.toggleAutoOptimize = function() {
|
|
||||||
sessionHistory.toggleAutoOptimize().then(() => {
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.autoOptimize = sessionHistory.autoOptimize;
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import template from '%/directives/sync-resolution-menu.pug';
|
|
||||||
|
|
||||||
export class SyncResolutionMenu {
|
|
||||||
constructor() {
|
|
||||||
this.restrict = 'E';
|
|
||||||
this.template = template;
|
|
||||||
this.scope = {
|
|
||||||
closeFunction: '&'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller($scope, modelManager, syncManager, archiveManager, $timeout) {
|
|
||||||
$scope.status = {};
|
|
||||||
|
|
||||||
$scope.close = function() {
|
|
||||||
$timeout(() => {
|
|
||||||
$scope.closeFunction()();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.downloadBackup = function(encrypted) {
|
|
||||||
archiveManager.downloadBackup(encrypted);
|
|
||||||
$scope.status.backupFinished = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.skipBackup = function() {
|
|
||||||
$scope.status.backupFinished = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.performSyncResolution = function() {
|
|
||||||
$scope.status.resolving = true;
|
|
||||||
syncManager.resolveOutOfSync().then(() => {
|
|
||||||
$scope.status.resolving = false;
|
|
||||||
$scope.status.attemptedResolution = true;
|
|
||||||
if(syncManager.isOutOfSync()) {
|
|
||||||
$scope.status.fail = true;
|
|
||||||
} else {
|
|
||||||
$scope.status.success = true;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import angular from 'angular';
|
|
||||||
import { Action, SFModelManager, SFItemParams, protocolManager } from 'snjs';
|
|
||||||
|
|
||||||
export class ActionsManager {
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
constructor(
|
|
||||||
httpManager,
|
|
||||||
modelManager,
|
|
||||||
authManager,
|
|
||||||
syncManager,
|
|
||||||
$rootScope,
|
|
||||||
$compile,
|
|
||||||
$timeout,
|
|
||||||
alertManager
|
|
||||||
) {
|
|
||||||
this.httpManager = httpManager;
|
|
||||||
this.modelManager = modelManager;
|
|
||||||
this.authManager = authManager;
|
|
||||||
this.syncManager = syncManager;
|
|
||||||
this.alertManager = alertManager;
|
|
||||||
this.$rootScope = $rootScope;
|
|
||||||
this.$compile = $compile;
|
|
||||||
this.$timeout = $timeout;
|
|
||||||
|
|
||||||
// Used when decrypting old items with new keys. This array is only kept in memory.
|
|
||||||
this.previousPasswords = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
get extensions() {
|
|
||||||
return this.modelManager.validItemsForContentType("Extension");
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async executeAction(action, extension, item, callback) {
|
|
||||||
|
|
||||||
var customCallback = (response, error) => {
|
|
||||||
action.running = false;
|
|
||||||
this.$timeout(() => {
|
|
||||||
callback(response, error);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
action.running = true;
|
|
||||||
|
|
||||||
let decrypted = action.access_type == "decrypted";
|
|
||||||
|
|
||||||
var triedPasswords = [];
|
|
||||||
|
|
||||||
let handleResponseDecryption = async (response, keys, merge) => {
|
|
||||||
var item = response.item;
|
|
||||||
|
|
||||||
await protocolManager.decryptItem(item, keys);
|
|
||||||
|
|
||||||
if(!item.errorDecrypting) {
|
|
||||||
if(merge) {
|
|
||||||
var items = await this.modelManager.mapResponseItemsToLocalModels([item], SFModelManager.MappingSourceRemoteActionRetrieved);
|
|
||||||
for(var mappedItem of items) {
|
|
||||||
this.modelManager.setItemDirty(mappedItem, true);
|
|
||||||
}
|
|
||||||
this.syncManager.sync();
|
|
||||||
customCallback({item: item});
|
|
||||||
} else {
|
|
||||||
item = this.modelManager.createItem(item);
|
|
||||||
customCallback({item: item});
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
// Error decrypting
|
|
||||||
if(!response.auth_params) {
|
|
||||||
// In some cases revisions were missing auth params. Instruct the user to email us to get this remedied.
|
|
||||||
this.alertManager.alert({text: "We were unable to decrypt this revision using your current keys, and this revision is missing metadata that would allow us to try different keys to decrypt it. This can likely be fixed with some manual intervention. Please email hello@standardnotes.org for assistance."});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try previous passwords
|
|
||||||
for(let passwordCandidate of this.previousPasswords) {
|
|
||||||
if(triedPasswords.includes(passwordCandidate)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
triedPasswords.push(passwordCandidate);
|
|
||||||
|
|
||||||
var keyResults = await protocolManager.computeEncryptionKeysForUser(passwordCandidate, response.auth_params);
|
|
||||||
if(!keyResults) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var success = await handleResponseDecryption(response, keyResults, merge);
|
|
||||||
if(success) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.presentPasswordModal((password) => {
|
|
||||||
this.previousPasswords.push(password);
|
|
||||||
handleResponseDecryption(response, keys, merge);
|
|
||||||
});
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (action.verb) {
|
|
||||||
case "get": {
|
|
||||||
this.alertManager.confirm({text: "Are you sure you want to replace the current note contents with this action's results?", onConfirm: () => {
|
|
||||||
this.httpManager.getAbsolute(action.url, {}, async (response) => {
|
|
||||||
action.error = false;
|
|
||||||
handleResponseDecryption(response, await this.authManager.keys(), true);
|
|
||||||
}, (response) => {
|
|
||||||
let error = (response && response.error) || {message: "An issue occurred while processing this action. Please try again."}
|
|
||||||
this.alertManager.alert({text: error.message});
|
|
||||||
action.error = true;
|
|
||||||
customCallback(null, error);
|
|
||||||
})
|
|
||||||
}})
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "render": {
|
|
||||||
this.httpManager.getAbsolute(action.url, {}, async (response) => {
|
|
||||||
action.error = false;
|
|
||||||
handleResponseDecryption(response, await this.authManager.keys(), false);
|
|
||||||
}, (response) => {
|
|
||||||
let error = (response && response.error) || {message: "An issue occurred while processing this action. Please try again."}
|
|
||||||
this.alertManager.alert({text: error.message});
|
|
||||||
action.error = true;
|
|
||||||
customCallback(null, error);
|
|
||||||
})
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "show": {
|
|
||||||
let win = window.open(action.url, '_blank');
|
|
||||||
if(win) {
|
|
||||||
win.focus();
|
|
||||||
}
|
|
||||||
customCallback();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "post": {
|
|
||||||
this.outgoingParamsForItem(item, extension, decrypted).then((itemParams) => {
|
|
||||||
var params = {
|
|
||||||
items: [itemParams] // Wrap it in an array
|
|
||||||
}
|
|
||||||
|
|
||||||
this.performPost(action, extension, params, (response) => {
|
|
||||||
if(response && response.error) {
|
|
||||||
this.alertManager.alert({text: "An issue occurred while processing this action. Please try again."});
|
|
||||||
}
|
|
||||||
customCallback(response);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
action.lastExecuted = new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
async outgoingParamsForItem(item, extension, decrypted = false) {
|
|
||||||
var keys = await this.authManager.keys();
|
|
||||||
if(decrypted) {
|
|
||||||
keys = null;
|
|
||||||
}
|
|
||||||
var itemParams = new SFItemParams(item, keys, await this.authManager.getAuthParams());
|
|
||||||
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"});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
presentRevisionPreviewModal(uuid, content) {
|
|
||||||
var scope = this.$rootScope.$new(true);
|
|
||||||
scope.uuid = uuid;
|
|
||||||
scope.content = content;
|
|
||||||
var el = this.$compile( "<revision-preview-modal uuid='uuid' content='content' class='sk-modal'></revision-preview-modal>" )(scope);
|
|
||||||
angular.element(document.body).append(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
presentPasswordModal(callback) {
|
|
||||||
var scope = this.$rootScope.$new(true);
|
|
||||||
scope.type = "password";
|
|
||||||
scope.title = "Decryption Assistance";
|
|
||||||
scope.message = "Unable to decrypt this item with your current keys. Please enter your account password at the time of this revision.";
|
|
||||||
scope.callback = callback;
|
|
||||||
var el = this.$compile( "<input-modal type='type' message='message' title='title' callback='callback'></input-modal>" )(scope);
|
|
||||||
angular.element(document.body).append(el);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { SFAlertManager } from 'snjs';
|
|
||||||
import { SKAlert } from 'sn-stylekit';
|
|
||||||
|
|
||||||
export class AlertManager extends SFAlertManager {
|
|
||||||
/* @ngInject */
|
|
||||||
constructor($timeout) {
|
|
||||||
super();
|
|
||||||
this.$timeout = $timeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
async alert({title, text, closeButtonText = "OK", onClose} = {}) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let buttons = [
|
|
||||||
{text: closeButtonText, style: "neutral", action: async () => {
|
|
||||||
if(onClose) {
|
|
||||||
this.$timeout(onClose);
|
|
||||||
}
|
|
||||||
resolve(true);
|
|
||||||
}}
|
|
||||||
]
|
|
||||||
let alert = new SKAlert({title, text, buttons});
|
|
||||||
alert.present();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async confirm({title, text, confirmButtonText = "Confirm", cancelButtonText = "Cancel", onConfirm, onCancel, destructive = false} = {}) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let buttons = [
|
|
||||||
{text: cancelButtonText, style: "neutral", action: async () => {
|
|
||||||
if(onCancel) {
|
|
||||||
this.$timeout(onCancel);
|
|
||||||
}
|
|
||||||
reject(false);
|
|
||||||
}},
|
|
||||||
{text: confirmButtonText, style: destructive ? "danger" : "info", action: async () => {
|
|
||||||
if(onConfirm) {
|
|
||||||
this.$timeout(onConfirm);
|
|
||||||
}
|
|
||||||
resolve(true);
|
|
||||||
}},
|
|
||||||
];
|
|
||||||
|
|
||||||
let alert = new SKAlert({title, text, buttons});
|
|
||||||
alert.present();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
28
app/assets/javascripts/controllers/abstract/pure_ctrl.js
Normal file
28
app/assets/javascripts/controllers/abstract/pure_ctrl.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export class PureCtrl {
|
||||||
|
constructor(
|
||||||
|
$timeout
|
||||||
|
) {
|
||||||
|
if(!$timeout) {
|
||||||
|
throw 'Invalid PureCtrl construction.';
|
||||||
|
}
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
this.state = {};
|
||||||
|
this.props = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async setState(state) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.state = Object.freeze(Object.assign({}, this.state, state));
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
initProps(props) {
|
||||||
|
if (Object.keys(this.props).length > 0) {
|
||||||
|
throw 'Already init-ed props.';
|
||||||
|
}
|
||||||
|
this.props = Object.freeze(Object.assign({}, this.props, props));
|
||||||
|
}
|
||||||
|
}
|
||||||
2
app/assets/javascripts/controllers/constants.js
Normal file
2
app/assets/javascripts/controllers/constants.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const PANEL_NAME_NOTES = 'notes';
|
||||||
|
export const PANEL_NAME_TAGS = 'tags';
|
||||||
1189
app/assets/javascripts/controllers/editor.js
Normal file
1189
app/assets/javascripts/controllers/editor.js
Normal file
File diff suppressed because it is too large
Load Diff
379
app/assets/javascripts/controllers/footer.js
Normal file
379
app/assets/javascripts/controllers/footer.js
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
import { PrivilegesManager } from '@/services/privilegesManager';
|
||||||
|
import template from '%/footer.pug';
|
||||||
|
import {
|
||||||
|
APP_STATE_EVENT_EDITOR_FOCUSED,
|
||||||
|
APP_STATE_EVENT_BEGAN_BACKUP_DOWNLOAD,
|
||||||
|
APP_STATE_EVENT_ENDED_BACKUP_DOWNLOAD
|
||||||
|
} from '@/state';
|
||||||
|
import {
|
||||||
|
STRING_GENERIC_SYNC_ERROR,
|
||||||
|
STRING_NEW_UPDATE_READY
|
||||||
|
} from '@/strings';
|
||||||
|
|
||||||
|
class FooterCtrl {
|
||||||
|
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$rootScope,
|
||||||
|
$timeout,
|
||||||
|
alertManager,
|
||||||
|
appState,
|
||||||
|
authManager,
|
||||||
|
componentManager,
|
||||||
|
modelManager,
|
||||||
|
nativeExtManager,
|
||||||
|
passcodeManager,
|
||||||
|
privilegesManager,
|
||||||
|
statusManager,
|
||||||
|
syncManager,
|
||||||
|
) {
|
||||||
|
this.$rootScope = $rootScope;
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
this.alertManager = alertManager;
|
||||||
|
this.appState = appState;
|
||||||
|
this.authManager = authManager;
|
||||||
|
this.componentManager = componentManager;
|
||||||
|
this.modelManager = modelManager;
|
||||||
|
this.nativeExtManager = nativeExtManager;
|
||||||
|
this.passcodeManager = passcodeManager;
|
||||||
|
this.privilegesManager = privilegesManager;
|
||||||
|
this.statusManager = statusManager;
|
||||||
|
this.syncManager = syncManager;
|
||||||
|
|
||||||
|
this.rooms = [];
|
||||||
|
this.themesWithIcons = [];
|
||||||
|
this.showSyncResolution = false;
|
||||||
|
|
||||||
|
this.addAppStateObserver();
|
||||||
|
this.updateOfflineStatus();
|
||||||
|
this.addSyncEventHandler();
|
||||||
|
this.findErrors();
|
||||||
|
this.registerMappingObservers();
|
||||||
|
this.registerComponentHandler();
|
||||||
|
this.addRootScopeListeners();
|
||||||
|
|
||||||
|
this.authManager.checkForSecurityUpdate().then((available) => {
|
||||||
|
this.securityUpdateAvailable = available;
|
||||||
|
})
|
||||||
|
this.statusManager.addStatusObserver((string) => {
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.arbitraryStatusMessage = string;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addRootScopeListeners() {
|
||||||
|
this.$rootScope.$on("security-update-status-changed", () => {
|
||||||
|
this.securityUpdateAvailable = this.authManager.securityUpdateAvailable;
|
||||||
|
})
|
||||||
|
this.$rootScope.$on("reload-ext-data", () => {
|
||||||
|
this.reloadExtendedData();
|
||||||
|
});
|
||||||
|
this.$rootScope.$on("new-update-available", () => {
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.onNewUpdateAvailable();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addAppStateObserver() {
|
||||||
|
this.appState.addObserver((eventName, data) => {
|
||||||
|
if(eventName === APP_STATE_EVENT_EDITOR_FOCUSED) {
|
||||||
|
this.closeAllRooms();
|
||||||
|
this.closeAccountMenu();
|
||||||
|
} else if(eventName === APP_STATE_EVENT_BEGAN_BACKUP_DOWNLOAD) {
|
||||||
|
this.backupStatus = this.statusManager.addStatusFromString(
|
||||||
|
"Saving local backup..."
|
||||||
|
);
|
||||||
|
} else if(eventName === APP_STATE_EVENT_ENDED_BACKUP_DOWNLOAD) {
|
||||||
|
if(data.success) {
|
||||||
|
this.backupStatus = this.statusManager.replaceStatusWithString(
|
||||||
|
this.backupStatus,
|
||||||
|
"Successfully saved backup."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.backupStatus = this.statusManager.replaceStatusWithString(
|
||||||
|
this.backupStatus,
|
||||||
|
"Unable to save local backup."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.backupStatus = this.statusManager.removeStatus(this.backupStatus);
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addSyncEventHandler() {
|
||||||
|
this.syncManager.addEventHandler((syncEvent, data) => {
|
||||||
|
this.$timeout(() => {
|
||||||
|
if(syncEvent === "local-data-loaded") {
|
||||||
|
if(this.offline && this.modelManager.noteCount() === 0) {
|
||||||
|
this.showAccountMenu = true;
|
||||||
|
}
|
||||||
|
} else if(syncEvent === "enter-out-of-sync") {
|
||||||
|
this.outOfSync = true;
|
||||||
|
} else if(syncEvent === "exit-out-of-sync") {
|
||||||
|
this.outOfSync = false;
|
||||||
|
} else if(syncEvent === 'sync:completed') {
|
||||||
|
this.syncUpdated();
|
||||||
|
this.findErrors();
|
||||||
|
this.updateOfflineStatus();
|
||||||
|
} else if(syncEvent === 'sync:error') {
|
||||||
|
this.findErrors();
|
||||||
|
this.updateOfflineStatus();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
registerMappingObservers() {
|
||||||
|
this.modelManager.addItemSyncObserver(
|
||||||
|
'room-bar',
|
||||||
|
'SN|Component',
|
||||||
|
(allItems, validItems, deletedItems, source) => {
|
||||||
|
this.rooms = this.modelManager.components.filter((candidate) => {
|
||||||
|
return candidate.area === 'rooms' && !candidate.deleted
|
||||||
|
});
|
||||||
|
if(this.queueExtReload) {
|
||||||
|
this.queueExtReload = false;
|
||||||
|
this.reloadExtendedData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.modelManager.addItemSyncObserver(
|
||||||
|
'footer-bar-themes',
|
||||||
|
'SN|Theme',
|
||||||
|
(allItems, validItems, deletedItems, source) => {
|
||||||
|
const themes = this.modelManager.validItemsForContentType('SN|Theme')
|
||||||
|
.filter((candidate) => {
|
||||||
|
return (
|
||||||
|
!candidate.deleted &&
|
||||||
|
candidate.content.package_info &&
|
||||||
|
candidate.content.package_info.dock_icon
|
||||||
|
);
|
||||||
|
}).sort((a, b) => {
|
||||||
|
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||||
|
});
|
||||||
|
const differ = themes.length !== this.themesWithIcons.length;
|
||||||
|
this.themesWithIcons = themes;
|
||||||
|
if(differ) {
|
||||||
|
this.reloadDockShortcuts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerComponentHandler() {
|
||||||
|
this.componentManager.registerHandler({
|
||||||
|
identifier: "roomBar",
|
||||||
|
areas: ["rooms", "modal"],
|
||||||
|
activationHandler: (component) => {},
|
||||||
|
actionHandler: (component, action, data) => {
|
||||||
|
if(action === "set-size") {
|
||||||
|
component.setLastSize(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focusHandler: (component, focused) => {
|
||||||
|
if(component.isEditor() && focused) {
|
||||||
|
this.closeAllRooms();
|
||||||
|
this.closeAccountMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadExtendedData() {
|
||||||
|
if(this.reloadInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.reloadInProgress = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reload consists of opening the extensions manager,
|
||||||
|
* then closing it after a short delay.
|
||||||
|
*/
|
||||||
|
const extWindow = this.rooms.find((room) => {
|
||||||
|
return room.package_info.identifier === this.nativeExtManager.extManagerId;
|
||||||
|
});
|
||||||
|
if(!extWindow) {
|
||||||
|
this.queueExtReload = true;
|
||||||
|
this.reloadInProgress = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.selectRoom(extWindow);
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.selectRoom(extWindow);
|
||||||
|
this.reloadInProgress = false;
|
||||||
|
this.$rootScope.$broadcast('ext-reload-complete');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUser() {
|
||||||
|
return this.authManager.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOfflineStatus() {
|
||||||
|
this.offline = this.authManager.offline();
|
||||||
|
}
|
||||||
|
|
||||||
|
openSecurityUpdate() {
|
||||||
|
this.authManager.presentPasswordWizard('upgrade-security');
|
||||||
|
}
|
||||||
|
|
||||||
|
findErrors() {
|
||||||
|
this.error = this.syncManager.syncStatus.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
accountMenuPressed() {
|
||||||
|
this.showAccountMenu = !this.showAccountMenu;
|
||||||
|
this.closeAllRooms();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSyncResolutionMenu = () => {
|
||||||
|
this.showSyncResolution = !this.showSyncResolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAccountMenu = () => {
|
||||||
|
this.showAccountMenu = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPasscode() {
|
||||||
|
return this.passcodeManager.hasPasscode();
|
||||||
|
}
|
||||||
|
|
||||||
|
lockApp() {
|
||||||
|
this.$rootScope.lockApplication();
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshData() {
|
||||||
|
this.isRefreshing = true;
|
||||||
|
this.syncManager.sync({
|
||||||
|
force: true,
|
||||||
|
performIntegrityCheck: true
|
||||||
|
}).then((response) => {
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.isRefreshing = false;
|
||||||
|
}, 200)
|
||||||
|
if(response && response.error) {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: STRING_GENERIC_SYNC_ERROR
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.syncUpdated();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
syncUpdated() {
|
||||||
|
this.lastSyncDate = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
onNewUpdateAvailable() {
|
||||||
|
this.newUpdateAvailable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
clickedNewUpdateAnnouncement() {
|
||||||
|
this.newUpdateAvailable = false;
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: STRING_NEW_UPDATE_READY
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadDockShortcuts() {
|
||||||
|
const shortcuts = [];
|
||||||
|
for(const theme of this.themesWithIcons) {
|
||||||
|
const name = theme.content.package_info.name;
|
||||||
|
const icon = theme.content.package_info.dock_icon;
|
||||||
|
if(!icon) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
shortcuts.push({
|
||||||
|
name: name,
|
||||||
|
component: theme,
|
||||||
|
icon: icon
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dockShortcuts = shortcuts.sort((a, b) => {
|
||||||
|
/** Circles first, then images */
|
||||||
|
const aType = a.icon.type;
|
||||||
|
const bType = b.icon.type;
|
||||||
|
if(aType === bType) {
|
||||||
|
return 0;
|
||||||
|
} else if(aType === 'circle' && bType === 'svg') {
|
||||||
|
return -1;
|
||||||
|
} else if(bType === 'circle' && aType === 'svg') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initSvgForShortcut(shortcut) {
|
||||||
|
const id = 'dock-svg-' + shortcut.component.uuid;
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const svg = shortcut.component.content.package_info.dock_icon.source;
|
||||||
|
const doc = parser.parseFromString(svg, 'image/svg+xml');
|
||||||
|
element.appendChild(doc.documentElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectShortcut(shortcut) {
|
||||||
|
this.componentManager.toggleComponent(shortcut.component);
|
||||||
|
}
|
||||||
|
|
||||||
|
onRoomDismiss(room) {
|
||||||
|
room.showRoom = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAllRooms() {
|
||||||
|
for(const room of this.rooms) {
|
||||||
|
room.showRoom = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectRoom(room) {
|
||||||
|
const run = () => {
|
||||||
|
this.$timeout(() => {
|
||||||
|
room.showRoom = !room.showRoom;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!room.showRoom) {
|
||||||
|
const requiresPrivilege = await this.privilegesManager.actionRequiresPrivilege(
|
||||||
|
PrivilegesManager.ActionManageExtensions
|
||||||
|
);
|
||||||
|
if(requiresPrivilege) {
|
||||||
|
this.privilegesManager.presentPrivilegesModal(
|
||||||
|
PrivilegesManager.ActionManageExtensions,
|
||||||
|
run
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clickOutsideAccountMenu() {
|
||||||
|
if(this.privilegesManager.authenticationInProgress()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.showAccountMenu = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Footer {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.scope = {};
|
||||||
|
this.template = template;
|
||||||
|
this.controller = FooterCtrl;
|
||||||
|
this.replace = true;
|
||||||
|
this.controllerAs = 'ctrl';
|
||||||
|
this.bindToController = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
app/assets/javascripts/controllers/index.js
Normal file
7
app/assets/javascripts/controllers/index.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export { PureCtrl } from './abstract/pure_ctrl';
|
||||||
|
export { EditorPanel } from './editor';
|
||||||
|
export { Footer } from './footer';
|
||||||
|
export { NotesPanel } from './notes/notes';
|
||||||
|
export { TagsPanel } from './tags';
|
||||||
|
export { Root } from './root';
|
||||||
|
export { LockScreen } from './lockScreen';
|
||||||
103
app/assets/javascripts/controllers/lockScreen.js
Normal file
103
app/assets/javascripts/controllers/lockScreen.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import template from '%/lock-screen.pug';
|
||||||
|
|
||||||
|
const ELEMENT_ID_PASSCODE_INPUT = 'passcode-input';
|
||||||
|
|
||||||
|
class LockScreenCtrl {
|
||||||
|
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$scope,
|
||||||
|
alertManager,
|
||||||
|
authManager,
|
||||||
|
passcodeManager,
|
||||||
|
) {
|
||||||
|
this.$scope = $scope;
|
||||||
|
this.alertManager = alertManager;
|
||||||
|
this.authManager = authManager;
|
||||||
|
this.passcodeManager = passcodeManager;
|
||||||
|
this.formData = {};
|
||||||
|
|
||||||
|
this.addVisibilityObserver();
|
||||||
|
this.addDestroyHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
get passcodeInput() {
|
||||||
|
return document.getElementById(
|
||||||
|
ELEMENT_ID_PASSCODE_INPUT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addDestroyHandler() {
|
||||||
|
this.$scope.$on('$destroy', () => {
|
||||||
|
this.passcodeManager.removeVisibilityObserver(
|
||||||
|
this.visibilityObserver
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addVisibilityObserver() {
|
||||||
|
this.visibilityObserver = this.passcodeManager
|
||||||
|
.addVisibilityObserver((visible) => {
|
||||||
|
if(visible) {
|
||||||
|
const input = this.passcodeInput;
|
||||||
|
if(input) {
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
submitPasscodeForm($event) {
|
||||||
|
if(
|
||||||
|
!this.formData.passcode ||
|
||||||
|
this.formData.passcode.length === 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.passcodeInput.blur();
|
||||||
|
this.passcodeManager.unlock(
|
||||||
|
this.formData.passcode,
|
||||||
|
(success) => {
|
||||||
|
if(!success) {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: "Invalid passcode. Please try again.",
|
||||||
|
onClose: () => {
|
||||||
|
this.passcodeInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.onSuccess()();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
forgotPasscode() {
|
||||||
|
this.formData.showRecovery = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
beginDeleteData() {
|
||||||
|
this.alertManager.confirm({
|
||||||
|
text: "Are you sure you want to clear all local data?",
|
||||||
|
destructive: true,
|
||||||
|
onConfirm: () => {
|
||||||
|
this.authManager.signout(true).then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LockScreen {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.template = template;
|
||||||
|
this.controller = LockScreenCtrl;
|
||||||
|
this.controllerAs = 'ctrl';
|
||||||
|
this.bindToController = true;
|
||||||
|
this.scope = {
|
||||||
|
onSuccess: '&',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
152
app/assets/javascripts/controllers/notes/note_utils.js
Normal file
152
app/assets/javascripts/controllers/notes/note_utils.js
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
export const SORT_KEY_CREATED_AT = 'created_at';
|
||||||
|
export const SORT_KEY_UPDATED_AT = 'updated_at';
|
||||||
|
export const SORT_KEY_CLIENT_UPDATED_AT = 'client_updated_at';
|
||||||
|
export const SORT_KEY_TITLE = 'title';
|
||||||
|
|
||||||
|
export function filterAndSortNotes({
|
||||||
|
notes,
|
||||||
|
selectedTag,
|
||||||
|
showArchived,
|
||||||
|
hidePinned,
|
||||||
|
filterText,
|
||||||
|
sortBy,
|
||||||
|
reverse
|
||||||
|
}) {
|
||||||
|
const filtered = filterNotes({
|
||||||
|
notes,
|
||||||
|
selectedTag,
|
||||||
|
showArchived,
|
||||||
|
hidePinned,
|
||||||
|
filterText,
|
||||||
|
});
|
||||||
|
const sorted = sortNotes({
|
||||||
|
notes: filtered,
|
||||||
|
sortBy,
|
||||||
|
reverse
|
||||||
|
})
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterNotes({
|
||||||
|
notes,
|
||||||
|
selectedTag,
|
||||||
|
showArchived,
|
||||||
|
hidePinned,
|
||||||
|
filterText
|
||||||
|
}) {
|
||||||
|
return notes.filter((note) => {
|
||||||
|
let canShowArchived = showArchived;
|
||||||
|
const canShowPinned = !hidePinned;
|
||||||
|
const isTrash = selectedTag.content.isTrashTag;
|
||||||
|
if (!isTrash && note.content.trashed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const isSmartTag = selectedTag.isSmartTag();
|
||||||
|
if (isSmartTag) {
|
||||||
|
canShowArchived = (
|
||||||
|
canShowArchived ||
|
||||||
|
selectedTag.content.isArchiveTag ||
|
||||||
|
isTrash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(note.archived && !canShowArchived) ||
|
||||||
|
(note.pinned && !canShowPinned)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return noteMatchesQuery({
|
||||||
|
note,
|
||||||
|
query: filterText
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function noteMatchesQuery({
|
||||||
|
note,
|
||||||
|
query
|
||||||
|
}) {
|
||||||
|
if(query.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const title = note.safeTitle().toLowerCase();
|
||||||
|
const text = note.safeText().toLowerCase();
|
||||||
|
const lowercaseText = query.toLowerCase();
|
||||||
|
|
||||||
|
const quotedText = stringBetweenQuotes(lowercaseText);
|
||||||
|
if(quotedText) {
|
||||||
|
return title.includes(quotedText) || text.includes(quotedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringIsUuid(lowercaseText)) {
|
||||||
|
return note.uuid === lowercaseText;
|
||||||
|
}
|
||||||
|
|
||||||
|
const words = lowercaseText.split(" ");
|
||||||
|
const matchesTitle = words.every((word) => {
|
||||||
|
return title.indexOf(word) >= 0;
|
||||||
|
});
|
||||||
|
const matchesBody = words.every((word) => {
|
||||||
|
return text.indexOf(word) >= 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return matchesTitle || matchesBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringBetweenQuotes(text) {
|
||||||
|
const matches = text.match(/"(.*?)"/);
|
||||||
|
return matches ? matches[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringIsUuid(text) {
|
||||||
|
const matches = text.match(
|
||||||
|
/\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/
|
||||||
|
);
|
||||||
|
return matches ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortNotes({
|
||||||
|
notes = [],
|
||||||
|
sortBy,
|
||||||
|
reverse
|
||||||
|
}) {
|
||||||
|
const sortValueFn = (a, b, pinCheck = false) => {
|
||||||
|
if (a.dummy) { return -1; }
|
||||||
|
if (b.dummy) { return 1; }
|
||||||
|
if (!pinCheck) {
|
||||||
|
if (a.pinned && b.pinned) {
|
||||||
|
return sortValueFn(a, b, true);
|
||||||
|
}
|
||||||
|
if (a.pinned) { return -1; }
|
||||||
|
if (b.pinned) { return 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
let aValue = a[sortBy] || '';
|
||||||
|
let bValue = b[sortBy] || '';
|
||||||
|
let vector = 1;
|
||||||
|
if (reverse) {
|
||||||
|
vector *= -1;
|
||||||
|
}
|
||||||
|
if (sortBy === SORT_KEY_TITLE) {
|
||||||
|
aValue = aValue.toLowerCase();
|
||||||
|
bValue = bValue.toLowerCase();
|
||||||
|
if (aValue.length === 0 && bValue.length === 0) {
|
||||||
|
return 0;
|
||||||
|
} else if (aValue.length === 0 && bValue.length !== 0) {
|
||||||
|
return 1 * vector;
|
||||||
|
} else if (aValue.length !== 0 && bValue.length === 0) {
|
||||||
|
return -1 * vector;
|
||||||
|
} else {
|
||||||
|
vector *= -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (aValue > bValue) { return -1 * vector; }
|
||||||
|
else if (aValue < bValue) { return 1 * vector; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = notes.sort(function (a, b) {
|
||||||
|
return sortValueFn(a, b);
|
||||||
|
})
|
||||||
|
return result;
|
||||||
|
}
|
||||||
722
app/assets/javascripts/controllers/notes/notes.js
Normal file
722
app/assets/javascripts/controllers/notes/notes.js
Normal file
@@ -0,0 +1,722 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import angular from 'angular';
|
||||||
|
import template from '%/notes.pug';
|
||||||
|
import { SFAuthManager } from 'snjs';
|
||||||
|
import { KeyboardManager } from '@/services/keyboardManager';
|
||||||
|
import { PureCtrl } from '@Controllers';
|
||||||
|
import {
|
||||||
|
APP_STATE_EVENT_NOTE_CHANGED,
|
||||||
|
APP_STATE_EVENT_TAG_CHANGED,
|
||||||
|
APP_STATE_EVENT_PREFERENCES_CHANGED,
|
||||||
|
APP_STATE_EVENT_EDITOR_FOCUSED
|
||||||
|
} from '@/state';
|
||||||
|
import {
|
||||||
|
PREF_NOTES_PANEL_WIDTH,
|
||||||
|
PREF_SORT_NOTES_BY,
|
||||||
|
PREF_SORT_NOTES_REVERSE,
|
||||||
|
PREF_NOTES_SHOW_ARCHIVED,
|
||||||
|
PREF_NOTES_HIDE_PINNED,
|
||||||
|
PREF_NOTES_HIDE_NOTE_PREVIEW,
|
||||||
|
PREF_NOTES_HIDE_DATE,
|
||||||
|
PREF_NOTES_HIDE_TAGS
|
||||||
|
} from '@/services/preferencesManager';
|
||||||
|
import {
|
||||||
|
PANEL_NAME_NOTES
|
||||||
|
} from '@/controllers/constants';
|
||||||
|
import {
|
||||||
|
SORT_KEY_CREATED_AT,
|
||||||
|
SORT_KEY_UPDATED_AT,
|
||||||
|
SORT_KEY_CLIENT_UPDATED_AT,
|
||||||
|
SORT_KEY_TITLE,
|
||||||
|
filterAndSortNotes
|
||||||
|
} from './note_utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the height of a note cell with nothing but the title,
|
||||||
|
* which *is* a display option
|
||||||
|
*/
|
||||||
|
const MIN_NOTE_CELL_HEIGHT = 51.0;
|
||||||
|
const DEFAULT_LIST_NUM_NOTES = 20;
|
||||||
|
|
||||||
|
|
||||||
|
const ELEMENT_ID_SEARCH_BAR = 'search-bar';
|
||||||
|
const ELEMENT_ID_SCROLL_CONTAINER = 'notes-scrollable';
|
||||||
|
|
||||||
|
class NotesCtrl extends PureCtrl {
|
||||||
|
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$timeout,
|
||||||
|
$rootScope,
|
||||||
|
appState,
|
||||||
|
authManager,
|
||||||
|
desktopManager,
|
||||||
|
keyboardManager,
|
||||||
|
modelManager,
|
||||||
|
preferencesManager,
|
||||||
|
privilegesManager,
|
||||||
|
syncManager,
|
||||||
|
) {
|
||||||
|
super($timeout);
|
||||||
|
this.$rootScope = $rootScope;
|
||||||
|
this.appState = appState;
|
||||||
|
this.authManager = authManager;
|
||||||
|
this.desktopManager = desktopManager;
|
||||||
|
this.keyboardManager = keyboardManager;
|
||||||
|
this.modelManager = modelManager;
|
||||||
|
this.preferencesManager = preferencesManager;
|
||||||
|
this.privilegesManager = privilegesManager;
|
||||||
|
this.syncManager = syncManager;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
notes: [],
|
||||||
|
renderedNotes: [],
|
||||||
|
selectedNote: null,
|
||||||
|
tag: null,
|
||||||
|
sortBy: null,
|
||||||
|
showArchived: null,
|
||||||
|
hidePinned: null,
|
||||||
|
sortReverse: null,
|
||||||
|
panelTitle: null,
|
||||||
|
mutable: { showMenu: false },
|
||||||
|
noteFilter: { text: '' },
|
||||||
|
}
|
||||||
|
|
||||||
|
this.panelController = {};
|
||||||
|
window.onresize = (event) => {
|
||||||
|
this.resetPagination({
|
||||||
|
keepCurrentIfLarger: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.addAppStateObserver();
|
||||||
|
this.addSignInObserver();
|
||||||
|
this.addSyncEventHandler();
|
||||||
|
this.addMappingObserver();
|
||||||
|
this.reloadPreferences();
|
||||||
|
this.resetPagination();
|
||||||
|
this.registerKeyboardShortcuts();
|
||||||
|
angular.element(document).ready(() => {
|
||||||
|
this.reloadPreferences();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addAppStateObserver() {
|
||||||
|
this.appState.addObserver((eventName, data) => {
|
||||||
|
if (eventName === APP_STATE_EVENT_TAG_CHANGED) {
|
||||||
|
this.handleTagChange(this.appState.getSelectedTag(), data.previousTag);
|
||||||
|
} else if (eventName === APP_STATE_EVENT_NOTE_CHANGED) {
|
||||||
|
this.handleNoteSelection(this.appState.getSelectedNote());
|
||||||
|
} else if (eventName === APP_STATE_EVENT_PREFERENCES_CHANGED) {
|
||||||
|
this.reloadPreferences();
|
||||||
|
this.reloadNotes();
|
||||||
|
} else if (eventName === APP_STATE_EVENT_EDITOR_FOCUSED) {
|
||||||
|
this.setShowMenuFalse();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addSignInObserver() {
|
||||||
|
this.authManager.addEventHandler((event) => {
|
||||||
|
if (event === SFAuthManager.DidSignInEvent) {
|
||||||
|
/** Delete dummy note if applicable */
|
||||||
|
if (this.state.selectedNote && this.state.selectedNote.dummy) {
|
||||||
|
this.modelManager.removeItemLocally(this.state.selectedNote);
|
||||||
|
this.selectNote(null).then(() => {
|
||||||
|
this.reloadNotes();
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* We want to see if the user will download any items from the server.
|
||||||
|
* If the next sync completes and our notes are still 0,
|
||||||
|
* we need to create a dummy.
|
||||||
|
*/
|
||||||
|
this.createDummyOnSynCompletionIfNoNotes = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addSyncEventHandler() {
|
||||||
|
this.syncManager.addEventHandler((syncEvent, data) => {
|
||||||
|
if (syncEvent === 'local-data-loaded') {
|
||||||
|
if (this.state.notes.length === 0) {
|
||||||
|
this.createNewNote();
|
||||||
|
}
|
||||||
|
} else if (syncEvent === 'sync:completed') {
|
||||||
|
if (this.createDummyOnSynCompletionIfNoNotes && this.state.notes.length === 0) {
|
||||||
|
this.createDummyOnSynCompletionIfNoNotes = false;
|
||||||
|
this.createNewNote();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addMappingObserver() {
|
||||||
|
this.modelManager.addItemSyncObserver(
|
||||||
|
'note-list',
|
||||||
|
'*',
|
||||||
|
async (allItems, validItems, deletedItems, source, sourceKey) => {
|
||||||
|
await this.reloadNotes();
|
||||||
|
const selectedNote = this.state.selectedNote;
|
||||||
|
if (selectedNote) {
|
||||||
|
const discarded = selectedNote.deleted || selectedNote.content.trashed;
|
||||||
|
const notIncluded = !this.state.notes.includes(selectedNote);
|
||||||
|
if (notIncluded || discarded) {
|
||||||
|
this.selectNextOrCreateNew();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.selectFirstNote();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Note has changed values, reset its flags */
|
||||||
|
const notes = allItems.filter((item) => item.content_type === 'Note');
|
||||||
|
for (const note of notes) {
|
||||||
|
this.loadFlagsForNote(note);
|
||||||
|
note.cachedCreatedAtString = note.createdAtString();
|
||||||
|
note.cachedUpdatedAtString = note.updatedAtString();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleTagChange(tag, previousTag) {
|
||||||
|
if (this.state.selectedNote && this.state.selectedNote.dummy) {
|
||||||
|
this.modelManager.removeItemLocally(this.state.selectedNote);
|
||||||
|
if (previousTag) {
|
||||||
|
_.remove(previousTag.notes, this.state.selectedNote);
|
||||||
|
}
|
||||||
|
await this.selectNote(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.setState({
|
||||||
|
tag: tag
|
||||||
|
});
|
||||||
|
|
||||||
|
this.resetScrollPosition();
|
||||||
|
this.setShowMenuFalse();
|
||||||
|
this.setNoteFilterText('');
|
||||||
|
this.desktopManager.searchText();
|
||||||
|
this.resetPagination();
|
||||||
|
|
||||||
|
await this.reloadNotes();
|
||||||
|
|
||||||
|
if (this.state.notes.length > 0) {
|
||||||
|
this.selectFirstNote();
|
||||||
|
} else if (this.syncManager.initialDataLoaded()) {
|
||||||
|
if (!tag.isSmartTag() || tag.content.isAllTag) {
|
||||||
|
this.createNewNote();
|
||||||
|
} else if (
|
||||||
|
this.state.selectedNote &&
|
||||||
|
!this.state.notes.includes(this.state.selectedNote)
|
||||||
|
) {
|
||||||
|
this.selectNote(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetScrollPosition() {
|
||||||
|
const scrollable = document.getElementById(ELEMENT_ID_SCROLL_CONTAINER);
|
||||||
|
if (scrollable) {
|
||||||
|
scrollable.scrollTop = 0;
|
||||||
|
scrollable.scrollLeft = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
async selectNote(note) {
|
||||||
|
this.appState.setSelectedNote(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeNoteFromList(note) {
|
||||||
|
const notes = this.state.notes;
|
||||||
|
_.pull(notes, note);
|
||||||
|
await this.setState({
|
||||||
|
notes: notes,
|
||||||
|
renderedNotes: notes.slice(0, this.notesToDisplay)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async reloadNotes() {
|
||||||
|
if (!this.state.tag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const notes = filterAndSortNotes({
|
||||||
|
notes: this.state.tag.notes,
|
||||||
|
selectedTag: this.state.tag,
|
||||||
|
showArchived: this.state.showArchived,
|
||||||
|
hidePinned: this.state.hidePinned,
|
||||||
|
filterText: this.state.noteFilter.text,
|
||||||
|
sortBy: this.state.sortBy,
|
||||||
|
reverse: this.state.sortReverse
|
||||||
|
});
|
||||||
|
for (const note of notes) {
|
||||||
|
if (note.errorDecrypting) {
|
||||||
|
this.loadFlagsForNote(note);
|
||||||
|
}
|
||||||
|
note.shouldShowTags = this.shouldShowTagsForNote(note);
|
||||||
|
}
|
||||||
|
await this.setState({
|
||||||
|
notes: notes,
|
||||||
|
renderedNotes: notes.slice(0, this.notesToDisplay)
|
||||||
|
});
|
||||||
|
this.reloadPanelTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowMenuFalse() {
|
||||||
|
this.setState({
|
||||||
|
mutable: {
|
||||||
|
...this.state.mutable,
|
||||||
|
showMenu: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleNoteSelection(note) {
|
||||||
|
if (this.state.selectedNote === note) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previousNote = this.state.selectedNote;
|
||||||
|
if (previousNote && previousNote.dummy) {
|
||||||
|
this.modelManager.removeItemLocally(previousNote);
|
||||||
|
this.removeNoteFromList(previousNote);
|
||||||
|
}
|
||||||
|
await this.setState({
|
||||||
|
selectedNote: note
|
||||||
|
})
|
||||||
|
if (!note) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedIndex = Math.max(0, this.displayableNotes().indexOf(note));
|
||||||
|
if (note.content.conflict_of) {
|
||||||
|
note.content.conflict_of = null;
|
||||||
|
this.modelManager.setItemDirty(note);
|
||||||
|
this.syncManager.sync();
|
||||||
|
}
|
||||||
|
if (this.isFiltering()) {
|
||||||
|
this.desktopManager.searchText(this.state.noteFilter.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadPreferences() {
|
||||||
|
const viewOptions = {};
|
||||||
|
const prevSortValue = this.state.sortBy;
|
||||||
|
let sortBy = this.preferencesManager.getValue(
|
||||||
|
PREF_SORT_NOTES_BY,
|
||||||
|
SORT_KEY_CREATED_AT
|
||||||
|
);
|
||||||
|
if (sortBy === SORT_KEY_UPDATED_AT) {
|
||||||
|
/** Use client_updated_at instead */
|
||||||
|
sortBy = SORT_KEY_CLIENT_UPDATED_AT;
|
||||||
|
}
|
||||||
|
viewOptions.sortBy = sortBy;
|
||||||
|
viewOptions.sortReverse = this.preferencesManager.getValue(
|
||||||
|
PREF_SORT_NOTES_REVERSE,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
viewOptions.showArchived = this.preferencesManager.getValue(
|
||||||
|
PREF_NOTES_SHOW_ARCHIVED,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
viewOptions.hidePinned = this.preferencesManager.getValue(
|
||||||
|
PREF_NOTES_HIDE_PINNED,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
viewOptions.hideNotePreview = this.preferencesManager.getValue(
|
||||||
|
PREF_NOTES_HIDE_NOTE_PREVIEW,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
viewOptions.hideDate = this.preferencesManager.getValue(
|
||||||
|
PREF_NOTES_HIDE_DATE,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
viewOptions.hideTags = this.preferencesManager.getValue(
|
||||||
|
PREF_NOTES_HIDE_TAGS,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
this.setState({
|
||||||
|
...viewOptions
|
||||||
|
});
|
||||||
|
if (prevSortValue && prevSortValue !== sortBy) {
|
||||||
|
this.selectFirstNote();
|
||||||
|
}
|
||||||
|
const width = this.preferencesManager.getValue(
|
||||||
|
PREF_NOTES_PANEL_WIDTH
|
||||||
|
);
|
||||||
|
if (width) {
|
||||||
|
this.panelController.setWidth(width);
|
||||||
|
if (this.panelController.isCollapsed()) {
|
||||||
|
this.appState.panelDidResize({
|
||||||
|
name: PANEL_NAME_NOTES,
|
||||||
|
collapsed: this.panelController.isCollapsed()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPanelResize = (newWidth, lastLeft, isAtMaxWidth, isCollapsed) => {
|
||||||
|
this.preferencesManager.setUserPrefValue(
|
||||||
|
PREF_NOTES_PANEL_WIDTH,
|
||||||
|
newWidth
|
||||||
|
);
|
||||||
|
this.preferencesManager.syncUserPreferences();
|
||||||
|
this.appState.panelDidResize({
|
||||||
|
name: PANEL_NAME_NOTES,
|
||||||
|
collapsed: isCollapsed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
paginate() {
|
||||||
|
this.notesToDisplay += this.pageSize;
|
||||||
|
this.reloadNotes();
|
||||||
|
if (this.searchSubmitted) {
|
||||||
|
this.desktopManager.searchText(this.state.noteFilter.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPagination({ keepCurrentIfLarger } = {}) {
|
||||||
|
const clientHeight = document.documentElement.clientHeight;
|
||||||
|
this.pageSize = clientHeight / MIN_NOTE_CELL_HEIGHT;
|
||||||
|
if (this.pageSize === 0) {
|
||||||
|
this.pageSize = DEFAULT_LIST_NUM_NOTES;
|
||||||
|
}
|
||||||
|
if (keepCurrentIfLarger && this.notesToDisplay > this.pageSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.notesToDisplay = this.pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadPanelTitle() {
|
||||||
|
let title;
|
||||||
|
if (this.isFiltering()) {
|
||||||
|
const resultCount = this.state.notes.length
|
||||||
|
title = `${resultCount} search results`;
|
||||||
|
} else if (this.state.tag) {
|
||||||
|
title = `${this.state.tag.title}`;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
panelTitle: title
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
optionsSubtitle() {
|
||||||
|
let base = "";
|
||||||
|
if (this.state.sortBy === 'created_at') {
|
||||||
|
base += " Date Added";
|
||||||
|
} else if (this.state.sortBy === 'client_updated_at') {
|
||||||
|
base += " Date Modified";
|
||||||
|
} else if (this.state.sortBy === 'title') {
|
||||||
|
base += " Title";
|
||||||
|
}
|
||||||
|
if (this.state.showArchived) {
|
||||||
|
base += " | + Archived"
|
||||||
|
}
|
||||||
|
if (this.state.hidePinned) {
|
||||||
|
base += " | – Pinned"
|
||||||
|
}
|
||||||
|
if (this.state.sortReverse) {
|
||||||
|
base += " | Reversed"
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFlagsForNote(note) {
|
||||||
|
const flags = [];
|
||||||
|
if (note.pinned) {
|
||||||
|
flags.push({
|
||||||
|
text: "Pinned",
|
||||||
|
class: 'info'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (note.archived) {
|
||||||
|
flags.push({
|
||||||
|
text: "Archived",
|
||||||
|
class: 'warning'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (note.content.protected) {
|
||||||
|
flags.push({
|
||||||
|
text: "Protected",
|
||||||
|
class: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (note.locked) {
|
||||||
|
flags.push({
|
||||||
|
text: "Locked",
|
||||||
|
class: 'neutral'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (note.content.trashed) {
|
||||||
|
flags.push({
|
||||||
|
text: "Deleted",
|
||||||
|
class: 'danger'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (note.content.conflict_of) {
|
||||||
|
flags.push({
|
||||||
|
text: "Conflicted Copy",
|
||||||
|
class: 'danger'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (note.errorDecrypting) {
|
||||||
|
flags.push({
|
||||||
|
text: "Missing Keys",
|
||||||
|
class: 'danger'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (note.deleted) {
|
||||||
|
flags.push({
|
||||||
|
text: "Deletion Pending Sync",
|
||||||
|
class: 'danger'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
note.flags = flags;
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayableNotes() {
|
||||||
|
return this.state.notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFirstNonProtectedNote() {
|
||||||
|
const displayableNotes = this.displayableNotes();
|
||||||
|
let index = 0;
|
||||||
|
let note = displayableNotes[index];
|
||||||
|
while (note && note.content.protected) {
|
||||||
|
index++;
|
||||||
|
if (index >= displayableNotes.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
note = displayableNotes[index];
|
||||||
|
}
|
||||||
|
return note;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectFirstNote() {
|
||||||
|
const note = this.getFirstNonProtectedNote();
|
||||||
|
if (note) {
|
||||||
|
this.selectNote(note);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectNextNote() {
|
||||||
|
const displayableNotes = this.displayableNotes();
|
||||||
|
const currentIndex = displayableNotes.indexOf(this.state.selectedNote);
|
||||||
|
if (currentIndex + 1 < displayableNotes.length) {
|
||||||
|
this.selectNote(displayableNotes[currentIndex + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectNextOrCreateNew() {
|
||||||
|
const note = this.getFirstNonProtectedNote();
|
||||||
|
if (note) {
|
||||||
|
this.selectNote(note);
|
||||||
|
} else if (!this.state.tag || !this.state.tag.isSmartTag()) {
|
||||||
|
this.createNewNote();
|
||||||
|
} else {
|
||||||
|
this.selectNote(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectPreviousNote() {
|
||||||
|
const displayableNotes = this.displayableNotes();
|
||||||
|
const currentIndex = displayableNotes.indexOf(this.state.selectedNote);
|
||||||
|
if (currentIndex - 1 >= 0) {
|
||||||
|
this.selectNote(displayableNotes[currentIndex - 1]);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createNewNote() {
|
||||||
|
if (this.state.selectedNote && this.state.selectedNote.dummy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const title = "Note" + (this.state.notes ? (" " + (this.state.notes.length + 1)) : "");
|
||||||
|
const newNote = this.modelManager.createItem({
|
||||||
|
content_type: 'Note',
|
||||||
|
content: {
|
||||||
|
text: '',
|
||||||
|
title: title
|
||||||
|
}
|
||||||
|
});
|
||||||
|
newNote.client_updated_at = new Date();
|
||||||
|
newNote.dummy = true;
|
||||||
|
this.modelManager.addItem(newNote);
|
||||||
|
this.modelManager.setItemDirty(newNote);
|
||||||
|
const selectedTag = this.appState.getSelectedTag();
|
||||||
|
if (!selectedTag.isSmartTag()) {
|
||||||
|
selectedTag.addItemAsRelationship(newNote);
|
||||||
|
this.modelManager.setItemDirty(selectedTag);
|
||||||
|
}
|
||||||
|
this.selectNote(newNote);
|
||||||
|
}
|
||||||
|
|
||||||
|
isFiltering() {
|
||||||
|
return this.state.noteFilter.text &&
|
||||||
|
this.state.noteFilter.text.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setNoteFilterText(text) {
|
||||||
|
await this.setState({
|
||||||
|
noteFilter: {
|
||||||
|
...this.state.noteFilter,
|
||||||
|
text: text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearFilterText() {
|
||||||
|
await this.setNoteFilterText('');
|
||||||
|
this.onFilterEnter();
|
||||||
|
this.filterTextChanged();
|
||||||
|
this.resetPagination();
|
||||||
|
}
|
||||||
|
|
||||||
|
async filterTextChanged() {
|
||||||
|
if (this.searchSubmitted) {
|
||||||
|
this.searchSubmitted = false;
|
||||||
|
}
|
||||||
|
await this.reloadNotes();
|
||||||
|
if (!this.state.notes.includes(this.state.selectedNote)) {
|
||||||
|
this.selectFirstNote();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterEnter() {
|
||||||
|
/**
|
||||||
|
* For Desktop, performing a search right away causes
|
||||||
|
* input to lose focus. We wait until user explicity hits
|
||||||
|
* enter before highlighting desktop search results.
|
||||||
|
*/
|
||||||
|
this.searchSubmitted = true;
|
||||||
|
this.desktopManager.searchText(this.state.noteFilter.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedMenuItem() {
|
||||||
|
this.setShowMenuFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePrefKey(key) {
|
||||||
|
this.preferencesManager.setUserPrefValue(key, !this.state[key]);
|
||||||
|
this.preferencesManager.syncUserPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedSortByCreated() {
|
||||||
|
this.setSortBy(SORT_KEY_CREATED_AT);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedSortByUpdated() {
|
||||||
|
this.setSortBy(SORT_KEY_CLIENT_UPDATED_AT);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedSortByTitle() {
|
||||||
|
this.setSortBy(SORT_KEY_TITLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleReverseSort() {
|
||||||
|
this.selectedMenuItem();
|
||||||
|
this.preferencesManager.setUserPrefValue(
|
||||||
|
PREF_SORT_NOTES_REVERSE,
|
||||||
|
!this.state.sortReverse
|
||||||
|
);
|
||||||
|
this.preferencesManager.syncUserPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
setSortBy(type) {
|
||||||
|
this.preferencesManager.setUserPrefValue(
|
||||||
|
PREF_SORT_NOTES_BY,
|
||||||
|
type
|
||||||
|
);
|
||||||
|
this.preferencesManager.syncUserPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldShowTagsForNote(note) {
|
||||||
|
if (this.state.hideTags || note.content.protected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.state.tag.content.isAllTag) {
|
||||||
|
return note.tags && note.tags.length > 0;
|
||||||
|
}
|
||||||
|
if (this.state.tag.isSmartTag()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Inside a tag, only show tags string if
|
||||||
|
* note contains tags other than this.state.tag
|
||||||
|
*/
|
||||||
|
return note.tags && note.tags.length > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearchBar() {
|
||||||
|
return document.getElementById(ELEMENT_ID_SEARCH_BAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerKeyboardShortcuts() {
|
||||||
|
/**
|
||||||
|
* In the browser we're not allowed to override cmd/ctrl + n, so we have to
|
||||||
|
* use Control modifier as well. These rules don't apply to desktop, but
|
||||||
|
* probably better to be consistent.
|
||||||
|
*/
|
||||||
|
this.newNoteKeyObserver = this.keyboardManager.addKeyObserver({
|
||||||
|
key: 'n',
|
||||||
|
modifiers: [
|
||||||
|
KeyboardManager.KeyModifierMeta,
|
||||||
|
KeyboardManager.KeyModifierCtrl
|
||||||
|
],
|
||||||
|
onKeyDown: (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.createNewNote();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.nextNoteKeyObserver = this.keyboardManager.addKeyObserver({
|
||||||
|
key: KeyboardManager.KeyDown,
|
||||||
|
elements: [
|
||||||
|
document.body,
|
||||||
|
this.getSearchBar()
|
||||||
|
],
|
||||||
|
onKeyDown: (event) => {
|
||||||
|
const searchBar = this.getSearchBar();
|
||||||
|
if (searchBar === document.activeElement) {
|
||||||
|
searchBar.blur()
|
||||||
|
}
|
||||||
|
this.selectNextNote();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.nextNoteKeyObserver = this.keyboardManager.addKeyObserver({
|
||||||
|
key: KeyboardManager.KeyUp,
|
||||||
|
element: document.body,
|
||||||
|
onKeyDown: (event) => {
|
||||||
|
this.selectPreviousNote();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.searchKeyObserver = this.keyboardManager.addKeyObserver({
|
||||||
|
key: "f",
|
||||||
|
modifiers: [
|
||||||
|
KeyboardManager.KeyModifierMeta,
|
||||||
|
KeyboardManager.KeyModifierShift
|
||||||
|
],
|
||||||
|
onKeyDown: (event) => {
|
||||||
|
const searchBar = this.getSearchBar();
|
||||||
|
if (searchBar) { searchBar.focus() };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotesPanel {
|
||||||
|
constructor() {
|
||||||
|
this.scope = {};
|
||||||
|
this.template = template;
|
||||||
|
this.replace = true;
|
||||||
|
this.controller = NotesCtrl;
|
||||||
|
this.controllerAs = 'self';
|
||||||
|
this.bindToController = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
339
app/assets/javascripts/controllers/root.js
Normal file
339
app/assets/javascripts/controllers/root.js
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import { SFAuthManager } from 'snjs';
|
||||||
|
import { getPlatformString } from '@/utils';
|
||||||
|
import template from '%/root.pug';
|
||||||
|
import {
|
||||||
|
APP_STATE_EVENT_PANEL_RESIZED
|
||||||
|
} from '@/state';
|
||||||
|
import {
|
||||||
|
PANEL_NAME_NOTES,
|
||||||
|
PANEL_NAME_TAGS
|
||||||
|
} from '@/controllers/constants';
|
||||||
|
import {
|
||||||
|
STRING_SESSION_EXPIRED,
|
||||||
|
STRING_DEFAULT_FILE_ERROR,
|
||||||
|
StringSyncException
|
||||||
|
} from '@/strings';
|
||||||
|
|
||||||
|
/** How often to automatically sync, in milliseconds */
|
||||||
|
const AUTO_SYNC_INTERVAL = 30000;
|
||||||
|
|
||||||
|
class RootCtrl {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$location,
|
||||||
|
$rootScope,
|
||||||
|
$scope,
|
||||||
|
$timeout,
|
||||||
|
alertManager,
|
||||||
|
appState,
|
||||||
|
authManager,
|
||||||
|
dbManager,
|
||||||
|
modelManager,
|
||||||
|
passcodeManager,
|
||||||
|
preferencesManager,
|
||||||
|
themeManager /** Unused below, required to load globally */,
|
||||||
|
statusManager,
|
||||||
|
storageManager,
|
||||||
|
syncManager,
|
||||||
|
) {
|
||||||
|
this.$rootScope = $rootScope;
|
||||||
|
this.$scope = $scope;
|
||||||
|
this.$location = $location;
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
this.dbManager = dbManager;
|
||||||
|
this.syncManager = syncManager;
|
||||||
|
this.statusManager = statusManager;
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
this.appState = appState;
|
||||||
|
this.authManager = authManager;
|
||||||
|
this.modelManager = modelManager;
|
||||||
|
this.alertManager = alertManager;
|
||||||
|
this.preferencesManager = preferencesManager;
|
||||||
|
this.passcodeManager = passcodeManager;
|
||||||
|
|
||||||
|
this.defineRootScopeFunctions();
|
||||||
|
this.handleAutoSignInFromParams();
|
||||||
|
this.initializeStorageManager();
|
||||||
|
this.addAppStateObserver();
|
||||||
|
this.addDragDropHandlers();
|
||||||
|
this.defaultLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineRootScopeFunctions() {
|
||||||
|
this.$rootScope.sync = () => {
|
||||||
|
this.syncManager.sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$rootScope.lockApplication = () => {
|
||||||
|
/** Reloading wipes current objects from memory */
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$rootScope.safeApply = (fn) => {
|
||||||
|
const phase = this.$scope.$root.$$phase;
|
||||||
|
if(phase === '$apply' || phase === '$digest') {
|
||||||
|
this.$scope.$eval(fn);
|
||||||
|
} else {
|
||||||
|
this.$scope.$apply(fn);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultLoad() {
|
||||||
|
this.$scope.platform = getPlatformString();
|
||||||
|
|
||||||
|
if(this.passcodeManager.isLocked()) {
|
||||||
|
this.$scope.needsUnlock = true;
|
||||||
|
} else {
|
||||||
|
this.loadAfterUnlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$scope.onSuccessfulUnlock = () => {
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.$scope.needsUnlock = false;
|
||||||
|
this.loadAfterUnlock();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$scope.onUpdateAvailable = () => {
|
||||||
|
this.$rootScope.$broadcast('new-update-available');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeStorageManager() {
|
||||||
|
this.storageManager.initialize(
|
||||||
|
this.passcodeManager.hasPasscode(),
|
||||||
|
this.authManager.isEphemeralSession()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addAppStateObserver() {
|
||||||
|
this.appState.addObserver((eventName, data) => {
|
||||||
|
if(eventName === APP_STATE_EVENT_PANEL_RESIZED) {
|
||||||
|
if(data.panel === PANEL_NAME_NOTES) {
|
||||||
|
this.notesCollapsed = data.collapsed;
|
||||||
|
}
|
||||||
|
if(data.panel === PANEL_NAME_TAGS) {
|
||||||
|
this.tagsCollapsed = data.collapsed;
|
||||||
|
}
|
||||||
|
let appClass = "";
|
||||||
|
if(this.notesCollapsed) { appClass += "collapsed-notes"; }
|
||||||
|
if(this.tagsCollapsed) { appClass += " collapsed-tags"; }
|
||||||
|
this.$scope.appClass = appClass;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAfterUnlock() {
|
||||||
|
this.openDatabase();
|
||||||
|
this.authManager.loadInitialData();
|
||||||
|
this.preferencesManager.load();
|
||||||
|
this.addSyncStatusObserver();
|
||||||
|
this.configureKeyRequestHandler();
|
||||||
|
this.addSyncEventHandler();
|
||||||
|
this.addSignOutObserver();
|
||||||
|
this.loadLocalData();
|
||||||
|
}
|
||||||
|
|
||||||
|
openDatabase() {
|
||||||
|
this.dbManager.setLocked(false);
|
||||||
|
this.dbManager.openDatabase({
|
||||||
|
onUpgradeNeeded: () => {
|
||||||
|
/**
|
||||||
|
* New database, delete syncToken so that items
|
||||||
|
* can be refetched entirely from server
|
||||||
|
*/
|
||||||
|
this.syncManager.clearSyncToken();
|
||||||
|
this.syncManager.sync();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addSyncStatusObserver() {
|
||||||
|
this.syncStatusObserver = this.syncManager.registerSyncStatusObserver((status) => {
|
||||||
|
if(status.retrievedCount > 20) {
|
||||||
|
const text = `Downloading ${status.retrievedCount} items. Keep app open.`
|
||||||
|
this.syncStatus = this.statusManager.replaceStatusWithString(
|
||||||
|
this.syncStatus,
|
||||||
|
text
|
||||||
|
);
|
||||||
|
this.showingDownloadStatus = true;
|
||||||
|
} else if(this.showingDownloadStatus) {
|
||||||
|
this.showingDownloadStatus = false;
|
||||||
|
const text = "Download Complete.";
|
||||||
|
this.syncStatus = this.statusManager.replaceStatusWithString(
|
||||||
|
this.syncStatus,
|
||||||
|
text
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.syncStatus = this.statusManager.removeStatus(this.syncStatus);
|
||||||
|
}, 2000);
|
||||||
|
} else if(status.total > 20) {
|
||||||
|
this.uploadSyncStatus = this.statusManager.replaceStatusWithString(
|
||||||
|
this.uploadSyncStatus,
|
||||||
|
`Syncing ${status.current}/${status.total} items...`
|
||||||
|
)
|
||||||
|
} else if(this.uploadSyncStatus) {
|
||||||
|
this.uploadSyncStatus = this.statusManager.removeStatus(
|
||||||
|
this.uploadSyncStatus
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
configureKeyRequestHandler() {
|
||||||
|
this.syncManager.setKeyRequestHandler(async () => {
|
||||||
|
const offline = this.authManager.offline();
|
||||||
|
const authParams = (
|
||||||
|
offline
|
||||||
|
? this.passcodeManager.passcodeAuthParams()
|
||||||
|
: await this.authManager.getAuthParams()
|
||||||
|
);
|
||||||
|
const keys = offline
|
||||||
|
? this.passcodeManager.keys()
|
||||||
|
: await this.authManager.keys();
|
||||||
|
return {
|
||||||
|
keys: keys,
|
||||||
|
offline: offline,
|
||||||
|
auth_params: authParams
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addSyncEventHandler() {
|
||||||
|
let lastShownDate;
|
||||||
|
this.syncManager.addEventHandler((syncEvent, data) => {
|
||||||
|
this.$rootScope.$broadcast(
|
||||||
|
syncEvent,
|
||||||
|
data || {}
|
||||||
|
);
|
||||||
|
if(syncEvent === 'sync-session-invalid') {
|
||||||
|
/** Don't show repeatedly; at most 30 seconds in between */
|
||||||
|
const SHOW_INTERVAL = 30;
|
||||||
|
const lastShownSeconds = (new Date() - lastShownDate) / 1000;
|
||||||
|
if(!lastShownDate || lastShownSeconds > SHOW_INTERVAL) {
|
||||||
|
lastShownDate = new Date();
|
||||||
|
setTimeout(() => {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: STRING_SESSION_EXPIRED
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
} else if(syncEvent === 'sync-exception') {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: StringSyncException(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadLocalData() {
|
||||||
|
const encryptionEnabled = this.authManager.user || this.passcodeManager.hasPasscode();
|
||||||
|
this.syncStatus = this.statusManager.addStatusFromString(
|
||||||
|
encryptionEnabled ? "Decrypting items..." : "Loading items..."
|
||||||
|
);
|
||||||
|
const incrementalCallback = (current, total) => {
|
||||||
|
const notesString = `${current}/${total} items...`
|
||||||
|
const status = encryptionEnabled
|
||||||
|
? `Decrypting ${notesString}`
|
||||||
|
: `Loading ${notesString}`;
|
||||||
|
this.syncStatus = this.statusManager.replaceStatusWithString(
|
||||||
|
this.syncStatus,
|
||||||
|
status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.syncManager.loadLocalItems({incrementalCallback}).then(() => {
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.$rootScope.$broadcast("initial-data-loaded");
|
||||||
|
this.syncStatus = this.statusManager.replaceStatusWithString(
|
||||||
|
this.syncStatus,
|
||||||
|
"Syncing..."
|
||||||
|
);
|
||||||
|
this.syncManager.sync({
|
||||||
|
performIntegrityCheck: true
|
||||||
|
}).then(() => {
|
||||||
|
this.syncStatus = this.statusManager.removeStatus(this.syncStatus);
|
||||||
|
})
|
||||||
|
setInterval(() => {
|
||||||
|
this.syncManager.sync();
|
||||||
|
}, AUTO_SYNC_INTERVAL);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addSignOutObserver() {
|
||||||
|
this.authManager.addEventHandler((event) => {
|
||||||
|
if(event === SFAuthManager.DidSignOutEvent) {
|
||||||
|
this.modelManager.handleSignout();
|
||||||
|
this.syncManager.handleSignout();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addDragDropHandlers() {
|
||||||
|
/**
|
||||||
|
* Disable dragging and dropping of files into main SN interface.
|
||||||
|
* both 'dragover' and 'drop' are required to prevent dropping of files.
|
||||||
|
* This will not prevent extensions from receiving drop events.
|
||||||
|
*/
|
||||||
|
window.addEventListener('dragover', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
window.addEventListener('drop', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: STRING_DEFAULT_FILE_ERROR
|
||||||
|
})
|
||||||
|
}, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAutoSignInFromParams() {
|
||||||
|
const urlParam = (key) => {
|
||||||
|
return this.$location.search()[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoSignInFromParams = async () => {
|
||||||
|
const server = urlParam('server');
|
||||||
|
const email = urlParam('email');
|
||||||
|
const pw = urlParam('pw');
|
||||||
|
if(!this.authManager.offline()) {
|
||||||
|
if(
|
||||||
|
await this.syncManager.getServerURL() === server
|
||||||
|
&& this.authManager.user.email === email
|
||||||
|
) {
|
||||||
|
/** Already signed in, return */
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
/** Sign out */
|
||||||
|
this.authManager.signout(true).then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.authManager.login(
|
||||||
|
server,
|
||||||
|
email,
|
||||||
|
pw,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
{}
|
||||||
|
).then((response) => {
|
||||||
|
window.location.reload();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(urlParam('server')) {
|
||||||
|
autoSignInFromParams();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Root {
|
||||||
|
constructor() {
|
||||||
|
this.template = template;
|
||||||
|
this.controller = RootCtrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
278
app/assets/javascripts/controllers/tags.js
Normal file
278
app/assets/javascripts/controllers/tags.js
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
import { SNNote, SNSmartTag } from 'snjs';
|
||||||
|
import template from '%/tags.pug';
|
||||||
|
import {
|
||||||
|
APP_STATE_EVENT_PREFERENCES_CHANGED,
|
||||||
|
APP_STATE_EVENT_TAG_CHANGED
|
||||||
|
} from '@/state';
|
||||||
|
import { PANEL_NAME_TAGS } from '@/controllers/constants';
|
||||||
|
import { PREF_TAGS_PANEL_WIDTH } from '@/services/preferencesManager';
|
||||||
|
import { STRING_DELETE_TAG } from '@/strings';
|
||||||
|
import { PureCtrl } from '@Controllers';
|
||||||
|
|
||||||
|
class TagsPanelCtrl extends PureCtrl {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$rootScope,
|
||||||
|
$timeout,
|
||||||
|
alertManager,
|
||||||
|
appState,
|
||||||
|
componentManager,
|
||||||
|
modelManager,
|
||||||
|
preferencesManager,
|
||||||
|
syncManager,
|
||||||
|
) {
|
||||||
|
super($timeout);
|
||||||
|
this.$rootScope = $rootScope;
|
||||||
|
this.alertManager = alertManager;
|
||||||
|
this.appState = appState;
|
||||||
|
this.componentManager = componentManager;
|
||||||
|
this.modelManager = modelManager;
|
||||||
|
this.preferencesManager = preferencesManager;
|
||||||
|
this.syncManager = syncManager;
|
||||||
|
this.panelController = {};
|
||||||
|
this.addSyncEventHandler();
|
||||||
|
this.addAppStateObserver();
|
||||||
|
this.addMappingObserver();
|
||||||
|
this.loadPreferences();
|
||||||
|
this.registerComponentHandler();
|
||||||
|
this.state = {
|
||||||
|
smartTags: this.modelManager.getSmartTags(),
|
||||||
|
noteCounts: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInit() {
|
||||||
|
this.selectTag(this.state.smartTags[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
addSyncEventHandler() {
|
||||||
|
this.syncManager.addEventHandler(async (syncEvent, data) => {
|
||||||
|
if (
|
||||||
|
syncEvent === 'local-data-loaded' ||
|
||||||
|
syncEvent === 'sync:completed' ||
|
||||||
|
syncEvent === 'local-data-incremental-load'
|
||||||
|
) {
|
||||||
|
await this.setState({
|
||||||
|
tags: this.modelManager.tags,
|
||||||
|
smartTags: this.modelManager.getSmartTags()
|
||||||
|
});
|
||||||
|
this.reloadNoteCounts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addAppStateObserver() {
|
||||||
|
this.appState.addObserver((eventName, data) => {
|
||||||
|
if (eventName === APP_STATE_EVENT_PREFERENCES_CHANGED) {
|
||||||
|
this.loadPreferences();
|
||||||
|
} else if (eventName === APP_STATE_EVENT_TAG_CHANGED) {
|
||||||
|
this.setState({
|
||||||
|
selectedTag: this.appState.getSelectedTag()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addMappingObserver() {
|
||||||
|
this.modelManager.addItemSyncObserver(
|
||||||
|
'tags-list-tags',
|
||||||
|
'Tag',
|
||||||
|
(allItems, validItems, deletedItems, source, sourceKey) => {
|
||||||
|
this.reloadNoteCounts();
|
||||||
|
|
||||||
|
if (!this.state.selectedTag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/** If the selected tag has been deleted, revert to All view. */
|
||||||
|
const selectedTag = allItems.find((tag) => {
|
||||||
|
return tag.uuid === this.state.selectedTag.uuid;
|
||||||
|
});
|
||||||
|
if (selectedTag && selectedTag.deleted) {
|
||||||
|
this.selectTag(this.state.smartTags[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadNoteCounts() {
|
||||||
|
let allTags = [];
|
||||||
|
if (this.state.tags) {
|
||||||
|
allTags = allTags.concat(this.state.tags);
|
||||||
|
}
|
||||||
|
if (this.state.smartTags) {
|
||||||
|
allTags = allTags.concat(this.state.smartTags);
|
||||||
|
}
|
||||||
|
const noteCounts = {};
|
||||||
|
for (const tag of allTags) {
|
||||||
|
const validNotes = SNNote.filterDummyNotes(tag.notes).filter((note) => {
|
||||||
|
return !note.archived && !note.content.trashed;
|
||||||
|
});
|
||||||
|
noteCounts[tag.uuid] = validNotes.length;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
noteCounts: noteCounts
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPreferences() {
|
||||||
|
const width = this.preferencesManager.getValue(PREF_TAGS_PANEL_WIDTH);
|
||||||
|
if (width) {
|
||||||
|
this.panelController.setWidth(width);
|
||||||
|
if (this.panelController.isCollapsed()) {
|
||||||
|
this.appState.panelDidResize({
|
||||||
|
name: PANEL_NAME_TAGS,
|
||||||
|
collapsed: this.panelController.isCollapsed()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPanelResize = (newWidth, lastLeft, isAtMaxWidth, isCollapsed) => {
|
||||||
|
this.preferencesManager.setUserPrefValue(
|
||||||
|
PREF_TAGS_PANEL_WIDTH,
|
||||||
|
newWidth,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
this.appState.panelDidResize({
|
||||||
|
name: PANEL_NAME_TAGS,
|
||||||
|
collapsed: isCollapsed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
registerComponentHandler() {
|
||||||
|
this.componentManager.registerHandler({
|
||||||
|
identifier: 'tags',
|
||||||
|
areas: ['tags-list'],
|
||||||
|
activationHandler: (component) => {
|
||||||
|
this.component = component;
|
||||||
|
},
|
||||||
|
contextRequestHandler: (component) => {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
actionHandler: (component, action, data) => {
|
||||||
|
if (action === 'select-item') {
|
||||||
|
if (data.item.content_type === 'Tag') {
|
||||||
|
const tag = this.modelManager.findItem(data.item.uuid);
|
||||||
|
if (tag) {
|
||||||
|
this.selectTag(tag);
|
||||||
|
}
|
||||||
|
} else if (data.item.content_type === 'SN|SmartTag') {
|
||||||
|
const smartTag = new SNSmartTag(data.item);
|
||||||
|
this.selectTag(smartTag);
|
||||||
|
}
|
||||||
|
} else if (action === 'clear-selection') {
|
||||||
|
this.selectTag(this.state.smartTags[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectTag(tag) {
|
||||||
|
if (tag.isSmartTag()) {
|
||||||
|
Object.defineProperty(tag, 'notes', {
|
||||||
|
get: () => {
|
||||||
|
return this.modelManager.notesMatchingSmartTag(tag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (tag.content.conflict_of) {
|
||||||
|
tag.content.conflict_of = null;
|
||||||
|
this.modelManager.setItemDirty(tag);
|
||||||
|
this.syncManager.sync();
|
||||||
|
}
|
||||||
|
this.appState.setSelectedTag(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
clickedAddNewTag() {
|
||||||
|
if (this.state.editingTag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newTag = this.modelManager.createItem({
|
||||||
|
content_type: 'Tag'
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
selectedTag: newTag,
|
||||||
|
editingTag: newTag,
|
||||||
|
newTag: newTag
|
||||||
|
});
|
||||||
|
this.modelManager.addItem(newTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
tagTitleDidChange(tag) {
|
||||||
|
this.setState({
|
||||||
|
editingTag: tag
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveTag($event, tag) {
|
||||||
|
$event.target.blur();
|
||||||
|
await this.setState({ editingTag: null });
|
||||||
|
if (!tag.title || tag.title.length === 0) {
|
||||||
|
if (this.editingOriginalName) {
|
||||||
|
tag.title = this.editingOriginalName;
|
||||||
|
this.editingOriginalName = null;
|
||||||
|
} else {
|
||||||
|
/** Newly created tag without content */
|
||||||
|
this.modelManager.removeItemLocally(tag);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingTag = this.modelManager.findTag(tag.title);
|
||||||
|
if (this.state.newTag === tag && matchingTag) {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: "A tag with this name already exists."
|
||||||
|
});
|
||||||
|
this.modelManager.removeItemLocally(tag);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modelManager.setItemDirty(tag);
|
||||||
|
this.syncManager.sync();
|
||||||
|
this.modelManager.resortTag(tag);
|
||||||
|
this.selectTag(tag);
|
||||||
|
this.setState({
|
||||||
|
newTag: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedRenameTag($event, tag) {
|
||||||
|
this.editingOriginalName = tag.title;
|
||||||
|
this.setState({
|
||||||
|
editingTag: tag
|
||||||
|
});
|
||||||
|
this.$timeout(() => {
|
||||||
|
document.getElementById('tag-' + tag.uuid).focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedDeleteTag(tag) {
|
||||||
|
this.removeTag(tag);
|
||||||
|
this.selectTag(this.state.smartTags[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTag(tag) {
|
||||||
|
this.alertManager.confirm({
|
||||||
|
text: STRING_DELETE_TAG,
|
||||||
|
destructive: true,
|
||||||
|
onConfirm: () => {
|
||||||
|
this.modelManager.setItemToBeDeleted(tag);
|
||||||
|
this.syncManager.sync().then(() => {
|
||||||
|
this.$rootScope.safeApply();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TagsPanel {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.scope = {};
|
||||||
|
this.template = template;
|
||||||
|
this.replace = true;
|
||||||
|
this.controller = TagsPanelCtrl;
|
||||||
|
this.controllerAs = 'self';
|
||||||
|
this.bindToController = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,8 @@
|
|||||||
export function infiniteScroll($rootScope, $window, $timeout) {
|
export function infiniteScroll($rootScope, $window, $timeout) {
|
||||||
return {
|
return {
|
||||||
link: function(scope, elem, attrs) {
|
link: function(scope, elem, attrs) {
|
||||||
var offset = parseInt(attrs.threshold) || 0;
|
const offset = parseInt(attrs.threshold) || 0;
|
||||||
var e = elem[0];
|
const e = elem[0];
|
||||||
|
|
||||||
elem.on('scroll', function() {
|
elem.on('scroll', function() {
|
||||||
if (
|
if (
|
||||||
scope.$eval(attrs.canLoad) &&
|
scope.$eval(attrs.canLoad) &&
|
||||||
@@ -5,7 +5,7 @@ export function selectOnClick($window) {
|
|||||||
link: function(scope, element, attrs) {
|
link: function(scope, element, attrs) {
|
||||||
element.on('focus', function() {
|
element.on('focus', function() {
|
||||||
if (!$window.getSelection().toString()) {
|
if (!$window.getSelection().toString()) {
|
||||||
// Required for mobile Safari
|
/** Required for mobile Safari */
|
||||||
this.setSelectionRange(0, this.value.length);
|
this.setSelectionRange(0, this.value.length);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
563
app/assets/javascripts/directives/views/accountMenu.js
Normal file
563
app/assets/javascripts/directives/views/accountMenu.js
Normal file
@@ -0,0 +1,563 @@
|
|||||||
|
import { isDesktopApplication, isNullOrUndefined } from '@/utils';
|
||||||
|
import { PrivilegesManager } from '@/services/privilegesManager';
|
||||||
|
import template from '%/directives/account-menu.pug';
|
||||||
|
import { protocolManager } from 'snjs';
|
||||||
|
import { PureCtrl } from '@Controllers';
|
||||||
|
import {
|
||||||
|
STRING_ACCOUNT_MENU_UNCHECK_MERGE,
|
||||||
|
STRING_SIGN_OUT_CONFIRMATION,
|
||||||
|
STRING_ERROR_DECRYPTING_IMPORT,
|
||||||
|
STRING_E2E_ENABLED,
|
||||||
|
STRING_LOCAL_ENC_ENABLED,
|
||||||
|
STRING_ENC_NOT_ENABLED,
|
||||||
|
STRING_IMPORT_SUCCESS,
|
||||||
|
STRING_REMOVE_PASSCODE_CONFIRMATION,
|
||||||
|
STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM,
|
||||||
|
STRING_NON_MATCHING_PASSCODES,
|
||||||
|
STRING_NON_MATCHING_PASSWORDS,
|
||||||
|
STRING_INVALID_IMPORT_FILE,
|
||||||
|
STRING_GENERATING_LOGIN_KEYS,
|
||||||
|
STRING_GENERATING_REGISTER_KEYS,
|
||||||
|
StringImportError
|
||||||
|
} from '@/strings';
|
||||||
|
|
||||||
|
const ELEMENT_ID_IMPORT_PASSWORD_INPUT = 'import-password-request';
|
||||||
|
|
||||||
|
class AccountMenuCtrl extends PureCtrl {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$scope,
|
||||||
|
$rootScope,
|
||||||
|
$timeout,
|
||||||
|
alertManager,
|
||||||
|
archiveManager,
|
||||||
|
appVersion,
|
||||||
|
authManager,
|
||||||
|
modelManager,
|
||||||
|
passcodeManager,
|
||||||
|
privilegesManager,
|
||||||
|
storageManager,
|
||||||
|
syncManager,
|
||||||
|
) {
|
||||||
|
super($timeout);
|
||||||
|
this.$scope = $scope;
|
||||||
|
this.$rootScope = $rootScope;
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
this.alertManager = alertManager;
|
||||||
|
this.archiveManager = archiveManager;
|
||||||
|
this.authManager = authManager;
|
||||||
|
this.modelManager = modelManager;
|
||||||
|
this.passcodeManager = passcodeManager;
|
||||||
|
this.privilegesManager = privilegesManager;
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
this.syncManager = syncManager;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
appVersion: 'v' + (window.electronAppVersion || appVersion),
|
||||||
|
user: this.authManager.user,
|
||||||
|
canAddPasscode: !this.authManager.isEphemeralSession(),
|
||||||
|
passcodeAutoLockOptions: this.passcodeManager.getAutoLockIntervalOptions(),
|
||||||
|
formData: {
|
||||||
|
mergeLocal: true,
|
||||||
|
ephemeral: false
|
||||||
|
},
|
||||||
|
mutable: {
|
||||||
|
backupEncrypted: this.encryptedBackupsAvailable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.syncStatus = this.syncManager.syncStatus;
|
||||||
|
this.syncManager.getServerURL().then((url) => {
|
||||||
|
this.setState({
|
||||||
|
server: url,
|
||||||
|
formData: { ...this.state.formData, url: url }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.authManager.checkForSecurityUpdate().then((available) => {
|
||||||
|
this.setState({
|
||||||
|
securityUpdateAvailable: available
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.reloadAutoLockInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInit() {
|
||||||
|
this.initProps({
|
||||||
|
closeFunction: this.closeFunction
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.props.closeFunction()();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedBackupsAvailable() {
|
||||||
|
return !isNullOrUndefined(this.authManager.user) || this.passcodeManager.hasPasscode();
|
||||||
|
}
|
||||||
|
|
||||||
|
submitMfaForm() {
|
||||||
|
const params = {
|
||||||
|
[this.state.formData.mfa.payload.mfa_key]: this.state.formData.userMfaCode
|
||||||
|
};
|
||||||
|
this.login(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
submitAuthForm() {
|
||||||
|
if (!this.state.formData.email || !this.state.formData.user_password) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.state.formData.showLogin) {
|
||||||
|
this.login();
|
||||||
|
} else {
|
||||||
|
this.register();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(extraParams) {
|
||||||
|
/** Prevent a timed sync from occuring while signing in. */
|
||||||
|
this.syncManager.lockSyncing();
|
||||||
|
this.state.formData.status = STRING_GENERATING_LOGIN_KEYS;
|
||||||
|
this.state.formData.authenticating = true;
|
||||||
|
const response = await this.authManager.login(
|
||||||
|
this.state.formData.url,
|
||||||
|
this.state.formData.email,
|
||||||
|
this.state.formData.user_password,
|
||||||
|
this.state.formData.ephemeral,
|
||||||
|
this.state.formData.strictSignin,
|
||||||
|
extraParams
|
||||||
|
);
|
||||||
|
const hasError = !response || response.error;
|
||||||
|
if (!hasError) {
|
||||||
|
await this.onAuthSuccess();
|
||||||
|
this.syncManager.unlockSyncing();
|
||||||
|
this.syncManager.sync({ performIntegrityCheck: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.syncManager.unlockSyncing();
|
||||||
|
this.state.formData.status = null;
|
||||||
|
const error = response
|
||||||
|
? response.error
|
||||||
|
: { message: "An unknown error occured." }
|
||||||
|
|
||||||
|
if (error.tag === 'mfa-required' || error.tag === 'mfa-invalid') {
|
||||||
|
this.state.formData.showLogin = false;
|
||||||
|
this.state.formData.mfa = error;
|
||||||
|
} else {
|
||||||
|
this.state.formData.showLogin = true;
|
||||||
|
this.state.formData.mfa = null;
|
||||||
|
if (error.message) {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.state.formData.authenticating = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async register() {
|
||||||
|
const confirmation = this.state.formData.password_conf;
|
||||||
|
if (confirmation !== this.state.formData.user_password) {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: STRING_NON_MATCHING_PASSWORDS
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.state.formData.confirmPassword = false;
|
||||||
|
this.state.formData.status = STRING_GENERATING_REGISTER_KEYS;
|
||||||
|
this.state.formData.authenticating = true;
|
||||||
|
const response = await this.authManager.register(
|
||||||
|
this.state.formData.url,
|
||||||
|
this.state.formData.email,
|
||||||
|
this.state.formData.user_password,
|
||||||
|
this.state.formData.ephemeral
|
||||||
|
)
|
||||||
|
if (!response || response.error) {
|
||||||
|
this.state.formData.status = null;
|
||||||
|
const error = response
|
||||||
|
? response.error
|
||||||
|
: { message: "An unknown error occured." };
|
||||||
|
this.state.formData.authenticating = false;
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: error.message
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await this.onAuthSuccess();
|
||||||
|
this.syncManager.sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeLocalChanged() {
|
||||||
|
if (!this.state.formData.mergeLocal) {
|
||||||
|
this.alertManager.confirm({
|
||||||
|
text: STRING_ACCOUNT_MENU_UNCHECK_MERGE,
|
||||||
|
destructive: true,
|
||||||
|
onCancel: () => {
|
||||||
|
this.state.formData.mergeLocal = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onAuthSuccess() {
|
||||||
|
if (this.state.formData.mergeLocal) {
|
||||||
|
this.$rootScope.$broadcast('major-data-change');
|
||||||
|
await this.clearDatabaseAndRewriteAllItems({ alternateUuids: true });
|
||||||
|
} else {
|
||||||
|
this.modelManager.removeAllItemsFromMemory();
|
||||||
|
await this.storageManager.clearAllModels();
|
||||||
|
}
|
||||||
|
this.state.formData.authenticating = false;
|
||||||
|
this.syncManager.refreshErroredItems();
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
openPasswordWizard(type) {
|
||||||
|
this.close();
|
||||||
|
this.authManager.presentPasswordWizard(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
async openPrivilegesModal() {
|
||||||
|
this.close();
|
||||||
|
const run = () => {
|
||||||
|
this.privilegesManager.presentPrivilegesManagementModal();
|
||||||
|
}
|
||||||
|
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
|
||||||
|
PrivilegesManager.ActionManagePrivileges
|
||||||
|
);
|
||||||
|
if (needsPrivilege) {
|
||||||
|
this.privilegesManager.presentPrivilegesModal(
|
||||||
|
PrivilegesManager.ActionManagePrivileges,
|
||||||
|
() => {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows IndexedDB unencrypted logs to be deleted
|
||||||
|
* `clearAllModels` will remove data from backing store,
|
||||||
|
* but not from working memory See:
|
||||||
|
* https://github.com/standardnotes/desktop/issues/131
|
||||||
|
*/
|
||||||
|
async clearDatabaseAndRewriteAllItems({ alternateUuids } = {}) {
|
||||||
|
await this.storageManager.clearAllModels();
|
||||||
|
await this.syncManager.markAllItemsDirtyAndSaveOffline(alternateUuids)
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyLocalData() {
|
||||||
|
this.alertManager.confirm({
|
||||||
|
text: STRING_SIGN_OUT_CONFIRMATION,
|
||||||
|
destructive: true,
|
||||||
|
onConfirm: async () => {
|
||||||
|
await this.authManager.signout(true);
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitImportPassword() {
|
||||||
|
await this.performImport(
|
||||||
|
this.state.importData.data,
|
||||||
|
this.state.importData.password
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async readFile(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function (e) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.target.result);
|
||||||
|
resolve(data);
|
||||||
|
} catch (e) {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: STRING_INVALID_IMPORT_FILE
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsText(file);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template
|
||||||
|
*/
|
||||||
|
async importFileSelected(files) {
|
||||||
|
const run = async () => {
|
||||||
|
const file = files[0];
|
||||||
|
const data = await this.readFile(file);
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.auth_params) {
|
||||||
|
await this.setState({
|
||||||
|
importData: {
|
||||||
|
...this.state.importData,
|
||||||
|
requestPassword: true,
|
||||||
|
data: data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const element = document.getElementById(
|
||||||
|
ELEMENT_ID_IMPORT_PASSWORD_INPUT
|
||||||
|
);
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.performImport(data, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
|
||||||
|
PrivilegesManager.ActionManageBackups
|
||||||
|
);
|
||||||
|
if (needsPrivilege) {
|
||||||
|
this.privilegesManager.presentPrivilegesModal(
|
||||||
|
PrivilegesManager.ActionManageBackups,
|
||||||
|
run
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async performImport(data, password) {
|
||||||
|
await this.setState({
|
||||||
|
importData: {
|
||||||
|
...this.state.importData,
|
||||||
|
loading: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const errorCount = await this.importJSONData(data, password);
|
||||||
|
this.setState({
|
||||||
|
importData: null
|
||||||
|
})
|
||||||
|
if (errorCount > 0) {
|
||||||
|
const message = StringImportError({ errorCount: errorCount })
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: message
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: STRING_IMPORT_SUCCESS
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async importJSONData(data, password) {
|
||||||
|
let errorCount = 0;
|
||||||
|
if (data.auth_params) {
|
||||||
|
const keys = await protocolManager.computeEncryptionKeysForUser(
|
||||||
|
password,
|
||||||
|
data.auth_params
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const throws = false;
|
||||||
|
await protocolManager.decryptMultipleItems(data.items, keys, throws);
|
||||||
|
const items = [];
|
||||||
|
for (const item of data.items) {
|
||||||
|
item.enc_item_key = null;
|
||||||
|
item.auth_hash = null;
|
||||||
|
if (item.errorDecrypting) {
|
||||||
|
errorCount++;
|
||||||
|
} else {
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.items = items;
|
||||||
|
} catch (e) {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: STRING_ERROR_DECRYPTING_IMPORT
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = await this.modelManager.importItems(data.items);
|
||||||
|
for (const item of items) {
|
||||||
|
/**
|
||||||
|
* Don't want to activate any components during import process in
|
||||||
|
* case of exceptions breaking up the import proccess
|
||||||
|
*/
|
||||||
|
if (item.content_type === 'SN|Component') {
|
||||||
|
item.active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.syncManager.sync();
|
||||||
|
return errorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadDataArchive() {
|
||||||
|
this.archiveManager.downloadBackup(this.state.mutable.backupEncrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
notesAndTagsCount() {
|
||||||
|
return this.modelManager.allItemsMatchingTypes([
|
||||||
|
'Note',
|
||||||
|
'Tag'
|
||||||
|
]).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptionStatusForNotes() {
|
||||||
|
const length = this.notesAndTagsCount();
|
||||||
|
return length + "/" + length + " notes and tags encrypted";
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptionEnabled() {
|
||||||
|
return this.passcodeManager.hasPasscode() || !this.authManager.offline();
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptionSource() {
|
||||||
|
if (!this.authManager.offline()) {
|
||||||
|
return "Account keys";
|
||||||
|
} else if (this.passcodeManager.hasPasscode()) {
|
||||||
|
return "Local Passcode";
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptionStatusString() {
|
||||||
|
if (!this.authManager.offline()) {
|
||||||
|
return STRING_E2E_ENABLED;
|
||||||
|
} else if (this.passcodeManager.hasPasscode()) {
|
||||||
|
return STRING_LOCAL_ENC_ENABLED;
|
||||||
|
} else {
|
||||||
|
return STRING_ENC_NOT_ENABLED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async reloadAutoLockInterval() {
|
||||||
|
const interval = await this.passcodeManager.getAutoLockInterval();
|
||||||
|
this.setState({
|
||||||
|
selectedAutoLockInterval: interval
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectAutoLockInterval(interval) {
|
||||||
|
const run = async () => {
|
||||||
|
await this.passcodeManager.setAutoLockInterval(interval);
|
||||||
|
this.reloadAutoLockInterval();
|
||||||
|
}
|
||||||
|
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
|
||||||
|
PrivilegesManager.ActionManagePasscode
|
||||||
|
);
|
||||||
|
if (needsPrivilege) {
|
||||||
|
this.privilegesManager.presentPrivilegesModal(
|
||||||
|
PrivilegesManager.ActionManagePasscode,
|
||||||
|
() => {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPasscode() {
|
||||||
|
return this.passcodeManager.hasPasscode();
|
||||||
|
}
|
||||||
|
|
||||||
|
addPasscodeClicked() {
|
||||||
|
this.state.formData.showPasscodeForm = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitPasscodeForm() {
|
||||||
|
const passcode = this.state.formData.passcode;
|
||||||
|
if (passcode !== this.state.formData.confirmPasscode) {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: STRING_NON_MATCHING_PASSCODES
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const func = this.state.formData.changingPasscode
|
||||||
|
? this.passcodeManager.changePasscode.bind(this.passcodeManager)
|
||||||
|
: this.passcodeManager.setPasscode.bind(this.passcodeManager);
|
||||||
|
func(passcode, async () => {
|
||||||
|
this.setState({
|
||||||
|
formData: {
|
||||||
|
...this.state.formData,
|
||||||
|
passcode: null,
|
||||||
|
confirmPasscode: null,
|
||||||
|
showPasscodeForm: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (await this.authManager.offline()) {
|
||||||
|
this.$rootScope.$broadcast('major-data-change');
|
||||||
|
this.clearDatabaseAndRewriteAllItems();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async changePasscodePressed() {
|
||||||
|
const run = () => {
|
||||||
|
this.state.formData.changingPasscode = true;
|
||||||
|
this.addPasscodeClicked();
|
||||||
|
}
|
||||||
|
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
|
||||||
|
PrivilegesManager.ActionManagePasscode
|
||||||
|
);
|
||||||
|
if (needsPrivilege) {
|
||||||
|
this.privilegesManager.presentPrivilegesModal(
|
||||||
|
PrivilegesManager.ActionManagePasscode,
|
||||||
|
run
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removePasscodePressed() {
|
||||||
|
const run = () => {
|
||||||
|
const signedIn = !this.authManager.offline();
|
||||||
|
let message = STRING_REMOVE_PASSCODE_CONFIRMATION;
|
||||||
|
if (!signedIn) {
|
||||||
|
message += STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM;
|
||||||
|
}
|
||||||
|
this.alertManager.confirm({
|
||||||
|
text: message,
|
||||||
|
destructive: true,
|
||||||
|
onConfirm: () => {
|
||||||
|
this.passcodeManager.clearPasscode();
|
||||||
|
if (this.authManager.offline()) {
|
||||||
|
this.syncManager.markAllItemsDirtyAndSaveOffline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
|
||||||
|
PrivilegesManager.ActionManagePasscode
|
||||||
|
);
|
||||||
|
if (needsPrivilege) {
|
||||||
|
this.privilegesManager.presentPrivilegesModal(
|
||||||
|
PrivilegesManager.ActionManagePasscode,
|
||||||
|
run
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isDesktopApplication() {
|
||||||
|
return isDesktopApplication();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AccountMenu {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.template = template;
|
||||||
|
this.controller = AccountMenuCtrl;
|
||||||
|
this.controllerAs = 'self';
|
||||||
|
this.bindToController = true;
|
||||||
|
this.scope = {
|
||||||
|
closeFunction: '&'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
104
app/assets/javascripts/directives/views/actionsMenu.js
Normal file
104
app/assets/javascripts/directives/views/actionsMenu.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import template from '%/directives/actions-menu.pug';
|
||||||
|
import { PureCtrl } from '@Controllers';
|
||||||
|
|
||||||
|
class ActionsMenuCtrl extends PureCtrl {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$scope,
|
||||||
|
$timeout,
|
||||||
|
actionsManager,
|
||||||
|
) {
|
||||||
|
super($timeout);
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
this.actionsManager = actionsManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInit() {
|
||||||
|
this.initProps({
|
||||||
|
item: this.item
|
||||||
|
})
|
||||||
|
this.loadExtensions();
|
||||||
|
};
|
||||||
|
|
||||||
|
async loadExtensions() {
|
||||||
|
const extensions = this.actionsManager.extensions.sort((a, b) => {
|
||||||
|
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||||
|
});
|
||||||
|
for (const extension of extensions) {
|
||||||
|
extension.loading = true;
|
||||||
|
await this.actionsManager.loadExtensionInContextOfItem(extension, this.props.item);
|
||||||
|
extension.loading = false;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
extensions: extensions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeAction(action, extension) {
|
||||||
|
if (action.verb === 'nested') {
|
||||||
|
if (!action.subrows) {
|
||||||
|
action.subrows = this.subRowsForAction(action, extension);
|
||||||
|
} else {
|
||||||
|
action.subrows = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
action.running = true;
|
||||||
|
const result = await this.actionsManager.executeAction(
|
||||||
|
action,
|
||||||
|
extension,
|
||||||
|
this.props.item
|
||||||
|
);
|
||||||
|
if (action.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
action.running = false;
|
||||||
|
this.handleActionResult(action, result);
|
||||||
|
await this.actionsManager.loadExtensionInContextOfItem(extension, this.props.item);
|
||||||
|
this.setState({
|
||||||
|
extensions: this.state.extensions
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleActionResult(action, result) {
|
||||||
|
switch (action.verb) {
|
||||||
|
case 'render': {
|
||||||
|
const item = result.item;
|
||||||
|
this.actionsManager.presentRevisionPreviewModal(
|
||||||
|
item.uuid,
|
||||||
|
item.content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subRowsForAction(parentAction, extension) {
|
||||||
|
if (!parentAction.subactions) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parentAction.subactions.map((subaction) => {
|
||||||
|
return {
|
||||||
|
onClick: () => {
|
||||||
|
this.executeAction(subaction, extension, parentAction);
|
||||||
|
},
|
||||||
|
label: subaction.label,
|
||||||
|
subtitle: subaction.desc,
|
||||||
|
spinnerClass: subaction.running ? 'info' : null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ActionsMenu {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.template = template;
|
||||||
|
this.replace = true;
|
||||||
|
this.controller = ActionsMenuCtrl;
|
||||||
|
this.controllerAs = 'self';
|
||||||
|
this.bindToController = true;
|
||||||
|
this.scope = {
|
||||||
|
item: '='
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
34
app/assets/javascripts/directives/views/componentModal.js
Normal file
34
app/assets/javascripts/directives/views/componentModal.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import template from '%/directives/component-modal.pug';
|
||||||
|
|
||||||
|
export class ComponentModalCtrl {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor($scope, $element) {
|
||||||
|
this.$element = $element;
|
||||||
|
this.$scope = $scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss(callback) {
|
||||||
|
this.$element.remove();
|
||||||
|
this.$scope.$destroy();
|
||||||
|
if(this.onDismiss && this.onDismiss()) {
|
||||||
|
this.onDismiss()(this.component)
|
||||||
|
}
|
||||||
|
callback && callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ComponentModal {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.template = template;
|
||||||
|
this.controller = ComponentModalCtrl;
|
||||||
|
this.controllerAs = 'ctrl';
|
||||||
|
this.bindToController = true;
|
||||||
|
this.scope = {
|
||||||
|
show: '=',
|
||||||
|
component: '=',
|
||||||
|
callback: '=',
|
||||||
|
onDismiss: '&'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
276
app/assets/javascripts/directives/views/componentView.js
Normal file
276
app/assets/javascripts/directives/views/componentView.js
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import template from '%/directives/component-view.pug';
|
||||||
|
import { isDesktopApplication } from '../../utils';
|
||||||
|
/**
|
||||||
|
* The maximum amount of time we'll wait for a component
|
||||||
|
* to load before displaying error
|
||||||
|
*/
|
||||||
|
const MAX_LOAD_THRESHOLD = 4000;
|
||||||
|
|
||||||
|
const VISIBILITY_CHANGE_LISTENER_KEY = 'visibilitychange';
|
||||||
|
|
||||||
|
class ComponentViewCtrl {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$scope,
|
||||||
|
$rootScope,
|
||||||
|
$timeout,
|
||||||
|
componentManager,
|
||||||
|
desktopManager,
|
||||||
|
themeManager
|
||||||
|
) {
|
||||||
|
this.$rootScope = $rootScope;
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
this.themeManager = themeManager;
|
||||||
|
this.desktopManager = desktopManager;
|
||||||
|
this.componentManager = componentManager;
|
||||||
|
this.componentValid = true;
|
||||||
|
|
||||||
|
$scope.$watch('ctrl.component', (component, prevComponent) => {
|
||||||
|
this.componentValueDidSet(component, prevComponent);
|
||||||
|
});
|
||||||
|
$scope.$on('ext-reload-complete', () => {
|
||||||
|
this.reloadStatus(false);
|
||||||
|
})
|
||||||
|
$scope.$on('$destroy', () => {
|
||||||
|
this.destroy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInit() {
|
||||||
|
this.registerComponentHandlers();
|
||||||
|
this.registerPackageUpdateObserver();
|
||||||
|
};
|
||||||
|
|
||||||
|
registerPackageUpdateObserver() {
|
||||||
|
this.updateObserver = this.desktopManager
|
||||||
|
.registerUpdateObserver((component) => {
|
||||||
|
if(component === this.component && component.active) {
|
||||||
|
this.reloadComponent();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
registerComponentHandlers() {
|
||||||
|
this.themeHandlerIdentifier = 'component-view-' + Math.random();
|
||||||
|
this.componentManager.registerHandler({
|
||||||
|
identifier: this.themeHandlerIdentifier,
|
||||||
|
areas: ['themes'],
|
||||||
|
activationHandler: (component) => {
|
||||||
|
this.reloadThemeStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.identifier = 'component-view-' + Math.random();
|
||||||
|
this.componentManager.registerHandler({
|
||||||
|
identifier: this.identifier,
|
||||||
|
areas: [this.component.area],
|
||||||
|
activationHandler: (component) => {
|
||||||
|
if(component !== this.component) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.handleActivation();
|
||||||
|
})
|
||||||
|
},
|
||||||
|
actionHandler: (component, action, data) => {
|
||||||
|
if(action === 'set-size') {
|
||||||
|
this.componentManager.handleSetSizeEvent(component, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibilityChange() {
|
||||||
|
if(document.visibilityState === 'hidden') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(this.issueLoading) {
|
||||||
|
this.reloadComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async reloadComponent() {
|
||||||
|
this.componentValid = false;
|
||||||
|
await this.componentManager.reloadComponent(this.component);
|
||||||
|
this.reloadStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadStatus(doManualReload = true) {
|
||||||
|
this.reloading = true;
|
||||||
|
const component = this.component;
|
||||||
|
const previouslyValid = this.componentValid;
|
||||||
|
const offlineRestricted = component.offlineOnly && !isDesktopApplication();
|
||||||
|
const hasUrlError = function(){
|
||||||
|
if(isDesktopApplication()) {
|
||||||
|
return !component.local_url && !component.hasValidHostedUrl();
|
||||||
|
} else {
|
||||||
|
return !component.hasValidHostedUrl();
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
this.expired = component.valid_until && component.valid_until <= new Date();
|
||||||
|
if(!component.lockReadonly) {
|
||||||
|
component.readonly = this.expired;
|
||||||
|
}
|
||||||
|
this.componentValid = !offlineRestricted && !hasUrlError;
|
||||||
|
if(!this.componentValid) {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
if(offlineRestricted) {
|
||||||
|
this.error = 'offline-restricted'
|
||||||
|
} else if(hasUrlError) {
|
||||||
|
this.error = 'url-missing'
|
||||||
|
} else {
|
||||||
|
this.error = null;
|
||||||
|
}
|
||||||
|
if(this.componentValid !== previouslyValid) {
|
||||||
|
if(this.componentValid) {
|
||||||
|
this.componentManager.reloadComponent(component, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(this.expired && doManualReload) {
|
||||||
|
this.$rootScope.$broadcast('reload-ext-dat');
|
||||||
|
}
|
||||||
|
this.reloadThemeStatus();
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.reloading = false;
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleActivation() {
|
||||||
|
if(!this.component.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const iframe = this.componentManager.iframeForComponent(
|
||||||
|
this.component
|
||||||
|
);
|
||||||
|
if(!iframe) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
if(this.loadTimeout) {
|
||||||
|
this.$timeout.cancel(this.loadTimeout);
|
||||||
|
}
|
||||||
|
this.loadTimeout = this.$timeout(() => {
|
||||||
|
this.handleIframeLoadTimeout();
|
||||||
|
}, MAX_LOAD_THRESHOLD);
|
||||||
|
|
||||||
|
iframe.onload = (event) => {
|
||||||
|
this.handleIframeLoad(iframe);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleIframeLoadTimeout() {
|
||||||
|
if(this.loading) {
|
||||||
|
this.loading = false;
|
||||||
|
this.issueLoading = true;
|
||||||
|
if(!this.didAttemptReload) {
|
||||||
|
this.didAttemptReload = true;
|
||||||
|
this.reloadComponent();
|
||||||
|
} else {
|
||||||
|
document.addEventListener(
|
||||||
|
VISIBILITY_CHANGE_LISTENER_KEY,
|
||||||
|
this.onVisibilityChange.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleIframeLoad(iframe) {
|
||||||
|
let desktopError = false;
|
||||||
|
if(isDesktopApplication()) {
|
||||||
|
try {
|
||||||
|
/** Accessing iframe.contentWindow.origin only allowed in desktop app. */
|
||||||
|
if(!iframe.contentWindow.origin || iframe.contentWindow.origin === 'null') {
|
||||||
|
desktopError = true;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
this.$timeout.cancel(this.loadTimeout);
|
||||||
|
await this.componentManager.registerComponentWindow(
|
||||||
|
this.component,
|
||||||
|
iframe.contentWindow
|
||||||
|
);
|
||||||
|
const avoidFlickerTimeout = 7;
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.loading = false;
|
||||||
|
this.issueLoading = desktopError ? true : false;
|
||||||
|
this.onLoad && this.onLoad(this.component);
|
||||||
|
}, avoidFlickerTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentValueDidSet(component, prevComponent) {
|
||||||
|
const dontSync = true;
|
||||||
|
if(prevComponent && component !== prevComponent) {
|
||||||
|
this.componentManager.deactivateComponent(
|
||||||
|
prevComponent,
|
||||||
|
dontSync
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if(component) {
|
||||||
|
this.componentManager.activateComponent(
|
||||||
|
component,
|
||||||
|
dontSync
|
||||||
|
);
|
||||||
|
this.reloadStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadThemeStatus() {
|
||||||
|
if(this.component.acceptsThemes()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(this.themeManager.hasActiveTheme()) {
|
||||||
|
if(!this.dismissedNoThemesMessage) {
|
||||||
|
this.showNoThemesMessage = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.showNoThemesMessage = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dismissNoThemesMessage() {
|
||||||
|
this.showNoThemesMessage = false;
|
||||||
|
this.dismissedNoThemesMessage = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
disableActiveTheme() {
|
||||||
|
this.themeManager.deactivateAllThemes();
|
||||||
|
this.dismissNoThemesMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
getUrl() {
|
||||||
|
const url = this.componentManager.urlForComponent(this.component);
|
||||||
|
this.component.runningLocally = (url === this.component.local_url);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.componentManager.deregisterHandler(this.themeHandlerIdentifier);
|
||||||
|
this.componentManager.deregisterHandler(this.identifier);
|
||||||
|
if(this.component && !this.manualDealloc) {
|
||||||
|
const dontSync = true;
|
||||||
|
this.componentManager.deactivateComponent(this.component, dontSync);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.desktopManager.deregisterUpdateObserver(this.updateObserver);
|
||||||
|
document.removeEventListener(
|
||||||
|
VISIBILITY_CHANGE_LISTENER_KEY,
|
||||||
|
this.onVisibilityChange.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ComponentView {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.template = template;
|
||||||
|
this.scope = {
|
||||||
|
component: '=',
|
||||||
|
onLoad: '=?',
|
||||||
|
manualDealloc: '=?'
|
||||||
|
};
|
||||||
|
this.controller = ComponentViewCtrl;
|
||||||
|
this.controllerAs = 'ctrl';
|
||||||
|
this.bindToController = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import template from '%/directives/conflict-resolution-modal.pug';
|
||||||
|
|
||||||
|
class ConflictResolutionCtrl {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$element,
|
||||||
|
alertManager,
|
||||||
|
archiveManager,
|
||||||
|
modelManager,
|
||||||
|
syncManager
|
||||||
|
) {
|
||||||
|
this.$element = $element;
|
||||||
|
this.alertManager = alertManager;
|
||||||
|
this.archiveManager = archiveManager;
|
||||||
|
this.modelManager = modelManager;
|
||||||
|
this.syncManager = syncManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInit() {
|
||||||
|
this.contentType = this.item1.content_type;
|
||||||
|
this.item1Content = this.createContentString(this.item1);
|
||||||
|
this.item2Content = this.createContentString(this.item2);
|
||||||
|
};
|
||||||
|
|
||||||
|
createContentString(item) {
|
||||||
|
const data = Object.assign({
|
||||||
|
created_at: item.created_at,
|
||||||
|
updated_at: item.updated_at
|
||||||
|
}, item.content);
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
keepItem1() {
|
||||||
|
this.alertManager.confirm({
|
||||||
|
text: `Are you sure you want to delete the item on the right?`,
|
||||||
|
destructive: true,
|
||||||
|
onConfirm: () => {
|
||||||
|
this.modelManager.setItemToBeDeleted(this.item2);
|
||||||
|
this.syncManager.sync().then(() => {
|
||||||
|
this.applyCallback();
|
||||||
|
})
|
||||||
|
this.dismiss();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
keepItem2() {
|
||||||
|
this.alertManager.confirm({
|
||||||
|
text: `Are you sure you want to delete the item on the left?`,
|
||||||
|
destructive: true,
|
||||||
|
onConfirm: () => {
|
||||||
|
this.modelManager.setItemToBeDeleted(this.item1);
|
||||||
|
this.syncManager.sync().then(() => {
|
||||||
|
this.applyCallback();
|
||||||
|
})
|
||||||
|
this.dismiss();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
keepBoth() {
|
||||||
|
this.applyCallback();
|
||||||
|
this.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
export() {
|
||||||
|
this.archiveManager.downloadBackupOfItems(
|
||||||
|
[this.item1, this.item2],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyCallback() {
|
||||||
|
this.callback && this.callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss() {
|
||||||
|
this.$element.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConflictResolutionModal {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.template = template;
|
||||||
|
this.controller = ConflictResolutionCtrl;
|
||||||
|
this.controllerAs = 'ctrl';
|
||||||
|
this.bindToController = true;
|
||||||
|
this.scope = {
|
||||||
|
item1: '=',
|
||||||
|
item2: '=',
|
||||||
|
callback: '='
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
110
app/assets/javascripts/directives/views/editorMenu.js
Normal file
110
app/assets/javascripts/directives/views/editorMenu.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { isDesktopApplication } from '@/utils';
|
||||||
|
import template from '%/directives/editor-menu.pug';
|
||||||
|
import { PureCtrl } from '@Controllers';
|
||||||
|
|
||||||
|
class EditorMenuCtrl extends PureCtrl {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$timeout,
|
||||||
|
componentManager,
|
||||||
|
modelManager,
|
||||||
|
syncManager,
|
||||||
|
) {
|
||||||
|
super($timeout);
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
this.componentManager = componentManager;
|
||||||
|
this.modelManager = modelManager;
|
||||||
|
this.syncManager = syncManager;
|
||||||
|
this.state = {
|
||||||
|
isDesktop: isDesktopApplication()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInit() {
|
||||||
|
const editors = this.componentManager.componentsForArea('editor-editor')
|
||||||
|
.sort((a, b) => {
|
||||||
|
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||||
|
});
|
||||||
|
const defaultEditor = editors.filter((e) => e.isDefaultEditor())[0];
|
||||||
|
this.setState({
|
||||||
|
editors: editors,
|
||||||
|
defaultEditor: defaultEditor
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
selectComponent(component) {
|
||||||
|
if(component) {
|
||||||
|
if(component.content.conflict_of) {
|
||||||
|
component.content.conflict_of = null;
|
||||||
|
this.modelManager.setItemDirty(component, true);
|
||||||
|
this.syncManager.sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.callback()(component);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDefaultForEditor(editor) {
|
||||||
|
if(this.defaultEditor === editor) {
|
||||||
|
this.removeEditorDefault(editor);
|
||||||
|
} else {
|
||||||
|
this.makeEditorDefault(editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offlineAvailableForComponent(component) {
|
||||||
|
return component.local_url && this.state.isDesktop;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeEditorDefault(component) {
|
||||||
|
const currentDefault = this.componentManager
|
||||||
|
.componentsForArea('editor-editor')
|
||||||
|
.filter((e) => e.isDefaultEditor())[0];
|
||||||
|
if(currentDefault) {
|
||||||
|
currentDefault.setAppDataItem('defaultEditor', false);
|
||||||
|
this.modelManager.setItemDirty(currentDefault);
|
||||||
|
}
|
||||||
|
component.setAppDataItem('defaultEditor', true);
|
||||||
|
this.modelManager.setItemDirty(component);
|
||||||
|
this.syncManager.sync();
|
||||||
|
this.setState({
|
||||||
|
defaultEditor: component
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEditorDefault(component) {
|
||||||
|
component.setAppDataItem('defaultEditor', false);
|
||||||
|
this.modelManager.setItemDirty(component);
|
||||||
|
this.syncManager.sync();
|
||||||
|
this.setState({
|
||||||
|
defaultEditor: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldDisplayRunningLocallyLabel(component) {
|
||||||
|
if(!component.runningLocally) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(component === this.selectedEditor) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EditorMenu {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.template = template;
|
||||||
|
this.controller = EditorMenuCtrl;
|
||||||
|
this.controllerAs = 'self';
|
||||||
|
this.bindToController = true;
|
||||||
|
this.scope = {
|
||||||
|
callback: '&',
|
||||||
|
selectedEditor: '=',
|
||||||
|
currentItem: '='
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/assets/javascripts/directives/views/inputModal.js
Normal file
37
app/assets/javascripts/directives/views/inputModal.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import template from '%/directives/input-modal.pug';
|
||||||
|
|
||||||
|
class InputModalCtrl {
|
||||||
|
|
||||||
|
/* @ngInject */
|
||||||
|
constructor($scope, $element) {
|
||||||
|
this.$element = $element;
|
||||||
|
this.formData = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss() {
|
||||||
|
this.$element.remove();
|
||||||
|
this.$scope.$destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.callback()(this.formData.input);
|
||||||
|
this.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InputModal {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.template = template;
|
||||||
|
this.controller = InputModalCtrl;
|
||||||
|
this.controllerAs = 'ctrl';
|
||||||
|
this.bindToController = true;
|
||||||
|
this.scope = {
|
||||||
|
type: '=',
|
||||||
|
title: '=',
|
||||||
|
message: '=',
|
||||||
|
placeholder: '=',
|
||||||
|
callback: '&'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,46 +1,48 @@
|
|||||||
import template from '%/directives/menu-row.pug';
|
import template from '%/directives/menu-row.pug';
|
||||||
|
|
||||||
|
class MenuRowCtrl {
|
||||||
|
|
||||||
|
onClick($event) {
|
||||||
|
if(this.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$event.stopPropagation();
|
||||||
|
this.action();
|
||||||
|
}
|
||||||
|
|
||||||
|
clickAccessoryButton($event) {
|
||||||
|
if(this.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$event.stopPropagation();
|
||||||
|
this.buttonAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class MenuRow {
|
export class MenuRow {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.restrict = 'E';
|
this.restrict = 'E';
|
||||||
this.transclude = true;
|
this.transclude = true;
|
||||||
this.template = template;
|
this.template = template;
|
||||||
|
this.controller = MenuRowCtrl;
|
||||||
|
this.controllerAs = 'ctrl';
|
||||||
|
this.bindToController = true;
|
||||||
this.scope = {
|
this.scope = {
|
||||||
action: '&',
|
action: '&',
|
||||||
circle: '=',
|
|
||||||
circleAlign: '=',
|
|
||||||
label: '=',
|
|
||||||
subtitle: '=',
|
|
||||||
hasButton: '=',
|
|
||||||
buttonText: '=',
|
|
||||||
buttonClass: '=',
|
|
||||||
buttonAction: '&',
|
buttonAction: '&',
|
||||||
spinnerClass: '=',
|
buttonClass: '=',
|
||||||
subRows: '=',
|
buttonText: '=',
|
||||||
faded: '=',
|
|
||||||
desc: '=',
|
desc: '=',
|
||||||
disabled: '=',
|
disabled: '=',
|
||||||
stylekitClass: '='
|
circle: '=',
|
||||||
|
circleAlign: '=',
|
||||||
|
faded: '=',
|
||||||
|
hasButton: '=',
|
||||||
|
label: '=',
|
||||||
|
spinnerClass: '=',
|
||||||
|
stylekitClass: '=',
|
||||||
|
subRows: '=',
|
||||||
|
subtitle: '=',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
controller($scope, componentManager) {
|
|
||||||
$scope.onClick = function($event) {
|
|
||||||
if($scope.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$event.stopPropagation();
|
|
||||||
$scope.action();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is for the accessory button
|
|
||||||
$scope.clickButton = function($event) {
|
|
||||||
if($scope.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$event.stopPropagation();
|
|
||||||
$scope.buttonAction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
339
app/assets/javascripts/directives/views/panelResizer.js
Normal file
339
app/assets/javascripts/directives/views/panelResizer.js
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
import angular from 'angular';
|
||||||
|
import template from '%/directives/panel-resizer.pug';
|
||||||
|
import { debounce } from '@/utils';
|
||||||
|
|
||||||
|
const PANEL_SIDE_RIGHT = 'right';
|
||||||
|
const PANEL_SIDE_LEFT = 'left';
|
||||||
|
|
||||||
|
const MOUSE_EVENT_MOVE = 'mousemove';
|
||||||
|
const MOUSE_EVENT_DOWN = 'mousedown';
|
||||||
|
const MOUSE_EVENT_UP = 'mouseup';
|
||||||
|
|
||||||
|
const WINDOW_EVENT_RESIZE = 'resize';
|
||||||
|
|
||||||
|
const PANEL_CSS_CLASS_HOVERABLE = 'hoverable';
|
||||||
|
const PANEL_CSS_CLASS_ALWAYS_VISIBLE = 'always-visible';
|
||||||
|
const PANEL_CSS_CLASS_DRAGGING = 'dragging';
|
||||||
|
const PANEL_CSS_CLASS_NO_SELECTION = 'no-selection';
|
||||||
|
const PANEL_CSS_CLASS_COLLAPSED = 'collapsed';
|
||||||
|
const PANEL_CSS_CLASS_ANIMATE_OPACITY = 'animate-opacity';
|
||||||
|
|
||||||
|
class PanelResizerCtrl {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$compile,
|
||||||
|
$element,
|
||||||
|
$scope,
|
||||||
|
$timeout,
|
||||||
|
) {
|
||||||
|
this.$compile = $compile;
|
||||||
|
this.$element = $element;
|
||||||
|
this.$scope = $scope;
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInit() {
|
||||||
|
this.configureControl();
|
||||||
|
this.configureDefaults();
|
||||||
|
this.addDoubleClickHandler();
|
||||||
|
this.reloadDefaultValues();
|
||||||
|
this.addMouseDownListener();
|
||||||
|
this.addMouseMoveListener();
|
||||||
|
this.addMouseUpListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
configureControl() {
|
||||||
|
this.control.setWidth = (value) => {
|
||||||
|
this.setWidth(value, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.control.setLeft = (value) => {
|
||||||
|
this.setLeft(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.control.flash = () => {
|
||||||
|
this.flash();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.control.isCollapsed = () => {
|
||||||
|
return this.isCollapsed();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
configureDefaults() {
|
||||||
|
this.panel = document.getElementById(this.panelId);
|
||||||
|
if (!this.panel) {
|
||||||
|
console.error('Panel not found for', this.panelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resizerColumn = this.$element[0];
|
||||||
|
this.currentMinWidth = this.minWidth || this.resizerColumn.offsetWidth;
|
||||||
|
this.pressed = false;
|
||||||
|
this.startWidth = this.panel.scrollWidth;
|
||||||
|
this.lastDownX = 0;
|
||||||
|
this.collapsed = false;
|
||||||
|
this.lastWidth = this.startWidth;
|
||||||
|
this.startLeft = this.panel.offsetLeft;
|
||||||
|
this.lastLeft = this.startLeft;
|
||||||
|
this.appFrame = null;
|
||||||
|
this.widthBeforeLastDblClick = 0;
|
||||||
|
|
||||||
|
if (this.property === PANEL_SIDE_RIGHT) {
|
||||||
|
this.configureRightPanel();
|
||||||
|
}
|
||||||
|
if (this.alwaysVisible) {
|
||||||
|
this.resizerColumn.classList.add(PANEL_CSS_CLASS_ALWAYS_VISIBLE);
|
||||||
|
}
|
||||||
|
if (this.hoverable) {
|
||||||
|
this.resizerColumn.classList.add(PANEL_CSS_CLASS_HOVERABLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configureRightPanel() {
|
||||||
|
const handleResize = debounce(event => {
|
||||||
|
this.reloadDefaultValues();
|
||||||
|
this.handleWidthEvent();
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.finishSettingWidth();
|
||||||
|
});
|
||||||
|
}, 250);
|
||||||
|
window.addEventListener(WINDOW_EVENT_RESIZE, handleResize);
|
||||||
|
this.$scope.$on('$destroy', () => {
|
||||||
|
window.removeEventListener(WINDOW_EVENT_RESIZE, handleResize);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getParentRect() {
|
||||||
|
return this.panel.parentNode.getBoundingClientRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadDefaultValues() {
|
||||||
|
this.startWidth = this.isAtMaxWidth()
|
||||||
|
? this.getParentRect().width
|
||||||
|
: this.panel.scrollWidth;
|
||||||
|
this.lastWidth = this.startWidth;
|
||||||
|
this.appFrame = document.getElementById('app').getBoundingClientRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
addDoubleClickHandler() {
|
||||||
|
this.resizerColumn.ondblclick = () => {
|
||||||
|
this.$timeout(() => {
|
||||||
|
const preClickCollapseState = this.isCollapsed();
|
||||||
|
if (preClickCollapseState) {
|
||||||
|
this.setWidth(this.widthBeforeLastDblClick || this.defaultWidth);
|
||||||
|
} else {
|
||||||
|
this.widthBeforeLastDblClick = this.lastWidth;
|
||||||
|
this.setWidth(this.currentMinWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.finishSettingWidth();
|
||||||
|
|
||||||
|
const newCollapseState = !preClickCollapseState;
|
||||||
|
this.onResizeFinish()(
|
||||||
|
this.lastWidth,
|
||||||
|
this.lastLeft,
|
||||||
|
this.isAtMaxWidth(),
|
||||||
|
newCollapseState
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
addMouseDownListener() {
|
||||||
|
this.resizerColumn.addEventListener(MOUSE_EVENT_DOWN, (event) => {
|
||||||
|
this.addInvisibleOverlay();
|
||||||
|
this.pressed = true;
|
||||||
|
this.lastDownX = event.clientX;
|
||||||
|
this.startWidth = this.panel.scrollWidth;
|
||||||
|
this.startLeft = this.panel.offsetLeft;
|
||||||
|
this.panel.classList.add(PANEL_CSS_CLASS_NO_SELECTION);
|
||||||
|
if (this.hoverable) {
|
||||||
|
this.resizerColumn.classList.add(PANEL_CSS_CLASS_DRAGGING);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addMouseMoveListener() {
|
||||||
|
document.addEventListener(MOUSE_EVENT_MOVE, (event) => {
|
||||||
|
if (!this.pressed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.property && this.property === PANEL_SIDE_LEFT) {
|
||||||
|
this.handleLeftEvent(event);
|
||||||
|
} else {
|
||||||
|
this.handleWidthEvent(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWidthEvent(event) {
|
||||||
|
let x;
|
||||||
|
if (event) {
|
||||||
|
x = event.clientX;
|
||||||
|
} else {
|
||||||
|
/** Coming from resize event */
|
||||||
|
x = 0;
|
||||||
|
this.lastDownX = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaX = x - this.lastDownX;
|
||||||
|
const newWidth = this.startWidth + deltaX;
|
||||||
|
this.setWidth(newWidth, false);
|
||||||
|
if (this.onResize()) {
|
||||||
|
this.onResize()(this.lastWidth, this.panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLeftEvent(event) {
|
||||||
|
const panelRect = this.panel.getBoundingClientRect();
|
||||||
|
const x = event.clientX || panelRect.x;
|
||||||
|
let deltaX = x - this.lastDownX;
|
||||||
|
let newLeft = this.startLeft + deltaX;
|
||||||
|
if (newLeft < 0) {
|
||||||
|
newLeft = 0;
|
||||||
|
deltaX = -this.startLeft;
|
||||||
|
}
|
||||||
|
const parentRect = this.getParentRect();
|
||||||
|
let newWidth = this.startWidth - deltaX;
|
||||||
|
if (newWidth < this.currentMinWidth) {
|
||||||
|
newWidth = this.currentMinWidth;
|
||||||
|
}
|
||||||
|
if (newWidth > parentRect.width) {
|
||||||
|
newWidth = parentRect.width;
|
||||||
|
}
|
||||||
|
if (newLeft + newWidth > parentRect.width) {
|
||||||
|
newLeft = parentRect.width - newWidth;
|
||||||
|
}
|
||||||
|
this.setLeft(newLeft, false);
|
||||||
|
this.setWidth(newWidth, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
addMouseUpListener() {
|
||||||
|
document.addEventListener(MOUSE_EVENT_UP, event => {
|
||||||
|
this.removeInvisibleOverlay();
|
||||||
|
if (this.pressed) {
|
||||||
|
this.pressed = false;
|
||||||
|
this.resizerColumn.classList.remove(PANEL_CSS_CLASS_DRAGGING);
|
||||||
|
this.panel.classList.remove(PANEL_CSS_CLASS_NO_SELECTION);
|
||||||
|
const isMaxWidth = this.isAtMaxWidth();
|
||||||
|
if (this.onResizeFinish) {
|
||||||
|
this.onResizeFinish()(
|
||||||
|
this.lastWidth,
|
||||||
|
this.lastLeft,
|
||||||
|
isMaxWidth,
|
||||||
|
this.isCollapsed()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.finishSettingWidth();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isAtMaxWidth() {
|
||||||
|
return (
|
||||||
|
Math.round(this.lastWidth + this.lastLeft) ===
|
||||||
|
Math.round(this.getParentRect().width)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isCollapsed() {
|
||||||
|
return this.lastWidth <= this.currentMinWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
setWidth(width, finish) {
|
||||||
|
if (width < this.currentMinWidth) {
|
||||||
|
width = this.currentMinWidth;
|
||||||
|
}
|
||||||
|
const parentRect = this.getParentRect();
|
||||||
|
if (width > parentRect.width) {
|
||||||
|
width = parentRect.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxWidth = this.appFrame.width - this.panel.getBoundingClientRect().x;
|
||||||
|
if (width > maxWidth) {
|
||||||
|
width = maxWidth;
|
||||||
|
}
|
||||||
|
if (Math.round(width + this.lastLeft) === Math.round(parentRect.width)) {
|
||||||
|
this.panel.style.width = `calc(100% - ${this.lastLeft}px)`;
|
||||||
|
this.panel.style.flexBasis = `calc(100% - ${this.lastLeft}px)`;
|
||||||
|
} else {
|
||||||
|
this.panel.style.flexBasis = width + 'px';
|
||||||
|
this.panel.style.width = width + 'px';
|
||||||
|
}
|
||||||
|
this.lastWidth = width;
|
||||||
|
if (finish) {
|
||||||
|
this.finishSettingWidth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLeft(left) {
|
||||||
|
this.panel.style.left = left + 'px';
|
||||||
|
this.lastLeft = left;
|
||||||
|
}
|
||||||
|
|
||||||
|
finishSettingWidth() {
|
||||||
|
if (!this.collapsable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.collapsed = this.isCollapsed();
|
||||||
|
if (this.collapsed) {
|
||||||
|
this.resizerColumn.classList.add(PANEL_CSS_CLASS_COLLAPSED);
|
||||||
|
} else {
|
||||||
|
this.resizerColumn.classList.remove(PANEL_CSS_CLASS_COLLAPSED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If an iframe is displayed adjacent to our panel, and the mouse exits over the iframe,
|
||||||
|
* document[onmouseup] is not triggered because the document is no longer the same over
|
||||||
|
* the iframe. We add an invisible overlay while resizing so that the mouse context
|
||||||
|
* remains in our main document.
|
||||||
|
*/
|
||||||
|
addInvisibleOverlay() {
|
||||||
|
if (this.overlay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.overlay = this.$compile(`<div id='resizer-overlay'></div>`)(this.$scope);
|
||||||
|
angular.element(document.body).prepend(this.overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeInvisibleOverlay() {
|
||||||
|
if (this.overlay) {
|
||||||
|
this.overlay.remove();
|
||||||
|
this.overlay = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flash() {
|
||||||
|
const FLASH_DURATION = 3000;
|
||||||
|
this.resizerColumn.classList.add(PANEL_CSS_CLASS_ANIMATE_OPACITY);
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.resizerColumn.classList.remove(PANEL_CSS_CLASS_ANIMATE_OPACITY);
|
||||||
|
}, FLASH_DURATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PanelResizer {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.template = template;
|
||||||
|
this.controller = PanelResizerCtrl;
|
||||||
|
this.controllerAs = 'ctrl';
|
||||||
|
this.bindToController = true;
|
||||||
|
this.scope = {
|
||||||
|
alwaysVisible: '=',
|
||||||
|
collapsable: '=',
|
||||||
|
control: '=',
|
||||||
|
defaultWidth: '=',
|
||||||
|
hoverable: '=',
|
||||||
|
index: '=',
|
||||||
|
minWidth: '=',
|
||||||
|
onResize: '&',
|
||||||
|
onResizeFinish: '&',
|
||||||
|
panelId: '=',
|
||||||
|
property: '='
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
284
app/assets/javascripts/directives/views/passwordWizard.js
Normal file
284
app/assets/javascripts/directives/views/passwordWizard.js
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
import { protocolManager } from 'snjs';
|
||||||
|
import template from '%/directives/password-wizard.pug';
|
||||||
|
import { STRING_FAILED_PASSWORD_CHANGE } from '@/strings';
|
||||||
|
|
||||||
|
const DEFAULT_CONTINUE_TITLE = "Continue";
|
||||||
|
const Steps = {
|
||||||
|
IntroStep: 0,
|
||||||
|
BackupStep: 1,
|
||||||
|
SignoutStep: 2,
|
||||||
|
PasswordStep: 3,
|
||||||
|
SyncStep: 4,
|
||||||
|
FinishStep: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
class PasswordWizardCtrl {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$element,
|
||||||
|
$scope,
|
||||||
|
$timeout,
|
||||||
|
alertManager,
|
||||||
|
archiveManager,
|
||||||
|
authManager,
|
||||||
|
modelManager,
|
||||||
|
syncManager,
|
||||||
|
) {
|
||||||
|
this.$element = $element;
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
this.$scope = $scope;
|
||||||
|
this.alertManager = alertManager;
|
||||||
|
this.archiveManager = archiveManager;
|
||||||
|
this.authManager = authManager;
|
||||||
|
this.modelManager = modelManager;
|
||||||
|
this.syncManager = syncManager;
|
||||||
|
this.registerWindowUnloadStopper();
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInit() {
|
||||||
|
this.syncStatus = this.syncManager.syncStatus;
|
||||||
|
this.formData = {};
|
||||||
|
this.configureDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
configureDefaults() {
|
||||||
|
if (this.type === 'change-pw') {
|
||||||
|
this.title = "Change Password";
|
||||||
|
this.changePassword = true;
|
||||||
|
} else if (this.type === 'upgrade-security') {
|
||||||
|
this.title = "Security Update";
|
||||||
|
this.securityUpdate = true;
|
||||||
|
}
|
||||||
|
this.continueTitle = DEFAULT_CONTINUE_TITLE;
|
||||||
|
this.step = Steps.IntroStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Confirms with user before closing tab */
|
||||||
|
registerWindowUnloadStopper() {
|
||||||
|
window.onbeforeunload = (e) => {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
this.$scope.$on("$destroy", () => {
|
||||||
|
window.onbeforeunload = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
titleForStep(step) {
|
||||||
|
switch (step) {
|
||||||
|
case Steps.BackupStep:
|
||||||
|
return "Download a backup of your data";
|
||||||
|
case Steps.SignoutStep:
|
||||||
|
return "Sign out of all your devices";
|
||||||
|
case Steps.PasswordStep:
|
||||||
|
return this.changePassword
|
||||||
|
? "Password information"
|
||||||
|
: "Enter your current password";
|
||||||
|
case Steps.SyncStep:
|
||||||
|
return "Encrypt and sync data with new keys";
|
||||||
|
case Steps.FinishStep:
|
||||||
|
return "Sign back in to your devices";
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async nextStep() {
|
||||||
|
if (this.lockContinue || this.isContinuing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isContinuing = true;
|
||||||
|
if (this.step === Steps.FinishStep) {
|
||||||
|
this.dismiss();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const next = () => {
|
||||||
|
this.step++;
|
||||||
|
this.initializeStep(this.step);
|
||||||
|
this.isContinuing = false;
|
||||||
|
}
|
||||||
|
const preprocessor = this.preprocessorForStep(this.step);
|
||||||
|
if (preprocessor) {
|
||||||
|
await preprocessor().then(next).catch(() => {
|
||||||
|
this.isContinuing = false;
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preprocessorForStep(step) {
|
||||||
|
if (step === Steps.PasswordStep) {
|
||||||
|
return async () => {
|
||||||
|
this.showSpinner = true;
|
||||||
|
this.continueTitle = "Generating Keys...";
|
||||||
|
const success = await this.validateCurrentPassword();
|
||||||
|
this.showSpinner = false;
|
||||||
|
this.continueTitle = DEFAULT_CONTINUE_TITLE;
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initializeStep(step) {
|
||||||
|
if (step === Steps.SyncStep) {
|
||||||
|
await this.initializeSyncingStep();
|
||||||
|
} else if (step === Steps.FinishStep) {
|
||||||
|
this.continueTitle = "Finish";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initializeSyncingStep() {
|
||||||
|
this.lockContinue = true;
|
||||||
|
this.formData.status = "Processing encryption keys...";
|
||||||
|
this.formData.processing = true;
|
||||||
|
|
||||||
|
const passwordSuccess = await this.processPasswordChange();
|
||||||
|
this.formData.statusError = !passwordSuccess;
|
||||||
|
this.formData.processing = passwordSuccess;
|
||||||
|
if(!passwordSuccess) {
|
||||||
|
this.formData.status = "Unable to process your password. Please try again.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.formData.status = "Encrypting and syncing data with new keys...";
|
||||||
|
|
||||||
|
const syncSuccess = await this.resyncData();
|
||||||
|
this.formData.statusError = !syncSuccess;
|
||||||
|
this.formData.processing = !syncSuccess;
|
||||||
|
if (syncSuccess) {
|
||||||
|
this.lockContinue = false;
|
||||||
|
if (this.changePassword) {
|
||||||
|
this.formData.status = "Successfully changed password and synced all items.";
|
||||||
|
} else if (this.securityUpdate) {
|
||||||
|
this.formData.status = "Successfully performed security update and synced all items.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.formData.status = STRING_FAILED_PASSWORD_CHANGE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateCurrentPassword() {
|
||||||
|
const currentPassword = this.formData.currentPassword;
|
||||||
|
const newPass = this.securityUpdate ? currentPassword : this.formData.newPassword;
|
||||||
|
if (!currentPassword || currentPassword.length === 0) {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: "Please enter your current password."
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.changePassword) {
|
||||||
|
if (!newPass || newPass.length === 0) {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: "Please enter a new password."
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (newPass !== this.formData.newPasswordConfirmation) {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: "Your new password does not match its confirmation."
|
||||||
|
});
|
||||||
|
this.formData.status = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.authManager.user.email) {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: "We don't have your email stored. Please log out then log back in to fix this issue."
|
||||||
|
});
|
||||||
|
this.formData.status = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Validate current password */
|
||||||
|
const authParams = await this.authManager.getAuthParams();
|
||||||
|
const password = this.formData.currentPassword;
|
||||||
|
const keys = await protocolManager.computeEncryptionKeysForUser(
|
||||||
|
password,
|
||||||
|
authParams
|
||||||
|
);
|
||||||
|
const success = keys.mk === (await this.authManager.keys()).mk;
|
||||||
|
if (success) {
|
||||||
|
this.currentServerPw = keys.pw;
|
||||||
|
} else {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: "The current password you entered is not correct. Please try again."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
async resyncData() {
|
||||||
|
await this.modelManager.setAllItemsDirty();
|
||||||
|
const response = await this.syncManager.sync();
|
||||||
|
if (!response || response.error) {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: STRING_FAILED_PASSWORD_CHANGE
|
||||||
|
})
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async processPasswordChange() {
|
||||||
|
const newUserPassword = this.securityUpdate
|
||||||
|
? this.formData.currentPassword
|
||||||
|
: this.formData.newPassword;
|
||||||
|
const currentServerPw = this.currentServerPw;
|
||||||
|
const results = await protocolManager.generateInitialKeysAndAuthParamsForUser(
|
||||||
|
this.authManager.user.email,
|
||||||
|
newUserPassword
|
||||||
|
);
|
||||||
|
const newKeys = results.keys;
|
||||||
|
const newAuthParams = results.authParams;
|
||||||
|
/**
|
||||||
|
* Perform a sync beforehand to pull in any last minutes changes before we change
|
||||||
|
* the encryption key (and thus cant decrypt new changes).
|
||||||
|
*/
|
||||||
|
await this.syncManager.sync();
|
||||||
|
const response = await this.authManager.changePassword(
|
||||||
|
await this.syncManager.getServerURL(),
|
||||||
|
this.authManager.user.email,
|
||||||
|
currentServerPw,
|
||||||
|
newKeys,
|
||||||
|
newAuthParams
|
||||||
|
);
|
||||||
|
if (response.error) {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: response.error.message
|
||||||
|
? response.error.message
|
||||||
|
: "There was an error changing your password. Please try again."
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadBackup(encrypted) {
|
||||||
|
this.archiveManager.downloadBackup(encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss() {
|
||||||
|
if (this.lockContinue) {
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: "Cannot close window until pending tasks are complete."
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$element.remove();
|
||||||
|
this.$scope.$destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PasswordWizard {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.template = template;
|
||||||
|
this.controller = PasswordWizardCtrl;
|
||||||
|
this.controllerAs = 'ctrl';
|
||||||
|
this.bindToController = true;
|
||||||
|
this.scope = {
|
||||||
|
type: '='
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/assets/javascripts/directives/views/permissionsModal.js
Normal file
38
app/assets/javascripts/directives/views/permissionsModal.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import template from '%/directives/permissions-modal.pug';
|
||||||
|
|
||||||
|
class PermissionsModalCtrl {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor($element) {
|
||||||
|
this.$element = $element;
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss() {
|
||||||
|
this.$element.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
accept() {
|
||||||
|
this.callback(true);
|
||||||
|
this.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
deny() {
|
||||||
|
this.callback(false);
|
||||||
|
this.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PermissionsModal {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.template = template;
|
||||||
|
this.controller = PermissionsModalCtrl;
|
||||||
|
this.controllerAs = 'ctrl';
|
||||||
|
this.bindToController = true;
|
||||||
|
this.scope = {
|
||||||
|
show: '=',
|
||||||
|
component: '=',
|
||||||
|
permissionsString: '=',
|
||||||
|
callback: '='
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
101
app/assets/javascripts/directives/views/privilegesAuthModal.js
Normal file
101
app/assets/javascripts/directives/views/privilegesAuthModal.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import template from '%/directives/privileges-auth-modal.pug';
|
||||||
|
|
||||||
|
class PrivilegesAuthModalCtrl {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$element,
|
||||||
|
$timeout,
|
||||||
|
privilegesManager,
|
||||||
|
) {
|
||||||
|
this.$element = $element;
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
this.privilegesManager = privilegesManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInit() {
|
||||||
|
this.authParameters = {};
|
||||||
|
this.sessionLengthOptions = this.privilegesManager.getSessionLengthOptions();
|
||||||
|
this.privilegesManager.getSelectedSessionLength().then((length) => {
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.selectedSessionLength = length;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.privilegesManager.netCredentialsForAction(this.action).then((credentials) => {
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.requiredCredentials = credentials.sort();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectSessionLength(length) {
|
||||||
|
this.selectedSessionLength = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
promptForCredential(credential) {
|
||||||
|
return this.privilegesManager.displayInfoForCredential(credential).prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.dismiss();
|
||||||
|
this.onCancel && this.onCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
isCredentialInFailureState(credential) {
|
||||||
|
if (!this.failedCredentials) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.failedCredentials.find((candidate) => {
|
||||||
|
return candidate === credential;
|
||||||
|
}) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
validate() {
|
||||||
|
const failed = [];
|
||||||
|
for (const cred of this.requiredCredentials) {
|
||||||
|
const value = this.authParameters[cred];
|
||||||
|
if (!value || value.length === 0) {
|
||||||
|
failed.push(cred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.failedCredentials = failed;
|
||||||
|
return failed.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
if (!this.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await this.privilegesManager.authenticateAction(
|
||||||
|
this.action,
|
||||||
|
this.authParameters
|
||||||
|
);
|
||||||
|
this.$timeout(() => {
|
||||||
|
if (result.success) {
|
||||||
|
this.privilegesManager.setSessionLength(this.selectedSessionLength);
|
||||||
|
this.onSuccess();
|
||||||
|
this.dismiss();
|
||||||
|
} else {
|
||||||
|
this.failedCredentials = result.failedCredentials;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss() {
|
||||||
|
this.$element.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PrivilegesAuthModal {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.template = template;
|
||||||
|
this.controller = PrivilegesAuthModalCtrl;
|
||||||
|
this.controllerAs = 'ctrl';
|
||||||
|
this.bindToController = true;
|
||||||
|
this.scope = {
|
||||||
|
action: '=',
|
||||||
|
onSuccess: '=',
|
||||||
|
onCancel: '='
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { PrivilegesManager } from '@/services/privilegesManager';
|
||||||
|
import template from '%/directives/privileges-management-modal.pug';
|
||||||
|
|
||||||
|
class PrivilegesManagementModalCtrl {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$timeout,
|
||||||
|
$element,
|
||||||
|
privilegesManager,
|
||||||
|
authManager,
|
||||||
|
passcodeManager,
|
||||||
|
) {
|
||||||
|
this.$element = $element;
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
this.privilegesManager = privilegesManager;
|
||||||
|
this.hasPasscode = passcodeManager.hasPasscode();
|
||||||
|
this.hasAccount = !authManager.offline();
|
||||||
|
this.reloadPrivileges();
|
||||||
|
}
|
||||||
|
|
||||||
|
displayInfoForCredential(credential) {
|
||||||
|
const info = this.privilegesManager.displayInfoForCredential(credential);
|
||||||
|
if (credential === PrivilegesManager.CredentialLocalPasscode) {
|
||||||
|
info.availability = this.hasPasscode;
|
||||||
|
} else if (credential === PrivilegesManager.CredentialAccountPassword) {
|
||||||
|
info.availability = this.hasAccount;
|
||||||
|
} else {
|
||||||
|
info.availability = true;
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayInfoForAction(action) {
|
||||||
|
return this.privilegesManager.displayInfoForAction(action).label;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCredentialRequiredForAction(action, credential) {
|
||||||
|
if (!this.privileges) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.privileges.isCredentialRequiredForAction(action, credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearSession() {
|
||||||
|
await this.privilegesManager.clearSession();
|
||||||
|
this.reloadPrivileges();
|
||||||
|
}
|
||||||
|
|
||||||
|
async reloadPrivileges() {
|
||||||
|
this.availableActions = this.privilegesManager.getAvailableActions();
|
||||||
|
this.availableCredentials = this.privilegesManager.getAvailableCredentials();
|
||||||
|
const sessionEndDate = await this.privilegesManager.getSessionExpirey();
|
||||||
|
this.sessionExpirey = sessionEndDate.toLocaleString();
|
||||||
|
this.sessionExpired = new Date() >= sessionEndDate;
|
||||||
|
this.credentialDisplayInfo = {};
|
||||||
|
for (const cred of this.availableCredentials) {
|
||||||
|
this.credentialDisplayInfo[cred] = this.displayInfoForCredential(cred);
|
||||||
|
}
|
||||||
|
const privs = await this.privilegesManager.getPrivileges();
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.privileges = privs;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
checkboxValueChanged(action, credential) {
|
||||||
|
this.privileges.toggleCredentialForAction(action, credential);
|
||||||
|
this.privilegesManager.savePrivileges();
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.dismiss();
|
||||||
|
this.onCancel && this.onCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss() {
|
||||||
|
this.$element.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PrivilegesManagementModal {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.template = template;
|
||||||
|
this.controller = PrivilegesManagementModalCtrl;
|
||||||
|
this.controllerAs = 'ctrl';
|
||||||
|
this.bindToController = true;
|
||||||
|
this.scope = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
133
app/assets/javascripts/directives/views/revisionPreviewModal.js
Normal file
133
app/assets/javascripts/directives/views/revisionPreviewModal.js
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { protocolManager, SNComponent, SFItem, SFModelManager } from 'snjs';
|
||||||
|
import template from '%/directives/revision-preview-modal.pug';
|
||||||
|
|
||||||
|
class RevisionPreviewModalCtrl {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$element,
|
||||||
|
$scope,
|
||||||
|
$timeout,
|
||||||
|
alertManager,
|
||||||
|
componentManager,
|
||||||
|
modelManager,
|
||||||
|
syncManager,
|
||||||
|
) {
|
||||||
|
this.$element = $element;
|
||||||
|
this.$scope = $scope;
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
this.alertManager = alertManager;
|
||||||
|
this.componentManager = componentManager;
|
||||||
|
this.modelManager = modelManager;
|
||||||
|
this.syncManager = syncManager;
|
||||||
|
this.createNote();
|
||||||
|
this.configureEditor();
|
||||||
|
$scope.$on('$destroy', () => {
|
||||||
|
if (this.identifier) {
|
||||||
|
this.componentManager.deregisterHandler(this.identifier);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createNote() {
|
||||||
|
this.note = new SFItem({
|
||||||
|
content: this.content,
|
||||||
|
content_type: "Note"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
configureEditor() {
|
||||||
|
/**
|
||||||
|
* Set UUID so editoForNote can find proper editor, but then generate new uuid
|
||||||
|
* for note as not to save changes to original, if editor makes changes.
|
||||||
|
*/
|
||||||
|
this.note.uuid = this.uuid;
|
||||||
|
const editorForNote = this.componentManager.editorForNote(this.note);
|
||||||
|
this.note.uuid = protocolManager.crypto.generateUUIDSync();
|
||||||
|
if (editorForNote) {
|
||||||
|
/**
|
||||||
|
* Create temporary copy, as a lot of componentManager is uuid based, so might
|
||||||
|
* interfere with active editor. Be sure to copy only the content, as the top level
|
||||||
|
* editor object has non-copyable properties like .window, which cannot be transfered
|
||||||
|
*/
|
||||||
|
const editorCopy = new SNComponent({
|
||||||
|
content: editorForNote.content
|
||||||
|
});
|
||||||
|
editorCopy.readonly = true;
|
||||||
|
editorCopy.lockReadonly = true;
|
||||||
|
this.identifier = editorCopy.uuid;
|
||||||
|
this.componentManager.registerHandler({
|
||||||
|
identifier: this.identifier,
|
||||||
|
areas: ['editor-editor'],
|
||||||
|
contextRequestHandler: (component) => {
|
||||||
|
if (component === this.editor) {
|
||||||
|
return this.note;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
componentForSessionKeyHandler: (key) => {
|
||||||
|
if (key === this.editor.sessionKey) {
|
||||||
|
return this.editor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.editor = editorCopy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
restore(asCopy) {
|
||||||
|
const run = () => {
|
||||||
|
let item;
|
||||||
|
if (asCopy) {
|
||||||
|
const contentCopy = Object.assign({}, this.content);
|
||||||
|
if (contentCopy.title) {
|
||||||
|
contentCopy.title += " (copy)";
|
||||||
|
}
|
||||||
|
item = this.modelManager.createItem({
|
||||||
|
content_type: 'Note',
|
||||||
|
content: contentCopy
|
||||||
|
});
|
||||||
|
this.modelManager.addItem(item);
|
||||||
|
} else {
|
||||||
|
const uuid = this.uuid;
|
||||||
|
item = this.modelManager.findItem(uuid);
|
||||||
|
item.content = Object.assign({}, this.content);
|
||||||
|
this.modelManager.mapResponseItemsToLocalModels(
|
||||||
|
[item],
|
||||||
|
SFModelManager.MappingSourceRemoteActionRetrieved
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.modelManager.setItemDirty(item);
|
||||||
|
this.syncManager.sync();
|
||||||
|
this.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!asCopy) {
|
||||||
|
this.alertManager.confirm({
|
||||||
|
text: "Are you sure you want to replace the current note's contents with what you see in this preview?",
|
||||||
|
destructive: true,
|
||||||
|
onConfirm: run
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss() {
|
||||||
|
this.$element.remove();
|
||||||
|
this.$scope.$destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RevisionPreviewModal {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.template = template;
|
||||||
|
this.controller = RevisionPreviewModalCtrl;
|
||||||
|
this.controllerAs = 'ctrl';
|
||||||
|
this.bindToController = true;
|
||||||
|
this.scope = {
|
||||||
|
uuid: '=',
|
||||||
|
content: '='
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
118
app/assets/javascripts/directives/views/sessionHistoryMenu.js
Normal file
118
app/assets/javascripts/directives/views/sessionHistoryMenu.js
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import template from '%/directives/session-history-menu.pug';
|
||||||
|
|
||||||
|
class SessionHistoryMenuCtrl {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$timeout,
|
||||||
|
actionsManager,
|
||||||
|
alertManager,
|
||||||
|
sessionHistory,
|
||||||
|
) {
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
this.alertManager = alertManager;
|
||||||
|
this.actionsManager = actionsManager;
|
||||||
|
this.sessionHistory = sessionHistory;
|
||||||
|
this.diskEnabled = this.sessionHistory.diskEnabled;
|
||||||
|
this.autoOptimize = this.sessionHistory.autoOptimize;
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInit() {
|
||||||
|
this.reloadHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadHistory() {
|
||||||
|
const history = this.sessionHistory.historyForItem(this.item);
|
||||||
|
this.entries = history.entries.slice(0).sort((a, b) => {
|
||||||
|
return a.item.updated_at < b.item.updated_at ? 1 : -1;
|
||||||
|
})
|
||||||
|
this.history = history;
|
||||||
|
}
|
||||||
|
|
||||||
|
openRevision(revision) {
|
||||||
|
this.actionsManager.presentRevisionPreviewModal(
|
||||||
|
revision.item.uuid,
|
||||||
|
revision.item.content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
classForRevision(revision) {
|
||||||
|
const vector = revision.operationVector();
|
||||||
|
if (vector === 0) {
|
||||||
|
return 'default';
|
||||||
|
} else if (vector === 1) {
|
||||||
|
return 'success';
|
||||||
|
} else if (vector === -1) {
|
||||||
|
return 'danger';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearItemHistory() {
|
||||||
|
this.alertManager.confirm({
|
||||||
|
text: "Are you sure you want to delete the local session history for this note?",
|
||||||
|
destructive: true,
|
||||||
|
onConfirm: () => {
|
||||||
|
this.sessionHistory.clearHistoryForItem(this.item).then(() => {
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.reloadHistory();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAllHistory() {
|
||||||
|
this.alertManager.confirm({
|
||||||
|
text: "Are you sure you want to delete the local session history for all notes?",
|
||||||
|
destructive: true,
|
||||||
|
onConfirm: () => {
|
||||||
|
this.sessionHistory.clearAllHistory().then(() => {
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.reloadHistory();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDiskSaving() {
|
||||||
|
const run = () => {
|
||||||
|
this.sessionHistory.toggleDiskSaving().then(() => {
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.diskEnabled = this.sessionHistory.diskEnabled;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!this.sessionHistory.diskEnabled) {
|
||||||
|
this.alertManager.confirm({
|
||||||
|
text: `Are you sure you want to save history to disk? This will decrease general
|
||||||
|
performance, especially as you type. You are advised to disable this feature
|
||||||
|
if you experience any lagging.`,
|
||||||
|
destructive: true,
|
||||||
|
onConfirm: run
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleAutoOptimize() {
|
||||||
|
this.sessionHistory.toggleAutoOptimize().then(() => {
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.autoOptimize = this.sessionHistory.autoOptimize;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SessionHistoryMenu {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.template = template;
|
||||||
|
this.controller = SessionHistoryMenuCtrl;
|
||||||
|
this.controllerAs = 'ctrl';
|
||||||
|
this.bindToController = true;
|
||||||
|
this.scope = {
|
||||||
|
item: '='
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import template from '%/directives/sync-resolution-menu.pug';
|
||||||
|
|
||||||
|
class SyncResolutionMenuCtrl {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$timeout,
|
||||||
|
archiveManager,
|
||||||
|
syncManager,
|
||||||
|
) {
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
this.archiveManager = archiveManager;
|
||||||
|
this.syncManager = syncManager;
|
||||||
|
this.status = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadBackup(encrypted) {
|
||||||
|
this.archiveManager.downloadBackup(encrypted);
|
||||||
|
this.status.backupFinished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
skipBackup() {
|
||||||
|
this.status.backupFinished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async performSyncResolution() {
|
||||||
|
this.status.resolving = true;
|
||||||
|
await this.syncManager.resolveOutOfSync();
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.status.resolving = false;
|
||||||
|
this.status.attemptedResolution = true;
|
||||||
|
if (this.syncManager.isOutOfSync()) {
|
||||||
|
this.status.fail = true;
|
||||||
|
} else {
|
||||||
|
this.status.success = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.$timeout(() => {
|
||||||
|
this.closeFunction()();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SyncResolutionMenu {
|
||||||
|
constructor() {
|
||||||
|
this.restrict = 'E';
|
||||||
|
this.template = template;
|
||||||
|
this.controller = SyncResolutionMenuCtrl;
|
||||||
|
this.controllerAs = 'ctrl';
|
||||||
|
this.bindToController = true;
|
||||||
|
this.scope = {
|
||||||
|
closeFunction: '&'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
// css
|
// css
|
||||||
import 'sn-stylekit/dist/stylekit.css';
|
import 'sn-stylekit/dist/stylekit.css';
|
||||||
import '../stylesheets/main.css.scss';
|
import '../stylesheets/index.css.scss';
|
||||||
|
|
||||||
// Vendor
|
// Vendor
|
||||||
import 'angular';
|
import 'angular';
|
||||||
@@ -19,4 +19,4 @@ SFItem.AppDomain = 'org.standardnotes.sn';
|
|||||||
|
|
||||||
// entry point
|
// entry point
|
||||||
// eslint-disable-next-line import/first
|
// eslint-disable-next-line import/first
|
||||||
import './app/app';
|
import './app';
|
||||||
279
app/assets/javascripts/services/actionsManager.js
Normal file
279
app/assets/javascripts/services/actionsManager.js
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import angular from 'angular';
|
||||||
|
import { Action, SFModelManager, SFItemParams, protocolManager } from 'snjs';
|
||||||
|
|
||||||
|
export class ActionsManager {
|
||||||
|
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
$compile,
|
||||||
|
$rootScope,
|
||||||
|
$timeout,
|
||||||
|
alertManager,
|
||||||
|
authManager,
|
||||||
|
httpManager,
|
||||||
|
modelManager,
|
||||||
|
syncManager,
|
||||||
|
) {
|
||||||
|
this.$compile = $compile;
|
||||||
|
this.$rootScope = $rootScope;
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
this.alertManager = alertManager;
|
||||||
|
this.authManager = authManager;
|
||||||
|
this.httpManager = httpManager;
|
||||||
|
this.modelManager = modelManager;
|
||||||
|
this.syncManager = syncManager;
|
||||||
|
/* Used when decrypting old items with new keys. This array is only kept in memory. */
|
||||||
|
this.previousPasswords = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get extensions() {
|
||||||
|
return this.modelManager.validItemsForContentType('Extension');
|
||||||
|
}
|
||||||
|
|
||||||
|
extensionsInContextOfItem(item) {
|
||||||
|
return this.extensions.filter((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.
|
||||||
|
*/
|
||||||
|
async loadExtensionInContextOfItem(extension, item) {
|
||||||
|
const params = {
|
||||||
|
content_type: item.content_type,
|
||||||
|
item_uuid: item.uuid
|
||||||
|
};
|
||||||
|
const emptyFunc = () => { };
|
||||||
|
return this.httpManager.getAbsolute(extension.url, params, emptyFunc).then((response) => {
|
||||||
|
this.updateExtensionFromRemoteResponse(extension, response);
|
||||||
|
return extension;
|
||||||
|
}).catch((response) => {
|
||||||
|
console.error("Error loading extension", response);
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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((action) => {
|
||||||
|
return new Action(action);
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
extension.actions = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeAction(action, extension, item) {
|
||||||
|
action.running = true;
|
||||||
|
let result;
|
||||||
|
switch (action.verb) {
|
||||||
|
case 'get':
|
||||||
|
result = await this.handleGetAction(action);
|
||||||
|
break;
|
||||||
|
case 'render':
|
||||||
|
result = await this.handleRenderAction(action);
|
||||||
|
break;
|
||||||
|
case 'show':
|
||||||
|
result = await this.handleShowAction(action);
|
||||||
|
break;
|
||||||
|
case 'post':
|
||||||
|
result = await this.handlePostAction(action, item, extension);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
action.lastExecuted = new Date();
|
||||||
|
action.running = false;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptResponse(response, keys) {
|
||||||
|
const responseItem = response.item;
|
||||||
|
await protocolManager.decryptItem(responseItem, keys);
|
||||||
|
if (!responseItem.errorDecrypting) {
|
||||||
|
return {
|
||||||
|
response: response,
|
||||||
|
item: responseItem
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.auth_params) {
|
||||||
|
/**
|
||||||
|
* In some cases revisions were missing auth params.
|
||||||
|
* Instruct the user to email us to get this remedied.
|
||||||
|
*/
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: `We were unable to decrypt this revision using your current keys,
|
||||||
|
and this revision is missing metadata that would allow us to try different
|
||||||
|
keys to decrypt it. This can likely be fixed with some manual intervention.
|
||||||
|
Please email hello@standardnotes.org for assistance.`
|
||||||
|
});
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Try previous passwords */
|
||||||
|
const triedPasswords = [];
|
||||||
|
for (const passwordCandidate of this.previousPasswords) {
|
||||||
|
if (triedPasswords.includes(passwordCandidate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
triedPasswords.push(passwordCandidate);
|
||||||
|
const keyResults = await protocolManager.computeEncryptionKeysForUser(
|
||||||
|
passwordCandidate,
|
||||||
|
response.auth_params
|
||||||
|
);
|
||||||
|
if (!keyResults) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const nestedResponse = await this.decryptResponse(
|
||||||
|
response,
|
||||||
|
keyResults
|
||||||
|
);
|
||||||
|
if (nestedResponse.item) {
|
||||||
|
return nestedResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.presentPasswordModal((password) => {
|
||||||
|
this.previousPasswords.push(password);
|
||||||
|
const result = this.decryptResponse(response, keys);
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async handlePostAction(action, item, extension) {
|
||||||
|
const decrypted = action.access_type === 'decrypted';
|
||||||
|
const itemParams = await this.outgoingParamsForItem(item, extension, decrypted);
|
||||||
|
const params = {
|
||||||
|
items: [itemParams]
|
||||||
|
}
|
||||||
|
const emptyFunc = () => { };
|
||||||
|
return this.httpManager.postAbsolute(action.url, params, emptyFunc).then((response) => {
|
||||||
|
action.error = false;
|
||||||
|
return {response: response};
|
||||||
|
}).catch((response) => {
|
||||||
|
action.error = true;
|
||||||
|
console.error("Action error response:", response);
|
||||||
|
this.alertManager.alert({
|
||||||
|
text: "An issue occurred while processing this action. Please try again."
|
||||||
|
});
|
||||||
|
return { response: response };
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleShowAction(action) {
|
||||||
|
const win = window.open(action.url, '_blank');
|
||||||
|
if (win) {
|
||||||
|
win.focus();
|
||||||
|
}
|
||||||
|
return { response: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleGetAction(action) {
|
||||||
|
const emptyFunc = () => {};
|
||||||
|
const onConfirm = async () => {
|
||||||
|
return this.httpManager.getAbsolute(action.url, {}, emptyFunc)
|
||||||
|
.then(async (response) => {
|
||||||
|
action.error = false;
|
||||||
|
await this.decryptResponse(response, await this.authManager.keys());
|
||||||
|
const items = await this.modelManager.mapResponseItemsToLocalModels(
|
||||||
|
[response.item],
|
||||||
|
SFModelManager.MappingSourceRemoteActionRetrieved
|
||||||
|
);
|
||||||
|
for (const mappedItem of items) {
|
||||||
|
this.modelManager.setItemDirty(mappedItem, true);
|
||||||
|
}
|
||||||
|
this.syncManager.sync();
|
||||||
|
return {
|
||||||
|
response: response,
|
||||||
|
item: response.item
|
||||||
|
};
|
||||||
|
}).catch((response) => {
|
||||||
|
const error = (response && response.error)
|
||||||
|
|| { message: "An issue occurred while processing this action. Please try again." }
|
||||||
|
this.alertManager.alert({ text: error.message });
|
||||||
|
action.error = true;
|
||||||
|
return { error: error };
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.alertManager.confirm({
|
||||||
|
text: "Are you sure you want to replace the current note contents with this action's results?",
|
||||||
|
onConfirm: () => {
|
||||||
|
onConfirm().then(resolve)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleRenderAction(action) {
|
||||||
|
const emptyFunc = () => {};
|
||||||
|
return this.httpManager.getAbsolute(action.url, {}, emptyFunc).then(async (response) => {
|
||||||
|
action.error = false;
|
||||||
|
const result = await this.decryptResponse(response, await this.authManager.keys());
|
||||||
|
const item = this.modelManager.createItem(result.item);
|
||||||
|
return {
|
||||||
|
response: result.response,
|
||||||
|
item: item
|
||||||
|
};
|
||||||
|
}).catch((response) => {
|
||||||
|
const error = (response && response.error)
|
||||||
|
|| { message: "An issue occurred while processing this action. Please try again." }
|
||||||
|
this.alertManager.alert({ text: error.message });
|
||||||
|
action.error = true;
|
||||||
|
return { error: error };
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async outgoingParamsForItem(item, extension, decrypted = false) {
|
||||||
|
let keys = await this.authManager.keys();
|
||||||
|
if (decrypted) {
|
||||||
|
keys = null;
|
||||||
|
}
|
||||||
|
const itemParams = new SFItemParams(
|
||||||
|
item,
|
||||||
|
keys,
|
||||||
|
await this.authManager.getAuthParams()
|
||||||
|
);
|
||||||
|
return itemParams.paramsForExtension();
|
||||||
|
}
|
||||||
|
|
||||||
|
presentRevisionPreviewModal(uuid, content) {
|
||||||
|
const scope = this.$rootScope.$new(true);
|
||||||
|
scope.uuid = uuid;
|
||||||
|
scope.content = content;
|
||||||
|
const el = this.$compile(
|
||||||
|
`<revision-preview-modal uuid='uuid' content='content'
|
||||||
|
class='sk-modal'></revision-preview-modal>`
|
||||||
|
)(scope);
|
||||||
|
angular.element(document.body).append(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
presentPasswordModal(callback) {
|
||||||
|
const scope = this.$rootScope.$new(true);
|
||||||
|
scope.type = "password";
|
||||||
|
scope.title = "Decryption Assistance";
|
||||||
|
scope.message = `Unable to decrypt this item with your current keys.
|
||||||
|
Please enter your account password at the time of this revision.`;
|
||||||
|
scope.callback = callback;
|
||||||
|
const el = this.$compile(
|
||||||
|
`<input-modal type='type' message='message'
|
||||||
|
title='title' callback='callback'></input-modal>`
|
||||||
|
)(scope);
|
||||||
|
angular.element(document.body).append(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
app/assets/javascripts/services/alertManager.js
Normal file
71
app/assets/javascripts/services/alertManager.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { SFAlertManager } from 'snjs';
|
||||||
|
import { SKAlert } from 'sn-stylekit';
|
||||||
|
|
||||||
|
export class AlertManager extends SFAlertManager {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor($timeout) {
|
||||||
|
super();
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
async alert({
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
closeButtonText = "OK",
|
||||||
|
onClose} = {}
|
||||||
|
) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const buttons = [
|
||||||
|
{
|
||||||
|
text: closeButtonText,
|
||||||
|
style: "neutral",
|
||||||
|
action: async () => {
|
||||||
|
if(onClose) {
|
||||||
|
this.$timeout(onClose);
|
||||||
|
}
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const alert = new SKAlert({title, text, buttons});
|
||||||
|
alert.present();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirm({
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
confirmButtonText = "Confirm",
|
||||||
|
cancelButtonText = "Cancel",
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
destructive = false
|
||||||
|
} = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const buttons = [
|
||||||
|
{
|
||||||
|
text: cancelButtonText,
|
||||||
|
style: "neutral",
|
||||||
|
action: async () => {
|
||||||
|
if(onCancel) {
|
||||||
|
this.$timeout(onCancel);
|
||||||
|
}
|
||||||
|
reject(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: confirmButtonText,
|
||||||
|
style: destructive ? "danger" : "info",
|
||||||
|
action: async () => {
|
||||||
|
if(onConfirm) {
|
||||||
|
this.$timeout(onConfirm);
|
||||||
|
}
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const alert = new SKAlert({title, text, buttons});
|
||||||
|
alert.present();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,9 +18,9 @@ export class ArchiveManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async downloadBackupOfItems(items, encrypted) {
|
async downloadBackupOfItems(items, encrypted) {
|
||||||
let run = async () => {
|
const run = async () => {
|
||||||
// download in Standard Notes format
|
// download in Standard Notes format
|
||||||
var keys, authParams;
|
let keys, authParams;
|
||||||
if(encrypted) {
|
if(encrypted) {
|
||||||
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
|
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
|
||||||
keys = this.passcodeManager.keys();
|
keys = this.passcodeManager.keys();
|
||||||
@@ -31,15 +31,15 @@ export class ArchiveManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.__itemsData(items, keys, authParams).then((data) => {
|
this.__itemsData(items, keys, authParams).then((data) => {
|
||||||
let modifier = encrypted ? "Encrypted" : "Decrypted";
|
const modifier = encrypted ? "Encrypted" : "Decrypted";
|
||||||
this.__downloadData(data, `Standard Notes ${modifier} Backup - ${this.__formattedDate()}.txt`);
|
this.__downloadData(data, `Standard Notes ${modifier} Backup - ${this.__formattedDate()}.txt`);
|
||||||
|
|
||||||
// download as zipped plain text files
|
// download as zipped plain text files
|
||||||
if(!keys) {
|
if(!keys) {
|
||||||
this.__downloadZippedItems(items);
|
this.__downloadZippedItems(items);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
if(await this.privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageBackups)) {
|
if(await this.privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageBackups)) {
|
||||||
this.privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => {
|
this.privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => {
|
||||||
@@ -65,8 +65,8 @@ export class ArchiveManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async __itemsData(items, keys, authParams) {
|
async __itemsData(items, keys, authParams) {
|
||||||
let data = await this.modelManager.getJSONDataForItems(items, keys, authParams);
|
const data = await this.modelManager.getJSONDataForItems(items, keys, authParams);
|
||||||
let blobData = new Blob([data], {type: 'text/json'});
|
const blobData = new Blob([data], {type: 'text/json'});
|
||||||
return blobData;
|
return blobData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ export class ArchiveManager {
|
|||||||
scriptTag.onload = function() {
|
scriptTag.onload = function() {
|
||||||
zip.workerScriptsPath = "assets/zip/";
|
zip.workerScriptsPath = "assets/zip/";
|
||||||
callback();
|
callback();
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
__downloadZippedItems(items) {
|
__downloadZippedItems(items) {
|
||||||
@@ -92,11 +92,11 @@ export class ArchiveManager {
|
|||||||
zip.createWriter(new zip.BlobWriter("application/zip"), (zipWriter) => {
|
zip.createWriter(new zip.BlobWriter("application/zip"), (zipWriter) => {
|
||||||
var index = 0;
|
var index = 0;
|
||||||
|
|
||||||
let nextFile = () => {
|
const nextFile = () => {
|
||||||
var item = items[index];
|
var item = items[index];
|
||||||
var name, contents;
|
var name, contents;
|
||||||
|
|
||||||
if(item.content_type == "Note") {
|
if(item.content_type === "Note") {
|
||||||
name = item.content.title;
|
name = item.content.title;
|
||||||
contents = item.content.text;
|
contents = item.content.text;
|
||||||
} else {
|
} else {
|
||||||
@@ -108,16 +108,12 @@ export class ArchiveManager {
|
|||||||
name = "";
|
name = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
var blob = new Blob([contents], {type: 'text/plain'});
|
const blob = new Blob([contents], {type: 'text/plain'});
|
||||||
|
let filePrefix = name.replace(/\//g, "").replace(/\\+/g, "");
|
||||||
var filePrefix = name.replace(/\//g, "").replace(/\\+/g, "");
|
const fileSuffix = `-${item.uuid.split("-")[0]}.txt`;
|
||||||
var fileSuffix = `-${item.uuid.split("-")[0]}.txt`
|
|
||||||
|
|
||||||
// Standard max filename length is 255. Slice the note name down to allow filenameEnd
|
// Standard max filename length is 255. Slice the note name down to allow filenameEnd
|
||||||
filePrefix = filePrefix.slice(0, (255 - fileSuffix.length));
|
filePrefix = filePrefix.slice(0, (255 - fileSuffix.length));
|
||||||
|
const fileName = `${item.content_type}/${filePrefix}${fileSuffix}`
|
||||||
let fileName = `${item.content_type}/${filePrefix}${fileSuffix}`
|
|
||||||
|
|
||||||
zipWriter.add(fileName, new zip.BlobReader(blob), () => {
|
zipWriter.add(fileName, new zip.BlobReader(blob), () => {
|
||||||
index++;
|
index++;
|
||||||
if(index < items.length) {
|
if(index < items.length) {
|
||||||
@@ -129,11 +125,11 @@ export class ArchiveManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
nextFile();
|
nextFile();
|
||||||
}, onerror);
|
}, onerror);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import { StorageManager } from './storageManager';
|
import { StorageManager } from './storageManager';
|
||||||
import { protocolManager, SFItem, SFPredicate, SFAuthManager } from 'snjs';
|
import { protocolManager, SFAuthManager } from 'snjs';
|
||||||
|
|
||||||
export class AuthManager extends SFAuthManager {
|
export class AuthManager extends SFAuthManager {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
@@ -24,24 +24,17 @@ export class AuthManager extends SFAuthManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadInitialData() {
|
loadInitialData() {
|
||||||
var userData = this.storageManager.getItemSync("user");
|
const userData = this.storageManager.getItemSync("user");
|
||||||
if(userData) {
|
if(userData) {
|
||||||
this.user = JSON.parse(userData);
|
this.user = JSON.parse(userData);
|
||||||
} else {
|
} else {
|
||||||
// legacy, check for uuid
|
// legacy, check for uuid
|
||||||
var idData = this.storageManager.getItemSync("uuid");
|
const idData = this.storageManager.getItemSync("uuid");
|
||||||
if(idData) {
|
if(idData) {
|
||||||
this.user = {uuid: idData};
|
this.user = {uuid: idData};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.configureUserPrefs();
|
|
||||||
this.checkForSecurityUpdate();
|
this.checkForSecurityUpdate();
|
||||||
|
|
||||||
this.modelManager.addItemSyncObserver("user-prefs", "SN|UserPreferences", (allItems, validItems, deletedItems, source, sourceKey) => {
|
|
||||||
this.userPreferencesDidChange();
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
offline() {
|
offline() {
|
||||||
@@ -144,48 +137,4 @@ export class AuthManager extends SFAuthManager {
|
|||||||
this.user = null;
|
this.user = null;
|
||||||
this._authParams = null;
|
this._authParams = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* User Preferences */
|
|
||||||
|
|
||||||
configureUserPrefs() {
|
|
||||||
let prefsContentType = "SN|UserPreferences";
|
|
||||||
|
|
||||||
let contentTypePredicate = new SFPredicate("content_type", "=", prefsContentType);
|
|
||||||
this.singletonManager.registerSingleton([contentTypePredicate], (resolvedSingleton) => {
|
|
||||||
this.userPreferences = resolvedSingleton;
|
|
||||||
}, (valueCallback) => {
|
|
||||||
// Safe to create. Create and return object.
|
|
||||||
var prefs = new SFItem({content_type: prefsContentType});
|
|
||||||
this.modelManager.addItem(prefs);
|
|
||||||
this.modelManager.setItemDirty(prefs, true);
|
|
||||||
this.$rootScope.sync();
|
|
||||||
valueCallback(prefs);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
userPreferencesDidChange() {
|
|
||||||
this.$rootScope.$broadcast("user-preferences-changed");
|
|
||||||
}
|
|
||||||
|
|
||||||
syncUserPreferences() {
|
|
||||||
if(this.userPreferences) {
|
|
||||||
this.modelManager.setItemDirty(this.userPreferences, true);
|
|
||||||
this.$rootScope.sync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserPrefValue(key, defaultValue) {
|
|
||||||
if(!this.userPreferences) { return defaultValue; }
|
|
||||||
var value = this.userPreferences.getAppDataItem(key);
|
|
||||||
return (value !== undefined && value != null) ? value : defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
setUserPrefValue(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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export class DBManager {
|
|||||||
db.close();
|
db.close();
|
||||||
};
|
};
|
||||||
db.onerror = function(errorEvent) {
|
db.onerror = function(errorEvent) {
|
||||||
console.log("Database error: " + errorEvent.target.errorCode);
|
console.error("Database error: " + errorEvent.target.errorCode);
|
||||||
}
|
}
|
||||||
resolve(db);
|
resolve(db);
|
||||||
};
|
};
|
||||||
@@ -161,12 +161,11 @@ export class DBManager {
|
|||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
deleteRequest.onerror = function(event) {
|
deleteRequest.onerror = function(event) {
|
||||||
console.log("Error deleting database.");
|
console.error("Error deleting database.");
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteRequest.onsuccess = function(event) {
|
deleteRequest.onsuccess = function(event) {
|
||||||
console.log("Database deleted successfully");
|
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3,6 +3,10 @@ import _ from 'lodash';
|
|||||||
import { isDesktopApplication } from '@/utils';
|
import { isDesktopApplication } from '@/utils';
|
||||||
import { SFItemParams, SFModelManager } from 'snjs';
|
import { SFItemParams, SFModelManager } from 'snjs';
|
||||||
|
|
||||||
|
const COMPONENT_DATA_KEY_INSTALL_ERROR = 'installError';
|
||||||
|
const COMPONENT_CONTENT_KEY_PACKAGE_INFO = 'package_info';
|
||||||
|
const COMPONENT_CONTENT_KEY_LOCAL_URL = 'local_url';
|
||||||
|
|
||||||
export class DesktopManager {
|
export class DesktopManager {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor(
|
constructor(
|
||||||
@@ -11,13 +15,15 @@ export class DesktopManager {
|
|||||||
modelManager,
|
modelManager,
|
||||||
syncManager,
|
syncManager,
|
||||||
authManager,
|
authManager,
|
||||||
passcodeManager
|
passcodeManager,
|
||||||
|
appState
|
||||||
) {
|
) {
|
||||||
this.passcodeManager = passcodeManager;
|
this.passcodeManager = passcodeManager;
|
||||||
this.modelManager = modelManager;
|
this.modelManager = modelManager;
|
||||||
this.authManager = authManager;
|
this.authManager = authManager;
|
||||||
this.syncManager = syncManager;
|
this.syncManager = syncManager;
|
||||||
this.$rootScope = $rootScope;
|
this.$rootScope = $rootScope;
|
||||||
|
this.appState = appState;
|
||||||
this.timeout = $timeout;
|
this.timeout = $timeout;
|
||||||
this.updateObservers = [];
|
this.updateObservers = [];
|
||||||
this.componentActivationObservers = [];
|
this.componentActivationObservers = [];
|
||||||
@@ -43,7 +49,10 @@ export class DesktopManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getExtServerHost() {
|
getExtServerHost() {
|
||||||
console.assert(this.extServerHost, "extServerHost is null");
|
console.assert(
|
||||||
|
this.extServerHost,
|
||||||
|
'extServerHost is null'
|
||||||
|
);
|
||||||
return this.extServerHost;
|
return this.extServerHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,8 +66,9 @@ export class DesktopManager {
|
|||||||
|
|
||||||
// All `components` should be installed
|
// All `components` should be installed
|
||||||
syncComponentsInstallation(components) {
|
syncComponentsInstallation(components) {
|
||||||
if(!this.isDesktop) return;
|
if(!this.isDesktop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Promise.all(components.map((component) => {
|
Promise.all(components.map((component) => {
|
||||||
return this.convertComponentForTransmission(component);
|
return this.convertComponentForTransmission(component);
|
||||||
})).then((data) => {
|
})).then((data) => {
|
||||||
@@ -67,11 +77,15 @@ export class DesktopManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async installComponent(component) {
|
async installComponent(component) {
|
||||||
this.installComponentHandler(await this.convertComponentForTransmission(component));
|
this.installComponentHandler(
|
||||||
|
await this.convertComponentForTransmission(component)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerUpdateObserver(callback) {
|
registerUpdateObserver(callback) {
|
||||||
var observer = {id: Math.random, callback: callback};
|
const observer = {
|
||||||
|
callback: callback
|
||||||
|
};
|
||||||
this.updateObservers.push(observer);
|
this.updateObservers.push(observer);
|
||||||
return observer;
|
return observer;
|
||||||
}
|
}
|
||||||
@@ -90,7 +104,6 @@ export class DesktopManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
deregisterUpdateObserver(observer) {
|
deregisterUpdateObserver(observer) {
|
||||||
_.pull(this.updateObservers, observer);
|
_.pull(this.updateObservers, observer);
|
||||||
}
|
}
|
||||||
@@ -109,39 +122,43 @@ export class DesktopManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
desktop_onComponentInstallationComplete(componentData, error) {
|
desktop_onComponentInstallationComplete(componentData, error) {
|
||||||
// console.log("Web|Component Installation/Update Complete", componentData, error);
|
const component = this.modelManager.findItem(componentData.uuid);
|
||||||
|
|
||||||
// Desktop is only allowed to change these keys:
|
|
||||||
let permissableKeys = ["package_info", "local_url"];
|
|
||||||
var component = this.modelManager.findItem(componentData.uuid);
|
|
||||||
|
|
||||||
if(!component) {
|
if(!component) {
|
||||||
console.error("desktop_onComponentInstallationComplete component is null for uuid", componentData.uuid);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(error) {
|
if(error) {
|
||||||
component.setAppDataItem("installError", error);
|
component.setAppDataItem(
|
||||||
|
COMPONENT_DATA_KEY_INSTALL_ERROR,
|
||||||
|
error
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
for(var key of permissableKeys) {
|
const permissableKeys = [
|
||||||
|
COMPONENT_CONTENT_KEY_PACKAGE_INFO,
|
||||||
|
COMPONENT_CONTENT_KEY_LOCAL_URL
|
||||||
|
];
|
||||||
|
for(const key of permissableKeys) {
|
||||||
component[key] = componentData.content[key];
|
component[key] = componentData.content[key];
|
||||||
}
|
}
|
||||||
this.modelManager.notifySyncObserversOfModels([component], SFModelManager.MappingSourceDesktopInstalled);
|
this.modelManager.notifySyncObserversOfModels(
|
||||||
component.setAppDataItem("installError", null);
|
[component],
|
||||||
|
SFModelManager.MappingSourceDesktopInstalled
|
||||||
|
);
|
||||||
|
component.setAppDataItem(
|
||||||
|
COMPONENT_DATA_KEY_INSTALL_ERROR,
|
||||||
|
null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
this.modelManager.setItemDirty(component);
|
||||||
this.modelManager.setItemDirty(component, true);
|
|
||||||
this.syncManager.sync();
|
this.syncManager.sync();
|
||||||
|
|
||||||
this.timeout(() => {
|
this.timeout(() => {
|
||||||
for(var observer of this.updateObservers) {
|
for(const observer of this.updateObservers) {
|
||||||
observer.callback(component);
|
observer.callback(component);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
desktop_registerComponentActivationObserver(callback) {
|
desktop_registerComponentActivationObserver(callback) {
|
||||||
var observer = {id: Math.random, callback: callback};
|
const observer = {id: Math.random, callback: callback};
|
||||||
this.componentActivationObservers.push(observer);
|
this.componentActivationObservers.push(observer);
|
||||||
return observer;
|
return observer;
|
||||||
}
|
}
|
||||||
@@ -152,10 +169,11 @@ export class DesktopManager {
|
|||||||
|
|
||||||
/* Notify observers that a component has been registered/activated */
|
/* Notify observers that a component has been registered/activated */
|
||||||
async notifyComponentActivation(component) {
|
async notifyComponentActivation(component) {
|
||||||
var serializedComponent = await this.convertComponentForTransmission(component);
|
const serializedComponent = await this.convertComponentForTransmission(
|
||||||
|
component
|
||||||
|
);
|
||||||
this.timeout(() => {
|
this.timeout(() => {
|
||||||
for(var observer of this.componentActivationObservers) {
|
for(const observer of this.componentActivationObservers) {
|
||||||
observer.callback(serializedComponent);
|
observer.callback(serializedComponent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -164,7 +182,7 @@ export class DesktopManager {
|
|||||||
/* Used to resolve "sn://" */
|
/* Used to resolve "sn://" */
|
||||||
desktop_setExtServerHost(host) {
|
desktop_setExtServerHost(host) {
|
||||||
this.extServerHost = host;
|
this.extServerHost = host;
|
||||||
this.$rootScope.$broadcast("desktop-did-set-ext-server-host");
|
this.appState.desktopExtensionsReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
desktop_setComponentInstallationSyncHandler(handler) {
|
desktop_setComponentInstallationSyncHandler(handler) {
|
||||||
@@ -183,7 +201,7 @@ export class DesktopManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async desktop_requestBackupFile(callback) {
|
async desktop_requestBackupFile(callback) {
|
||||||
var keys, authParams;
|
let keys, authParams;
|
||||||
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
|
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
|
||||||
keys = this.passcodeManager.keys();
|
keys = this.passcodeManager.keys();
|
||||||
authParams = this.passcodeManager.passcodeAuthParams();
|
authParams = this.passcodeManager.passcodeAuthParams();
|
||||||
@@ -191,11 +209,11 @@ export class DesktopManager {
|
|||||||
keys = await this.authManager.keys();
|
keys = await this.authManager.keys();
|
||||||
authParams = await this.authManager.getAuthParams();
|
authParams = await this.authManager.getAuthParams();
|
||||||
}
|
}
|
||||||
|
const nullOnEmpty = true;
|
||||||
this.modelManager.getAllItemsJSONData(
|
this.modelManager.getAllItemsJSONData(
|
||||||
keys,
|
keys,
|
||||||
authParams,
|
authParams,
|
||||||
true /* return null on empty */
|
nullOnEmpty
|
||||||
).then((data) => {
|
).then((data) => {
|
||||||
callback(data);
|
callback(data);
|
||||||
})
|
})
|
||||||
@@ -206,10 +224,12 @@ export class DesktopManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
desktop_didBeginBackup() {
|
desktop_didBeginBackup() {
|
||||||
this.$rootScope.$broadcast("did-begin-local-backup");
|
this.appState.beganBackupDownload();
|
||||||
}
|
}
|
||||||
|
|
||||||
desktop_didFinishBackup(success) {
|
desktop_didFinishBackup(success) {
|
||||||
this.$rootScope.$broadcast("did-finish-local-backup", {success: success});
|
this.appState.endedBackupDownload({
|
||||||
|
success: success
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,3 +18,4 @@ export { StorageManager } from './storageManager';
|
|||||||
export { SyncManager } from './syncManager';
|
export { SyncManager } from './syncManager';
|
||||||
export { ThemeManager } from './themeManager';
|
export { ThemeManager } from './themeManager';
|
||||||
export { AlertManager } from './alertManager';
|
export { AlertManager } from './alertManager';
|
||||||
|
export { PreferencesManager } from './preferencesManager';
|
||||||
@@ -33,8 +33,12 @@ export class ModelManager extends SFModelManager {
|
|||||||
this.handleSignout();
|
this.handleSignout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findTag(title) {
|
||||||
|
return _.find(this.tags, { title: title });
|
||||||
|
}
|
||||||
|
|
||||||
findOrCreateTagByTitle(title) {
|
findOrCreateTagByTitle(title) {
|
||||||
var tag = _.find(this.tags, {title: title})
|
let tag = this.findTag(title);
|
||||||
if(!tag) {
|
if(!tag) {
|
||||||
tag = this.createItem({content_type: "Tag", content: {title: title}});
|
tag = this.createItem({content_type: "Tag", content: {title: title}});
|
||||||
this.addItem(tag);
|
this.addItem(tag);
|
||||||
@@ -88,9 +92,7 @@ export class ModelManager extends SFModelManager {
|
|||||||
|
|
||||||
removeItemLocally(item, callback) {
|
removeItemLocally(item, callback) {
|
||||||
super.removeItemLocally(item, callback);
|
super.removeItemLocally(item, callback);
|
||||||
|
|
||||||
this.removeItemFromRespectiveArray(item);
|
this.removeItemFromRespectiveArray(item);
|
||||||
|
|
||||||
this.storageManager.deleteModel(item).then(callback);
|
this.storageManager.deleteModel(item).then(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10,8 +10,8 @@ export class NativeExtManager {
|
|||||||
this.syncManager = syncManager;
|
this.syncManager = syncManager;
|
||||||
this.singletonManager = singletonManager;
|
this.singletonManager = singletonManager;
|
||||||
|
|
||||||
this.extensionsManagerIdentifier = "org.standardnotes.extensions-manager";
|
this.extManagerId = "org.standardnotes.extensions-manager";
|
||||||
this.batchManagerIdentifier = "org.standardnotes.batch-manager";
|
this.batchManagerId = "org.standardnotes.batch-manager";
|
||||||
this.systemExtensions = [];
|
this.systemExtensions = [];
|
||||||
|
|
||||||
this.resolveExtensionsManager();
|
this.resolveExtensionsManager();
|
||||||
@@ -25,7 +25,7 @@ export class NativeExtManager {
|
|||||||
resolveExtensionsManager() {
|
resolveExtensionsManager() {
|
||||||
|
|
||||||
let contentTypePredicate = new SFPredicate("content_type", "=", "SN|Component");
|
let contentTypePredicate = new SFPredicate("content_type", "=", "SN|Component");
|
||||||
let packagePredicate = new SFPredicate("package_info.identifier", "=", this.extensionsManagerIdentifier);
|
let packagePredicate = new SFPredicate("package_info.identifier", "=", this.extManagerId);
|
||||||
|
|
||||||
this.singletonManager.registerSingleton([contentTypePredicate, packagePredicate], (resolvedSingleton) => {
|
this.singletonManager.registerSingleton([contentTypePredicate, packagePredicate], (resolvedSingleton) => {
|
||||||
// Resolved Singleton
|
// Resolved Singleton
|
||||||
@@ -58,7 +58,6 @@ export class NativeExtManager {
|
|||||||
}, (valueCallback) => {
|
}, (valueCallback) => {
|
||||||
// Safe to create. Create and return object.
|
// Safe to create. Create and return object.
|
||||||
let url = window._extensions_manager_location;
|
let url = window._extensions_manager_location;
|
||||||
// console.log("Installing Extensions Manager from URL", url);
|
|
||||||
if(!url) {
|
if(!url) {
|
||||||
console.error("window._extensions_manager_location must be set.");
|
console.error("window._extensions_manager_location must be set.");
|
||||||
return;
|
return;
|
||||||
@@ -66,7 +65,7 @@ export class NativeExtManager {
|
|||||||
|
|
||||||
let packageInfo = {
|
let packageInfo = {
|
||||||
name: "Extensions",
|
name: "Extensions",
|
||||||
identifier: this.extensionsManagerIdentifier
|
identifier: this.extManagerId
|
||||||
}
|
}
|
||||||
|
|
||||||
var item = {
|
var item = {
|
||||||
@@ -108,7 +107,7 @@ export class NativeExtManager {
|
|||||||
resolveBatchManager() {
|
resolveBatchManager() {
|
||||||
|
|
||||||
let contentTypePredicate = new SFPredicate("content_type", "=", "SN|Component");
|
let contentTypePredicate = new SFPredicate("content_type", "=", "SN|Component");
|
||||||
let packagePredicate = new SFPredicate("package_info.identifier", "=", this.batchManagerIdentifier);
|
let packagePredicate = new SFPredicate("package_info.identifier", "=", this.batchManagerId);
|
||||||
|
|
||||||
this.singletonManager.registerSingleton([contentTypePredicate, packagePredicate], (resolvedSingleton) => {
|
this.singletonManager.registerSingleton([contentTypePredicate, packagePredicate], (resolvedSingleton) => {
|
||||||
// Resolved Singleton
|
// Resolved Singleton
|
||||||
@@ -134,7 +133,6 @@ export class NativeExtManager {
|
|||||||
}, (valueCallback) => {
|
}, (valueCallback) => {
|
||||||
// Safe to create. Create and return object.
|
// Safe to create. Create and return object.
|
||||||
let url = window._batch_manager_location;
|
let url = window._batch_manager_location;
|
||||||
// console.log("Installing Batch Manager from URL", url);
|
|
||||||
if(!url) {
|
if(!url) {
|
||||||
console.error("window._batch_manager_location must be set.");
|
console.error("window._batch_manager_location must be set.");
|
||||||
return;
|
return;
|
||||||
@@ -142,7 +140,7 @@ export class NativeExtManager {
|
|||||||
|
|
||||||
let packageInfo = {
|
let packageInfo = {
|
||||||
name: "Batch Manager",
|
name: "Batch Manager",
|
||||||
identifier: this.batchManagerIdentifier
|
identifier: this.batchManagerId
|
||||||
}
|
}
|
||||||
|
|
||||||
var item = {
|
var item = {
|
||||||
86
app/assets/javascripts/services/preferencesManager.js
Normal file
86
app/assets/javascripts/services/preferencesManager.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { SFPredicate, SFItem } from 'snjs';
|
||||||
|
|
||||||
|
export const PREF_TAGS_PANEL_WIDTH = 'tagsPanelWidth';
|
||||||
|
export const PREF_NOTES_PANEL_WIDTH = 'notesPanelWidth';
|
||||||
|
export const PREF_EDITOR_WIDTH = 'editorWidth';
|
||||||
|
export const PREF_EDITOR_LEFT = 'editorLeft';
|
||||||
|
export const PREF_EDITOR_MONOSPACE_ENABLED = 'monospaceFont';
|
||||||
|
export const PREF_EDITOR_SPELLCHECK = 'spellcheck';
|
||||||
|
export const PREF_EDITOR_RESIZERS_ENABLED = 'marginResizersEnabled';
|
||||||
|
export const PREF_SORT_NOTES_BY = 'sortBy';
|
||||||
|
export const PREF_SORT_NOTES_REVERSE = 'sortReverse';
|
||||||
|
export const PREF_NOTES_SHOW_ARCHIVED = 'showArchived';
|
||||||
|
export const PREF_NOTES_HIDE_PINNED = 'hidePinned';
|
||||||
|
export const PREF_NOTES_HIDE_NOTE_PREVIEW = 'hideNotePreview';
|
||||||
|
export const PREF_NOTES_HIDE_DATE = 'hideDate';
|
||||||
|
export const PREF_NOTES_HIDE_TAGS = 'hideTags';
|
||||||
|
|
||||||
|
export class PreferencesManager {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(
|
||||||
|
modelManager,
|
||||||
|
singletonManager,
|
||||||
|
appState,
|
||||||
|
syncManager
|
||||||
|
) {
|
||||||
|
this.singletonManager = singletonManager;
|
||||||
|
this.modelManager = modelManager;
|
||||||
|
this.syncManager = syncManager;
|
||||||
|
this.appState = appState;
|
||||||
|
|
||||||
|
this.modelManager.addItemSyncObserver(
|
||||||
|
'user-prefs',
|
||||||
|
'SN|UserPreferences',
|
||||||
|
(allItems, validItems, deletedItems, source, sourceKey) => {
|
||||||
|
this.preferencesDidChange();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
const prefsContentType = 'SN|UserPreferences';
|
||||||
|
const contentTypePredicate = new SFPredicate(
|
||||||
|
'content_type',
|
||||||
|
'=',
|
||||||
|
prefsContentType
|
||||||
|
);
|
||||||
|
this.singletonManager.registerSingleton(
|
||||||
|
[contentTypePredicate],
|
||||||
|
(resolvedSingleton) => {
|
||||||
|
this.userPreferences = resolvedSingleton;
|
||||||
|
},
|
||||||
|
(valueCallback) => {
|
||||||
|
// Safe to create. Create and return object.
|
||||||
|
const prefs = new SFItem({content_type: prefsContentType});
|
||||||
|
this.modelManager.addItem(prefs);
|
||||||
|
this.modelManager.setItemDirty(prefs);
|
||||||
|
this.syncManager.sync();
|
||||||
|
valueCallback(prefs);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
preferencesDidChange() {
|
||||||
|
this.appState.setUserPreferences(this.userPreferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
syncUserPreferences() {
|
||||||
|
if(this.userPreferences) {
|
||||||
|
this.modelManager.setItemDirty(this.userPreferences);
|
||||||
|
this.syncManager.sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue(key, defaultValue) {
|
||||||
|
if(!this.userPreferences) { return defaultValue; }
|
||||||
|
const value = this.userPreferences.getAppDataItem(key);
|
||||||
|
return (value !== undefined && value != null) ? value : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserPrefValue(key, value, sync) {
|
||||||
|
this.userPreferences.setAppDataItem(key, value);
|
||||||
|
if(sync) {
|
||||||
|
this.syncUserPreferences();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,27 +41,29 @@ export class PrivilegesManager extends SFPrivilegesManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
presentPrivilegesModal(action, onSuccess, onCancel) {
|
async presentPrivilegesModal(action, onSuccess, onCancel) {
|
||||||
if(this.authenticationInProgress()) {
|
if (this.authenticationInProgress()) {
|
||||||
onCancel && onCancel();
|
onCancel && onCancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let customSuccess = () => {
|
const customSuccess = async () => {
|
||||||
onSuccess && onSuccess();
|
onSuccess && await onSuccess();
|
||||||
|
this.currentAuthenticationElement = null;
|
||||||
|
}
|
||||||
|
const customCancel = async () => {
|
||||||
|
onCancel && await onCancel();
|
||||||
this.currentAuthenticationElement = null;
|
this.currentAuthenticationElement = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let customCancel = () => {
|
const scope = this.$rootScope.$new(true);
|
||||||
onCancel && onCancel();
|
|
||||||
this.currentAuthenticationElement = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var scope = this.$rootScope.$new(true);
|
|
||||||
scope.action = action;
|
scope.action = action;
|
||||||
scope.onSuccess = customSuccess;
|
scope.onSuccess = customSuccess;
|
||||||
scope.onCancel = customCancel;
|
scope.onCancel = customCancel;
|
||||||
var el = this.$compile( "<privileges-auth-modal action='action' on-success='onSuccess' on-cancel='onCancel' class='sk-modal'></privileges-auth-modal>" )(scope);
|
const el = this.$compile(`
|
||||||
|
<privileges-auth-modal action='action' on-success='onSuccess'
|
||||||
|
on-cancel='onCancel' class='sk-modal'></privileges-auth-modal>
|
||||||
|
`)(scope);
|
||||||
angular.element(document.body).append(el);
|
angular.element(document.body).append(el);
|
||||||
|
|
||||||
this.currentAuthenticationElement = el;
|
this.currentAuthenticationElement = el;
|
||||||
@@ -69,7 +71,7 @@ export class PrivilegesManager extends SFPrivilegesManager {
|
|||||||
|
|
||||||
presentPrivilegesManagementModal() {
|
presentPrivilegesManagementModal() {
|
||||||
var scope = this.$rootScope.$new(true);
|
var scope = this.$rootScope.$new(true);
|
||||||
var el = this.$compile( "<privileges-management-modal class='sk-modal'></privileges-management-modal>")(scope);
|
var el = this.$compile("<privileges-management-modal class='sk-modal'></privileges-management-modal>")(scope);
|
||||||
angular.element(document.body).append(el);
|
angular.element(document.body).append(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ export class StatusManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
notifyObservers() {
|
notifyObservers() {
|
||||||
for(let observer of this.observers) {
|
for(const observer of this.observers) {
|
||||||
observer(this.getStatusString());
|
observer(this.getStatusString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,19 @@ import _ from 'lodash';
|
|||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import { SNTheme, SFItemParams } from 'snjs';
|
import { SNTheme, SFItemParams } from 'snjs';
|
||||||
import { StorageManager } from './storageManager';
|
import { StorageManager } from './storageManager';
|
||||||
|
import {
|
||||||
|
APP_STATE_EVENT_DESKTOP_EXTS_READY
|
||||||
|
} from '@/state';
|
||||||
|
|
||||||
export class ThemeManager {
|
export class ThemeManager {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor(componentManager, desktopManager, storageManager, passcodeManager, $rootScope) {
|
constructor(
|
||||||
|
componentManager,
|
||||||
|
desktopManager,
|
||||||
|
storageManager,
|
||||||
|
passcodeManager,
|
||||||
|
appState
|
||||||
|
) {
|
||||||
this.componentManager = componentManager;
|
this.componentManager = componentManager;
|
||||||
this.storageManager = storageManager;
|
this.storageManager = storageManager;
|
||||||
this.desktopManager = desktopManager;
|
this.desktopManager = desktopManager;
|
||||||
@@ -22,9 +31,11 @@ export class ThemeManager {
|
|||||||
this.cacheThemes();
|
this.cacheThemes();
|
||||||
})
|
})
|
||||||
|
|
||||||
if(desktopManager.isDesktop) {
|
if (desktopManager.isDesktop) {
|
||||||
$rootScope.$on("desktop-did-set-ext-server-host", () => {
|
appState.addObserver((eventName, data) => {
|
||||||
this.activateCachedThemes();
|
if (eventName === APP_STATE_EVENT_DESKTOP_EXTS_READY) {
|
||||||
|
this.activateCachedThemes();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.activateCachedThemes();
|
this.activateCachedThemes();
|
||||||
@@ -32,9 +43,9 @@ export class ThemeManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
activateCachedThemes() {
|
activateCachedThemes() {
|
||||||
let cachedThemes = this.getCachedThemes();
|
const cachedThemes = this.getCachedThemes();
|
||||||
let writeToCache = false;
|
const writeToCache = false;
|
||||||
for(var theme of cachedThemes) {
|
for (const theme of cachedThemes) {
|
||||||
this.activateTheme(theme, writeToCache);
|
this.activateTheme(theme, writeToCache);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +53,7 @@ export class ThemeManager {
|
|||||||
registerObservers() {
|
registerObservers() {
|
||||||
this.desktopManager.registerUpdateObserver((component) => {
|
this.desktopManager.registerUpdateObserver((component) => {
|
||||||
// Reload theme if active
|
// Reload theme if active
|
||||||
if(component.active && component.isTheme()) {
|
if (component.active && component.isTheme()) {
|
||||||
this.deactivateTheme(component);
|
this.deactivateTheme(component);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.activateTheme(component);
|
this.activateTheme(component);
|
||||||
@@ -50,13 +61,17 @@ export class ThemeManager {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.componentManager.registerHandler({identifier: "themeManager", areas: ["themes"], activationHandler: (component) => {
|
this.componentManager.registerHandler({
|
||||||
if(component.active) {
|
identifier: "themeManager",
|
||||||
this.activateTheme(component);
|
areas: ["themes"],
|
||||||
} else {
|
activationHandler: (component) => {
|
||||||
this.deactivateTheme(component);
|
if (component.active) {
|
||||||
|
this.activateTheme(component);
|
||||||
|
} else {
|
||||||
|
this.deactivateTheme(component);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
hasActiveTheme() {
|
hasActiveTheme() {
|
||||||
@@ -65,8 +80,8 @@ export class ThemeManager {
|
|||||||
|
|
||||||
deactivateAllThemes() {
|
deactivateAllThemes() {
|
||||||
var activeThemes = this.componentManager.getActiveThemes();
|
var activeThemes = this.componentManager.getActiveThemes();
|
||||||
for(var theme of activeThemes) {
|
for (var theme of activeThemes) {
|
||||||
if(theme) {
|
if (theme) {
|
||||||
this.componentManager.deactivateComponent(theme);
|
this.componentManager.deactivateComponent(theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,7 +90,7 @@ export class ThemeManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
activateTheme(theme, writeToCache = true) {
|
activateTheme(theme, writeToCache = true) {
|
||||||
if(_.find(this.activeThemes, {uuid: theme.uuid})) {
|
if (_.find(this.activeThemes, { uuid: theme.uuid })) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,19 +105,19 @@ export class ThemeManager {
|
|||||||
link.id = theme.uuid;
|
link.id = theme.uuid;
|
||||||
document.getElementsByTagName("head")[0].appendChild(link);
|
document.getElementsByTagName("head")[0].appendChild(link);
|
||||||
|
|
||||||
if(writeToCache) {
|
if (writeToCache) {
|
||||||
this.cacheThemes();
|
this.cacheThemes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deactivateTheme(theme) {
|
deactivateTheme(theme) {
|
||||||
var element = document.getElementById(theme.uuid);
|
var element = document.getElementById(theme.uuid);
|
||||||
if(element) {
|
if (element) {
|
||||||
element.disabled = true;
|
element.disabled = true;
|
||||||
element.parentNode.removeChild(element);
|
element.parentNode.removeChild(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
_.remove(this.activeThemes, {uuid: theme.uuid});
|
_.remove(this.activeThemes, { uuid: theme.uuid });
|
||||||
|
|
||||||
this.cacheThemes();
|
this.cacheThemes();
|
||||||
}
|
}
|
||||||
@@ -123,7 +138,7 @@ export class ThemeManager {
|
|||||||
|
|
||||||
getCachedThemes() {
|
getCachedThemes() {
|
||||||
let cachedThemes = this.storageManager.getItemSync(ThemeManager.CachedThemesKey, StorageManager.Fixed);
|
let cachedThemes = this.storageManager.getItemSync(ThemeManager.CachedThemesKey, StorageManager.Fixed);
|
||||||
if(cachedThemes) {
|
if (cachedThemes) {
|
||||||
let parsed = JSON.parse(cachedThemes);
|
let parsed = JSON.parse(cachedThemes);
|
||||||
return parsed.map((theme) => {
|
return parsed.map((theme) => {
|
||||||
return new SNTheme(theme);
|
return new SNTheme(theme);
|
||||||
128
app/assets/javascripts/state.js
Normal file
128
app/assets/javascripts/state.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { PrivilegesManager } from '@/services/privilegesManager';
|
||||||
|
|
||||||
|
export const APP_STATE_EVENT_TAG_CHANGED = 1;
|
||||||
|
export const APP_STATE_EVENT_NOTE_CHANGED = 2;
|
||||||
|
export const APP_STATE_EVENT_PREFERENCES_CHANGED = 3;
|
||||||
|
export const APP_STATE_EVENT_PANEL_RESIZED = 4;
|
||||||
|
export const APP_STATE_EVENT_EDITOR_FOCUSED = 5;
|
||||||
|
export const APP_STATE_EVENT_BEGAN_BACKUP_DOWNLOAD = 6;
|
||||||
|
export const APP_STATE_EVENT_ENDED_BACKUP_DOWNLOAD = 7;
|
||||||
|
export const APP_STATE_EVENT_DESKTOP_EXTS_READY = 8;
|
||||||
|
|
||||||
|
export class AppState {
|
||||||
|
|
||||||
|
/* @ngInject */
|
||||||
|
constructor($timeout, privilegesManager) {
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
this.privilegesManager = privilegesManager;
|
||||||
|
this.observers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
addObserver(callback) {
|
||||||
|
this.observers.push(callback);
|
||||||
|
return callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
async notifyEvent(eventName, data) {
|
||||||
|
/**
|
||||||
|
* Timeout is particullary important so we can give all initial
|
||||||
|
* controllers a chance to construct before propogting any events *
|
||||||
|
*/
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.$timeout(async () => {
|
||||||
|
for(const callback of this.observers) {
|
||||||
|
await callback(eventName, data);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedTag(tag) {
|
||||||
|
if(this.selectedTag === tag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previousTag = this.selectedTag;
|
||||||
|
this.selectedTag = tag;
|
||||||
|
this.notifyEvent(
|
||||||
|
APP_STATE_EVENT_TAG_CHANGED,
|
||||||
|
{previousTag: previousTag}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSelectedNote(note) {
|
||||||
|
const run = async () => {
|
||||||
|
const previousNote = this.selectedNote;
|
||||||
|
this.selectedNote = note;
|
||||||
|
await this.notifyEvent(
|
||||||
|
APP_STATE_EVENT_NOTE_CHANGED,
|
||||||
|
{ previousNote: previousNote }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (note && note.content.protected &&
|
||||||
|
await this.privilegesManager.actionRequiresPrivilege(
|
||||||
|
PrivilegesManager.ActionViewProtectedNotes
|
||||||
|
)) {
|
||||||
|
this.privilegesManager.presentPrivilegesModal(
|
||||||
|
PrivilegesManager.ActionViewProtectedNotes,
|
||||||
|
run
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedTag() {
|
||||||
|
return this.selectedTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedNote() {
|
||||||
|
return this.selectedNote;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserPreferences(preferences) {
|
||||||
|
this.userPreferences = preferences;
|
||||||
|
this.notifyEvent(
|
||||||
|
APP_STATE_EVENT_PREFERENCES_CHANGED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
panelDidResize({name, collapsed}) {
|
||||||
|
this.notifyEvent(
|
||||||
|
APP_STATE_EVENT_PANEL_RESIZED,
|
||||||
|
{
|
||||||
|
panel: name,
|
||||||
|
collapsed: collapsed
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
editorDidFocus() {
|
||||||
|
this.notifyEvent(
|
||||||
|
APP_STATE_EVENT_EDITOR_FOCUSED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beganBackupDownload() {
|
||||||
|
this.notifyEvent(
|
||||||
|
APP_STATE_EVENT_BEGAN_BACKUP_DOWNLOAD
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
endedBackupDownload({success}) {
|
||||||
|
this.notifyEvent(
|
||||||
|
APP_STATE_EVENT_ENDED_BACKUP_DOWNLOAD,
|
||||||
|
{success: success}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the desktop appplication extension server is ready.
|
||||||
|
*/
|
||||||
|
desktopExtensionsReady() {
|
||||||
|
this.notifyEvent(
|
||||||
|
APP_STATE_EVENT_DESKTOP_EXTS_READY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
51
app/assets/javascripts/strings.js
Normal file
51
app/assets/javascripts/strings.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/** @generic */
|
||||||
|
export const STRING_SESSION_EXPIRED = "Your session has expired. New changes will not be pulled in. Please sign out and sign back in to refresh your session.";
|
||||||
|
export const STRING_DEFAULT_FILE_ERROR = "Please use FileSafe or the Bold Editor to attach images and files. Learn more at standardnotes.org/filesafe.";
|
||||||
|
export const STRING_GENERIC_SYNC_ERROR = "There was an error syncing. Please try again. If all else fails, try signing out and signing back in.";
|
||||||
|
export function StringSyncException(data) {
|
||||||
|
return `There was an error while trying to save your items. Please contact support and share this message: ${data}.`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @footer */
|
||||||
|
export const STRING_NEW_UPDATE_READY = "A new update is ready to install. Please use the top-level 'Updates' menu to manage installation.";
|
||||||
|
|
||||||
|
/** @tags */
|
||||||
|
export const STRING_DELETE_TAG = "Are you sure you want to delete this tag? Note: deleting a tag will not delete its notes.";
|
||||||
|
|
||||||
|
/** @editor */
|
||||||
|
export const STRING_DELETED_NOTE = "The note you are attempting to edit has been deleted, and is awaiting sync. Changes you make will be disregarded.";
|
||||||
|
export const STRING_INVALID_NOTE = "The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note.";
|
||||||
|
export const STRING_ELLIPSES = "...";
|
||||||
|
export const STRING_GENERIC_SAVE_ERROR = "There was an error saving your note. Please try again.";
|
||||||
|
export const STRING_DELETE_PLACEHOLDER_ATTEMPT = "This note is a placeholder and cannot be deleted. To remove from your list, simply navigate to a different note.";
|
||||||
|
export const STRING_DELETE_LOCKED_ATTEMPT = "This note is locked. If you'd like to delete it, unlock it, and try again.";
|
||||||
|
export function StringDeleteNote({title, permanently}) {
|
||||||
|
return permanently
|
||||||
|
? `Are you sure you want to permanently delete ${title}?`
|
||||||
|
: `Are you sure you want to move ${title} to the trash?`;
|
||||||
|
}
|
||||||
|
export function StringEmptyTrash({count}) {
|
||||||
|
return `Are you sure you want to permanently delete ${count} note(s)?`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @account */
|
||||||
|
export const STRING_ACCOUNT_MENU_UNCHECK_MERGE = "Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?";
|
||||||
|
export const STRING_SIGN_OUT_CONFIRMATION = "Are you sure you want to end your session? This will delete all local items and extensions.";
|
||||||
|
export const STRING_ERROR_DECRYPTING_IMPORT = "There was an error decrypting your items. Make sure the password you entered is correct and try again.";
|
||||||
|
export const STRING_E2E_ENABLED = "End-to-end encryption is enabled. Your data is encrypted on your device first, then synced to your private cloud.";
|
||||||
|
export const STRING_LOCAL_ENC_ENABLED = "Encryption is enabled. Your data is encrypted using your passcode before it is saved to your device storage.";
|
||||||
|
export const STRING_ENC_NOT_ENABLED = "Encryption is not enabled. Sign in, register, or add a passcode lock to enable encryption.";
|
||||||
|
export const STRING_IMPORT_SUCCESS = "Your data has been successfully imported.";
|
||||||
|
export const STRING_REMOVE_PASSCODE_CONFIRMATION = "Are you sure you want to remove your local passcode?";
|
||||||
|
export const STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM = " This will remove encryption from your local data.";
|
||||||
|
export const STRING_NON_MATCHING_PASSCODES = "The two passcodes you entered do not match. Please try again.";
|
||||||
|
export const STRING_NON_MATCHING_PASSWORDS = "The two passwords you entered do not match. Please try again.";
|
||||||
|
export const STRING_GENERATING_LOGIN_KEYS = "Generating Login Keys...";
|
||||||
|
export const STRING_GENERATING_REGISTER_KEYS = "Generating Account Keys...";
|
||||||
|
export const STRING_INVALID_IMPORT_FILE = "Unable to open file. Ensure it is a proper JSON file and try again.";
|
||||||
|
export function StringImportError({errorCount}) {
|
||||||
|
return `Import complete. ${errorCount} items were not imported because there was an error decrypting them. Make sure the password is correct and try again.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @password_change */
|
||||||
|
export const STRING_FAILED_PASSWORD_CHANGE = "There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.";
|
||||||
@@ -16,6 +16,10 @@ export function parametersFromURL(url) {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isNullOrUndefined(value) {
|
||||||
|
return value === null || value === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function getPlatformString() {
|
export function getPlatformString() {
|
||||||
try {
|
try {
|
||||||
var platform = navigator.platform.toLowerCase();
|
var platform = navigator.platform.toLowerCase();
|
||||||
@@ -35,6 +39,23 @@ export function getPlatformString() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Via https://davidwalsh.name/javascript-debounce-function */
|
||||||
|
export function debounce(func, wait, immediate) {
|
||||||
|
var timeout;
|
||||||
|
return function () {
|
||||||
|
const context = this;
|
||||||
|
const args = arguments;
|
||||||
|
const later = function () {
|
||||||
|
timeout = null;
|
||||||
|
if (!immediate) func.apply(context, args);
|
||||||
|
};
|
||||||
|
const callNow = immediate && !timeout;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
if (callNow) func.apply(context, args);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function isDesktopApplication() {
|
export function isDesktopApplication() {
|
||||||
return window.isElectron;
|
return window.isElectron;
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user