Chortowod / Shiki Rating

// ==UserScript==
// @name         Shiki Rating
// @namespace    http://shikimori.one/
// @version      2.7.3
// @description  Добавляет рейтинг произведений на основе оценок пользователей шики (также при наведении не учитываются низкие оценки)
// @author       Chortowod
// @match        *://shikimori.org/*
// @match        *://shikimori.one/*
// @match        *://shikimori.me/*
// @icon         https://www.google.com/s2/favicons?domain=shikimori.one
// @license      MIT
// @require      https://gist.githubusercontent.com/arantius/3123124/raw/grant-none-shim.js
// @copyright    2024, Chortowod (https://openuserjs.org/users/Chortowod)
// @updateURL    https://openuserjs.org/meta/Chortowod/Shiki_Rating.meta.js
// @downloadURL  https://openuserjs.org/install/Chortowod/Shiki_Rating.user.js
// @require      https://gist.githubusercontent.com/Chortowod/814b010c68fc97e5f900df47bf79059c/raw/chtw_settings.js?v1
// @grant        none
// ==/UserScript==

let settings = new ChtwSettings('chtwRating', '<a target="_blank" href="https://openuserjs.org/scripts/Chortowod/Shiki_Rating">Рейтинг шикимори</a>');
let debug;

function initSettings() {
    settings.createOption('isDebug', 'Отладка', false);
    settings.createOption('lowest', 'Минимальный порог', 4, 'number');
    settings.createOption('starColor', 'Цвет звезд', '#a22123', 'color');
    settings.createOption('textColor', 'Цвет надписи', '#7b8084', 'color');
    debug = settings.getOption('isDebug');
}

function log(message) {
    if (debug) {
        console.log('Shiki Rating:');
        console.log(message);
    }
}

function getLocale() {
    return document.querySelector('body').getAttribute('data-locale');
}

function getLabelData() {
    return getLocale() === 'ru' ?
        {"0":"Невыносимо","1":"Хуже некуда","2":"Ужасно","3":"Очень плохо","4":"Плохо","5":"Более-менее","6":"Нормально","7":"Хорошо","8":"Отлично","9":"Великолепно","10":"Эпик вин!"} :
        {"0":"Unbearable","1":"Worst Ever","2":"Terrible","3":"Very Bad","4":"Bad","5":"So-so","6":"Fine","7":"Good","8":"Excellent","9":"Great","10":"Masterpiece!"};
}

function removeLastClass(domElement) {
    let classes = domElement.classList;
    classes.remove(classes.item(classes.length - 1));
}

function createElementFromHTML(htmlString) {
    var div = document.createElement('div');
    div.innerHTML = htmlString.trim();
    return div.firstChild;
}

function calculateShikiRating(scoreData, lowestRate = 0) {
    let sumScore = 0;
    let totalCount = 0;
    for (let i = 0; i < scoreData.length; i++) {
        if (scoreData[i][0] >= lowestRate) {
            sumScore += parseInt(scoreData[i][1]) * scoreData[i][0];
            totalCount += parseInt(scoreData[i][1]);
        }
    }
    log(sumScore);
    log(totalCount);
    let shikiScore = sumScore / totalCount;
    if (lowestRate) log('Оценка, не учитывая меньше '+lowestRate+': '+shikiScore);
    else log('Оценка: '+shikiScore);
    if (!shikiScore) return false;
    else return {'score': shikiScore.toFixed(2), 'scoreMath': Math.round(shikiScore), 'count': totalCount};
}

function scoreOnHover(eventType, score, total) {
    let shikiScoreElement = document.getElementById("shiki-score");
    let dataScore = shikiScoreElement.getAttribute(score);
    shikiScoreElement.addEventListener(eventType, function( event ) {
        shikiScoreElement.querySelector('.text-score .score-value').innerHTML = dataScore;
        let starElement = shikiScoreElement.querySelector("div.stars-container > div.stars.score");
        removeLastClass(starElement);
        starElement.classList.add("score-" + Math.round(dataScore));
        document.querySelector('.score-counter > strong').textContent = shikiScoreElement.getAttribute(total);
        shikiScoreElement.querySelector("div.text-score > div.score-notice").textContent = getLabelData()[Math.round(dataScore)];
    }, false);
}

function appendShikiRating() {
    'use strict';

    let currentPath = window.location.pathname.substring(0, 7);
    if (!(currentPath === "/animes" || currentPath === "/mangas" || currentPath === "/ranobe") || !document.querySelector(".c-info-right")) {
        log('Неподходящая страница');
        return;
    }

    if (document.querySelector("#shiki-score") !== null) {
        log('Уже был создано');
        scoreOnHover('mouseover', 'data-score-low', 'data-count-low');
        scoreOnHover('mouseout', 'data-score', 'data-count');
        return;
    }

    let newShikiRate, malRate;
    if (document.querySelector(".scores > .b-rate") === null) {
        // создаем контейнер с оценками вручную, если его нет
        let rateFromZero = createElementFromHTML('<div class="block" itemprop="aggregateRating" itemscope="" itemtype="http://schema.org/AggregateRating"><div class="subheadline m5">Рейтинг</div><div class="scores"><div class="b-rate" id="shiki-score"><div class="stars-container"><div class="hoverable-trigger"></div><div class="stars score score-9" style="color: rgb(162, 33, 35);"></div><div class="stars hover"></div><div class="stars background"></div></div><div class="text-score"><div class="score-value score-9"></div><div class="score-notice"></div></div></div><p class="score-counter" style="text-align: center; color: rgb(123, 128, 132);"></p></div></div>');
        document.querySelector(".c-info-right").prepend(rateFromZero);
        newShikiRate = document.getElementById('shiki-score');
    }
    else {
        // создаем контейнер с оценками по подобию уже имеющегося
        malRate = document.querySelector(".scores > .b-rate");
        malRate.setAttribute('id', 'mal-score');
        newShikiRate = malRate.cloneNode(true);
        newShikiRate.setAttribute('id', 'shiki-score');
        document.querySelector(".scores").appendChild(newShikiRate);
    }

    // подгружаем оценки
    let scoreDataJson = document.querySelector("#rates_scores_stats").getAttribute("data-stats");
    let scoreData = JSON.parse(scoreDataJson);

    // если оценок не оказалось
    if (!scoreData || scoreData.length === 0) {
        newShikiRate.innerHTML = getLocale() === 'ru' ? 'Недостаточно данных' : 'Insufficient data';
        newShikiRate.style.textAlign = 'center';
        newShikiRate.style.marginTop = '30px';
        return;
    }

    // считаем рейтинг
    let shikiRateData = calculateShikiRating(scoreData);
    let shikiRateLowData = calculateShikiRating(scoreData, settings.getOption('lowest')) || shikiRateData;

    // добавляем данные в аттрибуты для js-событий
    let shikiRateDiv = document.getElementById('shiki-score');
    shikiRateDiv.setAttribute('data-score', shikiRateData.score);
    shikiRateDiv.setAttribute('data-score-low', shikiRateLowData.score);
    shikiRateDiv.setAttribute('data-count', shikiRateData.count);
    shikiRateDiv.setAttribute('data-count-low', shikiRateLowData.count);

    // меняем значение оценки
    let scoreElement = newShikiRate.querySelector("div.text-score > div.score-value");
    scoreElement.innerHTML = shikiRateData.score;
    removeLastClass(scoreElement);
    scoreElement.classList.add("score-" + Math.round(shikiRateData.score));

    // меняем количество звезд
    let starElement = newShikiRate.querySelector("div.stars-container > div.stars.score");
    removeLastClass(starElement);
    starElement.style.color = settings.getOption('starColor');
    starElement.classList.add("score-" + shikiRateData.scoreMath);

    // меняем тип оценки (хорошо, плохо и т.п.)
    newShikiRate.querySelector("div.text-score > div.score-notice").textContent = getLabelData()[shikiRateData.scoreMath];

    // добавляем информацию к стандартной оценке, что это mal
    if (document.getElementById('mal-score')) {
        let malLabel = getLocale() === 'ru' ? 'На основе оценок MAL' : 'From MAL users';
        malRate.insertAdjacentHTML('afterend', '<p class="score-source">' + malLabel + '</p>');
        let malScoreLabelElement = document.querySelector('.score-source');
        malScoreLabelElement.style.marginBottom = '15px';
        malScoreLabelElement.style.textAlign = 'center';
        malScoreLabelElement.style.color = settings.getOption('textColor');
    }

    // добавляем информацию к шики оценке, что это шики
    let shikiCountLabel = '<strong>' + shikiRateData.count + '</strong>';
    shikiCountLabel = (getLocale() === 'ru') ? 'На основе ' + shikiCountLabel + ' оценок Shikimori' : 'From ' + shikiCountLabel + ' shiki users';
    newShikiRate.insertAdjacentHTML('afterend', '<p class="score-counter">' + shikiCountLabel + '</p>');
    let shikiScoreLabelElement = document.querySelector('.score-counter');
    shikiScoreLabelElement.style.textAlign = 'center';
    shikiScoreLabelElement.style.color = settings.getOption('textColor');

    // при наведении показывает оценку, не учитывая низкие
    scoreOnHover('mouseover', 'data-score-low', 'data-count-low');
    scoreOnHover('mouseout', 'data-score', 'data-count');
}

function ready(fn) {
    document.addEventListener('page:load', fn);
    document.addEventListener('turbolinks:load', fn);
    if (document.attachEvent ? document.readyState === "complete" : document.readyState !== "loading") fn();
    else document.addEventListener('DOMContentLoaded', fn);
}

ready(initSettings);
ready(appendShikiRating);