Shows a clan icon next to any username on the site that belongs to your clan, caches clan list & icon daily
当前为
// ==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);
})();