LynxChan Extended Minus Minus

LynxChan Extended with even more features

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         LynxChan Extended Minus Minus
// @namespace    https://rentry.org/8chanMinusMinus
// @version      1.53
// @description  LynxChan Extended with even more features
// @author       SaddestPanda & Dandelion & /gfg/
// @license      UNLICENSE
// @match       *://8chan.moe/*/res/*
// @match       *://8chan.se/*/res/*
// @match       *://8chan.cc/*/res/*
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.deleteValue
// @grant        GM.registerMenuCommand
// @run-at       document-idle
// ==/UserScript==

(async function () {
	"use strict";

	const SETTINGS_DEFINITIONS = {
		firstRun:{
			default:true,
			hidden:true,
			desc:"You shouldn't be able to see this setting! (firstRun)"
		},
		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. Previously spoilered images will have a red border around them indicating that they're spoilers.",
				kachina:"Makes the spoiler image Kachina from Genshin Impact.",
				thread:`Uses the first image of the first visible post on the current thread with the filename <b style="color: #6bc9ff;">"ThreadSpoiler.jpg"</b> (or .png or .webp)`,
				threadAlt:`same as above with the filename <b style="color: #6bc9ff;">"ThreadSpoilerAlt.jpg"</b> (or .png or .webp)`
			}
		},
		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. (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 <a href="https://www.dropbox.com/s/n6ys414nviitr9y/MadokaRunes-2.0.ttf">MadokaRunes.ttf</a> for it to show up properly.`
			}
		},
		useExtraStylingFixes:{
			default:true,
			desc:"Apply some styling fixes (Mark your posts and replies, smaller thumbnails etc.)"
		},
		//I swear this used to be a built in option on 8chan
		halfchanGreentexts:{
			default:false,
			desc:"Make the greentext brighter like 4chan"
		},
		showPostIndex:{
			default:true,
			desc:"Show the current index of a post on the thread. That is, the topmost post will start at 1 and count up from there."
		},
		showStubs:{
			default:true,
			desc:"Show post stubs when filtering"
		},
		preserveQuickReply:{
			default:false,
			desc:"Preserve the quick reply text when pressing the Esc key or X button"
		}
		//Would anyone ever want this off?
		/*ctrlEnterPost:{
			default:false,
			desc:"Enable Ctrl+Enter to post your reply"
		}*/
		/*redirectToCatalog:{
			default:false,
			desc:"Redirect to catalog when clicking on the index."
		}*/
	}

	const settingsNames = Object.keys(SETTINGS_DEFINITIONS);
	const settingsValues = await Promise.all(settingsNames.map(key => GM.getValue(key, SETTINGS_DEFINITIONS[key]['default'])));
	const settings = Object.fromEntries(settingsNames.map((key, index) => [key, settingsValues[index]]));

	console.log("%cLynx Minus Minus Started with settings:", "color:rgb(0, 140, 255)", settings);
	if (typeof api !== "undefined") {
		console.log("The script is not sandboxed. Adding quick reply shortcut.")
		
		function quickReplyShortcut(ev) {
			if (ev.ctrlKey && ev.key == "r") {
				ev.preventDefault(); qr.showQr(); document.getElementById('qrbody')?.focus();
			};
		}
		document.addEventListener("keydown",quickReplyShortcut);

	} else {
		console.log("JS script is sandboxed and can't access page JS... (If you can read this, let me know what browser/extension does this. Or maybe the site just failed to load?)")
	}

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

	addMyStyle("lynx-extended-css", `
	.marker-container {
		position: fixed;
		top: 16px;
		right: 0;
		width: 10px;
		height: calc(100vh - 40px);
		z-index: 11000;
		pointer-events: none;
	}

	.marker {
		position: absolute;
		width: 100%;
		height: 6px;
		background: #0092ff;
		cursor: pointer;
		pointer-events: auto;
		border-radius: 40% 0 0 40%;
		z-index: 5;
	}

	.marker.alt {
		background: #a8d8f8;
		z-index: 2;
	}

	#lynxExtendedMenu {
		position: fixed;
		top: 15px;
		left: 50%;
		transform: TranslateX(-50%);
		padding: 10px;
		z-index: 10000;
		font-family: Arial, sans-serif;
		font-size: 14px;
		box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
		background: #353535;
		border: 1px solid #737373;
		color: #ddd;
		border-radius: 4px;
		max-height:100%;
		overflow-y: auto;
	}
	/*What the fuck is up with CSS */
	/*.settings-content {
		max-height: 90%; 
	}*/
	.settings-footer {
		height:70px;
	}
	@media screen and (max-width: 1000px) {
		#lynxExtendedMenu{
			right:0;
			width:90%;
			/*bottom:15px;*/
		}
	}

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

	// Register menu command
	GM.registerMenuCommand("Show Options Menu", openMenu);
	try {
		createSettingsButton();
	} catch (error) {
		console.log("Error while creating settings button:", error);
	}

	//Open the settings menu on the first run
	if (settings.firstRun) {
		settings.firstRun = false;
		await GM.setValue("firstRun", settings.firstRun);
		openMenu();
	}
	
	function replyKeyboardShortcuts(ev) {
		if (ev.ctrlKey) {
			let combinations = {
				"s":["[spoiler]","[/spoiler]"],
				"b":["'''","'''"],
				"u":["__","__"],
				"i":["''","''"],
				"d":["[doom]","[/doom]"],
				"m":["[moe]","[/moe]"]
			}
			for (var key in combinations)
			{
				if (ev.key == key)
				{
					ev.preventDefault();
					console.log("ctrl+"+key+" pressed in textbox")
					const textBox = ev.target;
					let newText = textBox.value;
					const tags = combinations[key]
					const selectionStart = textBox.selectionStart
					const selectionEnd = textBox.selectionEnd
					
					if (selectionStart == selectionEnd) { //If there is nothing selected, make empty tags and center the cursor between it
						document.execCommand("insertText",false, tags[0] + tags[1]);
						//Center the cursor between tags
						textBox.selectionStart = textBox.selectionEnd = (textBox.selectionEnd - tags[1].length);
					} else {
						//Insert text and keep undo/redo support (Only replaces highlighted text)
						document.execCommand("insertText",false, tags[0] + newText.slice(selectionStart, selectionEnd) + tags[1])
					}
					return;
				}
			}
			//Ctrl+Enter to send reply
			if (ev.key=="Enter") {
				document.getElementById("qrbutton")?.click()
			}
		//I wanted to change this so it propagates upwards for any input but I decided it was kind of pointless.
		//The quick reply field is focused... EXCEPT when you're pressing the button at the bottom. Why??? JUST ALWAYS FOCUS IT
		} else if (ev.key == "Escape") {
			//Because some script managers cannot access the JS of the page we have to do some funny stuff
			//(So far violentmonkey isn't sandboxed)
			document.getElementById("quick-reply").querySelector(".close-btn").click()
		}
	}
	document.getElementById("qrbody").addEventListener("keydown", replyKeyboardShortcuts);

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

	function openMenu() {
		const oldMenu = document.getElementById("lynxExtendedMenu");
		if (oldMenu) {
			oldMenu.remove();
			return;
		}
		// Create options menu
		const menu = document.createElement("div");
		menu.id = "lynxExtendedMenu";
		menu.innerHTML = `<h3 style="text-align: center; color:#6bc9ff;" class='settings-header'>LynxChan Extended-- Options</h3><br>`;
		
		//we use createElement() here instead of setting innerHTML so we can attach onclick to elements
		//...In the future, at least. There aren't any onclicks added yet.
		let settings_content = document.createElement("div");
		settings_content.classList.add("settings-content");
		Object.keys(SETTINGS_DEFINITIONS).forEach((name) => {
			const setting = SETTINGS_DEFINITIONS[name];
			if (setting.hidden) {
				//pass
			}
			else if (setting.type == "radio") {
				let html = `<span>${setting.desc}</span><br><form id="${name}" action='#'>`
				for (const [value, description] of Object.entries(setting.options)) {
					html += `
					<label>
						<input name="${name}" type="radio" value="${value}" ${settings[name]==value ? "checked" : ""}
						<span>${description}</span>
					</label><br>
					`
				}
				html += "</form><br>"
				settings_content.innerHTML += html;
			} else {

				settings_content.innerHTML += `
				<label>
					<input type="checkbox" id="${name}" ${settings[name] ? "checked" : ""}>
					${setting.desc}
				</label><br><br>`
			}
		})
		menu.appendChild(settings_content);
		menu.innerHTML += `
			<div class='settings-footer'>
				<button id="saveSettings">Save</button>
				<button id="closeMenu">Close</button>
			</div>
		`
		document.body.appendChild(menu);

		// Save button functionality
		document.getElementById("saveSettings").addEventListener("click", async () => {
			Object.keys(SETTINGS_DEFINITIONS).forEach((name) => {
				const setting = SETTINGS_DEFINITIONS[name];
				if (!('hidden' in setting)) {
					if (setting.type=="radio") {
						settings[name] = document.querySelector(`input[name="${name}"]:checked`).value
					} else {
						settings[name] = document.getElementById(name).checked;
					}
				}
			})
			console.log("Saving settings ",settings)
			await Promise.all(Object.entries(settings).map(([key, value]) => GM.setValue(key, value)));
			alert("Settings saved!\nRefresh the page for the changes to take effect.");
			menu.remove();
		});

		// Close button functionality
		document.getElementById("closeMenu").addEventListener("click", () => {
			menu.remove();
		});
	}

	function createSettingsButton() {
		//Desktop
		document.querySelector("#navLinkSpan > .settingsButton").insertAdjacentHTML("afterend", `
		<span>/</span>
		<a id="navigation-lynxextended" class="coloredIcon lynxExtendedButton" title="LynxChan Extended-- Settings"></a>
		`);
		//Mobile
		document.querySelector("#sidebar-menu > ul > li > .settingsButton").parentElement.insertAdjacentHTML("afterend", `
			<li>
				<a id="navigation-lynxextended-mobile" class="coloredIcon lynxExtendedButton" title="LynxChan Extended-- Settings">Lynx Ex-- Settings</a>
			</li>
		`);
		document.querySelector("#navigation-lynxextended").addEventListener("click", openMenu);
		document.querySelector("#navigation-lynxextended-mobile").addEventListener("click", openMenu);
	}

	function createMarker(element, container, isReply) {
		const pageHeight = document.body.scrollHeight;
		const offsetTop = element.offsetTop;
		const percent = offsetTop / pageHeight;

		const marker = document.createElement("div");
		marker.classList.add("marker");
		if (isReply) {
			marker.classList.add("alt");
		}
		marker.style.top = `${percent * 100}%`;
		marker.dataset.postid = element.id;

		marker.addEventListener("click", () => {
			let elem = element?.previousElementSibling || element;
			elem.scrollIntoView({ behavior: "smooth", block: "start" });
		});

		container.appendChild(marker);
	}

	function recreateScrollMarkers() {
		let oldContainer = document.querySelector(".marker-container");
		if (oldContainer) {
			oldContainer.remove();
		}
		// Create marker container
		const markerContainer = document.createElement("div");
		if (settings.showScrollbarMarkers) {
			markerContainer.classList.add("marker-container");
			document.body.appendChild(markerContainer);
		}

		// Match and create markers for "my posts" (matches native & dollchan)
		document.querySelectorAll(".postCell:has(.innerPost > .postInfo.title :is(.linkName.youName, .de-post-counter-you)),.postCell:has(.innerPost.de-mypost)")
			.forEach((elem) => {
				createMarker(elem, markerContainer, false);
			});

		// Match and create markers for "replies" (matches native & dollchan)
		document.querySelectorAll(".postCell:has(.innerPost > .divMessage :is(.quoteLink.you, .de-ref-you)),.postCell:has(.innerPost.de-mypost-reply)")
			.forEach((elem) => {
				createMarker(elem, markerContainer, true);
			});
	}

	function addPostCount() {
		//This function causes a DOMException, I don't know why, just ignore it
		const posts = Array.from(document.getElementsByClassName("divPosts")[0].children);
		//Why is the insert method called unshift???? This inserts it at the beginning
		//(This is also insanely inefficient since we only need to do it once)
		posts.unshift(document.querySelector(".innerOP"))
		
		//This goes bottom to top so we stop when we've reached posts
		//that have already had a number inserted
		for (let i = posts.length-1; i>=0; i--)
		{
			//We've reached posts where we already added numbers, 
			// there's no need to keep going.
			if (posts[i].querySelector(".postNum")) {
				break;
			}

			const postInfoDiv = posts[i].getElementsByClassName("title")[0]
			if (!postInfoDiv) {
				console.error("[Lynx--] Failed to find post for div ",posts[i])
				continue;
			}
			const posterNameDiv = postInfoDiv.getElementsByClassName("linkName")[0];
			
			var newNode = document.createElement("span");
			newNode.innerText = i+1;
			newNode.className="postNum"
			if (i < Infinity) //knownBumpLimit
			{
				newNode.style = "color: rgb(123, 59, 200); font-weight: bold;"
			}
			else
			{
				newNode.style = "color: rgb(255, 4, 4); font-weight: bold;"
			}
			postInfoDiv.insertBefore(newNode, posterNameDiv);
			var foo = document.createTextNode("\u00A0");
			postInfoDiv.insertBefore(foo, posterNameDiv);
		}
	}

	const indicateCrossLinks = function() {
		const crossLinks = document.querySelectorAll(`a.quoteLink:not(.crossThread):not([href*='${api.boardUri}/res/${api.threadId}'])`);
		crossLinks.forEach(crossLink => {
			crossLink.classList.add("crossThread")
			//This is kind of an expensive operation isn't it? Plus checking :not(.crossThread)? Oh well who cares
			const hrefTokens = crossLink.href.split("#");
			const quoteLinkId = hrefTokens[1];
			crossLink.innerHTML = ">>" + quoteLinkId;
		});
	}
	function addDeletedChecks() {
		const postLinks = document.querySelectorAll(`a.quoteLink[href*='${api.boardUri}/res/${api.threadId}']`);
		
		//This goes bottom to top so we stop when we've reached a post with a check attached
		for (let i = postLinks.length-1; i>=0; i--)
		{
			//We've reached posts where we already added numbers, 
			// there's no need to keep going.
			if (postLinks[i].hasMouseOverEvent) {
				break;
			}
			var evListener = function(ev) {
				if (!document.getElementById(ev.target.href.split("#").pop())) {
					ev.target.classList.add("deleted")
					//Sadly this doesn't actually work and I don't know why
					//postLinks[i].removeEventListener("mouseenter",evListener)
				}
			}
			postLinks[i].addEventListener("mouseenter", evListener);
			//Why does js allow this
			postLinks[i].hasMouseOverEvent = true;
		}
	}

	addMyStyle("linkHelpers",`
		.quoteLink.crossThread::after {
			content: " \(Cross-thread\)";
		}
		.quoteLink.deleted::after {
			content: " \(Deleted\)";
		}
	`)


	const revealSpoilerImages = function() {
		const spoilers = document.querySelectorAll(".imgLink > img:is([src='/spoiler.png'],[src*='/custom.spoiler'])");
		spoilers.forEach(spoiler => {
		  const parent = spoiler.parentElement;
		  const hrefTokens = parent.href.split("/");
		  const fileNameTokens = hrefTokens[4].split(".");
	  
		  const thumbUrl = `/.media/t_${fileNameTokens[0]}`;
		  spoiler.src = thumbUrl;
		  spoiler.style.border = "thin dotted red";
		  spoiler.style.borderWidth = "2px";
		});
	}

	const executePostModifiers = function() {
		indicateCrossLinks();
		addDeletedChecks();
		// Recreate markers because the page grew taller. Is this heavy? probably not.
		if (settings.showScrollbarMarkers)
			recreateScrollMarkers();
		if (settings.showPostIndex)
			addPostCount();
		if (settings.spoilerImageType=="reveal")
			revealSpoilerImages();
	}

	// Create markers 1 second after page load
	setTimeout(() => {
		executePostModifiers();
	}, 1500);

	const observer = new MutationObserver((mt_callback) => {
		mt_callback.forEach(mut => {
			if (mut.type=="childList") {
				//console.log("MutationObserver!!!");
				executePostModifiers();
			}
		})
	})
	observer.observe(document.querySelector(".divPosts"), {'childList':true})

	// Apply the CSS if the setting is enabled
	if (settings.useExtraStylingFixes) {
		addMyStyle("extra-styling-css", `
			/* smaller thumbnails & image paddings */
			body .uploadCell img:not(.imgExpanded) {
				max-width: 160px;
				max-height: 125px;
				object-fit: contain;
				height: auto;
				width: auto;
				margin-right: 0em;
				margin-bottom: 0em;
			}

			.imgExpanded { max-height:100vh; object-fit:contain }

			.uploadCell .imgLink {
				margin-right: 1.5em;
			}

			/* smaller post spacing (not too much) */
			.divMessage {
				margin: .8em .8em .5em 3em;
			}

			/* Make your name in your post red */
			.youName { color: red; }
			.you { --link-color: red; }

			/* mark your posts and replies (same selectors are also used for detection above) */
			.postCell:has(.innerPost > .postInfo.title :is(.linkName.youName, .de-post-counter-you)),
			.postCell:has(.innerPost.de-mypost) {
				& > .innerPost {
					border-left: 3px dashed;
					border-left-color: #4BB2FFC2;
					padding-left: 0px;
				}
			}

			.postCell:has(.innerPost > .divMessage :is(.quoteLink.you, .de-ref-you)),
			.postCell:has(.innerPost.de-mypost-reply) {
				& > .innerPost {
					border-left: 2px solid;
					border-left-color:rgb(0, 102, 255);
					padding-left: 1px;
				}
			}
		`);
	}
	if (settings.halfchanGreentexts) {
		addMyStyle("halfchanGreentexts",
			`.greenText {
				filter: brightness(110%);
			}`
		);
	}
	
	if (settings.showStubs === false) {
		addMyStyle("hide-stubs",`
		.postCell:has(> span.unhideButton.glowOnHover) {
			display: none;
		}
		`);
	}

	if (settings.revealSpoilerText=="on") {
		addMyStyle("reveal-spoilers",`
			span.spoiler { color: white }
		`)
	} else if (settings.revealSpoilerText=="madoka") {
		addMyStyle("reveal-spoilers",`
			span.spoiler:not(:hover) {
				color: white;
				font-family:MadokaRunes!important;
			}
		`)
	}

	// Add functionality to apply the custom spoiler image CSS
	if (settings.spoilerImageType=="thread" || settings.spoilerImageType=="threadAlt") {
		let spoilerImageUrl = null;

		if (settings.spoilerImageType=="thread") {
			const spoilerLink = Array.from(document.querySelectorAll('a.originalNameLink[download^="ThreadSpoiler"]')).find(link => /\.(jpg|png|webp)$/i.test(link.download));
			spoilerImageUrl = spoilerLink ? spoilerLink.href : null;
		} else if (settings.spoilerImageType=="threadAlt") {
			const altSpoilerLink = Array.from(document.querySelectorAll('a.originalNameLink[download^="ThreadSpoilerAlt"]')).find(link => /\.(jpg|png|webp)$/i.test(link.download));
			spoilerImageUrl = altSpoilerLink ? altSpoilerLink.href : null;
		}

		if (spoilerImageUrl) {
			addMyStyle("thread-spoiler-css", `
				${settings.overrideBoardSpoilerImage ? '.uploadCell:not(.expandedCell) a.imgLink:has(>img[src*="/custom.spoiler"]),' : "" }
				.uploadCell:not(.expandedCell) a.imgLink:has(>img[src="/spoiler.png"]) {
					background-image: url("${spoilerImageUrl}");
					background-size: cover;
					outline: dashed 2px #ff0000f5;
					& > img {
						opacity: 0;
					}
				}
			`);
		}
	}
	else if (settings.spoilerImageType=="kachina") {
		addMyStyle("kachinaSpoilers",`
			${settings.overrideBoardSpoilerImage ? '.uploadCell:not(.expandedCell) a.imgLink:has(>img[src*="/custom.spoiler"]),' : "" }
			.uploadCell:not(.expandedCell) a.imgLink:has(>img[src="/spoiler.png"]) {    
				background-size: cover; 
				margin-right:5px;    
				background-image: url("data:image/jpeg;base64,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");
				& > img {        
					opacity: 1;        
					transform: translate(0, -25%) scale(0.5);    
				}
			}
		`)
	}
})();