DLSite RJ code preview

Make RJ code great again!

Per 23-09-2022. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name        DLSite RJ code preview
// @namespace   SettingDust
// @description Make RJ code great again!
// @include     *://*/*
// @version     2.1.5
// @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;
}

.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 })
})