您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a search bar to YouTube playlists. Search by title or channel name (using @). Does NOT work with shorts or when playlist video filter is set to "Shorts".
// ==UserScript== // @name YouTube Playlist Search Bar (Inchbrayock's Branch) // @namespace http://tampermonkey.net/ // @version 1.1 // @description Adds a search bar to YouTube playlists. Search by title or channel name (using @). Does NOT work with shorts or when playlist video filter is set to "Shorts". // @match https://www.youtube.com/playlist* // @author Setnour6 // @author Inchbrayock // @homepageURL https://github.com/Setnour6/Helpful-Userscripts // @grant none // @license GPL-3.0 // ==/UserScript== (function () { 'use strict'; // Array of placeholder texts that will be animated in the search input const PLACEHOLDERS = [ 'Search in playlist...', 'Type @ to search by channel...', 'Find your favorite videos...', 'Search through your collection...', 'Looking for something specific?', 'Filter playlist content...' ]; // CSS styles for the search bar components const STYLES = { container: ` display: flex; align-items: center; margin: 24px auto 20px; padding: 0 24px; width: 100%; max-width: 800px; box-sizing: border-box; transition: all 0.3s ease; `, searchContainer: ` display: flex; align-items: center; width: 100%; background-color: var(--yt-spec-badge-chip-background); border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); transition: all 0.3s ease; border: 1px solid rgba(255, 255, 255, 0.1); `, input: ` flex: 1; padding: 14px 20px; border: none; border-radius: 12px; color: var(--yt-spec-text-primary); background-color: transparent; font-family: 'YouTube Sans', Roboto, sans-serif; font-size: 15px; height: 48px; box-sizing: border-box; outline: none; transition: all 0.3s ease; ` }; // Global variables to track state let originalVideos = []; // Stores the original list of videos before filtering let isTyping = true; // Controls typing animation direction (typing vs deleting) let isAnimating = false; // Prevents multiple animations from running simultaneously let currentPlaceholderIndex = 0; // Tracks which placeholder text is currently being shown // Handles the animated typing effect for placeholder text async function animatePlaceholder(input) { if (isAnimating) return; isAnimating = true; while (document.getElementById('playlist-search-input') && !input.value && !input.matches(':focus')) { const placeholder = PLACEHOLDERS[currentPlaceholderIndex]; if (isTyping) { // Typing effect input.placeholder = ''; // Clear before starting for (let i = 0; i <= placeholder.length; i++) { if (!document.getElementById('playlist-search-input') || input.value || input.matches(':focus')) { isAnimating = false; return; } input.placeholder = placeholder.slice(0, i); await new Promise(resolve => setTimeout(resolve, 50)); } await new Promise(resolve => setTimeout(resolve, 2000)); isTyping = false; } else { // Deleting effect for (let i = placeholder.length; i >= 0; i--) { if (!document.getElementById('playlist-search-input') || input.value || input.matches(':focus')) { isAnimating = false; return; } input.placeholder = placeholder.slice(0, i); await new Promise(resolve => setTimeout(resolve, 30)); } currentPlaceholderIndex = (currentPlaceholderIndex + 1) % PLACEHOLDERS.length; isTyping = true; await new Promise(resolve => setTimeout(resolve, 500)); } } isAnimating = false; } // Handles visual changes when the search input is focused function handleFocus(searchContainer, input) { searchContainer.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.12)'; searchContainer.style.transform = 'translateY(-1px)'; searchContainer.style.borderColor = 'rgba(255, 255, 255, 0.2)'; input.placeholder = ''; } // Handles visual changes when the search input loses focus function handleBlur(searchContainer, input) { searchContainer.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.08)'; searchContainer.style.transform = 'none'; searchContainer.style.borderColor = 'rgba(255, 255, 255, 0.1)'; // Only restart animation if the input is empty if (!input.value) { // Reset all animation states isAnimating = false; isTyping = true; currentPlaceholderIndex = 0; input.placeholder = ''; setTimeout(() => { if (!input.value && !input.matches(':focus')) { animatePlaceholder(input); } }, 500); // Delay restart of animation } } // Filters the playlist videos based on search query function filterVideos(query) { const playlistContents = document.querySelector('#contents.ytd-playlist-video-list-renderer'); if (!playlistContents) return; // Store original videos on first search if (originalVideos.length === 0) { originalVideos = Array.from(document.querySelectorAll('#contents ytd-playlist-video-renderer')); } // Check if searching by channel name (using @) or video title const isChannelSearch = query.startsWith('@'); const searchQuery = isChannelSearch ? query.slice(1).toLowerCase() : query.toLowerCase(); // Filter videos based on search criteria const matchingVideos = originalVideos.filter(video => { const title = video.querySelector('#video-title').textContent.toLowerCase(); const channelName = video.querySelector('#channel-name a').textContent.toLowerCase(); return isChannelSearch ? channelName.includes(searchQuery) : title.includes(searchQuery); }); updatePlaylistContents(playlistContents, matchingVideos); } // Resets the playlist to show all videos function resetFilter() { const playlistContents = document.querySelector('#contents.ytd-playlist-video-list-renderer'); if (!playlistContents) return; updatePlaylistContents(playlistContents, originalVideos); } // Updates the playlist container with filtered videos function updatePlaylistContents(container, videos) { // Clear the container first container.textContent = ''; // Process videos in batches const BATCH_SIZE = 10; let currentIndex = 0; function processBatch() { const batch = videos.slice(currentIndex, currentIndex + BATCH_SIZE); if (batch.length === 0) return; requestAnimationFrame(() => { batch.forEach(video => container.appendChild(video)); currentIndex += BATCH_SIZE; // Schedule next batch if (currentIndex < videos.length) { setTimeout(processBatch, 16); // Roughly aims for 60fps } }); } processBatch(); } /** * Scrolls the page to load all videos in the playlist. * Keeps scrolling until the number of loaded <ytd-playlist-video-renderer> nodes * stops increasing for two consecutive passes. */ async function loadAllVideos() { const selector = '#contents ytd-playlist-video-renderer'; let prevCount = 0; let stablePasses = 0; // keep scrolling until count stabilizes twice while (stablePasses < 2) { window.scrollTo(0, document.documentElement.scrollHeight); // allow YouTube's infinite‑scroll to fetch more items await new Promise(res => setTimeout(res, 1000)); const currentCount = document.querySelectorAll(selector).length; if (currentCount === prevCount) { stablePasses++; } else { stablePasses = 0; prevCount = currentCount; } } } // Creates and injects the search bar into the YouTube playlist page function createSearchBar() { // Prevent duplicate search bars if (document.getElementById('playlist-search-bar')) return; const target = document.querySelector('#page-manager ytd-playlist-video-list-renderer'); if (!target) return; const container = document.createElement('div'); container.id = 'playlist-search-bar'; container.style.cssText = STYLES.container; const searchContainer = document.createElement('div'); searchContainer.style.cssText = STYLES.searchContainer; const input = document.createElement('input'); input.id = 'playlist-search-input'; input.placeholder = ''; input.style.cssText = STYLES.input; input.addEventListener('focus', () => handleFocus(searchContainer, input)); input.addEventListener('blur', () => handleBlur(searchContainer, input)); input.addEventListener('input', (e) => { const query = e.target.value.trim(); query ? filterVideos(query) : resetFilter(); }); // New: on Enter, first load all videos, then redo the filter input.addEventListener('keydown', async (e) => { if (e.key === 'Enter') { e.preventDefault(); const query = input.value.trim(); // clear any previous cache so we recapture everything originalVideos = []; // load all the videos in the playlist await loadAllVideos(); // rebuild originalVideos from the fully‑loaded list originalVideos = Array.from(document.querySelectorAll('#contents ytd-playlist-video-renderer')); // reapply the current filter (or reset if empty) query ? filterVideos(query) : resetFilter(); } }); searchContainer.appendChild(input); container.appendChild(searchContainer); target.parentNode.insertBefore(container, target); requestAnimationFrame(() => animatePlaceholder(input)); } // Initialize the script // Use MutationObserver to handle YouTube's dynamic page loading const observer = new MutationObserver(createSearchBar); observer.observe(document.body, { childList: true, subtree: true }); createSearchBar(); })();