AttachHowOldtoUserinPosts

Show how old a user is in posts

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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