Multi Forum Read Marker (Soutong + TT1069)

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

Version au 08/06/2025. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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});
    }
})();