DLSite RJ code preview

Make RJ code great again!

Pada tanggal 02 Oktober 2022. Lihat %(latest_version_link).

  1. // ==UserScript==
  2. // @name DLSite RJ code preview
  3. // @namespace SettingDust
  4. // @description Make RJ code great again!
  5. // @include *://*/*
  6. // @version 2.1.6
  7. // @license MIT
  8. // @grant GM.xmlHttpRequest
  9. // @grant GM_xmlhttpRequest
  10. // @run-at document-start
  11. // ==/UserScript==
  12.  
  13. const RJ_REGEX = new RegExp('R[JE][0-9]{6}', 'gi')
  14. const VOICELINK_CLASS = 'voicelink'
  15. const RJCODE_ATTRIBUTE = 'rjcode'
  16. const css = `
  17. .voicepopup {
  18. z-index: 50000;
  19. max-width: 80%;
  20. max-height: 80%;
  21. position: fixed;
  22. box-shadow: 0 0 0 2.5px rgba(0, 0, 0, 0.12);
  23. border-radius: 16px;
  24. background-color: white;
  25. display: flex;
  26. flex-direction: column;
  27. overflow: hidden;
  28. }
  29.  
  30. .voicepopup img {
  31. width: 100%;
  32. height: auto;
  33. max-width: 360px;
  34. }
  35.  
  36. .voicelink {
  37. color: currentColor;
  38. font-weight: bold;
  39. -webkit-text-stroke: 0.5px !important;
  40. }
  41.  
  42. .voicelink: hover {
  43. text-decoration: none;
  44. }
  45.  
  46. .voicepopup > .voice-info {
  47. padding: 12px 16px;
  48. font-size: 0.88rem;
  49. line-height: 1.2;
  50. max-width: 360px;
  51. display: grid;
  52. grid-gap: 6px;
  53. box-sizing: border-box;
  54. color: #212121;
  55. }
  56.  
  57. .voicepopup > .voice-info > p {
  58. margin: 0;
  59. font-weight: bold;
  60. }
  61.  
  62. .voicepopup > .voice-info > p > span {
  63. font-weight: normal;
  64. }
  65.  
  66. .voicepopup .voice-title {
  67. margin: 0;
  68. font-size: 1.1rem;
  69. font-weight: bold;
  70. line-height: 1;
  71. }
  72.  
  73. .voicepopup .error {
  74. height: 210px;
  75. line-height: 210px;
  76. text-align: center;
  77. }
  78.  
  79. .voicepopup.discord-dark {
  80. background-color: #36393f;
  81. color: #dcddde;
  82. font-size: 0.9375rem;
  83. }`
  84.  
  85. function getAdditionalPopupClasses() {
  86. const hostname = document.location.hostname
  87. switch (hostname) {
  88. case 'boards.4chan.org':
  89. return 'post reply'
  90. case 'discordapp.com':
  91. return 'discord-dark'
  92. default:
  93. return null
  94. }
  95. }
  96.  
  97. function getXmlHttpRequest() {
  98. return typeof GM !== 'undefined' && GM !== null
  99. ? GM.xmlHttpRequest
  100. : GM_xmlhttpRequest
  101. }
  102.  
  103. const Parser = {
  104. walkNodes: function (elem) {
  105. const rjNodeTreeWalker = document.createTreeWalker(
  106. elem,
  107. NodeFilter.SHOW_TEXT,
  108. {
  109. acceptNode: function (node) {
  110. if (node.parentElement.classList.contains(VOICELINK_CLASS))
  111. return NodeFilter.FILTER_ACCEPT
  112. if (node.nodeValue.match(RJ_REGEX)) return NodeFilter.FILTER_ACCEPT
  113. },
  114. },
  115. false,
  116. )
  117. while (rjNodeTreeWalker.nextNode()) {
  118. const node = rjNodeTreeWalker.currentNode
  119. if (node.parentElement.classList.contains(VOICELINK_CLASS))
  120. Parser.rebindEvents(node.parentElement)
  121. else {
  122. Parser.linkify(node)
  123. }
  124. }
  125. },
  126.  
  127. wrapRJCode: function (rjCode) {
  128. var e
  129. e = document.createElement('a')
  130. e.classList = VOICELINK_CLASS
  131. e.href = `https://www.dlsite.com/maniax/work/=/product_id/${rjCode}.html`
  132. e.innerHTML = rjCode
  133. e.target = '_blank'
  134. e.rel = 'noreferrer'
  135. e.classList.add(rjCode)
  136. e.setAttribute(RJCODE_ATTRIBUTE, rjCode.toUpperCase())
  137. e.addEventListener('mouseover', Popup.over)
  138. e.addEventListener('mouseout', Popup.out)
  139. e.addEventListener('mousemove', Popup.move)
  140. return e
  141. },
  142.  
  143. linkify: function (textNode) {
  144. const nodeOriginalText = textNode.nodeValue
  145. const matches = []
  146.  
  147. let match
  148. while ((match = RJ_REGEX.exec(nodeOriginalText))) {
  149. matches.push({
  150. index: match.index,
  151. value: match[0],
  152. })
  153. }
  154.  
  155. // Keep text in text node until first RJ code
  156. textNode.nodeValue = nodeOriginalText.substring(0, matches[0].index)
  157.  
  158. // Insert rest of text while linkifying RJ codes
  159. let prevNode = null
  160. for (let i = 0; i < matches.length; ++i) {
  161. // Insert linkified RJ code
  162. const rjLinkNode = Parser.wrapRJCode(matches[i].value)
  163. textNode.parentNode.insertBefore(
  164. rjLinkNode,
  165. prevNode ? prevNode.nextSibling : textNode.nextSibling,
  166. )
  167.  
  168. // Insert text after if there is any
  169. let upper
  170. if (i === matches.length - 1) upper = undefined
  171. else upper = matches[i + 1].index
  172. let substring
  173. if (
  174. (substring = nodeOriginalText.substring(matches[i].index + 8, upper))
  175. ) {
  176. const subtextNode = document.createTextNode(substring)
  177. textNode.parentNode.insertBefore(
  178. subtextNode,
  179. rjLinkNode.nextElementSibling,
  180. )
  181. prevNode = subtextNode
  182. } else {
  183. prevNode = rjLinkNode
  184. }
  185. }
  186. },
  187.  
  188. rebindEvents: function (elem) {
  189. if (elem.nodeName === 'A') {
  190. elem.addEventListener('mouseover', Popup.over)
  191. elem.addEventListener('mouseout', Popup.out)
  192. elem.addEventListener('mousemove', Popup.move)
  193. } else {
  194. const voicelinks = elem.querySelectorAll('.' + VOICELINK_CLASS)
  195. for (var i = 0, ii = voicelinks.length; i < ii; i++) {
  196. const voicelink = voicelinks[i]
  197. voicelink.addEventListener('mouseover', Popup.over)
  198. voicelink.addEventListener('mouseout', Popup.out)
  199. voicelink.addEventListener('mousemove', Popup.move)
  200. }
  201. }
  202. },
  203. }
  204.  
  205. var globalCodes = []
  206.  
  207. const Popup = {
  208. makePopup: function (e, rjCode) {
  209. const popup = document.createElement('div')
  210. popup.className = 'voicepopup ' + (getAdditionalPopupClasses() || '')
  211. popup.id = 'voice-' + rjCode
  212. popup.style = 'display: flex'
  213. document.body.appendChild(popup)
  214. DLsite.request(rjCode, function (workInfo) {
  215. if (workInfo === null)
  216. popup.innerHTML = "<div class='error'>Work not found.</span>"
  217. else {
  218. const img = document.createElement('img')
  219. img.src = workInfo.img
  220. img.onload = () => Popup.move(e)
  221.  
  222. let html = `<div class='voice-info'>
  223. <h4 class='voice-title'>${workInfo.title.trim()}</h4>
  224. <p>社团名:<span>${workInfo.circle.trim()}</span></p>`
  225. if (workInfo.date)
  226. html += `<p>贩卖日:<span>${workInfo.date}</span></p>`
  227. else if (workInfo.dateAnnounce)
  228. html += `<p>发布日期:<span>${workInfo.dateAnnounce}</span></p>`
  229.  
  230. html += `<p>年龄指定:<span>${workInfo.rating.trim()}</span></p>`
  231.  
  232. if (workInfo.cv) html += `<p>声优:<span>${workInfo.cv}</span></p>`
  233. if (workInfo.tags) {
  234. html += `<p>分类:<span>`
  235. workInfo.tags.forEach((tag) => { html += tag + '\u3000' })
  236. html += '</span></p>'
  237. }
  238.  
  239. if (workInfo.filesize) html += `<p>文件容量:<span>${workInfo.filesize}</span></p>`
  240.  
  241. html += '</div>'
  242. popup.innerHTML = html
  243. popup.insertBefore(img, popup.childNodes[0])
  244. }
  245.  
  246. Popup.move(e)
  247. })
  248. },
  249. humanFileSize: function (size) {
  250. if (!size) return ''
  251. var i = Math.floor(Math.log(size) / Math.log(1024))
  252. return (
  253. (size / Math.pow(1024, i)).toFixed(2) * 1 +
  254. ' ' +
  255. ['B', 'kB', 'MB', 'GB', 'TB'][i]
  256. )
  257. },
  258.  
  259. over: function (e) {
  260. const rjCode = e.target.getAttribute(RJCODE_ATTRIBUTE)
  261. const popup = document.querySelector('div#voice-' + rjCode)
  262. if (popup) {
  263. const style = popup.getAttribute('style').replace('none', 'flex')
  264. popup.setAttribute('style', style)
  265. } else {
  266. Popup.makePopup(e, rjCode)
  267. }
  268. },
  269.  
  270. out: function (e) {
  271. const rjCode = e.target.getAttribute('rjcode')
  272. const popup = document.querySelector('div#voice-' + rjCode)
  273. if (popup) {
  274. const style = popup.getAttribute('style').replace('flex', 'none')
  275. popup.setAttribute('style', style)
  276. }
  277. },
  278.  
  279. move: function (e) {
  280. const rjCode = e.target.getAttribute('rjcode')
  281. const popup = document.querySelector('div#voice-' + rjCode)
  282. if (popup) {
  283. // 如果右侧没有超出屏幕范围
  284. if (popup.offsetWidth + e.clientX + 24 < window.innerWidth) {
  285. popup.style.left = e.clientX + 8 + 'px'
  286. } else {
  287. // 显示在左侧
  288. popup.style.left = e.clientX - popup.offsetWidth - 8 + 'px'
  289. }
  290.  
  291. // 如果下方超出屏幕范围
  292. if (popup.offsetHeight + e.clientY + 16 > window.innerHeight) {
  293. // 尽可能靠下
  294. popup.style.top = window.innerHeight - popup.offsetHeight - 16 + 'px'
  295. } else {
  296. popup.style.top = e.clientY + 'px'
  297. }
  298. }
  299. },
  300. }
  301.  
  302. const DLsite = {
  303. parseWorkDOM: function (dom, rj) {
  304. // workInfo: {
  305. // rj: any;
  306. // img: string;
  307. // title: any;
  308. // circle: any;
  309. // date: any;
  310. // rating: any;
  311. // tags: any[];
  312. // cv: any;
  313. // filesize: any;
  314. // dateAnnounce: any;
  315. // }
  316. const workInfo = {}
  317. workInfo.rj = rj
  318.  
  319. let rj_group
  320. if (rj.slice(5) == '000') rj_group = rj
  321. else {
  322. rj_group = (parseInt(rj.slice(2, 5)) + 1).toString() + '000'
  323. rj_group = 'RJ' + ('000000' + rj_group).substring(rj_group.length)
  324. }
  325.  
  326. workInfo.img =
  327. 'https://img.dlsite.jp/modpub/images2/work/doujin/' +
  328. rj_group +
  329. '/' +
  330. rj +
  331. '_img_main.jpg'
  332. workInfo.title = dom.getElementById('work_name').textContent
  333. workInfo.circle = dom.querySelector('span.maker_name').textContent
  334.  
  335. const table_outline = dom.querySelector('table#work_outline')
  336. for (var i = 0, ii = table_outline.rows.length; i < ii; i++) {
  337. const row = table_outline.rows[i]
  338. const row_header = row.cells[0].textContent
  339. const row_data = row.cells[1]
  340. switch (true) {
  341. case row_header.includes('贩卖日'):
  342. workInfo.date = row_data.textContent
  343. break
  344. case row_header.includes('年龄指定'):
  345. workInfo.rating = row_data.textContent
  346. break
  347. case row_header.includes('分类'):
  348. const tag_nodes = row_data.querySelectorAll('a')
  349. workInfo.tags = [...tag_nodes].map((a) => {
  350. return a.textContent
  351. })
  352. break
  353. case row_header.includes('声优'):
  354. workInfo.cv = row_data.textContent
  355. break
  356. case row_header.includes('文件容量'):
  357. workInfo.filesize = row_data.textContent.replace('合计', '').trim()
  358. break
  359. default:
  360. break
  361. }
  362. }
  363.  
  364. const work_date_ana = dom.querySelector('strong.work_date_ana')
  365. if (work_date_ana) {
  366. workInfo.dateAnnounce = work_date_ana.innerText
  367. workInfo.img =
  368. 'https://img.dlsite.jp/modpub/images2/ana/doujin/' +
  369. rj_group +
  370. '/' +
  371. rj +
  372. '_ana_img_main.jpg'
  373. }
  374. console.log(workInfo)
  375.  
  376. return workInfo
  377. },
  378.  
  379. request: function (rjCode, callback) {
  380. const url = `https://www.dlsite.com/maniax/work/=/product_id/${rjCode}.html/?locale=zh_CN`
  381. getXmlHttpRequest()({
  382. method: 'GET',
  383. url,
  384. headers: {
  385. Accept: 'text/xml',
  386. 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:67.0)',
  387. },
  388. onload: function (resp) {
  389. if (resp.readyState === 4 && resp.status === 200) {
  390. const dom = new DOMParser().parseFromString(
  391. resp.responseText,
  392. 'text/html',
  393. )
  394. const workInfo = DLsite.parseWorkDOM(dom, rjCode)
  395. callback(workInfo)
  396. } else if (resp.readyState === 4 && resp.status === 404)
  397. DLsite.requestAnnounce(rjCode, callback)
  398. },
  399. })
  400. },
  401.  
  402. requestAnnounce: function (rjCode, callback) {
  403. const url = `https://www.dlsite.com/maniax/announce/=/product_id/${rjCode}.html/?locale=ja_JP`
  404. getXmlHttpRequest()({
  405. method: 'GET',
  406. url,
  407. headers: {
  408. Accept: 'text/xml',
  409. 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:67.0)',
  410. },
  411. onload: function (resp) {
  412. if (resp.readyState === 4 && resp.status === 200) {
  413. const dom = new DOMParser().parseFromString(
  414. resp.responseText,
  415. 'text/html',
  416. )
  417. const workInfo = DLsite.parseWorkDOM(dom, rjCode)
  418. callback(workInfo)
  419. } else if (resp.readyState === 4 && resp.status === 404) callback(null)
  420. },
  421. })
  422. },
  423. }
  424.  
  425. document.addEventListener('DOMContentLoaded', function () {
  426. const style = document.createElement('style')
  427. style.innerHTML = css
  428. document.head.appendChild(style)
  429.  
  430. Parser.walkNodes(document.body)
  431.  
  432. const observer = new MutationObserver(function (m) {
  433. for (let i = 0; i < m.length; ++i) {
  434. let addedNodes = m[i].addedNodes
  435.  
  436. for (let j = 0; j < addedNodes.length; ++j) {
  437. Parser.walkNodes(addedNodes[j])
  438. }
  439. }
  440. })
  441.  
  442. document.addEventListener('securitypolicyviolation', function (e) {
  443. if (e.blockedURI.includes('img.dlsite.jp')) {
  444. const img = document.querySelector(`img[src="${e.blockedURI}"]`)
  445. img.remove()
  446. }
  447. })
  448.  
  449. observer.observe(document.body, { childList: true, subtree: true })
  450. })