YouTube Comment to Twitter

Adds a button to YouTube watch pages to easily tweet a comment with the video link.

// ==UserScript==
// @name         YouTube Comment to Twitter
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Adds a button to YouTube watch pages to easily tweet a comment with the video link.
// @author       torch
// @match        *://www.youtube.com/watch*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// @license MIT
// ==/UserScript==

(function() {
    'use_strict';

    // --- Configuration ---
    const BUTTON_TEXT = "🐦 Твитнуть комментарий";
    const POPUP_TITLE = "Написать твит о видео";
    const TWITTER_MAX_LENGTH = 280; // Standard Twitter limit
    const TWITTER_URL_LENGTH = 23; // Standard length consumed by a t.co URL

    // --- Styles ---
    GM_addStyle(`
        #yt-comment-to-twitter-btn {
            background-color: #1DA1F2;
            color: white;
            border: none;
            padding: 8px 12px;
            text-align: center;
            text-decoration: none;
            display: inline-block;
            font-size: 14px;
            margin: 4px 2px;
            cursor: pointer;
            border-radius: 20px;
            font-weight: bold;
        }
        #yt-comment-to-twitter-btn:hover {
            background-color: #0c85d0;
        }
        .twitter-popup-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            z-index: 9998;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .twitter-popup-content {
            background-color: #1e1e1e; /* Darker theme for YouTube dark mode */
            color: #e0e0e0;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 0 15px rgba(0, 0, 0, 0.3);
            width: 400px;
            max-width: 90%;
            z-index: 9999;
        }
        .twitter-popup-content h3 {
            margin-top: 0;
            color: #1DA1F2;
        }
        .twitter-popup-content textarea {
            width: calc(100% - 20px);
            height: 100px;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #555;
            border-radius: 4px;
            background-color: #2a2a2a;
            color: #e0e0e0;
            resize: vertical;
        }
        .twitter-popup-content .char-counter {
            text-align: right;
            font-size: 0.9em;
            color: #aaa;
            margin-bottom: 10px;
        }
        .twitter-popup-content button {
            padding: 10px 15px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
            margin-right: 10px;
        }
        .twitter-popup-content .tweet-btn {
            background-color: #1DA1F2;
            color: white;
        }
        .twitter-popup-content .tweet-btn:hover {
            background-color: #0c85d0;
        }
        .twitter-popup-content .cancel-btn {
            background-color: #555;
            color: white;
        }
        .twitter-popup-content .cancel-btn:hover {
            background-color: #777;
        }
    `);

    let popupOverlay = null;

    // getVideoTitle is not needed for the tweet content anymore, but kept in case it's useful for other features later.
    // function getVideoTitle() {
    //     const titleElement = document.querySelector('h1.ytd-video-primary-info-renderer yt-formatted-string, h1.title.ytd-video-primary-info-renderer');
    //     return titleElement ? titleElement.textContent.trim() : "YouTube Video";
    // }

    function getVideoUrl() {
        return window.location.href;
    }

    function createPopup() {
        if (popupOverlay) {
            popupOverlay.style.display = 'flex'; // Show if already created
            if (popupOverlay.querySelector('textarea')) {
                popupOverlay.querySelector('textarea').focus();
            }
            return;
        }

        popupOverlay = document.createElement('div');
        popupOverlay.className = 'twitter-popup-overlay';
        popupOverlay.onclick = function(e) {
            if (e.target === popupOverlay) {
                closePopup();
            }
        };

        const popupContent = document.createElement('div');
        popupContent.className = 'twitter-popup-content';

        const title = document.createElement('h3');
        title.textContent = POPUP_TITLE;

        const textarea = document.createElement('textarea');
        textarea.placeholder = "Ваш комментарий...";

        const charCounter = document.createElement('div');
        charCounter.className = 'char-counter';
        const updateCharCounter = () => {
            // The tweet will consist of the comment, a space, and the URL.
            // Twitter uses t.co to shorten URLs, which takes up a fixed number of characters.
            const lengthOfComment = textarea.value.length;
            const lengthOfSpaceAndUrl = (lengthOfComment > 0 ? 1 : 0) + TWITTER_URL_LENGTH; // Add space only if comment exists
            const remaining = TWITTER_MAX_LENGTH - lengthOfComment - lengthOfSpaceAndUrl;
            charCounter.textContent = `${remaining} символов осталось`;
            charCounter.style.color = remaining < 0 ? 'red' : '#aaa';
        };
        textarea.addEventListener('input', updateCharCounter);


        const tweetButton = document.createElement('button');
        tweetButton.textContent = "Твитнуть";
        tweetButton.className = 'tweet-btn';
        tweetButton.onclick = function() {
            const comment = textarea.value.trim();
            // Comment can be empty, in which case only the URL is tweeted via the 'url' parameter.
            // Twitter usually pre-fills the text field with the URL if the text parameter is empty.

            const videoUrl = getVideoUrl();

            // Construct tweet text: only the comment.
            // The videoUrl will be passed in the 'url' parameter of the Twitter intent.
            // Twitter will append the URL to the comment text.
            let tweetText = comment;

            const twitterIntentUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}&url=${encodeURIComponent(videoUrl)}`;

            window.open(twitterIntentUrl, '_blank');
            closePopup();
        };

        const cancelButton = document.createElement('button');
        cancelButton.textContent = "Отмена";
        cancelButton.className = 'cancel-btn';
        cancelButton.onclick = closePopup;

        popupContent.appendChild(title);
        popupContent.appendChild(textarea);
        popupContent.appendChild(charCounter);
        popupContent.appendChild(tweetButton);
        popupContent.appendChild(cancelButton);
        popupOverlay.appendChild(popupContent);
        document.body.appendChild(popupOverlay);

        // Initialize counter
        updateCharCounter();
        textarea.focus();
    }

    function closePopup() {
        if (popupOverlay) {
            // Clear textarea for next time
            const textarea = popupOverlay.querySelector('textarea');
            if (textarea) {
                textarea.value = '';
            }
            popupOverlay.style.display = 'none';
        }
    }

    function addButton() {
        // More robust selectors for YouTube's dynamic layout
        const commonActionSelectors = [
            '#actions-inner #menu', // Older layout under video
            '#menu-container.ytd-watch-metadata', // Older layout alternative
            'ytd-video-actions #actions', // Newer layout for like/dislike etc.
            '#actions.ytd-watch-flexy' // Common actions row
        ];
        const fallbackSelectors = [
            '#info-contents #top-row.ytd-watch-info-text',
            '#meta-contents #info-contents',
            '#meta-contents #info',
            '#owner #subscribe-button' // As a last resort, place it near subscribe
        ];

        let actionsContainer = null;
        for (const selector of commonActionSelectors) {
            actionsContainer = document.querySelector(selector);
            if (actionsContainer) break;
        }

        if (!actionsContainer) {
            for (const selector of fallbackSelectors) {
                actionsContainer = document.querySelector(selector);
                if (actionsContainer) break;
            }
        }

        if (actionsContainer) {
            if (actionsContainer.querySelector('#yt-comment-to-twitter-btn')) {
                return; // Button already exists
            }

            const twitterButton = document.createElement('button');
            twitterButton.id = 'yt-comment-to-twitter-btn';
            twitterButton.textContent = BUTTON_TEXT;
            twitterButton.onclick = createPopup;

            // Attempt to insert it in a reasonable place
            if (actionsContainer.id === 'actions' && actionsContainer.parentElement?.tagName === 'YTD-VIDEO-ACTIONS') {
                // Preferred: Add next to like/share buttons
                 actionsContainer.insertBefore(twitterButton, actionsContainer.children[Math.min(2, actionsContainer.children.length)]);
            } else if (actionsContainer.firstChild) {
                 actionsContainer.insertBefore(twitterButton, actionsContainer.firstChild.nextSibling);
            } else {
                actionsContainer.appendChild(twitterButton);
            }
            // console.log("YouTube Comment to Twitter button added to:", actionsContainer);
        } else {
            // console.warn("Could not find a suitable container for the Twitter button after multiple attempts.");
        }
    }

    // YouTube uses dynamic loading, so we need to observe DOM changes
    function observeDOM() {
        const targetNode = document.body;
        const config = { childList: true, subtree: true };
        let lastPathname = window.location.pathname;
        let debounceTimer;

        const handleMutation = () => {
            // Try to add the button if it's not there
            if (!document.querySelector('#yt-comment-to-twitter-btn')) {
                addButton();
            }
        };

        const callback = function(mutationsList, observer) {
            // Check if navigation has happened to a new watch page
            if (window.location.pathname !== lastPathname && window.location.pathname.includes("/watch")) {
                lastPathname = window.location.pathname;
                // Wait a bit for the new page to load elements
                clearTimeout(debounceTimer);
                debounceTimer = setTimeout(handleMutation, 1000);
                return;
            }

            // General check for dynamic content loading on the current page
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    // Check if a potential container is now available
                    if (document.querySelector('#actions-inner #menu, #menu-container.ytd-watch-metadata, ytd-video-actions #actions, #actions.ytd-watch-flexy') && !document.querySelector('#yt-comment-to-twitter-btn')) {
                        clearTimeout(debounceTimer);
                        debounceTimer = setTimeout(handleMutation, 300); // Debounce to avoid multiple rapid adds
                        break;
                    }
                }
            }
        };

        const observer = new MutationObserver(callback);
        observer.observe(targetNode, config);

        // Initial attempt in case the element is already there
        if (window.location.pathname.includes("/watch")) {
           setTimeout(addButton, 1000); // Initial delay for page load
        }
    }

    // Make sure the script runs after the page is mostly loaded
    if (document.readyState === "complete" || document.readyState === "interactive") {
        observeDOM();
    } else {
        window.addEventListener('DOMContentLoaded', observeDOM);
    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址