2.0 - WebUI builder ("Cielight" merge) #9
@@ -28,6 +28,30 @@ 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 ---
 | 
				
			||||||
 | 
					def get_theme_name():
 | 
				
			||||||
 | 
					    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):
 | 
				
			||||||
 | 
					    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):
 | 
				
			||||||
 | 
					    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):
 | 
				
			||||||
 | 
					    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 ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route("/")
 | 
					@app.route("/")
 | 
				
			||||||
@@ -133,26 +157,30 @@ def delete_all_hero_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."""
 | 
					    """Serve uploaded photos from disk for a specific 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)."""
 | 
				
			||||||
    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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route("/site-info")
 | 
					@app.route("/site-info")
 | 
				
			||||||
def site_info():
 | 
					def site_info():
 | 
				
			||||||
 | 
					    """Serve the 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."""
 | 
				
			||||||
    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."""
 | 
				
			||||||
    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)
 | 
				
			||||||
@@ -160,13 +188,14 @@ def update_site_info():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@app.route("/api/themes")
 | 
					@app.route("/api/themes")
 | 
				
			||||||
def list_themes():
 | 
					def list_themes():
 | 
				
			||||||
 | 
					    """List available themes (folders in config/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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
@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."""
 | 
				
			||||||
    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:
 | 
				
			||||||
@@ -183,6 +212,7 @@ 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."""
 | 
				
			||||||
    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
 | 
					    # Remove thumbnail file if exists
 | 
				
			||||||
@@ -199,6 +229,7 @@ def remove_thumbnail():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@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."""
 | 
				
			||||||
    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:
 | 
				
			||||||
@@ -215,6 +246,117 @@ 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 ---
 | 
				
			||||||
 | 
					@app.route("/theme-editor")
 | 
				
			||||||
 | 
					def theme_editor():
 | 
				
			||||||
 | 
					    """Serve the theme editor page."""
 | 
				
			||||||
 | 
					    return render_template("theme-editor/index.html")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route("/api/theme-info", methods=["GET", "POST"])
 | 
				
			||||||
 | 
					def api_theme_info():
 | 
				
			||||||
 | 
					    theme_name = get_theme_name()
 | 
				
			||||||
 | 
					    if request.method == "GET":
 | 
				
			||||||
 | 
					        theme_yaml = get_theme_yaml(theme_name)
 | 
				
			||||||
 | 
					        google_fonts = theme_yaml.get("google_fonts", [])
 | 
				
			||||||
 | 
					        return jsonify({
 | 
				
			||||||
 | 
					            "theme_name": theme_name,
 | 
				
			||||||
 | 
					            "theme_yaml": theme_yaml,
 | 
				
			||||||
 | 
					            "google_fonts": google_fonts
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        data = request.get_json()
 | 
				
			||||||
 | 
					        theme_yaml = data.get("theme_yaml")
 | 
				
			||||||
 | 
					        theme_name = data.get("theme_name", theme_name)
 | 
				
			||||||
 | 
					        save_theme_yaml(theme_name, theme_yaml)
 | 
				
			||||||
 | 
					        return jsonify({"status": "ok"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route("/api/local-fonts")
 | 
				
			||||||
 | 
					def api_local_fonts():
 | 
				
			||||||
 | 
					    theme_name = request.args.get("theme")
 | 
				
			||||||
 | 
					    fonts = get_local_fonts(theme_name)
 | 
				
			||||||
 | 
					    return jsonify(fonts)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route("/api/favicon/upload", methods=["POST"])
 | 
				
			||||||
 | 
					def upload_favicon():
 | 
				
			||||||
 | 
					    """Upload favicon to theme folder and update theme.yaml."""
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					    ext = Path(file.filename).suffix.lower()
 | 
				
			||||||
 | 
					    if ext not in [".png", ".jpg", ".jpeg", ".ico"]:
 | 
				
			||||||
 | 
					        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)
 | 
				
			||||||
 | 
					    theme_yaml.setdefault("favicon", {})["path"] = filename
 | 
				
			||||||
 | 
					    with open(theme_yaml_path, "w") as f:
 | 
				
			||||||
 | 
					        yaml.safe_dump(theme_yaml, f, sort_keys=False, allow_unicode=True)
 | 
				
			||||||
 | 
					    return jsonify({"status": "ok", "filename": filename})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route("/api/favicon/remove", methods=["POST"])
 | 
				
			||||||
 | 
					def remove_favicon():
 | 
				
			||||||
 | 
					    """Remove favicon from theme folder and update theme.yaml."""
 | 
				
			||||||
 | 
					    data = request.get_json()
 | 
				
			||||||
 | 
					    theme_name = data.get("theme")
 | 
				
			||||||
 | 
					    if not theme_name:
 | 
				
			||||||
 | 
					        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)
 | 
				
			||||||
 | 
					    if "favicon" in theme_yaml:
 | 
				
			||||||
 | 
					        theme_yaml["favicon"]["path"] = ""
 | 
				
			||||||
 | 
					    with open(theme_yaml_path, "w") as f:
 | 
				
			||||||
 | 
					        yaml.safe_dump(theme_yaml, f, sort_keys=False, allow_unicode=True)
 | 
				
			||||||
 | 
					    return jsonify({"status": "ok"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route("/themes/<theme>/<filename>")
 | 
				
			||||||
 | 
					def serve_theme_asset(theme, filename):
 | 
				
			||||||
 | 
					    theme_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme
 | 
				
			||||||
 | 
					    return send_from_directory(theme_dir, filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route("/api/font/upload", methods=["POST"])
 | 
				
			||||||
 | 
					def upload_font():
 | 
				
			||||||
 | 
					    """Upload a font file to the theme's fonts folder (only .woff/.woff2 allowed)."""
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					    ext = Path(file.filename).suffix.lower()
 | 
				
			||||||
 | 
					    if ext not in [".woff", ".woff2"]:
 | 
				
			||||||
 | 
					        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)
 | 
				
			||||||
 | 
					    return jsonify({"status": "ok", "filename": file.filename})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route("/api/font/remove", methods=["POST"])
 | 
				
			||||||
 | 
					def remove_font():
 | 
				
			||||||
 | 
					    """Remove a font file from the theme's fonts folder."""
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# --- Run server ---
 | 
					# --- Run server ---
 | 
				
			||||||
if __name__ == "__main__":
 | 
					if __name__ == "__main__":
 | 
				
			||||||
    logging.info("Starting WebUI at http://127.0.0.1:5000")
 | 
					    logging.info("Starting WebUI at http://127.0.0.1:5000")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,8 +29,8 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
      div.style.gap = "8px";
 | 
					      div.style.gap = "8px";
 | 
				
			||||||
      div.style.marginBottom = "6px";
 | 
					      div.style.marginBottom = "6px";
 | 
				
			||||||
      div.innerHTML = `
 | 
					      div.innerHTML = `
 | 
				
			||||||
        <input type="text" placeholder="Label" value="${item.label || ""}" style="flex:1;" data-idx="${idx}" data-type="label">
 | 
					        <input type="text" placeholder="Label" value="${item.label || ""}" style="flex:1;" data-idx="${idx}" data-type="label" required>
 | 
				
			||||||
        <input type="text" placeholder="?tag=tag1,tag2" value="${item.href || ""}" style="flex:2;" data-idx="${idx}" data-type="href">
 | 
					        <input type="text" placeholder="?tag=tag1,tag2" value="${item.href || ""}" style="flex:2;" data-idx="${idx}" data-type="href" required>
 | 
				
			||||||
        <button type="button" class="remove-menu-item" data-idx="${idx}">🗑</button>
 | 
					        <button type="button" class="remove-menu-item" data-idx="${idx}">🗑</button>
 | 
				
			||||||
      `;
 | 
					      `;
 | 
				
			||||||
      menuList.appendChild(div);
 | 
					      menuList.appendChild(div);
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										393
									
								
								src/webui/js/theme-editor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										393
									
								
								src/webui/js/theme-editor.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,393 @@
 | 
				
			|||||||
 | 
					async function fetchThemeInfo() {
 | 
				
			||||||
 | 
					  const res = await fetch("/api/theme-info");
 | 
				
			||||||
 | 
					  return await res.json();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function fetchLocalFonts(theme) {
 | 
				
			||||||
 | 
					  const res = await fetch(`/api/local-fonts?theme=${encodeURIComponent(theme)}`);
 | 
				
			||||||
 | 
					  return await res.json();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function removeFont(theme, font) {
 | 
				
			||||||
 | 
					  const res = await fetch("/api/font/remove", {
 | 
				
			||||||
 | 
					    method: "POST",
 | 
				
			||||||
 | 
					    headers: { "Content-Type": "application/json" },
 | 
				
			||||||
 | 
					    body: JSON.stringify({ theme, font })
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  return await res.json();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function setColorInput(colorId, textId, value) {
 | 
				
			||||||
 | 
					  const colorInput = document.getElementById(colorId);
 | 
				
			||||||
 | 
					  const textInput = document.getElementById(textId);
 | 
				
			||||||
 | 
					  if (colorInput) colorInput.value = value;
 | 
				
			||||||
 | 
					  if (textInput) textInput.value = value;
 | 
				
			||||||
 | 
					  if (colorInput && textInput) {
 | 
				
			||||||
 | 
					    colorInput.addEventListener("input", () => {
 | 
				
			||||||
 | 
					      textInput.value = colorInput.value;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    textInput.addEventListener("input", () => {
 | 
				
			||||||
 | 
					      colorInput.value = textInput.value;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function setFontDropdown(selectId, value, options) {
 | 
				
			||||||
 | 
					  const select = document.getElementById(selectId);
 | 
				
			||||||
 | 
					  if (!select) return;
 | 
				
			||||||
 | 
					  select.innerHTML = options.map(opt =>
 | 
				
			||||||
 | 
					    `<option value="${opt}"${opt === value ? " selected" : ""}>${opt}</option>`
 | 
				
			||||||
 | 
					  ).join("");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function setFallbackDropdown(selectId, value) {
 | 
				
			||||||
 | 
					  const select = document.getElementById(selectId);
 | 
				
			||||||
 | 
					  if (!select) return;
 | 
				
			||||||
 | 
					  select.value = (value === "serif" || value === "sans-serif") ? value : "sans-serif";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function setTextInput(inputId, value) {
 | 
				
			||||||
 | 
					  const input = document.getElementById(inputId);
 | 
				
			||||||
 | 
					  if (input) input.value = value;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function renderGoogleFonts(googleFonts) {
 | 
				
			||||||
 | 
					  const container = document.getElementById("google-fonts-fields");
 | 
				
			||||||
 | 
					  container.innerHTML = "";
 | 
				
			||||||
 | 
					  googleFonts.forEach((font, idx) => {
 | 
				
			||||||
 | 
					    container.innerHTML += `
 | 
				
			||||||
 | 
					      <div class="input-field" data-idx="${idx}">
 | 
				
			||||||
 | 
					        <label>Family</label>
 | 
				
			||||||
 | 
					        <input type="text" name="google_fonts[${idx}][family]" value="${font.family || ""}">
 | 
				
			||||||
 | 
					        <label>Weights (comma separated)</label>
 | 
				
			||||||
 | 
					        <input type="text" name="google_fonts[${idx}][weights]" value="${(font.weights || []).join(',')}">
 | 
				
			||||||
 | 
					        <button type="button" class="remove-google-font" data-idx="${idx}">Remove</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function renderLocalFonts(fonts) {
 | 
				
			||||||
 | 
					  const listDiv = document.getElementById("local-fonts-list");
 | 
				
			||||||
 | 
					  if (!listDiv) return;
 | 
				
			||||||
 | 
					  listDiv.innerHTML = "";
 | 
				
			||||||
 | 
					  fonts.forEach(font => {
 | 
				
			||||||
 | 
					    listDiv.innerHTML += `
 | 
				
			||||||
 | 
					      <div class="font-item">
 | 
				
			||||||
 | 
					        <span>${font}</span>
 | 
				
			||||||
 | 
					        <button type="button" class="remove-font-btn danger" data-font="${font}">Remove</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			||||||
 | 
					  const themeInfo = await fetchThemeInfo();
 | 
				
			||||||
 | 
					  const themeNameSpan = document.getElementById("current-theme");
 | 
				
			||||||
 | 
					  if (themeNameSpan) themeNameSpan.textContent = themeInfo.theme_name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const themeYaml = themeInfo.theme_yaml;
 | 
				
			||||||
 | 
					  const googleFonts = themeYaml.google_fonts ? JSON.parse(JSON.stringify(themeYaml.google_fonts)) : [];
 | 
				
			||||||
 | 
					  let localFonts = await fetchLocalFonts(themeInfo.theme_name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Colors
 | 
				
			||||||
 | 
					  if (themeYaml.colors) {
 | 
				
			||||||
 | 
					    setColorInput("color-primary", "color-primary-text", themeYaml.colors.primary || "#0065a1");
 | 
				
			||||||
 | 
					    setColorInput("color-primary-dark", "color-primary-dark-text", themeYaml.colors.primary_dark || "#005384");
 | 
				
			||||||
 | 
					    setColorInput("color-secondary", "color-secondary-text", themeYaml.colors.secondary || "#00b0f0");
 | 
				
			||||||
 | 
					    setColorInput("color-accent", "color-accent-text", themeYaml.colors.accent || "#ffc700");
 | 
				
			||||||
 | 
					    setColorInput("color-text-dark", "color-text-dark-text", themeYaml.colors.text_dark || "#616161");
 | 
				
			||||||
 | 
					    setColorInput("color-background", "color-background-text", themeYaml.colors.background || "#fff");
 | 
				
			||||||
 | 
					    setColorInput("color-browser-color", "color-browser-color-text", themeYaml.colors.browser_color || "#fff");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Fonts
 | 
				
			||||||
 | 
					  function refreshFontDropdowns() {
 | 
				
			||||||
 | 
					    setFontDropdown("font-primary", document.getElementById("font-primary").value, [
 | 
				
			||||||
 | 
					      ...googleFonts.map(f => f.family),
 | 
				
			||||||
 | 
					      ...localFonts
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					    setFontDropdown("font-secondary", document.getElementById("font-secondary").value, [
 | 
				
			||||||
 | 
					      ...googleFonts.map(f => f.family),
 | 
				
			||||||
 | 
					      ...localFonts
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (themeYaml.fonts) {
 | 
				
			||||||
 | 
					    setFontDropdown("font-primary", themeYaml.fonts.primary?.name || "Lato", [
 | 
				
			||||||
 | 
					      ...googleFonts.map(f => f.family),
 | 
				
			||||||
 | 
					      ...localFonts
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					    setFallbackDropdown("font-primary-fallback", themeYaml.fonts.primary?.fallback || "sans-serif");
 | 
				
			||||||
 | 
					    setFontDropdown("font-secondary", themeYaml.fonts.secondary?.name || "Montserrat", [
 | 
				
			||||||
 | 
					      ...googleFonts.map(f => f.family),
 | 
				
			||||||
 | 
					      ...localFonts
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					    setFallbackDropdown("font-secondary-fallback", themeYaml.fonts.secondary?.fallback || "serif");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Font upload logic
 | 
				
			||||||
 | 
					  const fontUploadInput = document.getElementById("font-upload");
 | 
				
			||||||
 | 
					  const chooseFontBtn = document.getElementById("choose-font-btn");
 | 
				
			||||||
 | 
					  const fontUploadStatus = document.getElementById("font-upload-status");
 | 
				
			||||||
 | 
					  const localFontsList = document.getElementById("local-fonts-list");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Modal logic for font deletion
 | 
				
			||||||
 | 
					  const deleteFontModal = document.getElementById("delete-font-modal");
 | 
				
			||||||
 | 
					  const deleteFontModalClose = document.getElementById("delete-font-modal-close");
 | 
				
			||||||
 | 
					  const deleteFontModalConfirm = document.getElementById("delete-font-modal-confirm");
 | 
				
			||||||
 | 
					  const deleteFontModalCancel = document.getElementById("delete-font-modal-cancel");
 | 
				
			||||||
 | 
					  let fontToDelete = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function refreshLocalFonts() {
 | 
				
			||||||
 | 
					    renderLocalFonts(localFonts);
 | 
				
			||||||
 | 
					    refreshFontDropdowns();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (chooseFontBtn && fontUploadInput) {
 | 
				
			||||||
 | 
					    chooseFontBtn.addEventListener("click", () => fontUploadInput.click());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (fontUploadInput) {
 | 
				
			||||||
 | 
					    fontUploadInput.addEventListener("change", async (e) => {
 | 
				
			||||||
 | 
					      const file = e.target.files[0];
 | 
				
			||||||
 | 
					      if (!file) return;
 | 
				
			||||||
 | 
					      const ext = file.name.split('.').pop().toLowerCase();
 | 
				
			||||||
 | 
					      if (!["woff", "woff2"].includes(ext)) {
 | 
				
			||||||
 | 
					        fontUploadStatus.textContent = "Only .woff and .woff2 fonts are allowed.";
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const formData = new FormData();
 | 
				
			||||||
 | 
					      formData.append("file", file);
 | 
				
			||||||
 | 
					      formData.append("theme", themeInfo.theme_name);
 | 
				
			||||||
 | 
					      const res = await fetch("/api/font/upload", { method: "POST", body: formData });
 | 
				
			||||||
 | 
					      const result = await res.json();
 | 
				
			||||||
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
 | 
					        fontUploadStatus.textContent = "Font uploaded!";
 | 
				
			||||||
 | 
					        showToast("Font uploaded!", "success");
 | 
				
			||||||
 | 
					        localFonts = await fetchLocalFonts(themeInfo.theme_name);
 | 
				
			||||||
 | 
					        refreshLocalFonts();
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        fontUploadStatus.textContent = "Error uploading font.";
 | 
				
			||||||
 | 
					        showToast("Error uploading font.", "error");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Remove font button triggers modal
 | 
				
			||||||
 | 
					  if (localFontsList) {
 | 
				
			||||||
 | 
					    localFontsList.addEventListener("click", (e) => {
 | 
				
			||||||
 | 
					      if (e.target.classList.contains("remove-font-btn")) {
 | 
				
			||||||
 | 
					        fontToDelete = e.target.dataset.font;
 | 
				
			||||||
 | 
					        document.getElementById("delete-font-modal-text").textContent =
 | 
				
			||||||
 | 
					          `Are you sure you want to remove the font "${fontToDelete}"?`;
 | 
				
			||||||
 | 
					        deleteFontModal.style.display = "flex";
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Modal logic for font deletion
 | 
				
			||||||
 | 
					  if (deleteFontModal && deleteFontModalClose && deleteFontModalConfirm && deleteFontModalCancel) {
 | 
				
			||||||
 | 
					    deleteFontModalClose.onclick = deleteFontModalCancel.onclick = () => {
 | 
				
			||||||
 | 
					      deleteFontModal.style.display = "none";
 | 
				
			||||||
 | 
					      fontToDelete = null;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    window.onclick = function(event) {
 | 
				
			||||||
 | 
					      if (event.target === deleteFontModal) {
 | 
				
			||||||
 | 
					        deleteFontModal.style.display = "none";
 | 
				
			||||||
 | 
					        fontToDelete = null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    deleteFontModalConfirm.onclick = async () => {
 | 
				
			||||||
 | 
					      if (!fontToDelete) return;
 | 
				
			||||||
 | 
					      const result = await removeFont(themeInfo.theme_name, fontToDelete);
 | 
				
			||||||
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
 | 
					        showToast("Font removed!", "success");
 | 
				
			||||||
 | 
					        localFonts = await fetchLocalFonts(themeInfo.theme_name);
 | 
				
			||||||
 | 
					        refreshLocalFonts();
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        showToast("Error removing font.", "error");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      deleteFontModal.style.display = "none";
 | 
				
			||||||
 | 
					      fontToDelete = null;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Initial render of local fonts
 | 
				
			||||||
 | 
					  refreshLocalFonts();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Favicon logic
 | 
				
			||||||
 | 
					  const faviconInput = document.getElementById("favicon-path");
 | 
				
			||||||
 | 
					  const faviconUpload = document.getElementById("favicon-upload");
 | 
				
			||||||
 | 
					  const chooseFaviconBtn = document.getElementById("choose-favicon-btn");
 | 
				
			||||||
 | 
					  const faviconPreview = document.getElementById("favicon-preview");
 | 
				
			||||||
 | 
					  const removeFaviconBtn = document.getElementById("remove-favicon-btn");
 | 
				
			||||||
 | 
					  const deleteFaviconModal = document.getElementById("delete-favicon-modal");
 | 
				
			||||||
 | 
					  const deleteFaviconModalClose = document.getElementById("delete-favicon-modal-close");
 | 
				
			||||||
 | 
					  const deleteFaviconModalConfirm = document.getElementById("delete-favicon-modal-confirm");
 | 
				
			||||||
 | 
					  const deleteFaviconModalCancel = document.getElementById("delete-favicon-modal-cancel");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function updateFaviconPreview(src) {
 | 
				
			||||||
 | 
					    if (faviconPreview) {
 | 
				
			||||||
 | 
					      faviconPreview.src = src || "";
 | 
				
			||||||
 | 
					      faviconPreview.style.display = src ? "inline-block" : "none";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (removeFaviconBtn) {
 | 
				
			||||||
 | 
					      removeFaviconBtn.style.display = src ? "inline-block" : "none";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (chooseFaviconBtn) {
 | 
				
			||||||
 | 
					      chooseFaviconBtn.style.display = src ? "none" : "inline-block";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (chooseFaviconBtn && faviconUpload) {
 | 
				
			||||||
 | 
					    chooseFaviconBtn.addEventListener("click", () => faviconUpload.click());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (faviconUpload) {
 | 
				
			||||||
 | 
					    faviconUpload.addEventListener("change", async (e) => {
 | 
				
			||||||
 | 
					      const file = e.target.files[0];
 | 
				
			||||||
 | 
					      if (!file) return;
 | 
				
			||||||
 | 
					      const ext = file.name.split('.').pop().toLowerCase();
 | 
				
			||||||
 | 
					      if (!["png", "jpg", "jpeg", "ico"].includes(ext)) {
 | 
				
			||||||
 | 
					        showToast("Invalid file type for favicon.", "error");
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const formData = new FormData();
 | 
				
			||||||
 | 
					      formData.append("file", file);
 | 
				
			||||||
 | 
					      formData.append("theme", themeInfo.theme_name);
 | 
				
			||||||
 | 
					      const res = await fetch("/api/favicon/upload", { method: "POST", body: formData });
 | 
				
			||||||
 | 
					      const result = await res.json();
 | 
				
			||||||
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
 | 
					        faviconInput.value = result.filename;
 | 
				
			||||||
 | 
					        updateFaviconPreview(`/themes/${themeInfo.theme_name}/${result.filename}?t=${Date.now()}`);
 | 
				
			||||||
 | 
					        showToast("Favicon uploaded!", "success");
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        showToast("Error uploading favicon", "error");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (removeFaviconBtn) {
 | 
				
			||||||
 | 
					    removeFaviconBtn.addEventListener("click", () => {
 | 
				
			||||||
 | 
					      deleteFaviconModal.style.display = "flex";
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (deleteFaviconModal && deleteFaviconModalClose && deleteFaviconModalConfirm && deleteFaviconModalCancel) {
 | 
				
			||||||
 | 
					    deleteFaviconModalClose.onclick = deleteFaviconModalCancel.onclick = () => {
 | 
				
			||||||
 | 
					      deleteFaviconModal.style.display = "none";
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    window.onclick = function(event) {
 | 
				
			||||||
 | 
					      if (event.target === deleteFaviconModal) {
 | 
				
			||||||
 | 
					        deleteFaviconModal.style.display = "none";
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    deleteFaviconModalConfirm.onclick = async () => {
 | 
				
			||||||
 | 
					      const res = await fetch("/api/favicon/remove", {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        headers: { "Content-Type": "application/json" },
 | 
				
			||||||
 | 
					        body: JSON.stringify({ theme: themeInfo.theme_name })
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      const result = await res.json();
 | 
				
			||||||
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
 | 
					        faviconInput.value = "";
 | 
				
			||||||
 | 
					        updateFaviconPreview("");
 | 
				
			||||||
 | 
					        showToast("Favicon removed!", "success");
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        showToast("Error removing favicon", "error");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      deleteFaviconModal.style.display = "none";
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (themeYaml.favicon && themeYaml.favicon.path) {
 | 
				
			||||||
 | 
					    faviconInput.value = themeYaml.favicon.path;
 | 
				
			||||||
 | 
					    updateFaviconPreview(`/themes/${themeInfo.theme_name}/${themeYaml.favicon.path}?t=${Date.now()}`);
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    updateFaviconPreview("");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Google Fonts
 | 
				
			||||||
 | 
					  renderGoogleFonts(googleFonts);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Add Google Font
 | 
				
			||||||
 | 
					  const addGoogleFontBtn = document.getElementById("add-google-font");
 | 
				
			||||||
 | 
					  if (addGoogleFontBtn) {
 | 
				
			||||||
 | 
					    addGoogleFontBtn.addEventListener("click", () => {
 | 
				
			||||||
 | 
					      googleFonts.push({ family: "", weights: [] });
 | 
				
			||||||
 | 
					      renderGoogleFonts(googleFonts);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Remove Google Font
 | 
				
			||||||
 | 
					  const googleFontsFields = document.getElementById("google-fonts-fields");
 | 
				
			||||||
 | 
					  if (googleFontsFields) {
 | 
				
			||||||
 | 
					    googleFontsFields.addEventListener("click", (e) => {
 | 
				
			||||||
 | 
					      if (e.target.classList.contains("remove-google-font")) {
 | 
				
			||||||
 | 
					        const idx = parseInt(e.target.dataset.idx, 10);
 | 
				
			||||||
 | 
					        googleFonts.splice(idx, 1);
 | 
				
			||||||
 | 
					        renderGoogleFonts(googleFonts);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Form submit
 | 
				
			||||||
 | 
					  document.getElementById("theme-editor-form").addEventListener("submit", async (e) => {
 | 
				
			||||||
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					    const data = {};
 | 
				
			||||||
 | 
					    data.colors = {
 | 
				
			||||||
 | 
					      primary: document.getElementById("color-primary-text").value,
 | 
				
			||||||
 | 
					      primary_dark: document.getElementById("color-primary-dark-text").value,
 | 
				
			||||||
 | 
					      secondary: document.getElementById("color-secondary-text").value,
 | 
				
			||||||
 | 
					      accent: document.getElementById("color-accent-text").value,
 | 
				
			||||||
 | 
					      text_dark: document.getElementById("color-text-dark-text").value,
 | 
				
			||||||
 | 
					      background: document.getElementById("color-background-text").value,
 | 
				
			||||||
 | 
					      browser_color: document.getElementById("color-browser-color-text").value
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    data.fonts = {
 | 
				
			||||||
 | 
					      primary: {
 | 
				
			||||||
 | 
					        name: document.getElementById("font-primary").value,
 | 
				
			||||||
 | 
					        fallback: document.getElementById("font-primary-fallback").value
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      secondary: {
 | 
				
			||||||
 | 
					        name: document.getElementById("font-secondary").value,
 | 
				
			||||||
 | 
					        fallback: document.getElementById("font-secondary-fallback").value
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    data.favicon = {
 | 
				
			||||||
 | 
					      path: faviconInput.value
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    data.google_fonts = [];
 | 
				
			||||||
 | 
					    document.querySelectorAll("#google-fonts-fields .input-field").forEach(field => {
 | 
				
			||||||
 | 
					      const family = field.querySelector('input[name^="google_fonts"][name$="[family]"]').value;
 | 
				
			||||||
 | 
					      const weights = field.querySelector('input[name^="google_fonts"][name$="[weights]"]').value
 | 
				
			||||||
 | 
					        .split(",").map(w => w.trim()).filter(w => w);
 | 
				
			||||||
 | 
					      if (family) data.google_fonts.push({ family, weights });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const res = await fetch("/api/theme-info", {
 | 
				
			||||||
 | 
					      method: "POST",
 | 
				
			||||||
 | 
					      headers: { "Content-Type": "application/json" },
 | 
				
			||||||
 | 
					      body: JSON.stringify({ theme_name: themeInfo.theme_name, theme_yaml: data })
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    if (res.ok) {
 | 
				
			||||||
 | 
					      showToast("Theme saved!", "success");
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      showToast("Error saving theme.", "error");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@@ -49,27 +49,27 @@
 | 
				
			|||||||
        <div class="fields">
 | 
					        <div class="fields">
 | 
				
			||||||
          <div class="input-field">
 | 
					          <div class="input-field">
 | 
				
			||||||
            <label>Title</label>
 | 
					            <label>Title</label>
 | 
				
			||||||
            <input type="text" name="info.title" placeholder="Your site title">
 | 
					            <input type="text" name="info.title" placeholder="Your site title" required>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div class="input-field">
 | 
					          <div class="input-field">
 | 
				
			||||||
            <label>Subtitle</label>
 | 
					            <label>Subtitle</label>
 | 
				
			||||||
            <input type="text" name="info.subtitle" placeholder="Your site subtitle">
 | 
					            <input type="text" name="info.subtitle" placeholder="Your site subtitle" required>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div class="input-field">
 | 
					          <div class="input-field">
 | 
				
			||||||
            <label>Description</label>
 | 
					            <label>Description</label>
 | 
				
			||||||
            <input type="text" name="info.description" placeholder="Your site description">
 | 
					            <input type="text" name="info.description" placeholder="Your site description" required>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div class="input-field">
 | 
					          <div class="input-field">
 | 
				
			||||||
            <label>Canonical URL</label>
 | 
					            <label>Canonical URL</label>
 | 
				
			||||||
            <input type="text" name="info.canonical" placeholder="https://yoursite.com">
 | 
					            <input type="text" name="info.canonical" placeholder="https://yoursite.com" required>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div class="input-field">
 | 
					          <div class="input-field">
 | 
				
			||||||
            <label>Keywords (comma separated)</label>
 | 
					            <label>Keywords (comma separated)</label>
 | 
				
			||||||
            <input type="text" name="info.keywords" placeholder="photo, gallery, photography">
 | 
					            <input type="text" name="info.keywords" placeholder="photo, gallery, photography" required>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div class="input-field">
 | 
					          <div class="input-field">
 | 
				
			||||||
            <label>Author</label>
 | 
					            <label>Author</label>
 | 
				
			||||||
            <input type="text" name="info.author" placeholder="Your Name">
 | 
					            <input type="text" name="info.author" placeholder="Your Name" required>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </fieldset>
 | 
					      </fieldset>
 | 
				
			||||||
@@ -79,9 +79,9 @@
 | 
				
			|||||||
        <div class="fields">
 | 
					        <div class="fields">
 | 
				
			||||||
          <div class="input-field">
 | 
					          <div class="input-field">
 | 
				
			||||||
            <label>Instagram URL</label>
 | 
					            <label>Instagram URL</label>
 | 
				
			||||||
            <input type="text" name="social.instagram_url" placeholder="https://instagram.com/yourprofile">
 | 
					            <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;">
 | 
					            <input type="file" id="thumbnail-upload" accept="image/png,image/jpeg,image/webp" style="display:none;" required>
 | 
				
			||||||
            <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">
 | 
				
			||||||
            <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;">
 | 
				
			||||||
@@ -106,11 +106,11 @@
 | 
				
			|||||||
        <div class="fields">
 | 
					        <div class="fields">
 | 
				
			||||||
          <div class="input-field">
 | 
					          <div class="input-field">
 | 
				
			||||||
            <label>Copyright</label>
 | 
					            <label>Copyright</label>
 | 
				
			||||||
            <input type="text" name="footer.copyright">
 | 
					            <input type="text" name="footer.copyright" required>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div class="input-field">
 | 
					          <div class="input-field">
 | 
				
			||||||
            <label>Legal Label</label>
 | 
					            <label>Legal Label</label>
 | 
				
			||||||
            <input type="text" name="footer.legal_label">
 | 
					            <input type="text" name="footer.legal_label" re>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </fieldset>
 | 
					      </fieldset>
 | 
				
			||||||
@@ -143,7 +143,7 @@
 | 
				
			|||||||
        <div class="fields">
 | 
					        <div class="fields">
 | 
				
			||||||
          <div class="input-field">
 | 
					          <div class="input-field">
 | 
				
			||||||
            <label>Theme</label>
 | 
					            <label>Theme</label>
 | 
				
			||||||
            <select name="build.theme" id="theme-select"></select>
 | 
					            <select name="build.theme" id="theme-select" required></select>
 | 
				
			||||||
            <input type="file" id="theme-upload" webkitdirectory directory multiple style="display:none;">
 | 
					            <input type="file" id="theme-upload" webkitdirectory directory multiple style="display:none;">
 | 
				
			||||||
            <button type="button" id="choose-theme-btn" class="up-btn">📂 Upload custom theme folder</button>
 | 
					            <button type="button" id="choose-theme-btn" class="up-btn">📂 Upload custom theme folder</button>
 | 
				
			||||||
            <label class="thumbnail-form-label">Images processing</label>
 | 
					            <label class="thumbnail-form-label">Images processing</label>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -48,7 +48,7 @@ h2 {
 | 
				
			|||||||
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 | 
					  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
  border: 1px solid #2f2e2e80;
 | 
					  border: 1px solid #2f2e2e80;
 | 
				
			||||||
  border-radius: 8px;
 | 
					  border-radius: 8px;
 | 
				
			||||||
  padding: 0px 20px;
 | 
					  padding: 0px 20px 20px 20px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.upload-section label {
 | 
					.upload-section label {
 | 
				
			||||||
@@ -491,7 +491,7 @@ h2 {
 | 
				
			|||||||
  padding: 0 40px 40px 40px;
 | 
					  padding: 0 40px 40px 40px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form fieldset {
 | 
					fieldset {
 | 
				
			||||||
  background-color: rgb(67 67 67 / 26%);
 | 
					  background-color: rgb(67 67 67 / 26%);
 | 
				
			||||||
  border-radius: 6px;
 | 
					  border-radius: 6px;
 | 
				
			||||||
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 | 
					  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
@@ -501,7 +501,7 @@ h2 {
 | 
				
			|||||||
  margin: 32px auto;
 | 
					  margin: 32px auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form legend {
 | 
					legend {
 | 
				
			||||||
  font-size: 1.2em;
 | 
					  font-size: 1.2em;
 | 
				
			||||||
  font-weight: 700;
 | 
					  font-weight: 700;
 | 
				
			||||||
  color: #26c4ff;
 | 
					  color: #26c4ff;
 | 
				
			||||||
@@ -509,13 +509,13 @@ h2 {
 | 
				
			|||||||
  letter-spacing: 1px;
 | 
					  letter-spacing: 1px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form .fields {
 | 
					.fields {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-wrap: wrap;
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
  gap: 18px;
 | 
					  gap: 18px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form .input-field {
 | 
					.input-field {
 | 
				
			||||||
  flex: 1 1 calc(33.333% - 18px);
 | 
					  flex: 1 1 calc(33.333% - 18px);
 | 
				
			||||||
  min-width: 220px;
 | 
					  min-width: 220px;
 | 
				
			||||||
  max-width: 100%;
 | 
					  max-width: 100%;
 | 
				
			||||||
@@ -525,7 +525,7 @@ h2 {
 | 
				
			|||||||
  margin-bottom: 10px;
 | 
					  margin-bottom: 10px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form label {
 | 
					label {
 | 
				
			||||||
  font-size: 13px;
 | 
					  font-size: 13px;
 | 
				
			||||||
  font-weight: 600;
 | 
					  font-weight: 600;
 | 
				
			||||||
  color: #e3e3e3;
 | 
					  color: #e3e3e3;
 | 
				
			||||||
@@ -533,9 +533,9 @@ h2 {
 | 
				
			|||||||
  letter-spacing: 0.5px;
 | 
					  letter-spacing: 0.5px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form input,
 | 
					#site-info-form input, #theme-editor-form input,
 | 
				
			||||||
#site-info-form textarea,
 | 
					#site-info-form textarea, #theme-editor-form textarea,
 | 
				
			||||||
#site-info-form select {
 | 
					#site-info-form select, #theme-editor-form select {
 | 
				
			||||||
  /* background: rgba(4, 44, 60, 0.55);*/
 | 
					  /* background: rgba(4, 44, 60, 0.55);*/
 | 
				
			||||||
  background: #1f2223;
 | 
					  background: #1f2223;
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
@@ -551,23 +551,29 @@ h2 {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form input::placeholder,
 | 
					#site-info-form input::placeholder,
 | 
				
			||||||
#site-info-form textarea::placeholder {
 | 
					#theme-editor-form input::placeholder,
 | 
				
			||||||
 | 
					#site-info-form textarea::placeholder,
 | 
				
			||||||
 | 
					#theme-editor-form textarea::placeholder {
 | 
				
			||||||
  color: #585858;
 | 
					  color: #585858;
 | 
				
			||||||
  font-style: italic;
 | 
					  font-style: italic;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form input:focus,
 | 
					#site-info-form input:focus,
 | 
				
			||||||
 | 
					#theme-editor-form input:focus,
 | 
				
			||||||
#site-info-form textarea:focus,
 | 
					#site-info-form textarea:focus,
 | 
				
			||||||
#site-info-form select:focus {
 | 
					#theme-editor-form textarea:focus,
 | 
				
			||||||
 | 
					#site-info-form select:focus,
 | 
				
			||||||
 | 
					#theme-editor-form select:focus {
 | 
				
			||||||
  border-color: #585858;
 | 
					  border-color: #585858;
 | 
				
			||||||
  background: #161616;
 | 
					  background: #161616;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
#site-info-form textarea {
 | 
					#site-info-form textarea,
 | 
				
			||||||
 | 
					#theme-editor-form textarea {
 | 
				
			||||||
  min-height: 60px;
 | 
					  min-height: 60px;
 | 
				
			||||||
  resize: vertical;
 | 
					  resize: vertical;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form input[type="file"] {
 | 
					#input[type="file"] {
 | 
				
			||||||
  background: none;
 | 
					  background: none;
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
  border: none;
 | 
					  border: none;
 | 
				
			||||||
@@ -575,13 +581,13 @@ h2 {
 | 
				
			|||||||
  margin-top: 2px;
 | 
					  margin-top: 2px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form img#thumbnail-preview {
 | 
					img#thumbnail-preview {
 | 
				
			||||||
  margin-top: 8px;
 | 
					  margin-top: 8px;
 | 
				
			||||||
  border-radius: 8px;
 | 
					  border-radius: 8px;
 | 
				
			||||||
  border: 1px solid #585858;
 | 
					  border: 1px solid #585858;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form button[type="submit"] {
 | 
					#site-info-form button[type="submit"], #theme-editor-form button[type="submit"] {
 | 
				
			||||||
  background: linear-gradient(135deg, #26c4ff, #016074);
 | 
					  background: linear-gradient(135deg, #26c4ff, #016074);
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
  font-weight: 700;
 | 
					  font-weight: 700;
 | 
				
			||||||
@@ -595,11 +601,11 @@ h2 {
 | 
				
			|||||||
  transition: background 0.2s;
 | 
					  transition: background 0.2s;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form button[type="submit"]:hover {
 | 
					#site-info-form button[type="submit"]:hover,  #theme-editor-form button[type="submit"]:hover {
 | 
				
			||||||
  background: linear-gradient(135deg, #72d9ff, #26657e);
 | 
					  background: linear-gradient(135deg, #72d9ff, #26657e);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form button[type="button"] {
 | 
					#site-info-form button[type="button"], #theme-editor-form button[type="button"] {
 | 
				
			||||||
  background: #00000000;
 | 
					  background: #00000000;
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
  border: none;
 | 
					  border: none;
 | 
				
			||||||
@@ -612,47 +618,47 @@ h2 {
 | 
				
			|||||||
  border: 1px solid #585858;
 | 
					  border: 1px solid #585858;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form button[type="button"]:hover {
 | 
					#site-info-form button[type="button"]:hover, #theme-editor-form button[type="button"]:hover {
 | 
				
			||||||
  background: #2d2d2d;
 | 
					  background: #2d2d2d;
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media (max-width: 900px) {
 | 
					@media (max-width: 900px) {
 | 
				
			||||||
  #site-info-form {
 | 
					  #site-info-form, #theme-editor-form {
 | 
				
			||||||
    padding: 18px 8px;
 | 
					    padding: 18px 8px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  #site-info-form .fields,
 | 
					  .fields,
 | 
				
			||||||
  #site-info-form fieldset {
 | 
					  fieldset {
 | 
				
			||||||
    flex-direction: column;
 | 
					    flex-direction: column;
 | 
				
			||||||
    gap: 0;
 | 
					    gap: 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  #site-info-form .input-field {
 | 
					  .input-field {
 | 
				
			||||||
    min-width: 100%;
 | 
					    min-width: 100%;
 | 
				
			||||||
    margin-bottom: 12px;
 | 
					    margin-bottom: 12px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form button.remove-menu-item, #site-info-form button.remove-ip-paragraph {
 | 
					#site-info-form button.remove-menu-item, #site-info-form button.remove-ip-paragraph, #theme-editor-form button.remove-menu-item, #theme-editor-form button.remove-ip-paragraph {
 | 
				
			||||||
  margin-top: 0px;
 | 
					  margin-top: 0px;
 | 
				
			||||||
  margin-bottom: 4px;
 | 
					  margin-bottom: 4px;
 | 
				
			||||||
  border-radius: 30px;
 | 
					  border-radius: 30px;
 | 
				
			||||||
  background: #2d2d2d;
 | 
					  background: #2d2d2d;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form button.remove-menu-item:hover, #site-info-form button.remove-ip-paragraph:hover {
 | 
					#site-info-form button.remove-menu-item:hover, #site-info-form button.remove-ip-paragraph:hover, #theme-editor-form button.remove-menu-item:hover, #theme-editor-form button.remove-ip-paragraph:hover {
 | 
				
			||||||
  background: rgb(121, 26, 19);
 | 
					  background: rgb(121, 26, 19);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form button.remove-btn {
 | 
					#site-info-form button.remove-btn, #theme-editor-form button.remove-btn {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  border-radius: 30px;
 | 
					  border-radius: 30px;
 | 
				
			||||||
  background: #2d2d2d;
 | 
					  background: #2d2d2d;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form button.remove-btn:hover{
 | 
					#site-info-form button.remove-btn:hover, #theme-editor-form button.remove-btn:hover {
 | 
				
			||||||
  background: rgb(121, 26, 19);
 | 
					  background: rgb(121, 26, 19);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form .thumbnail-form-label {
 | 
					#site-info-form .thumbnail-form-label, #theme-editor-form .thumbnail-form-label {
 | 
				
			||||||
  margin-top: 10px;
 | 
					  margin-top: 10px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										178
									
								
								src/webui/theme-editor/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								src/webui/theme-editor/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,178 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					  <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
				
			||||||
 | 
					  <meta charset="UTF-8">
 | 
				
			||||||
 | 
					  <title>Theme Editor</title>
 | 
				
			||||||
 | 
					  <link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					  <!-- 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">
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="nav-btn">
 | 
				
			||||||
 | 
					        <label for="nav-check">
 | 
				
			||||||
 | 
					          <span></span>
 | 
				
			||||||
 | 
					          <span></span>
 | 
				
			||||||
 | 
					          <span></span>
 | 
				
			||||||
 | 
					        </label>
 | 
				
			||||||
 | 
					      </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>
 | 
				
			||||||
 | 
					        </ul>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  <!-- Toast container for notifications -->
 | 
				
			||||||
 | 
					  <div id="theme-editor" class="content-inner">
 | 
				
			||||||
 | 
					    <div id="toast-container"></div>
 | 
				
			||||||
 | 
					    <h1>Edit Theme</h1>
 | 
				
			||||||
 | 
					    <!-- Show current theme -->
 | 
				
			||||||
 | 
					    <div class="theme-info">
 | 
				
			||||||
 | 
					      <strong>Current theme:</strong> <span id="current-theme"></span>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <form id="theme-editor-form">
 | 
				
			||||||
 | 
					      <!-- Colors Section -->
 | 
				
			||||||
 | 
					      <fieldset>
 | 
				
			||||||
 | 
					        <h2>Colors</h2>
 | 
				
			||||||
 | 
					        <div class="fields">
 | 
				
			||||||
 | 
					          <div class="input-field">
 | 
				
			||||||
 | 
					            <label>Primary</label>
 | 
				
			||||||
 | 
					            <input type="color" name="colors.primary" id="color-primary">
 | 
				
			||||||
 | 
					            <input type="text" name="colors.primary_text" id="color-primary-text" style="width:100px;">
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="input-field">
 | 
				
			||||||
 | 
					            <label>Primary Dark</label>
 | 
				
			||||||
 | 
					            <input type="color" name="colors.primary_dark" id="color-primary-dark">
 | 
				
			||||||
 | 
					            <input type="text" name="colors.primary_dark_text" id="color-primary-dark-text" style="width:100px;">
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="input-field">
 | 
				
			||||||
 | 
					            <label>Secondary</label>
 | 
				
			||||||
 | 
					            <input type="color" name="colors.secondary" id="color-secondary">
 | 
				
			||||||
 | 
					            <input type="text" name="colors.secondary_text" id="color-secondary-text" style="width:100px;">
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="input-field">
 | 
				
			||||||
 | 
					            <label>Accent</label>
 | 
				
			||||||
 | 
					            <input type="color" name="colors.accent" id="color-accent">
 | 
				
			||||||
 | 
					            <input type="text" name="colors.accent_text" id="color-accent-text" style="width:100px;">
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="input-field">
 | 
				
			||||||
 | 
					            <label>Text Dark</label>
 | 
				
			||||||
 | 
					            <input type="color" name="colors.text_dark" id="color-text-dark">
 | 
				
			||||||
 | 
					            <input type="text" name="colors.text_dark_text" id="color-text-dark-text" style="width:100px;">
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="input-field">
 | 
				
			||||||
 | 
					            <label>Background</label>
 | 
				
			||||||
 | 
					            <input type="color" name="colors.background" id="color-background">
 | 
				
			||||||
 | 
					            <input type="text" name="colors.background_text" id="color-background-text" style="width:100px;">
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="input-field">
 | 
				
			||||||
 | 
					            <label>Browser Color</label>
 | 
				
			||||||
 | 
					            <input type="color" name="colors.browser_color" id="color-browser-color">
 | 
				
			||||||
 | 
					            <input type="text" name="colors.browser_color_text" id="color-browser-color-text" style="width:100px;">
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </fieldset>
 | 
				
			||||||
 | 
					      <!-- Google Fonts Section -->
 | 
				
			||||||
 | 
					      <fieldset>
 | 
				
			||||||
 | 
					        <h2>Google Fonts</h2>
 | 
				
			||||||
 | 
					        <div class="fields" id="google-fonts-fields">
 | 
				
			||||||
 | 
					          <!-- JS will render font family and weights inputs here -->
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <button type="button" id="add-google-font">Add Google Font</button>
 | 
				
			||||||
 | 
					      </fieldset>
 | 
				
			||||||
 | 
					      <!-- Custom Font Upload Section -->
 | 
				
			||||||
 | 
					      <fieldset>
 | 
				
			||||||
 | 
					        <h2>Upload Custom Font (.woff, .woff2)</h2>
 | 
				
			||||||
 | 
					        <div class="fields">
 | 
				
			||||||
 | 
					          <input type="file" id="font-upload" accept=".woff,.woff2" style="display:none;">
 | 
				
			||||||
 | 
					          <button type="button" id="choose-font-btn" class="up-btn">🖋️ Upload font</button>
 | 
				
			||||||
 | 
					          <div id="local-fonts-list" class="font-list"></div>
 | 
				
			||||||
 | 
					          <span id="font-upload-status"></span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </fieldset>
 | 
				
			||||||
 | 
					      <!-- Fonts Section -->
 | 
				
			||||||
 | 
					      <fieldset>
 | 
				
			||||||
 | 
					        <h2>Fonts</h2>
 | 
				
			||||||
 | 
					        <div class="fields">
 | 
				
			||||||
 | 
					          <div class="input-field">
 | 
				
			||||||
 | 
					            <label>Primary Font</label>
 | 
				
			||||||
 | 
					            <select name="fonts.primary.name" id="font-primary"></select>
 | 
				
			||||||
 | 
					            <label>Fallback</label>
 | 
				
			||||||
 | 
					            <select name="fonts.primary.fallback" id="font-primary-fallback">
 | 
				
			||||||
 | 
					              <option value="sans-serif">sans-serif</option>
 | 
				
			||||||
 | 
					              <option value="serif">serif</option>
 | 
				
			||||||
 | 
					            </select>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="input-field">
 | 
				
			||||||
 | 
					            <label>Secondary Font</label>
 | 
				
			||||||
 | 
					            <select name="fonts.secondary.name" id="font-secondary"></select>
 | 
				
			||||||
 | 
					            <label>Fallback</label>
 | 
				
			||||||
 | 
					            <select name="fonts.secondary.fallback" id="font-secondary-fallback">
 | 
				
			||||||
 | 
					              <option value="sans-serif">sans-serif</option>
 | 
				
			||||||
 | 
					              <option value="serif">serif</option>
 | 
				
			||||||
 | 
					            </select>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </fieldset>
 | 
				
			||||||
 | 
					      <!-- Favicon Section -->
 | 
				
			||||||
 | 
					      <fieldset>
 | 
				
			||||||
 | 
					        <h2>Favicon</h2>
 | 
				
			||||||
 | 
					        <div class="fields">
 | 
				
			||||||
 | 
					          <div class="input-field">
 | 
				
			||||||
 | 
					            <label>Favicon Path</label>
 | 
				
			||||||
 | 
					            <input type="text" name="favicon.path" id="favicon-path" readonly>
 | 
				
			||||||
 | 
					            <input type="file" id="favicon-upload" accept=".png,.jpg,.jpeg,.ico" style="display:none;">
 | 
				
			||||||
 | 
					            <button type="button" id="choose-favicon-btn" class="up-btn">🖼️ Upload favicon</button>
 | 
				
			||||||
 | 
					            <div class="favicon-form">
 | 
				
			||||||
 | 
					              <img id="favicon-preview" src="" alt="Favicon preview" style="max-width:48px;display:none;">
 | 
				
			||||||
 | 
					              <button type="button" id="remove-favicon-btn" class="remove-btn up-btn danger" style="display:none;">Remove</button>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </fieldset>
 | 
				
			||||||
 | 
					      <button type="submit">Save Theme</button>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  <!-- Delete confirmation modal for favicon -->
 | 
				
			||||||
 | 
					  <div id="delete-favicon-modal" class="modal" style="display:none;">
 | 
				
			||||||
 | 
					    <div class="modal-content">
 | 
				
			||||||
 | 
					      <span id="delete-favicon-modal-close" class="modal-close">×</span>
 | 
				
			||||||
 | 
					      <h3>Confirm Deletion</h3>
 | 
				
			||||||
 | 
					      <p id="delete-favicon-modal-text">Are you sure you want to remove this favicon?</p>
 | 
				
			||||||
 | 
					      <div class="modal-actions">
 | 
				
			||||||
 | 
					        <button id="delete-favicon-modal-confirm" class="modal-btn danger">Remove</button>
 | 
				
			||||||
 | 
					        <button id="delete-favicon-modal-cancel" class="modal-btn">Cancel</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  <!-- Delete confirmation modal for font -->
 | 
				
			||||||
 | 
					  <div id="delete-font-modal" class="modal" style="display:none;">
 | 
				
			||||||
 | 
					    <div class="modal-content">
 | 
				
			||||||
 | 
					      <span id="delete-font-modal-close" class="modal-close">×</span>
 | 
				
			||||||
 | 
					      <h3>Confirm Deletion</h3>
 | 
				
			||||||
 | 
					      <p id="delete-font-modal-text">Are you sure you want to remove this font?</p>
 | 
				
			||||||
 | 
					      <div class="modal-actions">
 | 
				
			||||||
 | 
					        <button id="delete-font-modal-confirm" class="modal-btn danger">Remove</button>
 | 
				
			||||||
 | 
					        <button id="delete-font-modal-cancel" class="modal-btn">Cancel</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  <script src="{{ url_for('static', filename='js/theme-editor.js') }}"></script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
		Reference in New Issue
	
	Block a user