Twitter Screenshot Button

Add a screenshot button to Twitter/X post menus

// ==UserScript==
// @name         Twitter Screenshot Button
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Add a screenshot button to Twitter/X post menus
// @author       You
// @match        https://twitter.com/*
// @match        https://x.com/*
// @grant        GM_addStyle
// @require      https://cdnjs.cloudflare.com/ajax/libs/dom-to-image/2.6.0/dom-to-image.min.js
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // Add only necessary button styles
    GM_addStyle(`
        .screenshot-button { 
            display: flex; 
            align-items: center;
            flex-direction: row;
            width: 100%;
            padding: 12px 16px;
            cursor: pointer;
            font-size: 15px;
            transition-property: background-color, box-shadow;
            transition-duration: 0.2s;
            outline-style: none;
            box-sizing: border-box;
            min-height: 0px;
            min-width: 0px;
            border: 0 solid black;
            background-color: rgba(0, 0, 0, 0);
            margin: 0px;
        }
        .screenshot-button:hover { 
            background-color: rgba(15, 20, 25, 0.1); 
        }
        .screenshot-icon { 
            margin-right: 0px; /* Keep margin 0, alignment handled by flex */
            width: 18.75px;
            height: 18.75px; 
            /* font-weight: bold; Removed as it doesn't apply well to SVG stroke */
            vertical-align: text-bottom; /* Align icon better with text */
        }
        .screenshot-notification { 
            position: fixed; 
            top: 20px; 
            left: 50%; 
            transform: translateX(-50%); 
            background-color: #1DA1F2; 
            color: white; 
            padding: 10px 20px; 
            border-radius: 20px; 
            z-index: 9999; 
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
            opacity: 1;
            transition: opacity 0.5s ease-out;
        }
        .screenshot-notification.fade-out {
            opacity: 0;
        }
    `);

    function findTweetMainContent(menuButton) {
        const article = menuButton.closest('article[role="article"]');
        if (!article) return null;
        return article;
    }

    function takeScreenshot(menuButton) {
        const notification = document.createElement('div');
        notification.className = 'screenshot-notification';
        notification.innerHTML = 'Taking screenshot...';
        document.body.appendChild(notification);

        try {
            const tweetContainer = findTweetMainContent(menuButton);
            if (!tweetContainer) {
                throw new Error('Could not find tweet content');
            }

            // Save original styles
            const originalStyles = {
                background: tweetContainer.style.background,
                backgroundColor: tweetContainer.style.backgroundColor,
                margin: tweetContainer.style.margin,
                border: tweetContainer.style.border,
                borderRadius: tweetContainer.style.borderRadius
            };

            // Optimize clarity settings
            const scale = window.devicePixelRatio * 2;

            // --- Start Background Color Detection ---
            let bgColor = 'rgb(255, 255, 255)'; // Default to white
            try {
                const bodyStyle = window.getComputedStyle(document.body);
                bgColor = bodyStyle.backgroundColor || bgColor;
                // If body has no color (transparent), try a main container
                if (!bgColor || bgColor === 'rgba(0, 0, 0, 0)' || bgColor === 'transparent') {
                    const mainContent = document.querySelector('main') || document.querySelector('#react-root'); // Common containers
                    if (mainContent) {
                        bgColor = window.getComputedStyle(mainContent).backgroundColor || 'rgb(255, 255, 255)';
                    }
                }
                 // Final fallback if detection fails
                if (bgColor === 'rgba(0, 0, 0, 0)' || bgColor === 'transparent') {
                   bgColor = 'rgb(255, 255, 255)';
                }
            } catch (bgError) {
                console.warn("Could not detect background color, defaulting to white.", bgError);
                bgColor = 'rgb(255, 255, 255)';
            }
            // --- End Background Color Detection ---

            const config = {
                height: tweetContainer.offsetHeight * scale,
                width: tweetContainer.offsetWidth * scale,
                style: {
                    transform: `scale(${scale})`,
                    transformOrigin: 'top left',
                    width: `${tweetContainer.offsetWidth}px`,
                    height: `${tweetContainer.offsetHeight}px`,
                    margin: 0, // Ensure no extra margin affects layout
                    border: 'none', // Remove borders for stitching
                    borderRadius: 0 // Remove border radius for stitching
                },
                quality: 1.0
            };
            // --- Add bgcolor to config ---
            config.bgcolor = bgColor;
            // --- End add bgcolor ---

            // Use dom-to-image for high-quality screenshot
            domtoimage.toBlob(tweetContainer, config)
                .then(function(blob) {
                    // Copy to clipboard
                    navigator.clipboard.write([
                        new ClipboardItem({
                            'image/png': blob
                        })
                    ]).then(() => {
                        notification.innerHTML = `
                            <div>Screenshot copied to clipboard!</div>
                            <button class="download-btn" style="
                                background: white;
                                color: #1DA1F2;
                                border: none;
                                padding: 5px 10px;
                                border-radius: 15px;
                                margin-top: 5px;
                                cursor: pointer;
                            ">Download</button>
                        `;
                        notification.style.backgroundColor = '#17BF63';

                        // Add download button functionality
                        const downloadBtn = notification.querySelector('.download-btn');
                        downloadBtn.addEventListener('click', () => {
                            const link = document.createElement('a');
                            link.download = `twitter-post-${Date.now()}.png`;
                            link.href = URL.createObjectURL(blob);
                            link.click();
                            URL.revokeObjectURL(link.href);
                            notification.remove();
                        });

                        // 设置3秒后渐隐消失
                        setTimeout(() => {
                            notification.classList.add('fade-out');
                            setTimeout(() => notification.remove(), 500);
                        }, 1500);
                    });
                })
                .catch(function(error) {
                    console.error('Screenshot failed:', error);
                    notification.textContent = 'Screenshot failed';
                    notification.style.backgroundColor = '#E0245E';
                    setTimeout(() => notification.remove(), 2000);
                });
        } catch (error) {
            console.error('Error during screenshot:', error);
            notification.textContent = 'Screenshot failed';
            notification.style.backgroundColor = '#E0245E';
            setTimeout(() => notification.remove(), 2000);
        }
    }

    function createScreenshotIcon() {
        const svgNS = "http://www.w3.org/2000/svg";
        const svg = document.createElementNS(svgNS, "svg");
        svg.setAttribute("xmlns", svgNS);
        svg.setAttribute("viewBox", "0 0 24 24");
        svg.setAttribute("width", "18.75");
        svg.setAttribute("height", "18.75");
        svg.setAttribute("fill", "none"); // Use fill=none for line icons
        svg.setAttribute("stroke", "currentColor"); // Inherit color via stroke
        svg.setAttribute("stroke-width", "2");
        svg.setAttribute("stroke-linecap", "round");
        svg.setAttribute("stroke-linejoin", "round");
        svg.classList.add("screenshot-icon");

        // Feather Icons: camera
        const path = document.createElementNS(svgNS, "path");
        path.setAttribute("d", "M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z");
        const circle = document.createElementNS(svgNS, "circle");
        circle.setAttribute("cx", "12");
        circle.setAttribute("cy", "13");
        circle.setAttribute("r", "4");

        svg.appendChild(path);
        svg.appendChild(circle);
        return svg;
    }

    function createThreadIcon() {
        const svgNS = "http://www.w3.org/2000/svg";
        const svg = document.createElementNS(svgNS, "svg");
        svg.setAttribute("xmlns", svgNS);
        svg.setAttribute("viewBox", "0 0 24 24");
        svg.setAttribute("width", "18.75");
        svg.setAttribute("height", "18.75");
        svg.setAttribute("fill", "none");
        svg.setAttribute("stroke", "currentColor");
        svg.setAttribute("stroke-width", "2");
        svg.setAttribute("stroke-linecap", "round");
        svg.setAttribute("stroke-linejoin", "round");
        svg.classList.add("screenshot-icon"); // Reuse same class for basic styling

        // Simple thread icon (line connecting dots)
        const path1 = document.createElementNS(svgNS, "path");
        path1.setAttribute("d", "M6 3v12");
        const circle1 = document.createElementNS(svgNS, "circle");
        circle1.setAttribute("cx", "6");
        circle1.setAttribute("cy", "3");
        circle1.setAttribute("r", "1");
        const circle2 = document.createElementNS(svgNS, "circle");
        circle2.setAttribute("cx", "6");
        circle2.setAttribute("cy", "9");
        circle2.setAttribute("r", "1");
         const circle3 = document.createElementNS(svgNS, "circle");
        circle3.setAttribute("cx", "6");
        circle3.setAttribute("cy", "15");
        circle3.setAttribute("r", "1");
        // Add a parallel element to suggest thread
        const path2 = document.createElementNS(svgNS, "path");
        path2.setAttribute("d", "M18 9v12");
        const circle4 = document.createElementNS(svgNS, "circle");
        circle4.setAttribute("cx", "18");
        circle4.setAttribute("cy", "9");
        circle4.setAttribute("r", "1");
        const circle5 = document.createElementNS(svgNS, "circle");
        circle5.setAttribute("cx", "18");
        circle5.setAttribute("cy", "15");
        circle5.setAttribute("r", "1");
         const circle6 = document.createElementNS(svgNS, "circle");
        circle6.setAttribute("cx", "18");
        circle6.setAttribute("cy", "21");
        circle6.setAttribute("r", "1");


        svg.appendChild(path1);
        svg.appendChild(circle1);
        svg.appendChild(circle2);
        svg.appendChild(circle3);
        svg.appendChild(path2);
        svg.appendChild(circle4);
        svg.appendChild(circle5);
        svg.appendChild(circle6);


        return svg;
    }

    async function captureThread(menuButton) {
        const notification = document.createElement('div');
        notification.className = 'screenshot-notification';
        notification.innerHTML = 'Capturing thread... Finding author and posts...';
        document.body.appendChild(notification);

        try {
            // 1. Find original tweet and author
            const originalArticle = findTweetMainContent(menuButton);
            if (!originalArticle) {
                throw new Error('Could not find the starting tweet.');
            }

            // Find author's handle (needs a robust selector, this is an example)
            // Twitter structure changes, this might need adjustment.
            const userElement = originalArticle.querySelector('[data-testid="User-Name"]'); // Try Test ID first
            let authorHandle = null;
            if (userElement) {
                 // Find the span containing the handle like '@handle'
                 const spans = userElement.querySelectorAll('span');
                 for (const span of spans) {
                     if (span.textContent.startsWith('@')) {
                         authorHandle = span.textContent;
                         break;
                     }
                 }
            }

            // Fallback if data-testid not found or handle not in spans
            if (!authorHandle) {
                const authorLink = originalArticle.querySelector('a[href*="/status/"][dir="ltr"]');
                 if (authorLink) {
                    const linkParts = authorLink.href.split('/');
                    // Usually the handle is the 3rd part like ['https:', '', 'twitter.com', 'handle', 'status', 'id']
                    if (linkParts.length > 3) {
                         authorHandle = '@' + linkParts[3];
                     }
                }
            }


            if (!authorHandle) {
                throw new Error('Could not reliably determine the author\'s handle.');
            }
            notification.innerHTML = `Capturing thread by ${authorHandle}... Expanding replies...`;
            console.log(`Author Handle: ${authorHandle}`);

            // 2. Find and click "Show more replies" repeatedly
            const conversationContainer = originalArticle.closest('div[data-testid="conversation"]'); // Find the container holding the thread
            let showMoreButton;
            const maxClicks = 15; // Limit clicks to prevent infinite loops
            let clicks = 0;
            const showMoreSelector = 'span.css-1jxf684.r-bcqeeo.r-1ttztb7.r-qvutc0.r-poiln3'; // User provided selector

            while (clicks < maxClicks) {
                 // Find the button within the conversation context if possible
                 showMoreButton = conversationContainer
                     ? conversationContainer.querySelector(showMoreSelector)
                     : document.querySelector(showMoreSelector); // Fallback to document search

                // Check if the button text actually indicates more replies
                if (showMoreButton && showMoreButton.textContent.includes('Show') && showMoreButton.closest('div[role="button"]')) { // Check text and if it's clickable
                    console.log(`Clicking "Show more" (${clicks + 1}/${maxClicks})`);
                     notification.innerHTML = `Capturing thread by ${authorHandle}... Expanding replies (${clicks + 1})...`;
                    showMoreButton.closest('div[role="button"]').click(); // Click the clickable parent
                    clicks++;
                    // Wait for content to load - adjust delay as needed
                    await new Promise(resolve => setTimeout(resolve, 1500)); // Wait 1.5 seconds
                } else {
                     console.log("No more 'Show more' buttons found or button text doesn't match.");
                    break; // Exit loop if no more buttons or limit reached
                }
            }
            if (clicks === maxClicks) {
                console.warn("Reached maximum 'Show more' clicks limit.");
            }

            notification.innerHTML = `Capturing thread by ${authorHandle}... Finding all posts...`;

            // 3. Filter replies by original author
            // Select all articles *after* the initial expansion
             const allArticles = Array.from(document.querySelectorAll('article[role="article"]'));
            const authorTweets = allArticles.filter(article => {
                // Re-check author handle for each potential tweet in the thread
                const userElement = article.querySelector('[data-testid="User-Name"]');
                let currentHandle = null;
                if (userElement) {
                     const spans = userElement.querySelectorAll('span');
                     for (const span of spans) {
                         if (span.textContent.startsWith('@')) {
                             currentHandle = span.textContent;
                             break;
                         }
                     }
                }
                 // Fallback check
                 if (!currentHandle) {
                     const authorLink = article.querySelector('a[href*="/status/"][dir="ltr"]');
                     if (authorLink) {
                        const linkParts = authorLink.href.split('/');
                        if (linkParts.length > 3) {
                            currentHandle = '@' + linkParts[3];
                        }
                     }
                 }
                return currentHandle === authorHandle;
            });


            if (authorTweets.length === 0) {
                 // If filtering removed everything, at least include the original tweet
                 authorTweets.push(originalArticle);
            }
            // Ensure tweets are in order (usually they are by DOM order, but sort just in case)
            // This relies on DOM order being correct. A more robust way might involve timestamps if available.
            authorTweets.sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);


            console.log(`Found ${authorTweets.length} tweets by ${authorHandle}`);
            notification.innerHTML = `Taking ${authorTweets.length} screenshots... (0%)`;

            // 4. Screenshot each tweet individually
            const blobs = [];
            const scale = window.devicePixelRatio * 1.5; // Slightly lower scale for potentially long images

            for (let i = 0; i < authorTweets.length; i++) {
                const tweet = authorTweets[i];
                 const percentage = Math.round(((i + 1) / authorTweets.length) * 100);
                 notification.innerHTML = `Taking ${authorTweets.length} screenshots... (${percentage}%)`;

                 // Ensure tweet is visible for screenshot (scrollIntoView might be needed sometimes)
                 // tweet.scrollIntoView({ block: 'nearest' });
                 // await new Promise(resolve => setTimeout(resolve, 100)); // Small delay after scroll

                try {
                    // ---> New: Check for and click internal "Show more" button within the tweet text
                    const internalShowMoreButton = tweet.querySelector('button[data-testid="tweet-text-show-more-link"]');
                    if (internalShowMoreButton) {
                        console.log(`Clicking internal "Show more" for tweet ${i + 1}`);
                        internalShowMoreButton.click();
                        // Wait a short moment for the text to expand
                        await new Promise(resolve => setTimeout(resolve, 500)); // 0.5 second delay
                    }
                    // <--- End new section

                     // --- Start Background Color Detection (for thread) ---
                     let threadBgColor = 'rgb(255, 255, 255)'; // Default to white
                     try {
                         const bodyStyle = window.getComputedStyle(document.body);
                         threadBgColor = bodyStyle.backgroundColor || threadBgColor;
                         if (!threadBgColor || threadBgColor === 'rgba(0, 0, 0, 0)' || threadBgColor === 'transparent') {
                             const mainContent = document.querySelector('main') || document.querySelector('#react-root');
                             if (mainContent) {
                                 threadBgColor = window.getComputedStyle(mainContent).backgroundColor || 'rgb(255, 255, 255)';
                             }
                         }
                         if (threadBgColor === 'rgba(0, 0, 0, 0)' || threadBgColor === 'transparent') {
                            threadBgColor = 'rgb(255, 255, 255)';
                         }
                     } catch (bgError) {
                         console.warn("Could not detect background color for thread tweet, defaulting to white.", bgError);
                         threadBgColor = 'rgb(255, 255, 255)';
                     }
                    // --- End Background Color Detection ---

                     const config = {
                         height: tweet.offsetHeight * scale,
                         width: tweet.offsetWidth * scale,
                         style: {
                             transform: `scale(${scale})`,
                             transformOrigin: 'top left',
                             width: `${tweet.offsetWidth}px`,
                             height: `${tweet.offsetHeight}px`,
                             margin: 0, // Ensure no extra margin affects layout
                             border: 'none', // Remove borders for stitching
                             borderRadius: 0 // Remove border radius for stitching
                         },
                         quality: 0.95 // Slightly lower quality for performance/size
                     };
                    // --- Add bgcolor to config (for thread) ---
                    config.bgcolor = threadBgColor;
                    // --- End add bgcolor ---

                    const blob = await domtoimage.toBlob(tweet, config);
                    blobs.push(blob);
                } catch (screenshotError) {
                    console.error(`Failed to screenshot tweet ${i + 1}:`, screenshotError);
                     // Optionally skip this tweet or stop the process
                     notification.innerHTML = `Error screenshotting tweet ${i + 1}. Skipping.`;
                     await new Promise(resolve => setTimeout(resolve, 1500));
                }
            }

            if (blobs.length === 0) {
                throw new Error("No screenshots were successfully taken.");
            }

            notification.innerHTML = `Combining ${blobs.length} screenshots...`;

            // 5. Combine images using Canvas
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');
            let totalHeight = 0;
            let maxWidth = 0;
            const images = [];

            // Convert blobs to Image objects to get dimensions
            for (const blob of blobs) {
                const img = new Image();
                 const url = URL.createObjectURL(blob);
                img.src = url;
                await new Promise(resolve => { img.onload = resolve; }); // Wait for image data to load
                images.push(img);
                totalHeight += img.height;
                maxWidth = Math.max(maxWidth, img.width);
                // URL.revokeObjectURL(url); // Revoke later after drawing
            }

            // Set canvas dimensions
            canvas.width = maxWidth;
            canvas.height = totalHeight;

            // Draw images onto canvas
            let currentY = 0;
            for (const img of images) {
                ctx.drawImage(img, 0, currentY);
                currentY += img.height;
                 URL.revokeObjectURL(img.src); // Revoke URL now
            }

            // 6. Get final blob from canvas
            canvas.toBlob(function(finalBlob) {
                // 7. Handle final blob (copy/download/notification)
                navigator.clipboard.write([
                    new ClipboardItem({ 'image/png': finalBlob })
                ]).then(() => {
                    notification.innerHTML = `
                        <div>Thread screenshot copied! (${images.length} posts)</div>
                        <button class="download-btn" style="background: white; color: #1DA1F2; border: none; padding: 5px 10px; border-radius: 15px; margin-top: 5px; cursor: pointer;">Download</button>
                    `;
                    notification.style.backgroundColor = '#17BF63';

                    const downloadBtn = notification.querySelector('.download-btn');
                    downloadBtn.addEventListener('click', () => {
                        const link = document.createElement('a');
                        link.download = `twitter-thread-${authorHandle.substring(1)}-${Date.now()}.png`;
                        link.href = URL.createObjectURL(finalBlob);
                        link.click();
                        URL.revokeObjectURL(link.href);
                        // Keep notification open after download click for a bit
                         setTimeout(() => {
                             notification.classList.add('fade-out');
                             setTimeout(() => notification.remove(), 500);
                         }, 1500);
                    });

                    // Auto fade out after longer time for thread capture
                    setTimeout(() => {
                         if (!notification.classList.contains('fade-out')) { // Avoid double fade if download clicked
                            notification.classList.add('fade-out');
                            setTimeout(() => notification.remove(), 500);
                         }
                    }, 4000); // Keep notification longer
                }).catch(err => {
                     console.error('Failed to copy final image:', err);
                     notification.textContent = 'Failed to copy thread screenshot.';
                     notification.style.backgroundColor = '#E0245E';
                     setTimeout(() => notification.remove(), 3000);
                });

            }, 'image/png', 0.9); // Specify type and quality

        } catch (error) {
            console.error('Capture Thread failed:', error);
            notification.textContent = `Capture Thread failed: ${error.message}`;
            notification.style.backgroundColor = '#E0245E';
            setTimeout(() => {
                notification.classList.add('fade-out');
                setTimeout(() => notification.remove(), 500);
            }, 3000);
        }
    }

    function addScreenshotButtonToMenu(menuButton) {
        const menu = document.querySelector('[role="menu"]');
        // Check if buttons already exist
        if (!menu || menu.querySelector('.screenshot-button') || menu.querySelector('.capture-thread-button')) return;

        // --- Screenshot Button ---
        const screenshotButton = document.createElement('div');
        screenshotButton.className = 'screenshot-button'; // Keep original class for styling
        screenshotButton.setAttribute('role', 'menuitem');
        screenshotButton.setAttribute('tabindex', '0');

        screenshotButton.appendChild(createScreenshotIcon());

        const textScreenshot = document.createElement('span');
        textScreenshot.textContent = 'Screenshot';
        textScreenshot.style.marginLeft = '12px';
        textScreenshot.style.fontSize = '15px';
        textScreenshot.style.fontWeight = 'bold';
        screenshotButton.appendChild(textScreenshot);

        screenshotButton.addEventListener('click', (event) => {
            event.stopPropagation(); // Prevent menu closing immediately if something goes wrong
            takeScreenshot(menuButton);
            // Attempt to close the menu after action
             setTimeout(() => {
                const closeButton = document.querySelector('[data-testid="Dropdown"] [aria-label="Close"]'); // More specific selector
                 if (closeButton) closeButton.click();
                 // Fallback for menu itself if close button not found reliably
                 else if (menu && menu.style.display !== 'none') {
                     // Heuristic: Clicking away might close it, or find a parent dismiss layer
                     // This part is tricky due to varying menu implementations
                 }
             }, 100); // Small delay
        });

        menu.insertBefore(screenshotButton, menu.firstChild); // Insert at the top


        // --- Capture Thread Button ---
        const captureThreadButton = document.createElement('div');
        // Use screenshot-button class for base styles, add specific class if needed
        captureThreadButton.className = 'screenshot-button capture-thread-button';
        captureThreadButton.setAttribute('role', 'menuitem');
        captureThreadButton.setAttribute('tabindex', '0');

        captureThreadButton.appendChild(createThreadIcon());

        const textThread = document.createElement('span');
        textThread.textContent = 'Capture Thread';
        textThread.style.marginLeft = '12px';
        textThread.style.fontSize = '15px';
        textThread.style.fontWeight = 'bold';
        captureThreadButton.appendChild(textThread);

        captureThreadButton.addEventListener('click', (event) => {
            event.stopPropagation();
            captureThread(menuButton);
             // Attempt to close the menu after action
             setTimeout(() => {
                const closeButton = document.querySelector('[data-testid="Dropdown"] [aria-label="Close"]');
                 if (closeButton) closeButton.click();
                 else if (menu && menu.style.display !== 'none') {
                    // Fallback...
                 }
            }, 100);
        });

        // Insert Capture Thread button below the Screenshot button
        if (screenshotButton.nextSibling) {
            menu.insertBefore(captureThreadButton, screenshotButton.nextSibling);
        } else {
            menu.appendChild(captureThreadButton);
        }
    }

    function addScreenshotButtons() {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.addedNodes.length) {
                    mutation.addedNodes.forEach((node) => {
                        // Check if the added node itself is a menu or contains one
                        if (node.nodeType === 1) { // Check if it's an element node
                            const menu = node.matches('[role="menu"]') ? node : node.querySelector('[role="menu"]');
                            if (menu) {
                                // Find the button that triggered this menu
                                const menuButton = document.querySelector('[aria-haspopup="menu"][aria-expanded="true"]');
                                // IMPORTANT CHECK: Ensure the menu was triggered by the "More" button (three dots)
                                // within an article, typically identified by data-testid="caret".
                                if (menuButton && menuButton.closest('article[role="article"]') && menuButton.getAttribute('data-testid') === 'caret') {
                                    console.log("Detected 'More' menu, adding buttons.");
                                    addScreenshotButtonToMenu(menuButton);
                                } else {
                                     // Optional: Log why buttons weren't added
                                     // console.log("Detected menu, but not triggered by the target 'More' button or not within an article.");
                                }
                            }
                        }
                    });
                }
            });
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    addScreenshotButtons();
})();