// ==UserScript==
// @name LynxChan Extended Minus Minus
// @namespace https://rentry.org/8chanMinusMinus
// @version 1.61
// @description LynxChan Extended with even more features
// @author SaddestPanda & Dandelion & /gfg/
// @license UNLICENSE
// @match *://8chan.moe/*/res/*
// @match *://8chan.se/*/res/*
// @match *://8chan.cc/*/res/*
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM.registerMenuCommand
// @run-at document-idle
// ==/UserScript==
(async function () {
"use strict";
const SETTINGS_DEFINITIONS = {
firstRun:{
default:true,
hidden:true,
desc:"You shouldn't be able to see this setting! (firstRun)"
},
addKeyboardHandlers:{
default:true,
desc:"Add keyboard Ctrl+ hotkeys to the quick reply box (Disable this for 8chanSS compatibility)"
},
showScrollbarMarkers:{
default:true,
desc:"Show your posts and replies on the scrollbar"
},
spoilerImageType:{
default:"off",
desc:"Override how the spoiler thumbnail looks",
type:"radio",
options:{
off:"Don't change the thumbnail.",
reveal:"Reveal spoilers. <span class='altText'>Previously spoilered images will have a red border around them indicating that they're spoilers.</span>",
reveal_blur:"Change to a blurred thumbnail. Unblurred when you hover your mouse over.",
kachina:"Makes the spoiler image Kachina from Genshin Impact.",
thread:`Use <b style="color: var(--link-color);">"ThreadSpoiler.jpg"</b> from the current thread. <span class="altText">(first posted jpg, png or webp image with that filename)</span>`,
threadAlt:`same as above with the filename <b style="color: var(--link-color);">"ThreadSpoilerAlt.jpg"</b> <span class="altText">(.jpg, .png or .webp)</span>`
}
},
overrideBoardSpoilerImage: {
default:true,
parent:"spoilerImageType",
//Not implemented yet
//depends: function() {return settings.spoilerImageType != "off"},
desc:"Override spoiler thumbnail even if the board has a custom thumbnail set. <span class='altText'>(For example, /v/'s spoiler thumbnail is an image of a monitor with a ? inside it)</span>"
},
revealSpoilerText:{
default:"off",
desc:"Reveal the spoiler text. Or make it into madoka runes.",
type:"radio",
options:{
off:"Don't reveal spoilers.",
on:"Spoilers will be always be shown by turning the text white.",
madoka:`Spoilers will turn into madoka runes. Please install <a href="https://www.dropbox.com/s/n6ys414nviitr9y/MadokaRunes-2.0.ttf"><u>MadokaRunes.ttf</u></a> for it to show up properly.`
}
},
useExtraStylingFixes:{
default:true,
desc:"Apply some styling fixes <span class='altText'>(Mark your posts and replies, smaller thumbnails etc.)</span>"
},
glowFirstPostByID:{
default:true,
desc:"Mark new/unique posters by adding a glow effect to their ID"
},
//I swear this used to be a built in option on 8chan
halfchanGreentexts:{
default:false,
desc:"Make the greentext brighter like 4chan"
},
showPostIndex:{
default:true,
desc:"Show the current index of a post on the thread. <span class='altText'>That is, the topmost post will start at 1 and count up from there.</span>"
},
showStubs:{
default:true,
desc:"Show post stubs when filtering"
},
preserveQuickReply:{
default:false,
desc:"Preserve the quick reply text when closing the box or refreshing the page"
}
/*redirectToCatalog:{
default:false,
desc:"Redirect to catalog when clicking on the index."
}*/
}
const settingsNames = Object.keys(SETTINGS_DEFINITIONS);
const settingsValues = await Promise.all(settingsNames.map(key => GM.getValue(key, SETTINGS_DEFINITIONS[key]['default'])));
const settings = Object.fromEntries(settingsNames.map((key, index) => [key, settingsValues[index]]));
console.log("%cLynx Minus Minus Started with settings:", "color:rgb(0, 140, 255)", settings);
if (typeof api !== "undefined") {
console.log("The script is not sandboxed. Adding quick reply shortcut.")
function quickReplyShortcut(ev) {
if ((ev.ctrlKey && ev.key == "q") || (ev.altKey && ev.key=="r")) {
ev.preventDefault();
//8chan's HTML will keep the text after a reload so attempt to clear it again
if (settings.preserveQuickReply===false) {
document.getElementById("qrbody").value = "";
}
qr.showQr(); document.getElementById('qrbody')?.focus();
};
}
document.addEventListener("keydown",quickReplyShortcut);
} else {
//I think greasemonkey sandboxes the script. I use violentmonkey though
console.log("JS script is sandboxed and can't access page JS... (If you can read this, let me know what browser/extension does this. Or maybe the site just failed to load?)")
}
function addMyStyle(newID, newStyle) {
let myStyle = document.createElement("style");
//myStyle.type = 'text/css';
myStyle.id = newID;
myStyle.textContent = newStyle;
document.querySelector("head").appendChild(myStyle);
}
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;
left: 50%;
transform: TranslateX(-50%);
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: var(--contrast-color);
color: var(--text-color);
border: 1px solid #737373;
border-radius: 4px;
max-height:100%;
overflow-y: auto;
& .altText {
opacity: 0.75;
}
}
/*What the fuck is up with CSS */
/*#lynxExtendedMenu.settings-content {
max-height: 90%;
}*/
#lynxExtendedMenu > .settings-footer {
height:70px;
}
@media screen and (max-width: 1000px) {
#lynxExtendedMenu{
right:0;
width:90%;
/*bottom:15px;*/
}
}
.lynxExtendedButton::before {
content: "\\e0da";
`);
// 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();
}
function replyKeyboardShortcuts(ev) {
if (ev.ctrlKey) {
let combinations = {
"s":["[spoiler]","[/spoiler]"],
"b":["'''","'''"],
"u":["__","__"],
"i":["''","''"],
"d":["[doom]","[/doom]"],
"m":["[moe]","[/moe]"]
}
for (var key in combinations)
{
if (ev.key == key)
{
ev.preventDefault();
console.log("ctrl+"+key+" pressed in textbox")
const textBox = ev.target;
let newText = textBox.value;
const tags = combinations[key]
const selectionStart = textBox.selectionStart
const selectionEnd = textBox.selectionEnd
if (selectionStart == selectionEnd) { //If there is nothing selected, make empty tags and center the cursor between it
document.execCommand("insertText",false, tags[0] + tags[1]);
//Center the cursor between tags
textBox.selectionStart = textBox.selectionEnd = (textBox.selectionEnd - tags[1].length);
} else {
//Insert text and keep undo/redo support (Only replaces highlighted text)
document.execCommand("insertText",false, tags[0] + newText.slice(selectionStart, selectionEnd) + tags[1])
}
return;
}
}
//Ctrl+Enter to send reply
if (ev.key=="Enter") {
document.getElementById("qrbutton")?.click()
}
}
}
if (settings.addKeyboardHandlers) {
document.getElementById("qrbody").addEventListener("keydown", replyKeyboardShortcuts);
//The quick reply field is focused... EXCEPT when you're pressing the button at the bottom. Why??? JUST ALWAYS FOCUS IT
document.getElementById("quick-reply").addEventListener('keydown',function(ev) {
if (ev.key == "Escape") {
document.getElementById("quick-reply").querySelector(".close-btn").click()
}
})
}
//I'm not sure who would ever want this on but I'm making it an option anyways
if (settings.preserveQuickReply===false) {
document.getElementById("quick-reply").querySelector(".close-btn").addEventListener("click", function(ev){
document.getElementById("qrbody").value = "";
});
//This doesn't replace the built in onclick but adds to it so the original onclick will still bring up the qr
document.getElementById("replyButton")?.addEventListener("click", function(ev){
ev.preventDefault();
document.getElementById("qrbody").value = "";
document.getElementById('qrbody')?.focus();
});
}
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: var(--subject-color);" class='settings-header'>LynxChan Extended-- Options</h3><br>`;
//we use createElement() here instead of setting innerHTML so we can attach onclick to elements
//...In the future, at least. There aren't any onclicks added yet.
let settings_content = document.createElement("div");
settings_content.classList.add("settings-content");
Object.keys(SETTINGS_DEFINITIONS).forEach((name) => {
const setting = SETTINGS_DEFINITIONS[name];
if (setting.hidden) {
//pass
}
else if (setting.type == "radio") {
let html = `<span>${setting.desc}</span><br><form id="${name}" action='#'>`
for (const [value, description] of Object.entries(setting.options)) {
html += `
<label>
<input name="${name}" type="radio" value="${value}" ${settings[name]==value ? "checked" : ""}
<span>${description}</span>
</label><br>
`
}
html += "</form><br>"
settings_content.innerHTML += html;
} else {
settings_content.innerHTML += `
<label>
<input type="checkbox" id="${name}" ${settings[name] ? "checked" : ""}>
${setting.desc}
</label><br><br>`
}
})
menu.appendChild(settings_content);
menu.innerHTML += `
<div class='settings-footer'>
<button id="saveSettings">Save</button>
<button id="closeMenu">Close</button>
</div>
`
document.body.appendChild(menu);
// Save button functionality
document.getElementById("saveSettings").addEventListener("click", async () => {
Object.keys(SETTINGS_DEFINITIONS).forEach((name) => {
const setting = SETTINGS_DEFINITIONS[name];
if (!('hidden' in setting)) {
if (setting.type=="radio") {
settings[name] = document.querySelector(`input[name="${name}"]:checked`).value
} else {
settings[name] = document.getElementById(name).checked;
}
}
})
console.log("Saving settings ",settings)
await Promise.all(Object.entries(settings).map(([key, value]) => GM.setValue(key, value)));
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() {
//Desktop
document.querySelector("#navLinkSpan > .settingsButton").insertAdjacentHTML("afterend", `
<span>/</span>
<a id="navigation-lynxextended" class="coloredIcon lynxExtendedButton" title="LynxChan Extended-- Settings"></a>
`);
//Mobile
document.querySelector("#sidebar-menu > ul > li > .settingsButton").parentElement.insertAdjacentHTML("afterend", `
<li>
<a id="navigation-lynxextended-mobile" class="coloredIcon lynxExtendedButton" title="LynxChan Extended-- Settings">Lynx Ex-- Settings</a>
</li>
`);
document.querySelector("#navigation-lynxextended").addEventListener("click", openMenu);
document.querySelector("#navigation-lynxextended-mobile").addEventListener("click", openMenu);
}
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 addPostCount() {
//This function causes a DOMException, I don't know why, just ignore it
const posts = Array.from(document.getElementsByClassName("divPosts")[0].children);
//Why is the insert method called unshift???? This inserts it at the beginning
//(This is also insanely inefficient since we only need to do it once)
posts.unshift(document.querySelector(".innerOP"))
//This goes bottom to top so we stop when we've reached posts
//that have already had a number inserted
for (let i = posts.length-1; i>=0; i--)
{
//We've reached posts where we already added numbers,
// there's no need to keep going.
if (posts[i].querySelector(".postNum")) {
break;
}
const postInfoDiv = posts[i].getElementsByClassName("title")[0]
if (!postInfoDiv) {
console.error("[Lynx--] Failed to find post for div ",posts[i])
continue;
}
const posterNameDiv = postInfoDiv.getElementsByClassName("linkName")[0];
var newNode = document.createElement("span");
newNode.innerText = i+1;
newNode.className="postNum"
if (i < Infinity) //knownBumpLimit
{
newNode.style = "color: rgb(123, 59, 200); font-weight: bold;"
}
else
{
newNode.style = "color: rgb(255, 4, 4); font-weight: bold;"
}
postInfoDiv.insertBefore(newNode, posterNameDiv);
var foo = document.createTextNode("\u00A0");
postInfoDiv.insertBefore(foo, posterNameDiv);
}
}
const indicateCrossLinks = function() {
const crossLinks = document.querySelectorAll(`a.quoteLink:not(.crossThread):not([href*='${api.boardUri}/res/${api.threadId}'])`);
crossLinks.forEach(crossLink => {
crossLink.classList.add("crossThread")
//This is kind of an expensive operation isn't it? Plus checking :not(.crossThread)? Oh well who cares
const hrefTokens = crossLink.href.split("#");
const quoteLinkId = hrefTokens[1];
crossLink.innerHTML = ">>" + quoteLinkId;
});
}
function addDeletedChecks() {
const postLinks = document.querySelectorAll(`a.quoteLink[href*='${api.boardUri}/res/${api.threadId}']`);
//This goes bottom to top so we stop when we've reached a post with a check attached
for (let i = postLinks.length-1; i>=0; i--)
{
//We've reached posts where we already added numbers,
// there's no need to keep going.
if (postLinks[i].hasMouseOverEvent) {
break;
}
var evListener = function(ev) {
if (!document.getElementById(ev.target.href.split("#").pop())) {
ev.target.classList.add("deleted")
//Sadly this doesn't actually work and I don't know why
//postLinks[i].removeEventListener("mouseenter",evListener)
}
}
postLinks[i].addEventListener("mouseenter", evListener);
//Why does js allow this
postLinks[i].hasMouseOverEvent = true;
}
}
addMyStyle("linkHelpers",`
.quoteLink.crossThread::after {
content: " \(Cross-thread\)";
}
.quoteLink.deleted::after {
content: " \(Deleted\)";
}
`)
/*function glowpost() {
// Create a frequency map to track occurrences of each item
const list = document.querySelectorAll(".labelId");
const countMap = Array.from(list).reduce((acc, item) => {
acc[item.style.backgroundColor] = (acc[item.style.backgroundColor] || 0) + 1;
return acc;
}, {});
// Filter the list to keep only items with a count of 1
Array.from(list).filter(item => countMap[item.style.backgroundColor] === 1).forEach((item) => {
item.style.boxShadow = "0 0 15px #26bf47";
item.title = "This is the first post from this ID.";
});
}*/
var idMap = {};
const glowpost = function() {
const list = document.querySelectorAll(".labelId");
list.forEach((poster) => {
if (idMap[poster.style.backgroundColor]===undefined){
idMap[poster.style.backgroundColor] = 1;
poster.style.boxShadow = "0 0 15px #26bf47";
poster.title = "This is the first post from this ID.";
}
});
}
const revealSpoilerImages = function() {
const spoilers = document.querySelectorAll(".imgLink > img:is([src='/spoiler.png'],[src*='/custom.spoiler'])");
spoilers.forEach(spoiler => {
const parent = spoiler.parentElement;
const hrefTokens = parent.href.split("/");
const fileNameTokens = hrefTokens[4].split(".");
const thumbUrl = `/.media/t_${fileNameTokens[0]}`;
spoiler.src = thumbUrl;
spoiler.classList.add('spoiler-thumb');
//spoiler.style.border = "2px dotted red";
});
}
if (settings.spoilerImageType.startsWith("reveal")) {
addMyStyle("sdfsdfsdfsdf",`
img.spoiler-thumb {
transition: 0.2s;
border: 2px dotted red;
${settings.spoilerImageType=="reveal_blur" ? "filter: blur(10px);" : ""}
}
img.spoiler-thumb:hover {
filter: blur(0);
}
`)
}
const executePostModifiers = function() {
indicateCrossLinks();
addDeletedChecks();
if (settings.glowFirstPostByID)
glowpost();
// Recreate markers because the page grew taller. Is this heavy? probably not.
if (settings.showScrollbarMarkers)
recreateScrollMarkers();
if (settings.showPostIndex)
addPostCount();
if (settings.spoilerImageType.startsWith("reveal"))
revealSpoilerImages();
if (settings.spoilerImageType=="thread" || settings.spoilerImageType=="threadAlt")
setThreadSpoiler();
}
// Create markers 1 second after page load
setTimeout(() => {
executePostModifiers();
}, 1500);
const observer = new MutationObserver((mt_callback) => {
mt_callback.forEach(mut => {
if (mut.type=="childList") {
//console.log("MutationObserver!!!");
executePostModifiers();
}
})
})
observer.observe(document.querySelector(".divPosts"), {'childList':true})
// 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;
}
.imgExpanded { max-height:100vh; object-fit:contain }
.uploadCell .imgLink {
margin-right: 1.5em;
}
/* smaller post spacing (not too much) */
.divMessage {
margin: .8em .8em .5em 3em;
}
/* Make your name in your post red */
.youName { color: red; }
.you { --link-color: red; }
/* 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:rgb(0, 102, 255);
padding-left: 1px;
}
}
`);
}
if (settings.halfchanGreentexts) {
addMyStyle("halfchanGreentexts",
`.greenText {
filter: brightness(110%);
}`
);
}
if (settings.showStubs === false) {
addMyStyle("hide-stubs",`
.postCell:has(> span.unhideButton.glowOnHover) {
display: none;
}
`);
}
if (settings.revealSpoilerText=="on") {
addMyStyle("reveal-spoilers",`
span.spoiler { color: white }
`)
} else if (settings.revealSpoilerText=="madoka") {
addMyStyle("reveal-spoilers",`
span.spoiler:not(:hover) {
color: white;
font-family:MadokaRunes!important;
}
`)
}
// Add functionality to apply the custom spoiler image CSS
let threadSpoilerFound = false;
function setThreadSpoiler() {
if (threadSpoilerFound) return;
let spoilerImageUrl = null;
if (settings.spoilerImageType=="thread") {
const spoilerLink = Array.from(document.querySelectorAll('a.originalNameLink[download^="ThreadSpoiler"]')).find(link => /\.(jpg|jpeg|png|webp)$/i.test(link.download));
spoilerImageUrl = spoilerLink ? spoilerLink.href : null;
} else if (settings.spoilerImageType=="threadAlt") {
const altSpoilerLink = Array.from(document.querySelectorAll('a.originalNameLink[download^="ThreadSpoilerAlt"]')).find(link => /\.(jpg|jpeg|png|webp)$/i.test(link.download));
spoilerImageUrl = altSpoilerLink ? altSpoilerLink.href : null;
}
if (spoilerImageUrl) {
addMyStyle("thread-spoiler-css", `
${settings.overrideBoardSpoilerImage ? '.uploadCell:not(.expandedCell) a.imgLink:has(>img[src*="/custom.spoiler"]),' : "" }
.uploadCell:not(.expandedCell) a.imgLink:has(>img[src="/spoiler.png"]) {
background-image: url("${spoilerImageUrl}");
background-size: cover;
outline: dashed 2px #ff0000f5;
& > img {
opacity: 0;
}
}
`);
threadSpoilerFound = true;
}
}
if (settings.spoilerImageType=="thread" || settings.spoilerImageType=="threadAlt") {
setThreadSpoiler();
}
else if (settings.spoilerImageType=="kachina") {
addMyStyle("kachinaSpoilers",`
${settings.overrideBoardSpoilerImage ? '.uploadCell:not(.expandedCell) a.imgLink:has(>img[src*="/custom.spoiler"]),' : "" }
.uploadCell:not(.expandedCell) a.imgLink:has(>img[src="/spoiler.png"]) {
background-size: cover;
margin-right:5px;
background-image: url("data:image/jpeg;base64,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");
& > img {
opacity: 0;
}
}
`)
}
})();