MLTSHP Image Saver

Adds download buttons to save MLTSHP images with their post titles as filenames

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

You will need to install an extension such as Tampermonkey to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         MLTSHP Image Saver
// @namespace    https://mltshp.com/
// @version      0.2
// @description  Adds download buttons to save MLTSHP images with their post titles as filenames
// @author       You
// @match        https://mltshp.com/*
// @match        https://mltshp-cdn.com/*
// @exclude      https://mltshp-cdn.com/r/*
// @icon         https://mltshp.com/static/images/apple-touch-icon.png
// @grant        GM_registerMenuCommand
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // DEBUG MODE - set to true for verbose console logging
    const DEBUG = false;

    // Store image-to-title mappings
    const IMAGE_MAP = new Map();
    
    // Store processed images to prevent duplicates
    const PROCESSED_IMAGES = new Set();

    // Control visibility of description button
    const SHOW_DESCRIPTION_BUTTON = false;

    // Image Caption Generator API configuration
    const IMAGE_CAPTION_API_URL = 'https://imagecaptiongenerator.com/api/generate';

    // Log function that only outputs when debug is enabled
    function log(...args) {
        if (DEBUG) {
            console.log('[MLTSHP Saver]', ...args);
        }
    }

    // Add CSS for the download button - using standard DOM methods instead of GM_addStyle
    function addStyles() {
        const styleElement = document.createElement('style');
        styleElement.textContent = `
            .mltshp-download-btn {
                background-color: green;
                color: white;
                padding: 6px 10px;
                border: 0px solid #000;
                border-radius: 4px;
                cursor: pointer;
                font-size: 14px;
                font-weight: normal;
                margin-left: 8px;
                display: none; /* Hide by default */
                text-decoration: none;
                transition: all 0.3s;
                vertical-align: middle;
                position: absolute;
                bottom: 5px;
                right: 5px;
                z-index: 9999;
                box-shadow: 0 2px 5px rgba(0,0,0,0.3);
            }
            
            .mltshp-describe-btn {
                background-color: #2196F3;
                color: white;
                padding: 6px 10px;
                border: 0px solid #000;
                border-radius: 4px;
                cursor: pointer;
                font-size: 14px;
                font-weight: normal;
                margin-right: 5px;
                display: none; /* Hide by default */
                text-decoration: none;
                transition: all 0.3s;
                vertical-align: middle;
                position: absolute;
                bottom: 5px;
                right: 80px;
                z-index: 9999;
                box-shadow: 0 2px 5px rgba(0,0,0,0.3);
            }
            
            .mltshp-description-container {
                display: none;
                position: absolute;
                bottom: -100px;
                left: 0;
                right: 0;
                background: rgba(0, 0, 0, 0.8);
                padding: 10px;
                border-radius: 4px;
                z-index: 10000;
                transition: opacity 0.3s ease-in-out;
                opacity: 0;
            }
            
            ${SHOW_DESCRIPTION_BUTTON ? `
            .mltshp-image-container:hover .mltshp-description-container {
                display: block;
                opacity: 1;
            }
            ` : ''}
            
            .mltshp-description-text {
                color: white;
                font-size: 12px;
                margin-bottom: 5px;
                max-height: 80px;
                overflow-y: auto;
            }
            
            .mltshp-copy-btn {
                background-color: #4CAF50;
                color: white;
                padding: 4px 8px;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                font-size: 12px;
                float: right;
            }
            
            /* Show buttons on image hover */
            .mltshp-image-container:hover .mltshp-download-btn,
            .mltshp-image-container:hover .mltshp-describe-btn {
                display: inline-block;
            }
            
            .mltshp-image-container {
                position: relative;
                display: inline-block;
            }
            
            @keyframes pulse-attention {
                0% { transform: scale(1); }
                50% { transform: scale(1.1); }
                100% { transform: scale(1); }
            }
            
            .mltshp-download-btn:hover {
                background-color: #ff7043;
                transform: scale(1.05);
            }
            
            .mltshp-download-btn:active {
                background-color: #e64a19;
                transform: scale(0.95);
            }
            
            /* For debugging: Highlight images that have been processed */
            .mltshp-processed-image {
                outline: ${DEBUG ? '1px solid #ff5722' : 'none'};
            }
            
            /* For debugging: Highlight title elements */
            .mltshp-title-element {
                outline: ${DEBUG ? '1px dashed #2196F3' : 'none'};
            }
            
            /* For debugging: Highlight describe button */
            .mltshp-describe-btn {
                outline: ${DEBUG ? '1px dashed #4CAF50' : 'none'};
            }
            
            /* For debugging: Highlight description container */
            .mltshp-description-container {
                outline: ${DEBUG ? '1px dashed #FFC107' : 'none'};
            }
            
            /* Floating button as a fallback */
            .mltshp-floating-btn {
                position: fixed;
                top: -80px; /* Moved 50px higher */
                right: 20px;
                z-index: 10000;
                padding: 8px 15px;
                background-color: #ff5722;
                color: white;
                font-weight: bold;
                border: none;
                border-radius: 4px;
                box-shadow: 0 2px 10px rgba(0,0,0,0.4);
                cursor: pointer;
            }
            
            /* Floating debug info panel */
            .mltshp-debug-panel {
                position: fixed;
                bottom: 10px;
                right: 10px;
                background: rgba(0, 0, 0, 0.8);
                color: white;
                padding: 10px;
                border-radius: 5px;
                font-size: 12px;
                z-index: 10001;
                max-width: 300px;
                max-height: 200px;
                overflow: auto;
                display: ${DEBUG ? 'block' : 'none'};
            }
        `;
        document.head.appendChild(styleElement);
    }
    
    // Add styles immediately
    addStyles();

    // Create a floating debug button
    function addFloatingButton() {
        const button = document.createElement('button');
        button.className = 'mltshp-floating-btn';
        button.textContent = 'Force Add Buttons';
        button.addEventListener('click', function() {
            scanPageAndAddButtons(true);
        });
        document.body.appendChild(button);
        
        if (DEBUG) {
            // Add debug panel
            const debugPanel = document.createElement('div');
            debugPanel.className = 'mltshp-debug-panel';
            debugPanel.innerHTML = '<h4>MLTSHP Image Saver Debug</h4><div id="mltshp-debug-content"></div>';
            document.body.appendChild(debugPanel);
        }
    }
    
    // Helper to update debug panel
    function updateDebugPanel(message) {
        if (!DEBUG) return;
        
        const panel = document.getElementById('mltshp-debug-content');
        if (panel) {
            panel.innerHTML = message + panel.innerHTML;
            if (panel.children.length > 10) {
                panel.removeChild(panel.lastChild);
            }
        }
    }
    
    // Function to check if an image should be skipped
    function shouldSkipImage(img) {
        // Skip logo-compact.svg and any header logos
        if (img.src.includes('logo-compact.svg') || 
            img.src.includes('logo') || 
            img.classList.contains('logo') || 
            (img.id && img.id.includes('logo')) ||
            (img.parentElement && 
             (img.parentElement.classList.contains('header') || 
              (img.parentElement.id && img.parentElement.id.includes('header'))))) {
            log('Skipping header/logo image:', img.src);
            return true;
        }
        
        // Skip very small images (likely icons)
        if (img.width < 50 || img.height < 50) {
            log('Skipping small image:', img.src);
            return true;
        }
        
        // Skip if image is not from MLTSHP domains
        if (!img.src.includes('mltshp.com') && !img.src.includes('mltshp-cdn.com')) {
            log('Skipping non-MLTSHP image:', img.src);
            return true;
        }
        
        return false;
    }
    
    // Function to get the full-size image URL (remove width and other parameters)
    function getFullSizeImageUrl(url) {
        // In case the URL has HTML entities, decode them. e.g. &amp; -> &
        const decodedUrl = url.replace(/&amp;/g, '&');

        // If the URL contains query parameters, remove them to get the original image
        if (decodedUrl.includes('?')) {
            return decodedUrl.split('?')[0];
        }
        return decodedUrl;
    }

    // Function to sanitize filenames by removing special characters
    function sanitizeFilename(filename) {
        // Replace special characters with dashes, keep spaces
        return filename
            .replace(/[\\/:*?"<>|]/g, '-') // Replace Windows illegal characters
            .replace(/\s{2,}/g, ' ')       // Replace multiple spaces with single space
            .replace(/^\s+|\s+$/g, '')     // Trim leading/trailing spaces
            .replace(/[^\w\-\. ]/g, '-')   // Replace other non-word chars (keep hyphens, periods and spaces)
            .substring(0, 100);            // Limit filename length
    }

    // NEW: Function to detect all posts and create image -> title mappings
    function buildImageTitleMap() {
        log('Building comprehensive image-title mapping...');
        
        // Clear any existing mappings
        IMAGE_MAP.clear();
        
        // 1. First identify all post containers on the page
        const postSelectors = [
            '.post', '.shakeshingle', 'article', 'li', '.content div', '.item', 
            '.span-6', '.span-8', '.image-content', '.the-image'
        ];
        
        const allPotentialContainers = [];
        postSelectors.forEach(selector => {
            document.querySelectorAll(selector).forEach(container => {
                allPotentialContainers.push(container);
            });
        });
        
        log(`Found ${allPotentialContainers.length} potential post containers`);
        
        // 2. Process each container to find image and title pairs
        allPotentialContainers.forEach((container, index) => {
            // Find images in this container
            const images = container.querySelectorAll('img');
            if (images.length === 0) return;
            
            // Find title elements in this container
            const h3Elements = container.querySelectorAll('h3');
            const titleElements = [...h3Elements];
            
            // Also look for other potential title elements
            const otherTitleElements = container.querySelectorAll('.description, .caption, p, h1, h2, h4');
            titleElements.push(...otherTitleElements);
            
            if (titleElements.length === 0) return;
            
            log(`Container ${index} has ${images.length} images and ${titleElements.length} potential title elements`);
            
            // Map each image to its most likely title
            images.forEach(img => {
                // Skip unwanted images
                if (shouldSkipImage(img)) return;
                
                const imageUrl = getFullSizeImageUrl(img.src);
                
                // If we already have this image mapped, skip
                if (IMAGE_MAP.has(imageUrl)) return;
                
                // Find the best title match for this image
                let bestTitleElement = null;
                let shortestDistance = Infinity;
                
                // Get image position
                const imgRect = img.getBoundingClientRect();
                
                // Calculate distance to each title element
                titleElements.forEach(titleEl => {
                    // Skip empty title elements
                    if (!titleEl.textContent || titleEl.textContent.trim().length < 3) return;
                    
                    const titleRect = titleEl.getBoundingClientRect();
                    
                    // Prefer title elements above the image
                    let distance;
                    if (titleRect.bottom <= imgRect.top) {
                        // Title is above image (preferred)
                        distance = imgRect.top - titleRect.bottom;
                    } else if (titleRect.top >= imgRect.bottom) {
                        // Title is below image (less preferred)
                        distance = titleRect.top - imgRect.bottom + 1000; // Add penalty
                    } else {
                        // Title overlaps image (least preferred)
                        distance = Math.abs(titleRect.top - imgRect.top) + 2000; // Add larger penalty
                    }
                    
                    // If this title is closer than our current best, update
                    if (distance < shortestDistance) {
                        shortestDistance = distance;
                        bestTitleElement = titleEl;
                    }
                });
                
                // If we found a title, map this image to it
                if (bestTitleElement) {
                    const title = bestTitleElement.textContent.trim();
                    log(`Mapping image ${imageUrl} to title "${title}"`);
                    
                    // Store the mapping
                    IMAGE_MAP.set(imageUrl, {
                        title: title,
                        titleElement: bestTitleElement,
                        distance: shortestDistance
                    });
                    
                    // Mark the title element for debugging
                    if (DEBUG) {
                        bestTitleElement.classList.add('mltshp-title-element');
                        bestTitleElement.title = `Mapped to image: ${imageUrl}`;
                    }
                } else {
                    // Try fallback methods for finding a title
                    
                    // Check alt text
                    if (img.alt && img.alt.trim() && img.alt !== 'alt text') {
                        IMAGE_MAP.set(imageUrl, {
                            title: img.alt.trim(),
                            titleElement: img,
                            source: 'alt-text'
                        });
                        log(`Using alt text for ${imageUrl}: "${img.alt.trim()}"`);
                        return;
                    }
                    
                    // Check for post URL ID
                    const postIdMatch = imageUrl.match(/\/([A-Za-z0-9]+)$/);
                    if (postIdMatch && postIdMatch[1]) {
                        const postId = postIdMatch[1];
                        const title = `MLTSHP Post ${postId}`;
                        IMAGE_MAP.set(imageUrl, {
                            title: title,
                            titleElement: img.parentElement,
                            source: 'post-id'
                        });
                        log(`Using post ID for ${imageUrl}: "${title}"`);
                        return;
                    }
                    
                    // Final fallback: create unique name
                    const uniqueId = Math.floor(Math.random() * 10000);
                    const fallbackTitle = `MLTSHP Image ${uniqueId} ${new Date().toISOString().slice(0, 10)}`;
                    IMAGE_MAP.set(imageUrl, {
                        title: fallbackTitle,
                        titleElement: img.parentElement,
                        source: 'fallback'
                    });
                    log(`Using fallback title for ${imageUrl}: "${fallbackTitle}"`);
                }
            });
        });
        
        // 3. Special case for single post pages
        if (window.location.pathname.includes('/p/')) {
            const mainImage = document.querySelector('.image-content img, .the-image img');
            const mainHeading = document.querySelector('h1, h2, h3');
            
            if (mainImage && mainHeading && mainHeading.textContent.trim()) {
                const imageUrl = getFullSizeImageUrl(mainImage.src);
                const title = mainHeading.textContent.trim();
                
                IMAGE_MAP.set(imageUrl, {
                    title: title,
                    titleElement: mainHeading,
                    source: 'single-post'
                });
                
                log(`Single post page: Mapped main image to title "${title}"`);
                
                if (DEBUG) {
                    mainHeading.classList.add('mltshp-title-element');
                }
            }
        }
        
        // Log summary
        log(`Completed mapping with ${IMAGE_MAP.size} image-title pairs`);
        
        // Debug output of all mappings
        if (DEBUG) {
            let debugMessage = '<strong>Image-Title Mappings:</strong><br>';
            IMAGE_MAP.forEach((value, key) => {
                debugMessage += `• ${key.substring(key.lastIndexOf('/') + 1)}: "${value.title.substring(0, 20)}${value.title.length > 20 ? '...' : ''}"<br>`;
            });
            updateDebugPanel(debugMessage);
        }
        
        return IMAGE_MAP.size;
    }
    
    // Function to get the title for a specific image
    function getTitleForImage(img) {
        const imageUrl = getFullSizeImageUrl(img.src);
        
        // Check if we have this image mapped
        if (IMAGE_MAP.has(imageUrl)) {
            return IMAGE_MAP.get(imageUrl);
        }
        
        // If not mapped, create a fallback title
        const uniqueId = Math.floor(Math.random() * 10000);
        const fallbackTitle = `MLTSHP Image ${uniqueId} ${new Date().toISOString().slice(0, 10)}`;
        return {
            title: fallbackTitle,
            titleElement: img.parentElement,
            source: 'dynamic-fallback'
        };
    }
    
    // Function to resize image
    async function resizeImage(imageUrl, maxWidth = 800, maxHeight = 800) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.crossOrigin = 'anonymous';
            
            img.onload = () => {
                // Calculate new dimensions while maintaining aspect ratio
                let width = img.width;
                let height = img.height;
                
                if (width > maxWidth) {
                    height = Math.round((height * maxWidth) / width);
                    width = maxWidth;
                }
                
                if (height > maxHeight) {
                    width = Math.round((width * maxHeight) / height);
                    height = maxHeight;
                }
                
                // Create canvas and resize image
                const canvas = document.createElement('canvas');
                canvas.width = width;
                canvas.height = height;
                
                const ctx = canvas.getContext('2d');
                ctx.drawImage(img, 0, 0, width, height);
                
                // Convert to base64 with reduced quality
                const base64 = canvas.toDataURL('image/jpeg', 0.8);
                resolve(base64);
            };
            
            img.onerror = (error) => {
                reject(new Error('Failed to load image: ' + error));
            };
            
            img.src = imageUrl;
        });
    }

    // Function to get image as base64
    async function getImageAsBase64(imageUrl) {
        try {
            log('Resizing image before conversion...');
            const resizedImage = await resizeImage(imageUrl);
            log('Image resized, base64 length:', resizedImage.length);
            return resizedImage;
        } catch (error) {
            console.error('Error converting image to base64:', error);
            throw error;
        }
    }

    // Function to call Image Caption Generator
    async function describeImage(imageUrl, button, retryCount = 0) {
        const originalText = button.textContent;
        button.textContent = 'Analyzing...';
        button.disabled = true;
        
        log('Calling Image Caption Generator for image:', imageUrl);
        
        try {
            // Convert image to base64
            const imageBase64 = await getImageAsBase64(imageUrl);
            log('Image converted to base64, length:', imageBase64.length);
            
            // Create a temporary form
            const form = document.createElement('form');
            form.method = 'POST';
            form.action = 'https://imagecaptiongenerator.com/';
            form.target = '_blank';
            form.style.display = 'none';
            
            // Create file input
            const fileInput = document.createElement('input');
            fileInput.type = 'file';
            fileInput.name = 'image';
            
            // Convert base64 to blob and create file
            const byteCharacters = atob(imageBase64.split(',')[1]);
            const byteArrays = [];
            
            for (let offset = 0; offset < byteCharacters.length; offset += 512) {
                const slice = byteCharacters.slice(offset, offset + 512);
                const byteNumbers = new Array(slice.length);
                
                for (let i = 0; i < slice.length; i++) {
                    byteNumbers[i] = byteCharacters.charCodeAt(i);
                }
                
                const byteArray = new Uint8Array(byteNumbers);
                byteArrays.push(byteArray);
            }
            
            const blob = new Blob(byteArrays, { type: 'image/jpeg' });
            const file = new File([blob], 'image.jpg', { type: 'image/jpeg' });
            
            // Create a DataTransfer object to set the file
            const dataTransfer = new DataTransfer();
            dataTransfer.items.add(file);
            fileInput.files = dataTransfer.files;
            
            form.appendChild(fileInput);
            document.body.appendChild(form);
            
            // Open the form in a new window
            const newWindow = window.open('', '_blank');
            form.submit();
            
            // Wait for the caption
            return new Promise((resolve, reject) => {
                let attempts = 0;
                const maxAttempts = 30; // 30 seconds
                
                const checkCaption = () => {
                    try {
                        if (!newWindow || newWindow.closed) {
                            throw new Error('Window was closed');
                        }
                        
                        // Try different selectors for the caption
                        const selectors = [
                            '.caption-result',
                            '.result-text',
                            '.generated-text',
                            '.result',
                            '.caption',
                            '#result',
                            '#caption',
                            'p.result',
                            'div.result',
                            'div.caption'
                        ];
                        
                        for (const selector of selectors) {
                            const element = newWindow.document.querySelector(selector);
                            if (element && element.textContent.trim()) {
                                const caption = element.textContent.trim();
                                log('Found caption:', caption);
                                newWindow.close();
                                document.body.removeChild(form);
                                resolve(caption);
                                return;
                            }
                        }
                        
                        attempts++;
                        if (attempts >= maxAttempts) {
                            throw new Error('Timeout waiting for caption');
                        }
                        
                        setTimeout(checkCaption, 1000);
                    } catch (error) {
                        log('Error checking caption:', error);
                        newWindow?.close();
                        document.body.removeChild(form);
                        reject(error);
                    }
                };
                
                // Start checking after a short delay to allow the page to load
                setTimeout(checkCaption, 2000);
            });
            
        } catch (error) {
            console.error('Error describing image:', error);
            log('Image Caption Generator error:', error);
            
            // Update debug panel with error
            if (DEBUG) {
                updateDebugPanel(`<strong>Image Caption Generator Error:</strong><br>
                    • Image: ${imageUrl.substring(imageUrl.lastIndexOf('/') + 1)}<br>
                    • Error: ${error.message}<br>
                    • Stack: ${error.stack}<br>`);
            }
            
            // If we haven't retried too many times
            if (retryCount < 3) {
                const delay = 5000; // 5 seconds
                log(`Retrying in ${delay/1000} seconds... (attempt ${retryCount + 1}/3)`);
                
                // Show countdown on button
                let remaining = delay/1000;
                const countdown = setInterval(() => {
                    remaining--;
                    button.textContent = `Retrying in ${remaining}...`;
                }, 1000);
                
                // Wait and retry
                await new Promise(resolve => setTimeout(resolve, delay));
                clearInterval(countdown);
                return describeImage(imageUrl, button, retryCount + 1);
            }
            
            return `Error analyzing image: ${error.message}`;
        } finally {
            button.textContent = originalText;
            button.disabled = false;
        }
    }

    // Function to copy text to clipboard
    function copyToClipboard(text) {
        const textarea = document.createElement('textarea');
        textarea.value = text;
        document.body.appendChild(textarea);
        textarea.select();
        document.execCommand('copy');
        document.body.removeChild(textarea);
    }

    // Function to add a download button to an image
    function addButtonToImage(img) {
        // Skip if already processed
        if (PROCESSED_IMAGES.has(img.src)) {
            log('Skipping already processed image:', img.src);
            return false;
        }
        
        // Mark as processed
        PROCESSED_IMAGES.add(img.src);
        
        // Get the image info
        const imageUrl = getFullSizeImageUrl(img.src);
        const imageInfo = getTitleForImage(img);
        
        log('Processing image:', {
            url: imageUrl,
            title: imageInfo.title,
            source: imageInfo.source
        });
        
        // For debugging
        img.classList.add('mltshp-processed-image');
        
        // Add image URL as data attribute
        img.setAttribute('data-mltshp-url', imageUrl);
        
        // Create container for the buttons
        const buttonContainer = document.createElement('div');
        buttonContainer.className = 'mltshp-image-container';
        buttonContainer.style.position = 'relative';
        buttonContainer.style.display = 'inline-block';
        
        if (DEBUG) {
            buttonContainer.setAttribute('data-debug', 'button-container');
            log('Created button container for image:', imageUrl);
        }
        
        // Create description container
        const descriptionContainer = document.createElement('div');
        descriptionContainer.className = 'mltshp-description-container';
        if (DEBUG) {
            descriptionContainer.setAttribute('data-debug', 'description-container');
            log('Created description container for image:', imageUrl);
        }
        
        const descriptionText = document.createElement('div');
        descriptionText.className = 'mltshp-description-text';
        if (DEBUG) {
            descriptionText.setAttribute('data-debug', 'description-text');
        }
        
        descriptionContainer.appendChild(descriptionText);
        
        // Create describe button
        const describeButton = document.createElement('button');
        describeButton.textContent = 'Describe';
        describeButton.className = 'mltshp-describe-btn';
        describeButton.style.display = SHOW_DESCRIPTION_BUTTON ? 'none' : 'none'; // Always hidden
        if (DEBUG) {
            describeButton.setAttribute('data-debug', 'describe-button');
            describeButton.setAttribute('data-image-url', imageUrl);
            log('Created describe button for image:', imageUrl);
        }
        
        describeButton.addEventListener('click', async function(e) {
            e.preventDefault();
            e.stopPropagation();
            
            const description = await describeImage(imageUrl, describeButton);
            descriptionText.textContent = description;
            
            // Automatically copy to clipboard
            copyToClipboard(description);
            
            // Show success message
            const originalText = descriptionText.textContent;
            descriptionText.textContent = 'Description copied to clipboard!';
            setTimeout(() => {
                descriptionText.textContent = originalText;
            }, 2000);
        });
        
        // Create download button
        const downloadButton = document.createElement('button');
        downloadButton.textContent = 'Download';
        downloadButton.className = 'mltshp-download-btn';
        
        // Add data attributes for debugging
        downloadButton.setAttribute('data-title', imageInfo.title);
        downloadButton.setAttribute('data-source', imageInfo.source || 'mapped');
        
        // Add click event listener
        downloadButton.addEventListener('click', function(e) {
            e.preventDefault();
            e.stopPropagation();
            downloadImage(imageUrl, imageInfo.title, downloadButton);
        });
        
        // Insert everything
        const parent = img.parentElement;
        if (parent) {
            parent.insertBefore(buttonContainer, img);
            buttonContainer.appendChild(img);
            buttonContainer.appendChild(describeButton);
            buttonContainer.appendChild(downloadButton);
            buttonContainer.appendChild(descriptionContainer);
            
            if (DEBUG) {
                log('Successfully added all elements for image:', {
                    url: imageUrl,
                    title: imageInfo.title,
                    elements: {
                        container: buttonContainer,
                        describeButton: describeButton,
                        downloadButton: downloadButton,
                        descriptionContainer: descriptionContainer
                    }
                });
                
                // Update debug panel with button creation info
                updateDebugPanel(`<strong>Added Buttons:</strong><br>
                    • Image: ${imageUrl.substring(imageUrl.lastIndexOf('/') + 1)}<br>
                    • Title: ${imageInfo.title}<br>
                    • Describe Button: ${describeButton.outerHTML}<br>
                    • Download Button: ${downloadButton.outerHTML}<br>`);
            }
            
            return true;
        }
        
        if (DEBUG) {
            log('Failed to add buttons - no parent element found for image:', imageUrl);
        }
        
        return false;
    }
    
    // Main function to scan the page and add buttons
    function scanPageAndAddButtons(forceRebuild = false) {
        log('Scanning page for images...');
        
        // Build the image-title map if needed or forced
        if (IMAGE_MAP.size === 0 || forceRebuild) {
            const mappedCount = buildImageTitleMap();
            log(`Built image map with ${mappedCount} entries`);
        }
        
        // Get all images on the page
        const allImages = document.querySelectorAll('img');
        log(`Found ${allImages.length} total images on page`);
        
        let addedButtonCount = 0;
        
        // Process each image
        allImages.forEach((img) => {
            // Skip unwanted images
            if (shouldSkipImage(img)) {
                return;
            }
            
            // Add button directly on the image
            if (addButtonToImage(img)) {
                addedButtonCount++;
            }
        });
        
        log(`Added ${addedButtonCount} image buttons`);
        
        // Update debug panel
        updateDebugPanel(`<strong>Added Buttons:</strong><br>• ${addedButtonCount} image buttons<br>`);
        
        return addedButtonCount;
    }

    // Function to get file extension from image URL
    function getFileExtension(url) {
        // Extract the base URL without query parameters
        const baseUrl = url.split('?')[0];
        
        const regex = /\.([a-zA-Z0-9]+)$/;
        const match = baseUrl.match(regex);
        
        // If no extension found in the URL, attempt to determine by checking last path segment
        if (!match) {
            // Extract ID from URL pattern like https://mltshp-cdn.com/r/1QZ0Z
            const idMatch = baseUrl.match(/\/([A-Za-z0-9]+)$/);
            if (idMatch) {
                log('No extension found, using jpg as default');
                return 'jpg';
            }
        }
        
        return match ? match[1].toLowerCase() : 'jpg';
    }

    // Function to download image with title as filename
    function downloadImage(imageUrl, title, button) {
        // Get file extension
        const extension = getFileExtension(imageUrl);
        
        // Create a sanitized filename
        const filename = sanitizeFilename(title) + '.' + extension;
        
        log('Downloading image with filename:', filename);
        
        // Save original button text
        const originalText = button.textContent;
        
        // Update button to show loading
        button.textContent = 'Downloading...';
        button.disabled = true;
        
        // Force binary download using the Fetch API
        fetchAndSaveImage(imageUrl, filename, button, originalText);
    }
    
    // Function to fetch and save image using fetch API
    function fetchAndSaveImage(imageUrl, filename, button, originalText) {
        log(`Fetching image from ${imageUrl} as ${filename}`);
        
        try {
            // Use GM_xmlhttpRequest if available for cross-origin support
            if (typeof GM_xmlhttpRequest !== 'undefined') {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: imageUrl,
                    responseType: 'blob',
                    onload: function(response) {
                        saveBlob(response.response, filename, button, originalText);
                    },
                    onerror: function(error) {
                        console.error('Error fetching image:', error);
                        handleDownloadError(button, originalText);
                    }
                });
            } else {
                // Fallback to fetch API
                fetch(imageUrl)
                    .then(response => {
                        if (!response.ok) {
                            throw new Error('Network response was not ok');
                        }
                        return response.blob();
                    })
                    .then(blob => {
                        saveBlob(blob, filename, button, originalText);
                    })
                    .catch(error => {
                        console.error('Error fetching image:', error);
                        handleDownloadError(button, originalText);
                    });
            }
        } catch (error) {
            console.error('Error in fetchAndSaveImage:', error);
            handleDownloadError(button, originalText);
        }
    }
    
    // Function to save a blob to disk
    function saveBlob(blob, filename, button, originalText) {
        try {
            // Create a Blob URL
            const url = URL.createObjectURL(blob);
            
            // Create an anchor element to trigger the download
            const a = document.createElement('a');
            a.href = url;
            a.download = filename;
            a.style.display = 'none';
            
            // Add to document, click to trigger download, then remove
            document.body.appendChild(a);
            a.click();
            
            // Clean up by removing the element and revoking the object URL
            setTimeout(() => {
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
                
                // Show success message
                button.textContent = 'Downloaded!';
                button.style.backgroundColor = '#2196F3';
                
                // Reset button after a delay
                setTimeout(() => {
                    button.textContent = originalText;
                    button.style.backgroundColor = '';
                    button.disabled = false;
                }, 2000);
            }, 100);
            
        } catch (error) {
            console.error('Error saving blob:', error);
            handleDownloadError(button, originalText);
        }
    }
    
    // Function to handle download errors
    function handleDownloadError(button, originalText) {
        button.textContent = 'Error!';
        button.style.backgroundColor = '#f44336';
        
        setTimeout(() => {
            button.textContent = originalText;
            button.style.backgroundColor = '';
            button.disabled = false;
        }, 2000);
    }
    
    // Wait for document to be ready
    function onReady(fn) {
        if (document.readyState !== 'loading') {
            fn();
        } else {
            document.addEventListener('DOMContentLoaded', fn);
        }
    }
    
    // Run initialization once the document is ready
    onReady(function() {
        log('MLTSHP Image Saver: Document ready, initializing...');
        
        // Build the initial image-title map
        setTimeout(() => {
            buildImageTitleMap();
            // Add buttons after mapping is complete
            setTimeout(() => scanPageAndAddButtons(), 500);
        }, 500);
        
        // Add floating button
        setTimeout(addFloatingButton, 1000);
        
        // Set up MutationObserver for dynamic content
        const observer = new MutationObserver(function(mutations) {
            // Only run if we detect new images
            let hasNewImages = false;
            
            mutations.forEach(mutation => {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === 1) { // Element node
                            if (node.tagName === 'IMG' || node.querySelector('img')) {
                                hasNewImages = true;
                            }
                        }
                    });
                }
            });
            
            if (hasNewImages) {
                setTimeout(() => scanPageAndAddButtons(), 500);
            }
        });
        
        observer.observe(document.body, { 
            childList: true, 
            subtree: true,
            attributes: true,
            attributeFilter: ['src']
        });
        
        // Also run periodically for any missed content
        setInterval(() => scanPageAndAddButtons(), 10000);
        
        // Show notification
        showNotification();
    });
    
    // Add a menu command to enable/disable script
    let isEnabled = true;
    
    function toggleScript() {
        isEnabled = !isEnabled;
        
        const buttons = document.querySelectorAll('.mltshp-download-btn');
        
        if (isEnabled) {
            buttons.forEach(btn => btn.style.display = 'inline-block');
            // Re-scan when enabling
            scanPageAndAddButtons();
            alert('MLTSHP Image Saver is now enabled');
        } else {
            buttons.forEach(btn => btn.style.display = 'none');
            alert('MLTSHP Image Saver is now disabled');
        }
    }
    
    // Register menu command if available
    if (typeof GM_registerMenuCommand !== 'undefined') {
        GM_registerMenuCommand('Toggle MLTSHP Image Saver', toggleScript);
    }
    
    // Show notification that script is running
    function showNotification() {
        const notification = document.createElement('div');
        notification.style.position = 'fixed';
        notification.style.bottom = '0px'; 
        notification.style.left = '10px';
        notification.style.padding = '8px 15px';
        notification.style.background = 'rgba(0, 0, 0, 0.7)';
        notification.style.color = 'white';
        notification.style.borderRadius = '4px';
        notification.style.fontSize = '14px';
        notification.style.zIndex = '9999';
        notification.style.opacity = '0';
        notification.style.transition = 'opacity 0.3s ease-in-out';
        notification.innerHTML = `
            <div>✅ MLTSHP Image Saver Active</div>
            <div style="font-size:12px;margin-top:3px;">Mapped ${IMAGE_MAP.size} images to unique titles</div>
        `;
        
        document.body.appendChild(notification);
        
        // Show notification
        setTimeout(() => {
            notification.style.opacity = '1';
            setTimeout(() => {
                notification.style.opacity = '0';
                setTimeout(() => {
                    notification.remove();
                }, 500);
            }, 5000);
        }, 1000);
    }
    
    log('MLTSHP Image Saver: Script initialized');
})();