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 %} | |