// ==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);
}
})();