2.0 - WebUI builder ("Cielight" merge) #9
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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."""
|
||||
|
@ -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")
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -43,6 +43,7 @@
|
||||
<!-- Info Section -->
|
||||
<fieldset>
|
||||
<h2>Info</h2>
|
||||
<p>Set the basic information for your site and SEO</p>
|
||||
<div class="fields">
|
||||
<div class="input-field">
|
||||
<label>Title</label>
|
||||
@ -73,6 +74,7 @@
|
||||
<!-- Social Section -->
|
||||
<fieldset>
|
||||
<h2>Social</h2>
|
||||
<p>Set your social media links and thumbnail for link sharing</p>
|
||||
<div class="fields">
|
||||
<div class="input-field">
|
||||
<label>Instagram URL</label>
|
||||
@ -91,6 +93,7 @@
|
||||
<!-- Menu Section -->
|
||||
<fieldset>
|
||||
<h2>Menu</h2>
|
||||
<p>Manage your site menu items. You can use tag combination to propose custom filters</p>
|
||||
<div class="fields">
|
||||
<div class="input-field" style="flex: 1 1 100%;">
|
||||
<div id="menu-items-list"></div>
|
||||
@ -101,6 +104,7 @@
|
||||
<!-- Footer Section -->
|
||||
<fieldset>
|
||||
<h2>Footer</h2>
|
||||
<p>Set your copyright informations and legal link name</p>
|
||||
<div class="fields">
|
||||
<div class="input-field">
|
||||
<label>Copyright</label>
|
||||
@ -115,6 +119,7 @@
|
||||
<!-- Legals Section -->
|
||||
<fieldset>
|
||||
<h2>Legals</h2>
|
||||
<p>Set your legal informations</p>
|
||||
<div class="fields">
|
||||
<div class="input-field">
|
||||
<label>Hoster Name</label>
|
||||
@ -138,13 +143,16 @@
|
||||
<!-- Build Section -->
|
||||
<fieldset>
|
||||
<h2>Build</h2>
|
||||
<p>Select a theme from the dropdown menu or add your custom theme folder</p>
|
||||
<div class="fields">
|
||||
<div class="input-field">
|
||||
<label>Theme</label>
|
||||
<select name="build.theme" id="theme-select" required></select>
|
||||
<button type="button" id="remove-theme-btn" class="remove-btn up-btn danger">🗑 Remove selected theme</button>
|
||||
<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>
|
||||
<label class="thumbnail-form-label">Images processing</label>
|
||||
<p>If checked, images will be converted for web and resized to fit the theme</p>
|
||||
<label>
|
||||
<input class="thumbnail-form-label" type="checkbox" name="build.convert_images" id="convert-images-checkbox">
|
||||
Convert images
|
||||
@ -171,6 +179,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Delete theme confirmation modal -->
|
||||
<div id="delete-theme-modal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<span id="delete-theme-modal-close" class="modal-close">×</span>
|
||||
<h3>Confirm Theme Deletion</h3>
|
||||
<p id="delete-theme-modal-text">Are you sure you want to remove this theme?</p>
|
||||
<div class="modal-actions">
|
||||
<button id="delete-theme-modal-confirm" class="modal-btn danger">Remove</button>
|
||||
<button id="delete-theme-modal-cancel" class="modal-btn">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Build success modal -->
|
||||
<div id="build-success-modal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
|
Reference in New Issue
Block a user