diff --git a/config/themes/modern/theme.yaml b/config/themes/modern/theme.yaml index 3f331f5..26cfa8a 100644 --- a/config/themes/modern/theme.yaml +++ b/config/themes/modern/theme.yaml @@ -8,8 +8,8 @@ colors: secondary: '#00b0f0' accent: '#ffc700' text_dark: '#616161' - background: '#fff' - browser_color: '#fff' + background: '#ffffff' + browser_color: '#ffffff' favicon: path: favicon.png google_fonts: diff --git a/config/themes/typewriter/theme.yaml b/config/themes/typewriter/theme.yaml index 7a30379..546e89d 100644 --- a/config/themes/typewriter/theme.yaml +++ b/config/themes/typewriter/theme.yaml @@ -8,8 +8,8 @@ colors: secondary: '#00b0f0' accent: '#ffc700' text_dark: '#616161' - background: '#fff' - browser_color: '#fff' + background: '#ffffff' + browser_color: '#ffffff' favicon: path: favicon.png fonts: diff --git a/src/py/webui/webui.py b/src/py/webui/webui.py index 874286b..bdeec5b 100644 --- a/src/py/webui/webui.py +++ b/src/py/webui/webui.py @@ -259,6 +259,25 @@ def upload_theme(): file.save(dest_path) return jsonify({"status": "ok", "theme": folder_name}) +@app.route("/api/theme/remove", methods=["POST"]) +def remove_theme(): + """Remove a custom theme folder.""" + data = request.get_json() + theme_name = data.get("theme") + if not theme_name: + return jsonify({"error": "❌ Missing theme"}), 400 + themes_dir = Path(__file__).resolve().parents[3] / "config" / "themes" + theme_folder = themes_dir / theme_name + if not theme_folder.exists() or not theme_folder.is_dir(): + return jsonify({"error": "❌ Theme not found"}), 404 + # Prevent removing default themes + if theme_name in ["modern", "classic"]: + return jsonify({"error": "❌ Cannot remove default theme"}), 400 + # Remove folder and all contents + import shutil + shutil.rmtree(theme_folder) + return jsonify({"status": "ok"}) + # --- Theme editor page & API --- @app.route("/theme-editor") def theme_editor(): @@ -284,6 +303,20 @@ def api_theme_info(): save_theme_yaml(theme_name, theme_yaml) return jsonify({"status": "ok"}) +@app.route("/api/theme-google-fonts", methods=["POST"]) +def update_theme_google_fonts(): + """Update only google_fonts in theme.yaml for current theme.""" + data = request.get_json() + theme_name = data.get("theme_name") + google_fonts = data.get("google_fonts", []) + theme_yaml_path = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "theme.yaml" + with open(theme_yaml_path, "r") as f: + theme_yaml = yaml.safe_load(f) + theme_yaml["google_fonts"] = google_fonts + 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("/api/local-fonts") def api_local_fonts(): """List local fonts for a theme.""" diff --git a/src/webui/js/site-info.js b/src/webui/js/site-info.js index 7a79439..a65caaf 100644 --- a/src/webui/js/site-info.js +++ b/src/webui/js/site-info.js @@ -98,6 +98,14 @@ document.addEventListener("DOMContentLoaded", () => { const deleteModalConfirm = document.getElementById("delete-modal-confirm"); const deleteModalCancel = document.getElementById("delete-modal-cancel"); + // Modal elements for theme deletion + const deleteThemeModal = document.getElementById("delete-theme-modal"); + const deleteThemeModalClose = document.getElementById("delete-theme-modal-close"); + const deleteThemeModalConfirm = document.getElementById("delete-theme-modal-confirm"); + const deleteThemeModalCancel = document.getElementById("delete-theme-modal-cancel"); + const deleteThemeModalText = document.getElementById("delete-theme-modal-text"); + let themeToDelete = null; + // Show/hide thumbnail preview, remove button, and choose button function updateThumbnailPreview(src) { if (thumbnailPreview) { @@ -201,6 +209,64 @@ document.addEventListener("DOMContentLoaded", () => { }); } + // Remove theme button triggers modal + const removeThemeBtn = document.getElementById("remove-theme-btn"); + if (removeThemeBtn && themeSelect) { + removeThemeBtn.addEventListener("click", () => { + const theme = themeSelect.value; + if (!theme) return showToast("❌ No theme selected", "error"); + if (["modern", "classic"].includes(theme)) { + showToast("❌ Cannot remove default theme", "error"); + return; + } + themeToDelete = theme; + deleteThemeModalText.textContent = `Are you sure you want to remove theme "${theme}"?`; + deleteThemeModal.style.display = "flex"; + }); + } + + // Modal logic for theme deletion + if (deleteThemeModal && deleteThemeModalClose && deleteThemeModalConfirm && deleteThemeModalCancel) { + deleteThemeModalClose.onclick = deleteThemeModalCancel.onclick = () => { + deleteThemeModal.style.display = "none"; + themeToDelete = null; + }; + window.onclick = function(event) { + if (event.target === deleteThemeModal) { + deleteThemeModal.style.display = "none"; + themeToDelete = null; + } + }; + deleteThemeModalConfirm.onclick = async () => { + if (!themeToDelete) return; + const res = await fetch("/api/theme/remove", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ theme: themeToDelete }) + }); + const result = await res.json(); + if (result.status === "ok") { + showToast("✅ Theme removed!", "success"); + // Refresh theme select + fetch("/api/themes") + .then(res => res.json()) + .then(themes => { + themeSelect.innerHTML = ""; + themes.forEach(theme => { + const option = document.createElement("option"); + option.value = theme; + option.textContent = theme; + themeSelect.appendChild(option); + }); + }); + } else { + showToast(result.error || "❌ Error removing theme", "error"); + } + deleteThemeModal.style.display = "none"; + themeToDelete = null; + }; + } + // Fetch theme list and populate select if (themeSelect) { fetch("/api/themes") diff --git a/src/webui/js/theme-editor.js b/src/webui/js/theme-editor.js index 558c3ee..ea047bf 100644 --- a/src/webui/js/theme-editor.js +++ b/src/webui/js/theme-editor.js @@ -170,28 +170,28 @@ document.addEventListener("DOMContentLoaded", async () => { } 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)) { - showToast("Only .woff and .woff2 fonts are allowed.", "error"); - 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") { - showToast("✅ Font uploaded!", "success"); - localFonts = await fetchLocalFonts(themeInfo.theme_name); - refreshLocalFonts(); - } else { - showToast("Error uploading font.", "error"); - } - }); -} + 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)) { + showToast("Only .woff and .woff2 fonts are allowed.", "error"); + 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") { + showToast("✅ Font uploaded!", "success"); + localFonts = await fetchLocalFonts(themeInfo.theme_name); + refreshLocalFonts(); + } else { + showToast("Error uploading font.", "error"); + } + }); + } // Remove font button triggers modal if (localFontsList) { @@ -333,20 +333,75 @@ document.addEventListener("DOMContentLoaded", async () => { // Add Google Font const addGoogleFontBtn = document.getElementById("add-google-font"); if (addGoogleFontBtn) { - addGoogleFontBtn.addEventListener("click", () => { + addGoogleFontBtn.addEventListener("click", async () => { googleFonts.push({ family: "", weights: [] }); + // Save immediately to backend + await fetch("/api/theme-google-fonts", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts }) + }); + // Fetch updated theme info and refresh dropdowns + const updatedThemeInfo = await fetchThemeInfo(); + const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || []; + googleFonts.length = 0; + googleFonts.push(...updatedGoogleFonts); renderGoogleFonts(googleFonts); + refreshFontDropdowns(); }); } - // 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); + // Save on blur for family/weights fields + googleFontsFields.addEventListener("blur", async (e) => { + if ( + e.target.name && + (e.target.name.endsWith("[family]") || e.target.name.endsWith("[weights]")) + ) { + // Update googleFonts array from the form fields + const fontFields = googleFontsFields.querySelectorAll(".input-field"); + googleFonts.length = 0; + fontFields.forEach(field => { + const family = field.querySelector('input[name^="google_fonts"][name$="[family]"]').value.trim(); + const weights = field.querySelector('input[name^="google_fonts"][name$="[weights]"]').value + .split(",").map(w => w.trim()).filter(Boolean); + googleFonts.push({ family, weights }); + }); + // Save immediately to backend + await fetch("/api/theme-google-fonts", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts }) + }); + // Fetch updated theme info and refresh dropdowns + const updatedThemeInfo = await fetchThemeInfo(); + const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || []; + googleFonts.length = 0; + googleFonts.push(...updatedGoogleFonts); renderGoogleFonts(googleFonts); + refreshFontDropdowns(); + } + }, true); // Use capture phase to catch blur from children + + // Delegate remove button click for Google Fonts + googleFontsFields.addEventListener("click", async (e) => { + if (e.target.classList.contains("remove-google-font")) { + const idx = Number(e.target.dataset.idx); + googleFonts.splice(idx, 1); + // Save immediately to backend + await fetch("/api/theme-google-fonts", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts }) + }); + // Fetch updated theme info and refresh dropdowns + const updatedThemeInfo = await fetchThemeInfo(); + const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || []; + googleFonts.length = 0; + googleFonts.push(...updatedGoogleFonts); + renderGoogleFonts(googleFonts); + refreshFontDropdowns(); } }); } diff --git a/src/webui/site-info/index.html b/src/webui/site-info/index.html index f632e07..36870f8 100644 --- a/src/webui/site-info/index.html +++ b/src/webui/site-info/index.html @@ -43,6 +43,7 @@