// ==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);
};
})();