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 });