diff --git a/app/index.html b/app/index.html
index 4b307a9..a89bb56 100644
--- a/app/index.html
+++ b/app/index.html
@@ -205,14 +205,17 @@
@@ -238,8 +241,8 @@
-
-
+
+
@@ -277,6 +280,7 @@
+
@@ -287,6 +291,7 @@
+
diff --git a/app/langs/zh_CN.json b/app/langs/zh_CN.json
index b68e1d6..ba30802 100644
--- a/app/langs/zh_CN.json
+++ b/app/langs/zh_CN.json
@@ -63,6 +63,7 @@
"Status": "状态",
"Percent": "完成度",
"Download / Upload Speed": "下载 / 上传速度",
+ "No Data": "无数据",
"No connected peers": "没有连接到其他节点",
"Failed to change some tasks state.": "修改一些任务状态时失败.",
"Confirm Remove": "确认删除",
diff --git a/app/scripts/config/constants.js b/app/scripts/config/constants.js
index e1877b4..efa5520 100644
--- a/app/scripts/config/constants.js
+++ b/app/scripts/config/constants.js
@@ -5,6 +5,8 @@
title: 'Aria Ng',
appPrefix: 'AriaNg',
optionStorageKey: 'Options',
+ globalStatStorageCapacity: 120,
+ taskStatStorageCapacity: 300,
lazySaveTimeout: 500
}).constant('ariaNgDefaultOptions', {
language: 'en',
diff --git a/app/scripts/config/defaultLanguage.js b/app/scripts/config/defaultLanguage.js
index 4c39e2c..09e2c77 100644
--- a/app/scripts/config/defaultLanguage.js
+++ b/app/scripts/config/defaultLanguage.js
@@ -67,6 +67,7 @@
'Status': 'Status',
'Percent': 'Percent',
'Download / Upload Speed': 'Download / Upload Speed',
+ 'No Data': 'No Data',
'No connected peers': 'No connected peers',
'Failed to change some tasks state.': 'Failed to change some tasks state.',
'Confirm Remove': 'Confirm Remove',
diff --git a/app/scripts/controllers/main.js b/app/scripts/controllers/main.js
index a8f397e..a25bfd3 100644
--- a/app/scripts/controllers/main.js
+++ b/app/scripts/controllers/main.js
@@ -1,7 +1,7 @@
(function () {
'use strict';
- angular.module('ariaNg').controller('MainController', ['$rootScope', '$scope', '$route', '$location', '$interval', 'aria2RpcErrors', 'ariaNgCommonService', 'ariaNgSettingService', 'aria2TaskService', 'aria2SettingService', function ($rootScope, $scope, $route, $location, $interval, aria2RpcErrors, ariaNgCommonService, ariaNgSettingService, aria2TaskService, aria2SettingService) {
+ angular.module('ariaNg').controller('MainController', ['$rootScope', '$scope', '$route', '$location', '$interval', 'aria2RpcErrors', 'ariaNgCommonService', 'ariaNgSettingService', 'ariaNgMonitorService', 'aria2TaskService', 'aria2SettingService', function ($rootScope, $scope, $route, $location, $interval, aria2RpcErrors, ariaNgCommonService, ariaNgSettingService, ariaNgMonitorService, aria2TaskService, aria2SettingService) {
var globalStatRefreshPromise = null;
var refreshGlobalStat = function (silent) {
@@ -10,13 +10,18 @@
$interval.cancel(globalStatRefreshPromise);
return;
}
-
+
if (response.success) {
$scope.globalStat = response.data;
+ ariaNgMonitorService.recordGlobalStat(response.data);
}
}, silent);
};
+ $scope.globalStatusContext = {
+ data: ariaNgMonitorService.getGlobalStatsData()
+ };
+
$scope.isTaskSelected = function () {
return $rootScope.taskContext.getSelectedTaskIds().length > 0;
};
diff --git a/app/scripts/controllers/task-detail.js b/app/scripts/controllers/task-detail.js
index ca9075c..974673f 100644
--- a/app/scripts/controllers/task-detail.js
+++ b/app/scripts/controllers/task-detail.js
@@ -1,7 +1,7 @@
(function () {
'use strict';
- angular.module('ariaNg').controller('TaskDetailController', ['$rootScope', '$scope', '$routeParams', '$interval', 'aria2RpcErrors', 'ariaNgCommonService', 'ariaNgSettingService', 'aria2TaskService', 'aria2SettingService', function ($rootScope, $scope, $routeParams, $interval, aria2RpcErrors, ariaNgCommonService, ariaNgSettingService, aria2TaskService, aria2SettingService) {
+ angular.module('ariaNg').controller('TaskDetailController', ['$rootScope', '$scope', '$routeParams', '$interval', 'aria2RpcErrors', 'ariaNgCommonService', 'ariaNgSettingService', 'ariaNgMonitorService', 'aria2TaskService', 'aria2SettingService', function ($rootScope, $scope, $routeParams, $interval, aria2RpcErrors, ariaNgCommonService, ariaNgSettingService, ariaNgMonitorService, aria2TaskService, aria2SettingService) {
var tabOrders = ['overview', 'blocks', 'filelist', 'btpeers'];
var downloadTaskRefreshPromise = null;
var pauseDownloadTaskRefresh = false;
@@ -32,7 +32,7 @@
$scope.peers = peers;
}
- $scope.healthPercent = aria2TaskService.estimateHealthPercentFromPeers(task, $scope.peers);
+ $scope.context.healthPercent = aria2TaskService.estimateHealthPercentFromPeers(task, $scope.peers);
}, silent);
};
@@ -53,9 +53,7 @@
var task = response.data;
if (task.status == 'active' && task.bittorrent) {
- if ($scope.context.currentTab == 'btpeers') {
- refreshBtPeers(task, true);
- }
+ refreshBtPeers(task, true);
} else {
if (tabOrders.indexOf('btpeers') >= 0) {
tabOrders.splice(tabOrders.indexOf('btpeers'), 1);
@@ -63,7 +61,7 @@
}
if (!$scope.task || $scope.task.status != task.status) {
- $scope.availableOptions = getAvailableOptions(task.status, !!task.bittorrent);
+ $scope.context.availableOptions = getAvailableOptions(task.status, !!task.bittorrent);
}
$scope.task = ariaNgCommonService.copyObjectTo(task, $scope.task);
@@ -71,16 +69,18 @@
$rootScope.taskContext.list = [$scope.task];
$rootScope.taskContext.selected = {};
$rootScope.taskContext.selected[$scope.task.gid] = true;
+
+ ariaNgMonitorService.recordStat(task.gid, task);
}, silent);
};
$scope.context = {
- currentTab: 'overview'
+ currentTab: 'overview',
+ healthPercent: 0,
+ statusData: ariaNgMonitorService.getEmptyStatsData($routeParams.gid),
+ availableOptions: []
};
- $scope.healthPercent = 0;
- $scope.availableOptions = [];
-
$rootScope.swipeActions.extentLeftSwipe = function () {
var tabIndex = tabOrders.indexOf($scope.context.currentTab);
@@ -151,10 +151,6 @@
}
}, true);
};
-
- $scope.loadBtPeers = function (task) {
- $rootScope.loadPromise = refreshBtPeers(task, false);
- };
$scope.loadTaskOption = function (task) {
$rootScope.loadPromise = aria2TaskService.getTaskOptions(task.gid, function (response) {
diff --git a/app/scripts/directives/chart.js b/app/scripts/directives/chart.js
new file mode 100644
index 0000000..bfe6fbc
--- /dev/null
+++ b/app/scripts/directives/chart.js
@@ -0,0 +1,198 @@
+(function () {
+ 'use strict';
+
+ angular.module('ariaNg').directive('ngChart', ['$window', 'chartTheme', function ($window, chartTheme) {
+ return {
+ restrict: 'E',
+ template: '',
+ scope: {
+ options: '=ngData'
+ },
+ link: function (scope, element, attrs) {
+ var options = {
+ ngTheme: 'default'
+ };
+
+ angular.extend(options, attrs);
+
+ var wrapper = element.find('div');
+ var wrapperParent = element.parent();
+ var parentHeight = wrapperParent.height();
+
+ var height = parseInt(attrs.height) || parentHeight || 200;
+ wrapper.css('height', height + 'px');
+
+ var chart = echarts.init(wrapper[0], chartTheme.get(options.ngTheme));
+
+ var setOptions = function (value) {
+ chart.setOption(value);
+ };
+
+ angular.element($window).on('resize', function () {
+ chart.resize();
+ scope.$apply();
+ });
+
+ scope.$watch(function () {
+ return scope.options;
+ }, function (value) {
+ if (value) {
+ setOptions(value);
+ }
+ }, true);
+ }
+ };
+ }]).directive('ngPopChart', ['$window', 'chartTheme', function ($window, chartTheme) {
+ return {
+ restrict: 'A',
+ scope: {
+ options: '=ngData'
+ },
+ link: function (scope, element, attrs) {
+ var options = {
+ ngTheme: 'default',
+ ngPopoverClass: '',
+ ngContainer: 'body',
+ ngTrigger: 'click',
+ ngPlacement: 'top'
+ };
+
+ angular.extend(options, attrs);
+
+ var chart = null;
+ var loadingIcon = '
';
+
+ element.popover({
+ container: options.ngContainer,
+ content: '',
+ html: true,
+ placement: options.ngPlacement,
+ template: '',
+ trigger: options.ngTrigger
+ }).on('shown.bs.popover', function () {
+ var wrapper = angular.element('.chart-pop');
+ var wrapperParent = wrapper.parent();
+ var parentHeight = wrapperParent.height();
+
+ wrapper.empty();
+
+ var height = parseInt(attrs.height) || parentHeight || 200;
+ wrapper.css('height', height + 'px');
+
+ chart = echarts.init(wrapper[0], chartTheme.get(options.ngTheme));
+ }).on('hide.bs.popover', function () {
+ if (chart && chart.isDisposed()) {
+ chart.dispose();
+ }
+ }).on('hidden.bs.popover', function () {
+ angular.element('.chart-pop').empty().append(loadingIcon);
+ });
+
+ var setOptions = function (value) {
+ if (chart && !chart.isDisposed()) {
+ chart.setOption(value);
+ }
+ };
+
+ scope.$watch(function () {
+ return scope.options;
+ }, function (value) {
+ if (value) {
+ setOptions(value);
+ }
+ }, true);
+ }
+ };
+ }]).factory('chartTheme', ['chartDefaultTheme', function (chartDefaultTheme) {
+ var themes = {
+ defaultTheme: chartDefaultTheme
+ };
+
+ return {
+ get: function (name) {
+ return themes[name + 'Theme'] ? themes[name + 'Theme'] : {};
+ }
+ };
+ }]).factory('chartDefaultTheme', function () {
+ return {
+ color: ['#74a329', '#3a89e9'],
+ legend: {
+ top: 'bottom'
+ },
+ toolbox: {
+ show: false
+ },
+ tooltip: {
+ show: true,
+ trigger: 'axis',
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
+ axisPointer: {
+ type: 'line',
+ lineStyle: {
+ color: '#233333',
+ type: 'dashed',
+ width: 1
+ },
+ crossStyle: {
+ color: '#008acd',
+ width: 1
+ },
+ shadowStyle: {
+ color: 'rgba(200,200,200,0.2)'
+ }
+ }
+ },
+ grid: {
+ x: 40,
+ y: 20,
+ x2: 30,
+ y2: 50
+ },
+ categoryAxis: {
+ axisLine: {
+ show: false
+ },
+ axisTick: {
+ show: false
+ },
+ splitLine: {
+ lineStyle: {
+ color: '#f3f3f3'
+ }
+ }
+ },
+ valueAxis: {
+ axisLine: {
+ show: false
+ },
+ axisTick: {
+ show: false
+ },
+ splitLine: {
+ lineStyle: {
+ color: '#f3f3f3'
+ }
+ },
+ splitArea: {
+ show: false
+ }
+ },
+ line: {
+ itemStyle: {
+ normal: {
+ lineStyle: {
+ width: 2,
+ type: 'solid'
+ }
+ }
+ },
+ smooth: true,
+ symbolSize: 6
+ },
+ textStyle: {
+ fontFamily: 'Hiragino Sans GB, Microsoft YaHei, STHeiti, Helvetica Neue, Helvetica, Arial, sans-serif'
+ },
+ animationDuration: 500
+ };
+ });
+})();
diff --git a/app/scripts/filters/volumn.js b/app/scripts/filters/volumn.js
index 1c266cb..193dd32 100644
--- a/app/scripts/filters/volumn.js
+++ b/app/scripts/filters/volumn.js
@@ -3,25 +3,30 @@
angular.module("ariaNg").filter('readableVolumn', ['numberFilter', function (numberFilter) {
var units = [ 'B', 'KB', 'MB', 'GB' ];
+ var defaultFractionSize = 2;
- return function (value) {
+ return function (value, fractionSize) {
var unit = units[0];
- if (!value) {
- value = 0;
- } else {
- for (var i = 1; i < units.length; i++) {
- if (value >= 1024) {
- value = value / 1024;
- unit = units[i];
- } else {
- break;
- }
- }
-
- value = numberFilter(value, 2);
+ if (angular.isUndefined(fractionSize)) {
+ fractionSize = defaultFractionSize;
}
+ if (!angular.isNumber(value)) {
+ value = parseInt(value);
+ }
+
+ for (var i = 1; i < units.length; i++) {
+ if (value >= 1024) {
+ value = value / 1024;
+ unit = units[i];
+ } else {
+ break;
+ }
+ }
+
+ value = numberFilter(value, fractionSize);
+
return value + ' ' + unit;
}
}]);
diff --git a/app/scripts/services/ariaNgMonitorService.js b/app/scripts/services/ariaNgMonitorService.js
new file mode 100644
index 0000000..b78f273
--- /dev/null
+++ b/app/scripts/services/ariaNgMonitorService.js
@@ -0,0 +1,159 @@
+(function () {
+ 'use strict';
+
+ angular.module('ariaNg').factory('ariaNgMonitorService', ['$translate', 'moment', 'ariaNgConstants', 'readableVolumnFilter', function ($translate, moment, ariaNgConstants, readableVolumnFilter) {
+ var storagesInMemory = {};
+ var globalStorageKey = 'global';
+
+ var getStorageCapacity = function (key) {
+ if (key == globalStorageKey) {
+ return ariaNgConstants.globalStatStorageCapacity;
+ } else {
+ return ariaNgConstants.taskStatStorageCapacity;
+ }
+ };
+
+ var initStorage = function (key) {
+ var data = {
+ legend: {
+ show: false
+ },
+ grid: {
+ x: 50,
+ y: 10,
+ x2: 10,
+ y2: 10
+ },
+ tooltip: {
+ show: true,
+ formatter: function (params) {
+ if (params[0].name == '') {
+ return '' + $translate.instant('No Data') + '
';
+ }
+
+ var time = moment(params[0].name, 'X').format('HH:mm:ss');
+ var uploadSpeed = readableVolumnFilter(params[0].value) + '/s';
+ var downloadSpeed = readableVolumnFilter(params[1].value) + '/s';
+
+ return '' + time + '
'
+ + ' ' + downloadSpeed +'
'
+ + ' ' + uploadSpeed + '
';
+ }
+ },
+ xAxis: {
+ data: [],
+ type: 'category',
+ boundaryGap: false,
+ axisLabel: {
+ show: false
+ }
+ },
+ yAxis: {
+ type: 'value',
+ axisLabel: {
+ formatter: function (value) {
+ return readableVolumnFilter(value, 0);
+ }
+ }
+ },
+ series: [{
+ type: 'line',
+ areaStyle: {
+ normal: {
+ opacity: 0.1
+ }
+ },
+ smooth: true,
+ symbolSize: 6,
+ showAllSymbol: false,
+ data: []
+ }, {
+ type: 'line',
+ areaStyle: {
+ normal: {
+ opacity: 0.1
+ }
+ },
+ smooth: true,
+ symbolSize: 6,
+ showAllSymbol: false,
+ data: []
+ }]
+ };
+
+ var timeData = data.xAxis.data;
+ var uploadData = data.series[0].data;
+ var downloadData = data.series[1].data;
+
+ for (var i = 0; i < getStorageCapacity(key); i++) {
+ timeData.push('');
+ uploadData.push('');
+ downloadData.push('');
+ }
+
+ storagesInMemory[key] = data;
+
+ return data;
+ };
+
+ var isStorageExist = function (key) {
+ return !angular.isUndefined(storagesInMemory[key]);
+ };
+
+ var pushToStorage = function (key, stat) {
+ var storage = storagesInMemory[key];
+ var timeData = storage.xAxis.data;
+ var uploadData = storage.series[0].data;
+ var downloadData = storage.series[1].data;
+
+ if (timeData.length >= getStorageCapacity(key)) {
+ timeData.shift();
+ uploadData.shift();
+ downloadData.shift();
+ }
+
+ timeData.push(stat.time);
+ uploadData.push(stat.uploadSpeed);
+ downloadData.push(stat.downloadSpeed);
+ };
+
+ var getStorage = function (key) {
+ return storagesInMemory[key];
+ };
+
+ var removeStorage = function (key) {
+ delete storagesInMemory[key];
+ };
+
+ return {
+ recordStat: function (key, stat) {
+ if (!isStorageExist(key)) {
+ initStorage(key);
+ }
+
+ stat.time = moment().format('X');
+ pushToStorage(key, stat);
+ },
+ getStatsData: function (key) {
+ if (!isStorageExist(key)) {
+ initStorage(key);
+ }
+
+ return getStorage(key);
+ },
+ getEmptyStatsData: function (key) {
+ if (isStorageExist(key)) {
+ removeStorage(key);
+ }
+
+ return this.getStatsData(key);
+ },
+ recordGlobalStat: function (stat) {
+ return this.recordStat(globalStorageKey, stat);
+ },
+ getGlobalStatsData: function () {
+ return this.getStatsData(globalStorageKey);
+ }
+ }
+ }]);
+})();
diff --git a/app/styles/aria-ng.css b/app/styles/aria-ng.css
index 5151cbe..1791d37 100644
--- a/app/styles/aria-ng.css
+++ b/app/styles/aria-ng.css
@@ -309,6 +309,17 @@ td {
content: "\f0c9";
}
+.skin-aria-ng .global-status {
+ margin-right: 10px;
+ color: inherit;
+}
+
+.skin-aria-ng .global-status:hover {
+ border: 1px solid #ccc;
+ margin-right: 9px;
+ margin-top: -1px
+}
+
.skin-aria-ng .progress-bar-primary {
background-color: #208fe5;
}
@@ -555,6 +566,63 @@ td {
cursor: -webkit-grabbing;
}
+/* global-status */
+.global-status {
+ cursor: pointer;
+}
+
+.global-status > .realtime-speed {
+ padding: 0 15px 0 15px;
+}
+
+.global-status > .realtime-speed:first-child {
+ padding-left: 5px;
+}
+
+.global-status > .realtime-speed:last-child {
+ padding-right: 5px;
+}
+
+.global-status span.realtime-speed > i {
+ padding-right: 2px;
+}
+
+/* chart */
+.chart-popover {
+ max-width: 320px;
+}
+
+.chart-popover .popover-content {
+ padding: 0;
+}
+
+.chart-pop-wrapper {
+ padding-left: 4px;
+ padding-right: 4px;
+ overflow-x: hidden;
+}
+
+.chart-pop {
+ display: table;
+}
+
+.chart-pop .loading {
+ width: 100%;
+ height: 100%;
+ display: table-cell;
+ text-align: center;
+ vertical-align: middle;
+}
+
+.global-status-chart {
+ width: 312px;
+ height: 200px;
+}
+
+.task-status-chart-wrapper {
+ overflow-x: hidden;
+}
+
/* task-table */
.task-table {
margin-left: 15px;
@@ -689,6 +757,14 @@ td {
background-color: #f5f5f5;
}
+.skin-aria-ng .settings-table > div.row.no-background {
+ background-color: inherit;
+}
+
+.skin-aria-ng .settings-table > div.row.no-hover:hover {
+ background-color: inherit;
+}
+
.settings-table .input-group-addon {
background-color: #eee;
}
@@ -755,11 +831,3 @@ td {
}
}
-/* miscellaneous */
-span.realtime-speed {
- padding: 0 15px 0 15px;
-}
-
-span.realtime-speed > i {
- padding-right: 2px;
-}
diff --git a/app/views/task-detail.html b/app/views/task-detail.html
index 95d74d9..9033a21 100644
--- a/app/views/task-detail.html
+++ b/app/views/task-detail.html
@@ -11,7 +11,7 @@
Files
- Peers
+ Peers
@@ -61,7 +61,7 @@
-
+
@@ -120,6 +120,13 @@
+
@@ -217,7 +224,7 @@