koyomate

したらば掲示板を実況向けにするスクリプト

  1. // ==UserScript==
  2. // @name koyomate
  3. // @namespace gunjobiyori.com
  4. // @version 0.1.0
  5. // @description したらば掲示板を実況向けにするスクリプト
  6. // @author euro_s
  7. // @match https://jbbs.shitaraba.net/bbs/read.cgi/internet/*
  8. // @grant GM_addStyle
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @run-at document-start
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. "use strict";
  17. function add_replied_comment_loaded(replied_elem, has_anc_elem) {
  18. var dl = document.createElement("dl");
  19. var cp_replied_dt = replied_elem.cloneNode(true);
  20. dl.classList.add("rep-comment");
  21. dl.appendChild(cp_replied_dt);
  22. dl.style.display = "none";
  23. has_anc_elem.insertBefore(dl, has_anc_elem.firstElementChild);
  24. }
  25.  
  26. function add_replied_comment_xhr(replied_elem, has_anc_elem) {
  27. var dl = document.createElement("dl");
  28. var cp_replied_dt = replied_elem.cloneNode(true);
  29. var cp_replied_dd = replied_elem.nextElementSibling.cloneNode(true);
  30. dl.classList.add("rep-comment");
  31. dl.appendChild(cp_replied_dt);
  32. dl.appendChild(cp_replied_dd);
  33. dl.style.display = "none";
  34. has_anc_elem.insertBefore(dl, has_anc_elem.firstElementChild);
  35. }
  36.  
  37. function getBbsUrl() {
  38. const url = window.location.href;
  39. const splitted = url.split('/');
  40. // URL ex: https://jbbs.shitaraba.net/bbs/read.cgi/internet/25835/1688993025/
  41. // splitted: ["https:", "", "jbbs.shitaraba.net", "bbs", "read.cgi", "internet", "25835", "1688993025", ""]
  42. return `https://${splitted[2]}/${splitted[5]}/${splitted[6]}`;
  43. }
  44.  
  45. // Function to update max-width of .img-popup
  46. function updateMaxWidth() {
  47. // Calculate max width as 80% of window's width
  48. let maxWidth = window.innerWidth * 0.8;
  49.  
  50. // Get all .img-popup elements and update their max-width
  51. let popups = document.querySelectorAll('.img-popup');
  52. popups.forEach(popup => {
  53. popup.style.maxWidth = maxWidth + 'px';
  54. });
  55. }
  56.  
  57. function reStyle() {
  58. const thread_body = document.getElementById("thread-body");
  59. var dts = Array.from(document.querySelectorAll("dl#thread-body > dt"));
  60. var dds = Array.from(document.querySelectorAll("dl#thread-body > dd"));
  61. var tables = Array.from(document.querySelectorAll("table"));
  62.  
  63. // Clear the original elements
  64. dts.forEach((dt) => dt.remove());
  65. dds.forEach((dd) => dd.remove());
  66. tables.forEach((table) => table.remove());
  67.  
  68. // Combine the dt and dd contents and append them to the parent
  69. dts.forEach((dt, index) => {
  70. let outerDiv = document.createElement("div");
  71. outerDiv.id = dt.id;
  72. outerDiv.classList.add("comment");
  73. let meta = document.createElement("span");
  74. meta.innerText = dt.querySelector("a").innerText + ": ";
  75. let comment = document.createElement("span");
  76. let aTags = dds[index].querySelectorAll("a");
  77. let imageLinks = Array.from(aTags).filter(a => a.innerText.match(/\.(jpeg|jpg|gif|png)$/i) !== null);
  78. imageLinks.forEach((link) => {
  79. let popup = document.createElement('img');
  80. popup.src = link.innerText;
  81. popup.className = 'img-popup';
  82. link.appendChild(popup);
  83. });
  84. // Update the max-width of all .img-popup elements
  85. updateMaxWidth();
  86. if (dt.querySelector("a").innerText == "1") {
  87. comment.innerHTML = dds[index].innerHTML;
  88. } else {
  89. comment.innerHTML = dds[index].innerHTML.replace(/<br>/g, " ").trim();
  90. }
  91. outerDiv.appendChild(meta);
  92. outerDiv.appendChild(comment);
  93.  
  94. thread_body.appendChild(outerDiv);
  95. });
  96.  
  97. const small = document.querySelector('body > small');
  98. if (small) {
  99. const aTags = small.querySelectorAll('a');
  100. aTags.forEach((a) => a.remove());
  101. const bbsUrl = getBbsUrl();
  102. const a = document.createElement('a');
  103. a.href = bbsUrl;
  104. a.innerText = '掲示板に戻る';
  105. small.appendChild(a);
  106. }
  107. }
  108.  
  109. function add_replied_comment() {
  110. var has_anc = document.querySelectorAll("#thread-body > div > span > span.res");
  111. var reg = /\/(\d+)$/;
  112. for (var i = 0; i < has_anc.length; i++) {
  113. var replied_url = has_anc[i].querySelector('a').getAttribute('href');
  114. var reg_result = reg.exec(replied_url);
  115. var replied_id;
  116. if (reg_result) {
  117. replied_id = reg_result[1];
  118. } else {
  119. continue;
  120. }
  121. var replied_elem = document.getElementById("comment_" + replied_id);
  122. if (replied_elem) {
  123. add_replied_comment_loaded(replied_elem, has_anc[i]);
  124. has_anc[i].addEventListener("mouseenter", function () {
  125. this.firstElementChild.style.display = "";
  126. });
  127. } else {
  128. has_anc[i].addEventListener("mouseenter", {
  129. replied_id: replied_id,
  130. replied_url: replied_url,
  131. has_anc_elem: has_anc[i],
  132. handleEvent: function () {
  133. if (this.has_anc_elem.firstElementChild.tagName === "DL") {
  134. this.has_anc_elem.firstElementChild.style.display = "";
  135. } else {
  136. const xhr = new XMLHttpRequest();
  137. xhr.responseType = "document";
  138. xhr.open("get", this.replied_url, true);
  139. xhr.timeout = 5 * 1000;
  140. xhr.addEventListener("load", {
  141. replied_id: this.replied_id,
  142. has_anc_elem: this.has_anc_elem,
  143. handleEvent: function (res) {
  144. if (res.target.status !== 200) {
  145. return;
  146. }
  147. replied_elem = res.target.responseXML.getElementById("comment_" + this.replied_id);
  148. console.log(replied_elem);
  149. if (replied_elem) {
  150. add_replied_comment_xhr(replied_elem, this.has_anc_elem);
  151. }
  152. this.has_anc_elem.firstElementChild.style.display = "";
  153. }
  154. });
  155. xhr.send();
  156. }
  157. }
  158. });
  159.  
  160. }
  161. has_anc[i].addEventListener("mouseleave", function () {
  162. if (this.firstElementChild.tagName === "DL") {
  163. this.firstElementChild.style.display = "none";
  164. }
  165. });
  166. }
  167. }
  168.  
  169. function upDownButtons() {
  170. // Create a new button element for scrolling to bottom
  171. const buttonDown = document.createElement("button");
  172. buttonDown.id = "scrollToBottomButton";
  173. buttonDown.innerHTML = `
  174. <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMCIgaGVpZ2h0PSIzMCIgdmlld0JveD0iMCAwIDMwIDMwIj4KICAgIDxwYXRoIGQ9Ik0xNSAyMEw1IDEwaDIwbC0xMCAxMHoiIGZpbGw9ImJsYWNrIi8+Cjwvc3ZnPgo="/>
  175. `;
  176.  
  177. // Create a new button element for scrolling to top
  178. const buttonUp = document.createElement("button");
  179. buttonUp.id = "scrollToTopButton";
  180. buttonUp.innerHTML = `
  181. <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMCIgaGVpZ2h0PSIzMCIgdmlld0JveD0iMCAwIDMwIDMwIj4KICAgIDxwYXRoIGQ9Ik0xNSAxMEw1IDIwaDIwbC0xMCAtMTB6IiBmaWxsPSJibGFjayIvPgo8L3N2Zz4K"/>
  182. `;
  183.  
  184. // Add the buttons to the document body
  185. document.body.append(buttonDown, buttonUp);
  186.  
  187. // Attach an event listener to the buttons to handle clicks
  188. buttonDown.addEventListener("click", function () {
  189. window.scrollTo({
  190. top: document.body.scrollHeight, // Scroll to the bottom of the page
  191. behavior: "smooth", // Animate the scroll
  192. });
  193. });
  194.  
  195. buttonUp.addEventListener("click", function () {
  196. window.scrollTo({
  197. top: 0, // Scroll to the top of the page
  198. behavior: "smooth", // Animate the scroll
  199. });
  200. });
  201. }
  202.  
  203. let isAutoReloading = true;
  204. function createProgressBar() {
  205. // Create the SVG element
  206. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  207. svg.setAttribute("width", "40");
  208. svg.setAttribute("height", "40");
  209. svg.style.position = "fixed";
  210. svg.style.right = "20px";
  211. svg.style.top = "120px";
  212. svg.style.cursor = "pointer";
  213.  
  214. // Create the background circle
  215. const bgCircle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
  216. bgCircle.setAttribute("cx", "20");
  217. bgCircle.setAttribute("cy", "20");
  218. bgCircle.setAttribute("r", "16");
  219. bgCircle.setAttribute("stroke", "#ddd");
  220. bgCircle.setAttribute("stroke-width", "4");
  221. bgCircle.setAttribute("fill", "none");
  222. svg.appendChild(bgCircle);
  223.  
  224. // Create the foreground circle (progress bar)
  225. const fgCircle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
  226. fgCircle.setAttribute("cx", "20");
  227. fgCircle.setAttribute("cy", "20");
  228. fgCircle.setAttribute("r", "16");
  229. fgCircle.setAttribute("stroke", "#3498db");
  230. fgCircle.setAttribute("stroke-width", "4");
  231. fgCircle.setAttribute("fill", "none");
  232. fgCircle.style.strokeDasharray = "113.04"; // 2 * PI * r (approx.)
  233. fgCircle.style.strokeDashoffset = "113.04";
  234. fgCircle.style.transform = "rotate(-90deg)";
  235. fgCircle.style.transformOrigin = "50% 50%";
  236. svg.addEventListener("click", function () {
  237. isAutoReloading = !isAutoReloading; // toggle auto reloading
  238. // Change the color of the progress bar based on the auto reloading status
  239. fgCircle.setAttribute("stroke", isAutoReloading ? "#3498db" : "#e74c3c");
  240. });
  241. svg.appendChild(fgCircle);
  242.  
  243. document.body.appendChild(svg);
  244. return fgCircle;
  245. }
  246.  
  247. let progressBar;
  248. function updateProgressBar(timeElapsed, totalTime) {
  249. const progress = timeElapsed / totalTime;
  250. const strokeLength = 113.04 * progress;
  251. progressBar.style.strokeDashoffset = 113.04 - strokeLength;
  252. }
  253.  
  254. async function autoReload() {
  255. progressBar = createProgressBar();
  256.  
  257. let elapsed = 0;
  258.  
  259. async function run() {
  260. if (isAutoReloading) {
  261. elapsed += 50; // update every 50 msec
  262. updateProgressBar(elapsed, 5000);
  263.  
  264. if (elapsed >= 5000) {
  265. await getMessage();
  266. elapsed = 0;
  267. }
  268. setTimeout(run, 50); // set the next run
  269. }
  270. }
  271.  
  272. run(); // initial run
  273. }
  274.  
  275. let toBottom = true;
  276. let lastScrollTop = 0;
  277. const FETCH_TIMEOUT = 1000;
  278. async function getMessage() {
  279. const lastMsg = document.querySelector("dl#thread-body > div.comment:last-child");
  280. const lastId = lastMsg.id.replace('comment_', '');
  281. const url = location.href + lastId + '-n';
  282.  
  283. try {
  284. const response = await Promise.race([
  285. fetch(url),
  286. new Promise((_, reject) =>
  287. setTimeout(() => reject(new Error('Timeout')), FETCH_TIMEOUT)
  288. )
  289. ]);
  290. if (!response.ok) {
  291. throw new Error(`HTTP error! status: ${response.status}`);
  292. } else {
  293. const arrayBuffer = await response.arrayBuffer();
  294. const html = new TextDecoder("euc-jp").decode(arrayBuffer);
  295. const parser = new DOMParser();
  296. const doc = parser.parseFromString(html, "text/html");
  297. var dts = Array.from(doc.querySelectorAll("dl#thread-body > dt"));
  298. var dds = Array.from(doc.querySelectorAll("dl#thread-body > dd"));
  299. for (let i = 0; i < dts.length; i++) {
  300. if (dts[i].id == lastMsg.id) {
  301. continue;
  302. }
  303. let outerDiv = document.createElement("div");
  304. outerDiv.id = dts[i].id;
  305. outerDiv.classList.add("comment");
  306. let meta = document.createElement("span");
  307. meta.innerText = dts[i].querySelector("a").innerText + ": ";
  308. let aTags = dds[i].querySelectorAll("a");
  309. let imageLinks = Array.from(aTags).filter(a => a.innerText.match(/\.(jpeg|jpg|gif|png)$/i) !== null);
  310. imageLinks.forEach((link) => {
  311. let popup = document.createElement('img');
  312. popup.src = link.innerText;
  313. popup.className = 'img-popup';
  314. link.appendChild(popup);
  315. });
  316. let comment = document.createElement("span");
  317. comment.innerHTML = dds[i].innerHTML.replace(/<br>/g, " ").trim();
  318. outerDiv.appendChild(meta);
  319. outerDiv.appendChild(comment);
  320.  
  321. lastMsg.parentNode.appendChild(outerDiv);
  322. }
  323.  
  324. if (toBottom) {
  325. window.scrollTo(0, document.body.scrollHeight);
  326. }
  327. }
  328. } catch (e) {
  329. console.error('Fetch failed!', e);
  330. }
  331. }
  332.  
  333. function bottomEvent() {
  334. window.addEventListener("scroll", function () {
  335. const st = window.scrollY;
  336. // Check if we're at the bottom of the page
  337. if (st < lastScrollTop) {
  338. toBottom = false;
  339. } else if (
  340. window.innerHeight + window.scrollY >=
  341. document.body.offsetHeight
  342. ) {
  343. toBottom = true;
  344. }
  345. lastScrollTop = st <= 0 ? 0 : st;
  346. });
  347. }
  348.  
  349. // Ensure the operation is performed after the DOM is fully loaded
  350. window.addEventListener(
  351. "load",
  352. async function () {
  353. new MutationObserver(add_replied_comment).observe(
  354. document.querySelector('#thread-body'), { childList: true }
  355. );
  356. reStyle();
  357. add_replied_comment();
  358. upDownButtons();
  359. createProgressBar();
  360. bottomEvent();
  361. await autoReload();
  362. // Scroll to the bottom of the page
  363. window.scrollTo({
  364. top: document.body.scrollHeight,
  365. behavior: "smooth",
  366. });
  367. },
  368. false
  369. );
  370.  
  371. // Update the max-width of all .img-popup elements whenever the window is resized
  372. window.addEventListener('resize', updateMaxWidth, false);
  373.  
  374. // ex. https://jbbs.shitaraba.net/bbs/read.cgi/internet/25835/1688993025/413-n
  375. const match = window.location.href.match(/(.+internet\/\d+\/\d+\/).+$/);
  376. if (match) {
  377. const url = match[1];
  378. window.location.href = url;
  379. }
  380.  
  381. ////////////////////////////////////////////////////////////////////////////////
  382. // CSS
  383. ////////////////////////////////////////////////////////////////////////////////
  384. GM_addStyle(`
  385. #thread-body {
  386. margin-left: 30px !important;
  387. margin-right: 30px !important;
  388. line-height: 2rem !important;
  389. }
  390. .site-header {
  391. display: none !important;
  392. }
  393. #new_response {
  394. display: none !important;
  395. }
  396. #g_floating_tag_zone {
  397. display: none !important;
  398. }
  399. #scrollToBottomButton, #scrollToTopButton {
  400. position: fixed;
  401. right: 20px;
  402. z-index: 10000;
  403. padding: 5px;
  404. cursor: pointer;
  405. background: #ddd;
  406. border: none;
  407. border-radius: 5px;
  408. transition: background 0.2s;
  409. }
  410. #scrollToBottomButton {
  411. top: 70px;
  412. }
  413. #scrollToTopButton {
  414. top: 20px;
  415. }
  416. #scrollToBottomButton:hover, #scrollToTopButton:hover {
  417. background: #bbb;
  418. }
  419. .img-popup {
  420. display: none;
  421. position: absolute;
  422. z-index: 1;
  423. border: 1px solid #ddd;
  424. }
  425. a:hover .img-popup {
  426. display: block;
  427. }
  428. `);
  429. })();