Greasy Fork

AniList Tier Labels

Adds a tier badge next to ratings on AniList.

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

// ==UserScript==
// @name         AniList Tier Labels
// @namespace    http://tampermonkey.net/
// @version      2.4
// @description  Adds a tier badge next to ratings on AniList.
// @match        *://anilist.co/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    /*** SETTINGS MANAGEMENT ***/
    const defaultSettings = {
        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' }
        ],
        enableRatingTextColor: true
    };

    function loadSettings() {
        try {
            const stored = localStorage.getItem('anilistTierLabelsSettings');
            return stored ? Object.assign({}, defaultSettings, JSON.parse(stored)) : defaultSettings;
        } catch {
            return defaultSettings;
        }
    }
    function saveSettings() {
        localStorage.setItem('anilistTierLabelsSettings', JSON.stringify(userSettings));
    }

    let userSettings = loadSettings();

    /*** TIER INDICATOR LOGIC ***/
    function getTier(rating) {
        if (rating === 0) return null; // skip if the score is 0
        return userSettings.tiers.find(tier => rating >= tier.min && rating <= tier.max) || null;
    }

    function createBadge(tier, isBlockView = false) {
        const 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 (isPercentage) {
            return numericScore;
        }
        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('%', '');
        const normalizedRating = normalizeScore(ratingText, scoreSystem, isPercentage);
        if (normalizedRating === null) return;

        const tier = getTier(normalizedRating);
        if (tier) {
            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;
            if (userSettings.enableRatingTextColor) {
                scoreEl.style.color = tier.color;
            }
            container.appendChild(scoreEl);
            container.appendChild(createBadge(tier, isBlockView));

            el.textContent = '';
            el.appendChild(container);
        }
    }

    function addTierIndicators() {
        // 1) List view
        document.querySelectorAll('.score:not(.media-card .score)').forEach(el => {
            processScoreElement(el, false, false);
        });
        // 2) Block view (media-card)
        document.querySelectorAll('.entry-card .score').forEach(el => {
            processScoreElement(el, false, true);
        });
        // 3) Average/Mean Score
        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 => {
            const percentageEl = row.querySelector('.percentage');
            if (!percentageEl || percentageEl.classList.contains('popularity') || percentageEl.dataset.tierModified) {
                return;
            }
            percentageEl.dataset.tierModified = "true";
            const childNodes = Array.from(percentageEl.childNodes);
            const textNode = childNodes.find(n => n.nodeType === Node.TEXT_NODE && n.textContent.trim() !== '');
            if (!textNode) return;
            const ratingText = textNode.textContent.trim().replace('%', '');
            const numericRating = parseFloat(ratingText);
            if (isNaN(numericRating)) return;
            const tier = getTier(numericRating);
            if (!tier) return;

            textNode.remove();
            const ratingWrapper = document.createElement('div');
            ratingWrapper.style.display = 'inline-flex';
            ratingWrapper.style.alignItems = 'center';
            ratingWrapper.style.gap = '6px';

            const textSpan = document.createElement('span');
            textSpan.textContent = numericRating + '%';
            if (userSettings.enableRatingTextColor) {
                textSpan.style.color = tier.color;
            }
            ratingWrapper.appendChild(textSpan);
            ratingWrapper.appendChild(createBadge(tier));
            const popularityEl = percentageEl.querySelector('.sub-row.popularity');
            if (popularityEl) {
                percentageEl.insertBefore(ratingWrapper, popularityEl);
            } else {
                percentageEl.appendChild(ratingWrapper);
            }
        });
    }

    /*** SETTINGS PANEL (APPENDED TO DEVELOPER PAGE) ***/
    function renderTierLabelSettingsInDeveloper() {
        // If already added, skip
        if (document.getElementById('tier-label-settings-container')) return;

        // We'll append to the .content area or a .card area on the developer page
        const devContent = document.querySelector('.content');
        if (!devContent) return;

        const isDark = document.body.classList.contains('site-theme-dark');

        // Container
        const container = document.createElement('div');
        container.id = 'tier-label-settings-container';
        container.style.marginTop = '20px';
        container.style.padding = '16px';
        container.style.border = '1px solid ' + (isDark ? '#151f2e' : '#fff');
        container.style.borderRadius = '6px';
        container.style.backgroundColor = isDark ? '#151f2e' : '#f9f9f9';
        container.style.color = isDark ? '#9cadbd' : '#5c728a';

        // Title
        const title = document.createElement('h3');
        title.textContent = 'AniList Tier Labels Settings';
        container.appendChild(title);

        // Toggle rating text color
        const toggleContainer = document.createElement('div');
        toggleContainer.style.marginBottom = '10px';

        const toggleLabel = document.createElement('label');
        toggleLabel.textContent = 'Enable rating text color: ';
        const toggleCheckbox = document.createElement('input');
        toggleCheckbox.type = 'checkbox';
        toggleCheckbox.checked = userSettings.enableRatingTextColor;
        toggleCheckbox.addEventListener('change', (e) => {
            userSettings.enableRatingTextColor = e.target.checked;
            saveSettings();
            refreshAllScores();
        });
        toggleLabel.appendChild(toggleCheckbox);
        toggleContainer.appendChild(toggleLabel);
        container.appendChild(toggleContainer);

        // Tiers section
        const tiersTitle = document.createElement('h4');
        tiersTitle.textContent = 'Tier Ranges & Colors';
        container.appendChild(tiersTitle);

        userSettings.tiers.forEach((tier, index) => {
            const tierBox = document.createElement('div');
            tierBox.style.padding = '8px';
            tierBox.style.marginBottom = '10px';
            tierBox.style.border = '1px solid ' + (isDark ? '#546575' : '#ddd');
            tierBox.style.borderRadius = '4px';

            const header = document.createElement('strong');
            header.textContent = `Tier ${index + 1}`;
            header.style.display = 'block';
            header.style.marginBottom = '6px';
            tierBox.appendChild(header);

            // fields: min, max, label, color, textColor
            const fields = [
                { label: 'Min', key: 'min', type: 'number', step: 'any', width: '60px' },
                { label: 'Max', key: 'max', type: 'number', step: 'any', width: '60px' },
                { label: 'Label', key: 'label', type: 'text', width: '60px' },
                { label: 'Badge Color', key: 'color', type: 'color', width: '50px' },
                { label: 'Text Color', key: 'textColor', type: 'color', width: '50px' }
            ];

            fields.forEach(field => {
                const row = document.createElement('div');
                row.style.marginBottom = '6px';

                const lbl = document.createElement('label');
                lbl.textContent = field.label + ': ';

                const input = document.createElement('input');
                input.type = field.type;
                input.value = tier[field.key];
                input.style.width = field.width;
                if (field.type === 'number') {
                    input.step = field.step;
                }

                // Dark/Light mode input styling
                if (field.type !== 'checkbox') {
                    // For color inputs, you might prefer to keep default
                    // But let's also apply a subtle border, text color, etc.
                    input.style.backgroundColor = isDark ? '#151f2e' : '#fff';
                    input.style.color = isDark ? '#fff' : '#000';
                    input.style.border = '1px solid ' + (isDark ? '#555' : '#ccc');
                    input.style.borderRadius = '4px';
                    input.style.padding = '2px 4px';
                }

                input.addEventListener('change', (e) => {
                    let newVal = e.target.value;
                    if (field.type === 'number') {
                        newVal = parseFloat(newVal);
                    }
                    userSettings.tiers[index][field.key] = newVal;
                    saveSettings();
                    refreshAllScores();
                });

                lbl.appendChild(input);
                row.appendChild(lbl);
                tierBox.appendChild(row);
            });

            container.appendChild(tierBox);
        });

        devContent.appendChild(container);
    }

    function refreshAllScores() {
        // Remove data-tierModified so they get recalculated
        document.querySelectorAll('[data-tierModified]').forEach(el => {
            delete el.dataset.tierModified;
        });
        addTierIndicators();
    }

    /*** INIT SCRIPT & WATCH FOR PAGE CHANGES ***/
    function initializeScript() {
        addTierIndicators();
        const observer = new MutationObserver(() => addTierIndicators());
        observer.observe(document.body, { childList: true, subtree: true, characterData: true });
    }

    // Show/hide settings panel depending on current page
    function onPageLoadOrNav() {
        // Always do tier indicators
        addTierIndicators();

        // If on developer page, render settings
        if (window.location.pathname.startsWith('/settings/developer')) {
            renderTierLabelSettingsInDeveloper();
        } else {
            // If we leave developer page, remove the container if present
            const old = document.getElementById('tier-label-settings-container');
            if (old) old.remove();
        }
    }

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

    // Handle SPA navigation
    window.addEventListener('popstate', () => {
        setTimeout(onPageLoadOrNav, 100);
    });
    const originalPushState = history.pushState;
    history.pushState = function () {
        originalPushState.apply(history, arguments);
        setTimeout(onPageLoadOrNav, 100);
    };
})();