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/*
// @match       *://8chan.se/*
// @grant       none
// @version     1.3
// @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
const excludedPages = [
    '*/catalog.html', // Add any other pages you want to exclude
];

// Function to save the scroll position for the current page
function saveScrollPosition() {
    // Check if the current page is in the excluded pages list
    if (excludedPages.includes(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);
}
})();