YouTube Grid Auto-Scroll & Search (Ultra Optimized - Instant) - More Robust Visibility Handling

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);
    }

})();