1337x – Steam Hover Preview

On-hover Steam thumbnail & description for 1337x torrent titles (cleans up names for better matches)

目前为 2025-04-24 提交的版本。查看 最新版本

// ==UserScript==
// @name         1337x – Steam Hover Preview
// @namespace    https://greasyfork.org/users/youruserid
// @version      1.1
// @description  On-hover Steam thumbnail & description for 1337x torrent titles (cleans up names for better matches)
// @author       Deon Holo
// @license      MIT
// @match        *://1337x.to/*
// @match        *://1337x.st/*
// @match        *://1337x.ws/*
// @match        *://1337x.eu/*
// @match        *://1337x.se/*
// @match        *://1337x.is/*
// @match        *://1337x.gd/*
// @grant        GM_xmlhttpRequest
// @connect      store.steampowered.com
// @run-at       document-idle
// ==/UserScript==

;(function(){
  'use strict';

  // create tooltip
  const tip = document.createElement('div');
  Object.assign(tip.style, {
    position      : 'fixed',
    padding       : '8px',
    background    : 'rgba(255,255,255,0.95)',
    border        : '1px solid #444',
    borderRadius  : '4px',
    boxShadow     : '0 2px 6px rgba(0,0,0,0.2)',
    zIndex        : 2147483647,
    maxWidth      : '300px',
    fontSize      : '12px',
    lineHeight    : '1.3',
    display       : 'none',
    pointerEvents : 'none'
  });
  document.body.appendChild(tip);

  let hoverCounter = 0;
  const cache = new Map();

  function gmFetch(url){
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method       : 'GET',
        url,
        responseType : 'json',
        onload       : r => resolve(r.response),
        onerror      : e => reject(e)
      });
    });
  }

  async function fetchSteam(name){
    if (cache.has(name)) return cache.get(name);

    let search;
    try {
      search = await gmFetch(
        `https://store.steampowered.com/api/storesearch/?cc=us&l=en&term=${encodeURIComponent(name)}`
      );
    } catch {
      return null;
    }
    const id = search.items?.[0]?.id;
    if (!id) return null;

    let details;
    try {
      details = await gmFetch(
        `https://store.steampowered.com/api/appdetails?appids=${id}&cc=us&l=en`
      );
    } catch {
      return null;
    }
    const data = details[id]?.data;
    cache.set(name, data);
    return data;
  }

  function cleanName(raw){
    let name = raw.trim();
    // split at dash, slash, bracket, 'Update' or 'Edition'
    name = name.split(/(?:[-\/\(\[]|Update|Edition)/i)[0].trim();
    // strip off version suffix
    name = name.replace(/ v[\d.].*$/i, '').trim();
    return name;
  }

  function positionTip(e){
    let x = e.clientX + 12;
    let y = e.clientY + 12;
    const w = tip.offsetWidth, h = tip.offsetHeight;
    if (x + w > window.innerWidth)  x = window.innerWidth  - w - 8;
    if (y + h > window.innerHeight) y = window.innerHeight - h - 8;
    tip.style.left = x + 'px';
    tip.style.top  = y + 'px';
  }

  async function showTip(e){
    const thisHover = ++hoverCounter;
    const raw       = e.target.textContent;
    const name      = cleanName(raw);

    tip.innerHTML     = `<p>Loading <strong>${name}</strong>…</p>`;
    tip.style.display = 'block';
    positionTip(e);

    // smaller debounce for snappier feel
    await new Promise(r => setTimeout(r, 100));
    if (thisHover !== hoverCounter) return;

    const data = await fetchSteam(name);
    if (thisHover !== hoverCounter) return;

    if (!data) {
      tip.innerHTML = `<p>No Steam info for<br><strong>${name}</strong>.</p>`;
      return;
    }
    tip.innerHTML = `
      <img src="${data.header_image}" style="width:100%;margin-bottom:6px">
      <p>${data.short_description}</p>
    `;
  }

  function hideTip(){
    hoverCounter++;
    tip.style.display = 'none';
  }

  const SEL = 'td.coll-1 a[href^="/torrent/"]';
  document.addEventListener('mouseover', e => {
    if (e.target.matches(SEL)) showTip(e);
  });
  document.addEventListener('mousemove', e => {
    if (e.target.matches(SEL) && tip.style.display === 'block') positionTip(e);
  });
  document.addEventListener('mouseout', e => {
    if (e.target.matches(SEL)) hideTip(e);
  });
})();