PlatesMania Like Avatars + Relative Time

Show avatars + relative timestamps in like list

当前为 2025-08-17 提交的版本,查看 最新版本

// ==UserScript==
// @name         PlatesMania Like Avatars + Relative Time
// @namespace    pm-like-avatars
// @version      1.1
// @description  Show avatars + relative timestamps in like list
// @match        https://platesmania.com/userXXXXXX
// @run-at       document-idle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function () {
    "use strict";

    const CACHE_KEY = "pm_profile_pic_cache_v1";
    const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;

    GM_addStyle(`
    .pm-like-avatar {
  width: 20px !important;
  height: 20px !important;
  border-radius: 4px !important;   /* <-- change this */
  object-fit: cover !important;
  vertical-align: text-bottom !important;
  margin-right: 6px !important;
  overflow: hidden !important;
}

  `);

    // ===== Cache stuff =====
    function readCache() {
        try {
            const raw = typeof GM_getValue === "function" ? GM_getValue(CACHE_KEY, "{}") : localStorage.getItem(CACHE_KEY) || "{}";
            return JSON.parse(raw);
        } catch {
            return {};
        }
    }
    function writeCache(obj) {
        const raw = JSON.stringify(obj);
        if (typeof GM_setValue === "function") GM_setValue(CACHE_KEY, raw);
        else localStorage.setItem(CACHE_KEY, raw);
    }
    function getFromCache(userId) {
        const cache = readCache();
        const entry = cache[userId];
        if (!entry) return null;
        if (Date.now() - entry.ts > MAX_AGE_MS) {
            delete cache[userId];
            writeCache(cache);
            return null;
        }
        return entry.url;
    }
    function putInCache(userId, url) {
        const cache = readCache();
        cache[userId] = { url, ts: Date.now() };
        writeCache(cache);
    }

    // ===== Avatar processing =====
    async function fetchProfileAvatar(userPath) {
        const res = await fetch(new URL(userPath, location.origin), { credentials: "same-origin" });
        if (!res.ok) throw new Error(res.status);
        const html = await res.text();
        const doc = new DOMParser().parseFromString(html, "text/html");
        const img = doc.querySelector(".profile-img[src]");
        return img ? new URL(img.getAttribute("src"), location.origin).toString() : null;
    }

    function addAvatarToRow(rowEl, avatarUrl) {
        if (!rowEl || rowEl.dataset.pmAvatarAdded === "1") return;
        const strong = rowEl.querySelector("strong");
        if (!strong) return;
        const userLink = strong.querySelector('a[href^="/user"]');
        if (!userLink) return;
        if (strong.querySelector("img.pm-like-avatar")) {
            rowEl.dataset.pmAvatarAdded = "1";
            return;
        }
        const img = document.createElement("img");
        img.className = "pm-like-avatar";
        img.src = avatarUrl;
        strong.insertBefore(img, userLink);
        rowEl.dataset.pmAvatarAdded = "1";
    }

    async function processRow(rowEl) {
        if (rowEl.dataset.pmAvatarAdded === "1") return;
        const userLink = rowEl.querySelector('strong > a[href^="/user"]');
        if (!userLink) return;
        const userHref = userLink.getAttribute("href");
        const userId = (userHref.match(/user(\d+)/) || [])[1];
        if (!userId) return;

        const cached = getFromCache(userId);
        if (cached) {
            addAvatarToRow(rowEl, cached);
            return;
        }

        try {
            const url = await fetchProfileAvatar(userHref);
            if (url) {
                putInCache(userId, url);
                addAvatarToRow(rowEl, url);
            }
        } catch {}
    }

    function processAllAvatars(root = document) {
        root.querySelectorAll(".col-xs-12.margin-bottom-5.bg-info").forEach(processRow);
    }

    // ===== Relative time =====
    function formatRelative(date) {
        const diff = (Date.now() - date.getTime()) / 1000;
        if (diff < 60) return `${Math.floor(diff)}s ago`;
        if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
        if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
        if (diff < 30 * 86400) return `${Math.floor(diff / 86400)}d ago`;
        return date.toLocaleDateString();
    }

    function parseMoscowTime(str) {
        // format: YYYY-MM-DD HH:mm:ss (Moscow time, UTC+3 permanent)
        const [datePart, timePart] = str.split(" ");
        const [y, m, d] = datePart.split("-").map(Number);
        const [hh, mm, ss] = timePart.split(":").map(Number);
        // create as if it's UTC, then shift from UTC+3
        const utcMs = Date.UTC(y, m - 1, d, hh - 3, mm, ss);
        return new Date(utcMs);
    }

    function processAllTimes(root = document) {
        root.querySelectorAll(".col-xs-12.margin-bottom-5.bg-info small").forEach((el) => {
            if (el.dataset.pmTimeDone === "1") return;
            const txt = el.textContent.trim();
            if (!/^\d{4}-\d{2}-\d{2}/.test(txt)) return;
            const date = parseMoscowTime(txt);
            el.textContent = formatRelative(date);
            el.dataset.pmTimeDone = "1";
        });
    }

    // ===== Observe dynamic changes =====
    function setupObserver() {
        const container = document.querySelector("#mCSB_2_container, #content") || document.body;
        const obs = new MutationObserver((mutations) => {
            mutations.forEach((m) => {
                m.addedNodes.forEach((n) => {
                    if (!(n instanceof HTMLElement)) return;
                    processAllAvatars(n);
                    processAllTimes(n);
                });
            });
        });
        obs.observe(container, { childList: true, subtree: true });
    }

    // Kick off
    processAllAvatars(document);
    processAllTimes(document);
    setupObserver();
    setInterval(() => {
        processAllAvatars(document);
        processAllTimes(document);
    }, 1500);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址