您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Save YouTube links with persistent storage
// ==UserScript== // @name YouTube Link Saver // @namespace http://tampermonkey.net/ // @version 1.0 // @description Save YouTube links with persistent storage // @author se7 // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @match https://www.youtube.com/* // @match https://youtu.be/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; // CSS styles GM_addStyle(` #yt-link-saver { display: inline-flex; align-items: center; margin-right: 8px; background: transparent; position: relative; } #yt-link-saver > div > button { background: transparent; color: var(--yt-spec-text-primary) !important; border: none; padding: 8px; cursor: pointer; font-weight: 500; transition: background 0.2s ease; font-size: 14px; display: flex; align-items: center; gap: 8px; height: 40px; border-radius: 20px; } #yt-link-saver > div > button svg { fill: currentColor; } #yt-link-saver > div > button:hover { background: var(--yt-spec-badge-chip-background); } #toggle-list { width: 40px !important; padding: 0 !important; justify-content: center; color: var(--yt-spec-text-primary) !important; } .video-count { background: var(--yt-spec-badge-chip-background); color: var(--yt-spec-text-primary); padding: 2px 6px; border-radius: 12px; font-size: 12px; min-width: 16px; height: 16px; display: inline-flex; align-items: center; justify-content: center; } #saved-links { position: absolute; top: 100%; right: 0; background: #282828; border-radius: 12px; padding: 16px; margin-top: 8px; width: 320px; max-height: 500px; overflow-y: auto; display: none; opacity: 0; transition: opacity 0.3s ease; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); z-index: 2200; } #saved-links.visible { display: block; opacity: 1; } #saved-links::-webkit-scrollbar { width: 8px; } #saved-links::-webkit-scrollbar-track { background: #333; border-radius: 4px; } #saved-links::-webkit-scrollbar-thumb { background: #555; border-radius: 4px; } #saved-links::-webkit-scrollbar-thumb:hover { background: #666; } .saved-link { margin: 8px 0; padding: 12px; border-radius: 8px; background: rgba(255, 255, 255, 0.05); transition: background 0.2s ease; position: relative; } .saved-link:hover { background: rgba(255, 255, 255, 0.1); } .saved-link a { color: #fff; text-decoration: none; font-weight: 500; display: block; margin-bottom: 6px; padding-right: 24px; line-height: 1.4; } .saved-link a:hover { color: #3ea6ff; } .delete-btn { position: absolute; top: 12px; right: 12px; color: rgba(255, 255, 255, 0.6); cursor: pointer; transition: color 0.2s ease; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; border-radius: 50%; } .delete-btn:hover { color: #ff4444; background: rgba(255, 255, 255, 0.1); } .saved-link small { color: rgba(255, 255, 255, 0.6); font-size: 12px; } #saved-links h3 { color: #fff; font-size: 16px; margin: 0 0 16px 0; padding-bottom: 12px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .yt-notification { position: fixed; bottom: 20px; right: 20px; padding: 12px 24px; background: #282828; border-radius: 8px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); z-index: 10000; font-family: 'YouTube Sans', 'Roboto', sans-serif; font-size: 14px; display: flex; align-items: center; gap: 8px; animation: slideIn 0.3s ease, fadeOut 0.3s ease 2.7s; pointer-events: none; color: #fff; } .yt-notification.success { border-left: 4px solid #2ba640; } .yt-notification.info { border-left: 4px solid #065fd4; } .yt-notification.error { border-left: 4px solid #ff4444; } @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } /* Saved video title highlighting */ .yt-saved-video { position: relative !important; } /* Remove badge styles and keep only color styles */ /* Watch page title */ ytd-watch-metadata h1.yt-saved-video yt-formatted-string { color: #2ba640 !important; } /* Search/browse page title */ #video-title.yt-saved-video, #video-title-link.yt-saved-video, #video-title-link.yt-saved-video yt-formatted-string, span#video-title.yt-saved-video { color: #2ba640 !important; } /* Playlist panel title */ ytd-playlist-panel-video-renderer span#video-title.yt-saved-video, ytd-playlist-panel-video-renderer h4.yt-saved-video span#video-title { color: #2ba640 !important; } /* New lockup view model title */ .yt-lockup-metadata-view-model-wiz__title.yt-saved-video, .yt-lockup-metadata-view-model-wiz__title.yt-saved-video span.yt-core-attributed-string { color: #2ba640 !important; } /* Ensure proper positioning for different views */ ytd-rich-grid-media, ytd-video-renderer, ytd-grid-video-renderer, ytd-compact-video-renderer h3 { position: relative; } .yt-tooltip { position: absolute; bottom: -30px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.8); color: #fff; padding: 6px 8px; border-radius: 4px; font-size: 12px; white-space: nowrap; opacity: 0; visibility: hidden; transition: opacity 0.2s, visibility 0.2s; pointer-events: none; z-index: 2201; } #yt-link-saver button:hover .yt-tooltip { opacity: 1; visibility: visible; } .yt-context-menu { position: fixed; background: #282828; border-radius: 4px; padding: 8px 0; min-width: 180px; box-shadow: 0 2px 10px rgba(0,0,0,0.3); z-index: 9999; opacity: 0; transform: scale(0.95); transition: opacity 0.1s, transform 0.1s; } .yt-context-menu.visible { opacity: 1; transform: scale(1); } .yt-context-menu-item { padding: 8px 16px; color: #fff; cursor: pointer; display: flex; align-items: center; gap: 12px; font-family: 'YouTube Sans', 'Roboto', sans-serif; font-size: 14px; } .yt-context-menu-item:hover { background: rgba(255, 255, 255, 0.1); } .yt-context-menu-item svg { width: 20px; height: 20px; fill: currentColor; } /* Search box styles */ .saved-links-header { margin-bottom: 16px; } .saved-links-title { color: #fff; font-size: 16px; margin: 0 0 12px 0; padding-bottom: 12px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .saved-links-actions { display: flex; gap: 8px; align-items: center; justify-content: flex-end; } .action-btn { background: transparent; border: none; cursor: pointer; padding: 6px 12px; border-radius: 18px; font-size: 13px; transition: all 0.2s ease; display: flex; align-items: center; gap: 6px; height: 32px; font-family: 'YouTube Sans', 'Roboto', sans-serif; color: #fff; } .action-btn svg { width: 18px; height: 18px; fill: currentColor; } .action-btn.primary { color: #3ea6ff !important; } .action-btn.primary:hover { background: rgba(62, 166, 255, 0.1) !important; } .action-btn.danger { color: #ff4444 !important; } .action-btn.danger:hover { background: rgba(255, 68, 68, 0.1) !important; } /* Remove old button styles */ .delete-all-btn, .export-btn, .import-btn { display: none; } .search-container { margin: 0 0 16px 0; position: relative; width: 100%; box-sizing: border-box; } .search-input { width: 100%; background: rgba(255, 255, 255, 0.1); border: none; border-radius: 4px; padding: 8px 32px 8px 12px; color: #fff; font-size: 14px; box-sizing: border-box; } .search-input:focus { outline: none; background: rgba(255, 255, 255, 0.15); } .search-input::placeholder { color: rgba(255, 255, 255, 0.5); } .clear-search { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); color: rgba(255, 255, 255, 0.5); cursor: pointer; padding: 4px; border-radius: 50%; display: none; } .clear-search:hover { color: #fff; background: rgba(255, 255, 255, 0.1); } .search-input:not(:placeholder-shown) + .clear-search { display: block; } .no-results { text-align: center; color: rgba(255, 255, 255, 0.5); padding: 20px 0; font-size: 14px; } /* Confirmation dialog styles */ .yt-confirm-dialog { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #282828; border-radius: 8px; padding: 24px; width: 300px; z-index: 10001; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); } .yt-confirm-dialog h4 { margin: 0 0 16px 0; color: #fff; font-size: 16px; } .yt-confirm-dialog p { margin: 0 0 20px 0; color: rgba(255, 255, 255, 0.8); font-size: 14px; line-height: 1.4; } .dialog-buttons { display: flex; justify-content: flex-end; gap: 8px; } .dialog-btn { padding: 8px 16px; border-radius: 4px; border: none; cursor: pointer; font-size: 14px; transition: all 0.2s ease; } .dialog-btn.cancel { background: transparent; color: #fff; } .dialog-btn.cancel:hover { background: rgba(255, 255, 255, 0.1); } .dialog-btn.confirm { background: #ff4444; color: #fff; } .dialog-btn.confirm:hover { background: #ff6666; } .dialog-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 10000; } /* Selection mode styles */ body.yt-selecting-video * { cursor: pointer !important; } body.yt-selecting-video ytd-rich-item-renderer:hover, body.yt-selecting-video ytd-video-renderer:hover, body.yt-selecting-video ytd-grid-video-renderer:hover, body.yt-selecting-video ytd-compact-video-renderer:hover { outline: 2px solid #2ba640 !important; outline-offset: 4px !important; } #save-button.selecting { background: rgba(43, 166, 64, 0.2) !important; color: #2ba640 !important; } `); // Storage functions function getSavedLinks() { const links = localStorage.getItem('ytSavedLinks'); return links ? JSON.parse(links) : []; } function isLinkSaved(url) { const links = getSavedLinks(); const videoId = getVideoIdFromUrl(url); return links.some(link => getVideoIdFromUrl(link.url) === videoId); } function showNotification(message, type = 'success') { const notification = document.createElement('div'); notification.className = `yt-notification ${type}`; const icon = document.createElement('span'); icon.textContent = type === 'success' ? '✓' : type === 'info' ? 'ℹ' : '⚠'; icon.style.fontWeight = 'bold'; const text = document.createElement('span'); text.textContent = message; notification.appendChild(icon); notification.appendChild(text); document.body.appendChild(notification); // Remove notification after animation setTimeout(() => { notification.remove(); }, 3000); } function getVideoIdFromUrl(url) { try { const urlObj = new URL(url); // Handle youtube.com/watch?v= URLs if (urlObj.hostname.includes('youtube.com') && urlObj.pathname === '/watch') { return urlObj.searchParams.get('v'); } // Handle youtu.be/VIDEO_ID URLs if (urlObj.hostname === 'youtu.be') { return urlObj.pathname.slice(1); // Remove leading slash } // Handle other potential YouTube URL formats if (urlObj.hostname.includes('youtube.com')) { const videoId = urlObj.searchParams.get('v'); if (videoId) return videoId; // Try getting from pathname for embed URLs const pathParts = urlObj.pathname.split('/'); if (pathParts.includes('embed') || pathParts.includes('v')) { return pathParts[pathParts.length - 1]; } } return null; } catch (e) { console.error('Error parsing URL:', e); return null; } } function checkAndHighlightTitles() { const savedLinks = getSavedLinks(); const savedVideoIds = savedLinks.map(link => getVideoIdFromUrl(link.url)); // Check watch page title const watchTitle = document.querySelector('ytd-watch-metadata h1'); if (watchTitle) { const currentVideoId = getVideoIdFromUrl(window.location.href); if (savedVideoIds.includes(currentVideoId)) { watchTitle.classList.add('yt-saved-video'); } else { watchTitle.classList.remove('yt-saved-video'); } } // Check all possible video title elements const titleSelectors = [ 'a#video-title', // Search/browse view 'a#video-title-link', // Grid view 'h3 a#video-title-link', // Alternative grid view 'ytd-rich-grid-media #video-title-link', // Home page grid 'span#video-title', // Compact view 'ytd-playlist-panel-video-renderer span#video-title', // Playlist panel '.yt-lockup-metadata-view-model-wiz__title' // New lockup view ]; titleSelectors.forEach(selector => { const videoTitles = document.querySelectorAll(selector); videoTitles.forEach(title => { let videoId; let parentH3; if (title.closest('ytd-playlist-panel-video-renderer')) { // For playlist panel, get video ID from parent link const parentLink = title.closest('a#wc-endpoint'); if (parentLink && parentLink.href) { videoId = getVideoIdFromUrl(parentLink.href); } parentH3 = title.closest('h4'); } else if (title.tagName.toLowerCase() === 'span') { // For compact view, get video ID from parent renderer parentH3 = title.closest('h3'); const renderer = title.closest('ytd-compact-video-renderer'); if (renderer) { const data = renderer.data || {}; videoId = data.videoId; if (!videoId) { const navigationEndpoint = renderer.data?.navigationEndpoint?.watchEndpoint; videoId = navigationEndpoint?.videoId; if (!videoId) { // Try getting from parent link const parentLink = title.closest('a'); if (parentLink && parentLink.href) { videoId = getVideoIdFromUrl(parentLink.href); } } } } } else { videoId = getVideoIdFromUrl(title.href); } if (savedVideoIds.includes(videoId)) { // Add yt-saved-video class to parent h3/h4 and title span if (parentH3) { parentH3.classList.add('yt-saved-video'); title.classList.add('yt-saved-video'); // Add color to span without the badge class title.style.color = '#2ba640'; } else { title.classList.add('yt-saved-video'); } // Also highlight the formatted string inside if it exists const formattedString = title.querySelector('yt-formatted-string'); if (formattedString) { formattedString.style.color = '#2ba640'; } } else { if (parentH3) { parentH3.classList.remove('yt-saved-video'); title.classList.remove('yt-saved-video'); title.style.color = ''; } else { title.classList.remove('yt-saved-video'); } const formattedString = title.querySelector('yt-formatted-string'); if (formattedString) { formattedString.style.color = ''; } } }); }); } // Create mutation observer to watch for new video elements const observer = new MutationObserver((mutations) => { checkAndHighlightTitles(); }); // Start observing observer.observe(document.body, { childList: true, subtree: true }); // Modify saveLink function to validate video ID function saveLink(url, title) { const videoId = getVideoIdFromUrl(url); if (!videoId) { showNotification('Cannot save - not a YouTube video page', 'error'); return false; } const links = getSavedLinks(); const existingIndex = links.findIndex(link => getVideoIdFromUrl(link.url) === videoId); // If video exists, remove it if (existingIndex !== -1) { links.splice(existingIndex, 1); localStorage.setItem('ytSavedLinks', JSON.stringify(links)); updateLinksList(); updateVideoCount(); showNotification('Video removed from saved list'); checkAndHighlightTitles(); return true; } // If video doesn't exist, add it links.push({ url, title, date: new Date().toISOString() }); localStorage.setItem('ytSavedLinks', JSON.stringify(links)); updateLinksList(); updateVideoCount(); showNotification('Video saved successfully'); checkAndHighlightTitles(); return true; } // Add delete function function deleteLink(index) { const links = getSavedLinks(); links.splice(index, 1); localStorage.setItem('ytSavedLinks', JSON.stringify(links)); updateLinksList(); updateVideoCount(); showNotification('Video deleted successfully'); checkAndHighlightTitles(); } // Modify handleSave function to handle toggle function handleSave(urlOrEvent = null, title = null) { // If first argument is an event or null, get URL from current page let url; if (!urlOrEvent || urlOrEvent instanceof Event) { url = window.location.href; } else { url = urlOrEvent; } // If no title provided, get from current page if (!title) { title = document.title.replace(' - YouTube', ''); } saveLink(url, title); } // Add keyboard shortcut handler document.addEventListener('keydown', (e) => { // Check if typing in an input/textarea if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { return; } // Check for backslash key if (e.key === '\\') { e.preventDefault(); handleSave(); } // Check for underscore key to toggle selection mode if (e.key === '_') { e.preventDefault(); if (isSelectingVideo) { exitVideoSelectionMode(); showNotification('Video selection cancelled', 'info'); } else { enterVideoSelectionMode(); } } }); // Add selection mode state let isSelectingVideo = false; // Function to enter video selection mode function enterVideoSelectionMode() { isSelectingVideo = true; document.body.classList.add('yt-selecting-video'); saveButton.classList.add('selecting'); showNotification('Click any video to save it', 'info'); } // Function to exit video selection mode function exitVideoSelectionMode() { isSelectingVideo = false; document.body.classList.remove('yt-selecting-video'); saveButton.classList.remove('selecting'); } // Handle video click in selection mode function handleVideoClick(e) { if (!isSelectingVideo) return; // Find the video element and get its URL and title const videoElement = e.target.closest('ytd-rich-item-renderer, ytd-video-renderer, ytd-grid-video-renderer, ytd-compact-video-renderer, ytd-playlist-panel-video-renderer, yt-lockup-view-model'); if (!videoElement) return; // Always prevent default behavior in selection mode e.preventDefault(); e.stopPropagation(); // Try multiple methods to find the video link and title let url, title; // Method 1: Try new lockup view model if (videoElement.tagName.toLowerCase() === 'yt-lockup-view-model') { const titleLink = videoElement.querySelector('.yt-lockup-metadata-view-model-wiz__title'); const titleSpan = titleLink?.querySelector('.yt-core-attributed-string'); if (titleLink && titleSpan) { title = titleSpan.textContent.trim(); // Handle both full URLs and relative URLs url = titleLink.href.startsWith('http') ? titleLink.href : 'https://www.youtube.com' + titleLink.href; } } // Method 2: Try playlist panel renderer else if (videoElement.tagName.toLowerCase() === 'ytd-playlist-panel-video-renderer') { const titleSpan = videoElement.querySelector('span#video-title'); const linkElement = videoElement.querySelector('a#wc-endpoint'); if (titleSpan && linkElement) { title = titleSpan.textContent.trim(); // Handle both full URLs and relative URLs url = linkElement.href.startsWith('http') ? linkElement.href : 'https://www.youtube.com' + linkElement.href; } } // Method 3: Try compact video renderer else if (videoElement.tagName.toLowerCase() === 'ytd-compact-video-renderer') { const titleSpan = videoElement.querySelector('span#video-title'); const linkElement = videoElement.querySelector('a.yt-simple-endpoint[href*="/watch"]'); if (titleSpan && linkElement) { title = titleSpan.textContent.trim(); // Handle both full URLs and relative URLs url = linkElement.href.startsWith('http') ? linkElement.href : 'https://www.youtube.com' + linkElement.href; } } // Method 4: Standard video title link (fallback) if (!url || !title) { const titleLink = videoElement.querySelector('a#video-title, a#video-title-link'); if (titleLink) { url = titleLink.href; title = titleLink.textContent.trim(); } } // Method 5: Try formatted string inside link (fallback) if (!title && url) { const formattedString = videoElement.querySelector('yt-formatted-string'); if (formattedString) { title = formattedString.textContent.trim(); } } // Method 6: Try metadata (final fallback) if (!url || !title) { const metadata = videoElement.data; if (metadata) { if (!url && metadata.navigationEndpoint?.commandMetadata?.webCommandMetadata?.url) { url = 'https://www.youtube.com' + metadata.navigationEndpoint.commandMetadata.webCommandMetadata.url; } if (!title && metadata.title?.runs?.[0]?.text) { title = metadata.title.runs[0].text; } } } // If we found both URL and title, save the video if (url && title) { handleSave(url, title); return false; } else { showNotification('Could not extract video data', 'error'); } } // Single click handler for selection mode document.addEventListener('click', (e) => { if (isSelectingVideo) { handleVideoClick(e); } }, true); // Create UI const container = document.createElement('div'); container.id = 'yt-link-saver'; container.className = 'collapsed'; const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.alignItems = 'center'; buttonContainer.style.gap = '8px'; const saveButton = document.createElement('button'); saveButton.id = 'save-button'; saveButton.setAttribute('aria-label', 'Save Video'); // Create bookmark icon using YouTube's style const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('height', '24'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('width', '24'); svg.style.fill = 'currentColor'; const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', 'M22 13h-4v4h-2v-4h-4v-2h4V7h2v4h4v2zm-8-6H2v1h12V7zM2 12h8v-1H2v1zm0 4h8v-1H2v1z'); svg.appendChild(path); saveButton.appendChild(svg); // Create tooltip for save button const saveTooltip = document.createElement('div'); saveTooltip.className = 'yt-tooltip'; saveTooltip.textContent = 'Save current video (Right click to select any video)'; saveButton.appendChild(saveTooltip); // Replace clipboard handler with selection mode saveButton.addEventListener('contextmenu', (e) => { e.preventDefault(); if (isSelectingVideo) { exitVideoSelectionMode(); showNotification('Video selection cancelled', 'info'); } else { enterVideoSelectionMode(); } }); // Add click handler to exit selection mode if clicking save button again saveButton.addEventListener('click', (e) => { if (isSelectingVideo) { e.preventDefault(); exitVideoSelectionMode(); showNotification('Video selection cancelled', 'info'); } else { handleSave(); } }); const toggleButton = document.createElement('button'); toggleButton.id = 'toggle-list'; toggleButton.setAttribute('aria-label', 'Saved Videos'); // Create list icon using YouTube's style const listSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); listSvg.setAttribute('height', '24'); listSvg.setAttribute('viewBox', '0 0 24 24'); listSvg.setAttribute('width', '24'); listSvg.style.fill = 'currentColor'; const listPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); listPath.setAttribute('d', 'M4 10h12v2H4zm0-4h12v2H4zm0 8h8v2H4zm10 0h8v2h-8zm-10 4h8v2H4zm10 0h8v2h-8z'); listSvg.appendChild(listPath); toggleButton.appendChild(listSvg); // Create tooltip for toggle button const toggleTooltip = document.createElement('div'); toggleTooltip.className = 'yt-tooltip'; toggleTooltip.textContent = 'Saved Videos'; toggleButton.appendChild(toggleTooltip); const videoCount = document.createElement('span'); videoCount.className = 'video-count'; const linksList = document.createElement('div'); linksList.id = 'saved-links'; buttonContainer.appendChild(saveButton); buttonContainer.appendChild(toggleButton); buttonContainer.appendChild(videoCount); container.appendChild(buttonContainer); container.appendChild(linksList); // Wait for DOM to be ready function initializeUI() { const masthead = document.querySelector('#end.style-scope.ytd-masthead'); if (masthead) { // Insert before the notification button const notificationBtn = masthead.querySelector('ytd-notification-topbar-button-renderer'); if (notificationBtn) { masthead.insertBefore(container, notificationBtn); } else { masthead.appendChild(container); } // Initialize video count updateVideoCount(); // Initial highlight check checkAndHighlightTitles(); } else { setTimeout(initializeUI, 100); } } // Start initialization initializeUI(); // Event listeners toggleButton.addEventListener('click', toggleSavedList); linksList.addEventListener('click', (e) => { if (e.target.classList.contains('delete-btn')) { e.stopPropagation(); // Stop event from bubbling up to document const index = parseInt(e.target.dataset.index); deleteLink(index); } }); // Update video count function updateVideoCount() { const count = getSavedLinks().length; videoCount.textContent = count; } // Toggle saved videos list function toggleSavedList() { linksList.classList.toggle('visible'); container.classList.toggle('collapsed'); if (linksList.classList.contains('visible')) { updateLinksList(); updateVideoCount(); checkAndHighlightTitles(); } } // Export saved videos to JSON function exportSavedVideos() { const links = getSavedLinks(); const jsonStr = JSON.stringify(links, null, 2); const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'youtube-saved-videos.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showNotification('Videos exported successfully'); } // Import saved videos from JSON function importSavedVideos(file) { const reader = new FileReader(); reader.onload = function(e) { try { const importedData = JSON.parse(e.target.result); // Validate data structure if (!Array.isArray(importedData)) { throw new Error('Invalid file format: Data must be an array'); } // Validate each entry importedData.forEach(item => { if (!item.url || !item.title || !item.date) { throw new Error('Invalid data structure: Each entry must have url, title, and date'); } }); // Get current links and merge with imported ones const currentLinks = getSavedLinks(); let newCount = 0; importedData.forEach(item => { const videoId = getVideoIdFromUrl(item.url); if (videoId && !currentLinks.some(link => getVideoIdFromUrl(link.url) === videoId)) { currentLinks.push(item); newCount++; } }); // Save merged links localStorage.setItem('ytSavedLinks', JSON.stringify(currentLinks)); updateLinksList(); updateVideoCount(); checkAndHighlightTitles(); showNotification(`Successfully imported ${newCount} new videos`); } catch (error) { showNotification('Error importing file: ' + error.message, 'error'); } }; reader.onerror = function() { showNotification('Error reading file', 'error'); }; reader.readAsText(file); } // Update links list with search functionality function updateLinksList(searchQuery = '') { const links = getSavedLinks(); // Only create header and search if they don't exist if (!linksList.querySelector('.saved-links-header')) { // Add title const title = document.createElement('h3'); title.className = 'saved-links-title'; title.textContent = 'Saved Videos'; linksList.appendChild(title); // Add header with action buttons const headerContainer = document.createElement('div'); headerContainer.className = 'saved-links-header'; const actionsContainer = document.createElement('div'); actionsContainer.className = 'saved-links-actions'; // Export button with icon const exportBtn = document.createElement('button'); exportBtn.className = 'action-btn primary'; const exportIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); exportIcon.setAttribute('viewBox', '0 0 24 24'); const exportPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); exportPath.setAttribute('d', 'M5 4v2h14V4H5zm0 10h4v6h6v-6h4l-7-7-7 7z'); exportIcon.appendChild(exportPath); const exportText = document.createElement('span'); exportText.textContent = 'Export'; exportBtn.appendChild(exportIcon); exportBtn.appendChild(exportText); exportBtn.addEventListener('click', (e) => { e.stopPropagation(); exportSavedVideos(); }); // Import button with icon const importBtn = document.createElement('button'); importBtn.className = 'action-btn primary'; const importIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); importIcon.setAttribute('viewBox', '0 0 24 24'); const importPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); importPath.setAttribute('d', 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z'); importIcon.appendChild(importPath); const importText = document.createElement('span'); importText.textContent = 'Import'; importBtn.appendChild(importIcon); importBtn.appendChild(importText); // Create hidden file input const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.json'; fileInput.style.display = 'none'; fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { importSavedVideos(e.target.files[0]); } e.target.value = ''; }); document.body.appendChild(fileInput); importBtn.addEventListener('click', (e) => { e.stopPropagation(); fileInput.click(); }); // Delete All button with icon const deleteAllBtn = document.createElement('button'); deleteAllBtn.className = 'action-btn danger'; const deleteIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); deleteIcon.setAttribute('viewBox', '0 0 24 24'); const deletePath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); deletePath.setAttribute('d', 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z'); deleteIcon.appendChild(deletePath); const deleteText = document.createElement('span'); deleteText.textContent = 'Delete All'; deleteAllBtn.appendChild(deleteIcon); deleteAllBtn.appendChild(deleteText); deleteAllBtn.addEventListener('click', showDeleteAllConfirmation); actionsContainer.appendChild(exportBtn); actionsContainer.appendChild(importBtn); actionsContainer.appendChild(deleteAllBtn); headerContainer.appendChild(actionsContainer); linksList.appendChild(headerContainer); // Add search box const searchContainer = document.createElement('div'); searchContainer.className = 'search-container'; const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.className = 'search-input'; searchInput.placeholder = 'Search saved videos...'; const clearSearch = document.createElement('span'); clearSearch.className = 'clear-search'; clearSearch.textContent = '✕'; clearSearch.addEventListener('click', () => { searchInput.value = ''; updateLinksContent(''); }); searchInput.addEventListener('input', (e) => { updateLinksContent(e.target.value); }); searchContainer.appendChild(searchInput); searchContainer.appendChild(clearSearch); linksList.appendChild(searchContainer); } // Update search input value if provided const searchInput = linksList.querySelector('.search-input'); if (searchQuery && searchInput.value !== searchQuery) { searchInput.value = searchQuery; } updateLinksContent(searchInput.value); } // Separate function to update only the links content function updateLinksContent(searchQuery) { const links = getSavedLinks(); // Remove only the links, keeping header and search const existingLinks = linksList.querySelectorAll('.saved-link, .no-results'); existingLinks.forEach(link => link.remove()); // Filter links based on search query const filteredLinks = searchQuery ? links.filter(link => link.title.toLowerCase().includes(searchQuery.toLowerCase())) : links; if (filteredLinks.length === 0) { const noResults = document.createElement('div'); noResults.className = 'no-results'; noResults.textContent = searchQuery ? 'No videos match your search' : 'No saved videos yet'; linksList.appendChild(noResults); return; } filteredLinks.forEach((link, index) => { const linkElement = document.createElement('div'); linkElement.className = 'saved-link'; const anchor = document.createElement('a'); anchor.href = link.url; anchor.target = '_blank'; anchor.textContent = link.title; const deleteBtn = document.createElement('span'); deleteBtn.className = 'delete-btn'; deleteBtn.textContent = '✕'; deleteBtn.dataset.index = links.indexOf(link); // Use original index const dateElement = document.createElement('small'); const date = new Date(link.date).toLocaleDateString(); dateElement.textContent = date; linkElement.appendChild(deleteBtn); linkElement.appendChild(anchor); linkElement.appendChild(dateElement); linksList.appendChild(linkElement); }); } // Delete all confirmation function showDeleteAllConfirmation() { const overlay = document.createElement('div'); overlay.className = 'dialog-overlay'; const dialog = document.createElement('div'); dialog.className = 'yt-confirm-dialog'; const title = document.createElement('h4'); title.textContent = 'Delete All Videos?'; const message = document.createElement('p'); message.textContent = 'Are you sure you want to delete all saved videos? This action cannot be undone.'; const buttons = document.createElement('div'); buttons.className = 'dialog-buttons'; const cancelBtn = document.createElement('button'); cancelBtn.className = 'dialog-btn cancel'; cancelBtn.textContent = 'Cancel'; cancelBtn.onclick = () => { document.body.removeChild(overlay); document.body.removeChild(dialog); }; const confirmBtn = document.createElement('button'); confirmBtn.className = 'dialog-btn confirm'; confirmBtn.textContent = 'Delete All'; confirmBtn.onclick = () => { showFinalConfirmation(); document.body.removeChild(overlay); document.body.removeChild(dialog); }; buttons.appendChild(cancelBtn); buttons.appendChild(confirmBtn); dialog.appendChild(title); dialog.appendChild(message); dialog.appendChild(buttons); document.body.appendChild(overlay); document.body.appendChild(dialog); } function showFinalConfirmation() { const overlay = document.createElement('div'); overlay.className = 'dialog-overlay'; const dialog = document.createElement('div'); dialog.className = 'yt-confirm-dialog'; const title = document.createElement('h4'); title.textContent = 'Final Confirmation'; const message = document.createElement('p'); message.textContent = 'Are you absolutely sure? All your saved videos will be permanently deleted.'; const buttons = document.createElement('div'); buttons.className = 'dialog-buttons'; const cancelBtn = document.createElement('button'); cancelBtn.className = 'dialog-btn cancel'; cancelBtn.textContent = 'Cancel'; cancelBtn.onclick = () => { document.body.removeChild(overlay); document.body.removeChild(dialog); }; const confirmBtn = document.createElement('button'); confirmBtn.className = 'dialog-btn confirm'; confirmBtn.textContent = 'Yes, Delete All'; confirmBtn.onclick = () => { localStorage.setItem('ytSavedLinks', '[]'); updateLinksList(); updateVideoCount(); showNotification('All videos have been deleted'); checkAndHighlightTitles(); document.body.removeChild(overlay); document.body.removeChild(dialog); }; buttons.appendChild(cancelBtn); buttons.appendChild(confirmBtn); dialog.appendChild(title); dialog.appendChild(message); dialog.appendChild(buttons); document.body.appendChild(overlay); document.body.appendChild(dialog); } // Add click outside handler document.addEventListener('click', (e) => { if (linksList.classList.contains('visible') && !container.contains(e.target) && !e.target.closest('.yt-confirm-dialog')) { linksList.classList.remove('visible'); container.classList.add('collapsed'); } }); // Initial highlight check //setTimeout(checkAndHighlightTitles, 1000); // Periodic check for new videos (every 2 seconds) // setInterval(checkAndHighlightTitles, 2000); })();