1/* ===================================================================== 2 * mymovies Lab â Widget "vetrina" di video verticali 3 * Da incorporare in qualsiasi pagina mymovies con una sola riga: 4 * 5 * <div class="lab-vshelf" 6 * data-json="https://www.mymovies.it/mya/videos.json" 7 * data-title="Da vedere in verticale" 8 * data-limit="12"></div> 9 * <script src="https://www.mymovies.it/mya/lab-widget.js" defer></script> 10 * 11 * Attributi opzionali sul contenitore: 12 * data-json URL del catalogo (default /lab/videos.json) â un domani un endpoint DB 13 * data-base base del player (default /lab/) -> link finale /lab/?v=slug 14 * data-title titolo della sezione (opzionale) 15 * data-limit numero massimo di item (0 = tutti) 16 * data-per-row quante card riempiono la larghezza (default 4) 17 * data-theme "dark" per pagine a sfondo scuro 18 * 19 * - Anteprima stile Instagram: poster statico, e al passaggio del mouse 20 * (desktop) o quando entra in vista (mobile) parte un video muto in loop 21 * sui primi secondi. Caricamento lazy: il video si scarica solo quando serve. 22 * - Classi isolate "labv-" + CSS scoped: non interferisce col sito. 23 * ===================================================================== */ 24(function () { 25 if (window.__labVShelfLoaded) return; 26 window.__labVShelfLoaded = true; 27 28 var PREVIEW_SECONDS = 6; // durata dell'anteprima prima del loop 29 var canHover = window.matchMedia && window.matchMedia('(hover: hover)').matches; 30 31 var CSS = '' + 32 '.labv-shelf{--labv-accent:#DE1163;--labv-per:4;--labv-gap:12px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;margin:0px 0;max-width:100%;}' + 33 '.labv-shelf *{box-sizing:border-box;}' + 34 '.labv-head{font-size:18px;font-weight:300;margin:0 0 12px 4px;color:#141414;}' + 35 '.labv-wrap{position:relative;max-width:100%;overflow:hidden;}' + 36 '.labv-row{display:flex;gap:var(--labv-gap);overflow-x:auto;padding:4px 2px 8px;scroll-snap-type:x proximity;-webkit-overflow-scrolling:touch;scrollbar-width:none;}' + 37 '.labv-row::-webkit-scrollbar{display:none;}' + 38 '.labv-card{flex:0 0 calc((100% - (var(--labv-per) - 1) * var(--labv-gap)) / var(--labv-per));min-width:0;text-decoration:none;color:inherit;scroll-snap-align:start;cursor:pointer;}' + 39 '.labv-arrow{position:absolute;top:42%;transform:translateY(-50%);width:40px;height:40px;border-radius:50%;border:none;background:rgba(20,20,20,.72);color:#fff;font-size:24px;line-height:1;cursor:pointer;z-index:5;display:none;align-items:center;justify-content:center;box-shadow:0 2px 10px rgba(0,0,0,.35);transition:opacity .2s;}' + 40 '.labv-wrap.has-more .labv-arrow{display:flex;}' + 41 '.labv-arrow:disabled{opacity:0;pointer-events:none;}' + 42 '.labv-prev{left:6px;}.labv-next{right:6px;}' + 43 '@media (max-width:700px){.labv-card{flex:0 0 calc((100% - 12px) / 2.2);}.labv-arrow{display:none!important;}.labv-head{font-size:16px;}}' + 44 '.labv-media{position:relative;width:100%;aspect-ratio:9/16;border-radius:14px;overflow:hidden;background:#000;box-shadow:0 4px 14px rgba(0,0,0,.18);transition:box-shadow .2s,transform .2s;}' + 45 '.labv-card:hover .labv-media{box-shadow:0 8px 22px rgba(0,0,0,.30);transform:translateY(-2px);}' + 46 '.labv-poster,.labv-video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;}' + 47 '.labv-video{opacity:0;transition:opacity .25s;}' + 48 '.labv-card.playing .labv-video{opacity:1;}' + 49 '.labv-badge{position:absolute;top:8px;right:8px;width:26px;height:26px;border-radius:50%;background:rgba(0,0,0,.5);color:#fff;display:flex;align-items:center;justify-content:center;font-size:11px;transition:opacity .2s;}' + 50 '.labv-card.playing .labv-badge{opacity:0;}' + 51 '.labv-grad{position:absolute;left:0;right:0;bottom:0;height:42%;background:linear-gradient(to top,rgba(0,0,0,.55),transparent);pointer-events:none;}' + 52 '.labv-meta{padding:8px 2px 0;}' + 53 '.labv-title{font-size:13.5px;font-weight:600;line-height:1.25;color:#141414;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}' + 54 '.labv-desc{font-size:11.5px;color:#6a6a6a;margin-top:3px;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden;}
54' + 55 '.labv-shelf.labv-dark .labv-head,.labv-shelf.labv-dark .labv-title{color:#fff;}' + 56 '.labv-shelf.labv-dark .labv-desc{color:#b6b6c0;}' + 57 '.mya-open{display:inline-flex;align-items:center;gap:7px;cursor:pointer;border:none;background:#DE1163;color:#fff;font:600 13.5px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;padding:9px 14px;border-radius:24px;text-decoration:none;}' + 58 '.mya-open::before{content:"\\25B6";font-size:11px;}' + 59 '.labv-overlay{position:fixed;inset:0;z-index:2147483000;background:rgba(0,0,0,.92);opacity:0;transition:opacity .2s;display:flex;align-items:center;justify-content:center;}' + 60 '.labv-overlay.open{opacity:1;}' + 61 '.labv-ov-frame{width:100%;height:100%;max-width:520px;border:0;background:#000;display:block;}'; 62 63 function injectCss() { 64 if (document.getElementById('labv-css')) return; 65 var s = document.createElement('style'); 66 s.id = 'labv-css'; 67 s.textContent = CSS; 68 document.head.appendChild(s); 69 } 70 71 function esc(s) { 72 return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { 73 return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; 74 }); 75 } 76 77 function buildCard(v, base) { 78 var slug = v.slug || ''; 79 var sep = base.indexOf('?') > -1 ? '&' : '?'; 80 var a = document.createElement('a'); 81 a.className = 'labv-card'; 82 a.href = base + sep + 'v=' + encodeURIComponent(slug); 83 a.setAttribute('aria-label', v.title || 'video'); 84 // click: apre l'overlay; lascia passare cmd/ctrl/shift/alt/middle-click e i "apri in nuova scheda" 85 a.addEventListener('click', function (e) { 86 if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; 87 e.preventDefault(); 88 openOverlay(a.href); 89 }); 90 a.innerHTML = 91 '<div class="labv-media">' + 92 (v.poster ? '<img class="labv-poster" loading="lazy" src="' + esc(v.poster) + '" alt="' + esc(v.title) + '">' : '') + 93 '<video class="labv-video" muted loop playsinline preload="none"' + 94 (v.poster ? ' poster="' + esc(v.poster) + '"' : '') + 95 // usa la clip leggera "preview" se presente, altrimenti il video pieno 96 ' data-src="' + esc(v.preview || v.src) + '"></video>' + 97 '<span class="labv-badge">►</span>' + 98 '<span class="labv-grad"></span>' + 99 '</div>' + 100 '<div class="labv-meta">' + 101 '<div class="labv-title">' + esc(v.title) + '</div>' + 102 (v.description ? '<div class="labv-desc">' + esc(v.description) + '</div>' : '') + 103 '</div>'; 104 105 var video = a.querySelector('.labv-video'); 106 107 function start() { 108 if (!video.src && video.dataset.src) video.src = video.dataset.src; 109 try { video.currentTime = 0; } catch (e) {} 110 var p = video.play(); 111 if (p && p.catch) p.catch(function () {}); 112 a.classList.add('playing'); 113 } 114 function stop() { 115 video.pause(); 116 a.classList.remove('playing'); 117 } 118 // limita l'anteprima ai primi secondi, poi riparte (effetto Instagram) 119 video.addEventListener('timeupdate', function () { 120 if (video.currentTime >= PREVIEW_SECONDS) { try { video.currentTime = 0; } catch (e) {} } 121 }); 122 123 if (canHover) { 124 a.addEventListener('mouseenter', start); 125 a.addEventListener('mouseleave', stop); 126 } 127 a._start = start; 128 a._stop = stop; 129 return a; 130 } 131 132 // apre il video verticale come OVERLAY (iframe) sopra la pagina; X/Esc/backdrop chiudono 133 function openOverlay(url) { 134 var sep = url.indexOf('?') > -1 ? '&' : '?'; 135 var ov = document.createElement('div'); 136 ov.className = 'labv-overlay'; 137 ov.innerHTML = '<iframe class="labv-ov-frame" src="' + url + sep + 'embed=1" allow="autoplay; fullscreen" allowfullscreen></iframe>'; 138 document.body.appendChild(ov); 139 var prevOverflow = document.documentElement.style.overflow; 140 document.documentElement.style.overflow = 'hidden'; // blocca lo scroll della pagina dietro 141 function close() { 142 if (!ov.parentNode) return; 143 ov.parentNode.removeChild(ov); 144 document.documentElement.style.overflow = prevOverflow; 145 window.removeEventListener('message', onMsg); 146 document.removeEventListener('keydown', onKey); 147 } 148 function onMsg(e) { if (e.data === 'mya-close') close(); } // la X del player chiude l'overlay 149 function onKey(e) { if (e.key === 'Escape') close(); } 150 ov.addEventListener('click', function (e) { if (e.target === ov) close(); }); // click fuori dal video 151 window.addEventListener('message', onMsg); 152 document.addEventListener('keydown', onKey); 153 requestAnimationFrame(function () { ov.classList.add('open'); }); 154 } 155 156 // frecce prev/next: scorrono di una "pagina" (larghezza della fila = 4 card) 157 function setupArrows(wrap, row) { 158 wrap.classList.add('has-more'); 159 var prev = wrap.querySelector('.labv-prev'); 160 var next = wrap.querySelector('.labv-next'); 161 function page(dir) { row.scrollBy({ left: dir * row.clientWidth, behavior: 'smooth' }); } 162 prev.addEventListener('click', function () { page(-1); }); 163 next.addEventListener('click', function () { page(1); });
164 function update() { 165 prev.disabled = row.scrollLeft <= 4; 166 next.disabled = row.scrollLeft + row.clientWidth >= row.scrollWidth - 4; 167 } 168 row.addEventListener('scroll', update, { passive: true }); 169 window.addEventListener('resize', update); 170 update(); 171 } 172 173 // su mobile (niente hover): UNA sola anteprima alla volta, quella piu' centrata 174 function setupAutoplay(cards) { 175 if (!('IntersectionObserver' in window)) return; 176 var ratios = new Map(); 177 var io = new IntersectionObserver(function (entries) { 178 entries.forEach(function (en) { 179 ratios.set(en.target, en.isIntersecting ? en.intersectionRatio : 0); 180 }); 181 var best = null, bestR = 0.6; 182 ratios.forEach(function (r, card) { if (r > bestR) { bestR = r; best = card; } }); 183 cards.forEach(function (c) { if (c === best) c._start(); else c._stop(); }); 184 }, { threshold: [0, 0.5, 0.75, 1] }); 185 cards.forEach(function (c) { io.observe(c); }); 186 } 187 188 function render(container) { 189 var url = container.getAttribute('data-json') || '/mya/videos.json'; 190 var base = container.getAttribute('data-base') || '/mya/'; 191 var title = container.getAttribute('data-title') || ''; 192 var limit = parseInt(container.getAttribute('data-limit') || '0', 10); 193 var perRow = parseInt(container.getAttribute('data-per-row') || '4', 10); 194 if (!(perRow >= 1)) perRow = 4; // quante card riempiono la larghezza (default 4) 195 var dark = (container.getAttribute('data-theme') || '') === 'dark'; 196 197 function paint(data) { 198 var vids = (data && data.videos) || []; 199 if (limit > 0) vids = vids.slice(0, limit); 200 if (!vids.length) { container.style.display = 'none'; return; } 201 202 container.classList.add('labv-shelf'); 203 container.style.setProperty('--labv-per', perRow); // numero di card per riga (dinamico) 204 if (dark) container.classList.add('labv-dark'); 205 container.innerHTML = 206 (title ? '<div class="labv-head">' + esc(title) + '</div>' : '') + 207 '<div class="labv-wrap">' + 208 '<button class="labv-arrow labv-prev" type="button" aria-label="Precedenti">‹</button>' + 209 '<div class="labv-row"></div>' + 210 '<button class="labv-arrow labv-next" type="button" aria-label="Successivi">›</button>' + 211 '</div>'; 212 213 var wrap = container.querySelector('.labv-wrap'); 214 var row = container.querySelector('.labv-row'); 215 var cards = []; 216 vids.forEach(function (v) { var c = buildCard(v, base); row.appendChild(c); cards.push(c); }); 217 if (!canHover) setupAutoplay(cards); 218 219 // frecce solo se ci sono piu card del numero visibile (su mobile nascoste: si scorre col dito) 220 if (cards.length > perRow) setupArrows(wrap, row); 221 } 222 223 // 1) JSON inline (<script type="application/json" class="labv-data">) â utile per demo/offline 224 var inline = container.querySelector('script.labv-data'); 225 if (inline) { 226 try { paint(JSON.parse(inline.textContent)); return; } catch (e) {} 227 } 228 // 2) altrimenti fetch dal catalogo (oggi videos.json, domani endpoint DB) 229 fetch(url, { credentials: 'omit' }) 230 .then(function (r) { return r.json(); }) 231 .then(paint) 232 .catch(function () { container.style.display = 'none'; }); 233 } 234 235 // Schede film: badge cliccabili (.mya-open[data-slug]) + auto-apertura UNA volta per utente. 236 // La scheda film (lato PHP) stampa, se esiste un verticale per quel film: 237 // <button class="mya-open" data-slug="SLUG">Guarda in verticale</button> 238 // <script>window.MYA_AUTOPLAY = 'SLUG';</script> (per l'auto-apertura una tantum) 239 function initFilmTriggers() { 240 var triggers = document.querySelectorAll('.mya-open[data-slug]'); 241 for (var i = 0; i < triggers.length; i++) { 242 (function (el) { 243 el.addEventListener('click', function (e) { e.preventDefault(); window.MYA.open(el.getAttribute('data-slug')); }); 244 })(triggers[i]); 245 } 246 if (window.MYA_AUTOPLAY) { 247 var seen = false; 248 try { seen = localStorage.getItem('mya_autoseen') === '1'; } catch (e) {} 249 if (!seen) { 250 try { localStorage.setItem('mya_autoseen', '1'); } catch (e) {} 251 setTimeout(function () { window.MYA.open(window.MYA_AUTOPLAY); }, 600); // piccolo ritardo dopo il render 252 } 253 } 254 } 255
256 function init() { 257 injectCss(); 258 var nodes = document.querySelectorAll('.lab-vshelf'); 259 for (var i = 0; i < nodes.length; i++) render(nodes[i]); 260 initFilmTriggers(); 261 } 262 263 // API globale: apri l'overlay verticale da qualsiasi pagina (es. scheda film). 264 // MYA.open('slug-del-video') -> apre /mya/?v=slug come overlay 265 window.MYA = window.MYA || { 266 open: function (slug, base) { 267 base = base || '/mya/'; 268 var sep = base.indexOf('?') > -1 ? '&' : '?'; 269 openOverlay(base + sep + 'v=' + encodeURIComponent(slug)); 270 } 271 }; 272 273 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); 274 else init(); 275})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.