transmission/web/src/torrent-row.js

409 lines
11 KiB
JavaScript
Raw Normal View History

/*
* This file Copyright (C) 2020 Mnemosyne LLC
*
* It may be used under the GNU GPL versions 2 or 3
* or any future license endorsed by Mnemosyne LLC.
*/
import { Formatter } from './formatter.js';
import { Torrent } from './torrent.js';
import { setTextContent } from './utils.js';
class TorrentRendererHelper {
static getProgressInfo(controller, t) {
const status = t.getStatus();
const classList = ['torrent-progress-bar'];
let percent = null;
if (status === Torrent._StatusStopped) {
classList.push('paused');
}
if (t.needsMetaData()) {
classList.push('magnet');
percent = Math.round(t.getMetadataPercentComplete() * 100);
} else if (status === Torrent._StatusCheck) {
classList.push('verify');
percent = Math.round(t.getRecheckProgress() * 100);
} else if (t.getLeftUntilDone() > 0) {
classList.push('leech');
percent = Math.round(t.getPercentDone() * 100);
} else {
classList.push('seed');
const seed_ratio_limit = t.seedRatioLimit(controller);
percent =
seed_ratio_limit > 0
? (t.getUploadRatio() * 100) / seed_ratio_limit
: 100;
}
if (t.isQueued()) {
classList.push('queued');
}
return {
classList,
percent,
};
}
static renderProgressbar(controller, t, progressbar) {
const info = TorrentRendererHelper.getProgressInfo(controller, t);
progressbar.className = info.classList.join(' ');
progressbar.style['background-size'] = `${info.percent}% 100%, 100% 100%`;
}
static formatUL(t) {
return `${Formatter.speedBps(t.getUploadSpeed())}`;
}
static formatDL(t) {
return `${Formatter.speedBps(t.getDownloadSpeed())}`;
}
static formatETA(t) {
const eta = t.getETA();
if (eta < 0 || eta >= 999 * 60 * 60) {
return '';
}
return `ETA: ${Formatter.timeInterval(eta)}`;
}
}
///
export class TorrentRendererFull {
static getPeerDetails(t) {
const fmt = Formatter;
const error = t.getErrorMessage();
if (error) {
return error;
}
if (t.isDownloading()) {
const peer_count = t.getPeersConnected();
const webseed_count = t.getWebseedsSendingToUs();
if (webseed_count && peer_count) {
// Downloading from 2 of 3 peer(s) and 2 webseed(s)
return [
'Downloading from',
t.getPeersSendingToUs(),
'of',
fmt.countString('peer', 'peers', peer_count),
'and',
fmt.countString('web seed', 'web seeds', webseed_count),
'',
TorrentRendererHelper.formatDL(t),
TorrentRendererHelper.formatUL(t),
].join(' ');
}
if (webseed_count) {
// Downloading from 2 webseed(s)
return [
'Downloading from',
fmt.countString('web seed', 'web seeds', webseed_count),
'',
TorrentRendererHelper.formatDL(t),
TorrentRendererHelper.formatUL(t),
].join(' ');
}
// Downloading from 2 of 3 peer(s)
return [
'Downloading from',
t.getPeersSendingToUs(),
'of',
fmt.countString('peer', 'peers', peer_count),
'',
TorrentRendererHelper.formatDL(t),
TorrentRendererHelper.formatUL(t),
].join(' ');
}
if (t.isSeeding()) {
return [
'Seeding to',
t.getPeersGettingFromUs(),
'of',
fmt.countString('peer', 'peers', t.getPeersConnected()),
'-',
TorrentRendererHelper.formatUL(t),
].join(' ');
}
if (t.isChecking()) {
return [
'Verifying local data (',
Formatter.percentString(100 * t.getRecheckProgress()),
'% tested)',
].join('');
}
return t.getStateString();
}
static getProgressDetails(controller, t) {
if (t.needsMetaData()) {
let MetaDataStatus = 'retrieving';
if (t.isStopped()) {
MetaDataStatus = 'needs';
}
const percent = 100 * t.getMetadataPercentComplete();
return [
`Magnetized transfer - ${MetaDataStatus} metadata (`,
Formatter.percentString(percent),
'%)',
].join('');
}
const sizeWhenDone = t.getSizeWhenDone();
const totalSize = t.getTotalSize();
const is_done = t.isDone() || t.isSeeding();
const c = [];
if (is_done) {
if (totalSize === sizeWhenDone) {
// seed: '698.05 MiB'
c.push(Formatter.size(totalSize));
} else {
// partial seed: '127.21 MiB of 698.05 MiB (18.2%)'
c.push(
Formatter.size(sizeWhenDone),
' of ',
Formatter.size(t.getTotalSize()),
' (',
t.getPercentDoneStr(),
'%)'
);
}
// append UL stats: ', uploaded 8.59 GiB (Ratio: 12.3)'
c.push(
', uploaded ',
Formatter.size(t.getUploadedEver()),
' (Ratio ',
Formatter.ratioString(t.getUploadRatio()),
')'
);
} else {
// not done yet
c.push(
Formatter.size(sizeWhenDone - t.getLeftUntilDone()),
' of ',
Formatter.size(sizeWhenDone),
' (',
t.getPercentDoneStr(),
'%)'
);
}
// maybe append eta
if (!t.isStopped() && (!is_done || t.seedRatioLimit(controller) > 0)) {
c.push(' - ');
const eta = t.getETA();
if (eta < 0 || eta >= 999 * 60 * 60 /* arbitrary */) {
c.push('remaining time unknown');
} else {
c.push(Formatter.timeInterval(t.getETA()), ' remaining');
}
}
return c.join('');
}
// eslint-disable-next-line class-methods-use-this
render(controller, t, root) {
const is_stopped = t.isStopped();
// name
let e = root._name_container;
setTextContent(e, t.getName());
e.classList.toggle('paused', is_stopped);
// progressbar
TorrentRendererHelper.renderProgressbar(controller, t, root._progressbar);
root._progressbar.classList.add('full');
// peer details
const has_error = t.getError() !== Torrent._ErrNone;
e = root._peer_details_container;
e.classList.toggle('error', has_error);
setTextContent(e, TorrentRendererFull.getPeerDetails(t));
// progress details
e = root._progress_details_container;
setTextContent(e, TorrentRendererFull.getProgressDetails(controller, t));
// pause/resume button
e = root._toggle_running_button;
e.alt = is_stopped ? 'Resume' : 'Pause';
e.dataset.action = is_stopped ? 'resume' : 'pause';
}
// eslint-disable-next-line class-methods-use-this
createRow(torrent) {
const root = document.createElement('li');
root.className = 'torrent';
const icon = document.createElement('div');
icon.classList.add('icon');
icon.dataset.iconMimeType = torrent
.getPrimaryMimeType()
.split('/', 1)
.pop();
icon.dataset.iconMultifile = torrent.getFileCount() > 1 ? 'true' : 'false';
const name = document.createElement('div');
name.className = 'torrent-name';
const peers = document.createElement('div');
peers.className = 'torrent-peer-details';
const progress = document.createElement('div');
progress.classList.add('torrent-progress');
const progressbar = document.createElement('div');
progressbar.classList.add('torrent-progress-bar', 'full');
progress.append(progressbar);
const button = document.createElement('a');
button.className = 'torrent-pauseresume-button';
progress.append(button);
const details = document.createElement('div');
details.className = 'torrent-progress-details';
root.append(icon);
root.append(name);
root.append(peers);
root.append(progress);
root.append(details);
root._icon = icon;
root._name_container = name;
root._peer_details_container = peers;
root._progress_details_container = details;
root._progressbar = progressbar;
root._toggle_running_button = button;
return root;
}
}
///
export class TorrentRendererCompact {
static getPeerDetails(t) {
const errorMessage = t.getErrorMessage();
if (errorMessage) {
return errorMessage;
}
if (t.isDownloading()) {
const have_dn = t.getDownloadSpeed() > 0;
const have_up = t.getUploadSpeed() > 0;
if (!have_up && !have_dn) {
return 'Idle';
}
const s = [`${TorrentRendererHelper.formatETA(t)} `];
if (have_dn) {
s.push(TorrentRendererHelper.formatDL(t));
}
if (have_up) {
s.push(TorrentRendererHelper.formatUL(t));
}
return s.join(' ');
}
if (t.isSeeding()) {
return `Ratio: ${Formatter.ratioString(
t.getUploadRatio()
)}, ${TorrentRendererHelper.formatUL(t)}`;
}
return t.getStateString();
}
// eslint-disable-next-line class-methods-use-this
render(controller, t, root) {
// name
let e = root._name_container;
e.classList.toggle('paused', t.isStopped());
setTextContent(e, t.getName());
// peer details
const has_error = t.getError() !== Torrent._ErrNone;
e = root._details_container;
e.classList.toggle('error', has_error);
setTextContent(e, TorrentRendererCompact.getPeerDetails(t));
// progressbar
TorrentRendererHelper.renderProgressbar(controller, t, root._progressbar);
root._progressbar.classList.add('compact');
}
// eslint-disable-next-line class-methods-use-this
createRow(torrent) {
const progressbar = document.createElement('div');
progressbar.classList.add('torrent-progress-bar', 'compact');
const icon = document.createElement('div');
icon.classList.add('icon');
icon.dataset.iconMimeType = torrent
.getPrimaryMimeType()
.split('/', 1)
.pop();
icon.dataset.iconMultifile = torrent.getFileCount() > 1 ? 'true' : 'false';
const details = document.createElement('div');
details.className = 'torrent-peer-details compact';
const name = document.createElement('div');
name.className = 'torrent-name compact';
const root = document.createElement('li');
root.append(progressbar);
root.append(details);
root.append(name);
root.append(icon);
root.className = 'torrent compact';
root._progressbar = progressbar;
root._details_container = details;
root._name_container = name;
return root;
}
}
///
export class TorrentRow {
constructor(view, controller, torrent) {
this._view = view;
this._torrent = torrent;
this._element = view.createRow(torrent);
const update = () => this.render(controller);
this._torrent.addEventListener('dataChanged', update);
update();
}
getElement() {
return this._element;
}
render(controller) {
const tor = this.getTorrent();
if (tor) {
this._view.render(controller, tor, this.getElement());
}
}
isSelected() {
return this.getElement().classList.contains('selected');
}
getTorrent() {
return this._torrent;
}
getTorrentId() {
return this.getTorrent().getId();
}
}