receptor/src/ui/torrent_details.js

478 lines
15 KiB
JavaScript
Raw Permalink 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,
2017-12-29 06:33:03 +01:00
Progress,
Input,
} from 'reactstrap';
2017-09-10 13:34:39 +02:00
import moment from 'moment';
import sha1 from 'js-sha1';
2017-09-10 13:34:39 +02:00
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-12-29 07:19:24 +01:00
import { formatBitrate } from '../bitrate';
2017-08-24 14:30:22 +02:00
const dlURI = (uri, token, id) => {
const dlToken = encodeURIComponent(btoa(String.fromCharCode(...new Uint8Array(sha1.arrayBuffer(id + token)))));
return `${uri.replace('ws', 'http')}/dl/${id}?token=${dlToken}`;
};
2017-10-13 06:34:26 +02:00
2017-12-29 06:33:03 +01:00
function basename(path) {
const parts = path.split("/");
return parts[parts.length - 1];
}
class File extends Component {
shouldComponentUpdate(nextProps, _) {
return nextProps.file !== this.props.file
|| nextProps.server !== this.props.server
|| nextProps.socket !== this.props.socket;
}
render() {
const { dispatch, file, server, socket } = this.props;
const { download_token } = server;
const { uri } = socket;
return (
<div className="file">
<Progress
value={file.progress * 100}
color={file.progress != 1.0 ? "success" : "primary"}
2017-12-29 06:33:03 +01:00
>
{file.progress === 1.0 ?
2018-03-17 16:17:50 +01:00
"done" : `${Math.floor(file.progress * 100)}%`}
</Progress>
<div className="path" title={file.path}>
{file.progress === 1.0 ?
<a href={dlURI(uri, download_token, file.id)} target="_new">
{basename(file.path)}
</a> : basename(file.path)}
</div>
<div>
<Input
type="select"
id="priority"
value={file.priority}
onChange={e => dispatch(updateResource({
id: file.id,
priority: parseInt(e.target.value)
}))}
>
<option value="0">Skip</option>
<option value="1">Lowest</option>
<option value="2">Low</option>
<option value="3">Normal</option>
<option value="4">High</option>
<option value="5">Highest</option>
</Input>
</div>
2017-12-29 06:33:03 +01:00
</div>
);
}
2017-08-24 14:30:22 +02:00
}
class Peer extends Component {
shouldComponentUpdate(nextProps, _) {
return nextProps.peer !== this.props.peer;
}
render() {
const { peer } = this.props;
return (
<div className="peer">
<div style={{flexGrow: 5}}>{peer.ip}</div>
<div style={{flexBasis: "18%"}}>{formatBitrate(peer.rate_up)} up</div>
<div style={{flexBasis: "18%"}}>{formatBitrate(peer.rate_down)} down</div>
<div style={{flexBasis: "18%"}}>has {`${(peer.availability * 100).toFixed(0)}%`}</div>
</div>
);
}
2017-12-29 07:19:24 +01: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
};
}
shouldComponentUpdate(nextProps, nextState) {
return nextProps.torrent !== this.props.torrent
|| nextProps.trackers !== this.props.trackers
|| nextProps.files !== this.props.files
|| nextProps.peers !== this.props.peers
2018-02-27 10:40:08 +01:00
|| nextState.removeDropdown !== this.state.removeDropdown
|| nextState.peersShown !== this.state.peersShown
|| nextState.trackersShown !== this.state.trackersShown
|| nextState.filesShown !== this.state.filesShown
|| nextState.infoShown !== this.state.infoShown;
}
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() {
const {
dispatch,
torrent,
files,
trackers,
peers,
server,
socket,
} = 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>
2018-02-26 19:51:02 +01:00
<div className="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-12-29 19:51:07 +01:00
<DropdownItem
onClick={() => {
dispatch(selectTorrent([torrent.id], SUBTRACT));
ws_send("REMOVE_RESOURCE", { id: torrent.id, artifacts: true });
}}
>Remove and delete files</DropdownItem>
2017-09-08 09:29:46 +02:00
</DropdownMenu>
</ButtonDropdown>
</div>
2017-12-29 06:33:03 +01:00
<ButtonGroup className="toggles">
2017-08-25 04:32:42 +02:00
<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-12-29 06:33:03 +01:00
<Card style={{marginBottom: "1rem"}}>
2017-09-10 13:34:39 +02:00
<CardBlock>
2018-03-14 20:44:38 +01:00
<button
className="btn btn-sm btn-outline-primary pull-right"
onClick={() => {
ws_send("VALIDATE_RESOURCES", { ids: [torrent.id] })
}}
>Initiate hash check</button>
2017-09-10 13:34:39 +02:00
<dl>
<dt>Type</dt>
<dd>{torrent.private ? "Private" : "Public"}</dd>
2017-09-10 13:34:39 +02:00
<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>
<dt>Comment</dt>
<dd>{torrent.comment || "None"}</dd>
<dt>Creator</dt>
<dd>{torrent.creator || "None"}</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}
strategy={torrent.strategy}
2017-09-10 13:34:39 +02:00
priorityChanged={priority =>
dispatch(updateResource({ id: torrent.id, priority }))}
strategyChanged={strategy =>
dispatch(updateResource({ id: torrent.id, strategy }))}
2017-09-10 13:34:39 +02:00
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"}}>
2017-12-29 06:33:03 +01:00
<CardBlock style={{padding: "0"}}>
2017-12-29 07:19:24 +01:00
<div className="files flex-table" style={{marginBottom: "0"}}>
{this.state.filesShown
? files.slice()
2019-06-15 07:56:20 +02:00
.sort((a, b) => a.path.localeCompare(b.path, undefined, {numeric: true}))
.map(file => <File
dispatch={dispatch}
file={file}
server={server}
socket={socket}
/>)
: null}
2017-12-29 06:33:03 +01:00
</div>
2017-08-25 04:32:42 +02:00
</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;
})()}
<button
className="btn btn-sm btn-outline-primary pull-right"
2017-12-29 21:17:13 +01:00
onClick={() => {
ws_send("UPDATE_TRACKER", { id: tracker.id })
}}
2017-09-10 13:50:37 +02:00
>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-12-29 07:19:24 +01:00
<Collapse isOpen={this.state.peersShown}>
<Card style={{marginBottom: "1rem"}}>
<CardBlock style={{padding: "0"}}>
<div className="peers flex-table" style={{marginBottom: "0"}}>
{this.state.peersShown ? peers.map(peer => <Peer peer={peer} />) : null}
2017-12-29 07:19:24 +01:00
</div>
{peers.length === 0 &&
<div style={{padding: "0.5rem 0 0 0.5rem"}}>
2017-12-29 08:08:04 +01:00
<p>No connected peers.</p>
2017-12-29 07:19:24 +01:00
</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>
<DropdownItem
onClick={() => {
dispatch(selectTorrent(selection, SUBTRACT));
selection.forEach(id => ws_send("REMOVE_RESOURCE", {
id,
artifacts: true
}));
}}
>Remove selected torrents and delete files</DropdownItem>
2017-09-08 09:29:46 +02:00
</DropdownMenu>
</ButtonDropdown>
</div>
</h3>
</div>
);
}
render() {
2017-09-10 13:50:37 +02:00
const {
torrents,
files,
trackers,
peers,
selection,
server,
socket,
2017-09-10 13:50:37 +02:00
dispatch
} = this.props;
const index_by_tid = (res) => {
let _indexed = {};
Object.values(res).map(r => {
if (!(r.torrent_id in _indexed)) {
_indexed[r.torrent_id] = [];
}
_indexed[r.torrent_id].push(r);
});
return _indexed;
};
const _files = index_by_tid(files);
const _trackers = index_by_tid(trackers);
const _peers = index_by_tid(peers);
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] || []}
server={server}
socket={socket}
2017-12-29 20:57:19 +01:00
key={id}
2017-08-25 04:32:42 +02:00
/>)}
2017-08-26 01:47:22 +02:00
{selection.length > 3 ?
2018-02-26 19:51:02 +01:00
<p className="text-center text-muted">
2017-08-26 01:47:22 +02:00
<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,
socket: state.socket,
}))(TorrentDetails);