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

Automatically scrolls to a specified text within a YouTube grid. Search box in masthead, search on button click only, stop button, animated border highlighting, and no results message. Improved robustness, accessibility, and user experience.

当前为 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.3
  8. // @description Automatically scrolls to a specified text within a YouTube grid. Search box in masthead, search on button click only, stop button, animated border highlighting, and no results message. Improved robustness, accessibility, and user experience.
  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 observer;
  24. let searchTimeout;
  25. const SEARCH_TIMEOUT_MS = 20000; // 20 seconds
  26. const SCROLL_DELAY_MS = 750;
  27. const MAX_SEARCH_LENGTH = 255; // Prevent excessively long search strings
  28.  
  29. GM_addStyle(`
  30. /* Existing CSS (with additions) */
  31. #floating-search-box {
  32. background-color: #222;
  33. padding: 5px;
  34. border: 1px solid #444;
  35. border-radius: 5px;
  36. display: flex;
  37. align-items: center;
  38. margin-left: 10px;
  39. }
  40. /* Responsive width for smaller screens */
  41. @media (max-width: 768px) {
  42. #floating-search-box input[type="text"] {
  43. width: 150px; /* Smaller width on smaller screens */
  44. }
  45. }
  46.  
  47. #floating-search-box input[type="text"] {
  48. background-color: #333;
  49. color: #fff;
  50. border: 1px solid #555;
  51. padding: 3px 5px;
  52. border-radius: 3px;
  53. margin-right: 5px;
  54. width: 200px;
  55. height: 30px;
  56. }
  57. #floating-search-box input[type="text"]:focus {
  58. outline: none;
  59. border-color: #065fd4;
  60. }
  61. #floating-search-box button {
  62. background-color: #065fd4;
  63. color: white;
  64. border: none;
  65. padding: 3px 8px;
  66. border-radius: 3px;
  67. cursor: pointer;
  68. height: 30px;
  69. }
  70. #floating-search-box button:hover {
  71. background-color: #0549a8;
  72. }
  73. #floating-search-box button:focus {
  74. outline: none;
  75. }
  76.  
  77. #stop-search-button {
  78. background-color: #aa0000; /* Red color */
  79. }
  80. #stop-search-button:hover {
  81. background-color: #800000;
  82. }
  83.  
  84. .highlighted-text {
  85. position: relative; /* Needed for the border to be positioned correctly */
  86. z-index: 1; /* Ensure the border is on top of other elements */
  87. }
  88.  
  89. /* Creates the animated border effect */
  90. .highlighted-text::before {
  91. content: '';
  92. position: absolute;
  93. top: -2px;
  94. left: -2px;
  95. right: -2px;
  96. bottom: -2px;
  97. border: 2px solid transparent;
  98. border-radius: 8px;
  99. background: linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet);
  100. background-size: 400% 400%;
  101. animation: gradientAnimation 5s ease infinite;
  102. z-index: -1;
  103. }
  104. /* Keyframes for the gradient animation */
  105. @keyframes gradientAnimation {
  106. 0% {
  107. background-position: 0% 50%;
  108. }
  109. 50% {
  110. background-position: 100% 50%;
  111. }
  112. 100% {
  113. background-position: 0% 50%;
  114. }
  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; /* Fixed position */
  123. top: 50px; /* Position below the masthead (adjust as needed)*/
  124. left: 50%;
  125. transform: translateX(-50%); /* Center horizontally */
  126. background-color: rgba(0, 0, 0, 0.8); /* Semi-transparent black */
  127. color: white;
  128. border-radius: 5px;
  129. z-index: 10000; /* Ensure it's on top */
  130. display: none; /* Initially hidden */
  131. }
  132.  
  133. /* Style for the no results message */
  134. #search-no-results-message {
  135. color: #aaa; /* Light gray */
  136. padding: 5px;
  137. position: fixed;
  138. top: 50px; /* Same position as error message */
  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; /* Initially hidden */
  145. }
  146.  
  147. `);
  148.  
  149.  
  150. // --- Create the Search Box ---
  151. function createSearchBox() {
  152. searchBox = document.createElement('div');
  153. searchBox.id = 'floating-search-box';
  154. searchBox.setAttribute('role', 'search'); // Add ARIA role for accessibility
  155.  
  156. searchInput = document.createElement('input');
  157. searchInput.type = 'text';
  158. searchInput.placeholder = 'Search to scroll...';
  159. searchInput.value = GM_getValue('lastSearchTerm', '');
  160. searchInput.setAttribute('aria-label', 'Search within YouTube grid'); // ARIA label
  161. searchInput.maxLength = MAX_SEARCH_LENGTH; // Limit input length
  162.  
  163. searchButton = document.createElement('button');
  164. searchButton.textContent = 'Search';
  165. searchButton.addEventListener('click', searchAndScroll);
  166. searchButton.setAttribute('aria-label', 'Start search'); // ARIA label
  167.  
  168. stopButton = document.createElement('button');
  169. stopButton.textContent = 'Stop';
  170. stopButton.id = 'stop-search-button';
  171. stopButton.addEventListener('click', stopSearch);
  172. stopButton.setAttribute('aria-label', 'Stop search'); // ARIA label
  173.  
  174. searchBox.appendChild(searchInput);
  175. searchBox.appendChild(searchButton);
  176. searchBox.appendChild(stopButton);
  177.  
  178. const mastheadEnd = document.querySelector('#end.ytd-masthead');
  179. const buttonsContainer = document.querySelector('#end #buttons');
  180.  
  181. if (mastheadEnd) {
  182. if(buttonsContainer){
  183. mastheadEnd.insertBefore(searchBox, buttonsContainer);
  184. } else{
  185. mastheadEnd.appendChild(searchBox);
  186. }
  187. } else {
  188. console.error("Could not find the YouTube masthead's end element.");
  189. showErrorMessage("Could not find the YouTube masthead. Search box placed at top of page.");
  190. document.body.insertBefore(searchBox, document.body.firstChild);
  191. }
  192.  
  193. // Trigger search on load if text is present and the URL indicates a channel page
  194. if (searchInput.value.trim() !== "" && window.location.href.includes("/videos")) {
  195. searchAndScroll();
  196. }
  197. }
  198.  
  199.  
  200. // --- Show Error Message ---
  201. function showErrorMessage(message) {
  202. let errorDiv = document.getElementById('search-error-message');
  203. if (!errorDiv) {
  204. errorDiv = document.createElement('div');
  205. errorDiv.id = 'search-error-message';
  206. document.body.appendChild(errorDiv);
  207. }
  208. errorDiv.textContent = message;
  209. errorDiv.style.display = 'block';
  210.  
  211. setTimeout(() => {
  212. errorDiv.style.display = 'none';
  213. }, 5000); // Hide after 5 seconds
  214. }
  215. // --- Show "No Results" Message ---
  216. function showNoResultsMessage() {
  217. let noResultsDiv = document.getElementById('search-no-results-message');
  218. if (!noResultsDiv) {
  219. noResultsDiv = document.createElement('div');
  220. noResultsDiv.id = 'search-no-results-message';
  221. noResultsDiv.textContent = "No matching results found."; // Set the text
  222. document.body.appendChild(noResultsDiv);
  223. }
  224. noResultsDiv.style.display = 'block';
  225. // Hide the message after a few seconds
  226. setTimeout(() => {
  227. noResultsDiv.style.display = 'none';
  228. }, 5000);
  229.  
  230. }
  231.  
  232. // --- Stop Search Function ---
  233. function stopSearch() {
  234. if (observer) {
  235. observer.disconnect();
  236. }
  237. isSearching = false;
  238. clearTimeout(searchTimeout);
  239. const prevHighlighted = document.querySelector('.highlighted-text');
  240. if (prevHighlighted) {
  241. prevHighlighted.classList.remove('highlighted-text');
  242. }
  243. }
  244.  
  245.  
  246. // --- Optimized Search and Scroll Function ---
  247. function searchAndScroll() {
  248. if (isSearching) return;
  249. isSearching = true;
  250. clearTimeout(searchTimeout);
  251.  
  252. if (observer) {
  253. observer.disconnect();
  254. }
  255.  
  256. targetText = searchInput.value.trim().toLowerCase();
  257. if (!targetText) {
  258. isSearching = false;
  259. return;
  260. }
  261.  
  262. GM_setValue('lastSearchTerm', targetText);
  263. // Remove previous highlights
  264. const prevHighlighted = document.querySelector('.highlighted-text');
  265. if (prevHighlighted) {
  266. prevHighlighted.classList.remove('highlighted-text');
  267. prevHighlighted.style.position = ''; //remove inline styles, if any
  268. }
  269.  
  270. let foundMatch = false; // Keep track of whether a match was found
  271.  
  272. observer = new IntersectionObserver((entries) => {
  273. for (const entry of entries) {
  274. if (entry.isIntersecting) {
  275. const titleElement = entry.target.querySelector('#video-title'); // More reliable selector
  276. if (titleElement && titleElement.textContent.toLowerCase().includes(targetText)) {
  277. foundMatch = true;
  278. entry.target.scrollIntoView({ behavior: 'auto', block: 'center' });
  279. entry.target.classList.add('highlighted-text');
  280. observer.disconnect();
  281. isSearching = false;
  282. clearTimeout(searchTimeout);
  283. return;
  284. }
  285. }
  286. }
  287. });
  288.  
  289. //Observe all grid items.
  290. document.querySelectorAll('ytd-rich-grid-media').forEach(item => {
  291. observer.observe(item);
  292. });
  293.  
  294. searchTimeout = setTimeout(() => {
  295. stopSearch();
  296. if (!foundMatch) {
  297. showNoResultsMessage(); //show no results message.
  298. }
  299. }, SEARCH_TIMEOUT_MS);
  300.  
  301. setTimeout(() => {
  302. if (!document.querySelector('.highlighted-text')) {
  303. window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'auto' });
  304. setTimeout(() => {
  305. if (!isSearching) return; // Check again in case stop was pressed
  306. isSearching = false;
  307. searchAndScroll(); // Recursive call
  308. }, SCROLL_DELAY_MS);
  309. } else {
  310. isSearching = false;
  311. }
  312. }, 100);
  313. }
  314.  
  315. // --- Initialization ---
  316. createSearchBox();
  317.  
  318. })();