// ==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 // @downloadURL 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:
...
// 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); }; })();