mirror of
https://github.com/qbittorrent/qBittorrent
synced 2025-08-20 13:23:34 -07:00
WebUI: Convert 'Pieces Bar' class to a custom element
Mootools is no longer used to create PiecesBar class (+ I cleaned it up a bit and turned into custom element but everything should work as before). PR #22670.
This commit is contained in:
parent
2477e13b3f
commit
de767871f1
2 changed files with 173 additions and 204 deletions
|
@ -755,19 +755,6 @@ td.generalLabel {
|
|||
}
|
||||
}
|
||||
|
||||
.piecesbarWrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.piecesbarCanvas {
|
||||
height: 100%;
|
||||
image-rendering: pixelated;
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#watched_folders_tab {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
|
|
@ -36,229 +36,211 @@ window.qBittorrent.PiecesBar ??= (() => {
|
|||
};
|
||||
};
|
||||
|
||||
const STATUS_DOWNLOADING = 1;
|
||||
const STATUS_DOWNLOADED = 2;
|
||||
class PiecesBar extends HTMLElement {
|
||||
static #STATUS_DOWNLOADING = 1;
|
||||
static #STATUS_DOWNLOADED = 2;
|
||||
// absolute max width of 4096
|
||||
// this is to support all browsers for size of canvas elements
|
||||
// see https://github.com/jhildenbiddle/canvas-size#test-results
|
||||
static #MAX_CANVAS_WIDTH = 4096;
|
||||
static #piecesBarUniqueId = 0;
|
||||
|
||||
// absolute max width of 4096
|
||||
// this is to support all browsers for size of canvas elements
|
||||
// see https://github.com/jhildenbiddle/canvas-size#test-results
|
||||
const MAX_CANVAS_WIDTH = 4096;
|
||||
#canvasEl;
|
||||
#ctx;
|
||||
#pieces;
|
||||
#styles;
|
||||
#id = ++PiecesBar.#piecesBarUniqueId;
|
||||
#resizeObserver;
|
||||
|
||||
let piecesBarUniqueId = 0;
|
||||
const PiecesBar = new Class({
|
||||
initialize: (pieces, parameters) => {
|
||||
const vals = {
|
||||
id: `piecesbar_${piecesBarUniqueId++}`,
|
||||
width: 0,
|
||||
height: 0,
|
||||
constructor(pieces, styles = {}) {
|
||||
super();
|
||||
this.setPieces(pieces);
|
||||
this.#styles = {
|
||||
height: 12,
|
||||
downloadingColor: "hsl(110deg 94% 27%)", // @TODO palette vars not supported for this value, apply average
|
||||
haveColor: "hsl(210deg 55% 55%)", // @TODO palette vars not supported for this value, apply average
|
||||
borderSize: 1,
|
||||
borderColor: "var(--color-border-default)"
|
||||
borderColor: "var(--color-border-default)",
|
||||
...styles
|
||||
};
|
||||
|
||||
if (parameters && (typeOf(parameters) === "object"))
|
||||
Object.append(vals, parameters);
|
||||
vals.height = Math.max(vals.height, 12);
|
||||
this.#canvasEl = document.createElement("canvas");
|
||||
this.#canvasEl.style.height = "100%";
|
||||
this.#canvasEl.style.imageRendering = "pixelated";
|
||||
this.#canvasEl.style.width = "100%";
|
||||
this.#ctx = this.#canvasEl.getContext("2d");
|
||||
|
||||
const obj = document.createElement("div");
|
||||
obj.id = vals.id;
|
||||
obj.className = "piecesbarWrapper";
|
||||
obj.style.border = `${vals.borderSize}px solid ${vals.borderColor}`;
|
||||
obj.style.height = `${vals.height}px`;
|
||||
obj.vals = vals;
|
||||
obj.vals.pieces = [pieces, []].pick();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.shadowRoot.host.id = `piecesbar_${this.#id}`;
|
||||
this.shadowRoot.host.style.display = "block";
|
||||
this.shadowRoot.host.style.height = `${this.#styles.height}px`;
|
||||
this.shadowRoot.host.style.border = `${this.#styles.borderSize}px solid ${this.#styles.borderColor}`;
|
||||
this.shadowRoot.append(this.#canvasEl);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.id = `${vals.id}_canvas`;
|
||||
canvas.className = "piecesbarCanvas";
|
||||
canvas.width = `${vals.width - (2 * vals.borderSize)}`;
|
||||
canvas.height = "1"; // will stretch vertically to take up the height of the parent
|
||||
obj.vals.canvas = canvas;
|
||||
obj.appendChild(obj.vals.canvas);
|
||||
|
||||
obj.setPieces = setPieces;
|
||||
obj.refresh = refresh;
|
||||
obj.clear = setPieces.bind(obj, []);
|
||||
obj._drawStatus = drawStatus;
|
||||
|
||||
if (vals.width > 0)
|
||||
obj.setPieces(vals.pieces);
|
||||
else
|
||||
setTimeout(() => { checkForParent(obj.id); });
|
||||
|
||||
return obj;
|
||||
}
|
||||
});
|
||||
|
||||
function setPieces(pieces) {
|
||||
if (!Array.isArray(pieces))
|
||||
pieces = [];
|
||||
|
||||
this.vals.pieces = pieces;
|
||||
this.refresh(true);
|
||||
}
|
||||
|
||||
function refresh(force) {
|
||||
if (!this.parentNode)
|
||||
return;
|
||||
|
||||
const pieces = this.vals.pieces;
|
||||
|
||||
// if the number of pieces is small, use that for the width,
|
||||
// and have it stretch horizontally.
|
||||
// this also limits the ratio below to >= 1
|
||||
const width = Math.min(this.offsetWidth, pieces.length, MAX_CANVAS_WIDTH);
|
||||
if ((this.vals.width === width) && !force)
|
||||
return;
|
||||
|
||||
this.vals.width = width;
|
||||
|
||||
// change canvas size to fit exactly in the space
|
||||
this.vals.canvas.width = width - (2 * this.vals.borderSize);
|
||||
|
||||
const canvas = this.vals.canvas;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const imageWidth = canvas.width;
|
||||
|
||||
if (imageWidth.length === 0)
|
||||
return;
|
||||
|
||||
let minStatus = Infinity;
|
||||
let maxStatus = 0;
|
||||
|
||||
for (const status of pieces) {
|
||||
if (status > maxStatus)
|
||||
maxStatus = status;
|
||||
if (status < minStatus)
|
||||
minStatus = status;
|
||||
this.#resizeObserver = new ResizeObserver(window.qBittorrent.Misc.createDebounceHandler(100, () => {
|
||||
this.#refresh();
|
||||
}));
|
||||
}
|
||||
|
||||
// if no progress then don't do anything
|
||||
if (maxStatus === 0)
|
||||
return;
|
||||
|
||||
// if all pieces are downloaded, fill entire image at once
|
||||
if (minStatus === STATUS_DOWNLOADED) {
|
||||
ctx.fillStyle = this.vals.haveColor;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
return;
|
||||
connectedCallback() {
|
||||
this.#resizeObserver.observe(this);
|
||||
this.#refresh();
|
||||
}
|
||||
|
||||
/* Linear transformation from pieces to pixels.
|
||||
*
|
||||
* The canvas size can vary in width so this figures out what to draw at each pixel.
|
||||
* Inspired by the GUI code here https://github.com/qbittorrent/qBittorrent/blob/25b3f2d1a6b14f0fe098fb79a3d034607e52deae/src/gui/properties/downloadedpiecesbar.cpp#L54
|
||||
*
|
||||
* example ratio > 1 (at least 2 pieces per pixel)
|
||||
* +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||
* pieces | 2 | 1 | 2 | 0 | 2 | 0 | 1 | 0 | 1 | 2 |
|
||||
* +---------+---------+---------+---------+---------+---------+
|
||||
* pixels | | | | | | |
|
||||
* +---------+---------+---------+---------+---------+---------+
|
||||
*
|
||||
* example ratio < 1 (at most 2 pieces per pixel)
|
||||
* This case shouldn't happen since the max pixels are limited to the number of pieces
|
||||
* +---------+---------+---------+---------+----------+--------+
|
||||
* pieces | 2 | 1 | 1 | 0 | 2 | 2 |
|
||||
* +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||
* pixels | | | | | | | | | | |
|
||||
* +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||
*/
|
||||
clear() {
|
||||
this.setPieces([]);
|
||||
}
|
||||
|
||||
const ratio = pieces.length / imageWidth;
|
||||
setPieces(pieces) {
|
||||
this.#pieces = !Array.isArray(pieces) ? [] : pieces;
|
||||
this.#refresh();
|
||||
}
|
||||
|
||||
let lastValue = null;
|
||||
let rectangleStart = 0;
|
||||
#refresh() {
|
||||
if (!this.isConnected)
|
||||
return;
|
||||
|
||||
// for each pixel compute its status based on the pieces
|
||||
for (let x = 0; x < imageWidth; ++x) {
|
||||
// find positions in the pieces array
|
||||
const piecesFrom = x * ratio;
|
||||
const piecesTo = (x + 1) * ratio;
|
||||
const piecesToInt = Math.ceil(piecesTo);
|
||||
// if the number of pieces is small, use that for the width,
|
||||
// and have it stretch horizontally.
|
||||
// this also limits the ratio below to >= 1
|
||||
const width = Math.min(this.offsetWidth, this.#pieces.length, PiecesBar.#MAX_CANVAS_WIDTH);
|
||||
|
||||
const statusValues = {
|
||||
[STATUS_DOWNLOADING]: 0,
|
||||
[STATUS_DOWNLOADED]: 0
|
||||
};
|
||||
// change canvas size to fit exactly in the space
|
||||
this.#canvasEl.width = width - (2 * this.#styles.borderSize);
|
||||
|
||||
// aggregate the status of each piece that contributes to this pixel
|
||||
for (let p = piecesFrom; p < piecesToInt; ++p) {
|
||||
const piece = Math.floor(p);
|
||||
const pieceStart = Math.max(piecesFrom, piece);
|
||||
const pieceEnd = Math.min(piece + 1, piecesTo);
|
||||
this.#ctx.clearRect(0, 0, this.#canvasEl.width, this.#canvasEl.height);
|
||||
|
||||
const amount = pieceEnd - pieceStart;
|
||||
const status = pieces[piece];
|
||||
const imageWidth = this.#canvasEl.width;
|
||||
|
||||
if (status in statusValues)
|
||||
statusValues[status] += amount;
|
||||
if (imageWidth.length === 0)
|
||||
return;
|
||||
|
||||
let minStatus = Infinity;
|
||||
let maxStatus = 0;
|
||||
|
||||
for (const status of this.#pieces) {
|
||||
if (status > maxStatus)
|
||||
maxStatus = status;
|
||||
if (status < minStatus)
|
||||
minStatus = status;
|
||||
}
|
||||
|
||||
// normalize to interval [0, 1]
|
||||
statusValues[STATUS_DOWNLOADING] /= ratio;
|
||||
statusValues[STATUS_DOWNLOADED] /= ratio;
|
||||
// if no progress then don't do anything
|
||||
if (maxStatus === 0)
|
||||
return;
|
||||
|
||||
// floats accumulate small errors, so smooth it out by rounding to hundredths place
|
||||
// this effectively limits each status to a value 1 in 100
|
||||
statusValues[STATUS_DOWNLOADING] = Math.round(statusValues[STATUS_DOWNLOADING] * 100) / 100;
|
||||
statusValues[STATUS_DOWNLOADED] = Math.round(statusValues[STATUS_DOWNLOADED] * 100) / 100;
|
||||
// if all pieces are downloaded, fill entire image at once
|
||||
if (minStatus === PiecesBar.#STATUS_DOWNLOADED) {
|
||||
this.#ctx.fillStyle = this.#styles.haveColor;
|
||||
this.#ctx.fillRect(0, 0, this.#canvasEl.width, this.#canvasEl.height);
|
||||
return;
|
||||
}
|
||||
|
||||
// float precision sometimes _still_ gives > 1
|
||||
statusValues[STATUS_DOWNLOADING] = Math.min(statusValues[STATUS_DOWNLOADING], 1);
|
||||
statusValues[STATUS_DOWNLOADED] = Math.min(statusValues[STATUS_DOWNLOADED], 1);
|
||||
/* Linear transformation from pieces to pixels.
|
||||
*
|
||||
* The canvas size can vary in width so this figures out what to draw at each pixel.
|
||||
* Inspired by the GUI code here https://github.com/qbittorrent/qBittorrent/blob/25b3f2d1a6b14f0fe098fb79a3d034607e52deae/src/gui/properties/downloadedpiecesbar.cpp#L54
|
||||
*
|
||||
* example ratio > 1 (at least 2 pieces per pixel)
|
||||
* +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||
* pieces | 2 | 1 | 2 | 0 | 2 | 0 | 1 | 0 | 1 | 2 |
|
||||
* +---------+---------+---------+---------+---------+---------+
|
||||
* pixels | | | | | | |
|
||||
* +---------+---------+---------+---------+---------+---------+
|
||||
*
|
||||
* example ratio < 1 (at most 2 pieces per pixel)
|
||||
* This case shouldn't happen since the max pixels are limited to the number of pieces
|
||||
* +---------+---------+---------+---------+----------+--------+
|
||||
* pieces | 2 | 1 | 1 | 0 | 2 | 2 |
|
||||
* +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||
* pixels | | | | | | | | | | |
|
||||
* +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||
*/
|
||||
|
||||
const ratio = this.#pieces.length / imageWidth;
|
||||
|
||||
let lastValue = null;
|
||||
let rectangleStart = 0;
|
||||
|
||||
// for each pixel compute its status based on the pieces
|
||||
for (let x = 0; x < imageWidth; ++x) {
|
||||
// find positions in the pieces array
|
||||
const piecesFrom = x * ratio;
|
||||
const piecesTo = (x + 1) * ratio;
|
||||
const piecesToInt = Math.ceil(piecesTo);
|
||||
|
||||
const statusValues = {
|
||||
[PiecesBar.#STATUS_DOWNLOADING]: 0,
|
||||
[PiecesBar.#STATUS_DOWNLOADED]: 0
|
||||
};
|
||||
|
||||
// aggregate the status of each piece that contributes to this pixel
|
||||
for (let p = piecesFrom; p < piecesToInt; ++p) {
|
||||
const piece = Math.floor(p);
|
||||
const pieceStart = Math.max(piecesFrom, piece);
|
||||
const pieceEnd = Math.min(piece + 1, piecesTo);
|
||||
|
||||
const amount = pieceEnd - pieceStart;
|
||||
const status = this.#pieces[piece];
|
||||
|
||||
if (status in statusValues)
|
||||
statusValues[status] += amount;
|
||||
}
|
||||
|
||||
// normalize to interval [0, 1]
|
||||
statusValues[PiecesBar.#STATUS_DOWNLOADING] /= ratio;
|
||||
statusValues[PiecesBar.#STATUS_DOWNLOADED] /= ratio;
|
||||
|
||||
// floats accumulate small errors, so smooth it out by rounding to hundredths place
|
||||
// this effectively limits each status to a value 1 in 100
|
||||
statusValues[PiecesBar.#STATUS_DOWNLOADING] = Math.round(statusValues[PiecesBar.#STATUS_DOWNLOADING] * 100) / 100;
|
||||
statusValues[PiecesBar.#STATUS_DOWNLOADED] = Math.round(statusValues[PiecesBar.#STATUS_DOWNLOADED] * 100) / 100;
|
||||
|
||||
// float precision sometimes _still_ gives > 1
|
||||
statusValues[PiecesBar.#STATUS_DOWNLOADING] = Math.min(statusValues[PiecesBar.#STATUS_DOWNLOADING], 1);
|
||||
statusValues[PiecesBar.#STATUS_DOWNLOADED] = Math.min(statusValues[PiecesBar.#STATUS_DOWNLOADED], 1);
|
||||
|
||||
if (!lastValue)
|
||||
lastValue = statusValues;
|
||||
|
||||
// group contiguous colors together and draw as a single rectangle
|
||||
if ((lastValue[PiecesBar.#STATUS_DOWNLOADING] === statusValues[PiecesBar.#STATUS_DOWNLOADING])
|
||||
&& (lastValue[PiecesBar.#STATUS_DOWNLOADED] === statusValues[PiecesBar.#STATUS_DOWNLOADED]))
|
||||
continue;
|
||||
|
||||
const rectangleWidth = x - rectangleStart;
|
||||
this.#drawStatus(rectangleStart, rectangleWidth, lastValue);
|
||||
|
||||
if (!lastValue)
|
||||
lastValue = statusValues;
|
||||
rectangleStart = x;
|
||||
}
|
||||
|
||||
// group contiguous colors together and draw as a single rectangle
|
||||
if ((lastValue[STATUS_DOWNLOADING] === statusValues[STATUS_DOWNLOADING])
|
||||
&& (lastValue[STATUS_DOWNLOADED] === statusValues[STATUS_DOWNLOADED]))
|
||||
continue;
|
||||
|
||||
const rectangleWidth = x - rectangleStart;
|
||||
this._drawStatus(ctx, rectangleStart, rectangleWidth, lastValue);
|
||||
|
||||
lastValue = statusValues;
|
||||
rectangleStart = x;
|
||||
// fill a rect at the end of the canvas
|
||||
if (rectangleStart < imageWidth) {
|
||||
const rectangleWidth = imageWidth - rectangleStart;
|
||||
this.#drawStatus(rectangleStart, rectangleWidth, lastValue);
|
||||
}
|
||||
}
|
||||
|
||||
// fill a rect at the end of the canvas
|
||||
if (rectangleStart < imageWidth) {
|
||||
const rectangleWidth = imageWidth - rectangleStart;
|
||||
this._drawStatus(ctx, rectangleStart, rectangleWidth, lastValue);
|
||||
#drawStatus(start, width, statusValues) {
|
||||
// mix the colors by using transparency and a composite mode
|
||||
this.#ctx.globalCompositeOperation = "lighten";
|
||||
|
||||
if (statusValues[PiecesBar.#STATUS_DOWNLOADING]) {
|
||||
this.#ctx.globalAlpha = statusValues[PiecesBar.#STATUS_DOWNLOADING];
|
||||
this.#ctx.fillStyle = this.#styles.downloadingColor;
|
||||
this.#ctx.fillRect(start, 0, width, this.#canvasEl.height);
|
||||
}
|
||||
|
||||
if (statusValues[PiecesBar.#STATUS_DOWNLOADED]) {
|
||||
this.#ctx.globalAlpha = statusValues[PiecesBar.#STATUS_DOWNLOADED];
|
||||
this.#ctx.fillStyle = this.#styles.haveColor;
|
||||
this.#ctx.fillRect(start, 0, width, this.#canvasEl.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawStatus(ctx, start, width, statusValues) {
|
||||
// mix the colors by using transparency and a composite mode
|
||||
ctx.globalCompositeOperation = "lighten";
|
||||
|
||||
if (statusValues[STATUS_DOWNLOADING]) {
|
||||
ctx.globalAlpha = statusValues[STATUS_DOWNLOADING];
|
||||
ctx.fillStyle = this.vals.downloadingColor;
|
||||
ctx.fillRect(start, 0, width, ctx.canvas.height);
|
||||
}
|
||||
|
||||
if (statusValues[STATUS_DOWNLOADED]) {
|
||||
ctx.globalAlpha = statusValues[STATUS_DOWNLOADED];
|
||||
ctx.fillStyle = this.vals.haveColor;
|
||||
ctx.fillRect(start, 0, width, ctx.canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
const checkForParent = (id) => {
|
||||
const obj = document.getElementById(id);
|
||||
if (!obj)
|
||||
return;
|
||||
if (!obj.parentNode)
|
||||
return setTimeout(() => { checkForParent(id); }, 100);
|
||||
|
||||
obj.refresh();
|
||||
};
|
||||
customElements.define("pieces-bar", PiecesBar);
|
||||
|
||||
return exports();
|
||||
})();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue