// ==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 // @downloadURL none // ==/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 = `
`; 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); } })();