// ==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);
})();