1(function ($) { 2 ("use strict"); 3 4 $(document).ready(function () { 5 initYouTubeGallery(); 6 }); 7 8 function initYouTubeGallery() { 9 // Helper for parsing JSON data 10 function parseMaybeJSON(val, fallback) { 11 if (typeof val === "string") { 12 try { 13 return JSON.parse(val); 14 } catch (e) { 15 return fallback; 16 } 17 } 18 return typeof val === "object" && val !== null ? val : fallback; 19 } 20 21 // Helper to truncate text responsively 22 function truncateText(text, length) { 23 if (!text) return ""; 24 const width = $(window).width(); 25 let maxLength; 26 if (width < 600) { 27 maxLength = length.sm || length.lg; 28 } else if (width < 900) { 29 maxLength = length.md || length.lg; 30 } else { 31 maxLength = length.lg; 32 } 33 return text.length > maxLength 34 ? text.substring(0, maxLength) + "..." 35 : text; 36 } 37 38 // Helper to sort videos 39 function sortVideos(videos, sortBy) { 40 const sorted = [...videos]; 41 switch (sortBy) { 42 case "title": 43 return sorted.sort((a, b) => 44 a.title.localeCompare(b.title, undefined, { 45 sensitivity: "base", 46 }) 47 ); 48 case "latest": 49 return sorted.sort( 50 (a, b) => 51 new Date(b.publishedAt).getTime() - 52 new Date(a.publishedAt).getTime() 53 ); 54 case "date": 55 return sorted.sort( 56 (a, b) => 57 new Date(a.publishedAt).getTime() - 58 new Date(b.publishedAt).getTime() 59 ); 60 case "popular": 61 return sorted.sort((a, b) => (b.viewCount || 0) - (a.viewCount || 0)); 62 default: 63 return videos; 64 } 65 } 66 67 // Extract playlist ID from URL or direct ID, and handle channel URLs/handles 68 function getPlaylistId(input) { 69 if (!input) return ""; 70 try { 71 const url = new URL(input); 72 if (url.hostname === "www.youtube.com") { 73 if (url.pathname.startsWith("/channel/")) { 74 return url.pathname.split("/channel/")[1]; 75 } else if (url.pathname.startsWith("/@")) { 76 return url.pathname.substring(2); // Return handle for later resolution 77 } else if (url.searchParams.get("list")) { 78 return url.searchParams.get("list"); 79 } 80 } 81 return input; 82 } catch (e) { 83 return input; 84 } 85 } 86 87 // Resolve handle to channel ID using YouTube API 88 function resolveHandleToChannelId(handle, apiKey, callback) { 89 $.get( 90 `https://www.googleapis.com/youtube/v3/search?part=snippet&q=${encodeURIComponent( 91 handle 92 )}&type=channel&key=${apiKey}` 93 ) 94 .done(function (data) { 95 if (data.items && data.items.length > 0) { 96 callback(data.items[0].snippet.channelId); 97 } else { 98 callback(null); 99 } 100 }) 101 .fail(function () { 102 callback(null); 103 }); 104 } 105 106 // Helper function to generate video player iframe 107 function generatePlayerIframe(videoId, config) { 108 const params = [ 109 `autoplay=${config.autoplay ? "1" : "0"}`, 110 `loop=${config.loop ? "1" : "0"}`, 111 `mute=${config.mute ? "1" : "0"}`, 112 `controls=${config.showPlayerControl ? "1" : "0"}`, 113 `modestbranding=${config.hideYoutubeLogo ? "1" : "0"}`, 114 config.loop ? `playlist=${videoId}` : null, 115 ] 116 .filter(Boolean) 117 .join("&"); 118 119 return ` 120 <div class="ultp-ytg-video-wrapper"> 121 <iframe 122 src="https://www.youtube.com/embed/${videoId}?${params}" 123 title="YouTube Video" 124 frameborder="0" 125 allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" 126 allowfullscreen 127 ></iframe> 128 </div> 129 `; 130 } 131 132 // Generate text content (title and description) 133 function getYoutubeTextContent( 134 enableTitle, 135 title, 136 titleLength, 137 enableDesc, 138 description, 139 descriptionLength, 140 videoId 141 ) { 142 let html = '<div class="ultp-ytg-content">'; 143 if (enableTitle) { 144 html += `<div class="ultp-ytg-title"><a href="https://www.youtube.com/watch?v=${videoId}" target="_blank" rel="noopener noreferrer">${truncateText( 145 title, 146 titleLength 147 )}</a></div>`; 148 } 149 if (enableDesc) { 150 html += `<div class="ultp-ytg-description">${truncateText( 151 description, 152 descriptionLength 153 )}</div>`; 154 } 155 html += "</div>"; 156 return html; 157 } 158 159 // Initialize YouTube Gallery 160 $(".wp-block-ultimate-post-youtube-gallery").each(function () { 161 const $block = $(this); 162 const $wrapper = $block.find(".ultp-block-wrapper"); 163 let $container = $block.find(".ultp-ytg-view-grid, .ultp-ytg-container"); 164 const $loadMoreBtn = $block.find(".ultp-ytg-loadmore-btn"); 165 166 // Get configuration from data attributes 167 const config = { 168 playlistIdOrUrl: $block.data("playlist") || "", 169 apiKey: $block.data("api-key") || "", 170 cacheDuration: parseInt($block.data("cache-duration")) || 0,
171 sortBy: $block.data("sort-by") || "date", 172 galleryLayout: $block.data("gallery-layout") || "grid", 173 videosPerPage: parseMaybeJSON($block.data("videos-per-page"), { 174 lg: 9, 175 md: 6, 176 sm: 3, 177 }), 178 showVideoTitle: $block.data("show-video-title") == "1", 179 videoTitleLength: parseMaybeJSON($block.data("video-title-length"), { 180 lg: 50, 181 md: 50, 182 sm: 50, 183 }), 184 loadMoreEnable: $block.data("load-more-enable") == "1", 185 moreButtonLabel: $block.data("more-button-label") || "More Videos", 186 autoplay: $block.data("autoplay") == "1", 187 loop: $block.data("loop") == "1", 188 mute: $block.data("mute") == "1", 189 showPlayerControl: $block.data("show-player-control") == "1", 190 hideYoutubeLogo: $block.data("hide-youtube-logo") == "1", 191 showDescription: $block.data("show-description") == "1", 192 videoDescriptionLength: parseMaybeJSON( 193 $block.data("video-description-length"), 194 { 195 lg: 100, 196 md: 100, 197 sm: 100, 198 } 199 ), 200 imageHeightRatio: $block.data("image-height-ratio") || "16-9", 201 galleryColumn: parseMaybeJSON($block.data("gallery-column"), { 202 lg: 3, 203 md: 2, 204 sm: 1, 205 }), 206 displayType: $block.data("display-type") || "grid", 207 enableListView: $block.data("enable-list-view") == "1", 208 enableIconAnimation: $block.data("enable-icon-animation") == "1", 209 defaultYoutubeIcon: $block.data("enable-youtube-icon") == "1", 210 imgHeight: $block.data("img-height"), 211 }; 212 213 let playlistId = getPlaylistId(config.playlistIdOrUrl); 214 215 // If it's a handle (starts with @), resolve to channel ID 216 if (playlistId.startsWith("@")) { 217 const handle = playlistId.substring(1); 218 resolveHandleToChannelId(handle, config.apiKey, function (channelId) { 219 if (channelId) { 220 playlistId = channelId; 221 proceedWithPlaylist(playlistId); 222 } else { 223 $wrapper.html( 224 '<p style="color:#888">Invalid handle or API key.</p>' 225 ); 226 } 227 }); 228 } else { 229 proceedWithPlaylist(playlistId); 230 } 231 232 function proceedWithPlaylist(playlistId) { 233 // Convert channel ID to uploads playlist ID 234 if (playlistId.startsWith("UC")) { 235 playlistId = "UU" + playlistId.substring(2); 236 } 237 238 if (!playlistId || !config.apiKey) { 239 $wrapper.html( 240 '<p style="color:#888">Please provide both YouTube playlist ID/URL and API key.</p>' 241 ); 242 return; 243 } 244 245 // State management 246 let allVideos = []; 247 let shownCount = config.videosPerPage.lg || 9; 248 let activeVideo = null; 249 let playingId = null; 250 251 // Update responsive counts 252 function updateResponsiveCounts() { 253 const width = $(window).width(); 254 if (width < 600) { 255 shownCount = Math.max(shownCount, config.videosPerPage.sm || 3); 256 } else if (width < 900) { 257 shownCount = Math.max(shownCount, config.videosPerPage.md || 6); 258 } else { 259 shownCount = Math.max(shownCount, config.videosPerPage.lg || 9); 260 } 261 } 262 263 // Render videos for playlist layout 264 function renderPlaylistLayout(videos) { 265 if (!videos.length) { 266 $container.html("<p>No videos found in this playlist.</p>"); 267 return; 268 } 269 270 if (!activeVideo) { 271 activeVideo = videos[0]; 272 } 273 274 let html = '<div class="ultp-ytg-main">'; 275 // Generate player iframe and place .ultp-ytg-content inside .ultp-ytg-video-wrapper 276 const playerWrapper = ` 277 <div class="ultp-ytg-video-wrapper"> 278 <iframe 279 src="https://www.youtube.com/embed/${activeVideo.videoId}?${[ 280 `autoplay=${config.autoplay ? "1" : "0"}`, 281 `loop=${config.loop ? "1" : "0"}`, 282 `mute=${config.mute ? "1" : "0"}`, 283 `controls=${config.showPlayerControl ? "1" : "0"}`, 284 `modestbranding=${config.hideYoutubeLogo ? "1" : "0"}`, 285 config.loop ? `playlist=${activeVideo.videoId}` : null, 286 ] 287 .filter(Boolean) 288 .join("&")}" 289 title="YouTube Video" 290 frameborder="0" 291 allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" 292 allowfullscreen 293 ></iframe> 294 ${getYoutubeTextContent( 295 config.showVideoTitle, 296 activeVideo.title, 297 config.videoTitleLength, 298 config.showDescription, 299 activeVideo.description, 300 config.videoDescriptionLength, 301 activeVideo.videoId 302 )} 303 </div> 304 `; 305 html += playerWrapper; 306 html += "</div>"; 307 308 html += '<div class="ultp-ytg-playlist-sidebar">'; 309 html += '<div class="ultp-ytg-playlist-items">'; 310 videos.forEach(function (video) { 311 const isActive = video.videoId === activeVideo.videoId; 312 html += ` 313 <div class="ultp-ytg-playlist-item ${ 314 isActive ? "active" : "" 315 }" data-video-id="${video.videoId}"> 316 <img src="${video.thumbnail}" alt="${video.title}" loading="lazy" /> 317 <div class="ultp-ytg-playlist-item-content"> 318 <div class="ultp-ytg-playlist-item-title"> 319 ${truncateText(video.title, config.videoTitleLength)} 320 </div> 321 </div> 322 </div> 323 `; 324 }); 325 html += "</div></div>"; 326 327 $container.html(html); 328 } 329 330 // Render videos for grid/list layout
331 function renderGridLayout(videos, count) { 332 if (!videos.length) { 333 $container.html("<p>No videos found in this playlist.</p>"); 334 return; 335 } 336 337 const displayedVideos = videos.slice(0, count); 338 let html = ""; 339 displayedVideos.forEach(function (video) { 340 const isPlaying = playingId === video.videoId; 341 html += `<div class="ultp-ytg-item${isPlaying ? " active" : ""}">`; 342 html += `<div class="ultp-ytg-video">`; 343 344 if (isPlaying) { 345 html += generatePlayerIframe(video.videoId, config); 346 } else { 347 // enableIconAnimation 348 const getSvgIcon = $(".ultp-ytg-play__icon").html(); 349 html += ` 350 <img src="${video.thumbnail}" alt="${ 351 video.title 352 }" loading="lazy" data-video-id="${ 353 video.videoId 354 }" style="cursor:pointer;" /> 355 <div class="ultp-ytg-play__icon${ 356 config.enableIconAnimation ? " ytg-icon-animation" : "" 357 }"> 358 ${getSvgIcon} 359 </div> 360 `; 361 } 362 363 html += `</div>`; 364 html += `<div class="ultp-ytg-inside">`; 365 html += getYoutubeTextContent( 366 config.showVideoTitle, 367 video.title, 368 config.videoTitleLength, 369 config.showDescription, 370 video.description, 371 config.videoDescriptionLength, 372 video.videoId 373 ); 374 html += `</div></div>`; 375 }); 376 377 $container.html(html); 378 379 // Update load more button visibility 380 if (config.loadMoreEnable && count < videos.length) { 381 $loadMoreBtn.show(); 382 } else { 383 $loadMoreBtn.hide(); 384 } 385 } 386 387 // Render based on layout type 388 function renderVideos(videos, count) { 389 if (config.galleryLayout === "playlist") { 390 renderPlaylistLayout(videos); 391 } else { 392 renderGridLayout(videos, count); 393 } 394 } 395 396 // Fetch videos from YouTube API 397 const cacheKey = `ultp_youtube_gallery_${playlistId}_${config.apiKey}_${config.sortBy}_${config.imgHeight}`; 398 const duration = config.cacheDuration; 399 let cached = null; 400 401 try { 402 cached = JSON.parse(localStorage.getItem(cacheKey)); 403 } catch (e) { 404 cached = null; 405 } 406 407 const now = Date.now(); 408 if ( 409 cached && 410 cached.data && 411 cached.timestamp && 412 duration > 0 && 413 now - cached.timestamp < duration * 1000 414 ) { 415 allVideos = sortVideos(cached.data, config.sortBy); 416 renderVideos(allVideos, shownCount); 417 } else { 418 // Only show loading skeleton for grid/list layouts, not for playlist layout 419 if (config.galleryLayout !== "playlist") { 420 $container.html(` 421 <div class="ultp-ytg-loading gallery-postx gallery-active"> 422 <div class="skeleton-box"></div> 423 <div class="skeleton-box"></div> 424 <div class="skeleton-box"></div> 425 <div class="skeleton-box"></div> 426 <div class="skeleton-box"></div> 427 <div class="skeleton-box"></div> 428 </div> 429 `); 430 } else { 431 $container.html(` 432 <div class="ultp-ytg-loading ultp-ytg-playlist-loading"> 433 <div class="ytg-loader"></div> 434 </div>`); 435 } 436 437 $.get("https://www.googleapis.com/youtube/v3/playlistItems", { 438 part: "snippet", 439 maxResults: 50, 440 playlistId: playlistId, 441 key: config.apiKey, 442 }) 443 .done(function (data) { 444 setTimeout(function () { 445 $container.empty(); // Remove loading after 3s 446 if (data.error) { 447 $container.html( 448 `<div class="ultp-ytg-error">${ 449 data.error.message || "Failed to fetch playlist." 450 }</div>` 451 ); 452 return; 453 } 454 455 const videos = (data.items || []) 456 .filter(function (item) { 457 return ( 458 item.snippet.title !== "Private video" && 459 item.snippet.title !== "Deleted video" 460 ); 461 }) 462 .map(function (item) { 463 return { 464 videoId: item.snippet.resourceId.videoId, 465 title: item.snippet.title, 466 thumbnail: 467 (item.snippet.thumbnails && 468 item.snippet.thumbnails[config.imgHeight] && 469 item.snippet.thumbnails[config.imgHeight].url) || 470 item.snippet.thumbnails[config.imgHeight].url || 471 item.snippet.thumbnails?.medium?.url || 472 "", 473 publishedAt: item.snippet.publishedAt || "", 474 description: item.snippet.description || "", 475 viewCount: 0, // Initialize for popular sorting 476 }; 477 }); // If sorting by popular, fetch view counts 478 if (config.sortBy === "popular") { 479 const videoIds = videos.map((v) => v.videoId).join(","); 480 $.get( 481 `https://www.googleapis.com/youtube/v3/videos?part=statistics&id=${videoIds}&key=${config.apiKey}` 482 ) 483 .done(function (statsData) { 484 if (statsData.items) { 485 const statsMap = {};
486 statsData.items.forEach(function (item) { 487 statsMap[item.id] = item.statistics.viewCount; 488 }); 489 videos.forEach(function (v) { 490 v.viewCount = parseInt(statsMap[v.videoId] || 0); 491 }); 492 } 493 allVideos = sortVideos(videos, config.sortBy); 494 if (duration > 0) { 495 try { 496 localStorage.setItem( 497 cacheKey, 498 JSON.stringify({ 499 data: videos, 500 timestamp: now, 501 }) 502 ); 503 } catch (e) { 504 console.warn("Failed to cache videos:", e); 505 } 506 } 507 renderVideos(allVideos, shownCount); 508 }) 509 .fail(function () { 510 console.warn( 511 "Failed to fetch video statistics for popular sorting." 512 ); 513 allVideos = sortVideos(videos, config.sortBy); 514 renderVideos(allVideos, shownCount); 515 }); 516 } else { 517 allVideos = sortVideos(videos, config.sortBy); 518 if (duration > 0) { 519 try { 520 localStorage.setItem( 521 cacheKey, 522 JSON.stringify({ 523 data: videos, 524 timestamp: now, 525 }) 526 ); 527 } catch (e) { 528 console.warn("Failed to cache videos:", e); 529 } 530 } 531 renderVideos(allVideos, shownCount); 532 } 533 }, 2000); 534 }) 535 .fail(function () { 536 setTimeout(function () { 537 $container.empty(); 538 $container.html( 539 '<div class="ultp-ytg-error">Failed to fetch videos. Please try again.</div>' 540 ); 541 }, 3000); 542 }); 543 } 544 545 // Event handlers 546 $block.on("click", ".ultp-ytg-playlist-item", function () { 547 const videoId = $(this).data("video-id"); 548 if (!videoId) return; 549 550 activeVideo = allVideos.find(function (v) { 551 return v.videoId === videoId; 552 }); 553 554 if (activeVideo) { 555 renderPlaylistLayout(allVideos); 556 } 557 }); 558 // Make clicking the play icon also start the video with loader fallback (per item) 559 $block.on("click", ".ultp-ytg-play__icon", function () { 560 // Find the previous img[data-video-id] sibling to get the videoId 561 const $img = $(this).siblings("img[data-video-id]"); 562 const videoId = $img.data("video-id"); 563 if (!videoId) return; 564 // Find the closest .ultp-ytg-item and its .ultp-ytg-video 565 const $item = $(this).closest(".ultp-ytg-item"); 566 const $videoDiv = $item.find(".ultp-ytg-video"); 567 // Show loader only in this video area 568 $videoDiv.html( 569 '<div class="ultp-ytg-loading"><div class="ytg-loader"></div></div>' 570 ); 571 // Add active class to this item, remove from others 572 $item 573 .addClass("active") 574 .siblings(".ultp-ytg-item") 575 .removeClass("active"); 576 setTimeout(function () { 577 playingId = videoId; 578 renderGridLayout(allVideos, shownCount); 579 }, 1000); 580 }); 581 582 $block.on("click", ".ultp-ytg-video img[data-video-id]", function () { 583 const videoId = $(this).data("video-id"); 584 if (!videoId) return; 585 586 playingId = videoId; 587 renderGridLayout(allVideos, shownCount); 588 }); 589 590 $loadMoreBtn.on("click", function () { 591 shownCount += config.videosPerPage.lg; 592 renderGridLayout(allVideos, shownCount); 593 }); 594 // Responsive handling 595 $(window).on("resize", function () { 596 if (allVideos.length) { 597 updateResponsiveCounts(); 598 renderVideos(allVideos, shownCount); 599 } 600 }); 601 } 602 }); 603 } 604})(jQuery);
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.