2.0 - WebUI builder ("Cielight" merge) #9
@ -8,8 +8,8 @@ colors:
|
|||||||
secondary: '#00b0f0'
|
secondary: '#00b0f0'
|
||||||
accent: '#ffc700'
|
accent: '#ffc700'
|
||||||
text_dark: '#616161'
|
text_dark: '#616161'
|
||||||
background: '#fff'
|
background: '#ffffff'
|
||||||
browser_color: '#fff'
|
browser_color: '#ffffff'
|
||||||
favicon:
|
favicon:
|
||||||
path: favicon.png
|
path: favicon.png
|
||||||
google_fonts:
|
google_fonts:
|
||||||
|
@ -8,8 +8,8 @@ colors:
|
|||||||
secondary: '#00b0f0'
|
secondary: '#00b0f0'
|
||||||
accent: '#ffc700'
|
accent: '#ffc700'
|
||||||
text_dark: '#616161'
|
text_dark: '#616161'
|
||||||
background: '#fff'
|
background: '#ffffff'
|
||||||
browser_color: '#fff'
|
browser_color: '#ffffff'
|
||||||
favicon:
|
favicon:
|
||||||
path: favicon.png
|
path: favicon.png
|
||||||
fonts:
|
fonts:
|
||||||
|
@ -259,6 +259,25 @@ 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})
|
||||||
|
|
||||||
|
@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 ---
|
# --- Theme editor page & API ---
|
||||||
@app.route("/theme-editor")
|
@app.route("/theme-editor")
|
||||||
def theme_editor():
|
def theme_editor():
|
||||||
@ -284,6 +303,20 @@ def api_theme_info():
|
|||||||
save_theme_yaml(theme_name, theme_yaml)
|
save_theme_yaml(theme_name, theme_yaml)
|
||||||
return jsonify({"status": "ok"})
|
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")
|
@app.route("/api/local-fonts")
|
||||||
def api_local_fonts():
|
def api_local_fonts():
|
||||||
"""List local fonts for a theme."""
|
"""List local fonts for a theme."""
|
||||||
|
@ -98,6 +98,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const deleteModalConfirm = document.getElementById("delete-modal-confirm");
|
const deleteModalConfirm = document.getElementById("delete-modal-confirm");
|
||||||
const deleteModalCancel = document.getElementById("delete-modal-cancel");
|
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
|
// Show/hide thumbnail preview, remove button, and choose button
|
||||||
function updateThumbnailPreview(src) {
|
function updateThumbnailPreview(src) {
|
||||||
if (thumbnailPreview) {
|
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
|
// Fetch theme list and populate select
|
||||||
if (themeSelect) {
|
if (themeSelect) {
|
||||||
fetch("/api/themes")
|
fetch("/api/themes")
|
||||||
|
@ -170,28 +170,28 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fontUploadInput) {
|
if (fontUploadInput) {
|
||||||
fontUploadInput.addEventListener("change", async (e) => {
|
fontUploadInput.addEventListener("change", async (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const ext = file.name.split('.').pop().toLowerCase();
|
const ext = file.name.split('.').pop().toLowerCase();
|
||||||
if (!["woff", "woff2"].includes(ext)) {
|
if (!["woff", "woff2"].includes(ext)) {
|
||||||
showToast("Only .woff and .woff2 fonts are allowed.", "error");
|
showToast("Only .woff and .woff2 fonts are allowed.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
formData.append("theme", themeInfo.theme_name);
|
formData.append("theme", themeInfo.theme_name);
|
||||||
const res = await fetch("/api/font/upload", { method: "POST", body: formData });
|
const res = await fetch("/api/font/upload", { method: "POST", body: formData });
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
if (result.status === "ok") {
|
if (result.status === "ok") {
|
||||||
showToast("✅ Font uploaded!", "success");
|
showToast("✅ Font uploaded!", "success");
|
||||||
localFonts = await fetchLocalFonts(themeInfo.theme_name);
|
localFonts = await fetchLocalFonts(themeInfo.theme_name);
|
||||||
refreshLocalFonts();
|
refreshLocalFonts();
|
||||||
} else {
|
} else {
|
||||||
showToast("Error uploading font.", "error");
|
showToast("Error uploading font.", "error");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove font button triggers modal
|
// Remove font button triggers modal
|
||||||
if (localFontsList) {
|
if (localFontsList) {
|
||||||
@ -333,20 +333,75 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
// Add Google Font
|
// Add Google Font
|
||||||
const addGoogleFontBtn = document.getElementById("add-google-font");
|
const addGoogleFontBtn = document.getElementById("add-google-font");
|
||||||
if (addGoogleFontBtn) {
|
if (addGoogleFontBtn) {
|
||||||
addGoogleFontBtn.addEventListener("click", () => {
|
addGoogleFontBtn.addEventListener("click", async () => {
|
||||||
googleFonts.push({ family: "", weights: [] });
|
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);
|
renderGoogleFonts(googleFonts);
|
||||||
|
refreshFontDropdowns();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove Google Font
|
|
||||||
const googleFontsFields = document.getElementById("google-fonts-fields");
|
const googleFontsFields = document.getElementById("google-fonts-fields");
|
||||||
if (googleFontsFields) {
|
if (googleFontsFields) {
|
||||||
googleFontsFields.addEventListener("click", (e) => {
|
// Save on blur for family/weights fields
|
||||||
if (e.target.classList.contains("remove-google-font")) {
|
googleFontsFields.addEventListener("blur", async (e) => {
|
||||||
const idx = parseInt(e.target.dataset.idx, 10);
|
if (
|
||||||
googleFonts.splice(idx, 1);
|
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);
|
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 -->
|
<!-- Info Section -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<h2>Info</h2>
|
<h2>Info</h2>
|
||||||
|
<p>Set the basic information for your site and SEO</p>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Title</label>
|
<label>Title</label>
|
||||||
@ -73,6 +74,7 @@
|
|||||||
<!-- Social Section -->
|
<!-- Social Section -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<h2>Social</h2>
|
<h2>Social</h2>
|
||||||
|
<p>Set your social media links and thumbnail for link sharing</p>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Instagram URL</label>
|
<label>Instagram URL</label>
|
||||||
@ -91,6 +93,7 @@
|
|||||||
<!-- Menu Section -->
|
<!-- Menu Section -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<h2>Menu</h2>
|
<h2>Menu</h2>
|
||||||
|
<p>Manage your site menu items. You can use tag combination to propose custom filters</p>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="input-field" style="flex: 1 1 100%;">
|
<div class="input-field" style="flex: 1 1 100%;">
|
||||||
<div id="menu-items-list"></div>
|
<div id="menu-items-list"></div>
|
||||||
@ -101,6 +104,7 @@
|
|||||||
<!-- Footer Section -->
|
<!-- Footer Section -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<h2>Footer</h2>
|
<h2>Footer</h2>
|
||||||
|
<p>Set your copyright informations and legal link name</p>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Copyright</label>
|
<label>Copyright</label>
|
||||||
@ -115,6 +119,7 @@
|
|||||||
<!-- Legals Section -->
|
<!-- Legals Section -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<h2>Legals</h2>
|
<h2>Legals</h2>
|
||||||
|
<p>Set your legal informations</p>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Hoster Name</label>
|
<label>Hoster Name</label>
|
||||||
@ -138,13 +143,16 @@
|
|||||||
<!-- Build Section -->
|
<!-- Build Section -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<h2>Build</h2>
|
<h2>Build</h2>
|
||||||
|
<p>Select a theme from the dropdown menu or add your custom theme folder</p>
|
||||||
<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" required></select>
|
<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;">
|
<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>
|
||||||
|
<p>If checked, images will be converted for web and resized to fit the theme</p>
|
||||||
<label>
|
<label>
|
||||||
<input class="thumbnail-form-label" type="checkbox" name="build.convert_images" id="convert-images-checkbox">
|
<input class="thumbnail-form-label" type="checkbox" name="build.convert_images" id="convert-images-checkbox">
|
||||||
Convert images
|
Convert images
|
||||||
@ -171,6 +179,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 -->
|
<!-- Build success modal -->
|
||||||
<div id="build-success-modal" class="modal" style="display:none;">
|
<div id="build-success-modal" class="modal" style="display:none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
Reference in New Issue
Block a user