/* * 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(); } }