diff --git a/client/source/sass/base/_form-elements.scss b/client/source/sass/base/_form-elements.scss
index 9a929ada..3da2cf87 100644
--- a/client/source/sass/base/_form-elements.scss
+++ b/client/source/sass/base/_form-elements.scss
@@ -89,17 +89,9 @@ $modal--body--foreground: desaturate(lighten($foreground, 20%), 10%);
.icon {
height: 16px;
- margin: 0 $spacing-unit * 1/3;
+ margin: 0 $spacing-unit * 1/3 0 0;
vertical-align: middle;
width: 16px;
-
- &:first-child {
- margin-left: 0;
- }
-
- &:last-child {
- margin-right: 0;
- }
}
}
diff --git a/client/source/sass/components/_notifications.scss b/client/source/sass/components/_notifications.scss
index 69b7a3c0..f5d01198 100644
--- a/client/source/sass/components/_notifications.scss
+++ b/client/source/sass/components/_notifications.scss
@@ -65,6 +65,10 @@ $notification--foreground: #8fa2b2;
}
}
+ & + .notification {
+ margin-top: $spacing-unit * 2/5;
+ }
+
&__content {
flex: 1 1 auto;
}
diff --git a/client/source/scripts/actions/SettingsActions.js b/client/source/scripts/actions/SettingsActions.js
index c478e559..3aa0515a 100644
--- a/client/source/scripts/actions/SettingsActions.js
+++ b/client/source/scripts/actions/SettingsActions.js
@@ -24,7 +24,7 @@ const SettingsActions = {
});
},
- saveSettings: (settings) => {
+ saveSettings: (settings, options = {}) => {
return axios.patch('/client/settings', settings)
.then((json = {}) => {
return json.data;
@@ -32,11 +32,12 @@ const SettingsActions = {
.then((data) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.SETTINGS_SAVE_REQUEST_SUCCESS,
- data
+ data,
+ options
});
})
.catch((error) => {
- console.error(error);
+ console.trace(error);
AppDispatcher.dispatchServerAction({
type: ActionTypes.SETTINGS_SAVE_REQUEST_ERROR,
error
diff --git a/client/source/scripts/actions/UIActions.js b/client/source/scripts/actions/UIActions.js
index cb4efad0..7ea2e3dc 100644
--- a/client/source/scripts/actions/UIActions.js
+++ b/client/source/scripts/actions/UIActions.js
@@ -27,34 +27,10 @@ const UIActions = {
},
dismissModal: () => {
- // TODO: Remove this try..catch.
- try {
- AppDispatcher.dispatchUIAction({
- type: ActionTypes.UI_DISPLAY_MODAL,
- data: null
- });
- } catch (err) {
- console.error(err);
- }
- },
-
- fetchSortProps: () => {
- return axios.get('/ui/sort-props')
- .then((json = {}) => {
- return json.data;
- })
- .then((data) => {
- AppDispatcher.dispatchServerAction({
- type: ActionTypes.UI_SORT_PROPS_REQUEST_SUCCESS,
- data
- });
- })
- .catch((error) => {
- AppDispatcher.dispatchServerAction({
- type: ActionTypes.UI_SORT_PROPS_REQUEST_ERROR,
- error
- });
- });
+ AppDispatcher.dispatchUIAction({
+ type: ActionTypes.UI_DISPLAY_MODAL,
+ data: null
+ });
},
handleDetailsClick: (data) => {
@@ -93,12 +69,6 @@ const UIActions = {
},
setTorrentsSort: (data) => {
- axios
- .post('/ui/sort-props', data)
- .catch(() => {
- console.log(error);
- });
-
AppDispatcher.dispatchUIAction({
type: ActionTypes.UI_SET_TORRENT_SORT,
data
diff --git a/client/source/scripts/components/modals/SettingsModal.js b/client/source/scripts/components/modals/SettingsModal.js
index a492a942..4041f5b4 100644
--- a/client/source/scripts/components/modals/SettingsModal.js
+++ b/client/source/scripts/components/modals/SettingsModal.js
@@ -2,14 +2,16 @@ import classnames from 'classnames';
import React from 'react';
import EventTypes from '../../constants/EventTypes';
+import LoadingIndicatorDots from '../icons/LoadingIndicatorDots';
import Modal from './Modal';
import SettingsSpeedLimit from './SettingsSpeedLimit';
import SettingsStore from '../../stores/SettingsStore';
const METHODS_TO_BIND = [
- 'handleSettingsChange',
'handleSaveSettingsClick',
- 'handleSettingsFetchRequestSuccess'
+ 'handleSaveSettingsError',
+ 'handleSettingsChange',
+ 'handleSettingsStoreChange'
];
export default class SettingsModal extends React.Component {
@@ -17,12 +19,8 @@ export default class SettingsModal extends React.Component {
super();
this.state = {
- settings: {
- speedLimits: {
- download: null,
- upload: null
- }
- }
+ isSavingSettings: false,
+ settings: SettingsStore.getSettings()
};
METHODS_TO_BIND.forEach((method) => {
@@ -31,21 +29,25 @@ export default class SettingsModal extends React.Component {
}
componentDidMount() {
- SettingsStore.listen(EventTypes.SETTINGS_FETCH_REQUEST_SUCCESS, this.handleSettingsFetchRequestSuccess);
+ SettingsStore.listen(EventTypes.SETTINGS_CHANGE,
+ this.handleSettingsStoreChange);
+ SettingsStore.listen(EventTypes.SETTINGS_SAVE_REQUEST_ERROR,
+ this.handleSaveSettingsError);
SettingsStore.fetchSettings('speedLimits');
}
componentWillUnmount() {
- SettingsStore.unlisten(EventTypes.SETTINGS_FETCH_REQUEST_SUCCESS, this.handleSettingsFetchRequestSuccess);
+ SettingsStore.unlisten(EventTypes.SETTINGS_CHANGE,
+ this.handleSettingsStoreChange);
}
getActions() {
let icon = null;
- let primaryButtonText = 'Add Torrent';
+ let primaryButtonText = 'Save Settings';
- if (this.state.isAddingTorrents) {
+ if (this.state.isSavingSettings) {
icon = ;
- primaryButtonText = 'Adding...';
+ primaryButtonText = 'Saving...';
}
return [
@@ -57,7 +59,13 @@ export default class SettingsModal extends React.Component {
},
{
clickHandler: this.handleSaveSettingsClick,
- content: 'Save Settings',
+ content: (
+
+ {icon}
+ {primaryButtonText}
+
+ ),
+ supplementalClassName: icon != null ? 'has-icon' : '',
triggerDismiss: false,
type: 'primary'
}
@@ -65,6 +73,8 @@ export default class SettingsModal extends React.Component {
}
handleSaveSettingsClick() {
+ this.setState({isSavingSettings: true});
+
let settingsToSave = Object.keys(this.state.settings).map((settingsKey) => {
return {
id: settingsKey,
@@ -72,14 +82,18 @@ export default class SettingsModal extends React.Component {
};
});
- SettingsStore.saveSettings(settingsToSave);
+ SettingsStore.saveSettings(settingsToSave, {dismissModal: true, notify: true});
+ }
+
+ handleSaveSettingsError() {
+ this.setState({isSavingSettings: false});
}
handleSettingsFetchRequestError(error) {
console.log(error);
}
- handleSettingsFetchRequestSuccess() {
+ handleSettingsStoreChange() {
this.setState({
settings: SettingsStore.getSettings()
});
diff --git a/client/source/scripts/components/modals/SettingsSpeedLimit.js b/client/source/scripts/components/modals/SettingsSpeedLimit.js
index 3ed37198..982ebe75 100644
--- a/client/source/scripts/components/modals/SettingsSpeedLimit.js
+++ b/client/source/scripts/components/modals/SettingsSpeedLimit.js
@@ -55,7 +55,7 @@ export default class SettingsSpeedLimit extends React.Component {
getDownloadValue() {
let displayedValue = this.state.downloadValue;
- if (displayedValue == null) {
+ if (displayedValue == null && this.props.settings.speedLimits != null) {
displayedValue = this.processSpeedsForDisplay(this.props.settings.speedLimits.download);
}
@@ -65,7 +65,7 @@ export default class SettingsSpeedLimit extends React.Component {
getUploadValue() {
let displayedValue = this.state.uploadValue;
- if (displayedValue == null) {
+ if (displayedValue == null && this.props.settings.speedLimits != null) {
displayedValue = this.processSpeedsForDisplay(this.props.settings.speedLimits.upload);
}
diff --git a/client/source/scripts/components/notifications/Notification.js b/client/source/scripts/components/notifications/Notification.js
index 9d7328fa..ca9f62a7 100644
--- a/client/source/scripts/components/notifications/Notification.js
+++ b/client/source/scripts/components/notifications/Notification.js
@@ -20,7 +20,7 @@ export default class Notification extends React.Component {
icon = ;
}
- if (this.props.count !== 1) {
+ if (!!this.props.accumulation && this.props.count !== 1) {
countText = (
{this.props.count}
diff --git a/client/source/scripts/components/sidebar/SpeedLimitDropdown.js b/client/source/scripts/components/sidebar/SpeedLimitDropdown.js
index 117f5c2c..fb10cb61 100644
--- a/client/source/scripts/components/sidebar/SpeedLimitDropdown.js
+++ b/client/source/scripts/components/sidebar/SpeedLimitDropdown.js
@@ -10,14 +10,13 @@ import SettingsStore from '../../stores/SettingsStore';
import TransferDataStore from '../../stores/TransferDataStore';
const METHODS_TO_BIND = ['handleSettingsFetchRequestSuccess', 'onTransferDataRequestSuccess'];
-const DEFAULT_SPEEDS = [1024, 10240, 102400, 512000, 1048576, 2097152, 5242880, 10485760, 0];
class SpeedLimitDropdown extends React.Component {
constructor() {
super();
this.state = {
- speedLimits: {},
+ speedLimits: SettingsStore.getSettings('speedLimits'),
throttle: null
};
@@ -27,27 +26,23 @@ class SpeedLimitDropdown extends React.Component {
}
componentDidMount() {
- SettingsStore.listen(EventTypes.SETTINGS_FETCH_REQUEST_SUCCESS, this.handleSettingsFetchRequestSuccess);
+ SettingsStore.listen(EventTypes.SETTINGS_CHANGE,
+ this.handleSettingsFetchRequestSuccess);
+ TransferDataStore.listen(EventTypes.CLIENT_TRANSFER_DATA_REQUEST_SUCCESS,
+ this.onTransferDataRequestSuccess);
SettingsStore.fetchSettings('speedLimits');
- TransferDataStore.listen(
- EventTypes.CLIENT_TRANSFER_DATA_REQUEST_SUCCESS,
- this.onTransferDataRequestSuccess
- );
TransferDataStore.fetchTransferData();
}
componentWillUnmount() {
- SettingsStore.unlisten(EventTypes.SETTINGS_FETCH_REQUEST_SUCCESS, this.handleSettingsFetchRequestSuccess);
- TransferDataStore.unlisten(
- EventTypes.CLIENT_TRANSFER_DATA_REQUEST_SUCCESS,
- this.onTransferDataRequestSuccess
- );
+ SettingsStore.unlisten(EventTypes.SETTINGS_CHANGE,
+ this.handleSettingsFetchRequestSuccess);
+ TransferDataStore.unlisten(EventTypes.CLIENT_TRANSFER_DATA_REQUEST_SUCCESS,
+ this.onTransferDataRequestSuccess);
}
onTransferDataRequestSuccess() {
- this.setState({
- throttle: TransferDataStore.getThrottles({latest: true})
- });
+ this.setState({throttle: TransferDataStore.getThrottles({latest: true})});
}
getDropdownHeader() {
@@ -82,7 +77,7 @@ class SpeedLimitDropdown extends React.Component {
let insertCurrentThrottle = true;
let currentThrottle = this.state.throttle;
- let speeds = this.state.speedLimits[property] || DEFAULT_SPEEDS;
+ let speeds = this.state.speedLimits[property];
let items = speeds.map((bytes) => {
let selected = false;
diff --git a/client/source/scripts/components/torrent-list/ActionBar.js b/client/source/scripts/components/torrent-list/ActionBar.js
index 83a5470c..0f04e31d 100644
--- a/client/source/scripts/components/torrent-list/ActionBar.js
+++ b/client/source/scripts/components/torrent-list/ActionBar.js
@@ -5,6 +5,7 @@ import Add from '../icons/Add';
import EventTypes from '../../constants/EventTypes';
import PauseIcon from '../icons/PauseIcon';
import Remove from '../icons/Remove';
+import SettingsStore from '../../stores/SettingsStore';
import SortDropdown from './SortDropdown';
import StartIcon from '../icons/StartIcon';
import StopIcon from '../icons/StopIcon';
@@ -28,7 +29,7 @@ export default class ActionBar extends React.Component {
super();
this.state = {
- sortBy: TorrentFilterStore.getTorrentsSort()
+ sortBy: SettingsStore.getSettings('sortTorrents')
};
METHODS_TO_BIND.forEach((method) => {
@@ -37,12 +38,13 @@ export default class ActionBar extends React.Component {
}
componentDidMount() {
- TorrentFilterStore.fetchSortProps();
- TorrentFilterStore.listen(EventTypes.UI_TORRENTS_SORT_CHANGE, this.onSortChange);
+ this.onSortChange();
+ SettingsStore.listen(EventTypes.SETTINGS_CHANGE, this.onSortChange);
+ SettingsStore.fetchSettings('sortTorrents');
}
componentWillUnmount() {
- TorrentFilterStore.unlisten(EventTypes.UI_TORRENTS_SORT_CHANGE, this.onSortChange);
+ SettingsStore.unlisten(EventTypes.SETTINGS_CHANGE, this.onSortChange);
}
handleAddTorrents() {
@@ -97,6 +99,7 @@ export default class ActionBar extends React.Component {
}
handleSortChange(sortBy) {
+ SettingsStore.saveSettings({id: 'sortTorrents', data: sortBy});
UIActions.setTorrentsSort(sortBy);
}
@@ -109,9 +112,9 @@ export default class ActionBar extends React.Component {
}
onSortChange() {
- this.setState({
- sortBy: TorrentFilterStore.getTorrentsSort()
- });
+ let sortBy = SettingsStore.getSettings('sortTorrents');
+ TorrentFilterStore.setTorrentsSort(sortBy);
+ this.setState({sortBy});
}
render() {
diff --git a/client/source/scripts/components/torrent-list/SortDropdown.js b/client/source/scripts/components/torrent-list/SortDropdown.js
index 4719028d..8d5c130b 100644
--- a/client/source/scripts/components/torrent-list/SortDropdown.js
+++ b/client/source/scripts/components/torrent-list/SortDropdown.js
@@ -3,7 +3,6 @@ import CSSTransitionGroup from 'react-addons-css-transition-group';
import React from 'react';
import Dropdown from '../forms/Dropdown';
-import UIActions from '../../actions/UIActions';
const METHODS_TO_BIND = [
'getDropdownHeader',
@@ -105,6 +104,10 @@ export default class SortDropdown extends React.Component {
}
render() {
+ if (this.props.selectedItem == null) {
+ return null;
+ }
+
return (
{
+ this.settings[property] = settings[property];
+ });
+
+ this.emit(EventTypes.SETTINGS_CHANGE);
+ this.emit(EventTypes.SETTINGS_FETCH_REQUEST_SUCCESS);
+ }
+
+ handleSettingsSaveRequestError() {
+ this.emit(EventTypes.SETTINGS_SAVE_REQUEST_ERROR);
+ }
+
+ handleSettingsSaveRequestSuccess(data, options = {}) {
+ this.emit(EventTypes.SETTINGS_SAVE_REQUEST_SUCCESS);
+
+ if (options.notify) {
+ NotificationStore.add({
+ adverb: 'Successfully',
+ action: 'saved',
+ subject: 'settings',
+ id: 'save-torrents-success'
+ });
+ }
+
+ if (options.dismissModal) {
+ UIStore.dismissModal();
+ }
+ }
+
+ saveSettings(settings, options) {
this.settings[settings.id] = settings.data;
- SettingsActions.saveSettings(settings);
+ SettingsActions.saveSettings(settings, options);
+ this.emit(EventTypes.SETTINGS_CHANGE);
}
}
@@ -48,6 +90,12 @@ SettingsStore.dispatcherID = AppDispatcher.register((payload) => {
case ActionTypes.SETTINGS_FETCH_REQUEST_ERROR:
SettingsStore.handleSettingsFetchError(action.error);
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;
}
});
diff --git a/client/source/scripts/stores/TorrentFilterStore.js b/client/source/scripts/stores/TorrentFilterStore.js
index 8a2e3c9d..d64c9495 100644
--- a/client/source/scripts/stores/TorrentFilterStore.js
+++ b/client/source/scripts/stores/TorrentFilterStore.js
@@ -2,6 +2,7 @@ import ActionTypes from '../constants/ActionTypes';
import AppDispatcher from '../dispatcher/AppDispatcher';
import BaseStore from './BaseStore';
import EventTypes from '../constants/EventTypes';
+import SettingsStore from './SettingsStore';
import TorrentActions from '../actions/TorrentActions';
import UIActions from '../actions/UIActions';
@@ -12,16 +13,7 @@ class TorrentFilterStoreClass extends BaseStore {
this.searchFilter = null;
this.statusFilter = 'all';
this.trackerFilter = 'all';
- this.sortTorrentsBy = {
- direction: 'desc',
- displayName: 'Date Added',
- property: 'sortBy',
- value: 'added'
- };
- }
-
- fetchSortProps() {
- UIActions.fetchSortProps();
+ this.sortTorrentsBy = SettingsStore.getSettings('sortTorrents');
}
fetchTorrentStatusCount() {
diff --git a/client/source/scripts/stores/TorrentStore.js b/client/source/scripts/stores/TorrentStore.js
index 6c875c61..7bf704a2 100644
--- a/client/source/scripts/stores/TorrentStore.js
+++ b/client/source/scripts/stores/TorrentStore.js
@@ -104,7 +104,10 @@ class TorrentStoreClass extends BaseStore {
handleAddTorrentSuccess(response) {
this.emit(EventTypes.CLIENT_ADD_TORRENT_SUCCESS);
- SettingsStore.saveSettings({id: 'torrentDestination', data: response.destination});
+ SettingsStore.saveSettings({
+ id: 'torrentDestination',
+ data: response.destination
+ });
NotificationStore.add({
adverb: 'Successfully',
diff --git a/client/source/scripts/stores/UIStore.js b/client/source/scripts/stores/UIStore.js
index 9e00f748..19e1237b 100644
--- a/client/source/scripts/stores/UIStore.js
+++ b/client/source/scripts/stores/UIStore.js
@@ -17,6 +17,10 @@ class UIStoreClass extends BaseStore {
this.torrentDetailsHash = null;
}
+ dismissModal() {
+ this.setActiveModal(null);
+ }
+
getActiveContextMenu() {
return this.activeContextMenu;
}
@@ -93,9 +97,9 @@ UIStore.dispatcherID = AppDispatcher.register((payload) => {
case ActionTypes.UI_DISPLAY_MODAL:
UIStore.setActiveModal(action.data);
break;
- case ActionTypes.CLIENT_MOVE_TORRENTS_SUCCESS:
case ActionTypes.CLIENT_ADD_TORRENT_SUCCESS:
- UIStore.setActiveModal(null);
+ case ActionTypes.CLIENT_MOVE_TORRENTS_SUCCESS:
+ UIStore.dismissModal();
break;
case ActionTypes.UI_DISPLAY_CONTEXT_MENU:
UIStore.setActiveContextMenu(action.data);
diff --git a/server/app.js b/server/app.js
index 56425c0d..3f238ffc 100644
--- a/server/app.js
+++ b/server/app.js
@@ -9,8 +9,7 @@ var logger = require('morgan');
var path = require('path');
var clientRoutes = require('./routes/client');
-var uiRoutes = require('./routes/ui');
-var routes = require('./routes/index');
+var mainRoutes = require('./routes/index');
var app = express();
@@ -36,9 +35,8 @@ app.use((req, res, next) => {
next();
});
-app.use('/', routes);
+app.use('/', mainRoutes);
app.use('/client', clientRoutes);
-app.use('/ui', uiRoutes);
// catch 404 and forward to error handler
app.use((req, res, next) => {
diff --git a/server/models/uiSettings.js b/server/models/uiSettings.js
deleted file mode 100644
index cc870f81..00000000
--- a/server/models/uiSettings.js
+++ /dev/null
@@ -1,62 +0,0 @@
-'use strict';
-
-let Datastore = require('nedb');
-
-let config = require('../../config');
-
-let uiDB = new Datastore({
- autoload: true,
- filename: `${config.dbPath}uiSettings.db`
-});
-
-uiDB.persistence.setAutocompactionInterval(config.dbCleanInterval);
-
-let getDbResponseHandler = (callback) => {
- return (error, docs) => {
- if (error) {
- callback(null, error);
- return;
- }
-
- if (Array.isArray(docs)) {
- callback(docs[0]);
- return;
- }
-
- callback(docs);
- return;
- };
-};
-
-let uiSettings = {
- get: (type, callback) => {
- uiDB.find({type}, getDbResponseHandler(callback));
- },
-
- set: (payload, callback) => {
- let newLocationData = Object.assign({}, {type: payload.type}, {data: payload.data});
- uiDB.update({type: payload.type, data: payload.data}, getDbResponseHandler(callback));
- },
-
- getLatestTorrentLocation: (callback) => {
- try {
- uiDB.find({type: 'location'}, getDbResponseHandler(callback));
- } catch (e) { console.log(e); }
- },
-
- getSortProps: (callback) => {
- uiDB.find({type: 'sort'}, getDbResponseHandler(callback));
- },
-
- setLatestTorrentLocation: (data, callback) => {
- let newLocationData = Object.assign({}, {type: 'location'}, {path: data.destination});
- uiDB.update({type: 'location'}, newLocationData, {upsert: true}, getDbResponseHandler(callback));
- },
-
- setSortProps: (sortProps, callback) => {
- let newSortPropData = Object.assign({}, {type: 'sort'}, sortProps);
- uiDB.update({type: 'sort'}, newSortPropData, {upsert: true}, getDbResponseHandler(callback));
- }
-}
-
-module.exports = uiSettings;
diff --git a/server/routes/ui.js b/server/routes/ui.js
deleted file mode 100644
index 632eade0..00000000
--- a/server/routes/ui.js
+++ /dev/null
@@ -1,36 +0,0 @@
-'use strict';
-
-let express = require('express');
-let router = express.Router();
-let xmlrpc = require('xmlrpc');
-
-let ajaxUtil = require('../util/ajaxUtil');
-let client = require('../models/client');
-let history = require('../models/history');
-let uiSettings = require('../models/uiSettings');
-
-router.get('/settings', (req, res, next) => {
- uiSettings.get(req.body.type, ajaxUtil.getResponseFn(res));
-});
-
-router.post('/settings', (req, res, next) => {
- uiSettings.set(req.body, ajaxUtil.getResponseFn(res));
-});
-
-router.post('/sort-props', (req, res, next) => {
- uiSettings.setSortProps(req.body, ajaxUtil.getResponseFn(res));
-});
-
-router.get('/sort-props', (req, res, next) => {
- uiSettings.getSortProps(ajaxUtil.getResponseFn(res));
-});
-
-router.get('/torrent-location', (req, res, next) => {
- uiSettings.getLatestTorrentLocation(ajaxUtil.getResponseFn(res));
-});
-
-router.post('/torrent-location', (req, res, next) => {
- uiSettings.setLatestTorrentLocation(req.body, ajaxUtil.getResponseFn(res));
-});
-
-module.exports = router;