diff --git a/roles/hysteria2/templates/export/global-index.html.j2 b/roles/hysteria2/templates/export/global-index.html.j2
index 54ecd95..29ef5da 100644
--- a/roles/hysteria2/templates/export/global-index.html.j2
+++ b/roles/hysteria2/templates/export/global-index.html.j2
@@ -759,7 +759,7 @@
-
+
Скачать QR
@@ -825,14 +825,75 @@
var caption = document.getElementById('lightbox-caption');
var closeBtn = document.getElementById('lightbox-close');
var downloadBtn = document.getElementById('lightbox-download');
+ var downloadSrc = '';
+
+ 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 downloadQrImage(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ var filename = downloadBtn.getAttribute('data-filename') || downloadBtn.getAttribute('download') || 'qr.png';
+ var src = downloadSrc || img.currentSrc || img.src;
+
+ function fromCanvas() {
+ try {
+ if (!img.complete || !img.naturalWidth) return false;
+ var canvas = document.createElement('canvas');
+ canvas.width = img.naturalWidth;
+ canvas.height = img.naturalHeight;
+ canvas.getContext('2d').drawImage(img, 0, 0);
+ canvas.toBlob(function (blob) {
+ if (blob) triggerFileDownload(blob, filename);
+ }, 'image/png');
+ return true;
+ } catch (err) {
+ return false;
+ }
+ }
+
+ if (src) {
+ fetch(src)
+ .then(function (res) {
+ if (!res.ok) throw new Error('fetch failed');
+ return res.blob();
+ })
+ .then(function (blob) { triggerFileDownload(blob, filename); })
+ .catch(function () {
+ if (!fromCanvas()) {
+ var a = document.createElement('a');
+ a.href = src;
+ a.download = filename;
+ a.rel = 'noopener';
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ }
+ });
+ } else {
+ fromCanvas();
+ }
+ }
function openLightbox(src, text, alt, filename) {
+ var safeName = filename || 'qr.png';
img.src = src;
img.alt = alt || text || 'QR';
caption.textContent = text || '';
- downloadBtn.href = src;
- downloadBtn.setAttribute('download', filename || 'qr.png');
- downloadBtn.setAttribute('aria-label', 'Скачать ' + (filename || 'qr.png'));
+ downloadSrc = src;
+ downloadBtn.href = '#';
+ downloadBtn.setAttribute('download', safeName);
+ downloadBtn.setAttribute('data-filename', safeName);
+ downloadBtn.setAttribute('aria-label', 'Скачать ' + safeName);
box.classList.add('open');
box.setAttribute('aria-hidden', 'false');
document.body.classList.add('lightbox-open');
@@ -844,7 +905,8 @@
box.setAttribute('aria-hidden', 'true');
document.body.classList.remove('lightbox-open');
img.removeAttribute('src');
- downloadBtn.removeAttribute('href');
+ downloadSrc = '';
+ downloadBtn.removeAttribute('data-filename');
}
document.querySelectorAll('[data-lightbox-src]').forEach(function (el) {
@@ -865,6 +927,7 @@
});
});
+ downloadBtn.addEventListener('click', downloadQrImage);
closeBtn.addEventListener('click', closeLightbox);
box.addEventListener('click', function (e) {
if (e.target === box) closeLightbox();
diff --git a/roles/hysteria2/templates/export/index.html.j2 b/roles/hysteria2/templates/export/index.html.j2
index 96aa3e9..0434ee6 100644
--- a/roles/hysteria2/templates/export/index.html.j2
+++ b/roles/hysteria2/templates/export/index.html.j2
@@ -463,7 +463,7 @@
-
+
Скачать QR
@@ -502,14 +502,75 @@
var caption = document.getElementById('lightbox-caption');
var closeBtn = document.getElementById('lightbox-close');
var downloadBtn = document.getElementById('lightbox-download');
+ var downloadSrc = '';
+
+ 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 downloadQrImage(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ var filename = downloadBtn.getAttribute('data-filename') || downloadBtn.getAttribute('download') || 'qr.png';
+ var src = downloadSrc || img.currentSrc || img.src;
+
+ function fromCanvas() {
+ try {
+ if (!img.complete || !img.naturalWidth) return false;
+ var canvas = document.createElement('canvas');
+ canvas.width = img.naturalWidth;
+ canvas.height = img.naturalHeight;
+ canvas.getContext('2d').drawImage(img, 0, 0);
+ canvas.toBlob(function (blob) {
+ if (blob) triggerFileDownload(blob, filename);
+ }, 'image/png');
+ return true;
+ } catch (err) {
+ return false;
+ }
+ }
+
+ if (src) {
+ fetch(src)
+ .then(function (res) {
+ if (!res.ok) throw new Error('fetch failed');
+ return res.blob();
+ })
+ .then(function (blob) { triggerFileDownload(blob, filename); })
+ .catch(function () {
+ if (!fromCanvas()) {
+ var a = document.createElement('a');
+ a.href = src;
+ a.download = filename;
+ a.rel = 'noopener';
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ }
+ });
+ } else {
+ fromCanvas();
+ }
+ }
function openLightbox(src, text, alt, filename) {
+ var safeName = filename || 'qr.png';
img.src = src;
img.alt = alt || text || 'QR';
caption.textContent = text || '';
- downloadBtn.href = src;
- downloadBtn.setAttribute('download', filename || 'qr.png');
- downloadBtn.setAttribute('aria-label', 'Скачать ' + (filename || 'qr.png'));
+ downloadSrc = src;
+ downloadBtn.href = '#';
+ downloadBtn.setAttribute('download', safeName);
+ downloadBtn.setAttribute('data-filename', safeName);
+ downloadBtn.setAttribute('aria-label', 'Скачать ' + safeName);
box.classList.add('open');
box.setAttribute('aria-hidden', 'false');
document.body.classList.add('lightbox-open');
@@ -521,7 +582,8 @@
box.setAttribute('aria-hidden', 'true');
document.body.classList.remove('lightbox-open');
img.removeAttribute('src');
- downloadBtn.removeAttribute('href');
+ downloadSrc = '';
+ downloadBtn.removeAttribute('data-filename');
}
document.querySelectorAll('[data-lightbox-src]').forEach(function (el) {
@@ -542,6 +604,7 @@
});
});
+ downloadBtn.addEventListener('click', downloadQrImage);
closeBtn.addEventListener('click', closeLightbox);
box.addEventListener('click', function (e) {
if (e.target === box) closeLightbox();