2017-08-25 03:46:55 +02:00
|
|
|
import React, { Component } from 'react';
|
2017-08-25 02:01:15 +02:00
|
|
|
import { connect } from 'react-redux';
|
2017-08-25 04:32:42 +02:00
|
|
|
import FontAwesome from 'react-fontawesome';
|
2017-08-25 03:46:55 +02:00
|
|
|
import {
|
|
|
|
ButtonGroup,
|
|
|
|
ButtonDropdown,
|
|
|
|
DropdownToggle,
|
|
|
|
DropdownMenu,
|
2017-08-25 04:32:42 +02:00
|
|
|
DropdownItem,
|
|
|
|
Collapse,
|
|
|
|
Card,
|
|
|
|
CardBlock,
|
2017-08-25 03:46:55 +02:00
|
|
|
} from 'reactstrap';
|
2017-09-10 13:34:39 +02:00
|
|
|
import moment from 'moment';
|
|
|
|
import TorrentOptions from './torrent_options';
|
2017-09-10 14:56:21 +02:00
|
|
|
import TorrentProgress from './torrent_progress';
|
2017-08-25 04:32:42 +02:00
|
|
|
import ws_send from '../socket';
|
2017-10-13 06:34:26 +02:00
|
|
|
import store from '../store';
|
2017-09-10 14:29:43 +02:00
|
|
|
import DateDisplay from './date';
|
2017-09-08 10:27:00 +02:00
|
|
|
import selectTorrent, {
|
|
|
|
EXCLUSIVE,
|
|
|
|
UNION,
|
|
|
|
SUBTRACT,
|
|
|
|
NONE
|
|
|
|
} from '../actions/selection';
|
2017-09-10 13:34:39 +02:00
|
|
|
import { updateResource } from '../actions/resources';
|
2017-08-24 14:30:22 +02:00
|
|
|
|
2017-10-13 06:34:26 +02:00
|
|
|
const dlURI = (uri, password, id) => `${uri.replace('ws', 'http')}/dl/${id}?password=${encodeURIComponent(password)}`;
|
|
|
|
|
2017-08-25 04:32:42 +02:00
|
|
|
function File({ file }) {
|
|
|
|
// TODO: show progress bar
|
|
|
|
// TODO: edit priority
|
2017-10-13 06:34:26 +02:00
|
|
|
const { uri, password } = store.getState().socket;
|
2017-08-24 14:30:22 +02:00
|
|
|
return (
|
2017-08-25 04:32:42 +02:00
|
|
|
<tr>
|
2017-10-13 06:34:26 +02:00
|
|
|
<td>
|
|
|
|
<a href={dlURI(uri, password, file.id)} target="_new">
|
|
|
|
{file.path}
|
|
|
|
</a>
|
|
|
|
</td>
|
2017-08-25 04:32:42 +02:00
|
|
|
<td>{file.priority}</td>
|
|
|
|
<td>{file.availability}</td>
|
|
|
|
</tr>
|
2017-08-24 14:30:22 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-08-25 04:32:42 +02:00
|
|
|
// TODO: move to separate component
|
|
|
|
function CollapseToggle({ text, onToggle, open }) {
|
|
|
|
return (
|
|
|
|
<button
|
|
|
|
className="btn btn-sm btn-default"
|
|
|
|
onClick={onToggle}
|
|
|
|
>
|
|
|
|
{text}
|
|
|
|
<FontAwesome
|
2017-09-08 09:35:12 +02:00
|
|
|
name={`caret-${open ? "up" : "down"}`}
|
2017-08-25 04:32:42 +02:00
|
|
|
style={{marginLeft: "0.25rem"}}
|
|
|
|
/>
|
|
|
|
</button>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
class Torrent extends Component {
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.state = {
|
|
|
|
infoShown: false,
|
|
|
|
filesShown: false,
|
|
|
|
trackersShown: false,
|
2017-09-08 09:29:46 +02:00
|
|
|
peersShown: false,
|
|
|
|
removeDropdown: false
|
2017-08-25 04:32:42 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleTorrentState(torrent) {
|
|
|
|
if (torrent.status === "paused") {
|
|
|
|
ws_send("RESUME_TORRENT", { id: torrent.id });
|
|
|
|
} else {
|
|
|
|
ws_send("PAUSE_TORRENT", { id: torrent.id });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
2017-09-10 13:50:37 +02:00
|
|
|
const { dispatch, torrent, files, trackers } = this.props;
|
2017-08-25 04:32:42 +02:00
|
|
|
const status = s => s[0].toUpperCase() + s.slice(1);
|
|
|
|
|
2017-08-25 14:57:02 +02:00
|
|
|
if (!torrent || !files) {
|
|
|
|
return (
|
|
|
|
<p>Loading...</p>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-08-25 04:32:42 +02:00
|
|
|
return (
|
|
|
|
<div>
|
2017-09-08 09:29:46 +02:00
|
|
|
<h4>{torrent.name}</h4>
|
|
|
|
<div className="torrent-controls">
|
|
|
|
<a
|
|
|
|
href="#"
|
|
|
|
style={{margin: "auto 0.5rem"}}
|
|
|
|
title={torrent.status === "paused" ? "Resume" : "Pause"}
|
|
|
|
onClick={e => {
|
|
|
|
e.preventDefault();
|
|
|
|
this.toggleTorrentState(torrent);
|
|
|
|
}}
|
2017-08-25 04:32:42 +02:00
|
|
|
>
|
2017-09-08 09:29:46 +02:00
|
|
|
<FontAwesome name={torrent.status === "paused" ? "play" : "pause"} />
|
|
|
|
</a>
|
|
|
|
<div class="status">{status(torrent.status)}</div>
|
2017-09-10 14:56:21 +02:00
|
|
|
<TorrentProgress torrent={torrent} />
|
2017-09-08 09:29:46 +02:00
|
|
|
<ButtonDropdown
|
|
|
|
isOpen={this.state.removeDropdown}
|
|
|
|
toggle={() => this.setState({ removeDropdown: !this.state.removeDropdown })}
|
|
|
|
>
|
|
|
|
<DropdownToggle color="danger" caret>
|
|
|
|
Remove
|
|
|
|
</DropdownToggle>
|
|
|
|
<DropdownMenu>
|
2017-09-08 10:27:00 +02:00
|
|
|
<DropdownItem
|
|
|
|
onClick={() => {
|
|
|
|
dispatch(selectTorrent([torrent.id], SUBTRACT));
|
|
|
|
ws_send("REMOVE_RESOURCE", { id: torrent.id });
|
|
|
|
}}
|
|
|
|
>Remove</DropdownItem>
|
2017-09-08 09:29:46 +02:00
|
|
|
<DropdownItem>Remove and delete files</DropdownItem>
|
|
|
|
</DropdownMenu>
|
|
|
|
</ButtonDropdown>
|
|
|
|
</div>
|
2017-08-25 04:32:42 +02:00
|
|
|
<ButtonGroup>
|
|
|
|
<CollapseToggle
|
|
|
|
text="Info"
|
|
|
|
onToggle={() => this.setState({ infoShown: !this.state.infoShown })}
|
|
|
|
open={this.state.infoShown}
|
|
|
|
/>
|
|
|
|
<CollapseToggle
|
|
|
|
text="Files"
|
|
|
|
onToggle={() => this.setState({ filesShown: !this.state.filesShown })}
|
|
|
|
open={this.state.filesShown}
|
|
|
|
/>
|
|
|
|
<CollapseToggle
|
|
|
|
text="Trackers"
|
|
|
|
onToggle={() => this.setState({ trackersShown: !this.state.trackersShown })}
|
|
|
|
open={this.state.trackersShown}
|
|
|
|
/>
|
|
|
|
<CollapseToggle
|
|
|
|
text="Peers"
|
|
|
|
onToggle={() => this.setState({ peersShown: !this.state.peersShown })}
|
|
|
|
open={this.state.peersShown}
|
|
|
|
/>
|
|
|
|
</ButtonGroup>
|
|
|
|
<Collapse isOpen={this.state.infoShown}>
|
2017-09-10 13:34:39 +02:00
|
|
|
<Card>
|
|
|
|
<CardBlock>
|
|
|
|
<dl>
|
|
|
|
<dt>Downloading to</dt>
|
|
|
|
<dd>{torrent.path}</dd>
|
|
|
|
<dt>Created</dt>
|
2017-09-10 14:29:43 +02:00
|
|
|
<dd><DateDisplay when={moment(torrent.created)} /></dd>
|
2017-09-10 13:34:39 +02:00
|
|
|
</dl>
|
|
|
|
<TorrentOptions
|
2017-09-10 14:21:08 +02:00
|
|
|
id={torrent.id}
|
2017-09-10 13:34:39 +02:00
|
|
|
priority={torrent.priority}
|
|
|
|
priorityChanged={priority =>
|
|
|
|
dispatch(updateResource({ id: torrent.id, priority }))}
|
|
|
|
downloadThrottle={torrent.throttle_down}
|
|
|
|
downloadThrottleChanged={throttle_down =>
|
|
|
|
dispatch(updateResource({ id: torrent.id, throttle_down }))}
|
|
|
|
uploadThrottle={torrent.throttle_up}
|
|
|
|
uploadThrottleChanged={throttle_up =>
|
|
|
|
dispatch(updateResource({ id: torrent.id, throttle_up }))}
|
|
|
|
/>
|
|
|
|
</CardBlock>
|
|
|
|
</Card>
|
2017-08-25 04:32:42 +02:00
|
|
|
</Collapse>
|
|
|
|
<Collapse isOpen={this.state.filesShown}>
|
|
|
|
<Card style={{marginBottom: "1rem"}}>
|
|
|
|
<CardBlock>
|
|
|
|
<table className="table table-striped" style={{marginBottom: "0"}}>
|
|
|
|
<tbody>
|
|
|
|
{files.map(file => <File file={file} />)}
|
|
|
|
</tbody>
|
|
|
|
</table>
|
|
|
|
</CardBlock>
|
|
|
|
</Card>
|
|
|
|
</Collapse>
|
2017-09-10 13:50:37 +02:00
|
|
|
<Collapse isOpen={this.state.trackersShown}>
|
|
|
|
<Card style={{marginBottom: "1rem"}}>
|
|
|
|
<CardBlock>
|
|
|
|
{trackers.map(tracker =>
|
|
|
|
<div>
|
|
|
|
<h5>{(() => {
|
|
|
|
const a = document.createElement("a");
|
|
|
|
a.href = tracker.url;
|
|
|
|
return a.hostname;
|
|
|
|
})()}
|
|
|
|
{/* TODO: wire up this button: */}
|
|
|
|
<button
|
|
|
|
className="btn btn-sm btn-outline-primary pull-right"
|
|
|
|
>Report</button>
|
|
|
|
</h5>
|
|
|
|
<dl>
|
|
|
|
<dt>URL</dt>
|
|
|
|
<dd>{tracker.url}</dd>
|
|
|
|
<dt>Last report</dt>
|
2017-09-10 14:29:43 +02:00
|
|
|
<dd><DateDisplay when={moment(tracker.last_report)} /></dd>
|
2017-09-10 13:50:37 +02:00
|
|
|
{tracker.error && <dt>Error</dt>}
|
|
|
|
{tracker.error &&
|
|
|
|
<dd className="text-danger">{tracker.error}</dd>}
|
|
|
|
</dl>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</CardBlock>
|
|
|
|
</Card>
|
|
|
|
</Collapse>
|
2017-08-25 04:32:42 +02:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-25 03:46:55 +02:00
|
|
|
class TorrentDetails extends Component {
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.state = {
|
|
|
|
removeDropdown: false
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-08-26 01:34:04 +02:00
|
|
|
componentDidMount() {
|
|
|
|
const { dispatch } = this.props;
|
|
|
|
const { ids } = this.props.match.params;
|
|
|
|
const _ids = ids.split(",");
|
2017-08-26 01:59:53 +02:00
|
|
|
dispatch(selectTorrent(_ids, UNION));
|
2017-08-26 01:34:04 +02:00
|
|
|
}
|
|
|
|
|
2017-08-26 01:37:28 +02:00
|
|
|
componentWillUnmount() {
|
|
|
|
const { dispatch } = this.props;
|
2017-08-26 02:01:31 +02:00
|
|
|
dispatch(selectTorrent([], EXCLUSIVE, false));
|
2017-08-26 01:37:28 +02:00
|
|
|
}
|
|
|
|
|
2017-09-08 10:27:00 +02:00
|
|
|
renderHeader() {
|
|
|
|
const { dispatch, selection } = this.props;
|
2017-08-25 03:46:55 +02:00
|
|
|
return (
|
|
|
|
<div>
|
2017-09-08 09:29:46 +02:00
|
|
|
<h3>
|
|
|
|
{selection.length} torrents
|
|
|
|
<div className="bulk-controls">
|
|
|
|
<a
|
|
|
|
href="#"
|
|
|
|
title="Resume all"
|
|
|
|
onClick={e => {
|
|
|
|
e.preventDefault();
|
|
|
|
selection.forEach(id => ws_send("RESUME_TORRENT", { id }));
|
|
|
|
}}
|
|
|
|
><FontAwesome name="play" /></a>
|
|
|
|
<a
|
|
|
|
href="#"
|
|
|
|
title="Pause all"
|
|
|
|
onClick={e => {
|
|
|
|
e.preventDefault();
|
|
|
|
selection.forEach(id => ws_send("PAUSE_TORRENT", { id }));
|
|
|
|
}}
|
|
|
|
><FontAwesome name="pause" /></a>
|
|
|
|
<ButtonDropdown
|
|
|
|
isOpen={this.state.removeDropdown}
|
|
|
|
toggle={() => this.setState({ removeDropdown: !this.state.removeDropdown })}
|
|
|
|
>
|
|
|
|
<DropdownToggle color="danger" caret>
|
|
|
|
Remove all
|
|
|
|
</DropdownToggle>
|
|
|
|
<DropdownMenu>
|
2017-09-08 10:27:00 +02:00
|
|
|
<DropdownItem
|
|
|
|
onClick={() => {
|
|
|
|
dispatch(selectTorrent(selection, SUBTRACT));
|
|
|
|
selection.forEach(id => ws_send("REMOVE_RESOURCE", { id }));
|
|
|
|
}}
|
|
|
|
>Remove selected torrents</DropdownItem>
|
2017-09-08 09:29:46 +02:00
|
|
|
<DropdownItem>Remove selected torrents and delete files</DropdownItem>
|
|
|
|
</DropdownMenu>
|
|
|
|
</ButtonDropdown>
|
|
|
|
</div>
|
|
|
|
</h3>
|
2017-08-25 03:46:55 +02:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
2017-09-10 13:50:37 +02:00
|
|
|
const {
|
|
|
|
torrents,
|
|
|
|
files,
|
|
|
|
trackers,
|
|
|
|
peers,
|
|
|
|
selection,
|
|
|
|
dispatch
|
|
|
|
} = this.props;
|
2017-08-25 04:32:42 +02:00
|
|
|
const _files = Object.values(files).reduce((s, f) => ({
|
|
|
|
...s, [f.torrent_id]: [...(s[f.torrent_id] || []), f]
|
|
|
|
}), {});
|
2017-09-10 13:50:37 +02:00
|
|
|
const _trackers = Object.values(trackers).reduce((s, t) => ({
|
|
|
|
...s, [t.torrent_id]: [...(s[t.torrent_id] || []), t]
|
|
|
|
}), {});
|
|
|
|
const _peers = Object.values(peers).reduce((s, p) => ({
|
|
|
|
...s, [p.torrent_id]: [...(s[p.torrent_id] || []), p]
|
|
|
|
}), {});
|
2017-08-25 03:46:55 +02:00
|
|
|
return (
|
|
|
|
<div>
|
2017-09-08 10:27:00 +02:00
|
|
|
{selection.length > 1 ? this.renderHeader.bind(this)() : null}
|
2017-08-26 01:47:22 +02:00
|
|
|
{selection.slice(0, 3).map(id => <Torrent
|
2017-09-08 10:27:00 +02:00
|
|
|
dispatch={dispatch}
|
2017-08-25 04:32:42 +02:00
|
|
|
torrent={torrents[id]}
|
|
|
|
files={_files[id] || []}
|
2017-09-10 13:50:37 +02:00
|
|
|
trackers={_trackers[id] || []}
|
|
|
|
peers={_peers[id] || []}
|
2017-08-25 04:32:42 +02:00
|
|
|
/>)}
|
2017-08-26 01:47:22 +02:00
|
|
|
{selection.length > 3 ?
|
|
|
|
<p class="text-center text-muted">
|
|
|
|
<strong>
|
|
|
|
...{selection.length - 3} more hidden...
|
|
|
|
</strong>
|
|
|
|
</p>
|
|
|
|
: null}
|
2017-08-25 03:46:55 +02:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default connect(state => ({
|
|
|
|
router: state.router,
|
2017-08-25 04:32:42 +02:00
|
|
|
torrents: state.torrents,
|
2017-08-25 15:43:54 +02:00
|
|
|
files: state.files,
|
2017-09-10 13:50:37 +02:00
|
|
|
trackers: state.trackers,
|
|
|
|
peers: state.peers,
|
2017-08-26 01:34:04 +02:00
|
|
|
selection: state.selection,
|
|
|
|
server: state.server
|
2017-08-25 03:46:55 +02:00
|
|
|
}))(TorrentDetails);
|