Greasy Fork

Greasy Fork is available in English.

HiAnime Links+

Adds direct links to MyAnimeList and AniList on HiAnime watch pages and its grid poster

当前为 2025-06-26 提交的版本,查看 最新版本

// ==UserScript==
// @name         HiAnime Links+
// @namespace    http://greasyfork.icu/users/1470715
// @author       cattishly6060
// @author       forked_bytes
// @description  Adds direct links to MyAnimeList and AniList on HiAnime watch pages and its grid poster
// @icon         https://icons.duckduckgo.com/ip3/hianime.to.ico
// @match        https://hianime.to/*
// @grant        GM_openInTab
// @version      1.0
// @license      0BSD
// ==/UserScript==

(function () {
  'use strict';

  // Add images
  const malBase64Img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAFKUExURQAAAC5Roi5Roi5Roi5Roi5Roi5Roi5Roi5Roi5Roi5Roi5Roi5Roi5Roi5Roi5RojVWpTxdqDlbpzJUpDVXpTdZppCjzv////r7/Vh0tXmPw2uEvYicysfR5vL0+bnE4LXC3uDl8ThZpkporvT1+oSZyPDz+DBTo6+829LZ66q42V55uHSMwZys06Sz1+js9bfD36Gx1Z2u1F96uPHz+X2Txf39/mB7uP7+/+/x+HOKwTBSo3aNwqOz1svT6PP1+k9ssc7W6fj6/Pf4+4qdy9rg76i32IGWx42gzIKXx73I4TRWpdfe7bTB3qy62kxqr97k8L3I4nWMwvj5/FRws83V6cXP5cHL46+93KKx1dDY6vX3++Po82uDvTpbp/v8/YWZycrT59Xc7LjE3+Hm8i9Soq6725qr0n6Uxktpr/z8/t3i8Ft3tqm32f4+yygAAAAPdFJOUwEVh9P50YNT8RfzVfv1z7hGe4QAAAABYktHRBcL1piPAAAAB3RJTUUH6QYZDiIGG4HDQAAAAZhJREFUSMft1ldXwjAUAOCwh+MyxLhQZIgDF4gL98a9F4pa9/r/r94mtVBPW5oHXzzehyT35H60TU9JCCHEZnc4oWY4XW4bYeHx1q7m4fWweqvlcqCwWf59dg0fcYvUA9iJSww4iIX10awVEasH+Ae/BQLBYDCkpDgMhiuzIUybACLYRSqgmVLa0sqyNhzSdsV2RDvltAsghl1MC2h3HJNEsgJSrBqjJ60LaC9AuI9+g3Q/VWNAH9AMDFIVDLHR8MhoNpvNGYCx/LgKJiaxn5ouzESj0VkwAGogmMNufoFPLeqCJV67zEFCvsAKmIHVNQbWOUjJ3UaxWNw0Blvb2O4AB7vVd2gAYC9J9+MKOLAC4PDoGBRwYgkA1leB06L5MygpB2fYnoP5KmnABbaXP0DpSo5rfZCR30xZC1jclPVBvITdrQ64Y7eUlyTpXgE4lHLAXsTDI596AniWlHgx+kRf3+j7h9A3/ZkNgBAomP0JiMWfAIJbll90U3SJb7s+0Y1d+OhASJ3f8hN7+HEGjz/1tasbGtnx5wvNirJSwodULQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyNS0wNi0yNVQxNDozNDowNiswMDowMNrDr1EAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjUtMDYtMjVUMTQ6MzQ6MDYrMDA6MDCrnhftAAAAKHRFWHRkYXRlOnRpbWVzdGFtcAAyMDI1LTA2LTI1VDE0OjM0OjA2KzAwOjAw/Is2MgAAAABJRU5ErkJggg==';
  const anilstBase64Img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAClUExURRkhLQAFHAAAGAAQIBoTFhkUGBkdJtfY2unp6qKjpweO1AOd6wOY4xQ/WwoWJFZbYv///wBPgQCx/wCq/wmGyK2vs/7+/lyRugCt/wAAFfLz9LDe/QCm/2VpcLq8v13C/xQdKnF1esXHyW3H/iEpNMfp/oCEiS40P4DN/gAAAI+SlgAADo3R/kNIUNzy/pudoTq6/+v4/hoNCQCI0xREYxgnNxFReJBTZkgAAAABYktHRBCVsg0sAAAAB3RJTUUH6QYaAyIo3TryMgAAAXNJREFUWMPtlWFXgjAUhoGpzMyxQEmQdJZKpaVW9v9/WrB7B9bpoJdz/Lbn4865z+4L253jWCwWy5VxPQ1zOt2SHrne55q+ezMouB0SDSKQAL8Lo4Jw1KEJvLGMNfK+rI8mVEGSGsE0ayMQD1hf0EqQzCqBnLcQKBbXAp2BKBCLOkEct+ggedQCsMinjCpQS10rfRCsyAK2hgbWPvQwoQoSKJQ5ip4zmkDlkIC/5FJiBpKAvcLGfXeDrYQ0gUkQCPMxthlFoN6gcZ4ogWHeSQJ3B9vOlnnuYDP7iCDY8NNjCEdhSxAUo8SUSWnu9CG7XFCNkl8t7EcfPcO5BOl/gunn8MtwbFRUCXAm4oU6RJOK6NiYAEdJPQ9OxgoQfje1IFL8iU6uWcI/wdF4XmCGoQyY0iR9iRkuE5hRwj1cKM7l3wxNgnKUaMZGUJ4rzSq7RCAWqX7Q0kCYJbaDN44PDKOmb6jgSfVUvcRwqVvhWCwWy3X4Aap4O8LCxI3vAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDI1LTA2LTI2VDAzOjM0OjQwKzAwOjAw9O3HTQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyNS0wNi0yNlQwMzozNDo0MCswMDowMIWwf/EAAAAodEVYdGRhdGU6dGltZXN0YW1wADIwMjUtMDYtMjZUMDM6MzQ6NDArMDA6MDDSpV4uAAAAAElFTkSuQmCC';

  // Add minimal spinner CSS
  const style = document.createElement('style');
  style.textContent = `
    @keyframes spin { to { transform: rotate(360deg); } }
    .mal-mini-spinner {
      display: inline-block;
      width: 12px;
      height: 12px;
      border: 2px solid rgba(0,0,0,0.2);
      border-top-color: #3498db;
      border-radius: 50%;
      animation: spin 0.8s linear infinite;
      margin-left: 5px;
      vertical-align: middle;
    }
    .mal-link-loading .tick {
      opacity: 0.3;
    }
  `;
  document.head.appendChild(style);

  /** @typedef {{mal_id: ?number, anilist_id: ?number}} Link */
  /** @type {Map<string, ?Link>} */
  const cachedLinkMap = new Map();

  /** @type {Object<string, string>} */
  const linkType = {
    MAL: 'mal',
    ANILIST: 'anilist',
  };

  function addHiAnimeBtnAndSubTitle() {
    Array.from(document.querySelectorAll("div.flw-item"))?.filter(e => e.querySelector('div.film-detail') && e.querySelector('div.film-poster') || []).forEach(e => {
      if (e.querySelector('.custom-jp-title')) return;

      const title = e.querySelector('.film-name > a')?.textContent || "";
      const titleJp = (e.querySelector('.film-name > a')?.getAttribute('data-jname') || title || "").replace('[Uncensored]', '').trim();
      const query = encodeURIComponent(titleJp.slice(0, 100));

      const uriMAL = `https://myanimelist.net/search/all?q=${query}&cat=all#anime`;
      const uriAnilist = `https://anilist.co/search/anime?search=${query}`;

      const linkEndpoint = e.querySelector('a.film-poster-ahref')?.href;
      const topPad = e.querySelector('.tick-rate') ? 35 : 10;
      const imgSize = 30;

      const aHiAnime = createLink(uriMAL, titleJp, linkEndpoint, linkType.MAL);
      aHiAnime.innerHTML = `<img class="tick" width="${imgSize}" height="${imgSize}" style="position: absolute; left: 10px; top: ${topPad}px;" src="${malBase64Img}">`;
      e.querySelector('div.film-poster').appendChild(aHiAnime);

      const aAnilist = createLink(uriAnilist, titleJp, linkEndpoint, linkType.ANILIST);
      aAnilist.innerHTML = `<img class="tick" width="${imgSize}" height="${imgSize}" style="position: absolute; left: 10px; top: ${topPad + imgSize + 3}px;" src="${anilstBase64Img}">`;
      e.querySelector('div.film-poster').appendChild(aAnilist);

      const jpTitleElement = `<h3 class="film-name custom-jp-title" style="font-size: 12px; color: gray;">${titleJp}</h3>`;
      e.querySelector('.film-detail')?.children?.[0]?.insertAdjacentHTML('afterend', jpTitleElement);

      [aHiAnime, aAnilist].forEach(e => e.addEventListener('click', async function (e) {
        e.preventDefault(); // Prevent default behavior

        // Get configuration from data attributes
        const endpoint = this.dataset.endpoint;
        const fallbackUrl = this.dataset.fallbackUrl;
        const type = this.dataset.type;

        // Get cached finalUrl
        const cachedFinalUrl = this.dataset.finalUrl;
        if (cachedFinalUrl) {
          GM_openInTab(cachedFinalUrl, {active: true});
          return;
        }

        // Get cached link
        /** @type {?Link} */
        const cachedLink = cachedLinkMap.get(endpoint);
        if (cachedLink) {
          let finalUrl;
          if (type === linkType.MAL && cachedLink.mal_id) {
            finalUrl = `https://myanimelist.net/anime/${cachedLink.mal_id}`;
          } else if (type === linkType.ANILIST && cachedLink.anilist_id) {
            finalUrl = `https://anilist.co/anime/${cachedLink.anilist_id}`;
          } else {
            finalUrl = fallbackUrl;
          }
          GM_openInTab(finalUrl || fallbackUrl, {active: true});
          return;
        }

        // Show loading state
        // Add small spinner next to existing icon
        const spinner = document.createElement('span');
        spinner.className = 'mal-mini-spinner';
        // Add loading class to parent
        this.classList.add('mal-link-loading');
        // Position spinner absolutely near the icon
        spinner.style.cssText = `
          position: absolute;
          left: 30px;
          // top: ${topPad + 9}px;
          top: ${type === linkType.MAL ? topPad + 9 : topPad + imgSize + 3 + 9}px;
        `;
        this.appendChild(spinner);
        this.style.pointerEvents = 'none';


        try {
          // Fetch required data
          const res = await fetch(endpoint);
          if (!res?.ok) {
            GM_openInTab(fallbackUrl, {active: true});
            return;
          }
          const data = await res.text();
          const malId = data.match(/"mal_id":"(\d+)",/)?.[1];
          const anilistId = data.match(/"anilist_id":"(\d+)",/)?.[1];
          if (malId || anilistId) {
            cachedLinkMap.set(endpoint, {
              mal_id: malId,
              anilist_id: anilistId
            });
          }

          if ((!malId && type === linkType.MAL)
            || (!anilistId && type === linkType.ANILIST)
            || (!anilistId && !malId)
            || !type) {
            GM_openInTab(fallbackUrl, {active: true});
            return;
          }

          // Open the link
          const finalUrl = type === linkType.MAL
            ? `https://myanimelist.net/anime/${malId}`
            : `https://anilist.co/anime/${anilistId}`;

          this.dataset.finalUrl = finalUrl;
          GM_openInTab(finalUrl, {active: true});

        } catch (error) {
          console.error('Failed to process link:', error);
        } finally {
          // Clean up (loading)
          spinner.remove();
          this.classList.remove('mal-link-loading');
          this.style.pointerEvents = '';
        }
      }));
    });
  }

  window.addEventListener('load', function () {
    addHiAnimeBtnAndSubTitle();
  });

  addHiAnimeBtnAndSubTitle();


  const syncData = JSON.parse(document.getElementById('syncData')?.textContent || null);
  if (!syncData) return;

  const title = document.getElementsByClassName('film-name')[0];
  if (!title) return;

  if (syncData.mal_id) {
    const a = createLink(`https://myanimelist.net/anime/${syncData.mal_id}`, 'MyAnimeList');
    a.innerHTML = ` <img width="25" height="25" src="${malBase64Img}">`;
    title.appendChild(a);
  }

  if (syncData.anilist_id) {
    const a = createLink(`https://anilist.co/anime/${syncData.anilist_id}`, 'AniList');
    a.innerHTML = ` <img width="25" height="25" src="${anilstBase64Img}">`;
    title.appendChild(a);
  }

  function createLink(href, title, endpoint, type) {
    const a = document.createElement('a');
    a.target = '_blank';
    a.rel = 'noreferrer,noopener';
    a.href = href;
    a.title = title;
    if (endpoint) {
      a.href = "#";
      a.dataset.endpoint = endpoint;
      a.dataset.fallbackUrl = href;
      a.dataset.type = type;
    }
    return a;
  }

})();