Spaces:
Paused
Paused
| <html lang="pt-BR"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>CineCapas — Baixar Capas de Filmes</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet"> | |
| <style> | |
| :root{ | |
| --bg:#0a0a0f;--surface:#111118;--surface2:#1a1a24; | |
| --border:#2a2a3a;--gold:#e8b84b;--gold2:#f5d07a; | |
| --text:#f0ede8;--muted:#6b6880;--red:#e74c3c;--green:#2ecc71;--blue:#3498db; | |
| } | |
| *{margin:0;padding:0;box-sizing:border-box;} | |
| body{background:var(--bg);color:var(--text);font-family:'DM Sans',sans-serif;font-weight:300;min-height:100vh;overflow-x:hidden;} | |
| body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:999;opacity:.35; | |
| background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.04'/%3E%3C/svg%3E");} | |
| /* ── Header ── */ | |
| header{text-align:center;padding:52px 20px 32px;} | |
| .logo-row{display:flex;align-items:center;justify-content:center;gap:14px;margin-bottom:8px;} | |
| .film-icon{width:42px;height:42px;display:grid;grid-template-columns:7px 1fr 7px;background:var(--gold);border-radius:4px;overflow:hidden;} | |
| .holes{background:var(--bg);display:flex;flex-direction:column;justify-content:space-around;padding:4px 2px;} | |
| .hole{width:4px;height:4px;background:var(--gold);border-radius:1px;} | |
| h1{font-family:'Bebas Neue',sans-serif;font-size:clamp(40px,8vw,76px);letter-spacing:6px; | |
| background:linear-gradient(135deg,var(--gold),var(--gold2) 50%,var(--gold)); | |
| -webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;line-height:.9;} | |
| .divider{width:56px;height:2px;background:var(--gold);margin:16px auto;position:relative;} | |
| .divider::before,.divider::after{content:'';position:absolute;top:50%;width:4px;height:4px; | |
| background:var(--gold);border-radius:50%;transform:translateY(-50%);} | |
| .divider::before{left:-8px;}.divider::after{right:-8px;} | |
| .tagline{color:var(--muted);font-size:11px;letter-spacing:3px;text-transform:uppercase;margin-top:8px;} | |
| /* ── Search ── */ | |
| .wrap{max-width:740px;margin:0 auto;padding:0 18px 70px;} | |
| .search-row{display:flex;gap:10px;margin-bottom:10px;} | |
| .iw{position:relative;flex:1;} | |
| .ilabel{position:absolute;top:-9px;left:12px;font-size:10px;letter-spacing:2px; | |
| text-transform:uppercase;color:var(--gold);background:var(--bg);padding:0 5px;z-index:1;} | |
| input[type=text]{width:100%;background:var(--surface);border:1px solid var(--border); | |
| border-radius:8px;color:var(--text);font-family:'DM Sans',sans-serif;font-size:15px; | |
| padding:16px 18px;outline:none;transition:border-color .2s,box-shadow .2s;} | |
| input:focus{border-color:var(--gold);box-shadow:0 0 0 3px rgba(232,184,75,.1);} | |
| input::placeholder{color:var(--muted);} | |
| button{cursor:pointer;font-family:'DM Sans',sans-serif;border:none;border-radius:8px; | |
| font-size:13px;font-weight:500;letter-spacing:1px;text-transform:uppercase;transition:all .2s;} | |
| .btn-go{background:var(--gold);color:#0a0a0f;padding:16px 26px;white-space:nowrap;} | |
| .btn-go:hover{background:var(--gold2);transform:translateY(-1px);box-shadow:0 6px 20px rgba(232,184,75,.25);} | |
| .btn-go:active{transform:translateY(0);} | |
| .btn-go:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none;} | |
| /* tabs */ | |
| .tabs{display:flex;gap:6px;margin-bottom:18px;} | |
| .tab{background:var(--surface2);border:1px solid var(--border);color:var(--muted); | |
| padding:8px 16px;border-radius:20px;font-size:12px;letter-spacing:.5px;font-weight:400;} | |
| .tab:hover{border-color:var(--gold);color:var(--gold);} | |
| .tab.on{background:rgba(232,184,75,.12);border-color:var(--gold);color:var(--gold);} | |
| .hint{font-size:12px;color:var(--muted);text-align:center;padding:4px 0 16px;} | |
| .hint em{color:var(--gold);font-style:normal;} | |
| /* ── Loader ── */ | |
| .loader{display:none;text-align:center;padding:44px;} | |
| .loader.on{display:block;} | |
| .spin{width:32px;height:32px;border:2px solid var(--border);border-top-color:var(--gold); | |
| border-radius:50%;animation:rot .7s linear infinite;margin:0 auto 12px;} | |
| @keyframes rot{to{transform:rotate(360deg)}} | |
| .loader p{color:var(--muted);font-size:13px;} | |
| /* ── Grid ── */ | |
| #grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px;margin-top:4px;} | |
| .card{background:var(--surface);border:1px solid var(--border);border-radius:10px;overflow:hidden; | |
| cursor:pointer;transition:transform .2s,border-color .2s,box-shadow .2s; | |
| animation:up .3s ease forwards;opacity:0;} | |
| @keyframes up{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}} | |
| .card:hover{transform:translateY(-4px);border-color:var(--gold);box-shadow:0 10px 28px rgba(0,0,0,.5);} | |
| .card img{width:100%;aspect-ratio:2/3;object-fit:cover;display:block;background:var(--surface2);} | |
| .no-img{width:100%;aspect-ratio:2/3;background:var(--surface2);display:flex;flex-direction:column; | |
| align-items:center;justify-content:center;color:var(--muted);font-size:11px;gap:6px;} | |
| .ci{padding:10px 12px;} | |
| .ct{font-size:12px;font-weight:500;line-height:1.35;margin-bottom:4px; | |
| display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;} | |
| .cm{font-size:11px;color:var(--muted);display:flex;gap:6px;align-items:center;flex-wrap:wrap;} | |
| .badge{font-size:9px;padding:2px 5px;border-radius:3px;text-transform:uppercase;letter-spacing:.5px;font-weight:500;} | |
| .bm{background:rgba(192,57,43,.2);color:var(--red);} | |
| .bt{background:rgba(39,174,96,.2);color:var(--green);} | |
| /* ── Messages ── */ | |
| .err{background:rgba(192,57,43,.1);border:1px solid rgba(192,57,43,.3);border-radius:8px; | |
| padding:13px 18px;color:var(--red);font-size:13px;text-align:center;display:none;margin-top:6px;} | |
| .empty{display:none;text-align:center;padding:52px 20px;} | |
| .empty .big{font-family:'Bebas Neue';font-size:54px;letter-spacing:4px;color:var(--border);display:block;margin-bottom:8px;} | |
| .empty p{color:var(--muted);font-size:13px;line-height:1.8;} | |
| /* ── Modal ── */ | |
| .ov{display:none;position:fixed;inset:0;background:rgba(0,0,0,.88);z-index:1000; | |
| align-items:center;justify-content:center;padding:20px;backdrop-filter:blur(10px);} | |
| .ov.on{display:flex;} | |
| .modal{background:var(--surface);border:1px solid var(--border);border-radius:16px; | |
| max-width:500px;width:100%;overflow:hidden;animation:min .22s ease; | |
| position:relative;max-height:92vh;overflow-y:auto;} | |
| @keyframes min{from{opacity:0;transform:scale(.93) translateY(14px)}to{opacity:1;transform:scale(1) translateY(0)}} | |
| .mcls{position:sticky;top:10px;float:right;margin:10px 10px -30px 0; | |
| background:rgba(255,255,255,.09);color:var(--text);border:none;width:30px;height:30px; | |
| border-radius:50%;font-size:16px;display:flex;align-items:center;justify-content:center; | |
| cursor:pointer;transition:background .2s;z-index:2;} | |
| .mcls:hover{background:rgba(255,255,255,.18);} | |
| .mpw{position:relative;background:#050507;display:flex;align-items:center;justify-content:center;min-height:180px;} | |
| #mposter{width:100%;max-height:380px;object-fit:contain;display:block;} | |
| #mposter.hide{display:none;} | |
| .noimg-l{padding:70px 20px;text-align:center;color:var(--muted);font-size:13px;width:100%;} | |
| .qbadge{position:absolute;bottom:10px;right:10px;background:var(--gold);color:#0a0a0f; | |
| font-size:9px;font-weight:700;letter-spacing:1px;padding:3px 8px;border-radius:4px;} | |
| .mbody{padding:18px 22px 26px;clear:both;} | |
| #mtitle{font-family:'Bebas Neue',sans-serif;font-size:26px;letter-spacing:2px; | |
| color:var(--gold);line-height:1;margin-bottom:5px;} | |
| #mmeta{color:var(--muted);font-size:12px;margin-bottom:6px;line-height:1.5;} | |
| #mplot{font-size:12.5px;color:#8080a0;line-height:1.65;margin-bottom:16px;font-style:italic;} | |
| /* Seletor de qualidade */ | |
| .ql{font-size:10px;text-transform:uppercase;letter-spacing:2px;color:var(--muted);margin-bottom:8px;} | |
| .qopts{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:16px;} | |
| .qb{background:var(--surface2);border:1px solid var(--border);color:var(--muted); | |
| padding:7px 13px;border-radius:6px;font-size:11px;text-transform:none;letter-spacing:0;font-weight:400;} | |
| .qb:hover{border-color:var(--gold);color:var(--gold);} | |
| .qb.on{background:rgba(232,184,75,.12);border-color:var(--gold);color:var(--gold);} | |
| .drow{display:flex;gap:9px;} | |
| .bdl{flex:1;background:var(--gold);color:#0a0a0f;padding:13px;font-size:13px;letter-spacing:1px; | |
| display:flex;align-items:center;justify-content:center;gap:7px;} | |
| .bdl:hover{background:var(--gold2);transform:translateY(-1px);box-shadow:0 6px 20px rgba(232,184,75,.25);} | |
| .bdl:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none;} | |
| .bop{background:var(--surface2);border:1px solid var(--border);color:var(--text); | |
| padding:13px 16px;font-size:13px;letter-spacing:.5px;} | |
| .bop:hover{border-color:var(--gold);color:var(--gold);} | |
| /* ── Toast ── */ | |
| .toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%) translateY(8px); | |
| background:var(--gold);color:#0a0a0f;padding:10px 20px;border-radius:8px; | |
| font-weight:500;font-size:13px;z-index:9999;opacity:0; | |
| transition:opacity .25s,transform .25s;pointer-events:none;} | |
| .toast.on{opacity:1;transform:translateX(-50%) translateY(0);} | |
| footer{text-align:center;padding:34px 20px;color:var(--muted);font-size:11px; | |
| letter-spacing:1px;border-top:1px solid var(--border);margin-top:40px;} | |
| footer a{color:var(--muted);text-decoration:none;} | |
| footer a:hover{color:var(--gold);} | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo-row"> | |
| <div class="film-icon"> | |
| <div class="holes"><div class="hole"></div><div class="hole"></div><div class="hole"></div></div> | |
| <div style="background:var(--gold)"></div> | |
| <div class="holes"><div class="hole"></div><div class="hole"></div><div class="hole"></div></div> | |
| </div> | |
| <h1>CineCapas</h1> | |
| </div> | |
| <div class="divider"></div> | |
| <p class="tagline">Baixe capas de filmes e séries em alta qualidade</p> | |
| </header> | |
| <div class="wrap"> | |
| <div class="search-row"> | |
| <div class="iw"> | |
| <span class="ilabel">Buscar</span> | |
| <input type="text" id="q" placeholder="Ex: Reacher, Coringa, Dupla Perigosa, The Dark Knight…" | |
| autocomplete="off" spellcheck="false"> | |
| </div> | |
| <button class="btn-go" id="sbtn">Buscar</button> | |
| </div> | |
| <div class="tabs"> | |
| <button class="tab on" data-type="multi">🎬 Filmes + Séries</button> | |
| <button class="tab" data-type="movie">🎥 Só Filmes</button> | |
| <button class="tab" data-type="tv">📺 Só Séries</button> | |
| </div> | |
| <p class="hint"> | |
| Banco de dados com <em>+1 milhão</em> de títulos · Suporte a <em>português</em> e <em>inglês</em> · Resultados em tempo real | |
| </p> | |
| <div class="loader" id="ldr"><div class="spin"></div><p id="lmsg">Buscando…</p></div> | |
| <div class="err" id="errmsg"></div> | |
| <div class="empty" id="emp"> | |
| <span class="big">404</span> | |
| <p>Nenhum resultado encontrado.<br>Tente o nome em inglês ou verifique a ortografia.</p> | |
| </div> | |
| <div id="grid"></div> | |
| </div> | |
| <!-- MODAL --> | |
| <div class="ov" id="ov"> | |
| <div class="modal"> | |
| <button class="mcls" id="mcls">✕</button> | |
| <div class="mpw"> | |
| <img id="mposter" src="" alt="Capa"> | |
| <div class="noimg-l" id="noimg" style="display:none">Sem imagem disponível</div> | |
| <span class="qbadge" id="qbadge"></span> | |
| </div> | |
| <div class="mbody"> | |
| <div id="mtitle"></div> | |
| <div id="mmeta"></div> | |
| <div id="mplot"></div> | |
| <div class="ql">Qualidade do download</div> | |
| <div class="qopts" id="qopts"> | |
| <button class="qb" data-w="w185">185px · SD</button> | |
| <button class="qb" data-w="w342">342px</button> | |
| <button class="qb on" data-w="w500">500px · HD ★</button> | |
| <button class="qb" data-w="w780">780px · Full HD</button> | |
| <button class="qb" data-w="original">Original</button> | |
| </div> | |
| <div class="drow"> | |
| <button class="bdl" id="bdl">⬇ Baixar Capa</button> | |
| <button class="bop" id="bop">🔗 Abrir</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="toast" id="toast"></div> | |
| <footer> | |
| CineCapas · Dados e imagens via <a href="https://www.themoviedb.org" target="_blank">TMDB</a> | |
| · Este produto usa a API TMDB mas não é endossado ou certificado por TMDB. | |
| </footer> | |
| <script> | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| // TMDB API — gratuita, 1M+ títulos, imagens sem CORS, atualização diária | |
| // Para obter sua própria chave: https://www.themoviedb.org/settings/api | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| const TMDB_KEY = '2dca580c2a14b55200e784d157207b4d'; // chave pública de demo (TMDB permite) | |
| const TMDB_BASE = 'https://api.themoviedb.org/3'; | |
| const IMG_BASE = 'https://image.tmdb.org/t/p/'; | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| // ESTADO | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| const S = { | |
| type : 'multi', // 'multi' | 'movie' | 'tv' | |
| imgPath : '', // ex: /abc123.jpg | |
| imgWidth : 'w500', // tamanho selecionado | |
| title : '', | |
| searchGen : 0, // geração da busca — ignora resultados de buscas antigas | |
| toastT : null, | |
| }; | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| // INIT | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const q = document.getElementById('q'); | |
| q.focus(); | |
| document.getElementById('sbtn').addEventListener('click', doSearch); | |
| q.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); }); | |
| // Tabs | |
| document.querySelectorAll('.tab').forEach(t => { | |
| t.addEventListener('click', () => { | |
| document.querySelectorAll('.tab').forEach(x => x.classList.remove('on')); | |
| t.classList.add('on'); | |
| S.type = t.dataset.type; | |
| }); | |
| }); | |
| // Modal fechar | |
| document.getElementById('mcls').addEventListener('click', closeModal); | |
| document.getElementById('ov').addEventListener('click', e => { | |
| if (e.target.id === 'ov') closeModal(); | |
| }); | |
| document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); }); | |
| // Qualidade | |
| document.getElementById('qopts').addEventListener('click', e => { | |
| const b = e.target.closest('[data-w]'); | |
| if (!b) return; | |
| document.querySelectorAll('#qopts [data-w]').forEach(x => x.classList.remove('on')); | |
| b.classList.add('on'); | |
| S.imgWidth = b.dataset.w; | |
| if (S.imgPath) refreshPoster(); | |
| }); | |
| // Ações modal | |
| document.getElementById('bdl').addEventListener('click', download); | |
| document.getElementById('bop').addEventListener('click', () => { | |
| if (S.imgPath) window.open(imgUrl(S.imgPath, S.imgWidth), '_blank'); | |
| }); | |
| }); | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| // BUSCA | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| async function doSearch() { | |
| const raw = document.getElementById('q').value.trim(); | |
| if (!raw) { showErr('Digite um nome para buscar!'); return; } | |
| // Geração controla buscas concorrentes sem AbortController | |
| const gen = ++S.searchGen; | |
| setLoad(true); | |
| clearUI(); | |
| try { | |
| const endpoint = S.type === 'multi' ? '/search/multi' : `/search/${S.type}`; | |
| const [resPT, resEN] = await Promise.all([ | |
| tmdb(endpoint, { query: raw, language: 'pt-BR', include_adult: false }), | |
| tmdb(endpoint, { query: raw, language: 'en-US', include_adult: false }), | |
| ]); | |
| if (gen !== S.searchGen) return; // busca mais recente sobrepõe esta | |
| let results = mergeById([ | |
| ...(resPT?.results ?? []), | |
| ...(resEN?.results ?? []), | |
| ]); | |
| if (S.type === 'multi') { | |
| results = results.filter(r => r.media_type === 'movie' || r.media_type === 'tv'); | |
| } | |
| results.length ? render(results) : showEmpty(); | |
| } catch (e) { | |
| if (gen !== S.searchGen) return; | |
| console.error('[CineCapas]', e); | |
| showErr('Erro ao buscar. Verifique sua conexão e tente novamente.'); | |
| } finally { | |
| if (gen === S.searchGen) setLoad(false); | |
| } | |
| } | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| // TMDB FETCH | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| async function tmdb(path, params = {}) { | |
| const qs = new URLSearchParams({ api_key: TMDB_KEY, ...params }); | |
| const res = await fetch(`${TMDB_BASE}${path}?${qs}`); | |
| if (!res.ok) throw new Error(`TMDB ${res.status}`); | |
| return res.json(); | |
| } | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| // RENDER GRID | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| function render(items) { | |
| const grid = document.getElementById('grid'); | |
| grid.innerHTML = ''; | |
| const frag = document.createDocumentFragment(); | |
| items.forEach((item, i) => { | |
| const isTV = item.media_type === 'tv' || S.type === 'tv'; | |
| const title = item.title || item.name || '—'; | |
| const year = (item.release_date || item.first_air_date || '').slice(0, 4); | |
| const poster = item.poster_path; | |
| const card = document.createElement('div'); | |
| card.className = 'card'; | |
| card.style.animationDelay = `${Math.min(i, 9) * 0.05}s`; | |
| card.innerHTML = | |
| (poster | |
| ? `<img src="${IMG_BASE}w342${esc(poster)}" alt="${esc(title)}" loading="lazy" decoding="async">` | |
| : `<div class="no-img">🎬<br>Sem capa</div>`) + | |
| `<div class="ci"> | |
| <div class="ct">${esc(title)}</div> | |
| <div class="cm"> | |
| ${year ? `<span>${esc(year)}</span>` : ''} | |
| <span class="badge ${isTV ? 'bt' : 'bm'}">${isTV ? 'Série' : 'Filme'}</span> | |
| </div> | |
| </div>`; | |
| card.addEventListener('click', () => openModal(item.id, isTV ? 'tv' : 'movie')); | |
| frag.appendChild(card); | |
| }); | |
| grid.appendChild(frag); | |
| } | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| // MODAL | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| async function openModal(id, type) { | |
| resetModal(); | |
| document.getElementById('ov').classList.add('on'); | |
| document.getElementById('mtitle').textContent = 'Carregando…'; | |
| document.getElementById('bdl').disabled = true; | |
| try { | |
| // Pega detalhes em PT, com fallback EN para poster/overview | |
| const [detPT, detEN] = await Promise.all([ | |
| tmdb(`/${type}/${id}`, { language: 'pt-BR', append_to_response: 'images' }), | |
| tmdb(`/${type}/${id}`, { language: 'en-US', append_to_response: 'images' }), | |
| ]); | |
| const title = detPT.title || detPT.name || detEN.title || detEN.name || '—'; | |
| const year = (detPT.release_date || detPT.first_air_date || '').slice(0, 4); | |
| const genres = (detPT.genres || []).map(g => g.name).join(', '); | |
| const runtime = detPT.runtime ? `${detPT.runtime} min` : | |
| detPT.episode_run_time?.[0] ? `${detPT.episode_run_time[0]} min/ep` : ''; | |
| // Overview: prefere PT, usa EN se PT vier vazio | |
| const overview= (detPT.overview?.trim()) || detEN.overview || ''; | |
| // Poster: prefere versão PT, senão EN | |
| // images.posters[0].file_path tem o melhor poster em alta res | |
| const bestPT = detPT.images?.posters?.[0]?.file_path; | |
| const bestEN = detEN.images?.posters?.[0]?.file_path; | |
| const posterPath = bestPT || bestEN || detPT.poster_path || detEN.poster_path || ''; | |
| S.imgPath = posterPath; | |
| S.title = title; | |
| if (posterPath) { | |
| document.getElementById('mposter').src = imgUrl(posterPath, S.imgWidth); | |
| document.getElementById('mposter').classList.remove('hide'); | |
| document.getElementById('noimg').style.display = 'none'; | |
| document.getElementById('qbadge').textContent = widthLabel(S.imgWidth); | |
| document.getElementById('bdl').disabled = false; | |
| } else { | |
| document.getElementById('mposter').classList.add('hide'); | |
| document.getElementById('noimg').style.display = ''; | |
| document.getElementById('qbadge').textContent = ''; | |
| } | |
| document.getElementById('mtitle').textContent = title; | |
| document.getElementById('mmeta').textContent = | |
| [year, runtime, genres].filter(Boolean).join(' · '); | |
| document.getElementById('mplot').textContent = overview; | |
| } catch (e) { | |
| console.error('[CineCapas] modal:', e); | |
| document.getElementById('mtitle').textContent = 'Erro ao carregar'; | |
| } | |
| } | |
| function resetModal() { | |
| S.imgPath = ''; | |
| S.title = ''; | |
| const img = document.getElementById('mposter'); | |
| img.src = ''; | |
| img.classList.remove('hide'); | |
| document.getElementById('noimg').style.display = 'none'; | |
| document.getElementById('mtitle').textContent = ''; | |
| document.getElementById('mmeta').textContent = ''; | |
| document.getElementById('mplot').textContent = ''; | |
| document.getElementById('qbadge').textContent = ''; | |
| document.getElementById('bdl').disabled = false; | |
| } | |
| function closeModal() { | |
| document.getElementById('ov').classList.remove('on'); | |
| setTimeout(() => { | |
| document.getElementById('mposter').src = ''; | |
| S.imgPath = ''; | |
| }, 260); | |
| } | |
| function refreshPoster() { | |
| const url = imgUrl(S.imgPath, S.imgWidth); | |
| document.getElementById('mposter').src = url; | |
| document.getElementById('qbadge').textContent = widthLabel(S.imgWidth); | |
| } | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| // DOWNLOAD | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| async function download() { | |
| if (!S.imgPath) { toast('Nenhuma capa disponível.'); return; } | |
| const url = imgUrl(S.imgPath, S.imgWidth); | |
| const btn = document.getElementById('bdl'); | |
| btn.disabled = true; | |
| btn.textContent = '⏳ Baixando…'; | |
| let objUrl = null; | |
| try { | |
| const res = await fetch(url); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const blob = await res.blob(); | |
| objUrl = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| const safe = S.title.replace(/[^a-z0-9]/gi, '_').toLowerCase().slice(0, 80); | |
| a.href = objUrl; | |
| a.download = `${safe || 'poster'}.jpg`; | |
| a.click(); | |
| toast('⬇ Download iniciado!'); | |
| } catch { | |
| window.open(url, '_blank'); | |
| toast('🔗 Abriu em nova aba — salve com botão direito.'); | |
| } finally { | |
| btn.disabled = false; | |
| btn.textContent = '⬇ Baixar Capa'; | |
| if (objUrl) setTimeout(() => URL.revokeObjectURL(objUrl), 2000); | |
| } | |
| } | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| // HELPERS | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| function imgUrl(path, w) { | |
| return `${IMG_BASE}${w}${path}`; | |
| } | |
| function widthLabel(w) { | |
| const map = { w185:'SD', w342:'SD+', w500:'HD', w780:'FULL HD', original:'ORIGINAL' }; | |
| return map[w] ?? w.toUpperCase(); | |
| } | |
| // Merge removendo IDs duplicados (PT e EN podem retornar o mesmo título) | |
| function mergeById(arr) { | |
| const seen = new Set(); | |
| return arr.filter(x => { | |
| const k = `${x.id}-${x.media_type ?? ''}`; | |
| if (seen.has(k)) return false; | |
| seen.add(k); | |
| return true; | |
| }); | |
| } | |
| function setLoad(on) { | |
| document.getElementById('ldr').classList.toggle('on', on); | |
| document.getElementById('sbtn').disabled = on; | |
| } | |
| function clearUI() { | |
| document.getElementById('errmsg').style.display = 'none'; | |
| document.getElementById('emp').style.display = 'none'; | |
| document.getElementById('grid').innerHTML = ''; | |
| } | |
| function showErr(msg) { | |
| const el = document.getElementById('errmsg'); | |
| el.textContent = msg; | |
| el.style.display = 'block'; | |
| setLoad(false); | |
| } | |
| function showEmpty() { | |
| document.getElementById('emp').style.display = 'block'; | |
| } | |
| function toast(msg) { | |
| const t = document.getElementById('toast'); | |
| t.textContent = msg; | |
| t.classList.add('on'); | |
| clearTimeout(S.toastT); | |
| S.toastT = setTimeout(() => t.classList.remove('on'), 3200); | |
| } | |
| // Escapa HTML — previne XSS com dados da API | |
| function esc(s) { | |
| return String(s ?? '') | |
| .replace(/&/g,'&').replace(/</g,'<') | |
| .replace(/>/g,'>').replace(/"/g,'"'); | |
| } | |
| </script> | |
| </body> | |
| </html> | |