1(function () { 2 'use strict'; 3 4 function posterPolicy(source, base, manifest) { 5 const src = String(source || '').trim(); 6 if (!/^\/media\/[A-Za-z0-9][A-Za-z0-9._-]*\.(avif|webp|png|jpe?g)$/i.test(src) || src.includes('..')) return null; 7 const policy = {src, ink: !!base && src === '/media/' + base + '-poster.avif'}; 8 const entry = policy.ink && manifest && Object.hasOwn(manifest, base) && manifest[base]; 9 if (!entry || entry.src !== src || !Number.isSafeInteger(entry.width) || entry.width < 1 || entry.width > 8000 || !Array.isArray(entry.candidates)) return policy; 10 const candidates = new Map(); 11 for (const item of entry.candidates.slice(0, 3)) { 12 if (!item || !Number.isSafeInteger(item.width) || item.width < 1 || item.width >= entry.width) continue; 13 const role = ['sm','md','lg'].find(role => item.src === '/media/' + base + '-poster-' + role + '.avif'); 14 if (!role || item.width > {sm:400,md:800,lg:1200}[role]) continue; 15 candidates.set(item.width, item.src); 16 } 17 if (candidates.size) { 18 candidates.set(entry.width, src); 19 policy.srcset = [...candidates].sort((a,b) => a[0]-b[0]).map(([width,url]) => url + ' ' + width + 'w').join(', '); 20 } 21 return policy; 22 } 23 24 function bindImageSource(img, policy, lazy, onFailure) { 25 let fallback = !policy.srcset; 26 let disposed = false; 27 let terminal = false;
28 const failed = () => { 29 if (disposed || terminal) return; 30 if (!fallback) { 31 fallback = true; 32 img.removeAttribute('srcset'); 33 img.removeAttribute('sizes'); 34 img.src = policy.src; 35 return; 36 } 37 terminal = true; 38 onFailure(); 39 }; 40 img.addEventListener('error', failed); 41 if (policy.srcset) { 42 img.sizes = lazy ? '(max-width: 600px) 64px, 88px' : '100vw'; 43 img.srcset = policy.srcset; 44 } 45 img.src = policy.src; 46 return {dispose() { 47 if (disposed) return; 48 disposed = true; 49 img.removeEventListener('error', failed); 50 }}; 51 } 52 53 function bindPreviewLifecycle(media, setVisible) { 54 const hide = () => setVisible(false); 55 const show = () => setVisible(true); 56 media.addEventListener('play', hide); 57 media.addEventListener('playing', hide); 58 media.addEventListener('ended', show); 59 return { 60 seek: hide, 61 dispose() { 62 media.removeEventListener('play', hide); 63 media.removeEventListener('playing', hide); 64 media.removeEventListener('ended', show); 65 } 66 }; 67 } 68 69 if (typeof module === 'object' && module.exports) { 70 module.exports = {posterPolicy, bindPreviewLifecycle, bindImageSource}; 71 return; 72 } 73 if (location.hostname !== 'wudang-academy.aioengine.io') return; 74 if (document.getElementById('wudang-midpoint-preview-style')) return; 75 76 const style = document.createElement('style'); 77 style.id = 'wudang-midpoint-preview-style'; 78 style.textContent = ` 79 .sp-ep-preview{display:block;position:relative;flex:0 0 88px;width:88px;aspect-ratio:16/9;overflow:hidden;border-radius:8px;background:#181712} 80 .sp-ep-preview img{display:block;width:100%;height:100%;object-fit:contain} 81 .sp-ep-preview[hidden]{display:none!important} 82 .sp-ep-header:has(.sp-ep-preview){gap:12px} 83 .sp-ep-header:has(.sp-ep-preview) h3{min-width:0;flex:1;overflow-wrap:anywhere} 84 .wudang-ink-preview{background:#302b20} 85 .wudang-ink-preview img{filter:sepia(.55) saturate(.68) contrast(1.06)} 86 .wudang-ink-preview:after{content:'';position:absolute;inset:0;pointer-events:none;box-shadow:inset 0 0 0 1px #b8860b66,inset 0 0 20px #2d201b22;background:linear-gradient(135deg,#dac59822,transparent 38%,transparent 70%,#3e312122)} 87 .mp-screen>.mp-midpoint-preview{position:absolute;inset:0;z-index:1;pointer-events:none;overflow:hidden} 88 .mp-midpoint-preview img{width:100%;height:100%;display:block;object-fit:contain} 89 .mp-midpoint-preview[hidden]{display:none!important} 90 .mp-preview-visible .mp-subtitle{visibility:hidden} 91 .sp-ep-progress[hidden]{display:none!important} 92 @media(max-width:600px){.sp-ep-preview{flex-basis:64px;width:64px;border-radius:6px}.sp-ep-header:has(.sp-ep-preview){gap:9px}.sp-ep-header:has(.sp-ep-preview) h3{font-size:15px;line-height:1.3}.sp-ep-header .sp-ep-progress{min-width:0;padding:2px 7px}.sp-ep-header .sp-ep-toggle{flex-shrink:0}} 93 `; 94 document.head.appendChild(style); 95 const bindings = new Map(); 96 const headerBindings = new Map(); 97 const manifest = window.WUDANG_POSTER_CANDIDATES_V1; 98 99 function previewImage(policy, className, lazy) { 100 const frame = document.createElement('span'); 101 frame.className = className + (policy.ink ? ' wudang-ink-preview' : ''); 102 frame.setAttribute('aria-hidden', 'true'); 103 const img = document.createElement('img'); 104 img.alt = ''; 105 img.decoding = 'async'; 106 img.loading = lazy ? 'lazy' : 'eager'; 107 frame.appendChild(img); 108 return {frame, img}; 109 } 110 111 function reconcile() { 112 for (const [img, binding] of headerBindings) { 113 if (!img.isConnected || !binding.owner.isConnected || binding.owner.dataset.mpPoster !== binding.source || binding.owner.dataset.mpSubtitleBase !== binding.base) { 114 binding.dispose(); binding.frame.remove(); headerBindings.delete(img); 115 } 116 } 117 for (const [video, binding] of bindings) { 118 if (!video.isConnected || !binding.active()) {binding.dispose(); bindings.delete(video);} 119 } 120 document.querySelectorAll('.sp-episode').forEach(episode => { 121 const holder = episode.querySelector('[data-mp-mode="video"]'); 122 if (!holder) return; 123 const policy = posterPolicy(holder.dataset.mpPoster, holder.dataset.mpSubtitleBase, manifest); 124 if (!policy) return; 125 const header = episode.querySelector('.sp-ep-header'); 126 if (header && !header.querySelector('.sp-ep-preview')) { 127 const thumbnail = previewImage(policy, 'sp-ep-preview', true); 128 const imageBinding = bindImageSource(thumbnail.img, policy, true, () => {thumbnail.frame.hidden = true;}); 129 headerBindings.set(thumbnail.img, {owner:holder, source:holder.dataset.mpPoster, base:holder.dataset.mpSubtitleBase, frame:thumbnail.frame, dispose:imageBinding.dispose}); 130 header.prepend(thumbnail.frame); 131 } 132 const video = holder.querySelector('video'); 133 const screen = holder.querySelector('.mp-screen'); 134 if (!video || !screen || bindings.has(video)) return; 135 // Player teardown clones its DOM. Only the registered live media instance 136 // may own a preview; inherited overlays are not new players. 137 const posterSource = holder.dataset.mpPoster; 138 const posterBase = holder.dataset.mpSubtitleBase; 139 const active = () => typeof getMediaPlayer === 'function' && getMediaPlayer(holder.id)?.media === video && holder.dataset.mpPoster === posterSource && holder.dataset.mpSubtitleBase === posterBase; 140 if (!active()) return; 141 screen.querySelectorAll('.mp-midpoint-preview').forEach(frame => frame.remove()); 142 const preview = previewImage(policy, 'mp-midpoint-preview', false); 143 preview.frame.hidden = true; 144 let ready = false;
145 let disposed = false; 146 let wanted = video.paused; 147 const render = () => { 148 if (disposed) return; 149 const visible = ready && wanted; 150 preview.frame.hidden = !visible; 151 holder.classList.toggle('mp-preview-visible', visible); 152 }; 153 const binding = bindPreviewLifecycle(video, visible => {wanted = visible; render();}); 154 const loaded = () => {ready = true; render();}; 155 const failed = () => {ready = false; render();}; 156 preview.img.addEventListener('load', loaded, {once:true}); 157 const imageBinding = bindImageSource(preview.img, policy, false, failed); 158 screen.appendChild(preview.frame); 159 if (preview.img.complete && preview.img.naturalWidth > 0) {ready = true; render();} 160 const seek = event => { 161 if (event.target.closest('.mp-wave,.mp-progress,.mp-skip')) binding.seek(); 162 }; 163 holder.addEventListener('pointerdown', seek); 164 holder.addEventListener('keydown', seek); 165 bindings.set(video, {active, dispose() { 166 disposed = true; 167 binding.dispose(); 168 preview.img.removeEventListener('load', loaded); 169 imageBinding.dispose(); 170 holder.removeEventListener('pointerdown', seek); 171 holder.removeEventListener('keydown', seek); 172 holder.classList.remove('mp-preview-visible'); 173 preview.frame.remove(); 174 }}); 175 }); 176 } 177 const root = document.getElementById('app'); 178 if (!root) return; 179 let scheduled = false; 180 const observer = new MutationObserver(() => { 181 if (scheduled) return; 182 scheduled = true; 183 queueMicrotask(() => {scheduled = false; reconcile();}); 184 }); 185 observer.observe(root, {childList:true, subtree:true, attributes:true, attributeFilter:['data-mp-poster','data-mp-subtitle-base']}); 186 reconcile(); 187 window.addEventListener('pagehide', () => { 188 observer.disconnect(); 189 for (const binding of bindings.values()) binding.dispose(); 190 bindings.clear(); 191 for (const binding of headerBindings.values()) {binding.dispose(); binding.frame.remove();} 192 headerBindings.clear(); 193 }); 194 window.addEventListener('pageshow', event => { 195 if (event.persisted) {observer.observe(root, {childList:true, subtree:true, attributes:true, attributeFilter:['data-mp-poster','data-mp-subtitle-base']}); reconcile();} 196 }); 197})();
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.