// ==UserScript==
// @name YouTube Quick Watch Later
// @namespace http://tampermonkey.net/
// @version 3.2
// @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";
// Configuration: Add "Save" translations here
const SAVE_BUTTON_TEXTS = [
"Save", // English
"儲存", // Traditional Chinese
"保存", // Simplified Chinese
"Guardar", // Spanish
"Sauvegarder", // French
"Speichern", // German
"Salvar", // Portuguese
"Сохранить", // Russian
"保存", // Japanese
"저장", // Korean
"บันทึก", // Thai
"Simpan", // Indonesian
"Lưu" // Vietnamese
];
// Get stored playlist name, default to "Watch later"
function getTargetPlaylist() {
return GM_getValue("targetPlaylist", "Watch later");
}
// Set target playlist
function setTargetPlaylist(playlistName) {
GM_setValue("targetPlaylist", playlistName);
console.log(`Target playlist set to: ${playlistName}`);
}
// Polling function to wait for playlists to load
function waitForPlaylists(callback, maxAttempts = 20, interval = 1000) {
let attempts = 0;
const checkInterval = setInterval(() => {
const playlists = document.querySelectorAll("#playlists > ytd-playlist-add-to-option-renderer");
if (playlists.length > 0) {
clearInterval(checkInterval);
callback(playlists);
} else if (attempts >= maxAttempts) {
clearInterval(checkInterval);
callback(null);
}
attempts++;
attempts > 0 && console.log(`Fetching Playlists Attempt: ${attempts}`)
}, interval);
}
// Create custom playlist selector modal
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;
}
// Show playlist selector
function showPlaylistSelector() {
const currentPlaylist = getTargetPlaylist();
// Create modal
const modal = createPlaylistSelectorModal();
const modalBody = modal.querySelector(".playlist-modal-body");
const closeBtn = modal.querySelector(".playlist-modal-close");
const overlay = modal.querySelector(".playlist-modal-overlay");
// Close modal function
function closeModal() {
modal.remove();
closeDialog();
}
// Close on X button or overlay click
closeBtn.addEventListener("click", closeModal);
overlay.addEventListener("click", function(e) {
if (e.target === overlay) {
closeModal();
}
});
// Open YouTube's save dialog to get playlists
const directSaveButton = Array.from(
document.querySelectorAll("ytd-menu-renderer yt-button-view-model .yt-spec-button-shape-next__button-text-content")
).find(element => containsSaveText(element.textContent));
if (directSaveButton) {
directSaveButton.click();
} else {
const menuButton = document.querySelector("#button-shape > button > yt-touch-feedback-shape > div");
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();
}
}, 100);
}
}
// Use polling to wait for playlists to load
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 playlistTitle = playlist.querySelector("#label");
if (playlistTitle) {
const playlistName = playlistTitle.textContent.trim();
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();
// Show confirmation
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);
}
});
});
}
// Register menu command to change playlist
GM_registerMenuCommand("Change Target Playlist", showPlaylistSelector);
function closeDialog() {
document.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Escape',
code: 'Escape',
keyCode: 27,
which: 27,
bubbles: true
}));
}
// Helper function to check if text contains any of the save button texts
function containsSaveText(text) {
return SAVE_BUTTON_TEXTS.some(saveText => text.includes(saveText));
}
// Find playlist by name with polling
function findPlaylistByName(playlistName, callback, maxAttempts = 20, interval = 300) {
let attempts = 0;
const checkInterval = setInterval(() => {
const playlists = document.querySelectorAll("#playlists > ytd-playlist-add-to-option-renderer");
for (let i = 0; i < playlists.length; i++) {
const playlistTitle = playlists[i].querySelector("#label");
if (playlistTitle && playlistTitle.textContent.trim() === playlistName) {
clearInterval(checkInterval);
callback(playlists[i].querySelector("#checkbox"));
return;
}
}
if (attempts >= maxAttempts) {
clearInterval(checkInterval);
callback(null);
}
attempts++;
}, interval);
}
// Handle playlist selection with fallback
function handlePlaylistSelection() {
const targetPlaylistName = getTargetPlaylist();
findPlaylistByName(targetPlaylistName, function(targetCheckbox) {
// If playlist not found, handle fallback
if (!targetCheckbox) {
const useWatchLater = confirm(
`Playlist "${targetPlaylistName}" not found.\n\nDo you want to save to "Watch later" instead?`
);
if (useWatchLater) {
// Update stored preference to Watch later
setTargetPlaylist("Watch later");
// Try to find Watch later playlist
findPlaylistByName("Watch later", function(watchLaterCheckbox) {
if (!watchLaterCheckbox) {
// If Watch later also not found, use first playlist
const firstPlaylist = document.querySelector("#playlists > ytd-playlist-add-to-option-renderer:first-child #checkbox");
if (firstPlaylist) {
handleCheckboxClick(firstPlaylist);
} else {
alert("Could not find any playlists.");
closeDialog();
}
} else {
handleCheckboxClick(watchLaterCheckbox);
}
});
} else {
// Let user input new playlist name
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(newCheckbox) {
if (!newCheckbox) {
alert(`Playlist "${trimmedName}" still not found. Please check the spelling and try again.`);
closeDialog();
} else {
handleCheckboxClick(newCheckbox);
}
});
} else {
closeDialog();
}
}
} else {
handleCheckboxClick(targetCheckbox);
}
});
}
// Helper function to handle checkbox click
function handleCheckboxClick(checkbox) {
// Check if video is already in playlist
if (checkbox && checkbox.getAttribute("aria-checked") === "true") {
const confirmRemove = confirm(
`This video is already in your "${getTargetPlaylist()}" playlist. Do you want to remove it?`
);
if (confirmRemove) {
checkbox.click();
closeDialog();
} else {
closeDialog();
}
} else if (checkbox) {
// Add to playlist
checkbox.click();
closeDialog();
}
}
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";
// Left click handler
button.addEventListener("click", function () {
// First check if Save button is directly visible
const directSaveButton = document.querySelector(
"ytd-menu-renderer yt-button-view-model .yt-spec-button-shape-next__button-text-content"
);
const directSaveButtonWithText = Array.from(
document.querySelectorAll("ytd-menu-renderer yt-button-view-model .yt-spec-button-shape-next__button-text-content")
).find(element => containsSaveText(element.textContent));
if (directSaveButtonWithText) {
// Save button is directly visible, click it
console.log("Direct save button found, clicking it");
directSaveButtonWithText.click();
// Then proceed to handle playlist selection
setTimeout(handlePlaylistSelection, 500);
} else {
// Save button is in submenu, use original logic
console.log("Save button not directly visible, opening menu");
// First click menu button
const menuButton = document.querySelector(
"#button-shape > button > yt-touch-feedback-shape > div"
);
if (menuButton) {
menuButton.click();
console.log("menu clicked");
}
// Next click the Save button
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();
// Then handle playlist selection
setTimeout(handlePlaylistSelection, 500);
}
}, 100);
}
});
// Right click handler for playlist change
button.addEventListener("contextmenu", function(e) {
e.preventDefault();
e.stopPropagation();
showPlaylistSelector();
});
targetDiv.appendChild(button);
}
setTimeout(addWatchLaterButton, 2000);
const observer = new MutationObserver(() => {
if (window.location.href.includes("/watch?")) {
setTimeout(addWatchLaterButton, 1000);
}
});
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;
&: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;
}
}
/* Subtle Modal Styles */
#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 {
text-align: center;
padding: 40px 20px;
color: #aaa;
}
.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;
}
/* Notification Styles */
.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;
}
.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;
}
}`
);
})();