IMASBBS Favs

IMASBBSのスレをふぁぼる機能を追加します。

// ==UserScript==
// @name         IMASBBS Favs
// @namespace    https://github.com/sakuro/
// @version      1.1.0
// @description  IMASBBSのスレをふぁぼる機能を追加します。
// @author       sakuro
// @match        http://imasbbs.com/
// @match        http://imasbbs.com/patio.cgi*
// @match        https://imasbbs.com/
// @match        https://imasbbs.com/patio.cgi*
// @icon         https://www.google.com/s2/favicons?domain=imasbbs.com
// @grant        none
// @license      MIT
// ==/UserScript==

// jshint esversion: 6
(function () {
    'use strict';

    const styles = document.styleSheets[0];
    styles.insertRule(".starred { color: #fc0; text-shadow: 1px 1px 1px #666; }");
    styles.insertRule(".star { color: #000; cursor: pointer }");

    class Stars {
        constructor() {
            this.load();
        }

        add(thread) {
            this.stars.set(thread, new Date());
            this.save();
        }

        updateTimeStamp(thread) {
            this.stars.set(thread, new Date());
            this.save();
        }

        remove(thread) {
            this.stars.delete(thread);
            this.save();
        }

        isStarred(thread) {
            return this.stars.has(thread);
        }

        load() {
            const data = localStorage.getItem("stars");
            this.stars = data ? new Map(JSON.parse(data).map((e) => [e[0], new Date(e[1])])) : new Map();
        }

        save() {
            if (this.stars.size == 0) {
                localStorage.removeItem("stars");
            } else {
                localStorage.setItem("stars", JSON.stringify([...this.stars]));
            }
        }

        clear() {
            this.stars = new Map();
            this.save();
        }
    }

    const stars = new Stars();

    class Star {
        constructor(thread, starred) {
            this.thread = thread;
            this.starred = starred;
            this.element = document.createElement("span");
            this.element.classList.add("star");
            this.element.addEventListener("click", (e) => {
                if (stars.isStarred(thread)) {
                    this.unstar();
                } else {
                    this.star();
                }
            });
            if (starred) {
                this.star();
            } else {
                this.unstar();
            }
        }

        star() {
            this.starred = true;
            this.element.classList.add("starred");
            this.element.textContent = "★";
            stars.add(this.thread);
        }

        unstar() {
            this.starred = false;
            this.element.classList.remove("starred");
            this.element.textContent = "☆";
            stars.remove(this.thread);
        }
    }

    const threadList = document.querySelector("table.bbs-item");
    const thread = new URLSearchParams(location.search).get("read");

    const rowId = (row) => {
        const link = row.querySelector("td:nth-child(2) a");
        const params = new URLSearchParams(new URL(link.href, location).search);
        return params.get("read");
    };

    const compare = (a, b) => {
        const aid = rowId(a);
        const bid = rowId(b);

        const aStar = stars.isStarred(aid);
        const bStar = stars.isStarred(bid);

        const aText = a.querySelector('td.w14e').textContent;
        const bText = b.querySelector('td.w14e').textContent;
        if (aStar < bStar) { return 1; }
        if (aStar > bStar) { return -1; }
        return bText.localeCompare(aText);
    };

    if (threadList) { // 一覧ページ
        const [head, ...rows] = threadList.querySelectorAll("tr");
        const title = head.querySelector("th:nth-child(1)");

        const array = Array.from(rows);
        const sortedRows = array.slice(1).sort((a, b) => compare(a, b));

        threadList.innerHTML = '';
        threadList.appendChild(head);
        sortedRows.forEach((row) => {
            threadList.appendChild(row);
            const title = row.querySelector("td:nth-child(2)");
            const link = title.querySelector("a");
            const tid = new URLSearchParams(new URL(link.attributes.getNamedItem("href").value, location).search).get("read");
            const star = new Star(tid, stars.isStarred(tid));

            title.insertAdjacentElement("afterbegin", star.element);
            link.addEventListener('click', (e) => {
                if (stars.isStarred(tid)) {
                    stars.updateTimeStamp(tid);
                }
            });
        });
    } else if (thread) { // スレページ
        const title = document.querySelector("#tr-ttl");
        const star = new Star(thread, stars.isStarred(thread));
        title.insertAdjacentElement("afterbegin", star.element);
    }
})();