Warosu media grid and hover

Grid view thumbnails show video icon + duration, hover shows full video with dynamic playbar, download link opens media in new tab and is right-clickable

// ==UserScript==
// @name         Warosu media grid and hover
// @namespace    warosu-filter
// @version      1.4
// @description  Grid view thumbnails show video icon + duration, hover shows full video with dynamic playbar, download link opens media in new tab and is right-clickable
// @match        https://warosu.org/*
// @grant        none
// @license MIT 
// ==/UserScript==

(function () {
    'use strict';

    let gridView = false;
    let observer = null;
    let gridContainer = null;
    let preview = null;

    // --- Control button ---
    const gridBtn = document.createElement('button');
    gridBtn.textContent = 'Grid View: OFF';
    Object.assign(gridBtn.style, {
        position: 'fixed', top: '10px', right: '10px', zIndex: '99999',
        padding: '6px 10px', background: '#444', color: 'white',
        border: '1px solid #888', borderRadius: '4px', cursor: 'pointer', fontSize: '13px'
    });
    document.body.appendChild(gridBtn);

    // --- Preview overlay ---
    preview = document.createElement('div');
    Object.assign(preview.style, {
        position: 'fixed', top: 0, left: 0, width: '100%', height: '100%',
        display: 'none', justifyContent: 'center', alignItems: 'center',
        background: 'transparent', zIndex: '99999', pointerEvents: 'none'
    });
    document.body.appendChild(preview);

    // --- Hover preview function ---
    function attachHover(wrapper, media) {
        wrapper.addEventListener('mouseenter', () => {
            preview.innerHTML = '';
            const src = media.tagName.toLowerCase() === 'img'
                ? (media.closest('a')?.href || media.src)
                : (media.currentSrc || media.src);

            const isVideo = media.tagName.toLowerCase() === 'video' || src.match(/\.(webm|mp4)$/i);
            const element = document.createElement(isVideo ? 'video' : 'img');

            if (isVideo) {
                element.src = src;
                element.autoplay = true;
                element.muted = true;
                element.loop = true;
                element.playsInline = true;
            } else {
                element.src = src;
            }

            Object.assign(element.style, {
                maxWidth: '100vw', maxHeight: '100vh',
                objectFit: 'contain', borderRadius: '6px'
            });

            const container = document.createElement('div');
            Object.assign(container.style, {
                position: 'relative', display: 'flex', justifyContent: 'center', alignItems: 'center',
                width: '100%', height: '100%'
            });
            container.appendChild(element);
            preview.appendChild(container);
            preview.style.display = 'flex';

            if (isVideo) {
                // Playbar
                const playbarContainer = document.createElement('div');
                Object.assign(playbarContainer.style, {
                    position: 'absolute', bottom: '20px', height: '8px',
                    background: 'rgba(0,0,0,0.2)', borderRadius: '3px',
                });
                container.appendChild(playbarContainer);

                const playbarFill = document.createElement('div');
                Object.assign(playbarFill.style, { width: '0%', height: '100%', background: 'lime', borderRadius: '3px' });
                playbarContainer.appendChild(playbarFill);

                const timeLabel = document.createElement('div');
                Object.assign(timeLabel.style, {
                    position: 'absolute', bottom: '30px', right: '10px',
                    padding: '2px 4px', fontSize: '12px', color: 'white',
                    background: 'rgba(0,0,0,0.6)', borderRadius: '3px', pointerEvents: 'none'
                });
                container.appendChild(timeLabel);

                function updatePlaybar() {
                    playbarContainer.style.width = element.clientWidth + 'px';
                    playbarContainer.style.left = element.offsetLeft + 'px';
                }

                element.addEventListener('loadedmetadata', () => {
                    updatePlaybar();
                    timeLabel.textContent = `0 / ${Math.floor(element.duration)}s`;
                });

                element.addEventListener('timeupdate', () => {
                    if (element.duration > 0) {
                        const pct = (element.currentTime / element.duration) * 100;
                        playbarFill.style.width = pct + '%';
                        timeLabel.textContent = `${Math.floor(element.currentTime)} / ${Math.floor(element.duration)}s`;
                    }
                });

                window.addEventListener('resize', updatePlaybar);
            }
        });

        wrapper.addEventListener('mouseleave', () => {
            preview.style.display = 'none';
            preview.innerHTML = '';
        });
    }

    // --- Grid View ---
    function buildGrid() {
        if (gridContainer) gridContainer.remove();
        gridContainer = document.createElement('div');
        Object.assign(gridContainer.style, {
            position: 'fixed', top: '50px', left: '0',
            width: '100%', height: 'calc(100% - 60px)',
            background: '#111', display: 'grid',
            gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))',
            gap: '10px', padding: '10px', overflowY: 'scroll',
            zIndex: '99998'
        });

        const mediaElems = document.querySelectorAll('img.thumb, video');
        mediaElems.forEach(media => {
            const post = media.closest('.post, table, div');
            if (!post) return;

            const mediaBlock = document.createElement('div');
            mediaBlock.style.display = 'flex';
            mediaBlock.style.flexDirection = 'column';
            mediaBlock.style.alignItems = 'center';
            mediaBlock.style.background = '#222';
            mediaBlock.style.borderRadius = '6px';
            mediaBlock.style.padding = '4px';
            mediaBlock.style.boxSizing = 'border-box';
            mediaBlock.style.minHeight = '180px';

            const thumbWrapper = document.createElement('a');
            thumbWrapper.href = '#';
            thumbWrapper.style.display = 'flex';
            thumbWrapper.style.justifyContent = 'center';
            thumbWrapper.style.alignItems = 'center';
            thumbWrapper.style.width = '100%';
            thumbWrapper.style.cursor = 'pointer';
            thumbWrapper.style.position = 'relative';

            const clone = media.cloneNode(true);
            Object.assign(clone.style, { maxWidth: '100%', maxHeight: '160px', objectFit: 'contain', display: 'block' });
            thumbWrapper.appendChild(clone);

            thumbWrapper.addEventListener('click', e => {
                e.preventDefault();
                post.scrollIntoView({ behavior: 'smooth', block: 'center' });
                gridView = false;
                gridContainer.remove();
                gridBtn.textContent = 'Grid View: OFF';
                gridBtn.style.background = '#444';
            });

            attachHover(thumbWrapper, media);

            // --- Add video icon + duration overlay for grid ---
            const isVideo = media.tagName.toLowerCase() === 'video' || (media.closest('a')?.href || '').match(/\.(webm|mp4)$/i);
            if (isVideo) {
                const icon = document.createElement('div');
                icon.textContent = '▶';
                Object.assign(icon.style, {
                    position: 'absolute', top: '2px', right: '2px',
                    fontSize: '12px', color: 'white', background: 'rgba(0,0,0,0.6)',
                    borderRadius: '3px', padding: '1px 3px', pointerEvents: 'none'
                });
                thumbWrapper.appendChild(icon);

                const tempVideo = document.createElement('video');
                tempVideo.preload = 'metadata';
                tempVideo.src = media.tagName.toLowerCase() === 'video' ? media.currentSrc || media.src : media.closest('a')?.href;
                tempVideo.addEventListener('loadedmetadata', () => {
                    const durationLabel = document.createElement('div');
                    durationLabel.textContent = Math.floor(tempVideo.duration) + 's';
                    Object.assign(durationLabel.style, {
                        position: 'absolute', top: '2px', left: '2px',
                        fontSize: '12px', color: 'white', background: 'rgba(0,0,0,0.6)',
                        borderRadius: '3px', padding: '1px 3px', pointerEvents: 'none'
                    });
                    thumbWrapper.appendChild(durationLabel);
                });
            }

            // --- Add download link for both images and videos ---
            const downloadLink = document.createElement('a');
            downloadLink.textContent = 'Download';
            downloadLink.href = media.tagName.toLowerCase() === 'video' ? media.currentSrc || media.src : media.closest('a')?.href || media.src;
            downloadLink.target = '_blank';
            downloadLink.style.cssText = `
                margin-top: 4px;
                padding: 2px 6px;
                font-size: 12px;
                background: #0f0;
                border: 1px solid #000;
                border-radius: 3px;
                display: inline-block;
                text-decoration: none;
                color: black;
                text-align: center;
            `;
            // STOP event propagation so right-click works
            downloadLink.addEventListener('click', e => e.stopPropagation());
            downloadLink.addEventListener('mousedown', e => e.stopPropagation());

            mediaBlock.appendChild(thumbWrapper);
            mediaBlock.appendChild(downloadLink);
            gridContainer.appendChild(mediaBlock);
        });

        document.body.appendChild(gridContainer);
    }

    gridBtn.addEventListener('click', () => {
        gridView = !gridView;
        gridBtn.textContent = gridView ? 'Grid View: ON' : 'Grid View: OFF';
        gridBtn.style.background = gridView ? '#2a7' : '#444';

        if (gridView) buildGrid();
        else if (gridContainer) gridContainer.remove();
    });

    // --- Attach hover to all posts ---
    function addHoverToPosts() {
        const mediaElems = document.querySelectorAll('.comment img, .comment video');
        mediaElems.forEach(media => {
            if (media.dataset.hoverAttached) return;
            media.dataset.hoverAttached = 'true';
            media.style.cursor = 'pointer';
            attachHover(media, media);
        });
    }

    addHoverToPosts();

    if (!observer) {
        observer = new MutationObserver(() => addHoverToPosts());
        observer.observe(document.body, { childList: true, subtree: true });
    }

})();