Adds quick Watch Later button with customizable playlist
// ==UserScript==
// @name YouTube Quick Watch Later (10/25 UI)
// @namespace http://tampermonkey.net/
// @version 4.0
// @description Adds quick Watch Later button with customizable playlist
// @author kavinned
// @match https://www.youtube.com/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @icon https://www.google.com/s2/favicons?sz=64&domain=YouTube.com
// @license MIT
// ==/UserScript==
(function () {
"use strict";
const SAVE_BUTTON_TEXTS = [
"Save", "儲存", "保存", "Guardar", "Sauvegarder",
"Speichern", "Salvar", "Сохранить", "保存", "저장",
"บันทึก", "Simpan", "Lưu"
];
function getTargetPlaylist() {
return GM_getValue("targetPlaylist", "Watch later");
}
function setTargetPlaylist(playlistName) {
GM_setValue("targetPlaylist", playlistName);
console.log(`✅ Target playlist set to: ${playlistName}`);
}
function waitForPlaylists(callback, maxAttempts = 25, interval = 400) {
let attempts = 0;
const checkInterval = setInterval(() => {
const playlists = document.querySelectorAll("toggleable-list-item-view-model");
if (playlists.length > 0) {
clearInterval(checkInterval);
console.log(`✅ Found ${playlists.length} playlists after ${attempts + 1} attempts`);
callback(playlists);
} else if (attempts >= maxAttempts) {
clearInterval(checkInterval);
console.log(`❌ Failed to find playlists after ${maxAttempts} attempts`);
callback(null);
}
attempts++;
if (attempts > 0 && attempts % 5 === 0) {
console.log(`⏳ Fetching Playlists Attempt: ${attempts}`);
}
}, interval);
}
function isVideoInPlaylist(playlistElement) {
const listItem = playlistElement.querySelector("yt-list-item-view-model");
if (!listItem) return false;
return listItem.getAttribute("aria-pressed") === "true";
}
function getPlaylistName(playlistElement) {
const nameElement = playlistElement.querySelector(".yt-list-item-view-model__text-wrapper span");
return nameElement ? nameElement.textContent.trim() : null;
}
function getClickableElement(playlistElement) {
return playlistElement.querySelector("yt-list-item-view-model");
}
function createPlaylistSelectorModal() {
const modal = document.createElement("div");
modal.id = "playlist-selector-modal";
const overlay = document.createElement("div");
overlay.className = "playlist-modal-overlay";
const content = document.createElement("div");
content.className = "playlist-modal-content";
const header = document.createElement("div");
header.className = "playlist-modal-header";
const title = document.createElement("h3");
title.textContent = "Select Target Playlist";
const closeBtn = document.createElement("button");
closeBtn.className = "playlist-modal-close";
closeBtn.textContent = "×";
header.appendChild(title);
header.appendChild(closeBtn);
const body = document.createElement("div");
body.className = "playlist-modal-body";
const loading = document.createElement("p");
loading.className = "playlist-loading";
loading.textContent = "Loading playlists...";
body.appendChild(loading);
content.appendChild(header);
content.appendChild(body);
overlay.appendChild(content);
modal.appendChild(overlay);
document.body.appendChild(modal);
return modal;
}
function showPlaylistSelector() {
const currentPlaylist = getTargetPlaylist();
const modal = createPlaylistSelectorModal();
const modalBody = modal.querySelector(".playlist-modal-body");
const closeBtn = modal.querySelector(".playlist-modal-close");
const overlay = modal.querySelector(".playlist-modal-overlay");
function closeModal() {
modal.remove();
closeDialog();
}
closeBtn.addEventListener("click", closeModal);
overlay.addEventListener("click", function(e) {
if (e.target === overlay) closeModal();
});
const directSaveButton = Array.from(
document.querySelectorAll("ytd-menu-renderer yt-button-view-model .yt-spec-button-shape-next__button-text-content, ytd-button-renderer button")
).find(element => containsSaveText(element.textContent));
if (directSaveButton) {
console.log("🎯 Direct save button found (new UI)");
directSaveButton.click();
} else {
console.log("🔍 Looking for save button in menu...");
const menuButton = document.querySelector("#button-shape > button, ytd-menu-renderer button[aria-label*='More']");
if (menuButton) {
menuButton.click();
setTimeout(function() {
const saveButtons = document.querySelectorAll("#items > ytd-menu-service-item-renderer > tp-yt-paper-item > yt-formatted-string");
const saveButton = Array.from(saveButtons).find((button) => containsSaveText(button.textContent));
if (saveButton) saveButton.click();
}, 150);
}
}
waitForPlaylists(function(playlists) {
if (!playlists || playlists.length === 0) {
modalBody.textContent = "";
const error = document.createElement("p");
error.className = "playlist-error";
error.textContent = "Could not load playlists. Please try again.";
modalBody.appendChild(error);
return;
}
modalBody.textContent = "";
playlists.forEach((playlist) => {
const playlistName = getPlaylistName(playlist);
if (playlistName) {
const playlistItem = document.createElement("div");
playlistItem.className = "playlist-item";
if (playlistName === currentPlaylist) {
playlistItem.classList.add("playlist-item-active");
}
const nameSpan = document.createElement("span");
nameSpan.className = "playlist-name";
nameSpan.textContent = playlistName;
playlistItem.appendChild(nameSpan);
if (playlistName === currentPlaylist) {
const badge = document.createElement("span");
badge.className = "playlist-badge";
badge.textContent = "Current";
playlistItem.appendChild(badge);
}
playlistItem.addEventListener("click", function() {
setTargetPlaylist(playlistName);
closeModal();
const notification = document.createElement("div");
notification.className = "playlist-notification";
notification.textContent = `Target playlist set to: ${playlistName}`;
document.body.appendChild(notification);
setTimeout(() => notification.classList.add("show"), 10);
setTimeout(() => {
notification.classList.remove("show");
setTimeout(() => notification.remove(), 300);
}, 2000);
});
modalBody.appendChild(playlistItem);
}
});
});
}
GM_registerMenuCommand("Change Target Playlist", showPlaylistSelector);
function closeDialog() {
document.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Escape',
code: 'Escape',
keyCode: 27,
which: 27,
bubbles: true
}));
}
function containsSaveText(text) {
return SAVE_BUTTON_TEXTS.some(saveText => text.includes(saveText));
}
function findPlaylistByName(playlistName, callback, maxAttempts = 25, interval = 400) {
let attempts = 0;
const checkInterval = setInterval(() => {
const playlists = document.querySelectorAll("toggleable-list-item-view-model");
for (let i = 0; i < playlists.length; i++) {
const name = getPlaylistName(playlists[i]);
if (name === playlistName) {
clearInterval(checkInterval);
console.log(`✅ Found playlist: ${playlistName}`);
callback(playlists[i]);
return;
}
}
if (attempts >= maxAttempts) {
clearInterval(checkInterval);
console.log(`❌ Playlist "${playlistName}" not found after ${maxAttempts} attempts`);
callback(null);
}
attempts++;
}, interval);
}
function handlePlaylistSelection() {
const targetPlaylistName = getTargetPlaylist();
findPlaylistByName(targetPlaylistName, function(targetPlaylist) {
if (!targetPlaylist) {
const useWatchLater = confirm(
`Playlist "${targetPlaylistName}" not found.\n\nDo you want to save to "Watch later" instead?`
);
if (useWatchLater) {
setTargetPlaylist("Watch later");
findPlaylistByName("Watch later", function(watchLaterPlaylist) {
if (!watchLaterPlaylist) {
const firstPlaylist = document.querySelector("toggleable-list-item-view-model");
if (firstPlaylist) {
handlePlaylistClick(firstPlaylist);
} else {
alert("Could not find any playlists.");
closeDialog();
}
} else {
handlePlaylistClick(watchLaterPlaylist);
}
});
} else {
const newPlaylistName = prompt("Enter the exact playlist name you want to save to:");
if (newPlaylistName && newPlaylistName.trim() !== "") {
const trimmedName = newPlaylistName.trim();
setTargetPlaylist(trimmedName);
findPlaylistByName(trimmedName, function(newPlaylist) {
if (!newPlaylist) {
alert(`Playlist "${trimmedName}" still not found. Please check the spelling and try again.`);
closeDialog();
} else {
handlePlaylistClick(newPlaylist);
}
});
} else {
closeDialog();
}
}
} else {
handlePlaylistClick(targetPlaylist);
}
});
}
function handlePlaylistClick(playlistElement) {
const isInPlaylist = isVideoInPlaylist(playlistElement);
const playlistName = getPlaylistName(playlistElement);
const clickableElement = getClickableElement(playlistElement);
if (!clickableElement) {
console.error("❌ Could not find clickable element for playlist:", playlistName);
alert("Error: Could not interact with playlist. Please try again.");
closeDialog();
return;
}
if (isInPlaylist) {
const confirmRemove = confirm(
`This video is already in your "${playlistName}" playlist. Do you want to remove it?`
);
if (confirmRemove) {
console.log(`🗑️ Removing from playlist: ${playlistName}`);
clickableElement.click();
setTimeout(closeDialog, 100);
} else {
closeDialog();
}
} else {
console.log(`➕ Adding to playlist: ${playlistName}`);
clickableElement.click();
setTimeout(closeDialog, 100);
}
}
function addWatchLaterButton() {
const targetDiv = document.querySelector(
"#top-level-buttons-computed > segmented-like-dislike-button-view-model > yt-smartimation > div"
);
if (!targetDiv || document.querySelector(".quick-watch-later")) return;
const button = document.createElement("button");
button.className = "quick-watch-later";
button.textContent = "WL";
button.title = "Quick Watch Later (Right-click to change playlist)";
button.addEventListener("click", function () {
const directSaveButtonWithText = Array.from(
document.querySelectorAll("ytd-menu-renderer yt-button-view-model .yt-spec-button-shape-next__button-text-content, ytd-button-renderer button")
).find(element => containsSaveText(element.textContent));
if (directSaveButtonWithText) {
console.log("🎯 Direct save button found, clicking it");
directSaveButtonWithText.click();
setTimeout(handlePlaylistSelection, 600);
} else {
console.log("🔍 Save button not directly visible, opening menu");
const menuButton = document.querySelector("#button-shape > button, ytd-menu-renderer button[aria-label*='More']");
if (menuButton) {
menuButton.click();
console.log("📂 Menu clicked");
}
setTimeout(function () {
const saveButtons = document.querySelectorAll("#items > ytd-menu-service-item-renderer > tp-yt-paper-item > yt-formatted-string");
const saveButton = Array.from(saveButtons).find((button) => containsSaveText(button.textContent));
if (saveButton) {
saveButton.click();
setTimeout(handlePlaylistSelection, 600);
}
}, 150);
}
});
button.addEventListener("contextmenu", function(e) {
e.preventDefault();
e.stopPropagation();
showPlaylistSelector();
});
targetDiv.appendChild(button);
console.log("✅ WL button added");
}
setTimeout(addWatchLaterButton, 2500);
const observer = new MutationObserver(() => {
if (window.location.href.includes("/watch?")) {
setTimeout(addWatchLaterButton, 1200);
}
});
observer.observe(document.body, { childList: true, subtree: true });
GM_addStyle(`
#top-level-buttons-computed > segmented-like-dislike-button-view-model > yt-smartimation > div {
display: flex;
flex-direction: row-reverse;
gap: 5px;
}
#top-level-buttons-computed > segmented-like-dislike-button-view-model > yt-smartimation > div > button {
flex-direction: row;
border-radius: 24px;
border: none;
padding-left: 20px;
padding-right: 20px;
color: white;
font-weight: bold;
background: #272727;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background: #414141;
}
}
.ryd-tooltip.ryd-tooltip-new-design {
height: 0px !important;
width: 0px !important;
}
@media only screen and (max-width: 1200px) {
#top-level-buttons-computed > segmented-like-dislike-button-view-model > yt-smartimation > div {
flex-direction: column;
}
#top-level-buttons-computed > segmented-like-dislike-button-view-model > yt-smartimation > div > button {
padding: 10.5px 0px;
}
}
#playlist-selector-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10000;
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.playlist-modal-overlay {
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.75);
display: flex;
justify-content: center;
align-items: center;
animation: fadeIn 0.15s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.playlist-modal-content {
background: rgba(33, 33, 33, 0.98);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
width: 90%;
max-width: 500px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
animation: slideUp 0.2s ease;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.playlist-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.playlist-modal-header h3 {
margin: 0;
color: #fff;
font-size: 18px;
font-weight: 500;
}
.playlist-modal-close {
background: rgba(255, 255, 255, 0.1);
border: none;
color: #aaa;
font-size: 28px;
line-height: 1;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
transition: all 0.15s ease;
}
.playlist-modal-close:hover {
background: rgba(255, 255, 255, 0.15);
color: #fff;
}
.playlist-modal-body {
padding: 12px;
overflow-y: auto;
max-height: calc(80vh - 80px);
}
.playlist-modal-body::-webkit-scrollbar {
width: 8px;
}
.playlist-modal-body::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
}
.playlist-modal-body::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
.playlist-modal-body::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
.playlist-loading,
.playlist-error {
display: grid;
place-items: center;
padding: 80px 20px;
min-height: 175px;
text-align: center;
color: #aaa;
font-size: 32px;
font-weight: 700;
}
.playlist-error {
color: #f44336;
}
.playlist-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
margin: 4px 0;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
cursor: pointer;
transition: all 0.15s ease;
border: 1px solid transparent;
}
.playlist-item:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
}
.playlist-item-active {
background: rgba(62, 166, 255, 0.15);
border-color: rgba(62, 166, 255, 0.4);
}
.playlist-item-active:hover {
background: rgba(62, 166, 255, 0.2);
border-color: rgba(62, 166, 255, 0.5);
}
.playlist-name {
color: #fff;
font-size: 14px;
font-weight: 400;
}
.playlist-badge {
background: #3ea6ff;
color: #000;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.playlist-notification {
position: fixed;
bottom: -100px;
left: 50%;
transform: translateX(-50%);
background: rgba(33, 33, 33, 0.95);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
color: #fff;
padding: 16px 24px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 10001;
transition: bottom 0.2s ease;
font-size: 14px;
}
.playlist-notification.show {
bottom: 24px;
}
`);
})();