Automatically scrolls to the *next* matching video title, highlighting all matches. Search box, search on click, stop, animated border, no results. Handles dynamic loading.
当前为
// ==UserScript==
// @name YouTube Grid Auto-Scroll & Search (Ultra Optimized - Instant)
// @match https://www.youtube.com/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @version 2.7
// @description Automatically scrolls to the *next* matching video title, highlighting all matches. Search box, search on click, stop, animated border, no results. Handles dynamic loading.
// @author Your Name (with further optimization)
// @license MIT
// @namespace https://greasyfork.org/users/1435316
// ==/UserScript==
(function() {
'use strict';
let targetText = "";
let searchBox;
let isSearching = false;
let searchInput;
let searchButton;
let stopButton;
let searchTimeout;
const SEARCH_TIMEOUT_MS = 20000; // 20 seconds
const SCROLL_DELAY_MS = 750;
const MAX_SEARCH_LENGTH = 255;
let lastFoundIndex = -1; // Index of the last highlighted element
let lastScrollHeight = 0; // Keep track of scroll height to detect end of content
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;
}
.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;
top: 50px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.8);
color: white;
border-radius: 5px;
z-index: 10000;
display: none;
}
/* Style for the no results message */
#search-no-results-message {
color: #aaa;
padding: 5px;
position: fixed;
top: 50px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.8);
border-radius: 5px;
z-index: 10000;
display: none;
}
`);
// --- Create the Search Box ---
function createSearchBox() {
searchBox = document.createElement('div');
searchBox.id = 'floating-search-box';
searchBox.setAttribute('role', 'search'); // Add ARIA role for accessibility
searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = 'Search to scroll...';
searchInput.value = GM_getValue('lastSearchTerm', '');
searchInput.setAttribute('aria-label', 'Search within YouTube grid'); // ARIA label
searchInput.maxLength = MAX_SEARCH_LENGTH; // Limit input length
searchButton = document.createElement('button');
searchButton.textContent = 'Search';
searchButton.addEventListener('click', searchAndScroll);
searchButton.setAttribute('aria-label', 'Start search'); // ARIA label
stopButton = document.createElement('button');
stopButton.textContent = 'Stop';
stopButton.id = 'stop-search-button';
stopButton.addEventListener('click', stopSearch);
stopButton.setAttribute('aria-label', 'Stop search'); // ARIA label
searchBox.appendChild(searchInput);
searchBox.appendChild(searchButton);
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("Could not find the YouTube masthead. Search box placed at top of page.");
document.body.insertBefore(searchBox, document.body.firstChild); //fallback
}
// Trigger search on load if text is present AND we're on a videos page
if (searchInput.value.trim() !== "" && window.location.href.includes("/videos")) {
searchAndScroll();
}
}
// --- Show Error Message ---
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(() => {
errorDiv.style.display = 'none';
}, 5000); // Hide after 5 seconds
}
// --- Show "No Results" Message ---
function showNoResultsMessage() {
let noResultsDiv = document.getElementById('search-no-results-message');
if (!noResultsDiv) {
noResultsDiv = document.createElement('div');
noResultsDiv.id = 'search-no-results-message';
noResultsDiv.textContent = "No matching results found.";
document.body.appendChild(noResultsDiv);
}
noResultsDiv.style.display = 'block';
setTimeout(() => {
noResultsDiv.style.display = 'none';
}, 5000);
}
// --- Stop Search Function ---
function stopSearch() {
if (observer) {
observer.disconnect();
observer = null; // Ensure observer is nulled out
}
isSearching = false;
clearTimeout(searchTimeout);
// Reset the index when a new search starts
lastFoundIndex = -1;
}
// --- Optimized Search and Scroll Function ---
function searchAndScroll() {
if (isSearching) return;
isSearching = true;
clearTimeout(searchTimeout);
targetText = searchInput.value.trim().toLowerCase();
if (!targetText) {
isSearching = false;
return;
}
GM_setValue('lastSearchTerm', targetText);
// Get all *visible* media elements
const mediaElements = Array.from(document.querySelectorAll('ytd-rich-grid-media:not([style*="display: none"])'));
// Find the next matching element, starting from lastFoundIndex + 1
let nextMatchIndex = -1;
for (let i = lastFoundIndex + 1; i < mediaElements.length; i++) {
const titleElement = mediaElements[i].querySelector('#video-title');
if (titleElement && titleElement.textContent.toLowerCase().includes(targetText)) {
nextMatchIndex = i;
break; // Stop searching once we find the *next* match
}
}
// If a match is found, highlight it and scroll to it
if (nextMatchIndex !== -1) {
const matchElement = mediaElements[nextMatchIndex];
matchElement.classList.add('highlighted-text');
matchElement.scrollIntoView({ behavior: 'auto', block: 'center' });
lastFoundIndex = nextMatchIndex;
isSearching = false; // Stop searching after finding a match
} else {
// No match found in the currently visible elements. Scroll down.
lastScrollHeight = document.documentElement.scrollHeight; // Store current height
window.scrollTo({ top: lastScrollHeight, behavior: 'auto' });
// Set a timeout to continue searching after a delay (to allow new content to load)
searchTimeout = setTimeout(() => {
if (!isSearching) return; // Check if searching was stopped
// Check if we've reached the end of the page
if (document.documentElement.scrollHeight === lastScrollHeight) {
stopSearch();
showNoResultsMessage(); // No more content, and no match found
} else {
isSearching = false; // allow searchAndScroll to run it again
searchAndScroll(); // Continue searching with newly loaded content
}
}, SCROLL_DELAY_MS);
}
// Set overall search timeout (for extreme cases)
searchTimeout = setTimeout(() => {
stopSearch();
showErrorMessage("Search timed out.");
}, SEARCH_TIMEOUT_MS);
}
// --- Initialization ---
createSearchBox();
})();