diff --git a/js&css/extension/init.js b/js&css/extension/init.js index ac9a91a25..b6f5e5116 100644 --- a/js&css/extension/init.js +++ b/js&css/extension/init.js @@ -176,18 +176,19 @@ document.addEventListener('it-message-from-youtube', function () { } } else if (message.action === 'blocklist') { if (!extension.storage.data.blocklist || typeof extension.storage.data.blocklist !== 'object') { - extension.storage.data.blocklist = {}; + extension.storage.data.blocklist = {videos: {}, channels: {}}; } switch(message.type) { case 'channel': - if (!extension.storage.data.blocklist.channels) { + if (!extension.storage.data.blocklist.channels || typeof extension.storage.data.blocklist.channels !== 'object') { extension.storage.data.blocklist.channels = {}; } if (message.added) { extension.storage.data.blocklist.channels[message.id] = { title: message.title, - preview: message.preview + preview: message.preview, + when: message.when } } else { delete extension.storage.data.blocklist.channels[message.id]; @@ -195,12 +196,13 @@ document.addEventListener('it-message-from-youtube', function () { break case 'video': - if (!extension.storage.data.blocklist.videos) { + if (!extension.storage.data.blocklist.videos || typeof extension.storage.data.blocklist.videos !== 'object') { extension.storage.data.blocklist.videos = {}; } if (message.added) { extension.storage.data.blocklist.videos[message.id] = { - title: message.title + title: message.title, + when: message.when } } else { delete extension.storage.data.blocklist.videos[message.id]; diff --git a/js&css/extension/www.youtube.com/styles.css b/js&css/extension/www.youtube.com/styles.css index d017b09e7..49e4ea1ef 100644 --- a/js&css/extension/www.youtube.com/styles.css +++ b/js&css/extension/www.youtube.com/styles.css @@ -365,9 +365,9 @@ html[it-channel-hide-featured-content=true] #secondary ytd-browse-secondary-cont content: ''; border-radius: 50%; background: #f00; - /* background-image: url('/stuff/icons/48.png'); - background-size: cover; - background-position: center; */ + /* background-image: url('/stuff/icons/48.png'); + background-size: cover; + background-position: center; */ } .it-button::after { @@ -463,7 +463,7 @@ ytd-guide-section-renderer .it-button::after { position: absolute; top: 4px; left: 4px; - z-index: 999; + z-index: 2400; visibility: hidden; width: 28px; height: 28px; @@ -530,6 +530,8 @@ ytd-guide-section-renderer .it-button::after { cursor: pointer; } +ytd-video-preview.it-blocklisted-video:hover .it-add-to-blocklist, +ytd-video-preview.it-blocklisted-channel:hover .it-add-to-blocklist, *:hover>.it-add-to-blocklist { visibility: visible; } @@ -545,48 +547,66 @@ ytd-guide-section-renderer .it-button::after { } .it-blocklisted-video .it-add-to-blocklist::after { - content: "Unblock Video"; + content: "Unblock Video"; } .it-blocklisted-channel .it-add-to-blocklist::after { - content: "Unblock Channel"!important; + content: "Unblock Channel"!important; } .it-blocklisted-video, -.it-blocklisted-channel { +html:not([data-page-type=channel]) .it-blocklisted-channel { opacity: .15; max-height: 4rem; overflow: hidden; transition: max-height 0.4s ease 0.1s; } +.it-blocklisted-video.ytd-vertical-list-renderer, +.it-blocklisted-video.ytd-item-section-renderer, +html:not([data-page-type=channel]) .it-blocklisted-channel.ytd-vertical-list-renderer, +html:not([data-page-type=channel]) .it-blocklisted-channel.ytd-item-section-renderer { + max-height: 120px; +} + ytd-grid-video-renderer .it-blocklisted-video, -ytd-grid-video-renderer .it-blocklisted-channel, +html:not([data-page-type=channel]) ytd-grid-video-renderer .it-blocklisted-channel, ytd-rich-grid-media .it-blocklisted-video, -ytd-rich-grid-media .it-blocklisted-channel { +html:not([data-page-type=channel]) ytd-rich-grid-media .it-blocklisted-channel { overflow: visible; + max-height: 120px; +} + +ytd-video-preview.it-blocklisted-video:hover .it-add-to-blocklist, +html:not([data-page-type=channel]) ytd-video-preview.it-blocklisted-channel:hover .it-add-to-blocklist, +body:has(.it-blocklisted-video:hover) ytd-video-preview.it-blocklisted-video, +html:not([data-page-type=channel]) body:has(.it-blocklisted-channel:hover) ytd-video-preview.it-blocklisted-channel { + max-height: var(--ytd-video-preview-height); + opacity: 1; } .it-blocklisted-video ytd-thumbnail, -.it-blocklisted-channel ytd-thumbnail { +html:not([data-page-type=channel]) .it-blocklisted-channel ytd-thumbnail { visibility: hidden; max-width: 0; transition: max-width 0.4s ease 0.1s; } .it-blocklisted-video:hover, -.it-blocklisted-channel:hover { +html:not([data-page-type=channel]) .it-blocklisted-channel:hover { opacity: 1; overflow: visible; - max-height: 120px; + height: auto; + max-height: fit-content; transition: max-height 0.4s ease 1.1s; } .it-blocklisted-video:hover ytd-thumbnail, -.it-blocklisted-channel:hover ytd-thumbnail { +html:not([data-page-type=channel]) .it-blocklisted-channel:hover ytd-thumbnail { visibility: visible; max-width: 220px; transition: max-width 0.4s ease 1.1s; + height: auto; } /*------------NEW---------------*/ @@ -1975,7 +1995,7 @@ html[it-theme=sunset][data-system-color-scheme=light][it-schedule=system_peferen --ytd-simple-badge-color: hsla(0, 0%, 100%, .6); --ytd-ad-badge-text-color: hsl(0, 0%, 7%); --ytd-shopping-product-info: hsla(0, 100%, 100%, .74); - --ytd-toggle-color: hsl(0, 0%, 93.3%); + --ytd-toggle-color: hsl(0, 0%, 93.3%); --ytd-survey-button-color: var(--yt-primary-text-color); --ytd-transcript-cue-hover-background-color: hsla(0, 0%, 53.3%, .4); --ytd-transcript-toolbar-background-color: hsla(0, 0%, 53.3%, .4); @@ -2076,7 +2096,7 @@ Need HTML in front to make CSS rule more specific than one they are overiding /*possible fix: #hover-overlays .yt-spec-icon-shape, -ytd-thumbnail-overlay-toggle-button-renderer .yt-spec-icon-shape {color:white;} +ytd-thumbnail-overlay-toggle-button-renderer .yt-spec-icon-shape {color:white;} */ /* html .yt-spec-icon-shape, */ @@ -2084,12 +2104,12 @@ html .yt-spec-icon-badge-shape--style-overlay .yt-spec-icon-badge-shape__icon, html .yt-spec-button-shape-next--mono.yt-spec-button-shape-next--text, html .yt-spec-button-shape-next--mono.yt-spec-button-shape-next--tonal, html .yt-video-attribute-view-model__title { - color: var(--yt-spec-text-primary); + color: var(--yt-spec-text-primary); } /*Dark colors get highlight*/ html .yt-spec-button-shape-next--mono.yt-spec-button-shape-next--tonal { - background-color: rgba(255, 255, 255, 0.1); + background-color: rgba(255, 255, 255, 0.1); } /*Light colors get shadow, overrides above highlight*/ html[it-theme=desert] .yt-spec-button-shape-next--mono.yt-spec-button-shape-next--tonal, @@ -2100,8 +2120,8 @@ html:not([dark]):not([it-theme=black]):not([it-theme=sunset]):not([it-theme=nigh /*subscribe button when not subscribed*/ html .yt-spec-button-shape-next--mono.yt-spec-button-shape-next--filled { - color: var(--yt-spec-base-background); - background-color: var(--yt-spec-text-primary); + color: var(--yt-spec-base-background); + background-color: var(--yt-spec-text-primary); } /*override bell and thumbs up icons hardcoded colors inside SVG data*/ diff --git a/js&css/web-accessible/core.js b/js&css/web-accessible/core.js index 1cf120ddf..681bf7dc5 100644 --- a/js&css/web-accessible/core.js +++ b/js&css/web-accessible/core.js @@ -40,6 +40,12 @@ var ImprovedTube = { playlist_id: /[?&]list=([^&]+)/, channel_link: /https:\/\/www.youtube.com\/@|((channel|user|c)\/)/ }, + button_icons: { + blocklist:{ + svg: [['viewBox', '0 0 24 24']], + path: [['d', 'M12 2a10 10 0 100 20 10 10 0 000-20zm0 18A8 8 0 015.69 7.1L16.9 18.31A7.9 7.9 0 0112 20zm6.31-3.1L7.1 5.69A8 8 0 0118.31 16.9z']] + } + }, video_src: false, initialVideoUpdateDone: false, latestVideoDuration: 0, @@ -174,8 +180,7 @@ document.addEventListener('it-message-from-extension', function () { } ImprovedTube.init(); - // need to run blocklist once just after page load to catch initial nodes - ImprovedTube.blocklist(); + ImprovedTube.blocklistInit(); // REACTION OR VISUAL FEEDBACK WHEN THE USER CHANGES A SETTING (already automated for our CSS features): } else if (message.action === 'storage-changed') { @@ -199,8 +204,9 @@ document.addEventListener('it-message-from-extension', function () { } switch(camelized_key) { + case 'blocklist': case 'blocklistActivate': - camelized_key = 'blocklist'; + ImprovedTube.blocklistInit(); break case 'playerPlaybackSpeed': diff --git a/js&css/web-accessible/functions.js b/js&css/web-accessible/functions.js index 02530b626..458b3d1d4 100644 --- a/js&css/web-accessible/functions.js +++ b/js&css/web-accessible/functions.js @@ -33,43 +33,21 @@ ImprovedTube.childHandler = function (node) { //console.log(node.nodeName); }; */ ImprovedTube.ytElementsHandler = function (node) { - var name = node.nodeName, + const name = node.nodeName, id = node.id; if (name === 'A') { if (node.href) { this.channelDefaultTab(node); - - if (this.storage.blocklist_activate && node.classList.contains('ytd-thumbnail')) { - this.blocklist('video', node); - } } - } /* else if (name === 'META') { // infos are not updated when clicking related videos... - if(node.getAttribute('name')) { - //if(node.getAttribute('name') === 'title') {ImprovedTube.title = node.content;} //duplicate - //if(node.getAttribute('name') === 'description') {ImprovedTube.description = node.content;} //duplicate - //if node.getAttribute('name') === 'themeColor') {ImprovedTube.themeColor = node.content;} //might help our darkmode/themes -//Do we need any of these here before the player starts? - //if(node.getAttribute('name') === 'keywords') {ImprovedTube.keywords = node.content;} - } else if (node.getAttribute('itemprop')) { - //if(node.getAttribute('itemprop') === 'name') {ImprovedTube.title = node.content;} - if(node.getAttribute('itemprop') === 'genre') {ImprovedTube.category = node.content;} - //if(node.getAttribute('itemprop') === 'channelId') {ImprovedTube.channelId = node.content;} - //if(node.getAttribute('itemprop') === 'videoId') {ImprovedTube.videoId = node.content;} -//The following infos will enable awesome, smart features. Some of which everyone should use. - //if(node.getAttribute('itemprop') === 'description') {ImprovedTube.description = node.content;} - //if(node.getAttribute('itemprop') === 'duration') {ImprovedTube.duration = node.content;} - //if(node.getAttribute('itemprop') === 'interactionCount'){ImprovedTube.views = node.content;} - //if(node.getAttribute('itemprop') === 'isFamilyFriendly'){ImprovedTube.isFamilyFriendly = node.content;} - //if(node.getAttribute('itemprop') === 'unlisted') {ImprovedTube.unlisted = node.content;} - //if(node.getAttribute('itemprop') === 'regionsAllowed'){ImprovedTube.regionsAllowed = node.content;} - //if(node.getAttribute('itemprop') === 'paid') {ImprovedTube.paid = node.content;} - // if(node.getAttribute('itemprop') === 'datePublished' ){ImprovedTube.datePublished = node.content;} - //to use in the "how long ago"-feature, not to fail without API key? just like the "day-of-week"-feature above - // if(node.getAttribute('itemprop') === 'uploadDate') {ImprovedTube.uploadDate = node.content;} + if (this.storage.blocklist_activate) { + // we are interested in thumbnails and video-previews, skip ones with 'button.it-add-to-blocklist' already + if (((node.href && node.classList.contains('ytd-thumbnail')) || node.classList.contains('ytd-video-preview')) + && !node.querySelector("button.it-add-to-blocklist")) { + this.blocklistNode(node); + } } - } */ - else if (name === 'YTD-TOGGLE-BUTTON-RENDERER' || name === 'YTD-PLAYLIST-LOOP-BUTTON-RENDERER') { + } else if (name === 'YTD-TOGGLE-BUTTON-RENDERER' || name === 'YTD-PLAYLIST-LOOP-BUTTON-RENDERER') { //can be precise previously node.parentComponent & node.parentComponent.parentComponent if (node.closest("YTD-MENU-RENDERER") && node.closest("YTD-PLAYLIST-PANEL-RENDERER")) { @@ -137,10 +115,7 @@ ImprovedTube.ytElementsHandler = function (node) { else if (name === 'YTD-PLAYLIST-HEADER-RENDERER' || (name === 'YTD-MENU-RENDERER' && node.classList.contains('ytd-playlist-panel-renderer'))) { this.playlistPopupUpdate(); } else if (name === 'YTD-SUBSCRIBE-BUTTON-RENDERER' || name === 'YT-SUBSCRIBE-BUTTON-VIEW-MODEL') { - if (this.storage.blocklist_activate && location.href.match(ImprovedTube.regex.channel)) { - ImprovedTube.blocklist('channel', node); - } - + ImprovedTube.blocklistChannel(node); ImprovedTube.elements.subscribe_button = node; } else if (id === 'chat-messages') { this.elements.livechat.button = document.querySelector('[aria-label="Close"]'); @@ -553,6 +528,35 @@ ImprovedTube.setCookie = function (name, value) { document.cookie = name + '=' + value + '; path=/; domain=.youtube.com; expires=' + date.toGMTString(); }; +ImprovedTube.createIconButton = function (options) { + const button = document.createElement('button'), + svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), + path = document.createElementNS('http://www.w3.org/2000/svg', 'path'), + type = this.button_icons[options.type]; + + for(const attr of type.svg) svg.setAttribute(attr[0], attr[1]); + for(const attr of type.path) path.setAttribute(attr[0], attr[1]); + + svg.appendChild(path); + button.appendChild(svg); + + if (options.className) button.className = options.className; + if (options.id) button.id = options.id; + if (options.onclick) { + if (!options.propagate) { + //we fully own all click events landing on this button + button.onclick = function (event) { + event.preventDefault(); + event.stopPropagation(); + options.onclick.apply(this, arguments); + } + } else { + button.onclick = options.onclick; + } + } + return button; +}; + ImprovedTube.createPlayerButton = function (options) { var controls = options.position == "right" ? this.elements.player_right_controls : this.elements.player_left_controls; if (controls) { diff --git a/js&css/web-accessible/init.js b/js&css/web-accessible/init.js index b4f5e01a8..66c09bcd9 100644 --- a/js&css/web-accessible/init.js +++ b/js&css/web-accessible/init.js @@ -121,6 +121,30 @@ ImprovedTube.init = function () { }; document.addEventListener('yt-navigate-finish', function () { +/* if (name === 'META') { // infos are not updated when clicking related videos... + if(node.getAttribute('name')) { + //if(node.getAttribute('name') === 'title') {ImprovedTube.title = node.content;} //duplicate + //if(node.getAttribute('name') === 'description') {ImprovedTube.description = node.content;} //duplicate + //if node.getAttribute('name') === 'themeColor') {ImprovedTube.themeColor = node.content;} //might help our darkmode/themes +//Do we need any of these here before the player starts? + //if(node.getAttribute('name') === 'keywords') {ImprovedTube.keywords = node.content;} + } else if (node.getAttribute('itemprop')) { + //if(node.getAttribute('itemprop') === 'name') {ImprovedTube.title = node.content;} + if(node.getAttribute('itemprop') === 'genre') {ImprovedTube.category = node.content;} + //if(node.getAttribute('itemprop') === 'channelId') {ImprovedTube.channelId = node.content;} + //if(node.getAttribute('itemprop') === 'videoId') {ImprovedTube.videoId = node.content;} +//The following infos will enable awesome, smart features. Some of which everyone should use. + //if(node.getAttribute('itemprop') === 'description') {ImprovedTube.description = node.content;} + //if(node.getAttribute('itemprop') === 'duration') {ImprovedTube.duration = node.content;} + //if(node.getAttribute('itemprop') === 'interactionCount'){ImprovedTube.views = node.content;} + //if(node.getAttribute('itemprop') === 'isFamilyFriendly'){ImprovedTube.isFamilyFriendly = node.content;} + //if(node.getAttribute('itemprop') === 'unlisted') {ImprovedTube.unlisted = node.content;} + //if(node.getAttribute('itemprop') === 'regionsAllowed'){ImprovedTube.regionsAllowed = node.content;} + //if(node.getAttribute('itemprop') === 'paid') {ImprovedTube.paid = node.content;} + // if(node.getAttribute('itemprop') === 'datePublished' ){ImprovedTube.datePublished = node.content;} + //to use in the "how long ago"-feature, not to fail without API key? just like the "day-of-week"-feature above + // if(node.getAttribute('itemprop') === 'uploadDate') {ImprovedTube.uploadDate = node.content;} +*/ ImprovedTube.pageType(); if(ImprovedTube.storage.undo_the_new_sidebar === true){ImprovedTube.undoTheNewSidebar();} ImprovedTube.commentsSidebar(); diff --git a/js&css/web-accessible/www.youtube.com/blocklist.js b/js&css/web-accessible/www.youtube.com/blocklist.js index 9a7cef9e4..c66886e1c 100644 --- a/js&css/web-accessible/www.youtube.com/blocklist.js +++ b/js&css/web-accessible/www.youtube.com/blocklist.js @@ -1,201 +1,170 @@ /*------------------------------------------------------------------------------ 4.8.0 BLOCKLIST ------------------------------------------------------------------------------*/ -// usage: -// () called only to turn On (rescans all elements on page)/Off -// ('video', node) called only for 'a#thumbnail.ytd-thumbnail[href]' -// ('channel', node) called only for 'ytd-subscribe-button-renderer.ytd-c4-tabbed-header-renderer' +ImprovedTube.blocklistNode = function (node) { + if (!this.storage.blocklist_activate || !node) return; -ImprovedTube.blocklist = function (type, node) { - if (this.storage.blocklist_activate) { - if (type === 'video') { - if (node.nodeName !== 'A' || !node.href) { alert(1) }; - const video = node.href.match(ImprovedTube.regex.video_id)?.[1], - channel = node.parentNode.__dataHost?.__data?.data?.shortBylineText?.runs?.[0]?.navigationEndpoint?.commandMetadata?.webCommandMetadata?.url ? node.parentNode.__dataHost.__data.data.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url.match(ImprovedTube.regex.channel).groups.name : undefined; - let mode = 'video', - blockedElement; - if (!video) return; // no video ID, something went horribly wrong, bail - - // YT reuses VIDEO elements dynamically, need to monitor and also dynamically readjust BLOCK style - if (!this.elements.observerList.includes(node)) { - // YT reuses VIDEO elements dynamically, need to monitor and also dynamically readjust BLOCK style whenever href is modified - this.blocklistObserver.observe(node, {attributes: true, - attributeFilter: ['href']}); - // keep track to only attach one observer per element - this.elements.observerList.push(node); - } + const video = node.href?.match(ImprovedTube.regex.video_id)?.[1] || (node.classList?.contains('ytd-video-preview') ? 'video-preview' : null), + channel = node.parentNode?.__dataHost?.__data?.data?.shortBylineText?.runs?.[0]?.navigationEndpoint?.commandMetadata?.webCommandMetadata?.url?.match(ImprovedTube.regex.channel)?.groups?.name, + blockedElement = node.blockedElement || this.blockedElementTypeHelper(node); - switch(node.parentNode.className.replace('style-scope ','')) { - case 'ytd-compact-video-renderer': - // list next to player - // node.parentNode.__dataHost.$.dismissible; - case 'ytd-rich-item-renderer': - // short reel - case 'ytd-rich-grid-media': - // grid reel - case 'ytd-rich-grid-slim-media': - // short grid reel - case 'ytd-playlist-video-renderer': - // playlist page - case 'ytd-playlist-panel-video-renderer': - // playlist next to player - // node.parentNode.closest('ytd-playlist-panel-video-renderer') - case 'ytd-structured-description-video-lockup-renderer': - // list under the player - // node.parentNode.closest('ytd-structured-description-video-lockup-renderer') - // or even node.parentNode.closest('ytd-compact-infocard-renderer') === node.parentNode.parentNode.parentNode.parentNode - blockedElement = node.parentNode.parentNode.parentNode; - break; - case 'ytd-grid-video-renderer': - // channel home screen grid - case 'ytd-reel-item-renderer': - // reel - blockedElement = node.parentNode.parentNode; - break; - } + if (!video) return; // not interested in nodes without one - if (!blockedElement) return; // couldnt find valid enveloping element, bail + // YT reuses Thumbnail cells dynamically, need to monitor all created Thumbnail links and dynamically apply/remove 'it-blocklisted-*' classes + if (!this.elements.observerList.includes(node)) { + this.blocklistObserver.observe(node, {attributes: true, + attributeFilter: ['href']}); + // keeping a list to attach only one observer per tracked element + this.elements.observerList.push(node); + } - node.blockedElement = blockedElement; - if (this.storage.blocklist) { - if (this.storage.blocklist.videos) { - if (this.storage.blocklist.videos[video] && !blockedElement.classList.contains('it-blocklisted-video')) { - // blocklisted video - blockedElement.classList.add('it-blocklisted-video'); - } else if (!this.storage.blocklist.videos[video] && blockedElement.classList.contains('it-blocklisted-video')) { - // video not blocklisted, show it - blockedElement.classList.remove('it-blocklisted-video'); - } - } - if (channel && this.storage.blocklist.channels ) { - // this thumbnail has channel information, can try channel blocklist - if (this.storage.blocklist.channels[channel] && !blockedElement.classList.contains('it-blocklisted-channel')) { - // blocked channel? = block all videos from that channel - blockedElement.classList.add('it-blocklisted-channel'); - } else if (!this.storage.blocklist.channels[channel] && blockedElement.classList.contains('it-blocklisted-channel')) { - // channel not blocked, show it - blockedElement.classList.remove('it-blocklisted-channel'); - } - } + if (!blockedElement) return; // unknown thumbnail cell type, bail out + + if (this.storage.blocklist) { + if (this.storage.blocklist.videos && ImprovedTube.storage.blocklist.videos[video]) { + // blocklisted video + blockedElement.classList.add('it-blocklisted-video'); + } else { + // video not blocklisted, show it. classList.remove() directly as there is no speed benefit to .has() before + blockedElement.classList.remove('it-blocklisted-video'); + } + if (this.storage.blocklist.channels && channel && ImprovedTube.storage.blocklist.channels[channel]) { + // blocked channel + blockedElement.classList.add('it-blocklisted-channel'); + } else { + // channel not blocked, show it. + blockedElement.classList.remove('it-blocklisted-channel'); + } } - if (node.querySelector("button.it-add-to-blocklist")) return; // skip blocklist button creation if one already exists - - let button = document.createElement('button'), - svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), - path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - - button.className = 'it-add-to-blocklist'; - button.addEventListener('click', function (event) { - if (this.parentNode.href) { - - const video = node.href.match(ImprovedTube.regex.video_id)?.[1], - channel = node.parentNode.__dataHost?.__data?.data?.shortBylineText?.runs?.[0]?.navigationEndpoint?.commandMetadata?.webCommandMetadata?.url ? node.parentNode.__dataHost.__data.data.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url.match(ImprovedTube.regex.channel).groups.name : undefined, - data = this.parentNode.__dataHost.__data?.data?.title, - blockedElement = node.blockedElement; - let title, - added = false, - type = 'video'; - - if (!video || !blockedElement) return; // need both video ID and blockedElement, otherwise bail - - if (data?.runs?.[0]?.text) { - title = data.runs[0].text; - } else if (data?.simpleText) { - title = data.simpleText; - } - - if (channel && blockedElement.classList.contains('it-blocklisted-channel')) { - // unblocking channel - type = 'channel'; - } else if (blockedElement.classList.contains('it-blocklisted-video')) { - // unblocking blocklisted video - } else { - // nothing blocked, clicking should block this video - added = true; - } - ImprovedTube.messages.send({action: 'blocklist', - added: added, - type: type, - id: type == 'channel' ? channel : video, - title: title}); - event.preventDefault(); - event.stopPropagation(); - } - }, true); - - svg.setAttributeNS(null, 'viewBox', '0 0 24 24'); - path.setAttributeNS(null, 'd', 'M12 2a10 10 0 100 20 10 10 0 000-20zm0 18A8 8 0 015.69 7.1L16.9 18.31A7.9 7.9 0 0112 20zm6.31-3.1L7.1 5.69A8 8 0 0118.31 16.9z'); - - svg.appendChild(path); - button.appendChild(svg); - - node.appendChild(button); - this.elements.blocklist_buttons.push(button); - } else if (type === 'channel') { - let button = node.parentNode.parentNode.querySelector("button.it-add-channel-to-blocklist"), - id = location.href.match(ImprovedTube.regex.channel).groups.name; - - // skip channel blocklist button creation if one already exists - if (button) { - if (this.storage.blocklist.channels[id] && button.added) { - button.innerText = 'Remove from blocklist'; - button.added = false; - } else if (!this.storage.blocklist.channels[id] && !button.added) { - button.innerText = 'Add to blocklist'; - button.added = true; - } + + // skip blocklist button creation if one already exists, in theory this never happens due to check in functions.js + if (node.querySelector("button.it-add-to-blocklist")) return; + + node.blockedElement = blockedElement; + + const button = this.createIconButton({ + type: 'blocklist', + className: 'it-add-to-blocklist', + onclick: function (event) { + if (!this.parentNode.href) return; // no href no action + const video = this.parentNode.href?.match(ImprovedTube.regex.video_id)?.[1], + channel = this.parentNode.parentNode?.__dataHost?.__data?.data?.shortBylineText?.runs?.[0]?.navigationEndpoint?.commandMetadata?.webCommandMetadata?.url?.match(ImprovedTube.regex.channel)?.groups?.name + // video-preview doesnt have Channel info, extract from source thumbnail + || ((video && this.parentNode?.classList.contains('ytd-video-preview')) ? ImprovedTube.elements.observerList.find(a => a.id == 'thumbnail' && a.href?.match(ImprovedTube.regex.video_id)?.[1] === video).parentNode?.__dataHost?.__data?.data?.shortBylineText?.runs?.[0]?.navigationEndpoint?.commandMetadata?.webCommandMetadata?.url?.match(ImprovedTube.regex.channel)?.groups?.name : null), + blockedElement = node.blockedElement, + + // Yes, this is horrible. Cant find better way of extracting title :( + title = this.parentNode?.__dataHost?.__data?.data?.title?.runs?.[0]?.text + || this.parentNode.__dataHost?.__data?.data?.title?.simpleText + || this.parentNode.__dataHost?.__data?.videoPreviewData?.accessibilityText + || this.parentNode.blockedElement?.querySelector('[title]')?.title; + let added = false, + type = 'video'; + + if (!video || !blockedElement || !title) { + console.error('blocklist: need video ID, blockedElement and title'); return; } - button = document.createElement('button'); - button.className = 'it-add-channel-to-blocklist'; - - if (this.storage.blocklist.channels[id]) { - button.innerText = 'Remove from blocklist'; - button.added = false; + // this button can perform three functions: + if (channel && blockedElement.classList.contains('it-blocklisted-channel')) { + // unblocking whole channel + type = 'channel'; + } else if (blockedElement.classList.contains('it-blocklisted-video')) { + // unblocking blocklisted video } else { - button.innerText = 'Add to blocklist'; - button.added = true; + // block this video + added = true; } - button.addEventListener('click', function (event) { - const data = ytInitialData.metadata.channelMetadataRenderer, - //let data = this.parentNode.__dataHost.__data.data, - id = location.href.match(ImprovedTube.regex.channel).groups.name; - - if (this.added) { // adding - ImprovedTube.storage.blocklist.channels[id] = {title: data.title, - preview: data.avatar.thumbnails[0].url}; - button.innerText = 'Remove from blocklist'; - } else { // removing - delete ImprovedTube.storage.blocklist.channels[id]; - button.innerText = 'Add to blocklist'; - } - ImprovedTube.messages.send({action: 'blocklist', - added: this.added, - type: 'channel', - id: id, - title: data.title, - preview: data.avatar.thumbnails[0].url}); - this.added = !this.added; - - event.preventDefault(); - event.stopPropagation(); - }, true); - - node.parentNode.parentNode.appendChild(button); - this.elements.blocklist_buttons.push(button); - } else if (arguments.length == 0) { - // scan whole page - for (let thumbnails of document.querySelectorAll('a.ytd-thumbnail[href]')) { - this.blocklist('video', thumbnails); - } - if (document.querySelector('YT-SUBSCRIBE-BUTTON-VIEW-MODEL')) { - this.blocklist('channel', document.querySelector('YT-SUBSCRIBE-BUTTON-VIEW-MODEL')); - } + // this message will trigger 'storage-changed' event and eventually blocklistInit() full rescan + ImprovedTube.messages.send({action: 'blocklist', + added: added, + type: type, + id: type == 'channel' ? channel : video, + title: title, + when: Date.parse(new Date().toDateString()) / 100000 + }); + } + }); + + node.appendChild(button); + this.elements.blocklist_buttons.push(button); +}; + +ImprovedTube.blocklistChannel = function (node) { + if (!this.storage.blocklist_activate || !node) return; + + const id = location.pathname.match(ImprovedTube.regex.channel)?.groups?.name; + let button = node.parentNode?.parentNode?.querySelector("button.it-add-channel-to-blocklist"); + + if (!id) return; // not on channel page + + // skip button.it-add-channel-to-blocklist creation if one already exists, adjust text only + if (button) { + button.innerText = this.storage.blocklist.channels[id] ? 'Remove from blocklist' : 'Add to blocklist'; + return; + } + + button = document.createElement('button'); + + button.className = 'it-add-channel-to-blocklist'; + button.innerText = this.storage.blocklist.channels[id] ? 'Remove from blocklist' : 'Add to blocklist'; + button.onclick = function (event) { + event.preventDefault(); + event.stopPropagation(); + + // no longer working: + //data = ytInitialData?.metadata?.channelMetadataRenderer, + //data = this.parentNode.__dataHost.__data.data, + //avatar= data?.avatar?.thumbnails[0]?.url + const id = location.pathname.match(ImprovedTube.regex.channel)?.groups?.name, + title = document.querySelector('yt-dynamic-text-view-model .yt-core-attributed-string')?.innerText, + preview = document.querySelector('yt-decorated-avatar-view-model img')?.src; + let added = false; + + if (!id || !title) { + console.error('blocklist click: no channel ID or metadata'); + return; + } + + // this message will trigger 'storage-changed' event and eventually blocklistInit() full rescan + ImprovedTube.messages.send({action: 'blocklist', + added: !ImprovedTube.storage.blocklist.channels[id], + type: 'channel', + id: id, + title: title, + preview: preview, + when: Date.parse(new Date().toDateString()) / 100000 + }); + }; + + node.parentNode.parentNode.appendChild(button); + this.elements.blocklist_buttons.push(button); +}; + +ImprovedTube.blocklistInit = function () { + if (this.storage.blocklist_activate) { + // initialize and (re)scan whole page. Called on load after 'storage-loaded' + // and blocklist 'storage-changed' event (adding/removing blocks) + if (!this.storage.blocklist || typeof this.storage.blocklist !== 'object') { + this.storage.blocklist = {videos: {}, channels: {}}; + } + if (!this.storage.blocklist.videos || typeof this.storage.blocklist.channels !== 'object') { + this.storage.blocklist.videos = {}; + } + if (!this.storage.blocklist.channels || typeof this.storage.blocklist.channels !== 'object') { + this.storage.blocklist.channels = {}; + } + for (const thumbnail of document.querySelectorAll('a.ytd-thumbnail[href], a.ytd-video-preview')) { + this.blocklistNode(thumbnail); + } + if (document.querySelector('YT-SUBSCRIBE-BUTTON-VIEW-MODEL')) { + this.blocklistChannel(document.querySelector('YT-SUBSCRIBE-BUTTON-VIEW-MODEL')); } } else { - // remove blocklist buttons + // Disable and unload Blocklist + // remove all 'it-add-to-blocklist' buttons for (let blocked of this.elements.blocklist_buttons) { blocked.remove(); } @@ -206,40 +175,85 @@ ImprovedTube.blocklist = function (type, node) { // release observer ImprovedTube.blocklistObserver.disconnect(); } - // remove all blocks from videos\channels - for (let blocked of document.querySelectorAll('.it-blocklisted-video, .it-blocklisted-channel')) { + // remove all video/channel blocks from thumbnails on current page + for (let blocked of document.querySelectorAll('.it-blocklisted-video')) { blocked.classList.remove('it-blocklisted-video'); + } + for (let blocked of document.querySelectorAll('.it-blocklisted-channel')) { blocked.classList.remove('it-blocklisted-channel'); } } }; ImprovedTube.blocklistObserver = new MutationObserver(function (mutationList) { - for (var mutation of mutationList) { - const video = mutation.target.href.match(ImprovedTube.regex.video_id)?.[1], - channel = mutation.target.parentNode.__dataHost?.__data?.data?.shortBylineText?.runs?.[0]?.navigationEndpoint?.commandMetadata?.webCommandMetadata?.url ? mutation.target.parentNode.__dataHost.__data.data.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url.match(ImprovedTube.regex.channel).groups.name : undefined, - blockedElement = mutation.target.blockedElement; + for (const mutation of mutationList) { + const video = mutation.target.href?.match(ImprovedTube.regex.video_id)?.[1], + channel = mutation.target.parentNode?.__dataHost?.__data?.data?.shortBylineText?.runs?.[0]?.navigationEndpoint?.commandMetadata?.webCommandMetadata?.url?.match(ImprovedTube.regex.channel)?.groups?.name + // video-preview doesnt have Channel info, extract from source thumbnail + || ((video && mutation.target?.classList.contains('ytd-video-preview')) ? ImprovedTube.elements.observerList.find(a => a.id == 'thumbnail' && a.href?.match(ImprovedTube.regex.video_id)?.[1] === video).parentNode?.__dataHost?.__data?.data?.shortBylineText?.runs?.[0]?.navigationEndpoint?.commandMetadata?.webCommandMetadata?.url?.match(ImprovedTube.regex.channel)?.groups?.name : null), + blockedElement = ImprovedTube.blockedElementTypeHelper(mutation.target); - if (!video || !blockedElement) return; // need both video ID and blockedElement, otherwise bail + if (!blockedElement) return; // unknown thumbnail cell type, bail out + mutation.target.blockedElement = blockedElement; - if (ImprovedTube.storage.blocklist.videos[video]) { - if (!blockedElement.classList.contains('it-blocklisted-video')) { + if (!video) { + // no video ID means monitored thumbnail/video-preview node went inactive + blockedElement.classList.remove('it-blocklisted-video'); + blockedElement.classList.remove('it-blocklisted-channel'); + return; + } + + if (ImprovedTube.storage.blocklist) { + if (ImprovedTube.storage.blocklist.videos && ImprovedTube.storage.blocklist.videos[video]) { blockedElement.classList.add('it-blocklisted-video'); - } - } else { - if (blockedElement.classList.contains('it-blocklisted-video')) { + } else { blockedElement.classList.remove('it-blocklisted-video'); } + if (ImprovedTube.storage.blocklist.channels && channel && ImprovedTube.storage.blocklist.channels[channel]) { + blockedElement.classList.add('it-blocklisted-channel'); + } else { + blockedElement.classList.remove('it-blocklisted-channel'); + } } + } +}); +ImprovedTube.blockedElementTypeHelper = function (node) { + switch(node.parentNode.className.replace('style-scope ','')) { + case 'ytd-compact-video-renderer': + // list next to player + // node.parentNode.__dataHost.$.dismissible; + case 'ytd-rich-item-renderer': + // short reel + case 'ytd-rich-grid-media': + // grid reel + case 'ytd-rich-grid-slim-media': + // short grid reel + case 'ytd-playlist-video-renderer': + // playlist page + case 'ytd-playlist-panel-video-renderer': + // playlist next to player + // node.parentNode.closest('ytd-playlist-panel-video-renderer') + case 'ytd-structured-description-video-lockup-renderer': + // list under the player + // node.parentNode.closest('ytd-structured-description-video-lockup-renderer') + // or even node.parentNode.closest('ytd-compact-infocard-renderer') === node.parentNode.parentNode.parentNode.parentNode + case 'ytd-video-renderer': + // search results + case 'ytd-video-preview': + // subscriptions/search thumbnail video-preview + return node.parentNode.parentNode.parentNode; + break; - if (channel && ImprovedTube.storage.blocklist.channels[channel] && !blockedElement.classList.contains('it-blocklisted-channel')) { - // blocked channel? = block all videos from that channel - blockedElement.classList.add('it-blocklisted-channel'); - } else if ((!channel || !ImprovedTube.storage.blocklist.channels[channel]) && blockedElement.classList.contains('it-blocklisted-channel')) { - // channel not blocked, show it - blockedElement.classList.remove('it-blocklisted-channel'); - } + case 'ytd-grid-video-renderer': + // channel home screen grid + case 'ytd-reel-item-renderer': + // reel + return node.parentNode.parentNode; + break; + default: + // unknown ones land here + break; } -}); +};