[YouTube] Thumbnail Shift Preview

Hover over a YouTube thumbnail and press Shift to preview the video page in the bottom-right corner.

// ==UserScript==
// @name         [YouTube] Thumbnail Shift Preview
// @name:ja      [YouTube] サムネイルシフトでプレビュー再生
// @namespace    http://tampermonkey.net/
// @version      2025-10-23.21
// @description  Hover over a YouTube thumbnail and press Shift to preview the video page in the bottom-right corner.
// @description:ja YouTubeのサムネイルにホバーしてShiftキーを押すと、右下に動画ページをプレビューします。
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @author       You (with contributions from Grok)
// @match        https://www.youtube.com/*
// @grant        none
// @license      MIT
// @run-at       document-end
// ==/UserScript==
(function () {
    'use strict';

    let currentHoveredLink = null;
    let shiftPressed = false;

    // 動画リンクを動的に監視
    function observeVideoLinks() {
        const observer = new MutationObserver(() => {
            const links = document.querySelectorAll('a#thumbnail, a[href*="/watch?v="], a[href*="youtu.be/"], a[href*="/shorts/"]');
            links.forEach((link) => {
                if (!link.dataset.listenerAdded) {
                    link.addEventListener('mouseenter', () => {
                        currentHoveredLink = link; // ホバー中のリンクを更新
                    });
                    link.addEventListener('mouseleave', () => {
                        if (currentHoveredLink === link) {
                            currentHoveredLink = null;
                        }
                    });
                    link.dataset.listenerAdded = true;
                }
            });
        });

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

    // ページ読み込み完了後に監視開始
    window.addEventListener('load', () => {
        observeVideoLinks();
    });

    // ShiftキーおよびEscキーのイベントリスナー
    document.addEventListener('keydown', (e) => {
        if (e.key === 'Shift') {
            shiftPressed = !shiftPressed; // Shiftキーでトグル
            if (!shiftPressed) {
                // Shiftがオフになったらプレビューを閉じる
                const existingWrapper = document.getElementById('shift-key-iframe-wrapper');
                if (existingWrapper) existingWrapper.remove();
            } else if (currentHoveredLink) {
                // Shiftがオンになり、ホバー中のリンクがあればプレビュー表示
                showPreview();
            }
        } else if (e.key === 'Escape') {
            // Escキーでプレビューを閉じる
            const existingWrapper = document.getElementById('shift-key-iframe-wrapper');
            if (existingWrapper) existingWrapper.remove();
            shiftPressed = false; // Escで閉じた場合、Shift状態もリセット
        }
    });

    // プレビュー表示関数
    function showPreview() {
        if (!shiftPressed || !currentHoveredLink) return;

        // リンク取得
        const href = currentHoveredLink.href;
        if (!href || (!href.includes('watch?v=') && !href.includes('youtu.be/') && !href.includes('/shorts/'))) {
            alert('⚠ 有効なYouTube動画リンクではありません: ' + href);
            console.log('リンク:', href);
            return;
        }

        // YouTube動画IDを抽出
        let videoId = '';
        if (href.includes('watch?v=')) {
            videoId = href.split('watch?v=')[1]?.split('&')[0];
        } else if (href.includes('youtu.be/')) {
            videoId = href.split('youtu.be/')[1]?.split('?')[0].split('/')[0];
        } else if (href.includes('/shorts/')) {
            videoId = href.split('/shorts/')[1]?.split('?')[0].split('/')[0];
        }

        if (!videoId) {
            alert('⚠ 動画IDの取得に失敗しました: ' + href);
            console.log('リンク:', href);
            return;
        }

        // 動画ページURLを構築
        const videoPageUrl = `https://www.youtube.com/watch?v=${videoId}`;

        // 既存のプレビューがあればiframeのsrcを更新
        const existingWrapper = document.getElementById('shift-key-iframe-wrapper');
        if (existingWrapper) {
            const iframe = existingWrapper.querySelector('iframe');
            if (iframe && iframe.src !== videoPageUrl) {
                iframe.src = videoPageUrl; // 新しい動画ページに更新
            }
            return;
        }

        // ラッパー生成
        const wrapper = document.createElement('div');
        wrapper.id = 'shift-key-iframe-wrapper';
        Object.assign(wrapper.style, {
            position: 'fixed',
            bottom: '10px', // 右下に表示
            right: '10px', // 右から10px
            width: '800px',
            height: '450px',
            zIndex: '99999',
            background: 'transparent',
            boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
            borderRadius: '8px',
            overflow: 'hidden',
            border: 'none',
            margin: '0',
            padding: '0'
        });

        // 閉じるボタン
        const closeButton = document.createElement('button');
        closeButton.textContent = '×';
        Object.assign(closeButton.style, {
            position: 'absolute',
            top: '5px',
            right: '5px',
            background: '#f44336',
            color: 'white',
            border: 'none',
            borderRadius: '50%',
            width: '30px',
            height: '30px',
            fontSize: '16px',
            cursor: 'pointer',
            zIndex: '1000000'
        });
        closeButton.addEventListener('click', () => {
            wrapper.remove();
            shiftPressed = false; // 閉じるボタンで閉じた場合、Shift状態もリセット
        });

        // iframe生成
        const iframe = document.createElement('iframe');
        Object.assign(iframe.style, {
            width: '1000px',
            height: '680px',
            border: 'none',
            display: 'block',
            transform: 'scale(0.8)',
            transformOrigin: 'top left',
            overflow: 'hidden',
            margin: '0',
            padding: '0',
            background: '#000'
        });
        iframe.setAttribute('scrolling', 'no');
        iframe.src = videoPageUrl; // ホバーしたサムネイルの動画ページを表示
        iframe.allow = 'autoplay; encrypted-media';
        iframe.allowFullscreen = true;
        iframe.referrerpolicy = 'strict-origin-when-cross-origin'; // YouTube要件対応

        wrapper.append(closeButton, iframe);
        document.body.appendChild(wrapper);
    }
})();