2.0 - WebUI builder ("Cielight" merge) #9
@@ -63,3 +63,4 @@ def upload_photo(section: str):
 | 
				
			|||||||
        return {"status": "ok", "uploaded": uploaded}
 | 
					        return {"status": "ok", "uploaded": uploaded}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {"error": "No valid files uploaded"}, 400
 | 
					    return {"error": "No valid files uploaded"}, 400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -136,6 +136,10 @@ def photos(section, filename):
 | 
				
			|||||||
    """Serve uploaded photos from disk."""
 | 
					    """Serve uploaded photos from disk."""
 | 
				
			||||||
    return send_from_directory(PHOTOS_DIR / section, filename)
 | 
					    return send_from_directory(PHOTOS_DIR / section, filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route("/photos/<path:filename>")
 | 
				
			||||||
 | 
					def serve_photo(filename):
 | 
				
			||||||
 | 
					    photos_dir = Path(__file__).resolve().parents[3] / "config" / "photos"
 | 
				
			||||||
 | 
					    return send_from_directory(photos_dir, filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route("/site-info")
 | 
					@app.route("/site-info")
 | 
				
			||||||
def site_info():
 | 
					def site_info():
 | 
				
			||||||
@@ -154,6 +158,28 @@ def update_site_info():
 | 
				
			|||||||
        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"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route("/api/themes")
 | 
				
			||||||
 | 
					def list_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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route("/api/thumbnail/upload", methods=["POST"])
 | 
				
			||||||
 | 
					def upload_thumbnail():
 | 
				
			||||||
 | 
					    PHOTOS_DIR = app.config["PHOTOS_DIR"]
 | 
				
			||||||
 | 
					    file = request.files.get("file")
 | 
				
			||||||
 | 
					    if not file:
 | 
				
			||||||
 | 
					        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
 | 
				
			||||||
 | 
					    with open(SITE_YAML, "w") as f:
 | 
				
			||||||
 | 
					        yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
 | 
				
			||||||
 | 
					    return jsonify({"status": "ok", "filename": filename})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# --- Run server ---
 | 
					# --- Run server ---
 | 
				
			||||||
if __name__ == "__main__":
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -63,6 +63,58 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
  const convertImagesCheckbox = document.getElementById("convert-images-checkbox");
 | 
					  const convertImagesCheckbox = document.getElementById("convert-images-checkbox");
 | 
				
			||||||
  const resizeImagesCheckbox = document.getElementById("resize-images-checkbox");
 | 
					  const resizeImagesCheckbox = document.getElementById("resize-images-checkbox");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --- Theme select ---
 | 
				
			||||||
 | 
					  const themeSelect = document.getElementById("theme-select");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --- Thumbnail upload ---
 | 
				
			||||||
 | 
					  const thumbnailInput = form?.elements["social.thumbnail"];
 | 
				
			||||||
 | 
					  const thumbnailUpload = document.getElementById("thumbnail-upload");
 | 
				
			||||||
 | 
					  const thumbnailPreview = document.getElementById("thumbnail-preview");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (thumbnailUpload) {
 | 
				
			||||||
 | 
					    thumbnailUpload.addEventListener("change", async (e) => {
 | 
				
			||||||
 | 
					      const file = e.target.files[0];
 | 
				
			||||||
 | 
					      if (!file) return;
 | 
				
			||||||
 | 
					      const formData = new FormData();
 | 
				
			||||||
 | 
					      formData.append("file", file);
 | 
				
			||||||
 | 
					      const res = await fetch("/api/thumbnail/upload", { method: "POST", body: formData });
 | 
				
			||||||
 | 
					      const result = await res.json();
 | 
				
			||||||
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
 | 
					        if (thumbnailInput) thumbnailInput.value = result.filename;
 | 
				
			||||||
 | 
					        if (thumbnailPreview) {
 | 
				
			||||||
 | 
					          thumbnailPreview.src = `/photos/${result.filename}`;
 | 
				
			||||||
 | 
					          thumbnailPreview.style.display = "block";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        status.textContent = "✅ Thumbnail uploaded!";
 | 
				
			||||||
 | 
					        setTimeout(() => status.textContent = "", 2000);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        status.textContent = "❌ Error uploading thumbnail";
 | 
				
			||||||
 | 
					        setTimeout(() => status.textContent = "", 2000);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Fetch theme list and populate select
 | 
				
			||||||
 | 
					  if (themeSelect) {
 | 
				
			||||||
 | 
					    fetch("/api/themes")
 | 
				
			||||||
 | 
					      .then(res => res.json())
 | 
				
			||||||
 | 
					      .then(themes => {
 | 
				
			||||||
 | 
					        themeSelect.innerHTML = "";
 | 
				
			||||||
 | 
					        themes.forEach(theme => {
 | 
				
			||||||
 | 
					          const option = document.createElement("option");
 | 
				
			||||||
 | 
					          option.value = theme;
 | 
				
			||||||
 | 
					          option.textContent = theme;
 | 
				
			||||||
 | 
					          themeSelect.appendChild(option);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        // Set selected value after loading config
 | 
				
			||||||
 | 
					        fetch("/api/site-info")
 | 
				
			||||||
 | 
					          .then(res => res.json())
 | 
				
			||||||
 | 
					          .then(data => {
 | 
				
			||||||
 | 
					            themeSelect.value = data.build?.theme || "";
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Load config
 | 
					  // Load config
 | 
				
			||||||
  if (form) {
 | 
					  if (form) {
 | 
				
			||||||
    fetch("/api/site-info")
 | 
					    fetch("/api/site-info")
 | 
				
			||||||
@@ -81,10 +133,16 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
        form.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || "");
 | 
					        form.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || "");
 | 
				
			||||||
        form.elements["info.author"].value = data.info?.author || "";
 | 
					        form.elements["info.author"].value = data.info?.author || "";
 | 
				
			||||||
        form.elements["social.instagram_url"].value = data.social?.instagram_url || "";
 | 
					        form.elements["social.instagram_url"].value = data.social?.instagram_url || "";
 | 
				
			||||||
        form.elements["social.thumbnail"].value = data.social?.thumbnail || "";
 | 
					        if (thumbnailInput) thumbnailInput.value = data.social?.thumbnail || "";
 | 
				
			||||||
 | 
					        if (thumbnailPreview && data.social?.thumbnail) {
 | 
				
			||||||
 | 
					          thumbnailPreview.src = `/photos/${data.social.thumbnail}`;
 | 
				
			||||||
 | 
					          thumbnailPreview.style.display = "block";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        form.elements["footer.copyright"].value = data.footer?.copyright || "";
 | 
					        form.elements["footer.copyright"].value = data.footer?.copyright || "";
 | 
				
			||||||
        form.elements["footer.legal_label"].value = data.footer?.legal_label || "";
 | 
					        form.elements["footer.legal_label"].value = data.footer?.legal_label || "";
 | 
				
			||||||
        form.elements["build.theme"].value = data.build?.theme || "";
 | 
					        if (themeSelect) {
 | 
				
			||||||
 | 
					          themeSelect.value = data.build?.theme || "";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        form.elements["legals.hoster_name"].value = data.legals?.hoster_name || "";
 | 
					        form.elements["legals.hoster_name"].value = data.legals?.hoster_name || "";
 | 
				
			||||||
        form.elements["legals.hoster_adress"].value = data.legals?.hoster_adress || "";
 | 
					        form.elements["legals.hoster_adress"].value = data.legals?.hoster_adress || "";
 | 
				
			||||||
        form.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || "";
 | 
					        form.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || "";
 | 
				
			||||||
@@ -149,9 +207,9 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
      updateMenuItemsFromInputs();
 | 
					      updateMenuItemsFromInputs();
 | 
				
			||||||
      updateIpParagraphsFromInputs();
 | 
					      updateIpParagraphsFromInputs();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // --- Build object with checkboxes ---
 | 
					      // --- Build object with checkboxes and theme select ---
 | 
				
			||||||
      const build = {
 | 
					      const build = {
 | 
				
			||||||
        theme: form.elements["build.theme"].value,
 | 
					        theme: themeSelect ? themeSelect.value : "",
 | 
				
			||||||
        convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked),
 | 
					        convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked),
 | 
				
			||||||
        resize_images: !!(resizeImagesCheckbox && resizeImagesCheckbox.checked)
 | 
					        resize_images: !!(resizeImagesCheckbox && resizeImagesCheckbox.checked)
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
@@ -167,7 +225,7 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        social: {
 | 
					        social: {
 | 
				
			||||||
          instagram_url: form.elements["social.instagram_url"].value,
 | 
					          instagram_url: form.elements["social.instagram_url"].value,
 | 
				
			||||||
          thumbnail: form.elements["social.thumbnail"].value
 | 
					          thumbnail: thumbnailInput ? thumbnailInput.value : ""
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        menu: {
 | 
					        menu: {
 | 
				
			||||||
          items: menuItems
 | 
					          items: menuItems
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,8 +55,10 @@
 | 
				
			|||||||
      <fieldset>
 | 
					      <fieldset>
 | 
				
			||||||
        <legend>Social</legend>
 | 
					        <legend>Social</legend>
 | 
				
			||||||
        <label>Instagram URL: <input type="text" name="social.instagram_url"></label><br>
 | 
					        <label>Instagram URL: <input type="text" name="social.instagram_url"></label><br>
 | 
				
			||||||
        <label>Thumbnail: <input type="text" name="social.thumbnail"></label><br>
 | 
					        <label>Thumbnail: <input type="text" name="social.thumbnail" readonly></label>
 | 
				
			||||||
      </fieldset>
 | 
					        <input type="file" id="thumbnail-upload" accept="image/png,image/jpeg,image/webp">
 | 
				
			||||||
 | 
					        <img id="thumbnail-preview" src="" alt="Thumbnail preview" style="max-width:100px;display:none;">
 | 
				
			||||||
 | 
					        </fieldset>
 | 
				
			||||||
      <fieldset>
 | 
					      <fieldset>
 | 
				
			||||||
        <legend>Menu</legend>
 | 
					        <legend>Menu</legend>
 | 
				
			||||||
        <div id="menu-items-list"></div>
 | 
					        <div id="menu-items-list"></div>
 | 
				
			||||||
@@ -69,7 +71,9 @@
 | 
				
			|||||||
      </fieldset>
 | 
					      </fieldset>
 | 
				
			||||||
      <fieldset>
 | 
					      <fieldset>
 | 
				
			||||||
        <legend>Build</legend>
 | 
					        <legend>Build</legend>
 | 
				
			||||||
        <label>Theme: <input type="text" name="build.theme"></label><br>
 | 
					        <label>Theme:
 | 
				
			||||||
 | 
					            <select name="build.theme" id="theme-select"></select>
 | 
				
			||||||
 | 
					        </label>
 | 
				
			||||||
        <label>
 | 
					        <label>
 | 
				
			||||||
            <input type="checkbox" name="build.convert_images" id="convert-images-checkbox">
 | 
					            <input type="checkbox" name="build.convert_images" id="convert-images-checkbox">
 | 
				
			||||||
            Convert images
 | 
					            Convert images
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user