1
0
Fork 0
This repository has been archived on 2025-05-26. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
Checkpoint-Golang/public/js/lv.js
2025-05-26 12:42:36 -05:00

166 lines
No EOL
17 KiB
JavaScript

const THUMBNAIL_CACHE=new Map,THUMBNAIL_REGISTRY=new Map,VIDEO_SERVICES=new Map,DEFAULT_ALLOW="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen; web-share",DEFAULT_SANDBOX="allow-scripts allow-same-origin allow-popups allow-forms allow-presentation";async function checkImage(e){if(THUMBNAIL_CACHE.has(e))return THUMBNAIL_CACHE.get(e);const t=new AbortController,n=setTimeout(()=>t.abort(),2e3);try{const o=await fetch(e,{method:"HEAD",signal:t.signal});clearTimeout(n);const s=o.ok;return THUMBNAIL_CACHE.set(e,s),s}catch{return clearTimeout(n),THUMBNAIL_CACHE.set(e,!1),!1}}function parseUrl(e){try{return new URL(e)}catch{return null}}class VideoServiceProvider{constructor(){this.name="generic"}canHandle(){return!1}getVideoId(){return null}getEmbedUrl(){return""}getThumbnailUrls(e,t,n){const s=n.getAttribute("thumbnail");return s?[s]:[]}parseParams(){return{}}getIframeAttributes(e){return{frameborder:e.getAttribute("frameborder")||"0",allow:e.getAttribute("allow")||DEFAULT_ALLOW,sandbox:e.getAttribute("sandbox")||DEFAULT_SANDBOX}}getDefaults(){return{autoload:!1}}}class YouTubeProvider extends VideoServiceProvider{constructor(){super(),this.name="youtube",this.THUMBNAIL_QUALITIES={maxres:"maxresdefault.jpg",sd:"sddefault.jpg",hq:"hqdefault.jpg",mq:"mqdefault.jpg",default:"default.jpg"},this.URL_PATTERNS=[/youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/,/youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})/,/youtu\.be\/([a-zA-Z0-9_-]{11})/]}canHandle(e){return e&&/youtube\.com|youtu\.be/.test(e)}getVideoId(e){if(!e)return null;const t=parseUrl(e);if(t){if(t.pathname.startsWith("/embed/")||t.hostname==="youtu.be"){const e=t.pathname.split("/");return e[e.length>2?2:1]}const e=t.searchParams.get("v");if(e)return e}for(const n of this.URL_PATTERNS){const t=e.match(n);if(t?.[1])return t[1]}return null}getEmbedUrl(e,t={},n){const o=n.getAttribute("no-cookie")!=="false",i=o?"youtube-nocookie.com":"youtube.com";let s=`https://www.${i}/embed/${e}?autoplay=1`;for(const[e,n]of Object.entries(t))e!=="autoplay"&&e&&n&&(s+=`&${e}=${encodeURIComponent(n)}`);return s}getThumbnailUrls(e,t,n){const i=n.getAttribute("thumbnail");if(i)return[i];const o=`https://img.youtube.com/vi/${e}`,s=[];return t&&this.THUMBNAIL_QUALITIES[t]?s.push(`${o}/${this.THUMBNAIL_QUALITIES[t]}`):window.matchMedia("(max-width: 767px)").matches?s.push(`${o}/${this.THUMBNAIL_QUALITIES.hq}`):s.push(`${o}/${this.THUMBNAIL_QUALITIES.maxres}`),s.includes(`${o}/${this.THUMBNAIL_QUALITIES.hq}`)||s.push(`${o}/${this.THUMBNAIL_QUALITIES.hq}`),s.includes(`${o}/${this.THUMBNAIL_QUALITIES.default}`)||s.push(`${o}/${this.THUMBNAIL_QUALITIES.default}`),s}parseParams(e){const t={},n=parseUrl(e);if(!n)return t;for(const[e,s]of n.searchParams.entries())t[e]=s;return(t.t||t.start)&&(t.start=t.t||t.start),t.list&&(t.playlist=t.list),t}}class BitchuteProvider extends VideoServiceProvider{constructor(){super(),this.name="bitchute",this.URL_PATTERNS=[/bitchute\.com\/video\/([a-zA-Z0-9_-]+)/,/bitchute\.com\/embed\/([a-zA-Z0-9_-]+)/]}canHandle(e){return e&&/bitchute\.com/.test(e)}getVideoId(e){if(!e)return null;const t=parseUrl(e);if(t){const e=t.pathname.split("/").filter(Boolean);for(let t=0;t<e.length-1;t++)if((e[t]==="embed"||e[t]==="video")&&t+1<e.length)return e[t+1]}for(const n of this.URL_PATTERNS){const t=e.match(n);if(t?.[1])return t[1]}return null}getEmbedUrl(e){return`https://www.bitchute.com/embed/${e}/`}getDefaults(){return{autoload:!0}}}VIDEO_SERVICES.set("youtube",new YouTubeProvider),VIDEO_SERVICES.set("bitchute",new BitchuteProvider);class LazyVideo extends HTMLElement{static get observedAttributes(){return["src","title","width","height","thumbnail-quality","no-cookie","autoload","frameborder","allow","loading","hide-title","thumbnail","service","align","container-fit"]}static get styles(){return`
:host {
--lv-aspect-ratio: 16 / 9;
display: var(--lv-display, block);
position: var(--lv-position, relative);
width: var(--lv-width, 100%);
max-width: var(--lv-max-width, 560px);
aspect-ratio: var(--lv-aspect-ratio);
background: var(--lv-background, #000);
overflow: var(--lv-overflow, hidden);
border-radius: var(--lv-border-radius, 0);
margin: var(--lv-margin, 0 auto);
}
:host([container-fit]) {
max-width: 100% !important;
max-height: auto !important;
width: 100%;
margin: 0;
}
/* Alignment control through attribute */
:host([align="left"]) { margin: var(--lv-margin-left, 0); }
:host([align="right"]) { margin: var(--lv-margin-right, 0 0 0 auto); }
:host([align="center"]) { margin: var(--lv-margin-center, 0 auto); }
/* Alignment classes for CSS variable-based alignment */
:host(.lv-align-left) { margin: var(--lv-margin-left, 0); }
:host(.lv-align-right) { margin: var(--lv-margin-right, 0 0 0 auto); }
:host(.lv-align-center) { margin: var(--lv-margin-center, 0 auto); }
:host([hide-title]), :host(:where(:not([hide-title]))) {
--lv-show-title: var(--lv-show-title, 1);
}
:host([hide-title]) [part="title-bar"] {
display: none;
}
:host([style*="height"]) { aspect-ratio: auto; }
[part="placeholder"], [part="iframe"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
}
[part="placeholder"] {
cursor: pointer;
background: var(--lv-placeholder-bg, #000);
}
[part="placeholder"]:focus {
outline: var(--lv-focus-outline, 2px solid #4285F4);
outline-offset: var(--lv-focus-outline-offset, 2px);
}
[part="thumbnail"] {
width: 100%;
height: 100%;
object-fit: var(--lv-thumbnail-object-fit, cover);
opacity: var(--lv-thumbnail-opacity, 0.85);
}
[part="placeholder"]:hover [part="thumbnail"],
[part="placeholder"]:focus [part="thumbnail"] {
opacity: var(--lv-thumbnail-hover-opacity, 1);
}
[part="title-bar"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: var(--lv-title-padding, 10px 12px);
background: var(--lv-title-bg, rgba(0, 0, 0, 0.75));
color: var(--lv-title-color, white);
font-family: var(--lv-title-font-family, Roboto, Arial, sans-serif);
font-size: var(--lv-title-font-size, 18px);
font-weight: var(--lv-title-font-weight, 500);
line-height: var(--lv-title-line-height, 1.2);
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
z-index: 2;
box-sizing: border-box;
display: var(--lv-show-title, block);
}
[part="play-button"] {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: var(--lv-play-button-width, 68px);
height: var(--lv-play-button-height, 48px);
background: var(--lv-play-button-bg, rgba(33, 33, 33, 0.8));
border-radius: var(--lv-play-button-radius, 8px);
}
[part="play-button"]::after {
content: '';
position: absolute;
top: 50%;
left: 55%;
transform: translate(-50%, -50%);
border-style: solid;
border-width: var(--lv-play-button-arrow-size, 12px 0 12px 20px);
border-color: transparent transparent transparent var(--lv-play-button-color, rgba(255, 255, 255, 0.9));
}
[part="placeholder"]:hover [part="play-button"] {
background: var(--lv-play-button-bg-hover, rgba(230, 33, 23, 1));
}
[part="timestamp"] {
position: absolute;
right: var(--lv-timestamp-right, 10px);
bottom: var(--lv-timestamp-bottom, 10px);
background: var(--lv-timestamp-bg, rgba(0, 0, 0, 0.7));
color: var(--lv-timestamp-color, white);
padding: var(--lv-timestamp-padding, 2px 6px);
border-radius: var(--lv-timestamp-radius, 3px);
font-size: var(--lv-timestamp-font-size, 12px);
font-family: var(--lv-timestamp-font-family, system-ui, sans-serif);
}
[part="iframe"] {
opacity: 0;
animation: fadeIn 0.3s ease forwards;
}
@keyframes fadeIn { to { opacity: 1; } }
[part="loading"], [part="fallback-thumbnail"] {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
width: 100%;
height: 100%;
}
[part="loading"] {
background: var(--lv-loading-bg, rgba(0,0,0,0.7));
color: var(--lv-loading-color, white);
font-family: var(--lv-loading-font-family, system-ui, sans-serif);
}
[part="fallback-thumbnail"] {
background: var(--lv-fallback-bg, #1a1a1a);
color: var(--lv-fallback-color, white);
font-family: var(--lv-fallback-font-family, system-ui, sans-serif);
font-size: var(--lv-fallback-font-size, 14px);
}
`}constructor(){super(),this.attachShadow({mode:"open"}),this._loaded=!1,this._placeholder=null,this._observer=null,this._handlers=new Map,this._videoService=null}connectedCallback(){if(!this.isConnected)return;!this._loaded&&!this._placeholder&&this._createPlaceholder(),this._getServiceOption("autoload")&&this._setupObserver(),this._updateAlignmentFromCSS(),this._setupStyleObserver()}disconnectedCallback(){this._cleanupObserver(),this._cleanupEventHandlers(),this._styleObserver&&(this._styleObserver.disconnect(),this._styleObserver=null),this._styleFrameId&&cancelAnimationFrame(this._styleFrameId)}attributeChangedCallback(e,t,n){if(!this.isConnected)return;switch(e){case"src":t!==n&&n!==null&&(this._loaded=!1,this._createPlaceholder());break;case"width":case"height":this._updateStyles();break;case"autoload":n==="true"||n===""?this._setupObserver():this._cleanupObserver();break;case"thumbnail":t!==n&&this._updateThumbnail();break;case"service":t!==n&&(this._loaded=!1,this._createPlaceholder());break}}_getServiceProvider(e){const t=this.getAttribute("service");if(t&&VIDEO_SERVICES.has(t))return VIDEO_SERVICES.get(t);if(e)for(const t of VIDEO_SERVICES.values())if(t.canHandle(e))return t;return VIDEO_SERVICES.get("youtube")}_getServiceOption(e){if(this.hasAttribute(e)){const t=this.getAttribute(e);return t===""||t==="true"||t!=="false"}return this._videoService?.getDefaults()[e]!==0[0]&&this._videoService.getDefaults()[e]}_cleanupObserver(){this._observer&&(this._observer.disconnect(),this._observer=null)}_cleanupEventHandlers(){this._handlers.forEach((e,t)=>{const[n,s]=t.split("|");n&&n.removeEventListener&&n.removeEventListener(s,e)}),this._handlers.clear()}_setupObserver(){if(!window.IntersectionObserver)return;this._cleanupObserver(),this._observer=new IntersectionObserver(e=>{e[0].isIntersecting&&!this._loaded&&(this._loadVideo(),this._cleanupObserver())},{rootMargin:"300px",threshold:.1}),this._observer.observe(this)}_updateThumbnail(){const e=this._placeholder?.querySelector('[part="thumbnail"]');if(!e)return;const t=this.getAttribute("thumbnail");if(t){e.src=t;const n=this._placeholder.querySelector('[part="fallback-thumbnail"]');n&&n.remove();return}const n=this._placeholder.dataset.videoId;if(n&&this._videoService){const s=this.getAttribute("thumbnail-quality"),t=this._videoService.getThumbnailUrls(n,s,this);t.length>0?this._loadThumbnail(t,e):this._createFallbackThumbnail()}}_createFallbackThumbnail(){if(!this._placeholder||this._placeholder.querySelector('[part="fallback-thumbnail"]'))return;const e=document.createElement("div");if(e.setAttribute("part","fallback-thumbnail"),this._videoService){const t=this._videoService.name;e.innerHTML=`
<div style="text-align: center;">
<div style="font-size: 18px; margin-bottom: 8px;">${t.charAt(0).toUpperCase()+t.slice(1)}</div>
<div>Click to play video</div>
</div>
`}else e.textContent="No thumbnail available";this._placeholder.appendChild(e)}async _createPlaceholder(){const e=this.getAttribute("src");this._videoService=this._getServiceProvider(e);const t=this._videoService?.getVideoId(e);if(!t){this.shadowRoot.innerHTML=`
<style>:host{display:block;padding:10px;color:red;background:#222;border-radius:var(--lv-border-radius,0)}</style>
<p>Error: Can't find video ID. Check the 'src' attribute.</p>
`;return}this._videoParams=this._videoService.parseParams(e);const s=this.getAttribute("title")||"Video",n=document.createElement("style");n.textContent=LazyVideo.styles;const o=this._buildPlaceholder(t,s);this.shadowRoot.innerHTML="",this.shadowRoot.append(n,o),this._updateStyles()}_buildPlaceholder(e,t){const n=document.createElement("div");n.setAttribute("part","placeholder"),n.setAttribute("role","button"),n.setAttribute("aria-label",`Play: ${t}`),n.setAttribute("tabindex","0"),n.dataset.videoId=e,n.dataset.service=this._videoService.name,this._placeholder=n;const c=this.getAttribute("thumbnail-quality"),a=this._videoService.getThumbnailUrls(e,c,this),s=document.createElement("img");if(s.setAttribute("part","thumbnail"),s.alt=`Thumbnail for ${t}`,s.loading="lazy",s.decoding="async",s.fetchPriority="low",s.style.backgroundColor="#111",n.appendChild(s),a.length>0?this._setupThumbnailObserver(s,a):this._createFallbackThumbnail(),!this.hasAttribute("hide-title")){const e=document.createElement("div");e.setAttribute("part","title-bar"),e.textContent=t,n.appendChild(e)}const r=document.createElement("div");r.setAttribute("part","play-button"),n.appendChild(r);const i=parseInt(this._videoParams.start||this._videoParams.t,10);if(!(i!=i)&&i>0){const e=document.createElement("div");e.setAttribute("part","timestamp"),e.textContent=this._formatTime(i),n.appendChild(e)}const o=e=>{(e.type==="click"||e.key==="Enter"||e.key===" ")&&(e.type!=="click"&&e.preventDefault(),this._loadVideo())};return n.addEventListener("click",o),n.addEventListener("keydown",o),this._handlers.set(`${n}|click`,o),this._handlers.set(`${n}|keydown`,o),n}_setupThumbnailObserver(e,t){if(!window.IntersectionObserver){this._loadThumbnail(t,e);return}this._thumbnailLoadAttempted=!1;const n=new IntersectionObserver(async s=>{if(s[0].isIntersecting&&!this._thumbnailLoadAttempted){this._thumbnailLoadAttempted=!0;try{await this._loadThumbnail(t,e)}catch{this._thumbnailLoadAttempted=!1}finally{n.disconnect()}}},{rootMargin:"300px",threshold:.1});n.observe(e)}async _loadThumbnail(e,t){if(e.length===1&&e[0]===this.getAttribute("thumbnail"))return t.src=e[0],!0;const o=this._placeholder?.dataset?.videoId,i=this._placeholder?.dataset?.service,n=o&&i?`${i}:${o}`:null;if(n&&THUMBNAIL_REGISTRY.has(n))try{const e=await THUMBNAIL_REGISTRY.get(n);if(e)return t.src=e,!0}catch{THUMBNAIL_REGISTRY.delete(n)}let s=null;try{const n=await Promise.all(e.map(e=>checkImage(e).then(t=>({url:e,valid:t})).catch(()=>({valid:!1})))),t=n.find(e=>e.valid);t&&(s=t.url)}catch{for(const t of e)try{if(await checkImage(t)){s=t;break}}catch{}}return s?(t.src=s,n&&THUMBNAIL_REGISTRY.set(n,Promise.resolve(s)),!0):(this._createFallbackThumbnail(),n&&THUMBNAIL_REGISTRY.set(n,Promise.resolve(null)),!1)}_formatTime(e){const t=Math.floor(e/3600),n=Math.floor(e%3600/60),s=e%60;return t>0?`${t}:${n.toString().padStart(2,"0")}:${s.toString().padStart(2,"0")}`:`${n}:${s.toString().padStart(2,"0")}`}_updateStyles(){const e=this.getAttribute("width"),t=this.getAttribute("height"),n=e=>e&&/[a-z%$]/.test(e);if(e?this.style.setProperty("width",n(e)?e:`${e}px`):this.style.removeProperty("width"),t?this.style.setProperty("height",n(t)?t:`${t}px`):this.style.removeProperty("height"),e&&t){const n=parseFloat(e),s=parseFloat(t);!(n!=n)&&!(s!=s)&&this.style.setProperty("--lv-aspect-ratio",`${n} / ${s}`)}}_loadVideo(){if(this._loaded||!this._placeholder)return;const t=document.createElement("div");t.setAttribute("part","loading"),t.textContent="Loading...",this.shadowRoot.appendChild(t);const n=this._placeholder.dataset.videoId,o=this.getAttribute("title")||"Video";if(!this._videoService){const e=this._placeholder.dataset.service;this._videoService=VIDEO_SERVICES.get(e)||VIDEO_SERVICES.get("youtube")}const i=this._videoService.getEmbedUrl(n,this._videoParams,this),e=document.createElement("iframe");e.setAttribute("part","iframe"),e.loading="lazy",e.src=i,e.title=o,e.setAttribute("credentialless","");const a=this._videoService.getIframeAttributes(this);for(const[t,n]of Object.entries(a))e.setAttribute(t,n);const s=()=>t.parentNode?.removeChild(t);e.addEventListener("load",s,{once:!0}),this._handlers.set(`${e}|load`,s),this._placeholder.replaceWith(e),this._loaded=!0,this._placeholder=null,this.dispatchEvent(new CustomEvent("video-loaded",{bubbles:!0,detail:{videoId:n,service:this._videoService.name}}))}_setupStyleObserver(){if(this._styleObserver)return;if(this._styleObserver=new MutationObserver(()=>{this._updateAlignmentFromCSS()}),this._styleObserver.observe(this,{attributes:!0,attributeFilter:["style"]}),window.getComputedStyle){let e;const t=()=>{e=requestAnimationFrame(()=>{this._updateAlignmentFromCSS(),e=requestAnimationFrame(t)})};t(),this._styleFrameId=e}}_updateAlignmentFromCSS(){if(this.hasAttribute("container-fit"))return;const t=window.getComputedStyle(this),e=t.getPropertyValue("--lv-align").trim();this.classList.remove("lv-align-left","lv-align-right","lv-align-center"),e==="left"?this.classList.add("lv-align-left"):e==="right"?this.classList.add("lv-align-right"):e==="center"&&this.classList.add("lv-align-center")}}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>customElements.define("lazy-video",LazyVideo)):customElements.define("lazy-video",LazyVideo)