LPSG Video Unlocker

Unlocks hidden videos and adds a premium gallery.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==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();

})();