1( function () { 2 'use strict'; 3 4 var STORAGE_KEY = 'wpgpyt_watch_later'; 5 6 function init() { 7 // Videos load dynamically, so watch for them 8 observeVideoList(); 9 } 10 11 // Watch for dynamically loaded videos 12 function observeVideoList() { 13 var attempts = 0; 14 var maxAttempts = 30; 15 16 // Poll for the list instead of observing (safer) 17 var checkInterval = setInterval( function () { 18 attempts++; 19 20 var list = document.querySelector( '#spidochetube_list' ); 21 if ( list && list.querySelectorAll( 'li' ).length ) { 22 setupWatchLater( list ); 23 clearInterval( checkInterval ); 24 } 25 26 // Stop after max attempts (prevent infinite loop) 27 if ( attempts >= maxAttempts ) { 28 clearInterval( checkInterval ); 29 } 30 }, 500 ); 31 } 32 33 function setupWatchLater( list ) { 34 var items = list.querySelectorAll( 'li' ); 35 36 items.forEach( function ( li ) { 37 var link = li.querySelector( 'a[data-youtubeid]' ); 38 if ( ! link ) { 39 return; 40 } 41 42 var videoId = link.getAttribute( 'data-youtubeid' ); 43 if ( ! videoId ) { 44 return; 45 } 46 47 // Avoid duplicate button 48 if ( li.querySelector( '.wpgpyt-save-btn' ) ) { 49 return; 50 } 51 52 var titleEl = li.querySelector( 'span' ); 53 var imgEl = li.querySelector( 'img' ); 54 var video = { 55 id: videoId, 56 title: titleEl ? titleEl.textContent : '', 57 thumb: imgEl ? imgEl.getAttribute( 'src' ) : '' 58 }; 59 60 var btn = document.createElement( 'button' ); 61 btn.className = 'wpgpyt-save-btn'; 62 btn.type = 'button'; 63 btn.setAttribute( 'aria-label', 'Save to Watch Later' ); 64 btn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg>'; 65 66 if ( isSaved( videoId ) ) { 67 btn.classList.add( 'wpgpyt-saved' ); 68 } 69 70 li.appendChild( btn ); 71 72 btn.addEventListener( 'click', function ( e ) { 73 e.preventDefault(); 74 e.stopPropagation(); 75 toggleSave( video, btn ); 76 } ); 77 } ); 78 79 injectSavedBar(); 80 updateCount(); 81 } 82 83 function getSaved() { 84 try { 85 return JSON.parse( localStorage.getItem( STORAGE_KEY ) ) || []; 86 } catch ( err ) { 87 return []; 88 } 89 } 90 91 function saveList( list ) { 92 try { 93 localStorage.setItem( STORAGE_KEY, JSON.stringify( list ) ); 94 } catch ( err ) {} 95 } 96 97 function isSaved( id ) { 98 return getSaved().some( function ( item ) { 99 return item.id === id; 100 } ); 101 } 102 103 function toggleSave( video, btn ) { 104 var list = getSaved(); 105 var index = -1; 106 for ( var i = 0; i < list.length; i++ ) { 107 if ( list[ i ].id === video.id ) { 108 index = i; 109 break; 110 } 111 } 112 113 if ( index === -1 ) { 114 list.push( video ); 115 btn.classList.add( 'wpgpyt-saved' ); 116 showToast( 'Saved to Watch Later' ); 117 } else { 118 list.splice( index, 1 ); 119 btn.classList.remove( 'wpgpyt-saved' ); 120 showToast( 'Removed from Watch Later' ); 121 } 122 123 saveList( list ); 124 updateCount(); 125 } 126 127 // === Saved Bar === 128 function injectSavedBar() { 129 var wrapper = document.querySelector( '#wpgpyt-wrapper' ); 130 if ( ! wrapper ) { 131 return; 132 } 133 134 if ( document.querySelector( '.wpgpyt-saved-bar' ) ) { 135 return; 136 } 137 138 var bar = document.createElement( 'div' ); 139 bar.className = 'wpgpyt-saved-bar'; 140 bar.innerHTML = 141 '<button class="wpgpyt-saved-count-btn" type="button">' + 142 '<svg viewBox="0 0 24 24"><path d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg>' + 143 '<span class="wpgpyt-saved-count-text">Saved (0)</span>' + 144 '</button>'; 145 146 wrapper.parentNode.insertBefore( bar, wrapper ); 147 148 bar.querySelector( '.wpgpyt-saved-count-btn' ).addEventListener( 'click', function () { 149 var count = getSaved().length; 150 if ( count === 0 ) { 151 showToast( 'No saved videos yet' ); 152 } else { 153 showToast( 'You have ' + count + ' saved video' + ( count > 1 ? 's' : '' ) ); 154 } 155 } ); 156 } 157 158 function updateCount() { 159 var text = document.querySelector( '.wpgpyt-saved-count-text' ); 160 if ( text ) { 161 text.textContent = 'Saved (' + getSaved().length + ')'; 162 } 163 } 164 165 // === Toast === 166 var toastTimer = null; 167 function showToast( message ) { 168 var toast = document.querySelector( '.wpgpyt-toast' ); 169 if ( ! toast ) { 170 toast = document.createElement( 'div' ); 171 toast.className = 'wpgpyt-toast'; 172 document.body.appendChild( toast ); 173 } 174 toast.textContent = message; 175 void toast.offsetWidth; 176 toast.classList.add( 'wpgpyt-toast-show' ); 177 clearTimeout( toastTimer ); 178 toastTimer = setTimeout( function () { 179 toast.classList.remove( 'wpgpyt-toast-show' ); 180 }, 2500 ); 181 } 182 183 // === DOM Ready === 184 if ( document.readyState === 'loading' ) { 185 document.addEventListener( 'DOMContentLoaded', init ); 186 } else { 187 init(); 188 } 189} )(); 190 191 192// ===== Video Search Feature ===== 193( function () { 194 'use strict'; 195 196 function initSearch() { 197 var attempts = 0; 198 var maxAttempts = 30; 199 200 var checkInterval = setInterval( function () { 201 attempts++; 202 203 var list = document.querySelector( '#spidochetube_list' ); 204 if ( list && list.querySelectorAll( 'li' ).length ) { 205 setupSearch( list ); 206 clearInterval( checkInterval ); 207 } 208 209 if ( attempts >= maxAttempts ) { 210 clearInterval( checkInterval ); 211 } 212 }, 500 ); 213 } 214 215 function setupSearch( list ) { 216 // Avoid duplicate search bar 217 if ( document.querySelector( '.wpgpyt-search-bar' ) ) { 218 return; 219 } 220 221 var wrapper = document.querySelector( '#wpgpyt-wrapper' ); 222 if ( ! wrapper ) { 223 return; 224 } 225 226 // Create search bar 227 var searchBar = document.createElement( 'div' ); 228 searchBar.className = 'wpgpyt-search-bar'; 229 searchBar.innerHTML = 230 '<input type="text" class="wpgpyt-search-input" placeholder="Search videos..." aria-label="Search videos">' +
231 '<svg class="wpgpyt-search-icon" viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>' + 232 '<button class="wpgpyt-search-clear" type="button" aria-label="Clear search">×</button>'; 233 234 // Insert before wrapper (or after saved bar if exists) 235 var savedBar = document.querySelector( '.wpgpyt-saved-bar' ); 236 if ( savedBar ) { 237 savedBar.parentNode.insertBefore( searchBar, savedBar.nextSibling ); 238 } else { 239 wrapper.parentNode.insertBefore( searchBar, wrapper ); 240 } 241 242 // Create "no results" message 243 var noResults = document.createElement( 'div' ); 244 noResults.className = 'wpgpyt-no-results'; 245 noResults.textContent = 'No videos found'; 246 wrapper.parentNode.insertBefore( noResults, wrapper.nextSibling ); 247 248 var input = searchBar.querySelector( '.wpgpyt-search-input' ); 249 var clearBtn = searchBar.querySelector( '.wpgpyt-search-clear' ); 250 251 // Search on input 252 input.addEventListener( 'input', function () { 253 var query = input.value.toLowerCase().trim(); 254 255 if ( query ) { 256 searchBar.classList.add( 'wpgpyt-has-text' ); 257 } else { 258 searchBar.classList.remove( 'wpgpyt-has-text' ); 259 } 260 261 filterVideos( list, query, noResults ); 262 } ); 263 264 // Clear button 265 clearBtn.addEventListener( 'click', function () { 266 input.value = ''; 267 searchBar.classList.remove( 'wpgpyt-has-text' ); 268 filterVideos( list, '', noResults ); 269 input.focus(); 270 } ); 271 } 272 273 function filterVideos( list, query, noResults ) { 274 var items = list.querySelectorAll( 'li' ); 275 var visibleCount = 0; 276 277 items.forEach( function ( li ) { 278 var titleEl = li.querySelector( 'span' ); 279 var title = titleEl ? titleEl.textContent.toLowerCase() : ''; 280 281 if ( ! query || title.indexOf( query ) !== -1 ) { 282 li.classList.remove( 'wpgpyt-filtered' ); 283 visibleCount++; 284 } else { 285 li.classList.add( 'wpgpyt-filtered' ); 286 } 287 } ); 288 289 // Show/hide no results message 290 if ( visibleCount === 0 && query ) { 291 noResults.classList.add( 'wpgpyt-show' ); 292 } else { 293 noResults.classList.remove( 'wpgpyt-show' ); 294 } 295 } 296 297 if ( document.readyState === 'loading' ) { 298 document.addEventListener( 'DOMContentLoaded', initSearch ); 299 } else { 300 initSearch(); 301 } 302} )(); 303 304 305// ===== Sticky Player Feature ===== 306( function () { 307 'use strict'; 308 309 function initSticky() { 310 var attempts = 0; 311 var maxAttempts = 30; 312 313 var checkInterval = setInterval( function () { 314 attempts++; 315 316 var player = document.querySelector( '#spidochetube_player' ); 317 if ( player && player.querySelector( 'iframe' ) ) { 318 setupSticky( player ); 319 clearInterval( checkInterval ); 320 } 321 322 if ( attempts >= maxAttempts ) { 323 clearInterval( checkInterval ); 324 } 325 }, 500 ); 326 } 327 328 function setupSticky( player ) { 329 // Avoid duplicate setup 330 if ( player.getAttribute( 'data-wpgpyt-sticky-ready' ) ) { 331 return; 332 } 333 player.setAttribute( 'data-wpgpyt-sticky-ready', '1' ); 334 335 // Add close button 336 var closeBtn = document.createElement( 'button' ); 337 closeBtn.className = 'wpgpyt-sticky-close'; 338 closeBtn.type = 'button'; 339 closeBtn.setAttribute( 'aria-label', 'Close sticky player' ); 340 closeBtn.innerHTML = '×'; 341 player.appendChild( closeBtn ); 342 343 // Add LIVE label 344 var label = document.createElement( 'span' ); 345 label.className = 'wpgpyt-sticky-label'; 346 label.textContent = 'Playing'; 347 player.appendChild( label ); 348 349 // Create placeholder (prevents layout jump when sticky) 350 var placeholder = document.createElement( 'div' ); 351 placeholder.className = 'wpgpyt-player-placeholder'; 352 player.parentNode.insertBefore( placeholder, player ); 353 354 var stickyDismissed = false; 355 356 // Close button handler 357 closeBtn.addEventListener( 'click', function () { 358 player.classList.remove( 'wpgpyt-sticky' ); 359 placeholder.classList.remove( 'wpgpyt-active' ); 360 stickyDismissed = true; 361 } ); 362 363 // Scroll handler 364 var ticking = false; 365 window.addEventListener( 'scroll', function () { 366 if ( ! ticking ) { 367 window.requestAnimationFrame( function () { 368 handleScroll( player, placeholder, stickyDismissed ); 369 ticking = false; 370 } ); 371 ticking = true; 372 } 373 } ); 374 375 // Reset dismiss when a new video is clicked 376 var list = document.querySelector( '#spidochetube_list' ); 377 if ( list ) { 378 list.addEventListener( 'click', function () { 379 stickyDismissed = false; 380 } ); 381 } 382 } 383 384 function handleScroll( player, placeholder, stickyDismissed ) { 385 if ( stickyDismissed ) { 386 return; 387 } 388 389 // Use placeholder position (since player may be fixed) 390 var reference = placeholder.classList.contains( 'wpgpyt-active' ) ? placeholder : player; 391 var rect = reference.getBoundingClientRect(); 392 393 // When player scrolled above viewport top 394 if ( rect.top < -50 && ! player.classList.contains( 'wpgpyt-sticky' ) ) { 395 // Set placeholder height to match player (prevent jump) 396 placeholder.style.height = player.offsetHeight + 'px'; 397 placeholder.classList.add( 'wpgpyt-active' ); 398 player.classList.add( 'wpgpyt-sticky' ); 399 } else if ( player.classList.contains( 'wpgpyt-sticky' ) ) { 400 // Check placeholder position to decide un-sticky 401 var phRect = placeholder.getBoundingClientRect(); 402 if ( phRect.top >= -50 ) { 403 player.classList.remove( 'wpgpyt-sticky' ); 404 placeholder.classList.remove( 'wpgpyt-active' ); 405 placeholder.style.height = ''; 406 } 407 } 408 } 409 410 if ( document.readyState === 'loading' ) { 411 document.addEventListener( 'DOMContentLoaded', initSticky ); 412 } else { 413 initSticky(); 414 } 415} )();
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.