Add regex toggle for WebUI torrent filtering
[qBittorrent.git] / src / webui / www / private / scripts / piecesbar.js
blobae98dda0083d1ecb1f509a76a431ed0357a42bcc
1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2022  Jesse Smick <jesse.smick@gmail.com>
4  *
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.
9  *
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.
14  *
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.
18  *
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.
27  */
29 'use strict';
31 if (window.qBittorrent === undefined) {
32     window.qBittorrent = {};
35 window.qBittorrent.PiecesBar = (() => {
36     const exports = () => {
37         return {
38             PiecesBar: PiecesBar
39         };
40     };
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) {
53             const vals = {
54                 'id': 'piecesbar_' + (piecesBarUniqueId++),
55                 'width': 0,
56                 'height': 0,
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
59                 'borderSize': 1,
60                 'borderColor': 'var(--color-border-default)'
61             };
63             if (parameters && ($type(parameters) === 'object'))
64                 $extend(vals, parameters);
65             vals.height = Math.max(vals.height, 12);
67             const obj = new Element('div', {
68                 'id': vals.id,
69                 'class': 'piecesbarWrapper',
70                 'styles': {
71                     'border': vals.borderSize.toString() + 'px solid ' + vals.borderColor,
72                     'height': vals.height.toString() + 'px',
73                 }
74             });
75             obj.vals = vals;
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
83             });
84             obj.appendChild(obj.vals.canvas);
86             obj.setPieces = setPieces;
87             obj.refresh = refresh;
88             obj.clear = setPieces.bind(obj, []);
89             obj._drawStatus = drawStatus;
91             if (vals.width > 0)
92                 obj.setPieces(vals.pieces);
93             else
94                 setTimeout(() => { checkForParent(obj.id); }, 1);
96             return obj;
97         }
98     });
100     function setPieces(pieces) {
101         if (!Array.isArray(pieces))
102             pieces = [];
104         this.vals.pieces = pieces;
105         this.refresh(true);
106     }
108     function refresh(force) {
109         if (!this.parentNode)
110             return;
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)
119             return;
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)
133             return;
135         let minStatus = Infinity;
136         let maxStatus = 0;
138         for (const status of pieces) {
139             if (status > maxStatus) {
140                 maxStatus = status;
141             }
143             if (status < minStatus) {
144                 minStatus = status;
145             }
146         }
148         // if no progress then don't do anything
149         if (maxStatus === 0)
150             return;
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);
156             return;
157         }
159         /* Linear transformation from pieces to pixels.
160          *
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
163          *
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          *        +---------+---------+---------+---------+---------+---------+
170          *
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          *        +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
178          */
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
195             };
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;
208             }
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);
223             if (!lastValue) {
224                 lastValue = statusValues;
225             }
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])) {
230                 continue;
231             }
233             const rectangleWidth = x - rectangleStart;
234             this._drawStatus(ctx, rectangleStart, rectangleWidth, lastValue);
236             lastValue = statusValues;
237             rectangleStart = x;
238         }
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);
244         }
245     }
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);
255         }
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);
261         }
262     }
264     function checkForParent(id) {
265         const obj = $(id);
266         if (!obj)
267             return;
268         if (!obj.parentNode)
269             return setTimeout(function() { checkForParent(id); }, 1);
271         obj.refresh();
272     }
274     return exports();
275 })();
277 Object.freeze(window.qBittorrent.PiecesBar);