Introduce client settings

This commit is contained in:
John Furrow
2016-06-06 21:01:10 -07:00
parent 30d75c124b
commit 02c3b5e4b0
27 changed files with 907 additions and 206 deletions

View File

@@ -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 {

View File

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

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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

View File

@@ -93,7 +93,6 @@ const TorrentActions = {
});
})
.catch((error) => {
console.trace(error);
AppDispatcher.dispatchServerAction({
type: ActionTypes.CLIENT_FETCH_TORRENTS_ERROR,
data: {

View File

@@ -13,7 +13,8 @@ import TorrentListView from './components/panels/TorrentListView';
class FloodApp extends React.Component {
componentDidMount() {
SettingsStore.fetchSettings();
SettingsStore.fetchClientSettings();
SettingsStore.fetchFloodSettings();
}
render() {

View File

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

View File

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

View File

@@ -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'
}
};

View File

@@ -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 (
<div className="form">
<div className="form__section">
<p className="form__section__heading">
Speed Limit Dropdown Presets
</p>
<p className="form__section__sub-heading">
Enter a comma-separated list of speeds in kB. 0 represents unlimited.
</p>
<div className="form__row">
<div className="form__column">
<label className="form__label">
Download Presets
</label>
<input className="textbox" type="text"
onChange={this.handleDownloadTextChange}
value={downloadValue} />
</div>
<div className="form__column">
<label className="form__label">
Upload Presets
</label>
<input className="textbox" type="text"
onChange={this.handleUploadTextChange}
value={uploadValue} />
</div>
</div>
</div>
<div className="form__section">
<div className="form__section__heading">
Slot Availability
</div>
<div className="form__row">
<div className="form__column">
<label className="form__label">
Upload Slots Per Torrent
</label>
<input className="textbox" type="text"
onChange={this.handleClientSettingFieldChange.bind(this, 'throttleMaxUploads')}
value={this.getFieldValue('throttleMaxUploads')} />
</div>
<div className="form__column">
<label className="form__label">
Upload Slots Divider
</label>
<input className="textbox" type="text"
onChange={this.handleClientSettingFieldChange.bind(this, 'throttleMaxUploadsDiv')}
value={this.getFieldValue('throttleMaxUploadsDiv')} />
</div>
<div className="form__column">
<label className="form__label">
Upload Slots Global
</label>
<input className="textbox" type="text"
onChange={this.handleClientSettingFieldChange.bind(this, 'throttleMaxUploadsGlobal')}
value={this.getFieldValue('throttleMaxUploadsGlobal')} />
</div>
</div>
<div className="form__row">
<div className="form__column">
<label className="form__label">
Download Slots Per Torrent
</label>
<input className="textbox" type="text"
onChange={this.handleClientSettingFieldChange.bind(this, 'throttleMaxDownloads')}
value={this.getFieldValue('throttleMaxDownloads')} />
</div>
<div className="form__column">
<label className="form__label">
Download Slots Divider
</label>
<input className="textbox" type="text"
onChange={this.handleClientSettingFieldChange.bind(this, 'throttleMaxDownloadsDiv')}
value={this.getFieldValue('throttleMaxDownloadsDiv')} />
</div>
<div className="form__column">
<label className="form__label">
Download Slots Global
</label>
<input className="textbox" type="text"
onChange={this.handleClientSettingFieldChange.bind(this, 'throttleMaxDownloadsGlobal')}
value={this.getFieldValue('throttleMaxDownloadsGlobal')} />
</div>
</div>
</div>
</div>
);
}
}

View File

@@ -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 (
<div className="form">
<div className="form__section">
<div className="form__section__heading">
Listening Port
</div>
<div className="form__row">
<div className="form__column form__column--small">
<label className="form__label">
Port Range
</label>
<input className="textbox" type="text"
onChange={this.handleClientSettingFieldChange.bind(this, 'networkPortRange')}
value={this.getFieldValue('networkPortRange')} />
</div>
<div className="form__column form__column--auto form__column--unlabled">
<Checkbox
checked={this.getFieldValue('networkPortRandom') === '1'}
onChange={this.handleClientSettingCheckboxChange.bind(this, 'networkPortRandom')}>
Randomize Port
</Checkbox>
</div>
<div className="form__column form__column--auto form__column--unlabled">
<Checkbox
checked={this.getFieldValue('networkPortOpen') === '1'}
onChange={this.handleClientSettingCheckboxChange.bind(this, 'networkPortOpen')}>
Open Port
</Checkbox>
</div>
</div>
</div>
<div className="form__section">
<div className="form__section__heading">
DHT
</div>
<div className="form__row">
<div className="form__column form__column--small">
<label className="form__label">
Port
</label>
<input className="textbox" type="text"
onChange={this.handleClientSettingFieldChange.bind(this, 'dhtPort')}
value={this.getFieldValue('dhtPort')} />
</div>
</div>
</div>
<div className="form__section">
<div className="form__section__heading">
Peers
</div>
<div className="form__row">
<div className="form__column">
<label className="form__label">
Minimum Peers
</label>
<input className="textbox" type="text"
onChange={this.handleClientSettingFieldChange.bind(this, 'throttleMinPeersNormal')}
value={this.getFieldValue('throttleMinPeersNormal')} />
</div>
<div className="form__column">
<label className="form__label">
Maximum Peers
</label>
<input className="textbox" type="text"
onChange={this.handleClientSettingFieldChange.bind(this, 'throttleMaxPeersNormal')}
value={this.getFieldValue('throttleMaxPeersNormal')} />
</div>
</div>
<div className="form__row">
<div className="form__column">
<label className="form__label">
Minimum Peers Seeding
</label>
<input className="textbox" type="text"
onChange={this.handleClientSettingFieldChange.bind(this, 'throttleMinPeersSeed')}
value={this.getFieldValue('throttleMinPeersSeed')} />
</div>
<div className="form__column">
<label className="form__label">
Maximum Peers Seeding
</label>
<input className="textbox" type="text"
onChange={this.handleClientSettingFieldChange.bind(this, 'throttleMaxPeersSeed')}
value={this.getFieldValue('throttleMaxPeersSeed')} />
</div>
</div>
<div className="form__row">
<div className="form__column form__column--half">
<label className="form__label">
Amount Desired
</label>
<input className="textbox" type="text"
onChange={this.handleClientSettingFieldChange.bind(this, 'trackersNumWant')}
value={this.getFieldValue('trackersNumWant')} />
</div>
</div>
</div>
</div>
);
}
}

View File

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

View File

@@ -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 (
<div>
<p className="modal__tab__introduction">
Provide a comma-separated list of speed values (in kilobytes per second). 0 represents unlimited.
</p>
<div className="form">
<div className="form__row">
<div className="form__column">
<label className="form__label">
Download Presets
</label>
<input className="textbox" type="text"
onChange={this.handleDownloadTextChange}
value={downloadValue} />
</div>
</div>
<div className="form__row">
<div className="form__column">
<label className="form__label">
Upload Presets
</label>
<input className="textbox" type="text"
onChange={this.handleUploadTextChange}
value={uploadValue} />
</div>
</div>
</div>
</div>
);
}
}

View File

@@ -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 (
<div className="form">
<div className="form__section">
<div className="form__section__heading">
Directories
</div>
<div className="form__row">
<div className="form__column">
<label className="form__label">
Default Download Directory
</label>
<input className="textbox" type="text"
onChange={this.handleClientSettingCheckboxChange.bind(this, 'directoryDefault')}
value={this.getFieldValue('directoryDefault')} />
</div>
</div>
</div>
</div>
);
}
}

View File

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

View File

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

View File

@@ -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',

View File

@@ -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',

View File

@@ -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;
SettingsActions.saveSettings(settings, options);
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;
}
});

View File

@@ -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() {

View File

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

View File

@@ -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') {

View File

@@ -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);
},
@@ -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();

View File

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

View File

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

13
shared/util/objectUtil.js Normal file
View File

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