RedGifs Improver

Adds buttons for direct links and downloading to the sidebar.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         RedGifs Improver
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Adds buttons for direct links and downloading to the sidebar.
// @license MIT
// @match        https://www.redgifs.com/*
// @connect      media.redgifs.com
// @connect      v3.redgifs.com
// @grant        GM_download
// @grant        GM_openInTab
// @grant        GM_xmlhttpRequest
// @run-at       document-idle
// ==/UserScript==


(function() {
    'use strict';

    // Global download queue for rate-limiting bulk downloads
    const downloadQueue = [];
    let isProcessingQueue = false;
    const DOWNLOAD_DELAY_MS = 5000;

    function processDownloadQueue() {
        if (isProcessingQueue) return;
        if (downloadQueue.length === 0) return;

        isProcessingQueue = true;
        const { mp4Url, filename } = downloadQueue.shift();

        triggerAutoDownload(mp4Url, filename, () => {
            setTimeout(() => {
                isProcessingQueue = false;
                processDownloadQueue();
            }, DOWNLOAD_DELAY_MS);
        });
    }

    function enqueueDownload(mp4Url, filename) {
        if (!downloadQueue.some(item => item.mp4Url === mp4Url)) {
            downloadQueue.push({ mp4Url, filename });
            processDownloadQueue();
        }
    }

    function allowRightClick(element) {
        if (!element) return;
        ['contextmenu', 'mousedown'].forEach(eventType => {
            element.addEventListener(eventType, function(e) {
                if (e.target.closest('.sideBar, .GifPreview-SideBarWrap, .custom-sidebar-item, .custom-tile-overlay-container')) {
                    return;
                }
                if (e.type === 'contextmenu' || e.button === 2) {
                    e.stopPropagation();
                    e.stopImmediatePropagation();
                }
            }, true);
        });
    }

    function triggerAutoDownload(mp4Url, filename, onComplete) {
        if (typeof GM_download === 'function') {
            GM_download({
                url: mp4Url,
                name: filename,
                saveAs: false,
                onload: () => {
                    if (typeof onComplete === 'function') onComplete();
                },
                onerror: (err) => {
                    console.warn('GM_download failed, attempting blob fallback:', err);
                    fallbackBlobDownload(mp4Url, filename, onComplete);
                }
            });
        } else {
            fallbackBlobDownload(mp4Url, filename, onComplete);
        }
    }

    function fallbackBlobDownload(mp4Url, filename, onComplete) {
        if (typeof GM_xmlhttpRequest === 'function') {
            GM_xmlhttpRequest({
                method: 'GET',
                url: mp4Url,
                responseType: 'blob',
                onload: function(response) {
                    if (response.status === 200) {
                        const blob = response.response;
                        const blobUrl = URL.createObjectURL(blob);
                        const a = document.createElement('a');
                        a.href = blobUrl;
                        a.download = filename;
                        a.style.display = 'none';
                        document.body.appendChild(a);
                        a.click();
                        setTimeout(() => {
                            document.body.removeChild(a);
                            URL.revokeObjectURL(blobUrl);
                            if (typeof onComplete === 'function') onComplete();
                        }, 1000);
                    } else {
                        if (typeof GM_openInTab === 'function') {
                            GM_openInTab(mp4Url, { active: true, insert: true });
                        }
                        if (typeof onComplete === 'function') onComplete();
                    }
                },
                onerror: () => {
                    if (typeof GM_openInTab === 'function') {
                        GM_openInTab(mp4Url, { active: true, insert: true });
                    }
                    if (typeof onComplete === 'function') onComplete();
                }
            });
        } else {
            if (typeof GM_openInTab === 'function') {
                GM_openInTab(mp4Url, { active: true, insert: true });
            }
            if (typeof onComplete === 'function') onComplete();
        }
    }

    function extractCdnUrl(tile) {
        const img = tile.querySelector('img.thumbnail, img[src*="redgifs.com"]');
        if (img && img.src) {
            const match = img.src.match(/\/([A-Za-z0-9]+)-(?:mobile|large|poster)/i);
            if (match && match[1]) {
                return `https://media.redgifs.com/${match[1]}.mp4`;
            }
        }

        let id = tile.getAttribute('data-feed-item-id') ||
                 tile.getAttribute('data-id') ||
                 tile.dataset?.feedItemId ||
                 tile.dataset?.id;

        if (id) {
            const formatted = id.charAt(0).toUpperCase() + id.slice(1);
            return `https://media.redgifs.com/${formatted}.mp4`;
        }

        return null;
    }

    function extractWatchId(tile) {
        let id = tile.getAttribute('data-feed-item-id') ||
                 tile.getAttribute('data-id') ||
                 tile.dataset?.feedItemId ||
                 tile.dataset?.id;
        if (id) return id;

        const img = tile.querySelector('img.thumbnail, img[src*="redgifs.com"]');
        if (img && img.src) {
            const match = img.src.match(/\/([A-Za-z0-9]+)-(?:mobile|large|poster)/i);
            if (match && match[1]) return match[1];
        }

        return null;
    }

    function getTileTimestamp(tile) {
        const img = tile.querySelector('img.thumbnail, img[alt*="uploaded by"]');
        if (!img || !img.alt) return 0;

        const match = img.alt.match(/(\d{2})\/(\d{2})\/(\d{4}),?\s*(\d{2}):(\d{2}):(\d{2})/);
        if (!match) return 0;

        const [, day, month, year, hours, minutes, seconds] = match;
        return new Date(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}`).getTime() || 0;
    }

    function sortTilesByNewest() {
        const tiles = Array.from(document.querySelectorAll('.tileItem, .GifPreview'));
        if (tiles.length < 2) return;

        const parent = tiles[0].parentElement;
        if (!parent) return;

        let needsSort = false;
        const sortedTiles = [...tiles].sort((a, b) => {
            const timeA = getTileTimestamp(a);
            const timeB = getTileTimestamp(b);
            return timeB - timeA;
        });

        for (let i = 0; i < tiles.length; i++) {
            if (tiles[i] !== sortedTiles[i]) {
                needsSort = true;
                break;
            }
        }

        if (needsSort) {
            sortedTiles.forEach(tile => parent.appendChild(tile));
        }
    }

    function processTile(tile) {
        const id = extractWatchId(tile);
        if (!id) return;

        const mp4Url = extractCdnUrl(tile);
        const watchUrl = `https://www.redgifs.com/watch/${id.toLowerCase()}`;

        allowRightClick(tile);

        let overlay = tile.querySelector('.custom-tile-overlay-container');

        if (tile.classList.contains('tileItem') && !tile.querySelector('.sideBar') && !tile.querySelector('.GifPreview-SideBarWrap')) {
            if (!overlay) {
                overlay = document.createElement('div');
                overlay.className = 'custom-tile-overlay-container';
                overlay.style.cssText = 'position: absolute !important; top: 8px !important; right: 8px !important; z-index: 10 !important; display: flex !important; gap: 4px !important; pointer-events: auto !important;';

                if (getComputedStyle(tile).position === 'static') {
                    tile.style.position = 'relative';
                }
                tile.appendChild(overlay);
            } else {
                // Ensure existing container is explicitly positioned at top right
                overlay.style.cssText = 'position: absolute !important; top: 8px !important; right: 8px !important; z-index: 10 !important; display: flex !important; gap: 4px !important; pointer-events: auto !important;';
            }

            if (!overlay.querySelector('.custom-tile-link-btn')) {
                const linkAnchor = document.createElement('a');
                linkAnchor.href = watchUrl;
                linkAnchor.className = 'custom-tile-btn custom-tile-link-btn';
                linkAnchor.title = `Direct link to watch/${id}`;
                linkAnchor.style.cssText = `
                    display: flex !important;
                    align-items: center !important;
                    justify-content: center !important;
                    width: 32px !important;
                    height: 32px !important;
                    background: rgba(235, 250, 99, 0.2) !important;
                    border: 1px solid #EBFA63 !important;
                    border-radius: 50% !important;
                    cursor: pointer !important;
                    box-sizing: border-box !important;
                `;
                linkAnchor.innerHTML = `
                    <svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path d="M12.083 4.16666H12.5C15.7217 4.16666 18.333 6.77801 18.333 9.99999C18.333 13.222 15.7217 15.8333 12.5 15.8333H12.083M7.91634 4.16666H7.49967C4.27801 4.16666 1.66634 6.77801 1.66634 9.99999C1.66634 13.222 4.27801 15.8333 7.49967 15.8333H7.91634M6.66634 9.99999H13.333" stroke="#EBFA63" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
                    </svg>
                `;
                linkAnchor.addEventListener('click', function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    if (typeof GM_openInTab === 'function') {
                        GM_openInTab(watchUrl, { active: true, insert: true });
                    } else {
                        window.location.href = watchUrl;
                    }
                }, true);
                overlay.appendChild(linkAnchor);
            }

            if (mp4Url && !overlay.querySelector('.custom-tile-dl-btn')) {
                const dlAnchor = document.createElement('a');
                dlAnchor.href = mp4Url;
                dlAnchor.className = 'custom-tile-btn custom-tile-dl-btn';
                dlAnchor.title = `Queue download for ${id}.mp4`;
                dlAnchor.style.cssText = `
                    display: flex !important;
                    align-items: center !important;
                    justify-content: center !important;
                    width: 32px !important;
                    height: 32px !important;
                    background: rgba(99, 250, 148, 0.2) !important;
                    border: 1px solid #63FA94 !important;
                    border-radius: 50% !important;
                    cursor: pointer !important;
                    box-sizing: border-box !important;
                `;
                dlAnchor.innerHTML = `
                    <svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10 3.33334V13.3333M10 13.3333L14.1667 9.16667M10 13.3333L5.83334 9.16667M3.33334 16.6667H16.6667" stroke="#63FA94" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
                    </svg>
                `;
                dlAnchor.addEventListener('click', function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    enqueueDownload(mp4Url, `${id}.mp4`);
                }, true);
                overlay.appendChild(dlAnchor);
            }
        } else {
            let sidebar = tile.querySelector('.sideBar, .GifPreview-SideBarWrap ul');
            if (!sidebar) return;

            if (!sidebar.querySelector('.custom-sidebar-link-item')) {
                const linkLi = document.createElement('li');
                linkLi.className = 'sideBarItem custom-sidebar-item custom-sidebar-link-item';
                linkLi.style.cssText = 'margin-bottom: 4px !important;';

                const linkAnchor = document.createElement('a');
                linkAnchor.href = watchUrl;
                linkAnchor.title = `Direct link to watch/${id}`;
                linkAnchor.style.cssText = `
                    display: flex !important;
                    align-items: center !important;
                    justify-content: center !important;
                    width: 32px !important;
                    height: 32px !important;
                    background: rgba(235, 250, 99, 0.2) !important;
                    border: 1px solid #EBFA63 !important;
                    border-radius: 50% !important;
                    cursor: pointer !important;
                    box-sizing: border-box !important;
                `;
                linkAnchor.innerHTML = `
                    <svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path d="M12.083 4.16666H12.5C15.7217 4.16666 18.333 6.77801 18.333 9.99999C18.333 13.222 15.7217 15.8333 12.5 15.8333H12.083M7.91634 4.16666H7.49967C4.27801 4.16666 1.66634 6.77801 1.66634 9.99999C1.66634 13.222 4.27801 15.8333 7.49967 15.8333H7.91634M6.66634 9.99999H13.333" stroke="#EBFA63" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                `;

                linkAnchor.addEventListener('click', function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    if (typeof GM_openInTab === 'function') {
                        GM_openInTab(watchUrl, { active: true, insert: true });
                    } else {
                        window.location.href = watchUrl;
                    }
                }, true);

                linkLi.appendChild(linkAnchor);
                sidebar.prepend(linkLi);
            }

            if (mp4Url && !sidebar.querySelector('.custom-sidebar-download-item')) {
                const dlLi = document.createElement('li');
                dlLi.className = 'sideBarItem custom-sidebar-item custom-sidebar-download-item';
                dlLi.style.cssText = 'margin-bottom: 4px !important;';

                const dlAnchor = document.createElement('a');
                dlAnchor.href = mp4Url;
                dlAnchor.title = `Queue download for ${id}.mp4`;
                dlAnchor.style.cssText = `
                    display: flex !important;
                    align-items: center !important;
                    justify-content: center !important;
                    width: 32px !important;
                    height: 32px !important;
                    background: rgba(99, 250, 148, 0.2) !important;
                    border: 1px solid #63FA94 !important;
                    border-radius: 50% !important;
                    cursor: pointer !important;
                    box-sizing: border-box !important;
                `;
                dlAnchor.innerHTML = `
                    <svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10 3.33334V13.3333M10 13.3333L14.1667 9.16667M10 13.3333L5.83334 9.16667M3.33334 16.6667H16.6667" stroke="#63FA94" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                `;

                dlAnchor.addEventListener('click', function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    enqueueDownload(mp4Url, `${id}.mp4`);
                }, true);

                dlLi.appendChild(dlAnchor);
                sidebar.prepend(dlLi);
            }
        }

        const mediaElements = tile.querySelectorAll('video, img, .Player-Video, .thumbnail');
        mediaElements.forEach(el => allowRightClick(el));
    }

    function scanAll() {
        const targets = document.querySelectorAll('.tileItem, .GifPreview, [class*="GifPreview"]');
        targets.forEach(tile => processTile(tile));

        sortTilesByNewest();
    }

    setInterval(scanAll, 500);
})();