LynxChan Extended Minus Minus

LynxChan Extended with even more features

当前为 2025-04-27 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         LynxChan Extended Minus Minus
// @namespace    https://rentry.org/8chanMinusMinus
// @version      2.0.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
// ==/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>`,
				//test:`[TEST OPTION] Randomly pick spoiler image from /gacha/ board <span class='altText'>(This is a test option. It selects the spoiler from var(--spoiler-img) after setting.)</span>`
			},
			nonewline:true
		},
		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.`
			}
		},
		markYourPosts:{
			default:true,
			desc:"<span class='boldText'>Style:</span> Mark your posts and replies <span class='altText'>(with a left border)</span>",
			nonewline:true
		},
		compactPosts:{
			default:true,
			desc:"<span class='boldText'>Style:</span> Make thumbnails and posts more compact",
			nonewline:true
		},
		showStubs:{
			default:true,
			desc:"<span class='boldText'>Style:</span> Show post stubs when filtering",
			nonewline:true
		},
		//I swear this used to be a built in option on 8chan
		halfchanGreentexts:{
			default:false,
			desc:"<span class='boldText'>Style:</span> Make the greentext brighter like 4chan"
		},
		glowFirstPostByID:{
			default:true,
			desc:"Mark new/unique posters by adding a glow effect to their ID"
		},
		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>"
		},
		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]]));

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

	// 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.markYourPosts = false;
			settings.compactPosts = false;
			await GM.setValue("markYourPosts", false);
			await GM.setValue("compactPosts", false);
		}
		// Remove the old setting
		await GM.deleteValue("useExtraStylingFixes");
	}

	//Apply all the styles as soon as possible
	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;
		}

		& .boldText {
			color: var(--link-color);
			font-weight: bold;
		}

		button {
			padding: 8px 16px;
			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:70px;
	}
	@media screen and (max-width: 1000px) {
		#lynxExtendedMenu{
			right:0;
			width:90%;
			/*bottom:15px;*/
		}
	}

	.lynxExtendedButton::before {
		content: "\\e0da";
	`);

	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;
			}
		`);
	}

	if (settings.markYourPosts) {
		addMyStyle("lynx-mark-posts", `
			/* Make your name in your post red */
			.youName { color: red; }
			.you { --link-color: red; }

			/* mark your posts and replies */
			#divThreads .postCell > .innerPost:has(> .postInfo.title > .youName) {
					border-left: 3px dashed #4BB2FFC2;
					padding-left: 1px;
			}

			#divThreads .postCell > .innerPost:has(> .divMessage .quoteLink.you) {
					border-left: 2px solid rgb(0, 102, 255);
					padding-left: 1px;
			}
		`);
	}

	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;
			}
		`)
	}

	//Everything in afterReady runs after document has loaded (like @run-at document-end)
	//Everything in afterReady runs after document has loaded (like @run-at document-end)
	//Everything in afterReady runs after document has loaded (like @run-at document-end)
	async function afterReady() {
		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", `
			<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);
		}

		//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();
				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>${setting.nonewline ? '' : '<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 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");
			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;

			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";
			if (localCount < 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);
			let foo = document.createTextNode("\u00A0");
			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", `<span class='reverseImageSearchDetails'><a href="https://pixiv.net/i/${m[1]}">pixiv</a></span>`)
				}
			}
		}
	
		/*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.style.boxShadow = "0 0 15px #26bf47";
					poster.title = "This is the first post from this ID.";
				} else if (!newpost && idMap[bgColor] == postNumber) {
					//inlines and hovers check the postID
					poster.style.boxShadow = "0 0 15px #26bf47";
					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;
					border: 2px dotted red;
					${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;
		function setThreadSpoiler(post) {
			if (threadSpoilerFound) return;
	
			let spoilerImageUrl = null;
	
			if (settings.spoilerImageType=="thread") {
				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;
			} else 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;
			} 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) {
				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 #ff0000f5;
						& > img {
							opacity: 0;
						}
					}
				`);
				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,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAgICAgJCAkKCgkNDgwODRMREBARExwUFhQWFBwrGx8bGx8bKyYuJSMlLiZENS8vNUROQj5CTl9VVV93cXecnNEBCAgICAkICQoKCQ0ODA4NExEQEBETHBQWFBYUHCsbHxsbHxsrJi4lIyUuJkQ1Ly81RE5CPkJOX1VVX3dxd5yc0f/CABEIAKAAoAMBIgACEQEDEQH/xAAzAAABBQEBAAAAAAAAAAAAAAAFAQIDBAYHAAEAAgMBAQAAAAAAAAAAAAAAAwQBAgUABv/aAAwDAQACEAMQAAAAxC025WeX1uT0GT6NKBlYHnZ9GAOs+9VK0lCVq8paJ06hHXewYwIinP6TrXQ4eeKYwOTdCDYAolYLpv5GfRWCogCYh0bgm7ZuFzrGu5qfBQjHmDFXtAK0gGrcsePMtu7czzq4vNyO0lPE+a9o5l9FZEaHm265dtehK9N5R2msDoT+bXHzjU5M46Xc17aYOONtWGRVR5D0E4+Xivei2577GLUvHcNpRAhAdBNVaENv1a1VsjwnxofoUjARgkWF5LQplGMU0A+ub7suX1KtnWazHThBYlZXZK3BtFWz6ZAgM+KJTHb9jTy5hQwm0fz2zlFIhemC5nadjONK6WPYCncAB7QMrajqxsAnDFkZfR1r3hx/rip9CJX4aJvMPFfOmw2/54cXrOW0rsFf1hPtMWcmw6GWCztkkNWFUp4PptbayOvXZvjpQ96xha951AhEqem8bVbdltUd687uz/on+W9q50DJrddR8ShTzxGQ9q6wkckoSJwSzevYIBPQhKqKbr4xVQ/rVKtGpNRKNb571r3Re7pUjWO0/LG6i6oOIZcaAb29ybMLpubafK8/EtNrQCai33GQjryzWFLUSbULagckaTS5bqVZy4C7zwwO16fm+7cVmxVHOAJrNNmNzk6HIULjXYh9M3qsXy93/8QAKhAAAgIBAwQCAgICAwAAAAAAAgMBBAUAERIGExQhMUEiUSMkByUVMkL/2gAIAQEAAQgAh0gUzoHHvEQixANWU4tgyivI5BhklyDoUMlBAUWbDq5rmRe9MrbGSxvlX6l8rXToMSJ1KKbnZ421qs1nQOhPf4JxDvvDD+dczGORDcUbIATv11bQZZXH6nMY77kC4xqDmJHWNCGARxWsBXqqg2OT3pcQXnk1kacsbBtKMrQUaSemhkFBSGGLvO3GYS1TXQZSBGUToab9+UsqSUHxfQY6Nht4xCl8rF9lWs1TaTMlaZty8qwcxtXF1qwKBImAUwRqMG8Dr2DrSHEWoOkVqcoDQmIcm7ZRTV3pyrDsrr6ugVB9Rrpqtc85GvZs1i3sVrJzEMXUvQytsWRyN6qsinF5y36CcrVzWQAXUkdI3DXDbv8Aw+KXTVGl9OYaUFxqoxtRw2FQFWyEkZYw3xE2LeOiOFaLWKkoxK1qWmEUkLu4BFywZsuY2vackCrYOzTZJvbjyMiYyvR2mGKilbaRKClTyBsYgN2h/GFmm9p8ixmRsVghNirdiVrgc7lLLiapCW2Gi94UJsXRnatbb4svKvcdO5E1Cm/ifjoKVMnceIxqFhMr1AxPGdTtuWiqjESRZC9OTcfHFtmhdSUdqUkJRHPsxMnXUlLGst3Ll50ujDlF+sXcqQK1gGhpDExAoxoLq2q51aiKixUlihbwHVajwHicGM65RMzqICNRO0xrnpGzHrGernkjDsWCbIgvjMHDEyUVzqHWrufeEK1flF6Qy2MZVpsOFs4M6TfE5l6oaMA4o1vvEbdz1GpLUHtqWDtqo/Kg2QQl2eiSM0ZPJs/jEL1+FxzjKAMRDsS8XWCketTj/XqnBdG3crs9qsL0/hh2rGDCx9ZbKpW1m+JYAMrMkcz0o7Jvv26HRqCHL7nlryK1riws3WjfifUtdczBxnyOOQO6mkCkZb1LdP0qbwzMbLvXDVPjgjJPCe83EVYCZdE065CoenLNdl0wVksLWylqky20ytHAhbrwpEck08iyQc6wrFMiQcPgHMQCMdbotmyI0qh3wyquopfYzL4WVXJBM6ai5MzBRicsyNxT0vePbuVumqCPb1Y7JHOw1sFkt4I6iXISK5IWT6Kziq7ORxh15XyAe9AnZDiJMFYQtL8jwIorNqNbEHeunTqFIGeGWQwaIbkMW2IlNsLijcmc1U7jS1VyFq9O64CB301yawc3MzlEJ20zqJQ7ytwkAfjXeE+m/wCviN5ltLWOtqQ1nYv22PQkYC3EVEAJm65uK1pBO0LRUgNzLN0Mdat0ZsNSQTJBYrTYWSdbvxtyN7dGuZG5DVWE8uSrllbFzpwy6IMbeDrnJEliUAUgbxsR8LgtvzY9oFEDCmHpNbhxPT7yrIzZVXhbdOuqVG0VFbzYOxja+OEnOxwU8kAt5+FcdSQltqvKTWK79ADlYzbX2jEKz39xk8mCRTsNZxSjjo31jjY2TWjbt2LOK4TAgWMkhk7AUjKJX49aBgoSCDj2tD0kZAptyx/FCIShYkVlS3rw67edp1KuWwTKfju+NVMTTzGSyQ5OvMqTXXOQLdQ7SUkiSEk77xMLlQ7Atp7GMlEzG+gH3EFzlvxXk4KY1zjltEF7/H1O27fIkeCqbGypxFSPvt7rq8Ks0ZWdKoiscsVy/LfVysLmS3TOwXKogrA2AjuwpYxGxpEtFR2naIozMexqRHqZrlqU/ehhgT62nQNKNKsBM7ERxLg4yAFUZyBGR/qDCifjntgK1qSUsmNyFdUezybLDEgCjspY1a6ddhLEzlMa7MahO+uxM/E1rG/qKdj7nnGt5+uU/cHGuYT6PuBBDwc80gDAkkpsI4Mci2vt2oS5XCKrauccxfkWXL5qNSvbHkusxfaGAgh13NteTEfPnDHxN8vrzHT8DP7k16kwj/rzKZ1BT9cyiYLWOXFt8oh1uovNNxUW7aqoSo8QGeEWjcBb/wD2CCsOWiMjh0TBuRDExEQHOJ+Jktbx99wI15P68pn1ElOp31HLW563/YyZFsGLvRWu5E7TH2FsK7GHpOzVi/dfkRPAZhTkZnOFkRRTxXRlIa6WG3bVtKE27Kddtf1AfqBsfX9uNST/AIKSH75xqTjXMvrc9bl9qrW7T1orXEsQ+oFvpjDry1jLDKbFvB5CwIY3B5XOvh5YPpmri4kYvVpx5eYqeoEmuCVkWva5ptmS1ueocUai0caG6zUXz+/x+pAvqQ/cRqZKPjC403VbFqOqalZOKpc/8eXQTk765yXSGMyt9V3VPH16QTCXHSpikbPVeYxwVbVIumaVe5TaTbmNSePsV0zy1+eu4cak9/ncNepj1KlaNUD8ZC0dZHMV5m4HbJ1C0i+8E16i5SpCl9cZeICvTVWxeYadjJ47pzrhVgxr322rpvERt9cU6d6zV1YwmXzmRuZU+nqljHW3kwjENpLLVvGyFgNcpjXOdTOp5a3j7//EADsQAAICAQIDBgQEBAMJAAAAAAECAxEAEiEEMUETIlFhcYEyQpGhBRBSsRQjgsEgotEkM2Jyc4OSwtL/2gAIAQEACT8AY7gi8Yi2HLkcti0IHMgnY9RkRAkkYyE/q5ZIivKw0qy3Xl5EdDkSAOjN3zWyGq98jeN2OvS36TyryyBlG6Mrbar35YymLiJFRl3CqFXkSpB3rHohWOknV5gXk81qNlG1bDC4RtyH3PPBvm2+dcoDxJAyVGfc0DdAZxUSncAFt84xPZWOcSw/7T4bOdBisXSE8j0BONX85W8Su2Rr2aqADJVhiRnDsyI4XysdMrUU7IIeYv4qyQKYkOsEli2/icmUBGBVWolqwhFG1evKsosyFDfKvDCbpQPXJFDGrXAjEnCqeg6Z+IpGfFyAPuc/ExO52dQmw9Mddv8AhGOSW5BRz9KyeNZGBI7WQRg15nFII5g7HBTdfcYu0qMoNkCvGuuFtV6CADs/Z6+mS6WjRBIo3p2BY4zKsrOEJB1JoIUsfXCobWBrG+7dcaPeFVkGoErJz3X9jjpbbs3QE3iEopoODa3jEIDe/NieuS1INgeZPgTkhPaWAUs6P9DiSSAbfAbAOcSTCToeHWI6YeHjecRHBaFqPfkXybOwPEmNFlMitNGWT5gqlSDnF8QZ7tZAVAT+k5JEZCRr+EaiCSD3Ko75FGxJPx99d/C7yOORflvY4hEykiJjudKjVRPVfA5YfskVj0O1/YnB3FliIJHxDvb+pAx2qSRpH62SAoH0GQWRA6Bmvu0AozRrVjo0E2a5NY5ZzPMnGDDlsbB8sg7RP3BBOPUcZNt6dBfXF5Dc4Tty8st0Hwk8wB0vLkjZLphf1zUVhFym/wBV/wDzj6REUDBefeyayswT0BQt+4yNlBKqAe73j09sYqorkfE1kamlBNixkffRSinwVtqxQQBQ2sgYoGkbbcgOWdABnkBXU4dgNzjFeEU0ij56+Y4x7CRwkq/82wb2wgAnSennnB8Mmk8wBZW9zlIoDSOfDqckaGO/5aCwQPFvPFHbRNoeuvgffBWjb6b5WkyBnvmQAQPuc0ntWmsgfLI5YD2vE0gMx+pJ/vgGkSBmB67Vm4MehvOm2PuM5EYOn5Hauf5dN/oMNNMQntzbBy5Ztan2OWwlC9igfQHNc2boucDwDMVJSGnDSUKpW1e1nEkg4hwAYJjYcLuUVmo35Nlq9kEHYgjxzdZOH+8ZzqFP9vy8f8DTCT9F0fo2fxNnnewxkDgc5BnY9pRHPYnNm5dwBrP9JOK4qL5lK8yPHOTmTJRw3BXtK27Sf9MZwKcRMu5klbtCPQmwCfAYInmhj7OVGGqM7cmHVGydo3mRS0H8OnEWvIBTYuPwHy9cCSEVo4kbEuD3VjG9qD1vJVbiYyhfhzsXGnmp8cUhhFICCKIogEHGo9mh5E9T4YGb2rOGm9V0kZAAvizY6Ajoi6skCjyUE/fDy5ahecUNYPwyAEH3qwcmiXUN9Isj0OS71QZs/EdPQLFGq/Whks8twk63VgpAI5E5Z4bhiXkQc5C2ypkPdjWlTakHix5DLck7JGuxPle5xo4CvzE6mZfBwKGceACbKxkAX7Xn4n3gKXXVD7DJmmBBt4tnN87HXCO0liMM1dWG4evEgUc4WaQIEjGkbGhn4fIo8CQ37Zwkvst5AVvaiwByaJPIW5wySnzOkfQZwsp9sqP3JP2wl66kZHY9DnBgyUaJsLeRwJEupSoJ11XOiOWbRdqzOw5sV7oVfTqcCiuX6R6DqfM4O1lOzSubHp5+gzi3NnZF5nyAGcFMQsRmkIkJaOIGi7dBnEOqkfNvhKg+6NiATqAZIr+KiDYwuWskjQRve43zhzDF1d9ifQDCN8OkX4HNZ9FzhSfAs2JIG8XGkYQnmGLZxxvyBziZK81x01SKFDtuVN9M4iUzJr+JKJNkcuvmM/losKFydunLFIi6k7A+v+gxdch2BOw9vAYdcjc3P7DwGRluxa5aJAMbfK9c1vcg5yPMD9xjLR3AYXXmpzmn+ZTgIkeY2SAUAKBr9d84iRmJobUDgGkEEir1YQwcXTUQwPli9i/6CSUPp4YSjKaIINg/XFLebb4Fu/05GGFeNZSf1XkgfcGiaBrpkOly1SRNup2+INyBzhEQKatt+XgM778gq9ThVrVVe/gA5kC+m+cWHRtmjSUSRBvvpyHh5WlJLkyEXf8ATnFPAUNMYiCzqOQ1HGYDTvqOu6IFm+ovGOouLbyJ0nnfjjVpRdWol6dtzi6n5ErtQ9Mcj1A+9ZImmHZe6euTVfghOSFiTzZFArPw4sf1Gev2zhZzvuEl5/XOGMSdAza2998DB78BWShSBsShI+14Y5I9dD5SOX6hipGoF23eO4vN20B2Y8824XiPxBBxak90ghqRvIsAMiSLiZZnikWMBS/DhCTqA6K1UcT7jIRNFwpjihgf4O+gYyEePQY7SIk/EwI7EkmNHKpZPPkBeEX5muTDBZK7X1vIVF9ayRx6nJ622s5RxB5WMa69sT3rEA9sU36YMRD3uRPWhhCyMQ8d73QoDCyopoKN/h5YiSRsXVlYWCNR2ORBZCApeyzUOQtiTWP3seZJWTs2eKRo2ZD0JXNMcfDxCNANgCN6HpmoIsoD11LsdskUbUMdclXJc1H8l/YYPveJZ8tsH13yFSfM1iBRa7A3zsYxQogdWHQFc7H/AGuTShEgIB06t8fQwNyKd1PmR/fE0sVBIG9XmsnwCnF7ONwGJu2IP7ZA76VLnSpYBTuCaxdKmyo6kn5j+RwPiy/bFkyx65+xGEZfscavXCpwi/W+W+VSuQ9ixoDdfY5HD2hPcIFaUyJZBRHe5i/BhuM/EuIVFFCOUrOv1cavvn4urQrIHMaRBA1ZFpCK2sjmwCmifTDKkpqOXQ+kMAKBxiqAlUHPYGgckxsF++RH64rjC2OfbNbeoGah9Mr65q+uA7EHDY3Lkb0KAOcK8Z7VhG6sCNhnFQCd0PZLK+jV65JHJ3tSSK4cUemSD0UYPjNN5IN2xXjOk60Q0GXyxVAA2o4p9iMWTNf0x2HquS/5clP0wYPyX8tFjenbSDXS+e+dlEw4USFAw7hTmv0rGT+JgnM/jqD7S+wZsqd4qPYaxH2rHkpJ5IMi7JHCzpFY2VtniavDHdnepJGUlNKjxPTzziZZeIYc3Zjt5A/lY7OUr9e8B9Dj5Iv3GMP/ACwX7g5Df9ORV7kZeMcfGOXiRMWN247ylQTsfA9c4dX4ueQmSVjrRxJIBa8u+njiBmXgFKX4zSOxHvgBcIU7/Iqd1Y5r0yn/AHzC2fyjXr9lGRxFhVhjZYjq7dSPoM0oq1r8C5s2P2OKRqG22o/6DC6sXUpF6ii7HwobfkT9cd8kbG/KvYkYf74B9MA+pw375QleokL9UU2w9DnDpLPHJFHHMy7p3gzel1jjW/BcLSdWVASxHpeRRMKay267m905Ni25ADSNuzV4nw8htjqZJpNKD5nY9FzikXjDCGiiI3AlOkFTkzsQ7K0YpQOTAgjIwpYagepddwScBwYD9M/bCR7Y2IuL98bTzFnfEUofZiMJZ2F7oaA8TiVEiVvsbxgY9ZeU+JRSyjEcnhZo0uI/zEKxg2BlcLxnLURUUp/9WwxJCYyClEyF22BB5ac4GeSGOUxCWKVSXI50jZE/DcK0qvEsuzkDSqgLkqFCRE6j6q3teEDfNlLa09H3w4RlYMXP/8QAKhEAAgIBBAEEAQMFAAAAAAAAAQIDEQAEEiExQRNRYXEiFIGRBRAyQpL/2gAIAQIBAT8AKrRzWKJHKKp3A1kJkjiKKq12bqryGeNygZNpPBPjnCdSjAIaCWAT1Qx9Q7brHHkeMG0nvjEhQ1ybxdCDRs188HBoYgOScaYWB93XjJNrUQDf384JYyqKRfk8dfWPHHY2EbdtH5zeKpqoe/GSwRseCAe7GMi+nSxqx/jIJpzUSgIB5rxjBkDH1Ojzjw7iTuNg1d4HPzhPtjOEBZidoFnIAjKHUgg9EZqVQr+RAsUTkccUe0hzT++SIpTrrrJCVkkAPnGkdrsmibIwSOq0Dxj/ACi/vjCMmti/sMl0kciMrKdpHPYxHj0GmjhiDSPztTybOSyThBLO6IS34xhgWWvOS671EVCw4PJrvP1HrQNBG1SGM7X8e385BoY0iQO25q5JPZz9Npq52j7OVo4/G4/AvLdhwBnofK/9DJ5BApLBiKHQvs1kszGVo9MoMlAPIelHtn6eJOXuRz2WwDTOApjW/wDahVZPpzAwli6ByMRywJKtgMoJvx8ZtAqxkUEUgtf4JyQG+qwGxRIydaiB4PNfX9hDvR5N3R6yDSD1W2gqW7JvFRSNj9XRxdCraZEThRzXQs8439PIHYGR6CQUyuB8g4ebNi8QqHBe6zXbW0tr0GBxmC9nN47V7BPjEDk2pOaWFnnVK/xP5WLqvfBQ85vPvm75GCY563uc9dTGyX3mrjdvyUkgKbHx75GJVjZxIFHsTW76xZ5jNGd1kccccHNC7LqNRMZAVc8DBqRnrjPXGUfbK+sNAHIImC/mdxIN+xzYu2jWwiqx0EUIWFbVWO6xycifaaB7wO2B3wSsMB+cCOReMj7DYoe54zTHatk0PF4Cv745hjQn8VFZDIknMZuj4wPm4ewwnP/EACoRAAICAQMDAwQCAwAAAAAAAAECAxEABBIhIjFBBRNhEBQyUXGBkZKh/9oACAEDAQE/AFeRZFBJA3cf3np8n2+lE8ky7CCStc81mqkgnmVzvL8C1u1Wrs5qdLqVSQrNuQcqCeeg+PF8YzaR0BlW2lALKvez85NAkLIBHTcMkgPUcaKZjuZTeUch0zPy52j/ALh0MdcO15EiWNzdmB5yLWGIGqpyQRV9NYJNrbh3qjV88ec0iM6F5bDGQMp88ZrtEUdWQFlY1QFkHNNJqbCHrFUqsbAw6J1tzqpABzXLVkUiMEAdSK6m4BzdplBJsAHk3jQad91ORzRzb85RyGH3pY46/JgMfppKoDACyMou/GaeB0LuRZAqx4GKx3d++Sp7czgcCzWF3N2x5N4k8qKVVqGGENYsNh0kTLyif64mmSKVJEFFThQzOzMQq+WxHi94xIjmkDFyp2mzVYsLKSwjYXmwCQv4DcjJ4/cldwKs+SBh06bRZVT/ADeBdOn7Y4dQEPOfe7uyn/GQ3MBRUEmqJxY+kNIaXwv7xHY/j0L8d89yYM+2VuKoE3iOuoBRxT13HnCp95oinN0OcGhj/eTIkBAdKvsasYHWvzBy2B4BzQsxlZSa4uu9/Qy7HSKj1LeGVY1L3dY7MDvXvVjE9QaPVyyMeomv3QHGR+r2e15q/U0kjaNwfix5xUZWWwaGOrFCEq89MDprKbuVIwYjK6UoBy1Xlxmt1MaQPJf5Clo0TeEnOc6vnDpVz7TF0bLKr+RhkCQMNlknvgkZTxm5pGF9818CvptPCENoBZ/gVh0Hxn2Z/WHSH6m64x5LFFdtcZ8eciYK4vCtjkYY1wxJhgBzcuGRFNG8MqWADZPgc5ON5AAs4ytiROSKBvOtVHucGvOFco4Ac//Z");
					& > 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});

	} //afterReady close

	//Starting afterReady
	if (document.readyState === "loading") {
		//Loading hasn't finished yet. Wait for the inital document to load and start.
		document.addEventListener("DOMContentLoaded", afterReady);
	} else {
		//Document has already loaded. Start.
		afterReady();
	}
})();