// ==UserScript==
// @name DLSite RJ code preview
// @namespace SettingDust
// @description Make RJ code great again!
// @include *://*/*
// @version 2.1.4
// @license MIT
// @grant GM.xmlHttpRequest
// @grant GM_xmlhttpRequest
// @run-at document-start
// ==/UserScript==
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
img.onload = () => Popup.move(e)
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 })
})