VoiceLinks

Makes RJ codes more useful.(8-bit RJCode supported.)

Versión del día 04/04/2024. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name        VoiceLinks
// @namespace   Sanya
// @description Makes RJ codes more useful.(8-bit RJCode supported.)
// @include     *://*/*
// @version     2.1.10
// @grant       GM.xmlHttpRequest
// @grant       GM_xmlhttpRequest
// @run-at      document-start
// @homepage    https://greasyfork.org/zh-CN/scripts/456775
// ==/UserScript==

(function () {
    'use strict';
    const RJ_REGEX = new RegExp("(R[JE][0-9]{8})|(R[JE][0-9]{6})", "gi");
    const RJ_REGEX_NEW = new RegExp("R[JE][0-9]{8}", "gi");
    const VOICELINK_CLASS = 'voicelink';
    const RJCODE_ATTRIBUTE = 'rjcode';
    const css = `
      .voicepopup {
          min-width: 600px !important;
          z-index: 50000 !important;
          max-width: 80% !important;
          position: fixed !important;
          line-height: 1.4em;
          font-size:1.1em!important;
          margin-bottom: 10px;
          box-shadow: 0 0 .125em 0 rgba(0,0,0,.5);
          border-radius: 0.5em;
          background-color:#8080C0;
          color:#F6F6F6;
          text-align: left;
          padding: 10px;
      }

      .voicepopup img {
          width: 270px;
          height: auto;
          margin: 3px 15px 3px 3px;
          max-width: fit-content;
      }
      
      .voicepopup a {
          text-decoration: none;
          color: pink;
      }
      
      .voicepopup .age-18{
          color: hsl(300deg 76% 77%);
      }
      
      .voicepopup .age-all{
          color: hsl(157deg 82% 52%);
      }

      .voice-title {
          font-size: 1.4em;
          font-weight: bold;
          text-align: center;
          margin: 5px 10px 0 0;
          display: block;
      }

      .rjcode {
          text-align: center;
          font-size: 1.2em;
          font-style: italic;
          opacity: 0.3;
      }

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

      .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 setUserSelectTitle(){
        // Make title selectable
        const hostname = document.location.hostname;
        if(!hostname.endsWith("dlsite.com")){
            return;
        }

        const title = document.getElementById("work_name");
        if(title){
            title.style.userSelect = "text";
        }
    }

    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_NEW))
                            return NodeFilter.FILTER_ACCEPT;
                        if (node.nodeValue.match(RJ_REGEX))
                            return NodeFilter.FILTER_ACCEPT;
                    }
                },
                false,
            );
            while (rjNodeTreeWalker.nextNode()) {
                const node = rjNodeTreeWalker.currentNode;

                //Ignore Element which let user input (textarea), input can be ignored because it's not a text node.
                if(node.parentElement.nodeName === "TEXTAREA"){
                    continue;
                }

                if (node.parentElement.classList.contains(VOICELINK_CLASS))
                    Parser.rebindEvents(node.parentElement);
                else
                    Parser.linkify(node);
            }
        },

        wrapRJCode: function (rjCode) {
            let e;
            e = document.createElement("a");
            e.classList = VOICELINK_CLASS;
            e.href = `https://www.dlsite.com/maniax/work/=/product_id/${rjCode.toUpperCase()}.html`
            e.innerHTML = rjCode;
            e.target = "_blank";
            e.rel = "noreferrer";
            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
                let code = matches[i].value
                const rjLinkNode = Parser.wrapRJCode(code);
                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 + matches[i].value.length, 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);
                }
            }
        },

    }

    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 imgContainer = document.createElement("div")
                    const img = document.createElement("img");
                    img.src = workInfo.img;
                    imgContainer.appendChild(img);

                    let html = `
                      <div>
                          <div class='voice-title'>${workInfo.title}</div>
                          <div class='rjcode'>[${workInfo.rj}]</div>
                          <br />
                          Circle: <a>${workInfo.circle}</a>
                          <br />
                  `;
                    if (workInfo.date)
                        html += `Release: <a>${workInfo.date}</a> <br />`;
                    else if (workInfo.dateAnnounce)
                        html += `Scheduled Release: <a>${workInfo.dateAnnounce}</a> <br />`;

                    if (workInfo.update)
                        html += `Update: <a>${workInfo.update}</a> <br />`;

                    let ratingClass = "age-all";
                    if(workInfo.rating.includes("18")){
                        ratingClass = "age-18";
                    }
                    html += `Age rating: <a class="${ratingClass}">${workInfo.rating}</a><br />`

                    if (workInfo.cv)
                        html += `CV: <a>${workInfo.cv}</a> <br />`;

                    if (workInfo.tags){
                        html += `Tags: <a>`
                        workInfo.tags.forEach(tag => {
                            html += tag + "\u3000";
                        });
                        html += "</a><br />";
                    }

                    if (workInfo.filesize)
                        html += `File size: ${workInfo.filesize}<br />`;

                    html += "</div>"
                    popup.innerHTML = html;

                    popup.insertBefore(imgContainer, popup.childNodes[0]);
                }

                Popup.move(e);
            });
        },

        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 + 10 < window.innerWidth - 10) {
                    popup.style.left = (e.clientX + 10) + "px";
                }
                else {
                    popup.style.left = (window.innerWidth - popup.offsetWidth - 10) + "px";
                }

                if (popup.offsetHeight + e.clientY + 50 > window.innerHeight) {
                    popup.style.top = (e.clientY - popup.offsetHeight - 8) + "px";
                }
                else {
                    popup.style.top = (e.clientY + 20) + "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((rj.length === 10 ? 7 : 5)) === "000")
                rj_group = rj;
            else {
                rj_group = (parseInt(rj.slice(2, (rj.length === 10 ? 7 : 5))) + 1).toString() + "000";
                if(rj_group.length < rj.length - 2){
                    let zero = Math.pow(10, rj.length - rj_group.length - 2).toString().slice(1)
                    rj_group = zero + rj_group
                }
                rj_group = "RJ" + rj_group; //("000000" + rj_group).substring(rj_group.length);
            }

            workInfo.img = "https://img.dlsite.jp/modpub/images2/work/doujin/" + rj_group + "/" + rj + "_img_main.jpg";

            let metaList = dom.getElementsByTagName("meta")
            for (let i = 0; i < metaList.length; i++){
                let meta = metaList[i];
                if(meta.getAttribute("property") === 'og:image'){
                    workInfo.img = meta.content;
                    break;
                }
            }

            workInfo.title = dom.getElementById("work_name").innerText;
            workInfo.circle = dom.querySelector("span.maker_name").innerText;

            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].innerText;
                const row_data = row.cells[1];
                switch (true) {
                    case (row_header.includes("販売日")||row_header.includes("贩卖日")||row_header.includes("Release date")||row_header.includes("販賣日")||row_header.includes("판매일")):
                        workInfo.date = row_data.innerText;
                        break;
                    case (row_header.includes("更新情報")||row_header.includes("更新信息")||row_header.includes("Update information")||row_header.includes("更新資訊")||row_header.includes("갱신 정보")):
                        workInfo.update = row_data.firstChild.data;
                        break;
                    case (row_header.includes("年齢指定")||row_header.includes("年龄指定")||row_header.includes("Age")||row_header.includes("年齡指定")||row_header.includes("연령 지정")):
                        workInfo.rating = row_data.innerText;
                        break;
                    case (row_header.includes("ジャンル")||row_header.includes("分类")||row_header.includes("Genre")||row_header.includes("分類")||row_header.includes("장르")):
                        const tag_nodes = row_data.querySelectorAll("a");
                        workInfo.tags = [...tag_nodes].map(a => { return a.innerText });
                        break;
                    case (row_header.includes("声優")||row_header.includes("声优")||row_header.includes("Voice Actor")||row_header.includes("聲優")||row_header.includes("성우")):
                        workInfo.cv = row_data.innerText;
                        break;
                    case (row_header.includes("ファイル容量")||row_header.includes("文件容量")||row_header.includes("File size")||row_header.includes("檔案容量")||row_header.includes("파일 용량")):
                        workInfo.filesize = row_data.innerText.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"
            }

            return workInfo;
        },

        request: function (rjCode, callback) {
            const url = `https://www.dlsite.com/maniax/work/=/product_id/${rjCode}.html`;
            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`;
            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 })

        // Make title selectable
        setUserSelectTitle();
    });
})();