1(function($) { 2 Galleria.addTheme({ 3 name: 'nextgen_pro_horizontal_filmstrip', 4 author: 'Imagely', 5 version: 2.0, 6 defaults: { 7 carousel: true, 8 thumbnails: true, 9 autoplay: true, 10 showInfo: false, 11 fullscreenDoubleTap: false, 12 trueFullscreen: false 13 }, 14 init: function(options) { 15 Galleria.requires(1.41, 'This version of Classic theme requires Galleria 1.4.1 or later'); 16 17 var self = this; 18 19 // TikTok video playback handling 20 var currentVideo = null; 21 var wasPlayingBeforeVideo = false; 22 var videoPlaying = false; 23 var currentVideoIndex = -1; 24 25 // Utility function to detect and convert video platform URLs to embed URLs 26 var getVideoEmbedInfo = function(url) { 27 if (!url || typeof url !== 'string') { 28 return { isEmbed: false, embedUrl: null, useIframe: false }; 29 } 30 31 var embedUrl = null; 32 var useIframe = false; 33 var isEmbed = false; 34 35 // YouTube URLs 36 var youtubeRegex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/; 37 var youtubeMatch = url.match(youtubeRegex); 38 if (youtubeMatch) { 39 embedUrl = 'https://www.youtube.com/embed/' + youtubeMatch[1] + '?autoplay=1&rel=0'; 40 useIframe = true; 41 isEmbed = true; 42 } 43 44 // Vimeo URLs 45 var vimeoRegex = /(?:vimeo\.com\/)(?:.*\/)?(\d+)/; 46 var vimeoMatch = url.match(vimeoRegex); 47 if (vimeoMatch && !isEmbed) { 48 embedUrl = 'https://player.vimeo.com/video/' + vimeoMatch[1] + '?autoplay=1'; 49 useIframe = true; 50 isEmbed = true; 51 } 52 53 // Dailymotion URLs 54 var dailymotionRegex = /(?:dailymotion\.com\/(?:embed\/)?video\/|dai\.ly\/)([a-zA-Z0-9]+)/; 55 var dailymotionMatch = url.match(dailymotionRegex); 56 if (dailymotionMatch && !isEmbed) { 57 embedUrl = 'https://www.dailymotion.com/embed/video/' + dailymotionMatch[1] + '?autoplay=1'; 58 useIframe = true; 59 isEmbed = true; 60 } 61 62 // Twitch URLs 63 var twitchRegex = /(?:twitch\.tv\/videos\/|twitch\.tv\/)(\d+|[a-zA-Z0-9_]+)/; 64 var twitchMatch = url.match(twitchRegex); 65 if (twitchMatch && !isEmbed) { 66 var twitchId = twitchMatch[1]; 67 // Check if it's a video ID (numeric) or channel name 68 if (/^\d+$/.test(twitchId)) { 69 embedUrl = 'https://player.twitch.tv/?video=' + twitchId + '&autoplay=true&parent=' + window.location.hostname; 70 } else { 71 embedUrl = 'https://player.twitch.tv/?channel=' + twitchI
71d + '&autoplay=true&parent=' + window.location.hostname; 72 } 73 useIframe = true; 74 isEmbed = true; 75 } 76 77 // VideoPress URLs 78 var videopressRegex = /(?:videopress\.com\/v\/|video\.wordpress\.com\/v\/)([a-zA-Z0-9]+)/; 79 var videopressMatch = url.match(videopressRegex); 80 if (videopressMatch && !isEmbed) { 81 embedUrl = 'https://videopress.com/embed/' + videopressMatch[1] + '?autoplay=1'; 82 useIframe = true; 83 isEmbed = true; 84 } 85 86 // Wistia URLs 87 var wistiaRegex = /(?:wistia\.com\/medias\/|wistia\.net\/embed\/)([a-zA-Z0-9]+)/; 88 var wistiaMatch = url.match(wistiaRegex); 89 if (wistiaMatch && !isEmbed) { 90 embedUrl = 'https://fast.wistia.net/embed/iframe/' + wistiaMatch[1] + '?autoplay=1'; 91 useIframe = true; 92 isEmbed = true; 93 } 94 95 return { 96 isEmbed: isEmbed, 97 embedUrl: embedUrl, 98 useIframe: useIframe, 99 originalUrl: url 100 }; 101 }; 102 103 // Adjust some settings specific to this gallery 104 options.showInfo = (options.showInfo && options._nggCaptionClass.length > 0) ? true : false; 105 options._nggCaptionPadding = parseInt(window.ngg_galleria.get_setting(options._nggGalleryID, 'caption_padding', '5')); 106 options._nggOverrideThumbnails = window.ngg_galleria.get_setting(options._nggGalleryID, 'override_thumbnail_settings', '0') != '0'; 107 108 // Galleria doesn't correctly position images with borders 109 var adjust_border_positioning = function($img) { 110 if (options._nggBorderSize > 0 && options.imageCrop !== true) { 111 var top = (parseInt($img.css('top')) - options._nggBorderSize); 112 var left = (parseInt($img.css('left')) - options._nggBorderSize); 113 if (top < 0) top = 0; 114 if (left < 0) left = 0; 115 $img.css('top', top + 'px'); 116 $img.css('left', left + 'px'); 117 } 118 }; 119 120 var cleanupVideo = function() { 121 if (currentVideo) { 122 // Stop video playback 123 var $video = $(currentVideo).find('video, iframe'); 124 if ($video.length) { 125 if ($video.is('video')) { 126 $video[0].pause(); 127 $video[0].src = ''; // Clear source to stop loading 128 $video.off('ended error playing'); 129 } else if ($video.is('iframe')) { 130 // For iframes, we can't directly stop playback, but we can remove the src 131 $video.attr('src', 'about:blank'); 132 } 133 } 134 $(currentVideo).remove(); 135 currentVideo = null; 136 videoPlaying = false; 137 currentVideoIndex = -1; 138 } 139 }; 140 141 // Helper function to show error banner (removes duplicate code) 142 var showVideoErrorBanner = function($container) { 143 // Get image dimensions to match error banner size 144 var $img = self.$('stage').find('.galleria-image img'); 145 var imgWidth = $img.width() || $img.attr('width') || 0; 146 var imgHeight = $img.height() || $img.attr('height') || 0; 147 148 var $errorMsg; 149 if (typeof NextGEN_TikTok !== 'undefined' && NextGEN_TikTok.create_error) { 150 var errorEl = NextGEN_TikTok.create_error('Video failed to load', 'ngg-tiktok-error'); 151 $errorMsg = $(errorEl); 152 } else { 153 // Fallback error element 154 $errorMsg = $('<div class="ngg-tiktok-error"><div class="ngg-tiktok-error-content"><span class="ngg-tiktok-error-icon">⚠</span><span class="ngg-tiktok-error-text">Video failed to load</span></div></div>'); 155 } 156 157 // Size error banner to match image dimensions 158 if (imgWidth > 0 && imgHeight > 0) { 159 $errorMsg.css({ 160 width: imgWidth + 'px', 161 height: imgHeight + 'px', 162 position: 'relative', 163 top: 'auto', 164 left: 'auto', 165 right: 'auto', 166 bottom: 'auto' 167 }); 168 } 169 170 $container.append($errorMsg); 171 }; 172 173 var handleVideoPlayback = function(index) { 174 var data = self.getData(index); 175 // Check for video URL - prioritize video_link, then tiktok_play_url 176 var videoUrl = data.video_link || data.tiktok_play_url; 177 178 // Check if this is a video (TikTok or generic video_link) 179 // For TikTok: check is_tiktok flag 180 // For generic video: check has_video_link flag or if video_link exists 181 var isVideo = (data.is_tiktok == 1 || data.is_tiktok == '1' || data.is_tiktok === true) 182 || (data.is_video_link == 1 || data.is_video_link == '1' || data.is_video_link === true) 183 || (data.video_link && data.video_link.length > 0); 184 185 if (!videoUrl || !isVideo) { 186 // Not a video - cleanup any existing video and show playback button 187 cleanupVideo(); 188 $('.galleria-playback-button').show(); 189 // Resume slideshow if it was playing before 190 if (wasPlayingBeforeVideo && !self._playing) { 191 self.play(); 192 } 193 return; 194 } 195 196 // This is a video - pause slideshow immediately to prevent auto-advance 197 wasPlayingBeforeVideo = self._playing; 198 if (wasPlayingBeforeVideo) { 199 self.pause(); 200 } 201 202 // Cleanup any previous video 203 cleanupVideo(); 204 205 // Hide playback button when showing video 206 $('.galleria-playback-button').hide(); 207 208 // Create video container
209 var $videoContainer = $('<div class="galleria-video-container"></div>'); 210 211 // Get the video URL - prioritize video_link, then TikTok URLs 212 var videoUrl = data.video_link || data.tiktok_play_url || data.tiktok_share_url; 213 214 // Check if this is an embed URL (YouTube, Vimeo, etc.) 215 var videoInfo = getVideoEmbedInfo(videoUrl); 216 var $videoElement; 217 218 if (videoInfo.useIframe && videoInfo.embedUrl) { 219 // Create iframe for embed platforms (YouTube, Vimeo, etc.) 220 $videoElement = $('<iframe>', { 221 src: videoInfo.embedUrl, 222 frameborder: '0', 223 allow: 'autoplay; fullscreen; picture-in-picture', 224 allowfullscreen: true, 225 webkitallowfullscreen: true, 226 mozallowfullscreen: true 227 }); 228 229 // Apply border styling if configured 230 if (options._nggBorderSize > 0) { 231 $videoElement.css({ 232 'border': 'solid ' + parseInt(options._nggBorderSize) + 'px ' + options._nggBorderColor 233 }); 234 } 235 236 // For iframes, we can't detect 'ended' event, so we'll rely on the platform's controls 237 videoPlaying = true; // Assume playing for embed videos 238 } else { 239 // Create HTML5 video element for direct video URLs (local files, TikTok, etc.) 240 $videoElement = $('<video>', { 241 controls: true, 242 autoplay: true, 243 playsinline: true, 244 preload: 'auto', 245 muted: false 246 }); 247 248 // Apply border styling if configured 249 if (options._nggBorderSize > 0) { 250 $videoElement.css({ 251 'border': 'solid ' + parseInt(options._nggBorderSize) + 'px ' + options._nggBorderColor 252 }); 253 } 254 255 // Set video source 256 $videoElement.attr('src', videoUrl); 257 258 // Track when video actually starts playing 259 $videoElement.on('playing', function() { 260 videoPlaying = true; 261 }); 262 } 263 264 // Handle video end - resume slideshow (only for HTML5 video, not iframes) 265 if ($videoElement.is('video')) { 266 $videoElement.on('ended', function() { 267 cleanupVideo(); 268 $('.galleria-playback-button').show(); 269 if (wasPlayingBeforeVideo) { 270 // Small delay to ensure cleanup is complete 271 setTimeout(function() { 272 self.play(); 273 self.next(); 274 }, 100); 275 } else { 276 // Even if slideshow wasn't playing, advance to next slide 277 self.next(); 278 } 279 }); 280 281 // Handle video error - try fallback URL (only for HTML5 video) 282 $videoElement.on('error', function() { 283 var currentSrc = $videoElement.attr('src'); 284 var isTikTok = (data.is_tiktok == 1 || data.is_tiktok == '1' || data.is_tiktok === true); 285 286 // Keep TikTok and video_link error handling completely separate 287 if (isTikTok) { 288 // TikTok video: Original error handling logic (preserved exactly as before) 289 // Try tiktok_share_url as fallback if tiktok_play_url failed 290 if (data.tiktok_share_url && currentSrc !== data.tiktok_share_url) { 291 // Try fallback URL 292 $videoElement.attr('src', data.tiktok_share_url); 293 } else { 294 // Video failed to load, show error banner 295 $videoElement.remove(); 296 showVideoErrorBanner($videoContainer); 297 } 298 } else { 299 // Generic video_link: Separate error handling (doesn't interfere with TikTok) 300 // For generic videos, we don't have fallback URLs, so show error immediately 301 // Video failed to load, show error banner 302 $videoElement.remove(); 303 showVideoErrorBanner($videoContainer); 304 } 305 }); 306 } 307
308 // Add to container and append to stage 309 $videoContainer.append($videoElement); 310 self.$('stage').append($videoContainer); 311 currentVideo = $videoContainer[0]; 312 currentVideoIndex = index; 313 314 // Force play attempt for HTML5 video (some browsers need user interaction, but autoplay should work) 315 if ($videoElement.is('video')) { 316 setTimeout(function() { 317 if ($videoElement[0] && $videoElement[0].paused) { 318 $videoElement[0].play().catch(function(err) { 319 console.warn('Video autoplay failed:', err); 320 }); 321 } 322 }, 100); 323 } 324 }; 325 326 // Set our thumbnail sizes to exactly the dimensions requested by the user 327 if (options._nggOverrideThumbnails) { 328 options._nggThumbnailWidth = parseInt(window.ngg_galleria.get_setting(options._nggGalleryID, 'thumbnail_width', '120')); 329 options._nggThumbnailHeight = parseInt(window.ngg_galleria.get_setting(options._nggGalleryID, 'thumbnail_height', '90')); 330 331 this.$('thumbnails').find('.galleria-image').each(function(ndx, image) { 332 $(image).css({ 333 width: options._nggThumbnailWidth + 'px', 334 height: options._nggThumbnailHeight + 'px' 335 }).find('img').css({ 336 'max-width': options._nggThumbnailWidth, 337 width: options._nggThumbnailWidth + 'px', 338 height: options._nggThumbnailHeight + 'px' 339 }); 340 }); 341 } else {
342 // Will be the height of the tallest thumbnail to display 343 options._nggThumbnailHeight = 1; 344 345 // Find the tallest thumbnail 346 for (var i = 0; i <= (this.getDataLength() - 1); i++) { 347 var img = this.getData(i); 348 if (img.thumb_dimensions.height > options._nggThumbnailHeight) { 349 options._nggThumbnailHeight = img.thumb_dimensions.height; 350 } 351 } 352 353 // Position shorter thumbnails to be vertically centered 354 this.$('thumbnails').find('.galleria-image').each(function(ndx, image) { 355 var cur_h = self._data[ndx].thumb_dimensions.height; 356 $(image).css({ 357 'width': self._data[ndx].thumb_dimensions.width + 'px', 358 'height': self._data[ndx].thumb_dimensions.height + 'px' 359 }).find('img').css({ 360 'width': self._data[ndx].thumb_dimensions.width + 'px', 361 'height': self._data[ndx].thumb_dimensions.height + 'px' 362 }); 363 if (cur_h < options._nggThumbnailHeight) { 364 $(image).css('top', (options._nggThumbnailHeight - cur_h) / 2); 365 } 366 }); 367 } 368 369 // It's much faster to add this by CSS than hooking onto 'loadfinish' 370 if (options._nggBorderSize > 0 && options.imageCrop !== true) { 371 $("<style type='text/css'>#displayed_gallery_" + options._nggGalleryID + " .galleria-stage .galleria-image img {" 372 + "border: solid " + parseInt(options._nggBorderSize) + 'px ' + options._nggBorderColor 373 + " } </style>").appendTo("head"); 374 } 375 376 // Adjust some dimensions before Galleria starts the display 377 this.$('info').css('max-height', options._nggCaptionHeight + 'px'); 378 this.$('thumbnails-container').css('height', options._nggThumbnailHeight + 'px'); 379 380 // Calculate how far from the bottom the carousel should appear. The 381 // extra 2 pixels is for the border-top/border-bottom of the thumbnails. 382 // The last 4 pixels is to match what Photocrati Slideshows look like 383 this.$('stage').css( 384 'bottom', 385 options._nggThumbnailHeight + (parseInt(options._nggBorderSize) > 0 ? parseInt(options._nggBorderSize) : 0) 386 + 2 + 4 + 'px' 387 ); 388 389 this.$('thumbnails-container').height(options._nggThumbnailHeight); 390 this.$('stage').css({ 391 bottom: (($(options._nggGalleryParent).offset() 392 + $(options._nggGalleryParent).outerHeight()) 393 - this.$('thumbnails-container').offset().top) 394 + 6 395 }); 396 397 // Let Galleria reflow itself after we've altered so much 398 this.rescale(); 399 400 if (!Galleria.TOUCH) { 401 this.addIdleState(this.get('image-nav-left'), { left: -50 }); 402 this.addIdleState(this.get('image-nav-right'), { right: -50 }); 403 this.addIdleState(this.get('counter'), { opacity: 0 }); 404 } 405 406 // Add the caption class to the Galleria container 407 if (options.showInfo && options._nggCaptionClass.length > 0) { 408 this.$('info').show(); 409 this.$('container').addClass(options._nggCaptionClass); 410 411 // if the info overflows its container this effect will let viewers read the remaining description 412 this.$('info').on('mouseenter', function() { 413 var $info = $(this); 414 var text = self.$('info-text'); 415 var diff = $info.outerHeight() - text.outerHeight(); 416 if (diff < 0) { 417 $info.stop().animate({ scrollTop: -diff }, ((-diff) / 17) * 450); 418 } 419 }); 420 this.$('info').on('mouseleave', function() { 421 var $info = $(this); 422 var text = self.$('info-text'); 423 var diff = $info.outerHeight() - text.outerHeight(); 424 if (diff < 0) { 425 $info.stop().animate({ scrollTop: 0 }, 'fast'); 426 } 427 }); 428 429 // Adjust the dimensions of the stage to fit captions 430 switch (options._nggCaptionClass) { 431 case 'caption_above_stage': { 432 this.$('stage').css({ top: parseInt(options._nggCaptionHeight) + options.imageMargin }); 433 break; 434 } 435 case 'caption_below_stage': { 436 this.$('thumbnails-container').css({ 437 bottom: (parseInt(options._nggCaptionHeight) + parseInt(options._nggCaptionPadding)) + 'px' 438 }); 439 this.$('stage').css({ 440 bottom: parseInt(options._nggCaptionHeight) 441 + (options._nggCaptionPadding * 2) 442 + this.$('thumbnails-container').height() 443 + options.imageMargin 444 + 6 445 }); 446 break; 447 } 448 case 'caption_overlay_bottom': { 449 this.$('info').css({ bottom: parseInt(this.$('stage').css('bottom')) }); 450 break; 451 } 452 } 453 454 this.rescale(); 455 } 456 457 // Thumbnail navigation shown on hover 458 $([this.$('thumb-nav-left'), this.$('thumb-nav-right')]) 459 .css({ display: 'none' }) 460 .on('mouseenter', function() { 461 if (!$(this).hasClass('disabled')) { 462 $(this).stop().animate({ 463 opacity: 1 464 }, 'fast'); 465 } 466 }) 467 .on('mouseleave', function() { 468 if (!$(this).hasClass('disabled')) { 469 $(this).stop().animate({ 470 opacity: 0.8 471 }, 'fast'); 472 } 473 }); 474 475 this.$('thumbnails-container').on('mouseenter', 476 this.proxy(function() { 477 var navList = [this.get('thumb-nav-left'), this.get('thumb-nav-right')]; 478 $(navList).css({ display : 'block' }); 479 $(navList).stop().animate({ 480 opacity: 0.8 481 }); 482 }) 483 ); 484 485 this.$('thumbnails-container').on('mouseleave', 486 this.proxy(function() { 487 var navList = [this.get('thumb-nav-left'), this.get('thumb-nav-right')]; 488 $(navList).stop().animate({ 489 opacity: 0 490 }, function() { 491 $(this).css({ display: 'none' }); 492 }); 493 }) 494 ); 495 496 // Handle video playback when slide loads 497 this.bind('loadstart', this.proxy(function(e) { 498 handleVideoPlayback(e.index); 499 })); 500 501 // Ensure video cleanup when navigating between slides 502 this.bind('image', this.proxy(function(e) { 503 // Only cleanup if we're navigating to a different slide 504 // This prevents cleanup when the same slide reloads 505 if (currentVideo && currentVideoIndex !== e.index) { 506 cleanupVideo(); 507 $('.galleria-playback-button').show(); 508 // Resume slideshow if it was playing before video 509 if (wasPlayingBeforeVideo && !self._playing) { 510 self.play(); 511 } 512 } 513 })); 514 515 // set slideshow speed 516 if (options._nggSlideshowSpeed) { 517 this.setPlaytime(options._nggSlideshowSpeed); 518 } 519 520 // add playback controls if we're to do so 521 if (options._nggShowPlaybackControls) { 522 // Add playback controls 523 var playback_button = $('<div/>').addClass('galleria-playback-button'); 524 if (this._playing) { 525 playback_button.removeClass('play').addClass('pause'); 526 } else { 527 playback_button.removeClass('pause').addClass('play'); 528 } 529 $(this._dom.stage).append(playback_button); 530 531 var button = $('<a/>') 532 .on('mouseenter', function() { $(this).parent().css('opacity', 0.9); }) 533 .on('mouseleave', function() { $(this).parent().css('opacity', 0.7); }) 534 .on('click', this.proxy(function(e) { 535 var controls = $(e.target).parent(); 536 if (this._playing) { 537 this.pause(); 538 controls.removeClass('pause').addClass('play'); 539 } else { 540 this.play().next(); 541 controls.removeClass('play').addClass('pause'); 542 } 543 })); 544 playback_button.append(button); 545 546 playback_button 547 .on('mouseenter', function() { $(this).css('opacity', 0.7); }) 548 .on('mouseleave', function() { $(this).animate({ opacity: 0.0}); }); 549 } 550 551 this.bind('rescale', this.proxy(function(e) { 552 var $img = $(self.getActiveImage()); 553 setTimeout(function() { 554 adjust_border_positioning($img); 555 }, 30); 556 })); 557 558 this.bind('loadfinish', this.proxy(function(e) { 559 var $img = $(e.imageTarget); 560 adjust_border_positioning($img); 561 562 // Adjust the Pro Lightbox triggers if they exist 563 $(options._nggGalleryParent).siblings('div.ngg-trigger-buttons').each(function () { 564 $(this).width(self.$('stage').width()); 565 $(this).css('margin', '0 auto'); 566 $(this).find('i').each(function () { 567 $(this).data('nplmodal-image-id', self.getData(self.getIndex()).image_id); 568 }); 569 }); 570 571 // The current thumbnail should have full opacity 572 $(e.thumbTarget).css('opacity', 1) 573 .parent() 574 .siblings() 575 .children() 576 .css('opacity', 0.6); 577 })); 578 579 this.bind('thumbnail', this.proxy(function(e) { 580 if (!Galleria.TOUCH ) { 581 // fade thumbnails 582 $(e.thumbTarget).css('opacity', 0.6).parent() 583 .on('mouseenter', function() { 584 $(this).not('.active').children().stop().fadeTo(100, 1); 585 }) 586 .on('mouseleave', function() { 587 $(this).not('.active').children().stop().fadeTo(400, 0.6); 588 }); 589 if (e.index === this.getIndex()) { 590 $(e.thumbTarget).css('opacity', 1); 591 } 592 } else { 593 $(e.thumbTarget).css('opacity', this.getIndex() ? 1 : 0.6); 594 } 595 })); 596 597 // Disable image right-click and drag when requested 598 if (window.ngg_galleria.get_setting(options._nggGalleryID, 'protect_images', false)) { 599 this.addElement('image-protection'); 600 document.oncontextmenu = function(event) { 601 event = event || window.event; 602 event.preventDefault(); 603 }; 604 this.prependChild('images', 'image-protection'); 605 this.$('thumbnails').find('.galleria-image').bind('dragstart', function(event) { 606 event.preventDefault(); 607 }); 608 } 609 } 610 }); 611 612 $(window).trigger('ngg.galleria.themeadded', ['nextgen_pro_horizontal_filmstrip']); 613 614}(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.