Old Reddit GearTools

Utility tools for old Reddit - works alongside filter scripts

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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

})();