From 524a0796c9bcf95693f4f3fe0ae1ad8633441d47 Mon Sep 17 00:00:00 2001 From: John Furrow Date: Mon, 4 Sep 2017 23:00:55 -0700 Subject: [PATCH] Migrate AddTorrentsByURL --- .../components/general/CustomScrollbars.js | 4 +- .../components/general/ListViewport.js | 6 +- .../general/filesystem/FilesystemBrowser.js | 241 ++++++++++ .../general/filesystem/TorrentDestination.js | 455 +++++------------- .../general/form-elements/TextboxRepeater.js | 84 ++-- .../components/modals/ModalActions.js | 1 + .../add-torrents-modal/AddTorrentsActions.js | 3 +- .../add-torrents-modal/AddTorrentsByURL.js | 233 +++------ .../components/torrent-list/TorrentList.js | 2 +- client/src/sass/components/_context-menu.scss | 22 +- client/src/sass/components/_filesystem.scss | 18 +- client/src/sass/components/_scrollbars.scss | 2 +- server/models/client.js | 2 +- 13 files changed, 501 insertions(+), 572 deletions(-) create mode 100644 client/src/javascript/components/general/filesystem/FilesystemBrowser.js diff --git a/client/src/javascript/components/general/CustomScrollbars.js b/client/src/javascript/components/general/CustomScrollbars.js index 1bc7e340..4749500b 100644 --- a/client/src/javascript/components/general/CustomScrollbars.js +++ b/client/src/javascript/components/general/CustomScrollbars.js @@ -5,6 +5,8 @@ import {Scrollbars} from 'react-custom-scrollbars'; const METHODS_TO_BIND = ['getHorizontalThumb', 'getVerticalThumb']; export default class CustomScrollbar extends React.Component { + scrollbarRef = null; + constructor() { super(); @@ -63,7 +65,7 @@ export default class CustomScrollbar extends React.Component { return ( this.scrollbarRef = ref} renderView={this.renderView} renderThumbHorizontal={this.getHorizontalThumb} renderThumbVertical={this.getVerticalThumb} diff --git a/client/src/javascript/components/general/ListViewport.js b/client/src/javascript/components/general/ListViewport.js index 32fd6ab1..90968214 100644 --- a/client/src/javascript/components/general/ListViewport.js +++ b/client/src/javascript/components/general/ListViewport.js @@ -167,7 +167,7 @@ class ListViewport extends React.Component { scrollTop: 0, itemHeight: null }, () => { - this.nodeRefs.outerScrollbar.refs.scrollbar.scrollTop(0); + this.nodeRefs.outerScrollbar.scrollbarRef.scrollTop(0); }); } @@ -200,7 +200,7 @@ class ListViewport extends React.Component { scrollToTop() { if (this.state.scrollTop !== 0) { if (this.nodeRefs.outerScrollbar != null) { - this.nodeRefs.outerScrollbar.refs.scrollbar.scrollToTop(); + this.nodeRefs.outerScrollbar.scrollbarRef.scrollToTop(); } this.lastScrollTop = 0; @@ -218,7 +218,7 @@ class ListViewport extends React.Component { if (nodeRefs.outerScrollbar) { this.setState({ - viewportHeight: nodeRefs.outerScrollbar.refs.scrollbar.getClientHeight() + viewportHeight: nodeRefs.outerScrollbar.scrollbarRef.getClientHeight() }); } } diff --git a/client/src/javascript/components/general/filesystem/FilesystemBrowser.js b/client/src/javascript/components/general/filesystem/FilesystemBrowser.js new file mode 100644 index 00000000..c95595a9 --- /dev/null +++ b/client/src/javascript/components/general/filesystem/FilesystemBrowser.js @@ -0,0 +1,241 @@ +import React from 'react'; +import {defineMessages} from 'react-intl'; + +import ArrowIcon from '../../../components/icons/ArrowIcon'; +import CustomScrollbars from '../../../components/general/CustomScrollbars'; +import EventTypes from '../../../constants/EventTypes'; +import File from '../../../components/icons/File'; +import FolderClosedSolid from '../../../components/icons/FolderClosedSolid'; +import UIStore from '../../../stores/UIStore'; + +const MESSAGES = defineMessages({ + EACCES: { + id: 'filesystem.error.eacces', + defaultMessage: 'Flood does not have permission to read this directory.' + }, + ENOENT: { + id: 'filesystem.error.enoent', + defaultMessage: 'This path does not exist. It will be created.' + }, + emptyDirectory: { + id: 'filesystem.empty.directory', + defaultMessage: 'Empty directory.' + }, + fetching: { + id: 'filesystem.fetching', + defaultMessage: 'Fetching directory structure...' + } +}); + +class FilesystemBrowser extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + directory: props.directory, + separator: '/' + }; + } + + componentDidMount() { + UIStore.listen( + EventTypes.FLOOD_FETCH_DIRECTORY_LIST_ERROR, + this.handleDirectoryListFetchError + ); + UIStore.listen( + EventTypes.FLOOD_FETCH_DIRECTORY_LIST_SUCCESS, + this.handleDirectoryListFetchSuccess + ); + UIStore.fetchDirectoryList({path: this.state.directory}); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.directory !== this.props.directory) { + this.setState({directory: nextProps.directory}); + UIStore.fetchDirectoryList({path: nextProps.directory}); + } + } + + componentWillUnmount() { + UIStore.unlisten( + EventTypes.FLOOD_FETCH_DIRECTORY_LIST_ERROR, + this.handleDirectoryListFetchError + ); + UIStore.unlisten( + EventTypes.FLOOD_FETCH_DIRECTORY_LIST_SUCCESS, + this.handleDirectoryListFetchSuccess + ); + } + + getNewDestination(nextDirectorySegment) { + const {directory, separator} = this.state; + + if (directory.endsWith(separator)) { + return `${directory}${nextDirectorySegment}`; + } + + return `${directory}${separator}${nextDirectorySegment}`; + } + + handleDirectoryClick = (directory) => { + const nextDirectory = this.getNewDestination(directory); + + this.setState({ + directory: nextDirectory, + isFetching: true + }); + + if (this.props.onDirectorySelection) { + this.props.onDirectorySelection(nextDirectory); + } + }; + + handleDirectoryListFetchError = (error) => { + this.setState({ + error, + isFetching: false + }); + }; + + handleDirectoryListFetchSuccess = (response) => { + // response includes hasParent, separator, and an array of directories. + this.setState({ + ...response, + directory: response.path, + error: null, + isFetching: false + }); + }; + + handleParentDirectoryClick = () => { + let {directory, separator} = this.state; + + if (directory.endsWith(separator)) { + directory = directory.substring(0, directory.length - 1); + } + + let directoryArr = directory.split(separator); + directoryArr.pop(); + + directory = directoryArr.join(separator); + + this.setState({ + directory, + isFetching: true + }); + + if (this.props.onDirectorySelection) { + this.props.onDirectorySelection(directory); + } + }; + + render() { + const { + directories, + error, + files = [], + hasParent + } = this.state; + let errorMessage = null; + let listItems = null; + let parentDirectory = null; + let shouldShowDirectoryList = true; + let shouldForceShowParentDirectory = false; + + if (directories == null) { + shouldShowDirectoryList = false; + errorMessage = ( +
+ + {this.props.intl.formatMessage(MESSAGES.fetching)} + +
+ ); + } + + if (error && error.data && error.data.code && MESSAGES[error.data.code]) { + shouldShowDirectoryList = false; + + if (error.data.code === 'EACCES') { + shouldForceShowParentDirectory = true; + } + + errorMessage = ( +
+ + {this.props.intl.formatMessage(MESSAGES[error.data.code])} + +
+ ); + } + + if (hasParent || shouldForceShowParentDirectory) { + parentDirectory = ( +
  • + + {this.props.intl.formatMessage({ + id: 'filesystem.parent.directory', + defaultMessage: 'Parent Directory' + })} +
  • + ); + } + + if (shouldShowDirectoryList) { + const directoryList = directories.map((directory, index) => { + return ( +
  • this.handleDirectoryClick(directory)}> + + {directory} +
  • + ); + }); + + const filesList = files.map((file, index) => { + return ( +
  • + + {file} +
  • + ); + }); + + listItems = directoryList.concat(filesList); + } + + if ((!listItems || listItems.length === 0) && !errorMessage) { + errorMessage = ( +
    + + {this.props.intl.formatMessage(MESSAGES.emptyDirectory)} + +
    + ); + } + + return ( + +
    + {parentDirectory} + {errorMessage} + {listItems} +
    +
    + ); + } +} + +export default FilesystemBrowser; diff --git a/client/src/javascript/components/general/filesystem/TorrentDestination.js b/client/src/javascript/components/general/filesystem/TorrentDestination.js index c46e506d..c2c87f1b 100644 --- a/client/src/javascript/components/general/filesystem/TorrentDestination.js +++ b/client/src/javascript/components/general/filesystem/TorrentDestination.js @@ -1,222 +1,66 @@ -import classnames from 'classnames'; -import CSSTransitionGroup from 'react-addons-css-transition-group'; -import {defineMessages, FormattedMessage, injectIntl} from 'react-intl'; +import _ from 'lodash'; +import {Checkbox, ContextMenu, FormElementAddon, FormRow, FormRowGroup, Portal, Textbox} from 'flood-ui-kit'; +import {FormattedMessage, injectIntl} from 'react-intl'; import React from 'react'; -import ArrowIcon from '../../../components/icons/ArrowIcon'; -import File from '../../../components/icons/File'; -import FolderClosedSolid from '../../../components/icons/FolderClosedSolid'; -import Checkbox from '../../general/form-elements/Checkbox'; -import CustomScrollbars from '../../../components/general/CustomScrollbars'; import EventTypes from '../../../constants/EventTypes'; -import Portal from '../../../components/general/Portal'; +import FilesystemBrowser from './FilesystemBrowser'; import Search from '../../../components/icons/Search'; import SettingsStore from '../../../stores/SettingsStore'; import UIStore from '../../../stores/UIStore'; -const MAX_PANEL_HEIGHT = 300; +class NewTorrentDestination extends React.Component { + contextMenuInstanceRef = null; + contextMenuNodeRef = null; -const MESSAGES = defineMessages({ - EACCES: { - id: 'filesystem.error.eacces', - defaultMessage: 'Flood does not have permission to read this directory.' - }, - ENOENT: { - id: 'filesystem.error.enoent', - defaultMessage: 'This path does not exist. It will be created.' - }, - emptyDirectory: { - id: 'filesystem.empty.directory', - defaultMessage: 'Empty directory.' - }, - fetching: { - id: 'filesystem.fetching', - defaultMessage: 'Fetching directory structure...' - } -}); - -const METHODS_TO_BIND = [ - 'handleBasePathCheckBoxCheck', - 'handleDestinationChange', - 'handleDirectoryClick', - 'handleDirectoryListButtonClick', - 'handleDirectoryListFetchError', - 'handleDirectoryListFetchSuccess', - 'handleDocumentClick', - 'handleModalDismiss', - 'handleParentDirectoryClick', - 'updateAttachedPanelPosition' -]; - -class TorrentDestination extends React.Component { constructor(props) { - super(); + super(props); - let baseDestination = SettingsStore.getFloodSettings('torrentDestination') + const destination = props.suggested + || SettingsStore.getFloodSettings('torrentDestination') || SettingsStore.getClientSettings('directoryDefault') || ''; - if (props.suggested) { - baseDestination = props.suggested; - } - this.state = { - attachedPanelMaxHeight: MAX_PANEL_HEIGHT, - baseDestination, - destination: baseDestination, + destination, isBasePath: false, error: null, directories: null, files: null, isFetching: false, - isDirectoryListOpen: false, - separator: '/' + isDirectoryListOpen: false }; - METHODS_TO_BIND.forEach((method) => { - this[method] = this[method].bind(this); - }); - + this.handleWindowResize = _.debounce(this.handleWindowResize, 100); } componentDidMount() { - UIStore.listen(EventTypes.FLOOD_FETCH_DIRECTORY_LIST_ERROR, - this.handleDirectoryListFetchError); - UIStore.listen(EventTypes.FLOOD_FETCH_DIRECTORY_LIST_SUCCESS, - this.handleDirectoryListFetchSuccess); UIStore.listen(EventTypes.UI_MODAL_DISMISSED, this.handleModalDismiss); - UIStore.fetchDirectoryList({path: this.state.baseDestination}); - - global.addEventListener('resize', this.updateAttachedPanelPosition); - global.document.addEventListener('click', this.handleDocumentClick); } - componentDidUpdate() { - this.updateAttachedPanelPosition(); + componentWillUpdate(_nextProps, nextState) { + if (!this.state.isDirectoryListOpen && nextState.isDirectoryListOpen) { + this.addDestinationOpenEventListeners(); + } else if (this.state.isDirectoryListOpen && !nextState.isDirectoryListOpen) { + this.removeDestinationOpenEventListeners() + } } componentWillUnmount() { - UIStore.unlisten(EventTypes.FLOOD_FETCH_DIRECTORY_LIST_ERROR, - this.handleDirectoryListFetchError); - UIStore.unlisten(EventTypes.FLOOD_FETCH_DIRECTORY_LIST_SUCCESS, - this.handleDirectoryListFetchSuccess); UIStore.unlisten(EventTypes.UI_MODAL_DISMISSED, this.handleModalDismiss); - global.removeEventListener('resize', this.updateAttachedPanelPosition); - global.document.removeEventListener('click', this.handleDocumentClick); + this.removeDestinationOpenEventListeners() } - getNewDestination(directory) { - let {baseDestination: newDestination, separator} = this.state; - - if (newDestination.endsWith(separator)) { - return `${newDestination}${directory}`; - } - - return `${newDestination}${separator}${directory}`; + addDestinationOpenEventListeners() { + global.document.addEventListener('click', this.handleDocumentClick); + global.addEventListener('resize', this.handleWindowResize); } - getDirectoryList() { - let { - attachedPanelMaxHeight, - directories, - error, - files = [], - hasParent - } = this.state; - let errorMessage = null; - let listItems = null; - let parentDirectory = null; - let shouldShowDirectoryList = true; - let shouldForceShowParentDirectory = false; - - if (directories == null) { - shouldShowDirectoryList = false; - errorMessage = ( - - {this.props.intl.formatMessage(MESSAGES.fetching)} - - ); + closeDirectoryList = () => { + if (this.state.isDirectoryListOpen) { + this.setState({isDirectoryListOpen: false}); } - - if (error && error.data && error.data.code && MESSAGES[error.data.code]) { - shouldShowDirectoryList = false; - - if (error.data.code === 'EACCES') { - shouldForceShowParentDirectory = true; - } - - errorMessage = ( - - {this.props.intl.formatMessage(MESSAGES[error.data.code])} - - ); - } - - if (hasParent || shouldForceShowParentDirectory) { - parentDirectory = ( -
  • {this.handleParentDirectoryClick();}}> - - {this.props.intl.formatMessage({ - id: 'filesystem.parent.directory', - defaultMessage: 'Parent Directory' - })} -
  • - ); - } - - if (shouldShowDirectoryList) { - let directoryList = directories.map((directory, index) => { - return ( -
  • {this.handleDirectoryClick(directory);}}> - - {directory} -
  • - ); - }); - let filesList = files.map((file, index) => { - return ( -
  • - - {file} -
  • - ); - }); - - listItems = directoryList.concat(filesList); - } - - if ((!listItems || listItems.length === 0) && !errorMessage) { - errorMessage = ( - - {this.props.intl.formatMessage(MESSAGES.emptyDirectory)} - - ); - } - - return ( -
    {this.attachedPanelRef = ref;}}> - -
    - {parentDirectory} - {errorMessage} - {listItems} -
    -
    -
    - ); - } + }; getValue() { return this.getDestination(); @@ -226,202 +70,123 @@ class TorrentDestination extends React.Component { return this.state.destination; } - isBasePath() { - return this.state.isBasePath; - } - - handleBasePathCheckBoxCheck(value) { + handleBasePathCheckBoxCheck = (value) => { this.setState({isBasePath: value}); - } + }; - handleDestinationChange(event) { - let destination = event.target.value; + handleDestinationChange = (event) => { + const destination = event.target.value; if (this.props.onChange) { this.props.onChange(destination); } - this.setState({baseDestination: destination, destination}); + this.setState({destination}); + }; - if (this.state.isDirectoryListOpen) { - UIStore.fetchDirectoryList({path: destination}); - } - } - - handleDirectoryListButtonClick(event) { - event.nativeEvent.stopImmediatePropagation(); - - let isOpening = !this.state.isDirectoryListOpen; + handleDirectoryListButtonClick = (event) => { + const isOpening = !this.state.isDirectoryListOpen; this.setState({ isDirectoryListOpen: isOpening, isFetching: isOpening }); + }; - if (isOpening) { - UIStore.fetchDirectoryList({path: this.state.destination}); - } + handleDirectorySelection = destination => { + // eslint-disable-next-line react/no-direct-mutation-state + this.state.textboxRef.value = destination; + this.setState({destination}); + }; + + handleDocumentClick = () => { + this.closeDirectoryList(); + }; + + handleModalDismiss = () => { + this.closeDirectoryList(); + }; + + handleWindowResize = () => { + this.closeDirectoryList(); + }; + + isBasePath() { + return this.state.isBasePath; } - handleDirectoryClick(directory) { - let newDestination = this.getNewDestination(directory); + removeDestinationOpenEventListeners() { + global.document.removeEventListener('click', this.handleDocumentClick); + global.removeEventListener('resize', this.handleWindowResize); + } + setTextboxRef = (ref) => { + if (this.state.textboxRef !== ref) { + this.setState({textboxRef: ref}); + } + }; + + toggleOpenState = () => { this.setState({ - baseDestination: newDestination, - destination: newDestination, - isFetching: true + isDirectoryListOpen: !this.state.isDirectoryListOpen }); - - if (this.props.onChange) { - this.props.onChange(newDestination); - } - - UIStore.fetchDirectoryList({path: newDestination}); - } - - handleDirectoryListFetchError(error) { - this.setState({ - error, - isFetching: false - }); - } - - handleDirectoryListFetchSuccess(response) { - // response includes hasParent, separator, and an array of directories. - this.setState({ - ...response, - baseDestination: response.path, - destination: response.path, - error: null, - isFetching: false - }); - } - - handleDocumentClick() { - if (this.state.isDirectoryListOpen) { - this.setState({isDirectoryListOpen: false}); - } - } - - handleModalDismiss() { - if (this.state.isDirectoryListOpen) { - this.setState({isDirectoryListOpen: false}); - } - } - - handlePanelClick(event) { - event.nativeEvent.stopImmediatePropagation(); - } - - handleParentDirectoryClick() { - let {destination, separator} = this.state; - - if (destination.endsWith(separator)) { - destination = destination.substring(0, destination.length - 1); - } - - let destinationArr = destination.split(separator); - destinationArr.pop(); - - destination = destinationArr.join(separator); - - this.setState({ - baseDestination: destination, - destination, - isFetching: true - }); - - if (this.props.onChange) { - this.props.onChange(destination); - } - - UIStore.fetchDirectoryList({path: destination}); - } - - handleTextboxClick(event) { - event.nativeEvent.stopImmediatePropagation(); - } - - updateAttachedPanelPosition() { - if (this.state.isDirectoryListOpen) { - global.requestAnimationFrame(() => { - if (this.textboxRef && this.attachedPanelRef) { - let windowHeight = window.innerHeight; - let {height: panelHeight} = this.attachedPanelRef - .getBoundingClientRect(); - let {left, bottom, width} = this.textboxRef.getBoundingClientRect(); - - this.attachedPanelRef.setAttribute( - 'style', `left: ${left}px; top: ${bottom}px; width: ${width}px;` - ); - - if (bottom + panelHeight >= windowHeight) { - let attachedPanelMaxHeight = Math.floor(windowHeight - bottom); - - if (this.state.attachedPanelMaxHeight !== attachedPanelMaxHeight) { - this.setState({attachedPanelMaxHeight}); - } - } else if (bottom + panelHeight + 10 < windowHeight - && this.state.attachedPanelMaxHeight !== MAX_PANEL_HEIGHT) { - this.setState({attachedPanelMaxHeight: MAX_PANEL_HEIGHT}); - } - } - }); - } - } + }; render() { - let textboxClasses = classnames('textbox', { - 'textbox--has-attached-panel--is-open': this.state.isDirectoryListOpen, - 'is-fulfilled': this.state.destination && this.state.destination !== '' - }); - let directoryList = null; - - if (this.state.isDirectoryListOpen) { - directoryList = this.getDirectoryList(); - } - return ( -
    -
    - + + event.nativeEvent.stopImmediatePropagation()} placeholder={this.props.intl.formatMessage({ id: 'torrents.add.destination.placeholder', defaultMessage: 'Destination' })} - ref={(ref) => {this.textboxRef = ref;}} - value={this.state.destination} - type="text" /> -
    - -
    - - - {directoryList} - - -
    -
    - + + + event.nativeEvent.stopImmediatePropagation()} + overlayProps={{isInteractive: false}} + padding={false} + ref={ref => this.contextMenuInstanceRef = ref} + setRef={ref => this.contextMenuNodeRef = ref} + scrolling={false} + triggerRef={this.state.textboxRef} + > + + + + + + + -
    -
    + + ); } } -export default injectIntl(TorrentDestination, {withRef: true}); +export default injectIntl(NewTorrentDestination, {withRef: true}); diff --git a/client/src/javascript/components/general/form-elements/TextboxRepeater.js b/client/src/javascript/components/general/form-elements/TextboxRepeater.js index 4af2f320..c752d718 100644 --- a/client/src/javascript/components/general/form-elements/TextboxRepeater.js +++ b/client/src/javascript/components/general/form-elements/TextboxRepeater.js @@ -1,75 +1,67 @@ -import classnames from 'classnames'; +import {FormElementAddon, FormRow, FormRowGroup, Textbox} from 'flood-ui-kit'; import React from 'react'; import AddMini from '../../icons/AddMini'; import RemoveMini from '../../icons/RemoveMini'; -const METHODS_TO_BIND = [ - 'getTextboxes', - 'handleTextboxChange' -]; +export default class TextboxRepeater extends React.PureComponent { + state = { + textboxes: [{id: 0, value: ''}] + }; -export default class TextboxRepeater extends React.Component { - constructor() { - super(); + _idCounter = 0; - METHODS_TO_BIND.forEach((method) => { - this[method] = this[method].bind(this); - }); + getID() { + return ++this._idCounter; } - getTextboxes() { - let textboxes = this.props.textboxes.map((textbox, index) => { - let addButton = ( - - ); + getTextboxes = () => { + return this.state.textboxes.map((textbox, index) => { let removeButton = null; if (index > 0) { removeButton = ( - + ); } - let inputClasses = classnames('textbox', { - 'is-fulfilled': textbox.value && textbox.value !== '' - }); - return ( -
    -
    - -
    - {removeButton} - {addButton} -
    -
    -
    + + + + + + {removeButton} + + ); }); + }; - return textboxes; - } + handleTextboxAdd = (index) => { + const textboxes = Object.assign([], this.state.textboxes); + textboxes.splice(index + 1, 0, {id: this.getID(), value: ''}); + this.setState({textboxes}); + }; - handleTextboxChange(index, event) { - this.props.handleTextboxChange(index, event.target.value); - } + handleTextboxRemove = (index) => { + const textboxes = Object.assign([], this.state.textboxes); + textboxes.splice(index, 1); + this.setState({textboxes}); + }; render() { return ( -
    + {this.getTextboxes()} -
    + ); } } diff --git a/client/src/javascript/components/modals/ModalActions.js b/client/src/javascript/components/modals/ModalActions.js index c5e81f26..3462190b 100644 --- a/client/src/javascript/components/modals/ModalActions.js +++ b/client/src/javascript/components/modals/ModalActions.js @@ -15,6 +15,7 @@ export default class ModalActions extends React.Component { return ( diff --git a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsActions.js b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsActions.js index 3cf5d42d..a6f535c3 100644 --- a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsActions.js +++ b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsActions.js @@ -51,6 +51,7 @@ class AddTorrentsActions extends React.Component { id: 'torrents.add.start.label', defaultMessage: 'Start Torrent' }), + id: 'start', triggerDismiss: false, type: 'checkbox' }, @@ -61,7 +62,7 @@ class AddTorrentsActions extends React.Component { defaultMessage: 'Cancel' }), triggerDismiss: true, - type: 'secondary' + type: 'tertiary' }, { clickHandler: this.props.onAddTorrentsClick, diff --git a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByURL.js b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByURL.js index 8b8eb72e..f1ea24e8 100644 --- a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByURL.js +++ b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByURL.js @@ -1,192 +1,105 @@ -import _ from 'lodash'; -import {defineMessages, FormattedMessage, injectIntl} from 'react-intl'; +import {Form, FormRow, Textbox} from 'flood-ui-kit'; +import {FormattedMessage, injectIntl} from 'react-intl'; import React from 'react'; import AddTorrentsActions from './AddTorrentsActions'; -import FormColumn from '../../general/form-elements/FormColumn'; -import FormLabel from '../../general/form-elements/FormLabel'; + +import ModalFormSectionHeader from '../../modals/ModalFormSectionHeader'; import SettingsStore from '../../../stores/SettingsStore'; import TextboxRepeater from '../../general/form-elements/TextboxRepeater'; import TorrentActions from '../../../actions/TorrentActions'; import TorrentDestination from '../../general/filesystem/TorrentDestination'; -import Validator from '../../../util/Validator'; - -const messages = defineMessages({ - mustSpecifyDestination: { - id: 'torrents.add.tab.destination.empty', - defaultMessage: 'You must specify a destination.' - }, - mustSpecifyURLs: { - id: 'torrents.add.tab.urls.empty', - defaultMessage: 'You must specify at least one torrent.' - } -}); - -const METHODS_TO_BIND = [ - 'handleAddTorrents', - 'handleStartTorrentsToggle', - 'handleTagsChange', - 'handleUrlAdd', - 'handleUrlChange', - 'handleUrlRemove' -]; class AddTorrentsByURL extends React.Component { - constructor(props) { - super(); + _formData = {}; - this.state = { - addTorrentsError: null, - errors: {}, - isAddingTorrents: false, - tags: '', - urlTextboxes: [{value: ''}], - startTorrents: SettingsStore.getFloodSettings('startTorrentsOnLoad') - }; + state = { + errors: {}, + isAddingTorrents: false, + tags: '', + urlTextboxes: [{value: ''}], + startTorrents: SettingsStore.getFloodSettings('startTorrentsOnLoad') + }; - METHODS_TO_BIND.forEach((method) => { - this[method] = this[method].bind(this); - }); + getURLsFromForm() { + return Object.keys(this._formData).reduce( + (accumulator, formItemKey) => { + if (/^urls/.test(formItemKey)) { + accumulator.push(this._formData[formItemKey]); + } - this.validatedFields = { - destination: { - isValid: Validator.isNotEmpty, - error: props.intl.formatMessage(messages.mustSpecifyDestination) + return accumulator; }, - urls: { - isValid: value => value !== '' && value != null, - error: props.intl.formatMessage(messages.mustSpecifyURLs) - } - }; + [] + ); } - handleAddTorrents() { + handleAddTorrents = () => { if (this.isFormValid()) { this.setState({isAddingTorrents: true}); - let torrentURLs = _.map(this.state.urlTextboxes, 'value'); TorrentActions.addTorrentsByUrls({ - urls: torrentURLs, - destination: this.torrentDestinationRef.getWrappedInstance().getDestination(), - isBasePath: this.torrentDestinationRef.getWrappedInstance().isBasePath(), - start: this.state.startTorrents, - tags: this.state.tags.split(',') + urls: this.getURLsFromForm(), + destination: this._formData.destination, + isBasePath: this._formData.useBasePath, + start: this._formData.start, + tags: this._formData.tags.split(',') }); } - } + }; - handleStartTorrentsToggle(value) { - this.setState({startTorrents: value}); - } - - handleTagsChange(event) { - this.setState({tags: event.target.value}); - } - - handleUrlRemove(index) { - let urlTextboxes = Object.assign([], this.state.urlTextboxes); - urlTextboxes.splice(index, 1); - this.setState({urlTextboxes}); - } - - handleUrlAdd(index) { - let urlTextboxes = Object.assign([], this.state.urlTextboxes); - urlTextboxes.splice(index + 1, 0, {value: ''}); - this.setState({urlTextboxes}); - } - - handleUrlChange(index, value) { - let urlTextboxes = Object.assign([], this.state.urlTextboxes); - urlTextboxes[index].value = value; - this.setState({urlTextboxes}); - } + handleFormChange = ({event, formData}) => { + this._formData = formData; + }; isFormValid() { - const areURLsDefined = this.state.urlTextboxes.some(({value}) => { - return this.validatedFields.urls.isValid(value); - }); - const isDestinationValid = this.validatedFields.destination - .isValid(this.torrentDestinationRef.getWrappedInstance().getDestination()); - const nextErrorsState = {}; - - if (!areURLsDefined) { - nextErrorsState.urls = this.validatedFields.urls.error; - } - - if (!isDestinationValid) { - nextErrorsState.destination = this.validatedFields.destination.error; - } - - if (!areURLsDefined || !isDestinationValid) { - this.setState({errors: nextErrorsState}); - } - - return isDestinationValid && areURLsDefined; + return true; } render() { - let error = null; - - if (this.state.addTorrentsError) { - error = ( -
    -
    - {this.state.addTorrentsError} -
    -
    - ); - } - return ( -
    - {error} -
    - - - - - - -
    -
    - - - - - this.torrentDestinationRef = ref} /> - -
    -
    - - - - - - -
    - + + + + + + + + + + + + + + + -
    + isAddingTorrents={this.state.isAddingTorrents} + /> + ); } } diff --git a/client/src/javascript/components/torrent-list/TorrentList.js b/client/src/javascript/components/torrent-list/TorrentList.js index b1a37634..9938f3ed 100644 --- a/client/src/javascript/components/torrent-list/TorrentList.js +++ b/client/src/javascript/components/torrent-list/TorrentList.js @@ -566,7 +566,7 @@ class TorrentListContainer extends React.Component { if (this.horizontalScrollRef != null) { this.setState({ torrentListViewportSize: - this.horizontalScrollRef.refs.scrollbar.getClientWidth() + this.horizontalScrollRef.scrollbarRef.getClientWidth() }); } } diff --git a/client/src/sass/components/_context-menu.scss b/client/src/sass/components/_context-menu.scss index fcd39264..99e0fb5b 100644 --- a/client/src/sass/components/_context-menu.scss +++ b/client/src/sass/components/_context-menu.scss @@ -1,12 +1,12 @@ -.context-menu { - font-size: 0.9em; - padding: $spacing-unit * 2/5 0; - opacity: 0; - position: fixed; - transition: opacity 0.25s, visibility 0.25s; - z-index: 10; +// .context-menu { +// font-size: 0.9em; +// padding: $spacing-unit * 2/5 0; +// opacity: 0; +// position: fixed; +// transition: opacity 0.25s, visibility 0.25s; +// z-index: 10; - &--is-open { - opacity: 1; - } -} +// &--is-open { +// opacity: 1; +// } +// } diff --git a/client/src/sass/components/_filesystem.scss b/client/src/sass/components/_filesystem.scss index f25fe13b..e1b8184f 100644 --- a/client/src/sass/components/_filesystem.scss +++ b/client/src/sass/components/_filesystem.scss @@ -1,5 +1,7 @@ $filesystem--directory-list--foreground: #5e728c; -$filesystem--directory-list--foreground--hover: saturate(lighten($filesystem--directory-list--foreground, 15%), 10%); +$filesystem--directory-list--foreground--hover: saturate(darken($filesystem--directory-list--foreground, 15%), 10%); +$filesystem--directory-list--background--hover: rgba($filesystem--directory-list--foreground, 0.1); +$filesystem--directory-list--parent--border-color: lighten($filesystem--directory-list--foreground, 43%); .filesystem { @@ -13,11 +15,14 @@ $filesystem--directory-list--foreground--hover: saturate(lighten($filesystem--di list-style: none; &__item { + padding: $spacing--xx-small $spacing--small; transition: color 0.25s; + white-space: nowrap; &--parent, &--directory { cursor: pointer; + transition: background $speed--x-fast, color $speed--x-fast; user-select: none; &:hover { @@ -25,8 +30,17 @@ $filesystem--directory-list--foreground--hover: saturate(lighten($filesystem--di } } + &--directory { + + &:hover { + background: $filesystem--directory-list--background--hover; + } + } + &--parent { - margin-bottom: $spacing-unit * 1/5; + border-bottom: 1px solid $filesystem--directory-list--parent--border-color; + margin-bottom: $spacing--small; + padding-bottom: $spacing--small; opacity: 0.75; .icon { diff --git a/client/src/sass/components/_scrollbars.scss b/client/src/sass/components/_scrollbars.scss index bb7ee893..4fc215e1 100644 --- a/client/src/sass/components/_scrollbars.scss +++ b/client/src/sass/components/_scrollbars.scss @@ -10,7 +10,7 @@ $scrollbar--thumb--background--inverted--hover: rgba(#e9eef2, 0.6); border-radius: 10px; cursor: pointer; opacity: 0; - transition: background 0.25s, opacity 0.5s, transform 0.125s; + transition: background 0.25s, opacity 0.5s; z-index: 2; &:active { diff --git a/server/models/client.js b/server/models/client.js index fc510a23..367ae997 100644 --- a/server/models/client.js +++ b/server/models/client.js @@ -53,7 +53,7 @@ var client = { addUrls (data, callback) { let urls = data.urls; let path = data.destination; - let isBasePath = data.isBasePath === 'true'; + let isBasePath = data.isBasePath; let start = data.start; let tags = data.tags; let request = new ClientRequest();