YouTube Detox
| // Basic test to verify script loading | ||
| console.log('YouTube Detox: RYD script starting'); | ||
| document.addEventListener('DOMContentLoaded', function() { | ||
| console.log('YouTube Detox: DOM Content Loaded'); | ||
| }); | ||
| // Also try with load event | ||
| window.addEventListener('load', function() { | ||
| console.log('YouTube Detox: Window Loaded'); | ||
| }); | ||
| // Return YouTube Dislike API integration | ||
| const RYD_API_URL = 'https://returnyoutubedislikeapi.com/votes'; | ||
| // Cache to store dislike counts and reduce API calls | ||
| const dislikeCache = new Map(); | ||
| const CACHE_EXPIRY = 1000 * 60 * 60; // 1 hour in milliseconds | ||
| // Fetch dislike count for a video | ||
| async function fetchDislikes(videoId) { | ||
| try { | ||
| const cachedData = dislikeCache.get(videoId); | ||
| if (cachedData) return cachedData.dislikes; | ||
| const response = await new Promise((resolve, reject) => { | ||
| const xhr = new XMLHttpRequest(); | ||
| xhr.open('GET', `${RYD_API_URL}?videoId=${videoId}`); | ||
| xhr.onload = () => xhr.status === 200 ? resolve(xhr.responseText) : reject(); | ||
| xhr.onerror = reject; | ||
| xhr.send(); | ||
| }); | ||
| const data = JSON.parse(response); | ||
| if (!data?.dislikes) return null; | ||
| dislikeCache.set(videoId, { dislikes: data.dislikes }); | ||
| return data.dislikes; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| // Format number for display (e.g., 1.5K, 1.2M, or 1,5 млн, 1,2 тыс. for Russian) | ||
| function formatNumber(number) { | ||
| const isRussian = document.documentElement.lang === 'ru-RU'; | ||
| if (isRussian) { | ||
| if (number >= 1000000) { | ||
| // Use comma as decimal separator for Russian locale | ||
| return (number / 1000000).toFixed(1).replace('.', ',') + ' млн'; | ||
| } | ||
| if (number >= 1000) { | ||
| return (number / 1000).toFixed(1).replace('.', ',') + ' тыс.'; | ||
| } | ||
| return number.toString(); | ||
| } else { | ||
| if (number >= 1000000) return (number / 1000000).toFixed(1) + 'M'; | ||
| if (number >= 1000) return (number / 1000).toFixed(1) + 'K'; | ||
| return number.toString(); | ||
| } | ||
| } | ||
| // Wait for an element to be present in the DOM | ||
| function waitForElement(selector, timeout = 5000) { | ||
| return new Promise((resolve, reject) => { | ||
| if (document.querySelector(selector)) { | ||
| return resolve(document.querySelector(selector)); | ||
| } | ||
| const observer = new MutationObserver(() => { | ||
| if (document.querySelector(selector)) { | ||
| observer.disconnect(); | ||
| resolve(document.querySelector(selector)); | ||
| } | ||
| }); | ||
| observer.observe(document.body, { | ||
| childList: true, | ||
| subtree: true | ||
| }); | ||
| setTimeout(() => { | ||
| observer.disconnect(); | ||
| reject(new Error(`Timeout waiting for ${selector}`)); | ||
| }, timeout); | ||
| }); | ||
| } | ||
| // Update dislike count in the UI | ||
| async function updateDislikeUI(dislikeCount) { | ||
| try { | ||
| // Wait for the dislike button view model | ||
| const dislikeButton = await waitForElement('dislike-button-view-model'); | ||
| if (!dislikeButton) return; | ||
| // Find the button element within the view model structure | ||
| const button = dislikeButton.querySelector('button.yt-spec-button-shape-next'); | ||
| if (!button) return; | ||
| // Find existing text content container or create new one | ||
| let textContainer = button.querySelector('.yt-spec-button-shape-next__button-text-content'); | ||
| if (!textContainer) { | ||
| textContainer = document.createElement('div'); | ||
| textContainer.className = 'yt-spec-button-shape-next__button-text-content'; | ||
| textContainer.style.marginLeft = '0px'; | ||
| // Insert after the icon | ||
| const iconDiv = button.querySelector('.yt-spec-button-shape-next__icon'); | ||
| if (iconDiv && iconDiv.nextSibling) { | ||
| button.insertBefore(textContainer, iconDiv.nextSibling); | ||
| } else { | ||
| button.appendChild(textContainer); | ||
| } | ||
| } | ||
| // Find or create the dislike count span | ||
| let dislikeText = textContainer.querySelector('.ytd-dislike-count'); | ||
| if (!dislikeText) { | ||
| dislikeText = document.createElement('span'); | ||
| dislikeText.className = 'ytd-dislike-count'; | ||
| textContainer.appendChild(dislikeText); | ||
| } | ||
| // Update the text and aria-label | ||
| if (dislikeCount !== null) { | ||
| const formattedCount = formatNumber(dislikeCount); | ||
| dislikeText.textContent = formattedCount; | ||
| button.setAttribute('aria-label', `${button.getAttribute('aria-label')} и ещё ${formattedCount} пользователям`); | ||
| } | ||
| } catch (error) { | ||
| console.error('[RYD] Error updating UI:', error); | ||
| } | ||
| } | ||
| // Extract video ID from URL | ||
| function getVideoId(url = window.location.href) { | ||
| try { | ||
| const urlObj = new URL(url); | ||
| return urlObj.searchParams.get('v'); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| // Main function to handle dislike display | ||
| async function handleDislikes() { | ||
| const videoId = getVideoId(); | ||
| if (!videoId) { | ||
| console.log('[RYD] No video ID found'); | ||
| return; | ||
| } | ||
| console.log('[RYD] Processing video:', videoId); | ||
| try { | ||
| await waitForElement('ytd-watch-flexy'); | ||
| const dislikes = await fetchDislikes(videoId); | ||
| await updateDislikeUI(dislikes); | ||
| } catch (error) { | ||
| console.error('[RYD] Error:', error); | ||
| } | ||
| } | ||
| // Initialize | ||
| document.addEventListener('yt-navigate-finish', handleDislikes); | ||
| // Initial load | ||
| if (document.readyState === 'loading') { | ||
| document.addEventListener('DOMContentLoaded', handleDislikes); | ||
| } else { | ||
| handleDislikes(); | ||
| } |
+11
-0
@@ -7,2 +7,13 @@ # Changelog | ||
| ## [1.2.0] - 2025-05-18 | ||
| ### Added | ||
| - Restored dislike counter functionality using Return YouTube Dislike API | ||
| - Seamless integration of dislike counts into YouTube's native interface | ||
| - Efficient caching system for dislike data | ||
| ## [1.1.2] - 2025-05-17 | ||
| ### Fixed | ||
| - Enhanced Shorts removal by hiding additional rich shelf elements in feeds | ||
| - Added comprehensive selectors to catch all variations of Shorts shelves | ||
| ## [1.1.1] - 2025-05-17 | ||
@@ -9,0 +20,0 @@ ### Fixed |
@@ -100,18 +100,5 @@ // Redirect shorts, home page and trending to subscription feed | ||
| // Block programmatic navigation to shorts and handle home redirects | ||
| const pushState = history.pushState; | ||
| history.pushState = function() { | ||
| pushState.apply(history, arguments); | ||
| setTimeout(handleNavigation, 0); | ||
| }; | ||
| const replaceState = history.replaceState; | ||
| replaceState = function() { | ||
| replaceState.apply(history, arguments); | ||
| setTimeout(handleNavigation, 0); | ||
| }; | ||
| // Also handle popstate events (browser back/forward buttons) | ||
| window.addEventListener('popstate', () => { | ||
| setTimeout(handleNavigation, 0); | ||
| }); | ||
| // Handle history changes without modifying the original methods | ||
| window.addEventListener('pushState', handleNavigation); | ||
| window.addEventListener('replaceState', handleNavigation); | ||
| window.addEventListener('popstate', handleNavigation); |
+6
-4
| { | ||
| "manifest_version": 2, | ||
| "name": "YouTube Detox", | ||
| "version": "1.1.1", | ||
| "version": "1.2.0", | ||
| "homepage_url": "https://github.com/lemjoe/yt-detox", | ||
@@ -19,3 +19,4 @@ "description": "Transform YouTube into a focused, distraction-free space. Remove Shorts, recommendations, and stay in control of your viewing experience.", | ||
| "permissions": [ | ||
| "*://*.youtube.com/*" | ||
| "*://*.youtube.com/*", | ||
| "*://*.returnyoutubedislikeapi.com/*" | ||
| ], | ||
@@ -31,7 +32,8 @@ "content_scripts": [ | ||
| "js": [ | ||
| "content-scripts/youtube-detox.js" | ||
| "content-scripts/youtube-detox.js", | ||
| "content-scripts/return-youtube-dislike.js" | ||
| ], | ||
| "run_at": "document_start" | ||
| "run_at": "document_end" | ||
| } | ||
| ] | ||
| } |
+9
-2
@@ -21,2 +21,8 @@ # YouTube Detox | ||
| - **Restored Dislike Counter** | ||
| - Brings back the dislike count using Return YouTube Dislike API | ||
| - Seamlessly integrates with YouTube's native interface | ||
| - Shows accurate community feedback for better content evaluation | ||
| - Includes efficient caching for faster loading | ||
| - **Clean, Focused Interface** | ||
@@ -45,3 +51,3 @@ - Centers video content for better viewing experience | ||
| For permanent installation, check the Firefox Add-ons store - this extension might already be available there! | ||
| For permanent installation, check the [Firefox Add-ons store](https://addons.mozilla.org/addon/yt-detox/)! | ||
@@ -53,3 +59,4 @@ ## Development | ||
| - `styles/youtube-detox.css`: CSS rules for hiding elements and layout modifications | ||
| - `content-scripts/youtube-detox.js`: JavaScript for handling dynamic content, navigation, and functionality | ||
| - `content-scripts/youtube-detox.js`: JavaScript for handling dynamic content and navigation | ||
| - `content-scripts/return-youtube-dislike.js`: JavaScript for dislike counter functionality | ||
@@ -56,0 +63,0 @@ ## Contributing |
@@ -49,2 +49,24 @@ /* Hide the entire secondary content area */ | ||
| /* Hide Shorts shelves and sections */ | ||
| ytd-rich-shelf-renderer:has(#dismissible), | ||
| ytd-rich-shelf-renderer:has(#rich-shelf-header-container), | ||
| ytd-rich-shelf-renderer:has(#title-text[title="Shorts"]), | ||
| ytd-rich-section-renderer:has(#title-text[title="Shorts"]), | ||
| ytd-reel-shelf-renderer { | ||
| display: none !important; | ||
| } | ||
| /* Style for dislike button */ | ||
| dislike-button-view-model button.yt-spec-button-shape-next { | ||
| width: auto !important; | ||
| min-width: 32px; | ||
| } | ||
| /* Style for dislike button text content */ | ||
| .yt-spec-button-shape-next__button-text-content { | ||
| margin-left: 0; | ||
| min-width: 24px; | ||
| padding: 0 6px; | ||
| } | ||
| /* Hide unwanted sidebar elements */ | ||
@@ -51,0 +73,0 @@ ytd-guide-entry-renderer[aria-label="Home"], |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet