Compare commits

...

15 Commits

Author SHA1 Message Date
Tom Misch
5266f36820 Merge GitHub updates into Gitea master
All checks were successful
Build & Push Docker Image / build-and-push (push) Successful in 1m53s
2026-05-11 13:03:58 +02:00
Kroonk
95f2847155 Fix Download: Saubere Trennung JSON (1-3) vs ZIP (4+) Response
- 1-3 Bilder: normales AJAX ohne blob, JSON-Response direkt parsen
- 4+ Bilder: nativer XMLHttpRequest mit blob fuer ZIP-Download
- Fehler-Handler zeigt jetzt den echten Server-Fehler an
  (vorher war xhr.responseJSON immer undefined wegen blob)
- ZIP-Fehler: Blob wird als Text gelesen und JSON geparst
- Download-Dateiname aus URL extrahiert statt leer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 23:52:47 +02:00
Kroonk
b65ade8381 Selection-Mode: Gesamtes Bild klickbar, Checkbox nach unten-rechts
- Capture-Listener auf document statt #main (aggressivster Ansatz)
- stopImmediatePropagation blockt Poptrox komplett
- Checkbox von oben-links nach unten-rechts verschoben
  (wird nicht mehr von Toolbar ueberlagert)
- Nicht-ausgewaehlte Bilder leicht abgedunkelt (opacity 0.85)
- Ausgewaehlte Bilder volle Helligkeit + gruener Rahmen
- Mobile: Checkbox 2em gross fuer bessere Sichtbarkeit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 23:43:33 +02:00
Kroonk
096d766358 Fix Bilder-Auswahl (Capture-Listener) + Mobile UX verbessern
- Selection-Click nutzt jetzt addEventListener mit capture:true
  auf #main, feuert garantiert VOR Poptrox und kann nicht entfernt werden
- Bild-Name/Ordner als data-Attribute auf .thumb gespeichert
- Mobile: Nav-Icons 1rem -> 1.3rem, bessere Touch-Targets
- Mobile: Selection-Checkboxen groesser (1.8em statt 1.4em)
- Mobile: Toolbar-Buttons mit mehr Padding

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 23:35:39 +02:00
Kroonk
75a5a596c5 Fix Selection-Toolbar: Position oben statt ueber Header, direktes Button-Binding
Toolbar wird jetzt am oberen Bildschirmrand angezeigt (Desktop),
damit sie nicht mehr den Header/Nav ueberlagert. Auf Mobile
bleibt sie unterhalb des Headers. Cancel/Download-Buttons nutzen
direktes Binding auf $toolbar statt Delegation auf document.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 23:29:28 +02:00
Kroonk
f4c106bd19 Fix Selection-Mode: Blockiere Lightbox wenn Auswahl aktiv
initSelectionMode wird jetzt VOR initPoptrox aufgerufen und nutzt
einen direkten Click-Handler auf a.image mit stopImmediatePropagation,
damit Poptrox im Selection-Mode nicht mehr die Lightbox oeffnet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 23:17:03 +02:00
Kroonk
74f1da13a3 Mobile-Optimierung: 2-Spalten-Grid, Icon-only Nav, kompakte Toolbar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 23:00:56 +02:00
Kroonk
159071a814 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>
2026-04-16 22:47:58 +02:00
Kroonk
43539fa1a3 Ordner-Tabelle: Fehlerbehandlung und Nachladen wenn leer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 22:17:52 +02:00
Kroonk
2c040b651d Ordner-Zuweisung als klickbare Tabelle statt Checkboxen
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 22:06:43 +02:00
Kroonk
2dcd559501 Fix leere Nutzertabelle: Fehler-Handler + DB-Fallback fuer loadUsers
- admin.js: .fail() Handler zeigt Fehlermeldung in der Tabelle statt stille Leere
- server.js: Fallback-Query (SELECT *) wenn role/created_at Spalten fehlen
- Server loggt DB-Fehler statt sie zu verschlucken

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 21:48:19 +02:00
Kroonk
c37aa3261c Verbessere Nutzer-Erstellung: JSON statt URL-encoded, Erfolgs-Feedback
- Sende Nutzer-Daten als JSON (konsistent mit anderen Admin-Endpoints)
- Zeige Erfolgs-Alert nach Erstellung
- Bessere Fehlermeldungen mit HTTP-Status
- Client-side Validierung vor dem Absenden

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 21:40:56 +02:00
Kroonk
e5e313924d Installiere JPEG/PNG/WebP Delegates fuer ImageMagick in Alpine
Alpine packt Format-Dekoder separat - ohne libjpeg-turbo, libpng, libwebp
kann magick keine JPG/PNG/WebP Dateien lesen ("no decode delegate").

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 21:35:08 +02:00
Kroonk
83058f80c6 Entferne Galerie-Menuepunkt (Watchtower-Deployment-Test)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 20:27:31 +02:00
Kroonk
ea15be0368 Fix Thumbnail-Generierung (magick statt convert) und DB-Migration fuer role-Spalte
- entrypoint.sh: ImageMagick 7 nutzt 'magick' statt 'convert', Auto-Detection
- database.js: Migration fuer role-Spalte bei bestehender DB (ALTER TABLE)
- main.js: Fallback auf Vollbild wenn Thumbnail fehlt (error handler)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 19:40:18 +02:00
18 changed files with 1768 additions and 88 deletions

View File

@@ -26,7 +26,7 @@ FROM node:20-alpine
# Getrennte RUNs halten einzelne Registry-Layer klein genug fuer Proxy-Limits. # Getrennte RUNs halten einzelne Registry-Layer klein genug fuer Proxy-Limits.
RUN apk add --no-cache docker-cli RUN apk add --no-cache docker-cli
RUN apk add --no-cache imagemagick RUN apk add --no-cache imagemagick libjpeg-turbo libpng libwebp
RUN apk add --no-cache sqlite-libs RUN apk add --no-cache sqlite-libs
WORKDIR /app WORKDIR /app

View File

@@ -7,8 +7,6 @@
<link rel="stylesheet" href="/assets/css/main.min.css" /> <link rel="stylesheet" href="/assets/css/main.min.css" />
<style> <style>
.admin-section { background: rgba(0,0,0,0.5); padding: 2em; margin-bottom: 2em; border-radius: 4px; } .admin-section { background: rgba(0,0,0,0.5); padding: 2em; margin-bottom: 2em; border-radius: 4px; }
.folder-check { display: inline-block; margin: 5px 10px; padding: 8px 12px; background: rgba(255,255,255,0.1); border-radius: 4px; }
.folder-check label { cursor: pointer; }
table { width: 100%; border-collapse: collapse; } table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 10px; border-bottom: 1px solid rgba(255,255,255,0.2); } th, td { text-align: left; padding: 10px; border-bottom: 1px solid rgba(255,255,255,0.2); }
.badge { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 0.8em; } .badge { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 0.8em; }
@@ -17,6 +15,14 @@
.badge-client { background: #4CAF50; } .badge-client { background: #4CAF50; }
.badge-expired { background: #666; } .badge-expired { background: #666; }
.btn-small { padding: 4px 10px; font-size: 0.85em; margin: 2px; cursor: pointer; } .btn-small { padding: 4px 10px; font-size: 0.85em; margin: 2px; cursor: pointer; }
.folder-row { cursor: pointer; transition: background 0.2s; }
.folder-row:hover { background: rgba(255,255,255,0.1); }
.folder-row.assigned { background: rgba(76,175,80,0.25); }
.folder-row.assigned:hover { background: rgba(76,175,80,0.35); }
.folder-status { font-weight: bold; }
.folder-status.yes { color: #4CAF50; }
.folder-status.no { color: #888; }
#folder-table-wrap { display: none; margin-top: 1em; }
</style> </style>
</head> </head>
<body class="is-preload"> <body class="is-preload">
@@ -65,13 +71,17 @@
<div class="admin-section"> <div class="admin-section">
<h2>Ordner-Zuweisung</h2> <h2>Ordner-Zuweisung</h2>
<p>Weise Unterordner aus dem Websitebilder-Verzeichnis einem Nutzer zu.</p> <p>Waehle einen Nutzer und klicke auf Ordner um sie zuzuweisen oder zu entfernen.</p>
<select id="user-select" style="margin-bottom: 1em;"> <select id="user-select">
<option value="">Nutzer waehlen...</option> <option value="">Nutzer waehlen...</option>
</select> </select>
<div id="folder-checkboxes" style="display:flex; flex-wrap:wrap; gap:5px; margin-bottom:1em;"></div> <div id="folder-table-wrap">
<button id="save-assignments">Zuweisung Speichern</button> <table id="folder-table">
<span id="assign-status" style="margin-left:10px;"></span> <thead><tr><th><i class="fa fa-folder"></i> Ordner</th><th style="width:120px; text-align:center;">Status</th></tr></thead>
<tbody id="folder-tbody"></tbody>
</table>
<span id="assign-status" style="display:inline-block; margin-top:10px;"></span>
</div>
</div> </div>
<div class="admin-section"> <div class="admin-section">

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;top:0;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;bottom:.6em;right:.6em;width:1.6em;height:1.6em;border:2px solid rgba(255,255,255,.8);border-radius:4px;background:rgba(0,0,0,.5);z-index:2;transition:all .2s ease;pointer-events:none}body.selection-mode #main .thumb .image{opacity:.85;transition:opacity .2s ease}body.selection-mode #main .thumb:active .image{opacity:.6}body.selection-mode #main .thumb.selected::before{background:#34a58e;border-color:#34a58e}body.selection-mode #main .thumb.selected .image{opacity:1;outline:3px solid #34a58e;outline-offset:-3px}@media screen and (max-width: 736px){#selection-toolbar{top:4em;padding:.6em 1em;gap:.6em}#selection-toolbar #sel-count{font-size:.9em}#selection-toolbar button{font-size:.85em;padding:.5em 1em}body.selection-mode #main .thumb::before{bottom:.3em;right:.3em;width:2em;height:2em;border-width:2.5px}body.selection-mode #main .thumb.selected .image{outline-width:4px}}#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

@@ -26,12 +26,18 @@ $(document).ready(function() {
$('#users-tbody').empty(); $('#users-tbody').empty();
$('#user-select').html('<option value="">Nutzer waehlen...</option>'); $('#user-select').html('<option value="">Nutzer waehlen...</option>');
if (!users || users.length === 0) {
$('#users-tbody').html('<tr><td colspan="5" style="text-align:center;">Keine Nutzer gefunden</td></tr>');
return;
}
users.forEach(function(u) { users.forEach(function(u) {
var badge = $('<span></span>').addClass('badge badge-' + u.role).text(u.role); var role = u.role || 'unbekannt';
var badge = $('<span></span>').addClass('badge badge-' + role).text(role);
var created = u.created_at ? new Date(u.created_at).toLocaleDateString('de-DE') : '-'; var created = u.created_at ? new Date(u.created_at).toLocaleDateString('de-DE') : '-';
var info = created; var info = created;
if (u.role === 'client' && u.expires_at) { if (role === 'client' && u.expires_at) {
var expDate = new Date(u.expires_at).toLocaleDateString('de-DE'); var expDate = new Date(u.expires_at).toLocaleDateString('de-DE');
if (u.expired) { if (u.expired) {
badge = $('<span></span>').addClass('badge badge-expired').text('abgelaufen'); badge = $('<span></span>').addClass('badge badge-expired').text('abgelaufen');
@@ -55,32 +61,47 @@ $(document).ready(function() {
row.append($('<td></td>').text(u.id)); row.append($('<td></td>').text(u.id));
row.append(userCell); row.append(userCell);
row.append($('<td></td>').text(u.role)); row.append($('<td></td>').text(role));
row.append($('<td></td>').text(info)); row.append($('<td></td>').text(info));
row.append(actions); row.append(actions);
$('#users-tbody').append(row); $('#users-tbody').append(row);
// Dropdown fuer Ordner-Zuweisung (nur non-admin) // Dropdown fuer Ordner-Zuweisung (nur non-admin)
if (u.role !== 'admin') { if (role !== 'admin') {
$('#user-select').append($('<option></option>').val(u.id).text(u.username + ' (' + u.role + ')')); $('#user-select').append($('<option></option>').val(u.id).text(u.username + ' (' + role + ')'));
} }
}); });
}).fail(function(xhr) {
var msg = 'HTTP ' + xhr.status + ': ' + xhr.statusText;
$('#users-tbody').html('<tr><td colspan="5" style="color:#e44d26;">Fehler beim Laden: ' + msg + '</td></tr>');
}); });
} }
// Nutzer erstellen // Nutzer erstellen
$('#create-user-form').submit(function(e) { $('#create-user-form').submit(function(e) {
e.preventDefault(); e.preventDefault();
$.post('/api/admin/users', { var username = $('#new-username').val().trim();
username: $('#new-username').val(), var password = $('#new-password').val();
password: $('#new-password').val(), var role = $('#new-role').val();
role: $('#new-role').val() if (!username || !password) {
}).done(function() { alert('Bitte Username und Passwort eingeben.');
return;
}
$.ajax({
url: '/api/admin/users',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ username: username, password: password, role: role })
}).done(function(data) {
alert('Nutzer "' + username + '" erfolgreich erstellt!');
$('#new-username').val(''); $('#new-username').val('');
$('#new-password').val(''); $('#new-password').val('');
loadUsers(); loadUsers();
}).fail(function(err) { }).fail(function(xhr) {
alert('Fehler: ' + (err.responseJSON ? err.responseJSON.error : 'Unbekannt')); var msg = 'Unbekannter Fehler';
if (xhr.responseJSON && xhr.responseJSON.error) msg = xhr.responseJSON.error;
else if (xhr.status) msg = 'HTTP ' + xhr.status + ': ' + xhr.statusText;
alert('Fehler beim Erstellen: ' + msg);
}); });
}); });
@@ -119,54 +140,89 @@ $(document).ready(function() {
// ==================== ORDNER-ZUWEISUNG ==================== // ==================== ORDNER-ZUWEISUNG ====================
var allFolders = [];
var assignedFolders = [];
function loadFolders() { function loadFolders() {
$.get('/api/admin/folders').done(function(folders) { $.get('/api/admin/folders').done(function(folders) {
$('#folder-checkboxes').empty(); allFolders = folders || [];
if (folders.length === 0) { renderFolderTable();
$('#folder-checkboxes').html('<p>Keine Unterordner in Websitebilder gefunden. Erstelle Ordner in Nextcloud.</p>');
return;
}
folders.forEach(function(f) {
var wrapper = $('<div class="folder-check"></div>');
var label = $('<label></label>');
$('<input type="checkbox" class="folder-cb"/>').val(f).appendTo(label);
label.append(' ');
$('<i class="fa fa-folder"></i>').appendTo(label);
label.append(' ' + f);
wrapper.append(label);
$('#folder-checkboxes').append(wrapper);
});
}); });
} }
// Bei User-Auswahl: zugewiesene Ordner laden function renderFolderTable() {
var $tbody = $('#folder-tbody');
$tbody.empty();
if (allFolders.length === 0) {
$tbody.html('<tr><td colspan="2" style="text-align:center;">Keine Unterordner gefunden. Erstelle Ordner in Nextcloud.</td></tr>');
return;
}
allFolders.forEach(function(f) {
var isAssigned = assignedFolders.indexOf(f) !== -1;
var cls = isAssigned ? 'folder-row assigned' : 'folder-row';
var statusCls = isAssigned ? 'folder-status yes' : 'folder-status no';
var statusText = isAssigned ? 'Zugewiesen' : '—';
$tbody.append(
'<tr class="' + cls + '" data-folder="' + f + '">' +
'<td><i class="fa fa-folder"></i> ' + f + '</td>' +
'<td style="text-align:center;"><span class="' + statusCls + '">' + statusText + '</span></td>' +
'</tr>'
);
});
}
// Bei User-Auswahl: zugewiesene Ordner laden und Tabelle anzeigen
$('#user-select').change(function() { $('#user-select').change(function() {
var uid = $(this).val(); var uid = $(this).val();
$('.folder-cb').prop('checked', false); if (!uid) {
if (!uid) return; $('#folder-table-wrap').hide();
$.get('/api/admin/assign?user_id=' + uid).done(function(assignedFolders) { assignedFolders = [];
assignedFolders.forEach(function(f) { return;
$('.folder-cb').filter(function() { }
return $(this).val() === f; // Ordner neu laden falls noch leer, dann Zuweisung anzeigen
}).prop('checked', true); var showTable = function() {
$.get('/api/admin/assign?user_id=' + uid).done(function(folders) {
assignedFolders = folders || [];
renderFolderTable();
$('#folder-table-wrap').show();
}).fail(function() {
assignedFolders = [];
renderFolderTable();
$('#folder-table-wrap').show();
}); });
}); };
if (allFolders.length === 0) {
$.get('/api/admin/folders').done(function(folders) {
allFolders = folders || [];
showTable();
}).fail(function() {
allFolders = [];
showTable();
});
} else {
showTable();
}
}); });
// Zuweisung speichern // Klick auf Ordner-Zeile: Toggle + sofort speichern
$('#save-assignments').click(function() { $(document).on('click', '.folder-row', function() {
var uid = $('#user-select').val(); var uid = $('#user-select').val();
if (!uid) return alert('Bitte Nutzer waehlen'); if (!uid) return;
var selected = []; var folder = $(this).data('folder');
$('.folder-cb:checked').each(function() { selected.push($(this).val()); }); var idx = assignedFolders.indexOf(folder);
if (idx !== -1) {
assignedFolders.splice(idx, 1);
} else {
assignedFolders.push(folder);
}
renderFolderTable();
$.ajax({ $.ajax({
url: '/api/admin/assign', url: '/api/admin/assign',
type: 'POST', type: 'POST',
contentType: 'application/json', contentType: 'application/json',
data: JSON.stringify({ user_id: uid, folders: selected }) data: JSON.stringify({ user_id: uid, folders: assignedFolders })
}).done(function() { }).done(function() {
$('#assign-status').text('Gespeichert!').fadeIn().delay(2000).fadeOut(); $('#assign-status').text('Gespeichert!').show().delay(1500).fadeOut();
}).fail(function() { }).fail(function() {
alert('Fehler beim Speichern'); alert('Fehler beim Speichern');
}); });

View File

@@ -1 +1 @@
$(document).ready(function(){function e(){$.get("/api/admin/users").done(function(e){$("#users-tbody").empty(),$("#user-select").html('<option value="">Nutzer waehlen...</option>'),e.forEach(function(e){var n='<span class="badge badge-'+e.role+'">'+e.role+"</span>",a=e.created_at?new Date(e.created_at).toLocaleDateString("de-DE"):"-";if("client"===e.role&&e.expires_at){var t=new Date(e.expires_at).toLocaleDateString("de-DE");e.expired?(n='<span class="badge badge-expired">abgelaufen</span>',a="Abgelaufen: "+t):a="Bis: "+t}var r='<button class="btn-small" onclick="changePassword('+e.id+", '"+e.username.replace(/'/g,"\\'")+"')\">PW</button>";r+='<button class="btn-small" onclick="deleteUser('+e.id+", '"+e.username.replace(/'/g,"\\'")+"')\">X</button>",$("#users-tbody").append("<tr><td>"+e.id+"</td><td>"+e.username+" "+n+"</td><td>"+e.role+"</td><td>"+a+"</td><td>"+r+"</td></tr>"),"admin"!==e.role&&$("#user-select").append('<option value="'+e.id+'">'+e.username+" ("+e.role+")</option>")})})}$.get("/api/auth/me").done(function(n){"admin"!==n.role?(alert("Kein Zugriff!"),window.location.href="/"):(e(),$.get("/api/admin/folders").done(function(e){$("#folder-checkboxes").empty(),0!==e.length?e.forEach(function(e){$("#folder-checkboxes").append('<div class="folder-check"><label><input type="checkbox" value="'+e+'" class="folder-cb"/> <i class="fa fa-folder"></i> '+e+"</label></div>")}):$("#folder-checkboxes").html("<p>Keine Unterordner in Websitebilder gefunden. Erstelle Ordner in Nextcloud.</p>")}),$("body").removeClass("is-preload"))}).fail(function(){window.location.href="/"}),$("#logout-btn").click(function(e){e.preventDefault(),$.post("/api/auth/logout").done(function(){window.location.href="/"})}),$("#create-user-form").submit(function(n){n.preventDefault(),$.post("/api/admin/users",{username:$("#new-username").val(),password:$("#new-password").val(),role:$("#new-role").val()}).done(function(){$("#new-username").val(""),$("#new-password").val(""),e()}).fail(function(e){alert("Fehler: "+(e.responseJSON?e.responseJSON.error:"Unbekannt"))})}),window.deleteUser=function(n,a){confirm('Nutzer "'+a+'" wirklich loeschen?')&&$.ajax({url:"/api/admin/users/"+n,type:"DELETE"}).done(e).fail(function(e){alert(e.responseJSON?e.responseJSON.error:"Fehler")})},window.changePassword=function(e,n){var a=prompt('Neues Passwort fuer "'+n+'":');a&&$.ajax({url:"/api/admin/users/"+e+"/password",type:"PUT",contentType:"application/json",data:JSON.stringify({new_password:a})}).done(function(){alert("Passwort geaendert!")}).fail(function(e){alert(e.responseJSON?e.responseJSON.error:"Fehler")})},$("#user-select").change(function(){var e=$(this).val();$(".folder-cb").prop("checked",!1),e&&$.get("/api/admin/assign?user_id="+e).done(function(e){e.forEach(function(e){$('.folder-cb[value="'+e+'"]').prop("checked",!0)})})}),$("#save-assignments").click(function(){var e=$("#user-select").val();if(!e)return alert("Bitte Nutzer waehlen");var n=[];$(".folder-cb:checked").each(function(){n.push($(this).val())}),$.ajax({url:"/api/admin/assign",type:"POST",contentType:"application/json",data:JSON.stringify({user_id:e,folders:n})}).done(function(){$("#assign-status").text("Gespeichert!").fadeIn().delay(2e3).fadeOut()}).fail(function(){alert("Fehler beim Speichern")})}),$("#admin-pw-form").submit(function(e){e.preventDefault(),$.post("/api/auth/change-password",{old_password:$("#admin-pw-old").val(),new_password:$("#admin-pw-new").val()}).done(function(){alert("Passwort geaendert!"),$("#admin-pw-old, #admin-pw-new").val("")}).fail(function(e){alert(e.responseJSON?e.responseJSON.error:"Fehler")})})}); $(document).ready(function(){function e(){$.get("/api/admin/users").done(function(e){$("#users-tbody").empty(),$("#user-select").html('<option value="">Nutzer waehlen...</option>'),e&&0!==e.length?e.forEach(function(e){var t=e.role||"unbekannt",n='<span class="badge badge-'+t+'">'+t+"</span>",a=e.created_at?new Date(e.created_at).toLocaleDateString("de-DE"):"-";if("client"===t&&e.expires_at){var r=new Date(e.expires_at).toLocaleDateString("de-DE");e.expired?(n='<span class="badge badge-expired">abgelaufen</span>',a="Abgelaufen: "+r):a="Bis: "+r}var o='<button class="btn-small" onclick="changePassword('+e.id+", '"+e.username.replace(/'/g,"\\'")+"')\">PW</button>";o+='<button class="btn-small" onclick="deleteUser('+e.id+", '"+e.username.replace(/'/g,"\\'")+"')\">X</button>",$("#users-tbody").append("<tr><td>"+e.id+"</td><td>"+e.username+" "+n+"</td><td>"+t+"</td><td>"+a+"</td><td>"+o+"</td></tr>"),"admin"!==t&&$("#user-select").append('<option value="'+e.id+'">'+e.username+" ("+t+")</option>")}):$("#users-tbody").html('<tr><td colspan="5" style="text-align:center;">Keine Nutzer gefunden</td></tr>')}).fail(function(e){var t="HTTP "+e.status+": "+e.statusText;$("#users-tbody").html('<tr><td colspan="5" style="color:#e44d26;">Fehler beim Laden: '+t+"</td></tr>")})}$.get("/api/auth/me").done(function(n){"admin"!==n.role?(alert("Kein Zugriff!"),window.location.href="/"):(e(),$.get("/api/admin/folders").done(function(e){t=e}),$("body").removeClass("is-preload"))}).fail(function(){window.location.href="/"}),$("#logout-btn").click(function(e){e.preventDefault(),$.post("/api/auth/logout").done(function(){window.location.href="/"})}),$("#create-user-form").submit(function(t){t.preventDefault();var n=$("#new-username").val().trim(),a=$("#new-password").val(),r=$("#new-role").val();n&&a?$.ajax({url:"/api/admin/users",type:"POST",contentType:"application/json",data:JSON.stringify({username:n,password:a,role:r})}).done(function(t){alert('Nutzer "'+n+'" erfolgreich erstellt!'),$("#new-username").val(""),$("#new-password").val(""),e()}).fail(function(e){var t="Unbekannter Fehler";e.responseJSON&&e.responseJSON.error?t=e.responseJSON.error:e.status&&(t="HTTP "+e.status+": "+e.statusText),alert("Fehler beim Erstellen: "+t)}):alert("Bitte Username und Passwort eingeben.")}),window.deleteUser=function(t,n){confirm('Nutzer "'+n+'" wirklich loeschen?')&&$.ajax({url:"/api/admin/users/"+t,type:"DELETE"}).done(e).fail(function(e){alert(e.responseJSON?e.responseJSON.error:"Fehler")})},window.changePassword=function(e,t){var n=prompt('Neues Passwort fuer "'+t+'":');n&&$.ajax({url:"/api/admin/users/"+e+"/password",type:"PUT",contentType:"application/json",data:JSON.stringify({new_password:n})}).done(function(){alert("Passwort geaendert!")}).fail(function(e){alert(e.responseJSON?e.responseJSON.error:"Fehler")})};var t=[],n=[];function a(){var e=$("#folder-tbody");e.empty(),0!==t.length?t.forEach(function(t){var a=-1!==n.indexOf(t),r=a?"folder-row assigned":"folder-row",o=a?"folder-status yes":"folder-status no",s=a?"Zugewiesen":"—";e.append('<tr class="'+r+'" data-folder="'+t+'"><td><i class="fa fa-folder"></i> '+t+'</td><td style="text-align:center;"><span class="'+o+'">'+s+"</span></td></tr>")}):e.html('<tr><td colspan="2" style="text-align:center;">Keine Unterordner gefunden. Erstelle Ordner in Nextcloud.</td></tr>')}$("#user-select").change(function(){var e=$(this).val();if(!e)return $("#folder-table-wrap").hide(),void(n=[]);var r=function(){$.get("/api/admin/assign?user_id="+e).done(function(e){n=e||[],a(),$("#folder-table-wrap").show()}).fail(function(){n=[],a(),$("#folder-table-wrap").show()})};0===t.length?$.get("/api/admin/folders").done(function(e){t=e||[],r()}).fail(function(){t=[],r()}):r()}),$(document).on("click",".folder-row",function(){var e=$("#user-select").val();if(e){var t=$(this).data("folder"),r=n.indexOf(t);-1!==r?n.splice(r,1):n.push(t),a(),$.ajax({url:"/api/admin/assign",type:"POST",contentType:"application/json",data:JSON.stringify({user_id:e,folders:n})}).done(function(){$("#assign-status").text("Gespeichert!").show().delay(1500).fadeOut()}).fail(function(){alert("Fehler beim Speichern")})}}),$("#admin-pw-form").submit(function(e){e.preventDefault(),$.post("/api/auth/change-password",{old_password:$("#admin-pw-old").val(),new_password:$("#admin-pw-new").val()}).done(function(){alert("Passwort geaendert!"),$("#admin-pw-old, #admin-pw-new").val("")}).fail(function(e){alert(e.responseJSON?e.responseJSON.error:"Fehler")})})});

View File

@@ -182,6 +182,8 @@
images.forEach(function(file) { images.forEach(function(file) {
var article = $('<article class="thumb"></article>'); var article = $('<article class="thumb"></article>');
article.attr('data-name', file.name);
if (file.folder) article.attr('data-folder', file.folder);
var prefix = file.folder ? encodeURIComponent(file.folder) + '/' : ''; var prefix = file.folder ? encodeURIComponent(file.folder) + '/' : '';
var fullPath = '/images/fulls/' + prefix + encodeURIComponent(file.name); var fullPath = '/images/fulls/' + prefix + encodeURIComponent(file.name);
var thumbPath = '/images/thumbs/' + prefix + encodeURIComponent(file.name); var thumbPath = '/images/thumbs/' + prefix + encodeURIComponent(file.name);
@@ -193,6 +195,7 @@
}); });
initThumbs(); initThumbs();
initSelectionMode(images);
initPoptrox(); initPoptrox();
}, },
error: function() { error: function() {
@@ -214,6 +217,14 @@
if ((x = $image_img.data("position"))) $image.css("background-position", x); if ((x = $image_img.data("position"))) $image.css("background-position", x);
$image_img.hide(); $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() { $image_img[0].addEventListener("load", function() {
EXIF.getData($image_img[0], function () { EXIF.getData($image_img[0], function () {
exifDatas[$image_img.data('name')] = getExifDataMarkup(this); exifDatas[$image_img.data('name')] = getExifDataMarkup(this);
@@ -282,6 +293,194 @@
return template; 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
// Capture-Phase auf document: faengt JEDEN Klick ab bevor Poptrox ihn sieht
document.addEventListener('click', function(e) {
if (!selectionActive) return;
var thumb = e.target.closest('#main .thumb');
if (!thumb) return;
e.preventDefault();
e.stopImmediatePropagation();
var $thumb = $(thumb);
var imgName = $thumb.attr('data-name');
var folder = $thumb.attr('data-folder') || null;
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();
}, true);
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 (direkt gebunden, nicht delegiert)
$toolbar.on('click', '#sel-download', function() {
if (selectedImages.length === 0) return;
var payload = JSON.stringify({ images: selectedImages });
if (selectedImages.length <= 3) {
// 1-3 Bilder: normales AJAX, Server gibt JSON mit URLs
$.ajax({
url: '/api/download/selected',
type: 'POST',
contentType: 'application/json',
data: payload,
success: function(resp) {
if (resp.mode === 'individual') {
resp.urls.forEach(function(url, i) {
setTimeout(function() {
var a = document.createElement('a');
a.href = url;
a.download = url.split('/').pop();
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}, i * 500);
});
}
},
error: function(xhr) {
var msg = (xhr.responseJSON && xhr.responseJSON.error) || 'Download fehlgeschlagen';
alert(msg);
}
});
} else {
// 4+ Bilder: Blob-Download fuer ZIP
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/download/selected', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.responseType = 'blob';
xhr.onload = function() {
if (xhr.status === 200) {
var url = window.URL.createObjectURL(xhr.response);
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);
} else {
// Fehler-Blob als Text lesen
var reader = new FileReader();
reader.onload = function() {
try {
var resp = JSON.parse(this.result);
alert(resp.error || 'Download fehlgeschlagen');
} catch(e) {
alert('Download fehlgeschlagen (Status ' + xhr.status + ')');
}
};
reader.readAsText(xhr.response);
}
};
xhr.onerror = function() {
alert('Netzwerkfehler beim Download');
};
xhr.send(payload);
}
});
// Abbrechen-Button (direkt gebunden)
$toolbar.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 // Galerie laden
loadGallery(); loadGallery();

File diff suppressed because one or more lines are too long

View File

@@ -6,6 +6,116 @@
color: _palette(fg); color: _palette(fg);
} }
/* ==================== Selection Mode ==================== */
#selection-toolbar {
position: fixed;
top: 0;
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;
// Checkbox unten-rechts (wird nicht von Toolbar ueberlagert)
&::before {
content: '';
position: absolute;
bottom: 0.6em;
right: 0.6em;
width: 1.6em;
height: 1.6em;
border: 2px solid rgba(255, 255, 255, 0.8);
border-radius: 4px;
background: rgba(0, 0, 0, 0.5);
z-index: 2;
transition: all 0.2s ease;
pointer-events: none;
}
// Leichtes Overlay damit klar ist: gesamtes Bild anklickbar
.image {
opacity: 0.85;
transition: opacity 0.2s ease;
}
&:active .image {
opacity: 0.6;
}
&.selected {
&::before {
background: _palette(accent1);
border-color: _palette(accent1);
}
.image {
opacity: 1;
outline: 3px solid _palette(accent1);
outline-offset: -3px;
}
}
}
}
@media screen and (max-width: 736px) {
#selection-toolbar {
top: _size(header);
padding: 0.6em 1em;
gap: 0.6em;
#sel-count {
font-size: 0.9em;
}
button {
font-size: 0.85em;
padding: 0.5em 1em;
}
}
body.selection-mode #main .thumb {
&::before {
bottom: 0.3em;
right: 0.3em;
width: 2em;
height: 2em;
border-width: 2.5px;
}
&.selected .image {
outline-width: 4px;
}
}
}
/* Footer */ /* Footer */
#footer { #footer {
.presentify { .presentify {

View File

@@ -110,15 +110,25 @@
} }
h1 { h1 {
font-size: 0.9em; font-size: 0.8em;
max-width: 40%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
nav { nav {
> ul { > ul {
> li { > li {
a { a {
font-size: 0.9em; font-size: 0;
padding: 0 1.15em; padding: 0 0.9em;
line-height: _size(header);
i {
font-size: 1.3rem;
vertical-align: middle;
}
} }
} }
} }

View File

@@ -27,10 +27,10 @@
minHeight: 18em minHeight: 18em
), ),
xsmall: ( xsmall: (
rows: 2, rows: 3,
columns: 1, columns: 2,
pad: 0.5, pad: 0.5,
minHeight: 18em minHeight: 12em
) )
) )
); );

View File

@@ -17,7 +17,15 @@ db.serialize(() => {
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`); )`);
// Migration: created_at hinzufuegen falls Tabelle schon existiert // Migration: Spalten hinzufuegen falls Tabelle schon existiert (alte DB ohne role/created_at)
db.run(`ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'client'`, (err) => {
// Ignoriere Fehler wenn Spalte schon existiert
if (!err) {
// Alte Admins (die einzigen User vor dem Update) als admin markieren
db.run(`UPDATE users SET role = 'admin' WHERE id = 1`);
console.log('Migration: role-Spalte hinzugefuegt, User ID 1 als admin gesetzt');
}
});
db.run(`ALTER TABLE users ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP`, (err) => { db.run(`ALTER TABLE users ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP`, (err) => {
// Ignoriere Fehler wenn Spalte schon existiert // Ignoriere Fehler wenn Spalte schon existiert
}); });

View File

@@ -5,6 +5,7 @@ const bcrypt = require('bcryptjs');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { execFile } = require('child_process'); const { execFile } = require('child_process');
const archiver = require('archiver');
const db = require('./database'); const db = require('./database');
const app = express(); const app = express();
@@ -19,7 +20,7 @@ const DEV_FULLS_DIR = path.join(__dirname, '../images/fulls');
const DEV_THUMBS_DIR = path.join(__dirname, '../images/thumbs'); const DEV_THUMBS_DIR = path.join(__dirname, '../images/thumbs');
const FULLS_DIR = process.env.FULLS_DIR || (fs.existsSync(DEFAULT_FULLS_DIR) ? DEFAULT_FULLS_DIR : DEV_FULLS_DIR); const FULLS_DIR = process.env.FULLS_DIR || (fs.existsSync(DEFAULT_FULLS_DIR) ? DEFAULT_FULLS_DIR : DEV_FULLS_DIR);
const THUMBS_DIR = process.env.THUMBS_DIR || (fs.existsSync(DEFAULT_THUMBS_DIR) ? DEFAULT_THUMBS_DIR : DEV_THUMBS_DIR); const THUMBS_DIR = process.env.THUMBS_DIR || (fs.existsSync(DEFAULT_THUMBS_DIR) ? DEFAULT_THUMBS_DIR : DEV_THUMBS_DIR);
const SYNC_IMAGE = process.env.SYNC_IMAGE || 'ghcr.io/kroonk/photography:latest'; const SYNC_IMAGE = process.env.SYNC_IMAGE || 'git.mischlabs.de/mrdiderot/photography:latest';
const SYNC_CONTAINER = process.env.SYNC_CONTAINER || 'photography-website'; const SYNC_CONTAINER = process.env.SYNC_CONTAINER || 'photography-website';
const IMAGE_RE = /\.(jpg|jpeg|png|gif|webp)$/i; const IMAGE_RE = /\.(jpg|jpeg|png|gif|webp)$/i;
let syncInProgress = false; let syncInProgress = false;
@@ -235,7 +236,14 @@ app.get('/images/fulls/', (req, res) => {
app.get('/api/admin/users', requireAdmin, (req, res) => { app.get('/api/admin/users', requireAdmin, (req, res) => {
db.all('SELECT id, username, role, created_at FROM users', (err, rows) => { db.all('SELECT id, username, role, created_at FROM users', (err, rows) => {
if (err) return res.json([]); if (err) {
console.error('User-Abfrage fehlgeschlagen:', err.message);
// Fallback: einfache Abfrage ohne neue Spalten
return db.all('SELECT * FROM users', (err2, rows2) => {
if (err2) return res.status(500).json({ error: 'DB Error: ' + err2.message });
res.json(rows2.map(u => ({ id: u.id, username: u.username, role: u.role || 'client', created_at: u.created_at || null })));
});
}
// Ablaufdatum berechnen fuer Clients // Ablaufdatum berechnen fuer Clients
const result = rows.map(u => { const result = rows.map(u => {
const out = { ...u }; const out = { ...u };
@@ -354,6 +362,121 @@ app.post('/api/admin/sync', requireAdmin, (req, res) => {
}); });
}); });
// ==================== DOWNLOAD API ====================
function isValidName(name) {
return name && !name.includes('/') && !name.includes('\\') && !name.includes('..');
}
app.post('/api/download/selected', (req, res) => {
const { images } = req.body;
if (!Array.isArray(images) || images.length === 0) {
return res.status(400).json({ error: 'Keine Bilder ausgewaehlt' });
}
// Alle Bilder validieren und Zugriff pruefen
let pending = images.length;
const verified = [];
let responded = false;
images.forEach((img, idx) => {
if (responded) return;
const name = img.name;
const folder = img.folder || null;
// Dateiname + Ordnername validieren
if (!isValidName(name) || !IMAGE_RE.test(name) || (folder && !isValidName(folder))) {
responded = true;
return res.status(400).json({ error: 'Ungueltiger Dateiname' });
}
const checkAccess = (ok) => {
if (responded) return;
if (!ok) {
responded = true;
return res.status(403).json({ error: 'Kein Zugriff auf: ' + name });
}
verified[idx] = img;
pending--;
if (pending === 0) sendFiles();
};
if (!folder) {
// Root-Bild: fuer alle zugaenglich
const filePath = path.join(FULLS_DIR, name);
checkAccess(fs.existsSync(filePath));
} else {
// Ordner-Bild: nur wenn User den Ordner zugewiesen hat
if (!req.user) return checkAccess(false);
db.get('SELECT 1 FROM user_folders WHERE user_id = ? AND folder_name = ?',
[req.user.id, folder], (err, row) => {
if (err || !row) return checkAccess(false);
const filePath = path.join(FULLS_DIR, folder, name);
checkAccess(fs.existsSync(filePath));
});
}
});
function sendFiles() {
if (verified.length <= 3) {
// Einzelne Downloads: URLs zurueckgeben
const urls = verified.map(img => {
const prefix = img.folder ? encodeURIComponent(img.folder) + '/' : '';
return '/images/fulls/' + prefix + encodeURIComponent(img.name);
});
return res.json({ mode: 'individual', urls });
}
// 4+ Bilder: ZIP streamen
res.setHeader('Content-Type', 'application/zip');
res.setHeader('Content-Disposition', 'attachment; filename="mischkomposition-auswahl.zip"');
const archive = archiver('zip', { zlib: { level: 5 } });
archive.on('error', () => {
if (!res.headersSent) res.status(500).json({ error: 'ZIP-Fehler' });
});
archive.pipe(res);
verified.forEach(img => {
const filePath = img.folder
? path.join(FULLS_DIR, img.folder, img.name)
: path.join(FULLS_DIR, img.name);
archive.file(filePath, { name: img.name });
});
archive.finalize();
}
});
app.get('/api/download/folder/:folderName', (req, res) => {
const folderName = req.params.folderName;
if (!isValidName(folderName)) {
return res.status(400).json({ error: 'Ungueltiger Ordnername' });
}
if (!req.user) {
return res.status(401).json({ error: 'Nicht eingeloggt' });
}
db.get('SELECT 1 FROM user_folders WHERE user_id = ? AND folder_name = ?',
[req.user.id, folderName], (err, row) => {
if (err || !row) {
return res.status(403).json({ error: 'Kein Zugriff auf diesen Ordner' });
}
const folderPath = path.join(FULLS_DIR, folderName);
const images = listImagesInDir(folderPath);
if (images.length === 0) {
return res.status(404).json({ error: 'Keine Bilder im Ordner' });
}
res.setHeader('Content-Type', 'application/zip');
res.setHeader('Content-Disposition', 'attachment; filename="' + encodeURIComponent(folderName) + '.zip"');
const archive = archiver('zip', { zlib: { level: 5 } });
archive.on('error', () => {
if (!res.headersSent) res.status(500).json({ error: 'ZIP-Fehler' });
});
archive.pipe(res);
images.forEach(name => {
archive.file(path.join(folderPath, name), { name });
});
archive.finalize();
});
});
// ==================== STATIC FILES ==================== // ==================== STATIC FILES ====================
app.get(/^\/images\/(fulls|thumbs)\/(.+)$/, serveProtectedImage); app.get(/^\/images\/(fulls|thumbs)\/(.+)$/, serveProtectedImage);

View File

@@ -3,10 +3,21 @@
FULLS_DIR="/app/public/images/fulls" FULLS_DIR="/app/public/images/fulls"
THUMBS_DIR="/app/public/images/thumbs" THUMBS_DIR="/app/public/images/thumbs"
# ImageMagick 7 (Alpine) nutzt "magick" statt "convert"
if command -v magick >/dev/null 2>&1; then
IM_CMD="magick"
elif command -v convert >/dev/null 2>&1; then
IM_CMD="convert"
else
echo "FEHLER: Weder magick noch convert gefunden! Thumbnails koennen nicht erzeugt werden."
IM_CMD=""
fi
# Thumbnails fuer einen Quell- und Zielordner generieren # Thumbnails fuer einen Quell- und Zielordner generieren
generate_thumbs_for_dir() { generate_thumbs_for_dir() {
src_dir="$1" src_dir="$1"
dst_dir="$2" dst_dir="$2"
[ -z "$IM_CMD" ] && return
mkdir -p "$dst_dir" mkdir -p "$dst_dir"
for img in "$src_dir"/*; do for img in "$src_dir"/*; do
[ -f "$img" ] || continue [ -f "$img" ] || continue
@@ -21,7 +32,10 @@ generate_thumbs_for_dir() {
if [ ! -f "$dst_dir/$filename" ] || [ "$img" -nt "$dst_dir/$filename" ]; then if [ ! -f "$dst_dir/$filename" ] || [ "$img" -nt "$dst_dir/$filename" ]; then
echo "Thumbnail erstellen: $dst_dir/$filename" echo "Thumbnail erstellen: $dst_dir/$filename"
convert "$img" -resize 512x512 -quality 80 "$dst_dir/$filename" 2>/dev/null $IM_CMD "$img" -resize 512x512 -quality 80 "$dst_dir/$filename"
if [ $? -ne 0 ]; then
echo "WARNUNG: Thumbnail fehlgeschlagen fuer $filename"
fi
fi fi
done done
} }

View File

@@ -11,7 +11,6 @@ layout: default
<nav> <nav>
<ul> <ul>
<li><a href="#" id="theme-toggle" title="Theme wechseln"><i class="fa fa-sun"></i></a></li> <li><a href="#" id="theme-toggle" title="Theme wechseln"><i class="fa fa-sun"></i></a></li>
<li><a href="#galerie" id="galerie-link">Galerie</a></li>
<li><a href="#login-panel" id="login-nav">Login&nbsp;&nbsp;<i class="fa fa-user"></i></a></li> <li><a href="#login-panel" id="login-nav">Login&nbsp;&nbsp;<i class="fa fa-user"></i></a></li>
<li><a href="#footer">About&nbsp;&nbsp;<i class="fa fa-info-circle text-fg"></i></a></li> <li><a href="#footer">About&nbsp;&nbsp;<i class="fa fa-info-circle text-fg"></i></a></li>
</ul> </ul>

1190
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,7 @@
}, },
"homepage": "https://github.com/Kroonk/Photography#readme", "homepage": "https://github.com/Kroonk/Photography#readme",
"dependencies": { "dependencies": {
"archiver": "^7.0.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"express": "^4.19.2", "express": "^4.19.2",