2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2022 Jesse Smick <jesse.smick@gmail.com>
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * In addition, as a special exception, the copyright holders give permission to
20 * link this program with the OpenSSL project's "OpenSSL" library (or with
21 * modified versions of it that use the same license as the "OpenSSL" library),
22 * and distribute the linked executables. You must obey the GNU General Public
23 * License in all respects for all of the code used other than "OpenSSL". If you
24 * modify file(s), you may extend this exception to your version of the file(s),
25 * but you are not obligated to do so. If you do not wish to do so, delete this
26 * exception statement from your version.
31 if (window.qBittorrent === undefined) {
32 window.qBittorrent = {};
35 window.qBittorrent.PiecesBar = (() => {
36 const exports = () => {
42 const STATUS_DOWNLOADING = 1;
43 const STATUS_DOWNLOADED = 2;
45 // absolute max width of 4096
46 // this is to support all browsers for size of canvas elements
47 // see https://github.com/jhildenbiddle/canvas-size#test-results
48 const MAX_CANVAS_WIDTH = 4096;
50 let piecesBarUniqueId = 0;
51 const PiecesBar = new Class({
52 initialize(pieces, parameters) {
54 'id': 'piecesbar_' + (piecesBarUniqueId++),
57 'downloadingColor': 'hsl(110deg 94% 27%)', // @TODO palette vars not supported for this value, apply average
58 'haveColor': 'hsl(210deg 55% 55%)', // @TODO palette vars not supported for this value, apply average
60 'borderColor': 'var(--color-border-default)'
63 if (parameters && ($type(parameters) === 'object'))
64 $extend(vals, parameters);
65 vals.height = Math.max(vals.height, 12);
67 const obj = new Element('div', {
69 'class': 'piecesbarWrapper',
71 'border': vals.borderSize.toString() + 'px solid ' + vals.borderColor,
72 'height': vals.height.toString() + 'px',
76 obj.vals.pieces = $pick(pieces, []);
78 obj.vals.canvas = new Element('canvas', {
79 'id': vals.id + '_canvas',
80 'class': 'piecesbarCanvas',
81 'width': (vals.width - (2 * vals.borderSize)).toString(),
82 'height': '1' // will stretch vertically to take up the height of the parent
84 obj.appendChild(obj.vals.canvas);
86 obj.setPieces = setPieces;
87 obj.refresh = refresh;
88 obj.clear = setPieces.bind(obj, []);
89 obj._drawStatus = drawStatus;
92 obj.setPieces(vals.pieces);
94 setTimeout(() => { checkForParent(obj.id); }, 1);
100 function setPieces(pieces) {
101 if (!Array.isArray(pieces))
104 this.vals.pieces = pieces;
108 function refresh(force) {
109 if (!this.parentNode)
112 const pieces = this.vals.pieces;
114 // if the number of pieces is small, use that for the width,
115 // and have it stretch horizontally.
116 // this also limits the ratio below to >= 1
117 const width = Math.min(this.offsetWidth, pieces.length, MAX_CANVAS_WIDTH);
118 if ((this.vals.width === width) && !force)
121 this.vals.width = width;
123 // change canvas size to fit exactly in the space
124 this.vals.canvas.width = width - (2 * this.vals.borderSize);
126 const canvas = this.vals.canvas;
127 const ctx = canvas.getContext('2d');
128 ctx.clearRect(0, 0, canvas.width, canvas.height);
130 const imageWidth = canvas.width;
132 if (imageWidth.length === 0)
135 let minStatus = Infinity;
138 for (const status of pieces) {
139 if (status > maxStatus) {
143 if (status < minStatus) {
148 // if no progress then don't do anything
152 // if all pieces are downloaded, fill entire image at once
153 if (minStatus === STATUS_DOWNLOADED) {
154 ctx.fillStyle = this.vals.haveColor;
155 ctx.fillRect(0, 0, canvas.width, canvas.height);
159 /* Linear transformation from pieces to pixels.
161 * The canvas size can vary in width so this figures out what to draw at each pixel.
162 * Inspired by the GUI code here https://github.com/qbittorrent/qBittorrent/blob/25b3f2d1a6b14f0fe098fb79a3d034607e52deae/src/gui/properties/downloadedpiecesbar.cpp#L54
164 * example ratio > 1 (at least 2 pieces per pixel)
165 * +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
166 * pieces | 2 | 1 | 2 | 0 | 2 | 0 | 1 | 0 | 1 | 2 |
167 * +---------+---------+---------+---------+---------+---------+
168 * pixels | | | | | | |
169 * +---------+---------+---------+---------+---------+---------+
171 * example ratio < 1 (at most 2 pieces per pixel)
172 * This case shouldn't happen since the max pixels are limited to the number of pieces
173 * +---------+---------+---------+---------+----------+--------+
174 * pieces | 2 | 1 | 1 | 0 | 2 | 2 |
175 * +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
176 * pixels | | | | | | | | | | |
177 * +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
180 const ratio = pieces.length / imageWidth;
182 let lastValue = null;
183 let rectangleStart = 0;
185 // for each pixel compute its status based on the pieces
186 for (let x = 0; x < imageWidth; ++x) {
187 // find positions in the pieces array
188 const piecesFrom = x * ratio;
189 const piecesTo = (x + 1) * ratio;
190 const piecesToInt = Math.ceil(piecesTo);
192 const statusValues = {
193 [STATUS_DOWNLOADING]: 0,
194 [STATUS_DOWNLOADED]: 0
197 // aggregate the status of each piece that contributes to this pixel
198 for (let p = piecesFrom; p < piecesToInt; ++p) {
199 const piece = Math.floor(p);
200 const pieceStart = Math.max(piecesFrom, piece);
201 const pieceEnd = Math.min(piece + 1, piecesTo);
203 const amount = pieceEnd - pieceStart;
204 const status = pieces[piece];
206 if (status in statusValues)
207 statusValues[status] += amount;
210 // normalize to interval [0, 1]
211 statusValues[STATUS_DOWNLOADING] /= ratio;
212 statusValues[STATUS_DOWNLOADED] /= ratio;
214 // floats accumulate small errors, so smooth it out by rounding to hundredths place
215 // this effectively limits each status to a value 1 in 100
216 statusValues[STATUS_DOWNLOADING] = Math.round(statusValues[STATUS_DOWNLOADING] * 100) / 100;
217 statusValues[STATUS_DOWNLOADED] = Math.round(statusValues[STATUS_DOWNLOADED] * 100) / 100;
219 // float precision sometimes _still_ gives > 1
220 statusValues[STATUS_DOWNLOADING] = Math.min(statusValues[STATUS_DOWNLOADING], 1);
221 statusValues[STATUS_DOWNLOADED] = Math.min(statusValues[STATUS_DOWNLOADED], 1);
224 lastValue = statusValues;
227 // group contiguous colors together and draw as a single rectangle
228 if ((lastValue[STATUS_DOWNLOADING] === statusValues[STATUS_DOWNLOADING])
229 && (lastValue[STATUS_DOWNLOADED] === statusValues[STATUS_DOWNLOADED])) {
233 const rectangleWidth = x - rectangleStart;
234 this._drawStatus(ctx, rectangleStart, rectangleWidth, lastValue);
236 lastValue = statusValues;
240 // fill a rect at the end of the canvas
241 if (rectangleStart < imageWidth) {
242 const rectangleWidth = imageWidth - rectangleStart;
243 this._drawStatus(ctx, rectangleStart, rectangleWidth, lastValue);
247 function drawStatus(ctx, start, width, statusValues) {
248 // mix the colors by using transparency and a composite mode
249 ctx.globalCompositeOperation = 'lighten';
251 if (statusValues[STATUS_DOWNLOADING]) {
252 ctx.globalAlpha = statusValues[STATUS_DOWNLOADING];
253 ctx.fillStyle = this.vals.downloadingColor;
254 ctx.fillRect(start, 0, width, ctx.canvas.height);
257 if (statusValues[STATUS_DOWNLOADED]) {
258 ctx.globalAlpha = statusValues[STATUS_DOWNLOADED];
259 ctx.fillStyle = this.vals.haveColor;
260 ctx.fillRect(start, 0, width, ctx.canvas.height);
264 function checkForParent(id) {
269 return setTimeout(function() { checkForParent(id); }, 1);
277 Object.freeze(window.qBittorrent.PiecesBar);