您需要先安装一个扩展,例如 篡改猴、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 correctly.
当前为
- // ==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.9
- // @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 correctly.
- // @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 prevButton; // Previous button
- let nextButton; // Next button
- 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
- let currentHighlightIndex = -1; // Index of *currently* highlighted element in highlightedElements
- let lastScrollHeight = 0;
- 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 = document.createElement('div');
- searchBox.id = 'floating-search-box';
- searchBox.setAttribute('role', 'search');
- 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');
- searchInput.maxLength = MAX_SEARCH_LENGTH;
- searchButton = document.createElement('button');
- searchButton.textContent = 'Search';
- searchButton.addEventListener('click', searchAndScroll);
- searchButton.setAttribute('aria-label', 'Start search');
- // Previous and Next buttons
- prevButton = document.createElement('button');
- prevButton.textContent = 'Prev';
- prevButton.id = 'prev-result-button';
- prevButton.addEventListener('click', () => navigateResults(-1)); // -1 for previous
- prevButton.setAttribute('aria-label', 'Previous result');
- prevButton.disabled = true; // Initially disabled
- nextButton = document.createElement('button');
- nextButton.textContent = 'Next';
- nextButton.id = 'next-result-button';
- nextButton.addEventListener('click', () => navigateResults(1)); // 1 for next
- nextButton.disabled = true; // Initially disabled
- stopButton = document.createElement('button');
- stopButton.textContent = 'Stop';
- 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("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() {
- isSearching = false;
- clearTimeout(searchTimeout);
- currentHighlightIndex = -1; // Reset current highlight index
- // Remove highlighting, but keep the array for potential re-use
- document.querySelectorAll('.highlighted-text').forEach(el => {
- el.classList.remove('highlighted-text');
- el.style.position = '';
- });
- updateNavButtons(); // Disable buttons
- }
- // --- Navigate Between Results ---
- function navigateResults(direction) {
- if (highlightedElements.length === 0) return;
- currentHighlightIndex += direction;
- // Wrap around
- if (currentHighlightIndex < 0) {
- currentHighlightIndex = highlightedElements.length - 1;
- } else if (currentHighlightIndex >= highlightedElements.length) {
- currentHighlightIndex = 0;
- }
- highlightedElements[currentHighlightIndex].scrollIntoView({ behavior: 'auto', block: 'center' });
- updateNavButtons();
- }
- // --- Update Navigation Buttons State ---
- function updateNavButtons() {
- prevButton.disabled = highlightedElements.length <= 1;
- nextButton.disabled = highlightedElements.length <= 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"])'));
- let foundMatch = false;
- // Find *all* matching elements and add them to highlightedElements
- highlightedElements = []; // Clear previous results
- 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');
- highlightedElements.push(mediaElements[i]);
- foundMatch = true;
- }
- }
- // Find the next match index, starting from the *element after* the current one
- let nextMatchIndex = -1;
- if (currentHighlightIndex !== -1 && highlightedElements.length > 0) {
- // Find the DOM element corresponding to currentHighlightIndex
- let currentElement = highlightedElements[currentHighlightIndex];
- // Get all currently visible media elements *again* (because more might have loaded)
- const currentMediaElements = Array.from(document.querySelectorAll('ytd-rich-grid-media:not([style*="display: none"])'));
- // Find the index of the current element within the *current* DOM
- let currentDomIndex = currentMediaElements.indexOf(currentElement);
- // Start searching from the element *after* the current one in the DOM
- for (let i = currentDomIndex + 1; i < currentMediaElements.length; i++) {
- const titleElement = currentMediaElements[i].querySelector('#video-title');
- if (titleElement && titleElement.textContent.toLowerCase().includes(targetText)) {
- // Find the index of this element within highlightedElements
- nextMatchIndex = highlightedElements.indexOf(currentMediaElements[i]);
- break;
- }
- }
- } else {
- // If no previous match, find the first one
- if (highlightedElements.length > 0) {
- nextMatchIndex = 0;
- }
- }
- if (nextMatchIndex !== -1) {
- // Scroll to the next match
- highlightedElements[nextMatchIndex].scrollIntoView({ behavior: 'auto', block: 'center' });
- currentHighlightIndex = nextMatchIndex;
- updateNavButtons();
- isSearching = false; // Stop searching after finding a match
- } else {
- // No new match found in the currently visible elements, scroll down
- lastScrollHeight = document.documentElement.scrollHeight;
- 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 we've reached the end of the page
- if (document.documentElement.scrollHeight === lastScrollHeight) {
- stopSearch();
- if (!foundMatch) {
- showNoResultsMessage();
- }
- } else {
- isSearching = false; // Allow re-entry into searchAndScroll
- searchAndScroll(); // Continue searching
- }
- }, SCROLL_DELAY_MS);
- }
- // Set overall search timeout (for extreme cases)
- searchTimeout = setTimeout(() => {
- stopSearch();
- showErrorMessage("Search timed out.");
- }, SEARCH_TIMEOUT_MS);
- }
- // --- Initialization ---
- createSearchBox();
- })();