VoiceLinks

Makes RJ codes more useful.(8-bit RJCode supported.)

Verze ze dne 16. 03. 2024. Zobrazit nejnovější verzi.

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