diff --git a/client/source/sass/components/_icons.scss b/client/source/sass/components/_icons.scss
index 38af9ad8..9bc1288c 100644
--- a/client/source/sass/components/_icons.scss
+++ b/client/source/sass/components/_icons.scss
@@ -8,6 +8,16 @@
}
}
+@keyframes spinner-spin {
+ 0% {
+ transform: rotate(0);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
.icon {
&--eta {
@@ -69,4 +79,8 @@
}
}
}
+
+ &--spinner {
+ animation: spinner-spin 1.25s linear infinite;
+ }
}
diff --git a/client/source/sass/components/_progress-bar.scss b/client/source/sass/components/_progress-bar.scss
index 54a6bf11..277f93b4 100644
--- a/client/source/sass/components/_progress-bar.scss
+++ b/client/source/sass/components/_progress-bar.scss
@@ -1,12 +1,25 @@
+@keyframes candy-stripe {
+ 0% {
+ background-position: 0 0;
+ }
+
+ 100% {
+ background-position: 4px 0;
+ }
+}
+
+$progress-bar--height: 3px;
+
$progress-bar--background: #e3e5e5;
$progress-bar--background--selected: rgba(#fff, 0.5);
$progress-bar--background--selected--stopped: rgba(#fff, 0.5);
$progress-bar--fill: $green;
-$progress-bar--fill--stopped: #e3e5e5;
+$progress-bar--fill--checking: #8899a8;
$progress-bar--fill--completed: $blue;
-$progress-bar--fill--selected: #fff;
$progress-bar--fill--error: #e95779;
+$progress-bar--fill--selected: #fff;
+$progress-bar--fill--stopped: #e3e5e5;
.progress-bar {
display: flex;
@@ -42,6 +55,10 @@ $progress-bar--fill--error: #e95779;
fill: $progress-bar--fill--error;
}
+ .is-checking & {
+ fill: $progress-bar--fill--checking;
+ }
+
.is-selected & {
fill: $progress-bar--fill--selected;
}
@@ -52,7 +69,7 @@ $progress-bar--fill--error: #e95779;
align-items: center;
background: $progress-bar--fill;
bottom: 0;
- height: 3px;
+ height: $progress-bar--height;
left: 0;
position: absolute;
top: 50%;
@@ -73,6 +90,10 @@ $progress-bar--fill--error: #e95779;
background: $progress-bar--fill--error;
}
+ .is-checking & {
+ background: $progress-bar--fill--checking;
+ }
+
.is-selected & {
background: $progress-bar--fill--selected;
}
@@ -110,6 +131,26 @@ $progress-bar--fill--error: #e95779;
background: $progress-bar--background--selected--stopped;
opacity: 1;
}
+
+ .is-checking & {
+ animation: candy-stripe 0.25s linear infinite;
+ background-color: transparent;
+ background-image: linear-gradient(-45deg, rgba($progress-bar--fill--checking, 0) 0,
+ rgba($progress-bar--fill--checking, 0) 25%, rgba($progress-bar--fill--checking, 0.5) 25%,
+ rgba($progress-bar--fill--checking, 0.5) 50%, rgba($progress-bar--fill--checking, 0) 50%,
+ rgba($progress-bar--fill--checking, 0) 75%, rgba($progress-bar--fill--checking, 0.5) 75%,
+ rgba($progress-bar--fill--checking, 0.5) 100%);
+ background-size: 4px 4px;
+ height: $progress-bar--height;
+ }
+
+ .is-selected.is-checking & {
+ background-image: linear-gradient(-45deg, rgba(#fff, 0) 0,
+ rgba(#fff, 0) 25%, rgba(#fff, 0.5) 25%,
+ rgba(#fff, 0.5) 50%, rgba(#fff, 0) 50%,
+ rgba(#fff, 0) 75%, rgba(#fff, 0.5) 75%,
+ rgba(#fff, 0.5) 100%);
+ }
}
}
}
diff --git a/client/source/scripts/actions/TorrentActions.js b/client/source/scripts/actions/TorrentActions.js
index 6ab67fb7..460ec0d2 100644
--- a/client/source/scripts/actions/TorrentActions.js
+++ b/client/source/scripts/actions/TorrentActions.js
@@ -79,6 +79,31 @@ const TorrentActions = {
});
},
+ checkHash: (hash) => {
+ return axios.post('/client/torrents/check-hash', {hash})
+ .then((json = {}) => {
+ return json.data;
+ })
+ .then((data) => {
+ AppDispatcher.dispatchServerAction({
+ type: ActionTypes.CLIENT_CHECK_HASH_SUCCESS,
+ data: {
+ data,
+ count: hash.length
+ }
+ });
+ })
+ .catch((error) => {
+ AppDispatcher.dispatchServerAction({
+ type: ActionTypes.CLIENT_CHECK_HASH_ERROR,
+ error: {
+ error,
+ count: hash.length
+ }
+ });
+ });
+ },
+
fetchTorrents: () => {
return axios.get('/client/torrents')
.then((json = {}) => {
diff --git a/client/source/scripts/components/icons/SpinnerIcon.js b/client/source/scripts/components/icons/SpinnerIcon.js
new file mode 100644
index 00000000..f7893f3b
--- /dev/null
+++ b/client/source/scripts/components/icons/SpinnerIcon.js
@@ -0,0 +1,32 @@
+import _ from 'lodash';
+import React from 'react';
+
+import BaseIcon from './BaseIcon';
+
+export default class SpinnerIcon extends BaseIcon {
+ constructor() {
+ super(...arguments);
+
+ this.id = _.uniqueId();
+ }
+
+ getViewBox() {
+ return '0 0 128 128';
+ }
+
+ render() {
+ let maskID = `icon--spinner__mask-id--${this.id}`;
+
+ return (
+
+ );
+ }
+}
diff --git a/client/source/scripts/components/torrent-list/TorrentList.js b/client/source/scripts/components/torrent-list/TorrentList.js
index e1424526..440daf0d 100644
--- a/client/source/scripts/components/torrent-list/TorrentList.js
+++ b/client/source/scripts/components/torrent-list/TorrentList.js
@@ -115,6 +115,10 @@ export default class TorrentListContainer extends React.Component {
action: 'remove',
clickHandler,
label: 'Remove'
+ }, {
+ action: 'check-hash',
+ clickHandler,
+ label: 'Check Hash'
}, {
type: 'separator'
}, {
@@ -137,6 +141,9 @@ export default class TorrentListContainer extends React.Component {
handleContextMenuItemClick(action, event) {
let selectedTorrents = TorrentStore.getSelectedTorrents();
switch (action) {
+ case 'check-hash':
+ TorrentActions.checkHash(selectedTorrents);
+ break;
case 'start':
TorrentActions.startTorrents(selectedTorrents);
break;
diff --git a/client/source/scripts/constants/ActionTypes.js b/client/source/scripts/constants/ActionTypes.js
index ae4630c1..c3ae0438 100644
--- a/client/source/scripts/constants/ActionTypes.js
+++ b/client/source/scripts/constants/ActionTypes.js
@@ -1,6 +1,8 @@
const ActionTypes = {
CLIENT_ADD_TORRENT_ERROR: 'CLIENT_ADD_TORRENT_ERROR',
CLIENT_ADD_TORRENT_SUCCESS: 'CLIENT_ADD_TORRENT_SUCCESS',
+ CLIENT_CHECK_HASH_ERROR: 'CLIENT_CHECK_HASH_ERROR',
+ CLIENT_CHECK_HASH_SUCCESS: 'CLIENT_CHECK_HASH_SUCCESS',
CLIENT_FETCH_TORRENT_STATUS_COUNT_REQUEST_ERROR: 'CLIENT_FETCH_TORRENT_STATUS_COUNT_REQUEST_ERROR',
CLIENT_FETCH_TORRENT_STATUS_COUNT_REQUEST_SUCCESS: 'CLIENT_FETCH_TORRENT_STATUS_COUNT_REQUEST_SUCCESS',
CLIENT_FETCH_TORRENT_TRACKER_COUNT_REQUEST_ERROR: 'CLIENT_FETCH_TORRENT_TRACKER_COUNT_REQUEST_ERROR',
diff --git a/client/source/scripts/stores/TorrentStore.js b/client/source/scripts/stores/TorrentStore.js
index 4237a676..374dd200 100644
--- a/client/source/scripts/stores/TorrentStore.js
+++ b/client/source/scripts/stores/TorrentStore.js
@@ -282,6 +282,7 @@ TorrentStore.dispatcherID = AppDispatcher.register((payload) => {
TorrentStore.handleAddTorrentError(action.error);
break;
case ActionTypes.CLIENT_ADD_TORRENT_SUCCESS:
+ TorrentStore.fetchTorrents();
TorrentStore.handleAddTorrentSuccess(action.data);
break;
case ActionTypes.CLIENT_FETCH_TORRENTS_SUCCESS:
@@ -313,9 +314,9 @@ TorrentStore.dispatcherID = AppDispatcher.register((payload) => {
case ActionTypes.UI_SET_TORRENT_SORT:
TorrentStore.triggerTorrentsFilter();
break;
- case ActionTypes.CLIENT_ADD_TORRENT_SUCCESS:
case ActionTypes.CLIENT_START_TORRENT_SUCCESS:
case ActionTypes.CLIENT_STOP_TORRENT_SUCCESS:
+ case ActionTypes.CLIENT_CHECK_HASH_SUCCESS:
TorrentStore.fetchTorrents();
break;
}
diff --git a/client/source/scripts/util/torrentStatusIcons.js b/client/source/scripts/util/torrentStatusIcons.js
index 570ef498..a05bdb5a 100644
--- a/client/source/scripts/util/torrentStatusIcons.js
+++ b/client/source/scripts/util/torrentStatusIcons.js
@@ -3,11 +3,13 @@ import React from 'react';
import ErrorIcon from '../components/icons/ErrorIcon';
import PauseIcon from '../components/icons/PauseIcon';
import propsMap from '../../../../shared/constants/propsMap';
+import SpinnerIcon from '../components/icons/SpinnerIcon';
import StartIcon from '../components/icons/StartIcon';
import StopIcon from '../components/icons/StopIcon';
const STATUS_ICON_MAP = {
error: ,
+ hashChecking: ,
stopped: ,
paused: ,
running:
@@ -33,6 +35,7 @@ export function torrentStatusIcons(status) {
if (condition) {
statusString = status;
}
+
return condition;
});
diff --git a/server/models/client.js b/server/models/client.js
index bdcc8ba4..c98910fd 100644
--- a/server/models/client.js
+++ b/server/models/client.js
@@ -54,6 +54,14 @@ var client = {
request.send();
},
+ checkHash: (hashes, callback) => {
+ let request = new ClientRequest();
+
+ request.add('checkHash', {hashes});
+ request.onComplete(callback);
+ request.send();
+ },
+
deleteTorrents: (hashes, callback) => {
let request = new ClientRequest();
diff --git a/server/routes/client.js b/server/routes/client.js
index 51fa5ab6..0035ec4a 100644
--- a/server/routes/client.js
+++ b/server/routes/client.js
@@ -60,6 +60,10 @@ router.patch('/torrents/:hash/file-priority', function(req, res, next) {
client.setFilePriority(req.params.hash, req.body, ajaxUtil.getResponseFn(res));
});
+router.post('/torrents/check-hash', function(req, res, next) {
+ client.checkHash(req.body.hash, ajaxUtil.getResponseFn(res));
+});
+
router.post('/torrents/move', function(req, res, next) {
client.moveTorrents(req.body, ajaxUtil.getResponseFn(res));
});