ExHentai Download Button with Batch Download Support

Adds download buttons (Original, Resample, H@H) to ExHentai/E-Hentai galleries with batch download support. Features progress tracking, error logging, and GP-aware downloading.

// ==UserScript==
// @name         ExHentai Download Button with Batch Download Support
// @namespace    https://github.com/troyt-666/exhentai-utilities
// @version      1.3.0
// @description  Adds download buttons (Original, Resample, H@H) to ExHentai/E-Hentai galleries with batch download support. Features progress tracking, error logging, and GP-aware downloading.
// @author       Troy T
// @homepageURL  https://github.com/troyt-666/exhentai-utilities
// @supportURL   https://github.com/troyt-666/exhentai-utilities/issues
// @match        https://exhentai.org/
// @match        https://exhentai.org/?*
// @match        https://e-hentai.org/
// @match        https://e-hentai.org/?*
// @connect      exhentai.org
// @connect      e-hentai.org
// @connect      hath.network
// @connect      *.hath.network
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// @icon         https://exhentai.org/favicon.ico
// ==/UserScript==

/*
 * ExHentai Download Button with Batch Support
 * 
 * This userscript enhances ExHentai/E-Hentai gallery pages by adding:
 * - Individual download buttons for each gallery (Original, Resample, H@H)
 * - Batch download functionality with progress tracking
 * - Error logging and retry capabilities
 * - Toast notifications for H@H operations
 * 
 * Part of the ExHentai Utilities toolkit:
 * https://github.com/troyt-666/exhentai-utilities
 * 
 * Installation:
 * 1. Install Tampermonkey or compatible userscript manager
 * 2. Click on this script's raw URL
 * 3. Tampermonkey will prompt to install
 * 
 * Usage:
 * - Individual downloads: Click buttons on each gallery
 * - Batch downloads: Use checkboxes and batch panel (top-right)
 * - H@H downloads are queued to your Hentai@Home client
 * 
 * Note: All downloads follow standard ExHentai rules and consume GP
 */

(function() {
    'use strict';

    // Function to show a toast notification with a shrinking progress bar
    function showToast(message) {
        // Create the toast container
        var toast = document.createElement('div');
        toast.style.position = 'fixed';
        toast.style.bottom = '20px';
        toast.style.right = '20px';
        toast.style.padding = '15px';
        toast.style.backgroundColor = '#333';
        toast.style.color = '#fff';
        toast.style.borderRadius = '5px';
        toast.style.boxShadow = '0px 0px 10px rgba(0,0,0,0.5)';
        toast.style.zIndex = '10000';
        toast.style.fontSize = '14px';
        toast.style.display = 'flex';
        toast.style.flexDirection = 'column';  // Stack icon/text and progress bar vertically
        toast.style.alignItems = 'center';
        toast.style.width = '300px';

        // Add an icon (optional)
        var icon = document.createElement('div');
        icon.innerHTML = '✔'; // Checkmark icon (you can replace this with any other icon)
        icon.style.marginBottom = '10px';
        icon.style.color = '#4CAF50'; // Green color for success
        icon.style.fontSize = '20px';
        toast.appendChild(icon);

        // Add the message
        var messageText = document.createElement('span');
        messageText.textContent = message;
        toast.appendChild(messageText);

        // Create a progress bar at the bottom of the toast
        var progressBar = document.createElement('div');
        progressBar.style.height = '5px';
        progressBar.style.width = '100%';
        progressBar.style.backgroundColor = '#4CAF50';  // Green progress bar
        progressBar.style.borderRadius = '0 0 5px 5px';  // Rounded corners only at the bottom
        progressBar.style.transition = 'width 3s linear';  // Smooth shrink over 3 seconds
        toast.appendChild(progressBar);

        // Append the toast to the body
        document.body.appendChild(toast);

        // Start the progress bar shrinking
        setTimeout(function() {
            progressBar.style.width = '0';  // Shrink the width to 0 over 3 seconds
        }, 100);  // Small delay to ensure the progress bar appears at full width first

        // Automatically remove the toast after 3 seconds
        setTimeout(function() {
            toast.style.transition = 'opacity 0.5s ease-in-out';
            toast.style.opacity = '0';
            setTimeout(function() {
                document.body.removeChild(toast);
            }, 500); // Wait for the fade-out transition to complete
        }, 3000); // Display for 3 seconds
    }

    // Function to show batch download progress
    function showBatchProgress(current, total, galleryTitle = '') {
        // Remove existing progress modal if any
        var existingModal = document.getElementById('batch-progress-modal');
        if (existingModal) {
            existingModal.remove();
        }

        // Create progress modal
        var modal = document.createElement('div');
        modal.id = 'batch-progress-modal';
        modal.style.position = 'fixed';
        modal.style.top = '50%';
        modal.style.left = '50%';
        modal.style.transform = 'translate(-50%, -50%)';
        modal.style.padding = '20px';
        modal.style.backgroundColor = '#333';
        modal.style.color = '#fff';
        modal.style.borderRadius = '10px';
        modal.style.boxShadow = '0px 0px 20px rgba(0,0,0,0.8)';
        modal.style.zIndex = '10001';
        modal.style.fontSize = '14px';
        modal.style.width = '400px';
        modal.style.textAlign = 'center';

        // Title
        var title = document.createElement('h3');
        title.textContent = 'Batch H@H Download Progress';
        title.style.margin = '0 0 15px 0';
        title.style.color = '#4CAF50';
        modal.appendChild(title);

        // Current gallery info
        var currentInfo = document.createElement('div');
        currentInfo.textContent = galleryTitle ? `Processing: ${galleryTitle}` : `Processing gallery ${current} of ${total}`;
        currentInfo.style.marginBottom = '15px';
        currentInfo.style.fontSize = '12px';
        modal.appendChild(currentInfo);

        // Progress bar container
        var progressContainer = document.createElement('div');
        progressContainer.style.width = '100%';
        progressContainer.style.height = '20px';
        progressContainer.style.backgroundColor = '#555';
        progressContainer.style.borderRadius = '10px';
        progressContainer.style.overflow = 'hidden';
        progressContainer.style.marginBottom = '10px';

        // Progress bar
        var progressBar = document.createElement('div');
        progressBar.style.height = '100%';
        progressBar.style.backgroundColor = '#4CAF50';
        progressBar.style.width = ((current / total) * 100) + '%';
        progressBar.style.transition = 'width 0.3s ease';
        progressContainer.appendChild(progressBar);
        modal.appendChild(progressContainer);

        // Progress text
        var progressText = document.createElement('div');
        progressText.textContent = `${current} of ${total} completed (${Math.round((current / total) * 100)}%)`;
        progressText.style.fontSize = '12px';
        modal.appendChild(progressText);

        document.body.appendChild(modal);
    }

    // Function to hide batch progress
    function hideBatchProgress() {
        var modal = document.getElementById('batch-progress-modal');
        if (modal) {
            modal.remove();
        }
    }


    // Create batch download control panel
    function createBatchDownloadPanel() {
        var panel = document.createElement('div');
        panel.id = 'batch-download-panel';
        panel.style.position = 'fixed';
        panel.style.top = '10px';
        panel.style.right = '10px';
        panel.style.backgroundColor = '#333';
        panel.style.color = '#fff';
        panel.style.borderRadius = '5px';
        panel.style.boxShadow = '0px 0px 10px rgba(0,0,0,0.5)';
        panel.style.zIndex = '9999';
        panel.style.fontSize = '12px';
        panel.style.transition = 'all 0.3s ease';

        // Create toggle button (always visible)
        var toggleButton = document.createElement('div');
        toggleButton.id = 'batch-toggle-btn';
        toggleButton.innerHTML = '📥 Batch H@H';
        toggleButton.style.padding = '8px 12px';
        toggleButton.style.cursor = 'pointer';
        toggleButton.style.backgroundColor = '#4CAF50';
        toggleButton.style.borderRadius = '5px';
        toggleButton.style.fontWeight = 'bold';
        toggleButton.style.textAlign = 'center';
        toggleButton.style.userSelect = 'none';
        toggleButton.style.fontSize = '11px';
        toggleButton.title = 'Click to expand/collapse batch download panel';
        panel.appendChild(toggleButton);

        // Create collapsible content container
        var contentContainer = document.createElement('div');
        contentContainer.id = 'batch-content';
        contentContainer.style.display = 'none';
        contentContainer.style.padding = '10px';
        contentContainer.style.borderTop = '1px solid #555';
        contentContainer.style.marginTop = '0';

        // Title for expanded view
        var title = document.createElement('div');
        title.textContent = 'Batch H@H Download';
        title.style.fontWeight = 'bold';
        title.style.textAlign = 'center';
        title.style.marginBottom = '10px';
        contentContainer.appendChild(title);

        // Controls container
        var controls = document.createElement('div');
        controls.style.display = 'flex';
        controls.style.flexDirection = 'column';
        controls.style.gap = '8px';
        controls.style.minWidth = '180px';

        // Select All/None buttons
        var selectButtonsContainer = document.createElement('div');
        selectButtonsContainer.style.display = 'flex';
        selectButtonsContainer.style.gap = '5px';

        var selectAllBtn = document.createElement('button');
        selectAllBtn.textContent = 'Select All';
        selectAllBtn.style.flex = '1';
        selectAllBtn.style.padding = '3px 6px';
        selectAllBtn.style.fontSize = '11px';
        selectAllBtn.style.cursor = 'pointer';
        selectAllBtn.style.border = '1px solid #555';
        selectAllBtn.style.backgroundColor = '#444';
        selectAllBtn.style.color = '#fff';
        selectAllBtn.style.borderRadius = '3px';

        var selectNoneBtn = document.createElement('button');
        selectNoneBtn.textContent = 'Select None';
        selectNoneBtn.style.flex = '1';
        selectNoneBtn.style.padding = '3px 6px';
        selectNoneBtn.style.fontSize = '11px';
        selectNoneBtn.style.cursor = 'pointer';
        selectNoneBtn.style.border = '1px solid #555';
        selectNoneBtn.style.backgroundColor = '#444';
        selectNoneBtn.style.color = '#fff';
        selectNoneBtn.style.borderRadius = '3px';

        selectButtonsContainer.appendChild(selectAllBtn);
        selectButtonsContainer.appendChild(selectNoneBtn);
        controls.appendChild(selectButtonsContainer);

        // Selected count
        var selectedCount = document.createElement('div');
        selectedCount.id = 'selected-count';
        selectedCount.textContent = 'Selected: 0';
        selectedCount.style.textAlign = 'center';
        selectedCount.style.fontSize = '11px';
        selectedCount.style.color = '#aaa';
        controls.appendChild(selectedCount);

        // Download button
        var batchDownloadBtn = document.createElement('button');
        batchDownloadBtn.textContent = 'Download Selected H@H';
        batchDownloadBtn.style.padding = '8px';
        batchDownloadBtn.style.fontSize = '11px';
        batchDownloadBtn.style.cursor = 'pointer';
        batchDownloadBtn.style.border = '1px solid #4CAF50';
        batchDownloadBtn.style.backgroundColor = '#4CAF50';
        batchDownloadBtn.style.color = '#fff';
        batchDownloadBtn.style.borderRadius = '3px';
        batchDownloadBtn.style.fontWeight = 'bold';
        controls.appendChild(batchDownloadBtn);

        // Error log container (initially hidden)
        var errorLogContainer = document.createElement('div');
        errorLogContainer.id = 'error-log-container';
        errorLogContainer.style.display = 'none';
        errorLogContainer.style.marginTop = '8px';
        errorLogContainer.style.border = '1px solid #ff6b6b';
        errorLogContainer.style.borderRadius = '3px';
        errorLogContainer.style.backgroundColor = '#2a1a1a';

        var errorLogTitle = document.createElement('div');
        errorLogTitle.textContent = 'Download Errors:';
        errorLogTitle.style.fontSize = '10px';
        errorLogTitle.style.color = '#ff6b6b';
        errorLogTitle.style.padding = '3px 5px';
        errorLogTitle.style.borderBottom = '1px solid #ff6b6b';
        errorLogTitle.style.fontWeight = 'bold';
        errorLogContainer.appendChild(errorLogTitle);

        var errorLogText = document.createElement('textarea');
        errorLogText.id = 'error-log-text';
        errorLogText.style.width = '100%';
        errorLogText.style.height = '80px';
        errorLogText.style.fontSize = '9px';
        errorLogText.style.backgroundColor = 'transparent';
        errorLogText.style.color = '#ff9999';
        errorLogText.style.border = 'none';
        errorLogText.style.padding = '5px';
        errorLogText.style.resize = 'none';
        errorLogText.style.outline = 'none';
        errorLogText.readOnly = true;
        errorLogContainer.appendChild(errorLogText);

        var clearLogBtn = document.createElement('button');
        clearLogBtn.textContent = 'Clear Log';
        clearLogBtn.style.width = '100%';
        clearLogBtn.style.padding = '3px';
        clearLogBtn.style.fontSize = '9px';
        clearLogBtn.style.cursor = 'pointer';
        clearLogBtn.style.border = '1px solid #ff6b6b';
        clearLogBtn.style.backgroundColor = '#ff6b6b';
        clearLogBtn.style.color = '#fff';
        clearLogBtn.style.borderRadius = '0 0 3px 3px';
        clearLogBtn.addEventListener('click', function() {
            errorLogText.value = '';
            errorLogContainer.style.display = 'none';
        });
        errorLogContainer.appendChild(clearLogBtn);

        controls.appendChild(errorLogContainer);

        contentContainer.appendChild(controls);
        panel.appendChild(contentContainer);
        document.body.appendChild(panel);

        // Toggle functionality
        var isExpanded = false;
        toggleButton.addEventListener('click', function() {
            isExpanded = !isExpanded;
            if (isExpanded) {
                contentContainer.style.display = 'block';
                toggleButton.innerHTML = '📤 Batch H@H';
                toggleButton.style.backgroundColor = '#ff6b6b';
                toggleButton.title = 'Click to collapse panel';
            } else {
                contentContainer.style.display = 'none';
                toggleButton.innerHTML = '📥 Batch H@H';
                toggleButton.style.backgroundColor = '#4CAF50';
                toggleButton.title = 'Click to expand panel';
            }
        });

        // Event listeners
        selectAllBtn.addEventListener('click', function() {
            var checkboxes = document.querySelectorAll('.gallery-batch-checkbox');
            checkboxes.forEach(function(cb) { cb.checked = true; });
            updateSelectedCount();
        });

        selectNoneBtn.addEventListener('click', function() {
            var checkboxes = document.querySelectorAll('.gallery-batch-checkbox');
            checkboxes.forEach(function(cb) { cb.checked = false; });
            updateSelectedCount();
        });

        batchDownloadBtn.addEventListener('click', function() {
            startBatchDownload();
        });
    }

    // Function to update selected count
    function updateSelectedCount() {
        var checkboxes = document.querySelectorAll('.gallery-batch-checkbox:checked');
        var countElement = document.getElementById('selected-count');
        if (countElement) {
            countElement.textContent = 'Selected: ' + checkboxes.length;
        }
    }

    // Function to log errors
    function logError(galleryTitle, galleryLink, errorMessage) {
        var errorLogText = document.getElementById('error-log-text');
        var errorLogContainer = document.getElementById('error-log-container');
        
        if (errorLogText && errorLogContainer) {
            var timestamp = new Date().toLocaleTimeString();
            var logEntry = `[${timestamp}] ${galleryTitle}\n${galleryLink}\nError: ${errorMessage}\n\n`;
            errorLogText.value += logEntry;
            errorLogContainer.style.display = 'block';
            
            // Scroll to bottom of textarea
            errorLogText.scrollTop = errorLogText.scrollHeight;
        }
    }

    // Create the batch download panel
    createBatchDownloadPanel();

    // Loop through all gallery items on the search page
    var galleryItems = document.querySelectorAll('.gl1t');

    galleryItems.forEach(function(item) {
        var galleryLink = item.querySelector('a').href; // Get the gallery link
        var galleryTitle = item.querySelector('.gl4t.glname.glink') ? 
                          item.querySelector('.gl4t.glname.glink').textContent.trim() : 
                          (item.querySelector('img') ? item.querySelector('img').title : 'Unknown Gallery'); // Get gallery title

        // Add checkbox for batch download
        var checkboxContainer = document.createElement('div');
        checkboxContainer.style.display = 'flex';
        checkboxContainer.style.alignItems = 'center';
        checkboxContainer.style.gap = '5px';
        checkboxContainer.style.marginBottom = '5px';

        var checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.className = 'gallery-batch-checkbox';
        checkbox.style.cursor = 'pointer';
        checkbox.addEventListener('change', updateSelectedCount);
        // Store gallery data for batch processing
        checkbox.dataset.galleryLink = galleryLink;
        checkbox.dataset.galleryTitle = galleryTitle;

        var checkboxLabel = document.createElement('label');
        checkboxLabel.textContent = 'Batch H@H';
        checkboxLabel.style.fontSize = '11px';
        checkboxLabel.style.cursor = 'pointer';
        checkboxLabel.addEventListener('click', function() {
            checkbox.checked = !checkbox.checked;
            updateSelectedCount();
        });

        checkboxContainer.appendChild(checkbox);
        checkboxContainer.appendChild(checkboxLabel);
        item.appendChild(checkboxContainer);

        // Create a container div for buttons
        var buttonContainer = document.createElement('div');
        buttonContainer.style.display = 'flex'; // Flex container for side-by-side buttons
        buttonContainer.style.gap = '5px'; // Gap between the buttons

        // Create a button style function to match the existing page style
        function createButton(text) {
            var button = document.createElement('button');
            button.innerText = text;
            button.style.padding = '5px 10px';  // Padding similar to the existing button style
            button.style.cursor = 'pointer';    // Pointer cursor for interaction
            button.style.border = '1px solid #ccc';  // Light border similar to the website
            button.style.backgroundColor = 'transparent'; // Match the page background color
            button.style.color = 'inherit';  // Use the same text color as the page
            button.style.fontSize = '12px';  // Match the font size for the website
            return button;
        }

        // Create the Original Archive button
        var downloadOriginalButton = createButton('Download Orginal');

        // Create the Resample Archive button
        var downloadResampleButton = createButton('Download Resample');

        // Create the Remote Server Download button (H@H)
        var downloadHaHButton = createButton('Download H@H');

        // Add all buttons to the container
        buttonContainer.appendChild(downloadOriginalButton);
        buttonContainer.appendChild(downloadResampleButton);
        buttonContainer.appendChild(downloadHaHButton);

        // Add the button container to the gallery item
        item.appendChild(buttonContainer);

        // Helper function to handle downloads (Original, Resample, or H@H)
        function handleDownloadButton(archiveType) {
            console.log("Fetching gallery page: " + galleryLink); // Log the gallery link being accessed
            if (archiveType === 'hath') {
                showToast("Fetching gallery page...");
            }

            // Step 1: Fetch the gallery page to find the archive download link
            GM_xmlhttpRequest({
                method: 'GET',
                url: galleryLink,
                onload: function(response) {
                    var parser = new DOMParser();
                    var doc = parser.parseFromString(response.responseText, 'text/html');

                    // Find the anchor element with "onclick" containing "popUp"
                    var archiveDownloadAnchor = doc.querySelector('a[onclick^="return popUp"]');
                    
                    if (archiveDownloadAnchor) {
                        console.log("Found archiveDownloadAnchor:", archiveDownloadAnchor); // Log the anchor element
                        if (archiveType === 'hath') {
                            showToast("Found archive download link!");
                        }

                        // Extract the URL from the onclick attribute (popUp() call)
                        var onclickContent = archiveDownloadAnchor.getAttribute('onclick');
                        var archiveUrlMatch = onclickContent.match(/popUp\('(.+?)'/);
                        
                        if (archiveUrlMatch && archiveUrlMatch[1]) {
                            var archiveUrl = archiveUrlMatch[1];
                            console.log("Extracted archive URL:", archiveUrl); // Log the extracted URL
                            if (archiveType === 'hath') {
                                showToast("Extracted archive URL successfully!");
                            }

                            // Step 2: Fetch the page where the form exists
                            GM_xmlhttpRequest({
                                method: 'GET',
                                url: archiveUrl,
                                onload: function(archivePageResponse) {
                                    var archiveDoc = parser.parseFromString(archivePageResponse.responseText, 'text/html');

                                    // Check if we are handling the H@H download
                                    if (archiveType === 'hath') {
                                        // Handle H@H download by submitting the form for H@H server
                                        var formElement = archiveDoc.querySelector('#hathdl_form');
                                        if (formElement) {
                                            var formAction = formElement.getAttribute('action');
                                            var formData = new FormData(formElement);
                                            formData.set('hathdl_xres', 'org'); // Set to 'Original'

                                            // Submit the form for remote server download
                                            GM_xmlhttpRequest({
                                                method: 'POST',
                                                url: formAction,
                                                data: new URLSearchParams(formData), // Simulate the form submission
                                                onload: function(formSubmitResponse) {
                                                    var successMessage = "An original resolution download has been queued for client";
                                                    if (formSubmitResponse.responseText.includes(successMessage)) {
                                                        showToast("H@H download successfully queued!");
                                                        console.log("H@H download successfully queued.");
                                                    } else {
                                                        showToast("Failed to queue H@H download.");
                                                        console.log("Failed to queue H@H download.");
                                                    }
                                                }
                                            });
                                        } else {
                                            showToast('H@H form not found!');
                                            console.log("Form element for H@H download not found.");
                                        }
                                        return;
                                    }

                                    // For original or resample downloads
                                    var formElement = archiveDoc.querySelector('form[action*="archiver.php"]');
                                    var formAction = formElement.getAttribute('action');
                                    console.log("Form action URL:", formAction); // Log the form action URL

                                    // Step 3: Simulate submitting the form by POSTing the request
                                    var dltypeValue = archiveType === 'original' ? 'org' : 'res';
                                    GM_xmlhttpRequest({
                                        method: 'POST',
                                        url: formAction,
                                        headers: {
                                            'Content-Type': 'application/x-www-form-urlencoded'
                                        },
                                        data: `dltype=${dltypeValue}&dlcheck=Download ${archiveType === 'original' ? 'Original' : 'Resample'} Archive`,
                                        onload: function(formSubmitResponse) {
                                            var formSubmitDoc = parser.parseFromString(formSubmitResponse.responseText, 'text/html');

                                            // Extract the final redirect URL
                                            var redirectLink = formSubmitDoc.querySelector('#continue a') || formSubmitDoc.querySelector('script').innerText.match(/document\.location\s*=\s*"(.+?)"/)[1];

                                            if (redirectLink) {
                                                console.log(`Found final ${archiveType} download URL from redirect:`, redirectLink);

                                                // Poll for the final download link
                                                var checkForDownloadLink = function() {
                                                    GM_xmlhttpRequest({
                                                        method: 'GET',
                                                        url: redirectLink,
                                                        onload: function(downloadPageResponse) {
                                                            var downloadDoc = parser.parseFromString(downloadPageResponse.responseText, 'text/html');

                                                            // Check for the download link or message
                                                            var finalDownloadLink = downloadDoc.querySelector('a[href^="/archive/"]') || downloadDoc.body.innerHTML.includes("Click Here To Start Downloading");

                                                            if (finalDownloadLink) {
                                                                var finalUrl = redirectLink + "?start=1";
                                                                console.log("Final download URL:", finalUrl);
                                                                var a = document.createElement('a');
                                                                a.href = finalUrl;
                                                                a.style.display = 'none';
                                                                document.body.appendChild(a);
                                                                a.click();
                                                                document.body.removeChild(a);
                                                            } else {
                                                                console.log("Download link not available yet. Retrying in 2 seconds...");
                                                                setTimeout(checkForDownloadLink, 2000);
                                                            }
                                                        }
                                                    });
                                                };

                                                // Start polling for the download link
                                                checkForDownloadLink();
                                            } else {
                                                alert('Could not extract the final download URL!');
                                                console.log("Redirect URL not found in page:", formSubmitResponse.responseText);
                                            }
                                        }
                                    });
                                }
                            });
                        } else {
                            alert('Could not extract archive URL from onclick attribute!');
                            console.log("Onclick content:", onclickContent);
                        }
                    } else {
                        alert('Archive download link not found!');
                        console.log("Archive download link not found in page:", response.responseText);
                    }
                }
            });
        }

        // Add event listeners for all buttons
        downloadOriginalButton.addEventListener('click', function() {
            handleDownloadButton('original');
        });
        downloadResampleButton.addEventListener('click', function() {
            handleDownloadButton('resample');
        });
        downloadHaHButton.addEventListener('click', function() {
            handleDownloadButton('hath');
        });
    });

    // Batch download functionality
    function startBatchDownload() {
        var selectedCheckboxes = document.querySelectorAll('.gallery-batch-checkbox:checked');
        
        if (selectedCheckboxes.length === 0) {
            showToast('Please select at least one gallery to download.');
            return;
        }

        var totalCount = selectedCheckboxes.length;
        var currentCount = 0;
        var successCount = 0;
        var failCount = 0;

        showToast(`Starting batch download of ${totalCount} galleries...`);

        // Process downloads sequentially with delay
        function processNextDownload() {
            if (currentCount >= totalCount) {
                // All downloads completed
                hideBatchProgress();
                showToast(`Batch download completed! Success: ${successCount}, Failed: ${failCount}`);
                return;
            }

            var checkbox = selectedCheckboxes[currentCount];
            var galleryLink = checkbox.dataset.galleryLink;
            var galleryTitle = checkbox.dataset.galleryTitle;

            showBatchProgress(currentCount + 1, totalCount, galleryTitle);

            // Process H@H download for this gallery
            processGalleryHaHDownload(galleryLink, galleryTitle, function(success, errorMessage) {
                if (success) {
                    successCount++;
                } else {
                    failCount++;
                    if (errorMessage) {
                        logError(galleryTitle, galleryLink, errorMessage);
                    }
                }
                currentCount++;
                
                // Wait 800ms before processing next download to avoid rate limiting
                setTimeout(processNextDownload, 800);
            });
        }

        // Start processing
        processNextDownload();
    }

    // Function to process H@H download for a single gallery
    function processGalleryHaHDownload(galleryLink, galleryTitle, callback) {
        console.log("[TRACE] Batch processing gallery: " + galleryLink);
        console.log("[TRACE] Gallery title: " + galleryTitle);
        console.log("[TRACE] Fetching gallery page: " + galleryLink);

        GM_xmlhttpRequest({
            method: 'GET',
            url: galleryLink,
            onload: function(response) {
                if (response.status !== 200) {
                    console.log("[TRACE] Failed to fetch gallery page, HTTP status: " + response.status);
                    callback(false, `Failed to fetch gallery page (HTTP ${response.status})`);
                    return;
                }

                console.log("[TRACE] Successfully fetched gallery page");
                var parser = new DOMParser();
                var doc = parser.parseFromString(response.responseText, 'text/html');

                var archiveDownloadAnchor = doc.querySelector('a[onclick^="return popUp"]');
                
                if (archiveDownloadAnchor) {
                    console.log("[TRACE] Found archiveDownloadAnchor:", archiveDownloadAnchor);
                    console.log("[TRACE] Found archive download link!");
                    
                    var onclickContent = archiveDownloadAnchor.getAttribute('onclick');
                    console.log("[TRACE] Onclick content: " + onclickContent);
                    var archiveUrlMatch = onclickContent.match(/popUp\('(.+?)'/);
                    
                    if (archiveUrlMatch && archiveUrlMatch[1]) {
                        var archiveUrl = archiveUrlMatch[1];
                        console.log("[TRACE] Extracted archive URL: " + archiveUrl);
                        console.log("[TRACE] Extracted archive URL successfully!");

                        GM_xmlhttpRequest({
                            method: 'GET',
                            url: archiveUrl,
                            onload: function(archivePageResponse) {
                                if (archivePageResponse.status !== 200) {
                                    console.log("[TRACE] Failed to fetch archive page, HTTP status: " + archivePageResponse.status);
                                    callback(false, `Failed to fetch archive page (HTTP ${archivePageResponse.status})`);
                                    return;
                                }

                                console.log("[TRACE] Successfully fetched archive page");
                                var archiveDoc = parser.parseFromString(archivePageResponse.responseText, 'text/html');

                                var formElement = archiveDoc.querySelector('#hathdl_form');
                                if (formElement) {
                                    console.log("[TRACE] Found H@H form element");
                                    var formAction = formElement.getAttribute('action');
                                    console.log("[TRACE] Form action URL: " + formAction);
                                    var formData = new FormData(formElement);
                                    formData.set('hathdl_xres', 'org');
                                    console.log("[TRACE] Set hathdl_xres to 'org' for original resolution");

                                    console.log("[TRACE] Submitting H@H form...");
                                    GM_xmlhttpRequest({
                                        method: 'POST',
                                        url: formAction,
                                        data: new URLSearchParams(formData),
                                        onload: function(formSubmitResponse) {
                                            if (formSubmitResponse.status !== 200) {
                                                console.log("[TRACE] Failed to submit H@H form, HTTP status: " + formSubmitResponse.status);
                                                callback(false, `Failed to submit H@H form (HTTP ${formSubmitResponse.status})`);
                                                return;
                                            }

                                            console.log("[TRACE] Successfully submitted H@H form");
                                            var successMessage = "An original resolution download has been queued for client";
                                            var success = formSubmitResponse.responseText.includes(successMessage);
                                            
                                            if (success) {
                                                console.log("[TRACE] H@H download successfully queued for: " + galleryTitle);
                                                callback(true);
                                            } else {
                                                console.log("[TRACE] H@H queue failed - success message not found in response");
                                                console.log("[TRACE] Response text: " + formSubmitResponse.responseText.substring(0, 500) + "...");
                                                // Try to extract error message from response
                                                var errorDoc = parser.parseFromString(formSubmitResponse.responseText, 'text/html');
                                                var errorElement = errorDoc.querySelector('.stuffbox') || errorDoc.querySelector('p');
                                                var errorText = errorElement ? errorElement.textContent.trim() : 'Unknown error occurred';
                                                console.log("[TRACE] Extracted error text: " + errorText);
                                                callback(false, `H@H queue failed: ${errorText}`);
                                            }
                                        },
                                        onerror: function(error) {
                                            console.log("[TRACE] Network error submitting H@H form: " + (error.error || 'Unknown network error'));
                                            callback(false, `Network error submitting H@H form: ${error.error || 'Unknown network error'}`);
                                        }
                                    });
                                } else {
                                    console.log("[TRACE] H@H form element not found on archive page");
                                    console.log("[TRACE] Archive page HTML: " + archivePageResponse.responseText.substring(0, 1000) + "...");
                                    callback(false, 'H@H form not found on archive page');
                                }
                            },
                            onerror: function(error) {
                                console.log("[TRACE] Network error fetching archive page: " + (error.error || 'Unknown network error'));
                                callback(false, `Network error fetching archive page: ${error.error || 'Unknown network error'}`);
                            }
                        });
                    } else {
                        console.log("[TRACE] Could not extract archive URL from onclick attribute");
                        console.log("[TRACE] Onclick content was: " + onclickContent);
                        callback(false, 'Could not extract archive URL from gallery page');
                    }
                } else {
                    console.log("[TRACE] Archive download link not found on gallery page");
                    console.log("[TRACE] Gallery page HTML: " + response.responseText.substring(0, 1000) + "...");
                    callback(false, 'Archive download link not found on gallery page');
                }
            },
            onerror: function(error) {
                console.log("[TRACE] Network error fetching gallery page: " + (error.error || 'Unknown network error'));
                callback(false, `Network error fetching gallery page: ${error.error || 'Unknown network error'}`);
            }
        });
    }
})();