- 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>
528 lines
15 KiB
JavaScript
528 lines
15 KiB
JavaScript
/*
|
|
Multiverse by HTML5 UP
|
|
html5up.net | @ajlkn
|
|
Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
|
|
|
|
Enhanced for dynamic gallery loading - Mischkomposition by Tom Misch
|
|
*/
|
|
|
|
(function ($) {
|
|
var $window = $(window),
|
|
$body = $("body"),
|
|
$wrapper = $("#wrapper");
|
|
|
|
// Breakpoints.
|
|
breakpoints({
|
|
xlarge: ["1281px", "1680px"],
|
|
large: ["981px", "1280px"],
|
|
medium: ["737px", "980px"],
|
|
small: ["481px", "736px"],
|
|
xsmall: [null, "480px"],
|
|
});
|
|
|
|
// Hack: Enable IE workarounds.
|
|
if (browser.name == "ie") $body.addClass("ie");
|
|
|
|
// Touch?
|
|
if (browser.mobile) $body.addClass("touch");
|
|
|
|
// Transitions supported?
|
|
if (browser.canUse("transition")) {
|
|
// Play initial animations on page load.
|
|
$window.on("load", function () {
|
|
window.setTimeout(function () {
|
|
$body.removeClass("is-preload");
|
|
}, 100);
|
|
});
|
|
|
|
// Prevent transitions/animations on resize.
|
|
var resizeTimeout;
|
|
|
|
$window.on("resize", function () {
|
|
window.clearTimeout(resizeTimeout);
|
|
|
|
$body.addClass("is-resizing");
|
|
|
|
resizeTimeout = window.setTimeout(function () {
|
|
$body.removeClass("is-resizing");
|
|
}, 100);
|
|
});
|
|
}
|
|
|
|
// Scroll back to top.
|
|
$window.scrollTop(0);
|
|
|
|
// Panels.
|
|
var $panels = $(".panel");
|
|
|
|
$panels.each(function () {
|
|
var $this = $(this),
|
|
$toggles = $('[href="#' + $this.attr("id") + '"]'),
|
|
$closer = $('<div class="closer" />').appendTo($this);
|
|
|
|
// Closer.
|
|
$closer.on("click", function (event) {
|
|
$this.trigger("---hide");
|
|
});
|
|
|
|
// Events.
|
|
$this
|
|
.on("click", function (event) {
|
|
event.stopPropagation();
|
|
})
|
|
.on("---toggle", function () {
|
|
if ($this.hasClass("active")) $this.triggerHandler("---hide");
|
|
else $this.triggerHandler("---show");
|
|
})
|
|
.on("---show", function () {
|
|
if ($body.hasClass("content-active")) $panels.trigger("---hide");
|
|
$this.addClass("active");
|
|
$toggles.addClass("active");
|
|
$body.addClass("content-active");
|
|
})
|
|
.on("---hide", function () {
|
|
$this.removeClass("active");
|
|
$toggles.removeClass("active");
|
|
$body.removeClass("content-active");
|
|
});
|
|
|
|
// Toggles.
|
|
$toggles
|
|
.removeAttr("href")
|
|
.css("cursor", "pointer")
|
|
.on("click", function (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
$this.trigger("---toggle");
|
|
});
|
|
});
|
|
|
|
// Global events.
|
|
$body.on("click", function (event) {
|
|
if ($body.hasClass("content-active")) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
$panels.trigger("---hide");
|
|
}
|
|
});
|
|
|
|
$window.on("keyup", function (event) {
|
|
if (event.keyCode == 27 && $body.hasClass("content-active")) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
$panels.trigger("---hide");
|
|
}
|
|
});
|
|
|
|
// Header.
|
|
var $header = $("#header");
|
|
|
|
$header.find("a").each(function () {
|
|
var $this = $(this),
|
|
href = $this.attr("href");
|
|
if (!href || href.charAt(0) == "#") return;
|
|
$this
|
|
.removeAttr("href")
|
|
.css("cursor", "pointer")
|
|
.on("click", function (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
window.location.href = href;
|
|
});
|
|
});
|
|
|
|
// Footer.
|
|
var $footer = $("#footer");
|
|
|
|
$footer.find(".copyright").each(function () {
|
|
var $this = $(this),
|
|
$parent = $this.parent(),
|
|
$lastParent = $parent.parent().children().last();
|
|
|
|
breakpoints.on("<=medium", function () {
|
|
$this.appendTo($lastParent);
|
|
});
|
|
|
|
breakpoints.on(">medium", function () {
|
|
$this.appendTo($parent);
|
|
});
|
|
});
|
|
|
|
// Main - Dynamische Galerie
|
|
var $main = $("#main"),
|
|
exifDatas = {};
|
|
|
|
var imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
|
|
|
function loadGallery() {
|
|
$.ajax({
|
|
url: '/images/fulls/',
|
|
dataType: 'json',
|
|
cache: false,
|
|
success: function(files) {
|
|
if(files.length === 0) {
|
|
$main.html('<p style="text-align:center;color:#fff;padding:2em;width:100%">Noch keine Bilder vorhanden.</p>');
|
|
return;
|
|
}
|
|
|
|
var images = files.filter(function(f) {
|
|
if (f.type !== 'file') return false;
|
|
var ext = f.name.split('.').pop().toLowerCase();
|
|
return imageExtensions.indexOf(ext) !== -1;
|
|
});
|
|
|
|
if (images.length === 0) {
|
|
$main.html('<p style="text-align:center;color:#fff;padding:2em;width:100%">Es wurden noch keine passenden Bilder für diesen Account hochgeladen oder zugewiesen.</p>');
|
|
return;
|
|
}
|
|
|
|
images.sort(function(a, b) {
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
|
|
images.forEach(function(file) {
|
|
var article = $('<article class="thumb"></article>');
|
|
var prefix = file.folder ? encodeURIComponent(file.folder) + '/' : '';
|
|
var fullPath = '/images/fulls/' + prefix + encodeURIComponent(file.name);
|
|
var thumbPath = '/images/thumbs/' + prefix + encodeURIComponent(file.name);
|
|
var link = $('<a class="image" href="' + fullPath + '"></a>');
|
|
var img = $('<img src="' + thumbPath + '" alt="" data-name="' + file.name.replace(/"/g, '"') + '" />');
|
|
link.append(img);
|
|
article.append(link);
|
|
$main.append(article);
|
|
});
|
|
|
|
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>');
|
|
}
|
|
});
|
|
}
|
|
|
|
function initThumbs() {
|
|
$main.children(".thumb").each(function () {
|
|
var $this = $(this),
|
|
$image = $this.find(".image"),
|
|
$image_img = $image.children("img"),
|
|
x;
|
|
|
|
if ($image.length == 0) return;
|
|
|
|
$image.css("background-image", "url('" + $image_img.attr("src") + "')");
|
|
if ((x = $image_img.data("position"))) $image.css("background-position", x);
|
|
$image_img.hide();
|
|
|
|
// Fallback: Wenn Thumbnail fehlt, Vollbild als Vorschau nutzen
|
|
$image_img[0].addEventListener("error", function() {
|
|
var fullUrl = $image.attr("href");
|
|
if (fullUrl) {
|
|
$image.css("background-image", "url('" + fullUrl + "')");
|
|
}
|
|
});
|
|
|
|
$image_img[0].addEventListener("load", function() {
|
|
EXIF.getData($image_img[0], function () {
|
|
exifDatas[$image_img.data('name')] = getExifDataMarkup(this);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function initPoptrox() {
|
|
$main.poptrox({
|
|
baseZIndex: 20000,
|
|
caption: function ($a) {
|
|
var $image_img = $a.children('img');
|
|
var data = exifDatas[$image_img.data('name')];
|
|
if (data === undefined) {
|
|
EXIF.getData($image_img[0], function () {
|
|
data = exifDatas[$image_img.data('name')] = getExifDataMarkup(this);
|
|
});
|
|
}
|
|
var downBtn = '<br/><a href="' + $a.attr('href') + '" download="' + $image_img.data('name') + '" class="button small" style="margin-top:10px;">Bild Speichern <i class="fa fa-download"></i></a>';
|
|
return data !== undefined ? '<p>' + data + downBtn + '</p>' : '<p>' + downBtn + '</p>';
|
|
},
|
|
fadeSpeed: 300,
|
|
onPopupClose: function () {
|
|
$body.removeClass("modal-active");
|
|
},
|
|
onPopupOpen: function () {
|
|
$body.addClass("modal-active");
|
|
},
|
|
overlayOpacity: 0,
|
|
popupCloserText: "",
|
|
popupHeight: 150,
|
|
popupLoaderText: "",
|
|
popupSpeed: 300,
|
|
popupWidth: 150,
|
|
selector: ".thumb > a.image",
|
|
usePopupCaption: true,
|
|
usePopupCloser: true,
|
|
usePopupDefaultStyling: false,
|
|
usePopupForceClose: true,
|
|
usePopupLoader: true,
|
|
usePopupNav: true,
|
|
windowMargin: 50,
|
|
});
|
|
|
|
breakpoints.on("<=xsmall", function () {
|
|
$main[0]._poptrox.windowMargin = 0;
|
|
});
|
|
|
|
breakpoints.on(">xsmall", function () {
|
|
$main[0]._poptrox.windowMargin = 50;
|
|
});
|
|
}
|
|
|
|
function getExifDataMarkup(img) {
|
|
var exif = $('#main').data('exif');
|
|
var template = '';
|
|
|
|
for (var current in exif) {
|
|
var current_data = exif[current];
|
|
var exif_data = EXIF.getTag(img, current_data['tag']);
|
|
if (typeof exif_data !== "undefined") {
|
|
template += '<i class="' + current_data['icon'] + '" aria-hidden="true"></i> ' + exif_data + ' ';
|
|
}
|
|
}
|
|
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();
|
|
|
|
// Theme Toggle
|
|
var currentTheme = localStorage.getItem('mischkomposition_theme') || 'main';
|
|
updateThemeIcon(currentTheme);
|
|
|
|
$('#theme-toggle').click(function(e) {
|
|
e.preventDefault();
|
|
currentTheme = currentTheme === 'main' ? 'light' : 'main';
|
|
localStorage.setItem('mischkomposition_theme', currentTheme);
|
|
$('#theme-style').attr('href', 'assets/css/' + currentTheme + '.min.css');
|
|
updateThemeIcon(currentTheme);
|
|
});
|
|
|
|
function updateThemeIcon(theme) {
|
|
if (theme === 'light') {
|
|
$('#theme-toggle i').removeClass('fa-sun').addClass('fa-moon');
|
|
} else {
|
|
$('#theme-toggle i').removeClass('fa-moon').addClass('fa-sun');
|
|
}
|
|
}
|
|
|
|
// Login Handling
|
|
$.get('/api/auth/me').done(function(u) {
|
|
$('#login-username, #login-password, #login-submit').hide();
|
|
$('#logout-btn').show().css('display', 'inline-block');
|
|
$('#pw-change-section').show();
|
|
if(u.role === 'admin') {
|
|
$('#admin-link').html('<a href="/admin.html" class="button">Admin-Dashboard</a>');
|
|
}
|
|
$('#login-nav').html('Account <i class="fa fa-user"></i>');
|
|
});
|
|
|
|
$('#login-form').submit(function(e) {
|
|
e.preventDefault();
|
|
$.post('/api/auth/login', { username: $('#login-username').val(), password: $('#login-password').val() })
|
|
.done(function() { window.location.reload(); })
|
|
.fail(function(xhr) {
|
|
var msg = 'Login fehlgeschlagen.';
|
|
if (xhr.responseJSON && xhr.responseJSON.error === 'Account abgelaufen') {
|
|
msg = 'Dein Account ist abgelaufen. Bitte kontaktiere den Administrator.';
|
|
}
|
|
alert(msg);
|
|
});
|
|
});
|
|
|
|
$('#logout-btn').click(function(e) {
|
|
e.preventDefault();
|
|
$.post('/api/auth/logout').done(function() { window.location.reload(); });
|
|
});
|
|
|
|
$('#pw-change-form').submit(function(e) {
|
|
e.preventDefault();
|
|
var newPw = $('#pw-new').val();
|
|
var confirmPw = $('#pw-confirm').val();
|
|
if (newPw !== confirmPw) { alert('Passwoerter stimmen nicht ueberein.'); return; }
|
|
$.post('/api/auth/change-password', { old_password: $('#pw-old').val(), new_password: newPw })
|
|
.done(function() { alert('Passwort geaendert!'); $('#pw-old, #pw-new, #pw-confirm').val(''); })
|
|
.fail(function(xhr) { alert(xhr.responseJSON ? xhr.responseJSON.error : 'Fehler'); });
|
|
});
|
|
|
|
})(jQuery);
|