koyomate

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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;
  }
  `);
})();