Compare commits

...

17 Commits
0.2.5 ... main

Author SHA1 Message Date
31b373b34a Bump version: 0.2.13 → 0.2.14 2025-07-10 17:12:23 +01:00
8665ab431c rework version 2025-07-10 17:12:14 +01:00
1b528a4277 new version 2025-07-10 17:11:09 +01:00
6210b5de7d updated toml 2025-07-10 17:10:00 +01:00
ab32c8032c add and update version to docker image 2025-07-10 17:08:58 +01:00
43ce8e1b89 Bump version: 0.2.10 → 0.2.11 2025-07-10 16:59:03 +01:00
826e91e7e2 Bump version: 0.2.9 → 0.2.10 2025-07-10 16:58:56 +01:00
98f7a87005 revert to 0.2.9 2025-07-10 16:58:50 +01:00
bc933c2308 Bump version: 0.2.9 → 0.2.10 2025-07-10 16:25:14 +01:00
d5c4d54041 Bump version: 0.2.8 → 0.2.9 2025-07-10 16:25:09 +01:00
ba9b4b8bd4 fix(favorites): preserve favorite status after image rename
Previously, if a user favorited a newly generated image (named 'image.png'), the favorite status would be lost once the image was automatically renamed to its final timestamped filename.

This change modifies the renaming logic to check the `favourites.json` file. If 'image.png' is found, its entry is updated with the new filename, ensuring the favorite status is preserved.
2025-07-10 16:24:57 +01:00
7f78912829 Bump version: 0.2.7 → 0.2.8 2025-06-30 14:28:42 +01:00
33404c7a37 keyboard support in the lightbox 2025-06-30 14:28:27 +01:00
0b7e0ca59d Bump version: 0.2.6 → 0.2.7 2025-06-30 13:16:55 +01:00
eeb82a9a3a switch title based on the page 2025-06-30 13:16:45 +01:00
96deba17de Bump version: 0.2.5 → 0.2.6 2025-06-30 11:56:22 +01:00
fe7a32b1de support for favourite images in gallery 2025-06-30 11:56:14 +01:00
6 changed files with 191 additions and 23 deletions

View File

@ -1,19 +1,24 @@
[tool.bumpversion] [tool.bumpversion]
current_version = "0.2.5" current_version = "0.2.14"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)" parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
serialize = ["{major}.{minor}.{patch}"] serialize = ["{major}.{minor}.{patch}"]
search = "{current_version}"
replace = "{new_version}" replace = "{new_version}"
regex = false regex = false
ignore_missing_version = false
ignore_missing_files = false
tag = true tag = true
sign_tags = false
tag_name = "{new_version}"
tag_message = "Bump version: {current_version} → {new_version}"
allow_dirty = false
commit = true commit = true
message = "Bump version: {current_version} → {new_version}" message = "Bump version: {current_version} → {new_version}"
tag_name = "{new_version}"
tag_message = "Bump version: {current_version} → {new_version}"
[[tool.bumpversion.files]]
filename = ".bumpversion.toml"
search = 'current_version = "{current_version}"'
replace = 'current_version = "{new_version}"'
[[tool.bumpversion.files]]
filename = "Dockerfile"
search = 'ARG VERSION="{current_version}"'
replace = 'ARG VERSION="{new_version}"'
moveable_tags = [] moveable_tags = []
commit_args = "" commit_args = ""
setup_hooks = [] setup_hooks = []

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

@ -3,9 +3,13 @@ FROM python:3.11-slim
# Set the working directory in the container # Set the working directory in the container
WORKDIR /app WORKDIR /app
# Set version label
ARG VERSION="0.2.14"
LABEL version=$VERSION
# Copy project files into the container # Copy project files into the container
COPY . /app COPY . /app
# Install dependencies # Install dependencies
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt

View File

@ -51,10 +51,22 @@ def load_config() -> configparser.ConfigParser:
def rename_image() -> str | None: def rename_image() -> str | None:
"""Renames 'image.png' in the output folder to a timestamped filename if it exists.""" """Renames 'image.png' in the output folder to a timestamped filename if it exists."""
old_path = os.path.join(user_config["comfyui"]["output_dir"], "image.png") old_path = os.path.join(user_config["comfyui"]["output_dir"], "image.png")
favourites_file = "./favourites.json"
if os.path.exists(old_path): if os.path.exists(old_path):
new_filename = f"{str(time.time())}.png" new_filename = f"{str(time.time())}.png"
new_path = os.path.join(user_config["comfyui"]["output_dir"], new_filename) new_path = os.path.join(user_config["comfyui"]["output_dir"], new_filename)
# Check if image.png is a favourite
if os.path.exists(favourites_file):
with open(favourites_file, 'r') as f:
favourites = json.load(f)
if "image.png" in favourites:
favourites.remove("image.png")
favourites.append(new_filename)
with open(favourites_file, 'w') as f:
json.dump(favourites, f)
os.rename(old_path, new_path) os.rename(old_path, new_path)
generate_thumbnail(new_path) generate_thumbnail(new_path)
print(f"Renamed 'image.png' to '{new_filename}'") print(f"Renamed 'image.png' to '{new_filename}'")

View File

@ -1,15 +1,50 @@
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/toggle", methods=["POST"])
def toggle_favourite():
data = request.get_json()
filename = data.get("filename")
if not filename:
return jsonify({"status": "error", "message": "Filename missing"}), 400
favourites = get_favourites()
is_favourited = False
if filename in favourites:
favourites.remove(filename)
else:
favourites.append(filename)
is_favourited = True
save_favourites(favourites)
return jsonify({"status": "success", "favourited": is_favourited})

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,15 +196,17 @@
<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 id="page-title">Image Archive</h1>
<!-- Empty gallery container; images will be loaded incrementally --> <!-- Empty gallery container; images will be loaded incrementally -->
<div class="gallery" id="gallery"></div> <div class="gallery" id="gallery"></div>
<!-- Lightbox --> <!-- Lightbox -->
<div class="lightbox" id="lightbox"> <div class="lightbox" id="lightbox" tabindex="-1" onkeyup="handleLightboxKeys(event)">
<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,11 +215,11 @@
<!-- Pass image filenames from Flask to JS --> <!-- Pass image filenames from Flask to JS -->
<script> <script>
const allImages = [ let allImages = JSON.parse(`[
{% for image in images %} {% for image in images %}
{ filename: "{{ image.filename }}" }, { "filename": "{{ image.filename }}", "favourited": {{ 'true' if image.favourited else 'false' }} }{% if not loop.last %},{% endif %}
{% endfor %} {% endfor %}
]; ]`);
</script> </script>
<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,35 @@
loadedCount += nextImages.length; 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();
}
// 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();
} }
@ -251,7 +297,45 @@
const images = getGalleryImages(); const images = getGalleryImages();
currentIndex = images.indexOf(imgEl); currentIndex = images.indexOf(imgEl);
showImageAndLoadDetails(currentIndex); showImageAndLoadDetails(currentIndex);
document.getElementById("lightbox").style.display = "flex"; 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;'; // solid vs outline heart
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) { function showImageAndLoadDetails(index) {
@ -259,8 +343,10 @@
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 +373,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 +399,27 @@
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();
}
}
}
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> </script>
</body> </body>