2.0 - WebUI builder ("Cielight" merge) #9
@ -1,7 +1,14 @@
|
||||
# --- Imports ---
|
||||
import logging
|
||||
import yaml
|
||||
import subprocess
|
||||
import zipfile
|
||||
import os
|
||||
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,
|
||||
send_file, after_this_request
|
||||
)
|
||||
from src.py.builder.gallery_builder import (
|
||||
GALLERY_YAML, load_yaml, save_yaml, update_gallery, update_hero
|
||||
)
|
||||
@ -19,61 +26,64 @@ app = Flask(
|
||||
static_url_path=""
|
||||
)
|
||||
|
||||
# --- Config paths ---
|
||||
SITE_YAML = Path(__file__).resolve().parents[3] / "config" / "site.yaml"
|
||||
|
||||
# --- Photos directory (configurable) ---
|
||||
PHOTOS_DIR = Path(__file__).resolve().parents[3] / "config" / "photos"
|
||||
app.config["PHOTOS_DIR"] = PHOTOS_DIR
|
||||
|
||||
# --- Register upload blueprint ---
|
||||
app.register_blueprint(upload_bp)
|
||||
|
||||
# --- Helper functions for theme editor ---
|
||||
# --- Theme editor helper functions ---
|
||||
def get_theme_name():
|
||||
"""Get current theme name from site.yaml."""
|
||||
site_yaml_path = Path(__file__).resolve().parents[3] / "config" / "site.yaml"
|
||||
with open(site_yaml_path, "r") as f:
|
||||
site_yaml = yaml.safe_load(f)
|
||||
return site_yaml.get("build", {}).get("theme", "modern")
|
||||
|
||||
def get_theme_yaml(theme_name):
|
||||
"""Load theme.yaml for a given theme."""
|
||||
theme_yaml_path = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "theme.yaml"
|
||||
with open(theme_yaml_path, "r") as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
def save_theme_yaml(theme_name, theme_yaml):
|
||||
"""Save theme.yaml for a given theme."""
|
||||
theme_yaml_path = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "theme.yaml"
|
||||
with open(theme_yaml_path, "w") as f:
|
||||
yaml.safe_dump(theme_yaml, f, sort_keys=False, allow_unicode=True)
|
||||
|
||||
def get_local_fonts(theme_name):
|
||||
"""List local font files for a theme."""
|
||||
fonts_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts"
|
||||
if not fonts_dir.exists():
|
||||
return []
|
||||
# Return full filenames, not just stem
|
||||
return [f.name for f in fonts_dir.glob("*") if f.is_file() and f.suffix in [".woff", ".woff2"]]
|
||||
|
||||
# --- Routes ---
|
||||
# --- ROUTES ---
|
||||
|
||||
# --- Main page ---
|
||||
@app.route("/")
|
||||
def index():
|
||||
"""Serve the main HTML page."""
|
||||
return render_template("index.html")
|
||||
|
||||
# --- Gallery & Hero API ---
|
||||
@app.route("/api/gallery", methods=["GET"])
|
||||
def get_gallery():
|
||||
"""Return JSON list of gallery images from YAML."""
|
||||
"""Get gallery images."""
|
||||
data = load_yaml(GALLERY_YAML)
|
||||
return jsonify(data.get("gallery", {}).get("images", []))
|
||||
|
||||
@app.route("/api/hero", methods=["GET"])
|
||||
def get_hero():
|
||||
"""Return JSON list of hero images from YAML."""
|
||||
"""Get hero images."""
|
||||
data = load_yaml(GALLERY_YAML)
|
||||
return jsonify(data.get("hero", {}).get("images", []))
|
||||
|
||||
@app.route("/api/gallery/update", methods=["POST"])
|
||||
def update_gallery_api():
|
||||
"""Update gallery images in YAML from frontend JSON."""
|
||||
"""Update gallery images."""
|
||||
images = request.json
|
||||
data = load_yaml(GALLERY_YAML)
|
||||
data["gallery"]["images"] = images
|
||||
@ -82,7 +92,7 @@ def update_gallery_api():
|
||||
|
||||
@app.route("/api/hero/update", methods=["POST"])
|
||||
def update_hero_api():
|
||||
"""Update hero images in YAML from frontend JSON."""
|
||||
"""Update hero images."""
|
||||
images = request.json
|
||||
data = load_yaml(GALLERY_YAML)
|
||||
data["hero"]["images"] = images
|
||||
@ -91,49 +101,48 @@ def update_hero_api():
|
||||
|
||||
@app.route("/api/gallery/refresh", methods=["POST"])
|
||||
def refresh_gallery():
|
||||
"""Refresh gallery YAML from photos/gallery folder."""
|
||||
"""Refresh gallery images from disk."""
|
||||
update_gallery()
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
@app.route("/api/hero/refresh", methods=["POST"])
|
||||
def refresh_hero():
|
||||
"""Refresh hero YAML from photos/hero folder."""
|
||||
"""Refresh hero images from disk."""
|
||||
update_hero()
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
# --- Gallery & Hero photo deletion ---
|
||||
@app.route("/api/gallery/delete", methods=["POST"])
|
||||
def delete_gallery_photo():
|
||||
"""Delete a gallery photo from disk and return status."""
|
||||
"""Delete a gallery photo."""
|
||||
data = request.json
|
||||
src = data.get("src")
|
||||
file_path = PHOTOS_DIR / "gallery" / src
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
return {"status": "ok"}
|
||||
return {"error": "File not found"}, 404
|
||||
return {"error": "❌ File not found"}, 404
|
||||
|
||||
@app.route("/api/hero/delete", methods=["POST"])
|
||||
def delete_hero_photo():
|
||||
"""Delete a hero photo from disk and return status."""
|
||||
"""Delete a hero photo."""
|
||||
data = request.json
|
||||
src = data.get("src")
|
||||
file_path = PHOTOS_DIR / "hero" / src
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
return {"status": "ok"}
|
||||
return {"error": "File not found"}, 404
|
||||
return {"error": "❌ File not found"}, 404
|
||||
|
||||
@app.route("/api/gallery/delete_all", methods=["POST"])
|
||||
def delete_all_gallery_photos():
|
||||
"""Delete all gallery photos from disk and YAML."""
|
||||
"""Delete all gallery photos."""
|
||||
gallery_dir = PHOTOS_DIR / "gallery"
|
||||
deleted = 0
|
||||
# Remove all files in gallery folder
|
||||
for file in gallery_dir.glob("*"):
|
||||
if file.is_file():
|
||||
file.unlink()
|
||||
deleted += 1
|
||||
# Clear YAML gallery images
|
||||
data = load_yaml(GALLERY_YAML)
|
||||
data["gallery"]["images"] = []
|
||||
save_yaml(data, GALLERY_YAML)
|
||||
@ -141,68 +150,69 @@ def delete_all_gallery_photos():
|
||||
|
||||
@app.route("/api/hero/delete_all", methods=["POST"])
|
||||
def delete_all_hero_photos():
|
||||
"""Delete all hero photos from disk and YAML."""
|
||||
"""Delete all hero photos."""
|
||||
hero_dir = PHOTOS_DIR / "hero"
|
||||
deleted = 0
|
||||
# Remove all files in hero folder
|
||||
for file in hero_dir.glob("*"):
|
||||
if file.is_file():
|
||||
file.unlink()
|
||||
deleted += 1
|
||||
# Clear YAML hero images
|
||||
data = load_yaml(GALLERY_YAML)
|
||||
data["hero"]["images"] = []
|
||||
save_yaml(data, GALLERY_YAML)
|
||||
return jsonify({"status": "ok", "deleted": deleted})
|
||||
|
||||
# --- Serve photos ---
|
||||
@app.route("/photos/<section>/<path:filename>")
|
||||
def photos(section, filename):
|
||||
"""Serve uploaded photos from disk for a specific section."""
|
||||
"""Serve a photo from a section."""
|
||||
return send_from_directory(PHOTOS_DIR / section, filename)
|
||||
|
||||
@app.route("/photos/<path:filename>")
|
||||
def serve_photo(filename):
|
||||
"""Serve uploaded photos from disk (generic)."""
|
||||
"""Serve a photo from the photos directory."""
|
||||
photos_dir = Path(__file__).resolve().parents[3] / "config" / "photos"
|
||||
return send_from_directory(photos_dir, filename)
|
||||
|
||||
# --- Site info page & API ---
|
||||
@app.route("/site-info")
|
||||
def site_info():
|
||||
"""Serve the site info editor page."""
|
||||
"""Render site info editor page."""
|
||||
return render_template("site-info/index.html")
|
||||
|
||||
@app.route("/api/site-info", methods=["GET"])
|
||||
def get_site_info():
|
||||
"""Return the site info YAML as JSON."""
|
||||
"""Get site info YAML as JSON."""
|
||||
with open(SITE_YAML, "r") as f:
|
||||
data = yaml.safe_load(f)
|
||||
return jsonify(data)
|
||||
|
||||
@app.route("/api/site-info", methods=["POST"])
|
||||
def update_site_info():
|
||||
"""Update the site info YAML from frontend JSON."""
|
||||
"""Update site info YAML."""
|
||||
data = request.json
|
||||
with open(SITE_YAML, "w") as f:
|
||||
yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
# --- Theme management ---
|
||||
@app.route("/api/themes")
|
||||
def list_themes():
|
||||
"""List available themes (folders in config/themes)."""
|
||||
"""List available themes."""
|
||||
themes_dir = Path(__file__).resolve().parents[3] / "config" / "themes"
|
||||
themes = [d.name for d in themes_dir.iterdir() if d.is_dir()]
|
||||
return jsonify(themes)
|
||||
|
||||
# --- Thumbnail upload/remove ---
|
||||
@app.route("/api/thumbnail/upload", methods=["POST"])
|
||||
def upload_thumbnail():
|
||||
"""Upload a thumbnail image and update site.yaml."""
|
||||
"""Upload thumbnail image and update site.yaml."""
|
||||
PHOTOS_DIR = app.config["PHOTOS_DIR"]
|
||||
file = request.files.get("file")
|
||||
if not file:
|
||||
return {"error": "No file provided"}, 400
|
||||
return {"error": "❌ No file provided"}, 400
|
||||
filename = "thumbnail.png"
|
||||
file.save(PHOTOS_DIR / filename)
|
||||
# Update site.yaml
|
||||
with open(SITE_YAML, "r") as f:
|
||||
data = yaml.safe_load(f)
|
||||
data.setdefault("social", {})["thumbnail"] = filename
|
||||
@ -212,13 +222,11 @@ def upload_thumbnail():
|
||||
|
||||
@app.route("/api/thumbnail/remove", methods=["POST"])
|
||||
def remove_thumbnail():
|
||||
"""Remove the thumbnail image and update site.yaml."""
|
||||
"""Remove thumbnail image and update site.yaml."""
|
||||
PHOTOS_DIR = app.config["PHOTOS_DIR"]
|
||||
thumbnail_path = PHOTOS_DIR / "thumbnail.png"
|
||||
# Remove thumbnail file if exists
|
||||
if thumbnail_path.exists():
|
||||
thumbnail_path.unlink()
|
||||
# Update site.yaml to remove thumbnail key
|
||||
with open(SITE_YAML, "r") as f:
|
||||
data = yaml.safe_load(f)
|
||||
if "social" in data and "thumbnail" in data["social"]:
|
||||
@ -227,14 +235,14 @@ def remove_thumbnail():
|
||||
yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
# --- Theme upload ---
|
||||
@app.route("/api/theme/upload", methods=["POST"])
|
||||
def upload_theme():
|
||||
"""Upload a custom theme folder and save it in config/themes."""
|
||||
"""Upload a custom theme folder."""
|
||||
themes_dir = Path(__file__).resolve().parents[3] / "config" / "themes"
|
||||
files = request.files.getlist("files")
|
||||
if not files:
|
||||
return jsonify({"error": "No files provided"}), 400
|
||||
# Get folder name from first file's webkitRelativePath
|
||||
return jsonify({"error": "❌ No files provided"}), 400
|
||||
first_path = files[0].filename
|
||||
folder_name = first_path.split("/")[0] if "/" in first_path else "custom"
|
||||
theme_folder = themes_dir / folder_name
|
||||
@ -246,14 +254,15 @@ def upload_theme():
|
||||
file.save(dest_path)
|
||||
return jsonify({"status": "ok", "theme": folder_name})
|
||||
|
||||
# --- Theme Editor API ---
|
||||
# --- Theme editor page & API ---
|
||||
@app.route("/theme-editor")
|
||||
def theme_editor():
|
||||
"""Serve the theme editor page."""
|
||||
"""Render theme editor page."""
|
||||
return render_template("theme-editor/index.html")
|
||||
|
||||
@app.route("/api/theme-info", methods=["GET", "POST"])
|
||||
def api_theme_info():
|
||||
"""Get or update theme.yaml for current theme."""
|
||||
theme_name = get_theme_name()
|
||||
if request.method == "GET":
|
||||
theme_yaml = get_theme_yaml(theme_name)
|
||||
@ -272,25 +281,25 @@ def api_theme_info():
|
||||
|
||||
@app.route("/api/local-fonts")
|
||||
def api_local_fonts():
|
||||
"""List local fonts for a theme."""
|
||||
theme_name = request.args.get("theme")
|
||||
fonts = get_local_fonts(theme_name)
|
||||
return jsonify(fonts)
|
||||
|
||||
|
||||
# --- Favicon upload/remove ---
|
||||
@app.route("/api/favicon/upload", methods=["POST"])
|
||||
def upload_favicon():
|
||||
"""Upload favicon to theme folder and update theme.yaml."""
|
||||
"""Upload favicon for a theme."""
|
||||
theme_name = request.form.get("theme")
|
||||
file = request.files.get("file")
|
||||
if not file or not theme_name:
|
||||
return jsonify({"error": "Missing file or theme"}), 400
|
||||
return jsonify({"error": "❌ Missing file or theme"}), 400
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
if ext not in [".png", ".jpg", ".jpeg", ".ico"]:
|
||||
return jsonify({"error": "Invalid file type"}), 400
|
||||
return jsonify({"error": "❌ Invalid file type"}), 400
|
||||
filename = "favicon" + ext
|
||||
theme_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name
|
||||
file.save(theme_dir / filename)
|
||||
# Update theme.yaml
|
||||
theme_yaml_path = theme_dir / "theme.yaml"
|
||||
with open(theme_yaml_path, "r") as f:
|
||||
theme_yaml = yaml.safe_load(f)
|
||||
@ -301,18 +310,16 @@ def upload_favicon():
|
||||
|
||||
@app.route("/api/favicon/remove", methods=["POST"])
|
||||
def remove_favicon():
|
||||
"""Remove favicon from theme folder and update theme.yaml."""
|
||||
"""Remove favicon for a theme."""
|
||||
data = request.get_json()
|
||||
theme_name = data.get("theme")
|
||||
if not theme_name:
|
||||
return jsonify({"error": "Missing theme"}), 400
|
||||
return jsonify({"error": "❌ Missing theme"}), 400
|
||||
theme_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name
|
||||
# Remove favicon file
|
||||
for ext in [".png", ".jpg", ".jpeg", ".ico"]:
|
||||
favicon_path = theme_dir / f"favicon{ext}"
|
||||
if favicon_path.exists():
|
||||
favicon_path.unlink()
|
||||
# Update theme.yaml
|
||||
theme_yaml_path = theme_dir / "theme.yaml"
|
||||
with open(theme_yaml_path, "r") as f:
|
||||
theme_yaml = yaml.safe_load(f)
|
||||
@ -322,21 +329,24 @@ def remove_favicon():
|
||||
yaml.safe_dump(theme_yaml, f, sort_keys=False, allow_unicode=True)
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
# --- Serve theme assets ---
|
||||
@app.route("/themes/<theme>/<filename>")
|
||||
def serve_theme_asset(theme, filename):
|
||||
"""Serve a theme asset file."""
|
||||
theme_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme
|
||||
return send_from_directory(theme_dir, filename)
|
||||
|
||||
# --- Font upload/remove ---
|
||||
@app.route("/api/font/upload", methods=["POST"])
|
||||
def upload_font():
|
||||
"""Upload a font file to the theme's fonts folder (only .woff/.woff2 allowed)."""
|
||||
"""Upload a font file for a theme."""
|
||||
theme_name = request.form.get("theme")
|
||||
file = request.files.get("file")
|
||||
if not file or not theme_name:
|
||||
return jsonify({"error": "Missing file or theme"}), 400
|
||||
return jsonify({"error": "❌ Missing theme or font"}), 400
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
if ext not in [".woff", ".woff2"]:
|
||||
return jsonify({"error": "Invalid font file type"}), 400
|
||||
return jsonify({"error": "❌ Invalid font file type"}), 400
|
||||
fonts_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts"
|
||||
fonts_dir.mkdir(parents=True, exist_ok=True)
|
||||
file.save(fonts_dir / file.filename)
|
||||
@ -344,18 +354,90 @@ def upload_font():
|
||||
|
||||
@app.route("/api/font/remove", methods=["POST"])
|
||||
def remove_font():
|
||||
"""Remove a font file from the theme's fonts folder."""
|
||||
"""Remove a font file for a theme."""
|
||||
data = request.get_json()
|
||||
theme_name = data.get("theme")
|
||||
font = data.get("font")
|
||||
if not theme_name or not font:
|
||||
return jsonify({"error": "Missing theme or font"}), 400
|
||||
return jsonify({"error": "❌ Missing theme or font"}), 400
|
||||
fonts_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts"
|
||||
font_path = fonts_dir / font
|
||||
if font_path.exists():
|
||||
font_path.unlink()
|
||||
return jsonify({"status": "ok"})
|
||||
return jsonify({"error": "Font not found"}), 404
|
||||
return jsonify({"error": "❌ Font not found"}), 404
|
||||
|
||||
# --- Build & Download ZIP ---
|
||||
@app.route("/api/build", methods=["POST"])
|
||||
def trigger_build():
|
||||
"""
|
||||
Validate site.yaml and run build.py.
|
||||
Does NOT create zip here; zip is created on demand in download route.
|
||||
"""
|
||||
site_yaml_path = Path(__file__).resolve().parents[3] / "config" / "site.yaml"
|
||||
output_folder = Path(__file__).resolve().parents[3] / "output"
|
||||
|
||||
if not site_yaml_path.exists():
|
||||
return jsonify({"status": "error", "message": "❌ site.yaml not found"}), 400
|
||||
|
||||
with open(site_yaml_path, "r") as f:
|
||||
site_data = yaml.safe_load(f) or {}
|
||||
|
||||
# Dynamically check all main sections and nested keys
|
||||
main_sections = list(site_data.keys())
|
||||
for section in main_sections:
|
||||
value = site_data.get(section)
|
||||
if not value:
|
||||
return jsonify({"status": "error", "message": f"❌ Site info are not set: missing {section}"}), 400
|
||||
if isinstance(value, dict):
|
||||
for k, v in value.items():
|
||||
if v is None or v == "" or (isinstance(v, list) and not v):
|
||||
return jsonify({"status": "error", "message": f"❌ Site info are not set: missing {section}.{k}"}), 400
|
||||
elif isinstance(value, list):
|
||||
if not value:
|
||||
return jsonify({"status": "error", "message": f"❌ Site info are not set: missing {section}"}), 400
|
||||
for idx, item in enumerate(value):
|
||||
if isinstance(item, dict):
|
||||
for k, v in item.items():
|
||||
if v is None or v == "" or (isinstance(v, list) and not v):
|
||||
return jsonify({"status": "error", "message": f"❌ Site info are not set: missing {section}[{idx}].{k}"}), 400
|
||||
elif item is None or item == "":
|
||||
return jsonify({"status": "error", "message": f"❌ Site info are not set: missing {section}[{idx}]"}), 400
|
||||
else:
|
||||
if value is None or value == "":
|
||||
return jsonify({"status": "error", "message": f"❌ Site info are not set: missing {section}"}), 400
|
||||
|
||||
try:
|
||||
subprocess.run(["python3", "build.py"], check=True)
|
||||
return jsonify({"status": "ok"})
|
||||
except Exception as e:
|
||||
return jsonify({"status": "error", "message": f"❌ {str(e)}"}), 500
|
||||
|
||||
@app.route("/download-output-zip", methods=["POST"])
|
||||
def download_output_zip():
|
||||
"""
|
||||
Create output zip on demand and send it to the user.
|
||||
Zip is deleted after sending.
|
||||
"""
|
||||
output_folder = Path(__file__).resolve().parents[3] / "output"
|
||||
zip_path = Path(__file__).resolve().parents[3] / "site_output.zip" # Store in lumeex/ root
|
||||
|
||||
# Create zip on demand
|
||||
with zipfile.ZipFile(zip_path, "w") as zipf:
|
||||
for root, dirs, files in os.walk(output_folder):
|
||||
for file in files:
|
||||
file_path = Path(root) / file
|
||||
zipf.write(file_path, file_path.relative_to(output_folder))
|
||||
|
||||
@after_this_request
|
||||
def remove_file(response):
|
||||
try:
|
||||
os.remove(zip_path)
|
||||
except Exception:
|
||||
pass
|
||||
return response
|
||||
|
||||
return send_file(zip_path, as_attachment=True)
|
||||
|
||||
# --- Run server ---
|
||||
if __name__ == "__main__":
|
||||
|
@ -10,12 +10,6 @@
|
||||
<!-- Top bar -->
|
||||
<div class="nav-bar">
|
||||
<div class="content-inner nav">
|
||||
<div class="nav-cta">
|
||||
<div class="arrow">→</div>
|
||||
<a class="button" href="#" target="_blank">
|
||||
<span id="step">🚀 Build !<i class="fa-solid fa-envelope"></i></span>
|
||||
</a>
|
||||
</div>
|
||||
<input type="checkbox" id="nav-check">
|
||||
<div class="nav-header">
|
||||
<div class="nav-title">
|
||||
@ -31,9 +25,12 @@
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<ul class="nav-list">
|
||||
<li class="nav-item appear2"><a href="/site-info">Site info</a></li>
|
||||
<li class="nav-item appear2"><a href="/theme-editor">Theme info</a></li>
|
||||
<li class="nav-item appear2"><a href="#">Gallery</a>
|
||||
<li class="nav-item"><a href="/gallery-editor">Gallery</a>
|
||||
<li class="nav-item"><a href="/site-info">Site info</a></li>
|
||||
<li class="nav-item"><a href="/theme-editor">Theme info</a></li>
|
||||
<li class="nav-item">
|
||||
<button id="build-btn" class="button">Build !</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -73,6 +70,7 @@
|
||||
</div>
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/upload.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/build.js') }}" defer></script>
|
||||
</div>
|
||||
<!-- Delete confirmation modal -->
|
||||
<div id="delete-modal" class="modal" style="display:none;">
|
||||
@ -86,5 +84,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Build success modal -->
|
||||
<div id="build-success-modal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<span id="build-success-modal-close" class="modal-close">×</span>
|
||||
<h3>✅ Build completed!</h3>
|
||||
<p>Your files are available in the output folder.</p>
|
||||
<button id="download-zip-btn" class="modal-btn">Download ZIP</button>
|
||||
<div id="zip-loader" style="display:none;">Creating ZIP...</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
81
src/webui/js/build.js
Normal file
81
src/webui/js/build.js
Normal file
@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Show a toast notification.
|
||||
* @param {string} message - The message to display.
|
||||
* @param {string} type - "success" or "error".
|
||||
* @param {number} duration - Duration in ms.
|
||||
*/
|
||||
function showToast(message, type = "success", duration = 3000) {
|
||||
const container = document.getElementById("toast-container");
|
||||
if (!container) return;
|
||||
const toast = document.createElement("div");
|
||||
toast.className = `toast ${type}`;
|
||||
toast.textContent = message;
|
||||
container.appendChild(toast);
|
||||
requestAnimationFrame(() => toast.classList.add("show"));
|
||||
setTimeout(() => {
|
||||
toast.classList.remove("show");
|
||||
setTimeout(() => container.removeChild(toast), 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Get build button and modal elements
|
||||
const buildBtn = document.getElementById("build-btn");
|
||||
const buildModal = document.getElementById("build-success-modal");
|
||||
const buildModalClose = document.getElementById("build-success-modal-close");
|
||||
const downloadZipBtn = document.getElementById("download-zip-btn");
|
||||
const zipLoader = document.getElementById("zip-loader");
|
||||
|
||||
// Handle build button click
|
||||
if (buildBtn) {
|
||||
buildBtn.addEventListener("click", async () => {
|
||||
// Trigger build on backend
|
||||
const res = await fetch("/api/build", { method: "POST" });
|
||||
const result = await res.json();
|
||||
if (result.status === "ok") {
|
||||
// Show build success modal
|
||||
if (buildModal) buildModal.style.display = "flex";
|
||||
} else {
|
||||
showToast(result.message || "❌ Build failed!", "error");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle download zip button click
|
||||
if (downloadZipBtn) {
|
||||
downloadZipBtn.addEventListener("click", async () => {
|
||||
if (zipLoader) zipLoader.style.display = "block";
|
||||
downloadZipBtn.disabled = true;
|
||||
|
||||
// Request zip creation and download from backend
|
||||
const res = await fetch("/download-output-zip", { method: "POST" });
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "site_output.zip";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} else {
|
||||
showToast("❌ Error creating ZIP", "error");
|
||||
}
|
||||
if (zipLoader) zipLoader.style.display = "none";
|
||||
downloadZipBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Modal close logic
|
||||
if (buildModal && buildModalClose) {
|
||||
buildModalClose.onclick = () => {
|
||||
buildModal.style.display = "none";
|
||||
};
|
||||
window.onclick = function(event) {
|
||||
if (event.target === buildModal) {
|
||||
buildModal.style.display = "none";
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
@ -63,7 +63,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
div.style.gap = "8px";
|
||||
div.style.marginBottom = "6px";
|
||||
div.innerHTML = `
|
||||
<textarea placeholder="Paragraph" style="flex:1;" data-idx="${idx}">${item.paragraph || ""}</textarea>
|
||||
<textarea placeholder="Paragraph" required style="flex:1;" data-idx="${idx}">${item.paragraph || ""}</textarea>
|
||||
<button type="button" class="remove-ip-paragraph" data-idx="${idx}">🗑</button>
|
||||
`;
|
||||
ipList.appendChild(div);
|
||||
@ -129,9 +129,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
if (result.status === "ok") {
|
||||
if (thumbnailInput) thumbnailInput.value = result.filename;
|
||||
updateThumbnailPreview(`/photos/${result.filename}?t=${Date.now()}`);
|
||||
showToast("Thumbnail uploaded!", "success");
|
||||
showToast("✅ Thumbnail uploaded!", "success");
|
||||
} else {
|
||||
showToast("Error uploading thumbnail", "error");
|
||||
showToast("❌ Error uploading thumbnail", "error");
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -159,9 +159,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
if (result.status === "ok") {
|
||||
if (thumbnailInput) thumbnailInput.value = "";
|
||||
updateThumbnailPreview("");
|
||||
showToast("Thumbnail removed!", "success");
|
||||
showToast("✅ Thumbnail removed!", "success");
|
||||
} else {
|
||||
showToast("Error removing thumbnail", "error");
|
||||
showToast("❌ Error removing thumbnail", "error");
|
||||
}
|
||||
deleteModal.style.display = "none";
|
||||
};
|
||||
@ -182,7 +182,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const res = await fetch("/api/theme/upload", { method: "POST", body: formData });
|
||||
const result = await res.json();
|
||||
if (result.status === "ok") {
|
||||
showToast("Theme uploaded!", "success");
|
||||
showToast("✅ Theme uploaded!", "success");
|
||||
// Refresh theme select after upload
|
||||
fetch("/api/themes")
|
||||
.then(res => res.json())
|
||||
@ -196,7 +196,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
showToast("Error uploading theme", "error");
|
||||
showToast("❌ Error uploading theme", "error");
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -311,6 +311,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
updateMenuItemsFromInputs();
|
||||
updateIpParagraphsFromInputs();
|
||||
|
||||
// Check if thumbnail is set before saving (uploaded or present in input)
|
||||
if (!thumbnailInput || !thumbnailInput.value) {
|
||||
showToast("❌ Thumbnail is required.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const build = {
|
||||
theme: themeSelect ? themeSelect.value : "",
|
||||
convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked),
|
||||
|
@ -10,12 +10,6 @@
|
||||
<!-- Top bar -->
|
||||
<div class="nav-bar">
|
||||
<div class="content-inner nav">
|
||||
<div class="nav-cta">
|
||||
<div class="arrow">→</div>
|
||||
<a class="button" href="#" target="_blank">
|
||||
<span id="step">🚀 Build !<i class="fa-solid fa-envelope"></i></span>
|
||||
</a>
|
||||
</div>
|
||||
<input type="checkbox" id="nav-check">
|
||||
<div class="nav-header">
|
||||
<div class="nav-title">
|
||||
@ -31,9 +25,12 @@
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<ul class="nav-list">
|
||||
<li class="nav-item appear2"><a href="/site-info">Site info</a></li>
|
||||
<li class="nav-item appear2"><a href="/theme-editor">Theme info</a></li>
|
||||
<li class="nav-item appear2"><a href="#">Gallery</a></li>
|
||||
<li class="nav-item"><a href="/gallery-editor">Gallery</a>
|
||||
<li class="nav-item"><a href="/site-info">Site info</a></li>
|
||||
<li class="nav-item"><a href="/theme-editor">Theme info</a></li>
|
||||
<li class="nav-item">
|
||||
<button id="build-btn" class="button">Build !</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -81,9 +78,10 @@
|
||||
<label>Instagram URL</label>
|
||||
<input type="text" name="social.instagram_url" placeholder="https://instagram.com/yourprofile" required>
|
||||
<label class="thumbnail-form-label">Thumbnail</label>
|
||||
<input type="file" id="thumbnail-upload" accept="image/png,image/jpeg,image/webp" style="display:none;" required>
|
||||
<input type="file" id="thumbnail-upload" accept="image/png,image/jpeg,image/webp" style="display:none;">
|
||||
<button type="button" id="choose-thumbnail-btn" class="up-btn">📸 Upload a photo</button>
|
||||
<div class="thumbnail-form">
|
||||
<input type="hidden" name="social.thumbnail" id="social-thumbnail">
|
||||
<img id="thumbnail-preview" src="" alt="Thumbnail preview" style="max-width:100px;display:none;">
|
||||
<button type="button" id="remove-thumbnail-btn" class="remove-btn up-btn danger" style="display:none;">Remove</button>
|
||||
</div>
|
||||
@ -120,15 +118,15 @@
|
||||
<div class="fields">
|
||||
<div class="input-field">
|
||||
<label>Hoster Name</label>
|
||||
<input type="text" name="legals.hoster_name" placeholder="Name">
|
||||
<input type="text" name="legals.hoster_name" placeholder="Name" required>
|
||||
</div>
|
||||
<div class="input-field">
|
||||
<label>Hoster Address</label>
|
||||
<input type="text" name="legals.hoster_address" placeholder="Street, Postal Code, City, Country">
|
||||
<input type="text" name="legals.hoster_address" placeholder="Street, Postal Code, City, Country" required>
|
||||
</div>
|
||||
<div class="input-field">
|
||||
<label>Hoster Contact</label>
|
||||
<input type="text" name="legals.hoster_contact" placeholder="Email or/and Phone">
|
||||
<input type="text" name="legals.hoster_contact" placeholder="Email or/and Phone" required>
|
||||
</div>
|
||||
<div class="input-field" style="flex: 1 1 100%;">
|
||||
<label>Intellectual Property</label>
|
||||
@ -173,6 +171,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Build success modal -->
|
||||
<div id="build-success-modal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<span id="build-success-modal-close" class="modal-close">×</span>
|
||||
<h3>✅ Build completed!</h3>
|
||||
<p>Your files are available in the output folder.</p>
|
||||
<button id="download-zip-btn" class="modal-btn">Download ZIP</button>
|
||||
<div id="zip-loader" style="display:none;">Creating ZIP...</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="{{ url_for('static', filename='js/site-info.js')}}"></script>
|
||||
<script src="{{ url_for('static', filename='js/build.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
@ -7,6 +7,7 @@ body {
|
||||
color: #FBFBFB;
|
||||
min-height: 100vh;
|
||||
margin:0px;
|
||||
padding-top: 70px;
|
||||
}
|
||||
|
||||
.content-inner {
|
||||
@ -256,12 +257,14 @@ h2 {
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
height: 70px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
background-color: #0c0d0c29;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
backdrop-filter: blur(20px);
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid #21212157;
|
||||
}
|
||||
|
||||
@ -287,6 +290,15 @@ h2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-btn label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.nav > .nav-links {
|
||||
display: inline;
|
||||
float: right;
|
||||
@ -345,7 +357,7 @@ h2 {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.nav-cta > .button {
|
||||
.nav-bar .button {
|
||||
padding: 10px 25px;
|
||||
border-radius: 40px;
|
||||
margin: 15px 20px 15px 10px;
|
||||
@ -356,17 +368,29 @@ h2 {
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-cta > .button:hover {
|
||||
.nav-bar .button:hover {
|
||||
background: linear-gradient(135deg, #72d9ff, #26657e);
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-links > ul {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.nav-btn span {
|
||||
display: block;
|
||||
height: 4px;
|
||||
width: 28px;
|
||||
background: #fff;
|
||||
margin: 4px 0;
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
/* --- Custom Upload Buttons --- */
|
||||
.up-btn {
|
||||
display: inline-block;
|
||||
|
@ -10,16 +10,10 @@
|
||||
<!-- Top bar -->
|
||||
<div class="nav-bar">
|
||||
<div class="content-inner nav">
|
||||
<div class="nav-cta">
|
||||
<div class="arrow">→</div>
|
||||
<a class="button" href="/site-info">
|
||||
<span id="step">← Back to Site Info</span>
|
||||
</a>
|
||||
</div>
|
||||
<input type="checkbox" id="nav-check">
|
||||
<div class="nav-header">
|
||||
<div class="nav-title">
|
||||
<img src="../img/logo.svg">
|
||||
<img src="{{ url_for('static', filename='img/logo.svg') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-btn">
|
||||
@ -31,9 +25,12 @@
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<ul class="nav-list">
|
||||
<li class="nav-item appear2"><a href="/site-info">Site info</a></li>
|
||||
<li class="nav-item appear2"><a href="/theme-editor">Theme editor</a></li>
|
||||
<li class="nav-item appear2"><a href="#">Gallery</a></li>
|
||||
<li class="nav-item"><a href="/gallery-editor">Gallery</a>
|
||||
<li class="nav-item"><a href="/site-info">Site info</a></li>
|
||||
<li class="nav-item"><a href="/theme-editor">Theme info</a></li>
|
||||
<li class="nav-item">
|
||||
<button id="build-btn" class="button">Build !</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -197,6 +194,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Build success modal -->
|
||||
<div id="build-success-modal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<span id="build-success-modal-close" class="modal-close">×</span>
|
||||
<h3>✅ Build completed!</h3>
|
||||
<p>Your files are available in the output folder.</p>
|
||||
<button id="download-zip-btn" class="modal-btn">Download ZIP</button>
|
||||
<div id="zip-loader" style="display:none;">Creating ZIP...</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="{{ url_for('static', filename='js/theme-editor.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/build.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user