LPSG Video Unlocker & Premium Gallery

Unlocks hidden videos and adds a premium, mobile-friendly gallery with a top navigation entry.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         LPSG Video Unlocker & Premium Gallery
// @namespace    MBing & CurlyWurly
// @version      4.6
// @description  Unlocks hidden videos and adds a premium, mobile-friendly gallery with a top navigation entry.
// @author       MBing & CurlyWurly
// @match        https://www.lpsg.com/*
// @icon         data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pjxzdmcgdmlld0JveD0iMCAwIDI1NiAyNTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3QgZmlsbD0ibm9uZSIgaGVpZ2h0PSIyNTYiIHdpZHRoPSIyNTYiLz48cGF0aCBkPSJNOTMuMiwxMjIuOEE3MC4zLDcwLjMsMCwwLDEsODgsOTZhNzIsNzIsMCwxLDEsNzIsNzIsNzAuMyw3MC4zLDAsMCwxLTI2LjgtNS4yaDBMMTIwLDE3Nkg5NnYyNEg3MnYyNEgzMlYxODRsNjEuMi02MS4yWiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRkZENzAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMTYiLz48Y2lyY2xlIGN4PSIxODAiIGN5PSI3NiIgcj0iMTIiIGZpbGw9IiNGRkQ3MDAiLz48L3N2Zz4=
// @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'
    };

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

        .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);
        }
        .pg-badge.video { color: var(--pg-highlight); }

        /* --- 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;
        }
        .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 Grid Icon ---
    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"><path d="M14 5.6C14 5.03995 14 4.75992 14.109 4.54601C14.2049 4.35785 14.3578 4.20487 14.546 4.10899C14.7599 4 15.0399 4 15.6 4H18.4C18.9601 4 19.2401 4 19.454 4.10899C19.6422 4.20487 19.7951 4.35785 19.891 4.54601C20 4.75992 20 5.03995 20 5.6V8.4C20 8.96005 20 9.24008 19.891 9.45399C19.7951 9.64215 19.6422 9.79513 19.454 9.89101C19.2401 10 18.9601 10 18.4 10H15.6C15.0399 10 14.7599 10 14.546 9.89101C14.3578 9.79513 14.2049 9.64215 14.109 9.45399C14 9.24008 14 8.96005 14 8.4V5.6Z"></path><path d="M4 5.6C4 5.03995 4 4.75992 4.10899 4.54601C4.20487 4.35785 4.35785 4.20487 4.54601 4.10899C4.75992 4 5.03995 4 5.6 4H8.4C8.96005 4 9.24008 4 9.45399 4.10899C9.64215 4.20487 9.79513 4.35785 9.89101 4.54601C10 4.75992 10 5.03995 10 5.6V8.4C10 8.96005 10 9.24008 9.89101 9.45399C9.79513 9.64215 9.64215 9.79513 9.45399 9.89101C9.24008 10 8.96005 10 8.4 10H5.6C5.03995 10 4.75992 10 4.54601 9.89101C4.35785 9.79513 4.20487 9.64215 4.10899 9.45399C4 9.24008 4 8.96005 4 8.4V5.6Z"></path><path d="M4 15.6C4 15.0399 4 14.7599 4.10899 14.546C4.20487 14.3578 4.35785 14.2049 4.54601 14.109C4.75992 14 5.03995 14 5.6 14H8.4C8.96005 14 9.24008 14 9.45399 14.109C9.64215 14.2049 9.79513 14.3578 9.89101 14.546C10 14.7599 10 15.0399 10 15.6V18.4C10 18.9601 10 19.2401 9.89101 19.454C9.79513 19.6422 9.64215 19.7951 9.45399 19.891C9.24008 20 8.96005 20 8.4 20H5.6C5.03995 20 4.75992 20 4.54601 19.891C4.35785 19.7951 4.20487 19.6422 4.10899 19.454C4 19.2401 4 18.9601 4 18.4V15.6Z"></path><path d="M14 15.6C14 15.0399 14 14.7599 14.109 14.546C14.2049 14.3578 14.3578 14.2049 14.546 14.109C14.7599 14 15.0399 14 15.6 14H18.4C18.9601 14 19.2401 14 19.454 14.109C19.6422 14.2049 19.7951 14.3578 19.891 14.546C20 14.7599 20 15.0399 20 15.6V18.4C20 18.9601 20 19.2401 19.891 19.454C19.7951 19.6422 19.6422 19.7951 19.454 19.891C19.2401 20 18.9601 20 18.4 20H15.6C15.0399 20 14.7599 20 14.546 19.891C14.3578 19.7951 14.2049 19.6422 14.109 19.454C14 19.2401 14 18.9601 14 18.4V15.6Z"></path></svg>`;
    // --------------------

    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 container = poster.parentElement.parentElement;

            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">
                    <video class="message-cell--main-video" controls playsinline preload="metadata"
                           poster="${imageUrl}" style="width: 100%; height: auto; display: block;">
                        ${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;
        });
    }

    let galleryState = {
        media: [],
        filteredMedia: [],
        currentIndex: 0,
        filter: 'all',
        container: null,
        lightbox: null
    };

    function scanMedia() {
        const videos = Array.from(document.querySelectorAll('.message-cell--main video')).map(v => ({
            type: 'video',
            el: v,
            src: v.currentSrc || v.querySelector('source')?.src,
            poster: v.poster,
            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;

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

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

        galleryState.filteredMedia.forEach((item, index) => {
            const card = document.createElement('div');
            card.className = 'pg-item';

            let content = '';
            let badge = '';

            if (item.type === 'video') {
                content = `<img src="${item.poster}" loading="lazy">`;
                badge = '<span class="pg-badge video">VIDEO</span>';
            } else {
                content = `<img src="${item.thumb}" loading="lazy">`;
                badge = item.subType === 'gif' ? '<span class="pg-badge">GIF</span>' : '<span class="pg-badge">IMG</span>';
            }

            card.innerHTML = `${badge}${content}`;
            card.onclick = () => openLightbox(index);

            if (item.type === 'video') {
                let previewVideo = null;
                card.onmouseenter = () => {
                    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(() => {});
                };
                card.onmouseleave = () => {
                    if (previewVideo) {
                        previewVideo.remove();
                        previewVideo = null;
                    }
                };
            }

            grid.appendChild(card);
        });
    }

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

    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;
        } else {
            mediaEl = document.createElement('img');
            mediaEl.className = 'pg-lb-media';
            mediaEl.src = item.src;
        }

        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() {
        const navGroup = document.querySelector('.p-navgroup.p-account');
        if (navGroup && !document.getElementById('pg-top-launcher')) {
            const btn = document.createElement("a");
            btn.id = "pg-top-launcher";
            btn.className = "p-navgroup-link u-ripple p-navgroup-link--iconic p-navgroup-link--iconic--media";
            btn.title = "Open Gallery";
            btn.href = "#";

            // Prevent default link behavior and open gallery
            btn.onclick = (e) => {
                e.preventDefault();
                e.stopPropagation();
                openGallery();
            };

            btn.innerHTML = `
                <i aria-hidden="true"><i class="fa fa-images"></i></i>
                <span class="p-navgroup-linkText">Gallery</span>
            `;

            // Insert at the end of the nav group
            navGroup.appendChild(btn);
        }
    }

    unlockVideos();
    setTimeout(unlockVideos, 1500);
    // Add a delay for button creation in case the nav renders slowly
    setTimeout(createLaunchButton, 500);
    // Run immediate attempt
    createLaunchButton();

})();