feat(gui, config): support simple folder grouping (fixes #2070) (#10563)

Adds a very simple way to group up folders to help with organising from
the GUI. 

Signed-off-by: Ben Norcombe <bennorcombe@pm.me>
This commit is contained in:
Ben Norcombe
2026-04-07 16:05:41 +01:00
committed by GitHub
parent 5d877f65f5
commit 6a26d56ad9
6 changed files with 517 additions and 460 deletions
+470 -460
View File
@@ -381,264 +381,269 @@
<div class="col-md-6" aria-labelledby="folder_list" role="region" >
<h3 id="folder_list"><span translate>Folders</span><span ng-if="folderList().length > 1"> ({{folderList().length}})</span></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 ng-repeat="(folderGroupName, groupedFolders) in foldersGrouped">
<h4 ng-if="folderGroupName !== ''">{{ folderGroupName }}
<span ng-if="groupedFolders.length > 1 && folderGroupName.length > 0"> ({{groupedFolders.length}})</span>
</h4>
<div class="panel-group" id="folders-{{ $index }}">
<div class="panel panel-default" ng-repeat="folder in groupedFolders">
<button class="btn panel-heading" data-toggle="collapse" data-parent="#folders-{{$index}}" data-target="#folder-{{$parent.$index}}-{{$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 class="hidden-xs">{{folderStatusText(folder)}}</span>
<span ng-switch-when="scanning" ng-if="scanPercentage(folder.id) != undefined">({{scanPercentage(folder.id) | percent}})</span>
<span ng-switch-when="syncing">({{syncPercentage(folder.id) | percent}}, {{model[folder.id].needBytes | binary}}B)</span>
<span class="inline-icon">
<span class="visible-xs fa fa-fw {{folderStatusIcon(folder)}}" aria-label="{{folderStatusText(folder)}}"></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-{{$parent.$index}}-{{$index}}" class="panel-collapse collapse">
<div class="panel-body less-padding">
<table class="table table-condensed table-striped table-auto">
<tbody>
<tr class="visible-xs">
<th><span class="fa fa-fw {{folderStatusIcon(folder)}}"></span>&nbsp;<span translate>Folder Status</span></th>
<td class="text-right">{{folderStatusText(folder)}}</td>
</tr>
<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">
<span tooltip data-original-title="{{model[folder.id].invalid || model[folder.id].error}}">{{model[folder.id].invalid || model[folder.id].error}}</span>
</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">
<div>
<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
</span>
</div>
<div ng-if="model[folder.id].ignorePatterns">
<a href="" ng-click="editFolderExisting(folder, '#folder-ignores')"><i class="small" translate>Reduced by ignore patterns</i></a>
</div>
<div ng-if="folder.ignoreDelete">
<i class="small">
<span translate>Altered by ignoring deletes.</span>
<a href="{{docsURL('advanced/folder-ignoredelete')}}" target="_blank">
<span class="fas fa-question-circle"></span>&nbsp;<span translate>Help</span>
</a>
</i>
</div>
</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>
<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 == 'sendreceive'" translate>Send &amp; Receive</span>
<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 set up, 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 set up, retrying</span>
</span>
</div>
</td>
</tr>
<tr ng-if="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="fa fa-fw fa-files-o"></span>&nbsp;<span translate>File Versioning</span></th>
<td class="text-right">
<span ng-switch="folder.versioning.type">
<span ng-switch-when="trashcan" translate>Trash Can</span>
<span ng-switch-when="simple" translate>Simple</span>
<span ng-switch-when="staggered" translate>Staggered</span>
<span ng-switch-when="external" tooltip data-original-title="{{folder.versioning.params.command}}" translate>External</span>
</span>
<span ng-if="folder.versioning.type != 'external'">
<span ng-if="(folder.versioning.type == 'trashcan' || folder.versioning.type == 'simple')" tooltip data-original-title="{{'Clean out after' | translate}}">
&ensp;<span class="fa fa-calendar"></span>&nbsp;<span ng-if="folder.versioning.params.cleanoutDays == 0" translate>Disabled</span><span ng-if="folder.versioning.params.cleanoutDays > 0">{{folder.versioning.params.cleanoutDays * 86400 | duration:"d"}}</span>
</span>
<span ng-if="folder.versioning.type == 'simple'" tooltip data-original-title="{{'Keep Versions' | translate}}">
&ensp;<span class="fa fa-file-archive-o"></span>&nbsp;{{folder.versioning.params.keep}}
</span>
<span ng-if="folder.versioning.type == 'staggered'" tooltip data-original-title="{{'Maximum Age' | translate}}">
&ensp;<span class="fa fa-calendar"></span>&nbsp;<span ng-if="folder.versioning.params.maxAge == 0" translate>Forever</span><span ng-if="folder.versioning.params.maxAge > 0">{{folder.versioning.params.maxAge | duration}}</span>
</span>
<span tooltip data-original-title="{{'Cleanup Interval' | translate}}">
&ensp;<span class="fa fa-recycle"></span>&nbsp;<span ng-if="folder.versioning.cleanupIntervalS == 0" translate>Disabled</span><span ng-if="folder.versioning.cleanupIntervalS > 0">{{folder.versioning.cleanupIntervalS | duration}}</span>
</span>
<!-- Keep the path last, so that it truncates without pushing other information out of the screen. -->
<span tooltip data-original-title="{{folder.versioning.fsPath === '' ? '.stversions' : folder.versioning.fsPath}}">
&ensp;<span class="fa fa-folder-open-o"></span>&nbsp;{{folder.versioning.fsPath === '' ? '.stversions' : folder.versioning.fsPath}}
</span>
</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 no-overflow-ellipse overflow-break-word">
<span ng-repeat="device in otherDevices(folder.devices)">
<span ng-if="folder.type !== 'receiveencrypted' && device.encryptionPassword" class="text-nowrap">
<span class="fa fa-lock"></span>&nbsp;<!-- Avoid stray space...
--></span><!-- Avoid stray space...
--><span ng-switch="completion[device.deviceID][folder.id].remoteState"><!-- Avoid stray space...
--><a ng-switch-when="notSharing" href="" ng-click="editDeviceExisting(devices[device.deviceID])" data-original-title="{{'The remote device has not accepted sharing this folder.' | translate}}" tooltip>{{deviceName(devices[device.deviceID])}}<sup>1</sup></a><!-- Avoid stray space...
--><a ng-switch-when="paused" href="" ng-click="editDeviceExisting(devices[device.deviceID])" data-original-title="{{'The remote device has paused this folder.' | translate}}" tooltip>{{deviceName(devices[device.deviceID])}}<sup>2</sup></a><!-- Avoid stray space...
--><a ng-switch-default href="" ng-click="editDeviceExisting(devices[device.deviceID])">{{deviceName(devices[device.deviceID])}}</a><!-- Avoid stray space...
--><span ng-if="!$last">,</span>
</span>
</span>
</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 translate-value-file="{{folderStats[folder.id].lastFile.filename | basename}}" ng-if="!folderStats[folder.id].lastFile.deleted">Updated {%file%}</span>
<span translate translate-value-file="{{folderStats[folder.id].lastFile.filename | basename}}" ng-if="folderStats[folder.id].lastFile.deleted">Deleted {%file%}</span>
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="panel-status pull-right text-{{folderClass(folder)}}" ng-switch="folderStatus(folder)">
<span class="hidden-xs">{{folderStatusText(folder)}}</span>
<span ng-switch-when="scanning" ng-if="scanPercentage(folder.id) != undefined">({{scanPercentage(folder.id) | percent}})</span>
<span ng-switch-when="syncing">({{syncPercentage(folder.id) | percent}}, {{model[folder.id].needBytes | binary}}B)</span>
<span class="inline-icon">
<span class="visible-xs fa fa-fw {{folderStatusIcon(folder)}}" aria-label="{{folderStatusText(folder)}}"></span>
<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) && ['outofsync', 'faileditems', 'localadditions'].indexOf(folderStatus(folder)) >= 0">
<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) && ['outofsync', 'faileditems', 'localunencrypted'].indexOf(folderStatus(folder)) >= 0">
<span class="fa fa-minus-circle"></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'" ng-disabled="folder.paused">
<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 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 less-padding">
<table class="table table-condensed table-striped table-auto">
<tbody>
<tr class="visible-xs">
<th><span class="fa fa-fw {{folderStatusIcon(folder)}}"></span>&nbsp;<span translate>Folder Status</span></th>
<td class="text-right">{{folderStatusText(folder)}}</td>
</tr>
<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">
<span tooltip data-original-title="{{model[folder.id].invalid || model[folder.id].error}}">{{model[folder.id].invalid || model[folder.id].error}}</span>
</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">
<div>
<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
</span>
</div>
<div ng-if="model[folder.id].ignorePatterns">
<a href="" ng-click="editFolderExisting(folder, '#folder-ignores')"><i class="small" translate>Reduced by ignore patterns</i></a>
</div>
<div ng-if="folder.ignoreDelete">
<i class="small">
<span translate>Altered by ignoring deletes.</span>
<a href="{{docsURL('advanced/folder-ignoredelete')}}" target="_blank">
<span class="fas fa-question-circle"></span>&nbsp;<span translate>Help</span>
</a>
</i>
</div>
</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>
<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 == 'sendreceive'" translate>Send &amp; Receive</span>
<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 set up, 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 set up, retrying</span>
</span>
</div>
</td>
</tr>
<tr ng-if="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="fa fa-fw fa-files-o"></span>&nbsp;<span translate>File Versioning</span></th>
<td class="text-right">
<span ng-switch="folder.versioning.type">
<span ng-switch-when="trashcan" translate>Trash Can</span>
<span ng-switch-when="simple" translate>Simple</span>
<span ng-switch-when="staggered" translate>Staggered</span>
<span ng-switch-when="external" tooltip data-original-title="{{folder.versioning.params.command}}" translate>External</span>
</span>
<span ng-if="folder.versioning.type != 'external'">
<span ng-if="(folder.versioning.type == 'trashcan' || folder.versioning.type == 'simple')" tooltip data-original-title="{{'Clean out after' | translate}}">
&ensp;<span class="fa fa-calendar"></span>&nbsp;<span ng-if="folder.versioning.params.cleanoutDays == 0" translate>Disabled</span><span ng-if="folder.versioning.params.cleanoutDays > 0">{{folder.versioning.params.cleanoutDays * 86400 | duration:"d"}}</span>
</span>
<span ng-if="folder.versioning.type == 'simple'" tooltip data-original-title="{{'Keep Versions' | translate}}">
&ensp;<span class="fa fa-file-archive-o"></span>&nbsp;{{folder.versioning.params.keep}}
</span>
<span ng-if="folder.versioning.type == 'staggered'" tooltip data-original-title="{{'Maximum Age' | translate}}">
&ensp;<span class="fa fa-calendar"></span>&nbsp;<span ng-if="folder.versioning.params.maxAge == 0" translate>Forever</span><span ng-if="folder.versioning.params.maxAge > 0">{{folder.versioning.params.maxAge | duration}}</span>
</span>
<span tooltip data-original-title="{{'Cleanup Interval' | translate}}">
&ensp;<span class="fa fa-recycle"></span>&nbsp;<span ng-if="folder.versioning.cleanupIntervalS == 0" translate>Disabled</span><span ng-if="folder.versioning.cleanupIntervalS > 0">{{folder.versioning.cleanupIntervalS | duration}}</span>
</span>
<!-- Keep the path last, so that it truncates without pushing other information out of the screen. -->
<span tooltip data-original-title="{{folder.versioning.fsPath === '' ? '.stversions' : folder.versioning.fsPath}}">
&ensp;<span class="fa fa-folder-open-o"></span>&nbsp;{{folder.versioning.fsPath === '' ? '.stversions' : folder.versioning.fsPath}}
</span>
</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 no-overflow-ellipse overflow-break-word">
<span ng-repeat="device in otherDevices(folder.devices)">
<span ng-if="folder.type !== 'receiveencrypted' && device.encryptionPassword" class="text-nowrap">
<span class="fa fa-lock"></span>&nbsp;<!-- Avoid stray space...
--></span><!-- Avoid stray space...
--><span ng-switch="completion[device.deviceID][folder.id].remoteState"><!-- Avoid stray space...
--><a ng-switch-when="notSharing" href="" ng-click="editDeviceExisting(devices[device.deviceID])" data-original-title="{{'The remote device has not accepted sharing this folder.' | translate}}" tooltip>{{deviceName(devices[device.deviceID])}}<sup>1</sup></a><!-- Avoid stray space...
--><a ng-switch-when="paused" href="" ng-click="editDeviceExisting(devices[device.deviceID])" data-original-title="{{'The remote device has paused this folder.' | translate}}" tooltip>{{deviceName(devices[device.deviceID])}}<sup>2</sup></a><!-- Avoid stray space...
--><a ng-switch-default href="" ng-click="editDeviceExisting(devices[device.deviceID])">{{deviceName(devices[device.deviceID])}}</a><!-- Avoid stray space...
--><span ng-if="!$last">,</span>
</span>
</span>
</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 translate-value-file="{{folderStats[folder.id].lastFile.filename | basename}}" ng-if="!folderStats[folder.id].lastFile.deleted">Updated {%file%}</span>
<span translate translate-value-file="{{folderStats[folder.id].lastFile.filename | basename}}" ng-if="folderStats[folder.id].lastFile.deleted">Deleted {%file%}</span>
</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) && ['outofsync', 'faileditems', 'localadditions'].indexOf(folderStatus(folder)) >= 0">
<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) && ['outofsync', 'faileditems', 'localunencrypted'].indexOf(folderStatus(folder)) >= 0">
<span class="fa fa-minus-circle"></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'" ng-disabled="folder.paused">
<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>
@@ -770,215 +775,220 @@
<!-- Remote devices -->
<h3><span translate>Remote Devices</span> <span ng-if="otherDevices().length > 1"> ({{otherDevices().length}})</span></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>
<div class="panel-status pull-right text-{{deviceClass(deviceCfg)}}" ng-switch="deviceStatus(deviceCfg)">
<span class="hidden-xs">{{deviceStatusText(deviceCfg)}}</span>
<span ng-switch-when="syncing">({{completion[deviceCfg.deviceID]._total | percent}}, {{completion[deviceCfg.deviceID]._needBytes | binary}}B)</span>
<span class="inline-icon">
<span class="visible-xs fa fa-fw {{deviceStatusIcon(deviceCfg)}}" aria-label="{{deviceStatusText(deviceCfg)}}"></span>
</span>
<span class="inline-icon">
<span ng-class="rdConnTypeIcon(rdConnType(deviceCfg.deviceID))" class="reception reception-theme"></span>
</span>
<div ng-repeat="(deviceGroupName, groupedDevices) in devicesGrouped">
<h4>{{ deviceGroupName }}
<span ng-if="groupedDevices.length > 1 && deviceGroupName.length > 0"> ({{groupedDevices.length}})</span>
</h4>
<div class="panel-group" id="devices-{{ $index }}">
<div class="panel panel-default" ng-repeat="deviceCfg in groupedDevices">
<button class="btn panel-heading" data-toggle="collapse" data-parent="#devices-{{ $parent.$index }}" data-target="#device-{{$parent.$index}}-{{$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>
<div class="panel-status pull-right text-{{deviceClass(deviceCfg)}}" ng-switch="deviceStatus(deviceCfg)">
<span class="hidden-xs">{{deviceStatusText(deviceCfg)}}</span>
<span ng-switch-when="syncing">({{completion[deviceCfg.deviceID]._total | percent}}, {{completion[deviceCfg.deviceID]._needBytes | binary}}B)</span>
<span class="inline-icon">
<span class="visible-xs fa fa-fw {{deviceStatusIcon(deviceCfg)}}" aria-label="{{deviceStatusText(deviceCfg)}}"></span>
</span>
<span class="inline-icon">
<span ng-class="rdConnTypeIcon(rdConnType(deviceCfg.deviceID))" class="reception reception-theme"></span>
</span>
</div>
<div class="panel-title-text">{{deviceName(deviceCfg)}}</div>
</h4>
</button>
<div id="device-{{$parent.$index}}-{{$index}}" class="panel-collapse collapse">
<div class="panel-body less-padding">
<table class="table table-condensed table-striped table-auto">
<tbody>
<tr class="visible-xs">
<th><span class="fa fa-fw {{deviceStatusIcon(deviceCfg)}}"></span>&nbsp;<span translate>Device Status</span></th>
<td class="text-right">{{deviceStatusText(deviceCfg)}}</td>
</tr>
<tr ng-if="!connections[deviceCfg.deviceID].connected">
<th><span class="fas fa-fw fa-eye"></span>&nbsp;<span translate>Last seen</span></th>
<td class="text-right">
<div ng-if="!deviceStats[deviceCfg.deviceID].lastSeenDays" translate>
Never
</div>
<div ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays">
<div>
{{deviceStats[deviceCfg.deviceID].lastSeen | date:"yyyy-MM-dd HH:mm:ss"}}
</div>
<div ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays >= 7">
<i ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays < 30" translate>More than a week ago</i>
<i class="text-warning" ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays >= 30 && deviceStats[deviceCfg.deviceID].lastSeenDays < 365" translate>More than a month ago</i>
<i class="text-danger" ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays >= 365" translate>More than a year ago</i>
</div>
</div>
</td>
</tr>
<tr ng-if="!connections[deviceCfg.deviceID].connected && deviceFolders(deviceCfg).length > 0">
<th><span class="fas fa-fw fa-cloud"></span>&nbsp;<span translate>Sync Status</span></th>
<td translate ng-if="completion[deviceCfg.deviceID]._total == 100" class="text-right">Up to Date</td>
<td ng-if="completion[deviceCfg.deviceID]._total < 100" class="text-right">
<span class="hidden-xs" translate>Out of Sync</span> ({{completion[deviceCfg.deviceID]._total | percent}})
</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="completion[deviceCfg.deviceID]._needItems">
<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 && !deviceCfg.paused" 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 && !deviceCfg.paused" 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">
<th><span class="reception reception-4 reception-theme"></span>&nbsp;<span translate>Connection Type</span></th>
<td class="text-right">
<span tooltip data-original-title="{{rdConnDetails(rdConnType(deviceCfg.deviceID))}}">
{{rdConnTypeString(rdConnType(deviceCfg.deviceID))}}
</span>
</td>
</tr>
<tr ng-if="connections[deviceCfg.deviceID].connected">
<th><span class="fas fa-fw fa-random"></span>&nbsp;<span translate>Number of Connections</span></th>
<td class="text-right">
<span ng-if="connections[deviceCfg.deviceID].secondary.length">1 + {{connections[deviceCfg.deviceID].secondary.length | alwaysNumber}}</span>
<span ng-if="!connections[deviceCfg.deviceID].secondary.length">1</span>
</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>
<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 == 'metadata'" translate>Metadata Only</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]) || deviceShortID(deviceCfg.introducedBy) }}</td>
</tr>
<tr ng-if="deviceCfg.autoAcceptFolders">
<th><span class="fa fa-fw fa-level-down"></span>&nbsp;<span translate>Auto Accept</span></th>
<td translate class="text-right">Yes</td>
</tr>
<tr>
<th><span class="fas fa-fw fa-qrcode"></span>&nbsp;<span translate>Identification</span></th>
<td class="text-right">
<span tooltip data-original-title="{{'Click to see full identification string and QR code.' | translate}}">
<a href="" ng-click="showDeviceIdentification(deviceCfg)">{{deviceShortID(deviceCfg.deviceID)}}</a>
</span>
</td>
</tr>
<tr ng-if="deviceCfg.untrusted">
<th><span class="fa fa-fw fa-user-secret"></span>&nbsp;<span translate>Untrusted</span></th>
<td translate class="text-right">Yes</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 no-overflow-ellipse overflow-break-word">
<span ng-repeat="folderID in deviceFolders(deviceCfg)">
<span ng-if="folderIsSharedEncrypted(folderID, deviceCfg.deviceID)" class="text-nowrap">
<span class="fa fa-lock"></span>&nbsp;<!-- Avoid stray space...
--></span><!-- Avoid stray space...
--><span ng-switch="completion[deviceCfg.deviceID][folderID].remoteState"><!-- Avoid stray space...
--><span ng-switch-when="notSharing" data-original-title="{{'The remote device has not accepted sharing this folder.' | translate}}" tooltip>{{folderLabel(folderID)}}<sup>1</sup></span><!-- Avoid stray space...
--><span ng-switch-when="paused" data-original-title="{{'The remote device has paused this folder.' | translate}}" tooltip>{{folderLabel(folderID)}}<sup>2</sup></span><!-- Avoid stray space...
--><span ng-switch-default>{{folderLabel(folderID)}}</span><!-- Avoid stray space...
--><span ng-if="!$last">,</span>
</span>
</span>
</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 class="panel-title-text">{{deviceName(deviceCfg)}}</div>
</h4>
</button>
<div id="device-{{$index}}" class="panel-collapse collapse">
<div class="panel-body less-padding">
<table class="table table-condensed table-striped table-auto">
<tbody>
<tr class="visible-xs">
<th><span class="fa fa-fw {{deviceStatusIcon(deviceCfg)}}"></span>&nbsp;<span translate>Device Status</span></th>
<td class="text-right">{{deviceStatusText(deviceCfg)}}</td>
</tr>
<tr ng-if="!connections[deviceCfg.deviceID].connected">
<th><span class="fas fa-fw fa-eye"></span>&nbsp;<span translate>Last seen</span></th>
<td class="text-right">
<div ng-if="!deviceStats[deviceCfg.deviceID].lastSeenDays" translate>
Never
</div>
<div ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays">
<div>
{{deviceStats[deviceCfg.deviceID].lastSeen | date:"yyyy-MM-dd HH:mm:ss"}}
</div>
<div ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays >= 7">
<i ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays < 30" translate>More than a week ago</i>
<i class="text-warning" ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays >= 30 && deviceStats[deviceCfg.deviceID].lastSeenDays < 365" translate>More than a month ago</i>
<i class="text-danger" ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays >= 365" translate>More than a year ago</i>
</div>
</div>
</td>
</tr>
<tr ng-if="!connections[deviceCfg.deviceID].connected && deviceFolders(deviceCfg).length > 0">
<th><span class="fas fa-fw fa-cloud"></span>&nbsp;<span translate>Sync Status</span></th>
<td translate ng-if="completion[deviceCfg.deviceID]._total == 100" class="text-right">Up to Date</td>
<td ng-if="completion[deviceCfg.deviceID]._total < 100" class="text-right">
<span class="hidden-xs" translate>Out of Sync</span> ({{completion[deviceCfg.deviceID]._total | percent}})
</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="completion[deviceCfg.deviceID]._needItems">
<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 && !deviceCfg.paused" 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 && !deviceCfg.paused" 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">
<th><span class="reception reception-4 reception-theme"></span>&nbsp;<span translate>Connection Type</span></th>
<td class="text-right">
<span tooltip data-original-title="{{rdConnDetails(rdConnType(deviceCfg.deviceID))}}">
{{rdConnTypeString(rdConnType(deviceCfg.deviceID))}}
</span>
</td>
</tr>
<tr ng-if="connections[deviceCfg.deviceID].connected">
<th><span class="fas fa-fw fa-random"></span>&nbsp;<span translate>Number of Connections</span></th>
<td class="text-right">
<span ng-if="connections[deviceCfg.deviceID].secondary.length">1 + {{connections[deviceCfg.deviceID].secondary.length | alwaysNumber}}</span>
<span ng-if="!connections[deviceCfg.deviceID].secondary.length">1</span>
</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>
<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 == 'metadata'" translate>Metadata Only</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]) || deviceShortID(deviceCfg.introducedBy) }}</td>
</tr>
<tr ng-if="deviceCfg.autoAcceptFolders">
<th><span class="fa fa-fw fa-level-down"></span>&nbsp;<span translate>Auto Accept</span></th>
<td translate class="text-right">Yes</td>
</tr>
<tr>
<th><span class="fas fa-fw fa-qrcode"></span>&nbsp;<span translate>Identification</span></th>
<td class="text-right">
<span tooltip data-original-title="{{'Click to see full identification string and QR code.' | translate}}">
<a href="" ng-click="showDeviceIdentification(deviceCfg)">{{deviceShortID(deviceCfg.deviceID)}}</a>
</span>
</td>
</tr>
<tr ng-if="deviceCfg.untrusted">
<th><span class="fa fa-fw fa-user-secret"></span>&nbsp;<span translate>Untrusted</span></th>
<td translate class="text-right">Yes</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 no-overflow-ellipse overflow-break-word">
<span ng-repeat="folderID in deviceFolders(deviceCfg)">
<span ng-if="folderIsSharedEncrypted(folderID, deviceCfg.deviceID)" class="text-nowrap">
<span class="fa fa-lock"></span>&nbsp;<!-- Avoid stray space...
--></span><!-- Avoid stray space...
--><span ng-switch="completion[deviceCfg.deviceID][folderID].remoteState"><!-- Avoid stray space...
--><span ng-switch-when="notSharing" data-original-title="{{'The remote device has not accepted sharing this folder.' | translate}}" tooltip>{{folderLabel(folderID)}}<sup>1</sup></span><!-- Avoid stray space...
--><span ng-switch-when="paused" data-original-title="{{'The remote device has paused this folder.' | translate}}" tooltip>{{folderLabel(folderID)}}<sup>2</sup></span><!-- Avoid stray space...
--><span ng-switch-default>{{folderLabel(folderID)}}</span><!-- Avoid stray space...
--><span ng-if="!$last">,</span>
</span>
</span>
</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)">
@@ -81,12 +81,14 @@ angular.module('syncthing.core')
$scope.model = {};
$scope.myID = '';
$scope.devices = {};
$scope.devicesGrouped = {};
$scope.discoveryCache = {};
$scope.protocolChanged = false;
$scope.reportData = {};
$scope.reportDataPreview = '';
$scope.reportPreview = false;
$scope.folders = {};
$scope.foldersGrouped = {};
$scope.seenError = '';
$scope.upgradeInfo = null;
$scope.deviceStats = {};
@@ -559,21 +561,44 @@ angular.module('syncthing.core')
$scope.config.options._urAcceptedStr = "" + $scope.config.options.urAccepted;
$scope.devices = deviceMap($scope.config.devices);
$scope.devicesGrouped = {};
for (var id in $scope.devices) {
$scope.completion[id] = {
_total: 100,
_needBytes: 0,
_needItems: 0
};
if ($scope.devicesGrouped[$scope.devices[id].group] === undefined) {
$scope.devicesGrouped[$scope.devices[id].group] = [];
}
$scope.devicesGrouped[$scope.devices[id].group].push($scope.devices[id]);
};
$scope.folders = folderMap($scope.config.folders);
$scope.foldersGrouped = {};
Object.keys($scope.folders).forEach(function (folder) {
refreshFolder(folder);
$scope.folders[folder].devices.forEach(function (deviceCfg) {
refreshCompletion(deviceCfg.deviceID, folder);
});
if ($scope.foldersGrouped[$scope.folders[folder].group] === undefined) {
$scope.foldersGrouped[$scope.folders[folder].group] = [];
}
$scope.foldersGrouped[$scope.folders[folder].group].push($scope.folders[folder]);
});
// Sort with blank group first if any then alphabetically
const blankSort = (a, b) => {
if (a[0] === "" && b[0] !== "") return -1;
if (b[0] === "" && a[0] !== "") return 1;
return a[0].localeCompare(b[0]);
};
$scope.foldersGrouped = Object.fromEntries(Object.entries($scope.foldersGrouped).sort(blankSort));
$scope.devicesGrouped = Object.fromEntries(Object.entries($scope.devicesGrouped).sort(blankSort));
refreshNoAuthWarning();
setDefaultTheme();
@@ -57,6 +57,16 @@
<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 class="form-group" ng-class="{'has-error': deviceEditor.deviceGroup.$invalid && deviceEditor.deviceGroup.$dirty && !editingDeviceDefaults()}">
<label for="deviceGroup"><span translate>Device Group</span></label>
<input name="deviceGroup" id="deviceGroup" class="form-control" type="text" ng-model="currentDevice.group" value="{{currentDevice.group}}" list="device-group-list"/>
<datalist id="device-group-list">
<option ng-repeat="(group, device) in devicesGrouped" value="{{group}}" />
</datalist>
<p class="help-block">
<span translate ng-if="deviceEditor.deviceGroup.$valid || deviceEditor.deviceGroup.$pristine">Optional group for the device. Can be different on each device.</span>
</p>
</div>
</div>
<div ng-if="!editingDeviceDefaults()" id="device-sharing" class="tab-pane">
<div class="row">
@@ -18,6 +18,16 @@
<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 class="form-group" ng-class="{'has-error': folderEditor.folderGroup.$invalid && folderEditor.folderGroup.$dirty && !editingFolderDefaults()}">
<label for="folderGroup"><span translate>Folder Group</span></label>
<input name="folderGroup" id="folderGroup" class="form-control" type="text" ng-model="currentFolder.group" value="{{currentFolder.group}}" list="folder-group-list"/>
<datalist id="folder-group-list">
<option ng-repeat="(group, folders) in foldersGrouped" value="{{group}}" />
</datalist>
<p class="help-block">
<span translate ng-if="folderEditor.folderGroup.$valid || folderEditor.folderGroup.$pristine">Optional group for the folder. Can be different on each device.</span>
</p>
</div>
<div ng-if="!editingFolderDefaults()" 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="has(['existing', 'new-pending'], currentFolder._editing)" id="folderID" class="form-control" type="text" ng-model="currentFolder.id" required="" aria-required="true" unique-folder value="{{currentFolder.id}}" />
+1
View File
@@ -36,6 +36,7 @@ type DeviceConfiguration struct {
Untrusted bool `json:"untrusted" xml:"untrusted"`
RemoteGUIPort int `json:"remoteGUIPort" xml:"remoteGUIPort"`
RawNumConnections int `json:"numConnections" xml:"numConnections"`
Group string `json:"group" xml:"group,attr,omitempty"`
}
func (cfg DeviceConfiguration) Copy() DeviceConfiguration {
+1
View File
@@ -54,6 +54,7 @@ type FolderConfiguration struct {
Path string `json:"path" xml:"path,attr"`
Type FolderType `json:"type" xml:"type,attr"`
Devices []FolderDeviceConfiguration `json:"devices" xml:"device"`
Group string `json:"group" xml:"group,attr,omitempty" restart:"false"`
RescanIntervalS int `json:"rescanIntervalS" xml:"rescanIntervalS,attr" default:"3600"`
FSWatcherEnabled bool `json:"fsWatcherEnabled" xml:"fsWatcherEnabled,attr" default:"true"`
FSWatcherDelayS float64 `json:"fsWatcherDelayS" xml:"fsWatcherDelayS,attr" default:"10"`