2019-09-02 14:52:14 +02:00

326 lines
8.5 KiB
TypeScript

/* eslint-disable no-magic-numbers */
"use strict";
// License: MIT
import { _ } from "../../lib/i18n";
import { formatSpeed } from "../../lib/formatters";
import { DownloadState } from "./state";
import { Rect } from "../../uikit/lib/rect";
// eslint-disable-next-line no-unused-vars
import { DownloadItem } from "./table";
function createInnerShadowGradient(
ctx: CanvasRenderingContext2D, w: number, colors: string[]) {
const g = ctx.createLinearGradient(0, 0, 0, w);
g.addColorStop(0, colors[0]);
g.addColorStop(3.0 / w, colors[1]);
g.addColorStop(4.0 / w, colors[2]);
g.addColorStop(1, colors[3]);
return g;
}
function makeRoundedRectPath(
ctx: CanvasRenderingContext2D,
x: number, y: number,
width: number, height: number,
radius: number) {
ctx.beginPath();
ctx.moveTo(x, y + radius);
ctx.lineTo(x, y + height - radius);
ctx.quadraticCurveTo(x, y + height, x + radius, y + height);
ctx.lineTo(x + width - radius, y + height);
ctx.quadraticCurveTo(x + width, y + height, x + width, y + height - radius);
ctx.lineTo(x + width, y + radius);
ctx.quadraticCurveTo(x + width, y, x + width - radius, y);
ctx.lineTo(x + radius, y);
ctx.quadraticCurveTo(x, y, x, y + radius);
}
function createVerticalGradient(
ctx: CanvasRenderingContext2D, height: number, c1: string, c2: string) {
const g = ctx.createLinearGradient(0, 0, 0, height);
g.addColorStop(0, c1);
g.addColorStop(1, c2);
return g;
}
function drawSpeedPass(
ctx: CanvasRenderingContext2D, h: number, step: number,
pass: any, speeds: number[]) {
let y = h + pass.y;
let x = pass.x + 0.5;
ctx.beginPath();
ctx.moveTo(x, y);
y -= speeds[0];
if (pass.f) {
ctx.lineTo(x, y);
}
else {
ctx.moveTo(x, y);
}
let slope = (speeds[1] - speeds[0]);
x += step * 0.7;
y -= slope * 0.7;
ctx.lineTo(x, y);
for (let j = 1, e = speeds.length - 1; j < e; ++j) {
y -= slope * 0.3;
slope = (speeds[j + 1] - speeds[j]);
y -= slope * 0.3;
ctx.quadraticCurveTo(step * j, h + pass.y - speeds[j], (x + step * 0.6), y);
x += step;
y -= slope * 0.4;
ctx.lineTo(x, y);
}
x += step * 0.3;
y -= slope * 0.3;
ctx.lineTo(x, y);
if (pass.f) {
ctx.lineTo(x, h);
ctx.fillStyle = createVerticalGradient(ctx, h - 7, pass.f[0], pass.f[1]);
ctx.fill();
}
if (pass.s) {
ctx.lineWidth = pass.sw || 1;
ctx.strokeStyle = pass.s;
ctx.stroke();
}
}
const speedPasses = Object.freeze([
{ x: 4, y: 0, f: ["#EADF91", "#F4EFB1"] },
{ x: 2, y: 0, f: ["#DFD58A", "#D3CB8B"] },
{ x: 1, y: 0, f: ["#D0BA70", "#DFCF6F"] },
{ x: 0, y: 0, f: ["#FF8B00", "#FFDF38"], s: "#F98F00" }
]);
const avgPass = Object.freeze({x: 0, y: 0, s: "rgba(0,0,200,0.3", sw: 2});
const ELEMS = [
"icon",
"infos", "name", "from", "size", "date", "eta", "etalabel",
"speedbox", "speedbar", "current", "average",
"progressbar", "progress"
];
export class Tooltip {
private readonly item: DownloadItem;
private readonly elem: HTMLElement;
private readonly speedbar: HTMLCanvasElement;
private readonly icon: HTMLElement;
private readonly name: HTMLElement;
private readonly from: HTMLElement;
private readonly size: HTMLElement;
private readonly date: HTMLElement;
private readonly eta: HTMLElement;
private readonly etalabel: HTMLElement;
private readonly speedbox: HTMLElement;
private readonly progressbar: HTMLElement;
private readonly progress: HTMLElement;
private readonly current: HTMLElement;
private readonly average: HTMLElement;
private lastPos: any;
constructor(item: DownloadItem, pos: number) {
this.update = this.update.bind(this);
this.item = item;
this.item.on("largeIcon", this.update);
const tmpl = (
document.querySelector<HTMLTemplateElement>("#tooltip-template"));
if (!tmpl) {
throw new Error("template failed");
}
const el = tmpl.content.firstElementChild;
if (!el) {
throw new Error("invalid template");
}
this.elem = el.cloneNode(true) as HTMLElement;
this.adjust(pos);
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self: any = this;
ELEMS.forEach(e => {
self[e] = this.elem.querySelector(`#tooltip-${e}`);
});
document.body.appendChild(this.elem);
this.item.on("stats", this.update);
this.item.on("update", this.update);
this.speedbar.width = this.speedbar.clientWidth;
this.speedbar.height = this.speedbar.clientHeight;
this.update();
this.adjust(pos);
}
update() {
const {item} = this;
if (!item.isFiltered) {
this.dismiss();
return;
}
const icon = item.largeIcon;
this.icon.className = icon;
this.name.textContent = item.destFull;
this.from.textContent = item.usable;
this.size.textContent = item.fmtSize;
this.date.textContent = new Date(item.startDate).toLocaleString();
this.eta.textContent = item.fmtETA;
const running = item.state === DownloadState.RUNNING;
const hidden = this.speedbox.classList.contains("hidden");
if (!running && !hidden) {
this.eta.style.fontWeight = "bold";
this.etalabel.classList.add("hidden");
this.speedbox.classList.add("hidden");
this.progressbar.classList.add("hidden");
this.adjust(null);
}
if (!running) {
return;
}
if (hidden) {
this.eta.style.fontWeight = "auto";
this.etalabel.classList.remove("hidden");
this.speedbox.classList.remove("hidden");
this.progressbar.classList.remove("hidden");
this.adjust(null);
}
this.progress.style.width = `${item.percent * 100}%`;
this.current.textContent = formatSpeed(item.stats.current);
this.average.textContent = formatSpeed(item.stats.avg);
this.drawSpeeds();
}
drawSpeeds() {
const {stats} = this.item;
const {speedbar: canvas} = this;
let w = canvas.width;
let h = canvas.height;
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Cannot acquire 2d context");
}
--w; --h;
const boxFillStyle = createInnerShadowGradient(
ctx, h, ["#B1A45A", "#F1DF7A", "#FEEC84", "#FFFDC4"]);
const boxStrokeStyle = createInnerShadowGradient(
ctx, 8, ["#816A1D", "#E7BE34", "#F8CC38", "#D8B231"]);
ctx.clearRect(0, 0, w, h);
ctx.save();
ctx.translate(0.5, 0.5);
ctx.lineWidth = 1;
ctx.strokeStyle = boxStrokeStyle;
ctx.fillStyle = boxFillStyle;
// draw container chunks back
ctx.fillStyle = boxFillStyle;
makeRoundedRectPath(ctx, 0, 0, w, h, 5);
ctx.fill();
let speeds = Array.from(stats.validValues);
let avgs = Array.from(stats.avgs.validValues);
if (speeds.length > 1) {
let [maxH] = speeds; let [minH] = speeds;
speeds.forEach(s => {
maxH = Math.max(maxH, s);
minH = Math.min(minH, s);
});
if (minH === maxH) {
speeds = speeds.map(() => 12);
}
else {
const r = (maxH - minH);
speeds = speeds.map(function(speed) {
return 3 + Math.round((h - 6) * (speed - minH) / r);
});
avgs = avgs.map(function(speed) {
return 3 + Math.round((h - 6) * (speed - minH) / r);
});
}
ctx.save();
ctx.clip();
const step = w / (speeds.length - 1);
for (const pass of speedPasses) {
drawSpeedPass(ctx, h, step, pass, speeds);
}
drawSpeedPass(ctx, h, step, avgPass, avgs);
ctx.restore();
}
makeRoundedRectPath(ctx, 0, 0, w, h, 3);
ctx.stroke();
ctx.restore();
}
adjust(pos: any) {
if (pos) {
this.lastPos = pos;
}
else {
pos = this.lastPos;
}
const {clientWidth, clientHeight} = this.elem;
if (!clientWidth) {
this.elem.style.left = `${pos.x + 10}px`;
this.elem.style.top = `${pos.y + 10}px`;
return;
}
const w = Math.max(
document.documentElement.clientWidth, window.innerWidth || 0);
const h = Math.max(
document.documentElement.clientHeight, window.innerHeight || 0);
const r = new Rect(pos.x + 10, pos.y + 10, 0, 0, clientWidth, clientHeight);
if (r.right > w) {
r.offset(-clientWidth - 20, 0);
}
if (r.left < 0) {
r.offset(-r.left, 0);
}
if (r.bottom > h) {
r.offset(0, -clientHeight - 20);
}
this.elem.style.left = `${r.left}px`;
this.elem.style.top = `${r.top}px`;
}
dismiss() {
if (this.elem.parentElement) {
this.elem.parentElement.removeChild(this.elem);
}
this.item.off("stats", this.update);
this.item.off("update", this.update);
this.item.off("largeIcon", this.update);
}
}