Bladeren bron

all: Remove untrusted feature flag (fixes #109) (#7567)

No longer hide the web UI controls for the new untrusted/encrypted
device feature. Testing hasn't been very widespread, but there has been
some and quite a few bugs have been caught and fixed. I believe its time
to not hide it anymore, and cautiously recommend usage. E.g. mention
that the feature hasn't been widely used yet and anyone using it is an
early adopter, but drop the bit about not using it with production data.
We can maybe stress the need for backups in general and especially
using this.
Simon Frei 4 jaren geleden
bovenliggende
commit
f71fcd440a

+ 17 - 5
gui/default/index.html

@@ -342,6 +342,7 @@
                     <span ng-if="folder.type == 'sendreceive'" class="fas fa-fw fa-folder"></span>
                     <span ng-if="folder.type == 'sendonly'" class="fas fa-fw fa-upload"></span>
                     <span ng-if="folder.type == 'receiveonly'" class="fas fa-fw fa-download"></span>
+                    <span ng-if="folder.type == 'receiveencrypted'" class="fas fa-fw fa-lock"></span>
                   </div>
                   <div class="panel-status pull-right text-{{folderClass(folder)}}" ng-switch="folderStatus(folder)">
                     <span ng-switch-when="paused"><span class="hidden-xs" translate>Paused</span><span class="visible-xs" aria-label="{{'Paused' | translate}}"><i class="fas fa-fw fa-pause"></i></span></span>
@@ -374,6 +375,7 @@
                     </span>
                     <span ng-switch-when="outofsync"><span class="hidden-xs" translate>Out of Sync</span><span class="visible-xs" aria-label="{{'Out of Sync' | translate}}"><i class="fas fa-fw fa-exclamation-circle"></i></span></span>
                     <span ng-switch-when="faileditems"><span class="hidden-xs" translate>Failed Items</span><span class="visible-xs" aria-label="{{'Failed Items' | translate}}"><i class="fas fa-fw fa-exclamation-circle"></i></span></span>
+                    <span ng-switch-when="localunencrypted"><span class="hidden-xs">{{'Unexpected Items' | translate}}</span><span class="visible-xs" aria-label="{{'Unexpected Items' | translate}}"><i class="fas fa-fw fa-exclamation-circle"></i></span></span>
                   </div>
                   <div class="panel-title-text">
                     <span tooltip data-original-title="{{folder.label.length != 0 ? folder.id : ''}}">{{folder.label.length != 0 ? folder.label : folder.id}}</span>
@@ -438,10 +440,16 @@
                           <a href="" ng-click="showFailed(folder.id)">{{model[folder.id].pullErrors | alwaysNumber | localeNumber}}&nbsp;<span translate>items</span></a>
                         </td>
                       </tr>
-                      <tr ng-if="folder.type == 'receiveonly' && canRevert(folder.id)">
+                      <tr ng-if="hasReceiveOnlyChanged(folder)">
                         <th><span class="fas fa-fw fa-exclamation-circle"></span>&nbsp;<span translate>Locally Changed Items</span></th>
                         <td class="text-right">
-                          <a href="" ng-click="showLocalChanged(folder.id)">{{model[folder.id].receiveOnlyTotalItems | alwaysNumber | localeNumber}} <span translate>items</span>, ~{{model[folder.id].receiveOnlyChangedBytes | binary}}B</a>
+                          <a href="" ng-click="showLocalChanged(folder.id, folder.type)">{{model[folder.id].receiveOnlyTotalItems | alwaysNumber | localeNumber}} <span translate>items</span>, ~{{model[folder.id].receiveOnlyChangedBytes | binary}}B</a>
+                        </td>
+                      </tr>
+                      <tr ng-if="hasReceiveEncryptedItems(folder)">
+                        <th><span class="fas fa-fw fa-exclamation-circle"></span>&nbsp;<span translate>Locally Changed Items</span></th>
+                        <td class="text-right">
+                          <a href="" ng-click="showLocalChanged(folder.id, folder.type)">{{receiveEncryptedItemsCount(folder) | alwaysNumber | localeNumber}} <span translate>items</span>, ~{{model[folder.id].receiveOnlyChangedBytes | binary}}B</a>
                         </td>
                       </tr>
                       <tr ng-if="folder.type != 'sendreceive'">
@@ -449,6 +457,7 @@
                         <td class="text-right">
                           <span ng-if="folder.type == 'sendonly'" translate>Send Only</span>
                           <span ng-if="folder.type == 'receiveonly'" translate>Receive Only</span>
+                          <span ng-if="folder.type == 'receiveencrypted'" translate>Receive Encrypted</span>
                         </td>
                       </tr>
                       <tr ng-if="folder.ignorePerms">
@@ -521,7 +530,7 @@
                           <span>{{folderStats[folder.id].lastScan | date:'yyyy-MM-dd HH:mm:ss'}}</span>
                         </td>
                       </tr>
-                      <tr ng-if="folder.type != 'sendonly' && folderStats[folder.id].lastFile && folderStats[folder.id].lastFile.filename">
+                      <tr ng-if="folder.type != 'sendonly' && folder.type != 'receiveencrypted' && folderStats[folder.id].lastFile && folderStats[folder.id].lastFile.filename">
                         <th><span class="fas fa-fw fa-exchange-alt"></span>&nbsp;<span translate>Latest Change</span></th>
                         <td class="text-right">
                           <span tooltip data-original-title="{{folderStats[folder.id].lastFile.filename}} @ {{folderStats[folder.id].lastFile.at | date:'yyyy-MM-dd HH:mm:ss'}}">
@@ -541,6 +550,9 @@
                   <button type="button" class="btn btn-sm btn-danger pull-left" ng-click="revertOverrideConfirmationModal('revert', folder.id)" ng-if="hasReceiveOnlyChanged(folder)">
                     <span class="fa fa-arrow-circle-down"></span>&nbsp;<span translate>Revert Local Changes</span>
                   </button>
+                  <button type="button" class="btn btn-sm btn-danger pull-left" ng-click="revertOverrideConfirmationModal('deleteEnc', folder.id)" ng-if="hasReceiveEncryptedItems(folder)">
+                    <span class="fa fa-arrow-lock"></span>&nbsp;<span translate>Delete Unexpected Items</span>
+                  </button>
                   <span class="pull-right">
                     <button ng-if="!folder.paused" type="button" class="btn btn-sm btn-default" ng-click="setFolderPause(folder.id, true)">
                       <span class="fas fa-pause"></span>&nbsp;<span translate>Pause</span>
@@ -760,8 +772,8 @@
                         </td>
                         <td ng-if="!connections[deviceCfg.deviceID].connected" class="text-right">
                           <span ng-repeat="addr in deviceCfg.addresses">
-                              <span tooltip data-original-title="{{'Configured' | translate}}">{{addr}}</span><br>
-                              <small ng-if="system.lastDialStatus[addr].error" tooltip data-original-title="{{system.lastDialStatus[addr].error}}" class="text-danger">{{abbreviatedError(addr)}}<br></small>
+                            <span tooltip data-original-title="{{'Configured' | translate}}">{{addr}}</span><br>
+                            <small ng-if="system.lastDialStatus[addr].error" tooltip data-original-title="{{system.lastDialStatus[addr].error}}" class="text-danger">{{abbreviatedError(addr)}}<br></small>
                           </span>
                           <span ng-repeat="addr in discoveryCache[deviceCfg.deviceID].addresses">
                             <span tooltip data-original-title="{{'Discovered' | translate}}">{{addr}}</span><br>

+ 85 - 9
gui/default/syncthing/core/syncthingController.js

@@ -769,6 +769,7 @@ angular.module('syncthing.core')
             $scope.currentSharing.shared = [];
             $scope.currentSharing.unrelated = [];
             $scope.currentSharing.selected = {};
+            $scope.currentSharing.encryptionPasswords = {};
             if (editing === 'folder') {
                 initShareEditingFolder();
             }
@@ -779,6 +780,9 @@ angular.module('syncthing.core')
                 if (n.deviceID !== $scope.myID) {
                     $scope.currentSharing.shared.push($scope.devices[n.deviceID]);
                 }
+                if (n.encryptionPassword !== '') {
+                    $scope.currentSharing.encryptionPasswords[n.deviceID] = n.encryptionPassword;
+                }
                 $scope.currentSharing.selected[n.deviceID] = true;
             });
             $scope.currentSharing.unrelated = $scope.deviceList().filter(function (n) {
@@ -903,9 +907,12 @@ angular.module('syncthing.core')
             if ($scope.hasFailedFiles(folderCfg.id)) {
                 return 'faileditems';
             }
-            if (folderInfo.receiveOnlyTotalItems) {
+            if ($scope.hasReceiveOnlyChanged(folderCfg)) {
                 return 'localadditions';
             }
+            if ($scope.hasReceiveEncryptedItems(folderCfg)) {
+                return 'localunencrypted';
+            }
             if (folderCfg.devices.length <= 1) {
                 return 'unshared';
             }
@@ -928,7 +935,7 @@ angular.module('syncthing.core')
             if (status === 'unknown') {
                 return 'info';
             }
-            if (status === 'stopped' || status === 'outofsync' || status === 'error' || status === 'faileditems') {
+            if (status === 'stopped' || status === 'outofsync' || status === 'error' || status === 'faileditems' || status === 'localunencrypted') {
                 return 'danger';
             }
             if (status === 'unshared' || status === 'scan-waiting' || status === 'sync-waiting' || status === 'clean-waiting') {
@@ -1553,6 +1560,13 @@ angular.module('syncthing.core')
             $scope.deviceFolders($scope.currentDevice).forEach(function (folderID) {
                 $scope.currentSharing.shared.push($scope.folders[folderID]);
                 $scope.currentSharing.selected[folderID] = true;
+                var folderdevices = $scope.folders[folderID].devices;
+                for (var i = 0; i < folderdevices.length; i++) {
+                    if (folderdevices[i].deviceID === deviceCfg.deviceID) {
+                        $scope.currentSharing.encryptionPasswords[folderID] = folderdevices[i].encryptionPassword;
+                        break;
+                    }
+                }
             });
             $scope.currentSharing.unrelated = $scope.folderList().filter(function (n) {
                 return !$scope.currentSharing.selected[n.id];
@@ -1656,6 +1670,8 @@ angular.module('syncthing.core')
                     for (i = 0; i < $scope.folders[id].devices.length; i++) {
                         if ($scope.folders[id].devices[i].deviceID === currentID) {
                             found = true;
+                            // Update encryption pw
+                            $scope.folders[id].devices[i].encryptionPassword = $scope.currentSharing.encryptionPasswords[id];
                             break;
                         }
                     }
@@ -1664,6 +1680,7 @@ angular.module('syncthing.core')
                         // Add device to folder
                         $scope.folders[id].devices.push({
                             deviceID: currentID,
+                            encryptionPassword: $scope.currentSharing.encryptionPasswords[id],
                         });
                     }
                 } else {
@@ -1822,19 +1839,32 @@ angular.module('syncthing.core')
         });
 
         $scope.setFSWatcherIntervalDefault = function () {
-            var defaultRescanIntervals = [60, 3600];
+            var defaultRescanIntervals = [60, 3600, 3600*24];
             if (defaultRescanIntervals.indexOf($scope.currentFolder.rescanIntervalS) === -1) {
                 return;
             }
             var idx;
             if ($scope.currentFolder.fsWatcherEnabled) {
                 idx = 1;
+            } else if ($scope.currentFolder.type === 'receiveencrypted') {
+                idx = 2;
             } else {
                 idx = 0;
             }
             $scope.currentFolder.rescanIntervalS = defaultRescanIntervals[idx];
         };
 
+        $scope.setDefaultsForFolderType = function () {
+            if ($scope.currentFolder.type === 'receiveencrypted') {
+                $scope.currentFolder.fsWatcherEnabled = false;
+                $scope.currentFolder.ignorePerms = true;
+                delete $scope.currentFolder.versioning;
+            } else {
+                $scope.currentFolder.fsWatcherEnabled = true;
+            }
+            $scope.setFSWatcherIntervalDefault();
+        };
+
         $scope.loadFormIntoScope = function (form) {
             console.log('loadFormIntoScope', form.$name);
             switch (form.$name) {
@@ -1853,6 +1883,7 @@ angular.module('syncthing.core')
 
         function editFolderModal() {
             initVersioningEditing();
+            $scope.currentFolder._recvEnc = $scope.currentFolder.type === 'receiveencrypted';
             $scope.folderPathErrors = {};
             $scope.folderEditor.$setPristine();
             $('#editFolder').modal().one('shown.bs.tab', function (e) {
@@ -2003,6 +2034,13 @@ angular.module('syncthing.core')
                 };
                 $scope.currentSharing.selected[device] = true;
                 $scope.currentFolder.label = pendingFolder.offeredBy[device].label;
+                for (var k in pendingFolder.offeredBy) {
+                    if (pendingFolder.offeredBy[k].receiveEncrypted) {
+                        $scope.currentFolder.type = "receiveencrypted";
+                        $scope.setDefaultsForFolderType();
+                        break;
+                    }
+                }
                 editFolderModal();
             });
         };
@@ -2039,6 +2077,7 @@ angular.module('syncthing.core')
             var newDevices = [];
             folderCfg.devices.forEach(function (dev) {
                 if ($scope.currentSharing.selected[dev.deviceID] === true) {
+                    dev.encryptionPassword = $scope.currentSharing.encryptionPasswords[dev.deviceID];
                     newDevices.push(dev);
                     delete $scope.currentSharing.selected[dev.deviceID];
                 };
@@ -2046,7 +2085,8 @@ angular.module('syncthing.core')
             for (var deviceID in $scope.currentSharing.selected) {
                 if ($scope.currentSharing.selected[deviceID] === true) {
                     newDevices.push({
-                        deviceID: deviceID
+                        deviceID: deviceID,
+                        encryptionPassword: $scope.currentSharing.encryptionPasswords[deviceID],
                     });
                 }
             }
@@ -2473,23 +2513,40 @@ angular.module('syncthing.core')
             return $scope.model[folder].errors !== 0;
         };
 
-        $scope.showLocalChanged = function (folder) {
+        $scope.showLocalChanged = function (folder, folderType) {
             $scope.localChangedFolder = folder;
+            $scope.localChangedType = folderType;
             $scope.localChanged = $scope.refreshLocalChanged(1, 10);
             $('#localChanged').modal().one('hidden.bs.modal', function () {
                 $scope.localChanged = {};
                 $scope.localChangedFolder = undefined;
+                $scope.localChangedType = undefined;
             });
         };
 
-        $scope.canRevert = function (folder) {
-            var f = $scope.model[folder];
-            if (!f) {
+        $scope.hasReceiveOnlyChanged = function (folderCfg) {
+            if (!folderCfg || folderCfg.type !== "receiveonly") {
                 return false;
             }
-            return $scope.model[folder].receiveOnlyTotalItems > 0;
+            var counts = $scope.model[folderCfg.id];
+            return counts && counts.receiveOnlyTotalItems > 0;
         };
 
+        $scope.hasReceiveEncryptedItems = function (folderCfg) {
+            if (!folderCfg || folderCfg.type !== "receiveencrypted") {
+                return false;
+            }
+            return $scope.receiveEncryptedItemsCount(folderCfg) > 0;
+        };
+
+        $scope.receiveEncryptedItemsCount = function (folderCfg) {
+            var counts = $scope.model[folderCfg.id];
+            if (!counts) {
+                return 0;
+            }
+            return counts.receiveOnlyTotalItems - counts.receiveOnlyChangedDeletes;
+        }
+
         $scope.revertOverride = function () {
             $http.post(
                 urlbase + "/db/" + $scope.revertOverrideParams.operation +"?folder="
@@ -2734,4 +2791,23 @@ angular.module('syncthing.core')
                     address.indexOf('unix://') == 0 ||
                     address.indexOf('unixs://') == 0);
         }
+    })
+    .directive('shareTemplate', function () {
+        return {
+            templateUrl: 'syncthing/core/editShareTemplate.html',
+            scope: {
+                selected: '=',
+                encryptionPasswords: '=',
+                id: '@',
+                label: '@',
+                folderType: '@',
+                untrusted: '=',
+            },
+            link: function(scope, elem, attrs) {
+                var plain = false;
+                scope.togglePasswordVisibility = function() {
+                    scope.plain = !scope.plain;
+                };
+            },
+        }
     });

+ 30 - 43
gui/default/syncthing/device/editDeviceModalView.html

@@ -63,50 +63,30 @@
               </div>
             </div>
           </div>
-          <div class="row">
-            <div class="col-md-12">
-              <div class="form-group" ng-if="currentSharing.shared.length">
-                <label translate for="folders">Shared Folders</label>
-                <p class="help-block">
-                  <span translate>Deselect folders to stop sharing with this device.</span>&emsp;
-                  <small><a href="#" ng-click="selectAllSharedFolders(true)" translate>Select All</a>&emsp;
-                    <a href="#" ng-click="selectAllSharedFolders(false)" translate>Deselect All</a></small>
-                </p>
-                <div class="row">
-                  <div class="col-md-4" ng-repeat="folder in currentSharing.shared">
-                    <div class="checkbox">
-                      <label ng-if="folder.label.length == 0">
-                        <input type="checkbox" ng-model="currentSharing.selected[folder.id]" />&nbsp;{{folder.id}}
-                      </label>
-                      <label ng-if="folder.label.length != 0">
-                        <input type="checkbox" ng-model="currentSharing.selected[folder.id]" />&nbsp; <span tooltip data-original-title="{{folder.id}}">{{folder.label}}</span>
-                      </label>
-                    </div>
-                  </div>
-                </div>
+          <div class="form-group">
+            <div class="form-horizontal" ng-if="currentSharing.shared.length">
+              <label translate for="folders">Shared Folders</label>
+              <p class="help-block">
+                <span translate>Deselect folders to stop sharing with this device.</span>&emsp;
+                <small><a href="#" ng-click="selectAllSharedFolders(true)" translate>Select All</a>&emsp;
+                  <a href="#" ng-click="selectAllSharedFolders(false)" translate>Deselect All</a></small>
+              </p>
+              <div class="form-group" ng-repeat="folder in currentSharing.shared">
+                <share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{folder.id}}" label="{{folderLabel(folder.id)}}" folder-type="{{folder.type}}" untrusted="currentDevice.untrusted" />
               </div>
-              <div class="form-group" ng-if="currentSharing.unrelated.length || folderList().length == 0">
-                <label translate for="folders">Unshared Folders</label>
-                <p class="help-block" ng-if="folderList().length > 0">
-                  <span translate>Select additional folders to share with this device.</span>&emsp;
-                  <small><a href="#" ng-click="selectAllUnrelatedFolders(true)" translate>Select All</a>&emsp;
-                    <a href="#" ng-click="selectAllUnrelatedFolders(false)" translate>Deselect All</a></small>
-                </p>
-                <p class="help-block" ng-if="folderList().length == 0">
-                  <span translate>There are no folders to share with this device.</span>
-                </p>
-                <div class="row">
-                  <div class="col-md-4" ng-repeat="folder in currentSharing.unrelated">
-                    <div class="checkbox">
-                      <label ng-if="folder.label.length == 0">
-                        <input type="checkbox" ng-model="currentSharing.selected[folder.id]">&nbsp;{{folder.id}}
-                      </label>
-                      <label ng-if="folder.label.length != 0">
-                        <input type="checkbox" ng-model="currentSharing.selected[folder.id]">&nbsp; <span tooltip data-original-title="{{folder.id}}">{{folder.label}}</span>
-                      </label>
-                    </div>
-                  </div>
-                </div>
+            </div>
+            <div class="form-horizontal" ng-if="currentSharing.unrelated.length">
+              <label translate for="folders">Unshared Folders</label>
+              <p class="help-block" ng-if="folderList().length > 0">
+                <span translate>Select additional folders to share with this device.</span>&emsp;
+                <small><a href="#" ng-click="selectAllUnrelatedFolders(true)" translate>Select All</a>&emsp;
+                  <a href="#" ng-click="selectAllUnrelatedFolders(false)" translate>Deselect All</a></small>
+              </p>
+              <p class="help-block" ng-if="folderList().length == 0">
+                <span translate>There are no folders to share with this device.</span>
+              </p>
+              <div class="form-group" ng-repeat="folder in currentSharing.unrelated">
+                <share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{folder.id}}" label="{{folderLabel(folder.id)}}" folder-type="{{folder.type}}" untrusted="currentDevice.untrusted" />
               </div>
             </div>
           </div>
@@ -156,6 +136,13 @@
               </div>
             </div>
           </div>
+          <div class="row">
+            <div class="form-group col-md-6">
+              <input type="checkbox" id="untrusted" ng-model="currentDevice.untrusted" />
+              <label for="untrusted" translate>Untrusted</label>
+              <p translate class="help-block">All folders shared with this device must be protected by a password, such that all sent data is unreadable without the given password.</p>
+            </div>
+          </div>
         </div>
       </div>
     </form>

+ 14 - 22
gui/default/syncthing/folder/editFolderModalView.html

@@ -5,7 +5,7 @@
         <li class="active"><a data-toggle="tab" href="#folder-general"><span class="fas fa-cog"></span> <span translate>General</span></a></li>
         <li><a data-toggle="tab" href="#folder-sharing"><span class="fas fa-share-alt"></span> <span translate>Sharing</span></a></li>
         <li><a data-toggle="tab" href="#folder-versioning"><span class="fas fa-copy"></span> <span translate>File Versioning</span></a></li>
-        <li ng-if="!editingDefaults"><a data-toggle="tab" href="#folder-ignores"><span class="fas fa-filter"></span> <span translate>Ignore Patterns</span></a></li>
+        <li ng-if="!editingDefaults" ng-class="{'disabled': currentFolder._recvEnc}"><a ng-attr-data-toggle="{{ currentFolder._recvEnc ? undefined : 'tab'}}" href="{{currentFolder._recvEnc ? '#' : '#folder-ignores'}}"><span class="fas fa-filter"></span> <span translate>Ignore Patterns</span></a></li>
         <li><a data-toggle="tab" href="#folder-advanced"><span class="fas fa-cogs"></span> <span translate>Advanced</span></a></li>
       </ul>
       <div class="tab-content">
@@ -48,24 +48,18 @@
         </div>
 
         <div id="folder-sharing" class="tab-pane">
-          <div class="form-group" ng-if="currentSharing.shared.length">
+          <div class="form-horizontal" ng-if="currentSharing.shared.length">
             <label translate>Currently Shared With Devices</label>
             <p class="help-block">
               <span translate>Deselect devices to stop sharing this folder with.</span>&emsp;
               <small><a href="#" ng-click="selectAllSharedDevices(true)" translate>Select All</a>&emsp;
                 <a href="#" ng-click="selectAllSharedDevices(false)" translate>Deselect All</a></small>
             </p>
-            <div class="row">
-              <div class="col-md-4" ng-repeat="device in currentSharing.shared">
-                <div class="checkbox">
-                  <label>
-                    <input type="checkbox" ng-model="currentSharing.selected[device.deviceID]" /> {{deviceName(device)}}
-                  </label>
-                </div>
-              </div>
+            <div class="form-group" ng-repeat="device in currentSharing.shared">
+              <share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{device.deviceID}}" label="{{deviceName(device)}}" folder-type="{{currentFolder.type}}" untrusted="device.untrusted" />
             </div>
           </div>
-          <div class="form-group" ng-if="currentSharing.unrelated.length || otherDevices().length <= 0">
+          <div class="form-horizontal" ng-if="currentSharing.unrelated.length || otherDevices().length <= 0">
             <label translate>Unshared Devices</label>
             <p class="help-block" ng-if="otherDevices().length > 0">
               <span translate>Select additional devices to share this folder with.</span>&emsp;
@@ -75,14 +69,8 @@
             <p class="help-block" ng-if="otherDevices().length <= 0">
               <span translate>There are no devices to share this folder with.</span>
             </p>
-            <div class="row">
-              <div class="col-md-4" ng-repeat="device in currentSharing.unrelated">
-                <div class="checkbox">
-                  <label>
-                    <input type="checkbox" ng-model="currentSharing.selected[device.deviceID]" /> {{deviceName(device)}}
-                  </label>
-                </div>
-              </div>
+            <div class="form-group" ng-repeat="device in currentSharing.unrelated" ng-init="id = device.deviceID; folder = currentFolder">
+              <share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{device.deviceID}}" label="{{deviceName(device)}}" folder-type="{{currentFolder.type}}" untrusted="device.untrusted" />
             </div>
           </div>
         </div>
@@ -153,7 +141,7 @@
               <div class="input-group-addon" translate>seconds</div>
             </div>
             <p class="help-block">
-              <span translate ng-if="folderEditor.cleanupIntervalS.$valid || folderEditor.cleanupIntervalS.$pristine" class="help-block">The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.</span>
+              <span translate ng-if="folderEditor.cleanupIntervalS.$valid || folderEditor.cleanupIntervalS.$pristine"class="help-block">The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.</span>
               <span translate ng-if="folderEditor.cleanupIntervalS.$error.required && folderEditor.cleanupIntervalS.$dirty">The cleanup interval cannot be blank.</span>
               <span translate ng-if="folderEditor.cleanupIntervalS.$error.min && folderEditor.cleanupIntervalS.$dirty">The interval must be a positive number of seconds.</span>
             </p>
@@ -217,13 +205,17 @@
             <div class="col-md-6 form-group">
               <label translate>Folder Type</label>
               &nbsp;<a href="https://docs.syncthing.net/users/foldertypes.html" target="_blank"><span class="fas fa-question-circle"></span>&nbsp;<span translate>Help</span></a>
-              <select class="form-control" ng-model="currentFolder.type">
+              <select class="form-control" ng-change="setDefaultsForFolderType()" ng-model="currentFolder.type" ng-disabled="editingExisting && currentFolder.type == 'receiveencrypted'">
                 <option value="sendreceive" translate>Send &amp; Receive</option>
                 <option value="sendonly" translate>Send Only</option>
                 <option value="receiveonly" translate>Receive Only</option>
+                <option value="receiveencrypted" ng-disabled="editingExisting" translate>Receive Encrypted</option>
               </select>
               <p ng-if="currentFolder.type == 'sendonly'" translate class="help-block">Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.</p>
               <p ng-if="currentFolder.type == 'receiveonly'" translate class="help-block">Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.</p>
+              <p ng-if="currentFolder.type == 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type "{%receiveEncrypted%}" too.</p>
+              <p ng-if="editingExisting && currentFolder.type == 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Folder type "{%receiveEncrypted%}" cannot be changed after adding the folder. You need to remove the folder, delete or decrypt the data on disk, and add the folder again.</p>
+              <p ng-if="editingExisting && currentFolder.type != 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Folder type "{%receiveEncrypted%}" can only be set when adding a new folder.</p>
             </div>
             <div class="col-md-6 form-group">
               <label translate>File Pull Order</label>
@@ -264,7 +256,7 @@
             </div>
             <div class="col-md-6 form-group">
               <label>
-                <input type="checkbox" ng-model="currentFolder.ignorePerms" /> <span translate>Ignore Permissions</span>
+                <input type="checkbox" ng-disabled="currentFolder._recvEnc" ng-model="currentFolder.ignorePerms" /> <span translate>Ignore Permissions</span>
               </label>
               <p translate class="help-block">
                 Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).

+ 7 - 3
gui/default/syncthing/transfer/localChangedFilesModalView.html

@@ -1,8 +1,12 @@
-<modal id="localChanged" status="info" icon="fas fa-exclamation-circle" heading="{{'Locally Changed Items' | translate}}" large="yes" closeable="yes">
-  <div class="modal-body">
-    <p translate>
+<modal id="localChanged" status="{{localChangedType === 'receiveencrypted' ? 'warning' : 'info'}}" icon="fas fa-exclamation-circle" heading="{{localChangedHeading(localChangedType)}}" large="yes" closeable="yes">
+  <div class="modal-body" ng-switch="localChangedType">
+    <p ng-switch-when="receiveonly" translate>
       The following items were changed locally.
     </p>
+    <p ng-switch-when="receiveencrypted">
+      <span translate>The following unexpected items were found.</span>
+      <span translate translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">You should never add or change anything locally in a "{%receiveEncrypted%}" folder.</span>
+    </p>
     <table class="table table-striped">
       <thead>
         <tr>

+ 0 - 958
gui/default/untrusted/index.html

@@ -1,958 +0,0 @@
-<!DOCTYPE html>
-<!--
-// Copyright (C) 2014 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
--->
-<html lang="en" ng-app="syncthing" ng-controller="SyncthingController">
-<head>
-  <meta charset="utf-8"/>
-  <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
-  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
-  <meta name="description" content=""/>
-  <meta name="author" content=""/>
-  <link rel="shortcut icon" href="assets/img/favicon-{{syncthingStatus()}}.png"/>
-  <link rel="mask-icon" href="assets/img/safari-pinned-tab.svg" color="#0882c8"/>
-
-  <title ng-bind="thisDeviceName() + ' | Syncthing'"></title>
-  <link href="vendor/bootstrap/css/bootstrap.css" rel="stylesheet"/>
-  <link href="vendor/daterangepicker/daterangepicker.css" rel="stylesheet"/>
-  <link href="assets/font/raleway.css" rel="stylesheet"/>
-  <link href="vendor/fork-awesome/css/fork-awesome.css" rel="stylesheet"/>
-  <link href="vendor/fork-awesome/css/v5-compat.css" rel="stylesheet"/>
-  <link href="assets/css/overrides.css" rel="stylesheet"/>
-  <link href="assets/css/theme.css" rel="stylesheet"/>
-  <link href="vendor/fancytree/css/ui.fancytree.css" rel="stylesheet"/>
-</head>
-
-<body>
-  <noscript>
-    <nav class="navbar navbar-top navbar-default" role="navigation">
-      <div class="container">
-        <span class="navbar-brand" aria-hidden="true">
-          <img class="logo hidden-xs" src="assets/img/logo-horizontal.svg" height="32" width="117" alt=""/>
-          <img class="logo hidden visible-xs" src="assets/img/favicon-default.png" height="32" alt=""/>
-        </span>
-      </div>
-    </nav>
-
-    <div class="container content">
-      <div class="row">
-        <div class="col-md-12">
-          <div class="panel panel-danger">
-            <div class="panel-heading">
-              <h3 class="panel-title">
-                <div class="panel-icon">
-                  <span class="fas fa-exclamation-circle"></span>
-                </div>
-                Warning!
-              </h3>
-            </div>
-            <div class="panel-body">
-              <p>
-              The Syncthing admin interface requires JavaScript. Please enable JavaScript in your web browser and try again.
-              </p>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </noscript>
-
-  <div class="ng-cloak">
-    <script type="text/javascript" src="syncthing/development/logbar.js"></script>
-    <div ng-if="version.isBeta" ng-include="'syncthing/development/logbar.html'"></div>
-    <!-- Top bar -->
-
-    <nav class="navbar navbar-top navbar-default" role="navigation">
-      <div class="container">
-        <span class="navbar-brand" aria-hidden="true">
-          <img class="logo hidden-xs" src="assets/img/logo-horizontal.svg" height="32" width="117" alt=""/>
-          <img class="logo hidden visible-xs" src="assets/img/favicon-default.png" height="32" alt=""/>
-        </span>
-        <p class="navbar-text hidden-xs" ng-class="{'hidden-sm':upgradeInfo && upgradeInfo.newer}">{{thisDeviceName()}}</p>
-        <ul class="nav navbar-nav navbar-right">
-          <li ng-if="upgradeInfo && upgradeInfo.newer" class="upgrade-newer">
-            <button type="button" class="btn navbar-btn btn-primary btn-sm" data-toggle="modal" data-target="#upgrade">
-              <span class="fas fa-arrow-circle-up"></span>
-              <span class="hidden-xs" translate translate-value-version="{{upgradeInfo.latest}}">Upgrade To {%version%}</span>
-            </button>
-          </li>
-          <li ng-if="upgradeInfo && upgradeInfo.majorNewer" class="upgrade-newer-major">
-            <button type="button" class="btn navbar-btn btn-danger btn-sm" data-toggle="modal" data-target="#majorUpgrade">
-              <span class="fas fa-arrow-circle-up"></span>
-              <span class="hidden-xs" translate translate-value-version="{{upgradeInfo.latest}}">Upgrade To {%version%}</span>
-            </button>
-          </li>
-          <li class="dropdown" language-select></li>
-          <li>
-            <a class="navbar-link" href="https://docs.syncthing.net/intro/gui.html" target="_blank">
-              <span class="fas fa-question-circle"></span>
-              <span class="hidden-xs" translate>Help</span>
-            </a>
-          </li>
-          <li class="dropdown action-menu">
-            <a href="#" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
-              <span class="fas fa-cog"></span>
-              <span class="hidden-xs" translate>Actions</span>
-              <span class="caret"></span>
-            </a>
-            <ul class="dropdown-menu">
-              <li><a href="" ng-click="showSettings()"><span class="fas fa-fw fa-cog"></span>&nbsp;<span translate>Settings</span></a></li>
-              <li><a href="" data-toggle="modal" data-target="#idqr" ng-click="currentDevice=thisDevice()"><span class="fas fa-fw fa-qrcode"></span>&nbsp;<span translate>Show ID</span></a></li>
-              <li class="divider" aria-hidden="true"></li>
-              <li><a href="" ng-click="shutdown()"><span class="fas fa-fw fa-power-off"></span>&nbsp;<span translate>Shutdown</span></a></li>
-              <li><a href="" ng-click="restart()"><span class="fas fa-fw fa-refresh"></span>&nbsp;<span translate>Restart</span></a></li>
-              <li class="divider" aria-hidden="true"></li>
-              <li class="visible-xs">
-                <a href="https://docs.syncthing.net/intro/gui.html" target="_blank">
-                  <span class="fas fa-fw fa-question-circle"></span>&nbsp;<span translate>Help</span>
-                </a>
-              </li>
-              <li><a href="" data-toggle="modal" data-target="#about"><span class="far fa-fw fa-heart"></span>&nbsp;<span translate>About</span></a></li>
-              <li class="divider" aria-hidden="true"></li>
-              <li><a href="" ng-click="advanced()"><span class="fas fa-fw fa-cogs"></span>&nbsp;<span translate>Advanced</span></a></li>
-              <li><a href="" ng-click="logging.show()"><span class="far fa-fw fa-file-alt"></span>&nbsp;<span translate>Logs</span></a></li>
-              <li class="divider" aria-hidden="true" ng-if="config.gui.debugging"></li>
-              <li><a href="/rest/debug/support" target="_blank" ng-if="config.gui.debugging"><span class="fa fa-user-md"></span>&nbsp;<span translate>Support Bundle</span></a></li>
-            </ul>
-          </li>
-        </ul>
-      </div>
-    </nav>
-
-    <div class="container content">
-
-      <!-- Panel: Open, no auth -->
-
-      <div ng-if="openNoAuth" class="row">
-        <div class="col-md-12">
-          <div class="panel panel-danger">
-            <div class="panel-heading">
-              <h3 class="panel-title">
-                <div class="panel-icon">
-                  <span class="fas fa-exclamation-circle"></span>
-                </div>
-                <span translate>Danger!</span>
-              </h3>
-            </div>
-            <div class="panel-body">
-              <p>
-              <span translate>The Syncthing admin interface is configured to allow remote access without a password.</span>
-              <b><span translate>This can easily give hackers access to read and change any files on your computer.</span></b>
-              <span translate>Please set a GUI Authentication User and Password in the Settings dialog.</span>
-              </p>
-            </div>
-            <div class="panel-footer">
-              <button type="button" class="btn btn-sm btn-default pull-right" ng-click="showSettings()">
-                <span class="fas fa-cog"></span>&nbsp;<span translate>Settings</span>
-              </button>
-              <div class="clearfix"></div>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- Panel: Restart Needed -->
-
-      <div ng-if="!configInSync" class="row">
-        <div class="col-md-12">
-          <div class="panel panel-warning">
-            <div class="panel-heading">
-              <h3 class="panel-title">
-                <div class="panel-icon">
-                  <span class="fas fa-exclamation-circle"></span>
-                </div>
-                <span translate>Restart Needed</span>
-              </h3>
-            </div>
-            <div class="panel-body">
-              <p translate>The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.</p>
-            </div>
-            <div class="panel-footer">
-              <button type="button" class="btn btn-sm btn-default pull-right" ng-click="restart()">
-                <span class="fas fa-refresh"></span>&nbsp;<span translate>Restart</span>
-              </button>
-              <div class="clearfix"></div>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <div ng-if="config">
-
-        <!-- Panel: Notifications -->
-
-        <div ng-if="config.options && config.options.unackedNotificationIDs" ng-include="'syncthing/core/notifications.html'"></div>
-
-        <!-- Panel: New Device -->
-
-        <div ng-repeat="(deviceID, pendingDevice) in pendingDevices" class="row">
-          <div class="col-md-12">
-            <div class="panel panel-warning">
-              <div class="panel-heading">
-                <h3 class="panel-title">
-                  <identicon class="panel-icon" data-value="device"></identicon>
-                  <span translate>New Device</span>
-                  <span class="pull-right">{{ pendingDevice.time | date:"yyyy-MM-dd HH:mm:ss" }}</span>
-                </h3>
-              </div>
-              <div class="panel-body">
-                <p>
-                  <span translate translate-value-device="{{ deviceID }}" translate-value-address="{{ pendingDevice.address }}" translate-value-name="{{ pendingDevice.name }}">
-                    Device "{%name%}" ({%device%} at {%address%}) wants to connect. Add new device?
-                  </span>
-                </p>
-              </div>
-              <div class="panel-footer clearfix">
-                <div class="pull-right">
-                  <button type="button" class="btn btn-sm btn-success" ng-click="addDevice(deviceID, pendingDevice.name)">
-                    <span class="fas fa-plus"></span>&nbsp;<span translate>Add Device</span>
-                  </button>
-                  <button type="button" class="btn btn-sm btn-danger" ng-click="ignoreDevice(deviceID, pendingDevice)">
-                    <span class="fas fa-times"></span>&nbsp;<span translate>Ignore</span>
-                  </button>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-
-        <!-- Panel: New Folder -->
-        <div ng-repeat="(folderID, pendingFolder) in pendingFolders">
-          <div ng-repeat="(deviceID, offeringDevice) in pendingFolder.offeredBy" class="row reject">
-            <div class="col-md-12">
-              <div class="panel panel-warning">
-                <div class="panel-heading">
-                  <h3 class="panel-title">
-                    <div class="panel-icon">
-                      <span class="fas fa-folder"></span>
-                    </div>
-                    <span translate ng-if="!folders[folderID]">New Folder</span>
-                    <span translate ng-if="folders[folderID]">Share Folder</span>
-                    <span class="pull-right">{{ offeringDevice.time | date:"yyyy-MM-dd HH:mm:ss" }}</span>
-                  </h3>
-                </div>
-                <div class="panel-body">
-                  <p>
-                    <span ng-if="offeringDevice.label.length == 0" translate translate-value-device="{{ deviceName(devices[deviceID]) }}" translate-value-folder="{{ folderID }}">
-                      {%device%} wants to share folder "{%folder%}".
-                    </span>
-                    <span ng-if="offeringDevice.label.length != 0" translate translate-value-device="{{ deviceName(devices[deviceID]) }}" translate-value-folder="{{ folderID }}" translate-value-folderlabel="{{ offeringDevice.label }}">
-                      {%device%} wants to share folder "{%folderlabel%}" ({%folder%}).
-                    </span>
-                    <span translate ng-if="folders[folderID]">Share this folder?</span>
-                    <span translate ng-if="!folders[folderID]">Add new folder?</span>
-                  </p>
-                </div>
-                <div class="panel-footer clearfix">
-                  <div class="pull-right">
-                    <button type="button" class="btn btn-sm btn-success" ng-click="addFolderAndShare(folderID, pendingFolder, deviceID)" ng-if="!folders[folderID]">
-                      <span class="fas fa-check"></span>&nbsp;<span translate>Add</span>
-                    </button>
-                    <button type="button" class="btn btn-sm btn-success" ng-click="shareFolderWithDevice(folderID, deviceID)" ng-if="folders[folderID]">
-                      <span class="fas fa-check"></span>&nbsp;<span translate>Share</span>
-                    </button>
-                    <button type="button" class="btn btn-sm btn-danger" ng-click="ignoreFolder(deviceID, folderID, offeringDevice)">
-                      <span class="fas fa-times"></span>&nbsp;<span translate>Ignore</span>
-                    </button>
-                  </div>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-
-      </div>
-
-      <!-- Panel: Notice -->
-
-      <div ng-if="errorList().length > 0" class="row">
-        <div class="col-md-12">
-          <div class="panel panel-warning">
-            <div class="panel-heading">
-              <h3 class="panel-title">
-                <div class="panel-icon">
-                  <span class="fas fa-exclamation-circle"></span>
-                </div>
-                <span translate>Notice</span>
-              </h3>
-            </div>
-            <div class="panel-body">
-              <p ng-repeat="err in errorList()">
-                <small>{{err.when | date:"yyyy-MM-dd HH:mm:ss"}}:</small>
-                <span ng-bind-html="friendlyDevices(err.message) | linky: '_blank'"></span>
-              </p>
-            </div>
-            <div class="panel-footer">
-              <button type="button" class="btn btn-sm btn-default pull-right" ng-click="clearErrors()">
-                <span class="fas fa-check"></span>&nbsp;<span translate>OK</span>
-              </button>
-              <div class="clearfix"></div>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- Panel: FS watcher errors -->
-
-      <div ng-if="sizeOf(fsWatcherErrorMap()) > 0" class="row">
-        <div class="col-md-12">
-          <div class="panel panel-warning">
-            <div class="panel-heading">
-              <h3 class="panel-title">
-                <div class="panel-icon">
-                  <span class="fas fa-exclamation-circle"></span>
-                </div>
-                <span translate>Filesystem Watcher Errors</span>
-              </h3>
-            </div>
-            <div class="panel-body">
-              <p>
-                <span translate>For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.</span>&emsp;<a href="https://forum.syncthing.net" target="_blank"><span class="fas fa-question-circle"></span>&nbsp;<span translate>Support</span></a>
-              </p>
-              <table>
-                <tr ng-repeat="(id, err) in fsWatcherErrorMap()">
-                  <td>{{folderLabel(id)}}</td><td>{{err}}</td>
-                </tr>
-              </table>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- First regular row -->
-
-      <div class="row">
-
-        <!-- Folder list (top left) -->
-
-        <div class="col-md-6" aria-labelledby="folder_list" role="region" >
-          <h3 id="folder_list" translate>Folders</h3>
-          <div class="panel-group" id="folders">
-            <div class="panel panel-default" ng-repeat="folder in folderList()">
-              <button class="btn panel-heading" data-toggle="collapse" data-parent="#folders" data-target="#folder-{{$index}}" aria-expanded="false">
-                <div class="panel-progress" ng-show="folderStatus(folder) == 'syncing'" ng-attr-style="width: {{syncPercentage(folder.id) | percent}}"></div>
-                <div class="panel-progress" ng-show="folderStatus(folder) == 'scanning' && scanProgress[folder.id] != undefined" ng-attr-style="width: {{scanPercentage(folder.id) | percent}}"></div>
-                <h4 class="panel-title">
-                  <div class="panel-icon hidden-xs">
-                    <span ng-if="folder.type == 'sendreceive'" class="fas fa-fw fa-folder"></span>
-                    <span ng-if="folder.type == 'sendonly'" class="fas fa-fw fa-upload"></span>
-                    <span ng-if="folder.type == 'receiveonly'" class="fas fa-fw fa-download"></span>
-                    <span ng-if="folder.type == 'receiveencrypted'" class="fas fa-fw fa-lock"></span>
-                  </div>
-                  <div class="panel-status pull-right text-{{folderClass(folder)}}" ng-switch="folderStatus(folder)">
-                    <span ng-switch-when="paused"><span class="hidden-xs" translate>Paused</span><span class="visible-xs" aria-label="{{'Paused' | translate}}"><i class="fas fa-fw fa-pause"></i></span></span>
-                    <span ng-switch-when="unknown"><span class="hidden-xs" translate>Unknown</span><span class="visible-xs" aria-label="{{'Unknown' | translate}}"><i class="fas fa-fw fa-question-circle"></i></span></span>
-                    <span ng-switch-when="unshared"><span class="hidden-xs" translate>Unshared</span><span class="visible-xs" aria-label="{{'Unshared' | translate}}"><i class="fas fa-fw fa-unlink"></i></span></span>
-                    <span ng-switch-when="scan-waiting"><span class="hidden-xs" translate>Waiting to Scan</span><span class="visible-xs" aria-label="{{'Waiting to Scan' | translate}}"><i class="fas fa-fw fa-hourglass-half"></i></span></span>
-                    <span ng-switch-when="cleaning"><span class="hidden-xs" translate>Cleaning Versions</span><span class="visible-xs" aria-label="{{'Cleaning Versions' | translate}}"><i class="fas fa-fw fa-recycle"></i></span></span>
-                    <span ng-switch-when="clean-waiting"><span class="hidden-xs" translate>Waiting to Clean</span><span class="visible-xs" aria-label="{{'Waiting to Clean' | translate}}"><i class="fas fa-fw fa-hourglass-half"></i></span></span>
-                    <span ng-switch-when="stopped"><span class="hidden-xs" translate>Stopped</span><span class="visible-xs" aria-label="{{'Stopped' | translate}}"><i class="fas fa-fw fa-stop"></i></span></span>
-                    <span ng-switch-when="scanning">
-                      <span class="hidden-xs" translate>Scanning</span>
-                      <span class="hidden-xs" ng-if="scanPercentage(folder.id) != undefined">
-                        ({{scanPercentage(folder.id) | percent}})
-                      </span>
-                      <span class="visible-xs" aria-label="{{'Scanning' | translate}}"><i class="fas fa-fw fa-search"></i></span>
-                    </span>
-                    <span ng-switch-when="idle"><span class="hidden-xs" translate>Up to Date</span><span class="visible-xs" aria-label="{{'Up to Date' | translate}}"><i class="fas fa-fw fa-check"></i></span></span>
-                    <span ng-switch-when="localadditions"><span class="hidden-xs" translate>Local Additions</span><span class="visible-xs" aria-label="{{'Local Additions' | translate}}"><i class="fas fa-fw fa-check"></i></span></span>
-                    <span ng-switch-when="sync-waiting">
-                      <span class="hidden-xs" translate>Waiting to Sync</span>
-                      <span class="visible-xs" aria-label="{{'Waiting to Sync' | translate}}"><i class="fas fa-fw fa-hourglass-half"></i></span>
-                    </span>
-                    <span ng-switch-when="sync-preparing">
-                      <span class="hidden-xs" translate>Preparing to Sync</span>
-                      <span class="visible-xs" aria-label="{{'Preparing to Sync' | translate}}"><i class="fas fa-fw fa-hourglass-half"></i></span>
-                    </span>
-                    <span ng-switch-when="syncing">
-                      <span class="hidden-xs" translate>Syncing</span>
-                      <span>({{syncPercentage(folder.id) | percent}}, {{model[folder.id].needBytes | binary}}B)</span>
-                    </span>
-                    <span ng-switch-when="outofsync"><span class="hidden-xs" translate>Out of Sync</span><span class="visible-xs" aria-label="{{'Out of Sync' | translate}}"><i class="fas fa-fw fa-exclamation-circle"></i></span></span>
-                    <span ng-switch-when="faileditems"><span class="hidden-xs" translate>Failed Items</span><span class="visible-xs" aria-label="{{'Failed Items' | translate}}"><i class="fas fa-fw fa-exclamation-circle"></i></span></span>
-                    <span ng-switch-when="localunencrypted"><span class="hidden-xs">{{'Unexpected Items' | translate}}</span><span class="visible-xs" aria-label="{{'Unexpected Items' | translate}}"><i class="fas fa-fw fa-exclamation-circle"></i></span></span>
-                  </div>
-                  <div class="panel-title-text">
-                    <span tooltip data-original-title="{{folder.label.length != 0 ? folder.id : ''}}">{{folder.label.length != 0 ? folder.label : folder.id}}</span>
-                  </div>
-                </h4>
-              </button>
-              <div id="folder-{{$index}}" class="panel-collapse collapse">
-                <div class="panel-body">
-                  <table class="table table-condensed table-striped table-auto">
-                    <tbody>
-                      <tr ng-show="folder.label != undefined && folder.label.length > 0">
-                        <th><span class="fas fa-fw fa-info-circle"></span>&nbsp;<span translate>Folder ID</span></th>
-                        <td class="text-right no-overflow-ellipse">{{folder.id}}</td>
-                      </tr>
-                      <tr>
-                        <th><span class="fas fa-fw fa-folder-open"></span>&nbsp;<span translate>Folder Path</span></th>
-                        <td class="text-right">
-                          <span tooltip data-original-title="{{folder.path}}">{{folder.path}}</span>
-                        </td>
-                      </tr>
-                      <tr ng-if="!folder.paused && (model[folder.id].invalid || model[folder.id].error)">
-                        <th><span class="fas fa-fw fa-exclamation-triangle"></span>&nbsp;<span translate>Error</span></th>
-                        <td class="text-right">{{model[folder.id].invalid || model[folder.id].error}}</td>
-                      </tr>
-                      <tr ng-if="!folder.paused">
-                        <th><span class="fas fa-fw fa-globe"></span>&nbsp;<span translate>Global State</span></th>
-                        <td class="text-right">
-                          <span tooltip data-original-title="{{model[folder.id].globalFiles | alwaysNumber | localeNumber}} {{'files' | translate}}, {{model[folder.id].globalDirectories | alwaysNumber | localeNumber}} {{'directories' | translate}}, ~{{model[folder.id].globalBytes | binary}}B">
-                            <span class="far fa-copy"></span>&nbsp;{{model[folder.id].globalFiles | alwaysNumber | localeNumber}}&ensp;
-                            <span class="far fa-folder"></span>&nbsp;{{model[folder.id].globalDirectories | alwaysNumber | localeNumber}}&ensp;
-                            <span class="far fa-hdd"></span>&nbsp;~{{model[folder.id].globalBytes | binary}}B
-                          </span>
-                        </td>
-                      </tr>
-                      <tr ng-if="!folder.paused">
-                        <th><span class="fas fa-fw fa-home"></span>&nbsp;<span translate>Local State</span></th>
-                        <td class="text-right">
-                          <span tooltip data-original-title="{{model[folder.id].localFiles | alwaysNumber | localeNumber}} {{'files' | translate}}, {{model[folder.id].localDirectories | alwaysNumber | localeNumber}} {{'directories' | translate}}, ~{{model[folder.id].localBytes | binary}}B">
-                            <span class="far fa-copy"></span>&nbsp;{{model[folder.id].localFiles | alwaysNumber | localeNumber}}&ensp;
-                            <span class="far fa-folder"></span>&nbsp;{{model[folder.id].localDirectories | alwaysNumber | localeNumber}}&ensp;
-                            <span class="far fa-hdd"></span>&nbsp;~{{model[folder.id].localBytes | binary}}B<!-- get rid of the annoying trailing whitespace
-                            --><span ng-if="model[folder.id].ignorePatterns"><br/><i><small translate class="text-muted">Reduced by ignore patterns</small></i></span>
-                          </span>
-                        </td>
-                      </tr>
-                      <tr ng-if="model[folder.id].needTotalItems > 0">
-                        <th><span class="fas fa-fw fa-cloud-download-alt"></span>&nbsp;<span translate>Out of Sync Items</span></th>
-                        <td class="text-right">
-                          <a href="" ng-click="showNeed(folder.id)">{{model[folder.id].needTotalItems | alwaysNumber | localeNumber}} <span translate>items</span>, ~{{model[folder.id].needBytes | binary}}B</a>
-                        </td>
-                      </tr>
-                      <tr ng-if="folderStatus(folder) === 'scanning' && scanRate(folder.id) > 0">
-                        <th><span class="fas fa-fw fa-hourglass-half"></span>&nbsp;<span translate>Scan Time Remaining</span></th>
-                        <td class="text-right">
-                          <span tooltip data-original-title="{{scanRate(folder.id) | binary}}B/s">~ {{scanRemaining(folder.id)}}</span>
-                        </td>
-                      </tr>
-                      <tr ng-if="hasFailedFiles(folder.id)">
-                        <th><span class="fas fa-fw fa-exclamation-circle"></span>&nbsp;<span translate>Failed Items</span></th>
-                        <!-- Show the number of failed items as a link to bring up the list. -->
-                        <td class="text-right">
-                          <a href="" ng-click="showFailed(folder.id)">{{model[folder.id].pullErrors | alwaysNumber | localeNumber}}&nbsp;<span translate>items</span></a>
-                        </td>
-                      </tr>
-                      <tr ng-if="hasReceiveOnlyChanged(folder)">
-                        <th><span class="fas fa-fw fa-exclamation-circle"></span>&nbsp;<span translate>Locally Changed Items</span></th>
-                        <td class="text-right">
-                          <a href="" ng-click="showLocalChanged(folder.id, folder.type)">{{model[folder.id].receiveOnlyTotalItems | alwaysNumber | localeNumber}} <span translate>items</span>, ~{{model[folder.id].receiveOnlyChangedBytes | binary}}B</a>
-                        </td>
-                      </tr>
-                      <tr ng-if="hasReceiveEncryptedItems(folder)">
-                        <th><span class="fas fa-fw fa-exclamation-circle"></span>&nbsp;<span translate>Locally Changed Items</span></th>
-                        <td class="text-right">
-                          <a href="" ng-click="showLocalChanged(folder.id, folder.type)">{{receiveEncryptedItemsCount(folder) | alwaysNumber | localeNumber}} <span translate>items</span>, ~{{model[folder.id].receiveOnlyChangedBytes | binary}}B</a>
-                        </td>
-                      </tr>
-                      <tr ng-if="folder.type != 'sendreceive'">
-                        <th><span class="fas fa-fw fa-folder"></span>&nbsp;<span translate>Folder Type</span></th>
-                        <td class="text-right">
-                          <span ng-if="folder.type == 'sendonly'" translate>Send Only</span>
-                          <span ng-if="folder.type == 'receiveonly'" translate>Receive Only</span>
-                          <span ng-if="folder.type == 'receiveencrypted'" translate>Receive Encrypted</span>
-                        </td>
-                      </tr>
-                      <tr ng-if="folder.ignorePerms">
-                        <th><span class="far fa-fw fa-minus-square"></span>&nbsp;<span translate>Ignore Permissions</span></th>
-                        <td class="text-right">
-                          <span translate>Yes</span>
-                        </td>
-                      </tr>
-                      <tr>
-                        <th><span class="fas fa-fw fa-refresh"></span>&nbsp;<span translate>Rescans</span></th>
-                        <td class="text-right">
-                          <div ng-if="folder.rescanIntervalS > 0">
-                            <span ng-if="!folder.fsWatcherEnabled" tooltip data-original-title="{{'Periodic scanning at given interval and disabled watching for changes' | translate}}">
-                              <span class="far fa-clock"></span>&nbsp;{{folder.rescanIntervalS | duration}}&ensp;
-                              <span class="fas fa-eye-slash"></span>&nbsp;<span translate>Disabled</span>
-                            </span>
-                            <span ng-if="folder.fsWatcherEnabled && (!model[folder.id].watchError || folder.paused || folderStatus(folder) === 'stopped')" tooltip data-original-title="{{'Periodic scanning at given interval and enabled watching for changes' | translate}}">
-                              <span class="far fa-clock"></span>&nbsp;{{folder.rescanIntervalS | duration}}&ensp;
-                              <span class="fas fa-eye"></span>&nbsp;<span translate>Enabled</span>
-                            </span>
-                            <span ng-if="folder.fsWatcherEnabled && !folder.paused && folderStatus(folder) !== 'stopped' && model[folder.id].watchError" tooltip data-original-title="{{'Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:' | translate}}<br/>{{model[folder.id].watchError}}">
-                              <span class="far fa-clock"></span>&nbsp;{{folder.rescanIntervalS | duration}}&ensp;
-                              <span class="fas fa-eye-slash"></span>&nbsp;<span translate>Failed to setup, retrying</span>
-                            </span>
-                          </div>
-                          <div ng-if="folder.rescanIntervalS <= 0">
-                            <span ng-if="!folder.fsWatcherEnabled" tooltip data-original-title="{{'Disabled periodic scanning and disabled watching for changes' | translate}}">
-                              <span class="far fa-clock"></span>&nbsp;<span translate>Disabled</span>&ensp;
-                              <span class="fas fa-eye-slash"></span>&nbsp;<span translate>Disabled</span>
-                            </span>
-                            <span ng-if="folder.fsWatcherEnabled && (!model[folder.id].watchError || folder.paused || folderStatus(folder) === 'stopped')" tooltip data-original-title="{{'Disabled periodic scanning and enabled watching for changes' | translate}}">
-                              <span class="far fa-clock"></span>&nbsp;<span translate>Disabled</span>&ensp;
-                              <span class="fas fa-eye"></span>&nbsp;<span translate>Enabled</span>
-                            </span>
-                            <span ng-if="folder.fsWatcherEnabled && !folder.paused && folderStatus(folder) !== 'stopped' && model[folder.id].watchError" tooltip data-original-title="{{'Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:' | translate}}<br/>{{model[folder.id].watchError}}">
-                              <span class="far fa-clock"></span>&nbsp;<span translate>Disabled</span>&ensp;
-                              <span class="fas fa-eye-slash"></span>&nbsp;<span translate>Failed to setup, retrying</span>
-                            </span>
-                          </div>
-                        </td>
-                      </tr>
-                      <tr ng-if="folder.order != 'random' && folder.type != 'sendonly'">
-                        <th><span class="fas fa-fw fa-sort"></span>&nbsp;<span translate>File Pull Order</span></th>
-                        <td class="text-right" ng-switch="folder.order">
-                          <span ng-switch-when="random" translate>Random</span>
-                          <span ng-switch-when="alphabetic" translate>Alphabetic</span>
-                          <span ng-switch-when="smallestFirst" translate>Smallest First</span>
-                          <span ng-switch-when="largestFirst" translate>Largest First</span>
-                          <span ng-switch-when="oldestFirst" translate>Oldest First</span>
-                          <span ng-switch-when="newestFirst" translate>Newest First</span>
-                        </td>
-                      </tr>
-                      <tr ng-if="folder.versioning.type">
-                        <th><span class="far fa-fw fa-copy"></span>&nbsp;<span translate>File Versioning</span></th>
-                        <td class="text-right" ng-switch="folder.versioning.type">
-                          <span ng-switch-when="trashcan" translate>Trash Can File Versioning</span>
-                          <span ng-switch-when="staggered" translate>Staggered File Versioning</span>
-                          <span ng-switch-when="simple" translate>Simple File Versioning</span>
-                          <span ng-switch-when="external" translate>External File Versioning</span>
-                        </td>
-                      </tr>
-                      <tr>
-                        <th><span class="fas fa-fw fa-share-alt"></span>&nbsp;<span translate>Shared With</span></th>
-                        <td class="text-right" ng-attr-title="{{sharesFolder(folder)}}">{{sharesFolder(folder)}}</td>
-                      </tr>
-                      <tr ng-if="folderStats[folder.id].lastScan">
-                        <th><span class="far fa-fw fa-clock"></span>&nbsp;<span translate>Last Scan</span></th>
-                        <td translate ng-if="folderStats[folder.id].lastScanDays >= 365" class="text-right">Never</td>
-                        <td ng-if="folderStats[folder.id].lastScanDays < 365" class="text-right">
-                          <span>{{folderStats[folder.id].lastScan | date:'yyyy-MM-dd HH:mm:ss'}}</span>
-                        </td>
-                      </tr>
-                      <tr ng-if="folder.type != 'sendonly' && folder.type != 'receiveencrypted' && folderStats[folder.id].lastFile && folderStats[folder.id].lastFile.filename">
-                        <th><span class="fas fa-fw fa-exchange-alt"></span>&nbsp;<span translate>Latest Change</span></th>
-                        <td class="text-right">
-                          <span tooltip data-original-title="{{folderStats[folder.id].lastFile.filename}} @ {{folderStats[folder.id].lastFile.at | date:'yyyy-MM-dd HH:mm:ss'}}">
-                            <span translate ng-if="!folderStats[folder.id].lastFile.deleted">Updated</span>
-                            <span translate ng-if="folderStats[folder.id].lastFile.deleted">Deleted</span>
-                            {{folderStats[folder.id].lastFile.filename | basename}}
-                          </span>
-                        </td>
-                      </tr>
-                    </tbody>
-                  </table>
-                </div>
-                <div class="panel-footer">
-                  <button type="button" class="btn btn-sm btn-danger pull-left" ng-click="revertOverrideConfirmationModal('override', folder.id)" ng-if="folderStatus(folder) == 'outofsync' && folder.type == 'sendonly'">
-                    <span class="fas fa-arrow-circle-up"></span>&nbsp;<span translate>Override Changes</span>
-                  </button>
-                  <button type="button" class="btn btn-sm btn-danger pull-left" ng-click="revertOverrideConfirmationModal('revert', folder.id)" ng-if="hasReceiveOnlyChanged(folder)">
-                    <span class="fa fa-arrow-circle-down"></span>&nbsp;<span translate>Revert Local Changes</span>
-                  </button>
-                  <button type="button" class="btn btn-sm btn-danger pull-left" ng-click="revertOverrideConfirmationModal('deleteEnc', folder.id)" ng-if="hasReceiveEncryptedItems(folder)">
-                    <span class="fa fa-arrow-lock"></span>&nbsp;<span translate>Delete Unexpected Items</span>
-                  </button>
-                  <span class="pull-right">
-                    <button ng-if="!folder.paused" type="button" class="btn btn-sm btn-default" ng-click="setFolderPause(folder.id, true)">
-                      <span class="fas fa-pause"></span>&nbsp;<span translate>Pause</span>
-                    </button>
-                    <button ng-if="folder.paused" type="button" class="btn btn-sm btn-default" ng-click="setFolderPause(folder.id, false)">
-                      <span class="fas fa-play"></span>&nbsp;<span translate>Resume</span>
-                    </button>
-                    <button type="button" class="btn btn-default btn-sm" ng-click="restoreVersions.show(folder.id)" ng-if="folder.versioning.type && folder.versioning.type != 'external'">
-                      <span class="fas fa-undo"></span>&nbsp;<span translate>Versions</span>
-                    </button>
-                    <button type="button" class="btn btn-sm btn-default" ng-click="rescanFolder(folder.id)" ng-disabled="['idle', 'stopped', 'unshared', 'outofsync', 'faileditems', 'localadditions'].indexOf(folderStatus(folder)) < 0">
-                      <span class="fas fa-refresh"></span>&nbsp;<span translate>Rescan</span>
-                    </button>
-                    <button type="button" class="btn btn-sm btn-default" ng-click="editFolderExisting(folder)">
-                      <span class="fas fa-pencil-alt"></span>&nbsp;<span translate>Edit</span>
-                    </button>
-                  </span>
-                  <div class="clearfix"></div>
-                </div>
-              </div>
-            </div>
-          </div>
-          <span class="pull-right">
-            <button type="button" class="btn btn-sm btn-default" ng-click="setAllFoldersPause(true)" ng-if="isAtleastOneFolderPausedStateSetTo(false)">
-              <span class="fas fa-pause"></span>&nbsp;<span translate>Pause All</span>
-            </button>
-            <button type="button" class="btn btn-sm btn-default" ng-click="setAllFoldersPause(false)" ng-if="isAtleastOneFolderPausedStateSetTo(true)">
-              <span class="fas fa-play"></span>&nbsp;<span translate>Resume All</span>
-            </button>
-            <button type="button" class="btn btn-sm btn-default" ng-click="rescanAllFolders()" ng-if="folderList().length > 0" ng-disabled="!isAtleastOneFolderPausedStateSetTo(false)">
-              <span class="fas fa-refresh"></span>&nbsp;<span translate>Rescan All</span>
-            </button>
-            <button type="button" class="btn btn-sm btn-default" ng-click="addFolder()">
-              <span class="fas fa-plus"></span>&nbsp;<span translate>Add Folder</span>
-            </button>
-          </span>
-          <div class="clearfix"></div>
-          <hr class="visible-sm"/>
-        </div>
-
-        <!-- Device list (top right) -->
-
-        <!-- This device -->
-
-        <div class="col-md-6" aria-label="{{'Devices' | translate}}" role="region">
-          <h3 translate>This Device</h3>
-          <div class="panel panel-default" ng-repeat="deviceCfg in [thisDevice()]">
-            <button class="btn panel-heading" data-toggle="collapse" data-target="#device-this" aria-expanded="true">
-              <h4 class="panel-title">
-                <identicon class="panel-icon" data-value="deviceCfg.deviceID"></identicon>
-                <div class="panel-title-text">{{deviceName(deviceCfg)}}</div>
-              </h4>
-            </button>
-            <div id="device-this" class="panel-collapse collapse in">
-              <div class="panel-body">
-                <table class="table table-condensed table-striped table-auto">
-                  <tbody>
-                    <tr>
-                      <th><span class="fas fa-fw fa-cloud-download-alt"></span>&nbsp;<span translate>Download Rate</span></th>
-                      <td class="text-right">
-                        <a href="#" class="toggler" ng-click="toggleUnits()">
-                          <span ng-if="!metricRates">{{connectionsTotal.inbps | binary}}B/s</span>
-                          <span ng-if="metricRates">{{connectionsTotal.inbps*8 | metric}}bps</span>
-                          ({{connectionsTotal.inBytesTotal | binary}}B)
-                          <small ng-if="config.options.maxRecvKbps > 0"><br/>
-                            <i class="text-muted"><span translate>Limit</span>:
-                              <span ng-if="!metricRates">{{config.options.maxRecvKbps*1024 | binary}}B/s</span>
-                              <span ng-if="metricRates">{{config.options.maxRecvKbps*1024*8 | metric}}bps</span>
-                            </i>
-                          </small>
-                        </a>
-                      </td>
-                    </tr>
-                    <tr>
-                      <th><span class="fas fa-fw fa-cloud-upload-alt"></span>&nbsp;<span translate>Upload Rate</span></th>
-                      <td class="text-right">
-                        <a href="#" class="toggler" ng-click="toggleUnits()">
-                          <span ng-if="!metricRates">{{connectionsTotal.outbps | binary}}B/s</span>
-                          <span ng-if="metricRates">{{connectionsTotal.outbps*8 | metric}}bps</span>
-                          ({{connectionsTotal.outBytesTotal | binary}}B)
-                          <small ng-if="config.options.maxSendKbps > 0"><br/>
-                            <i class="text-muted"><span translate>Limit</span>:
-                              <span ng-if="!metricRates">{{config.options.maxSendKbps*1024 | binary}}B/s</span>
-                              <span ng-if="metricRates">{{config.options.maxSendKbps*1024*8 | metric}}bps</span>
-                            </i>
-                          </small>
-                        </a>
-                      </td>
-                    </tr>
-                    <tr>
-                      <th><span class="fas fa-fw fa-home"></span>&nbsp;<span translate>Local State (Total)</span></th>
-                      <td class="text-right">
-                          <span tooltip data-original-title="{{localStateTotal.files | alwaysNumber | localeNumber}} {{'files' | translate}}, {{ localStateTotal.directories | alwaysNumber | localeNumber}} {{'directories' | translate}}, ~{{ localStateTotal.bytes | binary}}B">
-                            <span class="far fa-copy"></span>&nbsp;{{localStateTotal.files | alwaysNumber | localeNumber}}&ensp;
-                            <span class="far fa-folder"></span>&nbsp;{{localStateTotal.directories| alwaysNumber | localeNumber}}&ensp;
-                            <span class="far fa-hdd"></span>&nbsp;~{{localStateTotal.bytes | binary}}B
-                          </span>
-                      </td>
-                    </tr>
-                    <tr>
-                      <th><span class="fas fa-fw fa-sitemap"></span>&nbsp;<span translate>Listeners</span></th>
-                      <td class="text-right">
-                        <span ng-if="listenersFailed.length == 0" class="data text-success">
-                          <span>{{listenersTotal}}/{{listenersTotal}}</span>
-                        </span>
-                        <span ng-if="listenersFailed.length != 0" class="data" ng-class="{'text-danger': listenersFailed.length == listenersTotal}">
-                          <span popover data-trigger="hover" data-placement="bottom" data-html="true" data-content="{{listenersFailed.join('<br>\n')}}">
-                            {{listenersTotal-listenersFailed.length}}/{{listenersTotal}}
-                          </span>
-                        </span>
-                      </td>
-                    </tr>
-                    <tr ng-if="system.discoveryEnabled">
-                      <th><span class="fas fa-fw fa-map-signs"></span>&nbsp;<span translate>Discovery</span></th>
-                      <td class="text-right">
-                        <span ng-if="discoveryFailed.length == 0" class="data text-success">
-                          <span>{{discoveryTotal}}/{{discoveryTotal}}</span>
-                        </span>
-                        <span ng-if="discoveryFailed.length != 0" class="data" ng-class="{'text-danger': discoveryFailed.length == discoveryTotal}">
-                          <span popover data-trigger="hover" data-placement="bottom" data-content="{{'Click to see discovery failures' | translate}}.">
-                            <a href="" style="color:inherit" ng-click="showDiscoveryFailures()">{{discoveryTotal-discoveryFailed.length}}/{{discoveryTotal}}</a>
-                          </span>
-                        </span>
-                      </td>
-                    </tr>
-                    <tr>
-                      <th><span class="far fa-fw fa-clock"></span>&nbsp;<span translate>Uptime</span></th>
-                      <td class="text-right">{{system.uptime | duration:"m"}}</td>
-                    </tr>
-                    <tr>
-                      <th><span class="fas fa-fw fa-tag"></span>&nbsp;<span translate>Version</span></th>
-                      <td class="text-right">
-                        <span tooltip data-original-title="{{versionString()}}">{{versionString()}}</span>
-                      </td>
-                    </tr>
-                  </tbody>
-                </table>
-              </div>
-            </div>
-          </div>
-
-          <!-- Remote devices -->
-          <h3 translate>Remote Devices</h3>
-          <div class="panel-group" id="devices">
-            <div class="panel panel-default" ng-repeat="deviceCfg in otherDevices()">
-              <button class="btn panel-heading" data-toggle="collapse" data-parent="#devices" data-target="#device-{{$index}}" aria-expanded="false">
-                <div class="panel-progress" ng-show="deviceStatus(deviceCfg) == 'syncing'" ng-attr-style="width: {{completion[deviceCfg.deviceID]._total | percent}}"></div>
-                <h4 class="panel-title">
-                  <identicon class="panel-icon" data-value="deviceCfg.deviceID"></identicon>
-                  <span ng-switch="deviceStatus(deviceCfg)" class="pull-right text-{{deviceClass(deviceCfg)}}">
-                    <span ng-switch-when="insync"><span class="hidden-xs" translate>Up to Date</span><span class="visible-xs" aria-label="{{'Up to Date' | translate}}"><i class="fas fa-fw fa-check"></i></span></span>
-                    <span ng-switch-when="unused-insync"><span class="hidden-xs" translate>Connected (Unused)</span><span class="visible-xs" aria-label="{{'Connected (Unused)' | translate}}"><i class="fas fa-fw fa-unlink"></i></span></span>
-                    <span ng-switch-when="syncing">
-                      <span class="hidden-xs" translate>Syncing</span> ({{completion[deviceCfg.deviceID]._total | percent}}, {{completion[deviceCfg.deviceID]._needBytes | binary}}B)
-                    </span>
-                    <span ng-switch-when="paused"><span class="hidden-xs" translate>Paused</span><span class="visible-xs" aria-label="{{'Paused' | translate}}"><i class="fas fa-fw fa-pause"></i></span></span>
-                    <span ng-switch-when="unused-paused"><span class="hidden-xs" translate>Paused (Unused)</span><span class="visible-xs" aria-label="{{'Paused (Unused)' | translate}}"><i class="fas fa-fw fa-unlink"></i></span></span>
-                    <span ng-switch-when="disconnected"><span class="hidden-xs" translate>Disconnected</span><span class="visible-xs" aria-label="{{'Disconnected' | translate}}"><i class="fas fa-fw fa-power-off"></i></span></span>
-                    <span ng-switch-when="unused-disconnected"><span class="hidden-xs" translate>Disconnected (Unused)</span><span class="visible-xs" aria-label="{{'Disconnected (Unused)' | translate}}"><i class="fas fa-fw fa-unlink"></i></span></span>
-                  </span>
-                  <span>{{deviceName(deviceCfg)}}</span>
-                </h4>
-              </button>
-              <div id="device-{{$index}}" class="panel-collapse collapse">
-                <div class="panel-body">
-                  <table class="table table-condensed table-striped table-auto">
-                    <tbody>
-                      <tr ng-if="!connections[deviceCfg.deviceID].connected">
-                        <th><span class="fas fa-fw fa-eye"></span>&nbsp;<span translate>Last seen</span></th>
-                        <td translate ng-if="!deviceStats[deviceCfg.deviceID].lastSeenDays || deviceStats[deviceCfg.deviceID].lastSeenDays >= 365" class="text-right">Never</td>
-                        <td ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays < 365" class="text-right">{{deviceStats[deviceCfg.deviceID].lastSeen | date:"yyyy-MM-dd HH:mm:ss"}}</td>
-                      </tr>
-                      <tr ng-if="connections[deviceCfg.deviceID].connected">
-                        <th><span class="fas fa-fw fa-cloud-download-alt"></span>&nbsp;<span translate>Download Rate</span></th>
-                        <td class="text-right">
-                          <a href="#" class="toggler" ng-click="toggleUnits()">
-                            <span ng-if="!metricRates">{{connections[deviceCfg.deviceID].inbps | binary}}B/s</span>
-                            <span ng-if="metricRates">{{connections[deviceCfg.deviceID].inbps*8 | metric}}bps</span>
-                            ({{connections[deviceCfg.deviceID].inBytesTotal | binary}}B)
-                            <small ng-if="deviceCfg.maxRecvKbps > 0"><br/>
-                              <i class="text-muted"><span translate>Limit</span>:
-                                <span ng-if="!metricRates">{{deviceCfg.maxRecvKbps*1024 | binary}}B/s</span>
-                                <span ng-if="metricRates">{{deviceCfg.maxRecvKbps*1024*8 | metric}}bps</span>
-                              </i>
-                            </small>
-                          </a>
-                        </td>
-                      </tr>
-                      <tr ng-if="connections[deviceCfg.deviceID].connected">
-                        <th><span class="fas fa-fw fa-cloud-upload-alt"></span>&nbsp;<span translate>Upload Rate</span></th>
-                        <td class="text-right">
-                          <a href="#" class="toggler" ng-click="toggleUnits()">
-                            <span ng-if="!metricRates">{{connections[deviceCfg.deviceID].outbps | binary}}B/s</span>
-                            <span ng-if="metricRates">{{connections[deviceCfg.deviceID].outbps*8 | metric}}bps</span>
-                            ({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B)
-                            <small ng-if="deviceCfg.maxSendKbps > 0"><br/>
-                              <i class="text-muted"><span translate>Limit</span>:
-                                <span ng-if="!metricRates">{{deviceCfg.maxSendKbps*1024 | binary}}B/s</span>
-                                <span ng-if="metricRates">{{deviceCfg.maxSendKbps*1024*8 | metric}}bps</span>
-                              </i>
-                            </small>
-                          </a>
-                        </td>
-                      </tr>
-                      <tr ng-if="deviceStatus(deviceCfg) == 'syncing'">
-                        <th><span class="fas fa-fw fa-exchange-alt"></span>&nbsp;<span translate>Out of Sync Items</span></th>
-                        <td class="text-right">
-                          <a href="" ng-click="showRemoteNeed(deviceCfg)">{{completion[deviceCfg.deviceID]._needItems | alwaysNumber | localeNumber}} <span translate>items</span>, ~{{completion[deviceCfg.deviceID]._needBytes | binary}}B</a>
-                        </td>
-                      </tr>
-                      <tr>
-                        <th><span class="fas fa-fw fa-link"></span>&nbsp<span translate>Address</span></th>
-                        <td ng-if="connections[deviceCfg.deviceID].connected" class="text-right">
-                          <span tooltip data-original-title="{{ connections[deviceCfg.deviceID].type.indexOf('Relay') > -1 ? '' : connections[deviceCfg.deviceID].type }} {{ connections[deviceCfg.deviceID].crypto }}">
-                            {{deviceAddr(deviceCfg)}}
-                          </span>
-                        </td>
-                        <td ng-if="!connections[deviceCfg.deviceID].connected" class="text-right">
-                          <span ng-repeat="addr in deviceCfg.addresses">
-                            <span tooltip data-original-title="{{'Configured' | translate}}">{{addr}}</span><br>
-                            <small ng-if="system.lastDialStatus[addr].error" tooltip data-original-title="{{system.lastDialStatus[addr].error}}" class="text-danger">{{abbreviatedError(addr)}}<br></small>
-                          </span>
-                          <span ng-repeat="addr in discoveryCache[deviceCfg.deviceID].addresses">
-                            <span tooltip data-original-title="{{'Discovered' | translate}}">{{addr}}</span><br>
-                            <small ng-if="system.lastDialStatus[addr].error" tooltip data-original-title="{{system.lastDialStatus[addr].error}}" class="text-danger">{{abbreviatedError(addr)}}<br></small>
-                          </span>
-                        </td>
-                      </tr>
-                      <tr ng-if="connections[deviceCfg.deviceID].connected && connections[deviceCfg.deviceID].type.indexOf('Relay') > -1" tooltip data-original-title="Connections via relays might be rate limited by the relay">
-                        <th><span class="fas fa-fw fa-exclamation-triangle text-danger"></span>&nbsp;<span translate>Connection Type</span></th>
-                        <td class="text-right">{{connections[deviceCfg.deviceID].type}}</td>
-                      </tr>
-                      <tr ng-if="deviceCfg.allowedNetworks.length > 0">
-                        <th><span class="fas fa-fw fa-filter"></span>&nbsp;<span translate>Allowed Networks</span></th>
-                        <td class="text-right">
-                          <span>{{deviceCfg.allowedNetworks.join(", ")}}</span>
-                        </td>
-                      </tr>
-                      <tr ng-if="deviceCfg.compression != 'metadata'">
-                        <th><span class="fas fa-fw fa-compress"></span>&nbsp;<span translate>Compression</span></th>
-                        <td class="text-right">
-                          <span ng-if="deviceCfg.compression == 'always'" translate>All Data</span>
-                          <span ng-if="deviceCfg.compression == 'never'" translate>Off</span>
-                        </td>
-                      </tr>
-                      <tr ng-if="deviceCfg.introducer">
-                        <th><span class="far fa-fw fa-thumbs-up"></span>&nbsp;<span translate>Introducer</span></th>
-                        <td translate class="text-right">Yes</td>
-                      </tr>
-                      <tr ng-if="deviceCfg.introducedBy">
-                        <th><span class="far fa-fw fa-handshake-o"></span>&nbsp;<span translate>Introduced By</span></th>
-                        <td class="text-right">{{ deviceName(devices[deviceCfg.introducedBy]) || deviceCfg.introducedBy.substring(0, 5) }}</td>
-                      </tr>
-                      <tr ng-if="connections[deviceCfg.deviceID].clientVersion">
-                        <th><span class="fas fa-fw fa-tag"></span>&nbsp;<span translate>Version</span></th>
-                        <td class="text-right">{{connections[deviceCfg.deviceID].clientVersion}}</td>
-                      </tr>
-                      <tr ng-if="deviceFolders(deviceCfg).length > 0">
-                        <th><span class="fas fa-fw fa-folder"></span>&nbsp;<span translate>Folders</span></th>
-                        <td class="text-right" ng-attr-title="{{deviceFolders(deviceCfg).map(folderLabel).join(', ')}}">{{deviceFolders(deviceCfg).map(folderLabel).join(", ")}}</td>
-                      </tr>
-                      <tr ng-if="deviceCfg.remoteGUIPort > 0">
-                        <th><span class="fas fa-fw fa-desktop"></span>&nbsp;<span translate>Remote GUI</span></th>
-                        <td class="text-right" ng-attr-title="Port {{deviceCfg.remoteGUIPort}}">
-                          <!-- Apply RFC6874 encoding for IPv6 link-local zone identifier -->
-                          <a ng-if="hasRemoteGUIAddress(deviceCfg)" href="{{remoteGUIAddress(deviceCfg).replace('%', '%25')}}">{{remoteGUIAddress(deviceCfg)}}</a>
-                          <span translate ng-if="!hasRemoteGUIAddress(deviceCfg)">Unknown</span>
-                        </td>
-                      </tr>
-                    </tbody>
-                  </table>
-                </div>
-                <div class="panel-footer">
-                  <span class="pull-right">
-                    <button ng-if="!deviceCfg.paused" type="button" class="btn btn-sm btn-default" ng-click="setDevicePause(deviceCfg.deviceID, true)">
-                      <span class="fas fa-pause"></span>&nbsp;<span translate>Pause</span>
-                    </button>
-                    <button ng-if="deviceCfg.paused" type="button" class="btn btn-sm btn-default" ng-click="setDevicePause(deviceCfg.deviceID, false)">
-                      <span class="fas fa-play"></span>&nbsp;<span translate>Resume</span>
-                    </button>
-                    <button type="button" class="btn btn-sm btn-default" ng-click="editDeviceExisting(deviceCfg)">
-                      <span class="fas fa-pencil-alt"></span>&nbsp;<span translate>Edit</span>
-                    </button>
-                  </span>
-                  <div class="clearfix"></div>
-                </div>
-              </div>
-            </div>
-          </div>
-          <div class="form-group">
-            <span class="pull-right">
-              <button type="button" class="btn btn-sm btn-default" ng-click="setAllDevicesPause(true)" ng-if="isAtleastOneDevicePausedStateSetTo(false)">
-                <span class="fas fa-pause"></span>&nbsp;<span translate>Pause All</span>
-              </button>
-              <button type="button" class="btn btn-sm btn-default" ng-click="setAllDevicesPause(false)" ng-if="isAtleastOneDevicePausedStateSetTo(true)">
-                <span class="fas fa-play"></span>&nbsp;<span translate>Resume All</span>
-              </button>
-              <button type="button" class="btn btn-sm btn-default" ng-click="globalChanges()">
-                <span class="fas fa-fw fa-info-circle"></span>&nbsp;<span translate>Recent Changes</span>
-              </button>
-              <button type="button" class="btn btn-sm btn-default" ng-click="addDevice()">
-                <span class="fas fa-plus"></span>&nbsp;<span translate>Add Remote Device</span>
-              </button>
-            </span>
-            <div class="clearfix"></div>
-          </div>
-        </div>
-      </div> <!-- /row -->
-
-    </div> <!-- /container -->
-  </div> <!-- /ng-cloak -->
-
-  <!-- Bottom bar -->
-
-  <nav class="navbar navbar-default navbar-fixed-bottom">
-    <div class="container">
-      <ul class="nav navbar-nav">
-        <li><a class="navbar-link" href="https://syncthing.net/" target="_blank"><span class="fas fa-home"></span>&nbsp;<span translate>Home page</span></a></li>
-        <li><a class="navbar-link" href="https://docs.syncthing.net/" target="_blank"><span class="fas fa-book"></span>&nbsp;<span translate>Documentation</span></a></li>
-        <li><a class="navbar-link" href="https://forum.syncthing.net" target="_blank"><span class="fas fa-question-circle"></span>&nbsp;<span translate>Support</span></a></li>
-        <li><a class="navbar-link" href="https://data.syncthing.net/" target="_blank"><span class="fas fa-bar-chart"></span>&nbsp;<span translate>Statistics</span></a></li>
-        <li><a class="navbar-link" href="https://github.com/syncthing/syncthing/releases" target="_blank"><span class="far fa-file-alt"></span>&nbsp;<span translate>Changelog</span></a></li>
-        <li><a class="navbar-link" href="https://github.com/syncthing/syncthing/issues" target="_blank"><span class="fas fa-bug"></span>&nbsp;<span translate>Bugs</span></a></li>
-        <li><a class="navbar-link" href="https://github.com/syncthing/syncthing" target="_blank"><span class="fas fa-wrench"></span>&nbsp;<span translate>Source Code</span></a></li>
-        <li><a class="navbar-link" href="https://twitter.com/syncthing" target="_blank"><span class="fab fa-twitter"></span>&nbsp;Twitter</a></li>
-      </ul>
-    </div>
-  </nav>
-
-  <ng-include src="'syncthing/core/networkErrorDialogView.html'"></ng-include>
-  <ng-include src="'syncthing/core/httpErrorDialogView.html'"></ng-include>
-  <ng-include src="'syncthing/core/restartingDialogView.html'"></ng-include>
-  <ng-include src="'syncthing/core/upgradingDialogView.html'"></ng-include>
-  <ng-include src="'syncthing/core/shutdownDialogView.html'"></ng-include>
-  <ng-include src="'syncthing/device/idqrModalView.html'"></ng-include>
-  <ng-include src="'syncthing/device/editDeviceModalView.html'"></ng-include>
-  <ng-include src="'syncthing/device/globalChangesModalView.html'"></ng-include>
-  <ng-include src="'syncthing/folder/editFolderModalView.html'"></ng-include>
-  <ng-include src="'syncthing/folder/restoreVersionsModalView.html'"></ng-include>
-  <ng-include src="'syncthing/folder/restoreVersionsConfirmation.html'"></ng-include>
-  <ng-include src="'syncthing/settings/settingsModalView.html'"></ng-include>
-  <ng-include src="'syncthing/settings/advancedSettingsModalView.html'"></ng-include>
-  <ng-include src="'syncthing/settings/discardChangesConfirmation.html'"></ng-include>
-  <ng-include src="'syncthing/usagereport/usageReportModalView.html'"></ng-include>
-  <ng-include src="'syncthing/usagereport/usageReportPreviewModalView.html'"></ng-include>
-  <ng-include src="'syncthing/transfer/neededFilesModalView.html'"></ng-include>
-  <ng-include src="'syncthing/transfer/failedFilesModalView.html'"></ng-include>
-  <ng-include src="'syncthing/transfer/remoteNeededFilesModalView.html'"></ng-include>
-  <ng-include src="'syncthing/transfer/localChangedFilesModalView.html'"></ng-include>
-  <ng-include src="'syncthing/core/upgradeModalView.html'"></ng-include>
-  <ng-include src="'syncthing/core/majorUpgradeModalView.html'"></ng-include>
-  <ng-include src="'syncthing/core/aboutModalView.html'"></ng-include>
-  <ng-include src="'syncthing/core/discoveryFailuresModalView.html'"></ng-include>
-  <ng-include src="'syncthing/folder/removeFolderDialogView.html'"></ng-include>
-  <ng-include src="'syncthing/folder/revertOverrideView.html'"></ng-include>
-  <ng-include src="'syncthing/device/removeDeviceDialogView.html'"></ng-include>
-  <ng-include src="'syncthing/core/logViewerModalView.html'"></ng-include>
-
-  <!-- vendor scripts -->
-  <script type="text/javascript" src="vendor/jquery/jquery-2.2.2.js"></script>
-  <script type="text/javascript" src="vendor/angular/angular.js"></script>
-  <script type="text/javascript" src="vendor/angular/angular-sanitize.js"></script>
-  <script type="text/javascript" src="vendor/angular/angular-translate.js"></script>
-  <script type="text/javascript" src="vendor/angular/angular-translate-loader-static-files.js"></script>
-  <script type="text/javascript" src="vendor/angular/angular-dirPagination.js"></script>
-  <script type="text/javascript" src="vendor/moment/moment.js"></script>
-  <script type="text/javascript" src="vendor/bootstrap/js/bootstrap.js"></script>
-  <script type="text/javascript" src="vendor/daterangepicker/daterangepicker.js"></script>
-  <script type="text/javascript" src="vendor/fancytree/jquery.fancytree-all-deps.js"></script>
-  <!-- / vendor scripts -->
-
-  <!-- gui application code -->
-  <script type="text/javascript" src="syncthing/core/module.js"></script>
-  <script type="text/javascript" src="syncthing/core/alwaysNumberFilter.js"></script>
-  <script type="text/javascript" src="syncthing/core/basenameFilter.js"></script>
-  <script type="text/javascript" src="syncthing/core/binaryFilter.js"></script>
-  <script type="text/javascript" src="syncthing/core/localeNumberFilter.js"></script>
-  <script type="text/javascript" src="syncthing/core/percentFilter.js"></script>
-  <script type="text/javascript" src="syncthing/core/durationFilter.js"></script>
-  <script type="text/javascript" src="syncthing/core/eventService.js"></script>
-  <script type="text/javascript" src="syncthing/core/identiconDirective.js"></script>
-  <script type="text/javascript" src="syncthing/core/languageSelectDirective.js"></script>
-  <script type="text/javascript" src="syncthing/core/lastErrorComponentFilter.js"></script>
-  <script type="text/javascript" src="syncthing/core/localeService.js"></script>
-  <script type="text/javascript" src="syncthing/core/modalDirective.js"></script>
-  <script type="text/javascript" src="syncthing/core/metricFilter.js"></script>
-  <script type="text/javascript" src="syncthing/core/notificationDirective.js"></script>
-  <script type="text/javascript" src="syncthing/core/pathIsSubDirDirective.js"></script>
-  <script type="text/javascript" src="syncthing/core/popoverDirective.js"></script>
-  <script type="text/javascript" src="syncthing/core/selectOnClickDirective.js"></script>
-  <script type="text/javascript" src="syncthing/core/syncthingController.js"></script>
-  <script type="text/javascript" src="syncthing/core/tooltipDirective.js"></script>
-  <script type="text/javascript" src="syncthing/core/uncamelFilter.js"></script>
-  <script type="text/javascript" src="syncthing/core/uniqueFolderDirective.js"></script>
-  <script type="text/javascript" src="syncthing/core/validDeviceidDirective.js"></script>
-  <script type="text/javascript" src="assets/lang/valid-langs.js"></script>
-  <script type="text/javascript" src="assets/lang/prettyprint.js"></script>
-  <script type="text/javascript" src="meta.js"></script>
-  <script type="text/javascript" src="syncthing/app.js"></script>
-  <!-- / gui application code -->
-
-</body>
-</html>

+ 0 - 40
gui/default/untrusted/syncthing/core/editShareTemplate.html

@@ -1,40 +0,0 @@
-<div class="col-md-6 checkbox">
-  <label for="sharedwith-{{id}}">
-    <input id="sharedwith-{{id}}" ng-model="selected[id]" type="checkbox" />
-    <span tooltip data-original-title="{{id}}">{{label}}</span>
-  </label>
-</div>
-<div class="col-md-6">
-  <div class="input-group">
-    <span class="input-group-addon" ng-switch="folderType !== 'receiveencrypted' && !encryptionPasswords[id]">
-      <span ng-switch-when='true' class="fas fa-fw fa-unlock" />
-      <span ng-switch-default class="fas fa-fw fa-lock" />
-    </span>
-    <span ng-switch="folderType === 'receiveencrypted'">
-      <span ng-switch-when='true'>
-        <input class="form-control input-sm" type="password" placeholder="{{'Received data is already encrypted' | translate}}" disabled />
-      </span>
-      <span ng-switch-default ng-switch="selected[id]">
-        <span ng-switch-when='true' ng-switch="untrusted">
-          <span ng-switch-when='true' ng-class="{'has-error': !encryptionPasswords[id]}">
-            <input class="form-control input-sm" type="{{plain ? 'text' : 'password'}}" ng-model="encryptionPasswords[id]" required placeholder="{{'Device is untrusted, enter encryption password' | translate}}" />
-          </span>
-          <span ng-switch-default>
-            <input class="form-control input-sm" type="{{plain ? 'text' : 'password'}}" ng-model="encryptionPasswords[id]" placeholder="{{'If untrusted, enter encryption password' | translate}}" />
-          </span>
-        </span>
-        <span ng-switch-default>
-          <input class="form-control input-sm" type="password" placeholder="{{'Not shared' | translate}}" disabled />
-        </span>
-      </span>
-    </span>
-    <span ng-switch="selected[id] && folderType !== 'receiveencrypted'" class="input-group-addon">
-      <span ng-switch-when='true'>
-        <span class="button fas fa-fw fa-eye" ng-click="togglePasswordVisibility()" />
-      </span>
-      <span ng-switch-default>
-        <span class="button fas fa-fw fa-eye" disabled />
-      </span>
-    </span>
-  </div>
-</div>

+ 0 - 2813
gui/default/untrusted/syncthing/core/syncthingController.js

@@ -1,2813 +0,0 @@
-angular.module('syncthing.core')
-    .config(function ($locationProvider) {
-        $locationProvider.html5Mode({ enabled: true, requireBase: false }).hashPrefix('!');
-    })
-    .controller('SyncthingController', function ($scope, $http, $location, LocaleService, Events, $filter, $q, $compile, $timeout, $rootScope, $translate) {
-        'use strict';
-
-        // private/helper definitions
-
-        var prevDate = 0;
-        var navigatingAway = false;
-        var online = false;
-        var restarting = false;
-
-        function initController() {
-            LocaleService.autoConfigLocale();
-            setInterval($scope.refresh, 10000);
-            Events.start();
-        }
-
-        // public/scope definitions
-
-        $scope.completion = {};
-        $scope.config = {};
-        $scope.configInSync = true;
-        $scope.connections = {};
-        $scope.errors = [];
-        $scope.model = {};
-        $scope.myID = '';
-        $scope.devices = {};
-        $scope.discoveryCache = {};
-        $scope.protocolChanged = false;
-        $scope.reportData = {};
-        $scope.reportDataPreview = '';
-        $scope.reportPreview = false;
-        $scope.folders = {};
-        $scope.seenError = '';
-        $scope.upgradeInfo = null;
-        $scope.deviceStats = {};
-        $scope.folderStats = {};
-        $scope.pendingDevices = {};
-        $scope.pendingFolders = {};
-        $scope.progress = {};
-        $scope.version = {};
-        $scope.needed = {}
-        $scope.neededFolder = '';
-        $scope.failed = {};
-        $scope.localChanged = {};
-        $scope.scanProgress = {};
-        $scope.themes = [];
-        $scope.globalChangeEvents = {};
-        $scope.metricRates = false;
-        $scope.folderPathErrors = {};
-        $scope.currentSharing = {};
-        $scope.currentFolder = {};
-        $scope.currentDevice = {};
-        $scope.ignores = {
-            text: '',
-            error: null,
-            disabled: false,
-        };
-        resetRemoteNeed();
-
-        try {
-            $scope.metricRates = (window.localStorage["metricRates"] == "true");
-        } catch (exception) { }
-
-        $scope.versioningDefaults = {
-            selector: "none",
-            trashcanClean: 0,
-            cleanupIntervalS: 3600,
-            simpleKeep: 5,
-            staggeredMaxAge: 365,
-            staggeredCleanInterval: 3600,
-            externalCommand: "",
-        };
-
-        $scope.localStateTotal = {
-            bytes: 0,
-            directories: 0,
-            files: 0
-        };
-
-        $(window).bind('beforeunload', function () {
-            navigatingAway = true;
-        });
-
-        $scope.$on("$locationChangeSuccess", function () {
-            LocaleService.useLocale($location.search().lang);
-        });
-
-        $scope.needActions = {
-            'rm': 'Del',
-            'rmdir': 'Del (dir)',
-            'sync': 'Sync',
-            'touch': 'Update'
-        };
-        $scope.needIcons = {
-            'rm': 'far fa-fw fa-trash-alt',
-            'rmdir': 'far fa-fw fa-trash-alt',
-            'sync': 'far fa-fw fa-arrow-alt-circle-down',
-            'touch': 'fas fa-fw fa-asterisk'
-        };
-
-        $scope.$on(Events.ONLINE, function () {
-            if (online && !restarting) {
-                return;
-            }
-
-            console.log('UIOnline');
-
-            refreshSystem();
-            refreshDiscoveryCache();
-            refreshConfig();
-            refreshCluster();
-            refreshConnectionStats();
-            refreshDeviceStats();
-            refreshFolderStats();
-            refreshGlobalChanges();
-            refreshThemes();
-
-            $http.get(urlbase + '/system/version').success(function (data) {
-                console.log("version", data);
-                if ($scope.version.version && $scope.version.version !== data.version) {
-                    // We already have a version response, but it differs from
-                    // the new one. Reload the full GUI in case it's changed.
-                    document.location.reload(true);
-                }
-
-                $scope.version = data;
-            }).error($scope.emitHTTPError);
-
-            $http.get(urlbase + '/svc/report').success(function (data) {
-                $scope.reportData = data;
-                if ($scope.system && $scope.config.options.urAccepted > -1 && $scope.config.options.urSeen < $scope.system.urVersionMax && $scope.config.options.urAccepted < $scope.system.urVersionMax) {
-                    // Usage reporting format has changed, prompt the user to re-accept.
-                    $('#ur').modal();
-                }
-            }).error($scope.emitHTTPError);
-
-            $http.get(urlbase + '/system/upgrade').success(function (data) {
-                $scope.upgradeInfo = data;
-            }).error(function () {
-                $scope.upgradeInfo = null;
-            });
-
-            online = true;
-            restarting = false;
-            $('#networkError').modal('hide');
-            $('#restarting').modal('hide');
-            $('#shutdown').modal('hide');
-        });
-
-        $scope.$on(Events.OFFLINE, function () {
-            if (navigatingAway || !online) {
-                return;
-            }
-
-            console.log('UIOffline');
-            online = false;
-            if (!restarting) {
-                $('#networkError').modal();
-            }
-        });
-
-        $scope.$on('HTTPError', function (event, arg) {
-            // Emitted when a HTTP call fails. We use the status code to try
-            // to figure out what's wrong.
-
-            if (navigatingAway || !online) {
-                return;
-            }
-
-            console.log('HTTPError', arg);
-            online = false;
-            if (!restarting) {
-                if (arg.status === 0) {
-                    // A network error, not an HTTP error
-                    $scope.$emit(Events.OFFLINE);
-                } else if (arg.status >= 400 && arg.status <= 599) {
-                    // A genuine HTTP error
-                    $('#networkError').modal('hide');
-                    $('#restarting').modal('hide');
-                    $('#shutdown').modal('hide');
-                    $('#httpError').modal();
-                }
-            }
-        });
-
-        $scope.$on(Events.STATE_CHANGED, function (event, arg) {
-            var data = arg.data;
-            if ($scope.model[data.folder]) {
-                $scope.model[data.folder].state = data.to;
-                $scope.model[data.folder].error = data.error;
-
-                // If a folder has started scanning, then any scan progress is
-                // also obsolete.
-                if (data.to === 'scanning') {
-                    delete $scope.scanProgress[data.folder];
-                }
-
-                // If a folder finished scanning, then refresh folder stats
-                // to update last scan time.
-                if (data.from === 'scanning' && data.to === 'idle') {
-                    refreshFolderStats();
-                }
-            }
-        });
-
-        $scope.$on(Events.LOCAL_INDEX_UPDATED, function (event, arg) {
-            refreshFolderStats();
-            refreshGlobalChanges();
-        });
-
-        $scope.$on(Events.DEVICE_DISCONNECTED, function (event, arg) {
-            if (!$scope.connections[arg.data.id]) {
-                return;
-            }
-            $scope.connections[arg.data.id].connected = false;
-            refreshDeviceStats();
-        });
-
-        $scope.$on(Events.DEVICE_CONNECTED, function (event, arg) {
-            if (!$scope.connections[arg.data.id]) {
-                $scope.connections[arg.data.id] = {
-                    inbps: 0,
-                    outbps: 0,
-                    inBytesTotal: 0,
-                    outBytesTotal: 0,
-                    type: arg.data.type,
-                    address: arg.data.addr
-                };
-                $scope.completion[arg.data.id] = {
-                    _total: 100,
-                    _needBytes: 0,
-                    _needItems: 0
-                };
-            }
-        });
-
-        $scope.$on(Events.PENDING_DEVICES_CHANGED, function (event, arg) {
-            if (!(arg.data.added || arg.data.removed)) {
-                // Not enough information to update in place, just refresh it completely
-                refreshCluster();
-                return;
-            }
-
-            if (arg.data.added) {
-                arg.data.added.forEach(function (rejected) {
-                    var pendingDevice = {
-                        time: arg.time,
-                        name: rejected.name,
-                        address: rejected.address
-                    };
-                    console.log("rejected device:", rejected.deviceID, pendingDevice);
-                    $scope.pendingDevices[rejected.deviceID] = pendingDevice;
-                });
-            }
-
-            if (arg.data.removed) {
-                arg.data.removed.forEach(function (dev) {
-                    console.log("no longer pending device:", dev.deviceID);
-                    delete $scope.pendingDevices[dev.deviceID];
-                });
-            }
-        });
-
-        $scope.$on(Events.PENDING_FOLDERS_CHANGED, function (event, arg) {
-            if (!(arg.data.added || arg.data.removed)) {
-                // Not enough information to update in place, just refresh it completely
-                refreshCluster();
-                return;
-            }
-
-            if (arg.data.added) {
-                arg.data.added.forEach(function (rejected) {
-                    var offeringDevice = {
-                        time: arg.time,
-                        label: rejected.folderLabel,
-                        receiveEncrypted: rejected.receiveEncrypted,
-                    };
-                    console.log("rejected folder", rejected.folderID, "from device:", rejected.deviceID, offeringDevice);
-
-                    var pendingFolder = $scope.pendingFolders[rejected.folderID];
-                    if (pendingFolder === undefined) {
-                        pendingFolder = {
-                            offeredBy: {}
-                        };
-                    }
-                    pendingFolder.offeredBy[rejected.deviceID] = offeringDevice;
-                    $scope.pendingFolders[rejected.folderID] = pendingFolder;
-                });
-            }
-
-            if (arg.data.removed) {
-                arg.data.removed.forEach(function (folderDev) {
-                    console.log("no longer pending folder", folderDev.folderID, "from device:", folderDev.deviceID);
-                    if (folderDev.deviceID === undefined) {
-                        delete $scope.pendingFolders[folderDev.folderID];
-                    } else if ($scope.pendingFolders[folderDev.folderID]) {
-                        delete $scope.pendingFolders[folderDev.folderID].offeredBy[folderDev.deviceID];
-                    }
-                });
-            }
-        });
-
-        $scope.$on('ConfigLoaded', function () {
-            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(Events.CONFIG_SAVED, function (event, arg) {
-            updateLocalConfig(arg.data);
-
-            $http.get(urlbase + '/config/insync').success(function (data) {
-                $scope.configInSync = data.configInSync;
-            }).error($scope.emitHTTPError);
-        });
-
-        $scope.$on(Events.DOWNLOAD_PROGRESS, function (event, arg) {
-            var stats = arg.data;
-            var progress = {};
-            for (var folder in stats) {
-                progress[folder] = {};
-                for (var file in stats[folder]) {
-                    var s = stats[folder][file];
-                    var reused = 100 * s.reused / s.total;
-                    var copiedFromOrigin = 100 * s.copiedFromOrigin / s.total;
-                    var copiedFromElsewhere = 100 * s.copiedFromElsewhere / s.total;
-                    var pulled = 100 * s.pulled / s.total;
-                    var pulling = 100 * s.pulling / s.total;
-                    // We try to round up pulling to at least a percent so that it would be at least a bit visible.
-                    if (pulling < 1 && pulled + copiedFromElsewhere + copiedFromOrigin + reused <= 99) {
-                        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)) {
-                    if ($scope.neededFolder === folder) {
-                        $scope.refreshNeed($scope.needed.page, $scope.needed.perpage);
-                    }
-                } else if ($scope.neededFolder === folder) {
-                    for (file in $scope.progress[folder]) {
-                        if (!(file in progress[folder])) {
-                            $scope.refreshNeed($scope.needed.page, $scope.needed.perpage);
-                            break;
-                        }
-                    }
-                }
-            }
-            $scope.progress = progress;
-            console.log("DownloadProgress", $scope.progress);
-        });
-
-        $scope.$on(Events.FOLDER_SUMMARY, function (event, arg) {
-            var data = arg.data;
-            $scope.model[data.folder] = data.summary;
-            recalcLocalStateTotal();
-        });
-
-        $scope.$on(Events.FOLDER_COMPLETION, function (event, arg) {
-            var data = arg.data;
-            if (!$scope.completion[data.device]) {
-                $scope.completion[data.device] = {};
-            }
-            $scope.completion[data.device][data.folder] = data;
-            recalcCompletion(data.device);
-        });
-
-        $scope.$on(Events.FOLDER_ERRORS, function (event, arg) {
-            $scope.model[arg.data.folder].errors = arg.data.errors.length;
-        });
-
-        $scope.$on(Events.FOLDER_SCAN_PROGRESS, function (event, arg) {
-            var data = arg.data;
-            $scope.scanProgress[data.folder] = {
-                current: data.current,
-                total: data.total,
-                rate: data.rate
-            };
-            console.log("FolderScanProgress", data);
-        });
-
-        $scope.emitHTTPError = function (data, status, headers, config) {
-            $scope.$emit('HTTPError', { data: data, status: status, headers: headers, config: config });
-        };
-
-        var debouncedFuncs = {};
-
-        function refreshFolder(folder) {
-            if ($scope.folders[folder].paused) {
-                return;
-            }
-            var key = "refreshFolder" + folder;
-            if (!debouncedFuncs[key]) {
-                debouncedFuncs[key] = debounce(function () {
-                    $http.get(urlbase + '/db/status?folder=' + encodeURIComponent(folder)).success(function (data) {
-                        $scope.model[folder] = data;
-                        recalcLocalStateTotal();
-                        console.log("refreshFolder", folder, data);
-                    }).error($scope.emitHTTPError);
-                }, 1000);
-            }
-            debouncedFuncs[key]();
-        }
-
-        function updateLocalConfig(config) {
-            var hasConfig = !isEmptyObject($scope.config);
-
-            $scope.config = config;
-            $scope.config.options._listenAddressesStr = $scope.config.options.listenAddresses.join(', ');
-            $scope.config.options._globalAnnounceServersStr = $scope.config.options.globalAnnounceServers.join(', ');
-            $scope.config.options._urAcceptedStr = "" + $scope.config.options.urAccepted;
-
-            $scope.devices = deviceMap($scope.config.devices);
-            for (var id in $scope.devices) {
-                $scope.completion[id] = {
-                    _total: 100,
-                    _needBytes: 0,
-                    _needItems: 0
-                };
-            };
-            $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);
-                });
-            });
-
-            refreshNoAuthWarning();
-            setDefaultTheme();
-
-            if (!hasConfig) {
-                $scope.$emit('ConfigLoaded');
-            }
-        }
-
-        function refreshSystem() {
-            $http.get(urlbase + '/system/status').success(function (data) {
-                $scope.myID = data.myID;
-                $scope.system = data;
-
-                if ($scope.reportDataPreviewVersion === '') {
-                    $scope.reportDataPreviewVersion = $scope.system.urVersionMax;
-                }
-
-                var listenersFailed = [];
-                for (var address in data.connectionServiceStatus) {
-                    if (data.connectionServiceStatus[address].error) {
-                        listenersFailed.push(address + ": " + data.connectionServiceStatus[address].error);
-                    }
-                }
-                $scope.listenersFailed = listenersFailed;
-                $scope.listenersTotal = $scope.sizeOf(data.connectionServiceStatus);
-
-                $scope.discoveryTotal = data.discoveryMethods;
-                var discoveryFailed = [];
-                for (var disco in data.discoveryErrors) {
-                    if (data.discoveryErrors[disco]) {
-                        discoveryFailed.push(disco + ": " + data.discoveryErrors[disco]);
-                    }
-                }
-                $scope.discoveryFailed = discoveryFailed;
-
-                refreshNoAuthWarning();
-
-                console.log("refreshSystem", data);
-            }).error($scope.emitHTTPError);
-        }
-
-        function refreshNoAuthWarning() {
-            if (!$scope.system || !$scope.config || !$scope.config.gui) {
-                // We need all to be able to determine the state.
-                return
-            }
-
-            // If we're not listening on localhost, and there is no
-            // authentication configured, and the magic setting to silence the
-            // warning isn't set, then yell at the user.
-            var addr = $scope.system.guiAddressUsed;
-            var guiCfg = $scope.config.gui;
-            $scope.openNoAuth = addr.substr(0, 4) !== "127."
-                && addr.substr(0, 6) !== "[::1]:"
-                && addr.substr(0, 1) !== "/"
-                && (!guiCfg.user || !guiCfg.password)
-                && guiCfg.authMode !== 'ldap'
-                && !guiCfg.insecureAdminAccess;
-
-            if (guiCfg.user && guiCfg.password) {
-                $scope.dismissNotification('authenticationUserAndPassword');
-            }
-        }
-
-        function refreshCluster() {
-            $http.get(urlbase + '/cluster/pending/devices').success(function (data) {
-                $scope.pendingDevices = data;
-                console.log("refreshCluster devices", data);
-            }).error($scope.emitHTTPError);
-            $http.get(urlbase + '/cluster/pending/folders').success(function (data) {
-                $scope.pendingFolders = data;
-                console.log("refreshCluster folders", data);
-            }).error($scope.emitHTTPError);
-        }
-
-        function refreshDiscoveryCache() {
-            $http.get(urlbase + '/system/discovery').success(function (data) {
-                for (var device in data) {
-                    for (var i = 0; i < data[device].addresses.length; i++) {
-                        // Relay addresses are URLs with
-                        // .../?foo=barlongstuff that we strip away here. We
-                        // remove the final slash as well for symmetry with
-                        // tcp://192.0.2.42:1234 type addresses.
-                        data[device].addresses[i] = data[device].addresses[i].replace(/\/\?.*/, '');
-                    }
-                }
-                $scope.discoveryCache = data;
-                console.log("refreshDiscoveryCache", data);
-            }).error($scope.emitHTTPError);
-        }
-
-        function recalcLocalStateTotal() {
-            $scope.localStateTotal = {
-                bytes: 0,
-                directories: 0,
-                files: 0
-            };
-
-            for (var f in $scope.model) {
-                $scope.localStateTotal.bytes += $scope.model[f].localBytes;
-                $scope.localStateTotal.files += $scope.model[f].localFiles;
-                $scope.localStateTotal.directories += $scope.model[f].localDirectories;
-            }
-        }
-
-        function recalcCompletion(device) {
-            var total = 0, needed = 0, deletes = 0, items = 0;
-            for (var folder in $scope.completion[device]) {
-                if (folder === "_total" || folder === '_needBytes' || folder === '_needItems') {
-                    continue;
-                }
-                total += $scope.completion[device][folder].globalBytes;
-                needed += $scope.completion[device][folder].needBytes;
-                items += $scope.completion[device][folder].needItems;
-                deletes += $scope.completion[device][folder].needDeletes;
-            }
-            if (total == 0) {
-                $scope.completion[device]._total = 100;
-                $scope.completion[device]._needBytes = 0;
-                $scope.completion[device]._needItems = 0;
-            } else {
-                $scope.completion[device]._total = Math.floor(100 * (1 - needed / total));
-                $scope.completion[device]._needBytes = needed;
-                $scope.completion[device]._needItems = items + deletes;
-            }
-
-            if (needed == 0 && deletes > 0) {
-                // We don't need any data, but we have deletes that we need
-                // to do. Drop down the completion percentage to indicate
-                // that we have stuff to do.
-                $scope.completion[device]._total = 95;
-            }
-
-            console.log("recalcCompletion", device, $scope.completion[device]);
-        }
-
-        function refreshCompletion(device, folder) {
-            if (device === $scope.myID) {
-                return;
-            }
-
-            $http.get(urlbase + '/db/completion?device=' + device + '&folder=' + encodeURIComponent(folder)).success(function (data) {
-                if (!$scope.completion[device]) {
-                    $scope.completion[device] = {};
-                }
-                $scope.completion[device][folder] = data;
-                recalcCompletion(device);
-            }).error(function(data, status, headers, config) {
-                if (status === 404) {
-                    console.log("refreshCompletion:", data);
-                } else {
-                    $scope.emitHTTPError(data, status, headers, config);
-                }
-            })
-        }
-
-        function refreshConnectionStats() {
-            $http.get(urlbase + '/system/connections').success(function (data) {
-                var now = Date.now(),
-                    td = (now - prevDate) / 1000,
-                    id;
-
-                prevDate = now;
-
-                try {
-                    data.total.inbps = Math.max(0, (data.total.inBytesTotal - $scope.connectionsTotal.inBytesTotal) / td);
-                    data.total.outbps = Math.max(0, (data.total.outBytesTotal - $scope.connectionsTotal.outBytesTotal) / td);
-                } catch (e) {
-                    data.total.inbps = 0;
-                    data.total.outbps = 0;
-                }
-                $scope.connectionsTotal = data.total;
-
-                data = data.connections;
-                for (id in data) {
-                    if (!data.hasOwnProperty(id)) {
-                        continue;
-                    }
-                    try {
-                        data[id].inbps = Math.max(0, (data[id].inBytesTotal - $scope.connections[id].inBytesTotal) / td);
-                        data[id].outbps = Math.max(0, (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);
-            }).error($scope.emitHTTPError);
-        }
-
-        function refreshErrors() {
-            $http.get(urlbase + '/system/error').success(function (data) {
-                $scope.errors = data.errors;
-                console.log("refreshErrors", data);
-            }).error($scope.emitHTTPError);
-        }
-
-        function refreshConfig() {
-            $http.get(urlbase + '/config').success(function (data) {
-                updateLocalConfig(data);
-                console.log("refreshConfig", data);
-            }).error($scope.emitHTTPError);
-
-            $http.get(urlbase + '/config/insync').success(function (data) {
-                $scope.configInSync = data.configInSync;
-            }).error($scope.emitHTTPError);
-        }
-
-        $scope.refreshNeed = function (page, perpage) {
-            if (!$scope.neededFolder) {
-                return;
-            }
-            var url = urlbase + "/db/need?folder=" + encodeURIComponent($scope.neededFolder);
-            url += "&page=" + page;
-            url += "&perpage=" + perpage;
-            $http.get(url).success(function (data) {
-                console.log("refreshNeed", $scope.neededFolder, data);
-                parseNeeded(data);
-            }).error($scope.emitHTTPError);
-        }
-
-        function needAction(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';
-            }
-        }
-
-        function parseNeeded(data) {
-            $scope.needed = data;
-            var merged = [];
-            data.progress.forEach(function (item) {
-                item.type = "progress";
-                item.action = needAction(item);
-                merged.push(item);
-            });
-            data.queued.forEach(function (item) {
-                item.type = "queued";
-                item.action = needAction(item);
-                merged.push(item);
-            });
-            data.rest.forEach(function (item) {
-                item.type = "rest";
-                item.action = needAction(item);
-                merged.push(item);
-            });
-            $scope.needed.items = merged;
-        }
-
-        function pathJoin(base, name) {
-            base = expandTilde(base);
-            if (base[base.length - 1] !== $scope.system.pathSeparator) {
-                return base + $scope.system.pathSeparator + name;
-            }
-            return base + name;
-        }
-
-        function expandTilde(path) {
-            if (path && path.trim().charAt(0) === '~') {
-                return $scope.system.tilde + path.trim().substring(1);
-            }
-            return path;
-        }
-
-        function shouldSetDefaultFolderPath() {
-            return $scope.config.defaults.folder.path && !$scope.editingExisting && $scope.folderEditor.folderPath.$pristine && !$scope.editingDefaults;
-        }
-
-        function resetRemoteNeed() {
-            $scope.remoteNeed = {};
-            $scope.remoteNeedFolders = [];
-            $scope.remoteNeedDevice = undefined;
-        }
-
-
-        function setDefaultTheme() {
-            if (!document.getElementById("fallback-theme-css")) {
-
-                // check if no support for prefers-color-scheme
-                var colorSchemeNotSupported = typeof window.matchMedia === "undefined" || window.matchMedia('(prefers-color-scheme: dark)').media === 'not all';
-
-                if ($scope.config.gui.theme === "default" && colorSchemeNotSupported) {
-                    document.documentElement.style.display = 'none';
-                    document.head.insertAdjacentHTML(
-                        'beforeend',
-                        '<link id="fallback-theme-css" rel="stylesheet" href="theme-assets/light/assets/css/theme.css" onload="document.documentElement.style.display = \'\'">'
-                    );
-                }
-            }
-        }
-
-        function saveIgnores(ignores, cb) {
-            $http.post(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id), {
-                ignore: ignores
-            }).success(function () {
-                if (cb) {
-                    cb();
-                }
-            });
-        };
-
-        function initShareEditing(editing) {
-            $scope.currentSharing = {};
-            $scope.currentSharing.editing = editing;
-            $scope.currentSharing.shared = [];
-            $scope.currentSharing.unrelated = [];
-            $scope.currentSharing.selected = {};
-            $scope.currentSharing.encryptionPasswords = {};
-            if (editing === 'folder') {
-                initShareEditingFolder();
-            }
-        };
-
-        function initShareEditingFolder() {
-            $scope.currentFolder.devices.forEach(function (n) {
-                if (n.deviceID !== $scope.myID) {
-                    $scope.currentSharing.shared.push($scope.devices[n.deviceID]);
-                }
-                if (n.encryptionPassword !== '') {
-                    $scope.currentSharing.encryptionPasswords[n.deviceID] = n.encryptionPassword;
-                }
-                $scope.currentSharing.selected[n.deviceID] = true;
-            });
-            $scope.currentSharing.unrelated = $scope.deviceList().filter(function (n) {
-                return n.deviceID !== $scope.myID && !$scope.currentSharing.selected[n.deviceID];
-            });
-        }
-
-        $scope.refreshFailed = function (page, perpage) {
-            if (!$scope.failed || !$scope.failed.folder) {
-                return;
-            }
-            var url = urlbase + '/folder/errors?folder=' + encodeURIComponent($scope.failed.folder);
-            url += "&page=" + page + "&perpage=" + perpage;
-            $http.get(url).success(function (data) {
-                $scope.failed = data;
-            }).error($scope.emitHTTPError);
-        };
-
-        $scope.refreshRemoteNeed = function (folder, page, perpage) {
-            if (!$scope.remoteNeedDevice) {
-                return;
-            }
-            var url = urlbase + '/db/remoteneed?device=' + $scope.remoteNeedDevice.deviceID;
-            url += '&folder=' + encodeURIComponent(folder);
-            url += "&page=" + page + "&perpage=" + perpage;
-            $http.get(url).success(function (data) {
-                $scope.remoteNeed[folder] = data;
-            }).error(function (err) {
-                $scope.remoteNeed[folder] = undefined;
-                $scope.emitHTTPError(err);
-            });
-        };
-
-        $scope.refreshLocalChanged = function (page, perpage) {
-            if (!$scope.localChangedFolder) {
-                return;
-            }
-            var url = urlbase + '/db/localchanged?folder=';
-            url += encodeURIComponent($scope.localChangedFolder);
-            url += "&page=" + page + "&perpage=" + perpage;
-            $http.get(url).success(function (data) {
-                $scope.localChanged = data;
-            }).error($scope.emitHTTPError);
-        };
-
-        var refreshDeviceStats = debounce(function () {
-            $http.get(urlbase + "/stats/device").success(function (data) {
-                $scope.deviceStats = data;
-                for (var device in $scope.deviceStats) {
-                    $scope.deviceStats[device].lastSeen = new Date($scope.deviceStats[device].lastSeen);
-                    $scope.deviceStats[device].lastSeenDays = (new Date() - $scope.deviceStats[device].lastSeen) / 1000 / 86400;
-                }
-                console.log("refreshDeviceStats", data);
-            }).error($scope.emitHTTPError);
-        }, 2500);
-
-        var refreshFolderStats = debounce(function () {
-            $http.get(urlbase + "/stats/folder").success(function (data) {
-                $scope.folderStats = data;
-                for (var folder in $scope.folderStats) {
-                    if ($scope.folderStats[folder].lastFile) {
-                        $scope.folderStats[folder].lastFile.at = new Date($scope.folderStats[folder].lastFile.at);
-                    }
-
-                    $scope.folderStats[folder].lastScan = new Date($scope.folderStats[folder].lastScan);
-                    $scope.folderStats[folder].lastScanDays = (new Date() - $scope.folderStats[folder].lastScan) / 1000 / 86400;
-                }
-                console.log("refreshfolderStats", data);
-            }).error($scope.emitHTTPError);
-        }, 2500);
-
-        var refreshThemes = debounce(function () {
-            $http.get("themes.json").success(function (data) { // no urlbase here as this is served by the asset handler
-                $scope.themes = data.themes;
-            }).error($scope.emitHTTPError);
-        }, 2500);
-
-        var refreshGlobalChanges = debounce(function () {
-            $http.get(urlbase + "/events/disk?limit=25").success(function (data) {
-                if (!data) {
-                    // For reasons unknown this is called with data being the empty
-                    // string on shutdown, causing an error on .reverse().
-                    return;
-                }
-                data = data.reverse();
-                $scope.globalChangeEvents = data;
-                console.log("refreshGlobalChanges", data);
-            }).error($scope.emitHTTPError);
-        }, 2500);
-
-        $scope.refresh = function () {
-            refreshSystem();
-            refreshDiscoveryCache();
-            refreshConnectionStats();
-            refreshErrors();
-        };
-
-        $scope.folderStatus = function (folderCfg) {
-            if (folderCfg.paused) {
-                return 'paused';
-            }
-
-            var folderInfo = $scope.model[folderCfg.id];
-
-            // after restart syncthing process state may be empty
-            if (typeof folderInfo === 'undefined' || !folderInfo.state) {
-                return 'unknown';
-            }
-
-            var state = '' + folderInfo.state;
-            if (state === 'error') {
-                return 'stopped'; // legacy, the state is called "stopped" in the GUI
-            }
-
-            if (state !== 'idle') {
-                return state;
-            }
-
-            if (folderInfo.needTotalItems > 0) {
-                return 'outofsync';
-            }
-            if ($scope.hasFailedFiles(folderCfg.id)) {
-                return 'faileditems';
-            }
-            if ($scope.hasReceiveOnlyChanged(folderCfg)) {
-                return 'localadditions';
-            }
-            if ($scope.hasReceiveEncryptedItems(folderCfg)) {
-                return 'localunencrypted';
-            }
-            if (folderCfg.devices.length <= 1) {
-                return 'unshared';
-            }
-
-            return state;
-        };
-
-        $scope.folderClass = function (folderCfg) {
-            var status = $scope.folderStatus(folderCfg);
-
-            if (status === 'idle' || status === 'localadditions') {
-                return 'success';
-            }
-            if (status == 'paused') {
-                return 'default';
-            }
-            if (status === 'syncing' || status === 'sync-preparing' || status === 'scanning' || status === 'cleaning') {
-                return 'primary';
-            }
-            if (status === 'unknown') {
-                return 'info';
-            }
-            if (status === 'stopped' || status === 'outofsync' || status === 'error' || status === 'faileditems' || status === 'localunencrypted') {
-                return 'danger';
-            }
-            if (status === 'unshared' || status === 'scan-waiting' || status === 'sync-waiting' || status === 'clean-waiting') {
-                return 'warning';
-            }
-
-            return 'info';
-        };
-
-        $scope.syncPercentage = function (folder) {
-            if (typeof $scope.model[folder] === 'undefined') {
-                return 100;
-            }
-            if ($scope.model[folder].needTotalItems === 0) {
-                return 100;
-            }
-            if (($scope.model[folder].needBytes == 0 && $scope.model[folder].needDeletes > 0) || $scope.model[folder].globalBytes == 0) {
-                // We don't need any data, but we have deletes that we need
-                // to do. Drop down the completion percentage to indicate
-                // that we have stuff to do.
-                // Do the same thing in case we only have zero byte files to sync.
-                return 95;
-            }
-            var pct = 100 * $scope.model[folder].inSyncBytes / $scope.model[folder].globalBytes;
-            return Math.floor(pct);
-        };
-
-        $scope.scanPercentage = function (folder) {
-            if (!$scope.scanProgress[folder]) {
-                return undefined;
-            }
-            var pct = 100 * $scope.scanProgress[folder].current / $scope.scanProgress[folder].total;
-            return Math.floor(pct);
-        };
-
-        $scope.scanRate = function (folder) {
-            if (!$scope.scanProgress[folder]) {
-                return 0;
-            }
-            return $scope.scanProgress[folder].rate;
-        };
-
-        $scope.scanRemaining = function (folder) {
-            // Formats the remaining scan time as a string. Includes days and
-            // hours only when relevant, resulting in time stamps like:
-            // 00m 40s
-            // 32m 40s
-            // 2h 32m
-            // 4d 2h
-            // In case remaining scan time appears to be >31d, omit the
-            // details, i.e.:
-            // > 1 month
-
-            if (!$scope.scanProgress[folder]) {
-                return "";
-            }
-            // Calculate remaining bytes and seconds based on our current
-            // rate.
-
-            var remainingBytes = $scope.scanProgress[folder].total - $scope.scanProgress[folder].current;
-            var seconds = remainingBytes / $scope.scanProgress[folder].rate;
-            // Round up to closest ten seconds to avoid flapping too much to
-            // and fro.
-
-            seconds = Math.ceil(seconds / 10) * 10;
-
-            // Separate out the number of days.
-            var days = 0;
-            var res = [];
-            if (seconds >= 86400) {
-                days = Math.floor(seconds / 86400);
-                if (days > 31) {
-                    return '> 1 month';
-                }
-                res.push('' + days + 'd')
-                seconds = seconds % 86400;
-            }
-
-            // Separate out the number of hours.
-            var hours = 0;
-            if (seconds > 3600) {
-                hours = Math.floor(seconds / 3600);
-                res.push('' + hours + 'h')
-                seconds = seconds % 3600;
-            }
-
-            var d = new Date(1970, 0, 1).setSeconds(seconds);
-
-            if (days === 0) {
-                // Format minutes only if we're within a day of completion.
-                var f = $filter('date')(d, "m'm'");
-                res.push(f);
-            }
-
-            if (days === 0 && hours === 0) {
-                // Format seconds only when we're within an hour of completion.
-                var f = $filter('date')(d, "ss's'");
-                res.push(f);
-            }
-
-            return res.join(' ');
-        };
-
-        $scope.deviceStatus = function (deviceCfg) {
-            var status = '';
-
-            if ($scope.deviceFolders(deviceCfg).length === 0) {
-                status = 'unused-';
-            }
-
-            if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
-                return 'unknown';
-            }
-
-            if (deviceCfg.paused) {
-                return status + 'paused';
-            }
-
-            if ($scope.connections[deviceCfg.deviceID].connected) {
-                if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
-                    return status + 'insync';
-                } else {
-                    return 'syncing';
-                }
-            }
-
-            // Disconnected
-            return status + 'disconnected';
-        };
-
-        $scope.deviceClass = function (deviceCfg) {
-            if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
-                return 'info';
-            }
-
-            if (deviceCfg.paused) {
-                return 'default';
-            }
-
-            if ($scope.connections[deviceCfg.deviceID].connected) {
-                if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
-                    return 'success';
-                } else {
-                    return 'primary';
-                }
-            }
-
-            // Disconnected
-            return 'info';
-        };
-
-        $scope.syncthingStatus = function () {
-            var syncCount = 0;
-            var notifyCount = 0;
-            var pauseCount = 0;
-
-            // loop through all folders
-            var folderListCache = $scope.folderList();
-            for (var i = 0; i < folderListCache.length; i++) {
-                var status = $scope.folderStatus(folderListCache[i]);
-                switch (status) {
-                    case 'sync-preparing':
-                    case 'syncing':
-                        syncCount++;
-                        break;
-                    case 'stopped':
-                    case 'unknown':
-                    case 'outofsync':
-                    case 'error':
-                        notifyCount++;
-                        break;
-                }
-            }
-
-            // loop through all devices
-            var deviceCount = 0;
-            for (var id in $scope.devices) {
-                var status = $scope.deviceStatus({
-                    deviceID: id
-                });
-                switch (status) {
-                    case 'unknown':
-                        notifyCount++;
-                        break;
-                    case 'paused':
-                        pauseCount++;
-                        break;
-                    case 'unused':
-                        deviceCount--;
-                        break;
-                }
-                deviceCount++;
-            }
-
-            // enumerate notifications
-            if ($scope.openNoAuth || !$scope.configInSync || $scope.errorList().length > 0 || !online || Object.keys($scope.pendingDevices).length > 0 || Object.keys($scope.pendingFolders).length > 0) {
-                notifyCount++;
-            }
-
-            // at least one folder is syncing
-            if (syncCount > 0) {
-                return 'sync';
-            }
-
-            // a device is unknown or a folder is stopped/unknown/outofsync/error or some other notification is open or gui offline
-            if (notifyCount > 0) {
-                return 'notify';
-            }
-
-            // all used devices are paused except (this) one
-            if (pauseCount === deviceCount - 1) {
-                return 'pause';
-            }
-
-            return 'default';
-        };
-
-        $scope.deviceAddr = function (deviceCfg) {
-            var conn = $scope.connections[deviceCfg.deviceID];
-            if (conn && conn.connected) {
-                return conn.address;
-            }
-            return '?';
-        };
-
-        $scope.hasRemoteGUIAddress = function (deviceCfg) {
-            if (!deviceCfg.remoteGUIPort)
-                return false;
-            var conn = $scope.connections[deviceCfg.deviceID];
-            return conn && conn.connected && conn.address && conn.type.indexOf('Relay') == -1;
-        };
-
-        $scope.remoteGUIAddress = function (deviceCfg) {
-            // Assume hasRemoteGUIAddress is true or we would not be here
-            var conn = $scope.connections[deviceCfg.deviceID];
-            return 'http://' + replaceAddressPort(conn.address, deviceCfg.remoteGUIPort);
-        };
-
-        function replaceAddressPort(address, newPort) {
-            for (var index = address.length - 1; index >= 0; index--) {
-                if (address[index] === ":") {
-                    return address.substr(0, index) + ":" + newPort.toString();
-                }
-            }
-            return address;
-        }
-
-        $scope.friendlyNameFromShort = function (shortID) {
-            var matches = Object.keys($scope.devices).filter(function (id) {
-                return id.substr(0, 7) === shortID;
-            });
-            if (matches.length !== 1) {
-                return shortID;
-            }
-            return $scope.friendlyNameFromID(matches[0]);
-        };
-
-        $scope.friendlyNameFromID = function (deviceID) {
-            var match = $scope.devices[deviceID];
-            if (match) {
-                return $scope.deviceName(match);
-            }
-            return deviceID.substr(0, 6);
-        };
-
-        $scope.deviceName = function (deviceCfg) {
-            if (typeof deviceCfg === 'undefined' || typeof deviceCfg.deviceID === '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.setDevicePause = function (device, pause) {
-            $scope.devices[device].paused = pause;
-            $scope.config.devices = $scope.deviceList();
-            $scope.saveConfig();
-        };
-
-        $scope.setFolderPause = function (folder, pause) {
-            var cfg = $scope.folders[folder];
-            if (cfg) {
-                cfg.paused = pause;
-                $scope.config.folders = folderList($scope.folders);
-                $scope.saveConfig();
-            }
-        };
-
-        $scope.showDiscoveryFailures = function () {
-            $('#discovery-failures').modal();
-        };
-
-        $scope.logging = {
-            facilities: {},
-            refreshFacilities: function () {
-                $http.get(urlbase + '/system/debug').success(function (data) {
-                    var facilities = {};
-                    data.enabled = data.enabled || [];
-                    $.each(data.facilities, function (key, value) {
-                        facilities[key] = {
-                            description: value,
-                            enabled: data.enabled.indexOf(key) > -1
-                        }
-                    })
-                    $scope.logging.facilities = facilities;
-                }).error($scope.emitHTTPError);
-            },
-            show: function () {
-                $scope.logging.paused = false;
-                $scope.logging.refreshFacilities();
-                $scope.logging.timer = $timeout($scope.logging.fetch);
-                var textArea = $('#logViewerText');
-                textArea.on("scroll", $scope.logging.onScroll);
-                $('#logViewer').modal().one('shown.bs.modal', function () {
-                    // Scroll to bottom.
-                    textArea.scrollTop(textArea[0].scrollHeight);
-                }).one('hidden.bs.modal', function () {
-                    $timeout.cancel($scope.logging.timer);
-                    textArea.off("scroll", $scope.logging.onScroll);
-                    $scope.logging.timer = null;
-                    $scope.logging.entries = [];
-                });
-            },
-            onFacilityChange: function (facility) {
-                var enabled = $scope.logging.facilities[facility].enabled;
-                // Disable checkboxes while we're in flight.
-                $.each($scope.logging.facilities, function (key) {
-                    $scope.logging.facilities[key].enabled = null;
-                })
-                $http.post(urlbase + '/system/debug?' + (enabled ? 'enable=' : 'disable=') + facility)
-                    .success($scope.logging.refreshFacilities)
-                    .error($scope.emitHTTPError);
-            },
-            onScroll: function () {
-                var textArea = $('#logViewerText');
-                var scrollTop = textArea.prop('scrollTop');
-                var scrollHeight = textArea.prop('scrollHeight');
-                $scope.logging.paused = scrollHeight > (scrollTop + textArea.outerHeight());
-                // Browser events do not cause redraw, trigger manually.
-                $scope.$apply();
-            },
-            timer: null,
-            entries: [],
-            paused: false,
-            content: function () {
-                var content = "";
-                $.each($scope.logging.entries, function (idx, entry) {
-                    content += entry.when.split('.')[0].replace('T', ' ') + ' ' + entry.message + "\n";
-                });
-                return content;
-            },
-            fetch: function () {
-                var textArea = $('#logViewerText');
-                if ($scope.logging.paused) {
-                    if (!$scope.logging.timer) return;
-                    $scope.logging.timer = $timeout($scope.logging.fetch, 500);
-                    return;
-                }
-
-                var last = null;
-                if ($scope.logging.entries.length > 0) {
-                    last = $scope.logging.entries[$scope.logging.entries.length - 1].when;
-                }
-
-                $http.get(urlbase + '/system/log' + (last ? '?since=' + encodeURIComponent(last) : '')).success(function (data) {
-                    if (!$scope.logging.timer) return;
-                    $scope.logging.timer = $timeout($scope.logging.fetch, 2000);
-                    if (!$scope.logging.paused) {
-                        if (data.messages) {
-                            $scope.logging.entries.push.apply($scope.logging.entries, data.messages);
-                            // Wait for the text area to be redrawn, adding new lines, and then scroll to bottom.
-                            $timeout(function () {
-                                textArea.scrollTop(textArea[0].scrollHeight);
-                            });
-                        }
-                    }
-                });
-            }
-        };
-
-        $scope.discardChangedSettings = function () {
-            $("#discard-changes-confirmation").modal("hide");
-            $("#settings").off("hide.bs.modal").modal("hide");
-        };
-
-        $scope.showSettings = function () {
-            // Make a working copy
-            $scope.tmpOptions = angular.copy($scope.config.options);
-            $scope.tmpOptions.deviceName = $scope.thisDevice().name;
-            $scope.tmpOptions.upgrades = "none";
-            if ($scope.tmpOptions.autoUpgradeIntervalH > 0) {
-                $scope.tmpOptions.upgrades = "stable";
-            }
-            if ($scope.tmpOptions.upgradeToPreReleases) {
-                $scope.tmpOptions.upgrades = "candidate";
-            }
-            $scope.tmpGUI = angular.copy($scope.config.gui);
-            $scope.tmpRemoteIgnoredDevices = angular.copy($scope.config.remoteIgnoredDevices);
-            $scope.tmpDevices = angular.copy($scope.config.devices);
-            $('#settings').modal("show");
-            $("#settings a[href='#settings-general']").tab("show");
-            $("#settings").on('hide.bs.modal', function (event) {
-                if ($scope.settingsModified()) {
-                    event.preventDefault();
-                    $("#discard-changes-confirmation").modal("show");
-                } else {
-                    $("#settings").off("hide.bs.modal");
-                }
-            });
-        };
-
-        $scope.saveConfig = function (callback) {
-            var cfg = JSON.stringify($scope.config);
-            var opts = {
-                headers: {
-                    'Content-Type': 'application/json'
-                }
-            };
-            $http.put(urlbase + '/config', cfg, opts).success(function () {
-                refreshConfig();
-
-                if (callback) {
-                    callback();
-                }
-            }).error(function (data, status, headers, config) {
-                refreshConfig();
-                $scope.emitHTTPError(data, status, headers, config);
-            });
-        };
-
-        $scope.urVersions = function () {
-            var result = [];
-            if ($scope.system) {
-                for (var i = $scope.system.urVersionMax; i >= 2; i--) {
-                    result.push("" + i);
-                }
-            }
-            return result;
-        };
-
-        $scope.settingsModified = function () {
-            // Options has artificial properties injected into the temp config.
-            // Need to recompute them before we can check equality
-            var options = angular.copy($scope.config.options);
-            options.deviceName = $scope.thisDevice().name;
-            options.upgrades = "none";
-            if (options.autoUpgradeIntervalH > 0) {
-                options.upgrades = "stable";
-            }
-            if (options.upgradeToPreReleases) {
-                options.upgrades = "candidate";
-            }
-            var optionsEqual = angular.equals(options, $scope.tmpOptions);
-            var guiEquals = angular.equals($scope.config.gui, $scope.tmpGUI);
-            var ignoredDevicesEquals = angular.equals($scope.config.remoteIgnoredDevices, $scope.tmpRemoteIgnoredDevices);
-            var ignoredFoldersEquals = angular.equals($scope.config.devices, $scope.tmpDevices);
-            console.log("settings equals - options: " + optionsEqual + " gui: " + guiEquals + " ignDev: " + ignoredDevicesEquals + " ignFol: " + ignoredFoldersEquals);
-            return !optionsEqual || !guiEquals || !ignoredDevicesEquals || !ignoredFoldersEquals;
-        };
-
-        $scope.saveSettings = function () {
-            // Make sure something changed
-            if ($scope.settingsModified()) {
-                var themeChanged = $scope.config.gui.theme !== $scope.tmpGUI.theme;
-                // Angular has issues with selects with numeric values, so we handle strings here.
-                $scope.tmpOptions.urAccepted = parseInt($scope.tmpOptions._urAcceptedStr);
-                // Check if auto-upgrade has been enabled or disabled. This
-                // also has an effect on usage reporting, so do the check
-                // for that later.
-                if ($scope.tmpOptions.upgrades == "candidate") {
-                    $scope.tmpOptions.autoUpgradeIntervalH = $scope.tmpOptions.autoUpgradeIntervalH || 12;
-                    $scope.tmpOptions.upgradeToPreReleases = true;
-                    $scope.tmpOptions.urAccepted = $scope.system.urVersionMax;
-                    $scope.tmpOptions.urSeen = $scope.system.urVersionMax;
-                } else if ($scope.tmpOptions.upgrades == "stable") {
-                    $scope.tmpOptions.autoUpgradeIntervalH = $scope.tmpOptions.autoUpgradeIntervalH || 12;
-                    $scope.tmpOptions.upgradeToPreReleases = false;
-                } else {
-                    $scope.tmpOptions.autoUpgradeIntervalH = 0;
-                    $scope.tmpOptions.upgradeToPreReleases = false;
-                }
-
-                // Check if protocol will need to be changed on restart
-                if ($scope.config.gui.useTLS !== $scope.tmpGUI.useTLS) {
-                    $scope.protocolChanged = true;
-                }
-
-                // Parse strings to arrays before copying over
-                ['listenAddresses', 'globalAnnounceServers'].forEach(function (key) {
-                    $scope.tmpOptions[key] = $scope.tmpOptions["_" + key + "Str"].split(/[ ,]+/).map(function (x) {
-                        return x.trim();
-                    });
-                });
-
-                // Apply new settings locally
-                $scope.thisDeviceIn($scope.tmpDevices).name = $scope.tmpOptions.deviceName;
-                $scope.config.options = angular.copy($scope.tmpOptions);
-                $scope.config.gui = angular.copy($scope.tmpGUI);
-                $scope.config.remoteIgnoredDevices = angular.copy($scope.tmpRemoteIgnoredDevices);
-                $scope.config.devices = angular.copy($scope.tmpDevices);
-                // $scope.devices is updated by updateLocalConfig based on
-                // the config changed event, but settingsModified will look
-                // at it before that and conclude that the settings are
-                // modified (even though we just saved) unless we update
-                // here as well...
-                $scope.devices = deviceMap($scope.config.devices);
-
-                $scope.saveConfig(function () {
-                    if (themeChanged) {
-                        document.location.reload(true);
-                    }
-                });
-            }
-
-            $("#settings").off("hide.bs.modal").modal("hide");
-        };
-
-        $scope.saveAdvanced = function () {
-            $scope.config = $scope.advancedConfig;
-            $scope.saveConfig();
-            $('#advanced').modal("hide");
-        };
-
-        $scope.restart = function () {
-            restarting = true;
-            $('#restarting').modal();
-            $http.post(urlbase + '/system/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;
-            $('#upgrade').modal('hide');
-            $('#majorUpgrade').modal('hide');
-            $('#upgrading').modal();
-            $http.post(urlbase + '/system/upgrade').success(function () {
-                $('#restarting').modal();
-                $('#upgrading').modal('hide');
-            }).error(function () {
-                $('#upgrading').modal('hide');
-            });
-        };
-
-        $scope.shutdown = function () {
-            restarting = true;
-            $http.post(urlbase + '/system/shutdown').success(function () {
-                $('#shutdown').modal();
-            }).error($scope.emitHTTPError);
-            $scope.configInSync = true;
-        };
-
-        function editDeviceModal() {
-            $scope.currentDevice._addressesStr = $scope.currentDevice.addresses.join(', ');
-            $scope.deviceEditor.$setPristine();
-            $('#editDevice').modal();
-        }
-
-        $scope.editDeviceModalTitle = function() {
-            if ($scope.editingDefaults) {
-                return $translate.instant("Edit Device Defaults");
-            }
-            var title = '';
-            if ($scope.editingExisting) {
-                title += $translate.instant("Edit Device");
-            } else {
-                title += $translate.instant("Add Device");
-            }
-            var name = $scope.deviceName($scope.currentDevice);
-            if (name !== '') {
-                title += ' (' + name + ')';
-            }
-            return title;
-        };
-
-        $scope.editDeviceModalIcon = function() {
-            if ($scope.editingDefaults || $scope.editingExisting) {
-                return 'fas fa-pencil-alt';
-            }
-            return 'fas fa-desktop';
-        };
-
-        $scope.editDeviceExisting = function (deviceCfg) {
-            $scope.currentDevice = $.extend({}, deviceCfg);
-            $scope.editingExisting = true;
-            $scope.editingDefaults = false;
-            $scope.willBeReintroducedBy = undefined;
-            if (deviceCfg.introducedBy) {
-                var introducerDevice = $scope.devices[deviceCfg.introducedBy];
-                if (introducerDevice && introducerDevice.introducer) {
-                    $scope.willBeReintroducedBy = $scope.deviceName(introducerDevice);
-                }
-            }
-            initShareEditing('device');
-            $scope.deviceFolders($scope.currentDevice).forEach(function (folderID) {
-                $scope.currentSharing.shared.push($scope.folders[folderID]);
-                $scope.currentSharing.selected[folderID] = true;
-                var folderdevices = $scope.folders[folderID].devices;
-                for (var i = 0; i < folderdevices.length; i++) {
-                    if (folderdevices[i].deviceID === deviceCfg.deviceID) {
-                        $scope.currentSharing.encryptionPasswords[folderID] = folderdevices[i].encryptionPassword;
-                        break;
-                    }
-                }
-            });
-            $scope.currentSharing.unrelated = $scope.folderList().filter(function (n) {
-                return !$scope.currentSharing.selected[n.id];
-            });
-            editDeviceModal();
-        };
-
-        $scope.editDeviceDefaults = function () {
-            $http.get(urlbase + '/config/defaults/device').then(function (p) {
-                $scope.currentDevice = p.data;
-                $scope.editingDefaults = true;
-                editDeviceModal();
-            }, $scope.emitHTTPError);
-        };
-
-        $scope.selectAllSharedFolders = function (state) {
-            var folders = $scope.currentSharing.shared;
-            for (var i = 0; i < folders.length; i++) {
-                $scope.currentSharing.selected[folders[i].id] = !!state;
-            }
-        };
-
-        $scope.selectAllUnrelatedFolders = function (state) {
-            var folders = $scope.currentSharing.unrelated;
-            for (var i = 0; i < folders.length; i++) {
-                $scope.currentSharing.selected[folders[i].id] = !!state;
-            }
-        };
-
-        $scope.addDevice = function (deviceID, name) {
-            return $http.get(urlbase + '/system/discovery')
-                .success(function (registry) {
-                    $scope.discovery = [];
-                    for (var id in registry) {
-                        if ($scope.discovery.length === 5) {
-                            break;
-                        }
-                        if (id in $scope.devices) {
-                            continue
-                        }
-                        $scope.discovery.push(id);
-                    }
-                })
-                .then(function () {
-                    $http.get(urlbase + '/config/defaults/device').then(function (p) {
-                        $scope.currentDevice = p.data;
-                        $scope.currentDevice.name = name;
-                        $scope.currentDevice.deviceID = deviceID;
-                        $scope.editingExisting = false;
-                        $scope.editingDefaults = false;
-                        initShareEditing('device');
-                        $scope.currentSharing.unrelated = $scope.folderList();
-                        editDeviceModal();
-                    }, $scope.emitHTTPError);
-                });
-        };
-
-        $scope.deleteDevice = function () {
-            $('#editDevice').modal('hide');
-            if (!$scope.editingExisting) {
-                return;
-            }
-
-            var id = $scope.currentDevice.deviceID
-            delete $scope.devices[id];
-            $scope.config.devices = $scope.deviceList();
-
-            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 () {
-            $('#editDevice').modal('hide');
-            $scope.currentDevice.addresses = $scope.currentDevice._addressesStr.split(',').map(function (x) {
-                return x.trim();
-            });
-            delete $scope.currentDevice._addressesStr;
-            if ($scope.editingDefaults) {
-                $scope.config.defaults.device = $scope.currentDevice;
-            } else {
-                setDeviceConfig();
-            }
-            delete $scope.currentSharing;
-            delete $scope.currentDevice;
-            $scope.saveConfig();
-        };
-
-        function setDeviceConfig() {
-            var currentID = $scope.currentDevice.deviceID;
-            $scope.devices[currentID] = $scope.currentDevice;
-            $scope.config.devices = deviceList($scope.devices);
-
-            for (var id in $scope.currentSharing.selected) {
-                if ($scope.currentSharing.selected[id]) {
-                    var found = false;
-                    for (i = 0; i < $scope.folders[id].devices.length; i++) {
-                        if ($scope.folders[id].devices[i].deviceID === currentID) {
-                            found = true;
-                            // Update encryption pw
-                            $scope.folders[id].devices[i].encryptionPassword = $scope.currentSharing.encryptionPasswords[id];
-                            break;
-                        }
-                    }
-
-                    if (!found) {
-                        // Add device to folder
-                        $scope.folders[id].devices.push({
-                            deviceID: currentID,
-                            encryptionPassword: $scope.currentSharing.encryptionPasswords[id],
-                        });
-                    }
-                } else {
-                    // Remove device from folder
-                    $scope.folders[id].devices = $scope.folders[id].devices.filter(function (n) {
-                        return n.deviceID !== currentID;
-                    });
-                }
-            }
-
-            $scope.config.folders = folderList($scope.folders);
-        };
-
-        $scope.ignoreDevice = function (deviceID, pendingDevice) {
-            var ignoredDevice = angular.copy(pendingDevice);
-            ignoredDevice.deviceID = deviceID;
-            // Bump time
-            ignoredDevice.time = (new Date()).toISOString();
-            $scope.config.remoteIgnoredDevices.push(ignoredDevice);
-            $scope.saveConfig();
-        };
-
-        $scope.unignoreDeviceFromTemporaryConfig = function (ignoredDevice) {
-            $scope.tmpRemoteIgnoredDevices = $scope.tmpRemoteIgnoredDevices.filter(function (existingIgnoredDevice) {
-                return ignoredDevice.deviceID !== existingIgnoredDevice.deviceID;
-            });
-        };
-
-        $scope.ignoredFoldersCountTmpConfig = function () {
-            var count = 0;
-            ($scope.tmpDevices || []).forEach(function (deviceCfg) {
-                count += deviceCfg.ignoredFolders.length;
-            });
-            return count;
-        };
-
-        $scope.unignoreFolderFromTemporaryConfig = function (device, ignoredFolderID) {
-            for (var i = 0; i < $scope.tmpDevices.length; i++) {
-                if ($scope.tmpDevices[i].deviceID == device) {
-                    $scope.tmpDevices[i].ignoredFolders = $scope.tmpDevices[i].ignoredFolders.filter(function (existingIgnoredFolder) {
-                        return existingIgnoredFolder.id !== ignoredFolderID;
-                    });
-                    return;
-                }
-            }
-        };
-
-        $scope.otherDevices = function () {
-            return $scope.deviceList().filter(function (n) {
-                return n.deviceID !== $scope.myID;
-            });
-        };
-
-        $scope.thisDevice = function () {
-            return $scope.devices[$scope.myID];
-        };
-
-        $scope.thisDeviceIn = function (l) {
-            for (var i = 0; i < l.length; i++) {
-                var n = l[i];
-                if (n.deviceID === $scope.myID) {
-                    return n;
-                }
-            }
-        };
-
-        $scope.allDevices = function () {
-            var devices = $scope.otherDevices();
-            devices.push($scope.thisDevice());
-            return devices;
-        };
-
-        $scope.setAllDevicesPause = function (pause) {
-            for (var id in $scope.devices) {
-                $scope.devices[id].paused = pause;
-            };
-            $scope.config.devices = deviceList($scope.devices);
-            $scope.saveConfig();
-        }
-
-        $scope.isAtleastOneDevicePausedStateSetTo = function (pause) {
-            for (var id in $scope.devices) {
-                if ($scope.devices[id].paused == pause) {
-                    return true;
-                }
-            }
-
-            return false
-        }
-
-        $scope.errorList = function () {
-            if (!$scope.errors) {
-                return [];
-            }
-            return $scope.errors.filter(function (e) {
-                return e.when > $scope.seenError;
-            });
-        };
-
-        $scope.clearErrors = function () {
-            $scope.seenError = $scope.errors[$scope.errors.length - 1].when;
-            $http.post(urlbase + '/system/error/clear');
-        };
-
-        $scope.fsWatcherErrorMap = function () {
-            var errs = {}
-            $.each($scope.folders, function (id, cfg) {
-                if (cfg.fsWatcherEnabled && $scope.model[cfg.id] && $scope.model[id].watchError && !cfg.paused && $scope.folderStatus(cfg) !== 'stopped') {
-                    errs[id] = $scope.model[id].watchError;
-                }
-            });
-            return errs;
-        };
-
-        $scope.friendlyDevices = function (str) {
-            for (var id in $scope.devices) {
-                str = str.replace(id, $scope.deviceName($scope.devices[id]));
-            }
-            return str;
-        };
-
-        $scope.folderList = function () {
-            return folderList($scope.folders);
-        };
-
-        $scope.deviceList = function () {
-            return deviceList($scope.devices);
-        };
-
-        $scope.directoryList = [];
-
-        $scope.$watch('currentFolder.path', function (newvalue) {
-            if (!newvalue) {
-                return;
-            }
-            $scope.currentFolder.path = expandTilde(newvalue);
-            $http.get(urlbase + '/system/browse', {
-                params: { current: newvalue }
-            }).success(function (data) {
-                $scope.directoryList = data;
-            }).error($scope.emitHTTPError);
-        });
-
-        $scope.$watch('currentFolder.label', function (newvalue) {
-            if (!newvalue || !shouldSetDefaultFolderPath()) {
-                return;
-            }
-            $scope.currentFolder.path = pathJoin($scope.config.defaults.folder.path, newvalue);
-        });
-
-        $scope.$watch('currentFolder.id', function (newvalue) {
-            if (!newvalue || !shouldSetDefaultFolderPath() || $scope.currentFolder.label) {
-                return;
-            }
-            $scope.currentFolder.path = pathJoin($scope.config.defaults.folder.path, newvalue);
-        });
-
-        $scope.setFSWatcherIntervalDefault = function () {
-            var defaultRescanIntervals = [60, 3600, 3600*24];
-            if (defaultRescanIntervals.indexOf($scope.currentFolder.rescanIntervalS) === -1) {
-                return;
-            }
-            var idx;
-            if ($scope.currentFolder.fsWatcherEnabled) {
-                idx = 1;
-            } else if ($scope.currentFolder.type === 'receiveencrypted') {
-                idx = 2;
-            } else {
-                idx = 0;
-            }
-            $scope.currentFolder.rescanIntervalS = defaultRescanIntervals[idx];
-        };
-
-        $scope.setDefaultsForFolderType = function () {
-            if ($scope.currentFolder.type === 'receiveencrypted') {
-                $scope.currentFolder.fsWatcherEnabled = false;
-                $scope.currentFolder.ignorePerms = true;
-                delete $scope.currentFolder.versioning;
-            } else {
-                $scope.currentFolder.fsWatcherEnabled = true;
-            }
-            $scope.setFSWatcherIntervalDefault();
-        };
-
-        $scope.loadFormIntoScope = function (form) {
-            console.log('loadFormIntoScope', form.$name);
-            switch (form.$name) {
-                case 'deviceEditor':
-                    $scope.deviceEditor = form;
-                    break;
-                case 'folderEditor':
-                    $scope.folderEditor = form;
-                    break;
-            }
-        };
-
-        $scope.globalChanges = function () {
-            $('#globalChanges').modal();
-        };
-
-        function editFolderModal() {
-            initVersioningEditing();
-            $scope.currentFolder._recvEnc = $scope.currentFolder.type === 'receiveencrypted';
-            $scope.folderPathErrors = {};
-            $scope.folderEditor.$setPristine();
-            $('#editFolder').modal().one('shown.bs.tab', function (e) {
-                if (e.target.attributes.href.value === "#folder-ignores") {
-                    $('#folder-ignores textarea').focus();
-                }
-            }).one('hidden.bs.modal', function () {
-                $('.nav-tabs a[href="#folder-general"]').tab('show');
-                window.location.hash = "";
-            });
-        };
-
-        $scope.editFolderModalTitle = function() {
-            if ($scope.editingDefaults) {
-                return $translate.instant("Edit Folder Defaults");
-            }
-            var title = '';
-            if ($scope.editingExisting) {
-                title += $translate.instant("Edit Folder");
-            } else {
-                title += $translate.instant("Add Folder");
-            }
-            if ($scope.currentFolder.id !== '') {
-                title += ' (' + $scope.folderLabel($scope.currentFolder.id) + ')';
-            }
-            return title;
-        };
-
-        $scope.editFolderModalIcon = function() {
-            if ($scope.editingDefaults || $scope.editingExisting) {
-                return 'fas fa-pencil-alt';
-            }
-            return 'fas fa-folder';
-        };
-
-        function editFolder() {
-            if ($scope.currentFolder.path.length > 1 && $scope.currentFolder.path.slice(-1) === $scope.system.pathSeparator) {
-                $scope.currentFolder.path = $scope.currentFolder.path.slice(0, -1);
-            } else if (!$scope.currentFolder.path) {
-                // undefined path leads to invalid input field
-                $scope.currentFolder.path = '';
-            }
-            initShareEditing('folder');
-            editFolderModal();
-        }
-
-        $scope.internalVersioningEnabled = function(guiVersioning) {
-            if (!$scope.currentFolder._guiVersioning) {
-                return false;
-            }
-            return ['none', 'external'].indexOf($scope.currentFolder._guiVersioning.selector) === -1;
-        };
-
-        function initVersioningEditing() {
-            $scope.currentFolder._guiVersioning = angular.copy($scope.versioningDefaults);
-
-            var currentVersioning = $scope.currentFolder.versioning;
-
-            if (!currentVersioning || !currentVersioning.type || currentVersioning.type === 'none') {
-                return;
-            }
-
-            $scope.currentFolder._guiVersioning.cleanupIntervalS = +currentVersioning.cleanupIntervalS;
-            $scope.currentFolder._guiVersioning.selector = currentVersioning.type;
-
-            // Apply parameters currently in use
-            switch (currentVersioning.type) {
-            case "trashcan":
-                $scope.currentFolder._guiVersioning.trashcanClean = +currentVersioning.params.cleanoutDays;
-                break;
-            case "simple":
-                $scope.currentFolder._guiVersioning.simpleKeep = +currentVersioning.params.keep;
-                $scope.currentFolder._guiVersioning.trashcanClean = +currentVersioning.params.cleanoutDays;
-                break;
-            case "staggered":
-                $scope.currentFolder._guiVersioning.staggeredMaxAge = Math.floor(+currentVersioning.params.maxAge / 86400);
-                $scope.currentFolder._guiVersioning.staggeredCleanInterval = +currentVersioning.params.cleanInterval;
-                break;
-            case "external":
-                $scope.currentFolder._guiVersioning.externalCommand = currentVersioning.params.command;
-                break;
-            }
-        };
-
-        $scope.editFolderExisting = function(folderCfg) {
-            $scope.editingExisting = true;
-            $scope.editingDefaults = false;
-            $scope.currentFolder = angular.copy(folderCfg);
-
-            $scope.ignores.text = 'Loading...';
-            $scope.ignores.error = null;
-            $scope.ignores.disabled = true;
-            $http.get(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id))
-                .success(function (data) {
-                    $scope.currentFolder.ignores = data.ignore || [];
-                    $scope.ignores.text = $scope.currentFolder.ignores.join('\n');
-                    $scope.ignores.error = data.error;
-                    $scope.ignores.disabled = false;
-                })
-                .error(function (err) {
-                    $scope.ignores.text = $translate.instant("Failed to load ignore patterns.");
-                    $scope.emitHTTPError(err);
-                });
-
-            editFolder();
-        };
-
-        $scope.editFolderDefaults = function() {
-            $http.get(urlbase + '/config/defaults/folder')
-                 .success(function (data) {
-                     $scope.currentFolder = data;
-                     $scope.editingExisting = false;
-                     $scope.editingDefaults = true;
-                     editFolder();
-                 })
-                 .error($scope.emitHTTPError);
-        };
-
-        $scope.selectAllSharedDevices = function (state) {
-            var devices = $scope.currentSharing.shared;
-            for (var i = 0; i < devices.length; i++) {
-                $scope.currentSharing.selected[devices[i].deviceID] = !!state;
-            }
-        };
-
-        $scope.selectAllUnrelatedDevices = function (state) {
-            var devices = $scope.currentSharing.unrelated;
-            for (var i = 0; i < devices.length; i++) {
-                $scope.currentSharing.selected[devices[i].deviceID] = !!state;
-            }
-        };
-
-        $scope.addFolder = function () {
-            $http.get(urlbase + '/svc/random/string?length=10').success(function (data) {
-                var folderID = (data.random.substr(0, 5) + '-' + data.random.substr(5, 5)).toLowerCase();
-                addFolderInit(folderID).then(function() {
-                    // Triggers the watch that sets the path
-                    $scope.currentFolder.label = $scope.currentFolder.label;
-                    editFolderModal();
-                });
-            });
-        };
-
-        $scope.addFolderAndShare = function (folderID, pendingFolder, device) {
-            addFolderInit(folderID).then(function() {
-                $scope.currentFolder.viewFlags = {
-                    importFromOtherDevice: true
-                };
-                $scope.currentSharing.selected[device] = true;
-                $scope.currentFolder.label = pendingFolder.offeredBy[device].label;
-                for (var k in pendingFolder.offeredBy) {
-                    if (pendingFolder.offeredBy[k].receiveEncrypted) {
-                        $scope.currentFolder.type = "receiveencrypted";
-                        $scope.setDefaultsForFolderType();
-                        break;
-                    }
-                }
-                editFolderModal();
-            });
-        };
-
-        function addFolderInit(folderID) {
-            $scope.editingExisting = false;
-            $scope.editingDefaults = false;
-            return $http.get(urlbase + '/config/defaults/folder').then(function(p) {
-                $scope.currentFolder = p.data;
-                $scope.currentFolder.id = folderID;
-
-                initShareEditing('folder');
-                $scope.currentSharing.unrelated = $scope.currentSharing.unrelated.concat($scope.currentSharing.shared);
-                $scope.currentSharing.shared = [];
-
-                $scope.ignores.text = '';
-                $scope.ignores.error = null;
-                $scope.ignores.disabled = false;
-            }, $scope.emitHTTPError);
-        }
-
-        $scope.shareFolderWithDevice = function (folder, device) {
-            $scope.folders[folder].devices.push({
-                deviceID: device
-            });
-            $scope.config.folders = folderList($scope.folders);
-            $scope.saveConfig();
-        };
-
-        $scope.saveFolder = function () {
-            $('#editFolder').modal('hide');
-            var folderCfg = angular.copy($scope.currentFolder);
-            $scope.currentSharing.selected[$scope.myID] = true;
-            var newDevices = [];
-            folderCfg.devices.forEach(function (dev) {
-                if ($scope.currentSharing.selected[dev.deviceID] === true) {
-                    dev.encryptionPassword = $scope.currentSharing.encryptionPasswords[dev.deviceID];
-                    newDevices.push(dev);
-                    delete $scope.currentSharing.selected[dev.deviceID];
-                };
-            });
-            for (var deviceID in $scope.currentSharing.selected) {
-                if ($scope.currentSharing.selected[deviceID] === true) {
-                    newDevices.push({
-                        deviceID: deviceID,
-                        encryptionPassword: $scope.currentSharing.encryptionPasswords[deviceID],
-                    });
-                }
-            }
-            folderCfg.devices = newDevices;
-            delete $scope.currentSharing;
-
-            if (!folderCfg.versioning) {
-                folderCfg.versioning = {params: {}};
-            }
-            folderCfg.versioning.type = folderCfg._guiVersioning.selector;
-            if ($scope.internalVersioningEnabled()) {
-                folderCfg.versioning.cleanupIntervalS = folderCfg._guiVersioning.cleanupIntervalS;
-            }
-            switch (folderCfg._guiVersioning.selector) {
-            case "trashcan":
-                folderCfg.versioning.params.cleanoutDays = '' + folderCfg._guiVersioning.trashcanClean;
-                break;
-            case "simple":
-                folderCfg.versioning.params.keep = '' + folderCfg._guiVersioning.simpleKeep,
-                folderCfg.versioning.params.cleanoutDays = '' + folderCfg._guiVersioning.trashcanClean;
-                break;
-            case "staggered":
-                folderCfg.versioning.params.maxAge = '' + (folderCfg._guiVersioning.staggeredMaxAge * 86400);
-                folderCfg.versioning.params.cleanInterval = '' + folderCfg._guiVersioning.staggeredCleanInterval;
-                break;
-            case "external":
-                folderCfg.versioning.params.command = '' + folderCfg._guiVersioning.externalCommand;
-                break;
-            default:
-                delete folderCfg.versioning;
-            }
-            delete folderCfg._guiVersioning;
-
-            if ($scope.editingDefaults) {
-                $scope.config.defaults.folder = folderCfg;
-                $scope.saveConfig();
-            } else {
-                saveFolderExisting(folderCfg);
-            }
-        };
-
-        function saveFolderExisting(folderCfg) {
-            var ignoresLoaded = !$scope.ignores.disabled;
-            var ignores = $scope.ignores.text.split('\n');
-            // Split always returns a minimum 1-length array even for no patterns
-            if (ignores.length === 1 && ignores[0] === "") {
-                ignores = [];
-            }
-            if (!$scope.editingExisting && ignores.length) {
-                folderCfg.paused = true;
-            };
-
-            $scope.folders[folderCfg.id] = folderCfg;
-            $scope.config.folders = folderList($scope.folders);
-
-            function arrayEquals(a, b) {
-              return a.length === b.length && a.every(function(v, i) { return v === b[i] });
-            }
-
-            if (ignoresLoaded && $scope.editingExisting && !arrayEquals(ignores, folderCfg.ignores)) {
-                saveIgnores(ignores);
-            };
-
-            $scope.saveConfig(function () {
-                if (!$scope.editingExisting && ignores.length) {
-                    saveIgnores(ignores, function () {
-                        $scope.setFolderPause(folderCfg.id, false);
-                    });
-                }
-            });
-        };
-
-        $scope.ignoreFolder = function (device, folderID, offeringDevice) {
-            var ignoredFolder = {
-                id: folderID,
-                label: offeringDevice.label,
-                // Bump time
-                time: (new Date()).toISOString()
-            }
-
-            if (device in $scope.devices) {
-                $scope.devices[device].ignoredFolders.push(ignoredFolder);
-                $scope.saveConfig();
-            }
-        };
-
-        $scope.sharesFolder = function (folderCfg) {
-            var names = [];
-            folderCfg.devices.forEach(function (device) {
-                if (device.deviceID !== $scope.myID) {
-                    names.push($scope.deviceName($scope.devices[device.deviceID]));
-                }
-            });
-            names.sort();
-            return names.join(", ");
-        };
-
-        $scope.deviceFolders = function (deviceCfg) {
-            var folders = [];
-            $scope.folderList().forEach(function (folder) {
-                for (var i = 0; i < folder.devices.length; i++) {
-                    if (folder.devices[i].deviceID === deviceCfg.deviceID) {
-                        folders.push(folder.id);
-                        break;
-                    }
-                }
-            });
-            return folders;
-        };
-
-        $scope.folderLabel = function (folderID) {
-            if (!$scope.folders[folderID]) {
-                return folderID;
-            }
-            var label = $scope.folders[folderID].label;
-            return label && label.length > 0 ? label : folderID;
-        };
-
-        $scope.deleteFolder = function (id) {
-            $('#editFolder').modal('hide');
-            if (!$scope.editingExisting) {
-                return;
-            }
-
-            delete $scope.folders[id];
-            delete $scope.model[id];
-            $scope.config.folders = folderList($scope.folders);
-            recalcLocalStateTotal();
-
-            $scope.saveConfig();
-        };
-
-        function resetRestoreVersions() {
-            $scope.restoreVersions = {
-                folder: null,
-                selections: {},
-                versions: null,
-                tree: null,
-                errors: null,
-                filters: {},
-                massAction: function (name, action) {
-                    $.each($scope.restoreVersions.versions, function (key) {
-                        if (key.indexOf(name + '/') == 0 && (!$scope.restoreVersions.filters.text || key.indexOf($scope.restoreVersions.filters.text) > -1)) {
-                            if (action == 'unset') {
-                                delete $scope.restoreVersions.selections[key];
-                                return;
-                            }
-
-                            var availableVersions = [];
-                            $.each($scope.restoreVersions.filterVersions($scope.restoreVersions.versions[key]), function (idx, version) {
-                                availableVersions.push(version.versionTime);
-                            })
-
-                            if (availableVersions.length) {
-                                availableVersions.sort(function (a, b) { return a - b; });
-                                if (action == 'latest') {
-                                    $scope.restoreVersions.selections[key] = availableVersions.pop();
-                                } else if (action == 'oldest') {
-                                    $scope.restoreVersions.selections[key] = availableVersions.shift();
-                                }
-                            }
-                        }
-                    });
-                },
-                filterVersions: function (versions) {
-                    var filteredVersions = [];
-                    $.each(versions, function (idx, version) {
-                        if (moment(version.versionTime).isBetween($scope.restoreVersions.filters['start'], $scope.restoreVersions.filters['end'], null, '[]')) {
-                            filteredVersions.push(version);
-                        }
-                    });
-                    return filteredVersions;
-                },
-                selectionCount: function () {
-                    var count = 0;
-                    $.each($scope.restoreVersions.selections, function (key, value) {
-                        if (value) {
-                            count++;
-                        }
-                    });
-                    return count;
-                },
-
-                restore: function () {
-                    $scope.restoreVersions.tree.clear();
-                    $scope.restoreVersions.tree = null;
-                    $scope.restoreVersions.versions = null;
-                    var selections = {};
-                    $.each($scope.restoreVersions.selections, function (key, value) {
-                        if (value) {
-                            selections[key] = value;
-                        }
-                    });
-                    $scope.restoreVersions.selections = {};
-
-                    $http.post(urlbase + '/folder/versions?folder=' + encodeURIComponent($scope.restoreVersions.folder), selections).success(function (data) {
-                        if (Object.keys(data).length == 0) {
-                            $('#restoreVersions').modal('hide');
-                        } else {
-                            $scope.restoreVersions.errors = data;
-                        }
-                    });
-                },
-                show: function (folder) {
-                    $scope.restoreVersions.folder = folder;
-
-                    var closed = false;
-                    var modalShown = $q.defer();
-                    $('#restoreVersions').modal().one('hidden.bs.modal', function () {
-                        closed = true;
-                        resetRestoreVersions();
-                    }).one('shown.bs.modal', function () {
-                        modalShown.resolve();
-                    });
-
-                    var dataReceived = $http.get(urlbase + '/folder/versions?folder=' + encodeURIComponent($scope.restoreVersions.folder))
-                        .success(function (data) {
-                            $.each(data, function (key, values) {
-                                $.each(values, function (idx, value) {
-                                    value.modTime = new Date(value.modTime);
-                                    value.versionTime = new Date(value.versionTime);
-                                });
-                                values.sort(function (a, b) {
-                                    return b.versionTime - a.versionTime;
-                                });
-                            });
-                            if (closed) return;
-                            $scope.restoreVersions.versions = data;
-                        });
-
-                    $q.all([dataReceived, modalShown.promise]).then(function () {
-                        $timeout(function () {
-                            if (closed) {
-                                resetRestoreVersions();
-                                return;
-                            }
-
-                            $scope.restoreVersions.tree = $("#restoreTree").fancytree({
-                                extensions: ["table", "filter"],
-                                quicksearch: true,
-                                filter: {
-                                    autoApply: true,
-                                    counter: true,
-                                    hideExpandedCounter: true,
-                                    hideExpanders: true,
-                                    highlight: true,
-                                    leavesOnly: false,
-                                    nodata: true,
-                                    mode: "hide"
-                                },
-                                table: {
-                                    indentation: 20,
-                                    nodeColumnIdx: 0,
-                                },
-                                debugLevel: 2,
-                                source: buildTree($scope.restoreVersions.versions),
-                                renderColumns: function (event, data) {
-                                    var node = data.node,
-                                        $tdList = $(node.tr).find(">td"),
-                                        template;
-                                    if (node.folder) {
-                                        template = '<div ng-include="\'syncthing/folder/restoreVersionsMassActions.html\'" class="pull-right"/>';
-                                    } else {
-                                        template = '<div ng-include="\'syncthing/folder/restoreVersionsVersionSelector.html\'" class="pull-right"/>';
-                                    }
-
-                                    var scope = $rootScope.$new(true);
-                                    scope.key = node.key;
-                                    scope.restoreVersions = $scope.restoreVersions;
-
-                                    $tdList.eq(1).html(
-                                        $compile(template)(scope)
-                                    );
-
-                                    // Force angular to redraw.
-                                    $timeout(function () {
-                                        $scope.$apply();
-                                    });
-                                }
-                            }).fancytree("getTree");
-
-                            var minDate = moment(),
-                                maxDate = moment(0, 'X'),
-                                date;
-
-                            // Find version window.
-                            $.each($scope.restoreVersions.versions, function (key) {
-                                $.each($scope.restoreVersions.versions[key], function (idx, version) {
-                                    date = moment(version.versionTime);
-                                    if (date.isBefore(minDate)) {
-                                        minDate = date;
-                                    }
-                                    if (date.isAfter(maxDate)) {
-                                        maxDate = date;
-                                    }
-                                });
-                            });
-
-                            $scope.restoreVersions.filters['start'] = minDate;
-                            $scope.restoreVersions.filters['end'] = maxDate;
-
-                            var ranges = {
-                                'All time': [minDate, maxDate],
-                                'Today': [moment(), moment()],
-                                'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
-                                'Last 7 Days': [moment().subtract(6, 'days'), moment()],
-                                'Last 30 Days': [moment().subtract(29, 'days'), moment()],
-                                'This Month': [moment().startOf('month'), moment().endOf('month')],
-                                'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
-                            };
-
-                            // Filter out invalid ranges.
-                            $.each(ranges, function (key, range) {
-                                if (!range[0].isBetween(minDate, maxDate, null, '[]') && !range[1].isBetween(minDate, maxDate, null, '[]')) {
-                                    delete ranges[key];
-                                }
-                            });
-
-                            $("#restoreVersionDateRange").daterangepicker({
-                                timePicker: true,
-                                timePicker24Hour: true,
-                                timePickerSeconds: true,
-                                autoUpdateInput: true,
-                                opens: "left",
-                                drops: "up",
-                                startDate: minDate,
-                                endDate: maxDate,
-                                minDate: minDate,
-                                maxDate: maxDate,
-                                ranges: ranges,
-                                locale: {
-                                    format: 'YYYY/MM/DD HH:mm:ss',
-                                }
-                            }).on('apply.daterangepicker', function (ev, picker) {
-                                $scope.restoreVersions.filters['start'] = picker.startDate;
-                                $scope.restoreVersions.filters['end'] = picker.endDate;
-                                // Events for this UI element are not managed by angular.
-                                // Force angular to wake up.
-                                $timeout(function () {
-                                    $scope.$apply();
-                                });
-                            });
-                        });
-                    });
-                }
-            };
-        }
-        resetRestoreVersions();
-
-        $scope.$watchCollection('restoreVersions.filters', function () {
-            if (!$scope.restoreVersions.tree) return;
-
-            $scope.restoreVersions.tree.filterNodes(function (node) {
-                if (node.folder) return false;
-                if ($scope.restoreVersions.filters.text && node.key.indexOf($scope.restoreVersions.filters.text) < 0) {
-                    return false;
-                }
-                if ($scope.restoreVersions.filterVersions(node.data.versions).length == 0) {
-                    return false;
-                }
-                return true;
-            });
-        });
-
-        $scope.setAPIKey = function (cfg) {
-            $http.get(urlbase + '/svc/random/string?length=32').success(function (data) {
-                cfg.apiKey = data.random;
-            });
-        };
-
-        $scope.acceptUR = function () {
-            $scope.config.options.urAccepted = $scope.system.urVersionMax;
-            $scope.config.options.urSeen = $scope.system.urVersionMax;
-            $scope.saveConfig();
-            $('#ur').modal('hide');
-        };
-
-        $scope.declineUR = function () {
-            if ($scope.config.options.urAccepted === 0) {
-                $scope.config.options.urAccepted = -1;
-            }
-            $scope.config.options.urSeen = $scope.system.urVersionMax;
-            $scope.saveConfig();
-            $('#ur').modal('hide');
-        };
-
-        $scope.showNeed = function (folder) {
-            $scope.neededFolder = folder;
-            $scope.refreshNeed(1, 10);
-            $('#needed').modal().one('hidden.bs.modal', function () {
-                $scope.needed = undefined;
-                $scope.neededFolder = '';
-            });
-        };
-
-        $scope.showRemoteNeed = function (device) {
-            resetRemoteNeed();
-            $scope.remoteNeedDevice = device;
-            $scope.deviceFolders(device).forEach(function (folder) {
-                var comp = $scope.completion[device.deviceID][folder];
-                if (comp !== undefined && comp.needItems + comp.needDeletes === 0) {
-                    return;
-                }
-                $scope.remoteNeedFolders.push(folder);
-                $scope.refreshRemoteNeed(folder, 1, 10);
-            });
-            $('#remoteNeed').modal().one('hidden.bs.modal', function () {
-                resetRemoteNeed();
-            });
-        };
-
-        $scope.showFailed = function (folder) {
-            $scope.failed.folder = folder;
-            $scope.failed = $scope.refreshFailed(1, 10);
-            $('#failed').modal().one('hidden.bs.modal', function () {
-                $scope.failed = {};
-            });
-        };
-
-        $scope.hasFailedFiles = function (folder) {
-            if (!$scope.model[folder]) {
-                return false;
-            }
-            return $scope.model[folder].errors !== 0;
-        };
-
-        $scope.showLocalChanged = function (folder, folderType) {
-            $scope.localChangedFolder = folder;
-            $scope.localChangedType = folderType;
-            $scope.localChanged = $scope.refreshLocalChanged(1, 10);
-            $('#localChanged').modal().one('hidden.bs.modal', function () {
-                $scope.localChanged = {};
-                $scope.localChangedFolder = undefined;
-                $scope.localChangedType = undefined;
-            });
-        };
-
-        $scope.hasReceiveOnlyChanged = function (folderCfg) {
-            if (!folderCfg || folderCfg.type !== "receiveonly") {
-                return false;
-            }
-            var counts = $scope.model[folderCfg.id];
-            return counts && counts.receiveOnlyTotalItems > 0;
-        };
-
-        $scope.hasReceiveEncryptedItems = function (folderCfg) {
-            if (!folderCfg || folderCfg.type !== "receiveencrypted") {
-                return false;
-            }
-            return $scope.receiveEncryptedItemsCount(folderCfg) > 0;
-        };
-
-        $scope.receiveEncryptedItemsCount = function (folderCfg) {
-            var counts = $scope.model[folderCfg.id];
-            if (!counts) {
-                return 0;
-            }
-            return counts.receiveOnlyTotalItems - counts.receiveOnlyChangedDeletes;
-        }
-
-        $scope.revertOverride = function () {
-            $http.post(
-                urlbase + "/db/" + $scope.revertOverrideParams.operation +"?folder="
-                +encodeURIComponent($scope.revertOverrideParams.folderID));
-        };
-
-        $scope.revertOverrideConfirmationModal = function (type, folderID) {
-            var params = {
-                type: type,
-                folderID: folderID,
-            };
-            switch (type) {
-                case "override":
-                    params.heading = $translate.instant("Override");
-                    params.operation = "override";
-                    break;
-                case "revert":
-                    params.heading = $translate.instant("Revert");
-                    params.operation = "revert";
-                    break;
-                case "deleteEnc":
-                    params.heading = $translate.instant("Delete Unexpected Items");
-                    params.operation = "revert";
-                    break;
-            }
-            $scope.revertOverrideParams = params;
-            $('#revert-override-confirmation').modal('show');
-        };
-
-        $scope.advanced = function () {
-            $scope.advancedConfig = angular.copy($scope.config);
-            $scope.advancedConfig.devices.sort(deviceCompare);
-            $scope.advancedConfig.folders.sort(folderCompare);
-            $('#advanced').modal('show');
-        };
-
-        $scope.showReportPreview = function () {
-            $scope.reportPreview = true;
-        };
-
-        $scope.refreshReportDataPreview = function (ver, diff) {
-            $scope.reportDataPreview = '';
-            if (!ver) {
-                return;
-            }
-            var version = parseInt(ver);
-            if (diff && version > 2) {
-                $q.all([
-                    $http.get(urlbase + '/svc/report?version=' + version),
-                    $http.get(urlbase + '/svc/report?version=' + (version - 1)),
-                ]).then(function (responses) {
-                    var newReport = responses[0].data;
-                    var oldReport = responses[1].data;
-                    angular.forEach(oldReport, function (_, key) {
-                        delete newReport[key];
-                    });
-                    $scope.reportDataPreview = newReport;
-                });
-            } else {
-                $http.get(urlbase + '/svc/report?version=' + version).success(function (data) {
-                    $scope.reportDataPreview = data;
-                }).error($scope.emitHTTPError);
-            }
-        };
-
-        $scope.rescanAllFolders = function () {
-            $http.post(urlbase + "/db/scan");
-        };
-
-        $scope.rescanFolder = function (folder) {
-            $http.post(urlbase + "/db/scan?folder=" + encodeURIComponent(folder));
-        };
-
-        $scope.setAllFoldersPause = function (pause) {
-            var folderListCache = $scope.folderList();
-
-            for (var i = 0; i < folderListCache.length; i++) {
-                folderListCache[i].paused = pause;
-            }
-
-            $scope.config.folders = folderList(folderListCache);
-            $scope.saveConfig();
-        };
-
-        $scope.isAtleastOneFolderPausedStateSetTo = function (pause) {
-            var folderListCache = $scope.folderList();
-
-            for (var i = 0; i < folderListCache.length; i++) {
-                if (folderListCache[i].paused == pause) {
-                    return true;
-                }
-            }
-
-            return false;
-        };
-
-        $scope.activateAllFsWatchers = function () {
-            var folders = $scope.folderList();
-
-            $.each(folders, function (i) {
-                if (folders[i].fsWatcherEnabled) {
-                    return;
-                }
-                folders[i].fsWatcherEnabled = true;
-                if (folders[i].rescanIntervalS === 0) {
-                    return;
-                }
-                // Delay full scans, but scan at least once per day
-                folders[i].rescanIntervalS *= 60;
-                if (folders[i].rescanIntervalS > 86400) {
-                    folders[i].rescanIntervalS = 86400;
-                }
-            });
-
-            $scope.config.folders = folders;
-            $scope.saveConfig();
-        };
-
-        $scope.bumpFile = function (folder, file) {
-            var url = urlbase + "/db/prio?folder=" + encodeURIComponent(folder) + "&file=" + encodeURIComponent(file);
-            // In order to get the right view of data in the response.
-            url += "&page=" + $scope.needed.page;
-            url += "&perpage=" + $scope.needed.perpage;
-            $http.post(url).success(function (data) {
-                if ($scope.neededFolder === folder) {
-                    console.log("bumpFile", folder, data);
-                    parseNeeded(data);
-                }
-            }).error($scope.emitHTTPError);
-        };
-
-        $scope.versionString = function () {
-            if (!$scope.version.version) {
-                return '';
-            }
-
-            var os = {
-                'darwin': 'macOS',
-                'dragonfly': 'DragonFly BSD',
-                'freebsd': 'FreeBSD',
-                'openbsd': 'OpenBSD',
-                'netbsd': 'NetBSD',
-                'linux': 'Linux',
-                'windows': 'Windows',
-                'solaris': 'Solaris'
-            }[$scope.version.os] || $scope.version.os;
-
-            var arch = {
-                '386': '32-bit Intel/AMD',
-                'amd64': '64-bit Intel/AMD',
-                'arm': '32-bit ARM',
-                'arm64': '64-bit ARM',
-                'ppc64': '64-bit PowerPC',
-                'ppc64le': '64-bit PowerPC (LE)',
-                'mips': '32-bit MIPS',
-                'mipsle': '32-bit MIPS (LE)',
-                'mips64': '64-bit MIPS',
-                'mips64le': '64-bit MIPS (LE)',
-                'riscv64': '64-bit RISC-V',
-                's390x': '64-bit z/Architecture',
-            }[$scope.version.arch] || $scope.version.arch;
-
-            return $scope.version.version + ', ' + os + ' (' + arch + ')';
-        };
-
-        $scope.inputTypeFor = function (key, value) {
-            if (key.substr(0, 1) === '_') {
-                return 'skip';
-            }
-            if (value === null) {
-                return 'null';
-            }
-            if (typeof value === 'number') {
-                return 'number';
-            }
-            if (typeof value === 'boolean') {
-                return 'checkbox';
-            }
-            if (value instanceof Array) {
-                return 'list';
-            }
-            if (typeof value === 'object') {
-                return 'skip';
-            }
-            return 'text';
-        };
-
-        $scope.themeName = function (theme) {
-            return theme.replace('-', ' ').replace(/(?:^|\s)\S/g, function (a) {
-                return a.toUpperCase();
-            });
-        };
-
-        $scope.modalLoaded = function () {
-            // once all modal elements have been processed
-            if ($('modal').length === 0) {
-                // pseudo main. called on all definitions assigned
-                initController();
-            }
-        };
-
-        $scope.toggleUnits = function () {
-            $scope.metricRates = !$scope.metricRates;
-            try {
-                window.localStorage["metricRates"] = $scope.metricRates;
-            } catch (exception) { }
-        };
-
-        $scope.sizeOf = function (dict) {
-            if (dict === undefined) {
-                return 0;
-            }
-            return Object.keys(dict).length;
-        };
-
-        $scope.dismissNotification = function (id) {
-            var idx = $scope.config.options.unackedNotificationIDs.indexOf(id);
-            if (idx > -1) {
-                $scope.config.options.unackedNotificationIDs.splice(idx, 1);
-                $scope.saveConfig();
-            }
-        };
-
-        $scope.abbreviatedError = function (addr) {
-            var status = $scope.system.lastDialStatus[addr];
-            if (!status || !status.error) {
-                return null;
-            }
-            var time = $filter('date')(status.when, "HH:mm:ss")
-            var err = status.error.replace(/.+: /, '');
-            return err + " (" + time + ")";
-        }
-
-        $scope.setCrashReportingEnabled = function (enabled) {
-            $scope.config.options.crashReportingEnabled = enabled;
-            $scope.saveConfig();
-        };
-
-        $scope.isUnixAddress = function (address) {
-            return address != null &&
-                (address.indexOf('/') == 0 ||
-                    address.indexOf('unix://') == 0 ||
-                    address.indexOf('unixs://') == 0);
-        }
-    })
-    .directive('shareTemplate', function () {
-        return {
-            templateUrl: 'syncthing/core/editShareTemplate.html',
-            scope: {
-                selected: '=',
-                encryptionPasswords: '=',
-                id: '@',
-                label: '@',
-                folderType: '@',
-                untrusted: '=',
-            },
-            link: function(scope, elem, attrs) {
-                var plain = false;
-                scope.togglePasswordVisibility = function() {
-                    scope.plain = !scope.plain;
-                };
-            },
-        }
-    });

+ 0 - 166
gui/default/untrusted/syncthing/device/editDeviceModalView.html

@@ -1,166 +0,0 @@
-<modal id="editDevice" status="default" icon="{{editDeviceModalIcon()}}" heading="{{editDeviceModalTitle()}}" large="yes" closeable="yes">
-  <div class="modal-body">
-    <form role="form" name="deviceEditor">
-      <ul class="nav nav-tabs" ng-init="loadFormIntoScope(deviceEditor)">
-        <li class="active"><a data-toggle="tab" href="#device-general"><span class="fas fa-cog"></span> <span translate>General</span></a></li>
-        <li ng-if="!editingDefaults"><a data-toggle="tab" href="#device-sharing"><span class="fas fa-share-alt"></span> <span translate>Sharing</span></a></li>
-        <li><a data-toggle="tab" href="#device-advanced"><span class="fas fa-cogs"></span> <span translate>Advanced</span></a></li>
-      </ul>
-      <div class="tab-content">
-        <div id="device-general" class="tab-pane in active">
-          <div ng-if="!editingDefaults" class="form-group" ng-class="{'has-error': deviceEditor.deviceID.$invalid && deviceEditor.deviceID.$dirty}" ng-init="loadFormIntoScope(deviceEditor)">
-            <label translate for="deviceID">Device ID</label>
-            <div ng-if="!editingExisting">
-              <input name="deviceID" id="deviceID" class="form-control text-monospace" type="text" ng-model="currentDevice.deviceID" required="" valid-deviceid list="discovery-list" aria-required="true" />
-              <datalist id="discovery-list">
-                <option ng-repeat="id in discovery" value="{{id}}" />
-              </datalist>
-              <p class="help-block" ng-if="discovery && discovery.length !== 0">
-                <span translate>You can also select one of these nearby devices:</span>
-                <ul>
-                  <li ng-repeat="id in discovery"><a href="#" ng-click="currentDevice.deviceID = id">{{id}}</a></li>
-                </ul>
-              </p>
-              <p class="help-block">
-                <span translate ng-if="deviceEditor.deviceID.$valid || deviceEditor.deviceID.$pristine">The device ID to enter here can be found in the "Actions > Show ID" dialog on the other device. Spaces and dashes are optional (ignored).</span>
-                <span translate ng-show="deviceEditor.deviceID.$valid || deviceEditor.deviceID.$pristine">When adding a new device, keep in mind that this device must be added on the other side too.</span>
-                <span translate ng-if="deviceEditor.deviceID.$error.required && deviceEditor.deviceID.$dirty">The device ID cannot be blank.</span>
-                <span translate ng-if="deviceEditor.deviceID.$error.validDeviceid && deviceEditor.deviceID.$dirty">The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.</span>
-                <span translate ng-if="deviceEditor.deviceID.$error.unique && deviceEditor.deviceID.$dirty">A device with that ID is already added.</span>
-              </p>
-            </div>
-            <div ng-if="editingExisting" class="well well-sm text-monospace" select-on-click>{{currentDevice.deviceID}}</div>
-          </div>
-          <div class="form-group">
-            <label translate for="name">Device Name</label>
-            <input id="name" class="form-control" type="text" ng-model="currentDevice.name" />
-            <p translate ng-if="currentDevice.deviceID == myID" class="help-block">Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.</p>
-            <p translate ng-if="currentDevice.deviceID != myID" class="help-block">Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.</p>
-          </div>
-        </div>
-        <div ng-if="!editingDefaults" id="device-sharing" class="tab-pane">
-          <div class="row">
-            <div class="col-md-6">
-              <div class="form-group">
-                <div class="checkbox">
-                  <label>
-                    <input type="checkbox" ng-model="currentDevice.introducer">
-                    <span translate>Introducer</span>
-                    <p translate class="help-block">Add devices from the introducer to our device list, for mutually shared folders.</p>
-                  </label>
-                </div>
-              </div>
-            </div>
-            <div class="col-md-6">
-              <div class="form-group">
-                <div class="checkbox">
-                  <label>
-                    <input type="checkbox" ng-model="currentDevice.autoAcceptFolders">
-                    <span translate>Auto Accept</span>
-                    <p translate class="help-block">Automatically create or share folders that this device advertises at the default path.</p>
-                  </label>
-                </div>
-              </div>
-            </div>
-          </div>
-          <div class="form-group">
-            <div class="form-horizontal" ng-if="currentSharing.shared.length">
-              <label translate for="folders">Shared Folders</label>
-              <p class="help-block">
-                <span translate>Deselect folders to stop sharing with this device.</span>&emsp;
-                <small><a href="#" ng-click="selectAllSharedFolders(true)" translate>Select All</a>&emsp;
-                  <a href="#" ng-click="selectAllSharedFolders(false)" translate>Deselect All</a></small>
-              </p>
-              <div class="form-group" ng-repeat="folder in currentSharing.shared">
-                <share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{folder.id}}" label="{{folderLabel(folder.id)}}" folder-type="{{folder.type}}" untrusted="currentDevice.untrusted" />
-              </div>
-            </div>
-            <div class="form-horizontal" ng-if="currentSharing.unrelated.length">
-              <label translate for="folders">Unshared Folders</label>
-              <p class="help-block" ng-if="folderList().length > 0">
-                <span translate>Select additional folders to share with this device.</span>&emsp;
-                <small><a href="#" ng-click="selectAllUnrelatedFolders(true)" translate>Select All</a>&emsp;
-                  <a href="#" ng-click="selectAllUnrelatedFolders(false)" translate>Deselect All</a></small>
-              </p>
-              <p class="help-block" ng-if="folderList().length == 0">
-                <span translate>There are no folders to share with this device.</span>
-              </p>
-              <div class="form-group" ng-repeat="folder in currentSharing.unrelated">
-                <share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{folder.id}}" label="{{folderLabel(folder.id)}}" folder-type="{{folder.type}}" untrusted="currentDevice.untrusted" />
-              </div>
-            </div>
-          </div>
-        </div>
-        <div id="device-advanced" class="tab-pane">
-          <div class="row form-group">
-            <div class="col-md-6">
-              <div class="form-group">
-                <label translate for="addresses">Addresses</label>
-                <input ng-disabled="currentDevice.deviceID == myID" id="addresses" class="form-control" type="text" ng-model="currentDevice._addressesStr"></input>
-                <p translate class="help-block">Enter comma separated ("tcp://ip:port", "tcp://host:port") addresses or "dynamic" to perform automatic discovery of the address.</p>
-              </div>
-            </div>
-            <div class="col-md-6">
-              <div class="form-group">
-                <label translate>Compression</label>
-                <select class="form-control" ng-model="currentDevice.compression">
-                  <option value="always" translate>All Data</option>
-                  <option value="metadata" translate>Metadata Only</option>
-                  <option value="never" translate>Off</option>
-                </select>
-              </div>
-            </div>
-          </div>
-          <div class="row form-group">
-            <div class="col-md-12">
-              <label translate>Device rate limits</label>
-              <div class="row">
-                <div class="col-md-6" ng-class="{'has-error': deviceEditor.maxRecvKbps.$invalid && deviceEditor.maxRecvKbps.$dirty}">
-                  <div class="row">
-                    <span class="col-md-8" translate>Incoming Rate Limit (KiB/s)</span>
-                    <div class="col-md-4">
-                      <input name="maxRecvKbps" id="maxRecvKbps" class="form-control" type="number" pattern="\d+" ng-model="currentDevice.maxRecvKbps" min="0" />
-                    </div>
-                  </div>
-                  <p class="help-block" ng-if="!deviceEditor.maxRecvKbps.$valid && deviceEditor.maxRecvKbps.$dirty" translate>The rate limit must be a non-negative number (0: no limit)</p>
-                </div>
-                <div class="col-md-6" ng-class="{'has-error': deviceEditor.maxSendKbps.$invalid && deviceEditor.maxSendKbps.$dirty}">
-                  <div class="row">
-                    <span class="col-md-8" translate>Outgoing Rate Limit (KiB/s)</span>
-                    <div class="col-md-4">
-                      <input name="maxSendKbps" id="maxSendKbps" class="form-control" type="number" pattern="\d+" ng-model="currentDevice.maxSendKbps" min="0" />
-                    </div>
-                  </div>
-                  <p class="help-block" ng-if="!deviceEditor.maxSendKbps.$valid && deviceEditor.maxSendKbps.$dirty" translate>The rate limit must be a non-negative number (0: no limit)</p>
-                </div>
-              </div>
-            </div>
-          </div>
-          <div class="row">
-            <div class="form-group col-md-6">
-              <input type="checkbox" id="untrusted" ng-model="currentDevice.untrusted" />
-              <label for="untrusted" translate>Untrusted</label>
-              <p translate class="help-block">All folders shared with this device must be protected by a password, such that all sent data is unreadable without the given password.</p>
-            </div>
-          </div>
-        </div>
-      </div>
-    </form>
-  </div>
-  <div class="modal-footer">
-    <button type="button" class="btn btn-primary btn-sm" ng-click="saveDevice()" ng-disabled="deviceEditor.$invalid">
-      <span class="fas fa-check"></span>&nbsp;<span translate>Save</span>
-    </button>
-    <button ng-if="!editingDefaults" type="button" class="btn btn-default btn-sm" data-toggle="modal" data-target="#idqr" ng-if="editingExisting || deviceEditor.deviceID.$valid">
-      <span class="fas fa-qrcode"></span>&nbsp;<span translate>Show QR</span>
-    </button>
-    <button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
-      <span class="fas fa-times"></span>&nbsp;<span translate>Close</span>
-    </button>
-    <div ng-if="editingExisting && !editingDefaults" class="pull-left">
-      <button type="button" class="btn btn-warning btn-sm" data-toggle="modal" data-target="#remove-device-confirmation">
-        <span class="fas fa-minus-circle"></span>&nbsp;<span translate>Remove</span>
-      </button>
-    </div>
-  </div>
-</modal>

+ 0 - 281
gui/default/untrusted/syncthing/folder/editFolderModalView.html

@@ -1,281 +0,0 @@
-<modal id="editFolder" status="default" icon="{{editFolderModalIcon()}}" heading="{{editFolderModalTitle()}}" large="yes" closeable="yes">
-  <div class="modal-body">
-    <form role="form" name="folderEditor">
-      <ul class="nav nav-tabs" ng-init="loadFormIntoScope(folderEditor)">
-        <li class="active"><a data-toggle="tab" href="#folder-general"><span class="fas fa-cog"></span> <span translate>General</span></a></li>
-        <li><a data-toggle="tab" href="#folder-sharing"><span class="fas fa-share-alt"></span> <span translate>Sharing</span></a></li>
-        <li><a data-toggle="tab" href="#folder-versioning"><span class="fas fa-copy"></span> <span translate>File Versioning</span></a></li>
-        <li ng-if="!editingDefaults" ng-class="{'disabled': currentFolder._recvEnc}"><a ng-attr-data-toggle="{{ currentFolder._recvEnc ? undefined : 'tab'}}" href="{{currentFolder._recvEnc ? '#' : '#folder-ignores'}}"><span class="fas fa-filter"></span> <span translate>Ignore Patterns</span></a></li>
-        <li><a data-toggle="tab" href="#folder-advanced"><span class="fas fa-cogs"></span> <span translate>Advanced</span></a></li>
-      </ul>
-      <div class="tab-content">
-
-        <div id="folder-general" class="tab-pane in active">
-          <div class="form-group" ng-class="{'has-error': folderEditor.folderLabel.$invalid && folderEditor.folderLabel.$dirty && !editingDefaults}">
-            <label for="folderLabel"><span translate>Folder Label</span></label>
-            <input name="folderLabel" id="folderLabel" class="form-control" type="text" ng-model="currentFolder.label" value="{{currentFolder.label}}" />
-            <p class="help-block">
-              <span translate ng-if="folderEditor.folderLabel.$valid || folderEditor.folderLabel.$pristine">Optional descriptive label for the folder. Can be different on each device.</span>
-            </p>
-          </div>
-          <div ng-if="!editingDefaults" class="form-group" ng-class="{'has-error': folderEditor.folderID.$invalid && folderEditor.folderID.$dirty}">
-            <label for="folderID"><span translate>Folder ID</span></label>
-            <input name="folderID" ng-readonly="editingExisting || (!editingExisting && currentFolder.viewFlags.importFromOtherDevice)" id="folderID" class="form-control" type="text" ng-model="currentFolder.id" required="" aria-required="true" unique-folder value="{{currentFolder.id}}" />
-            <p class="help-block">
-              <span translate ng-if="folderEditor.folderID.$valid || folderEditor.folderID.$pristine">Required identifier for the folder. Must be the same on all cluster devices.</span>
-              <span translate ng-if="folderEditor.folderID.$error.uniqueFolder">The folder ID must be unique.</span>
-              <span translate ng-if="folderEditor.folderID.$error.required && folderEditor.folderID.$dirty">The folder ID cannot be blank.</span>
-              <span translate ng-show="!editingExisting">When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.</span>
-            </p>
-          </div>
-          <div class="form-group" ng-class="{'has-error': folderEditor.folderPath.$invalid && folderEditor.folderPath.$dirty && !editingDefaults}">
-            <label translate for="folderPath">Folder Path</label>
-            <input name="folderPath" ng-readonly="editingExisting && !editingDefaults" id="folderPath" class="form-control" type="text" ng-model="currentFolder.path" list="directory-list" ng-required="!editingDefaults" ng-aria-required="!editingDefaults" path-is-sub-dir />
-            <datalist id="directory-list">
-              <option ng-repeat="directory in directoryList" value="{{ directory }}" />
-            </datalist>
-            <p class="help-block">
-              <span ng-if="folderEditor.folderPath.$valid || folderEditor.folderPath.$pristine"><span translate>Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for</span> <code>{{system.tilde}}</code>.</br></span>
-              <span translate ng-if="folderEditor.folderPath.$error.required && folderEditor.folderPath.$dirty && !editingDefaults">The folder path cannot be blank.</span>
-              <span class="text-danger" translate translate-value-other-folder="{{folderPathErrors.otherID}}" ng-if="folderPathErrors.isSub && folderPathErrors.otherLabel.length == 0">Warning, this path is a subdirectory of an existing folder "{%otherFolder%}".</span>
-              <span class="text-danger" translate translate-value-other-folder="{{folderPathErrors.otherID}}" translate-value-other-folder-label="{{folderPathErrors.otherLabel}}" ng-if="folderPathErrors.isSub && folderPathErrors.otherLabel.length != 0">Warning, this path is a subdirectory of an existing folder "{%otherFolderLabel%}" ({%otherFolder%}).</span>
-              <span ng-if="folderPathErrors.isParent && !editingDefaults">
-                <span class="text-danger" translate translate-value-other-folder="{{folderPathErrors.otherID}}" ng-if="folderPathErrors.otherLabel.length == 0">Warning, this path is a parent directory of an existing folder "{%otherFolder%}".</span>
-                <span class="text-danger" translate translate-value-other-folder="{{folderPathErrors.otherID}}" translate-value-other-folder-label="{{folderPathErrors.otherLabel}}" ng-if="folderPathErrors.otherLabel.length != 0">Warning, this path is a parent directory of an existing folder "{%otherFolderLabel%}" ({%otherFolder%}).</span>
-              </span>
-            </p>
-          </div>
-        </div>
-
-        <div id="folder-sharing" class="tab-pane">
-          <div class="form-horizontal" ng-if="currentSharing.shared.length">
-            <label translate>Currently Shared With Devices</label>
-            <p class="help-block">
-              <span translate>Deselect devices to stop sharing this folder with.</span>&emsp;
-              <small><a href="#" ng-click="selectAllSharedDevices(true)" translate>Select All</a>&emsp;
-                <a href="#" ng-click="selectAllSharedDevices(false)" translate>Deselect All</a></small>
-            </p>
-            <div class="form-group" ng-repeat="device in currentSharing.shared">
-              <share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{device.deviceID}}" label="{{deviceName(device)}}" folder-type="{{currentFolder.type}}" untrusted="device.untrusted" />
-            </div>
-          </div>
-          <div class="form-horizontal" ng-if="currentSharing.unrelated.length || otherDevices().length <= 0">
-            <label translate>Unshared Devices</label>
-            <p class="help-block" ng-if="otherDevices().length > 0">
-              <span translate>Select additional devices to share this folder with.</span>&emsp;
-              <small><a href="#" ng-click="selectAllUnrelatedDevices(true)" translate>Select All</a>&emsp;
-                <a href="#" ng-click="selectAllUnrelatedDevices(false)" translate>Deselect All</a></small>
-            </p>
-            <p class="help-block" ng-if="otherDevices().length <= 0">
-              <span translate>There are no devices to share this folder with.</span>
-            </p>
-            <div class="form-group" ng-repeat="device in currentSharing.unrelated" ng-init="id = device.deviceID; folder = currentFolder">
-              <share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{device.deviceID}}" label="{{deviceName(device)}}" folder-type="{{currentFolder.type}}" untrusted="device.untrusted" />
-            </div>
-          </div>
-        </div>
-
-        <div id="folder-versioning" class="tab-pane">
-          <div class="form-group">
-            <label translate>File Versioning</label>&emsp;<a href="https://docs.syncthing.net/users/versioning.html" target="_blank"><span class="fas fa-question-circle"></span>&nbsp;<span translate>Help</span></a>
-            <select class="form-control" ng-model="currentFolder._guiVersioning.selector">
-              <option value="none" translate>No File Versioning</option>
-              <option value="trashcan" translate>Trash Can File Versioning</option>
-              <option value="simple" translate>Simple File Versioning</option>
-              <option value="staggered" translate>Staggered File Versioning</option>
-              <option value="external" translate>External File Versioning</option>
-            </select>
-          </div>
-          <div class="form-group" ng-if="currentFolder._guiVersioning.selector=='trashcan' || currentFolder._guiVersioning.selector=='simple'" ng-class="{'has-error': folderEditor.trashcanClean.$invalid && folderEditor.trashcanClean.$dirty}">
-            <p translate class="help-block">Files are moved to .stversions directory when replaced or deleted by Syncthing.</p>
-            <label translate for="trashcanClean">Clean out after</label>
-            <div class="input-group">
-              <input name="trashcanClean" id="trashcanClean" class="form-control text-right" type="number" ng-model="currentFolder._guiVersioning.trashcanClean" required="" aria-required="true" min="0" />
-              <div class="input-group-addon" translate>days</div>
-            </div>
-            <p class="help-block">
-              <span translate ng-if="folderEditor.trashcanClean.$valid || folderEditor.trashcanClean.$pristine">The number of days to keep files in the trash can. Zero means forever.</span>
-              <span translate ng-if="folderEditor.trashcanClean.$error.required && folderEditor.trashcanClean.$dirty">The number of days must be a number and cannot be blank.</span>
-              <span translate ng-if="folderEditor.trashcanClean.$error.min && folderEditor.trashcanClean.$dirty">A negative number of days doesn't make sense.</span>
-            </p>
-          </div>
-          <div class="form-group" ng-if="currentFolder._guiVersioning.selector=='simple'" ng-class="{'has-error': folderEditor.simpleKeep.$invalid && folderEditor.simpleKeep.$dirty}">
-            <p translate class="help-block">Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.</p>
-            <label translate for="simpleKeep">Keep Versions</label>
-            <input name="simpleKeep" id="simpleKeep" class="form-control" type="number" ng-model="currentFolder._guiVersioning.simpleKeep" required="" aria-required="true" min="1" />
-            <p class="help-block">
-              <span translate ng-if="folderEditor.simpleKeep.$valid || folderEditor.simpleKeep.$pristine">The number of old versions to keep, per file.</span>
-              <span translate ng-if="folderEditor.simpleKeep.$error.required && folderEditor.simpleKeep.$dirty">The number of versions must be a number and cannot be blank.</span>
-              <span translate ng-if="folderEditor.simpleKeep.$error.min && folderEditor.simpleKeep.$dirty">You must keep at least one version.</span>
-            </p>
-          </div>
-          <div class="form-group" ng-if="currentFolder._guiVersioning.selector=='staggered'" ng-class="{'has-error': folderEditor.staggeredMaxAge.$invalid && folderEditor.staggeredMaxAge.$dirty}">
-            <p class="help-block"><span translate>Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.</span> <span translate>Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.</span></p>
-            <p translate class="help-block">The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.</p>
-            <label translate for="staggeredMaxAge">Maximum Age</label>
-            <input name="staggeredMaxAge" id="staggeredMaxAge" class="form-control" type="number" ng-model="currentFolder._guiVersioning.staggeredMaxAge" required="" aria-required="true" min="0" />
-            <p class="help-block">
-              <span translate ng-if="folderEditor.staggeredMaxAge.$valid || folderEditor.staggeredMaxAge.$pristine">The maximum time to keep a version (in days, set to 0 to keep versions forever).</span>
-              <span translate ng-if="folderEditor.staggeredMaxAge.$error.required && folderEditor.staggeredMaxAge.$dirty">The maximum age must be a number and cannot be blank.</span>
-              <span translate ng-if="folderEditor.staggeredMaxAge.$error.min && folderEditor.staggeredMaxAge.$dirty">A negative number of days doesn't make sense.</span>
-            </p>
-          </div>
-          <div class="form-group" ng-if="internalVersioningEnabled()">
-            <label translate for="fsPath">Versions Path</label>
-            <input name="fsPath" id="fsPath" class="form-control" type="text" ng-model="currentFolder.versioning.fsPath" />
-            <p translate class="help-block">Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).</p>
-          </div>
-          <div class="form-group" ng-if="currentFolder._guiVersioning.selector=='external'" ng-class="{'has-error': folderEditor.externalCommand.$invalid && folderEditor.externalCommand.$dirty}">
-            <p translate class="help-block">An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.</p>
-            <label translate for="externalCommand">Command</label>
-            <textarea name="externalCommand" id="externalCommand" class="form-control" rows="1" ng-model="currentFolder._guiVersioning.externalCommand" required="" aria-required="true" />
-            <p class="help-block">
-              <span translate ng-if="folderEditor.externalCommand.$valid || folderEditor.externalCommand.$pristine">See external versioning help for supported templated command line parameters.</span>
-              <span translate ng-if="folderEditor.externalCommand.$error.required && folderEditor.externalCommand.$dirty">The path cannot be blank.</span>
-            </p>
-          </div>
-          <div class="form-group" ng-if="internalVersioningEnabled()" ng-class="{'has-error': folderEditor.cleanupIntervalS.$invalid && folderEditor.cleanupIntervalS.$dirty}">
-            <label translate for="cleanupIntervalS">Cleanup Interval</label>
-            <div class="input-group">
-              <input name="cleanupIntervalS" id="cleanupIntervalS" class="form-control text-right" type="number" ng-model="currentFolder._guiVersioning.cleanupIntervalS" required="" min="0" max="31536000" aria-required="true" />
-              <div class="input-group-addon" translate>seconds</div>
-            </div>
-            <p class="help-block">
-              <span translate ng-if="folderEditor.cleanupIntervalS.$valid || folderEditor.cleanupIntervalS.$pristine"class="help-block">The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.</span>
-              <span translate ng-if="folderEditor.cleanupIntervalS.$error.required && folderEditor.cleanupIntervalS.$dirty">The cleanup interval cannot be blank.</span>
-              <span translate ng-if="folderEditor.cleanupIntervalS.$error.min && folderEditor.cleanupIntervalS.$dirty">The interval must be a positive number of seconds.</span>
-            </p>
-          </div>
-        </div>
-
-        <div ng-if="!editingDefaults" id="folder-ignores" class="tab-pane">
-          <p translate>Enter ignore patterns, one per line.</p>
-          <div ng-class="{'has-error': ignores.error != null}">
-            <textarea class="form-control" rows="5" ng-model="ignores.text" ng-disabled="ignores.disabled"></textarea>
-            <p class="help-block" ng-if="ignores.error">
-              {{ignores.error}}
-            </p>
-          </div>
-          <hr />
-          <p class="small"><span translate>Quick guide to supported patterns</span> (<a href="https://docs.syncthing.net/users/ignoring.html" target="_blank" translate>full documentation</a>):</p>
-          <dl class="dl-horizontal dl-narrow small">
-            <dt><code>(?d)</code></dt>
-            <dd><b><span translate>Prefix indicating that the file can be deleted if preventing directory removal</span></b></dd>
-            <dt><code>(?i)</code></dt>
-            <dd><span translate>Prefix indicating that the pattern should be matched without case sensitivity</span></dd>
-            <dt><code>!</code></dt>
-            <dd><span translate>Inversion of the given condition (i.e. do not exclude)</span></dd>
-            <dt><code>*</code></dt>
-            <dd><span translate>Single level wildcard (matches within a directory only)</span></dd>
-            <dt><code>**</code></dt>
-            <dd><span translate>Multi level wildcard (matches multiple directory levels)</span></dd>
-            <dt><code>//</code></dt>
-            <dd><span translate>Comment, when used at the start of a line</span></dd>
-          </dl>
-          <hr />
-          <span translate ng-show="editingExisting" translate-value-path="{{currentFolder.path}}{{system.pathSeparator}}.stignore">Editing {%path%}.</span>
-          <span translate ng-show="!editingExisting" translate-value-path="{{currentFolder.path}}{{system.pathSeparator}}.stignore">Creating ignore patterns, overwriting an existing file at {%path%}.</span>
-        </div>
-
-        <div id="folder-advanced" class="tab-pane">
-          <div class="row form-group" ng-class="{'has-error': folderEditor.rescanIntervalS.$invalid && folderEditor.rescanIntervalS.$dirty}">
-            <div class="col-md-12">
-              <label translate>Scanning</label>
-              &nbsp;<a href="https://docs.syncthing.net/users/syncing.html#scanning" target="_blank"><span class="fas fa-question-circle"></span>&nbsp;<span translate>Help</span></a></br>
-
-              <div class="row">
-                <div class="col-md-6">
-                  <label>
-                    <input type="checkbox" ng-model="currentFolder.fsWatcherEnabled" ng-change="setFSWatcherIntervalDefault()" tooltip data-original-title="{{'Use notifications from the filesystem to detect changed items.' | translate }}">&nbsp;<span translate>Watch for Changes</span>
-                  </label>
-                  <p translate class="help-block">Watching for changes discovers most changes without periodic scanning.</p>
-                </div>
-                <div class="col-md-6">
-                  <label for="rescanIntervalS" translate>Full Rescan Interval (s)</label>
-                  <input name="rescanIntervalS" id="rescanIntervalS" class="form-control" type="number" ng-model="currentFolder.rescanIntervalS" required="" aria-required="true" min="0" />
-                  <p class="help-block" ng-if="!folderEditor.rescanIntervalS.$valid && folderEditor.rescanIntervalS.$dirty" translate>
-                    The rescan interval must be a non-negative number of seconds.
-                  </p>
-                </div>
-              </div>
-            </div>
-          </div>
-
-          <div class="row">
-            <div class="col-md-6 form-group">
-              <label translate>Folder Type</label>
-              &nbsp;<a href="https://docs.syncthing.net/users/foldertypes.html" target="_blank"><span class="fas fa-question-circle"></span>&nbsp;<span translate>Help</span></a>
-              <select class="form-control" ng-change="setDefaultsForFolderType()" ng-model="currentFolder.type" ng-disabled="editingExisting && currentFolder.type == 'receiveencrypted'">
-                <option value="sendreceive" translate>Send &amp; Receive</option>
-                <option value="sendonly" translate>Send Only</option>
-                <option value="receiveonly" translate>Receive Only</option>
-                <option value="receiveencrypted" ng-disabled="editingExisting" translate>Receive Encrypted</option>
-              </select>
-              <p ng-if="currentFolder.type == 'sendonly'" translate class="help-block">Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.</p>
-              <p ng-if="currentFolder.type == 'receiveonly'" translate class="help-block">Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.</p>
-              <p ng-if="currentFolder.type == 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type "{%receiveEncrypted%}" too.</p>
-              <p ng-if="editingExisting && currentFolder.type == 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Folder type "{%receiveEncrypted%}" cannot be changed after adding the folder. You need to remove the folder, delete or decrypt the data on disk, and add the folder again.</p>
-              <p ng-if="editingExisting && currentFolder.type != 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Folder type "{%receiveEncrypted%}" can only be set when adding a new folder.</p>
-            </div>
-            <div class="col-md-6 form-group">
-              <label translate>File Pull Order</label>
-              <select class="form-control" ng-model="currentFolder.order" ng-if="currentFolder.type != 'sendonly'">
-                <option value="random" translate>Random</option>
-                <option value="alphabetic" translate>Alphabetic</option>
-                <option value="smallestFirst" translate>Smallest First</option>
-                <option value="largestFirst" translate>Largest First</option>
-                <option value="oldestFirst" translate>Oldest First</option>
-                <option value="newestFirst" translate>Newest First</option>
-              </select>
-              <select class="form-control" ng-if="currentFolder.type == 'sendonly'" disabled>
-                <option value="disabled" translate>Disabled</option>
-              </select>
-            </div>
-          </div>
-
-          <div class="row">
-            <div class="col-md-6 form-group" ng-class="{'has-error': folderEditor.minDiskFree.$invalid && folderEditor.minDiskFree.$dirty}">
-              <label for="minDiskFree" translate>Minimum Free Disk Space</label><br />
-              <div class="row">
-                <div class="col-xs-9">
-                  <input name="minDiskFree" id="minDiskFree" class="form-control" type="number" ng-model="currentFolder.minDiskFree.value" required="" aria-required="true" min="0" step="0.01" />
-                </div>
-                <div class="col-xs-3">
-                  <select class="form-control" ng-model="currentFolder.minDiskFree.unit">
-                    <option value="%">%</option>
-                    <option value="kB">kB</option>
-                    <option value="MB">MB</option>
-                    <option value="GB">GB</option>
-                    <option value="TB">TB</option>
-                  </select>
-                </div>
-              </div>
-              <p class="help-block" ng-show="folderEditor.minDiskFree.$invalid" translate>
-                Enter a non-negative number (e.g., "2.35") and select a unit. Percentages are as part of the total disk size.
-              </p>
-            </div>
-            <div class="col-md-6 form-group">
-              <label>
-                <input type="checkbox" ng-disabled="currentFolder._recvEnc" ng-model="currentFolder.ignorePerms" /> <span translate>Ignore Permissions</span>
-              </label>
-              <p translate class="help-block">
-                Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).
-              </p>
-            </div>
-          </div>
-        </div>
-      </div>
-    </form>
-  </div>
-  <div class="modal-footer">
-    <button type="button" class="btn btn-primary btn-sm" ng-click="saveFolder()" ng-disabled="folderEditor.$invalid">
-      <span class="fas fa-check"></span>&nbsp;<span translate>Save</span>
-    </button>
-    <button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
-      <span class="fas fa-times"></span>&nbsp;<span translate>Close</span>
-    </button>
-    <button type="button" class="btn btn-warning pull-left btn-sm" data-toggle="modal" data-target="#remove-folder-confirmation" ng-if="editingExisting && !editingDefaults">
-      <span class="fas fa-minus-circle"></span>&nbsp;<span translate>Remove</span>
-    </button>
-  </div>
-</modal>

+ 0 - 35
gui/default/untrusted/syncthing/transfer/localChangedFilesModalView.html

@@ -1,35 +0,0 @@
-<modal id="localChanged" status="{{localChangedType === 'receiveencrypted' ? 'warning' : 'info'}}" icon="fas fa-exclamation-circle" heading="{{localChangedHeading(localChangedType)}}" large="yes" closeable="yes">
-  <div class="modal-body" ng-switch="localChangedType">
-    <p ng-switch-when="receiveonly" translate>
-      The following items were changed locally.
-    </p>
-    <p ng-switch-when="receiveencrypted">
-      <span translate>The following unexpected items were found.</span>
-      <span translate translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">You should never add or change anything locally in a "{%receiveEncrypted%}" folder.</span>
-    </p>
-    <table class="table table-striped">
-      <thead>
-        <tr>
-          <th translate>Path</th>
-          <th translate>Size</th>
-        </tr>
-      </thead>
-      <tr dir-paginate="file in localChanged.files | itemsPerPage: localChanged.perpage" current-page="localChanged.page" total-items="model[localChangedFolder].receiveOnlyTotalItems" pagination-id="localChanged">
-        <td class="file-path">{{file.name}}</td>
-        <td><span ng-hide="file.type == 'DIRECTORY'">{{file.size | binary}}B</span></td>
-      </tr>
-    </table>
-    <dir-pagination-controls on-page-change="refreshLocalChanged(newPageNumber, localChanged.perpage)" pagination-id="localChanged"></dir-pagination-controls>
-    <ul class="pagination pull-right">
-      <li ng-repeat="option in [10, 25, 50]" ng-class="{ active: localChanged.perpage == option }">
-        <a href="#" ng-click="refreshLocalChanged(localChanged.page, option)">{{option}}</a>
-      </li>
-    </ul>
-    <div class="clearfix"></div>
-  </div>
-  <div class="modal-footer">
-    <button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
-      <span class="fas fa-times"></span>&nbsp;<span translate>Close</span>
-    </button>
-  </div>
-</modal>

+ 1 - 6
lib/api/api.go

@@ -65,7 +65,6 @@ const (
 	EventSubBufferSize    = 1000
 	defaultEventTimeout   = time.Minute
 	httpsCertLifetimeDays = 820
-	featureFlagUntrusted  = "untrusted"
 )
 
 type service struct {
@@ -105,7 +104,7 @@ func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonNam
 	return &service{
 		id:      id,
 		cfg:     cfg,
-		statics: newStaticsServer(cfg.GUI().Theme, assetDir, cfg.Options().FeatureFlag(featureFlagUntrusted)),
+		statics: newStaticsServer(cfg.GUI().Theme, assetDir),
 		model:   m,
 		eventSubs: map[events.EventType]events.BufferedSubscription{
 			DefaultEventMask: defaultSub,
@@ -461,10 +460,6 @@ func (s *service) CommitConfiguration(from, to config.Configuration) bool {
 	// No action required when this changes, so mask the fact that it changed at all.
 	from.GUI.Debugging = to.GUI.Debugging
 
-	if untrusted := to.Options.FeatureFlag(featureFlagUntrusted); untrusted != from.Options.FeatureFlag(featureFlagUntrusted) {
-		s.statics.setUntrusted(untrusted)
-	}
-
 	if to.GUI == from.GUI {
 		// No GUI changes, we're done here.
 		return true

+ 1 - 31
lib/api/api_statics.go

@@ -30,17 +30,15 @@ type staticsServer struct {
 	mut             sync.RWMutex
 	theme           string
 	lastThemeChange time.Time
-	untrusted       bool
 }
 
-func newStaticsServer(theme, assetDir string, untrusted bool) *staticsServer {
+func newStaticsServer(theme, assetDir string) *staticsServer {
 	s := &staticsServer{
 		assetDir:        assetDir,
 		assets:          auto.Assets(),
 		mut:             sync.NewRWMutex(),
 		theme:           theme,
 		lastThemeChange: time.Now().UTC(),
-		untrusted:       untrusted,
 	}
 
 	seen := make(map[string]struct{})
@@ -62,10 +60,6 @@ func newStaticsServer(theme, assetDir string, untrusted bool) *staticsServer {
 		}
 	}
 
-	if untrusted {
-		l.Infoln(`Feature flag "untrusted":`, untrusted)
-	}
-
 	return s
 }
 
@@ -94,7 +88,6 @@ func (s *staticsServer) serveAsset(w http.ResponseWriter, r *http.Request) {
 	s.mut.RLock()
 	theme := s.theme
 	modificationTime := s.lastThemeChange
-	untrusted := s.untrusted
 	s.mut.RUnlock()
 
 	// If path starts with special prefix, get theme and file from path
@@ -112,37 +105,21 @@ func (s *staticsServer) serveAsset(w http.ResponseWriter, r *http.Request) {
 	}
 
 	// Check for an override for the current theme.
-	if untrusted && s.serveFromAssetDir(file, theme+"/untrusted", w, r) {
-		l.Debugln("serving", file, "from override untrusted")
-		return
-	}
 	if s.serveFromAssetDir(file, theme, w, r) {
 		return
 	}
 
 	// Check for a compiled in asset for the current theme.
-	if untrusted && s.serveFromAssets(file, theme+"/untrusted", modificationTime, w, r) {
-		l.Debugln("serving", file, "from compiled untrusted")
-		return
-	}
 	if s.serveFromAssets(file, theme, modificationTime, w, r) {
 		return
 	}
 
 	// Check for an overridden default asset.
-	if untrusted && s.serveFromAssetDir(file, config.DefaultTheme+"/untrusted", w, r) {
-		l.Debugln("serving", file, "from override untrusted")
-		return
-	}
 	if s.serveFromAssetDir(file, config.DefaultTheme, w, r) {
 		return
 	}
 
 	// Check for a compiled in default asset.
-	if untrusted && s.serveFromAssets(file, config.DefaultTheme+"/untrusted", modificationTime, w, r) {
-		l.Debugln("serving", file, "from compiled untrusted")
-		return
-	}
 	if s.serveFromAssets(file, config.DefaultTheme, modificationTime, w, r) {
 		return
 	}
@@ -189,13 +166,6 @@ func (s *staticsServer) setTheme(theme string) {
 	s.mut.Unlock()
 }
 
-func (s *staticsServer) setUntrusted(enabled bool) {
-	s.mut.Lock()
-	l.Infoln(`Feature flag "untrusted":`, enabled)
-	s.untrusted = enabled
-	s.mut.Unlock()
-}
-
 func (s *staticsServer) String() string {
 	return fmt.Sprintf("staticsServer@%p", s)
 }