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.

// ==UserScript==
// @name         Enhance YouTube Profile Pictures (HD Version with Caching)
// @namespace    typpi.online
// @version      5.3
// @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.
// @author       Nick2bad4u
// @match        https://www.youtube.com/*
// @grant        none
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @license      UnLicense
// @tag          youtube
// ==/UserScript==

(function () {
	'use strict';

	let debounceTimeout;
	const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
	const preloadedImages = new Map();

	// Load cache from localStorage
	function loadCache() {
		const cache = JSON.parse(localStorage.getItem('profilePicCache') || '{}');
		const now = Date.now();
		// Clear out expired cache entries
		Object.keys(cache).forEach((key) => {
			if (now - cache[key].timestamp > CACHE_TTL_MS) {
				delete cache[key]; // Remove expired entry
			}
		});
		localStorage.setItem('profilePicCache', JSON.stringify(cache)); // Update cache after removing expired entries
		return cache;
	}

	// Save cache to localStorage
	function saveCache(cache) {
		localStorage.setItem('profilePicCache', JSON.stringify(cache));
	}

	let cache = loadCache(); // Load the cache once when the script runs

	// Preload HD image
	function preloadHDImage(src) {
		const hdSrc = src.replace(/=s(32|88|48)-c/, '=s800-c'); // Adjust as needed for HD
		if (!preloadedImages.has(hdSrc)) {
			if (cache[hdSrc]) {
				// If in persistent cache, load directly from cache
				preloadedImages.set(hdSrc, cache[hdSrc].url);
			} else {
				// Preload HD image and store in cache
				const img = new Image();
				img.src = hdSrc;
				preloadedImages.set(hdSrc, hdSrc); // Store in memory
				cache[hdSrc] = {
					url: hdSrc,
					timestamp: Date.now(),
				}; // Cache with timestamp
				saveCache(cache); // Save the updated cache
			}
		}
	}

	// Function to enlarge profile pictures, show HD image, add black outline, and shift position
	function enlargeProfilePic(event) {
		clearTimeout(debounceTimeout);

		const img = event.target;

		// If the image is already enlarged, skip further processing
		if (img.dataset.enlarged === 'true') return;

		debounceTimeout = setTimeout(() => {
			const originalSrc = img.src;
			const hdSrc = originalSrc.replace(/=s(32|88|48)-c/, '=s800-c'); // Increase the size to 800px
			img.dataset.originalSrc = originalSrc; // Store the original src
			img.src = preloadedImages.get(hdSrc) || hdSrc;

			// Get the position of the original image
			const rect = img.getBoundingClientRect();

			// Set fixed size, position relative to the original image
			if (
				img.classList.contains(
					'h-5.w-5.inline.align-middle.rounded-full.flex-none',
				)
			) {
				img.style.transform = 'scale(6) translateX(20px)';
				img.style.transition = 'transform 0.2s ease';
				img.style.border = '1px solid black';
				img.style.zIndex = '9999';
				img.style.position = 'relative';
			} else {
				img.style.width = '260px'; // Adjust width as needed
				img.style.height = '260px'; // Adjust height as needed
				img.style.borderRadius = '50%'; // Make the image circular
				img.style.position = 'fixed';
				img.style.top = `${rect.top - 20}px`; // Adjust vertical position as needed
				img.style.left = `${rect.left + 70}px`; // Offset to the right
				img.style.border = '2px solid black';
				img.style.zIndex = '9999';
			}

			img.dataset.enlarged = 'true'; // Mark as enlarged to prevent re-enlarging

			// Reset after 3 seconds
			setTimeout(() => {
				resetProfilePic(img);
			}, 3000);
		}, 100);
	}

	// Function to reset profile pictures to original size and source
	function resetProfilePic(img) {
		img.src = img.dataset.originalSrc || img.src; // Restore the original src if it was replaced
		img.style.width = ''; // Clear custom width
		img.style.height = ''; // Clear custom height
		img.style.borderRadius = ''; // Clear circular style
		img.style.position = ''; // Reset position to default
		img.style.top = ''; // Clear top position
		img.style.left = ''; // Clear left position
		img.style.border = 'none'; // Remove any border
		img.style.zIndex = 'auto'; // Reset z-index
		img.style.transform = ''; // Remove any transform applied
		delete img.dataset.enlarged; // Remove the enlarged flag
	}

	// Add event listeners to profile pictures
	function addEventListeners() {
		const profilePicsChat = document.querySelectorAll(
			'.h-5.w-5.inline.align-middle.rounded-full.flex-none',
		);
		const profilePicsComments = document.querySelectorAll(
			'.style-scope yt-img-shadow img:not(#avatar-btn > yt-img-shadow img)',
		);
		const heartedThumbnails = document.querySelectorAll(
			'#creator-heart-button yt-img-shadow img, #creator-heart-button img',
		);

		profilePicsChat.forEach((pic) => {
			preloadHDImage(pic.src); // Preload HD image
			pic.addEventListener('mouseenter', enlargeProfilePic);
		});

		profilePicsComments.forEach((pic) => {
			preloadHDImage(pic.src); // Preload HD image
			pic.addEventListener('mouseenter', enlargeProfilePic);
		});

		heartedThumbnails.forEach((pic) => {
			preloadHDImage(pic.src); // Preload HD image
			pic.addEventListener('mouseenter', enlargeProfilePic); // Add hover event
		});
	}

	// Observe changes in the chat and comments section to dynamically add event listeners
	const observer = new MutationObserver((mutations) => {
		mutations.forEach((mutation) => {
			if (mutation.addedNodes.length > 0) {
				addEventListeners();
			}
		});
	});
	observer.observe(document.body, {
		childList: true,
		subtree: true,
		attributes: true, // Observe attribute changes
		attributeFilter: ['src'], // Only track changes in `src` attribute
	});

	// Initial call to add event listeners
	addEventListeners();
})();