support for favourite images in gallery

This commit is contained in:
Karl 2025-06-30 11:56:14 +01:00
parent 73acc92ee4
commit fe7a32b1de
3 changed files with 146 additions and 11 deletions

1
.gitignore vendored
View File

@ -10,3 +10,4 @@ prompts_log.jsonl
publish.sh publish.sh
test.py test.py
.vscode/launch.json .vscode/launch.json
favourites.json

View File

@ -1,15 +1,58 @@
from flask import Blueprint, render_template from flask import Blueprint, render_template, jsonify, request
import os import os
import json
bp = Blueprint("gallery_routes", __name__) bp = Blueprint("gallery_routes", __name__)
image_folder = "./output" image_folder = "./output"
favourites_file = "./favourites.json"
def get_favourites():
if not os.path.exists(favourites_file):
return []
with open(favourites_file, 'r') as f:
return json.load(f)
def save_favourites(favourites):
with open(favourites_file, 'w') as f:
json.dump(favourites, f)
@bp.route("/images", methods=["GET"]) @bp.route("/images", methods=["GET"])
def gallery(): def gallery():
favourites = get_favourites()
images = [ images = [
{"filename": f} {"filename": f, "favourited": f in favourites}
for f in os.listdir(image_folder) for f in os.listdir(image_folder)
if f.lower().endswith(("png", "jpg", "jpeg", "gif")) if f.lower().endswith(("png", "jpg", "jpeg", "gif"))
] ]
images = sorted(images, key=lambda x: os.path.getmtime(os.path.join(image_folder, x["filename"])), reverse=True) images = sorted(images, key=lambda x: os.path.getmtime(os.path.join(image_folder, x["filename"])), reverse=True)
return render_template("gallery.html", images=images) return render_template("gallery.html", images=images)
@bp.route("/favourites", methods=["GET"])
def get_favourites_route():
return jsonify(get_favourites())
@bp.route("/favourites/add", methods=["POST"])
def add_favourite():
data = request.get_json()
filename = data.get("filename")
if not filename:
return jsonify({"status": "error", "message": "Filename missing"}), 400
favourites = get_favourites()
if filename not in favourites:
favourites.append(filename)
save_favourites(favourites)
return jsonify({"status": "success"})
@bp.route("/favourites/remove", methods=["POST"])
def remove_favourite():
data = request.get_json()
filename = data.get("filename")
if not filename:
return jsonify({"status": "error", "message": "Filename missing"}), 400
favourites = get_favourites()
if filename in favourites:
favourites.remove(filename)
save_favourites(favourites)
return jsonify({"status": "success"})

View File

@ -116,6 +116,22 @@
/* lower than lightbox (999) */ /* lower than lightbox (999) */
} }
.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;
}
.button-link { .button-link {
background: #333; background: #333;
color: white; color: white;
@ -127,6 +143,7 @@
transition: background 0.3s; transition: background 0.3s;
display: inline-block; display: inline-block;
text-align: center; text-align: center;
border: none;
} }
.button-link:hover { .button-link:hover {
@ -179,6 +196,7 @@
<body> <body>
<a href="/" class="button-link home-button">Home</a> <a href="/" class="button-link home-button">Home</a>
<button class="button-link favourites-button" id="favourites-button" onclick="toggleFavouritesView()">Show Favourites</button>
<h1>Image Archive</h1> <h1>Image Archive</h1>
@ -188,6 +206,7 @@
<!-- Lightbox --> <!-- Lightbox -->
<div class="lightbox" id="lightbox"> <div class="lightbox" id="lightbox">
<span class="close" onclick="closeLightbox()">&times;</span> <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> <span class="arrow left" onclick="prevImage()">&#10094;</span>
<img id="lightbox-img" src="" /> <img id="lightbox-img" src="" />
<p id="lightbox-prompt"></p> <p id="lightbox-prompt"></p>
@ -196,10 +215,10 @@
<!-- Pass image filenames from Flask to JS --> <!-- Pass image filenames from Flask to JS -->
<script> <script>
const allImages = [ let allImages = [
{% for image in images %} {% for image in images %}
{ filename: "{{ image.filename }}" }, { filename: "{{ image.filename }}", favourited: {{ 'true' if image.favourited else 'false' }} },
{% endfor %} {% endfor %}
]; ];
</script> </script>
@ -209,12 +228,15 @@
let loadedCount = 0; let loadedCount = 0;
let currentIndex = 0; let currentIndex = 0;
const detailsCache = {}; // Cache for image details const detailsCache = {}; // Cache for image details
let showingFavourites = false;
let filteredImages = allImages;
function createImageElement(image) { function createImageElement(image) {
const img = document.createElement('img'); const img = document.createElement('img');
img.src = `/images/thumbnails/${image.filename}`; img.src = `/images/thumbnails/${image.filename}`;
img.dataset.fullsrc = `/images/${image.filename}`; img.dataset.fullsrc = `/images/${image.filename}`;
img.dataset.filename = image.filename; img.dataset.filename = image.filename;
img.dataset.favourited = image.favourited;
img.loading = 'lazy'; img.loading = 'lazy';
img.style.cursor = 'pointer'; img.style.cursor = 'pointer';
img.style.borderRadius = '10px'; img.style.borderRadius = '10px';
@ -223,7 +245,8 @@
} }
function loadNextBatch() { function loadNextBatch() {
const nextImages = allImages.slice(loadedCount, loadedCount + batchSize); const imagesToLoad = showingFavourites ? filteredImages : allImages;
const nextImages = imagesToLoad.slice(loadedCount, loadedCount + batchSize);
nextImages.forEach(image => { nextImages.forEach(image => {
const imgEl = createImageElement(image); const imgEl = createImageElement(image);
gallery.appendChild(imgEl); gallery.appendChild(imgEl);
@ -231,12 +254,32 @@
loadedCount += nextImages.length; loadedCount += nextImages.length;
} }
function renderGallery() {
gallery.innerHTML = '';
loadedCount = 0;
loadNextBatch();
}
function toggleFavouritesView() {
showingFavourites = !showingFavourites;
const button = document.getElementById('favourites-button');
if (showingFavourites) {
filteredImages = allImages.filter(img => img.favourited);
button.textContent = 'Show All';
} else {
filteredImages = allImages;
button.textContent = 'Show Favourites';
}
renderGallery();
}
// Load initial batch // Load initial batch
loadNextBatch(); renderGallery();
// Load more images when scrolling near bottom // Load more images when scrolling near bottom
window.addEventListener('scroll', () => { window.addEventListener('scroll', () => {
if (loadedCount >= allImages.length) return; // all loaded const imagesToLoad = showingFavourites ? filteredImages : allImages;
if (loadedCount >= imagesToLoad.length) return; // all loaded
if ((window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 100)) { if ((window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 100)) {
loadNextBatch(); loadNextBatch();
} }
@ -254,13 +297,48 @@
document.getElementById("lightbox").style.display = "flex"; document.getElementById("lightbox").style.display = "flex";
} }
function updateFavouriteHeart(isFavourited) {
const heart = document.getElementById('favourite-heart');
heart.innerHTML = isFavourited ? '&#9829;' : '&#9825;'; // solid vs outline heart
heart.style.color = isFavourited ? 'red' : 'white';
}
function toggleFavourite() {
const images = getGalleryImages();
const imgEl = images[currentIndex];
const filename = imgEl.dataset.filename;
let isFavourited = imgEl.dataset.favourited === 'true';
const url = isFavourited ? '/favourites/remove' : '/favourites/add';
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: filename })
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
isFavourited = !isFavourited;
imgEl.dataset.favourited = isFavourited;
updateFavouriteHeart(isFavourited);
// Update the main image list
const originalImage = allImages.find(img => img.filename === filename);
if (originalImage) {
originalImage.favourited = isFavourited;
}
}
});
}
function showImageAndLoadDetails(index) { function showImageAndLoadDetails(index) {
const images = getGalleryImages(); const images = getGalleryImages();
const imgEl = images[index]; const imgEl = images[index];
const filename = imgEl.dataset.filename; const filename = imgEl.dataset.filename;
const fullsrc = imgEl.dataset.fullsrc; const fullsrc = imgEl.dataset.fullsrc;
const isFavourited = imgEl.dataset.favourited === 'true';
document.getElementById("lightbox-img").src = fullsrc; document.getElementById("lightbox-img").src = fullsrc;
updateFavouriteHeart(isFavourited);
if (detailsCache[filename]) { if (detailsCache[filename]) {
document.getElementById("lightbox-prompt").textContent = document.getElementById("lightbox-prompt").textContent =
@ -287,12 +365,16 @@
function nextImage() { function nextImage() {
const images = getGalleryImages(); const images = getGalleryImages();
if (currentIndex + 1 >= images.length && loadedCount < allImages.length) { const imagesToLoad = showingFavourites ? filteredImages : allImages;
if (currentIndex + 1 >= images.length && loadedCount < imagesToLoad.length) {
loadNextBatch(); loadNextBatch();
// Wait briefly to ensure DOM updates // Wait briefly to ensure DOM updates
setTimeout(() => { setTimeout(() => {
currentIndex++; const updatedImages = getGalleryImages();
showImageAndLoadDetails(currentIndex); if (currentIndex + 1 < updatedImages.length) {
currentIndex++;
showImageAndLoadDetails(currentIndex);
}
}, 100); }, 100);
} else { } else {
currentIndex = (currentIndex + 1) % images.length; currentIndex = (currentIndex + 1) % images.length;
@ -309,6 +391,15 @@
function closeLightbox() { function closeLightbox() {
document.getElementById("lightbox").style.display = "none"; document.getElementById("lightbox").style.display = "none";
if (showingFavourites) {
// Refresh the gallery if a favourite was removed
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();
}
}
} }
</script> </script>
</body> </body>