Greasy Fork

Greasy Fork is available in English.

8chan Style Script

Script to style 8chan

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        8chan Style Script
// @namespace   8chanSS
// @match       *://8chan.moe/*
// @match       *://8chan.se/*
// @match       *://8chan.cc/*
// @exclude     *://8chan.moe/login.html
// @exclude     *://8chan.se/login.html
// @exclude     *://8chan.cc/login.html
// @exclude     *://8chan.moe/*mod.js
// @exclude     *://8chan.se/*mod.js
// @exclude     *://8chan.cc/*mod.js
// @grant       none
// @version     1.12
// @author      Anon
// @run-at      document-idle
// @description Script to style 8chan
// @license     MIT
// ==/UserScript==
(function () {
    var defaultConfig = {}; // TODO add menu and default configs to toggle options

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

    // Header Catalog Links
    // Function to append /catalog.html to links
    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';
                }
            }
        }
    }
    // Initial call to append links on page load
    appendCatalogToLinks();

    // Set up a MutationObserver to watch for changes in the #navboardsSpan div
    const observer = new MutationObserver(appendCatalogToLinks);
    const config = { childList: true, subtree: true };

    const navboardsSpan = document.getElementById('navBoardsSpan');
    if (navboardsSpan) {
        observer.observe(navboardsSpan, config);
    }

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

    // Scroll to last read post
    // Function to save the scroll position
    const MAX_PAGES = 50; // Maximum number of pages to store scroll positions
    const currentPage = window.location.href;

    // Specify pages to exclude from scroll position saving (supports wildcards)
    const excludedPagePatterns = [
        /\/catalog\.html$/i, // Exclude any page ending with /catalog.html (case-insensitive)
        // Add more patterns as needed
    ];

    // Function to check if current page matches any exclusion pattern
    function isExcludedPage(url) {
        return excludedPagePatterns.some(pattern => pattern.test(url));
    }

    // Function to save the scroll position for the current page
    function saveScrollPosition() {
        // Check if the current page matches any excluded pattern
        if (isExcludedPage(currentPage)) {
            return; // Skip saving scroll position for excluded pages
        }

        const scrollPosition = window.scrollY; // Get the current vertical scroll position
        localStorage.setItem(`scrollPosition_${currentPage}`, scrollPosition); // Store it in localStorage with a unique key

        // Manage the number of stored scroll positions
        manageScrollStorage();
    }

    // Function to restore the scroll position for the current page
    function restoreScrollPosition() {
        const savedPosition = localStorage.getItem(`scrollPosition_${currentPage}`); // Retrieve the saved position for the current page
        if (savedPosition) {
            window.scrollTo(0, parseInt(savedPosition, 10)); // Scroll to the saved position
        }
    }

    // Function to manage the number of stored scroll positions
    function manageScrollStorage() {
        const keys = Object.keys(localStorage).filter(key => key.startsWith('scrollPosition_'));

        // If the number of stored positions exceeds the limit, remove the oldest
        if (keys.length > MAX_PAGES) {
            // Sort keys by their creation time (assuming the order of keys reflects the order of storage)
            keys.sort((a, b) => {
                return localStorage.getItem(a) - localStorage.getItem(b);
            });
            // Remove the oldest entries until we are within the limit
            while (keys.length > MAX_PAGES) {
                localStorage.removeItem(keys.shift());
            }
        }
    }

    // Event listener to save scroll position before the page unloads
    window.addEventListener('beforeunload', saveScrollPosition);

    // Restore scroll position when the page loads
    window.addEventListener('load', restoreScrollPosition);

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

    // Toggle Announcement & Posting Form
    // Create the button
    const button = document.createElement('button');
    button.style.margin = '10px';
    const postingFormDiv = document.getElementById('postingForm');
    const announcementDiv = document.getElementById('dynamicAnnouncement');
    const panelMessageDiv = document.getElementById('panelMessage');

    // Check if divs exist
    if (postingFormDiv && announcementDiv && panelMessageDiv) {
        // Insert the button before the announcement div
        postingFormDiv.parentNode.insertBefore(button, postingFormDiv);
        // Retrieve the visibility states from localStorage
        const isPostingFormVisible = localStorage.getItem('postingFormVisible') === 'true';
        const isAnnouncementVisible = localStorage.getItem('announcementVisible') === 'true';
        const isPanelMessageVisible = localStorage.getItem('panelMessageVisible') === 'true';
        // Set the initial state of the divs and button based on stored values
        if (isPostingFormVisible) {
            postingFormDiv.style.display = 'block'; // Show the posting div
        } else {
            postingFormDiv.style.display = 'none'; // Hide the posting div
        }
        if (isAnnouncementVisible) {
            announcementDiv.style.display = 'block'; // Show the announcement div
        } else {
            announcementDiv.style.display = 'none'; // Hide the announcement div
        }
        if (isPanelMessageVisible) {
            panelMessageDiv.style.display = 'block'; // Show the panel message div
        } else {
            panelMessageDiv.style.display = 'none'; // Hide the panel message div
        }
        // Update button text based on the visibility of the announcement div
        button.textContent = (isPostingFormVisible && isAnnouncementVisible && isPanelMessageVisible) ? '-' : '+';
        // Add click event listener to the button
        button.addEventListener('click', () => {
            // Toggle visibility of both divs
            const isCurrentlyVisible = postingFormDiv.style.display !== 'none' && announcementDiv.style.display !== 'none' && panelMessageDiv.style.display !== 'none';

            if (isCurrentlyVisible) {
                postingFormDiv.style.display = 'none'; // Hide the posting div
                announcementDiv.style.display = 'none'; // Hide the announcement div
                panelMessageDiv.style.display = 'none'; // Hide the panel message div
                button.textContent = '+'; // Change button text
                localStorage.setItem('postingFormVisible', 'false'); // Save state
                localStorage.setItem('announcementVisible', 'false'); // Save state
                localStorage.setItem('panelMessageVisible', 'false'); // Save state
            } else {
                postingFormDiv.style.display = 'block'; // Hide the posting div
                announcementDiv.style.display = 'block'; // Show the announcement div
                panelMessageDiv.style.display = 'block'; // Show the panel message div
                button.textContent = '-'; // Change button text
                localStorage.setItem('postingFormVisible', 'true'); // Save state
                localStorage.setItem('announcementVisible', 'true'); // Save state
                localStorage.setItem('panelMessageVisible', 'true'); // Save state
            }
        });
    }

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

    // 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
            }
        }
    }
    // Add an event listener for keydown events
    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;
        }
    }

    // Attach the handler
    document.getElementById("qrbody")?.addEventListener("keydown", replyKeyboardShortcuts);

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

    // Catalog & Thread Image/Video Hover
    (function () {
        // Helper: Get full media src from thumbnail and filemime
        function getFullMediaSrcFromMime(thumbnailSrc, filemime) {
            if (!thumbnailSrc || !filemime) return null;
            // Remove "t_" from the filename
            let base = thumbnailSrc.replace(/\/t_/, '/');
            // Remove any extension if present
            base = base.replace(/\.(jpe?g|png|gif|webp|webm|mp4)$/i, '');
            // Map filemime to extension
            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;
        }

        // Track the floating media element
        let floatingMedia = null;
        let removeListener = null;

        // Mousemove handler to follow cursor and clamp to viewport
        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;
            }
            // Clamp to max 90vw/90vh
            mediaWidth = Math.min(mediaWidth, viewportWidth * 0.9);
            mediaHeight = Math.min(mediaHeight, viewportHeight * 0.9);

            let newX = event.clientX + 10;
            let newY = event.clientY + 10;
            // Clamp to viewport
            if (newX + mediaWidth > viewportWidth) {
                newX = viewportWidth - mediaWidth - 10;
            }
            if (newY + mediaHeight > viewportHeight) {
                newY = viewportHeight - mediaHeight - 10;
            }
            // Prevent negative positions
            newX = Math.max(newX, 0);
            newY = Math.max(newY, 0);

            // Double-check floatingMedia is still valid before using
            if (!floatingMedia) return;
            floatingMedia.style.left = `${newX}px`;
            floatingMedia.style.top = `${newY}px`;
            floatingMedia.style.maxWidth = '90vw';
            floatingMedia.style.maxHeight = '90vh';
        }

        // Remove the floating media and listeners
        function cleanupFloatingMedia() {
            // Remove event listener first to prevent race condition
            document.removeEventListener('mousemove', onMouseMove);
            if (floatingMedia && floatingMedia.parentNode) {
                floatingMedia.parentNode.removeChild(floatingMedia);
            }
            floatingMedia = null;
            if (removeListener) {
                removeListener();
                removeListener = null;
            }
        }

        // On thumbnail hover
        function onThumbEnter(e) {
            const thumb = e.currentTarget;
            // Find parent <a> with .linkThumb or .imgLink
            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';
            }

            // Remove on mouseleave or scroll
            removeListener = function () {
                thumb.removeEventListener('mouseleave', cleanupFloatingMedia);
                window.removeEventListener('scroll', cleanupFloatingMedia, true);
            };
            thumb.addEventListener('mouseleave', cleanupFloatingMedia);
            window.addEventListener('scroll', cleanupFloatingMedia, true);

            // Create and load the media
            if (filemime.startsWith('image/')) {
                floatingMedia = document.createElement('img');
                setCommonStyles(floatingMedia);
                floatingMedia.style.opacity = '0';
                floatingMedia.style.left = '-9999px';

                document.addEventListener('mousemove', onMouseMove);
                onMouseMove(e);

                floatingMedia.onload = function () {
                    if (!loaded) {
                        loaded = true;
                        floatingMedia.style.opacity = '1';
                        document.body.appendChild(floatingMedia);
                        onMouseMove(e);
                    }
                };
                floatingMedia.onerror = cleanupFloatingMedia;
                floatingMedia.src = fullSrc;
            } else if (filemime.startsWith('video/')) {
                floatingMedia = document.createElement('video');
                setCommonStyles(floatingMedia);
                floatingMedia.style.opacity = '0';
                floatingMedia.style.left = '-9999px';
                floatingMedia.autoplay = true;
                floatingMedia.loop = true;
                floatingMedia.muted = false;
                floatingMedia.playsInline = true;

                document.addEventListener('mousemove', onMouseMove);
                onMouseMove(e);

                floatingMedia.onloadeddata = function () {
                    if (!loaded) {
                        loaded = true;
                        floatingMedia.style.opacity = '1';
                        document.body.appendChild(floatingMedia);
                        onMouseMove(e);
                    }
                };
                floatingMedia.onerror = cleanupFloatingMedia;
                floatingMedia.src = fullSrc;
            }
        }

        // Attach listeners to all current and future thumbnails
        function attachThumbListeners(root) {
            const thumbs = (root || document).querySelectorAll('a.linkThumb > img, a.imgLink > img');
            thumbs.forEach(thumb => {
                // Prevent duplicate listeners
                if (!thumb._fullImgHoverBound) {
                    thumb.addEventListener('mouseenter', onThumbEnter);
                    thumb._fullImgHoverBound = true;
                }
            });
        }

        // Initial attach
        attachThumbListeners();

        // Observe for dynamically added thumbnails
        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 });
    })();
    /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    // Save Name checkbox
    (function () {
        // Create the checkbox element
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.id = 'saveNameCheckbox';
        checkbox.classList.add('postingCheckbox');

        // Create a label for the checkbox
        const label = document.createElement('label');
        label.htmlFor = 'saveNameCheckbox';
        label.textContent = 'Save Name';
        label.title = 'Save Name on refresh';

        // Find the element with the ID #qralwaysUseBypassCheckBox
        const alwaysUseBypassCheckbox = document.getElementById('qralwaysUseBypassCheckBox');
        if (alwaysUseBypassCheckbox) {
            // Append the checkbox first, then the label before the specified element
            alwaysUseBypassCheckbox.parentNode.insertBefore(checkbox, alwaysUseBypassCheckbox);
            alwaysUseBypassCheckbox.parentNode.insertBefore(label, checkbox.nextSibling);

            // Load the checkbox state from localStorage
            const savedCheckboxState = localStorage.getItem('saveNameCheckbox') === 'true';
            checkbox.checked = savedCheckboxState;

            // Find the name input field (adjust selector as needed)
            const nameInput = document.getElementById('qrname');
            if (nameInput) {
                // If the checkbox is checked and a name is saved, populate the input
                const savedName = localStorage.getItem('name');
                if (checkbox.checked && savedName !== null) {
                    nameInput.value = savedName;
                } else if (!checkbox.checked) {
                    // If checkbox is unchecked on page load, clear the name field
                    nameInput.value = '';
                }

                // Listen for changes to the name input and update localStorage if checkbox is checked
                nameInput.addEventListener('input', function () {
                    if (checkbox.checked) {
                        localStorage.setItem('name', nameInput.value);
                    }
                });

                // Event listener for checkbox change
                checkbox.addEventListener('change', function () {
                    if (checkbox.checked) {
                        // Save the current name input value to localStorage
                        localStorage.setItem('name', nameInput.value);
                    } else {
                        // Remove the item "name" from localStorage and clear the input field
                        localStorage.removeItem('name');
                        nameInput.value = '';
                    }
                    // Save the checkbox state in localStorage
                    localStorage.setItem('saveNameCheckbox', checkbox.checked);
                });
            }
        }
    })();

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

    // 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;
}
.innerPost {
margin-left: 40px;
display: block;
}
/* Cleanup */
#actionsForm,
#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);
    }
})();