const React = require('react') const prettyBytes = require('prettier-bytes') const Checkbox = require('material-ui/Checkbox').default const LinearProgress = require('material-ui/LinearProgress').default const TorrentSummary = require('../lib/torrent-summary') const TorrentPlayer = require('../lib/torrent-player') const { dispatcher } = require('../lib/dispatcher') const { calculateEta } = require('../lib/time') module.exports = class TorrentList extends React.Component { render () { const state = this.props.state const contents = [] if (state.downloadPathStatus === 'missing') { contents.push(

Download path missing: {state.saved.prefs.downloadPath}

Check that all drives are connected?

Alternatively, choose a new download path in Preferences

) } const torrentElems = state.saved.torrents.map( (torrentSummary) => this.renderTorrent(torrentSummary) ) contents.push(...torrentElems) contents.push(
Drop a torrent file here or paste a magnet link
) return (
{contents}
) } renderTorrent (torrentSummary) { const state = this.props.state const infoHash = torrentSummary.infoHash const isSelected = infoHash && state.selectedInfoHash === infoHash // Background image: show some nice visuals, like a frame from the movie, if possible const style = {} if (torrentSummary.posterFileName) { const gradient = 'linear-gradient(to bottom, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.4) 100%)' const posterPath = TorrentSummary.getPosterPath(torrentSummary) style.backgroundImage = `${gradient}, url('${posterPath}')` } // Foreground: name of the torrent, basic info like size, play button, // cast buttons if available, and delete const classes = ['torrent'] if (isSelected) classes.push('selected') if (!infoHash) classes.push('disabled') if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey') return (
{this.renderTorrentMetadata(torrentSummary)} {infoHash ? this.renderTorrentButtons(torrentSummary) : null} {isSelected ? this.renderTorrentDetails(torrentSummary) : null}
) } // Show name, download status, % complete renderTorrentMetadata (torrentSummary) { const name = torrentSummary.name || 'Loading torrent...' const elements = [(
{name}
)] // If it's downloading/seeding then show progress info const prog = torrentSummary.progress let progElems if (torrentSummary.error) { progElems = [getErrorMessage(torrentSummary)] } else if (torrentSummary.status !== 'paused' && prog) { progElems = [ renderDownloadCheckbox(), renderTorrentStatus(), renderProgressBar(), renderPercentProgress(), renderTotalProgress(), renderPeers(), renderSpeeds(), renderEta() ] } else { progElems = [ renderDownloadCheckbox(), renderTorrentStatus() ] } elements.push(
{progElems}
) return (
{elements}
) function renderDownloadCheckbox () { const infoHash = torrentSummary.infoHash const isActive = ['downloading', 'seeding'].includes(torrentSummary.status) return ( ) } function renderProgressBar () { const progress = Math.floor(100 * prog.progress) const styles = { wrapper: { display: 'inline-block', marginRight: 8 }, progress: { height: 8, width: 30 } } return (
) } function renderPercentProgress () { const progress = Math.floor(100 * prog.progress) return ({progress}%) } function renderTotalProgress () { const downloaded = prettyBytes(prog.downloaded) const total = prettyBytes(prog.length || 0) if (downloaded === total) { return ({downloaded}) } else { return ({downloaded} / {total}) } } function renderPeers () { if (prog.numPeers === 0) return const count = prog.numPeers === 1 ? 'peer' : 'peers' return ({prog.numPeers} {count}) } function renderSpeeds () { let str = '' if (prog.downloadSpeed > 0) str += ' ↓ ' + prettyBytes(prog.downloadSpeed) + '/s' if (prog.uploadSpeed > 0) str += ' ↑ ' + prettyBytes(prog.uploadSpeed) + '/s' if (str === '') return return ({str}) } function renderEta () { const downloaded = prog.downloaded const total = prog.length || 0 const missing = total - downloaded const downloadSpeed = prog.downloadSpeed if (downloadSpeed === 0 || missing === 0) return const etaStr = calculateEta(missing, downloadSpeed) return ({etaStr}) } function renderTorrentStatus () { let status if (torrentSummary.status === 'paused') { if (!torrentSummary.progress) status = '' else if (torrentSummary.progress.progress === 1) status = 'Not seeding' else status = 'Paused' } else if (torrentSummary.status === 'downloading') { if (!torrentSummary.progress) status = '' else if (!torrentSummary.progress.ready) status = 'Verifying' else status = 'Downloading' } else if (torrentSummary.status === 'seeding') { status = 'Seeding' } else { // torrentSummary.status is 'new' or something unexpected status = '' } return ({status}) } } // Download button toggles between torrenting (DL/seed) and paused // Play button starts streaming the torrent immediately, unpausing if needed renderTorrentButtons (torrentSummary) { const infoHash = torrentSummary.infoHash // Only show the play/dowload buttons for torrents that contain playable media let playButton if (!torrentSummary.error && TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) { playButton = ( play_circle_outline ) } return (
{playButton} close
) } // Show files, per-file download status and play buttons, and so on renderTorrentDetails (torrentSummary) { let filesElement if (torrentSummary.error || !torrentSummary.files) { let message = '' if (torrentSummary.error === 'path-missing') { // Special case error: this torrent's download dir or file is missing message = 'Missing path: ' + TorrentSummary.getFileOrFolder(torrentSummary) } else if (torrentSummary.error) { // General error for this torrent: just show the message message = torrentSummary.error.message || torrentSummary.error } else if (torrentSummary.status === 'paused') { // No file info, no infohash, and we're not trying to download from the DHT message = 'Failed to load torrent info. Click the download button to try again...' } else { // No file info, no infohash, trying to load from the DHT message = 'Downloading torrent info...' } filesElement = (
{message}
) } else { // We do know the files. List them and show download stats for each one const sortByName = this.props.state.saved.prefs.sortByName const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }) let fileRows = torrentSummary.files .filter((file) => !file.path.includes('/.____padding_file/')) .map((file, index) => ({ file, index })) if (sortByName) { fileRows = fileRows.sort((a, b) => collator.compare(a.file.name, b.file.name)) } fileRows = fileRows.map((obj) => this.renderFileRow(torrentSummary, obj.file, obj.index)) filesElement = (
{fileRows}
) } return (
{filesElement}
) } // Show a single torrentSummary file in the details view for a single torrent renderFileRow (torrentSummary, file, index) { // First, find out how much of the file we've downloaded // Are we even torrenting it? const isSelected = torrentSummary.selections && torrentSummary.selections[index] let isDone = false // Are we finished torrenting it? let progress = '' if (torrentSummary.progress && torrentSummary.progress.files && torrentSummary.progress.files[index]) { const fileProg = torrentSummary.progress.files[index] isDone = fileProg.numPiecesPresent === fileProg.numPieces progress = Math.floor(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%' } // Second, for media files where we saved our position, show how far we got let positionElem if (file.currentTime) { // Radial progress bar. 0% = start from 0:00, 270% = 3/4 of the way thru positionElem = this.renderRadialProgressBar(file.currentTime / file.duration) } // Finally, render the file as a table row const isPlayable = TorrentPlayer.isPlayable(file) const infoHash = torrentSummary.infoHash let icon let handleClick if (isPlayable) { icon = 'play_arrow' /* playable? add option to play */ handleClick = dispatcher('playFile', infoHash, index) } else { icon = 'description' /* file icon, opens in OS default app */ handleClick = isDone ? dispatcher('openPath', infoHash, index) : (e) => e.stopPropagation() // noop if file is not ready } // TODO: add a css 'disabled' class to indicate that a file cannot be opened/streamed let rowClass = '' if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream return ( {positionElem} {icon} {file.name} {isSelected ? progress : ''} {prettyBytes(file.length)} {isSelected ? 'close' : 'add'} ) } renderRadialProgressBar (fraction, cssClass) { const rotation = 360 * fraction const transformFill = { transform: 'rotate(' + (rotation / 2) + 'deg)' } const transformFix = { transform: 'rotate(' + rotation + 'deg)' } return (
) } } function stopPropagation (e) { e.stopPropagation() } function getErrorMessage (torrentSummary) { const err = torrentSummary.error if (err === 'path-missing') { return ( Path missing.
Fix and restart the app, or delete the torrent.
) } return 'Error' }