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>
160 lines
6.7 KiB
JavaScript
160 lines
6.7 KiB
JavaScript
/* Social-post export engine — generalization of the pipeline proven in
|
|
fotografia.andmore.pt/views/sessions/open_spots_ig.php.
|
|
|
|
Responsibilities: render a live-preview element to a high-resolution JPG
|
|
via html2canvas, pre-baking CSS blur (html2canvas can't render
|
|
`filter: blur()`), waiting for webfonts, and bundling multi-slide exports
|
|
into a ZIP (JSZip). Pure mechanism — all template/DOM knowledge lives in
|
|
the page that uses it. */
|
|
|
|
window.SocialEngine = (function () {
|
|
'use strict';
|
|
|
|
function chunk(arr, size) {
|
|
size = Math.max(1, size | 0);
|
|
var out = [];
|
|
for (var i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
|
|
return out;
|
|
}
|
|
|
|
/* html2canvas does not render CSS `filter: blur(...)`. Pre-bake the blur
|
|
into the photo with Canvas 2D, swap the bg-image to that bitmap, drop
|
|
the inline filter/transform, and return a restore() callback. The blur
|
|
radius is multiplied by the export scale so the upscaled bitmap matches
|
|
what's on screen. Resolves null when there is nothing to bake. */
|
|
function bakeBlur(bgEl, photoUrl, blurAmount, scale) {
|
|
return new Promise(function (resolve) {
|
|
if (!bgEl || !photoUrl || !(blurAmount > 0)) return resolve(null);
|
|
|
|
var rect = bgEl.getBoundingClientRect();
|
|
var W = Math.max(1, Math.round(rect.width * scale));
|
|
var H = Math.max(1, Math.round(rect.height * scale));
|
|
var blurPx = blurAmount * scale;
|
|
|
|
var img = new Image();
|
|
// Photos go through the same-origin media proxy (or are local hub
|
|
// uploads), so no crossOrigin attribute is needed and the canvas
|
|
// stays untainted.
|
|
img.onload = function () {
|
|
try {
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = W;
|
|
canvas.height = H;
|
|
var ctx = canvas.getContext('2d');
|
|
ctx.filter = 'blur(' + blurPx + 'px) brightness(0.85)';
|
|
|
|
// Draw slightly larger than the canvas so the soft edges
|
|
// don't reveal blank pixels — same trick as the CSS scale().
|
|
var overscan = 1.1 + (blurAmount / 200);
|
|
var ratio = Math.max(W / img.width, H / img.height) * overscan;
|
|
var dw = img.width * ratio;
|
|
var dh = img.height * ratio;
|
|
ctx.drawImage(img, (W - dw) / 2, (H - dh) / 2, dw, dh);
|
|
|
|
var dataUrl = canvas.toDataURL('image/jpeg', 0.9);
|
|
var original = {
|
|
backgroundImage: bgEl.style.backgroundImage,
|
|
filter: bgEl.style.filter,
|
|
transform: bgEl.style.transform,
|
|
};
|
|
bgEl.style.backgroundImage = "url('" + dataUrl + "')";
|
|
bgEl.style.filter = 'none';
|
|
bgEl.style.transform = 'none';
|
|
|
|
resolve(function () {
|
|
bgEl.style.backgroundImage = original.backgroundImage;
|
|
bgEl.style.filter = original.filter;
|
|
bgEl.style.transform = original.transform;
|
|
});
|
|
} catch (e) {
|
|
console.warn('Blur bake failed; exporting without baked blur.', e);
|
|
resolve(null);
|
|
}
|
|
};
|
|
img.onerror = function () {
|
|
console.warn('Failed to load photo for blur bake; exporting without baked blur.');
|
|
resolve(null);
|
|
};
|
|
img.src = photoUrl;
|
|
});
|
|
}
|
|
|
|
/* Render `el` (the live preview, at its on-screen size — its CSS uses
|
|
viewport-relative clamp()s, so rendering at another width would re-flow)
|
|
and let html2canvas upscale the pixel buffer to `targetW` via `scale`.
|
|
opts.blur = { bgEl, photoUrl, amount } enables the bake step.
|
|
Returns a JPEG Blob. */
|
|
async function renderBlob(el, targetW, opts) {
|
|
opts = opts || {};
|
|
if (typeof html2canvas !== 'function') {
|
|
throw new Error('A biblioteca de exportação ainda está a carregar. Tenta novamente em segundos.');
|
|
}
|
|
var rect = el.getBoundingClientRect();
|
|
var actualW = rect.width || el.offsetWidth || targetW;
|
|
var actualH = rect.height || el.offsetHeight || targetW;
|
|
var scale = actualW > 0 ? (targetW / actualW) : 1;
|
|
|
|
if (document.fonts && document.fonts.ready) {
|
|
await document.fonts.ready;
|
|
}
|
|
var restore = null;
|
|
if (opts.blur) {
|
|
restore = await bakeBlur(opts.blur.bgEl, opts.blur.photoUrl, opts.blur.amount, scale);
|
|
}
|
|
try {
|
|
var canvas = await html2canvas(el, {
|
|
backgroundColor: '#000',
|
|
scale: scale,
|
|
useCORS: true,
|
|
logging: false,
|
|
width: actualW,
|
|
height: actualH,
|
|
windowWidth: document.documentElement.clientWidth,
|
|
windowHeight: document.documentElement.clientHeight,
|
|
});
|
|
return await new Promise(function (resolve, reject) {
|
|
canvas.toBlob(function (blob) {
|
|
if (!blob) { reject(new Error('Falha ao gerar a imagem.')); return; }
|
|
resolve(blob);
|
|
}, 'image/jpeg', 0.95);
|
|
});
|
|
} finally {
|
|
if (restore) restore();
|
|
}
|
|
}
|
|
|
|
function saveBlob(blob, fileName) {
|
|
var url = URL.createObjectURL(blob);
|
|
var link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = fileName;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
setTimeout(function () { URL.revokeObjectURL(url); }, 1500);
|
|
}
|
|
|
|
/* files: [{ blob, name }]. One file downloads directly; several are
|
|
bundled into a single ZIP. */
|
|
async function saveAll(files, zipName) {
|
|
if (!files.length) return;
|
|
if (files.length === 1) { saveBlob(files[0].blob, files[0].name); return; }
|
|
if (typeof JSZip !== 'function') {
|
|
// ZIP library missing — degrade to sequential downloads.
|
|
files.forEach(function (f) { saveBlob(f.blob, f.name); });
|
|
return;
|
|
}
|
|
var zip = new JSZip();
|
|
files.forEach(function (f) { zip.file(f.name, f.blob); });
|
|
var blob = await zip.generateAsync({ type: 'blob' });
|
|
saveBlob(blob, zipName);
|
|
}
|
|
|
|
return {
|
|
chunk: chunk,
|
|
renderBlob: renderBlob,
|
|
saveBlob: saveBlob,
|
|
saveAll: saveAll,
|
|
};
|
|
})();
|