Reworked flow

This commit is contained in:
Djeex
2025-08-16 11:17:15 +02:00
parent 1b0b228273
commit 041db66b3d
5 changed files with 125 additions and 145 deletions

View File

@ -2,49 +2,64 @@ import logging
from pathlib import Path from pathlib import Path
from flask import Blueprint, request, current_app from flask import Blueprint, request, current_app
from werkzeug.utils import secure_filename 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__) upload_bp = Blueprint("upload", __name__)
# Allowed file extensions for uploads # --- Allowed file types ---
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "webp"} ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "webp"}
# Function to check if a file has an allowed extension
def allowed_file(filename: str) -> bool: 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 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): 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 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}") 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/<section>/upload", methods=["POST"]) @upload_bp.route("/api/<section>/upload", methods=["POST"])
def upload_photo(section: str): def upload_photo(section: str):
"""
Handle file uploads for gallery or hero section.
Accepts multiple files under 'files'.
"""
# Validate section # Validate section
if section not in ["gallery", "hero"]: if section not in ["gallery", "hero"]:
return {"error": "Invalid section"}, 400 return {"error": "Invalid section"}, 400
# Check if the request contains a file # Check if files are provided
if "file" not in request.files: if "files" not in request.files:
return {"error": "No file part"}, 400 return {"error": "No files provided"}, 400
file = request.files["file"]
# Check if a file was actually selected files = request.files.getlist("files")
if file.filename == "": if not files:
return {"error": "No selected file"}, 400 return {"error": "No selected files"}, 400
# Check file type and save it # Get photos directory from app config
if file and allowed_file(file.filename): PHOTOS_DIR = current_app.config.get("PHOTOS_DIR")
PHOTOS_DIR = current_app.config.get("PHOTOS_DIR") # Get base photos directory from config if not PHOTOS_DIR:
if not PHOTOS_DIR: return {"error": "Server misconfiguration"}, 500
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}
# If file type is not allowed folder = PHOTOS_DIR / section # Target folder
return {"error": "File type not allowed"}, 400 uploaded = []
# 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

View File

@ -2,111 +2,106 @@ import logging
from pathlib import Path from pathlib import Path
from flask import Flask, jsonify, request, send_from_directory, render_template from flask import Flask, jsonify, request, send_from_directory, render_template
from src.py.builder.gallery_builder import ( from src.py.builder.gallery_builder import (
GALLERY_YAML, GALLERY_DIR, HERO_DIR, GALLERY_YAML, load_yaml, save_yaml, update_gallery, update_hero
load_yaml, save_yaml, get_all_image_paths, update_gallery, update_hero
) )
from src.py.webui.upload import upload_bp from src.py.webui.upload import upload_bp
# --- Logging configuration --- # --- Logging configuration ---
# Logs messages to console with INFO level
logging.basicConfig(level=logging.INFO, format="%(message)s") logging.basicConfig(level=logging.INFO, format="%(message)s")
# --- Flask app setup --- # --- Flask app setup ---
# WEBUI_PATH points to the web UI folder (templates + static) WEBUI_PATH = Path(__file__).parents[2] / "webui" # Path to static/templates
WEBUI_PATH = Path(__file__).parents[2] / "webui"
app = Flask( app = Flask(
__name__, __name__,
template_folder=WEBUI_PATH, # where Flask looks for templates template_folder=WEBUI_PATH,
static_folder=WEBUI_PATH, # where Flask serves static files static_folder=WEBUI_PATH,
static_url_path="" # URL path prefix for static files static_url_path=""
) )
# --- Absolute photos directory --- # --- Photos directory (configurable) ---
# Used by upload.py and deletion endpoints
PHOTOS_DIR = Path(__file__).resolve().parents[3] / "config" / "photos" PHOTOS_DIR = Path(__file__).resolve().parents[3] / "config" / "photos"
app.config["PHOTOS_DIR"] = PHOTOS_DIR app.config["PHOTOS_DIR"] = PHOTOS_DIR
# --- Register upload blueprint --- # --- Register upload blueprint ---
# Handles /api/<section>/upload endpoints for gallery and hero images
app.register_blueprint(upload_bp) app.register_blueprint(upload_bp)
# --- Existing API routes --- # --- Routes ---
# Serve main page
@app.route("/") @app.route("/")
def index(): def index():
"""Serve the main HTML page."""
return render_template("index.html") return render_template("index.html")
# Get gallery images (returns JSON array)
@app.route("/api/gallery", methods=["GET"]) @app.route("/api/gallery", methods=["GET"])
def get_gallery(): def get_gallery():
"""Return JSON list of gallery images from YAML."""
data = load_yaml(GALLERY_YAML) data = load_yaml(GALLERY_YAML)
return jsonify(data.get("gallery", {}).get("images", [])) return jsonify(data.get("gallery", {}).get("images", []))
# Get hero images (returns JSON array)
@app.route("/api/hero", methods=["GET"]) @app.route("/api/hero", methods=["GET"])
def get_hero(): def get_hero():
"""Return JSON list of hero images from YAML."""
data = load_yaml(GALLERY_YAML) data = load_yaml(GALLERY_YAML)
return jsonify(data.get("hero", {}).get("images", [])) return jsonify(data.get("hero", {}).get("images", []))
# Update gallery images with new JSON data
@app.route("/api/gallery/update", methods=["POST"]) @app.route("/api/gallery/update", methods=["POST"])
def update_gallery_api(): def update_gallery_api():
"""Update gallery images in YAML from frontend JSON."""
images = request.json images = request.json
data = load_yaml(GALLERY_YAML) data = load_yaml(GALLERY_YAML)
data["gallery"]["images"] = images data["gallery"]["images"] = images
save_yaml(data, GALLERY_YAML) save_yaml(data, GALLERY_YAML)
return jsonify({"status": "ok"}) return jsonify({"status": "ok"})
# Update hero images with new JSON data
@app.route("/api/hero/update", methods=["POST"]) @app.route("/api/hero/update", methods=["POST"])
def update_hero_api(): def update_hero_api():
"""Update hero images in YAML from frontend JSON."""
images = request.json images = request.json
data = load_yaml(GALLERY_YAML) data = load_yaml(GALLERY_YAML)
data["hero"]["images"] = images data["hero"]["images"] = images
save_yaml(data, GALLERY_YAML) save_yaml(data, GALLERY_YAML)
return jsonify({"status": "ok"}) return jsonify({"status": "ok"})
# Refresh gallery from the folder (rebuild YAML)
@app.route("/api/gallery/refresh", methods=["POST"]) @app.route("/api/gallery/refresh", methods=["POST"])
def refresh_gallery(): def refresh_gallery():
"""Refresh gallery YAML from photos/gallery folder."""
update_gallery() update_gallery()
return jsonify({"status": "ok"}) return jsonify({"status": "ok"})
# Refresh hero images from the folder
@app.route("/api/hero/refresh", methods=["POST"]) @app.route("/api/hero/refresh", methods=["POST"])
def refresh_hero(): def refresh_hero():
"""Refresh hero YAML from photos/hero folder."""
update_hero() update_hero()
return jsonify({"status": "ok"}) return jsonify({"status": "ok"})
# Delete a gallery image file
@app.route("/api/gallery/delete", methods=["POST"]) @app.route("/api/gallery/delete", methods=["POST"])
def delete_gallery_photo(): def delete_gallery_photo():
"""Delete a gallery photo from disk and return status."""
data = request.json data = request.json
src = data.get("src") # filename only src = data.get("src")
file_path = PHOTOS_DIR / "gallery" / src file_path = PHOTOS_DIR / "gallery" / src
if file_path.exists(): if file_path.exists():
file_path.unlink() # remove the file file_path.unlink()
return {"status": "ok"} return {"status": "ok"}
return {"error": "File not found"}, 404 return {"error": "File not found"}, 404
# Delete a hero image file
@app.route("/api/hero/delete", methods=["POST"]) @app.route("/api/hero/delete", methods=["POST"])
def delete_hero_photo(): def delete_hero_photo():
"""Delete a hero photo from disk and return status."""
data = request.json data = request.json
src = data.get("src") # filename only src = data.get("src")
file_path = PHOTOS_DIR / "hero" / src file_path = PHOTOS_DIR / "hero" / src
if file_path.exists(): if file_path.exists():
file_path.unlink() # remove the file file_path.unlink()
return {"status": "ok"} return {"status": "ok"}
return {"error": "File not found"}, 404 return {"error": "File not found"}, 404
# Serve photos from /photos/<section>/<filename>
@app.route("/photos/<section>/<path:filename>") @app.route("/photos/<section>/<path:filename>")
def photos(section, filename): def photos(section, filename):
"""Serve uploaded photos from disk."""
return send_from_directory(PHOTOS_DIR / section, filename) return send_from_directory(PHOTOS_DIR / section, filename)
# --- Main entry point --- # --- Run server ---
if __name__ == "__main__": if __name__ == "__main__":
logging.info("Starting WebUI at http://127.0.0.1:5000") logging.info("Starting WebUI at http://127.0.0.1:5000")
app.run(debug=True) app.run(debug=True)

View File

@ -3,11 +3,14 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Photo WebUI</title> <title>Photo WebUI</title>
<!-- Link to your CSS in the package -->
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
</head> </head>
<body> <body>
<h1>Photo WebUI</h1> <h1>Photo WebUI</h1>
<!-- Toolbar with refresh and save buttons -->
<div class="toolbar"> <div class="toolbar">
<button onclick="refreshGallery()">🔄 Refresh Gallery</button> <button onclick="refreshGallery()">🔄 Refresh Gallery</button>
<button onclick="refreshHero()">🔄 Refresh Hero</button> <button onclick="refreshHero()">🔄 Refresh Hero</button>
@ -19,7 +22,7 @@
<h2>Gallery</h2> <h2>Gallery</h2>
<label> <label>
Upload Image: Upload Image:
<input type="file" id="upload-gallery" accept=".png,.jpg,.jpeg,.webp"> <input type="file" id="upload-gallery" accept=".png,.jpg,.jpeg,.webp" multiple>
</label> </label>
<div id="gallery"></div> <div id="gallery"></div>
</div> </div>
@ -29,10 +32,12 @@
<h2>Hero</h2> <h2>Hero</h2>
<label> <label>
Upload Image: Upload Image:
<input type="file" id="upload-hero" accept=".png,.jpg,.jpeg,.webp"> <input type="file" id="upload-hero" accept=".png,.jpg,.jpeg,.webp" multiple>
</label> </label>
<div id="hero"></div> <div id="hero"></div>
</div> </div>
<!-- JS files for rendering, uploading, and actions -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script> <script src="{{ url_for('static', filename='js/main.js') }}"></script>
<script src="{{ url_for('static', filename='js/upload.js') }}"></script> <script src="{{ url_for('static', filename='js/upload.js') }}"></script>
</body> </body>

View File

@ -1,29 +1,27 @@
// Arrays to store gallery and hero images // --- Arrays to store gallery and hero images ---
let galleryImages = []; let galleryImages = [];
let heroImages = []; let heroImages = [];
// Load images data from server // --- Load images from server on page load ---
async function loadData() { async function loadData() {
try { try {
// Fetch gallery images from API
const galleryRes = await fetch('/api/gallery'); const galleryRes = await fetch('/api/gallery');
galleryImages = await galleryRes.json(); galleryImages = await galleryRes.json();
renderGallery(); // Render gallery images renderGallery();
// Fetch hero images from API
const heroRes = await fetch('/api/hero'); const heroRes = await fetch('/api/hero');
heroImages = await heroRes.json(); heroImages = await heroRes.json();
renderHero(); // Render hero images renderHero();
} catch(err) { } catch(err) {
console.error(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() { function renderGallery() {
const container = document.getElementById('gallery'); const container = document.getElementById('gallery');
container.innerHTML = ''; // Clear current content container.innerHTML = '';
galleryImages.forEach((img, i) => { galleryImages.forEach((img, i) => {
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'photo'; div.className = 'photo';
@ -33,14 +31,14 @@ function renderGallery() {
onchange="updateTags(${i}, this.value)"> onchange="updateTags(${i}, this.value)">
<button onclick="deleteGalleryImage(${i})">🗑 Delete</button> <button onclick="deleteGalleryImage(${i})">🗑 Delete</button>
`; `;
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() { function renderHero() {
const container = document.getElementById('hero'); const container = document.getElementById('hero');
container.innerHTML = ''; // Clear current content container.innerHTML = '';
heroImages.forEach((img, i) => { heroImages.forEach((img, i) => {
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'photo'; div.className = 'photo';
@ -48,104 +46,93 @@ function renderHero() {
<img src="/photos/${img.src}"> <img src="/photos/${img.src}">
<button onclick="deleteHeroImage(${i})">🗑 Delete</button> <button onclick="deleteHeroImage(${i})">🗑 Delete</button>
`; `;
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) { 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); galleryImages[index].tags = value.split(',').map(t => t.trim()).filter(t => t);
} }
// Delete a gallery image // --- Delete gallery image ---
async function deleteGalleryImage(index) { async function deleteGalleryImage(index) {
const img = galleryImages[index]; const img = galleryImages[index];
try { try {
// Send only the filename to the server for deletion
const res = await fetch('/api/gallery/delete', { const res = await fetch('/api/gallery/delete', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ src: img.src.split('/').pop() }) // Keep only filename body: JSON.stringify({ src: img.src.split('/').pop() })
}); });
const data = await res.json(); const data = await res.json();
if (res.ok) { if (res.ok) {
// Remove image from array and re-render gallery
galleryImages.splice(index, 1); galleryImages.splice(index, 1);
renderGallery(); renderGallery();
await saveGallery(); // Save updated tags await saveGallery();
} else { } else alert("Error: " + data.error);
alert("Error: " + data.error);
}
} catch(err) { } catch(err) {
console.error(err); console.error(err); alert("Server error!");
alert("Server error!");
} }
} }
// Delete a hero image // --- Delete hero image ---
async function deleteHeroImage(index) { async function deleteHeroImage(index) {
const img = heroImages[index]; const img = heroImages[index];
try { try {
// Send only the filename to the server for deletion
const res = await fetch('/api/hero/delete', { const res = await fetch('/api/hero/delete', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ src: img.src.split('/').pop() }) // Keep only filename body: JSON.stringify({ src: img.src.split('/').pop() })
}); });
const data = await res.json(); const data = await res.json();
if (res.ok) { if (res.ok) {
// Remove image from array and re-render hero section
heroImages.splice(index, 1); heroImages.splice(index, 1);
renderHero(); renderHero();
await saveHero(); // Save updated hero images await saveHero();
} else { } else alert("Error: " + data.error);
alert("Error: " + data.error);
}
} catch(err) { } catch(err) {
console.error(err); console.error(err); alert("Server error!");
alert("Server error!");
} }
} }
// Save gallery images (with tags) to the server // --- Save gallery to server ---
async function saveGallery() { async function saveGallery() {
await fetch('/api/gallery/update', { await fetch('/api/gallery/update', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {'Content-Type': 'application/json'},
body: JSON.stringify(galleryImages) body: JSON.stringify(galleryImages)
}); });
} }
// Save hero images to the server // --- Save hero to server ---
async function saveHero() { async function saveHero() {
await fetch('/api/hero/update', { await fetch('/api/hero/update', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {'Content-Type': 'application/json'},
body: JSON.stringify(heroImages) body: JSON.stringify(heroImages)
}); });
} }
// Save both gallery and hero changes // --- Save all changes ---
async function saveChanges() { async function saveChanges() {
await saveGallery(); await saveGallery();
await saveHero(); await saveHero();
alert('✅ Changes saved!'); alert('✅ Changes saved!');
} }
// Refresh gallery images from the server folder // --- Refresh gallery from folder ---
async function refreshGallery() { async function refreshGallery() {
await fetch('/api/gallery/refresh', { method: 'POST' }); await fetch('/api/gallery/refresh', { method: 'POST' });
await loadData(); // Reload data after refresh await loadData();
alert('🔄 Gallery updated from photos/gallery folder'); alert('🔄 Gallery updated from photos/gallery folder');
} }
// Refresh hero images from the server folder // --- Refresh hero from folder ---
async function refreshHero() { async function refreshHero() {
await fetch('/api/hero/refresh', { method: 'POST' }); await fetch('/api/hero/refresh', { method: 'POST' });
await loadData(); // Reload data after refresh await loadData();
alert('🔄 Hero updated from photos/hero folder'); alert('🔄 Hero updated from photos/hero folder');
} }
// Initial load of images when page opens // --- Initialize ---
loadData(); loadData();

View File

@ -1,61 +1,39 @@
// Upload handler for gallery images // --- Upload gallery images ---
document.getElementById('upload-gallery').addEventListener('change', async (e) => { document.getElementById('upload-gallery').addEventListener('change', async (e) => {
const file = e.target.files[0]; const files = e.target.files;
if (!file) return; // Exit if no file is selected if (!files.length) return;
// Create a FormData object to send the file
const formData = new FormData(); 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 { 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(); const data = await res.json();
if (res.ok) { if (res.ok) {
alert('✅ Gallery image uploaded!'); alert(`${data.uploaded.length} gallery image(s) uploaded!`);
refreshGallery(); // Refresh the gallery list from the server refreshGallery();
} else { } else alert('Error: ' + data.error);
alert('Error: ' + data.error); // Show server error if upload failed } catch(err) {
} console.error(err); alert('Server error!');
} catch (err) { } finally { e.target.value = ''; }
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
}
}); });
// Upload handler for hero images // --- Upload hero images ---
document.getElementById('upload-hero').addEventListener('change', async (e) => { document.getElementById('upload-hero').addEventListener('change', async (e) => {
const file = e.target.files[0]; const files = e.target.files;
if (!file) return; // Exit if no file is selected if (!files.length) return;
// Create a FormData object to send the file
const formData = new FormData(); 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 { 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(); const data = await res.json();
if (res.ok) { if (res.ok) {
alert('✅ Hero image uploaded!'); alert(`${data.uploaded.length} hero image(s) uploaded!`);
refreshHero(); // Refresh the hero list from the server refreshHero();
} else { } else alert('Error: ' + data.error);
alert('Error: ' + data.error); // Show server error if upload failed } catch(err) {
} console.error(err); alert('Server error!');
} catch (err) { } finally { e.target.value = ''; }
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
}
}); });