Greasy Fork

8chan Style Script

Script to style 8chan

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

// ==UserScript==
// @name        8chan Style Script
// @namespace   8chanSS
// @match       *://8chan.moe/*
// @match       *://8chan.se/*
// @grant       none
// @version     1.4
// @author      Anon
// @run-at      document-end
// @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);

    /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // Fix for Image Hover
    (function () {
        'use strict';

        // Function to handle mouse movement
        function onMouseMove(event) {
            const img = document.querySelector('img[style*="position: fixed"]');
            if (img) {
                // Get the viewport dimensions
                const viewportWidth = window.innerWidth;
                const viewportHeight = window.innerHeight;

                // Calculate the new position
                let newX = event.clientX + 10; // Offset to avoid cursor overlap
                let newY = event.clientY + 10; // Offset to avoid cursor overlap

                // Ensure the image stays within the viewport
                if (newX + img.width > viewportWidth) {
                    newX = viewportWidth - img.width - 10; // Adjust for right edge
                }
                if (newY + img.height > viewportHeight) {
                    newY = viewportHeight - img.height - 10; // Adjust for bottom edge
                }

                // Update the image position
                img.style.left = `${newX}px`;
                img.style.top = `${newY}px`;
            }
        }

        // Function to handle mouse enter and leave
        function onMouseEnter() {
            document.addEventListener('mousemove', onMouseMove);
        }

        function onMouseLeave() {
            document.removeEventListener('mousemove', onMouseMove);
        }

        // Observe for the image to appear and disappear
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeType === Node.ELEMENT_NODE && node.matches('img[style*="position: fixed"]')) {
                        onMouseEnter();
                    }
                });
                mutation.removedNodes.forEach((node) => {
                    if (node.nodeType === Node.ELEMENT_NODE && node.matches('img[style*="position: fixed"]')) {
                        onMouseLeave();
                    }
                });
            });
        });

        // Start observing the body for changes
        observer.observe(document.body, { childList: true, subtree: true });
    })();

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

    // Add Name Save checkbox to QR
    (function () {
        // Create the checkbox element
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.id = 'saveNameCheckbox';

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

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

            // Event listener for checkbox change
            checkbox.addEventListener('change', function () {
                if (!checkbox.checked) {
                    // If the checkbox is unticked, remove the item "name" from localStorage
                    localStorage.removeItem('name');
                }
                // 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;
}
#qrbody {
resize: vertical;
max-height: 50vh;
}
.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 */
#footer,
#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;
}
/* 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);
    }
})();