YouTube Comment to Twitter

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);
    }

})();