koyomate

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         koyomate
// @namespace    gunjobiyori.com
// @version      0.1.0
// @description  したらば掲示板を実況向けにするスクリプト
// @author       euro_s
// @match        https://jbbs.shitaraba.net/bbs/read.cgi/internet/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function () {
    "use strict";
    function add_replied_comment_loaded(replied_elem, has_anc_elem) {
        var dl = document.createElement("dl");
        var cp_replied_dt = replied_elem.cloneNode(true);
        dl.classList.add("rep-comment");
        dl.appendChild(cp_replied_dt);
        dl.style.display = "none";
        has_anc_elem.insertBefore(dl, has_anc_elem.firstElementChild);
    }

    function add_replied_comment_xhr(replied_elem, has_anc_elem) {
        var dl = document.createElement("dl");
        var cp_replied_dt = replied_elem.cloneNode(true);
        var cp_replied_dd = replied_elem.nextElementSibling.cloneNode(true);
        dl.classList.add("rep-comment");
        dl.appendChild(cp_replied_dt);
        dl.appendChild(cp_replied_dd);
        dl.style.display = "none";
        has_anc_elem.insertBefore(dl, has_anc_elem.firstElementChild);
    }

    function getBbsUrl() {
      const url = window.location.href;
      const splitted = url.split('/');
      // URL ex: https://jbbs.shitaraba.net/bbs/read.cgi/internet/25835/1688993025/
      // splitted: ["https:", "", "jbbs.shitaraba.net", "bbs", "read.cgi", "internet", "25835", "1688993025", ""]
      return `https://${splitted[2]}/${splitted[5]}/${splitted[6]}`;
    }

    // Function to update max-width of .img-popup
    function updateMaxWidth() {
        // Calculate max width as 80% of window's width
        let maxWidth = window.innerWidth * 0.8;

        // Get all .img-popup elements and update their max-width
        let popups = document.querySelectorAll('.img-popup');
        popups.forEach(popup => {
            popup.style.maxWidth = maxWidth + 'px';
        });
    }

    function reStyle() {
        const thread_body = document.getElementById("thread-body");
        var dts = Array.from(document.querySelectorAll("dl#thread-body > dt"));
        var dds = Array.from(document.querySelectorAll("dl#thread-body > dd"));
        var tables = Array.from(document.querySelectorAll("table"));

        // Clear the original elements
        dts.forEach((dt) => dt.remove());
        dds.forEach((dd) => dd.remove());
        tables.forEach((table) => table.remove());

        // Combine the dt and dd contents and append them to the parent
        dts.forEach((dt, index) => {
            let outerDiv = document.createElement("div");
            outerDiv.id = dt.id;
            outerDiv.classList.add("comment");
            let meta = document.createElement("span");
            meta.innerText = dt.querySelector("a").innerText + ": ";
            let comment = document.createElement("span");
            let aTags = dds[index].querySelectorAll("a");
            let imageLinks = Array.from(aTags).filter(a => a.innerText.match(/\.(jpeg|jpg|gif|png)$/i) !== null);
            imageLinks.forEach((link) => {
              let popup = document.createElement('img');
              popup.src = link.innerText;
              popup.className = 'img-popup';
              link.appendChild(popup);
            });
            // Update the max-width of all .img-popup elements
            updateMaxWidth();
            if (dt.querySelector("a").innerText == "1") {
                comment.innerHTML = dds[index].innerHTML;
            } else {
                comment.innerHTML = dds[index].innerHTML.replace(/<br>/g, " ").trim();
            }
            outerDiv.appendChild(meta);
            outerDiv.appendChild(comment);

            thread_body.appendChild(outerDiv);
        });

        const small = document.querySelector('body > small');
        if (small) {
          const aTags = small.querySelectorAll('a');
          aTags.forEach((a) => a.remove());
          const bbsUrl = getBbsUrl();
          const a = document.createElement('a');
          a.href = bbsUrl;
          a.innerText = '掲示板に戻る';
          small.appendChild(a);
        }
    }

    function add_replied_comment() {
        var has_anc = document.querySelectorAll("#thread-body > div > span > span.res");
        var reg = /\/(\d+)$/;
        for (var i = 0; i < has_anc.length; i++) {
            var replied_url = has_anc[i].querySelector('a').getAttribute('href');
            var reg_result = reg.exec(replied_url);
            var replied_id;
            if (reg_result) {
                replied_id = reg_result[1];
            } else {
                continue;
            }
            var replied_elem = document.getElementById("comment_" + replied_id);
            if (replied_elem) {
                add_replied_comment_loaded(replied_elem, has_anc[i]);
                has_anc[i].addEventListener("mouseenter", function () {
                    this.firstElementChild.style.display = "";
                });
            } else {
                has_anc[i].addEventListener("mouseenter", {
                    replied_id: replied_id,
                    replied_url: replied_url,
                    has_anc_elem: has_anc[i],
                    handleEvent: function () {
                        if (this.has_anc_elem.firstElementChild.tagName === "DL") {
                            this.has_anc_elem.firstElementChild.style.display = "";
                        } else {
                            const xhr = new XMLHttpRequest();
                            xhr.responseType = "document";
                            xhr.open("get", this.replied_url, true);
                            xhr.timeout = 5 * 1000;
                            xhr.addEventListener("load", {
                                replied_id: this.replied_id,
                                has_anc_elem: this.has_anc_elem,
                                handleEvent: function (res) {
                                    if (res.target.status !== 200) {
                                        return;
                                    }
                                    replied_elem = res.target.responseXML.getElementById("comment_" + this.replied_id);
                                    console.log(replied_elem);
                                    if (replied_elem) {
                                        add_replied_comment_xhr(replied_elem, this.has_anc_elem);
                                    }
                                    this.has_anc_elem.firstElementChild.style.display = "";
                                }
                            });
                            xhr.send();
                        }
                    }
                });

            }
            has_anc[i].addEventListener("mouseleave", function () {
                if (this.firstElementChild.tagName === "DL") {
                    this.firstElementChild.style.display = "none";
                }
            });
        }
    }

    function upDownButtons() {
        // Create a new button element for scrolling to bottom
        const buttonDown = document.createElement("button");
        buttonDown.id = "scrollToBottomButton";
        buttonDown.innerHTML = `
        <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMCIgaGVpZ2h0PSIzMCIgdmlld0JveD0iMCAwIDMwIDMwIj4KICAgIDxwYXRoIGQ9Ik0xNSAyMEw1IDEwaDIwbC0xMCAxMHoiIGZpbGw9ImJsYWNrIi8+Cjwvc3ZnPgo="/>
        `;

        // Create a new button element for scrolling to top
        const buttonUp = document.createElement("button");
        buttonUp.id = "scrollToTopButton";
        buttonUp.innerHTML = `
        <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMCIgaGVpZ2h0PSIzMCIgdmlld0JveD0iMCAwIDMwIDMwIj4KICAgIDxwYXRoIGQ9Ik0xNSAxMEw1IDIwaDIwbC0xMCAtMTB6IiBmaWxsPSJibGFjayIvPgo8L3N2Zz4K"/>
        `;

        // Add the buttons to the document body
        document.body.append(buttonDown, buttonUp);

        // Attach an event listener to the buttons to handle clicks
        buttonDown.addEventListener("click", function () {
          window.scrollTo({
            top: document.body.scrollHeight, // Scroll to the bottom of the page
            behavior: "smooth", // Animate the scroll
          });
        });

        buttonUp.addEventListener("click", function () {
          window.scrollTo({
            top: 0, // Scroll to the top of the page
            behavior: "smooth", // Animate the scroll
          });
        });
      }

      let isAutoReloading = true;
      function createProgressBar() {
        // Create the SVG element
        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svg.setAttribute("width", "40");
        svg.setAttribute("height", "40");
        svg.style.position = "fixed";
        svg.style.right = "20px";
        svg.style.top = "120px";
        svg.style.cursor = "pointer";

        // Create the background circle
        const bgCircle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
        bgCircle.setAttribute("cx", "20");
        bgCircle.setAttribute("cy", "20");
        bgCircle.setAttribute("r", "16");
        bgCircle.setAttribute("stroke", "#ddd");
        bgCircle.setAttribute("stroke-width", "4");
        bgCircle.setAttribute("fill", "none");
        svg.appendChild(bgCircle);

        // Create the foreground circle (progress bar)
        const fgCircle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
        fgCircle.setAttribute("cx", "20");
        fgCircle.setAttribute("cy", "20");
        fgCircle.setAttribute("r", "16");
        fgCircle.setAttribute("stroke", "#3498db");
        fgCircle.setAttribute("stroke-width", "4");
        fgCircle.setAttribute("fill", "none");
        fgCircle.style.strokeDasharray = "113.04"; // 2 * PI * r (approx.)
        fgCircle.style.strokeDashoffset = "113.04";
        fgCircle.style.transform = "rotate(-90deg)";
        fgCircle.style.transformOrigin = "50% 50%";
        svg.addEventListener("click", function () {
          isAutoReloading = !isAutoReloading; // toggle auto reloading
          // Change the color of the progress bar based on the auto reloading status
          fgCircle.setAttribute("stroke", isAutoReloading ? "#3498db" : "#e74c3c");
        });
        svg.appendChild(fgCircle);

        document.body.appendChild(svg);
        return fgCircle;
      }

      let progressBar;
      function updateProgressBar(timeElapsed, totalTime) {
        const progress = timeElapsed / totalTime;
        const strokeLength = 113.04 * progress;
        progressBar.style.strokeDashoffset = 113.04 - strokeLength;
      }

      async function autoReload() {
        progressBar = createProgressBar();

        let elapsed = 0;

        async function run() {
          if (isAutoReloading) {
            elapsed += 50; // update every 50 msec
            updateProgressBar(elapsed, 5000);

            if (elapsed >= 5000) {
              await getMessage();
              elapsed = 0;
            }
            setTimeout(run, 50); // set the next run
          }
        }

        run(); // initial run
      }

      let toBottom = true;
      let lastScrollTop = 0;
      const FETCH_TIMEOUT = 1000;
      async function getMessage() {
        const lastMsg = document.querySelector("dl#thread-body > div.comment:last-child");
        const lastId = lastMsg.id.replace('comment_', '');
        const url = location.href + lastId + '-n';

        try {
          const response = await Promise.race([
            fetch(url),
            new Promise((_, reject) =>
              setTimeout(() => reject(new Error('Timeout')), FETCH_TIMEOUT)
            )
          ]);
          if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
          } else {
            const arrayBuffer = await response.arrayBuffer();
            const html = new TextDecoder("euc-jp").decode(arrayBuffer);
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, "text/html");
            var dts = Array.from(doc.querySelectorAll("dl#thread-body > dt"));
            var dds = Array.from(doc.querySelectorAll("dl#thread-body > dd"));
            for (let i = 0; i < dts.length; i++) {
                if (dts[i].id == lastMsg.id) {
                    continue;
                }
                let outerDiv = document.createElement("div");
                outerDiv.id = dts[i].id;
                outerDiv.classList.add("comment");
                let meta = document.createElement("span");
                meta.innerText = dts[i].querySelector("a").innerText + ": ";
                let aTags = dds[i].querySelectorAll("a");
                let imageLinks = Array.from(aTags).filter(a => a.innerText.match(/\.(jpeg|jpg|gif|png)$/i) !== null);
                imageLinks.forEach((link) => {
                  let popup = document.createElement('img');
                  popup.src = link.innerText;
                  popup.className = 'img-popup';
                  link.appendChild(popup);
                });
                let comment = document.createElement("span");
                comment.innerHTML = dds[i].innerHTML.replace(/<br>/g, " ").trim();
                outerDiv.appendChild(meta);
                outerDiv.appendChild(comment);

                lastMsg.parentNode.appendChild(outerDiv);
            }

            if (toBottom) {
              window.scrollTo(0, document.body.scrollHeight);
            }
          }
        } catch (e) {
          console.error('Fetch failed!', e);
        }
      }

      function bottomEvent() {
        window.addEventListener("scroll", function () {
          const st = window.scrollY;
          // Check if we're at the bottom of the page
          if (st < lastScrollTop) {
            toBottom = false;
          } else if (
            window.innerHeight + window.scrollY >=
            document.body.offsetHeight
          ) {
            toBottom = true;
          }
          lastScrollTop = st <= 0 ? 0 : st;
        });
      }

    // Ensure the operation is performed after the DOM is fully loaded
    window.addEventListener(
        "load",
        async function () {
            new MutationObserver(add_replied_comment).observe(
                document.querySelector('#thread-body'), { childList: true }
            );
            reStyle();
            add_replied_comment();
            upDownButtons();
            createProgressBar();
            bottomEvent();
            await autoReload();
            // Scroll to the bottom of the page
            window.scrollTo({
              top: document.body.scrollHeight,
              behavior: "smooth",
            });
        },
        false
    );

    // Update the max-width of all .img-popup elements whenever the window is resized
    window.addEventListener('resize', updateMaxWidth, false);

    // ex. https://jbbs.shitaraba.net/bbs/read.cgi/internet/25835/1688993025/413-n
    const match = window.location.href.match(/(.+internet\/\d+\/\d+\/).+$/);
    if (match) {
        const url = match[1];
        window.location.href = url;
    }

    ////////////////////////////////////////////////////////////////////////////////
    // CSS
    ////////////////////////////////////////////////////////////////////////////////
    GM_addStyle(`
  #thread-body {
    margin-left: 30px !important;
    margin-right: 30px !important;
    line-height: 2rem !important;
  }
  .site-header {
    display: none !important;
  }
  #new_response {
    display: none !important;
  }
  #g_floating_tag_zone {
    display: none !important;
  }
  #scrollToBottomButton, #scrollToTopButton {
    position: fixed;
    right: 20px;
    z-index: 10000;
    padding: 5px;
    cursor: pointer;
    background: #ddd;
    border: none;
    border-radius: 5px;
    transition: background 0.2s;
  }
  #scrollToBottomButton {
    top: 70px;
  }
  #scrollToTopButton {
    top: 20px;
  }
  #scrollToBottomButton:hover, #scrollToTopButton:hover {
    background: #bbb;
  }
  .img-popup {
      display: none;
      position: absolute;
      z-index: 1;
      border: 1px solid #ddd;
  }
  a:hover .img-popup {
      display: block;
  }
  `);
})();