Karl aa8e1d1701 refactor(ui): implement base template and shared CSS
Introduce a `base.html` template to establish a common structure for all pages using Jinja2 template inheritance. This eliminates redundant HTML boilerplate across multiple files.

Additionally, common CSS rules have been extracted from individual templates and consolidated into a single `static/css/main.css` file. All pages now link to this shared stylesheet.

This refactoring significantly reduces code duplication, improves maintainability, and ensures a consistent user interface.
2025-07-18 13:56:38 +01:00

379 lines
12 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}Image Archive{% endblock %}
{% block head %}
<style>
h1 {
text-align: center;
margin-bottom: 2rem;
}
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(500px, 1fr));
gap: 20px;
}
.gallery img {
width: 100%;
height: auto;
border-radius: 10px;
cursor: pointer;
transition: transform 0.2s ease;
}
.gallery img:hover {
transform: scale(1.02);
}
/* Lightbox styles */
.lightbox {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
justify-content: center;
align-items: center;
flex-direction: column;
z-index: 999;
padding: 20px 0;
}
.lightbox img {
max-width: 90%;
max-height: 80%;
border-radius: 10px;
}
.lightbox .close {
position: absolute;
top: 20px;
right: 30px;
font-size: 30px;
color: white;
cursor: pointer;
}
.arrow {
position: absolute;
top: 50%;
font-size: 40px;
color: white;
cursor: pointer;
user-select: none;
transform: translateY(-50%);
}
.arrow.left {
left: 20px;
}
.arrow.right {
right: 20px;
}
#lightbox-prompt {
color: #ccc;
font-family: monospace;
white-space: pre-wrap;
background: rgba(0, 0, 0, 0.6);
padding: 10px 20px;
border-radius: 10px;
max-width: 80%;
text-align: left;
margin-top: 20px;
max-height: 25vh;
overflow-y: auto;
}
.home-button {
position: fixed;
top: 20px;
right: 20px;
z-index: 500;
}
.favourites-button {
position: fixed;
top: 20px;
right: 120px;
z-index: 500;
}
.favourite-heart {
position: absolute;
top: 20px;
left: 30px;
font-size: 30px;
color: white;
cursor: pointer;
}
@media (max-width: 600px) {
body {
padding: 1rem;
font-size: 14px;
}
.gallery {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 12px;
}
.gallery img {
border-radius: 8px;
}
.lightbox .close {
font-size: 36px;
top: 10px;
right: 15px;
}
.arrow {
font-size: 48px;
top: 50%;
}
#lightbox-prompt {
font-size: 14px;
max-width: 90%;
padding: 8px 16px;
max-height: 20vh;
overflow-y: auto;
}
.button-link {
font-size: 14px;
padding: 8px 16px;
}
}
</style>
{% endblock %}
{% block content %}
<a href="/" class="button-link home-button">Home</a>
<button class="button-link favourites-button" id="favourites-button" onclick="toggleFavouritesView()">Show Favourites</button>
<h1 id="page-title">Image Archive</h1>
<div class="gallery" id="gallery"></div>
<div class="lightbox" id="lightbox" tabindex="-1" onkeyup="handleLightboxKeys(event)">
<span class="close" onclick="closeLightbox()">&times;</span>
<span class="favourite-heart" id="favourite-heart" onclick="toggleFavourite()">&#9825;</span>
<span class="arrow left" onclick="prevImage()">&#10094;</span>
<img id="lightbox-img" src="" />
<p id="lightbox-prompt"></p>
<span class="arrow right" onclick="nextImage()">&#10095;</span>
</div>
{% endblock %}
{% block scripts %}
<script>
let allImages = JSON.parse(`[
{% for image in images %}
{ "filename": "{{ image.filename }}", "favourited": {{ 'true' if image.favourited else 'false' }} }{% if not loop.last %},{% endif %}
{% endfor %}
]`);
</script>
<script>
const gallery = document.getElementById('gallery');
const batchSize = 9;
let loadedCount = 0;
let currentIndex = 0;
const detailsCache = {};
let showingFavourites = false;
let filteredImages = allImages;
function createImageElement(image) {
const img = document.createElement('img');
img.src = `/images/thumbnails/${image.filename}`;
img.dataset.fullsrc = `/images/${image.filename}`;
img.dataset.filename = image.filename;
img.dataset.favourited = image.favourited;
img.loading = 'lazy';
img.style.cursor = 'pointer';
img.style.borderRadius = '10px';
img.addEventListener('click', () => openLightbox(img));
return img;
}
function loadNextBatch() {
const imagesToLoad = showingFavourites ? filteredImages : allImages;
const nextImages = imagesToLoad.slice(loadedCount, loadedCount + batchSize);
nextImages.forEach(image => {
const imgEl = createImageElement(image);
gallery.appendChild(imgEl);
});
loadedCount += nextImages.length;
}
function renderGallery() {
gallery.innerHTML = '';
loadedCount = 0;
loadNextBatch();
}
function toggleFavouritesView() {
showingFavourites = !showingFavourites;
const button = document.getElementById('favourites-button');
const pageTitle = document.getElementById('page-title');
if (showingFavourites) {
filteredImages = allImages.filter(img => img.favourited);
button.textContent = 'Show All';
pageTitle.textContent = 'Favourites';
} else {
filteredImages = allImages;
button.textContent = 'Show Favourites';
pageTitle.textContent = 'Image Archive';
}
renderGallery();
}
renderGallery();
window.addEventListener('scroll', () => {
const imagesToLoad = showingFavourites ? filteredImages : allImages;
if (loadedCount >= imagesToLoad.length) return;
if ((window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 100)) {
loadNextBatch();
}
});
function getGalleryImages() {
return Array.from(gallery.querySelectorAll('img'));
}
function openLightbox(imgEl) {
const images = getGalleryImages();
currentIndex = images.indexOf(imgEl);
showImageAndLoadDetails(currentIndex);
const lightbox = document.getElementById("lightbox");
lightbox.style.display = "flex";
lightbox.focus();
}
function updateFavouriteHeart(isFavourited) {
const heart = document.getElementById('favourite-heart');
heart.innerHTML = isFavourited ? '&#9829;' : '&#9825;';
heart.style.color = isFavourited ? 'red' : 'white';
}
function toggleFavourite() {
const images = getGalleryImages();
const imgEl = images[currentIndex];
const filename = imgEl.dataset.filename;
fetch('/favourites/toggle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: filename })
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
const isFavourited = data.favourited;
imgEl.dataset.favourited = isFavourited;
updateFavouriteHeart(isFavourited);
const originalImage = allImages.find(img => img.filename === filename);
if (originalImage) {
originalImage.favourited = isFavourited;
}
if (showingFavourites) {
filteredImages = allImages.filter(img => img.favourited);
renderGallery();
}
}
});
}
function showImageAndLoadDetails(index) {
const images = getGalleryImages();
const imgEl = images[index];
const filename = imgEl.dataset.filename;
const fullsrc = imgEl.dataset.fullsrc;
const isFavourited = imgEl.dataset.favourited === 'true';
document.getElementById("lightbox-img").src = fullsrc;
updateFavouriteHeart(isFavourited);
if (detailsCache[filename]) {
document.getElementById("lightbox-prompt").textContent =
`Model:${detailsCache[filename].model} - Created:${detailsCache[filename].date}\n\n${detailsCache[filename].prompt}`;
} else {
document.getElementById("lightbox-prompt").textContent = "Loading…";
fetch(`/image-details/${encodeURIComponent(filename)}`)
.then(response => {
if (!response.ok) throw new Error("Network response was not ok");
return response.json();
})
.then(data => {
detailsCache[filename] = data;
document.getElementById("lightbox-prompt").textContent =
`Model:${data.model} - Created:${data.date}\n\n${data.prompt}`;
})
.catch(() => {
document.getElementById("lightbox-prompt").textContent = "Couldnt load details.";
});
}
}
function nextImage() {
const images = getGalleryImages();
const imagesToLoad = showingFavourites ? filteredImages : allImages;
if (currentIndex + 1 >= images.length && loadedCount < imagesToLoad.length) {
loadNextBatch();
setTimeout(() => {
const updatedImages = getGalleryImages();
if (currentIndex + 1 < updatedImages.length) {
currentIndex++;
showImageAndLoadDetails(currentIndex);
}
}, 100);
} else {
currentIndex = (currentIndex + 1) % images.length;
showImageAndLoadDetails(currentIndex);
}
}
function prevImage() {
const images = getGalleryImages();
currentIndex = (currentIndex - 1 + images.length) % images.length;
showImageAndLoadDetails(currentIndex);
}
function closeLightbox() {
document.getElementById("lightbox").style.display = "none";
if (showingFavourites) {
const currentImage = getGalleryImages()[currentIndex];
const wasFavourited = currentImage.dataset.favourited === 'true';
const originalImage = allImages.find(img => img.filename === currentImage.dataset.filename);
if (originalImage && !originalImage.favourited) {
renderGallery();
}
}
}
function handleLightboxKeys(e) {
if (e.key === 'f') {
toggleFavourite();
} else if (e.key === 'Escape') {
closeLightbox();
} else if (e.key === 'ArrowLeft') {
prevImage();
} else if (e.key === 'ArrowRight') {
nextImage();
}
}
</script>
{% endblock %}