4chan - Vanilla+

This script enhances your 4chan browsing experience by removing redirect URLs from links (when the "Linkify URLs" option is enabled), automatically loading all media in a thread upon entry, and seamlessly displaying Catbox-hosted videos and images directly within the thread.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         4chan - Vanilla+
// @namespace    http://tampermonkey.net/
// @version      2.42
// @description  This script enhances your 4chan browsing experience by removing redirect URLs from links (when the "Linkify URLs" option is enabled), automatically loading all media in a thread upon entry, and seamlessly displaying Catbox-hosted videos and images directly within the thread.
// @author       Airman
// @match        https://*.4chan.org/*
// @grant        none
// @run-at       document-end
// @license GNU GPLv3
// ==/UserScript==

(function() {
    'use strict';

    function bypassRedirects() {
        // Select all links on the page
        let links = document.querySelectorAll('a');
        // Loop through each link
        links.forEach(link => {
            // Check if the link URL matches the 4chan redirect URL
            if (link.href.startsWith('https://sys.4chan.org/derefer?url=')) {
                // Extract the target URL from the redirect URL by removing the prefix
                let targetUrl = link.href.replace('https://sys.4chan.org/derefer?url=', '');
                // Decode the target URL to get the actual URL
                targetUrl = decodeURIComponent(targetUrl);
                // If the target URL is found, update the link href to it
                if (targetUrl) {
                    link.href = targetUrl;
                    console.log(`Updated link: ${link.href}`);
                }
            }
        });
    }

    function renderCatboxMedia() {
        // Render all Catbox images and videos inline
        let catboxLinks = document.querySelectorAll('a[href*="catbox.moe"]');
        const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
        const videoExtensions = ['.mp4', '.webm'];

        catboxLinks.forEach(link => {
            let mediaUrl = link.href;

            // Check if the media has already been rendered for this link
            if (link.classList.contains('media-rendered')) {
                return;
            }

            // Mark the link as processed to avoid duplicate rendering
            link.classList.add('media-rendered');

            // Check if the URL is an image file
            if (imageExtensions.some(ext => mediaUrl.endsWith(ext))) {
                let thumbImg = new Image();
                thumbImg.src = mediaUrl;
                thumbImg.style.maxWidth = '150px';
                thumbImg.style.display = 'block';
                thumbImg.style.marginTop = '10px';
                thumbImg.style.cursor = 'pointer';
                thumbImg.classList.add('rendered-media');

                let fullImg = new Image();
                fullImg.src = mediaUrl;
                fullImg.style.maxWidth = '100%';
                fullImg.style.display = 'none';
                fullImg.style.marginTop = '10px';
                fullImg.style.cursor = 'pointer';
                fullImg.classList.add('rendered-media');

                thumbImg.addEventListener('click', () => {
                    thumbImg.style.display = 'none';
                    fullImg.style.display = 'block';
                });

                fullImg.addEventListener('click', () => {
                    fullImg.style.display = 'none';
                    thumbImg.style.display = 'block';
                });

                // Insert the thumbnail image after the link
                link.parentNode.insertBefore(thumbImg, link.nextSibling);
                link.parentNode.insertBefore(fullImg, thumbImg.nextSibling);
                console.log(`Rendered Catbox image: ${mediaUrl}`);

            } else if (videoExtensions.some(ext => mediaUrl.endsWith(ext))) {
                // Fetch the video thumbnail
                fetchVideoThumbnail(mediaUrl).then(thumbnailUrl => {
                    let thumbDiv = document.createElement('div');
                    let fullVideo = document.createElement('video');
                    let closeWrapper = document.createElement('span');
                    let closeLink = document.createElement('a');

                    closeWrapper.textContent = ' - [';
                    closeLink.textContent = 'Close';
                    closeLink.style.cursor = 'pointer';
                    closeLink.style.textDecoration = 'underline';
                    closeWrapper.style.display = 'none';

                    closeWrapper.appendChild(closeLink);
                    closeWrapper.appendChild(document.createTextNode(']'));

                    fullVideo.src = mediaUrl;
                    fullVideo.controls = true;
                    fullVideo.style.maxWidth = '100%';
                    fullVideo.style.display = 'none';
                    fullVideo.style.marginTop = '10px';
                    fullVideo.classList.add('rendered-media');

                    if (thumbnailUrl) {
                        thumbDiv.style.backgroundImage = `url(${thumbnailUrl})`;
                    } else {
                        thumbDiv.textContent = 'Click to view video';
                    }

                    thumbDiv.style.width = '150px';
                    thumbDiv.style.height = '150px';
                    thumbDiv.style.display = 'flex';
                    thumbDiv.style.alignItems = 'center';
                    thumbDiv.style.justifyContent = 'center';
                    thumbDiv.style.marginTop = '10px';
                    thumbDiv.style.cursor = 'pointer';
                    thumbDiv.style.backgroundSize = 'cover';
                    thumbDiv.style.backgroundColor = '#000';
                    thumbDiv.style.color = '#fff';
                    thumbDiv.classList.add('rendered-media');

                    thumbDiv.addEventListener('click', () => {
                         fullVideo.play();
                        thumbDiv.style.display = 'none';
                        fullVideo.style.display = 'block';
                        closeWrapper.style.display = 'inline';
                    });

                    closeLink.addEventListener('click', () => {
                        fullVideo.pause();
                        fullVideo.style.display = 'none';
                        thumbDiv.style.display = 'flex';
                        closeWrapper.style.display = 'none';
                    });

                    // Insert the elements after the link
                    link.parentNode.insertBefore(closeWrapper, link.nextSibling);
                    link.parentNode.insertBefore(thumbDiv, closeWrapper.nextSibling);
                    link.parentNode.insertBefore(fullVideo, thumbDiv.nextSibling);
                    console.log(`Rendered Catbox video: ${mediaUrl}`);
                });
            }
        });
    }

    function fetchVideoThumbnail(videoUrl) {
        return new Promise((resolve, reject) => {
            let video = document.createElement('video');
            video.src = videoUrl;
            video.crossOrigin = 'anonymous';
            video.addEventListener('loadeddata', () => {
                video.currentTime = 1; // Set the time to 1 second to get a frame
            });

            video.addEventListener('seeked', () => {
                let canvas = document.createElement('canvas');
                canvas.width = video.videoWidth;
                canvas.height = video.videoHeight;
                let ctx = canvas.getContext('2d');
                ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
                resolve(canvas.toDataURL('image/jpeg'));
            });

            video.addEventListener('error', () => {
                reject('Error loading video thumbnail');
            });
        });
    }

    function preloadImages() {
        // Preload thread images (thumbnails and full-sized images)
        let thumbImages = document.querySelectorAll('.fileThumb img');
        thumbImages.forEach(imgElement => {
            if (imgElement.classList.contains('media-rendered')) {
                return;
            }

            imgElement.classList.add('media-rendered');
            let thumbUrl = imgElement.src;
            let fullUrl = imgElement.parentNode.href;

            if (thumbUrl) {
                let thumbImg = new Image();
                thumbImg.src = thumbUrl;
                console.log(`Preloading thumbnail image: ${thumbUrl}`);
            }
            if (fullUrl) {
                let fullImg = new Image();
                fullImg.src = fullUrl;
                console.log(`Preloading full-sized image: ${fullUrl}`);
            }
        });
    }

    // Run the functions initially to bypass redirects and render images and videos inline on the existing page content
    bypassRedirects();
    renderCatboxMedia();
    preloadImages();

    // Intercept Fetch API requests
    (function() {
        const originalFetch = window.fetch;
        window.fetch = function() {
            return originalFetch.apply(this, arguments).then(response => {
                // Check if the request URL matches the pattern for thread updates
                if (response.url.includes('.json')) {
                    response.clone().json().then(data => {
                        console.log('Update request completed:', response.url);
                        // Run the bypass and render functions after the update request completes
                        setTimeout(function() {
                            console.log('Now running functions after the update:');
                            bypassRedirects();
                            renderCatboxMedia();
                            preloadImages();
                        }, 1000);
                    });
                }
                return response;
            });
        };
    })();

    // Intercept XMLHttpRequest requests
    (function() {
        const originalOpen = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function(method, url) {
            this.addEventListener('load', function() {
                // Check if the request URL matches the pattern for thread updates
                if (url.includes('.json')) {
                    console.log('Update request completed:', url);
                    // Run the bypass and render functions after the update request completes
                    setTimeout(function() {
                        console.log('Now running functions after the update:');
                        bypassRedirects();
                        renderCatboxMedia();
                        preloadImages();
                    }, 1000);
                }
            });
            originalOpen.apply(this, arguments);
        };
    })();
})();