Greasy Fork

AniList Tier Labels

Adds a tier badge next to ratings on Anilist, including Mean Score. Supports different scoring systems and colors the score number according to the tier.

目前为 2025-02-13 提交的版本。查看 最新版本

// ==UserScript==
// @name         AniList Tier Labels
// @namespace    http://tampermonkey.net/
// @version      2.3
// @description  Adds a tier badge next to ratings on Anilist, including Mean Score. Supports different scoring systems and colors the score number according to the tier.
// @match        *://anilist.co/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // Tier definitions with colors
    const tiers = [
        { min: 95, max: 100, label: 'S+', color: '#FFD700', textColor: '#000000' },
        { min: 85, max: 94.9, label: 'S', color: '#ff7f00', textColor: '#FFFFFF' },
        { min: 75, max: 84.9, label: 'A', color: '#aa00ff', textColor: '#FFFFFF' },
        { min: 65, max: 74.9, label: 'B', color: '#007fff', textColor: '#FFFFFF' },
        { min: 55, max: 64.9, label: 'C', color: '#00aa00', textColor: '#FFFFFF' },
        { min: 41, max: 54.9, label: 'D', color: '#aaaaaa', textColor: '#FFFFFF' },
        { min: 0,  max: 40.9, label: 'F', color: '#666666', textColor: '#FFFFFF' }
    ];

    function getTier(rating) {
        if (rating === 0) return null; // Skip if the score is 0
        return tiers.find(tier => rating >= tier.min && rating <= tier.max) || null;
    }

    function createBadge(tier, isBlockView = false) {
        let badge = document.createElement('span');
        badge.textContent = tier.label;
        badge.style.cssText = `
            background-color: ${tier.color};
            color: ${tier.textColor};
            font-size: ${isBlockView ? '10px' : '12px'};
            font-weight: bold;
            padding: ${isBlockView ? '1px 4px' : '2px 6px'};
            border-radius: 4px;
            display: inline-block;
            margin-left: 5px;
            vertical-align: middle;
            white-space: nowrap;
        `;
        return badge;
    }

    function getScoreSystem() {
        const container = document.querySelector('.content.container');
        if (container) {
            if (container.querySelector('.medialist.table.POINT_100')) return 'POINT_100';
            if (container.querySelector('.medialist.table.POINT_10_DECIMAL')) return 'POINT_10';
            if (container.querySelector('.medialist.table.POINT_5')) return 'POINT_5';
        }
        return 'UNKNOWN';
    }

    function normalizeScore(score, scoreSystem, isPercentage = false) {
        const numericScore = parseFloat(score);
        if (isNaN(numericScore)) return null;

        // If it's already a 0-100 percentage, just return
        if (isPercentage) {
            return numericScore;
        }

        // Otherwise, convert based on the scoring system
        switch (scoreSystem) {
            case 'POINT_100':
                return numericScore;
            case 'POINT_10':
                return numericScore * 10;
            case 'POINT_5':
                return numericScore * 20;
            default:
                return numericScore * 10;
        }
    }

    function processScoreElement(el, isPercentage = false, isBlockView = false) {
        if (el.dataset.tierModified) return;
        el.dataset.tierModified = "true";

        const scoreSystem = getScoreSystem();
        let ratingText = el.getAttribute('score') || el.innerText.trim().replace('%', '');
        let normalizedRating = normalizeScore(ratingText, scoreSystem, isPercentage);
        if (normalizedRating === null) return;

        let tier = getTier(normalizedRating);
        if (tier) {
            // Build a small inline container
            const container = document.createElement('div');
            container.style.cssText = `
                display: inline-flex;
                align-items: center;
                gap: 4px;
                ${isBlockView ? 'background-color: rgba(0, 0, 0, 0.5); padding: 2px 6px; border-radius: 4px; overflow: hidden;' : ''}
            `;

            const scoreEl = document.createElement('span');
            scoreEl.textContent = isPercentage ? `${ratingText}%` : ratingText;
            scoreEl.style.color = tier.color; // Color the score number with the tier's color

            container.appendChild(scoreEl);
            container.appendChild(createBadge(tier, isBlockView));

            // Clear out the original text and append the container
            el.textContent = '';
            el.appendChild(container);
        }
    }

    function addTierIndicators() {
        // 1) List View (Decimal Scores)
        document.querySelectorAll('.score:not(.media-card .score)').forEach(el => {
            processScoreElement(el, false, false);
        });

        // 2) Block View (Media Cards)
        document.querySelectorAll('.entry-card .score').forEach(el => {
            processScoreElement(el, false, true);
        });

        // 3) Average / Mean Score (Profile Stats, etc.)
        document.querySelectorAll('.data-set').forEach(dataSet => {
            const label = dataSet.querySelector('.type');
            const value = dataSet.querySelector('.value');
            if (
                label &&
                value &&
                !value.dataset.tierModified &&
                (label.innerText.includes('Average Score') || label.innerText.includes('Mean Score'))
            ) {
                processScoreElement(value, true, false);
            }
        });

        // 4) Top 100 View
        document.querySelectorAll('.row.score').forEach(row => {
            // We'll look for a .percentage that isn't purely .popularity
            const percentageEl = row.querySelector('.percentage');
            if (!percentageEl || percentageEl.classList.contains('popularity') || percentageEl.dataset.tierModified) {
                return;
            }
            percentageEl.dataset.tierModified = "true";

            // The "X users" sub-row is typically a child: <div class="sub-row popularity">...</div>
            // We only remove the text node that says "91%", preserving the sub-row popularity.
            const childNodes = Array.from(percentageEl.childNodes);
            // The numeric rating is usually a text node
            const textNode = childNodes.find(n => n.nodeType === Node.TEXT_NODE && n.textContent.trim() !== '');
            if (!textNode) return;

            // Extract the numeric rating from the text node
            const ratingText = textNode.textContent.trim().replace('%', '');
            const numericRating = parseFloat(ratingText);
            if (isNaN(numericRating)) return;

            // Determine the tier
            const tier = getTier(numericRating);
            if (!tier) return;

            // Remove the original text node so we can replace it with a badge + colored text
            textNode.remove();

            // Build a small inline-flex container for rating + badge
            const ratingWrapper = document.createElement('div');
            ratingWrapper.style.display = 'inline-flex';
            ratingWrapper.style.alignItems = 'center';
            ratingWrapper.style.gap = '6px';

            // The rating text
            const textSpan = document.createElement('span');
            textSpan.textContent = numericRating + '%';
            textSpan.style.color = tier.color;

            ratingWrapper.appendChild(textSpan);
            ratingWrapper.appendChild(createBadge(tier));

            // Insert this wrapper before the popularity sub-row if it exists
            const popularityEl = percentageEl.querySelector('.sub-row.popularity');
            if (popularityEl) {
                percentageEl.insertBefore(ratingWrapper, popularityEl);
            } else {
                // Otherwise, just append to the .percentage container
                percentageEl.appendChild(ratingWrapper);
            }
        });
    }

    function initializeScript() {
        addTierIndicators();
        const statsObserver = new MutationObserver(() => {
            // On any DOM change, try to add indicators again
            addTierIndicators();
        });
        statsObserver.observe(document.body, {
            childList: true,
            subtree: true,
            characterData: true
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initializeScript);
    } else {
        initializeScript();
    }

    // Re-run when navigating around the site via history
    window.addEventListener('popstate', () => {
        setTimeout(addTierIndicators, 100);
    });

    const pushState = history.pushState;
    history.pushState = function () {
        pushState.apply(history, arguments);
        setTimeout(addTierIndicators, 100);
    };
})();