From 75f37b669545cfeab3fcab3a062dd7fd483a74d9 Mon Sep 17 00:00:00 2001 From: nym21 Date: Sat, 27 Jun 2026 12:05:39 +0200 Subject: [PATCH] website: fix capture button --- website/scripts/utils/chart/capture.js | 79 +++++++++++++++++++++++--- website/scripts/utils/env.js | 3 +- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/website/scripts/utils/chart/capture.js b/website/scripts/utils/chart/capture.js index 4eea2207e..71a8bf87b 100644 --- a/website/scripts/utils/chart/capture.js +++ b/website/scripts/utils/chart/capture.js @@ -3,6 +3,15 @@ import { style } from "../elements.js"; import { colors } from "../colors.js"; export const canCapture = !ios || canShare; +const captureFileName = "bitview-chart.png"; +const openUrls = new Set(); + +window.addEventListener("pagehide", () => { + for (const url of openUrls) { + URL.revokeObjectURL(url); + } + openUrls.clear(); +}); /** * @typedef {Object} LegendItem @@ -279,13 +288,69 @@ function drawLegend(ctx, rows, x, y, metrics) { } /** @param {HTMLCanvasElement} canvas */ -function openCanvas(canvas) { - canvas.toBlob((blob) => { - if (!blob) return; - const url = URL.createObjectURL(blob); - window.open(url, "_blank"); - setTimeout(() => URL.revokeObjectURL(url), 100); - }, "image/png"); +function canvasToBlob(canvas) { + return new Promise((resolve) => canvas.toBlob(resolve, "image/png")); +} + +/** @param {HTMLCanvasElement} canvas */ +function canvasToBlobSync(canvas) { + const data = canvas.toDataURL("image/png").split(",")[1]; + const binary = atob(data); + const bytes = new Uint8Array(binary.length); + + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + + return new Blob([bytes], { type: "image/png" }); +} + +/** @param {Blob} blob */ +async function shareBlob(blob) { + if (!canShare || !("share" in navigator) || !("File" in window)) { + return false; + } + + const file = new File([blob], captureFileName, { type: "image/png" }); + if (!navigator.canShare({ files: [file] })) { + return false; + } + + try { + await navigator.share({ + files: [file], + title: document.title, + }); + return true; + } catch (error) { + return error instanceof DOMException && error.name === "AbortError"; + } +} + +/** @param {Blob} blob */ +function openBlob(blob) { + const url = URL.createObjectURL(blob); + openUrls.add(url); + + const win = window.open(url, "_blank"); + if (win) return; + + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = captureFileName; + anchor.click(); +} + +/** @param {HTMLCanvasElement} canvas */ +async function openCanvas(canvas) { + const blob = ios ? canvasToBlobSync(canvas) : await canvasToBlob(canvas); + if (!blob) return; + + if (ios && (await shareBlob(blob))) { + return; + } + + openBlob(blob); } /** diff --git a/website/scripts/utils/env.js b/website/scripts/utils/env.js index 5ec80eaba..cb12e84e1 100644 --- a/website/scripts/utils/env.js +++ b/website/scripts/utils/env.js @@ -2,5 +2,6 @@ export const localhost = window.location.hostname === "localhost"; const userAgent = navigator.userAgent.toLowerCase(); const iphone = userAgent.includes("iphone"); const ipad = userAgent.includes("ipad"); -export const ios = iphone || ipad; +const touchMac = userAgent.includes("macintosh") && navigator.maxTouchPoints > 1; +export const ios = iphone || ipad || touchMac; export const canShare = "canShare" in navigator;