// ==UserScript==
// @name         JYNGMN Image Downloader
// @namespace    http://tampermonkey.net/
// @version      1.25
// @description  Download images from specified WebURLs as a ZIP file, log image URLs, and add DOWNLOAD buttons correctly, without leaving the page. Compatible with Chrome and Edge browsers, supports multiple pages.
// @author
// @match        https://www.xinggan5.top/*
// @connect      *
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// ==/UserScript==

(function () {
    'use strict';

    // Logging to indicate script initialization
    console.log('WebURL Image Downloader script initializing...');

    // Utility function to convert relative URLs to absolute URLs
    function makeAbsoluteURL(url, base) {
        try {
            return new URL(url, base || window.location.origin).href;
        } catch (error) {
            console.error(`Invalid URL: ${url} - Error: ${error.message}`);
            return url;
        }
    }

    // Function to inject external scripts, such as JSZip
    async function injectScript(url) {
        return new Promise((resolve, reject) => {
            if (document.querySelector(`script[src="${url}"]`)) {
                console.log(`Script already injected: ${url}`);
                resolve();
                return;
            }

            const script = document.createElement('script');
            script.src = url;
            script.onload = () => {
                console.log(`Injected script loaded: ${url}`);
                resolve();
            };
            script.onerror = () => {
                console.error(`Failed to load script: ${url}`);
                reject(new Error(`Failed to load script: ${url}`));
            };
            document.head.appendChild(script);
        });
    }

    // Function to fetch and parse HTML from a given URL using GM_xmlhttpRequest
    function fetchHTML(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                onload: function (response) {
                    if (response.status >= 200 && response.status < 400) {
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(response.responseText, 'text/html');
                        resolve(doc);
                    } else {
                        reject(new Error(`Failed to fetch page: ${url} - Status: ${response.status}`));
                    }
                },
                onerror: function () {
                    reject(new Error(`Network error while fetching page: ${url}`));
                }
            });
        });
    }

    // Function to fetch an image blob from a URL using fetch API
    async function fetchImage(url) {
        try {
            console.log(`Fetching image: ${url}`);
            const response = await fetch(url, { mode: 'cors', credentials: 'omit' });
            if (!response.ok) throw new Error(`Failed to fetch image: ${url} - Status: ${response.status}`);
            return await response.blob();
        } catch (error) {
            console.error(`Error fetching image: ${error.message}`);
            return null;
        }
    }

    // Utility function to extract file extension from a URL
    function getFileExtension(url) {
        const match = url.match(/\.([a-zA-Z0-9]+)(?:\?.*)?$/);
        return match ? match[1].toLowerCase() : 'jpg';
    }

    // Function to trigger download of a ZIP blob using an anchor element
    function downloadUsingAnchor(blobUrl, fileName) {
        const a = document.createElement('a');
        a.href = blobUrl;
        a.download = fileName;
        a.style.display = 'none';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        console.log(`Download initiated for: ${fileName}`);
    }

    // Function to create and download the ZIP file using JSZip
    async function createAndDownloadZip(zip, zipName = 'images.zip') {
        try {
            console.log('Generating ZIP Blob...');
            const zipBlob = await zip.generateAsync({ type: 'blob', compression: 'STORE' });
            console.log('ZIP Blob generated. Size:', zipBlob.size, 'bytes');

            const blobUrl = URL.createObjectURL(zipBlob);
            downloadUsingAnchor(blobUrl, zipName);
        } catch (error) {
            console.error(`Error creating or downloading ZIP: ${error.message}`);
            alert(`An error occurred while creating the ZIP file: ${error.message}`);
        }
    }

    /**
     * Main function to download images from a given URL.
     * @param {string} linkUrl - The URL from which to download images.
     * @param {boolean} useCurrentPage - If true, use the current document instead of fetching the linkUrl.
     */
    async function downloadImagesFromUrl(linkUrl, useCurrentPage = false) {
        try {
            console.log(`Starting image download process for URL: ${useCurrentPage ? window.location.href : linkUrl}`);
            let doc;

            if (useCurrentPage) {
                doc = document;
            } else {
                doc = await fetchHTML(linkUrl);
            }

            // Extracting the title for the ZIP file name
            const jianjieDiv = doc.querySelector('div.article-meta');
            if (!jianjieDiv) {
                throw new Error('div.article-meta not found on the target page.');
            }

            const jianjieText = jianjieDiv.textContent.trim();
            console.log(`Extracted jianjie text: ${jianjieText}`);

            const titleMatch = jianjieText.match(/\[.*?\d+P\]/);
            if (!titleMatch || !titleMatch[0]) {
                console.error('TITLE not found in extracted text.');
                alert('Title pattern not matched. Please check the page structure.');
                return;
            }

            const TITLE = titleMatch[0].trim().replace(/[\\/:*?"<>|]/g, '_');
            console.log(`Extracted TITLE for ZIP: ${TITLE}`);

            // Collecting all image page URLs from div.pagination p
            const pageDiv = doc.querySelector('div.pagination p');
            let pageUrls = [];

            if (pageDiv) {
                const pageLinks = pageDiv.querySelectorAll('a[href]');
                pageUrls = Array.from(pageLinks)
                    .filter(link => /^\d+$/.test(link.textContent.trim()))
                    .map(link => makeAbsoluteURL(link.getAttribute('href'), useCurrentPage ? window.location.href : linkUrl));
            } else {
                console.warn('div.pagination p not found. Proceeding with the current page only.');
            }

            // If useCurrentPage is true and pageUrls is empty, add the current page URL
            if (useCurrentPage && pageUrls.length === 0) {
                pageUrls.push(useCurrentPage ? window.location.href : linkUrl);
            }

            console.log(`Total related pages found: ${pageUrls.length}`);

            // To store unique image URLs
            const images = new Set();

            // Function to process a single page and collect image URLs
            async function processPage(pageUrl) {
                try {
                    console.log(`Processing page: ${pageUrl}`);
                    const pageDoc = useCurrentPage && pageUrl === window.location.href ? document : await fetchHTML(pageUrl);
                    const contentDivs = pageDoc.querySelectorAll('article.article-content p');
                    if (contentDivs.length > 0) {
                        let imagesInPage = [];
                        contentDivs.forEach(div => {
                            const imgs = div.querySelectorAll('img[src]');
                            imagesInPage.push(...imgs);
                        });
                        console.log(`Found ${imagesInPage.length} images in ${pageUrl}`);
                        imagesInPage.forEach(img => {
                            const src = img.getAttribute('src');
                            if (src) {
                                images.add(makeAbsoluteURL(src, pageUrl));
                            }
                        });
                    } else {
                        console.warn(`No article.article-content p found in ${pageUrl}`);
                    }
                } catch (e) {
                    console.warn(`Failed to process ${pageUrl}: ${e.message}`);
                }
            }

            // Process all pages sequentially or with concurrency
            const CONCURRENT_LIMIT = 5;
            let index = 0;

            async function processNext() {
                while (index < pageUrls.length) {
                    const currentIndex = index++;
                    const pageUrl = pageUrls[currentIndex];
                    await processPage(pageUrl);
                }
            }

            const workers = [];
            for (let i = 0; i < CONCURRENT_LIMIT; i++) {
                workers.push(processNext());
            }
            await Promise.all(workers);

            if (images.size === 0) {
                console.error('No images found.');
                alert('No images found on the target page(s).');
                return;
            }

            const imageArray = Array.from(images);
            console.log(`Total images collected: ${imageArray.length}`);

            // Initialize JSZip
            const zip = new JSZip();
            let counter = 0;
            const totalImages = imageArray.length;

            for (const imgUrl of imageArray) {
                try {
                    const blob = await fetchImage(imgUrl);
                    if (!blob) {
                        console.warn(`Skipping image due to fetch failure: ${imgUrl}`);
                        continue;
                    }

                    const extension = getFileExtension(imgUrl);
                    const validExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff'];
                    const finalExtension = validExtensions.includes(extension) ? extension : 'jpg';
                    const filename = `${String(counter + 1).padStart(3, '0')}.${finalExtension}`;
                    zip.file(filename, blob);
                    counter++;
                    console.log(`Added ${filename} to ZIP (${counter}/${totalImages})`);
                } catch (e) {
                    console.warn(`Failed to fetch/add image ${imgUrl}: ${e.message}`);
                }
            }

            if (counter === 0) {
                console.error('No images were successfully added to the ZIP.');
                alert('No images were successfully downloaded.');
                return;
            }

            console.log(`Successfully added ${counter} images to the ZIP.`);
            await createAndDownloadZip(zip, `${TITLE}.zip`);

        } catch (error) {
            console.error(`Error during image download process: ${error.message}`);
            alert('An error occurred during the image download process.');
        }
    }

    /**
     * Function to add a DOWNLOAD button to a target element with an associated linkUrl.
     * @param {HTMLElement} targetElement - The element to which the button will be appended.
     * @param {string} linkUrl - The URL to be processed when the button is clicked.
     * @param {boolean} useCurrentPage - If true, processes the current page without fetching linkUrl.
     */
    function addDownloadButton(targetElement, linkUrl, useCurrentPage = false) {
        const button = document.createElement('button');
        button.innerText = 'DOWNLOAD';
        button.classList.add('download-button');
        // Styling the button for better appearance
        button.style.marginLeft = '10px';
        button.style.padding = '5px 10px';
        button.style.cursor = 'pointer';
        button.style.backgroundColor = '#28a745';
        button.style.color = '#fff';
        button.style.border = 'none';
        button.style.borderRadius = '4px';
        button.style.fontSize = '14px';
        button.style.transition = 'background-color 0.3s';

        // Hover effects for the button
        button.addEventListener('mouseenter', () => {
            button.style.backgroundColor = '#218838';
        });
        button.addEventListener('mouseleave', () => {
            button.style.backgroundColor = '#28a745';
        });

        // Click event to initiate the download process
        button.addEventListener('click', async (event) => {
            event.preventDefault(); // Prevent default behavior if applicable
            button.disabled = true;
            const originalText = button.innerText;
            button.innerText = 'Processing...';
            console.log(`DOWNLOAD button clicked for URL: ${linkUrl}, useCurrentPage: ${useCurrentPage}`);

            await downloadImagesFromUrl(linkUrl, useCurrentPage);

            button.innerText = originalText;
            button.disabled = false;
        });

        targetElement.appendChild(button);
        console.log(`DOWNLOAD button added to`, targetElement);
    }

    /**
     * Function to find relevant elements and add DOWNLOAD buttons accordingly.
     * Differentiates between Button1 and Button2 based on element location.
     */
    function findAndAddButtons() {
        // Button 1: Adding DOWNLOAD button to div.node p a b
        const titleLinks = document.querySelectorAll('div.node p a b');
        console.log(`Found ${titleLinks.length} <b> elements within div.node p a`);

        titleLinks.forEach((bElement, index) => {
            const aElement = bElement.closest('a'); // Find the closest parent <a>
            if (!aElement) {
                console.warn(`No parent <a> found for <b> element at index ${index}.`);
                return;
            }

            const href = aElement.getAttribute('href');
            if (!href) {
                console.warn(`No href found in parent <a> element for <b> at index ${index}.`);
                return;
            }

            const span = bElement.querySelector('span');
            const targetElement = span || bElement; // Use <span> if available, else <b>

            if (!targetElement.querySelector('button.download-button')) {
                const linkUrl = makeAbsoluteURL(href, window.location.href);
                console.log(`Adding DOWNLOAD button to`, targetElement, `for URL: ${linkUrl}`);
                addDownloadButton(targetElement, linkUrl, false); // useCurrentPage = false
            } else {
                console.log(`DOWNLOAD button already exists for <b> element at index ${index}.`);
            }
        });

        // Button 2: Adding DOWNLOAD button to div.toptip
        const itemInfoDivs = document.querySelectorAll('div.toptip');
        console.log(`Found ${itemInfoDivs.length} div.toptip elements`);

        itemInfoDivs.forEach((div, index) => {
            if (!div.querySelector('button.download-button')) {
                console.log(`Adding DOWNLOAD button to div.toptip at index ${index}`);
                addDownloadButton(div, window.location.href, true); // useCurrentPage = true
            } else {
                console.log(`DOWNLOAD button already exists for div.toptip at index ${index}`);
            }
        });
    }

    /**
     * Function to initialize the script: inject JSZip and add buttons
     */
    async function initializeScript() {
        console.log('Initializing WebURL Image Downloader script...');
        try {
            await injectScript('https://cdn.jsdelivr.net/npm/jszip@3.10.0/dist/jszip.min.js');
            if (typeof JSZip === 'undefined') {
                throw new Error('JSZip failed to load after injection.');
            }
            console.log('JSZip loaded successfully. Version:', JSZip.version);
            findAndAddButtons();

            // Optional: Observe DOM changes to add buttons dynamically if the page is single-page or dynamically loaded
            const observer = new MutationObserver((mutations) => {
                for (const mutation of mutations) {
                    if (mutation.type === 'childList') {
                        findAndAddButtons();
                    }
                }
            });

            observer.observe(document.body, { childList: true, subtree: true });
            console.log('MutationObserver set to watch for new elements to add DOWNLOAD buttons.');

        } catch (error) {
            console.error('Failed to inject JSZip:', error.message);
            alert('Failed to load JSZip library. Please check the console for details.');
        }
    }

    // Wait for the DOM to be fully loaded before initializing
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initializeScript);
    } else {
        initializeScript();
    }

})();

Do you understand this script?