From 5885492c2c0b52ecfa23b66fbd93e958042d5f5e Mon Sep 17 00:00:00 2001 From: "Lucijano JL." <218673409+Luzijano@users.noreply.github.com> Date: Sun, 5 Jul 2026 01:27:25 +0200 Subject: [PATCH] Add hide watched videos setting --- _locales/en/messages.json | 3 + js&css/extension/init.js | 2 + .../www.youtube.com/general/general.css | 12 ++ .../www.youtube.com/general/general.js | 108 ++++++++++++++++++ menu/skeleton-parts/general.js | 20 +++- tests/unit/hide-watched-videos.test.js | 37 ++++++ 6 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 tests/unit/hide-watched-videos.test.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 686f493a0..b788b0448 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -789,6 +789,9 @@ "hideWatchedVideos": { "message": "Hide watched videos" }, + "hideWatchedVideosThreshold": { + "message": "Hide watched videos at" + }, "high": { "message": "High" }, diff --git a/js&css/extension/init.js b/js&css/extension/init.js index b89aca314..561a59457 100644 --- a/js&css/extension/init.js +++ b/js&css/extension/init.js @@ -11,6 +11,7 @@ window.addEventListener('yt-navigate-finish', function () { extension.features.trackWatchedVideos(); extension.features.thumbnailsQuality(); extension.features.stickyNavigation(); + extension.features.hideWatchedVideos(); extension.features.hideSponsoredVideosOnHome?.(); }); @@ -46,6 +47,7 @@ extension.events.on('init', function () { extension.features.disableThumbnailPlayback(); extension.features.muteThumbnailPreviews(); extension.features.markWatchedVideos(); + extension.features.hideWatchedVideos(); extension.features.relatedVideos(); extension.features.sidePanels(); extension.features.stickyNavigation(); diff --git a/js&css/extension/www.youtube.com/general/general.css b/js&css/extension/www.youtube.com/general/general.css index 3a3fef248..ba1f0e1a5 100644 --- a/js&css/extension/www.youtube.com/general/general.css +++ b/js&css/extension/www.youtube.com/general/general.css @@ -52,6 +52,18 @@ html[it-cursorLighting=true] ytd-mini-guide-renderer {opacity:.4; transition:2 html[it-cursorLighting=true]:not([data-page-type=video]) #guide-content:hover, html[it-cursorLighting=true] ytd-mini-guide-renderer:hover {opacity:1; transition:.3s} +/*-------------------------------------------------------------- +# HIDE WATCHED VIDEOS +--------------------------------------------------------------*/ +ytd-compact-video-renderer[it-hide-watched-video], +ytd-rich-item-renderer[it-hide-watched-video], +ytd-video-renderer[it-hide-watched-video], +ytd-grid-video-renderer[it-hide-watched-video], +ytd-playlist-video-renderer[it-hide-watched-video], +ytd-reel-item-renderer[it-hide-watched-video] { + display: none !important; +} + /*-------------------------------------------------------------- # SQUARED THUMBNAILS --------------------------------------------------------------*/ diff --git a/js&css/extension/www.youtube.com/general/general.js b/js&css/extension/www.youtube.com/general/general.js index bc72419ae..aeb45d225 100644 --- a/js&css/extension/www.youtube.com/general/general.js +++ b/js&css/extension/www.youtube.com/general/general.js @@ -703,6 +703,114 @@ extension.features.trackWatchedVideos = function () { } }; +/*-------------------------------------------------------------- +# HIDE WATCHED VIDEOS +--------------------------------------------------------------*/ + +extension.features.hideWatchedVideos = function (anything) { + var enabled = anything === true || extension.storage.get('hide_watched_videos') === true; + var selector = [ + 'ytd-compact-video-renderer', + 'ytd-rich-item-renderer', + 'ytd-video-renderer', + 'ytd-grid-video-renderer', + 'ytd-playlist-video-renderer', + 'ytd-reel-item-renderer' + ].join(','); + + function getVideoId(renderer) { + var link = renderer.querySelector('a[href*="/watch"], a[href*="/shorts/"]'); + var href = link && link.href; + + if (!href) { + return ''; + } + + var queryId = extension.functions.getUrlParameter(href, 'v'); + + if (queryId) { + return queryId; + } + + var shortsMatch = href.match(/\/shorts\/([^?&/]+)/); + + return shortsMatch ? shortsMatch[1] : ''; + } + + function getWatchedPercent(renderer) { + if (renderer.querySelector('ytd-thumbnail-overlay-watched-status-renderer')) { + return 100; + } + + var progress = renderer.querySelector('ytd-thumbnail-overlay-resume-playback-renderer #progress, #progress.ytd-thumbnail-overlay-resume-playback-renderer'); + + if (progress && progress.style && progress.style.width) { + var width = parseFloat(progress.style.width); + + if (!Number.isNaN(width)) { + return width; + } + } + + var videoId = getVideoId(renderer); + var watched = extension.storage.get('watched'); + + if (videoId && watched && watched[videoId]) { + return 100; + } + + return 0; + } + + function apply() { + var threshold = parseInt(extension.storage.get('hide_watched_videos_threshold'), 10); + + if (Number.isNaN(threshold)) { + threshold = 90; + } + + threshold = Math.max(1, Math.min(100, threshold)); + + document.querySelectorAll(selector).forEach(function (renderer) { + var hide = enabled && getWatchedPercent(renderer) >= threshold; + + renderer.toggleAttribute('it-hide-watched-video', hide); + }); + } + + if (extension.features.hideWatchedVideos.observer) { + extension.features.hideWatchedVideos.observer.disconnect(); + extension.features.hideWatchedVideos.observer = null; + } + + apply(); + + if (enabled) { + var scheduled = false; + var schedule = function () { + if (scheduled) { + return; + } + + scheduled = true; + requestAnimationFrame(function () { + scheduled = false; + apply(); + }); + }; + + extension.features.hideWatchedVideos.observer = new MutationObserver(schedule); + extension.features.hideWatchedVideos.observer.observe(document.documentElement, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['style'] + }); + } +}; + +extension.features.hideWatchedVideosThreshold = extension.features.hideWatchedVideos; + /*-------------------------------------------------------------- # THUMBNAILS QUALITY --------------------------------------------------------------*/ diff --git a/menu/skeleton-parts/general.js b/menu/skeleton-parts/general.js index 0992e4e01..8f0c8788e 100644 --- a/menu/skeleton-parts/general.js +++ b/menu/skeleton-parts/general.js @@ -468,7 +468,25 @@ extension.skeleton.main.layers.section.general = { }, hide_watched_videos: { component: 'switch', - text: 'hideWatchedVideos' + text: 'hideWatchedVideos', + on: { + click: function () { + setTimeout(() => { + if (satus.storage.get('hide_watched_videos') && !satus.storage.get('track_watched_videos')) { + satus.storage.set('track_watched_videos', true); + } + }, 250); + } + } + }, + hide_watched_videos_threshold: { + component: 'slider', + variant: 'row', + text: 'hideWatchedVideosThreshold', + min: 1, + max: 100, + step: 1, + value: 90 }, hide_watch_later: { component: 'switch', diff --git a/tests/unit/hide-watched-videos.test.js b/tests/unit/hide-watched-videos.test.js new file mode 100644 index 000000000..838270b86 --- /dev/null +++ b/tests/unit/hide-watched-videos.test.js @@ -0,0 +1,37 @@ +const fs = require('fs'); +const path = require('path'); + +describe('Hide watched videos (#4128)', () => { + let initContent; + let generalContent; + let generalCssContent; + let menuContent; + + beforeAll(() => { + initContent = fs.readFileSync(path.join(__dirname, '../../js&css/extension/init.js'), 'utf8'); + generalContent = fs.readFileSync(path.join(__dirname, '../../js&css/extension/www.youtube.com/general/general.js'), 'utf8'); + generalCssContent = fs.readFileSync(path.join(__dirname, '../../js&css/extension/www.youtube.com/general/general.css'), 'utf8'); + menuContent = fs.readFileSync(path.join(__dirname, '../../menu/skeleton-parts/general.js'), 'utf8'); + }); + + test('initializes the hide watched videos feature', () => { + expect(initContent).toContain('extension.features.hideWatchedVideos();'); + }); + + test('hides watched thumbnail renderers by threshold', () => { + expect(generalContent).toContain('extension.features.hideWatchedVideos = function'); + expect(generalContent).toContain('ytd-thumbnail-overlay-watched-status-renderer'); + expect(generalContent).toContain('ytd-thumbnail-overlay-resume-playback-renderer #progress'); + expect(generalContent).toContain('hide_watched_videos_threshold'); + expect(generalContent).toContain('it-hide-watched-video'); + expect(generalCssContent).toContain('[it-hide-watched-video]'); + expect(generalCssContent).toContain('display: none !important'); + }); + + test('exposes a settings threshold control and enables tracking explicitly', () => { + expect(menuContent).toContain('hide_watched_videos_threshold'); + expect(menuContent).toContain("component: 'slider'"); + expect(menuContent).toContain("text: 'hideWatchedVideosThreshold'"); + expect(menuContent).toContain("satus.storage.set('track_watched_videos', true)"); + }); +});