From 02c3b5e4b08f64d7619fda1ded15332b6ebc651e Mon Sep 17 00:00:00 2001 From: John Furrow Date: Mon, 6 Jun 2016 21:01:10 -0700 Subject: [PATCH] Introduce client settings --- client/source/sass/base/_form-elements.scss | 60 +++++- client/source/sass/components/_modals.scss | 34 ++- client/source/sass/components/_torrents.scss | 2 +- .../source/scripts/actions/ClientActions.js | 42 +++- .../source/scripts/actions/SettingsActions.js | 2 - .../source/scripts/actions/TorrentActions.js | 1 - client/source/scripts/app.js | 3 +- .../components/modals/AddTorrentsActions.js | 5 +- .../modals/AddTorrentsDestination.js | 3 +- .../components/modals/SettingsModal.js | 75 +++++-- .../components/settings/BandwidthTab.js | 195 ++++++++++++++++++ .../components/settings/ConnectivityTab.js | 115 +++++++++++ .../components/settings/SettingsTab.js | 36 ++++ .../components/settings/SpeedLimitTab.js | 133 ------------ .../scripts/components/settings/StorageTab.js | 35 ++++ .../components/sidebar/SpeedLimitDropdown.js | 6 +- .../components/torrent-list/ActionBar.js | 8 +- .../source/scripts/constants/ActionTypes.js | 5 + client/source/scripts/constants/EventTypes.js | 4 + client/source/scripts/stores/SettingsStore.js | 118 +++++++++-- .../scripts/stores/TorrentFilterStore.js | 2 +- client/source/scripts/stores/TorrentStore.js | 2 +- server/models/ClientRequest.js | 38 +++- server/models/client.js | 45 +++- server/routes/client.js | 26 ++- shared/constants/clientSettingsMap.js | 105 ++++++++++ shared/util/objectUtil.js | 13 ++ 27 files changed, 907 insertions(+), 206 deletions(-) create mode 100644 client/source/scripts/components/settings/BandwidthTab.js create mode 100644 client/source/scripts/components/settings/ConnectivityTab.js create mode 100644 client/source/scripts/components/settings/SettingsTab.js delete mode 100644 client/source/scripts/components/settings/SpeedLimitTab.js create mode 100644 client/source/scripts/components/settings/StorageTab.js create mode 100644 shared/constants/clientSettingsMap.js create mode 100644 shared/util/objectUtil.js diff --git a/client/source/sass/base/_form-elements.scss b/client/source/sass/base/_form-elements.scss index fb5e7b87..7aedcbac 100644 --- a/client/source/sass/base/_form-elements.scss +++ b/client/source/sass/base/_form-elements.scss @@ -30,6 +30,11 @@ $checkbox--border--hover: $checkbox--border; $modal--body--foreground: desaturate(lighten($foreground, 20%), 10%); +$form--section--heading--margin: $spacing-unit * 2/5; +$form--section--margin: $spacing-unit; +$form--row--margin: $spacing-unit * 3/5; +$form--column--padding: $spacing-unit * 2/5; + .textbox, .button, .checkbox { @@ -191,17 +196,36 @@ $modal--body--foreground: desaturate(lighten($foreground, 20%), 10%); .form { + &__section { + + &__heading { + margin-bottom: $form--section--heading--margin; + + & + .form__section__sub-heading { + margin-bottom: $form--section--heading--margin; + margin-top: $form--section--heading--margin * -1; + } + } + + & + .form__section { + margin-top: $form--section--margin; + } + } + &__row { display: flex; & + .form__row { - margin-top: $spacing-unit; + margin-top: $form--row--margin; } } &__column { + display: flex; flex: 1; - padding: 0 $spacing-unit * 2/5; + flex-direction: column; + justify-content: flex-end; + padding: 0 $form--column--padding; &:first-child { padding-left: 0; @@ -210,6 +234,38 @@ $modal--body--foreground: desaturate(lighten($foreground, 20%), 10%); &:last-child { padding-right: 0; } + + &--auto { + flex-grow: 0; + flex-shrink: 1; + flex-basis: auto; + } + + &--half { + max-width: 50%; + + &:last-child { + padding-right: $form--column--padding; + } + } + + &--small { + max-width: 125px; + + // For small columns which are the only column in the row, keep the + // column's padding. + &:first-child { + + &:last-child { + padding-right: $form--column--padding; + } + } + } + + &--unlabled { + justify-content: center; + padding-top: $spacing-unit * 3/5; + } } &__label { diff --git a/client/source/sass/components/_modals.scss b/client/source/sass/components/_modals.scss index 78cb2b5e..e1e3f0c0 100644 --- a/client/source/sass/components/_modals.scss +++ b/client/source/sass/components/_modals.scss @@ -1,8 +1,11 @@ $modal--background: #12191f; + $modal--heading--background: #161c24; $modal--heading--foreground: #7d95ab; $modal--heading--border: #0f151b; +$modal--sub-heading--foreground: desaturate(darken($modal--heading--foreground, 23%), 2%); + $modal--transition--duration: 0.5s; $modal--transition--scale: 0.85; @@ -106,15 +109,6 @@ $modal--tabs--in-body--background: #11171d; } } - &__tab { - - &__introduction { - color: $modal--heading--foreground; - font-size: 0.9em; - margin-bottom: $spacing-unit * 3/4; - } - } - &__header { background: $modal--heading--background; border-radius: $modal--border-radius $modal--border-radius 0 0; @@ -173,6 +167,7 @@ $modal--tabs--in-body--background: #11171d; } &__footer { + flex: 0 0 auto; padding: 0 $modal--padding--horizontal $modal--padding--vertical $modal--padding--horizontal; .modal { @@ -283,6 +278,10 @@ $modal--tabs--in-body--background: #11171d; &__content { flex: 1 0 auto; + + & + .modal__footer { + margin-top: $spacing-unit * 3/5; + } } } } @@ -372,4 +371,21 @@ $modal--tabs--in-body--background: #11171d; } } } + + .form { + + &__section { + + &__heading { + color: $modal--heading--foreground; + font-size: 0.9em; + font-weight: 500; + } + + &__sub-heading { + color: $modal--sub-heading--foreground; + font-size: 0.8em; + } + } + } } diff --git a/client/source/sass/components/_torrents.scss b/client/source/sass/components/_torrents.scss index 94241b37..fd47df8c 100644 --- a/client/source/sass/components/_torrents.scss +++ b/client/source/sass/components/_torrents.scss @@ -369,7 +369,7 @@ $more-info--border: $textbox-repeater--button--border; &--eta { opacity: 0; - transition: opacity 1s, visibility 1s; + transition: color 0.25s, opacity 1s, visibility 1s; visibility: hidden; .torrent__details--segment { diff --git a/client/source/scripts/actions/ClientActions.js b/client/source/scripts/actions/ClientActions.js index 286007a4..2a6bed50 100644 --- a/client/source/scripts/actions/ClientActions.js +++ b/client/source/scripts/actions/ClientActions.js @@ -1,9 +1,49 @@ import axios from 'axios'; -import AppDispatcher from '../dispatcher/AppDispatcher'; import ActionTypes from '../constants/ActionTypes'; +import AppDispatcher from '../dispatcher/AppDispatcher'; const ClientActions = { + fetchSettings: (property) => { + return axios.get('/client/settings', {params: {property}}) + .then((json = {}) => { + return json.data; + }) + .then((data) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.CLIENT_SETTINGS_FETCH_REQUEST_SUCCESS, + data + }); + }) + .catch((error) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.CLIENT_SETTINGS_FETCH_REQUEST_ERROR, + error + }); + }); + }, + + saveSettings: (settings, options) => { + return axios.patch('/client/settings', settings) + .then((json = {}) => { + return json.data; + }) + .then((data) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.CLIENT_SETTINGS_SAVE_SUCCESS, + data, + options + }); + }) + .catch((error) => { + AppDispatcher.dispatchServerAction({ + type: ActionTypes.CLIENT_SETTINGS_SAVE_ERROR, + error, + options + }); + }); + }, + setThrottle: (direction, throttle) => { return axios.put('/client/settings/speed-limits', { direction, diff --git a/client/source/scripts/actions/SettingsActions.js b/client/source/scripts/actions/SettingsActions.js index 9e96de4f..26474ca3 100644 --- a/client/source/scripts/actions/SettingsActions.js +++ b/client/source/scripts/actions/SettingsActions.js @@ -16,7 +16,6 @@ const SettingsActions = { }); }) .catch((error) => { - console.trace(error); AppDispatcher.dispatchServerAction({ type: ActionTypes.SETTINGS_FETCH_REQUEST_ERROR, error @@ -37,7 +36,6 @@ const SettingsActions = { }); }) .catch((error) => { - console.trace(error); AppDispatcher.dispatchServerAction({ type: ActionTypes.SETTINGS_SAVE_REQUEST_ERROR, error diff --git a/client/source/scripts/actions/TorrentActions.js b/client/source/scripts/actions/TorrentActions.js index cd577201..6ab67fb7 100644 --- a/client/source/scripts/actions/TorrentActions.js +++ b/client/source/scripts/actions/TorrentActions.js @@ -93,7 +93,6 @@ const TorrentActions = { }); }) .catch((error) => { - console.trace(error); AppDispatcher.dispatchServerAction({ type: ActionTypes.CLIENT_FETCH_TORRENTS_ERROR, data: { diff --git a/client/source/scripts/app.js b/client/source/scripts/app.js index dee98cfd..455a04f8 100644 --- a/client/source/scripts/app.js +++ b/client/source/scripts/app.js @@ -13,7 +13,8 @@ import TorrentListView from './components/panels/TorrentListView'; class FloodApp extends React.Component { componentDidMount() { - SettingsStore.fetchSettings(); + SettingsStore.fetchClientSettings(); + SettingsStore.fetchFloodSettings(); } render() { diff --git a/client/source/scripts/components/modals/AddTorrentsActions.js b/client/source/scripts/components/modals/AddTorrentsActions.js index 0637a6ca..44faad71 100644 --- a/client/source/scripts/components/modals/AddTorrentsActions.js +++ b/client/source/scripts/components/modals/AddTorrentsActions.js @@ -20,7 +20,8 @@ export default class AddTorrentsActions extends React.Component { } componentWillMount() { - let startTorrentsOnLoad = SettingsStore.getSettings('startTorrentsOnLoad'); + let startTorrentsOnLoad = SettingsStore.getFloodSettings( + 'startTorrentsOnLoad'); if (startTorrentsOnLoad !== true) { this.setState({startTorrentsOnLoad: false}); } @@ -65,7 +66,7 @@ export default class AddTorrentsActions extends React.Component { } handleStartTorrentsToggle(value) { - SettingsStore.saveSettings({id: 'startTorrentsOnLoad', data: value}); + SettingsStore.saveFloodSettings({id: 'startTorrentsOnLoad', data: value}); if (!!this.props.onStartTorrentsToggle) { this.props.onStartTorrentsToggle(value); } diff --git a/client/source/scripts/components/modals/AddTorrentsDestination.js b/client/source/scripts/components/modals/AddTorrentsDestination.js index 0e225bd3..3410780d 100644 --- a/client/source/scripts/components/modals/AddTorrentsDestination.js +++ b/client/source/scripts/components/modals/AddTorrentsDestination.js @@ -20,7 +20,8 @@ export default class AddTorrentsDestination extends React.Component { } componentWillMount() { - let destination = SettingsStore.getSettings('torrentDestination') || ''; + let destination = SettingsStore.getFloodSettings('torrentDestination') + || ''; if (this.props.suggested) { destination = this.props.suggested; } diff --git a/client/source/scripts/components/modals/SettingsModal.js b/client/source/scripts/components/modals/SettingsModal.js index 8f6de57f..c0690965 100644 --- a/client/source/scripts/components/modals/SettingsModal.js +++ b/client/source/scripts/components/modals/SettingsModal.js @@ -1,16 +1,19 @@ import classnames from 'classnames'; import React from 'react'; +import BandwidthTab from '../settings/BandwidthTab'; +import ConnectivityTab from '../settings/ConnectivityTab'; import EventTypes from '../../constants/EventTypes'; import LoadingIndicatorDots from '../icons/LoadingIndicatorDots'; import Modal from './Modal'; import SettingsStore from '../../stores/SettingsStore'; -import SpeedLimitTab from '../settings/SpeedLimitTab'; +import StorageTab from '../settings/StorageTab'; const METHODS_TO_BIND = [ 'handleSaveSettingsClick', 'handleSaveSettingsError', - 'handleSettingsChange', + 'handleClientSettingsChange', + 'handleFloodSettingsChange', 'handleSettingsStoreChange' ]; @@ -20,7 +23,10 @@ export default class SettingsModal extends React.Component { this.state = { isSavingSettings: false, - settings: SettingsStore.getSettings() + changedClientSettings: {}, + changedFloodSettings: {}, + clientSettings: SettingsStore.getClientSettings(), + floodSettings: SettingsStore.getFloodSettings() }; METHODS_TO_BIND.forEach((method) => { @@ -33,12 +39,14 @@ export default class SettingsModal extends React.Component { this.handleSettingsStoreChange); SettingsStore.listen(EventTypes.SETTINGS_SAVE_REQUEST_ERROR, this.handleSaveSettingsError); - SettingsStore.fetchSettings('speedLimits'); + SettingsStore.fetchFloodSettings('speedLimits'); } componentWillUnmount() { SettingsStore.unlisten(EventTypes.SETTINGS_CHANGE, this.handleSettingsStoreChange); + SettingsStore.unlisten(EventTypes.SETTINGS_SAVE_REQUEST_ERROR, + this.handleSaveSettingsError); } getActions() { @@ -75,14 +83,22 @@ export default class SettingsModal extends React.Component { handleSaveSettingsClick() { this.setState({isSavingSettings: true}); - let settingsToSave = Object.keys(this.state.settings).map((settingsKey) => { + let floodSettings = Object.keys(this.state.changedFloodSettings).map((settingsKey) => { return { id: settingsKey, - data: this.state.settings[settingsKey] + data: this.state.changedFloodSettings[settingsKey] }; }); - SettingsStore.saveSettings(settingsToSave, {dismissModal: true, notify: true}); + let clientSettings = Object.keys(this.state.changedClientSettings).map((settingsKey) => { + return { + id: settingsKey, + data: this.state.changedClientSettings[settingsKey] + }; + }); + + SettingsStore.saveFloodSettings(floodSettings, {dismissModal: true, notify: true}); + SettingsStore.saveClientSettings(clientSettings, {dismissModal: true, notify: true}); } handleSaveSettingsError() { @@ -95,13 +111,23 @@ export default class SettingsModal extends React.Component { handleSettingsStoreChange() { this.setState({ - settings: SettingsStore.getSettings() + clientSettings: SettingsStore.getClientSettings(), + floodSettings: SettingsStore.getFloodSettings() }); } - handleSettingsChange(changedSettings) { - let settings = this.mergeObjects(this.state.settings, changedSettings); - this.setState({settings}); + handleFloodSettingsChange(changedSettings) { + let floodSettings = this.mergeObjects(this.state.floodSettings, changedSettings); + let changedFloodSettings = this.mergeObjects(this.state.changedFloodSettings, changedSettings); + + this.setState({floodSettings, changedFloodSettings}); + } + + handleClientSettingsChange(changedSettings) { + let clientSettings = this.mergeObjects(this.state.clientSettings, changedSettings); + let changedClientSettings = this.mergeObjects(this.state.changedClientSettings, changedSettings); + + this.setState({clientSettings, changedClientSettings}); } mergeObjects(objA, objB) { @@ -123,13 +149,30 @@ export default class SettingsModal extends React.Component { render() { let tabs = { - 'speed-limit': { - content: SpeedLimitTab, + bandwidth: { + content: BandwidthTab, props: { - onSettingsChange: this.handleSettingsChange, - settings: this.state.settings + onClientSettingsChange: this.handleClientSettingsChange, + onSettingsChange: this.handleFloodSettingsChange, + settings: this.mergeObjects(this.state.floodSettings, this.state.clientSettings) }, - label: 'Speed Limits' + label: 'Bandwidth' + }, + connectivity: { + content: ConnectivityTab, + props: { + onClientSettingsChange: this.handleClientSettingsChange, + settings: this.state.clientSettings + }, + label: 'Connectivity' + }, + storage: { + content: StorageTab, + props: { + onClientSettingsChange: this.handleClientSettingsChange, + settings: this.state.clientSettings + }, + label: 'Storage' } }; diff --git a/client/source/scripts/components/settings/BandwidthTab.js b/client/source/scripts/components/settings/BandwidthTab.js new file mode 100644 index 00000000..03b01dda --- /dev/null +++ b/client/source/scripts/components/settings/BandwidthTab.js @@ -0,0 +1,195 @@ +import React from 'react'; + +import SettingsTab from './SettingsTab'; + +const METHODS_TO_BIND = ['handleDownloadTextChange', 'handleUploadTextChange']; + +export default class BandwidthTab extends SettingsTab { + constructor() { + super(); + + this.state = { + downloadValue: null, + uploadValue: null + }; + + METHODS_TO_BIND.forEach((method) => { + this[method] = this[method].bind(this); + }); + } + + arrayToString(array) { + return array.join(', '); + } + + getTextboxValue(input = []) { + if (Array.isArray(input)) { + return this.arrayToString(input); + } + + return input; + } + + handleDownloadTextChange(event) { + this.setState({ + downloadValue: event.target.value + }); + + this.props.onSettingsChange({ + speedLimits: { + download: this.processSpeedsForSave(event.target.value), + upload: this.processSpeedsForSave(this.getUploadValue()) + } + }); + } + + handleUploadTextChange(event) { + this.setState({ + uploadValue: event.target.value + }); + + this.props.onSettingsChange({ + speedLimits: { + download: this.processSpeedsForSave(this.getDownloadValue()), + upload: this.processSpeedsForSave(event.target.value) + } + }); + } + + getDownloadValue() { + let displayedValue = this.state.downloadValue; + + if (displayedValue == null && this.props.settings.speedLimits != null) { + displayedValue = this.processSpeedsForDisplay(this.props.settings.speedLimits.download); + } + + return displayedValue; + } + + getUploadValue() { + let displayedValue = this.state.uploadValue; + + if (displayedValue == null && this.props.settings.speedLimits != null) { + displayedValue = this.processSpeedsForDisplay(this.props.settings.speedLimits.upload); + } + + return displayedValue; + } + + processSpeedsForDisplay(speeds = []) { + if (!speeds || speeds.length === 0) { + return; + } + + return this.arrayToString(speeds.map((speed) => { + return Number(speed) / 1024; + })); + } + + processSpeedsForSave(speeds = '') { + if (speeds === '') { + return []; + } + + return this.stringToArray(speeds).map((speed) => { + return Number(speed) * 1024; + }); + } + + stringToArray(string = '') { + return string.replace(/\s/g, '').split(','); + } + + render() { + let downloadValue = this.getDownloadValue() || 0; + let uploadValue = this.getUploadValue() || 0; + + return ( +
+
+

+ Speed Limit Dropdown Presets +

+

+ Enter a comma-separated list of speeds in kB. 0 represents unlimited. +

+
+
+ + +
+
+ + +
+
+
+
+
+ Slot Availability +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ ); + } +} diff --git a/client/source/scripts/components/settings/ConnectivityTab.js b/client/source/scripts/components/settings/ConnectivityTab.js new file mode 100644 index 00000000..6ce3d4eb --- /dev/null +++ b/client/source/scripts/components/settings/ConnectivityTab.js @@ -0,0 +1,115 @@ +import _ from 'lodash'; +import React from 'react'; + +import Checkbox from '../forms/Checkbox'; +import SettingsTab from './SettingsTab'; + +export default class ConnectivityTab extends SettingsTab { + constructor() { + super(...arguments); + + this.state = {}; + } + + render() { + return ( +
+
+
+ Listening Port +
+
+
+ + +
+
+ + Randomize Port + +
+
+ + Open Port + +
+
+
+
+
+ DHT +
+
+
+ + +
+
+
+
+
+ Peers +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ ); + } +} diff --git a/client/source/scripts/components/settings/SettingsTab.js b/client/source/scripts/components/settings/SettingsTab.js new file mode 100644 index 00000000..68b4d0da --- /dev/null +++ b/client/source/scripts/components/settings/SettingsTab.js @@ -0,0 +1,36 @@ +import React from 'react'; + +const METHODS_TO_BIND = ['handleClientSettingFieldChange']; + +export default class SettingsTab extends React.Component { + constructor() { + super(); + + METHODS_TO_BIND.forEach((method) => { + this[method] = this[method].bind(this); + }); + } + + getFieldValue(fieldName) { + if (this.state[fieldName] == null) { + return this.props.settings[fieldName] || ''; + } + + return this.state[fieldName]; + } + + handleClientSettingFieldChange(fieldName, event) { + let newState = {[fieldName]: event.target.value}; + + this.setState(newState); + this.props.onClientSettingsChange(newState); + } + + handleClientSettingCheckboxChange(fieldName, value) { + let checkedValue = value ? '1' : '0'; + let newState = {[fieldName]: checkedValue}; + + this.setState(newState); + this.props.onClientSettingsChange(newState); + } +} diff --git a/client/source/scripts/components/settings/SpeedLimitTab.js b/client/source/scripts/components/settings/SpeedLimitTab.js deleted file mode 100644 index 2d62a341..00000000 --- a/client/source/scripts/components/settings/SpeedLimitTab.js +++ /dev/null @@ -1,133 +0,0 @@ -import React from 'react'; - -const METHODS_TO_BIND = ['handleDownloadTextChange', 'handleUploadTextChange']; - -export default class SpeedLimitTab extends React.Component { - constructor() { - super(); - - this.state = { - downloadValue: null, - uploadValue: null - }; - - METHODS_TO_BIND.forEach((method) => { - this[method] = this[method].bind(this); - }); - } - - arrayToString(array) { - return array.join(', '); - } - - getTextboxValue(input = []) { - if (Array.isArray(input)) { - return this.arrayToString(input); - } - - return input; - } - - handleDownloadTextChange(event) { - this.setState({ - downloadValue: event.target.value - }); - - this.props.onSettingsChange({ - speedLimits: { - download: this.processSpeedsForSave(event.target.value) - } - }); - } - - handleUploadTextChange(event) { - this.setState({ - uploadValue: event.target.value - }); - - this.props.onSettingsChange({ - speedLimits: { - upload: this.processSpeedsForSave(event.target.value) - } - }); - } - - getDownloadValue() { - let displayedValue = this.state.downloadValue; - - if (displayedValue == null && this.props.settings.speedLimits != null) { - displayedValue = this.processSpeedsForDisplay(this.props.settings.speedLimits.download); - } - - return displayedValue; - } - - getUploadValue() { - let displayedValue = this.state.uploadValue; - - if (displayedValue == null && this.props.settings.speedLimits != null) { - displayedValue = this.processSpeedsForDisplay(this.props.settings.speedLimits.upload); - } - - return displayedValue; - } - - processSpeedsForDisplay(speeds = []) { - if (!speeds || speeds.length === 0) { - return; - } - - return this.arrayToString(speeds.map((speed) => { - return Number(speed) / 1024; - })); - } - - processSpeedsForSave(speeds = '') { - if (speeds === '') { - return []; - } - - return this.stringToArray(speeds).map((speed) => { - return Number(speed) * 1024; - }); - } - - stringToArray(string = '') { - return string.replace(/\s/g, '').split(','); - } - - render() { - let downloadValue = this.getDownloadValue() || 0; - let uploadValue = this.getUploadValue() || 0; - - return ( -
-

- Provide a comma-separated list of speed values (in kilobytes per second). 0 represents unlimited. -

-
-
-
- - -
-
-
-
- - -
-
-
-
- ); - } -} diff --git a/client/source/scripts/components/settings/StorageTab.js b/client/source/scripts/components/settings/StorageTab.js new file mode 100644 index 00000000..044e750e --- /dev/null +++ b/client/source/scripts/components/settings/StorageTab.js @@ -0,0 +1,35 @@ +import _ from 'lodash'; +import React from 'react'; + +import Checkbox from '../forms/Checkbox'; +import SettingsTab from './SettingsTab'; + +export default class StorageTab extends SettingsTab { + constructor() { + super(...arguments); + + this.state = {}; + } + + render() { + return ( +
+
+
+ Directories +
+
+
+ + +
+
+
+
+ ); + } +} diff --git a/client/source/scripts/components/sidebar/SpeedLimitDropdown.js b/client/source/scripts/components/sidebar/SpeedLimitDropdown.js index fb10cb61..a8cacc8b 100644 --- a/client/source/scripts/components/sidebar/SpeedLimitDropdown.js +++ b/client/source/scripts/components/sidebar/SpeedLimitDropdown.js @@ -16,7 +16,7 @@ class SpeedLimitDropdown extends React.Component { super(); this.state = { - speedLimits: SettingsStore.getSettings('speedLimits'), + speedLimits: SettingsStore.getFloodSettings('speedLimits'), throttle: null }; @@ -30,7 +30,7 @@ class SpeedLimitDropdown extends React.Component { this.handleSettingsFetchRequestSuccess); TransferDataStore.listen(EventTypes.CLIENT_TRANSFER_DATA_REQUEST_SUCCESS, this.onTransferDataRequestSuccess); - SettingsStore.fetchSettings('speedLimits'); + SettingsStore.fetchFloodSettings('speedLimits'); TransferDataStore.fetchTransferData(); } @@ -129,7 +129,7 @@ class SpeedLimitDropdown extends React.Component { } handleSettingsFetchRequestSuccess() { - let speedLimits = SettingsStore.getSettings('speedLimits'); + let speedLimits = SettingsStore.getFloodSettings('speedLimits'); if (!!speedLimits) { this.setState({speedLimits}); diff --git a/client/source/scripts/components/torrent-list/ActionBar.js b/client/source/scripts/components/torrent-list/ActionBar.js index 0f04e31d..02ae4a42 100644 --- a/client/source/scripts/components/torrent-list/ActionBar.js +++ b/client/source/scripts/components/torrent-list/ActionBar.js @@ -29,7 +29,7 @@ export default class ActionBar extends React.Component { super(); this.state = { - sortBy: SettingsStore.getSettings('sortTorrents') + sortBy: SettingsStore.getFloodSettings('sortTorrents') }; METHODS_TO_BIND.forEach((method) => { @@ -40,7 +40,7 @@ export default class ActionBar extends React.Component { componentDidMount() { this.onSortChange(); SettingsStore.listen(EventTypes.SETTINGS_CHANGE, this.onSortChange); - SettingsStore.fetchSettings('sortTorrents'); + SettingsStore.fetchFloodSettings('sortTorrents'); } componentWillUnmount() { @@ -99,7 +99,7 @@ export default class ActionBar extends React.Component { } handleSortChange(sortBy) { - SettingsStore.saveSettings({id: 'sortTorrents', data: sortBy}); + SettingsStore.saveFloodSettings({id: 'sortTorrents', data: sortBy}); UIActions.setTorrentsSort(sortBy); } @@ -112,7 +112,7 @@ export default class ActionBar extends React.Component { } onSortChange() { - let sortBy = SettingsStore.getSettings('sortTorrents'); + let sortBy = SettingsStore.getFloodSettings('sortTorrents'); TorrentFilterStore.setTorrentsSort(sortBy); this.setState({sortBy}); } diff --git a/client/source/scripts/constants/ActionTypes.js b/client/source/scripts/constants/ActionTypes.js index e506de43..ae4630c1 100644 --- a/client/source/scripts/constants/ActionTypes.js +++ b/client/source/scripts/constants/ActionTypes.js @@ -21,9 +21,14 @@ const ActionTypes = { CLIENT_SET_FILE_PRIORITY_SUCCESS: 'CLIENT_SET_FILE_PRIORITY_SUCCESS', CLIENT_SET_THROTTLE_ERROR: 'CLIENT_SET_THROTTLE_ERROR', CLIENT_SET_THROTTLE_SUCCESS: 'CLIENT_SET_THROTTLE_SUCCESS', + CLIENT_SETTINGS_FETCH_REQUEST_ERROR: 'CLIENT_SETTINGS_FETCH_REQUEST_ERROR', + CLIENT_SETTINGS_FETCH_REQUEST_SUCCESS: 'CLIENT_SETTINGS_FETCH_REQUEST_SUCCESS', + CLIENT_SETTINGS_SAVE_ERROR: 'CLIENT_SETTINGS_SAVE_ERROR', + CLIENT_SETTINGS_SAVE_SUCCESS: 'CLIENT_SETTINGS_SAVE_SUCCESS', CLIENT_START_TORRENT_ERROR: 'CLIENT_START_TORRENT_ERROR', CLIENT_START_TORRENT_SUCCESS: 'CLIENT_START_TORRENT_SUCCESS', CLIENT_STOP_TORRENT_ERROR: 'CLIENT_STOP_TORRENT_ERROR', + CLIENT_STOP_TORRENT_SUCCESS: 'CLIENT_STOP_TORRENT_SUCCESS', SETTINGS_FETCH_REQUEST_SUCCESS: 'SETTINGS_FETCH_REQUEST_SUCCESS', SETTINGS_FETCH_REQUEST_ERROR: 'SETTINGS_FETCH_REQUEST_ERROR', SETTINGS_SAVE_REQUEST_SUCCESS: 'SETTINGS_SAVE_REQUEST_SUCCESS', diff --git a/client/source/scripts/constants/EventTypes.js b/client/source/scripts/constants/EventTypes.js index afddfc16..647bcc5d 100644 --- a/client/source/scripts/constants/EventTypes.js +++ b/client/source/scripts/constants/EventTypes.js @@ -5,6 +5,10 @@ const EventTypes = { CLIENT_SET_THROTTLE_SUCCESS: 'CLIENT_SET_THROTTLE_SUCCESS', CLIENT_MOVE_TORRENTS_REQUEST_ERROR: 'CLIENT_MOVE_TORRENTS_REQUEST_ERROR', CLIENT_MOVE_TORRENTS_SUCCESS: 'CLIENT_MOVE_TORRENTS_SUCCESS', + CLIENT_SETTINGS_FETCH_REQUEST_ERROR: 'CLIENT_SETTINGS_FETCH_REQUEST_ERROR', + CLIENT_SETTINGS_FETCH_REQUEST_SUCCESS: 'CLIENT_SETTINGS_FETCH_REQUEST_SUCCESS', + CLIENT_SETTINGS_SAVE_REQUEST_ERROR: 'CLIENT_SETTINGS_SAVE_REQUEST_ERROR', + CLIENT_SETTINGS_SAVE_REQUEST_SUCCESS: 'CLIENT_SETTINGS_SAVE_REQUEST_SUCCESS', CLIENT_TORRENTS_REQUEST_ERROR: 'CLIENT_TORRENTS_REQUEST_ERROR', CLIENT_TORRENT_STATUS_COUNT_CHANGE: 'CLIENT_TORRENT_STATUS_COUNT_CHANGE', CLIENT_TORRENT_STATUS_COUNT_REQUEST_ERROR: 'CLIENT_TORRENT_STATUS_COUNT_REQUEST_ERROR', diff --git a/client/source/scripts/stores/SettingsStore.js b/client/source/scripts/stores/SettingsStore.js index 41a826ad..cf21001b 100644 --- a/client/source/scripts/stores/SettingsStore.js +++ b/client/source/scripts/stores/SettingsStore.js @@ -1,6 +1,7 @@ import ActionTypes from '../constants/ActionTypes'; import AppDispatcher from '../dispatcher/AppDispatcher'; import BaseStore from './BaseStore'; +import ClientActions from '../actions/ClientActions'; import EventTypes from '../constants/EventTypes'; import NotificationStore from './NotificationStore'; import SettingsActions from '../actions/SettingsActions'; @@ -10,8 +11,15 @@ class SettingsStoreClass extends BaseStore { constructor() { super(); + this.fetchStatus = { + clientSettingsFetched: false, + floodSettingsFetched: false + }; + + this.clientSettings = {}; + // Default settings are overridden by settings stored in database. - this.settings = { + this.floodSettings = { sortTorrents: { direction: 'desc', displayName: 'Date Added', @@ -25,16 +33,61 @@ class SettingsStoreClass extends BaseStore { }; } - fetchSettings(property) { + fetchClientSettings(property) { + ClientActions.fetchSettings(property); + } + + fetchFloodSettings(property) { SettingsActions.fetchSettings(property); } - getSettings(property) { + getClientSettings(property) { if (property) { - return this.settings[property]; + return this.clientSettings[property]; } - return this.settings; + return Object.assign({}, this.clientSettings); + } + + getFloodSettings(property) { + if (property) { + return this.floodSettings[property]; + } + + return Object.assign({}, this.floodSettings); + } + + handleClientSettingsFetchSuccess(settings) { + this.fetchStatus.clientSettingsFetched = true; + this.clientSettings = settings; + + this.emit(EventTypes.CLIENT_SETTINGS_FETCH_REQUEST_SUCCESS); + this.processSettingsState(); + } + + handleClientSettingsFetchError(error) { + this.emit(EventTypes.CLIENT_SETTINGS_FETCH_REQUEST_ERROR); + } + + handleClientSettingsSaveRequestError() { + this.emit(EventTypes.CLIENT_SETTINGS_SAVE_REQUEST_ERROR); + } + + handleClientSettingsSaveRequestSuccess(data, options) { + this.emit(EventTypes.CLIENT_SETTINGS_SAVE_REQUEST_SUCCESS); + + if (options.notify) { + NotificationStore.add({ + adverb: 'Successfully', + action: 'saved', + subject: 'settings', + id: 'save-settings-success' + }); + } + + if (options.dismissModal) { + UIStore.dismissModal(); + } } handleSettingsFetchError(error) { @@ -42,12 +95,14 @@ class SettingsStoreClass extends BaseStore { } handleSettingsFetchSuccess(settings) { + this.fetchStatus.floodSettingsFetched = true; + Object.keys(settings).forEach((property) => { - this.settings[property] = settings[property]; + this.floodSettings[property] = settings[property]; }); - this.emit(EventTypes.SETTINGS_CHANGE); this.emit(EventTypes.SETTINGS_FETCH_REQUEST_SUCCESS); + this.processSettingsState(); } handleSettingsSaveRequestError() { @@ -62,7 +117,7 @@ class SettingsStoreClass extends BaseStore { adverb: 'Successfully', action: 'saved', subject: 'settings', - id: 'save-torrents-success' + id: 'save-settings-success' }); } @@ -71,11 +126,38 @@ class SettingsStoreClass extends BaseStore { } } - saveSettings(settings, options) { - this.settings[settings.id] = settings.data; + processSettingsState() { + if (this.fetchStatus.clientSettingsFetched + && this.fetchStatus.floodSettingsFetched) { + this.emit(EventTypes.SETTINGS_CHANGE); + } + } + + saveFloodSettings(settings, options) { + if (!Array.isArray(settings)) { + settings = [settings]; + } + SettingsActions.saveSettings(settings, options); + this.updateLocalSettings(settings, 'floodSettings'); this.emit(EventTypes.SETTINGS_CHANGE); } + + saveClientSettings(settings, options) { + if (!Array.isArray(settings)) { + settings = [settings]; + } + + ClientActions.saveSettings(settings, options); + this.updateLocalSettings(settings, 'clientSettings'); + this.emit(EventTypes.SETTINGS_CHANGE); + } + + updateLocalSettings(settings, settingsType) { + settings.forEach((setting) => { + this[settingsType][setting.id] = setting.data; + }); + } } let SettingsStore = new SettingsStoreClass(); @@ -84,18 +166,30 @@ SettingsStore.dispatcherID = AppDispatcher.register((payload) => { const {action, source} = payload; switch (action.type) { - case ActionTypes.SETTINGS_FETCH_REQUEST_SUCCESS: - SettingsStore.handleSettingsFetchSuccess(action.data); + case ActionTypes.CLIENT_SETTINGS_FETCH_REQUEST_ERROR: + SettingsStore.handleClientSettingsFetchError(action.error); + break; + case ActionTypes.CLIENT_SETTINGS_FETCH_REQUEST_SUCCESS: + SettingsStore.handleClientSettingsFetchSuccess(action.data); break; case ActionTypes.SETTINGS_FETCH_REQUEST_ERROR: SettingsStore.handleSettingsFetchError(action.error); break; + case ActionTypes.SETTINGS_FETCH_REQUEST_SUCCESS: + SettingsStore.handleSettingsFetchSuccess(action.data); + break; case ActionTypes.SETTINGS_SAVE_REQUEST_ERROR: SettingsStore.handleSettingsSaveRequestError(action.error); break; case ActionTypes.SETTINGS_SAVE_REQUEST_SUCCESS: SettingsStore.handleSettingsSaveRequestSuccess(action.data, action.options); break; + case ActionTypes.CLIENT_SETTINGS_SAVE_ERROR: + SettingsStore.handleClientSettingsSaveRequestError(action.error); + break; + case ActionTypes.CLIENT_SETTINGS_SAVE_SUCCESS: + SettingsStore.handleClientSettingsSaveRequestSuccess(action.data, action.options); + break; } }); diff --git a/client/source/scripts/stores/TorrentFilterStore.js b/client/source/scripts/stores/TorrentFilterStore.js index d64c9495..4a619399 100644 --- a/client/source/scripts/stores/TorrentFilterStore.js +++ b/client/source/scripts/stores/TorrentFilterStore.js @@ -13,7 +13,7 @@ class TorrentFilterStoreClass extends BaseStore { this.searchFilter = null; this.statusFilter = 'all'; this.trackerFilter = 'all'; - this.sortTorrentsBy = SettingsStore.getSettings('sortTorrents'); + this.sortTorrentsBy = SettingsStore.getFloodSettings('sortTorrents'); } fetchTorrentStatusCount() { diff --git a/client/source/scripts/stores/TorrentStore.js b/client/source/scripts/stores/TorrentStore.js index 2b6386d1..e6821fcc 100644 --- a/client/source/scripts/stores/TorrentStore.js +++ b/client/source/scripts/stores/TorrentStore.js @@ -104,7 +104,7 @@ class TorrentStoreClass extends BaseStore { handleAddTorrentSuccess(response) { this.emit(EventTypes.CLIENT_ADD_TORRENT_SUCCESS); - SettingsStore.saveSettings({ + SettingsStore.saveFloodSettings({ id: 'torrentDestination', data: response.destination }); diff --git a/server/models/ClientRequest.js b/server/models/ClientRequest.js index 65cc618b..d816a8d3 100644 --- a/server/models/ClientRequest.js +++ b/server/models/ClientRequest.js @@ -1,11 +1,12 @@ 'use strict'; -let util = require('util'); - -let clientUtil = require('../util/clientUtil'); let fs = require('fs'); let mv = require('mv'); let path = require('path'); +let util = require('util'); + +let clientSettingsMap = require('../../shared/constants/clientSettingsMap'); +let clientUtil = require('../util/clientUtil'); let rTorrentPropMap = require('../util/rTorrentPropMap'); let scgi = require('../util/scgi'); let stringUtil = require('../../shared/util/stringUtil'); @@ -163,6 +164,27 @@ class ClientRequest { ); } + fetchSettingsMethodCall(options) { + let requestedSettings = []; + + if (options.requestedSettings) { + requestedSettings = options.requestedSettings; + } else { + requestedSettings = clientSettingsMap.defaults.map((settingsKey) => { + return clientSettingsMap[settingsKey]; + }); + } + + // Ensure client's response gets mapped to the correct requested property. + if (options.setPropertiesArr) { + options.setPropertiesArr(requestedSettings); + } + + requestedSettings.forEach((settingsKey) => { + this.requests.push(this.getMethodCall(settingsKey)); + }); + } + getTorrentDetailsMethodCall(options) { var peerParams = [options.hash, ''].concat(options.peerProps); var fileParams = [options.hash, ''].concat(options.fileProps); @@ -251,6 +273,16 @@ class ClientRequest { }); } + setSettingsMethodCall(options) { + console.log(options); + let settings = this.getEnsuredArray(options.settings); + + settings.forEach((setting) => { + this.requests.push(this.getMethodCall(`${clientSettingsMap[setting.id]}.set`, + ['', setting.data])); + }); + } + setThrottleMethodCall(options) { let methodName = 'throttle.global_down.max_rate.set'; if (options.direction === 'upload') { diff --git a/server/models/client.js b/server/models/client.js index 9c59663b..bdcc8ba4 100644 --- a/server/models/client.js +++ b/server/models/client.js @@ -4,6 +4,7 @@ let fs = require('fs'); let util = require('util'); let clientResponseUtil = require('../util/clientResponseUtil'); +let clientSettingsMap = require('../../shared/constants/clientSettingsMap'); let ClientRequest = require('./ClientRequest'); let clientUtil = require('../util/clientUtil'); let propsMap = require('../../shared/constants/propsMap'); @@ -61,6 +62,33 @@ var client = { request.send(); }, + getSettings: (options, callback) => { + let properties = []; + let request = new ClientRequest(); + let response = {}; + + request.add('fetchSettings', { + options, + setPropertiesArr: (propertiesArr) => { + properties = propertiesArr; + } + }); + + request.postProcess((data) => { + if (!data) { + return null; + } + + data.forEach((datum, index) => { + response[clientSettingsMap[properties[index]]] = datum[0]; + }); + + return response; + }); + request.onComplete(callback); + request.send(); + }, + getTorrentStatusCount: (callback) => { callback(_statusCount); }, @@ -154,7 +182,7 @@ var client = { mainRequest.send(); }, - setFilePriority: (hashes, data, callback) => { + setFilePriority: (hashes, data, callback) => { // TODO Add support for multiple hashes. let fileIndex = data.fileIndices[0]; let request = new ClientRequest(); @@ -164,7 +192,7 @@ var client = { request.send(); }, - setPriority: (hashes, data, callback) => { + setPriority: (hashes, data, callback) => { let request = new ClientRequest(); request.add('setPriority', {hashes, priority: data.priority}); @@ -172,6 +200,19 @@ var client = { request.send(); }, + setSettings: (payloads, callback) => { + let request = new ClientRequest(); + + if (payloads.length === 0) { + callback({}); + return; + } + + request.add('setSettings', {settings: payloads}); + request.onComplete(callback); + request.send(); + }, + setSpeedLimits: (data, callback) => { let request = new ClientRequest(); diff --git a/server/routes/client.js b/server/routes/client.js index c72c4911..51fa5ab6 100644 --- a/server/routes/client.js +++ b/server/routes/client.js @@ -24,23 +24,28 @@ router.post('/add-files', upload.array('torrents'), function(req, res, next) { client.addFiles(req, ajaxUtil.getResponseFn(res)); }); +router.get('/settings', function(req, res, next) { + client.getSettings(req.query, ajaxUtil.getResponseFn(res)); +}); + +router.patch('/settings', function(req, res, next) { + client.setSettings(req.body, ajaxUtil.getResponseFn(res)); +}); + router.put('/settings/speed-limits', function(req, res, next) { client.setSpeedLimits(req.body, ajaxUtil.getResponseFn(res)); }); router.post('/start', function(req, res, next) { - var hashes = req.body.hashes; - client.startTorrent(hashes, ajaxUtil.getResponseFn(res)); + client.startTorrent(req.body.hashes, ajaxUtil.getResponseFn(res)); }); router.post('/stop', function(req, res, next) { - var hashes = req.body.hashes; - client.stopTorrent(hashes, ajaxUtil.getResponseFn(res)); + client.stopTorrent(req.body.hashes, ajaxUtil.getResponseFn(res)); }); router.post('/torrent-details', function(req, res, next) { - var hash = req.body.hash; - client.getTorrentDetails(hash, ajaxUtil.getResponseFn(res)); + client.getTorrentDetails(req.body.hash, ajaxUtil.getResponseFn(res)); }); router.get('/torrents', function(req, res, next) { @@ -59,6 +64,10 @@ router.post('/torrents/move', function(req, res, next) { client.moveTorrents(req.body, ajaxUtil.getResponseFn(res)); }); +router.post('/torrents/delete', function(req, res, next) { + client.deleteTorrents(req.body.hash, ajaxUtil.getResponseFn(res)); +}); + router.get('/torrents/status-count', function(req, res, next) { client.getTorrentStatusCount(ajaxUtil.getResponseFn(res)); }); @@ -67,11 +76,6 @@ router.get('/torrents/tracker-count', function(req, res, next) { client.getTorrentTrackerCount(ajaxUtil.getResponseFn(res)); }); -router.post('/torrents/delete', function(req, res, next) { - var hash = req.body.hash; - client.deleteTorrents(hash, ajaxUtil.getResponseFn(res)); -}); - router.get('/methods.json', function(req, res, next) { var type = req.query.type; var args = req.query.args; diff --git a/shared/constants/clientSettingsMap.js b/shared/constants/clientSettingsMap.js new file mode 100644 index 00000000..c71e930f --- /dev/null +++ b/shared/constants/clientSettingsMap.js @@ -0,0 +1,105 @@ +'use strict'; + +let objectUtil = require('../util/objectUtil'); + +const clientSettingsMap = objectUtil.reflect({ + dhtPort: 'dht.port', + dhtStatus: 'dht.statistics', + directoryDefault: 'directory.default', + maxFileSize: 'system.file.max_size', + networkBindAddress: 'network.bind_address', + networkHttpCert: 'network.http.cacert', + networkHttpMaxOpen: 'network.http.max_open', + networkHttpPath: 'network.http.capath', + networkHttpProxy: 'network.http.proxy_address', + networkLocalAddress: 'network.local_address', + networkMaxOpenFiles: 'network.max_open_files', + networkMaxOpenSockets: 'network.max_open_sockets', + networkPortOpen: 'network.port_open', + networkPortRandom: 'network.port_random', + networkPortRange: 'network.port_range', + networkReceiveBufferSize: 'network.receive_buffer.size', + networkScgiDontRoute: 'network.scgi.dont_route', + networkSendBufferSize: 'network.send_buffer.size', + piecesHashOnCompletion: 'pieces.hash.on_completion', + piecesMemoryMax: 'pieces.memory.max', + piecesPreloadMinRate: 'pieces.preload.min_rate', + piecesPreloadMinSize: 'pieces.preload.min_size', + piecesPreloadType: 'pieces.preload.type', + piecesSyncAlwaysSafe: 'pieces.sync.always_safe', + piecesSyncTimeout: 'pieces.sync.timeout', + piecesSyncTimeoutSafe: 'pieces.sync.timeout_safe', + protocolPex: 'protocol.pex', + sessionOnCompletion: 'session.on_completion', + sessionPath: 'session.path', + sessionUseLock: 'session.use_lock', + systemFileSplitSize: 'system.file.split_size', + systemFileSplitSuffix: 'system.file.split_suffix', + throttleDownMax: 'throttle.global_down.max_rate', + throttleGlobalUpMax: 'throttle.global_up.max_rate', + throttleMaxDownloadsDiv: 'throttle.max_downloads.div', + throttleMaxDownloadsGlobal: 'throttle.max_downloads.global', + throttleMaxPeersNormal: 'throttle.max_peers.normal', + throttleMaxPeersSeed: 'throttle.max_peers.seed', + throttleMaxDownloads: 'throttle.max_downloads', + throttleMaxDownloadsDiv: 'throttle.max_downloads.div', + throttleMaxDownloadsGlobal: 'throttle.max_downloads.global', + throttleMaxUploads: 'throttle.max_uploads', + throttleMaxUploadsDiv: 'throttle.max_uploads.div', + throttleMaxUploadsGlobal: 'throttle.max_uploads.global', + throttleMinPeersNormal: 'throttle.min_peers.normal', + throttleMinPeersSeed: 'throttle.min_peers.seed', + trackersNumWant: 'trackers.numwant', + trackersUseUdp: 'trackers.use_udp' +}); + +clientSettingsMap.defaults = [ + 'dhtPort', + 'dhtStatus', + 'directoryDefault', + 'maxFileSize', + 'networkBindAddress', + 'networkHttpCert', + 'networkHttpMaxOpen', + 'networkHttpPath', + 'networkHttpProxy', + 'networkLocalAddress', + 'networkMaxOpenFiles', + 'networkMaxOpenSockets', + 'networkPortOpen', + 'networkPortRandom', + 'networkPortRange', + 'networkReceiveBufferSize', + 'networkScgiDontRoute', + 'networkSendBufferSize', + 'piecesHashOnCompletion', + 'piecesMemoryMax', + 'piecesPreloadMinRate', + 'piecesPreloadMinSize', + 'piecesPreloadType', + 'piecesSyncAlwaysSafe', + 'piecesSyncTimeout', + 'piecesSyncTimeoutSafe', + 'protocolPex', + 'sessionOnCompletion', + 'sessionPath', + 'sessionUseLock', + 'systemFileSplitSize', + 'systemFileSplitSuffix', + 'throttleDownMax', + 'throttleGlobalUpMax', + 'throttleMaxDownloadsDiv', + 'throttleMaxDownloadsGlobal', + 'throttleMaxPeersNormal', + 'throttleMaxPeersSeed', + 'throttleMaxDownloads', + 'throttleMaxUploads', + 'throttleMaxUploadsDiv', + 'throttleMaxUploadsGlobal', + 'throttleMinPeersNormal', + 'throttleMinPeersSeed', + 'trackersNumWant', + 'trackersUseUdp' +]; + +module.exports = clientSettingsMap; diff --git a/shared/util/objectUtil.js b/shared/util/objectUtil.js new file mode 100644 index 00000000..f9f890c0 --- /dev/null +++ b/shared/util/objectUtil.js @@ -0,0 +1,13 @@ +'use strict'; + +let objectUtil = { + reflect: (hash) => { + return Object.keys(hash).reduce((memo, key) => { + memo[key] = hash[key]; + memo[hash[key]] = key; + return memo; + }, {}); + } +} + +module.exports = objectUtil;