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
+
+
+
+
+
+
+
+ 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;