Multi Forum Read Marker (Soutong + TT1069)

标记已访问帖子,支持搜同和TT1069,使用localStorage + beforeunload防止快速切换丢记录,标记文字标题左侧。

2025-06-08 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

// ==UserScript==
// @name         Multi Forum Read Marker (Soutong + TT1069)
// @namespace    https://felixchristian.dev/userscripts/multi-forum-read-marker
// @version      1.3.0
// @description  标记已访问帖子,支持搜同和TT1069,使用localStorage + beforeunload防止快速切换丢记录,标记文字标题左侧。
// @author       FelixChristian
// @license      MIT
// @match        https://soutong.men/forum.php?mod=forumdisplay&fid=*
// @match        https://soutong.men/forum.php?mod=viewthread&tid=*
// @match        https://www.tt1069.com/bbs/thread-*-*-*.html
// @match        https://www.tt1069.com/bbs/forum-*-*.html
// @match        https://www.tt1069.com/bbs/forum.php?mod=forumdisplay&fid=*
// @match        https://www.tt1069.com/bbs/forum.php?mod=forumdisplay&fid=*&*
// @match        https://www.tt1069.com/bbs/forum.php?mod=viewthread&tid=*
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function () {
    'use strict';

    const STORAGE_KEY = 'visitedTids';

    // 先尝试读取GM存储的记录,如果localStorage里有临时数据用临时数据
    let visitedTids = {};
    try {
        visitedTids = GM_getValue(STORAGE_KEY, {});
        const localData = localStorage.getItem(STORAGE_KEY + '_temp');
        if (localData) {
            const localObj = JSON.parse(localData);
            // 合并localStorage里更“新”的访问数据
            visitedTids = {...visitedTids, ...localObj};
        }
    } catch (e) {
        // 读取失败忽略
        visitedTids = {};
    }

    const HOST = location.hostname;
    const isSoutong = HOST.includes('soutong.men');
    const isTT1069 = HOST.includes('tt1069.com');

    function getCurrentTid() {
        if (location.href.includes('tid=')) {
            try {
                const url = new URL(location.href);
                return url.searchParams.get('tid');
            } catch (e) {
                return null;
            }
        }
        const match = location.href.match(/thread-(\d+)-/);
        return match ? match[1] : null;
    }

    // 保存访问记录到本地缓存和 localStorage(同步)
    function saveCurrentTid(tid) {
        if (!tid) return;
        visitedTids[tid] = Date.now();
        try {
            localStorage.setItem(STORAGE_KEY + '_temp', JSON.stringify(visitedTids));
        } catch (e) {
            // localStorage写入失败忽略
        }
    }

    // 离开页面时写回GM存储,确保持久化
    window.addEventListener('beforeunload', () => {
        try {
            const localData = localStorage.getItem(STORAGE_KEY + '_temp');
            if (localData) {
                const obj = JSON.parse(localData);
                GM_setValue(STORAGE_KEY, obj);
                // 清理临时数据(可选)
                localStorage.removeItem(STORAGE_KEY + '_temp');
            }
        } catch (e) {
            // 忽略错误
        }
    });

    // 标记已访问帖子
    function markReadThreads() {
        const threadLinks = document.querySelectorAll('a.s.xst');

        threadLinks.forEach(link => {
            let tid = null;
            try {
                const fullUrl = new URL(link.href, location.origin);
                tid = fullUrl.searchParams.get('tid');
                if (!tid) {
                    const match = link.href.match(/thread-(\d+)-/);
                    tid = match ? match[1] : null;
                }
            } catch (e) {
                return;
            }

            if (tid && visitedTids[tid] && !link.dataset.markedVisited) {
                const tag = document.createElement('span');
                tag.textContent = '[已读] ';
                tag.style.color = 'red';
                tag.style.fontWeight = 'bold';
                tag.style.marginRight = '4px';
                link.insertBefore(tag, link.firstChild);
                link.dataset.markedVisited = 'true';
            }
        });
    }

    const isViewThread = location.href.includes('mod=viewthread') || /thread-\d+-/.test(location.pathname);
    const isForumDisplay = location.href.includes('mod=forumdisplay') || /forum-\d+-\d+\.html/.test(location.pathname);

    // 当前是帖子详情页 => 记录访问
    if (isViewThread) {
        const tid = getCurrentTid();
        saveCurrentTid(tid);
    }

    // 当前是论坛列表页 => 标记已访问帖子
    if (isForumDisplay) {
        window.addEventListener('load', markReadThreads);
        const observer = new MutationObserver(markReadThreads);
        observer.observe(document.body, {childList: true, subtree: true});
    }
})();