diff --git a/client/source/scripts/stores/BaseStore.js b/client/source/scripts/stores/BaseStore.js new file mode 100644 index 00000000..37dcb4db --- /dev/null +++ b/client/source/scripts/stores/BaseStore.js @@ -0,0 +1,11 @@ +import {EventEmitter} from 'events'; + +export default class BaseStore extends EventEmitter { + listen(event, callback) { + this.on(event, callback); + } + + unlisten(event, callback) { + this.removeListener(event, callback); + } +} diff --git a/client/source/scripts/stores/ClientDataStore.js b/client/source/scripts/stores/ClientDataStore.js new file mode 100644 index 00000000..d888a560 --- /dev/null +++ b/client/source/scripts/stores/ClientDataStore.js @@ -0,0 +1,120 @@ +import ActionTypes from '../constants/ActionTypes'; +import AppDispatcher from '../dispatcher/AppDispatcher'; +import BaseStore from './BaseStore'; +import ClientActions from '../actions/ClientActions'; +import EventTypes from '../constants/EventTypes'; + +const historyLength = 100; +const pollInterval = 5000; + +class ClientDataStoreClass extends BaseStore { + constructor() { + super(); + + this.pollTransferDataID = null; + this.transferRates = {download: [], upload: []}; + this.transferTotals = {download: null, upload: null}; + } + + fetchTransferData() { + ClientActions.fetchTransferData(); + + if (this.pollTransferDataID === null) { + this.startPollingTransferData(); + } + } + + getTransferTotals() { + return this.transferTotals; + } + + getTransferRate() { + return this.transferRate; + } + + getTransferRates() { + return this.transferRates; + } + + handleTransferDataSuccess(transferData) { + this.transferTotals = { + download: transferData.downloadTotal, + upload: transferData.uploadTotal + }; + + this.transferRate = { + download: transferData.downloadRate, + upload: transferData.uploadRate + }; + + // add the latest download & upload rates to the end of the array and remove + // the first element in the array. if the arrays are empty, fill in zeros + // for the first n entries. + let index = 0; + let downloadRateHistory = Object.assign([], this.transferRates.download); + let uploadRateHistory = Object.assign([], this.transferRates.upload); + + if (uploadRateHistory.length === historyLength) { + downloadRateHistory.shift(); + uploadRateHistory.shift(); + downloadRateHistory.push(parseInt(transferData.downloadRate)); + uploadRateHistory.push(parseInt(transferData.uploadRate)); + } else { + while (index < historyLength) { + if (index < historyLength - 1) { + uploadRateHistory[index] = 0; + downloadRateHistory[index] = 0; + } else { + downloadRateHistory[index] = parseInt(transferData.downloadRate); + uploadRateHistory[index] = parseInt(transferData.uploadRate); + } + index++; + } + } + + this.transferRates = { + download: downloadRateHistory, + upload: uploadRateHistory + }; + + this.emit(EventTypes.CLIENT_TRANSFER_DATA_REQUEST_SUCCESS); + } + + handleTransferDataError() { + this.emit(EventTypes.CLIENT_TRANSFER_DATA_REQUEST_ERROR); + } + + startPollingTransferData() { + this.pollTransferDataID = setInterval( + this.fetchTransferData.bind(this), + pollInterval + ); + } + + startPollingTorrents() { + clearInterval(this.pollTransferDataID); + this.pollTransferDataID = null; + } + + stopPollingTorrentDetails() { + clearInterval(this.pollTorrentDetailsIntervalID); + this.isPollingTorrents = false; + } +} + +const ClientDataStore = new ClientDataStoreClass(); + +AppDispatcher.register((payload) => { + const {action, source} = payload; + + switch (action.type) { + case ActionTypes.CLIENT_FETCH_TRANSFER_DATA_SUCCESS: + ClientDataStore.handleTransferDataSuccess(action.data.transferData); + break; + case ActionTypes.CLIENT_FETCH_TRANSFER_DATA_ERROR: + ClientDataStore.handleTransferDataError(action.data.error); + break; + } +}); + +export default ClientDataStore; diff --git a/client/source/scripts/stores/TorrentFilterStore.js b/client/source/scripts/stores/TorrentFilterStore.js new file mode 100644 index 00000000..f1f8cc6d --- /dev/null +++ b/client/source/scripts/stores/TorrentFilterStore.js @@ -0,0 +1,66 @@ +import ActionTypes from '../constants/ActionTypes'; +import AppDispatcher from '../dispatcher/AppDispatcher'; +import BaseStore from './BaseStore'; +import TorrentActions from '../actions/TorrentActions'; +import EventTypes from '../constants/EventTypes'; + +class TorrentFilterStoreClass extends BaseStore { + constructor() { + super(); + + this.searchFilter = null; + this.statusFilter = 'all'; + this.sortTorrentsBy = { + direction: 'desc', + displayName: 'Date Added', + property: 'added' + }; + } + + getStatusFilter() { + return this.statusFilter; + } + + setStatusFilter(filter) { + this.statusFilter = filter; + this.emit(EventTypes.UI_TORRENTS_FILTER_STATUS_CHANGE); + } + + getSearchFilter() { + return this.searchFilter; + } + + setSearchFilter(filter) { + this.searchFilter = filter; + this.emit(EventTypes.UI_TORRENTS_FILTER_SEARCH_CHANGE); + } + + getTorrentsSort() { + return this.sortTorrentsBy; + } + + setTorrentsSort(sortBy) { + this.sortTorrentsBy = sortBy; + this.emit(EventTypes.UI_TORRENTS_SORT_CHANGE) + } +} + +const TorrentFilterStore = new TorrentFilterStoreClass(); + +AppDispatcher.register((payload) => { + const {action, source} = payload; + + switch (action.type) { + case ActionTypes.UI_SET_TORRENT_SEARCH_FILTER: + TorrentFilterStore.setSearchFilter(action.data); + break; + case ActionTypes.UI_SET_TORRENT_STATUS_FILTER: + TorrentFilterStore.setStatusFilter(action.data); + break; + case ActionTypes.UI_SET_TORRENT_SORT: + TorrentFilterStore.setTorrentsSort(action.data); + break; + } +}); + +export default TorrentFilterStore; diff --git a/client/source/scripts/stores/TorrentStore.js b/client/source/scripts/stores/TorrentStore.js new file mode 100644 index 00000000..ca9ff754 --- /dev/null +++ b/client/source/scripts/stores/TorrentStore.js @@ -0,0 +1,172 @@ +import _ from 'lodash'; + +import ActionTypes from '../constants/ActionTypes'; +import AppDispatcher from '../dispatcher/AppDispatcher'; +import BaseStore from './BaseStore'; +import EventTypes from '../constants/EventTypes'; +import {filterTorrents} from '../util/filterTorrents'; +import {searchTorrents} from '../util/searchTorrents'; +import {selectTorrents} from '../util/selectTorrents'; +import {sortTorrents} from '../util/sortTorrents'; +import TorrentActions from '../actions/TorrentActions'; +import TorrentFilterStore from './TorrentFilterStore'; +import UIStore from './UIStore'; + +const pollInterval = 5000; + +class TorrentStoreClass extends BaseStore { + constructor() { + super(); + + this.pollTorrentDetailsIntervalID = null; + this.pollTorrentsIntervalID = null; + this.isPollingTorrentDetails = false; + this.isPollingTorrents = false; + this.selectedTorrents = []; + this.torrentDetails = {}; + this.torrents = []; + } + + fetchTorrentDetails() { + TorrentActions.fetchTorrentDetails(UIStore.getTorrentDetailsHash()); + if (!this.isPollingTorrentDetails) { + this.stopPollingTorrentDetails(); + this.startPollingTorrentDetails(); + } + } + + fetchTorrents() { + TorrentActions.fetchTorrents(); + + if (!this.isPollingTorrents) { + this.startPollingTorrents(); + } + } + + getTorrentDetails(hash) { + return this.torrentDetails[hash] || {}; + } + + setTorrentDetails(hash, torrentDetails) { + this.torrentDetails[hash] = torrentDetails; + this.emit(EventTypes.CLIENT_TORRENT_DETAILS_CHANGE); + } + + getSelectedTorrents() { + return this.selectedTorrents; + } + + setSelectedTorrents(event, hash) { + this.selectedTorrents = selectTorrents({ + event, + hash, + selectedTorrents: this.selectedTorrents, + torrentList: this.torrents + }); + this.emit(EventTypes.UI_TORRENT_SELECTION_CHANGE); + } + + getTorrent(hash) { + return _.find(this.torrents, (torrent) => { + return torrent.hash === hash; + }); + } + + getTorrents() { + if (TorrentFilterStore.getStatusFilter() || + TorrentFilterStore.getSearchFilter()) { + return this.filteredTorrents; + } + + return this.torrents; + } + + setTorrents(torrents) { + this.torrents = sortTorrents( + Object.assign([], torrents), + TorrentFilterStore.getTorrentsSort() + ); + + let statusFilter = TorrentFilterStore.getStatusFilter(); + let searchFilter = TorrentFilterStore.getSearchFilter(); + + if (statusFilter || searchFilter) { + let filteredTorrents = Object.assign([], this.torrents); + + if (statusFilter && statusFilter !== 'all') { + filteredTorrents = filterTorrents(filteredTorrents, statusFilter); + } + + if (searchFilter && searchFilter !== '') { + filteredTorrents = searchTorrents(filteredTorrents, searchFilter); + } + + this.filteredTorrents = filteredTorrents; + } + + this.emit(EventTypes.CLIENT_TORRENTS_REQUEST_SUCCESS); + } + + startPollingTorrentDetails() { + this.isPollingTorrentDetails = true; + this.pollTorrentDetailsIntervalID = setInterval( + this.fetchTorrentDetails.bind(this), + pollInterval + ); + } + + startPollingTorrents() { + this.isPollingTorrents = true; + this.pollTorrentsIntervalID = setInterval( + this.fetchTorrents.bind(this), + pollInterval + ); + } + + stopPollingTorrentDetails() { + clearInterval(this.pollTorrentDetailsIntervalID); + this.isPollingTorrents = false; + } + + stopPollingTorrents() { + clearInterval(this.pollTorrentsIntervalID); + this.isPollingTorrentDetails = false; + } + + triggerTorrentsFilter() { + this.setTorrents(this.torrents); + } +} + +const TorrentStore = new TorrentStoreClass(); + +AppDispatcher.register((payload) => { + const {action, source} = payload; + + switch (action.type) { + case ActionTypes.CLIENT_FETCH_TORRENT_DETAILS_SUCCESS: + TorrentStore.setTorrentDetails(action.data.hash, action.data.torrentDetails); + break; + case ActionTypes.CLIENT_FETCH_TORRENTS_SUCCESS: + TorrentStore.setTorrents(action.data.torrents); + break; + case ActionTypes.CLIENT_FETCH_TORRENTS_ERROR: + console.log(action); + break; + case ActionTypes.UI_CLICK_TORRENT: + TorrentStore.setSelectedTorrents(action.data.event, action.data.hash); + break; + case ActionTypes.UI_SET_TORRENT_STATUS_FILTER: + case ActionTypes.UI_SET_TORRENT_SEARCH_FILTER: + 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: + TorrentStore.fetchTorrents(); + break; + } +}); + +export default TorrentStore; diff --git a/client/source/scripts/stores/UIStore.js b/client/source/scripts/stores/UIStore.js new file mode 100644 index 00000000..69887a8a --- /dev/null +++ b/client/source/scripts/stores/UIStore.js @@ -0,0 +1,63 @@ +import ActionTypes from '../constants/ActionTypes'; +import AppDispatcher from '../dispatcher/AppDispatcher'; +import BaseStore from './BaseStore'; +import EventTypes from '../constants/EventTypes'; +import {selectTorrents} from '../util/selectTorrents'; +import TorrentActions from '../actions/TorrentActions'; + +class UIStoreClass extends BaseStore { + constructor() { + super(); + + this.activeModal = null; + this.torrentDetailsHash = null; + this.torrentDetailsOpen = false; + } + + getActiveModal() { + return this.activeModal; + } + + setActiveModal(modal) { + this.activeModal = modal; + this.emit(EventTypes.UI_MODAL_CHANGE); + } + + getTorrentDetailsHash() { + return this.torrentDetailsHash; + } + + handleTorrentClick(hash) { + this.torrentDetailsHash = hash; + this.emit(EventTypes.UI_TORRENT_DETAILS_HASH_CHANGE); + } + + handleTorrentDetailsClick(hash, event) { + this.torrentDetailsOpen = !this.torrentDetailsOpen; + this.emit(EventTypes.UI_TORRENT_DETAILS_OPEN_CHANGE); + } + + isTorrentDetailsOpen() { + return this.torrentDetailsOpen; + } +} + +const UIStore = new UIStoreClass(); + +AppDispatcher.register((payload) => { + const {action, source} = payload; + + switch (action.type) { + case ActionTypes.UI_CLICK_TORRENT: + UIStore.handleTorrentClick(action.data.hash); + break; + case ActionTypes.UI_CLICK_TORRENT_DETAILS: + UIStore.handleTorrentDetailsClick(action.data.hash, action.data.event); + break; + case ActionTypes.UI_DISPLAY_MODAL: + UIStore.setActiveModal(action.data); + break; + } +}); + +export default UIStore;