Unlocks hidden videos and adds a premium gallery.
// ==UserScript==
// @name LPSG Video Unlocker
// @namespace CurlyWurly
// @version 5.3.0
// @description Unlocks hidden videos and adds a premium gallery.
// @author CurlyWurly
// @match https://www.lpsg.com/*
// @icon data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pjxzdmcgdmlld0JveD0iMCAwIDI1NiAyNTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3QgZmlsbD0ibm9uZSIgaGVpZ2h0PSIyNTYiIHdpZHRoPSIyNTYiLz48cGF0aCBkPSJNOTMuMiwxMjIuOEE3MC4zLDcwLjMsMCwwLDEsODgsOTZhNzIsNzIsMCwxLDEsNzIsNzIsNzAuMyw3MC4zLDAsMCwxLTI2LjgtNS4yaDBMMTIwLDE3Nkg5NnYyNEg3MnYyMEgzMlYxODRsNjEuMi02MS4yWiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRkZENzAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMTYiLz48Y2lyY2xlIGN4PSIxODAiIGN5PSI3NiIgcj0iMTIiIGZpbGw9IiNGRkQ3MDAiLz48L3N2Zz4=
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const config = {
volume: 0.1,
formats: ['mp4', 'm4v', 'mov'],
glassBase: 'rgba(20, 20, 20, 0.85)',
glassBorder: '1px solid rgba(255, 255, 255, 0.1)',
highlight: '#FFD700',
maxRetries: 3,
retryDelay: 1000 // Base time (ms). Actual delay = retryDelay * attemptNumber
};
const styles = `
:root {
--pg-glass: ${config.glassBase};
--pg-border: ${config.glassBorder};
--pg-highlight: ${config.highlight};
--pg-text: #fff;
}
/* --- Top Bar Integration --- */
.p-navgroup-link--iconic--media {
color: inherit;
}
/* --- Main Container --- */
#pg-container {
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
background: rgba(0,0,0,0.92);
backdrop-filter: blur(15px);
z-index: 99999;
display: flex; flex-direction: column;
opacity: 0; transition: opacity 0.3s ease;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
#pg-container.pg-visible { opacity: 1; }
/* --- Header & Filters --- */
.pg-header {
padding: 15px 20px;
display: flex; justify-content: space-between; align-items: center;
background: rgba(0,0,0,0.4);
border-bottom: var(--pg-border);
flex-shrink: 0;
}
.pg-filters { display: flex; gap: 10px; }
.pg-filter-btn {
background: transparent; border: var(--pg-border); color: #aaa;
padding: 6px 16px; border-radius: 20px; cursor: pointer;
font-size: 13px; transition: all 0.2s;
}
.pg-filter-btn.active, .pg-filter-btn:hover {
background: rgba(255, 215, 0, 0.15); border-color: var(--pg-highlight); color: #fff;
}
.pg-close {
background: none; border: none; color: #fff; font-size: 24px; cursor: pointer;
}
/* --- Grid Layout --- */
.pg-gallery-scroll {
flex-grow: 1; overflow-y: auto; padding: 20px;
scrollbar-width: thin; scrollbar-color: #444 transparent;
}
.pg-grid {
column-count: 2; column-gap: 15px;
width: 100%; max-width: 1600px; margin: 0 auto;
}
@media (min-width: 768px) { .pg-grid { column-count: 3; } }
@media (min-width: 1200px) { .pg-grid { column-count: 4; } }
@media (min-width: 1600px) { .pg-grid { column-count: 5; } }
/* --- Grid Items --- */
.pg-item {
break-inside: avoid; margin-bottom: 15px;
position: relative; border-radius: 8px; overflow: hidden;
border: var(--pg-border); background: #111;
cursor: pointer; transition: transform 0.2s;
/* No fixed min-height here, we rely on aspect-ratio or content */
}
.pg-item:hover { transform: scale(1.02); z-index: 2; box-shadow: 0 5px 15px rgba(0,0,0,0.5); }
/* IMPORTANT: width 100% ensures aspect-ratio works correctly based on column width */
.pg-item img, .pg-item video {
width: 100%; height: auto; display: block; object-fit: cover;
}
.pg-badge {
position: absolute; top: 8px; right: 8px;
background: rgba(0,0,0,0.7); color: #fff;
padding: 3px 8px; border-radius: 4px; font-size: 10px;
font-weight: bold; pointer-events: none;
backdrop-filter: blur(4px); z-index: 5;
}
.pg-badge.video { color: var(--pg-highlight); }
/* --- Retry & Status Overlays --- */
.pg-status-overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(20, 20, 20, 0.85);
display: flex; flex-direction: column;
align-items: center; justify-content: center;
z-index: 4;
backdrop-filter: blur(2px);
opacity: 1; transition: opacity 0.3s;
}
.pg-status-text {
color: #ccc; font-size: 12px; margin-top: 5px;
text-align: center;
}
.pg-status-icon { font-size: 20px; }
.pg-status-retry { color: var(--pg-highlight); animation: pg-pulse 1.5s infinite; }
.pg-status-broken { color: #ff5555; }
@keyframes pg-pulse { 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } }
/* --- Lazy Load --- */
.pg-lazy-img {
opacity: 0;
transition: opacity 0.4s ease-in-out;
}
.pg-lazy-img.pg-loaded {
opacity: 1;
}
/* --- Lightbox (Individual View) --- */
#pg-lightbox {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.95);
display: flex; flex-direction: column;
z-index: 100000; opacity: 0; pointer-events: none; transition: opacity 0.3s;
}
#pg-lightbox.active { opacity: 1; pointer-events: auto; }
.pg-lb-content {
flex-grow: 1; display: flex; align-items: center; justify-content: center;
overflow: hidden; padding: 20px; position: relative;
}
.pg-lb-media {
max-width: 100%; max-height: 100%;
object-fit: contain; box-shadow: 0 0 30px rgba(0,0,0,0.5);
border-radius: 4px;
}
.pg-lb-header {
position: absolute; top: 0; left: 0; right: 0;
padding: 15px; display: flex; justify-content: center;
z-index: 10; background: linear-gradient(to bottom, rgba(0,0,0,0.7), transparent);
}
.pg-lb-counter { font-size: 14px; color: #fff; background: rgba(0,0,0,0.4); padding: 4px 12px; border-radius: 20px; }
.pg-lb-controls {
position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%);
display: flex; gap: 20px; align-items: center;
background: rgba(30,30,30,0.8); padding: 10px 25px; border-radius: 30px;
border: var(--pg-border); backdrop-filter: blur(10px);
z-index: 10;
opacity: 0.2;
transition: opacity 0.3s ease, background 0.3s;
}
.pg-lb-controls:hover { opacity: 1; background: rgba(30,30,30,0.95); }
.pg-ctrl-btn {
background: none; border: none; color: #fff; font-size: 20px; cursor: pointer;
padding: 5px 10px; transition: color 0.2s;
display: flex; align-items: center; justify-content: center;
}
.pg-ctrl-btn:hover { color: var(--pg-highlight); }
.pg-ctrl-btn:disabled { color: #444; cursor: default; }
.pg-loader {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 30px; height: 30px; border: 3px solid #333; border-top-color: var(--pg-highlight);
border-radius: 50%; animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: translate(-50%, -50%) rotate(360deg); } }
`;
const styleSheet = document.createElement("style");
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
// --- SVG Icons ---
const gridIconSvg = `<svg viewBox="0 0 24 24" width="22" height="22" stroke="white" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>`;
const warnIconSvg = `<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>`;
const brokenIconSvg = `<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>`;
// --- Unlock Logic ---
function unlockVideos() {
const easterEggPoster = document.getElementsByClassName("video-easter-egg-poster");
[...easterEggPoster].forEach(poster => {
const img = poster.querySelector('img');
if (!img) return;
const imageUrl = img.src;
const sourceElements = config.formats.map(format => {
let videoUrl = imageUrl.replace("attachments/posters", "video")
.replace("/lsvideo/thumbnails", "lsvideo/videos")
.replace(/\.(jpg|jpeg|png)$/i, `.${format}`);
let mimeType = format === 'mp4' ? 'video/mp4' : (format === 'mov' ? 'video/quicktime' : 'video/x-m4v');
return `<source src="${videoUrl}" type="${mimeType}">`;
}).join('');
const videoHTML = `
<div class="newVideoDiv" style="display: inline-block; max-width: 100%;">
<video class="message-cell--main-video" controls playsinline preload="metadata"
poster="${imageUrl}" style="max-width: 100%; width: auto; max-height: 80vh; display: block; margin: 0 auto;">
${sourceElements}
<div class="bbMediaWrapper-fallback">Video format not supported.</div>
</video>
</div>`;
poster.insertAdjacentHTML('afterend', videoHTML);
poster.remove();
});
document.querySelectorAll('.video-easter-egg-blocker, .video-easter-egg-overlay').forEach(el => el.remove());
document.querySelectorAll('video').forEach(v => {
if (!v.dataset.volSet) {
v.volume = config.volume;
v.dataset.volSet = "true";
}
});
document.querySelectorAll('img[loading="lazy"]').forEach(img => {
img.loading = 'eager';
if (img.dataset.src) img.src = img.dataset.src;
});
}
// --- State Management ---
let galleryState = {
media: [],
filteredMedia: [],
currentIndex: 0,
filter: 'all',
container: null,
lightbox: null
};
// --- Sequential Retry Logic ---
const retryQueue = [];
let isRetrying = false;
function queueRetry(imgElement, fullSrc, attemptNum) {
retryQueue.push({ img: imgElement, src: fullSrc, attempt: attemptNum });
if (!isRetrying) {
processRetryQueue();
}
}
function processRetryQueue() {
if (retryQueue.length === 0) {
isRetrying = false;
return;
}
isRetrying = true;
const item = retryQueue.shift();
const dynamicDelay = config.retryDelay * item.attempt;
setTimeout(() => {
const separator = item.src.includes('?') ? '&' : '?';
const timestamp = Date.now();
item.img.src = `${item.src}${separator}retry=${timestamp}`;
processRetryQueue();
}, dynamicDelay);
}
function setStatusOverlay(cardElement, type, text) {
let overlay = cardElement.querySelector('.pg-status-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.className = 'pg-status-overlay';
cardElement.appendChild(overlay);
}
if (type === 'clear') {
overlay.remove();
return;
}
const iconHtml = type === 'retry'
? `<div class="pg-status-icon pg-status-retry">${warnIconSvg}</div>`
: `<div class="pg-status-icon pg-status-broken">${brokenIconSvg}</div>`;
const txtClass = type === 'broken' ? 'pg-status-broken' : 'pg-status-retry';
overlay.innerHTML = `
${iconHtml}
<div class="pg-status-text ${txtClass}">${text}</div>
`;
}
// --- Lazy Loading ---
let lazyImageObserver;
function initLazyObserver() {
if (lazyImageObserver) lazyImageObserver.disconnect();
const options = {
root: document.querySelector('.pg-gallery-scroll'),
rootMargin: '200px',
threshold: 0.01
};
lazyImageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
const src = img.dataset.src;
if (src) {
img.src = src;
observer.unobserve(img);
}
}
});
}, options);
document.querySelectorAll('.pg-lazy-img').forEach(img => {
lazyImageObserver.observe(img);
});
}
function scanMedia() {
const videos = Array.from(document.querySelectorAll('.message-cell--main video')).map(v => {
const posterSrc = v.getAttribute('data-poster') || v.getAttribute('poster') || v.poster;
return {
type: 'video',
el: v,
src: v.currentSrc || v.querySelector('source')?.src,
poster: posterSrc,
width: v.videoWidth || 300,
height: v.videoHeight || 169,
id: v.src || Math.random().toString(36)
};
});
const posterUrls = new Set(videos.map(v => v.poster));
const images = Array.from(document.querySelectorAll('.message-cell--main img'))
.filter(img => {
const notPoster = !posterUrls.has(img.src);
const isLinkedFile = img.closest('.file.file--linked');
const isAttachment = img.src.includes('/attachments/') || img.src.includes('/data/attachments/');
if (img.width < 50 && img.height < 50) return false;
return (isLinkedFile || isAttachment) && notPoster;
})
.map(img => {
const parentLink = img.closest('a');
let fullUrl = img.src;
// Grab dimensions from the thumbnail to prevent layout shift
// Natural dimensions are best, attribute second best
const w = parseInt(img.getAttribute('width') || img.naturalWidth || 0);
const h = parseInt(img.getAttribute('height') || img.naturalHeight || 0);
if (parentLink && parentLink.href) {
if (parentLink.href.includes('/attachments/')) {
fullUrl = parentLink.href;
}
else if (parentLink.href.match(/\.(jpg|jpeg|png|gif|webp)$/i)) {
fullUrl = parentLink.href;
}
}
const isGif = fullUrl.toLowerCase().endsWith('.gif');
return {
type: 'image',
subType: isGif ? 'gif' : 'img',
el: img,
src: fullUrl,
thumb: img.src,
width: w,
height: h,
id: fullUrl
};
});
return [...videos, ...images];
}
function buildGalleryUI() {
if (document.getElementById('pg-container')) return;
const container = document.createElement('div');
container.id = 'pg-container';
container.innerHTML = `
<div class="pg-header">
<div class="pg-filters">
<button class="pg-filter-btn active" data-filter="all">All (<span id="pg-count-all">0</span>)</button>
<button class="pg-filter-btn" data-filter="image">Images</button>
<button class="pg-filter-btn" data-filter="video">Videos</button>
</div>
<button class="pg-close">✖</button>
</div>
<div class="pg-gallery-scroll">
<div class="pg-grid" id="pg-grid"></div>
</div>
<div id="pg-lightbox">
<div class="pg-lb-header">
<span class="pg-lb-counter"></span>
</div>
<div class="pg-lb-content" id="pg-lb-stage">
<div class="pg-loader"></div>
</div>
<div class="pg-lb-controls">
<button class="pg-ctrl-btn" id="pg-prev">❮</button>
<button class="pg-ctrl-btn" id="pg-grid-view">${gridIconSvg}</button>
<button class="pg-ctrl-btn" id="pg-next">❯</button>
</div>
</div>
`;
document.body.appendChild(container);
galleryState.container = container;
galleryState.lightbox = container.querySelector('#pg-lightbox');
container.querySelectorAll('.pg-filter-btn').forEach(btn => {
btn.onclick = (e) => {
container.querySelectorAll('.pg-filter-btn').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
galleryState.filter = e.target.dataset.filter;
renderGrid();
};
});
container.querySelector('.pg-close').onclick = closeGallery;
container.querySelector('#pg-grid-view').onclick = closeLightbox;
container.querySelector('#pg-prev').onclick = () => navLightbox(-1);
container.querySelector('#pg-next').onclick = () => navLightbox(1);
document.addEventListener('keydown', handleKeyInput);
// Prevent the background page from scrolling when the gallery is open.
// We intercept wheel events on the container, stop propagation, and
// manually forward the scroll delta to the gallery scroll area.
container.addEventListener('wheel', (e) => {
const galleryScroll = container.querySelector('.pg-gallery-scroll');
const lb = container.querySelector('#pg-lightbox');
const isLbOpen = lb && lb.classList.contains('active');
// In lightbox mode there's nothing to scroll, just block the event.
if (isLbOpen) {
e.preventDefault();
return;
}
// In grid mode, redirect delta to the scrollable container.
if (galleryScroll) {
e.preventDefault();
galleryScroll.scrollTop += e.deltaY;
}
}, { passive: false });
let touchStartX = 0;
let touchEndX = 0;
const stage = document.getElementById('pg-lb-stage');
stage.addEventListener('touchstart', e => touchStartX = e.changedTouches[0].screenX);
stage.addEventListener('touchend', e => {
touchEndX = e.changedTouches[0].screenX;
if (touchEndX < touchStartX - 50) navLightbox(1);
if (touchEndX > touchStartX + 50) navLightbox(-1);
});
}
// Reorder items so that when CSS column-count fills top-to-bottom within each
// column, the first *visual* row shows items 0,1,2,3 (not 0,N/4,N/2,3N/4).
// This keeps masonry tight packing while ensuring the earliest page items are
// loaded first, avoiding hitting the rate limit on unrelated items.
function reorderForColumns(items, numCols) {
const result = [];
for (let col = 0; col < numCols; col++) {
for (let i = col; i < items.length; i += numCols) {
result.push(items[i]);
}
}
return result;
}
function getColumnCount() {
const w = window.innerWidth;
if (w >= 1600) return 5;
if (w >= 1200) return 4;
if (w >= 768) return 3;
return 2;
}
function renderGrid() {
const grid = document.getElementById('pg-grid');
grid.innerHTML = '';
galleryState.filteredMedia = galleryState.media.filter(m =>
galleryState.filter === 'all' || m.type === galleryState.filter
);
document.getElementById('pg-count-all').innerText = galleryState.filteredMedia.length;
// Reorder so the top-left visible items are the first items on the page.
const numCols = getColumnCount();
const displayOrder = reorderForColumns(galleryState.filteredMedia, numCols);
displayOrder.forEach((item) => {
const index = galleryState.filteredMedia.indexOf(item);
const card = document.createElement('div');
card.className = 'pg-item';
let content = '';
let badge = '';
let imageNode = null;
if (item.type === 'video') {
const poster = item.poster ? item.poster : '';
content = poster ? `<img src="${poster}" loading="lazy">` : `<div style="height:150px; display:flex; align-items:center; justify-content:center; background:#222; color:#555;">No Thumb</div>`;
badge = '<span class="pg-badge video">VIDEO</span>';
} else {
imageNode = document.createElement('img');
imageNode.className = 'pg-lazy-img';
imageNode.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; // Placeholder
imageNode.dataset.src = item.thumb;
imageNode.dataset.fullSrc = item.src;
imageNode.dataset.retries = 0;
// PREVENT LAYOUT SHIFT (CLS)
// We use the dimensions scraped from the thumbnail to reserve space.
// If dimensions exist, aspect-ratio holds the box open.
if (item.width > 0 && item.height > 0) {
imageNode.style.aspectRatio = `${item.width} / ${item.height}`;
} else {
// Fallback if dimensions are unknown
imageNode.style.minHeight = "200px";
}
imageNode.onload = function () {
this.classList.add('pg-loaded');
setStatusOverlay(card, 'clear');
};
imageNode.onerror = function () {
const fullSrc = this.dataset.fullSrc;
let retries = parseInt(this.dataset.retries || '0');
if (retries < config.maxRetries) {
retries++;
this.dataset.retries = retries;
setStatusOverlay(card, 'retry', `Retrying (${retries}/${config.maxRetries})`);
queueRetry(this, fullSrc, retries);
} else {
setStatusOverlay(card, 'broken', 'Image Broken');
this.classList.add('pg-loaded');
}
};
content = '';
badge = item.subType === 'gif' ? '<span class="pg-badge">GIF</span>' : '<span class="pg-badge">IMG</span>';
}
card.innerHTML = `${badge}${content}`;
if (imageNode) {
card.appendChild(imageNode);
}
card.onclick = () => openLightbox(index);
// Video Preview on Hover
if (item.type === 'video') {
let previewVideo = null;
let hoverTimeout = null;
card.onmouseenter = () => {
hoverTimeout = setTimeout(() => {
previewVideo = document.createElement('video');
previewVideo.src = item.src;
previewVideo.muted = true;
previewVideo.loop = true;
previewVideo.style.cssText = "position:absolute; top:0; left:0; width:100%; height:100%; object-fit:cover; z-index:1;";
card.appendChild(previewVideo);
previewVideo.play().catch(() => { });
}, 200);
};
card.onmouseleave = () => {
if (hoverTimeout) clearTimeout(hoverTimeout);
if (previewVideo) {
previewVideo.remove();
previewVideo = null;
}
};
}
grid.appendChild(card);
});
setTimeout(initLazyObserver, 50);
}
function openGallery() {
galleryState.media = scanMedia();
if (galleryState.media.length === 0) {
alert("No media found on this page.");
return;
}
buildGalleryUI();
renderGrid();
requestAnimationFrame(() => {
document.getElementById('pg-container').classList.add('pg-visible');
document.body.style.overflow = 'hidden';
});
}
function closeGallery() {
const c = document.getElementById('pg-container');
if (c) {
c.classList.remove('pg-visible');
setTimeout(() => c.remove(), 300);
document.body.style.overflow = '';
document.removeEventListener('keydown', handleKeyInput);
if (lazyImageObserver) lazyImageObserver.disconnect();
// Clear Queue
retryQueue.length = 0;
isRetrying = false;
}
}
function openLightbox(index) {
galleryState.currentIndex = index;
const lb = document.getElementById('pg-lightbox');
lb.classList.add('active');
updateLightboxContent();
}
function closeLightbox() {
const lb = document.getElementById('pg-lightbox');
const stage = document.getElementById('pg-lb-stage');
const existingVideo = stage.querySelector('video');
if (existingVideo) existingVideo.pause();
lb.classList.remove('active');
}
function navLightbox(direction) {
const newIndex = galleryState.currentIndex + direction;
if (newIndex >= 0 && newIndex < galleryState.filteredMedia.length) {
galleryState.currentIndex = newIndex;
updateLightboxContent();
}
}
function updateLightboxContent() {
const item = galleryState.filteredMedia[galleryState.currentIndex];
const stage = document.getElementById('pg-lb-stage');
const counter = document.querySelector('.pg-lb-counter');
const prevBtn = document.getElementById('pg-prev');
const nextBtn = document.getElementById('pg-next');
prevBtn.disabled = galleryState.currentIndex === 0;
nextBtn.disabled = galleryState.currentIndex === galleryState.filteredMedia.length - 1;
counter.innerText = `${galleryState.currentIndex + 1} / ${galleryState.filteredMedia.length}`;
stage.innerHTML = '<div class="pg-loader"></div>';
let mediaEl;
if (item.type === 'video') {
mediaEl = document.createElement('video');
mediaEl.className = 'pg-lb-media';
mediaEl.src = item.src;
mediaEl.controls = true;
mediaEl.autoplay = true;
mediaEl.volume = config.volume;
if (item.poster) mediaEl.poster = item.poster;
} else {
mediaEl = document.createElement('img');
mediaEl.className = 'pg-lb-media';
mediaEl.src = item.src;
mediaEl.onerror = function () {
stage.querySelector('.pg-loader')?.remove();
stage.insertAdjacentHTML('beforeend', `<div style="color:#fff; text-align:center;">Failed to load full size image.</div>`);
};
}
mediaEl.onload = () => stage.querySelector('.pg-loader')?.remove();
mediaEl.oncanplay = () => stage.querySelector('.pg-loader')?.remove();
stage.appendChild(mediaEl);
}
function handleKeyInput(e) {
const lb = document.getElementById('pg-lightbox');
const isLbOpen = lb && lb.classList.contains('active');
if (e.key === 'Escape') {
if (isLbOpen) closeLightbox();
else closeGallery();
} else if (isLbOpen) {
if (e.key === 'ArrowLeft') navLightbox(-1);
if (e.key === 'ArrowRight') navLightbox(1);
}
}
function createLaunchButton() {
// Find ALL account navigation groups (Desktop top bar AND Mobile sticky nav)
const navGroups = document.querySelectorAll('.p-navgroup.p-account');
navGroups.forEach(navGroup => {
// Check if we already added a button to THIS specific group to prevent duplicates
if (navGroup.querySelector('.pg-gallery-launcher')) return;
const btn = document.createElement("a");
// We use a class 'pg-gallery-launcher' to identify our button instead of an ID
// since there will now be two of them.
btn.className = "p-navgroup-link u-ripple p-navgroup-link--iconic p-navgroup-link--iconic--media pg-gallery-launcher";
btn.title = "Open Gallery";
btn.href = "#";
btn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
openGallery();
};
btn.innerHTML = `
<i aria-hidden="true" class="fa fa-images"></i>
<span class="p-navgroup-linkText">Gallery</span>
`;
navGroup.appendChild(btn);
});
}
unlockVideos();
setTimeout(unlockVideos, 1500);
setTimeout(createLaunchButton, 500);
createLaunchButton();
})();