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;