Add move torrents option to context menu

This commit is contained in:
John Furrow
2016-02-28 19:24:59 -08:00
parent 7e3403238e
commit 635b30afeb
27 changed files with 574 additions and 74 deletions

View File

@@ -92,6 +92,70 @@
}
}
.checkbox {
line-height: 1;
position: relative;
&:hover {
.checkbox {
&__decoy {
border-color: $blue;
}
}
}
input[type="checkbox"] {
left: 0;
opacity: 0;
position: absolute;
top: 50%;
transform: translateY(-50%);
&:checked {
& + .checkbox {
&__decoy {
.icon {
fill: $blue;
}
}
}
}
}
&__decoy {
@extend .textbox;
background: $white;
display: inline-block;
height: $spacing-unit * 3/5;
margin-right: $spacing-unit * 1.5/5;
margin-top: -2px;
padding: 0;
position: relative;
vertical-align: middle;
width: $spacing-unit * 3/5;
.icon {
fill: transparent;
height: 10px;
left: 50%;
position: absolute;
top: 50%;
transition: fill 0.25s;
transform: translate(-43%, -43%);
width: 10px;
}
}
&__label {
color: darken($modal--body--foreground, 10%);
}
}
.form {
&__label {

View File

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

View File

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

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

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

View File

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

View File

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

View 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()} />
);
}
}

View File

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

View File

@@ -130,7 +130,7 @@ export default class ContextMenu extends React.Component {
}
ContextMenu.defaultProps = {
width: 150
width: 200
};
ContextMenu.propTypes = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,6 +29,7 @@
"lodash": "^4.3.0",
"morgan": "~1.5.1",
"multer": "^1.1.0",
"mv": "^2.1.1",
"nedb": "^1.7.2",
"nodemon": "^1.8.1",
"object-assign": "^2.0.0",

File diff suppressed because one or more lines are too long

View File

@@ -476,14 +476,14 @@ th {
100% {
opacity: 0; } }
.textbox, .dropzone__selected-files,
.textbox, .checkbox__decoy, .dropzone__selected-files,
.button {
appearance: none;
-moz-appearance: none;
-webkit-appearance: none;
outline: none; }
.textbox, .dropzone__selected-files {
.textbox, .checkbox__decoy, .dropzone__selected-files {
background: #e9eff5;
border-radius: 4px;
border: 1px solid #d6e2ea;
@@ -494,39 +494,39 @@ th {
-webkit-transition: background 0.25s, border 0.25s, color 0.25s;
transition: background 0.25s, border 0.25s, color 0.25s;
width: 100%; }
.textbox::-webkit-input-placeholder, .dropzone__selected-files::-webkit-input-placeholder {
.textbox::-webkit-input-placeholder, .checkbox__decoy::-webkit-input-placeholder, .dropzone__selected-files::-webkit-input-placeholder {
color: #abbac7;
font-style: italic;
-webkit-transition: color 0.25s;
transition: color 0.25s; }
.textbox::-moz-placeholder, .dropzone__selected-files::-moz-placeholder {
.textbox::-moz-placeholder, .checkbox__decoy::-moz-placeholder, .dropzone__selected-files::-moz-placeholder {
color: #abbac7;
font-style: italic;
-webkit-transition: color 0.25s;
transition: color 0.25s; }
.textbox:-ms-input-placeholder, .dropzone__selected-files:-ms-input-placeholder {
.textbox:-ms-input-placeholder, .checkbox__decoy:-ms-input-placeholder, .dropzone__selected-files:-ms-input-placeholder {
color: #abbac7;
font-style: italic;
-webkit-transition: color 0.25s;
transition: color 0.25s; }
.textbox::placeholder, .dropzone__selected-files::placeholder {
.textbox::placeholder, .checkbox__decoy::placeholder, .dropzone__selected-files::placeholder {
color: #abbac7;
font-style: italic;
-webkit-transition: color 0.25s;
transition: color 0.25s; }
.textbox:focus, .dropzone__selected-files:focus {
.textbox:focus, .checkbox__decoy:focus, .dropzone__selected-files:focus {
background: #fdfefe;
border-color: #c7d6df;
color: #258de5; }
.textbox:focus::-webkit-input-placeholder, .dropzone__selected-files:focus::-webkit-input-placeholder {
.textbox:focus::-webkit-input-placeholder, .checkbox__decoy:focus::-webkit-input-placeholder, .dropzone__selected-files:focus::-webkit-input-placeholder {
color: #abbac7; }
.textbox:focus::-moz-placeholder, .dropzone__selected-files:focus::-moz-placeholder {
.textbox:focus::-moz-placeholder, .checkbox__decoy:focus::-moz-placeholder, .dropzone__selected-files:focus::-moz-placeholder {
color: #abbac7; }
.textbox:focus:-ms-input-placeholder, .dropzone__selected-files:focus:-ms-input-placeholder {
.textbox:focus:-ms-input-placeholder, .checkbox__decoy:focus:-ms-input-placeholder, .dropzone__selected-files:focus:-ms-input-placeholder {
color: #abbac7; }
.textbox:focus::placeholder, .dropzone__selected-files:focus::placeholder {
.textbox:focus::placeholder, .checkbox__decoy:focus::placeholder, .dropzone__selected-files:focus::placeholder {
color: #abbac7; }
.textbox.is-fulfilled, .dropzone__selected-files {
.textbox.is-fulfilled, .is-fulfilled.checkbox__decoy, .dropzone__selected-files {
background: #fdfefe; }
.button {
@@ -569,6 +569,44 @@ th {
background: #1a80d7;
box-shadow: inset 0 0 0 1px #1773c0; }
.checkbox {
line-height: 1;
position: relative; }
.checkbox:hover .checkbox__decoy {
border-color: #258de5; }
.checkbox input[type="checkbox"] {
left: 0;
opacity: 0;
position: absolute;
top: 50%;
-webkit-transform: translateY(-50%);
transform: translateY(-50%); }
.checkbox input[type="checkbox"]:checked + .checkbox__decoy .icon {
fill: #258de5; }
.checkbox__decoy {
background: #fff;
display: inline-block;
height: 15px;
margin-right: 7.5px;
margin-top: -2px;
padding: 0;
position: relative;
vertical-align: middle;
width: 15px; }
.checkbox__decoy .icon {
fill: transparent;
height: 10px;
left: 50%;
position: absolute;
top: 50%;
-webkit-transition: fill 0.25s;
transition: fill 0.25s;
-webkit-transform: translate(-43%, -43%);
transform: translate(-43%, -43%);
width: 10px; }
.checkbox__label {
color: #768a9a; }
.form__label {
color: #abbac7;
display: block;
@@ -1642,7 +1680,7 @@ body {
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
width: 10px; }
.search .textbox, .search .dropzone__selected-files {
.search .textbox, .search .checkbox__decoy, .search .dropzone__selected-files {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
@@ -1660,22 +1698,22 @@ body {
-webkit-transition: background 0.25s, border 0.25s, color 0.25s;
transition: background 0.25s, border 0.25s, color 0.25s;
width: 100%; }
.search .textbox::-webkit-input-placeholder, .search .dropzone__selected-files::-webkit-input-placeholder {
.search .textbox::-webkit-input-placeholder, .search .checkbox__decoy::-webkit-input-placeholder, .search .dropzone__selected-files::-webkit-input-placeholder {
color: rgba(83, 113, 138, 0.4);
font-style: italic;
-webkit-transition: color 0.25s;
transition: color 0.25s; }
.search .textbox::-moz-placeholder, .search .dropzone__selected-files::-moz-placeholder {
.search .textbox::-moz-placeholder, .search .checkbox__decoy::-moz-placeholder, .search .dropzone__selected-files::-moz-placeholder {
color: rgba(83, 113, 138, 0.4);
font-style: italic;
-webkit-transition: color 0.25s;
transition: color 0.25s; }
.search .textbox:-ms-input-placeholder, .search .dropzone__selected-files:-ms-input-placeholder {
.search .textbox:-ms-input-placeholder, .search .checkbox__decoy:-ms-input-placeholder, .search .dropzone__selected-files:-ms-input-placeholder {
color: rgba(83, 113, 138, 0.4);
font-style: italic;
-webkit-transition: color 0.25s;
transition: color 0.25s; }
.search .textbox::placeholder, .search .dropzone__selected-files::placeholder {
.search .textbox::placeholder, .search .checkbox__decoy::placeholder, .search .dropzone__selected-files::placeholder {
color: rgba(83, 113, 138, 0.4);
font-style: italic;
-webkit-transition: color 0.25s;
@@ -1683,19 +1721,19 @@ body {
.search.is-in-use .icon {
fill: #258de5;
opacity: 1; }
.search.is-in-use .textbox, .search.is-in-use .dropzone__selected-files {
.search.is-in-use .textbox, .search.is-in-use .checkbox__decoy, .search.is-in-use .dropzone__selected-files {
background: rgba(37, 141, 229, 0.25);
border-bottom: 1px solid rgba(37, 141, 229, 0.3);
border-top: 1px solid rgba(37, 141, 229, 0.3);
color: #258de5;
padding-right: 45px; }
.search.is-in-use .textbox::-webkit-input-placeholder, .search.is-in-use .dropzone__selected-files::-webkit-input-placeholder {
.search.is-in-use .textbox::-webkit-input-placeholder, .search.is-in-use .checkbox__decoy::-webkit-input-placeholder, .search.is-in-use .dropzone__selected-files::-webkit-input-placeholder {
color: rgba(37, 141, 229, 0.4); }
.search.is-in-use .textbox::-moz-placeholder, .search.is-in-use .dropzone__selected-files::-moz-placeholder {
.search.is-in-use .textbox::-moz-placeholder, .search.is-in-use .checkbox__decoy::-moz-placeholder, .search.is-in-use .dropzone__selected-files::-moz-placeholder {
color: rgba(37, 141, 229, 0.4); }
.search.is-in-use .textbox:-ms-input-placeholder, .search.is-in-use .dropzone__selected-files:-ms-input-placeholder {
.search.is-in-use .textbox:-ms-input-placeholder, .search.is-in-use .checkbox__decoy:-ms-input-placeholder, .search.is-in-use .dropzone__selected-files:-ms-input-placeholder {
color: rgba(37, 141, 229, 0.4); }
.search.is-in-use .textbox::placeholder, .search.is-in-use .dropzone__selected-files::placeholder {
.search.is-in-use .textbox::placeholder, .search.is-in-use .checkbox__decoy::placeholder, .search.is-in-use .dropzone__selected-files::placeholder {
color: rgba(37, 141, 229, 0.4); }
.application__sidebar {

File diff suppressed because one or more lines are too long

View File

@@ -3,7 +3,9 @@
let util = require('util');
let clientUtil = require('../util/clientUtil');
let Q = require('q');
let fs = require('fs');
let mv = require('mv');
let path = require('path');
let rTorrentPropMap = require('../util/rTorrentPropMap');
let scgi = require('../util/scgi');
let stringUtil = require('../../shared/util/stringUtil');
@@ -23,6 +25,10 @@ class ClientRequest {
if (options.postProcess) {
this.postProcessFn = options.postProcess;
}
if (options.name) {
this.name = options.name;
}
}
add(request, options) {
@@ -51,7 +57,7 @@ class ClientRequest {
}
handleError(error) {
console.trace(error);
console.trace(this.name, error);
this.clearRequestQueue();
@@ -83,9 +89,16 @@ class ClientRequest {
}
send() {
// TODO: Remove this.
if (!this) {
console.log('\n\n\n\n\n\n\nthis is null\n\n\n\n\n\n');
}
let handleSuccess = this.handleSuccess.bind(this);
let handleError = this.handleError.bind(this);
scgi.methodCall('system.multicall', [this.requests])
.then(this.handleSuccess.bind(this))
.catch(this.handleError.bind(this));
.then(handleSuccess)
.catch(handleError);
}
// TODO: Separate these and add support for additional clients.
@@ -126,6 +139,14 @@ class ClientRequest {
});
}
checkHashMethodCall(options) {
let hashes = this.getEnsuredArray(options.hashes);
hashes.forEach((hash) => {
this.requests.push(this.getMethodCall('d.check_hash', [hash]));
})
}
createDirectoryMethodCall(options) {
this.requests.push(
this.getMethodCall('execute', ['mkdir', '-p', options.path])
@@ -160,11 +181,24 @@ class ClientRequest {
moveTorrentsMethodCall(options) {
let hashes = this.getEnsuredArray(options.hashes);
let destinationPath = options.destinationPath;
let sourcePath = options.sourcePath;
let filenames = this.getEnsuredArray(options.filenames);
let sourcePaths = this.getEnsuredArray(options.sourcePaths);
this.moveInProgress = true;
sourcePaths.forEach((source, index) => {
let callback = function () {};
let destination = `${destinationPath}${path.sep}${filenames[index]}`;
let isLastRequest = index + 1 === sourcePaths.length;
// let {hashes, destinationPath, sourcePath} = options;
if (isLastRequest) {
callback = this.handleSuccess.bind(this);
}
if (source !== destination) {
mv(source, destination, {mkdirp: true}, callback);
} else if (isLastRequest) {
callback();
}
});
}
removeTorrentsMethodCall(options) {
@@ -175,6 +209,17 @@ class ClientRequest {
});
}
setDownloadPathMethodCall(options) {
let hashes = this.getEnsuredArray(options.hashes);
hashes.forEach((hash) => {
this.requests.push(this.getMethodCall('d.directory.set',
[hash, options.path]));
this.requests.push(this.getMethodCall('d.open', [hash]));
this.requests.push(this.getMethodCall('d.close', [hash]));
});
}
setFilePriorityMethodCall(options) {
let hashes = this.getEnsuredArray(options.hashes);

View File

@@ -7,7 +7,7 @@ let stringUtil = require('../../shared/util/stringUtil');
const MAX_CLEANUP_INTERVAL = 1000 * 60 * 60; // 1 hour
const MAX_NEXT_ERA_UPDATE_INTERVAL = 1000 * 60 * 60 * 12; // 12 hours
const CUMULATIVE_DATA_BUFFER = 1000 * 2;
const CUMULATIVE_DATA_BUFFER = 1000 * 2; // 2 seconds
const REQUIRED_FIELDS = ['interval', 'maxTime', 'name'];
class HistoryEra {
@@ -29,7 +29,6 @@ class HistoryEra {
this.removeOutdatedData(this.db);
let cleanupInterval = this.opts.maxTime;
let nextEraUpdateInterval = this.opts.nextEraUpdateInterval;
if (cleanupInterval === 0 || cleanupInterval > MAX_CLEANUP_INTERVAL) {
@@ -40,11 +39,11 @@ class HistoryEra {
nextEraUpdateInterval = MAX_NEXT_ERA_UPDATE_INTERVAL;
}
this.startAutoCleanup(cleanupInterval, this.db);
if (nextEraUpdateInterval) {
this.startNextEraUpdate(nextEraUpdateInterval, this.db);
}
this.startAutoCleanup(cleanupInterval, this.db);
}
addData(data) {

View File

@@ -45,7 +45,8 @@ const REQUESTED_DATA = [
'comment',
'isPrivate',
'directory',
'filename'
'filename',
'isMultiFile'
];
class Torrent {

View File

@@ -109,20 +109,46 @@ var client = {
request.send();
},
moveFiles: function(data, callback) {
moveTorrents: function(data, callback) {
let destinationPath = data.destination;
let hashes = data.hashes;
let sourcePath = data.source;
let request = new ClientRequest();
let filenames = data.filenames;
let moveFiles = data.moveFiles;
let sourcePaths = data.sources;
let mainRequest = new ClientRequest();
request.add('createDirectory', {path: destinationPath});
request.add('stopTorrents', {hashes});
request.onComplete(function () {
request.add('moveTorrents', {hashes, destinationPath, sourcePath});
request.add('startTorrents', {hashes});
request.onComplete(callback);
})
request.send();
let startTorrents = function() {
let startTorrentsRequest = new ClientRequest();
startTorrentsRequest.add('startTorrents', {hashes});
startTorrentsRequest.onComplete(callback);
startTorrentsRequest.send();
};
let checkHash = function() {
let checkHashRequest = new ClientRequest();
checkHashRequest.add('checkHash', {hashes});
checkHashRequest.onComplete(afterCheckHash);
checkHashRequest.send();
}
let moveTorrents = function () {
let moveTorrentsRequest = new ClientRequest();
moveTorrentsRequest.onComplete(checkHash);
moveTorrentsRequest.add('moveTorrents',
{filenames, sourcePaths, destinationPath});
};
let afterCheckHash = startTorrents;
let afterSetPath = checkHash;
if (moveFiles) {
afterSetPath = moveTorrents;
}
mainRequest.add('stopTorrents', {hashes});
mainRequest.add('setDownloadPath', {hashes, path: destinationPath});
mainRequest.onComplete(afterSetPath);
mainRequest.send();
},
setFilePriority: function (hashes, data, callback) {

View File

@@ -62,6 +62,10 @@ router.patch('/torrents/:hash/file-priority', function(req, res, next) {
client.setFilePriority(req.params.hash, req.body, ajaxUtil.getResponseFn(res));
});
router.post('/torrents/move', function(req, res, next) {
client.moveTorrents(req.body, ajaxUtil.getResponseFn(res));
});
router.get('/torrents/status-count', function(req, res, next) {
client.getTorrentStatusCount(ajaxUtil.getResponseFn(res));
});

View File

@@ -56,7 +56,7 @@ var clientUtil = {
// 'tiedToFile',
// 'trackerNumWant',
// 'trackerSize',
// 'isMultiFile',
'isMultiFile',
// 'isPexActive',
'isPrivate',
@@ -126,7 +126,7 @@ var clientUtil = {
// 'd.tied_to_file=',
// 'd.tracker_numwant=',
// 'd.tracker_size=',
// 'd.is_multi_file=',
'd.is_multi_file=',
// 'd.is_pex_active=',
'd.is_private=',