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.

目前為 2024-11-08 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Old Reddit with New Reddit Profile Pictures - Universal Version
// @namespace    https://github.com/Nick2bad4u/UserStyles
// @version      4.2
// @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");

  let profilePictureCache = JSON.parse(
    localStorage.getItem("profilePictureCache") || "{}",
  );
  let cacheTimestamps = JSON.parse(
    localStorage.getItem("cacheTimestamps") || "{}",
  );
  const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
  const MAX_CACHE_SIZE = 25000; // Maximum number of cache entries

  function saveCache() {
    localStorage.setItem(
      "profilePictureCache",
      JSON.stringify(profilePictureCache),
    );
    localStorage.setItem("cacheTimestamps", JSON.stringify(cacheTimestamps));
  }

  function flushOldCache() {
    console.log("Flushing old cache");
    const now = Date.now();
    for (const username in cacheTimestamps) {
      if (now - cacheTimestamps[username] > CACHE_DURATION) {
        console.log(`Deleting cache for ${username}`);
        delete profilePictureCache[username];
        delete cacheTimestamps[username];
      }
    }
    saveCache();
    console.log("Old cache entries flushed");
  }

  function limitCacheSize() {
    const cacheEntries = Object.keys(profilePictureCache);
    if (cacheEntries.length > MAX_CACHE_SIZE) {
      console.log("Cache size exceeded, removing oldest entries");
      const sortedEntries = cacheEntries.sort(
        (a, b) => cacheTimestamps[a] - cacheTimestamps[b],
      );
      const entriesToRemove = sortedEntries.slice(
        0,
        cacheEntries.length - MAX_CACHE_SIZE,
      );
      entriesToRemove.forEach((username) => {
        delete profilePictureCache[username];
        delete cacheTimestamps[username];
      });
      saveCache();
      console.log("Cache size limited");
    }
  }

  async function fetchProfilePictures(usernames) {
    console.log("Fetching profile pictures");
    const uncachedUsernames = usernames.filter(
      (username) =>
        !profilePictureCache[username] &&
        username !== "[deleted]" &&
        username !== "[removed]",
    );
    if (uncachedUsernames.length === 0) {
      console.log("All usernames are cached");
      return usernames.map((username) => profilePictureCache[username]);
    }

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

    const fetchPromises = uncachedUsernames.map(async (username) => {
      try {
        const response = await fetch(
          `https://www.reddit.com/user/${username}/about.json`,
        );
        if (!response.ok) {
          console.error(
            `Error fetching profile picture for ${username}: ${response.statusText}`,
          );
          return null;
        }
        const data = await response.json();
        if (data.data && data.data.icon_img) {
          const profilePictureUrl = data.data.icon_img.split("?")[0];
          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;
      }
    });

    const results = await Promise.all(fetchPromises);
    limitCacheSize();
    return usernames.map((username) => profilePictureCache[username]);
  }

  async function injectProfilePictures(comments) {
    console.log(`Comments found: ${comments.length}`);
    const usernames = Array.from(comments)
      .map((comment) => comment.textContent.trim())
      .filter(
        (username) => username !== "[deleted]" && username !== "[removed]",
      );
    const profilePictureUrls = await fetchProfilePictures(usernames);

    comments.forEach((comment, index) => {
      const username = usernames[index];
      const profilePictureUrl = profilePictureUrls[index];
      if (
        profilePictureUrl &&
        !comment.previousElementSibling?.classList.contains("profile-picture")
      ) {
        console.log(`Injecting profile picture: ${username}`);
        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");
        });
        comment.insertAdjacentElement("beforebegin", img);

        const enlargedImg = document.createElement("img");
        enlargedImg.src = profilePictureUrl;
        enlargedImg.classList.add("enlarged-profile-picture");
        document.body.appendChild(enlargedImg);
        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`;
        });
        img.addEventListener("mouseout", () => {
          enlargedImg.style.display = "none";
        });
      }
    });
    console.log("Profile pictures injected");
  }

  function setupObserver() {
    console.log("Setting up observer");
    const observer = new MutationObserver((mutations) => {
      const comments = document.querySelectorAll(".author, .c-username");
      if (comments.length > 0) {
        console.log("New comments detected");
        observer.disconnect();
        injectProfilePictures(comments);
      }
    });
    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
    console.log("Observer initialized");
  }

  function runScript() {
    flushOldCache();
    console.log("Cache loaded:", profilePictureCache);
    setupObserver();
  }

  window.addEventListener("load", () => {
    console.log("Page loaded");
    runScript();
    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);
})();