// ==UserScript== // @name LynxChan Extended // @namespace lynx.ext // @version 1.0.0 // @description Adds new functionality to LynxChan imageboards // @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 // @downloadURL none // ==/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), }; console.log("%cLynx Extended: Started with settings:", "color:rgb(0, 140, 255)", settings); addMyStyle("lynx-extended-css", ` .marker-container { position: fixed; top: 16px; right: 0; width: 10px; height: calc(100vh - 40px); 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); try { createSettingsButton(); } catch (error) { console.log("Error while creating settings button:", error); } //Open the settings menu on the first run if (settings.firstRun) { settings.firstRun = false; await GM.setValue("firstRun", settings.firstRun); openMenu(); } // Create markers 1 second after page load setTimeout(() => { recreateScrollMarkers(); }, 1500); 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 = `

LynxChan Extended Options





(uses the first image of the first visible post on the current thread with the filename "ThreadSpoiler.jpg" (or .png or .webp))


(same as above with the filename "ThreadSpoilerAlt.jpg" (or .png or .webp))



`; 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", ` / `); document.querySelector("#navigation-lynxextended").addEventListener("click", openMenu); } 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); }); } if (settings.showScrollbarMarkers) { // Add a second observer for #threadList (new posts) const threadObserver = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { mutation.addedNodes.forEach((node) => { if (node.classList && node.classList.contains("postCell")) { // Recreate markers because the page grew taller. Is this heavy? probably not. recreateScrollMarkers(); } }); } } }); const threadList = document.querySelector("#threadList"); if (threadList) { threadObserver.observe(threadList, { childList: true }); } } // Add functionality to apply the custom spoiler image CSS if (settings.useThreadSpoilerImage || settings.useAlternativeSpoilerImage) { 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; } if (spoilerImageUrl) { addMyStyle("thread-spoiler-css", ` .uploadCell:not(.expandedCell) a.imgLink:has(>img[src="/spoiler.png"]) { background-image: url("${spoilerImageUrl}"); background-size: cover; outline: dashed 2px #ff0000f5; & > img[src="/spoiler.png"] { opacity: 0; } } `); } } // 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; } } `); } })();