Просмотр исходного кода

merge AriaNg commit(https://github.com/mayswind/AriaNg/commit/e657b4ec772a16139ed217feaf1675822b61cd2a)

MaysWind 3 лет назад
Родитель
Сommit
e2a93bb7cb

+ 1 - 1
app/index.html

@@ -276,7 +276,7 @@
                 </li>
                 <li data-href-match="/status">
                     <a href="#!/status">
-                        <span class="label pull-right" ng-if="globalStatusContext.isEnabled || isCurrentRpcUseWebSocket" ng-class="{'label-primary': taskContext.rpcStatus === 'Connecting', 'label-success': taskContext.rpcStatus === 'Connected', 'label-danger': taskContext.rpcStatus === 'Disconnected'}" ng-bind="taskContext.rpcStatus | translate"></span>
+                        <span class="label status-label pull-right auto-ellipsis" ng-if="globalStatusContext.isEnabled || isCurrentRpcUseWebSocket" ng-class="{'label-primary': taskContext.rpcStatus === 'Connecting' || taskContext.rpcStatus === 'Reconnecting', 'label-default': taskContext.rpcStatus === 'Waiting to reconnect', 'label-success': taskContext.rpcStatus === 'Connected', 'label-danger': taskContext.rpcStatus === 'Disconnected'}" ng-bind="taskContext.rpcStatus | translate"></span>
                         <i class="fa fa-server"></i> <span translate>Aria2 Status</span>
                     </a>
                 </li>

+ 4 - 0
app/langs/zh_Hans.txt

@@ -13,6 +13,8 @@ False=否
 Connecting=连接中
 Connected=已连接
 Disconnected=未连接
+Reconnecting=重连中
+Waiting to reconnect=等待重连
 Global=全局
 New=新建
 Start=开始任务
@@ -167,6 +169,7 @@ Tips: You can use the "noprefix" tag to ignore the prefix, "nosuffix" tag to ign
 Example: ${downspeed:noprefix:nosuffix:scale\=1}=示例: ${downspeed:noprefix:nosuffix:scale\=1}
 Updating Page Title Interval=页面标题更新间隔
 Enable Browser Notification=启用浏览器通知
+WebSocket Auto Reconnect Interval=WebSocket 自动重连时间
 Aria2 RPC Alias=Aria2 RPC 别名
 Aria2 RPC Address=Aria2 RPC 地址
 Aria2 RPC Protocol=Aria2 RPC 协议
@@ -232,6 +235,7 @@ Hide Secret=隐藏密钥
 Aria2 Version=Aria2 版本
 Enabled Features=已启用的功能
 Operations=操作
+Reconnect=重新连接
 Save Session=保存会话
 Shutdown Aria2=关闭 Aria2
 Confirm Shutdown=确认关闭

+ 4 - 0
app/langs/zh_Hant.txt

@@ -13,6 +13,8 @@ False=否
 Connecting=連線中
 Connected=已連線
 Disconnected=未連線
+Reconnecting=重連中
+Waiting to reconnect=等待重連
 Global=全域
 New=新增
 Start=開始工作
@@ -167,6 +169,7 @@ Tips: You can use the "noprefix" tag to ignore the prefix, "nosuffix" tag to ign
 Example: ${downspeed:noprefix:nosuffix:scale\=1}=示例: ${downspeed:noprefix:nosuffix:scale\=1}
 Updating Page Title Interval=頁面標題更新間隔
 Enable Browser Notification=啟用瀏覽器通知
+WebSocket Auto Reconnect Interval=WebSocket 自动重連線時間
 Aria2 RPC Alias=Aria2 RPC 別名
 Aria2 RPC Address=Aria2 RPC 位址
 Aria2 RPC Protocol=Aria2 RPC 協定
@@ -232,6 +235,7 @@ Hide Secret=隱藏金鑰
 Aria2 Version=Aria2 版本
 Enabled Features=已啟用的功能
 Operations=操作
+Reconnect=重新連線
 Save Session=儲存會話
 Shutdown Aria2=關閉 Aria2
 Confirm Shutdown=確認關閉

+ 1 - 1
app/scripts/config/constants.js

@@ -14,7 +14,6 @@
         defaultSecureProtocol: 'https',
         defaultPathSeparator: '/',
         httpRequestTimeout: 20000,
-        websocketAutoReconnect: true,
         globalStatStorageCapacity: 120,
         taskStatStorageCapacity: 300,
         lazySaveTimeout: 500,
@@ -36,6 +35,7 @@
         httpMethod: 'POST',
         secret: '',
         extendRpcServers: [],
+        webSocketReconnectInterval: 5000,
         globalStatRefreshInterval: 1000,
         downloadTaskRefreshInterval: 1000,
         keyboardShortcuts: true,

+ 4 - 0
app/scripts/config/defaultLanguage.js

@@ -17,6 +17,8 @@
             'Connecting': 'Connecting',
             'Connected': 'Connected',
             'Disconnected': 'Disconnected',
+            'Reconnecting': 'Reconnecting',
+            'Waiting to reconnect': 'Waiting to reconnect',
             'Global': 'Global',
             'New': 'New',
             'Start': 'Start',
@@ -171,6 +173,7 @@
             'Example: ${downspeed:noprefix:nosuffix:scale=1}': 'Example: ${downspeed:noprefix:nosuffix:scale=1}',
             'Updating Page Title Interval': 'Updating Page Title Interval',
             'Enable Browser Notification': 'Enable Browser Notification',
+            'WebSocket Auto Reconnect Interval': 'WebSocket Auto Reconnect Interval',
             'Aria2 RPC Alias': 'Aria2 RPC Alias',
             'Aria2 RPC Address': 'Aria2 RPC Address',
             'Aria2 RPC Protocol': 'Aria2 RPC Protocol',
@@ -236,6 +239,7 @@
             'Aria2 Version': 'Aria2 Version',
             'Enabled Features': 'Enabled Features',
             'Operations': 'Operations',
+            'Reconnect': 'Reconnect',
             'Save Session': 'Save Session',
             'Shutdown Aria2': 'Shutdown Aria2',
             'Confirm Shutdown': 'Confirm Shutdown',

+ 6 - 0
app/scripts/controllers/settings-ariang.js

@@ -74,6 +74,7 @@
             nativeSettings: getNativeSettings(),
             sessionSettings: ariaNgSettingService.getAllSessionOptions(),
             rpcSettings: ariaNgSettingService.getAllRpcSettings(),
+            isSupportReconnect: aria2SettingService.canReconnect(),
             isSupportBlob: ariaNgFileService.isSupportBlob(),
             isSupportDarkMode: ariaNgSettingService.isBrowserSupportDarkMode(),
             importSettings: null,
@@ -209,6 +210,11 @@
             }
         };
 
+        $scope.setWebSocketReconnectInterval = function (value) {
+            setNeedRefreshPage();
+            ariaNgSettingService.setWebSocketReconnectInterval(value);
+        };
+
         $scope.setTitleRefreshInterval = function (value) {
             setNeedRefreshPage();
             ariaNgSettingService.setTitleRefreshInterval(value);

+ 18 - 13
app/scripts/controllers/status.js

@@ -1,11 +1,19 @@
 (function () {
     'use strict';
 
-    angular.module('ariaNg').controller('Aria2StatusController', ['$rootScope', '$scope', 'ariaNgLocalizationService', 'ariaNgSettingService', 'aria2SettingService', function ($rootScope, $scope, ariaNgLocalizationService, ariaNgSettingService, aria2SettingService) {
+    angular.module('ariaNg').controller('Aria2StatusController', ['$rootScope', '$scope', '$timeout', 'ariaNgLocalizationService', 'ariaNgSettingService', 'aria2SettingService', function ($rootScope, $scope, $timeout, ariaNgLocalizationService, ariaNgSettingService, aria2SettingService) {
         $scope.context = {
             host: ariaNgSettingService.getCurrentRpcUrl(),
-            status: 'Connecting',
-            serverStatus: null
+            serverStatus: null,
+            isSupportReconnect: aria2SettingService.canReconnect()
+        };
+
+        $scope.reconnect = function () {
+            if (!$scope.context.isSupportReconnect || ($rootScope.taskContext.rpcStatus !== 'Disconnected' && $rootScope.taskContext.rpcStatus !== 'Waiting to reconnect')) {
+                return;
+            }
+
+            aria2SettingService.reconnect();
         };
 
         $scope.saveSession = function () {
@@ -26,15 +34,12 @@
             }, true);
         };
 
-        $rootScope.loadPromise = (function () {
-            return aria2SettingService.getAria2Status(function (response) {
-                if (response.success) {
-                    $scope.context.status = 'Connected';
-                    $scope.context.serverStatus = response.data;
-                } else {
-                    $scope.context.status = 'Disconnected';
-                }
-            });
-        })();
+        aria2SettingService.getAria2Status(function (response) {
+            if (response.success) {
+                $scope.context.serverStatus = response.data;
+            }
+        });
+
+        $rootScope.loadPromise = $timeout(function () {}, 100);
     }]);
 }());

+ 27 - 7
app/scripts/core/root.js

@@ -1,7 +1,7 @@
 (function () {
     'use strict';
 
-    angular.module('ariaNg').run(['$window', '$rootScope', '$location', '$document', 'ariaNgCommonService', 'ariaNgLocalizationService', 'ariaNgLogService', 'ariaNgSettingService', 'aria2TaskService', 'ariaNgNativeElectronService', function ($window, $rootScope, $location, $document, ariaNgCommonService, ariaNgLocalizationService, ariaNgLogService, ariaNgSettingService, aria2TaskService, ariaNgNativeElectronService) {
+    angular.module('ariaNg').run(['$window', '$rootScope', '$location', '$document', '$timeout', 'ariaNgCommonService', 'ariaNgLocalizationService', 'ariaNgLogService', 'ariaNgSettingService', 'aria2TaskService', 'ariaNgNativeElectronService', function ($window, $rootScope, $location, $document, $timeout, ariaNgCommonService, ariaNgLocalizationService, ariaNgLogService, ariaNgSettingService, aria2TaskService, ariaNgNativeElectronService) {
         var autoRefreshAfterPageLoad = false;
 
         var isUrlMatchUrl2 = function (url, url2) {
@@ -547,15 +547,35 @@
         });
 
         aria2TaskService.onConnectionSuccess(function () {
-            if ($rootScope.taskContext.rpcStatus !== 'Connected') {
-                $rootScope.taskContext.rpcStatus = 'Connected';
-            }
+            $timeout(function () {
+                if ($rootScope.taskContext.rpcStatus !== 'Connected') {
+                    $rootScope.taskContext.rpcStatus = 'Connected';
+                }
+            });
         });
 
         aria2TaskService.onConnectionFailed(function () {
-            if ($rootScope.taskContext.rpcStatus !== 'Disconnected') {
-                $rootScope.taskContext.rpcStatus = 'Disconnected';
-            }
+            $timeout(function () {
+                if ($rootScope.taskContext.rpcStatus !== 'Disconnected') {
+                    $rootScope.taskContext.rpcStatus = 'Disconnected';
+                }
+            });
+        });
+
+        aria2TaskService.onConnectionReconnecting(function () {
+            $timeout(function () {
+                if ($rootScope.taskContext.rpcStatus !== 'Reconnecting') {
+                    $rootScope.taskContext.rpcStatus = 'Reconnecting';
+                }
+            });
+        });
+
+        aria2TaskService.onConnectionWaitingToReconnect(function () {
+            $timeout(function () {
+                if ($rootScope.taskContext.rpcStatus !== 'Waiting to reconnect') {
+                    $rootScope.taskContext.rpcStatus = 'Waiting to reconnect';
+                }
+            });
         });
 
         aria2TaskService.onTaskCompleted(function (event) {

+ 3 - 0
app/scripts/services/aria2HttpRpcService.js

@@ -116,6 +116,9 @@
                     }
                 });
             },
+            reconnect: function () {
+                //Not implement
+            },
             on: function (eventName, callback) {
                 //Not implement
             }

+ 25 - 0
app/scripts/services/aria2RpcService.js

@@ -11,6 +11,8 @@
         var onOperationErrorCallbacks = [];
         var onConnectionSuccessCallbacks = [];
         var onConnectionFailedCallbacks = [];
+        var onConnectionReconnectingCallbacks = [];
+        var onConnectionWaitingToReconnectCallbacks = [];
         var onDownloadStartCallbacks = [];
         var onDownloadPauseCallbacks = [];
         var onDownloadStopCallbacks = [];
@@ -49,6 +51,8 @@
                 requestBody: requestBody,
                 connectionSuccessCallback: requestContext.connectionSuccessCallback,
                 connectionFailedCallback: requestContext.connectionFailedCallback,
+                connectionReconnectingCallback: requestContext.connectionReconnectingCallback,
+                connectionWaitingToReconnectCallback: requestContext.connectionWaitingToReconnectCallback,
                 successCallback: requestContext.successCallback,
                 errorCallback: requestContext.errorCallback
             };
@@ -144,6 +148,14 @@
                 fireCustomEvent(onConnectionFailedCallbacks);
             };
 
+            context.connectionReconnectingCallback = function () {
+                fireCustomEvent(onConnectionReconnectingCallbacks);
+            };
+
+            context.connectionWaitingToReconnectCallback = function () {
+                fireCustomEvent(onConnectionWaitingToReconnectCallbacks);
+            };
+
             if (secret && !isSystemMethod) {
                 finalParams.push(aria2RpcConstants.rpcTokenPrefix + secret);
             }
@@ -294,6 +306,13 @@
 
                 return requestParams;
             },
+            canReconnect: function () {
+                return ariaNgSettingService.isCurrentRpcUseWebSocket();
+            },
+            reconnect: function (context) {
+                ariaNgLogService.info("[aria2RpcService.reconnect] reconnect now");
+                rpcImplementService.reconnect(buildRequestContext('', context));
+            },
             addUri: function (context, returnContextOnly) {
                 var urls = context.task.urls;
                 var options = buildRequestOptions(context.task.options, context);
@@ -498,6 +517,12 @@
             onConnectionFailed: function (context) {
                 onConnectionFailedCallbacks.push(context.callback);
             },
+            onConnectionReconnecting: function (context) {
+                onConnectionReconnectingCallbacks.push(context.callback);
+            },
+            onConnectionWaitingToReconnect: function (context) {
+                onConnectionWaitingToReconnectCallbacks.push(context.callback);
+            },
             onDownloadStart: function (context) {
                 onDownloadStartCallbacks.push(context.callback);
             },

+ 6 - 0
app/scripts/services/aria2SettingService.js

@@ -259,6 +259,12 @@
                     }
                 });
             },
+            canReconnect: function () {
+                return aria2RpcService.canReconnect();
+            },
+            reconnect: function () {
+                return aria2RpcService.reconnect({});
+            },
             saveSession: function (callback, silent) {
                 return aria2RpcService.saveSession({
                     silent: !!silent,

+ 19 - 1
app/scripts/services/aria2TaskService.js

@@ -920,7 +920,6 @@
                 aria2RpcService.onConnectionSuccess({
                     callback: callback
                 });
-
             },
             onConnectionFailed: function (callback) {
                 if (!callback) {
@@ -931,7 +930,26 @@
                 aria2RpcService.onConnectionFailed({
                     callback: callback
                 });
+            },
+            onConnectionReconnecting: function (callback) {
+                if (!callback) {
+                    ariaNgLogService.warn('[aria2TaskService.onConnectionReconnecting] callback is null');
+                    return;
+                }
+
+                aria2RpcService.onConnectionReconnecting({
+                    callback: callback
+                });
+            },
+            onConnectionWaitingToReconnect: function (callback) {
+                if (!callback) {
+                    ariaNgLogService.warn('[aria2TaskService.onConnectionWaitingToReconnect] callback is null');
+                    return;
+                }
 
+                aria2RpcService.onConnectionWaitingToReconnect({
+                    callback: callback
+                });
             },
             onFirstSuccess: function (callback) {
                 if (!callback) {

+ 59 - 4
app/scripts/services/aria2WebSocketRpcService.js

@@ -1,9 +1,13 @@
 (function () {
     'use strict';
 
-    angular.module('ariaNg').factory('aria2WebSocketRpcService', ['$q', '$websocket', 'ariaNgConstants', 'ariaNgSettingService', 'ariaNgLogService', function ($q, $websocket, ariaNgConstants, ariaNgSettingService, ariaNgLogService) {
+    angular.module('ariaNg').factory('aria2WebSocketRpcService', ['$q', '$websocket', '$timeout', 'ariaNgConstants', 'ariaNgSettingService', 'ariaNgLogService', function ($q, $websocket, $timeout, ariaNgConstants, ariaNgSettingService, ariaNgLogService) {
+        var websocketStatusConnecting = 0;
+        var websocketStatusOpen = 1;
+
         var rpcUrl = ariaNgSettingService.getCurrentRpcUrl();
         var socketClient = null;
+        var pendingReconnect = null;
 
         var sendIdStates = {};
         var eventCallbacks = {};
@@ -73,7 +77,7 @@
             if (socketClient === null) {
                 try {
                     socketClient = $websocket(rpcUrl, {
-                        reconnectIfNotNormalClose: ariaNgConstants.websocketAutoReconnect
+                        maxTimeout: 1 // ms
                     });
 
                     socketClient.onMessage(function (message) {
@@ -107,7 +111,12 @@
                     socketClient.onClose(function (e) {
                         ariaNgLogService.warn('[aria2WebSocketRpcService.onClose] websocket is closed', e);
 
-                        if (context && context.connectionFailedCallback) {
+                        if (ariaNgSettingService.getWebSocketReconnectInterval() > 0 && context && context.connectionWaitingToReconnectCallback) {
+                            context.connectionWaitingToReconnectCallback({
+                                rpcUrl: rpcUrl
+                            });
+                            planToReconnect(context);
+                        } else if (context && context.connectionFailedCallback) {
                             context.connectionFailedCallback({
                                 rpcUrl: rpcUrl
                             });
@@ -128,6 +137,46 @@
             };
         };
 
+        var reconnect = function (context) {
+            if (!context || !socketClient) {
+                return;
+            }
+
+            if (context.connectionReconnectingCallback) {
+                context.connectionReconnectingCallback({
+                    rpcUrl: rpcUrl
+                });
+            }
+
+            socketClient.reconnect();
+        };
+
+        var planToReconnect = function (context) {
+            if (pendingReconnect) {
+                ariaNgLogService.warn('[aria2WebSocketRpcService.planToReconnect] another reconnection is pending');
+                return;
+            }
+
+            pendingReconnect = $timeout(function () {
+                if (socketClient == null) {
+                    ariaNgLogService.warn('[aria2WebSocketRpcService.planToReconnect] websocket is null');
+                    pendingReconnect = null;
+                    return;
+                }
+
+                if (socketClient.readyState === websocketStatusConnecting || socketClient.readyState === websocketStatusOpen) {
+                    ariaNgLogService.warn('[aria2WebSocketRpcService.planToReconnect] websocket current state is already ' + socketClient.readyState);
+                    pendingReconnect = null;
+                    return;
+                }
+
+                reconnect(context);
+                pendingReconnect = null;
+            }, ariaNgSettingService.getWebSocketReconnectInterval());
+
+            ariaNgLogService.debug('[aria2WebSocketRpcService.planToReconnect] next reconnection is pending in ' + ariaNgSettingService.getWebSocketReconnectInterval() + "ms");
+        }
+
         return {
             request: function (context) {
                 if (!context) {
@@ -135,7 +184,10 @@
                 }
 
                 var client = getSocketClient({
-                    connectionFailedCallback: context.connectionFailedCallback
+                    connectionSuccessCallback: context.connectionSuccessCallback,
+                    connectionFailedCallback: context.connectionFailedCallback,
+                    connectionReconnectingCallback: context.connectionReconnectingCallback,
+                    connectionWaitingToReconnectCallback: context.connectionWaitingToReconnectCallback
                 });
                 var uniqueId = context.uniqueId;
                 var requestBody = angular.toJson(context.requestBody);
@@ -163,6 +215,9 @@
 
                 return deferred.promise;
             },
+            reconnect: function (context) {
+                reconnect(context);
+            },
             on: function (eventName, callback) {
                 var callbacks = eventCallbacks[eventName];
 

+ 6 - 0
app/scripts/services/ariaNgSettingService.js

@@ -380,6 +380,12 @@
             setBrowserNotification: function (value) {
                 setOption('browserNotification', value);
             },
+            getWebSocketReconnectInterval: function () {
+                return getOption('webSocketReconnectInterval');
+            },
+            setWebSocketReconnectInterval: function (value) {
+                setOption('webSocketReconnectInterval', value);
+            },
             getTitleRefreshInterval: function () {
                 return getOption('titleRefreshInterval');
             },

+ 4 - 0
app/styles/theme/default.css

@@ -193,6 +193,10 @@
     }
 }
 
+.skin-aria-ng .sidebar .status-label {
+    max-width: 95px;
+}
+
 .skin-aria-ng .content-wrapper, .right-side {
     background-color: #fff;
 }

+ 13 - 0
app/views/settings-ariang.html

@@ -103,6 +103,19 @@
                             </select>
                         </div>
                     </div>
+                    <div class="row" ng-if="context.isSupportReconnect">
+                        <div class="setting-key setting-key-without-desc col-sm-4">
+                            <span translate>WebSocket Auto Reconnect Interval</span>
+                            <span class="asterisk">*</span>
+                        </div>
+                        <div class="setting-value col-sm-8">
+                            <select class="form-control" style="width: 100%;"
+                                    ng-model="context.settings.webSocketReconnectInterval"
+                                    ng-change="setWebSocketReconnectInterval(context.settings.webSocketReconnectInterval)"
+                                    ng-options="time.optionValue as (time.name | translate: {value: time.value}) for time in context.availableTime">
+                            </select>
+                        </div>
+                    </div>
                     <div class="row">
                         <div class="setting-key setting-key-without-desc col-sm-4">
                             <span translate>Updating Page Title Interval</span>

+ 19 - 9
app/views/status.html

@@ -13,24 +13,29 @@
                 <span translate>Aria2 Status</span>
             </div>
             <div class="setting-value col-sm-8">
-                <span class="label" ng-class="{'label-primary': context.status === 'Connecting', 'label-success': context.status === 'Connected', 'label-danger': context.status === 'Disconnected'}"
-                      ng-bind="context.status | translate"></span>
+                <span class="label" ng-class="{'label-primary': taskContext.rpcStatus === 'Connecting' || taskContext.rpcStatus === 'Reconnecting', 'label-default': taskContext.rpcStatus === 'Waiting to reconnect', 'label-success': taskContext.rpcStatus === 'Connected', 'label-danger': taskContext.rpcStatus === 'Disconnected'}"
+                      ng-bind="taskContext.rpcStatus | translate"></span>
             </div>
         </div>
-        <div class="row ng-cloak" ng-if="context.serverStatus">
+        <div class="row ng-cloak">
             <div class="setting-key col-sm-4">
                 <span translate>Aria2 Version</span>
             </div>
             <div class="setting-value col-sm-8">
-                <span ng-bind="context.serverStatus.version"></span>
+                <span ng-if="!context.serverStatus && (taskContext.rpcStatus === 'Connecting' || taskContext.rpcStatus === 'Reconnecting' || taskContext.rpcStatus === 'Connected')"><i class="fa fa-spinner fa-spin"></i></span>
+                <span ng-if="!context.serverStatus && !(taskContext.rpcStatus === 'Connecting' || taskContext.rpcStatus === 'Reconnecting' || taskContext.rpcStatus === 'Connected')">-</span>
+                <span ng-if="context.serverStatus" ng-bind="context.serverStatus.version"></span>
             </div>
         </div>
-        <div class="row ng-cloak" ng-if="context.serverStatus">
+        <div class="row ng-cloak">
             <div class="setting-key col-sm-4">
                 <span translate>Enabled Features</span>
             </div>
             <div class="setting-value col-sm-8">
-                <div class="checkbox checkbox-primary checkbox-compact default-cursor" ng-repeat="feature in context.serverStatus.enabledFeatures">
+                <span ng-if="!context.serverStatus && (taskContext.rpcStatus === 'Connecting' || taskContext.rpcStatus === 'Reconnecting' || taskContext.rpcStatus === 'Connected')"><i class="fa fa-spinner fa-spin"></i></span>
+                <span ng-if="!context.serverStatus && !(taskContext.rpcStatus === 'Connecting' || taskContext.rpcStatus === 'Reconnecting' || taskContext.rpcStatus === 'Connected')">-</span>
+                <div class="checkbox checkbox-primary checkbox-compact default-cursor" ng-if="context.serverStatus"
+                     ng-repeat="feature in context.serverStatus.enabledFeatures">
                     <input id="{{'feature_' + $index}}" type="checkbox" checked="checked" disabled="disabled" class="default-cursor"/>
                     <label for="{{'feature_' + $index}}" class="text-cursor">
                         <span ng-bind="feature"></span>
@@ -38,13 +43,18 @@
                 </div>
             </div>
         </div>
-        <div class="row ng-cloak" ng-if="context.serverStatus">
+        <div class="row ng-cloak" ng-if="context.serverStatus || context.isSupportReconnect">
             <div class="setting-key setting-key-without-desc col-sm-4">
                 <span translate>Operations</span>
             </div>
             <div class="setting-value col-sm-8">
-                <button class="btn btn-sm btn-primary" ng-click="saveSession()" promise-btn><span translate>Save Session</span></button>
-                <button class="btn btn-sm btn-danger promise-btn-style" ng-click="shutdown()"><span translate>Shutdown Aria2</span></button>
+                <button class="btn btn-sm btn-primary" ng-if="context.isSupportReconnect"
+                        ng-disabled="taskContext.rpcStatus !== 'Disconnected' && taskContext.rpcStatus !== 'Waiting to reconnect'"
+                        ng-click="reconnect()" promise-btn><span translate>Reconnect</span></button>
+                <button class="btn btn-sm btn-primary" ng-if="context.serverStatus"
+                        ng-click="saveSession()" promise-btn><span translate>Save Session</span></button>
+                <button class="btn btn-sm btn-danger promise-btn-style" ng-if="context.serverStatus"
+                        ng-click="shutdown()"><span translate>Shutdown Aria2</span></button>
             </div>
         </div>
     </div>