Old Reddit with New Reddit Profile Pictures - Universal Version

Injects new Reddit profile pictures into Old Reddit and Reddit-Stream.com next to the username. Caches in localstorage.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Old Reddit with New Reddit Profile Pictures - Universal Version
// @namespace    typpi.online
// @version      7.0.7
// @description  Injects new Reddit profile pictures into Old Reddit and Reddit-Stream.com next to the username. Caches in localstorage.
// @author       Nick2bad4u
// @match        *://*.reddit.com/*
// @match        *://reddit-stream.com/*
// @connect      reddit.com
// @connect      reddit-stream.com
// @grant        GM_xmlhttpRequest
// @homepageURL  https://github.com/Nick2bad4u/UserStyles
// @license      Unlicense
// @resource     https://www.google.com/s2/favicons?sz=64&domain=reddit.com
// @icon         https://www.google.com/s2/favicons?sz=64&domain=reddit.com
// @icon64       https://www.google.com/s2/favicons?sz=64&domain=reddit.com
// @run-at       document-start
// @tag          reddit
// ==/UserScript==

(function () {
	'use strict';
	console.log('Script loaded');

	// Initialize the profile picture cache from localStorage or as an empty object if not present
	let profilePictureCache = JSON.parse(localStorage.getItem('profilePictureCache') || '{}');

	// Initialize the cache timestamps from localStorage or as an empty object if not present
	let cacheTimestamps = JSON.parse(localStorage.getItem('cacheTimestamps') || '{}');

	// Define the cache duration as 7 days in milliseconds
	const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds

	// Define the maximum number of cache entries
	const MAX_CACHE_SIZE = 25000; // Maximum number of cache entries

	// Function to save the current state of the cache to localStorage
	function saveCache() {
		localStorage.setItem('profilePictureCache', JSON.stringify(profilePictureCache));
		localStorage.setItem('cacheTimestamps', JSON.stringify(cacheTimestamps));
	}

	// Function to flush old cache entries
	function flushOldCache() {
		console.log('Flushing old cache');
		const now = Date.now();

		// Iterate over all usernames in the cache timestamps
		for (const username in cacheTimestamps) {
			// Check if the cache entry is older than the allowed cache duration
			if (now - cacheTimestamps[username] > CACHE_DURATION) {
				console.log(`Deleting cache for ${username}`);
				// Delete the cache entry for this username
				delete profilePictureCache[username];
				// Delete the timestamp for this username
				delete cacheTimestamps[username];
			}
		}

		// Save the updated cache state to localStorage
		saveCache();
		console.log('Old cache entries flushed');
	}

	// Function to limit the size of the profile picture cache
	function limitCacheSize() {
		// Get an array of all usernames currently in the cache
		const cacheEntries = Object.keys(profilePictureCache);

		// Check if the cache size exceeds the maximum allowed size
		if (cacheEntries.length > MAX_CACHE_SIZE) {
			console.log('Cache size exceeded, removing oldest entries');

			// Sort the cache entries by their timestamps in ascending order
			const sortedEntries = cacheEntries.sort((a, b) => cacheTimestamps[a] - cacheTimestamps[b]);

			// Determine the number of entries to remove to fit within the maximum cache size
			const entriesToRemove = sortedEntries.slice(0, cacheEntries.length - MAX_CACHE_SIZE);

			// Remove the oldest entries from the cache and timestamps
			entriesToRemove.forEach((username) => {
				delete profilePictureCache[username];
				delete cacheTimestamps[username];
			});

			// Save the updated cache state
			saveCache();
			console.log('Cache size limited');
		}
	}

	// Asynchronous function to fetch profile pictures for a list of usernames
	async function fetchProfilePictures(usernames) {
		console.log('Fetching profile pictures');

		// Filter out usernames that are already in the cache or are marked as deleted or removed
		const uncachedUsernames = usernames.filter((username) => !profilePictureCache[username] && username !== '[deleted]' && username !== '[removed]');

		// If all usernames are cached, return the cached profile pictures
		if (uncachedUsernames.length === 0) {
			console.log('All usernames are cached');
			return usernames.map((username) => profilePictureCache[username]);
		}

		console.log(`Fetching profile pictures for: ${uncachedUsernames.join(', ')}`);

		// Map over the uncached usernames and fetch their profile pictures
		const fetchPromises = uncachedUsernames.map(async (username) => {
			try {
				// Fetch user data from Reddit
				const response = await fetch(`https://www.reddit.com/user/${username}/about.json`);

				// Check if the response is successful
				if (!response.ok) {
					console.error(`Error fetching profile picture for ${username}: ${response.statusText}`);
					return null;
				}

				// Parse the response JSON data
				const data = await response.json();

				// Check if the data contains a profile picture URL
				if (data.data && data.data.icon_img) {
					const profilePictureUrl = data.data.icon_img.split('?')[0];

					// Cache the profile picture URL and timestamp
					profilePictureCache[username] = profilePictureUrl;
					cacheTimestamps[username] = Date.now();
					saveCache();
					console.log(`Fetched profile picture: ${username}`);
					return profilePictureUrl;
				} else {
					console.warn(`No profile picture found for: ${username}`);
					return null;
				}
			} catch (error) {
				console.error(`Error fetching profile picture for ${username}:`, error);
				return null;
			}
		});

		// Wait for all fetch promises to resolve
		// eslint-disable-next-line @typescript-eslint/no-unused-vars
		const results = await Promise.all(fetchPromises);

		// Limit the cache size if necessary
		limitCacheSize();

		// Return the profile pictures, using the cache for already fetched usernames
		return usernames.map((username) => profilePictureCache[username]);
	}

	// Asynchronous function to inject profile pictures into comments
	async function injectProfilePictures(comments) {
		console.log(`Comments found: ${comments.length}`);

		// Extract usernames from comments, filtering out deleted or removed comments
		const usernames = Array.from(comments)
			.map((comment) => comment.textContent.trim())
			.filter((username) => username !== '[deleted]' && username !== '[removed]');

		// Fetch profile pictures for the extracted usernames
		const profilePictureUrls = await fetchProfilePictures(usernames);

		// Iterate over each comment and inject the corresponding profile picture
		comments.forEach((comment, index) => {
			const username = usernames[index];
			const profilePictureUrl = profilePictureUrls[index];

			// Check if the profile picture URL is valid and the profile picture is not already injected
			if (profilePictureUrl && !comment.previousElementSibling?.classList.contains('profile-picture')) {
				console.log(`Injecting profile picture: ${username}`);

				// Create and configure the img element for the profile picture
				const img = document.createElement('img');
				img.src = profilePictureUrl;
				img.classList.add('profile-picture');
				img.onerror = () => {
					img.style.display = 'none';
				};
				img.addEventListener('click', () => {
					window.open(profilePictureUrl, '_blank');
				});

				// Insert the profile picture before the comment element
				comment.insertAdjacentElement('beforebegin', img);

				// Create an enlarged version of the profile picture for hover effect
				const enlargedImg = document.createElement('img');
				enlargedImg.src = profilePictureUrl;
				enlargedImg.classList.add('enlarged-profile-picture');
				document.body.appendChild(enlargedImg);

				// Show the enlarged profile picture on mouseover
				img.addEventListener('mouseover', () => {
					enlargedImg.style.display = 'block';
					const rect = img.getBoundingClientRect();
					enlargedImg.style.top = `${rect.top + window.scrollY + 20}px`;
					enlargedImg.style.left = `${rect.left + window.scrollX + 20}px`;
				});

				// Hide the enlarged profile picture on mouseout
				img.addEventListener('mouseout', () => {
					enlargedImg.style.display = 'none';
				});
			}
		});

		console.log('Profile pictures injected');
	}

	// Function to set up a MutationObserver to detect new comments
	function setupObserver() {
		console.log('Setting up observer');

		// Create a new MutationObserver instance and define the callback function
		// eslint-disable-next-line @typescript-eslint/no-unused-vars
		const observer = new MutationObserver((mutations) => {
			// Select all elements with the classes 'author' or 'c-username'
			const comments = document.querySelectorAll('.author, .c-username');

			// If new comments are detected, disconnect the observer and inject profile pictures
			if (comments.length > 0) {
				console.log('New comments detected');
				observer.disconnect();
				injectProfilePictures(comments);
			}
		});

		// Start observing the document body for changes in the child elements and subtree
		observer.observe(document.body, {
			childList: true,
			subtree: true,
		});

		console.log('Observer initialized');
	}

	// Function to run the script
	function runScript() {
		// Flush old cache data
		flushOldCache();

		// Log the current state of the profile picture cache
		console.log('Cache loaded:', profilePictureCache);

		// Set up a MutationObserver to detect new comments
		setupObserver();
	}

	// Add an event listener for the 'load' event to ensure the script runs after the page has fully loaded
	window.addEventListener('load', () => {
		console.log('Page loaded');

		// Run the script immediately after the page loads
		runScript();

		// Set an interval to run the script every 10 seconds
		setInterval(runScript, 10000); // Run every 10 seconds
	});
	const style = document.createElement('style');
	style.textContent = `
        .profile-picture {
            width: 20px;
            height: 20px;
            border-radius: 50%;
            margin-right: 5px;
            transition: transform 0.2s ease-in-out;
            position: relative;
            z-index: 1;
            cursor: pointer;
        }
        .enlarged-profile-picture {
            width: 250px;
            height: 250px;
            border-radius: 50%;
            position: absolute;
            display: none;
            z-index: 1000;
            pointer-events: none;
            outline: 3px solid #000;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 1);
            background-color: rgba(0, 0, 0, 1);
        }
    `;
	document.head.appendChild(style);
})();