sync accounts

This commit is contained in:
Mo Bitar
2017-01-26 19:50:47 -06:00
parent a255b8487e
commit b7492176d1
15 changed files with 167 additions and 114 deletions

View File

@@ -1,6 +1,7 @@
'use strict'; 'use strict';
var Neeto = Neeto || {}; var Neeto = Neeto || {};
var SN = SN || {};
// detect IE8 and above, and edge. // detect IE8 and above, and edge.
// IE and Edge do not support pbkdf2 in WebCrypto, therefore we need to use CryptoJS // IE and Edge do not support pbkdf2 in WebCrypto, therefore we need to use CryptoJS
@@ -19,18 +20,4 @@ angular.module('app.frontend', [
.config(function (RestangularProvider, apiControllerProvider) { .config(function (RestangularProvider, apiControllerProvider) {
RestangularProvider.setDefaultHeaders({"Content-Type": "application/json"}); RestangularProvider.setDefaultHeaders({"Content-Type": "application/json"});
RestangularProvider.setFullRequestInterceptor(function(element, operation, route, url, headers, params, httpConfig) {
var token = localStorage.getItem("jwt");
if(token) {
headers = _.extend(headers, {Authorization: "Bearer " + localStorage.getItem("jwt")});
}
return {
element: element,
params: params,
headers: headers,
httpConfig: httpConfig
};
});
}) })

View File

@@ -19,9 +19,9 @@ angular.module('app.frontend')
/** /**
* Insert 4 spaces when a tab key is pressed, * Insert 4 spaces when a tab key is pressed,
* only used when inside of the text editor. * only used when inside of the text editor.
* If the shift key is pressed first, this event is * If the shift key is pressed first, this event is
* not fired. * not fired.
*/ */
var handleTab = function (event) { var handleTab = function (event) {
if (!event.shiftKey && event.which == 9) { if (!event.shiftKey && event.which == 9) {
event.preventDefault(); event.preventDefault();
@@ -29,13 +29,13 @@ angular.module('app.frontend')
var end = this.selectionEnd; var end = this.selectionEnd;
var spaces = " "; var spaces = " ";
// Insert 4 spaces // Insert 4 spaces
this.value = this.value.substring(0, start) this.value = this.value.substring(0, start)
+ spaces + this.value.substring(end); + spaces + this.value.substring(end);
// Place cursor 4 spaces away from where // Place cursor 4 spaces away from where
// the tab key was pressed // the tab key was pressed
this.selectionStart = this.selectionEnd = start + 4; this.selectionStart = this.selectionEnd = start + 4;
} }
} }
@@ -88,7 +88,7 @@ angular.module('app.frontend')
} }
} }
}) })
.controller('EditorCtrl', function ($sce, $timeout, apiController, markdownRenderer, $rootScope, extensionManager) { .controller('EditorCtrl', function ($sce, $timeout, apiController, markdownRenderer, $rootScope, extensionManager, syncManager) {
this.setNote = function(note, oldNote) { this.setNote = function(note, oldNote) {
this.editorMode = 'edit'; this.editorMode = 'edit';
@@ -148,12 +148,18 @@ angular.module('app.frontend')
if(success) { if(success) {
if(statusTimeout) $timeout.cancel(statusTimeout); if(statusTimeout) $timeout.cancel(statusTimeout);
statusTimeout = $timeout(function(){ statusTimeout = $timeout(function(){
this.noteStatus = "All changes saved" this.saveError = false;
var status = "All changes saved"
if(syncManager.offline) {
status += " (offline)";
}
this.noteStatus = status;
}.bind(this), 200) }.bind(this), 200)
} else { } else {
if(statusTimeout) $timeout.cancel(statusTimeout); if(statusTimeout) $timeout.cancel(statusTimeout);
statusTimeout = $timeout(function(){ statusTimeout = $timeout(function(){
this.noteStatus = "(Offline) — All changes saved" this.saveError = true;
this.noteStatus = "Error saving"
}.bind(this), 200) }.bind(this), 200)
} }
}.bind(this)); }.bind(this));

View File

@@ -12,6 +12,10 @@ angular.module('app.frontend')
link:function(scope, elem, attrs, ctrl) { link:function(scope, elem, attrs, ctrl) {
scope.$on("sync:updated_token", function(){ scope.$on("sync:updated_token", function(){
ctrl.syncUpdated(); ctrl.syncUpdated();
ctrl.findErrors();
})
scope.$on("sync:error", function(){
ctrl.findErrors();
}) })
} }
} }
@@ -19,9 +23,16 @@ angular.module('app.frontend')
.controller('HeaderCtrl', function (apiController, modelManager, $timeout, dbManager, syncManager) { .controller('HeaderCtrl', function (apiController, modelManager, $timeout, dbManager, syncManager) {
this.user = apiController.user; this.user = apiController.user;
this.offline = syncManager.offline;
this.findErrors = function() {
this.error = syncManager.syncProviders.filter(function(provider){return provider.error}).length > 0 ? true : false;
}
this.findErrors();
this.accountMenuPressed = function() { this.accountMenuPressed = function() {
this.serverData = {url: syncManager.serverURL()}; this.serverData = {};
this.showAccountMenu = !this.showAccountMenu; this.showAccountMenu = !this.showAccountMenu;
this.showFaq = false; this.showFaq = false;
this.showNewPasswordForm = false; this.showNewPasswordForm = false;

View File

@@ -7,11 +7,11 @@ angular.module('app.frontend')
return domain; return domain;
} }
this.$get = function($rootScope, Restangular, modelManager, dbManager, keyManager, syncManager) { this.$get = function($rootScope, Restangular, modelManager, dbManager, syncManager) {
return new ApiController($rootScope, Restangular, modelManager, dbManager, keyManager, syncManager); return new ApiController($rootScope, Restangular, modelManager, dbManager, syncManager);
} }
function ApiController($rootScope, Restangular, modelManager, dbManager, keyManager, syncManager) { function ApiController($rootScope, Restangular, modelManager, dbManager, syncManager) {
var userData = localStorage.getItem("user"); var userData = localStorage.getItem("user");
if(userData) { if(userData) {
@@ -79,7 +79,7 @@ angular.module('app.frontend')
var params = {password: keys.pw, email: email}; var params = {password: keys.pw, email: email};
_.merge(request, params); _.merge(request, params);
request.post().then(function(response){ request.post().then(function(response){
this.handleAuthResponse(response, url, authParams, mk); this.handleAuthResponse(response, email, url, authParams, mk);
callback(response); callback(response);
}.bind(this)) }.bind(this))
.catch(function(response){ .catch(function(response){
@@ -90,13 +90,16 @@ angular.module('app.frontend')
}.bind(this)) }.bind(this))
} }
this.handleAuthResponse = function(response, url, authParams, mk) { this.handleAuthResponse = function(response, email, url, authParams, mk) {
localStorage.setItem("server", url); var params = {
localStorage.setItem("jwt", response.token); url: url,
localStorage.setItem("user", JSON.stringify(response.user)); email: email,
localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"]))); uuid: response.user.uuid,
keyManager.addKey(SNKeyName, mk); ek: mk,
syncManager.addStandardFileSyncProvider(url); jwt: response.token,
auth_params: _.omit(authParams, ["pw_nonce"])
}
syncManager.addAccountBasedSyncProvider(params);
} }
this.register = function(url, email, password, callback) { this.register = function(url, email, password, callback) {
@@ -107,7 +110,7 @@ angular.module('app.frontend')
var params = _.merge({password: keys.pw, email: email}, authParams); var params = _.merge({password: keys.pw, email: email}, authParams);
_.merge(request, params); _.merge(request, params);
request.post().then(function(response){ request.post().then(function(response){
this.handleAuthResponse(response, url, authParams, mk); this.handleAuthResponse(response, email, url, authParams, mk);
callback(response); callback(response);
}.bind(this)) }.bind(this))
.catch(function(response){ .catch(function(response){
@@ -256,19 +259,6 @@ angular.module('app.frontend')
return JSON.parse(JSON.stringify(object)); return JSON.parse(JSON.stringify(object));
} }
this.signoutOfStandardFile = function(destroyAll, callback) {
syncManager.removeStandardFileSyncProvider();
if(destroyAll) {
this.destroyLocalData(callback);
} else {
localStorage.removeItem("user");
localStorage.removeItem("jwt");
localStorage.removeItem("server");
localStorage.removeItem("auth_params");
callback();
}
}
this.destroyLocalData = function(callback) { this.destroyLocalData = function(callback) {
dbManager.clearAllItems(function(){ dbManager.clearAllItems(function(){
localStorage.clear(); localStorage.clear();

View File

@@ -3,8 +3,7 @@ class AccountDataMenu {
constructor() { constructor() {
this.restrict = "E"; this.restrict = "E";
this.templateUrl = "frontend/directives/account-data-menu.html"; this.templateUrl = "frontend/directives/account-data-menu.html";
this.scope = { this.scope = {};
};
} }
controller($scope, apiController, modelManager, keyManager) { controller($scope, apiController, modelManager, keyManager) {
@@ -13,7 +12,7 @@ class AccountDataMenu {
$scope.keys = keyManager.keys; $scope.keys = keyManager.keys;
$scope.destroyLocalData = function() { $scope.destroyLocalData = function() {
if(!confirm("Are you sure you want to end your session? This will delete all local items, sync providers, keys, and extensions.")) { if(!confirm("Are you sure you want to end your session? This will delete all local items, sync accounts, keys, and extensions.")) {
return; return;
} }

View File

@@ -10,7 +10,7 @@ class AccountNewAccountSection {
controller($scope, apiController, modelManager, $timeout, dbManager, syncManager) { controller($scope, apiController, modelManager, $timeout, dbManager, syncManager) {
'ngInject'; 'ngInject';
$scope.formData = {mergeLocal: true, url: syncManager.serverURL()}; $scope.formData = {mergeLocal: true, url: syncManager.defaultServerURL()};
$scope.user = apiController.user; $scope.user = apiController.user;
$scope.showForm = syncManager.syncProviders.length == 0; $scope.showForm = syncManager.syncProviders.length == 0;
@@ -20,15 +20,10 @@ class AccountNewAccountSection {
} }
$scope.submitExternalSyncURL = function() { $scope.submitExternalSyncURL = function() {
syncManager.addSyncProviderFromURL($scope.newSyncData.url); syncManager.addSyncProviderFromURL($scope.formData.secretUrl);
$scope.newSyncData.showAddSyncForm = false; $scope.formData.showAddLinkForm = false;
} $scope.formData.secretUrl = null;
$scope.showForm = false;
$scope.signOutPressed = function() {
$scope.showAccountMenu = false;
apiController.signoutOfStandardFile(false, function(){
window.location.reload();
})
} }
$scope.submitPasswordChange = function() { $scope.submitPasswordChange = function() {

View File

@@ -16,7 +16,7 @@ class AccountSyncSection {
$scope.enableSyncProvider = function(provider, primary) { $scope.enableSyncProvider = function(provider, primary) {
if(!provider.keyName) { if(!provider.keyName) {
alert("You must choose an encryption key for this provider before enabling it."); alert("You must choose an encryption key for this account before enabling it.");
return; return;
} }
@@ -24,19 +24,19 @@ class AccountSyncSection {
} }
$scope.removeSyncProvider = function(provider) { $scope.removeSyncProvider = function(provider) {
if(provider.isStandardNotesAccount) { if(provider.primary) {
alert("To remove your Standard Notes sync, sign out of your Standard Notes account.") alert("You cannot remove your main sync account. Instead, end your session by destroying all local data. Or, choose another account to be your primary sync account.")
return; return;
} }
if(confirm("Are you sure you want to remove this sync provider?")) { if(confirm("Are you sure you want to remove this sync account?")) {
syncManager.removeSyncProvider(provider); syncManager.removeSyncProvider(provider);
} }
} }
$scope.changeEncryptionKey = function(provider) { $scope.changeEncryptionKey = function(provider) {
if(provider.isStandardNotesAccount) { if(provider.isStandardNotesAccount) {
alert("To change your encryption key for your Standard Notes account, you need to change your password. However, this functionality is not currently supported."); alert("To change your encryption key for your Standard File account, you need to change your password. However, this functionality is not currently available.");
return; return;
} }

View File

@@ -16,7 +16,6 @@ angular
// Whenever the scope variable updates we simply // Whenever the scope variable updates we simply
// show if it evaluates to 'true' and hide if 'false' // show if it evaluates to 'true' and hide if 'false'
scope.$watch('show', function(newVal){ scope.$watch('show', function(newVal){
console.log("show value changed", newVal);
newVal ? showSpinner() : hideSpinner(); newVal ? showSpinner() : hideSpinner();
}); });
@@ -29,7 +28,6 @@ angular
} }
function showElement(show) { function showElement(show) {
console.log("show:", show);
show ? elem.css({display:''}) : elem.css({display:'none'}); show ? elem.css({display:''}) : elem.css({display:'none'});
} }

View File

@@ -2,8 +2,9 @@ export const SNKeyName = "Standard Notes Key";
class SyncManager { class SyncManager {
constructor(modelManager, syncRunner) { constructor(modelManager, syncRunner, keyManager) {
this.modelManager = modelManager; this.modelManager = modelManager;
this.keyManager = keyManager;
this.syncRunner = syncRunner; this.syncRunner = syncRunner;
this.syncRunner.setOnChangeProviderCallback(function(){ this.syncRunner.setOnChangeProviderCallback(function(){
this.didMakeChangesToSyncProviders(); this.didMakeChangesToSyncProviders();
@@ -15,14 +16,19 @@ class SyncManager {
return this.enabledProviders.length == 0; return this.enabledProviders.length == 0;
} }
serverURL() { defaultServerURL() {
return localStorage.getItem("server") || "https://n3.standardnotes.org"; return "https://n3.standardnotes.org";
} }
get enabledProviders() { get enabledProviders() {
return this.syncProviders.filter(function(provider){return provider.enabled == true}); return this.syncProviders.filter(function(provider){return provider.enabled == true});
} }
/* Used when adding a new account with */
markAllOfflineItemsDirtyAndSave() {
}
sync(callback) { sync(callback) {
this.syncRunner.sync(this.enabledProviders, callback); this.syncRunner.sync(this.enabledProviders, callback);
} }
@@ -57,21 +63,6 @@ class SyncManager {
return _.find(this.syncProviders, {primary: true}); return _.find(this.syncProviders, {primary: true});
} }
removeStandardFileSyncProvider() {
var sfProvider = _.find(this.syncProviders, {url: this.serverURL() + "/items/sync"})
_.pull(this.syncProviders, sfProvider);
this.didMakeChangesToSyncProviders();
}
addStandardFileSyncProvider(url) {
var defaultProvider = new SyncProvider({url: url + "/items/sync", primary: !this.primarySyncProvider()});
defaultProvider.keyName = SNKeyName;
defaultProvider.enabled = this.syncProviders.length == 0;
this.syncProviders.push(defaultProvider);
this.didMakeChangesToSyncProviders();
return defaultProvider;
}
didMakeChangesToSyncProviders() { didMakeChangesToSyncProviders() {
localStorage.setItem("syncProviders", JSON.stringify(_.map(this.syncProviders, function(provider) { localStorage.setItem("syncProviders", JSON.stringify(_.map(this.syncProviders, function(provider) {
return provider.asJSON() return provider.asJSON()
@@ -89,22 +80,58 @@ class SyncManager {
} else { } else {
// no providers saved, this means migrating from old system to new // no providers saved, this means migrating from old system to new
// check if user is signed in // check if user is signed in
if(this.offline && localStorage.getItem("user")) { var userJSON = localStorage.getItem("user");
var defaultProvider = this.addStandardFileSyncProvider(this.serverURL()); if(this.offline && userJSON) {
defaultProvider.syncToken = localStorage.getItem("syncToken"); var user = JSON.parse(userJSON);
// migrate old key structure to new var params = {
var mk = localStorage.getItem("mk"); url: localStorage.getItem("server"),
if(mk) { email: user.email,
keyManager.addKey(SNKeyName, mk); uuid: user.uuid,
localStorage.removeItem("mk"); ek: localStorage.getItem("mk"),
jwt: response.token,
auth_params: JSON.parse(localStorage.getItem("auth_params")),
} }
var defaultProvider = this.addAccountBasedSyncProvider(params);
defaultProvider.syncToken = localStorage.getItem("syncToken");
localStorage.removeItem("mk");
localStorage.removeItem("syncToken");
localStorage.removeItem("auth_params");
localStorage.removeItem("user");
localStorage.removeItem("server");
this.didMakeChangesToSyncProviders(); this.didMakeChangesToSyncProviders();
} }
} }
} }
addAccountBasedSyncProvider({url, email, uuid, ek, jwt, auth_params} = {}) {
var provider = new SyncProvider({
url: url + "/items/sync",
primary: !this.primarySyncProvider(),
email: email,
uuid: uuid,
jwt: jwt,
auth_params: auth_params,
type: SN.SyncProviderType.account
});
provider.keyName = provider.name;
this.syncProviders.push(provider);
this.didMakeChangesToSyncProviders();
this.keyManager.addKey(provider.keyName, ek);
if(this.syncProviders.length == 0) {
this.enableSyncProvider(provider, true);
}
return provider;
}
addSyncProviderFromURL(url) { addSyncProviderFromURL(url) {
var provider = new SyncProvider({url: url}); var provider = new SyncProvider({url: url});
provider.type = SN.SyncProviderType.URL;
this.syncProviders.push(provider); this.syncProviders.push(provider);
this.didMakeChangesToSyncProviders(); this.didMakeChangesToSyncProviders();
} }
@@ -123,8 +150,6 @@ class SyncManager {
this.addAllDataAsNeedingSyncForProvider(syncProvider); this.addAllDataAsNeedingSyncForProvider(syncProvider);
this.didMakeChangesToSyncProviders(); this.didMakeChangesToSyncProviders();
this.syncWithProvider(syncProvider); this.syncWithProvider(syncProvider);
this.syncWithProvider(syncProvider);
this.syncWithProvider(syncProvider);
} }
addAllDataAsNeedingSyncForProvider(syncProvider) { addAllDataAsNeedingSyncForProvider(syncProvider) {

View File

@@ -1,3 +1,8 @@
SN.SyncProviderType = {
Account: 1,
URL: 2
}
class SyncProvider { class SyncProvider {
constructor(obj) { constructor(obj) {
@@ -31,13 +36,28 @@ class SyncProvider {
else return "secondary"; else return "secondary";
} }
get name() {
if(this.type == SN.SyncProviderType.account) {
return this.email + "@" + this.url;
} else {
return this.url;
}
}
asJSON() { asJSON() {
return { return {
enabled: this.enabled, enabled: this.enabled,
url: this.url, url: this.url,
type: this.type,
primary: this.primary, primary: this.primary,
keyName: this.keyName, keyName: this.keyName,
syncToken: this.syncToken syncToken: this.syncToken,
// account based
email: this.email,
uuid: this.uuid,
jwt: this.jwt,
auth_params: this.auth_params
} }
} }
} }

View File

@@ -39,7 +39,6 @@ class SyncRunner {
} }
syncOffline(items, callback) { syncOffline(items, callback) {
console.log("Writing items offline", items);
this.writeItemsToLocalStorage(items, true, function(responseItems){ this.writeItemsToLocalStorage(items, true, function(responseItems){
// delete anything needing to be deleted // delete anything needing to be deleted
for(var item of items) { for(var item of items) {
@@ -128,7 +127,9 @@ class SyncRunner {
request.sync_token = provider.syncToken; request.sync_token = provider.syncToken;
request.cursor_token = provider.cursorToken; request.cursor_token = provider.cursorToken;
request.post().then(function(response) { var headers = provider.jwt ? {Authorization: "Bearer " + provider.jwt} : {};
request.post("", undefined, undefined, headers).then(function(response) {
provider.error = null;
if(!provider.primary) { if(!provider.primary) {
console.log("Completed sync for provider:", provider.url, "Response:", response); console.log("Completed sync for provider:", provider.url, "Response:", response);
@@ -170,15 +171,19 @@ class SyncRunner {
}.bind(this)) }.bind(this))
.catch(function(response){ .catch(function(response){
console.log("Sync error: ", response); console.log("Sync error: ", response);
var error = response.data.error || {message: "Could not connect to server."};
// Re-add subItems since this operation failed. We'll have to try again. // Re-add subItems since this operation failed. We'll have to try again.
provider.addPendingItems(subItems); provider.addPendingItems(subItems);
provider.syncOpInProgress = false; provider.syncOpInProgress = false;
provider.error = error;
if(provider.primary) { if(provider.primary) {
this.writeItemsToLocalStorage(allItems, false, null); this.writeItemsToLocalStorage(allItems, false, null);
} }
this.rootScope.$broadcast("sync:error", error);
if(callback) { if(callback) {
callback({error: "Sync error"}); callback({error: "Sync error"});
} }

View File

@@ -14,6 +14,10 @@
margin-top: 10px; margin-top: 10px;
} }
.mt-15 {
margin-top: 15px;
}
.faded { .faded {
opacity: 0.5; opacity: 0.5;
} }
@@ -74,11 +78,15 @@
section { section {
padding: 5px; padding: 5px;
padding-bottom: 2px;
margin-top: 5px; margin-top: 5px;
&.inline-h { &.inline-h {
padding: 5px 0px; padding-top: 5px;
padding-left: 0;
padding-right: 0;
} }
} }
input { input {
@@ -121,6 +129,10 @@
.large-padding { .large-padding {
padding: 15px; padding: 15px;
} }
.red {
color: red;
}
} }
.footer-bar-link { .footer-bar-link {

View File

@@ -23,7 +23,7 @@
%label.center-align.block.faded — OR — %label.center-align.block.faded — OR —
%a.block.center-align.medium-text{"ng-if" => "!formData.showAddLinkForm", "ng-click" => "formData.showAddLinkForm = true"} Add sync using secret link %a.block.center-align.medium-text{"ng-if" => "!formData.showAddLinkForm", "ng-click" => "formData.showAddLinkForm = true"} Add sync using secret link
%form{"ng-if" => "formData.showAddLinkForm"} %form{"ng-if" => "formData.showAddLinkForm"}
%input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Secret URL', :required => true, :type => 'url', 'ng-model' => 'formData.url'} %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Secret URL', :required => true, :type => 'url', 'ng-model' => 'formData.secretUrl'}
%button.btn.dark-button.btn-block{"ng-click" => "submitExternalSyncURL()"} %button.btn.dark-button.btn-block{"ng-click" => "submitExternalSyncURL()"}
Add Sync Account Add Sync Account
%a.block.center-align.mt-5{"ng-click" => "formData.showAddLinkForm = false"} Cancel %a.block.center-align.mt-5{"ng-click" => "formData.showAddLinkForm = false"} Cancel

View File

@@ -3,7 +3,7 @@
%div{"ng-if" => "showSection"} %div{"ng-if" => "showSection"}
.small-v-space .small-v-space
%section.white-bg{"ng-repeat" => "provider in syncProviders"} %section.white-bg.medium-padding{"ng-repeat" => "provider in syncProviders"}
%label {{!provider.enabled ? 'Not enabled' : (provider.primary ? 'Main' : 'Secondary')}} %label {{!provider.enabled ? 'Not enabled' : (provider.primary ? 'Main' : 'Secondary')}}
%em{"ng-if" => "provider.keyName"} Using key: {{provider.keyName}} %em{"ng-if" => "provider.keyName"} Using key: {{provider.keyName}}
%p {{provider.url}} %p {{provider.url}}
@@ -16,11 +16,15 @@
{{key.name}} {{key.name}}
%button{"ng-click" => "saveKey(provider)"} Set %button{"ng-click" => "saveKey(provider)"} Set
%div{"ng-if" => "!provider.enabled"} %button.light{"ng-if" => "!provider.enabled || !provider.primary", "ng-click" => "enableSyncProvider(provider, true)"} Set as Main
%button.light{"ng-click" => "enableSyncProvider(provider, true)"} Set as Main %button.light{"ng-if" => "syncProviders.length > 1 && (provider.primary || !provider.enabled)", "ng-click" => "enableSyncProvider(provider, false)"} Add as Secondary
%button.light{"ng-if" => "syncProviders.length > 1", "ng-click" => "enableSyncProvider(provider, false)"} Add as Secondary
%button.light{"ng-if" => "provider.keyName", "ng-click" => "changeEncryptionKey(provider)"} Change Encryption Key %button.light{"ng-if" => "provider.keyName", "ng-click" => "changeEncryptionKey(provider)"} Change Encryption Key
%button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Provider %button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Account
%div{"style" => "height: 30px;", "delay-hide" => "true", "show" => "provider.syncOpInProgress", "delay" => "1000"}
%strong{"style" => "float: left;"} Syncing: {{provider.syncStatus.statusString}} .mt-15{"ng-if" => "provider.error"}
.spinner{"style" => "float: right"} %strong.red Error syncing: {{provider.error.message}}
.mt-15{"style" => "height: 15px;", "delay-hide" => "true", "show" => "provider.syncOpInProgress", "delay" => "1000"}
.spinner{"style" => "float: left; margin-top: 3px; margin-left: 2px;"}
%strong{"style" => "float: left; margin-left: 7px;"} Syncing:
  {{provider.syncStatus.statusString}}

View File

@@ -1,7 +1,7 @@
.footer-bar .footer-bar
.pull-left .pull-left
.footer-bar-link .footer-bar-link
%a{"ng-click" => "ctrl.accountMenuPressed()"} Account %a{"ng-click" => "ctrl.accountMenuPressed()", "ng-class" => "{red: ctrl.error}"} Account
%account-data-menu{"ng-if" => "ctrl.showAccountMenu"} %account-data-menu{"ng-if" => "ctrl.showAccountMenu"}
.footer-bar-link .footer-bar-link
@@ -14,11 +14,12 @@
.pull-right .pull-right
.footer-bar-link .footer-bar-link{"style" => "margin-right: 5px;"}
%div{"ng-if" => "ctrl.lastSyncDate", "style" => "float: left; font-weight: normal; margin-right: 8px;"} %div{"ng-if" => "ctrl.lastSyncDate", "style" => "float: left; font-weight: normal; margin-right: 8px;"}
%span{"ng-if" => "!ctrl.isRefreshing"} %span{"ng-if" => "!ctrl.isRefreshing"}
Last refreshed {{ctrl.lastSyncDate | appDateTime}} Last refreshed {{ctrl.lastSyncDate | appDateTime}}
%span{"ng-if" => "ctrl.isRefreshing"} %span{"ng-if" => "ctrl.isRefreshing"}
.spinner{"style" => "margin-top: 2px;"} .spinner{"style" => "margin-top: 2px;"}
%a{"ng-click" => "ctrl.refreshData()"} Refresh %strong{"ng-if" => "ctrl.offline"} Offline
%a{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"} Refresh