diff --git a/src/index.html b/src/index.html index 00a028b..5448fd2 100644 --- a/src/index.html +++ b/src/index.html @@ -372,6 +372,7 @@ + diff --git a/src/scripts/config/constants.js b/src/scripts/config/constants.js index ceac949..a5f3093 100644 --- a/src/scripts/config/constants.js +++ b/src/scripts/config/constants.js @@ -12,6 +12,7 @@ defaultLanguage: 'en', defaultHost: 'localhost', defaultSecureProtocol: 'https', + defaultPathSeparator: '/', globalStatStorageCapacity: 120, taskStatStorageCapacity: 300, lazySaveTimeout: 500, diff --git a/src/scripts/controllers/task-detail.js b/src/scripts/controllers/task-detail.js index 7d568f0..7a48ca4 100644 --- a/src/scripts/controllers/task-detail.js +++ b/src/scripts/controllers/task-detail.js @@ -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); diff --git a/src/scripts/directives/indeterminate.js b/src/scripts/directives/indeterminate.js new file mode 100644 index 0000000..9a73b82 --- /dev/null +++ b/src/scripts/directives/indeterminate.js @@ -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); + }); + } + }; + }); +}()); diff --git a/src/scripts/filters/fileOrderBy.js b/src/scripts/filters/fileOrderBy.js index 56038c4..21a1a40 100644 --- a/src/scripts/filters/fileOrderBy.js +++ b/src/scripts/filters/fileOrderBy.js @@ -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; } diff --git a/src/scripts/services/aria2TaskService.js b/src/scripts/services/aria2TaskService.js index 500491c..9395612 100644 --- a/src/scripts/services/aria2TaskService.js +++ b/src/scripts/services/aria2TaskService.js @@ -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) { diff --git a/src/styles/core/extend.css b/src/styles/core/extend.css index 393411d..1bde328 100644 --- a/src/styles/core/extend.css +++ b/src/styles/core/extend.css @@ -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% diff --git a/src/styles/theme/default.css b/src/styles/theme/default.css index a88ac47..3251043 100644 --- a/src/styles/theme/default.css +++ b/src/styles/theme/default.css @@ -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; } diff --git a/src/views/task-detail.html b/src/views/task-detail.html index bdf1395..ca809d7 100644 --- a/src/views/task-detail.html +++ b/src/views/task-detail.html @@ -182,17 +182,17 @@
- Progress - + Progress +
@@ -226,27 +226,38 @@ Archives - +
-
-
+
+
+ +
+ + +
+
+
- +
-
+
- +