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://.
This commit is contained in:
2026-07-23 10:28:14 +03:00
parent 1d0e7d3bb1
commit 7724c0502a
2 changed files with 138 additions and 8 deletions
@@ -734,12 +734,12 @@
{% endif %}
<nav class="files" aria-label="Файлы">
<a href="{{ server.dir | e }}/{{ user.name | e }}.url" download>.url</a>
<a href="{{ server.dir | e }}/{{ user.name | e }}.txt" download>.txt</a>
<a class="file-download" href="{{ server.dir | e }}/{{ user.name | e }}.url" download="{{ server.name | e }}-{{ user.name | e }}.url" data-filename="{{ server.name | e }}-{{ user.name | e }}.url" data-text="{{ (user.url ~ '\n') | e }}">.url</a>
<a class="file-download" href="{{ server.dir | e }}/{{ user.name | e }}.txt" download="{{ server.name | e }}-{{ user.name | e }}.txt" data-filename="{{ server.name | e }}-{{ user.name | e }}.txt" data-text="{{ ('Server: ' ~ server.name ~ '\nDomain: ' ~ server.domain ~ ':' ~ server.port ~ '\nUser: ' ~ user.name ~ '\nPassword: ' ~ user.password ~ '\n\nURL:\n' ~ user.url ~ '\n') | e }}">.txt</a>
{% if user.has_png | default(false) %}
<a href="{{ server.dir | e }}/{{ user.name | e }}.png" download>.png</a>
<a class="file-download" href="{{ server.dir | e }}/{{ user.name | e }}.png" download="{{ server.name | e }}-{{ user.name | e }}.png" data-filename="{{ server.name | e }}-{{ user.name | e }}.png">.png</a>
{% endif %}
<a href="{{ server.dir | e }}/{{ user.name | e }}.qr.txt" download>.qr.txt</a>
<a class="file-download" href="{{ server.dir | e }}/{{ user.name | e }}.qr.txt" download="{{ server.name | e }}-{{ user.name | e }}.qr.txt" data-filename="{{ server.name | e }}-{{ user.name | e }}.qr.txt" data-text="{{ ('URL: ' ~ user.url ~ '\n') | e }}">.qr.txt</a>
</nav>
</article>
{% 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);
});
});
})();
</script>
</body>
</html>
+69 -4
View File
@@ -440,12 +440,12 @@
{% endif %}
<nav class="files" aria-label="Файлы">
<a href="{{ user.name | e }}.url" download>.url</a>
<a href="{{ user.name | e }}.txt" download>.txt</a>
<a class="file-download" href="{{ user.name | e }}.url" download="{{ hysteria2_output_name | e }}-{{ user.name | e }}.url" data-filename="{{ hysteria2_output_name | e }}-{{ user.name | e }}.url" data-text="{{ (user.url ~ '\n') | e }}">.url</a>
<a class="file-download" href="{{ user.name | e }}.txt" download="{{ hysteria2_output_name | e }}-{{ user.name | e }}.txt" data-filename="{{ hysteria2_output_name | e }}-{{ user.name | e }}.txt" data-text="{{ ('Server: ' ~ hysteria2_output_name ~ '\nDomain: ' ~ hysteria2_domain ~ ':' ~ hysteria2_listen_port ~ '\nUser: ' ~ user.name ~ '\nPassword: ' ~ user.password ~ '\n\nURL:\n' ~ user.url ~ '\n') | e }}">.txt</a>
{% if user.has_png | default(false) %}
<a href="{{ user.name | e }}.png" download>.png</a>
<a class="file-download" href="{{ user.name | e }}.png" download="{{ hysteria2_output_name | e }}-{{ user.name | e }}.png" data-filename="{{ hysteria2_output_name | e }}-{{ user.name | e }}.png">.png</a>
{% endif %}
<a href="{{ user.name | e }}.qr.txt" download>.qr.txt</a>
<a class="file-download" href="{{ user.name | e }}.qr.txt" download="{{ hysteria2_output_name | e }}-{{ user.name | e }}.qr.txt" data-filename="{{ hysteria2_output_name | e }}-{{ user.name | e }}.qr.txt" data-text="{{ ('URL: ' ~ user.url ~ '\n') | e }}">.qr.txt</a>
</nav>
</article>
{% 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);
});
});
})();
</script>
</body>
</html>