Greasy Fork

LynxChan Extended Minus Minus

LynxChan Extended with even more features

安装此脚本?
作者推荐脚本

您可能也喜欢8chan-buffs

安装此脚本
// ==UserScript==
// @name         LynxChan Extended Minus Minus
// @namespace    https://rentry.org/8chanMinusMinus
// @version      2.1.1
// @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,
			type:"checkbox_with_colors",
			desc:"Show your posts and replies on the scrollbar",
			color1Default:"#0092ff",
			color1Desc:"<b>Your marker:</b>",
			color2Default:"#a8d8f8",
			color2Desc:"<b>Reply marker:</b>"
		},
		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 lineBefore'>(Previously spoilered images will have a red border around them indicating that they're spoilers.)</span>",
				reveal_blur:"Change to a blurred thumbnail <span class='altText lineBefore'>(Unblurred when you hover your mouse over.)</span>",
				kachina:"Makes the spoiler image Kachina from Genshin Impact.",
				thread:`<b>Use <b style="color: var(--link-color);">"ThreadSpoiler.jpg"</b> from the current thread <span class="altText lineBefore">(first posted jpg, png or webp image with that filename)</span></b>`,
				threadAlt:`same as above with the filename <b style="color: var(--link-color);">"ThreadSpoilerAlt.jpg"</b> <span class="altText lineBefore">(jpg, png or webp; uses ThreadSpoiler.jpg until this is found)</span>`,
				//test:`[TEST OPTION] Randomly pick spoiler image from /gacha/ board <span class='altText lineBefore'>(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:"Also override board's custom thumbnail image <span class='altText lineBefore'>(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.`
			}
		},
		markPostEdge:{
			default:true,
			type:"checkbox_with_colors",
			desc:"<span class='boldText'>Style:</span> Mark your posts and replies <span class='altText'>(with a left border)</span>",
			color1Default:"#4BB2FF",
			color1Desc:"<b>Your border:</b>",
			color2Default:"#0066ff",
			color2Desc:"<b>Reply border:</b>",
			nonewline:true
		},
		markYouText:{
			default:true,
			type:"checkbox_with_colors",
			desc:"<span class='boldText'>Style:</span> Color your name and (You) links",
			color1Default:"#ff2222",
			color1Desc:"<b>Color:</b>",
			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,
			type:"checkbox_with_colors",
			desc:"Mark new/unique posters by adding a glow effect to their ID",
			color1Default:"#26bf47",
			color1Desc:"<b>Glow color:</b>"
		},
		showPostIndex:{
			default:true,
			type:"checkbox_with_colors",
			desc:"Show the current index of a post on the thread. <span class='altText'>(OP: 1, first post: 2 etc.)</span>",
			color1Default:"#7b3bcc",
			color1Desc:"<b>Index color:</b>"
		},
		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;
    		filter: drop-shadow(0px 0px 1px #000000BA);
		}

		.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", `
			<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();
                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 = `<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>${setting.nonewline ? '' : '<br>'}`;
					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 += `
						<label style="margin-left:0.5em;">
							${colorDesc}
							<input type="color" id="${name}_color${idx}" value="${colorValue}" ${settings[name] ? '' : 'disabled'}>
						</label>
						`;
					});
					settings_content.innerHTML += `
					<label>
						<input type="checkbox" id="${name}" ${settings[name] ? "checked" : ""}>
						${setting.desc}
					</label>
					${colorHtml}
					<br>${setting.nonewline ? '' : '<br>'}`;
				} 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>
					<button id="resetSettings" style="float: right;">Reset</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] = 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", `<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.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;
						background-position: center;
						& > 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;
						background-position: center;
						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;
					background-position: center;
					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);
})();