Greasy Fork

8chan Style Script

Script to style 8chan

目前为 2025-04-20 提交的版本。查看 最新版本

// ==UserScript==
// @name        8chan Style Script
// @namespace   8chanSS
// @match       *://8chan.moe/*/res/*
// @match       *://8chan.se/*/res/*
// @match       *://8chan.cc/*/res/*
// @match       *://8chan.moe/*/catalog.html
// @match       *://8chan.se/*/catalog.html
// @match       *://8chan.cc/*/catalog.html
// @grant       none
// @version     1.17
// @author      Anon
// @run-at      document-idle
// @description Script to style 8chan
// @license     MIT
// ==/UserScript==
(function () {
    // --- Settings ---
    const scriptSettings = {
        beepOnYou: { label: "Beep on (You)", default: false },
        enableScrollSave: { label: "Save Scroll Position", default: true },
        enableScrollArrows: { label: "Show Up/Down Arrows", default: false },
        blurSpoilers: { label: "Blur Spoilers", default: false },
        enableHeaderCatalogLinks: { label: "Header Catalog Links", default: true },
        enableCatalogImageHover: { label: "Catalog and Image Hover", default: true },
        enableSaveName: { label: "Save Name checkbox", default: true },
        hoverVideoVolume: { label: "Hover Video Volume (0-100%)", default: 50, type: "number", min: 0, max: 100 },
        hidePostingForm: { label: "Hide Posting Form", default: false },
        hideAnnouncement: { label: "Hide Announcement", default: false },
        hidePanelMessage: { label: "Hide Panel Message", default: false },
        enableSidebar: { label: "Enable Sidebar", default: false },
        enableStickyQR: { label: "Enable Sticky Quick Reply", default: false },
        enableFitReplies: { label: "Fit Replies", default: false }
    };

    function getSetting(key) {
        const val = localStorage.getItem('8chanSS_' + key);
        if (val === null) return scriptSettings[key].default;
        if (scriptSettings[key].type === "number") return Number(val);
        return val === 'true';
    }
    function setSetting(key, value) {
        localStorage.setItem('8chanSS_' + key, value);
    }

    // --- Menu Icon ---
    const themeSelector = document.getElementById('themesBefore');
    let link = null;
    let bracketSpan = null;
    if (themeSelector) {
        bracketSpan = document.createElement('span');
        bracketSpan.textContent = '] [ ';
        link = document.createElement('a');
        link.id = '8chanSS-icon';
        link.href = '#';
        link.textContent = '8chanSS';

        themeSelector.parentNode.insertBefore(bracketSpan, themeSelector.nextSibling);
        themeSelector.parentNode.insertBefore(link, bracketSpan.nextSibling);
    }

    // --- Floating Settings Menu ---
    function createSettingsMenu() {
        let menu = document.getElementById('8chanSS-menu');
        if (menu) return menu;
        menu = document.createElement('div');
        menu.id = '8chanSS-menu';
        menu.style.position = 'fixed';
        menu.style.top = '80px';
        menu.style.right = '30px';
        menu.style.zIndex = 99999;
        menu.style.background = '#222';
        menu.style.color = '#fff';
        menu.style.padding = '0';
        menu.style.borderRadius = '8px';
        menu.style.boxShadow = '0 4px 16px rgba(0,0,0,0.25)';
        menu.style.display = 'none';
        menu.style.minWidth = '240px';
        menu.style.fontFamily = 'sans-serif';
        menu.style.userSelect = 'none';

        // Draggable
        let isDragging = false, dragOffsetX = 0, dragOffsetY = 0;
        const header = document.createElement('div');
        header.style.display = 'flex';
        header.style.justifyContent = 'space-between';
        header.style.alignItems = 'center';
        header.style.marginBottom = '2px';
        header.style.cursor = 'move';
        header.style.background = '#333';
        header.style.padding = '5px 18px 5px';
        header.style.borderTopLeftRadius = '8px';
        header.style.borderTopRightRadius = '8px';
        header.addEventListener('mousedown', function (e) {
            isDragging = true;
            const rect = menu.getBoundingClientRect();
            dragOffsetX = e.clientX - rect.left;
            dragOffsetY = e.clientY - rect.top;
            document.body.style.userSelect = 'none';
        });
        document.addEventListener('mousemove', function (e) {
            if (!isDragging) return;
            let newLeft = e.clientX - dragOffsetX;
            let newTop = e.clientY - dragOffsetY;
            const menuRect = menu.getBoundingClientRect();
            const menuWidth = menuRect.width;
            const menuHeight = menuRect.height;
            const viewportWidth = window.innerWidth;
            const viewportHeight = window.innerHeight;
            newLeft = Math.max(0, Math.min(newLeft, viewportWidth - menuWidth));
            newTop = Math.max(0, Math.min(newTop, viewportHeight - menuHeight));
            menu.style.left = newLeft + 'px';
            menu.style.top = newTop + 'px';
            menu.style.right = 'auto';
        });
        document.addEventListener('mouseup', function () {
            isDragging = false;
            document.body.style.userSelect = '';
        });

        // Title and close button
        const title = document.createElement('span');
        title.textContent = '8chanSS Settings';
        title.style.fontWeight = 'bold';
        header.appendChild(title);

        const closeBtn = document.createElement('button');
        closeBtn.textContent = '✕';
        closeBtn.style.background = 'none';
        closeBtn.style.border = 'none';
        closeBtn.style.color = '#fff';
        closeBtn.style.fontSize = '18px';
        closeBtn.style.cursor = 'pointer';
        closeBtn.style.marginLeft = '10px';
        closeBtn.addEventListener('click', () => {
            menu.style.display = 'none';
        });
        header.appendChild(closeBtn);

        menu.appendChild(header);

        // Settings checkboxes and number/slider inputs
        const content = document.createElement('div');
        content.style.padding = '18px 22px 18px 18px';

        // Store current (unsaved) values
        const tempSettings = {};
        Object.keys(scriptSettings).forEach(key => {
            tempSettings[key] = getSetting(key);
        });

        Object.keys(scriptSettings).forEach(key => {
            const setting = scriptSettings[key];
            const wrapper = document.createElement('div');
            wrapper.style.marginBottom = '8px';

            if (key === "hoverVideoVolume") {
                // Compact slider for hover video volume
                const label = document.createElement('label');
                label.htmlFor = 'setting_' + key;
                label.textContent = setting.label + ': ';
                label.style.marginRight = '8px';

                const slider = document.createElement('input');
                slider.type = 'range';
                slider.id = 'setting_' + key;
                slider.min = setting.min;
                slider.max = setting.max;
                slider.value = tempSettings[key];
                slider.style.verticalAlign = 'middle';
                slider.style.marginRight = '6px';
                slider.style.width = '80px'; // Compact width

                const valueLabel = document.createElement('span');
                valueLabel.textContent = slider.value + '%';
                valueLabel.style.display = 'inline-block';
                valueLabel.style.minWidth = '32px';

                slider.addEventListener('input', function () {
                    let val = Number(slider.value);
                    if (isNaN(val)) val = setting.default;
                    val = Math.max(setting.min, Math.min(setting.max, val));
                    slider.value = val;
                    tempSettings[key] = val;
                    valueLabel.textContent = val + '%';
                });

                wrapper.appendChild(label);
                wrapper.appendChild(slider);
                wrapper.appendChild(valueLabel);
            } else {
                // Checkbox for boolean settings
                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.id = 'setting_' + key;
                checkbox.checked = tempSettings[key];
                checkbox.style.marginRight = '8px';

                checkbox.addEventListener('change', function () {
                    tempSettings[key] = checkbox.checked;
                });

                const label = document.createElement('label');
                label.htmlFor = checkbox.id;
                label.textContent = setting.label;

                wrapper.appendChild(checkbox);
                wrapper.appendChild(label);
            }
            content.appendChild(wrapper);
        });

        // Button container for Save and Reset buttons
        const buttonContainer = document.createElement('div');
        buttonContainer.style.display = 'flex';
        buttonContainer.style.gap = '10px';
        buttonContainer.style.marginTop = '10px';
        buttonContainer.style.marginBottom = '5px';

        // Save Button
        const saveBtn = document.createElement('button');
        saveBtn.textContent = 'Save';
        saveBtn.style.background = '#4caf50';
        saveBtn.style.color = '#fff';
        saveBtn.style.border = 'none';
        saveBtn.style.borderRadius = '4px';
        saveBtn.style.padding = '8px 18px';
        saveBtn.style.fontSize = '15px';
        saveBtn.style.cursor = 'pointer';
        saveBtn.style.flex = '1';
        saveBtn.addEventListener('click', function () {
            Object.keys(tempSettings).forEach(key => {
                setSetting(key, tempSettings[key]);
            });
            saveBtn.textContent = 'Saved!';
            setTimeout(() => { saveBtn.textContent = 'Save'; }, 900);
            setTimeout(() => { window.location.reload(); }, 400);
        });
        buttonContainer.appendChild(saveBtn);

        // Reset Button
        const resetBtn = document.createElement('button');
        resetBtn.textContent = 'Reset';
        resetBtn.style.background = '#dd3333';
        resetBtn.style.color = '#fff';
        resetBtn.style.border = 'none';
        resetBtn.style.borderRadius = '4px';
        resetBtn.style.padding = '8px 18px';
        resetBtn.style.fontSize = '15px';
        resetBtn.style.cursor = 'pointer';
        resetBtn.style.flex = '1';
        resetBtn.addEventListener('click', function () {
            if (confirm('Reset all 8chanSS settings to defaults?')) {
                // Find and remove all 8chanSS_ localStorage items
                Object.keys(localStorage).forEach(key => {
                    if (key.startsWith('8chanSS_')) {
                        localStorage.removeItem(key);
                    }
                });
                resetBtn.textContent = 'Reset!';
                setTimeout(() => { resetBtn.textContent = 'Reset'; }, 900);
                setTimeout(() => { window.location.reload(); }, 400);
            }
        });
        buttonContainer.appendChild(resetBtn);

        content.appendChild(buttonContainer);

        // Info
        const info = document.createElement('div');
        info.style.fontSize = '11px';
        info.style.marginTop = '12px';
        info.style.opacity = '0.7';
        info.textContent = 'Press Save to apply changes. Page will reload.';
        content.appendChild(info);

        menu.appendChild(content);

        document.body.appendChild(menu);
        return menu;
    }

    // Hook up the icon to open/close the menu
    if (link) {
        let menu = createSettingsMenu();
        link.style.cursor = 'pointer';
        link.title = 'Open 8chanSS settings';
        link.addEventListener('click', function (e) {
            e.preventDefault();
            menu = createSettingsMenu();
            menu.style.display = (menu.style.display === 'none') ? 'block' : 'none';
        });
    }

    /* --- Scroll Arrows Feature --- */
    function featureScrollArrows() {
        // Only add once
        if (document.getElementById('scroll-arrow-up') || document.getElementById('scroll-arrow-down')) return;

        // Styles for arrows
        const style = document.createElement('style');
        style.textContent = `
                                    .scroll-arrow-btn {
                                        position: fixed;
                                        right: 330px;
                                        width: 36px;
                                        height: 35px;
                                        background: #222;
                                        color: #fff;
                                        border: none;
                                        border-radius: 50%;
                                        box-shadow: 0 2px 8px rgba(0,0,0,0.18);
                                        font-size: 22px;
                                        cursor: pointer;
                                        opacity: 0.7;
                                        z-index: 99998;
                                        display: flex;
                                        align-items: center;
                                        justify-content: center;
                                        transition: opacity 0.2s, background 0.2s;
                                    }
                                    .scroll-arrow-btn:hover {
                                        opacity: 1;
                                        background: #444;
                                    }
                                    #scroll-arrow-up { bottom: 80px; }
                                    #scroll-arrow-down { bottom: 32px; }
                                    `;
        document.head.appendChild(style);

        // Up arrow
        const upBtn = document.createElement('button');
        upBtn.id = 'scroll-arrow-up';
        upBtn.className = 'scroll-arrow-btn';
        upBtn.title = 'Scroll to top';
        upBtn.innerHTML = '▲';
        upBtn.addEventListener('click', () => {
            window.scrollTo({ top: 0, behavior: 'smooth' });
        });

        // Down arrow
        const downBtn = document.createElement('button');
        downBtn.id = 'scroll-arrow-down';
        downBtn.className = 'scroll-arrow-btn';
        downBtn.title = 'Scroll to bottom';
        downBtn.innerHTML = '▼';
        downBtn.addEventListener('click', () => {
            const footer = document.getElementById('footer');
            if (footer) {
                footer.scrollIntoView({ behavior: 'smooth', block: 'end' });
            } else {
                window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
            }
        });

        document.body.appendChild(upBtn);
        document.body.appendChild(downBtn);
    }

    /* --- Feature: Beep on (You) --- */
    function featureBeepOnYou() {
        // Beep sound (same base64)
        const beep = new Audio('data:audio/wav;base64,UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA');

        // Store the original title
        const originalTitle = document.title;
        let isNotifying = false;

        // Create MutationObserver to detect when you are quoted
        const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === 1 && node.querySelector && node.querySelector('a.quoteLink.you')) {
                        playBeep();
                        addNotificationToTitle();
                    }
                });
            });
        });

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

        // Function to play the beep sound
        function playBeep() {
            if (beep.paused) {
                beep.play().catch(e => console.warn("Beep failed:", e));
            } else {
                beep.addEventListener('ended', () => beep.play(), { once: true });
            }
        }

        // Function to add notification to the title
        function addNotificationToTitle() {
            if (!isNotifying && !document.hasFocus()) {
                isNotifying = true;
                document.title = "(!) " + originalTitle;
            }
        }

        // Remove notification when tab regains focus
        window.addEventListener('focus', () => {
            if (isNotifying) {
                document.title = originalTitle;
                isNotifying = false;
            }
        });
    }

    // --- Feature: Header Catalog Links ---
    function featureHeaderCatalogLinks() {
        function appendCatalogToLinks() {
            const navboardsSpan = document.getElementById('navBoardsSpan');
            if (navboardsSpan) {
                const links = navboardsSpan.getElementsByTagName('a');
                for (let link of links) {
                    if (link.href && !link.href.endsWith('/catalog.html')) {
                        link.href += '/catalog.html';
                    }
                }
            }
        }
        appendCatalogToLinks();
        const observer = new MutationObserver(appendCatalogToLinks);
        const config = { childList: true, subtree: true };
        const navboardsSpan = document.getElementById('navBoardsSpan');
        if (navboardsSpan) {
            observer.observe(navboardsSpan, config);
        }
    }


    // --- Feature: Save Scroll Position ---
    function featureSaveScrollPosition() {
        const MAX_PAGES = 50;
        const currentPage = window.location.href;
        const excludedPagePatterns = [
            /\/catalog\.html$/i,
        ];
        function isExcludedPage(url) {
            return excludedPagePatterns.some(pattern => pattern.test(url));
        }
        function saveScrollPosition() {
            if (isExcludedPage(currentPage)) return;
            const scrollPosition = window.scrollY;
            localStorage.setItem(`8chanSS_scrollPosition_${currentPage}`, scrollPosition);
            manageScrollStorage();
        }
        function restoreScrollPosition() {
            const savedPosition = localStorage.getItem(`8chanSS_scrollPosition_${currentPage}`);
            if (savedPosition) {
                window.scrollTo(0, parseInt(savedPosition, 10));
            }
        }
        function manageScrollStorage() {
            const keys = Object.keys(localStorage).filter(key => key.startsWith('8chanSS_scrollPosition_'));
            if (keys.length > MAX_PAGES) {
                keys.sort((a, b) => {
                    return localStorage.getItem(a) - localStorage.getItem(b);
                });
                while (keys.length > MAX_PAGES) {
                    localStorage.removeItem(keys.shift());
                }
            }
        }
        window.addEventListener('beforeunload', saveScrollPosition);
        window.addEventListener('load', restoreScrollPosition);
    }

    // --- Feature: Catalog & Image Hover (with video fix and volume setting) ---
    function featureCatalogImageHover() {
        function getFullMediaSrcFromMime(thumbnailSrc, filemime) {
            if (!thumbnailSrc || !filemime) return null;
            let base = thumbnailSrc.replace(/\/t_/, '/');
            base = base.replace(/\.(jpe?g|png|gif|webp|webm|mp4)$/i, '');
            const mimeToExt = {
                'image/jpeg': '.jpg',
                'image/jpg': '.jpg',
                'image/png': '.png',
                'image/gif': '.gif',
                'image/webp': '.webp',
                'video/mp4': '.mp4',
                'video/webm': '.webm'
            };
            const ext = mimeToExt[filemime.toLowerCase()];
            if (!ext) return null;
            return base + ext;
        }

        let floatingMedia = null;
        let removeListeners = null;
        let hoverTimeout = null;
        let lastThumb = null;
        let isStillHovering = false;

        function cleanupFloatingMedia() {
            if (hoverTimeout) {
                clearTimeout(hoverTimeout);
                hoverTimeout = null;
            }
            if (removeListeners) {
                removeListeners();
                removeListeners = null;
            }
            if (floatingMedia) {
                if (floatingMedia.tagName === 'VIDEO') {
                    try {
                        floatingMedia.pause();
                        floatingMedia.removeAttribute('src');
                        floatingMedia.load();
                    } catch (e) { }
                }
                if (floatingMedia.parentNode) {
                    floatingMedia.parentNode.removeChild(floatingMedia);
                }
            }
            floatingMedia = null;
            lastThumb = null;
            isStillHovering = false;
            document.removeEventListener('mousemove', onMouseMove);
        }

        function onMouseMove(event) {
            if (!floatingMedia) return;
            const viewportWidth = window.innerWidth;
            const viewportHeight = window.innerHeight;
            let mediaWidth = 0, mediaHeight = 0;
            if (floatingMedia.tagName === 'IMG') {
                mediaWidth = floatingMedia.naturalWidth || floatingMedia.width || floatingMedia.offsetWidth || 0;
                mediaHeight = floatingMedia.naturalHeight || floatingMedia.height || floatingMedia.offsetHeight || 0;
            } else if (floatingMedia.tagName === 'VIDEO') {
                mediaWidth = floatingMedia.videoWidth || floatingMedia.offsetWidth || 0;
                mediaHeight = floatingMedia.videoHeight || floatingMedia.offsetHeight || 0;
            }
            mediaWidth = Math.min(mediaWidth, viewportWidth * 0.9);
            mediaHeight = Math.min(mediaHeight, viewportHeight * 0.9);
            let newX = event.clientX + 10;
            let newY = event.clientY + 10;
            if (newX + mediaWidth > viewportWidth) {
                newX = viewportWidth - mediaWidth - 10;
            }
            if (newY + mediaHeight > viewportHeight) {
                newY = viewportHeight - mediaHeight - 10;
            }
            newX = Math.max(newX, 0);
            newY = Math.max(newY, 0);
            floatingMedia.style.left = `${newX}px`;
            floatingMedia.style.top = `${newY}px`;
            floatingMedia.style.maxWidth = '90vw';
            floatingMedia.style.maxHeight = '90vh';
        }

        function onThumbEnter(e) {
            const thumb = e.currentTarget;
            // Debounce: if already hovering this thumb, do nothing
            if (lastThumb === thumb) return;
            lastThumb = thumb;

            // Clean up any previous floating media and debounce
            cleanupFloatingMedia();

            isStillHovering = true;

            // Listen for mouseleave to cancel hover if left before timeout
            function onLeave() {
                isStillHovering = false;
                cleanupFloatingMedia();
            }
            thumb.addEventListener('mouseleave', onLeave, { once: true });

            // Debounce: wait a short time before showing preview
            hoverTimeout = setTimeout(() => {
                hoverTimeout = null;
                // If mouse has left before timeout, do not show preview
                if (!isStillHovering) return;

                const parentA = thumb.closest('a.linkThumb, a.imgLink');
                if (!parentA) return;
                const filemime = parentA.getAttribute('data-filemime');
                const fullSrc = getFullMediaSrcFromMime(thumb.getAttribute('src'), filemime);
                if (!fullSrc) return;

                let loaded = false;
                function setCommonStyles(el) {
                    el.style.position = 'fixed';
                    el.style.zIndex = 9999;
                    el.style.pointerEvents = 'none';
                    el.style.maxWidth = '90vw';
                    el.style.maxHeight = '90vh';
                    el.style.transition = 'opacity 0.15s';
                    el.style.opacity = '0';
                    el.style.left = '-9999px';
                }

                // Setup cleanup listeners
                removeListeners = function () {
                    window.removeEventListener('scroll', cleanupFloatingMedia, true);
                };
                window.addEventListener('scroll', cleanupFloatingMedia, true);

                if (filemime && filemime.startsWith('image/')) {
                    floatingMedia = document.createElement('img');
                    setCommonStyles(floatingMedia);
                    floatingMedia.onload = function () {
                        if (!loaded && floatingMedia && isStillHovering) {
                            loaded = true;
                            floatingMedia.style.opacity = '1';
                            document.body.appendChild(floatingMedia);
                            document.addEventListener('mousemove', onMouseMove);
                            onMouseMove(e);
                        }
                    };
                    floatingMedia.onerror = cleanupFloatingMedia;
                    floatingMedia.src = fullSrc;
                } else if (filemime && filemime.startsWith('video/')) {
                    floatingMedia = document.createElement('video');
                    setCommonStyles(floatingMedia);
                    floatingMedia.autoplay = true;
                    floatingMedia.loop = true;
                    floatingMedia.muted = false;
                    floatingMedia.playsInline = true;
                    // Set volume from settings (0-100)
                    let volume = typeof getSetting === "function" ? getSetting('hoverVideoVolume') : 50;
                    if (typeof volume !== 'number' || isNaN(volume)) volume = 50;
                    floatingMedia.volume = Math.max(0, Math.min(1, volume / 100));
                    floatingMedia.onloadeddata = function () {
                        if (!loaded && floatingMedia && isStillHovering) {
                            loaded = true;
                            floatingMedia.style.opacity = '1';
                            document.body.appendChild(floatingMedia);
                            document.addEventListener('mousemove', onMouseMove);
                            onMouseMove(e);
                        }
                    };
                    floatingMedia.onerror = cleanupFloatingMedia;
                    floatingMedia.src = fullSrc;
                }
            }, 120); // 120ms debounce for both images and videos
        }

        function attachThumbListeners(root) {
            const thumbs = (root || document).querySelectorAll('a.linkThumb > img, a.imgLink > img');
            thumbs.forEach(thumb => {
                if (!thumb._fullImgHoverBound) {
                    thumb.addEventListener('mouseenter', onThumbEnter);
                    thumb._fullImgHoverBound = true;
                }
            });
        }

        attachThumbListeners();
        const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        attachThumbListeners(node);
                    }
                });
            });
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // --- Feature: Save Name Checkbox ---
    function featureSaveNameCheckbox() {
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.id = 'saveNameCheckbox';
        checkbox.classList.add('postingCheckbox');
        const label = document.createElement('label');
        label.htmlFor = 'saveNameCheckbox';
        label.textContent = 'Save Name';
        label.title = 'Save Name on refresh';
        const alwaysUseBypassCheckbox = document.getElementById('qralwaysUseBypassCheckBox');
        if (alwaysUseBypassCheckbox) {
            alwaysUseBypassCheckbox.parentNode.insertBefore(checkbox, alwaysUseBypassCheckbox);
            alwaysUseBypassCheckbox.parentNode.insertBefore(label, checkbox.nextSibling);
            const savedCheckboxState = localStorage.getItem('8chanSS_saveNameCheckbox') === 'true';
            checkbox.checked = savedCheckboxState;
            const nameInput = document.getElementById('qrname');
            if (nameInput) {
                const savedName = localStorage.getItem('8chanSS_name');
                if (checkbox.checked && savedName !== null) {
                    nameInput.value = savedName;
                } else if (!checkbox.checked) {
                    nameInput.value = '';
                }
                nameInput.addEventListener('input', function () {
                    if (checkbox.checked) {
                        localStorage.setItem('8chanSS_name', nameInput.value);
                    }
                });
                checkbox.addEventListener('change', function () {
                    if (checkbox.checked) {
                        localStorage.setItem('8chanSS_name', nameInput.value);
                    } else {
                        localStorage.removeItem('8chanSS_name');
                        nameInput.value = '';
                    }
                    localStorage.setItem('8chanSS_saveNameCheckbox', checkbox.checked);
                });
            }
        }
    }

    /* --- Feature: Blur Spoilers --- */
    function featureBlurSpoilers() {
        function revealSpoilers() {
            const spoilerLinks = document.querySelectorAll('a.imgLink');
            spoilerLinks.forEach(link => {
                const img = link.querySelector('img');
                if (img && !img.src.includes('/.media/t_')) {
                    let href = link.getAttribute('href');
                    if (href) {
                        // Extract filename without extension
                        const match = href.match(/\/\.media\/([^\/]+)\.[a-zA-Z0-9]+$/);
                        if (match) {
                            // Use the thumbnail path (t_filename)
                            const transformedSrc = `/\.media/t_${match[1]}`;
                            img.src = transformedSrc;

                            // Apply blur style
                            img.style.filter = 'blur(5px)';
                            img.style.transition = 'filter 0.3s ease';

                            // Unblur on hover
                            img.addEventListener('mouseover', () => {
                                img.style.filter = 'none';
                            });
                            img.addEventListener('mouseout', () => {
                                img.style.filter = 'blur(5px)';
                            });
                        }
                    }
                }
            });
        }

        // Initial run
        revealSpoilers();

        // Observe for dynamically added spoilers
        const observer = new MutationObserver(revealSpoilers);
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // --- Feature: CSS Class Toggles ---
    function featureCssClassToggles() {
        // Map of setting keys to CSS class names
        const classToggles = {
            'enableFitReplies': 'fit-replies',
            'enableSidebar': 'ss-sidebar',
            'enableStickyQR': 'sticky-qr'
            // Add more class toggles here in the future
        };

        // Process each toggle
        Object.entries(classToggles).forEach(([settingKey, className]) => {
            if (getSetting(settingKey)) {
                document.documentElement.classList.add(className);
            } else {
                document.documentElement.classList.remove(className);
            }
        });
    }

    // --- Feature: Hide/Show Posting Form, Announcement, Panel Message ---
    function featureHideElements() {
        // These settings are: hidePostingForm, hideAnnouncement, hidePanelMessage
        const postingFormDiv = document.getElementById('postingForm');
        const announcementDiv = document.getElementById('dynamicAnnouncement');
        const panelMessageDiv = document.getElementById('panelMessage');
        if (postingFormDiv) {
            postingFormDiv.style.display = getSetting('hidePostingForm') ? 'none' : '';
        }
        if (announcementDiv) {
            announcementDiv.style.display = getSetting('hideAnnouncement') ? 'none' : '';
        }
        if (panelMessageDiv) {
            panelMessageDiv.style.display = getSetting('hidePanelMessage') ? 'none' : '';
        }
    }

    // --- Feature Initialization based on Settings ---
    if (getSetting('blurSpoilers')) {
        featureBlurSpoilers();
    }
    if (getSetting('enableHeaderCatalogLinks')) {
        featureHeaderCatalogLinks();
    }
    if (getSetting('enableScrollSave')) {
        featureSaveScrollPosition();
    }
    if (getSetting('enableCatalogImageHover')) {
        featureCatalogImageHover();
    }
    if (getSetting('enableSaveName')) {
        featureSaveNameCheckbox();
    }
    if (getSetting('enableScrollArrows')) {
        featureScrollArrows();
    }
    if (getSetting('beepOnYou')) {
        featureBeepOnYou();
    }
    // Always run hide/show feature (it will respect settings)
    featureHideElements();
    featureCssClassToggles();

    /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    // Keyboard Shortcuts
    // QR (CTRL+Q)
    function toggleQR(event) {
        // Check if Ctrl + Q is pressed
        if (event.ctrlKey && (event.key === 'q' || event.key === 'Q')) {
            const hiddenDiv = document.getElementById('quick-reply');
            // Toggle QR
            if (hiddenDiv.style.display === 'none' || hiddenDiv.style.display === '') {
                hiddenDiv.style.display = 'block'; // Show the div
            }
            else {
                hiddenDiv.style.display = 'none'; // Hide the div
            }
        }
    }
    document.addEventListener('keydown', toggleQR);

    // Clear textarea and hide quick-reply on Escape key
    function clearTextarea(event) {
        // Check if Escape key is pressed
        if (event.key === 'Escape') {
            // Clear the textarea
            const textarea = document.getElementById('qrbody');
            if (textarea) {
                textarea.value = ''; // Clear the textarea
            }

            // Hide the quick-reply div
            const quickReply = document.getElementById('quick-reply');
            if (quickReply) {
                quickReply.style.display = 'none'; // Hide the quick-reply
            }
        }
    }
    document.addEventListener('keydown', clearTextarea);

    // Tags
    const bbCodeCombinations = new Map([
        ["s", ["[spoiler]", "[/spoiler]"]],
        ["b", ["'''", "'''"]],
        ["u", ["__", "__"]],
        ["i", ["''", "''"]],
        ["d", ["[doom]", "[/doom]"]],
        ["m", ["[moe]", "[/moe]"]],
        ["c", ["[code]", "[/code]"]],
    ]);

    function replyKeyboardShortcuts(ev) {
        const key = ev.key.toLowerCase();
        // Special case: alt+c for [code] tag
        if (key === "c" && ev.altKey && !ev.ctrlKey && bbCodeCombinations.has(key)) {
            ev.preventDefault();
            const textBox = ev.target;
            const [openTag, closeTag] = bbCodeCombinations.get(key);
            const { selectionStart, selectionEnd, value } = textBox;
            if (selectionStart === selectionEnd) {
                // No selection: insert empty tags and place cursor between them
                const before = value.slice(0, selectionStart);
                const after = value.slice(selectionEnd);
                const newCursor = selectionStart + openTag.length;
                textBox.value = before + openTag + closeTag + after;
                textBox.selectionStart = textBox.selectionEnd = newCursor;
            } else {
                // Replace selected text with tags around it
                const before = value.slice(0, selectionStart);
                const selected = value.slice(selectionStart, selectionEnd);
                const after = value.slice(selectionEnd);
                textBox.value = before + openTag + selected + closeTag + after;
                // Keep selection around the newly wrapped text
                textBox.selectionStart = selectionStart + openTag.length;
                textBox.selectionEnd = selectionEnd + openTag.length;
            }
            return;
        }
        // All other tags: ctrl+key
        if (ev.ctrlKey && !ev.altKey && bbCodeCombinations.has(key) && key !== "c") {
            ev.preventDefault();
            const textBox = ev.target;
            const [openTag, closeTag] = bbCodeCombinations.get(key);
            const { selectionStart, selectionEnd, value } = textBox;
            if (selectionStart === selectionEnd) {
                // No selection: insert empty tags and place cursor between them
                const before = value.slice(0, selectionStart);
                const after = value.slice(selectionEnd);
                const newCursor = selectionStart + openTag.length;
                textBox.value = before + openTag + closeTag + after;
                textBox.selectionStart = textBox.selectionEnd = newCursor;
            } else {
                // Replace selected text with tags around it
                const before = value.slice(0, selectionStart);
                const selected = value.slice(selectionStart, selectionEnd);
                const after = value.slice(selectionEnd);
                textBox.value = before + openTag + selected + closeTag + after;
                // Keep selection around the newly wrapped text
                textBox.selectionStart = selectionStart + openTag.length;
                textBox.selectionEnd = selectionEnd + openTag.length;
            }
            return;
        }
    }
    document.getElementById("qrbody")?.addEventListener("keydown", replyKeyboardShortcuts);

    /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    // Custom CSS injection
    function addCustomCSS(css) {
        if (!css) return;
        const style = document.createElement('style');
        style.type = 'text/css';
        style.appendChild(document.createTextNode(css));
        document.head.appendChild(style);
    }
    // Get the current URL path
    const currentPath = window.location.pathname.toLowerCase();
    const currentHost = window.location.hostname.toLowerCase();

    // Apply CSS based on URL pattern
    // Thread page CSS
    if (/\/res\/[^/]+\.html$/.test(currentPath)) {
        const css = `
/* Quick Reply */
:root.sticky-qr #quick-reply {
display: block;
top: auto !important;
bottom: 0;
left: auto !important;
position: fixed;
right: 0 !important;
}
#quick-reply {
padding: 0;
opacity: 0.7;
transition: opacity 0.3s ease;
}
#quick-reply:hover,
#quick-reply:focus-within {
opacity: 1;
}
#qrbody {
resize: vertical;
max-height: 50vh;
height: 130px;
}
.floatingMenu {
padding: 0 !important;
}
#qrFilesBody {
max-width: 300px;
}
/* Banner */
:root.ss-sidebar #bannerImage {
width: 305px;
right: 0;
position: fixed;
top: 26px;
}
.innerUtility.top {
margin-top: 2em;
background-color: transparent !important;
color: var(--link-color) !important;
}
.innerUtility.top a {
color: var(--link-color) !important;
}
/* Hover Posts */
img[style*="position: fixed"] {
max-width: 80vw;
max-height: 80vh !important;
z-index: 200;
}
.quoteTooltip {
z-index: 110;
}
/* (You) Replies */
.innerPost:has(.youName) {
border-left: solid #68b723 5px;
}
.innerPost:has(.quoteLink.you) {
border-left: solid #dd003e 5px;
}
/* Filename & Thumbs */
.originalNameLink {
display: inline;
overflow-wrap: anywhere;
white-space: normal;
}
.multipleUploads .uploadCell:not(.expandedCell) {
  max-width: 215px;
}
`;
        addCustomCSS(css);
    }

    if (/^8chan\.(se|moe)$/.test(currentHost)) {
        // General CSS for all pages
        const css = `
/* Margins */
:root.ss-sidebar #mainPanel {
    margin-right: 305px;
}
/* Cleanup */
#navFadeEnd,
#navFadeMid,
#navTopBoardsSpan,
.coloredIcon.linkOverboard,
.coloredIcon.linkSfwOver,
.coloredIcon.multiboardButton,
#navLinkSpan>span:nth-child(9),
#navLinkSpan>span:nth-child(11),
#navLinkSpan>span:nth-child(13) {
display: none;
}
footer {
visibility: hidden;
height: 0;
}
/* Header */
#dynamicHeaderThread,
.navHeader {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}
/* Thread Watcher */
#watchedMenu .floatingContainer {
min-width: 330px;
}
#watchedMenu .watchedCellLabel > a:after {
content: " - "attr(href);
filter: saturate(50%);
font-style: italic;
font-weight: bold;
}
#watchedMenu {
box-shadow: -3px 3px 2px 0px rgba(0,0,0,0.19);
}
/* Posts */
.quoteTooltip .innerPost {
overflow: hidden;
box-shadow: -3px 3px 2px 0px rgba(0,0,0,0.19);
}
:root.fit-replies .innerPost {
margin-left: 10px;
display: flow-root;
}
`;
        addCustomCSS(css);
    }

    // Catalog page CSS
    if (/\/catalog\.html$/.test(currentPath)) {
        const css = `
#dynamicAnnouncement {
display: none;
}
#postingForm {
margin: 2em auto;
}
`;
        addCustomCSS(css);
    }
})();