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:
skomerko 2025-05-12 18:43:35 +02:00 committed by GitHub
commit de767871f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 173 additions and 204 deletions

View file

@ -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 { #watched_folders_tab {
border-collapse: collapse; border-collapse: collapse;
} }

View file

@ -36,92 +36,81 @@ window.qBittorrent.PiecesBar ??= (() => {
}; };
}; };
const STATUS_DOWNLOADING = 1; class PiecesBar extends HTMLElement {
const STATUS_DOWNLOADED = 2; static #STATUS_DOWNLOADING = 1;
static #STATUS_DOWNLOADED = 2;
// absolute max width of 4096 // absolute max width of 4096
// this is to support all browsers for size of canvas elements // this is to support all browsers for size of canvas elements
// see https://github.com/jhildenbiddle/canvas-size#test-results // see https://github.com/jhildenbiddle/canvas-size#test-results
const MAX_CANVAS_WIDTH = 4096; static #MAX_CANVAS_WIDTH = 4096;
static #piecesBarUniqueId = 0;
let piecesBarUniqueId = 0; #canvasEl;
const PiecesBar = new Class({ #ctx;
initialize: (pieces, parameters) => { #pieces;
const vals = { #styles;
id: `piecesbar_${piecesBarUniqueId++}`, #id = ++PiecesBar.#piecesBarUniqueId;
width: 0, #resizeObserver;
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 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 haveColor: "hsl(210deg 55% 55%)", // @TODO palette vars not supported for this value, apply average
borderSize: 1, borderSize: 1,
borderColor: "var(--color-border-default)" borderColor: "var(--color-border-default)",
...styles
}; };
if (parameters && (typeOf(parameters) === "object")) this.#canvasEl = document.createElement("canvas");
Object.append(vals, parameters); this.#canvasEl.style.height = "100%";
vals.height = Math.max(vals.height, 12); this.#canvasEl.style.imageRendering = "pixelated";
this.#canvasEl.style.width = "100%";
this.#ctx = this.#canvasEl.getContext("2d");
const obj = document.createElement("div"); this.attachShadow({ mode: "open" });
obj.id = vals.id; this.shadowRoot.host.id = `piecesbar_${this.#id}`;
obj.className = "piecesbarWrapper"; this.shadowRoot.host.style.display = "block";
obj.style.border = `${vals.borderSize}px solid ${vals.borderColor}`; this.shadowRoot.host.style.height = `${this.#styles.height}px`;
obj.style.height = `${vals.height}px`; this.shadowRoot.host.style.border = `${this.#styles.borderSize}px solid ${this.#styles.borderColor}`;
obj.vals = vals; this.shadowRoot.append(this.#canvasEl);
obj.vals.pieces = [pieces, []].pick();
const canvas = document.createElement("canvas"); this.#resizeObserver = new ResizeObserver(window.qBittorrent.Misc.createDebounceHandler(100, () => {
canvas.id = `${vals.id}_canvas`; this.#refresh();
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) { connectedCallback() {
if (!this.parentNode) this.#resizeObserver.observe(this);
this.#refresh();
}
clear() {
this.setPieces([]);
}
setPieces(pieces) {
this.#pieces = !Array.isArray(pieces) ? [] : pieces;
this.#refresh();
}
#refresh() {
if (!this.isConnected)
return; return;
const pieces = this.vals.pieces;
// if the number of pieces is small, use that for the width, // if the number of pieces is small, use that for the width,
// and have it stretch horizontally. // and have it stretch horizontally.
// this also limits the ratio below to >= 1 // this also limits the ratio below to >= 1
const width = Math.min(this.offsetWidth, pieces.length, MAX_CANVAS_WIDTH); const width = Math.min(this.offsetWidth, this.#pieces.length, PiecesBar.#MAX_CANVAS_WIDTH);
if ((this.vals.width === width) && !force)
return;
this.vals.width = width;
// change canvas size to fit exactly in the space // change canvas size to fit exactly in the space
this.vals.canvas.width = width - (2 * this.vals.borderSize); this.#canvasEl.width = width - (2 * this.#styles.borderSize);
const canvas = this.vals.canvas; this.#ctx.clearRect(0, 0, this.#canvasEl.width, this.#canvasEl.height);
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
const imageWidth = canvas.width; const imageWidth = this.#canvasEl.width;
if (imageWidth.length === 0) if (imageWidth.length === 0)
return; return;
@ -129,7 +118,7 @@ window.qBittorrent.PiecesBar ??= (() => {
let minStatus = Infinity; let minStatus = Infinity;
let maxStatus = 0; let maxStatus = 0;
for (const status of pieces) { for (const status of this.#pieces) {
if (status > maxStatus) if (status > maxStatus)
maxStatus = status; maxStatus = status;
if (status < minStatus) if (status < minStatus)
@ -141,9 +130,9 @@ window.qBittorrent.PiecesBar ??= (() => {
return; return;
// if all pieces are downloaded, fill entire image at once // if all pieces are downloaded, fill entire image at once
if (minStatus === STATUS_DOWNLOADED) { if (minStatus === PiecesBar.#STATUS_DOWNLOADED) {
ctx.fillStyle = this.vals.haveColor; this.#ctx.fillStyle = this.#styles.haveColor;
ctx.fillRect(0, 0, canvas.width, canvas.height); this.#ctx.fillRect(0, 0, this.#canvasEl.width, this.#canvasEl.height);
return; return;
} }
@ -168,7 +157,7 @@ window.qBittorrent.PiecesBar ??= (() => {
* +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ * +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
*/ */
const ratio = pieces.length / imageWidth; const ratio = this.#pieces.length / imageWidth;
let lastValue = null; let lastValue = null;
let rectangleStart = 0; let rectangleStart = 0;
@ -181,8 +170,8 @@ window.qBittorrent.PiecesBar ??= (() => {
const piecesToInt = Math.ceil(piecesTo); const piecesToInt = Math.ceil(piecesTo);
const statusValues = { const statusValues = {
[STATUS_DOWNLOADING]: 0, [PiecesBar.#STATUS_DOWNLOADING]: 0,
[STATUS_DOWNLOADED]: 0 [PiecesBar.#STATUS_DOWNLOADED]: 0
}; };
// aggregate the status of each piece that contributes to this pixel // aggregate the status of each piece that contributes to this pixel
@ -192,35 +181,35 @@ window.qBittorrent.PiecesBar ??= (() => {
const pieceEnd = Math.min(piece + 1, piecesTo); const pieceEnd = Math.min(piece + 1, piecesTo);
const amount = pieceEnd - pieceStart; const amount = pieceEnd - pieceStart;
const status = pieces[piece]; const status = this.#pieces[piece];
if (status in statusValues) if (status in statusValues)
statusValues[status] += amount; statusValues[status] += amount;
} }
// normalize to interval [0, 1] // normalize to interval [0, 1]
statusValues[STATUS_DOWNLOADING] /= ratio; statusValues[PiecesBar.#STATUS_DOWNLOADING] /= ratio;
statusValues[STATUS_DOWNLOADED] /= ratio; statusValues[PiecesBar.#STATUS_DOWNLOADED] /= ratio;
// floats accumulate small errors, so smooth it out by rounding to hundredths place // floats accumulate small errors, so smooth it out by rounding to hundredths place
// this effectively limits each status to a value 1 in 100 // this effectively limits each status to a value 1 in 100
statusValues[STATUS_DOWNLOADING] = Math.round(statusValues[STATUS_DOWNLOADING] * 100) / 100; statusValues[PiecesBar.#STATUS_DOWNLOADING] = Math.round(statusValues[PiecesBar.#STATUS_DOWNLOADING] * 100) / 100;
statusValues[STATUS_DOWNLOADED] = Math.round(statusValues[STATUS_DOWNLOADED] * 100) / 100; statusValues[PiecesBar.#STATUS_DOWNLOADED] = Math.round(statusValues[PiecesBar.#STATUS_DOWNLOADED] * 100) / 100;
// float precision sometimes _still_ gives > 1 // float precision sometimes _still_ gives > 1
statusValues[STATUS_DOWNLOADING] = Math.min(statusValues[STATUS_DOWNLOADING], 1); statusValues[PiecesBar.#STATUS_DOWNLOADING] = Math.min(statusValues[PiecesBar.#STATUS_DOWNLOADING], 1);
statusValues[STATUS_DOWNLOADED] = Math.min(statusValues[STATUS_DOWNLOADED], 1); statusValues[PiecesBar.#STATUS_DOWNLOADED] = Math.min(statusValues[PiecesBar.#STATUS_DOWNLOADED], 1);
if (!lastValue) if (!lastValue)
lastValue = statusValues; lastValue = statusValues;
// group contiguous colors together and draw as a single rectangle // group contiguous colors together and draw as a single rectangle
if ((lastValue[STATUS_DOWNLOADING] === statusValues[STATUS_DOWNLOADING]) if ((lastValue[PiecesBar.#STATUS_DOWNLOADING] === statusValues[PiecesBar.#STATUS_DOWNLOADING])
&& (lastValue[STATUS_DOWNLOADED] === statusValues[STATUS_DOWNLOADED])) && (lastValue[PiecesBar.#STATUS_DOWNLOADED] === statusValues[PiecesBar.#STATUS_DOWNLOADED]))
continue; continue;
const rectangleWidth = x - rectangleStart; const rectangleWidth = x - rectangleStart;
this._drawStatus(ctx, rectangleStart, rectangleWidth, lastValue); this.#drawStatus(rectangleStart, rectangleWidth, lastValue);
lastValue = statusValues; lastValue = statusValues;
rectangleStart = x; rectangleStart = x;
@ -229,36 +218,29 @@ window.qBittorrent.PiecesBar ??= (() => {
// fill a rect at the end of the canvas // fill a rect at the end of the canvas
if (rectangleStart < imageWidth) { if (rectangleStart < imageWidth) {
const rectangleWidth = imageWidth - rectangleStart; const rectangleWidth = imageWidth - rectangleStart;
this._drawStatus(ctx, rectangleStart, rectangleWidth, lastValue); this.#drawStatus(rectangleStart, rectangleWidth, lastValue);
} }
} }
function drawStatus(ctx, start, width, statusValues) { #drawStatus(start, width, statusValues) {
// mix the colors by using transparency and a composite mode // mix the colors by using transparency and a composite mode
ctx.globalCompositeOperation = "lighten"; this.#ctx.globalCompositeOperation = "lighten";
if (statusValues[STATUS_DOWNLOADING]) { if (statusValues[PiecesBar.#STATUS_DOWNLOADING]) {
ctx.globalAlpha = statusValues[STATUS_DOWNLOADING]; this.#ctx.globalAlpha = statusValues[PiecesBar.#STATUS_DOWNLOADING];
ctx.fillStyle = this.vals.downloadingColor; this.#ctx.fillStyle = this.#styles.downloadingColor;
ctx.fillRect(start, 0, width, ctx.canvas.height); this.#ctx.fillRect(start, 0, width, this.#canvasEl.height);
} }
if (statusValues[STATUS_DOWNLOADED]) { if (statusValues[PiecesBar.#STATUS_DOWNLOADED]) {
ctx.globalAlpha = statusValues[STATUS_DOWNLOADED]; this.#ctx.globalAlpha = statusValues[PiecesBar.#STATUS_DOWNLOADED];
ctx.fillStyle = this.vals.haveColor; this.#ctx.fillStyle = this.#styles.haveColor;
ctx.fillRect(start, 0, width, ctx.canvas.height); this.#ctx.fillRect(start, 0, width, this.#canvasEl.height);
}
} }
} }
const checkForParent = (id) => { customElements.define("pieces-bar", PiecesBar);
const obj = document.getElementById(id);
if (!obj)
return;
if (!obj.parentNode)
return setTimeout(() => { checkForParent(id); }, 100);
obj.refresh();
};
return exports(); return exports();
})(); })();