8chan Lightweight Extended Suite

Minimalist extender for 8chan with in-line replies, spoiler revealing and media preview for videos & images

当前为 2025-04-20 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         8chan Lightweight Extended Suite
// @namespace    https://greasyfork.org/en/scripts/533173
// @version      2.5.1
// @description  Minimalist extender for 8chan with in-line replies, spoiler revealing and media preview for videos & images
// @author       impregnator
// @match        https://8chan.moe/*
// @match        https://8chan.se/*
// @match        https://8chan.cc/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Function to process images and replace spoiler placeholders with thumbnails
    function processImages(images, isCatalog = false) {
        images.forEach(img => {
            // Check if the image is a spoiler placeholder (custom or default)
            if (img.src.includes('custom.spoiler') || img.src.includes('spoiler.png')) {
                let fullFileUrl;
                if (isCatalog) {
                    // Catalog: Get the href from the parent <a class="linkThumb">
                    const link = img.closest('a.linkThumb');
                    if (link) {
                        // Construct the thumbnail URL based on the thread URL
                        fullFileUrl = link.href;
                        const threadMatch = fullFileUrl.match(/\/([a-z0-9]+)\/res\/([0-9]+)\.html$/i);
                        if (threadMatch && threadMatch[1] && threadMatch[2]) {
                            const board = threadMatch[1];
                            const threadId = threadMatch[2];
                            // Fetch the thread page to find the actual image URL
                            fetchThreadImage(board, threadId).then(thumbnailUrl => {
                                if (thumbnailUrl) {
                                    img.src = thumbnailUrl;
                                }
                            });
                        }
                    }
                } else {
                    // Thread: Get the parent <a> element containing the full-sized file URL
                    const link = img.closest('a.imgLink');
                    if (link) {
                        // Extract the full-sized file URL
                        fullFileUrl = link.href;
                        // Extract the file hash (everything after /.media/ up to the extension)
                        const fileHash = fullFileUrl.match(/\/\.media\/([a-f0-9]+)\.[a-z0-9]+$/i);
                        if (fileHash && fileHash[1]) {
                            // Construct the thumbnail URL using the current domain
                            const thumbnailUrl = `${window.location.origin}/.media/t_${fileHash[1]}`;
                            // Replace the spoiler image with the thumbnail
                            img.src = thumbnailUrl;
                        }
                    }
                }
            }
        });
    }

    // Function to fetch the thread page and extract the thumbnail URL
    async function fetchThreadImage(board, threadId) {
        try {
            const response = await fetch(`https://${window.location.host}/${board}/res/${threadId}.html`);
            const text = await response.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(text, 'text/html');
            // Find the first image in the thread's OP post
            const imgLink = doc.querySelector('.uploadCell a.imgLink');
            if (imgLink) {
                const fullFileUrl = imgLink.href;
                const fileHash = fullFileUrl.match(/\/\.media\/([a-f0-9]+)\.[a-z0-9]+$/i);
                if (fileHash && fileHash[1]) {
                    return `${window.location.origin}/.media/t_${fileHash[1]}`;
                }
            }
            return null;
        } catch (error) {
            console.error('Error fetching thread image:', error);
            return null;
        }
    }

    // Process existing images on page load
    const isCatalogPage = window.location.pathname.includes('catalog.html');
    if (isCatalogPage) {
        const initialCatalogImages = document.querySelectorAll('.catalogCell a.linkThumb img');
        processImages(initialCatalogImages, true);
    } else {
        const initialThreadImages = document.querySelectorAll('.uploadCell img');
        processImages(initialThreadImages, false);
    }

    // Set up MutationObserver to handle dynamically added posts
    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            if (mutation.addedNodes.length) {
                // Check each added node for new images
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (isCatalogPage) {
                            const newCatalogImages = node.querySelectorAll('.catalogCell a.linkThumb img');
                            processImages(newCatalogImages, true);
                        } else {
                            const newThreadImages = node.querySelectorAll('.uploadCell img');
                            processImages(newThreadImages, false);
                        }
                    }
                });
            }
        });
    });

    // Observe changes to the document body, including child nodes and subtrees
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
})();

//Opening all posts from the catalog in a new tag section

// Add click event listener to catalog thumbnail images
document.addEventListener('click', function(e) {
    // Check if the clicked element is an image inside a catalog cell
    if (e.target.tagName === 'IMG' && e.target.closest('.catalogCell')) {
        // Find the parent link with class 'linkThumb'
        const link = e.target.closest('.linkThumb');
        if (link) {
            // Prevent default link behavior
            e.preventDefault();
            // Open the thread in a new tab
            window.open(link.href, '_blank');
        }
    }
});

//Automatically redirect to catalog section

// Redirect to catalog if on a board's main page, excluding overboard pages
(function() {
    const currentPath = window.location.pathname;
    // Check if the path matches a board's main page (e.g., /v/, /a/) but not overboard pages
    if (currentPath.match(/^\/[a-zA-Z0-9]+\/$/) && !currentPath.match(/^\/(sfw|overboard)\/$/)) {
        // Redirect to the catalog page
        window.location.replace(currentPath + 'catalog.html');
    }
})();

// Text spoiler revealer

(function() {
    // Function to reveal spoilers
    function revealSpoilers() {
        const spoilers = document.querySelectorAll('span.spoiler');
        spoilers.forEach(spoiler => {
            // Override default spoiler styles to make text visible
            spoiler.style.background = 'none';
            spoiler.style.color = 'inherit';
            spoiler.style.textShadow = 'none';
        });
    }

    // Run initially for existing spoilers
    revealSpoilers();

    // Set up MutationObserver to watch for new spoilers
    const observer = new MutationObserver((mutations) => {
        mutations.forEach(mutation => {
            if (mutation.addedNodes.length > 0) {
                // Check if new nodes contain spoilers
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        const newSpoilers = node.querySelectorAll('span.spoiler');
                        newSpoilers.forEach(spoiler => {
                            spoiler.style.background = 'none';
                            spoiler.style.color = 'inherit';
                            spoiler.style.textShadow = 'none';
                        });
                    }
                });
            }
        });
    });

    // Observe the document body for changes (new posts)
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
})();

//Hash navigation
// Add # links to backlinks and quote links for scrolling
(function() {
    // Function to add # link to backlinks and quote links
    function addHashLinks(container = document) {
        const links = container.querySelectorAll('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink');
        links.forEach(link => {
            // Skip if # link already exists or processed
            if (link.nextSibling && link.nextSibling.classList && link.nextSibling.classList.contains('hash-link-container')) return;
            if (link.dataset.hashProcessed) return;
            // Create # link as a span to avoid <a> processing
            const hashLink = document.createElement('span');
            hashLink.textContent = ' #';
            hashLink.style.cursor = 'pointer';
            hashLink.style.color = '#0000EE'; // Match link color
            hashLink.title = 'Scroll to post';
            hashLink.className = 'hash-link';
            hashLink.dataset.hashListener = 'true'; // Mark as processed
            // Wrap # link in a span to isolate it
            const container = document.createElement('span');
            container.className = 'hash-link-container';
            container.appendChild(hashLink);
            link.insertAdjacentElement('afterend', container);
            link.dataset.hashProcessed = 'true'; // Mark as processed
        });
    }

    // Event delegation for hash link clicks to mimic .linkSelf behavior
    document.addEventListener('click', function(e) {
        if (e.target.classList.contains('hash-link')) {
            e.preventDefault();
            e.stopPropagation();
            const link = e.target.closest('.hash-link-container').previousElementSibling;
            const postId = link.textContent.replace('>>', '');
            if (document.getElementById(postId)) {
                window.location.hash = `#${postId}`;
                console.log(`Navigated to post #${postId}`);
            } else {
                console.log(`Post ${postId} not found`);
            }
        }
    }, true);

    // Process existing backlinks and quote links on page load
    addHashLinks();
    console.log('Hash links applied on page load');

    // Patch inline reply logic to apply hash links to new inline content
    if (window.tooltips) {
        // Patch loadTooltip to apply hash links after content is loaded
        const originalLoadTooltip = tooltips.loadTooltip;
        tooltips.loadTooltip = function(element, quoteUrl, sourceId, isInline) {
            originalLoadTooltip.apply(this, arguments);
            if (isInline) {
                // Wait for content to be fully loaded
                setTimeout(() => {
                    addHashLinks(element);
                    console.log('Hash links applied to loaded tooltip content:', quoteUrl);
                }, 0);
            }
        };

        // Patch addLoadedTooltip to ensure hash links are applied
        const originalAddLoadedTooltip = tooltips.addLoadedTooltip;
        tooltips.addLoadedTooltip = function(htmlContents, tooltip, quoteUrl, replyId, isInline) {
            originalAddLoadedTooltip.apply(this, arguments);
            if (isInline) {
                addHashLinks(htmlContents);
                console.log('Hash links applied to inline tooltip content:', quoteUrl);
            }
        };

        // Patch addInlineClick to apply hash links after appending
        const originalAddInlineClick = tooltips.addInlineClick;
        tooltips.addInlineClick = function(quote, innerPost, isBacklink, quoteTarget, sourceId) {
            if (!quote.href || quote.classList.contains('hash-link') || quote.closest('.hash-link-container') || quote.href.includes('#q')) {
                console.log('Skipped invalid or hash link:', quote.href || quote.textContent);
                return;
            }
            // Clone quote to remove existing listeners
            const newQuote = quote.cloneNode(true);
            quote.parentNode.replaceChild(newQuote, quote);
            quote = newQuote;

            // Reapply hover events
            tooltips.addHoverEvents(quote, innerPost, quoteTarget, sourceId);
            console.log('Hover events reapplied for:', quoteTarget.quoteUrl);

            // Add click handler
            quote.addEventListener('click', function(e) {
                console.log('linkQuote clicked:', quoteTarget.quoteUrl);
                if (!tooltips.inlineReplies) {
                    console.log('inlineReplies disabled');
                    return;
                }
                e.preventDefault();
                e.stopPropagation();

                // Find or create replyPreview
                let replyPreview = innerPost.querySelector('.replyPreview');
                if (!replyPreview) {
                    replyPreview = document.createElement('div');
                    replyPreview.className = 'replyPreview';
                    innerPost.appendChild(replyPreview);
                }

                // Check for duplicates or loading
                if (tooltips.loadingPreviews[quoteTarget.quoteUrl] ||
                    tooltips.quoteAlreadyAdded(quoteTarget.quoteUrl, innerPost)) {
                    console.log('Duplicate or loading:', quoteTarget.quoteUrl);
                    return;
                }

                // Create and load inline post
                const placeHolder = document.createElement('div');
                placeHolder.style.whiteSpace = 'normal';
                placeHolder.className = 'inlineQuote';
                tooltips.loadTooltip(placeHolder, quoteTarget.quoteUrl, sourceId, true);

                // Verify post loaded
                if (!placeHolder.querySelector('.linkSelf')) {
                    console.log('Failed to load post:', quoteTarget.quoteUrl);
                    return;
                }

                // Add close button
                const close = document.createElement('a');
                close.innerText = 'X';
                close.className = 'closeInline';
                close.onclick = () => placeHolder.remove();
                placeHolder.querySelector('.postInfo').prepend(close);

                // Process quotes in the new inline post
                Array.from(placeHolder.querySelectorAll('.linkQuote'))
                    .forEach(a => tooltips.processQuote(a, false, true));

                if (tooltips.bottomBacklinks) {
                    const alts = placeHolder.querySelector('.altBacklinks');
                    if (alts && alts.firstChild) {
                        Array.from(alts.firstChild.children)
                            .forEach(a => tooltips.processQuote(a, true));
                    }
                }

                // Append to replyPreview and apply hash links
                replyPreview.appendChild(placeHolder);
                addHashLinks(placeHolder);
                console.log('Inline post appended and hash links applied:', quoteTarget.quoteUrl);

                tooltips.removeIfExists();
            }, true);
        };

        // Patch processQuote to skip hash links
        const originalProcessQuote = tooltips.processQuote;
        tooltips.processQuote = function(quote, isBacklink) {
            if (!quote.href || quote.classList.contains('hash-link') || quote.closest('.hash-link-container') || quote.href.includes('#q')) {
                console.log('Skipped invalid or hash link in processQuote:', quote.href || quote.textContent);
                return;
            }
            originalProcessQuote.apply(this, arguments);
        };
    }

    // Set up MutationObserver to handle dynamically added or updated backlinks and quote links
    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            if (mutation.addedNodes.length) {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        // Check for new backlink or quote link <a> elements
                        const newLinks = node.matches('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink') ? [node] : node.querySelectorAll('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink');
                        newLinks.forEach(link => {
                            addHashLinks(link.parentElement);
                            console.log('Hash links applied to new link:', link.textContent);
                        });
                    }
                });
            }
        });
    });

    // Observe changes to the posts container
    const postsContainer = document.querySelector('.divPosts') || document.body;
    observer.observe(postsContainer, {
        childList: true,
        subtree: true
    });
})();
//--Hash navigation

//Inline reply chains

(function() {
    'use strict';

    console.log('Userscript is running');

    // Add CSS for visual nesting
    const style = document.createElement('style');
    style.innerHTML = `
        .inlineQuote .replyPreview {
            margin-left: 20px;
            border-left: 1px solid #ccc;
            padding-left: 10px;
        }
        .closeInline {
            color: #ff0000;
            cursor: pointer;
            margin-left: 5px;
            font-weight: bold;
        }
    `;
    document.head.appendChild(style);

    // Wait for tooltips to initialize
    window.addEventListener('load', function() {
        if (!window.tooltips) {
            console.error('tooltips module not found');
            return;
        }
        console.log('tooltips module found');

        // Ensure Inline Replies is enabled
        if (!tooltips.inlineReplies) {
            console.log('Enabling Inline Replies');
            localStorage.setItem('inlineReplies', 'true');
            tooltips.inlineReplies = true;

            // Check and update the checkbox, retrying if not yet loaded
            const enableCheckbox = () => {
                const inlineCheckbox = document.getElementById('settings-SW5saW5lIFJlcGxpZX');
                if (inlineCheckbox) {
                    inlineCheckbox.checked = true;
                    console.log('Inline Replies checkbox checked');
                    return true;
                }
                console.warn('Inline Replies checkbox not found, retrying...');
                return false;
            };

            // Try immediately
            if (!enableCheckbox()) {
                // Retry every 500ms up to 5 seconds
                let attempts = 0;
                const maxAttempts = 10;
                const interval = setInterval(() => {
                    if (enableCheckbox() || attempts >= maxAttempts) {
                        clearInterval(interval);
                        if (attempts >= maxAttempts) {
                            console.error('Failed to find Inline Replies checkbox after retries');
                        }
                    }
                    attempts++;
                }, 500);
            }
        } else {
            console.log('Inline Replies already enabled');
        }

        // Override addLoadedTooltip to ensure replyPreview exists
        const originalAddLoadedTooltip = tooltips.addLoadedTooltip;
        tooltips.addLoadedTooltip = function(htmlContents, tooltip, quoteUrl, replyId, isInline) {
            console.log('addLoadedTooltip called for:', quoteUrl);
            originalAddLoadedTooltip.apply(this, arguments);
            if (isInline) {
                let replyPreview = htmlContents.querySelector('.replyPreview');
                if (!replyPreview) {
                    replyPreview = document.createElement('div');
                    replyPreview.className = 'replyPreview';
                    htmlContents.appendChild(replyPreview);
                }
            }
        };

        // Override addInlineClick for nested replies, excluding post number links
        tooltips.addInlineClick = function(quote, innerPost, isBacklink, quoteTarget, sourceId) {
            // Skip post number links (href starts with #q)
            if (quote.href.includes('#q')) {
                console.log('Skipping post number link:', quote.href);
                return;
            }

            // Remove existing listeners by cloning
            const newQuote = quote.cloneNode(true);
            quote.parentNode.replaceChild(newQuote, quote);
            quote = newQuote;

            // Reapply hover events to preserve preview functionality
            tooltips.addHoverEvents(quote, innerPost, quoteTarget, sourceId);
            console.log('Hover events reapplied for:', quoteTarget.quoteUrl);

            // Add click handler
            quote.addEventListener('click', function(e) {
                console.log('linkQuote clicked:', quoteTarget.quoteUrl);
                if (!tooltips.inlineReplies) {
                    console.log('inlineReplies disabled');
                    return;
                }
                e.preventDefault();
                e.stopPropagation(); // Prevent site handlers

                // Find or create replyPreview
                let replyPreview = innerPost.querySelector('.replyPreview');
                if (!replyPreview) {
                    replyPreview = document.createElement('div');
                    replyPreview.className = 'replyPreview';
                    innerPost.appendChild(replyPreview);
                }

                // Check for duplicates or loading
                if (tooltips.loadingPreviews[quoteTarget.quoteUrl] ||
                    tooltips.quoteAlreadyAdded(quoteTarget.quoteUrl, innerPost)) {
                    console.log('Duplicate or loading:', quoteTarget.quoteUrl);
                    return;
                }

                // Create and load inline post
                const placeHolder = document.createElement('div');
                placeHolder.style.whiteSpace = 'normal';
                placeHolder.className = 'inlineQuote';
                tooltips.loadTooltip(placeHolder, quoteTarget.quoteUrl, sourceId, true);

                // Verify post loaded
                if (!placeHolder.querySelector('.linkSelf')) {
                    console.log('Failed to load post:', quoteTarget.quoteUrl);
                    return;
                }

                // Add close button
                const close = document.createElement('a');
                close.innerText = 'X';
                close.className = 'closeInline';
                close.onclick = () => placeHolder.remove();
                placeHolder.querySelector('.postInfo').prepend(close);

                // Process quotes in the new inline post
                Array.from(placeHolder.querySelectorAll('.linkQuote'))
                    .forEach(a => tooltips.processQuote(a, false, true));

                if (tooltips.bottomBacklinks) {
                    const alts = placeHolder.querySelector('.altBacklinks');
                    if (alts && alts.firstChild) {
                        Array.from(alts.firstChild.children)
                            .forEach(a => tooltips.processQuote(a, true));
                    }
                }

                // Append to replyPreview
                replyPreview.appendChild(placeHolder);
                console.log('Inline post appended:', quoteTarget.quoteUrl);

                tooltips.removeIfExists();
            }, true); // Use capture phase
        };

        // Reprocess all existing linkQuote and backlink elements, excluding post numbers
        console.log('Reprocessing linkQuote elements');
        const quotes = document.querySelectorAll('.linkQuote, .panelBacklinks a');
        quotes.forEach(quote => {
            const innerPost = quote.closest('.innerPost, .innerOP');
            if (!innerPost) {
                console.log('No innerPost found for quote:', quote.href);
                return;
            }

            // Skip post number links
            if (quote.href.includes('#q')) {
                console.log('Skipping post number link:', quote.href);
                return;
            }

            const isBacklink = quote.parentElement.classList.contains('panelBacklinks') ||
                               quote.parentElement.classList.contains('altBacklinks');
            const quoteTarget = api.parsePostLink(quote.href);
            const sourceId = api.parsePostLink(innerPost.querySelector('.linkSelf').href).post;

            tooltips.addInlineClick(quote, innerPost, isBacklink, quoteTarget, sourceId);
        });

        // Observe for dynamically added posts
        const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType !== 1) return;
                    const newQuotes = node.querySelectorAll('.linkQuote, .panelBacklinks a');
                    newQuotes.forEach(quote => {
                        if (quote.dataset.processed || quote.href.includes('#q')) {
                            if (quote.href.includes('#q')) {
                                console.log('Skipping post number link:', quote.href);
                            }
                            return;
                        }
                        quote.dataset.processed = 'true';
                        const innerPost = quote.closest('.innerPost, .innerOP');
                        if (!innerPost) return;

                        const isBacklink = quote.parentElement.classList.contains('panelBacklinks') ||
                                           quote.parentElement.classList.contains('altBacklinks');
                        const quoteTarget = api.parsePostLink(quote.href);
                        const sourceId = api.parsePostLink(innerPost.querySelector('.linkSelf').href).post;

                        tooltips.addInlineClick(quote, innerPost, isBacklink, quoteTarget, sourceId);
                    });
                });
            });
        });
        observer.observe(document.querySelector('.divPosts') || document.body, {
            childList: true,
            subtree: true
        });
        console.log('MutationObserver set up');
    });
})();

//--Inline replies

//Auto TOS Accept with Delay
(function() {
    'use strict';

    // Check if on the disclaimer page
    if (window.location.pathname === '/.static/pages/disclaimer.html') {
        // Redirect to confirmed page after 1-second delay
        setTimeout(() => {
            window.location.replace('https://8chan.se/.static/pages/confirmed.html');
            console.log('Automatically redirected from disclaimer to confirmed page after 1-second delay');
        }, 1000);
    }
})();
//--Auto TOS Accept with Delay

//Media Auto-Preview
// Auto-preview images and videos on hover for un-expanded thumbnails, disabling native hover
(function() {
    'use strict';

    // Disable native hover preview
    localStorage.setItem('hoveringImage', 'false'); // Disable "Image Preview on Hover" setting
    if (window.thumbs && typeof window.thumbs.removeHoveringExpand === 'function') {
        window.thumbs.removeHoveringExpand(); // Remove native hover listeners
    }
    // Override addHoveringExpand to prevent re-enabling
    if (window.thumbs) {
        window.thumbs.addHoveringExpand = function() {
            // Do nothing to prevent native hover preview
            console.log('Native hover preview (addHoveringExpand) blocked by userscript');
        };
    }

    // Supported file extensions for images and videos
    const supportedExtensions = {
        image: ['.gif', '.webp', '.png', '.jfif', '.pjpeg', '.jpeg', '.pjp', '.jpg', '.bmp', '.dib', '.svgz', '.svg'],
        video: ['.webm', '.m4v', '.mp4', '.ogm', '.ogv', '.avi', '.asx', '.mpg', '.mpeg']
    };

    // Create preview container
    const previewContainer = document.createElement('div');
    previewContainer.style.position = 'fixed';
    previewContainer.style.zIndex = '1000';
    previewContainer.style.pointerEvents = 'none'; // Allow clicks to pass through
    previewContainer.style.display = 'none';
    document.body.appendChild(previewContainer);

    // Function to check if URL is a supported image or video
    function isSupportedMedia(url) {
        const ext = (url.match(/\.[a-z0-9]+$/i) || [''])[0].toLowerCase();
        return supportedExtensions.image.includes(ext) || supportedExtensions.video.includes(ext);
    }

    // Function to check if URL is a video
    function isVideo(url) {
        const ext = (url.match(/\.[a-z0-9]+$/i) || [''])[0].toLowerCase();
        return supportedExtensions.video.includes(ext);
    }

    // Function to check if link is in un-expanded state
    function isUnexpanded(link) {
        const thumbnail = link.querySelector('img:not(.imgExpanded)');
        const expanded = link.querySelector('img.imgExpanded');
        return thumbnail && window.getComputedStyle(thumbnail).display !== 'none' &&
               (!expanded || window.getComputedStyle(expanded).display === 'none');
    }

    // Function to calculate preview dimensions
    function getPreviewDimensions(naturalWidth, naturalHeight) {
        // Detect zoom level
        const zoomLevel = window.devicePixelRatio || 1; // Fallback to 1 if undefined
        // Content area (excludes scrollbar) for max size
        const maxWidth = document.documentElement.clientWidth;
        const maxHeight = document.documentElement.clientHeight;
        // Screen resolution for small media check
        const screenWidth = window.screen.width || 1920; // Fallback to 1920
        const screenHeight = window.screen.height || 1080; // Fallback to 1080

        // If media fits within screen resolution, use full native size
        if (naturalWidth <= screenWidth && naturalHeight <= screenHeight) {
            let width = naturalWidth;
            let height = naturalHeight;

            // If native size exceeds content area, scale down
            const scaleByWidth = maxWidth / width;
            const scaleByHeight = maxHeight / height;
            const scale = Math.min(scaleByWidth, scaleByHeight, 1);
            width = Math.round(width * scale);
            height = Math.round(height * scale);

            return { width, height };
        }

        // Otherwise, adjust for zoom and scale to fit content area
        let width = naturalWidth / zoomLevel;
        let height = naturalHeight / zoomLevel;

        const scaleByWidth = maxWidth / width;
        const scaleByHeight = maxHeight / height;
        const scale = Math.min(scaleByWidth, scaleByHeight, 1);
        width = Math.round(width * scale);
        height = Math.round(height * scale);

        return { width, height };
    }

    // Function to position preview near cursor
    function positionPreview(event) {
        const mouseX = event.clientX;
        const mouseY = event.clientY;
        const previewWidth = previewContainer.offsetWidth;
        const previewHeight = previewContainer.offsetHeight;

        // Skip if dimensions are not yet available
        if (previewWidth === 0 || previewHeight === 0) {
            return;
        }

        // Use content area for positioning (excludes scrollbar)
        const maxWidth = document.documentElement.clientWidth;
        const maxHeight = document.documentElement.clientHeight;

        // Calculate centered position
        const centerX = (maxWidth - previewWidth) / 2;
        const centerY = (maxHeight - previewHeight) / 2;

        // Allow cursor to influence position with a bounded offset
        const maxOffset = 100; // Maximum pixels to shift from center
        const cursorOffsetX = Math.max(-maxOffset, Math.min(maxOffset, mouseX - maxWidth / 2));
        const cursorOffsetY = Math.max(-maxOffset, Math.min(maxOffset, mouseY - maxHeight / 2));

        // Calculate initial position with cursor influence
        let left = centerX + cursorOffsetX;
        let top = centerY + cursorOffsetY;

        // Ensure preview stays fully within content area
        left = Math.max(0, Math.min(left, maxWidth - previewWidth));
        top = Math.max(0, Math.min(top, maxHeight - previewHeight));

        previewContainer.style.left = `${left}px`;
        previewContainer.style.top = `${top}px`;
    }

    // Function to show preview
    function showPreview(link, event) {
        if (!isUnexpanded(link)) return; // Skip if expanded
        const url = link.href;
        if (!isSupportedMedia(url)) return;

        // Clear existing preview
        previewContainer.innerHTML = '';

        if (isVideo(url)) {
            // Create video element
            const video = document.createElement('video');
            video.src = url;
            video.autoplay = true;
            video.muted = false; // Play with audio
            video.loop = true;
            video.style.maxWidth = '100%';
            video.style.maxHeight = '100%';

            // Set dimensions and position when metadata is loaded
            video.onloadedmetadata = () => {
                const { width, height } = getPreviewDimensions(video.videoWidth, video.videoHeight);
                video.width = width;
                video.height = height;
                previewContainer.style.width = `${width}px`;
                previewContainer.style.height = `${height}px`;
                previewContainer.style.display = 'block'; // Show after dimensions are set
                positionPreview(event);
            };

            previewContainer.appendChild(video);
        } else {
            // Create image element
            const img = document.createElement('img');
            img.src = url;
            img.style.maxWidth = '100%';
            img.style.maxHeight = '100%';

            // Set dimensions and position when image is loaded
            img.onload = () => {
                const { width, height } = getPreviewDimensions(img.naturalWidth, img.naturalHeight);
                img.width = width;
                img.height = height;
                previewContainer.style.width = `${width}px`;
                previewContainer.style.height = `${height}px`;
                previewContainer.style.display = 'block'; // Show after dimensions are set
                positionPreview(event);
            };

            previewContainer.appendChild(img);
        }
    }

    // Function to hide preview
    function hidePreview() {
        previewContainer.style.display = 'none';
        // Stop video playback
        const video = previewContainer.querySelector('video');
        if (video) {
            video.pause();
            video.currentTime = 0;
        }
        previewContainer.innerHTML = '';
    }

    // Function to apply hover events to links
    function applyHoverEvents(container = document) {
        const links = container.querySelectorAll('.uploadCell a.imgLink');
        links.forEach(link => {
            // Skip if already processed
            if (link.dataset.previewProcessed) return;
            link.dataset.previewProcessed = 'true';

            link.addEventListener('mouseenter', (e) => {
                showPreview(link, e);
            });

            link.addEventListener('mousemove', (e) => {
                if (previewContainer.style.display === 'block') {
                    positionPreview(e);
                }
            });

            link.addEventListener('mouseleave', () => {
                hidePreview();
            });

            // Hide preview on click if expanded
            link.addEventListener('click', () => {
                if (!isUnexpanded(link)) {
                    hidePreview();
                }
            });
        });
    }

    // Apply hover events to existing links on page load
    applyHoverEvents();
    console.log('Media preview events applied on page load');

    // Patch inline reply logic to apply hover events to new inline content
    if (window.tooltips) {
        // Patch loadTooltip to apply hover events after content is loaded
        const originalLoadTooltip = tooltips.loadTooltip;
        tooltips.loadTooltip = function(element, quoteUrl, sourceId, isInline) {
            originalLoadTooltip.apply(this, arguments);
            if (isInline) {
                setTimeout(() => {
                    applyHoverEvents(element);
                    console.log('Media preview events applied to loaded tooltip content:', quoteUrl);
                }, 0);
            }
        };

        // Patch addLoadedTooltip to ensure hover events are applied
        const originalAddLoadedTooltip = tooltips.addLoadedTooltip;
        tooltips.addLoadedTooltip = function(htmlContents, tooltip, quoteUrl, replyId, isInline) {
            originalAddLoadedTooltip.apply(this, arguments);
            if (isInline) {
                applyHoverEvents(htmlContents);
                console.log('Media preview events applied to inline tooltip content:', quoteUrl);
            }
        };
    }

    // Set up MutationObserver to handle dynamically added posts
    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            if (mutation.addedNodes.length) {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        // Handle new posts and inline replies
                        const newLinks = node.matches('.uploadCell a.imgLink') ? [node] : node.querySelectorAll('.uploadCell a.imgLink');
                        newLinks.forEach(link => {
                            applyHoverEvents(link.parentElement);
                            console.log('Media preview events applied to new link:', link.href);
                        });
                    }
                });
            }
        });
    });

    // Observe changes to the posts container
    const postsContainer = document.querySelector('.divPosts') || document.body;
    observer.observe(postsContainer, {
        childList: true,
        subtree: true
    });
})();
//--Media Auto-Preview

//Post Age Tooltip
// Show a tooltip with time elapsed since post when hovering over date/time
(function() {
    'use strict';

    // Create tooltip container
    const tooltip = document.createElement('div');
    tooltip.style.position = 'fixed';
    tooltip.style.zIndex = '1000';
    tooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
    tooltip.style.color = '#fff';
    tooltip.style.padding = '5px 10px';
    tooltip.style.borderRadius = '4px';
    tooltip.style.fontSize = '12px';
    tooltip.style.pointerEvents = 'none';
    tooltip.style.display = 'none';
    document.body.appendChild(tooltip);

    // Parse timestamp (e.g., "04/16/2025 (Wed) 21:23:21")
    function parseTimestamp(text) {
        const match = text.match(/^(\d{2})\/(\d{2})\/(\d{4}).*?(\d{2}):(\d{2}):(\d{2})$/);
        if (!match) return null;
        const [, month, day, year, hours, minutes, seconds] = match;
        const isoString = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
        const date = new Date(isoString);
        return isNaN(date.getTime()) ? null : date;
    }

    // Format elapsed time
    function formatElapsedTime(postDate) {
        const now = new Date();
        const diffMs = now - postDate;
        if (diffMs < 0) return 'Just now';

        const diffSeconds = Math.floor(diffMs / 1000);
        if (diffSeconds < 60) {
            return `${diffSeconds} second${diffSeconds === 1 ? '' : 's'} ago`;
        }

        const diffMinutes = Math.floor(diffSeconds / 60);
        if (diffMinutes < 60) {
            return `${diffMinutes} minute${diffMinutes === 1 ? '' : 's'} ago`;
        }

        const diffHours = Math.floor(diffMinutes / 60);
        if (diffHours < 24) {
            const remainingMinutes = diffMinutes % 60;
            return `${diffHours} hour${diffHours === 1 ? '' : 's'} and ${remainingMinutes} minute${remainingMinutes === 1 ? '' : 's'} ago`;
        }

        const diffDays = Math.floor(diffHours / 24);
        const remainingHours = diffHours % 24;
        return `${diffDays} day${diffDays === 1 ? '' : 's'} and ${remainingHours} hour${remainingHours === 1 ? '' : 's'} ago`;
    }

    // Position tooltip above element
    function positionTooltip(event, element) {
        const rect = element.getBoundingClientRect();
        const left = event.clientX;
        const top = rect.top - tooltip.offsetHeight - 5;

        tooltip.style.left = `${left}px`;
        tooltip.style.top = `${top}px`;
    }

    // Show tooltip
    function showTooltip(element, event) {
        const postDate = parseTimestamp(element.textContent);
        if (!postDate) {
            tooltip.style.display = 'none';
            return;
        }

        tooltip.textContent = formatElapsedTime(postDate);
        tooltip.style.display = 'block';
        positionTooltip(event, element);
    }

    // Hide tooltip
    function hideTooltip() {
        tooltip.style.display = 'none';
    }

    // Apply tooltip events to labelCreated elements
    function applyTooltipEvents(container = document) {
        const dateSpans = container.querySelectorAll('span.labelCreated');
        dateSpans.forEach(span => {
            // Remove existing listeners to avoid duplicates
            span.removeEventListener('mouseenter', showTooltip);
            span.removeEventListener('mouseleave', hideTooltip);

            span.addEventListener('mouseenter', (e) => {
                showTooltip(span, e);
            });

            span.addEventListener('mouseleave', () => {
                hideTooltip();
            });
        });
    }

    // Apply tooltip events on page load
    applyTooltipEvents();
    console.log('Post age tooltip events applied on page load');

    // Patch inline reply logic
    if (window.tooltips) {
        const originalLoadTooltip = tooltips.loadTooltip;
        tooltips.loadTooltip = function(element, quoteUrl, sourceId, isInline) {
            originalLoadTooltip.apply(this, arguments);
            if (isInline) {
                setTimeout(() => {
                    applyTooltipEvents(element);
                    console.log('Post age tooltip events applied to loaded tooltip content:', quoteUrl);
                }, 0);
            }
        };

        const originalAddLoadedTooltip = tooltips.addLoadedTooltip;
        tooltips.addLoadedTooltip = function(htmlContents, tooltip, quoteUrl, replyId, isInline) {
            originalAddLoadedTooltip.apply(this, arguments);
            if (isInline) {
                applyTooltipEvents(htmlContents);
                console.log('Post age tooltip events applied to inline tooltip content:', quoteUrl);
            }
        };
    }

//Force-Enable Local Times
(function() {
    'use strict';
    localStorage.setItem('localTime', 'true');
    console.log('Local Times setting enabled');
})();

    // MutationObserver for dynamically added posts
    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            if (mutation.addedNodes.length) {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        const newSpans = node.matches('span.labelCreated') ? [node] : node.querySelectorAll('span.labelCreated');
                        newSpans.forEach(span => {
                            applyTooltipEvents(span.parentElement);
                            console.log('Post age tooltip events applied to new span:', span.textContent);
                        });
                    }
                });
            }
        });
    });

    // Observe posts container
    const postsContainer = document.querySelector('.divPosts') || document.body;
    observer.observe(postsContainer, {
        childList: true,
        subtree: true
    });
})();
//--Post Age Tooltip

//Last Read Post Tracker
(function() {
    'use strict';

    // Only run on thread pages (e.g., /vyt/res/24600.html)
    if (!window.location.pathname.match(/\/res\/\d+\.html$/)) {
        console.log('Not a thread page, exiting Last Read Post Tracker');
        return;
    }

    // Get thread ID from URL (e.g., "24600" from /vyt/res/24600.html)
    const threadIdMatch = window.location.pathname.match(/(\d+)\.html$/);
    if (!threadIdMatch) {
        console.error('Could not extract thread ID from URL:', window.location.pathname);
        return;
    }
    const threadId = threadIdMatch[1];

    // Load last read posts from localStorage
    let lastReadPosts = {};
    try {
        lastReadPosts = JSON.parse(localStorage.getItem('lastReadPosts') || '{}');
    } catch (e) {
        console.error('Failed to parse lastReadPosts from localStorage:', e);
    }
    let lastReadPostId = lastReadPosts[threadId] || null;
    let currentArrow = null;

    // Throttle function to limit scroll event frequency
    function throttle(fn, wait) {
        let lastCall = 0;
        return function(...args) {
            const now = Date.now();
            if (now - lastCall >= wait) {
                lastCall = now;
                fn(...args);
            }
        };
    }

    // Add arrow to a post (only called on page load)
    function addArrow(postContainer) {
        if (currentArrow) {
            currentArrow.remove();
            currentArrow = null;
        }
        const postInfo = postContainer.querySelector('.postInfo.title');
        if (!postInfo) {
            console.error('postInfo.title not found in postContainer:', postContainer.outerHTML);
            return;
        }
        const arrow = document.createElement('span');
        arrow.textContent = '→';
        arrow.style.color = '#ff0000';
        arrow.style.marginLeft = '5px';
        postInfo.appendChild(arrow);
        currentArrow = arrow;
        console.log(`Added arrow to post on load: ${postContainer.id || postContainer.className}`);
    }

    // Update last read post based on scroll position (no arrow during scroll)
    function updateLastReadPost() {
        // Try postCell and postContainer as selectors
        const posts = document.querySelectorAll('.postCell, .postContainer');
        if (!posts.length) {
            console.warn('No post elements found. Available classes:',
                Array.from(document.querySelectorAll('[class*="post"], [class*="reply"]'))
                    .map(el => el.className)
                    .filter((v, i, a) => a.indexOf(v) === i));
            return;
        }

        let newLastReadPostId = lastReadPostId;
        posts.forEach(post => {
            const rect = post.getBoundingClientRect();
            // Extract post ID from id (e.g., "p485759" -> "485759") or linkQuote
            let postId = post.id.match(/^(?:pc|p|post-)?(\d+)$/)?.[1];
            if (!postId) {
                const linkQuote = post.querySelector('.linkQuote');
                postId = linkQuote ? linkQuote.textContent.trim() : null;
            }
            if (!postId) {
                console.warn('Could not extract post ID from:', post.outerHTML);
                return;
            }
            // Consider post read if its top is above viewport center and visible
            if (rect.top < window.innerHeight / 2 && rect.bottom > 0) {
                newLastReadPostId = postId;
            }
        });

        if (newLastReadPostId && newLastReadPostId !== lastReadPostId) {
            lastReadPostId = newLastReadPostId;
            console.log(`Tracked last read post for thread ${threadId}: ${lastReadPostId} (no arrow)`);
        }
    }

    // Save last read post to localStorage when leaving the thread
    function saveLastReadPost() {
        if (lastReadPostId) {
            lastReadPosts[threadId] = lastReadPostId;
            try {
                localStorage.setItem('lastReadPosts', JSON.stringify(lastReadPosts));
                console.log(`Saved last read post for thread ${threadId}: ${lastReadPostId}`);
            } catch (e) {
                console.error('Failed to save lastReadPosts to localStorage:', e);
            }
        }
    }

    // Scroll to last read post on load and show arrow
    function scrollToLastReadPost() {
        if (lastReadPostId) {
            const postContainer = document.querySelector(`[id="pc${lastReadPostId}"], [id="p${lastReadPostId}"], [id="post-${lastReadPostId}"], .postCell .linkQuote[href*="${lastReadPostId}"], .postContainer .linkQuote[href*="${lastReadPostId}"]`)?.closest('.postCell, .postContainer');
            if (postContainer) {
                postContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
                addArrow(postContainer);
                console.log(`Scrolled to last read post: ${lastReadPostId}`);
            } else {
                console.warn(`Last read post for ID ${lastReadPostId} not found, retrying in 500ms...`);
                setTimeout(scrollToLastReadPost, 500); // Retry if DOM not fully loaded
            }
        } else {
            console.log('No last read post found for thread:', threadId);
        }
    }

    // Initialize on page load
    window.addEventListener('load', () => {
        scrollToLastReadPost();
        // Attach throttled scroll handler
        const throttledUpdate = throttle(updateLastReadPost, 200);
        window.addEventListener('scroll', throttledUpdate);
        // Log DOM state for debugging
        console.log('Initial post elements found:', document.querySelectorAll('.postCell, .postContainer').length);
    });

    // Save last read post when leaving the thread
    window.addEventListener('beforeunload', saveLastReadPost);

    console.log('Last Read Post Tracker initialized for thread', threadId);
})();
//--Last Read Post Tracker

//Post Number Click Hash Purge
(function() {
    'use strict';

    // Event delegation for post number clicks (.linkQuote with #q<postId> in .postInfo.title)
    document.addEventListener('click', function(e) {
        const link = e.target.closest('.postInfo.title .linkQuote[href*="#q"]');
        if (link) {
            e.preventDefault(); // Block qr.js's default hash-setting
            e.stopPropagation(); // Stop other handlers
            const postId = link.href.match(/#q(\d+)/)?.[1];
            if (!postId) {
                console.warn('Could not extract post ID from link:', link.href);
                return;
            }
            const post = document.getElementById(postId);
            if (!post) {
                console.warn(`Post ${postId} not found for quick reply`);
                return;
            }

            // Preserve current scroll position
            const scrollX = window.scrollX;
            const scrollY = window.scrollY;
            console.log(`Preserving scroll position: x=${scrollX}, y=${scrollY}`);

            // Temporarily block scrollIntoView to prevent qr.js scrolling
            const originalScrollIntoView = Element.prototype.scrollIntoView;
            Element.prototype.scrollIntoView = function() {
                console.log(`Blocked scrollIntoView for post ${postId} during click`);
            };

            // Manually trigger quick reply
            if (window.qr && typeof window.qr.showQr === 'function') {
                window.qr.showQr(postId);
                // Restore scrollIntoView
                Element.prototype.scrollIntoView = originalScrollIntoView;
                // Clear any hash using only history.replaceState
                history.replaceState(null, '', window.location.pathname);
                // Ensure no residual hash
                if (window.location.hash) {
                    console.log(`Residual hash detected: ${window.location.hash}, clearing`);
                    history.replaceState(null, '', window.location.pathname);
                }
                // Restore scroll position to counter any changes
                window.scrollTo(scrollX, scrollY);
                console.log(`Post number click #q${postId}, opened quick reply, cleared hash, restored scroll: x=${scrollX}, y=${scrollY}`);
            } else {
                console.warn('qr.showQr not available, falling back to default behavior');
                // Allow default behavior if qr.js is unavailable
                Element.prototype.scrollIntoView = originalScrollIntoView;
                window.location.hash = `#q${postId}`;
            }
        }
    }, true);
})();
//--Post Number Click Hash Purge

//Quick Reply Clear Button
(function() {
    'use strict';

    // Function to add Clear button to quick reply form
    function addClearButton() {
        const qrForm = document.querySelector('#quick-reply');
        if (!qrForm) {
            console.log('Quick reply form not found');
            return;
        }

        // Check if Clear button already exists
        if (qrForm.querySelector('.qr-clear-button')) {
            console.log('Clear button already added');
            return;
        }

        // Create Clear button
        const clearButton = document.createElement('button');
        clearButton.type = 'button'; // Prevent form submission
        clearButton.className = 'qr-clear-button';
        clearButton.textContent = 'Clear';
        clearButton.style.marginLeft = '5px';
        clearButton.style.padding = '2px 6px';
        clearButton.style.cursor = 'pointer';
        clearButton.style.border = '1px solid';
        clearButton.style.borderRadius = '3px';

        // Add click handler to clear all fields
        clearButton.addEventListener('click', () => {
            const qrBody = qrForm.querySelector('#qrbody');
            const qrName = qrForm.querySelector('#qrname');
            const qrSubject = qrForm.querySelector('#qrsubject');
            if (qrBody) qrBody.value = '';
            if (qrName) qrName.value = '';
            if (qrSubject) qrSubject.value = '';
            console.log('Cleared all quick reply fields');
        });

        // Insert button after the submit button or at the end of the form
        const submitButton = qrForm.querySelector('input[type="submit"]');
        if (submitButton) {
            submitButton.insertAdjacentElement('afterend', clearButton);
        } else {
            qrForm.appendChild(clearButton);
        }
        console.log('Added Clear button to quick reply form');
    }

    // Function to clear message body only
    function clearMessageBody() {
        const qrBody = document.querySelector('#qrbody');
        if (qrBody) {
            qrBody.value = '';
            console.log('Cleared quick reply message body');
        } else {
            console.log('Quick reply message body not found');
        }
    }

    // Track quick reply display state
    let isQrVisible = document.querySelector('#quick-reply') && window.getComputedStyle(document.querySelector('#quick-reply')).display !== 'none';

    // Observe quick reply form for display changes
    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
                const qrForm = document.querySelector('#quick-reply');
                if (!qrForm) return;
                const isNowVisible = window.getComputedStyle(qrForm).display !== 'none';
                if (isNowVisible && !isQrVisible) {
                    // Quick reply opened
                    addClearButton();
                    console.log('Quick reply opened, added Clear button');
                } else if (!isNowVisible && isQrVisible) {
                    // Quick reply closed
                    clearMessageBody();
                }
                isQrVisible = isNowVisible;
            }
        });
    });

    // Start observing the quick reply form (if it exists)
    const qrForm = document.querySelector('#quick-reply');
    if (qrForm) {
        observer.observe(qrForm, {
            attributes: true,
            attributeFilter: ['style']
        });
        // Initial check
        if (window.getComputedStyle(qrForm).display !== 'none') {
            addClearButton();
            isQrVisible = true;
        }
    }

    // Handle direct close button clicks
    document.addEventListener('click', (e) => {
        if (e.target.closest('.close-btn')) {
            clearMessageBody();
        }
    }, true);

    console.log('Quick Reply Clear Button initialized');
})();
//--Quick Reply Clear Button

//Hash Quote Click Hash Purge
(function() {
    'use strict';

    // Event delegation for hash quote clicks (.hash-link)
    document.addEventListener('click', function(e) {
        if (e.target.classList.contains('hash-link')) {
            e.preventDefault(); // Block original Hash navigation handler
            e.stopPropagation(); // Stop other handlers
            const link = e.target.closest('.hash-link-container').previousElementSibling;
            if (!link || !link.textContent.startsWith('>>')) {
                console.warn('Invalid hash link or no associated quote:', e.target);
                return;
            }
            const postId = link.textContent.replace('>>', '');
            const post = document.getElementById(postId);
            if (!post) {
                console.warn(`Post ${postId} not found for hash quote navigation`);
                return;
            }

            // Preserve current scroll position as fallback
            const scrollX = window.scrollX;
            const scrollY = window.scrollY;
            console.log(`Preserving scroll position for hash quote: x=${scrollX}, y=${scrollY}`);

            // Scroll to post
            post.scrollIntoView({ behavior: 'smooth', block: 'center' });
            // Set hash temporarily to trigger scroll (if needed by browser)
            window.location.hash = `#${postId}`;
            // Immediately clear hash
            history.replaceState(null, '', window.location.pathname);
            // Ensure no residual hash
            if (window.location.hash) {
                console.log(`Residual hash detected: ${window.location.hash}, clearing`);
                history.replaceState(null, '', window.location.pathname);
            }
            // Restore scroll position if browser overrides
            window.scrollTo(scrollX, scrollY);
            console.log(`Hash quote click #${postId}, scrolled to post, cleared hash, restored scroll: x=${scrollX}, y=${scrollY}`);
        }
    }, true);
})();
//--Hash Quote Click Hash Purge