LynxChan Extended (8chan)

Use Lynx-- instead, its more complete -> https://greasyfork.org/en/scripts/533169-lynxchan-extended-minus-minus

Install this script?
Author's suggested script

You may also like LynxChan Extended Minus Minus.

Install this script
// ==UserScript==
// @name         LynxChan Extended (8chan)
// @namespace    lynx.ext
// @version      1.0.5
// @description  Use Lynx-- instead, its more complete -> https://greasyfork.org/en/scripts/533169-lynxchan-extended-minus-minus  
// @author       SaddestPanda
// @license      UNLICENSE
// @match        https://8chan.moe/*
// @match        https://8chan.se/*
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.deleteValue
// @grant        GM.registerMenuCommand
// @run-at       document-idle
// ==/UserScript==

(async function () {
    "use strict";

    // Default settings
    const defaultSettings = {
        firstRun: true,
        showScrollbarMarkers: true,
        useThreadSpoilerImage: true,
        useAlternativeSpoilerImage: false,
        useExtraStylingFixes: true,
    };


    const settings = {
        firstRun: await GM.getValue("firstRun", defaultSettings.firstRun),
        showScrollbarMarkers: await GM.getValue("showScrollbarMarkers", defaultSettings.showScrollbarMarkers),
        useThreadSpoilerImage: await GM.getValue("useThreadSpoilerImage", defaultSettings.useThreadSpoilerImage),
        useAlternativeSpoilerImage: await GM.getValue("useAlternativeSpoilerImage", defaultSettings.useAlternativeSpoilerImage),
        useExtraStylingFixes: await GM.getValue("useExtraStylingFixes", defaultSettings.useExtraStylingFixes),
    };

    addMyStyle("lynx-extended-css", `
    .marker-container {
        position: fixed;
        top: 16px;
        right: 0;
        width: 10px;
        height: calc(100vh - 35px);
        z-index: 11000;
        pointer-events: none;
    }

    .marker {
        position: absolute;
        width: 100%;
        height: 6px;
        background: #0092ff;
        cursor: pointer;
        pointer-events: auto;
        border-radius: 40% 0 0 40%;
        z-index: 5;
    }

    .marker.alt {
        background: #a8d8f8;
        z-index: 2;
    }

    #lynxExtendedMenu {
        position: fixed;
        top: 15px;
        right: 100px;
        padding: 10px;
        z-index: 10000;
        font-family: Arial, sans-serif;
        font-size: 14px;
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
        background: #353535;
        border: 1px solid #737373;
        color: #ddd;
        border-radius: 4px;
    }
    `);

    // Register menu command
    GM.registerMenuCommand("Show Options Menu", openMenu);
    waitForElement("#navLinkSpan > .settingsButton", 50, () => {
        try {
            createSettingsButton();
        } catch (error) {
            console.log("Error while creating settings button:", error);
        }
    });

    function openMenu() {
        const oldMenu = document.getElementById("lynxExtendedMenu");
        if (oldMenu) {
            oldMenu.remove();
            return;
        }
        // Create options menu
        const menu = document.createElement("div");
        menu.id = "lynxExtendedMenu";
        menu.innerHTML = `
            <h3 style="text-align: center; color:#6bc9ff;">LynxChan Extended Options</h3><br>
            <label>
                <input type="checkbox" id="showScrollbarMarkers" ${settings.showScrollbarMarkers ? "checked" : ""}>
                Show your posts and replies on the scrollbar
            </label><br><br>
            <label>
                <input type="checkbox" id="useThreadSpoilerImage" ${settings.useThreadSpoilerImage ? "checked" : ""}>
                Use each thread's custom spoiler image
            </label><br>
            (uses the first image of the first visible post on the current thread with the filename <b style="color: #6bc9ff;">"ThreadSpoiler.jpg"</b> (or .png or .webp))<br><br>
            <label>
                <input type="checkbox" id="useAlternativeSpoilerImage" ${settings.useAlternativeSpoilerImage ? "checked" : ""}>
                Use each thread's alternative custom spoiler image
            </label><br>
            (same as above with the filename <b style="color: #6bc9ff;">"ThreadSpoilerAlt.jpg"</b> (or .png or .webp))<br><br>
            <label>
                <input type="checkbox" id="useExtraStylingFixes" ${settings.useExtraStylingFixes ? "checked" : ""}>
                Apply some styling fixes (Mark your posts and replies, smaller thumbnails etc.)
            </label><br><br>
            <button id="saveSettings">Save</button>
            <button id="closeMenu">Close</button>
        `;
        document.body.appendChild(menu);

        // Save button functionality
        document.getElementById("saveSettings").addEventListener("click", async () => {
            settings.showScrollbarMarkers = document.getElementById("showScrollbarMarkers").checked;
            settings.useThreadSpoilerImage = document.getElementById("useThreadSpoilerImage").checked;
            settings.useAlternativeSpoilerImage = document.getElementById("useAlternativeSpoilerImage").checked;
            settings.useExtraStylingFixes = document.getElementById("useExtraStylingFixes").checked;

            await GM.setValue("showScrollbarMarkers", settings.showScrollbarMarkers);
            await GM.setValue("useThreadSpoilerImage", settings.useThreadSpoilerImage);
            await GM.setValue("useAlternativeSpoilerImage", settings.useAlternativeSpoilerImage);
            await GM.setValue("useExtraStylingFixes", settings.useExtraStylingFixes);

            alert("Settings saved!\nRefresh the page for the changes to take effect.");
            menu.remove();
        });

        // Close button functionality
        document.getElementById("closeMenu").addEventListener("click", () => {
            menu.remove();
        });
    }

    function createSettingsButton() {
        document.querySelector("#navLinkSpan > .settingsButton").insertAdjacentHTML("afterend", `
        <span>/</span>
        <a id="navigation-lynxextended" class="lynxExtendedSettings" title="LynxChan Extended Settings"
            style="width: 13px;height: 13px;display: inline-block;fill: #2eb1ff;vertical-align: middle;margin-left: 1px;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
                <path
                    d="M352 320c88.4 0 160-71.6 160-160c0-15.3-2.2-30.1-6.2-44.2c-3.1-10.8-16.4-13.2-24.3-5.3l-76.8 76.8c-3 3-7.1 4.7-11.3 4.7L336 192c-8.8 0-16-7.2-16-16l0-57.4c0-4.2 1.7-8.3 4.7-11.3l76.8-76.8c7.9-7.9 5.4-21.2-5.3-24.3C382.1 2.2 367.3 0 352 0C263.6 0 192 71.6 192 160c0 19.1 3.4 37.5 9.5 54.5L19.9 396.1C7.2 408.8 0 426.1 0 444.1C0 481.6 30.4 512 67.9 512c18 0 35.3-7.2 48-19.9L297.5 310.5c17 6.2 35.4 9.5 54.5 9.5zM80 408a24 24 0 1 1 0 48 24 24 0 1 1 0-48z">
                </path>
            </svg>
        </a>
        `);
        document.querySelector("#navigation-lynxextended").addEventListener("click", openMenu);
    }

    /**
     * Waits for an element identified by a selector to appear in the DOM and executes the callback.
     *
     * @param {string} selector - The CSS selector of the element to wait for.
     * @param {number} delay - The time in milliseconds to wait before checking for the element again.
     * @param {function} callback - The callback function to execute once the element is found.
     */
    function waitForElement(selector, delay, callback) {
        if (document.querySelector(selector)) {
            callback();
        } else {
            setTimeout(() => waitForElement(selector, delay, callback), delay);
        }
    }

    function addMyStyle(newID, newStyle) {
        let myStyle = document.createElement("style");
        //myStyle.type = 'text/css';
        myStyle.id = newID;
        myStyle.textContent = newStyle;
        document.querySelector("head").appendChild(myStyle);
    }

    function createMarker(element, container, isReply) {
        const pageHeight = document.body.scrollHeight;
        const offsetTop = element.offsetTop;
        const percent = offsetTop / pageHeight;

        const marker = document.createElement("div");
        marker.classList.add("marker");
        if (isReply) {
            marker.classList.add("alt");
        }
        marker.style.top = `${percent * 100}%`;
        marker.dataset.postid = element.id;

        marker.addEventListener("click", () => {
            let elem = element?.previousElementSibling || element;
            elem.scrollIntoView({ behavior: "smooth", block: "start" });
        });

        container.appendChild(marker);
    }

    function recreateScrollMarkers() {
        let oldContainer = document.querySelector(".marker-container");
        if (oldContainer) {
            oldContainer.remove();
        }
        // Create marker container
        const markerContainer = document.createElement("div");
        if (settings.showScrollbarMarkers) {
            markerContainer.classList.add("marker-container");
            document.body.appendChild(markerContainer);
        }

        // Match and create markers for "my posts" (matches native & dollchan)
        document.querySelectorAll(".postCell:has(.innerPost > .postInfo.title :is(.linkName.youName, .de-post-counter-you)),.postCell:has(.innerPost.de-mypost)")
            .forEach((elem) => {
                createMarker(elem, markerContainer, false);
            });

        // Match and create markers for "replies" (matches native & dollchan)
        document.querySelectorAll(".postCell:has(.innerPost > .divMessage :is(.quoteLink.you, .de-ref-you)),.postCell:has(.innerPost.de-mypost-reply)")
            .forEach((elem) => {
                createMarker(elem, markerContainer, true);
            });
    }

    // Function to fetch the thread spoiler image URL
    function getSpoilerUrl() {
        let spoilerImageUrl = null;

        if (settings.useThreadSpoilerImage) {
            const spoilerLink = Array.from(document.querySelectorAll('a.originalNameLink[download^="ThreadSpoiler"]')).find(link => /\.(jpg|png|webp)$/i.test(link.download));
            spoilerImageUrl = spoilerLink ? spoilerLink.href : null;
        }

        if (settings.useAlternativeSpoilerImage) {
            const altSpoilerLink = Array.from(document.querySelectorAll('a.originalNameLink[download^="ThreadSpoilerAlt"]')).find(link => /\.(jpg|png|webp)$/i.test(link.download));
            spoilerImageUrl = altSpoilerLink ? altSpoilerLink.href : null;
        }

        return spoilerImageUrl;
    }

    // Function to apply the thread spoiler image CSS
    function applySpoilerCss(spoilerImageUrl) {
        addMyStyle("thread-spoiler-css", `
            .uploadCell:not(.expandedCell) a.imgLink:has(>img[src="/spoiler.png"]),
            .uploadCell:not(.expandedCell) a.imgLink:has(>img[src*="/custom.spoiler"]) {
                background-image: url("${spoilerImageUrl}");
                background-size: cover;
                outline: dashed 2px #ff0000f5;
                & > img {
                    opacity: 0;
                }
            }
        `);
    }

    async function start() {
        console.log("%cLynx Extended: Started with settings:", "color:rgb(0, 140, 255)", settings);

        //Open the settings menu on the first run
        if (settings.firstRun) {
            settings.firstRun = false;
            await GM.setValue("firstRun", settings.firstRun);
            openMenu();
        }

        if (settings.showScrollbarMarkers) {
            // Create markers 1 second after page load
            setTimeout(() => {
                recreateScrollMarkers();
            }, 1000);
            //TODO LATER: why was mutation observer not working?
            let postCount = document.querySelectorAll("#threadList .postCell")?.length || 0;
            let interval = setInterval(() => {
                let newPostCount = document.querySelectorAll("#threadList .postCell")?.length || 0;
                if (newPostCount !== postCount) {
                    postCount = newPostCount;
                    recreateScrollMarkers();
                }
            }, 500);
            
        }

        // Add functionality to apply the custom spoiler image CSS
        if (settings.useThreadSpoilerImage || settings.useAlternativeSpoilerImage) {
            let spoilerImageUrl = getSpoilerUrl();

            if (spoilerImageUrl) {
                applySpoilerCss(spoilerImageUrl);
            } else {
                // Re-check every second if the spoiler image URL is blank
                const spoilerCheckInterval = setInterval(() => {
                    spoilerImageUrl = getSpoilerUrl();
                    if (spoilerImageUrl) {
                        clearInterval(spoilerCheckInterval);
                        applySpoilerCss(spoilerImageUrl);
                    }
                }, 1000);
            }
        }

        // Apply the CSS if the setting is enabled
        if (settings.useExtraStylingFixes) {
            addMyStyle("extra-styling-css", `
            /* smaller thumbnails & image paddings */
            body .uploadCell img:not(.imgExpanded) {
                max-width: 160px;
                max-height: 125px;
                object-fit: contain;
                height: auto;
                width: auto;
                margin-right: 0em;
                margin-bottom: 0em;
            }

            .uploadCell .imgLink {
                margin-right: 1.5em;
            }

            /* smaller post spacing (not too much) */
            .divMessage {
                margin: .8em .8em .5em 3em;
            }

            .greenText {
                filter: brightness(110%);
            }

            /* mark your posts and replies (same selectors are also used for detection above) */
            .postCell:has(.innerPost > .postInfo.title :is(.linkName.youName, .de-post-counter-you)),
            .postCell:has(.innerPost.de-mypost) {
                & > .innerPost {
                    border-left: 3px dashed;
                    border-left-color: #4BB2FFC2;
                    padding-left: 0px;
                }
            }

            .postCell:has(.innerPost > .divMessage :is(.quoteLink.you, .de-ref-you)),
            .postCell:has(.innerPost.de-mypost-reply) {
                & > .innerPost {
                    border-left: 2px solid;
                    border-left-color: #a8d8f8b0;
                    padding-left: 1px;
                }
            }
        `);
        }
    }

    // Wait for #threadList and then start
    waitForElement("#threadList", 50, start);

})();