LynxChan Extended Minus Minus

LynxChan Extended with even more features

目前為 2025-04-18 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         LynxChan Extended Minus Minus
// @namespace    https://rentry.org/8chanMinusMinus
// @version      1.36
// @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)`
			}
		},
		useExtraStylingFixes:{
			default:true,
			desc:"Apply some styling fixes (Mark your posts and replies, smaller thumbnails etc.)"
		},
		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 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.`
			}
		},
		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."
		},
		/*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);

	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;
		right: 100px;
		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;
	}
	`);

	// 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;
			}
		}
	} else if (ev.key == "Escape") {
		//Because greasemonkey cannot access the JS of the page we have to do some funny stuff
		document.getElementById("quick-reply").querySelector(".close-btn").click()
	}
}
document.getElementById("qrbody").addEventListener("keydown", replyKeyboardShortcuts);

	// Create markers 1 second after page load
	setTimeout(() => {
		recreateScrollMarkers();
	}, 1500);
	if (settings.showPostIndex) {
		setTimeout(() => {
			addPostCount();
		}, 1400);
	}

	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;">LynxChan Extended-- Options</h3><br>
		`;
		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>"
				menu.innerHTML += html;
			} else {

				menu.innerHTML += `
				<label>
					<input type="checkbox" id="${name}" ${settings[name] ? "checked" : ""}>
					${setting.desc}
				</label><br><br>`
			}
		})
		menu.innerHTML += `
			<button id="saveSettings">Save</button>
			<button id="closeMenu">Close</button>
		`
		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() {
		document.querySelector("#navLinkSpan > .settingsButton").insertAdjacentHTML("afterend", `
		<span>/</span>
		<a id="navigation-lynxextended" class="lynxExtendedSettings" title="LynxChan Extended Settings"
			style="width: 13px;height: 13px;display: inline-block;fill: var(--link-color); vertical-align: middle;margin-left: 1px;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
				<path
					d="M352 320c88.4 0 160-71.6 160-160c0-15.3-2.2-30.1-6.2-44.2c-3.1-10.8-16.4-13.2-24.3-5.3l-76.8 76.8c-3 3-7.1 4.7-11.3 4.7L336 192c-8.8 0-16-7.2-16-16l0-57.4c0-4.2 1.7-8.3 4.7-11.3l76.8-76.8c7.9-7.9 5.4-21.2-5.3-24.3C382.1 2.2 367.3 0 352 0C263.6 0 192 71.6 192 160c0 19.1 3.4 37.5 9.5 54.5L19.9 396.1C7.2 408.8 0 426.1 0 444.1C0 481.6 30.4 512 67.9 512c18 0 35.3-7.2 48-19.9L297.5 310.5c17 6.2 35.4 9.5 54.5 9.5zM80 408a24 24 0 1 1 0 48 24 24 0 1 1 0-48z">
				</path>
			</svg>
		</a>
		`);
		document.querySelector("#navigation-lynxextended").addEventListener("click", openMenu);
	}

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

	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"))
		
		for (let i=0; i<posts.length; i++)
		{
			//Already added, ignore
			if (posts[i].querySelector(".postNum")) {
				continue;
			}

			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 revealSpoilerImages = function() {
		const spoilers = document.querySelectorAll(".imgLink > img[src='/spoiler.png']");
		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";
		});
	}

	if (settings.showScrollbarMarkers || settings.showPostIndex) {
		const observer = new MutationObserver((mt_callback) => {
			mt_callback.forEach(mut => {
				if (mut.type=="childList") {
					//console.log("MutationObserver!!!");
					// Recreate markers because the page grew taller. Is this heavy? probably not.
					recreateScrollMarkers();
					if (settings.showPostIndex)
						addPostCount();
					if (settings.spoilerImageType=="reveal")
						revealSpoilerImages();
				}
			})
		})
		observer.observe(document.querySelector(".divPosts"), {'childList':true})

		// I'm not sure why but this doesn't work
		// // Add a second observer for #threadList (new posts)
		// const threadObserver = new MutationObserver((mutationsList) => {
		// 	for (const mutation of mutationsList) {
		// 		if (mutation.type === 'childList') {
		// 			mutation.addedNodes.forEach((node) => {
		// 				if (node.classList && node.classList.contains("postCell")) {
		// 					console.log("ThreadObverver!!!")
		// 					// Recreate markers because the page grew taller. Is this heavy? probably not.
		// 					recreateScrollMarkers();
		// 					if (settings.showPostIndex)
		// 						addPostCount();
		// 					if (settings.spoilerImageType=="reveal")
		// 						revealSpoilerImages();
		// 				}
		// 			});
		// 		}
		// 	}
		// });
		// const threadList = document.querySelector("#threadList");
		// if (threadList) {
		// 	threadObserver.observe(threadList, { 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;
			}

			/*.greenText {
				filter: brightness(110%);
			}*/

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