2020-10-25 21:13:48 +00:00
|
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
*
|
2020-10-24 01:04:25 +00:00
|
|
|
|
* 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();
|
|
|
|
|
}
|
|
|
|
|
}
|