MAL (MyAnimeList) - Accurate Progress & Sorting On Anime List

Accurately sorts your anime list by progress, and displays correct progress percentages for each anime

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name         MAL (MyAnimeList) - Accurate Progress & Sorting On Anime List
// @version      3.2
// @author       Ezektor
// @description  Accurately sorts your anime list by progress, and displays correct progress percentages for each anime
// @match        https://myanimelist.net/animelist/*
// @grant        none
// @icon         https://myanimelist.net/favicon.ico
// @namespace    https://gf.zukizuki.org/users/1414348
// ==/UserScript==

(function() {
    'use strict';

    let isSortingEnabled = false;
    let originalRows = [];
    let sortDirection = 'desc';

    const toggleButton = document.createElement('button');
    toggleButton.innerHTML = '➖ Default';
    toggleButton.style.position = 'fixed';
    toggleButton.style.bottom = '20px';
    toggleButton.style.right = '20px';
    toggleButton.style.padding = '10px';
    toggleButton.style.backgroundColor = '#9e9e9e';
    toggleButton.style.color = 'white';
    toggleButton.style.border = 'none';
    toggleButton.style.borderRadius = '5px';
    toggleButton.style.cursor = 'pointer';
    toggleButton.style.zIndex = '9999';
    document.body.appendChild(toggleButton);

    function isAllAnimePage() {
        return window.location.href.includes('/animelist/') && window.location.href.includes('?status=7');
    }

    function calculateProgress(row) {
        const progressCell = row.querySelector('.data.progress');
        if (!progressCell) return 0;

        const progressText = progressCell.innerText.trim();
        const [watched, total] = progressText.replace(/\[[^\]]*\]/g, '').split('/').map(s => parseInt(s.trim(), 10) || 0);
        return total == null ? 100 : total === 0 ? 0 : Math.floor((watched / total) * 100);
    }

    function addProgressPercentage(row) {
        const progressCell = row.querySelector('.data.progress');
        if (!progressCell) return;

        const progressText = progressCell.innerText.trim();
        if (/^\d+$/.test(progressText)) return;

        const existingPercentage = progressCell.querySelector('.progress-percentage')
        if (existingPercentage) {
            existingPercentage.remove();
        }

        const progressPercentage = calculateProgress(row);

        const progressPercentageElement = document.createElement('div');
        progressPercentageElement.style.fontSize = '12px';
        progressPercentageElement.style.marginTop = '5px';
        progressPercentageElement.classList.add('progress-percentage');
        progressPercentageElement.textContent = `${progressPercentage}% progress`;

        if (progressPercentage <= 33) {
            progressPercentageElement.style.color = '#f44336';
        } else if (progressPercentage <= 66) {
            progressPercentageElement.style.color = '#ff9800';
        } else {
            progressPercentageElement.style.color = '#4caf50';
        }

        progressCell.appendChild(progressPercentageElement);
    }

    function sortRowsByProgress(rows, direction) {

        return [...rows].sort((a, b) => {

            const progressA = calculateProgress(a);
            const progressB = calculateProgress(b);

            const isCompletedA = a.querySelector('.data.status').classList.contains('completed');
            const isCompletedB = b.querySelector('.data.status').classList.contains('completed');

            const isDroppedA = a.querySelector('.data.status').classList.contains('dropped');
            const isDroppedB = b.querySelector('.data.status').classList.contains('dropped');

            if (isAllAnimePage()) {
                if (isDroppedA && !isDroppedB) return 1;
                if (!isDroppedA && isDroppedB) return -1;

                if (isCompletedA && !isCompletedB) return direction === 'desc' ? -1 : 1;
                if (!isCompletedA && isCompletedB) return direction === 'desc' ? 1 : -1;
            }

            return direction === 'asc'
                ? progressA - progressB
                : progressB - progressA;
        });
    }

    function updateTable(sortedRows) {
        const table = document.querySelector('table');
        const tbody = table.querySelector('tbody.list-item');
        tbody.innerHTML = '';
        sortedRows.forEach(row => tbody.appendChild(row));
    }

    function restoreOriginalOrder() {
        const table = document.querySelector('table');
        const tbody = table.querySelector('tbody.list-item');
        tbody.innerHTML = '';
        originalRows.forEach(row => tbody.appendChild(row));
    }

    function storeOriginalRows() {
        const rows = document.querySelectorAll('tbody.list-item .list-table-data');
        originalRows = [...rows];
    }

    function main() {
        const rows = document.querySelectorAll('tbody.list-item .list-table-data');

        rows.forEach(addProgressPercentage);

        if (isSortingEnabled) {
            const sortedRows = sortRowsByProgress(rows, sortDirection);

            updateTable(sortedRows);
        }
    }

    toggleButton.addEventListener('click', () => {
        if (!isSortingEnabled) {
            isSortingEnabled = true;
            sortDirection = 'desc';
            toggleButton.style.backgroundColor = '#4CAF50';
            toggleButton.innerHTML = 'Sorting In: 📉 Descending Order';
            main();
        } else if (sortDirection === 'desc') {
            sortDirection = 'asc';
            toggleButton.innerHTML = 'Sorting In: 📈 Ascending Order';
            main();
        } else {
            isSortingEnabled = false;
            toggleButton.style.backgroundColor = '#9e9e9e';
            toggleButton.innerHTML = '➖ Default';
            restoreOriginalOrder();
        }
    });

    window.addEventListener('load', () => {
        storeOriginalRows();
        main();
    });
})();