github.com/kayoticsully/syncthing@v0.8.9-0.20140724133906-c45a2fdc03f8/gui/app.js (about)

     1  // Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
     2  // All rights reserved. Use of this source code is governed by an MIT-style
     3  // license that can be found in the LICENSE file.
     4  
     5  /*jslint browser: true, continue: true, plusplus: true */
     6  /*global $: false, angular: false */
     7  
     8  'use strict';
     9  
    10  var syncthing = angular.module('syncthing', ['pascalprecht.translate']);
    11  var urlbase = 'rest';
    12  
    13  syncthing.config(function ($httpProvider, $translateProvider) {
    14      $httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token';
    15      $httpProvider.defaults.xsrfCookieName = 'CSRF-Token';
    16  
    17      $translateProvider.useStaticFilesLoader({
    18          prefix: 'lang-',
    19          suffix: '.json'
    20      });
    21      $translateProvider.preferredLanguage('en');
    22  });
    23  
    24  syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $location) {
    25      var prevDate = 0;
    26      var getOK = true;
    27      var restarting = false;
    28  
    29      $scope.connections = {};
    30      $scope.config = {};
    31      $scope.myID = '';
    32      $scope.nodes = [];
    33      $scope.configInSync = true;
    34      $scope.protocolChanged = false;
    35      $scope.errors = [];
    36      $scope.seenError = '';
    37      $scope.model = {};
    38      $scope.repos = {};
    39      $scope.reportData = {};
    40      $scope.reportPreview = false;
    41      $scope.upgradeInfo = {};
    42  
    43      $scope.$on("$locationChangeSuccess", function () {
    44          var lang = $location.search().lang;
    45          if (lang) {
    46              $translate.use(lang);
    47          }
    48      });
    49  
    50      $scope.needActions = {
    51          'rm': 'Del',
    52          'rmdir': 'Del (dir)',
    53          'sync': 'Sync',
    54          'touch': 'Update',
    55      }
    56      $scope.needIcons = {
    57          'rm': 'remove',
    58          'rmdir': 'remove',
    59          'sync': 'download',
    60          'touch': 'asterisk',
    61      }
    62  
    63      function getSucceeded() {
    64          if (!getOK) {
    65              $scope.init();
    66              $('#networkError').modal('hide');
    67              getOK = true;
    68          }
    69          if (restarting) {
    70              $scope.init();
    71              $('#restarting').modal('hide');
    72              $('#shutdown').modal('hide');
    73              restarting = false;
    74          }
    75      }
    76  
    77      function getFailed() {
    78          if (restarting) {
    79              return;
    80          }
    81          if (getOK) {
    82              $('#networkError').modal({backdrop: 'static', keyboard: false});
    83              getOK = false;
    84          }
    85      }
    86  
    87      $scope.refresh = function () {
    88          $http.get(urlbase + '/system').success(function (data) {
    89              getSucceeded();
    90              $scope.system = data;
    91          }).error(function () {
    92              getFailed();
    93          });
    94          Object.keys($scope.repos).forEach(function (id) {
    95              if (typeof $scope.model[id] === 'undefined') {
    96                  // Never fetched before
    97                  $http.get(urlbase + '/model?repo=' + encodeURIComponent(id)).success(function (data) {
    98                      $scope.model[id] = data;
    99                  });
   100              } else {
   101                  $http.get(urlbase + '/model/version?repo=' + encodeURIComponent(id)).success(function (data) {
   102                      if (data.version > $scope.model[id].version) {
   103                          $http.get(urlbase + '/model?repo=' + encodeURIComponent(id)).success(function (data) {
   104                              $scope.model[id] = data;
   105                          });
   106                      }
   107                  });
   108              }
   109          });
   110          $http.get(urlbase + '/connections').success(function (data) {
   111              var now = Date.now(),
   112              td = (now - prevDate) / 1000,
   113              id;
   114  
   115              prevDate = now;
   116              for (id in data) {
   117                  if (!data.hasOwnProperty(id)) {
   118                      continue;
   119                  }
   120                  try {
   121                      data[id].inbps = Math.max(0, 8 * (data[id].InBytesTotal - $scope.connections[id].InBytesTotal) / td);
   122                      data[id].outbps = Math.max(0, 8 * (data[id].OutBytesTotal - $scope.connections[id].OutBytesTotal) / td);
   123                  } catch (e) {
   124                      data[id].inbps = 0;
   125                      data[id].outbps = 0;
   126                  }
   127              }
   128              $scope.connections = data;
   129          });
   130          $http.get(urlbase + '/errors').success(function (data) {
   131              $scope.errors = data;
   132          });
   133      };
   134  
   135      $scope.repoStatus = function (repo) {
   136          if (typeof $scope.model[repo] === 'undefined') {
   137              return 'Unknown';
   138          }
   139  
   140          if ($scope.model[repo].invalid !== '') {
   141              return 'Stopped';
   142          }
   143  
   144          var state = '' + $scope.model[repo].state;
   145          state = state[0].toUpperCase() + state.substr(1);
   146  
   147          if (state == "Syncing" || state == "Idle") {
   148              state += " (" + $scope.syncPercentage(repo) + "%)";
   149          }
   150  
   151          return state;
   152      };
   153  
   154      $scope.repoClass = function (repo) {
   155          if (typeof $scope.model[repo] === 'undefined') {
   156              return 'info';
   157          }
   158  
   159          if ($scope.model[repo].invalid !== '') {
   160              return 'danger';
   161          }
   162  
   163          var state = '' + $scope.model[repo].state;
   164          if (state == 'idle') {
   165              return 'success';
   166          }
   167          if (state == 'syncing') {
   168              return 'primary';
   169          }
   170          return 'info';
   171      };
   172  
   173      $scope.syncPercentage = function (repo) {
   174          if (typeof $scope.model[repo] === 'undefined') {
   175              return 100;
   176          }
   177          if ($scope.model[repo].globalBytes === 0) {
   178              return 100;
   179          }
   180  
   181          var pct = 100 * $scope.model[repo].inSyncBytes / $scope.model[repo].globalBytes;
   182          return Math.floor(pct);
   183      };
   184  
   185      $scope.nodeStatus = function (nodeCfg) {
   186          var conn = $scope.connections[nodeCfg.NodeID];
   187          if (conn) {
   188              if (conn.Completion === 100) {
   189                  return 'Up to Date';
   190              } else {
   191                  return 'Syncing (' + conn.Completion + '%)';
   192              }
   193          }
   194  
   195          return 'Disconnected';
   196      };
   197  
   198      $scope.nodeIcon = function (nodeCfg) {
   199          var conn = $scope.connections[nodeCfg.NodeID];
   200          if (conn) {
   201              if (conn.Completion === 100) {
   202                  return 'ok';
   203              } else {
   204                  return 'refresh';
   205              }
   206          }
   207  
   208          return 'minus';
   209      };
   210  
   211      $scope.nodeClass = function (nodeCfg) {
   212          var conn = $scope.connections[nodeCfg.NodeID];
   213          if (conn) {
   214              if (conn.Completion === 100) {
   215                  return 'success';
   216              } else {
   217                  return 'primary';
   218              }
   219          }
   220  
   221          return 'info';
   222      };
   223  
   224      $scope.nodeAddr = function (nodeCfg) {
   225          var conn = $scope.connections[nodeCfg.NodeID];
   226          if (conn) {
   227              return conn.Address;
   228          }
   229          return '?';
   230      };
   231  
   232      $scope.nodeCompletion = function (nodeCfg) {
   233          var conn = $scope.connections[nodeCfg.NodeID];
   234          if (conn) {
   235              return conn.Completion + '%';
   236          }
   237          return '';
   238      };
   239  
   240      $scope.nodeVer = function (nodeCfg) {
   241          if (nodeCfg.NodeID === $scope.myID) {
   242              return $scope.version;
   243          }
   244          var conn = $scope.connections[nodeCfg.NodeID];
   245          if (conn) {
   246              return conn.ClientVersion;
   247          }
   248          return '?';
   249      };
   250  
   251      $scope.findNode = function (nodeID) {
   252          var matches = $scope.nodes.filter(function (n) { return n.NodeID == nodeID; });
   253          if (matches.length != 1) {
   254              return undefined;
   255          }
   256          return matches[0];
   257      };
   258  
   259      $scope.nodeName = function (nodeCfg) {
   260          if (typeof nodeCfg === 'undefined') {
   261              return "";
   262          }
   263          if (nodeCfg.Name) {
   264              return nodeCfg.Name;
   265          }
   266          return nodeCfg.NodeID.substr(0, 6);
   267      };
   268  
   269      $scope.thisNodeName = function () {
   270          var node = $scope.thisNode();
   271          if (typeof node === 'undefined') {
   272              return "(unknown node)";
   273          }
   274          if (node.Name) {
   275              return node.Name;
   276          }
   277          return node.NodeID.substr(0, 6);
   278      };
   279  
   280      $scope.editSettings = function () {
   281          // Make a working copy
   282          $scope.tmpOptions = angular.copy($scope.config.Options);
   283          $scope.tmpOptions.UREnabled = ($scope.tmpOptions.URAccepted > 0);
   284          $scope.tmpGUI = angular.copy($scope.config.GUI);
   285          $('#settings').modal({backdrop: 'static', keyboard: true});
   286      };
   287  
   288      $scope.saveConfig = function() {
   289          var cfg = JSON.stringify($scope.config);
   290          var opts = {headers: {'Content-Type': 'application/json'}};
   291          $http.post(urlbase + '/config', cfg, opts).success(function () {
   292              $http.get(urlbase + '/config/sync').success(function (data) {
   293                  $scope.configInSync = data.configInSync;
   294              });
   295          });
   296      };
   297  
   298      $scope.saveSettings = function () {
   299          // Make sure something changed
   300          var changed = !angular.equals($scope.config.Options, $scope.tmpOptions) ||
   301                        !angular.equals($scope.config.GUI, $scope.tmpGUI);
   302          if (changed) {
   303              // Check if usage reporting has been enabled or disabled
   304              if ($scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted <= 0) {
   305                  $scope.tmpOptions.URAccepted = 1000;
   306              } else if (!$scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted > 0){
   307                  $scope.tmpOptions.URAccepted = -1;
   308              }
   309  
   310              // Check if protocol will need to be changed on restart
   311              if($scope.config.GUI.UseTLS !== $scope.tmpGUI.UseTLS){
   312                  $scope.protocolChanged = true;
   313              }
   314  
   315              // Apply new settings locally
   316              $scope.config.Options = angular.copy($scope.tmpOptions);
   317              $scope.config.GUI = angular.copy($scope.tmpGUI);
   318              $scope.config.Options.ListenAddress = $scope.config.Options.ListenStr.split(',').map(function (x) { return x.trim(); });
   319  
   320              $scope.saveConfig();
   321          }
   322  
   323          $('#settings').modal("hide");
   324      };
   325  
   326      $scope.restart = function () {
   327          restarting = true;
   328          $scope.restartingTitle = "Restarting"
   329          $scope.restartingBody = "Syncthing is restarting."
   330          $('#restarting').modal({backdrop: 'static', keyboard: false});
   331          $http.post(urlbase + '/restart');
   332          $scope.configInSync = true;
   333  
   334          // Switch webpage protocol if needed
   335          if($scope.protocolChanged){
   336              var protocol = 'http';
   337  
   338              if($scope.config.GUI.UseTLS){
   339                 protocol = 'https';
   340              }
   341  
   342              setTimeout(function(){
   343                  window.location.protocol = protocol;
   344              }, 1000);
   345  
   346              $scope.protocolChanged = false;
   347          }
   348      };
   349  
   350      $scope.upgrade = function () {
   351          $scope.restartingTitle = "Upgrading"
   352          $scope.restartingBody = "Syncthing is upgrading."
   353          $('#restarting').modal({backdrop: 'static', keyboard: false});
   354          $http.post(urlbase + '/upgrade').success(function () {
   355              restarting = true;
   356              $scope.restartingBody = "Syncthing is restarting into the new version."
   357          }).error(function () {
   358              $('#restarting').modal('hide');
   359          });
   360      };
   361  
   362      $scope.shutdown = function () {
   363          restarting = true;
   364          $http.post(urlbase + '/shutdown').success(function () {
   365              $('#shutdown').modal({backdrop: 'static', keyboard: false});
   366          });
   367          $scope.configInSync = true;
   368      };
   369  
   370      $scope.editNode = function (nodeCfg) {
   371          $scope.currentNode = $.extend({}, nodeCfg);
   372          $scope.editingExisting = true;
   373          $scope.editingSelf = (nodeCfg.NodeID == $scope.myID);
   374          $scope.currentNode.AddressesStr = nodeCfg.Addresses.join(', ');
   375          $scope.nodeEditor.$setPristine();
   376          $('#editNode').modal({backdrop: 'static', keyboard: true});
   377      };
   378  
   379      $scope.idNode = function () {
   380          $('#idqr').modal('show');
   381      };
   382  
   383      $scope.addNode = function () {
   384          $scope.currentNode = {AddressesStr: 'dynamic'};
   385          $scope.editingExisting = false;
   386          $scope.editingSelf = false;
   387          $scope.nodeEditor.$setPristine();
   388          $('#editNode').modal({backdrop: 'static', keyboard: true});
   389      };
   390  
   391      $scope.deleteNode = function () {
   392          $('#editNode').modal('hide');
   393          if (!$scope.editingExisting) {
   394              return;
   395          }
   396  
   397          $scope.nodes = $scope.nodes.filter(function (n) {
   398              return n.NodeID !== $scope.currentNode.NodeID;
   399          });
   400          $scope.config.Nodes = $scope.nodes;
   401  
   402          for (var id in $scope.repos) {
   403              $scope.repos[id].Nodes = $scope.repos[id].Nodes.filter(function (n) {
   404                  return n.NodeID !== $scope.currentNode.NodeID;
   405              });
   406          }
   407  
   408          $scope.saveConfig();
   409      };
   410  
   411      $scope.saveNode = function () {
   412          var nodeCfg, done, i;
   413  
   414          $('#editNode').modal('hide');
   415          nodeCfg = $scope.currentNode;
   416          nodeCfg.Addresses = nodeCfg.AddressesStr.split(',').map(function (x) { return x.trim(); });
   417  
   418          done = false;
   419          for (i = 0; i < $scope.nodes.length; i++) {
   420              if ($scope.nodes[i].NodeID === nodeCfg.NodeID) {
   421                  $scope.nodes[i] = nodeCfg;
   422                  done = true;
   423                  break;
   424              }
   425          }
   426  
   427          if (!done) {
   428              $scope.nodes.push(nodeCfg);
   429          }
   430  
   431          $scope.nodes.sort(nodeCompare);
   432          $scope.config.Nodes = $scope.nodes;
   433  
   434          $scope.saveConfig();
   435      };
   436  
   437      $scope.otherNodes = function () {
   438          return $scope.nodes.filter(function (n){
   439              return n.NodeID !== $scope.myID;
   440          });
   441      };
   442  
   443      $scope.thisNode = function () {
   444          var i, n;
   445  
   446          for (i = 0; i < $scope.nodes.length; i++) {
   447              n = $scope.nodes[i];
   448              if (n.NodeID === $scope.myID) {
   449                  return n;
   450              }
   451          }
   452      };
   453  
   454      $scope.allNodes = function () {
   455          var nodes = $scope.otherNodes();
   456          nodes.push($scope.thisNode());
   457          return nodes;
   458      };
   459  
   460      $scope.errorList = function () {
   461          return $scope.errors.filter(function (e) {
   462              return e.Time > $scope.seenError;
   463          });
   464      };
   465  
   466      $scope.clearErrors = function () {
   467          $scope.seenError = $scope.errors[$scope.errors.length - 1].Time;
   468          $http.post(urlbase + '/error/clear');
   469      };
   470  
   471      $scope.friendlyNodes = function (str) {
   472          for (var i = 0; i < $scope.nodes.length; i++) {
   473              var cfg = $scope.nodes[i];
   474              str = str.replace(cfg.NodeID, $scope.nodeName(cfg));
   475          }
   476          return str;
   477      };
   478  
   479      $scope.repoList = function () {
   480          return repoList($scope.repos);
   481      };
   482  
   483      $scope.editRepo = function (nodeCfg) {
   484          $scope.currentRepo = angular.copy(nodeCfg);
   485          $scope.currentRepo.selectedNodes = {};
   486          $scope.currentRepo.Nodes.forEach(function (n) {
   487              $scope.currentRepo.selectedNodes[n.NodeID] = true;
   488          });
   489          if ($scope.currentRepo.Versioning && $scope.currentRepo.Versioning.Type === "simple") {
   490              $scope.currentRepo.simpleFileVersioning = true;
   491              $scope.currentRepo.simpleKeep = +$scope.currentRepo.Versioning.Params.keep;
   492          }
   493          $scope.currentRepo.simpleKeep = $scope.currentRepo.simpleKeep || 5;
   494          $scope.editingExisting = true;
   495          $scope.repoEditor.$setPristine();
   496          $('#editRepo').modal({backdrop: 'static', keyboard: true});
   497      };
   498  
   499      $scope.addRepo = function () {
   500          $scope.currentRepo = {selectedNodes: {}};
   501          $scope.editingExisting = false;
   502          $scope.repoEditor.$setPristine();
   503          $('#editRepo').modal({backdrop: 'static', keyboard: true});
   504      };
   505  
   506      $scope.saveRepo = function () {
   507          var repoCfg, done, i;
   508  
   509          $('#editRepo').modal('hide');
   510          repoCfg = $scope.currentRepo;
   511          repoCfg.Nodes = [];
   512          repoCfg.selectedNodes[$scope.myID] = true;
   513          for (var nodeID in repoCfg.selectedNodes) {
   514              if (repoCfg.selectedNodes[nodeID] === true) {
   515                  repoCfg.Nodes.push({NodeID: nodeID});
   516              }
   517          }
   518          delete repoCfg.selectedNodes;
   519  
   520          if (repoCfg.simpleFileVersioning) {
   521              repoCfg.Versioning = {
   522                  'Type': 'simple',
   523                  'Params': {
   524                      'keep': '' + repoCfg.simpleKeep,
   525                  }
   526              };
   527              delete repoCfg.simpleFileVersioning;
   528              delete repoCfg.simpleKeep;
   529          } else {
   530              delete repoCfg.Versioning;
   531          }
   532  
   533          $scope.repos[repoCfg.ID] = repoCfg;
   534          $scope.config.Repositories = repoList($scope.repos);
   535  
   536          $scope.saveConfig();
   537      };
   538  
   539      $scope.sharesRepo = function(repoCfg) {
   540          var names = [];
   541          repoCfg.Nodes.forEach(function (node) {
   542              names.push($scope.nodeName($scope.findNode(node.NodeID)));
   543          });
   544          names.sort();
   545          return names.join(", ");
   546      };
   547  
   548      $scope.deleteRepo = function () {
   549          $('#editRepo').modal('hide');
   550          if (!$scope.editingExisting) {
   551              return;
   552          }
   553  
   554          delete $scope.repos[$scope.currentRepo.ID];
   555          $scope.config.Repositories = repoList($scope.repos);
   556  
   557          $scope.saveConfig();
   558      };
   559  
   560      $scope.setAPIKey = function (cfg) {
   561          cfg.APIKey = randomString(30, 32);
   562      };
   563  
   564      $scope.init = function() {
   565          $http.get(urlbase + '/version').success(function (data) {
   566              $scope.version = data;
   567          });
   568  
   569          $http.get(urlbase + '/system').success(function (data) {
   570              $scope.system = data;
   571              $scope.myID = data.myID;
   572          });
   573  
   574          $http.get(urlbase + '/config').success(function (data) {
   575              $scope.config = data;
   576              $scope.config.Options.ListenStr = $scope.config.Options.ListenAddress.join(', ');
   577  
   578              $scope.nodes = $scope.config.Nodes;
   579              $scope.nodes.sort(nodeCompare);
   580  
   581              $scope.repos = repoMap($scope.config.Repositories);
   582  
   583              $scope.refresh();
   584  
   585              if ($scope.config.Options.URAccepted == 0) {
   586                  // If usage reporting has been neither accepted nor declined,
   587                  // we want to ask the user to make a choice. But we don't want
   588                  // to bug them during initial setup, so we set a cookie with
   589                  // the time of the first visit. When that cookie is present
   590                  // and the time is more than four hours ago, we ask the
   591                  // question.
   592  
   593                  var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1");
   594                  if (!firstVisit) {
   595                      document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30*24*3600;
   596                  } else {
   597                      if (+firstVisit < Date.now() - 4*3600*1000){
   598                          $('#ur').modal({backdrop: 'static', keyboard: false});
   599                      }
   600                  }
   601              }
   602          });
   603  
   604          $http.get(urlbase + '/config/sync').success(function (data) {
   605              $scope.configInSync = data.configInSync;
   606          });
   607  
   608          $http.get(urlbase + '/report').success(function (data) {
   609              $scope.reportData = data;
   610          });
   611  
   612          $http.get(urlbase + '/upgrade').success(function (data) {
   613              $scope.upgradeInfo = data;
   614          }).error(function () {
   615              $scope.upgradeInfo = {};
   616          });
   617      };
   618  
   619      $scope.acceptUR = function () {
   620          $scope.config.Options.URAccepted = 1000; // Larger than the largest existing report version
   621          $scope.saveConfig();
   622          $('#ur').modal('hide');
   623      };
   624  
   625      $scope.declineUR = function () {
   626          $scope.config.Options.URAccepted = -1;
   627          $scope.saveConfig();
   628          $('#ur').modal('hide');
   629      };
   630  
   631      $scope.showNeed = function (repo) {
   632          $scope.neededLoaded = false;
   633          $('#needed').modal({backdrop: 'static', keyboard: true});
   634          $http.get(urlbase + "/need?repo=" + encodeURIComponent(repo)).success(function (data) {
   635              $scope.needed = data;
   636              $scope.neededLoaded = true;
   637          });
   638      };
   639  
   640      $scope.needAction = function (file) {
   641          var fDelete = 4096;
   642          var fDirectory = 16384;
   643  
   644          if ((file.Flags & (fDelete+fDirectory)) === fDelete+fDirectory) {
   645              return 'rmdir';
   646          } else if ((file.Flags & fDelete) === fDelete) {
   647              return 'rm';
   648          } else if ((file.Flags & fDirectory) === fDirectory) {
   649              return 'touch';
   650          } else {
   651              return 'sync';
   652          }
   653      };
   654  
   655      $scope.override = function (repo) {
   656          $http.post(urlbase + "/model/override?repo=" + encodeURIComponent(repo)).success(function () {
   657              $scope.refresh();
   658          });
   659      };
   660  
   661      $scope.about = function () {
   662          $('#about').modal('show');
   663      };
   664  
   665      $scope.init();
   666      setInterval($scope.refresh, 10000);
   667  });
   668  
   669  function nodeCompare(a, b) {
   670      if (typeof a.Name !== 'undefined' && typeof b.Name !== 'undefined') {
   671          if (a.Name < b.Name)
   672              return -1;
   673          return a.Name > b.Name;
   674      }
   675      if (a.NodeID < b.NodeID) {
   676          return -1;
   677      }
   678      return a.NodeID > b.NodeID;
   679  }
   680  
   681  function repoCompare(a, b) {
   682      if (a.Directory < b.Directory) {
   683          return -1;
   684      }
   685      return a.Directory > b.Directory;
   686  }
   687  
   688  function repoMap(l) {
   689      var m = {};
   690      l.forEach(function (r) {
   691          m[r.ID] = r;
   692      });
   693      return m;
   694  }
   695  
   696  function repoList(m) {
   697      var l = [];
   698      for (var id in m) {
   699          l.push(m[id]);
   700      }
   701      l.sort(repoCompare);
   702      return l;
   703  }
   704  
   705  function decimals(val, num) {
   706      var digits, decs;
   707  
   708      if (val === 0) {
   709          return 0;
   710      }
   711  
   712      digits = Math.floor(Math.log(Math.abs(val)) / Math.log(10));
   713      decs = Math.max(0, num - digits);
   714      return decs;
   715  }
   716  
   717  function randomString(len, bits)
   718  {
   719      bits = bits || 36;
   720      var outStr = "", newStr;
   721      while (outStr.length < len)
   722      {
   723          newStr = Math.random().toString(bits).slice(2);
   724          outStr += newStr.slice(0, Math.min(newStr.length, (len - outStr.length)));
   725      }
   726      return outStr.toLowerCase();
   727  }
   728  
   729  syncthing.filter('natural', function () {
   730      return function (input, valid) {
   731          return input.toFixed(decimals(input, valid));
   732      };
   733  });
   734  
   735  syncthing.filter('binary', function () {
   736      return function (input) {
   737          if (input === undefined) {
   738              return '0 ';
   739          }
   740          if (input > 1024 * 1024 * 1024) {
   741              input /= 1024 * 1024 * 1024;
   742              return input.toFixed(decimals(input, 2)) + ' Gi';
   743          }
   744          if (input > 1024 * 1024) {
   745              input /= 1024 * 1024;
   746              return input.toFixed(decimals(input, 2)) + ' Mi';
   747          }
   748          if (input > 1024) {
   749              input /= 1024;
   750              return input.toFixed(decimals(input, 2)) + ' Ki';
   751          }
   752          return Math.round(input) + ' ';
   753      };
   754  });
   755  
   756  syncthing.filter('metric', function () {
   757      return function (input) {
   758          if (input === undefined) {
   759              return '0 ';
   760          }
   761          if (input > 1000 * 1000 * 1000) {
   762              input /= 1000 * 1000 * 1000;
   763              return input.toFixed(decimals(input, 2)) + ' G';
   764          }
   765          if (input > 1000 * 1000) {
   766              input /= 1000 * 1000;
   767              return input.toFixed(decimals(input, 2)) + ' M';
   768          }
   769          if (input > 1000) {
   770              input /= 1000;
   771              return input.toFixed(decimals(input, 2)) + ' k';
   772          }
   773          return Math.round(input) + ' ';
   774      };
   775  });
   776  
   777  syncthing.filter('short', function () {
   778      return function (input) {
   779          return input.substr(0, 6);
   780      };
   781  });
   782  
   783  syncthing.filter('alwaysNumber', function () {
   784      return function (input) {
   785          if (input === undefined) {
   786              return 0;
   787          }
   788          return input;
   789      };
   790  });
   791  
   792  syncthing.filter('shortPath', function () {
   793      return function (input) {
   794          if (input === undefined)
   795              return "";
   796          var parts = input.split(/[\/\\]/);
   797          if (!parts || parts.length <= 3) {
   798              return input;
   799          }
   800          return ".../" + parts.slice(parts.length-2).join("/");
   801      };
   802  });
   803  
   804  syncthing.filter('basename', function () {
   805      return function (input) {
   806          if (input === undefined)
   807              return "";
   808          var parts = input.split(/[\/\\]/);
   809          if (!parts || parts.length < 1) {
   810              return input;
   811          }
   812          return parts[parts.length-1];
   813      };
   814  });
   815  
   816  syncthing.filter('clean', function () {
   817      return function (input) {
   818          return encodeURIComponent(input).replace(/%/g, '');
   819      };
   820  });
   821  
   822  syncthing.directive('optionEditor', function () {
   823      return {
   824          restrict: 'C',
   825          replace: true,
   826          transclude: true,
   827          scope: {
   828              setting: '=setting',
   829          },
   830          template: '<input type="text" ng-model="config.Options[setting.id]"></input>',
   831      };
   832  });
   833  
   834  syncthing.directive('uniqueRepo', function() {
   835      return {
   836          require: 'ngModel',
   837          link: function(scope, elm, attrs, ctrl) {
   838              ctrl.$parsers.unshift(function(viewValue) {
   839                  if (scope.editingExisting) {
   840                      // we shouldn't validate
   841                      ctrl.$setValidity('uniqueRepo', true);
   842                  } else if (scope.repos[viewValue]) {
   843                      // the repo exists already
   844                      ctrl.$setValidity('uniqueRepo', false);
   845                  } else {
   846                      // the repo is unique
   847                      ctrl.$setValidity('uniqueRepo', true);
   848                  }
   849                  return viewValue;
   850              });
   851          }
   852      };
   853  });
   854  
   855  syncthing.directive('validNodeid', function($http) {
   856      return {
   857          require: 'ngModel',
   858          link: function(scope, elm, attrs, ctrl) {
   859              ctrl.$parsers.unshift(function(viewValue) {
   860                  if (scope.editingExisting) {
   861                      // we shouldn't validate
   862                      ctrl.$setValidity('validNodeid', true);
   863                  } else {
   864                      $http.get(urlbase + '/nodeid?id='+viewValue).success(function (resp) {
   865                          if (resp.error) {
   866                              ctrl.$setValidity('validNodeid', false);
   867                          } else {
   868                              scope.currentNode.NodeID = resp.id;
   869                              ctrl.$setValidity('validNodeid', true);
   870                          }
   871                      });
   872                  }
   873                  return viewValue;
   874              });
   875          }
   876      };
   877  });
   878  
   879  syncthing.directive('modal', function () {
   880      return {
   881          restrict: 'E',
   882          templateUrl: 'modal.html',
   883          replace: true,
   884          transclude: true,
   885          scope: {
   886              title: '@',
   887              status: '@',
   888              icon: '@',
   889              close: '@',
   890              large: '@',
   891          },
   892      }
   893  });