AnimeStars Clan Highlighter

Shows a clan icon next to any username on the site that belongs to your clan, caches clan list & icon daily

当前为 2025-05-18 提交的版本,查看 最新版本

// ==UserScript==
// @name         AnimeStars Clan Highlighter
// @namespace    animestars
// @author       Allystark
// @version      1.1
// @description  Shows a clan icon next to any username on the site that belongs to your clan, caches clan list & icon daily
// @match        https://astars.club/*
// @match        https://asstars1.astars.club/*
// @match        https://animestars.org/*
// @match        https://as1.astars.club/*
// @match        https://asstars.tv/*
// @grant        none
// ==/UserScript==

;(async function() {
    'use strict';

    const STORAGE_KEY = 'myClanData';
    const CACHE_DURATION = 24 * 60 * 60 * 1000; // 1 day
    const DEFAULT_ICON = '🐇'; // fallback symbol

    // load or refresh clan data (members list + icon URL)
    async function getClanData() {
        let raw = localStorage.getItem(STORAGE_KEY);
        if (raw) {
            try {
                const data = JSON.parse(raw);
                if (Date.now() - data.fetched < CACHE_DURATION) {
                    return data;
                }
            } catch {}
        }
        // find the “My Club” link in header menu
        const menu = document.querySelector('.lgn__menus .lgn__menu');
        const clubA = menu?.querySelector('a[href^="/clubs/"]');
        if (!clubA) return { members: [], icon: null, fetched: Date.now() };

        const res = await fetch(clubA.href, { credentials: 'include' });
        const html = await res.text();
        const doc = new DOMParser().parseFromString(html, 'text/html');

        // collect usernames
        const members = Array.from(doc.querySelectorAll('.club__member-name'))
                             .map(el => el.textContent.trim());
        // grab icon if present
        const iconEl = doc.querySelector('.club__name img');
        const icon = iconEl ? iconEl.src : null;

        const clanData = { members, icon, fetched: Date.now() };
        localStorage.setItem(STORAGE_KEY, JSON.stringify(clanData));
        return clanData;
    }

    // attach icon or symbol to bottom-left of container el
    function annotateEl(container, iconUrl, offsetBottom=-4, offsetLeft=-4) {
        // avoid double-annotation
        if (container.querySelector('.clan-marker')) return;
        container.style.position = container.style.position || 'relative';

        const marker = iconUrl
          ? document.createElement('img')
          : document.createElement('span');

        marker.className = 'clan-marker';
        Object.assign(marker.style, {
            position: 'absolute',
            bottom: `${offsetBottom}px`,
            left:   `${offsetLeft}px`,
            width:  iconUrl ? '16px' : 'auto',
            height: iconUrl ? '16px' : 'auto',
            fontSize: iconUrl ? '' : '16px',
            lineHeight: iconUrl ? '' : '1'
        });

        if (iconUrl) {
            marker.src = iconUrl;
            marker.alt = 'Clan icon';
        } else {
            marker.textContent = DEFAULT_ICON;
        }
        container.append(marker);
    }

    // scan various elements and annotate clan members
    function scanAndAnnotate(members, icon) {
        // 1) card-show__owner
        document.querySelectorAll('a.card-show__owner').forEach(a => {
            const name = a.querySelector('.card-show__owner-name')?.textContent.trim();
            if (name && members.includes(name)) annotateEl(a, icon);
        });
        // 2) profile__friends-item
        document.querySelectorAll('a.profile__friends-item').forEach(a => {
            const name = a.querySelector('.profile__friends-name')?.textContent.trim();
            if (name && members.includes(name)) annotateEl(a, icon);
        });
        // 3) ncard__user-name
        document.querySelectorAll('a.ncard__meta-item.ncard__user-name').forEach(a => {
            const name = Array.from(a.childNodes)
                              .filter(n => n.nodeType === Node.TEXT_NODE)
                              .map(n => n.textContent.trim()).join('');
            if (name && members.includes(name)) annotateEl(a, icon);
        });
        // 4) dropdown notifications
        document.querySelectorAll('div.dropdown-item').forEach(div => {
            const link = div.querySelector('span.font-weight-bold a[href^="/user/"]');
            const name = link?.textContent.trim();
            if (name && members.includes(name)) annotateEl(div, icon, 26, 8);
        });
    }

    // initial load + annotate
    const { members, icon } = await getClanData();
    scanAndAnnotate(members, icon);

    // re-annotate on DOM changes (SPA navigation)
    new MutationObserver(() => scanAndAnnotate(members, icon))
        .observe(document.body, { childList: true, subtree: true });

    // refresh clan data daily
    setInterval(async () => {
        const data = await getClanData();
        scanAndAnnotate(data.members, data.icon);
    }, CACHE_DURATION);
})();