2.0 - WebUI builder ("Cielight" merge) #9
@@ -1,7 +1,14 @@
 | 
				
			|||||||
 | 
					# --- Imports ---
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
import yaml
 | 
					import yaml
 | 
				
			||||||
 | 
					import subprocess
 | 
				
			||||||
 | 
					import zipfile
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
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,
 | 
				
			||||||
 | 
					    send_file, after_this_request
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
from src.py.builder.gallery_builder import (
 | 
					from src.py.builder.gallery_builder import (
 | 
				
			||||||
    GALLERY_YAML, load_yaml, save_yaml, update_gallery, update_hero
 | 
					    GALLERY_YAML, load_yaml, save_yaml, update_gallery, update_hero
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -19,61 +26,64 @@ app = Flask(
 | 
				
			|||||||
    static_url_path=""
 | 
					    static_url_path=""
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- Config paths ---
 | 
				
			||||||
SITE_YAML = Path(__file__).resolve().parents[3] / "config" / "site.yaml"
 | 
					SITE_YAML = Path(__file__).resolve().parents[3] / "config" / "site.yaml"
 | 
				
			||||||
 | 
					 | 
				
			||||||
# --- Photos directory (configurable) ---
 | 
					 | 
				
			||||||
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 ---
 | 
				
			||||||
app.register_blueprint(upload_bp)
 | 
					app.register_blueprint(upload_bp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# --- Helper functions for theme editor ---
 | 
					# --- Theme editor helper functions ---
 | 
				
			||||||
def get_theme_name():
 | 
					def get_theme_name():
 | 
				
			||||||
 | 
					    """Get current theme name from site.yaml."""
 | 
				
			||||||
    site_yaml_path = Path(__file__).resolve().parents[3] / "config" / "site.yaml"
 | 
					    site_yaml_path = Path(__file__).resolve().parents[3] / "config" / "site.yaml"
 | 
				
			||||||
    with open(site_yaml_path, "r") as f:
 | 
					    with open(site_yaml_path, "r") as f:
 | 
				
			||||||
        site_yaml = yaml.safe_load(f)
 | 
					        site_yaml = yaml.safe_load(f)
 | 
				
			||||||
    return site_yaml.get("build", {}).get("theme", "modern")
 | 
					    return site_yaml.get("build", {}).get("theme", "modern")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_theme_yaml(theme_name):
 | 
					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"
 | 
					    theme_yaml_path = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "theme.yaml"
 | 
				
			||||||
    with open(theme_yaml_path, "r") as f:
 | 
					    with open(theme_yaml_path, "r") as f:
 | 
				
			||||||
        return yaml.safe_load(f)
 | 
					        return yaml.safe_load(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def save_theme_yaml(theme_name, theme_yaml):
 | 
					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"
 | 
					    theme_yaml_path = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "theme.yaml"
 | 
				
			||||||
    with open(theme_yaml_path, "w") as f:
 | 
					    with open(theme_yaml_path, "w") as f:
 | 
				
			||||||
        yaml.safe_dump(theme_yaml, f, sort_keys=False, allow_unicode=True)
 | 
					        yaml.safe_dump(theme_yaml, f, sort_keys=False, allow_unicode=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_local_fonts(theme_name):
 | 
					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"
 | 
					    fonts_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts"
 | 
				
			||||||
    if not fonts_dir.exists():
 | 
					    if not fonts_dir.exists():
 | 
				
			||||||
        return []
 | 
					        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"]]
 | 
					    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("/")
 | 
					@app.route("/")
 | 
				
			||||||
def index():
 | 
					def index():
 | 
				
			||||||
    """Serve the main HTML page."""
 | 
					 | 
				
			||||||
    return render_template("index.html")
 | 
					    return render_template("index.html")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- Gallery & Hero API ---
 | 
				
			||||||
@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."""
 | 
					    """Get gallery images."""
 | 
				
			||||||
    data = load_yaml(GALLERY_YAML)
 | 
					    data = load_yaml(GALLERY_YAML)
 | 
				
			||||||
    return jsonify(data.get("gallery", {}).get("images", []))
 | 
					    return jsonify(data.get("gallery", {}).get("images", []))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@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."""
 | 
					    """Get hero images."""
 | 
				
			||||||
    data = load_yaml(GALLERY_YAML)
 | 
					    data = load_yaml(GALLERY_YAML)
 | 
				
			||||||
    return jsonify(data.get("hero", {}).get("images", []))
 | 
					    return jsonify(data.get("hero", {}).get("images", []))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@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."""
 | 
					    """Update gallery images."""
 | 
				
			||||||
    images = request.json
 | 
					    images = request.json
 | 
				
			||||||
    data = load_yaml(GALLERY_YAML)
 | 
					    data = load_yaml(GALLERY_YAML)
 | 
				
			||||||
    data["gallery"]["images"] = images
 | 
					    data["gallery"]["images"] = images
 | 
				
			||||||
@@ -82,7 +92,7 @@ def update_gallery_api():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@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."""
 | 
					    """Update hero images."""
 | 
				
			||||||
    images = request.json
 | 
					    images = request.json
 | 
				
			||||||
    data = load_yaml(GALLERY_YAML)
 | 
					    data = load_yaml(GALLERY_YAML)
 | 
				
			||||||
    data["hero"]["images"] = images
 | 
					    data["hero"]["images"] = images
 | 
				
			||||||
@@ -91,49 +101,48 @@ def update_hero_api():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@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."""
 | 
					    """Refresh gallery images from disk."""
 | 
				
			||||||
    update_gallery()
 | 
					    update_gallery()
 | 
				
			||||||
    return jsonify({"status": "ok"})
 | 
					    return jsonify({"status": "ok"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@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."""
 | 
					    """Refresh hero images from disk."""
 | 
				
			||||||
    update_hero()
 | 
					    update_hero()
 | 
				
			||||||
    return jsonify({"status": "ok"})
 | 
					    return jsonify({"status": "ok"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- Gallery & Hero photo deletion ---
 | 
				
			||||||
@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."""
 | 
					    """Delete a gallery photo."""
 | 
				
			||||||
    data = request.json
 | 
					    data = request.json
 | 
				
			||||||
    src = data.get("src")
 | 
					    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()
 | 
					        file_path.unlink()
 | 
				
			||||||
        return {"status": "ok"}
 | 
					        return {"status": "ok"}
 | 
				
			||||||
    return {"error": "File not found"}, 404
 | 
					    return {"error": "❌ File not found"}, 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@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."""
 | 
					    """Delete a hero photo."""
 | 
				
			||||||
    data = request.json
 | 
					    data = request.json
 | 
				
			||||||
    src = data.get("src")
 | 
					    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()
 | 
					        file_path.unlink()
 | 
				
			||||||
        return {"status": "ok"}
 | 
					        return {"status": "ok"}
 | 
				
			||||||
    return {"error": "File not found"}, 404
 | 
					    return {"error": "❌ File not found"}, 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route("/api/gallery/delete_all", methods=["POST"])
 | 
					@app.route("/api/gallery/delete_all", methods=["POST"])
 | 
				
			||||||
def delete_all_gallery_photos():
 | 
					def delete_all_gallery_photos():
 | 
				
			||||||
    """Delete all gallery photos from disk and YAML."""
 | 
					    """Delete all gallery photos."""
 | 
				
			||||||
    gallery_dir = PHOTOS_DIR / "gallery"
 | 
					    gallery_dir = PHOTOS_DIR / "gallery"
 | 
				
			||||||
    deleted = 0
 | 
					    deleted = 0
 | 
				
			||||||
    # Remove all files in gallery folder
 | 
					 | 
				
			||||||
    for file in gallery_dir.glob("*"):
 | 
					    for file in gallery_dir.glob("*"):
 | 
				
			||||||
        if file.is_file():
 | 
					        if file.is_file():
 | 
				
			||||||
            file.unlink()
 | 
					            file.unlink()
 | 
				
			||||||
            deleted += 1
 | 
					            deleted += 1
 | 
				
			||||||
    # Clear YAML gallery images
 | 
					 | 
				
			||||||
    data = load_yaml(GALLERY_YAML)
 | 
					    data = load_yaml(GALLERY_YAML)
 | 
				
			||||||
    data["gallery"]["images"] = []
 | 
					    data["gallery"]["images"] = []
 | 
				
			||||||
    save_yaml(data, GALLERY_YAML)
 | 
					    save_yaml(data, GALLERY_YAML)
 | 
				
			||||||
@@ -141,68 +150,69 @@ def delete_all_gallery_photos():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@app.route("/api/hero/delete_all", methods=["POST"])
 | 
					@app.route("/api/hero/delete_all", methods=["POST"])
 | 
				
			||||||
def delete_all_hero_photos():
 | 
					def delete_all_hero_photos():
 | 
				
			||||||
    """Delete all hero photos from disk and YAML."""
 | 
					    """Delete all hero photos."""
 | 
				
			||||||
    hero_dir = PHOTOS_DIR / "hero"
 | 
					    hero_dir = PHOTOS_DIR / "hero"
 | 
				
			||||||
    deleted = 0
 | 
					    deleted = 0
 | 
				
			||||||
    # Remove all files in hero folder
 | 
					 | 
				
			||||||
    for file in hero_dir.glob("*"):
 | 
					    for file in hero_dir.glob("*"):
 | 
				
			||||||
        if file.is_file():
 | 
					        if file.is_file():
 | 
				
			||||||
            file.unlink()
 | 
					            file.unlink()
 | 
				
			||||||
            deleted += 1
 | 
					            deleted += 1
 | 
				
			||||||
    # Clear YAML hero images
 | 
					 | 
				
			||||||
    data = load_yaml(GALLERY_YAML)
 | 
					    data = load_yaml(GALLERY_YAML)
 | 
				
			||||||
    data["hero"]["images"] = []
 | 
					    data["hero"]["images"] = []
 | 
				
			||||||
    save_yaml(data, GALLERY_YAML)
 | 
					    save_yaml(data, GALLERY_YAML)
 | 
				
			||||||
    return jsonify({"status": "ok", "deleted": deleted})
 | 
					    return jsonify({"status": "ok", "deleted": deleted})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- Serve photos ---
 | 
				
			||||||
@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 for a specific section."""
 | 
					    """Serve a photo from a section."""
 | 
				
			||||||
    return send_from_directory(PHOTOS_DIR / section, filename)
 | 
					    return send_from_directory(PHOTOS_DIR / section, filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route("/photos/<path:filename>")
 | 
					@app.route("/photos/<path:filename>")
 | 
				
			||||||
def serve_photo(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"
 | 
					    photos_dir = Path(__file__).resolve().parents[3] / "config" / "photos"
 | 
				
			||||||
    return send_from_directory(photos_dir, filename)
 | 
					    return send_from_directory(photos_dir, filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- Site info page & API ---
 | 
				
			||||||
@app.route("/site-info")
 | 
					@app.route("/site-info")
 | 
				
			||||||
def site_info():
 | 
					def site_info():
 | 
				
			||||||
    """Serve the site info editor page."""
 | 
					    """Render site info editor page."""
 | 
				
			||||||
    return render_template("site-info/index.html")
 | 
					    return render_template("site-info/index.html")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route("/api/site-info", methods=["GET"])
 | 
					@app.route("/api/site-info", methods=["GET"])
 | 
				
			||||||
def get_site_info():
 | 
					def get_site_info():
 | 
				
			||||||
    """Return the site info YAML as JSON."""
 | 
					    """Get site info YAML as JSON."""
 | 
				
			||||||
    with open(SITE_YAML, "r") as f:
 | 
					    with open(SITE_YAML, "r") as f:
 | 
				
			||||||
        data = yaml.safe_load(f)
 | 
					        data = yaml.safe_load(f)
 | 
				
			||||||
    return jsonify(data)
 | 
					    return jsonify(data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route("/api/site-info", methods=["POST"])
 | 
					@app.route("/api/site-info", methods=["POST"])
 | 
				
			||||||
def update_site_info():
 | 
					def update_site_info():
 | 
				
			||||||
    """Update the site info YAML from frontend JSON."""
 | 
					    """Update site info YAML."""
 | 
				
			||||||
    data = request.json
 | 
					    data = request.json
 | 
				
			||||||
    with open(SITE_YAML, "w") as f:
 | 
					    with open(SITE_YAML, "w") as f:
 | 
				
			||||||
        yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
 | 
					        yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
 | 
				
			||||||
    return jsonify({"status": "ok"})
 | 
					    return jsonify({"status": "ok"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- Theme management ---
 | 
				
			||||||
@app.route("/api/themes")
 | 
					@app.route("/api/themes")
 | 
				
			||||||
def list_themes():
 | 
					def list_themes():
 | 
				
			||||||
    """List available themes (folders in config/themes)."""
 | 
					    """List available themes."""
 | 
				
			||||||
    themes_dir = Path(__file__).resolve().parents[3] / "config" / "themes"
 | 
					    themes_dir = Path(__file__).resolve().parents[3] / "config" / "themes"
 | 
				
			||||||
    themes = [d.name for d in themes_dir.iterdir() if d.is_dir()]
 | 
					    themes = [d.name for d in themes_dir.iterdir() if d.is_dir()]
 | 
				
			||||||
    return jsonify(themes)
 | 
					    return jsonify(themes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- Thumbnail upload/remove ---
 | 
				
			||||||
@app.route("/api/thumbnail/upload", methods=["POST"])
 | 
					@app.route("/api/thumbnail/upload", methods=["POST"])
 | 
				
			||||||
def upload_thumbnail():
 | 
					def upload_thumbnail():
 | 
				
			||||||
    """Upload a thumbnail image and update site.yaml."""
 | 
					    """Upload thumbnail image and update site.yaml."""
 | 
				
			||||||
    PHOTOS_DIR = app.config["PHOTOS_DIR"]
 | 
					    PHOTOS_DIR = app.config["PHOTOS_DIR"]
 | 
				
			||||||
    file = request.files.get("file")
 | 
					    file = request.files.get("file")
 | 
				
			||||||
    if not file:
 | 
					    if not file:
 | 
				
			||||||
        return {"error": "No file provided"}, 400
 | 
					        return {"error": "❌ No file provided"}, 400
 | 
				
			||||||
    filename = "thumbnail.png"
 | 
					    filename = "thumbnail.png"
 | 
				
			||||||
    file.save(PHOTOS_DIR / filename)
 | 
					    file.save(PHOTOS_DIR / filename)
 | 
				
			||||||
    # Update site.yaml
 | 
					 | 
				
			||||||
    with open(SITE_YAML, "r") as f:
 | 
					    with open(SITE_YAML, "r") as f:
 | 
				
			||||||
        data = yaml.safe_load(f)
 | 
					        data = yaml.safe_load(f)
 | 
				
			||||||
    data.setdefault("social", {})["thumbnail"] = filename
 | 
					    data.setdefault("social", {})["thumbnail"] = filename
 | 
				
			||||||
@@ -212,13 +222,11 @@ def upload_thumbnail():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@app.route("/api/thumbnail/remove", methods=["POST"])
 | 
					@app.route("/api/thumbnail/remove", methods=["POST"])
 | 
				
			||||||
def remove_thumbnail():
 | 
					def remove_thumbnail():
 | 
				
			||||||
    """Remove the thumbnail image and update site.yaml."""
 | 
					    """Remove thumbnail image and update site.yaml."""
 | 
				
			||||||
    PHOTOS_DIR = app.config["PHOTOS_DIR"]
 | 
					    PHOTOS_DIR = app.config["PHOTOS_DIR"]
 | 
				
			||||||
    thumbnail_path = PHOTOS_DIR / "thumbnail.png"
 | 
					    thumbnail_path = PHOTOS_DIR / "thumbnail.png"
 | 
				
			||||||
    # Remove thumbnail file if exists
 | 
					 | 
				
			||||||
    if thumbnail_path.exists():
 | 
					    if thumbnail_path.exists():
 | 
				
			||||||
        thumbnail_path.unlink()
 | 
					        thumbnail_path.unlink()
 | 
				
			||||||
    # Update site.yaml to remove thumbnail key
 | 
					 | 
				
			||||||
    with open(SITE_YAML, "r") as f:
 | 
					    with open(SITE_YAML, "r") as f:
 | 
				
			||||||
        data = yaml.safe_load(f)
 | 
					        data = yaml.safe_load(f)
 | 
				
			||||||
    if "social" in data and "thumbnail" in data["social"]:
 | 
					    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)
 | 
					        yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
 | 
				
			||||||
    return jsonify({"status": "ok"})
 | 
					    return jsonify({"status": "ok"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- Theme upload ---
 | 
				
			||||||
@app.route("/api/theme/upload", methods=["POST"])
 | 
					@app.route("/api/theme/upload", methods=["POST"])
 | 
				
			||||||
def upload_theme():
 | 
					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"
 | 
					    themes_dir = Path(__file__).resolve().parents[3] / "config" / "themes"
 | 
				
			||||||
    files = request.files.getlist("files")
 | 
					    files = request.files.getlist("files")
 | 
				
			||||||
    if not files:
 | 
					    if not files:
 | 
				
			||||||
        return jsonify({"error": "No files provided"}), 400
 | 
					        return jsonify({"error": "❌ No files provided"}), 400
 | 
				
			||||||
    # Get folder name from first file's webkitRelativePath
 | 
					 | 
				
			||||||
    first_path = files[0].filename
 | 
					    first_path = files[0].filename
 | 
				
			||||||
    folder_name = first_path.split("/")[0] if "/" in first_path else "custom"
 | 
					    folder_name = first_path.split("/")[0] if "/" in first_path else "custom"
 | 
				
			||||||
    theme_folder = themes_dir / folder_name
 | 
					    theme_folder = themes_dir / folder_name
 | 
				
			||||||
@@ -246,14 +254,15 @@ def upload_theme():
 | 
				
			|||||||
        file.save(dest_path)
 | 
					        file.save(dest_path)
 | 
				
			||||||
    return jsonify({"status": "ok", "theme": folder_name})
 | 
					    return jsonify({"status": "ok", "theme": folder_name})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# --- Theme Editor API ---
 | 
					# --- Theme editor page & API ---
 | 
				
			||||||
@app.route("/theme-editor")
 | 
					@app.route("/theme-editor")
 | 
				
			||||||
def theme_editor():
 | 
					def theme_editor():
 | 
				
			||||||
    """Serve the theme editor page."""
 | 
					    """Render theme editor page."""
 | 
				
			||||||
    return render_template("theme-editor/index.html")
 | 
					    return render_template("theme-editor/index.html")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route("/api/theme-info", methods=["GET", "POST"])
 | 
					@app.route("/api/theme-info", methods=["GET", "POST"])
 | 
				
			||||||
def api_theme_info():
 | 
					def api_theme_info():
 | 
				
			||||||
 | 
					    """Get or update theme.yaml for current theme."""
 | 
				
			||||||
    theme_name = get_theme_name()
 | 
					    theme_name = get_theme_name()
 | 
				
			||||||
    if request.method == "GET":
 | 
					    if request.method == "GET":
 | 
				
			||||||
        theme_yaml = get_theme_yaml(theme_name)
 | 
					        theme_yaml = get_theme_yaml(theme_name)
 | 
				
			||||||
@@ -272,25 +281,25 @@ def api_theme_info():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@app.route("/api/local-fonts")
 | 
					@app.route("/api/local-fonts")
 | 
				
			||||||
def api_local_fonts():
 | 
					def api_local_fonts():
 | 
				
			||||||
 | 
					    """List local fonts for a theme."""
 | 
				
			||||||
    theme_name = request.args.get("theme")
 | 
					    theme_name = request.args.get("theme")
 | 
				
			||||||
    fonts = get_local_fonts(theme_name)
 | 
					    fonts = get_local_fonts(theme_name)
 | 
				
			||||||
    return jsonify(fonts)
 | 
					    return jsonify(fonts)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- Favicon upload/remove ---
 | 
				
			||||||
@app.route("/api/favicon/upload", methods=["POST"])
 | 
					@app.route("/api/favicon/upload", methods=["POST"])
 | 
				
			||||||
def upload_favicon():
 | 
					def upload_favicon():
 | 
				
			||||||
    """Upload favicon to theme folder and update theme.yaml."""
 | 
					    """Upload favicon for a theme."""
 | 
				
			||||||
    theme_name = request.form.get("theme")
 | 
					    theme_name = request.form.get("theme")
 | 
				
			||||||
    file = request.files.get("file")
 | 
					    file = request.files.get("file")
 | 
				
			||||||
    if not file or not theme_name:
 | 
					    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()
 | 
					    ext = Path(file.filename).suffix.lower()
 | 
				
			||||||
    if ext not in [".png", ".jpg", ".jpeg", ".ico"]:
 | 
					    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
 | 
					    filename = "favicon" + ext
 | 
				
			||||||
    theme_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name
 | 
					    theme_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name
 | 
				
			||||||
    file.save(theme_dir / filename)
 | 
					    file.save(theme_dir / filename)
 | 
				
			||||||
    # Update theme.yaml
 | 
					 | 
				
			||||||
    theme_yaml_path = theme_dir / "theme.yaml"
 | 
					    theme_yaml_path = theme_dir / "theme.yaml"
 | 
				
			||||||
    with open(theme_yaml_path, "r") as f:
 | 
					    with open(theme_yaml_path, "r") as f:
 | 
				
			||||||
        theme_yaml = yaml.safe_load(f)
 | 
					        theme_yaml = yaml.safe_load(f)
 | 
				
			||||||
@@ -301,18 +310,16 @@ def upload_favicon():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@app.route("/api/favicon/remove", methods=["POST"])
 | 
					@app.route("/api/favicon/remove", methods=["POST"])
 | 
				
			||||||
def remove_favicon():
 | 
					def remove_favicon():
 | 
				
			||||||
    """Remove favicon from theme folder and update theme.yaml."""
 | 
					    """Remove favicon for a theme."""
 | 
				
			||||||
    data = request.get_json()
 | 
					    data = request.get_json()
 | 
				
			||||||
    theme_name = data.get("theme")
 | 
					    theme_name = data.get("theme")
 | 
				
			||||||
    if not theme_name:
 | 
					    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
 | 
					    theme_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name
 | 
				
			||||||
    # Remove favicon file
 | 
					 | 
				
			||||||
    for ext in [".png", ".jpg", ".jpeg", ".ico"]:
 | 
					    for ext in [".png", ".jpg", ".jpeg", ".ico"]:
 | 
				
			||||||
        favicon_path = theme_dir / f"favicon{ext}"
 | 
					        favicon_path = theme_dir / f"favicon{ext}"
 | 
				
			||||||
        if favicon_path.exists():
 | 
					        if favicon_path.exists():
 | 
				
			||||||
            favicon_path.unlink()
 | 
					            favicon_path.unlink()
 | 
				
			||||||
    # Update theme.yaml
 | 
					 | 
				
			||||||
    theme_yaml_path = theme_dir / "theme.yaml"
 | 
					    theme_yaml_path = theme_dir / "theme.yaml"
 | 
				
			||||||
    with open(theme_yaml_path, "r") as f:
 | 
					    with open(theme_yaml_path, "r") as f:
 | 
				
			||||||
        theme_yaml = yaml.safe_load(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)
 | 
					        yaml.safe_dump(theme_yaml, f, sort_keys=False, allow_unicode=True)
 | 
				
			||||||
    return jsonify({"status": "ok"})
 | 
					    return jsonify({"status": "ok"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- Serve theme assets ---
 | 
				
			||||||
@app.route("/themes/<theme>/<filename>")
 | 
					@app.route("/themes/<theme>/<filename>")
 | 
				
			||||||
def serve_theme_asset(theme, filename):
 | 
					def serve_theme_asset(theme, filename):
 | 
				
			||||||
 | 
					    """Serve a theme asset file."""
 | 
				
			||||||
    theme_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme
 | 
					    theme_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme
 | 
				
			||||||
    return send_from_directory(theme_dir, filename)
 | 
					    return send_from_directory(theme_dir, filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- Font upload/remove ---
 | 
				
			||||||
@app.route("/api/font/upload", methods=["POST"])
 | 
					@app.route("/api/font/upload", methods=["POST"])
 | 
				
			||||||
def upload_font():
 | 
					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")
 | 
					    theme_name = request.form.get("theme")
 | 
				
			||||||
    file = request.files.get("file")
 | 
					    file = request.files.get("file")
 | 
				
			||||||
    if not file or not theme_name:
 | 
					    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()
 | 
					    ext = Path(file.filename).suffix.lower()
 | 
				
			||||||
    if ext not in [".woff", ".woff2"]:
 | 
					    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 = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts"
 | 
				
			||||||
    fonts_dir.mkdir(parents=True, exist_ok=True)
 | 
					    fonts_dir.mkdir(parents=True, exist_ok=True)
 | 
				
			||||||
    file.save(fonts_dir / file.filename)
 | 
					    file.save(fonts_dir / file.filename)
 | 
				
			||||||
@@ -344,18 +354,90 @@ def upload_font():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@app.route("/api/font/remove", methods=["POST"])
 | 
					@app.route("/api/font/remove", methods=["POST"])
 | 
				
			||||||
def remove_font():
 | 
					def remove_font():
 | 
				
			||||||
    """Remove a font file from the theme's fonts folder."""
 | 
					    """Remove a font file for a theme."""
 | 
				
			||||||
    data = request.get_json()
 | 
					    data = request.get_json()
 | 
				
			||||||
    theme_name = data.get("theme")
 | 
					    theme_name = data.get("theme")
 | 
				
			||||||
    font = data.get("font")
 | 
					    font = data.get("font")
 | 
				
			||||||
    if not theme_name or not 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"
 | 
					    fonts_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts"
 | 
				
			||||||
    font_path = fonts_dir / font
 | 
					    font_path = fonts_dir / font
 | 
				
			||||||
    if font_path.exists():
 | 
					    if font_path.exists():
 | 
				
			||||||
        font_path.unlink()
 | 
					        font_path.unlink()
 | 
				
			||||||
        return jsonify({"status": "ok"})
 | 
					        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 ---
 | 
					# --- Run server ---
 | 
				
			||||||
if __name__ == "__main__":
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,12 +10,6 @@
 | 
				
			|||||||
  <!-- Top bar -->	
 | 
					  <!-- Top bar -->	
 | 
				
			||||||
  <div class="nav-bar">
 | 
					  <div class="nav-bar">
 | 
				
			||||||
    <div class="content-inner nav">
 | 
					    <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">
 | 
					      <input type="checkbox" id="nav-check">
 | 
				
			||||||
      <div class="nav-header">
 | 
					      <div class="nav-header">
 | 
				
			||||||
        <div class="nav-title">
 | 
					        <div class="nav-title">
 | 
				
			||||||
@@ -31,9 +25,12 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="nav-links">
 | 
					      <div class="nav-links">
 | 
				
			||||||
        <ul class="nav-list">
 | 
					        <ul class="nav-list">
 | 
				
			||||||
          <li class="nav-item appear2"><a href="/site-info">Site info</a></li>
 | 
					          <li class="nav-item"><a href="/gallery-editor">Gallery</a>
 | 
				
			||||||
          <li class="nav-item appear2"><a href="/theme-editor">Theme info</a></li>
 | 
					          <li class="nav-item"><a href="/site-info">Site info</a></li>
 | 
				
			||||||
          <li class="nav-item appear2"><a href="#">Gallery</a>
 | 
					          <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>
 | 
					        </ul>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
@@ -73,6 +70,7 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <script src="{{ url_for('static', filename='js/main.js') }}" defer></script>
 | 
					    <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/upload.js') }}" defer></script>
 | 
				
			||||||
 | 
					    <script src="{{ url_for('static', filename='js/build.js') }}" defer></script>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  <!-- Delete confirmation modal -->
 | 
					  <!-- Delete confirmation modal -->
 | 
				
			||||||
  <div id="delete-modal" class="modal" style="display:none;">
 | 
					  <div id="delete-modal" class="modal" style="display:none;">
 | 
				
			||||||
@@ -86,5 +84,15 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </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>
 | 
					</body>
 | 
				
			||||||
</html>
 | 
					</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.gap = "8px";
 | 
				
			||||||
      div.style.marginBottom = "6px";
 | 
					      div.style.marginBottom = "6px";
 | 
				
			||||||
      div.innerHTML = `
 | 
					      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>
 | 
					        <button type="button" class="remove-ip-paragraph" data-idx="${idx}">🗑</button>
 | 
				
			||||||
      `;
 | 
					      `;
 | 
				
			||||||
      ipList.appendChild(div);
 | 
					      ipList.appendChild(div);
 | 
				
			||||||
@@ -129,9 +129,9 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        if (thumbnailInput) thumbnailInput.value = result.filename;
 | 
					        if (thumbnailInput) thumbnailInput.value = result.filename;
 | 
				
			||||||
        updateThumbnailPreview(`/photos/${result.filename}?t=${Date.now()}`);
 | 
					        updateThumbnailPreview(`/photos/${result.filename}?t=${Date.now()}`);
 | 
				
			||||||
        showToast("Thumbnail uploaded!", "success");
 | 
					        showToast("✅ Thumbnail uploaded!", "success");
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        showToast("Error uploading thumbnail", "error");
 | 
					        showToast("❌ Error uploading thumbnail", "error");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -159,9 +159,9 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        if (thumbnailInput) thumbnailInput.value = "";
 | 
					        if (thumbnailInput) thumbnailInput.value = "";
 | 
				
			||||||
        updateThumbnailPreview("");
 | 
					        updateThumbnailPreview("");
 | 
				
			||||||
        showToast("Thumbnail removed!", "success");
 | 
					        showToast("✅ Thumbnail removed!", "success");
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        showToast("Error removing thumbnail", "error");
 | 
					        showToast("❌ Error removing thumbnail", "error");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      deleteModal.style.display = "none";
 | 
					      deleteModal.style.display = "none";
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
@@ -182,7 +182,7 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
      const res = await fetch("/api/theme/upload", { method: "POST", body: formData });
 | 
					      const res = await fetch("/api/theme/upload", { method: "POST", body: formData });
 | 
				
			||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        showToast("Theme uploaded!", "success");
 | 
					        showToast("✅ Theme uploaded!", "success");
 | 
				
			||||||
        // Refresh theme select after upload
 | 
					        // Refresh theme select after upload
 | 
				
			||||||
        fetch("/api/themes")
 | 
					        fetch("/api/themes")
 | 
				
			||||||
          .then(res => res.json())
 | 
					          .then(res => res.json())
 | 
				
			||||||
@@ -196,7 +196,7 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
            });
 | 
					            });
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        showToast("Error uploading theme", "error");
 | 
					        showToast("❌ Error uploading theme", "error");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -311,6 +311,12 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
      updateMenuItemsFromInputs();
 | 
					      updateMenuItemsFromInputs();
 | 
				
			||||||
      updateIpParagraphsFromInputs();
 | 
					      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 = {
 | 
					      const build = {
 | 
				
			||||||
        theme: themeSelect ? themeSelect.value : "",
 | 
					        theme: themeSelect ? themeSelect.value : "",
 | 
				
			||||||
        convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked),
 | 
					        convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,12 +10,6 @@
 | 
				
			|||||||
  <!-- Top bar -->	
 | 
					  <!-- Top bar -->	
 | 
				
			||||||
  <div class="nav-bar">
 | 
					  <div class="nav-bar">
 | 
				
			||||||
    <div class="content-inner nav">
 | 
					    <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">
 | 
					      <input type="checkbox" id="nav-check">
 | 
				
			||||||
      <div class="nav-header">
 | 
					      <div class="nav-header">
 | 
				
			||||||
        <div class="nav-title">
 | 
					        <div class="nav-title">
 | 
				
			||||||
@@ -31,9 +25,12 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="nav-links">
 | 
					      <div class="nav-links">
 | 
				
			||||||
        <ul class="nav-list">
 | 
					        <ul class="nav-list">
 | 
				
			||||||
          <li class="nav-item appear2"><a href="/site-info">Site info</a></li>
 | 
					          <li class="nav-item"><a href="/gallery-editor">Gallery</a>
 | 
				
			||||||
          <li class="nav-item appear2"><a href="/theme-editor">Theme info</a></li>
 | 
					          <li class="nav-item"><a href="/site-info">Site info</a></li>
 | 
				
			||||||
          <li class="nav-item appear2"><a href="#">Gallery</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>
 | 
					        </ul>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
@@ -81,9 +78,10 @@
 | 
				
			|||||||
            <label>Instagram URL</label>
 | 
					            <label>Instagram URL</label>
 | 
				
			||||||
            <input type="text" name="social.instagram_url" placeholder="https://instagram.com/yourprofile" required>
 | 
					            <input type="text" name="social.instagram_url" placeholder="https://instagram.com/yourprofile" required>
 | 
				
			||||||
            <label class="thumbnail-form-label">Thumbnail</label>
 | 
					            <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>
 | 
					            <button type="button" id="choose-thumbnail-btn" class="up-btn">📸 Upload a photo</button>
 | 
				
			||||||
            <div class="thumbnail-form">
 | 
					            <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;">
 | 
					            <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>
 | 
					            <button type="button" id="remove-thumbnail-btn" class="remove-btn up-btn danger" style="display:none;">Remove</button>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
@@ -120,15 +118,15 @@
 | 
				
			|||||||
        <div class="fields">
 | 
					        <div class="fields">
 | 
				
			||||||
          <div class="input-field">
 | 
					          <div class="input-field">
 | 
				
			||||||
            <label>Hoster Name</label>
 | 
					            <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>
 | 
				
			||||||
          <div class="input-field">
 | 
					          <div class="input-field">
 | 
				
			||||||
            <label>Hoster Address</label>
 | 
					            <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>
 | 
				
			||||||
          <div class="input-field">
 | 
					          <div class="input-field">
 | 
				
			||||||
            <label>Hoster Contact</label>
 | 
					            <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>
 | 
				
			||||||
          <div class="input-field" style="flex: 1 1 100%;">
 | 
					          <div class="input-field" style="flex: 1 1 100%;">
 | 
				
			||||||
            <label>Intellectual Property</label>
 | 
					            <label>Intellectual Property</label>
 | 
				
			||||||
@@ -173,6 +171,17 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </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/site-info.js')}}"></script>
 | 
				
			||||||
 | 
					<script src="{{ url_for('static', filename='js/build.js') }}"></script>
 | 
				
			||||||
</body>
 | 
					</body>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
@@ -7,6 +7,7 @@ body {
 | 
				
			|||||||
  color: #FBFBFB;
 | 
					  color: #FBFBFB;
 | 
				
			||||||
  min-height: 100vh;
 | 
					  min-height: 100vh;
 | 
				
			||||||
  margin:0px;
 | 
					  margin:0px;
 | 
				
			||||||
 | 
					  padding-top: 70px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.content-inner {
 | 
					.content-inner {
 | 
				
			||||||
@@ -256,11 +257,13 @@ h2 {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.nav-bar {
 | 
					.nav-bar {
 | 
				
			||||||
  height: 70px;
 | 
					  position: fixed;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 70px;
 | 
				
			||||||
  background-color: #0c0d0c29;
 | 
					  background-color: #0c0d0c29;
 | 
				
			||||||
  position: relative;
 | 
					  z-index: 1000;
 | 
				
			||||||
  z-index: 1;
 | 
					 | 
				
			||||||
  backdrop-filter: blur(20px);
 | 
					  backdrop-filter: blur(20px);
 | 
				
			||||||
  border-bottom: 1px solid #21212157;
 | 
					  border-bottom: 1px solid #21212157;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -287,6 +290,15 @@ h2 {
 | 
				
			|||||||
  display: none;
 | 
					  display: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav-btn label {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  width: 32px;
 | 
				
			||||||
 | 
					  height: 32px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.nav > .nav-links {
 | 
					.nav > .nav-links {
 | 
				
			||||||
  display: inline;
 | 
					  display: inline;
 | 
				
			||||||
  float: right;
 | 
					  float: right;
 | 
				
			||||||
@@ -345,7 +357,7 @@ h2 {
 | 
				
			|||||||
  font-weight: 700;
 | 
					  font-weight: 700;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.nav-cta > .button {
 | 
					.nav-bar .button {
 | 
				
			||||||
  padding: 10px 25px;
 | 
					  padding: 10px 25px;
 | 
				
			||||||
  border-radius: 40px;
 | 
					  border-radius: 40px;
 | 
				
			||||||
  margin: 15px 20px 15px 10px;
 | 
					  margin: 15px 20px 15px 10px;
 | 
				
			||||||
@@ -356,17 +368,29 @@ h2 {
 | 
				
			|||||||
  text-decoration: none;
 | 
					  text-decoration: none;
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
  font-weight: 700;
 | 
					  font-weight: 700;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.nav-cta > .button:hover {
 | 
					.nav-bar .button:hover {
 | 
				
			||||||
  background: linear-gradient(135deg, #72d9ff, #26657e);
 | 
					  background: linear-gradient(135deg, #72d9ff, #26657e);
 | 
				
			||||||
  transition: all 0.2s ease;
 | 
					  transition: all 0.2s ease;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.nav-links > ul {
 | 
					.nav-links > ul {
 | 
				
			||||||
  display: inline-block;
 | 
					  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 --- */
 | 
					/* --- Custom Upload Buttons --- */
 | 
				
			||||||
.up-btn {
 | 
					.up-btn {
 | 
				
			||||||
  display: inline-block;
 | 
					  display: inline-block;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,16 +10,10 @@
 | 
				
			|||||||
  <!-- Top bar -->	
 | 
					  <!-- Top bar -->	
 | 
				
			||||||
  <div class="nav-bar">
 | 
					  <div class="nav-bar">
 | 
				
			||||||
    <div class="content-inner nav">
 | 
					    <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">
 | 
					      <input type="checkbox" id="nav-check">
 | 
				
			||||||
      <div class="nav-header">
 | 
					      <div class="nav-header">
 | 
				
			||||||
        <div class="nav-title">
 | 
					        <div class="nav-title">
 | 
				
			||||||
          <img src="../img/logo.svg">
 | 
					          <img src="{{ url_for('static', filename='img/logo.svg') }}">
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="nav-btn">
 | 
					      <div class="nav-btn">
 | 
				
			||||||
@@ -31,9 +25,12 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="nav-links">
 | 
					      <div class="nav-links">
 | 
				
			||||||
        <ul class="nav-list">
 | 
					        <ul class="nav-list">
 | 
				
			||||||
          <li class="nav-item appear2"><a href="/site-info">Site info</a></li>
 | 
					          <li class="nav-item"><a href="/gallery-editor">Gallery</a>
 | 
				
			||||||
          <li class="nav-item appear2"><a href="/theme-editor">Theme editor</a></li>
 | 
					          <li class="nav-item"><a href="/site-info">Site info</a></li>
 | 
				
			||||||
          <li class="nav-item appear2"><a href="#">Gallery</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>
 | 
					        </ul>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
@@ -197,6 +194,17 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </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/theme-editor.js') }}"></script>
 | 
				
			||||||
 | 
					  <script src="{{ url_for('static', filename='js/build.js') }}"></script>
 | 
				
			||||||
</body>
 | 
					</body>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
		Reference in New Issue
	
	Block a user