// ==UserScript==
// @name YouTube Grid Auto-Scroll & Search (Ultra Optimized - Instant) - jQuery & Background
// @match https://www.youtube.com/*
// @grant GM_addStyle
// @version 3.1
// @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. Uses jQuery. Works in background tabs.
// @author Your Name (with further optimization & jQuery)
// @license MIT
// @namespace https://greasyfork.org/users/1435316
// @require https://code.jquery.com/jquery-3.7.1.min.js
// ==/UserScript==
(function() {
'use strict';
let targetText = "";
let $searchBox;
let isSearching = false;
let $searchInput;
let $searchButton;
let $stopButton;
let $prevButton;
let $nextButton;
let searchTimeout;
const SEARCH_TIMEOUT_MS = 20000; // 20 seconds
const SCROLL_DELAY_MS = 750;
const MAX_SEARCH_LENGTH = 255;
let highlightedElements = []; // Array to store all highlighted elements (jQuery objects)
let currentHighlightIndex = -1; // Index of *currently* highlighted element
let lastScrollHeight = 0;
let observer = null; // MutationObserver
let isPaused = false; // Flag to pause the observer
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 */
}
`);
// --- Create the Search Box ---
function createSearchBox() {
$searchBox = $('<div>', {
id: 'floating-search-box',
role: 'search'
});
$searchInput = $('<input>', {
type: 'text',
placeholder: 'Search to scroll...',
value: '',
'aria-label': 'Search within YouTube grid',
maxlength: MAX_SEARCH_LENGTH
});
$searchButton = $('<button>', {
text: 'Search',
'aria-label': 'Start search',
click: searchAndScroll
});
$prevButton = $('<button>', {
text: 'Prev',
id: 'prev-result-button',
'aria-label': 'Previous result',
disabled: true,
click: () => navigateResults(-1)
});
$nextButton = $('<button>', {
text: 'Next',
id: 'next-result-button',
'aria-label': 'Next result',
disabled: true,
click: () => navigateResults(1)
});
$stopButton = $('<button>', {
text: 'Stop',
id: 'stop-search-button',
'aria-label': 'Stop search',
click: stopSearch
});
$searchBox.append($searchInput, $searchButton, $prevButton, $nextButton, $stopButton);
const $mastheadEnd = $('#end.ytd-masthead');
const $buttonsContainer = $('#end #buttons');
if ($mastheadEnd.length) {
if ($buttonsContainer.length) {
$searchBox.insertBefore($buttonsContainer);
} else {
$mastheadEnd.append($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.");
$('body').prepend($searchBox); // Fallback
}
}
// --- Show Error Message ---
function showErrorMessage(message) {
let $errorDiv = $('#search-error-message');
if (!$errorDiv.length) {
$errorDiv = $('<div>', { id: 'search-error-message' }).appendTo('body');
}
$errorDiv.text(message).show();
setTimeout(() => $errorDiv.hide(), 5000);
}
// --- Show "No Results" Message ---
function showNoResultsMessage() {
let $noResultsDiv = $('#search-no-results-message');
if (!$noResultsDiv.length) {
$noResultsDiv = $('<div>', { id: 'search-no-results-message', text: "No matching results found." }).appendTo('body');
}
$noResultsDiv.show();
setTimeout(() => $noResultsDiv.hide(), 5000);
}
// --- Stop Search Function ---
function stopSearch() {
isSearching = false;
clearTimeout(searchTimeout);
currentHighlightIndex = -1;
highlightedElements.forEach($el => $el.removeClass('highlighted-text').css('position', ''));
highlightedElements = []; // Clear the array
updateNavButtons();
resumeObserver(); // Resume when search stops
}
// --- Navigate Between Results ---
function navigateResults(direction) {
if (highlightedElements.length === 0) return;
currentHighlightIndex += direction;
if (currentHighlightIndex < 0) {
currentHighlightIndex = highlightedElements.length - 1;
} else if (currentHighlightIndex >= highlightedElements.length) {
currentHighlightIndex = 0;
}
highlightedElements[currentHighlightIndex][0].scrollIntoView({ behavior: 'auto', block: 'center' });
updateNavButtons();
}
// --- Update Navigation Buttons State ---
function updateNavButtons() {
$prevButton.prop('disabled', highlightedElements.length <= 1);
$nextButton.prop('disabled', highlightedElements.length <= 1);
}
// --- Pause the observer ---
function pauseObserver() {
if (observer && !isPaused) {
observer.disconnect();
isPaused = true;
//console.log("Observer paused");
}
}
// --- Resume the observer ---
function resumeObserver() {
if (observer && isPaused) {
observeDOM(); // Re-connect
isPaused = false;
//console.log("Observer resumed");
}
}
// --- Optimized Search and Scroll Function ---
function searchAndScroll() {
if (isSearching) return;
isSearching = true;
clearTimeout(searchTimeout);
targetText = $searchInput.val().trim().toLowerCase();
if (!targetText) {
isSearching = false;
return;
}
pauseObserver(); // Pause the observer during active search
// Get all media elements (regardless of visibility)
const $mediaElements = $('ytd-rich-grid-media'); // No :visible check
let foundMatch = false;
highlightedElements = []; // Clear previous results
$mediaElements.each(function() {
const $titleElement = $(this).find('#video-title');
if ($titleElement.length && $titleElement.text().toLowerCase().includes(targetText)) {
$(this).addClass('highlighted-text');
highlightedElements.push($(this));
foundMatch = true;
} else {
$(this).removeClass('highlighted-text'); // Remove highlighting if no match
}
});
// Find the next match index
let nextMatchIndex = -1;
if(currentHighlightIndex !== -1 && highlightedElements.length > 0) {
// Start searching from the element *after* the current one in highlightedElements
for(let i = currentHighlightIndex + 1; i < highlightedElements.length; i++) {
if(highlightedElements[i].is(":visible")){ //Only consider visible ones for scrolling
nextMatchIndex = i;
break;
}
}
} else {
// If no previous match, find the *first* visible one
for (let i = 0; i < highlightedElements.length; i++) {
if (highlightedElements[i].is(":visible")) {
nextMatchIndex = i;
break;
}
}
}
if (nextMatchIndex !== -1) {
// Scroll to the *next* match
highlightedElements[nextMatchIndex][0].scrollIntoView({ behavior: 'auto', block: 'center' });
currentHighlightIndex = nextMatchIndex;
updateNavButtons();
isSearching = false; // Stop searching after first visible match
} else {
// No new *visible* match found, scroll down
lastScrollHeight = document.documentElement.scrollHeight;
// Use native scrollTo for background tab compatibility
window.scrollTo({ top: lastScrollHeight, behavior: 'auto' });
searchTimeout = setTimeout(() => {
if (!isSearching) return; // Check again, in case stopSearch was called
if (document.documentElement.scrollHeight === lastScrollHeight) { // End of page
stopSearch();
if (!foundMatch) {
showNoResultsMessage();
}
} else {
isSearching = false; // Allow re-entry
searchAndScroll(); // Continue searching after scroll
}
}, SCROLL_DELAY_MS);
}
searchTimeout = setTimeout(() => {
stopSearch();
showErrorMessage("Search timed out.");
}, SEARCH_TIMEOUT_MS);
}
// --- Mutation Observer ---
function observeDOM() {
if (observer) {
observer.disconnect(); // Disconnect any existing observer
}
observer = new MutationObserver(mutations => {
if (!isSearching) { // Only re-run search if NOT actively searching
searchAndScroll(); // Re-evaluate on DOM changes
}
});
// Observe the entire document body for changes
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class'] // Observe style and class changes
});
}
// --- Initialization ---
createSearchBox();
observeDOM(); // Start observing
})();