// ==UserScript==
// @name 1-click feed maintenance
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Mark a video in homepage feed as already watched, not interested, don't recommend in 1 click instead of 5
// @author lwkjef
// @match https://www.youtube.com/
// @icon https://www.google.com/s2/favicons?domain=youtube.com
// @grant none
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @license MIT
// ==/UserScript==
/* eslint-env jquery */
// jQuery selectors for Youtube elements
const contentsGridSelector = '#contents.ytd-rich-grid-renderer';
const videoSelector = 'ytd-rich-item-renderer';
const menuButtonSelector = '#menu button';
const popupContainerSelector = 'ytd-popup-container'; // parent of ironDropdownSelector and paperDialogSelector
const ironDropdownSelector = 'tp-yt-iron-dropdown'
const paperDialogSelector = 'tp-yt-paper-dialog';
const notInterestedSelector = 'tp-yt-paper-item:contains("Not interested")';
const dontRecommendSelector = 'tp-yt-paper-item:contains("Don\'t recommend channel")';
const tellUsWhySelector = 'ytd-button-renderer:contains("Tell us why")';
const iDontLikeTheVideoSelector = 'tp-yt-paper-checkbox:contains("I don\'t like the video")';
const iveAlreadyWatchedTheVideoSelector = 'tp-yt-paper-checkbox:contains("I\'ve already watched the video")';
const submitSelector = 'tp-yt-paper-button:contains("Submit")';
// internal constants
const oneClickButtonId = 'one-click-feed-button';
const alreadyWatchedButtonText = 'already watched';
const iDontLikeTheVideoButtonText = 'block video';
const dontRecommendText = 'block channel';
(function(){
'use strict'
$(document).ready(function(){
let observer = new MutationObserver(function contentsMutated(mutationsList, observer) {
for (let mutation of mutationsList) {
for (let addedNode of mutation.addedNodes) {
addButtons(addedNode);
}
}
});
$(contentsGridSelector).each(function() {
// hook up observer first, so we don't miss any new video nodes
observer.observe(this, {attributes: false, childList: true, subtree: false});
// immediately backfill all existing video nodes
addButtons(this);
});
});
}())
/*
* add 1-click buttons to all video nodes contained in the given node that don't already have buttons
*/
function addButtons(node) {
$(node).find(videoSelector).not(`:has(#${oneClickButtonId})`).each(function() {
this.appendChild(createButton(alreadyWatchedButtonText, alreadyWatchedOnClick));
this.appendChild(createButton(iDontLikeTheVideoButtonText, iDontLikeTheVideoOnClick));
this.appendChild(createButton(dontRecommendText, dontRecommendOnClick));
});
}
/*
* create and return a new 1-click button with the given text and onclick callback
*/
function createButton(text, onclick, cssObj) {
let button = document.createElement('button');
button.setAttribute ('id', oneClickButtonId);
button.setAttribute ('type', 'button');
button.innerHTML = text;
button.onclick = onclick;
//let btnStyle = button.style;
//cssObj = cssObj || {position: 'absolute', bottom: '7%', left:'4%', 'z-index': 3};
//Object.keys(cssObj).forEach(key => {btnStyle[key] = cssObj[key]});
return button;
}
/*
* "already watched" 1-click button callback
*/
async function alreadyWatchedOnClick(e) {
e.preventDefault();
let videoNode = e.target.parentNode;
let ironDropdownNode = await openMenu(videoNode);
await clickNotInterested(ironDropdownNode);
let paperDialogNode = await clickTellUsWhy(videoNode);
await clickAlreadyWatched(paperDialogNode);
videoNode.style.visibility = 'hidden';
}
/*
* "I dont like the video" 1-click button callback
*/
async function iDontLikeTheVideoOnClick(e) {
e.preventDefault();
let videoNode = e.target.parentNode;
let ironDropdownNode = await openMenu(videoNode);
await clickNotInterested(ironDropdownNode);
let paperDialogNode = await clickTellUsWhy(videoNode);
await clickIDontLikeTheVideo(paperDialogNode);
videoNode.style.visibility = 'hidden';
}
/*
* "dont recommend" 1-click button callback
*/
async function dontRecommendOnClick(e) {
e.preventDefault();
let videoNode = e.target.parentNode;
let ironDropdownNode = await openMenu(videoNode);
await clickDontRecommend(ironDropdownNode);
videoNode.style.visibility = 'hidden';
}
/*
* click menu button on the given video node, wait for iron dropdown to be added, and return iron dropdown node.
*/
async function openMenu(videoNode) {
return await doAndAwaitNodeAdded(ironDropdownSelector, popupContainerSelector, function() {
$(videoNode).find(menuButtonSelector).trigger('click');
});
}
/*
* click not interested button on the given iron dropdown node, and wait for the iron dropdown to be removed.
*/
async function clickNotInterested(ironDropdownNode) {
return await doAndAwaitNodeRemoved(ironDropdownSelector, popupContainerSelector, function() {
$(ironDropdownNode).find(notInterestedSelector).trigger('click');
});
}
/*
* click dont recommend channel button on the given popup node, and wait for the popup to be removed.
*/
async function clickDontRecommend(ironDropdownNode) {
return await doAndAwaitNodeRemoved(ironDropdownSelector, popupContainerSelector, function() {
$(ironDropdownNode).find(dontRecommendSelector).trigger('click');
});
}
/*
* click tell us why button on the given video node, wait for popup to be added, and return popup node.
*/
async function clickTellUsWhy(videoNode) {
return await doAndAwaitNodeAdded(paperDialogSelector, popupContainerSelector, function() {
$(videoNode).find(tellUsWhySelector).trigger('click');
});
}
/*
* click I dont like the video button on the given popup node, click submit button on the given popup node, and wait for the popup to be removed.
*/
async function clickIDontLikeTheVideo(paperDialogNode) {
return await doAndAwaitNodeRemoved(paperDialogSelector, popupContainerSelector, function() {
$(paperDialogNode).find(iDontLikeTheVideoSelector).trigger('click');
$(paperDialogNode).find(submitSelector).trigger('click');
});
}
/*
* click Ive already watched the video button on the given popup node, click submit button on the given popup node, and wait for the popup to be removed.
*/
async function clickAlreadyWatched(paperDialogNode) {
return await doAndAwaitNodeRemoved(paperDialogSelector, popupContainerSelector, function() {
$(paperDialogNode).find(iveAlreadyWatchedTheVideoSelector).trigger('click');
$(paperDialogNode).find(submitSelector).trigger('click');
});
}
/*
* Execute the given function, then wait until node is added. ancestorSelector MUST exist and should be as close as possible to target node for efficiency.
*/
async function doAndAwaitNodeAdded(nodeName, ancestorSelector, f) {
let node;
let observer;
await new Promise(resolve => {
observer = new MutationObserver(function(mutationsList, observer) {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
for (let addedNode of mutation.addedNodes) {
if (equalsIgnoreCase(addedNode.nodeName, nodeName) && addedNode.style.display !== 'none') {
// entire dropdown node added
node = addedNode;
resolve(addedNode);
}
}
} else if (mutation.type === 'attributes') {
if (equalsIgnoreCase(mutation.target.nodeName, nodeName) && mutation.target.style.display !== 'none') {
// dropdown added by unsetting display:none
node = mutation.target;
resolve(mutation.target);
}
}
}
});
$(ancestorSelector).each(function() {
observer.observe(this, {attributes: true, childList: true, subtree: true});
});
f();
});
observer.disconnect();
return node;
}
/*
* Execute the given function, then wait until given node is removed. ancestorSelector MUST exist and should be as close as possible to target node for efficiency.
*/
async function doAndAwaitNodeRemoved(nodeName, ancestorSelector, f) {
let observer;
await new Promise(resolve => {
observer = new MutationObserver(function(mutationsList, observer) {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
for (let removedNode of mutation.removedNodes) {
if (equalsIgnoreCase(removedNode.nodeName, nodeName)) {
// entire node removed
resolve(removedNode);
}
}
} else if (mutation.type === 'attributes') {
if (equalsIgnoreCase(mutation.target.nodeName, nodeName) && mutation.target.style.display === 'none') {
// removed by setting display:none
resolve(mutation.target);
}
}
}
});
$(ancestorSelector).each(function() {
observer.observe(this, {attributes: true, childList: true, subtree: true});
});
f();
});
observer.disconnect();
}
/*
* True if strings lowercased are equivalent, false otherwise.
*/
function equalsIgnoreCase(one, two) {
return one.toLowerCase() === two.toLowerCase();
}