// ==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);
})();