Files
dtf-system/web/site-upload.js
Cauê Faleiros 8dddf0fa25
All checks were successful
Build and deploy / Validate source (push) Successful in 6s
Build and deploy / Integration suite on a real stack (push) Successful in 2m31s
Build and deploy / Secret scan and release gate (push) Successful in 6s
Build and deploy / Publish images (push) Successful in 1m0s
feat: redesign the Montagem page, with thumbnails, check chips and a DPI price scale
The product page is rebuilt around mockup 1. The two ways to send are
cards under the title; the PNG/CDR route choice is gone (it never changed
the price, and the picker now takes every allowed format). Each file card
has a thumbnail, its size, check chips with a plain sentence for any
problem, a "Ver detalhes da conferência" link and a copies stepper; a CDR
file says "Conferência manual · preço cheio" and asks for the length. The
buy box shows the price scale in DPI with the customer's step, the metre
and 10 cm length, the total and what the resolution saved, and "Sai em N
partes" only above 20 m. Folha já montada gets "Ampliar" instead of zoom.
Pricing, grade and checks are unchanged.

Fixes on top: the card thumbnail is a small image made once, not the full
preview re-parsed on every repaint; sizes from 1 GB up read in GB; a failed
upload is shown with "Tentar de novo" instead of retrying silently on every
cart change. The browser suites wait for the product and the reloaded page
instead of racing them, and the artwork suite delays imagemDaArte, which
the packing now uses, fixing the CI failure of f4aacb5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:36:41 -03:00

153 lines
7.1 KiB
JavaScript

/* Site DTF — Accepting files: the drop zone, the accept rules, first measurement.
Extracted verbatim from the single inline script in web/index.html.
Loaded as classic scripts in the order listed there: they share one global
scope and run top to bottom, exactly as the original did. */
// ── upload
// Untrusted filenames and recovered cart text must never become HTML.
function escapeHTML(value){
return String(value).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function recusa(lista){
const el=$('recusa'); if(!el) return;
if(!lista.length){ el.style.display='none'; el.innerHTML=''; return; }
const exts=escapeHTML([...new Set(lista.map(f=>extDe(f.name)))].join(', '));
el.style.display='block';
el.innerHTML = ehFolha()
? '<b>'+exts+' não dá para enviar aqui.</b> A folha pronta aceita PNG, JPG, TIFF, '+
'PDF, PSD, AI e CDR.'
: '<b>'+exts+' não dá para enviar aqui.</b> Artes avulsas precisam de PNG ou JPG com '+
'fundo transparente — é o que a montagem enxerga para encaixar. '+
'Se o seu arquivo é CDR, AI ou PSD, exporte em PNG ou use <b>Arquivo por metro</b>.';
}
const Z=$('zona'), I=$('inp');
Z.addEventListener('click',()=>I.click());
Z.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){I.click();e.preventDefault();}});
['dragover','dragenter'].forEach(ev=>Z.addEventListener(ev,e=>{e.preventDefault();Z.classList.add('sobre')}));
['dragleave','drop'].forEach(ev=>Z.addEventListener(ev,e=>{
e.preventDefault(); Z.classList.remove('sobre');
if(ev==='drop') sel([...e.dataTransfer.files]);
}));
I.addEventListener('change',()=>sel([...I.files]));
function sel(fs){
if(!fs.length) return;
// Pixels cannot tell a loose artwork from a finished sheet, so the customer
// declares it by choosing the product in the selector above. The declaration is
// verified in medirFolha; the file never changes product or price on its own.
const regra = ehFolha() ? ACEITA.folha : ACEITA.avulsa;
const fora = fs.filter(f=>!regra.test(f.name));
fs = fs.filter(f=>regra.test(f.name));
recusa(fora);
const max=window.dtfUploadMaxBytes||5*1073741824;
const grandes=fs.filter(f=>f.size>max);
if(grandes.length){
const el=$('recusa');
el.style.display='block';
el.innerHTML='<b>Arquivo acima do limite de '+(max/1073741824).toFixed(0)+
' GB.</b> Divida a folha em partes menores antes de enviar.';
fs=fs.filter(f=>f.size<=max);
}
if(!fs.length) return;
if(ehFolha()){
const novas=fs.map(f=>({f, med:null, rep:1, m:0, semAnalise:null}));
folhas=folhas.concat(novas); // soma, não substitui
pintaFolha();
novas.forEach(x=>{
x.pct=5; pintaFolha();
medirFolha(x.f).then(md=>{
if(md && md.rejected){
x.measurementError=md.reason; x.pct=null; pintaFolha(); return;
}
if(md && md.dpiFolha!=null && md.dpiFolha<DPI_RECUSA) return recusaFolha(x, md);
x.med=md; if(md) x.m=md.alt/100; x.pct=55; pintaFolha();
if(md && md.grande){
// Graded at once from its size in pixels. A PNG is then read as a
// stream into a small copy: the preview, and the sheet check.
x.an={dpi:md.dpiFolha, grande:true, artes:0, residuos:0, encostadas:0,
fundoChapado:false, aproveitamento:0, alturaCm:md.alt, fonteDpi:'arquivo'};
if(!/\.png$/i.test(x.f.name)){ x.pct=null; pintaFolha(); return; }
x.pct=60; pintaFolha();
let mostrado=0;
previaPngGrande(x.f, 600, p=>{
const pct=60+Math.floor(p*38);
if(pct-mostrado>=1){ mostrado=pct; x.pct=pct; pintaProgressoFolha(x); }
}).then(cv=>{
if(cv && folhas.includes(x)){
x.previewSrc=cv.toDataURL('image/webp',0.85); x.miniSrc=miniatura(cv);
try{
const an=analisarFolha(cv, md.larg);
x.an={...an, dpi:md.dpiFolha, fonteDpi:'arquivo', grande:false};
}catch(e){}
}
}).catch(()=>{}).finally(()=>{ x.pct=null; pintaFolha(); });
}else if(md && RENDERIZA.test(x.f.name)){
carregarImagem(x.f).then(img=>{
if(img){ x.previewSrc=img.src; x.miniSrc=miniatura(img); try{ x.an=analisarFolha(img, md.larg); if(x.an) x.an.fonteDpi='arquivo'; }
catch(e){} }
x.pct=null; pintaFolha();
});
}else if(md && /\.pdf$/i.test(x.f.name) && x.f.size>GRANDE_BYTES){
x.semAnalise='PDF grande: a resolução das imagens de dentro é conferida pela equipe';
x.pct=null; pintaFolha();
}else if(md && /\.pdf$/i.test(x.f.name)){
x.pct=70; pintaFolha();
rasterizarPdf(x.f, md.larg, md.alt).then(r=>{
if(r && r.erro){
x.semAnalise = r.erro + (r.semWorker
? ' · este navegador não deixa o leitor rodar em segundo plano'
: '');
}
if(r && r.tela){
x.miniSrc=miniatura(r.tela);
try{
x.an=analisarFolha(r.tela, md.larg);
// vetor puro não tem resolução · nota máxima, e está certo
x.an.dpi = r.dpi==null ? DPI_AVISA : r.dpi;
x.an.vetor = r.dpi==null;
x.an.fonteDpi = r.dpi==null ? 'vetor' : 'imagens';
x.an.res = r.res || null;
}catch(e){}
}
x.pct=null; pintaFolha();
});
}else{ x.pct=null; pintaFolha(); }
});
});
return;
}
const novos=fs.map(f=>({f,px:0,py:0,cm:0,q:1,prop:1,giro:0,esp:false}));
artes=novos.concat(artes); // a mais recente fica no topo da fila
recemChegada=novos[0];
pintaArtes();
$('lista').scrollIntoView({behavior:'smooth',block:'nearest'});
Promise.all(novos.map(medir)).then(pintaArtes);
}
function medir(a){
// A large artwork is measured from its header and packed from a small copy
// read as a stream (previaPngGrande); the file itself is never decoded.
if(a.f.size>GRANDE_BYTES){
return dimensoesImagem(a.f).then(async d=>{
if(!d || !(d.w>0) || !(d.h>0)){ a.decodeError=true; return a; }
a.px=d.w; a.py=d.h; a.prop=d.h/d.w; a.grande=true;
if(!/\.png$/i.test(a.f.name)){ a.decodeError='grande'; return a; }
a.lendo=0; pintaArtes();
let mostrado=0;
const cv=await previaPngGrande(a.f, 1200, p=>{
const pct=Math.floor(p*100);
if(pct-mostrado>=1){ mostrado=pct; a.lendo=pct; pintaProgressoArte(a); }
}).catch(()=>null);
a.lendo=null;
if(cv && artes.includes(a)) a.src=cv.toDataURL('image/webp',0.9); else if(!cv) a.decodeError=true;
return a;
}).catch(()=>{ a.decodeError=true; return a; });
}
return carregarImagem(a.f).then(img=>{
if(img){ a.prop=img.height/img.width; a.px=img.width; a.py=img.height; a.src=img.src; }
else a.decodeError=true;
return a;
});
}
// A metragem sai do arquivo, nunca do peso em bytes.
// imagem → proporção da imagem aplicada à largura do filme
// PDF → página única lida pelo PDF.js, com boxes, rotação e UserUnit
// TIFF, PSD, AI, CDR → o navegador não abre; o cliente informa o comprimento