您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Dims watched YouTube videos proportionally to the watched time.
// ==UserScript== // @name YouTube Watched Video Dimmer // @namespace https://greasyfork.org/users/1458847 // @version 1.1 // @license MIT // @description Dims watched YouTube videos proportionally to the watched time. // @author Ev Haus, netjeff, actionless // @match https://*.youtube.com/* // @match https://youtube.com/* // @noframes // @grant GM_getValue // @grant GM_setValue // ==/UserScript== /* * This script is based on the original "YouTube: Hide Watched Videos" by Ev Haus, netjeff, and actionless. * Modifications include: * - Simplified the code by removing Shorts and hide options. * - Implemented proportional dimming based on watched time. * - Settings for dimming are now configurable directly within the code. */ (function () { "use strict"; const WATCHED_THRESHOLD_PERCENT = 10; const MIN_DIM_OPACITY = 0.2; const MAX_DIM_OPACITY = 0.9; const DEBUG = false; if ( typeof trustedTypes !== 'undefined' && trustedTypes.defaultPolicy === null ) { const s = (s) => s; trustedTypes.createPolicy('default', { createHTML: s, createScriptURL: s, createScript: s, }); } const logDebug = (...msgs) => { if (DEBUG) console.debug('[YT-HWV]', msgs); }; const addStyle = (aCss) => { const head = document.getElementsByTagName('head')[0]; if (head) { const style = document.createElement('style'); style.setAttribute('type', 'text/css'); style.textContent = aCss; head.appendChild(style); return style; } return null; }; const css = ` .YT-HWV-WATCHED-HIDDEN { display: none !important } .YT-HWV-HIDDEN-ROW-PARENT { padding-bottom: 10px } .YT-HWV-BUTTONS { background: transparent; border: 1px solid var(--ytd-searchbox-legacy-border-color); border-radius: 40px; display: flex; gap: 5px; margin: 0 20px; } .YT-HWV-BUTTON { align-items: center; background: transparent; border: 0; border-radius: 40px; color: var(--yt-spec-icon-inactive); cursor: pointer; display: flex; height: 40px; justify-content: center; outline: 0; width: 40px; } .YT-HWV-BUTTON:focus, .YT-HWV-BUTTON:hover { background: var(--yt-spec-badge-chip-background); } .YT-HWV-BUTTON-DISABLED { color: var(--yt-spec-icon-disabled) } `; addStyle(css); const BUTTONS = [ { icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 48 48"><path fill="currentColor" d="M24 9C14 9 5.46 15.22 2 24c3.46 8.78 12 15 22 15 10.01 0 18.54-6.22 22-15-3.46-8.78-11.99-15-22-15zm0 25c-5.52 0-10-4.48-10-10s4.48-10 10-10 10 4.48 10 10-4.48 10-10 10zm0-16c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6z"/></svg>', name: 'Toggle Watched Videos', stateKey: 'YTHWV_STATE', type: 'toggle', }, ]; const debounce = function (func, wait, immediate) { let timeout; return (...args) => { const later = () => { timeout = null; if (!immediate) func.apply(this, args); }; const callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(this, args); }; }; const findWatchedElements = () => { const watched = document.querySelectorAll([ '.ytd-thumbnail-overlay-resume-playback-renderer', '.ytThumbnailOverlayProgressBarHostWatchedProgressBarSegmentModern', ].join(','), ); const withThreshold = Array.from(watched).filter((bar) => { return ( bar && bar.style.width && Number.parseInt(bar.style.width, 10) >= WATCHED_THRESHOLD_PERCENT ); }); logDebug( `Found ${watched.length} watched elements, ${withThreshold.length} meeting threshold (${WATCHED_THRESHOLD_PERCENT}%)` ); return withThreshold; }; const findButtonAreaTarget = () => { return document.querySelector('#container #end #buttons'); }; const determineYoutubeSection = () => { const { href } = window.location; let youtubeSection = 'misc'; if (href.includes('/watch?')) { youtubeSection = 'watch'; } else if ( href.match(/.*\/(user|channel|c)\/.+\/videos/u) || href.match(/.*\/@.*/u) ) { youtubeSection = 'channel'; } else if (href.includes('/feed/subscriptions')) { youtubeSection = 'subscriptions'; } else if (href.includes('/feed/history')) { youtubeSection = 'history'; } else if (href.includes('/feed/trending')) { youtubeSection = 'trending'; } else if (href.includes('/playlist?')) { youtubeSection = 'playlist'; } else if (href.includes('/results?')) { youtubeSection = 'search'; } else if (href === 'https://www.youtube.com/' || href === 'http://www.youtube.com/') { youtubeSection = 'home'; } return youtubeSection; }; const calculateOpacity = (percentage) => { const sigmoidFactor = 4; const clampedPercent = Math.max(WATCHED_THRESHOLD_PERCENT, Math.min(100, percentage)); const normalizedPercentage = (clampedPercent - WATCHED_THRESHOLD_PERCENT) / (100 - WATCHED_THRESHOLD_PERCENT); const shiftedPercentage = (normalizedPercentage - 0.5) * sigmoidFactor; const sigmoid = 1 / (1 + Math.exp(-shiftedPercentage)); const invertedSigmoid = 1 - sigmoid; const opacity = MIN_DIM_OPACITY + invertedSigmoid * (MAX_DIM_OPACITY - MIN_DIM_OPACITY); return Math.max(MIN_DIM_OPACITY, Math.min(MAX_DIM_OPACITY, opacity)); }; const updateClassOnWatchedItems = async () => { document .querySelectorAll('.YT-HWV-WATCHED-HIDDEN') .forEach((el) => el.classList.remove('YT-HWV-WATCHED-HIDDEN')); const potentialItems = document.querySelectorAll( 'ytd-rich-item-renderer, ytd-grid-video-renderer, ytd-item-section-renderer, ytd-playlist-video-renderer, ytd-compact-video-renderer, ytd-playlist-panel-video-renderer, ytd-video-renderer' ); potentialItems.forEach(el => { if (el.style.opacity) { el.style.opacity = ''; } el.classList.remove('YT-HWV-HIDDEN-ROW-PARENT'); }); if (window.location.href.includes('/feed/history')) return; const section = determineYoutubeSection(); const state = await GM_getValue(`YTHWV_STATE_${section}`, 'normal'); if (state === 'normal') return; findWatchedElements().forEach((progressBarElement) => { const percentage = Number.parseInt(progressBarElement.style.width, 10); if (isNaN(percentage)) return; let itemToModify; let itemToDimOnly; if (section === 'subscriptions') { itemToModify = progressBarElement.closest('.ytd-rich-item-renderer') || progressBarElement.closest('.ytd-grid-video-renderer') || progressBarElement.closest('ytd-item-section-renderer'); } else if (section === 'playlist') { itemToModify = progressBarElement.closest('ytd-playlist-video-renderer'); } else if (section === 'watch') { itemToModify = progressBarElement.closest('ytd-compact-video-renderer'); if (itemToModify?.closest('ytd-compact-autoplay-renderer')) { itemToModify = null; } const watchedItemInPlaylistPanel = progressBarElement.closest('ytd-playlist-panel-video-renderer'); if (!itemToModify && watchedItemInPlaylistPanel) { itemToDimOnly = watchedItemInPlaylistPanel; } } else { itemToModify = progressBarElement.closest('ytd-rich-item-renderer') || progressBarElement.closest('ytd-video-renderer') || progressBarElement.closest('ytd-grid-video-renderer'); } if (itemToModify) { itemToModify.style.opacity = ''; itemToModify.classList.remove('YT-HWV-WATCHED-HIDDEN'); itemToModify.classList.remove('YT-HWV-HIDDEN-ROW-PARENT'); if (state === 'dimmed') { const opacity = calculateOpacity(percentage); itemToModify.style.opacity = opacity.toFixed(2); logDebug(`Dimming item: ${itemToModify.tagName} to opacity ${opacity.toFixed(2)} (${percentage}%)`); } } if (itemToDimOnly && state === 'dimmed') { itemToDimOnly.style.opacity = ''; itemToModify?.classList.remove('YT-HWV-WATCHED-HIDDEN'); const opacity = calculateOpacity(percentage); itemToDimOnly.style.opacity = opacity.toFixed(2); logDebug(`Dimming only item: ${itemToDimOnly.tagName} to opacity ${opacity.toFixed(2)} (${percentage}%)`); } }); }; const renderButtons = async () => { const target = findButtonAreaTarget(); if (!target) return; const existingButtons = target.parentNode.querySelector('.YT-HWV-BUTTONS'); const buttonArea = document.createElement('div'); buttonArea.classList.add('YT-HWV-BUTTONS'); for (const { icon, name, stateKey, type } of BUTTONS) { if (type === 'toggle') { const section = determineYoutubeSection(); if (section === 'history') { if (existingButtons) existingButtons.remove(); return; } const storageKey = `${stateKey}_${section}`; const toggleButtonState = await GM_getValue(storageKey, 'normal'); const button = document.createElement('button'); button.title = `${name} : currently "${toggleButtonState}" for section "${section}"`; button.classList.add('YT-HWV-BUTTON'); if (toggleButtonState === 'dimmed') { button.classList.add('YT-HWV-BUTTON-DISABLED'); } let currentIcon = icon; button.innerHTML = currentIcon; buttonArea.appendChild(button); button.addEventListener('click', async () => { const currentState = await GM_getValue(storageKey, 'normal'); logDebug(`Button ${name} clicked. Current state: ${currentState}, Section: ${section}`); let newState = 'dimmed'; if (currentState === 'dimmed') { newState = 'normal'; } logDebug(`Setting new state to: ${newState}`); await GM_setValue(storageKey, newState); await updateClassOnWatchedItems(); await renderButtons(); }); } } if (buttonArea.hasChildNodes()) { if (existingButtons) { if (existingButtons.innerHTML !== buttonArea.innerHTML) { target.parentNode.replaceChild(buttonArea, existingButtons); logDebug('Re-rendered menu buttons'); } } else { target.parentNode.insertBefore(buttonArea, target); logDebug('Rendered menu buttons'); } } else if (existingButtons) { existingButtons.remove(); logDebug('Removed menu buttons'); } }; const run = debounce(async (mutations) => { if ( mutations && mutations.length > 0 && mutations.every(m => m.target.classList?.contains('YT-HWV-BUTTON') || m.target.classList?.contains('YT-HWV-BUTTONS') || m.addedNodes?.[0]?.classList?.contains('YT-HWV-BUTTONS') || m.removedNodes?.[0]?.classList?.contains('YT-HWV-BUTTONS') ) ) { return; } logDebug('Running check for watched videos due to DOM change or initial load'); await updateClassOnWatchedItems(); await renderButtons(); }, 250); const send = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function (data) { this.addEventListener( 'readystatechange', function () { if (this.readyState === 4 && ( this.responseURL.includes('/browse_ajax') || this.responseURL.includes('/player') || this.responseURL.includes('/search') || this.responseURL.includes('/next') )) { logDebug(`AJAX detected (${this.responseURL}), scheduling update.`); setTimeout(run, 500); } }, false, ); send.call(this, data); }; const observeDOM = (() => { const MutationObserver = window.MutationObserver || window.WebKitMutationObserver; return (obj, callback) => { if (!obj || !MutationObserver) { console.warn('[YT-HWV] MutationObserver not available.'); return; } logDebug('Attaching DOM listener'); const obs = new MutationObserver((mutations) => { if (mutations.some(m => m.addedNodes.length > 0 || m.removedNodes.length > 0)) { callback(mutations); } }); obs.observe(obj, { childList: true, subtree: true }); }; })(); logDebug('Starting Script (Simplified GM with Variable Dimming)'); observeDOM(document.body, run); run(); })();