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)); });