Sleazy Fork is available in English.

DLSite RJ code preview

Make RJ code great again!

As of 23/09/2022. See the latest version.

// ==UserScript==
// @name        DLSite RJ code preview
// @namespace   SettingDust
// @description Make RJ code great again!
// @include     *://*/*
// @version     2.1.3
// @license     MIT
// @grant       GM.xmlHttpRequest
// @grant       GM_xmlhttpRequest
// @run-at      document-start
// ==/UserScript==

"use strict";
const RJ_REGEX = new RegExp("R[JE][0-9]{6}", "gi");
const VOICELINK_CLASS = "voicelink";
const RJCODE_ATTRIBUTE = "rjcode";
const css = `
.voicepopup {
  z-index: 50000;
  max-width: 80%;
  max-height: 80%;
  position: fixed;
  box-shadow: 0 0 0 2.5px rgba(0, 0, 0, 0.12);
  border-radius: 16px;
  background-color: white;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.voicepopup img {
  width: 100%;
  height: auto;
  max-width: 360px;
}

.voicelink {
  text-shadow: 1px 1px 1px #333;
  color: #333;
}

.voicelink: hover {
  text-decoration: none;
}

.voicepopup > .voice-info {
  padding: 12px 16px;
  font-size: 0.88rem;
  line-height: 1.2;
  max-width: 360px;
  display: grid;
  grid-gap: 6px;
  box-sizing: border-box;
}

.voicepopup > .voice-info > p {
  margin: 0;
  font-weight: bold;
}

.voicepopup > .voice-info > p > span {
  font-weight: normal;
}

.voicepopup .voice-title {
  margin: 0;
  font-size: 1.1rem;
  font-weight: bold;
  line-height: 1;
}

.voicepopup .error {
  height: 210px;
  line-height: 210px;
  text-align: center;
}

.voicepopup.discord-dark {
  background-color: #36393f;
  color: #dcddde;
  font-size: 0.9375rem;
}`;

function getAdditionalPopupClasses() {
  const hostname = document.location.hostname;
  switch (hostname) {
    case "boards.4chan.org":
      return "post reply";
    case "discordapp.com":
      return "discord-dark";
    default:
      return null;
  }
}

function getXmlHttpRequest() {
  return typeof GM !== "undefined" && GM !== null
    ? GM.xmlHttpRequest
    : GM_xmlhttpRequest;
}

const Parser = {
  walkNodes: function (elem) {
    const rjNodeTreeWalker = document.createTreeWalker(
      elem,
      NodeFilter.SHOW_TEXT,
      {
        acceptNode: function (node) {
          if (node.parentElement.classList.contains(VOICELINK_CLASS))
            return NodeFilter.FILTER_ACCEPT;
          if (node.nodeValue.match(RJ_REGEX)) return NodeFilter.FILTER_ACCEPT;
        },
      },
      false
    );
    while (rjNodeTreeWalker.nextNode()) {
      const node = rjNodeTreeWalker.currentNode;
      if (node.parentElement.classList.contains(VOICELINK_CLASS))
        Parser.rebindEvents(node.parentElement);
      else {
        Parser.linkify(node);
      }
    }
  },

  wrapRJCode: function (rjCode) {
    var e;
    e = document.createElement("a");
    e.classList = VOICELINK_CLASS;
    e.href = `https://www.dlsite.com/maniax/work/=/product_id/${rjCode}.html`;
    e.innerHTML = rjCode;
    e.target = "_blank";
    e.rel = "noreferrer";
    e.classList.add(rjCode);
    e.setAttribute(RJCODE_ATTRIBUTE, rjCode.toUpperCase());
    e.addEventListener("mouseover", Popup.over);
    e.addEventListener("mouseout", Popup.out);
    e.addEventListener("mousemove", Popup.move);
    return e;
  },

  linkify: function (textNode) {
    const nodeOriginalText = textNode.nodeValue;
    const matches = [];

    let match;
    while ((match = RJ_REGEX.exec(nodeOriginalText))) {
      matches.push({
        index: match.index,
        value: match[0],
      });
    }

    // Keep text in text node until first RJ code
    textNode.nodeValue = nodeOriginalText.substring(0, matches[0].index);

    // Insert rest of text while linkifying RJ codes
    let prevNode = null;
    for (let i = 0; i < matches.length; ++i) {
      // Insert linkified RJ code
      const rjLinkNode = Parser.wrapRJCode(matches[i].value);
      textNode.parentNode.insertBefore(
        rjLinkNode,
        prevNode ? prevNode.nextSibling : textNode.nextSibling
      );

      // Insert text after if there is any
      let upper;
      if (i === matches.length - 1) upper = undefined;
      else upper = matches[i + 1].index;
      let substring;
      if (
        (substring = nodeOriginalText.substring(matches[i].index + 8, upper))
      ) {
        const subtextNode = document.createTextNode(substring);
        textNode.parentNode.insertBefore(
          subtextNode,
          rjLinkNode.nextElementSibling
        );
        prevNode = subtextNode;
      } else {
        prevNode = rjLinkNode;
      }
    }
  },

  rebindEvents: function (elem) {
    if (elem.nodeName === "A") {
      elem.addEventListener("mouseover", Popup.over);
      elem.addEventListener("mouseout", Popup.out);
      elem.addEventListener("mousemove", Popup.move);
    } else {
      const voicelinks = elem.querySelectorAll("." + VOICELINK_CLASS);
      for (var i = 0, ii = voicelinks.length; i < ii; i++) {
        const voicelink = voicelinks[i];
        voicelink.addEventListener("mouseover", Popup.over);
        voicelink.addEventListener("mouseout", Popup.out);
        voicelink.addEventListener("mousemove", Popup.move);
      }
    }
  },
};

var globalCodes = [];

const Popup = {
  makePopup: function (e, rjCode) {
    const popup = document.createElement("div");
    popup.className = "voicepopup " + (getAdditionalPopupClasses() || "");
    popup.id = "voice-" + rjCode;
    popup.style = "display: flex";
    document.body.appendChild(popup);
    DLsite.request(rjCode, function (workInfo) {
      if (workInfo === null)
        popup.innerHTML = "<div class='error'>Work not found.</span>";
      else {
        const img = document.createElement("img");
        img.src = workInfo.img;

        let html = `
                      <div class='voice-info'>
                          <h4 class='voice-title'>${workInfo.title.trim()}</h4>
                          <p>社团名:<span>${workInfo.circle.trim()}</span></p>
                  `;
        if (workInfo.date)
          html += `<p>贩卖日:<span>${workInfo.date}</span></p>`;
        else if (workInfo.dateAnnounce)
          html += `<p>发布日期:<span>${workInfo.dateAnnounce}</span></p>`;

        html += `<p>年龄指定:<span>${workInfo.rating.trim()}</span></p>`;

        if (workInfo.cv) html += `<p>声优:<span>${workInfo.cv}</span></p>`;
        if (workInfo.tags) {
          html += `<p>分类:<span>`;
          workInfo.tags.forEach((tag) => {
            html += tag + "\u3000";
          });
          html += "</span></p>";
        }

        if (workInfo.filesize)
          html += `<p>文件容量:<span>${workInfo.filesize}</span></p>`;

        html += "</div>";
        popup.innerHTML = html;
        popup.insertBefore(img, popup.childNodes[0]);
      }

      Popup.move(e);
    });
  },
  humanFileSize: function (size) {
    if (!size) return "";
    var i = Math.floor(Math.log(size) / Math.log(1024));
    return (
      (size / Math.pow(1024, i)).toFixed(2) * 1 +
      " " +
      ["B", "kB", "MB", "GB", "TB"][i]
    );
  },

  over: function (e) {
    const rjCode = e.target.getAttribute(RJCODE_ATTRIBUTE);
    const popup = document.querySelector("div#voice-" + rjCode);
    if (popup) {
      const style = popup.getAttribute("style").replace("none", "flex");
      popup.setAttribute("style", style);
    } else {
      Popup.makePopup(e, rjCode);
    }
  },

  out: function (e) {
    const rjCode = e.target.getAttribute("rjcode");
    const popup = document.querySelector("div#voice-" + rjCode);
    if (popup) {
      const style = popup.getAttribute("style").replace("flex", "none");
      popup.setAttribute("style", style);
    }
  },

  move: function (e) {
    const rjCode = e.target.getAttribute("rjcode");
    const popup = document.querySelector("div#voice-" + rjCode);
    if (popup) {
      // 如果右侧没有超出屏幕范围
      if (popup.offsetWidth + e.clientX + 24 < window.innerWidth) {
        popup.style.left = e.clientX + 8 + "px";
      } else {
        // 显示在左侧
        popup.style.left = e.clientX - popup.offsetWidth - 8 + "px";
      }

      // 如果下方超出屏幕范围
      if (popup.offsetHeight + e.clientY + 16 > window.innerHeight) {
        // 尽可能靠下
        popup.style.top = window.innerHeight - popup.offsetHeight - 16 + "px";
      } else {
        popup.style.top = e.clientY + "px";
      }
    }
  },
};

const DLsite = {
  parseWorkDOM: function (dom, rj) {
    // workInfo: {
    //     rj: any;
    //     img: string;
    //     title: any;
    //     circle: any;
    //     date: any;
    //     rating: any;
    //     tags: any[];
    //     cv: any;
    //     filesize: any;
    //     dateAnnounce: any;
    // }
    const workInfo = {};
    workInfo.rj = rj;

    let rj_group;
    if (rj.slice(5) == "000") rj_group = rj;
    else {
      rj_group = (parseInt(rj.slice(2, 5)) + 1).toString() + "000";
      rj_group = "RJ" + ("000000" + rj_group).substring(rj_group.length);
    }

    workInfo.img =
      "https://img.dlsite.jp/modpub/images2/work/doujin/" +
      rj_group +
      "/" +
      rj +
      "_img_main.jpg";
    workInfo.title = dom.getElementById("work_name").textContent;
    workInfo.circle = dom.querySelector("span.maker_name").textContent;

    const table_outline = dom.querySelector("table#work_outline");
    for (var i = 0, ii = table_outline.rows.length; i < ii; i++) {
      const row = table_outline.rows[i];
      const row_header = row.cells[0].textContent;
      const row_data = row.cells[1];
      switch (true) {
        case row_header.includes("贩卖日"):
          workInfo.date = row_data.textContent;
          break;
        case row_header.includes("年龄指定"):
          workInfo.rating = row_data.textContent;
          break;
        case row_header.includes("分类"):
          const tag_nodes = row_data.querySelectorAll("a");
          workInfo.tags = [...tag_nodes].map((a) => {
            return a.textContent;
          });
          break;
        case row_header.includes("声优"):
          workInfo.cv = row_data.textContent;
          break;
        case row_header.includes("文件容量"):
          workInfo.filesize = row_data.textContent.replace("合计", "").trim();
          break;
        default:
          break;
      }
    }

    const work_date_ana = dom.querySelector("strong.work_date_ana");
    if (work_date_ana) {
      workInfo.dateAnnounce = work_date_ana.innerText;
      workInfo.img =
        "https://img.dlsite.jp/modpub/images2/ana/doujin/" +
        rj_group +
        "/" +
        rj +
        "_ana_img_main.jpg";
    }
    console.log(workInfo);

    return workInfo;
  },

  request: function (rjCode, callback) {
    const url = `https://www.dlsite.com/maniax/work/=/product_id/${rjCode}.html/?locale=zh_CN`;
    getXmlHttpRequest()({
      method: "GET",
      url,
      headers: {
        Accept: "text/xml",
        "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:67.0)",
      },
      onload: function (resp) {
        if (resp.readyState === 4 && resp.status === 200) {
          const dom = new DOMParser().parseFromString(
            resp.responseText,
            "text/html"
          );
          const workInfo = DLsite.parseWorkDOM(dom, rjCode);
          callback(workInfo);
        } else if (resp.readyState === 4 && resp.status === 404)
          DLsite.requestAnnounce(rjCode, callback);
      },
    });
  },

  requestAnnounce: function (rjCode, callback) {
    const url = `https://www.dlsite.com/maniax/announce/=/product_id/${rjCode}.html/?locale=ja_JP`;
    getXmlHttpRequest()({
      method: "GET",
      url,
      headers: {
        Accept: "text/xml",
        "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:67.0)",
      },
      onload: function (resp) {
        if (resp.readyState === 4 && resp.status === 200) {
          const dom = new DOMParser().parseFromString(
            resp.responseText,
            "text/html"
          );
          const workInfo = DLsite.parseWorkDOM(dom, rjCode);
          callback(workInfo);
        } else if (resp.readyState === 4 && resp.status === 404) callback(null);
      },
    });
  },
};

document.addEventListener("DOMContentLoaded", function () {
  const style = document.createElement("style");
  style.innerHTML = css;
  document.head.appendChild(style);

  Parser.walkNodes(document.body);

  const observer = new MutationObserver(function (m) {
    for (let i = 0; i < m.length; ++i) {
      let addedNodes = m[i].addedNodes;

      for (let j = 0; j < addedNodes.length; ++j) {
        Parser.walkNodes(addedNodes[j]);
      }
    }
  });

  document.addEventListener("securitypolicyviolation", function (e) {
    if (e.blockedURI.includes("img.dlsite.jp")) {
      const img = document.querySelector(`img[src="${e.blockedURI}"]`);
      img.remove();
    }
  });

  observer.observe(document.body, { childList: true, subtree: true });
});