diff --git a/.babelrc b/.babelrc
index 2b95d25e0..b61ae9684 100644
--- a/.babelrc
+++ b/.babelrc
@@ -9,6 +9,7 @@
],
"plugins": [
"@babel/plugin-transform-runtime",
+ "@babel/plugin-proposal-class-properties",
"angularjs-annotate"
]
}
diff --git a/.eslintrc b/.eslintrc
index 41f74d004..137b28126 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,7 +1,11 @@
{
- "extends": ["semistandard", "prettier"],
+ "extends": ["eslint:recommended", "semistandard", "prettier"],
+ "parser": "babel-eslint",
"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": {
"browser": true
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 000000000..8056bfbc9
--- /dev/null
+++ b/.vscode/launch.json
@@ -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}"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/app/assets/javascripts/app/app.js b/app/assets/javascripts/app.js
similarity index 95%
rename from app/assets/javascripts/app/app.js
rename to app/assets/javascripts/app.js
index 7a17502e6..e1d67d5e7 100644
--- a/app/assets/javascripts/app/app.js
+++ b/app/assets/javascripts/app.js
@@ -4,7 +4,11 @@ import angular from 'angular';
import { configRoutes } from './routes';
import {
- Home,
+ AppState
+} from './state';
+
+import {
+ Root,
TagsPanel,
NotesPanel,
EditorPanel,
@@ -65,7 +69,8 @@ import {
StorageManager,
SyncManager,
ThemeManager,
- AlertManager
+ AlertManager,
+ PreferencesManager
} from './services';
angular.module('app', ['ngSanitize']);
@@ -79,7 +84,7 @@ angular
// Controllers
angular
.module('app')
- .directive('home', () => new Home())
+ .directive('root', () => new Root())
.directive('tagsPanel', () => new TagsPanel())
.directive('notesPanel', () => new NotesPanel())
.directive('editorPanel', () => new EditorPanel())
@@ -138,6 +143,8 @@ angular
// Services
angular
.module('app')
+ .service('appState', AppState)
+ .service('preferencesManager', PreferencesManager)
.service('actionsManager', ActionsManager)
.service('archiveManager', ArchiveManager)
.service('authManager', AuthManager)
diff --git a/app/assets/javascripts/app/controllers/editor.js b/app/assets/javascripts/app/controllers/editor.js
deleted file mode 100644
index 7968ff405..000000000
--- a/app/assets/javascripts/app/controllers/editor.js
+++ /dev/null
@@ -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;
- }
- });
- };
- }
-}
diff --git a/app/assets/javascripts/app/controllers/footer.js b/app/assets/javascripts/app/controllers/footer.js
deleted file mode 100644
index 30544f7b1..000000000
--- a/app/assets/javascripts/app/controllers/footer.js
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/app/assets/javascripts/app/controllers/home.js b/app/assets/javascripts/app/controllers/home.js
deleted file mode 100644
index 980cb20ac..000000000
--- a/app/assets/javascripts/app/controllers/home.js
+++ /dev/null
@@ -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();
- }
- }
-}
diff --git a/app/assets/javascripts/app/controllers/index.js b/app/assets/javascripts/app/controllers/index.js
deleted file mode 100644
index 00936ae3e..000000000
--- a/app/assets/javascripts/app/controllers/index.js
+++ /dev/null
@@ -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';
diff --git a/app/assets/javascripts/app/controllers/lockScreen.js b/app/assets/javascripts/app/controllers/lockScreen.js
deleted file mode 100644
index 60c7842c3..000000000
--- a/app/assets/javascripts/app/controllers/lockScreen.js
+++ /dev/null
@@ -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();
- })
- }})
- }
- }
-}
diff --git a/app/assets/javascripts/app/controllers/notes.js b/app/assets/javascripts/app/controllers/notes.js
deleted file mode 100644
index 3cdc5003e..000000000
--- a/app/assets/javascripts/app/controllers/notes.js
+++ /dev/null
@@ -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()};
- }
- })
- }
-}
diff --git a/app/assets/javascripts/app/controllers/tags.js b/app/assets/javascripts/app/controllers/tags.js
deleted file mode 100644
index fd55d06a5..000000000
--- a/app/assets/javascripts/app/controllers/tags.js
+++ /dev/null
@@ -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]);
- }
- }
-}
diff --git a/app/assets/javascripts/app/directives/views/accountMenu.js b/app/assets/javascripts/app/directives/views/accountMenu.js
deleted file mode 100644
index e95836034..000000000
--- a/app/assets/javascripts/app/directives/views/accountMenu.js
+++ /dev/null
@@ -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();
- }
- }
-}
diff --git a/app/assets/javascripts/app/directives/views/actionsMenu.js b/app/assets/javascripts/app/directives/views/actionsMenu.js
deleted file mode 100644
index 88abd4305..000000000
--- a/app/assets/javascripts/app/directives/views/actionsMenu.js
+++ /dev/null
@@ -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
- }
- })
- }
- }
-}
diff --git a/app/assets/javascripts/app/directives/views/componentModal.js b/app/assets/javascripts/app/directives/views/componentModal.js
deleted file mode 100644
index a465fc282..000000000
--- a/app/assets/javascripts/app/directives/views/componentModal.js
+++ /dev/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();
- }
- }
-}
diff --git a/app/assets/javascripts/app/directives/views/componentView.js b/app/assets/javascripts/app/directives/views/componentView.js
deleted file mode 100644
index d05764f00..000000000
--- a/app/assets/javascripts/app/directives/views/componentView.js
+++ /dev/null
@@ -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();
- });
- }
-}
diff --git a/app/assets/javascripts/app/directives/views/conflictResolutionModal.js b/app/assets/javascripts/app/directives/views/conflictResolutionModal.js
deleted file mode 100644
index cf027ee74..000000000
--- a/app/assets/javascripts/app/directives/views/conflictResolutionModal.js
+++ /dev/null
@@ -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();
- }
- }
-}
diff --git a/app/assets/javascripts/app/directives/views/editorMenu.js b/app/assets/javascripts/app/directives/views/editorMenu.js
deleted file mode 100644
index 6dba895ba..000000000
--- a/app/assets/javascripts/app/directives/views/editorMenu.js
+++ /dev/null
@@ -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;
- }
- }
- }
-}
diff --git a/app/assets/javascripts/app/directives/views/inputModal.js b/app/assets/javascripts/app/directives/views/inputModal.js
deleted file mode 100644
index ded5b8e93..000000000
--- a/app/assets/javascripts/app/directives/views/inputModal.js
+++ /dev/null
@@ -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();
- }
- }
-}
diff --git a/app/assets/javascripts/app/directives/views/panelResizer.js b/app/assets/javascripts/app/directives/views/panelResizer.js
deleted file mode 100644
index 06b291925..000000000
--- a/app/assets/javascripts/app/directives/views/panelResizer.js
+++ /dev/null
@@ -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("
")($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);
- };
-};
diff --git a/app/assets/javascripts/app/directives/views/passwordWizard.js b/app/assets/javascripts/app/directives/views/passwordWizard.js
deleted file mode 100644
index f5af60373..000000000
--- a/app/assets/javascripts/app/directives/views/passwordWizard.js
+++ /dev/null
@@ -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));
- }
- })
- }
- }
-}
diff --git a/app/assets/javascripts/app/directives/views/permissionsModal.js b/app/assets/javascripts/app/directives/views/permissionsModal.js
deleted file mode 100644
index 0b15b4e27..000000000
--- a/app/assets/javascripts/app/directives/views/permissionsModal.js
+++ /dev/null
@@ -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) {
-
- }
-}
diff --git a/app/assets/javascripts/app/directives/views/privilegesAuthModal.js b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js
deleted file mode 100644
index 1de610591..000000000
--- a/app/assets/javascripts/app/directives/views/privilegesAuthModal.js
+++ /dev/null
@@ -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;
- }
- })
- })
- }
- }
-}
diff --git a/app/assets/javascripts/app/directives/views/privilegesManagementModal.js b/app/assets/javascripts/app/directives/views/privilegesManagementModal.js
deleted file mode 100644
index 630f498b4..000000000
--- a/app/assets/javascripts/app/directives/views/privilegesManagementModal.js
+++ /dev/null
@@ -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();
- }
- }
-}
diff --git a/app/assets/javascripts/app/directives/views/revisionPreviewModal.js b/app/assets/javascripts/app/directives/views/revisionPreviewModal.js
deleted file mode 100644
index 8bf436b66..000000000
--- a/app/assets/javascripts/app/directives/views/revisionPreviewModal.js
+++ /dev/null
@@ -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();
- }
- }
- }
-}
diff --git a/app/assets/javascripts/app/directives/views/sessionHistoryMenu.js b/app/assets/javascripts/app/directives/views/sessionHistoryMenu.js
deleted file mode 100644
index 974a6d789..000000000
--- a/app/assets/javascripts/app/directives/views/sessionHistoryMenu.js
+++ /dev/null
@@ -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;
- })
- });
- }
- }
-}
diff --git a/app/assets/javascripts/app/directives/views/syncResolutionMenu.js b/app/assets/javascripts/app/directives/views/syncResolutionMenu.js
deleted file mode 100644
index ff210d2db..000000000
--- a/app/assets/javascripts/app/directives/views/syncResolutionMenu.js
+++ /dev/null
@@ -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;
- }
- })
- }
- }
-}
diff --git a/app/assets/javascripts/app/services/actionsManager.js b/app/assets/javascripts/app/services/actionsManager.js
deleted file mode 100644
index 065647024..000000000
--- a/app/assets/javascripts/app/services/actionsManager.js
+++ /dev/null
@@ -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( "" )(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( "" )(scope);
- angular.element(document.body).append(el);
- }
-}
diff --git a/app/assets/javascripts/app/services/alertManager.js b/app/assets/javascripts/app/services/alertManager.js
deleted file mode 100644
index b2e6fc4ee..000000000
--- a/app/assets/javascripts/app/services/alertManager.js
+++ /dev/null
@@ -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();
- })
- }
-}
diff --git a/app/assets/javascripts/controllers/abstract/pure_ctrl.js b/app/assets/javascripts/controllers/abstract/pure_ctrl.js
new file mode 100644
index 000000000..62ea6b79d
--- /dev/null
+++ b/app/assets/javascripts/controllers/abstract/pure_ctrl.js
@@ -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));
+ }
+}
\ No newline at end of file
diff --git a/app/assets/javascripts/controllers/constants.js b/app/assets/javascripts/controllers/constants.js
new file mode 100644
index 000000000..41466e047
--- /dev/null
+++ b/app/assets/javascripts/controllers/constants.js
@@ -0,0 +1,2 @@
+export const PANEL_NAME_NOTES = 'notes';
+export const PANEL_NAME_TAGS = 'tags';
diff --git a/app/assets/javascripts/controllers/editor.js b/app/assets/javascripts/controllers/editor.js
new file mode 100644
index 000000000..b42fc206d
--- /dev/null
+++ b/app/assets/javascripts/controllers/editor.js
@@ -0,0 +1,1189 @@
+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';
+import { PureCtrl } from '@Controllers';
+import {
+ APP_STATE_EVENT_NOTE_CHANGED,
+ APP_STATE_EVENT_PREFERENCES_CHANGED
+} from '@/state';
+import {
+ STRING_DELETED_NOTE,
+ STRING_INVALID_NOTE,
+ STRING_ELLIPSES,
+ STRING_GENERIC_SAVE_ERROR,
+ STRING_DELETE_PLACEHOLDER_ATTEMPT,
+ STRING_DELETE_LOCKED_ATTEMPT,
+ StringDeleteNote,
+ StringEmptyTrash
+} from '@/strings'
+import {
+ PREF_EDITOR_WIDTH,
+ PREF_EDITOR_LEFT,
+ PREF_EDITOR_MONOSPACE_ENABLED,
+ PREF_EDITOR_SPELLCHECK,
+ PREF_EDITOR_RESIZERS_ENABLED
+} from '@/services/preferencesManager'
+
+const NOTE_PREVIEW_CHAR_LIMIT = 80;
+const MINIMUM_STATUS_DURATION = 400;
+const SAVE_TIMEOUT_DEBOUNCE = 350;
+const SAVE_TIMEOUT_NO_DEBOUNCE = 100;
+const EDITOR_DEBOUNCE = 200;
+
+const APP_DATA_KEY_PINNED = 'pinned';
+const APP_DATA_KEY_LOCKED = 'locked';
+const APP_DATA_KEY_ARCHIVED = 'archived';
+const APP_DATA_KEY_PREFERS_PLAIN_EDITOR = 'prefersPlainEditor';
+
+const ELEMENT_ID_NOTE_TEXT_EDITOR = 'note-text-editor';
+const ELEMENT_ID_NOTE_TITLE_EDITOR = 'note-title-editor';
+const ELEMENT_ID_EDITOR_CONTENT = 'editor-content';
+const ELEMENT_ID_NOTE_TAGS_COMPONENT_CONTAINER = 'note-tags-component-container';
+
+const DESKTOP_MONOSPACE_FAMILY = `Menlo,Consolas,'DejaVu Sans Mono',monospace`;
+const WEB_MONOSPACE_FAMILY = `monospace`;
+const SANS_SERIF_FAMILY = `inherit`;
+
+class EditorCtrl extends PureCtrl {
+ /* @ngInject */
+ constructor(
+ $timeout,
+ $rootScope,
+ alertManager,
+ appState,
+ authManager,
+ actionsManager,
+ componentManager,
+ desktopManager,
+ keyboardManager,
+ modelManager,
+ preferencesManager,
+ privilegesManager,
+ sessionHistory /** Unused below, required to load globally */,
+ syncManager,
+ ) {
+ super($timeout);
+ this.$rootScope = $rootScope;
+ this.alertManager = alertManager;
+ this.appState = appState;
+ this.actionsManager = actionsManager;
+ this.authManager = authManager;
+ this.componentManager = componentManager;
+ this.desktopManager = desktopManager;
+ this.keyboardManager = keyboardManager;
+ this.modelManager = modelManager;
+ this.preferencesManager = preferencesManager;
+ this.privilegesManager = privilegesManager;
+ this.syncManager = syncManager;
+
+ this.state = {
+ componentStack: [],
+ editorDebounce: EDITOR_DEBOUNCE,
+ isDesktop: isDesktopApplication(),
+ spellcheck: true
+ }
+
+ this.leftResizeControl = {};
+ this.rightResizeControl = {};
+
+ this.addAppStateObserver();
+ this.addSyncEventHandler();
+ this.addSyncStatusObserver();
+ this.addMappingObservers();
+ this.registerComponentHandler();
+ this.registerKeyboardShortcuts();
+
+ /** Used by .pug template */
+ this.prefKeyMonospace = PREF_EDITOR_MONOSPACE_ENABLED;
+ this.prefKeySpellcheck = PREF_EDITOR_SPELLCHECK;
+ this.prefKeyMarginResizers = PREF_EDITOR_RESIZERS_ENABLED;
+ }
+
+ addAppStateObserver() {
+ this.appState.addObserver((eventName, data) => {
+ if (eventName === APP_STATE_EVENT_NOTE_CHANGED) {
+ this.handleNoteSelectionChange(
+ this.appState.getSelectedNote(),
+ data.previousNote
+ );
+ } else if (eventName === APP_STATE_EVENT_PREFERENCES_CHANGED) {
+ this.loadPreferences();
+ }
+ })
+ }
+
+ async handleNoteSelectionChange(note, previousNote) {
+ this.setState({
+ note: this.appState.getSelectedNote(),
+ showExtensions: false,
+ showOptionsMenu: false,
+ altKeyDown: false,
+ noteStatus: null
+ })
+ if (!note) {
+ return;
+ }
+ const associatedEditor = this.editorForNote(note);
+ if (associatedEditor && associatedEditor !== this.state.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.setState({
+ noteReady: false,
+ selectedEditor: associatedEditor
+ });
+ } else if (!associatedEditor) {
+ /** No editor */
+ this.setState({
+ selectedEditor: null
+ });
+ }
+ await this.setState({
+ noteReady: true,
+ });
+ this.reloadTagsString();
+ this.loadPreferences();
+
+ if (note.safeText().length === 0 && note.dummy) {
+ this.focusTitle(100);
+ }
+ if (previousNote && previousNote !== note) {
+ if (previousNote.dummy) {
+ this.performNoteDeletion(previousNote);
+ }
+ }
+
+ this.reloadComponentContext();
+ }
+
+ addMappingObservers() {
+ this.modelManager.addItemSyncObserver(
+ 'editor-note-observer',
+ 'Note',
+ (allItems, validItems, deletedItems, source) => {
+ if (!this.state.note) {
+ return;
+ }
+ if (this.state.note.deleted || this.state.note.content.trashed) {
+ return;
+ }
+ if (!SFModelManager.isMappingSourceRetrieved(source)) {
+ return;
+ }
+ const matchingNote = allItems.find((item) => {
+ return item.uuid === this.state.note.uuid;
+ });
+ if (!matchingNote) {
+ return;
+ }
+ this.reloadTagsString();
+ });
+
+ this.modelManager.addItemSyncObserver(
+ 'editor-tag-observer',
+ 'Tag',
+ (allItems, validItems, deletedItems, source) => {
+ if (!this.state.note) {
+ return;
+ }
+ for (const tag of allItems) {
+ if (
+ !this.state.note.savedTagsString ||
+ tag.deleted ||
+ tag.hasRelationshipWithItem(this.state.note)
+ ) {
+ this.reloadTagsString();
+ break;
+ }
+ }
+ });
+
+ this.modelManager.addItemSyncObserver(
+ 'editor-component-observer',
+ 'SN|Component',
+ (allItems, validItems, deletedItems, source) => {
+ if (!this.state.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 */
+ const editors = allItems.filter(function (item) {
+ return item.isEditor();
+ });
+ if (editors.length === 0) {
+ return;
+ }
+ /** Find the most recent editor for note */
+ const editor = this.editorForNote(this.state.note);
+ this.setState({
+ selectedEditor: editor
+ })
+ if (!editor) {
+ this.reloadFont();
+ }
+ });
+ }
+
+ addSyncEventHandler() {
+ this.syncManager.addEventHandler((eventName, data) => {
+ if (!this.state.note) {
+ return;
+ }
+ if (eventName === 'sync:taking-too-long') {
+ this.setState({
+ syncTakingTooLong: true
+ })
+ } else if (eventName === 'sync:completed') {
+ this.setState({
+ syncTakingTooLong: false
+ })
+ if (this.state.note.dirty) {
+ /** if we're still dirty, don't change status, a sync is likely upcoming. */
+ } else {
+ const savedItem = data.savedItems.find((item) => {
+ return item.uuid === this.state.note.uuid
+ });
+ const isInErrorState = this.state.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.state.note.dirty) {
+ this.showErrorStatus();
+ }
+ }
+ })
+ }
+
+ addSyncStatusObserver() {
+ this.syncStatusObserver = this.syncManager.
+ registerSyncStatusObserver((status) => {
+ if (status.localError) {
+ this.$timeout(() => {
+ this.showErrorStatus({
+ message: "Offline Saving Issue",
+ desc: "Changes not saved"
+ });
+ }, 500)
+ }
+ })
+ }
+
+ editorForNote(note) {
+ return this.componentManager.editorForNote(note);
+ }
+
+ setMenuState(menu, state) {
+ this.setState({
+ [menu]: state
+ });
+ this.closeAllMenus({ exclude: menu });
+ }
+
+ toggleMenu(menu) {
+ this.setMenuState(menu, !this.state[menu]);
+ }
+
+ closeAllMenus({ exclude } = {}) {
+ const allMenus = [
+ 'showOptionsMenu',
+ 'showEditorMenu',
+ 'showExtensions',
+ 'showSessionHistory'
+ ];
+ const menuState = {};
+ for (const candidate of allMenus) {
+ if (candidate !== exclude) {
+ menuState[candidate] = false;
+ }
+ }
+ this.setState(menuState);
+ }
+
+ editorMenuOnSelect = (component) => {
+ if (!component || component.area === 'editor-editor') {
+ /** If plain editor or other editor */
+ this.setMenuState('showEditorMenu', false);
+ const editor = component;
+ if (this.state.selectedEditor && editor !== this.state.selectedEditor) {
+ this.disassociateComponentWithCurrentNote(this.state.selectedEditor);
+ }
+ if (editor) {
+ const prefersPlain = this.state.note.getAppDataItem(
+ APP_DATA_KEY_PREFERS_PLAIN_EDITOR
+ ) === true;
+ if (prefersPlain) {
+ this.state.note.setAppDataItem(
+ APP_DATA_KEY_PREFERS_PLAIN_EDITOR,
+ false
+ );
+ this.modelManager.setItemDirty(this.state.note);
+ }
+ this.associateComponentWithCurrentNote(editor);
+ } else {
+ /** Note prefers plain editor */
+ if (!this.state.note.getAppDataItem(APP_DATA_KEY_PREFERS_PLAIN_EDITOR)) {
+ this.state.note.setAppDataItem(
+ APP_DATA_KEY_PREFERS_PLAIN_EDITOR,
+ true
+ );
+ this.modelManager.setItemDirty(this.state.note);
+ }
+
+ this.reloadFont();
+ }
+
+ this.setState({
+ selectedEditor: editor
+ });
+ } else if (component.area === 'editor-stack') {
+ this.toggleStackComponentForCurrentItem(component);
+ }
+
+ /** Dirtying can happen above */
+ this.syncManager.sync();
+ }
+
+ hasAvailableExtensions() {
+ return this.actionsManager.extensionsInContextOfItem(this.state.note).length > 0;
+ }
+
+ focusEditor({ delay } = {}) {
+ setTimeout(() => {
+ const element = document.getElementById(ELEMENT_ID_NOTE_TEXT_EDITOR);
+ if (element) {
+ element.focus();
+ }
+ }, delay)
+ }
+
+ focusTitle(delay) {
+ setTimeout(function () {
+ document.getElementById(ELEMENT_ID_NOTE_TITLE_EDITOR).focus();
+ }, delay)
+ }
+
+ clickedTextArea() {
+ this.setMenuState('showOptionsMenu', false);
+ }
+
+ saveNote({
+ bypassDebouncer,
+ updateClientModified,
+ dontUpdatePreviews
+ }) {
+ const note = this.state.note;
+ note.dummy = false;
+ if (note.deleted) {
+ this.alertManager.alert({
+ text: STRING_DELETED_NOTE
+ });
+ return;
+ }
+ if (!this.modelManager.findItem(note.uuid)) {
+ this.alertManager.alert({
+ text: STRING_INVALID_NOTE
+ });
+ return;
+ }
+
+ this.showSavingStatus();
+
+ if (!dontUpdatePreviews) {
+ const text = note.text || '';
+ const truncate = text.length > NOTE_PREVIEW_CHAR_LIMIT;
+ const substring = text.substring(0, NOTE_PREVIEW_CHAR_LIMIT);
+ const previewPlain = substring + (truncate ? STRING_ELLIPSES : '');
+ note.content.preview_plain = previewPlain;
+ note.content.preview_html = null;
+ }
+ this.modelManager.setItemDirty(
+ note,
+ true,
+ updateClientModified
+ );
+ if (this.saveTimeout) {
+ this.$timeout.cancel(this.saveTimeout);
+ }
+
+ const noDebounce = bypassDebouncer || this.authManager.offline();
+ const syncDebouceMs = noDebounce
+ ? SAVE_TIMEOUT_NO_DEBOUNCE
+ : SAVE_TIMEOUT_DEBOUNCE;
+ this.saveTimeout = this.$timeout(() => {
+ this.syncManager.sync().then((response) => {
+ if (response && response.error && !this.didShowErrorAlert) {
+ this.didShowErrorAlert = true;
+ this.alertManager.alert({
+ text: STRING_GENERIC_SAVE_ERROR
+ });
+ }
+ })
+ }, syncDebouceMs)
+ }
+
+ showSavingStatus() {
+ this.setStatus(
+ { message: "Saving..." },
+ false
+ );
+ }
+
+ showAllChangesSavedStatus() {
+ this.setState({
+ saveError: false,
+ syncTakingTooLong: false
+ })
+ let status = "All changes saved";
+ if (this.authManager.offline()) {
+ status += " (offline)";
+ }
+ this.setStatus(
+ { message: status }
+ );
+ }
+
+ showErrorStatus(error) {
+ if (!error) {
+ error = {
+ message: "Sync Unreachable",
+ desc: "Changes saved offline"
+ }
+ }
+ this.setState({
+ saveError: true,
+ syncTakingTooLong: false
+ })
+ this.setStatus(error);
+ }
+
+ setStatus(status, wait = true) {
+ let waitForMs;
+ if (!this.state.noteStatus || !this.state.noteStatus.date) {
+ waitForMs = 0;
+ } else {
+ waitForMs = MINIMUM_STATUS_DURATION - (new Date() - this.state.noteStatus.date);
+ }
+ if (!wait || waitForMs < 0) {
+ waitForMs = 0;
+ }
+ if (this.statusTimeout) {
+ this.$timeout.cancel(this.statusTimeout);
+ }
+ this.statusTimeout = this.$timeout(() => {
+ status.date = new Date();
+ this.setState({
+ noteStatus: status
+ })
+ }, waitForMs)
+ }
+
+ contentChanged() {
+ this.saveNote({
+ updateClientModified: true
+ });
+ }
+
+ onTitleEnter($event) {
+ $event.target.blur();
+ this.onTitleChange();
+ this.focusEditor();
+ }
+
+ onTitleChange() {
+ this.saveNote({
+ dontUpdatePreviews: true,
+ updateClientModified: true
+ });
+ }
+
+ onNameFocus() {
+ this.editingName = true;
+ }
+
+ onContentFocus() {
+ this.appState.editorDidFocus();
+ }
+
+ onNameBlur() {
+ this.editingName = false;
+ }
+
+ selectedMenuItem(hide) {
+ if (hide) {
+ this.setMenuState('showOptionsMenu', false);
+ }
+ }
+
+ async deleteNote(permanently) {
+ if (this.state.note.dummy) {
+ this.alertManager.alert({
+ text: STRING_DELETE_PLACEHOLDER_ATTEMPT
+ });
+ return;
+ }
+ const run = () => {
+ if (this.state.note.locked) {
+ this.alertManager.alert({
+ text: STRING_DELETE_LOCKED_ATTEMPT
+ });
+ return;
+ }
+ const title = this.state.note.safeTitle().length
+ ? `'${this.state.note.title}'`
+ : "this note";
+ const text = StringDeleteNote({
+ title: title,
+ permanently: permanently
+ })
+ this.alertManager.confirm({
+ text: text,
+ destructive: true,
+ onConfirm: () => {
+ if (permanently) {
+ this.performNoteDeletion(this.state.note);
+ } else {
+ this.state.note.content.trashed = true;
+ this.saveNote({
+ bypassDebouncer: true,
+ dontUpdatePreviews: true
+ });
+ }
+ this.appState.setSelectedNote(null);
+ this.setMenuState('showOptionsMenu', false);
+ }
+ })
+ }
+ const requiresPrivilege = await this.privilegesManager.actionRequiresPrivilege(
+ PrivilegesManager.ActionDeleteNote
+ );
+ if (requiresPrivilege) {
+ this.privilegesManager.presentPrivilegesModal(
+ PrivilegesManager.ActionDeleteNote,
+ () => {
+ run();
+ }
+ );
+ } else {
+ run();
+ }
+ }
+
+ performNoteDeletion(note) {
+ this.modelManager.setItemToBeDeleted(note);
+ if (note === this.state.note) {
+ this.setState({
+ note: null
+ })
+ }
+ if (note.dummy) {
+ this.modelManager.removeItemLocally(note);
+ return;
+ }
+
+ this.syncManager.sync().then(() => {
+ if (this.authManager.offline()) {
+ /**
+ * When deleting items while ofline, we need
+ * to explictly tell angular to refresh UI
+ */
+ setTimeout(function () {
+ this.$rootScope.safeApply();
+ }, 50);
+ }
+ });
+ }
+
+ restoreTrashedNote() {
+ this.state.note.content.trashed = false;
+ this.saveNote({
+ bypassDebouncer: true,
+ dontUpdatePreviews: true
+ });
+ this.appState.setSelectedNote(null);
+ }
+
+ deleteNotePermanantely() {
+ this.deleteNote(true);
+ }
+
+ getTrashCount() {
+ return this.modelManager.trashedItems().length;
+ }
+
+ emptyTrash() {
+ const count = this.getTrashCount();
+ this.alertManager.confirm({
+ text: StringEmptyTrash({ count }),
+ destructive: true,
+ onConfirm: () => {
+ this.modelManager.emptyTrash();
+ this.syncManager.sync();
+ }
+ })
+ }
+
+ togglePin() {
+ this.state.note.setAppDataItem(
+ APP_DATA_KEY_PINNED,
+ !this.state.note.pinned
+ );
+ this.saveNote({
+ bypassDebouncer: true,
+ dontUpdatePreviews: true
+ });
+ }
+
+ toggleLockNote() {
+ this.state.note.setAppDataItem(
+ APP_DATA_KEY_LOCKED,
+ !this.state.note.locked
+ );
+ this.saveNote({
+ bypassDebouncer: true,
+ dontUpdatePreviews: true
+ });
+ }
+
+ toggleProtectNote() {
+ this.state.note.content.protected = !this.state.note.content.protected;
+ this.saveNote({
+ bypassDebouncer: true,
+ dontUpdatePreviews: true
+ });
+
+ /** Show privilegesManager if protection is not yet set up */
+ this.privilegesManager.actionHasPrivilegesConfigured(
+ PrivilegesManager.ActionViewProtectedNotes
+ ).then((configured) => {
+ if (!configured) {
+ this.privilegesManager.presentPrivilegesManagementModal();
+ }
+ });
+ }
+
+ toggleNotePreview() {
+ this.state.note.content.hidePreview = !this.state.note.content.hidePreview;
+ this.saveNote({
+ bypassDebouncer: true,
+ dontUpdatePreviews: true
+ });
+ }
+
+ toggleArchiveNote() {
+ this.state.note.setAppDataItem(
+ APP_DATA_KEY_ARCHIVED,
+ !this.state.note.archived
+ );
+ this.saveNote({
+ bypassDebouncer: true,
+ dontUpdatePreviews: true
+ });
+ }
+
+ clickedEditNote() {
+ this.focusEditor({
+ delay: 100
+ });
+ }
+
+ reloadTagsString() {
+ this.setState({
+ tagsString: this.state.note.tagsString()
+ });
+ }
+
+ addTag(tag) {
+ const strings = this.state.note.tags.map((currentTag) => {
+ return currentTag.title;
+ });
+ strings.push(tag.title);
+ this.updateTags(strings);
+ this.reloadTagsString();
+ }
+
+ removeTag(tag) {
+ const strings = this.state.note.tags.map((currentTag) => {
+ return currentTag.title;
+ }).filter((title) => {
+ return title !== tag.title;
+ });
+ this.updateTags(strings);
+ this.reloadTagsString();
+ }
+
+ updateTag(stringTags) {
+ const toRemove = [];
+ for (const tag of this.state.note.tags) {
+ if (stringTags.indexOf(tag.title) === -1) {
+ toRemove.push(tag);
+ }
+ }
+ for (const tagToRemove of toRemove) {
+ tagToRemove.removeItemAsRelationship(this.state.note);
+ }
+ this.modelManager.setItemsDirty(toRemove);
+ const tags = [];
+ for (const tagString of stringTags) {
+ const existingRelationship = _.find(
+ this.state.note.tags,
+ { title: tagString }
+ );
+ if (!existingRelationship) {
+ tags.push(
+ this.modelManager.findOrCreateTagByTitle(tagString)
+ );
+ }
+ }
+ for (const tag of tags) {
+ tag.addItemAsRelationship(this.state.note);
+ }
+ this.modelManager.setItemsDirty(tags);
+ this.syncManager.sync();
+ }
+
+ updateTagsFromTagsString() {
+ if (this.state.tagsString === this.state.note.tagsString()) {
+ return;
+ }
+ const strings = this.state.tagsString.split('#').filter((string) => {
+ return string.length > 0;
+ }).map((string) => {
+ return string.trim();
+ })
+ this.state.note.dummy = false;
+ this.updateTags(strings);
+ }
+
+ onPanelResizeFinish = (width, left, isMaxWidth) => {
+ if (isMaxWidth) {
+ this.preferencesManager.setUserPrefValue(
+ PREF_EDITOR_WIDTH,
+ null
+ );
+ } else {
+ if (width !== undefined && width !== null) {
+ this.preferencesManager.setUserPrefValue(
+ PREF_EDITOR_WIDTH,
+ width
+ );
+ this.leftResizeControl.setWidth(width);
+ }
+ }
+ if (left !== undefined && left !== null) {
+ this.preferencesManager.setUserPrefValue(
+ PREF_EDITOR_LEFT,
+ left
+ );
+ this.rightResizeControl.setLeft(left);
+ }
+ this.preferencesManager.syncUserPreferences();
+ }
+
+ loadPreferences() {
+ const monospaceEnabled = this.preferencesManager.getValue(
+ PREF_EDITOR_MONOSPACE_ENABLED,
+ true
+ );
+ const spellcheck = this.preferencesManager.getValue(
+ PREF_EDITOR_SPELLCHECK,
+ true
+ );
+ const marginResizersEnabled = this.preferencesManager.getValue(
+ PREF_EDITOR_RESIZERS_ENABLED,
+ true
+ );
+ this.setState({
+ monospaceEnabled,
+ spellcheck,
+ marginResizersEnabled
+ });
+
+ if (!document.getElementById(ELEMENT_ID_EDITOR_CONTENT)) {
+ /** Elements have not yet loaded due to ng-if around wrapper */
+ return;
+ }
+
+ this.reloadFont();
+
+ if (this.state.marginResizersEnabled) {
+ const width = this.preferencesManager.getValue(
+ PREF_EDITOR_WIDTH,
+ null
+ );
+ if (width != null) {
+ this.leftResizeControl.setWidth(width);
+ this.rightResizeControl.setWidth(width);
+ }
+ const left = this.preferencesManager.getValue(
+ PREF_EDITOR_LEFT,
+ null
+ );
+ if (left != null) {
+ this.leftResizeControl.setLeft(left);
+ this.rightResizeControl.setLeft(left);
+ }
+ }
+ }
+
+ reloadFont() {
+ const editor = document.getElementById(
+ ELEMENT_ID_NOTE_TEXT_EDITOR
+ );
+ if (!editor) {
+ return;
+ }
+ if (this.state.monospaceEnabled) {
+ if (this.state.isDesktop) {
+ editor.style.fontFamily = DESKTOP_MONOSPACE_FAMILY;
+ } else {
+ editor.style.fontFamily = WEB_MONOSPACE_FAMILY;
+ }
+ } else {
+ editor.style.fontFamily = SANS_SERIF_FAMILY;
+ }
+ }
+
+ async toggleKey(key) {
+ this[key] = !this[key];
+ this.preferencesManager.setUserPrefValue(
+ key,
+ this[key],
+ true
+ );
+ this.reloadFont();
+
+ if (key === PREF_EDITOR_SPELLCHECK) {
+ /** Allows textarea to reload */
+ await this.setState({
+ noteReady: false
+ })
+ this.setState({
+ noteReady: true
+ })
+ this.reloadFont();
+ } else if (key === PREF_EDITOR_RESIZERS_ENABLED && this[key] === true) {
+ this.$timeout(() => {
+ this.leftResizeControl.flash();
+ this.rightResizeControl.flash();
+ })
+ }
+ }
+
+ /** @components */
+
+ onEditorLoad = (editor) => {
+ this.desktopManager.redoSearch();
+ }
+
+ registerComponentHandler() {
+ this.componentManager.registerHandler({
+ identifier: 'editor',
+ areas: [
+ 'note-tags',
+ 'editor-stack',
+ 'editor-editor'
+ ],
+ activationHandler: (component) => {
+ if (component.area === 'note-tags') {
+ this.setState({
+ tagsComponent: component.active ? component : null
+ });
+ } else if (component.area === 'editor-editor') {
+ if (
+ component === this.state.selectedEditor &&
+ !component.active
+ ) {
+ this.setState({ selectedEditor: null });
+ }
+ else if (this.state.selectedEditor) {
+ if (this.state.selectedEditor.active && this.state.note) {
+ if (
+ component.isExplicitlyEnabledForItem(this.state.note)
+ && !this.state.selectedEditor.isExplicitlyEnabledForItem(this.state.note)
+ ) {
+ this.setState({ selectedEditor: component });
+ }
+ }
+ }
+ else if(this.state.note) {
+ const enableable = (
+ component.isExplicitlyEnabledForItem(this.state.note)
+ || component.isDefaultEditor()
+ );
+ if (
+ component.active
+ && enableable
+ ) {
+ this.setState({ selectedEditor: component });
+ } else {
+ /**
+ * Not a candidate, and no qualified editor.
+ * Disable the current editor.
+ */
+ this.setState({ selectedEditor: null });
+ }
+ }
+
+ } else if (component.area === 'editor-stack') {
+ this.reloadComponentContext();
+ }
+ },
+ contextRequestHandler: (component) => {
+ if (
+ component === this.state.selectedEditor ||
+ component === this.state.tagsComponent ||
+ this.state.componentStack.includes(component)
+ ) {
+ return this.state.note;
+ }
+ },
+ focusHandler: (component, focused) => {
+ if (component.isEditor() && focused) {
+ this.closeAllMenus();
+ }
+ },
+ actionHandler: (component, action, data) => {
+ if (action === 'set-size') {
+ const setSize = function (element, size) {
+ const widthString = typeof size.width === 'string'
+ ? size.width
+ : `${data.width}px`;
+ const 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') {
+ const container = document.getElementById(
+ ELEMENT_ID_NOTE_TAGS_COMPONENT_CONTAINER
+ );
+ setSize(container, data);
+ }
+ }
+ }
+ else if (action === 'associate-item') {
+ if (data.item.content_type === 'Tag') {
+ const tag = this.modelManager.findItem(data.item.uuid);
+ this.addTag(tag);
+ }
+ }
+ else if (action === 'deassociate-item') {
+ const tag = this.modelManager.findItem(data.item.uuid);
+ this.removeTag(tag);
+ }
+ else if (action === 'save-items') {
+ const includesNote = data.items.map((item) => {
+ return item.uuid
+ }).includes(this.state.note.uuid);
+ if (includesNote) {
+ this.showSavingStatus();
+ }
+ }
+ }
+ });
+ }
+
+ reloadComponentStackArray() {
+ const components = this.componentManager.componentsForArea('editor-stack')
+ .sort((a, b) => {
+ return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
+ });
+
+ this.setState({
+ state: components
+ })
+ }
+
+ reloadComponentContext() {
+ this.reloadComponentStackArray();
+ if (this.state.note) {
+ for (const component of this.state.componentStack) {
+ if (component.active) {
+ this.componentManager.setComponentHidden(
+ component,
+ !component.isExplicitlyEnabledForItem(this.state.note)
+ );
+ }
+ }
+ }
+
+ this.componentManager.contextItemDidChangeInArea('note-tags');
+ this.componentManager.contextItemDidChangeInArea('editor-stack');
+ this.componentManager.contextItemDidChangeInArea('editor-editor');
+ }
+
+ toggleStackComponentForCurrentItem(component) {
+ if (component.hidden || !component.active) {
+ this.componentManager.setComponentHidden(component, false);
+ this.associateComponentWithCurrentNote(component);
+ if (!component.active) {
+ this.componentManager.activateComponent(component);
+ }
+ this.componentManager.contextItemDidChangeInArea('editor-stack');
+ } else {
+ this.componentManager.setComponentHidden(component, true);
+ this.disassociateComponentWithCurrentNote(component);
+ }
+ }
+
+ disassociateComponentWithCurrentNote(component) {
+ component.associatedItemIds = component.associatedItemIds.filter((id) => {
+ return id !== this.state.note.uuid;
+ });
+
+ if (!component.disassociatedItemIds.includes(this.state.note.uuid)) {
+ component.disassociatedItemIds.push(this.state.note.uuid);
+ }
+
+ this.modelManager.setItemDirty(component);
+ this.syncManager.sync();
+ }
+
+ associateComponentWithCurrentNote(component) {
+ component.disassociatedItemIds = component.disassociatedItemIds
+ .filter((id) => {
+ return id !== this.state.note.uuid;
+ });
+
+ if (!component.associatedItemIds.includes(this.state.note.uuid)) {
+ component.associatedItemIds.push(this.state.note.uuid);
+ }
+
+ this.modelManager.setItemDirty(component);
+ this.syncManager.sync();
+ }
+
+ registerKeyboardShortcuts() {
+ this.altKeyObserver = this.keyboardManager.addKeyObserver({
+ modifiers: [
+ KeyboardManager.KeyModifierAlt
+ ],
+ onKeyDown: () => {
+ this.setState({
+ altKeyDown: true
+ })
+ },
+ onKeyUp: () => {
+ this.setState({
+ altKeyDown: false
+ });
+ }
+ })
+
+ this.trashKeyObserver = this.keyboardManager.addKeyObserver({
+ key: KeyboardManager.KeyBackspace,
+ notElementIds: [
+ ELEMENT_ID_NOTE_TEXT_EDITOR,
+ ELEMENT_ID_NOTE_TITLE_EDITOR
+ ],
+ modifiers: [KeyboardManager.KeyModifierMeta],
+ onKeyDown: () => {
+ this.deleteNote();
+ },
+ })
+
+ this.deleteKeyObserver = this.keyboardManager.addKeyObserver({
+ key: KeyboardManager.KeyBackspace,
+ modifiers: [
+ KeyboardManager.KeyModifierMeta,
+ KeyboardManager.KeyModifierShift,
+ KeyboardManager.KeyModifierAlt
+ ],
+ onKeyDown: (event) => {
+ event.preventDefault();
+ this.deleteNote(true);
+ },
+ })
+ }
+
+ onSystemEditorLoad() {
+ 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(
+ ELEMENT_ID_NOTE_TEXT_EDITOR
+ );
+ this.tabObserver = this.keyboardManager.addKeyObserver({
+ element: editor,
+ key: KeyboardManager.KeyTab,
+ onKeyDown: (event) => {
+ if (this.state.note.locked || event.shiftKey) {
+ return;
+ }
+ event.preventDefault();
+ /** Using document.execCommand gives us undo support */
+ const insertSuccessful = document.execCommand(
+ 'insertText',
+ false,
+ '\t'
+ );
+ if (!insertSuccessful) {
+ /** document.execCommand works great on Chrome/Safari but not Firefox */
+ const start = editor.selectionStart;
+ const end = editor.selectionEnd;
+ const 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;
+ }
+
+ const note = this.state.note;
+ note.text = editor.value;
+ this.setState({
+ note: note
+ })
+ this.saveNote({
+ bypassDebouncer: true
+ });
+ },
+ })
+
+ /**
+ * Handles when the editor is destroyed,
+ * (and not when our controller is destroyed.)
+ */
+ angular.element(editor).on('$destroy', () => {
+ if (this.tabObserver) {
+ this.keyboardManager.removeKeyObserver(this.tabObserver);
+ this.loadedTabListener = false;
+ }
+ });
+ };
+}
+
+export class EditorPanel {
+ constructor() {
+ this.restrict = 'E';
+ this.scope = {};
+ this.template = template;
+ this.replace = true;
+ this.controller = EditorCtrl;
+ this.controllerAs = 'self';
+ this.bindToController = true;
+ }
+}
diff --git a/app/assets/javascripts/controllers/footer.js b/app/assets/javascripts/controllers/footer.js
new file mode 100644
index 000000000..fbcf45b78
--- /dev/null
+++ b/app/assets/javascripts/controllers/footer.js
@@ -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;
+ }
+}
diff --git a/app/assets/javascripts/controllers/index.js b/app/assets/javascripts/controllers/index.js
new file mode 100644
index 000000000..d78987c9e
--- /dev/null
+++ b/app/assets/javascripts/controllers/index.js
@@ -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';
\ No newline at end of file
diff --git a/app/assets/javascripts/controllers/lockScreen.js b/app/assets/javascripts/controllers/lockScreen.js
new file mode 100644
index 000000000..bb62fd5ab
--- /dev/null
+++ b/app/assets/javascripts/controllers/lockScreen.js
@@ -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: '&',
+ };
+ }
+}
diff --git a/app/assets/javascripts/controllers/notes/note_utils.js b/app/assets/javascripts/controllers/notes/note_utils.js
new file mode 100644
index 000000000..e1bb78f61
--- /dev/null
+++ b/app/assets/javascripts/controllers/notes/note_utils.js
@@ -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;
+}
diff --git a/app/assets/javascripts/controllers/notes/notes.js b/app/assets/javascripts/controllers/notes/notes.js
new file mode 100644
index 000000000..f1a4f6781
--- /dev/null
+++ b/app/assets/javascripts/controllers/notes/notes.js
@@ -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;
+ }
+}
diff --git a/app/assets/javascripts/controllers/root.js b/app/assets/javascripts/controllers/root.js
new file mode 100644
index 000000000..c6199f1cb
--- /dev/null
+++ b/app/assets/javascripts/controllers/root.js
@@ -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;
+ }
+}
diff --git a/app/assets/javascripts/controllers/tags.js b/app/assets/javascripts/controllers/tags.js
new file mode 100644
index 000000000..afdc50a62
--- /dev/null
+++ b/app/assets/javascripts/controllers/tags.js
@@ -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;
+ }
+}
diff --git a/app/assets/javascripts/app/directives/functional/autofocus.js b/app/assets/javascripts/directives/functional/autofocus.js
similarity index 100%
rename from app/assets/javascripts/app/directives/functional/autofocus.js
rename to app/assets/javascripts/directives/functional/autofocus.js
diff --git a/app/assets/javascripts/app/directives/functional/click-outside.js b/app/assets/javascripts/directives/functional/click-outside.js
similarity index 100%
rename from app/assets/javascripts/app/directives/functional/click-outside.js
rename to app/assets/javascripts/directives/functional/click-outside.js
diff --git a/app/assets/javascripts/app/directives/functional/delay-hide.js b/app/assets/javascripts/directives/functional/delay-hide.js
similarity index 100%
rename from app/assets/javascripts/app/directives/functional/delay-hide.js
rename to app/assets/javascripts/directives/functional/delay-hide.js
diff --git a/app/assets/javascripts/app/directives/functional/elemReady.js b/app/assets/javascripts/directives/functional/elemReady.js
similarity index 100%
rename from app/assets/javascripts/app/directives/functional/elemReady.js
rename to app/assets/javascripts/directives/functional/elemReady.js
diff --git a/app/assets/javascripts/app/directives/functional/file-change.js b/app/assets/javascripts/directives/functional/file-change.js
similarity index 100%
rename from app/assets/javascripts/app/directives/functional/file-change.js
rename to app/assets/javascripts/directives/functional/file-change.js
diff --git a/app/assets/javascripts/app/directives/functional/index.js b/app/assets/javascripts/directives/functional/index.js
similarity index 100%
rename from app/assets/javascripts/app/directives/functional/index.js
rename to app/assets/javascripts/directives/functional/index.js
diff --git a/app/assets/javascripts/app/directives/functional/infiniteScroll.js b/app/assets/javascripts/directives/functional/infiniteScroll.js
similarity index 82%
rename from app/assets/javascripts/app/directives/functional/infiniteScroll.js
rename to app/assets/javascripts/directives/functional/infiniteScroll.js
index 4a8e172a4..58cdee77f 100644
--- a/app/assets/javascripts/app/directives/functional/infiniteScroll.js
+++ b/app/assets/javascripts/directives/functional/infiniteScroll.js
@@ -2,9 +2,8 @@
export function infiniteScroll($rootScope, $window, $timeout) {
return {
link: function(scope, elem, attrs) {
- var offset = parseInt(attrs.threshold) || 0;
- var e = elem[0];
-
+ const offset = parseInt(attrs.threshold) || 0;
+ const e = elem[0];
elem.on('scroll', function() {
if (
scope.$eval(attrs.canLoad) &&
diff --git a/app/assets/javascripts/app/directives/functional/lowercase.js b/app/assets/javascripts/directives/functional/lowercase.js
similarity index 100%
rename from app/assets/javascripts/app/directives/functional/lowercase.js
rename to app/assets/javascripts/directives/functional/lowercase.js
diff --git a/app/assets/javascripts/app/directives/functional/selectOnClick.js b/app/assets/javascripts/directives/functional/selectOnClick.js
similarity index 87%
rename from app/assets/javascripts/app/directives/functional/selectOnClick.js
rename to app/assets/javascripts/directives/functional/selectOnClick.js
index e7bccb79b..b657ba422 100644
--- a/app/assets/javascripts/app/directives/functional/selectOnClick.js
+++ b/app/assets/javascripts/directives/functional/selectOnClick.js
@@ -5,7 +5,7 @@ export function selectOnClick($window) {
link: function(scope, element, attrs) {
element.on('focus', function() {
if (!$window.getSelection().toString()) {
- // Required for mobile Safari
+ /** Required for mobile Safari */
this.setSelectionRange(0, this.value.length);
}
});
diff --git a/app/assets/javascripts/app/directives/functional/snEnter.js b/app/assets/javascripts/directives/functional/snEnter.js
similarity index 100%
rename from app/assets/javascripts/app/directives/functional/snEnter.js
rename to app/assets/javascripts/directives/functional/snEnter.js
diff --git a/app/assets/javascripts/directives/views/accountMenu.js b/app/assets/javascripts/directives/views/accountMenu.js
new file mode 100644
index 000000000..50efc1b05
--- /dev/null
+++ b/app/assets/javascripts/directives/views/accountMenu.js
@@ -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: '&'
+ };
+ }
+}
diff --git a/app/assets/javascripts/directives/views/actionsMenu.js b/app/assets/javascripts/directives/views/actionsMenu.js
new file mode 100644
index 000000000..d34b00dd8
--- /dev/null
+++ b/app/assets/javascripts/directives/views/actionsMenu.js
@@ -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: '='
+ };
+ }
+}
diff --git a/app/assets/javascripts/directives/views/componentModal.js b/app/assets/javascripts/directives/views/componentModal.js
new file mode 100644
index 000000000..5b7b9db3c
--- /dev/null
+++ b/app/assets/javascripts/directives/views/componentModal.js
@@ -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: '&'
+ };
+ }
+}
diff --git a/app/assets/javascripts/directives/views/componentView.js b/app/assets/javascripts/directives/views/componentView.js
new file mode 100644
index 000000000..c7c74fd73
--- /dev/null
+++ b/app/assets/javascripts/directives/views/componentView.js
@@ -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;
+ }
+}
diff --git a/app/assets/javascripts/directives/views/conflictResolutionModal.js b/app/assets/javascripts/directives/views/conflictResolutionModal.js
new file mode 100644
index 000000000..08dc1095c
--- /dev/null
+++ b/app/assets/javascripts/directives/views/conflictResolutionModal.js
@@ -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: '='
+ };
+ }
+}
diff --git a/app/assets/javascripts/directives/views/editorMenu.js b/app/assets/javascripts/directives/views/editorMenu.js
new file mode 100644
index 000000000..6ee413689
--- /dev/null
+++ b/app/assets/javascripts/directives/views/editorMenu.js
@@ -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: '='
+ };
+ }
+}
diff --git a/app/assets/javascripts/app/directives/views/index.js b/app/assets/javascripts/directives/views/index.js
similarity index 100%
rename from app/assets/javascripts/app/directives/views/index.js
rename to app/assets/javascripts/directives/views/index.js
diff --git a/app/assets/javascripts/directives/views/inputModal.js b/app/assets/javascripts/directives/views/inputModal.js
new file mode 100644
index 000000000..12c56495d
--- /dev/null
+++ b/app/assets/javascripts/directives/views/inputModal.js
@@ -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: '&'
+ };
+ }
+}
diff --git a/app/assets/javascripts/app/directives/views/menuRow.js b/app/assets/javascripts/directives/views/menuRow.js
similarity index 54%
rename from app/assets/javascripts/app/directives/views/menuRow.js
rename to app/assets/javascripts/directives/views/menuRow.js
index 9eecf7daa..6d5373654 100644
--- a/app/assets/javascripts/app/directives/views/menuRow.js
+++ b/app/assets/javascripts/directives/views/menuRow.js
@@ -1,46 +1,48 @@
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 {
constructor() {
this.restrict = 'E';
this.transclude = true;
this.template = template;
+ this.controller = MenuRowCtrl;
+ this.controllerAs = 'ctrl';
+ this.bindToController = true;
this.scope = {
action: '&',
- circle: '=',
- circleAlign: '=',
- label: '=',
- subtitle: '=',
- hasButton: '=',
- buttonText: '=',
- buttonClass: '=',
buttonAction: '&',
- spinnerClass: '=',
- subRows: '=',
- faded: '=',
+ buttonClass: '=',
+ buttonText: '=',
desc: '=',
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();
- }
- }
}
diff --git a/app/assets/javascripts/directives/views/panelResizer.js b/app/assets/javascripts/directives/views/panelResizer.js
new file mode 100644
index 000000000..ef72a601a
--- /dev/null
+++ b/app/assets/javascripts/directives/views/panelResizer.js
@@ -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(``)(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: '='
+ };
+ }
+}
diff --git a/app/assets/javascripts/directives/views/passwordWizard.js b/app/assets/javascripts/directives/views/passwordWizard.js
new file mode 100644
index 000000000..2dbabb5ae
--- /dev/null
+++ b/app/assets/javascripts/directives/views/passwordWizard.js
@@ -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: '='
+ };
+ }
+}
diff --git a/app/assets/javascripts/directives/views/permissionsModal.js b/app/assets/javascripts/directives/views/permissionsModal.js
new file mode 100644
index 000000000..12ec241a7
--- /dev/null
+++ b/app/assets/javascripts/directives/views/permissionsModal.js
@@ -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: '='
+ };
+ }
+}
diff --git a/app/assets/javascripts/directives/views/privilegesAuthModal.js b/app/assets/javascripts/directives/views/privilegesAuthModal.js
new file mode 100644
index 000000000..b9aef3a4f
--- /dev/null
+++ b/app/assets/javascripts/directives/views/privilegesAuthModal.js
@@ -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: '='
+ };
+ }
+}
diff --git a/app/assets/javascripts/directives/views/privilegesManagementModal.js b/app/assets/javascripts/directives/views/privilegesManagementModal.js
new file mode 100644
index 000000000..6d003510b
--- /dev/null
+++ b/app/assets/javascripts/directives/views/privilegesManagementModal.js
@@ -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 = {};
+ }
+}
diff --git a/app/assets/javascripts/directives/views/revisionPreviewModal.js b/app/assets/javascripts/directives/views/revisionPreviewModal.js
new file mode 100644
index 000000000..1c1da915a
--- /dev/null
+++ b/app/assets/javascripts/directives/views/revisionPreviewModal.js
@@ -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: '='
+ };
+ }
+}
diff --git a/app/assets/javascripts/directives/views/sessionHistoryMenu.js b/app/assets/javascripts/directives/views/sessionHistoryMenu.js
new file mode 100644
index 000000000..70027ac2b
--- /dev/null
+++ b/app/assets/javascripts/directives/views/sessionHistoryMenu.js
@@ -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: '='
+ };
+ }
+}
diff --git a/app/assets/javascripts/directives/views/syncResolutionMenu.js b/app/assets/javascripts/directives/views/syncResolutionMenu.js
new file mode 100644
index 000000000..e7b589d55
--- /dev/null
+++ b/app/assets/javascripts/directives/views/syncResolutionMenu.js
@@ -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: '&'
+ };
+ }
+}
diff --git a/app/assets/javascripts/app/filters/appDate.js b/app/assets/javascripts/filters/appDate.js
similarity index 100%
rename from app/assets/javascripts/app/filters/appDate.js
rename to app/assets/javascripts/filters/appDate.js
diff --git a/app/assets/javascripts/app/filters/index.js b/app/assets/javascripts/filters/index.js
similarity index 100%
rename from app/assets/javascripts/app/filters/index.js
rename to app/assets/javascripts/filters/index.js
diff --git a/app/assets/javascripts/app/filters/trusted.js b/app/assets/javascripts/filters/trusted.js
similarity index 100%
rename from app/assets/javascripts/app/filters/trusted.js
rename to app/assets/javascripts/filters/trusted.js
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/index.js
similarity index 90%
rename from app/assets/javascripts/main.js
rename to app/assets/javascripts/index.js
index 8d4df584f..da9548355 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/index.js
@@ -2,7 +2,7 @@
// css
import 'sn-stylekit/dist/stylekit.css';
-import '../stylesheets/main.css.scss';
+import '../stylesheets/index.css.scss';
// Vendor
import 'angular';
@@ -19,4 +19,4 @@ SFItem.AppDomain = 'org.standardnotes.sn';
// entry point
// eslint-disable-next-line import/first
-import './app/app';
+import './app';
diff --git a/app/assets/javascripts/app/models/noteHistoryEntry.js b/app/assets/javascripts/models/noteHistoryEntry.js
similarity index 100%
rename from app/assets/javascripts/app/models/noteHistoryEntry.js
rename to app/assets/javascripts/models/noteHistoryEntry.js
diff --git a/app/assets/javascripts/app/routes.js b/app/assets/javascripts/routes.js
similarity index 100%
rename from app/assets/javascripts/app/routes.js
rename to app/assets/javascripts/routes.js
diff --git a/app/assets/javascripts/services/actionsManager.js b/app/assets/javascripts/services/actionsManager.js
new file mode 100644
index 000000000..e15501d79
--- /dev/null
+++ b/app/assets/javascripts/services/actionsManager.js
@@ -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(
+ ``
+ )(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(
+ ``
+ )(scope);
+ angular.element(document.body).append(el);
+ }
+}
diff --git a/app/assets/javascripts/services/alertManager.js b/app/assets/javascripts/services/alertManager.js
new file mode 100644
index 000000000..c832c6062
--- /dev/null
+++ b/app/assets/javascripts/services/alertManager.js
@@ -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();
+ });
+ }
+}
diff --git a/app/assets/javascripts/app/services/archiveManager.js b/app/assets/javascripts/services/archiveManager.js
similarity index 85%
rename from app/assets/javascripts/app/services/archiveManager.js
rename to app/assets/javascripts/services/archiveManager.js
index c9774ad78..0bc101c60 100644
--- a/app/assets/javascripts/app/services/archiveManager.js
+++ b/app/assets/javascripts/services/archiveManager.js
@@ -18,9 +18,9 @@ export class ArchiveManager {
}
async downloadBackupOfItems(items, encrypted) {
- let run = async () => {
+ const run = async () => {
// download in Standard Notes format
- var keys, authParams;
+ let keys, authParams;
if(encrypted) {
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
keys = this.passcodeManager.keys();
@@ -31,15 +31,15 @@ export class ArchiveManager {
}
}
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`);
// download as zipped plain text files
if(!keys) {
this.__downloadZippedItems(items);
}
- })
- }
+ });
+ };
if(await this.privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageBackups)) {
this.privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => {
@@ -65,8 +65,8 @@ export class ArchiveManager {
}
async __itemsData(items, keys, authParams) {
- let data = await this.modelManager.getJSONDataForItems(items, keys, authParams);
- let blobData = new Blob([data], {type: 'text/json'});
+ const data = await this.modelManager.getJSONDataForItems(items, keys, authParams);
+ const blobData = new Blob([data], {type: 'text/json'});
return blobData;
}
@@ -84,7 +84,7 @@ export class ArchiveManager {
scriptTag.onload = function() {
zip.workerScriptsPath = "assets/zip/";
callback();
- }
+ };
}
__downloadZippedItems(items) {
@@ -92,11 +92,11 @@ export class ArchiveManager {
zip.createWriter(new zip.BlobWriter("application/zip"), (zipWriter) => {
var index = 0;
- let nextFile = () => {
+ const nextFile = () => {
var item = items[index];
var name, contents;
- if(item.content_type == "Note") {
+ if(item.content_type === "Note") {
name = item.content.title;
contents = item.content.text;
} else {
@@ -108,16 +108,12 @@ export class ArchiveManager {
name = "";
}
- var blob = new Blob([contents], {type: 'text/plain'});
-
- var filePrefix = name.replace(/\//g, "").replace(/\\+/g, "");
- var fileSuffix = `-${item.uuid.split("-")[0]}.txt`
-
+ const blob = new Blob([contents], {type: 'text/plain'});
+ let filePrefix = name.replace(/\//g, "").replace(/\\+/g, "");
+ const fileSuffix = `-${item.uuid.split("-")[0]}.txt`;
// Standard max filename length is 255. Slice the note name down to allow filenameEnd
filePrefix = filePrefix.slice(0, (255 - fileSuffix.length));
-
- let fileName = `${item.content_type}/${filePrefix}${fileSuffix}`
-
+ const fileName = `${item.content_type}/${filePrefix}${fileSuffix}`
zipWriter.add(fileName, new zip.BlobReader(blob), () => {
index++;
if(index < items.length) {
@@ -129,11 +125,11 @@ export class ArchiveManager {
});
}
});
- }
+ };
nextFile();
}, onerror);
- })
+ });
}
diff --git a/app/assets/javascripts/app/services/authManager.js b/app/assets/javascripts/services/authManager.js
similarity index 68%
rename from app/assets/javascripts/app/services/authManager.js
rename to app/assets/javascripts/services/authManager.js
index 993146c59..f5c53e97b 100644
--- a/app/assets/javascripts/app/services/authManager.js
+++ b/app/assets/javascripts/services/authManager.js
@@ -1,6 +1,6 @@
import angular from 'angular';
import { StorageManager } from './storageManager';
-import { protocolManager, SFItem, SFPredicate, SFAuthManager } from 'snjs';
+import { protocolManager, SFAuthManager } from 'snjs';
export class AuthManager extends SFAuthManager {
/* @ngInject */
@@ -24,24 +24,17 @@ export class AuthManager extends SFAuthManager {
}
loadInitialData() {
- var userData = this.storageManager.getItemSync("user");
+ const userData = this.storageManager.getItemSync("user");
if(userData) {
this.user = JSON.parse(userData);
} else {
// legacy, check for uuid
- var idData = this.storageManager.getItemSync("uuid");
+ const idData = this.storageManager.getItemSync("uuid");
if(idData) {
this.user = {uuid: idData};
}
}
-
- this.configureUserPrefs();
this.checkForSecurityUpdate();
-
- this.modelManager.addItemSyncObserver("user-prefs", "SN|UserPreferences", (allItems, validItems, deletedItems, source, sourceKey) => {
- this.userPreferencesDidChange();
- });
-
}
offline() {
@@ -144,48 +137,4 @@ export class AuthManager extends SFAuthManager {
this.user = 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();
- }
- }
}
diff --git a/app/assets/javascripts/app/services/componentManager.js b/app/assets/javascripts/services/componentManager.js
similarity index 100%
rename from app/assets/javascripts/app/services/componentManager.js
rename to app/assets/javascripts/services/componentManager.js
diff --git a/app/assets/javascripts/app/services/dbManager.js b/app/assets/javascripts/services/dbManager.js
similarity index 96%
rename from app/assets/javascripts/app/services/dbManager.js
rename to app/assets/javascripts/services/dbManager.js
index a19696cfa..b7cb79927 100644
--- a/app/assets/javascripts/app/services/dbManager.js
+++ b/app/assets/javascripts/services/dbManager.js
@@ -40,7 +40,7 @@ export class DBManager {
db.close();
};
db.onerror = function(errorEvent) {
- console.log("Database error: " + errorEvent.target.errorCode);
+ console.error("Database error: " + errorEvent.target.errorCode);
}
resolve(db);
};
@@ -161,12 +161,11 @@ export class DBManager {
return new Promise((resolve, reject) => {
deleteRequest.onerror = function(event) {
- console.log("Error deleting database.");
+ console.error("Error deleting database.");
resolve();
};
deleteRequest.onsuccess = function(event) {
- console.log("Database deleted successfully");
resolve();
};
diff --git a/app/assets/javascripts/app/services/desktopManager.js b/app/assets/javascripts/services/desktopManager.js
similarity index 72%
rename from app/assets/javascripts/app/services/desktopManager.js
rename to app/assets/javascripts/services/desktopManager.js
index e22ecbb73..8cea48dde 100644
--- a/app/assets/javascripts/app/services/desktopManager.js
+++ b/app/assets/javascripts/services/desktopManager.js
@@ -3,6 +3,10 @@ import _ from 'lodash';
import { isDesktopApplication } from '@/utils';
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 {
/* @ngInject */
constructor(
@@ -11,13 +15,15 @@ export class DesktopManager {
modelManager,
syncManager,
authManager,
- passcodeManager
+ passcodeManager,
+ appState
) {
this.passcodeManager = passcodeManager;
this.modelManager = modelManager;
this.authManager = authManager;
this.syncManager = syncManager;
this.$rootScope = $rootScope;
+ this.appState = appState;
this.timeout = $timeout;
this.updateObservers = [];
this.componentActivationObservers = [];
@@ -43,7 +49,10 @@ export class DesktopManager {
}
getExtServerHost() {
- console.assert(this.extServerHost, "extServerHost is null");
+ console.assert(
+ this.extServerHost,
+ 'extServerHost is null'
+ );
return this.extServerHost;
}
@@ -57,8 +66,9 @@ export class DesktopManager {
// All `components` should be installed
syncComponentsInstallation(components) {
- if(!this.isDesktop) return;
-
+ if(!this.isDesktop) {
+ return;
+ }
Promise.all(components.map((component) => {
return this.convertComponentForTransmission(component);
})).then((data) => {
@@ -67,11 +77,15 @@ export class DesktopManager {
}
async installComponent(component) {
- this.installComponentHandler(await this.convertComponentForTransmission(component));
+ this.installComponentHandler(
+ await this.convertComponentForTransmission(component)
+ );
}
registerUpdateObserver(callback) {
- var observer = {id: Math.random, callback: callback};
+ const observer = {
+ callback: callback
+ };
this.updateObservers.push(observer);
return observer;
}
@@ -90,7 +104,6 @@ export class DesktopManager {
}
}
-
deregisterUpdateObserver(observer) {
_.pull(this.updateObservers, observer);
}
@@ -109,39 +122,43 @@ export class DesktopManager {
}
desktop_onComponentInstallationComplete(componentData, error) {
- // console.log("Web|Component Installation/Update Complete", componentData, error);
-
- // Desktop is only allowed to change these keys:
- let permissableKeys = ["package_info", "local_url"];
- var component = this.modelManager.findItem(componentData.uuid);
-
+ const component = this.modelManager.findItem(componentData.uuid);
if(!component) {
- console.error("desktop_onComponentInstallationComplete component is null for uuid", componentData.uuid);
return;
}
-
if(error) {
- component.setAppDataItem("installError", error);
+ component.setAppDataItem(
+ COMPONENT_DATA_KEY_INSTALL_ERROR,
+ error
+ );
} 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];
}
- this.modelManager.notifySyncObserversOfModels([component], SFModelManager.MappingSourceDesktopInstalled);
- component.setAppDataItem("installError", null);
+ this.modelManager.notifySyncObserversOfModels(
+ [component],
+ SFModelManager.MappingSourceDesktopInstalled
+ );
+ component.setAppDataItem(
+ COMPONENT_DATA_KEY_INSTALL_ERROR,
+ null
+ );
}
-
- this.modelManager.setItemDirty(component, true);
+ this.modelManager.setItemDirty(component);
this.syncManager.sync();
-
this.timeout(() => {
- for(var observer of this.updateObservers) {
+ for(const observer of this.updateObservers) {
observer.callback(component);
}
});
}
desktop_registerComponentActivationObserver(callback) {
- var observer = {id: Math.random, callback: callback};
+ const observer = {id: Math.random, callback: callback};
this.componentActivationObservers.push(observer);
return observer;
}
@@ -152,10 +169,11 @@ export class DesktopManager {
/* Notify observers that a component has been registered/activated */
async notifyComponentActivation(component) {
- var serializedComponent = await this.convertComponentForTransmission(component);
-
+ const serializedComponent = await this.convertComponentForTransmission(
+ component
+ );
this.timeout(() => {
- for(var observer of this.componentActivationObservers) {
+ for(const observer of this.componentActivationObservers) {
observer.callback(serializedComponent);
}
});
@@ -164,7 +182,7 @@ export class DesktopManager {
/* Used to resolve "sn://" */
desktop_setExtServerHost(host) {
this.extServerHost = host;
- this.$rootScope.$broadcast("desktop-did-set-ext-server-host");
+ this.appState.desktopExtensionsReady();
}
desktop_setComponentInstallationSyncHandler(handler) {
@@ -183,7 +201,7 @@ export class DesktopManager {
}
async desktop_requestBackupFile(callback) {
- var keys, authParams;
+ let keys, authParams;
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
keys = this.passcodeManager.keys();
authParams = this.passcodeManager.passcodeAuthParams();
@@ -191,11 +209,11 @@ export class DesktopManager {
keys = await this.authManager.keys();
authParams = await this.authManager.getAuthParams();
}
-
+ const nullOnEmpty = true;
this.modelManager.getAllItemsJSONData(
keys,
authParams,
- true /* return null on empty */
+ nullOnEmpty
).then((data) => {
callback(data);
})
@@ -206,10 +224,12 @@ export class DesktopManager {
}
desktop_didBeginBackup() {
- this.$rootScope.$broadcast("did-begin-local-backup");
+ this.appState.beganBackupDownload();
}
desktop_didFinishBackup(success) {
- this.$rootScope.$broadcast("did-finish-local-backup", {success: success});
+ this.appState.endedBackupDownload({
+ success: success
+ });
}
}
diff --git a/app/assets/javascripts/app/services/httpManager.js b/app/assets/javascripts/services/httpManager.js
similarity index 100%
rename from app/assets/javascripts/app/services/httpManager.js
rename to app/assets/javascripts/services/httpManager.js
diff --git a/app/assets/javascripts/app/services/index.js b/app/assets/javascripts/services/index.js
similarity index 94%
rename from app/assets/javascripts/app/services/index.js
rename to app/assets/javascripts/services/index.js
index f6ff34078..1abc49619 100644
--- a/app/assets/javascripts/app/services/index.js
+++ b/app/assets/javascripts/services/index.js
@@ -18,3 +18,4 @@ export { StorageManager } from './storageManager';
export { SyncManager } from './syncManager';
export { ThemeManager } from './themeManager';
export { AlertManager } from './alertManager';
+export { PreferencesManager } from './preferencesManager';
diff --git a/app/assets/javascripts/app/services/keyboardManager.js b/app/assets/javascripts/services/keyboardManager.js
similarity index 100%
rename from app/assets/javascripts/app/services/keyboardManager.js
rename to app/assets/javascripts/services/keyboardManager.js
diff --git a/app/assets/javascripts/app/services/migrationManager.js b/app/assets/javascripts/services/migrationManager.js
similarity index 100%
rename from app/assets/javascripts/app/services/migrationManager.js
rename to app/assets/javascripts/services/migrationManager.js
diff --git a/app/assets/javascripts/app/services/modelManager.js b/app/assets/javascripts/services/modelManager.js
similarity index 97%
rename from app/assets/javascripts/app/services/modelManager.js
rename to app/assets/javascripts/services/modelManager.js
index 25b7e2c2f..57a3d47f0 100644
--- a/app/assets/javascripts/app/services/modelManager.js
+++ b/app/assets/javascripts/services/modelManager.js
@@ -33,8 +33,12 @@ export class ModelManager extends SFModelManager {
this.handleSignout();
}
+ findTag(title) {
+ return _.find(this.tags, { title: title });
+ }
+
findOrCreateTagByTitle(title) {
- var tag = _.find(this.tags, {title: title})
+ let tag = this.findTag(title);
if(!tag) {
tag = this.createItem({content_type: "Tag", content: {title: title}});
this.addItem(tag);
@@ -88,9 +92,7 @@ export class ModelManager extends SFModelManager {
removeItemLocally(item, callback) {
super.removeItemLocally(item, callback);
-
this.removeItemFromRespectiveArray(item);
-
this.storageManager.deleteModel(item).then(callback);
}
diff --git a/app/assets/javascripts/app/services/nativeExtManager.js b/app/assets/javascripts/services/nativeExtManager.js
similarity index 91%
rename from app/assets/javascripts/app/services/nativeExtManager.js
rename to app/assets/javascripts/services/nativeExtManager.js
index 08eb5f84b..4b99155ba 100644
--- a/app/assets/javascripts/app/services/nativeExtManager.js
+++ b/app/assets/javascripts/services/nativeExtManager.js
@@ -10,8 +10,8 @@ export class NativeExtManager {
this.syncManager = syncManager;
this.singletonManager = singletonManager;
- this.extensionsManagerIdentifier = "org.standardnotes.extensions-manager";
- this.batchManagerIdentifier = "org.standardnotes.batch-manager";
+ this.extManagerId = "org.standardnotes.extensions-manager";
+ this.batchManagerId = "org.standardnotes.batch-manager";
this.systemExtensions = [];
this.resolveExtensionsManager();
@@ -25,7 +25,7 @@ export class NativeExtManager {
resolveExtensionsManager() {
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) => {
// Resolved Singleton
@@ -58,7 +58,6 @@ export class NativeExtManager {
}, (valueCallback) => {
// Safe to create. Create and return object.
let url = window._extensions_manager_location;
- // console.log("Installing Extensions Manager from URL", url);
if(!url) {
console.error("window._extensions_manager_location must be set.");
return;
@@ -66,7 +65,7 @@ export class NativeExtManager {
let packageInfo = {
name: "Extensions",
- identifier: this.extensionsManagerIdentifier
+ identifier: this.extManagerId
}
var item = {
@@ -108,7 +107,7 @@ export class NativeExtManager {
resolveBatchManager() {
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) => {
// Resolved Singleton
@@ -134,7 +133,6 @@ export class NativeExtManager {
}, (valueCallback) => {
// Safe to create. Create and return object.
let url = window._batch_manager_location;
- // console.log("Installing Batch Manager from URL", url);
if(!url) {
console.error("window._batch_manager_location must be set.");
return;
@@ -142,7 +140,7 @@ export class NativeExtManager {
let packageInfo = {
name: "Batch Manager",
- identifier: this.batchManagerIdentifier
+ identifier: this.batchManagerId
}
var item = {
diff --git a/app/assets/javascripts/app/services/passcodeManager.js b/app/assets/javascripts/services/passcodeManager.js
similarity index 100%
rename from app/assets/javascripts/app/services/passcodeManager.js
rename to app/assets/javascripts/services/passcodeManager.js
diff --git a/app/assets/javascripts/services/preferencesManager.js b/app/assets/javascripts/services/preferencesManager.js
new file mode 100644
index 000000000..74ef7a676
--- /dev/null
+++ b/app/assets/javascripts/services/preferencesManager.js
@@ -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();
+ }
+ }
+}
diff --git a/app/assets/javascripts/app/services/privilegesManager.js b/app/assets/javascripts/services/privilegesManager.js
similarity index 74%
rename from app/assets/javascripts/app/services/privilegesManager.js
rename to app/assets/javascripts/services/privilegesManager.js
index 044e63653..3fad35bc7 100644
--- a/app/assets/javascripts/app/services/privilegesManager.js
+++ b/app/assets/javascripts/services/privilegesManager.js
@@ -41,27 +41,29 @@ export class PrivilegesManager extends SFPrivilegesManager {
});
}
- presentPrivilegesModal(action, onSuccess, onCancel) {
- if(this.authenticationInProgress()) {
+ async presentPrivilegesModal(action, onSuccess, onCancel) {
+ if (this.authenticationInProgress()) {
onCancel && onCancel();
return;
}
- let customSuccess = () => {
- onSuccess && onSuccess();
+ const customSuccess = async () => {
+ onSuccess && await onSuccess();
+ this.currentAuthenticationElement = null;
+ }
+ const customCancel = async () => {
+ onCancel && await onCancel();
this.currentAuthenticationElement = null;
}
- let customCancel = () => {
- onCancel && onCancel();
- this.currentAuthenticationElement = null;
- }
-
- var scope = this.$rootScope.$new(true);
+ const scope = this.$rootScope.$new(true);
scope.action = action;
scope.onSuccess = customSuccess;
scope.onCancel = customCancel;
- var el = this.$compile( "" )(scope);
+ const el = this.$compile(`
+
+ `)(scope);
angular.element(document.body).append(el);
this.currentAuthenticationElement = el;
@@ -69,7 +71,7 @@ export class PrivilegesManager extends SFPrivilegesManager {
presentPrivilegesManagementModal() {
var scope = this.$rootScope.$new(true);
- var el = this.$compile( "")(scope);
+ var el = this.$compile("")(scope);
angular.element(document.body).append(el);
}
diff --git a/app/assets/javascripts/app/services/sessionHistory.js b/app/assets/javascripts/services/sessionHistory.js
similarity index 100%
rename from app/assets/javascripts/app/services/sessionHistory.js
rename to app/assets/javascripts/services/sessionHistory.js
diff --git a/app/assets/javascripts/app/services/singletonManager.js b/app/assets/javascripts/services/singletonManager.js
similarity index 100%
rename from app/assets/javascripts/app/services/singletonManager.js
rename to app/assets/javascripts/services/singletonManager.js
diff --git a/app/assets/javascripts/app/services/statusManager.js b/app/assets/javascripts/services/statusManager.js
similarity index 96%
rename from app/assets/javascripts/app/services/statusManager.js
rename to app/assets/javascripts/services/statusManager.js
index d85a3bda6..a4ac84831 100644
--- a/app/assets/javascripts/app/services/statusManager.js
+++ b/app/assets/javascripts/services/statusManager.js
@@ -49,7 +49,7 @@ export class StatusManager {
}
notifyObservers() {
- for(let observer of this.observers) {
+ for(const observer of this.observers) {
observer(this.getStatusString());
}
}
diff --git a/app/assets/javascripts/app/services/storageManager.js b/app/assets/javascripts/services/storageManager.js
similarity index 100%
rename from app/assets/javascripts/app/services/storageManager.js
rename to app/assets/javascripts/services/storageManager.js
diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/services/syncManager.js
similarity index 100%
rename from app/assets/javascripts/app/services/syncManager.js
rename to app/assets/javascripts/services/syncManager.js
diff --git a/app/assets/javascripts/app/services/themeManager.js b/app/assets/javascripts/services/themeManager.js
similarity index 72%
rename from app/assets/javascripts/app/services/themeManager.js
rename to app/assets/javascripts/services/themeManager.js
index 4c66cfdc8..0695f450e 100644
--- a/app/assets/javascripts/app/services/themeManager.js
+++ b/app/assets/javascripts/services/themeManager.js
@@ -2,10 +2,19 @@ import _ from 'lodash';
import angular from 'angular';
import { SNTheme, SFItemParams } from 'snjs';
import { StorageManager } from './storageManager';
+import {
+ APP_STATE_EVENT_DESKTOP_EXTS_READY
+} from '@/state';
export class ThemeManager {
/* @ngInject */
- constructor(componentManager, desktopManager, storageManager, passcodeManager, $rootScope) {
+ constructor(
+ componentManager,
+ desktopManager,
+ storageManager,
+ passcodeManager,
+ appState
+ ) {
this.componentManager = componentManager;
this.storageManager = storageManager;
this.desktopManager = desktopManager;
@@ -22,9 +31,11 @@ export class ThemeManager {
this.cacheThemes();
})
- if(desktopManager.isDesktop) {
- $rootScope.$on("desktop-did-set-ext-server-host", () => {
- this.activateCachedThemes();
+ if (desktopManager.isDesktop) {
+ appState.addObserver((eventName, data) => {
+ if (eventName === APP_STATE_EVENT_DESKTOP_EXTS_READY) {
+ this.activateCachedThemes();
+ }
})
} else {
this.activateCachedThemes();
@@ -32,9 +43,9 @@ export class ThemeManager {
}
activateCachedThemes() {
- let cachedThemes = this.getCachedThemes();
- let writeToCache = false;
- for(var theme of cachedThemes) {
+ const cachedThemes = this.getCachedThemes();
+ const writeToCache = false;
+ for (const theme of cachedThemes) {
this.activateTheme(theme, writeToCache);
}
}
@@ -42,7 +53,7 @@ export class ThemeManager {
registerObservers() {
this.desktopManager.registerUpdateObserver((component) => {
// Reload theme if active
- if(component.active && component.isTheme()) {
+ if (component.active && component.isTheme()) {
this.deactivateTheme(component);
setTimeout(() => {
this.activateTheme(component);
@@ -50,13 +61,17 @@ export class ThemeManager {
}
})
- this.componentManager.registerHandler({identifier: "themeManager", areas: ["themes"], activationHandler: (component) => {
- if(component.active) {
- this.activateTheme(component);
- } else {
- this.deactivateTheme(component);
+ this.componentManager.registerHandler({
+ identifier: "themeManager",
+ areas: ["themes"],
+ activationHandler: (component) => {
+ if (component.active) {
+ this.activateTheme(component);
+ } else {
+ this.deactivateTheme(component);
+ }
}
- }});
+ });
}
hasActiveTheme() {
@@ -65,8 +80,8 @@ export class ThemeManager {
deactivateAllThemes() {
var activeThemes = this.componentManager.getActiveThemes();
- for(var theme of activeThemes) {
- if(theme) {
+ for (var theme of activeThemes) {
+ if (theme) {
this.componentManager.deactivateComponent(theme);
}
}
@@ -75,7 +90,7 @@ export class ThemeManager {
}
activateTheme(theme, writeToCache = true) {
- if(_.find(this.activeThemes, {uuid: theme.uuid})) {
+ if (_.find(this.activeThemes, { uuid: theme.uuid })) {
return;
}
@@ -90,19 +105,19 @@ export class ThemeManager {
link.id = theme.uuid;
document.getElementsByTagName("head")[0].appendChild(link);
- if(writeToCache) {
+ if (writeToCache) {
this.cacheThemes();
}
}
deactivateTheme(theme) {
var element = document.getElementById(theme.uuid);
- if(element) {
+ if (element) {
element.disabled = true;
element.parentNode.removeChild(element);
}
- _.remove(this.activeThemes, {uuid: theme.uuid});
+ _.remove(this.activeThemes, { uuid: theme.uuid });
this.cacheThemes();
}
@@ -123,7 +138,7 @@ export class ThemeManager {
getCachedThemes() {
let cachedThemes = this.storageManager.getItemSync(ThemeManager.CachedThemesKey, StorageManager.Fixed);
- if(cachedThemes) {
+ if (cachedThemes) {
let parsed = JSON.parse(cachedThemes);
return parsed.map((theme) => {
return new SNTheme(theme);
diff --git a/app/assets/javascripts/state.js b/app/assets/javascripts/state.js
new file mode 100644
index 000000000..b58bb9b47
--- /dev/null
+++ b/app/assets/javascripts/state.js
@@ -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
+ );
+ }
+
+}
diff --git a/app/assets/javascripts/strings.js b/app/assets/javascripts/strings.js
new file mode 100644
index 000000000..a47cab6af
--- /dev/null
+++ b/app/assets/javascripts/strings.js
@@ -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.";
diff --git a/app/assets/javascripts/app/utils.js b/app/assets/javascripts/utils.js
similarity index 83%
rename from app/assets/javascripts/app/utils.js
rename to app/assets/javascripts/utils.js
index d448bab22..44f2e3a2c 100644
--- a/app/assets/javascripts/app/utils.js
+++ b/app/assets/javascripts/utils.js
@@ -16,6 +16,10 @@ export function parametersFromURL(url) {
return obj;
}
+export function isNullOrUndefined(value) {
+ return value === null || value === undefined;
+}
+
export function getPlatformString() {
try {
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() {
return window.isElectron;
}
diff --git a/app/assets/stylesheets/app/_editor.scss b/app/assets/stylesheets/_editor.scss
similarity index 100%
rename from app/assets/stylesheets/app/_editor.scss
rename to app/assets/stylesheets/_editor.scss
diff --git a/app/assets/stylesheets/app/_footer.scss b/app/assets/stylesheets/_footer.scss
similarity index 100%
rename from app/assets/stylesheets/app/_footer.scss
rename to app/assets/stylesheets/_footer.scss
diff --git a/app/assets/stylesheets/app/_ionicons.scss b/app/assets/stylesheets/_ionicons.scss
similarity index 100%
rename from app/assets/stylesheets/app/_ionicons.scss
rename to app/assets/stylesheets/_ionicons.scss
diff --git a/app/assets/stylesheets/app/_lock-screen.scss b/app/assets/stylesheets/_lock-screen.scss
similarity index 100%
rename from app/assets/stylesheets/app/_lock-screen.scss
rename to app/assets/stylesheets/_lock-screen.scss
diff --git a/app/assets/stylesheets/app/_main.scss b/app/assets/stylesheets/_main.scss
similarity index 100%
rename from app/assets/stylesheets/app/_main.scss
rename to app/assets/stylesheets/_main.scss
diff --git a/app/assets/stylesheets/app/_menus.scss b/app/assets/stylesheets/_menus.scss
similarity index 100%
rename from app/assets/stylesheets/app/_menus.scss
rename to app/assets/stylesheets/_menus.scss
diff --git a/app/assets/stylesheets/app/_modals.scss b/app/assets/stylesheets/_modals.scss
similarity index 100%
rename from app/assets/stylesheets/app/_modals.scss
rename to app/assets/stylesheets/_modals.scss
diff --git a/app/assets/stylesheets/app/_notes.scss b/app/assets/stylesheets/_notes.scss
similarity index 100%
rename from app/assets/stylesheets/app/_notes.scss
rename to app/assets/stylesheets/_notes.scss
diff --git a/app/assets/stylesheets/app/_stylekit-sub.scss b/app/assets/stylesheets/_stylekit-sub.scss
similarity index 100%
rename from app/assets/stylesheets/app/_stylekit-sub.scss
rename to app/assets/stylesheets/_stylekit-sub.scss
diff --git a/app/assets/stylesheets/app/_tags.scss b/app/assets/stylesheets/_tags.scss
similarity index 100%
rename from app/assets/stylesheets/app/_tags.scss
rename to app/assets/stylesheets/_tags.scss
diff --git a/app/assets/stylesheets/app/_ui.scss b/app/assets/stylesheets/_ui.scss
similarity index 100%
rename from app/assets/stylesheets/app/_ui.scss
rename to app/assets/stylesheets/_ui.scss
diff --git a/app/assets/stylesheets/index.css.scss b/app/assets/stylesheets/index.css.scss
new file mode 100644
index 000000000..89b23f1f6
--- /dev/null
+++ b/app/assets/stylesheets/index.css.scss
@@ -0,0 +1,11 @@
+@import "main";
+@import "ui";
+@import "footer";
+@import "tags";
+@import "notes";
+@import "editor";
+@import "menus";
+@import "modals";
+@import "lock-screen";
+@import "stylekit-sub";
+@import "ionicons";
diff --git a/app/assets/stylesheets/main.css.scss b/app/assets/stylesheets/main.css.scss
deleted file mode 100644
index 9e28dc002..000000000
--- a/app/assets/stylesheets/main.css.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-@import "app/main";
-@import "app/ui";
-@import "app/footer";
-@import "app/tags";
-@import "app/notes";
-@import "app/editor";
-@import "app/menus";
-@import "app/modals";
-@import "app/lock-screen";
-@import "app/stylekit-sub";
-@import "app/ionicons";
diff --git a/app/assets/templates/directives/account-menu.pug b/app/assets/templates/directives/account-menu.pug
index 1d87544fe..81b0dee61 100644
--- a/app/assets/templates/directives/account-menu.pug
+++ b/app/assets/templates/directives/account-menu.pug
@@ -2,126 +2,249 @@
#account-panel.sk-panel
.sk-panel-header
.sk-panel-header-title Account
- a.sk-a.info.close-button(ng-click='close()') Close
+ a.sk-a.info.close-button(ng-click='self.close()') Close
.sk-panel-content
- .sk-panel-section.sk-panel-hero(ng-if='!user && !formData.showLogin && !formData.showRegister && !formData.mfa')
+ .sk-panel-section.sk-panel-hero(
+ ng-if=`
+ !self.state.user &&
+ !self.state.formData.showLogin &&
+ !self.state.formData.showRegister &&
+ !self.state.formData.mfa`
+ )
.sk-panel-row
.sk-h1 Sign in or register to enable sync and end-to-end encryption.
.sk-panel-row
.sk-button-group.stretch
- .sk-button.info.featured(ng-click='formData.showLogin = true')
+ .sk-button.info.featured(ng-click='self.state.formData.showLogin = true')
.sk-label Sign In
- .sk-button.info.featured(ng-click='formData.showRegister = true')
+ .sk-button.info.featured(ng-click='self.state.formData.showRegister = true')
.sk-label Register
.sk-panel-row.sk-p
- | Standard Notes is free on every platform, and comes standard with sync and encryption.
- .sk-panel-section(ng-if='formData.showLogin || formData.showRegister')
+ | Standard Notes is free on every platform, and comes
+ | standard with sync and encryption.
+ .sk-panel-section(ng-if=`
+ self.state.formData.showLogin ||
+ self.state.formData.showRegister`
+ )
.sk-panel-section-title
- | {{formData.showLogin ? "Sign In" : "Register"}}
- form.sk-panel-form(ng-submit='submitAuthForm()')
+ | {{self.state.formData.showLogin ? "Sign In" : "Register"}}
+ form.sk-panel-form(ng-submit='self.submitAuthForm()')
.sk-panel-section
- input.sk-input.contrast(name='email', ng-model='formData.email', ng-model-options='{allowInvalid: true}', placeholder='Email', required='', should-focus='true', sn-autofocus='true', spellcheck='false', type='email')
- input.sk-input.contrast(name='password', ng-model='formData.user_password', placeholder='Password', required='', sn-enter='submitAuthForm()', type='password')
- input.sk-input.contrast(name='password', ng-if='formData.showRegister', ng-model='formData.password_conf', placeholder='Confirm Password', required='', sn-enter='submitAuthForm()', type='password')
+ input.sk-input.contrast(
+ name='email',
+ ng-model='self.state.formData.email',
+ ng-model-options='{allowInvalid: true}',
+ placeholder='Email',
+ required='',
+ should-focus='true',
+ sn-autofocus='true',
+ spellcheck='false',
+ type='email'
+ )
+ input.sk-input.contrast(
+ name='password',
+ ng-model='self.state.formData.user_password',
+ placeholder='Password',
+ required='',
+ sn-enter='self.submitAuthForm()',
+ type='password'
+ )
+ input.sk-input.contrast(
+ name='password',
+ ng-if='self.state.formData.showRegister',
+ ng-model='self.state.formData.password_conf',
+ placeholder='Confirm Password',
+ required='',
+ sn-enter='self.submitAuthForm()',
+ type='password'
+ )
.sk-panel-row
- a.sk-panel-row.sk-bold(ng-click='formData.showAdvanced = !formData.showAdvanced')
+ a.sk-panel-row.sk-bold(
+ ng-click=`
+ self.state.formData.showAdvanced = !self.state.formData.showAdvanced
+ `
+ )
| Advanced Options
- .sk-notification.unpadded.contrast.advanced-options.sk-panel-row(ng-if='formData.showAdvanced')
+ .sk-notification.unpadded.contrast.advanced-options.sk-panel-row(
+ ng-if='self.state.formData.showAdvanced'
+ )
.sk-panel-column.stretch
.sk-notification-title.sk-panel-row.padded-row Advanced Options
.bordered-row.padded-row
label.sk-label Sync Server Domain
- input.sk-input.mt-5.sk-base(name='server', ng-model='formData.url', placeholder='Server URL', required='', type='text')
- label.sk-label.padded-row(ng-if='formData.showLogin')
- input.sk-input(ng-model='formData.strictSignin', type='checkbox')
+ input.sk-input.mt-5.sk-base(
+ name='server',
+ ng-model='self.state.formData.url',
+ placeholder='Server URL',
+ required='',
+ type='text'
+ )
+ label.sk-label.padded-row(ng-if='self.state.formData.showLogin')
+ input.sk-input(
+ ng-model='self.state.formData.strictSignin',
+ type='checkbox'
+ )
| Use strict sign in
span
- a.info(href='https://standardnotes.org/help/security', rel='noopener', target='_blank') (Learn more)
- .sk-panel-section.form-submit(ng-if='!formData.authenticating')
+ a.info(
+ href='https://standardnotes.org/help/security',
+ rel='noopener',
+ target='_blank'
+ ) (Learn more)
+ .sk-panel-section.form-submit(ng-if='!self.state.formData.authenticating')
.sk-button-group.stretch
- .sk-button.info.featured(ng-click='submitAuthForm()', ng-disabled='formData.authenticating')
- .sk-label {{formData.showLogin ? "Sign In" : "Register"}}
- .sk-notification.neutral(ng-if='formData.showRegister')
+ .sk-button.info.featured(
+ ng-click='self.submitAuthForm()',
+ ng-disabled='self.state.formData.authenticating'
+ )
+ .sk-label {{self.state.formData.showLogin ? "Sign In" : "Register"}}
+ .sk-notification.neutral(ng-if='self.state.formData.showRegister')
.sk-notification-title No Password Reset.
.sk-notification-text
- | Because your notes are encrypted using your password, Standard Notes does not have a password reset option. You cannot forget your password.
- .sk-panel-section.no-bottom-pad(ng-if='formData.status')
+ | Because your notes are encrypted using your password,
+ | Standard Notes does not have a password reset option.
+ | You cannot forget your password.
+ .sk-panel-section.no-bottom-pad(ng-if='self.state.formData.status')
.sk-horizontal-group
.sk-spinner.small.neutral
- .sk-label {{formData.status}}
- .sk-panel-section.no-bottom-pad(ng-if='!formData.authenticating')
+ .sk-label {{self.state.formData.status}}
+ .sk-panel-section.no-bottom-pad(ng-if='!self.state.formData.authenticating')
label.sk-panel-row.justify-left
.sk-horizontal-group
- input(ng-false-value='true', ng-model='formData.ephemeral', ng-true-value='false', type='checkbox')
+ input(
+ ng-false-value='true',
+ ng-model='self.state.formData.ephemeral',
+ ng-true-value='false',
+ type='checkbox'
+ )
| Stay signed in
- label.sk-panel-row.justify-left(ng-if='notesAndTagsCount() > 0')
+ label.sk-panel-row.justify-left(ng-if='self.notesAndTagsCount() > 0')
.sk-panel-row
- input(ng-bind='true', ng-change='mergeLocalChanged()', ng-model='formData.mergeLocal', type='checkbox')
- | Merge local data ({{notesAndTagsCount()}} notes and tags)
- .sk-panel-section(ng-if='formData.mfa')
- form.sk-panel-form(ng-submit='submitMfaForm()')
- .sk-p.sk-panel-row {{formData.mfa.message}}
+ input(
+ ng-bind='true',
+ ng-change='self.mergeLocalChanged()',
+ ng-model='self.state.formData.mergeLocal',
+ type='checkbox'
+ )
+ | Merge local data ({{self.notesAndTagsCount()}} notes and tags)
+ .sk-panel-section(ng-if='self.state.formData.mfa')
+ form.sk-panel-form(ng-submit='self.submitMfaForm()')
+ .sk-p.sk-panel-row {{self.state.formData.mfa.message}}
.sk-panel-row
- input.sk-input.contrast(autofocus='true', name='mfa', ng-model='formData.userMfaCode', placeholder='Enter Code', required='', should-focus='true', sn-autofocus='true')
- .sk-button-group.stretch.sk-panel-row.form-submit(ng-if='!formData.status')
+ input.sk-input.contrast(
+ autofocus='true',
+ name='mfa',
+ ng-model='self.state.formData.userMfaCode',
+ placeholder='Enter Code',
+ required='',
+ should-focus='true',
+ sn-autofocus='true'
+ )
+ .sk-button-group.stretch.sk-panel-row.form-submit(
+ ng-if='!self.state.formData.status'
+ )
button.sk-button.info.featured(type='submit')
.sk-label Sign In
- .sk-panel-section.no-bottom-pad(ng-if='formData.status')
+ .sk-panel-section.no-bottom-pad(ng-if='self.state.formData.status')
.sk-panel-row
.sk-panel-row
.sk-horizontal-group
.sk-spinner.small.neutral
- .sk-label {{formData.status}}
- div(ng-if='!formData.showLogin && !formData.showRegister && !formData.mfa')
- .sk-panel-section(ng-if='user')
- .sk-notification.danger(ng-if='syncStatus.error')
+ .sk-label {{self.state.formData.status}}
+ div(
+ ng-if=`
+ !self.state.formData.showLogin &&
+ !self.state.formData.showRegister &&
+ !self.state.formData.mfa`
+ )
+ .sk-panel-section(ng-if='self.state.user')
+ .sk-notification.danger(ng-if='self.syncStatus.error')
.sk-notification-title Sync Unreachable
.sk-notification-text
- | Hmm...we can't seem to sync your account. The reason: {{syncStatus.error.message}}
- a.sk-a.info-contrast.sk-bold.sk-panel-row(href='https://standardnotes.org/help', rel='noopener', target='_blank') Need help?
+ | Hmm...we can't seem to sync your account.
+ | The reason: {{self.syncStatus.error.message}}
+ a.sk-a.info-contrast.sk-bold.sk-panel-row(
+ href='https://standardnotes.org/help',
+ rel='noopener',
+ target='_blank'
+ ) Need help?
.sk-panel-row
.sk-panel-column
- .sk-h1.sk-bold.wrap {{user.email}}
- .sk-subtitle.subtle.normal {{server}}
- .sk-horizontal-group(delay='1000', delay-hide='true', show='syncStatus.syncOpInProgress || syncStatus.needsMoreSync')
+ .sk-h1.sk-bold.wrap {{self.state.user.email}}
+ .sk-subtitle.subtle.normal {{self.state.server}}
+ .sk-horizontal-group(
+ delay='1000',
+ delay-hide='true',
+ show='self.syncStatus.syncOpInProgress || self.syncStatus.needsMoreSync'
+ )
.sk-spinner.small.info
.sk-sublabel
- | {{"Syncing" + (syncStatus.total > 0 ? ":" : "")}}
- span(ng-if='syncStatus.total > 0') {{syncStatus.current}}/{{syncStatus.total}}
+ | {{"Syncing" + (self.syncStatus.total > 0 ? ":" : "")}}
+ span(
+ ng-if='self.syncStatus.total > 0'
+ ) {{self.syncStatus.current}}/{{self.syncStatus.total}}
.sk-panel-row
- a.sk-a.info.sk-panel-row.condensed(ng-click="openPasswordWizard('change-pw')")
+ a.sk-a.info.sk-panel-row.condensed(
+ ng-click="self.openPasswordWizard('change-pw')"
+ )
| Change Password
- a.sk-a.info.sk-panel-row.condensed(ng-click="openPrivilegesModal('')", ng-show='user')
+ a.sk-a.info.sk-panel-row.condensed(
+ ng-click="self.openPrivilegesModal('')",
+ ng-show='self.state.user'
+ )
| Manage Privileges
- a.sk-panel-row.justify-left.condensed.success(ng-click="openPasswordWizard('upgrade-security')", ng-if='securityUpdateAvailable')
+ a.sk-panel-row.justify-left.condensed.success(
+ ng-click="self.openPasswordWizard('upgrade-security')",
+ ng-if='self.state.securityUpdateAvailable'
+ )
.inline.sk-circle.small.success.mr-8
.inline Security Update Available
.sk-panel-section
.sk-panel-section-title Encryption
- .sk-panel-section-subtitle.info(ng-if='encryptionEnabled()')
- | {{encryptionStatusForNotes()}}
+ .sk-panel-section-subtitle.info(ng-if='self.encryptionEnabled()')
+ | {{self.encryptionStatusForNotes()}}
p.sk-p
- | {{encryptionStatusString()}}
+ | {{self.encryptionStatusString()}}
.sk-panel-section
.sk-panel-section-title Passcode Lock
- div(ng-if='!hasPasscode()')
- div(ng-if='canAddPasscode')
- .sk-panel-row(ng-if='!formData.showPasscodeForm')
- .sk-button.info(ng-click='addPasscodeClicked(); $event.stopPropagation();')
+ div(ng-if='!self.hasPasscode()')
+ div(ng-if='self.state.canAddPasscode')
+ .sk-panel-row(ng-if='!self.state.formData.showPasscodeForm')
+ .sk-button.info(
+ ng-click='self.addPasscodeClicked(); $event.stopPropagation();'
+ )
.sk-label Add Passcode
- p.sk-p Add a passcode to lock the application and encrypt on-device key storage.
- div(ng-if='!canAddPasscode')
+ p.sk-p
+ | Add a passcode to lock the application and
+ | encrypt on-device key storage.
+ div(ng-if='!self.state.canAddPasscode')
p.sk-p
- | Adding a passcode is not supported in temporary sessions. Please sign out, then sign back in with the "Stay signed in" option checked.
- form.sk-panel-form(ng-if='formData.showPasscodeForm', ng-submit='submitPasscodeForm()')
+ | Adding a passcode is not supported in temporary sessions. Please sign
+ | out, then sign back in with the "Stay signed in" option checked.
+ form.sk-panel-form(
+ ng-if='self.state.formData.showPasscodeForm',
+ ng-submit='self.submitPasscodeForm()'
+ )
.sk-panel-row
- input.sk-input.contrast(ng-model='formData.passcode', placeholder='Passcode', should-focus='true', sn-autofocus='true', type='password')
- input.sk-input.contrast(ng-model='formData.confirmPasscode', placeholder='Confirm Passcode', type='password')
+ input.sk-input.contrast(
+ ng-model='self.state.formData.passcode',
+ placeholder='Passcode',
+ should-focus='true',
+ sn-autofocus='true',
+ type='password'
+ )
+ input.sk-input.contrast(
+ ng-model='self.state.formData.confirmPasscode',
+ placeholder='Confirm Passcode',
+ type='password'
+ )
.sk-button-group.stretch.sk-panel-row.form-submit
button.sk-button.info(type='submit')
.sk-label Set Passcode
- a.neutral.sk-a.sk-panel-row(ng-click='formData.showPasscodeForm = false') Cancel
- div(ng-if='hasPasscode() && !formData.showPasscodeForm')
+ a.neutral.sk-a.sk-panel-row(
+ ng-click='self.state.formData.showPasscodeForm = false'
+ ) Cancel
+ div(ng-if='self.hasPasscode() && !self.state.formData.showPasscodeForm')
.sk-p
| Passcode lock is enabled.
.sk-notification.contrast
@@ -130,51 +253,98 @@
.sk-panel-row
.sk-horizontal-group
.sk-h4.sk-bold Autolock
- a.sk-a.info(ng-class="{'boxed' : option.value == selectedAutoLockInterval}", ng-click='selectAutoLockInterval(option.value)', ng-repeat='option in passcodeAutoLockOptions')
+ a.sk-a.info(
+ ng-class=`{
+ 'boxed' : option.value == self.state.selectedAutoLockInterval
+ }`,
+ ng-click='self.selectAutoLockInterval(option.value)',
+ ng-repeat='option in self.state.passcodeAutoLockOptions'
+ )
| {{option.label}}
.sk-p The autolock timer begins when the window or tab loses focus.
.sk-panel-row
- a.sk-a.info.sk-panel-row.condensed(ng-click="openPrivilegesModal('')", ng-show='!user') Manage Privileges
- a.sk-a.info.sk-panel-row.condensed(ng-click='changePasscodePressed()') Change Passcode
- a.sk-a.danger.sk-panel-row.condensed(ng-click='removePasscodePressed()') Remove Passcode
- .sk-panel-section(ng-if='!importData.loading')
+ a.sk-a.info.sk-panel-row.condensed(
+ ng-click="self.openPrivilegesModal('')",
+ ng-show='!self.state.user'
+ ) Manage Privileges
+ a.sk-a.info.sk-panel-row.condensed(
+ ng-click='self.changePasscodePressed()'
+ ) Change Passcode
+ a.sk-a.danger.sk-panel-row.condensed(
+ ng-click='self.removePasscodePressed()'
+ ) Remove Passcode
+ .sk-panel-section(ng-if='!self.state.importData.loading')
.sk-panel-section-title Data Backups
.sk-p
| Download a backup of all your data.
.sk-panel-row
- form.sk-panel-form.sk-panel-row(ng-if='encryptedBackupsAvailable()')
+ form.sk-panel-form.sk-panel-row(ng-if='self.encryptedBackupsAvailable()')
.sk-input-group
label
- input(ng-change='archiveFormData.encrypted = true', ng-model='archiveFormData.encrypted', ng-value='true', type='radio')
+ input(
+ ng-change='self.state.mutable.backupEncrypted = true',
+ ng-model='self.state.mutable.backupEncrypted',
+ ng-value='true',
+ type='radio'
+ )
| Encrypted
label
- input(ng-change='archiveFormData.encrypted = false', ng-model='archiveFormData.encrypted', ng-value='false', type='radio')
+ input(
+ ng-change='self.state.mutable.backupEncrypted = false',
+ ng-model='self.state.mutable.backupEncrypted',
+ ng-value='false',
+ type='radio'
+ )
| Decrypted
.sk-button-group.sk-panel-row.justify-left
- .sk-button.info(ng-click='downloadDataArchive()')
+ .sk-button.info(ng-click='self.downloadDataArchive()')
.sk-label Download Backup
label.sk-button.info
- input(file-change='->', handler='importFileSelected(files)', style='display: none;', type='file')
+ input(
+ file-change='->',
+ handler='self.importFileSelected(files)',
+ style='display: none;',
+ type='file'
+ )
.sk-label Import Backup
- span(ng-if='isDesktopApplication()')
- | Backups are automatically created on desktop and can be managed via the "Backups" top-level menu.
- #import-password-request(ng-if='importData.requestPassword')
- form.sk-panel-form.stretch(ng-submit='submitImportPassword()')
+ span(ng-if='self.isDesktopApplication()')
+ | Backups are automatically created on desktop and can be managed
+ | via the "Backups" top-level menu.
+ #import-password-request(ng-if='self.state.importData.requestPassword')
+ form.sk-panel-form.stretch(ng-submit='self.submitImportPassword()')
p Enter the account password associated with the import file.
- input.sk-input.contrast.mt-5(autofocus='true', ng-model='importData.password', placeholder='Enter File Account Password', type='password')
+ input.sk-input.contrast.mt-5(
+ autofocus='true',
+ ng-model='self.state.importData.password',
+ placeholder='Enter File Account Password',
+ type='password'
+ )
.sk-button-group.stretch.sk-panel-row.form-submit
button.sk-button.info(type='submit')
.sk-label Decrypt & Import
p
- | Importing from backup will not overwrite existing data, but instead create a duplicate of any differing data.
+ | Importing from backup will not overwrite existing data,
+ | but instead create a duplicate of any differing data.
p
- | If you'd like to import only a selection of items instead of the whole file, please use the Batch Manager extension.
+ | If you'd like to import only a selection of items instead of
+ | the whole file, please use the Batch Manager extension.
.sk-panel-row
- .sk-spinner.small.info(ng-if='importData.loading')
+ .sk-spinner.small.info(ng-if='self.state.importData.loading')
.sk-panel-footer
.sk-panel-row
- .sk-p.left.neutral.faded {{appVersion}}
- a.sk-a.right(ng-click='formData.showLogin = false; formData.showRegister = false;', ng-if='formData.showLogin || formData.showRegister')
+ .sk-p.left.neutral.faded {{self.state.appVersion}}
+ a.sk-a.right(
+ ng-click=`
+ self.state.formData.showLogin = false;
+ self.state.formData.showRegister = false;
+ `,
+ ng-if='self.state.formData.showLogin || self.state.formData.showRegister'
+ )
| Cancel
- a.sk-a.right.danger(ng-click='destroyLocalData()', ng-if='!formData.showLogin && !formData.showRegister')
- | {{ user ? "Sign out and clear local data" : "Clear all local data" }}
+ a.sk-a.right.danger(
+ ng-click='self.destroyLocalData()',
+ ng-if=`
+ !self.state.formData.showLogin &&
+ !self.state.formData.showRegister`
+ )
+ | {{ self.state.user ? "Sign out and clear local data" : "Clear all local data" }}
diff --git a/app/assets/templates/directives/actions-menu.pug b/app/assets/templates/directives/actions-menu.pug
index 864562eaf..aa7345c66 100644
--- a/app/assets/templates/directives/actions-menu.pug
+++ b/app/assets/templates/directives/actions-menu.pug
@@ -1,16 +1,35 @@
.sn-component
.sk-menu-panel.dropdown-menu
- a.no-decoration(href='https://standardnotes.org/extensions', ng-if='extensions.length == 0', rel='noopener', target='blank')
+ a.no-decoration(
+ href='https://standardnotes.org/extensions',
+ ng-if='self.state.extensions.length == 0',
+ rel='noopener',
+ target='blank'
+ )
menu-row(label="'Download Actions'")
- div(ng-repeat='extension in extensions')
- .sk-menu-panel-header(ng-click='extension.hide = !extension.hide; $event.stopPropagation();')
+ div(ng-repeat='extension in self.state.extensions')
+ .sk-menu-panel-header(
+ ng-click='extension.hide = !extension.hide; $event.stopPropagation();'
+ )
.sk-menu-panel-column
.sk-menu-panel-header-title {{extension.name}}
.sk-spinner.small.loading(ng-if='extension.loading')
div(ng-if='extension.hide') …
- menu-row(action='executeAction(action, extension);', label='action.label', ng-if='!extension.hide', ng-repeat='action in extension.actionsWithContextForItem(item)', spinner-class="action.running ? 'info' : null", sub-rows='action.subrows', subtitle='action.desc')
+ menu-row(
+ action='self.executeAction(action, extension);',
+ label='action.label',
+ ng-if='!extension.hide',
+ ng-repeat='action in extension.actionsWithContextForItem(self.props.item)',
+ spinner-class="action.running ? 'info' : null",
+ sub-rows='action.subrows',
+ subtitle='action.desc'
+ )
.sk-sublabel(ng-if="action.access_type")
| Uses
strong {{action.access_type}}
| access to this note.
- menu-row(faded='true', label="'No Actions Available'", ng-if='extension.actionsWithContextForItem(item).length == 0')
+ menu-row(
+ faded='true',
+ label="'No Actions Available'",
+ ng-if='extension.actionsWithContextForItem(self.props.item).length == 0'
+ )
diff --git a/app/assets/templates/directives/component-modal.pug b/app/assets/templates/directives/component-modal.pug
index 910e7a085..5e5c82efa 100644
--- a/app/assets/templates/directives/component-modal.pug
+++ b/app/assets/templates/directives/component-modal.pug
@@ -1,9 +1,15 @@
-.sk-modal-background(ng-click="dismiss()")
-.sk-modal-content(ng-attr-id="component-content-outer-{{component.uuid}}")
+.sk-modal-background(ng-click="ctrl.dismiss()")
+.sk-modal-content(
+ ng-attr-id="component-content-outer-{{ctrl.component.uuid}}"
+ )
.sn-component
- .sk-panel(ng-attr-id="component-content-inner-{{component.uuid}}")
+ .sk-panel(
+ ng-attr-id="component-content-inner-{{ctrl.component.uuid}}"
+ )
.sk-panel-header
.sk-panel-header-title
- | {{component.name}}
- a.sk-a.info.close-button(ng-click="dismiss()") Close
- component-view.component-view(component="component")
+ | {{ctrl.component.name}}
+ a.sk-a.info.close-button(ng-click="ctrl.dismiss()") Close
+ component-view.component-view(
+ component="ctrl.component"
+ )
diff --git a/app/assets/templates/directives/component-view.pug b/app/assets/templates/directives/component-view.pug
index 56c15096f..59a8a388d 100644
--- a/app/assets/templates/directives/component-view.pug
+++ b/app/assets/templates/directives/component-view.pug
@@ -1,23 +1,23 @@
-.sn-component(ng-if='issueLoading')
+.sn-component(ng-if='ctrl.issueLoading')
.sk-app-bar.no-edges.no-top-edge.dynamic-height
.left
.sk-app-bar-item
- .sk-label.warning There was an issue loading {{component.name}}.
+ .sk-label.warning There was an issue loading {{ctrl.component.name}}.
.right
- .sk-app-bar-item(ng-click='reloadComponent()')
+ .sk-app-bar-item(ng-click='ctrl.reloadComponent()')
.sk-button.info
.sk-label Reload
-.sn-component(ng-if='showNoThemesMessage')
+.sn-component(ng-if='ctrl.showNoThemesMessage')
.sk-app-bar.no-edges.no-top-edge.dynamic-height
.left
.sk-app-bar-item
.sk-label.warning This extension does not support themes.
.right
- .sk-app-bar-item(ng-click='noThemesMessageDismiss()')
+ .sk-app-bar-item(ng-click='ctrl.dismissNoThemesMessage()')
.sk-label Dismiss
- .sk-app-bar-item(ng-click='disableActiveTheme()')
+ .sk-app-bar-item(ng-click='ctrl.disableActiveTheme()')
.sk-label Disable Active Theme
-.sn-component(ng-if='expired')
+.sn-component(ng-if='ctrl.expired')
.sk-app-bar.no-edges.no-top-edge.dynamic-height
.left
.sk-app-bar-item
@@ -25,19 +25,29 @@
.sk-circle.danger.small
.sk-app-bar-item-column
div
- a.sk-label.sk-base(href='https://dashboard.standardnotes.org', rel='noopener', target='_blank')
- | Your Extended subscription expired on {{component.dateToLocalizedString(component.valid_until)}}.
+ a.sk-label.sk-base(
+ href='https://dashboard.standardnotes.org',
+ rel='noopener',
+ target='_blank'
+ )
+ | Your Extended subscription expired on
+ | {{ctrl.component.dateToLocalizedString(ctrl.component.valid_until)}}.
.sk-p
| Extensions are in a read-only state.
.right
- .sk-app-bar-item(ng-click='reloadComponent()')
+ .sk-app-bar-item(ng-click='ctrl.reloadComponent()')
.sk-button.info
.sk-label Reload
.sk-app-bar-item
.sk-app-bar-item-column
.sk-button.warning
- a.sk-label(href='https://standardnotes.org/help/41/expired', rel='noopener', target='_blank') Help
-.sn-component(ng-if="error == 'offline-restricted'")
+ a.sk-label(
+ href='https://standardnotes.org/help/41/expired',
+ rel='noopener',
+ target='_blank'
+ ) Help
+
+.sn-component(ng-if="ctrl.error == 'offline-restricted'")
.sk-panel.static
.sk-panel-content
.sk-panel-section.stretch
@@ -51,22 +61,35 @@
ul
li.sk-p
strong Enable the Hosted option
- | for this extension by opening the 'Extensions' menu and toggling 'Use hosted when local is unavailable' under this extension's options. Then press Reload below.
+ | for this extension by opening the 'Extensions' menu and
+ | toggling 'Use hosted when local is unavailable' under this
+ | extension's options. Then press Reload below.
li.sk-p
strong Use the Desktop application.
.sk-panel-row
- .sk-button.info(ng-click='reloadStatus()', ng-if='!reloading')
+ .sk-button.info(
+ ng-click='ctrl.reloadStatus()',
+ ng-if='!ctrl.reloading'
+ )
.sk-label Reload
- .sk-spinner.info.small(ng-if='reloading')
-.sn-component(ng-if="error == 'url-missing'")
+ .sk-spinner.info.small(ng-if='ctrl.reloading')
+.sn-component(ng-if="ctrl.error == 'url-missing'")
.sk-panel.static
.sk-panel-content
.sk-panel-section.stretch
.sk-panel-section-title This extension is not installed correctly.
- p Please uninstall {{component.name}}, then re-install it.
+ p Please uninstall {{ctrl.component.name}}, then re-install it.
p
- | This issue can occur if you access Standard Notes using an older version of the app.
+ | This issue can occur if you access Standard Notes using an older
+ | version of the app.
| Ensure you are running at least version 2.1 on all platforms.
-iframe(data-component-id='{{component.uuid}}', frameborder='0', ng-attr-id='component-iframe-{{component.uuid}}', ng-if='component && componentValid', ng-src='{{getUrl() | trusted}}', sandbox='allow-scripts allow-top-navigation-by-user-activation allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-modals allow-forms')
+iframe(
+ data-component-id='{{ctrl.component.uuid}}',
+ frameborder='0',
+ ng-attr-id='component-iframe-{{ctrl.component.uuid}}',
+ ng-if='ctrl.component && ctrl.componentValid',
+ ng-src='{{ctrl.getUrl() | trusted}}',
+ sandbox='allow-scripts allow-top-navigation-by-user-activation allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-modals allow-forms'
+ )
| Loading
-.loading-overlay(ng-if='loading')
+.loading-overlay(ng-if='ctrl.loading')
diff --git a/app/assets/templates/directives/conflict-resolution-modal.pug b/app/assets/templates/directives/conflict-resolution-modal.pug
index 93b32ee9c..9a4d4fdf5 100644
--- a/app/assets/templates/directives/conflict-resolution-modal.pug
+++ b/app/assets/templates/directives/conflict-resolution-modal.pug
@@ -6,21 +6,26 @@
.sk-panel-header
h1.sk-panel-header-title Conflicted items — choose which version to keep
.sk-horizontal-group
- a.sk-a.info.close-button(ng-click="keepItem1()") Keep left
- a.sk-a.info.close-button(ng-click="keepItem2()") Keep right
- a.sk-a.info.close-button(ng-click="keepBoth()") Keep both
- a.sk-a.info.close-button(ng-click="export()") Export
- a.sk-a.info.close-button(ng-click="dismiss(); $event.stopPropagation()") Close
+ a.sk-a.info.close-button(ng-click="ctrl.keepItem1()") Keep left
+ a.sk-a.info.close-button(ng-click="ctrl.keepItem2()") Keep right
+ a.sk-a.info.close-button(ng-click="ctrl.keepBoth()") Keep both
+ a.sk-a.info.close-button(ng-click="ctrl.export()") Export
+ a.sk-a.info.close-button(
+ ng-click="ctrl.dismiss(); $event.stopPropagation()"
+ ) Close
.sk-panel-content.selectable
.sk-panel-section
h3
strong Content type:
- | {{contentType}}
+ | {{ctrl.contentType}}
p
- | You may wish to look at the "created_at" and "updated_at" fields of the items to gain better context in deciding which to keep.
+ | You may wish to look at the "created_at" and "updated_at" fields
+ | of the items to gain better context in deciding which to keep.
#items
#item1.sk-panel.static.item
- p.normal(style="white-space: pre-wrap; font-size: 16px;") {{item1Content}}
+ p.normal(style="white-space: pre-wrap; font-size: 16px;")
+ | {{ctrl.item1Content}}
.border
#item2.sk-panel.static.item
- p.normal(style="white-space: pre-wrap; font-size: 16px;") {{item2Content}}
+ p.normal(style="white-space: pre-wrap; font-size: 16px;")
+ | {{ctrl.item2Content}}
diff --git a/app/assets/templates/directives/editor-menu.pug b/app/assets/templates/directives/editor-menu.pug
index 51309ea34..b62c43344 100644
--- a/app/assets/templates/directives/editor-menu.pug
+++ b/app/assets/templates/directives/editor-menu.pug
@@ -3,10 +3,34 @@
.sk-menu-panel-section
.sk-menu-panel-header
.sk-menu-panel-header-title Note Editor
- menu-row(action='selectComponent(null)', circle="selectedEditor == null && 'success'", label="'Plain Editor'")
- menu-row(action='selectComponent(editor)', button-action='toggleDefaultForEditor(editor)', button-class="defaultEditor == editor ? 'warning' : 'info'", button-text="defaultEditor == editor ? 'Undefault' : 'Set Default'", circle="selectedEditor === editor && 'success'", has-button='selectedEditor == editor || defaultEditor == editor', label='editor.name', ng-repeat='editor in editors')
- .sk-menu-panel-column(ng-if='component.content.conflict_of || shouldDisplayRunningLocallyLabel(editor)')
- strong.danger.medium-text(ng-if='editor.content.conflict_of') Conflicted copy
- .sk-sublabel(ng-if='shouldDisplayRunningLocallyLabel(editor)') Running Locally
- a.no-decoration(href='https://standardnotes.org/extensions', ng-if='editors.length == 0', rel='noopener', target='blank')
+ menu-row(
+ action='self.selectComponent(null)',
+ circle="self.selectedEditor == null && 'success'",
+ label="'Plain Editor'"
+ )
+ menu-row(
+ ng-repeat='editor in self.state.editors'
+ action='self.selectComponent(editor)',
+ button-action='self.toggleDefaultForEditor(editor)',
+ button-class="self.state.defaultEditor == editor ? 'warning' : 'info'",
+ button-text="self.state.defaultEditor == editor ? 'Undefault' : 'Set Default'",
+ circle="self.selectedEditor === editor && 'success'",
+ has-button='self.selectedEditor == editor || self.state.defaultEditor == editor',
+ label='editor.name',
+ )
+ .sk-menu-panel-column(
+ ng-if='editor.content.conflict_of || self.shouldDisplayRunningLocallyLabel(editor)'
+ )
+ strong.danger.medium-text(
+ ng-if='editor.content.conflict_of'
+ ) Conflicted copy
+ .sk-sublabel(
+ ng-if='self.shouldDisplayRunningLocallyLabel(editor)'
+ ) Running Locally
+ a.no-decoration(
+ href='https://standardnotes.org/extensions',
+ ng-if='self.state.editors.length == 0',
+ rel='noopener',
+ target='blank'
+ )
menu-row(label="'Download More Editors'")
diff --git a/app/assets/templates/directives/input-modal.pug b/app/assets/templates/directives/input-modal.pug
index e1852835d..e6c0ded10 100644
--- a/app/assets/templates/directives/input-modal.pug
+++ b/app/assets/templates/directives/input-modal.pug
@@ -5,15 +5,21 @@
.sn-component
.sk-panel
.sk-panel-header
- .sk-h1.sk-panel-header-title {{title}}
- a.sk-a.info.close-button(ng-click="dismiss()") Close
+ .sk-h1.sk-panel-header-title {{ctrl.title}}
+ a.sk-a.info.close-button(ng-click="ctrl.dismiss()") Close
.sk-panel-content
.sk-panel-section
- .sk-p.sk-panel-row {{message}}
+ .sk-p.sk-panel-row {{ctrl.message}}
.sk-panel-row
.sk-panel-column.stretch
- form(ng-submit="submit()")
- input.sk-input.contrast(ng-model="formData.input" placeholder="{{placeholder}}" should-focus="true" sn-autofocus="true" type="{{type}}")
+ form(ng-submit="ctrl.submit()")
+ input.sk-input.contrast(
+ ng-model="ctrl.formData.input"
+ placeholder="{{ctrl.placeholder}}"
+ should-focus="true"
+ sn-autofocus="true"
+ type="{{ctrl.type}}"
+ )
.sk-panel-footer
- a.sk-a.info.right(ng-click="submit()")
+ a.sk-a.info.right(ng-click="ctrl.submit()")
| Submit
diff --git a/app/assets/templates/directives/menu-row.pug b/app/assets/templates/directives/menu-row.pug
index caa286553..20b33ffad 100644
--- a/app/assets/templates/directives/menu-row.pug
+++ b/app/assets/templates/directives/menu-row.pug
@@ -1,20 +1,39 @@
-.sk-menu-panel-row.row(ng-attr-title='{{desc}}', ng-click='onClick($event)')
+.sk-menu-panel-row.row(
+ ng-attr-title='{{ctrl.desc}}',
+ ng-click='ctrl.onClick($event)'
+ )
.sk-menu-panel-column
.left
- .sk-menu-panel-column(ng-if="circle && (!circleAlign || circleAlign == 'left')")
- .sk-circle.small(ng-class='circle')
- .sk-menu-panel-column(ng-class="{'faded' : faded || disabled}")
- .sk-label(ng-class='stylekitClass')
- | {{label}}
- .sk-sublabel(ng-if='subtitle')
- | {{subtitle}}
+ .sk-menu-panel-column(
+ ng-if=`
+ ctrl.circle &&
+ (!ctrl.circleAlign || ctrl.circleAlign == 'left')
+ `
+ )
+ .sk-circle.small(ng-class='ctrl.circle')
+ .sk-menu-panel-column(
+ ng-class="{'faded' : ctrl.faded || ctrl.disabled}"
+ )
+ .sk-label(ng-class='ctrl.stylekitClass')
+ | {{ctrl.label}}
+ .sk-sublabel(ng-if='ctrl.subtitle')
+ | {{ctrl.subtitle}}
ng-transclude
- .sk-menu-panel-subrows(ng-if='subRows && subRows.length > 0')
- menu-row(action='row.onClick()', label='row.label', ng-repeat='row in subRows', spinner-class='row.spinnerClass', subtitle='row.subtitle')
- .sk-menu-panel-column(ng-if="circle && circleAlign == 'right'")
- .sk-circle.small(ng-class='circle')
- .sk-menu-panel-column(ng-if='hasButton')
- .sk-button(ng-class='buttonClass', ng-click='clickButton($event)')
- .sk-label {{buttonText}}
- .sk-menu-panel-column(ng-if='spinnerClass')
- .sk-spinner.small(ng-class='spinnerClass')
+ .sk-menu-panel-subrows(ng-if='ctrl.subRows && ctrl.subRows.length > 0')
+ menu-row(
+ ng-repeat='row in ctrl.subRows',
+ action='row.onClick()',
+ label='row.label',
+ spinner-class='row.spinnerClass',
+ subtitle='row.subtitle'
+ )
+ .sk-menu-panel-column(ng-if="ctrl.circle && ctrl.circleAlign == 'right'")
+ .sk-circle.small(ng-class='ctrl.circle')
+ .sk-menu-panel-column(ng-if='ctrl.hasButton')
+ .sk-button(
+ ng-class='ctrl.buttonClass',
+ ng-click='ctrl.clickAccessoryButton($event)'
+ )
+ .sk-label {{ctrl.buttonText}}
+ .sk-menu-panel-column(ng-if='ctrl.spinnerClass')
+ .sk-spinner.small(ng-class='ctrl.spinnerClass')
diff --git a/app/assets/templates/directives/password-wizard.pug b/app/assets/templates/directives/password-wizard.pug
index 588b9d858..5f9329323 100644
--- a/app/assets/templates/directives/password-wizard.pug
+++ b/app/assets/templates/directives/password-wizard.pug
@@ -5,89 +5,133 @@
.sn-component
.sk-panel
.sk-panel-header
- .sk-panel-header-title {{title}}
- a.sk-a.info.close-button(ng-click='dismiss()') Close
+ .sk-panel-header-title {{ctrl.title}}
+ a.sk-a.info.close-button(ng-click='ctrl.dismiss()') Close
.sk-panel-content
- div(ng-if='step == 0')
- div(ng-if='changePassword')
+ div(ng-if='ctrl.step == 0')
+ div(ng-if='ctrl.changePassword')
p.sk-p.sk-panel-row
- | Changing your password involves changing your encryption key, which requires your data to be re-encrypted and synced.
+ | Changing your password involves changing your encryption key,
+ | which requires your data to be re-encrypted and synced.
| If you have many items, syncing your data can take several minutes.
- p.sk-p.sk-panel-row You must keep the application window open during this process.
- div(ng-if='securityUpdate')
+ p.sk-p.sk-panel-row
+ | You must keep the application window open during this process.
+ div(ng-if='ctrl.securityUpdate')
p.sk-p.sk-panel-row
- | A new update is available for your account. Updates address improvements and enhancements to our security specification.
- | This process will guide you through the update, and perform the steps necessary with your supervision.
+ | A new update is available for your account. Updates address
+ | improvements and enhancements to our security specification.
+ | This process will guide you through the update, and perform the
+ | steps necessary with your supervision.
.sk-panel-row
.sk-panel-column
p.sk-p For more information about security updates, please visit
- a.sk-a.info(href='https://standardnotes.org/help/security', rel='noopener', target='_blank') standardnotes.org/help/security.
+ a.sk-a.info(
+ href='https://standardnotes.org/help/security',
+ rel='noopener',
+ target='_blank'
+ ) standardnotes.org/help/security.
p.sk-panel-row.sk-p
.info Press Continue to proceed.
p
- .sk-panel-section(ng-if='step > 0')
- .sk-panel-section-title Step {{step}} — {{titleForStep(step)}}
- div(ng-if='step == 1')
+ .sk-panel-section(ng-if='ctrl.step > 0')
+ .sk-panel-section-title Step {{ctrl.step}} — {{ctrl.titleForStep(ctrl.step)}}
+ div(ng-if='ctrl.step == 1')
p.sk-panel-row.sk-p
- | As a result of this process, the entirety of your data will be re-encrypted and synced to your account. This is a generally safe process,
- | but unforeseen factors like poor network connectivity or a sudden shutdown of your computer may cause this process to fail.
- | It's best to be on the safe side before large operations such as this one.
+ | As a result of this process, the entirety of your data will be
+ | re-encrypted and synced to your account. This is a generally safe
+ | process, but unforeseen factors like poor network connectivity or a
+ | sudden shutdown of your computer may cause this process to fail. It's
+ | best to be on the safe side before large operations such as this one.
.sk-panel-row
.sk-panel-row
.sk-button-group
- .sk-button.info(ng-click='downloadBackup(true)')
+ .sk-button.info(ng-click='ctrl.downloadBackup(true)')
.sk-label Download Encrypted Backup
- .sk-button.info(ng-click='downloadBackup(false)')
+ .sk-button.info(ng-click='ctrl.downloadBackup(false)')
.sk-label Download Decrypted Backup
- div(ng-if='step == 2')
+ div(ng-if='ctrl.step == 2')
p.sk-p.sk-panel-row
- | As a result of this process, your encryption keys will change.
- | Any device on which you use Standard Notes will need to end its session. After this process completes, you will be asked to sign back in.
- p.sk-p.bold.sk-panel-row.info-i Please sign out of all applications (excluding this one), including:
+ | As a result of this process, your encryption keys will change. Any
+ | device on which you use Standard Notes will need to end its session.
+ | After this process completes, you will be asked to sign back in.
+ p.sk-p.bold.sk-panel-row.info-i
+ | Please sign out of all applications (excluding this one), including:
ul
li.sk-p Desktop
li.sk-p Web (Chrome, Firefox, Safari)
li.sk-p Mobile (iOS and Android)
p.sk-p.sk-panel-row
- | If you do not currently have access to a device you're signed in on, you may proceed,
- | but must make signing out and back in the first step upon gaining access to that device.
- p.sk-p.sk-panel-row Press Continue only when you have completed signing out of all your devices.
- div(ng-if='step == 3')
- div(ng-if='changePassword')
- div(ng-if='securityUpdate')
+ | If you do not currently have access to a device you're signed in on,
+ | you may proceed, but must make signing out and back in the first step
+ | upon gaining access to that device.
+ p.sk-p.sk-panel-row
+ | Press Continue only when you have
+ | completed signing out of all your devices.
+ div(ng-if='ctrl.step == 3')
+ div(ng-if='ctrl.changePassword')
+ div(ng-if='ctrl.securityUpdate')
p.sk-panel-row
- | Enter your current password. We'll run this through our encryption scheme to generate strong new encryption keys.
+ | Enter your current password. We'll run this through our encryption
+ | scheme to generate strong new encryption keys.
.sk-panel-row
.sk-panel-row
.sk-panel-column.stretch
form.sk-panel-form
- input.sk-input.contrast(ng-model='formData.currentPassword', placeholder='Current Password', should-focus='true', sn-autofocus='true', type='password')
- input.sk-input.contrast(ng-if='changePassword', ng-model='formData.newPassword', placeholder='New Password', type='password')
- input.sk-input.contrast(ng-if='changePassword', ng-model='formData.newPasswordConfirmation', placeholder='Confirm New Password', type='password')
- div(ng-if='step == 4')
+ input.sk-input.contrast(
+ ng-model='ctrl.formData.currentPassword',
+ placeholder='Current Password',
+ should-focus='true',
+ sn-autofocus='true',
+ type='password'
+ )
+ input.sk-input.contrast(
+ ng-if='ctrl.changePassword',
+ ng-model='ctrl.formData.newPassword',
+ placeholder='New Password',
+ type='password'
+ )
+ input.sk-input.contrast(
+ ng-if='ctrl.changePassword',
+ ng-model='ctrl.formData.newPasswordConfirmation',
+ placeholder='Confirm New Password',
+ type='password'
+ )
+ div(ng-if='ctrl.step == 4')
p.sk-panel-row
- | Your data is being re-encrypted with your new keys and synced to your account.
- p.sk-panel-row.danger(ng-if='lockContinue')
+ | Your data is being re-encrypted with your new
+ | keys and synced to your account.
+ p.sk-panel-row.danger(ng-if='ctrl.lockContinue')
| Do not close this window until this process completes.
.sk-panel-row
.sk-panel-column
- .sk-spinner.small.inline.info.mr-5(ng-if='formData.processing')
- .inline.bold(ng-class="{'info' : !formData.statusError, 'error' : formData.statusError}")
- | {{formData.status}}
- .sk-panel-column(delay='1000', delay-hide='true', show='syncStatus.syncOpInProgress || syncStatus.needsMoreSync')
+ .sk-spinner.small.inline.info.mr-5(ng-if='ctrl.formData.processing')
+ .inline.bold(
+ ng-class="{'info' : !ctrl.formData.statusError, 'error' : ctrl.formData.statusError}"
+ )
+ | {{ctrl.formData.status}}
+ .sk-panel-column(
+ delay='1000',
+ delay-hide='true',
+ show='ctrl.syncStatus.syncOpInProgress || ctrl.syncStatus.needsMoreSync'
+ )
p.info
- | Syncing {{syncStatus.current}}/{{syncStatus.total}}
- div(ng-if='step == 5')
- div(ng-if='changePassword')
+ | Syncing {{ctrl.syncStatus.current}}/{{ctrl.syncStatus.total}}
+ div(ng-if='ctrl.step == 5')
+ div(ng-if='ctrl.changePassword')
p.sk-p.sk-panel-row.info-i Your password has been successfully changed.
- div(ng-if='securityUpdate')
+ div(ng-if='ctrl.securityUpdate')
p.sk-p.sk-panel-row.info-i
| The security update has been successfully applied to your account.
p.sk-p.sk-panel-row
- | Please ensure you are running the latest version of Standard Notes on all platforms to ensure maximum compatibility.
- p.sk-p.sk-panel-row You may now sign back in on all your devices and close this window.
+ | Please ensure you are running the latest version of Standard Notes
+ | on all platforms to ensure maximum compatibility.
+ p.sk-p.sk-panel-row
+ | You may now sign back in on all your devices and close this window.
.sk-panel-footer
.empty
- a.sk-a.info.right(ng-class="{'disabled' : lockContinue}", ng-click='continue()', ng-disabled='lockContinue')
- .sk-spinner.small.inline.info.mr-5(ng-if='showSpinner')
- | {{continueTitle}}
+ a.sk-a.info.right(
+ ng-class="{'disabled' : ctrl.lockContinue}",
+ ng-click='ctrl.nextStep()',
+ ng-disabled='ctrl.lockContinue')
+ .sk-spinner.small.inline.info.mr-5(ng-if='ctrl.showSpinner')
+ | {{ctrl.continueTitle}}
diff --git a/app/assets/templates/directives/permissions-modal.pug b/app/assets/templates/directives/permissions-modal.pug
index 34741022e..f9973a5b6 100644
--- a/app/assets/templates/directives/permissions-modal.pug
+++ b/app/assets/templates/directives/permissions-modal.pug
@@ -1,21 +1,25 @@
-.sk-modal-background(ng-click='deny()')
+.sk-modal-background(ng-click='ctrl.deny()')
#permissions-modal.sk-modal-content
.sn-component
.sk-panel
.sk-panel-header
.sk-panel-header-title Activate Extension
- a.sk-a.info.close-button(ng-click='deny()') Cancel
+ a.sk-a.info.close-button(ng-click='ctrl.deny()') Cancel
.sk-panel-content
.sk-panel-section
.sk-panel-row
.sk-h2
- strong {{component.name}}
+ strong {{ctrl.component.name}}
| would like to interact with your
- | {{permissionsString}}
+ | {{ctrl.permissionsString}}
.sk-panel-row
p.sk-p
| Extensions use an offline messaging system to communicate. Learn more at
- a.sk-a.info(href='https://standardnotes.org/permissions', rel='noopener', target='_blank') https://standardnotes.org/permissions.
+ a.sk-a.info(
+ href='https://standardnotes.org/permissions',
+ rel='noopener',
+ target='_blank'
+ ) https://standardnotes.org/permissions.
.sk-panel-footer
- .sk-button.info.big.block.bold(ng-click='accept()')
+ .sk-button.info.big.block.bold(ng-click='ctrl.accept()')
.sk-label Continue
diff --git a/app/assets/templates/directives/privileges-auth-modal.pug b/app/assets/templates/directives/privileges-auth-modal.pug
index bae3343f0..3c578b256 100644
--- a/app/assets/templates/directives/privileges-auth-modal.pug
+++ b/app/assets/templates/directives/privileges-auth-modal.pug
@@ -1,25 +1,37 @@
-.sk-modal-background(ng-click="cancel()")
+.sk-modal-background(ng-click="ctrl.cancel()")
#privileges-modal.sk-modal-content
.sn-component
.sk-panel
.sk-panel-header
.sk-panel-header-title Authentication Required
- a.close-button.info(ng-click="cancel()") Cancel
+ a.close-button.info(ng-click="ctrl.cancel()") Cancel
.sk-panel-content
.sk-panel-section
- div(ng-repeat="credential in requiredCredentials")
+ div(ng-repeat="credential in ctrl.requiredCredentials")
.sk-p.sk-bold.sk-panel-row
- strong {{promptForCredential(credential)}}
+ strong {{ctrl.promptForCredential(credential)}}
.sk-panel-row
- input.sk-input.contrast(ng-model="authenticationParameters[credential]" should-focus="$index == 0" sn-autofocus="true" sn-enter="submit()" type="password")
+ input.sk-input.contrast(
+ ng-model="ctrl.authParameters[credential]"
+ should-focus="$index == 0"
+ sn-autofocus="true"
+ sn-enter="ctrl.submit()"
+ type="password"
+ )
.sk-panel-row
- label.sk-label.danger(ng-if="isCredentialInFailureState(credential)") Invalid authentication. Please try again.
+ label.sk-label.danger(
+ ng-if="ctrl.isCredentialInFailureState(credential)"
+ ) Invalid authentication. Please try again.
.sk-panel-row
.sk-panel-row
.sk-horizontal-group
.sk-p.sk-bold Remember For
- a.sk-a.info(ng-class="{'boxed' : option.value == selectedSessionLength}" ng-click="selectSessionLength(option.value)" ng-repeat="option in sessionLengthOptions")
+ a.sk-a.info(
+ ng-repeat="option in ctrl.sessionLengthOptions"
+ ng-class="{'boxed' : option.value == ctrl.selectedSessionLength}"
+ ng-click="ctrl.selectSessionLength(option.value)"
+ )
| {{option.label}}
.sk-panel-footer.extra-padding
- .sk-button.info.big.block.bold(ng-click="submit()")
+ .sk-button.info.big.block.bold(ng-click="ctrl.submit()")
.sk-label Submit
diff --git a/app/assets/templates/directives/privileges-management-modal.pug b/app/assets/templates/directives/privileges-management-modal.pug
index ca60bef65..b54ce1857 100644
--- a/app/assets/templates/directives/privileges-management-modal.pug
+++ b/app/assets/templates/directives/privileges-management-modal.pug
@@ -1,39 +1,51 @@
-.sk-modal-background(ng-click='cancel()')
+.sk-modal-background(ng-click='ctrl.cancel()')
#privileges-modal.sk-modal-content
.sn-component
.sk-panel
.sk-panel-header
.sk-panel-header-title Manage Privileges
- a.sk-a.close-button.info(ng-click='cancel()') Done
+ a.sk-a.close-button.info(ng-click='ctrl.cancel()') Done
.sk-panel-content
.sk-panel-section
table.sk-table
thead
tr
th
- th(ng-repeat='cred in availableCredentials')
+ th(ng-repeat='cred in ctrl.availableCredentials')
.priv-header
- strong {{credentialDisplayInfo[cred].label}}
- .sk-p.font-small(ng-show='!credentialDisplayInfo[cred].availability', style='margin-top: 2px') Not Configured
+ strong {{ctrl.credentialDisplayInfo[cred].label}}
+ .sk-p.font-small(
+ ng-show='!ctrl.credentialDisplayInfo[cred].availability',
+ style='margin-top: 2px'
+ ) Not Configured
tbody
- tr(ng-repeat='action in availableActions')
+ tr(ng-repeat='action in ctrl.availableActions')
td
- .sk-p {{displayInfoForAction(action)}}
- th(ng-repeat='credential in availableCredentials')
- input(ng-checked='isCredentialRequiredForAction(action, credential)', ng-click='checkboxValueChanged(action, credential)', ng-disabled='!credentialDisplayInfo[credential].availability', type='checkbox')
- .sk-panel-section(ng-if='sessionExpirey && !sessionExpired')
- .sk-p.sk-panel-row You will not be asked to authenticate until {{sessionExpirey}}.
- a.sk-a.sk-panel-row.info(ng-click='clearSession()') Clear Session
+ .sk-p {{ctrl.displayInfoForAction(action)}}
+ th(ng-repeat='credential in ctrl.availableCredentials')
+ input(
+ ng-checked='ctrl.isCredentialRequiredForAction(action, credential)',
+ ng-click='ctrl.checkboxValueChanged(action, credential)',
+ ng-disabled='!ctrl.credentialDisplayInfo[credential].availability',
+ type='checkbox'
+ )
+ .sk-panel-section(ng-if='ctrl.sessionExpirey && !ctrl.sessionExpired')
+ .sk-p.sk-panel-row
+ | You will not be asked to authenticate until {{ctrl.sessionExpirey}}.
+ a.sk-a.sk-panel-row.info(ng-click='ctrl.clearSession()') Clear Session
.sk-panel-footer
.sk-h2.sk-bold About Privileges
.sk-panel-section.no-bottom-pad
.sk-panel-row
.text-content
.sk-p
- | Privileges represent interface level authentication for accessing certain items and features.
- | Note that when your application is unlocked, your data exists in temporary memory in an unencrypted state.
- | Privileges are meant to protect against unwanted access in the event of an unlocked application, but do not affect data encryption state.
+ | Privileges represent interface level authentication for accessing
+ | certain items and features. Note that when your application is unlocked,
+ | your data exists in temporary memory in an unencrypted state.
+ | Privileges are meant to protect against unwanted access in the event of
+ | an unlocked application, but do not affect data encryption state.
p.sk-p
- | Privileges sync across your other devices; however, note that if you require
- | a "Local Passcode" privilege, and another device does not have a local passcode set up, the local passcode
- | requirement will be ignored on that device.
+ | Privileges sync across your other devices; however, note that if you
+ | require a "Local Passcode" privilege, and another device does not have
+ | a local passcode set up, the local passcode requirement will be ignored
+ | on that device.
diff --git a/app/assets/templates/directives/revision-preview-modal.pug b/app/assets/templates/directives/revision-preview-modal.pug
index 37eb5f38c..713389c5d 100644
--- a/app/assets/templates/directives/revision-preview-modal.pug
+++ b/app/assets/templates/directives/revision-preview-modal.pug
@@ -7,10 +7,21 @@
.sk-panel-header
.sk-panel-header-title Preview
.sk-horizontal-group
- a.sk-a.info.close-button(ng-click="restore(false)") Restore
- a.sk-a.info.close-button(ng-click="restore(true)") Restore as copy
- a.sk-a.info.close-button(ng-click="dismiss(); $event.stopPropagation()") Close
- .sk-panel-content.selectable(ng-if="!editor")
- .sk-h2 {{content.title}}
- p.normal.sk-p(style="white-space: pre-wrap; font-size: 16px;") {{content.text}}
- component-view.component-view(component="editor" ng-if="editor")
+ a.sk-a.info.close-button(
+ ng-click="ctrl.restore(false)"
+ ) Restore
+ a.sk-a.info.close-button(
+ ng-click="ctrl.restore(true)"
+ ) Restore as copy
+ a.sk-a.info.close-button(
+ ng-click="ctrl.dismiss(); $event.stopPropagation()"
+ ) Close
+ .sk-panel-content.selectable(ng-if="!ctrl.editor")
+ .sk-h2 {{ctrl.content.title}}
+ p.normal.sk-p(
+ style="white-space: pre-wrap; font-size: 16px;"
+ ) {{ctrl.content.text}}
+ component-view.component-view(
+ component="ctrl.editor"
+ ng-if="ctrl.editor"
+ )
diff --git a/app/assets/templates/directives/session-history-menu.pug b/app/assets/templates/directives/session-history-menu.pug
index 961b8e115..36788db7a 100644
--- a/app/assets/templates/directives/session-history-menu.pug
+++ b/app/assets/templates/directives/session-history-menu.pug
@@ -1,17 +1,35 @@
#session-history-menu.sn-component
.sk-menu-panel.dropdown-menu
.sk-menu-panel-header
- .sk-menu-panel-header-title {{history.entries.length || 'No'}} revisions
- a.sk-a.info.sk-h5(ng-click='showOptions = !showOptions; $event.stopPropagation();') Options
- div(ng-if='showOptions')
- menu-row(action='clearItemHistory()', label="'Clear note local history'")
- menu-row(action='clearAllHistory()', label="'Clear all local history'")
- menu-row(action='toggleAutoOptimize()', label="(autoOptimize ? 'Disable' : 'Enable') + ' auto cleanup'")
+ .sk-menu-panel-header-title {{ctrl.history.entries.length || 'No'}} revisions
+ a.sk-a.info.sk-h5(
+ ng-click='ctrl.showOptions = !ctrl.showOptions; $event.stopPropagation();'
+ ) Options
+ div(ng-if='ctrl.showOptions')
+ menu-row(
+ action='ctrl.clearItemHistory()'
+ label="'Clear note local history'"
+ )
+ menu-row(
+ action='ctrl.clearAllHistory()'
+ label="'Clear all local history'"
+ )
+ menu-row(
+ action='ctrl.toggleAutoOptimize()'
+ label="(ctrl.autoOptimize ? 'Disable' : 'Enable') + ' auto cleanup'")
.sk-sublabel
| Automatically cleans up small revisions to conserve space.
- menu-row(action='toggleDiskSaving()', label="(diskEnabled ? 'Disable' : 'Enable') + ' saving history to disk'")
+ menu-row(
+ action='ctrl.toggleDiskSaving()'
+ label="(ctrl.diskEnabled ? 'Disable' : 'Enable') + ' saving history to disk'"
+ )
.sk-sublabel
- | Saving to disk is not recommended. Decreases performance and increases app loading time and memory footprint.
- menu-row(action='openRevision(revision);', label='revision.previewTitle()', ng-repeat='revision in entries')
- .sk-sublabel.opaque(ng-class='classForRevision(revision)')
+ | Saving to disk is not recommended. Decreases performance and increases app
+ | loading time and memory footprint.
+ menu-row(
+ ng-repeat='revision in ctrl.entries'
+ action='ctrl.openRevision(revision);'
+ label='revision.previewTitle()'
+ )
+ .sk-sublabel.opaque(ng-class='ctrl.classForRevision(revision)')
| {{revision.previewSubTitle()}}
diff --git a/app/assets/templates/directives/sync-resolution-menu.pug b/app/assets/templates/directives/sync-resolution-menu.pug
index 9b84bb116..471944349 100644
--- a/app/assets/templates/directives/sync-resolution-menu.pug
+++ b/app/assets/templates/directives/sync-resolution-menu.pug
@@ -2,54 +2,59 @@
#sync-resolution-menu.sk-panel.sk-panel-right
.sk-panel-header
.sk-panel-header-title Out of Sync
- a.sk-a.info.close-button(ng-click='close()') Close
+ a.sk-a.info.close-button(ng-click='ctrl.close()') Close
.sk-panel-content
.sk-panel-section
.sk-panel-row.sk-p
- | We've detected that the data on the server may not match the data in the current application session.
+ | We've detected that the data on the server may not match
+ | the data in the current application session.
.sk-p.sk-panel-row
.sk-panel-column
strong.sk-panel-row Option 1 — Restart App:
- .sk-p Quit the application and re-open it. Sometimes, this may resolve the issue.
+ .sk-p
+ | Quit the application and re-open it.
+ | Sometimes, this may resolve the issue.
.sk-p.sk-panel-row
.sk-panel-column
strong.sk-panel-row Option 2 (recommended) — Sign Out:
.sk-p
- | Sign out of your account, then sign back in. This will ensure your data is consistent with the server.
+ | Sign out of your account, then sign back in.
+ | This will ensure your data is consistent with the server.
| Be sure to download a backup of your data before doing so.
.sk-p.sk-panel-row
.sk-panel-column
strong.sk-panel-row Option 3 — Sync Resolution:
.sk-p
- | We can attempt to reconcile changes by downloading all data from the server.
- | No existing data will be overwritten. If the local contents of an item differ from what the server has,
- | a conflicted copy will be created.
- div(ng-if='!status.backupFinished')
+ | We can attempt to reconcile changes by downloading all data from the
+ | server. No existing data will be overwritten. If the local contents of
+ | an item differ from what the server has, a conflicted copy will be created.
+ div(ng-if='!ctrl.status.backupFinished')
.sk-p.sk-panel-row
- | Please download a backup before we attempt to perform a full account sync resolution.
+ | Please download a backup before we attempt to
+ | perform a full account sync resolution.
.sk-panel-row
.sk-button-group
- .sk-button.info(ng-click='downloadBackup(true)')
+ .sk-button.info(ng-click='ctrl.downloadBackup(true)')
.sk-label Encrypted
- .sk-button.info(ng-click='downloadBackup(false)')
+ .sk-button.info(ng-click='ctrl.downloadBackup(false)')
.sk-label Decrypted
- .sk-button.danger(ng-click='skipBackup()')
+ .sk-button.danger(ng-click='ctrl.skipBackup()')
.sk-label Skip
- div(ng-if='status.backupFinished')
- .sk-panel-row(ng-if='!status.resolving && !status.attemptedResolution')
- .sk-button.info(ng-click='performSyncResolution()')
+ div(ng-if='ctrl.status.backupFinished')
+ .sk-panel-row(ng-if='!ctrl.status.resolving && !ctrl.status.attemptedResolution')
+ .sk-button.info(ng-click='ctrl.performSyncResolution()')
.sk-label Perform Sync Resolution
- .sk-panel-row.justify-left(ng-if='status.resolving')
+ .sk-panel-row.justify-left(ng-if='ctrl.status.resolving')
.sk-horizontal-group
.sk-spinner.small.info
.sk-label Attempting sync resolution...
- .sk-panel-column(ng-if='status.fail')
+ .sk-panel-column(ng-if='ctrl.status.fail')
.sk-panel-row.sk-label.danger Sync Resolution Failed
.sk-p.sk-panel-row
- | We attempted to reconcile local content and server content, but were unable to do so.
- | At this point, we recommend signing out of your account and signing back in. You may
- | wish to download a data backup before doing so.
- .sk-panel-column(ng-if='status.success')
+ | We attempted to reconcile local content and server content, but were
+ | unable to do so. At this point, we recommend signing out of your account
+ | and signing back in. You may wish to download a data backup before doing so.
+ .sk-panel-column(ng-if='ctrl.status.success')
.sk-panel-row.sk-label.success Sync Resolution Success
.sk-p.sk-panel-row
| Your local data is now in sync with the server. You may close this window.
diff --git a/app/assets/templates/editor.pug b/app/assets/templates/editor.pug
index c4f38f819..e3c425452 100644
--- a/app/assets/templates/editor.pug
+++ b/app/assets/templates/editor.pug
@@ -1,72 +1,253 @@
#editor-column.section.editor.sn-component(aria-label='Note')
.sn-component
- .sk-app-bar.no-edges(ng-if='ctrl.note.locked', ng-init="ctrl.lockText = 'Note Locked'", ng-mouseleave="ctrl.lockText = 'Note Locked'", ng-mouseover="ctrl.lockText = 'Unlock'")
+ .sk-app-bar.no-edges(
+ ng-if='self.state.note.locked',
+ ng-init="self.lockText = 'Note Locked'",
+ ng-mouseleave="self.lockText = 'Note Locked'",
+ ng-mouseover="self.lockText = 'Unlock'"
+ )
.left
- .sk-app-bar-item(ng-click='ctrl.toggleLockNote()')
+ .sk-app-bar-item(ng-click='self.toggleLockNote()')
.sk-label.warning
i.icon.ion-locked
- | {{ctrl.lockText}}
- #editor-title-bar.section-title-bar(ng-class="{'locked' : ctrl.note.locked}", ng-show='ctrl.note && !ctrl.note.errorDecrypting')
+ | {{self.lockText}}
+ #editor-title-bar.section-title-bar(
+ ng-class="{'locked' : self.state.note.locked}",
+ ng-show='self.state.note && !self.state.note.errorDecrypting'
+ )
.title
- input#note-title-editor.input(ng-blur='ctrl.onNameBlur()', ng-change='ctrl.onTitleChange()', ng-disabled='ctrl.note.locked', ng-focus='ctrl.onNameFocus()', ng-keyup='$event.keyCode == 13 && ctrl.onTitleEnter($event)', ng-model='ctrl.note.title', select-on-click='true', spellcheck='false')
+ input#note-title-editor.input(
+ ng-blur='self.onNameBlur()',
+ ng-change='self.onTitleChange()',
+ ng-disabled='self.state.note.locked',
+ ng-focus='self.onNameFocus()',
+ ng-keyup='$event.keyCode == 13 && self.onTitleEnter($event)',
+ ng-model='self.state.note.title',
+ select-on-click='true',
+ spellcheck='false')
#save-status
- .message(ng-class="{'warning sk-bold': ctrl.syncTakingTooLong, 'danger sk-bold': ctrl.saveError}") {{ctrl.noteStatus.message}}
- .desc(ng-show='ctrl.noteStatus.desc') {{ctrl.noteStatus.desc}}
+ .message(
+ ng-class="{'warning sk-bold': self.state.syncTakingTooLong, 'danger sk-bold': self.state.saveError}"
+ ) {{self.state.noteStatus.message}}
+ .desc(ng-show='self.state.noteStatus.desc') {{self.state.noteStatus.desc}}
.editor-tags
- #note-tags-component-container(ng-if='ctrl.tagsComponent')
- component-view.component-view(component='ctrl.tagsComponent', ng-class="{'locked' : ctrl.note.locked}", ng-style="ctrl.note.locked && {'pointer-events' : 'none'}")
- input.tags-input(ng-blur='ctrl.updateTagsFromTagsString($event, ctrl.tagsString)', ng-disabled='ctrl.note.locked', ng-if='!(ctrl.tagsComponent && ctrl.tagsComponent.active)', ng-keyup='$event.keyCode == 13 && $event.target.blur();', ng-model='ctrl.tagsString', placeholder='#tags', spellcheck='false', type='text')
- .sn-component(ng-if='ctrl.note')
+ #note-tags-component-container(ng-if='self.state.tagsComponent')
+ component-view.component-view(
+ component='self.state.tagsComponent',
+ ng-class="{'locked' : self.state.note.locked}",
+ ng-style="self.state.note.locked && {'pointer-events' : 'none'}"
+ )
+ input.tags-input(
+ ng-blur='self.updateTagsFromTagsString($event, self.state.tagsString)',
+ ng-disabled='self.state.note.locked',
+ ng-if='!(self.state.tagsComponent && self.state.tagsComponent.active)',
+ ng-keyup='$event.keyCode == 13 && $event.target.blur();',
+ ng-model='self.state.tagsString',
+ placeholder='#tags',
+ spellcheck='false',
+ type='text'
+ )
+ .sn-component(ng-if='self.state.note')
#editor-menu-bar.sk-app-bar.no-edges
.left
- .sk-app-bar-item(click-outside='ctrl.showMenu = false;', is-open='ctrl.showMenu', ng-class="{'selected' : ctrl.showMenu}", ng-click="ctrl.toggleMenu('showMenu')")
+ .sk-app-bar-item(
+ click-outside=`self.setMenuState('showOptionsMenu', false)`,
+ is-open='self.state.showOptionsMenu',
+ ng-class="{'selected' : self.state.showOptionsMenu}",
+ ng-click="self.toggleMenu('showOptionsMenu')"
+ )
.sk-label Options
- .sk-menu-panel.dropdown-menu(ng-if='ctrl.showMenu')
+ .sk-menu-panel.dropdown-menu(ng-if='self.state.showOptionsMenu')
.sk-menu-panel-section
.sk-menu-panel-header
.sk-menu-panel-header-title Note Options
- menu-row(action='ctrl.selectedMenuItem(true); ctrl.togglePin()', desc="'Pin or unpin a note from the top of your list'", label="ctrl.note.pinned ? 'Unpin' : 'Pin'")
- menu-row(action='ctrl.selectedMenuItem(true); ctrl.toggleArchiveNote()', desc="'Archive or unarchive a note from your Archived system tag'", label="ctrl.note.archived ? 'Unarchive' : 'Archive'")
- menu-row(action='ctrl.selectedMenuItem(true); ctrl.toggleLockNote()', desc="'Locking notes prevents unintentional editing'", label="ctrl.note.locked ? 'Unlock' : 'Lock'")
- menu-row(action='ctrl.selectedMenuItem(true); ctrl.toggleProtectNote()', desc="'Protecting a note will require credentials to view it (Manage Privileges via Account menu)'", label="ctrl.note.content.protected ? 'Unprotect' : 'Protect'")
- menu-row(action='ctrl.selectedMenuItem(true); ctrl.toggleNotePreview()', circle="ctrl.note.content.hidePreview ? 'danger' : 'success'", circle-align="'right'", desc="'Hide or unhide the note preview from the list of notes'", label="'Preview'")
- menu-row(action='ctrl.selectedMenuItem(); ctrl.deleteNote()', desc="'Send this note to the trash'", label="'Move to Trash'", ng-show='!ctrl.altKeyDown && !ctrl.note.content.trashed && !ctrl.note.errorDecrypting', stylekit-class="'warning'")
- menu-row(action='ctrl.selectedMenuItem(); ctrl.deleteNotePermanantely()', desc="'Delete this note permanently from all your devices'", label="'Delete Permanently'", ng-show='!ctrl.note.content.trashed && ctrl.note.errorDecrypting', stylekit-class="'danger'")
- div(ng-if='ctrl.note.content.trashed || ctrl.altKeyDown')
- menu-row(action='ctrl.selectedMenuItem(true); ctrl.restoreTrashedNote()', desc="'Undelete this note and restore it back into your notes'", label="'Restore'", ng-show='ctrl.note.content.trashed', stylekit-class="'info'")
- menu-row(action='ctrl.selectedMenuItem(true); ctrl.deleteNotePermanantely()', desc="'Delete this note permanently from all your devices'", label="'Delete Permanently'", stylekit-class="'danger'")
- menu-row(action='ctrl.selectedMenuItem(true); ctrl.emptyTrash()', desc="'Permanently delete all notes in the trash'", label="'Empty Trash'", ng-show='ctrl.note.content.trashed || !ctrl.altKeyDown', stylekit-class="'danger'", subtitle="ctrl.getTrashCount() + ' notes in trash'")
+ menu-row(
+ action='self.selectedMenuItem(true); self.togglePin()',
+ desc="'Pin or unpin a note from the top of your list'",
+ label="self.state.note.pinned ? 'Unpin' : 'Pin'"
+ )
+ menu-row(
+ action='self.selectedMenuItem(true); self.toggleArchiveNote()',
+ desc="'Archive or unarchive a note from your Archived system tag'",
+ label="self.state.note.archived ? 'Unarchive' : 'Archive'"
+ )
+ menu-row(
+ action='self.selectedMenuItem(true); self.toggleLockNote()',
+ desc="'Locking notes prevents unintentional editing'",
+ label="self.state.note.locked ? 'Unlock' : 'Lock'"
+ )
+ menu-row(
+ action='self.selectedMenuItem(true); self.toggleProtectNote()',
+ desc=`'Protecting a note will require credentials to view
+ it (Manage Privileges via Account menu)'`,
+ label="self.state.note.content.protected ? 'Unprotect' : 'Protect'"
+ )
+ menu-row(
+ action='self.selectedMenuItem(true); self.toggleNotePreview()',
+ circle="self.state.note.content.hidePreview ? 'danger' : 'success'",
+ circle-align="'right'",
+ desc="'Hide or unhide the note preview from the list of notes'",
+ label="'Preview'"
+ )
+ menu-row(
+ action='self.selectedMenuItem(); self.deleteNote()',
+ desc="'Send this note to the trash'",
+ label="'Move to Trash'",
+ ng-show='!self.state.altKeyDown && !self.state.note.content.trashed && !self.state.note.errorDecrypting',
+ stylekit-class="'warning'"
+ )
+ menu-row(
+ action='self.selectedMenuItem(); self.deleteNotePermanantely()',
+ desc="'Delete this note permanently from all your devices'",
+ label="'Delete Permanently'",
+ ng-show='!self.state.note.content.trashed && self.state.note.errorDecrypting',
+ stylekit-class="'danger'"
+ )
+ div(ng-if='self.state.note.content.trashed || self.state.altKeyDown')
+ menu-row(
+ action='self.selectedMenuItem(true); self.restoreTrashedNote()',
+ desc="'Undelete this note and restore it back into your notes'",
+ label="'Restore'",
+ ng-show='self.state.note.content.trashed',
+ stylekit-class="'info'"
+ )
+ menu-row(
+ action='self.selectedMenuItem(true); self.deleteNotePermanantely()',
+ desc="'Delete this note permanently from all your devices'",
+ label="'Delete Permanently'",
+ stylekit-class="'danger'"
+ )
+ menu-row(
+ action='self.selectedMenuItem(true); self.emptyTrash()',
+ desc="'Permanently delete all notes in the trash'",
+ label="'Empty Trash'",
+ ng-show='self.state.note.content.trashed || !self.state.altKeyDown',
+ stylekit-class="'danger'",
+ subtitle="self.getTrashCount() + ' notes in trash'"
+ )
.sk-menu-panel-section
.sk-menu-panel-header
.sk-menu-panel-header-title Global Display
- menu-row(action="ctrl.selectedMenuItem(true); ctrl.toggleKey('monospaceFont')", circle="ctrl.monospaceFont ? 'success' : 'neutral'", desc="'Toggles the font style for the default editor'", disabled='ctrl.selectedEditor', label="'Monospace Font'", subtitle="ctrl.selectedEditor ? 'Not available with editor extensions' : null")
- menu-row(action="ctrl.selectedMenuItem(true); ctrl.toggleKey('spellcheck')", circle="ctrl.spellcheck ? 'success' : 'neutral'", desc="'Toggles spellcheck for the default editor'", disabled='ctrl.selectedEditor', label="'Spellcheck'", subtitle="ctrl.selectedEditor ? 'Not available with editor extensions' : (ctrl.isDesktop ? 'May degrade editor performance' : null)")
- menu-row(action="ctrl.selectedMenuItem(true); ctrl.toggleKey('marginResizersEnabled')", circle="ctrl.marginResizersEnabled ? 'success' : 'neutral'", desc="'Allows for editor left and right margins to be resized'", faded='!ctrl.marginResizersEnabled', label="'Margin Resizers'")
- .sk-app-bar-item(click-outside='ctrl.showEditorMenu = false;', is-open='ctrl.showEditorMenu', ng-class="{'selected' : ctrl.showEditorMenu}", ng-click="ctrl.toggleMenu('showEditorMenu')")
+ menu-row(
+ action="self.selectedMenuItem(true); self.toggleKey(self.prefKeyMonospace)",
+ circle="self.state.monospaceEnabled ? 'success' : 'neutral'",
+ desc="'Toggles the font style for the default editor'",
+ disabled='self.state.selectedEditor',
+ label="'Monospace Font'",
+ subtitle="self.state.selectedEditor ? 'Not available with editor extensions' : null"
+ )
+ menu-row(
+ action="self.selectedMenuItem(true); self.toggleKey(self.prefKeySpellcheck)",
+ circle="self.state.spellcheck ? 'success' : 'neutral'",
+ desc="'Toggles spellcheck for the default editor'",
+ disabled='self.state.selectedEditor',
+ label="'Spellcheck'",
+ subtitle=`
+ self.state.selectedEditor
+ ? 'Not available with editor extensions'
+ : (self.state.isDesktop ? 'May degrade editor performance' : null)
+ `)
+ menu-row(
+ action="self.selectedMenuItem(true); self.toggleKey(self.prefKeyMarginResizers)",
+ circle="self.state.marginResizersEnabled ? 'success' : 'neutral'",
+ desc="'Allows for editor left and right margins to be resized'",
+ faded='!self.state.marginResizersEnabled',
+ label="'Margin Resizers'"
+ )
+ .sk-app-bar-item(
+ click-outside=`self.setMenuState('showEditorMenu', false)`
+ is-open='self.state.showEditorMenu',
+ ng-class="{'selected' : self.state.showEditorMenu}",
+ ng-click="self.toggleMenu('showEditorMenu')"
+ )
.sk-label Editor
- editor-menu(callback='ctrl.editorMenuOnSelect', current-item='ctrl.note', ng-if='ctrl.showEditorMenu', selected-editor='ctrl.selectedEditor')
- .sk-app-bar-item(click-outside='ctrl.showExtensions = false;', is-open='ctrl.showExtensions', ng-class="{'selected' : ctrl.showExtensions}", ng-click="ctrl.toggleMenu('showExtensions')")
+ editor-menu(
+ callback='self.editorMenuOnSelect',
+ current-item='self.state.note',
+ ng-if='self.state.showEditorMenu',
+ selected-editor='self.state.selectedEditor'
+ )
+ .sk-app-bar-item(
+ click-outside=`self.setMenuState('showExtensions', false)`,
+ is-open='self.state.showExtensions',
+ ng-class="{'selected' : self.state.showExtensions}",
+ ng-click="self.toggleMenu('showExtensions')"
+ )
.sk-label Actions
- actions-menu(item='ctrl.note', ng-if='ctrl.showExtensions')
- .sk-app-bar-item(click-outside='ctrl.showSessionHistory = false;', is-open='ctrl.showSessionHistory', ng-click="ctrl.toggleMenu('showSessionHistory')")
+ actions-menu(
+ item='self.state.note',
+ ng-if='self.state.showExtensions'
+ )
+ .sk-app-bar-item(
+ click-outside=`self.setMenuState('showSessionHistory', false)`,
+ is-open='self.state.showSessionHistory',
+ ng-click="self.toggleMenu('showSessionHistory')"
+ )
.sk-label Session History
- session-history-menu(item='ctrl.note', ng-if='ctrl.showSessionHistory')
- #editor-content.editor-content(ng-if='ctrl.noteReady && !ctrl.note.errorDecrypting')
- panel-resizer.left(control='ctrl.leftResizeControl', hoverable='true', min-width='300', ng-if='ctrl.marginResizersEnabled', on-resize-finish='ctrl.onPanelResizeFinish', panel-id="'editor-content'", property="'left'")
- component-view.component-view(component='ctrl.selectedEditor', ng-if='ctrl.selectedEditor', on-load='ctrl.onEditorLoad')
- textarea#note-text-editor.editable(dir='auto', ng-attr-spellcheck='{{ctrl.spellcheck}}', ng-change='ctrl.contentChanged()', ng-click='ctrl.clickedTextArea()', ng-focus='ctrl.onContentFocus()', ng-if='!ctrl.selectedEditor', ng-model='ctrl.note.text', ng-model-options='{ debounce: ctrl.EditorNgDebounce}', ng-readonly='ctrl.note.locked', ng-trim='false')
- | {{ctrl.onSystemEditorLoad()}}
- panel-resizer(control='ctrl.rightResizeControl', hoverable='true', min-width='300', ng-if='ctrl.marginResizersEnabled', on-resize-finish='ctrl.onPanelResizeFinish', panel-id="'editor-content'", property="'right'")
- .section(ng-show='ctrl.note.errorDecrypting')
+ session-history-menu(
+ item='self.state.note',
+ ng-if='self.state.showSessionHistory'
+ )
+ #editor-content.editor-content(
+ ng-if='self.state.noteReady && !self.state.note.errorDecrypting'
+ )
+ panel-resizer.left(
+ control='self.leftResizeControl',
+ hoverable='true',
+ min-width='300',
+ ng-if='self.state.marginResizersEnabled',
+ on-resize-finish='self.onPanelResizeFinish',
+ panel-id="'editor-content'",
+ property="'left'"
+ )
+ component-view.component-view(
+ component='self.state.selectedEditor',
+ ng-if='self.state.selectedEditor',
+ on-load='self.onEditorLoad'
+ )
+ textarea#note-text-editor.editable(
+ dir='auto',
+ ng-attr-spellcheck='{{self.state.spellcheck}}',
+ ng-change='self.contentChanged()',
+ ng-click='self.clickedTextArea()',
+ ng-focus='self.onContentFocus()',
+ ng-if='!self.state.selectedEditor',
+ ng-model='self.state.note.text',
+ ng-model-options='{ debounce: self.state.editorDebounce}',
+ ng-readonly='self.state.note.locked',
+ ng-trim='false'
+ )
+ | {{self.onSystemEditorLoad()}}
+ panel-resizer(
+ control='self.rightResizeControl',
+ hoverable='true', min-width='300',
+ ng-if='self.state.marginResizersEnabled',
+ on-resize-finish='self.onPanelResizeFinish',
+ panel-id="'editor-content'",
+ property="'right'"
+ )
+ .section(ng-show='self.state.note.errorDecrypting')
p.medium-padding(style='padding-top: 0 !important;')
- | There was an error decrypting this item. Ensure you are running the latest version of this app, then sign out and sign back in to try again.
- #editor-pane-component-stack(ng-show='ctrl.note')
- #component-stack-menu-bar.sk-app-bar.no-edges(ng-if='ctrl.componentStack.length')
+ | There was an error decrypting this item. Ensure you are running the
+ | latest version of this app, then sign out and sign back in to try again.
+ #editor-pane-component-stack(ng-show='self.state.note')
+ #component-stack-menu-bar.sk-app-bar.no-edges(ng-if='self.componentStack.length')
.left
- .sk-app-bar-item(ng-click='ctrl.toggleStackComponentForCurrentItem(component)', ng-repeat='component in ctrl.componentStack')
+ .sk-app-bar-item(
+ ng-click='self.toggleStackComponentForCurrentItem(component)',
+ ng-repeat='component in self.componentStack'
+ )
.sk-app-bar-item-column
- .sk-circle.small(ng-class="{'info' : !component.hidden && component.active, 'neutral' : component.hidden || !component.active}")
+ .sk-circle.small(
+ ng-class="{'info' : !component.hidden && component.active, 'neutral' : component.hidden || !component.active}"
+ )
.sk-app-bar-item-column
.sk-label {{component.name}}
.sn-component
- component-view.component-view.component-stack-item(component='component', manual-dealloc='true', ng-if='component.active', ng-repeat='component in ctrl.componentStack', ng-show='!component.hidden')
+ component-view.component-view.component-stack-item(
+ component='component',
+ manual-dealloc='true',
+ ng-if='component.active',
+ ng-repeat='component in self.componentStack',
+ ng-show='!component.hidden'
+ )
diff --git a/app/assets/templates/footer.pug b/app/assets/templates/footer.pug
index 023292371..8820dda46 100644
--- a/app/assets/templates/footer.pug
+++ b/app/assets/templates/footer.pug
@@ -1,35 +1,68 @@
.sn-component
#footer-bar.sk-app-bar.no-edges.no-bottom-edge
.left
- .sk-app-bar-item(click-outside='ctrl.clickOutsideAccountMenu()', is-open='ctrl.showAccountMenu', ng-click='ctrl.accountMenuPressed()')
+ .sk-app-bar-item(
+ click-outside='ctrl.clickOutsideAccountMenu()',
+ is-open='ctrl.showAccountMenu',
+ ng-click='ctrl.accountMenuPressed()'
+ )
.sk-app-bar-item-column
- .sk-circle.small(ng-class="ctrl.error ? 'danger' : (ctrl.getUser() ? 'info' : 'neutral')")
+ .sk-circle.small(
+ ng-class="ctrl.error ? 'danger' : (ctrl.getUser() ? 'info' : 'neutral')"
+ )
.sk-app-bar-item-column
.sk-label.title(ng-class='{red: ctrl.error}') Account
- account-menu(close-function='ctrl.closeAccountMenu', ng-click='$event.stopPropagation()', ng-if='ctrl.showAccountMenu', on-successful-auth='ctrl.onAuthSuccess')
+ account-menu(
+ close-function='ctrl.closeAccountMenu',
+ ng-click='$event.stopPropagation()',
+ ng-if='ctrl.showAccountMenu',
+ )
.sk-app-bar-item
- a.no-decoration.sk-label.title(href='https://standardnotes.org/help', rel='noopener', target='_blank')
+ a.no-decoration.sk-label.title(
+ href='https://standardnotes.org/help',
+ rel='noopener',
+ target='_blank'
+ )
| Help
.sk-app-bar-item.border
.sk-app-bar-item(ng-repeat='room in ctrl.rooms track by room.uuid')
.sk-app-bar-item-column(ng-click='ctrl.selectRoom(room)')
.sk-label {{room.name}}
- component-modal(component='room', ng-if='room.showRoom', on-dismiss='ctrl.onRoomDismiss')
+ component-modal(
+ component='room',
+ ng-if='room.showRoom',
+ on-dismiss='ctrl.onRoomDismiss'
+ )
.center
.sk-app-bar-item(ng-show='ctrl.arbitraryStatusMessage')
.sk-app-bar-item-column
span.neutral.sk-label {{ctrl.arbitraryStatusMessage}}
.right
- .sk-app-bar-item(ng-click='ctrl.openSecurityUpdate()', ng-show='ctrl.securityUpdateAvailable')
+ .sk-app-bar-item(
+ ng-click='ctrl.openSecurityUpdate()',
+ ng-show='ctrl.securityUpdateAvailable'
+ )
span.success.sk-label Security update available.
- .sk-app-bar-item(ng-click='ctrl.clickedNewUpdateAnnouncement()', ng-show='ctrl.newUpdateAvailable == true')
+ .sk-app-bar-item(
+ ng-click='ctrl.clickedNewUpdateAnnouncement()',
+ ng-show='ctrl.newUpdateAvailable == true'
+ )
span.info.sk-label New update available.
- .sk-app-bar-item.no-pointer(ng-if='ctrl.lastSyncDate && !ctrl.isRefreshing')
+ .sk-app-bar-item.no-pointer(
+ ng-if='ctrl.lastSyncDate && !ctrl.isRefreshing'
+ )
.sk-label.subtle
| Last refreshed {{ctrl.lastSyncDate | appDateTime}}
- .sk-app-bar-item(ng-click='ctrl.toggleSyncResolutionMenu()', ng-if='(ctrl.outOfSync && !ctrl.isRefreshing) || ctrl.showSyncResolution')
+ .sk-app-bar-item(
+ ng-click='ctrl.toggleSyncResolutionMenu()',
+ ng-if='(ctrl.outOfSync && !ctrl.isRefreshing) || ctrl.showSyncResolution'
+ )
.sk-label.warning(ng-if='ctrl.outOfSync') Potentially Out of Sync
- sync-resolution-menu(close-function='ctrl.toggleSyncResolutionMenu', ng-click='$event.stopPropagation();', ng-if='ctrl.showSyncResolution')
+ sync-resolution-menu(
+ close-function='ctrl.toggleSyncResolutionMenu',
+ ng-click='$event.stopPropagation();',
+ ng-if='ctrl.showSyncResolution'
+ )
.sk-app-bar-item(ng-if='ctrl.lastSyncDate && ctrl.isRefreshing')
.sk-spinner.small
.sk-app-bar-item(ng-if='ctrl.offline')
@@ -38,12 +71,24 @@
.sk-label Refresh
.sk-app-bar-item.border(ng-if='ctrl.dockShortcuts.length > 0')
.sk-app-bar-item.dock-shortcut(ng-repeat='shortcut in ctrl.dockShortcuts')
- .sk-app-bar-item-column(ng-class="{'underline': shortcut.component.active}", ng-click='ctrl.selectShortcut(shortcut)')
+ .sk-app-bar-item-column(
+ ng-class="{'underline': shortcut.component.active}",
+ ng-click='ctrl.selectShortcut(shortcut)'
+ )
.div(ng-if="shortcut.icon.type == 'circle'", title='{{shortcut.name}}')
- .sk-circle.small(ng-style="{'background-color': shortcut.icon.background_color, 'border-color': shortcut.icon.border_color}")
+ .sk-circle.small(
+ ng-style="{'background-color': shortcut.icon.background_color, 'border-color': shortcut.icon.border_color}"
+ )
.div(ng-if="shortcut.icon.type == 'svg'", title='{{shortcut.name}}')
- .svg-item(elem-ready='ctrl.initSvgForShortcut(shortcut)', ng-attr-id='dock-svg-{{shortcut.component.uuid}}')
+ .svg-item(
+ elem-ready='ctrl.initSvgForShortcut(shortcut)',
+ ng-attr-id='dock-svg-{{shortcut.component.uuid}}'
+ )
.sk-app-bar-item.border(ng-if='ctrl.hasPasscode()')
- #lock-item.sk-app-bar-item(ng-click='ctrl.lockApp()', ng-if='ctrl.hasPasscode()', title='Locks application and wipes unencrypted data from memory.')
+ #lock-item.sk-app-bar-item(
+ ng-click='ctrl.lockApp()',
+ ng-if='ctrl.hasPasscode()',
+ title='Locks application and wipes unencrypted data from memory.'
+ )
.sk-label
i#footer-lock-icon.icon.ion-locked
diff --git a/app/assets/templates/home.pug b/app/assets/templates/home.pug
deleted file mode 100644
index 6a1eb149e..000000000
--- a/app/assets/templates/home.pug
+++ /dev/null
@@ -1,7 +0,0 @@
-.main-ui-view(ng-class='platform')
- lock-screen(ng-if='needsUnlock', on-success='onSuccessfulUnlock')
- #app.app(ng-class='appClass', ng-if='!needsUnlock')
- tags-panel(add-new='tagsAddNew', remove-tag='removeTag', save='tagsSave', selection-made='tagsSelectionMade')
- notes-panel(add-new='notesAddNew', selection-made='notesSelectionMade', tag='selectedTag')
- editor-panel(note='selectedNote', remove='deleteNote', update-tags='updateTagsForNote')
- footer(ng-if='!needsUnlock')
diff --git a/app/assets/templates/lock-screen.pug b/app/assets/templates/lock-screen.pug
index 46cd227ff..55f2be715 100644
--- a/app/assets/templates/lock-screen.pug
+++ b/app/assets/templates/lock-screen.pug
@@ -4,18 +4,31 @@
.sk-panel-header-title Passcode Required
.sk-panel-content
.sk-panel-section
- form.sk-panel-form.sk-panel-row(ng-submit='submitPasscodeForm()')
+ form.sk-panel-form.sk-panel-row(ng-submit='ctrl.submitPasscodeForm($event)')
.sk-panel-column.stretch
- input#passcode-input.center-text.sk-input.contrast(autocomplete='new-password', autofocus='true', ng-model='formData.passcode', placeholder='Enter Passcode', should-focus='true', sn-autofocus='true', type='password')
+ input#passcode-input.center-text.sk-input.contrast(
+ autocomplete='new-password',
+ autofocus='true',
+ ng-model='ctrl.formData.passcode',
+ placeholder='Enter Passcode',
+ should-focus='true',
+ sn-autofocus='true',
+ type='password'
+ )
.sk-button-group.stretch.sk-panel-row.form-submit
button.sk-button.info(type='submit')
.sk-label Unlock
.sk-panel-footer
#passcode-reset
- a.sk-a.neutral(ng-click='forgotPasscode()', ng-if='!formData.showRecovery') Forgot?
- div(ng-if='formData.showRecovery')
+ a.sk-a.neutral(
+ ng-click='ctrl.forgotPasscode()',
+ ng-if='!ctrl.formData.showRecovery'
+ ) Forgot?
+ div(ng-if='ctrl.formData.showRecovery')
.sk-p
- | If you forgot your local passcode, your only option is to clear your local data from this device
- | and sign back in to your account.
+ | If you forgot your local passcode, your only option is to clear
+ | your local data from this device and sign back in to your account.
.sk-panel-row
- a.sk-a.danger.center-text(ng-click='beginDeleteData()') Delete Local Data
+ a.sk-a.danger.center-text(
+ ng-click='ctrl.beginDeleteData()'
+ ) Delete Local Data
diff --git a/app/assets/templates/notes.pug b/app/assets/templates/notes.pug
index 461008c36..208d19632 100644
--- a/app/assets/templates/notes.pug
+++ b/app/assets/templates/notes.pug
@@ -3,53 +3,149 @@
#notes-title-bar.section-title-bar
.padded
.section-title-bar-header
- .title {{ctrl.panelTitle}}
- .sk-button.contrast.wide(ng-click='ctrl.createNewNote()', title='Create a new note in the selected tag')
+ .title {{self.state.panelTitle}}
+ .sk-button.contrast.wide(
+ ng-click='self.createNewNote()',
+ title='Create a new note in the selected tag'
+ )
.sk-label
i.icon.ion-plus.add-button
.filter-section(role='search')
- input#search-bar.filter-bar(lowercase='true', ng-blur='ctrl.onFilterEnter()', ng-change='ctrl.filterTextChanged()', ng-keyup='$event.keyCode == 13 && ctrl.onFilterEnter();', ng-model='ctrl.noteFilter.text', placeholder='Search', select-on-click='true', title='Searches notes in the currently selected tag')
- #search-clear-button(ng-click='ctrl.clearFilterText();', ng-show='ctrl.noteFilter.text') ✕
+ input#search-bar.filter-bar(
+ lowercase='true',
+ ng-blur='self.onFilterEnter()',
+ ng-change='self.filterTextChanged()',
+ ng-keyup='$event.keyCode == 13 && self.onFilterEnter();',
+ ng-model='self.state.noteFilter.text',
+ placeholder='Search',
+ select-on-click='true',
+ title='Searches notes in the currently selected tag'
+ )
+ #search-clear-button(
+ ng-click='self.clearFilterText();',
+ ng-show='self.state.noteFilter.text'
+ ) ✕
#notes-menu-bar.sn-component
.sk-app-bar.no-edges
.left
- .sk-app-bar-item(ng-class="{'selected' : ctrl.showMenu}", ng-click='ctrl.showMenu = !ctrl.showMenu')
+ .sk-app-bar-item(
+ ng-class="{'selected' : self.state.mutable.showMenu}",
+ ng-click='self.state.mutable.showMenu = !self.state.mutable.showMenu'
+ )
.sk-app-bar-item-column
.sk-label
| Options
.sk-app-bar-item-column
- .sk-sublabel {{ctrl.optionsSubtitle()}}
- #notes-options-menu.sk-menu-panel.dropdown-menu(ng-show='ctrl.showMenu')
+ .sk-sublabel {{self.optionsSubtitle()}}
+ #notes-options-menu.sk-menu-panel.dropdown-menu(
+ ng-show='self.state.mutable.showMenu'
+ )
.sk-menu-panel-header
.sk-menu-panel-header-title Sort By
- a.info.sk-h5(ng-click='ctrl.toggleReverseSort()')
- | {{ctrl.sortReverse === true ? 'Disable Reverse Sort' : 'Enable Reverse Sort'}}
- menu-row(action="ctrl.selectedMenuItem(); ctrl.selectedSortByCreated()" circle="ctrl.sortBy == 'created_at' && 'success'" desc="'Sort notes by newest first'" label="'Date Added'")
- menu-row(action="ctrl.selectedMenuItem(); ctrl.selectedSortByUpdated()" circle="ctrl.sortBy == 'client_updated_at' && 'success'" desc="'Sort notes with the most recently updated first'" label="'Date Modified'")
- menu-row(action="ctrl.selectedMenuItem(); ctrl.selectedSortByTitle()" circle="ctrl.sortBy == 'title' && 'success'" desc="'Sort notes alphabetically by their title'" label="'Title'")
+ a.info.sk-h5(ng-click='self.toggleReverseSort()')
+ | {{self.state.sortReverse === true ? 'Disable Reverse Sort' : 'Enable Reverse Sort'}}
+ menu-row(
+ action="self.selectedMenuItem(); self.selectedSortByCreated()"
+ circle="self.state.sortBy == 'created_at' && 'success'"
+ desc="'Sort notes by newest first'"
+ label="'Date Added'"
+ )
+ menu-row(
+ action="self.selectedMenuItem(); self.selectedSortByUpdated()"
+ circle="self.state.sortBy == 'client_updated_at' && 'success'"
+ desc="'Sort notes with the most recently updated first'"
+ label="'Date Modified'"
+ )
+ menu-row(
+ action="self.selectedMenuItem(); self.selectedSortByTitle()"
+ circle="self.state.sortBy == 'title' && 'success'"
+ desc="'Sort notes alphabetically by their title'"
+ label="'Title'"
+ )
.sk-menu-panel-section
.sk-menu-panel-header
.sk-menu-panel-header-title Display
- menu-row(action="ctrl.selectedMenuItem(); ctrl.togglePrefKey('showArchived')" circle="ctrl.showArchived ? 'success' : 'danger'" desc="'Archived notes are usually hidden. You can explicitly show them with this option.'" faded="!ctrl.showArchived" label="'Archived Notes'")
- menu-row(action="ctrl.selectedMenuItem(); ctrl.togglePrefKey('hidePinned')" circle="ctrl.hidePinned ? 'danger' : 'success'" desc="'Pinned notes always appear on top. You can hide them temporarily with this option so you can focus on other notes in the list.'" faded="ctrl.hidePinned" label="'Pinned Notes'")
- menu-row(action="ctrl.selectedMenuItem(); ctrl.togglePrefKey('hideNotePreview')" circle="ctrl.hideNotePreview ? 'danger' : 'success'" desc="'Hide the note preview for a more condensed list of notes'" faded="ctrl.hideNotePreview" label="'Note Preview'")
- menu-row(action="ctrl.selectedMenuItem(); ctrl.togglePrefKey('hideDate')" circle="ctrl.hideDate ? 'danger' : 'success'" desc="'Hide the date displayed in each row'" faded="ctrl.hideDate" label="'Date'")
- menu-row(action="ctrl.selectedMenuItem(); ctrl.togglePrefKey('hideTags')" circle="ctrl.hideTags ? 'danger' : 'success'" desc="'Hide the list of tags associated with each note'" faded="ctrl.hideTags" label="'Tags'")
+ menu-row(
+ action="self.selectedMenuItem(); self.togglePrefKey('showArchived')"
+ circle="self.state.showArchived ? 'success' : 'danger'"
+ desc=`'Archived notes are usually hidden.
+ You can explicitly show them with this option.'`
+ faded="!self.state.showArchived"
+ label="'Archived Notes'"
+ )
+ menu-row(
+ action="self.selectedMenuItem(); self.togglePrefKey('hidePinned')"
+ circle="self.state.hidePinned ? 'danger' : 'success'"
+ desc=`'Pinned notes always appear on top. You can hide them temporarily
+ with this option so you can focus on other notes in the list.'`
+ faded="self.state.hidePinned"
+ label="'Pinned Notes'"
+ )
+ menu-row(
+ action="self.selectedMenuItem(); self.togglePrefKey('hideNotePreview')"
+ circle="self.state.hideNotePreview ? 'danger' : 'success'"
+ desc="'Hide the note preview for a more condensed list of notes'"
+ faded="self.state.hideNotePreview"
+ label="'Note Preview'"
+ )
+ menu-row(
+ action="self.selectedMenuItem(); self.togglePrefKey('hideDate')"
+ circle="self.state.hideDate ? 'danger' : 'success'"
+ desc="'Hide the date displayed in each row'"
+ faded="self.state.hideDate"
+ label="'Date'"
+ )
+ menu-row(
+ action="self.selectedMenuItem(); self.togglePrefKey('hideTags')"
+ circle="self.state.hideTags ? 'danger' : 'success'"
+ desc="'Hide the list of tags associated with each note'"
+ faded="self.state.hideTags"
+ label="'Tags'"
+ )
.scrollable
- #notes-scrollable.infinite-scroll(can-load='true', infinite-scroll='ctrl.paginate()', threshold='200')
- .note(ng-class="{'selected' : ctrl.selectedNote == note}", ng-click='ctrl.selectNote(note, true)', ng-repeat='note in (ctrl.renderedNotes = (ctrl.notes | limitTo:ctrl.notesToDisplay)) track by note.uuid')
+ #notes-scrollable.infinite-scroll(
+ can-load='true',
+ infinite-scroll='self.paginate()',
+ threshold='200'
+ )
+ .note(
+ ng-class="{'selected' : self.state.selectedNote == note}",
+ ng-click='self.selectNote(note, true)',
+ ng-repeat='note in self.state.renderedNotes track by note.uuid'
+ )
.note-flags(ng-show='note.flags.length > 0')
.flag(ng-class='flag.class', ng-repeat='flag in note.flags')
.label {{flag.text}}
.name(ng-show='note.title')
| {{note.title}}
- .note-preview(ng-if='!ctrl.hideNotePreview && !note.content.hidePreview && !note.content.protected')
- .html-preview(ng-bind-html='note.content.preview_html', ng-show='note.content.preview_html')
- .plain-preview(ng-show='!note.content.preview_html && note.content.preview_plain') {{note.content.preview_plain}}
- .default-preview(ng-show='!note.content.preview_html && !note.content.preview_plain') {{note.text}}
- .date.faded(ng-show='!ctrl.hideDate')
- span(ng-show="ctrl.sortBy == 'client_updated_at'") Modified {{note.cachedUpdatedAtString || 'Now'}}
- span(ng-show="ctrl.sortBy != 'client_updated_at'") {{note.cachedCreatedAtString || 'Now'}}
+ .note-preview(
+ ng-if=`
+ !self.state.hideNotePreview &&
+ !note.content.hidePreview &&
+ !note.content.protected`
+ )
+ .html-preview(
+ ng-bind-html='note.content.preview_html',
+ ng-show='note.content.preview_html'
+ )
+ .plain-preview(
+ ng-show='!note.content.preview_html && note.content.preview_plain'
+ ) {{note.content.preview_plain}}
+ .default-preview(
+ ng-show='!note.content.preview_html && !note.content.preview_plain'
+ ) {{note.text}}
+ .date.faded(ng-show='!self.state.hideDate')
+ span(ng-show="self.state.sortBy == 'client_updated_at'")
+ | Modified {{note.cachedUpdatedAtString || 'Now'}}
+ span(ng-show="self.state.sortBy != 'client_updated_at'")
+ | {{note.cachedCreatedAtString || 'Now'}}
.tags-string(ng-show='note.shouldShowTags')
.faded {{note.savedTagsString || note.tagsString()}}
- panel-resizer(collapsable="true" control="ctrl.panelController" default-width="300" hoverable="true" on-resize-finish="ctrl.onPanelResize" panel-id="'notes-column'")
+ panel-resizer(
+ collapsable="true"
+ control="self.panelController"
+ default-width="300"
+ hoverable="true"
+ on-resize-finish="self.onPanelResize"
+ panel-id="'notes-column'"
+ )
diff --git a/app/assets/templates/root.pug b/app/assets/templates/root.pug
new file mode 100644
index 000000000..c8483e2d7
--- /dev/null
+++ b/app/assets/templates/root.pug
@@ -0,0 +1,18 @@
+.main-ui-view(
+ ng-class='platform'
+ )
+ lock-screen(
+ ng-if='needsUnlock',
+ on-success='onSuccessfulUnlock'
+ )
+ #app.app(
+ ng-class='appClass',
+ ng-if='!needsUnlock'
+ )
+ tags-panel
+ notes-panel
+ editor-panel
+
+ footer(
+ ng-if='!needsUnlock'
+ )
diff --git a/app/assets/templates/tags.pug b/app/assets/templates/tags.pug
index cabdcced3..4f3340043 100644
--- a/app/assets/templates/tags.pug
+++ b/app/assets/templates/tags.pug
@@ -1,35 +1,64 @@
#tags-column.sn-component.section.tags(aria-label='Tags')
- .component-view-container(ng-if='ctrl.component.active')
- component-view.component-view(component='ctrl.component')
- #tags-content.content(ng-if='!(ctrl.component && ctrl.component.active)')
+ .component-view-container(ng-if='self.component.active')
+ component-view.component-view(component='self.component')
+ #tags-content.content(ng-if='!(self.component && self.component.active)')
.tags-title-section.section-title-bar
.section-title-bar-header
.sk-h3.title
span.sk-bold Views
- .sk-button.sk-secondary-contrast.wide(ng-click='ctrl.clickedAddNewTag()', title='Create a new tag')
+ .sk-button.sk-secondary-contrast.wide(
+ ng-click='self.clickedAddNewTag()',
+ title='Create a new tag'
+ )
.sk-label
i.icon.ion-plus.add-button
.scrollable
.infinite-scroll
- .tag(ng-class="{'selected' : ctrl.selectedTag == tag, 'faded' : !tag.content.isAllTag}", ng-click='ctrl.selectTag(tag)', ng-repeat='tag in ctrl.smartTags')
+ .tag(
+ ng-class="{'selected' : self.state.selectedTag == tag, 'faded' : !tag.content.isAllTag}",
+ ng-click='self.selectTag(tag)',
+ ng-repeat='tag in self.state.smartTags'
+ )
.tag-info
input.title(ng-disabled='true', ng-model='tag.title')
- .count(ng-show='tag.content.isAllTag') {{tag.cachedNoteCount}}
+ .count(ng-show='tag.content.isAllTag') {{self.state.noteCounts[tag.uuid]}}
.tags-title-section.section-title-bar
.section-title-bar-header
.sk-h3.title
span.sk-bold Tags
- .tag(ng-class="{'selected' : ctrl.selectedTag == tag}", ng-click='ctrl.selectTag(tag)', ng-repeat='tag in ctrl.tags track by tag.uuid')
+ .tag(
+ ng-class="{'selected' : self.state.selectedTag == tag}",
+ ng-click='self.selectTag(tag)',
+ ng-repeat='tag in self.state.tags track by tag.uuid'
+ )
.tag-info
.tag-icon #
- input.title(ng-attr-id='tag-{{tag.uuid}}', ng-blur='ctrl.saveTag($event, tag)', ng-change='ctrl.tagTitleDidChange(tag)', ng-class="{'editing' : ctrl.editingTag == tag}", ng-click='ctrl.selectTag(tag)', ng-keyup='$event.keyCode == 13 && $event.target.blur()', ng-model='tag.title', should-focus='ctrl.newTag || ctrl.editingTag == tag', sn-autofocus='true', spellcheck='false')
- .count {{tag.cachedNoteCount}}
+ input.title(
+ ng-attr-id='tag-{{tag.uuid}}',
+ ng-blur='self.saveTag($event, tag)',
+ ng-change='self.tagTitleDidChange(tag)',
+ ng-class="{'editing' : self.state.editingTag == tag}",
+ ng-click='self.selectTag(tag)',
+ ng-keyup='$event.keyCode == 13 && $event.target.blur()',
+ ng-model='tag.title',
+ should-focus='self.state.newTag || self.state.editingTag == tag',
+ sn-autofocus='true',
+ spellcheck='false'
+ )
+ .count {{self.state.noteCounts[tag.uuid]}}
.danger.small-text.bold(ng-show='tag.content.conflict_of') Conflicted Copy
.danger.small-text.bold(ng-show='tag.errorDecrypting') Missing Keys
- .menu(ng-show='ctrl.selectedTag == tag')
- a.item(ng-click='ctrl.selectedRenameTag($event, tag)', ng-show='!ctrl.editingTag') Rename
- a.item(ng-click='ctrl.saveTag($event, tag)', ng-show='ctrl.editingTag') Save
- a.item(ng-click='ctrl.selectedDeleteTag(tag)') Delete
- .no-tags-placeholder(ng-show='ctrl.tags.length == 0')
+ .menu(ng-show='self.state.selectedTag == tag')
+ a.item(ng-click='self.selectedRenameTag($event, tag)', ng-show='!self.state.editingTag') Rename
+ a.item(ng-click='self.saveTag($event, tag)', ng-show='self.state.editingTag') Save
+ a.item(ng-click='self.selectedDeleteTag(tag)') Delete
+ .no-tags-placeholder(ng-show='self.state.tags.length == 0')
| No tags. Create one using the add button above.
- panel-resizer(collapsable='true', control='ctrl.panelController', default-width='150', hoverable='true', on-resize-finish='ctrl.onPanelResize', panel-id="'tags-column'")
+ panel-resizer(
+ collapsable='true',
+ control='self.panelController',
+ default-width='150',
+ hoverable='true',
+ on-resize-finish='self.onPanelResize',
+ panel-id="'tags-column'"
+ )
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
deleted file mode 100644
index de6be7945..000000000
--- a/app/helpers/application_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module ApplicationHelper
-end
diff --git a/app/mailers/.keep b/app/mailers/.keep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/app/models/.keep b/app/models/.keep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/app/views/application/app.html.erb b/app/views/application/app.html.erb
index bdc56278c..f01d26bfe 100644
--- a/app/views/application/app.html.erb
+++ b/app/views/application/app.html.erb
@@ -44,7 +44,7 @@
-
+