'use strict'; const CFG = JSON.parse(document.getElementById('engineConfig').textContent); const CSRF_TOKEN = CFG.csrf || ''; const DAY_NAMES = ['DOM','SEG','TER','QUA','QUI','SEX','SÁB']; const MONTH_NAMES = ['JAN','FEV','MAR','ABR','MAI','JUN','JUL','AGO','SET','OUT','NOV','DEZ']; /* ── State ─────────────────────────────────────────────────── */ let items = []; // [{id, title, subtitle, date}] let nextItemId = 1; let slides = []; // arrays of items (chunked by template max) let currentSlide = 0; let currentBg = null; // gradient/solid CSS or image URL let bgIsImage = false; let blurAmount = 0; let overlaySource = 'none'; let overlayValue = ''; let overlayOpacity = 50; function esc(str) { return String(str ?? '') .replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } /* ── Dynamic items (platform content endpoint) ─────────────── */ let dynSelected = new Set((CFG.presel || []).filter(id => (CFG.dynamic || []).some(d => d.id === id))); let dynCatValue = ''; // active category filter ('' = all) // Items visible under the current category filter (selection persists across // filter changes — hidden-but-selected items stay in the post). function dynVisible() { return (CFG.dynamic || []).filter(d => !dynCatValue || (d.category || '') === dynCatValue); } // Build the category dropdown from the distinct categories present in the // dynamic items (first-seen order = the platform's order). Hidden when <2. function buildCatFilter() { const sel = document.getElementById('dynCatFilter'); const bar = document.getElementById('dynFilterBar'); if (!sel || !bar) return; const seen = new Map(); (CFG.dynamic || []).forEach(d => { const k = d.category || ''; if (k && !seen.has(k)) seen.set(k, d.category_label || k); }); if (seen.size < 2) { bar.style.display = 'none'; return; } let html = ``; seen.forEach((lbl, k) => { html += ``; }); sel.innerHTML = html; sel.value = dynCatValue; bar.style.display = ''; } function onCatFilter(v) { dynCatValue = v; renderDynList(); } function renderDynList() { const list = document.getElementById('dynList'); if (!list) return; list.innerHTML = dynVisible().map(d => { const dp = d.date_start ? dateParts(d.date_start.slice(0, 10)) : null; return ``; }).join(''); list.querySelectorAll('input[data-dyn-id]').forEach(cb => { cb.addEventListener('change', () => { cb.checked ? dynSelected.add(cb.dataset.dynId) : dynSelected.delete(cb.dataset.dynId); rebuildSlides(); }); }); } function dynToggleAll() { // Operates on the currently-visible (filtered) items only. const visible = dynVisible(); const everyOn = visible.length > 0 && visible.every(d => dynSelected.has(d.id)); visible.forEach(d => everyOn ? dynSelected.delete(d.id) : dynSelected.add(d.id)); document.getElementById('dynSelectAllBtn').textContent = everyOn ? 'Selecionar todos' : 'Limpar'; renderDynList(); rebuildSlides(); } // Selected dynamic items, normalized to the same shape as manual items. function dynamicItems() { return (CFG.dynamic || []) .filter(d => dynSelected.has(d.id)) .map(d => ({ id: 'dyn:' + d.id, title: d.title, subtitle: d.subtitle || '', date: d.date_start ? d.date_start.slice(0, 10) : '', })); } /* ── Lazy gallery browser (photo_session content, Fase 4) ──── */ // Sessions render collapsed (title + count only); the thumbnail images are // only attached on first expand, so a platform with many galleries doesn't // fetch hundreds of photos on page load. The same accordion is used in two // places: the Fundo card (mode 'bg': click = use photo as background) and // the photo-templates picker (mode 'multi': numbered multi-select). let selectedPhotos = []; // [{url, title, subtitle, date}], in selection order function initGalleryAccordion(containerId, mode) { const box = document.getElementById(containerId); if (!box) return; box.innerHTML = (CFG.dynamicPhoto || []).map(s => `
`).join(''); box.querySelectorAll('.ig-gal-head').forEach(head => { head.addEventListener('click', () => { const body = head.nextElementSibling; const opening = body.classList.contains('d-none'); if (opening && body.dataset.loaded !== '1') { const s = (CFG.dynamicPhoto || []).find(x => String(x.id) === head.dataset.session); body.innerHTML = (s ? s.media : []).map(m => `
`).join(''); body.dataset.loaded = '1'; body.querySelectorAll('.ig-photo-pick').forEach(el => { el.addEventListener('click', () => mode === 'multi' ? togglePhoto(el) : pickPhotoAsBg(el)); }); if (mode === 'multi') syncPhotoPicker(); } body.classList.toggle('d-none', !opening); const icon = head.querySelector('.bi'); if (icon) icon.className = opening ? 'bi bi-chevron-up' : 'bi bi-chevron-down'; }); }); } // Fundo card: a session photo as the post background (full-res via proxy). function pickPhotoAsBg(el) { clearBgActive(); el.classList.add('active'); currentBg = el.dataset.url; bgIsImage = true; applyStyle(); } function togglePhoto(el) { const url = el.dataset.url; const idx = selectedPhotos.findIndex(p => p.url === url); if (idx >= 0) { selectedPhotos.splice(idx, 1); } else { const session = (CFG.dynamicPhoto || []).find(s => String(s.id) === el.dataset.session); selectedPhotos.push({ url, title: session?.title || '', subtitle: session?.subtitle || '', date: session?.date || '', }); } syncPhotoPicker(); rebuildSlides(); } function syncPhotoPicker() { document.querySelectorAll('#photoPicker .ig-photo-pick').forEach(el => { const idx = selectedPhotos.findIndex(p => p.url === el.dataset.url); el.classList.toggle('active', idx >= 0); el.querySelector('.ig-photo-pick-num').textContent = idx >= 0 ? String(idx + 1) : ''; }); const cnt = document.getElementById('photoSelCount'); if (cnt) cnt.textContent = selectedPhotos.length + ' selecionada' + (selectedPhotos.length === 1 ? '' : 's'); } /* ── Manual items ──────────────────────────────────────────── */ function addItem(data) { items.push(Object.assign({ id: nextItemId++, title: '', subtitle: '', date: '' }, data || {})); renderItemList(); rebuildSlides(); } function removeItem(id) { items = items.filter(i => i.id !== id); renderItemList(); rebuildSlides(); } function onItemField(id, field, value) { const it = items.find(i => i.id === id); if (it) { it[field] = value; rebuildSlides(); } } function renderItemList() { const box = document.getElementById('itemList'); document.getElementById('itemsEmpty').style.display = items.length ? 'none' : ''; box.innerHTML = items.map(it => `
`).join(''); } function dateParts(iso) { if (!iso) return null; const d = new Date(iso + 'T12:00:00'); if (isNaN(d)) return null; return { dayName: DAY_NAMES[d.getDay()], dayNum: String(d.getDate()), month: MONTH_NAMES[d.getMonth()], year: String(d.getFullYear()), }; } /* ── Template / format ─────────────────────────────────────── */ function currentTemplate() { const slug = document.querySelector('input[name="tplPick"]:checked')?.value; return CFG.templates.find(t => t.slug === slug) || CFG.templates[0]; } function currentFormatId() { return document.querySelector('input[name="postFormat"]:checked').value; } // Export dimensions for the active format, honoring the paper orientation swap. function formatDims() { const id = currentFormatId(); const f = CFG.formats[id]; let w = f.w, h = f.h; if (f.group === 'paper' && document.getElementById('orientSwap').checked) { [w, h] = [h, w]; } return { id, w, h, group: f.group }; } function maxItemsPerSlide() { const tpl = currentTemplate(); const id = currentFormatId(); return tpl.maxItems[id] || tpl.maxItems['default'] || 1; } function isPhotoTemplate(tpl) { return (tpl.itemTypes || []).includes('photo_session'); } function toggleCard(id, show) { const el = document.getElementById(id); if (el) el.style.display = show ? '' : 'none'; } function onTemplateChange() { const tpl = currentTemplate(); const photo = isPhotoTemplate(tpl); const isGrid = tpl.slug === 'photo-grid'; // The heading/tagline inputs only apply to the list template; the single // announcement and photo templates take their caption from the item itself. const single = !tpl.multi || photo; document.getElementById('headingWrap').style.display = single ? 'none' : ''; document.getElementById('taglineWrap').style.display = single ? 'none' : ''; toggleCard('dynContentCard', !photo); toggleCard('manualContentCard', !photo); toggleCard('photoCard', photo); toggleCard('fundoCard', !isGrid); toggleCard('estiloCard', !isGrid); toggleCard('textoCard', !isGrid); rebuildSlides(); } function onFormatChange() { const { w, h, group } = formatDims(); document.getElementById('orientWrap').style.display = group === 'paper' ? '' : 'none'; const post = document.getElementById('postPreview'); post.style.aspectRatio = w + ' / ' + h; document.getElementById('previewWrapper').classList.toggle('is-wide', w > h); rebuildSlides(); } /* ── Slides (overflow pagination) ──────────────────────────── */ function rebuildSlides() { const tpl = currentTemplate(); const usable = isPhotoTemplate(tpl) ? selectedPhotos : dynamicItems().concat(items.filter(i => i.title.trim() !== '')); slides = SocialEngine.chunk(usable, maxItemsPerSlide()); if (!slides.length) slides = [[]]; if (currentSlide >= slides.length) currentSlide = slides.length - 1; renderSlideNav(); updateSlideVisuals(); } function renderSlideNav() { const bar = document.getElementById('slideToolbar'); const dots = document.getElementById('slideDots'); bar.style.display = slides.length > 1 ? '' : 'none'; dots.innerHTML = ''; slides.forEach((_, i) => { const dot = document.createElement('button'); dot.type = 'button'; dot.className = 'ig-slide-dot' + (i === currentSlide ? ' active' : ''); dot.addEventListener('click', () => { currentSlide = i; renderSlideNav(); updateSlideVisuals(); }); dots.appendChild(dot); }); } function slidePrev() { if (currentSlide > 0) { currentSlide--; renderSlideNav(); updateSlideVisuals(); } } function slideNext() { if (currentSlide < slides.length - 1) { currentSlide++; renderSlideNav(); updateSlideVisuals(); } } /* ── Slide rendering (templates) ───────────────────────────── */ function logoRowHtml() { if (!CFG.logo || !document.getElementById('showLogo')?.checked) return ''; return `
`; } function itemCardHtml(it) { const dp = dateParts(it.date); return `
${dp ? `
${dp.dayName}
${dp.dayNum}
${dp.month}
` : ''}
${esc(it.title)}
${it.subtitle ? `
${esc(it.subtitle)}
` : ''}
`; } function renderSlide() { const content = document.getElementById('postContent'); const tpl = currentTemplate(); const slide = slides[currentSlide] || []; const cta = document.getElementById('ctaText').value.trim(); let html = logoRowHtml(); if (tpl.slug === 'photo-grid') { content.innerHTML = ''; return; } if (tpl.slug === 'photo-highlight') { const it = slide[0]; if (!it) { html += '

Seleciona uma foto à esquerda

'; } else { const dp = dateParts(it.date); html += `
${dp ? `

${dp.dayName} · ${dp.dayNum} ${dp.month} ${dp.year}

` : ''}

${esc(it.title)}

${it.subtitle ? `

${esc(it.subtitle)}

` : ''}
`; } } else if (tpl.slug === 'single-announcement') { const it = slide[0]; if (!it) { html += '

Adiciona um item à esquerda

'; } else { const dp = dateParts(it.date); html += `
${dp ? `

${dp.dayName} · ${dp.dayNum} ${dp.month} ${dp.year}

` : ''}

${esc(it.title)}

${it.subtitle ? `

${esc(it.subtitle)}

` : ''}
`; } } else { // event-list const tagline = document.getElementById('tagline').value.trim(); const heading = document.getElementById('headingText').value.trim(); html += `
${tagline ? `

${esc(tagline)}

` : ''} ${heading ? `

${esc(heading)}

` : ''}
${slide.length ? slide.map(itemCardHtml).join('') : '

Adiciona itens à esquerda

'}
`; } if (cta) html += `
${esc(cta)}
`; content.innerHTML = html; } /* ── Background / style ────────────────────────────────────── */ function luminance(hex) { const m = /^#([0-9a-f]{6})$/i.exec(hex || ''); if (!m) return 0; const n = parseInt(m[1], 16); return (0.2126 * (n >> 16) + 0.7152 * ((n >> 8) & 255) + 0.0722 * (n & 255)) / 255; } // The displayed background: the user's pick, except for photo-highlight where // the current slide's selected photo always wins. function effectiveBg() { const tpl = currentTemplate(); if (tpl.slug === 'photo-highlight') { const it = (slides[currentSlide] || [])[0]; return { bg: it ? it.url : null, isImage: !!it }; } return { bg: currentBg, isImage: bgIsImage }; } function applyStyle() { const post = document.getElementById('postPreview'); const bg = document.getElementById('photoBg'); const { bg: effBg, isImage: effIsImage } = effectiveBg(); if (effIsImage && effBg) { bg.style.display = ''; bg.style.backgroundImage = "url('" + effBg.replace(/'/g, "\\'") + "')"; bg.style.filter = blurAmount > 0 ? `blur(${blurAmount}px) brightness(.85)` : ''; bg.style.transform = blurAmount > 0 ? `scale(${1 + blurAmount / 280})` : ''; post.style.background = ''; post.classList.add('is-photo'); post.classList.remove('is-light'); } else { bg.style.display = 'none'; bg.style.backgroundImage = ''; bg.style.filter = ''; bg.style.transform = ''; post.style.background = effBg || ''; post.classList.remove('is-photo'); // Solid light colors flip the text palette + drop the dark wash. post.classList.toggle('is-light', !!effBg && luminance(effBg) > 0.72); } document.getElementById('blurWrap').style.display = effIsImage ? '' : 'none'; const ov = document.getElementById('customOverlay'); if ((overlaySource === 'gradient' || overlaySource === 'solid') && overlayValue) { ov.style.background = overlayValue; ov.style.opacity = (overlayOpacity / 100).toString(); } else { ov.style.background = ''; ov.style.opacity = '0'; } post.classList.toggle('has-watermark', !!document.getElementById('showWatermark')?.checked); const content = document.getElementById('postContent'); content.dataset.alignH = document.querySelector('input[name="alignH"]:checked').value; content.dataset.alignV = document.querySelector('input[name="alignV"]:checked').value; renderSlide(); // logo toggle lives inside slide markup } // Switches the canvas between the photo-grid layout and the normal // background+content layout, then applies the current style. function updateSlideVisuals() { const tpl = currentTemplate(); const grid = document.getElementById('photoGrid'); const content = document.getElementById('postContent'); const overlay = document.querySelector('.ig-overlay'); if (tpl.slug === 'photo-grid') { const slide = slides[currentSlide] || []; grid.style.display = 'grid'; content.style.display = 'none'; overlay.style.display = 'none'; document.getElementById('photoBg').style.display = 'none'; document.getElementById('customOverlay').style.opacity = '0'; const cols = slide.length <= 1 ? 1 : (slide.length <= 4 ? 2 : 3); grid.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; grid.innerHTML = slide.length ? slide.map(p => `
`).join('') : '
Seleciona fotos à esquerda
'; return; } grid.style.display = 'none'; content.style.display = ''; overlay.style.display = ''; applyStyle(); } function onBlurChange() { blurAmount = parseInt(document.getElementById('blurRange').value, 10) || 0; document.getElementById('blurValue').textContent = blurAmount + ' px'; applyStyle(); } function onAlignChange() { applyStyle(); } function onOverlayChange() { overlaySource = document.querySelector('input[name="overlaySource"]:checked').value; const cur = overlayValue || ''; if (overlaySource === 'gradient' && !cur.startsWith('linear-gradient') && !cur.startsWith('radial-gradient')) { overlayValue = CFG.presets.gradients[0].css; } else if (overlaySource === 'solid' && (cur === '' || cur.includes('gradient'))) { overlayValue = CFG.presets.solids[0].css; } const hasPreset = overlaySource !== 'none'; document.getElementById('ovGradientWrap').style.display = overlaySource === 'gradient' ? '' : 'none'; document.getElementById('ovSolidWrap').style.display = overlaySource === 'solid' ? '' : 'none'; document.getElementById('ovOpacityWrap').style.display = hasPreset ? '' : 'none'; markActive('#ovGradientList .ig-preset, #ovSolidList .ig-preset', overlayValue); applyStyle(); } function onOverlayOpacityChange() { overlayOpacity = parseInt(document.getElementById('ovOpacity').value, 10) || 0; document.getElementById('ovOpacityValue').textContent = overlayOpacity + ' %'; applyStyle(); } function markActive(selector, css) { document.querySelectorAll(selector).forEach(b => b.classList.toggle('active', b.dataset.bg === css)); } function clearBgActive() { document.querySelectorAll('#gradientList .ig-preset, #solidList .ig-preset, .ig-img-preset, #bgGalleryList .ig-photo-pick') .forEach(b => b.classList.remove('active')); } function renderPresets(containerId, presets, onPick) { const el = document.getElementById(containerId); el.innerHTML = presets.map(p => ` `).join(''); el.querySelectorAll('.ig-preset').forEach(b => b.addEventListener('click', () => onPick(b))); } function pickBgPreset(btn) { clearBgActive(); btn.classList.add('active'); currentBg = btn.dataset.bg; bgIsImage = false; applyStyle(); } function pickOverlayPreset(btn) { overlayValue = btn.dataset.bg; markActive('#ovGradientList .ig-preset, #ovSolidList .ig-preset', overlayValue); applyStyle(); } /* Image backgrounds (gallery + local file) */ function bindImgPreset(btn) { btn.addEventListener('click', () => { clearBgActive(); btn.classList.add('active'); currentBg = btn.dataset.img; bgIsImage = true; applyStyle(); }); } document.getElementById('imageInput').addEventListener('change', function () { if (this.files[0]) readLocalImage(this.files[0]); }); const dropZone = document.getElementById('dropZone'); ['dragenter','dragover'].forEach(ev => dropZone.addEventListener(ev, e => { e.preventDefault(); dropZone.classList.add('dragging'); })); ['dragleave','drop'].forEach(ev => dropZone.addEventListener(ev, () => dropZone.classList.remove('dragging'))); dropZone.addEventListener('drop', e => { e.preventDefault(); const file = e.dataTransfer.files[0]; if (file && file.type.startsWith('image/')) readLocalImage(file); }); function readLocalImage(file) { const reader = new FileReader(); reader.onload = e => { clearBgActive(); currentBg = e.target.result; bgIsImage = true; applyStyle(); }; reader.readAsDataURL(file); } /* Gallery upload / delete */ document.getElementById('bgUploadInput').addEventListener('change', function () { const file = this.files[0]; if (!file) return; const icon = document.getElementById('bgUploadIcon'); icon.className = 'bi bi-arrow-clockwise ig-spin'; const fd = new FormData(); fd.append('action', 'upload_bg'); fd.append('app', CFG.app); fd.append('bg_image', file); fetch(window.location.pathname + '?app=' + encodeURIComponent(CFG.app), { method: 'POST', headers: { 'X-CSRF-Token': CSRF_TOKEN }, body: fd, }) .then(r => r.json()) .then(data => { if (data.status !== 'success') { alert(data.message); return; } document.getElementById('noPresetsHint')?.remove(); const list = document.getElementById('bgPresetList'); const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'ig-img-preset'; btn.dataset.img = data.url; btn.dataset.id = data.id; btn.style.backgroundImage = "url('" + data.url + "')"; btn.innerHTML = '×'; bindImgPreset(btn); list.appendChild(btn); const count = document.getElementById('bgCount'); count.textContent = (parseInt(count.textContent, 10) || 0) + 1; btn.click(); }) .catch(() => alert('Erro ao guardar a imagem.')) .finally(() => { icon.className = 'bi bi-plus-circle'; this.value = ''; }); }); function deleteBg(ev, id) { ev.stopPropagation(); if (!confirm('Eliminar este fundo da galeria?')) return; const fd = new FormData(); fd.append('action', 'delete_bg'); fd.append('app', CFG.app); fd.append('bg_id', id); fetch(window.location.pathname + '?app=' + encodeURIComponent(CFG.app), { method: 'POST', headers: { 'X-CSRF-Token': CSRF_TOKEN }, body: fd, }).then(() => { document.querySelector('.ig-img-preset[data-id="' + id + '"]')?.remove(); const count = document.getElementById('bgCount'); count.textContent = Math.max(0, (parseInt(count.textContent, 10) || 1) - 1); }); } /* ── Export ────────────────────────────────────────────────── */ function exportFileName(n) { const tpl = currentTemplate().slug; const date = new Date().toISOString().slice(0, 10); return `${CFG.app}_${tpl}_${date}_${n}.jpg`; } function blurOpts() { const { bg, isImage } = effectiveBg(); if (!isImage || !(blurAmount > 0)) return null; return { bgEl: document.getElementById('photoBg'), photoUrl: bg, amount: blurAmount }; } async function exportCurrent(btn) { const { w } = formatDims(); btn.disabled = true; try { const blob = await SocialEngine.renderBlob(document.getElementById('postPreview'), w, { blur: blurOpts() }); SocialEngine.saveBlob(blob, exportFileName(currentSlide + 1)); } catch (err) { console.error(err); alert('Erro ao gerar a imagem: ' + (err?.message || err)); } finally { btn.disabled = false; } } async function exportAll(btn) { const { w } = formatDims(); const original = currentSlide; btn.disabled = true; try { const files = []; for (let i = 0; i < slides.length; i++) { currentSlide = i; renderSlideNav(); updateSlideVisuals(); // Give the DOM a frame to settle before capturing. await new Promise(r => requestAnimationFrame(() => setTimeout(r, 60))); const blob = await SocialEngine.renderBlob(document.getElementById('postPreview'), w, { blur: blurOpts() }); files.push({ blob, name: exportFileName(i + 1) }); } const date = new Date().toISOString().slice(0, 10); await SocialEngine.saveAll(files, `${CFG.app}_${currentTemplate().slug}_${date}.zip`); } catch (err) { console.error(err); alert('Erro ao exportar: ' + (err?.message || err)); } finally { currentSlide = original; renderSlideNav(); updateSlideVisuals(); btn.disabled = false; } } /* ── Screenshot mode ───────────────────────────────────────── */ function enterScreenshotMode() { const { w, h } = formatDims(); const dest = document.getElementById('screenshotPost'); const vw = window.innerWidth, vh = window.innerHeight; if (vh * w / h <= vw) { dest.style.height = '100dvh'; dest.style.width = `calc(100dvh * ${w} / ${h})`; } else { dest.style.width = '100vw'; dest.style.height = `calc(100vw * ${h} / ${w})`; } dest.innerHTML = ''; const clone = document.getElementById('postPreview').cloneNode(true); clone.removeAttribute('id'); clone.style.aspectRatio = w + ' / ' + h; dest.appendChild(clone); const overlay = document.getElementById('screenshotOverlay'); const closeBtn = document.getElementById('screenshotCloseBtn'); overlay.style.display = 'flex'; document.body.style.overflow = 'hidden'; closeBtn.style.opacity = '1'; clearTimeout(window._scTimer); window._scTimer = setTimeout(() => { closeBtn.style.opacity = '0'; }, 2000); } function exitScreenshotMode() { document.getElementById('screenshotOverlay').style.display = 'none'; document.body.style.overflow = ''; } document.getElementById('screenshotOverlay').addEventListener('click', function (e) { if (e.target === this) exitScreenshotMode(); }); /* ── Init ──────────────────────────────────────────────────── */ renderPresets('gradientList', CFG.presets.gradients, pickBgPreset); renderPresets('solidList', CFG.presets.solids, pickBgPreset); renderPresets('ovGradientList', CFG.presets.gradients, pickOverlayPreset); renderPresets('ovSolidList', CFG.presets.solids, pickOverlayPreset); document.querySelectorAll('#bgPresetList .ig-img-preset').forEach(bindImgPreset); // Default background: first brand gradient. const firstGrad = document.querySelector('#gradientList .ig-preset'); if (firstGrad) { firstGrad.classList.add('active'); currentBg = firstGrad.dataset.bg; } buildCatFilter(); renderDynList(); initGalleryAccordion('bgGalleryList', 'bg'); initGalleryAccordion('photoPicker', 'multi'); if (!(CFG.dynamic || []).length) addItem(); // empty manual row only without dynamic source onTemplateChange(); // sets heading visibility + builds slides onFormatChange(); // sets aspect ratio applyStyle();