Greasy Fork镜像 支持简体中文。

YouTube增强 - 自动摘要与布局优化

为YouTube添加Gemini自动摘要功能,并优化缩略图布局

目前為 2025-05-10 提交的版本,檢視 最新版本

// ==UserScript==
// @name         YouTube增强 - 自动摘要与布局优化
// @namespace    http://tampermonkey.net/
// @version     0.9
// @description  为YouTube添加Gemini自动摘要功能,并优化缩略图布局
// @author       Combined script (original by hengyu and Claude)
// @match        *://www.youtube.com/*
// @match        *://gemini.google.com/*
// @exclude      https://accounts.youtube.com/*
// @exclude      https://studio.youtube.com/*
// @exclude      https://music.youtube.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_addStyle
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // === 配置参数 ===
    const DEBUG = false; // 设置为true启用详细日志
    const CHECK_INTERVAL_MS = 200;
    const YOUTUBE_ELEMENT_TIMEOUT_MS = 10000;
    const GEMINI_ELEMENT_TIMEOUT_MS = 15000;
    const GEMINI_PROMPT_EXPIRY_MS = 300000;

    // === 常量与ID ===
    const BUTTON_ID = 'gemini-summarize-btn';
    const THUMBNAIL_BUTTON_CLASS = 'gemini-thumbnail-btn';
    const YOUTUBE_NOTIFICATION_ID = 'gemini-yt-notification';
    const GEMINI_NOTIFICATION_ID = 'gemini-auto-notification';

    // === 调试日志 ===
    function debugLog(message) {
        if (DEBUG) {
            console.log(`[YT-Enhanced] ${message}`);
        }
    }

    // === CSS 样式 ===
    // 1. Gemini摘要功能的样式
    GM_addStyle(`
        .${THUMBNAIL_BUTTON_CLASS} {
            position: absolute;
            top: 5px;
            right: 5px;
            background-color: rgba(0, 0, 0, 0.7);
            color: white;
            border: none;
            border-radius: 4px;
            padding: 4px 8px;
            font-size: 12px;
            cursor: pointer;
            z-index: 100;
            display: flex;
            align-items: center;
            opacity: 0;
            transition: opacity 0.2s ease;
        }

        #dismissible:hover .${THUMBNAIL_BUTTON_CLASS},
        ytd-grid-video-renderer:hover .${THUMBNAIL_BUTTON_CLASS},
        ytd-video-renderer:hover .${THUMBNAIL_BUTTON_CLASS},
        ytd-rich-item-renderer:hover .${THUMBNAIL_BUTTON_CLASS},
        ytd-compact-video-renderer:hover .${THUMBNAIL_BUTTON_CLASS} {
            opacity: 1;
        }

        .${THUMBNAIL_BUTTON_CLASS}:hover {
            background-color: rgba(0, 0, 0, 0.9);
        }

        #gemini-popup {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
            z-index: 9999;
            width: 300px;
            display: none;
        }
        #gemini-popup .button {
            width: 100%;
            padding: 10px;
            margin: 5px 0;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }
        #gemini-popup .button:hover {
            background-color: #45a049;
        }
        #gemini-popup .status {
            margin-top: 10px;
            padding: 10px;
            border-radius: 4px;
            display: none;
        }
        #gemini-popup .success {
            background-color: #dff0d8;
            color: #3c763d;
        }
        #gemini-popup .error {
            background-color: #f2dede;
            color: #a94442;
        }
    `);

    // 2. 布局优化的样式
    GM_addStyle(`
        :root {
            --yt-layout-max-width: 1800px;
            --yt-layout-spacing: 16px;
            --yt-thumbnail-aspect-ratio: 16 / 9;
        }

        ytd-browse[page-subtype="home"] #primary,
        ytd-browse[page-subtype="subscriptions"] #primary {
            max-width: var(--yt-layout-max-width) !important;
            margin: 0 auto !important;
            padding: 0 24px !important;
        }

        ytd-rich-grid-renderer {
            padding: 0 !important;
            margin: 0 -8px !important;
            width: 100% !important;
            max-width: 100% !important;
        }

        ytd-rich-grid-row {
            margin: 0 !important;
            padding: 0 8px !important;
        }

        ytd-rich-item-renderer {
            margin: 0 0 20px !important;
            padding: 0 8px !important;
        }

        #thumbnail.ytd-thumbnail {
            aspect-ratio: var(--yt-thumbnail-aspect-ratio);
            overflow: hidden;
            border-radius: 12px;
        }

        #thumbnail.ytd-thumbnail img {
            object-fit: cover;
            width: 100%;
            height: 100%;
        }

        #meta.ytd-rich-grid-media {
            padding: 12px 4px 0 !important;
        }

        #video-title.ytd-rich-grid-media {
            line-height: 1.4;
            margin-bottom: 6px !important;
        }

        #metadata-line.ytd-video-meta-block {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
        }

        .yt-filler-item {
            background-color: rgba(240, 240, 240, 0.1);
            border-radius: 12px;
            overflow: hidden;
            margin: 0 0 20px !important;
            padding: 0 8px !important;
            box-sizing: border-box;
        }

        .yt-filler-thumbnail {
            background-color: rgba(200, 200, 200, 0.1);
            width: 100%;
            aspect-ratio: 16/9;
            border-radius: 12px;
            position: relative;
        }

        .yt-filler-meta {
            padding: 12px 0 0;
        }

        .yt-filler-title {
            height: 20px;
            margin-bottom: 8px;
            background-color: rgba(200, 200, 200, 0.1);
            border-radius: 4px;
            width: 90%;
        }

        .yt-filler-info {
            height: 16px;
            background-color: rgba(200, 200, 200, 0.1);
            border-radius: 4px;
            width: 60%;
            margin-top: 4px;
        }

        .yt-filler-item:after {
            content: "filler";
            position: absolute;
            opacity: 0.01;
            pointer-events: none;
        }

        @media (min-width: 1600px) {
            ytd-rich-item-renderer,
            .yt-filler-item {
                width: calc(20% - 16px) !important;
            }

            ytd-rich-grid-row #contents.ytd-rich-grid-row {
                max-height: none !important;
            }

            ytd-shelf-renderer[is-rich-shelf] #scroll-container.ytd-shelf-renderer {
                max-width: 100% !important;
            }
        }

        @media (min-width: 1000px) and (max-width: 1599px) {
            ytd-rich-item-renderer,
            .yt-filler-item {
                width: calc(25% - 16px) !important;
            }
        }

        @media (max-width: 999px) {
            ytd-rich-item-renderer,
            .yt-filler-item {
                width: calc(33.333% - 16px) !important;
            }
        }

        @media (max-width: 640px) {
            ytd-rich-item-renderer,
            .yt-filler-item {
                width: calc(50% - 16px) !important;
            }
        }

        ytd-shelf-renderer[is-rich-shelf] #scroll-container.ytd-shelf-renderer,
        ytd-horizontal-list-renderer[has-hover-animations]:not([hidden]),
        ytd-expanded-shelf-contents-renderer {
            padding: 0 !important;
        }

        ytd-grid-video-renderer {
            margin: 0 8px 20px !important;
        }
    `);

    // === 通用工具函数 ===
    function waitForElement(selectors, timeoutMs, parent = document) {
        const selectorArray = Array.isArray(selectors) ? selectors : [selectors];
        const combinedSelector = selectorArray.join(', ');

        return new Promise((resolve, reject) => {
            const initialElement = findVisibleElement(combinedSelector, parent);
            if (initialElement) {
                debugLog(`Element found immediately: ${combinedSelector}`);
                return resolve(initialElement);
            }

            let observer = null;
            let timeoutId = null;

            const cleanup = () => {
                if (observer) {
                    observer.disconnect();
                    observer = null;
                    debugLog(`MutationObserver disconnected for: ${combinedSelector}`);
                }
                if (timeoutId) {
                    clearTimeout(timeoutId);
                    timeoutId = null;
                }
            };

            const onTimeout = () => {
                cleanup();
                debugLog(`Element not found or not visible after ${timeoutMs}ms: ${combinedSelector}`);
                reject(new Error(`Element not found or not visible: ${combinedSelector}`));
            };

            const checkNode = (node) => {
                if (node && node.nodeType === Node.ELEMENT_NODE) {
                    if (node.matches(combinedSelector) && isElementVisible(node)) {
                         debugLog(`Element found via MutationObserver (direct match): ${combinedSelector}`);
                         cleanup();
                         resolve(node);
                         return true;
                    }
                    const foundDescendant = findVisibleElement(combinedSelector, node);
                    if (foundDescendant) {
                         debugLog(`Element found via MutationObserver (descendant): ${combinedSelector}`);
                         cleanup();
                         resolve(foundDescendant);
                         return true;
                    }
                }
                return false;
            };

            timeoutId = setTimeout(onTimeout, timeoutMs);

            observer = new MutationObserver((mutations) => {
                for (const mutation of mutations) {
                    if (mutation.type === 'childList') {
                        for (const node of mutation.addedNodes) {
                            if (checkNode(node)) return;
                        }
                    } else if (mutation.type === 'attributes') {
                        if (checkNode(mutation.target)) return;
                    }
                }
                const element = findVisibleElement(combinedSelector, parent);
                if (element) {
                    debugLog(`Element found via MutationObserver (fallback check): ${combinedSelector}`);
                    cleanup();
                    resolve(element);
                }
            });

            observer.observe(parent === document ? document.documentElement : parent, {
                childList: true,
                subtree: true,
                attributes: true,
                attributeFilter: ['style', 'class', 'disabled']
            });
            debugLog(`MutationObserver started for: ${combinedSelector}`);
        });
    }

    function findVisibleElement(selector, parent) {
        try {
            const elements = parent.querySelectorAll(selector);
            for (const el of elements) {
                if (isElementVisible(el)) {
                    if (selector.includes('button') && el.disabled) {
                       continue;
                    }
                    return el;
                }
            }
        } catch (e) {
            debugLog(`Error finding element with selector "${selector}": ${e}`);
        }
        return null;
    }

    function isElementVisible(el) {
        if (!el) return false;
        return (el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0);
    }

    function copyToClipboard(text) {
        navigator.clipboard.writeText(text).then(() => {
            debugLog("Text copied to clipboard via modern API.");
        }).catch(err => {
            debugLog(`Clipboard API failed: ${err}, using legacy method.`);
            legacyClipboardCopy(text);
        });
    }

    function legacyClipboardCopy(text) {
        const textarea = document.createElement('textarea');
        textarea.value = text;
        textarea.style.position = 'fixed';
        textarea.style.top = '-9999px';
        textarea.style.left = '-9999px';
        document.body.appendChild(textarea);
        textarea.select();
        try {
            const successful = document.execCommand('copy');
            debugLog(`Legacy copy attempt: ${successful ? 'Success' : 'Fail'}`);
        } catch (err) {
            debugLog('Failed to copy to clipboard using legacy execCommand: ' + err);
        }
        document.body.removeChild(textarea);
    }

    function showNotification(elementId, message, styles, duration = 15000) {
        let existingNotification = document.getElementById(elementId);
        if (existingNotification) {
            const existingTimeoutId = existingNotification.dataset.timeoutId;
            if (existingTimeoutId) {
                clearTimeout(parseInt(existingTimeoutId));
            }
            existingNotification.remove();
        }

        const notification = document.createElement('div');
        notification.id = elementId;
        notification.textContent = message;
        Object.assign(notification.style, styles);

        document.body.appendChild(notification);

        const closeButton = document.createElement('button');
        closeButton.textContent = '✕';
        Object.assign(closeButton.style, {
            position: 'absolute', top: '5px', right: '10px', background: 'transparent',
            border: 'none', color: 'inherit', fontSize: '16px', cursor: 'pointer', padding: '0', lineHeight: '1'
        });
        closeButton.onclick = () => notification.remove();
        notification.appendChild(closeButton);

        const timeoutId = setTimeout(() => notification.remove(), duration);
        notification.dataset.timeoutId = timeoutId.toString();
    }

    function debounce(func, wait) {
        let timeout;
        return function() {
            const context = this;
            const args = arguments;
            clearTimeout(timeout);
            timeout = setTimeout(() => {
                func.apply(context, args);
            }, wait);
        };
    }

    // === 脚本1: YouTube到Gemini功能 ===
    const YOUTUBE_NOTIFICATION_STYLE = {
        position: 'fixed', bottom: '20px', left: '50%', transform: 'translateX(-50%)',
        backgroundColor: 'rgba(0,0,0,0.85)', color: 'white', padding: '15px 35px 15px 20px',
        borderRadius: '8px', zIndex: '9999', maxWidth: 'calc(100% - 40px)', textAlign: 'left',
        boxSizing: 'border-box', whiteSpace: 'pre-wrap',
        boxShadow: '0 4px 12px rgba(0,0,0,0.3)'
    };

    const GEMINI_NOTIFICATION_STYLES = {
        info: { backgroundColor: '#e8f4fd', color: '#1967d2', border: '1px solid #a8c7fa' },
        warning: { backgroundColor: '#fef7e0', color: '#a56300', border: '1px solid #fdd663' },
        error: { backgroundColor: '#fce8e6', color: '#c5221f', border: '1px solid #f7a7a5' }
    };

    const BASE_GEMINI_NOTIFICATION_STYLE = {
        position: 'fixed', bottom: '20px', right: '20px', padding: '15px 35px 15px 20px',
        borderRadius: '8px', zIndex: '9999', maxWidth: '350px', textAlign: 'left',
        boxSizing: 'border-box', boxShadow: '0 4px 12px rgba(0,0,0,0.15)', whiteSpace: 'pre-wrap'
    };

    function isVideoPage() {
        return window.location.pathname === '/watch' && new URLSearchParams(window.location.search).has('v');
    }

    function getVideoInfoFromElement(element) {
        try {
            let videoId = '';
            const linkElement = element.querySelector('a[href*="/watch?v="]');
            if (linkElement) {
                const href = linkElement.getAttribute('href');
                const match = href.match(/\/watch\?v=([^&]+)/);
                if (match && match[1]) {
                    videoId = match[1];
                }
            }

            let videoTitle = '';
            const titleElement = element.querySelector('#video-title, .title, [title]');
            if (titleElement) {
                videoTitle = titleElement.textContent?.trim() || titleElement.getAttribute('title')?.trim() || '';
            }

            if (!videoId || !videoTitle) {
                return null;
            }

            return {
                id: videoId,
                title: videoTitle,
                url: `https://www.youtube.com/watch?v=${videoId}`
            };
        } catch (error) {
            console.error('获取视频信息时出错:', error);
            return null;
        }
    }

    function handleThumbnailButtonClick(event, videoInfo) {
        event.preventDefault();
        event.stopPropagation();

        try {
            if (!videoInfo || !videoInfo.url || !videoInfo.title) {
                throw new Error('视频信息不完整');
            }

            const prompt = `请分析这个YouTube视频: ${videoInfo.url}\n\n提供一个全面的摘要,包括主要观点、关键见解和视频中讨论的重要细节,以结构化的方式分解内容,并包括任何重要的结论或要点。`;
            debugLog(`从缩略图生成提示词: ${videoInfo.title}`);

            GM_setValue('geminiPrompt', prompt);
            GM_setValue('videoTitle', videoInfo.title);
            GM_setValue('timestamp', Date.now());

            window.open('https://gemini.google.com/', '_blank');
            debugLog("从缩略图打开Gemini标签页。");

            const notificationMessage = `
已跳转到 Gemini!
系统将尝试自动输入提示词并发送请求。

视频: "${videoInfo.title}"

(如果自动操作失败,提示词已复制到剪贴板,请手动粘贴)
            `.trim();
            showNotification(YOUTUBE_NOTIFICATION_ID, notificationMessage, YOUTUBE_NOTIFICATION_STYLE, 10000);

            copyToClipboard(prompt);

        } catch (error) {
            console.error("[YT-Enhanced] 处理缩略图按钮点击时出错:", error);
            showNotification(YOUTUBE_NOTIFICATION_ID, `创建摘要时出错: ${error.message}`, { ...YOUTUBE_NOTIFICATION_STYLE, backgroundColor: '#d93025', color: 'white' }, 10000);
        }
    }

    function addThumbnailButtons() {
        const videoElementSelectors = [
            'ytd-rich-item-renderer',
            'ytd-grid-video-renderer',
            'ytd-video-renderer',
            'ytd-compact-video-renderer',
            'ytd-playlist-video-renderer'
        ];

        const videoElements = document.querySelectorAll(videoElementSelectors.join(','));

        videoElements.forEach(element => {
            if (element.querySelector(`.${THUMBNAIL_BUTTON_CLASS}`)) {
                return;
            }

            const thumbnailContainer = element.querySelector('#thumbnail, .thumbnail, a[href*="/watch"]');
            if (!thumbnailContainer) {
                return;
            }

            const videoInfo = getVideoInfoFromElement(element);
            if (!videoInfo) {
                return;
            }

            const button = document.createElement('button');
            button.className = THUMBNAIL_BUTTON_CLASS;
            button.textContent = '📝 总结';
            button.title = '使用Gemini总结此视频';

            button.addEventListener('click', (e) => handleThumbnailButtonClick(e, videoInfo));

            thumbnailContainer.style.position = 'relative';
            thumbnailContainer.appendChild(button);
        });
    }

    function setupThumbnailButtonObserver() {
        addThumbnailButtons();

        const observer = new MutationObserver((mutations) => {
            let shouldAddButtons = false;

            for (const mutation of mutations) {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            if (node.tagName && (
                                node.tagName.toLowerCase().includes('ytd-') ||
                                node.querySelector('ytd-rich-item-renderer, ytd-grid-video-renderer, ytd-video-renderer')
                            )) {
                                shouldAddButtons = true;
                                break;
                            }
                        }
                    }
                }
                if (shouldAddButtons) break;
            }

            if (shouldAddButtons) {
                clearTimeout(window.thumbnailButtonTimeout);
                window.thumbnailButtonTimeout = setTimeout(addThumbnailButtons, 200);
            }
        });

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

        window.addEventListener('yt-navigate-finish', () => {
            debugLog("检测到页面导航,添加缩略图按钮");
            setTimeout(addThumbnailButtons, 300);
        });
    }

    async function addSummarizeButton() {
        if (!isVideoPage()) {
            debugLog("Not a video page, skipping button add.");
            removeSummarizeButtonIfExists();
            return;
        }

        if (document.getElementById(BUTTON_ID)) {
            debugLog("Summarize button already exists.");
            return;
        }

        debugLog("Video page detected. Attempting to add summarize button...");

        const containerSelectors = [
            '#top-row.ytd-watch-metadata > #subscribe-button',
            '#meta-contents #subscribe-button',
            '#owner #subscribe-button',
            '#meta-contents #top-row',
            '#above-the-fold #title',
            'ytd-watch-metadata #actions',
            '#masthead #end'
        ];

        try {
            const anchorElement = await waitForElement(containerSelectors, YOUTUBE_ELEMENT_TIMEOUT_MS);
            debugLog(`Found anchor element using selector matching: ${anchorElement.tagName}[id="${anchorElement.id}"][class="${anchorElement.className}"]`);

            if (document.getElementById(BUTTON_ID)) {
                debugLog("Button was added concurrently, skipping.");
                return;
            }

            const button = document.createElement('button');
            button.id = BUTTON_ID;
            button.textContent = '📝 Gemini摘要';

            Object.assign(button.style, {
                backgroundColor: '#1a73e8',
                color: 'white', border: 'none', borderRadius: '18px',
                padding: '0 16px', margin: '0 8px', cursor: 'pointer', fontWeight: '500',
                height: '36px',
                display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                fontSize: '14px',
                zIndex: '100',
                whiteSpace: 'nowrap',
                transition: 'background-color 0.3s ease'
            });
            button.onmouseover = () => button.style.backgroundColor = '#185abc';
            button.onmouseout = () => button.style.backgroundColor = '#1a73e8';

            button.addEventListener('click', handleSummarizeClick);

            if (anchorElement.id?.includes('subscribe-button') || anchorElement.tagName === 'BUTTON') {
                anchorElement.parentNode.insertBefore(button, anchorElement);
                debugLog(`Button inserted before anchor: ${anchorElement.id || anchorElement.tagName}`);
            } else if (anchorElement.id === 'actions' || anchorElement.id === 'end' || anchorElement.id === 'top-row') {
                anchorElement.insertBefore(button, anchorElement.firstChild);
                debugLog(`Button inserted as first child of container: ${anchorElement.id || anchorElement.tagName}`);
            } else {
                anchorElement.appendChild(button);
                debugLog(`Button appended to container: ${anchorElement.id || anchorElement.tagName}`);
            }

            debugLog("Summarize button successfully added!");

        } catch (error) {
            console.error('[YT-Enhanced] Failed to add summarize button:', error);
            removeSummarizeButtonIfExists();
        }
    }

    function handleSummarizeClick() {
        try {
            const youtubeUrl = window.location.href;
            const titleElement = document.querySelector('h1.ytd-watch-metadata, #video-title, #title h1');
            const videoTitle = titleElement?.textContent?.trim() || document.title.replace(/ - YouTube$/, '').trim() || 'Unknown Video';

            const prompt = `请分析这个YouTube视频: ${youtubeUrl}\n\n提供一个全面的摘要,包括主要观点、关键见解和视频中讨论的重要细节,以结构化的方式分解内容,并包括任何重要的结论或要点。`;
            debugLog(`Generated prompt for: ${videoTitle}`);

            GM_setValue('geminiPrompt', prompt);
            GM_setValue('videoTitle', videoTitle);
            GM_setValue('timestamp', Date.now());

            window.open('https://gemini.google.com/', '_blank');
            debugLog("Opened Gemini tab.");

            const notificationMessage = `
已跳转到 Gemini!
系统将尝试自动输入提示词并发送请求。

视频: "${videoTitle}"

(如果自动操作失败,提示词已复制到剪贴板,请手动粘贴)
            `.trim();
            showNotification(YOUTUBE_NOTIFICATION_ID, notificationMessage, YOUTUBE_NOTIFICATION_STYLE, 10000);

            copyToClipboard(prompt);

        } catch (error) {
            console.error("[YT-Enhanced] Error during summarize button click:", error);
            showNotification(YOUTUBE_NOTIFICATION_ID, `创建摘要时出错: ${error.message}`, { ...YOUTUBE_NOTIFICATION_STYLE, backgroundColor: '#d93025', color: 'white' }, 10000);
        }
    }

    function removeSummarizeButtonIfExists() {
        const button = document.getElementById(BUTTON_ID);
        if (button) {
            button.remove();
            debugLog("Removed existing summarize button.");
        }
    }

    function showGeminiNotification(message, type = "info") {
        const style = { ...BASE_GEMINI_NOTIFICATION_STYLE, ...(GEMINI_NOTIFICATION_STYLES[type] || GEMINI_NOTIFICATION_STYLES.info) };
        showNotification(GEMINI_NOTIFICATION_ID, message, style, 12000);
    }

    async function handleGeminiPage() {
        debugLog("Gemini page detected. Checking for pending prompt...");

        const prompt = GM_getValue('geminiPrompt', '');
        const timestamp = GM_getValue('timestamp', 0);
        const videoTitle = GM_getValue('videoTitle', 'N/A');
         // Clean up expired/invalid data immediately
        if (!prompt || Date.now() - timestamp > GEMINI_PROMPT_EXPIRY_MS) {
            debugLog("No valid prompt found or prompt expired.");
            GM_deleteValue('geminiPrompt');
            GM_deleteValue('timestamp');
            GM_deleteValue('videoTitle');
            return;
        }

        debugLog("Valid prompt found. Waiting for Gemini input area...");
        showGeminiNotification(`检测到来自 YouTube 的请求...\n视频: "${videoTitle}"`, "info");

        // Define selectors for input area and send button
        const textareaSelectors = [
            // More specific selectors first
            'div.input-area > div.input-box > div[contenteditable="true"]', // Common structure
            'div[role="textbox"][contenteditable="true"]',
            'textarea[aria-label*="Prompt"]', // Less common but possible
            // Broader fallbacks
            'div[contenteditable="true"]',
            'textarea'
        ];
        const sendButtonSelectors = [
            // More specific selectors first
            'button[aria-label*="Send message"], button[aria-label*="发送消息"]', // Common aria-labels
            'button:has(span[class*="send-icon"])', // Structure based
            'button.send-button', // Potential class
            // Fallbacks (less reliable, might match other buttons)
            'button:has(mat-icon[data-mat-icon-name="send"])', // Material icon (keep as fallback)
            'button[aria-label="Run"], button[aria-label="Submit"]'
        ];

        try {
            // Wait for the input area
            const textarea = await waitForElement(textareaSelectors, GEMINI_ELEMENT_TIMEOUT_MS);
            debugLog("Found input area. Inserting prompt.");

            // --- Input Prompt ---
            textarea.focus();
            let inputSuccess = false;
            if (textarea.isContentEditable) {
                textarea.textContent = prompt; // Use textContent for contenteditable
                inputSuccess = true;
            } else if (textarea.tagName === 'TEXTAREA') {
                textarea.value = prompt;
                inputSuccess = true;
            }

            if (!inputSuccess) {
                throw new Error("Could not determine how to input text into the found element.");
            }

            // Trigger input event to ensure Gemini UI updates (e.g., enables send button)
            textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
            textarea.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); // Also trigger change
            debugLog("Prompt inserted and events dispatched.");

            // Short delay to allow UI to potentially update (e.g., enabling send button)
            await new Promise(resolve => setTimeout(resolve, 150)); // Slightly longer? 150ms

            // --- Find and Click Send Button ---
            debugLog("Waiting for send button to be enabled...");
            const sendButton = await waitForElement(sendButtonSelectors, GEMINI_ELEMENT_TIMEOUT_MS);

            // Check if button is truly clickable (not disabled)
            if (sendButton.disabled || sendButton.getAttribute('aria-disabled') === 'true') {
                debugLog("Send button found but is disabled. Waiting a bit longer...");
                await new Promise(resolve => setTimeout(resolve, 500)); // Wait half a second more
                if (sendButton.disabled || sendButton.getAttribute('aria-disabled') === 'true') {
                    throw new Error("Send button remained disabled.");
                }
                debugLog("Send button became enabled after waiting.");
            }

            debugLog("Clicking send button...");
            sendButton.click();

            // --- Success ---
            debugLog("Successfully sent prompt to Gemini.");
            const successMessage = `
已自动发送视频摘要请求!
正在为视频分析做准备:
"${videoTitle}"

请稍候...
            `.trim();
            showGeminiNotification(successMessage, "info");

            // Clean up stored data after successful submission
            GM_deleteValue('geminiPrompt');
            GM_deleteValue('timestamp');
            GM_deleteValue('videoTitle');

        } catch (error) {
            console.error('[YT-Enhanced] Error handling Gemini page:', error);
            showGeminiNotification(`自动操作失败: ${error.message}\n\n提示词已复制到剪贴板,请手动粘贴并发送。`, "error");
            copyToClipboard(prompt); // Ensure clipboard has the prompt on error
            // Optionally clear GM values even on error to prevent retries on refresh
            GM_deleteValue('geminiPrompt');
            GM_deleteValue('timestamp');
            GM_deleteValue('videoTitle');
        }
    }

    // === 脚本2: YouTube布局优化功能 ===
    function fillEmptySpaces() {
        // 主要处理以下类型的空缺:
        // 1. 被移除的广告区块
        // 2. 行末未填满的空间

        // 在不同页面类型中使用不同的策略
        const isHomePage = window.location.pathname === "/" || window.location.pathname === "/feed/subscriptions";

        if (isHomePage) {
            fillHomePageGaps();
        }

        // 每次内容变化时都重新检查
        observeContentChanges();
    }

    function fillHomePageGaps() {
        setTimeout(() => {
            const gridRows = document.querySelectorAll('ytd-rich-grid-row');

            // 检查每一行的视频数量
            gridRows.forEach(row => {
                const container = row.querySelector('#contents');
                if (!container) return;

                const items = container.querySelectorAll('ytd-rich-item-renderer');

                // 检测间隙,这里通过元素可见性和位置来发现广告留下的空缺
                let columnsPerRow = 5; // 默认大屏为5列

                // 根据屏幕宽度确定应该有几列
                if (window.innerWidth < 640) {
                    columnsPerRow = 2;
                } else if (window.innerWidth < 1000) {
                    columnsPerRow = 3;
                } else if (window.innerWidth < 1600) {
                    columnsPerRow = 4;
                }

                // 检查行中是否有间隙或行末不满
                const visibleItems = Array.from(items).filter(item =>
                    window.getComputedStyle(item).display !== 'none' &&
                    item.offsetParent !== null
                );

                // 添加填充元素直到达到应有的列数
                const missingCount = columnsPerRow - visibleItems.length;

                if (missingCount > 0) {
                    for (let i = 0; i < missingCount; i++) {
                        const fillerItem = createFillerItem();
                        container.appendChild(fillerItem);
                    }
                }
            });

            // 如果页面中有广告标记的元素,也进行替换
            replaceAdElements();

        }, 1000); // 给页面加载一些时间
    }

    function createFillerItem() {
        const fillerItem = document.createElement('div');
        fillerItem.className = 'yt-filler-item';
        fillerItem.dataset.fillerItem = 'true'; // 添加数据属性以便识别

        // 缩略图区域
        const thumbnail = document.createElement('div');
        thumbnail.className = 'yt-filler-thumbnail';

        // 元数据区域
        const meta = document.createElement('div');
        meta.className = 'yt-filler-meta';

        // 标题占位
        const title = document.createElement('div');
        title.className = 'yt-filler-title';

        // 频道信息占位
        const channelInfo = document.createElement('div');
        channelInfo.className = 'yt-filler-info';

        // 观看数占位
        const viewInfo = document.createElement('div');
        viewInfo.className = 'yt-filler-info';
        viewInfo.style.width = '40%';

        // 组装元素
        meta.appendChild(title);
        meta.appendChild(channelInfo);
        meta.appendChild(viewInfo);

        fillerItem.appendChild(thumbnail);
        fillerItem.appendChild(meta);

        return fillerItem;
    }

    function replaceAdElements() {
        // 查找常见的广告容器选择器
        const adSelectors = [
            'ytd-ad-slot-renderer',
            'ytd-in-feed-ad-layout-renderer',
            'ytd-promoted-video-renderer',
            'ytd-display-ad-renderer',
            'ytd-statement-banner-renderer',
            'ytd-ad-element',
            'ytd-ad-break-item-renderer',
            'ytd-banner-promo-renderer',
            '[id^="ad-"]'
        ];

        adSelectors.forEach(selector => {
            const adElements = document.querySelectorAll(selector);
            adElements.forEach(adEl => {
                if (adEl && !adEl.classList.contains('yt-ad-replaced')) {
                    const fillerItem = createFillerItem();
                    adEl.parentNode.insertBefore(fillerItem, adEl);
                    adEl.classList.add('yt-ad-replaced');
                    adEl.style.display = 'none';
                }
            });
        });
    }

    function observeContentChanges() {
        // 观察DOM变化以检测新的广告元素或内容加载
        const observer = new MutationObserver(mutations => {
            let shouldCheckForGaps = false;

            mutations.forEach(mutation => {
                // 如果添加了新节点或有元素可见性改变,检查是否需要填充空缺
                if (mutation.addedNodes.length > 0 ||
                    (mutation.attributeName === 'style' || mutation.attributeName === 'class')) {
                    shouldCheckForGaps = true;
                }
            });

            if (shouldCheckForGaps) {
                fillHomePageGaps();
            }
        });

        // 监听整个文档的变化
        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['style', 'class']
        });
    }

    // === 主执行逻辑 ===
    debugLog("脚本开始执行...");

    if (window.location.hostname.includes('www.youtube.com')) {
        debugLog("YouTube域名检测到。");

        // 初始化缩略图按钮功能
        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            setupThumbnailButtonObserver();
        } else {
            window.addEventListener('DOMContentLoaded', setupThumbnailButtonObserver, { once: true });
        }

        // 初始检查,以防脚本在页面准备好后加载
        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            addSummarizeButton();
            fillEmptySpaces(); // 启动布局优化
        } else {
            window.addEventListener('DOMContentLoaded', () => {
                addSummarizeButton();
                fillEmptySpaces(); // 启动布局优化
            }, { once: true });
        }

        // 监听YouTube的特定导航事件(比URL轮询更可靠)
        // 'yt-navigate-finish'在导航和内容更新后触发
        window.addEventListener('yt-navigate-finish', () => {
            debugLog("yt-navigate-finish事件检测到。");
            // 使用requestAnimationFrame确保事件后布局可能稳定
            requestAnimationFrame(() => {
                addSummarizeButton();
                fillEmptySpaces(); // 重新优化布局
            });
        });

        // 处理浏览器的后退/前进
        window.addEventListener('popstate', () => {
            debugLog("popstate事件检测到。");
            requestAnimationFrame(() => {
                addSummarizeButton();
                fillEmptySpaces(); // 重新优化布局
            });
        });

        // YouTube的无限滚动处理
        window.addEventListener('scroll', debounce(() => {
            fillHomePageGaps();
        }, 500));

    } else if (window.location.hostname.includes('gemini.google.com')) {
        debugLog("Gemini域名检测到。");

        // 一旦DOM准备好就处理Gemini逻辑
        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            handleGeminiPage();
        } else {
            window.addEventListener('DOMContentLoaded', handleGeminiPage, { once: true });
        }
    } else {
        debugLog(`脚本加载在不被识别的域名上: ${window.location.hostname}`);
    }

    // 创建弹出窗口
    function createPopup() {
        const popup = document.createElement('div');
        popup.id = 'gemini-popup';
        popup.innerHTML = `
            <button id="gemini-start-summary" class="button">开始总结当前视频</button>
            <div id="gemini-status" class="status"></div>
        `;
        document.body.appendChild(popup);
        return popup;
    }

    // 显示弹出窗口
    function showPopup() {
        const popup = document.getElementById('gemini-popup') || createPopup();
        popup.style.display = 'block';

        const startButton = document.getElementById('gemini-start-summary');
        const statusDiv = document.getElementById('gemini-status');

        startButton.onclick = () => {
            try {
                if (!isVideoPage()) {
                    showStatus('请在YouTube视频页面使用此功能', 'error');
                    return;
                }

                handleSummarizeClick();
                showStatus('开始总结视频...', 'success');
                popup.style.display = 'none';
            } catch (error) {
                showStatus('发生错误:' + error.message, 'error');
            }
        };

        function showStatus(message, type) {
            statusDiv.textContent = message;
            statusDiv.className = 'status ' + type;
            statusDiv.style.display = 'block';
        }
    }

    // 初次执行布局优化,确保页面结构加载后运行
    if (window.location.hostname.includes('www.youtube.com')) {
        setTimeout(fillEmptySpaces, 1500);
    }

})();

QingJ © 2025

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