// ==UserScript==
// @name VoiceLinks
// @namespace Sanya
// @description Makes RJ codes more useful.(8-bit RJCode supported.)
// @include *://*/*
// @version 2.1.7
// @grant GM.xmlHttpRequest
// @grant GM_xmlhttpRequest
// @run-at document-start
// ==/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 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;
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 })
});
})();