@@ -525,17 +525,19 @@ angular.module('app')
|
||||
this.leftResizeControl = {};
|
||||
this.rightResizeControl = {};
|
||||
|
||||
this.onPanelResizeFinish = function(width, left, isMaxWidth) {
|
||||
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();
|
||||
}
|
||||
@@ -546,7 +548,11 @@ angular.module('app')
|
||||
|
||||
this.loadPreferences = function() {
|
||||
this.monospaceFont = authManager.getUserPrefValue("monospaceFont", "monospace");
|
||||
this.spellcheck = authManager.getUserPrefValue("spellcheck", true);
|
||||
|
||||
// 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")) {
|
||||
@@ -560,11 +566,13 @@ angular.module('app')
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -811,6 +819,9 @@ angular.module('app')
|
||||
var parent = this;
|
||||
var handleTab = function (event) {
|
||||
if (!event.shiftKey && event.which == 9) {
|
||||
if(parent.note.locked) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
// Using document.execCommand gives us undo support
|
||||
|
||||
@@ -24,7 +24,7 @@ angular.module('app')
|
||||
})
|
||||
.controller('FooterCtrl', function ($rootScope, authManager, modelManager, $timeout, dbManager,
|
||||
syncManager, storageManager, passcodeManager, componentManager, singletonManager, nativeExtManager,
|
||||
privilegesManager) {
|
||||
privilegesManager, statusManager) {
|
||||
|
||||
authManager.checkForSecurityUpdate().then((available) => {
|
||||
this.securityUpdateAvailable = available;
|
||||
@@ -34,22 +34,28 @@ angular.module('app')
|
||||
this.securityUpdateAvailable = authManager.securityUpdateAvailable;
|
||||
})
|
||||
|
||||
statusManager.addStatusObserver((string) => {
|
||||
$timeout(() => {
|
||||
this.arbitraryStatusMessage = string;
|
||||
})
|
||||
})
|
||||
|
||||
$rootScope.$on("did-begin-local-backup", () => {
|
||||
$timeout(() => {
|
||||
this.arbitraryStatusMessage = "Saving local backup...";
|
||||
this.backupStatus = statusManager.addStatusFromString("Saving local backup...");
|
||||
})
|
||||
});
|
||||
|
||||
$rootScope.$on("did-finish-local-backup", (event, data) => {
|
||||
$timeout(() => {
|
||||
if(data.success) {
|
||||
this.arbitraryStatusMessage = "Successfully saved backup.";
|
||||
this.backupStatus = statusManager.replaceStatusWithString(this.backupStatus, "Successfully saved backup.");
|
||||
} else {
|
||||
this.arbitraryStatusMessage = "Unable to save local backup.";
|
||||
this.backupStatus = statusManager.replaceStatusWithString(this.backupStatus, "Unable to save local backup.");
|
||||
}
|
||||
|
||||
$timeout(() => {
|
||||
this.arbitraryStatusMessage = null;
|
||||
this.backupStatus = statusManager.removeStatus(this.backupStatus);
|
||||
}, 2000)
|
||||
})
|
||||
});
|
||||
@@ -59,12 +65,18 @@ angular.module('app')
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -74,8 +86,8 @@ angular.module('app')
|
||||
this.selectRoom(extWindow);
|
||||
this.reloadInProgress = false;
|
||||
$rootScope.$broadcast("ext-reload-complete");
|
||||
}, 2000)
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
this.getUser = function() {
|
||||
return authManager.user;
|
||||
@@ -176,6 +188,10 @@ angular.module('app')
|
||||
|
||||
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) => {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
angular.module('app')
|
||||
.controller('HomeCtrl', function ($scope, $location, $rootScope, $timeout, modelManager,
|
||||
dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager, migrationManager,
|
||||
privilegesManager) {
|
||||
privilegesManager, statusManager) {
|
||||
|
||||
// Lock syncing until local data is loaded. Syncing may be called from a variety of places,
|
||||
// such as when the window focuses, for example. We don't want sync to occur until all local items are loaded,
|
||||
// otherwise, if sync happens first, then load, the load may override synced values.
|
||||
syncManager.lockSyncing();
|
||||
|
||||
storageManager.initialize(passcodeManager.hasPasscode(), authManager.isEphemeralSession());
|
||||
|
||||
@@ -32,41 +37,28 @@ angular.module('app')
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
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(null, function() {
|
||||
// new database, delete syncToken so that items can be refetched entirely from server
|
||||
syncManager.clearSyncToken();
|
||||
syncManager.sync();
|
||||
})
|
||||
}
|
||||
|
||||
function initiateSync() {
|
||||
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();
|
||||
@@ -98,17 +90,31 @@ angular.module('app')
|
||||
}
|
||||
});
|
||||
|
||||
syncManager.loadLocalItems().then(() => {
|
||||
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(() => {
|
||||
|
||||
// First unlock after initially locked to wait for local data loaded.
|
||||
syncManager.unlockSyncing();
|
||||
|
||||
$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
|
||||
syncManager.sync({performIntegrityCheck: true});
|
||||
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) => {
|
||||
@@ -119,6 +125,38 @@ angular.module('app')
|
||||
})
|
||||
}
|
||||
|
||||
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(null, function() {
|
||||
// new database, delete syncToken so that items can be refetched entirely from server
|
||||
syncManager.clearSyncToken();
|
||||
syncManager.sync();
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Editor Callbacks
|
||||
*/
|
||||
@@ -259,9 +297,24 @@ angular.module('app')
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
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();
|
||||
alert("Please use FileSafe to attach images and files. Learn more at standardnotes.org/filesafe.")
|
||||
}, false)
|
||||
|
||||
|
||||
// Handle Auto Sign In From URL
|
||||
/*
|
||||
Handle Auto Sign In From URL
|
||||
*/
|
||||
|
||||
function urlParam(key) {
|
||||
return $location.search()[key];
|
||||
|
||||
@@ -13,6 +13,19 @@ class LockScreen {
|
||||
|
||||
$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;
|
||||
|
||||
@@ -91,6 +91,15 @@ angular.module('app')
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -173,8 +182,9 @@ angular.module('app')
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
window.onresize = (event) => {
|
||||
this.resetPagination({keepCurrentIfLarger: true});
|
||||
};
|
||||
|
||||
this.paginate = function() {
|
||||
this.notesToDisplay += this.DefaultNotesToDisplayValue
|
||||
@@ -184,7 +194,12 @@ angular.module('app')
|
||||
}
|
||||
}
|
||||
|
||||
this.resetPagination = function() {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,13 @@ class ActionsMenu {
|
||||
// reload extension actions
|
||||
actionsManager.loadExtensionInContextOfItem(extension, $scope.item, function(ext){
|
||||
// keep nested state
|
||||
if(parentAction) {
|
||||
var matchingAction = _.find(ext.actions, {label: parentAction.label});
|
||||
matchingAction.subrows = $scope.subRowsForAction(parentAction, extension);
|
||||
}
|
||||
// 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);
|
||||
// }
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,6 +93,23 @@ class ComponentView {
|
||||
}
|
||||
}, 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);
|
||||
|
||||
@@ -101,7 +118,7 @@ class ComponentView {
|
||||
// we don't use ng-show because it causes problems with rendering iframes after timeout, for some reason.
|
||||
$timeout(() => {
|
||||
$scope.loading = false;
|
||||
$scope.issueLoading = false;
|
||||
$scope.issueLoading = desktopError; /* Typically we'd just set this to false at this point, but we now account for desktopError */
|
||||
}, 7)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,9 +50,17 @@ class PanelResizer {
|
||||
let resizerWidth = resizerColumn.offsetWidth;
|
||||
let minWidth = $scope.minWidth || resizerWidth;
|
||||
var pressed = false;
|
||||
var startWidth = panel.scrollWidth, startX = 0, lastDownX = 0, collapsed, lastWidth = startWidth, startLeft, lastLeft;
|
||||
var 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 = () => {
|
||||
@@ -91,7 +99,8 @@ class PanelResizer {
|
||||
}
|
||||
|
||||
function reloadDefaultValues() {
|
||||
startWidth = panel.scrollWidth;
|
||||
startWidth = $scope.isAtMaxWidth() ? getParentRect().width : panel.scrollWidth;
|
||||
lastWidth = startWidth;
|
||||
appFrame = document.getElementById("app").getBoundingClientRect();
|
||||
}
|
||||
reloadDefaultValues();
|
||||
@@ -120,9 +129,9 @@ class PanelResizer {
|
||||
width = maxWidth;
|
||||
}
|
||||
|
||||
if(width == parentRect.width) {
|
||||
panel.style.width = "100%";
|
||||
panel.style.flexBasis = "100%";
|
||||
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";
|
||||
@@ -135,14 +144,6 @@ class PanelResizer {
|
||||
}
|
||||
}
|
||||
|
||||
$scope.isCollapsed = function() {
|
||||
return lastWidth <= minWidth;
|
||||
}
|
||||
|
||||
$scope.isAtMaxWidth = function() {
|
||||
return lastWidth == getParentRect().width;
|
||||
}
|
||||
|
||||
$scope.setLeft = function(left) {
|
||||
panel.style.left = left + "px";
|
||||
lastLeft = left;
|
||||
@@ -218,10 +219,7 @@ class PanelResizer {
|
||||
})
|
||||
|
||||
function handleWidthEvent(event) {
|
||||
var rect = panel.getBoundingClientRect();
|
||||
var panelMaxX = rect.left + (startWidth || panel.style.maxWidth);
|
||||
|
||||
var x;
|
||||
let x;
|
||||
if(event) {
|
||||
x = event.clientX;
|
||||
} else {
|
||||
|
||||
@@ -22,7 +22,7 @@ class SessionHistoryMenu {
|
||||
})
|
||||
$scope.history = history;
|
||||
}
|
||||
|
||||
|
||||
$scope.reloadHistory();
|
||||
|
||||
$scope.openRevision = function(revision) {
|
||||
@@ -65,6 +65,11 @@ class SessionHistoryMenu {
|
||||
}
|
||||
|
||||
$scope.toggleDiskSaving = function() {
|
||||
if(!sessionHistory.diskEnabled) {
|
||||
if(!confirm("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.")){
|
||||
return;
|
||||
}
|
||||
}
|
||||
sessionHistory.toggleDiskSaving().then(() => {
|
||||
$timeout(() => {
|
||||
$scope.diskEnabled = sessionHistory.diskEnabled;
|
||||
|
||||
@@ -122,18 +122,22 @@ class ActionsManager {
|
||||
|
||||
switch (action.verb) {
|
||||
case "get": {
|
||||
this.httpManager.getAbsolute(action.url, {}, async (response) => {
|
||||
action.error = false;
|
||||
handleResponseDecryption(response, await this.authManager.keys(), true);
|
||||
}, (response) => {
|
||||
action.error = true;
|
||||
customCallback(null);
|
||||
})
|
||||
if(confirm("Are you sure you want to replace the current note contents with this action's results?")) {
|
||||
this.httpManager.getAbsolute(action.url, {}, async (response) => {
|
||||
action.error = false;
|
||||
handleResponseDecryption(response, await this.authManager.keys(), true);
|
||||
}, (response) => {
|
||||
if(response && response.error) {
|
||||
alert("An issue occurred while processing this action. Please try again.");
|
||||
}
|
||||
action.error = true;
|
||||
customCallback(null);
|
||||
})
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "render": {
|
||||
|
||||
this.httpManager.getAbsolute(action.url, {}, async (response) => {
|
||||
action.error = false;
|
||||
handleResponseDecryption(response, await this.authManager.keys(), false);
|
||||
@@ -158,7 +162,10 @@ class ActionsManager {
|
||||
items: [itemParams] // Wrap it in an array
|
||||
}
|
||||
|
||||
this.performPost(action, extension, params, function(response){
|
||||
this.performPost(action, extension, params, (response) => {
|
||||
if(response && response.error) {
|
||||
alert("An issue occurred while processing this action. Please try again.");
|
||||
}
|
||||
customCallback(response);
|
||||
});
|
||||
})
|
||||
|
||||
@@ -24,6 +24,11 @@ class AuthManager extends SFAuthManager {
|
||||
|
||||
this.configureUserPrefs();
|
||||
this.checkForSecurityUpdate();
|
||||
|
||||
this.modelManager.addItemSyncObserver("user-prefs", "SN|UserPreferences", (allItems, validItems, deletedItems, source, sourceKey) => {
|
||||
this.userPreferencesDidChange();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
offline() {
|
||||
@@ -136,7 +141,6 @@ class AuthManager extends SFAuthManager {
|
||||
let contentTypePredicate = new SFPredicate("content_type", "=", prefsContentType);
|
||||
this.singletonManager.registerSingleton([contentTypePredicate], (resolvedSingleton) => {
|
||||
this.userPreferences = resolvedSingleton;
|
||||
this.userPreferencesDidChange();
|
||||
}, (valueCallback) => {
|
||||
// Safe to create. Create and return object.
|
||||
var prefs = new SFItem({content_type: prefsContentType});
|
||||
|
||||
@@ -28,6 +28,10 @@ class DesktopManager {
|
||||
})
|
||||
}
|
||||
|
||||
saveBackup() {
|
||||
this.majorDataChangeHandler && this.majorDataChangeHandler();
|
||||
}
|
||||
|
||||
getApplicationDataPath() {
|
||||
console.assert(this.applicationDataPath, "applicationDataPath is null");
|
||||
return this.applicationDataPath;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
class MigrationManager extends SFMigrationManager {
|
||||
|
||||
constructor($rootScope, modelManager, syncManager, componentManager, storageManager) {
|
||||
super(modelManager, syncManager, storageManager);
|
||||
constructor($rootScope, modelManager, syncManager, componentManager, storageManager, statusManager, authManager, desktopManager) {
|
||||
super(modelManager, syncManager, storageManager, authManager);
|
||||
this.componentManager = componentManager;
|
||||
this.statusManager = statusManager;
|
||||
this.desktopManager = desktopManager;
|
||||
}
|
||||
|
||||
registeredMigrations() {
|
||||
return [
|
||||
this.editorToComponentMigration(),
|
||||
this.componentUrlToHostedUrl()
|
||||
this.componentUrlToHostedUrl(),
|
||||
this.removeTagReferencesFromNotes()
|
||||
];
|
||||
}
|
||||
|
||||
@@ -54,6 +57,10 @@ class MigrationManager extends SFMigrationManager {
|
||||
component.url value to store clientData, such as the CodeEditor, which stores the programming language for the note
|
||||
in the note's clientData[component.url]. We want to rewrite any matching items to transfer that clientData into
|
||||
clientData[component.uuid].
|
||||
|
||||
April 3, 2019 note: it seems this migration is mis-named. The first part of the description doesn't match what the code is actually doing.
|
||||
It has nothing to do with url/hosted_url relationship and more to do with just mapping client data from the note's hosted_url to its uuid
|
||||
|
||||
Created: July 6, 2018
|
||||
*/
|
||||
componentUrlToHostedUrl() {
|
||||
@@ -62,10 +69,10 @@ class MigrationManager extends SFMigrationManager {
|
||||
content_type: "SN|Component",
|
||||
handler: async (components) => {
|
||||
let hasChanges = false;
|
||||
var notes = this.modelManager.validItemsForContentType("Note");
|
||||
for(var note of notes) {
|
||||
for(var component of components) {
|
||||
var clientData = note.getDomainDataItem(component.hosted_url, ComponentManager.ClientDataDomain);
|
||||
let notes = this.modelManager.validItemsForContentType("Note");
|
||||
for(let note of notes) {
|
||||
for(let component of components) {
|
||||
let clientData = note.getDomainDataItem(component.hosted_url, ComponentManager.ClientDataDomain);
|
||||
if(clientData) {
|
||||
note.setDomainDataItem(component.uuid, clientData, ComponentManager.ClientDataDomain);
|
||||
note.setDomainDataItem(component.hosted_url, null, ComponentManager.ClientDataDomain);
|
||||
@@ -81,6 +88,74 @@ class MigrationManager extends SFMigrationManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Migrate notes which have relationships on tags to migrate those relationships to the tags themselves.
|
||||
That is, notes.content.references should not include any mention of tags.
|
||||
This will apply to notes created before the schema change. Now, only tags reference notes.
|
||||
Created: April 3, 2019
|
||||
*/
|
||||
removeTagReferencesFromNotes() {
|
||||
return {
|
||||
name: "remove-tag-references-from-notes",
|
||||
content_type: "Note",
|
||||
handler: async (notes) => {
|
||||
|
||||
let needsSync = false;
|
||||
let status = this.statusManager.addStatusFromString("Optimizing data...");
|
||||
let dirtyCount = 0;
|
||||
|
||||
for(let note of notes) {
|
||||
if(!note.content) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let references = note.content.references;
|
||||
// Remove any tag references, and transfer them to the tag if neccessary.
|
||||
let newReferences = [];
|
||||
|
||||
for(let reference of references) {
|
||||
if(reference.content_type != "Tag") {
|
||||
newReferences.push(reference);
|
||||
continue;
|
||||
}
|
||||
|
||||
// is Tag content_type, we will not be adding this to newReferences
|
||||
let tag = this.modelManager.findItem(reference.uuid);
|
||||
if(tag && !tag.hasRelationshipWithItem(note)) {
|
||||
tag.addItemAsRelationship(note);
|
||||
tag.setDirty(true, true);
|
||||
dirtyCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if(newReferences.length != references.length) {
|
||||
note.content.references = newReferences;
|
||||
note.setDirty(true, true);
|
||||
dirtyCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if(dirtyCount > 0) {
|
||||
if(isDesktopApplication()) {
|
||||
this.desktopManager.saveBackup();
|
||||
}
|
||||
|
||||
status = this.statusManager.replaceStatusWithString(status, `${dirtyCount} items optimized.`);
|
||||
await this.syncManager.sync();
|
||||
|
||||
status = this.statusManager.replaceStatusWithString(status, `Optimization complete.`);
|
||||
setTimeout(() => {
|
||||
this.statusManager.removeStatus(status);
|
||||
}, 2000);
|
||||
} else {
|
||||
this.statusManager.removeStatus(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app').service('migrationManager', MigrationManager);
|
||||
|
||||
@@ -11,6 +11,7 @@ class PasscodeManager {
|
||||
this._hasPasscode = this.storageManager.getItemSync("offlineParams", StorageManager.Fixed) != null;
|
||||
this._locked = this._hasPasscode;
|
||||
|
||||
this.visibilityObservers = [];
|
||||
this.passcodeChangeObservers = [];
|
||||
|
||||
this.configureAutoLock();
|
||||
@@ -37,6 +38,21 @@ class PasscodeManager {
|
||||
return this._keys;
|
||||
}
|
||||
|
||||
addVisibilityObserver(callback) {
|
||||
this.visibilityObservers.push(callback);
|
||||
return callback;
|
||||
}
|
||||
|
||||
removeVisibilityObserver(callback) {
|
||||
_.pull(this.visibilityObservers, callback);
|
||||
}
|
||||
|
||||
notifiyVisibilityObservers(visible) {
|
||||
for(let callback of this.visibilityObservers) {
|
||||
callback(visible);
|
||||
}
|
||||
}
|
||||
|
||||
async setAutoLockInterval(interval) {
|
||||
return this.storageManager.setItem(PasscodeManager.AutoLockIntervalKey, JSON.stringify(interval), StorageManager.FixedEncrypted);
|
||||
}
|
||||
@@ -213,6 +229,8 @@ class PasscodeManager {
|
||||
} else {
|
||||
this.beginAutoLockTimer();
|
||||
}
|
||||
|
||||
this.notifiyVisibilityObservers(visible);
|
||||
}
|
||||
|
||||
async beginAutoLockTimer() {
|
||||
|
||||
67
app/assets/javascripts/app/services/statusManager.js
Normal file
67
app/assets/javascripts/app/services/statusManager.js
Normal file
@@ -0,0 +1,67 @@
|
||||
class StatusManager {
|
||||
|
||||
constructor() {
|
||||
this.statuses = [];
|
||||
this.observers = [];
|
||||
}
|
||||
|
||||
statusFromString(string) {
|
||||
return {string: string};
|
||||
}
|
||||
|
||||
replaceStatusWithString(status, string) {
|
||||
this.removeStatus(status);
|
||||
return this.addStatusFromString(string);
|
||||
}
|
||||
|
||||
addStatusFromString(string) {
|
||||
return this.addStatus(this.statusFromString(string));
|
||||
}
|
||||
|
||||
addStatus(status) {
|
||||
if(typeof status !== "object") {
|
||||
console.error("Attempting to set non-object status", status);
|
||||
return;
|
||||
}
|
||||
|
||||
this.statuses.push(status);
|
||||
this.notifyObservers();
|
||||
return status;
|
||||
}
|
||||
|
||||
removeStatus(status) {
|
||||
_.pull(this.statuses, status);
|
||||
this.notifyObservers();
|
||||
return null;
|
||||
}
|
||||
|
||||
getStatusString() {
|
||||
let result = "";
|
||||
this.statuses.forEach((status, index) => {
|
||||
if(index > 0) {
|
||||
result += " ";
|
||||
}
|
||||
result += status.string;
|
||||
})
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
notifyObservers() {
|
||||
for(let observer of this.observers) {
|
||||
observer(this.getStatusString());
|
||||
}
|
||||
}
|
||||
|
||||
addStatusObserver(callback) {
|
||||
this.observers.push(callback);
|
||||
}
|
||||
|
||||
removeStatusObserver(callback) {
|
||||
_.pull(this.statuses, callback);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').service('statusManager', StatusManager);
|
||||
@@ -107,7 +107,12 @@ class StorageManager extends SFStorageManager {
|
||||
|
||||
async setItem(key, value, vaultKey) {
|
||||
var storage = this.getVault(vaultKey);
|
||||
storage.setItem(key, value);
|
||||
try {
|
||||
storage.setItem(key, value);
|
||||
} catch (e) {
|
||||
console.error("Exception while trying to setItem in StorageManager:", e);
|
||||
alert("The application's local storage is out of space. If you have Session History save-to-disk enabled, please disable it, and try again.");
|
||||
}
|
||||
|
||||
if(vaultKey === StorageManager.FixedEncrypted || (!vaultKey && this.itemsStorageMode === StorageManager.FixedEncrypted)) {
|
||||
this.writeEncryptedStorageToDisk();
|
||||
|
||||
@@ -88,7 +88,7 @@ $heading-height: 75px;
|
||||
iframe {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
position: absolute; // Required for autocomplete window to show
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,32 +13,20 @@
|
||||
@font-face { font-family: "Ionicons"; src: url("../assets/ionicons.eot?v=2.0.0"); src: url("../assets/ionicons.eot?v=2.0.1#iefix") format("embedded-opentype"), url("../assets/ionicons.ttf?v=2.0.1") format("truetype"), url("../assets/ionicons.woff?v=2.0.1") format("woff"), url("../assets/ionicons.svg?v=2.0.1#Ionicons") format("svg"); font-weight: normal; font-style: normal; }
|
||||
|
||||
.ion, .ionicons,
|
||||
.ion-ios-box:before,
|
||||
.ion-bookmark:before,
|
||||
.ion-locked:before,
|
||||
.ion-plus:before,
|
||||
.ion-arrow-return-left:before,
|
||||
.ion-arrow-return-right:before,
|
||||
.ion-key:before,
|
||||
.ion-lock-combination:before,
|
||||
.ion-eye-disabled:before
|
||||
{
|
||||
display: inline-block; font-family: "Ionicons"; speak: none; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; text-rendering: auto; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.ion-ios-box:before { content: "\f3ec"; }
|
||||
|
||||
.ion-locked:before { content: "\f200"; }
|
||||
|
||||
.ion-bookmark:before { content: "\f26b"; }
|
||||
.ion-plus:before { content: "\f218"; }
|
||||
|
||||
.ion-arrow-return-left:before { content: "\f265"; }
|
||||
|
||||
.ion-arrow-return-right:before { content: "\f266"; }
|
||||
|
||||
.ion-key:before { content: "\f296"; }
|
||||
|
||||
.ion-lock-combination:before { content: "\f4d4"; }
|
||||
|
||||
.ion-eye-disabled:before { content: "\f306"; }
|
||||
|
||||
/*# sourceMappingURL=ionicons.css.map */
|
||||
|
||||
@@ -6,6 +6,8 @@ $z-index-dropdown-menu: 100;
|
||||
$z-index-resizer-overlay: 1000;
|
||||
$z-index-panel-resizer: 1001;
|
||||
|
||||
$z-index-component-view: 1000;
|
||||
|
||||
$z-index-footer-bar: 2000;
|
||||
$z-index-footer-bar-item: 2000;
|
||||
$z-index-footer-bar-item-panel: 2000;
|
||||
@@ -188,12 +190,15 @@ $footer-height: 32px;
|
||||
}
|
||||
|
||||
.section-title-bar {
|
||||
font-weight: bold;
|
||||
|
||||
.padded {
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.section-title-bar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -180,6 +180,7 @@
|
||||
|
||||
.sn-component {
|
||||
min-width: 100%;
|
||||
z-index: $z-index-component-view;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
|
||||
@@ -19,6 +19,14 @@
|
||||
|
||||
|
||||
.sk-app-bar {
|
||||
|
||||
&.dynamic-height {
|
||||
min-height: 2rem !important;
|
||||
height: inherit !important;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&.no-top-edge {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
transition: height .1s ease-in-out;
|
||||
position: relative;
|
||||
|
||||
> .info {
|
||||
> .tag-info {
|
||||
height: 20px;
|
||||
|
||||
> .title {
|
||||
@@ -65,6 +65,14 @@
|
||||
text-overflow: ellipsis;
|
||||
width: 75%;
|
||||
|
||||
// Required for Safari to avoid highlighting when dragging panel resizers
|
||||
// Make sure to undo if it's selected (for editing)
|
||||
-webkit-user-select: none;
|
||||
|
||||
&.editing {
|
||||
-webkit-user-select: text !important;
|
||||
}
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -96,8 +104,10 @@
|
||||
}
|
||||
|
||||
&.selected {
|
||||
> .title {
|
||||
cursor: text;
|
||||
> .tag-info {
|
||||
.title {
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
%form.sk-panel-form{"ng-submit" => "submitAuthForm()"}
|
||||
.sk-panel-section
|
||||
%input.sk-input.contrast{:placeholder => 'Email', "sn-autofocus" => 'true', "should-focus" => "true", :name => 'email', :required => true, :type => 'email', 'ng-model' => 'formData.email'}
|
||||
%input.sk-input.contrast{:placeholder => 'Email', "sn-autofocus" => 'true', "should-focus" => "true", :name => 'email', :required => true, :type => 'email', 'ng-model' => 'formData.email', 'spellcheck' => 'false'}
|
||||
%input.sk-input.contrast{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'formData.user_password', 'sn-enter' => 'submitAuthForm()'}
|
||||
%input.sk-input.contrast{:placeholder => 'Confirm Password', "ng-if" => "formData.showRegister", :name => 'password', :required => true, :type => 'password', 'ng-model' => 'formData.password_conf', 'sn-enter' => 'submitAuthForm()'}
|
||||
.sk-panel-row
|
||||
@@ -39,7 +39,7 @@
|
||||
%input.sk-input{"type" => "checkbox", "ng-model" => "formData.strictSignin"}
|
||||
Use strict sign in
|
||||
%span
|
||||
%a{"href" => "https://standardnotes.org/help/security", "target" => "_blank"} (Learn more)
|
||||
%a.info{"href" => "https://standardnotes.org/help/security", "target" => "_blank", "rel" => "noopener"} (Learn more)
|
||||
|
||||
.sk-panel-section.form-submit{"ng-if" => "!formData.authenticating"}
|
||||
.sk-button-group.stretch
|
||||
@@ -85,7 +85,7 @@
|
||||
.sk-notification.danger{"ng-if" => "syncStatus.error"}
|
||||
.sk-notification-title Sync Unreachable
|
||||
.sk-notification-text Hmm...we can't seem to sync your account. The reason: {{syncStatus.error.message}}
|
||||
%a.sk-a.info-contrast.sk-bold.sk-panel-row{"href" => "https://standardnotes.org/help", "target" => "_blank"} Need help?
|
||||
%a.sk-a.info-contrast.sk-bold.sk-panel-row{"href" => "https://standardnotes.org/help", "target" => "_blank", "rel" => "noopener"} Need help?
|
||||
|
||||
.sk-panel-row
|
||||
.sk-panel-column
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.sn-component
|
||||
.sk-menu-panel.dropdown-menu
|
||||
|
||||
%a.no-decoration{"ng-if" => "extensions.length == 0", "href" => "https://standardnotes.org/extensions", "target" => "blank"}
|
||||
%a.no-decoration{"ng-if" => "extensions.length == 0", "href" => "https://standardnotes.org/extensions", "target" => "blank", "rel" => "noopener"}
|
||||
%menu-row{"label" => "'Download Actions'"}
|
||||
|
||||
%div{"ng-repeat" => "extension in extensions"}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
.sn-component{"ng-if" => "issueLoading"}
|
||||
.sk-app-bar.no-edges.no-top-edge
|
||||
.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}}.
|
||||
.right
|
||||
.sk-app-bar-item{"ng-click" => "reloadComponent()"}
|
||||
.sk-label Reload
|
||||
.sk-button.info
|
||||
.sk-label Reload
|
||||
|
||||
.sn-component{"ng-if" => "showNoThemesMessage"}
|
||||
.sk-app-bar.no-edges.no-top-edge
|
||||
.sk-app-bar.no-edges.no-top-edge.dynamic-height
|
||||
.left
|
||||
.sk-app-bar-item
|
||||
.sk-label.warning This extension does not support themes.
|
||||
@@ -19,19 +20,25 @@
|
||||
.sk-label Disable Active Theme
|
||||
|
||||
.sn-component{"ng-if" => "expired"}
|
||||
.sk-app-bar.no-edges.no-top-edge
|
||||
.sk-app-bar.no-edges.no-top-edge.dynamic-height
|
||||
.left
|
||||
.sk-app-bar-item
|
||||
.sk-app-bar-item-column
|
||||
.sk-circle.danger.small
|
||||
.sk-app-bar-item-column
|
||||
%a.sk-label.sk-base{"href" => "https://dashboard.standardnotes.org", "target" => "_blank"}
|
||||
Your Extended subscription expired on {{component.dateToLocalizedString(component.valid_until)}}.
|
||||
Extensions are in a read-only state.
|
||||
%div
|
||||
%a.sk-label.sk-base{"href" => "https://dashboard.standardnotes.org", "target" => "_blank", "rel" => "noopener"}
|
||||
Your Extended subscription expired on {{component.dateToLocalizedString(component.valid_until)}}.
|
||||
.sk-p
|
||||
Extensions are in a read-only state.
|
||||
.right
|
||||
.sk-app-bar-item{"ng-click" => "reloadComponent()"}
|
||||
.sk-button.info
|
||||
.sk-label Reload
|
||||
.sk-app-bar-item
|
||||
.sk-app-bar-item-column
|
||||
%a.sk-label{"href" => "https://standardnotes.org/help", "target" => "_blank"} Help
|
||||
.sk-button.warning
|
||||
%a.sk-label{"href" => "https://standardnotes.org/help/41/expired", "target" => "_blank", "rel" => "noopener"} Help
|
||||
|
||||
.sn-component{"ng-if" => "error == 'offline-restricted'"}
|
||||
.sk-panel.static
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
%strong.danger.medium-text{"ng-if" => "editor.content.conflict_of"} Conflicted copy
|
||||
.sk-sublabel{"ng-if" => "shouldDisplayRunningLocallyLabel(editor)"} Running Locally
|
||||
|
||||
%a.no-decoration{"ng-if" => "editors.length == 0", "href" => "https://standardnotes.org/extensions", "target" => "blank"}
|
||||
%a.no-decoration{"ng-if" => "editors.length == 0", "href" => "https://standardnotes.org/extensions", "target" => "blank", "rel" => "noopener"}
|
||||
%menu-row{"label" => "'Download More Editors'"}
|
||||
|
||||
@@ -19,9 +19,10 @@
|
||||
%p.sk-p.sk-panel-row
|
||||
A new update is available for your account. Updates address improvements and enhancements to our security specification.
|
||||
This process will guide you through the update, and perform the steps necessary with your supervision.
|
||||
%p.sk-p.sk-panel-row
|
||||
For more information about security updates, please visit
|
||||
%a.sk-a.info{"href" => "https://standardnotes.org/help/security", "target" => "_blank"} standardnotes.org/help/security.
|
||||
.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", "target" => "_blank", "rel" => "noopener"} standardnotes.org/help/security.
|
||||
|
||||
%p.sk-panel-row.sk-p
|
||||
.info Press Continue to proceed.
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
.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", "target" => "_blank"} https://standardnotes.org/permissions.
|
||||
%a.sk-a.info{"href" => "https://standardnotes.org/permissions", "target" => "_blank", "rel" => "noopener"} https://standardnotes.org/permissions.
|
||||
.sk-panel-footer
|
||||
.sk-button.info.big.block.bold{"ng-click" => "accept()"}
|
||||
.sk-label Continue
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
Automatically cleans up small revisions to conserve space.
|
||||
%menu-row{"label" => "(diskEnabled ? 'Disable' : 'Enable') + ' saving history to disk'", "action" => "toggleDiskSaving()"}
|
||||
.sk-sublabel
|
||||
Saving to disk may increase app loading time and memory footprint.
|
||||
Saving to disk is not recommended. Decreases performance and increases app loading time and memory footprint.
|
||||
|
||||
%menu-row{"ng-repeat" => "revision in entries",
|
||||
"action" => "openRevision(revision);",
|
||||
|
||||
@@ -8,16 +8,20 @@
|
||||
.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.
|
||||
|
||||
.sk-p.sk-panel-row
|
||||
.sk-panel-column
|
||||
%strong.sk-panel-row Option 1 (recommended) — Sign Out:
|
||||
%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.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.
|
||||
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 2 — Sync Resolution:
|
||||
.sk-p We can attempt to reconcile changes by downloading all data from the server.
|
||||
%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.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
.title
|
||||
%input.input#note-title-editor{"ng-model" => "ctrl.note.title", "ng-keyup" => "$event.keyCode == 13 && ctrl.onTitleEnter($event)",
|
||||
"ng-change" => "ctrl.onTitleChange()", "ng-focus" => "ctrl.onNameFocus()", "ng-blur" => "ctrl.onNameBlur()",
|
||||
"select-on-click" => "true", "ng-disabled" => "ctrl.note.locked"}
|
||||
"select-on-click" => "true", "ng-disabled" => "ctrl.note.locked", "spellcheck" => "false"}
|
||||
|
||||
#save-status
|
||||
.message{"ng-class" => "{'warning sk-bold': ctrl.syncTakingTooLong, 'danger sk-bold': ctrl.saveError}"} {{ctrl.noteStatus.message}}
|
||||
@@ -38,11 +38,12 @@
|
||||
%menu-row{"label" => "ctrl.note.locked ? 'Unlock' : 'Lock'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleLockNote()", "desc" => "'Locking notes prevents unintentional editing'"}
|
||||
%menu-row{"label" => "ctrl.note.content.protected ? 'Unprotect' : 'Protect'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleProtectNote()", "desc" => "'Protecting a note will require credentials to view it (Manage Privileges via Account menu)'"}
|
||||
%menu-row{"label" => "'Preview'", "circle" => "ctrl.note.content.hidePreview ? 'danger' : 'success'", "circle-align" => "'right'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleNotePreview()", "desc" => "'Hide or unhide the note preview from the list of notes'"}
|
||||
%menu-row{"ng-show" => "!ctrl.note.content.trashed", "label" => "'Move to Trash'", "action" => "ctrl.selectedMenuItem(); ctrl.deleteNote()", "stylekit-class" => "'warning'", "desc" => "'Send this note to the trash'"}
|
||||
%menu-row{"ng-show" => "!ctrl.note.content.trashed && !ctrl.note.errorDecrypting", "label" => "'Move to Trash'", "action" => "ctrl.selectedMenuItem(); ctrl.deleteNote()", "stylekit-class" => "'warning'", "desc" => "'Send this note to the trash'"}
|
||||
%menu-row{"ng-show" => "!ctrl.note.content.trashed && ctrl.note.errorDecrypting", "label" => "'Delete Permanently'", "action" => "ctrl.selectedMenuItem(); ctrl.deleteNotePermanantely()", "stylekit-class" => "'danger'", "desc" => "'Delete this note permanently from all your devices'"}
|
||||
|
||||
%div{"ng-if" => "ctrl.note.content.trashed"}
|
||||
%menu-row{"label" => "'Restore'", "action" => "ctrl.selectedMenuItem(true); ctrl.restoreTrashedNote()", "stylekit-class" => "'info'", "desc" => "'Undelete this note and restore it back into your notes'"}
|
||||
%menu-row{"label" => "'Delete Forever'", "action" => "ctrl.selectedMenuItem(true); ctrl.deleteNotePermanantely()", "stylekit-class" => "'danger'", "desc" => "'Delete this note permanently from all your devices'"}
|
||||
%menu-row{"label" => "'Delete Permanently'", "action" => "ctrl.selectedMenuItem(true); ctrl.deleteNotePermanantely()", "stylekit-class" => "'danger'", "desc" => "'Delete this note permanently from all your devices'"}
|
||||
%menu-row{"label" => "'Empty Trash'", "subtitle" => "ctrl.getTrashCount() + ' notes in trash'", "action" => "ctrl.selectedMenuItem(true); ctrl.emptyTrash()", "stylekit-class" => "'danger'", "desc" => "'Permanently delete all notes in the trash'"}
|
||||
|
||||
.sk-menu-panel-section
|
||||
@@ -78,7 +79,7 @@
|
||||
|
||||
%textarea.editable#note-text-editor{"ng-if" => "!ctrl.selectedEditor", "ng-model" => "ctrl.note.text", "ng-readonly" => "ctrl.note.locked",
|
||||
"ng-change" => "ctrl.contentChanged()", "ng-trim" => "false", "ng-click" => "ctrl.clickedTextArea()",
|
||||
"ng-focus" => "ctrl.onContentFocus()", "dir" => "auto", "ng-attr-spellcheck" => "{{ctrl.spellcheck}}", "ng-model-options"=>"{ debounce: 200 }"}
|
||||
"ng-focus" => "ctrl.onContentFocus()", "dir" => "auto", "ng-attr-spellcheck" => "{{ctrl.spellcheck}}", "ng-model-options"=>"{ debounce: 300 }"}
|
||||
{{ctrl.onSystemEditorLoad()}}
|
||||
|
||||
%panel-resizer{"ng-if" => "ctrl.marginResizersEnabled", "panel-id" => "'editor-content'", "on-resize-finish" => "ctrl.onPanelResizeFinish", "control" => "ctrl.rightResizeControl", "min-width" => 300, "hoverable" => "true", "property" => "'right'"}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
%account-menu{"ng-click" => "$event.stopPropagation()", "ng-if" => "ctrl.showAccountMenu", "on-successful-auth" => "ctrl.onAuthSuccess", "close-function" => "ctrl.closeAccountMenu"}
|
||||
|
||||
.sk-app-bar-item
|
||||
%a.no-decoration.sk-label.title{"href" => "https://standardnotes.org/help", "target" => "_blank"}
|
||||
%a.no-decoration.sk-label.title{"href" => "https://standardnotes.org/help", "target" => "_blank", "rel" => "noopener"}
|
||||
Help
|
||||
|
||||
.sk-app-bar-item.border
|
||||
@@ -59,6 +59,6 @@
|
||||
.svg-item{"ng-attr-id" => "dock-svg-{{shortcut.component.uuid}}", "elem-ready" => "ctrl.initSvgForShortcut(shortcut)"}
|
||||
|
||||
.sk-app-bar-item.border{"ng-if" => "ctrl.hasPasscode()"}
|
||||
.sk-app-bar-item#lock-item{"ng-if" => "ctrl.hasPasscode()", "title" => "Locks application and wipes unencrypted data from memory."}
|
||||
.sk-app-bar-item#lock-item{"ng-if" => "ctrl.hasPasscode()", "ng-click" => "ctrl.lockApp()", "title" => "Locks application and wipes unencrypted data from memory."}
|
||||
.sk-label
|
||||
%i.icon.ion-locked#footer-lock-icon{"ng-if" => "ctrl.hasPasscode()", "ng-click" => "ctrl.lockApp()"}
|
||||
%i.icon.ion-locked#footer-lock-icon
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
.sk-panel-section
|
||||
%form.sk-panel-form.sk-panel-row{"ng-submit" => "submitPasscodeForm()"}
|
||||
.sk-panel-column.stretch
|
||||
%input.center-text.sk-input.contrast{:type => 'password',
|
||||
%input#passcode-input.center-text.sk-input.contrast{:type => 'password',
|
||||
"ng-model" => "formData.passcode", "autofocus" => "true", "sn-autofocus" => "true", "should-focus" => "true",
|
||||
"placeholder" => "Enter Passcode", "autocomplete" => "new-password"}
|
||||
.sk-button-group.stretch.sk-panel-row.form-submit
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
.section-title-bar-header
|
||||
.title {{ctrl.panelTitle}}
|
||||
.sk-button.contrast.wide{"ng-click" => "ctrl.createNewNote()", "title" => "Create a new note in the selected tag"}
|
||||
.sk-label +
|
||||
.sk-label
|
||||
%i.icon.ion-plus.add-button
|
||||
.filter-section{"role" => "search"}
|
||||
%input.filter-bar#search-bar.mousetrap{"select-on-click" => "true", "ng-model" => "ctrl.noteFilter.text", "placeholder" => "Search",
|
||||
"ng-change" => "ctrl.filterTextChanged()", "lowercase" => "true", "ng-blur" => "ctrl.onFilterEnter()", "ng-keyup" => "$event.keyCode == 13 && ctrl.onFilterEnter();",
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
.sk-h3.title
|
||||
%span.sk-bold Views
|
||||
.sk-button.sk-secondary-contrast.wide{"ng-click" => "ctrl.clickedAddNewTag()", "title" => "Create a new tag"}
|
||||
.sk-label +
|
||||
.sk-label
|
||||
%i.icon.ion-plus.add-button
|
||||
|
||||
.scrollable
|
||||
.infinite-scroll
|
||||
.tag{"ng-repeat" => "tag in ctrl.smartTags", "ng-click" => "ctrl.selectTag(tag)",
|
||||
"ng-class" => "{'selected' : ctrl.selectedTag == tag, 'faded' : !tag.content.isAllTag}"}
|
||||
.info
|
||||
.tag-info
|
||||
%input.title{"ng-disabled" => "true", "ng-model" => "tag.title"}
|
||||
.count{"ng-show" => "tag.content.isAllTag"} {{tag.cachedNoteCount}}
|
||||
|
||||
@@ -24,14 +25,14 @@
|
||||
.sk-h3.title
|
||||
%span.sk-bold Tags
|
||||
.tag{"ng-repeat" => "tag in ctrl.tags track by tag.uuid", "ng-click" => "ctrl.selectTag(tag)", "ng-class" => "{'selected' : ctrl.selectedTag == tag}"}
|
||||
.info
|
||||
%input.title{"ng-attr-id" => "tag-{{tag.uuid}}", "ng-click" => "ctrl.selectTag(tag)", "ng-model" => "tag.title",
|
||||
.tag-info
|
||||
%input.title{"ng-class" => "{'editing' : ctrl.editingTag == tag}", "ng-attr-id" => "tag-{{tag.uuid}}", "ng-click" => "ctrl.selectTag(tag)", "ng-model" => "tag.title",
|
||||
"ng-keyup" => "$event.keyCode == 13 && $event.target.blur()", "sn-autofocus" => "true", "should-focus" => "ctrl.newTag || ctrl.editingTag == tag",
|
||||
"ng-change" => "ctrl.tagTitleDidChange(tag)", "ng-blur" => "ctrl.saveTag($event, tag)", "spellcheck" => "false"}
|
||||
.count {{tag.cachedNoteCount}}
|
||||
|
||||
.red.small-text.bold{"ng-show" => "tag.content.conflict_of"} Conflicted copy
|
||||
.red.small-text.bold{"ng-show" => "tag.errorDecrypting"} Unable to Decrypt
|
||||
.red.small-text.bold{"ng-show" => "tag.content.conflict_of"} Conflicted Copy
|
||||
.red.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
|
||||
|
||||
BIN
dist/assets/ionicons.eot
vendored
BIN
dist/assets/ionicons.eot
vendored
Binary file not shown.
32
dist/assets/ionicons.svg
vendored
32
dist/assets/ionicons.svg
vendored
@@ -1,13 +1,13 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<!--
|
||||
2018-12-13: Created with FontForge (http://fontforge.org)
|
||||
2019-4-11: Created with FontForge (http://fontforge.org)
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
|
||||
<metadata>
|
||||
Created by FontForge 20170925 at Thu Dec 13 12:46:07 2018
|
||||
By mo
|
||||
Copyright (c) 2018, mo
|
||||
Created by FontForge 20190318 at Thu Apr 11 10:47:26 2019
|
||||
By Mo Bitar
|
||||
Copyright (c) 2019, Mo Bitar
|
||||
</metadata>
|
||||
<defs>
|
||||
<font id="Ionicons" horiz-adv-x="384" >
|
||||
@@ -16,35 +16,21 @@ Copyright (c) 2018, mo
|
||||
font-weight="400"
|
||||
font-stretch="normal"
|
||||
units-per-em="512"
|
||||
panose-1="2 0 5 3 0 0 0 0 0 0"
|
||||
panose-1="2 0 5 9 0 0 0 0 0 0"
|
||||
ascent="448"
|
||||
descent="-64"
|
||||
bbox="0 -64 448 448"
|
||||
bbox="0 -32 384 416"
|
||||
underline-thickness="25.6"
|
||||
underline-position="-51.2"
|
||||
unicode-range="U+F200-F4D4"
|
||||
unicode-range="U+F200-F266"
|
||||
/>
|
||||
<missing-glyph />
|
||||
<glyph glyph-name="ion-ios-box" unicode="" horiz-adv-x="320"
|
||||
d="M16 48v192h288v-192h-288zM128 208c-9 0 -16 -7 -16 -16s7 -16 16 -16h64c9 0 16 7 16 16s-7 16 -16 16h-64zM0 336h320v-80h-16h-288h-16v80z" />
|
||||
<glyph glyph-name="ion-lock-combination" unicode=""
|
||||
d="M320 271c39 -35 64 -86 64 -143c0 -106 -86 -192 -192 -192s-192 86 -192 192c0 57 25 108 64 143v49c0 71 57 128 128 128s128 -57 128 -128v-49zM96 320v-26c28 16 61 26 96 26s68 -10 96 -26v26c0 53 -43 96 -96 96s-96 -43 -96 -96zM192 -32c88 0 160 72 160 160
|
||||
s-72 160 -160 160s-160 -72 -160 -160s72 -160 160 -160zM192 272c80 0 144 -64 144 -144s-64 -144 -144 -144s-144 64 -144 144s64 144 144 144zM315 95c2 9 4 18 4 29h-7v7h8c0 11 -2 20 -5 30l-25 -7l-4 12l25 8c-4 10 -8 18 -14 26l-11 -8l-4 6l11 8c-6 8 -13 16 -21 22
|
||||
l-15 -21l-11 8l15 20c-8 6 -17 11 -27 14l-4 -13l-6 2l5 13c-9 3 -20 5 -29 5v-13v-6h-16v6v13c-11 -1 -19 -2 -29 -5l4 -12l-6 -2l-4 12c-10 -3 -19 -8 -27 -14l15 -20l-10 -8l-15 21c-8 -6 -16 -14 -22 -22l11 -8l-4 -5l-10 8c-6 -8 -10 -17 -14 -27l24 -8l-5 -12l-25 7
|
||||
c-3 -10 -3 -19 -3 -30h6v-7h-5c0 -11 2 -20 4 -29l24 7l4 -12l-24 -8c4 -10 8 -19 14 -27l9 7l4 -5l-9 -7c6 -8 13 -16 21 -22l15 21l10 -8l-15 -20c8 -5 17 -11 27 -14l3 11l7 -2l-4 -11c10 -3 18 -4 29 -5v13v6h16v-6v-13c9 1 19 2 29 5l-5 12l6 2l4 -12c10 3 19 8 27 14
|
||||
l-15 20l11 8l15 -21c8 6 15 14 21 22l-11 7l4 6l10 -8c6 8 11 17 15 27l-25 8l4 12zM111 128c0 45 36 81 81 81s81 -36 81 -81s-36 -81 -81 -81s-81 36 -81 81z" />
|
||||
<glyph glyph-name="ion-arrow-return-right" unicode=""
|
||||
d="M384 256l-128 -96v64h-192v-128h248c4 0 8 -4 8 -8v-48c0 -4 -4 -8 -8 -8h-304c-4 0 -8 4 -8 8v240c0 4 4 8 8 8h248v64z" />
|
||||
<glyph glyph-name="ion-key" unicode="" horiz-adv-x="192"
|
||||
d="M144 147c28 -17 48 -48 48 -83c0 -53 -43 -96 -96 -96s-96 43 -96 96c0 36 19 67 48 84c0 0 7 16 12 41c0 4 11 6 11 12v20c0 5 -7 9 -7 11v8v8c0 2 0 4 1 6c0 1 1 1 1 2l2 1l4 4v1c2 2 3 4 3 6c0 1 1 8 1 9c0 3 -2 6 -4 8l-1 1l-4 4v0l-1 1v0c-2 2 -3 5 -3 8v7
|
||||
c0 3 2 7 4 9v0l5 5v0c2 2 4 3 4 6v24c0 3 -2 6 -4 8v1l-4 4l-1 1c-2 2 -3 5 -3 8v18c0 11 0 19 9 23c3 1 14 3 23 3c20 0 30 -8 32 -36c0 0 7 -81 9 -143s7 -90 7 -90zM96 0c18 0 32 14 32 32s-14 32 -32 32s-32 -14 -32 -32s14 -32 32 -32z" />
|
||||
<glyph glyph-name="ion-eye-disabled" unicode="" horiz-adv-x="448"
|
||||
d="M344 290c41 -27 76 -66 104 -100c-51 -54 -124 -135 -224 -135c-36 0 -65 8 -92 22l-77 -77l-23 23l71 71c-36 25 -68 61 -103 98c78 85 142 137 224 137c34 0 64 -9 92 -23l77 78l23 -23zM134 192c0 -19 6 -36 16 -51l27 27c-4 7 -5 15 -5 24c0 29 23 53 52 53h8
|
||||
c-5 -6 -8 -13 -8 -21c0 -3 0 -5 1 -8l51 50c-15 11 -33 17 -52 17c-49 0 -90 -41 -90 -91zM224 101c49 0 90 41 90 91c0 19 -6 37 -16 52l-50 -51c3 -1 5 -1 8 -1c8 0 14 3 20 7v-7c0 -29 -23 -53 -52 -53c-9 0 -17 2 -24 6l-28 -27c15 -11 33 -17 52 -17z" />
|
||||
<glyph glyph-name="ion-arrow-return-left" unicode=""
|
||||
d="M128 352v-64h248c4 0 8 -4 8 -8v-240c0 -4 -4 -8 -8 -8h-304c-4 0 -8 4 -8 8v48c0 4 4 8 8 8h248v128h-192v-64l-128 96z" />
|
||||
<glyph glyph-name="ion-bookmark" unicode="" horiz-adv-x="192"
|
||||
d="M184 384c4 0 8 -4 8 -8v-56h-192v56c0 4 4 8 8 8h176zM0 0v304h192v-304l-96 96z" />
|
||||
<glyph glyph-name="ion-plus" unicode=""
|
||||
d="M384 224v-64h-160v-160h-64v160h-160v64h160v160h64v-160h160z" />
|
||||
<glyph glyph-name="ion-locked" unicode=""
|
||||
d="M22 -32c-12 0 -22 10 -22 22v212c0 12 10 22 22 22h3h19v31c0 42 17 87 43 115s64 46 105 46v0v0c41 0 79 -18 105 -46s43 -73 43 -115v-31h22c12 0 22 -10 22 -22v-212c0 -12 -10 -22 -22 -22h-340zM97 255v-31h17h155h18v31c0 27 -10 61 -28 80v0v1
|
||||
c-18 19 -42 29 -67 29v0v0c-25 0 -49 -10 -67 -29v-1v0c-18 -19 -28 -53 -28 -80z" />
|
||||
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 1.7 KiB |
BIN
dist/assets/ionicons.ttf
vendored
BIN
dist/assets/ionicons.ttf
vendored
Binary file not shown.
BIN
dist/assets/ionicons.woff
vendored
BIN
dist/assets/ionicons.woff
vendored
Binary file not shown.
1980
package-lock.json
generated
1980
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "standard-notes-web",
|
||||
"version": "3.0.7",
|
||||
"version": "3.0.8",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -38,7 +38,7 @@
|
||||
"serve-static": "^1.13.2",
|
||||
"sn-models": "0.1.14",
|
||||
"sn-stylekit": "2.0.13",
|
||||
"standard-file-js": "0.3.54",
|
||||
"standard-file-js": "0.3.57",
|
||||
"grunt-shell": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,7 +482,7 @@ describe("syncing", () => {
|
||||
})
|
||||
}
|
||||
|
||||
it.only('syncing a note should collapse its properties into the content object after sync', async () => {
|
||||
it('syncing a note should collapse its properties into the content object after sync', async () => {
|
||||
let note = new SNNote();
|
||||
note.title = "Foo";
|
||||
note.setDirty(true);
|
||||
|
||||
Reference in New Issue
Block a user