8chan Style Script

Script to style 8chan

目前為 2025-04-19 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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.15
// @author      Anon
// @run-at      document-idle
// @description Script to style 8chan
// @license     MIT
// ==/UserScript==
(function () {
    // --- Settings ---
    const scriptSettings = {
        enableScrollSave: { label: "Save Scroll Position", default: true },
        enableScrollArrows: { label: "Show Scroll Arrows", default: false },
        enableHeaderCatalogLinks: { label: "Header Catalog Links", default: true },
        enableCatalogImageHover: { label: "Catalog and Image Hover", default: true },
        enableSaveName: { label: "Save Name checkbox", default: true },
        enableFitReplies: { label: "Fit Replies", default: false },
        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 }
    };

    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('userscript-settings-menu');
        if (menu) return menu;
        menu = document.createElement('div');
        menu.id = 'userscript-settings-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);
        });

        // 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.marginTop = '10px';
        saveBtn.style.marginBottom = '5px';
        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);
        });
        content.appendChild(saveBtn);

        // 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: Fit Replies (CSS toggle) ---
    function featureFitReplies() {
        document.documentElement.classList.add('fit-replies');
        if (!document.getElementById('fit-replies-style')) {
            const style = document.createElement('style');
            style.id = 'fit-replies-style';
            style.textContent = `
                                :root.fit-replies .innerPost {
                                    margin-left: 40px;
                                    display: block;
                                }
            `;
            document.head.appendChild(style);
        }
    }

    // --- 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(`scrollPosition_${currentPage}`, scrollPosition);
            manageScrollStorage();
        }
        function restoreScrollPosition() {
            const savedPosition = localStorage.getItem(`scrollPosition_${currentPage}`);
            if (savedPosition) {
                window.scrollTo(0, parseInt(savedPosition, 10));
            }
        }
        function manageScrollStorage() {
            const keys = Object.keys(localStorage).filter(key => key.startsWith('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;

        function cleanupFloatingMedia() {
            if (hoverTimeout) {
                clearTimeout(hoverTimeout);
                hoverTimeout = null;
            }
            if (removeListeners) {
                removeListeners();
                removeListeners = null;
            }
            if (floatingMedia) {
                // If it's a video, pause and remove src to stop playback
                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;
            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
            cleanupFloatingMedia();

            // Debounce: wait a short time before showing preview
            hoverTimeout = setTimeout(() => {
                hoverTimeout = null;
                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 () {
                    thumb.removeEventListener('mouseleave', cleanupFloatingMedia);
                    window.removeEventListener('scroll', cleanupFloatingMedia, true);
                };
                thumb.addEventListener('mouseleave', cleanupFloatingMedia);
                window.addEventListener('scroll', cleanupFloatingMedia, true);

                if (filemime && filemime.startsWith('image/')) {
                    floatingMedia = document.createElement('img');
                    setCommonStyles(floatingMedia);
                    floatingMedia.onload = function () {
                        if (!loaded && floatingMedia) {
                            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 = getSetting('hoverVideoVolume');
                    if (typeof volume !== 'number' || isNaN(volume)) volume = 50;
                    floatingMedia.volume = Math.max(0, Math.min(1, volume / 100));
                    floatingMedia.onloadeddata = function () {
                        if (!loaded && floatingMedia) {
                            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
        }

        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('saveNameCheckbox') === 'true';
            checkbox.checked = savedCheckboxState;
            const nameInput = document.getElementById('qrname');
            if (nameInput) {
                const savedName = localStorage.getItem('name');
                if (checkbox.checked && savedName !== null) {
                    nameInput.value = savedName;
                } else if (!checkbox.checked) {
                    nameInput.value = '';
                }
                nameInput.addEventListener('input', function () {
                    if (checkbox.checked) {
                        localStorage.setItem('name', nameInput.value);
                    }
                });
                checkbox.addEventListener('change', function () {
                    if (checkbox.checked) {
                        localStorage.setItem('name', nameInput.value);
                    } else {
                        localStorage.removeItem('name');
                        nameInput.value = '';
                    }
                    localStorage.setItem('saveNameCheckbox', checkbox.checked);
                });
            }
        }
    }

    // --- 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('enableFitReplies')) {
        featureFitReplies();
    }
    if (getSetting('enableHeaderCatalogLinks')) {
        featureHeaderCatalogLinks();
    }
    if (getSetting('enableScrollSave')) {
        featureSaveScrollPosition();
    }
    if (getSetting('enableCatalogImageHover')) {
        featureCatalogImageHover();
    }
    if (getSetting('enableSaveName')) {
        featureSaveNameCheckbox();
    }
    if (getSetting('enableScrollArrows')) {
        featureScrollArrows();
    }
    // Always run hide/show feature (it will respect settings)
    featureHideElements();

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

    // Keyboard Shortcuts
    // QR (CTRL+Q)
    function toggleDiv(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', toggleDiv);

    // 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 */
#quick-reply {
display: block;
padding: 0 !important;
top: auto !important;
bottom: 0;
left: auto !important;
position: fixed;
right: 0 !important;
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 */
#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 */
.originalNameLink {
display: inline;
overflow-wrap: anywhere;
white-space: normal;
}
`;
        addCustomCSS(css);
    }

    if (/^8chan\.(se|moe)$/.test(currentHost)) {
        // General CSS for all pages
        const css = `
/* Margins */
#mainPanel {
margin-left: 10px;
margin-right: 305px;
margin-top: 0;
margin-bottom: 0;
}
/* 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);
}
`;
        addCustomCSS(css);
    }

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