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

Automatically scrolls to the *next* matching video title, highlighting all matches. Search box, search on click, stop, animated border, no results. Handles dynamic loading.

当前为 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.7
  8. // @description Automatically scrolls to the *next* matching video title, highlighting all matches. Search box, search on click, stop, animated border, no results. Handles dynamic loading.
  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 searchTimeout;
  24. const SEARCH_TIMEOUT_MS = 20000; // 20 seconds
  25. const SCROLL_DELAY_MS = 750;
  26. const MAX_SEARCH_LENGTH = 255;
  27. let lastFoundIndex = -1; // Index of the last highlighted element
  28. let lastScrollHeight = 0; // Keep track of scroll height to detect end of content
  29.  
  30. GM_addStyle(`
  31. /* Existing CSS (with additions) */
  32. #floating-search-box {
  33. background-color: #222;
  34. padding: 5px;
  35. border: 1px solid #444;
  36. border-radius: 5px;
  37. display: flex;
  38. align-items: center;
  39. margin-left: 10px;
  40. }
  41. /* Responsive width for smaller screens */
  42. @media (max-width: 768px) {
  43. #floating-search-box input[type="text"] {
  44. width: 150px; /* Smaller width on smaller screens */
  45. }
  46. }
  47.  
  48. #floating-search-box input[type="text"] {
  49. background-color: #333;
  50. color: #fff;
  51. border: 1px solid #555;
  52. padding: 3px 5px;
  53. border-radius: 3px;
  54. margin-right: 5px;
  55. width: 200px;
  56. height: 30px;
  57. }
  58. #floating-search-box input[type="text"]:focus {
  59. outline: none;
  60. border-color: #065fd4;
  61. }
  62. #floating-search-box button {
  63. background-color: #065fd4;
  64. color: white;
  65. border: none;
  66. padding: 3px 8px;
  67. border-radius: 3px;
  68. cursor: pointer;
  69. height: 30px;
  70. }
  71. #floating-search-box button:hover {
  72. background-color: #0549a8;
  73. }
  74. #floating-search-box button:focus {
  75. outline: none;
  76. }
  77.  
  78. #stop-search-button {
  79. background-color: #aa0000; /* Red color */
  80. }
  81. #stop-search-button:hover {
  82. background-color: #800000;
  83. }
  84.  
  85. .highlighted-text {
  86. position: relative; /* Needed for the border to be positioned correctly */
  87. z-index: 1; /* Ensure the border is on top of other elements */
  88. }
  89.  
  90. /* Creates the animated border effect */
  91. .highlighted-text::before {
  92. content: '';
  93. position: absolute;
  94. top: -2px;
  95. left: -2px;
  96. right: -2px;
  97. bottom: -2px;
  98. border: 2px solid transparent; /* Transparent border to start */
  99. border-radius: 8px; /* Rounded corners */
  100. background: linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet); /* Rainbow gradient */
  101. background-size: 400% 400%; /* Make the gradient larger than the element */
  102. animation: gradientAnimation 5s ease infinite; /* Animate the background position */
  103. z-index: -1; /* Behind the content */
  104. }
  105. /* Keyframes for the gradient animation */
  106. @keyframes gradientAnimation {
  107. 0% {
  108. background-position: 0% 50%;
  109. }
  110. 50% {
  111. background-position: 100% 50%;
  112. }
  113. 100% {
  114. background-position: 0% 50%;
  115. }
  116. }
  117. /* Style for the error message */
  118. #search-error-message {
  119. color: red;
  120. font-weight: bold;
  121. padding: 5px;
  122. position: fixed;
  123. top: 50px;
  124. left: 50%;
  125. transform: translateX(-50%);
  126. background-color: rgba(0, 0, 0, 0.8);
  127. color: white;
  128. border-radius: 5px;
  129. z-index: 10000;
  130. display: none;
  131. }
  132.  
  133. /* Style for the no results message */
  134. #search-no-results-message {
  135. color: #aaa;
  136. padding: 5px;
  137. position: fixed;
  138. top: 50px;
  139. left: 50%;
  140. transform: translateX(-50%);
  141. background-color: rgba(0, 0, 0, 0.8);
  142. border-radius: 5px;
  143. z-index: 10000;
  144. display: none;
  145. }
  146. `);
  147.  
  148. // --- Create the Search Box ---
  149. function createSearchBox() {
  150. searchBox = document.createElement('div');
  151. searchBox.id = 'floating-search-box';
  152. searchBox.setAttribute('role', 'search'); // Add ARIA role for accessibility
  153.  
  154. searchInput = document.createElement('input');
  155. searchInput.type = 'text';
  156. searchInput.placeholder = 'Search to scroll...';
  157. searchInput.value = GM_getValue('lastSearchTerm', '');
  158. searchInput.setAttribute('aria-label', 'Search within YouTube grid'); // ARIA label
  159. searchInput.maxLength = MAX_SEARCH_LENGTH; // Limit input length
  160.  
  161. searchButton = document.createElement('button');
  162. searchButton.textContent = 'Search';
  163. searchButton.addEventListener('click', searchAndScroll);
  164. searchButton.setAttribute('aria-label', 'Start search'); // ARIA label
  165.  
  166. stopButton = document.createElement('button');
  167. stopButton.textContent = 'Stop';
  168. stopButton.id = 'stop-search-button';
  169. stopButton.addEventListener('click', stopSearch);
  170. stopButton.setAttribute('aria-label', 'Stop search'); // ARIA label
  171.  
  172. searchBox.appendChild(searchInput);
  173. searchBox.appendChild(searchButton);
  174. searchBox.appendChild(stopButton);
  175.  
  176. const mastheadEnd = document.querySelector('#end.ytd-masthead');
  177. const buttonsContainer = document.querySelector('#end #buttons');
  178.  
  179. if (mastheadEnd) {
  180. if(buttonsContainer){
  181. mastheadEnd.insertBefore(searchBox, buttonsContainer);
  182. } else{
  183. mastheadEnd.appendChild(searchBox);
  184. }
  185. } else {
  186. console.error("Could not find the YouTube masthead's end element.");
  187. showErrorMessage("Could not find the YouTube masthead. Search box placed at top of page.");
  188. document.body.insertBefore(searchBox, document.body.firstChild); //fallback
  189. }
  190.  
  191. // Trigger search on load if text is present AND we're on a videos page
  192. if (searchInput.value.trim() !== "" && window.location.href.includes("/videos")) {
  193. searchAndScroll();
  194. }
  195. }
  196.  
  197.  
  198. // --- Show Error Message ---
  199. function showErrorMessage(message) {
  200. let errorDiv = document.getElementById('search-error-message');
  201. if (!errorDiv) {
  202. errorDiv = document.createElement('div');
  203. errorDiv.id = 'search-error-message';
  204. document.body.appendChild(errorDiv);
  205. }
  206. errorDiv.textContent = message;
  207. errorDiv.style.display = 'block';
  208.  
  209. setTimeout(() => {
  210. errorDiv.style.display = 'none';
  211. }, 5000); // Hide after 5 seconds
  212. }
  213. // --- Show "No Results" Message ---
  214. function showNoResultsMessage() {
  215. let noResultsDiv = document.getElementById('search-no-results-message');
  216. if (!noResultsDiv) {
  217. noResultsDiv = document.createElement('div');
  218. noResultsDiv.id = 'search-no-results-message';
  219. noResultsDiv.textContent = "No matching results found.";
  220. document.body.appendChild(noResultsDiv);
  221. }
  222. noResultsDiv.style.display = 'block';
  223.  
  224. setTimeout(() => {
  225. noResultsDiv.style.display = 'none';
  226. }, 5000);
  227. }
  228.  
  229. // --- Stop Search Function ---
  230. function stopSearch() {
  231. if (observer) {
  232. observer.disconnect();
  233. observer = null; // Ensure observer is nulled out
  234. }
  235. isSearching = false;
  236. clearTimeout(searchTimeout);
  237. // Reset the index when a new search starts
  238. lastFoundIndex = -1;
  239. }
  240. // --- Optimized Search and Scroll Function ---
  241. function searchAndScroll() {
  242. if (isSearching) return;
  243. isSearching = true;
  244. clearTimeout(searchTimeout);
  245.  
  246. targetText = searchInput.value.trim().toLowerCase();
  247. if (!targetText) {
  248. isSearching = false;
  249. return;
  250. }
  251.  
  252. GM_setValue('lastSearchTerm', targetText);
  253.  
  254. // Get all *visible* media elements
  255. const mediaElements = Array.from(document.querySelectorAll('ytd-rich-grid-media:not([style*="display: none"])'));
  256.  
  257. // Find the next matching element, starting from lastFoundIndex + 1
  258. let nextMatchIndex = -1;
  259. for (let i = lastFoundIndex + 1; i < mediaElements.length; i++) {
  260. const titleElement = mediaElements[i].querySelector('#video-title');
  261. if (titleElement && titleElement.textContent.toLowerCase().includes(targetText)) {
  262. nextMatchIndex = i;
  263. break; // Stop searching once we find the *next* match
  264. }
  265. }
  266.  
  267. // If a match is found, highlight it and scroll to it
  268. if (nextMatchIndex !== -1) {
  269. const matchElement = mediaElements[nextMatchIndex];
  270. matchElement.classList.add('highlighted-text');
  271. matchElement.scrollIntoView({ behavior: 'auto', block: 'center' });
  272. lastFoundIndex = nextMatchIndex;
  273. isSearching = false; // Stop searching after finding a match
  274. } else {
  275. // No match found in the currently visible elements. Scroll down.
  276. lastScrollHeight = document.documentElement.scrollHeight; // Store current height
  277. window.scrollTo({ top: lastScrollHeight, behavior: 'auto' });
  278.  
  279. // Set a timeout to continue searching after a delay (to allow new content to load)
  280. searchTimeout = setTimeout(() => {
  281. if (!isSearching) return; // Check if searching was stopped
  282.  
  283. // Check if we've reached the end of the page
  284. if (document.documentElement.scrollHeight === lastScrollHeight) {
  285. stopSearch();
  286. showNoResultsMessage(); // No more content, and no match found
  287. } else {
  288. isSearching = false; // allow searchAndScroll to run it again
  289. searchAndScroll(); // Continue searching with newly loaded content
  290. }
  291. }, SCROLL_DELAY_MS);
  292. }
  293.  
  294. // Set overall search timeout (for extreme cases)
  295. searchTimeout = setTimeout(() => {
  296. stopSearch();
  297. showErrorMessage("Search timed out.");
  298. }, SEARCH_TIMEOUT_MS);
  299. }
  300.  
  301. // --- Initialization ---
  302. createSearchBox();
  303.  
  304. })();