AttachHowOldtoUserinPosts

Show how old a user is in posts

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 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();
}());