support tree view for bittorrent files

This commit is contained in:
MaysWind 2018-08-14 01:10:45 +08:00
parent 26d9eafbe7
commit 4305b80c21
9 changed files with 375 additions and 40 deletions

View file

@ -372,6 +372,7 @@
<script src="scripts/directives/pieceBar.js"></script>
<script src="scripts/directives/pieceMap.js"></script>
<script src="scripts/directives/chart.js"></script>
<script src="scripts/directives/indeterminate.js"></script>
<script src="scripts/directives/placeholder.js"></script>
<script src="scripts/directives/setting.js"></script>
<script src="scripts/directives/settingDialog.js"></script>

View file

@ -12,6 +12,7 @@
defaultLanguage: 'en',
defaultHost: 'localhost',
defaultSecureProtocol: 'https',
defaultPathSeparator: '/',
globalStatStorageCapacity: 120,
taskStatStorageCapacity: 300,
lazySaveTimeout: 500,

View file

@ -65,6 +65,7 @@
};
var includeLocalPeer = true;
var addVirtualFileNode = true;
if (!$scope.task) {
return aria2TaskService.getTaskStatus($routeParams.gid, function (response) {
@ -82,7 +83,7 @@
}
}, silent, includeLocalPeer);
}
}, silent);
}, silent, addVirtualFileNode);
} else {
return aria2TaskService.getTaskStatusAndBtPeers($routeParams.gid, function (response) {
if (!response.success) {
@ -91,7 +92,7 @@
processTask(response.task);
processPeers(response.peers);
}, silent, requireBtPeers($scope.task), includeLocalPeer);
}, silent, requireBtPeers($scope.task), includeLocalPeer, addVirtualFileNode);
}
};
@ -106,7 +107,7 @@
for (var i = 0; i < $scope.task.files.length; i++) {
var file = $scope.task.files[i];
if (file && file.selected) {
if (file && file.selected && !file.isDir) {
selectedFileIndex.push(file.index);
}
}
@ -122,10 +123,78 @@
}, silent);
};
var setSelectedNode = function (node, value) {
if (!node) {
return;
}
if (node.files && node.files.length) {
for (var i = 0; i < node.files.length; i++) {
var fileNode = node.files[i];
fileNode.selected = value;
}
}
if (node.subDirs && node.subDirs.length) {
for (var i = 0; i < node.subDirs.length; i++) {
var dirNode = node.subDirs[i];
setSelectedNode(dirNode, value);
}
}
node.selected = value;
node.partialSelected = false;
};
var updateDirNodeSelectedStatus = function (node) {
if (!node) {
return;
}
var selectedSubNodesCount = 0;
var partitalSelectedSubNodesCount = 0;
if (node.files && node.files.length) {
for (var i = 0; i < node.files.length; i++) {
var fileNode = node.files[i];
selectedSubNodesCount += (fileNode.selected ? 1 : 0);
}
}
if (node.subDirs && node.subDirs.length) {
for (var i = 0; i < node.subDirs.length; i++) {
var dirNode = node.subDirs[i];
updateDirNodeSelectedStatus(dirNode);
selectedSubNodesCount += (dirNode.selected ? 1 : 0);
partitalSelectedSubNodesCount += (dirNode.partialSelected ? 1 : 0);
}
}
node.selected = (selectedSubNodesCount > 0 && selectedSubNodesCount === (node.subDirs.length + node.files.length));
node.partialSelected = ((selectedSubNodesCount > 0 && selectedSubNodesCount < (node.subDirs.length + node.files.length)) || partitalSelectedSubNodesCount > 0);
};
var updateAllDirNodesSelectedStatus = function () {
if (!$scope.task || !$scope.task.multiDir) {
return;
}
for (var i = 0; i < $scope.task.files.length; i++) {
var node = $scope.task.files[i];
if (!node.isDir) {
continue;
}
updateDirNodeSelectedStatus(node);
}
};
$scope.context = {
currentTab: 'overview',
isEnableSpeedChart: ariaNgSettingService.getDownloadTaskRefreshInterval() > 0,
showChooseFilesToolbar: false,
collapsedDirs: {},
btPeers: [],
healthPercent: 0,
collapseTrackers: true,
@ -165,6 +234,10 @@
};
$scope.changeFileListDisplayOrder = function (type, autoSetReverse) {
if ($scope.task && $scope.task.multiDir) {
return;
}
var oldType = ariaNgCommonService.parseOrderType(ariaNgSettingService.getFileListDisplayOrder());
var newType = ariaNgCommonService.parseOrderType(type);
@ -183,6 +256,10 @@
};
$scope.getFileListOrderType = function () {
if ($scope.task && $scope.task.multiDir) {
return null;
}
return ariaNgSettingService.getFileListDisplayOrder();
};
@ -191,14 +268,16 @@
$scope.context.showChooseFilesToolbar = true;
};
$scope.getSelectedFileCount = function () {
var count = 0;
$scope.isAnyFileSelected = function () {
for (var i = 0; i < $scope.task.files.length; i++) {
count += $scope.task.files[i].selected ? 1 : 0;
var file = $scope.task.files[i];
if (!file.isDir && file.selected) {
return true;
}
}
return count;
return false;
};
$scope.selectFiles = function (type) {
@ -207,14 +286,22 @@
}
for (var i = 0; i < $scope.task.files.length; i++) {
var file = $scope.task.files[i];
if (file.isDir) {
continue;
}
if (type === 'all') {
$scope.task.files[i].selected = true;
file.selected = true;
} else if (type === 'none') {
$scope.task.files[i].selected = false;
file.selected = false;
} else if (type === 'reverse') {
$scope.task.files[i].selected = !$scope.task.files[i].selected;
file.selected = !file.selected;
}
}
updateAllDirNodesSelectedStatus();
};
$scope.chooseSpecifiedFiles = function (type) {
@ -222,12 +309,19 @@
return;
}
var files = $scope.task.files;
var extensions = ariaNgFileTypes[type];
var fileIndexes = [];
var isAllSelected = true;
for (var i = 0; i < $scope.task.files.length; i++) {
var extension = ariaNgCommonService.getFileExtension($scope.task.files[i].fileName);
for (var i = 0; i < files.length; i++) {
var file = files[i];
if (file.isDir) {
continue;
}
var extension = ariaNgCommonService.getFileExtension(file.fileName);
if (extension) {
extension = extension.toLowerCase();
@ -236,7 +330,7 @@
if (extensions.indexOf(extension) >= 0) {
fileIndexes.push(i);
if (!$scope.task.files[i].selected) {
if (!file.selected) {
isAllSelected = false;
}
}
@ -244,8 +338,14 @@
for (var i = 0; i < fileIndexes.length; i++) {
var index = fileIndexes[i];
$scope.task.files[index].selected = !isAllSelected;
var file = files[index];
if (file && !file.isDir) {
file.selected = !isAllSelected;
}
}
updateAllDirNodesSelectedStatus();
};
$scope.saveChoosedFiles = function () {
@ -263,12 +363,43 @@
}
};
$scope.setSelectedFile = function () {
$scope.setSelectedFile = function (updateNodeSelectedStatus) {
if (updateNodeSelectedStatus) {
updateAllDirNodesSelectedStatus();
}
if (!$scope.context.showChooseFilesToolbar) {
setSelectFiles(true);
}
};
$scope.collapseDir = function (dirNode, newValue) {
var nodePath = dirNode.nodePath;
if (angular.isUndefined(newValue)) {
newValue = !$scope.context.collapsedDirs[nodePath];
}
if (newValue) {
for (var i = 0; i < dirNode.subDirs.length; i++) {
$scope.collapseDir(dirNode.subDirs[i], newValue);
}
}
if (nodePath) {
$scope.context.collapsedDirs[nodePath] = newValue;
}
};
$scope.setSelectedNode = function (dirNode) {
setSelectedNode(dirNode, dirNode.selected);
updateAllDirNodesSelectedStatus();
if (!$scope.context.showChooseFilesToolbar) {
$scope.setSelectedFile(false);
}
};
$scope.changePeerListDisplayOrder = function (type, autoSetReverse) {
var oldType = ariaNgCommonService.parseOrderType(ariaNgSettingService.getPeerListDisplayOrder());
var newType = ariaNgCommonService.parseOrderType(type);

View file

@ -0,0 +1,17 @@
(function () {
'use strict';
angular.module('ariaNg').directive('ngIndeterminate', function () {
return {
restrict: 'A',
scope: {
indeterminate: '=ngIndeterminate'
},
link: function (scope, element) {
scope.$watch('indeterminate', function () {
element[0].indeterminate = (scope.indeterminate === 'true' || scope.indeterminate === true);
});
}
};
});
}());

View file

@ -3,7 +3,7 @@
angular.module('ariaNg').filter('fileOrderBy', ['$filter', 'ariaNgCommonService', function ($filter, ariaNgCommonService) {
return function (array, type) {
if (!angular.isArray(array)) {
if (!angular.isArray(array) || !type) {
return array;
}

View file

@ -1,7 +1,7 @@
(function () {
'use strict';
angular.module('ariaNg').factory('aria2TaskService', ['$q', 'bittorrentPeeridService', 'aria2Errors', 'aria2RpcService', 'ariaNgCommonService', 'ariaNgLocalizationService', 'ariaNgLogService', 'ariaNgSettingService', function ($q, bittorrentPeeridService, aria2Errors, aria2RpcService, ariaNgCommonService, ariaNgLocalizationService, ariaNgLogService, ariaNgSettingService) {
angular.module('ariaNg').factory('aria2TaskService', ['$q', 'bittorrentPeeridService', 'ariaNgConstants', 'aria2Errors', 'aria2RpcService', 'ariaNgCommonService', 'ariaNgLocalizationService', 'ariaNgLogService', 'ariaNgSettingService', function ($q, bittorrentPeeridService, ariaNgConstants, aria2Errors, aria2RpcService, ariaNgCommonService, ariaNgLocalizationService, ariaNgLogService, ariaNgSettingService) {
var getFileName = function (file) {
if (!file) {
ariaNgLogService.warn('[aria2TaskService.getFileName] file is null');
@ -54,6 +54,150 @@
};
};
var getRelativePath = function (task, file) {
var downloadPath = task.dir;
var relativePath = file.path;
if (downloadPath) {
downloadPath = downloadPath.replace(/\\/g, ariaNgConstants.defaultPathSeparator);
}
if (relativePath) {
relativePath = relativePath.replace(/\\/g, ariaNgConstants.defaultPathSeparator);
}
var trimStartPathSeparator = function () {
if (relativePath.length > 1 && relativePath.charAt(0) === ariaNgConstants.defaultPathSeparator) {
relativePath = relativePath.substr(1);
}
};
var trimEndPathSeparator = function () {
if (relativePath.length > 1 && relativePath.charAt(relativePath.length - 1) === ariaNgConstants.defaultPathSeparator) {
relativePath = relativePath.substr(0, relativePath.length - 1);
}
};
if (downloadPath && relativePath.indexOf(downloadPath) === 0) {
relativePath = relativePath.substr(downloadPath.length);
}
trimStartPathSeparator();
if (task.bittorrent && task.bittorrent.mode === 'multi' && task.bittorrent.info && task.bittorrent.info.name) {
var bittorrentName = task.bittorrent.info.name;
if (relativePath.indexOf(bittorrentName) === 0) {
relativePath = relativePath.substr(bittorrentName.length);
}
}
trimStartPathSeparator();
if (file.fileName && ((relativePath.lastIndexOf(file.fileName) + file.fileName.length) === relativePath.length)) {
relativePath = relativePath.substr(0, relativePath.length - file.fileName.length);
}
trimEndPathSeparator();
return relativePath;
};
var getDirectoryNode = function (path, allDirectories, allDirectoryMap) {
var node = allDirectoryMap[path];
if (node) {
return node;
}
var parentNode = null;
var nodeName = path;
if (path.length) {
var parentPath = '';
var lastSeparatorIndex = path.lastIndexOf(ariaNgConstants.defaultPathSeparator);
if (lastSeparatorIndex > 0) {
parentPath = path.substring(0, lastSeparatorIndex);
nodeName = path.substring(lastSeparatorIndex + 1);
}
parentNode = getDirectoryNode(parentPath, allDirectories, allDirectoryMap);
}
node = {
isDir: true,
nodePath: path,
nodeName: nodeName,
relativePath: (parentNode && parentNode.nodePath) || '',
level: (parentNode && parentNode.level + 1) || 0,
length: 0,
selected: true,
partialSelected: false,
files: [],
subDirs: []
};
allDirectories.push(node);
allDirectoryMap[path] = node;
if (parentNode) {
parentNode.subDirs.push(node);
}
return node;
};
var pushFileToDirectoryNode = function (file, allDirectories, allDirectoryMap) {
if (!file || !allDirectories || !allDirectoryMap) {
return;
}
var nodePath = file.relativePath || '';
var directoryNode = getDirectoryNode(nodePath, allDirectories, allDirectoryMap);
directoryNode.files.push(file);
return directoryNode;
};
var fillAllNodes = function (node, allDirectoryMap, allNodes) {
if (!node) {
return;
}
var allSubNodesLength = 0;
var selectedSubNodesCount = 0;
var partitalSelectedSubNodesCount = 0;
if (node.subDirs && node.subDirs.length) {
for (var i = 0; i < node.subDirs.length; i++) {
var dirNode = node.subDirs[i];
allNodes.push(dirNode);
fillAllNodes(dirNode, allDirectoryMap, allNodes);
allSubNodesLength += dirNode.length;
selectedSubNodesCount += (dirNode.selected ? 1 : 0);
partitalSelectedSubNodesCount += (dirNode.partialSelected ? 1 : 0);
}
}
if (node.files && node.files.length) {
for (var i = 0; i < node.files.length; i++) {
var fileNode = node.files[i];
allNodes.push(fileNode);
allSubNodesLength += fileNode.length;
selectedSubNodesCount += (fileNode.selected ? 1 : 0);
}
}
node.length = allSubNodesLength;
node.selected = (selectedSubNodesCount > 0 && selectedSubNodesCount === (node.subDirs.length + node.files.length));
node.partialSelected = ((selectedSubNodesCount > 0 && selectedSubNodesCount < (node.subDirs.length + node.files.length)) || partitalSelectedSubNodesCount > 0);
};
var getTaskErrorDescription = function (task) {
if (!task.errorCode) {
return '';
@ -121,12 +265,14 @@
return combinedPieces;
};
var processDownloadTask = function (task) {
var processDownloadTask = function (task, addVirtualFileNode) {
if (!task) {
ariaNgLogService.warn('[aria2TaskService.processDownloadTask] task is null');
return task;
}
addVirtualFileNode = addVirtualFileNode && task.bittorrent && task.bittorrent.mode === 'multi';
var pieceStatus = getPieceStatus(task.bitfield, task.numPieces);
task.totalLength = parseInt(task.totalLength);
@ -156,6 +302,8 @@
if (task.files) {
var selectedFileCount = 0;
var allDirectories = [];
var allDirectoryMap = {};
for (var i = 0; i < task.files.length; i++) {
var file = task.files[i];
@ -166,9 +314,24 @@
file.completedLength = parseInt(file.completedLength);
file.completePercent = (file.length > 0 ? file.completedLength / file.length * 100 : 0);
if (addVirtualFileNode) {
file.relativePath = getRelativePath(task, file);
var dirNode = pushFileToDirectoryNode(file, allDirectories, allDirectoryMap);
file.level = dirNode.level + 1;
}
selectedFileCount += file.selected ? 1 : 0;
}
if (addVirtualFileNode && allDirectories.length > 1) {
var allNodes = [];
var rootNode = allDirectoryMap[''];
fillAllNodes(rootNode, allDirectoryMap, allNodes);
task.files = allNodes;
task.multiDir = true;
}
task.selectedFileCount = selectedFileCount;
}
@ -304,7 +467,7 @@
}
});
},
getTaskStatus: function (gid, callback, silent) {
getTaskStatus: function (gid, callback, silent, addVirtualFileNode) {
return aria2RpcService.tellStatus({
gid: gid,
silent: !!silent,
@ -315,7 +478,7 @@
}
if (response.success) {
processDownloadTask(response.data);
processDownloadTask(response.data, addVirtualFileNode);
}
callback(response);
@ -371,7 +534,7 @@
}
});
},
getTaskStatusAndBtPeers: function (gid, callback, silent, requirePeers, includeLocalPeer) {
getTaskStatusAndBtPeers: function (gid, callback, silent, requirePeers, includeLocalPeer, addVirtualFileNode) {
var methods = [
aria2RpcService.tellStatus({ gid: gid }, true)
];
@ -393,7 +556,7 @@
if (response.success && response.data.length > 0) {
response.task = response.data[0][0];
processDownloadTask(response.task);
processDownloadTask(response.task, addVirtualFileNode);
}
if (response.success && response.task.bittorrent && response.data.length > 1) {
@ -685,14 +848,14 @@
callback: createTaskEventCallback(this.getTaskStatus, callback, 'error')
});
},
processDownloadTasks: function (tasks) {
processDownloadTasks: function (tasks, addVirtualFileNode) {
if (!angular.isArray(tasks)) {
ariaNgLogService.warn('[aria2TaskService.processDownloadTasks] tasks is not array', tasks);
return;
}
for (var i = 0; i < tasks.length; i++) {
processDownloadTask(tasks[i]);
processDownloadTask(tasks[i], addVirtualFileNode);
}
},
getPieceStatus: function (bitField, pieceCount) {

View file

@ -110,6 +110,14 @@
margin-bottom: 2px;
}
.checkbox-inline {
display: inline-block;
}
.icon-expand + .checkbox {
margin-left: 4px;
}
/* angular-input-dropdown */
input-dropdown[input-class-name="form-control"] > .input-dropdown {
width: 100%

View file

@ -454,7 +454,10 @@
}
/* awesome-bootstrap-checkbox extend */
.skin-aria-ng .checkbox-primary input[type="checkbox"]:checked + label::before, .checkbox-primary input[type="radio"]:checked + label::before {
.skin-aria-ng .checkbox-primary input[type="checkbox"]:checked + label::before,
.skin-aria-ng .checkbox-primary input[type="radio"]:checked + label::before,
.skin-aria-ng .checkbox-primary input[type="checkbox"]:indeterminate + label::before,
.skin-aria-ng .checkbox-primary input[type="radio"]:indeterminate + label::before {
background-color: #208fe5;
border-color: #208fe5;
}

View file

@ -182,17 +182,17 @@
<div class="task-table-title">
<div class="row">
<div class="col-sm-8">
<a ng-click="changeFileListDisplayOrder('name:asc', true)" translate>File Name</a>
<i class="fa" ng-class="{'fa-sort-asc fa-order-asc': isSetFileListDisplayOrder('name:asc'), 'fa-sort-desc fa-order-desc': isSetFileListDisplayOrder('name:desc')}"></i>
<a ng-click="changeFileListDisplayOrder('name:asc', true)" ng-class="{true: 'default-cursor'}[task.multiDir]" translate>File Name</a>
<i ng-if="!task.multiDir" class="fa" ng-class="{'fa-sort-asc fa-order-asc': isSetFileListDisplayOrder('name:asc'), 'fa-sort-desc fa-order-desc': isSetFileListDisplayOrder('name:desc')}"></i>
<a ng-click="showChooseFilesToolbar()" ng-if="task && task.files && task.files.length > 1 && (task.status === 'waiting' || task.status === 'paused')" translate>(Choose Files)</a>
</div>
<div class="col-sm-2">
<a ng-click="changeFileListDisplayOrder('percent:desc', true)" translate>Progress</a>
<i class="fa" ng-class="{'fa-sort-asc fa-order-asc': isSetFileListDisplayOrder('percent:asc'), 'fa-sort-desc fa-order-desc': isSetFileListDisplayOrder('percent:desc')}"></i>
<a ng-click="changeFileListDisplayOrder('percent:desc', true)" ng-class="{true: 'default-cursor'}[task.multiDir]" translate>Progress</a>
<i ng-if="!task.multiDir" class="fa" ng-class="{'fa-sort-asc fa-order-asc': isSetFileListDisplayOrder('percent:asc'), 'fa-sort-desc fa-order-desc': isSetFileListDisplayOrder('percent:desc')}"></i>
</div>
<div class="col-sm-2">
<a ng-click="changeFileListDisplayOrder('size:asc', true)" translate>File Size</a>
<i class="fa" ng-class="{'fa-sort-asc fa-order-asc': isSetFileListDisplayOrder('size:asc'), 'fa-sort-desc fa-order-desc': isSetFileListDisplayOrder('size:desc')}"></i>
<a ng-click="changeFileListDisplayOrder('size:asc', true)" ng-class="{true: 'default-cursor'}[task.multiDir]" translate>File Size</a>
<i ng-if="!task.multiDir" class="fa" ng-class="{'fa-sort-asc fa-order-asc': isSetFileListDisplayOrder('size:asc'), 'fa-sort-desc fa-order-desc': isSetFileListDisplayOrder('size:desc')}"></i>
</div>
</div>
</div>
@ -226,21 +226,32 @@
<i class="fa fa-file-archive-o"></i>
<span translate>Archives</span>
</button>
<button class="btn btn-xs btn-success" ng-click="saveChoosedFiles()" ng-disabled="getSelectedFileCount() < 1" translate>Confirm</button>
<button class="btn btn-xs btn-success" ng-click="saveChoosedFiles()" ng-disabled="!isAnyFileSelected()" translate>Confirm</button>
<button class="btn btn-xs btn-default" ng-click="cancelChooseFiles()" translate>Cancel</button>
</div>
</div>
</div>
<div class="task-table-body">
<div class="row" ng-repeat="file in task.files | fileOrderBy: getFileListOrderType()" data-file-index="{{file.index}}">
<div class="col-sm-8">
<div class="row" ng-repeat="file in task.files | fileOrderBy: getFileListOrderType()"
ng-if="!context.collapsedDirs[file.relativePath]" data-file-index="{{file.index}}">
<div class="col-sm-10" ng-if="file.isDir" style="{{'padding-left: ' + (file.level * 16) + 'px'}}">
<i class="icon-expand pointer-cursor fa" ng-click="collapseDir(file)"
ng-class="{true: 'fa-plus', false: 'fa-minus'}[!!context.collapsedDirs[file.nodePath]]"
title="{{(context.collapsedDirs[file.nodePath] ? 'Expand' : 'Collapse') | translate}}"></i>
<div class="checkbox checkbox-primary checkbox-inline">
<input id="{{'node_' + file.nodePath}}" type="checkbox" ng-disabled="!task || !task.files || task.files.length <= 1 || (task.status !== 'waiting' && task.status !== 'paused')"
ng-model="file.selected" ng-indeterminate="file.partialSelected" ng-change="setSelectedNode(file)"/>
<label for="{{'node_' + file.nodePath}}" class="allow-word-break" ng-bind="file.nodeName" title="{{file.nodeName}}"></label>
</div>
</div>
<div class="col-sm-8" ng-if="!file.isDir" style="{{'padding-left: ' + (11 + 3 + 4 + file.level * 16) + 'px'}}">
<div class="checkbox checkbox-primary">
<input id="{{'file_' + file.index}}" type="checkbox" ng-disabled="!task || !task.files || task.files.length < 2 || (task.status !== 'waiting' && task.status !== 'paused')"
ng-model="file.selected" ng-change="setSelectedFile()"/>
<input id="{{'file_' + file.index}}" type="checkbox" ng-disabled="!task || !task.files || task.files.length <= 1 || (task.status !== 'waiting' && task.status !== 'paused')"
ng-model="file.selected" ng-change="setSelectedFile(true)"/>
<label for="{{'file_' + file.index}}" class="allow-word-break" ng-bind="file.fileName" title="{{file.fileName}}"></label>
</div>
</div>
<div class="col-sm-2">
<div class="col-sm-2" ng-if="!file.isDir">
<div class="progress">
<div class="progress-bar progress-bar-primary" role="progressbar"
aria-valuenow="{{file.completePercent}}" aria-valuemin="1"