From debbf072806546128e9f118cfe029d90b32b1327 Mon Sep 17 00:00:00 2001 From: Djeex Date: Sun, 31 Aug 2025 14:34:22 +0200 Subject: [PATCH 01/27] New bottom upload btn + images count --- VERSION | 2 +- src/webui/gallery-editor/index.html | 18 ++++++++++ src/webui/js/gallery-editor.js | 56 +++++++++++++++++++++++++++-- src/webui/js/upload.js | 53 +++++++++++++++++++++++++++ src/webui/style/style.css | 20 +++++++++-- 5 files changed, 143 insertions(+), 6 deletions(-) diff --git a/VERSION b/VERSION index 10bf840..50aea0e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.1 \ No newline at end of file +2.1.0 \ No newline at end of file diff --git a/src/webui/gallery-editor/index.html b/src/webui/gallery-editor/index.html index 329a65c..6c3efd8 100644 --- a/src/webui/gallery-editor/index.html +++ b/src/webui/gallery-editor/index.html @@ -18,7 +18,16 @@ +
+ +
+ + + +
@@ -31,8 +40,17 @@ + + +
diff --git a/src/webui/js/gallery-editor.js b/src/webui/js/gallery-editor.js index 640e567..2aa209c 100644 --- a/src/webui/js/gallery-editor.js +++ b/src/webui/js/gallery-editor.js @@ -54,11 +54,35 @@ function renderGallery() { renderTags(i, img.tags || []); }); - // Show/hide Remove All button + // Update gallery count (top) + const galleryCount = document.getElementById('gallery-count'); + if (galleryCount) { + galleryCount.innerHTML = `

${galleryImages.length} photos

`; + } + + // Update gallery count (bottom) + const galleryCountBottom = document.getElementById('gallery-count-bottom'); + if (galleryCountBottom) { + galleryCountBottom.innerHTML = `

${galleryImages.length} photos

`; + } + + // Show/hide Remove All button (top) const removeAllBtn = document.getElementById('remove-all-gallery'); if (removeAllBtn) { removeAllBtn.style.display = galleryImages.length > 0 ? 'inline-block' : 'none'; } + + // Show/hide bottom upload row + const bottomGalleryUpload = document.getElementById('bottom-gallery-upload'); + if (bottomGalleryUpload) { + bottomGalleryUpload.style.display = galleryImages.length > 0 ? 'flex' : 'none'; + } + + // Show/hide Remove All button (bottom) + const removeAllBtnBottom = document.getElementById('remove-all-gallery-bottom'); + if (removeAllBtnBottom) { + removeAllBtnBottom.style.display = galleryImages.length > 0 ? 'inline-block' : 'none'; + } } // --- Render tags for a single image --- @@ -244,11 +268,35 @@ function renderHero() { container.appendChild(div); }); - // Show/hide Remove All button + // Update hero count (top) + const heroCount = document.getElementById('hero-count'); + if (heroCount) { + heroCount.innerHTML = `

${heroImages.length} photos

`; + } + + // Update hero count (bottom) + const heroCountBottom = document.getElementById('hero-count-bottom'); + if (heroCountBottom) { + heroCountBottom.innerHTML = `

${heroImages.length} photos

`; + } + + // Show/hide Remove All button (top) const removeAllBtn = document.getElementById('remove-all-hero'); if (removeAllBtn) { removeAllBtn.style.display = heroImages.length > 0 ? 'inline-block' : 'none'; } + + // Show/hide bottom upload row + const bottomHeroUpload = document.getElementById('bottom-hero-upload'); + if (bottomHeroUpload) { + bottomHeroUpload.style.display = heroImages.length > 0 ? 'flex' : 'none'; + } + + // Show/hide Remove All button (bottom) + const removeAllBtnBottom = document.getElementById('remove-all-hero-bottom'); + if (removeAllBtnBottom) { + removeAllBtnBottom.style.display = heroImages.length > 0 ? 'inline-block' : 'none'; + } } // --- Save gallery to server --- @@ -429,9 +477,13 @@ document.addEventListener('DOMContentLoaded', () => { // Bulk delete buttons const removeAllGalleryBtn = document.getElementById('remove-all-gallery'); + const removeAllGalleryBtnBottom = document.getElementById('remove-all-gallery-bottom'); const removeAllHeroBtn = document.getElementById('remove-all-hero'); + const removeAllHeroBtnBottom = document.getElementById('remove-all-hero-bottom'); if (removeAllGalleryBtn) removeAllGalleryBtn.onclick = () => showDeleteModal('gallery-all'); + if (removeAllGalleryBtnBottom) removeAllGalleryBtnBottom.onclick = () => showDeleteModal('gallery-all'); if (removeAllHeroBtn) removeAllHeroBtn.onclick = () => showDeleteModal('hero-all'); + if (removeAllHeroBtnBottom) removeAllHeroBtnBottom.onclick = () => showDeleteModal('hero-all'); }); // --- Initialize --- diff --git a/src/webui/js/upload.js b/src/webui/js/upload.js index d2a45f1..8316b6e 100644 --- a/src/webui/js/upload.js +++ b/src/webui/js/upload.js @@ -11,6 +11,7 @@ function hideLoader() { if (loader) loader.classList.remove("active"); } + // --- Upload gallery images --- const galleryInput = document.getElementById('upload-gallery'); if (galleryInput) { @@ -47,6 +48,58 @@ if (heroInput) { 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 = ''; } + }); +} + +// --- Upload gallery images (bottom button) --- +const galleryInputBottom = document.getElementById('upload-gallery-bottom'); +if (galleryInputBottom) { + galleryInputBottom.addEventListener('change', async (e) => { + const files = e.target.files; + if (!files.length) return; + showLoader("Uploading photos..."); + 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 data = await res.json(); + hideLoader(); + if (res.ok) { + showToast(`✅ ${data.uploaded.length} gallery image(s) uploaded!`, "success"); + if (typeof refreshGallery === "function") refreshGallery(); + } else showToast('Error: ' + data.error, "error"); + } catch(err) { + hideLoader(); + console.error(err); + showToast('Server error!', "error"); + } finally { e.target.value = ''; } + }); +} + +// --- Upload hero images (bottom button) --- +const heroInputBottom = document.getElementById('upload-hero-bottom'); +if (heroInputBottom) { + heroInputBottom.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(); diff --git a/src/webui/style/style.css b/src/webui/style/style.css index c75c539..c397faa 100644 --- a/src/webui/style/style.css +++ b/src/webui/style/style.css @@ -241,7 +241,7 @@ h2 { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 15px; - margin-top: 30px; + margin: 30px 0 0 0; } /* --- Photo Card --- */ @@ -521,18 +521,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 +546,7 @@ h2 { flex-direction: column; align-items: stretch; gap: 8px; + flex-wrap: wrap; } } @@ -1036,4 +1042,12 @@ justify-content: center; position: relative; flex-wrap: nowrap; } +} + +#hero-count-bottom, #gallery-count-bottom { + flex-basis: 100%; +} + +.bottom-action-row { + margin-top: 30px; } \ No newline at end of file -- 2.49.0 From 021e0c7974379a503f2f14f8552e7e9e83ee359b Mon Sep 17 00:00:00 2001 From: Djeex Date: Mon, 1 Sep 2025 14:37:31 +0000 Subject: [PATCH 02/27] Preview link --- .gitignore | 1 + docker/.env | 2 ++ docker/.sh/entrypoint.sh | 2 ++ docker/docker-compose.yaml | 9 +++++++-- src/py/webui/webui.py | 10 +++++++++- src/webui/js/build.js | 12 ++++++++++++ src/webui/style/style.css | 8 ++++++++ src/webui/template/base.html | 5 ++++- 8 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 docker/.env diff --git a/.gitignore b/.gitignore index 7adc878..a833ea8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .* +!.env !.sh !.gitignore output/ diff --git a/docker/.env b/docker/.env new file mode 100644 index 0000000..c03748b --- /dev/null +++ b/docker/.env @@ -0,0 +1,2 @@ +PREVIEW_PORT=6565 +WEBUI_PORT=5000 \ No newline at end of file diff --git a/docker/.sh/entrypoint.sh b/docker/.sh/entrypoint.sh index 77d9eb2..c1f5d73 100644 --- a/docker/.sh/entrypoint.sh +++ b/docker/.sh/entrypoint.sh @@ -43,7 +43,9 @@ start_server() { cat /tmp/build_logs_fifo >&2 & cat /tmp/build_logs_fifo2 >&2 & + PREVIEW_PORT="${PREVIEW_PORT:-3000}" echo "Starting preview HTTP server on port 3000..." + echo "Preview host port is set to: ${PREVIEW_PORT}" python3 -u -m http.server 3000 -d /app/output & SERVER_PID=$! diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index f48000c..793d5af 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -2,10 +2,15 @@ services: lumeex: container_name: lmx build: .. + env_file: + - .env + environment: + - PREVIEW_PORT=${PREVIEW_PORT:-3000} # port for preview server - set it in .env file + - WEBUI_PORT=${WEBUI_PORT:-5000} # port for webui server - set it in .env file volumes: - ../config:/app/config # mount config directory - ../output:/app/output # mount output directory ports: - - "3000:3000" - - "5000:5000" + - "${PREVIEW_PORT:-3000}:3000" + - "${WEBUI_PORT:-5000}:5000" \ No newline at end of file diff --git a/src/py/webui/webui.py b/src/py/webui/webui.py index 75969ac..7a80aff 100644 --- a/src/py/webui/webui.py +++ b/src/py/webui/webui.py @@ -30,6 +30,8 @@ app = Flask( static_url_path="" ) +WEBUI_PORT = int(os.getenv("WEBUI_PORT", 5000)) + # --- Config paths --- SITE_YAML = Path(__file__).resolve().parents[3] / "config" / "site.yaml" PHOTOS_DIR = Path(__file__).resolve().parents[3] / "config" / "photos" @@ -72,9 +74,14 @@ def get_local_fonts(theme_name): def index(): return render_template("index.html") +PREVIEW_PORT = int(os.getenv("PREVIEW_PORT", 3000)) + @app.context_processor def inject_version(): - return dict(lumeex_version=lumeex_version) + return dict( + lumeex_version=lumeex_version, + preview_port=PREVIEW_PORT + ) # --- Gallery & Hero API --- @app.route("/gallery-editor") @@ -488,4 +495,5 @@ def download_output_zip(): # --- Run server --- if __name__ == "__main__": logging.info("Starting WebUI at http://0.0.0.0:5000") + logging.info(f"Host port is {WEBUI_PORT}") app.run(host="0.0.0.0", port=5000, debug=True) \ No newline at end of file diff --git a/src/webui/js/build.js b/src/webui/js/build.js index 91022da..3c7790b 100644 --- a/src/webui/js/build.js +++ b/src/webui/js/build.js @@ -92,6 +92,18 @@ document.addEventListener("DOMContentLoaded", () => { }); } + // Preview Site button + const previewBtn = document.getElementById("preview-site-btn"); + if (previewBtn) { + const previewPort = previewBtn.getAttribute("data-preview-port") || "3000"; + previewBtn.onclick = () => { + const host = window.location.hostname; + const protocol = window.location.protocol; + const url = `${protocol}//${host}:${previewPort}/`; + window.open(url, "_blank"); + }; + } + // Modal close logic if (buildModal && buildModalClose) { buildModalClose.onclick = () => { diff --git a/src/webui/style/style.css b/src/webui/style/style.css index c397faa..74eba61 100644 --- a/src/webui/style/style.css +++ b/src/webui/style/style.css @@ -416,6 +416,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; } diff --git a/src/webui/template/base.html b/src/webui/template/base.html index 0aa7213..ee6233f 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.

- +
-- 2.49.0 From 5b65e5efe3ad44fe3cdec7e1c25f14970dae91c7 Mon Sep 17 00:00:00 2001 From: Djeex Date: Mon, 1 Sep 2025 14:57:52 +0000 Subject: [PATCH 03/27] Better logging --- docker/.sh/entrypoint.sh | 22 +++++++++++----------- src/py/webui/webui.py | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docker/.sh/entrypoint.sh b/docker/.sh/entrypoint.sh index c1f5d73..131ea41 100644 --- a/docker/.sh/entrypoint.sh +++ b/docker/.sh/entrypoint.sh @@ -5,12 +5,12 @@ CYAN="\033[1;36m" NC="\033[0m" copy_default_config() { - echo "Checking configuration directory..." + echo "[~] Checking configuration directory..." if [ ! -d "/app/config" ]; then mkdir -p /app/config fi - echo "Checking if default config files need to be copied..." + echo "[~] Checking if default config files need to be copied..." files_copied=false for file in /app/default/*; do @@ -18,16 +18,16 @@ copy_default_config() { target="/app/config/$filename" if [ ! -e "$target" ]; then - echo "Copying default config file: $filename" + echo "[→] Copying default config file: $filename" cp -r "$file" "$target" files_copied=true fi done if [ "$files_copied" = true ]; then - echo "Default configuration files copied successfully." + echo "[✓] Default configuration files copied successfully." else - echo "No default files needed to be copied." + echo "[✓] No default files needed to be copied." fi } @@ -44,12 +44,12 @@ start_server() { cat /tmp/build_logs_fifo2 >&2 & PREVIEW_PORT="${PREVIEW_PORT:-3000}" - echo "Starting preview HTTP server on port 3000..." - echo "Preview host port is set to: ${PREVIEW_PORT}" + echo "[~]Starting preview HTTP server on port 3000..." + echo "[i] Preview host port is set to: ${PREVIEW_PORT}" python3 -u -m http.server 3000 -d /app/output & SERVER_PID=$! - echo "Starting Lumeex Flask webui..." + echo "[~] Starting Lumeex Flask webui..." python3 -u -m src.py.webui.webui & WEBUI_PID=$! @@ -74,15 +74,15 @@ fi case "$1" in build) - echo "Running build.py..." + echo "[~] Running build.py..." python3 -u /app/build.py 2>&1 | tee /tmp/build_logs_fifo ;; gallery) - echo "Running gallery.py..." + echo "[~] Running gallery.py..." python3 -u /app/gallery.py 2>&1 | tee /tmp/build_logs_fifo2 ;; *) - echo "Unknown command: $1" + echo "[!] Unknown command: $1" exec "$@" ;; esac diff --git a/src/py/webui/webui.py b/src/py/webui/webui.py index 7a80aff..f452f1a 100644 --- a/src/py/webui/webui.py +++ b/src/py/webui/webui.py @@ -494,6 +494,6 @@ def download_output_zip(): # --- Run server --- if __name__ == "__main__": - logging.info("Starting WebUI at http://0.0.0.0:5000") - logging.info(f"Host port is {WEBUI_PORT}") + logging.info("[~] Starting WebUI at http://0.0.0.0:5000") + logging.info(f"[i] WebUI host port is set to {WEBUI_PORT}") app.run(host="0.0.0.0", port=5000, debug=True) \ No newline at end of file -- 2.49.0 From e8718e71abe6caed29c46d5fa013eb9f79089788 Mon Sep 17 00:00:00 2001 From: Djeex Date: Mon, 1 Sep 2025 14:59:13 +0000 Subject: [PATCH 04/27] Fixed port --- docker/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/.env b/docker/.env index c03748b..bcc894c 100644 --- a/docker/.env +++ b/docker/.env @@ -1,2 +1,2 @@ -PREVIEW_PORT=6565 +PREVIEW_PORT=3000 WEBUI_PORT=5000 \ No newline at end of file -- 2.49.0 From f98f2d598f90f12bfbaf33d094185d1de2003007 Mon Sep 17 00:00:00 2001 From: Djeex Date: Mon, 1 Sep 2025 16:16:27 +0000 Subject: [PATCH 05/27] Fixed footer icon and git link --- src/webui/style/style.css | 1 + src/webui/template/base.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/webui/style/style.css b/src/webui/style/style.css index 74eba61..368d246 100644 --- a/src/webui/style/style.css +++ b/src/webui/style/style.css @@ -934,6 +934,7 @@ justify-content: center; width: 16px; height: 16px; display: flex; + margin: auto; } .icon-text { diff --git a/src/webui/template/base.html b/src/webui/template/base.html index ee6233f..f5b44cb 100644 --- a/src/webui/template/base.html +++ b/src/webui/template/base.html @@ -68,7 +68,7 @@ -- 2.49.0 From 31987555765581e99fc0423f85a11d7bfb502e61 Mon Sep 17 00:00:00 2001 From: Djeex Date: Mon, 1 Sep 2025 16:46:43 +0000 Subject: [PATCH 06/27] Top save button + fixed some CSS --- src/webui/site-info/index.html | 2 ++ src/webui/style/style.css | 34 ++++++++++++++++++++----------- src/webui/theme-editor/index.html | 4 +++- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/webui/site-info/index.html b/src/webui/site-info/index.html index 564e31b..8eeebe4 100644 --- a/src/webui/site-info/index.html +++ b/src/webui/site-info/index.html @@ -6,6 +6,7 @@

Edit Site Info

+

Info

@@ -132,6 +133,7 @@
+

Steps

Follow the steps to generate your static gallery

diff --git a/src/webui/style/style.css b/src/webui/style/style.css index 368d246..373df7b 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 { @@ -244,6 +244,14 @@ h2 { margin: 30px 0 0 0; } +#hero-count-bottom, #gallery-count-bottom { + flex-basis: 100%; +} + +.bottom-action-row { + margin-top: 30px; +} + /* --- Photo Card --- */ .photo { background-color: rgb(67 67 67 / 26%); @@ -618,6 +626,10 @@ label { box-shadow: 0 2px 8px rgba(0,0,0,0.07); } +#site-info-form button[type="submit"].top-save, #theme-editor-form button[type="submit"].top-save { + margin-bottom: 0; +} + #theme-editor-form input, #theme-editor-form textarea,#theme-editor-form select { margin-bottom: 18px; } @@ -626,7 +638,9 @@ label { gap: 0 18px; } - +.theme-info { + margin-bottom: 25px; +} #site-info-form input::placeholder, #theme-editor-form input::placeholder, @@ -919,6 +933,7 @@ justify-content: center; #footer a { color: #fff; + margin: auto; } .footer-credit .lum-first::before { @@ -1035,14 +1050,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; } @@ -1053,10 +1070,3 @@ justify-content: center; } } -#hero-count-bottom, #gallery-count-bottom { - flex-basis: 100%; -} - -.bottom-action-row { - margin-top: 30px; -} \ No newline at end of file diff --git a/src/webui/theme-editor/index.html b/src/webui/theme-editor/index.html index 2a97c8e..084a272 100644 --- a/src/webui/theme-editor/index.html +++ b/src/webui/theme-editor/index.html @@ -10,6 +10,7 @@ Current theme:
+

Colors

@@ -133,8 +134,9 @@
- +
+

Steps

Follow the steps to generate your static gallery

-- 2.49.0 From f8bebb9c95d0f9dce568391b2911b94f9abcc1bf Mon Sep 17 00:00:00 2001 From: Djeex Date: Mon, 1 Sep 2025 22:13:16 +0000 Subject: [PATCH 07/27] Fixed forms and key issues --- config/site.yaml | 2 +- src/py/webui/webui.py | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/config/site.yaml b/config/site.yaml index 166b129..166fe66 100644 --- a/config/site.yaml +++ b/config/site.yaml @@ -30,7 +30,7 @@ build: # Change this by your legals legals: hoster_name: - hoster_adress: + hoster_address: hoster_contact: intellectual_property: - paragraph: "" diff --git a/src/py/webui/webui.py b/src/py/webui/webui.py index f452f1a..dbf7810 100644 --- a/src/py/webui/webui.py +++ b/src/py/webui/webui.py @@ -210,9 +210,21 @@ def get_site_info(): @app.route("/api/site-info", methods=["POST"]) def update_site_info(): """Update site info YAML.""" - data = request.json + new_data = request.json + with open(SITE_YAML, "r") as f: + old_data = yaml.safe_load(f) or {} + + def deep_merge(old, new): + for k, v in new.items(): + if isinstance(v, dict) and isinstance(old.get(k), dict): + old[k] = deep_merge(old[k], v) + else: + old[k] = v + return old + + merged = deep_merge(old_data, new_data) with open(SITE_YAML, "w") as f: - yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True) + yaml.safe_dump(merged, f, sort_keys=False, allow_unicode=True) return jsonify({"status": "ok"}) # --- Theme management --- -- 2.49.0 From 795f5fbd1389c025285b26bed700e232d603c493 Mon Sep 17 00:00:00 2001 From: Djeex Date: Tue, 2 Sep 2025 14:44:54 +0200 Subject: [PATCH 08/27] Redone form save logic --- config/themes/modern/theme.yaml | 40 ++- config/themes/typewriter/theme.yaml | 20 +- src/webui/js/site-info.js | 408 ++++++++++++++++++++-------- src/webui/js/theme-editor.js | 285 ++++++++++++++----- src/webui/site-info/index.html | 354 +++++++++++++----------- src/webui/style/style.css | 53 ++-- src/webui/theme-editor/index.html | 339 ++++++++++++----------- 7 files changed, 925 insertions(+), 574 deletions(-) diff --git a/config/themes/modern/theme.yaml b/config/themes/modern/theme.yaml index 26cfa8a..0b23d8a 100644 --- a/config/themes/modern/theme.yaml +++ b/config/themes/modern/theme.yaml @@ -1,32 +1,28 @@ -#-----------------------------------# -# Modern theme for Lumeex # -# https://git.djeex.fr/Djeex/lumeex # -#-----------------------------------# colors: - primary: '#0065a1' + primary: '#0065A1' primary_dark: '#005384' - secondary: '#00b0f0' - accent: '#ffc700' + secondary: '#00B0F0' + accent: '#FFC700' text_dark: '#616161' - background: '#ffffff' - browser_color: '#ffffff' + background: '#FFFFFF' + browser_color: '#FFFFFF' favicon: path: favicon.png -google_fonts: - - family: Lato - weights: - - '200' - - '400' - - '700' - - family: Montserrat - weights: - - '200' - - '400' - - '700' fonts: primary: - name: Lato fallback: sans-serif + name: Lato secondary: + fallback: serif name: Montserrat - fallback: serif \ No newline at end of file +google_fonts: +- family: Lato + weights: + - '200' + - '400' + - '700' +- family: Montserrat + weights: + - '200' + - '400' + - '700' diff --git a/config/themes/typewriter/theme.yaml b/config/themes/typewriter/theme.yaml index 546e89d..fd251d0 100644 --- a/config/themes/typewriter/theme.yaml +++ b/config/themes/typewriter/theme.yaml @@ -1,21 +1,17 @@ -#-----------------------------------# -# Typewriter theme for Lumeex # -# https://git.djeex.fr/Djeex/lumeex # -#-----------------------------------# colors: - primary: '#0065a1' + accent: '#FFC700' + background: '#FFFFFF' + browser_color: '#FFFFFF' + primary: '#0065A1' primary_dark: '#005384' - secondary: '#00b0f0' - accent: '#ffc700' + secondary: '#00B0F0' text_dark: '#616161' - background: '#ffffff' - browser_color: '#ffffff' favicon: path: favicon.png fonts: primary: - name: Trixie + name: trixie.woff fallback: sans-serif secondary: - name: Trixie - fallback: serif \ No newline at end of file + name: trixie.woff + fallback: serif diff --git a/src/webui/js/site-info.js b/src/webui/js/site-info.js index 3af7338..b349bdb 100644 --- a/src/webui/js/site-info.js +++ b/src/webui/js/site-info.js @@ -26,14 +26,19 @@ function hideLoader() { } document.addEventListener("DOMContentLoaded", () => { - // Form and menu logic - const form = document.getElementById("site-info-form"); + // --- Section Forms --- + const infoForm = document.getElementById("info-form"); + const socialForm = document.getElementById("social-form"); + const menuForm = document.getElementById("menu-form"); + const footerForm = document.getElementById("footer-form"); + const legalsForm = document.getElementById("legals-form"); + const buildForm = 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) => { @@ -50,7 +55,6 @@ document.addEventListener("DOMContentLoaded", () => { }); } - // Update menu items from inputs function updateMenuItemsFromInputs() { const inputs = menuList.querySelectorAll("input"); const items = []; @@ -62,12 +66,11 @@ document.addEventListener("DOMContentLoaded", () => { 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) => { @@ -83,7 +86,6 @@ document.addEventListener("DOMContentLoaded", () => { }); } - // Update IP paragraphs from textareas function updateIpParagraphsFromInputs() { const textareas = ipList.querySelectorAll("textarea"); ipParagraphs = Array.from(textareas).map(textarea => ({ @@ -91,27 +93,27 @@ document.addEventListener("DOMContentLoaded", () => { })).filter(item => item.paragraph !== ""); } - // Build options + // --- Build options --- const convertImagesCheckbox = document.getElementById("convert-images-checkbox"); const resizeImagesCheckbox = document.getElementById("resize-images-checkbox"); - // Theme select + // --- 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 + // --- 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 elements for theme deletion + // --- 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"); @@ -119,7 +121,7 @@ document.addEventListener("DOMContentLoaded", () => { 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) { if (thumbnailPreview) { thumbnailPreview.src = src || ""; @@ -133,12 +135,12 @@ document.addEventListener("DOMContentLoaded", () => { } } - // Choose thumbnail button triggers file input + // --- Choose thumbnail button triggers file input --- if (chooseThumbnailBtn && thumbnailUpload) { chooseThumbnailBtn.addEventListener("click", () => thumbnailUpload.click()); } - // Handle thumbnail upload and refresh preview (with cache busting) + // --- Handle thumbnail upload and refresh preview (with cache busting) --- if (thumbnailUpload) { thumbnailUpload.addEventListener("change", async (e) => { const file = e.target.files[0]; @@ -156,17 +158,18 @@ document.addEventListener("DOMContentLoaded", () => { } else { showToast("❌ Error uploading thumbnail", "error"); } + updateSectionStatus("social"); }); } - // Remove thumbnail button triggers modal + // --- Remove thumbnail button triggers modal --- if (removeThumbnailBtn) { removeThumbnailBtn.addEventListener("click", () => { deleteModal.style.display = "flex"; }); } - // Modal logic for thumbnail deletion + // --- Modal logic for thumbnail deletion --- if (deleteModal && deleteModalClose && deleteModalConfirm && deleteModalCancel) { deleteModalClose.onclick = deleteModalCancel.onclick = () => { deleteModal.style.display = "none"; @@ -187,10 +190,11 @@ document.addEventListener("DOMContentLoaded", () => { showToast("❌ Error removing thumbnail", "error"); } deleteModal.style.display = "none"; + updateSectionStatus("social"); }; } - // Theme upload logic (custom theme folder) + // --- Theme upload logic (custom theme folder) --- const themeUpload = document.getElementById("theme-upload"); const chooseThemeBtn = document.getElementById("choose-theme-btn"); if (chooseThemeBtn && themeUpload) { @@ -223,10 +227,11 @@ document.addEventListener("DOMContentLoaded", () => { } else { showToast("❌ Error uploading theme", "error"); } + updateSectionStatus("build"); }); } - // Remove theme button triggers modal + // --- Remove theme button triggers modal --- const removeThemeBtn = document.getElementById("remove-theme-btn"); if (removeThemeBtn && themeSelect) { removeThemeBtn.addEventListener("click", () => { @@ -242,7 +247,7 @@ document.addEventListener("DOMContentLoaded", () => { }); } - // Modal logic for theme deletion + // --- Modal logic for theme deletion --- if (deleteThemeModal && deleteThemeModalClose && deleteThemeModalConfirm && deleteThemeModalCancel) { deleteThemeModalClose.onclick = deleteThemeModalCancel.onclick = () => { deleteThemeModal.style.display = "none"; @@ -283,10 +288,11 @@ document.addEventListener("DOMContentLoaded", () => { } deleteThemeModal.style.display = "none"; themeToDelete = null; + updateSectionStatus("build"); }; } - // Fetch theme list and populate select + // --- Fetch theme list and populate select --- if (themeSelect) { fetch("/api/themes") .then(res => res.json()) @@ -307,139 +313,307 @@ document.addEventListener("DOMContentLoaded", () => { }); } - // Load config from server and populate form - if (form) { - fetch("/api/site-info") - .then(res => res.json()) - .then(data => { - 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 || ""; + // --- Load config from server and populate forms --- + let loadedConfig = {}; + fetch("/api/site-info") + .then(res => res.json()) + .then(data => { + loadedConfig = data; + // Info + if (infoForm) { + infoForm.elements["info.title"].value = data.info?.title || ""; + infoForm.elements["info.subtitle"].value = data.info?.subtitle || ""; + infoForm.elements["info.description"].value = data.info?.description || ""; + infoForm.elements["info.canonical"].value = data.info?.canonical || ""; + infoForm.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || ""); + infoForm.elements["info.author"].value = data.info?.author || ""; + } + // Social + if (socialForm) { + socialForm.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; - } - }); - } + } + // Menu + menuItems = Array.isArray(data.menu?.items) ? data.menu.items : []; + renderMenuItems(); + // Footer + if (footerForm) { + footerForm.elements["footer.copyright"].value = data.footer?.copyright || ""; + footerForm.elements["footer.legal_label"].value = data.footer?.legal_label || ""; + } + // Legals + ipParagraphs = Array.isArray(data.legals?.intellectual_property) + ? data.legals.intellectual_property + : []; + renderIpParagraphs(); + if (legalsForm) { + legalsForm.elements["legals.hoster_name"].value = data.legals?.hoster_name || ""; + legalsForm.elements["legals.hoster_address"].value = data.legals?.hoster_address || ""; + legalsForm.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; + } + // Initial status update + ["info", "social", "menu", "footer", "legals", "build"].forEach(updateSectionStatus); + }); - // Add menu item + // --- Add menu item --- if (addMenuBtn) { addMenuBtn.addEventListener("click", () => { menuItems.push({ label: "", href: "" }); renderMenuItems(); + updateSectionStatus("menu"); }); } - // Remove menu item + // --- Remove menu item --- 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 + // --- Update menuItems on input change --- menuList.addEventListener("input", () => { updateMenuItemsFromInputs(); + updateSectionStatus("menu"); }); - // Add paragraph + // --- Add paragraph --- if (addIpBtn) { addIpBtn.addEventListener("click", () => { ipParagraphs.push({ paragraph: "" }); renderIpParagraphs(); + updateSectionStatus("legals"); }); } - // Remove paragraph + // --- Remove paragraph --- 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 + // --- 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: infoForm.elements["info.title"].value, + subtitle: infoForm.elements["info.subtitle"].value, + description: infoForm.elements["info.description"].value, + canonical: infoForm.elements["info.canonical"].value, + keywords: infoForm.elements["info.keywords"].value.split(",").map(i => i.trim()).filter(Boolean), + author: infoForm.elements["info.author"].value + }; + case "social": + return { + instagram_url: socialForm.elements["social.instagram_url"].value, + thumbnail: thumbnailInput ? thumbnailInput.value : "" + }; + case "menu": + updateMenuItemsFromInputs(); + return { items: menuItems }; + case "footer": + return { + copyright: footerForm.elements["footer.copyright"].value, + legal_label: footerForm.elements["footer.legal_label"].value + }; + case "legals": + updateIpParagraphsFromInputs(); + return { + hoster_name: legalsForm.elements["legals.hoster_name"].value, + hoster_address: legalsForm.elements["legals.hoster_address"].value, + hoster_contact: legalsForm.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] || {}; + 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(values.items) === JSON.stringify(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 --- + [ + { form: infoForm, section: "info" }, + { form: socialForm, section: "social" }, + { form: menuForm, section: "menu" }, + { form: footerForm, section: "footer" }, + { form: legalsForm, section: "legals" }, + { form: buildForm, section: "build" } + ].forEach(({ form, section }) => { + if (!form) return; + form.addEventListener("input", () => updateSectionStatus(section)); + form.addEventListener("change", () => updateSectionStatus(section)); + }); + + // --- Save section handler (form submit) --- + [ + { form: infoForm, section: "info" }, + { form: socialForm, section: "social" }, + { form: menuForm, section: "menu" }, + { form: footerForm, section: "footer" }, + { form: legalsForm, section: "legals" }, + { form: buildForm, section: "build" } + ].forEach(({ form, section }) => { + if (!form) return; 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(); + // Native browser validation + 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 + // Social section: check thumbnail + if (section === "social") { + if (!thumbnailInput || !thumbnailInput.value) { + showToast("❌ Thumbnail is required.", "error"); + updateSectionStatus(section); + return; } - }; - // --- REMOVE loader for save --- - // showLoader("Saving..."); + } + // Menu section: check all menu items + 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; + } + } + // Legals section: check all paragraphs + if (section === "legals") { + updateIpParagraphsFromInputs(); + if (!ipParagraphs.length || !ipParagraphs.every(ip => ip.paragraph)) { + showToast("❌ Please fill all intellectual property paragraphs.", "error"); + updateSectionStatus(section); + return; + } + } + // Build payload for this section only + let payload = {}; + payload[section] = getSectionValues(section); + const res = await fetch("/api/site-info", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -447,10 +621,18 @@ document.addEventListener("DOMContentLoaded", () => { }); const result = await res.json(); if (result.status === "ok") { - showToast("✅ Site info saved!", "success"); + showToast("✅ Section saved!", "success"); + // Reload config for this section + 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..4d853f3 100644 --- a/src/webui/js/theme-editor.js +++ b/src/webui/js/theme-editor.js @@ -116,54 +116,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 +460,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 +486,7 @@ document.addEventListener("DOMContentLoaded", async () => { googleFonts.push(...updatedGoogleFonts); renderGoogleFonts(googleFonts); refreshFontDropdowns(); + updateSectionStatus("google-fonts", loadedConfig); }); } @@ -396,6 +516,7 @@ document.addEventListener("DOMContentLoaded", async () => { googleFonts.push(...updatedGoogleFonts); renderGoogleFonts(googleFonts); refreshFontDropdowns(); + updateSectionStatus("google-fonts", loadedConfig); } }, true); @@ -414,54 +535,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/site-info/index.html b/src/webui/site-info/index.html index 8eeebe4..9e86233 100644 --- a/src/webui/site-info/index.html +++ b/src/webui/site-info/index.html @@ -4,178 +4,204 @@ {% block content %} -

Edit Site Info

-
- - -
-

Info

-

Set the basic information for your site and SEO

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- -
-

Social

-

Set your social media links and thumbnail for link sharing

-
-
- - - - - -
- - - -
-
-
-
- -
-

Menu

-

Manage your site menu items. You can use tag combination to propose custom filters

-
-
- - -
-
-
- -
-

Footer

-

Set your copyright informations and legal link name

-
-
- - -
-
- - -
-
-
- -
-

Legals

-

Set your legal informations

-
-
- - -
-
- - -
-
- - -
-
- -
- -
-
-
- -
-

Build

-

Select a theme from the dropdown menu or add your custom theme folder

-
-
- - - - - - -

If checked, images will be converted for web and resized to fit the theme

- - -
-
-
- -
- -
-

Steps

-

Follow the steps to generate your static gallery

- +

Edit Site Info

+ + +
+
+

Info

+

+

Set the basic information for your site and SEO

+
+
+ +
-
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + +
+
+

Social

+

Set your social media links and thumbnail for link sharing

+

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

Legals

+

Set your legal informations

+

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

Build

+

Select a theme from the dropdown menu or add your custom theme folder

+

+
+
+ + + + + + +

If checked, images will be converted for web and resized to fit the theme

+ + +
+
+ +
+
+ + +
+

Steps

+

Follow the steps to generate your static gallery

+ +
-
-