diff --git a/src/py/webui/webui.py b/src/py/webui/webui.py index 6d132db..3425b0c 100644 --- a/src/py/webui/webui.py +++ b/src/py/webui/webui.py @@ -28,6 +28,30 @@ app.config["PHOTOS_DIR"] = PHOTOS_DIR # --- Register upload blueprint --- 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 --- @app.route("/") @@ -133,26 +157,30 @@ def delete_all_hero_photos(): @app.route("/photos/
/") 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) @app.route("/photos/") def serve_photo(filename): + """Serve uploaded photos from disk (generic).""" photos_dir = Path(__file__).resolve().parents[3] / "config" / "photos" return send_from_directory(photos_dir, filename) @app.route("/site-info") def site_info(): + """Serve the site info editor page.""" return render_template("site-info/index.html") @app.route("/api/site-info", methods=["GET"]) def get_site_info(): + """Return the site info YAML as JSON.""" with open(SITE_YAML, "r") as f: data = yaml.safe_load(f) return jsonify(data) @app.route("/api/site-info", methods=["POST"]) def update_site_info(): + """Update the site info YAML from frontend JSON.""" data = request.json with open(SITE_YAML, "w") as f: yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True) @@ -160,13 +188,14 @@ def update_site_info(): @app.route("/api/themes") def list_themes(): + """List available themes (folders in config/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(): + """Upload a thumbnail image and update site.yaml.""" PHOTOS_DIR = app.config["PHOTOS_DIR"] file = request.files.get("file") if not file: @@ -183,6 +212,7 @@ def upload_thumbnail(): @app.route("/api/thumbnail/remove", methods=["POST"]) def remove_thumbnail(): + """Remove the thumbnail image and update site.yaml.""" PHOTOS_DIR = app.config["PHOTOS_DIR"] thumbnail_path = PHOTOS_DIR / "thumbnail.png" # Remove thumbnail file if exists @@ -199,6 +229,7 @@ def remove_thumbnail(): @app.route("/api/theme/upload", methods=["POST"]) def upload_theme(): + """Upload a custom theme folder and save it in config/themes.""" themes_dir = Path(__file__).resolve().parents[3] / "config" / "themes" files = request.files.getlist("files") if not files: @@ -215,7 +246,118 @@ def upload_theme(): file.save(dest_path) 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//") +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 --- if __name__ == "__main__": logging.info("Starting WebUI at http://127.0.0.1:5000") - app.run(debug=True) + app.run(debug=True) \ No newline at end of file diff --git a/src/webui/js/site-info.js b/src/webui/js/site-info.js index bbf0d17..34c2838 100644 --- a/src/webui/js/site-info.js +++ b/src/webui/js/site-info.js @@ -29,8 +29,8 @@ document.addEventListener("DOMContentLoaded", () => { div.style.gap = "8px"; div.style.marginBottom = "6px"; div.innerHTML = ` - - + + `; menuList.appendChild(div); diff --git a/src/webui/js/theme-editor.js b/src/webui/js/theme-editor.js new file mode 100644 index 0000000..7a3271b --- /dev/null +++ b/src/webui/js/theme-editor.js @@ -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 => + `` + ).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 += ` +
+ + + + + +
+ `; + }); +} + +function renderLocalFonts(fonts) { + const listDiv = document.getElementById("local-fonts-list"); + if (!listDiv) return; + listDiv.innerHTML = ""; + fonts.forEach(font => { + listDiv.innerHTML += ` +
+ ${font} + +
+ `; + }); +} + +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"); + } + }); +}); \ No newline at end of file diff --git a/src/webui/site-info/index.html b/src/webui/site-info/index.html index 5199332..9fa3106 100644 --- a/src/webui/site-info/index.html +++ b/src/webui/site-info/index.html @@ -49,27 +49,27 @@
- +
- +
- +
- +
- +
- +
@@ -79,9 +79,9 @@
- + - +
@@ -106,11 +106,11 @@
- +
- +
@@ -143,7 +143,7 @@
- + diff --git a/src/webui/style/style.css b/src/webui/style/style.css index 2a4468d..b37bc9c 100644 --- a/src/webui/style/style.css +++ b/src/webui/style/style.css @@ -48,7 +48,7 @@ h2 { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); border: 1px solid #2f2e2e80; border-radius: 8px; - padding: 0px 20px; + padding: 0px 20px 20px 20px; } .upload-section label { @@ -491,7 +491,7 @@ h2 { padding: 0 40px 40px 40px; } -#site-info-form fieldset { +fieldset { background-color: rgb(67 67 67 / 26%); border-radius: 6px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); @@ -501,7 +501,7 @@ h2 { margin: 32px auto; } -#site-info-form legend { +legend { font-size: 1.2em; font-weight: 700; color: #26c4ff; @@ -509,13 +509,13 @@ h2 { letter-spacing: 1px; } -#site-info-form .fields { +.fields { display: flex; flex-wrap: wrap; gap: 18px; } -#site-info-form .input-field { +.input-field { flex: 1 1 calc(33.333% - 18px); min-width: 220px; max-width: 100%; @@ -525,7 +525,7 @@ h2 { margin-bottom: 10px; } -#site-info-form label { +label { font-size: 13px; font-weight: 600; color: #e3e3e3; @@ -533,9 +533,9 @@ h2 { letter-spacing: 0.5px; } -#site-info-form input, -#site-info-form textarea, -#site-info-form select { +#site-info-form input, #theme-editor-form input, +#site-info-form textarea, #theme-editor-form textarea, +#site-info-form select, #theme-editor-form select { /* background: rgba(4, 44, 60, 0.55);*/ background: #1f2223; color: #fff; @@ -551,23 +551,29 @@ h2 { } #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; font-style: italic; } #site-info-form input:focus, +#theme-editor-form input: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; background: #161616; } -#site-info-form textarea { +#site-info-form textarea, +#theme-editor-form textarea { min-height: 60px; resize: vertical; } -#site-info-form input[type="file"] { +#input[type="file"] { background: none; color: #fff; border: none; @@ -575,13 +581,13 @@ h2 { margin-top: 2px; } -#site-info-form img#thumbnail-preview { +img#thumbnail-preview { margin-top: 8px; border-radius: 8px; 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); color: #fff; font-weight: 700; @@ -595,11 +601,11 @@ h2 { 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); } -#site-info-form button[type="button"] { +#site-info-form button[type="button"], #theme-editor-form button[type="button"] { background: #00000000; color: #fff; border: none; @@ -612,47 +618,47 @@ h2 { 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; color: #fff; } @media (max-width: 900px) { - #site-info-form { + #site-info-form, #theme-editor-form { padding: 18px 8px; } - #site-info-form .fields, - #site-info-form fieldset { + .fields, + fieldset { flex-direction: column; gap: 0; } - #site-info-form .input-field { + .input-field { min-width: 100%; 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-bottom: 4px; border-radius: 30px; 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); } -#site-info-form button.remove-btn { +#site-info-form button.remove-btn, #theme-editor-form button.remove-btn { border-radius: 30px; 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); } -#site-info-form .thumbnail-form-label { +#site-info-form .thumbnail-form-label, #theme-editor-form .thumbnail-form-label { margin-top: 10px; } \ No newline at end of file diff --git a/src/webui/theme-editor/index.html b/src/webui/theme-editor/index.html new file mode 100644 index 0000000..2a0bec4 --- /dev/null +++ b/src/webui/theme-editor/index.html @@ -0,0 +1,178 @@ + + + + + + Theme Editor + + + + + + +
+
+

Edit Theme

+ +
+ Current theme: +
+
+ +
+

Colors

+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+ +
+

Google Fonts

+
+ +
+ +
+ +
+

Upload Custom Font (.woff, .woff2)

+
+ + +
+ +
+
+ +
+

Fonts

+
+
+ + + + +
+
+ + + + +
+
+
+ +
+

Favicon

+
+
+ + + + +
+ + +
+
+
+
+ +
+
+ + + + + + + \ No newline at end of file