YouTube Grid Auto-Scroll & Search (Ultra Optimized - Instant)

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.

当前为 2025-03-02 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Grid Auto-Scroll & Search (Ultra Optimized - Instant)
  3. // @match https://www.youtube.com/*
  4. // @grant GM_addStyle
  5. // @grant GM_getValue
  6. // @grant GM_setValue
  7. // @version 2.9
  8. // @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.
  9. // @author Your Name (with further optimization)
  10. // @license MIT
  11. // @namespace https://greasyfork.org/users/1435316
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. let targetText = "";
  18. let searchBox;
  19. let isSearching = false;
  20. let searchInput;
  21. let searchButton;
  22. let stopButton;
  23. let prevButton; // Previous button
  24. let nextButton; // Next button
  25. let searchTimeout;
  26. const SEARCH_TIMEOUT_MS = 20000; // 20 seconds
  27. const SCROLL_DELAY_MS = 750;
  28. const MAX_SEARCH_LENGTH = 255;
  29. let highlightedElements = []; // Array to store all highlighted elements
  30. let currentHighlightIndex = -1; // Index of *currently* highlighted element in highlightedElements
  31. let lastScrollHeight = 0;
  32.  
  33. GM_addStyle(`
  34. /* Existing CSS (with additions) */
  35. #floating-search-box {
  36. background-color: #222;
  37. padding: 5px;
  38. border: 1px solid #444;
  39. border-radius: 5px;
  40. display: flex;
  41. align-items: center;
  42. margin-left: 10px;
  43. }
  44. /* Responsive width for smaller screens */
  45. @media (max-width: 768px) {
  46. #floating-search-box input[type="text"] {
  47. width: 150px; /* Smaller width on smaller screens */
  48. }
  49. }
  50.  
  51. #floating-search-box input[type="text"] {
  52. background-color: #333;
  53. color: #fff;
  54. border: 1px solid #555;
  55. padding: 3px 5px;
  56. border-radius: 3px;
  57. margin-right: 5px;
  58. width: 200px;
  59. height: 30px;
  60. }
  61. #floating-search-box input[type="text"]:focus {
  62. outline: none;
  63. border-color: #065fd4;
  64. }
  65. #floating-search-box button {
  66. background-color: #065fd4;
  67. color: white;
  68. border: none;
  69. padding: 3px 8px;
  70. border-radius: 3px;
  71. cursor: pointer;
  72. height: 30px;
  73. }
  74. #floating-search-box button:hover {
  75. background-color: #0549a8;
  76. }
  77. #floating-search-box button:focus {
  78. outline: none;
  79. }
  80.  
  81. #stop-search-button {
  82. background-color: #aa0000; /* Red color */
  83. }
  84. #stop-search-button:hover {
  85. background-color: #800000;
  86. }
  87.  
  88. /* Style for navigation buttons */
  89. #prev-result-button, #next-result-button {
  90. background-color: #444;
  91. color: white;
  92. margin: 0 3px; /* Add some spacing */
  93. }
  94. #prev-result-button:hover, #next-result-button:hover {
  95. background-color: #555;
  96. }
  97.  
  98. .highlighted-text {
  99. position: relative; /* Needed for the border to be positioned correctly */
  100. z-index: 1; /* Ensure the border is on top of other elements */
  101. }
  102.  
  103. /* Creates the animated border effect */
  104. .highlighted-text::before {
  105. content: '';
  106. position: absolute;
  107. top: -2px;
  108. left: -2px;
  109. right: -2px;
  110. bottom: -2px;
  111. border: 2px solid transparent; /* Transparent border to start */
  112. border-radius: 8px; /* Rounded corners */
  113. background: linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet); /* Rainbow gradient */
  114. background-size: 400% 400%; /* Make the gradient larger than the element */
  115. animation: gradientAnimation 5s ease infinite; /* Animate the background position */
  116. z-index: -1; /* Behind the content */
  117. }
  118. /* Keyframes for the gradient animation */
  119. @keyframes gradientAnimation {
  120. 0% {
  121. background-position: 0% 50%;
  122. }
  123. 50% {
  124. background-position: 100% 50%;
  125. }
  126. 100% {
  127. background-position: 0% 50%;
  128. }
  129. }
  130.  
  131. /* Style for the error message */
  132. #search-error-message {
  133. color: red;
  134. font-weight: bold;
  135. padding: 5px;
  136. position: fixed; /* Fixed position */
  137. top: 50px; /* Position below the masthead (adjust as needed)*/
  138. left: 50%;
  139. transform: translateX(-50%); /* Center horizontally */
  140. background-color: rgba(0, 0, 0, 0.8); /* Semi-transparent black */
  141. color: white;
  142. border-radius: 5px;
  143. z-index: 10000; /* Ensure it's on top */
  144. display: none; /* Initially hidden */
  145. }
  146.  
  147. /* Style for the no results message */
  148. #search-no-results-message {
  149. color: #aaa; /* Light gray */
  150. padding: 5px;
  151. position: fixed;
  152. top: 50px; /* Same position as error message */
  153. left: 50%;
  154. transform: translateX(-50%);
  155. background-color: rgba(0, 0, 0, 0.8);
  156. border-radius: 5px;
  157. z-index: 10000;
  158. display: none; /* Initially hidden */
  159. }
  160.  
  161. `);
  162.  
  163. // --- Create the Search Box ---
  164. function createSearchBox() {
  165. searchBox = document.createElement('div');
  166. searchBox.id = 'floating-search-box';
  167. searchBox.setAttribute('role', 'search');
  168.  
  169. searchInput = document.createElement('input');
  170. searchInput.type = 'text';
  171. searchInput.placeholder = 'Search to scroll...';
  172. searchInput.value = GM_getValue('lastSearchTerm', '');
  173. searchInput.setAttribute('aria-label', 'Search within YouTube grid');
  174. searchInput.maxLength = MAX_SEARCH_LENGTH;
  175.  
  176. searchButton = document.createElement('button');
  177. searchButton.textContent = 'Search';
  178. searchButton.addEventListener('click', searchAndScroll);
  179. searchButton.setAttribute('aria-label', 'Start search');
  180.  
  181. // Previous and Next buttons
  182. prevButton = document.createElement('button');
  183. prevButton.textContent = 'Prev';
  184. prevButton.id = 'prev-result-button';
  185. prevButton.addEventListener('click', () => navigateResults(-1)); // -1 for previous
  186. prevButton.setAttribute('aria-label', 'Previous result');
  187. prevButton.disabled = true; // Initially disabled
  188.  
  189. nextButton = document.createElement('button');
  190. nextButton.textContent = 'Next';
  191. nextButton.id = 'next-result-button';
  192. nextButton.addEventListener('click', () => navigateResults(1)); // 1 for next
  193. nextButton.disabled = true; // Initially disabled
  194.  
  195. stopButton = document.createElement('button');
  196. stopButton.textContent = 'Stop';
  197. stopButton.id = 'stop-search-button';
  198. stopButton.addEventListener('click', stopSearch);
  199. stopButton.setAttribute('aria-label', 'Stop search');
  200.  
  201. searchBox.appendChild(searchInput);
  202. searchBox.appendChild(searchButton);
  203. searchBox.appendChild(prevButton);
  204. searchBox.appendChild(nextButton);
  205. searchBox.appendChild(stopButton);
  206.  
  207. const mastheadEnd = document.querySelector('#end.ytd-masthead');
  208. const buttonsContainer = document.querySelector('#end #buttons');
  209.  
  210. if (mastheadEnd) {
  211. if(buttonsContainer){
  212. mastheadEnd.insertBefore(searchBox, buttonsContainer);
  213. } else{
  214. mastheadEnd.appendChild(searchBox);
  215. }
  216. } else {
  217. console.error("Could not find the YouTube masthead's end element.");
  218. showErrorMessage("Could not find the YouTube masthead. Search box placed at top of page.");
  219. document.body.insertBefore(searchBox, document.body.firstChild); //fallback
  220. }
  221.  
  222. // Trigger search on load if text is present AND we're on a videos page
  223. if (searchInput.value.trim() !== "" && window.location.href.includes("/videos")) {
  224. searchAndScroll();
  225. }
  226. }
  227.  
  228. // --- Show Error Message ---
  229. function showErrorMessage(message) {
  230. let errorDiv = document.getElementById('search-error-message');
  231. if (!errorDiv) {
  232. errorDiv = document.createElement('div');
  233. errorDiv.id = 'search-error-message';
  234. document.body.appendChild(errorDiv);
  235. }
  236. errorDiv.textContent = message;
  237. errorDiv.style.display = 'block';
  238.  
  239. setTimeout(() => {
  240. errorDiv.style.display = 'none';
  241. }, 5000); // Hide after 5 seconds
  242. }
  243.  
  244. // --- Show "No Results" Message ---
  245. function showNoResultsMessage() {
  246. let noResultsDiv = document.getElementById('search-no-results-message');
  247. if (!noResultsDiv) {
  248. noResultsDiv = document.createElement('div');
  249. noResultsDiv.id = 'search-no-results-message';
  250. noResultsDiv.textContent = "No matching results found.";
  251. document.body.appendChild(noResultsDiv);
  252. }
  253. noResultsDiv.style.display = 'block';
  254.  
  255. setTimeout(() => {
  256. noResultsDiv.style.display = 'none';
  257. }, 5000);
  258. }
  259.  
  260. // --- Stop Search Function ---
  261. function stopSearch() {
  262. isSearching = false;
  263. clearTimeout(searchTimeout);
  264. currentHighlightIndex = -1; // Reset current highlight index
  265. // Remove highlighting, but keep the array for potential re-use
  266. document.querySelectorAll('.highlighted-text').forEach(el => {
  267. el.classList.remove('highlighted-text');
  268. el.style.position = '';
  269. });
  270. updateNavButtons(); // Disable buttons
  271. }
  272.  
  273. // --- Navigate Between Results ---
  274. function navigateResults(direction) {
  275. if (highlightedElements.length === 0) return;
  276.  
  277. currentHighlightIndex += direction;
  278.  
  279. // Wrap around
  280. if (currentHighlightIndex < 0) {
  281. currentHighlightIndex = highlightedElements.length - 1;
  282. } else if (currentHighlightIndex >= highlightedElements.length) {
  283. currentHighlightIndex = 0;
  284. }
  285.  
  286. highlightedElements[currentHighlightIndex].scrollIntoView({ behavior: 'auto', block: 'center' });
  287. updateNavButtons();
  288. }
  289.  
  290. // --- Update Navigation Buttons State ---
  291. function updateNavButtons() {
  292. prevButton.disabled = highlightedElements.length <= 1;
  293. nextButton.disabled = highlightedElements.length <= 1;
  294. }
  295.  
  296.  
  297. // --- Optimized Search and Scroll Function ---
  298. function searchAndScroll() {
  299. if (isSearching) return;
  300. isSearching = true;
  301. clearTimeout(searchTimeout);
  302.  
  303. targetText = searchInput.value.trim().toLowerCase();
  304. if (!targetText) {
  305. isSearching = false;
  306. return;
  307. }
  308.  
  309. GM_setValue('lastSearchTerm', targetText);
  310.  
  311. // Get all *visible* media elements
  312. const mediaElements = Array.from(document.querySelectorAll('ytd-rich-grid-media:not([style*="display: none"])'));
  313. let foundMatch = false;
  314.  
  315. // Find *all* matching elements and add them to highlightedElements
  316. highlightedElements = []; // Clear previous results
  317. for (let i = 0; i < mediaElements.length; i++) {
  318. const titleElement = mediaElements[i].querySelector('#video-title');
  319. if (titleElement && titleElement.textContent.toLowerCase().includes(targetText)) {
  320. mediaElements[i].classList.add('highlighted-text');
  321. highlightedElements.push(mediaElements[i]);
  322. foundMatch = true;
  323. }
  324. }
  325.  
  326. // Find the next match index, starting from the *element after* the current one
  327. let nextMatchIndex = -1;
  328. if (currentHighlightIndex !== -1 && highlightedElements.length > 0) {
  329. // Find the DOM element corresponding to currentHighlightIndex
  330. let currentElement = highlightedElements[currentHighlightIndex];
  331.  
  332. // Get all currently visible media elements *again* (because more might have loaded)
  333. const currentMediaElements = Array.from(document.querySelectorAll('ytd-rich-grid-media:not([style*="display: none"])'));
  334.  
  335. // Find the index of the current element within the *current* DOM
  336. let currentDomIndex = currentMediaElements.indexOf(currentElement);
  337.  
  338. // Start searching from the element *after* the current one in the DOM
  339. for (let i = currentDomIndex + 1; i < currentMediaElements.length; i++) {
  340. const titleElement = currentMediaElements[i].querySelector('#video-title');
  341. if (titleElement && titleElement.textContent.toLowerCase().includes(targetText)) {
  342. // Find the index of this element within highlightedElements
  343. nextMatchIndex = highlightedElements.indexOf(currentMediaElements[i]);
  344. break;
  345. }
  346. }
  347. } else {
  348. // If no previous match, find the first one
  349. if (highlightedElements.length > 0) {
  350. nextMatchIndex = 0;
  351. }
  352. }
  353.  
  354.  
  355. if (nextMatchIndex !== -1) {
  356. // Scroll to the next match
  357. highlightedElements[nextMatchIndex].scrollIntoView({ behavior: 'auto', block: 'center' });
  358. currentHighlightIndex = nextMatchIndex;
  359. updateNavButtons();
  360. isSearching = false; // Stop searching after finding a match
  361. } else {
  362. // No new match found in the currently visible elements, scroll down
  363. lastScrollHeight = document.documentElement.scrollHeight;
  364. window.scrollTo({ top: lastScrollHeight, behavior: 'auto' });
  365.  
  366. // Set a timeout to continue searching after a delay (to allow new content to load)
  367. searchTimeout = setTimeout(() => {
  368. if (!isSearching) return;
  369.  
  370. // Check if we've reached the end of the page
  371. if (document.documentElement.scrollHeight === lastScrollHeight) {
  372. stopSearch();
  373. if (!foundMatch) {
  374. showNoResultsMessage();
  375. }
  376. } else {
  377. isSearching = false; // Allow re-entry into searchAndScroll
  378. searchAndScroll(); // Continue searching
  379. }
  380. }, SCROLL_DELAY_MS);
  381. }
  382.  
  383. // Set overall search timeout (for extreme cases)
  384. searchTimeout = setTimeout(() => {
  385. stopSearch();
  386. showErrorMessage("Search timed out.");
  387. }, SEARCH_TIMEOUT_MS);
  388. }
  389.  
  390. // --- Initialization ---
  391. createSearchBox();
  392.  
  393. })();