mirror of
https://github.com/zoriya/flood.git
synced 2025-12-22 23:25:27 +00:00
Add move torrents option to context menu
This commit is contained in:
@@ -190,6 +190,28 @@ const TorrentActions = {
|
||||
});
|
||||
},
|
||||
|
||||
moveTorrents: function(hashes, options) {
|
||||
let {destination, filenames, sources, moveFiles} = options;
|
||||
|
||||
return axios.post('/client/torrents/move',
|
||||
{hashes, destination, filenames, sources, moveFiles})
|
||||
.then((json = {}) => {
|
||||
return json.data;
|
||||
})
|
||||
.then((data) => {
|
||||
AppDispatcher.dispatchServerAction({
|
||||
type: ActionTypes.CLIENT_MOVE_TORRENTS_SUCCESS,
|
||||
data
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
AppDispatcher.dispatchServerAction({
|
||||
type: ActionTypes.CLIENT_MOVE_TORRENTS_ERROR,
|
||||
error
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
pauseTorrents: function(hashes) {
|
||||
return axios.post('/client/pause', {
|
||||
hashes
|
||||
|
||||
@@ -2,6 +2,7 @@ import axios from 'axios';
|
||||
|
||||
import AppDispatcher from '../dispatcher/AppDispatcher';
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
import TorrentStore from '../stores/TorrentStore';
|
||||
|
||||
const UIActions = {
|
||||
displayContextMenu: function(data) {
|
||||
|
||||
54
client/source/scripts/components/forms/Checkbox.js
Normal file
54
client/source/scripts/components/forms/Checkbox.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import classnames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import Checkmark from '../icons/Checkmark';
|
||||
|
||||
const METHODS_TO_BIND = ['handleCheckboxChange'];
|
||||
|
||||
export default class SearchBox extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {checked: false};
|
||||
|
||||
METHODS_TO_BIND.forEach((method) => {
|
||||
this[method] = this[method].bind(this);
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.state.checked !== this.props.checked) {
|
||||
this.setState({checked: this.props.checked});
|
||||
}
|
||||
}
|
||||
|
||||
handleCheckboxChange() {
|
||||
let currentCheckedState = this.state.checked;
|
||||
let newCheckedState = !currentCheckedState;
|
||||
|
||||
this.setState({checked: newCheckedState})
|
||||
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(newCheckedState);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let classes = classnames('checkbox', {
|
||||
'is-checked': this.state.checked
|
||||
});
|
||||
|
||||
return (
|
||||
<label className={classes}>
|
||||
<input type="checkbox" checked={this.state.checked}
|
||||
onChange={this.handleCheckboxChange} />
|
||||
<span className="checkbox__decoy">
|
||||
<Checkmark />
|
||||
</span>
|
||||
<span className="checkbox__label">
|
||||
{this.props.children}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
}
|
||||
14
client/source/scripts/components/icons/Checkmark.js
Normal file
14
client/source/scripts/components/icons/Checkmark.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import BaseIcon from './BaseIcon';
|
||||
|
||||
export default class Checkmark extends BaseIcon {
|
||||
render() {
|
||||
return (
|
||||
<svg className={`icon icon--checkmark ${this.props.className}`}
|
||||
xmlns={this.getXmlns()} viewBox={this.getViewBox()}>
|
||||
<polygon points="55.5,18.6 46.1,8.7 24.4,31.5 13.9,20.4 4.5,30.3 24.4,51.3 24.4,51.3 24.4,51.3"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,11 @@ export default class AddTorrents extends React.Component {
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({destination: UIStore.getLatestTorrentLocation()});
|
||||
let destination = UIStore.getLatestTorrentLocation();
|
||||
if (this.props.suggested) {
|
||||
destination = this.props.suggested;
|
||||
}
|
||||
this.setState({destination});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -46,6 +50,10 @@ export default class AddTorrents extends React.Component {
|
||||
}
|
||||
|
||||
onLatestTorrentLocationChange() {
|
||||
if (this.props.suggested) {
|
||||
return;
|
||||
}
|
||||
|
||||
let destination = UIStore.getLatestTorrentLocation();
|
||||
|
||||
if (this.props.onChange) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import CSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
import React from 'react';
|
||||
|
||||
import AddTorrents from './AddTorrents';
|
||||
import MoveTorrents from './MoveTorrents';
|
||||
import EventTypes from '../../constants/EventTypes';
|
||||
import Modal from './Modal';
|
||||
import UIActions from '../../actions/UIActions';
|
||||
@@ -81,6 +82,11 @@ export default class Modals extends React.Component {
|
||||
heading={modalOptions.heading} />
|
||||
);
|
||||
break;
|
||||
case 'move-torrents':
|
||||
modal = (
|
||||
<MoveTorrents dismiss={this.dismissModal} />
|
||||
);
|
||||
break;
|
||||
case 'add-torrents':
|
||||
modal = (
|
||||
<AddTorrents dismiss={this.dismissModal} />
|
||||
|
||||
152
client/source/scripts/components/modals/MoveTorrents.js
Normal file
152
client/source/scripts/components/modals/MoveTorrents.js
Normal file
@@ -0,0 +1,152 @@
|
||||
import _ from 'lodash';
|
||||
import classnames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import AddTorrentsDestination from './AddTorrentsDestination';
|
||||
import AppDispatcher from '../../dispatcher/AppDispatcher';
|
||||
import Checkbox from '../forms/Checkbox';
|
||||
import EventTypes from '../../constants/EventTypes';
|
||||
import LoadingIndicatorDots from '../icons/LoadingIndicatorDots';
|
||||
import Modal from './Modal';
|
||||
import ModalActions from './ModalActions';
|
||||
import TorrentActions from '../../actions/TorrentActions';
|
||||
import TorrentStore from '../../stores/TorrentStore';
|
||||
|
||||
const METHODS_TO_BIND = [
|
||||
'confirmMoveTorrents',
|
||||
'handleCheckboxChange',
|
||||
'handleDestinationChange',
|
||||
'handleTextboxChange',
|
||||
'onMoveError'
|
||||
];
|
||||
|
||||
export default class AddTorrents extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
moveTorrentsError: null,
|
||||
destination: null,
|
||||
isExpanded: false,
|
||||
isSettingDownloadPath: false,
|
||||
moveTorrents: false,
|
||||
originalSource: null
|
||||
};
|
||||
|
||||
METHODS_TO_BIND.forEach((method) => {
|
||||
this[method] = this[method].bind(this);
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
let filenames = TorrentStore.getSelectedTorrentsFilename();
|
||||
let sources = TorrentStore.getSelectedTorrentsDownloadLocations();
|
||||
|
||||
if (sources.length === 1) {
|
||||
let originalSource = this.removeTrailingFilename(sources[0], filenames[0]);
|
||||
this.setState({originalSource, destination: originalSource});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
TorrentStore.listen(EventTypes.CLIENT_MOVE_TORRENTS_REQUEST_ERROR, this.onMoveError);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
TorrentStore.unlisten(EventTypes.CLIENT_MOVE_TORRENTS_REQUEST_ERROR, this.onMoveError);
|
||||
}
|
||||
|
||||
onMoveError() {
|
||||
this.setState({isSettingDownloadPath: false});
|
||||
}
|
||||
|
||||
confirmMoveTorrents() {
|
||||
let filenames = TorrentStore.getSelectedTorrentsFilename();
|
||||
let sources = TorrentStore.getSelectedTorrentsDownloadLocations();
|
||||
|
||||
if (sources.length) {
|
||||
this.setState({isSettingDownloadPath: true});
|
||||
TorrentActions.moveTorrents(TorrentStore.getSelectedTorrents(), {
|
||||
destination: this.state.destination,
|
||||
filenames,
|
||||
moveFiles: this.state.moveTorrents,
|
||||
sources
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getActions() {
|
||||
let icon = null;
|
||||
let primaryButtonText = 'Set Location';
|
||||
|
||||
if (this.state.isSettingDownloadPath) {
|
||||
icon = <LoadingIndicatorDots viewBox="0 0 32 32" />;
|
||||
primaryButtonText = 'Setting...';
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
clickHandler: null,
|
||||
content: 'Cancel',
|
||||
triggerDismiss: true,
|
||||
type: 'secondary'
|
||||
},
|
||||
{
|
||||
clickHandler: this.confirmMoveTorrents,
|
||||
content: (
|
||||
<span>
|
||||
{icon}
|
||||
{primaryButtonText}
|
||||
</span>
|
||||
),
|
||||
supplementalClassName: icon != null ? 'has-icon' : '',
|
||||
triggerDismiss: false,
|
||||
type: 'primary'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
handleCheckboxChange(checkboxState) {
|
||||
this.setState({moveTorrents: checkboxState});
|
||||
}
|
||||
|
||||
handleDestinationChange(destination) {
|
||||
this.setState({destination});
|
||||
}
|
||||
|
||||
handleTextboxChange(event) {
|
||||
let destination = event.target.value;
|
||||
this.setState({destination});
|
||||
}
|
||||
|
||||
getContent() {
|
||||
return (
|
||||
<div className="form">
|
||||
<AddTorrentsDestination onChange={this.handleDestinationChange}
|
||||
suggested={this.state.originalSource} />
|
||||
<div className="form__row">
|
||||
<Checkbox onChange={this.handleCheckboxChange}>Move data</Checkbox>
|
||||
</div>
|
||||
<ModalActions actions={this.getActions()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
removeTrailingFilename(path, filename) {
|
||||
let directoryPath = path.substring(0, path.length - filename.length);
|
||||
|
||||
if (directoryPath.charAt(directoryPath.length - 1) === '/' || directoryPath.charAt(directoryPath.length - 1) === '\\') {
|
||||
directoryPath = directoryPath.substring(0, directoryPath.length - 1);
|
||||
}
|
||||
|
||||
return directoryPath;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal heading="Set Download Location"
|
||||
dismiss={this.props.dismiss}
|
||||
content={this.getContent()} />
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import UIStore from '../../stores/UIStore';
|
||||
const METHODS_TO_BIND = [
|
||||
'onReceiveTorrentsError',
|
||||
'onReceiveTorrentsSuccess',
|
||||
'handleContextMenuItemClick',
|
||||
'handleDetailsClick',
|
||||
'handleRightClick',
|
||||
'handleTorrentClick',
|
||||
@@ -101,6 +102,10 @@ export default class TorrentListContainer extends React.Component {
|
||||
action: 'remove',
|
||||
clickHandler,
|
||||
label: 'Remove'
|
||||
}, {
|
||||
action: 'move',
|
||||
clickHandler,
|
||||
label: 'Set Download Location'
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -119,9 +124,16 @@ export default class TorrentListContainer extends React.Component {
|
||||
case 'remove':
|
||||
TorrentActions.deleteTorrents(selectedTorrents);
|
||||
break;
|
||||
case 'move':
|
||||
this.handleContextMenuMoveClick(selectedTorrents);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleContextMenuMoveClick(hashes) {
|
||||
UIActions.displayModal('move-torrents');
|
||||
}
|
||||
|
||||
handleDetailsClick(torrent, event) {
|
||||
UIActions.handleDetailsClick({
|
||||
hash: torrent.hash,
|
||||
|
||||
@@ -130,7 +130,7 @@ export default class ContextMenu extends React.Component {
|
||||
}
|
||||
|
||||
ContextMenu.defaultProps = {
|
||||
width: 150
|
||||
width: 200
|
||||
};
|
||||
|
||||
ContextMenu.propTypes = {
|
||||
|
||||
@@ -13,6 +13,8 @@ const ActionTypes = {
|
||||
CLIENT_FETCH_TRANSFER_DATA_SUCCESS: 'CLIENT_FETCH_TRANSFER_DATA_SUCCESS',
|
||||
CLIENT_FETCH_TRANSFER_HISTORY_ERROR: 'CLIENT_FETCH_TRANSFER_HISTORY_ERROR',
|
||||
CLIENT_FETCH_TRANSFER_HISTORY_SUCCESS: 'CLIENT_FETCH_TRANSFER_HISTORY_SUCCESS',
|
||||
CLIENT_MOVE_TORRENTS_SUCCESS: 'CLIENT_MOVE_TORRENTS_SUCCESS',
|
||||
CLIENT_MOVE_TORRENTS_ERROR: 'CLIENT_MOVE_TORRENTS_ERROR',
|
||||
CLIENT_REMOVE_TORRENT_ERROR: 'CLIENT_REMOVE_TORRENT_ERROR',
|
||||
CLIENT_REMOVE_TORRENT_SUCCESS: 'CLIENT_REMOVE_TORRENT_SUCCESS',
|
||||
CLIENT_SET_FILE_PRIORITY_ERROR: 'CLIENT_SET_FILE_PRIORITY_ERROR',
|
||||
|
||||
@@ -3,6 +3,8 @@ const EventTypes = {
|
||||
CLIENT_ADD_TORRENT_SUCCESS: 'CLIENT_ADD_TORRENT_SUCCESS',
|
||||
CLIENT_SET_THROTTLE_ERROR: 'CLIENT_SET_THROTTLE_ERROR',
|
||||
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_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',
|
||||
|
||||
@@ -4,12 +4,13 @@ export default class BaseStore extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.dispatcherID = null;
|
||||
this.on('uncaughtException', this.handleError);
|
||||
this.setMaxListeners(20);
|
||||
}
|
||||
|
||||
handleError(error) {
|
||||
console.error(error);
|
||||
console.trace(error);
|
||||
}
|
||||
|
||||
listen(event, callback) {
|
||||
|
||||
@@ -106,9 +106,9 @@ class TorrentFilterStoreClass extends BaseStore {
|
||||
}
|
||||
}
|
||||
|
||||
const TorrentFilterStore = new TorrentFilterStoreClass();
|
||||
let TorrentFilterStore = new TorrentFilterStoreClass();
|
||||
|
||||
AppDispatcher.register((payload) => {
|
||||
TorrentFilterStore.dispatcherID = AppDispatcher.register((payload) => {
|
||||
const {action, source} = payload;
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
@@ -54,11 +54,23 @@ class TorrentStoreClass extends BaseStore {
|
||||
return this.selectedTorrents;
|
||||
}
|
||||
|
||||
handleAddTorrentError(error) {
|
||||
getSelectedTorrentsDownloadLocations() {
|
||||
return this.selectedTorrents.map((hash) => {
|
||||
return this.torrents[hash].basePath;
|
||||
});
|
||||
}
|
||||
|
||||
getSelectedTorrentsFilename() {
|
||||
return this.selectedTorrents.map((hash) => {
|
||||
return this.torrents[hash].filename;
|
||||
});
|
||||
}
|
||||
|
||||
handleAddTorrentError() {
|
||||
this.emit(EventTypes.CLIENT_ADD_TORRENT_ERROR);
|
||||
}
|
||||
|
||||
handleAddTorrentSuccess(data) {
|
||||
handleAddTorrentSuccess() {
|
||||
this.emit(EventTypes.CLIENT_ADD_TORRENT_SUCCESS);
|
||||
}
|
||||
|
||||
@@ -88,6 +100,14 @@ class TorrentStoreClass extends BaseStore {
|
||||
return this.sortedTorrents;
|
||||
}
|
||||
|
||||
handleMoveTorrentsSuccess(data) {
|
||||
this.emit(EventTypes.CLIENT_MOVE_TORRENTS_SUCCESS);
|
||||
}
|
||||
|
||||
handleMoveTorrentsError(error) {
|
||||
this.emit(EventTypes.CLIENT_MOVE_TORRENTS_REQUEST_ERROR);
|
||||
}
|
||||
|
||||
setTorrents(torrents) {
|
||||
let torrentsSort = TorrentFilterStore.getTorrentsSort();
|
||||
|
||||
@@ -155,9 +175,9 @@ class TorrentStoreClass extends BaseStore {
|
||||
}
|
||||
}
|
||||
|
||||
const TorrentStore = new TorrentStoreClass();
|
||||
let TorrentStore = new TorrentStoreClass();
|
||||
|
||||
AppDispatcher.register((payload) => {
|
||||
TorrentStore.dispatcherID = AppDispatcher.register((payload) => {
|
||||
const {action, source} = payload;
|
||||
|
||||
switch (action.type) {
|
||||
@@ -173,6 +193,12 @@ AppDispatcher.register((payload) => {
|
||||
case ActionTypes.CLIENT_FETCH_TORRENTS_SUCCESS:
|
||||
TorrentStore.setTorrents(action.data.torrents);
|
||||
break;
|
||||
case ActionTypes.CLIENT_MOVE_TORRENTS_SUCCESS:
|
||||
TorrentStore.handleMoveTorrentsSuccess(action.data);
|
||||
break;
|
||||
case ActionTypes.CLIENT_MOVE_TORRENTS_ERROR:
|
||||
TorrentStore.handleMoveTorrentsError(action.error);
|
||||
break;
|
||||
case ActionTypes.CLIENT_FETCH_TORRENTS_ERROR:
|
||||
console.log(action);
|
||||
break;
|
||||
|
||||
@@ -128,9 +128,9 @@ class TransferDataStoreClass extends BaseStore {
|
||||
}
|
||||
}
|
||||
|
||||
const TransferDataStore = new TransferDataStoreClass();
|
||||
let TransferDataStore = new TransferDataStoreClass();
|
||||
|
||||
AppDispatcher.register((payload) => {
|
||||
TransferDataStore.dispatcherID = AppDispatcher.register((payload) => {
|
||||
const {action, source} = payload;
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import BaseStore from './BaseStore';
|
||||
import EventTypes from '../constants/EventTypes';
|
||||
import {selectTorrents} from '../util/selectTorrents';
|
||||
import TorrentActions from '../actions/TorrentActions';
|
||||
import TorrentStore from './TorrentStore';
|
||||
|
||||
class UIStoreClass extends BaseStore {
|
||||
constructor() {
|
||||
@@ -77,9 +78,9 @@ class UIStoreClass extends BaseStore {
|
||||
}
|
||||
}
|
||||
|
||||
const UIStore = new UIStoreClass();
|
||||
let UIStore = new UIStoreClass();
|
||||
|
||||
AppDispatcher.register((payload) => {
|
||||
UIStore.dispatcherID = AppDispatcher.register((payload) => {
|
||||
const {action, source} = payload;
|
||||
|
||||
switch (action.type) {
|
||||
@@ -92,6 +93,9 @@ AppDispatcher.register((payload) => {
|
||||
case ActionTypes.UI_DISPLAY_MODAL:
|
||||
UIStore.setActiveModal(action.data);
|
||||
break;
|
||||
case ActionTypes.CLIENT_MOVE_TORRENTS_SUCCESS:
|
||||
UIStore.setActiveModal(null);
|
||||
break;
|
||||
case ActionTypes.UI_DISPLAY_CONTEXT_MENU:
|
||||
UIStore.setActiveContextMenu(action.data);
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user