From 041db66b3daf1a17c2974507b952732761f13726 Mon Sep 17 00:00:00 2001 From: Djeex Date: Sat, 16 Aug 2025 11:17:15 +0200 Subject: [PATCH] Reworked flow --- src/py/webui/upload.py | 65 ++++++++++++++++++++------------- src/py/webui/webui.py | 49 ++++++++++++------------- src/webui/index.html | 9 +++-- src/webui/js/main.js | 81 ++++++++++++++++++------------------------ src/webui/js/upload.js | 66 ++++++++++++---------------------- 5 files changed, 125 insertions(+), 145 deletions(-) diff --git a/src/py/webui/upload.py b/src/py/webui/upload.py index fc2a800..20b4fb2 100644 --- a/src/py/webui/upload.py +++ b/src/py/webui/upload.py @@ -2,49 +2,64 @@ import logging from pathlib import Path from flask import Blueprint, request, current_app from werkzeug.utils import secure_filename +from src.py.builder.gallery_builder import update_gallery, update_hero -# Create a Flask blueprint for upload routes +# --- Create Flask blueprint for upload routes --- upload_bp = Blueprint("upload", __name__) -# Allowed file extensions for uploads +# --- Allowed file types --- ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "webp"} -# Function to check if a file has an allowed extension def allowed_file(filename: str) -> bool: + """Check if the uploaded file has an allowed extension.""" return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS -# Function to save uploaded file to a given folder def save_uploaded_file(file, folder: Path): - folder.mkdir(parents=True, exist_ok=True) # Create folder if it doesn't exist + """Save an uploaded file to the specified folder.""" + folder.mkdir(parents=True, exist_ok=True) # Create folder if not exists filename = secure_filename(file.filename) # Sanitize filename - file.save(folder / filename) # Save file to folder + file.save(folder / filename) # Save to disk logging.info(f"[✓] Uploaded {filename} to {folder}") - return filename # Return saved filename + return filename -# Route to handle photo uploads for gallery or hero @upload_bp.route("/api/
/upload", methods=["POST"]) def upload_photo(section: str): + """ + Handle file uploads for gallery or hero section. + Accepts multiple files under 'files'. + """ # Validate section if section not in ["gallery", "hero"]: return {"error": "Invalid section"}, 400 - # Check if the request contains a file - if "file" not in request.files: - return {"error": "No file part"}, 400 - file = request.files["file"] + # Check if files are provided + if "files" not in request.files: + return {"error": "No files provided"}, 400 + + files = request.files.getlist("files") + if not files: + return {"error": "No selected files"}, 400 - # Check if a file was actually selected - if file.filename == "": - return {"error": "No selected file"}, 400 + # Get photos directory from app config + PHOTOS_DIR = current_app.config.get("PHOTOS_DIR") + if not PHOTOS_DIR: + return {"error": "Server misconfiguration"}, 500 - # Check file type and save it - if file and allowed_file(file.filename): - PHOTOS_DIR = current_app.config.get("PHOTOS_DIR") # Get base photos directory from config - if not PHOTOS_DIR: - return {"error": "Server misconfiguration"}, 500 - folder = PHOTOS_DIR / section # Target folder (gallery or hero) - filename = save_uploaded_file(file, folder) - return {"status": "ok", "filename": filename} + folder = PHOTOS_DIR / section # Target folder + uploaded = [] - # If file type is not allowed - return {"error": "File type not allowed"}, 400 + # Save each valid file + for file in files: + if file and allowed_file(file.filename): + filename = save_uploaded_file(file, folder) + uploaded.append(filename) + + # Update YAML if any files were uploaded + if uploaded: + if section == "gallery": + update_gallery() + else: + update_hero() + return {"status": "ok", "uploaded": uploaded} + + return {"error": "No valid files uploaded"}, 400 diff --git a/src/py/webui/webui.py b/src/py/webui/webui.py index 952cc69..35ddae5 100644 --- a/src/py/webui/webui.py +++ b/src/py/webui/webui.py @@ -2,111 +2,106 @@ import logging from pathlib import Path from flask import Flask, jsonify, request, send_from_directory, render_template from src.py.builder.gallery_builder import ( - GALLERY_YAML, GALLERY_DIR, HERO_DIR, - load_yaml, save_yaml, get_all_image_paths, update_gallery, update_hero + GALLERY_YAML, load_yaml, save_yaml, update_gallery, update_hero ) from src.py.webui.upload import upload_bp # --- Logging configuration --- -# Logs messages to console with INFO level logging.basicConfig(level=logging.INFO, format="%(message)s") # --- Flask app setup --- -# WEBUI_PATH points to the web UI folder (templates + static) -WEBUI_PATH = Path(__file__).parents[2] / "webui" +WEBUI_PATH = Path(__file__).parents[2] / "webui" # Path to static/templates app = Flask( __name__, - template_folder=WEBUI_PATH, # where Flask looks for templates - static_folder=WEBUI_PATH, # where Flask serves static files - static_url_path="" # URL path prefix for static files + template_folder=WEBUI_PATH, + static_folder=WEBUI_PATH, + static_url_path="" ) -# --- Absolute photos directory --- -# Used by upload.py and deletion endpoints +# --- Photos directory (configurable) --- PHOTOS_DIR = Path(__file__).resolve().parents[3] / "config" / "photos" app.config["PHOTOS_DIR"] = PHOTOS_DIR # --- Register upload blueprint --- -# Handles /api/
/upload endpoints for gallery and hero images app.register_blueprint(upload_bp) -# --- Existing API routes --- +# --- Routes --- -# Serve main page @app.route("/") def index(): + """Serve the main HTML page.""" return render_template("index.html") -# Get gallery images (returns JSON array) @app.route("/api/gallery", methods=["GET"]) def get_gallery(): + """Return JSON list of gallery images from YAML.""" data = load_yaml(GALLERY_YAML) return jsonify(data.get("gallery", {}).get("images", [])) -# Get hero images (returns JSON array) @app.route("/api/hero", methods=["GET"]) def get_hero(): + """Return JSON list of hero images from YAML.""" data = load_yaml(GALLERY_YAML) return jsonify(data.get("hero", {}).get("images", [])) -# Update gallery images with new JSON data @app.route("/api/gallery/update", methods=["POST"]) def update_gallery_api(): + """Update gallery images in YAML from frontend JSON.""" images = request.json data = load_yaml(GALLERY_YAML) data["gallery"]["images"] = images save_yaml(data, GALLERY_YAML) return jsonify({"status": "ok"}) -# Update hero images with new JSON data @app.route("/api/hero/update", methods=["POST"]) def update_hero_api(): + """Update hero images in YAML from frontend JSON.""" images = request.json data = load_yaml(GALLERY_YAML) data["hero"]["images"] = images save_yaml(data, GALLERY_YAML) return jsonify({"status": "ok"}) -# Refresh gallery from the folder (rebuild YAML) @app.route("/api/gallery/refresh", methods=["POST"]) def refresh_gallery(): + """Refresh gallery YAML from photos/gallery folder.""" update_gallery() return jsonify({"status": "ok"}) -# Refresh hero images from the folder @app.route("/api/hero/refresh", methods=["POST"]) def refresh_hero(): + """Refresh hero YAML from photos/hero folder.""" update_hero() return jsonify({"status": "ok"}) -# Delete a gallery image file @app.route("/api/gallery/delete", methods=["POST"]) def delete_gallery_photo(): + """Delete a gallery photo from disk and return status.""" data = request.json - src = data.get("src") # filename only + src = data.get("src") file_path = PHOTOS_DIR / "gallery" / src if file_path.exists(): - file_path.unlink() # remove the file + file_path.unlink() return {"status": "ok"} return {"error": "File not found"}, 404 -# Delete a hero image file @app.route("/api/hero/delete", methods=["POST"]) def delete_hero_photo(): + """Delete a hero photo from disk and return status.""" data = request.json - src = data.get("src") # filename only + src = data.get("src") file_path = PHOTOS_DIR / "hero" / src if file_path.exists(): - file_path.unlink() # remove the file + file_path.unlink() return {"status": "ok"} return {"error": "File not found"}, 404 -# Serve photos from /photos/
/ @app.route("/photos/
/") def photos(section, filename): + """Serve uploaded photos from disk.""" return send_from_directory(PHOTOS_DIR / section, filename) -# --- Main entry point --- +# --- Run server --- if __name__ == "__main__": logging.info("Starting WebUI at http://127.0.0.1:5000") app.run(debug=True) diff --git a/src/webui/index.html b/src/webui/index.html index cc82873..59c0cb6 100644 --- a/src/webui/index.html +++ b/src/webui/index.html @@ -3,11 +3,14 @@ Photo WebUI + +

Photo WebUI

+
@@ -19,7 +22,7 @@

Gallery

@@ -29,10 +32,12 @@

Hero

+ + diff --git a/src/webui/js/main.js b/src/webui/js/main.js index 3f0113d..9d349de 100644 --- a/src/webui/js/main.js +++ b/src/webui/js/main.js @@ -1,29 +1,27 @@ -// Arrays to store gallery and hero images +// --- Arrays to store gallery and hero images --- let galleryImages = []; let heroImages = []; -// Load images data from server +// --- Load images from server on page load --- async function loadData() { try { - // Fetch gallery images from API const galleryRes = await fetch('/api/gallery'); galleryImages = await galleryRes.json(); - renderGallery(); // Render gallery images + renderGallery(); - // Fetch hero images from API const heroRes = await fetch('/api/hero'); heroImages = await heroRes.json(); - renderHero(); // Render hero images + renderHero(); } catch(err) { console.error(err); - alert("Error while loading images!"); + alert("Error loading images!"); } } -// Render gallery images in the page +// --- Render gallery images with tags and delete buttons --- function renderGallery() { const container = document.getElementById('gallery'); - container.innerHTML = ''; // Clear current content + container.innerHTML = ''; galleryImages.forEach((img, i) => { const div = document.createElement('div'); div.className = 'photo'; @@ -33,14 +31,14 @@ function renderGallery() { onchange="updateTags(${i}, this.value)"> `; - container.appendChild(div); // Add image div to container + container.appendChild(div); }); } -// Render hero images in the page +// --- Render hero images with delete buttons --- function renderHero() { const container = document.getElementById('hero'); - container.innerHTML = ''; // Clear current content + container.innerHTML = ''; heroImages.forEach((img, i) => { const div = document.createElement('div'); div.className = 'photo'; @@ -48,104 +46,93 @@ function renderHero() { `; - container.appendChild(div); // Add image div to container + container.appendChild(div); }); } -// Update tags for a gallery image +// --- Update tags for gallery image --- function updateTags(index, value) { - // Split tags by comma, trim spaces, and remove empty values galleryImages[index].tags = value.split(',').map(t => t.trim()).filter(t => t); } -// Delete a gallery image +// --- Delete gallery image --- async function deleteGalleryImage(index) { const img = galleryImages[index]; try { - // Send only the filename to the server for deletion const res = await fetch('/api/gallery/delete', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ src: img.src.split('/').pop() }) // Keep only filename + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ src: img.src.split('/').pop() }) }); const data = await res.json(); if (res.ok) { - // Remove image from array and re-render gallery galleryImages.splice(index, 1); renderGallery(); - await saveGallery(); // Save updated tags - } else { - alert("Error: " + data.error); - } + await saveGallery(); + } else alert("Error: " + data.error); } catch(err) { - console.error(err); - alert("Server error!"); + console.error(err); alert("Server error!"); } } -// Delete a hero image +// --- Delete hero image --- async function deleteHeroImage(index) { const img = heroImages[index]; try { - // Send only the filename to the server for deletion const res = await fetch('/api/hero/delete', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ src: img.src.split('/').pop() }) // Keep only filename + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ src: img.src.split('/').pop() }) }); const data = await res.json(); if (res.ok) { - // Remove image from array and re-render hero section heroImages.splice(index, 1); renderHero(); - await saveHero(); // Save updated hero images - } else { - alert("Error: " + data.error); - } + await saveHero(); + } else alert("Error: " + data.error); } catch(err) { - console.error(err); - alert("Server error!"); + console.error(err); alert("Server error!"); } } -// Save gallery images (with tags) to the server +// --- Save gallery to server --- async function saveGallery() { await fetch('/api/gallery/update', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: {'Content-Type': 'application/json'}, body: JSON.stringify(galleryImages) }); } -// Save hero images to the server +// --- Save hero to server --- async function saveHero() { await fetch('/api/hero/update', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: {'Content-Type': 'application/json'}, body: JSON.stringify(heroImages) }); } -// Save both gallery and hero changes +// --- Save all changes --- async function saveChanges() { await saveGallery(); await saveHero(); alert('✅ Changes saved!'); } -// Refresh gallery images from the server folder +// --- Refresh gallery from folder --- async function refreshGallery() { await fetch('/api/gallery/refresh', { method: 'POST' }); - await loadData(); // Reload data after refresh + await loadData(); alert('🔄 Gallery updated from photos/gallery folder'); } -// Refresh hero images from the server folder +// --- Refresh hero from folder --- async function refreshHero() { await fetch('/api/hero/refresh', { method: 'POST' }); - await loadData(); // Reload data after refresh + await loadData(); alert('🔄 Hero updated from photos/hero folder'); } -// Initial load of images when page opens +// --- Initialize --- loadData(); diff --git a/src/webui/js/upload.js b/src/webui/js/upload.js index 045e625..7ff8d8a 100644 --- a/src/webui/js/upload.js +++ b/src/webui/js/upload.js @@ -1,61 +1,39 @@ -// Upload handler for gallery images +// --- Upload gallery images --- document.getElementById('upload-gallery').addEventListener('change', async (e) => { - const file = e.target.files[0]; - if (!file) return; // Exit if no file is selected + const files = e.target.files; + if (!files.length) return; - // Create a FormData object to send the file const formData = new FormData(); - formData.append('file', file); // Key must match what upload.py expects + for (const file of files) formData.append('files', file); try { - // Send POST request to the gallery upload endpoint - const res = await fetch('/api/gallery/upload', { - method: 'POST', - body: formData - }); - + const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData }); const data = await res.json(); if (res.ok) { - alert('✅ Gallery image uploaded!'); - refreshGallery(); // Refresh the gallery list from the server - } else { - alert('Error: ' + data.error); // Show server error if upload failed - } - } catch (err) { - console.error(err); - alert('Server error!'); // Network or server failure - } finally { - e.target.value = ''; // Reset file input so the same file can be uploaded again if needed - } + alert(`✅ ${data.uploaded.length} gallery image(s) uploaded!`); + refreshGallery(); + } else alert('Error: ' + data.error); + } catch(err) { + console.error(err); alert('Server error!'); + } finally { e.target.value = ''; } }); -// Upload handler for hero images +// --- Upload hero images --- document.getElementById('upload-hero').addEventListener('change', async (e) => { - const file = e.target.files[0]; - if (!file) return; // Exit if no file is selected + const files = e.target.files; + if (!files.length) return; - // Create a FormData object to send the file const formData = new FormData(); - formData.append('file', file); // Key must match what upload.py expects + for (const file of files) formData.append('files', file); try { - // Send POST request to the hero upload endpoint - const res = await fetch('/api/hero/upload', { - method: 'POST', - body: formData - }); - + const res = await fetch('/api/hero/upload', { method: 'POST', body: formData }); const data = await res.json(); if (res.ok) { - alert('✅ Hero image uploaded!'); - refreshHero(); // Refresh the hero list from the server - } else { - alert('Error: ' + data.error); // Show server error if upload failed - } - } catch (err) { - console.error(err); - alert('Server error!'); // Network or server failure - } finally { - e.target.value = ''; // Reset file input so the same file can be uploaded again if needed - } + alert(`✅ ${data.uploaded.length} hero image(s) uploaded!`); + refreshHero(); + } else alert('Error: ' + data.error); + } catch(err) { + console.error(err); alert('Server error!'); + } finally { e.target.value = ''; } });