diff --git a/app/assets/javascripts/app/app.frontend.js b/app/assets/javascripts/app/app.frontend.js
index f19dfe4fd..9563cc6fe 100644
--- a/app/assets/javascripts/app/app.frontend.js
+++ b/app/assets/javascripts/app/app.frontend.js
@@ -20,9 +20,6 @@ angular.module('app.frontend', [
.config(function (RestangularProvider, apiControllerProvider) {
RestangularProvider.setDefaultHeaders({"Content-Type": "application/json"});
- var url = apiControllerProvider.defaultServerURL();
- RestangularProvider.setBaseUrl(url + "/api");
-
RestangularProvider.setFullRequestInterceptor(function(element, operation, route, url, headers, params, httpConfig) {
var token = localStorage.getItem("jwt");
if(token) {
diff --git a/app/assets/javascripts/app/frontend/controllers/header.js b/app/assets/javascripts/app/frontend/controllers/header.js
index 2d8164a83..f0f3975eb 100644
--- a/app/assets/javascripts/app/frontend/controllers/header.js
+++ b/app/assets/javascripts/app/frontend/controllers/header.js
@@ -1,5 +1,5 @@
angular.module('app.frontend')
- .directive("header", function(apiController, extensionManager){
+ .directive("header", function(apiController){
return {
restrict: 'E',
scope: {},
@@ -16,15 +16,9 @@ angular.module('app.frontend')
}
}
})
- .controller('HeaderCtrl', function ($state, apiController, modelManager, $timeout, extensionManager, dbManager) {
+ .controller('HeaderCtrl', function (apiController, modelManager, $timeout, dbManager) {
this.user = apiController.user;
- this.extensionManager = extensionManager;
- this.loginData = {mergeLocal: true};
-
- this.changePasswordPressed = function() {
- this.showNewPasswordForm = !this.showNewPasswordForm;
- }
this.accountMenuPressed = function() {
this.serverData = {url: apiController.getServer()};
@@ -32,126 +26,19 @@ angular.module('app.frontend')
this.showFaq = false;
this.showNewPasswordForm = false;
this.showExtensionsMenu = false;
+ this.showIOMenu = false;
}
this.toggleExtensions = function() {
this.showAccountMenu = false;
+ this.showIOMenu = false;
this.showExtensionsMenu = !this.showExtensionsMenu;
}
- this.toggleExtensionForm = function() {
- this.newExtensionData = {};
- this.showNewExtensionForm = !this.showNewExtensionForm;
- }
-
- this.submitNewExtensionForm = function() {
- if(this.newExtensionData.url) {
- extensionManager.addExtension(this.newExtensionData.url, function(response){
- if(!response) {
- alert("Unable to register this extension. Make sure the link is valid and try again.");
- } else {
- this.newExtensionData.url = "";
- this.showNewExtensionForm = false;
- }
- }.bind(this))
- }
- }
-
- this.selectedAction = function(action, extension) {
- action.running = true;
- extensionManager.executeAction(action, extension, null, function(response){
- action.running = false;
- if(response && response.error) {
- action.error = true;
- alert("There was an error performing this action. Please try again.");
- } else {
- action.error = false;
- apiController.sync(null);
- }
- })
- }
-
- this.syncProviderActionIsEnabled = function(action) {
- var provider = apiController.syncProviderForURL(action.url);
- if(!provider) {
- return null;
- }
- return provider.status;
- }
-
- this.enableSyncProvider = function(action, extension, primary) {
- if(extension.encrypted && !extension.ek) {
- alert("You must set an encryption key for this extension before enabling this action.");
- return;
- }
- var provider = apiController.findOrCreateSyncProviderForUrl(action.url);
- provider.primary = primary;
- provider.enabled = true;
- provider.ek = extension.ek;
- apiController.addSyncProvider(provider);
- }
-
- this.disableSyncProvider = function(action, extension) {
- apiController.removeSyncProvider(apiController.syncProviderForURL(action.url));
- }
-
- this.deleteExtension = function(extension) {
- if(confirm("Are you sure you want to delete this extension?")) {
- extensionManager.deleteExtension(extension);
- var syncProviderAction = extension.syncProviderAction;
- if(syncProviderAction) {
- apiController.removeSyncProvider(apiController.syncProviderForURL(syncProviderAction.url));
- }
- }
- }
-
- this.reloadExtensionsPressed = function() {
- if(confirm("For your security, reloading extensions will disable any currently enabled sync providers and repeat actions.")) {
- extensionManager.refreshExtensionsFromServer();
- var syncProviderAction = extension.syncProviderAction;
- if(syncProviderAction) {
- apiController.removeSyncProvider(apiController.syncProviderForURL(syncProviderAction.url));
- }
- }
- }
-
- this.changeServer = function() {
- apiController.setServer(this.serverData.url, true);
- }
-
- this.signOutPressed = function() {
+ this.toggleIO = function() {
+ this.showIOMenu = !this.showIOMenu;
+ this.showExtensionsMenu = false;
this.showAccountMenu = false;
- apiController.signout(function(){
- window.location.reload();
- })
- }
-
- this.submitPasswordChange = function() {
- this.passwordChangeData.status = "Generating New Keys...";
-
- $timeout(function(){
- if(data.password != data.password_confirmation) {
- alert("Your new password does not match its confirmation.");
- return;
- }
-
- apiController.changePassword(this.passwordChangeData.current_password, this.passwordChangeData.new_password, function(response){
-
- })
-
- }.bind(this))
- }
-
- this.localNotesCount = function() {
- return modelManager.filteredNotes.length;
- }
-
- this.mergeLocalChanged = function() {
- if(!this.loginData.mergeLocal) {
- if(!confirm("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?")) {
- this.loginData.mergeLocal = true;
- }
- }
}
this.refreshData = function() {
@@ -171,110 +58,4 @@ angular.module('app.frontend')
this.syncUpdated = function() {
this.lastSyncDate = new Date();
}
-
- this.loginSubmitPressed = function() {
- this.loginData.status = "Generating Login Keys...";
-
- $timeout(function(){
- apiController.login(this.loginData.email, this.loginData.user_password, function(response){
- if(!response || response.error) {
- var error = response ? response.error : {message: "An unknown error occured."}
- this.loginData.status = null;
- if(!response || (response && !response.didDisplayAlert)) {
- alert(error.message);
- }
- } else {
- this.onAuthSuccess(response.user);
- }
- }.bind(this));
- }.bind(this))
- }
-
- this.submitRegistrationForm = function() {
- this.loginData.status = "Generating Account Keys...";
-
- $timeout(function(){
- apiController.register(this.loginData.email, this.loginData.user_password, function(response){
- if(!response || response.error) {
- var error = response ? response.error : {message: "An unknown error occured."}
- this.loginData.status = null;
- alert(error.message);
- } else {
- this.onAuthSuccess(response.user);
- }
- }.bind(this));
- }.bind(this))
- }
-
- this.encryptionStatusForNotes = function() {
- var allNotes = modelManager.filteredNotes;
- return allNotes.length + "/" + allNotes.length + " notes encrypted";
- }
-
- this.archiveEncryptionFormat = {encrypted: true};
-
- this.downloadDataArchive = function() {
- var link = document.createElement('a');
- link.setAttribute('download', 'notes.json');
- link.href = apiController.itemsDataFile(this.archiveEncryptionFormat.encrypted);
- link.click();
- }
-
- this.performImport = function(data, password) {
- this.importData.loading = true;
- // allow loading indicator to come up with timeout
- $timeout(function(){
- apiController.importJSONData(data, password, function(success, response){
- console.log("Import response:", success, response);
- this.importData.loading = false;
- if(success) {
- this.importData = null;
- } else {
- alert("There was an error importing your data. Please try again.");
- }
- }.bind(this))
- }.bind(this))
- }
-
- this.submitImportPassword = function() {
- this.performImport(this.importData.data, this.importData.password);
- }
-
- this.importFileSelected = function(files) {
- this.importData = {};
-
- var file = files[0];
- var reader = new FileReader();
- reader.onload = function(e) {
- var data = JSON.parse(e.target.result);
- $timeout(function(){
- if(data.auth_params) {
- // request password
- this.importData.requestPassword = true;
- this.importData.data = data;
- } else {
- this.performImport(data, null);
- }
- }.bind(this))
- }.bind(this)
-
- reader.readAsText(file);
- }
-
- this.onAuthSuccess = function(user) {
- var block = function(){
- window.location.reload();
- this.showLogin = false;
- this.showRegistration = false;
- }.bind(this);
-
- if(!this.loginData.mergeLocal) {
- dbManager.clearAllItems(function(){
- block();
- });
- } else {
- block();
- }
- }
-
- });
+});
diff --git a/app/assets/javascripts/app/frontend/models/app/extension.js b/app/assets/javascripts/app/frontend/models/app/extension.js
index 2e44cfd09..36f0a8a54 100644
--- a/app/assets/javascripts/app/frontend/models/app/extension.js
+++ b/app/assets/javascripts/app/frontend/models/app/extension.js
@@ -56,7 +56,7 @@ class Extension extends Item {
this.content_type = "Extension";
}
- syncProviderAction() {
+ get syncProviderAction() {
return _.find(this.actions, {sync_provider: true})
}
diff --git a/app/assets/javascripts/app/frontend/models/sync/syncProvider.js b/app/assets/javascripts/app/frontend/models/sync/syncProvider.js
index 115ec20b1..a21bcce8d 100644
--- a/app/assets/javascripts/app/frontend/models/sync/syncProvider.js
+++ b/app/assets/javascripts/app/frontend/models/sync/syncProvider.js
@@ -1,5 +1,6 @@
class SyncProvider {
constructor(obj) {
+ this.encrypted = true;
_.merge(this, obj);
}
@@ -28,8 +29,9 @@ class SyncProvider {
return {
enabled: this.enabled,
url: this.url,
- encrypted: this.encrypted,
- ek: this.ek
+ primary: this.primary,
+ keyName: this.keyName,
+ syncToken: this.syncToken
}
}
diff --git a/app/assets/javascripts/app/services/apiController.js b/app/assets/javascripts/app/services/apiController.js
index f7ea39895..a134a31f5 100644
--- a/app/assets/javascripts/app/services/apiController.js
+++ b/app/assets/javascripts/app/services/apiController.js
@@ -7,24 +7,11 @@ angular.module('app.frontend')
return domain;
}
- var url;
-
- this.defaultServerURL = function() {
- if(!url) {
- url = localStorage.getItem("server");
- if(!url) {
- url = "https://n3.standardnotes.org";
- }
- }
- return url;
+ this.$get = function($rootScope, Restangular, modelManager, dbManager, keyManager, syncManager) {
+ return new ApiController($rootScope, Restangular, modelManager, dbManager, keyManager, syncManager);
}
-
- this.$get = function($rootScope, Restangular, modelManager, dbManager) {
- return new ApiController($rootScope, Restangular, modelManager, dbManager);
- }
-
- function ApiController($rootScope, Restangular, modelManager, dbManager) {
+ function ApiController($rootScope, Restangular, modelManager, dbManager, keyManager, syncManager) {
var userData = localStorage.getItem("user");
if(userData) {
@@ -36,35 +23,15 @@ angular.module('app.frontend')
this.user = {uuid: idData};
}
}
- this.syncToken = localStorage.getItem("syncToken");
-
- /*
- Config
- */
-
- this.getServer = function() {
- if(!url) {
- url = localStorage.getItem("server");
- if(!url) {
- url = "https://n3.standardnotes.org";
- this.setServer(url);
- }
- }
- return url;
- }
-
- this.setServer = function(url, refresh) {
- localStorage.setItem("server", url);
- if(refresh) {
- window.location.reload();
- }
- }
-
/*
Auth
*/
+ this.defaultServerURL = function() {
+ return localStorage.getItem("server") || "https://n3.standardnotes.org";
+ }
+
this.getAuthParams = function() {
return JSON.parse(localStorage.getItem("auth_params"));
}
@@ -73,8 +40,9 @@ angular.module('app.frontend')
return localStorage.getItem("jwt");
}
- this.getAuthParamsForEmail = function(email, callback) {
- var request = Restangular.one("auth", "params");
+ this.getAuthParamsForEmail = function(url, email, callback) {
+ var requestUrl = url + "/auth/params";
+ var request = Restangular.oneUrl(requestUrl, requestUrl);
request.get({email: email}).then(function(response){
callback(response.plain());
})
@@ -96,10 +64,10 @@ angular.module('app.frontend')
}
}
- this.login = function(email, password, callback) {
- this.getAuthParamsForEmail(email, function(authParams){
+ this.login = function(url, email, password, callback) {
+ this.getAuthParamsForEmail(url, email, function(authParams){
if(!authParams) {
- callback(null);
+ callback({error: "Unable to get authentication parameters."});
return;
}
@@ -113,36 +81,44 @@ angular.module('app.frontend')
}
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, authParams), function(keys){
- this.setMk(keys.mk);
- var request = Restangular.one("auth/sign_in");
+ var mk = keys.mk;
+ var requestUrl = url + "/auth/sign_in";
+ var request = Restangular.oneUrl(requestUrl, requestUrl);
var params = {password: keys.pw, email: email};
_.merge(request, params);
request.post().then(function(response){
+ localStorage.setItem("server", url);
localStorage.setItem("jwt", response.token);
localStorage.setItem("user", JSON.stringify(response.user));
localStorage.setItem("auth_params", JSON.stringify(authParams));
+ keyManager.addKey(SNKeyName, mk);
+ this.addStandardFileSyncProvider(url);
callback(response);
- })
+ }.bind(this))
.catch(function(response){
+ console.log("Error logging in", response);
callback(response.data);
})
}.bind(this));
}.bind(this))
}
- this.register = function(email, password, callback) {
+ this.register = function(url, email, password, callback) {
Neeto.crypto.generateInitialEncryptionKeysForUser({password: password, email: email}, function(keys, authParams){
- this.setMk(keys.mk);
- keys.mk = null;
- var request = Restangular.one("auth");
+ var mk = keys.mk;
+ var requestUrl = url + "/auth";
+ var request = Restangular.oneUrl(requestUrl, requestUrl);
var params = _.merge({password: keys.pw, email: email}, authParams);
_.merge(request, params);
request.post().then(function(response){
+ localStorage.setItem("server", url);
localStorage.setItem("jwt", response.token);
localStorage.setItem("user", JSON.stringify(response.user));
localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"])));
+ keyManager.addKey(SNKeyName, mk);
+ this.addStandardFileSyncProvider(url);
callback(response);
- })
+ }.bind(this))
.catch(function(response){
callback(response.data);
})
@@ -190,8 +166,9 @@ angular.module('app.frontend')
// }.bind(this));
// }
- this._performPasswordChange = function(email, current_keys, new_keys, callback) {
- var request = Restangular.one("auth");
+ this._performPasswordChange = function(url, email, current_keys, new_keys, callback) {
+ var requestUrl = url + "/auth";
+ var request = Restangular.oneUrl(requestUrl, requestUrl);
var params = {password: new_keys.pw, password_confirmation: new_keys.pw, current_password: current_keys.pw, email: email};
_.merge(request, params);
request.patch().then(function(response){
@@ -204,73 +181,6 @@ angular.module('app.frontend')
Sync
*/
- this.syncProviderForURL = function(url) {
- return _.find(this.syncProviders, {url: url});
- }
-
- this.findOrCreateSyncProviderForUrl = function(url) {
- var provider = _.find(this.syncProviders, {url: url});
- if(!provider) {
- provider = new SyncProvider({url: url})
- }
- return provider;
- }
-
- this.setEncryptionStatusForProviderURL = function(providerURL, encrypted) {
- this.providerForURL(providerURL).encrypted = encrypted;
- this.persistSyncProviders();
- }
-
- this.loadSyncProviders = function() {
- var providers = [];
- var saved = localStorage.getItem("syncProviders");
- if(saved) {
- var parsed = JSON.parse(saved);
- for(var p of parsed) {
- providers.push(new SyncProvider(p));
- }
- } else {
- // no providers saved, use default
- if(this.isUserSignedIn()) {
- var defaultProvider = new SyncProvider(this.getServer() + "/items/sync", true);
- providers.push(defaultProvider);
- }
- }
-
- this.syncProviders = providers;
- }
- this.loadSyncProviders();
-
- this.addSyncProvider = function(syncProvider) {
- if(syncProvider.primary) {
- for(var provider of this.syncProviders) {
- provider.primary = false;
- }
- }
-
- // since we're adding a new provider, we need to send it EVERYTHING we have now.
- syncProvider.addPendingItems(modelManager.allItems);
-
- this.syncProviders.push(syncProvider);
- this.persistSyncProviders();
- }
-
- this.removeSyncProvider = function(provider) {
- _.pull(this.syncProviders, provider);
- this.persistSyncProviders();
- }
-
- this.persistSyncProviders = function() {
- localStorage.setItem("syncProviders", JSON.stringify(_.map(this.syncProviders, function(provider) {
- return provider.asJSON()
- })));
- }
-
- this.setSyncToken = function(syncToken) {
- this.syncToken = syncToken;
- localStorage.setItem("syncToken", this.syncToken);
- }
-
this.syncWithOptions = function(callback, options = {}) {
var allDirtyItems = modelManager.getDirtyItems();
@@ -306,7 +216,7 @@ angular.module('app.frontend')
}
for(let provider of this.syncProviders) {
- if(provider.enabled == false) {
+ if(!provider.enabled) {
continue;
}
provider.addPendingItems(allDirtyItems);
@@ -354,18 +264,17 @@ angular.module('app.frontend')
return this.paramsForItem(item, provider.encrypted, provider.ek, options.additionalFields, false);
}.bind(this));
- if(provider.primary) {
- // only primary providers receive items (or care about received items)
- request.sync_token = this.syncToken;
- request.cursor_token = provider.cursorToken;
- }
+ request.sync_token = provider.syncToken;
+ request.cursor_token = provider.cursorToken;
request.post().then(function(response) {
+ console.log("Sync completion", response);
+
+ provider.syncToken = response.sync_token;
+
if(provider.primary) {
- // handle sync token
- this.setSyncToken(response.sync_token);
- $rootScope.$broadcast("sync:updated_token", this.syncToken);
+ $rootScope.$broadcast("sync:updated_token", provider.syncToken);
// handle cursor token (more results waiting, perform another sync)
provider.cursorToken = response.cursor_token;
@@ -382,6 +291,7 @@ angular.module('app.frontend')
}
provider.syncOpInProgress = false;
+ this.didMakeChangesToSyncProviders();
if(provider.cursorToken || provider.repeatOnCompletion == true) {
this.__performSyncWithProvider(provider, options, callback);
@@ -434,12 +344,13 @@ angular.module('app.frontend')
}
this.handleItemsResponse = function(responseItems, omitFields, syncProvider) {
- this.decryptItemsWithKey(responseItems, syncProvider ? syncProvider.ek : null);
+ var ek = syncProvider ? keyManager.keyForName(syncProvider.keyName).key : null;
+ this.decryptItemsWithKey(responseItems, ek);
return modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields);
}
this.paramsForExportFile = function(item, ek, encrypted) {
- return _.omit(this.paramsForItem(item, encrypted, ["created_at", "updated_at"], true), ["deleted"]);
+ return _.omit(this.paramsForItem(item, encrypted, ek, ["created_at", "updated_at"], true), ["deleted"]);
}
this.paramsForExtension = function(item, encrypted) {
@@ -482,8 +393,10 @@ angular.module('app.frontend')
*/
this.clearSyncToken = function() {
- this.syncToken = null;
- localStorage.removeItem("syncToken");
+ var primary = this.primarySyncProvider();
+ if(primary) {
+ primary.syncToken = null;
+ }
}
this.importJSONData = function(data, password, callback) {
@@ -526,7 +439,7 @@ angular.module('app.frontend')
Export
*/
- this.itemsDataFile = function(encrypted) {
+ this.itemsDataFile = function(encrypted, custom_ek) {
var textFile = null;
var makeTextFile = function (text) {
var data = new Blob([text], {type: 'text/json'});
@@ -543,15 +456,21 @@ angular.module('app.frontend')
return textFile;
}.bind(this);
+ var ek = custom_ek;
+ if(encrypted && !custom_ek) {
+ ek = this.retrieveMk();
+ }
+
var items = _.map(modelManager.allItemsMatchingTypes(["Tag", "Note"]), function(item){
- return this.paramsForExportFile(item, encrypted);
+ return this.paramsForExportFile(item, ek, encrypted);
}.bind(this));
var data = {
items: items
}
- if(encrypted) {
+ if(encrypted && !custom_ek) {
+ // auth params are only needed when encrypted with a standard file key
data["auth_params"] = this.getAuthParams();
}
@@ -616,7 +535,8 @@ angular.module('app.frontend')
localStorage.setItem('mk', mk);
}
- this.signout = function(callback) {
+ this.signoutOfStandardFile = function(callback) {
+ this.removeStandardFileSyncProvider();
dbManager.clearAllItems(function(){
localStorage.clear();
callback();
diff --git a/app/assets/javascripts/app/services/directives/accountDataMenu.js b/app/assets/javascripts/app/services/directives/accountDataMenu.js
new file mode 100644
index 000000000..91a032c2a
--- /dev/null
+++ b/app/assets/javascripts/app/services/directives/accountDataMenu.js
@@ -0,0 +1,17 @@
+class AccountDataMenu {
+
+ constructor() {
+ this.restrict = "E";
+ this.templateUrl = "frontend/directives/account-data-menu.html";
+ this.scope = {
+ };
+ }
+
+ controller($scope, apiController, modelManager) {
+ 'ngInject';
+
+
+ }
+}
+
+angular.module('app.frontend').directive('accountDataMenu', () => new AccountDataMenu);
diff --git a/app/assets/javascripts/app/services/directives/accountKeysSection.js b/app/assets/javascripts/app/services/directives/accountKeysSection.js
new file mode 100644
index 000000000..f1d30d1fe
--- /dev/null
+++ b/app/assets/javascripts/app/services/directives/accountKeysSection.js
@@ -0,0 +1,23 @@
+class AccountKeysSection {
+
+ constructor() {
+ this.restrict = "E";
+ this.templateUrl = "frontend/directives/account-keys-section.html";
+ this.scope = {
+ };
+ }
+
+ controller($scope, apiController, keyManager) {
+ 'ngInject';
+
+ $scope.newKeyData = {};
+ $scope.keys = keyManager.keys;
+
+ $scope.submitNewKeyForm = function() {
+ keyManager.addKey($scope.newKeyData.name, $scope.newKeyData.key);
+ $scope.newKeyData.showForm = false;
+ }
+ }
+}
+
+angular.module('app.frontend').directive('accountKeysSection', () => new AccountKeysSection);
diff --git a/app/assets/javascripts/app/services/directives/accountSyncSection.js b/app/assets/javascripts/app/services/directives/accountSyncSection.js
new file mode 100644
index 000000000..e2c5fb08d
--- /dev/null
+++ b/app/assets/javascripts/app/services/directives/accountSyncSection.js
@@ -0,0 +1,36 @@
+class AccountSyncSection {
+
+ constructor() {
+ this.restrict = "E";
+ this.templateUrl = "frontend/directives/account-sync-section.html";
+ this.scope = {
+ };
+ }
+
+ controller($scope, apiController, modelManager, keyManager) {
+ 'ngInject';
+
+ $scope.syncProviders = apiController.syncProviders;
+ $scope.newSyncData = {showAddSyncForm: false}
+ $scope.keys = keyManager.keys;
+
+ $scope.submitExternalSyncURL = function() {
+ apiController.addSyncProviderFromURL($scope.newSyncData.url);
+ $scope.newSyncData.showAddSyncForm = false;
+ }
+
+ $scope.enableSyncProvider = function(provider, primary) {
+ if(!provider.keyName) {
+ alert("You must choose an encryption key for this provider before enabling it.");
+ return;
+ }
+ apiController.enableSyncProvider(provider, primary);
+ }
+
+ $scope.removeSyncProvider = function(provider) {
+ apiController.removeSyncProvider(provider);
+ }
+ }
+}
+
+angular.module('app.frontend').directive('accountSyncSection', () => new AccountSyncSection);
diff --git a/app/assets/javascripts/app/services/directives/accountVendorAccountSection.js b/app/assets/javascripts/app/services/directives/accountVendorAccountSection.js
new file mode 100644
index 000000000..0b73f6c5e
--- /dev/null
+++ b/app/assets/javascripts/app/services/directives/accountVendorAccountSection.js
@@ -0,0 +1,113 @@
+class AccountVendorAccountSection {
+
+ constructor() {
+ this.restrict = "E";
+ this.templateUrl = "frontend/directives/account-vendor-account-section.html";
+ this.scope = {
+ };
+ }
+
+ controller($scope, apiController, modelManager, $timeout, dbManager) {
+ 'ngInject';
+
+ $scope.loginData = {mergeLocal: true, url: apiController.defaultServerURL()};
+ $scope.user = apiController.user;
+
+ $scope.changePasswordPressed = function() {
+ $scope.showNewPasswordForm = !$scope.showNewPasswordForm;
+ }
+
+ $scope.signOutPressed = function() {
+ $scope.showAccountMenu = false;
+ apiController.signoutOfStandardFile(function(){
+ window.location.reload();
+ })
+ }
+
+ $scope.submitPasswordChange = function() {
+ $scope.passwordChangeData.status = "Generating New Keys...";
+
+ $timeout(function(){
+ if(data.password != data.password_confirmation) {
+ alert("Your new password does not match its confirmation.");
+ return;
+ }
+
+ apiController.changePassword($scope.passwordChangeData.current_password, $scope.passwordChangeData.new_password, function(response){
+
+ })
+
+ })
+ }
+
+ $scope.localNotesCount = function() {
+ return modelManager.filteredNotes.length;
+ }
+
+ $scope.mergeLocalChanged = function() {
+ if(!$scope.loginData.mergeLocal) {
+ if(!confirm("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?")) {
+ $scope.loginData.mergeLocal = true;
+ }
+ }
+ }
+
+ $scope.loginSubmitPressed = function() {
+ $scope.loginData.status = "Generating Login Keys...";
+ console.log("logging in with url", $scope.loginData.url);
+ $timeout(function(){
+ apiController.login($scope.loginData.url, $scope.loginData.email, $scope.loginData.user_password, function(response){
+ if(!response || response.error) {
+ var error = response ? response.error : {message: "An unknown error occured."}
+ $scope.loginData.status = null;
+ if(!response || (response && !response.didDisplayAlert)) {
+ alert(error.message);
+ }
+ } else {
+ $scope.onAuthSuccess(response.user);
+ }
+ });
+ })
+ }
+
+ $scope.submitRegistrationForm = function() {
+ $scope.loginData.status = "Generating Account Keys...";
+
+ $timeout(function(){
+ apiController.register($scope.loginData.url, $scope.loginData.email, $scope.loginData.user_password, function(response){
+ if(!response || response.error) {
+ var error = response ? response.error : {message: "An unknown error occured."}
+ $scope.loginData.status = null;
+ alert(error.message);
+ } else {
+ $scope.onAuthSuccess(response.user);
+ }
+ });
+ })
+ }
+
+ $scope.encryptionStatusForNotes = function() {
+ var allNotes = modelManager.filteredNotes;
+ return allNotes.length + "/" + allNotes.length + " notes encrypted";
+ }
+
+ $scope.onAuthSuccess = function(user) {
+ var block = function(){
+ window.location.reload();
+ $scope.showLogin = false;
+ $scope.showRegistration = false;
+ };
+
+ if(!$scope.loginData.mergeLocal) {
+ dbManager.clearAllItems(function(){
+ block();
+ });
+ } else {
+ block();
+ }
+ }
+
+ }
+}
+
+angular.module('app.frontend').directive('accountVendorAccountSection', () => new AccountVendorAccountSection);
diff --git a/app/assets/javascripts/app/services/directives/globalExtensionsMenu.js b/app/assets/javascripts/app/services/directives/globalExtensionsMenu.js
new file mode 100644
index 000000000..579471118
--- /dev/null
+++ b/app/assets/javascripts/app/services/directives/globalExtensionsMenu.js
@@ -0,0 +1,109 @@
+class GlobalExtensionsMenu {
+
+ constructor() {
+ this.restrict = "E";
+ this.templateUrl = "frontend/directives/global-extensions-menu.html";
+ this.scope = {
+ };
+ }
+
+ controller($scope, apiController, extensionManager) {
+ 'ngInject';
+
+ $scope.extensionManager = extensionManager;
+
+ $scope.toggleExtensionForm = function() {
+ $scope.newExtensionData = {};
+ $scope.showNewExtensionForm = !$scope.showNewExtensionForm;
+ }
+
+ $scope.submitNewExtensionForm = function() {
+ if($scope.newExtensionData.url) {
+ extensionManager.addExtension($scope.newExtensionData.url, function(response){
+ if(!response) {
+ alert("Unable to register this extension. Make sure the link is valid and try again.");
+ } else {
+ $scope.newExtensionData.url = "";
+ $scope.showNewExtensionForm = false;
+ }
+ })
+ }
+ }
+
+ $scope.selectedAction = function(action, extension) {
+ action.running = true;
+ extensionManager.executeAction(action, extension, null, function(response){
+ action.running = false;
+ if(response && response.error) {
+ action.error = true;
+ alert("There was an error performing this action. Please try again.");
+ } else {
+ action.error = false;
+ apiController.sync(null);
+ }
+ })
+ }
+
+ $scope.changeExtensionEncryptionFormat = function(encrypted, extension) {
+ var provider = $scope.syncProviderForExtension(extension);
+ if(provider) {
+ if(confirm("Changing encryption status will update all your items and re-sync them back to the server. This can take several minutes. Are you sure you want to continue?")) {
+ extensionManager.changeExtensionEncryptionFormat(encrypted, extension);
+ apiController.resyncAllDataForProvider(provider);
+ } else {
+ // revert setting
+ console.log("reverting");
+ extension.encrypted = extensionManager.extensionUsesEncryptedData(extension);
+ }
+ } else {
+ extensionManager.changeExtensionEncryptionFormat(encrypted, extension);
+ }
+ }
+
+ $scope.deleteExtension = function(extension) {
+ if(confirm("Are you sure you want to delete this extension?")) {
+ extensionManager.deleteExtension(extension);
+ var syncProviderAction = extension.syncProviderAction;
+ if(syncProviderAction) {
+ apiController.removeSyncProvider(apiController.syncProviderForURL(syncProviderAction.url));
+ }
+ }
+ }
+
+ $scope.reloadExtensionsPressed = function() {
+ if(confirm("For your security, reloading extensions will disable any currently enabled sync providers and repeat actions.")) {
+ extensionManager.refreshExtensionsFromServer();
+ var syncProviderAction = extension.syncProviderAction;
+ if(syncProviderAction) {
+ apiController.removeSyncProvider(apiController.syncProviderForURL(syncProviderAction.url));
+ }
+ }
+ }
+
+ $scope.setEncryptionKeyForExtension = function(extension) {
+ extension.formData.changingKey = false;
+ var ek = extension.formData.ek;
+ extensionManager.setEkForExtension(extension, ek);
+ if(extension.formData.changingKey) {
+ var syncAction = extension.syncProviderAction;
+ if(syncAction) {
+ var provider = apiController.syncProviderForURL(syncAction.url);
+ provider.ek = ek;
+ apiController.didMakeChangesToSyncProviders();
+ apiController.resyncAllDataForProvider(provider);
+ }
+ }
+ }
+
+ $scope.changeEncryptionKeyPressed = function(extension) {
+ if(!confirm("Changing your encryption key will re-encrypt all your notes with the new key and sync them back to the server. This can take several minutes. We strongly recommend downloading a backup of your notes before continuing.")) {
+ return;
+ }
+
+ extension.formData.changingKey = true;
+ }
+ }
+
+}
+
+angular.module('app.frontend').directive('globalExtensionsMenu', () => new GlobalExtensionsMenu);
diff --git a/app/assets/javascripts/app/services/directives/importExportMenu.js b/app/assets/javascripts/app/services/directives/importExportMenu.js
new file mode 100644
index 000000000..59654399a
--- /dev/null
+++ b/app/assets/javascripts/app/services/directives/importExportMenu.js
@@ -0,0 +1,79 @@
+class ImportExportMenu {
+
+ constructor() {
+ this.restrict = "E";
+ this.templateUrl = "frontend/directives/import-export-menu.html";
+ this.scope = {
+ };
+ }
+
+ controller($scope, apiController, $timeout) {
+ 'ngInject';
+
+ $scope.archiveFormData = {encryption_type: $scope.user ? 'mk' : 'ek'};
+ $scope.user = apiController.user;
+
+ $scope.downloadDataArchive = function() {
+ if(!apiController.isUserSignedIn() && $scope.archiveFormData.encryption_type == 'ek') {
+ if(!$scope.archiveFormData.ek) {
+ alert("You must set an encryption key to export the data encrypted.")
+ return;
+ }
+ }
+
+ var link = document.createElement('a');
+ link.setAttribute('download', 'notes.json');
+
+ var ek = $scope.archiveFormData.encryption_type == 'ek' ? $scope.archiveFormData.ek : null;
+ var encrypted = $scope.archiveFormData.encryption_type != 'none';
+
+ link.href = apiController.itemsDataFile(encrypted, ek);
+ link.click();
+ }
+
+ $scope.performImport = function(data, password) {
+ $scope.importData.loading = true;
+ // allow loading indicator to come up with timeout
+ $timeout(function(){
+ apiController.importJSONData(data, password, function(success, response){
+ console.log("Import response:", success, response);
+ $scope.importData.loading = false;
+ if(success) {
+ $scope.importData = null;
+ } else {
+ alert("There was an error importing your data. Please try again.");
+ }
+ })
+ })
+ }
+
+ $scope.submitImportPassword = function() {
+ $scope.performImport($scope.importData.data, $scope.importData.password);
+ }
+
+ $scope.importFileSelected = function(files) {
+ $scope.importData = {};
+
+ var file = files[0];
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ var data = JSON.parse(e.target.result);
+ $timeout(function(){
+ if(data.auth_params) {
+ // request password
+ $scope.importData.requestPassword = true;
+ $scope.importData.data = data;
+ } else {
+ $scope.performImport(data, null);
+ }
+ })
+ }
+
+ reader.readAsText(file);
+ }
+
+ }
+
+}
+
+angular.module('app.frontend').directive('importExportMenu', () => new ImportExportMenu);
diff --git a/app/assets/javascripts/app/services/extensionManager.js b/app/assets/javascripts/app/services/extensionManager.js
index 4be4ecff0..28f9710b4 100644
--- a/app/assets/javascripts/app/services/extensionManager.js
+++ b/app/assets/javascripts/app/services/extensionManager.js
@@ -6,6 +6,7 @@ class ExtensionManager {
this.apiController = apiController;
this.enabledRepeatActionUrls = JSON.parse(localStorage.getItem("enabledRepeatActionUrls")) || [];
this.decryptedExtensions = JSON.parse(localStorage.getItem("decryptedExtensions")) || [];
+ this.extensionEks = JSON.parse(localStorage.getItem("extensionEks")) || {};
modelManager.addItemSyncObserver("extensionManager", "Extension", function(items){
for (var ext of items) {
@@ -31,6 +32,15 @@ class ExtensionManager {
})
}
+ ekForExtension(extension) {
+ return this.extensionEks[extension.url];
+ }
+
+ setEkForExtension(extension, ek) {
+ this.extensionEks[extension.url] = ek;
+ localStorage.setItem("extensionEks", JSON.stringify(this.extensionEks));
+ }
+
actionWithURL(url) {
for (var extension of this.extensions) {
return _.find(extension.actions, {url: url})
@@ -42,6 +52,7 @@ class ExtensionManager {
}
changeExtensionEncryptionFormat(encrypted, extension) {
+ console.log("changing encryption status");
if(encrypted) {
_.pull(this.decryptedExtensions, extension.url);
} else {
diff --git a/app/assets/javascripts/app/services/keyManager.js b/app/assets/javascripts/app/services/keyManager.js
new file mode 100644
index 000000000..8a874ecf4
--- /dev/null
+++ b/app/assets/javascripts/app/services/keyManager.js
@@ -0,0 +1,26 @@
+class KeyManager {
+
+ constructor() {
+ this.keys = JSON.parse(localStorage.getItem("keys")) || [];
+ }
+
+ addKey(name, key) {
+ this.keys.push({name: name, key: key});
+ this.persist();
+ }
+
+ keyForName(name) {
+ return _.find(this.keys, {name: name});
+ }
+
+ deleteKey(name) {
+ _.pull(this.keys, {name: name});
+ this.persist();
+ }
+
+ persist() {
+ localStorage.setItem("keys", JSON.stringify(this.keys));
+ }
+}
+
+angular.module('app.frontend').service('keyManager', KeyManager);
diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js
new file mode 100644
index 000000000..bfa5de223
--- /dev/null
+++ b/app/assets/javascripts/app/services/syncManager.js
@@ -0,0 +1,111 @@
+class SyncManager {
+
+ let SNKeyName = "Standard Notes Key";
+
+ constructor(modelManager) {
+ this.modelManager = modelManager;
+ }
+
+ syncProviderForURL(url) {
+ var provider = _.find(this.syncProviders, {url: url});
+ return provider;
+ }
+
+ findOrCreateSyncProviderForUrl(url) {
+ var provider = _.find(this.syncProviders, {url: url});
+ if(!provider) {
+ provider = new SyncProvider({url: url})
+ }
+ return provider;
+ }
+
+ setEncryptionStatusForProviderURL(providerURL, encrypted) {
+ this.providerForURL(providerURL).encrypted = encrypted;
+ this.didMakeChangesToSyncProviders();
+ }
+
+ primarySyncProvider() {
+ return _.find(this.syncProviders, {primary: true});
+ }
+
+ removeStandardFileSyncProvider() {
+ var sfProvider = _.find(this.syncProviders, {url: this.defaultServerURL() + "/items/sync"})
+ _.pull(this.syncProviders, sfProvider);
+ this.didMakeChangesToSyncProviders();
+ }
+
+ addStandardFileSyncProvider(url) {
+ var defaultProvider = new SyncProvider({url: url + "/items/sync", primary: this.syncProviders.length == 0});
+ defaultProvider.keyName = SNKeyName;
+ defaultProvider.enabled = this.syncProviders.length == 0;
+ this.syncProviders.push(defaultProvider);
+ return defaultProvider;
+ }
+
+ didMakeChangesToSyncProviders() {
+ localStorage.setItem("syncProviders", JSON.stringify(_.map(this.syncProviders, function(provider) {
+ return provider.asJSON()
+ })));
+ }
+
+ loadSyncProviders() {
+ this.syncProviders = [];
+ var saved = localStorage.getItem("syncProviders");
+ if(saved) {
+ var parsed = JSON.parse(saved);
+ for(var p of parsed) {
+ this.syncProviders.push(new SyncProvider(p));
+ }
+ } else {
+ // no providers saved, use default
+ if(this.isUserSignedIn()) {
+ var defaultProvider = this.addStandardFileSyncProvider(this.defaultServerURL());
+ defaultProvider.syncToken = localStorage.getItem("syncToken");
+ // migrate old key structure to new
+ var mk = localStorage.getItem("mk");
+ if(mk) {
+ keyManager.addKey(SNKeyName, mk);
+ localStorage.removeItem("mk");
+ }
+ this.didMakeChangesToSyncProviders();
+ }
+ }
+ }
+
+ this.loadSyncProviders();
+
+ addSyncProviderFromURL(url) {
+ var provider = new SyncProvider({url: url});
+ this.syncProviders.push(provider);
+ this.didMakeChangesToSyncProviders();
+ }
+
+ enableSyncProvider(syncProvider, primary) {
+ if(primary) {
+ for(var provider of this.syncProviders) {
+ provider.primary = false;
+ }
+ }
+
+ syncProvider.enabled = true;
+ syncProvider.primary = primary;
+
+ // since we're enabling a new provider, we need to send it EVERYTHING we have now.
+ syncProvider.addPendingItems(this.modelManager.allItems);
+ this.didMakeChangesToSyncProviders();
+ }
+
+ resyncAllDataForProvider(syncProvider) {
+ syncProvider.addPendingItems(this.modelManager.allItems);
+ this.sync();
+ }
+
+ removeSyncProvider(provider) {
+ _.pull(this.syncProviders, provider);
+ this.didMakeChangesToSyncProviders();
+ }
+
+
+}
+
+angular.module('app.frontend').service('syncManager', SyncManager);
diff --git a/app/assets/stylesheets/app/_directives.scss b/app/assets/stylesheets/app/_directives.scss
index 4ca0da65b..6037d8051 100644
--- a/app/assets/stylesheets/app/_directives.scss
+++ b/app/assets/stylesheets/app/_directives.scss
@@ -64,9 +64,207 @@
margin-top: 1px;
font-size: 12px;
}
-
}
-
+ }
+ }
+ }
+}
+
+
+/**
+Extensions
+*/
+
+.extensions-panel {
+ font-size: 14px;
+
+ .extension-link {
+ margin-top: 8px;
+
+ a {
+ color: $blue-color !important;
+ font-weight: bold;
+ }
+ }
+}
+
+.extension-form {
+ margin-top: 8px;
+}
+
+.registered-extensions {
+
+ .extension {
+ margin-bottom: 18px;
+ background-color: #f6f6f6;
+ border: 1px solid #f2f2f2;
+ padding: 14px 6px;
+ padding-bottom: 8px;
+ color: black;
+
+ .ek-input-wrapper {
+ text-align: center;
+ margin-top: 14px;
+ height: 30px;
+
+ > input {
+ height: 100%;
+ border: 1px solid rgba(gray, 0.15);
+ padding: 5px;
+ width: 78%;
+ margin-right: 0;
+ }
+
+ > button {
+ width: 20% !important;
+ height: 100% !important;
+ display: inline-block !important;
+ margin: 0 !important;
+ }
+ }
+
+ a.option-link {
+ margin-top: 6px;
+ display: block;
+ text-align: center;
+ }
+
+ .show-ek {
+ > .ek {
+ font-weight: bold;
+ margin-top: 2px;
+ }
+
+ > .disclaimer {
+ margin-top: 2px;
+ font-size: 10px;
+ font-style: italic;
+ width: 60%;
+ margin-left: auto;
+ margin-right: auto;
+ }
+ }
+
+ a {
+ color: $blue-color !important;
+ font-size: 12px !important;
+ font-weight: bold !important;
+ }
+
+ .extension-name {
+ font-weight: bold;
+ font-size: 16px;
+ margin-bottom: 6px;
+ text-align: center;
+ }
+
+ .encryption-format {
+ margin-top: 4px;
+ font-size: 12px;
+ text-align: center;
+
+ > .title {
+ font-size: 13px;
+ // font-weight: bold;
+ margin-bottom: 2px;
+ }
+ }
+
+ .extension-subtitle {
+ font-size: 14px;
+ margin-bottom: 10px;
+ }
+
+ .extension-actions {
+ margin-top: 15px;
+ font-size: 12px;
+
+ .action {
+ padding: 13px;
+ margin-bottom: 10px;
+ background-color: rgba(white, 0.9);
+ border: 1px solid rgba(gray, 0.15);
+
+ .action-name {
+ font-weight: bold;
+ }
+
+ .action-permissions {
+ margin-top: 2px;
+ a {
+ font-weight: normal !important;
+ }
+ }
+
+ > .execute-type {
+ font-size: 12px;
+ margin-bottom: 1px;
+ }
+
+ > .error {
+ color: red;
+ margin-top: 6px;
+ }
+
+ > .last-run {
+ opacity: 0.5;
+ font-size: 11px;
+ margin-top: 6px;
+ }
+ }
+
+ }
+ }
+}
+
+.account-data-menu {
+ padding: 5px !important;
+
+ .providers {
+ font-size: 12px;
+
+ .provider {
+ background-color: #f6f6f6;
+ border: 1px solid #f2f2f2;
+ padding: 10px 10px;
+ padding-bottom: 8px;
+ margin-bottom: 10px;
+
+ > .type {
+ font-weight: bold;
+ }
+
+ > .url {
+ // white-space: nowrap;
+ // text-overflow: ellipsis;
+ // overflow: hidden;
+ word-wrap: break-word;
+ margin-top: 4px;
+ }
+
+ > .options {
+ margin-top: 10px;
+ }
+ }
+ }
+}
+
+.account-keys-section {
+ .keys {
+ .key {
+ background-color: #f6f6f6;
+ border: 1px solid #f2f2f2;
+ padding: 10px 10px;
+ padding-bottom: 8px;
+
+ > .name {
+ font-size: 13px;
+ font-weight: bold;
+ margin-bottom: 2px;
+ }
+
+ > .value {
+ word-wrap: break-word;
}
}
}
diff --git a/app/assets/stylesheets/app/_header.scss b/app/assets/stylesheets/app/_header.scss
index c888e18d9..6d71631c9 100644
--- a/app/assets/stylesheets/app/_header.scss
+++ b/app/assets/stylesheets/app/_header.scss
@@ -12,6 +12,7 @@
a {
color: $dark-gray;
}
+
}
.header-content {
@@ -50,50 +51,99 @@
.login-panel .login-input {
border-radius: 0px;
}
+}
- .items {
+.items {
- .item {
+ .item {
- display: inline-block;
- margin-right: 7px;
- position: relative;
- cursor: pointer;
- font-weight: bold;
+ display: inline-block;
+ margin-right: 7px;
+ position: relative;
+ cursor: pointer;
+ font-weight: bold;
- a {
- color: #515263;
- }
+ a {
+ color: #515263;
+ }
- .panel {
- position: absolute;
- right: 0px;
- bottom: 20px;
- min-width: 300px;
- z-index: 1000;
- margin-top: 15px;
- box-shadow: 0px 0px 15px rgba(black, 0.2);
- border: none;
- cursor: default;
- max-height: 85vh;
- overflow: auto;
+ .panel {
+ position: absolute;
+ right: 0px;
+ bottom: 20px;
+ min-width: 300px;
+ z-index: 1000;
+ margin-top: 15px;
+ box-shadow: 0px 0px 15px rgba(black, 0.2);
+ border: none;
+ cursor: default;
+ max-height: 85vh;
+ overflow: auto;
+ background-color: white;
+ font-weight: normal;
+
+ section {
+ margin-bottom: 10px;
+ }
+
+ h3 {
+ font-weight: bold;
+ margin-top: 0px;
+ margin-bottom: 4px;
+ font-size: 16px;
+ }
+
+ a {
+ font-size: 12px;
+ color: $blue-color;
+ font-weight: bold;
+ }
+
+ button.light {
+ font-weight: bold;
+ margin-bottom: 0px;
+ font-size: 12px;
+ height: 30px;
+ padding-top: 3px;
+ text-align: center;
+ margin-bottom: 6px;
background-color: white;
- font-weight: normal;
+ display: block;
+ width: 100%;
+ border: 1px solid rgba(gray, 0.15);
+ cursor: pointer;
+ color: $blue-color;
-
- .storage-text {
- font-size: 14px;
+ &:hover {
+ background-color: rgba(gray, 0.10);
}
- .checkbox {
- font-size: 14px;
- font-weight: normal;
+ .execution-spinner {
margin-left: auto;
margin-right: auto;
+ text-align: center;
+ margin-top: 3px;
}
- }
- }
- }
+ }
+
+ .storage-text {
+ font-size: 14px;
+ }
+
+ .checkbox {
+ font-size: 14px;
+ font-weight: normal;
+ margin-left: auto;
+ margin-right: auto;
+ }
+ }
+ }
+}
+
+.item.io {
+ .enc-option {
+ display: block;
+ }
}
.half-button {
@@ -127,11 +177,7 @@
.link-item {
margin-bottom: 8px;
- a {
- font-size: 12px;
- color: $blue-color;
- font-weight: bold;
- }
+
}
input {
@@ -157,17 +203,12 @@
cursor: pointer;
}
- > .icon-container {
- display: block;
- margin-bottom: 10px;
- }
-
- > .meta-container {
+ .meta-container {
display: block;
font-size: 10px;
}
- > .action-container {
+ .action-container {
font-size: 12px;
margin-top: 6px;
@@ -209,16 +250,6 @@
margin-bottom: 8px !important;
}
- > .icon-container {
- margin-bottom: 10px;
- .icon {
- height: 35px;
- &.archive {
- height: 30px;
- }
- }
- }
-
.meta-container {
> .title {
font-size: 13px;
@@ -273,132 +304,3 @@ a.disabled {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
-
-/**
-Extensions
-*/
-
-.extensions-panel {
- font-size: 14px;
-
- .extension-link {
- margin-top: 8px;
-
- a {
- color: $blue-color !important;
- font-weight: bold;
- }
- }
-}
-
-.extension-form {
- margin-top: 8px;
-}
-
-.registered-extensions {
-
-
- .extension {
- margin-bottom: 18px;
- background-color: #f6f6f6;
- border: 1px solid #f2f2f2;
- padding: 14px 6px;
- padding-bottom: 8px;
- color: black;
-
- a {
- color: $blue-color !important;
- font-size: 12px !important;
- font-weight: bold !important;
- }
-
- .extension-name {
- font-weight: bold;
- font-size: 16px;
- margin-bottom: 6px;
- text-align: center;
- }
-
- .encryption-format {
- margin-top: 4px;
- font-size: 12px;
- text-align: center;
-
- > .title {
- font-size: 13px;
- // font-weight: bold;
- margin-bottom: 2px;
- }
- }
-
- .extension-subtitle {
- font-size: 14px;
- margin-bottom: 10px;
- }
-
- .extension-actions {
- margin-top: 15px;
- font-size: 12px;
-
- .action {
- padding: 13px;
- margin-bottom: 10px;
- background-color: rgba(white, 0.9);
- border: 1px solid rgba(gray, 0.15);
-
- .action-name {
- font-weight: bold;
- }
-
- .action-permissions {
- margin-top: 2px;
- a {
- font-weight: normal !important;
- }
- }
-
- .execute {
- font-weight: bold;
- margin-bottom: 0px;
- font-size: 12px;
- height: 30px;
- padding-top: 7px;
- text-align: center;
- margin-top: 6px;
- border: 1px solid rgba(gray, 0.15);
- cursor: pointer;
- color: $blue-color;
-
- &:hover {
- background-color: rgba(gray, 0.10);
- }
-
- .execution-spinner {
- margin-left: auto;
- margin-right: auto;
- text-align: center;
- margin-top: 3px;
- }
- }
-
- > .execute-type {
- font-size: 12px;
- margin-bottom: 1px;
- }
-
- > .error {
- color: red;
- margin-top: 6px;
- }
-
- > .last-run {
- opacity: 0.5;
- font-size: 11px;
- margin-top: 6px;
- }
- }
-
- }
- }
-
-}
diff --git a/app/assets/templates/frontend/directives/account-data-menu.html.haml b/app/assets/templates/frontend/directives/account-data-menu.html.haml
new file mode 100644
index 000000000..d4d7447a8
--- /dev/null
+++ b/app/assets/templates/frontend/directives/account-data-menu.html.haml
@@ -0,0 +1,18 @@
+.panel.panel-default.account-panel.panel-right.account-data-menu
+ .panel-body
+ .account-items
+ %section.account-item
+ %h3{"ng-click" => "showSN = !showSN"} Standard Notes Account
+ %account-vendor-account-section{"ng-if" => "showSN"}
+
+ %section.account-item
+ %h3{"ng-click" => "showSync = !showSync"} Sync Providers
+ %account-sync-section{"ng-if" => "showSync"}
+
+ %section.account-item
+ %h3{"ng-click" => "showKeys = !showKeys"} Encryption Keys
+ %account-keys-section{"ng-if" => "showKeys"}
+
+ %section.account-item
+ %h3{"ng-click" => "showIO = !showIO"} Import/Export
+ %import-export-menu{"ng-if" => "showIO"}
diff --git a/app/assets/templates/frontend/directives/account-keys-section.html.haml b/app/assets/templates/frontend/directives/account-keys-section.html.haml
new file mode 100644
index 000000000..bdaca70da
--- /dev/null
+++ b/app/assets/templates/frontend/directives/account-keys-section.html.haml
@@ -0,0 +1,11 @@
+.account-keys-section
+ .keys
+ .key{"ng-repeat" => "key in keys"}
+ .name {{key.name}}
+ .value {{key.key}}
+
+ %a{"ng-click" => "newKeyData.showForm = !newKeyData.showForm"} Add New Key
+ %form{"ng-if" => "newKeyData.showForm"}
+ %input{"ng-model" => "newKeyData.name", "placeholder" => "Name your key"}
+ %input{"ng-model" => "newKeyData.key", "placeholder" => "Key"}
+ %button.light{"ng-click" => "submitNewKeyForm()"} Add Key
diff --git a/app/assets/templates/frontend/directives/account-sync-section.html.haml b/app/assets/templates/frontend/directives/account-sync-section.html.haml
new file mode 100644
index 000000000..b99e52fc4
--- /dev/null
+++ b/app/assets/templates/frontend/directives/account-sync-section.html.haml
@@ -0,0 +1,22 @@
+.providers
+ .provider{"ng-repeat" => "provider in syncProviders"}
+ .type {{provider.primary == null ? 'Not enabled' : (provider.primary ? 'Primary' : 'Secondary')}}
+ .key{"ng-if" => "provider.keyName"} Using key: {{provider.keyName}}
+ .url {{provider.url}}
+ .options
+ %div{"ng-if" => "!provider.enabled"}
+ %strong Choose encryption key:
+ %select{"ng-model" => "provider.keyName"}
+ %option{"ng-repeat" => "key in keys", "ng-selected" => "{{key.name == provider.keyName}}", "value" => "{{key.name}}"}
+ {{key.name}}
+ %button.light{"ng-click" => "enableSyncProvider(provider, true)"} Enable as Primary sync provider
+ %button.light{"ng-click" => "enableSyncProvider(provider, false)"} Enable as Secondary sync provider
+ %button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Provider
+ %button.light{"ng-if" => "provider.keyName", "ng-click" => "changeEncryptionKey(provider)"} Change Encryption Key
+
+ %a{"ng-click" => "newSyncData.showAddSyncForm = !newSyncData.showAddSyncForm"} Add external sync with Secret URL
+ %form.sync-form{"ng-if" => "newSyncData.showAddSyncForm"}
+ .form-tag.has-feedback
+ %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Secret URL', :required => true, :type => 'url', 'ng-model' => 'newSyncData.url'}
+ %button.btn.dark-button.btn-block{"ng-click" => "submitExternalSyncURL()"}
+ Add External Sync
diff --git a/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml b/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml
new file mode 100644
index 000000000..3b0ac24de
--- /dev/null
+++ b/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml
@@ -0,0 +1,42 @@
+.registration-login
+ %div{"ng-if" => "user"}
+ .email {{user.email}}
+ .server {{serverURL}}
+ .links{"ng-if" => "user"}
+ .link-item
+ %a{"ng-click" => "signOutPressed()"} Sign Out
+ .meta-container
+ .title Local Encryption
+ .desc Notes are encrypted locally before being sent to the server. Neither the server owner nor an intrusive entity can decrypt your locally encrypted notes.
+ .action-container
+ %span.status-title Status:
+ {{encryptionStatusForNotes()}}
+ %div{"ng-if" => "!user"}
+ .meta-container
+ .title Sign in or Register (optional)
+ .desc Enter your Standard File account information.
+ .action-container
+ %form.account-form{'name' => "loginForm"}
+ .form-tag.has-feedback
+ %input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'loginData.url'}
+ .form-tag.has-feedback
+ %input.form-control.login-input{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'loginData.email'}
+ .form-tag.has-feedback
+ %input.form-control.login-input{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'loginData.user_password'}
+ .checkbox{"ng-if" => "localNotesCount() > 0"}
+ %label
+ %input{"type" => "checkbox", "ng-model" => "loginData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"}
+ Merge local notes ({{localNotesCount()}} notes)
+ %button.btn.dark-button.half-button{"ng-click" => "loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
+ %span Sign In
+ %button.btn.dark-button.half-button{"ng-click" => "submitRegistrationForm()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
+ %span Register
+ %br
+ .login-forgot{"style" => "padding-top: 4px;"}
+ %a.btn.btn-link{"ng-click" => "showResetForm = !showResetForm"} Passwords cannot be forgotten.
+ .panel-status-text{"ng-if" => "loginData.status", "style" => "font-size: 14px;"} {{loginData.status}}
+
+ %div{"ng-if" => "showResetForm"}
+ %p{"style" => "font-size: 13px; text-align: center;"}
+ Because notes are locally encrypted using a secret key derived from your password, there's no way to decrypt these notes if you forget your password.
+ For this reason, Standard Notes cannot offer a password reset option. You must make sure to store or remember your password.
diff --git a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml
new file mode 100644
index 000000000..efa0873ad
--- /dev/null
+++ b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml
@@ -0,0 +1,82 @@
+.panel.panel-default.account-panel.panel-right.extensions-panel
+ .panel-body
+ %div{"style" => "font-size: 18px;", "ng-if" => "!extensionManager.extensions.length"} No extensions installed
+ .registered-extensions{"ng-if" => "extensionManager.extensions.length"}
+ .extension{"ng-repeat" => "extension in extensionManager.extensions", "ng-init" => "extension.formData = {}"}
+ .extension-name {{extension.name}}
+ .encryption-format
+ .title Send data:
+ %label
+ %input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "true", "ng-change" => "changeExtensionEncryptionFormat(true, extension)"}
+ Encrypted
+ %label
+ %input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "false", "ng-change" => "changeExtensionEncryptionFormat(false, extension)"}
+ Decrypted
+ .ek-input-wrapper{"ng-if" => "extension.encrypted && (!extensionManager.ekForExtension(extension) || extension.formData.changingKey)"}
+ %input{"ng-model" => "extension.formData.ek", "placeholder" => "Set encryption key"}
+ %button.light{"ng-click" => "setEncryptionKeyForExtension(extension)"} Set
+ .extension-actions
+ .action{"ng-repeat" => "action in extension.actionsInGlobalContext()"}
+ %div{"ng-if" => "!action.sync_provider"}
+ .action-name {{action.label}}
+ .action-desc{"style" => "font-style: italic;"} {{action.desc}}
+ .execute-type{"ng-if" => "action.repeat_mode == 'watch'"}
+ Repeats when a change is made to your items.
+ .execute-type{"ng-if" => "action.repeat_mode == 'loop'"}
+ Repeats at most once every {{action.repeat_timeout}} seconds
+ .action-permissions
+ %a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}}
+ %div{"ng-if" => "action.showPermissions"}
+ {{action.permissionsString}}
+ .encryption-type
+ %span {{action.encryptionModeString}}
+ %div{"ng-if" => "action.sync_provider"}
+ .action-name This is a sync provider action.
+ .action-desc{"style" => "margin-top: -5px;"}
+ %p Enabling this sync provider as a primary provider will replace your current sync provider.
+ %p Enabling this sync provider as a secondary provider will save your data with this provider as a backup, but will not be used to pull changes.
+ %p You can have only one primary provider, and multiple secondary providers.
+
+ %div{"ng-if" => "!syncProviderActionIsEnabled(action)"}
+ %button.light.execute{"ng-click" => "enableSyncProvider(action, extension, true)"} Enable as primary sync provider
+ %button.light.execute{"ng-click" => "enableSyncProvider(action, extension, false)"} Enable as backup sync provider
+ %div{"ng-if" => "syncProviderActionIsEnabled(action)"}
+ %button.light.execute{"ng-click" => "disableSyncProvider(action, extension)"} Remove as sync provider
+
+ .execute{"ng-if" => "!action.sync_provider"}
+ %div{"ng-if" => "action.repeat_mode"}
+ %div{"ng-if" => "extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.disableRepeatAction(action, extension)"} Disable
+ %div{"ng-if" => "!extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.enableRepeatAction(action, extension)"} Enable
+ %div{"ng-if" => "!action.repeat_mode", "ng-click" => "selectedAction(action, extension)"}
+ %div{"ng-if" => "!action.running"}
+ Perform Action
+ %div{"ng-if" => "action.running"}
+ .spinner.execution-spinner
+ .last-run{"ng-if" => "!action.error && action.lastExecuted && !action.running"}
+ Last run {{action.lastExecuted | appDateTime}}
+ .error{"ng-if" => "action.error"}
+ Error performing action.
+
+
+ %a.option-link{"ng-if" => "extension.encrypted && extensionManager.ekForExtension(extension) && !extension.formData.changingKey", "ng-click" => "extension.formData.showEk = !extension.formData.showEk"}
+ Show Encryption Key
+ .show-ek{"style" => "text-align: center", "ng-if" => "extension.formData.showEk"}
+ .ek {{extensionManager.ekForExtension(extension)}}
+ .disclaimer This key is saved locally and never sent to any servers.
+ %a.option-link{"ng-if" => "extension.encrypted && extensionManager.ekForExtension(extension) && !extension.formData.changingKey", "ng-click" => "changeEncryptionKeyPressed(extension)"}
+ Change Encryption Key
+ %a.option-link{"ng-click" => "deleteExtension(extension)"} Remove extension
+
+ .extension-link
+ %a{"ng-click" => "toggleExtensionForm()"} Add new extension
+
+ %form.extension-form{"ng-if" => "showNewExtensionForm"}
+ .form-tag.has-feedback
+ %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Extension URL', :required => true, :type => 'url', 'ng-model' => 'newExtensionData.url'}
+ %button.btn.dark-button.btn-block{"ng-click" => "submitNewExtensionForm()", :type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
+ %span.ladda-label Add Extension
+
+ .extension-link
+ %a{"ng-click" => "reloadExtensionsPressed()", "ng-if" => "extensionManager.extensions.length > 0"} Reload all extensions
+ .extension-link
+ %a{"href" => "https://standardnotes.org/extensions", "target" => "_blank"} List of available extensions
diff --git a/app/assets/templates/frontend/directives/import-export-menu.html.haml b/app/assets/templates/frontend/directives/import-export-menu.html.haml
new file mode 100644
index 000000000..0a6b5916a
--- /dev/null
+++ b/app/assets/templates/frontend/directives/import-export-menu.html.haml
@@ -0,0 +1,26 @@
+.options{"style" => "font-size: 12px; margin-top: 4px;"}
+ %label.enc-option{"ng-if" => "user"}
+ %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'mk'", "ng-change" => "archiveFormData.encryption_type = 'mk'"}
+ Encrypted with Standard File key
+ %label.enc-option
+ %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'ek'", "ng-change" => "archiveFormData.encryption_type = 'ek'"}
+ {{user ? 'Encrypted with custom key' : 'Encrypted' }}
+ %div{"ng-if" => "!user || (user && archiveFormData.encryption_type == 'ek')"}
+ %input{"ng-model" => "archiveFormData.ek", "placeholder" => "Encryption key"}
+ %label.enc-option
+ %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'none'", "ng-change" => "archiveFormData.encryption_type = 'none'"}
+ Decrypted
+.action-container
+ %a{"ng-click" => "downloadDataArchive()"} Download Data Archive
+
+%div{"ng-if" => "!importData.loading"}
+ %label#import-archive
+ %input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"}
+ %a.disabled
+ %span
+ Import Data from Archive
+ .import-password{"ng-if" => "importData.requestPassword"}
+ Enter the account password associated with the import file.
+ %input.field{"type" => "text", "ng-model" => "importData.password"}
+ %button{"ng-click" => "submitImportPassword()"} Decrypt & Import
+.spinner{"ng-if" => "importData.loading"}
diff --git a/app/assets/templates/frontend/header.html.haml b/app/assets/templates/frontend/header.html.haml
index ad14a0fee..67106684f 100644
--- a/app/assets/templates/frontend/header.html.haml
+++ b/app/assets/templates/frontend/header.html.haml
@@ -1,175 +1,16 @@
.header
.header-content
.menu.left
+
.items
.item.account
- %div{"ng-click" => "ctrl.accountMenuPressed()"}
- %div{"ng-if" => "ctrl.user"} Account
- %div{"ng-if" => "!ctrl.user"} Sign in or Register
- .panel.panel-default.account-panel.panel-right{"ng-if" => "ctrl.showAccountMenu"}
- .panel-body
- .account-items
- .account-item.registration-login{"ng-if" => "!ctrl.user"}
- .account-item
- .meta-container
- .title Server
- .desc Enter your Standard File server address, or use the default.
- .action-container
- %form.account-form{'ng-submit' => 'ctrl.changeServer()', 'name' => "serverChangeForm"}
- .form-tag.has-feedback
- %input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'ctrl.serverData.url'}
- %button.btn.dark-button.btn-block{:type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
- %span.ladda-label Set Server
- .meta-container
- .title Sign in or Register
- .desc
- %form.account-form{'name' => "loginForm"}
- .form-tag.has-feedback
- %input.form-control.login-input{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'ctrl.loginData.email'}
- .form-tag.has-feedback
- %input.form-control.login-input{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.loginData.user_password'}
- .checkbox{"ng-if" => "ctrl.localNotesCount() > 0"}
- %label
- %input{"type" => "checkbox", "ng-model" => "ctrl.loginData.mergeLocal", "ng-bind" => "true", "ng-change" => "ctrl.mergeLocalChanged()"}
- Merge local notes ({{ctrl.localNotesCount()}} notes)
- %button.btn.dark-button.half-button{"ng-click" => "ctrl.loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
- %span Sign In
- %button.btn.dark-button.half-button{"ng-click" => "ctrl.submitRegistrationForm()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
- %span Register
- %br
- .login-forgot{"style" => "padding-top: 4px;"}
- %a.btn.btn-link{"ng-click" => "ctrl.showResetForm = !ctrl.showResetForm"} Passwords cannot be forgotten.
- .panel-status-text{"ng-if" => "ctrl.loginData.status", "style" => "font-size: 14px;"} {{ctrl.loginData.status}}
-
- %div{"ng-if" => "ctrl.showResetForm"}
- %p{"style" => "font-size: 13px; text-align: center;"}
- Because notes are locally encrypted using a secret key derived from your password, there's no way to decrypt these notes if you forget your password.
- For this reason, Standard Notes cannot offer a password reset option. You must make sure to store or remember your password.
-
- .account-item{"ng-if" => "ctrl.user"}
- .email {{ctrl.user.email}}
- .server {{ctrl.serverData.url}}
- .links{"ng-if" => "ctrl.user"}
- -# .link-item
- -# %a{"ng-click" => "ctrl.changePasswordPressed()"} Change Password
- -# %form.account-form{"ng-if" => "ctrl.showNewPasswordForm", 'ng-submit' => 'ctrl.submitPasswordChange()', 'name' => "passwordChangeForm"}
- -# .form-tag.has-feedback
- -# %input.form-control.login-input{:autofocus => 'autofocus', :name => 'current', :placeholder => 'Current password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.current_password'}
- -# .form-tag.has-feedback
- -# %input.form-control.login-input{:placeholder => 'New password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.new_password', "autocomplete" => "new-password"}
- -# .form-tag.has-feedback
- -# %input.form-control.login-input{:placeholder => 'Confirm password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.new_password_confirmation', "autocomplete" => "new-password"}
- -# %button.btn.dark-button.btn-block{:type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
- -# %span.ladda-label Change Password
- -# .panel-status-text{"ng-if" => "ctrl.passwordChangeData.status", "style" => "font-size: 14px;"}
- -# {{ctrl.passwordChangeData.status}}
- .link-item
- %a{"ng-click" => "ctrl.signOutPressed()"} Sign Out
- .meta-container
- .title Local Encryption
- .desc Notes are encrypted locally before being sent to the server. Neither the server owner nor an intrusive entity can decrypt your locally encrypted notes.
- .action-container
- %span.status-title Status:
- {{ctrl.encryptionStatusForNotes()}}
- .account-item{"ng-if" => "ctrl.user"}
- .meta-container
- .title Data Archives
- .options{"style" => "font-size: 12px; margin-top: 4px;"}
- %label
- %input{"type" => "radio", "ng-model" => "ctrl.archiveEncryptionFormat.encrypted", "ng-value" => "true", "ng-change" => "ctrl.archiveEncryptionFormat.encrypted = true"}
- Encrypted
- %label
- %input{"type" => "radio", "ng-model" => "ctrl.archiveEncryptionFormat.encrypted", "ng-value" => "false", "ng-change" => "ctrl.archiveEncryptionFormat.encrypted = false"}
- Decrypted
- .action-container
- %a{"ng-click" => "ctrl.downloadDataArchive()"} Download Data Archive
- %br
- %div{"ng-if" => "!ctrl.importData.loading"}
- %label#import-archive
- %input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "ctrl.importFileSelected(files)"}
- %a.disabled
- %span
- Import Data from Archive
- .import-password{"ng-if" => "ctrl.importData.requestPassword"}
- Enter the account password associated with the import file.
- %input.field{"type" => "text", "ng-model" => "ctrl.importData.password"}
- %button{"ng-click" => "ctrl.submitImportPassword()"} Decrypt & Import
- .spinner{"ng-if" => "ctrl.importData.loading"}
+ %a{"ng-click" => "ctrl.accountMenuPressed()"} Data
+ %account-data-menu
+ -# {"ng-if" => "ctrl.showAccountMenu"}
.item
%a{"ng-click" => "ctrl.toggleExtensions()"} Extensions
- .panel.panel-default.account-panel.panel-right.extensions-panel{"ng-if" => "ctrl.showExtensionsMenu"}
- .panel-body
- %div{"style" => "font-size: 18px;", "ng-if" => "!ctrl.extensionManager.extensions.length"} No extensions installed
- .registered-extensions{"ng-if" => "ctrl.extensionManager.extensions.length"}
- .extension{"ng-repeat" => "extension in ctrl.extensionManager.extensions"}
- .extension-name {{extension.name}}
- .encryption-format
- .title Send data:
- %label
- %input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "true", "ng-change" => "ctrl.extensionManager.changeExtensionEncryptionFormat(true, extension)"}
- Encrypted
- %label
- %input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "false", "ng-change" => "ctrl.extensionManager.changeExtensionEncryptionFormat(false, extension)"}
- Decrypted
- .encryption-key{"ng-if" => "extension.encrypted"}
- %input{"ng-model" => "extension.ek", "placeholder" => "Set encryption key"}
- .extension-actions
- .action{"ng-repeat" => "action in extension.actionsInGlobalContext()"}
- %div{"ng-if" => "!action.sync_provider"}
- .action-name {{action.label}}
- .action-desc{"style" => "font-style: italic;"} {{action.desc}}
- .execute-type{"ng-if" => "action.repeat_mode == 'watch'"}
- Repeats when a change is made to your items.
- .execute-type{"ng-if" => "action.repeat_mode == 'loop'"}
- Repeats at most once every {{action.repeat_timeout}} seconds
- .action-permissions
- %a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}}
- %div{"ng-if" => "action.showPermissions"}
- {{action.permissionsString}}
- .encryption-type
- %span {{action.encryptionModeString}}
- %div{"ng-if" => "action.sync_provider"}
- .action-name This is a sync provider action.
- .action-desc{"style" => "margin-top: -5px;"}
- %p Enabling this sync provider as a primary provider will replace your current sync provider.
- %p Enabling this sync provider as a secondary provider will save your data with this provider as a backup, but will not be used to pull changes.
- %p You can have only one primary provider, and multiple secondary providers.
-
- %div{"ng-if" => "!ctrl.syncProviderActionIsEnabled(action)"}
- .execute{"ng-click" => "ctrl.enableSyncProvider(action, extension, true)"} Enable as primary sync provider
- .execute{"ng-click" => "ctrl.enableSyncProvider(action, extension, false)"} Enable as backup sync provider
- %div{"ng-if" => "ctrl.syncProviderActionIsEnabled(action)"}
- .execute{"ng-click" => "ctrl.disableSyncProvider(action, extension)"} Remove as sync provider
-
- .execute{"ng-if" => "!action.sync_provider"}
- %div{"ng-if" => "action.repeat_mode"}
- %div{"ng-if" => "ctrl.extensionManager.isRepeatActionEnabled(action)", "ng-click" => "ctrl.extensionManager.disableRepeatAction(action, extension)"} Disable
- %div{"ng-if" => "!ctrl.extensionManager.isRepeatActionEnabled(action)", "ng-click" => "ctrl.extensionManager.enableRepeatAction(action, extension)"} Enable
- %div{"ng-if" => "!action.repeat_mode", "ng-click" => "ctrl.selectedAction(action, extension)"}
- %div{"ng-if" => "!action.running"}
- Perform Action
- %div{"ng-if" => "action.running"}
- .spinner.execution-spinner
- .last-run{"ng-if" => "!action.error && action.lastExecuted && !action.running"}
- Last run {{action.lastExecuted | appDateTime}}
- .error{"ng-if" => "action.error"}
- Error performing action.
- %a{"ng-click" => "ctrl.deleteExtension(extension)", "style" => "margin-top: 22px; display: block; text-align: center;"} Remove extension
-
- .extension-link
- %a{"ng-click" => "ctrl.toggleExtensionForm()"} Add new extension
-
- %form.extension-form{"ng-if" => "ctrl.showNewExtensionForm"}
- .form-tag.has-feedback
- %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Extension URL', :required => true, :type => 'url', 'ng-model' => 'ctrl.newExtensionData.url'}
- %button.btn.dark-button.btn-block{"ng-click" => "ctrl.submitNewExtensionForm()", :type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
- %span.ladda-label Add Extension
-
- .extension-link
- %a{"ng-click" => "ctrl.reloadExtensionsPressed()", "ng-if" => "ctrl.extensionManager.extensions.length > 0"} Reload all extensions
- .extension-link
- %a{"href" => "https://standardnotes.org/extensions", "target" => "_blank"} List of available extensions
+ %global-extensions-menu{"ng-if" => "ctrl.showExtensionsMenu"}
.item
%a{"href" => "https://standardnotes.org", "target" => "_blank"}