From 3b88ee623be8f99b8665ed27b6e875dead882644 Mon Sep 17 00:00:00 2001 From: Dennis Wilson Date: Wed, 26 Nov 2014 13:39:59 +0100 Subject: [PATCH] GUI Rework: reorganized folders and split app.js --- .gitignore | 2 + gui/app.js | 1371 ----------------- gui/{ => assets/css}/overrides.css | 0 gui/{ => assets}/font/raleway-500.woff | Bin gui/{ => assets}/font/raleway.css | 0 gui/{ => assets}/img/favicon.png | Bin gui/{ => assets}/img/logo-text-256.png | Bin gui/{ => assets}/img/logo-text-64.png | Bin gui/{ => assets}/lang/README-FIRST.txt | 0 gui/{ => assets}/lang/lang-be.json | 0 gui/{ => assets}/lang/lang-bg.json | 0 gui/{ => assets}/lang/lang-cs.json | 0 gui/{ => assets}/lang/lang-de.json | 0 gui/{ => assets}/lang/lang-en.json | 0 gui/{ => assets}/lang/lang-fr.json | 0 gui/{ => assets}/lang/lang-hu.json | 0 gui/{ => assets}/lang/lang-it.json | 0 gui/{ => assets}/lang/lang-lt.json | 0 gui/{ => assets}/lang/lang-nb.json | 0 gui/{ => assets}/lang/lang-nn.json | 0 gui/{ => assets}/lang/lang-pl.json | 0 gui/{ => assets}/lang/lang-pt-PT.json | 0 gui/{ => assets}/lang/lang-sv.json | 0 gui/{ => assets}/lang/lang-zh-CN.json | 0 gui/{ => assets}/lang/lang-zh-TW.json | 0 gui/{ => assets}/lang/valid-langs.js | 0 gui/index.html | 49 +- gui/scripts/syncthing/app.js | 154 ++ .../core/controllers/eventController.js | 51 + .../core/controllers/syncthingController.js | 979 ++++++++++++ .../core/directives/identiconDirective.js | 65 + .../core/directives/modalDirective.js | 16 + .../core/directives/uniqueFolderDirective.js | 21 + .../core/directives/validDeviceidDirective.js | 23 + .../core/filters/alwaysNumberFilter.js | 9 + .../syncthing/core/filters/basenameFilter.js | 12 + .../syncthing/core/filters/binaryFilter.js | 21 + .../syncthing/core/filters/metricFilter.js | 21 + .../syncthing/core/filters/naturalFilter.js | 6 + gui/scripts/syncthing/core/module.js | 1 + .../angular/angular-translate-loader.min.js | 0 .../angular/angular-translate.min.js | 0 gui/{ => vendor}/angular/angular.min.js | 0 .../bootstrap/css/bootstrap-theme.min.css | 0 .../bootstrap/css/bootstrap.min.css | 0 .../fonts/glyphicons-halflings-regular.eot | Bin .../fonts/glyphicons-halflings-regular.svg | Bin .../fonts/glyphicons-halflings-regular.ttf | Bin .../fonts/glyphicons-halflings-regular.woff | Bin .../bootstrap/js/bootstrap.min.js | 0 gui/{ => vendor}/jquery/jquery-2.0.3.min.js | 0 internal/auto/gui.files.go | 292 ++-- 52 files changed, 1591 insertions(+), 1502 deletions(-) delete mode 100644 gui/app.js rename gui/{ => assets/css}/overrides.css (100%) rename gui/{ => assets}/font/raleway-500.woff (100%) rename gui/{ => assets}/font/raleway.css (100%) rename gui/{ => assets}/img/favicon.png (100%) rename gui/{ => assets}/img/logo-text-256.png (100%) rename gui/{ => assets}/img/logo-text-64.png (100%) rename gui/{ => assets}/lang/README-FIRST.txt (100%) rename gui/{ => assets}/lang/lang-be.json (100%) rename gui/{ => assets}/lang/lang-bg.json (100%) rename gui/{ => assets}/lang/lang-cs.json (100%) rename gui/{ => assets}/lang/lang-de.json (100%) rename gui/{ => assets}/lang/lang-en.json (100%) rename gui/{ => assets}/lang/lang-fr.json (100%) rename gui/{ => assets}/lang/lang-hu.json (100%) rename gui/{ => assets}/lang/lang-it.json (100%) rename gui/{ => assets}/lang/lang-lt.json (100%) rename gui/{ => assets}/lang/lang-nb.json (100%) rename gui/{ => assets}/lang/lang-nn.json (100%) rename gui/{ => assets}/lang/lang-pl.json (100%) rename gui/{ => assets}/lang/lang-pt-PT.json (100%) rename gui/{ => assets}/lang/lang-sv.json (100%) rename gui/{ => assets}/lang/lang-zh-CN.json (100%) rename gui/{ => assets}/lang/lang-zh-TW.json (100%) rename gui/{ => assets}/lang/valid-langs.js (100%) create mode 100644 gui/scripts/syncthing/app.js create mode 100644 gui/scripts/syncthing/core/controllers/eventController.js create mode 100644 gui/scripts/syncthing/core/controllers/syncthingController.js create mode 100644 gui/scripts/syncthing/core/directives/identiconDirective.js create mode 100644 gui/scripts/syncthing/core/directives/modalDirective.js create mode 100644 gui/scripts/syncthing/core/directives/uniqueFolderDirective.js create mode 100644 gui/scripts/syncthing/core/directives/validDeviceidDirective.js create mode 100644 gui/scripts/syncthing/core/filters/alwaysNumberFilter.js create mode 100644 gui/scripts/syncthing/core/filters/basenameFilter.js create mode 100644 gui/scripts/syncthing/core/filters/binaryFilter.js create mode 100644 gui/scripts/syncthing/core/filters/metricFilter.js create mode 100644 gui/scripts/syncthing/core/filters/naturalFilter.js create mode 100644 gui/scripts/syncthing/core/module.js rename gui/{ => vendor}/angular/angular-translate-loader.min.js (100%) rename gui/{ => vendor}/angular/angular-translate.min.js (100%) rename gui/{ => vendor}/angular/angular.min.js (100%) rename gui/{ => vendor}/bootstrap/css/bootstrap-theme.min.css (100%) rename gui/{ => vendor}/bootstrap/css/bootstrap.min.css (100%) rename gui/{ => vendor}/bootstrap/fonts/glyphicons-halflings-regular.eot (100%) rename gui/{ => vendor}/bootstrap/fonts/glyphicons-halflings-regular.svg (100%) rename gui/{ => vendor}/bootstrap/fonts/glyphicons-halflings-regular.ttf (100%) rename gui/{ => vendor}/bootstrap/fonts/glyphicons-halflings-regular.woff (100%) rename gui/{ => vendor}/bootstrap/js/bootstrap.min.js (100%) rename gui/{ => vendor}/jquery/jquery-2.0.3.min.js (100%) diff --git a/.gitignore b/.gitignore index 89c7c042f..3f9a44c8d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,11 @@ syncthing.exe *.zip *.asc *.sublime* +.idea/ .jshintrc coverage.out files/pidx bin perfstats*.csv coverage.xml +!gui/scripts/syncthing diff --git a/gui/app.js b/gui/app.js deleted file mode 100644 index 56a56350f..000000000 --- a/gui/app.js +++ /dev/null @@ -1,1371 +0,0 @@ -// Copyright (C) 2014 The Syncthing Authors. -// -// This program is free software: you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation, either version 3 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program. If not, see . - -/*jslint browser: true, continue: true, plusplus: true */ -/*global $: false, angular: false, console: false, validLangs: false */ - -'use strict'; - -var syncthing = angular.module('syncthing', ['pascalprecht.translate']); -var urlbase = 'rest'; -var guiVersion = null; - -syncthing.config(function ($httpProvider, $translateProvider) { - $httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token'; - $httpProvider.defaults.xsrfCookieName = 'CSRF-Token'; - $httpProvider.interceptors.push(function() { - return { - response: function(response) { - var responseVersion = response.headers()['x-syncthing-version']; - if (!guiVersion) { - guiVersion = responseVersion; - } else if (guiVersion != responseVersion) { - document.location.reload(true); - } - return response; - } - }; - }); - - $translateProvider.useStaticFilesLoader({ - prefix: 'lang/lang-', - suffix: '.json' - }); -}); - -syncthing.controller('EventCtrl', function ($scope, $http) { - $scope.lastEvent = null; - var lastID = 0; - - var successFn = function (data) { - // When Syncthing restarts while the long polling connection is in - // progress the browser on some platforms returns a 200 (since the - // headers has been flushed with the return code 200), with no data. - // This basically means that the connection has been reset, and the call - // was not actually sucessful. - if (!data) { - errorFn(data); - return; - } - - $scope.$emit('UIOnline'); - - if (lastID > 0) { - data.forEach(function (event) { - console.log("event", event.id, event.type, event.data); - $scope.$emit(event.type, event); - }); - } - - $scope.lastEvent = data[data.length - 1]; - lastID = $scope.lastEvent.id; - - setTimeout(function () { - $http.get(urlbase + '/events?since=' + lastID) - .success(successFn) - .error(errorFn); - }, 500); - }; - - var errorFn = function (data) { - $scope.$emit('UIOffline'); - - setTimeout(function () { - $http.get(urlbase + '/events?limit=1') - .success(successFn) - .error(errorFn); - }, 1000); - }; - - $http.get(urlbase + '/events?limit=1') - .success(successFn) - .error(errorFn); -}); - -syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $location) { - var prevDate = 0; - var getOK = true; - var navigatingAway = false; - var online = false; - var restarting = false; - - $scope.completion = {}; - $scope.config = {}; - $scope.configInSync = true; - $scope.connections = {}; - $scope.errors = []; - $scope.model = {}; - $scope.myID = ''; - $scope.devices = []; - $scope.protocolChanged = false; - $scope.reportData = {}; - $scope.reportPreview = false; - $scope.folders = {}; - $scope.seenError = ''; - $scope.upgradeInfo = null; - $scope.stats = {}; - $scope.progress = {}; - - $http.get(urlbase + "/lang").success(function (langs) { - // Find the first language in the list provided by the user's browser - // that is a prefix of a language we have available. That is, "en" - // sent by the browser will match "en" or "en-US", while "zh-TW" will - // match only "zh-TW" and not "zh-CN". - - var lang, matching; - for (var i = 0; i < langs.length; i++) { - lang = langs[i]; - if (lang.length < 2) { - continue; - } - matching = validLangs.filter(function (possibleLang) { - // The langs returned by the /rest/langs call will be in lower - // case. We compare to the lowercase version of the language - // code we have as well. - possibleLang = possibleLang.toLowerCase(); - if (possibleLang.length > lang.length) { - return possibleLang.indexOf(lang) === 0; - } else { - return lang.indexOf(possibleLang) === 0; - } - }); - if (matching.length >= 1) { - $translate.use(matching[0]); - return; - } - } - // Fallback if nothing matched - $translate.use("en"); - }); - - $(window).bind('beforeunload', function () { - navigatingAway = true; - }); - - $scope.$on("$locationChangeSuccess", function () { - var lang = $location.search().lang; - if (lang) { - $translate.use(lang); - } - }); - - $scope.needActions = { - 'rm': 'Del', - 'rmdir': 'Del (dir)', - 'sync': 'Sync', - 'touch': 'Update', - }; - $scope.needIcons = { - 'rm': 'remove', - 'rmdir': 'remove', - 'sync': 'download', - 'touch': 'asterisk', - }; - - $scope.$on('UIOnline', function (event, arg) { - if (online && !restarting) { - return; - } - - console.log('UIOnline'); - $scope.init(); - online = true; - restarting = false; - $('#networkError').modal('hide'); - $('#restarting').modal('hide'); - $('#shutdown').modal('hide'); - }); - - $scope.$on('UIOffline', function (event, arg) { - if (navigatingAway || !online) { - return; - } - - console.log('UIOffline'); - online = false; - if (!restarting) { - $('#networkError').modal(); - } - }); - - $scope.$on('StateChanged', function (event, arg) { - var data = arg.data; - if ($scope.model[data.folder]) { - $scope.model[data.folder].state = data.to; - } - }); - - $scope.$on('LocalIndexUpdated', function (event, arg) { - var data = arg.data; - refreshFolder(data.folder); - - // Update completion status for all devices that we share this folder with. - $scope.folders[data.folder].Devices.forEach(function (deviceCfg) { - refreshCompletion(deviceCfg.DeviceID, data.folder); - }); - }); - - $scope.$on('RemoteIndexUpdated', function (event, arg) { - var data = arg.data; - refreshFolder(data.folder); - refreshCompletion(data.device, data.folder); - }); - - $scope.$on('DeviceDisconnected', function (event, arg) { - delete $scope.connections[arg.data.id]; - refreshDeviceStats(); - }); - - $scope.$on('DeviceConnected', function (event, arg) { - if (!$scope.connections[arg.data.id]) { - $scope.connections[arg.data.id] = { - inbps: 0, - outbps: 0, - InBytesTotal: 0, - OutBytesTotal: 0, - Address: arg.data.addr, - }; - $scope.completion[arg.data.id] = { - _total: 100, - }; - } - }); - - $scope.$on('ConfigLoaded', function (event) { - if ($scope.config.Options.URAccepted === 0) { - // If usage reporting has been neither accepted nor declined, - // we want to ask the user to make a choice. But we don't want - // to bug them during initial setup, so we set a cookie with - // the time of the first visit. When that cookie is present - // and the time is more than four hours ago, we ask the - // question. - - var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1"); - if (!firstVisit) { - document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30 * 24 * 3600; - } else { - if (+firstVisit < Date.now() - 4 * 3600 * 1000) { - $('#ur').modal(); - } - } - } - }); - - $scope.$on('ConfigSaved', function (event, arg) { - updateLocalConfig(arg.data); - - $http.get(urlbase + '/config/sync').success(function (data) { - $scope.configInSync = data.configInSync; - }); - }); - - $scope.$on('DownloadProgress', function (event, arg) { - var stats = arg.data; - var progress = {}; - for(var folder in stats){ - refreshFolder(folder); - progress[folder] = {}; - for(var file in stats[folder]){ - var s = stats[folder][file]; - var reused = Math.floor(100 * s.Reused / s.Total); - var copiedFromOrigin = Math.floor(100 * s.CopiedFromOrigin / s.Total); - var copiedFromElsewhere = Math.floor(100 * s.CopiedFromElsewhere / s.Total); - var pulled = Math.floor(100 * s.Pulled / s.Total); - var pulling = Math.floor(100 * s.Pulling / s.Total); - // We can do the following, because if s.Pulling > 0, than reused + copied + pulled < 100 because off rounding them down. - // We do this to show which files are currently being pulled - if (s.Pulling && pulling == 0) { - pulling = 1; - } - progress[folder][file] = { - Reused: reused, - CopiedFromOrigin: copiedFromOrigin, - CopiedFromElsewhere: copiedFromElsewhere, - Pulled: pulled, - Pulling: pulling, - BytesTotal: s.BytesTotal, - BytesDone: s.BytesDone, - }; - } - } - for(var folder in $scope.progress){ - if (!(folder in progress)) { - refreshFolder(folder); - if ($scope.neededFolder == folder) { - refreshNeed(folder); - } - } else if ($scope.neededFolder == folder) { - for(file in $scope.progress[folder]){ - if (!(file in progress[folder])) { - refreshNeed(folder); - break; - } - } - } - } - $scope.progress = progress; - console.log("DownloadProgress", $scope.progress); - }); - - var debouncedFuncs = {}; - - function refreshFolder(folder) { - var key = "refreshFolder" + folder; - if (!debouncedFuncs[key]) { - debouncedFuncs[key] = debounce(function () { - $http.get(urlbase + '/model?folder=' + encodeURIComponent(folder)).success(function (data) { - $scope.model[folder] = data; - console.log("refreshFolder", folder, data); - }); - }, 1000, true); - } - debouncedFuncs[key](); - } - - function updateLocalConfig(config) { - var hasConfig = !isEmptyObject($scope.config); - - $scope.config = config; - $scope.config.Options.ListenStr = $scope.config.Options.ListenAddress.join(', '); - - $scope.devices = $scope.config.Devices; - $scope.devices.forEach(function (deviceCfg) { - $scope.completion[deviceCfg.DeviceID] = { - _total: 100, - }; - }); - $scope.devices.sort(deviceCompare); - - $scope.folders = folderMap($scope.config.Folders); - Object.keys($scope.folders).forEach(function (folder) { - refreshFolder(folder); - $scope.folders[folder].Devices.forEach(function (deviceCfg) { - refreshCompletion(deviceCfg.DeviceID, folder); - }); - }); - - if (!hasConfig) { - $scope.$emit('ConfigLoaded'); - } - } - - function refreshSystem() { - $http.get(urlbase + '/system').success(function (data) { - $scope.myID = data.myID; - $scope.system = data; - console.log("refreshSystem", data); - }); - } - - function refreshCompletion(device, folder) { - if (device === $scope.myID) { - return; - } - - var key = "refreshCompletion" + device + folder; - if (!debouncedFuncs[key]) { - debouncedFuncs[key] = debounce(function () { - $http.get(urlbase + '/completion?device=' + device + '&folder=' + encodeURIComponent(folder)).success(function (data) { - if (!$scope.completion[device]) { - $scope.completion[device] = {}; - } - $scope.completion[device][folder] = data.completion; - - var tot = 0, - cnt = 0; - for (var cmp in $scope.completion[device]) { - if (cmp === "_total") { - continue; - } - tot += $scope.completion[device][cmp]; - cnt += 1; - } - $scope.completion[device]._total = tot / cnt; - - console.log("refreshCompletion", device, folder, $scope.completion[device]); - }); - }, 1000, true); - } - debouncedFuncs[key](); - } - - function refreshConnectionStats() { - $http.get(urlbase + '/connections').success(function (data) { - var now = Date.now(), - td = (now - prevDate) / 1000, - id; - - prevDate = now; - for (id in data) { - if (!data.hasOwnProperty(id)) { - continue; - } - try { - data[id].inbps = Math.max(0, 8 * (data[id].InBytesTotal - $scope.connections[id].InBytesTotal) / td); - data[id].outbps = Math.max(0, 8 * (data[id].OutBytesTotal - $scope.connections[id].OutBytesTotal) / td); - } catch (e) { - data[id].inbps = 0; - data[id].outbps = 0; - } - } - $scope.connections = data; - console.log("refreshConnections", data); - }); - } - - function refreshErrors() { - $http.get(urlbase + '/errors').success(function (data) { - $scope.errors = data.errors; - console.log("refreshErrors", data); - }); - } - - function refreshConfig() { - $http.get(urlbase + '/config').success(function (data) { - updateLocalConfig(data); - console.log("refreshConfig", data); - }); - - $http.get(urlbase + '/config/sync').success(function (data) { - $scope.configInSync = data.configInSync; - }); - } - - function refreshNeed(folder) { - $http.get(urlbase + "/need?folder=" + encodeURIComponent(folder)).success(function (data) { - if ($scope.neededFolder == folder) { - console.log("refreshNeed", folder, data); - $scope.needed = data; - } - }); - } - - var refreshDeviceStats = debounce(function () { - $http.get(urlbase + "/stats/device").success(function (data) { - $scope.stats = data; - for (var device in $scope.stats) { - $scope.stats[device].LastSeen = new Date($scope.stats[device].LastSeen); - $scope.stats[device].LastSeenDays = (new Date() - $scope.stats[device].LastSeen) / 1000 / 86400; - } - console.log("refreshDeviceStats", data); - }); - }, 500); - - $scope.init = function () { - refreshSystem(); - refreshConfig(); - refreshConnectionStats(); - refreshDeviceStats(); - - $http.get(urlbase + '/version').success(function (data) { - $scope.version = data.version; - }); - - $http.get(urlbase + '/report').success(function (data) { - $scope.reportData = data; - }); - - $http.get(urlbase + '/upgrade').success(function (data) { - $scope.upgradeInfo = data; - }).error(function () { - $scope.upgradeInfo = null; - }); - }; - - $scope.refresh = function () { - refreshSystem(); - refreshConnectionStats(); - refreshErrors(); - }; - - $scope.folderStatus = function (folder) { - if (typeof $scope.model[folder] === 'undefined') { - return 'unknown'; - } - - if ($scope.model[folder].invalid !== '') { - return 'stopped'; - } - - return '' + $scope.model[folder].state; - }; - - $scope.folderClass = function (folder) { - if (typeof $scope.model[folder] === 'undefined') { - return 'info'; - } - - if ($scope.model[folder].invalid !== '') { - return 'danger'; - } - - var state = '' + $scope.model[folder].state; - if (state == 'idle') { - return 'success'; - } - if (state == 'syncing') { - return 'primary'; - } - if (state == 'scanning') { - return 'primary'; - } - return 'info'; - }; - - $scope.syncPercentage = function (folder) { - if (typeof $scope.model[folder] === 'undefined') { - return 100; - } - if ($scope.model[folder].globalBytes === 0) { - return 100; - } - - var pct = 100 * $scope.model[folder].inSyncBytes / $scope.model[folder].globalBytes; - return Math.floor(pct); - }; - - $scope.deviceIcon = function (deviceCfg) { - if ($scope.connections[deviceCfg.DeviceID]) { - if ($scope.completion[deviceCfg.DeviceID] && $scope.completion[deviceCfg.DeviceID]._total === 100) { - return 'ok'; - } else { - return 'refresh'; - } - } - - return 'minus'; - }; - - $scope.deviceClass = function (deviceCfg) { - if ($scope.connections[deviceCfg.DeviceID]) { - if ($scope.completion[deviceCfg.DeviceID] && $scope.completion[deviceCfg.DeviceID]._total === 100) { - return 'success'; - } else { - return 'primary'; - } - } - - return 'info'; - }; - - $scope.deviceAddr = function (deviceCfg) { - var conn = $scope.connections[deviceCfg.DeviceID]; - if (conn) { - return conn.Address; - } - return '?'; - }; - - $scope.deviceCompletion = function (deviceCfg) { - var conn = $scope.connections[deviceCfg.DeviceID]; - if (conn) { - return conn.Completion + '%'; - } - return ''; - }; - - $scope.findDevice = function (deviceID) { - var matches = $scope.devices.filter(function (n) { - return n.DeviceID == deviceID; - }); - if (matches.length != 1) { - return undefined; - } - return matches[0]; - }; - - $scope.deviceName = function (deviceCfg) { - if (typeof deviceCfg === 'undefined') { - return ""; - } - if (deviceCfg.Name) { - return deviceCfg.Name; - } - return deviceCfg.DeviceID.substr(0, 6); - }; - - $scope.thisDeviceName = function () { - var device = $scope.thisDevice(); - if (typeof device === 'undefined') { - return "(unknown device)"; - } - if (device.Name) { - return device.Name; - } - return device.DeviceID.substr(0, 6); - }; - - $scope.editSettings = function () { - // Make a working copy - $scope.tmpOptions = angular.copy($scope.config.Options); - $scope.tmpOptions.UREnabled = ($scope.tmpOptions.URAccepted > 0); - $scope.tmpOptions.DeviceName = $scope.thisDevice().Name; - $scope.tmpOptions.AutoUpgradeEnabled = ($scope.tmpOptions.AutoUpgradeIntervalH > 0); - $scope.tmpGUI = angular.copy($scope.config.GUI); - $('#settings').modal(); - }; - - $scope.saveConfig = function () { - var cfg = JSON.stringify($scope.config); - var opts = { - headers: { - 'Content-Type': 'application/json' - } - }; - $http.post(urlbase + '/config', cfg, opts).success(function () { - $http.get(urlbase + '/config/sync').success(function (data) { - $scope.configInSync = data.configInSync; - }); - }); - }; - - $scope.saveSettings = function () { - // Make sure something changed - var changed = !angular.equals($scope.config.Options, $scope.tmpOptions) || - !angular.equals($scope.config.GUI, $scope.tmpGUI); - if (changed) { - // Check if usage reporting has been enabled or disabled - if ($scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted <= 0) { - $scope.tmpOptions.URAccepted = 1000; - } else if (!$scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted > 0) { - $scope.tmpOptions.URAccepted = -1; - } - - // Check if auto-upgrade has been enabled or disabled - if ($scope.tmpOptions.AutoUpgradeEnabled) { - $scope.tmpOptions.AutoUpgradeIntervalH = $scope.tmpOptions.AutoUpgradeIntervalH || 12; - } else { - $scope.tmpOptions.AutoUpgradeIntervalH = 0; - } - - // Check if protocol will need to be changed on restart - if ($scope.config.GUI.UseTLS !== $scope.tmpGUI.UseTLS) { - $scope.protocolChanged = true; - } - - // Apply new settings locally - $scope.thisDevice().Name = $scope.tmpOptions.DeviceName; - $scope.config.Options = angular.copy($scope.tmpOptions); - $scope.config.GUI = angular.copy($scope.tmpGUI); - $scope.config.Options.ListenAddress = $scope.config.Options.ListenStr.split(',').map(function (x) { - return x.trim(); - }); - - $scope.saveConfig(); - } - - $('#settings').modal("hide"); - }; - - $scope.restart = function () { - restarting = true; - $('#restarting').modal(); - $http.post(urlbase + '/restart'); - $scope.configInSync = true; - - // Switch webpage protocol if needed - if ($scope.protocolChanged) { - var protocol = 'http'; - - if ($scope.config.GUI.UseTLS) { - protocol = 'https'; - } - - setTimeout(function () { - window.location.protocol = protocol; - }, 2500); - - $scope.protocolChanged = false; - } - }; - - $scope.upgrade = function () { - restarting = true; - $('#upgrading').modal(); - $http.post(urlbase + '/upgrade').success(function () { - $('#restarting').modal(); - $('#upgrading').modal('hide'); - }).error(function () { - $('#upgrading').modal('hide'); - }); - }; - - $scope.shutdown = function () { - restarting = true; - $http.post(urlbase + '/shutdown').success(function () { - $('#shutdown').modal(); - }); - $scope.configInSync = true; - }; - - $scope.editDevice = function (deviceCfg) { - $scope.currentDevice = $.extend({}, deviceCfg); - $scope.editingExisting = true; - $scope.editingSelf = (deviceCfg.DeviceID == $scope.myID); - $scope.currentDevice.AddressesStr = deviceCfg.Addresses.join(', '); - $scope.deviceEditor.$setPristine(); - $('#editDevice').modal(); - }; - - $scope.idDevice = function () { - $('#idqr').modal('show'); - }; - - $scope.addDevice = function () { - $http.get(urlbase + '/discovery') - .success(function (registry) { - $scope.discovery = registry; - }) - .then(function () { - $scope.currentDevice = { - AddressesStr: 'dynamic', - Compression: true, - Introducer: false - }; - $scope.editingExisting = false; - $scope.editingSelf = false; - $scope.deviceEditor.$setPristine(); - $('#editDevice').modal(); - }); - }; - - $scope.deleteDevice = function () { - $('#editDevice').modal('hide'); - if (!$scope.editingExisting) { - return; - } - - $scope.devices = $scope.devices.filter(function (n) { - return n.DeviceID !== $scope.currentDevice.DeviceID; - }); - $scope.config.Devices = $scope.devices; - - for (var id in $scope.folders) { - $scope.folders[id].Devices = $scope.folders[id].Devices.filter(function (n) { - return n.DeviceID !== $scope.currentDevice.DeviceID; - }); - } - - $scope.saveConfig(); - }; - - $scope.saveDevice = function () { - var deviceCfg, done, i; - - $('#editDevice').modal('hide'); - deviceCfg = $scope.currentDevice; - deviceCfg.Addresses = deviceCfg.AddressesStr.split(',').map(function (x) { - return x.trim(); - }); - - done = false; - for (i = 0; i < $scope.devices.length; i++) { - if ($scope.devices[i].DeviceID === deviceCfg.DeviceID) { - $scope.devices[i] = deviceCfg; - done = true; - break; - } - } - - if (!done) { - $scope.devices.push(deviceCfg); - } - - $scope.devices.sort(deviceCompare); - $scope.config.Devices = $scope.devices; - - $scope.saveConfig(); - }; - - $scope.otherDevices = function () { - return $scope.devices.filter(function (n) { - return n.DeviceID !== $scope.myID; - }); - }; - - $scope.thisDevice = function () { - var i, n; - - for (i = 0; i < $scope.devices.length; i++) { - n = $scope.devices[i]; - if (n.DeviceID === $scope.myID) { - return n; - } - } - }; - - $scope.allDevices = function () { - var devices = $scope.otherDevices(); - devices.push($scope.thisDevice()); - return devices; - }; - - $scope.errorList = function () { - return $scope.errors.filter(function (e) { - return e.Time > $scope.seenError; - }); - }; - - $scope.clearErrors = function () { - $scope.seenError = $scope.errors[$scope.errors.length - 1].Time; - $http.post(urlbase + '/error/clear'); - }; - - $scope.friendlyDevices = function (str) { - for (var i = 0; i < $scope.devices.length; i++) { - var cfg = $scope.devices[i]; - str = str.replace(cfg.DeviceID, $scope.deviceName(cfg)); - } - return str; - }; - - $scope.folderList = function () { - return folderList($scope.folders); - }; - - $scope.directoryList = []; - - $scope.$watch('currentFolder.Path', function (newvalue) { - $http.get( - urlbase + '/autocomplete/directory', - { - params: { current: newvalue } - } - ).success(function (data) { - $scope.directoryList = data; - }); - }); - - $scope.editFolder = function (deviceCfg) { - $scope.currentFolder = angular.copy(deviceCfg); - $scope.currentFolder.selectedDevices = {}; - $scope.currentFolder.Devices.forEach(function (n) { - $scope.currentFolder.selectedDevices[n.DeviceID] = true; - }); - if ($scope.currentFolder.Versioning && $scope.currentFolder.Versioning.Type === "simple") { - $scope.currentFolder.simpleFileVersioning = true; - $scope.currentFolder.FileVersioningSelector = "simple"; - $scope.currentFolder.simpleKeep = +$scope.currentFolder.Versioning.Params.keep; - } else if ($scope.currentFolder.Versioning && $scope.currentFolder.Versioning.Type === "staggered") { - $scope.currentFolder.staggeredFileVersioning = true; - $scope.currentFolder.FileVersioningSelector = "staggered"; - $scope.currentFolder.staggeredMaxAge = Math.floor(+$scope.currentFolder.Versioning.Params.maxAge / 86400); - $scope.currentFolder.staggeredCleanInterval = +$scope.currentFolder.Versioning.Params.cleanInterval; - $scope.currentFolder.staggeredVersionsPath = $scope.currentFolder.Versioning.Params.versionsPath; - } else { - $scope.currentFolder.FileVersioningSelector = "none"; - } - $scope.currentFolder.simpleKeep = $scope.currentFolder.simpleKeep || 5; - $scope.currentFolder.staggeredCleanInterval = $scope.currentFolder.staggeredCleanInterval || 3600; - $scope.currentFolder.staggeredVersionsPath = $scope.currentFolder.staggeredVersionsPath || ""; - - // staggeredMaxAge can validly be zero, which we should not replace - // with the default value of 365. So only set the default if it's - // actually undefined. - if (typeof $scope.currentFolder.staggeredMaxAge === 'undefined') { - $scope.currentFolder.staggeredMaxAge = 365; - } - - $scope.editingExisting = true; - $scope.folderEditor.$setPristine(); - $('#editFolder').modal(); - }; - - $scope.addFolder = function () { - $scope.currentFolder = { - selectedDevices: {} - }; - $scope.currentFolder.RescanIntervalS = 60; - $scope.currentFolder.FileVersioningSelector = "none"; - $scope.currentFolder.simpleKeep = 5; - $scope.currentFolder.staggeredMaxAge = 365; - $scope.currentFolder.staggeredCleanInterval = 3600; - $scope.currentFolder.staggeredVersionsPath = ""; - $scope.editingExisting = false; - $scope.folderEditor.$setPristine(); - $('#editFolder').modal(); - }; - - $scope.saveFolder = function () { - var folderCfg, done, i; - - $('#editFolder').modal('hide'); - folderCfg = $scope.currentFolder; - folderCfg.Devices = []; - folderCfg.selectedDevices[$scope.myID] = true; - for (var deviceID in folderCfg.selectedDevices) { - if (folderCfg.selectedDevices[deviceID] === true) { - folderCfg.Devices.push({ - DeviceID: deviceID - }); - } - } - delete folderCfg.selectedDevices; - - if (folderCfg.FileVersioningSelector === "simple") { - folderCfg.Versioning = { - 'Type': 'simple', - 'Params': { - 'keep': '' + folderCfg.simpleKeep, - } - }; - delete folderCfg.simpleFileVersioning; - delete folderCfg.simpleKeep; - } else if (folderCfg.FileVersioningSelector === "staggered") { - folderCfg.Versioning = { - 'Type': 'staggered', - 'Params': { - 'maxAge': '' + (folderCfg.staggeredMaxAge * 86400), - 'cleanInterval': '' + folderCfg.staggeredCleanInterval, - 'versionsPath': '' + folderCfg.staggeredVersionsPath, - } - }; - delete folderCfg.staggeredFileVersioning; - delete folderCfg.staggeredMaxAge; - delete folderCfg.staggeredCleanInterval; - delete folderCfg.staggeredVersionsPath; - - } else { - delete folderCfg.Versioning; - } - - $scope.folders[folderCfg.ID] = folderCfg; - $scope.config.Folders = folderList($scope.folders); - - $scope.saveConfig(); - }; - - $scope.sharesFolder = function (folderCfg) { - var names = []; - folderCfg.Devices.forEach(function (device) { - if (device.DeviceID != $scope.myID) { - names.push($scope.deviceName($scope.findDevice(device.DeviceID))); - } - }); - names.sort(); - return names.join(", "); - }; - - $scope.deleteFolder = function () { - $('#editFolder').modal('hide'); - if (!$scope.editingExisting) { - return; - } - - delete $scope.folders[$scope.currentFolder.ID]; - $scope.config.Folders = folderList($scope.folders); - - $scope.saveConfig(); - }; - - $scope.editIgnores = function () { - if (!$scope.editingExisting) { - return; - } - - $('#editIgnoresButton').attr('disabled', 'disabled'); - $http.get(urlbase + '/ignores?folder=' + encodeURIComponent($scope.currentFolder.ID)) - .success(function (data) { - data.ignore = data.ignore || []; - - $('#editFolder').modal('hide'); - var textArea = $('#editIgnores textarea'); - - textArea.val(data.ignore.join('\n')); - - $('#editIgnores').modal() - .on('hidden.bs.modal', function () { - $('#editFolder').modal(); - }) - .on('shown.bs.modal', function () { - textArea.focus(); - }); - }) - .then(function () { - $('#editIgnoresButton').removeAttr('disabled'); - }); - }; - - $scope.saveIgnores = function () { - if (!$scope.editingExisting) { - return; - } - - $http.post(urlbase + '/ignores?folder=' + encodeURIComponent($scope.currentFolder.ID), { - ignore: $('#editIgnores textarea').val().split('\n') - }); - }; - - $scope.setAPIKey = function (cfg) { - cfg.APIKey = randomString(30, 32); - }; - - $scope.showURPreview = function () { - $('#settings').modal('hide'); - $('#urPreview').modal().on('hidden.bs.modal', function () { - $('#settings').modal(); - }); - }; - - $scope.acceptUR = function () { - $scope.config.Options.URAccepted = 1000; // Larger than the largest existing report version - $scope.saveConfig(); - $('#ur').modal('hide'); - }; - - $scope.declineUR = function () { - $scope.config.Options.URAccepted = -1; - $scope.saveConfig(); - $('#ur').modal('hide'); - }; - - $scope.showNeed = function (folder) { - $scope.neededFolder = folder; - refreshNeed(folder); - $('#needed').modal().on('hidden.bs.modal', function(){ - $scope.neededFolder = undefined; - $scope.needed = undefined; - }); - }; - - $scope.needAction = function (file) { - var fDelete = 4096; - var fDirectory = 16384; - - if ((file.Flags & (fDelete + fDirectory)) === fDelete + fDirectory) { - return 'rmdir'; - } else if ((file.Flags & fDelete) === fDelete) { - return 'rm'; - } else if ((file.Flags & fDirectory) === fDirectory) { - return 'touch'; - } else { - return 'sync'; - } - }; - - $scope.override = function (folder) { - $http.post(urlbase + "/model/override?folder=" + encodeURIComponent(folder)); - }; - - $scope.about = function () { - $('#about').modal('show'); - }; - - $scope.showReportPreview = function () { - $scope.reportPreview = true; - }; - - $scope.rescanFolder = function (folder) { - $http.post(urlbase + "/scan?folder=" + encodeURIComponent(folder)); - }; - - $scope.init(); - setInterval($scope.refresh, 10000); -}); - -function deviceCompare(a, b) { - if (typeof a.Name !== 'undefined' && typeof b.Name !== 'undefined') { - if (a.Name < b.Name) - return -1; - return a.Name > b.Name; - } - if (a.DeviceID < b.DeviceID) { - return -1; - } - return a.DeviceID > b.DeviceID; -} - -function folderCompare(a, b) { - if (a.ID < b.ID) { - return -1; - } - return a.ID > b.ID; -} - -function folderMap(l) { - var m = {}; - l.forEach(function (r) { - m[r.ID] = r; - }); - return m; -} - -function folderList(m) { - var l = []; - for (var id in m) { - l.push(m[id]); - } - l.sort(folderCompare); - return l; -} - -function decimals(val, num) { - var digits, decs; - - if (val === 0) { - return 0; - } - - digits = Math.floor(Math.log(Math.abs(val)) / Math.log(10)); - decs = Math.max(0, num - digits); - return decs; -} - -function randomString(len, bits) { - bits = bits || 36; - var outStr = "", - newStr; - while (outStr.length < len) { - newStr = Math.random().toString(bits).slice(2); - outStr += newStr.slice(0, Math.min(newStr.length, (len - outStr.length))); - } - return outStr.toLowerCase(); -} - -function isEmptyObject(obj) { - var name; - for (name in obj) { - return false; - } - return true; -} - -function debounce(func, wait) { - var timeout, args, context, timestamp, result, again; - - var later = function () { - var last = Date.now() - timestamp; - if (last < wait) { - timeout = setTimeout(later, wait - last); - } else { - timeout = null; - if (again) { - again = false; - result = func.apply(context, args); - context = args = null; - } - } - }; - - return function () { - context = this; - args = arguments; - timestamp = Date.now(); - var callNow = !timeout; - if (!timeout) { - timeout = setTimeout(later, wait); - result = func.apply(context, args); - context = args = null; - } else { - again = true; - } - - return result; - }; -} - -syncthing.filter('natural', function () { - return function (input, valid) { - return input.toFixed(decimals(input, valid)); - }; -}); - -syncthing.filter('binary', function () { - return function (input) { - if (input === undefined) { - return '0 '; - } - if (input > 1024 * 1024 * 1024) { - input /= 1024 * 1024 * 1024; - return input.toFixed(decimals(input, 2)) + ' Gi'; - } - if (input > 1024 * 1024) { - input /= 1024 * 1024; - return input.toFixed(decimals(input, 2)) + ' Mi'; - } - if (input > 1024) { - input /= 1024; - return input.toFixed(decimals(input, 2)) + ' Ki'; - } - return Math.round(input) + ' '; - }; -}); - -syncthing.filter('metric', function () { - return function (input) { - if (input === undefined) { - return '0 '; - } - if (input > 1000 * 1000 * 1000) { - input /= 1000 * 1000 * 1000; - return input.toFixed(decimals(input, 2)) + ' G'; - } - if (input > 1000 * 1000) { - input /= 1000 * 1000; - return input.toFixed(decimals(input, 2)) + ' M'; - } - if (input > 1000) { - input /= 1000; - return input.toFixed(decimals(input, 2)) + ' k'; - } - return Math.round(input) + ' '; - }; -}); - -syncthing.filter('alwaysNumber', function () { - return function (input) { - if (input === undefined) { - return 0; - } - return input; - }; -}); - -syncthing.filter('basename', function () { - return function (input) { - if (input === undefined) - return ""; - var parts = input.split(/[\/\\]/); - if (!parts || parts.length < 1) { - return input; - } - return parts[parts.length - 1]; - }; -}); - -syncthing.directive('uniqueFolder', function () { - return { - require: 'ngModel', - link: function (scope, elm, attrs, ctrl) { - ctrl.$parsers.unshift(function (viewValue) { - if (scope.editingExisting) { - // we shouldn't validate - ctrl.$setValidity('uniqueFolder', true); - } else if (scope.folders[viewValue]) { - // the folder exists already - ctrl.$setValidity('uniqueFolder', false); - } else { - // the folder is unique - ctrl.$setValidity('uniqueFolder', true); - } - return viewValue; - }); - } - }; -}); - -syncthing.directive('validDeviceid', function ($http) { - return { - require: 'ngModel', - link: function (scope, elm, attrs, ctrl) { - ctrl.$parsers.unshift(function (viewValue) { - if (scope.editingExisting) { - // we shouldn't validate - ctrl.$setValidity('validDeviceid', true); - } else { - $http.get(urlbase + '/deviceid?id=' + viewValue).success(function (resp) { - if (resp.error) { - ctrl.$setValidity('validDeviceid', false); - } else { - ctrl.$setValidity('validDeviceid', true); - } - }); - } - return viewValue; - }); - } - }; -}); - -syncthing.directive('modal', function () { - return { - restrict: 'E', - templateUrl: 'modal.html', - replace: true, - transclude: true, - scope: { - title: '@', - status: '@', - icon: '@', - close: '@', - large: '@', - }, - }; -}); - -syncthing.directive('identicon', ['$window', function ($window) { - var svgNS = 'http://www.w3.org/2000/svg'; - - function Identicon (value, size) { - var svg = document.createElementNS(svgNS, 'svg'); - var shouldFillRectAt = function (row, col) { - return !($window.parseInt(value.charCodeAt(row + col * size), 10) % 2); - }; - var shouldMirrorRectAt = function (row, col) { - return !(size % 2 && col === middleCol) - }; - var mirrorColFor = function (col) { - return size - col - 1; - }; - var fillRectAt = function (row, col) { - var rect = document.createElementNS(svgNS, 'rect'); - - rect.setAttribute('x', (col * rectSize) + '%'); - rect.setAttribute('y', (row * rectSize) + '%'); - rect.setAttribute('width', rectSize + '%'); - rect.setAttribute('height', rectSize + '%'); - - svg.appendChild(rect); - }; - var rect; - var row; - var col; - var middleCol; - var rectSize; - - svg.setAttribute('class', 'identicon'); - size = size || 5; - rectSize = 100 / size; - middleCol = Math.ceil(size / 2) - 1; - - if (value) { - value = value.toString().replace(/[\W_]/i, ''); - - for (row = 0; row < size; ++row) { - for (col = middleCol; col > -1; --col) { - if (shouldFillRectAt(row, col)) { - fillRectAt(row, col); - - if (shouldMirrorRectAt(row, col)) { - fillRectAt(row, mirrorColFor(col)); - } - } - } - } - } - - return svg; - } - - return { - restrict: 'E', - scope: { - value: '=' - }, - link: function (scope, element, attributes) { - element.append(new Identicon(scope.value)); - } - } -}]); diff --git a/gui/overrides.css b/gui/assets/css/overrides.css similarity index 100% rename from gui/overrides.css rename to gui/assets/css/overrides.css diff --git a/gui/font/raleway-500.woff b/gui/assets/font/raleway-500.woff similarity index 100% rename from gui/font/raleway-500.woff rename to gui/assets/font/raleway-500.woff diff --git a/gui/font/raleway.css b/gui/assets/font/raleway.css similarity index 100% rename from gui/font/raleway.css rename to gui/assets/font/raleway.css diff --git a/gui/img/favicon.png b/gui/assets/img/favicon.png similarity index 100% rename from gui/img/favicon.png rename to gui/assets/img/favicon.png diff --git a/gui/img/logo-text-256.png b/gui/assets/img/logo-text-256.png similarity index 100% rename from gui/img/logo-text-256.png rename to gui/assets/img/logo-text-256.png diff --git a/gui/img/logo-text-64.png b/gui/assets/img/logo-text-64.png similarity index 100% rename from gui/img/logo-text-64.png rename to gui/assets/img/logo-text-64.png diff --git a/gui/lang/README-FIRST.txt b/gui/assets/lang/README-FIRST.txt similarity index 100% rename from gui/lang/README-FIRST.txt rename to gui/assets/lang/README-FIRST.txt diff --git a/gui/lang/lang-be.json b/gui/assets/lang/lang-be.json similarity index 100% rename from gui/lang/lang-be.json rename to gui/assets/lang/lang-be.json diff --git a/gui/lang/lang-bg.json b/gui/assets/lang/lang-bg.json similarity index 100% rename from gui/lang/lang-bg.json rename to gui/assets/lang/lang-bg.json diff --git a/gui/lang/lang-cs.json b/gui/assets/lang/lang-cs.json similarity index 100% rename from gui/lang/lang-cs.json rename to gui/assets/lang/lang-cs.json diff --git a/gui/lang/lang-de.json b/gui/assets/lang/lang-de.json similarity index 100% rename from gui/lang/lang-de.json rename to gui/assets/lang/lang-de.json diff --git a/gui/lang/lang-en.json b/gui/assets/lang/lang-en.json similarity index 100% rename from gui/lang/lang-en.json rename to gui/assets/lang/lang-en.json diff --git a/gui/lang/lang-fr.json b/gui/assets/lang/lang-fr.json similarity index 100% rename from gui/lang/lang-fr.json rename to gui/assets/lang/lang-fr.json diff --git a/gui/lang/lang-hu.json b/gui/assets/lang/lang-hu.json similarity index 100% rename from gui/lang/lang-hu.json rename to gui/assets/lang/lang-hu.json diff --git a/gui/lang/lang-it.json b/gui/assets/lang/lang-it.json similarity index 100% rename from gui/lang/lang-it.json rename to gui/assets/lang/lang-it.json diff --git a/gui/lang/lang-lt.json b/gui/assets/lang/lang-lt.json similarity index 100% rename from gui/lang/lang-lt.json rename to gui/assets/lang/lang-lt.json diff --git a/gui/lang/lang-nb.json b/gui/assets/lang/lang-nb.json similarity index 100% rename from gui/lang/lang-nb.json rename to gui/assets/lang/lang-nb.json diff --git a/gui/lang/lang-nn.json b/gui/assets/lang/lang-nn.json similarity index 100% rename from gui/lang/lang-nn.json rename to gui/assets/lang/lang-nn.json diff --git a/gui/lang/lang-pl.json b/gui/assets/lang/lang-pl.json similarity index 100% rename from gui/lang/lang-pl.json rename to gui/assets/lang/lang-pl.json diff --git a/gui/lang/lang-pt-PT.json b/gui/assets/lang/lang-pt-PT.json similarity index 100% rename from gui/lang/lang-pt-PT.json rename to gui/assets/lang/lang-pt-PT.json diff --git a/gui/lang/lang-sv.json b/gui/assets/lang/lang-sv.json similarity index 100% rename from gui/lang/lang-sv.json rename to gui/assets/lang/lang-sv.json diff --git a/gui/lang/lang-zh-CN.json b/gui/assets/lang/lang-zh-CN.json similarity index 100% rename from gui/lang/lang-zh-CN.json rename to gui/assets/lang/lang-zh-CN.json diff --git a/gui/lang/lang-zh-TW.json b/gui/assets/lang/lang-zh-TW.json similarity index 100% rename from gui/lang/lang-zh-TW.json rename to gui/assets/lang/lang-zh-TW.json diff --git a/gui/lang/valid-langs.js b/gui/assets/lang/valid-langs.js similarity index 100% rename from gui/lang/valid-langs.js rename to gui/assets/lang/valid-langs.js diff --git a/gui/index.html b/gui/index.html index af8f66fd3..8cd89939e 100644 --- a/gui/index.html +++ b/gui/index.html @@ -15,29 +15,29 @@ // You should have received a copy of the GNU General Public License along // with this program. If not, see . --> - + - + Syncthing | {{thisDeviceName()}} - - - + + + -
+