您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automatically scrolls to the *next* matching video title, highlighting all matches. Search box, search/stop, next/previous buttons, animated border, no results message. Handles dynamic loading and browser minimize/restore correctly. Does NOT remember previous search.
当前为
// ==UserScript== // @name YouTube Grid Auto-Scroll & Search (Ultra Optimized - Instant) - More Robust Visibility Handling // @match https://www.youtube.com/* // @grant GM_addStyle // @version 3.3 // @description Automatically scrolls to the *next* matching video title, highlighting all matches. Search box, search/stop, next/previous buttons, animated border, no results message. Handles dynamic loading and browser minimize/restore correctly. Does NOT remember previous search. // @author Your Name // @license MIT // @namespace https://gf.zukizuki.org/users/1435316 // ==/UserScript== (function() { 'use strict'; let targetText = ""; let searchBox; let isSearching = false; let searchInput; let searchButton; let stopButton; let prevButton; let nextButton; let scrollContinuationTimeout; let overallSearchTimeout; const SEARCH_TIMEOUT_MS = 20000; const SCROLL_DELAY_MS = 750; const MAX_SEARCH_LENGTH = 255; let highlightedElements = []; let currentHighlightIndex = -1; let lastScrollHeight = 0; let hasScrolledToResultThisSession = false; GM_addStyle(` /* Existing CSS (with additions) */ #floating-search-box { background-color: #222; padding: 5px; border: 1px solid #444; border-radius: 5px; display: flex; align-items: center; margin-left: 10px; } /* Responsive width for smaller screens */ @media (max-width: 768px) { #floating-search-box input[type="text"] { width: 150px; /* Smaller width on smaller screens */ } } #floating-search-box input[type="text"] { background-color: #333; color: #fff; border: 1px solid #555; padding: 3px 5px; border-radius: 3px; margin-right: 5px; width: 200px; height: 30px; } #floating-search-box input[type="text"]:focus { outline: none; border-color: #065fd4; } #floating-search-box button { background-color: #065fd4; color: white; border: none; padding: 3px 8px; border-radius: 3px; cursor: pointer; height: 30px; } #floating-search-box button:hover { background-color: #0549a8; } #floating-search-box button:focus { outline: none; } #stop-search-button { background-color: #aa0000; /* Red color */ } #stop-search-button:hover { background-color: #800000; } /* Style for navigation buttons */ #prev-result-button, #next-result-button { background-color: #444; color: white; margin: 0 3px; /* Add some spacing */ } #prev-result-button:hover, #next-result-button:hover { background-color: #555; } .highlighted-text { position: relative; /* Needed for the border to be positioned correctly */ z-index: 1; /* Ensure the border is on top of other elements */ } /* Creates the animated border effect */ .highlighted-text::before { content: ''; position: absolute; top: -2px; left: -2px; right: -2px; bottom: -2px; border: 2px solid transparent; /* Transparent border to start */ border-radius: 8px; /* Rounded corners */ background: linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet); /* Rainbow gradient */ background-size: 400% 400%; /* Make the gradient larger than the element */ animation: gradientAnimation 5s ease infinite; /* Animate the background position */ z-index: -1; /* Behind the content */ } /* Keyframes for the gradient animation */ @keyframes gradientAnimation { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } /* Style for the error message */ #search-error-message { color: red; font-weight: bold; padding: 5px; position: fixed; /* Fixed position */ top: 50px; /* Position below the masthead (adjust as needed)*/ left: 50%; transform: translateX(-50%); /* Center horizontally */ background-color: rgba(0, 0, 0, 0.8); /* Semi-transparent black */ color: white; border-radius: 5px; z-index: 10000; /* Ensure it's on top */ display: none; /* Initially hidden */ } /* Style for the no results message */ #search-no-results-message { color: #aaa; /* Light gray */ padding: 5px; position: fixed; top: 50px; /* Same position as error message */ left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.8); border-radius: 5px; z-index: 10000; display: none; /* Initially hidden */ } `); function ensureSearchBoxAttached() { if (searchBox && !document.body.contains(searchBox)) { console.log("YouTube Grid Auto-Scroll: Search box detached, attempting to re-attach."); const mastheadEnd = document.querySelector('#end.ytd-masthead'); const buttonsContainer = document.querySelector('#end #buttons'); if (mastheadEnd) { if (buttonsContainer) { mastheadEnd.insertBefore(searchBox, buttonsContainer); } else { mastheadEnd.appendChild(searchBox); } } else { console.error("YouTube Grid Auto-Scroll: Could not find masthead to re-attach search box. Search UI might not be visible."); if (!document.body.contains(searchBox)) { document.body.insertBefore(searchBox, document.body.firstChild); } } } } function createSearchBox() { searchBox = document.createElement('div'); searchBox.id = 'floating-search-box'; searchBox.setAttribute('role', 'search'); searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.placeholder = 'Поиск для прокрутки...'; searchInput.value = ''; searchInput.setAttribute('aria-label', 'Search within YouTube grid'); searchInput.maxLength = MAX_SEARCH_LENGTH; searchButton = document.createElement('button'); searchButton.textContent = 'Поиск'; searchButton.addEventListener('click', () => { stopSearch(); isSearching = true; hasScrolledToResultThisSession = false; currentHighlightIndex = -1; // Ensure search starts from the first match searchAndScroll(); }); searchButton.setAttribute('aria-label', 'Start search'); prevButton = document.createElement('button'); prevButton.textContent = 'Пред.'; prevButton.id = 'prev-result-button'; prevButton.addEventListener('click', () => navigateResults(-1)); prevButton.setAttribute('aria-label', 'Previous result'); prevButton.disabled = true; nextButton = document.createElement('button'); nextButton.textContent = 'След.'; nextButton.id = 'next-result-button'; nextButton.addEventListener('click', () => navigateResults(1)); nextButton.disabled = true; stopButton = document.createElement('button'); stopButton.textContent = 'Стоп'; stopButton.id = 'stop-search-button'; stopButton.addEventListener('click', stopSearch); stopButton.setAttribute('aria-label', 'Stop search'); searchBox.appendChild(searchInput); searchBox.appendChild(searchButton); searchBox.appendChild(prevButton); searchBox.appendChild(nextButton); searchBox.appendChild(stopButton); const mastheadEnd = document.querySelector('#end.ytd-masthead'); const buttonsContainer = document.querySelector('#end #buttons'); if (mastheadEnd) { if(buttonsContainer){ mastheadEnd.insertBefore(searchBox, buttonsContainer); } else{ mastheadEnd.appendChild(searchBox); } } else { console.error("Could not find the YouTube masthead's end element."); showErrorMessage("Не удалось найти шапку YouTube. Блок поиска размещен вверху страницы."); document.body.insertBefore(searchBox, document.body.firstChild); } } function showErrorMessage(message) { let errorDiv = document.getElementById('search-error-message'); if (!errorDiv) { errorDiv = document.createElement('div'); errorDiv.id = 'search-error-message'; document.body.appendChild(errorDiv); } errorDiv.textContent = message; errorDiv.style.display = 'block'; setTimeout(() => { if(errorDiv) errorDiv.style.display = 'none'; }, 5000); } function showNoResultsMessage() { let noResultsDiv = document.getElementById('search-no-results-message'); if (!noResultsDiv) { noResultsDiv = document.createElement('div'); noResultsDiv.id = 'search-no-results-message'; noResultsDiv.textContent = "Совпадений не найдено."; document.body.appendChild(noResultsDiv); } noResultsDiv.style.display = 'block'; setTimeout(() => { if(noResultsDiv) noResultsDiv.style.display = 'none'; }, 5000); } function stopSearch() { isSearching = false; // hasScrolledToResultThisSession = false; // Should be reset when a new search *starts* clearTimeout(scrollContinuationTimeout); clearTimeout(overallSearchTimeout); currentHighlightIndex = -1; highlightedElements = []; document.querySelectorAll('.highlighted-text').forEach(el => { el.classList.remove('highlighted-text'); }); updateNavButtons(); } function navigateResults(direction) { if (highlightedElements.length === 0) return; currentHighlightIndex += direction; if (currentHighlightIndex < 0) currentHighlightIndex = highlightedElements.length - 1; else if (currentHighlightIndex >= highlightedElements.length) currentHighlightIndex = 0; if (highlightedElements[currentHighlightIndex]) { highlightedElements[currentHighlightIndex].scrollIntoView({ behavior: 'auto', block: 'center' }); hasScrolledToResultThisSession = true; // User actively navigated } updateNavButtons(); } function updateNavButtons() { prevButton.disabled = highlightedElements.length <= 1; nextButton.disabled = highlightedElements.length <= 1; } function searchAndScroll() { if (searchBox && !document.body.contains(searchBox)) { console.warn("YouTube Grid Auto-Scroll: Search box is not in the document. Stopping script operations."); showErrorMessage("UI поиска потерян. Пожалуйста, перезагрузите страницу или попробуйте переустановить скрипт."); stopSearch(); // Full stop, including clearing isSearching return; } if (!isSearching) { // If not in an active search session, do nothing further. clearTimeout(scrollContinuationTimeout); clearTimeout(overallSearchTimeout); return; } clearTimeout(scrollContinuationTimeout); targetText = searchInput.value.trim().toLowerCase(); if (!targetText) { stopSearch(); // This sets isSearching = false return; } clearTimeout(overallSearchTimeout); // Clear previous overall timeout overallSearchTimeout = setTimeout(() => { if (isSearching) { // Check if search is still considered active showErrorMessage("Поиск прерван по таймауту."); stopSearch(); // This sets isSearching = false } }, SEARCH_TIMEOUT_MS); document.querySelectorAll('.highlighted-text').forEach(el => { el.classList.remove('highlighted-text'); }); const mediaElements = Array.from(document.querySelectorAll('ytd-rich-grid-media:not([style*="display: none"])')); let newlyFoundHighlightedElements = []; for (let i = 0; i < mediaElements.length; i++) { const titleElement = mediaElements[i].querySelector('#video-title'); if (titleElement && titleElement.textContent.toLowerCase().includes(targetText)) { mediaElements[i].classList.add('highlighted-text'); newlyFoundHighlightedElements.push(mediaElements[i]); } } highlightedElements = newlyFoundHighlightedElements; // Update global list for Prev/Next updateNavButtons(); let elementToScrollTo = null; let newActiveHighlightIndex = -1; if (highlightedElements.length > 0) { if (currentHighlightIndex === -1 || currentHighlightIndex >= highlightedElements.length) { // This case is for initial search (currentHighlightIndex is -1 from search button or stopSearch) // or if the previous index is now out of bounds for the new set of highlights. newActiveHighlightIndex = 0; // Target the first found item } else { // This case is for subsequent calls to searchAndScroll during an active auto-scroll session // (e.g., after page scroll and new items are loaded). // currentHighlightIndex holds the index from the *previous successful find*. // We want to scroll to the *next* item in the *current* set of highlightedElements. newActiveHighlightIndex = (currentHighlightIndex + 1) % highlightedElements.length; } elementToScrollTo = highlightedElements[newActiveHighlightIndex]; } if (elementToScrollTo) { elementToScrollTo.scrollIntoView({ behavior: 'auto', block: 'center' }); currentHighlightIndex = newActiveHighlightIndex; // Update the global current index hasScrolledToResultThisSession = true; isSearching = false; // Found a match, pause active auto-searching. User can use Prev/Next or start new search. clearTimeout(overallSearchTimeout); // Successful find, clear overall timeout. } else { // No matches found in the current viewport (or highlightedElements is empty). // Continue searching by scrolling down, but only if isSearching is still true. if (!isSearching) { // If isSearching was set to false by other means (e.g. stop button during processing) clearTimeout(overallSearchTimeout); return; } lastScrollHeight = document.documentElement.scrollHeight; window.scrollTo({ top: lastScrollHeight, behavior: 'auto' }); scrollContinuationTimeout = setTimeout(() => { if (!isSearching) return; // Stop if search was cancelled during this timeout if (document.documentElement.scrollHeight === lastScrollHeight) { // Reached end of page, and no new content loaded. const searchWasActiveBeforeStop = isSearching; // Capture state stopSearch(); // End the search session (sets isSearching = false) if (searchWasActiveBeforeStop && !hasScrolledToResultThisSession && targetText) { showNoResultsMessage(); } } else { // New content might have loaded. isSearching is still true. // currentHighlightIndex might point to the last found item. // The next call to searchAndScroll will try to find the item after currentHighlightIndex. searchAndScroll(); // Continue searching } }, SCROLL_DELAY_MS); } } // --- Event Listener for Visibility Change --- document.addEventListener('visibilitychange', function() { if (!document.hidden) { // Tab is now visible ensureSearchBoxAttached(); // If a search was (or was intended to be) active when tab was hidden, // robustly restart the search process. if (isSearching || (searchInput && searchInput.value.trim() && currentHighlightIndex !== -1) ) { // The condition `currentHighlightIndex !== -1` might indicate a previous successful search session // whose state we want to refresh or continue if the user expects it. // However, the most robust way if `isSearching` was true is a full restart. // If `isSearching` is false but there's text and a highlight, user might expect current view. // Let's primarily focus on restarting if `isSearching` was true (meaning an operation was pending). const wasSearchingBeforeVisibilityChange = isSearching; // Capture the flag const currentSearchTermInBox = searchInput ? searchInput.value : ""; if (wasSearchingBeforeVisibilityChange) { console.log("YouTube Grid Auto-Scroll: Tab became visible during an active search. Restarting search process."); stopSearch(); // Fully reset state: sets isSearching=false, clears timers, highlights, currentHighlightIndex. if (searchInput) searchInput.value = currentSearchTermInBox; // Restore text if stopSearch cleared it (it doesn't) if (currentSearchTermInBox.trim()) { isSearching = true; // Re-enable searching flag hasScrolledToResultThisSession = false; // Reset for this new search session currentHighlightIndex = -1; // Ensure search starts from the very first match searchAndScroll(); // Start a fresh search } } // If !wasSearchingBeforeVisibilityChange, we assume the script was idle or had completed a search. // In that case, we don't automatically restart anything. User can initiate new search if needed. } } // No special action when tab is hidden; existing timeouts will be throttled by browser. }); // --- Initialization --- if (document.readyState === 'complete' || document.readyState === 'interactive') { createSearchBox(); } else { document.addEventListener('DOMContentLoaded', createSearchBox); } })();