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-19 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         AnimeStars Clan Highlighter
// @namespace    animestars
// @author       Allystark
// @version      1.2
// @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 = '🐇';

    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 {}
        }
        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');

        const members = Array.from(doc.querySelectorAll('.club__member-name'))
                             .map(el => el.textContent.trim());
        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;
    }

    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',
            borderRadius: '0px'
        });

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

    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);
        });
        // 5) trade list items
        document.querySelectorAll('a.trade__list-item').forEach(el => {
            const txt = el.querySelector('.trade__list-name')?.textContent.trim() || '';
            const avatar = el.querySelector('div.trade__list-image');
            const name = txt.replace(/^от |^для /, '').trim();
            if (name && members.includes(name)) annotateEl(avatar, icon);
        });
        // 6) notification blocks
        document.querySelectorAll('div.notification').forEach(el => {
            const link = el.querySelector('.notification__text a[href^="/user/"]');
            const name = link?.textContent.trim();
            if (name && members.includes(name)) annotateEl(el, icon, 8, 8);
        });
        // 7) trade header
        document.querySelectorAll('div.trade__header').forEach(div => {
            const name = div.querySelector('a.trade__header-name')?.textContent.trim() || '';
            const avatar = div.querySelector('a.trade__header-avatar');
            if (name && members.includes(name)) annotateEl(avatar, icon, -8, -8);
        });
        // 8) history names
        document.querySelectorAll('div.history__name a[href^="/user/"]')
            .forEach(a => {
                const name = a.textContent.trim();
                if (name && members.includes(name)) annotateEl(a.parentElement.parentElement, icon, 0, 0);
            });
    }

    const { members, icon } = await getClanData();
    scanAndAnnotate(members, icon);

    new MutationObserver(() => scanAndAnnotate(members, icon))
        .observe(document.body, { childList: true, subtree: true });
})();