AttachHowOldtoUserinPosts

Show how old a user is in posts

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         AttachHowOldtoUserinPosts
// @namespace    https://jirehlov.com
// @version      2.2
// @description  Show how old a user is in posts
// @author       Jirehlov
// @match        https://bgm.tv/*
// @match        https://chii.in/*
// @match        https://bangumi.tv/*
// @grant        none
// @license      MIT
// ==/UserScript==
(function () {
	const delay = 4000;
	const ageColors = [
		{
			threshold: 2,
			color: "#FFC966"
		},
		{
			threshold: 5,
			color: "#FFA500"
		},
		{
			threshold: 10,
			color: "#F09199"
		},
		{
			threshold: Infinity,
			color: "#FF0000"
		}
	];
	const DB_NAME = "UserAgesDB";
	const STORE_NAME = "userAges";
	function openDB() {
		return new Promise((resolve, reject) => {
			const request = indexedDB.open(DB_NAME, 1);
			request.onupgradeneeded = event => {
				const db = event.target.result;
				if (!db.objectStoreNames.contains(STORE_NAME)) {
					db.createObjectStore(STORE_NAME);
				}
			};
			request.onsuccess = () => resolve(request.result);
			request.onerror = () => reject(request.error);
		});
	}
	function getAll() {
		return openDB().then(db => {
			return new Promise((resolve, reject) => {
				const transaction = db.transaction(STORE_NAME, "readonly");
				const store = transaction.objectStore(STORE_NAME);
				const request = store.getAllKeys();
				request.onsuccess = () => resolve(request.result);
				request.onerror = () => reject(request.error);
			});
		});
	}
	function getAllKeys() {
		return openDB().then(db => {
			return new Promise((resolve, reject) => {
				const transaction = db.transaction(STORE_NAME, "readonly");
				const store = transaction.objectStore(STORE_NAME);
				const request = store.getAllKeys();
				request.onsuccess = () => resolve(request.result.map(key => parseInt(key, 10)).sort((a, b) => a - b));
				request.onerror = () => reject(request.error);
			});
		});
	}
	function getItem(key) {
		return openDB().then(db => {
			return new Promise((resolve, reject) => {
				const transaction = db.transaction(STORE_NAME, "readonly");
				const store = transaction.objectStore(STORE_NAME);
				const request = store.get(key.toString());
				request.onsuccess = () => resolve(request.result || null);
				request.onerror = () => reject(request.error);
			});
		});
	}
	function setItem(key, value) {
		return openDB().then(db => {
			return new Promise((resolve, reject) => {
				const transaction = db.transaction(STORE_NAME, "readwrite");
				const store = transaction.objectStore(STORE_NAME);
				store.put(value, key.toString());
				transaction.oncomplete = () => resolve();
				transaction.onerror = () => reject(transaction.error);
			});
		});
	}
	function setItems(entries) {
		return openDB().then(db => {
			return new Promise((resolve, reject) => {
				const transaction = db.transaction(STORE_NAME, "readwrite");
				const store = transaction.objectStore(STORE_NAME);
				for (const [key, value] of Object.entries(entries)) {
					store.put(value, key.toString());
				}
				transaction.oncomplete = () => resolve();
				transaction.onerror = () => reject(transaction.error);
			});
		});
	}
	function calculateAge(birthDate) {
		const [year, month, day] = birthDate.split("-").map(num => num.padStart(2, "0"));
		const d = new Date(`${ year }-${ month }-${ day }T00:00:00+08:00`);
		const now = new Date();
		let age = now.getUTCFullYear() - d.getUTCFullYear();
		if (now.getUTCMonth() < d.getUTCMonth() || now.getUTCMonth() === d.getUTCMonth() && now.getUTCDate() <= d.getUTCDate()) {
			age--;
		}
		return age;
	}
	function fetchAndStoreUserAge(userLink, delayTime) {
		setTimeout(() => {
			fetch(userLink, { credentials: "omit" }).then(response => response.text()).then(data => {
				const parser = new DOMParser();
				const doc = parser.parseFromString(data, "text/html");
				let registrationDateElement = doc.querySelector("ul.network_service li:first-child span.tip");
				let registrationDate = registrationDateElement ? registrationDateElement.textContent.replace(/加入/g, "").trim() : null;
				if (registrationDate) {
					const userId = userLink.split("/").pop();
					if (userId) {
						setItem(userId, registrationDate).then(() => displayUserAge(userId, registrationDate));
					}
				}
			}).catch(console.error);
		}, delayTime);
	}
	function displayUserAge(userId, registrationDate) {
		const userAnchor = $("strong a.l[href$='/user/" + userId + "']");
		if (userAnchor.length > 0 && userAnchor.next(".age-badge").length === 0) {
			const userAge = calculateAge(registrationDate);
			if (!isNaN(userAge)) {
				const badgeColor = ageColors.find(color => userAge <= color.threshold).color;
				const badge = $(`
				<span class="age-badge" style="
					background-color: ${ badgeColor };
					font-size: 11px;
					padding: 2px 5px;
					color: #FFF;
					border-radius: 100px;
					line-height: 150%;
					display: inline-block;
					position: relative;
					cursor: pointer;
				">${ userAge }年
					<span class="tooltip" style="
						visibility: hidden;
						background-color: rgba(0, 0, 0, 0.75);
						color: #fff;
						text-align: center;
						padding: 5px 8px;
						border-radius: 5px;
						position: absolute;
						bottom: 150%;
						left: 50%;
						transform: translateX(-50%);
						white-space: nowrap;
						font-size: 12px;
						opacity: 0;
						transition: opacity 0.3s;
						z-index: 1000;
					">${ registrationDate }</span>
				</span>
			`);
				badge.hover(function () {
					$(this).find(".tooltip").css({
						visibility: "visible",
						opacity: "1"
					});
				}, function () {
					$(this).find(".tooltip").css({
						visibility: "hidden",
						opacity: "0"
					});
				});
				userAnchor.after(badge);
			}
		}
	}
	function importUserAges() {
		const input = document.createElement("input");
		input.type = "file";
		input.accept = "application/json";
		input.onchange = function (event) {
			const file = event.target.files[0];
			if (file) {
				const reader = new FileReader();
				reader.onload = function (e) {
					try {
						const userAges = JSON.parse(e.target.result);
						setItems(userAges).then(() => alert("导入成功")).catch(error => alert("导入失败: " + error));
					} catch (error) {
						alert("无效的JSON文件: " + error);
					}
				};
				reader.readAsText(file);
			}
		};
		input.click();
	}
	function exportUserAges() {
		getAll().then(keys => {
			openDB().then(db => {
				const transaction = db.transaction(STORE_NAME, "readonly");
				const store = transaction.objectStore(STORE_NAME);
				const allData = {};
				let completed = 0;
				keys.forEach(key => {
					const request = store.get(key.toString());
					request.onsuccess = () => {
						allData[key] = request.result;
						completed++;
						if (completed === keys.length) {
							const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
							const filename = `userAges_${ timestamp }_${ keys.length }entries.json`;
							const blob = new Blob([JSON.stringify(allData, null, 2)], { type: "application/json" });
							const url = URL.createObjectURL(blob);
							const a = document.createElement("a");
							a.href = url;
							a.download = filename;
							a.click();
							URL.revokeObjectURL(url);
						}
					};
				});
			});
		});
	}
	function clearUserAges() {
		localStorage.removeItem("lastFetchedUserAges");
		openDB().then(db => {
			const transaction = db.transaction(STORE_NAME, "readwrite");
			const store = transaction.objectStore(STORE_NAME);
			store.clear();
			alert("所有用户生日数据已清除");
		});
	}
	const badgeUserPanel = document.querySelector("ul#badgeUserPanel");
	if (badgeUserPanel) {
		const importButton = document.createElement("a");
		importButton.href = "#";
		importButton.textContent = "导入用户生日数据";
		importButton.onclick = event => {
			event.preventDefault();
			importUserAges();
		};
		const exportButton = document.createElement("a");
		exportButton.href = "#";
		exportButton.textContent = "导出用户生日数据";
		exportButton.onclick = event => {
			event.preventDefault();
			exportUserAges();
		};
		const clearButton = document.createElement("a");
		clearButton.href = "#";
		clearButton.textContent = "清除用户生日数据";
		clearButton.onclick = event => {
			event.preventDefault();
			clearUserAges();
		};
		const listItem = document.createElement("li");
		listItem.appendChild(importButton);
		badgeUserPanel.appendChild(listItem);
		const exportListItem = document.createElement("li");
		exportListItem.appendChild(exportButton);
		badgeUserPanel.appendChild(exportListItem);
		const clearListItem = document.createElement("li");
		clearListItem.appendChild(clearButton);
		badgeUserPanel.appendChild(clearListItem);
	}
	const userLinks = [];
	$("strong a.l:not(.avatar)").each(function () {
		const userLink = $(this).attr("href");
		const userId = userLink.split("/").pop();
		const styleAttr = $(this).closest("div.inner").prev("a.avatar").find("span.avatarNeue").attr("style");
		const avatarMatch = styleAttr ? styleAttr.match(/\/(\d+)\.jpg/) : null;
		let realUserId = avatarMatch ? avatarMatch[1] : userId;
		userLinks.push({
			userLink,
			userId,
			realUserId
		});
	});
	function processUsersInWorker(userLinks) {
		return new Promise((resolve, reject) => {
			const worker = new Worker(URL.createObjectURL(new Blob([`
			let db;

const openDB = async (DB_NAME, STORE_NAME) => {
	if (!db) {
		db = await new Promise((resolve, reject) => {
			const request = indexedDB.open(DB_NAME, 1);
			request.onupgradeneeded = event => {
				const db = event.target.result;
				if (!db.objectStoreNames.contains(STORE_NAME)) {
					db.createObjectStore(STORE_NAME);
				}
			};
			request.onsuccess = () => resolve(request.result);
			request.onerror = () => reject(request.error);
		});
	}
	return db;
};

const getAllKeys = async (STORE_NAME) => {
	const transaction = db.transaction(STORE_NAME, "readonly");
	const store = transaction.objectStore(STORE_NAME);
	const request = store.getAllKeys();
	const keys = await new Promise((resolve, reject) => {
		request.onsuccess = () => resolve(request.result.map(key => parseInt(key, 10)).sort((a, b) => a - b));
		request.onerror = () => reject(request.error);
	});
	return keys;
};

const getItem = async (STORE_NAME, key) => {
	const transaction = db.transaction(STORE_NAME, "readonly");
	const store = transaction.objectStore(STORE_NAME);
	const request = store.get(key.toString());
	const item = await new Promise((resolve, reject) => {
		request.onsuccess = () => resolve(request.result || null);
		request.onerror = () => reject(request.error);
	});
	return item;
};

const findClosestMatchingDates = async (STORE_NAME, userId) => {
	const sortedUserIds = await getAllKeys(STORE_NAME);
	let lower = -1, upper = -1;
	for (let i = 0; i < sortedUserIds.length; i++) {
		if (sortedUserIds[i] < userId) {
			lower = sortedUserIds[i];
		}
		if (sortedUserIds[i] > userId) {
			upper = sortedUserIds[i];
			break;
		}
	}
	if (lower !== -1 && upper !== -1) {
		const [lowerDate, upperDate] = await Promise.all([
			getItem(STORE_NAME, lower),
			getItem(STORE_NAME, upper)
		]);
		return lowerDate === upperDate ? lowerDate : null;
	}
	return null;
};

self.onmessage = async function(event) {
	const { userLinks, DB_NAME, STORE_NAME } = event.data;
	await openDB(DB_NAME, STORE_NAME);
	const results = await Promise.all(userLinks.map(async ({ userLink, userId, realUserId }) => {
		let storedDate = await getItem(STORE_NAME, userId);
		let dateFromFindClosest = false;

		if (!storedDate) {
			const idToCheck = !isNaN(userId) ? userId : !isNaN(realUserId) ? realUserId : null;
			if (idToCheck !== null) {
				storedDate = await findClosestMatchingDates(STORE_NAME, idToCheck);
				if (storedDate) {
					dateFromFindClosest = true;
				}
			}
		}

		return { userId, storedDate, dateFromFindClosest };
	}));

	self.postMessage({ results });
};
		`], { type: "application/javascript" })));
			worker.onmessage = function (event) {
				resolve(event.data.results);
			};
			worker.onerror = function (error) {
				reject(error);
			};
			worker.postMessage({
				userLinks,
				DB_NAME,
				STORE_NAME
			});
		});
	}
	(async () => {
		try {
			const startTime = Date.now();
			const results = await processUsersInWorker(userLinks);
			const usersToFetch = [];
			const totalUsers = userLinks.length;
			let processedUsers = 0;
			let lastLoggedProgress = 0;
			results.forEach(({userId, storedDate, dateFromFindClosest}) => {
				if (storedDate) {
					if (dateFromFindClosest) {
						setItem(userId, storedDate);
					}
					displayUserAge(userId, storedDate);
				} else {
					usersToFetch.push(userLinks.find(link => link.userId === userId).userLink);
				}
				processedUsers++;
				const progress = (processedUsers / totalUsers * 100).toFixed(0);
				if (Date.now() - startTime > 10000 && progress - lastLoggedProgress >= 10) {
					console.log(`Progress: ${ progress }%`);
					lastLoggedProgress = progress;
				}
			});
			const uniqueUsersToFetch = [...new Set(usersToFetch)];
			if (uniqueUsersToFetch.length > 0) {
				console.log("Users to fetch:", uniqueUsersToFetch);
			}
			uniqueUsersToFetch.forEach((userLink, index) => {
				fetchAndStoreUserAge(userLink, index * delay);
			});
		} catch (error) {
			console.error("Error processing users in worker:", error);
		}
	})();
	const jsonURL = "https://jirehlov.com/userages.json";
	function fetchUserAgesOnline() {
		const lastFetchTime = parseInt(localStorage.getItem("lastFetchedUserAges"), 10);
		const now = Date.now();
		if (!lastFetchTime || now - lastFetchTime > 7 * 24 * 60 * 60 * 1000) {
			fetch(jsonURL).then(response => response.json()).then(data => setItems(data)).then(() => localStorage.setItem("lastFetchedUserAges", now.toString())).catch(error => console.error("Failed to fetch and save user ages:", error));
		}
	}
	fetchUserAgesOnline();
}());