Chortowod / Shikimori Various Statistic

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

let siteName = "https://shikimori.one";

let settings = new ChtwSettings('chtwVarStat', '<a target="_black" href="https://openuserjs.org/scripts/Chortowod/Shikimori_Various_Statistic">Разная статистика</a>');
let debug;
let style = `
        .chtw-watched-item {
            display: flex;
            justify-content: space-between;
        }
        .chtw-watched-detail {
            display: none;
        }
        .chtw-watched-detail > .chtw-watched-item {
            justify-content: space-between;
            flex-direction: column;
            align-items: center;
            border-bottom: 1px solid #7f7f7f38;
            text-align: center;
        }
        .chtw-watched-detail > .chtw-watched-item:last-child {
            border: none;
        }
        #chtwWatchedButtonShowAll {
            color: var(--link-color);
            text-align: center;
            width: 100%;
        }
        #chtwWatchedButtonShowAll:hover {
            color: var(--link-hover-color);
        }
        .chtw-watched-detail-header {
            text-align: center;
            font-weight: bold;
        }
        .chtw-watched-detail-header {
            margin-bottom: 8px;
            margin-top: 10px;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .chtw-watched-detail-header::before, .chtw-watched-detail-header::after {
            content: "";
            display: block;
            height: 2px;
            min-width: 50%;
        }
        .chtw-watched-detail-header::before {
            background: linear-gradient(to right, rgba(240, 240, 240, 0), #7b8084);
        }
        .chtw-watched-detail-header::after {
            background: linear-gradient(to left, rgba(240, 240, 240, 0), #7b8084);
        }
`;

function initSettings() {
    settings.createOption('history', 'История просмотра тайтла', true);
    settings.createOption('avTitleFr', 'Средн. оценка тайтла друзей', true);
    settings.createOption('avTitle', 'Средн. оценка тайтла', true);
    settings.createOption('avProfileFr', 'Средняя оценка друга', true);
    settings.createOption('allProfileFr', 'Всего тайтлов друга', true);

    settings.createOption('isDebug', 'Режим отладки', false);
    debug = settings.getOption('isDebug');
    settings.addStyle(style);
}

function log (message) { debug&&console.log(message) }

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

function findCompletedEntries(entries, type, lang) {
    let watchedEntries = [];
    let watchedMatch1 = (type === 'Anime') ? 'Просмотрено' : 'Прочитано';
    let watchedMatch2 = (type === 'Anime') ? 'Просмотрено и оценено' : 'Прочитано и оценено';
    let watchedMatchEn = 'Completed';
    if (lang === 'ru') {
        entries.forEach(function (entry) {
            if (entry.description === watchedMatch1 || entry.description.includes(watchedMatch2)) {
                watchedEntries.push(entry);
            }
        });
    }
    else {
        entries.forEach(function (entry) {
            if (entry.description.includes(watchedMatchEn)) watchedEntries.push(entry);
        });
    }

    return watchedEntries.reverse();
}

function showWatchHistory() {
    if (!settings.getOption('history')) return;
    let currentPath = window.location.pathname.substring(0, 7);
    if (document.getElementById('chtwDateWatchedWrapper') || !(currentPath === "/animes" || currentPath === "/mangas" || currentPath === "/ranobe")) {
        log('Неподходящая страница или блок уже есть');
        return;
    }
    let targetType = (currentPath === "/animes") ? 'Anime' : 'Manga';
    let id = $('.c-image > .b-user_rate').data('target_id');
    if (id) {
        let profileID = $('body').data('user').id;
        let url = siteName+'/api/users/'+profileID+'/history?target_id='+id+'&limit=100&target_type='+targetType;
        $.ajax({
            url: url,
            success: function(data) {
                if (data.length === 0) {
                    log('Данных не найдено.');
                }
                else {
                    let completedEntries = findCompletedEntries(data, targetType, getLocale());
                    let dateOptions = { year: 'numeric', month: 'long', day: 'numeric' };
                    let dateLocale = (getLocale() === 'ru') ? 'ru-RU' : 'en-GB';
                    let dateTitle = (getLocale() === 'ru') ? 'История' : 'History';
                    let parentBlock = $('.b-animes-menu');

                    let chtwHistoryBlock = document.createElement('div');
                    chtwHistoryBlock.id = 'chtwDateWatchedWrapper';
                    chtwHistoryBlock.classList.add("block");
                    chtwHistoryBlock.style.fontSize = '12px';

                    let chtwHistoryBlockHeader = document.createElement('div');
                    chtwHistoryBlockHeader.classList.add("subheadline");
                    chtwHistoryBlockHeader.innerHTML = dateTitle;

                    let chtwHistoryBlockBody = document.createElement('div');
                    chtwHistoryBlockBody.classList.add("chtw-watched-body");

                    let chtwHistoryBlockShowDetail = document.createElement('button');
                    chtwHistoryBlockShowDetail.id = 'chtwWatchedButtonShowAll';
                    chtwHistoryBlockShowDetail.innerHTML = (getLocale() === 'ru') ? 'показать/скрыть подробнее' : 'show/hide details';

                    let chtwHistoryBlockDetail = document.createElement('div');
                    chtwHistoryBlockDetail.classList.add("chtw-watched-detail");

                    let chtwHistoryBlockDetailHeader = document.createElement('div');
                    chtwHistoryBlockDetailHeader.classList.add("chtw-watched-detail-header");
                    chtwHistoryBlockDetailHeader.src = '-';

                    chtwHistoryBlockDetail.append(chtwHistoryBlockDetailHeader);
                    chtwHistoryBlock.append(chtwHistoryBlockHeader);
                    chtwHistoryBlock.append(chtwHistoryBlockBody);
                    chtwHistoryBlock.append(chtwHistoryBlockDetail);
                    chtwHistoryBlock.append(chtwHistoryBlockShowDetail);

                    completedEntries.forEach(function (entry, index) {
                        let textLocale = ((getLocale() === 'ru') ? ((entry.target.url.slice(0,6) === '/anime') ? 'Просмотрено' : 'Прочитано') : 'Completed');
                        let dataText = new Date(entry.created_at);
                        let dateRus = dataText.toLocaleDateString(dateLocale, dateOptions);
                        let newEntry = document.createElement('div');
                        newEntry.classList.add("chtw-watched-item");
                        let newEntryText = document.createElement('div');
                        newEntryText.innerHTML = textLocale+' #'+(index+1)+': ';
                        let newEntryDate = document.createElement('div');
                        newEntryDate.innerHTML = dateRus;
                        newEntry.append(newEntryText);
                        newEntry.append(newEntryDate);
                        chtwHistoryBlockBody.append(newEntry);
                    });

                    data.forEach(function (entry) {
                        let dataText = new Date(entry.created_at);
                        let dateRus = dataText.toLocaleDateString(dateLocale, dateOptions);
                        let newEntry = document.createElement('div');
                        newEntry.classList.add("chtw-watched-item");
                        let newEntryDate = document.createElement('div');
                        newEntryDate.innerHTML = dateRus;
                        let newEntryText = document.createElement('div');
                        newEntryText.innerHTML = entry.description;
                        newEntry.append(newEntryDate);
                        newEntry.append(newEntryText);
                        chtwHistoryBlockDetail.append(newEntry);
                    });
                    parentBlock.prepend(chtwHistoryBlock);
                    $('#chtwWatchedButtonShowAll').on('click', function () {
                        $(this).prev().slideToggle();
                    });
                }
            },
            error: function(XMLHttpRequest, textStatus, errorThrown) {
                console.log("Status: " + textStatus + " | Error: " + errorThrown);
            },
            complete: function() {
            }
        });
    }
}

function calculateAverageScore(scoreData) {
    let sumScore = 0;
    let totalCount = 0;
    for (let i = 0; i < scoreData.length; i++) {
        if (scoreData[i].name) {
            sumScore += parseInt(scoreData[i].value) * scoreData[i].name;
            totalCount += parseInt(scoreData[i].value);
        }
        else {
            sumScore += parseInt(scoreData[i][1]) * scoreData[i][0];
            totalCount += parseInt(scoreData[i][1]);
        }
    }
    return (sumScore / totalCount).toFixed(2);
}

function calculateAllTitles(scoreData) {
    let totalCount = 0;
    for (let i = 0; i < scoreData.length; i++) totalCount += parseInt(scoreData[i].value);
    return totalCount;
}

function calculateAverageFriendsRating() {
    var friendRate = document.querySelectorAll("div[class*=friend-rate] div[class=status]");
    var friendRateStatus = [];
    var sum = 0;
    var avg = 0;
    var count = 0;
    for (var i = friendRate.length - 1; i >= 0; i--) {
        friendRateStatus[i] = friendRate[i].innerText.replace(/[^-0-9]/gim,'');
        if (friendRateStatus[i] !== "") {
            sum += Number(friendRateStatus[i]);
            count++;
        }
    }
    let result = sum / count;
    return result ? ((result % 1 === 0) ? result : result.toFixed(2)) : false;
}

function appendAverageFriendsRating() {
    if (!settings.getOption('avTitleFr')) return;
    let element = document.querySelector(".block > .friend-rate");
    log(element);
    if (!element) return;
    let data = calculateAverageFriendsRating();
    log(data);
    if (data) element.previousSibling.innerHTML = (getLocale() === 'ru') ? 'У друзей (средняя: '+data+')' : 'Friends (average: '+data+')';
}

function appendAverageRating() {
    if (!settings.getOption('avProfileFr')) return;
    let element = document.querySelector(".p-user_rates.p-profiles .mini-charts > .scores > #scores");
    if (!element) return;
    element = JSON.parse(element.getAttribute("data-stats"));
    if (!element || !element.length) return;
    element = calculateAverageScore(element);
    if (element) document.querySelector(".p-user_rates.p-profiles .mini-charts > .scores > div.subheadline").innerHTML = (getLocale() === 'ru') ? 'Оценки (средняя: '+element+')' : 'Scores (average: '+element+')';
}

function appendAverageTitleRating() {
    if (!settings.getOption('avTitle')) return;
    let elementRates = document.querySelector("#rates_scores_stats");
    if (!elementRates) return;
    let element = JSON.parse(elementRates.getAttribute("data-stats"));
    if (!element || !element.length) return;
    element = calculateAverageScore(element);
    if (element) elementRates.previousSibling.innerHTML = (getLocale() === 'ru') ? 'Оценки (средняя: '+element+')' : 'Scores (average: '+element+')';
}

function appendOverallTitles() {
    if (!settings.getOption('allProfileFr')) return;
    let element = document.querySelector(".p-user_rates.p-profiles .mini-charts > .types > #types");
    if (!element) return;
    element = JSON.parse(element.getAttribute("data-stats"));
    if (!element || !element.length) return;
    element = calculateAllTitles(element);
    if (element) document.querySelector(".p-user_rates.p-profiles .mini-charts > .types > div.subheadline").innerHTML = (getLocale() === 'ru') ? 'Типы (всего: '+element+')' : 'Kinds (overall: '+element+')';
}

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(appendAverageRating);
ready(appendOverallTitles);
ready(appendAverageFriendsRating);
ready(appendAverageTitleRating);
ready(showWatchHistory);