YouTube Screenshot Button

Adds a screenshot button to the YouTube player.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Вам потребуется установить расширение, например Tampermonkey или Violentmonkey, чтобы установить этот скрипт.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         YouTube Screenshot Button
// @namespace    http://tampermonkey.net/
// @version      2.0.0
// @author       Yukiteru
// @description  Adds a screenshot button to the YouTube player.
// @license      MIT
// @match        https://www.youtube.com/*
// @grant        GM_log
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    const LOG_PREFIX = '[YT Screenshot]';
    const log = (msg) => GM_log(`${LOG_PREFIX} ${msg}`);

    // 1 = Download Only, 2 = Copy to Clipboard, 3 = Both
    const CONFIG = {
        ACTION: GM_getValue('screenshot_action', 3) 
    };

    const SVG_PATH = "M19.5,7h-3.14l-1.39-2H9.03L7.64,7H4.5C3.67,7,3,7.67,3,8.5v11C3,20.33,3.67,21,4.5,21h15c0.83,0,1.5-0.67,1.5-1.5v-11 C21,7.67,20.33,7,19.5,7z M19.5,19.5h-15v-11h4.05l1.83-2h3.24l1.83,2h4.05V19.5z M12,10.25c-2.07,0-3.75,1.68-3.75,3.75S9.93,17.75,12,17.75s3.75-1.68,3.75-3.75S14.07,10.25,12,10.25z M12,16.25 c-1.24,0-2.25-1.01-2.25-2.25s1.01-2.25,2.25-2.25s2.25,1.01,2.25,2.25S13.24,16.25,12,16.25z";
    let currentSettingsBtn = null;
    let menuCmdId = null;

    function getTranslations() {
        const lang = (navigator.language || 'en').split('-')[0];
        const dict = { en: "Screenshot (s)", zh: "截图 (s)", ja: "スクリーンショット (s)", ko: "스크린샷 (s)" };
        return dict[lang] || dict.en;
    }

    // Dynamic Tampermonkey menu
    function setupMenu() {
        const actionNames = {
            1: 'Download Only',
            2: 'Copy to Clipboard',
            3: 'Download & Copy'
        };

        if (menuCmdId !== null) {
            GM_unregisterMenuCommand(menuCmdId);
        }

        menuCmdId = GM_registerMenuCommand(`Action: ${actionNames[CONFIG.ACTION]}`, () => {
            const promptText = "Select screenshot behavior:\n\n1 = Download Only\n2 = Copy to Clipboard\n3 = Download & Copy";
            const newAction = prompt(promptText, CONFIG.ACTION);
            
            if (newAction && ['1', '2', '3'].includes(newAction.trim())) {
                const parsedAction = parseInt(newAction.trim(), 10);
                GM_setValue('screenshot_action', parsedAction);
                CONFIG.ACTION = parsedAction;
                setupMenu(); // Refresh menu UI instantly
            } else if (newAction !== null) {
                alert("Invalid input. Please enter 1, 2, or 3.");
            }
        });
    }

    // Tooltip hijack to match YouTube native UI
    function showNativeTooltip(text, referenceBtn) {
        if (currentSettingsBtn) {
            currentSettingsBtn.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
        }

        const tooltipEl = document.querySelector('.ytp-tooltip');
        const textWrapper = document.querySelector('.ytp-tooltip-text-wrapper');
        const player = document.querySelector('.html5-video-player');

        if (tooltipEl && textWrapper && player) {
            const textSpan = textWrapper.querySelector('.ytp-tooltip-text');
            if (textSpan) {
                textSpan.textContent = text;
                const btnRect = referenceBtn.getBoundingClientRect();
                const playerRect = player.getBoundingClientRect();
                const tooltipRect = tooltipEl.getBoundingClientRect();
                
                const leftPos = (btnRect.left - playerRect.left) + (btnRect.width / 2) - (tooltipRect.width / 2);
                tooltipEl.style.left = `${leftPos}px`;
            }
        }
    }

    function hideNativeTooltip() {
        if (currentSettingsBtn) {
            currentSettingsBtn.dispatchEvent(new MouseEvent('mouseout', { bubbles: true }));
        }
    }

    function createButton() {
        const btn = document.createElement('button');
        btn.className = 'ytp-button';
        btn.id = 'yt-custom-screenshot-btn';
        btn.setAttribute('aria-label', getTranslations());

        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('viewBox', '2 3 20 20'); // Centered & padded
        svg.setAttribute('width', '24');
        svg.setAttribute('height', '24');
        svg.setAttribute('fill', 'white');

        const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        path.setAttribute('d', SVG_PATH);
        
        svg.appendChild(path);
        btn.appendChild(svg);

        btn.addEventListener('click', handleScreenshot);
        btn.addEventListener('mouseover', () => showNativeTooltip(getTranslations(), btn));
        btn.addEventListener('mouseout', hideNativeTooltip);

        return btn;
    }

    async function handleScreenshot() {
        const video = document.querySelector('video.video-stream');
        if (!video) return log('Error: Video element not found.');

        try {
            const canvas = document.createElement('canvas');
            canvas.width = video.videoWidth;
            canvas.height = video.videoHeight;
            canvas.getContext('2d', { alpha: false }).drawImage(video, 0, 0);

            const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
            if (!blob) throw new Error('Failed to generate blob.');

            if (CONFIG.ACTION === 2 || CONFIG.ACTION === 3) {
                const clipItem = new window.ClipboardItem({ [blob.type]: blob });
                await navigator.clipboard.write([clipItem]).catch(e => log(`Clipboard error: ${e}`));
            }

            if (CONFIG.ACTION === 1 || CONFIG.ACTION === 3) {
                downloadBlob(blob);
            }
        } catch (err) {
            log(`Capture failed: ${err}`);
        }
    }

    function downloadBlob(blob) {
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = generateFilename('png');
        a.click();
        URL.revokeObjectURL(url);
    }

    function generateFilename(ext) {
        try {
            const titleNode = document.querySelector('.ytd-watch-metadata #title') || document.querySelector('title');
            const safeTitle = (titleNode ? titleNode.textContent.trim() : 'yt_video').replace(/[\\/:*?"<>|]/g, '_'); 
            
            const video = document.querySelector('video.video-stream');
            const progress = video ? video.currentTime.toFixed(2) : '0.00';
            
            const d = new Date();
            const timeStr = `${d.getFullYear()}${(d.getMonth()+1).toString().padStart(2,'0')}${d.getDate().toString().padStart(2,'0')}_${d.getHours().toString().padStart(2,'0')}${d.getMinutes().toString().padStart(2,'0')}${d.getSeconds().toString().padStart(2,'0')}`;
            
            return `${safeTitle}_${progress}s_${timeStr}.${ext}`;
        } catch (e) {
            return `screenshot_${Date.now()}.${ext}`;
        }
    }

    function setupShortcut() {
        document.addEventListener('keydown', (e) => {
            const activeTag = document.activeElement ? document.activeElement.tagName.toLowerCase() : '';
            const isTyping = activeTag === 'input' || activeTag === 'textarea' || (document.activeElement && document.activeElement.isContentEditable);
            
            if (!isTyping && e.key.toLowerCase() === 's') {
                handleScreenshot();
            }
        });
    }

    function injectButton() {
        if (document.getElementById('yt-custom-screenshot-btn')) return;

        const settingsBtn = document.querySelector('.ytp-right-controls .ytp-settings-button') || 
                            document.querySelector('.ytp-right-controls-left > .ytp-settings-button');
        const video = document.querySelector('video.video-stream');

        if (settingsBtn && video) {
            currentSettingsBtn = settingsBtn;
            settingsBtn.insertAdjacentElement('beforebegin', createButton());
        }
    }

    // Bypass iframe clipboard restriction for embedded videos
    function fixIframeClipboard() {
        if (window.self !== window.top) return;
        document.querySelectorAll('iframe').forEach(iframe => {
            if (/youtube\.com\/embed/.test(iframe.src) && !iframe.allow.includes('clipboard-write')) {
                iframe.allow += ' clipboard-write;';
            }
        });
    }

    function init() {
        fixIframeClipboard();
        setupMenu();
        setupShortcut();

        const observer = new MutationObserver(injectButton);
        observer.observe(document.body, { childList: true, subtree: true });
    }

    if (location.host.includes('youtube.com')) {
        init();
    }

})();