Old Reddit GearTools

Utility tools for old Reddit - works alongside filter scripts

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.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

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         Old Reddit GearTools
// @namespace    http://tampermonkey.net/
// @version      1.2.1
// @description  Utility tools for old Reddit - works alongside filter scripts
// @author       Crates
// @license      MIT
// @match        https://old.reddit.com/*
// @match        https://www.reddit.com/*
// @grant        GM_setClipboard
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_notification
// @grant        GM_addStyle
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // ========== EARLY STYLE INJECTION (before DOM loads) ==========
    // Inject critical styles immediately to prevent flicker
    let sidebarHidden = GM_getValue('sidebarHidden', true);
    let subCssDisabled = GM_getValue('subCssDisabled', false);

    function updateEarlyStyles() {
        const earlyStyleEl = document.getElementById('pwr-early-styles');
        if (earlyStyleEl) {
            earlyStyleEl.textContent = `
                ${sidebarHidden ? `
                .side {
                    display: none !important;
                }
                .content {
                    margin-right: 10px !important;
                }
                ` : ''}
                ${subCssDisabled ? `
                link[title="applied_subreddit_stylesheet"] {
                    display: none !important;
                }
                ` : ''}
            `;
        }
    }

    const earlyStyles = `
        ${sidebarHidden ? `
        .side {
            display: none !important;
        }
        .content {
            margin-right: 10px !important;
        }
        ` : ''}
        ${subCssDisabled ? `
        link[title="applied_subreddit_stylesheet"] {
            display: none !important;
        }
        ` : ''}
    `;

    // Inject styles as early as possible
    const earlyStyleEl = document.createElement('style');
    earlyStyleEl.id = 'pwr-early-styles';
    earlyStyleEl.textContent = earlyStyles;
    (document.head || document.documentElement).appendChild(earlyStyleEl);

    // Disable subreddit CSS immediately if setting is on
    if (subCssDisabled) {
        // Use MutationObserver to catch and disable stylesheet as soon as it's added
        const cssObserver = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                mutation.addedNodes.forEach((node) => {
                    if (node.tagName === 'LINK' && node.title === 'applied_subreddit_stylesheet') {
                        node.disabled = true;
                        node.dataset.pwrDisabled = 'true';
                    }
                });
            });
        });
        cssObserver.observe(document.documentElement, { childList: true, subtree: true });

        // Store observer to disconnect later
        window.pwrCssObserver = cssObserver;
    }

    // ========== WAIT FOR DOM TO CONTINUE ==========
    function onDOMReady(fn) {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', fn);
        } else {
            fn();
        }
    }

    onDOMReady(function() {
        // Only run on old reddit
        if (!document.querySelector('#header-bottom-left')) return;

        // Disconnect early CSS observer if it exists
        if (window.pwrCssObserver) {
            window.pwrCssObserver.disconnect();
        }

    // ========== STYLES ==========
    const styles = `
        #pwr-tools-trig {
            cursor: pointer;
            color: #369;
        }
        #pwr-tools-trig:hover {
            text-decoration: underline;
        }
        #pwr-tools-trig svg {
            vertical-align: middle;
            margin-top: -2px;
        }

        #pwr-tools-dropdown {
            display: none;
            position: absolute;
            top: 100%;
            left: 0;
            background: #fff;
            border: 1px solid #c7c7c7;
            border-radius: 0 0 3px 3px;
            box-shadow: 0 2px 6px rgba(0,0,0,0.15);
            z-index: 10001;
            min-width: 220px;
            font-size: 12px;
            padding: 0;
        }

        #pwr-tools-dropdown.active {
            display: block;
        }

        .pwr-tools-header {
            background: #f6f7f8;
            border-bottom: 1px solid #c7c7c7;
            padding: 8px 12px;
            font-weight: bold;
            color: #333;
            font-size: 11px;
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }

        .pwr-tools-item {
            display: flex;
            align-items: center;
            padding: 10px 12px;
            cursor: pointer;
            color: #333;
            border-bottom: 1px solid #ededed;
            transition: background 0.15s;
        }

        .pwr-tools-item:last-child {
            border-bottom: none;
        }

        .pwr-tools-item:hover {
            background: #f0f7ff;
        }

        .pwr-tools-item svg {
            margin-right: 10px;
            flex-shrink: 0;
            color: #666;
        }

        .pwr-tools-item:hover svg {
            color: #369;
        }

        .pwr-tools-item-text {
            flex: 1;
        }

        .pwr-tools-item-title {
            font-weight: 500;
            color: #333;
        }

        .pwr-tools-item-desc {
            font-size: 10px;
            color: #888;
            margin-top: 2px;
        }

        .pwr-tools-item.disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }

        .pwr-tools-item.disabled:hover {
            background: #fff;
        }

        .pwr-tools-toast {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background: #333;
            color: #fff;
            padding: 12px 20px;
            border-radius: 4px;
            font-size: 13px;
            z-index: 100000;
            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
            animation: pwr-toast-in 0.3s ease;
        }

        .pwr-tools-toast.success {
            background: #5a9e5a;
        }

        .pwr-tools-toast.error {
            background: #c44;
        }

        @keyframes pwr-toast-in {
            from {
                opacity: 0;
                transform: translateY(20px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }

        .pwr-tools-li {
            position: relative;
        }

        /* Modal styles */
        .pwr-tools-modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0,0,0,0.5);
            z-index: 100001;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .pwr-tools-modal {
            background: #fff;
            border-radius: 4px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.3);
            max-width: 500px;
            width: 90%;
            max-height: 80vh;
            display: flex;
            flex-direction: column;
        }

        .pwr-tools-modal-header {
            padding: 15px 20px;
            border-bottom: 1px solid #c7c7c7;
            font-weight: bold;
            font-size: 14px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .pwr-tools-modal-close {
            cursor: pointer;
            font-size: 20px;
            color: #888;
            line-height: 1;
        }

        .pwr-tools-modal-close:hover {
            color: #333;
        }

        .pwr-tools-modal-body {
            padding: 20px;
            overflow-y: auto;
            flex: 1;
        }

        .pwr-tools-modal textarea {
            width: 100%;
            height: 200px;
            font-family: monospace;
            font-size: 11px;
            border: 1px solid #c7c7c7;
            border-radius: 3px;
            padding: 10px;
            resize: vertical;
        }

        .pwr-tools-modal-footer {
            padding: 15px 20px;
            border-top: 1px solid #c7c7c7;
            display: flex;
            justify-content: flex-end;
            gap: 10px;
        }

        .pwr-tools-btn {
            padding: 8px 16px;
            border: 1px solid #c7c7c7;
            border-radius: 3px;
            cursor: pointer;
            font-size: 12px;
            background: #f6f7f8;
        }

        .pwr-tools-btn:hover {
            background: #eee;
        }

        .pwr-tools-btn-primary {
            background: #369;
            color: #fff;
            border-color: #369;
        }

        .pwr-tools-btn-primary:hover {
            background: #2a5a8a;
        }

        .pwr-tools-count {
            background: #369;
            color: #fff;
            font-size: 10px;
            padding: 2px 6px;
            border-radius: 10px;
            margin-left: 8px;
        }

        /* NSFW Blur styles */
        .pwr-nsfw-blurred a.thumbnail img {
            filter: blur(15px) !important;
            transition: filter 0.2s;
        }

        .pwr-nsfw-blurred a.thumbnail:hover img {
            filter: blur(5px) !important;
        }

        .pwr-nsfw-blur-content {
            filter: blur(20px);
            transition: filter 0.2s;
        }

        .pwr-nsfw-blur-content:hover {
            filter: blur(3px);
        }

        .pwr-tools-active {
            background: #5a9e5a;
            color: #fff;
            font-size: 9px;
            padding: 2px 5px;
            border-radius: 3px;
            margin-left: 6px;
            font-weight: normal;
        }

        /* Sidebar hidden styles */
        .pwr-sidebar-hidden .side {
            display: none !important;
        }

        .pwr-sidebar-hidden .content {
            margin-right: 10px !important;
        }

        /* Disable sub CSS indicator */
        .pwr-nocss-active link[rel="stylesheet"][href*="reddit.com/r/"],
        .pwr-nocss-active style[data-subreddit],
        .pwr-nocss-active .stylesheet-customize-container {
            display: none !important;
        }
    `;

        const styleEl = document.createElement('style');
        styleEl.textContent = styles;
        document.head.appendChild(styleEl);

        // ========== ICONS ==========
        const icons = {
        gear: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
            <circle cx="12" cy="12" r="3"></circle>
            <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
        </svg>`,
        video: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <polygon points="23 7 16 12 23 17 23 7"></polygon>
            <rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
        </svg>`,
        image: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
            <circle cx="8.5" cy="8.5" r="1.5"></circle>
            <polyline points="21 15 16 10 5 21"></polyline>
        </svg>`,
        link: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
            <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
        </svg>`,
        expand: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <polyline points="15 3 21 3 21 9"></polyline>
            <polyline points="9 21 3 21 3 15"></polyline>
            <line x1="21" y1="3" x2="14" y2="10"></line>
            <line x1="3" y1="21" x2="10" y2="14"></line>
        </svg>`,
        collapse: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <polyline points="4 14 10 14 10 20"></polyline>
            <polyline points="20 10 14 10 14 4"></polyline>
            <line x1="14" y1="10" x2="21" y2="3"></line>
            <line x1="3" y1="21" x2="10" y2="14"></line>
        </svg>`,
        download: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
            <polyline points="7 10 12 15 17 10"></polyline>
            <line x1="12" y1="15" x2="12" y2="3"></line>
        </svg>`,
        eye: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
            <circle cx="12" cy="12" r="3"></circle>
        </svg>`,
        eyeOff: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
            <line x1="1" y1="1" x2="23" y2="23"></line>
        </svg>`,
        sidebar: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
            <line x1="15" y1="3" x2="15" y2="21"></line>
        </svg>`,
        load: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <polyline points="1 4 1 10 7 10"></polyline>
            <path d="M3.51 15a9 9 0 1 0 .49-3.5"></path>
        </svg>`,
        css: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <polyline points="16 18 22 12 16 6"></polyline>
            <polyline points="8 6 2 12 8 18"></polyline>
            <line x1="14" y1="4" x2="10" y2="20"></line>
        </svg>`
    };

    // ========== UTILITIES ==========
    function escapeHtml(s) {
        return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
    }

    function showToast(message, type = 'info') {
        const existing = document.querySelector('.pwr-tools-toast');
        if (existing) existing.remove();

        const toast = document.createElement('div');
        toast.className = `pwr-tools-toast ${type}`;
        toast.textContent = message;
        document.body.appendChild(toast);

        setTimeout(() => toast.remove(), 3000);
    }

    function getVisiblePosts() {
        // Get all posts, respecting any filter that may have hidden some
        const posts = document.querySelectorAll('#siteTable > .thing.link');
        return Array.from(posts).filter(post => {
            const style = window.getComputedStyle(post);
            return style.display !== 'none' && style.visibility !== 'hidden';
        });
    }

    function copyToClipboard(text) {
        if (typeof GM_setClipboard === 'function') {
            GM_setClipboard(text);
            return true;
        }
        // Fallback
        const textarea = document.createElement('textarea');
        textarea.value = text;
        textarea.style.position = 'fixed';
        textarea.style.opacity = '0';
        document.body.appendChild(textarea);
        textarea.select();
        const success = document.execCommand('copy');
        document.body.removeChild(textarea);
        return success;
    }

    function showModal(title, content, buttons = []) {
        const overlay = document.createElement('div');
        overlay.className = 'pwr-tools-modal-overlay';

        const modal = document.createElement('div');
        modal.className = 'pwr-tools-modal';

        modal.innerHTML = `
            <div class="pwr-tools-modal-header">
                <span>${title}</span>
                <span class="pwr-tools-modal-close">&times;</span>
            </div>
            <div class="pwr-tools-modal-body">${content}</div>
            <div class="pwr-tools-modal-footer"></div>
        `;

        const footer = modal.querySelector('.pwr-tools-modal-footer');
        buttons.forEach(btn => {
            const button = document.createElement('button');
            button.className = `pwr-tools-btn ${btn.primary ? 'pwr-tools-btn-primary' : ''}`;
            button.textContent = btn.text;
            button.onclick = () => {
                if (btn.onClick) btn.onClick(modal);
                if (btn.close !== false) overlay.remove();
            };
            footer.appendChild(button);
        });

        modal.querySelector('.pwr-tools-modal-close').onclick = () => overlay.remove();
        overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };

        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        return modal;
    }

    // ========== TOOL FUNCTIONS ==========

    // Tool 1: Copy Video URLs (Redgifs, Imgur, Gfycat, etc.)
    function copyVideoUrls() {
        const posts = getVisiblePosts();
        const videoUrls = [];

        const videoPatterns = [
            /redgifs\.com/i,
            /gfycat\.com/i,
            /imgur\.com.*\.(gifv|mp4|gif)/i,
            /v\.redd\.it/i,
            /streamable\.com/i,
            /streamja\.com/i,
            /streamff\.com/i,
            /dubz\.co/i,
            /streamwo\.com/i
        ];

        posts.forEach(post => {
            const link = post.querySelector('a.title');
            if (!link) return;

            const href = link.href;
            if (videoPatterns.some(p => p.test(href))) {
                videoUrls.push({
                    title: link.textContent.trim().substring(0, 60),
                    url: href
                });
            }

            // Also check for embedded v.redd.it
            const dataUrl = post.getAttribute('data-url');
            if (dataUrl && /v\.redd\.it/i.test(dataUrl) && !videoUrls.find(v => v.url === dataUrl)) {
                videoUrls.push({
                    title: link.textContent.trim().substring(0, 60),
                    url: dataUrl
                });
            }
        });

        if (videoUrls.length === 0) {
            showToast('No video URLs found in visible posts', 'error');
            return;
        }

        const urlText = videoUrls.map(v => v.url).join('\n');
        const listHtml = videoUrls.map(v =>
            `<div style="margin-bottom:8px;padding-bottom:8px;border-bottom:1px solid #eee;">
                <div style="font-size:11px;color:#666;margin-bottom:2px;">${escapeHtml(v.title)}...</div>
                <div style="font-family:monospace;font-size:10px;word-break:break-all;">${escapeHtml(v.url)}</div>
            </div>`
        ).join('');

        showModal(
            `Video URLs Found (${videoUrls.length})`,
            `<div style="max-height:300px;overflow-y:auto;margin-bottom:10px;">${listHtml}</div>
             <textarea readonly style="width:100%;height:100px;font-size:10px;">${urlText}</textarea>`,
            [
                { text: 'Cancel' },
                { text: 'Copy All', primary: true, onClick: () => {
                    copyToClipboard(urlText);
                    showToast(`Copied ${videoUrls.length} video URLs!`, 'success');
                }}
            ]
        );
    }

    // Tool 2: Copy Image URLs
    function copyImageUrls() {
        const posts = getVisiblePosts();
        const imageUrls = [];

        const imagePatterns = [
            /i\.redd\.it/i,
            /imgur\.com.*\.(jpg|jpeg|png|gif)$/i,
            /i\.imgur\.com/i,
            /preview\.redd\.it/i
        ];

        posts.forEach(post => {
            const link = post.querySelector('a.title');
            if (!link) return;

            const href = link.href;
            const dataUrl = post.getAttribute('data-url') || '';

            // Check main link
            if (imagePatterns.some(p => p.test(href)) || /\.(jpg|jpeg|png|gif|webp)$/i.test(href)) {
                imageUrls.push({
                    title: link.textContent.trim().substring(0, 60),
                    url: href
                });
            }
            // Check data-url
            else if (imagePatterns.some(p => p.test(dataUrl)) || /\.(jpg|jpeg|png|gif|webp)$/i.test(dataUrl)) {
                imageUrls.push({
                    title: link.textContent.trim().substring(0, 60),
                    url: dataUrl
                });
            }

        });

        if (imageUrls.length === 0) {
            showToast('No image URLs found in visible posts', 'error');
            return;
        }

        const urlText = imageUrls.map(i => i.url).join('\n');

        showModal(
            `Image URLs Found (${imageUrls.length})`,
            `<textarea readonly style="width:100%;height:250px;font-size:10px;">${urlText}</textarea>`,
            [
                { text: 'Cancel' },
                { text: 'Copy All', primary: true, onClick: () => {
                    copyToClipboard(urlText);
                    showToast(`Copied ${imageUrls.length} image URLs!`, 'success');
                }}
            ]
        );
    }

    // Tool 3: Copy All Post Links
    function copyAllPostLinks() {
        const posts = getVisiblePosts();
        const links = [];

        posts.forEach(post => {
            const titleLink = post.querySelector('a.title');
            const commentsLink = post.querySelector('a.comments');

            if (titleLink) {
                links.push({
                    title: titleLink.textContent.trim().substring(0, 80),
                    contentUrl: titleLink.href,
                    commentsUrl: commentsLink ? commentsLink.href : null
                });
            }
        });

        if (links.length === 0) {
            showToast('No posts found', 'error');
            return;
        }

        const format = `Title | Content URL | Comments URL\n${'='.repeat(80)}\n` +
            links.map(l => `${l.title}\n  Content: ${l.contentUrl}\n  Comments: ${l.commentsUrl || 'N/A'}`).join('\n\n');

        showModal(
            `Post Links (${links.length})`,
            `<textarea readonly style="width:100%;height:300px;font-size:10px;">${format}</textarea>`,
            [
                { text: 'Cancel' },
                { text: 'Copy URLs Only', onClick: () => {
                    const urlsOnly = links.map(l => l.contentUrl).join('\n');
                    copyToClipboard(urlsOnly);
                    showToast(`Copied ${links.length} content URLs!`, 'success');
                }},
                { text: 'Copy All', primary: true, onClick: () => {
                    copyToClipboard(format);
                    showToast(`Copied ${links.length} post details!`, 'success');
                }}
            ]
        );
    }

    // Tool 4: Expand/Collapse All Previews
    function toggleAllPreviews() {
        const posts = getVisiblePosts();
        const expanders = [];

        posts.forEach(post => {
            // Find expando button - check it exists and is visible
            const expando = post.querySelector('.expando-button');
            if (expando &&
                !expando.classList.contains('hidden') &&
                expando.offsetParent !== null) {
                expanders.push(expando);
            }
        });

        if (expanders.length === 0) {
            showToast('No expandable content found', 'error');
            return;
        }

        // Determine current state - if most are collapsed, expand; otherwise collapse
        const collapsedCount = expanders.filter(btn => btn.classList.contains('collapsed')).length;
        const shouldExpand = collapsedCount > expanders.length / 2;

        let toggled = 0;

        // Store scroll position
        const scrollPos = window.scrollY;

        expanders.forEach(btn => {
            const isCollapsed = btn.classList.contains('collapsed');
            if ((shouldExpand && isCollapsed) || (!shouldExpand && !isCollapsed)) {
                // Simple click - view property doesn't work in userscript sandbox
                btn.click();
                toggled++;
            }
        });

        // Restore scroll position after a brief delay
        requestAnimationFrame(() => {
            window.scrollTo(window.scrollX, scrollPos);
        });

        showToast(`${shouldExpand ? 'Expanded' : 'Collapsed'} ${toggled} previews`, 'success');
    }

    // Tool 6: Blur NSFW Content
    let nsfwBlurred = GM_getValue('nsfwBlurred', false);

    function applyNsfwBlur() {
        const posts = document.querySelectorAll('#siteTable > .thing.link');
        posts.forEach(post => {
            const isNsfw = post.classList.contains('over18') || post.querySelector('.nsfw-stamp');
            const thumbnail = post.querySelector('a.thumbnail img');
            const expando = post.querySelector('.expando');

            if (isNsfw) {
                if (nsfwBlurred) {
                    post.classList.add('pwr-nsfw-blurred');
                    if (thumbnail) thumbnail.style.filter = 'blur(15px)';
                    if (expando) expando.classList.add('pwr-nsfw-blur-content');
                } else {
                    post.classList.remove('pwr-nsfw-blurred');
                    if (thumbnail) thumbnail.style.filter = '';
                    if (expando) expando.classList.remove('pwr-nsfw-blur-content');
                }
            }
        });
    }

    function toggleNsfwBlur() {
        nsfwBlurred = !nsfwBlurred;
        GM_setValue('nsfwBlurred', nsfwBlurred);
        applyNsfwBlur();

        const count = document.querySelectorAll('.pwr-nsfw-blurred').length;
        showToast(nsfwBlurred ? `NSFW blur enabled (${count} posts)` : 'NSFW blur disabled', 'success');

        // Update the menu item indicator
        updateNsfwMenuItem();
    }

    function updateNsfwMenuItem() {
        const nsfwItem = document.querySelector('[data-tool="nsfw"]');
        if (nsfwItem) {
            const title = nsfwItem.querySelector('.pwr-tools-item-title');
            if (title) {
                title.innerHTML = nsfwBlurred ? 'Blur NSFW <span class="pwr-tools-active">ON</span>' : 'Blur NSFW';
            }
        }
    }

    // Tool 7: Toggle Sidebar
    // sidebarHidden is declared at top level for early style injection

    function applySidebarState() {
        const body = document.body;
        if (sidebarHidden) {
            body.classList.add('pwr-sidebar-hidden');
        } else {
            body.classList.remove('pwr-sidebar-hidden');
        }
        // Update early styles
        updateEarlyStyles();
    }

    function toggleSidebar() {
        sidebarHidden = !sidebarHidden;
        GM_setValue('sidebarHidden', sidebarHidden);
        applySidebarState();

        showToast(sidebarHidden ? 'Sidebar hidden' : 'Sidebar visible', 'success');

        // Update the menu item indicator
        updateSidebarMenuItem();
    }

    function updateSidebarMenuItem() {
        const sidebarItem = document.querySelector('[data-tool="sidebar"]');
        if (sidebarItem) {
            const title = sidebarItem.querySelector('.pwr-tools-item-title');
            if (title) {
                title.innerHTML = sidebarHidden ? 'Toggle Sidebar <span class="pwr-tools-active">HIDDEN</span>' : 'Toggle Sidebar';
            }
        }
    }

    // Tool 8: Disable Subreddit CSS
    // subCssDisabled is declared at top level for early style injection

    function applySubCssState() {
        const body = document.body;
        if (subCssDisabled) {
            body.classList.add('pwr-nocss-active');
            // Also remove subreddit stylesheet links directly
            document.querySelectorAll('link[rel="stylesheet"]').forEach(link => {
                if (link.href && link.href.includes('/r/') && link.href.includes('stylesheet')) {
                    link.disabled = true;
                    link.dataset.pwrDisabled = 'true';
                }
            });
            // Handle the main subreddit stylesheet
            const subStylesheet = document.querySelector('link[title="applied_subreddit_stylesheet"]');
            if (subStylesheet) {
                subStylesheet.disabled = true;
                subStylesheet.dataset.pwrDisabled = 'true';
            }
        } else {
            body.classList.remove('pwr-nocss-active');
            // Re-enable stylesheets
            document.querySelectorAll('link[data-pwr-disabled="true"]').forEach(link => {
                link.disabled = false;
                delete link.dataset.pwrDisabled;
            });
        }
        // Update early styles
        updateEarlyStyles();
    }

    function toggleSubCss() {
        subCssDisabled = !subCssDisabled;
        GM_setValue('subCssDisabled', subCssDisabled);
        applySubCssState();

        showToast(subCssDisabled ? 'Subreddit CSS disabled' : 'Subreddit CSS enabled', 'success');

        // Update the menu item indicator
        updateSubCssMenuItem();
    }

    function updateSubCssMenuItem() {
        const cssItem = document.querySelector('[data-tool="subcss"]');
        if (cssItem) {
            const title = cssItem.querySelector('.pwr-tools-item-title');
            if (title) {
                title.innerHTML = subCssDisabled ? 'Disable Sub CSS <span class="pwr-tools-active">OFF</span>' : 'Disable Sub CSS';
            }
        }
    }

    // Watch for new posts loaded dynamically (RES infinite scroll, etc.)
    const observer = new MutationObserver((mutations) => {
        if (nsfwBlurred) {
            applyNsfwBlur();
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // Tool 5: Quick Stats for Visible Posts
    function showQuickStats() {
        const posts = getVisiblePosts();
        const stats = {
            total: posts.length,
            images: 0,
            videos: 0,
            links: 0,
            selfPosts: 0,
            totalScore: 0,
            totalComments: 0,
            domains: {}
        };

        posts.forEach(post => {
            // Score
            const score = post.querySelector('.score.unvoted');
            if (score) {
                const val = parseInt(score.textContent);
                if (!isNaN(val)) stats.totalScore += val;
            }

            // Comments
            const comments = post.querySelector('a.comments');
            if (comments) {
                const match = comments.textContent.match(/(\d+)/);
                if (match) stats.totalComments += parseInt(match[1]);
            }

            // Type
            if (post.classList.contains('self')) {
                stats.selfPosts++;
            } else {
                const dataUrl = post.getAttribute('data-url') || '';
                const titleHref = post.querySelector('a.title')?.href || '';

                if (/v\.redd\.it|redgifs|gfycat|streamable/i.test(dataUrl + titleHref)) {
                    stats.videos++;
                } else if (/i\.redd\.it|imgur|preview\.redd\.it/i.test(dataUrl + titleHref) ||
                           /\.(jpg|jpeg|png|gif|webp)$/i.test(dataUrl + titleHref)) {
                    stats.images++;
                } else {
                    stats.links++;
                }
            }

            // Domain
            const domain = post.querySelector('.domain a');
            if (domain) {
                const d = domain.textContent.replace(/[()]/g, '');
                stats.domains[d] = (stats.domains[d] || 0) + 1;
            }
        });

        const topDomains = Object.entries(stats.domains)
            .sort((a, b) => b[1] - a[1])
            .slice(0, 5)
            .map(([d, c]) => `${d}: ${c}`)
            .join('<br>');

        showModal(
            'Quick Stats for Visible Posts',
            `<table style="width:100%;border-collapse:collapse;">
                <tr><td style="padding:8px;border-bottom:1px solid #eee;"><strong>Total Posts</strong></td><td style="text-align:right;padding:8px;border-bottom:1px solid #eee;">${stats.total}</td></tr>
                <tr><td style="padding:8px;border-bottom:1px solid #eee;">📷 Images</td><td style="text-align:right;padding:8px;border-bottom:1px solid #eee;">${stats.images}</td></tr>
                <tr><td style="padding:8px;border-bottom:1px solid #eee;">🎬 Videos</td><td style="text-align:right;padding:8px;border-bottom:1px solid #eee;">${stats.videos}</td></tr>
                <tr><td style="padding:8px;border-bottom:1px solid #eee;">🔗 Links</td><td style="text-align:right;padding:8px;border-bottom:1px solid #eee;">${stats.links}</td></tr>
                <tr><td style="padding:8px;border-bottom:1px solid #eee;">📝 Self Posts</td><td style="text-align:right;padding:8px;border-bottom:1px solid #eee;">${stats.selfPosts}</td></tr>
                <tr><td style="padding:8px;border-bottom:1px solid #eee;"><strong>Total Score</strong></td><td style="text-align:right;padding:8px;border-bottom:1px solid #eee;">${stats.totalScore.toLocaleString()}</td></tr>
                <tr><td style="padding:8px;border-bottom:1px solid #eee;"><strong>Total Comments</strong></td><td style="text-align:right;padding:8px;border-bottom:1px solid #eee;">${stats.totalComments.toLocaleString()}</td></tr>
                <tr><td colspan="2" style="padding:12px 8px 4px;"><strong>Top Domains</strong></td></tr>
                <tr><td colspan="2" style="padding:4px 8px 8px;font-size:11px;color:#666;">${topDomains || 'None'}</td></tr>
            </table>`,
            [{ text: 'Close', primary: true }]
        );
    }

    // Tool 9: Load N Posts
    function loadAndExpand() {
        // Don't run on comment pages
        if (document.querySelector('.commentarea')) {
            showToast('Not available on comment pages', 'error');
            return;
        }

        const defaultTarget = 200;
        let cancelRequested = false;

        showModal(
            'Load Posts',
            `<div style="margin-bottom:12px;font-size:13px;color:#555;">
                Loads posts via infinite scroll until the target count is reached.
             </div>
             <div style="display:flex;align-items:center;gap:10px;">
                 <label style="font-size:12px;font-weight:bold;color:#333;white-space:nowrap;">Target posts:</label>
                 <input id="pwr-load-target" type="number" min="25" max="2000" step="25" value="${defaultTarget}"
                     style="width:90px;padding:6px 8px;border:1px solid #c7c7c7;border-radius:3px;font-size:13px;">
             </div>
             <div id="pwr-load-status" style="margin-top:12px;font-size:12px;color:#888;min-height:18px;"></div>`,
            [
                { text: 'Close', close: false, onClick: (modal) => {
                    cancelRequested = true;
                    modal.closest('.pwr-tools-modal-overlay')?.remove();
                }},
                { text: 'Load Posts', primary: true, close: false, onClick: (modal) => {
                    const input = modal.querySelector('#pwr-load-target');
                    const target = Math.max(25, Math.min(2000, parseInt(input.value) || defaultTarget));
                    const statusEl = modal.querySelector('#pwr-load-status');
                    const loadBtn = modal.querySelector('.pwr-tools-btn-primary');
                    const closeBtn = modal.querySelector('button:not(.pwr-tools-btn-primary)');

                    // Swap Load button for Cancel during loading
                    loadBtn.disabled = true;
                    loadBtn.style.display = 'none';
                    closeBtn.textContent = 'Cancel';
                    statusEl.style.color = '#369';

                    runLoadAndExpand(target, statusEl, () => cancelRequested, () => {
                        closeBtn.textContent = cancelRequested ? 'Close' : 'Done';
                        loadBtn.style.display = '';
                        loadBtn.disabled = false;
                        loadBtn.textContent = 'Load Again';
                        if (statusEl.style.color !== 'rgb(204, 68, 68)') {
                            statusEl.style.color = '#555';
                        }
                    });
                }}
            ]
        );
    }

    function runLoadAndExpand(target, statusEl, isCancelled, onDone) {
        // We fetch up to BATCH_SIZE pages concurrently. Since each Reddit page URL
        // requires the `after` token from the previous response, we can't fire all
        // requests at once — but we can chain them: fetch pages 1-4 in sequence with
        // no gap between them, collect their docs, append in order, then immediately
        // fire the next batch of 4. This keeps 1 request always in flight and
        // saturates the pipeline without hammering Reddit too hard.

        const BATCH_SIZE = 4;

        const siteTable = document.querySelector('.sitetable.linklisting');
        if (!siteTable) {
            if (statusEl) statusEl.textContent = 'Could not find post list.';
            onDone();
            return;
        }

        function updateStatus(msg) {
            if (statusEl) statusEl.textContent = msg;
        }

        function getPostCount() {
            return document.querySelectorAll('.thing.link').length;
        }

        async function fetchPage(url) {
            const res = await fetch(url);
            if (!res.ok) throw new Error(`HTTP ${res.status}`);
            const html = await res.text();
            return new DOMParser().parseFromString(html, 'text/html');
        }

        // Append posts from a parsed doc. Returns next page URL or null.
        // Does NOT restore scroll — posts append at the bottom and page grows naturally.
        function appendFromDoc(doc) {
            doc.querySelectorAll('.thing.link').forEach(post => {
                const clone = post.cloneNode(true);
                clone.removeAttribute('data-numbered');
                siteTable.appendChild(clone);
            });

            const newNextHref = doc.querySelector('.next-button a')?.href || null;
            const liveNextBtn = document.querySelector('.next-button a');
            if (liveNextBtn && newNextHref) {
                liveNextBtn.href = newNextHref;
            } else if (liveNextBtn && !newNextHref) {
                liveNextBtn.closest('.next-button')?.remove();
            }

            // Let FilterTools/AutoTools observers process the new posts
            window.dispatchEvent(new Event('scroll'));

            return newNextHref;
        }

        // Fetch a chain of up to `count` pages sequentially (no gaps),
        // firing each request the moment the previous URL is known.
        // Returns array of parsed docs in order, plus the final next URL.
        async function fetchBatch(startUrl, count) {
            const docs = [];
            let url = startUrl;
            for (let i = 0; i < count && url; i++) {
                const doc = await fetchPage(url);
                docs.push(doc);
                url = doc.querySelector('.next-button a')?.href || null;
                // Fire next fetch immediately — no await gap between requests
            }
            return { docs, nextUrl: url };
        }

        async function run() {
            let nextUrl = document.querySelector('.next-button a')?.href || null;

            if (!nextUrl) {
                updateStatus(`Already at end — ${getPostCount()} posts loaded.`);
                onDone();
                return;
            }

            while (nextUrl && getPostCount() < target) {
                if (isCancelled()) {
                    updateStatus(`Cancelled — ${getPostCount()} posts loaded.`);
                    statusEl.style.color = '#888';
                    onDone();
                    return;
                }

                updateStatus(`Loading... ${getPostCount()} / ${target} posts`);

                const remaining = target - getPostCount();
                const batchCount = Math.min(BATCH_SIZE, Math.ceil(remaining / 25));

                try {
                    const { docs, nextUrl: discovered } = await fetchBatch(nextUrl, batchCount);

                    // Append all pages from this batch in order
                    let lastNext = null;
                    for (const doc of docs) {
                        lastNext = appendFromDoc(doc);
                    }

                    nextUrl = discovered || lastNext;

                } catch (err) {
                    console.warn('[GearTools] Load error:', err);
                    updateStatus(`Done! Loaded ${getPostCount()} posts (fetch error).`);
                    onDone();
                    return;
                }
            }

            updateStatus(`Done! Loaded ${getPostCount()} posts.`);
            onDone();
        }

        run().catch(err => {
            console.error('[GearTools] runLoad failed:', err);
            if (statusEl) statusEl.textContent = 'Error during loading.';
            onDone();
        });
    }

    // ========== BUILD UI ==========
    function buildToolsMenu() {
        const tabMenu = document.querySelector('.tabmenu');
        if (!tabMenu) return;

        // Find the filter trigger (pwr-trig) to place our icon next to it
        const filterTrig = document.getElementById('pwr-trig');
        const filterLi = filterTrig ? filterTrig.closest('li') : null;

        // Create our tools tab
        const toolsLi = document.createElement('li');
        toolsLi.className = 'pwr-tools-li';

        const toolsTrig = document.createElement('a');
        toolsTrig.href = '#';
        toolsTrig.className = 'choice';
        toolsTrig.id = 'pwr-tools-trig';
        toolsTrig.title = 'Reddit Tools';
        toolsTrig.innerHTML = icons.gear;

        // Create dropdown
        const dropdown = document.createElement('div');
        dropdown.id = 'pwr-tools-dropdown';

        dropdown.innerHTML = `
            <div class="pwr-tools-header">Tools</div>
            <div class="pwr-tools-item" data-tool="videos">
                ${icons.video}
                <div class="pwr-tools-item-text">
                    <div class="pwr-tools-item-title">Copy Video URLs</div>
                    <div class="pwr-tools-item-desc">Redgifs, Gfycat, Streamable, v.redd.it</div>
                </div>
            </div>
            <div class="pwr-tools-item" data-tool="images">
                ${icons.image}
                <div class="pwr-tools-item-text">
                    <div class="pwr-tools-item-title">Copy Image URLs</div>
                    <div class="pwr-tools-item-desc">i.redd.it, Imgur, direct images</div>
                </div>
            </div>
            <div class="pwr-tools-item" data-tool="links">
                ${icons.link}
                <div class="pwr-tools-item-text">
                    <div class="pwr-tools-item-title">Copy All Post Links</div>
                    <div class="pwr-tools-item-desc">Export all visible post URLs</div>
                </div>
            </div>
            <div class="pwr-tools-item" data-tool="expand">
                ${icons.expand}
                <div class="pwr-tools-item-text">
                    <div class="pwr-tools-item-title">Toggle All Previews</div>
                    <div class="pwr-tools-item-desc">Expand or collapse all media</div>
                </div>
            </div>
            <div class="pwr-tools-item" data-tool="loadexpand">
                ${icons.load}
                <div class="pwr-tools-item-text">
                    <div class="pwr-tools-item-title">Load N Posts</div>
                    <div class="pwr-tools-item-desc">Scroll-load posts to a target count</div>
                </div>
            </div>
            <div class="pwr-tools-item" data-tool="stats">
                ${icons.download}
                <div class="pwr-tools-item-text">
                    <div class="pwr-tools-item-title">Quick Stats</div>
                    <div class="pwr-tools-item-desc">View stats for visible posts</div>
                </div>
            </div>
            <div class="pwr-tools-item" data-tool="nsfw">
                ${icons.eyeOff}
                <div class="pwr-tools-item-text">
                    <div class="pwr-tools-item-title">Blur NSFW</div>
                    <div class="pwr-tools-item-desc">Toggle blur on NSFW thumbnails & content</div>
                </div>
            </div>
            <div class="pwr-tools-item" data-tool="sidebar">
                ${icons.sidebar}
                <div class="pwr-tools-item-text">
                    <div class="pwr-tools-item-title">Toggle Sidebar</div>
                    <div class="pwr-tools-item-desc">Show or hide the sidebar</div>
                </div>
            </div>
            <div class="pwr-tools-item" data-tool="subcss">
                ${icons.css}
                <div class="pwr-tools-item-text">
                    <div class="pwr-tools-item-title">Disable Sub CSS</div>
                    <div class="pwr-tools-item-desc">Toggle subreddit custom styles</div>
                </div>
            </div>
        `;

        // Tool actions
        dropdown.querySelectorAll('.pwr-tools-item').forEach(item => {
            item.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();

                const tool = item.dataset.tool;
                dropdown.classList.remove('active');

                switch (tool) {
                    case 'videos': copyVideoUrls(); break;
                    case 'images': copyImageUrls(); break;
                    case 'links': copyAllPostLinks(); break;
                    case 'expand': toggleAllPreviews(); break;
                    case 'loadexpand': loadAndExpand(); break;
                    case 'stats': showQuickStats(); break;
                    case 'nsfw': toggleNsfwBlur(); break;
                    case 'sidebar': toggleSidebar(); break;
                    case 'subcss': toggleSubCss(); break;
                }
            });
        });

        // Toggle dropdown
        toolsTrig.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            // Close FilterTools dropdown if open
            const filterMenu = document.getElementById('orfs-menu');
            if (filterMenu) filterMenu.classList.remove('active');
            dropdown.classList.toggle('active');
        });

        // Close on outside click
        document.addEventListener('click', (e) => {
            if (!toolsLi.contains(e.target)) {
                dropdown.classList.remove('active');
            }
        });

        toolsLi.appendChild(toolsTrig);
        toolsLi.appendChild(dropdown);

        // Insert after filter if it exists, otherwise at the end
        if (filterLi) {
            filterLi.insertAdjacentElement('afterend', toolsLi);
        } else {
            tabMenu.appendChild(toolsLi);
        }
    }

    // ========== INIT ==========
    function init() {
        buildToolsMenu();

        // Apply sidebar state (hidden by default)
        applySidebarState();
        setTimeout(updateSidebarMenuItem, 100);

        // Apply sub CSS state (disabled by default)
        applySubCssState();
        setTimeout(updateSubCssMenuItem, 100);

        // Apply NSFW blur if it was previously enabled
        if (nsfwBlurred) {
            applyNsfwBlur();
            // Small delay to ensure DOM is ready for menu update
            setTimeout(updateNsfwMenuItem, 100);
        }
    }

    // Run init
    init();

    }); // end onDOMReady

})();