@@ -243,12 +250,8 @@ function renderHero() {
`;
container.appendChild(div);
});
-
- // Show/hide Remove All button
- const removeAllBtn = document.getElementById('remove-all-hero');
- if (removeAllBtn) {
- removeAllBtn.style.display = heroImages.length > 0 ? 'inline-block' : 'none';
- }
+ updateCountAndButtons('hero', heroImages.length);
+ applyFadeInImages(container);
}
// --- Save gallery to server ---
@@ -294,33 +297,27 @@ async function refreshHero() {
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");
toast.addEventListener("transitionend", () => toast.remove());
}, duration);
}
-let pendingDelete = null; // { type: 'gallery'|'hero'|'gallery-all'|'hero-all', index: number|null }
+let pendingDelete = null;
// --- Show delete confirmation modal ---
function showDeleteModal(type, index = null) {
pendingDelete = { type, index };
const modalText = document.getElementById('delete-modal-text');
- if (type === 'gallery-all') {
- modalText.textContent = "Are you sure you want to delete ALL gallery images?";
- } else if (type === 'hero-all') {
- modalText.textContent = "Are you sure you want to delete ALL hero images?";
- } else {
- modalText.textContent = "Are you sure you want to delete this image?";
- }
+ modalText.textContent =
+ type === 'gallery-all' ? "Are you sure you want to delete ALL gallery images?"
+ : type === 'hero-all' ? "Are you sure you want to delete ALL hero images?"
+ : "Are you sure you want to delete this image?";
document.getElementById('delete-modal').style.display = 'flex';
}
@@ -333,15 +330,10 @@ function hideDeleteModal() {
// --- Confirm deletion ---
async function confirmDelete() {
if (!pendingDelete) return;
- if (pendingDelete.type === 'gallery') {
- await actuallyDeleteGalleryImage(pendingDelete.index);
- } else if (pendingDelete.type === 'hero') {
- await actuallyDeleteHeroImage(pendingDelete.index);
- } else if (pendingDelete.type === 'gallery-all') {
- await actuallyDeleteAllGalleryImages();
- } else if (pendingDelete.type === 'hero-all') {
- await actuallyDeleteAllHeroImages();
- }
+ if (pendingDelete.type === 'gallery') await actuallyDeleteGalleryImage(pendingDelete.index);
+ else if (pendingDelete.type === 'hero') await actuallyDeleteHeroImage(pendingDelete.index);
+ else if (pendingDelete.type === 'gallery-all') await actuallyDeleteAllGalleryImages();
+ else if (pendingDelete.type === 'hero-all') await actuallyDeleteAllHeroImages();
hideDeleteModal();
}
@@ -423,15 +415,35 @@ async function actuallyDeleteAllHeroImages() {
// --- Modal event listeners and bulk delete buttons ---
document.addEventListener('DOMContentLoaded', () => {
- document.getElementById('delete-modal-close').onclick = hideDeleteModal;
- document.getElementById('delete-modal-cancel').onclick = hideDeleteModal;
- document.getElementById('delete-modal-confirm').onclick = confirmDelete;
+ ['delete-modal-close', 'delete-modal-cancel'].forEach(id => {
+ const el = document.getElementById(id);
+ if (el) el.onclick = hideDeleteModal;
+ });
+ const confirmBtn = document.getElementById('delete-modal-confirm');
+ if (confirmBtn) confirmBtn.onclick = confirmDelete;
+
+ // Gallery filter radios
+ const showAllRadio = document.getElementById('show-all-radio');
+ const showUntaggedRadio = document.getElementById('show-untagged-radio');
+ if (showAllRadio) showAllRadio.addEventListener('change', () => {
+ showOnlyUntagged = false;
+ renderGallery();
+ });
+ if (showUntaggedRadio) showUntaggedRadio.addEventListener('change', () => {
+ showOnlyUntagged = true;
+ renderGallery();
+ });
// Bulk delete buttons
- const removeAllGalleryBtn = document.getElementById('remove-all-gallery');
- const removeAllHeroBtn = document.getElementById('remove-all-hero');
- if (removeAllGalleryBtn) removeAllGalleryBtn.onclick = () => showDeleteModal('gallery-all');
- if (removeAllHeroBtn) removeAllHeroBtn.onclick = () => showDeleteModal('hero-all');
+ [
+ ['remove-all-gallery', 'gallery-all'],
+ ['remove-all-gallery-bottom', 'gallery-all'],
+ ['remove-all-hero', 'hero-all'],
+ ['remove-all-hero-bottom', 'hero-all']
+ ].forEach(([btnId, type]) => {
+ const btn = document.getElementById(btnId);
+ if (btn) btn.onclick = () => showDeleteModal(type);
+ });
});
// --- Initialize ---
diff --git a/src/webui/js/site-info.js b/src/webui/js/site-info.js
index 3af7338..ec58b1e 100644
--- a/src/webui/js/site-info.js
+++ b/src/webui/js/site-info.js
@@ -12,7 +12,6 @@ function showToast(message, type = "success", duration = 3000) {
}, duration);
}
-// --- Loader helpers ---
function showLoader(text = "Uploading...") {
const loader = document.getElementById("global-loader");
if (loader) {
@@ -26,119 +25,99 @@ function hideLoader() {
}
document.addEventListener("DOMContentLoaded", () => {
- // Form and menu logic
- const form = document.getElementById("site-info-form");
+ // --- Section Forms ---
+ const forms = {
+ info: document.getElementById("info-form"),
+ social: document.getElementById("social-form"),
+ menu: document.getElementById("menu-form"),
+ footer: document.getElementById("footer-form"),
+ legals: document.getElementById("legals-form"),
+ build: document.getElementById("build-form")
+ };
+
+ // --- Menu logic ---
const menuList = document.getElementById("menu-items-list");
const addMenuBtn = document.getElementById("add-menu-item");
-
let menuItems = [];
-
- // Render menu items
function renderMenuItems() {
menuList.innerHTML = "";
menuItems.forEach((item, idx) => {
- const div = document.createElement("div");
- div.style.display = "flex";
- div.style.gap = "8px";
- div.style.marginBottom = "6px";
- div.innerHTML = `
-
-
-
+ menuList.innerHTML += `
+
+
+
+
+
`;
- menuList.appendChild(div);
});
}
-
- // Update menu items from inputs
function updateMenuItemsFromInputs() {
const inputs = menuList.querySelectorAll("input");
- const items = [];
+ menuItems = [];
for (let i = 0; i < inputs.length; i += 2) {
const label = inputs[i].value.trim();
const href = inputs[i + 1].value.trim();
- if (label || href) items.push({ label, href });
+ if (label || href) menuItems.push({ label, href });
}
- menuItems = items;
}
- // Intellectual property paragraphs logic
+ // --- Intellectual property paragraphs logic ---
const ipList = document.getElementById("ip-list");
const addIpBtn = document.getElementById("add-ip-paragraph");
let ipParagraphs = [];
-
- // Render IP paragraphs
function renderIpParagraphs() {
ipList.innerHTML = "";
ipParagraphs.forEach((item, idx) => {
- const div = document.createElement("div");
- div.style.display = "flex";
- div.style.gap = "8px";
- div.style.marginBottom = "6px";
- div.innerHTML = `
-
-
+ ipList.innerHTML += `
+
+
+
+
`;
- ipList.appendChild(div);
});
}
-
- // Update IP paragraphs from textareas
function updateIpParagraphsFromInputs() {
- const textareas = ipList.querySelectorAll("textarea");
- ipParagraphs = Array.from(textareas).map(textarea => ({
- paragraph: textarea.value.trim()
- })).filter(item => item.paragraph !== "");
+ ipParagraphs = Array.from(ipList.querySelectorAll("textarea"))
+ .map(textarea => ({ paragraph: textarea.value.trim() }))
+ .filter(item => item.paragraph !== "");
}
- // Build options
+ // --- Build options & Theme select ---
const convertImagesCheckbox = document.getElementById("convert-images-checkbox");
const resizeImagesCheckbox = document.getElementById("resize-images-checkbox");
-
- // Theme select
const themeSelect = document.getElementById("theme-select");
- // Thumbnail upload and modal logic
- const thumbnailInput = form?.elements["social.thumbnail"];
+ // --- Thumbnail upload and modal logic ---
+ const thumbnailInput = document.getElementById("social-thumbnail");
const thumbnailUpload = document.getElementById("thumbnail-upload");
const chooseThumbnailBtn = document.getElementById("choose-thumbnail-btn");
const thumbnailPreview = document.getElementById("thumbnail-preview");
const removeThumbnailBtn = document.getElementById("remove-thumbnail-btn");
- // Modal elements for delete confirmation
- const deleteModal = document.getElementById("delete-modal");
- const deleteModalClose = document.getElementById("delete-modal-close");
- const deleteModalConfirm = document.getElementById("delete-modal-confirm");
- const deleteModalCancel = document.getElementById("delete-modal-cancel");
+ // --- Modal helpers ---
+ function setupModal(modal, closeBtn, confirmBtn, cancelBtn, onConfirm) {
+ if (!modal) return;
+ if (closeBtn) closeBtn.onclick = () => modal.style.display = "none";
+ if (cancelBtn) cancelBtn.onclick = () => modal.style.display = "none";
+ window.addEventListener("click", (e) => {
+ if (e.target === modal) modal.style.display = "none";
+ });
+ if (confirmBtn && onConfirm) confirmBtn.onclick = onConfirm;
+ }
- // 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
+ // --- Thumbnail preview logic ---
function updateThumbnailPreview(src) {
if (thumbnailPreview) {
thumbnailPreview.src = src || "";
thumbnailPreview.style.display = src ? "block" : "none";
}
- if (removeThumbnailBtn) {
- removeThumbnailBtn.style.display = src ? "inline-block" : "none";
- }
- if (chooseThumbnailBtn) {
- chooseThumbnailBtn.style.display = src ? "none" : "inline-block";
- }
+ if (removeThumbnailBtn) removeThumbnailBtn.style.display = src ? "inline-block" : "none";
+ if (chooseThumbnailBtn) chooseThumbnailBtn.style.display = src ? "none" : "inline-block";
}
- // Choose thumbnail button triggers file input
if (chooseThumbnailBtn && thumbnailUpload) {
chooseThumbnailBtn.addEventListener("click", () => thumbnailUpload.click());
}
-
- // Handle thumbnail upload and refresh preview (with cache busting)
if (thumbnailUpload) {
thumbnailUpload.addEventListener("change", async (e) => {
const file = e.target.files[0];
@@ -156,27 +135,20 @@ document.addEventListener("DOMContentLoaded", () => {
} else {
showToast("❌ Error uploading thumbnail", "error");
}
+ updateSectionStatus("social");
});
}
-
- // Remove thumbnail button triggers modal
if (removeThumbnailBtn) {
removeThumbnailBtn.addEventListener("click", () => {
- deleteModal.style.display = "flex";
+ document.getElementById("delete-modal").style.display = "flex";
});
}
-
- // Modal logic for thumbnail deletion
- if (deleteModal && deleteModalClose && deleteModalConfirm && deleteModalCancel) {
- deleteModalClose.onclick = deleteModalCancel.onclick = () => {
- deleteModal.style.display = "none";
- };
- window.onclick = function(event) {
- if (event.target === deleteModal) {
- deleteModal.style.display = "none";
- }
- };
- deleteModalConfirm.onclick = async () => {
+ setupModal(
+ document.getElementById("delete-modal"),
+ document.getElementById("delete-modal-close"),
+ document.getElementById("delete-modal-confirm"),
+ document.getElementById("delete-modal-cancel"),
+ async () => {
const res = await fetch("/api/thumbnail/remove", { method: "POST" });
const result = await res.json();
if (result.status === "ok") {
@@ -186,48 +158,38 @@ document.addEventListener("DOMContentLoaded", () => {
} else {
showToast("❌ Error removing thumbnail", "error");
}
- deleteModal.style.display = "none";
- };
- }
+ document.getElementById("delete-modal").style.display = "none";
+ updateSectionStatus("social");
+ }
+ );
- // Theme upload logic (custom theme folder)
+ // --- Theme upload logic ---
const themeUpload = document.getElementById("theme-upload");
const chooseThemeBtn = document.getElementById("choose-theme-btn");
if (chooseThemeBtn && themeUpload) {
chooseThemeBtn.addEventListener("click", () => themeUpload.click());
themeUpload.addEventListener("change", async (e) => {
const files = Array.from(e.target.files);
- if (files.length === 0) return;
+ if (!files.length) return;
showLoader("Uploading theme...");
const formData = new FormData();
- files.forEach(file => {
- formData.append("files", file, file.webkitRelativePath || file.name);
- });
+ files.forEach(file => formData.append("files", file, file.webkitRelativePath || file.name));
const res = await fetch("/api/theme/upload", { method: "POST", body: formData });
const result = await res.json();
hideLoader();
if (result.status === "ok") {
showToast("✅ Theme uploaded!", "success");
- // Refresh theme select after upload
- 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);
- });
- });
+ refreshThemes();
} else {
showToast("❌ Error uploading theme", "error");
}
+ updateSectionStatus("build");
});
}
- // Remove theme button triggers modal
+ // --- Remove theme logic ---
const removeThemeBtn = document.getElementById("remove-theme-btn");
+ let themeToDelete = null;
if (removeThemeBtn && themeSelect) {
removeThemeBtn.addEventListener("click", () => {
const theme = themeSelect.value;
@@ -237,24 +199,16 @@ document.addEventListener("DOMContentLoaded", () => {
return;
}
themeToDelete = theme;
- deleteThemeModalText.textContent = `Are you sure you want to remove theme "${theme}"?`;
- deleteThemeModal.style.display = "flex";
+ document.getElementById("delete-theme-modal-text").textContent = `Are you sure you want to remove theme "${theme}"?`;
+ document.getElementById("delete-theme-modal").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 () => {
+ setupModal(
+ document.getElementById("delete-theme-modal"),
+ document.getElementById("delete-theme-modal-close"),
+ document.getElementById("delete-theme-modal-confirm"),
+ document.getElementById("delete-theme-modal-cancel"),
+ async () => {
if (!themeToDelete) return;
showLoader("Removing theme...");
const res = await fetch("/api/theme/remove", {
@@ -266,28 +220,18 @@ document.addEventListener("DOMContentLoaded", () => {
hideLoader();
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);
- });
- });
+ refreshThemes();
} else {
showToast(result.error || "❌ Error removing theme", "error");
}
- deleteThemeModal.style.display = "none";
+ document.getElementById("delete-theme-modal").style.display = "none";
themeToDelete = null;
- };
- }
+ updateSectionStatus("build");
+ }
+ );
- // Fetch theme list and populate select
- if (themeSelect) {
+ // --- Theme select refresh ---
+ function refreshThemes() {
fetch("/api/themes")
.then(res => res.json())
.then(themes => {
@@ -298,148 +242,276 @@ document.addEventListener("DOMContentLoaded", () => {
option.textContent = theme;
themeSelect.appendChild(option);
});
- // Set selected value after loading config
- fetch("/api/site-info")
- .then(res => res.json())
- .then(data => {
- themeSelect.value = data.build?.theme || "";
- });
+ loadConfigAndUpdateBuildStatus();
});
}
- // Load config from server and populate form
- if (form) {
+ // --- Config loading ---
+ let loadedConfig = {};
+ function loadConfigAndUpdateBuildStatus() {
fetch("/api/site-info")
.then(res => res.json())
.then(data => {
+ loadedConfig = data;
+ // Info
+ if (forms.info) {
+ forms.info.elements["info.title"].value = data.info?.title || "";
+ forms.info.elements["info.subtitle"].value = data.info?.subtitle || "";
+ forms.info.elements["info.description"].value = data.info?.description || "";
+ forms.info.elements["info.canonical"].value = data.info?.canonical || "";
+ forms.info.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || "");
+ forms.info.elements["info.author"].value = data.info?.author || "";
+ }
+ // Social
+ if (forms.social) {
+ forms.social.elements["social.instagram_url"].value = data.social?.instagram_url || "";
+ if (thumbnailInput) thumbnailInput.value = data.social?.thumbnail || "";
+ updateThumbnailPreview(data.social?.thumbnail ? `/photos/${data.social.thumbnail}?t=${Date.now()}` : "");
+ }
+ // Menu
+ menuItems = Array.isArray(data.menu?.items) ? data.menu.items : [];
+ renderMenuItems();
+ // Footer
+ if (forms.footer) {
+ forms.footer.elements["footer.copyright"].value = data.footer?.copyright || "";
+ forms.footer.elements["footer.legal_label"].value = data.footer?.legal_label || "";
+ }
+ // Legals
ipParagraphs = Array.isArray(data.legals?.intellectual_property)
? data.legals.intellectual_property
: [];
renderIpParagraphs();
- menuItems = Array.isArray(data.menu?.items) ? data.menu.items : [];
- renderMenuItems();
- form.elements["info.title"].value = data.info?.title || "";
- form.elements["info.subtitle"].value = data.info?.subtitle || "";
- form.elements["info.description"].value = data.info?.description || "";
- form.elements["info.canonical"].value = data.info?.canonical || "";
- form.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || "");
- form.elements["info.author"].value = data.info?.author || "";
- form.elements["social.instagram_url"].value = data.social?.instagram_url || "";
- if (thumbnailInput) thumbnailInput.value = data.social?.thumbnail || "";
- updateThumbnailPreview(data.social?.thumbnail ? `/photos/${data.social.thumbnail}?t=${Date.now()}` : "");
- form.elements["footer.copyright"].value = data.footer?.copyright || "";
- form.elements["footer.legal_label"].value = data.footer?.legal_label || "";
- if (themeSelect) {
- themeSelect.value = data.build?.theme || "";
- }
- form.elements["legals.hoster_name"].value = data.legals?.hoster_name || "";
- form.elements["legals.hoster_address"].value = data.legals?.hoster_address || "";
- form.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || "";
- // Build checkboxes
- if (convertImagesCheckbox) {
- convertImagesCheckbox.checked = !!data.build?.convert_images;
- }
- if (resizeImagesCheckbox) {
- resizeImagesCheckbox.checked = !!data.build?.resize_images;
+ if (forms.legals) {
+ forms.legals.elements["legals.hoster_name"].value = data.legals?.hoster_name || "";
+ forms.legals.elements["legals.hoster_address"].value = data.legals?.hoster_address || "";
+ forms.legals.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || "";
}
+ // Build
+ if (themeSelect) themeSelect.value = data.build?.theme || "";
+ if (convertImagesCheckbox) convertImagesCheckbox.checked = !!data.build?.convert_images;
+ if (resizeImagesCheckbox) resizeImagesCheckbox.checked = !!data.build?.resize_images;
+ ["info", "social", "menu", "footer", "legals"].forEach(updateSectionStatus);
+ updateSectionStatus("build");
});
}
+ if (themeSelect) refreshThemes();
+ else loadConfigAndUpdateBuildStatus();
- // Add menu item
- if (addMenuBtn) {
- addMenuBtn.addEventListener("click", () => {
- menuItems.push({ label: "", href: "" });
- renderMenuItems();
- });
- }
-
- // Remove menu item
+ // --- Add/remove menu items ---
+ if (addMenuBtn) addMenuBtn.addEventListener("click", () => {
+ menuItems.push({ label: "", href: "" });
+ renderMenuItems();
+ updateSectionStatus("menu");
+ });
menuList.addEventListener("click", (e) => {
if (e.target.classList.contains("remove-menu-item")) {
const idx = parseInt(e.target.getAttribute("data-idx"));
menuItems.splice(idx, 1);
renderMenuItems();
+ updateSectionStatus("menu");
}
});
-
- // Update menuItems on input change
menuList.addEventListener("input", () => {
updateMenuItemsFromInputs();
+ updateSectionStatus("menu");
});
- // Add paragraph
- if (addIpBtn) {
- addIpBtn.addEventListener("click", () => {
- ipParagraphs.push({ paragraph: "" });
- renderIpParagraphs();
- });
- }
-
- // Remove paragraph
+ // --- Add/remove IP paragraphs ---
+ if (addIpBtn) addIpBtn.addEventListener("click", () => {
+ ipParagraphs.push({ paragraph: "" });
+ renderIpParagraphs();
+ updateSectionStatus("legals");
+ });
ipList.addEventListener("click", (e) => {
if (e.target.classList.contains("remove-ip-paragraph")) {
const idx = parseInt(e.target.getAttribute("data-idx"));
ipParagraphs.splice(idx, 1);
renderIpParagraphs();
+ updateSectionStatus("legals");
}
});
-
- // Update ipParagraphs on input change
ipList.addEventListener("input", () => {
updateIpParagraphsFromInputs();
+ updateSectionStatus("legals");
});
- // Save config to server
- if (form) {
+ // --- Section value helpers ---
+ function getSectionValues(section) {
+ switch (section) {
+ case "info":
+ return {
+ title: forms.info.elements["info.title"].value,
+ subtitle: forms.info.elements["info.subtitle"].value,
+ description: forms.info.elements["info.description"].value,
+ canonical: forms.info.elements["info.canonical"].value,
+ keywords: forms.info.elements["info.keywords"].value.split(",").map(i => i.trim()).filter(Boolean),
+ author: forms.info.elements["info.author"].value
+ };
+ case "social":
+ return {
+ instagram_url: forms.social.elements["social.instagram_url"].value,
+ thumbnail: thumbnailInput ? thumbnailInput.value : ""
+ };
+ case "menu":
+ updateMenuItemsFromInputs();
+ return { items: menuItems };
+ case "footer":
+ return {
+ copyright: forms.footer.elements["footer.copyright"].value,
+ legal_label: forms.footer.elements["footer.legal_label"].value
+ };
+ case "legals":
+ updateIpParagraphsFromInputs();
+ return {
+ hoster_name: forms.legals.elements["legals.hoster_name"].value,
+ hoster_address: forms.legals.elements["legals.hoster_address"].value,
+ hoster_contact: forms.legals.elements["legals.hoster_contact"].value,
+ intellectual_property: ipParagraphs
+ };
+ case "build":
+ return {
+ theme: themeSelect ? themeSelect.value : "",
+ convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked),
+ resize_images: !!(resizeImagesCheckbox && resizeImagesCheckbox.checked)
+ };
+ default:
+ return {};
+ }
+ }
+
+ function isSectionSaved(section) {
+ const values = getSectionValues(section);
+ const config = loadedConfig[section] || {};
+ function normalizeMenuItems(items) {
+ return (items || []).map(item => ({
+ label: item.label || "",
+ href: item.href || ""
+ }));
+ }
+ switch (section) {
+ case "info":
+ return Object.keys(values).every(
+ key => values[key] && (
+ key === "keywords"
+ ? Array.isArray(config.keywords) && values.keywords.join(",") === config.keywords.join(",")
+ : values[key] === (config[key] || "")
+ )
+ );
+ case "social":
+ return values.instagram_url && values.thumbnail &&
+ values.instagram_url === (config.instagram_url || "") &&
+ values.thumbnail === (config.thumbnail || "");
+ case "menu":
+ return JSON.stringify(normalizeMenuItems(values.items)) === JSON.stringify(normalizeMenuItems(config.items));
+ case "footer":
+ return values.copyright && values.legal_label &&
+ values.copyright === (config.copyright || "") &&
+ values.legal_label === (config.legal_label || "");
+ case "legals":
+ return values.hoster_name && values.hoster_address && values.hoster_contact &&
+ values.hoster_name === (config.hoster_name || "") &&
+ values.hoster_address === (config.hoster_address || "") &&
+ values.hoster_contact === (config.hoster_contact || "") &&
+ JSON.stringify(values.intellectual_property) === JSON.stringify(config.intellectual_property || []);
+ case "build":
+ return values.theme === (config.theme || "") &&
+ !!values.convert_images === !!config.convert_images &&
+ !!values.resize_images === !!config.resize_images;
+ default:
+ return true;
+ }
+ }
+
+ function isSectionComplete(section) {
+ const values = getSectionValues(section);
+ switch (section) {
+ case "info":
+ return (
+ values.title &&
+ values.subtitle &&
+ values.description &&
+ values.canonical &&
+ values.keywords.length > 0 &&
+ values.author
+ );
+ case "social":
+ return values.instagram_url && values.thumbnail;
+ case "menu":
+ return Array.isArray(values.items) && values.items.every(item => item.label && item.href);
+ case "footer":
+ return values.copyright && values.legal_label;
+ case "legals":
+ return (
+ values.hoster_name &&
+ values.hoster_address &&
+ values.hoster_contact &&
+ Array.isArray(values.intellectual_property) &&
+ values.intellectual_property.length > 0 &&
+ values.intellectual_property.every(ip => ip.paragraph)
+ );
+ case "build":
+ return !!values.theme;
+ default:
+ return true;
+ }
+ }
+
+ function updateSectionStatus(section) {
+ const statusEl = document.querySelector(`#${section}-section .section-status`);
+ if (!statusEl) return;
+ if (!isSectionComplete(section)) {
+ statusEl.innerHTML = "⚠️ Section not yet saved. Please fill required fields";
+ statusEl.style.color = "#ffc700";
+ statusEl.style.display = "";
+ statusEl.style.fontStyle = "normal";
+ return;
+ }
+ if (isSectionSaved(section)) {
+ statusEl.innerHTML = "";
+ statusEl.style.display = "none";
+ } else {
+ statusEl.innerHTML = "⚠️ Section not yet saved";
+ statusEl.style.color = "#ffc700";
+ statusEl.style.display = "";
+ statusEl.style.fontStyle = "normal";
+ }
+ }
+
+ // --- Listen for changes in each section ---
+ Object.entries(forms).forEach(([section, form]) => {
+ if (!form) return;
+ form.addEventListener("input", () => updateSectionStatus(section));
+ form.addEventListener("change", () => updateSectionStatus(section));
form.addEventListener("submit", async (e) => {
e.preventDefault();
- updateMenuItemsFromInputs();
- updateIpParagraphsFromInputs();
-
- // Check if thumbnail is set before saving (uploaded or present in input)
- if (!thumbnailInput || !thumbnailInput.value) {
- showLoader("Saving...");
- showToast("❌ Thumbnail is required.", "error");
- hideLoader();
+ if (!form.reportValidity()) {
+ showToast("❌ Please fill all required fields before saving.", "error");
+ updateSectionStatus(section);
return;
}
-
- const build = {
- theme: themeSelect ? themeSelect.value : "",
- convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked),
- resize_images: !!(resizeImagesCheckbox && resizeImagesCheckbox.checked)
- };
-
- const payload = {
- info: {
- title: form.elements["info.title"].value,
- subtitle: form.elements["info.subtitle"].value,
- description: form.elements["info.description"].value,
- canonical: form.elements["info.canonical"].value,
- keywords: form.elements["info.keywords"].value.split(",").map(i => i.trim()).filter(Boolean),
- author: form.elements["info.author"].value
- },
- social: {
- instagram_url: form.elements["social.instagram_url"].value,
- thumbnail: thumbnailInput ? thumbnailInput.value : ""
- },
- menu: {
- items: menuItems
- },
- footer: {
- copyright: form.elements["footer.copyright"].value,
- legal_label: form.elements["footer.legal_label"].value
- },
- build,
- legals: {
- hoster_name: form.elements["legals.hoster_name"].value,
- hoster_address: form.elements["legals.hoster_address"].value,
- hoster_contact: form.elements["legals.hoster_contact"].value,
- intellectual_property: ipParagraphs
+ if (section === "social" && (!thumbnailInput || !thumbnailInput.value)) {
+ showToast("❌ Thumbnail is required.", "error");
+ updateSectionStatus(section);
+ return;
+ }
+ if (section === "menu") {
+ updateMenuItemsFromInputs();
+ if (!menuItems.length || !menuItems.every(item => item.label && item.href)) {
+ showToast("❌ Please fill all menu item fields.", "error");
+ updateSectionStatus(section);
+ return;
}
- };
- // --- REMOVE loader for save ---
- // showLoader("Saving...");
+ }
+ if (section === "legals") {
+ updateIpParagraphsFromInputs();
+ if (!ipParagraphs.length || !ipParagraphs.every(ip => ip.paragraph)) {
+ showToast("❌ Please fill all intellectual property paragraphs.", "error");
+ updateSectionStatus(section);
+ return;
+ }
+ }
+ let payload = {};
+ payload[section] = getSectionValues(section);
const res = await fetch("/api/site-info", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -447,10 +519,16 @@ document.addEventListener("DOMContentLoaded", () => {
});
const result = await res.json();
if (result.status === "ok") {
- showToast("✅ Site info saved!", "success");
+ showToast("✅ Section saved!", "success");
+ fetch("/api/site-info")
+ .then(res => res.json())
+ .then(data => {
+ loadedConfig = data;
+ updateSectionStatus(section);
+ });
} else {
- showToast("❌ Error saving site info", "error");
+ showToast("❌ Error saving section", "error");
}
});
- }
+ });
});
\ No newline at end of file
diff --git a/src/webui/js/theme-editor.js b/src/webui/js/theme-editor.js
index f7b45c4..481c51d 100644
--- a/src/webui/js/theme-editor.js
+++ b/src/webui/js/theme-editor.js
@@ -70,9 +70,11 @@ function setupColorPicker(colorId, btnId, textId, initial) {
function setFontDropdown(selectId, value, options) {
const select = document.getElementById(selectId);
if (!select) return;
- select.innerHTML = options.map(opt =>
- `
`
- ).join("");
+ select.innerHTML = options.map(opt => {
+ // Remove extension if present
+ const base = opt.replace(/\.(woff2?|ttf|otf)$/, "");
+ return `
`;
+ }).join("");
}
function setFallbackDropdown(selectId, value) {
@@ -116,54 +118,173 @@ function renderLocalFonts(fonts) {
});
}
+// --- Section helpers ---
+function getSectionValues(section) {
+ switch (section) {
+ case "colors":
+ return {
+ 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
+ };
+ case "google-fonts":
+ const googleFontsFields = document.getElementById("google-fonts-fields");
+ const fonts = [];
+ if (googleFontsFields) {
+ googleFontsFields.querySelectorAll(".input-field").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);
+ if (family) fonts.push({ family, weights });
+ });
+ }
+ return fonts;
+ case "fonts":
+ return {
+ 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
+ }
+ };
+ case "favicon":
+ return {
+ path: document.getElementById("favicon-path").value
+ };
+ default:
+ return {};
+ }
+}
+
+function isSectionComplete(section) {
+ switch (section) {
+ case "colors":
+ const v = getSectionValues("colors");
+ return (
+ v.primary &&
+ v.primary_dark &&
+ v.secondary &&
+ v.accent &&
+ v.text_dark &&
+ v.background &&
+ v.browser_color
+ );
+ case "google-fonts":
+ const fonts = getSectionValues("google-fonts");
+ return fonts.every(f => f.family);
+ case "fonts":
+ const f = getSectionValues("fonts");
+ return f.primary.name && f.primary.fallback && f.secondary.name && f.secondary.fallback;
+ case "favicon":
+ const fav = getSectionValues("favicon");
+ return !!fav.path;
+ default:
+ return true;
+ }
+}
+
+function isSectionSaved(section, loadedConfig) {
+ switch (section) {
+ case "colors":
+ const v = getSectionValues("colors");
+ const c = loadedConfig.colors || {};
+ return (
+ v.primary === c.primary &&
+ v.primary_dark === c.primary_dark &&
+ v.secondary === c.secondary &&
+ v.accent === c.accent &&
+ v.text_dark === c.text_dark &&
+ v.background === c.background &&
+ v.browser_color === c.browser_color
+ );
+ case "google-fonts":
+ const fonts = getSectionValues("google-fonts");
+ const cf = loadedConfig.google_fonts || [];
+ return JSON.stringify(fonts) === JSON.stringify(cf);
+ case "fonts":
+ const f = getSectionValues("fonts");
+ const cfnt = loadedConfig.fonts || {};
+ return (
+ f.primary.name === (cfnt.primary?.name || "") &&
+ f.primary.fallback === (cfnt.primary?.fallback || "") &&
+ f.secondary.name === (cfnt.secondary?.name || "") &&
+ f.secondary.fallback === (cfnt.secondary?.fallback || "")
+ );
+ case "favicon":
+ const fav = getSectionValues("favicon");
+ const cfav = loadedConfig.favicon || {};
+ return fav.path === (cfav.path || "");
+ default:
+ return true;
+ }
+}
+
+function updateSectionStatus(section, loadedConfig) {
+ const statusEl = document.querySelector(`#${section}-form .section-status`);
+ if (!statusEl) return;
+ if (!isSectionComplete(section)) {
+ statusEl.innerHTML = "⚠️ Section not yet saved. Please fill required fields";
+ statusEl.style.color = "#ffc700";
+ statusEl.style.display = "";
+ statusEl.style.fontStyle = "normal";
+ return;
+ }
+ if (isSectionSaved(section, loadedConfig)) {
+ statusEl.innerHTML = "";
+ statusEl.style.display = "none";
+ } else {
+ statusEl.innerHTML = "⚠️ Section not yet saved";
+ statusEl.style.color = "#ffc700";
+ statusEl.style.display = "";
+ statusEl.style.fontStyle = "normal";
+ }
+}
+
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 loadedConfig = themeInfo.theme_yaml;
+ let googleFonts = loadedConfig.google_fonts ? JSON.parse(JSON.stringify(loadedConfig.google_fonts)) : [];
let localFonts = await fetchLocalFonts(themeInfo.theme_name);
// Colors
- if (themeYaml.colors) {
- setupColorPicker("color-primary", "color-primary-btn", "color-primary-text", themeYaml.colors.primary || "#0065a1");
- setupColorPicker("color-primary-dark", "color-primary-dark-btn", "color-primary-dark-text", themeYaml.colors.primary_dark || "#005384");
- setupColorPicker("color-secondary", "color-secondary-btn", "color-secondary-text", themeYaml.colors.secondary || "#00b0f0");
- setupColorPicker("color-accent", "color-accent-btn", "color-accent-text", themeYaml.colors.accent || "#ffc700");
- setupColorPicker("color-text-dark", "color-text-dark-btn", "color-text-dark-text", themeYaml.colors.text_dark || "#616161");
- setupColorPicker("color-background", "color-background-btn", "color-background-text", themeYaml.colors.background || "#fff");
- setupColorPicker("color-browser-color", "color-browser-color-btn", "color-browser-color-text", themeYaml.colors.browser_color || "#fff");
+ if (loadedConfig.colors) {
+ setupColorPicker("color-primary", "color-primary-btn", "color-primary-text", loadedConfig.colors.primary || "#0065a1");
+ setupColorPicker("color-primary-dark", "color-primary-dark-btn", "color-primary-dark-text", loadedConfig.colors.primary_dark || "#005384");
+ setupColorPicker("color-secondary", "color-secondary-btn", "color-secondary-text", loadedConfig.colors.secondary || "#00b0f0");
+ setupColorPicker("color-accent", "color-accent-btn", "color-accent-text", loadedConfig.colors.accent || "#ffc700");
+ setupColorPicker("color-text-dark", "color-text-dark-btn", "color-text-dark-text", loadedConfig.colors.text_dark || "#616161");
+ setupColorPicker("color-background", "color-background-btn", "color-background-text", loadedConfig.colors.background || "#fff");
+ setupColorPicker("color-browser-color", "color-browser-color-btn", "color-browser-color-text", loadedConfig.colors.browser_color || "#fff");
}
// Fonts
function refreshFontDropdowns() {
- setFontDropdown("font-primary", document.getElementById("font-primary").value, [
+ setFontDropdown("font-primary", loadedConfig.fonts?.primary?.name || "Lato", [
...googleFonts.map(f => f.family),
...localFonts
]);
- setFontDropdown("font-secondary", document.getElementById("font-secondary").value, [
+ setFontDropdown("font-secondary", loadedConfig.fonts?.secondary?.name || "Montserrat", [
...googleFonts.map(f => f.family),
...localFonts
]);
+ setFallbackDropdown("font-primary-fallback", loadedConfig.fonts?.primary?.fallback || "sans-serif");
+ setFallbackDropdown("font-secondary-fallback", loadedConfig.fonts?.secondary?.fallback || "serif");
}
- 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");
- }
+ refreshFontDropdowns();
// 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
@@ -341,9 +462,9 @@ document.addEventListener("DOMContentLoaded", async () => {
};
}
- if (themeYaml.favicon && themeYaml.favicon.path) {
- faviconInput.value = themeYaml.favicon.path;
- updateFaviconPreview(`/themes/${themeInfo.theme_name}/${themeYaml.favicon.path}?t=${Date.now()}`);
+ if (loadedConfig.favicon && loadedConfig.favicon.path) {
+ faviconInput.value = loadedConfig.favicon.path;
+ updateFaviconPreview(`/themes/${themeInfo.theme_name}/${loadedConfig.favicon.path}?t=${Date.now()}`);
} else {
updateFaviconPreview("");
}
@@ -367,6 +488,7 @@ document.addEventListener("DOMContentLoaded", async () => {
googleFonts.push(...updatedGoogleFonts);
renderGoogleFonts(googleFonts);
refreshFontDropdowns();
+ updateSectionStatus("google-fonts", loadedConfig);
});
}
@@ -396,6 +518,7 @@ document.addEventListener("DOMContentLoaded", async () => {
googleFonts.push(...updatedGoogleFonts);
renderGoogleFonts(googleFonts);
refreshFontDropdowns();
+ updateSectionStatus("google-fonts", loadedConfig);
}
}, true);
@@ -414,54 +537,72 @@ document.addEventListener("DOMContentLoaded", async () => {
googleFonts.push(...updatedGoogleFonts);
renderGoogleFonts(googleFonts);
refreshFontDropdowns();
+ updateSectionStatus("google-fonts", loadedConfig);
}
});
}
- document.getElementById("theme-editor-form").addEventListener("submit", async (e) => {
- e.preventDefault();
- showLoader("Saving theme...");
- 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 })
- });
- hideLoader();
- if (res.ok) {
- showToast("✅ Theme saved!", "success");
- } else {
- showToast("Error saving theme.", "error");
- }
+ // --- Section status listeners ---
+ [
+ { form: document.getElementById("colors-form"), section: "colors" },
+ { form: document.getElementById("google-fonts-form"), section: "google-fonts" },
+ { form: document.getElementById("fonts-form"), section: "fonts" },
+ { form: document.getElementById("favicon-form"), section: "favicon" }
+ ].forEach(({ form, section }) => {
+ if (!form) return;
+ form.addEventListener("input", () => updateSectionStatus(section, loadedConfig));
+ form.addEventListener("change", () => updateSectionStatus(section, loadedConfig));
});
+
+ // --- Section save handlers ---
+ [
+ { form: document.getElementById("colors-form"), section: "colors" },
+ { form: document.getElementById("google-fonts-form"), section: "google-fonts" },
+ { form: document.getElementById("fonts-form"), section: "fonts" },
+ { form: document.getElementById("favicon-form"), section: "favicon" }
+ ].forEach(({ form, section }) => {
+ if (!form) return;
+ form.addEventListener("submit", async (e) => {
+ e.preventDefault();
+ if (!form.reportValidity() || !isSectionComplete(section)) {
+ showToast("❌ Please fill all required fields before saving.", "error");
+ updateSectionStatus(section, loadedConfig);
+ return;
+ }
+ // Merge with loadedConfig to avoid overwriting other sections
+ let payload = { ...loadedConfig };
+ switch (section) {
+ case "colors":
+ payload.colors = getSectionValues("colors");
+ break;
+ case "google-fonts":
+ payload.google_fonts = getSectionValues("google-fonts");
+ break;
+ case "fonts":
+ payload.fonts = getSectionValues("fonts");
+ break;
+ case "favicon":
+ payload.favicon = getSectionValues("favicon");
+ break;
+ }
+ showLoader("Saving...");
+ const res = await fetch("/api/theme-info", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ theme_name: themeInfo.theme_name, theme_yaml: payload })
+ });
+ hideLoader();
+ if (res.ok) {
+ showToast("✅ Section saved!", "success");
+ const updatedThemeInfo = await fetchThemeInfo();
+ loadedConfig = updatedThemeInfo.theme_yaml;
+ updateSectionStatus(section, loadedConfig);
+ } else {
+ showToast("Error saving section.", "error");
+ }
+ });
+ });
+
+ // Initial status update
+ ["colors", "google-fonts", "fonts", "favicon"].forEach(section => updateSectionStatus(section, loadedConfig));
});
\ No newline at end of file
diff --git a/src/webui/js/upload.js b/src/webui/js/upload.js
index d2a45f1..27e745f 100644
--- a/src/webui/js/upload.js
+++ b/src/webui/js/upload.js
@@ -11,54 +11,37 @@ function hideLoader() {
if (loader) loader.classList.remove("active");
}
-// --- Upload gallery images ---
-const galleryInput = document.getElementById('upload-gallery');
-if (galleryInput) {
- galleryInput.addEventListener('change', async (e) => {
+// --- Generic upload handler ---
+function setupUpload(inputId, apiUrl, loaderText, successMsg, refreshFn) {
+ const input = document.getElementById(inputId);
+ if (!input) return;
+ input.addEventListener('change', async (e) => {
const files = e.target.files;
if (!files.length) return;
- showLoader("Uploading photos...");
+ showLoader(loaderText);
const formData = new FormData();
for (const file of files) formData.append('files', file);
try {
- const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData });
+ const res = await fetch(apiUrl, { method: 'POST', body: formData });
const data = await res.json();
hideLoader();
if (res.ok) {
- showToast(`✅ ${data.uploaded.length} gallery image(s) uploaded!`, "success");
- if (typeof refreshGallery === "function") refreshGallery();
+ showToast(`✅ ${data.uploaded.length} ${successMsg}`, "success");
+ if (typeof refreshFn === "function") refreshFn();
} else showToast('Error: ' + data.error, "error");
} catch(err) {
hideLoader();
console.error(err);
showToast('Server error!', "error");
- } finally { e.target.value = ''; }
+ } finally {
+ e.target.value = '';
+ }
});
}
-// --- Upload hero images ---
-const heroInput = document.getElementById('upload-hero');
-if (heroInput) {
- heroInput.addEventListener('change', async (e) => {
- const files = e.target.files;
- if (!files.length) return;
- showLoader("Uploading hero photos...");
- const formData = new FormData();
- for (const file of files) formData.append('files', file);
-
- try {
- const res = await fetch('/api/hero/upload', { method: 'POST', body: formData });
- const data = await res.json();
- hideLoader();
- if (res.ok) {
- showToast(`✅ ${data.uploaded.length} hero image(s) uploaded!`, "success");
- if (typeof refreshHero === "function") refreshHero();
- } else showToast('Error: ' + data.error, "error");
- } catch(err) {
- hideLoader();
- console.error(err);
- showToast('Server error!', "error");
- } finally { e.target.value = ''; }
- });
-}
\ No newline at end of file
+// --- Setup all upload inputs ---
+setupUpload('upload-gallery', '/api/gallery/upload', "Uploading photos...", "gallery image(s) uploaded!", refreshGallery);
+setupUpload('upload-hero', '/api/hero/upload', "Uploading hero photos...", "hero image(s) uploaded!", refreshHero);
+setupUpload('upload-gallery-bottom', '/api/gallery/upload', "Uploading photos...", "gallery image(s) uploaded!", refreshGallery);
+setupUpload('upload-hero-bottom', '/api/hero/upload', "Uploading hero photos...", "hero image(s) uploaded!", refreshHero);
\ No newline at end of file
diff --git a/src/webui/site-info/index.html b/src/webui/site-info/index.html
index 564e31b..eaf5d05 100644
--- a/src/webui/site-info/index.html
+++ b/src/webui/site-info/index.html
@@ -4,176 +4,204 @@
{% block content %}
-
Edit Site Info
-
-
-
Steps
-
Follow the steps to generate your static gallery
-
+
Edit Site Info
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Steps
+
Follow the steps to generate your static gallery
+
+
-
-
-
-
×
-
Confirm Deletion
-
Are you sure you want to remove this thumbnail?
-
-
-
-
+
+
+
+
×
+
Confirm Deletion
+
Are you sure you want to remove this thumbnail?
+
+
+
-
-
-
-
-
×
-
Confirm Theme Deletion
-
Are you sure you want to remove this theme?
-
-
-
-
+
+
+
+
+
+
×
+
Confirm Theme Deletion
+
Are you sure you want to remove this theme?
+
+
+
+
{% endblock %}
diff --git a/src/webui/style/style.css b/src/webui/style/style.css
index c75c539..abc69e2 100644
--- a/src/webui/style/style.css
+++ b/src/webui/style/style.css
@@ -7,7 +7,7 @@ body {
flex-direction: column;
min-height: 100vh;
margin:0px;
- width: 100vw;
+ min-width: 320px;
}
a {
@@ -236,11 +236,32 @@ h2 {
cursor: pointer;
}
+/* --- Filter Row --- */
+.filter-row label {
+ display: inline-block;
+ margin-right: 16px;
+ font-weight: bold;
+ cursor: pointer;
+}
+
+.filter-row input[type="radio"] {
+ accent-color: #55c3ec;
+ margin-right: 6px;
+}
+
/* --- Gallery & Hero Grid --- */
#gallery, #hero {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 15px;
+ margin: 30px 0 0 0;
+}
+
+#hero-count-bottom, #gallery-count-bottom {
+ flex-basis: 100%;
+}
+
+.bottom-action-row {
margin-top: 30px;
}
@@ -316,6 +337,20 @@ h2 {
.toast.success { background-color: #28a7468c; }
.toast.error { background-color: #dc3545; }
+/* img fade in */
+
+.fade-in-img {
+ opacity: 0;
+ transform: scale(1.02);
+ transition: opacity 1.2s ease-out, transform 1.2s ease-out;
+ will-change: opacity, transform;
+}
+
+.fade-in-img.loaded {
+ opacity: 1;
+ transform: scale(1);
+}
+
/* --- Tags --- */
.tag-input {
display: flex;
@@ -366,15 +401,18 @@ h2 {
z-index: 999;
display: none;
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
+ padding: 8px;
+ border-radius: 10px;
}
.tag-input ul.suggestions li {
- padding: 6px 8px;
+ padding: 6px 10px;
cursor: pointer;
}
.tag-input ul.suggestions li:hover {
background-color: #007782;
+ border-radius: 7px;
}
.tags-display {
@@ -406,7 +444,9 @@ h2 {
background-color: #007782;
color: white;
cursor: pointer;
+ border-radius: 7px;
}
+
.suggestions li {
cursor: pointer;
}
@@ -416,6 +456,14 @@ h2 {
display: flex;
}
+.modal-actions-row {
+ display: flex;
+ gap: 16px;
+ flex-wrap: wrap;
+ justify-content: center; /* Center horizontally */
+ align-items: center; /* Center vertically */
+}
+
.flex-column {
flex-direction: column;
}
@@ -521,18 +569,23 @@ h2 {
align-items: center;
gap: 16px;
margin-bottom: 10px;
+ flex-wrap: wrap;
}
/* --- Remove All Buttons --- */
-#remove-all-hero, #remove-all-gallery {
+#remove-all-hero, #remove-all-gallery, .bottom-remove-btn {
background: #2d2d2d;
color: white;
display: none;
margin-bottom: 6px;
}
+.bottom-remove-btn {
+ display: inherit;
+}
+
#remove-all-gallery:hover,
-#remove-all-hero:hover {
+#remove-all-hero:hover, .bottom-remove-btn:hover {
background: rgb(121, 26, 19);
}
@@ -541,6 +594,7 @@ h2 {
flex-direction: column;
align-items: stretch;
gap: 8px;
+ flex-wrap: wrap;
}
}
@@ -588,9 +642,9 @@ label {
letter-spacing: 0.5px;
}
-#site-info-form input, #theme-editor-form input,
-#site-info-form textarea, #theme-editor-form textarea,
-#site-info-form select, #theme-editor-form select {
+form input,
+form textarea,
+form select {
background: #1f2223;
color: #fff;
border: 1px solid #585858;
@@ -612,27 +666,24 @@ label {
gap: 0 18px;
}
+.theme-info {
+ margin-bottom: 25px;
+}
-
-#site-info-form input::placeholder,
-#theme-editor-form input::placeholder,
-#site-info-form textarea::placeholder,
-#theme-editor-form textarea::placeholder {
+form input::placeholder,
+form textarea::placeholder {
color: #585858;
font-style: italic;
}
-#site-info-form input:focus,
-#theme-editor-form input:focus,
-#site-info-form textarea:focus,
-#theme-editor-form textarea:focus,
-#site-info-form select:focus,
-#theme-editor-form select:focus {
+form input:focus,
+form textarea:focus,
+form select:focus {
border-color: #585858;
background: #161616;
}
-#site-info-form textarea,
-#theme-editor-form textarea {
+
+form textarea {
min-height: 60px;
resize: vertical;
}
@@ -651,7 +702,7 @@ img#thumbnail-preview {
border: 1px solid #585858;
}
-#site-info-form button[type="submit"], #theme-editor-form button[type="submit"] {
+form button[type="submit"] {
background: linear-gradient(135deg, #26c4ff, #016074);
color: #fff;
font-weight: 700;
@@ -659,17 +710,18 @@ img#thumbnail-preview {
border-radius: 30px;
padding: 12px 32px;
font-size: 1.1em;
- margin: 0 0 45px 0;
+ margin: 16px 0 0 0;
cursor: pointer;
box-shadow: 0 4px 16px rgba(38,196,255,0.15);
transition: background 0.2s;
+ display: block;
}
-#site-info-form button[type="submit"]:hover, #theme-editor-form button[type="submit"]:hover {
+form button[type="submit"]:hover {
background: linear-gradient(135deg, #72d9ff, #26657e);
}
-#site-info-form button[type="button"], #theme-editor-form button[type="button"] {
+form button[type="button"] {
background: #00000000;
color: #fff;
border: none;
@@ -682,34 +734,36 @@ img#thumbnail-preview {
border: 1px solid #585858;
}
-#site-info-form button[type="button"]:hover, #theme-editor-form button[type="button"]:hover {
+form button[type="button"]:hover{
background: #2d2d2d;
color: #fff;
}
-#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 {
+form button.remove-menu-item,
+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, #theme-editor-form button.remove-menu-item:hover, #theme-editor-form button.remove-ip-paragraph:hover {
+form button.remove-menu-item:hover,
+form button.remove-ip-paragraph:hover {
background: rgb(121, 26, 19);
}
-#site-info-form button.remove-btn, #theme-editor-form button.remove-btn {
+form button.remove-btn {
border-radius: 30px;
background: #2d2d2d;
}
-#site-info-form button.remove-btn:hover, #theme-editor-form button.remove-btn:hover {
+form button.remove-btn:hover {
background: rgb(121, 26, 19);
}
-#site-info-form .thumbnail-form-label, #theme-editor-form .thumbnail-form-label {
+form .thumbnail-form-label {
margin-top: 10px;
}
@@ -771,6 +825,10 @@ fieldset p, .section p {
margin-top: 16px;
}
+.section-status {
+ font-style: normal;
+}
+
/* --- Stepper --- */
#stepper {
@@ -905,6 +963,7 @@ justify-content: center;
#footer a {
color: #fff;
+ margin: auto;
}
.footer-credit .lum-first::before {
@@ -920,6 +979,7 @@ justify-content: center;
width: 16px;
height: 16px;
display: flex;
+ margin: auto;
}
.icon-text {
@@ -993,13 +1053,13 @@ justify-content: center;
#menu-items-list > div {
flex-direction: column;
}
-
- #stepper {
- flex-direction: column;
+
+ form button[type="button"], form button[type="submit"], #stepper li {
+ width: 100%;
}
- #stepper li {
- width: 100%;
+ #stepper {
+ flex-direction: column;
}
.footer-container, .footer-links {
@@ -1020,14 +1080,16 @@ justify-content: center;
/* Hide the default arrow */
color: transparent;
}
- #site-info-form, #theme-editor-form {
- padding: 18px 8px;
- }
+
.input-field {
min-width: 100%;
margin-bottom: 12px;
}
+ #site-info-form button[type="submit"], #theme-editor-form button[type="submit"] {
+ width: 100%;
+ }
+
#color-picker .input-field{
min-width: 170px;
}
diff --git a/src/webui/template/base.html b/src/webui/template/base.html
index 0aa7213..f5b44cb 100644
--- a/src/webui/template/base.html
+++ b/src/webui/template/base.html
@@ -48,7 +48,10 @@
×
✅ Build completed!
Your files are available in the output folder.
-
+
+
+
+
Creating ZIP...
@@ -65,7 +68,7 @@
diff --git a/src/webui/theme-editor/index.html b/src/webui/theme-editor/index.html
index 2a97c8e..cbd3f8e 100644
--- a/src/webui/theme-editor/index.html
+++ b/src/webui/theme-editor/index.html
@@ -4,176 +4,193 @@
{% block content %}
-
Edit Theme
-
-
- Current theme:
-
-