Toonily Manga Loader

Forces Toonily to load all manga images at once and dynamically loads them into a single page strip with a stats window.

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         Toonily Manga Loader
// @namespace    github.com/longkidkoolstar
// @version      1.1
// @description  Forces Toonily to load all manga images at once and dynamically loads them into a single page strip with a stats window.
// @author       longkidkoolstar
// @match        https://toonily.com/webtoon/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/js/all.min.js
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.deleteValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    let imageUrls = [];
    let loadedImages = 0;
    let totalImages = 0;
    let reloadMode = false;

    // Function to simulate click on the "Load All Images" switch
    function enableLoadAllImages() {
        const loadAllImagesButton = document.querySelector('#btn-lazyload-controller');
        if (loadAllImagesButton && loadAllImagesButton.querySelector('.fa-toggle-off')) {
            loadAllImagesButton.click(); // Simulate clicking to enable "Load All Images"
            console.log("Forcing 'Load All Images'...");
        }
    }

    // Hooking into XMLHttpRequest to capture all image URLs from AJAX responses
    function interceptAjaxRequests() {
        const originalOpen = XMLHttpRequest.prototype.open;

        XMLHttpRequest.prototype.open = function(method, url, ...args) {
            this.addEventListener('load', function() {
                if (url.includes('/chapters/manga')) {
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(this.responseText, 'text/html');
                    const imgs = doc.querySelectorAll('img.wp-manga-chapter-img');

                    imgs.forEach(img => {
                        const imgUrl = img.src.trim();
                        if (!imageUrls.includes(imgUrl)) {
                            imageUrls.push(imgUrl);
                        }
                    });
                }
            });

            return originalOpen.apply(this, [method, url, ...args]);
        };
    }

    // Function to remove all other HTML elements except for the manga container
    function removeOtherElements() {
        const bodyChildren = Array.from(document.body.children);
        bodyChildren.forEach(child => {
            if (!child.id || child.id !== 'manga-container') {
                child.remove();
            }
        });
    }

    // Helper function to create a page container for each image
    function createPageContainer(pageUrl) {
        const container = document.createElement('div');
        container.className = 'manga-page-container';

        const img = document.createElement('img');
        img.src = pageUrl;
        img.style.maxWidth = '100%';
        img.style.display = 'block';
        img.style.margin = '0px auto';  //Note: The 0px dictates the space between images and the auto center them

        img.onload = function() {
            loadedImages++;
            updateStats(); // Update the stats when an image is fully loaded
        };

        addClickEventToImage(img);

        container.appendChild(img);
        return container;
    }

    // Function to extract already loaded image URLs from the page
    function extractImageUrlsFromPage() {
        const images = document.querySelectorAll('.reading-content img.wp-manga-chapter-img');
        images.forEach(img => {
            const src = img.src.trim();
            if (src.startsWith('https://cdn.toonily.com/chapters/manga') && !imageUrls.includes(src)) {
                imageUrls.push(src);
            }
        });
        totalImages = imageUrls.length;
    }

// Helper function to create an exit button
function createExitButton() {
    const exitButton = document.createElement('button');
    exitButton.textContent = 'Exit';
    exitButton.style.backgroundColor = '#e74c3c';
    exitButton.style.color = '#fff';
    exitButton.style.border = 'none';
    exitButton.style.padding = '10px';
    exitButton.style.margin = '10px auto';
    exitButton.style.display = 'block';  // Center the button
    exitButton.style.cursor = 'pointer';
    exitButton.style.borderRadius = '5px';

    exitButton.addEventListener('click', function() {
        window.location.reload();  // Reload the page when clicked
    });

    return exitButton;
}

// Function to load all manga images into a single strip
function loadMangaImages() {
    const mangaContainer = document.createElement('div');
    mangaContainer.id = 'manga-container';
    document.body.appendChild(mangaContainer);

    imageUrls.forEach((url, index) => {
        const pageContainer = createPageContainer(url);

        // Add exit button to the first loaded page
        if (index === 0) {
            const topExitButton = createExitButton();
            mangaContainer.appendChild(topExitButton);
        }

        mangaContainer.appendChild(pageContainer);

        // Add exit button to the last loaded page
        if (index === imageUrls.length - 1) {
            const bottomExitButton = createExitButton();
            mangaContainer.appendChild(bottomExitButton);
        }
    });

    removeOtherElements(); // Remove all other page elements after loading
}


    // Function to update the stats window
    function updateStats() {
        const statsPages = document.querySelector('.ml-stats-pages');
        const loadingImages = document.querySelector('.ml-loading-images');
        const totalImagesDisplay = document.querySelector('.ml-total-images');
        if (statsPages) statsPages.textContent = `${loadedImages}/${totalImages} loaded`;
        if (loadingImages) loadingImages.textContent = `${totalImages - loadedImages} images loading`;
        if (totalImagesDisplay) totalImagesDisplay.textContent = `${totalImages} images in chapter`;
    }

    // Function to create the stats window
    async function createStatsWindow() {
        const statsWindow = document.createElement('div');
        statsWindow.className = 'ml-stats';
        statsWindow.style.position = 'fixed';
        statsWindow.style.bottom = '10px';
        statsWindow.style.right = '10px';
        statsWindow.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
        statsWindow.style.padding = '1px';
        statsWindow.style.color = 'white';
        statsWindow.style.borderRadius = '8px';
        statsWindow.style.zIndex = '9999';
        statsWindow.style.transition = 'opacity 0.3s';
        statsWindow.style.opacity = '0.5';

        statsWindow.addEventListener('mouseenter', function() {
            statsWindow.style.opacity = '1';
        });

        statsWindow.addEventListener('mouseleave', function() {
            statsWindow.style.opacity = '0.5';
        });

        const statsWrapper = document.createElement('div');
        statsWrapper.style.display = 'flex';
        statsWrapper.style.alignItems = 'center'; // Center vertically
        const collapseButton = document.createElement('span');
        collapseButton.className = 'ml-stats-collapse';
        collapseButton.title = 'Hide stats';
        collapseButton.textContent = '>>';
        collapseButton.style.cursor = 'pointer';
        collapseButton.style.marginRight = '10px'; // Space between button and content
        collapseButton.addEventListener('click', async function() {
            contentContainer.style.display = contentContainer.style.display === 'none' ? 'block' : 'none';
            collapseButton.textContent = contentContainer.style.display === 'none' ? '<<' : '>>';
            await GM.setValue('statsCollapsed', contentContainer.style.display === 'none');
        });
        (async () => {
            const isCollapsed = await GM.getValue('statsCollapsed', false);
            contentContainer.style.display = isCollapsed ? 'none' : 'block';
            collapseButton.textContent = isCollapsed ? '<<' : '>>';
        })();

        const contentContainer = document.createElement('div');
        contentContainer.className = 'ml-stats-content';

        const statsText = document.createElement('span');
        statsText.className = 'ml-stats-pages';
        statsText.textContent = `0/0 loaded`; // Initial stats

        const infoButton = document.createElement('i');
        infoButton.innerHTML = '<i class="fas fa-question-circle"></i>';
        infoButton.title = 'See userscript information and help';
        infoButton.style.marginLeft = '5px';
        infoButton.style.marginRight = '5px'; // Add space to the right
        infoButton.addEventListener('click', function() {
            alert('This userscript loads manga pages in a single view. Click on an image to reload.');
        });

        const moreStatsButton = document.createElement('i');
        moreStatsButton.innerHTML = '<i class="fas fa-chart-pie"></i>';
        moreStatsButton.title = 'See detailed page stats';
        moreStatsButton.style.marginRight = '5px'; // Add space to the right
        moreStatsButton.addEventListener('click', function() {
            const statsBox = document.querySelector('.ml-floating-msg');
            statsBox.style.display = statsBox.style.display === 'block' ? 'none' : 'block';
        });

        const refreshButton = document.createElement('i');
        refreshButton.innerHTML = '<i class="fas fa-sync-alt"></i>';
        refreshButton.title = 'Click an image to reload it.';
        refreshButton.addEventListener('click', function() {
            reloadMode = !reloadMode;
            refreshButton.style.color = reloadMode ? 'orange' : '';
            console.log(`Reload mode is now ${reloadMode ? 'enabled' : 'disabled'}.`);
        });

        const miniExitButton = document.createElement('button');
        miniExitButton.innerHTML = '<i class="fas fa-sign-out-alt"></i>';
        miniExitButton.title = 'Exit the Manga Loader';
        miniExitButton.style.marginLeft = '10px'; // Space between other buttons
        miniExitButton.style.backgroundColor = '#e74c3c';  // Red color for the button
        miniExitButton.style.color = '#fff';
        miniExitButton.style.border = 'none';
        miniExitButton.style.padding = '1px 5px';
        miniExitButton.style.borderRadius = '5px';
        miniExitButton.style.cursor = 'pointer';

        miniExitButton.addEventListener('click', function() {
            window.location.reload();  // Refresh the page
        });

        contentContainer.appendChild(statsText);
        contentContainer.appendChild(infoButton);
        contentContainer.appendChild(moreStatsButton);
        contentContainer.appendChild(refreshButton);
        contentContainer.appendChild(miniExitButton);  // Add mini exit button to the content

        statsWrapper.appendChild(collapseButton);
        statsWrapper.appendChild(contentContainer);
        statsWindow.appendChild(statsWrapper);

        const statsBox = document.createElement('pre');
        statsBox.className = 'ml-box ml-floating-msg';
        statsBox.innerHTML = `<strong>Stats:</strong><br><span class="ml-loading-images">0 images loading</span><br><span class="ml-total-images">0 images in chapter</span><br><span class="ml-loaded-pages">0 pages parsed</span>`;
        statsBox.style.display = 'none'; // Initially hidden
        statsWindow.appendChild(statsBox);

        document.body.appendChild(statsWindow);
    }

    // Add the click event to images
    function addClickEventToImage(image) {
        image.addEventListener('click', function() {
            if (reloadMode) {
                const imgSrc = image.dataset.src || image.src;
                image.src = ''; // Clear the src to trigger reload
                setTimeout(() => {
                    image.src = imgSrc; // Retry loading after clearing
                }, 100); // Short delay to ensure proper reload
            }
        });
    }

    // Create and add the "Load Manga" button
    const loadMangaButton = document.createElement('button');
    loadMangaButton.textContent = 'Load Manga';
    loadMangaButton.style.position = 'fixed';
    loadMangaButton.style.bottom = '10px';
    loadMangaButton.style.right = '10px';
    loadMangaButton.style.zIndex = '9999';
    loadMangaButton.style.padding = '10px';
    loadMangaButton.style.backgroundColor = '#f39c12';
    loadMangaButton.style.border = 'none';
    loadMangaButton.style.borderRadius = '5px';
    loadMangaButton.style.cursor = 'pointer';

    // Add hover effect to the button
    loadMangaButton.addEventListener('mouseover', function() {
        loadMangaButton.style.backgroundColor = '#ff5500';
    });

    loadMangaButton.addEventListener('mouseout', function() {
        loadMangaButton.style.backgroundColor = '#f39c12';
    });

    const mangaInfo = document.querySelector("body > div.wrap > div > div > div > div.profile-manga.summary-layout-1 > div > div > div > div.tab-summary");
    if (!mangaInfo) {
        document.body.appendChild(loadMangaButton);

        loadMangaButton.addEventListener('click', async function() {
            enableLoadAllImages(); // Force the site to load all images
            interceptAjaxRequests(); // Hook into AJAX requests to capture image URLs
            extractImageUrlsFromPage(); // Initially extract image URLs from the page

            loadMangaImages(); // Load all collected images
            loadMangaButton.remove(); // Remove the button after loading
            await createStatsWindow(); // Create the stats window
        });
    }

    // Wait for the DOM to finish loading before adding the button
    window.addEventListener('DOMContentLoaded', function() {
        if (!mangaInfo) {
            document.body.appendChild(loadMangaButton);
        }
    });

})();