receptor/src/ui/torrent_details.js

334 lines
9.8 KiB
JavaScript
Raw Normal View History

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';
import {
ButtonGroup,
ButtonDropdown,
DropdownToggle,
DropdownMenu,
2017-08-25 04:32:42 +02:00
DropdownItem,
Collapse,
Card,
CardBlock,
} from 'reactstrap';
2017-09-10 13:34:39 +02:00
import moment from 'moment';
import TorrentOptions from './torrent_options';
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>
<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>
);
}
}
class TorrentDetails extends Component {
constructor() {
super();
this.state = {
removeDropdown: false
};
}
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));
}
componentWillUnmount() {
const { dispatch } = this.props;
dispatch(selectTorrent([], EXCLUSIVE, false));
}
2017-09-08 10:27:00 +02:00
renderHeader() {
const { dispatch, selection } = this.props;
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>
</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]
}), {});
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}
</div>
);
}
}
export default connect(state => ({
router: state.router,
2017-08-25 04:32:42 +02:00
torrents: state.torrents,
files: state.files,
2017-09-10 13:50:37 +02:00
trackers: state.trackers,
peers: state.peers,
selection: state.selection,
server: state.server
}))(TorrentDetails);