// ==UserScript==
// @name AttachHowOldtoUserinPosts
// @namespace https://jirehlov.com
// @version 2.1
// @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"
}
];
let requestCount = 0;
let usersToFetch = [];
const sortedUserIds = Object.keys(localStorage).filter(key => key.startsWith("userAge_")).map(key => key.replace("userAge_", "")).filter(value => Number.isInteger(parseInt(value, 10))).map(value => parseInt(value, 10)).sort((a, b) => a - b);
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 findClosestMatchingDates(userId) {
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) {
let lowerDate = localStorage.getItem("userAge_" + lower);
let upperDate = localStorage.getItem("userAge_" + upper);
if (lowerDate === upperDate) {
return lowerDate;
}
}
return null;
}
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 registrationDate = doc.querySelector("ul.network_service li:first-child span.tip")?.textContent?.replace(/加入/g, "").trim();
if (registrationDate) {
const userId = userLink.split("/").pop();
localStorage.setItem("userAge_" + userId, registrationDate);
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)) {
let 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);
Object.keys(userAges).forEach(key => {
const storageKey = key.startsWith("userAge_") ? key : "userAge_" + key;
localStorage.setItem(storageKey, userAges[key]);
});
alert("导入成功");
} catch (error) {
alert("无效的JSON文件\uFF1A", error);
}
};
reader.readAsText(file);
}
};
input.click();
}
function exportUserAges() {
const userAges = {};
Object.keys(localStorage).forEach(key => {
if (key.startsWith("userAge_")) {
userAges[key.replace("userAge_", "")] = localStorage.getItem(key);
}
});
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const entryCount = Object.keys(userAges).length;
const filename = `userAges_${ timestamp }_${ entryCount }entries.json`;
const blob = new Blob([JSON.stringify(userAges, 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() {
Object.keys(localStorage).forEach(key => {
if (key.startsWith("userAge_")) {
localStorage.removeItem(key);
}
});
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);
}
$("strong a.l:not(.avatar)").each(function () {
const userLink = $(this).attr("href");
const userId = userLink.split("/").pop();
const avatarMatch = $(this).closest("div.inner").prev("a.avatar").find("span.avatarNeue").attr("style")?.match(/\/(\d+)\.jpg/);
let realUserId = avatarMatch ? avatarMatch[1] : userId;
let storedDate = localStorage.getItem("userAge_" + userId);
if (!storedDate) {
if (!isNaN(userId)) {
storedDate = findClosestMatchingDates(userId);
if (storedDate) {
console.log(`${ userId } ${ storedDate }`);
}
} else if (!isNaN(realUserId)) {
storedDate = findClosestMatchingDates(realUserId);
if (storedDate) {
console.log(`${ userId } ${ realUserId } ${ storedDate }`);
}
}
}
if (storedDate) {
localStorage.setItem("userAge_" + userId, storedDate);
displayUserAge(userId, storedDate);
} else {
usersToFetch.push(userLink);
}
});
usersToFetch = [...new Set(usersToFetch)];
console.log("Users to fetch:", usersToFetch);
usersToFetch.forEach((userLink, index) => {
fetchAndStoreUserAge(userLink, index * delay);
});
const jsonURL = "https://jirehlov.com/userages.json";
function fetchUserAgesOnline() {
const lastFetchTime = localStorage.getItem("lastFetchedUserAges");
const now = Date.now();
if (!lastFetchTime || now - parseInt(lastFetchTime, 10) > 7 * 24 * 60 * 60 * 1000) {
fetch(jsonURL).then(response => response.json()).then(data => {
Object.keys(data).forEach(key => {
localStorage.setItem("userAge_" + key, data[key]);
});
localStorage.setItem("lastFetchedUserAges", now);
}).catch(error => console.error("Failed to fetch user ages:", error));
}
}
fetchUserAgesOnline();
}());