WTR-Lab Library Auto-Sorter by Progress

Automatically loads all items and sorts your WTR-Lab library by reading progress. Sort order (asc/desc) is configurable in the menu.

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         WTR-Lab Library Auto-Sorter by Progress
// @namespace    https://greatest.deepsurf.us/en/users/1433142-masuriii
// @version      2.2
// @description  Automatically loads all items and sorts your WTR-Lab library by reading progress. Sort order (asc/desc) is configurable in the menu.
// @author       MasuRii
// @match        https://wtr-lab.com/en/library*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=wtr-lab.com
// @license      MIT
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

/*
The MIT License (MIT)

Copyright (c) 2025 MasuRii

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

(function() {
    'use strict';

    // --- CONFIGURATION ---
    const DEBUG = true; // Set to true to enable console logging for debugging.
    const SORT_ORDER_KEY = 'wtr_lab_sorter_order';
    const ASC = 'ascending';
    const DESC = 'descending';
    const DEFAULT_SORT_ORDER = ASC;

    // --- HELPER FUNCTIONS ---
    function debugLog(...messages) {
        if (DEBUG) {
            console.log('[WTR-Lab Sorter]:', ...messages);
        }
    }

    function getProgressPercent(item) {
        const progressSpan = item.querySelector('.progress .progress-bar span');
        if (progressSpan) {
            const text = progressSpan.textContent.trim().replace('%', '');
            const percent = parseFloat(text);
            return isNaN(percent) ? 0 : percent;
        }
        return 0; // Default to 0 if progress element is not found
    }

    // --- CORE LOGIC ---

    /**
     * Registers menu commands in Tampermonkey to change the sort order.
     */
    async function registerMenu() {
        const currentOrder = await GM_getValue(SORT_ORDER_KEY, DEFAULT_SORT_ORDER);

        const setOrder = async (order) => {
            await GM_setValue(SORT_ORDER_KEY, order);
            alert(`Sort order set to ${order}. Reloading page to apply changes.`);
            location.reload();
        };

        let ascLabel = "Sort Ascending (Lowest First)";
        if (currentOrder === ASC) {
            ascLabel = "✅ " + ascLabel;
        }

        let descLabel = "Sort Descending (Highest First)";
        if (currentOrder === DESC) {
            descLabel = "✅ " + descLabel;
        }

        GM_registerMenuCommand(ascLabel, () => setOrder(ASC));
        GM_registerMenuCommand(descLabel, () => setOrder(DESC));
    }

    /**
     * Finds and clicks the "Load More" button repeatedly until it disappears.
     */
    async function clickLoadMoreUntilDone() {
        const loadMoreSelector = '.load-more button';
        const waitTime = 1000; // Time in ms to wait after each click for content to load

        while (true) {
            const loadMoreButton = document.querySelector(loadMoreSelector);
            if (!loadMoreButton || loadMoreButton.disabled) {
                debugLog('"Load More" button not found or is disabled. Assuming all items are loaded.');
                break;
            }

            debugLog('Found "Load More" button. Clicking...');
            loadMoreButton.click();

            // Wait for a moment to allow the new items to be added to the DOM
            await new Promise(resolve => setTimeout(resolve, waitTime));
        }
    }

    /**
     * Sorts the library items based on the saved sort order.
     * @param {HTMLElement} rootContainer - The container element holding the library items.
     */
    async function sortLibrary(rootContainer) {
        debugLog('Attempting to sort library items...');
        const sortOrder = await GM_getValue(SORT_ORDER_KEY, DEFAULT_SORT_ORDER);
        debugLog(`Current sort order: ${sortOrder}`);

        const items = Array.from(rootContainer.querySelectorAll('.serie-item'));
        if (items.length === 0) {
            debugLog('No items found to sort. This might be an error if you have items in your library.');
            return;
        }

        const actualContainer = items[0].parentNode;
        debugLog(`Found ${items.length} items to sort.`);

        items.sort((a, b) => {
            const percentA = getProgressPercent(a);
            const percentB = getProgressPercent(b);
            if (sortOrder === DESC) {
                return percentB - percentA; // Sort descending (100% -> 0%)
            } else {
                return percentA - percentB; // Sort ascending (0% -> 100%)
            }
        });

        debugLog('Sorting complete. Re-appending items to the DOM.');
        items.forEach(item => actualContainer.appendChild(item));
    }

    /**
     * The main execution flow, called only after the initial content is loaded.
     */
    async function runMainLogic() {
        debugLog('Initial content detected. Starting main logic.');
        const rootContainer = document.querySelector('.library-list');
        if (!rootContainer) {
            debugLog('Error: Library container disappeared. Cannot proceed.');
            return;
        }

        debugLog('Starting "Load More" process...');
        await clickLoadMoreUntilDone();

        debugLog('Starting sorting process...');
        await sortLibrary(rootContainer);

        debugLog('Script finished.');
    }

    /**
     * Initializes the script, sets up the menu, and waits for content to appear.
     */
    function initialize() {
        debugLog('Script initializing...');
        registerMenu();

        const rootContainer = document.querySelector('.library-list');
        if (!rootContainer) {
            debugLog('Error: Library container (.library-list) not found on page load. Script cannot run.');
            return;
        }

        // Use a MutationObserver to wait for the website to load content dynamically.
        debugLog('Setting up MutationObserver to wait for initial library content.');
        const observer = new MutationObserver((mutations, obs) => {
            // Check if either the items or the load more button have appeared.
            const contentIsReady = rootContainer.querySelector('.serie-item') || rootContainer.querySelector('.load-more button');

            if (contentIsReady) {
                debugLog('Observer detected content. Disconnecting observer and running main logic.');
                obs.disconnect(); // IMPORTANT: Stop observing to prevent this from running multiple times.
                runMainLogic();
            }
        });

        observer.observe(rootContainer, {
            childList: true, // Watch for new children being added (like the items)
            subtree: true    // Watch descendants as well
        });
    }

    // --- SCRIPT KICK-OFF ---
    if (document.readyState === 'loading') {
        window.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }
})();