LPSG Video Unlocker

Unlocks hidden videos and adds a premium gallery.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         LPSG Video Unlocker
// @namespace    CurlyWurly
// @version      5.4.7
// @description  Unlocks hidden videos and adds a premium gallery.
// @author       CurlyWurly
// @match        https://www.lpsg.com/*
// @icon         data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pjxzdmcgdmlld0JveD0iMCAwIDI1NiAyNTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3QgZmlsbD0ibm9uZSIgaGVpZ2h0PSIyNTYiIHdpZHRoPSIyNTYiLz48cGF0aCBkPSJNOTMuMiwxMjIuOEE3MC4zLDcwLjMsMCwwLDEsODgsOTZhNzIsNzIsMCwxLEsNzIsNzIsNzAuMyw3MC4zLDAsMCwxLTI2LjgtNS4yaDBMMTIwLDE3Nkg5NnYyNEg3MnYyMEgzMlYxODRsNjEuMi02MS4yWiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRkZENzAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMTYiLz48Y2lyY2xlIGN4PSIxODAiIGN5PSI3NiIgcj0iMTIiIGZpbGw9IiNGRkQ3MDAiLz48L3N2Zz4=
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
        (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);

    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
    };

    const styles = `
        :root {
            --pg-glass: ${config.glassBase};
            --pg-border: ${config.glassBorder};
            --pg-highlight: ${config.highlight};
            --pg-text: #fff;
        }

        .p-navgroup-link--iconic--media {
            color: inherit;
        }

        #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;
            overscroll-behavior: none;
        }
        #pg-container.pg-visible { opacity: 1; }

        .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;
        }

        .pg-gallery-scroll {
            flex-grow: 1; overflow-y: auto; padding: 20px;
            scrollbar-width: thin; scrollbar-color: #444 transparent;
            overscroll-behavior: contain;
            -webkit-overflow-scrolling: touch;
        }
        .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; } }

        .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;
        }
        .pg-item:hover { transform: scale(1.02); z-index: 2; box-shadow: 0 5px 15px rgba(0,0,0,0.5); }

        .pg-item img, .pg-item video {
            width: 100%; height: auto; display: block; object-fit: cover;
        }

        .pg-preview-video {
            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
            object-fit: cover; z-index: 1;
            pointer-events: none;
        }

        .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); }

        .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; } }

        .pg-lazy-img {
            opacity: 0;
            transition: opacity 0.4s ease-in-out;
        }
        .pg-lazy-img.pg-loaded {
            opacity: 1;
        }

        #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; }

        @keyframes pg-play-appear {
            from { opacity: 0; transform: translate(-50%, -50%) scale(0.7); }
            to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
        }

        .pg-ios-play-btn {
            position: absolute; top: 50%; left: 50%;
            transform: translate(-50%, -50%);
            width: 80px; height: 80px;
            background: rgba(255, 255, 255, 0.12);
            backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
            border-radius: 50%;
            display: flex; align-items: center; justify-content: center;
            border: 1.5px solid rgba(255, 255, 255, 0.2);
            cursor: pointer;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
            transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1),
                        background 0.25s ease,
                        box-shadow 0.25s ease;
            animation: pg-play-appear 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) both;
        }
        .pg-ios-play-btn:hover {
            transform: translate(-50%, -50%) scale(1.12);
            background: rgba(255, 255, 255, 0.22);
            box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
        }
        .pg-ios-play-btn:active {
            transform: translate(-50%, -50%) scale(0.95);
        }
        .pg-ios-play-btn svg {
            filter: drop-shadow(0 1px 3px rgba(0,0,0,0.3));
            margin-left: 3px;
        }

        .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
    };

    // --- Scroll Lock ---
    let savedScrollY = 0;

    function lockBodyScroll() {
        savedScrollY = window.scrollY || document.documentElement.scrollTop || 0;
        document.body.style.position = 'fixed';
        document.body.style.top = `-${savedScrollY}px`;
        document.body.style.left = '0';
        document.body.style.right = '0';
        document.body.style.width = '100%';
        document.body.style.overflow = 'hidden';
    }

    function unlockBodyScroll() {
        document.body.style.position = '';
        document.body.style.top = '';
        document.body.style.left = '';
        document.body.style.right = '';
        document.body.style.width = '';
        document.body.style.overflow = '';
        window.scrollTo(0, savedScrollY);
    }

    // --- 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;

                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);

        container.addEventListener('wheel', (e) => {
            const galleryScroll = container.querySelector('.pg-gallery-scroll');
            const lb = container.querySelector('#pg-lightbox');
            const isLbOpen = lb && lb.classList.contains('active');

            if (isLbOpen) {
                e.preventDefault();
                return;
            }

            if (galleryScroll) {
                e.preventDefault();
                galleryScroll.scrollTop += e.deltaY;
            }
        }, { passive: false });

        container.addEventListener('touchmove', (e) => {
            const galleryScroll = container.querySelector('.pg-gallery-scroll');
            const lb = container.querySelector('#pg-lightbox');
            const isLbOpen = lb && lb.classList.contains('active');

            if (isLbOpen) {
                e.preventDefault();
                return;
            }

            if (galleryScroll && galleryScroll.contains(e.target)) {
                return;
            }

            e.preventDefault();
        }, { 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);
        });
    }

    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;

        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';
                imageNode.dataset.src = item.thumb;
                imageNode.dataset.fullSrc = item.src;
                imageNode.dataset.retries = 0;

                if (item.width > 0 && item.height > 0) {
                    imageNode.style.aspectRatio = `${item.width} / ${item.height}`;
                } else {
                    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 hover preview — desktop only, skipped entirely on iOS
            if (item.type === 'video' && !isIOS) {
                let previewVideo = null;
                let hoverTimeout = null;

                card.onmouseenter = () => {
                    hoverTimeout = setTimeout(() => {
                        previewVideo = document.createElement('video');
                        previewVideo.className = 'pg-preview-video';
                        previewVideo.src = item.src;
                        previewVideo.muted = true;
                        previewVideo.loop = true;
                        previewVideo.playsInline = true;
                        card.appendChild(previewVideo);
                        previewVideo.play().catch(() => { });
                    }, 200);
                };

                card.onmouseleave = () => {
                    if (hoverTimeout) clearTimeout(hoverTimeout);
                    if (previewVideo) {
                        previewVideo.pause();
                        previewVideo.src = '';
                        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();

        lockBodyScroll();

        requestAnimationFrame(() => {
            document.getElementById('pg-container').classList.add('pg-visible');
        });
    }

    function closeGallery() {
        const c = document.getElementById('pg-container');
        if (c) {
            c.classList.remove('pg-visible');
            setTimeout(() => c.remove(), 300);
            unlockBodyScroll();
            document.removeEventListener('keydown', handleKeyInput);
            if (lazyImageObserver) lazyImageObserver.disconnect();

            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>';

        if (item.type === 'video') {
            if (isIOS) {
                // iOS: poster + play overlay → opens native AVPlayer
                const wrapper = document.createElement('div');
                wrapper.style.cssText = 'position:relative; display:flex; align-items:center; justify-content:center; max-width:100%; max-height:100%; cursor:pointer;';

                if (item.poster) {
                    const img = document.createElement('img');
                    img.src = item.poster;
                    img.className = 'pg-lb-media';
                    img.onload = () => stage.querySelector('.pg-loader')?.remove();
                    img.onerror = () => stage.querySelector('.pg-loader')?.remove();
                    wrapper.appendChild(img);
                } else {
                    stage.querySelector('.pg-loader')?.remove();
                    wrapper.style.width = '80%';
                    wrapper.style.height = '60%';
                    wrapper.style.background = '#111';
                    wrapper.style.borderRadius = '8px';
                }

                const playBtn = document.createElement('div');
                playBtn.className = 'pg-ios-play-btn';
                playBtn.innerHTML = `<svg viewBox="0 0 24 24" width="32" height="32" fill="white"><path d="M8 5v14l11-7z"/></svg>`;

                playBtn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    window.open(item.src, '_blank');
                });
                wrapper.addEventListener('click', (e) => {
                    window.open(item.src, '_blank');
                });

                wrapper.appendChild(playBtn);
                stage.appendChild(wrapper);
            } else {
                const mediaEl = document.createElement('video');
                mediaEl.className = 'pg-lb-media';
                mediaEl.src = item.src;
                mediaEl.controls = true;
                mediaEl.playsInline = true;
                mediaEl.autoplay = true;
                mediaEl.volume = config.volume;
                if (item.poster) mediaEl.poster = item.poster;
                mediaEl.oncanplay = () => stage.querySelector('.pg-loader')?.remove();
                mediaEl.onerror = () => stage.querySelector('.pg-loader')?.remove();
                stage.appendChild(mediaEl);
            }
        } else {
            const mediaEl = document.createElement('img');
            mediaEl.className = 'pg-lb-media';
            mediaEl.src = item.src;
            mediaEl.onload = () => stage.querySelector('.pg-loader')?.remove();
            mediaEl.onerror = () => {
                stage.querySelector('.pg-loader')?.remove();
                stage.insertAdjacentHTML('beforeend', '<div style="color:#fff; text-align:center;">Failed to load full size image.</div>');
            };
            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() {
        const navGroups = document.querySelectorAll('.p-navgroup.p-account');

        navGroups.forEach(navGroup => {
            if (navGroup.querySelector('.pg-gallery-launcher')) return;

            const btn = document.createElement("a");
            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();

})();