Greasy Fork 支持简体中文。

AttachHowOldtoUserinPosts

Show how old a user is in posts

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