// ==UserScript== // @name LynxChan Extended Minus Minus // @namespace https://rentry.org/8chanMinusMinus // @version 2.1.0 // @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-start // @downloadURL none // ==/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, type:"checkbox_with_colors", desc:"Show your posts and replies on the scrollbar", color1Default:"#0092ff", color1Desc:"Your marker:", color2Default:"#a8d8f8", color2Desc:"Reply marker:" }, spoilerImageType:{ default:"off", desc:"Override how the spoiler thumbnail looks:", type:"radio", options:{ off:"Don't change the thumbnail.", reveal:"Reveal spoilers (Previously spoilered images will have a red border around them indicating that they're spoilers.)", 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 "ThreadSpoiler.jpg" from the current thread (first posted jpg, png or webp image with that filename)`, threadAlt:`same as above with the filename "ThreadSpoilerAlt.jpg" (jpg, png or webp; uses ThreadSpoiler.jpg until this is found)`, //test:`[TEST OPTION] Randomly pick spoiler image from /gacha/ board (This is a test option. It selects the spoiler from var(--spoiler-img) after setting.)` }, nonewline:true }, overrideBoardSpoilerImage: { default:true, parent:"spoilerImageType", //Not implemented yet //depends: function() {return settings.spoilerImageType != "off"}, desc:"Also override board's custom thumbnail image (for example, /v/'s spoiler thumbnail is an image of a monitor with a ? inside it)" }, 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 MadokaRunes.ttf for it to show up properly.` } }, markPostEdge:{ default:true, type:"checkbox_with_colors", desc:"Style: Mark your posts and replies (with a left border)", color1Default:"#4BB2FF", color1Desc:"Your border:", color2Default:"#0066ff", color2Desc:"Reply border:", nonewline:true }, markYouText:{ default:true, type:"checkbox_with_colors", desc:"Style: Color your name and (You) links", color1Default:"#ff2222", color1Desc:"Color:", nonewline:true }, compactPosts:{ default:true, desc:"Style: Make thumbnails and posts more compact", nonewline:true }, showStubs:{ default:true, desc:"Style: Show post stubs when filtering", nonewline:true }, //I swear this used to be a built in option on 8chan halfchanGreentexts:{ default:false, desc:"Style: Make the greentext brighter like 4chan" }, glowFirstPostByID:{ default:true, type:"checkbox_with_colors", desc:"Mark new/unique posters by adding a glow effect to their ID", color1Default:"#26bf47", color1Desc:"Glow color:" }, showPostIndex:{ default:true, type:"checkbox_with_colors", desc:"Show the current index of a post on the thread. (OP: 1, first post: 2 etc.)", color1Default:"#7b3bcc", color1Desc:"Index color:" }, 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); //Collect all color fields for checkbox_with_colors settings //In the userscript storage they look like settingName_color1 etc. const colorSettingKeys = []; settingsNames.forEach(key => { const def = SETTINGS_DEFINITIONS[key]; if (def.type === "checkbox_with_colors") { Object.keys(def).forEach(k => { const match = k.match(/^color(\d+)Default$/); if (match) { colorSettingKeys.push(`${key}_color${match[1]}`); } }); } }); //Compose all keys to load: main settings + color fields const allSettingKeys = [...settingsNames, ...colorSettingKeys]; //For each color field, get its default from the definition function getDefaultForKey(key) { const colorMatch = key.match(/^(.+)_color(\d+)$/); if (colorMatch) { const [_, base, idx] = colorMatch; const def = SETTINGS_DEFINITIONS[base]; //Return color setting default like color1Default return def && def[`color${idx}Default`] ? def[`color${idx}Default`] : undefined; } //Return regular setting return SETTINGS_DEFINITIONS[key]?.default; } const allSettingDefaults = allSettingKeys.map(getDefaultForKey); const allSettingValues = await Promise.all(allSettingKeys.map((key, i) => GM.getValue(key, allSettingDefaults[i]))); const settings = Object.fromEntries(allSettingKeys.map((key, i) => [key, allSettingValues[i]])); function addMyStyle(newID, newStyle) { let myStyle = document.createElement("style"); //myStyle.type = 'text/css'; myStyle.id = newID; myStyle.textContent = newStyle; document.head.appendChild(myStyle); } function waitForDom(callback) { if (document.readyState === "loading") { //Loading hasn't finished yet. Wait for the inital document to load and start. document.addEventListener("DOMContentLoaded", callback); } else { //Document has already loaded. Start. callback(); } } if (document?.head) { runASAP(); } else { //On some environments document.head doesn't exist yet? waitForDom(runASAP); } async function runASAP() { // Migrations can be removed in a few weeks // Migrate old useExtraStylingFixes setting if present const oldStyling = await GM.getValue("useExtraStylingFixes", undefined); if (typeof oldStyling !== "undefined") { // If oldStyling is false, set both new options to false if (oldStyling === false) { settings.markPostEdge = false; settings.compactPosts = false; await GM.setValue("markPostEdge", false); await GM.setValue("compactPosts", false); } // Remove the old setting await GM.deleteValue("useExtraStylingFixes"); } // Migrate old markYourPosts setting if present const oldMarkYourPosts = await GM.getValue("markYourPosts", undefined); if (typeof oldMarkYourPosts !== "undefined") { settings.markPostEdge = oldMarkYourPosts; settings.markYouText = oldMarkYourPosts; await GM.setValue("markPostEdge", oldMarkYourPosts); await GM.setValue("markYouText", oldMarkYourPosts); await GM.deleteValue("markYourPosts"); } //Secret tip for anyone manually editing colors: //if you edit the saved value in your userscript manager's settings database manually, you can use semi-transparent colors for the color pickers (until you click save on the settings menu). //or easier: just copy the relevant part of the css and paste it to the css box in the website settings. Add !important if you want to force it like: color: red !important; //Apply all the styles as soon as possible if (settings.compactPosts) { addMyStyle("lynx-compact-posts", ` /* 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; } `); } const markerColor1 = settings.showScrollbarMarkers_color1 || SETTINGS_DEFINITIONS.showScrollbarMarkers.color1Default; const markerColor2 = settings.showScrollbarMarkers_color2 || SETTINGS_DEFINITIONS.showScrollbarMarkers.color2Default; const indexColor = settings.showPostIndex_color1 || SETTINGS_DEFINITIONS.showPostIndex.color1Default; const glowColor = settings.glowFirstPostByID_color1 || SETTINGS_DEFINITIONS.glowFirstPostByID.color1Default; addMyStyle("lynx-extended-css", ` :root { --showScrollbarMarkers_color1: ${markerColor1}; --showScrollbarMarkers_color2: ${markerColor2}; --showPostIndex_color1: ${indexColor}; --glowFirstPostByID_color1: ${glowColor}; } .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: var(--showScrollbarMarkers_color1); cursor: pointer; pointer-events: auto; border-radius: 40% 0 0 40%; z-index: 5; } .marker.alt { background: var(--showScrollbarMarkers_color2); z-index: 2; } .postNum.index { color: var(--showPostIndex_color1); font-weight: bold; } .labelId.glows { box-shadow: 0 0 15px var(--glowFirstPostByID_color1); } #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.8; font-size: 0.9em; &.lineBefore:before { content: "—— "; } } & .boldText { color: var(--link-color); font-weight: bold; } & input[type="color"] { width: 40px; height: 20px; padding: 1px; transform: translate(0, 2px); } & button { padding: 10px 20px; margin-right: 4px; filter: contrast(115%) brightness(110%); &:hover { filter: brightness(130%); } } } /*What the fuck is up with CSS */ /*#lynxExtendedMenu.settings-content { max-height: 90%; }*/ #lynxExtendedMenu > .settings-footer { height: auto; } @media screen and (max-width: 1000px) { #lynxExtendedMenu{ right:0; width:90%; /*bottom:15px;*/ } } .lynxExtendedButton::before { content: "\\e0da"; `); if (settings.markPostEdge) { const color1 = settings.markPostEdge_color1 || SETTINGS_DEFINITIONS.markPostEdge.color1Default; const color2 = settings.markPostEdge_color2 || SETTINGS_DEFINITIONS.markPostEdge.color2Default; addMyStyle("lynx-mark-posts", ` /* mark your posts and replies */ #divThreads .postCell .innerPost:has(> .postInfo.title > .youName) { border-left: 3px dashed var(--markPostEdge_color1, ${color1}); padding-left: 1px; } #divThreads .postCell .innerPost:has(> .divMessage .quoteLink.you) { border-left: 2px solid var(--markPostEdge_color2, ${color2}); padding-left: 1px; } `); } if (settings.markYouText) { const color1 = settings.markYouText_color1 || SETTINGS_DEFINITIONS.markYouText.color1Default; addMyStyle("lynx-mark-you-text", ` .youName { color: var(--markYouText_color1, ${color1}); } .you { --link-color: var(--markYouText_color1, ${color1}); } `); } if (settings.halfchanGreentexts) { addMyStyle("lynx-halfchanGreentexts", `.greenText { filter: brightness(110%); } `); } if (settings.showStubs === false) { addMyStyle("lynx-hide-stubs",` .postCell:has(> span.unhideButton.glowOnHover) { display: none; } `); } if (settings.revealSpoilerText=="on") { addMyStyle("lynx-reveal-spoilertext1",` span.spoiler { color: white } `); } else if (settings.revealSpoilerText=="madoka") { addMyStyle("lynx-reveal-spoilertext2",` span.spoiler:not(:hover) { color: white; font-family: MadokaRunes !important; } `); } } //End of runASAP() //Everything in runAfterDom runs after document has loaded (like @run-at document-end) //Everything in runAfterDom runs after document has loaded (like @run-at document-end) //Everything in runAfterDom runs after document has loaded (like @run-at document-end) async function runAfterDom() { 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 createSettingsButton() { //Desktop document.querySelector("#navLinkSpan > .settingsButton").insertAdjacentHTML("afterend", ` / `); //Mobile document.querySelector("#sidebar-menu > ul > li > .settingsButton").parentElement.insertAdjacentHTML("afterend", `
  • Lynx Ex-- Settings
  • `); document.querySelector("#navigation-lynxextended").addEventListener("click", openMenu); document.querySelector("#navigation-lynxextended-mobile").addEventListener("click", openMenu); } //Register menu command for the settings button 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); 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(); const qrBody = document.getElementById("qrbody"); if (qrBody) { qrBody.value = ""; 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 = `

    LynxChan Extended-- Options


    `; //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 = `${setting.desc}
    `; for (const [value, description] of Object.entries(setting.options)) { html += `
    `; } html += `
    ${setting.nonewline ? '' : '
    '}`; settings_content.innerHTML += html; } else if (setting.type == "checkbox_with_colors") { let colorHtml = ""; let colorFields = Object.keys(setting).filter(k => /^color\d+Default$/.test(k)); colorFields.forEach((colorKey) => { const idx = colorKey.match(/^color(\d+)Default$/)[1]; const colorValue = settings[`${name}_color${idx}`] || setting[`color${idx}Default`]; const colorDesc = setting[`color${idx}Desc`] || ""; colorHtml += ` `; }); settings_content.innerHTML += ` ${colorHtml}
    ${setting.nonewline ? '' : '
    '}`; } else { settings_content.innerHTML += `
    ${setting.nonewline ? '' : '
    '}`; } }) menu.appendChild(settings_content); menu.innerHTML += ` `; 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] = menu.querySelector(`input[name="${name}"]:checked`).value } else if (setting.type=="checkbox_with_colors") { settings[name] = document.getElementById(name).checked; let colorFields = Object.keys(setting).filter(k => /^color\d+Default$/.test(k)); colorFields.forEach((colorKey) => { const idx = colorKey.match(/^color(\d+)Default$/)[1]; const colorName = `${name}_color${idx}`; const colorValue = document.getElementById(colorName).value; settings[colorName] = colorValue; // Set CSS variable on body (so it can be used without a refresh) document.body.style.setProperty(`--${colorName}`, colorValue); }); } 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))); setTimeout(()=>{ alert("Settings saved!\nFor most settings you must refresh the page for the changes to take effect.\n\n(only color pickers don't need a refresh)"); }, 1); // menu.remove(); }); // Reset button functionality document.getElementById("resetSettings").addEventListener("click", async () => { if (!confirm("Are you sure you want to reset all settings? This will delete all saved data.")) return; const keys = await GM.listValues(); await Promise.all(keys.map(key => GM.deleteValue(key))); alert("All settings have been reset.\nRefreshing automatically for the changes to take effect."); menu.remove(); location.reload(); }); // Close button functionality document.getElementById("closeMenu").addEventListener("click", () => { menu.remove(); }); } 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; if (elem) 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"); 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); }); } let postCount = 1; const postIndexLookup = {}; function addPostCount(post, newpost = true) { // const posts = Array.from(document.querySelectorAll(".innerOP, .divPosts > .postCell")); if (post.querySelector(".postNum")) { return; } const postInfoDiv = post.getElementsByClassName("title")[0] if (!postInfoDiv) { console.error("[Lynx--] Failed to find post for div ", post); return; } const posterNameDiv = postInfoDiv.getElementsByClassName("linkName")[0]; const postNumber = post.querySelector(".linkQuote")?.textContent; if (!postNumber) return; let localCount = postCount; if (newpost) { postIndexLookup[postNumber] = localCount; postCount++; } else { //Show cached post count for inlines & hovers localCount = postIndexLookup[postNumber]; if (!localCount) return; } let newNode = document.createElement("span"); newNode.innerText = localCount; newNode.className = "postNum index"; if (localCount < Infinity) //knownBumpLimit { // color is handled by .postNum.index newNode.style = ""; } else { newNode.style = "color: rgb(255, 4, 4); font-weight: bold;" } postInfoDiv.insertBefore(newNode, posterNameDiv); let foo = document.createTextNode("\u00A0"); // Non-breaking space postInfoDiv.insertBefore(foo, posterNameDiv); } //mark cross-thread links. const indicateCrossLinks = function(post) { const crossLinks = post.querySelectorAll(`a.quoteLink:not(.crossThread):not([href*='${api.boardUri}/res/${api.threadId}'])`); crossLinks.forEach(crossLink => { //ignore cross-board links (they look obvious like >>>/board/123456 ) if (!crossLink.href.includes(`/${api.boardUri}/`)) { return; } crossLink.classList.add("crossThread"); const hrefTokens = crossLink.href.split("#"); const quoteLinkId = hrefTokens[1]; crossLink.innerHTML = ">>" + quoteLinkId; }); } function addDeletedChecks(post) { const postLinks = post.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 (S.Panda: postlinks[i] is gone by the time the event is ran) //postLinks[i].removeEventListener("mouseenter",evListener) ev.target.closest("a.quoteLink")?.removeEventListener("mouseenter", evListener); } } postLinks[i].addEventListener("mouseenter", evListener); //Why does js allow this postLinks[i].hasMouseOverEvent = true; } } addMyStyle("lynx-linkHelpers",` .quoteLink.crossThread::after { content: " \(Cross-thread\)"; } .quoteLink.deleted::after { content: " \(Deleted\)"; } `) function imageSearchHooks(post) { //You ever think about how we're iterating over every single post every single time for all these different functions instead of just looping once? //S.Panda: yeah, thankfully no more. const fileNameElements = Array.from(post.querySelectorAll(".originalNameLink[href]")); const regex = /(\d+)_p\d+/; for (let i = fileNameElements.length-1; i>=0; i--) { const parent = fileNameElements[i].parentElement if (parent.querySelector(".reverseImageSearchDetails")) { return; } let m; if ((m = regex.exec(fileNameElements[i].innerText)) !== null) { parent.insertAdjacentHTML("beforeend", `pixiv`) } } } /*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(post, newpost = true) { const list = post.querySelectorAll(".labelId"); const postNumber = post.querySelector(".linkQuote")?.textContent; list.forEach((poster) => { const bgColor = poster.style.backgroundColor; if (newpost && idMap[bgColor] === undefined) { idMap[bgColor] = postNumber; poster.classList.add("glows"); poster.title = "This is the first post from this ID."; } else if (!newpost && idMap[bgColor] == postNumber) { poster.classList.add("glows"); poster.title = "This is the first post from this ID."; } }); } const revealSpoilerImages = function(post) { const spoilers = post.querySelectorAll(".imgLink > img:is([src='/spoiler.png'],[src*='/custom.spoiler'])"); spoilers.forEach(spoiler => { spoiler.classList.add('spoiler-thumb'); const parent = spoiler.parentElement; const hrefTokens = parent.href.split("/"); const fileNameTokens = hrefTokens[4].split("."); const thumbUrl = `/.media/t_${fileNameTokens[0]}`; spoiler.src = thumbUrl; //spoiler.style.border = "2px dotted red"; }); } if (settings.spoilerImageType.startsWith("reveal")) { addMyStyle("lynx-reveal-spoilerimage",` img.spoiler-thumb { transition: 0.2s; outline: 2px dotted #ff0000ee; ${settings.spoilerImageType=="reveal_blur" ? "filter: blur(10px);" : ""} } img.spoiler-thumb:hover { filter: blur(0); } `) } // Add functionality to apply the custom spoiler image CSS let threadSpoilerFound = false; let tsFallbackUsed = false; function setThreadSpoiler(post) { if (threadSpoilerFound) return; let spoilerImageUrl = null; //When the option is "threadAlt", fallback to "thread" if "threadAlt" doesn't exist yet. if (settings.spoilerImageType == "threadAlt") { const altSpoilerLink = Array.from(post.querySelectorAll('a.originalNameLink[download^="ThreadSpoilerAlt"]')).find(link => /\.(jpg|jpeg|png|webp)$/i.test(link.download)); spoilerImageUrl = altSpoilerLink ? altSpoilerLink.href : null; tsFallbackUsed = false; //stop looking for threadAlt } if (settings.spoilerImageType == "thread" || (!spoilerImageUrl && !tsFallbackUsed && settings.spoilerImageType == "threadAlt")) { const spoilerLink = Array.from(post.querySelectorAll('a.originalNameLink[download^="ThreadSpoiler"]')).find(link => /\.(jpg|jpeg|png|webp)$/i.test(link.download)); spoilerImageUrl = spoilerLink ? spoilerLink.href : null; if (settings.spoilerImageType == "threadAlt") { tsFallbackUsed = true; //Keep looking for threadAlt } } else if (settings.spoilerImageType == "test") { const myArray = [ 'https://8chan.moe/.media/f76e9657d6b506115ccd0ade73d3d562777a441e4b6bb396610669396ff3032a.png', 'https://8chan.moe/.media/1074fdb6eea4ba609910581e7824106736a1bcad446ace1ae0792b231b52cf9a.png', 'https://8chan.moe/.media/c32b4de8490d7e77987f0e2a381d5935ffa6fec9b98c78ea7c05bd4381d6f64b.png', 'https://8chan.moe/.media/bb225302110d52494ec2bea68693d566acee09767212ce4ee8c0d83d49cfa05b.png' ]; spoilerImageUrl = myArray[Math.floor(Math.random() * myArray.length)]; addMyStyle("lynx-thread-spoiler-css1", ` body { --spoiler-img: url("${spoilerImageUrl}") } .uploadCell:not(.expandedCell) a.imgLink:has(>img[src="/spoiler.png"]), .uploadCell:not(.expandedCell) a.imgLink:has(>img[src*="/custom.spoiler"]) { background-image: var(--spoiler-img); background-size: cover; & > img { opacity: 0; } } `); threadSpoilerFound = true; return; } if (spoilerImageUrl) { document.head?.querySelector("#lynx-thread-spoiler-css2")?.remove(); //Remove if the style already exists (from fallback) addMyStyle("lynx-thread-spoiler-css2", ` ${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 #ff000090; & > img { opacity: 0; } } `); if (!tsFallbackUsed) { threadSpoilerFound = true; } } } if (settings.spoilerImageType=="kachina") { addMyStyle("lynx-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; } } `) } function iterateAllPosts() { //Get ALL posts (this does NOT include inlined posts and hovered posts) const allPosts = document.querySelectorAll("#divThreads > .opCell > .innerOP, .divPosts > .postCell"); const postsArray = Array.from(allPosts); //use an array to find the last post postsArray.forEach((post, index) => { if (index == postsArray.length-1) { //only the last post sends batching=false iterateSinglePost(post, true, false); } else { iterateSinglePost(post, true, true); } }); } /** * Processes a single post element. * * @param {HTMLElement} post - The post here can be an .innerPost or one of its containers * @param {boolean} newpost - True if this is a new post in the thread (i.e. not a tooltip or inline) * @param {boolean} batching - False if this is not from a batch from iterateAllPosts (or not the last post of the batch) */ function iterateSinglePost(post, newpost = true, batching = false) { // console.log("Lynx-- processing post", {post}, {newpost}, {batching}); indicateCrossLinks(post); addDeletedChecks(post); imageSearchHooks(post); if (settings.glowFirstPostByID) glowpost(post, newpost); if (settings.spoilerImageType.startsWith("reveal")) revealSpoilerImages(post); if (settings.showPostIndex) addPostCount(post, newpost); //Run only if its a new post in the thread if (newpost) { if (settings.spoilerImageType=="thread" || settings.spoilerImageType=="threadAlt") setThreadSpoiler(post); //This still has to iterate all posts, do it last and only when necessary. if (batching === false && settings.showScrollbarMarkers) recreateScrollMarkers(); } } //Start running and observing //At startup, iterate over all posts after a delay // setTimeout(() => { // iterateAllPosts(); // }, 100); //I guess we don't need a delay anymore iterateAllPosts(); //Observe posts and all their children const observer = new MutationObserver((mt_callback) => { mt_callback.forEach(mut => { if (mut.type=="childList" && mut.addedNodes?.length > 0) { //console.log("MutationObserver!!!"); mut.addedNodes.forEach(node => { //New posts, new inlined posts, new hovered posts all contain .innerPost and are always in a div container. //New posts are div.postCell and new inlines are div.inlineQuote if (node.tagName === "DIV" && node.querySelector(".innerPost,.innerOP")) { // console.log("lynx ~ observer:", {node}, {mut}); if (node.classList?.contains("postCell")) { iterateSinglePost(node, true); } else { iterateSinglePost(node, false); } } }); } }) }); observer.observe(document.querySelector(".divPosts"), {childList: true, subtree: true}); //Observe the hover tooltip (ignore everything else) const toolObserver = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.classList?.contains("quoteTooltip")) { //New hover tooltip div.quoteTooltip found iterateSinglePost(node, false); } }); } } }); toolObserver.observe(document.body, {childList: true}); } //End of runAfterDom() //Starting runAfterDom when the document is ready waitForDom(runAfterDom); })();