Spaces:
Runtime error
Runtime error
{% extends "base.html" %} | |
{% block title %}Flux Bildarchiv{% endblock %} | |
{% block content %} | |
<div class="container-fluid bg-light p-4 rounded shadow-sm"> | |
<!-- Suche --> | |
<div class="mb-3 search-container"> | |
<form id="searchForm" action="/archive" method="get" class="d-flex flex-column"> | |
<div class="mb-3 w-100"> | |
<label for="search" class="form-label">Suche:</label> | |
<input | |
type="text" | |
class="form-control" | |
id="search" | |
name="search" | |
value="{{ search_query }}" | |
> | |
</div> | |
<div class="d-flex flex-wrap mt-3"> | |
<button type="submit" class="btn btn-primary flex-fill" style="width: 33.33%;">Suchen</button> | |
<button type="reset" class="btn btn-secondary flex-fill ms-2" style="width: 33.33%;">Zurücksetzen</button> | |
</div> | |
</form> | |
</div> | |
<!-- Filter Accordion --> | |
<!-- Archiv Anzeige --> | |
<!-- Archiv Anzeige --> | |
<div id="archive" class="container-fluid"><div id="archive" class="container"> | |
<div class="accordion" id="filterAccordion"> | |
<div class="accordion-item"> | |
<h2 class="accordion-header" id="headingOne"> | |
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="false" aria-controls="collapseOne"> | |
Filter- und Anzeigeoptionen | |
</button> | |
</h2> | |
<div id="collapseOne" class="accordion-collapse collapse" aria-labelledby="headingOne"> | |
<div class="accordion-body"> | |
<!-- Filterformular --> | |
<form id="filterForm" action="/archive" method="get" class="d-flex flex-wrap gap-3 align-items-center"> | |
<div class="mb-3 flex-grow-1"> | |
<label for="album_filter" class="form-label">Album:</label> | |
<select class="form-control" id="album_filter" name="album"> | |
<option value="">Alle</option> | |
{% for album in albums %} | |
<option value="{{ album[0] }}" {% if album[0] == selected_album %}selected{% endif %}>{{ album[1] }}</option> | |
{% endfor %} | |
</select> | |
</div> | |
<div class="mb-3 flex-grow-1"> | |
<label for="category_filter" class="form-label">Kategorie:</label> | |
<select class="form-control" id="category_filter" name="category" multiple> | |
<option value="">Alle</option> | |
{% for category in categories %} | |
<option value="{{ category[0] }}" {% if category[0] in selected_categories %}selected{% endif %}>{{ category[1] }}</option> | |
{% endfor %} | |
</select> | |
</div> | |
<button type="submit" class="btn btn-primary flex-fill">Filtern</button> | |
</form> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div id="tollbarAll" class="toolbar"> | |
<button id="thumbgalleryBtn" class="btn btn-secondary">Thumbgallery</button> | |
<button id="slideshowBtn" class="btn btn-secondary">Slideshow</button> | |
<select id="gridLayout" class="form-select"> | |
<option value="2">2 Bilder</option> | |
<option value="3" selected>3 Bilder</option> | |
<option value="4">4 Bilder</option> | |
<option value="5">5 Bilder</option> | |
<option value="6">6 Bilder</option> | |
</select> | |
<div class="dropdown"> | |
<button class="btn btn-secondary dropdown-toggle" type="button" id="actionMenu" data-bs-toggle="dropdown" aria-expanded="false"> | |
Optionen | |
</button> | |
<ul class="dropdown-menu" aria-labelledby="actionMenu"> | |
<li><a class="dropdown-item" href="#" id="deleteSelected">Löschen</a></li> | |
<li><a class="dropdown-item" href="#" id="addToCategory">Zu Kategorie hinzufügen</a></li> | |
<li><a class="dropdown-item" href="#" id="addToAlbum">Zu Album hinzufügen</a></li> | |
<li><a class="dropdown-item" href="#" id="downloadSelected">Aktuelle Auswahl downloaden</a></li> | |
</ul> | |
</div> | |
</div> | |
<div class="d-flex justify-content-between align-items-center mb-3 mt-3"> | |
<!-- Links: Checkbox "Alles auswählen" --> | |
<div class="d-flex align-items-center"> | |
<input type="checkbox" id="selectAll" /> | |
<label for="selectAll" class="ms-1 mb-0">Alles auswählen</label> | |
</div> | |
<!-- Rechts: Items-per-page-Auswahl --> | |
<div class="d-flex align-items-center"> | |
<label for="itemsPerPageSelect" class="me-2 mb-0">Bilder pro Seite:</label> | |
<select id="itemsPerPageSelect" class="form-select form-select-sm" style="width: auto;"> | |
<option value="15" {% if items_per_page == 15 %}selected{% endif %}>15</option> | |
<option value="30" {% if items_per_page == 30 %}selected{% endif %}>30</option> | |
<option value="50" {% if items_per_page == 50 %}selected{% endif %}>50</option> | |
<option value="75" {% if items_per_page == 75 %}selected{% endif %}>75</option> | |
<option value="100" {% if items_per_page == 100 %}selected{% endif %}>100</option> | |
</select> | |
</div> | |
</div> | |
</div> | |
<!-- Bildgrid --> | |
<div class="row row-cols-3" id="imageGrid"> | |
{% for log in logs %} | |
<div class="col mb-3"> | |
<div class="card custom-bg"> | |
<div class="card-body p-0 position-relative"> | |
<img src="{{ log.output_file }}" | |
class="img-fluid image-thumbnail" | |
alt="Generiertes Bild" | |
data-id="{{ log.id }}" | |
data-filename="{{ log.output_file.split('/')[-1] }}" | |
data-format="{{ log.output_file.split('.')[-1] }}" | |
data-timestamp="{{ log.timestamp }}" | |
data-album="{{ log.album }}" | |
data-category="{{ log.category }}" | |
data-prompt="{{ log.prompt }}" | |
data-optimized_prompt="{{ log.optimized_prompt }}"> | |
<input type="checkbox" class="form-check-input select-item position-absolute top-0 end-0 m-2"> | |
</div> | |
</div> | |
</div> | |
{% endfor %} | |
</div> | |
</div> | |
</div> | |
<!-- Paginierung --> | |
<div class="d-flex justify-content-center mt-4"> | |
{% if page > 1 %} | |
<a class="btn btn-secondary me-2" href="?page={{ page - 1 }}&items_per_page={{ items_per_page }}{% if search_query %}&search={{ search_query }}{% endif %}{% if selected_album %}&album={{ selected_album }}{% endif %}{% if selected_categories %}&category={{ selected_categories | join(',') }}{% endif %}">Vorherige Seite</a> | |
{% endif %} | |
{% if logs|length == items_per_page %} | |
<a class="btn btn-secondary" href="?page={{ page + 1 }}&items_per_page={{ items_per_page }}{% if search_query %}&search={{ search_query }}{% endif %}{% if selected_album %}&album={{ selected_album }}{% endif %}{% if selected_categories %}&category={{ selected_categories | join(',') }}{% endif %}">Nächste Seite</a> | |
{% endif %} | |
</div> | |
<button id="scrollTopBtn" style="display: none; position: fixed; bottom: 20px; right: 20px; z-index: 99; border: none; background: transparent;"> | |
<img src="/static/arrow-up1.png" alt="Nach oben" style="width: 50px; height: 50px;"> | |
</button> | |
<!-- "Nach oben"-Button --> | |
<!-- <button id="scrollTopBtn" class="btn btn-primary" style="display: none; position: fixed; bottom: 20px; right: 20px; width: 100px; z-index: 99;"> | |
Nach oben | |
</button> --> | |
<!-- Bild-Detail Modal --> | |
<div id="imageModal" class="modal fade" tabindex="-1" aria-labelledby="imageModalLabel" aria-hidden="true"> | |
<div class="modal-dialog modal-lg"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title" id="imageModalLabel">Bilddetails</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button> | |
</div> | |
<div class="modal-body"> | |
<div class="image-container" style="cursor: pointer;"> | |
<img id="modalImage" src="" class="img-fluid mb-3" alt="Bild"> | |
</div> | |
<div class="image-details"> | |
<p><strong>Dateiname:</strong> <span id="modalFilename"></span></p> | |
<p><strong>Bildformat:</strong> <span id="modalFormat"></span></p> | |
<p><strong>Datum:</strong> <span id="modalTimestamp"></span></p> | |
<p><strong>Album:</strong> <span id="modalAlbum"></span></p> | |
<p><strong>Kategorie:</strong> <span id="modalCategory"></span></p> | |
<p><strong>Eingabeaufforderung:</strong> <span id="modalPrompt"></span></p> | |
<p><strong>Optimierte Eingabeaufforderung:</strong> <span id="modalOptimizedPrompt"></span></p> | |
</div> | |
</div> | |
<div class="modal-footer"> | |
<button type="button" class="btn btn-primary" id="modalDownloadBtn"> | |
<i class="fas fa-download"></i> Download | |
</button> | |
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Thumbnail-Galerie Modal --> | |
<div id="thumbGalleryModal" class="modal fade" tabindex="-1"> | |
<div class="modal-dialog modal-lg"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title">Thumbnail-Galerie</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> | |
</div> | |
<div class="modal-body"> | |
<div id="thumbGalleryContainer" class="d-flex flex-wrap justify-content-center"> | |
</div> | |
</div> | |
<div class="modal-footer"> | |
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Slideshow Modal --> | |
<div id="slideshowModal" class="modal fade" tabindex="-1"> | |
<div class="modal-dialog modal-lg"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title">Diashow</h5> | |
<div class="btn-group ms-auto me-2"> | |
<button class="btn btn-primary btn-sm" id="playSlideshow"> | |
<i class="fas fa-play"></i> | |
</button> | |
<button class="btn btn-primary btn-sm" id="pauseSlideshow" style="display: none;"> | |
<i class="fas fa-pause"></i> | |
</button> | |
<button class="btn btn-primary btn-sm" id="fullscreenBtn"> | |
<i class="fas fa-expand"></i> | |
</button> | |
<button class="btn btn-primary btn-sm" id="downloadCurrentSlide"> | |
<i class="fas fa-download"></i> | |
</button> | |
</div> | |
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> | |
</div> | |
<div class="modal-body"> | |
<div id="carouselExampleControls" class="carousel slide" data-bs-interval="false"> | |
<div id="slideshowContainer" class="carousel-inner"></div> | |
<button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleControls" data-bs-slide="prev"> | |
<span class="carousel-control-prev-icon" aria-hidden="true"></span> | |
<span class="visually-hidden">Vorherige</span> | |
</button> | |
<button class="carousel-control-next" type="button" data-bs-target="#carouselExampleControls" data-bs-slide="next"> | |
<span class="carousel-control-next-icon" aria-hidden="true"></span> | |
<span class="visually-hidden">Nächste</span> | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Modal für Zuweisung zu Album/Kategorie --> | |
<div id="assignAlbumModal" class="modal fade" tabindex="-1"> | |
<div class="modal-dialog"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title">Zu Album hinzufügen</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> | |
</div> | |
<div class="modal-body"> | |
<select class="form-control" id="albumSelect"> | |
{% for album in albums %} | |
<option value="{{ album[0] }}">{{ album[1] }}</option> | |
{% endfor %} | |
</select> | |
</div> | |
<div class="modal-footer"> | |
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button> | |
<button type="button" class="btn btn-primary" id="assignAlbumBtn">Hinzufügen</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div id="assignCategoryModal" class="modal fade" tabindex="-1"> | |
<div class="modal-dialog"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title">Zu Kategorie hinzufügen</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> | |
</div> | |
<div class="modal-body"> | |
<select class="form-control" id="categorySelect" multiple> | |
{% for category in categories %} | |
<option value="{{ category[0] }}">{{ category[1] }}</option> | |
{% endfor %} | |
</select> | |
</div> | |
<div class="modal-footer"> | |
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button> | |
<button type="button" class="btn btn-primary" id="assignCategoryBtn">Hinzufügen</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- CSS Styles --> | |
<style> | |
.image-container { | |
position: relative; | |
text-align: center; | |
max-height: 80vh; | |
overflow: auto; | |
} | |
.image-container img { | |
max-width: 100%; | |
height: auto; | |
transition: transform 0.2s; | |
} | |
.image-container img:hover { | |
transform: scale(1.02); | |
} | |
.thumb-container { | |
position: relative; | |
display: inline-block; | |
} | |
.download-thumb { | |
position: absolute; | |
bottom: 5px; | |
right: 5px; | |
opacity: 0; | |
transition: opacity 0.3s; | |
} | |
.thumb-container:hover .download-thumb { | |
opacity: 1; | |
} | |
.carousel-item img { | |
max-height: 80vh; | |
object-fit: contain; | |
} | |
#slideshowModal.fullscreen .modal-dialog { | |
max-width: 100%; | |
margin: 0; | |
height: 100vh; | |
} | |
#slideshowModal.fullscreen .modal-content { | |
height: 100%; | |
border: none; | |
border-radius: 0; | |
} | |
.carousel-control-prev, | |
.carousel-control-next { | |
width: 10%; | |
opacity: 0; | |
transition: opacity 0.3s; | |
} | |
.carousel:hover .carousel-control-prev, | |
.carousel:hover .carousel-control-next { | |
opacity: 0.5; | |
} | |
.modal-dialog { | |
max-width: 90vw; | |
margin: 1.75rem auto; | |
} | |
.image-details { | |
margin-top: 1rem; | |
padding: 1rem; | |
background-color: rgba(0,0,0,0.02); | |
border-radius: 4px; | |
} | |
.modal-footer { | |
justify-content: space-between; | |
} | |
.container-fluid.bg-light { | |
background-color: rgba(248, 249, 250, 0.8) ; /* 50% Transparenz */ | |
} | |
/* Für Geräte mit einer maximalen Breite von 768px (Tablets und kleiner) */ | |
@media (max-width: 768px) { | |
#tollbarAll { | |
display: none; | |
} | |
#imageGrid { | |
display: grid; | |
grid-template-columns: 1fr; /* 1 Bild pro Reihe */ | |
gap: 15px; /* Abstand zwischen Bildern */ | |
} | |
/* Bildkarten auf volle Breite skalieren */ | |
.card { | |
width: 100%; /* Volle Breite */ | |
margin: 0 auto; | |
} | |
.card img { | |
width: 100%; /* Bild nimmt gesamte Breite der Karte ein */ | |
height: auto; | |
} | |
} | |
</style> | |
<!-- {% block scripts %} | |
<script src="/static/script.js"></script> | |
{% endblock %} --> | |
<script> | |
document.addEventListener('DOMContentLoaded', function () { | |
// Hilfsfunktion für Einzelbild-Download | |
async function downloadSingleImage(filename) { | |
try { | |
const response = await fetch(`/flux-pics/${filename}`); | |
if (!response.ok) { | |
const errorText = await response.text(); | |
console.error('Server Error:', errorText); | |
throw new Error(`HTTP error! status: ${response.status}`); | |
} | |
const blob = await response.blob(); | |
const url = window.URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.style.display = 'none'; | |
a.href = url; | |
a.download = filename; | |
document.body.appendChild(a); | |
a.click(); | |
window.URL.revokeObjectURL(url); | |
} catch (error) { | |
console.error('Fehler beim Download:', error); | |
alert('Ein Fehler ist aufgetreten: ' + error.message); | |
} | |
} | |
// Funktion zur automatischen Anpassung des Layouts für mobile Geräte | |
function updateLayoutForMobile() { | |
const gridLayout = document.getElementById('gridLayout'); | |
const imageGrid = document.getElementById('imageGrid'); | |
if (window.innerWidth <= 768) { | |
gridLayout.value = 1; // Ein Bild pro Zeile auf mobilen Geräten | |
imageGrid.className = 'row row-cols-1'; | |
} else { | |
const columns = parseInt(gridLayout.value); | |
imageGrid.className = `row row-cols-1 row-cols-md-${columns}`; | |
} | |
} | |
// Initiale Layout-Anpassung | |
updateLayoutForMobile(); | |
// Event Listener für Fenstergrößenänderung (Responsive Verhalten) | |
window.addEventListener('resize', updateLayoutForMobile); | |
// Event Listener für Grid Layout | |
document.getElementById('gridLayout').addEventListener('change', function () { | |
const columns = parseInt(this.value); | |
const imageGrid = document.getElementById('imageGrid'); | |
imageGrid.className = `row row-cols-1 row-cols-md-${columns}`; | |
}); | |
// Funktion zur Öffnung des Bilddetails-Modals | |
function openImageModal(img) { | |
const modal = new bootstrap.Modal(document.getElementById('imageModal')); | |
const modalImg = document.getElementById('modalImage'); | |
const filename = img.dataset.filename; | |
modalImg.src = img.src; | |
document.getElementById('modalFilename').textContent = filename; | |
document.getElementById('modalFormat').textContent = img.dataset.format; | |
document.getElementById('modalTimestamp').textContent = img.dataset.timestamp; | |
document.getElementById('modalAlbum').textContent = img.dataset.album; | |
document.getElementById('modalCategory').textContent = img.dataset.category; | |
document.getElementById('modalPrompt').textContent = img.dataset.prompt; | |
document.getElementById('modalOptimizedPrompt').textContent = img.dataset.optimized_prompt; | |
modal.show(); | |
} | |
document.querySelectorAll('.image-thumbnail').forEach(function (img) { | |
img.addEventListener('click', function () { | |
openImageModal(this); | |
}); | |
}); | |
// Funktion für die "Alle auswählen"-Checkbox | |
const selectAllCheckbox = document.getElementById('selectAll'); | |
const itemCheckboxes = document.querySelectorAll('.select-item'); | |
selectAllCheckbox.addEventListener('change', function () { | |
itemCheckboxes.forEach(checkbox => { | |
checkbox.checked = selectAllCheckbox.checked; | |
}); | |
}); | |
// Funktion zum Sammeln ausgewählter Bilder | |
function getSelectedImages() { | |
const selectedImages = []; | |
document.querySelectorAll('.select-item:checked').forEach(checkbox => { | |
const img = checkbox.closest('.card').querySelector('img'); | |
if (img && img.dataset.filename) { | |
selectedImages.push(img.dataset.filename); | |
} | |
}); | |
return selectedImages; | |
} | |
// Download der ausgewählten Bilder | |
document.getElementById('downloadSelected').addEventListener('click', async function () { | |
const selectedImages = getSelectedImages(); | |
if (selectedImages.length === 0) { | |
alert('Keine Bilder ausgewählt.'); | |
return; | |
} | |
let downloadType = 'single'; | |
if (selectedImages.length > 1) { | |
const choice = confirm('Möchten Sie die Bilder als ZIP-Datei herunterladen? Klicken Sie "OK" für ZIP oder "Abbrechen" für Einzeldownloads.'); | |
if (choice) { | |
downloadType = 'zip'; | |
} | |
} | |
try { | |
if (downloadType === 'zip') { | |
const response = await fetch('/flux-pics', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ selectedImages }) | |
}); | |
if (!response.ok) { | |
const errorText = await response.text(); | |
console.error('Server Error:', errorText); | |
throw new Error(`HTTP error! status: ${response.status}`); | |
} | |
const blob = await response.blob(); | |
const url = window.URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.style.display = 'none'; | |
a.href = url; | |
a.download = 'images.zip'; | |
document.body.appendChild(a); | |
a.click(); | |
window.URL.revokeObjectURL(url); | |
} else { | |
for (const filename of selectedImages) { | |
await downloadSingleImage(filename); | |
} | |
} | |
alert('Download erfolgreich abgeschlossen.'); | |
} catch (error) { | |
console.error('Fehler beim Downloaden:', error); | |
alert('Ein Fehler ist aufgetreten: ' + error.message); | |
} | |
}); | |
// Automatisches Aktualisieren der Bilder pro Seite | |
const itemsPerPageSelect = document.getElementById('itemsPerPageSelect'); | |
if (itemsPerPageSelect) { | |
itemsPerPageSelect.addEventListener('change', function () { | |
const form = document.getElementById('generateForm'); | |
if (form) { | |
form.submit(); | |
} | |
}); | |
} | |
// "Nach oben"-Button | |
const scrollTopBtn = document.getElementById('scrollTopBtn'); | |
window.addEventListener('scroll', function () { | |
scrollTopBtn.style.display = window.scrollY > 300 ? 'block' : 'none'; | |
}); | |
scrollTopBtn.addEventListener('click', function () { | |
window.scrollTo({ top: 0, behavior: 'smooth' }); | |
}); | |
}); | |
</script> | |
{% endblock %} | |