YouTube Enhancer (Full-Sized Image Preview)

Viewing original video thumbnails, avatars, and channel banners, with a thumbnail preview above the panel.

目前為 2024-11-03 提交的版本,檢視 最新版本

// ==UserScript==
// @name         YouTube Enhancer (Full-Sized Image Preview)
// @description  Viewing original video thumbnails, avatars, and channel banners, with a thumbnail preview above the panel.
// @icon         https://raw.githubusercontent.com/exyezed/youtube-enhancer/refs/heads/main/extras/youtube-enhancer.png
// @version      1.1
// @author       exyezed
// @namespace    https://github.com/exyezed/youtube-enhancer/
// @supportURL   https://github.com/exyezed/youtube-enhancer/issues
// @license      MIT
// @match        https://www.youtube.com/*
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
        .YouTubeEnhancerFullSized-button {
            position: absolute;
            bottom: 10px;
            left: 5px;
            background-color: rgba(0, 0, 0, 0.75);
            color: white;
            border: none;
            border-radius: 3px;
            padding: 3px;
            font-size: 18px;
            cursor: pointer;
            z-index: 2000;
            opacity: 0;
            transition: opacity 0.3s;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .YouTubeEnhancerFullSized-container {
            position: relative;
        }
        .YouTubeEnhancerFullSized-container:hover .YouTubeEnhancerFullSized-button {
            opacity: 1;
        }
        .YouTubeEnhancerFullSized-clickable-image {
            cursor: pointer;
        }
        #YouTubeEnhancerFullSized-custom-image {
            width: 100%;
            height: auto;
            margin-bottom: 20px;
            box-sizing: border-box;
            border-radius: 10px;
            cursor: pointer;
        }
    `);

    const defaultIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M18 20H4V6h9V4H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-9h-2zm-7.79-3.17l-1.96-2.36L5.5 18h11l-3.54-4.71zM20 4V1h-2v3h-3c.01.01 0 2 0 2h3v2.99c.01.01 2 0 2 0V6h3V4z"/></svg>`;
    
    const hoverIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M19 7v2.99s-1.99.01-2 0V7h-3s.01-1.99 0-2h3V2h2v3h3v2zm-3 4V8h-3V5H5c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-8zM5 19l3-4l2 3l3-4l4 5z"/></svg>`;

    let YouTubeEnhancerFullSizedCurrentVideoId = '';

    function YouTubeEnhancerFullSizedOpenImage(url) {
        window.open(url, '_blank');
    }

    function YouTubeEnhancerFullSizedAddButton(element, getFullSizeUrl) {
        if (!element.closest('.YouTubeEnhancerFullSized-container')) {
            const container = document.createElement('div');
            container.className = 'YouTubeEnhancerFullSized-container';
            element.parentNode.insertBefore(container, element);
            container.appendChild(element);

            const button = document.createElement('button');
            button.className = 'YouTubeEnhancerFullSized-button';
            button.innerHTML = defaultIcon;
            
            button.addEventListener('mouseenter', () => {
                button.innerHTML = hoverIcon;
            });
            
            button.addEventListener('mouseleave', () => {
                button.innerHTML = defaultIcon;
            });
            
            button.onclick = (e) => {
                e.stopPropagation();
                e.preventDefault();
                YouTubeEnhancerFullSizedOpenImage(getFullSizeUrl(element.src));
            };
            container.appendChild(button);
        }
    }

    function YouTubeEnhancerFullSizedProcessAvatars() {
        const avatars = document.querySelectorAll('yt-avatar-shape img, yt-img-shadow#avatar img');
        avatars.forEach(img => {
            if (!img.closest('.YouTubeEnhancerFullSized-container')) {
                const isWatchPage = window.location.pathname.startsWith('/watch');
                YouTubeEnhancerFullSizedAddButton(img, (src) => src.replace(/=s\d+-c-k-c0x00ffffff-no-rj.*/, '=s0'));
                
                if (isWatchPage) {
                    const button = img.closest('.YouTubeEnhancerFullSized-container').querySelector('.YouTubeEnhancerFullSized-button');
                    if (button) {
                        button.style.display = 'none';
                    }
                }
            }
        });
    }

    function YouTubeEnhancerFullSizedProcessChannelBanners() {
        const banners = document.querySelectorAll('yt-image-banner-view-model img');
        banners.forEach(img => {
            if (!img.closest('.YouTubeEnhancerFullSized-container')) {
                YouTubeEnhancerFullSizedAddButton(img, (src) => src.replace(/=w\d+-.*/, '=s0'));
            }
        });
    }

    function YouTubeEnhancerFullSizedProcessVideoThumbnails() {
        const thumbnails = document.querySelectorAll('ytd-thumbnail img, ytd-playlist-thumbnail img');
        thumbnails.forEach(img => {
            if (!img.closest('.YouTubeEnhancerFullSized-container')) {
                YouTubeEnhancerFullSizedAddButton(img, (src) => {
                    const videoId = src.match(/\/vi\/([^\/]+)/)[1];
                    return `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`;
                });
            }
        });
    }

    function YouTubeEnhancerFullSizedGetVideoId() {
        const urlParams = new URLSearchParams(window.location.search);
        return urlParams.get('v');
    }

    function YouTubeEnhancerFullSizedAddOrUpdateImage() {
        const newVideoId = YouTubeEnhancerFullSizedGetVideoId();
        if (newVideoId && newVideoId !== YouTubeEnhancerFullSizedCurrentVideoId) {
            YouTubeEnhancerFullSizedCurrentVideoId = newVideoId;
            const targetElement = document.querySelector('#secondary-inner #panels');
            if (targetElement) {
                let img = document.getElementById('YouTubeEnhancerFullSized-custom-image');
                if (!img) {
                    img = document.createElement('img');
                    img.id = 'YouTubeEnhancerFullSized-custom-image';
                    targetElement.parentNode.insertBefore(img, targetElement);
                    
                    img.addEventListener('click', () => {
                        const maxResUrl = `https://i.ytimg.com/vi/${YouTubeEnhancerFullSizedCurrentVideoId}/maxresdefault.jpg`;
                        YouTubeEnhancerFullSizedOpenImage(maxResUrl);
                    });
                }
                img.src = `https://i.ytimg.com/vi/${YouTubeEnhancerFullSizedCurrentVideoId}/mqdefault.jpg`;
            }
        }
    }

    function YouTubeEnhancerFullSizedObservePageChanges() {
        const observer = new MutationObserver((mutations) => {
            YouTubeEnhancerFullSizedProcessAvatars();
            YouTubeEnhancerFullSizedProcessChannelBanners();
            YouTubeEnhancerFullSizedProcessVideoThumbnails();
            YouTubeEnhancerFullSizedAddOrUpdateImage();
        });

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

    // Initial processing
    YouTubeEnhancerFullSizedProcessAvatars();
    YouTubeEnhancerFullSizedProcessChannelBanners();
    YouTubeEnhancerFullSizedProcessVideoThumbnails();
    YouTubeEnhancerFullSizedAddOrUpdateImage();

    // Observe for future changes
    YouTubeEnhancerFullSizedObservePageChanges();

    // Check when URL changes
    window.addEventListener('yt-navigate-finish', () => {
        YouTubeEnhancerFullSizedProcessAvatars();
        YouTubeEnhancerFullSizedProcessChannelBanners();
        YouTubeEnhancerFullSizedProcessVideoThumbnails();
        YouTubeEnhancerFullSizedAddOrUpdateImage();
    });
    console.log('YouTube Enhancer (Full-Sized Image Preview) is running');
})();