Bilder-Download: Mehrfach-Auswahl + Ordner-ZIP

- Selection-Mode: Bilder in Galerie per Klick auswaehlen
- 1-3 Bilder: einzeln herunterladen, 4+: als ZIP
- Ordner-Download: ganzen zugewiesenen Ordner als ZIP
- Sicherheit: Zugriffspruefung + Path-Traversal-Schutz
- archiver Package fuer Streaming-ZIP-Erstellung

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kroonk
2026-04-16 22:47:58 +02:00
parent 43539fa1a3
commit 159071a814
9 changed files with 1542 additions and 24 deletions

View File

@@ -1 +1 @@
.text-fg{color:#a0a0a1}#footer .presentify{margin:0 0 1.4em 0;display:grid;grid-template-columns:auto 1fr;column-gap:1rem;align-items:center}#footer .presentify .presentify-logo{display:flex;align-items:center;justify-content:center}#footer .presentify .presentify-logo a{display:flex;align-items:center;justify-content:center;border:0}#footer .presentify .presentify-logo img{width:72px;height:72px;display:block}#footer .presentify .presentify-text{margin:0}
.text-fg{color:#a0a0a1}#selection-toolbar{position:fixed;bottom:4em;left:0;width:100%;background:rgba(31,34,36,.95);padding:.75em 1.5em;display:flex;align-items:center;gap:1em;z-index:10003;transform:translateY(100%);transition:transform .3s ease;box-sizing:border-box}#selection-toolbar.visible{transform:translateY(0)}#selection-toolbar #sel-count{color:#fff;font-weight:400;margin-right:auto;white-space:nowrap}#selection-toolbar button{white-space:nowrap}body.selection-mode #main .thumb{cursor:pointer;position:relative}body.selection-mode #main .thumb::before{content:"";position:absolute;top:.6em;left:.6em;width:1.4em;height:1.4em;border:2px solid rgba(255,255,255,.7);border-radius:3px;background:rgba(0,0,0,.4);z-index:2;transition:all .2s ease;pointer-events:none}body.selection-mode #main .thumb.selected::before{background:#34a58e;border-color:#34a58e}body.selection-mode #main .thumb.selected .image{outline:3px solid #34a58e;outline-offset:-3px}@media screen and (max-width: 736px){#selection-toolbar{bottom:auto;top:4em;transform:translateY(-200%)}#selection-toolbar.visible{transform:translateY(0)}}#footer .presentify{margin:0 0 1.4em 0;display:grid;grid-template-columns:auto 1fr;column-gap:1rem;align-items:center}#footer .presentify .presentify-logo{display:flex;align-items:center;justify-content:center}#footer .presentify .presentify-logo a{display:flex;align-items:center;justify-content:center;border:0}#footer .presentify .presentify-logo img{width:72px;height:72px;display:block}#footer .presentify .presentify-text{margin:0}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -194,6 +194,7 @@
initThumbs();
initPoptrox();
initSelectionMode(images);
},
error: function() {
$main.html('<p style="text-align:center;color:#fff;padding:2em;">Keine Bilder gefunden. Lade Bilder in deinen Nextcloud-Ordner hoch.</p>');
@@ -290,6 +291,177 @@
return template;
}
// ==================== SELECTION MODE ====================
function initSelectionMode(allImages) {
var selectedImages = [];
var selectionActive = false;
// Toolbar erstellen (versteckt)
var $toolbar = $('<div id="selection-toolbar"></div>');
$toolbar.html(
'<span id="sel-count">0 ausgewaehlt</span>' +
'<button id="sel-download" class="button small" disabled>' +
'<i class="fa fa-download"></i> Herunterladen</button>' +
'<button id="sel-cancel" class="button small">' +
'<i class="fa fa-times"></i> Abbrechen</button>'
);
$('body').append($toolbar);
// Selection-Toggle Button im Header
var $selectToggle = $('<li><a href="#" id="select-mode-toggle" title="Bilder auswaehlen">' +
'<i class="fa fa-check-square"></i></a></li>');
$('#header nav > ul').prepend($selectToggle);
// Ordner-Download Buttons (nur wenn Ordner-Bilder vorhanden)
var folders = [];
allImages.forEach(function(img) {
if (img.folder && folders.indexOf(img.folder) === -1) {
folders.push(img.folder);
}
});
if (folders.length > 0) {
folders.forEach(function(folder) {
var $folderBtn = $('<li><a href="#" class="folder-download-btn" ' +
'data-folder="' + folder + '" title="Ordner \'' + folder + '\' herunterladen">' +
'<i class="fa fa-folder-open"></i></a></li>');
$('#header nav > ul').prepend($folderBtn);
});
}
// Selection-Mode toggle
$(document).on('click', '#select-mode-toggle', function(e) {
e.preventDefault();
e.stopPropagation();
selectionActive = !selectionActive;
$('body').toggleClass('selection-mode', selectionActive);
if (!selectionActive) clearSelection();
$toolbar.toggleClass('visible', selectionActive);
});
// Klick auf Thumbnail im Selection-Mode
$(document).on('click', '.thumb', function(e) {
if (!selectionActive) return;
e.preventDefault();
e.stopPropagation();
var $thumb = $(this);
var $img = $thumb.find('img');
var imgName = $img.data('name');
var imgHref = $thumb.find('a.image').attr('href');
// Ordner aus href extrahieren
var folder = null;
var hrefPath = imgHref.replace('/images/fulls/', '');
var parts = hrefPath.split('/');
if (parts.length > 1) {
folder = decodeURIComponent(parts[0]);
}
var idx = -1;
for (var i = 0; i < selectedImages.length; i++) {
if (selectedImages[i].name === imgName && selectedImages[i].folder === folder) {
idx = i; break;
}
}
if (idx !== -1) {
selectedImages.splice(idx, 1);
$thumb.removeClass('selected');
} else {
selectedImages.push({ name: imgName, folder: folder });
$thumb.addClass('selected');
}
updateToolbar();
});
function updateToolbar() {
var count = selectedImages.length;
$('#sel-count').text(count + ' ausgewaehlt');
$('#sel-download').prop('disabled', count === 0);
if (count > 3) {
$('#sel-download').html('<i class="fa fa-file-archive-o"></i> Als ZIP (' + count + ')');
} else if (count > 0) {
$('#sel-download').html('<i class="fa fa-download"></i> Herunterladen (' + count + ')');
} else {
$('#sel-download').html('<i class="fa fa-download"></i> Herunterladen');
}
}
function clearSelection() {
selectedImages = [];
$('.thumb').removeClass('selected');
updateToolbar();
}
// Download-Button
$(document).on('click', '#sel-download', function() {
if (selectedImages.length === 0) return;
$.ajax({
url: '/api/download/selected',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ images: selectedImages }),
xhrFields: { responseType: 'blob' },
success: function(data, status, xhr) {
var ct = xhr.getResponseHeader('Content-Type');
if (ct && ct.indexOf('application/json') !== -1) {
var reader = new FileReader();
reader.onload = function() {
var resp = JSON.parse(this.result);
if (resp.mode === 'individual') {
resp.urls.forEach(function(url, i) {
setTimeout(function() {
var a = document.createElement('a');
a.href = url;
a.download = '';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}, i * 300);
});
}
};
reader.readAsText(data);
} else {
var url = window.URL.createObjectURL(data);
var a = document.createElement('a');
a.href = url;
a.download = 'mischkomposition-auswahl.zip';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}
},
error: function(xhr) {
alert(xhr.responseJSON ? xhr.responseJSON.error : 'Download fehlgeschlagen');
}
});
});
// Abbrechen-Button
$(document).on('click', '#sel-cancel', function() {
selectionActive = false;
$('body').removeClass('selection-mode');
$toolbar.removeClass('visible');
clearSelection();
});
// Ordner-Download
$(document).on('click', '.folder-download-btn', function(e) {
e.preventDefault();
e.stopPropagation();
var folder = $(this).data('folder');
var a = document.createElement('a');
a.href = '/api/download/folder/' + encodeURIComponent(folder);
a.download = folder + '.zip';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
}
// Galerie laden
loadGallery();

File diff suppressed because one or more lines are too long

View File

@@ -6,6 +6,85 @@
color: _palette(fg);
}
/* ==================== Selection Mode ==================== */
#selection-toolbar {
position: fixed;
bottom: _size(header);
left: 0;
width: 100%;
background: rgba(31, 34, 36, 0.95);
padding: 0.75em 1.5em;
display: flex;
align-items: center;
gap: 1em;
z-index: _misc(z-index-base) + 3;
transform: translateY(100%);
transition: transform 0.3s ease;
box-sizing: border-box;
&.visible {
transform: translateY(0);
}
#sel-count {
color: _palette(fg-bold);
font-weight: _font(weight-extrabold);
margin-right: auto;
white-space: nowrap;
}
button {
white-space: nowrap;
}
}
body.selection-mode {
#main .thumb {
cursor: pointer;
position: relative;
&::before {
content: '';
position: absolute;
top: 0.6em;
left: 0.6em;
width: 1.4em;
height: 1.4em;
border: 2px solid rgba(255, 255, 255, 0.7);
border-radius: 3px;
background: rgba(0, 0, 0, 0.4);
z-index: 2;
transition: all 0.2s ease;
pointer-events: none;
}
&.selected {
&::before {
background: _palette(accent1);
border-color: _palette(accent1);
}
.image {
outline: 3px solid _palette(accent1);
outline-offset: -3px;
}
}
}
}
@media screen and (max-width: 736px) {
#selection-toolbar {
bottom: auto;
top: _size(header);
transform: translateY(-200%);
&.visible {
transform: translateY(0);
}
}
}
/* Footer */
#footer {
.presentify {