From 1d1d7d8304b20d9f7023a8d702445f2f262a4e6b Mon Sep 17 00:00:00 2001 From: Djeex Date: Thu, 4 Sep 2025 12:22:52 +0200 Subject: [PATCH] Refactored Galley Editor --- src/webui/js/gallery-editor.js | 239 ++++++++++----------------------- 1 file changed, 73 insertions(+), 166 deletions(-) diff --git a/src/webui/js/gallery-editor.js b/src/webui/js/gallery-editor.js index 83b4bea..e7ebcd6 100644 --- a/src/webui/js/gallery-editor.js +++ b/src/webui/js/gallery-editor.js @@ -1,19 +1,14 @@ -// --- Arrays to store gallery and hero images --- let galleryImages = []; let heroImages = []; -let allTags = []; // global tag list +let allTags = []; let showOnlyUntagged = false; // --- Fade-in helper --- function applyFadeInImages(container) { - const fadeImages = container.querySelectorAll("img.fade-in-img"); - fadeImages.forEach(img => { - const onLoad = () => { - img.classList.add("loaded"); - }; - if (img.complete && img.naturalHeight !== 0) { - onLoad(); - } else { + container.querySelectorAll("img.fade-in-img").forEach(img => { + const onLoad = () => img.classList.add("loaded"); + if (img.complete && img.naturalHeight !== 0) onLoad(); + else { img.addEventListener("load", onLoad, { once: true }); img.addEventListener("error", () => { console.warn("Image failed to load:", img.dataset?.src || img.src); @@ -25,13 +20,10 @@ function applyFadeInImages(container) { // --- Load images from server on page load --- async function loadData() { try { - const galleryRes = await fetch('/api/gallery'); - galleryImages = await galleryRes.json(); + galleryImages = await (await fetch('/api/gallery')).json(); updateAllTags(); renderGallery(); - - const heroRes = await fetch('/api/hero'); - heroImages = await heroRes.json(); + heroImages = await (await fetch('/api/hero')).json(); renderHero(); } catch(err) { console.error(err); @@ -43,31 +35,44 @@ async function loadData() { function updateAllTags() { allTags = []; galleryImages.forEach(img => { - if (img.tags) img.tags.forEach(t => { + (img.tags || []).forEach(t => { if (!allTags.includes(t)) allTags.push(t); }); }); } +// --- Helper: update count and button visibility --- +function updateCountAndButtons(prefix, count) { + const countTop = document.getElementById(`${prefix}-count`); + const countBottom = document.getElementById(`${prefix}-count-bottom`); + if (countTop) countTop.innerHTML = `

${count} photos

`; + if (countBottom) countBottom.innerHTML = `

${count} photos

`; + + const removeAllBtn = document.getElementById(`remove-all-${prefix}`); + const removeAllBtnBottom = document.getElementById(`remove-all-${prefix}-bottom`); + if (removeAllBtn) removeAllBtn.style.display = count > 0 ? 'inline-block' : 'none'; + if (removeAllBtnBottom) removeAllBtnBottom.style.display = count > 0 ? 'inline-block' : 'none'; + + const bottomUpload = document.getElementById(`bottom-${prefix}-upload`); + if (bottomUpload) bottomUpload.style.display = count > 0 ? 'flex' : 'none'; +} + // --- Render gallery images with tags and delete buttons --- function renderGallery() { const container = document.getElementById('gallery'); container.innerHTML = ''; - - // Filter images if toggle is on - let imagesToShow = galleryImages; - if (showOnlyUntagged) { - imagesToShow = galleryImages.filter(img => !img.tags || img.tags.length === 0); - } + let imagesToShow = showOnlyUntagged + ? galleryImages.filter(img => !img.tags || img.tags.length === 0) + : galleryImages; imagesToShow.forEach((img) => { - const i = galleryImages.indexOf(img); // Use real index for tag/delete actions + const i = galleryImages.indexOf(img); const div = document.createElement('div'); div.className = 'photo flex-item flex-column'; div.innerHTML = ` -
- -
+
+ +
@@ -77,41 +82,10 @@ function renderGallery() {
`; container.appendChild(div); - renderTags(i, img.tags || []); }); - // Update gallery count (top) - const galleryCount = document.getElementById('gallery-count'); - if (galleryCount) { - galleryCount.innerHTML = `

${imagesToShow.length} photos

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

${imagesToShow.length} photos

`; - } - - // Show/hide Remove All button (top) - const removeAllBtn = document.getElementById('remove-all-gallery'); - if (removeAllBtn) { - removeAllBtn.style.display = imagesToShow.length > 0 ? 'inline-block' : 'none'; - } - - // Show/hide bottom upload row - const bottomGalleryUpload = document.getElementById('bottom-gallery-upload'); - if (bottomGalleryUpload) { - bottomGalleryUpload.style.display = imagesToShow.length > 0 ? 'flex' : 'none'; - } - - // Show/hide Remove All button (bottom) - const removeAllBtnBottom = document.getElementById('remove-all-gallery-bottom'); - if (removeAllBtnBottom) { - removeAllBtnBottom.style.display = imagesToShow.length > 0 ? 'inline-block' : 'none'; - } - - // Fade-in effect for loaded images + updateCountAndButtons('gallery', imagesToShow.length); applyFadeInImages(container); } @@ -119,7 +93,6 @@ function renderGallery() { function renderTags(imgIndex, tags) { const tagsDisplay = document.querySelector(`.tags-display[data-index="${imgIndex}"]`); const inputContainer = document.querySelector(`.tag-input[data-index="${imgIndex}"]`); - tagsDisplay.innerHTML = ''; inputContainer.innerHTML = ''; @@ -127,7 +100,6 @@ function renderTags(imgIndex, tags) { const span = document.createElement('span'); span.className = 'tag'; span.textContent = tag; - const remove = document.createElement('span'); remove.className = 'remove-tag'; remove.textContent = '×'; @@ -136,7 +108,6 @@ function renderTags(imgIndex, tags) { updateTags(imgIndex, tags); renderTags(imgIndex, tags); }; - span.appendChild(remove); tagsDisplay.appendChild(span); }); @@ -146,11 +117,10 @@ function renderTags(imgIndex, tags) { input.placeholder = 'Add tag...'; inputContainer.appendChild(input); - // --- Validate button --- const validateBtn = document.createElement('button'); validateBtn.textContent = '✔️'; validateBtn.className = 'validate-tag-btn'; - validateBtn.style.display = 'none'; // hidden by default + validateBtn.style.display = 'none'; validateBtn.style.marginLeft = '4px'; inputContainer.appendChild(validateBtn); @@ -170,30 +140,20 @@ function renderTags(imgIndex, tags) { const updateSuggestions = () => { const value = input.value.toLowerCase(); - const allTagsFlat = galleryImages.flatMap(img => img.tags || []); const tagCount = {}; allTagsFlat.forEach(t => tagCount[t] = (tagCount[t] || 0) + 1); - - const allTagsSorted = Object.keys(tagCount) - .sort((a, b) => tagCount[b] - tagCount[a]); - + const allTagsSorted = Object.keys(tagCount).sort((a, b) => tagCount[b] - tagCount[a]); const suggestions = allTagsSorted.filter(t => t.toLowerCase().startsWith(value) && !tags.includes(t)); - suggestionBox.innerHTML = ''; selectedIndex = -1; - if (suggestions.length) { suggestionBox.style.display = 'block'; suggestions.forEach((s, idx) => { const li = document.createElement('li'); li.style.fontStyle = 'italic'; li.style.textAlign = 'left'; - - const boldPart = `${s.substring(0, input.value.length)}`; - const rest = s.substring(input.value.length); - li.innerHTML = boldPart + rest; - + li.innerHTML = `${s.substring(0, input.value.length)}${s.substring(input.value.length)}`; li.addEventListener('mousedown', (e) => { e.preventDefault(); addTag(s); @@ -201,7 +161,6 @@ function renderTags(imgIndex, tags) { input.focus(); updateSuggestions(); }); - li.onmouseover = () => selectedIndex = idx; suggestionBox.appendChild(li); }); @@ -218,7 +177,6 @@ function renderTags(imgIndex, tags) { updateSuggestions(); validateBtn.style.display = input.value.trim() ? 'inline-block' : 'none'; }); - input.addEventListener('keydown', (e) => { const items = suggestionBox.querySelectorAll('li'); if (e.key === 'ArrowDown') { @@ -249,26 +207,22 @@ function renderTags(imgIndex, tags) { validateBtn.style.display = 'none'; } }); - input.addEventListener('blur', () => { suggestionBox.style.display = 'none'; input.value = ''; validateBtn.style.display = 'none'; }); - - // --- Validate button action --- - validateBtn.onclick = () => { + validateBtn.addEventListener('mousedown', (e) => { + e.preventDefault(); if (input.value.trim()) { addTag(input.value.trim()); input.value = ''; updateSuggestions(); validateBtn.style.display = 'none'; } - }; + }); updateSuggestions(); - if (!input.value.trim()) { - suggestionBox.style.display = 'none'; - } + if (!input.value.trim()) suggestionBox.style.display = 'none'; } // --- Update tags in galleryImages array --- @@ -296,38 +250,7 @@ function renderHero() { `; container.appendChild(div); }); - - // 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'; - } - - // Fade-in effect for loaded images + updateCountAndButtons('hero', heroImages.length); applyFadeInImages(container); } @@ -374,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'; } @@ -413,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(); } @@ -503,41 +415,36 @@ 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; - // --- Radio toggle for gallery filter --- + // Gallery filter radios const showAllRadio = document.getElementById('show-all-radio'); const showUntaggedRadio = document.getElementById('show-untagged-radio'); - - if (showAllRadio && showUntaggedRadio) { - showAllRadio.addEventListener('change', () => { - if (showAllRadio.checked) { - showOnlyUntagged = false; - renderGallery(); - } - }); - showUntaggedRadio.addEventListener('change', () => { - if (showUntaggedRadio.checked) { - showOnlyUntagged = true; - renderGallery(); - } - }); - } + 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 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'); + [ + ['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 --- loadData(); \ No newline at end of file