WebUI: use template literals instead of string concatenation
[qBittorrent.git] / src / webui / www / private / scripts / piecesbar.js
blob49d159cb143fc62265e64103ffc7075fe6880e92
1 /*
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.
29 "use strict";
31 window.qBittorrent ??= {};
32 window.qBittorrent.PiecesBar ??= (() => {
33 const exports = () => {
34 return {
35 PiecesBar: PiecesBar
39 const STATUS_DOWNLOADING = 1;
40 const STATUS_DOWNLOADED = 2;
42 // absolute max width of 4096
43 // this is to support all browsers for size of canvas elements
44 // see https://github.com/jhildenbiddle/canvas-size#test-results
45 const MAX_CANVAS_WIDTH = 4096;
47 let piecesBarUniqueId = 0;
48 const PiecesBar = new Class({
49 initialize: (pieces, parameters) => {
50 const vals = {
51 id: `piecesbar_${piecesBarUniqueId++}`,
52 width: 0,
53 height: 0,
54 downloadingColor: "hsl(110deg 94% 27%)", // @TODO palette vars not supported for this value, apply average
55 haveColor: "hsl(210deg 55% 55%)", // @TODO palette vars not supported for this value, apply average
56 borderSize: 1,
57 borderColor: "var(--color-border-default)"
60 if (parameters && (typeOf(parameters) === "object"))
61 Object.append(vals, parameters);
62 vals.height = Math.max(vals.height, 12);
64 const obj = document.createElement("div");
65 obj.id = vals.id;
66 obj.className = "piecesbarWrapper";
67 obj.style.border = `${vals.borderSize}px solid ${vals.borderColor}`;
68 obj.style.height = `${vals.height}px`;
69 obj.vals = vals;
70 obj.vals.pieces = [pieces, []].pick();
72 const canvas = document.createElement("canvas");
73 canvas.id = `${vals.id}_canvas`;
74 canvas.className = "piecesbarCanvas";
75 canvas.width = `${vals.width - (2 * vals.borderSize)}`;
76 canvas.height = "1"; // will stretch vertically to take up the height of the parent
77 obj.vals.canvas = canvas;
78 obj.appendChild(obj.vals.canvas);
80 obj.setPieces = setPieces;
81 obj.refresh = refresh;
82 obj.clear = setPieces.bind(obj, []);
83 obj._drawStatus = drawStatus;
85 if (vals.width > 0)
86 obj.setPieces(vals.pieces);
87 else
88 setTimeout(() => { checkForParent(obj.id); });
90 return obj;
92 });
94 function setPieces(pieces) {
95 if (!Array.isArray(pieces))
96 pieces = [];
98 this.vals.pieces = pieces;
99 this.refresh(true);
102 function refresh(force) {
103 if (!this.parentNode)
104 return;
106 const pieces = this.vals.pieces;
108 // if the number of pieces is small, use that for the width,
109 // and have it stretch horizontally.
110 // this also limits the ratio below to >= 1
111 const width = Math.min(this.offsetWidth, pieces.length, MAX_CANVAS_WIDTH);
112 if ((this.vals.width === width) && !force)
113 return;
115 this.vals.width = width;
117 // change canvas size to fit exactly in the space
118 this.vals.canvas.width = width - (2 * this.vals.borderSize);
120 const canvas = this.vals.canvas;
121 const ctx = canvas.getContext("2d");
122 ctx.clearRect(0, 0, canvas.width, canvas.height);
124 const imageWidth = canvas.width;
126 if (imageWidth.length === 0)
127 return;
129 let minStatus = Infinity;
130 let maxStatus = 0;
132 for (const status of pieces) {
133 if (status > maxStatus)
134 maxStatus = status;
135 if (status < minStatus)
136 minStatus = status;
139 // if no progress then don't do anything
140 if (maxStatus === 0)
141 return;
143 // if all pieces are downloaded, fill entire image at once
144 if (minStatus === STATUS_DOWNLOADED) {
145 ctx.fillStyle = this.vals.haveColor;
146 ctx.fillRect(0, 0, canvas.width, canvas.height);
147 return;
150 /* Linear transformation from pieces to pixels.
152 * The canvas size can vary in width so this figures out what to draw at each pixel.
153 * Inspired by the GUI code here https://github.com/qbittorrent/qBittorrent/blob/25b3f2d1a6b14f0fe098fb79a3d034607e52deae/src/gui/properties/downloadedpiecesbar.cpp#L54
155 * example ratio > 1 (at least 2 pieces per pixel)
156 * +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
157 * pieces | 2 | 1 | 2 | 0 | 2 | 0 | 1 | 0 | 1 | 2 |
158 * +---------+---------+---------+---------+---------+---------+
159 * pixels | | | | | | |
160 * +---------+---------+---------+---------+---------+---------+
162 * example ratio < 1 (at most 2 pieces per pixel)
163 * This case shouldn't happen since the max pixels are limited to the number of pieces
164 * +---------+---------+---------+---------+----------+--------+
165 * pieces | 2 | 1 | 1 | 0 | 2 | 2 |
166 * +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
167 * pixels | | | | | | | | | | |
168 * +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
171 const ratio = pieces.length / imageWidth;
173 let lastValue = null;
174 let rectangleStart = 0;
176 // for each pixel compute its status based on the pieces
177 for (let x = 0; x < imageWidth; ++x) {
178 // find positions in the pieces array
179 const piecesFrom = x * ratio;
180 const piecesTo = (x + 1) * ratio;
181 const piecesToInt = Math.ceil(piecesTo);
183 const statusValues = {
184 [STATUS_DOWNLOADING]: 0,
185 [STATUS_DOWNLOADED]: 0
188 // aggregate the status of each piece that contributes to this pixel
189 for (let p = piecesFrom; p < piecesToInt; ++p) {
190 const piece = Math.floor(p);
191 const pieceStart = Math.max(piecesFrom, piece);
192 const pieceEnd = Math.min(piece + 1, piecesTo);
194 const amount = pieceEnd - pieceStart;
195 const status = pieces[piece];
197 if (status in statusValues)
198 statusValues[status] += amount;
201 // normalize to interval [0, 1]
202 statusValues[STATUS_DOWNLOADING] /= ratio;
203 statusValues[STATUS_DOWNLOADED] /= ratio;
205 // floats accumulate small errors, so smooth it out by rounding to hundredths place
206 // this effectively limits each status to a value 1 in 100
207 statusValues[STATUS_DOWNLOADING] = Math.round(statusValues[STATUS_DOWNLOADING] * 100) / 100;
208 statusValues[STATUS_DOWNLOADED] = Math.round(statusValues[STATUS_DOWNLOADED] * 100) / 100;
210 // float precision sometimes _still_ gives > 1
211 statusValues[STATUS_DOWNLOADING] = Math.min(statusValues[STATUS_DOWNLOADING], 1);
212 statusValues[STATUS_DOWNLOADED] = Math.min(statusValues[STATUS_DOWNLOADED], 1);
214 if (!lastValue)
215 lastValue = statusValues;
217 // group contiguous colors together and draw as a single rectangle
218 if ((lastValue[STATUS_DOWNLOADING] === statusValues[STATUS_DOWNLOADING])
219 && (lastValue[STATUS_DOWNLOADED] === statusValues[STATUS_DOWNLOADED]))
220 continue;
222 const rectangleWidth = x - rectangleStart;
223 this._drawStatus(ctx, rectangleStart, rectangleWidth, lastValue);
225 lastValue = statusValues;
226 rectangleStart = x;
229 // fill a rect at the end of the canvas
230 if (rectangleStart < imageWidth) {
231 const rectangleWidth = imageWidth - rectangleStart;
232 this._drawStatus(ctx, rectangleStart, rectangleWidth, lastValue);
236 function drawStatus(ctx, start, width, statusValues) {
237 // mix the colors by using transparency and a composite mode
238 ctx.globalCompositeOperation = "lighten";
240 if (statusValues[STATUS_DOWNLOADING]) {
241 ctx.globalAlpha = statusValues[STATUS_DOWNLOADING];
242 ctx.fillStyle = this.vals.downloadingColor;
243 ctx.fillRect(start, 0, width, ctx.canvas.height);
246 if (statusValues[STATUS_DOWNLOADED]) {
247 ctx.globalAlpha = statusValues[STATUS_DOWNLOADED];
248 ctx.fillStyle = this.vals.haveColor;
249 ctx.fillRect(start, 0, width, ctx.canvas.height);
253 const checkForParent = (id) => {
254 const obj = $(id);
255 if (!obj)
256 return;
257 if (!obj.parentNode)
258 return setTimeout(() => { checkForParent(id); }, 100);
260 obj.refresh();
263 return exports();
264 })();
265 Object.freeze(window.qBittorrent.PiecesBar);