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 window
.qBittorrent
??= {};
32 window
.qBittorrent
.PiecesBar
??= (() => {
33 const exports
= () => {
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
) => {
51 id
: `piecesbar_${piecesBarUniqueId++}`,
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
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");
66 obj
.className
= "piecesbarWrapper";
67 obj
.style
.border
= `${vals.borderSize}px solid ${vals.borderColor}`;
68 obj
.style
.height
= `${vals.height}px`;
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
;
86 obj
.setPieces(vals
.pieces
);
88 setTimeout(() => { checkForParent(obj
.id
); });
94 function setPieces(pieces
) {
95 if (!Array
.isArray(pieces
))
98 this.vals
.pieces
= pieces
;
102 function refresh(force
) {
103 if (!this.parentNode
)
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
)
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)
129 let minStatus
= Infinity
;
132 for (const status
of pieces
) {
133 if (status
> maxStatus
)
135 if (status
< minStatus
)
139 // if no progress then don't do anything
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
);
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);
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
]))
222 const rectangleWidth
= x
- rectangleStart
;
223 this._drawStatus(ctx
, rectangleStart
, rectangleWidth
, lastValue
);
225 lastValue
= statusValues
;
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
) => {
258 return setTimeout(() => { checkForParent(id
); }, 100);
265 Object
.freeze(window
.qBittorrent
.PiecesBar
);