shared/platform-tools was a git submodule. On this branch it is a plain
directory of tracked files, frozen at ea3616abae30106df283e73a3e2f9e5bb0e6ae90
— the same commit fotografia.andmore.pt pins. Contents are byte-identical to
that tree, verified blob by blob.
The submodule caused every deployment failure this site has had:
- .gitmodules named no `branch`, so the update fell back to
refs/remotes/origin/HEAD — a ref a plain server-side clone never writes.
"Atualizar submódulo" failed with "Unable to find refs/remotes/origin/HEAD
revision in submodule path".
- Its URL was http://, which the origin answers with 523.
- This install is deployed by uploading files, and a submodule is invisible
to that. The tree moved forward while shared/platform-tools stayed on the
old pin, config/functions_secrets.php died on a missing compat_secrets.php,
and the whole site — public pages included — went down.
Each of those was separately fixable. Together they mean a second repository
nobody can see from the deployment path, gating whether the first one boots. A
plain directory deploys like every other file and cannot fall out of step.
The cost is that platform-tools updates no longer arrive by bumping a pin; they
are a copy-over-and-commit. shared/platform-tools/VENDORED.md records the
upstream SHA, the procedure, and which four files differ from upstream in line
endings only (social/*.js, .css, functions_publications.php — CRLF upstream,
normalised to LF here; content otherwise identical).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
746 lines
31 KiB
JavaScript
746 lines
31 KiB
JavaScript
'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,'>').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 = `<option value="">Todos · ${esc(CFG.categoryLabel || 'Categoria')}</option>`;
|
|
seen.forEach((lbl, k) => { html += `<option value="${esc(k)}">${esc(lbl)}</option>`; });
|
|
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 `<label class="list-group-item list-group-item-action d-flex align-items-center gap-3 py-2" style="cursor:pointer;">
|
|
<input type="checkbox" class="form-check-input flex-shrink-0 mt-0" data-dyn-id="${esc(d.id)}"
|
|
${dynSelected.has(d.id) ? 'checked' : ''}>
|
|
<div class="flex-grow-1 min-w-0">
|
|
<div class="small fw-medium text-truncate">${esc(d.title)}</div>
|
|
<div class="text-muted" style="font-size:.78rem;">
|
|
${dp ? `${dp.dayName}, ${dp.dayNum} ${dp.month}` : ''}${d.subtitle ? ` · ${esc(d.subtitle)}` : ''}
|
|
</div>
|
|
</div>
|
|
</label>`;
|
|
}).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 => `
|
|
<div class="border rounded mb-1">
|
|
<button type="button" class="ig-gal-head" data-session="${esc(s.id)}">
|
|
<span class="text-truncate">${esc(s.title)}${s.date ? ' · ' + esc(s.date) : ''}</span>
|
|
<span class="text-muted flex-shrink-0">${s.media.length} <i class="bi bi-chevron-down"></i></span>
|
|
</button>
|
|
<div class="ig-gal-body d-flex gap-2 flex-wrap p-2 d-none" data-loaded="0"></div>
|
|
</div>`).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 => `
|
|
<div class="ig-photo-pick" data-session="${esc(s.id)}" data-url="${esc(m.url)}"
|
|
style="background-image:url('${esc(m.thumb)}')">
|
|
<span class="ig-photo-pick-num"></span>
|
|
</div>`).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 => `
|
|
<div class="border rounded p-2">
|
|
<div class="d-flex gap-2 mb-1">
|
|
<input type="text" class="form-control form-control-sm" placeholder="Título *"
|
|
value="${esc(it.title)}" oninput="onItemField(${it.id},'title',this.value)">
|
|
<button type="button" class="btn btn-sm btn-outline-danger py-0 px-2"
|
|
onclick="removeItem(${it.id})" title="Remover"><i class="bi bi-x-lg"></i></button>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<input type="text" class="form-control form-control-sm" placeholder="Subtítulo / local"
|
|
value="${esc(it.subtitle)}" oninput="onItemField(${it.id},'subtitle',this.value)">
|
|
<input type="date" class="form-control form-control-sm" style="max-width:150px;"
|
|
value="${esc(it.date)}" oninput="onItemField(${it.id},'date',this.value)">
|
|
</div>
|
|
</div>`).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 `<div class="ig-logo-row"><img src="${esc(CFG.logo)}" alt=""></div>`;
|
|
}
|
|
|
|
function itemCardHtml(it) {
|
|
const dp = dateParts(it.date);
|
|
return `<div class="ig-session-card">
|
|
${dp ? `<div class="ig-date-block">
|
|
<div class="ig-date-dayname">${dp.dayName}</div>
|
|
<div class="ig-date-num">${dp.dayNum}</div>
|
|
<div class="ig-date-month">${dp.month}</div>
|
|
</div>
|
|
<div class="ig-card-divider"></div>` : ''}
|
|
<div class="ig-session-info">
|
|
<div class="ig-session-type">${esc(it.title)}</div>
|
|
${it.subtitle ? `<div class="ig-session-avail">${esc(it.subtitle)}</div>` : ''}
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
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 += '<p class="ig-empty-msg">Seleciona uma foto à esquerda</p>';
|
|
} else {
|
|
const dp = dateParts(it.date);
|
|
html += `<div class="ig-announce">
|
|
${dp ? `<p class="ig-announce-date">${dp.dayName} · ${dp.dayNum} ${dp.month} ${dp.year}</p>` : ''}
|
|
<h2 class="ig-announce-title">${esc(it.title)}</h2>
|
|
${it.subtitle ? `<p class="ig-announce-subtitle">${esc(it.subtitle)}</p>` : ''}
|
|
<div class="ig-heading-divider"></div>
|
|
</div>`;
|
|
}
|
|
} else if (tpl.slug === 'single-announcement') {
|
|
const it = slide[0];
|
|
if (!it) {
|
|
html += '<p class="ig-empty-msg">Adiciona um item à esquerda</p>';
|
|
} else {
|
|
const dp = dateParts(it.date);
|
|
html += `<div class="ig-announce">
|
|
${dp ? `<p class="ig-announce-date">${dp.dayName} · ${dp.dayNum} ${dp.month} ${dp.year}</p>` : ''}
|
|
<h2 class="ig-announce-title">${esc(it.title)}</h2>
|
|
${it.subtitle ? `<p class="ig-announce-subtitle">${esc(it.subtitle)}</p>` : ''}
|
|
<div class="ig-heading-divider"></div>
|
|
</div>`;
|
|
}
|
|
} else { // event-list
|
|
const tagline = document.getElementById('tagline').value.trim();
|
|
const heading = document.getElementById('headingText').value.trim();
|
|
html += `<div class="ig-title-block">
|
|
${tagline ? `<p class="ig-tagline">${esc(tagline)}</p>` : ''}
|
|
${heading ? `<h2 class="ig-heading">${esc(heading)}</h2><div class="ig-heading-divider"></div>` : ''}
|
|
</div>
|
|
<div class="ig-sessions">
|
|
${slide.length ? slide.map(itemCardHtml).join('') : '<p class="ig-empty-msg">Adiciona itens à esquerda</p>'}
|
|
</div>`;
|
|
}
|
|
|
|
if (cta) html += `<div class="ig-cta-row"><span class="ig-cta">${esc(cta)}</span></div>`;
|
|
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 => `<div class="ig-grid-cell" style="background-image:url('${esc(p.url)}')"></div>`).join('')
|
|
: '<div class="ig-grid-cell ig-grid-empty">Seleciona fotos à esquerda</div>';
|
|
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 => `
|
|
<button type="button" class="ig-preset" data-bg="${esc(p.css)}"
|
|
style="background:${esc(p.css)}" title="${esc(p.name)}"></button>`).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 = '<span class="ig-img-del" title="Eliminar" onclick="deleteBg(event,' + data.id + ')">×</span>';
|
|
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();
|