- // ==UserScript==
- // @name DLSite RJ code preview
- // @namespace SettingDust
- // @description Make RJ code great again!
- // @include *://*/*
- // @version 2.1.6
- // @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 {
- color: currentColor;
- font-weight: bold;
- -webkit-text-stroke: 0.5px !important;
- }
-
- .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;
- color: #212121;
- }
-
- .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 })
- })