Greasy Fork is available in English.

YT: not interested in one click

Hover a thumbnail on youtube.com and click an icon at the right: "Not interested" and "Don't recommend channel"

  1. // ==UserScript==
  2. // @name YT: not interested in one click
  3. // @description Hover a thumbnail on youtube.com and click an icon at the right: "Not interested" and "Don't recommend channel"
  4. // @version 1.3.1
  5. //
  6. // @match https://www.youtube.com/*
  7. //
  8. // @noframes
  9. // @grant none
  10. //
  11. // @author wOxxOm
  12. // @namespace wOxxOm.scripts
  13. // @license MIT License
  14. // ==/UserScript==
  15. 'use strict';
  16.  
  17. const THUMB2 = 'yt-thumbnail-view-model';
  18. const {
  19. THUMB = THUMB2 + ',ytd-thumbnail,ytd-playlist-thumbnail',
  20. PREVIEW_TAG = 'ytd-video-preview',
  21. PREVIEW_PARENT = '#media-container', // parent for the added buttons; must be visible
  22. ENTRY = [
  23. 'ytd-rich-item-renderer', // home
  24. 'ytd-compact-video-renderer', // watch (recommendations)
  25. 'ytd-playlist-video-renderer', // watch later, likes
  26. 'ytd-playlist-panel-video-renderer', // playlist
  27. 'ytd-video-renderer', // history
  28. ].join(','),
  29. MENU1 = 'ytd-menu-popup-renderer',
  30. MENU2 = 'yt-sheet-view-model',
  31. MENU_BTN = '.dropdown-trigger, .yt-lockup-metadata-view-model-wiz__menu-button button',
  32. } = getStorage() || {};
  33. const ME = 'yt-one-click-dismiss';
  34. const COMMANDS = {
  35. NOT_INTERESTED: 'video',
  36. REMOVE: 'channel',
  37. DELETE: 'unwatch',
  38. };
  39. let STYLE;
  40. let inlinable;
  41.  
  42. addEventListener('click', onClick, true);
  43. addEventListener('mousedown', onClick, true);
  44. addEventListener('mouseover', onHover, true);
  45. addEventListener('yt-action', ({detail: d}) => {
  46. if (d.actionName === 'yt-set-cookie-command') {
  47. inlinable = !d.args[0].setCookieCommand.value;
  48. }
  49. });
  50.  
  51. function onHover(evt, delayed) {
  52. const inline = evt.target.closest(PREVIEW_TAG);
  53. const el = inline || evt.target.closest(THUMB);
  54. const thumb = el && inline === el ? $(THUMB, inline) : el;
  55. if (thumb && !thumb.getElementsByClassName(ME)[0] && (
  56. inline ||
  57. delayed || (
  58. inlinable != null || (inlinable = getProp($(PREVIEW_TAG), 'inlinePreviewIsEnabled')) != null
  59. ? !inlinable
  60. : setTimeout(getInlineState, 250, evt) && false
  61. )
  62. )) {
  63. if (inline) {
  64. addButtons($(PREVIEW_PARENT, el),
  65. getProp(el, 'mediaRenderer') ||
  66. getProp(el, 'opts.mediaRenderer'));
  67. } else {
  68. addButtons(thumb, thumb);
  69. }
  70. }
  71. }
  72.  
  73. async function onClick(e) {
  74. if (e.button) return;
  75. const me = e.target;
  76. const thumb = me[ME]; if (!thumb) return;
  77. const a = me.closest('a');
  78. const upd = thumb.localName === THUMB2;
  79. const MENU = upd ? MENU2 : MENU1;
  80. const POPUPICON = upd
  81. ? 'props.data.leadingImage.sources.0.clientResource.imageName'
  82. : 'data.icon.iconType';
  83. e.stopPropagation();
  84. e.stopImmediatePropagation();
  85. e.preventDefault();
  86. if (e.type === 'click') return;
  87. if (a) setPointerEvents(a, 'none');
  88. await new Promise(r => me.addEventListener('mouseup', r, {once: true}));
  89. let index, menu, popup, entry, el;
  90. if ((entry = thumb.closest(ENTRY)) && (el = $(MENU_BTN, entry))) {
  91. await 0;
  92. index = STYLE.sheet.insertRule(`${MENU}:not(#\\0) { opacity: 0 !important }`);
  93. el.dispatchEvent(new Event('click'));
  94. if ((popup = await waitFor('ytd-popup-container')))
  95. menu = await waitFor(MENU, popup);
  96. }
  97. if (a) setTimeout(setPointerEvents, 0, a);
  98. if (!menu) {
  99. STYLE.sheet.deleteRule(index);
  100. el = me.nextSibling;
  101. me.remove();
  102. me.title = 'No menu button?\nWait a few seconds for the site to load.';
  103. await new Promise(setTimeout);
  104. el.before(me);
  105. await timedPromise(null, 5000);
  106. me.title = '';
  107. return;
  108. }
  109. if (me.title)
  110. me.title = '';
  111. if (!isMenuReady(menu)) {
  112. let mo;
  113. if (!await timedPromise(resolve => {
  114. mo = new MutationObserver(() => isMenuReady(menu) && resolve(true));
  115. mo.observe(menu, {attributes: true, attributeFilter: ['style']});
  116. })) console.warn('Timeout waiting for px in `style` of', menu);
  117. mo.disconnect();
  118. }
  119. await new Promise(setTimeout);
  120. el = getProp(popup, `popups_.${MENU}.target`, true);
  121. if (a) a.style.removeProperty('pointer-events');
  122. if (el && !entry.contains(el)) {
  123. console.warn('Menu is not for the video you clicked', [menu, entry]);
  124. STYLE.sheet.deleteRule(index);
  125. return;
  126. }
  127. try {
  128. for (el of $('[role=listbox], [role=menu]', menu).children) {
  129. if (me.dataset.block === (COMMANDS[getProp(el, POPUPICON)] || {}).block) {
  130. el.click();
  131. break;
  132. }
  133. }
  134. } catch (e) {}
  135. await new Promise(setTimeout);
  136. document.body.click();
  137. await new Promise(setTimeout);
  138. STYLE.sheet.deleteRule(index);
  139. }
  140.  
  141. function addButtons(parent, thumb) {
  142. const upd = thumb.localName === THUMB2;
  143. const ITEMS = upd ? 'data.content.lockupViewModel.metadata.lockupMetadataViewModel.menuButton.' +
  144. 'buttonViewModel.onTap.innertubeCommand.showSheetCommand.panelLoadingStrategy.' +
  145. 'inlineContent.sheetViewModel.content.listViewModel.listItems'
  146. : 'data.menu.menuRenderer.items';
  147. const ITEM = upd ? 'listItemViewModel' : 'menuServiceItemRenderer';
  148. const ICON = upd ? 'leadingImage.sources.0.clientResource.imageName' : 'icon.iconType';
  149. const TEXT = upd ? 'title.content' : 'text.runs.0.text';
  150. const elems = [];
  151. const shown = {};
  152. for (const item of getProp(upd ? thumb.closest(ENTRY) : thumb, ITEMS) || []) {
  153. const menu = item[ITEM];
  154. const type = getProp(menu, ICON);
  155. let data = COMMANDS[type]; if (!data) continue;
  156. let {el} = data;
  157. if (!el) {
  158. data = COMMANDS[type] = {block: data};
  159. el = data.el = document.createElement('div');
  160. el.className = ME;
  161. el.dataset.block = data.block;
  162. }
  163. el.title = getProp(menu, TEXT) || data.text;
  164. el[ME] = thumb;
  165. shown[type] = 1;
  166. if (el.parentElement !== parent)
  167. elems.push(el);
  168. }
  169. for (let v in COMMANDS) {
  170. if (!shown[v] && (v = COMMANDS[v].el))
  171. v.remove();
  172. }
  173. requestAnimationFrame(() => parent.append(...elems));
  174. if (!STYLE) initStyle();
  175. }
  176.  
  177. function getInlineState(e) {
  178. if (e.target.matches(':hover') && !$(PREVIEW_TAG).getBoundingClientRect().width) {
  179. onHover(e, true);
  180. }
  181. }
  182.  
  183. function getProp(obj, path, isRaw) {
  184. if (!obj) return;
  185. if (obj instanceof Node) {
  186. obj = (obj = obj.wrappedJSObject || obj).polymerController || obj.__instance || obj.inst || obj;
  187. obj = !isRaw && obj.__data || obj;
  188. }
  189. for (const p of path.split('.'))
  190. if (obj) obj = obj[p]; else return;
  191. return obj;
  192. }
  193.  
  194. function getStorage() {
  195. try {
  196. return JSON.parse(localStorage[GM_info.script.name]);
  197. } catch (e) {}
  198. }
  199.  
  200. function isMenuReady(menu) {
  201. return menu.style.cssText.includes('px;');
  202. }
  203.  
  204. function $(sel, base = document) {
  205. return base.querySelector(sel);
  206. }
  207.  
  208. function setPointerEvents(el, value) {
  209. if (value != null) el.style.setProperty('pointer-events', value, 'important');
  210. else el.style.removeProperty('pointer-events');
  211. }
  212.  
  213. function timedPromise(promiseInit, ms = 1000) {
  214. ms = new Promise(resolve => setTimeout(resolve, ms));
  215. return promiseInit
  216. ? Promise.race([ms, new Promise(promiseInit)])
  217. : ms;
  218. }
  219.  
  220. async function waitFor(sel, base = document) {
  221. return $(sel, base) || timedPromise(resolve => {
  222. new MutationObserver((_, o) => {
  223. const el = $(sel, base); if (!el) return;
  224. o.disconnect();
  225. resolve(el);
  226. }).observe(base, {childList: true, subtree: true});
  227. });
  228. }
  229.  
  230. function initStyle() {
  231. STYLE = document.createElement('style');
  232. STYLE.textContent = /*language=CSS*/ `
  233. ${PREVIEW_PARENT} .${ME} {
  234. opacity: .5;
  235. }
  236. ${PREVIEW_PARENT} .${ME},
  237. :is(${THUMB}):hover .${ME},
  238. :is(${THUMB}):hover ~ .${ME} {
  239. display: block;
  240. }
  241. .${ME} {
  242. display: none;
  243. position: absolute;
  244. width: 16px;
  245. height: 16px;
  246. border-radius: 100%;
  247. border: 2px solid #fff;
  248. right: 8px;
  249. margin: 0;
  250. padding: 0;
  251. background: #0006;
  252. box-shadow: .5px .5px 7px #000;
  253. pointer-events: auto;
  254. cursor: pointer;
  255. opacity: .75;
  256. z-index: 11000;
  257. }
  258. ${PREVIEW_PARENT} .${ME}:hover,
  259. .${ME}:hover {
  260. opacity: 1;
  261. }
  262. .${ME}:active {
  263. color: yellow;
  264. }
  265. .${ME}[data-block] { top: 70px; }
  266. .${ME}[data-block="channel"] { top: 100px; }
  267. yt-thumbnail-view-model .${ME} { margin-top: 10px; }
  268. ${PREVIEW_TAG} .${ME}[data-block] { right: 18px; margin-top: 24px; }
  269. .ytd-playlist-panel-video-renderer .${ME}[data-block="unwatch"],
  270. .ytd-playlist-video-renderer .${ME}[data-block="unwatch"] {
  271. top: 15px;
  272. }
  273. ytd-compact-video-renderer .${ME}[data-block] {
  274. top: 57px;
  275. right: 7px;
  276. box-shadow: .5px .5px 4px 6px #000;
  277. background: #000;
  278. }
  279. ytd-compact-video-renderer .${ME}[data-block="channel"] {
  280. top: 81px;
  281. }
  282. ytd-compact-video-renderer ytd-thumbnail-overlay-toggle-button-renderer:nth-child(1) {
  283. top: -4px;
  284. }
  285. ytd-compact-video-renderer ytd-thumbnail-overlay-toggle-button-renderer:nth-child(2) {
  286. top: 24px;
  287. }
  288. .${ME}::before {
  289. position: absolute;
  290. content: '';
  291. top: -8px;
  292. left: -6px;
  293. width: 32px;
  294. height: 30px;
  295. }
  296. .${ME}::after {
  297. content: "";
  298. position: absolute;
  299. top: 0;
  300. left: 0;
  301. right: 0;
  302. bottom: 0;
  303. height: 0;
  304. margin: auto;
  305. border: none;
  306. border-bottom: 2px solid #fff;
  307. }
  308. .${ME}[data-block="video"]::after {
  309. transform: rotate(45deg);
  310. }
  311. .${ME}[data-block="channel"]::after {
  312. margin: auto 3px;
  313. }
  314. `.replace(/;/g, '!important;');
  315. document.head.appendChild(STYLE);
  316. }