diff --git a/roles/hysteria2/templates/export/global-index.html.j2 b/roles/hysteria2/templates/export/global-index.html.j2 index d112506..b2c3d5f 100644 --- a/roles/hysteria2/templates/export/global-index.html.j2 +++ b/roles/hysteria2/templates/export/global-index.html.j2 @@ -718,16 +718,19 @@ +{% set _png = namespace(b64='', data='') %} {% if user.has_png | default(false) %} +{% set _png.b64 = lookup('pipe', 'base64 < ' ~ ((hysteria2_output_dir ~ '/' ~ server.dir ~ '/' ~ user.name ~ '.png') | quote)) | regex_replace('\\s+', '') %} +{% set _png.data = 'data:image/png;base64,' ~ _png.b64 %}
.url
.txt
-{% if user.has_png | default(false) %}
- .png
+{% if user.has_png | default(false) and _png.b64 %}
+ .png
{% endif %}
- .qr.txt
+ .qr.txt
{% endfor %}
@@ -870,10 +873,16 @@
e.preventDefault();
e.stopPropagation();
var filename = downloadBtn.getAttribute('data-filename') || 'qr.png';
- if (downloadQrFromCanvas(filename)) return;
-
var src = downloadSrc || img.currentSrc || img.src;
- if (!src) return;
+
+ // Embedded data: URLs work on file:// (no CORS / tainted canvas)
+ if (src && src.indexOf('data:') === 0) {
+ triggerDataUrlDownload(src, filename);
+ return;
+ }
+ if (downloadQrFromCanvas(filename)) return;
+ if (!src || location.protocol === 'file:') return;
+
fetch(src)
.then(function (res) {
if (!res.ok) throw new Error('fetch failed');
@@ -960,7 +969,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 || !qrImg.complete || !qrImg.naturalWidth) return false;
+ if (!qrImg) return false;
+ var src = qrImg.currentSrc || qrImg.src || '';
+ if (src.indexOf('data:image/') === 0) {
+ triggerDataUrlDownload(src, filename);
+ return true;
+ }
+ if (!qrImg.complete || !qrImg.naturalWidth) return false;
try {
var canvas = document.createElement('canvas');
canvas.width = qrImg.naturalWidth;
@@ -980,15 +995,20 @@
var filename = link.getAttribute('data-filename') || link.getAttribute('download') || 'download';
var href = link.getAttribute('href');
var fallbackText = link.getAttribute('data-text');
+ var pngB64 = link.getAttribute('data-png-b64');
var isPng = /\.png$/i.test(filename);
- // Sync paths first — async fetch loses the user-gesture in Safari/Chrome
+ // Sync paths first — file:// blocks fetch (CORS) and taints canvas
+ if (pngB64) {
+ triggerDataUrlDownload('data:image/png;base64,' + pngB64, filename);
+ return;
+ }
if (isPng && pngFromCard(link, filename)) return;
if (fallbackText !== null) {
triggerFileDownload(new Blob([fallbackText], { type: 'text/plain;charset=utf-8' }), filename);
return;
}
- if (!href) return;
+ if (!href || location.protocol === 'file:') return;
fetch(href)
.then(function (res) {
diff --git a/roles/hysteria2/templates/export/index.html.j2 b/roles/hysteria2/templates/export/index.html.j2
index 6834a34..a489be1 100644
--- a/roles/hysteria2/templates/export/index.html.j2
+++ b/roles/hysteria2/templates/export/index.html.j2
@@ -424,16 +424,19 @@
.url
.txt
-{% if user.has_png | default(false) %}
- .png
+{% if user.has_png | default(false) and _png.b64 %}
+ .png
{% endif %}
- .qr.txt
+ .qr.txt
{% endfor %}
@@ -547,10 +550,16 @@
e.preventDefault();
e.stopPropagation();
var filename = downloadBtn.getAttribute('data-filename') || 'qr.png';
- if (downloadQrFromCanvas(filename)) return;
-
var src = downloadSrc || img.currentSrc || img.src;
- if (!src) return;
+
+ // Embedded data: URLs work on file:// (no CORS / tainted canvas)
+ if (src && src.indexOf('data:') === 0) {
+ triggerDataUrlDownload(src, filename);
+ return;
+ }
+ if (downloadQrFromCanvas(filename)) return;
+ if (!src || location.protocol === 'file:') return;
+
fetch(src)
.then(function (res) {
if (!res.ok) throw new Error('fetch failed');
@@ -637,7 +646,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 || !qrImg.complete || !qrImg.naturalWidth) return false;
+ if (!qrImg) return false;
+ var src = qrImg.currentSrc || qrImg.src || '';
+ if (src.indexOf('data:image/') === 0) {
+ triggerDataUrlDownload(src, filename);
+ return true;
+ }
+ if (!qrImg.complete || !qrImg.naturalWidth) return false;
try {
var canvas = document.createElement('canvas');
canvas.width = qrImg.naturalWidth;
@@ -657,15 +672,20 @@
var filename = link.getAttribute('data-filename') || link.getAttribute('download') || 'download';
var href = link.getAttribute('href');
var fallbackText = link.getAttribute('data-text');
+ var pngB64 = link.getAttribute('data-png-b64');
var isPng = /\.png$/i.test(filename);
- // Sync paths first — async fetch loses the user-gesture in Safari/Chrome
+ // Sync paths first — file:// blocks fetch (CORS) and taints canvas
+ if (pngB64) {
+ triggerDataUrlDownload('data:image/png;base64,' + pngB64, filename);
+ return;
+ }
if (isPng && pngFromCard(link, filename)) return;
if (fallbackText !== null) {
triggerFileDownload(new Blob([fallbackText], { type: 'text/plain;charset=utf-8' }), filename);
return;
}
- if (!href) return;
+ if (!href || location.protocol === 'file:') return;
fetch(href)
.then(function (res) {