Enhance YouTube Profile Pictures (HD Version with Caching)

Enlarges YouTube profile pictures on mouse over, shows HD version, Caches HD images for faster display using localStorage caching. Enlarges profile picture when a creator hearts a comment.

  1. // ==UserScript==
  2. // @name Enhance YouTube Profile Pictures (HD Version with Caching)
  3. // @namespace typpi.online
  4. // @version 5.3
  5. // @description Enlarges YouTube profile pictures on mouse over, shows HD version, Caches HD images for faster display using localStorage caching. Enlarges profile picture when a creator hearts a comment.
  6. // @author Nick2bad4u
  7. // @match https://www.youtube.com/*
  8. // @grant none
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  10. // @license UnLicense
  11. // @tag youtube
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. let debounceTimeout;
  18. const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
  19. const preloadedImages = new Map();
  20.  
  21. // Load cache from localStorage
  22. function loadCache() {
  23. const cache = JSON.parse(localStorage.getItem('profilePicCache') || '{}');
  24. const now = Date.now();
  25. // Clear out expired cache entries
  26. Object.keys(cache).forEach((key) => {
  27. if (now - cache[key].timestamp > CACHE_TTL_MS) {
  28. delete cache[key]; // Remove expired entry
  29. }
  30. });
  31. localStorage.setItem('profilePicCache', JSON.stringify(cache)); // Update cache after removing expired entries
  32. return cache;
  33. }
  34.  
  35. // Save cache to localStorage
  36. function saveCache(cache) {
  37. localStorage.setItem('profilePicCache', JSON.stringify(cache));
  38. }
  39.  
  40. let cache = loadCache(); // Load the cache once when the script runs
  41.  
  42. // Preload HD image
  43. function preloadHDImage(src) {
  44. const hdSrc = src.replace(/=s(32|88|48)-c/, '=s800-c'); // Adjust as needed for HD
  45. if (!preloadedImages.has(hdSrc)) {
  46. if (cache[hdSrc]) {
  47. // If in persistent cache, load directly from cache
  48. preloadedImages.set(hdSrc, cache[hdSrc].url);
  49. } else {
  50. // Preload HD image and store in cache
  51. const img = new Image();
  52. img.src = hdSrc;
  53. preloadedImages.set(hdSrc, hdSrc); // Store in memory
  54. cache[hdSrc] = {
  55. url: hdSrc,
  56. timestamp: Date.now(),
  57. }; // Cache with timestamp
  58. saveCache(cache); // Save the updated cache
  59. }
  60. }
  61. }
  62.  
  63. // Function to enlarge profile pictures, show HD image, add black outline, and shift position
  64. function enlargeProfilePic(event) {
  65. clearTimeout(debounceTimeout);
  66.  
  67. const img = event.target;
  68.  
  69. // If the image is already enlarged, skip further processing
  70. if (img.dataset.enlarged === 'true') return;
  71.  
  72. debounceTimeout = setTimeout(() => {
  73. const originalSrc = img.src;
  74. const hdSrc = originalSrc.replace(/=s(32|88|48)-c/, '=s800-c'); // Increase the size to 800px
  75. img.dataset.originalSrc = originalSrc; // Store the original src
  76. img.src = preloadedImages.get(hdSrc) || hdSrc;
  77.  
  78. // Get the position of the original image
  79. const rect = img.getBoundingClientRect();
  80.  
  81. // Set fixed size, position relative to the original image
  82. if (
  83. img.classList.contains(
  84. 'h-5.w-5.inline.align-middle.rounded-full.flex-none',
  85. )
  86. ) {
  87. img.style.transform = 'scale(6) translateX(20px)';
  88. img.style.transition = 'transform 0.2s ease';
  89. img.style.border = '1px solid black';
  90. img.style.zIndex = '9999';
  91. img.style.position = 'relative';
  92. } else {
  93. img.style.width = '260px'; // Adjust width as needed
  94. img.style.height = '260px'; // Adjust height as needed
  95. img.style.borderRadius = '50%'; // Make the image circular
  96. img.style.position = 'fixed';
  97. img.style.top = `${rect.top - 20}px`; // Adjust vertical position as needed
  98. img.style.left = `${rect.left + 70}px`; // Offset to the right
  99. img.style.border = '2px solid black';
  100. img.style.zIndex = '9999';
  101. }
  102.  
  103. img.dataset.enlarged = 'true'; // Mark as enlarged to prevent re-enlarging
  104.  
  105. // Reset after 3 seconds
  106. setTimeout(() => {
  107. resetProfilePic(img);
  108. }, 3000);
  109. }, 100);
  110. }
  111.  
  112. // Function to reset profile pictures to original size and source
  113. function resetProfilePic(img) {
  114. img.src = img.dataset.originalSrc || img.src; // Restore the original src if it was replaced
  115. img.style.width = ''; // Clear custom width
  116. img.style.height = ''; // Clear custom height
  117. img.style.borderRadius = ''; // Clear circular style
  118. img.style.position = ''; // Reset position to default
  119. img.style.top = ''; // Clear top position
  120. img.style.left = ''; // Clear left position
  121. img.style.border = 'none'; // Remove any border
  122. img.style.zIndex = 'auto'; // Reset z-index
  123. img.style.transform = ''; // Remove any transform applied
  124. delete img.dataset.enlarged; // Remove the enlarged flag
  125. }
  126.  
  127. // Add event listeners to profile pictures
  128. function addEventListeners() {
  129. const profilePicsChat = document.querySelectorAll(
  130. '.h-5.w-5.inline.align-middle.rounded-full.flex-none',
  131. );
  132. const profilePicsComments = document.querySelectorAll(
  133. '.style-scope yt-img-shadow img:not(#avatar-btn > yt-img-shadow img)',
  134. );
  135. const heartedThumbnails = document.querySelectorAll(
  136. '#creator-heart-button yt-img-shadow img, #creator-heart-button img',
  137. );
  138.  
  139. profilePicsChat.forEach((pic) => {
  140. preloadHDImage(pic.src); // Preload HD image
  141. pic.addEventListener('mouseenter', enlargeProfilePic);
  142. });
  143.  
  144. profilePicsComments.forEach((pic) => {
  145. preloadHDImage(pic.src); // Preload HD image
  146. pic.addEventListener('mouseenter', enlargeProfilePic);
  147. });
  148.  
  149. heartedThumbnails.forEach((pic) => {
  150. preloadHDImage(pic.src); // Preload HD image
  151. pic.addEventListener('mouseenter', enlargeProfilePic); // Add hover event
  152. });
  153. }
  154.  
  155. // Observe changes in the chat and comments section to dynamically add event listeners
  156. const observer = new MutationObserver((mutations) => {
  157. mutations.forEach((mutation) => {
  158. if (mutation.addedNodes.length > 0) {
  159. addEventListeners();
  160. }
  161. });
  162. });
  163. observer.observe(document.body, {
  164. childList: true,
  165. subtree: true,
  166. attributes: true, // Observe attribute changes
  167. attributeFilter: ['src'], // Only track changes in `src` attribute
  168. });
  169.  
  170. // Initial call to add event listeners
  171. addEventListeners();
  172. })();