// ==UserScript==
// @name YouTube Grid Auto-Scroll & Search (Ultra Optimized - Instant) - jQuery & Background - On Demand
// @match https://www.youtube.com/*
// @grant GM_addStyle
// @version 3.2
// @description Automatically scrolls to matching video titles on YouTube, highlighting all matches. Search box, search/stop, next/previous buttons, animated border, no results message. Uses jQuery. Works in background tabs. Only searches when the Search button is clicked.
// @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 = []; // jQuery objects
let currentHighlightIndex = -1;
let lastScrollHeight = 0;
let observer = null;
let isPaused = false; // Flag to pause the observer
let initialSearch = true; // Flag to run search function on first click
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) $buttonsContainer.length ? $searchBox.insertBefore($buttonsContainer) : $mastheadEnd.append($searchBox);
else { console.error("Could not find YouTube masthead end."); showErrorMessage("Masthead not found. Search box at top."); $('body').prepend($searchBox); }
}
// --- Show Error/No Results Messages ---
function showMessage(message, type) {
const id = type === 'error' ? 'search-error-message' : 'search-no-results-message';
let $messageDiv = $('#' + id);
if (!$messageDiv.length) $messageDiv = $('<div>', { id: id }).appendTo('body');
$messageDiv.text(message).show();
setTimeout(() => $messageDiv.hide(), 5000);
}
const showErrorMessage = (message) => showMessage(message, 'error');
const showNoResultsMessage = () => showMessage("No matching results found.", 'no-results');
// --- Stop Search ---
function stopSearch() {
isSearching = false;
initialSearch = true; // Reset initial search flag
clearTimeout(searchTimeout);
currentHighlightIndex = -1;
highlightedElements.forEach($el => $el.removeClass('highlighted-text').css('position', ''));
highlightedElements = [];
updateNavButtons();
resumeObserver();
}
// --- Navigate Results ---
function navigateResults(direction) {
if (highlightedElements.length === 0) return;
currentHighlightIndex = (currentHighlightIndex + direction + highlightedElements.length) % highlightedElements.length;
highlightedElements[currentHighlightIndex][0].scrollIntoView({ behavior: 'auto', block: 'center' });
updateNavButtons();
}
// --- Update Navigation Buttons ---
function updateNavButtons() {
$prevButton.prop('disabled', highlightedElements.length <= 1);
$nextButton.prop('disabled', highlightedElements.length <= 1);
}
// --- Pause/Resume Observer ---
function pauseObserver() {
if (observer && !isPaused) { observer.disconnect(); isPaused = true; }
}
function resumeObserver() {
if (observer && isPaused) { observeDOM(); isPaused = false; }
}
// --- Search and Scroll ---
function searchAndScroll() {
if (isSearching && !initialSearch) return; // Prevent re-entry if already searching (and not the initial search)
isSearching = true;
initialSearch = false; // Set the flag to false after the first click
clearTimeout(searchTimeout);
targetText = $searchInput.val().trim().toLowerCase();
if (!targetText) { isSearching = false; return; }
pauseObserver();
const $mediaElements = $('ytd-rich-grid-media');
let foundMatch = false;
highlightedElements = [];
$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');
});
let nextMatchIndex = -1;
if (currentHighlightIndex !== -1 && highlightedElements.length > 0) {
for (let i = currentHighlightIndex + 1; i < highlightedElements.length; i++)
if (highlightedElements[i].is(":visible")) { nextMatchIndex = i; break; }
} else {
for (let i = 0; i < highlightedElements.length; i++)
if (highlightedElements[i].is(":visible")) { nextMatchIndex = i; break; }
}
if (nextMatchIndex !== -1) {
highlightedElements[nextMatchIndex][0].scrollIntoView({ behavior: 'auto', block: 'center' });
currentHighlightIndex = nextMatchIndex;
updateNavButtons();
isSearching = false; // Stop after *first* visible match
} else {
lastScrollHeight = document.documentElement.scrollHeight;
window.scrollTo({ top: lastScrollHeight, behavior: 'auto' });
searchTimeout = setTimeout(() => {
if (!isSearching) return;
if (document.documentElement.scrollHeight === lastScrollHeight) {
stopSearch();
if (!foundMatch) showNoResultsMessage();
} else {
isSearching = false; // Allow re-entry after scroll
searchAndScroll();
}
}, SCROLL_DELAY_MS);
}
searchTimeout = setTimeout(() => { stopSearch(); showErrorMessage("Search timed out."); }, SEARCH_TIMEOUT_MS);
}
// --- Mutation Observer ---
function observeDOM() {
if (observer) observer.disconnect();
observer = new MutationObserver(() => {
//No action if the user did not click on "search"
});
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
}
// --- Initialization ---
createSearchBox();
observeDOM(); // Start observing, but it won't do anything until the search button is clicked
})();