From 5da47c176bc854e5bba15769e124edc54f5c81e9 Mon Sep 17 00:00:00 2001 From: Sergey Antropoff Date: Thu, 23 Jul 2026 10:28:14 +0300 Subject: [PATCH] Force download for .url/.txt/.png/.qr.txt chips Browser often opens these files instead of downloading; use blob downloads with embedded text fallbacks and canvas for PNG on file://. Co-authored-by: Cursor --- .../templates/export/global-index.html.j2 | 73 ++++++++++++++++++- .../hysteria2/templates/export/index.html.j2 | 73 ++++++++++++++++++- 2 files changed, 138 insertions(+), 8 deletions(-) diff --git a/roles/hysteria2/templates/export/global-index.html.j2 b/roles/hysteria2/templates/export/global-index.html.j2 index 29ef5da..6f8cdc7 100644 --- a/roles/hysteria2/templates/export/global-index.html.j2 +++ b/roles/hysteria2/templates/export/global-index.html.j2 @@ -734,12 +734,12 @@ {% endif %} {% endfor %} @@ -936,6 +936,71 @@ if (e.key === 'Escape' && box.classList.contains('open')) closeLightbox(); }); })(); + + (function () { + function triggerFileDownload(blob, filename) { + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + a.rel = 'noopener'; + document.body.appendChild(a); + a.click(); + a.remove(); + setTimeout(function () { URL.revokeObjectURL(url); }, 1500); + } + + function pngFromCard(link, filename) { + var card = link.closest('.user-card'); + var qrImg = card && card.querySelector('img[data-lightbox-src], .qr-block img'); + if (!qrImg) return false; + try { + if (!qrImg.complete || !qrImg.naturalWidth) return false; + var canvas = document.createElement('canvas'); + canvas.width = qrImg.naturalWidth; + canvas.height = qrImg.naturalHeight; + canvas.getContext('2d').drawImage(qrImg, 0, 0); + canvas.toBlob(function (blob) { + if (blob) triggerFileDownload(blob, filename); + }, 'image/png'); + return true; + } catch (err) { + return false; + } + } + + document.querySelectorAll('a.file-download').forEach(function (link) { + link.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + var filename = link.getAttribute('data-filename') || link.getAttribute('download') || 'download'; + var href = link.getAttribute('href'); + var fallbackText = link.getAttribute('data-text'); + var isPng = /\.png$/i.test(filename); + + function fallback() { + if (fallbackText !== null) { + triggerFileDownload(new Blob([fallbackText], { type: 'text/plain;charset=utf-8' }), filename); + return; + } + if (isPng) pngFromCard(link, filename); + } + + if (!href) { + fallback(); + return; + } + + fetch(href) + .then(function (res) { + if (!res.ok) throw new Error('fetch failed'); + return res.blob(); + }) + .then(function (blob) { triggerFileDownload(blob, filename); }) + .catch(fallback); + }); + }); + })(); diff --git a/roles/hysteria2/templates/export/index.html.j2 b/roles/hysteria2/templates/export/index.html.j2 index 0434ee6..70713c7 100644 --- a/roles/hysteria2/templates/export/index.html.j2 +++ b/roles/hysteria2/templates/export/index.html.j2 @@ -440,12 +440,12 @@ {% endif %} {% endfor %} @@ -613,6 +613,71 @@ if (e.key === 'Escape' && box.classList.contains('open')) closeLightbox(); }); })(); + + (function () { + function triggerFileDownload(blob, filename) { + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + a.rel = 'noopener'; + document.body.appendChild(a); + a.click(); + a.remove(); + setTimeout(function () { URL.revokeObjectURL(url); }, 1500); + } + + function pngFromCard(link, filename) { + var card = link.closest('.user-card'); + var qrImg = card && card.querySelector('img[data-lightbox-src], .qr-block img'); + if (!qrImg) return false; + try { + if (!qrImg.complete || !qrImg.naturalWidth) return false; + var canvas = document.createElement('canvas'); + canvas.width = qrImg.naturalWidth; + canvas.height = qrImg.naturalHeight; + canvas.getContext('2d').drawImage(qrImg, 0, 0); + canvas.toBlob(function (blob) { + if (blob) triggerFileDownload(blob, filename); + }, 'image/png'); + return true; + } catch (err) { + return false; + } + } + + document.querySelectorAll('a.file-download').forEach(function (link) { + link.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + var filename = link.getAttribute('data-filename') || link.getAttribute('download') || 'download'; + var href = link.getAttribute('href'); + var fallbackText = link.getAttribute('data-text'); + var isPng = /\.png$/i.test(filename); + + function fallback() { + if (fallbackText !== null) { + triggerFileDownload(new Blob([fallbackText], { type: 'text/plain;charset=utf-8' }), filename); + return; + } + if (isPng) pngFromCard(link, filename); + } + + if (!href) { + fallback(); + return; + } + + fetch(href) + .then(function (res) { + if (!res.ok) throw new Error('fetch failed'); + return res.blob(); + }) + .then(function (blob) { triggerFileDownload(blob, filename); }) + .catch(fallback); + }); + }); + })();