diff --git a/roles/hysteria2/templates/export/global-index.html.j2 b/roles/hysteria2/templates/export/global-index.html.j2
index dba17c9..d112506 100644
--- a/roles/hysteria2/templates/export/global-index.html.j2
+++ b/roles/hysteria2/templates/export/global-index.html.j2
@@ -516,8 +516,10 @@
background: var(--bg-input);
color: var(--accent);
text-decoration: none;
+ font: inherit;
font-size: 0.85rem;
font-weight: 650;
+ cursor: pointer;
transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.lightbox-download:hover {
@@ -625,7 +627,7 @@
- {{ server.mode | default('salamander') | e }}
+ {{ server.mode | default('masquerade') | e }}
@@ -759,10 +761,10 @@
@@ -827,6 +829,16 @@
var downloadBtn = document.getElementById('lightbox-download');
var downloadSrc = '';
+ function triggerDataUrlDownload(dataUrl, filename) {
+ var a = document.createElement('a');
+ a.href = dataUrl;
+ a.download = filename;
+ a.rel = 'noopener';
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ }
+
function triggerFileDownload(blob, filename) {
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
@@ -839,49 +851,36 @@
setTimeout(function () { URL.revokeObjectURL(url); }, 1500);
}
+ function downloadQrFromCanvas(filename) {
+ if (!img.complete || !img.naturalWidth) return false;
+ try {
+ var canvas = document.createElement('canvas');
+ canvas.width = img.naturalWidth;
+ canvas.height = img.naturalHeight;
+ canvas.getContext('2d').drawImage(img, 0, 0);
+ // toDataURL sync — download stays in the user-gesture (Safari/Chrome)
+ triggerDataUrlDownload(canvas.toDataURL('image/png'), filename);
+ return true;
+ } catch (err) {
+ return false;
+ }
+ }
+
function downloadQrImage(e) {
e.preventDefault();
e.stopPropagation();
- var filename = downloadBtn.getAttribute('data-filename') || downloadBtn.getAttribute('download') || 'qr.png';
+ var filename = downloadBtn.getAttribute('data-filename') || 'qr.png';
+ if (downloadQrFromCanvas(filename)) return;
+
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();
- }
+ if (!src) return;
+ fetch(src)
+ .then(function (res) {
+ if (!res.ok) throw new Error('fetch failed');
+ return res.blob();
+ })
+ .then(function (blob) { triggerFileDownload(blob, filename); })
+ .catch(function () { /* avoid navigating to the image URL */ });
}
function openLightbox(src, text, alt, filename) {
@@ -890,8 +889,6 @@
img.alt = alt || text || 'QR';
caption.textContent = text || '';
downloadSrc = src;
- downloadBtn.href = '#';
- downloadBtn.setAttribute('download', safeName);
downloadBtn.setAttribute('data-filename', safeName);
downloadBtn.setAttribute('aria-label', 'Скачать ' + safeName);
box.classList.add('open');
@@ -938,6 +935,16 @@
})();
(function () {
+ function triggerDataUrlDownload(dataUrl, filename) {
+ var a = document.createElement('a');
+ a.href = dataUrl;
+ a.download = filename;
+ a.rel = 'noopener';
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ }
+
function triggerFileDownload(blob, filename) {
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
@@ -953,16 +960,13 @@
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;
+ if (!qrImg || !qrImg.complete || !qrImg.naturalWidth) 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');
+ triggerDataUrlDownload(canvas.toDataURL('image/png'), filename);
return true;
} catch (err) {
return false;
@@ -978,18 +982,13 @@
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();
+ // Sync paths first — async fetch loses the user-gesture in Safari/Chrome
+ if (isPng && pngFromCard(link, filename)) return;
+ if (fallbackText !== null) {
+ triggerFileDownload(new Blob([fallbackText], { type: 'text/plain;charset=utf-8' }), filename);
return;
}
+ if (!href) return;
fetch(href)
.then(function (res) {
@@ -997,7 +996,7 @@
return res.blob();
})
.then(function (blob) { triggerFileDownload(blob, filename); })
- .catch(fallback);
+ .catch(function () { /* do not navigate to the file URL */ });
});
});
})();
diff --git a/roles/hysteria2/templates/export/index.html.j2 b/roles/hysteria2/templates/export/index.html.j2
index 70713c7..6834a34 100644
--- a/roles/hysteria2/templates/export/index.html.j2
+++ b/roles/hysteria2/templates/export/index.html.j2
@@ -333,8 +333,10 @@
background: var(--bg-input);
color: var(--accent);
text-decoration: none;
+ font: inherit;
font-size: 0.85rem;
font-weight: 650;
+ cursor: pointer;
}
.lightbox-download:hover {
border-color: var(--accent);
@@ -463,10 +465,10 @@
@@ -504,6 +506,16 @@
var downloadBtn = document.getElementById('lightbox-download');
var downloadSrc = '';
+ function triggerDataUrlDownload(dataUrl, filename) {
+ var a = document.createElement('a');
+ a.href = dataUrl;
+ a.download = filename;
+ a.rel = 'noopener';
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ }
+
function triggerFileDownload(blob, filename) {
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
@@ -516,49 +528,36 @@
setTimeout(function () { URL.revokeObjectURL(url); }, 1500);
}
+ function downloadQrFromCanvas(filename) {
+ if (!img.complete || !img.naturalWidth) return false;
+ try {
+ var canvas = document.createElement('canvas');
+ canvas.width = img.naturalWidth;
+ canvas.height = img.naturalHeight;
+ canvas.getContext('2d').drawImage(img, 0, 0);
+ // toDataURL sync — download stays in the user-gesture (Safari/Chrome)
+ triggerDataUrlDownload(canvas.toDataURL('image/png'), filename);
+ return true;
+ } catch (err) {
+ return false;
+ }
+ }
+
function downloadQrImage(e) {
e.preventDefault();
e.stopPropagation();
- var filename = downloadBtn.getAttribute('data-filename') || downloadBtn.getAttribute('download') || 'qr.png';
+ var filename = downloadBtn.getAttribute('data-filename') || 'qr.png';
+ if (downloadQrFromCanvas(filename)) return;
+
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();
- }
+ if (!src) return;
+ fetch(src)
+ .then(function (res) {
+ if (!res.ok) throw new Error('fetch failed');
+ return res.blob();
+ })
+ .then(function (blob) { triggerFileDownload(blob, filename); })
+ .catch(function () { /* avoid navigating to the image URL */ });
}
function openLightbox(src, text, alt, filename) {
@@ -567,8 +566,6 @@
img.alt = alt || text || 'QR';
caption.textContent = text || '';
downloadSrc = src;
- downloadBtn.href = '#';
- downloadBtn.setAttribute('download', safeName);
downloadBtn.setAttribute('data-filename', safeName);
downloadBtn.setAttribute('aria-label', 'Скачать ' + safeName);
box.classList.add('open');
@@ -615,6 +612,16 @@
})();
(function () {
+ function triggerDataUrlDownload(dataUrl, filename) {
+ var a = document.createElement('a');
+ a.href = dataUrl;
+ a.download = filename;
+ a.rel = 'noopener';
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ }
+
function triggerFileDownload(blob, filename) {
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
@@ -630,16 +637,13 @@
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;
+ if (!qrImg || !qrImg.complete || !qrImg.naturalWidth) 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');
+ triggerDataUrlDownload(canvas.toDataURL('image/png'), filename);
return true;
} catch (err) {
return false;
@@ -655,18 +659,13 @@
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();
+ // Sync paths first — async fetch loses the user-gesture in Safari/Chrome
+ if (isPng && pngFromCard(link, filename)) return;
+ if (fallbackText !== null) {
+ triggerFileDownload(new Blob([fallbackText], { type: 'text/plain;charset=utf-8' }), filename);
return;
}
+ if (!href) return;
fetch(href)
.then(function (res) {
@@ -674,7 +673,7 @@
return res.blob();
})
.then(function (blob) { triggerFileDownload(blob, filename); })
- .catch(fallback);
+ .catch(function () { /* do not navigate to the file URL */ });
});
});
})();